mirror of
https://github.com/hcengineering/platform.git
synced 2024-11-22 11:42:30 +03:00
TSK-943:General Status support (#2842)
Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
parent
17ccbb1512
commit
585d82320e
@ -28,7 +28,8 @@ export async function createUpdateSpaceKanban (
|
||||
|
||||
await ctx.with('find-or-update', {}, (ctx) =>
|
||||
findOrUpdate(ctx, client, spaceId, task.class.State, sid, {
|
||||
title: st.name,
|
||||
ofAttribute: task.attribute.State,
|
||||
name: st.name,
|
||||
color: st.color,
|
||||
rank
|
||||
})
|
||||
@ -37,8 +38,8 @@ export async function createUpdateSpaceKanban (
|
||||
}
|
||||
|
||||
const doneStates = [
|
||||
{ class: task.class.WonState, title: 'Won' },
|
||||
{ class: task.class.LostState, title: 'Lost' }
|
||||
{ class: task.class.WonState, name: 'Won' },
|
||||
{ class: task.class.LostState, name: 'Lost' }
|
||||
]
|
||||
const doneStateRanks = genRanks(doneStates.length)
|
||||
for (const st of doneStates) {
|
||||
@ -49,10 +50,11 @@ export async function createUpdateSpaceKanban (
|
||||
break
|
||||
}
|
||||
|
||||
const sid = ('generated-' + spaceId + '.done-state.' + st.title.toLowerCase().replace(' ', '_')) as Ref<DoneState>
|
||||
const sid = `generated-${spaceId}.done-state.${st.name.toLowerCase().replace(' ', '_')}` as Ref<DoneState>
|
||||
await ctx.with('gen-done-state', {}, (ctx) =>
|
||||
findOrUpdate(ctx, client, spaceId, st.class, sid, {
|
||||
title: st.title,
|
||||
ofAttribute: task.attribute.DoneState,
|
||||
name: st.name,
|
||||
rank
|
||||
})
|
||||
)
|
||||
|
@ -264,7 +264,8 @@ export function createModel (builder: Builder): void {
|
||||
task.class.WonState,
|
||||
core.space.Model,
|
||||
{
|
||||
title: board.string.Completed,
|
||||
ofAttribute: task.attribute.DoneState,
|
||||
name: board.string.Completed,
|
||||
rank: '0'
|
||||
},
|
||||
board.state.Completed
|
||||
|
@ -13,26 +13,11 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import { Board, Card } from '@hcengineering/board'
|
||||
import {
|
||||
AttachedDoc,
|
||||
Class,
|
||||
Doc,
|
||||
DOMAIN_TX,
|
||||
generateId,
|
||||
Ref,
|
||||
TxCollectionCUD,
|
||||
TxCreateDoc,
|
||||
TxCUD,
|
||||
TxOperations,
|
||||
TxProcessor,
|
||||
TxUpdateDoc
|
||||
} from '@hcengineering/core'
|
||||
import { Ref, TxOperations } from '@hcengineering/core'
|
||||
import { createOrUpdate, MigrateOperation, MigrationClient, MigrationUpgradeClient } from '@hcengineering/model'
|
||||
import core from '@hcengineering/model-core'
|
||||
import { DOMAIN_TAGS } from '@hcengineering/model-tags'
|
||||
import { createKanbanTemplate, createSequence, DOMAIN_TASK } from '@hcengineering/model-task'
|
||||
import tags, { TagElement, TagReference } from '@hcengineering/tags'
|
||||
import { createKanbanTemplate, createSequence } from '@hcengineering/model-task'
|
||||
import tags from '@hcengineering/tags'
|
||||
import task, { createKanban, KanbanTemplate } from '@hcengineering/task'
|
||||
import board from './plugin'
|
||||
|
||||
@ -77,12 +62,12 @@ async function createSpace (tx: TxOperations): Promise<void> {
|
||||
async function createDefaultKanbanTemplate (tx: TxOperations): Promise<Ref<KanbanTemplate>> {
|
||||
const defaultKanban = {
|
||||
states: [
|
||||
{ color: 9, title: 'To do' },
|
||||
{ color: 9, title: 'Done' }
|
||||
{ color: 9, name: 'To do' },
|
||||
{ color: 9, name: 'Done' }
|
||||
],
|
||||
doneStates: [
|
||||
{ isWon: true, title: 'Won' },
|
||||
{ isWon: false, title: 'Lost' }
|
||||
{ isWon: true, name: 'Won' },
|
||||
{ isWon: false, name: 'Lost' }
|
||||
]
|
||||
}
|
||||
|
||||
@ -123,74 +108,7 @@ async function createDefaults (tx: TxOperations): Promise<void> {
|
||||
)
|
||||
}
|
||||
|
||||
interface CardLabel extends AttachedDoc {
|
||||
title: string
|
||||
color: number
|
||||
isHidden?: boolean
|
||||
}
|
||||
async function migrateLabels (client: MigrationClient): Promise<void> {
|
||||
const objectClass = 'board:class:CardLabel' as Ref<Class<Doc>>
|
||||
const txes = await client.find<TxCUD<CardLabel>>(DOMAIN_TX, { objectClass }, { sort: { modifiedOn: 1 } })
|
||||
const collectionTxes = await client.find<TxCollectionCUD<Board, CardLabel>>(
|
||||
DOMAIN_TX,
|
||||
{ 'tx.objectClass': objectClass },
|
||||
{ sort: { modifiedOn: 1 } }
|
||||
)
|
||||
await Promise.all([...txes, ...collectionTxes].map(({ _id }) => client.delete<Doc>(DOMAIN_TX, _id)))
|
||||
const removed = txes.filter(({ _class }) => _class === core.class.TxRemoveDoc).map(({ objectId }) => objectId)
|
||||
const createTxes = txes.filter(
|
||||
({ _class, objectId }) => _class === core.class.TxCreateDoc && !removed.includes(objectId)
|
||||
) as unknown as TxCreateDoc<CardLabel>[]
|
||||
const cardLabels = createTxes.map((createTx) => {
|
||||
const cardLabel = TxProcessor.createDoc2Doc(createTx)
|
||||
const updateTxes = collectionTxes
|
||||
.map(({ tx }) => tx)
|
||||
.filter(
|
||||
({ _class, objectId }) => _class === core.class.TxUpdateDoc && objectId === createTx.objectId
|
||||
) as unknown as TxUpdateDoc<CardLabel>[]
|
||||
return updateTxes.reduce((label, updateTx) => TxProcessor.updateDoc2Doc(label, updateTx), cardLabel)
|
||||
})
|
||||
await Promise.all(
|
||||
cardLabels.map((cardLabel) =>
|
||||
client.create<TagElement>(DOMAIN_TAGS, {
|
||||
_class: tags.class.TagElement,
|
||||
space: tags.space.Tags,
|
||||
targetClass: board.class.Card,
|
||||
category: board.category.Other,
|
||||
_id: cardLabel._id as unknown as Ref<TagElement>,
|
||||
modifiedBy: cardLabel.modifiedBy,
|
||||
modifiedOn: cardLabel.modifiedOn,
|
||||
title: cardLabel.title,
|
||||
color: cardLabel.color,
|
||||
description: ''
|
||||
})
|
||||
)
|
||||
)
|
||||
const cards = (await client.find<Card>(DOMAIN_TASK, { _class: board.class.Card })).filter((card) =>
|
||||
Array.isArray(card.labels)
|
||||
)
|
||||
for (const card of cards) {
|
||||
const labelRefs = card.labels as unknown as Array<Ref<CardLabel>>
|
||||
await client.update<Card>(DOMAIN_TASK, { _id: card._id }, { labels: labelRefs.length })
|
||||
for (const labelRef of labelRefs) {
|
||||
const cardLabel = cardLabels.find(({ _id }) => _id === labelRef)
|
||||
if (cardLabel === undefined) continue
|
||||
await client.create<TagReference>(DOMAIN_TAGS, {
|
||||
_class: tags.class.TagReference,
|
||||
attachedToClass: board.class.Card,
|
||||
_id: generateId(),
|
||||
attachedTo: card._id,
|
||||
space: card.space,
|
||||
tag: cardLabel._id as unknown as Ref<TagElement>,
|
||||
title: cardLabel.title,
|
||||
color: cardLabel.color,
|
||||
modifiedBy: cardLabel.modifiedBy,
|
||||
modifiedOn: cardLabel.modifiedOn,
|
||||
collection: 'labels'
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
async function migrateLabels (client: MigrationClient): Promise<void> {}
|
||||
export const boardOperation: MigrateOperation = {
|
||||
async migrate (client: MigrationClient): Promise<void> {
|
||||
await Promise.all([migrateLabels(client)])
|
||||
|
@ -60,6 +60,7 @@ import {
|
||||
TVersion
|
||||
} from './core'
|
||||
import { TAccount, TSpace } from './security'
|
||||
import { TStatus, TStatusCategory } from './status'
|
||||
import { TUserStatus } from './transient'
|
||||
import { TTx, TTxApplyIf, TTxCollectionCUD, TTxCreateDoc, TTxCUD, TTxMixin, TTxRemoveDoc, TTxUpdateDoc } from './tx'
|
||||
|
||||
@ -67,6 +68,7 @@ export * from './core'
|
||||
export { coreOperation } from './migration'
|
||||
export * from './security'
|
||||
export * from './tx'
|
||||
export * from './status'
|
||||
export { core as default }
|
||||
|
||||
export function createModel (builder: Builder): void {
|
||||
@ -114,7 +116,9 @@ export function createModel (builder: Builder): void {
|
||||
TFullTextSearchContext,
|
||||
TConfiguration,
|
||||
TConfigurationElement,
|
||||
TIndexConfiguration
|
||||
TIndexConfiguration,
|
||||
TStatus,
|
||||
TStatusCategory
|
||||
)
|
||||
|
||||
builder.createDoc(
|
||||
|
57
models/core/src/status.ts
Normal file
57
models/core/src/status.ts
Normal file
@ -0,0 +1,57 @@
|
||||
//
|
||||
// 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 { Attribute, DOMAIN_STATUS, DOMAIN_MODEL, Ref, Status, StatusCategory } from '@hcengineering/core'
|
||||
import { Model, Prop, TypeRef, TypeString, UX } from '@hcengineering/model'
|
||||
import { Asset, IntlString } from '@hcengineering/platform'
|
||||
import core from './component'
|
||||
import { TDoc } from './core'
|
||||
|
||||
// S T A T U S
|
||||
|
||||
@Model(core.class.Status, core.class.Doc, DOMAIN_STATUS)
|
||||
@UX(core.string.Status)
|
||||
export class TStatus extends TDoc implements Status {
|
||||
// We attach to attribute, so we could distinguish between
|
||||
ofAttribute!: Ref<Attribute<Status>>
|
||||
|
||||
@Prop(TypeRef(core.class.StatusCategory), core.string.StatusCategory)
|
||||
category!: Ref<StatusCategory>
|
||||
|
||||
@Prop(TypeString(), core.string.Name)
|
||||
name!: string
|
||||
|
||||
// @Prop(TypeNumber(), core.string.Color)
|
||||
color!: number
|
||||
|
||||
@Prop(TypeString(), core.string.Description)
|
||||
description!: string
|
||||
|
||||
// @Prop(TypeString(), core.string.Rank)
|
||||
rank!: string
|
||||
}
|
||||
|
||||
@Model(core.class.StatusCategory, core.class.Doc, DOMAIN_MODEL)
|
||||
@UX(core.string.StatusCategory)
|
||||
export class TStatusCategory extends TDoc implements StatusCategory {
|
||||
// We attach to attribute, so we could distinguish between
|
||||
ofAttribute!: Ref<Attribute<Status>>
|
||||
|
||||
icon!: Asset
|
||||
label!: IntlString
|
||||
color!: number
|
||||
defaultStatusName!: string
|
||||
order!: number
|
||||
}
|
@ -15,7 +15,7 @@
|
||||
|
||||
// To help typescript locate view plugin properly
|
||||
import type { Employee } from '@hcengineering/contact'
|
||||
import { Doc, FindOptions, IndexKind, Ref } from '@hcengineering/core'
|
||||
import { FindOptions, IndexKind, Ref, SortingOrder } from '@hcengineering/core'
|
||||
import { Customer, Funnel, Lead, leadId } from '@hcengineering/lead'
|
||||
import {
|
||||
Builder,
|
||||
@ -33,10 +33,11 @@ import attachment from '@hcengineering/model-attachment'
|
||||
import chunter from '@hcengineering/model-chunter'
|
||||
import contact, { TContact } from '@hcengineering/model-contact'
|
||||
import core from '@hcengineering/model-core'
|
||||
import task, { actionTemplates, TSpaceWithStates, TTask } from '@hcengineering/model-task'
|
||||
import view, { actionTemplates as viewTemplates, createAction } from '@hcengineering/model-view'
|
||||
import task, { TSpaceWithStates, TTask, actionTemplates } from '@hcengineering/model-task'
|
||||
import view, { createAction, actionTemplates as viewTemplates } from '@hcengineering/model-view'
|
||||
import workbench from '@hcengineering/model-workbench'
|
||||
import setting from '@hcengineering/setting'
|
||||
import { ViewOptionsModel } from '@hcengineering/view'
|
||||
import lead from './plugin'
|
||||
|
||||
@Model(lead.class.Funnel, task.class.SpaceWithStates)
|
||||
@ -256,6 +257,31 @@ export function createModel (builder: Builder): void {
|
||||
},
|
||||
lead.viewlet.ListLead
|
||||
)
|
||||
const leadViewOptions: ViewOptionsModel = {
|
||||
groupBy: ['state', 'assignee'],
|
||||
orderBy: [
|
||||
['state', SortingOrder.Ascending],
|
||||
['modifiedOn', SortingOrder.Descending],
|
||||
['dueDate', SortingOrder.Descending],
|
||||
['rank', SortingOrder.Ascending]
|
||||
],
|
||||
other: [
|
||||
{
|
||||
key: 'shouldShowAll',
|
||||
type: 'toggle',
|
||||
defaultValue: false,
|
||||
actionTarget: 'category',
|
||||
action: view.function.ShowEmptyGroups,
|
||||
label: view.string.ShowEmptyGroups
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
const lookupLeadOptions: FindOptions<Lead> = {
|
||||
lookup: {
|
||||
attachedTo: lead.mixin.Customer
|
||||
}
|
||||
}
|
||||
|
||||
builder.createDoc(
|
||||
view.class.Viewlet,
|
||||
@ -264,11 +290,11 @@ export function createModel (builder: Builder): void {
|
||||
attachTo: lead.class.Lead,
|
||||
descriptor: task.viewlet.Kanban,
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||
options: {
|
||||
lookup: {
|
||||
attachedTo: lead.mixin.Customer
|
||||
}
|
||||
} as FindOptions<Doc>, // TODO: fix
|
||||
viewOptions: {
|
||||
...leadViewOptions,
|
||||
groupDepth: 1
|
||||
},
|
||||
options: lookupLeadOptions,
|
||||
config: []
|
||||
},
|
||||
lead.viewlet.KanbanLead
|
||||
|
@ -62,16 +62,16 @@ async function createSpace (tx: TxOperations): Promise<void> {
|
||||
async function createDefaultKanbanTemplate (tx: TxOperations): Promise<Ref<KanbanTemplate>> {
|
||||
const defaultKanban = {
|
||||
states: [
|
||||
{ color: 9, title: 'Incoming' },
|
||||
{ color: 10, title: 'Negotation' },
|
||||
{ color: 1, title: 'Offer preparing' },
|
||||
{ color: 0, title: 'Make a decision' },
|
||||
{ color: 11, title: 'Contract conclusion' },
|
||||
{ color: 9, title: 'Done' }
|
||||
{ color: 9, name: 'Incoming' },
|
||||
{ color: 10, name: 'Negotation' },
|
||||
{ color: 1, name: 'Offer preparing' },
|
||||
{ color: 0, name: 'Make a decision' },
|
||||
{ color: 11, name: 'Contract conclusion' },
|
||||
{ color: 9, name: 'Done' }
|
||||
],
|
||||
doneStates: [
|
||||
{ isWon: true, title: 'Won' },
|
||||
{ isWon: false, title: 'Lost' }
|
||||
{ isWon: true, name: 'Won' },
|
||||
{ isWon: false, name: 'Lost' }
|
||||
]
|
||||
}
|
||||
|
||||
|
@ -14,7 +14,7 @@
|
||||
//
|
||||
|
||||
import type { Employee, Organization } from '@hcengineering/contact'
|
||||
import { Doc, FindOptions, IndexKind, Lookup, Ref, Timestamp } from '@hcengineering/core'
|
||||
import { IndexKind, Lookup, Ref, SortingOrder, Timestamp } from '@hcengineering/core'
|
||||
import {
|
||||
Builder,
|
||||
Collection,
|
||||
@ -41,9 +41,9 @@ import presentation from '@hcengineering/model-presentation'
|
||||
import tags from '@hcengineering/model-tags'
|
||||
import task, { actionTemplates, DOMAIN_TASK, TSpaceWithStates, TTask } from '@hcengineering/model-task'
|
||||
import tracker from '@hcengineering/model-tracker'
|
||||
import notification from '@hcengineering/notification'
|
||||
import view, { actionTemplates as viewTemplates, createAction } from '@hcengineering/model-view'
|
||||
import workbench, { Application, createNavigateAction } from '@hcengineering/model-workbench'
|
||||
import notification from '@hcengineering/notification'
|
||||
import { getEmbeddedLabel, IntlString } from '@hcengineering/platform'
|
||||
import {
|
||||
Applicant,
|
||||
@ -55,7 +55,7 @@ import {
|
||||
VacancyList
|
||||
} from '@hcengineering/recruit'
|
||||
import setting from '@hcengineering/setting'
|
||||
import { KeyBinding } from '@hcengineering/view'
|
||||
import { KeyBinding, ViewOptionsModel } from '@hcengineering/view'
|
||||
import recruit from './plugin'
|
||||
import { createReviewModel, reviewTableConfig, reviewTableOptions } from './review'
|
||||
import { TOpinion, TReview } from './review-model'
|
||||
@ -281,6 +281,7 @@ export function createModel (builder: Builder): void {
|
||||
label: recruit.string.Applications,
|
||||
createLabel: recruit.string.ApplicationCreateLabel,
|
||||
createComponent: recruit.component.CreateApplication,
|
||||
descriptors: [view.viewlet.Table, task.viewlet.Kanban, recruit.viewlet.ApplicantDashboard],
|
||||
baseQuery: {
|
||||
doneState: null
|
||||
}
|
||||
@ -408,7 +409,8 @@ export function createModel (builder: Builder): void {
|
||||
{
|
||||
attachTo: recruit.class.Applicant,
|
||||
descriptor: view.viewlet.Table,
|
||||
config: ['', 'attachedTo', 'state', 'doneState', 'modifiedOn']
|
||||
config: ['', 'attachedTo', 'state', 'doneState', 'modifiedOn'],
|
||||
variant: 'short'
|
||||
},
|
||||
recruit.viewlet.VacancyApplicationsShort
|
||||
)
|
||||
@ -588,6 +590,27 @@ export function createModel (builder: Builder): void {
|
||||
}
|
||||
}
|
||||
|
||||
const applicantViewOptions: ViewOptionsModel = {
|
||||
groupBy: ['state', 'doneState', 'assignee'],
|
||||
orderBy: [
|
||||
['state', SortingOrder.Ascending],
|
||||
['doneState', SortingOrder.Ascending],
|
||||
['modifiedOn', SortingOrder.Descending],
|
||||
['dueDate', SortingOrder.Descending],
|
||||
['rank', SortingOrder.Ascending]
|
||||
],
|
||||
other: [
|
||||
{
|
||||
key: 'shouldShowAll',
|
||||
type: 'toggle',
|
||||
defaultValue: false,
|
||||
actionTarget: 'category',
|
||||
action: view.function.ShowEmptyGroups,
|
||||
label: view.string.ShowEmptyGroups
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
builder.createDoc(
|
||||
view.class.Viewlet,
|
||||
core.space.Model,
|
||||
@ -595,9 +618,13 @@ export function createModel (builder: Builder): void {
|
||||
attachTo: recruit.class.Applicant,
|
||||
descriptor: task.viewlet.Kanban,
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||
viewOptions: {
|
||||
...applicantViewOptions,
|
||||
groupDepth: 1
|
||||
},
|
||||
options: {
|
||||
lookup: applicantKanbanLookup
|
||||
} as FindOptions<Doc>, // TODO: fix
|
||||
},
|
||||
config: []
|
||||
},
|
||||
recruit.viewlet.ApplicantKanban
|
||||
|
@ -232,14 +232,14 @@ async function createDefaults (tx: TxOperations): Promise<void> {
|
||||
async function createDefaultKanbanTemplate (tx: TxOperations): Promise<Ref<KanbanTemplate>> {
|
||||
const defaultKanban = {
|
||||
states: [
|
||||
{ color: 9, title: 'HR Interview' },
|
||||
{ color: 10, title: 'Technical Interview' },
|
||||
{ color: 1, title: 'Test task' },
|
||||
{ color: 0, title: 'Offer' }
|
||||
{ color: 9, name: 'HR Interview' },
|
||||
{ color: 10, name: 'Technical Interview' },
|
||||
{ color: 1, name: 'Test task' },
|
||||
{ color: 0, name: 'Offer' }
|
||||
],
|
||||
doneStates: [
|
||||
{ isWon: true, title: 'Won' },
|
||||
{ isWon: false, title: 'Lost' }
|
||||
{ isWon: true, name: 'Won' },
|
||||
{ isWon: false, name: 'Lost' }
|
||||
]
|
||||
}
|
||||
|
||||
|
@ -15,17 +15,4 @@
|
||||
|
||||
import { Builder } from '@hcengineering/model'
|
||||
|
||||
import core from '@hcengineering/core'
|
||||
import serverTask from '@hcengineering/server-task'
|
||||
import task from '@hcengineering/task'
|
||||
import serverNotification from '@hcengineering/server-notification'
|
||||
|
||||
export function createModel (builder: Builder): void {
|
||||
builder.mixin(task.class.Issue, core.class.Class, serverNotification.mixin.HTMLPresenter, {
|
||||
presenter: serverTask.function.IssueHTMLPresenter
|
||||
})
|
||||
|
||||
builder.mixin(task.class.Issue, core.class.Class, serverNotification.mixin.TextPresenter, {
|
||||
presenter: serverTask.function.IssueTextPresenter
|
||||
})
|
||||
}
|
||||
export function createModel (builder: Builder): void {}
|
||||
|
@ -15,7 +15,7 @@
|
||||
|
||||
import type { Employee } from '@hcengineering/contact'
|
||||
import contact from '@hcengineering/contact'
|
||||
import { Arr, Class, Doc, Domain, FindOptions, IndexKind, Ref, Space, Timestamp } from '@hcengineering/core'
|
||||
import { Arr, Attribute, Class, Doc, Domain, IndexKind, Ref, Space, Status, Timestamp } from '@hcengineering/core'
|
||||
import {
|
||||
Builder,
|
||||
Collection,
|
||||
@ -32,25 +32,20 @@ import {
|
||||
TypeString,
|
||||
UX
|
||||
} from '@hcengineering/model'
|
||||
import attachment from '@hcengineering/model-attachment'
|
||||
import chunter from '@hcengineering/model-chunter'
|
||||
import core, { TAttachedDoc, TClass, TDoc, TSpace } from '@hcengineering/model-core'
|
||||
import core, { TAttachedDoc, TClass, TDoc, TSpace, TStatus } from '@hcengineering/model-core'
|
||||
import view, { actionTemplates as viewTemplates, createAction, template } from '@hcengineering/model-view'
|
||||
import notification from '@hcengineering/notification'
|
||||
import { IntlString } from '@hcengineering/platform'
|
||||
import tags from '@hcengineering/tags'
|
||||
import {
|
||||
DOMAIN_STATE,
|
||||
DoneState,
|
||||
DoneStateTemplate,
|
||||
Issue,
|
||||
Kanban,
|
||||
KanbanCard,
|
||||
KanbanTemplate,
|
||||
KanbanTemplateSpace,
|
||||
LostState,
|
||||
LostStateTemplate,
|
||||
Project,
|
||||
Sequence,
|
||||
State,
|
||||
StateTemplate,
|
||||
@ -68,25 +63,15 @@ export { default } from './plugin'
|
||||
|
||||
export const DOMAIN_TASK = 'task' as Domain
|
||||
export const DOMAIN_KANBAN = 'kanban' as Domain
|
||||
@Model(task.class.State, core.class.Doc, DOMAIN_STATE, [task.interface.DocWithRank])
|
||||
@Model(task.class.State, core.class.Status)
|
||||
@UX(task.string.TaskState, task.icon.TaskState, undefined, 'rank')
|
||||
export class TState extends TDoc implements State {
|
||||
@Prop(TypeString(), task.string.TaskStateTitle)
|
||||
title!: string
|
||||
|
||||
color!: number
|
||||
|
||||
declare rank: string
|
||||
export class TState extends TStatus implements State {
|
||||
isArchived!: boolean
|
||||
}
|
||||
|
||||
@Model(task.class.DoneState, core.class.Doc, DOMAIN_STATE, [task.interface.DocWithRank])
|
||||
@UX(task.string.TaskStateDone, task.icon.TaskState, undefined, 'title')
|
||||
export class TDoneState extends TDoc implements DoneState {
|
||||
@Prop(TypeString(), task.string.TaskStateTitle)
|
||||
title!: string
|
||||
|
||||
declare rank: string
|
||||
}
|
||||
@Model(task.class.DoneState, core.class.Status)
|
||||
@UX(task.string.TaskStateDone, task.icon.TaskState, undefined, 'name')
|
||||
export class TDoneState extends TStatus implements DoneState {}
|
||||
|
||||
@Model(task.class.WonState, task.class.DoneState)
|
||||
export class TWonState extends TDoneState implements WonState {}
|
||||
@ -99,13 +84,13 @@ export class TLostState extends TDoneState implements LostState {}
|
||||
*
|
||||
* No domain is specified, since pure Tasks could not exists
|
||||
*/
|
||||
@Model(task.class.Task, core.class.AttachedDoc, DOMAIN_TASK, [task.interface.DocWithRank])
|
||||
@Model(task.class.Task, core.class.AttachedDoc, DOMAIN_TASK)
|
||||
@UX(task.string.Task, task.icon.Task, task.string.Task)
|
||||
export class TTask extends TAttachedDoc implements Task {
|
||||
@Prop(TypeRef(task.class.State), task.string.TaskState)
|
||||
@Prop(TypeRef(task.class.State), task.string.TaskState, { _id: task.attribute.State })
|
||||
state!: Ref<State>
|
||||
|
||||
@Prop(TypeRef(task.class.DoneState), task.string.TaskStateDone)
|
||||
@Prop(TypeRef(task.class.DoneState), task.string.TaskStateDone, { _id: task.attribute.DoneState })
|
||||
doneState!: Ref<DoneState> | null
|
||||
|
||||
@Prop(TypeString(), task.string.TaskNumber)
|
||||
@ -124,14 +109,11 @@ export class TTask extends TAttachedDoc implements Task {
|
||||
|
||||
declare rank: string
|
||||
|
||||
// @Prop(Collection(task.class.TodoItem), task.string.Todos)
|
||||
// todoItems!: number
|
||||
|
||||
@Prop(Collection(tags.class.TagReference, task.string.TaskLabels), task.string.TaskLabels)
|
||||
labels!: number
|
||||
}
|
||||
|
||||
@Model(task.class.TodoItem, core.class.AttachedDoc, DOMAIN_TASK, [task.interface.DocWithRank])
|
||||
@Model(task.class.TodoItem, core.class.AttachedDoc, DOMAIN_TASK)
|
||||
@UX(task.string.Todo)
|
||||
export class TTodoItem extends TAttachedDoc implements TodoItem {
|
||||
@Prop(TypeMarkup(), task.string.TodoName, task.icon.Task)
|
||||
@ -153,38 +135,6 @@ export class TTodoItem extends TAttachedDoc implements TodoItem {
|
||||
declare rank: string
|
||||
}
|
||||
|
||||
@Model(task.class.SpaceWithStates, core.class.Space)
|
||||
export class TSpaceWithStates extends TSpace {}
|
||||
|
||||
@Model(task.class.Project, task.class.SpaceWithStates)
|
||||
@UX(task.string.ProjectName, task.icon.Task)
|
||||
export class TProject extends TSpaceWithStates implements Project {}
|
||||
|
||||
@Model(task.class.Issue, task.class.Task, DOMAIN_TASK)
|
||||
@UX(task.string.Task, task.icon.Task, task.string.Task, 'number')
|
||||
export class TIssue extends TTask implements Issue {
|
||||
// We need to declare, to provide property with label
|
||||
@Prop(TypeRef(core.class.Doc), task.string.TaskParent)
|
||||
declare attachedTo: Ref<Doc>
|
||||
|
||||
@Prop(TypeString(), task.string.IssueName)
|
||||
@Index(IndexKind.FullText)
|
||||
name!: string
|
||||
|
||||
@Prop(TypeMarkup(), task.string.Description)
|
||||
@Index(IndexKind.FullText)
|
||||
description!: string
|
||||
|
||||
@Prop(Collection(chunter.class.Comment), task.string.TaskComments)
|
||||
comments!: number
|
||||
|
||||
@Prop(Collection(attachment.class.Attachment), attachment.string.Attachments, { shortLabel: attachment.string.Files })
|
||||
attachments!: number
|
||||
|
||||
@Prop(TypeRef(contact.class.Employee), task.string.TaskAssignee)
|
||||
declare assignee: Ref<Employee> | null
|
||||
}
|
||||
|
||||
@Mixin(task.mixin.KanbanCard, core.class.Class)
|
||||
export class TKanbanCard extends TClass implements KanbanCard {
|
||||
card!: AnyComponent
|
||||
@ -197,6 +147,9 @@ export class TKanban extends TDoc implements Kanban {
|
||||
attachedTo!: Ref<Space>
|
||||
}
|
||||
|
||||
@Model(task.class.SpaceWithStates, core.class.Space)
|
||||
export class TSpaceWithStates extends TSpace {}
|
||||
|
||||
@Model(task.class.KanbanTemplateSpace, core.class.Space)
|
||||
export class TKanbanTemplateSpace extends TSpace implements KanbanTemplateSpace {
|
||||
name!: IntlString
|
||||
@ -205,10 +158,13 @@ export class TKanbanTemplateSpace extends TSpace implements KanbanTemplateSpace
|
||||
editor!: AnyComponent
|
||||
}
|
||||
|
||||
@Model(task.class.StateTemplate, core.class.AttachedDoc, DOMAIN_KANBAN, [task.interface.DocWithRank])
|
||||
@Model(task.class.StateTemplate, core.class.AttachedDoc, DOMAIN_KANBAN)
|
||||
export class TStateTemplate extends TAttachedDoc implements StateTemplate {
|
||||
// We attach to attribute, so we could distinguish between
|
||||
ofAttribute!: Ref<Attribute<Status>>
|
||||
|
||||
@Prop(TypeString(), task.string.StateTemplateTitle)
|
||||
title!: string
|
||||
name!: string
|
||||
|
||||
@Prop(TypeString(), task.string.StateTemplateColor)
|
||||
color!: number
|
||||
@ -216,10 +172,13 @@ export class TStateTemplate extends TAttachedDoc implements StateTemplate {
|
||||
declare rank: string
|
||||
}
|
||||
|
||||
@Model(task.class.DoneStateTemplate, core.class.AttachedDoc, DOMAIN_KANBAN, [task.interface.DocWithRank])
|
||||
@Model(task.class.DoneStateTemplate, core.class.AttachedDoc, DOMAIN_KANBAN)
|
||||
export class TDoneStateTemplate extends TAttachedDoc implements DoneStateTemplate {
|
||||
// We attach to attribute, so we could distinguish between
|
||||
ofAttribute!: Ref<Attribute<Status>>
|
||||
|
||||
@Prop(TypeString(), task.string.StateTemplateTitle)
|
||||
title!: string
|
||||
name!: string
|
||||
|
||||
declare rank: string
|
||||
}
|
||||
@ -334,10 +293,8 @@ export function createModel (builder: Builder): void {
|
||||
TKanbanTemplate,
|
||||
TSequence,
|
||||
TTask,
|
||||
TSpaceWithStates,
|
||||
TProject,
|
||||
TIssue,
|
||||
TTodoItem
|
||||
TTodoItem,
|
||||
TSpaceWithStates
|
||||
)
|
||||
|
||||
builder.createDoc(
|
||||
@ -351,33 +308,14 @@ export function createModel (builder: Builder): void {
|
||||
task.viewlet.StatusTable
|
||||
)
|
||||
|
||||
builder.createDoc(
|
||||
view.class.Viewlet,
|
||||
core.space.Model,
|
||||
{
|
||||
attachTo: task.class.Issue,
|
||||
descriptor: task.viewlet.StatusTable,
|
||||
config: ['', 'name', 'assignee', 'state', 'doneState', 'attachments', 'comments', 'modifiedOn']
|
||||
},
|
||||
task.viewlet.TableIssue
|
||||
)
|
||||
|
||||
builder.mixin(task.class.Task, core.class.Class, view.mixin.ObjectPresenter, {
|
||||
presenter: view.component.ObjectPresenter
|
||||
})
|
||||
|
||||
builder.mixin(task.class.Issue, core.class.Class, view.mixin.ObjectPresenter, {
|
||||
presenter: task.component.TaskPresenter
|
||||
})
|
||||
|
||||
builder.mixin(task.class.KanbanTemplate, core.class.Class, view.mixin.ObjectPresenter, {
|
||||
presenter: task.component.KanbanTemplatePresenter
|
||||
})
|
||||
|
||||
builder.mixin(task.class.Issue, core.class.Class, view.mixin.ObjectEditor, {
|
||||
editor: task.component.EditIssue
|
||||
})
|
||||
|
||||
builder.mixin(task.class.Task, core.class.Class, view.mixin.ObjectEditorHeader, {
|
||||
editor: task.component.TaskHeader
|
||||
})
|
||||
@ -387,30 +325,6 @@ export function createModel (builder: Builder): void {
|
||||
fields: ['assignee']
|
||||
})
|
||||
|
||||
builder.createDoc(
|
||||
view.class.Viewlet,
|
||||
core.space.Model,
|
||||
{
|
||||
attachTo: task.class.Issue,
|
||||
descriptor: task.viewlet.Kanban,
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||
options: {
|
||||
lookup: {
|
||||
assignee: contact.class.Employee,
|
||||
_id: {
|
||||
todoItems: task.class.TodoItem
|
||||
}
|
||||
}
|
||||
} as FindOptions<Doc>,
|
||||
config: []
|
||||
},
|
||||
task.viewlet.KanbanIssue
|
||||
)
|
||||
|
||||
builder.mixin(task.class.Issue, core.class.Class, task.mixin.KanbanCard, {
|
||||
card: task.component.KanbanCard
|
||||
})
|
||||
|
||||
builder.createDoc(
|
||||
view.class.ActionCategory,
|
||||
core.space.Model,
|
||||
|
@ -13,13 +13,18 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import { Class, Doc, Ref, Space, TxOperations } from '@hcengineering/core'
|
||||
import { Class, Doc, Domain, Ref, Space, TxOperations, DOMAIN_STATUS } from '@hcengineering/core'
|
||||
import { createOrUpdate, MigrateOperation, MigrationClient, MigrationUpgradeClient } from '@hcengineering/model'
|
||||
import core from '@hcengineering/model-core'
|
||||
import { KanbanTemplate, StateTemplate, DoneStateTemplate, genRanks, createKanban } from '@hcengineering/task'
|
||||
import { DOMAIN_TASK } from '.'
|
||||
import task from './plugin'
|
||||
import tags from '@hcengineering/model-tags'
|
||||
import { createKanban, DoneStateTemplate, genRanks, KanbanTemplate, StateTemplate } from '@hcengineering/task'
|
||||
import { DOMAIN_TASK, DOMAIN_KANBAN } from '.'
|
||||
import task from './plugin'
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export const DOMAIN_STATE = 'state' as Domain
|
||||
|
||||
/**
|
||||
* @public
|
||||
@ -30,8 +35,8 @@ export interface KanbanTemplateData {
|
||||
title: KanbanTemplate['title']
|
||||
description?: string
|
||||
shortDescription?: string
|
||||
states: Pick<StateTemplate, 'title' | 'color'>[]
|
||||
doneStates: (Pick<DoneStateTemplate, 'title'> & { isWon: boolean })[]
|
||||
states: Pick<StateTemplate, 'name' | 'color'>[]
|
||||
doneStates: (Pick<DoneStateTemplate, 'name'> & { isWon: boolean })[]
|
||||
}
|
||||
|
||||
/**
|
||||
@ -79,8 +84,9 @@ export async function createKanbanTemplate (
|
||||
task.class.KanbanTemplate,
|
||||
'doneStatesC',
|
||||
{
|
||||
ofAttribute: task.attribute.DoneState,
|
||||
rank: doneStateRanks[i],
|
||||
title: st.title
|
||||
name: st.name
|
||||
}
|
||||
)
|
||||
)
|
||||
@ -90,8 +96,9 @@ export async function createKanbanTemplate (
|
||||
await Promise.all(
|
||||
data.states.map((st, i) =>
|
||||
client.addCollection(task.class.StateTemplate, data.space, data.kanbanId, task.class.KanbanTemplate, 'statesC', {
|
||||
ofAttribute: task.attribute.State,
|
||||
rank: stateRanks[i],
|
||||
title: st.title,
|
||||
name: st.name,
|
||||
color: st.color
|
||||
})
|
||||
)
|
||||
@ -100,26 +107,6 @@ export async function createKanbanTemplate (
|
||||
return tmpl
|
||||
}
|
||||
|
||||
async function createDefaultProject (tx: TxOperations): Promise<void> {
|
||||
const createTx = await tx.findOne(core.class.TxCreateDoc, {
|
||||
objectId: task.space.TasksPublic
|
||||
})
|
||||
if (createTx === undefined) {
|
||||
await tx.createDoc(
|
||||
task.class.Project,
|
||||
core.space.Space,
|
||||
{
|
||||
name: 'public',
|
||||
description: 'Public tasks',
|
||||
private: false,
|
||||
archived: false,
|
||||
members: []
|
||||
},
|
||||
task.space.TasksPublic
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
async function createDefaultSequence (tx: TxOperations): Promise<void> {
|
||||
const current = await tx.findOne(core.class.Space, {
|
||||
_id: task.space.Sequence
|
||||
@ -143,15 +130,15 @@ async function createDefaultSequence (tx: TxOperations): Promise<void> {
|
||||
async function createDefaultKanbanTemplate (tx: TxOperations): Promise<Ref<KanbanTemplate>> {
|
||||
const defaultKanban = {
|
||||
states: [
|
||||
{ color: 9, title: 'Open' },
|
||||
{ color: 10, title: 'In Progress' },
|
||||
{ color: 1, title: 'Under review' },
|
||||
{ color: 0, title: 'Done' },
|
||||
{ color: 11, title: 'Invalid' }
|
||||
{ color: 9, name: 'Open' },
|
||||
{ color: 10, name: 'In Progress' },
|
||||
{ color: 1, name: 'Under review' },
|
||||
{ color: 0, name: 'Done' },
|
||||
{ color: 11, name: 'Invalid' }
|
||||
],
|
||||
doneStates: [
|
||||
{ isWon: true, title: 'Won' },
|
||||
{ isWon: false, title: 'Lost' }
|
||||
{ isWon: true, name: 'Won' },
|
||||
{ isWon: false, name: 'Lost' }
|
||||
]
|
||||
}
|
||||
|
||||
@ -199,8 +186,6 @@ async function createSpace (tx: TxOperations): Promise<void> {
|
||||
async function createDefaults (tx: TxOperations): Promise<void> {
|
||||
await createSpace(tx)
|
||||
await createDefaultSequence(tx)
|
||||
await createDefaultProject(tx)
|
||||
await createSequence(tx, task.class.Issue)
|
||||
await createDefaultKanban(tx)
|
||||
}
|
||||
|
||||
@ -219,6 +204,37 @@ async function migrateTodoItems (client: MigrationClient): Promise<void> {
|
||||
export const taskOperation: MigrateOperation = {
|
||||
async migrate (client: MigrationClient): Promise<void> {
|
||||
await Promise.all([migrateTodoItems(client)])
|
||||
|
||||
const stateClasses = client.hierarchy.getDescendants(task.class.State)
|
||||
const doneStateClasses = client.hierarchy.getDescendants(task.class.DoneState)
|
||||
|
||||
const stateTemplateClasses = client.hierarchy.getDescendants(task.class.StateTemplate)
|
||||
const doneStateTemplatesClasses = client.hierarchy.getDescendants(task.class.DoneStateTemplate)
|
||||
|
||||
await client.move(DOMAIN_STATE, { _class: { $in: [...stateClasses, ...doneStateClasses] } }, DOMAIN_STATUS)
|
||||
|
||||
await client.update(
|
||||
DOMAIN_STATUS,
|
||||
{ _class: { $in: stateClasses }, ofAttribute: { $exists: false } },
|
||||
{ ofAttribute: task.attribute.State }
|
||||
)
|
||||
await client.update(
|
||||
DOMAIN_STATUS,
|
||||
{ _class: { $in: doneStateClasses }, ofAttribute: { $exists: false } },
|
||||
{ ofAttribute: task.attribute.DoneState }
|
||||
)
|
||||
|
||||
await client.update(
|
||||
DOMAIN_STATUS,
|
||||
{ _class: { $in: [...stateClasses, ...doneStateClasses] }, title: { $exists: true } },
|
||||
{ $rename: { title: 'name' } }
|
||||
)
|
||||
|
||||
await client.update(
|
||||
DOMAIN_KANBAN,
|
||||
{ _class: { $in: [...stateTemplateClasses, ...doneStateTemplatesClasses] }, title: { $exists: true } },
|
||||
{ $rename: { title: 'name' } }
|
||||
)
|
||||
},
|
||||
async upgrade (client: MigrationUpgradeClient): Promise<void> {
|
||||
const tx = new TxOperations(client, core.account.System)
|
||||
|
@ -43,7 +43,6 @@ export default mergeIds(taskId, task, {
|
||||
},
|
||||
component: {
|
||||
ProjectView: '' as AnyComponent,
|
||||
CreateProject: '' as AnyComponent,
|
||||
EditIssue: '' as AnyComponent,
|
||||
TaskPresenter: '' as AnyComponent,
|
||||
KanbanTemplatePresenter: '' as AnyComponent,
|
||||
|
@ -47,30 +47,29 @@ import {
|
||||
} from '@hcengineering/model'
|
||||
import attachment from '@hcengineering/model-attachment'
|
||||
import chunter from '@hcengineering/model-chunter'
|
||||
import core, { DOMAIN_SPACE, TAttachedDoc, TDoc, TSpace, TType } from '@hcengineering/model-core'
|
||||
import core, { DOMAIN_SPACE, TAttachedDoc, TDoc, TSpace, TStatus, TType } from '@hcengineering/model-core'
|
||||
import view, { actionTemplates, classPresenter, createAction } from '@hcengineering/model-view'
|
||||
import workbench, { createNavigateAction } from '@hcengineering/model-workbench'
|
||||
import notification from '@hcengineering/notification'
|
||||
import { Asset, IntlString } from '@hcengineering/platform'
|
||||
import { IntlString } from '@hcengineering/platform'
|
||||
import setting from '@hcengineering/setting'
|
||||
import tags, { TagElement } from '@hcengineering/tags'
|
||||
import task from '@hcengineering/task'
|
||||
import {
|
||||
Component,
|
||||
ComponentStatus,
|
||||
Issue,
|
||||
IssueChildInfo,
|
||||
IssueParentInfo,
|
||||
IssuePriority,
|
||||
IssueStatus,
|
||||
IssueStatusCategory,
|
||||
IssueTemplate,
|
||||
IssueTemplateChild,
|
||||
Component,
|
||||
ComponentStatus,
|
||||
Project,
|
||||
Scrum,
|
||||
ScrumRecord,
|
||||
Sprint,
|
||||
SprintStatus,
|
||||
Project,
|
||||
TimeReportDayType,
|
||||
TimeSpendReport,
|
||||
trackerId
|
||||
@ -89,34 +88,8 @@ export const DOMAIN_TRACKER = 'tracker' as Domain
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
@Model(tracker.class.IssueStatus, core.class.AttachedDoc, DOMAIN_TRACKER)
|
||||
export class TIssueStatus extends TAttachedDoc implements IssueStatus {
|
||||
@Index(IndexKind.Indexed)
|
||||
name!: string
|
||||
|
||||
description?: string
|
||||
color?: number
|
||||
|
||||
@Prop(TypeRef(tracker.class.IssueStatusCategory), tracker.string.StatusCategory)
|
||||
@Index(IndexKind.Indexed)
|
||||
category!: Ref<IssueStatusCategory>
|
||||
|
||||
@Prop(TypeString(), tracker.string.Rank)
|
||||
@Hidden()
|
||||
rank!: string
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
@Model(tracker.class.IssueStatusCategory, core.class.Doc, DOMAIN_MODEL)
|
||||
export class TIssueStatusCategory extends TDoc implements IssueStatusCategory {
|
||||
label!: IntlString
|
||||
icon!: Asset
|
||||
color!: number
|
||||
defaultStatusName!: string
|
||||
order!: number
|
||||
}
|
||||
@Model(tracker.class.IssueStatus, core.class.Status)
|
||||
export class TIssueStatus extends TStatus implements IssueStatus {}
|
||||
|
||||
/**
|
||||
* @public
|
||||
@ -207,7 +180,7 @@ export class TIssue extends TAttachedDoc implements Issue {
|
||||
@Index(IndexKind.FullText)
|
||||
description!: Markup
|
||||
|
||||
@Prop(TypeRef(tracker.class.IssueStatus), tracker.string.Status)
|
||||
@Prop(TypeRef(tracker.class.IssueStatus), tracker.string.Status, { _id: tracker.attribute.IssueStatus })
|
||||
@Index(IndexKind.Indexed)
|
||||
status!: Ref<IssueStatus>
|
||||
|
||||
@ -498,7 +471,6 @@ export function createModel (builder: Builder): void {
|
||||
TIssue,
|
||||
TIssueTemplate,
|
||||
TIssueStatus,
|
||||
TIssueStatusCategory,
|
||||
TTypeIssuePriority,
|
||||
TTypeComponentStatus,
|
||||
TSprint,
|
||||
@ -773,9 +745,10 @@ export function createModel (builder: Builder): void {
|
||||
)
|
||||
|
||||
builder.createDoc(
|
||||
tracker.class.IssueStatusCategory,
|
||||
core.class.StatusCategory,
|
||||
core.space.Model,
|
||||
{
|
||||
ofAttribute: tracker.attribute.IssueStatus,
|
||||
label: tracker.string.CategoryBacklog,
|
||||
icon: tracker.icon.CategoryBacklog,
|
||||
color: 12,
|
||||
@ -786,9 +759,10 @@ export function createModel (builder: Builder): void {
|
||||
)
|
||||
|
||||
builder.createDoc(
|
||||
tracker.class.IssueStatusCategory,
|
||||
core.class.StatusCategory,
|
||||
core.space.Model,
|
||||
{
|
||||
ofAttribute: tracker.attribute.IssueStatus,
|
||||
label: tracker.string.CategoryUnstarted,
|
||||
icon: tracker.icon.CategoryUnstarted,
|
||||
color: 13,
|
||||
@ -799,9 +773,10 @@ export function createModel (builder: Builder): void {
|
||||
)
|
||||
|
||||
builder.createDoc(
|
||||
tracker.class.IssueStatusCategory,
|
||||
core.class.StatusCategory,
|
||||
core.space.Model,
|
||||
{
|
||||
ofAttribute: tracker.attribute.IssueStatus,
|
||||
label: tracker.string.CategoryStarted,
|
||||
icon: tracker.icon.CategoryStarted,
|
||||
color: 14,
|
||||
@ -812,9 +787,10 @@ export function createModel (builder: Builder): void {
|
||||
)
|
||||
|
||||
builder.createDoc(
|
||||
tracker.class.IssueStatusCategory,
|
||||
core.class.StatusCategory,
|
||||
core.space.Model,
|
||||
{
|
||||
ofAttribute: tracker.attribute.IssueStatus,
|
||||
label: tracker.string.CategoryCompleted,
|
||||
icon: tracker.icon.CategoryCompleted,
|
||||
color: 15,
|
||||
@ -825,9 +801,10 @@ export function createModel (builder: Builder): void {
|
||||
)
|
||||
|
||||
builder.createDoc(
|
||||
tracker.class.IssueStatusCategory,
|
||||
core.class.StatusCategory,
|
||||
core.space.Model,
|
||||
{
|
||||
ofAttribute: tracker.attribute.IssueStatus,
|
||||
label: tracker.string.CategoryCanceled,
|
||||
icon: tracker.icon.CategoryCanceled,
|
||||
color: 16,
|
||||
@ -870,6 +847,14 @@ export function createModel (builder: Builder): void {
|
||||
presenters: [tracker.component.IssueStatistics]
|
||||
})
|
||||
|
||||
builder.mixin(tracker.class.IssueStatus, core.class.Class, view.mixin.ObjectPresenter, {
|
||||
presenter: tracker.component.StatusPresenter
|
||||
})
|
||||
|
||||
builder.mixin(tracker.class.IssueStatus, core.class.Class, view.mixin.AttributePresenter, {
|
||||
presenter: tracker.component.StatusRefPresenter
|
||||
})
|
||||
|
||||
builder.mixin(tracker.class.IssueStatus, core.class.Class, view.mixin.SortFuncs, {
|
||||
func: tracker.function.IssueStatusSort
|
||||
})
|
||||
@ -894,14 +879,6 @@ export function createModel (builder: Builder): void {
|
||||
component: view.component.ValueFilter
|
||||
})
|
||||
|
||||
builder.mixin(tracker.class.IssueStatus, core.class.Class, view.mixin.ObjectPresenter, {
|
||||
presenter: tracker.component.StatusPresenter
|
||||
})
|
||||
|
||||
builder.mixin(tracker.class.IssueStatus, core.class.Class, view.mixin.AttributePresenter, {
|
||||
presenter: tracker.component.StatusRefPresenter
|
||||
})
|
||||
|
||||
builder.mixin(tracker.class.TypeIssuePriority, core.class.Class, view.mixin.AttributePresenter, {
|
||||
presenter: tracker.component.PriorityRefPresenter
|
||||
})
|
||||
@ -947,10 +924,6 @@ export function createModel (builder: Builder): void {
|
||||
fields: ['assignee']
|
||||
})
|
||||
|
||||
builder.mixin(tracker.class.IssueStatus, core.class.Class, view.mixin.AllValuesFunc, {
|
||||
func: tracker.function.GetAllStatuses
|
||||
})
|
||||
|
||||
builder.mixin(tracker.class.TypeIssuePriority, core.class.Class, view.mixin.AllValuesFunc, {
|
||||
func: tracker.function.GetAllPriority
|
||||
})
|
||||
@ -1342,7 +1315,7 @@ export function createModel (builder: Builder): void {
|
||||
|
||||
const statusOptions: FindOptions<IssueStatus> = {
|
||||
lookup: {
|
||||
category: tracker.class.IssueStatusCategory
|
||||
category: core.class.StatusCategory
|
||||
},
|
||||
sort: { rank: SortingOrder.Ascending }
|
||||
}
|
||||
@ -1377,6 +1350,9 @@ export function createModel (builder: Builder): void {
|
||||
attribute: 'status',
|
||||
_class: tracker.class.IssueStatus,
|
||||
placeholder: tracker.string.SetStatus,
|
||||
query: {
|
||||
ofAttribute: tracker.attribute.IssueStatus
|
||||
},
|
||||
fillQuery: {
|
||||
space: 'space'
|
||||
},
|
||||
|
@ -21,11 +21,13 @@ import core, {
|
||||
generateId,
|
||||
Ref,
|
||||
SortingOrder,
|
||||
StatusCategory,
|
||||
TxCollectionCUD,
|
||||
TxCreateDoc,
|
||||
TxOperations,
|
||||
TxResult,
|
||||
TxUpdateDoc
|
||||
TxUpdateDoc,
|
||||
DOMAIN_STATUS
|
||||
} from '@hcengineering/core'
|
||||
import { createOrUpdate, MigrateOperation, MigrationClient, MigrationUpgradeClient } from '@hcengineering/model'
|
||||
import { DOMAIN_SPACE } from '@hcengineering/model-core'
|
||||
@ -35,7 +37,6 @@ import {
|
||||
genRanks,
|
||||
Issue,
|
||||
IssueStatus,
|
||||
IssueStatusCategory,
|
||||
IssueTemplate,
|
||||
IssueTemplateChild,
|
||||
Project,
|
||||
@ -55,9 +56,9 @@ enum DeprecatedIssueStatus {
|
||||
interface CreateProjectIssueStatusesArgs {
|
||||
tx: TxOperations
|
||||
projectId: Ref<Project>
|
||||
categories: IssueStatusCategory[]
|
||||
categories: StatusCategory[]
|
||||
defaultStatusId?: Ref<IssueStatus>
|
||||
defaultCategoryId?: Ref<IssueStatusCategory>
|
||||
defaultCategoryId?: Ref<StatusCategory>
|
||||
}
|
||||
|
||||
const categoryByDeprecatedIssueStatus = {
|
||||
@ -81,15 +82,14 @@ async function createProjectIssueStatuses ({
|
||||
const { _id: category, defaultStatusName } = statusCategory
|
||||
const rank = issueStatusRanks[i]
|
||||
|
||||
await tx.addCollection(
|
||||
tracker.class.IssueStatus,
|
||||
attachedTo,
|
||||
attachedTo,
|
||||
tracker.class.Project,
|
||||
'issueStatuses',
|
||||
{ name: defaultStatusName, category, rank },
|
||||
category === defaultCategoryId ? defaultStatusId : undefined
|
||||
)
|
||||
if (defaultStatusName !== undefined) {
|
||||
await tx.createDoc(
|
||||
tracker.class.IssueStatus,
|
||||
attachedTo,
|
||||
{ ofAttribute: tracker.attribute.IssueStatus, name: defaultStatusName, category, rank },
|
||||
category === defaultCategoryId ? defaultStatusId : undefined
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -105,11 +105,7 @@ async function createDefaultProject (tx: TxOperations): Promise<void> {
|
||||
// Create new if not deleted by customers.
|
||||
if (current === undefined && currentDeleted === undefined) {
|
||||
const defaultStatusId: Ref<IssueStatus> = generateId()
|
||||
const categories = await tx.findAll(
|
||||
tracker.class.IssueStatusCategory,
|
||||
{},
|
||||
{ sort: { order: SortingOrder.Ascending } }
|
||||
)
|
||||
const categories = await tx.findAll(core.class.StatusCategory, {}, { sort: { order: SortingOrder.Ascending } })
|
||||
|
||||
await tx.createDoc<Project>(
|
||||
tracker.class.Project,
|
||||
@ -137,7 +133,7 @@ async function fixProjectIssueStatusesOrder (tx: TxOperations, project: Project)
|
||||
const statuses = await tx.findAll(
|
||||
tracker.class.IssueStatus,
|
||||
{ attachedTo: project._id },
|
||||
{ lookup: { category: tracker.class.IssueStatusCategory } }
|
||||
{ lookup: { category: core.class.StatusCategory } }
|
||||
)
|
||||
statuses.sort((a, b) => (a.$lookup?.category?.order ?? 0) - (b.$lookup?.category?.order ?? 0))
|
||||
const issueStatusRanks = genRanks(statuses.length)
|
||||
@ -170,11 +166,7 @@ async function upgradeProjectIssueStatuses (tx: TxOperations): Promise<void> {
|
||||
const projects = await tx.findAll(tracker.class.Project, { issueStatuses: undefined })
|
||||
|
||||
if (projects.length > 0) {
|
||||
const categories = await tx.findAll(
|
||||
tracker.class.IssueStatusCategory,
|
||||
{},
|
||||
{ sort: { order: SortingOrder.Ascending } }
|
||||
)
|
||||
const categories = await tx.findAll(core.class.StatusCategory, {}, { sort: { order: SortingOrder.Ascending } })
|
||||
|
||||
for (const project of projects) {
|
||||
const defaultStatusId: Ref<IssueStatus> = generateId()
|
||||
@ -754,6 +746,22 @@ export const trackerOperation: MigrateOperation = {
|
||||
await fillRank(client)
|
||||
await renameProject(client)
|
||||
await setCreate(client)
|
||||
|
||||
// Move all status objects into status domain
|
||||
await client.move(
|
||||
DOMAIN_TRACKER,
|
||||
{
|
||||
_class: tracker.class.IssueStatus
|
||||
},
|
||||
DOMAIN_STATUS
|
||||
)
|
||||
await client.update(
|
||||
DOMAIN_STATUS,
|
||||
{ _class: tracker.class.IssueStatus, ofAttribute: { $exists: false } },
|
||||
{
|
||||
ofAttribute: tracker.attribute.IssueStatus
|
||||
}
|
||||
)
|
||||
},
|
||||
async upgrade (client: MigrationUpgradeClient): Promise<void> {
|
||||
const tx = new TxOperations(client, core.account.System)
|
||||
|
@ -64,7 +64,8 @@ import type {
|
||||
ViewOptions,
|
||||
AllValuesFunc,
|
||||
LinkProvider,
|
||||
ObjectPanel
|
||||
ObjectPanel,
|
||||
AllValuesFuncGetter
|
||||
} from '@hcengineering/view'
|
||||
import view from './plugin'
|
||||
|
||||
@ -220,9 +221,7 @@ export class TSortFuncs extends TClass implements ClassSortFuncs {
|
||||
|
||||
@Mixin(view.mixin.AllValuesFunc, core.class.Class)
|
||||
export class TAllValuesFunc extends TClass implements AllValuesFunc {
|
||||
func!: Resource<
|
||||
(space: Ref<Space> | undefined, onUpdate: () => void, queryId: Ref<Doc>) => Promise<any[] | undefined>
|
||||
>
|
||||
func!: Resource<AllValuesFuncGetter>
|
||||
}
|
||||
|
||||
@Model(view.class.ViewletPreference, preference.class.Preference)
|
||||
@ -774,6 +773,18 @@ export function createModel (builder: Builder): void {
|
||||
},
|
||||
view.action.Open
|
||||
)
|
||||
|
||||
builder.mixin(core.class.Status, core.class.Class, view.mixin.SortFuncs, {
|
||||
func: view.function.StatusSort
|
||||
})
|
||||
|
||||
builder.mixin(core.class.Status, core.class.Class, view.mixin.ObjectPresenter, {
|
||||
presenter: view.component.StatusPresenter
|
||||
})
|
||||
|
||||
builder.mixin(core.class.Status, core.class.Class, view.mixin.AttributePresenter, {
|
||||
presenter: view.component.StatusRefPresenter
|
||||
})
|
||||
}
|
||||
|
||||
export default view
|
||||
|
@ -67,7 +67,9 @@ export default mergeIds(viewId, view, {
|
||||
ListView: '' as AnyComponent,
|
||||
IndexedDocumentPreview: '' as AnyComponent,
|
||||
SpaceRefPresenter: '' as AnyComponent,
|
||||
EnumPresenter: '' as AnyComponent
|
||||
EnumPresenter: '' as AnyComponent,
|
||||
StatusPresenter: '' as AnyComponent,
|
||||
StatusRefPresenter: '' as AnyComponent
|
||||
},
|
||||
string: {
|
||||
Table: '' as IntlString,
|
||||
|
@ -23,13 +23,17 @@ import type {
|
||||
BlobData,
|
||||
Class,
|
||||
Collection,
|
||||
Configuration,
|
||||
ConfigurationElement,
|
||||
Doc,
|
||||
DocIndexState,
|
||||
FullTextSearchContext,
|
||||
Enum,
|
||||
EnumOf,
|
||||
FullTextData,
|
||||
FullTextSearchContext,
|
||||
Hyperlink,
|
||||
IndexingConfiguration,
|
||||
IndexStageState,
|
||||
Interface,
|
||||
Obj,
|
||||
PluginConfiguration,
|
||||
@ -39,12 +43,9 @@ import type {
|
||||
Space,
|
||||
Timestamp,
|
||||
Type,
|
||||
UserStatus,
|
||||
Configuration,
|
||||
ConfigurationElement,
|
||||
IndexStageState,
|
||||
IndexingConfiguration
|
||||
UserStatus
|
||||
} from './classes'
|
||||
import { Status, StatusCategory } from './status'
|
||||
import type {
|
||||
Tx,
|
||||
TxApplyIf,
|
||||
@ -54,7 +55,8 @@ import type {
|
||||
TxMixin,
|
||||
TxModelUpgrade,
|
||||
TxRemoveDoc,
|
||||
TxUpdateDoc
|
||||
TxUpdateDoc,
|
||||
TxWorkspaceEvent
|
||||
} from './tx'
|
||||
|
||||
/**
|
||||
@ -78,6 +80,7 @@ export default plugin(coreId, {
|
||||
Attribute: '' as Ref<Class<AnyAttribute>>,
|
||||
Tx: '' as Ref<Class<Tx>>,
|
||||
TxModelUpgrade: '' as Ref<Class<TxModelUpgrade>>,
|
||||
TxWorkspaceEvent: '' as Ref<Class<TxWorkspaceEvent>>,
|
||||
TxApplyIf: '' as Ref<Class<TxApplyIf>>,
|
||||
TxCUD: '' as Ref<Class<TxCUD<Doc>>>,
|
||||
TxCreateDoc: '' as Ref<Class<TxCreateDoc<Doc>>>,
|
||||
@ -111,7 +114,10 @@ export default plugin(coreId, {
|
||||
DocIndexState: '' as Ref<Class<DocIndexState>>,
|
||||
IndexStageState: '' as Ref<Class<IndexStageState>>,
|
||||
|
||||
Configuration: '' as Ref<Class<Configuration>>
|
||||
Configuration: '' as Ref<Class<Configuration>>,
|
||||
|
||||
Status: '' as Ref<Class<Status>>,
|
||||
StatusCategory: '' as Ref<Class<StatusCategory>>
|
||||
},
|
||||
mixin: {
|
||||
FullTextSearchContext: '' as Ref<Mixin<FullTextSearchContext>>,
|
||||
@ -161,6 +167,8 @@ export default plugin(coreId, {
|
||||
Private: '' as IntlString,
|
||||
Object: '' as IntlString,
|
||||
System: '' as IntlString,
|
||||
CreatedBy: '' as IntlString
|
||||
CreatedBy: '' as IntlString,
|
||||
Status: '' as IntlString,
|
||||
StatusCategory: '' as IntlString
|
||||
}
|
||||
})
|
||||
|
@ -31,6 +31,7 @@ export * from './storage'
|
||||
export * from './tx'
|
||||
export * from './utils'
|
||||
export * from './backup'
|
||||
export * from './status'
|
||||
|
||||
addStringsLoader(coreId, async (lang: string) => {
|
||||
return await import(`./lang/${lang}.json`)
|
||||
|
@ -28,6 +28,8 @@
|
||||
"Hyperlink": "URL",
|
||||
"Object": "Object",
|
||||
"System": "System",
|
||||
"CreatedBy": "Reporter"
|
||||
"CreatedBy": "Reporter",
|
||||
"Status": "Status",
|
||||
"StatusCategory": "Status category"
|
||||
}
|
||||
}
|
||||
|
@ -28,6 +28,8 @@
|
||||
"Hyperlink": "URL",
|
||||
"Object": "Объект",
|
||||
"System": "Система",
|
||||
"CreatedBy": "Автор"
|
||||
"CreatedBy": "Автор",
|
||||
"Status": "Статус",
|
||||
"StatusCategory": "Категория статуса"
|
||||
}
|
||||
}
|
||||
|
@ -4,7 +4,7 @@ import core from './component'
|
||||
import { Hierarchy } from './hierarchy'
|
||||
import { getObjectValue } from './objvalue'
|
||||
import { createPredicates, isPredicate } from './predicate'
|
||||
import { SortingOrder, SortingQuery, Storage } from './storage'
|
||||
import { SortQuerySelector, SortingOrder, SortingQuery, SortingRules, Storage } from './storage'
|
||||
|
||||
/**
|
||||
* @public
|
||||
@ -70,7 +70,30 @@ export async function resultSort<T extends Doc> (
|
||||
result.sort(sortFunc)
|
||||
}
|
||||
|
||||
function getSortingResult (aValue: any, bValue: any, order: SortingOrder): number {
|
||||
function mapSortingValue (order: SortingOrder | SortingRules<any>, val: any): any {
|
||||
if (typeof order !== 'object') {
|
||||
return val
|
||||
}
|
||||
for (const r of order.cases) {
|
||||
if (typeof r.query === 'object') {
|
||||
const q: SortQuerySelector<any> = r.query
|
||||
if (q.$in?.includes(val) ?? false) {
|
||||
return r.index
|
||||
}
|
||||
if (q.$nin !== undefined && !q.$nin.includes(val)) {
|
||||
return r.index
|
||||
}
|
||||
if (q.$ne !== undefined && q.$ne !== val) {
|
||||
return r.index
|
||||
}
|
||||
}
|
||||
if (r.query === val) {
|
||||
return r.index
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getSortingResult (aValue: any, bValue: any, order: SortingOrder | SortingRules<any>): number {
|
||||
let res = 0
|
||||
if (typeof aValue === 'undefined') {
|
||||
return typeof bValue === 'undefined' ? 0 : -1
|
||||
@ -78,12 +101,19 @@ function getSortingResult (aValue: any, bValue: any, order: SortingOrder): numbe
|
||||
if (typeof bValue === 'undefined') {
|
||||
return 1
|
||||
}
|
||||
|
||||
const orderOrder = typeof order === 'object' ? order.order : order
|
||||
|
||||
if (Array.isArray(aValue) && Array.isArray(bValue)) {
|
||||
res = (aValue.sort((a, b) => (a - b) * order)[0] ?? 0) - (bValue.sort((a, b) => (a - b) * order)[0] ?? 0)
|
||||
res =
|
||||
(aValue.map((it) => mapSortingValue(order, it)).sort((a, b) => (a - b) * orderOrder)[0] ?? 0) -
|
||||
(bValue.map((it) => mapSortingValue(order, it)).sort((a, b) => (a - b) * orderOrder)[0] ?? 0)
|
||||
} else {
|
||||
res = typeof aValue === 'string' ? aValue.localeCompare(bValue) : aValue - bValue
|
||||
const aaValue = mapSortingValue(order, aValue)
|
||||
const bbValue = mapSortingValue(order, bValue)
|
||||
res = typeof aaValue === 'string' ? aaValue.localeCompare(bbValue) : aaValue - bbValue
|
||||
}
|
||||
return res * order
|
||||
return res * orderOrder
|
||||
}
|
||||
|
||||
async function getEnums<T extends Doc> (
|
||||
|
91
packages/core/src/status.ts
Normal file
91
packages/core/src/status.ts
Normal file
@ -0,0 +1,91 @@
|
||||
//
|
||||
// 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 { Asset, IntlString } from '@hcengineering/platform'
|
||||
import { Attribute, Doc, Domain, Ref } from './classes'
|
||||
import { WithLookup } from './storage'
|
||||
import { IdMap, toIdMap } from './utils'
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export interface StatusCategory extends Doc {
|
||||
ofAttribute: Ref<Attribute<Status>>
|
||||
icon: Asset
|
||||
label: IntlString
|
||||
color: number
|
||||
defaultStatusName?: string
|
||||
order: number // category order
|
||||
}
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export const DOMAIN_STATUS = 'status' as Domain
|
||||
|
||||
/**
|
||||
* @public
|
||||
*
|
||||
* Status is attached to attribute, and if user attribute will be removed, all status values will be remove as well.
|
||||
*/
|
||||
export interface Status extends Doc {
|
||||
// We attach to attribute, so we could distinguish between
|
||||
ofAttribute: Ref<Attribute<Status>>
|
||||
|
||||
// Optional category.
|
||||
category?: Ref<StatusCategory>
|
||||
|
||||
// Status with case insensitivity name match will be assumed same.
|
||||
name: string
|
||||
|
||||
// Optional color
|
||||
color?: number
|
||||
// Optional description
|
||||
description?: string
|
||||
// Lexorank rank for ordering.
|
||||
rank: string
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export class StatusValue {
|
||||
constructor (readonly name: string, readonly color: number | undefined, readonly values: WithLookup<Status>[]) {}
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*
|
||||
* Allow to query for status keys/values.
|
||||
*/
|
||||
export class StatusManager {
|
||||
byId: IdMap<WithLookup<Status>>
|
||||
|
||||
constructor (readonly statuses: WithLookup<Status>[]) {
|
||||
this.byId = toIdMap(statuses)
|
||||
}
|
||||
|
||||
get (ref: Ref<Status>): WithLookup<Status> | undefined {
|
||||
return this.byId.get(ref)
|
||||
}
|
||||
|
||||
filter (predicate: (value: WithLookup<Status>) => boolean): WithLookup<Status>[] {
|
||||
return this.statuses.filter(predicate)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export type CategoryType = number | string | undefined | Ref<Doc> | StatusValue
|
@ -123,15 +123,41 @@ export type FindOptions<T extends Doc> = {
|
||||
projection?: Projection<T>
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
|
||||
export type SortQuerySelector<T> = {
|
||||
$in?: T[]
|
||||
$nin?: T[]
|
||||
$ne?: T
|
||||
}
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export type SortRuleQueryType<T> = (T extends Array<infer U> ? U | U[] : T) | SortQuerySelector<T>
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export interface SortingRules<T> {
|
||||
order: SortingOrder
|
||||
default?: string | number
|
||||
cases: {
|
||||
query: SortRuleQueryType<T>
|
||||
index: string | number
|
||||
}[]
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export type SortingQuery<T extends Doc> = {
|
||||
[P in keyof T]?: SortingOrder
|
||||
[P in keyof T]?: SortingOrder | SortingRules<T[P]>
|
||||
} & {
|
||||
// support nested queries e.g. 'user.friends.name'
|
||||
// this will mark all unrecognized properties as any (including nested queries)
|
||||
[key: string]: SortingOrder
|
||||
[key: string]: SortingOrder | SortingRules<any>
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -13,6 +13,7 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import justClone from 'just-clone'
|
||||
import type { KeysByType } from 'simplytyped'
|
||||
import type {
|
||||
Account,
|
||||
@ -34,7 +35,6 @@ import { _getOperator } from './operator'
|
||||
import { _toDoc } from './proxy'
|
||||
import type { DocumentQuery, TxResult } from './storage'
|
||||
import { generateId } from './utils'
|
||||
import justClone from 'just-clone'
|
||||
|
||||
/**
|
||||
* @public
|
||||
@ -43,10 +43,34 @@ export interface Tx extends Doc {
|
||||
objectSpace: Ref<Space> // space where transaction will operate
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export enum WorkspaceEvent {
|
||||
UpgradeScheduled,
|
||||
Upgrade,
|
||||
IndexingUpdate
|
||||
}
|
||||
|
||||
/**
|
||||
* Event to be send by server during model upgrade procedure.
|
||||
* @public
|
||||
*/
|
||||
export interface TxWorkspaceEvent extends Tx {
|
||||
event: WorkspaceEvent
|
||||
params: any
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export interface IndexingUpdateEvent {
|
||||
_class: Ref<Class<Doc>>[]
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export interface TxModelUpgrade extends Tx {}
|
||||
|
||||
/**
|
||||
|
@ -13,69 +13,41 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { Class, Doc, DocumentQuery, DocumentUpdate, FindOptions, Ref, SortingOrder } from '@hcengineering/core'
|
||||
import { createQuery, getClient } from '@hcengineering/presentation'
|
||||
import { getPlatformColor, ScrollBox, Scroller } from '@hcengineering/ui'
|
||||
import { CategoryType, Doc, DocumentUpdate, Ref } from '@hcengineering/core'
|
||||
import { getClient } from '@hcengineering/presentation'
|
||||
import { ScrollBox, Scroller } from '@hcengineering/ui'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import { CardDragEvent, Item, StateType, TypeState } from '../types'
|
||||
import { CardDragEvent, Item } from '../types'
|
||||
import { calcRank } from '../utils'
|
||||
import KanbanRow from './KanbanRow.svelte'
|
||||
|
||||
export let _class: Ref<Class<Item>>
|
||||
export let options: FindOptions<Item> | undefined = undefined
|
||||
export let states: TypeState[] = []
|
||||
export let query: DocumentQuery<Item> = {}
|
||||
export let fieldName: string
|
||||
export let categories: CategoryType[] = []
|
||||
export let objects: Item[] = []
|
||||
export let groupByDocs: Record<string | number, Item[]>
|
||||
export let getGroupByValues: (groupByDocs: Record<string | number, Item[]>, category: CategoryType) => Item[]
|
||||
export let setGroupByValues: (
|
||||
groupByDocs: Record<string | number, Item[]>,
|
||||
category: CategoryType,
|
||||
docs: Item[]
|
||||
) => void
|
||||
|
||||
export let selection: number | undefined = undefined
|
||||
export let checked: Doc[] = []
|
||||
export let dontUpdateRank: boolean = false
|
||||
|
||||
export let getUpdateProps: (doc: Doc, state: CategoryType) => DocumentUpdate<Item> | undefined
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
let objects: Item[] = []
|
||||
|
||||
const objsQ = createQuery()
|
||||
$: objsQ.query(
|
||||
_class,
|
||||
query,
|
||||
(result) => {
|
||||
objects = result
|
||||
fillStateObjects(result, fieldName)
|
||||
dispatch('content', objects)
|
||||
},
|
||||
{
|
||||
sort: { rank: SortingOrder.Ascending },
|
||||
...options
|
||||
}
|
||||
)
|
||||
|
||||
function fieldNameChange (fieldName: string) {
|
||||
fillStateObjects(objects, fieldName)
|
||||
}
|
||||
|
||||
$: fieldNameChange(fieldName)
|
||||
|
||||
function fillStateObjects (objects: Item[], fieldName: string): void {
|
||||
objectByState.clear()
|
||||
for (const object of objects) {
|
||||
const arr = objectByState.get((object as any)[fieldName]) ?? []
|
||||
arr.push(object)
|
||||
objectByState.set((object as any)[fieldName], arr)
|
||||
}
|
||||
objectByState = objectByState
|
||||
}
|
||||
|
||||
async function move (state: StateType) {
|
||||
async function move (state: CategoryType) {
|
||||
if (dragCard === undefined) {
|
||||
return
|
||||
}
|
||||
let updates: DocumentUpdate<Item> = {}
|
||||
let updates = getUpdateProps(dragCard, state)
|
||||
|
||||
if (dragCardInitialState !== state) {
|
||||
updates = {
|
||||
...updates,
|
||||
[fieldName]: state
|
||||
}
|
||||
if (updates === undefined) {
|
||||
panelDragLeave(undefined, dragCardState)
|
||||
return
|
||||
}
|
||||
|
||||
if (!dontUpdateRank && dragCardInitialRank !== dragCard.rank) {
|
||||
@ -95,35 +67,61 @@
|
||||
|
||||
let dragCard: Item | undefined
|
||||
let dragCardInitialRank: string | undefined
|
||||
let dragCardInitialState: StateType
|
||||
|
||||
let objectByState: Map<StateType, Item[]> = new Map<StateType, Item[]>()
|
||||
let dragCardInitialState: CategoryType
|
||||
let dragCardInitialPosition: number | undefined
|
||||
let dragCardState: CategoryType | undefined
|
||||
|
||||
let isDragging = false
|
||||
|
||||
async function updateDone (query: DocumentUpdate<Item>): Promise<void> {
|
||||
async function updateDone (updateValue: DocumentUpdate<Item>): Promise<void> {
|
||||
isDragging = false
|
||||
if (dragCard === undefined) {
|
||||
return
|
||||
}
|
||||
await client.update(dragCard, query)
|
||||
await client.update(dragCard, updateValue)
|
||||
}
|
||||
|
||||
function panelDragOver (event: Event, state: TypeState): void {
|
||||
event.preventDefault()
|
||||
const card = dragCard as any
|
||||
if (card !== undefined && card[fieldName] !== state._id) {
|
||||
const oldArr = objectByState.get(card[fieldName]) ?? []
|
||||
const index = oldArr.findIndex((p) => p._id === card._id)
|
||||
function panelDragOver (event: Event | undefined, state: CategoryType): void {
|
||||
event?.preventDefault()
|
||||
if (dragCard !== undefined && dragCardState !== state) {
|
||||
const updates = getUpdateProps(dragCard, state)
|
||||
if (updates === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
const oldArr = getGroupByValues(groupByDocs, dragCardState)
|
||||
const index = oldArr.findIndex((p) => p._id === dragCard?._id)
|
||||
if (index !== -1) {
|
||||
oldArr.splice(index, 1)
|
||||
objectByState.set(card[fieldName], oldArr)
|
||||
setGroupByValues(groupByDocs, dragCardState, oldArr)
|
||||
}
|
||||
card[fieldName] = state._id
|
||||
const arr = objectByState.get(card[fieldName]) ?? []
|
||||
arr.push(card)
|
||||
objectByState.set(card[fieldName], arr)
|
||||
objectByState = objectByState
|
||||
|
||||
dragCardState = state
|
||||
const arr = getGroupByValues(groupByDocs, state) ?? []
|
||||
arr.push(dragCard)
|
||||
setGroupByValues(groupByDocs, state, arr)
|
||||
|
||||
groupByDocs = groupByDocs
|
||||
}
|
||||
}
|
||||
function panelDragLeave (event: Event | undefined, state: CategoryType): void {
|
||||
event?.preventDefault()
|
||||
if (dragCard !== undefined && state !== dragCardInitialState) {
|
||||
// We need to restore original position
|
||||
const oldArr = getGroupByValues(groupByDocs, state)
|
||||
const index = oldArr.findIndex((p) => p._id === dragCard?._id)
|
||||
if (index !== -1) {
|
||||
oldArr.splice(index, 1)
|
||||
setGroupByValues(groupByDocs, state, oldArr)
|
||||
}
|
||||
|
||||
if (dragCardInitialPosition !== undefined) {
|
||||
const newArr = getGroupByValues(groupByDocs, dragCardInitialState)
|
||||
newArr.splice(dragCardInitialPosition, 0, dragCard)
|
||||
setGroupByValues(groupByDocs, dragCardInitialPosition, newArr)
|
||||
}
|
||||
|
||||
groupByDocs = groupByDocs
|
||||
}
|
||||
}
|
||||
|
||||
@ -137,10 +135,14 @@
|
||||
return false
|
||||
}
|
||||
|
||||
function cardDragOver (evt: CardDragEvent, object: Item): void {
|
||||
function cardDragOver (evt: CardDragEvent, object: Item, state: CategoryType): void {
|
||||
if (dragCard !== undefined && !dontUpdateRank) {
|
||||
const updates = getUpdateProps(dragCard, state)
|
||||
if (updates === undefined) {
|
||||
return
|
||||
}
|
||||
if (object._id !== dragCard._id) {
|
||||
let arr = objectByState.get((object as any)[fieldName]) ?? []
|
||||
let arr = getGroupByValues(groupByDocs, state) ?? []
|
||||
const dragCardIndex = arr.findIndex((p) => p._id === dragCard?._id)
|
||||
const targetIndex = arr.findIndex((p) => p._id === object._id)
|
||||
if (
|
||||
@ -150,15 +152,15 @@
|
||||
) {
|
||||
arr.splice(dragCardIndex, 1)
|
||||
arr = [...arr.slice(0, targetIndex), dragCard, ...arr.slice(targetIndex)]
|
||||
objectByState.set((object as any)[fieldName], arr)
|
||||
objectByState = objectByState
|
||||
setGroupByValues(groupByDocs, state, arr)
|
||||
groupByDocs = groupByDocs
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
function cardDrop (evt: CardDragEvent, object: Item): void {
|
||||
function cardDrop (evt: CardDragEvent, object: Item, state: CategoryType): void {
|
||||
if (!dontUpdateRank && dragCard !== undefined) {
|
||||
const arr = objectByState.get((object as any)[fieldName]) ?? []
|
||||
const arr = getGroupByValues(groupByDocs, state) ?? []
|
||||
const s = arr.findIndex((p) => p._id === dragCard?._id)
|
||||
if (s !== -1) {
|
||||
const newRank = calcRank(arr[s - 1], arr[s + 1])
|
||||
@ -167,9 +169,12 @@
|
||||
}
|
||||
isDragging = false
|
||||
}
|
||||
function onDragStart (object: Item, state: TypeState): void {
|
||||
dragCardInitialState = state._id
|
||||
function onDragStart (object: Item, state: CategoryType): void {
|
||||
dragCardInitialState = state
|
||||
dragCardState = state
|
||||
dragCardInitialRank = object.rank
|
||||
const items = getGroupByValues(groupByDocs, state) ?? []
|
||||
dragCardInitialPosition = items.findIndex((p) => p._id === object._id)
|
||||
dragCard = object
|
||||
isDragging = true
|
||||
dispatch('obj-focus', object)
|
||||
@ -184,19 +189,31 @@
|
||||
const stateRefs: HTMLElement[] = []
|
||||
const stateRows: KanbanRow[] = []
|
||||
|
||||
$: stateRefs.length = states.length
|
||||
$: stateRows.length = states.length
|
||||
$: stateRefs.length = categories.length
|
||||
$: stateRows.length = categories.length
|
||||
|
||||
function scrollInto (statePos: number, obj: Item): void {
|
||||
stateRefs[statePos]?.scrollIntoView({ behavior: 'auto', block: 'nearest' })
|
||||
stateRows[statePos]?.scroll(obj)
|
||||
}
|
||||
|
||||
function getState (doc: Item): number {
|
||||
let pos = 0
|
||||
for (const st of categories) {
|
||||
const stateObjs = getGroupByValues(groupByDocs, st) ?? []
|
||||
if (stateObjs.findIndex((it) => it._id === doc._id) !== -1) {
|
||||
return pos
|
||||
}
|
||||
pos++
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
export function select (offset: 1 | -1 | 0, of?: Doc, dir?: 'vertical' | 'horizontal'): void {
|
||||
let pos = (of !== undefined ? objects.findIndex((it) => it._id === of._id) : selection) ?? -1
|
||||
if (pos === -1) {
|
||||
for (const st of states) {
|
||||
const stateObjs = objectByState.get(st) ?? []
|
||||
for (const st of categories) {
|
||||
const stateObjs = getGroupByValues(groupByDocs, st) ?? []
|
||||
if (stateObjs.length > 0) {
|
||||
pos = objects.findIndex((it) => it._id === stateObjs[0]._id)
|
||||
break
|
||||
@ -215,12 +232,12 @@
|
||||
if (obj === undefined) {
|
||||
return
|
||||
}
|
||||
const fState = (obj as any)[fieldName]
|
||||
let objState = states.findIndex((it) => it._id === fState)
|
||||
|
||||
let objState = getState(obj)
|
||||
if (objState === -1) {
|
||||
return
|
||||
}
|
||||
const stateObjs = objectByState.get(states[objState]) ?? []
|
||||
const stateObjs = getGroupByValues(groupByDocs, categories.indexOf(objState)) ?? []
|
||||
const statePos = stateObjs.findIndex((it) => it._id === obj._id)
|
||||
if (statePos === undefined) {
|
||||
return
|
||||
@ -235,7 +252,7 @@
|
||||
} else {
|
||||
while (objState > 0) {
|
||||
objState--
|
||||
const nstateObjs = objectByState.get(states[objState]) ?? []
|
||||
const nstateObjs = getGroupByValues(groupByDocs, categories[objState]) ?? []
|
||||
if (nstateObjs.length > 0) {
|
||||
const obj = nstateObjs[statePos] ?? nstateObjs[nstateObjs.length - 1]
|
||||
scrollInto(objState, obj)
|
||||
@ -252,9 +269,9 @@
|
||||
dispatch('obj-focus', obj)
|
||||
return
|
||||
} else {
|
||||
while (objState < states.length - 1) {
|
||||
while (objState < categories.length - 1) {
|
||||
objState++
|
||||
const nstateObjs = objectByState.get(states[objState]) ?? []
|
||||
const nstateObjs = getGroupByValues(groupByDocs, categories[objState]) ?? []
|
||||
if (nstateObjs.length > 0) {
|
||||
const obj = nstateObjs[statePos] ?? nstateObjs[nstateObjs.length - 1]
|
||||
scrollInto(objState, obj)
|
||||
@ -265,7 +282,7 @@
|
||||
}
|
||||
}
|
||||
if (offset === 0) {
|
||||
scrollInto(objState, obj)
|
||||
// scrollInto(objState, obj)
|
||||
dispatch('obj-focus', obj)
|
||||
}
|
||||
}
|
||||
@ -288,29 +305,20 @@
|
||||
<div class="kanban-container top-divider">
|
||||
<ScrollBox>
|
||||
<div class="kanban-content">
|
||||
{#each states as state, si (state._id)}
|
||||
{@const stateObjects = objectByState.get(state._id) ?? []}
|
||||
{#each categories as state, si (state)}
|
||||
{@const stateObjects = getGroupByValues(groupByDocs, state)}
|
||||
|
||||
<div
|
||||
class="panel-container step-lr75"
|
||||
bind:this={stateRefs[si]}
|
||||
on:dragover={(event) => panelDragOver(event, state)}
|
||||
on:drop={() => {
|
||||
move(state._id)
|
||||
move(state)
|
||||
isDragging = false
|
||||
}}
|
||||
>
|
||||
{#if $$slots.header !== undefined}
|
||||
<slot name="header" state={toAny(state)} count={stateObjects.length} />
|
||||
{:else}
|
||||
<div class="header">
|
||||
<div class="bar" style="background-color: {getPlatformColor(state.color)}" />
|
||||
<div class="flex-between label">
|
||||
<div>
|
||||
<span class="lines-limit-2">{state.title}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
<Scroller padding={'.5rem 0'} on:dragover on:drop>
|
||||
<slot name="beforeCard" {state} />
|
||||
@ -324,8 +332,8 @@
|
||||
{selection}
|
||||
{checkedSet}
|
||||
{state}
|
||||
{cardDragOver}
|
||||
{cardDrop}
|
||||
cardDragOver={(evt, obj) => cardDragOver(evt, obj, state)}
|
||||
cardDrop={(evt, obj) => cardDrop(evt, obj, state)}
|
||||
{onDragStart}
|
||||
{showMenu}
|
||||
>
|
||||
|
@ -13,10 +13,11 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { Doc, Ref } from '@hcengineering/core'
|
||||
import { CategoryType, Doc, Ref } from '@hcengineering/core'
|
||||
import ui, { Button, IconMoreH } from '@hcengineering/ui'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import { slide } from 'svelte/transition'
|
||||
import { CardDragEvent, Item, TypeState } from '../types'
|
||||
import { CardDragEvent, Item } from '../types'
|
||||
|
||||
export let stateObjects: Item[]
|
||||
export let isDragging: boolean
|
||||
@ -24,11 +25,11 @@
|
||||
export let objects: Item[]
|
||||
export let selection: number | undefined = undefined
|
||||
export let checkedSet: Set<Ref<Doc>>
|
||||
export let state: TypeState
|
||||
export let state: CategoryType
|
||||
|
||||
export let cardDragOver: (evt: CardDragEvent, object: Item) => void
|
||||
export let cardDrop: (evt: CardDragEvent, object: Item) => void
|
||||
export let onDragStart: (object: Item, state: TypeState) => void
|
||||
export let onDragStart: (object: Item, state: CategoryType) => void
|
||||
export let showMenu: (evt: MouseEvent, object: Item) => void
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
@ -48,9 +49,13 @@
|
||||
stateRefs[pos]?.scrollIntoView({ behavior: 'auto', block: 'nearest' })
|
||||
}
|
||||
}
|
||||
|
||||
let limit = 50
|
||||
|
||||
$: limitedObjects = stateObjects.slice(0, limit)
|
||||
</script>
|
||||
|
||||
{#each stateObjects as object, i}
|
||||
{#each limitedObjects as object, i (object._id)}
|
||||
{@const dragged = isDragging && object._id === dragCard?._id}
|
||||
<div
|
||||
bind:this={stateRefs[i]}
|
||||
@ -80,6 +85,23 @@
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
{#if stateObjects.length > limitedObjects.length}
|
||||
<div class="step-tb75">
|
||||
<div class="card-container h-18 flex-row-center flex-between p-4">
|
||||
<span class="p-1">
|
||||
{limitedObjects.length}/{stateObjects.length}
|
||||
</span>
|
||||
<Button
|
||||
size={'small'}
|
||||
icon={IconMoreH}
|
||||
label={ui.string.ShowMore}
|
||||
on:click={() => {
|
||||
limit = limit + 20
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style lang="scss">
|
||||
.card-container {
|
||||
|
@ -22,7 +22,7 @@ export interface TypeState {
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export type Item = DocWithRank & { state: StateType, doneState: StateType | null }
|
||||
export type Item = DocWithRank
|
||||
|
||||
/**
|
||||
* @public
|
||||
|
@ -131,7 +131,7 @@ export function Prop (type: Type<PropertyType>, label: IntlString, extra: Partia
|
||||
modifiedBy: core.account.System,
|
||||
modifiedOn: Date.now(),
|
||||
objectSpace: core.space.Model,
|
||||
objectId: propertyKey as Ref<Attribute<PropertyType>>,
|
||||
objectId: extra._id ?? (propertyKey as Ref<Attribute<PropertyType>>),
|
||||
objectClass: core.class.Attribute,
|
||||
attributes: {
|
||||
...extra,
|
||||
@ -240,7 +240,8 @@ function generateIds (objectId: Ref<Doc>, txes: TxCreateDoc<Attribute<PropertyTy
|
||||
return txes.map((tx) => {
|
||||
const withId = {
|
||||
...tx,
|
||||
objectId: `${objectId}_${tx.objectId}`
|
||||
// Do not override custom attribute id if specified
|
||||
objectId: tx.objectId !== tx.attributes.name ? tx.objectId : `${objectId}_${tx.objectId}`
|
||||
}
|
||||
withId.attributes.attributeOf = objectId as Ref<Class<Obj>>
|
||||
return withId
|
||||
|
234
packages/presentation/src/pipeline.ts
Normal file
234
packages/presentation/src/pipeline.ts
Normal file
@ -0,0 +1,234 @@
|
||||
import {
|
||||
Class,
|
||||
Client,
|
||||
Doc,
|
||||
DocumentQuery,
|
||||
FindOptions,
|
||||
FindResult,
|
||||
Hierarchy,
|
||||
ModelDb,
|
||||
Ref,
|
||||
Tx,
|
||||
TxResult,
|
||||
WithLookup
|
||||
} from '@hcengineering/core'
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export interface PresentationMiddleware {
|
||||
next?: PresentationMiddleware
|
||||
|
||||
tx: (tx: Tx) => Promise<TxResult>
|
||||
|
||||
notifyTx: (tx: Tx) => Promise<void>
|
||||
|
||||
findAll: <T extends Doc>(
|
||||
_class: Ref<Class<T>>,
|
||||
query: DocumentQuery<T>,
|
||||
options?: FindOptions<T>
|
||||
) => Promise<FindResult<T>>
|
||||
|
||||
findOne: <T extends Doc>(
|
||||
_class: Ref<Class<T>>,
|
||||
query: DocumentQuery<T>,
|
||||
options?: FindOptions<T>
|
||||
) => Promise<WithLookup<T> | undefined>
|
||||
|
||||
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>
|
||||
}>
|
||||
|
||||
close: () => Promise<void>
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export type PresentationMiddlewareCreator = (client: Client, next?: PresentationMiddleware) => PresentationMiddleware
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export interface PresentationPipeline extends Client, Exclude<PresentationMiddleware, 'next'> {
|
||||
close: () => Promise<void>
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export class PresentationPipelineImpl implements PresentationPipeline {
|
||||
private head: PresentationMiddleware | undefined
|
||||
|
||||
private constructor (readonly client: Client) {}
|
||||
|
||||
getHierarchy (): Hierarchy {
|
||||
return this.client.getHierarchy()
|
||||
}
|
||||
|
||||
getModel (): ModelDb {
|
||||
return this.client.getModel()
|
||||
}
|
||||
|
||||
async notifyTx (tx: Tx): Promise<void> {
|
||||
await this.head?.notifyTx(tx)
|
||||
}
|
||||
|
||||
static create (client: Client, constructors: PresentationMiddlewareCreator[]): PresentationPipeline {
|
||||
const pipeline = new PresentationPipelineImpl(client)
|
||||
pipeline.head = pipeline.buildChain(constructors)
|
||||
return pipeline
|
||||
}
|
||||
|
||||
private buildChain (constructors: PresentationMiddlewareCreator[]): PresentationMiddleware | undefined {
|
||||
let current: PresentationMiddleware | undefined
|
||||
for (let index = constructors.length - 1; index >= 0; index--) {
|
||||
const element = constructors[index]
|
||||
current = element(this.client, current)
|
||||
}
|
||||
return current
|
||||
}
|
||||
|
||||
async findAll<T extends Doc>(
|
||||
_class: Ref<Class<T>>,
|
||||
query: DocumentQuery<T>,
|
||||
options?: FindOptions<T>
|
||||
): Promise<FindResult<T>> {
|
||||
return this.head !== undefined
|
||||
? await this.head.findAll(_class, query, options)
|
||||
: await this.client.findAll(_class, query, options)
|
||||
}
|
||||
|
||||
async findOne<T extends Doc>(
|
||||
_class: Ref<Class<T>>,
|
||||
query: DocumentQuery<T>,
|
||||
options?: FindOptions<T>
|
||||
): Promise<WithLookup<T> | undefined> {
|
||||
return this.head !== undefined
|
||||
? await this.head.findOne(_class, query, options)
|
||||
: await this.client.findOne(_class, query, options)
|
||||
}
|
||||
|
||||
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>
|
||||
}> {
|
||||
return this.head !== undefined
|
||||
? await this.head.subscribe(_class, query, options, refresh)
|
||||
: { unsubscribe: () => {} }
|
||||
}
|
||||
|
||||
async tx (tx: Tx): Promise<TxResult> {
|
||||
if (this.head === undefined) {
|
||||
return await this.client.tx(tx)
|
||||
} else {
|
||||
return await this.head.tx(tx)
|
||||
}
|
||||
}
|
||||
|
||||
async close (): Promise<void> {
|
||||
await this.head?.close()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export abstract class BasePresentationMiddleware {
|
||||
constructor (protected readonly client: Client, readonly next?: PresentationMiddleware) {}
|
||||
|
||||
async provideNotifyTx (tx: Tx): Promise<void> {
|
||||
await this.next?.notifyTx(tx)
|
||||
}
|
||||
|
||||
async provideClose (): Promise<void> {
|
||||
await this.next?.close()
|
||||
}
|
||||
|
||||
async findAll<T extends Doc>(
|
||||
_class: Ref<Class<T>>,
|
||||
query: DocumentQuery<T>,
|
||||
options?: FindOptions<T>
|
||||
): Promise<FindResult<T>> {
|
||||
return await this.provideFindAll(_class, query, options)
|
||||
}
|
||||
|
||||
async findOne<T extends Doc>(
|
||||
_class: Ref<Class<T>>,
|
||||
query: DocumentQuery<T>,
|
||||
options?: FindOptions<T>
|
||||
): Promise<WithLookup<T> | undefined> {
|
||||
return await this.provideFindOne(_class, query, options)
|
||||
}
|
||||
|
||||
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>
|
||||
}> {
|
||||
return await this.provideSubscribe(_class, query, options, refresh)
|
||||
}
|
||||
|
||||
protected async provideTx (tx: Tx): Promise<TxResult> {
|
||||
if (this.next !== undefined) {
|
||||
return await this.next.tx(tx)
|
||||
}
|
||||
return await this.client.tx(tx)
|
||||
}
|
||||
|
||||
protected async provideFindAll<T extends Doc>(
|
||||
_class: Ref<Class<T>>,
|
||||
query: DocumentQuery<T>,
|
||||
options?: FindOptions<T>
|
||||
): Promise<FindResult<T>> {
|
||||
if (this.next !== undefined) {
|
||||
return await this.next.findAll(_class, query, options)
|
||||
}
|
||||
return await this.client.findAll(_class, query, options)
|
||||
}
|
||||
|
||||
protected async provideFindOne<T extends Doc>(
|
||||
_class: Ref<Class<T>>,
|
||||
query: DocumentQuery<T>,
|
||||
options?: FindOptions<T>
|
||||
): Promise<WithLookup<T> | undefined> {
|
||||
if (this.next !== undefined) {
|
||||
return await this.next.findOne(_class, query, options)
|
||||
}
|
||||
return await this.client.findOne(_class, query, options)
|
||||
}
|
||||
|
||||
protected async provideSubscribe<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>
|
||||
}> {
|
||||
if (this.next !== undefined) {
|
||||
return await this.next.subscribe(_class, query, options, refresh)
|
||||
}
|
||||
return { unsubscribe: () => {} }
|
||||
}
|
||||
}
|
299
packages/presentation/src/status.ts
Normal file
299
packages/presentation/src/status.ts
Normal file
@ -0,0 +1,299 @@
|
||||
//
|
||||
// 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 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>> = []
|
||||
statusFields.push(attr)
|
||||
const v = (query as any)[attr.name]
|
||||
|
||||
if (v !== undefined) {
|
||||
// Only add filter if we have filer inside.
|
||||
if (v?.$in !== undefined) {
|
||||
target.push(...v.$in)
|
||||
} else {
|
||||
target.push(v)
|
||||
}
|
||||
|
||||
// Find all similar name statues for same attribute name.
|
||||
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
|
||||
)
|
||||
if (statuses !== undefined) {
|
||||
target.push(...statuses.map((it) => it._id))
|
||||
}
|
||||
}
|
||||
}
|
||||
target = target.filter((it, idx, arr) => arr.indexOf(it) === idx)
|
||||
;(query as any)[attr.name] = { $in: target }
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
@ -43,10 +43,14 @@ import view, { AttributeEditor } from '@hcengineering/view'
|
||||
import { deepEqual } from 'fast-equals'
|
||||
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
|
||||
let pipeline: PresentationPipeline
|
||||
|
||||
const txListeners: Array<(tx: Tx) => void> = []
|
||||
|
||||
@ -68,7 +72,7 @@ export function removeTxListener (l: (tx: Tx) => void): void {
|
||||
}
|
||||
|
||||
class UIClient extends TxOperations implements Client {
|
||||
constructor (client: Client, private readonly liveQuery: LQ) {
|
||||
constructor (client: Client, private readonly liveQuery: Client) {
|
||||
super(client, getCurrentAccount()._id)
|
||||
}
|
||||
|
||||
@ -89,7 +93,7 @@ class UIClient extends TxOperations implements Client {
|
||||
}
|
||||
|
||||
override async tx (tx: Tx): Promise<TxResult> {
|
||||
return await super.tx(tx)
|
||||
return await this.client.tx(tx)
|
||||
}
|
||||
}
|
||||
|
||||
@ -103,28 +107,36 @@ export function getClient (): TxOperations {
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export function setClient (_client: Client): void {
|
||||
export async function setClient (_client: Client): Promise<void> {
|
||||
if (liveQuery !== undefined) {
|
||||
void liveQuery.close()
|
||||
await liveQuery.close()
|
||||
}
|
||||
if (pipeline !== undefined) {
|
||||
await pipeline.close()
|
||||
}
|
||||
pipeline = PresentationPipelineImpl.create(_client, [StatusMiddleware.create])
|
||||
|
||||
const needRefresh = liveQuery !== undefined
|
||||
liveQuery = new LQ(_client)
|
||||
client = new UIClient(_client, liveQuery)
|
||||
liveQuery = new LQ(pipeline)
|
||||
client = new UIClient(pipeline, liveQuery)
|
||||
|
||||
_client.notify = (tx: Tx) => {
|
||||
pipeline.notifyTx(tx).catch((err) => console.log(err))
|
||||
|
||||
liveQuery.tx(tx).catch((err) => console.log(err))
|
||||
|
||||
txListeners.forEach((it) => it(tx))
|
||||
}
|
||||
if (needRefresh) {
|
||||
refreshClient()
|
||||
await refreshClient()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export function refreshClient (): void {
|
||||
void liveQuery?.refreshConnect()
|
||||
export async function refreshClient (): Promise<void> {
|
||||
await liveQuery?.refreshConnect()
|
||||
for (const q of globalQueries) {
|
||||
q.refreshClient()
|
||||
}
|
||||
@ -140,10 +152,12 @@ export class LiveQuery {
|
||||
private oldQuery: DocumentQuery<Doc> | undefined
|
||||
private oldOptions: FindOptions<Doc> | undefined
|
||||
private oldCallback: ((result: FindResult<any>) => void) | undefined
|
||||
private reqId = 0
|
||||
unsubscribe = () => {}
|
||||
clientRecreated = false
|
||||
|
||||
constructor (dontDestroy: boolean = false) {
|
||||
if (!dontDestroy) {
|
||||
constructor (noDestroy: boolean = false) {
|
||||
if (!noDestroy) {
|
||||
onDestroy(() => {
|
||||
this.unsubscribe()
|
||||
})
|
||||
@ -158,43 +172,58 @@ export class LiveQuery {
|
||||
callback: (result: FindResult<T>) => void,
|
||||
options?: FindOptions<T>
|
||||
): boolean {
|
||||
if (!this.needUpdate(_class, query, callback, options)) {
|
||||
if (!this.needUpdate(_class, query, callback, options) && !this.clientRecreated) {
|
||||
return false
|
||||
}
|
||||
return this.doQuery<T>(_class, query, callback, options)
|
||||
// One time refresh in case of client recreation
|
||||
this.clientRecreated = false
|
||||
void this.doQuery<T>(_class, query, callback, options)
|
||||
return true
|
||||
}
|
||||
|
||||
private doQuery<T extends Doc>(
|
||||
private async doQuery<T extends Doc>(
|
||||
_class: Ref<Class<T>>,
|
||||
query: DocumentQuery<T>,
|
||||
callback: (result: FindResult<T>) => void,
|
||||
options: FindOptions<T> | undefined
|
||||
): boolean {
|
||||
): Promise<void> {
|
||||
const id = ++this.reqId
|
||||
const piplineQuery = await pipeline.subscribe(_class, query, options, () => {
|
||||
// Refresh query if pipeline decide it is required.
|
||||
this.refreshClient()
|
||||
})
|
||||
if (id !== this.reqId) {
|
||||
// If we have one more request after this one, no need to do something.
|
||||
piplineQuery.unsubscribe()
|
||||
return
|
||||
}
|
||||
|
||||
this.unsubscribe()
|
||||
this.oldCallback = callback
|
||||
this.oldClass = _class
|
||||
this.oldOptions = options
|
||||
this.oldQuery = query
|
||||
|
||||
const unsub = liveQuery.query(_class, query, callback, options)
|
||||
const unsub = liveQuery.query(_class, piplineQuery.query ?? query, callback, piplineQuery.options ?? options)
|
||||
this.unsubscribe = () => {
|
||||
unsub()
|
||||
piplineQuery.unsubscribe()
|
||||
this.oldCallback = undefined
|
||||
this.oldClass = undefined
|
||||
this.oldOptions = undefined
|
||||
this.oldQuery = undefined
|
||||
this.unsubscribe = () => {}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
refreshClient (): void {
|
||||
this.clientRecreated = true
|
||||
if (this.oldClass !== undefined && this.oldQuery !== undefined && this.oldCallback !== undefined) {
|
||||
const _class = this.oldClass
|
||||
const query = this.oldQuery
|
||||
const callback = this.oldCallback
|
||||
const options = this.oldOptions
|
||||
this.doQuery(_class, query, callback, options)
|
||||
void this.doQuery(_class, query, callback, options)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -24,8 +24,10 @@ import core, {
|
||||
FindOptions,
|
||||
findProperty,
|
||||
FindResult,
|
||||
generateId,
|
||||
getObjectValue,
|
||||
Hierarchy,
|
||||
IndexingUpdateEvent,
|
||||
Lookup,
|
||||
LookupData,
|
||||
matchQuery,
|
||||
@ -43,7 +45,9 @@ import core, {
|
||||
TxRemoveDoc,
|
||||
TxResult,
|
||||
TxUpdateDoc,
|
||||
WithLookup
|
||||
TxWorkspaceEvent,
|
||||
WithLookup,
|
||||
WorkspaceEvent
|
||||
} from '@hcengineering/core'
|
||||
import { deepEqual } from 'fast-equals'
|
||||
|
||||
@ -57,7 +61,7 @@ interface Query {
|
||||
result: Doc[] | Promise<Doc[]>
|
||||
options?: FindOptions<Doc>
|
||||
total: number
|
||||
callbacks: Callback[]
|
||||
callbacks: Map<string, Callback>
|
||||
}
|
||||
|
||||
/**
|
||||
@ -138,12 +142,10 @@ export class LiveQuery extends TxProcessor implements Client {
|
||||
options?: FindOptions<T>
|
||||
): Query {
|
||||
const callback: () => void = () => {}
|
||||
const q = this.createQuery(_class, query, callback, options)
|
||||
const index = q.callbacks.indexOf(callback as (result: Doc[]) => void)
|
||||
if (index !== -1) {
|
||||
q.callbacks.splice(index, 1)
|
||||
}
|
||||
if (q.callbacks.length === 0) {
|
||||
const callbackId = generateId()
|
||||
const q = this.createQuery(_class, query, { callback, callbackId }, options)
|
||||
q.callbacks.delete(callbackId)
|
||||
if (q.callbacks.size === 0) {
|
||||
this.queue.push(q)
|
||||
}
|
||||
return q
|
||||
@ -213,7 +215,7 @@ export class LiveQuery extends TxProcessor implements Client {
|
||||
}
|
||||
|
||||
private removeFromQueue (q: Query): boolean {
|
||||
if (q.callbacks.length === 0) {
|
||||
if (q.callbacks.size === 0) {
|
||||
const queueIndex = this.queue.indexOf(q)
|
||||
if (queueIndex !== -1) {
|
||||
this.queue.splice(queueIndex, 1)
|
||||
@ -223,8 +225,14 @@ export class LiveQuery extends TxProcessor implements Client {
|
||||
return false
|
||||
}
|
||||
|
||||
private pushCallback (q: Query, callback: (result: Doc[]) => void): void {
|
||||
q.callbacks.push(callback)
|
||||
private pushCallback (
|
||||
q: Query,
|
||||
callback: {
|
||||
callback: (result: Doc[]) => void
|
||||
callbackId: string
|
||||
}
|
||||
): void {
|
||||
q.callbacks.set(callback.callbackId, callback.callback)
|
||||
setTimeout(async () => {
|
||||
if (q !== undefined) {
|
||||
await this.callback(q)
|
||||
@ -235,7 +243,10 @@ export class LiveQuery extends TxProcessor implements Client {
|
||||
private getQuery<T extends Doc>(
|
||||
_class: Ref<Class<T>>,
|
||||
query: DocumentQuery<T>,
|
||||
callback: (result: Doc[]) => void,
|
||||
callback: {
|
||||
callback: (result: Doc[]) => void
|
||||
callbackId: string
|
||||
},
|
||||
options?: FindOptions<T>
|
||||
): Query | undefined {
|
||||
const current = this.findQuery(_class, query, options)
|
||||
@ -250,7 +261,7 @@ export class LiveQuery extends TxProcessor implements Client {
|
||||
private createQuery<T extends Doc>(
|
||||
_class: Ref<Class<T>>,
|
||||
query: DocumentQuery<T>,
|
||||
callback: (result: FindResult<T>) => void,
|
||||
callback: { callback: (result: FindResult<T>) => void, callbackId: string },
|
||||
options?: FindOptions<T>
|
||||
): Query {
|
||||
const queries = this.queries.get(_class) ?? []
|
||||
@ -261,8 +272,9 @@ export class LiveQuery extends TxProcessor implements Client {
|
||||
result,
|
||||
total: 0,
|
||||
options: options as FindOptions<Doc>,
|
||||
callbacks: [callback as (result: Doc[]) => void]
|
||||
callbacks: new Map()
|
||||
}
|
||||
q.callbacks.set(callback.callbackId, callback.callback as unknown as Callback)
|
||||
queries.push(q)
|
||||
result
|
||||
.then(async (result) => {
|
||||
@ -303,16 +315,14 @@ export class LiveQuery extends TxProcessor implements Client {
|
||||
modifiedOn: 1
|
||||
}
|
||||
}
|
||||
const callbackId = generateId()
|
||||
const q =
|
||||
this.getQuery(_class, query, callback as (result: Doc[]) => void, options) ??
|
||||
this.createQuery(_class, query, callback, options)
|
||||
this.getQuery(_class, query, { callback: callback as (result: Doc[]) => void, callbackId }, options) ??
|
||||
this.createQuery(_class, query, { callback, callbackId }, options)
|
||||
|
||||
return () => {
|
||||
const index = q.callbacks.indexOf(callback as (result: Doc[]) => void)
|
||||
if (index !== -1) {
|
||||
q.callbacks.splice(index, 1)
|
||||
}
|
||||
if (q.callbacks.length === 0) {
|
||||
q.callbacks.delete(callbackId)
|
||||
if (q.callbacks.size === 0) {
|
||||
this.queue.push(q)
|
||||
}
|
||||
}
|
||||
@ -329,12 +339,16 @@ export class LiveQuery extends TxProcessor implements Client {
|
||||
return true
|
||||
} else {
|
||||
const pos = q.result.findIndex((p) => p._id === _id)
|
||||
q.result.splice(pos, 1)
|
||||
q.total--
|
||||
if (pos !== -1) {
|
||||
q.result.splice(pos, 1)
|
||||
q.total--
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const pos = q.result.findIndex((p) => p._id === _id)
|
||||
q.result[pos] = match
|
||||
if (pos !== -1) {
|
||||
q.result[pos] = match
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@ -772,7 +786,7 @@ export class LiveQuery extends TxProcessor implements Client {
|
||||
q.result = await q.result
|
||||
}
|
||||
const result = q.result
|
||||
q.callbacks.forEach((callback) => {
|
||||
Array.from(q.callbacks.values()).forEach((callback) => {
|
||||
callback(toFindResult(this.clone(result), q.total))
|
||||
})
|
||||
}
|
||||
@ -938,9 +952,42 @@ export class LiveQuery extends TxProcessor implements Client {
|
||||
}
|
||||
|
||||
async tx (tx: Tx): Promise<TxResult> {
|
||||
if (tx._class === core.class.TxWorkspaceEvent) {
|
||||
await this.checkUpdateFulltextQueries(tx)
|
||||
return {}
|
||||
}
|
||||
return await super.tx(tx)
|
||||
}
|
||||
|
||||
private async checkUpdateFulltextQueries (tx: Tx): Promise<void> {
|
||||
const evt = tx as TxWorkspaceEvent
|
||||
if (evt.event === WorkspaceEvent.IndexingUpdate) {
|
||||
const indexingParam = evt.params as IndexingUpdateEvent
|
||||
for (const q of [...this.queue]) {
|
||||
if (indexingParam._class.includes(q._class) && q.query.$search !== undefined) {
|
||||
if (!(await this.removeFromQueue(q))) {
|
||||
try {
|
||||
await this.refresh(q)
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const v of this.queries.values()) {
|
||||
for (const q of v) {
|
||||
if (indexingParam._class.includes(q._class) && q.query.$search !== undefined) {
|
||||
try {
|
||||
await this.refresh(q)
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async __updateLookup (q: Query, updatedDoc: WithLookup<Doc>, ops: any): Promise<void> {
|
||||
for (const key in ops) {
|
||||
if (!key.startsWith('$')) {
|
||||
|
@ -15,19 +15,32 @@
|
||||
-->
|
||||
<script lang="ts">
|
||||
import board, { Card } from '@hcengineering/board'
|
||||
import { Class, Doc, DocumentQuery, FindOptions, Ref, SortingOrder, WithLookup } from '@hcengineering/core'
|
||||
import {
|
||||
CategoryType,
|
||||
Class,
|
||||
Doc,
|
||||
DocumentQuery,
|
||||
DocumentUpdate,
|
||||
FindOptions,
|
||||
Ref,
|
||||
SortingOrder,
|
||||
WithLookup
|
||||
} from '@hcengineering/core'
|
||||
import { Kanban as KanbanUI } from '@hcengineering/kanban'
|
||||
import { createQuery, getClient } from '@hcengineering/presentation'
|
||||
import type { Kanban, SpaceWithStates, State } from '@hcengineering/task'
|
||||
import type { DocWithRank, Kanban, SpaceWithStates, State } from '@hcengineering/task'
|
||||
import task, { calcRank } from '@hcengineering/task'
|
||||
import { getEventPositionElement, showPopup } from '@hcengineering/ui'
|
||||
import {
|
||||
ActionContext,
|
||||
ContextMenu,
|
||||
focusStore,
|
||||
getGroupByValues,
|
||||
groupBy,
|
||||
ListSelectionProvider,
|
||||
SelectDirection,
|
||||
selectionStore
|
||||
selectionStore,
|
||||
setGroupByValues
|
||||
} from '@hcengineering/view-resources'
|
||||
import { onMount } from 'svelte'
|
||||
import AddCard from './add-card/AddCard.svelte'
|
||||
@ -72,7 +85,8 @@
|
||||
async function addItem (title: any) {
|
||||
const lastOne = await client.findOne(task.class.State, {}, { sort: { rank: SortingOrder.Descending } })
|
||||
await client.createDoc(task.class.State, space, {
|
||||
title,
|
||||
name: title,
|
||||
ofAttribute: task.attribute.State,
|
||||
color: 9,
|
||||
rank: calcRank(lastOne, undefined)
|
||||
})
|
||||
@ -97,6 +111,33 @@
|
||||
}
|
||||
|
||||
$: resultQuery = { ...query, doneState: null, isArchived: { $nin: [true] }, space }
|
||||
|
||||
const cardQuery = createQuery()
|
||||
let cards: DocWithRank[] = []
|
||||
|
||||
$: cardQuery.query<DocWithRank>(
|
||||
_class,
|
||||
resultQuery,
|
||||
(result) => {
|
||||
cards = result
|
||||
},
|
||||
{
|
||||
...options
|
||||
}
|
||||
)
|
||||
$: groupByDocs = groupBy(cards, 'state')
|
||||
|
||||
const getUpdateProps = (doc: Doc, category: CategoryType): DocumentUpdate<DocWithRank> | undefined => {
|
||||
const groupValue =
|
||||
typeof category === 'object' ? category.values.find((it) => it.space === doc.space)?._id : category
|
||||
if (groupValue === undefined) {
|
||||
return undefined
|
||||
}
|
||||
return {
|
||||
state: groupValue,
|
||||
space: doc.space
|
||||
} as any
|
||||
}
|
||||
</script>
|
||||
|
||||
<ActionContext
|
||||
@ -106,17 +147,18 @@
|
||||
/>
|
||||
<KanbanUI
|
||||
bind:this={kanbanUI}
|
||||
{_class}
|
||||
{options}
|
||||
query={resultQuery}
|
||||
{states}
|
||||
fieldName={'state'}
|
||||
objects={cards}
|
||||
getGroupByValues={(groupByDocs, category) => getGroupByValues(groupByDocs, category)}
|
||||
{setGroupByValues}
|
||||
categories={states.map((it) => it._id)}
|
||||
on:content={(evt) => {
|
||||
listProvider.update(evt.detail)
|
||||
}}
|
||||
on:obj-focus={(evt) => {
|
||||
listProvider.updateFocus(evt.detail)
|
||||
}}
|
||||
{groupByDocs}
|
||||
{getUpdateProps}
|
||||
checked={$selectionStore ?? []}
|
||||
on:check={(evt) => {
|
||||
listProvider.updateSelection(evt.detail.docs, evt.detail.value)
|
||||
@ -137,10 +179,16 @@
|
||||
</svelte:fragment>
|
||||
|
||||
<svelte:fragment slot="header" let:state>
|
||||
<ListHeader {state} />
|
||||
{@const st = states.find((it) => it._id === state)}
|
||||
{#if st}
|
||||
<ListHeader state={st} />
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
|
||||
<svelte:fragment slot="afterCard" let:state={targetState}>
|
||||
<AddCard {space} state={targetState} />
|
||||
{@const st = states.find((it) => it._id === targetState)}
|
||||
{#if st}
|
||||
<AddCard {space} state={st} />
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
</KanbanUI>
|
||||
|
@ -1,6 +1,13 @@
|
||||
<script lang="ts">
|
||||
import { State } from '@hcengineering/task'
|
||||
import { Button, getEventPositionElement, getPlatformColor, IconMoreV, showPopup } from '@hcengineering/ui'
|
||||
import {
|
||||
Button,
|
||||
getEventPositionElement,
|
||||
getPlatformColor,
|
||||
getPlatformColorForText,
|
||||
IconMoreV,
|
||||
showPopup
|
||||
} from '@hcengineering/ui'
|
||||
import { ContextMenu } from '@hcengineering/view-resources'
|
||||
export let state: State
|
||||
|
||||
@ -10,12 +17,17 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex-col h-16">
|
||||
<div class="h-2 border-radius-1" style="background-color: {getPlatformColor(state.color)}" />
|
||||
<div class="flex-between h-full font-medium pr-2 pl-4">
|
||||
<span class="lines-limit-2">{state.title}</span>
|
||||
<div class="flex">
|
||||
<Button icon={IconMoreV} kind="transparent" on:click={showMenu} />
|
||||
{#if state}
|
||||
<div class="flex-col h-16">
|
||||
<div
|
||||
class="h-2 border-radius-1"
|
||||
style="background-color: {state.color ? getPlatformColor(state.color) : getPlatformColorForText(state.name)}"
|
||||
/>
|
||||
<div class="flex-between h-full font-medium pr-2 pl-4">
|
||||
<span class="lines-limit-2">{state.name}</span>
|
||||
<div class="flex">
|
||||
<Button icon={IconMoreV} kind="transparent" on:click={showMenu} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
@ -29,7 +29,9 @@ import core, {
|
||||
Tx,
|
||||
TxApplyIf,
|
||||
TxHandler,
|
||||
TxResult
|
||||
TxResult,
|
||||
TxWorkspaceEvent,
|
||||
WorkspaceEvent
|
||||
} from '@hcengineering/core'
|
||||
import {
|
||||
getMetadata,
|
||||
@ -167,7 +169,10 @@ class Connection implements ClientConnection {
|
||||
}
|
||||
} else {
|
||||
const tx = resp.result as Tx
|
||||
if (tx?._class === core.class.TxModelUpgrade) {
|
||||
if (
|
||||
(tx?._class === core.class.TxWorkspaceEvent && (tx as TxWorkspaceEvent).event === WorkspaceEvent.Upgrade) ||
|
||||
tx?._class === core.class.TxModelUpgrade
|
||||
) {
|
||||
console.log('Processing upgrade')
|
||||
websocket.send(
|
||||
serialize({
|
||||
|
@ -15,18 +15,18 @@
|
||||
<script lang="ts">
|
||||
import { CalendarMode } from '@hcengineering/calendar-resources'
|
||||
import { Employee, EmployeeAccount } from '@hcengineering/contact'
|
||||
import contact from '@hcengineering/contact-resources/src/plugin'
|
||||
import { DocumentQuery, getCurrentAccount, Ref } from '@hcengineering/core'
|
||||
import { Department, fromTzDate, Request, RequestType, Staff } from '@hcengineering/hr'
|
||||
import { createQuery, getClient } from '@hcengineering/presentation'
|
||||
import tracker, { Issue } from '@hcengineering/tracker'
|
||||
import { Label } from '@hcengineering/ui'
|
||||
import { groupBy } from '@hcengineering/view-resources'
|
||||
import hr from '../plugin'
|
||||
import { EmployeeReports, getEndDate, getStartDate } from '../utils'
|
||||
import MonthTableView from './schedule/MonthTableView.svelte'
|
||||
import MonthView from './schedule/MonthView.svelte'
|
||||
import YearView from './schedule/YearView.svelte'
|
||||
import { groupBy } from '@hcengineering/view-resources'
|
||||
import contact from '@hcengineering/contact-resources/src/plugin'
|
||||
|
||||
export let department: Ref<Department>
|
||||
export let descendants: Map<Ref<Department>, Department[]>
|
||||
|
@ -17,13 +17,13 @@
|
||||
import chunter from '@hcengineering/chunter'
|
||||
import type { Contact, Employee, Person } from '@hcengineering/contact'
|
||||
import contact from '@hcengineering/contact'
|
||||
import { ExpandRightDouble } from '@hcengineering/contact-resources'
|
||||
import { EmployeeBox, ExpandRightDouble, UserBox } from '@hcengineering/contact-resources'
|
||||
import {
|
||||
Account,
|
||||
Class,
|
||||
Client,
|
||||
fillDefaults,
|
||||
Doc,
|
||||
fillDefaults,
|
||||
FindOptions,
|
||||
generateId,
|
||||
Markup,
|
||||
@ -39,7 +39,6 @@
|
||||
InlineAttributeBar,
|
||||
SpaceSelect
|
||||
} from '@hcengineering/presentation'
|
||||
import { EmployeeBox, UserBox } from '@hcengineering/contact-resources'
|
||||
import type { Applicant, Candidate, Vacancy } from '@hcengineering/recruit'
|
||||
import task, { calcRank, State } from '@hcengineering/task'
|
||||
import ui, {
|
||||
@ -48,6 +47,7 @@
|
||||
createFocusManager,
|
||||
deviceOptionsStore as deviceInfo,
|
||||
FocusHandler,
|
||||
getColorNumberByText,
|
||||
getPlatformColor,
|
||||
Label,
|
||||
showPopup,
|
||||
@ -245,7 +245,7 @@
|
||||
}
|
||||
|
||||
$: states = rawStates.map((s) => {
|
||||
return { id: s._id, label: s.title, color: s.color }
|
||||
return { id: s._id, label: s.name, color: s.color ?? getColorNumberByText(s.name) }
|
||||
})
|
||||
|
||||
const manager = createFocusManager()
|
||||
@ -414,8 +414,13 @@
|
||||
>
|
||||
<div slot="content" class="flex-row-center" class:empty={!selectedState}>
|
||||
{#if selectedState}
|
||||
<div class="color" style="background-color: {getPlatformColor(selectedState.color)}" />
|
||||
<span class="label overflow-label">{selectedState.title}</span>
|
||||
<div
|
||||
class="color"
|
||||
style="background-color: {getPlatformColor(
|
||||
selectedState.color ?? getColorNumberByText(selectedState.name)
|
||||
)}"
|
||||
/>
|
||||
<span class="label overflow-label">{selectedState.name}</span>
|
||||
{:else}
|
||||
<div class="color" />
|
||||
<span class="label overflow-label"><Label label={presentation.string.NotSelected} /></span>
|
||||
|
@ -26,6 +26,7 @@
|
||||
createFocusManager,
|
||||
deviceOptionsStore as deviceInfo,
|
||||
FocusHandler,
|
||||
getColorNumberByText,
|
||||
getPlatformColor,
|
||||
Label,
|
||||
ListView,
|
||||
@ -100,7 +101,7 @@
|
||||
}
|
||||
|
||||
$: states = rawStates.map((s) => {
|
||||
return { id: s._id, label: s.title, color: s.color }
|
||||
return { id: s._id, label: s.name, color: s.color ?? getColorNumberByText(s.name) }
|
||||
})
|
||||
|
||||
const manager = createFocusManager()
|
||||
|
@ -64,7 +64,8 @@
|
||||
task.class.KanbanTemplate,
|
||||
'statesC',
|
||||
{
|
||||
title: 'New State',
|
||||
ofAttribute: task.attribute.State,
|
||||
name: 'New State',
|
||||
color: 9,
|
||||
rank: [...genRanks(1)][0]
|
||||
}
|
||||
@ -74,12 +75,12 @@
|
||||
const doneStates = [
|
||||
{
|
||||
class: task.class.WonStateTemplate,
|
||||
title: 'Won',
|
||||
name: 'Won',
|
||||
rank: ranks[0]
|
||||
},
|
||||
{
|
||||
class: task.class.LostStateTemplate,
|
||||
title: 'Lost',
|
||||
name: 'Lost',
|
||||
rank: ranks[1]
|
||||
}
|
||||
]
|
||||
@ -93,7 +94,8 @@
|
||||
task.class.KanbanTemplate,
|
||||
'doneStatesC',
|
||||
{
|
||||
title: ds.title,
|
||||
ofAttribute: task.attribute.DoneState,
|
||||
name: ds.name,
|
||||
rank: ds.rank
|
||||
}
|
||||
)
|
||||
|
@ -13,10 +13,9 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import core, { Class, Doc, Ref, RefTo } from '@hcengineering/core'
|
||||
import core, { Class, Doc, DOMAIN_STATUS, Ref, RefTo } from '@hcengineering/core'
|
||||
import { TypeRef } from '@hcengineering/model'
|
||||
import { getClient } from '@hcengineering/presentation'
|
||||
import { DOMAIN_STATE } from '@hcengineering/task'
|
||||
import { DropdownLabelsIntl, Label } from '@hcengineering/ui'
|
||||
import view from '@hcengineering/view-resources/src/plugin'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
@ -35,7 +34,7 @@
|
||||
return (
|
||||
hierarchy.hasMixin(p, view.mixin.AttributeEditor) &&
|
||||
p.label !== undefined &&
|
||||
hierarchy.getDomain(p._id) !== DOMAIN_STATE
|
||||
hierarchy.getDomain(p._id) !== DOMAIN_STATUS
|
||||
)
|
||||
})
|
||||
.map((p) => {
|
||||
|
@ -33,7 +33,6 @@
|
||||
"Kanban": "Kanban",
|
||||
"ApplicationLabelTask": "Tasks",
|
||||
"Projects": "Projects",
|
||||
"CreateProject": "New Project",
|
||||
"ProjectNamePlaceholder": "Project name",
|
||||
"TaskNamePlaceholder": "The boring task",
|
||||
"TodoDescriptionPlaceholder": "todo...",
|
||||
|
@ -33,7 +33,6 @@
|
||||
"Kanban": "Канбан",
|
||||
"ApplicationLabelTask": "Задачи",
|
||||
"Projects": "Проекты",
|
||||
"CreateProject": "Новый проект",
|
||||
"ProjectNamePlaceholder": "Название проекта",
|
||||
"TaskNamePlaceholder": "Задача",
|
||||
"TodoDescriptionPlaceholder": "todo...",
|
||||
|
@ -1,75 +0,0 @@
|
||||
<!--
|
||||
// Copyright © 2020 Anticrm Platform Contributors.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import core, { Ref } from '@hcengineering/core'
|
||||
import presentation, { getClient, SpaceCreateCard } from '@hcengineering/presentation'
|
||||
import { EditBox, Grid, IconFolder, ToggleWithLabel } from '@hcengineering/ui'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import task, { createKanban, KanbanTemplate } from '@hcengineering/task'
|
||||
import KanbanTemplateSelector from './kanban/KanbanTemplateSelector.svelte'
|
||||
import plugin from '../plugin'
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
let name: string = ''
|
||||
const description: string = ''
|
||||
let templateId: Ref<KanbanTemplate> | undefined
|
||||
|
||||
export function canClose (): boolean {
|
||||
return name === '' && templateId !== undefined
|
||||
}
|
||||
|
||||
const client = getClient()
|
||||
|
||||
async function createProject (): Promise<void> {
|
||||
if (
|
||||
templateId !== undefined &&
|
||||
(await client.findOne(task.class.KanbanTemplate, { _id: templateId })) === undefined
|
||||
) {
|
||||
throw Error(`Failed to find target kanban template: ${templateId}`)
|
||||
}
|
||||
|
||||
const id = await client.createDoc(task.class.Project, core.space.Space, {
|
||||
name,
|
||||
description,
|
||||
private: false,
|
||||
archived: false,
|
||||
members: []
|
||||
})
|
||||
|
||||
await createKanban(client, id, templateId)
|
||||
}
|
||||
</script>
|
||||
|
||||
<SpaceCreateCard
|
||||
label={plugin.string.CreateProject}
|
||||
okAction={createProject}
|
||||
canSave={name.length > 0}
|
||||
on:close={() => {
|
||||
dispatch('close')
|
||||
}}
|
||||
>
|
||||
<Grid column={1} rowGap={1.5}>
|
||||
<EditBox
|
||||
label={plugin.string.ProjectName}
|
||||
icon={IconFolder}
|
||||
bind:value={name}
|
||||
placeholder={plugin.string.ProjectNamePlaceholder}
|
||||
focus
|
||||
/>
|
||||
<ToggleWithLabel label={presentation.string.MakePrivate} description={presentation.string.MakePrivateDescription} />
|
||||
<KanbanTemplateSelector folders={[task.space.ProjectTemplates]} bind:template={templateId} />
|
||||
</Grid>
|
||||
</SpaceCreateCard>
|
@ -106,7 +106,7 @@
|
||||
p._id,
|
||||
{
|
||||
_id: p._id,
|
||||
label: p.title,
|
||||
label: p.name,
|
||||
values: [
|
||||
{ color: 10, value: 0 },
|
||||
{ color: 0, value: 0 },
|
||||
|
@ -1,78 +0,0 @@
|
||||
<!--
|
||||
// Copyright © 2020, 2021 Anticrm Platform Contributors.
|
||||
// Copyright © 2021 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 { getClient } from '@hcengineering/presentation'
|
||||
import type { Issue } from '@hcengineering/task'
|
||||
import task from '@hcengineering/task'
|
||||
import { StyledTextBox } from '@hcengineering/text-editor'
|
||||
import { EditBox, Grid } from '@hcengineering/ui'
|
||||
import { createEventDispatcher, onMount } from 'svelte'
|
||||
import plugin from '../plugin'
|
||||
|
||||
export let object: Issue
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
const client = getClient()
|
||||
|
||||
function change (field: string, value: any) {
|
||||
client.updateCollection(
|
||||
object._class,
|
||||
object.space,
|
||||
object._id,
|
||||
object.attachedTo,
|
||||
object.attachedToClass,
|
||||
object.collection,
|
||||
{ [field]: value }
|
||||
)
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
dispatch('open', { ignoreKeys: ['comments', 'name', 'description', 'number'] })
|
||||
})
|
||||
const onChangeDescription = (evt: CustomEvent<any>) => change('description', evt.detail)
|
||||
</script>
|
||||
|
||||
{#if object !== undefined}
|
||||
<Grid column={1} rowGap={1.5}>
|
||||
<EditBox
|
||||
label={plugin.string.TaskName}
|
||||
bind:value={object.name}
|
||||
icon={task.icon.Task}
|
||||
placeholder={plugin.string.TaskNamePlaceholder}
|
||||
focus
|
||||
on:change={() => change('name', object.name)}
|
||||
/>
|
||||
|
||||
<div class="description">
|
||||
<StyledTextBox
|
||||
bind:content={object.description}
|
||||
placeholder={plugin.string.DescriptionPlaceholder}
|
||||
on:value={onChangeDescription}
|
||||
/>
|
||||
</div>
|
||||
</Grid>
|
||||
{/if}
|
||||
|
||||
<style lang="scss">
|
||||
.description {
|
||||
display: flex;
|
||||
padding: 1rem;
|
||||
height: 12rem;
|
||||
border-radius: 0.25rem;
|
||||
background-color: var(--accent-bg-color);
|
||||
border: 1px solid var(--divider-color);
|
||||
}
|
||||
</style>
|
@ -1,78 +0,0 @@
|
||||
<!--
|
||||
// Copyright © 2020, 2021 Anticrm Platform Contributors.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { AttachmentsPresenter } from '@hcengineering/attachment-resources'
|
||||
import { CommentsPresenter } from '@hcengineering/chunter-resources'
|
||||
import type { WithLookup } from '@hcengineering/core'
|
||||
import notification from '@hcengineering/notification'
|
||||
import { Avatar } from '@hcengineering/contact-resources'
|
||||
import type { Issue, TodoItem } from '@hcengineering/task'
|
||||
import { ActionIcon, Component, IconMoreH, showPopup, tooltip } from '@hcengineering/ui'
|
||||
import { ContextMenu } from '@hcengineering/view-resources'
|
||||
import task from '../plugin'
|
||||
import TaskPresenter from './TaskPresenter.svelte'
|
||||
|
||||
export let object: WithLookup<Issue>
|
||||
|
||||
const showMenu = (ev?: Event): void => {
|
||||
showPopup(ContextMenu, { object }, ev ? (ev.target as HTMLElement) : null)
|
||||
}
|
||||
|
||||
$: todoItems = (object.$lookup?.todoItems as TodoItem[]) ?? []
|
||||
$: doneTasks = todoItems.filter((it) => it.done)
|
||||
</script>
|
||||
|
||||
<div class="flex-col pt-2 pb-2 pr-4 pl-4">
|
||||
<div class="flex-between mb-2">
|
||||
<div class="flex">
|
||||
<TaskPresenter value={object} />
|
||||
{#if todoItems.length > 0}
|
||||
<span
|
||||
class="ml-2"
|
||||
use:tooltip={{
|
||||
label: task.string.TodoItems,
|
||||
component: task.component.TodoItemsPopup,
|
||||
props: { value: object }
|
||||
}}
|
||||
>
|
||||
({doneTasks?.length}/{todoItems.length})
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="flex-row-center">
|
||||
<div class="mr-2">
|
||||
<Component is={notification.component.NotificationPresenter} props={{ value: object }} />
|
||||
</div>
|
||||
<ActionIcon label={task.string.More} action={showMenu} icon={IconMoreH} size={'small'} />
|
||||
</div>
|
||||
</div>
|
||||
<div class="caption-color mb-3 lines-limit-4">{object.name}</div>
|
||||
<!-- <div class="text-sm lines-limit-2">{object.description}</div> -->
|
||||
<div class="flex-between">
|
||||
<div class="flex-row-center">
|
||||
{#if (object.attachments ?? 0) > 0}
|
||||
<div class="step-lr75">
|
||||
<AttachmentsPresenter value={object.attachments} {object} />
|
||||
</div>
|
||||
{/if}
|
||||
{#if (object.comments ?? 0) > 0}
|
||||
<div class="step-lr75">
|
||||
<CommentsPresenter value={object.comments} {object} />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<Avatar avatar={object.$lookup?.assignee?.avatar} size={'x-small'} />
|
||||
</div>
|
||||
</div>
|
@ -64,7 +64,7 @@
|
||||
itemsDS = doneStates.map((s) => {
|
||||
return {
|
||||
id: s._id,
|
||||
label: s.title,
|
||||
label: s.name,
|
||||
icon: s._class === task.class.WonState ? Won : Lost,
|
||||
color: s._class === task.class.WonState ? 'var(--won-color)' : 'var(--lost-color)'
|
||||
}
|
||||
|
@ -1,48 +0,0 @@
|
||||
<!--
|
||||
// Copyright © 2020, 2021 Anticrm Platform Contributors.
|
||||
// Copyright © 2021 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 { getClient } from '@hcengineering/presentation'
|
||||
import type { Issue, Task } from '@hcengineering/task'
|
||||
import { Icon } from '@hcengineering/ui'
|
||||
import task from '../plugin'
|
||||
|
||||
export let value: Task
|
||||
|
||||
const client = getClient()
|
||||
const shortLabel = client.getHierarchy().getClass(value._class).shortLabel
|
||||
|
||||
let name: string | undefined = undefined
|
||||
|
||||
$: if (client.getHierarchy().isDerived(value._class, task.class.Issue)) {
|
||||
name = (value as Issue).name
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex item">
|
||||
<Icon icon={task.icon.Task} size={'large'} />
|
||||
<div class="ml-2">
|
||||
{#if shortLabel}{shortLabel}-{/if}{value.number}
|
||||
</div>
|
||||
{#if name}
|
||||
<div class="ml-1">{name}</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.item {
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
@ -15,12 +15,12 @@
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { getClient } from '@hcengineering/presentation'
|
||||
import type { Issue } from '@hcengineering/task'
|
||||
import type { Task } from '@hcengineering/task'
|
||||
import { Icon } from '@hcengineering/ui'
|
||||
import { DocNavLink } from '@hcengineering/view-resources'
|
||||
import task from '../plugin'
|
||||
|
||||
export let value: Issue
|
||||
export let value: Task
|
||||
export let inline: boolean = false
|
||||
|
||||
const client = getClient()
|
||||
|
@ -61,7 +61,7 @@
|
||||
on:drop={onDone(wonState)}
|
||||
>
|
||||
<div class="mr-2"><Won size={'small'} /></div>
|
||||
{wonState.title}
|
||||
{wonState.name}
|
||||
</div>
|
||||
{/each}
|
||||
{#each lostStates as lostState}
|
||||
@ -80,7 +80,7 @@
|
||||
on:drop={onDone(lostState)}
|
||||
>
|
||||
<div class="mr-2"><Lost size={'small'} /></div>
|
||||
{lostState.title}
|
||||
{lostState.name}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
@ -78,12 +78,14 @@
|
||||
const lastOne = await client.findOne(_class, {}, { sort: { rank: SortingOrder.Descending } })
|
||||
if (hierarchy.isDerived(_class, task.class.DoneState)) {
|
||||
await client.createDoc(_class, kanban.space, {
|
||||
title: 'New Done State',
|
||||
ofAttribute: task.attribute.State,
|
||||
name: 'New Done State',
|
||||
rank: calcRank(lastOne, undefined)
|
||||
})
|
||||
} else {
|
||||
await client.createDoc(task.class.State, kanban.space, {
|
||||
title: 'New State',
|
||||
ofAttribute: task.attribute.State,
|
||||
name: 'New State',
|
||||
color: 9,
|
||||
rank: calcRank(lastOne, undefined)
|
||||
})
|
||||
|
@ -105,12 +105,14 @@
|
||||
if (hierarchy.isDerived(_class, task.class.DoneState)) {
|
||||
const targetClass = _class === task.class.WonState ? task.class.WonStateTemplate : task.class.LostStateTemplate
|
||||
await client.addCollection(targetClass, kanban.space, kanban._id, kanban._class, 'doneStatesC', {
|
||||
title: 'New Done State',
|
||||
ofAttribute: task.attribute.DoneState,
|
||||
name: 'New Done State',
|
||||
rank: calcRank(lastOne, undefined)
|
||||
})
|
||||
} else {
|
||||
await client.addCollection(task.class.StateTemplate, kanban.space, kanban._id, kanban._class, 'statesC', {
|
||||
title: 'New State',
|
||||
name: 'New State',
|
||||
ofAttribute: task.attribute.DoneState,
|
||||
color: 9,
|
||||
rank: calcRank(lastOne, undefined)
|
||||
})
|
||||
|
@ -14,61 +14,94 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { Class, Doc, DocumentQuery, FindOptions, Ref, SortingOrder } from '@hcengineering/core'
|
||||
import { Kanban as KanbanUI } from '@hcengineering/kanban'
|
||||
import contact from '@hcengineering/contact'
|
||||
import {
|
||||
CategoryType,
|
||||
Class,
|
||||
Doc,
|
||||
DocumentQuery,
|
||||
DocumentUpdate,
|
||||
FindOptions,
|
||||
generateId,
|
||||
Ref
|
||||
} from '@hcengineering/core'
|
||||
import { Item, Kanban as KanbanUI } from '@hcengineering/kanban'
|
||||
import { getResource } from '@hcengineering/platform'
|
||||
import { createQuery, getClient } from '@hcengineering/presentation'
|
||||
import type { Kanban, SpaceWithStates, State, Task } from '@hcengineering/task'
|
||||
import task from '@hcengineering/task'
|
||||
import { getEventPositionElement, showPopup } from '@hcengineering/ui'
|
||||
import { createQuery, getClient, statusStore } from '@hcengineering/presentation'
|
||||
import { Kanban, SpaceWithStates, Task, TaskGrouping, TaskOrdering } from '@hcengineering/task'
|
||||
import { getEventPositionElement, Label, showPopup } from '@hcengineering/ui'
|
||||
import {
|
||||
AttributeModel,
|
||||
CategoryOption,
|
||||
Viewlet,
|
||||
ViewOptionModel,
|
||||
ViewOptions,
|
||||
ViewQueryOption
|
||||
} from '@hcengineering/view'
|
||||
import {
|
||||
ActionContext,
|
||||
focusStore,
|
||||
getCategories,
|
||||
getGroupByValues,
|
||||
getPresenter,
|
||||
groupBy,
|
||||
ListSelectionProvider,
|
||||
Menu,
|
||||
noCategory,
|
||||
SelectDirection,
|
||||
selectionStore
|
||||
selectionStore,
|
||||
setGroupByValues
|
||||
} from '@hcengineering/view-resources'
|
||||
import view from '@hcengineering/view-resources/src/plugin'
|
||||
import { onMount } from 'svelte'
|
||||
import task from '../../plugin'
|
||||
import KanbanDragDone from './KanbanDragDone.svelte'
|
||||
|
||||
export let _class: Ref<Class<Task>>
|
||||
export let space: Ref<SpaceWithStates>
|
||||
export let query: DocumentQuery<Task>
|
||||
export let options: FindOptions<Task> | undefined
|
||||
export let space: Ref<SpaceWithStates> | undefined = undefined
|
||||
export let baseMenuClass: Ref<Class<Doc>> | undefined = undefined
|
||||
// export let config: string[]
|
||||
export let query: DocumentQuery<Task> = {}
|
||||
export let viewOptionsConfig: ViewOptionModel[] | undefined
|
||||
export let viewOptions: ViewOptions
|
||||
export let viewlet: Viewlet
|
||||
|
||||
let kanban: Kanban
|
||||
let states: State[] = []
|
||||
export let options: FindOptions<Task> | undefined
|
||||
|
||||
const client = getClient()
|
||||
$: currentSpace = space
|
||||
$: groupByKey = (viewOptions.groupBy[0] ?? noCategory) as TaskGrouping
|
||||
$: orderBy = viewOptions.orderBy
|
||||
$: sort = { [orderBy[0]]: orderBy[1] }
|
||||
|
||||
const kanbanQuery = createQuery()
|
||||
$: kanbanQuery.query(task.class.Kanban, { attachedTo: space }, (result) => {
|
||||
kanban = result[0]
|
||||
$: dontUpdateRank = orderBy[0] !== TaskOrdering.Manual
|
||||
|
||||
const spaceQuery = createQuery()
|
||||
|
||||
let currentProject: SpaceWithStates | undefined
|
||||
$: spaceQuery.query(task.class.SpaceWithStates, { _id: currentSpace }, (res) => {
|
||||
currentProject = res.shift()
|
||||
})
|
||||
|
||||
const statesQuery = createQuery()
|
||||
$: if (kanban !== undefined) {
|
||||
statesQuery.query(
|
||||
task.class.State,
|
||||
{ space: kanban.space },
|
||||
(result) => {
|
||||
states = result
|
||||
},
|
||||
{
|
||||
sort: {
|
||||
rank: SortingOrder.Ascending
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
let resultQuery: DocumentQuery<any> = { ...query, doneState: null }
|
||||
$: getResultQuery(query, viewOptionsConfig, viewOptions).then((p) => (resultQuery = p))
|
||||
|
||||
$: clazz = client.getHierarchy().getClass(_class)
|
||||
$: presenterMixin = client.getHierarchy().as(clazz, task.mixin.KanbanCard)
|
||||
$: cardPresenter = getResource(presenterMixin.card)
|
||||
/* eslint-disable no-undef */
|
||||
const client = getClient()
|
||||
const hierarchy = client.getHierarchy()
|
||||
|
||||
async function getResultQuery (
|
||||
query: DocumentQuery<Task>,
|
||||
viewOptions: ViewOptionModel[] | undefined,
|
||||
viewOptionsStore: ViewOptions
|
||||
): Promise<DocumentQuery<Task>> {
|
||||
if (viewOptions === undefined) return query
|
||||
let result = hierarchy.clone(query)
|
||||
for (const viewOption of viewOptions) {
|
||||
if (viewOption.actionTarget !== 'query') continue
|
||||
const queryOption = viewOption as ViewQueryOption
|
||||
const f = await getResource(queryOption.action)
|
||||
result = f(viewOptionsStore[queryOption.key] ?? queryOption.defaultValue, query)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
let kanbanUI: KanbanUI
|
||||
const listProvider = new ListSelectionProvider((offset: 1 | -1 | 0, of?: Doc, dir?: SelectDirection) => {
|
||||
@ -84,18 +117,100 @@
|
||||
// selection = undefined
|
||||
})
|
||||
}
|
||||
const onContent = (evt: any) => {
|
||||
listProvider.update(evt.detail)
|
||||
}
|
||||
const onObjFocus = (evt: any) => {
|
||||
listProvider.updateFocus(evt.detail)
|
||||
}
|
||||
const handleCheck = (evt: any) => {
|
||||
listProvider.updateSelection(evt.detail.docs, evt.detail.value)
|
||||
}
|
||||
const onContextMenu = (evt: any) => showMenu(evt.detail.evt, evt.detail.objects)
|
||||
const issuesQuery = createQuery()
|
||||
let tasks: Task[] = []
|
||||
|
||||
$: resultQuery = { ...query, doneState: null }
|
||||
$: groupByDocs = groupBy(tasks, groupByKey, categories)
|
||||
|
||||
$: issuesQuery.query<Task>(
|
||||
_class,
|
||||
resultQuery,
|
||||
(result) => {
|
||||
tasks = result
|
||||
},
|
||||
{
|
||||
...options,
|
||||
lookup: {
|
||||
...options?.lookup,
|
||||
assignee: contact.class.Employee,
|
||||
space: task.class.SpaceWithStates,
|
||||
state: task.class.State,
|
||||
doneState: task.class.DoneState
|
||||
},
|
||||
sort: {
|
||||
...options?.sort,
|
||||
...sort
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
let categories: CategoryType[] = []
|
||||
|
||||
const queryId = generateId()
|
||||
|
||||
$: updateCategories(_class, tasks, groupByKey, viewOptions, viewOptionsConfig)
|
||||
|
||||
function update () {
|
||||
updateCategories(_class, tasks, groupByKey, viewOptions, viewOptionsConfig)
|
||||
}
|
||||
|
||||
async function updateCategories (
|
||||
_class: Ref<Class<Doc>>,
|
||||
docs: Doc[],
|
||||
groupByKey: string,
|
||||
viewOptions: ViewOptions,
|
||||
viewOptionsModel: ViewOptionModel[] | undefined
|
||||
) {
|
||||
categories = await getCategories(client, _class, docs, groupByKey, $statusStore, viewlet.descriptor)
|
||||
for (const viewOption of viewOptionsModel ?? []) {
|
||||
if (viewOption.actionTarget !== 'category') continue
|
||||
const categoryFunc = viewOption as CategoryOption
|
||||
if (viewOptions[viewOption.key] ?? viewOption.defaultValue) {
|
||||
const categoryAction = await getResource(categoryFunc.action)
|
||||
const res = await categoryAction(_class, space, groupByKey, update, queryId, $statusStore, viewlet.descriptor)
|
||||
if (res !== undefined) {
|
||||
categories = res
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getHeader (_class: Ref<Class<Doc>>, groupByKey: string): void {
|
||||
if (groupByKey === noCategory) {
|
||||
headerComponent = undefined
|
||||
} else {
|
||||
getPresenter(client, _class, { key: groupByKey }, { key: groupByKey }).then((p) => (headerComponent = p))
|
||||
}
|
||||
}
|
||||
|
||||
let headerComponent: AttributeModel | undefined
|
||||
$: getHeader(_class, groupByKey)
|
||||
|
||||
const getUpdateProps = (doc: Doc, category: CategoryType): DocumentUpdate<Item> | undefined => {
|
||||
const groupValue =
|
||||
typeof category === 'object' ? category.values.find((it) => it.space === doc.space)?._id : category
|
||||
if (groupValue === undefined) {
|
||||
return undefined
|
||||
}
|
||||
return {
|
||||
[groupByKey]: groupValue,
|
||||
space: doc.space
|
||||
}
|
||||
}
|
||||
|
||||
$: clazz = client.getHierarchy().getClass(_class)
|
||||
$: presenterMixin = client.getHierarchy().as(clazz, task.mixin.KanbanCard)
|
||||
$: cardPresenter = getResource(presenterMixin.card)
|
||||
|
||||
let kanban: Kanban
|
||||
|
||||
const kanbanQuery = createQuery()
|
||||
$: kanbanQuery.query(task.class.Kanban, { attachedTo: space }, (result) => {
|
||||
kanban = result[0]
|
||||
})
|
||||
|
||||
const getDoneUpdate = (e: any) => ({ doneState: e.detail._id } as DocumentUpdate<Doc>)
|
||||
</script>
|
||||
|
||||
{#await cardPresenter then presenter}
|
||||
@ -106,18 +221,41 @@
|
||||
/>
|
||||
<KanbanUI
|
||||
bind:this={kanbanUI}
|
||||
{_class}
|
||||
{options}
|
||||
query={resultQuery}
|
||||
{states}
|
||||
fieldName={'state'}
|
||||
on:content={onContent}
|
||||
on:obj-focus={onObjFocus}
|
||||
checked={$selectionStore ?? []}
|
||||
on:check={handleCheck}
|
||||
on:contextmenu={onContextMenu}
|
||||
{categories}
|
||||
{dontUpdateRank}
|
||||
objects={tasks}
|
||||
getGroupByValues={(groupByDocs, category) =>
|
||||
groupByKey === noCategory ? tasks : getGroupByValues(groupByDocs, category)}
|
||||
{setGroupByValues}
|
||||
{getUpdateProps}
|
||||
{groupByDocs}
|
||||
on:content={(evt) => {
|
||||
listProvider.update(evt.detail)
|
||||
}}
|
||||
on:obj-focus={(evt) => {
|
||||
listProvider.updateFocus(evt.detail)
|
||||
}}
|
||||
selection={listProvider.current($focusStore)}
|
||||
checked={$selectionStore ?? []}
|
||||
on:check={(evt) => {
|
||||
listProvider.updateSelection(evt.detail.docs, evt.detail.value)
|
||||
}}
|
||||
on:contextmenu={(evt) => showMenu(evt.detail.evt, evt.detail.objects)}
|
||||
>
|
||||
<svelte:fragment slot="header" let:state let:count>
|
||||
<!-- {@const status = $statusStore.get(state._id)} -->
|
||||
<div class="header flex-col">
|
||||
<div class="flex-row-center flex-between">
|
||||
{#if groupByKey === noCategory}
|
||||
<span class="text-base fs-bold overflow-label content-accent-color pointer-events-none">
|
||||
<Label label={view.string.NoGrouping} />
|
||||
</span>
|
||||
{:else if headerComponent}
|
||||
<svelte:component this={headerComponent.presenter} value={state} {space} kind={'list-header'} />
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="card" let:object let:dragged>
|
||||
<svelte:component this={presenter} {object} {dragged} />
|
||||
</svelte:fragment>
|
||||
@ -127,9 +265,40 @@
|
||||
{kanban}
|
||||
on:done={(e) => {
|
||||
// eslint-disable-next-line no-undef
|
||||
onDone({ doneState: e.detail._id })
|
||||
onDone(getDoneUpdate(e))
|
||||
}}
|
||||
/>
|
||||
</svelte:fragment>
|
||||
</KanbanUI>
|
||||
{/await}
|
||||
|
||||
<style lang="scss">
|
||||
.names {
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
|
||||
.header {
|
||||
padding-bottom: 0.75rem;
|
||||
border-bottom: 1px solid var(--divider-color);
|
||||
|
||||
.label {
|
||||
color: var(--caption-color);
|
||||
.counter {
|
||||
color: rgba(var(--caption-color), 0.8);
|
||||
}
|
||||
}
|
||||
}
|
||||
.tracker-card {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
// padding: 0.5rem 1rem;
|
||||
min-height: 6.5rem;
|
||||
}
|
||||
.states-bar {
|
||||
flex-shrink: 10;
|
||||
width: fit-content;
|
||||
margin: 0.625rem 1rem 0;
|
||||
}
|
||||
</style>
|
||||
|
@ -32,7 +32,7 @@
|
||||
<svelte:component this={value._class === task.class.WonState ? Won : Lost} size={'small'} />
|
||||
</div>
|
||||
{#if showTitle}
|
||||
{value.title}
|
||||
{value.name}
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
@ -14,20 +14,16 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { Ref } from '@hcengineering/core'
|
||||
import { createQuery } from '@hcengineering/presentation'
|
||||
import { Ref, StatusValue } from '@hcengineering/core'
|
||||
import { statusStore } from '@hcengineering/presentation'
|
||||
import type { DoneState } from '@hcengineering/task'
|
||||
import task from '@hcengineering/task'
|
||||
import DoneStatePresenter from './DoneStatePresenter.svelte'
|
||||
|
||||
export let value: Ref<DoneState> | null | undefined
|
||||
export let value: Ref<DoneState> | StatusValue
|
||||
export let showTitle: boolean = true
|
||||
|
||||
let state: DoneState | undefined
|
||||
const query = createQuery()
|
||||
$: value && query.query(task.class.DoneState, { _id: value }, (res) => ([state] = res), { limit: 1 })
|
||||
</script>
|
||||
|
||||
{#if state}
|
||||
{#if value}
|
||||
{@const state = $statusStore.get(typeof value === 'string' ? value : value.values[0]._id)}
|
||||
<DoneStatePresenter value={state} {showTitle} />
|
||||
{/if}
|
||||
|
@ -59,7 +59,7 @@
|
||||
<div class="mr-2" style="color: {getColor(state._class)}">
|
||||
<svelte:component this={state._class === task.class.WonState ? Won : Lost} size={'small'} />
|
||||
</div>
|
||||
<span class="label">{state.title}</span>
|
||||
<span class="label">{state.name}</span>
|
||||
</button>
|
||||
{/each}
|
||||
<button
|
||||
|
@ -15,15 +15,18 @@
|
||||
-->
|
||||
<script lang="ts">
|
||||
import type { State } from '@hcengineering/task'
|
||||
import { getPlatformColor } from '@hcengineering/ui'
|
||||
import { getColorNumberByText, getPlatformColor } from '@hcengineering/ui'
|
||||
|
||||
export let value: State
|
||||
export let value: State | undefined
|
||||
</script>
|
||||
|
||||
{#if value}
|
||||
<div class="flex-row-center">
|
||||
<div class="state-container" style="background-color: {getPlatformColor(value.color)}" />
|
||||
<span class="overflow-label">{value.title}</span>
|
||||
<div
|
||||
class="state-container"
|
||||
style="background-color: {getPlatformColor(value.color ?? getColorNumberByText(value.name))}"
|
||||
/>
|
||||
<span class="overflow-label">{value.name}</span>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
|
@ -14,23 +14,20 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { Ref } from '@hcengineering/core'
|
||||
import { createQuery } from '@hcengineering/presentation'
|
||||
import task, { State } from '@hcengineering/task'
|
||||
import { Ref, StatusValue } from '@hcengineering/core'
|
||||
import { statusStore } from '@hcengineering/presentation'
|
||||
import { State } from '@hcengineering/task'
|
||||
import StateEditor from './StateEditor.svelte'
|
||||
import StatePresenter from './StatePresenter.svelte'
|
||||
|
||||
export let value: Ref<State>
|
||||
export let value: Ref<State> | StatusValue
|
||||
export let onChange: ((value: Ref<State>) => void) | undefined = undefined
|
||||
|
||||
let state: State | undefined
|
||||
const query = createQuery()
|
||||
$: query.query(task.class.State, { _id: value }, (res) => ([state] = res), { limit: 1 })
|
||||
</script>
|
||||
|
||||
{#if state}
|
||||
{#if onChange !== undefined}
|
||||
<StateEditor {value} space={state.space} {onChange} kind="link" size="medium" />
|
||||
{#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="medium" />
|
||||
{:else}
|
||||
<StatePresenter value={state} />
|
||||
{/if}
|
||||
|
@ -14,37 +14,23 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { Ref, SortingOrder } from '@hcengineering/core'
|
||||
import { createQuery } from '@hcengineering/presentation'
|
||||
import { Ref } from '@hcengineering/core'
|
||||
import { statusStore } from '@hcengineering/presentation'
|
||||
import task, { SpaceWithStates, State } from '@hcengineering/task'
|
||||
import { getPlatformColor, ScrollerBar } from '@hcengineering/ui'
|
||||
import { getColorNumberByText, getPlatformColor, ScrollerBar } from '@hcengineering/ui'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import StatesBarElement from './StatesBarElement.svelte'
|
||||
import type { StatesBarPosition } from '../..'
|
||||
import StatesBarElement from './StatesBarElement.svelte'
|
||||
|
||||
export let space: Ref<SpaceWithStates>
|
||||
export let state: Ref<State> | undefined = undefined
|
||||
export let gap: 'none' | 'small' | 'big' = 'small'
|
||||
|
||||
let states: State[] = []
|
||||
$: states = $statusStore.filter((it) => it.space === space && it.ofAttribute === task.attribute.State)
|
||||
let divScroll: HTMLElement
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
const statesQuery = createQuery()
|
||||
statesQuery.query(
|
||||
task.class.State,
|
||||
space != null ? { space } : {},
|
||||
(res) => {
|
||||
states = res
|
||||
},
|
||||
{
|
||||
sort: {
|
||||
rank: SortingOrder.Ascending
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
const selectItem = (ev: Event, item: State): void => {
|
||||
const el: HTMLElement = ev.currentTarget as HTMLElement
|
||||
const rect = el.getBoundingClientRect()
|
||||
@ -72,10 +58,10 @@
|
||||
<ScrollerBar {gap} bind:scroller={divScroll}>
|
||||
{#each states as item, i (item._id)}
|
||||
<StatesBarElement
|
||||
label={item.title}
|
||||
label={item.name}
|
||||
position={getPosition(i)}
|
||||
selected={item._id === state}
|
||||
color={getPlatformColor(item.color)}
|
||||
color={getPlatformColor(item.color ?? getColorNumberByText(item.name))}
|
||||
on:click={(ev) => {
|
||||
if (item._id !== state) selectItem(ev, item)
|
||||
}}
|
||||
|
@ -26,7 +26,8 @@
|
||||
getPlatformColor,
|
||||
eventToHTMLElement,
|
||||
Component,
|
||||
IconCircles
|
||||
IconCircles,
|
||||
getColorNumberByText
|
||||
} from '@hcengineering/ui'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import { ColorsPopup } from '@hcengineering/view-resources'
|
||||
@ -126,13 +127,18 @@
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<div
|
||||
class="color"
|
||||
style="background-color: {getPlatformColor(state.color)}"
|
||||
style="background-color: {getPlatformColor(state.color ?? getColorNumberByText(state.name))}"
|
||||
on:click={() => {
|
||||
showPopup(ColorsPopup, { selected: getPlatformColor(state.color) }, elements[i], onColorChange(state))
|
||||
showPopup(
|
||||
ColorsPopup,
|
||||
{ selected: getPlatformColor(state.color ?? getColorNumberByText(state.name)) },
|
||||
elements[i],
|
||||
onColorChange(state)
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<div class="flex-grow caption-color">
|
||||
<AttributeEditor maxWidth={'20rem'} _class={state._class} object={state} key="title" />
|
||||
<AttributeEditor maxWidth={'20rem'} _class={state._class} object={state} key="name" />
|
||||
</div>
|
||||
{#if states.length > 1}
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
@ -174,7 +180,7 @@
|
||||
<Won size={'medium'} />
|
||||
</div>
|
||||
<div class="flex-grow caption-color">
|
||||
<AttributeEditor maxWidth={'13rem'} _class={state._class} object={state} key="title" />
|
||||
<AttributeEditor maxWidth={'13rem'} _class={state._class} object={state} key="name" />
|
||||
</div>
|
||||
{#if wonStates.length > 1}
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
@ -217,7 +223,7 @@
|
||||
<Lost size={'medium'} />
|
||||
</div>
|
||||
<div class="flex-grow caption-color">
|
||||
<AttributeEditor maxWidth={'13rem'} _class={state._class} object={state} key="title" />
|
||||
<AttributeEditor maxWidth={'13rem'} _class={state._class} object={state} key="name" />
|
||||
</div>
|
||||
{#if lostStates.length > 1}
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
|
@ -17,7 +17,7 @@
|
||||
import { Ref, SortingOrder } from '@hcengineering/core'
|
||||
import { createQuery } from '@hcengineering/presentation'
|
||||
import task, { SpaceWithStates, State } from '@hcengineering/task'
|
||||
import { getPlatformColor, resizeObserver } from '@hcengineering/ui'
|
||||
import { getColorNumberByText, getPlatformColor, resizeObserver } from '@hcengineering/ui'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
|
||||
export let space: Ref<SpaceWithStates>
|
||||
@ -48,8 +48,11 @@
|
||||
dispatch('close', state)
|
||||
}}
|
||||
>
|
||||
<div class="color" style="background-color: {getPlatformColor(state.color)}" />
|
||||
<span class="label">{state.title}</span>
|
||||
<div
|
||||
class="color"
|
||||
style="background-color: {getPlatformColor(state.color ?? getColorNumberByText(state.name))}"
|
||||
/>
|
||||
<span class="label">{state.name}</span>
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
|
@ -18,12 +18,9 @@ import { Resources } from '@hcengineering/platform'
|
||||
import { SpaceWithStates } from '@hcengineering/task'
|
||||
import { showPopup } from '@hcengineering/ui'
|
||||
import AssignedTasks from './components/AssignedTasks.svelte'
|
||||
import CreateProject from './components/CreateProject.svelte'
|
||||
import EditIssue from './components/EditIssue.svelte'
|
||||
import KanbanTemplateEditor from './components/kanban/KanbanTemplateEditor.svelte'
|
||||
import KanbanTemplateSelector from './components/kanban/KanbanTemplateSelector.svelte'
|
||||
import KanbanView from './components/kanban/KanbanView.svelte'
|
||||
import KanbanCard from './components/KanbanCard.svelte'
|
||||
import DoneStateEditor from './components/state/DoneStateEditor.svelte'
|
||||
import DoneStatePresenter from './components/state/DoneStatePresenter.svelte'
|
||||
import EditStatuses from './components/state/EditStatuses.svelte'
|
||||
@ -52,11 +49,8 @@ export type StatesBarPosition = 'start' | 'middle' | 'end' | undefined
|
||||
|
||||
export default async (): Promise<Resources> => ({
|
||||
component: {
|
||||
CreateProject,
|
||||
TaskPresenter,
|
||||
KanbanTemplatePresenter,
|
||||
EditIssue,
|
||||
KanbanCard,
|
||||
Dashboard,
|
||||
TemplatesIcon,
|
||||
KanbanView,
|
||||
|
@ -20,7 +20,6 @@ import { AnyComponent } from '@hcengineering/ui'
|
||||
|
||||
export default mergeIds(taskId, task, {
|
||||
string: {
|
||||
CreateProject: '' as IntlString,
|
||||
Description: '' as IntlString,
|
||||
DescriptionPlaceholder: '' as IntlString,
|
||||
ShortDescription: '' as IntlString,
|
||||
|
@ -16,28 +16,24 @@
|
||||
import type { Employee } from '@hcengineering/contact'
|
||||
import {
|
||||
AttachedDoc,
|
||||
Attribute,
|
||||
Class,
|
||||
Doc,
|
||||
Domain,
|
||||
Interface,
|
||||
Markup,
|
||||
Mixin,
|
||||
Ref,
|
||||
Space,
|
||||
Status,
|
||||
Timestamp,
|
||||
TxOperations
|
||||
} from '@hcengineering/core'
|
||||
import { NotificationType } from '@hcengineering/notification'
|
||||
import type { Asset, IntlString, Plugin } from '@hcengineering/platform'
|
||||
import { plugin } from '@hcengineering/platform'
|
||||
import type { AnyComponent } from '@hcengineering/ui'
|
||||
import { ViewletDescriptor } from '@hcengineering/view'
|
||||
import { genRanks } from './utils'
|
||||
import { NotificationType } from '@hcengineering/notification'
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export const DOMAIN_STATE = 'state' as Domain
|
||||
|
||||
/**
|
||||
* @public
|
||||
@ -46,22 +42,25 @@ export interface DocWithRank extends Doc {
|
||||
rank: string
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export interface SpaceWithStates extends Space {}
|
||||
|
||||
// S T A T E
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export interface State extends DocWithRank {
|
||||
title: string
|
||||
color: number
|
||||
export interface State extends Status {
|
||||
isArchived?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export interface DoneState extends DocWithRank {
|
||||
title: string
|
||||
export interface DoneState extends Status {
|
||||
name: string
|
||||
}
|
||||
|
||||
/**
|
||||
@ -99,27 +98,6 @@ export interface TodoItem extends AttachedDoc, DocWithRank {
|
||||
items?: number
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export interface SpaceWithStates extends Space {}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export interface Project extends SpaceWithStates {}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export interface Issue extends Task {
|
||||
name: string
|
||||
description: string
|
||||
|
||||
comments?: number
|
||||
attachments?: number
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
@ -183,6 +161,26 @@ export interface KanbanTemplateSpace extends Space {
|
||||
editor?: AnyComponent
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export enum TaskGrouping {
|
||||
State = 'state',
|
||||
DoneStatus = 'doneState',
|
||||
Assignee = 'assignee',
|
||||
NoGrouping = '#no_category'
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export enum TaskOrdering {
|
||||
State = 'state',
|
||||
LastUpdated = 'modifiedOn',
|
||||
DueDate = 'dueDate',
|
||||
Manual = 'rank'
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
@ -201,6 +199,10 @@ const task = plugin(taskId, {
|
||||
interface: {
|
||||
DocWithRank: '' as Ref<Interface<DocWithRank>>
|
||||
},
|
||||
attribute: {
|
||||
State: '' as Ref<Attribute<State>>,
|
||||
DoneState: '' as Ref<Attribute<DoneState>>
|
||||
},
|
||||
string: {
|
||||
StartDate: '' as IntlString,
|
||||
DueDate: '' as IntlString,
|
||||
@ -230,8 +232,6 @@ const task = plugin(taskId, {
|
||||
Dashboard: '' as IntlString
|
||||
},
|
||||
class: {
|
||||
Issue: '' as Ref<Class<Issue>>,
|
||||
Project: '' as Ref<Class<Project>>,
|
||||
State: '' as Ref<Class<State>>,
|
||||
DoneState: '' as Ref<Class<DoneState>>,
|
||||
WonState: '' as Ref<Class<WonState>>,
|
||||
@ -264,7 +264,7 @@ const task = plugin(taskId, {
|
||||
},
|
||||
global: {
|
||||
// Global task root, if not attached to some other object.
|
||||
Task: '' as Ref<Issue>
|
||||
Task: '' as Ref<Task>
|
||||
},
|
||||
space: {
|
||||
ProjectTemplates: '' as Ref<KanbanTemplateSpace>,
|
||||
@ -293,7 +293,8 @@ export async function createKanban (
|
||||
): Promise<Ref<Kanban>> {
|
||||
if (templateId === undefined) {
|
||||
await client.createDoc(task.class.State, attachedTo, {
|
||||
title: 'New State',
|
||||
ofAttribute: task.attribute.State,
|
||||
name: 'New State',
|
||||
color: 9,
|
||||
rank: [...genRanks(1)][0]
|
||||
})
|
||||
@ -301,11 +302,13 @@ export async function createKanban (
|
||||
const ranks = [...genRanks(2)]
|
||||
await Promise.all([
|
||||
client.createDoc(task.class.WonState, attachedTo, {
|
||||
title: 'Won',
|
||||
ofAttribute: task.attribute.DoneState,
|
||||
name: 'Won',
|
||||
rank: ranks[0]
|
||||
}),
|
||||
client.createDoc(task.class.LostState, attachedTo, {
|
||||
title: 'Lost',
|
||||
ofAttribute: task.attribute.DoneState,
|
||||
name: 'Lost',
|
||||
rank: ranks[1]
|
||||
})
|
||||
])
|
||||
@ -325,8 +328,10 @@ export async function createKanban (
|
||||
tmplStates.map(
|
||||
async (state) =>
|
||||
await client.createDoc(task.class.State, attachedTo, {
|
||||
ofAttribute: task.attribute.State,
|
||||
color: state.color,
|
||||
title: state.title,
|
||||
description: state.description,
|
||||
name: state.name,
|
||||
rank: state.rank
|
||||
})
|
||||
)
|
||||
@ -345,7 +350,12 @@ export async function createKanban (
|
||||
return
|
||||
}
|
||||
|
||||
return await client.createDoc(cl, attachedTo, { title: state.title, rank: state.rank })
|
||||
return await client.createDoc(cl, attachedTo, {
|
||||
ofAttribute: task.attribute.DoneState,
|
||||
description: state.description,
|
||||
name: state.name,
|
||||
rank: state.rank
|
||||
})
|
||||
})
|
||||
)
|
||||
|
||||
|
@ -42,7 +42,9 @@
|
||||
"CreateProject": "Create project",
|
||||
"NewProject": "New project",
|
||||
"ProjectTitlePlaceholder": "Project title",
|
||||
"ProjectIdentifierPlaceholder": "Project ID",
|
||||
"Identifier": "Project Identifier",
|
||||
"IdentifierExists": "Project identifier already exists",
|
||||
"ProjectIdentifierPlaceholder": "Project Identifier",
|
||||
"ChooseIcon": "Choose icon",
|
||||
"AddIssue": "Add Issue",
|
||||
"NewIssue": "New issue",
|
||||
|
@ -42,6 +42,8 @@
|
||||
"CreateProject": "Создать проект",
|
||||
"NewProject": "Новый проект",
|
||||
"ProjectTitlePlaceholder": "Название проекта",
|
||||
"Identifier": "Идентификатор проекта",
|
||||
"IdentifierExists": "Идентификатор уже существует проекта",
|
||||
"ProjectIdentifierPlaceholder": "Идентификатор проекта",
|
||||
"ChooseIcon": "Выбрать иконку",
|
||||
"AddIssue": "Добавить задачу",
|
||||
|
@ -13,7 +13,7 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { AttachedData, FindOptions, Ref, SortingOrder } from '@hcengineering/core'
|
||||
import core, { AttachedData, FindOptions, Ref, SortingOrder } from '@hcengineering/core'
|
||||
import { getClient, ObjectPopup } from '@hcengineering/presentation'
|
||||
import { calcRank, Issue, IssueDraft } from '@hcengineering/tracker'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
@ -29,7 +29,7 @@
|
||||
const options: FindOptions<Issue> = {
|
||||
lookup: {
|
||||
space: tracker.class.Project,
|
||||
status: [tracker.class.IssueStatus, { category: tracker.class.IssueStatusCategory }]
|
||||
status: [tracker.class.IssueStatus, { category: core.class.StatusCategory }]
|
||||
},
|
||||
sort: { modifiedOn: SortingOrder.Descending }
|
||||
}
|
||||
|
@ -1,11 +1,11 @@
|
||||
<script lang="ts">
|
||||
import { StatusCategory } from '@hcengineering/core'
|
||||
import { IconSize } from '@hcengineering/ui'
|
||||
import { IssueStatusCategory } from '@hcengineering/tracker'
|
||||
import tracker from '../../plugin'
|
||||
|
||||
export let size: IconSize
|
||||
export let fill: string = 'currentColor'
|
||||
export let category: IssueStatusCategory
|
||||
export let category: StatusCategory
|
||||
export let statusIcon: {
|
||||
index: number | undefined
|
||||
count: number | undefined
|
||||
|
@ -4,7 +4,7 @@
|
||||
import { Issue, IssueStatus } from '@hcengineering/tracker'
|
||||
import { Label, ticker } from '@hcengineering/ui'
|
||||
import tracker from '../../plugin'
|
||||
import { statusStore } from '../../utils'
|
||||
import { statusStore } from '@hcengineering/presentation'
|
||||
import Duration from './Duration.svelte'
|
||||
import StatusPresenter from './StatusPresenter.svelte'
|
||||
|
||||
|
@ -13,12 +13,12 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { WithLookup } from '@hcengineering/core'
|
||||
import core, { StatusCategory, WithLookup } from '@hcengineering/core'
|
||||
import { createQuery, getClient } from '@hcengineering/presentation'
|
||||
import { IssueStatus, IssueStatusCategory } from '@hcengineering/tracker'
|
||||
import { IssueStatus } from '@hcengineering/tracker'
|
||||
import { getPlatformColor, IconSize } from '@hcengineering/ui'
|
||||
import tracker from '../../plugin'
|
||||
import { statusStore } from '../../utils'
|
||||
import { statusStore } from '@hcengineering/presentation'
|
||||
import StatusIcon from '../icons/StatusIcon.svelte'
|
||||
|
||||
export let value: WithLookup<IssueStatus>
|
||||
@ -29,7 +29,7 @@
|
||||
|
||||
const client = getClient()
|
||||
|
||||
let category: IssueStatusCategory | undefined
|
||||
let category: StatusCategory | undefined
|
||||
let statuses: IssueStatus[] = []
|
||||
const statusIcon: {
|
||||
index: number | undefined
|
||||
@ -40,8 +40,8 @@
|
||||
|
||||
$: if (value.category === tracker.issueStatusCategory.Started) {
|
||||
const _s = [
|
||||
...$statusStore.statuses.filter(
|
||||
(it) => it.attachedTo === value.attachedTo && it.category === tracker.issueStatusCategory.Started
|
||||
...$statusStore.filter(
|
||||
(it) => it.ofAttribute === value.ofAttribute && it.category === tracker.issueStatusCategory.Started
|
||||
)
|
||||
]
|
||||
_s.sort((a, b) => a.rank.localeCompare(b.rank))
|
||||
@ -54,9 +54,9 @@
|
||||
category = status.$lookup.category
|
||||
}
|
||||
if (category === undefined) {
|
||||
category = await client.findOne(tracker.class.IssueStatusCategory, { _id: value.category })
|
||||
category = await client.findOne(core.class.StatusCategory, { _id: value.category })
|
||||
}
|
||||
if (dynamicFillCategories.includes(value.category)) {
|
||||
if (value.category !== undefined && dynamicFillCategories.includes(value.category)) {
|
||||
const index = statuses.findIndex((p) => p._id === value._id)
|
||||
if (index !== -1) {
|
||||
statusIcon.index = index + 1
|
||||
|
@ -13,55 +13,66 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import contact, { Employee } from '@hcengineering/contact'
|
||||
import { employeeByIdStore, employeesStore } from '@hcengineering/contact-resources'
|
||||
import { Class, Doc, DocumentQuery, generateId, IdMap, Lookup, Ref, toIdMap, WithLookup } from '@hcengineering/core'
|
||||
import { Kanban, TypeState } from '@hcengineering/kanban'
|
||||
import contact from '@hcengineering/contact'
|
||||
import { employeeByIdStore } from '@hcengineering/contact-resources'
|
||||
import {
|
||||
CategoryType,
|
||||
Class,
|
||||
Doc,
|
||||
DocumentQuery,
|
||||
DocumentUpdate,
|
||||
generateId,
|
||||
Lookup,
|
||||
Ref,
|
||||
WithLookup
|
||||
} from '@hcengineering/core'
|
||||
import { Item, Kanban } from '@hcengineering/kanban'
|
||||
import notification from '@hcengineering/notification'
|
||||
import { getResource } from '@hcengineering/platform'
|
||||
import { createQuery, getClient } from '@hcengineering/presentation'
|
||||
import { createQuery, getClient, statusStore } from '@hcengineering/presentation'
|
||||
import tags from '@hcengineering/tags'
|
||||
import {
|
||||
Component as ComponentType,
|
||||
Issue,
|
||||
IssuesGrouping,
|
||||
IssuesOrdering,
|
||||
IssueStatus,
|
||||
Project,
|
||||
Sprint
|
||||
} from '@hcengineering/tracker'
|
||||
import { Issue, IssuesGrouping, IssuesOrdering, Project } from '@hcengineering/tracker'
|
||||
import {
|
||||
Button,
|
||||
Component,
|
||||
getEventPositionElement,
|
||||
Icon,
|
||||
IconAdd,
|
||||
Label,
|
||||
Loading,
|
||||
showPanel,
|
||||
showPopup,
|
||||
tooltip
|
||||
} from '@hcengineering/ui'
|
||||
import { CategoryOption, Viewlet, ViewOptionModel, ViewOptions, ViewQueryOption } from '@hcengineering/view'
|
||||
import {
|
||||
AttributeModel,
|
||||
CategoryOption,
|
||||
Viewlet,
|
||||
ViewOptionModel,
|
||||
ViewOptions,
|
||||
ViewQueryOption
|
||||
} from '@hcengineering/view'
|
||||
import {
|
||||
ActionContext,
|
||||
focusStore,
|
||||
getCategories,
|
||||
getGroupByValues,
|
||||
getPresenter,
|
||||
groupBy,
|
||||
ListSelectionProvider,
|
||||
Menu,
|
||||
noCategory,
|
||||
SelectDirection,
|
||||
selectionStore
|
||||
selectionStore,
|
||||
setGroupByValues
|
||||
} from '@hcengineering/view-resources'
|
||||
import { sortCategories } from '@hcengineering/view-resources/src/utils'
|
||||
import view from '@hcengineering/view-resources/src/plugin'
|
||||
import { onMount } from 'svelte'
|
||||
import tracker from '../../plugin'
|
||||
import { issuesGroupBySorting, mapKanbanCategories } from '../../utils'
|
||||
import ComponentEditor from '../components/ComponentEditor.svelte'
|
||||
import CreateIssue from '../CreateIssue.svelte'
|
||||
import AssigneePresenter from './AssigneePresenter.svelte'
|
||||
import SubIssuesSelector from './edit/SubIssuesSelector.svelte'
|
||||
import IssuePresenter from './IssuePresenter.svelte'
|
||||
import IssueStatusIcon from './IssueStatusIcon.svelte'
|
||||
import ParentNamesPresenter from './ParentNamesPresenter.svelte'
|
||||
import PriorityEditor from './PriorityEditor.svelte'
|
||||
import StatusEditor from './StatusEditor.svelte'
|
||||
@ -75,9 +86,12 @@
|
||||
export let viewlet: Viewlet
|
||||
|
||||
$: currentSpace = space || tracker.project.DefaultProject
|
||||
$: groupBy = (viewOptions.groupBy[0] ?? noCategory) as IssuesGrouping
|
||||
$: groupByKey = (viewOptions.groupBy[0] ?? noCategory) as IssuesGrouping
|
||||
$: orderBy = viewOptions.orderBy
|
||||
$: sort = { [orderBy[0]]: orderBy[1] }
|
||||
|
||||
// issuesGroupBySorting[groupByKey]
|
||||
|
||||
$: dontUpdateRank = orderBy[0] !== IssuesOrdering.Manual
|
||||
|
||||
const spaceQuery = createQuery()
|
||||
@ -116,8 +130,12 @@
|
||||
const lookup: Lookup<Issue> = {
|
||||
assignee: contact.class.Employee,
|
||||
space: tracker.class.Project,
|
||||
status: tracker.class.IssueStatus,
|
||||
component: tracker.class.Component,
|
||||
sprint: tracker.class.Sprint,
|
||||
_id: {
|
||||
subIssues: tracker.class.Issue
|
||||
subIssues: tracker.class.Issue,
|
||||
labels: tags.class.TagReference
|
||||
}
|
||||
}
|
||||
|
||||
@ -137,11 +155,9 @@
|
||||
}
|
||||
const issuesQuery = createQuery()
|
||||
let issues: Issue[] = []
|
||||
const lookupIssue: Lookup<Issue> = {
|
||||
status: tracker.class.IssueStatus,
|
||||
component: tracker.class.Component,
|
||||
sprint: tracker.class.Sprint
|
||||
}
|
||||
|
||||
$: groupByDocs = groupBy(issues, groupByKey, categories)
|
||||
|
||||
$: issuesQuery.query(
|
||||
tracker.class.Issue,
|
||||
resultQuery,
|
||||
@ -149,80 +165,19 @@
|
||||
issues = result
|
||||
},
|
||||
{
|
||||
lookup: lookupIssue,
|
||||
sort: issuesGroupBySorting[groupBy]
|
||||
lookup,
|
||||
sort
|
||||
}
|
||||
)
|
||||
|
||||
const statusesQuery = createQuery()
|
||||
let statuses: WithLookup<IssueStatus>[] = []
|
||||
let statusesMap: IdMap<IssueStatus> = new Map()
|
||||
$: statusesQuery.query(
|
||||
tracker.class.IssueStatus,
|
||||
{
|
||||
space: currentSpace
|
||||
},
|
||||
(result) => {
|
||||
statuses = result
|
||||
statusesMap = toIdMap(result)
|
||||
},
|
||||
{
|
||||
lookup: { category: tracker.class.IssueStatusCategory }
|
||||
}
|
||||
)
|
||||
|
||||
const componentsQuery = createQuery()
|
||||
let components: ComponentType[] = []
|
||||
$: componentsQuery.query(
|
||||
tracker.class.Component,
|
||||
{
|
||||
space: currentSpace
|
||||
},
|
||||
(result) => {
|
||||
components = result
|
||||
}
|
||||
)
|
||||
|
||||
const sprintsQuery = createQuery()
|
||||
let sprints: Sprint[] = []
|
||||
$: sprintsQuery.query(
|
||||
tracker.class.Sprint,
|
||||
{
|
||||
space: currentSpace
|
||||
},
|
||||
(result) => {
|
||||
sprints = result
|
||||
}
|
||||
)
|
||||
|
||||
let states: TypeState[]
|
||||
let categories: CategoryType[] = []
|
||||
|
||||
const queryId = generateId()
|
||||
|
||||
$: updateCategories(
|
||||
tracker.class.Issue,
|
||||
issues,
|
||||
groupBy,
|
||||
viewOptions,
|
||||
viewOptionsConfig,
|
||||
statuses,
|
||||
components,
|
||||
sprints,
|
||||
$employeesStore
|
||||
)
|
||||
$: updateCategories(tracker.class.Issue, issues, groupByKey, viewOptions, viewOptionsConfig)
|
||||
|
||||
function update () {
|
||||
updateCategories(
|
||||
tracker.class.Issue,
|
||||
issues,
|
||||
groupBy,
|
||||
viewOptions,
|
||||
viewOptionsConfig,
|
||||
statuses,
|
||||
components,
|
||||
sprints,
|
||||
$employeesStore
|
||||
)
|
||||
updateCategories(tracker.class.Issue, issues, groupByKey, viewOptions, viewOptionsConfig)
|
||||
}
|
||||
|
||||
async function updateCategories (
|
||||
@ -230,45 +185,50 @@
|
||||
docs: Doc[],
|
||||
groupByKey: string,
|
||||
viewOptions: ViewOptions,
|
||||
viewOptionsModel: ViewOptionModel[] | undefined,
|
||||
statuses: WithLookup<IssueStatus>[],
|
||||
components: ComponentType[],
|
||||
sprints: Sprint[],
|
||||
assignee: Employee[]
|
||||
viewOptionsModel: ViewOptionModel[] | undefined
|
||||
) {
|
||||
let categories = await getCategories(client, _class, docs, groupByKey, viewlet.descriptor)
|
||||
categories = await getCategories(client, _class, docs, groupByKey, $statusStore, viewlet.descriptor)
|
||||
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 = await f(_class, space, groupByKey, update, queryId)
|
||||
const categoryAction = await getResource(categoryFunc.action)
|
||||
const res = await categoryAction(_class, space, groupByKey, update, queryId, $statusStore, viewlet.descriptor)
|
||||
if (res !== undefined) {
|
||||
for (const category of categories) {
|
||||
if (!res.includes(category)) {
|
||||
res.push(category)
|
||||
}
|
||||
}
|
||||
|
||||
categories = await sortCategories(client, _class, res, groupByKey, viewlet.descriptor)
|
||||
categories = res
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
const indexes = new Map(categories.map((p, i) => [p, i]))
|
||||
const res = await mapKanbanCategories(groupByKey, categories, statuses, components, sprints, assignee)
|
||||
res.sort((a, b) => {
|
||||
const aIndex = indexes.get(a._id ?? undefined) ?? -1
|
||||
const bIndex = indexes.get(b._id ?? undefined) ?? -1
|
||||
return aIndex - bIndex
|
||||
})
|
||||
states = res
|
||||
}
|
||||
|
||||
const fullFilled: { [key: string]: boolean } = {}
|
||||
|
||||
function getHeader (_class: Ref<Class<Doc>>, groupByKey: string): void {
|
||||
if (groupByKey === noCategory) {
|
||||
headerComponent = undefined
|
||||
} else {
|
||||
getPresenter(client, _class, { key: groupByKey }, { key: groupByKey }).then((p) => (headerComponent = p))
|
||||
}
|
||||
}
|
||||
|
||||
let headerComponent: AttributeModel | undefined
|
||||
$: getHeader(tracker.class.Issue, groupByKey)
|
||||
|
||||
const getUpdateProps = (doc: Doc, category: CategoryType): DocumentUpdate<Item> | undefined => {
|
||||
const groupValue =
|
||||
typeof category === 'object' ? category.values.find((it) => it.space === doc.space)?._id : category
|
||||
if (groupValue === undefined) {
|
||||
return undefined
|
||||
}
|
||||
return {
|
||||
[groupByKey]: groupValue,
|
||||
space: doc.space
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if !states?.length}
|
||||
{#if categories.length === 0}}
|
||||
<Loading />
|
||||
{:else}
|
||||
<ActionContext
|
||||
@ -279,12 +239,14 @@
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<Kanban
|
||||
bind:this={kanbanUI}
|
||||
_class={tracker.class.Issue}
|
||||
{states}
|
||||
{categories}
|
||||
{dontUpdateRank}
|
||||
options={{ sort, lookup }}
|
||||
query={resultQuery}
|
||||
fieldName={groupBy}
|
||||
objects={issues}
|
||||
getGroupByValues={(groupByDocs, category) =>
|
||||
groupByKey === noCategory ? issues : getGroupByValues(groupByDocs, category)}
|
||||
{setGroupByValues}
|
||||
{getUpdateProps}
|
||||
{groupByDocs}
|
||||
on:content={(evt) => {
|
||||
listProvider.update(evt.detail)
|
||||
}}
|
||||
@ -299,27 +261,23 @@
|
||||
on:contextmenu={(evt) => showMenu(evt.detail.evt, evt.detail.objects)}
|
||||
>
|
||||
<svelte:fragment slot="header" let:state let:count>
|
||||
{@const status = statusesMap.get(state._id)}
|
||||
<!-- {@const status = $statusStore.get(state._id)} -->
|
||||
<div class="header flex-col">
|
||||
<div class="flex-between label font-medium w-full h-full">
|
||||
<div class="flex-row-center gap-2">
|
||||
{#if state.icon}
|
||||
{#if groupBy === 'status' && status}
|
||||
<IssueStatusIcon value={status} size="small" />
|
||||
{:else}
|
||||
<Icon icon={state.icon} size="small" />
|
||||
{/if}
|
||||
{/if}
|
||||
<span class="lines-limit-2 ml-2">{state.title}</span>
|
||||
<span class="counter ml-2 text-md">{count}</span>
|
||||
</div>
|
||||
<div class="flex-row-center flex-between">
|
||||
{#if groupByKey === noCategory}
|
||||
<span class="text-base fs-bold overflow-label content-accent-color pointer-events-none">
|
||||
<Label label={view.string.NoGrouping} />
|
||||
</span>
|
||||
{:else if headerComponent}
|
||||
<svelte:component this={headerComponent.presenter} value={state} {space} kind={'list-header'} />
|
||||
{/if}
|
||||
<div class="flex gap-1">
|
||||
<Button
|
||||
icon={IconAdd}
|
||||
kind={'transparent'}
|
||||
showTooltip={{ label: tracker.string.AddIssueTooltip, direction: 'left' }}
|
||||
on:click={() => {
|
||||
showPopup(CreateIssue, { space: currentSpace, [groupBy]: state._id }, 'top')
|
||||
showPopup(CreateIssue, { space: currentSpace, [groupByKey]: state._id }, 'top')
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
@ -329,69 +287,71 @@
|
||||
<svelte:fragment slot="card" let:object>
|
||||
{@const issue = toIssue(object)}
|
||||
{@const issueId = object._id}
|
||||
<div
|
||||
class="tracker-card"
|
||||
on:click={() => {
|
||||
showPanel(tracker.component.EditIssue, object._id, object._class, 'content')
|
||||
}}
|
||||
>
|
||||
<div class="flex-col ml-4 mr-8">
|
||||
<div class="flex clear-mins names">
|
||||
<IssuePresenter value={issue} />
|
||||
<ParentNamesPresenter value={issue} />
|
||||
{#key issueId}
|
||||
<div
|
||||
class="tracker-card"
|
||||
on:click={() => {
|
||||
showPanel(tracker.component.EditIssue, object._id, object._class, 'content')
|
||||
}}
|
||||
>
|
||||
<div class="flex-col ml-4 mr-8">
|
||||
<div class="flex clear-mins names">
|
||||
<IssuePresenter value={issue} />
|
||||
<ParentNamesPresenter value={issue} />
|
||||
</div>
|
||||
<div class="flex-row-center gap-1 mt-1">
|
||||
{#if groupByKey !== 'status'}
|
||||
<StatusEditor value={issue} kind="list" isEditable={false} />
|
||||
{/if}
|
||||
<span class="fs-bold caption-color lines-limit-2">
|
||||
{object.title}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-row-center gap-1 mt-1">
|
||||
{#if groupBy !== 'status'}
|
||||
<StatusEditor value={issue} kind="list" isEditable={false} />
|
||||
{/if}
|
||||
<span class="fs-bold caption-color lines-limit-2">
|
||||
{object.title}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="abs-rt-content">
|
||||
<AssigneePresenter
|
||||
value={issue.assignee ? $employeeByIdStore.get(issue.assignee) : null}
|
||||
defaultClass={contact.class.Employee}
|
||||
object={issue}
|
||||
isEditable={true}
|
||||
/>
|
||||
<div class="flex-center mt-2">
|
||||
<Component is={notification.component.NotificationPresenter} props={{ value: object }} />
|
||||
</div>
|
||||
</div>
|
||||
<div class="buttons-group xsmall-gap states-bar">
|
||||
{#if issue && issue.subIssues > 0}
|
||||
<SubIssuesSelector value={issue} {currentProject} />
|
||||
{/if}
|
||||
<PriorityEditor value={issue} isEditable={true} kind={'link-bordered'} size={'inline'} justify={'center'} />
|
||||
<ComponentEditor
|
||||
value={issue}
|
||||
isEditable={true}
|
||||
kind={'link-bordered'}
|
||||
size={'inline'}
|
||||
justify={'center'}
|
||||
width={''}
|
||||
bind:onlyIcon={fullFilled[issueId]}
|
||||
/>
|
||||
<EstimationEditor kind={'list'} size={'small'} value={issue} />
|
||||
<div
|
||||
class="clear-mins"
|
||||
use:tooltip={{
|
||||
component: fullFilled[issueId] ? tags.component.LabelsPresenter : undefined,
|
||||
props: { object: issue, kind: 'full' }
|
||||
}}
|
||||
>
|
||||
<Component
|
||||
is={tags.component.LabelsPresenter}
|
||||
props={{ object: issue, ckeckFilled: fullFilled[issueId] }}
|
||||
on:change={(res) => {
|
||||
if (res.detail.full) fullFilled[issueId] = true
|
||||
}}
|
||||
<div class="abs-rt-content">
|
||||
<AssigneePresenter
|
||||
value={issue.assignee ? $employeeByIdStore.get(issue.assignee) : null}
|
||||
defaultClass={contact.class.Employee}
|
||||
object={issue}
|
||||
isEditable={true}
|
||||
/>
|
||||
<div class="flex-center mt-2">
|
||||
<Component is={notification.component.NotificationPresenter} props={{ value: object }} />
|
||||
</div>
|
||||
</div>
|
||||
<div class="buttons-group xsmall-gap states-bar">
|
||||
{#if issue && issue.subIssues > 0}
|
||||
<SubIssuesSelector value={issue} {currentProject} />
|
||||
{/if}
|
||||
<PriorityEditor value={issue} isEditable={true} kind={'link-bordered'} size={'inline'} justify={'center'} />
|
||||
<ComponentEditor
|
||||
value={issue}
|
||||
isEditable={true}
|
||||
kind={'link-bordered'}
|
||||
size={'inline'}
|
||||
justify={'center'}
|
||||
width={''}
|
||||
bind:onlyIcon={fullFilled[issueId]}
|
||||
/>
|
||||
<EstimationEditor kind={'list'} size={'small'} value={issue} />
|
||||
<div
|
||||
class="clear-mins"
|
||||
use:tooltip={{
|
||||
component: fullFilled[issueId] ? tags.component.LabelsPresenter : undefined,
|
||||
props: { object: issue, kind: 'full' }
|
||||
}}
|
||||
>
|
||||
<Component
|
||||
is={tags.component.LabelsPresenter}
|
||||
props={{ object: issue, ckeckFilled: fullFilled[issueId], lookupField: 'labels' }}
|
||||
on:change={(res) => {
|
||||
if (res.detail.full) fullFilled[issueId] = true
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/key}
|
||||
</svelte:fragment>
|
||||
</Kanban>
|
||||
{/if}
|
||||
|
@ -20,7 +20,7 @@
|
||||
import { Button, eventToHTMLElement, SelectPopup, showPopup, TooltipAlignment } from '@hcengineering/ui'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import tracker from '../../plugin'
|
||||
import { statusStore } from '../../utils'
|
||||
import { statusStore } from '@hcengineering/presentation'
|
||||
import IssueStatusIcon from './IssueStatusIcon.svelte'
|
||||
import StatusPresenter from './StatusPresenter.svelte'
|
||||
|
||||
@ -65,7 +65,7 @@
|
||||
)
|
||||
}
|
||||
|
||||
$: statuses = $statusStore.statuses.filter((it) => it.attachedTo === value?.space)
|
||||
$: statuses = $statusStore.statuses.filter((it) => it.space === value?.space)
|
||||
|
||||
$: selectedStatus = statuses?.find((status) => status._id === value.status) ?? statuses?.[0]
|
||||
$: selectedStatusLabel = shouldShowLabel ? selectedStatus?.name : undefined
|
||||
|
@ -14,7 +14,6 @@
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { IssueStatus } from '@hcengineering/tracker'
|
||||
import { statusStore } from '../../utils'
|
||||
import IssueStatusIcon from './IssueStatusIcon.svelte'
|
||||
|
||||
export let value: IssueStatus | undefined
|
||||
@ -23,12 +22,11 @@
|
||||
</script>
|
||||
|
||||
{#if value}
|
||||
{@const icon = $statusStore.byId.get(value._id)?.$lookup?.category?.icon}
|
||||
<div class="flex-presenter">
|
||||
{#if !inline && icon}
|
||||
{#if !inline}
|
||||
<IssueStatusIcon {value} {size} />
|
||||
{/if}
|
||||
<span class="overflow-label" class:ml-2={!inline && icon !== undefined}>
|
||||
<span class="overflow-label" class:ml-2={!inline}>
|
||||
{value.name}
|
||||
</span>
|
||||
</div>
|
||||
|
@ -13,15 +13,14 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { Ref } from '@hcengineering/core'
|
||||
import { IssueStatus } from '@hcengineering/tracker'
|
||||
import { statusStore } from '../../utils'
|
||||
import { Ref, Status, StatusValue } from '@hcengineering/core'
|
||||
import { statusStore } from '@hcengineering/presentation'
|
||||
import StatusPresenter from './StatusPresenter.svelte'
|
||||
|
||||
export let value: Ref<IssueStatus> | undefined
|
||||
export let value: Ref<Status> | StatusValue | undefined
|
||||
export let size: 'small' | 'medium' = 'medium'
|
||||
</script>
|
||||
|
||||
{#if value}
|
||||
<StatusPresenter value={$statusStore.byId.get(value)} {size} />
|
||||
<StatusPresenter value={$statusStore.get(typeof value === 'string' ? value : value.values[0]?._id)} {size} />
|
||||
{/if}
|
||||
|
@ -13,7 +13,7 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { Ref, SortingOrder, WithLookup } from '@hcengineering/core'
|
||||
import core, { Ref, SortingOrder, WithLookup } from '@hcengineering/core'
|
||||
import { createQuery } from '@hcengineering/presentation'
|
||||
import { Issue, IssueStatus } from '@hcengineering/tracker'
|
||||
import {
|
||||
@ -101,7 +101,7 @@
|
||||
sort: { modifiedOn: SortingOrder.Descending },
|
||||
lookup: {
|
||||
space: tracker.class.Project,
|
||||
status: [tracker.class.IssueStatus, { category: tracker.class.IssueStatusCategory }]
|
||||
status: [tracker.class.IssueStatus, { category: core.class.StatusCategory }]
|
||||
}
|
||||
}
|
||||
)
|
||||
|
@ -29,7 +29,8 @@
|
||||
} from '@hcengineering/ui'
|
||||
import { getIssueId } from '../../../issues'
|
||||
import tracker from '../../../plugin'
|
||||
import { statusStore, subIssueListProvider } from '../../../utils'
|
||||
import { subIssueListProvider } from '../../../utils'
|
||||
import { statusStore } from '@hcengineering/presentation'
|
||||
|
||||
export let value: WithLookup<Issue>
|
||||
export let currentProject: Project | undefined = undefined
|
||||
|
@ -29,7 +29,8 @@
|
||||
} from '@hcengineering/ui'
|
||||
import { getIssueId } from '../../../issues'
|
||||
import tracker from '../../../plugin'
|
||||
import { statusStore, subIssueListProvider } from '../../../utils'
|
||||
import { subIssueListProvider } from '../../../utils'
|
||||
import { statusStore } from '@hcengineering/presentation'
|
||||
|
||||
export let object: WithLookup<Doc & { related: number }> | undefined
|
||||
export let value: WithLookup<Doc & { related: number }> | undefined
|
||||
|
@ -0,0 +1,41 @@
|
||||
<script lang="ts">
|
||||
import presentation, { Card, getClient } from '@hcengineering/presentation'
|
||||
import { Project } from '@hcengineering/tracker'
|
||||
import EditBox from '@hcengineering/ui/src/components/EditBox.svelte'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import tracker from '../../plugin'
|
||||
|
||||
export let project: Project
|
||||
|
||||
let identifier = project.identifier
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
function save () {
|
||||
dispatch('close', identifier)
|
||||
}
|
||||
|
||||
let projects: Set<string> = new Set()
|
||||
|
||||
$: getClient()
|
||||
.findAll(tracker.class.Project, {})
|
||||
.then((pr) => {
|
||||
projects = new Set(pr.map((p) => p.identifier))
|
||||
})
|
||||
</script>
|
||||
|
||||
<Card
|
||||
label={projects.has(identifier) ? tracker.string.IdentifierExists : tracker.string.Identifier}
|
||||
okLabel={presentation.string.Save}
|
||||
okAction={save}
|
||||
canSave={identifier !== project.identifier && !projects.has(identifier)}
|
||||
on:close={() => {
|
||||
dispatch('close')
|
||||
}}
|
||||
>
|
||||
<div class="float-left-box">
|
||||
<div class="float-left p-2">
|
||||
<EditBox bind:value={identifier} />
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
@ -14,17 +14,17 @@
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { Employee } from '@hcengineering/contact'
|
||||
import { AccountArrayEditor } from '@hcengineering/contact-resources'
|
||||
import { AccountArrayEditor, AssigneeBox } from '@hcengineering/contact-resources'
|
||||
import core, { Account, DocumentUpdate, generateId, getCurrentAccount, Ref, SortingOrder } from '@hcengineering/core'
|
||||
import { Asset } from '@hcengineering/platform'
|
||||
import presentation, { Card, getClient } from '@hcengineering/presentation'
|
||||
import { AssigneeBox } from '@hcengineering/contact-resources'
|
||||
import { StyledTextBox } from '@hcengineering/text-editor'
|
||||
import { genRanks, IssueStatus, Project, TimeReportDayType } from '@hcengineering/tracker'
|
||||
import { Button, EditBox, eventToHTMLElement, Label, showPopup, ToggleWithLabel } from '@hcengineering/ui'
|
||||
import { Button, EditBox, eventToHTMLElement, IconEdit, Label, showPopup, ToggleWithLabel } from '@hcengineering/ui'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import tracker from '../../plugin'
|
||||
import TimeReportDayDropdown from '../issues/timereport/TimeReportDayDropdown.svelte'
|
||||
import ChangeIdentity from './ChangeIdentity.svelte'
|
||||
import ProjectIconChooser from './ProjectIconChooser.svelte'
|
||||
|
||||
export let project: Project | undefined = undefined
|
||||
@ -50,7 +50,7 @@
|
||||
isNew ? createProject() : updateProject()
|
||||
}
|
||||
|
||||
let identifier: string = 'TSK'
|
||||
let identifier: string = project?.identifier ?? 'TSK'
|
||||
|
||||
const defaultStatusId: Ref<IssueStatus> = generateId()
|
||||
|
||||
@ -72,7 +72,7 @@
|
||||
}
|
||||
|
||||
async function updateProject () {
|
||||
const { sequence, issueStatuses, defaultIssueStatus, identifier, ...projectData } = getProjectData()
|
||||
const { sequence, issueStatuses, defaultIssueStatus, ...projectData } = getProjectData()
|
||||
const update: DocumentUpdate<Project> = {}
|
||||
if (projectData.name !== project?.name) {
|
||||
update.name = projectData.name
|
||||
@ -92,6 +92,9 @@
|
||||
if (projectData.defaultTimeReportDay !== project?.defaultTimeReportDay) {
|
||||
update.defaultTimeReportDay = projectData.defaultTimeReportDay
|
||||
}
|
||||
if (projectData.identifier !== project?.identifier) {
|
||||
update.identifier = projectData.identifier
|
||||
}
|
||||
if (projectData.members.length !== project?.members.length) {
|
||||
update.members = projectData.members
|
||||
} else {
|
||||
@ -118,8 +121,8 @@
|
||||
defaultCategoryId = tracker.issueStatusCategory.Backlog
|
||||
): Promise<void> {
|
||||
const categories = await client.findAll(
|
||||
tracker.class.IssueStatusCategory,
|
||||
{},
|
||||
core.class.StatusCategory,
|
||||
{ ofAttribute: tracker.attribute.IssueStatus },
|
||||
{ sort: { order: SortingOrder.Ascending } }
|
||||
)
|
||||
const issueStatusRanks = [...genRanks(categories.length)]
|
||||
@ -128,15 +131,19 @@
|
||||
const { _id: category, defaultStatusName } = statusCategory
|
||||
const rank = issueStatusRanks[i]
|
||||
|
||||
await client.addCollection(
|
||||
tracker.class.IssueStatus,
|
||||
projectId,
|
||||
projectId,
|
||||
tracker.class.Project,
|
||||
'issueStatuses',
|
||||
{ name: defaultStatusName, category, rank },
|
||||
category === defaultCategoryId ? defaultStatusId : undefined
|
||||
)
|
||||
if (defaultStatusName !== undefined) {
|
||||
await client.createDoc(
|
||||
tracker.class.IssueStatus,
|
||||
projectId,
|
||||
{
|
||||
ofAttribute: tracker.attribute.IssueStatus,
|
||||
name: defaultStatusName,
|
||||
category,
|
||||
rank
|
||||
},
|
||||
category === defaultCategoryId ? defaultStatusId : undefined
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -147,6 +154,13 @@
|
||||
}
|
||||
})
|
||||
}
|
||||
function changeIdentity (ev: MouseEvent) {
|
||||
showPopup(ChangeIdentity, { project }, eventToHTMLElement(ev), (result) => {
|
||||
if (result != null) {
|
||||
identifier = result
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<Card
|
||||
@ -170,12 +184,17 @@
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<EditBox
|
||||
bind:value={identifier}
|
||||
disabled={!isNew}
|
||||
placeholder={tracker.string.ProjectIdentifierPlaceholder}
|
||||
kind={'large-style'}
|
||||
/>
|
||||
<div class="flex-row-center">
|
||||
<EditBox
|
||||
bind:value={identifier}
|
||||
disabled={!isNew}
|
||||
placeholder={tracker.string.ProjectIdentifierPlaceholder}
|
||||
kind={'large-style'}
|
||||
/>
|
||||
{#if !isNew}
|
||||
<Button size={'small'} icon={IconEdit} on:click={changeIdentity} />
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<StyledTextBox
|
||||
alwaysEdit
|
||||
|
@ -17,7 +17,7 @@
|
||||
import { Issue } from '@hcengineering/tracker'
|
||||
import { floorFractionDigits, Label } from '@hcengineering/ui'
|
||||
import tracker from '../../plugin'
|
||||
import { statusStore } from '../../utils'
|
||||
import { statusStore } from '@hcengineering/presentation'
|
||||
import EstimationProgressCircle from '../issues/timereport/EstimationProgressCircle.svelte'
|
||||
import TimePresenter from '../issues/timereport/TimePresenter.svelte'
|
||||
export let docs: Issue[] | undefined = undefined
|
||||
|
@ -13,9 +13,9 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { AttachedData, Class, Ref, SortingOrder } from '@hcengineering/core'
|
||||
import core, { Class, Data, Ref, SortingOrder, StatusCategory } from '@hcengineering/core'
|
||||
import { createQuery, getClient, MessageBox } from '@hcengineering/presentation'
|
||||
import { calcRank, IssueStatus, IssueStatusCategory, Project } from '@hcengineering/tracker'
|
||||
import { calcRank, IssueStatus, Project } from '@hcengineering/tracker'
|
||||
import {
|
||||
Button,
|
||||
closeTooltip,
|
||||
@ -28,10 +28,10 @@
|
||||
Scroller,
|
||||
showPopup
|
||||
} from '@hcengineering/ui'
|
||||
import { statusStore } from '@hcengineering/presentation'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import { flip } from 'svelte/animate'
|
||||
import tracker from '../../plugin'
|
||||
import { statusStore } from '../../utils'
|
||||
import StatusEditor from './StatusEditor.svelte'
|
||||
import StatusPresenter from './StatusPresenter.svelte'
|
||||
|
||||
@ -43,9 +43,9 @@
|
||||
const projectQuery = createQuery()
|
||||
|
||||
let project: Project | undefined
|
||||
let statusCategories: IssueStatusCategory[] | undefined
|
||||
let statusCategories: StatusCategory[] | undefined
|
||||
|
||||
let editingStatus: IssueStatus | Partial<AttachedData<IssueStatus>> | null = null
|
||||
let editingStatus: IssueStatus | Partial<Data<IssueStatus>> | null = null
|
||||
let draggingStatus: IssueStatus | null = null
|
||||
let hoveringStatus: IssueStatus | null = null
|
||||
|
||||
@ -53,8 +53,8 @@
|
||||
|
||||
async function updateStatusCategories () {
|
||||
statusCategories = await client.findAll(
|
||||
tracker.class.IssueStatusCategory,
|
||||
{},
|
||||
core.class.StatusCategory,
|
||||
{ ofAttribute: tracker.attribute.IssueStatus },
|
||||
{ sort: { order: SortingOrder.Ascending } }
|
||||
)
|
||||
}
|
||||
@ -72,20 +72,14 @@
|
||||
const nextStatus = $statusStore.statuses[$statusStore.statuses.findIndex(({ _id }) => _id === prevStatus._id) + 1]
|
||||
|
||||
isSaving = true
|
||||
await client.addCollection(
|
||||
tracker.class.IssueStatus,
|
||||
projectId,
|
||||
projectId,
|
||||
tracker.class.Project,
|
||||
'issueStatuses',
|
||||
{
|
||||
name: editingStatus.name,
|
||||
description: editingStatus.description,
|
||||
color: editingStatus.color,
|
||||
category: editingStatus.category,
|
||||
rank: calcRank(prevStatus, nextStatus)
|
||||
}
|
||||
)
|
||||
await client.createDoc(tracker.class.IssueStatus, projectId, {
|
||||
ofAttribute: tracker.attribute.IssueStatus,
|
||||
name: editingStatus.name,
|
||||
description: editingStatus.description,
|
||||
color: editingStatus.color,
|
||||
category: editingStatus.category,
|
||||
rank: calcRank(prevStatus, nextStatus)
|
||||
})
|
||||
isSaving = false
|
||||
}
|
||||
|
||||
@ -101,7 +95,7 @@
|
||||
return
|
||||
}
|
||||
|
||||
const updates: Partial<AttachedData<IssueStatus>> = {}
|
||||
const updates: Partial<Data<IssueStatus>> = {}
|
||||
if (status.name !== editingStatus.name) {
|
||||
updates.name = editingStatus.name
|
||||
}
|
||||
@ -198,7 +192,7 @@
|
||||
}
|
||||
|
||||
async function handleDrop (toItem: IssueStatus) {
|
||||
if (draggingStatus?._id !== toItem._id && draggingStatus?.category === toItem.category) {
|
||||
if (draggingStatus != null && draggingStatus?._id !== toItem._id && draggingStatus?.category === toItem.category) {
|
||||
const fromIndex = getStatusIndex(draggingStatus)
|
||||
const toIndex = getStatusIndex(toItem)
|
||||
const [prev, next] = [
|
||||
@ -251,7 +245,7 @@
|
||||
<div class="popupPanel-body__main-content py-10 clear-mins">
|
||||
{#each statusCategories as category}
|
||||
{@const statuses =
|
||||
$statusStore.statuses.filter((s) => s.attachedTo === projectId && s.category === category._id) ?? []}
|
||||
$statusStore.statuses.filter((s) => s.space === projectId && s.category === category._id) ?? []}
|
||||
{@const isSingle = statuses.length === 1}
|
||||
<div class="flex-between category-name">
|
||||
<Label label={category.label} />
|
||||
|
@ -25,8 +25,19 @@ import {
|
||||
} from '@hcengineering/core'
|
||||
import { Resources, translate } from '@hcengineering/platform'
|
||||
import { getClient, MessageBox, ObjectSearchResult } from '@hcengineering/presentation'
|
||||
import { Issue, Scrum, ScrumRecord, Sprint, Project } from '@hcengineering/tracker'
|
||||
import { Issue, Project, Scrum, ScrumRecord, Sprint } from '@hcengineering/tracker'
|
||||
import { showPopup } from '@hcengineering/ui'
|
||||
import ComponentEditor from './components/components/ComponentEditor.svelte'
|
||||
import ComponentPresenter from './components/components/ComponentPresenter.svelte'
|
||||
import Components from './components/components/Components.svelte'
|
||||
import ComponentStatusEditor from './components/components/ComponentStatusEditor.svelte'
|
||||
import ComponentStatusPresenter from './components/components/ComponentStatusPresenter.svelte'
|
||||
import ComponentTitlePresenter from './components/components/ComponentTitlePresenter.svelte'
|
||||
import EditComponent from './components/components/EditComponent.svelte'
|
||||
import IconPresenter from './components/components/IconComponent.svelte'
|
||||
import LeadPresenter from './components/components/LeadPresenter.svelte'
|
||||
import ProjectComponents from './components/components/ProjectComponents.svelte'
|
||||
import TargetDatePresenter from './components/components/TargetDatePresenter.svelte'
|
||||
import CreateIssue from './components/CreateIssue.svelte'
|
||||
import Inbox from './components/inbox/Inbox.svelte'
|
||||
import Active from './components/issues/Active.svelte'
|
||||
@ -52,23 +63,12 @@ import TitlePresenter from './components/issues/TitlePresenter.svelte'
|
||||
import MyIssues from './components/myissues/MyIssues.svelte'
|
||||
import NewIssueHeader from './components/NewIssueHeader.svelte'
|
||||
import NopeComponent from './components/NopeComponent.svelte'
|
||||
import EditComponent from './components/components/EditComponent.svelte'
|
||||
import IconPresenter from './components/components/IconComponent.svelte'
|
||||
import LeadPresenter from './components/components/LeadPresenter.svelte'
|
||||
import ComponentEditor from './components/components/ComponentEditor.svelte'
|
||||
import ComponentPresenter from './components/components/ComponentPresenter.svelte'
|
||||
import Components from './components/components/Components.svelte'
|
||||
import ComponentStatusEditor from './components/components/ComponentStatusEditor.svelte'
|
||||
import ComponentStatusPresenter from './components/components/ComponentStatusPresenter.svelte'
|
||||
import ComponentTitlePresenter from './components/components/ComponentTitlePresenter.svelte'
|
||||
import TargetDatePresenter from './components/components/TargetDatePresenter.svelte'
|
||||
import ProjectComponents from './components/components/ProjectComponents.svelte'
|
||||
import RelationsPopup from './components/RelationsPopup.svelte'
|
||||
import SetDueDateActionPopup from './components/SetDueDateActionPopup.svelte'
|
||||
import SetParentIssueActionPopup from './components/SetParentIssueActionPopup.svelte'
|
||||
import SprintComponentEditor from './components/sprints/SprintComponentEditor.svelte'
|
||||
import SprintDatePresenter from './components/sprints/SprintDatePresenter.svelte'
|
||||
import SprintLeadPresenter from './components/sprints/SprintLeadPresenter.svelte'
|
||||
import SprintComponentEditor from './components/sprints/SprintComponentEditor.svelte'
|
||||
import CreateIssueTemplate from './components/templates/CreateIssueTemplate.svelte'
|
||||
import Views from './components/views/Views.svelte'
|
||||
import Statuses from './components/workflow/Statuses.svelte'
|
||||
@ -107,15 +107,14 @@ import ComponentSelector from './components/ComponentSelector.svelte'
|
||||
import IssueTemplatePresenter from './components/templates/IssueTemplatePresenter.svelte'
|
||||
import IssueTemplates from './components/templates/IssueTemplates.svelte'
|
||||
|
||||
import { deleteObject } from '@hcengineering/view-resources/src/utils'
|
||||
import { deleteObject } from '@hcengineering/view-resources'
|
||||
import MoveAndDeleteSprintPopup from './components/sprints/MoveAndDeleteSprintPopup.svelte'
|
||||
import EditIssueTemplate from './components/templates/EditIssueTemplate.svelte'
|
||||
import TemplateEstimationEditor from './components/templates/EstimationEditor.svelte'
|
||||
import {
|
||||
getAllPriority,
|
||||
getAllComponents,
|
||||
getAllPriority,
|
||||
getAllSprints,
|
||||
getAllStatuses,
|
||||
issuePrioritySort,
|
||||
issueStatusSort,
|
||||
moveIssuesToAnotherSprint,
|
||||
@ -125,14 +124,14 @@ import {
|
||||
} from './utils'
|
||||
|
||||
import { EmployeeAccount } from '@hcengineering/contact'
|
||||
import DeleteComponentPresenter from './components/components/DeleteComponentPresenter.svelte'
|
||||
import StatusRefPresenter from './components/issues/StatusRefPresenter.svelte'
|
||||
import TimeSpendReportPopup from './components/issues/timereport/TimeSpendReportPopup.svelte'
|
||||
import DeleteComponentPresenter from './components/components/DeleteComponentPresenter.svelte'
|
||||
import IssueStatistics from './components/sprints/IssueStatistics.svelte'
|
||||
import SprintRefPresenter from './components/sprints/SprintRefPresenter.svelte'
|
||||
import CreateProject from './components/projects/CreateProject.svelte'
|
||||
import ProjectPresenter from './components/projects/ProjectPresenter.svelte'
|
||||
import MoveIssues from './components/issues/Move.svelte'
|
||||
import IssueStatistics from './components/sprints/IssueStatistics.svelte'
|
||||
import SprintRefPresenter from './components/sprints/SprintRefPresenter.svelte'
|
||||
|
||||
export { default as SubIssueList } from './components/issues/edit/SubIssueList.svelte'
|
||||
|
||||
@ -435,7 +434,6 @@ export default async (): Promise<Resources> => ({
|
||||
IssuePrioritySort: issuePrioritySort,
|
||||
SprintSort: sprintSort,
|
||||
SubIssueQuery: subIssueQuery,
|
||||
GetAllStatuses: getAllStatuses,
|
||||
GetAllPriority: getAllPriority,
|
||||
GetAllComponents: getAllComponents,
|
||||
GetAllSprints: getAllSprints
|
||||
|
@ -12,12 +12,12 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
import { Client, Doc, Ref, Space } from '@hcengineering/core'
|
||||
import { Client, Doc, Ref } from '@hcengineering/core'
|
||||
import type { IntlString, Metadata, Resource } from '@hcengineering/platform'
|
||||
import { mergeIds } from '@hcengineering/platform'
|
||||
import { IssueDraft } from '@hcengineering/tracker'
|
||||
import { AnyComponent, Location } from '@hcengineering/ui'
|
||||
import { SortFunc, Viewlet, ViewletDescriptor, ViewQueryAction } from '@hcengineering/view'
|
||||
import { AllValuesFuncGetter, SortFunc, Viewlet, ViewletDescriptor, ViewQueryAction } from '@hcengineering/view'
|
||||
import tracker, { trackerId } from '../../tracker/lib'
|
||||
|
||||
export default mergeIds(trackerId, tracker, {
|
||||
@ -90,6 +90,7 @@ export default mergeIds(trackerId, tracker, {
|
||||
Low: '' as IntlString,
|
||||
Title: '' as IntlString,
|
||||
Identifier: '' as IntlString,
|
||||
IdentifierExists: '' as IntlString,
|
||||
Description: '' as IntlString,
|
||||
Status: '' as IntlString,
|
||||
DefaultIssueStatus: '' as IntlString,
|
||||
@ -388,17 +389,8 @@ export default mergeIds(trackerId, tracker, {
|
||||
IssuePrioritySort: '' as SortFunc,
|
||||
SprintSort: '' as SortFunc,
|
||||
SubIssueQuery: '' as ViewQueryAction,
|
||||
GetAllStatuses: '' as Resource<
|
||||
(space: Ref<Space> | undefined, onUpdate: () => void, queryId: Ref<Doc>) => Promise<any[] | undefined>
|
||||
>,
|
||||
GetAllPriority: '' as Resource<
|
||||
(space: Ref<Space> | undefined, onUpdate: () => void, queryId: Ref<Doc>) => Promise<any[] | undefined>
|
||||
>,
|
||||
GetAllComponents: '' as Resource<
|
||||
(space: Ref<Space> | undefined, onUpdate: () => void, queryId: Ref<Doc>) => Promise<any[] | undefined>
|
||||
>,
|
||||
GetAllSprints: '' as Resource<
|
||||
(space: Ref<Space> | undefined, onUpdate: () => void, queryId: Ref<Doc>) => Promise<any[] | undefined>
|
||||
>
|
||||
GetAllPriority: '' as Resource<AllValuesFuncGetter>,
|
||||
GetAllComponents: '' as Resource<AllValuesFuncGetter>,
|
||||
GetAllSprints: '' as Resource<AllValuesFuncGetter>
|
||||
}
|
||||
})
|
||||
|
@ -13,7 +13,7 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import { Employee, getName } from '@hcengineering/contact'
|
||||
import { Employee } from '@hcengineering/contact'
|
||||
import core, {
|
||||
ApplyOperations,
|
||||
AttachedDoc,
|
||||
@ -22,28 +22,26 @@ import core, {
|
||||
Doc,
|
||||
DocumentQuery,
|
||||
DocumentUpdate,
|
||||
IdMap,
|
||||
Ref,
|
||||
SortingOrder,
|
||||
Space,
|
||||
StatusCategory,
|
||||
StatusValue,
|
||||
toIdMap,
|
||||
TxCollectionCUD,
|
||||
TxOperations,
|
||||
TxUpdateDoc,
|
||||
WithLookup
|
||||
TxUpdateDoc
|
||||
} from '@hcengineering/core'
|
||||
import { TypeState } from '@hcengineering/kanban'
|
||||
import { Asset, IntlString, translate } from '@hcengineering/platform'
|
||||
import { Asset, IntlString } from '@hcengineering/platform'
|
||||
import { createQuery, getClient } from '@hcengineering/presentation'
|
||||
import { calcRank } from '@hcengineering/task'
|
||||
import {
|
||||
Component,
|
||||
ComponentStatus,
|
||||
Issue,
|
||||
IssuePriority,
|
||||
IssuesDateModificationPeriod,
|
||||
IssuesGrouping,
|
||||
IssuesOrdering,
|
||||
IssueStatus,
|
||||
Project,
|
||||
Sprint,
|
||||
SprintStatus,
|
||||
@ -58,11 +56,9 @@ import {
|
||||
MILLISECONDS_IN_WEEK
|
||||
} from '@hcengineering/ui'
|
||||
import { ViewletDescriptor } from '@hcengineering/view'
|
||||
import { CategoryQuery, ListSelectionProvider, SelectDirection } from '@hcengineering/view-resources'
|
||||
import { writable } from 'svelte/store'
|
||||
import { CategoryQuery, groupBy, ListSelectionProvider, SelectDirection } from '@hcengineering/view-resources'
|
||||
import tracker from './plugin'
|
||||
import { defaultComponentStatuses, defaultPriorities, defaultSprintStatuses, issuePriorities } from './types'
|
||||
import { calcRank } from '@hcengineering/task'
|
||||
import { defaultComponentStatuses, defaultPriorities, defaultSprintStatuses } from './types'
|
||||
|
||||
export * from './types'
|
||||
|
||||
@ -133,18 +129,6 @@ export const getIssuesModificationDatePeriodTime = (period: IssuesDateModificati
|
||||
}
|
||||
}
|
||||
|
||||
export const groupBy = (data: any, key: any): { [key: string]: any[] } => {
|
||||
return data.reduce((storage: { [key: string]: any[] }, item: any) => {
|
||||
const group = item[key] ?? undefined
|
||||
|
||||
storage[group] = storage[group] ?? []
|
||||
|
||||
storage[group].push(item)
|
||||
|
||||
return storage
|
||||
}, {})
|
||||
}
|
||||
|
||||
export interface FilterAction {
|
||||
icon?: Asset | AnySvelteComponent
|
||||
label?: IntlString
|
||||
@ -344,28 +328,33 @@ const listIssueKanbanStatusOrder = [
|
||||
] as const
|
||||
|
||||
export async function issueStatusSort (
|
||||
value: Array<Ref<IssueStatus>>,
|
||||
value: StatusValue[],
|
||||
viewletDescriptorId?: Ref<ViewletDescriptor>
|
||||
): Promise<Array<Ref<IssueStatus>>> {
|
||||
return await new Promise((resolve) => {
|
||||
// TODO: How we track category updates.
|
||||
const query = createQuery(true)
|
||||
query.query(tracker.class.IssueStatus, { _id: { $in: value } }, (res) => {
|
||||
if (viewletDescriptorId === tracker.viewlet.Kanban) {
|
||||
res.sort((a, b) => {
|
||||
const res = listIssueKanbanStatusOrder.indexOf(a.category) - listIssueKanbanStatusOrder.indexOf(b.category)
|
||||
if (res === 0) {
|
||||
return a.rank.localeCompare(b.rank)
|
||||
}
|
||||
return res
|
||||
})
|
||||
} else {
|
||||
res.sort((a, b) => listIssueStatusOrder.indexOf(a.category) - listIssueStatusOrder.indexOf(b.category))
|
||||
): Promise<StatusValue[]> {
|
||||
// TODO: How we track category updates.
|
||||
|
||||
if (viewletDescriptorId === tracker.viewlet.Kanban) {
|
||||
value.sort((a, b) => {
|
||||
const res =
|
||||
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)
|
||||
}
|
||||
resolve(res.map((p) => p._id))
|
||||
query.unsubscribe()
|
||||
return res
|
||||
})
|
||||
})
|
||||
} else {
|
||||
value.sort((a, b) => {
|
||||
const res =
|
||||
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 res
|
||||
})
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
export async function issuePrioritySort (value: IssuePriority[]): Promise<IssuePriority[]> {
|
||||
@ -390,108 +379,6 @@ export async function sprintSort (value: Array<Ref<Sprint>>): Promise<Array<Ref<
|
||||
})
|
||||
}
|
||||
|
||||
export async function mapKanbanCategories (
|
||||
groupBy: string,
|
||||
categories: any[],
|
||||
statuses: Array<WithLookup<IssueStatus>>,
|
||||
components: Component[],
|
||||
sprints: Sprint[],
|
||||
assignee: Employee[]
|
||||
): Promise<TypeState[]> {
|
||||
if (groupBy === IssuesGrouping.NoGrouping) {
|
||||
return [{ _id: undefined, color: UNSET_COLOR, title: await translate(tracker.string.NoGrouping, {}) }]
|
||||
}
|
||||
if (groupBy === IssuesGrouping.Priority) {
|
||||
const res: TypeState[] = []
|
||||
for (const priority of categories) {
|
||||
const title = await translate((issuePriorities as any)[priority].label, {})
|
||||
res.push({
|
||||
_id: priority,
|
||||
title,
|
||||
color: UNSET_COLOR,
|
||||
icon: (issuePriorities as any)[priority].icon
|
||||
})
|
||||
}
|
||||
return res
|
||||
}
|
||||
if (groupBy === IssuesGrouping.Status) {
|
||||
return statuses
|
||||
.filter((p) => categories.includes(p._id))
|
||||
.map((status) => {
|
||||
const category = '$lookup' in status ? status.$lookup?.category : undefined
|
||||
return {
|
||||
_id: status._id,
|
||||
title: status.name,
|
||||
icon: category?.icon,
|
||||
color: status.color ?? category?.color ?? UNSET_COLOR
|
||||
}
|
||||
})
|
||||
}
|
||||
if (groupBy === IssuesGrouping.Assignee) {
|
||||
const noAssignee = await translate(tracker.string.NoAssignee, {})
|
||||
const res: TypeState[] = assignee
|
||||
.filter((p) => categories.includes(p._id))
|
||||
.map((employee) => {
|
||||
return {
|
||||
_id: employee._id,
|
||||
title: getName(employee),
|
||||
color: UNSET_COLOR,
|
||||
icon: undefined
|
||||
}
|
||||
})
|
||||
if (categories.includes(undefined)) {
|
||||
res.push({
|
||||
_id: null,
|
||||
title: noAssignee,
|
||||
color: UNSET_COLOR,
|
||||
icon: undefined
|
||||
})
|
||||
}
|
||||
return res
|
||||
}
|
||||
if (groupBy === IssuesGrouping.Component) {
|
||||
const noComponent = await translate(tracker.string.NoComponent, {})
|
||||
const res: TypeState[] = components
|
||||
.filter((p) => categories.includes(p._id))
|
||||
.map((component) => ({
|
||||
_id: component._id,
|
||||
title: component.label,
|
||||
color: UNSET_COLOR,
|
||||
icon: undefined
|
||||
}))
|
||||
if (categories.includes(undefined)) {
|
||||
res.push({
|
||||
_id: null,
|
||||
title: noComponent,
|
||||
color: UNSET_COLOR,
|
||||
icon: undefined
|
||||
})
|
||||
}
|
||||
return res
|
||||
}
|
||||
if (groupBy === IssuesGrouping.Sprint) {
|
||||
const noSprint = await translate(tracker.string.NoSprint, {})
|
||||
const res: TypeState[] = sprints
|
||||
.filter((p) => categories.includes(p._id))
|
||||
.map((sprint) => ({
|
||||
_id: sprint._id,
|
||||
title: sprint.label,
|
||||
color: UNSET_COLOR,
|
||||
icon: undefined
|
||||
}))
|
||||
if (categories.includes(undefined)) {
|
||||
res.push({
|
||||
_id: null,
|
||||
title: noSprint,
|
||||
color: UNSET_COLOR,
|
||||
icon: undefined
|
||||
})
|
||||
}
|
||||
return res
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
@ -604,14 +491,6 @@ async function getAllSomething (
|
||||
return await promise
|
||||
}
|
||||
|
||||
export async function getAllStatuses (
|
||||
space: Ref<Space> | undefined,
|
||||
onUpdate: () => void,
|
||||
queryId: Ref<Doc>
|
||||
): Promise<any[] | undefined> {
|
||||
return await getAllSomething(tracker.class.IssueStatus, space, onUpdate, queryId)
|
||||
}
|
||||
|
||||
export async function getAllPriority (
|
||||
space: Ref<Space> | undefined,
|
||||
onUpdate: () => void,
|
||||
@ -682,15 +561,6 @@ export async function removeProject (project: Project): Promise<void> {
|
||||
await client.removeDoc(tracker.class.Project, core.space.Space, project._id)
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export interface StatusStore {
|
||||
statuses: Array<WithLookup<IssueStatus>>
|
||||
byId: IdMap<WithLookup<IssueStatus>>
|
||||
version: number
|
||||
}
|
||||
|
||||
async function updateIssuesOnMove (
|
||||
client: TxOperations,
|
||||
applyOps: ApplyOperations,
|
||||
@ -761,27 +631,3 @@ export async function moveIssueToSpace (
|
||||
}
|
||||
await applyOps.commit()
|
||||
}
|
||||
|
||||
// Issue status live query
|
||||
export const statusStore = writable<StatusStore>({ statuses: [], byId: new Map(), version: 0 })
|
||||
|
||||
const query = createQuery(true)
|
||||
query.query(
|
||||
tracker.class.IssueStatus,
|
||||
{},
|
||||
(res) => {
|
||||
statusStore.update((old) => ({
|
||||
version: old.version + 1,
|
||||
statuses: res,
|
||||
byId: toIdMap(res)
|
||||
}))
|
||||
},
|
||||
{
|
||||
lookup: {
|
||||
category: tracker.class.IssueStatusCategory
|
||||
},
|
||||
sort: {
|
||||
rank: SortingOrder.Ascending
|
||||
}
|
||||
}
|
||||
)
|
||||
|
@ -14,35 +14,31 @@
|
||||
//
|
||||
|
||||
import { Employee, EmployeeAccount } from '@hcengineering/contact'
|
||||
import type { AttachedDoc, Class, Doc, Markup, Ref, RelatedDocument, Space, Timestamp, Type } from '@hcengineering/core'
|
||||
import type {
|
||||
AttachedDoc,
|
||||
Attribute,
|
||||
Class,
|
||||
Doc,
|
||||
Markup,
|
||||
Ref,
|
||||
RelatedDocument,
|
||||
Space,
|
||||
Status,
|
||||
StatusCategory,
|
||||
Timestamp,
|
||||
Type
|
||||
} from '@hcengineering/core'
|
||||
import type { Asset, IntlString, Plugin, Resource } from '@hcengineering/platform'
|
||||
import { plugin } from '@hcengineering/platform'
|
||||
import type { TagCategory, TagElement } from '@hcengineering/tags'
|
||||
import { TagReference } from '@hcengineering/tags'
|
||||
import { AnyComponent, Location, ResolvedLocation } from '@hcengineering/ui'
|
||||
import { Action, ActionCategory } from '@hcengineering/view'
|
||||
import { TagReference } from '@hcengineering/tags'
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export interface IssueStatus extends AttachedDoc {
|
||||
name: string
|
||||
description?: string
|
||||
color?: number
|
||||
category: Ref<IssueStatusCategory>
|
||||
rank: string
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export interface IssueStatusCategory extends Doc {
|
||||
icon: Asset
|
||||
label: IntlString
|
||||
color: number
|
||||
defaultStatusName: string
|
||||
order: number
|
||||
}
|
||||
export interface IssueStatus extends Status {}
|
||||
|
||||
/**
|
||||
* @public
|
||||
@ -391,7 +387,6 @@ export default plugin(trackerId, {
|
||||
IssueTemplate: '' as Ref<Class<IssueTemplate>>,
|
||||
Component: '' as Ref<Class<Component>>,
|
||||
IssueStatus: '' as Ref<Class<IssueStatus>>,
|
||||
IssueStatusCategory: '' as Ref<Class<IssueStatusCategory>>,
|
||||
TypeIssuePriority: '' as Ref<Class<Type<IssuePriority>>>,
|
||||
TypeComponentStatus: '' as Ref<Class<Type<ComponentStatus>>>,
|
||||
Sprint: '' as Ref<Class<Sprint>>,
|
||||
@ -417,12 +412,15 @@ export default plugin(trackerId, {
|
||||
CreateIssue: '' as AnyComponent,
|
||||
CreateIssueTemplate: '' as AnyComponent
|
||||
},
|
||||
attribute: {
|
||||
IssueStatus: '' as Ref<Attribute<Status>>
|
||||
},
|
||||
issueStatusCategory: {
|
||||
Backlog: '' as Ref<IssueStatusCategory>,
|
||||
Unstarted: '' as Ref<IssueStatusCategory>,
|
||||
Started: '' as Ref<IssueStatusCategory>,
|
||||
Completed: '' as Ref<IssueStatusCategory>,
|
||||
Canceled: '' as Ref<IssueStatusCategory>
|
||||
Backlog: '' as Ref<StatusCategory>,
|
||||
Unstarted: '' as Ref<StatusCategory>,
|
||||
Started: '' as Ref<StatusCategory>,
|
||||
Completed: '' as Ref<StatusCategory>,
|
||||
Canceled: '' as Ref<StatusCategory>
|
||||
},
|
||||
icon: {
|
||||
TrackerApplication: '' as Asset,
|
||||
|
@ -47,7 +47,7 @@
|
||||
return 0
|
||||
}
|
||||
const selectStates = await client.findAll(targetClass, { _id: { $in: Array.from(ids) } }, {})
|
||||
const unique = new Set(selectStates.map((s) => (s as State).title))
|
||||
const unique = new Set(selectStates.map((s) => (s as State).name))
|
||||
return unique.size
|
||||
}
|
||||
|
||||
|
@ -13,19 +13,17 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { Doc, FindResult, getObjectValue, RefTo, SortingOrder, Ref, Space } from '@hcengineering/core'
|
||||
import core, { Doc, FindResult, getObjectValue, Ref, RefTo, SortingOrder, Space, Status } from '@hcengineering/core'
|
||||
import { translate } from '@hcengineering/platform'
|
||||
import presentation, { getClient } from '@hcengineering/presentation'
|
||||
import type { State } from '@hcengineering/task'
|
||||
import task from '@hcengineering/task'
|
||||
import ui, {
|
||||
addNotification,
|
||||
Button,
|
||||
CheckBox,
|
||||
deviceOptionsStore,
|
||||
Label,
|
||||
Loading,
|
||||
resizeObserver,
|
||||
deviceOptionsStore,
|
||||
addNotification
|
||||
resizeObserver
|
||||
} from '@hcengineering/ui'
|
||||
import { Filter } from '@hcengineering/view'
|
||||
import { createEventDispatcher, onMount } from 'svelte'
|
||||
@ -48,27 +46,28 @@
|
||||
|
||||
let values: (Doc | undefined | null)[] = []
|
||||
let objectsPromise: Promise<FindResult<Doc>> | undefined
|
||||
$: targetClass = (hierarchy.getAttribute(filter.key._class, filter.key.key).type as RefTo<Doc>).to
|
||||
$: clazz = hierarchy.getClass(targetClass)
|
||||
const targets = new Map<any, number>()
|
||||
$: isState = clazz._id === task.class.State ?? false
|
||||
let statesCount: number[] = []
|
||||
let states: State[]
|
||||
$: targetClass = (filter.key.attribute.type as RefTo<Doc>).to
|
||||
$: clazz = hierarchy.getClass(targetClass)
|
||||
|
||||
const groupValues = (val: State[]): (Doc | undefined | null)[] => {
|
||||
states = val
|
||||
$: isStatus = client.getHierarchy().isDerived(targetClass, core.class.Status) ?? false
|
||||
let statusesCount: number[] = []
|
||||
let statuses: Status[]
|
||||
|
||||
const groupValues = (val: Status[]): (Doc | undefined | null)[] => {
|
||||
statuses = val
|
||||
const result: Doc[] = []
|
||||
statesCount = []
|
||||
const unique = [...new Set(val.map((v) => v.title))]
|
||||
statusesCount = []
|
||||
const unique = [...new Set(val.map((v) => v.name))]
|
||||
unique.forEach((label, i) => {
|
||||
let count = 0
|
||||
states.forEach((state) => {
|
||||
if (state.title === label) {
|
||||
statuses.forEach((state) => {
|
||||
if (state.name === label) {
|
||||
if (!count) result[i] = state
|
||||
count += targets.get(state._id) ?? 0
|
||||
}
|
||||
})
|
||||
statesCount[i] = count
|
||||
statusesCount[i] = count
|
||||
})
|
||||
return result
|
||||
}
|
||||
@ -106,14 +105,16 @@
|
||||
const oldSize = filter.value.length
|
||||
filter.value = filter.value.filter((p) => !notExisting.includes(p))
|
||||
onChange(filter)
|
||||
addNotification(await translate(view.string.FilterUpdated), filter.key.label, FilterRemovedNotification, {
|
||||
addNotification(await translate(view.string.FilterUpdated, {}), filter.key.label, FilterRemovedNotification, {
|
||||
description: await translate(view.string.FilterRemoved, { count: oldSize - (filter.value.length ?? 0) })
|
||||
})
|
||||
}
|
||||
if (targets.has(undefined)) {
|
||||
values.unshift(undefined)
|
||||
}
|
||||
if (isState) values = groupValues(values as State[])
|
||||
if (isStatus) {
|
||||
values = groupValues(values as Status[])
|
||||
}
|
||||
objectsPromise = undefined
|
||||
}
|
||||
|
||||
@ -123,20 +124,10 @@
|
||||
|
||||
function toggle (value: Doc | undefined | null): void {
|
||||
if (isSelected(value, filter.value)) {
|
||||
if (isState) {
|
||||
const ids = states.filter((state) => state.title === (value as State).title).map((s) => s._id)
|
||||
filter.value = filter.value.filter((p) => !ids.includes(p))
|
||||
} else filter.value = filter.value.filter((p) => (value ? p !== value._id : p != null))
|
||||
filter.value = filter.value.filter((p) => (value ? p !== value._id : p != null))
|
||||
} else {
|
||||
if (value) {
|
||||
if (isState) {
|
||||
filter.value = [
|
||||
...filter.value,
|
||||
...states
|
||||
.filter((state) => state.title === states.filter((s) => s._id === value._id)[0].title)
|
||||
.map((state) => state._id)
|
||||
]
|
||||
} else filter.value = [...filter.value, value._id]
|
||||
filter.value = [...filter.value, value._id]
|
||||
} else {
|
||||
filter.value = [...filter.value, undefined]
|
||||
}
|
||||
@ -155,7 +146,7 @@
|
||||
})
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
getValues(search)
|
||||
$: if (targetClass) getValues(search)
|
||||
</script>
|
||||
|
||||
<div class="selectPopup" use:resizeObserver={() => dispatch('changeContent')}>
|
||||
@ -197,7 +188,7 @@
|
||||
{/if}
|
||||
</div>
|
||||
<div class="dark-color ml-2">
|
||||
{#if isState}{statesCount[i]}{:else}{targets.get(value?._id)}{/if}
|
||||
{#if isStatus}{statusesCount[i]}{:else}{targets.get(value?._id)}{/if}
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
|
@ -131,14 +131,17 @@
|
||||
return props.length
|
||||
}
|
||||
|
||||
let dragItem: Doc | undefined = undefined
|
||||
let dragItem: {
|
||||
doc?: Doc
|
||||
revert?: () => void
|
||||
} = {}
|
||||
|
||||
let listDiv: HTMLDivElement
|
||||
</script>
|
||||
|
||||
<div class="list-container" bind:this={listDiv}>
|
||||
<ListCategories
|
||||
newObjectProps={space ? { space } : {}}
|
||||
newObjectProps={() => (space ? { space } : {})}
|
||||
{elementByIndex}
|
||||
{indexById}
|
||||
{docs}
|
||||
|
@ -13,13 +13,13 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { Class, Doc, generateId, Lookup, Ref, Space } from '@hcengineering/core'
|
||||
import { CategoryType, Class, Doc, generateId, Lookup, Ref, Space } from '@hcengineering/core'
|
||||
import { getResource, IntlString } from '@hcengineering/platform'
|
||||
import { getClient } from '@hcengineering/presentation'
|
||||
import { getClient, statusStore } from '@hcengineering/presentation'
|
||||
import { AnyComponent } from '@hcengineering/ui'
|
||||
import { AttributeModel, BuildModelKey, CategoryOption, ViewOptionModel, ViewOptions } from '@hcengineering/view'
|
||||
import { createEventDispatcher, onDestroy } from 'svelte'
|
||||
import { buildModel, getAdditionalHeader, getCategories, getPresenter, groupBy } from '../../utils'
|
||||
import { buildModel, getAdditionalHeader, getCategories, getGroupByValues, getPresenter, groupBy } from '../../utils'
|
||||
import { CategoryQuery, noCategory } from '../../viewOptions'
|
||||
import ListCategory from './ListCategory.svelte'
|
||||
|
||||
@ -41,17 +41,21 @@
|
||||
export let props: Record<string, any> = {}
|
||||
export let level: number
|
||||
export let initIndex = 0
|
||||
export let newObjectProps: Record<string, any>
|
||||
export let newObjectProps: (doc: Doc) => Record<string, any> | undefined
|
||||
export let docByIndex: Map<number, Doc>
|
||||
export let viewOptionsConfig: ViewOptionModel[] | undefined
|
||||
export let dragItem: Doc | undefined
|
||||
export let dragItem: {
|
||||
doc?: Doc
|
||||
revert?: () => void
|
||||
}
|
||||
export let listDiv: HTMLDivElement
|
||||
|
||||
$: groupByKey = viewOptions.groupBy[level] ?? noCategory
|
||||
$: groupedDocs = groupBy(docs, groupByKey)
|
||||
let categories: any[] = []
|
||||
let categories: CategoryType[] = []
|
||||
$: updateCategories(_class, docs, groupByKey, viewOptions, viewOptionsConfig)
|
||||
|
||||
$: groupByDocs = groupBy(docs, groupByKey, categories)
|
||||
|
||||
const client = getClient()
|
||||
const hierarchy = client.getHierarchy()
|
||||
|
||||
@ -71,20 +75,15 @@
|
||||
viewOptions: ViewOptions,
|
||||
viewOptionsModel: ViewOptionModel[] | undefined
|
||||
) {
|
||||
categories = await getCategories(client, _class, docs, groupByKey)
|
||||
categories = await getCategories(client, _class, docs, groupByKey, $statusStore)
|
||||
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, space, groupByKey, update, queryId))
|
||||
const res = hierarchy.clone(await f(_class, space, groupByKey, update, queryId, $statusStore))
|
||||
if (res !== undefined) {
|
||||
for (const category of categories) {
|
||||
if (!res.includes(category)) {
|
||||
res.push(category)
|
||||
}
|
||||
}
|
||||
categories = res
|
||||
return
|
||||
}
|
||||
@ -113,7 +112,7 @@
|
||||
let res = initIndex
|
||||
for (let index = 0; index < i; index++) {
|
||||
const cat = categories[index]
|
||||
res += groupedDocs[cat]?.length ?? 0
|
||||
res += groupByDocs[cat]?.length ?? 0
|
||||
}
|
||||
return res
|
||||
}
|
||||
@ -123,8 +122,8 @@
|
||||
const dispatch = createEventDispatcher()
|
||||
</script>
|
||||
|
||||
{#each categories as category, i (category)}
|
||||
{@const items = groupedDocs[category] ?? []}
|
||||
{#each categories as category, i (typeof category === 'object' ? category.name : category)}
|
||||
{@const items = groupByKey === noCategory || category === undefined ? docs : getGroupByValues(groupByDocs, category)}
|
||||
<ListCategory
|
||||
{elementByIndex}
|
||||
{indexById}
|
||||
|
@ -13,7 +13,7 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { Class, Doc, DocumentUpdate, Lookup, Ref, Space } from '@hcengineering/core'
|
||||
import { Class, Doc, DocumentUpdate, Lookup, PrimitiveType, Ref, Space, StatusValue } from '@hcengineering/core'
|
||||
import { IntlString } from '@hcengineering/platform'
|
||||
import { getClient } from '@hcengineering/presentation'
|
||||
import { calcRank, DocWithRank } from '@hcengineering/task'
|
||||
@ -33,7 +33,7 @@
|
||||
import ListHeader from './ListHeader.svelte'
|
||||
import ListItem from './ListItem.svelte'
|
||||
|
||||
export let category: any
|
||||
export let category: PrimitiveType | StatusValue
|
||||
export let headerComponent: AttributeModel | undefined
|
||||
export let singleCat: boolean
|
||||
export let groupByKey: string
|
||||
@ -57,10 +57,13 @@
|
||||
export let _class: Ref<Class<Doc>>
|
||||
export let config: (string | BuildModelKey)[]
|
||||
export let viewOptions: ViewOptions
|
||||
export let newObjectProps: Record<string, any>
|
||||
export let newObjectProps: (doc: Doc) => Record<string, any> | undefined
|
||||
export let docByIndex: Map<number, Doc>
|
||||
export let viewOptionsConfig: ViewOptionModel[] | undefined
|
||||
export let dragItem: Doc | undefined
|
||||
export let dragItem: {
|
||||
doc?: Doc
|
||||
revert?: () => void
|
||||
}
|
||||
export let listDiv: HTMLDivElement
|
||||
|
||||
$: lastLevel = level + 1 >= viewOptions.groupBy.length
|
||||
@ -108,7 +111,18 @@
|
||||
$: limited = limitGroup(items, limit)
|
||||
$: selectedObjectIdsSet = new Set<Ref<Doc>>(selectedObjectIds.map((it) => it._id))
|
||||
|
||||
$: newObjectProps = { [groupByKey]: category, ...newObjectProps }
|
||||
$: _newObjectProps = (doc: Doc) => {
|
||||
const groupValue =
|
||||
typeof category === 'object' ? category.values.find((it) => it.space === doc.space)?._id : category
|
||||
if (groupValue === undefined) {
|
||||
return undefined
|
||||
}
|
||||
return {
|
||||
...newObjectProps(doc),
|
||||
[groupByKey]: groupValue,
|
||||
space: doc.space
|
||||
}
|
||||
}
|
||||
|
||||
function isSelected (doc: Doc, focusStore: FocusSelection): boolean {
|
||||
return focusStore.focus?._id === doc._id
|
||||
@ -145,22 +159,25 @@
|
||||
|
||||
function dragEnterCat (ev: MouseEvent) {
|
||||
ev.preventDefault()
|
||||
if (dragItemIndex === undefined && dragItem !== undefined) {
|
||||
const index = items.findIndex((p) => p._id === dragItem?._id)
|
||||
if (dragItemIndex === undefined && dragItem.doc !== undefined) {
|
||||
const index = items.findIndex((p) => p._id === dragItem.doc?._id)
|
||||
if (index !== -1) {
|
||||
dragItemIndex = index
|
||||
return
|
||||
}
|
||||
if (isBorder(ev, 'top')) {
|
||||
items.unshift(dragItem)
|
||||
dragItemIndex = 0
|
||||
items = items
|
||||
dispatch('row-focus', dragItem)
|
||||
} else if (isBorder(ev, 'bottom')) {
|
||||
items.push(dragItem)
|
||||
dragItemIndex = items.length - 1
|
||||
items = items
|
||||
dispatch('row-focus', dragItem)
|
||||
const props = _newObjectProps(dragItem.doc)
|
||||
if (props !== undefined) {
|
||||
if (isBorder(ev, 'top')) {
|
||||
items.unshift(dragItem.doc)
|
||||
dragItemIndex = 0
|
||||
items = items
|
||||
dispatch('row-focus', dragItem)
|
||||
} else if (isBorder(ev, 'bottom')) {
|
||||
items.push(dragItem.doc)
|
||||
dragItemIndex = items.length - 1
|
||||
items = items
|
||||
dispatch('row-focus', dragItem)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -220,18 +237,24 @@
|
||||
}
|
||||
|
||||
async function drop (update: DocumentUpdate<Doc> = {}) {
|
||||
if (dragItem !== undefined) {
|
||||
for (const key in newObjectProps) {
|
||||
const value = newObjectProps[key]
|
||||
if ((dragItem as any)[key] !== value) {
|
||||
;(update as any)[key] = value
|
||||
if (dragItem.doc !== undefined) {
|
||||
const props = _newObjectProps(dragItem.doc)
|
||||
if (props !== undefined) {
|
||||
for (const key in props) {
|
||||
const value = props[key]
|
||||
if ((dragItem as any)[key] !== value) {
|
||||
;(update as any)[key] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
if (Object.keys(update).length > 0) {
|
||||
await client.update(dragItem, update)
|
||||
if (Object.keys(update).length > 0) {
|
||||
await client.update(dragItem.doc, update)
|
||||
}
|
||||
} else {
|
||||
dragItem.revert?.()
|
||||
}
|
||||
}
|
||||
dragItem = undefined
|
||||
dragItem.doc = undefined
|
||||
dragItem.revert = undefined
|
||||
dragItemIndex = undefined
|
||||
}
|
||||
|
||||
@ -240,11 +263,12 @@
|
||||
const rect = listDiv.getBoundingClientRect()
|
||||
const inRect = ev.clientY > rect.top && ev.clientY < rect.top + rect.height
|
||||
if (!inRect) {
|
||||
if (items.findIndex((p) => p._id === dragItem?._id) === -1 && dragItem !== undefined) {
|
||||
items = [...items.slice(0, initIndex), dragItem, ...items.slice(initIndex)]
|
||||
if (items.findIndex((p) => p._id === dragItem.doc?._id) === -1 && dragItem.doc !== undefined) {
|
||||
items = [...items.slice(0, initIndex), dragItem.doc, ...items.slice(initIndex)]
|
||||
}
|
||||
if (level === 0) {
|
||||
dragItem = undefined
|
||||
dragItem.doc = undefined
|
||||
dragItem.revert = undefined
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -261,7 +285,16 @@
|
||||
ev.dataTransfer.dropEffect = 'move'
|
||||
}
|
||||
ev.target?.addEventListener('dragend', (e) => dragEndListener(e, i))
|
||||
dragItem = docObject
|
||||
dragItem = {
|
||||
doc: docObject,
|
||||
revert: () => {
|
||||
const d = items.find((it) => it._id === docObject._id)
|
||||
if (d === undefined) {
|
||||
items.splice(i, 0, docObject)
|
||||
items = items
|
||||
}
|
||||
}
|
||||
}
|
||||
dragItemIndex = i
|
||||
dispatch('dragstart', {
|
||||
target: ev.target,
|
||||
@ -289,7 +322,7 @@
|
||||
{createItemDialog}
|
||||
{createItemLabel}
|
||||
{extraHeaders}
|
||||
{newObjectProps}
|
||||
newObjectProps={_newObjectProps}
|
||||
flat={flatHeaders}
|
||||
{props}
|
||||
on:more={() => {
|
||||
@ -317,7 +350,7 @@
|
||||
{createItemDialog}
|
||||
{createItemLabel}
|
||||
{viewOptions}
|
||||
{newObjectProps}
|
||||
newObjectProps={_newObjectProps}
|
||||
{flatHeaders}
|
||||
{props}
|
||||
level={level + 1}
|
||||
|
@ -44,13 +44,13 @@
|
||||
export let flat = false
|
||||
export let props: Record<string, any> = {}
|
||||
export let level: number
|
||||
export let newObjectProps: Record<string, any>
|
||||
export let newObjectProps: (doc: Doc) => Record<string, any> | undefined
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
const handleCreateItem = (event: MouseEvent) => {
|
||||
if (createItemDialog === undefined) return
|
||||
showPopup(createItemDialog, newObjectProps, eventToHTMLElement(event))
|
||||
showPopup(createItemDialog, newObjectProps(items[0]), eventToHTMLElement(event))
|
||||
}
|
||||
</script>
|
||||
|
||||
|
@ -0,0 +1,37 @@
|
||||
<!--
|
||||
// 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 { Status, StatusValue, WithLookup } from '@hcengineering/core'
|
||||
import { Asset } from '@hcengineering/platform'
|
||||
import { AnySvelteComponent, Icon } from '@hcengineering/ui'
|
||||
|
||||
export let icon: Asset | AnySvelteComponent | undefined = undefined
|
||||
|
||||
export let value: Status | WithLookup<Status> | StatusValue | undefined
|
||||
export let size: 'small' | 'medium' = 'small'
|
||||
</script>
|
||||
|
||||
{#if value}
|
||||
<div class="flex-presenter">
|
||||
{#if icon && typeof icon === 'string'}
|
||||
<Icon {icon} {size} />
|
||||
{:else if icon !== undefined && typeof icon !== 'string'}
|
||||
<svelte:component this={icon} {value} {size} />
|
||||
{/if}
|
||||
<span class="overflow-label" class:ml-2={icon !== undefined}>
|
||||
{value.name}
|
||||
</span>
|
||||
</div>
|
||||
{/if}
|
@ -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 { 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'
|
||||
|
||||
export let value: Ref<Status> | StatusValue | undefined
|
||||
export let size: 'small' | 'medium' = 'medium'
|
||||
export let icon: Asset | AnySvelteComponent | undefined = undefined
|
||||
</script>
|
||||
|
||||
{#if value}
|
||||
<StatusPresenter value={$statusStore.get(typeof value === 'string' ? value : value.values[0]._id)} {size} {icon} />
|
||||
{/if}
|
@ -27,9 +27,12 @@ import ColorsPopup from './components/ColorsPopup.svelte'
|
||||
import DateEditor from './components/DateEditor.svelte'
|
||||
import DatePresenter from './components/DatePresenter.svelte'
|
||||
import DocAttributeBar from './components/DocAttributeBar.svelte'
|
||||
import DocNavLink from './components/DocNavLink.svelte'
|
||||
import EditBoxPopup from './components/EditBoxPopup.svelte'
|
||||
import EditDoc from './components/EditDoc.svelte'
|
||||
import EnumArrayEditor from './components/EnumArrayEditor.svelte'
|
||||
import EnumEditor from './components/EnumEditor.svelte'
|
||||
import EnumPresenter from './components/EnumPresenter.svelte'
|
||||
import FilterBar from './components/filter/FilterBar.svelte'
|
||||
import FilterTypePopup from './components/filter/FilterTypePopup.svelte'
|
||||
import ObjectFilter from './components/filter/ObjectFilter.svelte'
|
||||
@ -49,11 +52,14 @@ import MarkupEditor from './components/MarkupEditor.svelte'
|
||||
import MarkupEditorPopup from './components/MarkupEditorPopup.svelte'
|
||||
import MarkupPresenter from './components/MarkupPresenter.svelte'
|
||||
import Menu from './components/Menu.svelte'
|
||||
import TreeItem from './components/navigator/TreeItem.svelte'
|
||||
import TreeNode from './components/navigator/TreeNode.svelte'
|
||||
import NumberEditor from './components/NumberEditor.svelte'
|
||||
import NumberPresenter from './components/NumberPresenter.svelte'
|
||||
import ObjectPresenter from './components/ObjectPresenter.svelte'
|
||||
import RolePresenter from './components/RolePresenter.svelte'
|
||||
import SpacePresenter from './components/SpacePresenter.svelte'
|
||||
import SpaceRefPresenter from './components/SpaceRefPresenter.svelte'
|
||||
import StringEditor from './components/StringEditor.svelte'
|
||||
import StringPresenter from './components/StringPresenter.svelte'
|
||||
import Table from './components/Table.svelte'
|
||||
@ -62,12 +68,8 @@ import TimestampPresenter from './components/TimestampPresenter.svelte'
|
||||
import UpDownNavigator from './components/UpDownNavigator.svelte'
|
||||
import ValueSelector from './components/ValueSelector.svelte'
|
||||
import ViewletSettingButton from './components/ViewletSettingButton.svelte'
|
||||
import SpaceRefPresenter from './components/SpaceRefPresenter.svelte'
|
||||
import EnumArrayEditor from './components/EnumArrayEditor.svelte'
|
||||
import EnumPresenter from './components/EnumPresenter.svelte'
|
||||
import TreeNode from './components/navigator/TreeNode.svelte'
|
||||
import TreeItem from './components/navigator/TreeItem.svelte'
|
||||
import DocNavLink from './components/DocNavLink.svelte'
|
||||
import StatusPresenter from './components/status/StatusPresenter.svelte'
|
||||
import StatusRefPresenter from './components/status/StatusRefPresenter.svelte'
|
||||
|
||||
import {
|
||||
afterResult,
|
||||
@ -82,11 +84,7 @@ import {
|
||||
|
||||
import { IndexedDocumentPreview } from '@hcengineering/presentation'
|
||||
import { showEmptyGroups } from './viewOptions'
|
||||
|
||||
function PositionElementAlignment (e?: Event): PopupAlignment | undefined {
|
||||
return getEventPopupPositionElement(e)
|
||||
}
|
||||
|
||||
import { statusSort } from './utils'
|
||||
export { getActions, invokeAction } from './actions'
|
||||
export { default as ActionContext } from './components/ActionContext.svelte'
|
||||
export { default as ActionHandler } from './components/ActionHandler.svelte'
|
||||
@ -95,30 +93,33 @@ export { default as FixedColumn } from './components/FixedColumn.svelte'
|
||||
export { default as SourcePresenter } from './components/inference/SourcePresenter.svelte'
|
||||
export { default as LinkPresenter } from './components/LinkPresenter.svelte'
|
||||
export { default as List } from './components/list/List.svelte'
|
||||
export { default as MarkupPresenter } from './components/MarkupPresenter.svelte'
|
||||
export { default as MarkupPreviewPopup } from './components/MarkupPreviewPopup.svelte'
|
||||
export { default as ContextMenu } from './components/Menu.svelte'
|
||||
export { default as ObjectBox } from './components/ObjectBox.svelte'
|
||||
export { default as ObjectSearchBox } from './components/ObjectSearchBox.svelte'
|
||||
export { default as ObjectPresenter } from './components/ObjectPresenter.svelte'
|
||||
export { default as ObjectSearchBox } from './components/ObjectSearchBox.svelte'
|
||||
export { default as StatusPresenter } from './components/status/StatusPresenter.svelte'
|
||||
export { default as StatusRefPresenter } from './components/status/StatusRefPresenter.svelte'
|
||||
export { default as TableBrowser } from './components/TableBrowser.svelte'
|
||||
export { default as ValueSelector } from './components/ValueSelector.svelte'
|
||||
export { default as MarkupPreviewPopup } from './components/MarkupPreviewPopup.svelte'
|
||||
export { default as MarkupPresenter } from './components/MarkupPresenter.svelte'
|
||||
export * from './context'
|
||||
export * from './filter'
|
||||
export * from './selection'
|
||||
export * from './utils'
|
||||
export {
|
||||
buildModel,
|
||||
getActiveViewletId,
|
||||
getAdditionalHeader,
|
||||
getCategories,
|
||||
getCollectionCounter,
|
||||
getFiltredKeys,
|
||||
getObjectPresenter,
|
||||
getObjectPreview,
|
||||
groupBy,
|
||||
isCollectionAttr,
|
||||
LoadingProps,
|
||||
setActiveViewletId,
|
||||
getAdditionalHeader,
|
||||
groupBy,
|
||||
getCategories
|
||||
setActiveViewletId
|
||||
} from './utils'
|
||||
export * from './viewOptions'
|
||||
export {
|
||||
@ -152,6 +153,10 @@ export {
|
||||
EditBoxPopup
|
||||
}
|
||||
|
||||
function PositionElementAlignment (e?: Event): PopupAlignment | undefined {
|
||||
return getEventPopupPositionElement(e)
|
||||
}
|
||||
|
||||
export default async (): Promise<Resources> => ({
|
||||
actionImpl,
|
||||
component: {
|
||||
@ -194,7 +199,9 @@ export default async (): Promise<Resources> => ({
|
||||
IndexedDocumentPreview,
|
||||
SpaceRefPresenter,
|
||||
EnumArrayEditor,
|
||||
EnumPresenter
|
||||
EnumPresenter,
|
||||
StatusPresenter,
|
||||
StatusRefPresenter
|
||||
},
|
||||
popup: {
|
||||
PositionElementAlignment
|
||||
@ -208,6 +215,7 @@ export default async (): Promise<Resources> => ({
|
||||
FilterAfterResult: afterResult,
|
||||
FilterNestedMatchResult: nestedMatchResult,
|
||||
FilterNestedDontMatchResult: nestedDontMatchResult,
|
||||
ShowEmptyGroups: showEmptyGroups
|
||||
ShowEmptyGroups: showEmptyGroups,
|
||||
StatusSort: statusSort
|
||||
}
|
||||
})
|
||||
|
@ -16,7 +16,7 @@
|
||||
|
||||
import { IntlString, mergeIds } from '@hcengineering/platform'
|
||||
import { AnyComponent } from '@hcengineering/ui'
|
||||
import view, { viewId } from '@hcengineering/view'
|
||||
import view, { SortFunc, viewId } from '@hcengineering/view'
|
||||
|
||||
export default mergeIds(viewId, view, {
|
||||
component: {
|
||||
@ -64,5 +64,8 @@ export default mergeIds(viewId, view, {
|
||||
Shown: '' as IntlString,
|
||||
ShowEmptyGroups: '' as IntlString,
|
||||
Total: '' as IntlString
|
||||
},
|
||||
function: {
|
||||
StatusSort: '' as SortFunc
|
||||
}
|
||||
})
|
||||
|
@ -16,11 +16,13 @@
|
||||
|
||||
import core, {
|
||||
AttachedDoc,
|
||||
CategoryType,
|
||||
Class,
|
||||
Client,
|
||||
Collection,
|
||||
Doc,
|
||||
DocumentUpdate,
|
||||
getObjectValue,
|
||||
Hierarchy,
|
||||
Lookup,
|
||||
Obj,
|
||||
@ -29,11 +31,14 @@ import core, {
|
||||
ReverseLookup,
|
||||
ReverseLookups,
|
||||
Space,
|
||||
Status,
|
||||
StatusManager,
|
||||
StatusValue,
|
||||
TxOperations
|
||||
} from '@hcengineering/core'
|
||||
import type { IntlString } from '@hcengineering/platform'
|
||||
import { getResource } from '@hcengineering/platform'
|
||||
import { AttributeCategory, getAttributePresenterClass, KeyedAttribute } from '@hcengineering/presentation'
|
||||
import { AttributeCategory, createQuery, getAttributePresenterClass, KeyedAttribute } from '@hcengineering/presentation'
|
||||
import {
|
||||
AnyComponent,
|
||||
ErrorPresenter,
|
||||
@ -499,46 +504,180 @@ export type FixedWidthStore = Record<string, number>
|
||||
|
||||
export const fixedWidthStore = writable<FixedWidthStore>({})
|
||||
|
||||
export function groupBy<T extends Doc> (docs: T[], key: string): { [key: string]: T[] } {
|
||||
export function groupBy<T extends Doc> (
|
||||
docs: T[],
|
||||
key: string,
|
||||
categories?: CategoryType[]
|
||||
): { [key: string | number]: T[] } {
|
||||
return docs.reduce((storage: { [key: string]: T[] }, item: T) => {
|
||||
const group = (item as any)[key] ?? undefined
|
||||
let group = getObjectValue(key, item) ?? undefined
|
||||
|
||||
if (categories !== undefined) {
|
||||
for (const c of categories) {
|
||||
if (typeof c === 'object') {
|
||||
const st = c.values.find((it) => it._id === group)
|
||||
if (st !== undefined) {
|
||||
group = st.name
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
storage[group] = storage[group] ?? []
|
||||
|
||||
storage[group].push(item)
|
||||
|
||||
return storage
|
||||
}, {})
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export function getGroupByValues<T extends Doc> (
|
||||
groupByDocs: Record<string | number, T[]>,
|
||||
category: CategoryType
|
||||
): T[] {
|
||||
if (typeof category === 'object') {
|
||||
return groupByDocs[category.name] ?? []
|
||||
} else if (category !== undefined) {
|
||||
return groupByDocs[category] ?? []
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export function setGroupByValues (
|
||||
groupByDocs: Record<string | number, Doc[]>,
|
||||
category: CategoryType,
|
||||
docs: Doc[]
|
||||
): void {
|
||||
if (typeof category === 'object') {
|
||||
groupByDocs[category.name] = docs
|
||||
} else if (category !== undefined) {
|
||||
groupByDocs[category] = docs
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Group category references into categories.
|
||||
* @public
|
||||
*/
|
||||
export async function groupByCategory (
|
||||
client: TxOperations,
|
||||
_class: Ref<Class<Doc>>,
|
||||
key: string,
|
||||
categories: any[],
|
||||
mgr: StatusManager,
|
||||
viewletDescriptorId?: Ref<ViewletDescriptor>
|
||||
): Promise<CategoryType[]> {
|
||||
const h = client.getHierarchy()
|
||||
const attr = h.getAttribute(_class, key)
|
||||
if (attr === undefined) return categories
|
||||
if (key === noCategory) return [undefined]
|
||||
|
||||
const attrClass = getAttributePresenterClass(h, attr).attrClass
|
||||
|
||||
const isStatusField = h.isDerived(attrClass, core.class.Status)
|
||||
|
||||
let existingCategories: any[] = []
|
||||
|
||||
if (isStatusField) {
|
||||
existingCategories = await groupByStatusCategories(h, attrClass, categories, mgr, viewletDescriptorId)
|
||||
} else {
|
||||
const valueSet = new Set<any>()
|
||||
for (const v of categories) {
|
||||
if (!valueSet.has(v)) {
|
||||
valueSet.add(v)
|
||||
existingCategories.push(v)
|
||||
}
|
||||
}
|
||||
}
|
||||
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>()
|
||||
|
||||
for (const v of categories) {
|
||||
const status = mgr.byId.get(v)
|
||||
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) || it.space === status.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<any[]> {
|
||||
): Promise<CategoryType[]> {
|
||||
if (key === noCategory) return [undefined]
|
||||
const existingCategories = Array.from(new Set(docs.map((x: any) => x[key] ?? undefined)))
|
||||
|
||||
return await sortCategories(client, _class, existingCategories, key, viewletDescriptorId)
|
||||
return await groupByCategory(
|
||||
client,
|
||||
_class,
|
||||
key,
|
||||
docs.map((it) => getObjectValue(key, it) ?? undefined),
|
||||
mgr,
|
||||
viewletDescriptorId
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export async function sortCategories (
|
||||
client: TxOperations,
|
||||
_class: Ref<Class<Doc>>,
|
||||
hierarchy: Hierarchy,
|
||||
attrClass: Ref<Class<Doc>>,
|
||||
existingCategories: any[],
|
||||
key: string,
|
||||
viewletDescriptorId?: Ref<ViewletDescriptor>
|
||||
): Promise<any[]> {
|
||||
if (key === noCategory) return [undefined]
|
||||
const hierarchy = client.getHierarchy()
|
||||
const attr = hierarchy.getAttribute(_class, key)
|
||||
if (attr === undefined) return existingCategories
|
||||
const attrClass = getAttributePresenterClass(hierarchy, attr).attrClass
|
||||
const clazz = hierarchy.getClass(attrClass)
|
||||
const sortFunc = hierarchy.as(clazz, view.mixin.SortFuncs)
|
||||
if (sortFunc?.func === undefined) return existingCategories
|
||||
if (sortFunc?.func === undefined) {
|
||||
const isStatusField = hierarchy.isDerived(attrClass, core.class.Status)
|
||||
if (isStatusField) {
|
||||
existingCategories.sort((a, b) => {
|
||||
return a.values[0].rank.localeCompare(b.values[0].rank)
|
||||
})
|
||||
} else {
|
||||
existingCategories.sort((a, b) => {
|
||||
return JSON.stringify(a).localeCompare(JSON.stringify(b))
|
||||
})
|
||||
}
|
||||
|
||||
return existingCategories
|
||||
}
|
||||
const f = await getResource(sortFunc.func)
|
||||
|
||||
return await f(existingCategories, viewletDescriptorId)
|
||||
@ -666,3 +805,34 @@ export async function getObjectLinkFragment (
|
||||
loc.fragment = getPanelURI(component, object._id, Hierarchy.mixinOrClass(object), 'content')
|
||||
return loc
|
||||
}
|
||||
|
||||
export async function statusSort (
|
||||
value: Array<Ref<Status>>,
|
||||
viewletDescriptorId?: Ref<ViewletDescriptor>
|
||||
): Promise<Array<Ref<Status>>> {
|
||||
return await new Promise((resolve) => {
|
||||
// TODO: How we track category updates.
|
||||
const query = createQuery(true)
|
||||
query.query(
|
||||
core.class.Status,
|
||||
{ _id: { $in: value } },
|
||||
(res) => {
|
||||
res.sort((a, b) => {
|
||||
const res = (a.$lookup?.category?.order ?? 0) - (b.$lookup?.category?.order ?? 0)
|
||||
if (res === 0) {
|
||||
return a.rank.localeCompare(b.rank)
|
||||
}
|
||||
return res
|
||||
})
|
||||
|
||||
resolve(res.map((p) => p._id))
|
||||
query.unsubscribe()
|
||||
},
|
||||
{
|
||||
sort: {
|
||||
rank: 1
|
||||
}
|
||||
}
|
||||
)
|
||||
})
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Class, Doc, Ref, SortingOrder, Space } from '@hcengineering/core'
|
||||
import core, { Class, Doc, Ref, SortingOrder, Space, StatusManager } from '@hcengineering/core'
|
||||
import { getResource } from '@hcengineering/platform'
|
||||
import { createQuery, getAttributePresenterClass, getClient, LiveQuery } from '@hcengineering/presentation'
|
||||
import { getCurrentLocation, locationToUrl } from '@hcengineering/ui'
|
||||
@ -6,12 +6,14 @@ import {
|
||||
DropdownViewOption,
|
||||
ToggleViewOption,
|
||||
Viewlet,
|
||||
ViewletDescriptor,
|
||||
ViewOptionModel,
|
||||
ViewOptions,
|
||||
ViewOptionsModel
|
||||
} from '@hcengineering/view'
|
||||
import { get, writable } from 'svelte/store'
|
||||
import view from './plugin'
|
||||
import { groupByCategory } from './utils'
|
||||
|
||||
export const noCategory = '#no_category'
|
||||
|
||||
@ -105,7 +107,9 @@ export async function showEmptyGroups (
|
||||
space: Ref<Space> | undefined,
|
||||
key: string,
|
||||
onUpdate: () => void,
|
||||
queryId: Ref<Doc>
|
||||
queryId: Ref<Doc>,
|
||||
mgr: StatusManager,
|
||||
viewletDescriptorId?: Ref<ViewletDescriptor>
|
||||
): Promise<any[] | undefined> {
|
||||
const client = getClient()
|
||||
const hierarchy = client.getHierarchy()
|
||||
@ -114,16 +118,19 @@ export async function showEmptyGroups (
|
||||
if (attr === undefined) return
|
||||
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.
|
||||
const statuses = mgr.statuses.filter((it) => it.ofAttribute === attr._id).map((it) => it._id)
|
||||
return await groupByCategory(client, _class, key, statuses, mgr)
|
||||
}
|
||||
|
||||
const mixin = hierarchy.as(attributeClass, view.mixin.AllValuesFunc)
|
||||
if (mixin.func !== undefined) {
|
||||
const f = await getResource(mixin.func)
|
||||
const res = await f(space, onUpdate, queryId)
|
||||
if (res !== undefined) {
|
||||
const sortFunc = hierarchy.as(attributeClass, view.mixin.SortFuncs)
|
||||
if (sortFunc?.func === undefined) return res
|
||||
const f = await getResource(sortFunc.func)
|
||||
|
||||
return await f(res)
|
||||
return await groupByCategory(client, _class, key, res, mgr, viewletDescriptorId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -16,6 +16,7 @@
|
||||
|
||||
import type {
|
||||
AnyAttribute,
|
||||
CategoryType,
|
||||
Class,
|
||||
Client,
|
||||
Doc,
|
||||
@ -25,9 +26,12 @@ import type {
|
||||
Mixin,
|
||||
Obj,
|
||||
ObjQueryType,
|
||||
PrimitiveType,
|
||||
Ref,
|
||||
SortingOrder,
|
||||
Space,
|
||||
StatusManager,
|
||||
StatusValue,
|
||||
Type,
|
||||
UXObject
|
||||
} from '@hcengineering/core'
|
||||
@ -36,10 +40,10 @@ import type { Preference } from '@hcengineering/preference'
|
||||
import type {
|
||||
AnyComponent,
|
||||
AnySvelteComponent,
|
||||
PopupAlignment,
|
||||
PopupPosAlignment,
|
||||
Location,
|
||||
Location as PlatformLocation,
|
||||
Location
|
||||
PopupAlignment,
|
||||
PopupPosAlignment
|
||||
} from '@hcengineering/ui'
|
||||
|
||||
/**
|
||||
@ -236,7 +240,9 @@ export interface ListHeaderExtra extends Class<Doc> {
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export type SortFunc = Resource<(values: any[], viewletDescriptorId?: Ref<ViewletDescriptor>) => Promise<any[]>>
|
||||
export type SortFunc = Resource<
|
||||
(values: (PrimitiveType | StatusValue)[], viewletDescriptorId?: Ref<ViewletDescriptor>) => Promise<any[]>
|
||||
>
|
||||
|
||||
/**
|
||||
* @public
|
||||
@ -245,11 +251,20 @@ export interface ClassSortFuncs extends Class<Doc> {
|
||||
func: SortFunc
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export type AllValuesFuncGetter = (
|
||||
space: Ref<Space> | undefined,
|
||||
onUpdate: () => void,
|
||||
queryId: Ref<Doc>
|
||||
) => Promise<any[] | undefined>
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export interface AllValuesFunc extends Class<Doc> {
|
||||
func: Resource<(space: Ref<Space> | undefined, onUpdate: () => void, queryId: Ref<Doc>) => Promise<any[] | undefined>>
|
||||
func: Resource<AllValuesFuncGetter>
|
||||
}
|
||||
|
||||
/**
|
||||
@ -486,19 +501,22 @@ export interface ViewOption {
|
||||
actionTarget?: 'query' | 'category'
|
||||
action?: Resource<(value: any, ...params: any) => any>
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export type ViewCategoryAction = Resource<
|
||||
(
|
||||
export type ViewCategoryActionFunc = (
|
||||
_class: Ref<Class<Doc>>,
|
||||
space: Ref<Space> | undefined,
|
||||
key: string,
|
||||
onUpdate: () => void,
|
||||
queryId: Ref<Doc>
|
||||
) => Promise<any[] | undefined>
|
||||
>
|
||||
queryId: Ref<Doc>,
|
||||
mgr: StatusManager,
|
||||
viewletDescriptorId?: Ref<ViewletDescriptor>
|
||||
) => Promise<CategoryType[] | undefined>
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export type ViewCategoryAction = Resource<ViewCategoryActionFunc>
|
||||
|
||||
/**
|
||||
* @public
|
||||
|
@ -54,7 +54,7 @@
|
||||
if (attachTo) {
|
||||
viewlets = await client.findAll(
|
||||
view.class.Viewlet,
|
||||
{ attachTo },
|
||||
{ attachTo, variant: { $exists: false } },
|
||||
{
|
||||
lookup: {
|
||||
descriptor: view.class.ViewletDescriptor
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user