mirror of
https://github.com/hcengineering/platform.git
synced 2024-12-22 02:51:54 +03:00
[UBER-197] Aggregate components properly (#3265)
Signed-off-by: Ruslan Bayandinov <wazsone@ya.ru>
This commit is contained in:
parent
00df8de669
commit
20da8625d2
@ -19743,7 +19743,7 @@ packages:
|
||||
dev: false
|
||||
|
||||
file:projects/model-view.tgz_typescript@4.8.4:
|
||||
resolution: {integrity: sha512-MXdgL834gD1r+h6dZaXCpGCSiHrO2TxGXDHfF951mTThYcbz2X5P950/XcADZgvC4riAXQoJRVCjn1iR1paanA==, tarball: file:projects/model-view.tgz}
|
||||
resolution: {integrity: sha512-nswV/jP/DzqP9yt1TnthbVdziRUYEW3rs/mqG3VTSIwO0Z0Sy0hGbFDr21n8YjxBf756QK3qAItBlJwRq4N3Tg==, tarball: file:projects/model-view.tgz}
|
||||
id: file:projects/model-view.tgz
|
||||
name: '@rush-temp/model-view'
|
||||
version: 0.0.0
|
||||
@ -22070,7 +22070,7 @@ packages:
|
||||
dev: false
|
||||
|
||||
file:projects/tracker-resources.tgz_a1d864769aaf53d09b76fe134ab55e60:
|
||||
resolution: {integrity: sha512-jMfAS0PwGvid0QCtAiVe3bDOLx4PnRdtmz23urZXRSf+s9mtZNpKIGPNDic8wragI9GjAbjJ85bHt8fZaxsSlw==, tarball: file:projects/tracker-resources.tgz}
|
||||
resolution: {integrity: sha512-aahQ2kYGfdS+98z6xeLjeFffJYcfU106RpeslK2u9g9zaAPmdZa6L72ZRFiEDkW1y6n2d3TpDasCCshMw2lEHg==, tarball: file:projects/tracker-resources.tgz}
|
||||
id: file:projects/tracker-resources.tgz
|
||||
name: '@rush-temp/tracker-resources'
|
||||
version: 0.0.0
|
||||
@ -22210,7 +22210,7 @@ packages:
|
||||
dev: false
|
||||
|
||||
file:projects/view-resources.tgz_a1d864769aaf53d09b76fe134ab55e60:
|
||||
resolution: {integrity: sha512-jeJXZ80aZypm/6JrOsW+XJ6z8mzAUf7CJpVfPP8qTYIV2FsTqYBY4aHK0roPkaUVEAd/Ox9oAwKDksoR8EgB/w==, tarball: file:projects/view-resources.tgz}
|
||||
resolution: {integrity: sha512-Taw9Dsr8rIkwsDJBqW4FddqW78UyIqHptCBk+J2c4nJlYnEUYKzbJaszb0N/fOkTN43I72SoxOQlyygIH85sKw==, tarball: file:projects/view-resources.tgz}
|
||||
id: file:projects/view-resources.tgz
|
||||
name: '@rush-temp/view-resources'
|
||||
version: 0.0.0
|
||||
@ -22245,7 +22245,7 @@ packages:
|
||||
dev: false
|
||||
|
||||
file:projects/view.tgz:
|
||||
resolution: {integrity: sha512-BKoOzFnUY/wlXgnnf7GsXZi9v3rZao+9EUVJznob9Dd0PK919rBLq/3XtXduTsX9I5XL63X6aThlzp6BpUyCNQ==, tarball: file:projects/view.tgz}
|
||||
resolution: {integrity: sha512-Q5nEEe6n/qr7hJMMUeD2WSC2/MDdStM4h1maedLCV4OB6t2x0HTmNxm7b8mjROeuTV9lsxUezlMuq2xnBbF7ug==, tarball: file:projects/view.tgz}
|
||||
name: '@rush-temp/view'
|
||||
version: 0.0.0
|
||||
dependencies:
|
||||
@ -22259,6 +22259,7 @@ packages:
|
||||
eslint-plugin-n: 15.5.1_eslint@8.27.0
|
||||
eslint-plugin-promise: 6.1.1_eslint@8.27.0
|
||||
prettier: 2.8.8
|
||||
svelte: 3.55.1
|
||||
typescript: 4.8.4
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
@ -16,10 +16,11 @@
|
||||
import { DOMAIN_MODEL } from '@hcengineering/core'
|
||||
import { Builder, Model } from '@hcengineering/model'
|
||||
import core, { TDoc } from '@hcengineering/model-core'
|
||||
import type { Asset, IntlString, Resource } from '@hcengineering/platform'
|
||||
import { Asset, IntlString, Resource } from '@hcengineering/platform'
|
||||
// Import types to prevent .svelte components to being exposed to type typescript.
|
||||
import { ObjectSearchCategory, ObjectSearchFactory } from '@hcengineering/presentation/src/types'
|
||||
import presentation from './plugin'
|
||||
import { PresentationMiddlewareCreator, PresentationMiddlewareFactory } from '@hcengineering/presentation'
|
||||
|
||||
export { presentationId } from '@hcengineering/presentation/src/plugin'
|
||||
export { default } from './plugin'
|
||||
@ -34,6 +35,11 @@ export class TObjectSearchCategory extends TDoc implements ObjectSearchCategory
|
||||
query!: Resource<ObjectSearchFactory>
|
||||
}
|
||||
|
||||
export function createModel (builder: Builder): void {
|
||||
builder.createModel(TObjectSearchCategory)
|
||||
@Model(presentation.class.PresentationMiddlewareFactory, core.class.Doc, DOMAIN_MODEL)
|
||||
export class TPresentationMiddlewareFactory extends TDoc implements PresentationMiddlewareFactory {
|
||||
createPresentationMiddleware!: Resource<PresentationMiddlewareCreator>
|
||||
}
|
||||
|
||||
export function createModel (builder: Builder): void {
|
||||
builder.createModel(TObjectSearchCategory, TPresentationMiddlewareFactory)
|
||||
}
|
||||
|
@ -14,8 +14,7 @@
|
||||
//
|
||||
|
||||
import activity from '@hcengineering/activity'
|
||||
import type { Employee, EmployeeAccount } from '@hcengineering/contact'
|
||||
import contact from '@hcengineering/contact'
|
||||
import contact, { Employee, EmployeeAccount } from '@hcengineering/contact'
|
||||
import {
|
||||
DOMAIN_MODEL,
|
||||
DateRangeMode,
|
||||
@ -1011,6 +1010,18 @@ export function createModel (builder: Builder): void {
|
||||
inlineEditor: tracker.component.ComponentSelector
|
||||
})
|
||||
|
||||
builder.mixin(tracker.class.Component, core.class.Class, view.mixin.AttributePresenter, {
|
||||
presenter: tracker.component.ComponentRefPresenter
|
||||
})
|
||||
|
||||
builder.mixin(tracker.class.Component, core.class.Class, view.mixin.Aggregation, {
|
||||
createAggregationManager: tracker.aggregation.CreateComponentAggregationManager
|
||||
})
|
||||
|
||||
builder.mixin(tracker.class.Component, core.class.Class, view.mixin.Groupping, {
|
||||
grouppingManager: tracker.aggregation.GrouppingComponentManager
|
||||
})
|
||||
|
||||
builder.mixin(tracker.class.Milestone, core.class.Class, view.mixin.ObjectPresenter, {
|
||||
presenter: tracker.component.MilestonePresenter
|
||||
})
|
||||
|
@ -34,6 +34,7 @@
|
||||
"@hcengineering/model-core": "^0.6.0",
|
||||
"@hcengineering/preference": "^0.6.6",
|
||||
"@hcengineering/model-preference": "^0.6.0",
|
||||
"@hcengineering/model-presentation": "^0.6.0"
|
||||
"@hcengineering/model-presentation": "^0.6.0",
|
||||
"@hcengineering/presentation": "^0.6.2"
|
||||
}
|
||||
}
|
||||
|
@ -13,14 +13,24 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import type { Account, Class, Client, Data, Doc, DocumentQuery, Domain, Ref, Space } from '@hcengineering/core'
|
||||
import { DOMAIN_MODEL } from '@hcengineering/core'
|
||||
import Core, {
|
||||
DOMAIN_MODEL,
|
||||
Account,
|
||||
Class,
|
||||
Client,
|
||||
Data,
|
||||
Doc,
|
||||
DocumentQuery,
|
||||
Domain,
|
||||
Ref,
|
||||
Space
|
||||
} from '@hcengineering/core'
|
||||
import { Builder, Mixin, Model } from '@hcengineering/model'
|
||||
import core, { TClass, TDoc } from '@hcengineering/model-core'
|
||||
import preference, { TPreference } from '@hcengineering/model-preference'
|
||||
import type { Asset, IntlString, Resource, Status } from '@hcengineering/platform'
|
||||
import type { AnyComponent, Location } from '@hcengineering/ui'
|
||||
import type {
|
||||
import { Asset, IntlString, Resource, Status } from '@hcengineering/platform'
|
||||
import { AnyComponent, Location } from '@hcengineering/ui'
|
||||
import {
|
||||
Action,
|
||||
ActionCategory,
|
||||
ActivityAttributePresenter,
|
||||
@ -68,8 +78,13 @@ import type {
|
||||
ViewOptionsModel,
|
||||
Viewlet,
|
||||
ViewletDescriptor,
|
||||
ViewletPreference
|
||||
ViewletPreference,
|
||||
Aggregation,
|
||||
CreateAggregationManagerFunc,
|
||||
GrouppingManagerResource,
|
||||
Groupping
|
||||
} from '@hcengineering/view'
|
||||
import presentation from '@hcengineering/model-presentation'
|
||||
import view from './plugin'
|
||||
|
||||
export { viewId } from '@hcengineering/view'
|
||||
@ -260,6 +275,16 @@ export class TAllValuesFunc extends TClass implements AllValuesFunc {
|
||||
func!: GetAllValuesFunc
|
||||
}
|
||||
|
||||
@Mixin(view.mixin.Groupping, core.class.Class)
|
||||
export class TGroupping extends TClass implements Groupping {
|
||||
grouppingManager!: GrouppingManagerResource
|
||||
}
|
||||
|
||||
@Mixin(view.mixin.Aggregation, core.class.Class)
|
||||
export class TAggregation extends TClass implements Aggregation {
|
||||
createAggregationManager!: CreateAggregationManagerFunc
|
||||
}
|
||||
|
||||
@Model(view.class.ViewletPreference, preference.class.Preference)
|
||||
export class TViewletPreference extends TPreference implements ViewletPreference {
|
||||
attachedTo!: Ref<Viewlet>
|
||||
@ -408,7 +433,9 @@ export function createModel (builder: Builder): void {
|
||||
TArrayEditor,
|
||||
TInlineAttributEditor,
|
||||
TFilteredView,
|
||||
TAllValuesFunc
|
||||
TAllValuesFunc,
|
||||
TAggregation,
|
||||
TGroupping
|
||||
)
|
||||
|
||||
classPresenter(
|
||||
@ -517,6 +544,15 @@ export function createModel (builder: Builder): void {
|
||||
view.viewlet.List
|
||||
)
|
||||
|
||||
builder.createDoc(
|
||||
presentation.class.PresentationMiddlewareFactory,
|
||||
core.space.Model,
|
||||
{
|
||||
createPresentationMiddleware: view.function.CreateDocMiddleware
|
||||
},
|
||||
view.pipeline.PresentationMiddleware
|
||||
)
|
||||
|
||||
createAction(
|
||||
builder,
|
||||
{
|
||||
@ -990,6 +1026,14 @@ export function createModel (builder: Builder): void {
|
||||
builder.mixin(core.class.Status, core.class.Class, view.mixin.AttributePresenter, {
|
||||
presenter: view.component.StatusRefPresenter
|
||||
})
|
||||
|
||||
builder.mixin(Core.class.Status, core.class.Class, view.mixin.Aggregation, {
|
||||
createAggregationManager: view.aggregation.CreateStatusAggregationManager
|
||||
})
|
||||
|
||||
builder.mixin(Core.class.Status, core.class.Class, view.mixin.Groupping, {
|
||||
grouppingManager: view.aggregation.GrouppingStatusManager
|
||||
})
|
||||
}
|
||||
|
||||
export default view
|
||||
|
@ -13,9 +13,11 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import { Ref } from '@hcengineering/core'
|
||||
import { IntlString, mergeIds } from '@hcengineering/platform'
|
||||
import type { AnyComponent } from '@hcengineering/ui'
|
||||
import { AnyComponent } from '@hcengineering/ui'
|
||||
import { FilterFunction, ViewAction, ViewCategoryAction, viewId } from '@hcengineering/view'
|
||||
import { PresentationMiddlewareFactory } from '@hcengineering/presentation'
|
||||
import view from '@hcengineering/view-resources/src/plugin'
|
||||
|
||||
export default mergeIds(viewId, view, {
|
||||
@ -118,5 +120,8 @@ export default mergeIds(viewId, view, {
|
||||
FilterDateNotSpecified: '' as FilterFunction,
|
||||
FilterDateCustom: '' as FilterFunction,
|
||||
ShowEmptyGroups: '' as ViewCategoryAction
|
||||
},
|
||||
pipeline: {
|
||||
PresentationMiddleware: '' as Ref<PresentationMiddlewareFactory>
|
||||
}
|
||||
})
|
||||
|
@ -15,8 +15,8 @@
|
||||
|
||||
import { Asset, IntlString } from '@hcengineering/platform'
|
||||
import { Attribute, Doc, Domain, Ref } from './classes'
|
||||
import { AggregateValue, AggregateValueData, DocManager, IdMap } from './utils'
|
||||
import { WithLookup } from './storage'
|
||||
import { IdMap, toIdMap } from './utils'
|
||||
|
||||
/**
|
||||
* @public
|
||||
@ -60,8 +60,14 @@ export interface Status extends Doc {
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export class StatusValue {
|
||||
constructor (readonly name: string, readonly color: number | undefined, readonly values: WithLookup<Status>[]) {}
|
||||
export class StatusValue extends AggregateValue {
|
||||
constructor (
|
||||
readonly name: string | undefined,
|
||||
readonly color: number | undefined,
|
||||
readonly values: AggregateValueData[]
|
||||
) {
|
||||
super(name, values)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -69,23 +75,20 @@ export class StatusValue {
|
||||
*
|
||||
* Allow to query for status keys/values.
|
||||
*/
|
||||
export class StatusManager {
|
||||
byId: IdMap<WithLookup<Status>>
|
||||
|
||||
constructor (readonly statuses: WithLookup<Status>[]) {
|
||||
this.byId = toIdMap(statuses)
|
||||
export class StatusManager extends DocManager {
|
||||
get (ref: Ref<WithLookup<Status>>): WithLookup<Status> | undefined {
|
||||
return this.getIdMap().get(ref) as WithLookup<Status>
|
||||
}
|
||||
|
||||
get (ref: Ref<Status>): WithLookup<Status> | undefined {
|
||||
return this.byId.get(ref)
|
||||
getDocs (): Array<WithLookup<Status>> {
|
||||
return this.docs as Status[]
|
||||
}
|
||||
|
||||
filter (predicate: (value: WithLookup<Status>) => boolean): WithLookup<Status>[] {
|
||||
return this.statuses.filter(predicate)
|
||||
getIdMap (): IdMap<WithLookup<Status>> {
|
||||
return this.byId as IdMap<WithLookup<Status>>
|
||||
}
|
||||
|
||||
filter (predicate: (value: Status) => boolean): Status[] {
|
||||
return this.getDocs().filter(predicate)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export type CategoryType = number | string | undefined | Ref<Doc> | StatusValue
|
||||
|
@ -13,7 +13,7 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import { Account, AnyAttribute, Class, Doc, DocData, DocIndexState, IndexKind, Obj, Ref } from './classes'
|
||||
import { Account, AnyAttribute, Class, Doc, DocData, DocIndexState, IndexKind, Obj, Ref, Space } from './classes'
|
||||
import core from './component'
|
||||
import { Hierarchy } from './hierarchy'
|
||||
import { FindResult } from './storage'
|
||||
@ -224,3 +224,59 @@ export function fillDefaults<T extends Doc> (
|
||||
}
|
||||
return object
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export class AggregateValueData {
|
||||
constructor (
|
||||
readonly name: string,
|
||||
readonly _id: Ref<Doc>,
|
||||
readonly space: Ref<Space>,
|
||||
readonly rank?: string,
|
||||
readonly category?: Ref<Doc>
|
||||
) {}
|
||||
|
||||
getRank (): string {
|
||||
return this.rank ?? ''
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export class AggregateValue {
|
||||
constructor (readonly name: string | undefined, readonly values: AggregateValueData[]) {}
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export type CategoryType = number | string | undefined | Ref<Doc> | AggregateValue
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export class DocManager {
|
||||
protected readonly byId: IdMap<Doc>
|
||||
|
||||
constructor (protected readonly docs: Doc[]) {
|
||||
this.byId = toIdMap(docs)
|
||||
}
|
||||
|
||||
get (ref: Ref<Doc>): Doc | undefined {
|
||||
return this.byId.get(ref)
|
||||
}
|
||||
|
||||
getDocs (): Doc[] {
|
||||
return this.docs
|
||||
}
|
||||
|
||||
getIdMap (): IdMap<Doc> {
|
||||
return this.byId
|
||||
}
|
||||
|
||||
filter (predicate: (value: Doc) => boolean): Doc[] {
|
||||
return this.docs.filter(predicate)
|
||||
}
|
||||
}
|
||||
|
@ -17,8 +17,6 @@ import core, { PluginConfiguration, SortingOrder } from '@hcengineering/core'
|
||||
import { Plugin, Resource, getResourcePlugin } from '@hcengineering/platform'
|
||||
import { writable } from 'svelte/store'
|
||||
import { createQuery } from '.'
|
||||
import { statusStore } from './status'
|
||||
export { statusStore }
|
||||
|
||||
/**
|
||||
* @public
|
||||
|
@ -49,6 +49,7 @@ export * from './drafts'
|
||||
export { presentationId }
|
||||
export * from './configuration'
|
||||
export * from './context'
|
||||
export * from './pipeline'
|
||||
|
||||
addStringsLoader(presentationId, async (lang: string) => {
|
||||
return await import(`../lang/${lang}.json`)
|
||||
|
@ -12,6 +12,7 @@ import {
|
||||
TxResult,
|
||||
WithLookup
|
||||
} from '@hcengineering/core'
|
||||
import { Resource } from '@hcengineering/platform'
|
||||
|
||||
/**
|
||||
* @public
|
||||
@ -232,3 +233,10 @@ export abstract class BasePresentationMiddleware {
|
||||
return { unsubscribe: () => {} }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export interface PresentationMiddlewareFactory extends Doc {
|
||||
createPresentationMiddleware: Resource<PresentationMiddlewareCreator>
|
||||
}
|
||||
|
@ -18,6 +18,7 @@ import { Class, Ref } from '@hcengineering/core'
|
||||
import type { Asset, IntlString, Metadata, Plugin } from '@hcengineering/platform'
|
||||
import { plugin } from '@hcengineering/platform'
|
||||
import { ObjectSearchCategory } from './types'
|
||||
import { PresentationMiddlewareFactory } from './pipeline'
|
||||
|
||||
/**
|
||||
* @public
|
||||
@ -26,7 +27,8 @@ export const presentationId = 'presentation' as Plugin
|
||||
|
||||
export default plugin(presentationId, {
|
||||
class: {
|
||||
ObjectSearchCategory: '' as Ref<Class<ObjectSearchCategory>>
|
||||
ObjectSearchCategory: '' as Ref<Class<ObjectSearchCategory>>,
|
||||
PresentationMiddlewareFactory: '' as Ref<Class<PresentationMiddlewareFactory>>
|
||||
},
|
||||
string: {
|
||||
Create: '' as IntlString,
|
||||
|
@ -1,316 +0,0 @@
|
||||
//
|
||||
// Copyright © 2023 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import core, {
|
||||
AnyAttribute,
|
||||
Attribute,
|
||||
Class,
|
||||
Client,
|
||||
Doc,
|
||||
DocumentQuery,
|
||||
FindOptions,
|
||||
FindResult,
|
||||
generateId,
|
||||
Hierarchy,
|
||||
Ref,
|
||||
RefTo,
|
||||
SortingOrder,
|
||||
SortingRules,
|
||||
Status,
|
||||
StatusManager,
|
||||
Tx,
|
||||
TxResult
|
||||
} from '@hcengineering/core'
|
||||
import { LiveQuery } from '@hcengineering/query'
|
||||
import { writable } from 'svelte/store'
|
||||
import { BasePresentationMiddleware, PresentationMiddleware } from './pipeline'
|
||||
|
||||
// Issue status live query
|
||||
export const statusStore = writable<StatusManager>(new StatusManager([]))
|
||||
|
||||
interface StatusSubscriber<T extends Doc = Doc> {
|
||||
attributes: Array<Ref<AnyAttribute>>
|
||||
|
||||
_class: Ref<Class<T>>
|
||||
query: DocumentQuery<T>
|
||||
options?: FindOptions<T>
|
||||
|
||||
refresh: () => void
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export class StatusMiddleware extends BasePresentationMiddleware implements PresentationMiddleware {
|
||||
mgr: StatusManager | Promise<StatusManager> | undefined
|
||||
status: Status[] | undefined
|
||||
statusQuery: (() => void) | undefined
|
||||
lq: LiveQuery
|
||||
|
||||
subscribers: Map<string, StatusSubscriber> = new Map()
|
||||
private constructor (client: Client, next?: PresentationMiddleware) {
|
||||
super(client, next)
|
||||
this.lq = new LiveQuery(client)
|
||||
}
|
||||
|
||||
async notifyTx (tx: Tx): Promise<void> {
|
||||
await this.lq.tx(tx)
|
||||
await this.provideNotifyTx(tx)
|
||||
}
|
||||
|
||||
async close (): Promise<void> {
|
||||
this.statusQuery?.()
|
||||
return await this.provideClose()
|
||||
}
|
||||
|
||||
async getManager (): Promise<StatusManager> {
|
||||
if (this.mgr !== undefined) {
|
||||
if (this.mgr instanceof Promise) {
|
||||
this.mgr = await this.mgr
|
||||
}
|
||||
return this.mgr
|
||||
}
|
||||
this.mgr = new Promise<StatusManager>((resolve) => {
|
||||
this.statusQuery = this.lq.query(
|
||||
core.class.Status,
|
||||
{},
|
||||
(res) => {
|
||||
const first = this.status === undefined
|
||||
this.status = res
|
||||
this.mgr = new StatusManager(res)
|
||||
statusStore.set(this.mgr)
|
||||
if (!first) {
|
||||
this.refreshSubscribers()
|
||||
}
|
||||
resolve(this.mgr)
|
||||
},
|
||||
{
|
||||
lookup: {
|
||||
category: core.class.StatusCategory
|
||||
},
|
||||
sort: {
|
||||
rank: SortingOrder.Ascending
|
||||
}
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
return await this.mgr
|
||||
}
|
||||
|
||||
private refreshSubscribers (): void {
|
||||
for (const s of this.subscribers.values()) {
|
||||
// TODO: Do something more smart and track if used status field is changed.
|
||||
s.refresh()
|
||||
}
|
||||
}
|
||||
|
||||
async subscribe<T extends Doc>(
|
||||
_class: Ref<Class<T>>,
|
||||
query: DocumentQuery<T>,
|
||||
options: FindOptions<T> | undefined,
|
||||
refresh: () => void
|
||||
): Promise<{
|
||||
unsubscribe: () => void
|
||||
query?: DocumentQuery<T>
|
||||
options?: FindOptions<T>
|
||||
}> {
|
||||
const ret = await this.provideSubscribe(_class, query, options, refresh)
|
||||
const h = this.client.getHierarchy()
|
||||
|
||||
const id = generateId()
|
||||
const s: StatusSubscriber<T> = {
|
||||
_class,
|
||||
query,
|
||||
refresh,
|
||||
options,
|
||||
attributes: []
|
||||
}
|
||||
const statusFields: Array<Attribute<Status>> = []
|
||||
const allAttrs = h.getAllAttributes(_class)
|
||||
|
||||
const updatedQuery: DocumentQuery<T> = { ...(ret.query ?? query) }
|
||||
const finalOptions = { ...(ret.options ?? options ?? {}) }
|
||||
|
||||
await this.updateQueryOptions<T>(allAttrs, h, statusFields, updatedQuery, finalOptions)
|
||||
|
||||
if (statusFields.length > 0) {
|
||||
this.subscribers.set(id, s)
|
||||
return {
|
||||
unsubscribe: () => {
|
||||
ret.unsubscribe()
|
||||
this.subscribers.delete(id)
|
||||
},
|
||||
query: updatedQuery,
|
||||
options: finalOptions
|
||||
}
|
||||
}
|
||||
return { unsubscribe: (await ret).unsubscribe }
|
||||
}
|
||||
|
||||
static create (client: Client, next?: PresentationMiddleware): StatusMiddleware {
|
||||
return new StatusMiddleware(client, next)
|
||||
}
|
||||
|
||||
async findAll<T extends Doc>(
|
||||
_class: Ref<Class<T>>,
|
||||
query: DocumentQuery<T>,
|
||||
options?: FindOptions<T> | undefined
|
||||
): Promise<FindResult<T>> {
|
||||
const statusFields: Array<Attribute<Status>> = []
|
||||
const h = this.client.getHierarchy()
|
||||
const allAttrs = h.getAllAttributes(_class)
|
||||
const finalOptions = options ?? {}
|
||||
|
||||
await this.updateQueryOptions<T>(allAttrs, h, statusFields, query, finalOptions)
|
||||
|
||||
const result = await this.provideFindAll(_class, query, finalOptions)
|
||||
// We need to add $
|
||||
if (statusFields.length > 0) {
|
||||
// We need to update $lookup for status fields and provide $status group fields.
|
||||
for (const attr of statusFields) {
|
||||
for (const r of result) {
|
||||
const resultDoc = Hierarchy.toDoc(r)
|
||||
if (resultDoc.$lookup === undefined) {
|
||||
resultDoc.$lookup = {}
|
||||
}
|
||||
|
||||
// TODO: Check for mixin?
|
||||
const stateValue = (r as any)[attr.name]
|
||||
const status = (await this.getManager()).byId.get(stateValue)
|
||||
if (status !== undefined) {
|
||||
;(resultDoc.$lookup as any)[attr.name] = status
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
private categorizeStatus (mgr: StatusManager, attr: AnyAttribute, target: Array<Ref<Status>>): Array<Ref<Status>> {
|
||||
for (const sid of [...target]) {
|
||||
const s = mgr.byId.get(sid)
|
||||
if (s !== undefined) {
|
||||
const statuses = mgr.statuses.filter(
|
||||
(it) =>
|
||||
it.ofAttribute === attr._id &&
|
||||
it.name.toLowerCase().trim() === s.name.toLowerCase().trim() &&
|
||||
it._id !== s._id
|
||||
)
|
||||
target.push(...statuses.map((it) => it._id))
|
||||
}
|
||||
}
|
||||
return target.filter((it, idx, arr) => arr.indexOf(it) === idx)
|
||||
}
|
||||
|
||||
private async updateQueryOptions<T extends Doc>(
|
||||
allAttrs: Map<string, AnyAttribute>,
|
||||
h: Hierarchy,
|
||||
statusFields: Array<Attribute<Status>>,
|
||||
query: DocumentQuery<T>,
|
||||
finalOptions: FindOptions<T>
|
||||
): Promise<void> {
|
||||
for (const attr of allAttrs.values()) {
|
||||
try {
|
||||
if (attr.type._class === core.class.RefTo && h.isDerived((attr.type as RefTo<Doc>).to, core.class.Status)) {
|
||||
const mgr = await this.getManager()
|
||||
let target: Array<Ref<Status>> = []
|
||||
let targetNin: Array<Ref<Status>> = []
|
||||
statusFields.push(attr)
|
||||
const v = (query as any)[attr.name]
|
||||
|
||||
if (v != null) {
|
||||
// Only add filter if we have filer inside.
|
||||
if (typeof v === 'string') {
|
||||
target.push(v as Ref<Status>)
|
||||
} else {
|
||||
if (v.$in !== undefined) {
|
||||
target.push(...v.$in)
|
||||
} else if (v.$nin !== undefined) {
|
||||
targetNin.push(...v.$nin)
|
||||
} else if (v.$ne !== undefined) {
|
||||
targetNin.push(v.$ne)
|
||||
}
|
||||
}
|
||||
|
||||
// Find all similar name statues for same attribute name.
|
||||
target = this.categorizeStatus(mgr, attr, target)
|
||||
targetNin = this.categorizeStatus(mgr, attr, targetNin)
|
||||
if (target.length > 0 || targetNin.length > 0) {
|
||||
;(query as any)[attr.name] = {}
|
||||
if (target.length > 0) {
|
||||
;(query as any)[attr.name].$in = target
|
||||
}
|
||||
if (targetNin.length > 0) {
|
||||
;(query as any)[attr.name].$nin = targetNin
|
||||
}
|
||||
}
|
||||
}
|
||||
if (finalOptions.lookup !== undefined) {
|
||||
// Remove lookups by status field
|
||||
if ((finalOptions.lookup as any)[attr.name] !== undefined) {
|
||||
const { [attr.name]: _, ...newLookup } = finalOptions.lookup as any
|
||||
finalOptions.lookup = newLookup
|
||||
}
|
||||
}
|
||||
|
||||
// Update sorting if defined.
|
||||
this.updateCustomSorting<T>(finalOptions, attr, mgr)
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.error(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private updateCustomSorting<T extends Doc>(
|
||||
finalOptions: FindOptions<T>,
|
||||
attr: AnyAttribute,
|
||||
mgr: StatusManager
|
||||
): void {
|
||||
const attrSort = finalOptions.sort?.[attr.name]
|
||||
if (attrSort !== undefined && typeof attrSort !== 'object') {
|
||||
// Fill custom sorting.
|
||||
const statuses = mgr.statuses.filter((it) => it.ofAttribute === attr._id)
|
||||
statuses.sort((a, b) => {
|
||||
let ret = 0
|
||||
if (a.category !== undefined && b.category !== undefined) {
|
||||
ret = (a.$lookup?.category?.order ?? 0) - (b.$lookup?.category?.order ?? 0)
|
||||
}
|
||||
if (ret === 0) {
|
||||
if (a.name.toLowerCase().trim() === b.name.toLowerCase().trim()) {
|
||||
return 0
|
||||
}
|
||||
ret = a.rank.localeCompare(b.rank)
|
||||
}
|
||||
return ret
|
||||
})
|
||||
if (finalOptions.sort === undefined) {
|
||||
finalOptions.sort = {}
|
||||
}
|
||||
|
||||
const rules: SortingRules<any> = {
|
||||
order: attrSort,
|
||||
cases: statuses.map((it, idx) => ({ query: it._id, index: idx })),
|
||||
default: statuses.length + 1
|
||||
}
|
||||
;(finalOptions.sort as any)[attr.name] = rules
|
||||
}
|
||||
}
|
||||
|
||||
async tx (tx: Tx): Promise<TxResult> {
|
||||
return await this.provideTx(tx)
|
||||
}
|
||||
}
|
@ -45,8 +45,6 @@ import { onDestroy } from 'svelte'
|
||||
import { KeyedAttribute } from '..'
|
||||
import { PresentationPipeline, PresentationPipelineImpl } from './pipeline'
|
||||
import plugin from './plugin'
|
||||
import { StatusMiddleware, statusStore } from './status'
|
||||
export { statusStore }
|
||||
|
||||
let liveQuery: LQ
|
||||
let client: TxOperations
|
||||
@ -114,7 +112,10 @@ export async function setClient (_client: Client): Promise<void> {
|
||||
if (pipeline !== undefined) {
|
||||
await pipeline.close()
|
||||
}
|
||||
pipeline = PresentationPipelineImpl.create(_client, [StatusMiddleware.create])
|
||||
const factories = await _client.findAll(plugin.class.PresentationMiddlewareFactory, {})
|
||||
const promises = factories.map(async (it) => await getResource(it.createPresentationMiddleware))
|
||||
const creators = await Promise.all(promises)
|
||||
pipeline = PresentationPipelineImpl.create(_client, creators)
|
||||
|
||||
const needRefresh = liveQuery !== undefined
|
||||
liveQuery = new LQ(pipeline)
|
||||
|
@ -26,7 +26,7 @@
|
||||
} from '@hcengineering/core'
|
||||
import { Item, Kanban as KanbanUI } from '@hcengineering/kanban'
|
||||
import { getResource } from '@hcengineering/platform'
|
||||
import { createQuery, getClient, statusStore, ActionContext } from '@hcengineering/presentation'
|
||||
import { createQuery, getClient, ActionContext } from '@hcengineering/presentation'
|
||||
import { Kanban, SpaceWithStates, Task, TaskGrouping, TaskOrdering } from '@hcengineering/task'
|
||||
import {
|
||||
ColorDefinition,
|
||||
@ -173,7 +173,7 @@
|
||||
viewOptions: ViewOptions,
|
||||
viewOptionsModel: ViewOptionModel[] | undefined
|
||||
) {
|
||||
categories = await getCategories(client, _class, docs, groupByKey, $statusStore, viewlet.descriptor)
|
||||
categories = await getCategories(client, _class, docs, groupByKey, viewlet.descriptor)
|
||||
for (const viewOption of viewOptionsModel ?? []) {
|
||||
if (viewOption.actionTarget !== 'category') continue
|
||||
const categoryFunc = viewOption as CategoryOption
|
||||
@ -191,7 +191,6 @@
|
||||
groupByKey,
|
||||
update,
|
||||
queryId,
|
||||
$statusStore,
|
||||
viewlet.descriptor
|
||||
)
|
||||
if (res !== undefined) {
|
||||
|
@ -14,8 +14,8 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { Ref, StatusValue } from '@hcengineering/core'
|
||||
import { statusStore } from '@hcengineering/presentation'
|
||||
import { Ref, Status, StatusValue } from '@hcengineering/core'
|
||||
import { statusStore } from '@hcengineering/view-resources'
|
||||
import type { DoneState } from '@hcengineering/task'
|
||||
import DoneStatePresenter from './DoneStatePresenter.svelte'
|
||||
import DoneStateEditor from './DoneStateEditor.svelte'
|
||||
@ -23,10 +23,11 @@
|
||||
export let value: Ref<DoneState> | StatusValue
|
||||
export let showTitle: boolean = true
|
||||
export let onChange: ((value: Ref<DoneState>) => void) | undefined = undefined
|
||||
|
||||
$: state = $statusStore.get(typeof value === 'string' ? value : (value?.values?.[0]?._id as Ref<Status>))
|
||||
</script>
|
||||
|
||||
{#if value}
|
||||
{@const state = $statusStore.get(typeof value === 'string' ? value : value.values[0]._id)}
|
||||
{#if onChange !== undefined && state !== undefined}
|
||||
<DoneStateEditor value={state._id} space={state.space} {onChange} kind="link" size="medium" />
|
||||
{:else}
|
||||
|
@ -27,8 +27,6 @@
|
||||
export let value: State | undefined
|
||||
export let shouldShowAvatar = true
|
||||
export let inline: boolean = false
|
||||
export let colorInherit: boolean = false
|
||||
export let accent: boolean = false
|
||||
export let disabled: boolean = false
|
||||
export let oneLine: boolean = false
|
||||
|
||||
|
@ -14,25 +14,23 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { Ref, StatusValue } from '@hcengineering/core'
|
||||
import { statusStore } from '@hcengineering/presentation'
|
||||
import { Ref, Status, StatusValue } from '@hcengineering/core'
|
||||
import type { ButtonSize } from '@hcengineering/ui'
|
||||
import { State } from '@hcengineering/task'
|
||||
import { statusStore } from '@hcengineering/view-resources'
|
||||
import StateEditor from './StateEditor.svelte'
|
||||
import StatePresenter from './StatePresenter.svelte'
|
||||
|
||||
export let value: Ref<State> | StatusValue
|
||||
export let onChange: ((value: Ref<State>) => void) | undefined = undefined
|
||||
export let colorInherit: boolean = false
|
||||
export let accent: boolean = false
|
||||
export let size: ButtonSize = 'medium'
|
||||
$: state = $statusStore.get(typeof value === 'string' ? value : (value?.values?.[0]?._id as Ref<Status>))
|
||||
</script>
|
||||
|
||||
{#if value}
|
||||
{@const state = $statusStore.get(typeof value === 'string' ? value : value.values?.[0]?._id)}
|
||||
{#if onChange !== undefined && state !== undefined}
|
||||
<StateEditor value={state._id} space={state.space} {onChange} kind="link" {size} />
|
||||
{:else}
|
||||
<StatePresenter value={state} {colorInherit} {accent} on:accent-color />
|
||||
<StatePresenter value={state} on:accent-color />
|
||||
{/if}
|
||||
{/if}
|
||||
|
@ -15,9 +15,10 @@
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { Ref } from '@hcengineering/core'
|
||||
import { BreadcrumbsElement, statusStore } from '@hcengineering/presentation'
|
||||
import { BreadcrumbsElement } from '@hcengineering/presentation'
|
||||
import task, { SpaceWithStates, State } from '@hcengineering/task'
|
||||
import { ScrollerBar, getColorNumberByText, getPlatformColor, themeStore } from '@hcengineering/ui'
|
||||
import { statusStore } from '@hcengineering/view-resources'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import type { StatesBarPosition } from '../..'
|
||||
|
||||
|
@ -60,6 +60,7 @@
|
||||
"@hcengineering/chunter-resources": "^0.6.0",
|
||||
"@hcengineering/workbench-resources": "^0.6.1",
|
||||
"@hcengineering/activity-resources": "^0.6.1",
|
||||
"@hcengineering/activity": "^0.6.0"
|
||||
"@hcengineering/activity": "^0.6.0",
|
||||
"@hcengineering/query": "^0.6.5"
|
||||
}
|
||||
}
|
||||
|
238
plugins/tracker-resources/src/component.ts
Normal file
238
plugins/tracker-resources/src/component.ts
Normal file
@ -0,0 +1,238 @@
|
||||
//
|
||||
// Copyright © 2023 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import {
|
||||
AggregateValue,
|
||||
AggregateValueData,
|
||||
AnyAttribute,
|
||||
Attribute,
|
||||
Class,
|
||||
Client,
|
||||
Doc,
|
||||
DocumentQuery,
|
||||
Hierarchy,
|
||||
Ref,
|
||||
SortingOrder,
|
||||
Space,
|
||||
Tx,
|
||||
WithLookup,
|
||||
matchQuery
|
||||
} from '@hcengineering/core'
|
||||
import { LiveQuery } from '@hcengineering/query'
|
||||
import tracker, { Component, ComponentManager } from '@hcengineering/tracker'
|
||||
import { AggregationManager, GrouppingManager } from '@hcengineering/view'
|
||||
import { get, writable } from 'svelte/store'
|
||||
|
||||
export const componentStore = writable<ComponentManager>(new ComponentManager([]))
|
||||
|
||||
/**
|
||||
* @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)
|
||||
}
|
||||
|
||||
async updateLookup (resultDoc: WithLookup<Doc>, attr: Attribute<Doc>): Promise<void> {
|
||||
const value = (resultDoc as any)[attr.name]
|
||||
const doc = (await this.getManager()).getIdMap().get(value)
|
||||
if (doc !== undefined) {
|
||||
;(resultDoc.$lookup as any)[attr.name] = doc
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export const grouppingComponentManager: GrouppingManager = {
|
||||
groupByCategories: groupByComponentCategories,
|
||||
groupValues: groupComponentValues,
|
||||
groupValuesWithEmpty: groupComponentValuesWithEmpty,
|
||||
hasValue: hasComponentValue
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export function groupByComponentCategories (categories: any[]): AggregateValue[] {
|
||||
const mgr = get(componentStore)
|
||||
|
||||
const existingCategories: AggregateValue[] = [new AggregateValue(undefined, [])]
|
||||
const componentMap = new Map<string, AggregateValue>()
|
||||
|
||||
const usedSpaces = new Set<Ref<Space>>()
|
||||
const componentsList: Array<WithLookup<Component>> = []
|
||||
for (const v of categories) {
|
||||
const component = mgr.getIdMap().get(v)
|
||||
if (component !== undefined) {
|
||||
componentsList.push(component)
|
||||
usedSpaces.add(component.space)
|
||||
}
|
||||
}
|
||||
|
||||
for (const component of componentsList) {
|
||||
if (component !== undefined) {
|
||||
let fst = componentMap.get(component.label.toLowerCase().trim())
|
||||
if (fst === undefined) {
|
||||
const components = mgr
|
||||
.getDocs()
|
||||
.filter(
|
||||
(it) =>
|
||||
it.label.toLowerCase().trim() === component.label.toLowerCase().trim() &&
|
||||
(categories.includes(it._id) || usedSpaces.has(it.space))
|
||||
)
|
||||
.sort((a, b) => a.label.localeCompare(b.label))
|
||||
.map((it) => new AggregateValueData(it.label, it._id, it.space))
|
||||
fst = new AggregateValue(component.label, components)
|
||||
componentMap.set(component.label.toLowerCase().trim(), fst)
|
||||
existingCategories.push(fst)
|
||||
}
|
||||
}
|
||||
}
|
||||
return existingCategories
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export function groupComponentValues (val: Doc[], targets: Set<any>): Doc[] {
|
||||
const values = val
|
||||
const result: Doc[] = []
|
||||
const unique = [...new Set(val.map((c) => (c as Component).label.trim().toLocaleLowerCase()))]
|
||||
unique.forEach((label, i) => {
|
||||
let exists = false
|
||||
values.forEach((c) => {
|
||||
if ((c as Component).label.trim().toLocaleLowerCase() === label) {
|
||||
if (!exists) {
|
||||
result[i] = c
|
||||
exists = targets.has(c?._id)
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export function hasComponentValue (value: Doc | undefined | null, values: any[]): boolean {
|
||||
const mgr = get(componentStore)
|
||||
const componentSet = new Set(
|
||||
mgr
|
||||
.filter((it) => it.label.trim().toLocaleLowerCase() === (value as Component)?.label?.trim()?.toLocaleLowerCase())
|
||||
.map((it) => it._id)
|
||||
)
|
||||
return values.some((it) => componentSet.has(it))
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export function groupComponentValuesWithEmpty (
|
||||
hierarchy: Hierarchy,
|
||||
_class: Ref<Class<Doc>>,
|
||||
key: string,
|
||||
query: DocumentQuery<Doc> | undefined
|
||||
): Array<Ref<Doc>> {
|
||||
const mgr = get(componentStore)
|
||||
// We do not need extensions for all status categories.
|
||||
let componentsList = 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
|
||||
}
|
||||
componentsList = matchQuery<Doc>(componentsList, resQuery, _class, hierarchy) as unknown as Array<
|
||||
WithLookup<Component>
|
||||
>
|
||||
}
|
||||
return componentsList.map((it) => it._id)
|
||||
}
|
@ -14,14 +14,14 @@
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { TxUpdateDoc } from '@hcengineering/core'
|
||||
import { statusStore } from '@hcengineering/presentation'
|
||||
import { Issue } from '@hcengineering/tracker'
|
||||
import { statusStore } from '@hcengineering/view-resources'
|
||||
import IssueStatusIcon from '../issues/IssueStatusIcon.svelte'
|
||||
|
||||
export let tx: TxUpdateDoc<Issue>
|
||||
$: value = tx.operations.status
|
||||
|
||||
$: status = value && $statusStore.byId.get(value)
|
||||
$: status = value && $statusStore.getIdMap().get(value)
|
||||
</script>
|
||||
|
||||
<div class="icon">
|
||||
|
@ -19,8 +19,9 @@
|
||||
import tracker from '../../plugin'
|
||||
import view from '@hcengineering/view'
|
||||
import { DocNavLink } from '@hcengineering/view-resources'
|
||||
import { translate } from '@hcengineering/platform'
|
||||
|
||||
export let value: WithLookup<Component>
|
||||
export let value: WithLookup<Component> | undefined
|
||||
export let shouldShowAvatar = true
|
||||
export let onClick: (() => void) | undefined = undefined
|
||||
export let disabled = false
|
||||
@ -28,24 +29,32 @@
|
||||
export let accent: boolean = false
|
||||
export let noUnderline = false
|
||||
export let kind: 'list' | undefined = undefined
|
||||
|
||||
let label: string
|
||||
|
||||
$: if (value !== undefined) {
|
||||
label = value.label
|
||||
} else {
|
||||
translate(tracker.string.NoComponent, {})
|
||||
.then((r) => {
|
||||
label = r
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err)
|
||||
})
|
||||
}
|
||||
$: disabled = disabled || value === undefined
|
||||
</script>
|
||||
|
||||
{#if value}
|
||||
<DocNavLink object={value} {onClick} {disabled} {noUnderline} {inline} {accent} component={view.component.EditDoc}>
|
||||
<span class="flex-presenter" class:inline-presenter={inline} class:list={kind === 'list'}>
|
||||
{#if !inline && shouldShowAvatar}
|
||||
<div class="icon" use:tooltip={{ label: tracker.string.Component }}>
|
||||
<Icon icon={tracker.icon.Component} size={'small'} />
|
||||
</div>
|
||||
{/if}
|
||||
<span
|
||||
title={value.label}
|
||||
class="label nowrap"
|
||||
class:no-underline={disabled || noUnderline}
|
||||
class:fs-bold={accent}
|
||||
>
|
||||
{value.label}
|
||||
</span>
|
||||
<DocNavLink object={value} {onClick} {disabled} {noUnderline} {inline} {accent} component={view.component.EditDoc}>
|
||||
<span class="flex-presenter" class:inline-presenter={inline} class:list={kind === 'list'}>
|
||||
{#if !inline && shouldShowAvatar}
|
||||
<div class="icon" use:tooltip={{ label: tracker.string.Component }}>
|
||||
<Icon icon={tracker.icon.Component} size={'small'} />
|
||||
</div>
|
||||
{/if}
|
||||
<span title={label} class="label nowrap" class:no-underline={disabled || noUnderline} class:fs-bold={accent}>
|
||||
{label}
|
||||
</span>
|
||||
</DocNavLink>
|
||||
{/if}
|
||||
</span>
|
||||
</DocNavLink>
|
||||
|
@ -0,0 +1,30 @@
|
||||
<!--
|
||||
// Copyright © 2023 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { AggregateValue, Ref } from '@hcengineering/core'
|
||||
import ComponentPresenter from './ComponentPresenter.svelte'
|
||||
import { Component } from '@hcengineering/tracker'
|
||||
|
||||
import { componentStore } from '../../component'
|
||||
|
||||
export let value: Ref<Component> | AggregateValue | undefined
|
||||
export let kind: 'list' | undefined = undefined
|
||||
|
||||
$: componentValue = $componentStore.get(
|
||||
typeof value === 'string' ? value : (value?.values?.[0]?._id as Ref<Component>)
|
||||
)
|
||||
</script>
|
||||
|
||||
<ComponentPresenter value={componentValue} {kind} on:accent-color />
|
@ -3,8 +3,8 @@
|
||||
import { createQuery } from '@hcengineering/presentation'
|
||||
import { Issue, IssueStatus } from '@hcengineering/tracker'
|
||||
import { Label, ticker, Row } from '@hcengineering/ui'
|
||||
import { statusStore } from '@hcengineering/view-resources'
|
||||
import tracker from '../../plugin'
|
||||
import { statusStore } from '@hcengineering/presentation'
|
||||
import Duration from './Duration.svelte'
|
||||
import StatusPresenter from './StatusPresenter.svelte'
|
||||
|
||||
@ -84,7 +84,7 @@
|
||||
displaySt = result
|
||||
}
|
||||
|
||||
$: updateStatus(txes, $statusStore.byId, $ticker)
|
||||
$: updateStatus(txes, $statusStore.getIdMap(), $ticker)
|
||||
</script>
|
||||
|
||||
<Row>
|
||||
|
@ -14,9 +14,10 @@
|
||||
-->
|
||||
<script lang="ts">
|
||||
import core, { StatusCategory, WithLookup } from '@hcengineering/core'
|
||||
import { getClient, statusStore } from '@hcengineering/presentation'
|
||||
import { getClient } from '@hcengineering/presentation'
|
||||
import { IssueStatus } from '@hcengineering/tracker'
|
||||
import { IconSize } from '@hcengineering/ui'
|
||||
import { statusStore } from '@hcengineering/view-resources'
|
||||
import tracker from '../../plugin'
|
||||
import StatusIcon from '../icons/StatusIcon.svelte'
|
||||
|
||||
|
@ -29,7 +29,7 @@
|
||||
import { Item, Kanban } from '@hcengineering/kanban'
|
||||
import notification from '@hcengineering/notification'
|
||||
import { getResource } from '@hcengineering/platform'
|
||||
import { createQuery, getClient, statusStore, ActionContext } from '@hcengineering/presentation'
|
||||
import { createQuery, getClient, ActionContext } from '@hcengineering/presentation'
|
||||
import tags from '@hcengineering/tags'
|
||||
import { Issue, IssuesGrouping, IssuesOrdering, Project } from '@hcengineering/tracker'
|
||||
import {
|
||||
@ -197,7 +197,7 @@
|
||||
viewOptions: ViewOptions,
|
||||
viewOptionsModel: ViewOptionModel[] | undefined
|
||||
) {
|
||||
categories = await getCategories(client, _class, docs, groupByKey, $statusStore, viewlet.descriptor)
|
||||
categories = await getCategories(client, _class, docs, groupByKey, viewlet.descriptor)
|
||||
for (const viewOption of viewOptionsModel ?? []) {
|
||||
if (viewOption.actionTarget !== 'category') continue
|
||||
const categoryFunc = viewOption as CategoryOption
|
||||
@ -214,7 +214,6 @@
|
||||
groupByKey,
|
||||
update,
|
||||
queryId,
|
||||
$statusStore,
|
||||
viewlet.descriptor
|
||||
)
|
||||
if (res !== undefined) {
|
||||
|
@ -15,10 +15,11 @@
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { DocumentUpdate, Ref } from '@hcengineering/core'
|
||||
import { SpaceSelect, createQuery, getClient, statusStore } from '@hcengineering/presentation'
|
||||
import { SpaceSelect, createQuery, getClient } from '@hcengineering/presentation'
|
||||
import { Component, Issue, Project } from '@hcengineering/tracker'
|
||||
import ui, { Button, Label, Spinner } from '@hcengineering/ui'
|
||||
import view from '@hcengineering/view'
|
||||
import { statusStore } from '@hcengineering/view-resources'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import tracker from '../../plugin'
|
||||
import { collectIssues, findTargetStatus, moveIssueToSpace } from '../../utils'
|
||||
|
@ -14,10 +14,19 @@
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { AttachedData, Ref, StatusManager, WithLookup } from '@hcengineering/core'
|
||||
import { getClient, statusStore } from '@hcengineering/presentation'
|
||||
import { getClient } from '@hcengineering/presentation'
|
||||
import { Issue, IssueDraft, IssueStatus, Project } from '@hcengineering/tracker'
|
||||
import type { ButtonKind, ButtonSize, IconSize } from '@hcengineering/ui'
|
||||
import { Button, SelectPopup, TooltipAlignment, eventToHTMLElement, showPopup } from '@hcengineering/ui'
|
||||
import {
|
||||
ButtonKind,
|
||||
ButtonSize,
|
||||
IconSize,
|
||||
Button,
|
||||
SelectPopup,
|
||||
TooltipAlignment,
|
||||
eventToHTMLElement,
|
||||
showPopup
|
||||
} from '@hcengineering/ui'
|
||||
import { statusStore } from '@hcengineering/view-resources'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import tracker from '../../plugin'
|
||||
import IssueStatusIcon from './IssueStatusIcon.svelte'
|
||||
|
@ -15,8 +15,9 @@
|
||||
<script lang="ts">
|
||||
import { IssueStatus } from '@hcengineering/tracker'
|
||||
import IssueStatusIcon from './IssueStatusIcon.svelte'
|
||||
import { createQuery, statusStore } from '@hcengineering/presentation'
|
||||
import { createQuery } from '@hcengineering/presentation'
|
||||
import core, { IdMap, Ref, StatusCategory, toIdMap } from '@hcengineering/core'
|
||||
import { statusStore } from '@hcengineering/view-resources'
|
||||
|
||||
export let value: Ref<IssueStatus>[]
|
||||
|
||||
@ -41,7 +42,7 @@
|
||||
})
|
||||
}
|
||||
|
||||
$: statuses = sort(value.map((p) => $statusStore.byId.get(p)) as IssueStatus[], categories)
|
||||
$: statuses = sort(value.map((p) => $statusStore.getIdMap().get(p)) as IssueStatus[], categories)
|
||||
</script>
|
||||
|
||||
<div class="flex-presenter flex-gap-1-5">
|
||||
|
@ -14,7 +14,7 @@
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { Ref, Status, StatusValue } from '@hcengineering/core'
|
||||
import { statusStore } from '@hcengineering/presentation'
|
||||
import { statusStore } from '@hcengineering/view-resources'
|
||||
import StatusPresenter from './StatusPresenter.svelte'
|
||||
|
||||
export let value: Ref<Status> | StatusValue | undefined
|
||||
@ -22,15 +22,10 @@
|
||||
export let kind: 'list-header' | undefined = undefined
|
||||
export let colorInherit: boolean = false
|
||||
export let accent: boolean = false
|
||||
|
||||
$: statusValue = $statusStore.get(typeof value === 'string' ? value : (value?.values?.[0]?._id as Ref<Status>))
|
||||
</script>
|
||||
|
||||
{#if value}
|
||||
<StatusPresenter
|
||||
value={$statusStore.get(typeof value === 'string' ? value : value.values?.[0]?._id)}
|
||||
{size}
|
||||
{kind}
|
||||
{colorInherit}
|
||||
{accent}
|
||||
on:accent-color
|
||||
/>
|
||||
<StatusPresenter value={statusValue} {size} {kind} {colorInherit} {accent} on:accent-color />
|
||||
{/if}
|
||||
|
@ -14,7 +14,7 @@
|
||||
-->
|
||||
<script lang="ts">
|
||||
import core, { SortingOrder, WithLookup } from '@hcengineering/core'
|
||||
import { createQuery, statusStore } from '@hcengineering/presentation'
|
||||
import { createQuery } from '@hcengineering/presentation'
|
||||
import { Issue, IssueStatus } from '@hcengineering/tracker'
|
||||
import {
|
||||
Icon,
|
||||
@ -29,7 +29,7 @@
|
||||
themeStore,
|
||||
tooltip
|
||||
} from '@hcengineering/ui'
|
||||
import { ListSelectionProvider } from '@hcengineering/view-resources'
|
||||
import { ListSelectionProvider, statusStore } from '@hcengineering/view-resources'
|
||||
import { getIssueId, issueLinkFragmentProvider } from '../../../issues'
|
||||
import tracker from '../../../plugin'
|
||||
import IssueStatusIcon from '../IssueStatusIcon.svelte'
|
||||
@ -118,7 +118,7 @@
|
||||
subIssuesQeury.unsubscribe()
|
||||
}
|
||||
|
||||
$: parentStatus = parentIssue ? $statusStore.byId.get(parentIssue.status) : undefined
|
||||
$: parentStatus = parentIssue ? $statusStore.getIdMap().get(parentIssue.status) : undefined
|
||||
</script>
|
||||
|
||||
{#if parentIssue}
|
||||
|
@ -14,7 +14,7 @@
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { Ref, SortingOrder, WithLookup } from '@hcengineering/core'
|
||||
import { createQuery, statusStore } from '@hcengineering/presentation'
|
||||
import { createQuery } from '@hcengineering/presentation'
|
||||
import { Issue, Project } from '@hcengineering/tracker'
|
||||
import {
|
||||
Button,
|
||||
@ -26,6 +26,7 @@
|
||||
showPanel,
|
||||
showPopup
|
||||
} from '@hcengineering/ui'
|
||||
import { statusStore } from '@hcengineering/view-resources'
|
||||
import { getIssueId } from '../../../issues'
|
||||
import tracker from '../../../plugin'
|
||||
import { subIssueListProvider } from '../../../utils'
|
||||
@ -77,7 +78,8 @@
|
||||
}
|
||||
|
||||
$: if (subIssues) {
|
||||
const doneStatuses = $statusStore.statuses
|
||||
const doneStatuses = $statusStore
|
||||
.getDocs()
|
||||
.filter(
|
||||
(s) =>
|
||||
s.category === tracker.issueStatusCategory.Completed || s.category === tracker.issueStatusCategory.Canceled
|
||||
|
@ -14,9 +14,10 @@
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { Ref, Status } from '@hcengineering/core'
|
||||
import { getClient, statusStore } from '@hcengineering/presentation'
|
||||
import { getClient } from '@hcengineering/presentation'
|
||||
import { Issue, Project } from '@hcengineering/tracker'
|
||||
import { Button, Label } from '@hcengineering/ui'
|
||||
import { statusStore } from '@hcengineering/view-resources'
|
||||
import tracker from '../../../plugin'
|
||||
import { findTargetStatus } from '../../../utils'
|
||||
import StatusRefPresenter from '../StatusRefPresenter.svelte'
|
||||
|
@ -14,9 +14,9 @@
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { DocumentUpdate, Ref } from '@hcengineering/core'
|
||||
import { statusStore } from '@hcengineering/presentation'
|
||||
import { Issue, Project } from '@hcengineering/tracker'
|
||||
import { Label } from '@hcengineering/ui'
|
||||
import { statusStore } from '@hcengineering/view-resources'
|
||||
import tracker from '../../../plugin'
|
||||
import { findTargetStatus, issueToAttachedData } from '../../../utils'
|
||||
import StatusEditor from '../StatusEditor.svelte'
|
||||
|
@ -14,7 +14,7 @@
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { Issue, Project } from '@hcengineering/tracker'
|
||||
import { statusStore } from '@hcengineering/presentation'
|
||||
import { statusStore } from '@hcengineering/view-resources'
|
||||
|
||||
import IssueStatusIcon from '../IssueStatusIcon.svelte'
|
||||
import { getIssueId } from '../../../issues'
|
||||
@ -23,7 +23,7 @@
|
||||
export let issue: Issue
|
||||
export let size: 'small' | 'medium' | 'large' = 'small'
|
||||
|
||||
$: status = $statusStore.byId.get(issue.status)
|
||||
$: status = $statusStore.getIdMap().get(issue.status)
|
||||
$: huge = size === 'medium' || size === 'large'
|
||||
$: text = project ? `${getIssueId(project, issue)} ${issue.title}` : issue.title
|
||||
</script>
|
||||
|
@ -26,9 +26,9 @@
|
||||
showPanel,
|
||||
showPopup
|
||||
} from '@hcengineering/ui'
|
||||
import { statusStore } from '@hcengineering/view-resources'
|
||||
import tracker from '../../../plugin'
|
||||
import { subIssueListProvider } from '../../../utils'
|
||||
import { statusStore } from '@hcengineering/presentation'
|
||||
import RelatedIssuePresenter from './RelatedIssuePresenter.svelte'
|
||||
|
||||
export let object: WithLookup<Doc & { related: number }> | undefined
|
||||
@ -69,7 +69,8 @@
|
||||
}
|
||||
|
||||
$: if (subIssues) {
|
||||
const doneStatuses = $statusStore.statuses
|
||||
const doneStatuses = $statusStore
|
||||
.getDocs()
|
||||
.filter((s) => s.category === tracker.issueStatusCategory.Completed)
|
||||
.map((p) => p._id)
|
||||
countComplete = subIssues.filter((si) => doneStatuses.includes(si.status)).length
|
||||
|
@ -17,7 +17,7 @@
|
||||
import { Issue } from '@hcengineering/tracker'
|
||||
import { floorFractionDigits, Label } from '@hcengineering/ui'
|
||||
import tracker from '../../plugin'
|
||||
import { statusStore } from '@hcengineering/presentation'
|
||||
import { statusStore } from '@hcengineering/view-resources'
|
||||
import EstimationProgressCircle from '../issues/timereport/EstimationProgressCircle.svelte'
|
||||
import TimePresenter from '../issues/timereport/TimePresenter.svelte'
|
||||
import { FixedColumn } from '@hcengineering/view-resources'
|
||||
@ -30,13 +30,13 @@
|
||||
$: noParents = docs?.filter((it) => !ids.has(it.attachedTo as Ref<Issue>))
|
||||
|
||||
$: rootNoBacklogIssues = noParents?.filter(
|
||||
(it) => $statusStore.byId.get(it.status)?.category !== tracker.issueStatusCategory.Backlog
|
||||
(it) => $statusStore.getIdMap().get(it.status)?.category !== tracker.issueStatusCategory.Backlog
|
||||
)
|
||||
|
||||
$: totalEstimation = floorFractionDigits(
|
||||
(rootNoBacklogIssues ?? [{ estimation: 0, childInfo: [] } as unknown as Issue])
|
||||
.map((it) => {
|
||||
const cat = $statusStore.byId.get(it.status)?.category
|
||||
const cat = $statusStore.getIdMap().get(it.status)?.category
|
||||
|
||||
let retEst = it.estimation
|
||||
if (it.childInfo?.length > 0) {
|
||||
|
@ -2,11 +2,11 @@
|
||||
import { Ref } from '@hcengineering/core'
|
||||
import { Issue, IssueStatus, Project } from '@hcengineering/tracker'
|
||||
import { Button, Label, SelectPopup, eventToHTMLElement, showPopup } from '@hcengineering/ui'
|
||||
import presentation, { getClient, statusStore } from '@hcengineering/presentation'
|
||||
import presentation, { getClient } from '@hcengineering/presentation'
|
||||
import tracker from '../../plugin'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import IssueStatusIcon from '../issues/IssueStatusIcon.svelte'
|
||||
import { StatusPresenter } from '@hcengineering/view-resources'
|
||||
import { StatusPresenter, statusStore } from '@hcengineering/view-resources'
|
||||
|
||||
export let projectId: Ref<Project>
|
||||
export let issues: Issue[]
|
||||
@ -17,10 +17,10 @@
|
||||
const client = getClient()
|
||||
|
||||
let newStatus: IssueStatus =
|
||||
$statusStore.statuses.find(
|
||||
(s) => s._id !== status._id && s.category === status.category && s.space === projectId
|
||||
) ??
|
||||
$statusStore.statuses.find((s) => s._id !== status._id && s.space === projectId) ??
|
||||
$statusStore
|
||||
.getDocs()
|
||||
.find((s) => s._id !== status._id && s.category === status.category && s.space === projectId) ??
|
||||
$statusStore.getDocs().find((s) => s._id !== status._id && s.space === projectId) ??
|
||||
status
|
||||
|
||||
async function remove () {
|
||||
@ -54,7 +54,7 @@
|
||||
SelectPopup,
|
||||
{ value: statusesInfo, placeholder: tracker.string.SetStatus, searchable: true },
|
||||
eventToHTMLElement(event),
|
||||
(val) => (newStatus = $statusStore.byId.get(val) ?? newStatus)
|
||||
(val) => (newStatus = $statusStore.getIdMap().get(val) ?? newStatus)
|
||||
)
|
||||
}
|
||||
</script>
|
||||
|
@ -28,13 +28,13 @@
|
||||
Scroller,
|
||||
showPopup
|
||||
} from '@hcengineering/ui'
|
||||
import { statusStore } from '@hcengineering/presentation'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import { flip } from 'svelte/animate'
|
||||
import tracker from '../../plugin'
|
||||
import StatusEditor from './StatusEditor.svelte'
|
||||
import StatusPresenter from './StatusPresenter.svelte'
|
||||
import RemoveStatus from './RemoveStatus.svelte'
|
||||
import { statusStore } from '@hcengineering/view-resources'
|
||||
|
||||
export let projectId: Ref<Project>
|
||||
export let projectClass: Ref<Class<Project>>
|
||||
@ -88,7 +88,7 @@
|
||||
async function editStatus () {
|
||||
if (statusCategories && editingStatus?.name && editingStatus?.category && '_id' in editingStatus) {
|
||||
const statusId = '_id' in editingStatus ? editingStatus._id : undefined
|
||||
const status = statusId && $statusStore.byId.get(statusId)
|
||||
const status = statusId && $statusStore.getIdMap().get(statusId)
|
||||
|
||||
if (!status) {
|
||||
return
|
||||
@ -238,7 +238,7 @@
|
||||
|
||||
$: projectQuery.query(projectClass, { _id: projectId }, (result) => ([project] = result), { limit: 1 })
|
||||
$: updateStatusCategories()
|
||||
$: projectStatuses = $statusStore.statuses.filter((status) => status.space === projectId)
|
||||
$: projectStatuses = $statusStore.getDocs().filter((status) => status.space === projectId)
|
||||
</script>
|
||||
|
||||
<Panel isHeader={false} isAside={false} on:fullsize on:close={() => dispatch('close')}>
|
||||
|
@ -29,6 +29,7 @@ import { Issue, Project, Scrum, ScrumRecord, Milestone } from '@hcengineering/tr
|
||||
import { showPopup } from '@hcengineering/ui'
|
||||
import ComponentEditor from './components/components/ComponentEditor.svelte'
|
||||
import ComponentPresenter from './components/components/ComponentPresenter.svelte'
|
||||
import ComponentRefPresenter from './components/components/ComponentRefPresenter.svelte'
|
||||
import Components from './components/components/Components.svelte'
|
||||
import ComponentTitlePresenter from './components/components/ComponentTitlePresenter.svelte'
|
||||
import EditComponent from './components/components/EditComponent.svelte'
|
||||
@ -132,6 +133,7 @@ import ProjectSpacePresenter from './components/projects/ProjectSpacePresenter.s
|
||||
import IssueStatistics from './components/milestones/IssueStatistics.svelte'
|
||||
import MilestoneRefPresenter from './components/milestones/MilestoneRefPresenter.svelte'
|
||||
import MilestoneFilter from './components/milestones/MilestoneFilter.svelte'
|
||||
import { ComponentAggregationManager, grouppingComponentManager } from './component'
|
||||
|
||||
export { default as SubIssueList } from './components/issues/edit/SubIssueList.svelte'
|
||||
|
||||
@ -385,6 +387,7 @@ export default async (): Promise<Resources> => ({
|
||||
Components,
|
||||
IssuePresenter,
|
||||
ComponentPresenter,
|
||||
ComponentRefPresenter,
|
||||
ComponentTitlePresenter,
|
||||
TitlePresenter,
|
||||
ModificationDatePresenter,
|
||||
@ -473,5 +476,9 @@ export default async (): Promise<Resources> => ({
|
||||
},
|
||||
resolver: {
|
||||
Location: resolveLocation
|
||||
},
|
||||
aggregation: {
|
||||
CreateComponentAggregationManager: ComponentAggregationManager.create,
|
||||
GrouppingComponentManager: grouppingComponentManager
|
||||
}
|
||||
})
|
||||
|
@ -17,7 +17,15 @@ import type { Asset, IntlString, Metadata, Resource } from '@hcengineering/platf
|
||||
import { mergeIds } from '@hcengineering/platform'
|
||||
import { IssueDraft } from '@hcengineering/tracker'
|
||||
import { AnyComponent, Location } from '@hcengineering/ui'
|
||||
import { GetAllValuesFunc, SortFunc, Viewlet, ViewletDescriptor, ViewQueryAction } from '@hcengineering/view'
|
||||
import {
|
||||
CreateAggregationManagerFunc,
|
||||
GetAllValuesFunc,
|
||||
GrouppingManagerResource,
|
||||
SortFunc,
|
||||
Viewlet,
|
||||
ViewletDescriptor,
|
||||
ViewQueryAction
|
||||
} from '@hcengineering/view'
|
||||
import tracker, { trackerId } from '../../tracker/lib'
|
||||
|
||||
export default mergeIds(trackerId, tracker, {
|
||||
@ -314,6 +322,7 @@ export default mergeIds(trackerId, tracker, {
|
||||
IssuePresenter: '' as AnyComponent,
|
||||
ComponentTitlePresenter: '' as AnyComponent,
|
||||
ComponentPresenter: '' as AnyComponent,
|
||||
ComponentRefPresenter: '' as AnyComponent,
|
||||
TitlePresenter: '' as AnyComponent,
|
||||
ModificationDatePresenter: '' as AnyComponent,
|
||||
PriorityPresenter: '' as AnyComponent,
|
||||
@ -382,5 +391,9 @@ export default mergeIds(trackerId, tracker, {
|
||||
GetAllPriority: '' as GetAllValuesFunc,
|
||||
GetAllComponents: '' as GetAllValuesFunc,
|
||||
GetAllMilestones: '' as GetAllValuesFunc
|
||||
},
|
||||
aggregation: {
|
||||
CreateComponentAggregationManager: '' as CreateAggregationManagerFunc,
|
||||
GrouppingComponentManager: '' as GrouppingManagerResource
|
||||
}
|
||||
})
|
||||
|
@ -61,7 +61,7 @@ import {
|
||||
import { ViewletDescriptor } from '@hcengineering/view'
|
||||
import { CategoryQuery, groupBy, ListSelectionProvider, SelectDirection } from '@hcengineering/view-resources'
|
||||
import tracker from './plugin'
|
||||
import { defaultMilestoneStatuses, defaultPriorities } from './types'
|
||||
import { defaultPriorities, defaultMilestoneStatuses } from './types'
|
||||
|
||||
export * from './types'
|
||||
|
||||
@ -296,7 +296,7 @@ export async function issueStatusSort (
|
||||
listIssueKanbanStatusOrder.indexOf(a.values[0].category as Ref<StatusCategory>) -
|
||||
listIssueKanbanStatusOrder.indexOf(b.values[0].category as Ref<StatusCategory>)
|
||||
if (res === 0) {
|
||||
return a.values[0].rank.localeCompare(b.values[0].rank)
|
||||
return a.values[0].getRank().localeCompare(b.values[0].getRank())
|
||||
}
|
||||
return res
|
||||
})
|
||||
@ -306,7 +306,7 @@ export async function issueStatusSort (
|
||||
listIssueStatusOrder.indexOf(a.values[0].category as Ref<StatusCategory>) -
|
||||
listIssueStatusOrder.indexOf(b.values[0].category as Ref<StatusCategory>)
|
||||
if (res === 0) {
|
||||
return a.values[0].rank.localeCompare(b.values[0].rank)
|
||||
return a.values[0].getRank().localeCompare(b.values[0].getRank())
|
||||
}
|
||||
return res
|
||||
})
|
||||
|
@ -14,11 +14,13 @@
|
||||
//
|
||||
|
||||
import { Employee, EmployeeAccount } from '@hcengineering/contact'
|
||||
import type {
|
||||
import {
|
||||
AttachedDoc,
|
||||
Attribute,
|
||||
Class,
|
||||
Doc,
|
||||
DocManager,
|
||||
IdMap,
|
||||
Markup,
|
||||
Ref,
|
||||
RelatedDocument,
|
||||
@ -26,7 +28,8 @@ import type {
|
||||
Status,
|
||||
StatusCategory,
|
||||
Timestamp,
|
||||
Type
|
||||
Type,
|
||||
WithLookup
|
||||
} from '@hcengineering/core'
|
||||
import type { Asset, IntlString, Plugin, Resource } from '@hcengineering/platform'
|
||||
import { plugin } from '@hcengineering/platform'
|
||||
@ -310,6 +313,29 @@ export interface Component extends Doc {
|
||||
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
|
||||
*/
|
||||
|
@ -46,6 +46,7 @@
|
||||
"@hcengineering/presentation": "^0.6.2",
|
||||
"@hcengineering/setting": "^0.6.7",
|
||||
"@hcengineering/text-editor": "^0.6.0",
|
||||
"@hcengineering/query": "^0.6.5",
|
||||
"fast-equals": "^2.0.3"
|
||||
}
|
||||
}
|
||||
|
@ -25,7 +25,7 @@ import core, {
|
||||
Ref
|
||||
} from '@hcengineering/core'
|
||||
import { getResource } from '@hcengineering/platform'
|
||||
import type { Action, ViewAction, ViewActionInput, ViewContextType } from '@hcengineering/view'
|
||||
import { Action, ViewAction, ViewActionInput, ViewContextType } from '@hcengineering/view'
|
||||
import view from './plugin'
|
||||
import { FocusSelection } from './selection'
|
||||
|
||||
|
@ -19,7 +19,7 @@
|
||||
import view from '../plugin'
|
||||
import { getObjectLinkFragment } from '../utils'
|
||||
|
||||
export let object: Doc
|
||||
export let object: Doc | undefined
|
||||
export let disabled = false
|
||||
export let onClick: ((event: MouseEvent) => void) | undefined = undefined
|
||||
export let noUnderline = false
|
||||
|
@ -13,9 +13,9 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import core, { Doc, FindResult, getObjectValue, Ref, RefTo, SortingOrder, Space, Status } from '@hcengineering/core'
|
||||
import { translate } from '@hcengineering/platform'
|
||||
import presentation, { getClient, statusStore } from '@hcengineering/presentation'
|
||||
import core, { Doc, FindResult, getObjectValue, Ref, RefTo, SortingOrder, Space } from '@hcengineering/core'
|
||||
import { getResource, translate } from '@hcengineering/platform'
|
||||
import presentation, { getClient } from '@hcengineering/presentation'
|
||||
import ui, {
|
||||
addNotification,
|
||||
deviceOptionsStore,
|
||||
@ -27,7 +27,7 @@
|
||||
Loading,
|
||||
resizeObserver
|
||||
} from '@hcengineering/ui'
|
||||
import { Filter } from '@hcengineering/view'
|
||||
import { Filter, GrouppingManager } from '@hcengineering/view'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import { FILTER_DEBOUNCE_MS, sortFilterValues } from '../../filter'
|
||||
import view from '../../plugin'
|
||||
@ -49,32 +49,18 @@
|
||||
|
||||
let values: (Doc | undefined | null)[] = []
|
||||
let objectsPromise: Promise<FindResult<Doc>> | undefined
|
||||
let grouppingManager: GrouppingManager | undefined
|
||||
|
||||
const targets = new Set<any>()
|
||||
$: targetClass = (filter.key.attribute.type as RefTo<Doc>).to
|
||||
$: clazz = hierarchy.getClass(targetClass)
|
||||
|
||||
$: isStatus = client.getHierarchy().isDerived(targetClass, core.class.Status) ?? false
|
||||
$: mixin = hierarchy.classHierarchyMixin(targetClass, view.mixin.Groupping)
|
||||
$: if (mixin?.grouppingManager !== undefined) {
|
||||
getResource(mixin.grouppingManager).then((mgr) => (grouppingManager = mgr))
|
||||
}
|
||||
|
||||
let filterUpdateTimeout: number | undefined
|
||||
|
||||
const groupValues = (val: Status[]): Doc[] => {
|
||||
const statuses = val
|
||||
const result: Doc[] = []
|
||||
const unique = [...new Set(val.map((v) => v.name.trim().toLocaleLowerCase()))]
|
||||
unique.forEach((label, i) => {
|
||||
let exists = false
|
||||
statuses.forEach((state) => {
|
||||
if (state.name.trim().toLocaleLowerCase() === label) {
|
||||
if (!exists) {
|
||||
result[i] = state
|
||||
exists = targets.has(state?._id)
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
return result
|
||||
}
|
||||
|
||||
async function getValues (search: string): Promise<void> {
|
||||
if (objectsPromise) {
|
||||
await objectsPromise
|
||||
@ -108,8 +94,8 @@
|
||||
const options = clazz.sortingKey !== undefined ? { sort: { [clazz.sortingKey]: SortingOrder.Ascending } } : {}
|
||||
objectsPromise = client.findAll(targetClass, resultQuery, options)
|
||||
values = await objectsPromise
|
||||
if (isStatus) {
|
||||
values = groupValues(values as Status[])
|
||||
if (grouppingManager !== undefined) {
|
||||
values = grouppingManager.groupValues(values as Doc[], targets)
|
||||
}
|
||||
if (targets.has(undefined)) {
|
||||
values.unshift(undefined)
|
||||
@ -130,13 +116,8 @@
|
||||
}
|
||||
|
||||
function isSelected (value: Doc | undefined | null, values: any[]): boolean {
|
||||
if (isStatus) {
|
||||
const statusSet = new Set(
|
||||
$statusStore
|
||||
.filter((it) => it.name.trim().toLocaleLowerCase() === (value as Status)?.name?.trim()?.toLocaleLowerCase())
|
||||
.map((it) => it._id)
|
||||
)
|
||||
return values.some((it) => statusSet.has(it))
|
||||
if (grouppingManager !== undefined) {
|
||||
return grouppingManager.hasValue(value, values)
|
||||
}
|
||||
return values.includes(value?._id ?? value)
|
||||
}
|
||||
|
@ -15,7 +15,7 @@
|
||||
<script lang="ts">
|
||||
import { CategoryType, Class, Doc, DocumentQuery, generateId, Lookup, Ref, Space } from '@hcengineering/core'
|
||||
import { getResource, IntlString } from '@hcengineering/platform'
|
||||
import { getClient, statusStore } from '@hcengineering/presentation'
|
||||
import { getClient } from '@hcengineering/presentation'
|
||||
import { AnyComponent, AnySvelteComponent } from '@hcengineering/ui'
|
||||
import { AttributeModel, BuildModelKey, CategoryOption, ViewOptionModel, ViewOptions } from '@hcengineering/view'
|
||||
import { createEventDispatcher, onDestroy, SvelteComponentTyped } from 'svelte'
|
||||
@ -84,14 +84,14 @@
|
||||
viewOptions: ViewOptions,
|
||||
viewOptionsModel: ViewOptionModel[] | undefined
|
||||
) {
|
||||
categories = await getCategories(client, _class, docs, groupByKey, $statusStore)
|
||||
categories = await getCategories(client, _class, docs, groupByKey)
|
||||
if (level === 0) {
|
||||
for (const viewOption of viewOptionsModel ?? []) {
|
||||
if (viewOption.actionTarget !== 'category') continue
|
||||
const categoryFunc = viewOption as CategoryOption
|
||||
if (viewOptions[viewOption.key] ?? viewOption.defaultValue) {
|
||||
const f = await getResource(categoryFunc.action)
|
||||
const res = hierarchy.clone(await f(_class, query, groupByKey, update, queryId, $statusStore))
|
||||
const res = hierarchy.clone(await f(_class, query, groupByKey, update, queryId))
|
||||
if (res !== undefined) {
|
||||
categories = concatCategories(res, categories)
|
||||
return
|
||||
|
@ -13,7 +13,7 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { Class, Doc, DocumentUpdate, Lookup, PrimitiveType, Ref, Space, StatusValue } from '@hcengineering/core'
|
||||
import { AggregateValue, Class, Doc, DocumentUpdate, Lookup, PrimitiveType, Ref, Space } from '@hcengineering/core'
|
||||
import { IntlString } from '@hcengineering/platform'
|
||||
import { getClient } from '@hcengineering/presentation'
|
||||
import { DocWithRank, calcRank } from '@hcengineering/task'
|
||||
@ -34,7 +34,7 @@
|
||||
import ListHeader from './ListHeader.svelte'
|
||||
import ListItem from './ListItem.svelte'
|
||||
|
||||
export let category: PrimitiveType | StatusValue
|
||||
export let category: PrimitiveType | AggregateValue
|
||||
export let headerComponent: AttributeModel | undefined
|
||||
export let singleCat: boolean
|
||||
export let oneCat: boolean
|
||||
|
@ -13,7 +13,7 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { Doc, Ref, Space } from '@hcengineering/core'
|
||||
import { AggregateValue, Doc, PrimitiveType, Ref, Space } from '@hcengineering/core'
|
||||
import { IntlString } from '@hcengineering/platform'
|
||||
import ui, {
|
||||
ActionIcon,
|
||||
@ -40,7 +40,7 @@
|
||||
import { noCategory } from '../../viewOptions'
|
||||
|
||||
export let groupByKey: string
|
||||
export let category: any
|
||||
export let category: PrimitiveType | AggregateValue
|
||||
export let headerComponent: AttributeModel | undefined
|
||||
export let space: Ref<Space> | undefined
|
||||
export let limited: number
|
||||
|
@ -16,15 +16,17 @@
|
||||
import { Ref, Status, StatusValue } from '@hcengineering/core'
|
||||
import { Asset } from '@hcengineering/platform'
|
||||
import { AnySvelteComponent } from '@hcengineering/ui'
|
||||
import { statusStore } from '@hcengineering/presentation'
|
||||
|
||||
import StatusPresenter from './StatusPresenter.svelte'
|
||||
import { statusStore } from '../../status'
|
||||
|
||||
export let value: Ref<Status> | StatusValue | undefined
|
||||
export let size: 'small' | 'medium' = 'medium'
|
||||
export let icon: Asset | AnySvelteComponent | undefined = undefined
|
||||
|
||||
$: statusValue = $statusStore.get(typeof value === 'string' ? value : (value?.values?.[0]?._id as Ref<Status>))
|
||||
</script>
|
||||
|
||||
{#if value}
|
||||
<StatusPresenter value={$statusStore.get(typeof value === 'string' ? value : value.values?.[0]._id)} {size} {icon} />
|
||||
<StatusPresenter value={statusValue} {size} {icon} />
|
||||
{/if}
|
||||
|
@ -104,6 +104,8 @@ import {
|
||||
import { IndexedDocumentPreview } from '@hcengineering/presentation'
|
||||
import { statusSort } from './utils'
|
||||
import { showEmptyGroups } from './viewOptions'
|
||||
import { AggregationMiddleware } from './middleware'
|
||||
import { grouppingStatusManager, StatusAggregationManager } from './status'
|
||||
export { getActions, invokeAction } from './actions'
|
||||
export { default as ActionHandler } from './components/ActionHandler.svelte'
|
||||
export { default as AddSavedView } from './components/filter/AddSavedView.svelte'
|
||||
@ -129,6 +131,8 @@ export { default as ParentsNavigator } from './components/ParentsNavigator.svelt
|
||||
export * from './filter'
|
||||
export * from './selection'
|
||||
export * from './utils'
|
||||
export * from './status'
|
||||
export * from './middleware'
|
||||
export {
|
||||
buildModel,
|
||||
getActiveViewletId,
|
||||
@ -257,6 +261,11 @@ export default async (): Promise<Resources> => ({
|
||||
FilterDateMonth: dateMonth,
|
||||
FilterDateNextMonth: dateNextMonth,
|
||||
FilterDateNotSpecified: dateNotSpecified,
|
||||
FilterDateCustom: dateCustom
|
||||
FilterDateCustom: dateCustom,
|
||||
CreateDocMiddleware: AggregationMiddleware.create
|
||||
},
|
||||
aggregation: {
|
||||
CreateStatusAggregationManager: StatusAggregationManager.create,
|
||||
GrouppingStatusManager: grouppingStatusManager
|
||||
}
|
||||
})
|
||||
|
234
plugins/view-resources/src/middleware.ts
Normal file
234
plugins/view-resources/src/middleware.ts
Normal file
@ -0,0 +1,234 @@
|
||||
import core, {
|
||||
Doc,
|
||||
Ref,
|
||||
AnyAttribute,
|
||||
Class,
|
||||
DocumentQuery,
|
||||
FindOptions,
|
||||
Client,
|
||||
Tx,
|
||||
TxResult,
|
||||
FindResult,
|
||||
Attribute,
|
||||
Hierarchy,
|
||||
RefTo,
|
||||
generateId
|
||||
} from '@hcengineering/core'
|
||||
import { BasePresentationMiddleware, PresentationMiddleware } from '@hcengineering/presentation'
|
||||
import view, { AggregationManager } from '@hcengineering/view'
|
||||
import { getResource } from '@hcengineering/platform'
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export interface DocSubScriber<T extends Doc = Doc> {
|
||||
attributes: Array<Ref<AnyAttribute>>
|
||||
|
||||
_class: Ref<Class<T>>
|
||||
query: DocumentQuery<T>
|
||||
options?: FindOptions<T>
|
||||
|
||||
refresh: () => void
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export class AggregationMiddleware extends BasePresentationMiddleware implements PresentationMiddleware {
|
||||
mgrs: Map<Ref<Class<Doc>>, AggregationManager> = new Map<Ref<Class<Doc>>, AggregationManager>()
|
||||
docs: Doc[] | undefined
|
||||
|
||||
subscribers: Map<string, DocSubScriber> = new Map()
|
||||
private constructor (client: Client, next?: PresentationMiddleware) {
|
||||
super(client, next)
|
||||
}
|
||||
|
||||
static create (client: Client, next?: PresentationMiddleware): AggregationMiddleware {
|
||||
return new AggregationMiddleware(client, next)
|
||||
}
|
||||
|
||||
async notifyTx (tx: Tx): Promise<void> {
|
||||
const promises: Array<Promise<void>> = []
|
||||
for (const [, value] of this.mgrs) {
|
||||
promises.push(value.notifyTx(tx))
|
||||
}
|
||||
await Promise.all(promises)
|
||||
await this.provideNotifyTx(tx)
|
||||
}
|
||||
|
||||
async close (): Promise<void> {
|
||||
this.mgrs.forEach((mgr) => mgr.close())
|
||||
return await this.provideClose()
|
||||
}
|
||||
|
||||
async tx (tx: Tx): Promise<TxResult> {
|
||||
return await this.provideTx(tx)
|
||||
}
|
||||
|
||||
private refreshSubscribers (): void {
|
||||
for (const s of this.subscribers.values()) {
|
||||
// TODO: Do something more smart and track if used component field is changed.
|
||||
s.refresh()
|
||||
}
|
||||
}
|
||||
|
||||
async subscribe<T extends Doc>(
|
||||
_class: Ref<Class<T>>,
|
||||
query: DocumentQuery<T>,
|
||||
options: FindOptions<T> | undefined,
|
||||
refresh: () => void
|
||||
): Promise<{
|
||||
unsubscribe: () => void
|
||||
query?: DocumentQuery<T>
|
||||
options?: FindOptions<T>
|
||||
}> {
|
||||
const ret = await this.provideSubscribe(_class, query, options, refresh)
|
||||
const h = this.client.getHierarchy()
|
||||
|
||||
const id = generateId()
|
||||
const s: DocSubScriber<T> = {
|
||||
_class,
|
||||
query,
|
||||
refresh,
|
||||
options,
|
||||
attributes: []
|
||||
}
|
||||
const statusFields: Array<Attribute<Doc>> = []
|
||||
const allAttrs = h.getAllAttributes(_class)
|
||||
|
||||
const updatedQuery: DocumentQuery<T> = { ...(ret.query ?? query) }
|
||||
const finalOptions = { ...(ret.options ?? options ?? {}) }
|
||||
|
||||
await this.updateQueryOptions<T>(allAttrs, h, statusFields, updatedQuery, finalOptions)
|
||||
|
||||
if (statusFields.length > 0) {
|
||||
this.subscribers.set(id, s)
|
||||
return {
|
||||
unsubscribe: () => {
|
||||
ret.unsubscribe()
|
||||
this.subscribers.delete(id)
|
||||
},
|
||||
query: updatedQuery,
|
||||
options: finalOptions
|
||||
}
|
||||
}
|
||||
return { unsubscribe: (await ret).unsubscribe }
|
||||
}
|
||||
|
||||
private async getAggregationManager (_class: Ref<Class<Doc>>): Promise<AggregationManager | undefined> {
|
||||
let mgr = this.mgrs.get(_class)
|
||||
|
||||
if (mgr === undefined) {
|
||||
const h = this.client.getHierarchy()
|
||||
const mixin = h.classHierarchyMixin(_class, view.mixin.Aggregation)
|
||||
if (mixin?.createAggregationManager !== undefined) {
|
||||
const f = await getResource(mixin.createAggregationManager)
|
||||
mgr = f(this.client, this.refreshSubscribers)
|
||||
this.mgrs.set(_class, mgr)
|
||||
}
|
||||
}
|
||||
|
||||
return mgr
|
||||
}
|
||||
|
||||
async findAll<T extends Doc>(
|
||||
_class: Ref<Class<T>>,
|
||||
query: DocumentQuery<T>,
|
||||
options?: FindOptions<T> | undefined
|
||||
): Promise<FindResult<T>> {
|
||||
const docFields: Array<Attribute<Doc>> = []
|
||||
const h = this.client.getHierarchy()
|
||||
const allAttrs = h.getAllAttributes(_class)
|
||||
const finalOptions = options ?? {}
|
||||
|
||||
await this.updateQueryOptions<T>(allAttrs, h, docFields, query, finalOptions)
|
||||
|
||||
const result = await this.provideFindAll(_class, query, finalOptions)
|
||||
// We need to add $
|
||||
if (docFields.length > 0) {
|
||||
// We need to update $lookup for doc fields and provide $doc group fields.
|
||||
for (const attr of docFields) {
|
||||
for (const r of result) {
|
||||
const resultDoc = Hierarchy.toDoc(r)
|
||||
if (resultDoc.$lookup === undefined) {
|
||||
resultDoc.$lookup = {}
|
||||
}
|
||||
|
||||
const mgr = await this.getAggregationManager((attr.type as RefTo<Doc>).to)
|
||||
if (mgr !== undefined) {
|
||||
await mgr.updateLookup(resultDoc, attr)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
private async updateQueryOptions<T extends Doc>(
|
||||
allAttrs: Map<string, AnyAttribute>,
|
||||
h: Hierarchy,
|
||||
docFields: Array<Attribute<Doc>>,
|
||||
query: DocumentQuery<T>,
|
||||
finalOptions: FindOptions<T>
|
||||
): Promise<void> {
|
||||
for (const attr of allAttrs.values()) {
|
||||
try {
|
||||
if (attr.type._class !== core.class.RefTo) {
|
||||
continue
|
||||
}
|
||||
const mgr = await this.getAggregationManager((attr.type as RefTo<Doc>).to)
|
||||
if (mgr === undefined) {
|
||||
continue
|
||||
}
|
||||
if (h.isDerived((attr.type as RefTo<Doc>).to, mgr.getAttrClass())) {
|
||||
let target: Array<Ref<Doc>> = []
|
||||
let targetNin: Array<Ref<Doc>> = []
|
||||
docFields.push(attr)
|
||||
const v = (query as any)[attr.name]
|
||||
|
||||
if (v != null) {
|
||||
// Only add filter if we have filer inside.
|
||||
if (typeof v === 'string') {
|
||||
target.push(v as Ref<Doc>)
|
||||
} else {
|
||||
if (v.$in !== undefined) {
|
||||
target.push(...v.$in)
|
||||
} else if (v.$nin !== undefined) {
|
||||
targetNin.push(...v.$nin)
|
||||
} else if (v.$ne !== undefined) {
|
||||
targetNin.push(v.$ne)
|
||||
}
|
||||
}
|
||||
|
||||
// Find all similar name statues for same attribute name.
|
||||
target = await mgr.categorize(target, attr)
|
||||
targetNin = await mgr.categorize(targetNin, attr)
|
||||
if (target.length > 0 || targetNin.length > 0) {
|
||||
;(query as any)[attr.name] = {}
|
||||
if (target.length > 0) {
|
||||
;(query as any)[attr.name].$in = target
|
||||
}
|
||||
if (targetNin.length > 0) {
|
||||
;(query as any)[attr.name].$nin = targetNin
|
||||
}
|
||||
}
|
||||
}
|
||||
if (finalOptions.lookup !== undefined) {
|
||||
// Remove lookups by status field
|
||||
if ((finalOptions.lookup as any)[attr.name] !== undefined) {
|
||||
const { [attr.name]: _, ...newLookup } = finalOptions.lookup as any
|
||||
finalOptions.lookup = newLookup
|
||||
}
|
||||
}
|
||||
|
||||
// Update sorting if defined.
|
||||
if (mgr.updateSorting !== undefined) {
|
||||
await mgr.updateSorting(finalOptions, attr)
|
||||
}
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.error(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -14,9 +14,10 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import { IntlString, mergeIds } from '@hcengineering/platform'
|
||||
import { IntlString, Resource, mergeIds } from '@hcengineering/platform'
|
||||
import { PresentationMiddlewareCreator } from '@hcengineering/presentation'
|
||||
import { AnyComponent } from '@hcengineering/ui'
|
||||
import view, { SortFunc, viewId } from '@hcengineering/view'
|
||||
import view, { CreateAggregationManagerFunc, GrouppingManagerResource, SortFunc, viewId } from '@hcengineering/view'
|
||||
|
||||
export default mergeIds(viewId, view, {
|
||||
component: {
|
||||
@ -94,6 +95,11 @@ export default mergeIds(viewId, view, {
|
||||
Show: '' as IntlString
|
||||
},
|
||||
function: {
|
||||
StatusSort: '' as SortFunc
|
||||
StatusSort: '' as SortFunc,
|
||||
CreateDocMiddleware: '' as Resource<PresentationMiddlewareCreator>
|
||||
},
|
||||
aggregation: {
|
||||
CreateStatusAggregationManager: '' as CreateAggregationManagerFunc,
|
||||
GrouppingStatusManager: '' as GrouppingManagerResource
|
||||
}
|
||||
})
|
||||
|
283
plugins/view-resources/src/status.ts
Normal file
283
plugins/view-resources/src/status.ts
Normal file
@ -0,0 +1,283 @@
|
||||
//
|
||||
// Copyright © 2023 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import core, {
|
||||
AggregateValue,
|
||||
AggregateValueData,
|
||||
AnyAttribute,
|
||||
Attribute,
|
||||
Class,
|
||||
Client,
|
||||
Doc,
|
||||
DocumentQuery,
|
||||
FindOptions,
|
||||
Hierarchy,
|
||||
Ref,
|
||||
SortingOrder,
|
||||
SortingRules,
|
||||
Space,
|
||||
Status,
|
||||
StatusManager,
|
||||
StatusValue,
|
||||
Tx,
|
||||
WithLookup,
|
||||
matchQuery
|
||||
} from '@hcengineering/core'
|
||||
import { LiveQuery } from '@hcengineering/query'
|
||||
import { AggregationManager, GrouppingManager } from '@hcengineering/view'
|
||||
import { get, writable } from 'svelte/store'
|
||||
|
||||
// Issue status live query
|
||||
export const statusStore = writable<StatusManager>(new StatusManager([]))
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export class StatusAggregationManager implements AggregationManager {
|
||||
docs: Doc[] | undefined
|
||||
mgr: StatusManager | Promise<StatusManager> | 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): StatusAggregationManager {
|
||||
return new StatusAggregationManager(client, lqCallback)
|
||||
}
|
||||
|
||||
private async getManager (): Promise<StatusManager> {
|
||||
if (this.mgr !== undefined) {
|
||||
if (this.mgr instanceof Promise) {
|
||||
this.mgr = await this.mgr
|
||||
}
|
||||
return this.mgr
|
||||
}
|
||||
this.mgr = new Promise<StatusManager>((resolve) => {
|
||||
this.query = this.lq.query(
|
||||
core.class.Status,
|
||||
{},
|
||||
(res) => {
|
||||
const first = this.docs === undefined
|
||||
this.docs = res
|
||||
this.mgr = new StatusManager(res)
|
||||
statusStore.set(this.mgr)
|
||||
if (!first) {
|
||||
this.lqCallback()
|
||||
}
|
||||
resolve(this.mgr)
|
||||
},
|
||||
{
|
||||
lookup: {
|
||||
category: core.class.StatusCategory
|
||||
},
|
||||
sort: {
|
||||
rank: 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 core.class.Status
|
||||
}
|
||||
|
||||
async categorize (target: Array<Ref<Doc>>, attr: AnyAttribute): Promise<Array<Ref<Doc>>> {
|
||||
const mgr = await this.getManager()
|
||||
for (const sid of [...target]) {
|
||||
const s = mgr.getIdMap().get(sid as Ref<Status>) as WithLookup<Status>
|
||||
if (s !== undefined) {
|
||||
let statuses = mgr.getDocs()
|
||||
statuses = statuses.filter(
|
||||
(it) =>
|
||||
it.ofAttribute === attr._id &&
|
||||
it.name.toLowerCase().trim() === s.name.toLowerCase().trim() &&
|
||||
it._id !== s._id
|
||||
)
|
||||
target.push(...statuses.map((it) => it._id))
|
||||
}
|
||||
}
|
||||
return target.filter((it, idx, arr) => arr.indexOf(it) === idx)
|
||||
}
|
||||
|
||||
async updateLookup (resultDoc: WithLookup<Doc>, attr: Attribute<Doc>): Promise<void> {
|
||||
const value = (resultDoc as any)[attr.name]
|
||||
const doc = (await this.getManager()).getIdMap().get(value)
|
||||
if (doc !== undefined) {
|
||||
;(resultDoc.$lookup as any)[attr.name] = doc
|
||||
}
|
||||
}
|
||||
|
||||
async updateSorting<T extends Doc>(finalOptions: FindOptions<T>, attr: AnyAttribute): Promise<void> {
|
||||
const attrSort = finalOptions.sort?.[attr.name]
|
||||
if (attrSort !== undefined && typeof attrSort !== 'object') {
|
||||
// Fill custom sorting.
|
||||
let statuses = (await this.getManager()).getDocs()
|
||||
statuses = statuses.filter((it) => it.ofAttribute === attr._id)
|
||||
statuses.sort((a, b) => {
|
||||
let ret = 0
|
||||
if (a.category !== undefined && b.category !== undefined) {
|
||||
ret = (a.$lookup?.category?.order ?? 0) - (b.$lookup?.category?.order ?? 0)
|
||||
}
|
||||
if (ret === 0) {
|
||||
if (a.name.toLowerCase().trim() === b.name.toLowerCase().trim()) {
|
||||
return 0
|
||||
}
|
||||
ret = a.rank.localeCompare(b.rank)
|
||||
}
|
||||
return ret
|
||||
})
|
||||
if (finalOptions.sort === undefined) {
|
||||
finalOptions.sort = {}
|
||||
}
|
||||
|
||||
const rules: SortingRules<any> = {
|
||||
order: attrSort,
|
||||
cases: statuses.map((it, idx) => ({ query: it._id, index: idx })),
|
||||
default: statuses.length + 1
|
||||
}
|
||||
;(finalOptions.sort as any)[attr.name] = rules
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export const grouppingStatusManager: GrouppingManager = {
|
||||
groupByCategories: groupByStatusCategories,
|
||||
groupValues: groupStatusValues,
|
||||
groupValuesWithEmpty: groupStatusValuesWithEmpty,
|
||||
hasValue: hasStatusValue
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export function groupByStatusCategories (categories: any[]): AggregateValue[] {
|
||||
const mgr = get(statusStore)
|
||||
|
||||
const existingCategories: AggregateValue[] = []
|
||||
const statusMap = new Map<string, AggregateValue>()
|
||||
|
||||
const usedSpaces = new Set<Ref<Space>>()
|
||||
const statusesList: Array<WithLookup<Status>> = []
|
||||
for (const v of categories) {
|
||||
const status = mgr.getIdMap().get(v)
|
||||
if (status !== undefined) {
|
||||
statusesList.push(status)
|
||||
usedSpaces.add(status.space)
|
||||
}
|
||||
}
|
||||
|
||||
for (const status of statusesList) {
|
||||
if (status !== undefined) {
|
||||
let fst = statusMap.get(status.name.toLowerCase().trim())
|
||||
if (fst === undefined) {
|
||||
const statuses = mgr
|
||||
.getDocs()
|
||||
.filter(
|
||||
(it) =>
|
||||
it.ofAttribute === status.ofAttribute &&
|
||||
it.name.toLowerCase().trim() === status.name.toLowerCase().trim() &&
|
||||
(categories.includes(it._id) || usedSpaces.has(it.space))
|
||||
)
|
||||
.sort((a, b) => a.rank.localeCompare(b.rank))
|
||||
.map((it) => new AggregateValueData(it.name, it._id, it.space, it.rank, it.category))
|
||||
fst = new StatusValue(status.name, status.color, statuses)
|
||||
statusMap.set(status.name.toLowerCase().trim(), fst)
|
||||
existingCategories.push(fst)
|
||||
}
|
||||
}
|
||||
}
|
||||
return existingCategories
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export function groupStatusValues (val: Doc[], targets: Set<any>): Doc[] {
|
||||
const values = val
|
||||
const result: Doc[] = []
|
||||
const unique = [...new Set(val.map((v) => (v as Status).name.trim().toLocaleLowerCase()))]
|
||||
unique.forEach((label, i) => {
|
||||
let exists = false
|
||||
values.forEach((value) => {
|
||||
if ((value as Status).name.trim().toLocaleLowerCase() === label) {
|
||||
if (!exists) {
|
||||
result[i] = value
|
||||
exists = targets.has(value?._id)
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export function hasStatusValue (value: Doc | undefined | null, values: any[]): boolean {
|
||||
const mgr = get(statusStore)
|
||||
const statusSet = new Set(
|
||||
mgr
|
||||
.filter((it) => it.name.trim().toLocaleLowerCase() === (value as Status)?.name?.trim()?.toLocaleLowerCase())
|
||||
.map((it) => it._id)
|
||||
)
|
||||
return values.some((it) => statusSet.has(it))
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export function groupStatusValuesWithEmpty (
|
||||
hierarchy: Hierarchy,
|
||||
_class: Ref<Class<Doc>>,
|
||||
key: string,
|
||||
query: DocumentQuery<Doc> | undefined
|
||||
): Array<Ref<Doc>> {
|
||||
const mgr = get(statusStore)
|
||||
const attr = hierarchy.getAttribute(_class, key)
|
||||
// We do not need extensions for all status categories.
|
||||
let statusList = mgr.filter((it) => {
|
||||
return it.ofAttribute === attr._id
|
||||
})
|
||||
if (query !== undefined) {
|
||||
const { [key]: st, space } = query
|
||||
const resQuery: DocumentQuery<Doc> = {}
|
||||
if (space !== undefined) {
|
||||
resQuery.space = space
|
||||
}
|
||||
if (st !== undefined) {
|
||||
resQuery._id = st
|
||||
}
|
||||
statusList = matchQuery<Doc>(statusList, resQuery, _class, hierarchy) as unknown as Array<WithLookup<Status>>
|
||||
}
|
||||
return statusList.map((it) => it._id)
|
||||
}
|
@ -17,6 +17,7 @@
|
||||
import core, {
|
||||
AccountRole,
|
||||
AttachedDoc,
|
||||
AggregateValue,
|
||||
CategoryType,
|
||||
Class,
|
||||
Client,
|
||||
@ -34,10 +35,7 @@ import core, {
|
||||
ReverseLookups,
|
||||
Space,
|
||||
Status,
|
||||
StatusManager,
|
||||
StatusValue,
|
||||
TxOperations,
|
||||
WithLookup
|
||||
TxOperations
|
||||
} from '@hcengineering/core'
|
||||
import type { IntlString } from '@hcengineering/platform'
|
||||
import { getResource } from '@hcengineering/platform'
|
||||
@ -59,6 +57,7 @@ import {
|
||||
} from '@hcengineering/ui'
|
||||
import type { BuildModelOptions, Viewlet, ViewletDescriptor } from '@hcengineering/view'
|
||||
import view, { AttributeModel, BuildModelKey } from '@hcengineering/view'
|
||||
|
||||
import { get, writable } from 'svelte/store'
|
||||
import plugin from './plugin'
|
||||
import { noCategory } from './viewOptions'
|
||||
@ -597,7 +596,7 @@ export function groupBy<T extends Doc> (docs: T[], key: string, categories?: Cat
|
||||
*/
|
||||
export function getGroupByValues<T extends Doc> (groupByDocs: Record<any, T[]>, category: CategoryType): T[] {
|
||||
if (typeof category === 'object') {
|
||||
return groupByDocs[category.name] ?? []
|
||||
return groupByDocs[category.name as any] ?? []
|
||||
} else {
|
||||
return groupByDocs[category as any] ?? []
|
||||
}
|
||||
@ -612,7 +611,7 @@ export function setGroupByValues (
|
||||
docs: Doc[]
|
||||
): void {
|
||||
if (typeof category === 'object') {
|
||||
groupByDocs[category.name] = docs
|
||||
groupByDocs[category.name as any] = docs
|
||||
} else if (category !== undefined) {
|
||||
groupByDocs[category] = docs
|
||||
}
|
||||
@ -626,8 +625,7 @@ export async function groupByCategory (
|
||||
client: TxOperations,
|
||||
_class: Ref<Class<Doc>>,
|
||||
key: string,
|
||||
categories: any[],
|
||||
mgr: StatusManager,
|
||||
categories: CategoryType[],
|
||||
viewletDescriptorId?: Ref<ViewletDescriptor>
|
||||
): Promise<CategoryType[]> {
|
||||
const h = client.getHierarchy()
|
||||
@ -636,13 +634,12 @@ export async function groupByCategory (
|
||||
if (key === noCategory) return [undefined]
|
||||
|
||||
const attrClass = getAttributePresenterClass(h, attr).attrClass
|
||||
|
||||
const isStatusField = h.isDerived(attrClass, core.class.Status)
|
||||
|
||||
const mixin = h.classHierarchyMixin(attrClass, view.mixin.Groupping)
|
||||
let existingCategories: any[] = []
|
||||
|
||||
if (isStatusField) {
|
||||
existingCategories = await groupByStatusCategories(h, attrClass, categories, mgr, viewletDescriptorId)
|
||||
if (mixin?.grouppingManager !== undefined) {
|
||||
const grouppingManager = await getResource(mixin.grouppingManager)
|
||||
existingCategories = grouppingManager.groupByCategories(categories)
|
||||
} else {
|
||||
const valueSet = new Set<any>()
|
||||
for (const v of categories) {
|
||||
@ -655,56 +652,11 @@ export async function groupByCategory (
|
||||
return await sortCategories(h, attrClass, existingCategories, viewletDescriptorId)
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export async function groupByStatusCategories (
|
||||
hierarchy: Hierarchy,
|
||||
attrClass: Ref<Class<Doc>>,
|
||||
categories: any[],
|
||||
mgr: StatusManager,
|
||||
viewletDescriptorId?: Ref<ViewletDescriptor>
|
||||
): Promise<StatusValue[]> {
|
||||
const existingCategories: StatusValue[] = []
|
||||
const statusMap = new Map<string, StatusValue>()
|
||||
|
||||
const usedSpaces = new Set<Ref<Space>>()
|
||||
const statusesList: Array<WithLookup<Status>> = []
|
||||
for (const v of categories) {
|
||||
const status = mgr.byId.get(v)
|
||||
if (status !== undefined) {
|
||||
statusesList.push(status)
|
||||
usedSpaces.add(status.space)
|
||||
}
|
||||
}
|
||||
|
||||
for (const status of statusesList) {
|
||||
if (status !== undefined) {
|
||||
let fst = statusMap.get(status.name.toLowerCase().trim())
|
||||
if (fst === undefined) {
|
||||
const statuses = mgr.statuses
|
||||
.filter(
|
||||
(it) =>
|
||||
it.ofAttribute === status.ofAttribute &&
|
||||
it.name.toLowerCase().trim() === status.name.toLowerCase().trim() &&
|
||||
(categories.includes(it._id) || usedSpaces.has(it.space))
|
||||
)
|
||||
.sort((a, b) => a.rank.localeCompare(b.rank))
|
||||
fst = new StatusValue(status.name, status.color, statuses)
|
||||
statusMap.set(status.name.toLowerCase().trim(), fst)
|
||||
existingCategories.push(fst)
|
||||
}
|
||||
}
|
||||
}
|
||||
return await sortCategories(hierarchy, attrClass, existingCategories, viewletDescriptorId)
|
||||
}
|
||||
|
||||
export async function getCategories (
|
||||
client: TxOperations,
|
||||
_class: Ref<Class<Doc>>,
|
||||
docs: Doc[],
|
||||
key: string,
|
||||
mgr: StatusManager,
|
||||
viewletDescriptorId?: Ref<ViewletDescriptor>
|
||||
): Promise<CategoryType[]> {
|
||||
if (key === noCategory) return [undefined]
|
||||
@ -714,7 +666,6 @@ export async function getCategories (
|
||||
_class,
|
||||
key,
|
||||
docs.map((it) => getObjectValue(key, it) ?? undefined),
|
||||
mgr,
|
||||
viewletDescriptorId
|
||||
)
|
||||
}
|
||||
@ -724,7 +675,7 @@ export async function getCategories (
|
||||
*/
|
||||
export function getCategorySpaces (categories: CategoryType[]): Array<Ref<Space>> {
|
||||
return Array.from(
|
||||
(categories.filter((it) => typeof it === 'object') as StatusValue[]).reduce<Set<Ref<Space>>>((arr, val) => {
|
||||
(categories.filter((it) => typeof it === 'object') as AggregateValue[]).reduce<Set<Ref<Space>>>((arr, val) => {
|
||||
val.values.forEach((it) => arr.add(it.space))
|
||||
return arr
|
||||
}, new Set())
|
||||
@ -733,12 +684,12 @@ export function getCategorySpaces (categories: CategoryType[]): Array<Ref<Space>
|
||||
|
||||
export function concatCategories (arr1: CategoryType[], arr2: CategoryType[]): CategoryType[] {
|
||||
const uniqueValues: Set<string | number | undefined> = new Set()
|
||||
const uniqueObjects: Map<string | number, StatusValue> = new Map()
|
||||
const uniqueObjects: Map<string | number, AggregateValue> = new Map()
|
||||
|
||||
for (const item of arr1) {
|
||||
if (typeof item === 'object') {
|
||||
const id = item.name
|
||||
uniqueObjects.set(id, item)
|
||||
uniqueObjects.set(id as any, item)
|
||||
} else {
|
||||
uniqueValues.add(item)
|
||||
}
|
||||
@ -747,8 +698,8 @@ export function concatCategories (arr1: CategoryType[], arr2: CategoryType[]): C
|
||||
for (const item of arr2) {
|
||||
if (typeof item === 'object') {
|
||||
const id = item.name
|
||||
if (!uniqueObjects.has(id)) {
|
||||
uniqueObjects.set(id, item)
|
||||
if (!uniqueObjects.has(id as any)) {
|
||||
uniqueObjects.set(id as any, item)
|
||||
}
|
||||
} else {
|
||||
uniqueValues.add(item)
|
||||
|
@ -1,19 +1,10 @@
|
||||
import core, {
|
||||
Class,
|
||||
Doc,
|
||||
DocumentQuery,
|
||||
Ref,
|
||||
SortingOrder,
|
||||
Status,
|
||||
StatusManager,
|
||||
WithLookup,
|
||||
matchQuery
|
||||
} from '@hcengineering/core'
|
||||
import { Class, Doc, DocumentQuery, Ref, SortingOrder } from '@hcengineering/core'
|
||||
import { getResource } from '@hcengineering/platform'
|
||||
import { LiveQuery, createQuery, getAttributePresenterClass, getClient } from '@hcengineering/presentation'
|
||||
import { locationToUrl, getCurrentResolvedLocation } from '@hcengineering/ui'
|
||||
import {
|
||||
DropdownViewOption,
|
||||
Groupping,
|
||||
ToggleViewOption,
|
||||
ViewOptionModel,
|
||||
ViewOptions,
|
||||
@ -118,7 +109,6 @@ export async function showEmptyGroups (
|
||||
key: string,
|
||||
onUpdate: () => void,
|
||||
queryId: Ref<Doc>,
|
||||
mgr: StatusManager,
|
||||
viewletDescriptorId?: Ref<ViewletDescriptor>
|
||||
): Promise<any[] | undefined> {
|
||||
const client = getClient()
|
||||
@ -129,32 +119,27 @@ export async function showEmptyGroups (
|
||||
const { attrClass } = getAttributePresenterClass(hierarchy, attr)
|
||||
const attributeClass = hierarchy.getClass(attrClass)
|
||||
|
||||
if (hierarchy.isDerived(attrClass, core.class.Status)) {
|
||||
// We do not need extensions for all status categories.
|
||||
let statusList = mgr.filter((it) => {
|
||||
return it.ofAttribute === attr._id
|
||||
})
|
||||
if (query !== undefined) {
|
||||
const { [key]: st, space } = query
|
||||
const resQuery: DocumentQuery<Status> = {}
|
||||
if (space !== undefined) {
|
||||
resQuery.space = space
|
||||
}
|
||||
if (st !== undefined) {
|
||||
resQuery._id = st
|
||||
}
|
||||
statusList = matchQuery<Status>(statusList, resQuery, _class, hierarchy) as unknown as Array<WithLookup<Status>>
|
||||
let groupMixin: Groupping | undefined
|
||||
if (hierarchy.hasMixin(attributeClass, view.mixin.Groupping)) {
|
||||
groupMixin = hierarchy.as(attributeClass, view.mixin.Groupping)
|
||||
} else {
|
||||
const _attributeClass = hierarchy.classHierarchyMixin(attrClass, view.mixin.Groupping)
|
||||
if (_attributeClass !== undefined) {
|
||||
groupMixin = hierarchy.as(_attributeClass, view.mixin.Groupping)
|
||||
}
|
||||
const statuses = statusList.map((it) => it._id)
|
||||
return await groupByCategory(client, _class, key, statuses, mgr, viewletDescriptorId)
|
||||
}
|
||||
if (groupMixin?.grouppingManager !== undefined) {
|
||||
const grouppingManager = await getResource(groupMixin.grouppingManager)
|
||||
const docs = grouppingManager.groupValuesWithEmpty(hierarchy, _class, key, query)
|
||||
return await groupByCategory(client, _class, key, docs, viewletDescriptorId)
|
||||
}
|
||||
|
||||
const mixin = hierarchy.as(attributeClass, view.mixin.AllValuesFunc)
|
||||
if (mixin.func !== undefined) {
|
||||
const f = await getResource(mixin.func)
|
||||
const allValuesMixin = hierarchy.as(attributeClass, view.mixin.AllValuesFunc)
|
||||
if (allValuesMixin.func !== undefined) {
|
||||
const f = await getResource(allValuesMixin.func)
|
||||
const res = await f(query, onUpdate, queryId)
|
||||
if (res !== undefined) {
|
||||
return await groupByCategory(client, _class, key, res, mgr, viewletDescriptorId)
|
||||
return await groupByCategory(client, _class, key, res, viewletDescriptorId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -14,15 +14,18 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import type {
|
||||
import {
|
||||
Account,
|
||||
AggregateValue,
|
||||
AnyAttribute,
|
||||
Attribute,
|
||||
CategoryType,
|
||||
Class,
|
||||
Client,
|
||||
Doc,
|
||||
DocumentQuery,
|
||||
FindOptions,
|
||||
Hierarchy,
|
||||
Lookup,
|
||||
Mixin,
|
||||
Obj,
|
||||
@ -31,14 +34,15 @@ import type {
|
||||
Ref,
|
||||
SortingOrder,
|
||||
Space,
|
||||
StatusManager,
|
||||
StatusValue,
|
||||
Tx,
|
||||
Type,
|
||||
UXObject
|
||||
UXObject,
|
||||
WithLookup
|
||||
} from '@hcengineering/core'
|
||||
import { Asset, IntlString, Plugin, Resource, Status, plugin } from '@hcengineering/platform'
|
||||
import type { Preference } from '@hcengineering/preference'
|
||||
import type {
|
||||
import { Preference } from '@hcengineering/preference'
|
||||
import {
|
||||
AnyComponent,
|
||||
AnySvelteComponent,
|
||||
Location,
|
||||
@ -308,6 +312,62 @@ export interface AllValuesFunc extends Class<Doc> {
|
||||
func: GetAllValuesFunc
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export interface GrouppingManager {
|
||||
groupByCategories: (categories: any[]) => AggregateValue[]
|
||||
groupValues: (val: Doc[], targets: Set<any>) => Doc[]
|
||||
groupValuesWithEmpty: (
|
||||
hierarchy: Hierarchy,
|
||||
_class: Ref<Class<Doc>>,
|
||||
key: string,
|
||||
query: DocumentQuery<Doc> | undefined
|
||||
) => Array<Ref<Doc>>
|
||||
hasValue: (value: Doc | undefined | null, values: any[]) => boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export type GrouppingManagerResource = Resource<GrouppingManager>
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export interface Groupping extends Class<Doc> {
|
||||
grouppingManager: GrouppingManagerResource
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export interface AggregationManager {
|
||||
close: () => void
|
||||
notifyTx: (tx: Tx) => Promise<void>
|
||||
updateLookup: (resultDoc: WithLookup<Doc>, attr: Attribute<Doc>) => Promise<void>
|
||||
categorize: (target: Array<Ref<Doc>>, attr: AnyAttribute) => Promise<Array<Ref<Doc>>>
|
||||
getAttrClass: () => Ref<Class<Doc>>
|
||||
updateSorting?: (finalOptions: FindOptions<Doc>, attr: AnyAttribute) => Promise<void>
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export type AggregationManagerResource = Resource<AggregationManager>
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export type CreateAggregationManagerFunc = Resource<(client: Client, lqCallback: () => void) => AggregationManager>
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export interface Aggregation extends Class<Doc> {
|
||||
createAggregationManager: CreateAggregationManagerFunc
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
@ -578,7 +638,6 @@ export type ViewCategoryActionFunc = (
|
||||
key: string,
|
||||
onUpdate: () => void,
|
||||
queryId: Ref<Doc>,
|
||||
mgr: StatusManager,
|
||||
viewletDescriptorId?: Ref<ViewletDescriptor>
|
||||
) => Promise<CategoryType[] | undefined>
|
||||
/**
|
||||
@ -690,7 +749,9 @@ const view = plugin(viewId, {
|
||||
ObjectPanel: '' as Ref<Mixin<ObjectPanel>>,
|
||||
LinkProvider: '' as Ref<Mixin<LinkProvider>>,
|
||||
SpacePresenter: '' as Ref<Mixin<SpacePresenter>>,
|
||||
AttributeFilterPresenter: '' as Ref<Mixin<AttributeFilterPresenter>>
|
||||
AttributeFilterPresenter: '' as Ref<Mixin<AttributeFilterPresenter>>,
|
||||
Aggregation: '' as Ref<Mixin<Aggregation>>,
|
||||
Groupping: '' as Ref<Mixin<Groupping>>
|
||||
},
|
||||
class: {
|
||||
ViewletPreference: '' as Ref<Class<ViewletPreference>>,
|
||||
|
Loading…
Reference in New Issue
Block a user