Signed-off-by: Denis Bykhov <bykhov.denis@gmail.com>
This commit is contained in:
Denis Bykhov
2023-09-04 23:06:34 +06:00
committed by GitHub
parent a2388cd1d4
commit 2422baa108
97 changed files with 1678 additions and 1268 deletions
+10 -31
View File
@@ -1,12 +1,12 @@
import { MeasureContext, Ref, TxOperations } from '@hcengineering/core'
import task, { DoneState, genRanks, Kanban, SpaceWithStates, State } from '@hcengineering/task'
import task, { DoneState, SpaceWithStates, State } from '@hcengineering/task'
import { findOrUpdate } from './utils'
export async function createUpdateSpaceKanban (
ctx: MeasureContext,
spaceId: Ref<SpaceWithStates>,
client: TxOperations
): Promise<Ref<State>[]> {
): Promise<[Ref<State>[], Ref<DoneState>[]]> {
const rawStates = [
{ color: 9, name: 'Initial' },
{ color: 10, name: 'Intermidiate' },
@@ -15,55 +15,34 @@ export async function createUpdateSpaceKanban (
{ color: 11, name: 'Invalid' }
]
const states: Array<Ref<State>> = []
const stateRanks = genRanks(rawStates.length)
for (const st of rawStates) {
const rank = stateRanks.next().value
if (rank === undefined) {
console.error('Failed to generate rank')
break
}
const sid = ('generated-' + spaceId + '.state.' + st.name.toLowerCase().replace(' ', '_')) as Ref<State>
await ctx.with('find-or-update', {}, (ctx) =>
findOrUpdate(ctx, client, spaceId, task.class.State, sid, {
findOrUpdate(ctx, client, task.space.Statuses, task.class.State, sid, {
ofAttribute: task.attribute.State,
name: st.name,
color: st.color,
rank
color: st.color
})
)
states.push(sid)
}
const done: Array<Ref<DoneState>> = []
const doneStates = [
{ class: task.class.WonState, name: 'Won' },
{ class: task.class.LostState, name: 'Lost' }
]
const doneStateRanks = genRanks(doneStates.length)
for (const st of doneStates) {
const rank = doneStateRanks.next().value
if (rank === undefined) {
console.error('Failed to generate rank')
break
}
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, {
findOrUpdate(ctx, client, task.space.Statuses, st.class, sid, {
ofAttribute: task.attribute.DoneState,
name: st.name,
rank
name: st.name
})
)
done.push(sid)
}
await ctx.with('create-kanban', {}, (ctx) =>
findOrUpdate(ctx, client, spaceId, task.class.Kanban, ('generated-' + spaceId + '.kanban') as Ref<Kanban>, {
attachedTo: spaceId
})
)
return states
return [states, done]
}
+8 -6
View File
@@ -1,4 +1,4 @@
import contact, { Channel, PersonAccount, Person, Employee } from '@hcengineering/contact'
import contact, { Channel, Employee, Person, PersonAccount } from '@hcengineering/contact'
import core, {
AttachedData,
Data,
@@ -87,6 +87,10 @@ async function genVacansyApplicants (
candidates: Ref<Candidate>[],
emoloyeeIds: Ref<Employee>[]
): Promise<void> {
const [states, doneStates] = await ctx.with('create-kanbad', {}, (ctx) =>
createUpdateSpaceKanban(ctx, vacancyId, client)
)
const vacancy: Data<Vacancy> = {
name: faker.company.companyName(),
description: faker.lorem.sentences(2),
@@ -95,7 +99,9 @@ async function genVacansyApplicants (
members: accountIds,
number: faker.datatype.number(),
private: false,
archived: false
archived: false,
states,
doneStates
}
const vacancyId = (options.random ? `vacancy-${generateId()}-${i}` : `vacancy-genid-${i}`) as Ref<Vacancy>
@@ -125,10 +131,6 @@ async function genVacansyApplicants (
console.log('Vacancy attachments generated', vacancy.name)
const states = await ctx.with('create-kanbad', {}, (ctx) => createUpdateSpaceKanban(ctx, vacancyId, client))
console.log('States generated', vacancy.name)
const applicantsForCount = options.applicants.min + faker.datatype.number(options.applicants.max)
const applicantsFor = faker.random.arrayElements(candidates, applicantsForCount)
+1 -2
View File
@@ -265,8 +265,7 @@ export function createModel (builder: Builder): void {
core.space.Model,
{
ofAttribute: task.attribute.DoneState,
name: board.string.Completed,
rank: '0'
name: board.string.Completed
},
board.state.Completed
)
+24 -30
View File
@@ -14,31 +14,14 @@
//
import { Ref, TxOperations } from '@hcengineering/core'
import { createOrUpdate, MigrateOperation, MigrationClient, MigrationUpgradeClient } from '@hcengineering/model'
import { MigrateOperation, MigrationClient, MigrationUpgradeClient, createOrUpdate } from '@hcengineering/model'
import core from '@hcengineering/model-core'
import { createKanbanTemplate, createSequence } from '@hcengineering/model-task'
import tags from '@hcengineering/tags'
import task, { createKanban, KanbanTemplate } from '@hcengineering/task'
import task, { KanbanTemplate, createStates } from '@hcengineering/task'
import board from './plugin'
async function createSpace (tx: TxOperations): Promise<void> {
const current = await tx.findOne(core.class.Space, {
_id: board.space.DefaultBoard
})
if (current === undefined) {
await tx.createDoc(
board.class.Board,
core.space.Space,
{
name: 'Default',
description: 'Default board',
private: false,
archived: false,
members: []
},
board.space.DefaultBoard
)
}
const currentTemplate = await tx.findOne(core.class.Space, {
_id: board.space.BoardTemplates
})
@@ -58,6 +41,28 @@ async function createSpace (tx: TxOperations): Promise<void> {
board.space.BoardTemplates
)
}
const current = await tx.findOne(core.class.Space, {
_id: board.space.DefaultBoard
})
if (current === undefined) {
const defaultTmpl = await createDefaultKanbanTemplate(tx)
const [states, doneStates] = await createStates(tx, defaultTmpl)
await tx.createDoc(
board.class.Board,
core.space.Space,
{
name: 'Default',
description: 'Default board',
private: false,
archived: false,
members: [],
states,
doneStates
},
board.space.DefaultBoard
)
}
}
async function createDefaultKanbanTemplate (tx: TxOperations): Promise<Ref<KanbanTemplate>> {
@@ -80,20 +85,9 @@ async function createDefaultKanbanTemplate (tx: TxOperations): Promise<Ref<Kanba
doneStates: defaultKanban.doneStates
})
}
async function createDefaultKanban (tx: TxOperations): Promise<void> {
const current = await tx.findOne(task.class.Kanban, {
attachedTo: board.space.DefaultBoard
})
if (current !== undefined) return
const defaultTmpl = await createDefaultKanbanTemplate(tx)
await createKanban(tx, board.space.DefaultBoard, defaultTmpl)
}
async function createDefaults (tx: TxOperations): Promise<void> {
await createSpace(tx)
await createSequence(tx, board.class.Card)
await createDefaultKanban(tx)
await createOrUpdate(
tx,
tags.class.TagCategory,
+3 -3
View File
@@ -14,9 +14,9 @@
// limitations under the License.
//
import { boardId } from '@hcengineering/board'
import { Board, boardId } from '@hcengineering/board'
import board from '@hcengineering/board-resources/src/plugin'
import type { Ref, Space } from '@hcengineering/core'
import type { Ref } from '@hcengineering/core'
import { IntlString, mergeIds } from '@hcengineering/platform'
import { KanbanTemplate, Sequence } from '@hcengineering/task'
import type { AnyComponent } from '@hcengineering/ui'
@@ -42,7 +42,7 @@ export default mergeIds(boardId, board, {
CardCoverEditor: '' as AnyComponent
},
space: {
DefaultBoard: '' as Ref<Space>
DefaultBoard: '' as Ref<Board>
},
template: {
DefaultBoard: '' as Ref<KanbanTemplate>
+3 -7
View File
@@ -13,7 +13,7 @@
// limitations under the License.
//
import { Attribute, DOMAIN_STATUS, DOMAIN_MODEL, Ref, Status, StatusCategory } from '@hcengineering/core'
import { Attribute, DOMAIN_MODEL, DOMAIN_STATUS, 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'
@@ -22,13 +22,13 @@ import { TDoc } from './core'
// S T A T U S
@Model(core.class.Status, core.class.Doc, DOMAIN_STATUS)
@UX(core.string.Status)
@UX(core.string.Status, undefined, undefined, undefined, 'name')
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>
category?: Ref<StatusCategory>
@Prop(TypeString(), core.string.Name)
name!: string
@@ -38,9 +38,6 @@ export class TStatus extends TDoc implements Status {
@Prop(TypeString(), core.string.Description)
description!: string
// @Prop(TypeString(), core.string.Rank)
rank!: string
}
@Model(core.class.StatusCategory, core.class.Doc, DOMAIN_MODEL)
@@ -48,7 +45,6 @@ export class TStatus extends TDoc implements Status {
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
+7 -5
View File
@@ -35,16 +35,16 @@ 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 { generateClassNotificationTypes } from '@hcengineering/model-notification'
import task, { TSpaceWithStates, TTask, actionTemplates } from '@hcengineering/model-task'
import tracker from '@hcengineering/model-tracker'
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 { generateClassNotificationTypes } from '@hcengineering/model-notification'
import notification from '@hcengineering/notification'
import lead from './plugin'
import tracker from '@hcengineering/model-tracker'
import setting from '@hcengineering/setting'
import { State } from '@hcengineering/task'
import { ViewOptionsModel } from '@hcengineering/view'
import lead from './plugin'
export { leadId } from '@hcengineering/lead'
export { leadOperation } from './migration'
@@ -83,6 +83,8 @@ export class TLead extends TTask implements Lead {
@Prop(TypeRef(task.class.State), task.string.TaskState, { _id: task.attribute.State })
declare status: Ref<State>
declare space: Ref<Funnel>
}
@Mixin(lead.mixin.Customer, contact.class.Contact)
+24 -30
View File
@@ -17,29 +17,11 @@ import { Ref, TxOperations } from '@hcengineering/core'
import { MigrateOperation, MigrationClient, MigrationUpgradeClient } from '@hcengineering/model'
import core from '@hcengineering/model-core'
import { createKanbanTemplate, createSequence } from '@hcengineering/model-task'
import task, { createKanban, KanbanTemplate } from '@hcengineering/task'
import lead from './plugin'
import task, { KanbanTemplate, createStates } from '@hcengineering/task'
import { PaletteColorIndexes } from '@hcengineering/ui/src/colors'
import lead from './plugin'
async function createSpace (tx: TxOperations): Promise<void> {
const current = await tx.findOne(core.class.Space, {
_id: lead.space.DefaultFunnel
})
if (current === undefined) {
await tx.createDoc(
lead.class.Funnel,
core.space.Space,
{
name: 'Funnel',
description: 'Default funnel',
private: false,
archived: false,
members: []
},
lead.space.DefaultFunnel
)
}
const currentTemplate = await tx.findOne(core.class.Space, {
_id: lead.space.FunnelTemplates
})
@@ -59,6 +41,28 @@ async function createSpace (tx: TxOperations): Promise<void> {
lead.space.FunnelTemplates
)
}
const current = await tx.findOne(core.class.Space, {
_id: lead.space.DefaultFunnel
})
if (current === undefined) {
const defaultTmpl = await createDefaultKanbanTemplate(tx)
const [states, doneStates] = await createStates(tx, defaultTmpl)
await tx.createDoc(
lead.class.Funnel,
core.space.Space,
{
name: 'Funnel',
description: 'Default funnel',
private: false,
archived: false,
members: [],
states,
doneStates
},
lead.space.DefaultFunnel
)
}
}
async function createDefaultKanbanTemplate (tx: TxOperations): Promise<Ref<KanbanTemplate>> {
@@ -86,15 +90,6 @@ async function createDefaultKanbanTemplate (tx: TxOperations): Promise<Ref<Kanba
})
}
async function createDefaultKanban (tx: TxOperations): Promise<void> {
const current = await tx.findOne(task.class.Kanban, {
attachedTo: lead.space.DefaultFunnel
})
if (current !== undefined) return
const defaultTmpl = await createDefaultKanbanTemplate(tx)
await createKanban(tx, lead.space.DefaultFunnel, defaultTmpl)
}
async function fixTemplateSpace (tx: TxOperations): Promise<void> {
const templateSpace = await tx.findOne(task.class.KanbanTemplateSpace, { _id: lead.space.FunnelTemplates })
if (templateSpace !== undefined && templateSpace?.attachedToClass === undefined) {
@@ -105,7 +100,6 @@ async function fixTemplateSpace (tx: TxOperations): Promise<void> {
async function createDefaults (tx: TxOperations): Promise<void> {
await createSpace(tx)
await createSequence(tx, lead.class.Lead)
await createDefaultKanban(tx)
await fixTemplateSpace(tx)
}
+4 -4
View File
@@ -14,10 +14,10 @@
// limitations under the License.
//
import type { Ref, Space } from '@hcengineering/core'
import { leadId } from '@hcengineering/lead'
import { NotificationGroup, NotificationType } from '@hcengineering/notification'
import type { Ref } from '@hcengineering/core'
import { Funnel, leadId } from '@hcengineering/lead'
import lead from '@hcengineering/lead-resources/src/plugin'
import { NotificationGroup, NotificationType } from '@hcengineering/notification'
import type { IntlString } from '@hcengineering/platform'
import { mergeIds } from '@hcengineering/platform'
import { KanbanTemplate } from '@hcengineering/task'
@@ -45,7 +45,7 @@ export default mergeIds(leadId, lead, {
NewItemsHeader: '' as AnyComponent
},
space: {
DefaultFunnel: '' as Ref<Space>
DefaultFunnel: '' as Ref<Funnel>
},
template: {
DefaultFunnel: '' as Ref<KanbanTemplate>
+5 -14
View File
@@ -15,9 +15,7 @@
import type { Employee, Person } from '@hcengineering/contact'
import contact from '@hcengineering/contact'
import attachment from '@hcengineering/model-attachment'
import chunter from '@hcengineering/model-chunter'
import { Arr, Attribute, Class, Doc, Domain, IndexKind, Ref, Space, Status, Timestamp } from '@hcengineering/core'
import { Arr, Attribute, Class, Doc, Domain, IndexKind, Ref, Status, Timestamp } from '@hcengineering/core'
import {
Builder,
Collection,
@@ -34,6 +32,8 @@ import {
TypeString,
UX
} from '@hcengineering/model'
import attachment from '@hcengineering/model-attachment'
import chunter from '@hcengineering/model-chunter'
import core, { TAttachedDoc, TClass, TDoc, TSpace, TStatus } from '@hcengineering/model-core'
import view, { createAction, template, actionTemplates as viewTemplates } from '@hcengineering/model-view'
import {} from '@hcengineering/notification'
@@ -42,7 +42,6 @@ import tags from '@hcengineering/tags'
import {
DoneState,
DoneStateTemplate,
Kanban,
KanbanCard,
KanbanTemplate,
KanbanTemplateSpace,
@@ -146,16 +145,11 @@ export class TKanbanCard extends TClass implements KanbanCard {
card!: AnyComponent
}
@Model(task.class.Kanban, core.class.Doc, DOMAIN_KANBAN)
export class TKanban extends TDoc implements Kanban {
states!: Arr<Ref<State>>
doneStates!: Arr<Ref<DoneState>>
attachedTo!: Ref<Space>
}
@Model(task.class.SpaceWithStates, core.class.Space)
export class TSpaceWithStates extends TSpace {
templateId!: Ref<KanbanTemplate>
states!: Arr<Ref<State>>
doneStates!: Arr<Ref<DoneState>>
}
@Model(task.class.KanbanTemplateSpace, core.class.Space)
@@ -171,7 +165,6 @@ export class TKanbanTemplateSpace extends TSpace implements KanbanTemplateSpace
export class TStateTemplate extends TDoc implements StateTemplate {
// We attach to attribute, so we could distinguish between
ofAttribute!: Ref<Attribute<Status>>
attachedTo!: Ref<KanbanTemplate>
@Prop(TypeString(), task.string.StateTemplateTitle)
@@ -187,7 +180,6 @@ export class TStateTemplate extends TDoc implements StateTemplate {
export class TDoneStateTemplate extends TDoc implements DoneStateTemplate {
// We attach to attribute, so we could distinguish between
ofAttribute!: Ref<Attribute<Status>>
attachedTo!: Ref<KanbanTemplate>
@Prop(TypeString(), task.string.StateTemplateTitle)
@@ -298,7 +290,6 @@ export function createModel (builder: Builder): void {
TWonState,
TLostState,
TKanbanCard,
TKanban,
TKanbanTemplateSpace,
TStateTemplate,
TDoneStateTemplate,
+154 -4
View File
@@ -13,12 +13,26 @@
// limitations under the License.
//
import { Class, DOMAIN_TX, Doc, Domain, Ref, Space, TxOperations } from '@hcengineering/core'
import {
Class,
DOMAIN_STATUS,
DOMAIN_TX,
Doc,
Domain,
Ref,
Space,
Status,
TxCollectionCUD,
TxCreateDoc,
TxOperations,
TxUpdateDoc
} from '@hcengineering/core'
import { MigrateOperation, MigrationClient, MigrationUpgradeClient, createOrUpdate } from '@hcengineering/model'
import core from '@hcengineering/model-core'
import core, { DOMAIN_SPACE } from '@hcengineering/model-core'
import tags from '@hcengineering/model-tags'
import { DoneStateTemplate, KanbanTemplate, StateTemplate, genRanks } from '@hcengineering/task'
import view, { Filter } from '@hcengineering/view'
import { DOMAIN_VIEW } from '@hcengineering/model-view'
import { DoneStateTemplate, KanbanTemplate, StateTemplate, Task, genRanks } from '@hcengineering/task'
import view, { Filter, FilteredView } from '@hcengineering/view'
import { DOMAIN_TASK } from '.'
import task from './plugin'
@@ -27,6 +41,8 @@ import task from './plugin'
*/
export const DOMAIN_STATE = 'state' as Domain
type OldStatus = Status & { rank: string }
/**
* @public
*/
@@ -123,8 +139,29 @@ async function createDefaultSequence (tx: TxOperations): Promise<void> {
}
}
async function createDefaultStatesSpace (tx: TxOperations): Promise<void> {
const current = await tx.findOne(core.class.Space, {
_id: task.space.Statuses
})
if (current === undefined) {
await tx.createDoc(
core.class.Space,
core.space.Space,
{
name: 'Statuses',
description: 'Internal space to store all Statuses',
members: [],
private: false,
archived: false
},
task.space.Statuses
)
}
}
async function createDefaults (tx: TxOperations): Promise<void> {
await createDefaultSequence(tx)
await createDefaultStatesSpace(tx)
}
async function renameState (client: MigrationClient): Promise<void> {
@@ -189,9 +226,122 @@ async function renameStatePrefs (client: MigrationUpgradeClient): Promise<void>
}
}
async function migrateStatuses (client: MigrationClient): Promise<void> {
const oldStatuses = await client.find<OldStatus>(DOMAIN_STATUS, { space: { $ne: task.space.Statuses } })
const newStatuses: Map<string, Status> = new Map()
const oldStatusesMap = new Map<Ref<Status>, Ref<Status>>()
oldStatuses.sort((a, b) => a.rank.localeCompare(b.rank))
for (const oldStatus of oldStatuses) {
const name = oldStatus.name.toLowerCase().trim()
const mapId = `${oldStatus.ofAttribute}_${name}`
const current = newStatuses.get(mapId)
if (current !== undefined) {
oldStatusesMap.set(oldStatus._id, current._id)
if (client.hierarchy.isDerived(oldStatus._class, task.class.DoneState)) {
await client.update(DOMAIN_SPACE, { _id: oldStatus.space }, { $addToSet: { doneStates: current._id } })
} else {
await client.update(DOMAIN_SPACE, { _id: oldStatus.space }, { $addToSet: { states: current._id } })
}
} else {
newStatuses.set(mapId, oldStatus)
if (client.hierarchy.isDerived(oldStatus._class, task.class.DoneState)) {
await client.update(DOMAIN_SPACE, { _id: oldStatus.space }, { $addToSet: { doneStates: oldStatus._id } })
} else {
await client.update(DOMAIN_SPACE, { _id: oldStatus.space }, { $addToSet: { states: oldStatus._id } })
}
}
}
if (oldStatusesMap.size > 0) {
const tasks = await client.find<Task>(DOMAIN_TASK, {})
for (const task of tasks) {
const update: any = {}
const newStatus = oldStatusesMap.get(task.status)
if (newStatus !== undefined) {
update.status = newStatus
}
if (task.doneState != null) {
const newDoneStatus = oldStatusesMap.get(task.doneState)
if (newStatus !== undefined) {
update.doneState = newDoneStatus
}
}
if (Object.keys(update).length > 0) {
await client.update(DOMAIN_TASK, { _id: task._id }, update)
}
const txes = await client.find(DOMAIN_TX, { 'tx.objectId': task._id })
for (const tx of txes) {
const update: any = {}
const ctx = tx as TxCollectionCUD<Doc, Task>
if (ctx.tx._class === core.class.TxCreateDoc) {
const createTx = ctx.tx as TxCreateDoc<Task>
const newStatus = oldStatusesMap.get(createTx.attributes.status)
if (newStatus !== undefined) {
update['tx.attributes.status'] = newStatus
}
if (createTx.attributes.doneState != null) {
const newDoneStatus = oldStatusesMap.get(createTx.attributes.doneState)
if (newStatus !== undefined) {
update['tx.attributes.doneState'] = newDoneStatus
}
}
} else if (ctx.tx._class === core.class.TxUpdateDoc) {
const updateTx = ctx.tx as TxUpdateDoc<Task>
if (updateTx.operations.status !== undefined) {
const newStatus = oldStatusesMap.get(updateTx.operations.status)
if (newStatus !== undefined) {
update['tx.operations.status'] = newStatus
}
}
if (updateTx.operations.doneState != null) {
const newDoneStatus = oldStatusesMap.get(updateTx.operations.doneState)
if (newStatus !== undefined) {
update['tx.operations.doneState'] = newDoneStatus
}
}
}
if (Object.keys(update).length > 0) {
await client.update(DOMAIN_TX, { _id: tx._id }, update)
}
}
}
const descendants = client.hierarchy.getDescendants(task.class.Task)
const filters = await client.find<FilteredView>(DOMAIN_VIEW, {
_class: view.class.FilteredView,
filterClass: { $in: descendants }
})
for (const filter of filters) {
const filters = JSON.parse(filter.filters) as Filter[]
let changed = false
for (const filter of filters) {
if (['status, doneStatus'].includes(filter.key.key)) {
for (let index = 0; index < filter.value.length; index++) {
const val = filter.value[index]
const newVal = oldStatusesMap.get(val)
if (newVal !== undefined) {
filter.value[index] = newVal
changed = true
}
}
}
}
if (changed) {
await client.update(DOMAIN_VIEW, { _id: filter._id }, { filters: JSON.stringify(filters) })
}
}
}
const toRemove = Array.from(oldStatusesMap.keys())
for (const remove of toRemove) {
await client.delete(DOMAIN_STATUS, remove)
}
await client.update(DOMAIN_STATUS, { rank: { $exists: true } }, { $unset: { rank: '' } })
await client.update(DOMAIN_STATUS, { space: { $ne: task.space.Statuses } }, { space: task.space.Statuses })
}
export const taskOperation: MigrateOperation = {
async migrate (client: MigrationClient): Promise<void> {
await renameState(client)
await migrateStatuses(client)
},
async upgrade (client: MigrationUpgradeClient): Promise<void> {
const tx = new TxOperations(client, core.account.System)
-4
View File
@@ -958,10 +958,6 @@ export function createModel (builder: Builder): void {
createAggregationManager: tracker.aggregation.CreateComponentAggregationManager
})
builder.mixin(tracker.class.Component, core.class.Class, view.mixin.Groupping, {
grouppingManager: tracker.aggregation.GrouppingComponentManager
})
builder.mixin(tracker.class.Milestone, core.class.Class, view.mixin.ObjectPresenter, {
presenter: tracker.component.MilestonePresenter
})
+9 -15
View File
@@ -13,13 +13,13 @@
// limitations under the License.
//
import core, { Ref, TxOperations, generateId } from '@hcengineering/core'
import core, { TxOperations } from '@hcengineering/core'
import { MigrateOperation, MigrationClient, MigrationUpgradeClient, createOrUpdate } from '@hcengineering/model'
import tags from '@hcengineering/tags'
import { IssueStatus, Project, TimeReportDayType, createStatuses } from '@hcengineering/tracker'
import tracker from './plugin'
import { DOMAIN_TRACKER } from '.'
import { DOMAIN_TASK } from '@hcengineering/model-task'
import tags from '@hcengineering/tags'
import { Project, TimeReportDayType, createStatuses } from '@hcengineering/tracker'
import { DOMAIN_TRACKER } from '.'
import tracker from './plugin'
async function createDefaultProject (tx: TxOperations): Promise<void> {
const current = await tx.findOne(tracker.class.Project, {
@@ -32,7 +32,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 states = await createStatuses(tx, tracker.class.IssueStatus, tracker.attribute.IssueStatus)
await tx.createDoc<Project>(
tracker.class.Project,
@@ -45,19 +45,13 @@ async function createDefaultProject (tx: TxOperations): Promise<void> {
archived: false,
identifier: 'TSK',
sequence: 0,
defaultIssueStatus: defaultStatusId,
defaultIssueStatus: states[0],
defaultTimeReportDay: TimeReportDayType.PreviousWorkDay,
defaultAssignee: undefined
defaultAssignee: undefined,
states
},
tracker.project.DefaultProject
)
await createStatuses(
tx,
tracker.project.DefaultProject,
tracker.class.IssueStatus,
tracker.attribute.IssueStatus,
defaultStatusId
)
}
}
+11 -30
View File
@@ -13,27 +13,18 @@
// limitations under the License.
//
import Core, {
DOMAIN_MODEL,
Account,
Class,
Client,
Data,
Doc,
DocumentQuery,
Domain,
Ref,
Space
} from '@hcengineering/core'
import { Account, Class, Client, DOMAIN_MODEL, Data, Doc, DocumentQuery, Domain, Ref, Space } from '@hcengineering/core'
import { Builder, Mixin, Model } from '@hcengineering/model'
import core, { TClass, TDoc } from '@hcengineering/model-core'
import preference, { TPreference } from '@hcengineering/model-preference'
import presentation from '@hcengineering/model-presentation'
import { Asset, IntlString, Resource, Status } from '@hcengineering/platform'
import { AnyComponent, Location } from '@hcengineering/ui'
import {
Action,
ActionCategory,
ActivityAttributePresenter,
Aggregation,
AllValuesFunc,
ArrayEditor,
AttributeEditor,
@@ -45,10 +36,13 @@ import {
ClassSortFuncs,
CollectionEditor,
CollectionPresenter,
CreateAggregationManagerFunc,
Filter,
FilterMode,
FilteredView,
GetAllValuesFunc,
Groupping,
GrouppingManagerResource,
IgnoreActions,
InlineAttributEditor,
KeyBinding,
@@ -79,13 +73,8 @@ import {
ViewOptionsModel,
Viewlet,
ViewletDescriptor,
ViewletPreference,
Aggregation,
CreateAggregationManagerFunc,
GrouppingManagerResource,
Groupping
ViewletPreference
} from '@hcengineering/view'
import presentation from '@hcengineering/model-presentation'
import view from './plugin'
export { viewId } from '@hcengineering/view'
@@ -765,6 +754,10 @@ export function createModel (builder: Builder): void {
component: view.component.ObjectFilter
})
builder.mixin(core.class.Status, core.class.Class, view.mixin.AttributeFilter, {
component: view.component.StatusFilter
})
builder.mixin(core.class.TypeTimestamp, core.class.Class, view.mixin.AttributeFilter, {
component: view.component.DateFilter,
group: 'bottom'
@@ -1019,10 +1012,6 @@ 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
})
@@ -1030,14 +1019,6 @@ export function createModel (builder: Builder): void {
builder.mixin(core.class.Status, core.class.Class, view.mixin.AttributePresenter, {
presenter: view.component.StatusRefPresenter
})
builder.mixin(Core.class.Status, core.class.Class, view.mixin.Aggregation, {
createAggregationManager: view.aggregation.CreateStatusAggregationManager
})
builder.mixin(Core.class.Status, core.class.Class, view.mixin.Groupping, {
grouppingManager: view.aggregation.GrouppingStatusManager
})
}
export default view
+1 -27
View File
@@ -15,8 +15,7 @@
import { Asset, IntlString } from '@hcengineering/platform'
import { Attribute, Doc, Domain, Ref } from './classes'
import { AggregateValue, AggregateValueData, DocManager, IdMap } from './utils'
import { WithLookup } from './storage'
import { AggregateValue, AggregateValueData } from './utils'
/**
* @public
@@ -53,8 +52,6 @@ export interface Status extends Doc {
color?: number
// Optional description
description?: string
// Lexorank rank for ordering.
rank: string
}
/**
@@ -69,26 +66,3 @@ export class StatusValue extends AggregateValue {
super(name, values)
}
}
/**
* @public
*
* Allow to query for status keys/values.
*/
export class StatusManager extends DocManager {
get (ref: Ref<WithLookup<Status>>): WithLookup<Status> | undefined {
return this.getIdMap().get(ref) as WithLookup<Status>
}
getDocs (): Array<WithLookup<Status>> {
return this.docs as Status[]
}
getIdMap (): IdMap<WithLookup<Status>> {
return this.byId as IdMap<WithLookup<Status>>
}
filter (predicate: (value: Status) => boolean): Status[] {
return this.getDocs().filter(predicate)
}
}
+6 -3
View File
@@ -1,7 +1,7 @@
import { Organization } from '@hcengineering/contact'
import core, { Account, Client, Data, Doc, Ref, SortingOrder, TxOperations } from '@hcengineering/core'
import recruit, { Applicant, Vacancy } from '@hcengineering/recruit'
import task, { KanbanTemplate, State, calcRank, createKanban } from '@hcengineering/task'
import task, { KanbanTemplate, State, calcRank, createStates } from '@hcengineering/task'
export async function createVacancy (
rawClient: Client,
@@ -23,6 +23,8 @@ export async function createVacancy (
const incResult = await client.update(sequence, { $inc: { sequence: 1 } }, true)
const [states, doneStates] = await createStates(client, templateId)
const id = await client.createDoc(recruit.class.Vacancy, core.space.Space, {
name,
description: template.shortDescription ?? '',
@@ -31,10 +33,11 @@ export async function createVacancy (
archived: false,
company,
number: (incResult as any).object.sequence,
members: []
members: [],
states,
doneStates
})
await createKanban(client, id, templateId)
return id
}
@@ -1,23 +1,23 @@
import { readable } from 'svelte/store'
import board, { Board, CommonBoardPreference } from '@hcengineering/board'
import core, { getCurrentAccount, Ref, TxOperations } from '@hcengineering/core'
import type { KanbanTemplate, TodoItem } from '@hcengineering/task'
import core, { Ref, TxOperations, getCurrentAccount } from '@hcengineering/core'
import preference from '@hcengineering/preference'
import { createKanban } from '@hcengineering/task'
import { createQuery, getClient } from '@hcengineering/presentation'
import type { KanbanTemplate, TodoItem } from '@hcengineering/task'
import { createStates } from '@hcengineering/task'
import {
EastSideColor,
FeijoaColor,
FernColor,
FlamingoColor,
MalibuColor,
MediumTurquoiseColor,
MoodyBlueColor,
SeaBuckthornColor,
FeijoaColor,
EastSideColor,
SalmonColor,
SeaBuckthornColor,
SeagullColor,
areDatesEqual
} from '@hcengineering/ui'
import { readable } from 'svelte/store'
export async function createBoard (
client: TxOperations,
@@ -25,16 +25,19 @@ export async function createBoard (
description: string,
templateId?: Ref<KanbanTemplate>
): Promise<Ref<Board>> {
const [states, doneStates] = await createStates(client, templateId)
const boardRef = await client.createDoc(board.class.Board, core.space.Space, {
name,
description,
private: false,
archived: false,
members: [getCurrentAccount()._id],
templateId
templateId,
states,
doneStates
})
await Promise.all([createKanban(client, boardRef, templateId)])
return boardRef
}
@@ -10,7 +10,7 @@
import MessageComponent from './Message.svelte'
export let withHeader: boolean = true
export let filterClass = chunter.class.ChunterMessage
export let search: string = ''
let searchQuery: DocumentQuery<ChunterMessage> = { $search: search }
@@ -22,7 +22,6 @@
$: updateSearchQuery(search)
const client = getClient()
export let filterClass = chunter.class.ChunterMessage
let messages: ChunterMessage[] = []
let resultQuery: DocumentQuery<ChunterMessage> = { ...searchQuery }
@@ -90,7 +89,7 @@
/>
</div>
{/if}
<FilterBar _class={filterClass} query={searchQuery} on:change={(e) => (resultQuery = e.detail)} />
<FilterBar _class={filterClass} space={undefined} query={searchQuery} on:change={(e) => (resultQuery = e.detail)} />
{#if messages.length > 0}
<Scroller>
{#each messages as message}
@@ -91,6 +91,7 @@
<FilterBar
_class={contact.class.Contact}
space={undefined}
{viewOptions}
query={searchQuery}
on:change={(e) => (resultQuery = e.detail)}
+12 -2
View File
@@ -28,7 +28,17 @@ import {
getName,
Person
} from '@hcengineering/contact'
import { Client, Doc, IdMap, ObjQueryType, Ref, Timestamp, getCurrentAccount, toIdMap } from '@hcengineering/core'
import {
Client,
Doc,
IdMap,
ObjQueryType,
Ref,
Timestamp,
TxOperations,
getCurrentAccount,
toIdMap
} from '@hcengineering/core'
import notification, { DocUpdateTx, DocUpdates } from '@hcengineering/notification'
import { getResource } from '@hcengineering/platform'
import { createQuery, getClient } from '@hcengineering/presentation'
@@ -48,7 +58,7 @@ export function formatDate (dueDateMs: Timestamp): string {
})
}
export async function employeeSort (value: Array<Ref<Employee>>): Promise<Array<Ref<Employee>>> {
export async function employeeSort (client: TxOperations, value: Array<Ref<Employee>>): Promise<Array<Ref<Employee>>> {
const h = getClient().getHierarchy()
return value.sort((a, b) => {
const employeeId1 = a as Ref<Employee> | null | undefined
@@ -120,7 +120,7 @@
/>
<ViewletSettingButton bind:viewOptions bind:viewlet />
</div>
<FilterBar {_class} query={searchQuery} {viewOptions} on:change={(e) => (resultQuery = e.detail)} />
<FilterBar {_class} query={searchQuery} {viewOptions} space={undefined} on:change={(e) => (resultQuery = e.detail)} />
{#if viewlet}
{#if loading}
+1
View File
@@ -43,6 +43,7 @@ export interface Customer extends Contact {
* @public
*/
export interface Lead extends Task {
space: Ref<Funnel>
attachedTo: Ref<Customer>
status: Ref<State>
startDate: Timestamp | null
@@ -40,7 +40,7 @@
getClient
} from '@hcengineering/presentation'
import type { Applicant, Candidate, Vacancy } from '@hcengineering/recruit'
import task, { State, calcRank } from '@hcengineering/task'
import task, { State, calcRank, getStates } from '@hcengineering/task'
import ui, {
Button,
ColorPopup,
@@ -60,6 +60,7 @@
import CandidateCard from './CandidateCard.svelte'
import VacancyCard from './VacancyCard.svelte'
import VacancyOrgPresenter from './VacancyOrgPresenter.svelte'
import { statusStore } from '@hcengineering/view-resources'
export let space: Ref<Vacancy>
export let candidate: Ref<Candidate>
@@ -108,10 +109,6 @@
if (selectedState === undefined) {
throw new Error(`Please select initial state:${_space}`)
}
const state = await client.findOne(task.class.State, { space: _space, _id: selectedState?._id })
if (state === undefined) {
throw new Error(`create application: state not found space:${_space}`)
}
const sequence = await client.findOne(task.class.Sequence, { attachedTo: recruit.class.Applicant })
if (sequence === undefined) {
throw new Error('sequence object not found')
@@ -142,7 +139,7 @@
'applications',
{
...doc,
status: state._id,
status: selectedState._id,
doneState: null,
number: (incResult as any).object.sequence,
assignee: doc.assignee,
@@ -193,21 +190,12 @@
let states: Array<{ id: number | string; color: number; label: string }> = []
let selectedState: State | undefined
let rawStates: State[] = []
const statesQuery = createQuery()
$: rawStates = getStates(vacancy, $statusStore)
const spaceQuery = createQuery()
let vacancy: Vacancy | undefined
$: if (_space) {
statesQuery.query(
task.class.State,
{ space: _space },
(res) => {
rawStates = res
},
{ sort: { rank: SortingOrder.Ascending } }
)
spaceQuery.query(recruit.class.Vacancy, { _id: _space }, (res) => {
vacancy = res.shift()
})
@@ -28,7 +28,7 @@
import { Card, createQuery, getClient, InlineAttributeBar, MessageBox } from '@hcengineering/presentation'
import { Vacancy as VacancyClass } from '@hcengineering/recruit'
import tags from '@hcengineering/tags'
import task, { createKanban, KanbanTemplate } from '@hcengineering/task'
import task, { createStates, KanbanTemplate } from '@hcengineering/task'
import tracker, {
calcRank,
Issue,
@@ -76,7 +76,8 @@
comments: 0,
company: '' as Ref<Organization>,
fullDescription: '',
location: ''
location: '',
states: []
}
export function canClose (): boolean {
return name === '' && templateId !== undefined
@@ -142,7 +143,8 @@
estimation: template.estimation,
reports: 0,
relations: [{ _id: id, _class: recruit.class.Vacancy }],
childInfo: []
childInfo: [],
doneState: null
})
if ((template.labels?.length ?? 0) > 0) {
const tagElements = await client.findAll(tags.class.TagElement, { _id: { $in: template.labels } })
@@ -172,6 +174,8 @@
const incResult = await client.update(sequence, { $inc: { sequence: 1 } }, true)
const [states, doneStates] = await createStates(client, templateId)
const id = await client.createDoc(
recruit.class.Vacancy,
core.space.Space,
@@ -185,7 +189,9 @@
number: (incResult as any).object.sequence,
company,
members: [getCurrentAccount()._id],
templateId
templateId,
states,
doneStates
},
objectId
)
@@ -199,8 +205,6 @@
}
}
await createKanban(client, id, templateId)
await descriptionBox.createAttachments()
objectId = generateId()
@@ -93,6 +93,7 @@
<StateRefPresenter
size={'small'}
kind={'link-bordered'}
space={object.space}
shrink={1}
value={object.status}
onChange={(status) => {
@@ -15,11 +15,11 @@
<script lang="ts">
import contact from '@hcengineering/contact'
import ExpandRightDouble from '@hcengineering/contact-resources/src/components/icons/ExpandRightDouble.svelte'
import { FindOptions, SortingOrder } from '@hcengineering/core'
import { FindOptions } from '@hcengineering/core'
import { OK, Severity, Status } from '@hcengineering/platform'
import presentation, { Card, SpaceSelect, createQuery, getClient } from '@hcengineering/presentation'
import type { Applicant, Vacancy } from '@hcengineering/recruit'
import task, { State } from '@hcengineering/task'
import task, { State, getStates } from '@hcengineering/task'
import ui, {
Button,
ColorPopup,
@@ -28,13 +28,14 @@
ListView,
Status as StatusControl,
createFocusManager,
defaultBackground,
deviceOptionsStore as deviceInfo,
getColorNumberByText,
getPlatformColorDef,
defaultBackground,
showPopup,
themeStore
} from '@hcengineering/ui'
import { statusStore } from '@hcengineering/view-resources'
import { moveToSpace } from '@hcengineering/view-resources/src/utils'
import { createEventDispatcher } from 'svelte'
import recruit from '../plugin'
@@ -79,25 +80,18 @@
let states: Array<{ id: number | string; color: number; label: string }> = []
let selectedState: State | undefined
let rawStates: State[] = []
const statesQuery = createQuery()
const spaceQuery = createQuery()
let vacancy: Vacancy | undefined
$: if (_space) {
statesQuery.query(
task.class.State,
{ space: _space },
(res) => {
rawStates = res
},
{ sort: { rank: SortingOrder.Ascending } }
)
spaceQuery.query(recruit.class.Vacancy, { _id: _space }, (res) => {
vacancy = res.shift()
})
}
$: rawStates = getStates(vacancy, $statusStore)
$: if (rawStates.findIndex((it) => it._id === selectedState?._id) === -1) {
selectedState = rawStates[0]
}
@@ -228,6 +228,7 @@
<FilterBar
_class={recruit.mixin.VacancyList}
space={undefined}
{viewOptions}
query={searchQuery}
on:change={(e) => (resultQuery = e.detail)}
@@ -165,6 +165,7 @@
<FilterBar
_class={recruit.class.Vacancy}
space={undefined}
{viewOptions}
query={searchQuery}
on:change={(e) => (resultQuery = e.detail)}
@@ -167,7 +167,7 @@
/>
<ViewletSettingButton bind:viewOptions bind:viewlet />
</div>
<FilterBar {_class} query={searchQuery} {viewOptions} on:change={(e) => (resultQuery = e.detail)} />
<FilterBar {_class} query={searchQuery} space={undefined} {viewOptions} on:change={(e) => (resultQuery = e.detail)} />
<Component is={tags.component.TagsCategoryBar} props={{ targetClass: _class, category }} on:change={handleChange} />
@@ -13,60 +13,66 @@
// limitations under the License.
-->
<script lang="ts">
import { Class, Data, Ref } from '@hcengineering/core'
import presentation, { Card, createQuery, getClient } from '@hcengineering/presentation'
import { DoneState, SpaceWithStates, State, createState } from '@hcengineering/task'
import { EditBox, Label } from '@hcengineering/ui'
import { createEventDispatcher } from 'svelte'
import task from '../plugin'
import presentation, { Card, getClient } from '@hcengineering/presentation'
import { calcRank, DoneState, Kanban, KanbanTemplate, KanbanTemplateSpace, State } from '@hcengineering/task'
import { Class, Data, generateId, Ref, SortingOrder } from '@hcengineering/core'
const dispatch = createEventDispatcher()
const client = getClient()
const hierarchy = client.getHierarchy()
export let status: State | undefined = undefined
export let _class: Ref<Class<State | DoneState>> | undefined = status?._class
export let template: KanbanTemplate | undefined = undefined
export let value = status?.name ?? ''
const isTemplate = template !== undefined
export let space: Kanban | KanbanTemplateSpace | undefined
export let space: Ref<SpaceWithStates>
let canSave = true
let _space: SpaceWithStates | undefined = undefined
const query = createQuery()
$: query.query(task.class.SpaceWithStates, { _id: space }, (res) => {
_space = res[0]
})
const canSave = true
async function save () {
if (space === undefined && template === undefined && status?.space === undefined) return
const attachedTo = isTemplate && template?._id ? { attachedTo: template._id } : {}
const kanban = space as Kanban
if (_class !== undefined && status === undefined) {
const query = isTemplate ? { ...attachedTo } : kanban?.attachedTo ? { space: kanban.attachedTo } : {}
const lastOne = await client.findOne(_class, query, { sort: { rank: SortingOrder.Descending } })
let newDoc: Data<State> = {
ofAttribute: task.attribute.State,
name: value.trim(),
rank: calcRank(lastOne, undefined),
...attachedTo
}
if (_space === undefined || _class === undefined) return
if (status === undefined) {
if (!hierarchy.isDerived(_class, task.class.DoneState)) {
newDoc = {
const newDoc: Data<State> = {
ofAttribute: task.attribute.State,
name: value.trim(),
color: 9,
rank: calcRank(lastOne, undefined),
...attachedTo
color: 9
}
const id = await createState(client, _class, newDoc)
await client.update(_space, { $push: { states: id } })
} else {
const newDoc: Data<DoneState> = {
ofAttribute: task.attribute.DoneState,
name: value.trim()
}
const id = await createState(client, _class, newDoc)
await client.update(_space, { $push: { states: id } })
}
} else {
const id = await createState(client, _class, { ...status, name: value.trim() })
if (!hierarchy.isDerived(_class, task.class.DoneState)) {
const states = _space.states
const index = states.findIndex((x) => x === status?._id)
if (index !== -1) {
states[index] = id
await client.update(_space, { states })
}
} else {
const states = _space.doneStates ?? []
const index = states.findIndex((x) => x === status?._id)
if (index !== -1) {
states[index] = id
await client.update(_space, { doneStates: states })
}
}
const ops = client.apply(template?.space ?? kanban?.attachedTo ?? generateId()).notMatch(_class, {
space: isTemplate && template ? template.space : kanban?.attachedTo,
name: value.trim(),
...attachedTo
})
await ops.createDoc(_class, isTemplate && template ? template.space : kanban?.attachedTo, newDoc)
canSave = await ops.commit()
}
if (status !== undefined && _class !== undefined) {
const ops = client.apply(status._id).notMatch(_class, { space: status.space, name: value.trim(), ...attachedTo })
await ops.update(status, { name: value.trim() })
canSave = await ops.commit()
}
if (canSave) dispatch('close')
dispatch('close')
}
</script>
@@ -0,0 +1,84 @@
<!--
// 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 { Class, Data, Ref, SortingOrder } from '@hcengineering/core'
import presentation, { Card, getClient } from '@hcengineering/presentation'
import { DoneStateTemplate, KanbanTemplate, KanbanTemplateSpace, StateTemplate, calcRank } from '@hcengineering/task'
import { EditBox, Label } from '@hcengineering/ui'
import { createEventDispatcher } from 'svelte'
import task from '../plugin'
const dispatch = createEventDispatcher()
const client = getClient()
const hierarchy = client.getHierarchy()
export let status: StateTemplate | undefined = undefined
export let _class: Ref<Class<StateTemplate | DoneStateTemplate>> | undefined = status?._class
export let template: KanbanTemplate
export let space: KanbanTemplateSpace
export let value = status?.name ?? ''
let canSave = true
async function save () {
if (space === undefined && template === undefined && status?.space === undefined) return
const attachedTo = { attachedTo: template._id }
if (_class !== undefined && status === undefined) {
const lastOne = await client.findOne(_class, attachedTo, { sort: { rank: SortingOrder.Descending } })
let newDoc: Data<StateTemplate> = {
ofAttribute: task.attribute.State,
name: value.trim(),
rank: calcRank(lastOne, undefined),
...attachedTo
}
if (!hierarchy.isDerived(_class, task.class.DoneState)) {
newDoc = {
ofAttribute: task.attribute.State,
name: value.trim(),
color: 9,
rank: calcRank(lastOne, undefined),
...attachedTo
}
}
const ops = client.apply(template.space).notMatch(_class, {
space: template.space,
name: value.trim(),
...attachedTo
})
await ops.createDoc(_class, template.space, newDoc)
canSave = await ops.commit()
}
if (status !== undefined && _class !== undefined) {
const ops = client.apply(status._id).notMatch(_class, { space: status.space, name: value.trim(), ...attachedTo })
await ops.update(status, { name: value.trim() })
canSave = await ops.commit()
}
if (canSave) dispatch('close')
}
</script>
<Card
label={task.string.StatusPopupTitle}
okAction={save}
canSave
okLabel={presentation.string.Save}
on:changeContent
onCancel={() => dispatch('close')}
>
<EditBox focusIndex={1} bind:value placeholder={task.string.StatusName} kind={'large-style'} autoFocus fullSize />
<svelte:fragment slot="error">
{#if !canSave}
<Label label={task.string.NameAlreadyExists} />
{/if}
</svelte:fragment>
</Card>
@@ -14,16 +14,16 @@
// limitations under the License.
-->
<script lang="ts">
import { Class, DocumentQuery, FindOptions, Ref, SortingOrder } from '@hcengineering/core'
import { Class, DocumentQuery, FindOptions, IdMap, Ref, Status } from '@hcengineering/core'
import { createQuery, getClient } from '@hcengineering/presentation'
import { DoneState, SpaceWithStates, State, Task } from '@hcengineering/task'
import type { TabItem } from '@hcengineering/ui'
import { TabList } from '@hcengineering/ui'
import { TableBrowser } from '@hcengineering/view-resources'
import { TableBrowser, statusStore } from '@hcengineering/view-resources'
import task from '../plugin'
import StatesBar from './state/StatesBar.svelte'
import Lost from './icons/Lost.svelte'
import Won from './icons/Won.svelte'
import StatesBar from './state/StatesBar.svelte'
export let _class: Ref<Class<Task>>
export let space: Ref<SpaceWithStates>
@@ -33,14 +33,28 @@
let doneStatusesView: boolean = false
let state: Ref<State> | undefined = undefined
let _space: SpaceWithStates | undefined = undefined
const selectedDoneStates: Set<Ref<DoneState>> = new Set<Ref<DoneState>>()
$: resConfig = updateConfig(config)
let doneStates: DoneState[] = []
let itemsDS: TabItem[] = []
$: doneStates = getDoneStates(_space, $statusStore)
$: itemsDS = getItems(doneStates)
let selectedDS: string[] = []
let withoutDone: boolean = false
let resultQuery: DocumentQuery<Task>
function getItems (doneStates: DoneState[]): TabItem[] {
const itemsDS: TabItem[] = doneStates.map((s) => {
return {
id: s._id,
label: s.name,
icon: s._class === task.class.WonState ? Won : Lost,
color: s._class === task.class.WonState ? 'var(--theme-won-color)' : 'var(--theme-lost-color)'
}
})
itemsDS.unshift({ id: 'NoDoneState', labelIntl: task.string.NoDoneState })
return itemsDS
}
function updateConfig (config: string[]): string[] {
if (state !== undefined) {
return config.filter((p) => p !== 'status')
@@ -51,34 +65,28 @@
return config
}
const doneStateQuery = createQuery()
doneStateQuery.query(
task.class.DoneState,
space != null
? {
space
}
: {},
(res) => {
doneStates = res
itemsDS = doneStates.map((s) => {
return {
id: s._id,
label: s.name,
icon: s._class === task.class.WonState ? Won : Lost,
color: s._class === task.class.WonState ? 'var(--theme-won-color)' : 'var(--theme-lost-color)'
}
})
itemsDS.unshift({ id: 'NoDoneState', labelIntl: task.string.NoDoneState })
},
const spaceQuery = createQuery()
$: spaceQuery.query(
task.class.SpaceWithStates,
{
sort: {
_class: SortingOrder.Descending,
rank: SortingOrder.Descending
}
_id: space
},
(res) => {
_space = res[0]
}
)
function getDoneStates (space: SpaceWithStates | undefined, statusStore: IdMap<Status>): DoneState[] {
if (space === undefined) {
return []
}
const doneStates = space.doneStates
? space.doneStates.map((x) => statusStore.get(x) as DoneState).filter((p) => p !== undefined)
: []
return doneStates
}
const client = getClient()
async function updateQuery (query: DocumentQuery<Task>, selectedDoneStates: Set<Ref<DoneState>>): Promise<void> {
@@ -13,29 +13,36 @@
// limitations under the License.
-->
<script lang="ts">
import { Ref } from '@hcengineering/core'
import { IdMap, Ref, Status } from '@hcengineering/core'
import { createQuery } from '@hcengineering/presentation'
import type { Kanban } from '@hcengineering/task'
import type { SpaceWithStates } from '@hcengineering/task'
import task, { DoneState, LostState, WonState } from '@hcengineering/task'
import { createEventDispatcher } from 'svelte'
import Won from '../icons/Won.svelte'
import Lost from '../icons/Lost.svelte'
import { statusStore } from '@hcengineering/view-resources'
export let kanban: Kanban
export let space: Ref<SpaceWithStates>
let wonStates: WonState[] = []
let lostStates: LostState[] = []
let _space: SpaceWithStates | undefined = undefined
const dispatch = createEventDispatcher()
const doneStatesQ = createQuery()
$: if (kanban !== undefined) {
doneStatesQ.query(task.class.DoneState, { space: kanban.space }, (result) => {
wonStates = result.filter((x) => x._class === task.class.WonState)
lostStates = result.filter((x) => x._class === task.class.LostState)
})
} else {
doneStatesQ.unsubscribe()
const query = createQuery()
query.query(task.class.SpaceWithStates, { _id: space }, (result) => {
_space = result[0]
})
function getStates (space: SpaceWithStates | undefined, statusStore: IdMap<Status>): void {
if (space === undefined) return
const result: Status[] =
(space.doneStates?.map((p) => statusStore.get(p))?.filter((p) => p !== undefined) as Status[]) ?? []
wonStates = result.filter((x) => x._class === task.class.WonState)
lostStates = result.filter((x) => x._class === task.class.LostState)
}
$: getStates(_space, $statusStore)
let hoveredDoneState: Ref<DoneState> | undefined
const onDone = (state: DoneState) => async () => {
@@ -1,77 +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 { Ref, SortingOrder } from '@hcengineering/core'
import { createQuery, getClient } from '@hcengineering/presentation'
import type { DoneState, Kanban, State } from '@hcengineering/task'
import task, { calcRank } from '@hcengineering/task'
import StatesEditor from '../state/StatesEditor.svelte'
export let kanban: Kanban
let states: State[] = []
let doneStates: DoneState[] = []
let wonStates: DoneState[] = []
let lostStates: DoneState[] = []
$: wonStates = doneStates.filter((x) => x._class === task.class.WonState)
$: lostStates = doneStates.filter((x) => x._class === task.class.LostState)
const client = getClient()
const statesQ = createQuery()
$: statesQ.query(
task.class.State,
{ space: kanban.space },
(result) => {
states = result
},
{
sort: {
rank: SortingOrder.Ascending
}
}
)
const doneStatesQ = createQuery()
$: doneStatesQ.query(
task.class.DoneState,
{ space: kanban.space },
(result) => {
doneStates = result
},
{
sort: {
rank: SortingOrder.Ascending
}
}
)
async function onMove ({ detail: { stateID, position } }: { detail: { stateID: Ref<State>; position: number } }) {
const [prev, next] = [states[position - 1], states[position + 1]]
const state = states.find((x) => x._id === stateID)
if (state === undefined) {
return
}
await client.updateDoc(state._class, state.space, state._id, {
rank: calcRank(prev, next)
})
}
</script>
<StatesEditor {states} {wonStates} {lostStates} space={kanban} on:delete on:move={onMove} />
@@ -14,21 +14,20 @@
// limitations under the License.
-->
<script lang="ts">
import { createEventDispatcher } from 'svelte'
import { Ref, Space, SortingOrder } from '@hcengineering/core'
import core from '@hcengineering/core'
import core, { Ref, SortingOrder, Space } from '@hcengineering/core'
import { createQuery, getClient } from '@hcengineering/presentation'
import type {
State,
DoneState,
DoneStateTemplate,
KanbanTemplate,
StateTemplate,
DoneState,
KanbanTemplateSpace
KanbanTemplateSpace,
State,
StateTemplate
} from '@hcengineering/task'
import task, { calcRank } from '@hcengineering/task'
import { createEventDispatcher } from 'svelte'
import StatesEditor from '../state/StatesEditor.svelte'
import StatesTemplateEditor from '../state/StatesTemplateEditor.svelte'
export let kanban: KanbanTemplate
export let folder: KanbanTemplateSpace
@@ -103,7 +102,7 @@
}
</script>
<StatesEditor
<StatesTemplateEditor
template={kanban}
space={folder}
{states}
@@ -27,7 +27,7 @@
import { Item, Kanban as KanbanUI } from '@hcengineering/kanban'
import { getResource } from '@hcengineering/platform'
import { createQuery, getClient, ActionContext } from '@hcengineering/presentation'
import { Kanban, SpaceWithStates, Task, TaskGrouping, TaskOrdering } from '@hcengineering/task'
import { SpaceWithStates, Task, TaskGrouping, TaskOrdering } from '@hcengineering/task'
import {
ColorDefinition,
defaultBackground,
@@ -75,7 +75,6 @@
export let options: FindOptions<Task> | undefined
$: currentSpace = space
$: groupByKey = (viewOptions.groupBy[0] ?? noCategory) as TaskGrouping
$: orderBy = viewOptions.orderBy
$: sort = { [orderBy[0]]: orderBy[1] }
@@ -88,13 +87,6 @@
accentColors = accentColors
}
const spaceQuery = createQuery()
let currentProject: SpaceWithStates | undefined
$: spaceQuery.query(task.class.SpaceWithStates, { _id: currentSpace }, (res) => {
currentProject = res.shift()
})
let resultQuery: DocumentQuery<any> = { ...query }
$: getResultQuery(query, viewOptionsConfig, viewOptions).then((p) => (resultQuery = { ...p, ...query }))
@@ -160,20 +152,21 @@
const queryId = generateId()
$: updateCategories(_class, tasks, groupByKey, viewOptions, viewOptionsConfig)
$: updateCategories(_class, space, tasks, groupByKey, viewOptions, viewOptionsConfig)
function update () {
updateCategories(_class, tasks, groupByKey, viewOptions, viewOptionsConfig)
updateCategories(_class, space, tasks, groupByKey, viewOptions, viewOptionsConfig)
}
async function updateCategories (
_class: Ref<Class<Doc>>,
space: Ref<SpaceWithStates> | undefined,
docs: Doc[],
groupByKey: string,
viewOptions: ViewOptions,
viewOptionsModel: ViewOptionModel[] | undefined
) {
categories = await getCategories(client, _class, docs, groupByKey, viewlet.descriptor)
categories = await getCategories(client, _class, space, docs, groupByKey, viewlet.descriptor)
for (const viewOption of viewOptionsModel ?? []) {
if (viewOption.actionTarget !== 'category') continue
const categoryFunc = viewOption as CategoryOption
@@ -188,6 +181,7 @@
const res = await categoryAction(
_class,
spaces.length > 0 ? { space: { $in: Array.from(spaces.values()) } } : {},
space,
groupByKey,
update,
queryId,
@@ -231,13 +225,6 @@
$: 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>
@@ -301,13 +288,15 @@
</svelte:fragment>
<!-- eslint-disable-next-line no-undef -->
<svelte:fragment slot="doneBar" let:onDone>
<KanbanDragDone
{kanban}
on:done={(e) => {
// eslint-disable-next-line no-undef
onDone(getDoneUpdate(e))
}}
/>
{#if space}
<KanbanDragDone
{space}
on:done={(e) => {
// eslint-disable-next-line no-undef
onDone(getDoneUpdate(e))
}}
/>
{/if}
</svelte:fragment>
</KanbanUI>
{/await}
@@ -26,7 +26,7 @@
export let value: Ref<DoneState> | null | undefined
export let onChange: (value: any) => void
export let space: Ref<SpaceWithStates>
export let space: Ref<SpaceWithStates> | undefined
export let kind: ButtonKind = 'no-border'
export let size: ButtonSize = 'small'
export let justify: 'left' | 'center' = 'center'
@@ -16,11 +16,12 @@
<script lang="ts">
import { Ref, Status, StatusValue } from '@hcengineering/core'
import { statusStore } from '@hcengineering/view-resources'
import type { DoneState } from '@hcengineering/task'
import type { DoneState, SpaceWithStates } from '@hcengineering/task'
import DoneStatePresenter from './DoneStatePresenter.svelte'
import DoneStateEditor from './DoneStateEditor.svelte'
export let value: Ref<DoneState> | StatusValue
export let space: Ref<SpaceWithStates> | undefined
export let showTitle: boolean = true
export let onChange: ((value: Ref<DoneState>) => void) | undefined = undefined
@@ -29,7 +30,7 @@
{#if value}
{#if onChange !== undefined && state !== undefined}
<DoneStateEditor value={state._id} space={state.space} {onChange} kind="link" size="medium" />
<DoneStateEditor value={state._id} {space} {onChange} kind="link" size="medium" />
{:else}
<DoneStatePresenter value={state} {showTitle} />
{/if}
@@ -14,7 +14,7 @@
// limitations under the License.
-->
<script lang="ts">
import { Class, Doc, Ref, SortingOrder } from '@hcengineering/core'
import { Class, Doc, IdMap, Ref, Status } from '@hcengineering/core'
import { createQuery } from '@hcengineering/presentation'
import { DoneState, SpaceWithStates } from '@hcengineering/task'
import { Label, PaletteColorIndexes, getPlatformColor, resizeObserver, themeStore } from '@hcengineering/ui'
@@ -23,24 +23,29 @@
import Lost from '../icons/Lost.svelte'
import Unknown from '../icons/Unknown.svelte'
import Won from '../icons/Won.svelte'
import { statusStore } from '@hcengineering/view-resources'
export let space: Ref<SpaceWithStates>
let states: DoneState[] = []
const dispatch = createEventDispatcher()
const statesQuery = createQuery()
statesQuery.query(
task.class.DoneState,
{ space },
(res) => {
states = res
},
{
sort: {
_class: SortingOrder.Descending,
rank: SortingOrder.Ascending
}
}
)
let _space: SpaceWithStates
function getStates (space: SpaceWithStates | undefined, statesStore: IdMap<Status>): void {
if (space === undefined) return
const res: Status[] =
(space.doneStates?.map((p) => statesStore.get(p))?.filter((p) => p !== undefined) as Status[]) ?? []
res.sort((a, b) => a._class.localeCompare(b._class))
states = res
}
$: getStates(_space, $statusStore)
const spaceQuery = createQuery()
spaceQuery.query(task.class.SpaceWithStates, { _id: space }, (res) => {
_space = res[0]
})
function getColor (_class: Ref<Class<Doc>>): string {
return _class === task.class.WonState
? getPlatformColor(PaletteColorIndexes.Crocodile, $themeStore.dark)
@@ -14,64 +14,67 @@
// limitations under the License.
-->
<script lang="ts">
import type { Class, Doc, DocumentQuery, Obj, Ref } from '@hcengineering/core'
import core from '@hcengineering/core'
import { createQuery, getClient, MessageBox } from '@hcengineering/presentation'
import type { DoneState, Kanban, SpaceWithStates, State } from '@hcengineering/task'
import task from '../../plugin'
import KanbanEditor from '../kanban/KanbanEditor.svelte'
import { Icon, Label, showPopup, Panel, Scroller } from '@hcengineering/ui'
import type { Doc, DocumentQuery, IdMap, Ref, Status } from '@hcengineering/core'
import { MessageBox, createQuery, getClient } from '@hcengineering/presentation'
import type { DoneState, LostState, SpaceWithStates, State, WonState } from '@hcengineering/task'
import { Icon, Label, Panel, Scroller, showPopup } from '@hcengineering/ui'
import { statusStore } from '@hcengineering/view-resources'
import { createEventDispatcher } from 'svelte'
import workbench from '@hcengineering/workbench'
import task from '../../plugin'
import StatesEditor from './StatesEditor.svelte'
export let _id: Ref<SpaceWithStates>
export let spaceClass: Ref<Class<Obj>>
let kanban: Kanban | undefined
let spaceClassInstance: Class<SpaceWithStates> | undefined
let spaceInstance: SpaceWithStates | undefined
const client = getClient()
const hierarchy = client.getHierarchy()
const dispatch = createEventDispatcher()
const kanbanQ = createQuery()
$: kanbanQ.query(task.class.Kanban, { attachedTo: _id }, (result) => {
kanban = result[0]
})
const spaceQ = createQuery()
$: spaceQ.query<Class<SpaceWithStates>>(core.class.Class, { _id: spaceClass }, (result) => {
spaceClassInstance = result.shift()
})
const spaceI = createQuery()
$: spaceI.query<SpaceWithStates>(spaceClass, { _id }, (result) => {
spaceInstance = result.shift()
$: spaceI.query<SpaceWithStates>(task.class.SpaceWithStates, { _id }, (result) => {
spaceInstance = result[0]
})
$: spaceClass = spaceInstance ? hierarchy.getClass(spaceInstance._class) : undefined
$: [states, doneStates] = getStates(spaceInstance, $statusStore)
$: wonStates = doneStates.filter((x) => x._class === task.class.WonState) as WonState[]
$: lostStates = doneStates.filter((x) => x._class === task.class.LostState) as LostState[]
function getStates (space: SpaceWithStates | undefined, statusStore: IdMap<Status>): [Status[], DoneState[]] {
if (space === undefined) {
return [[], []]
}
const states = space.states.map((x) => statusStore.get(x) as Status).filter((p) => p !== undefined)
const doneStates = space.doneStates
? space.doneStates.map((x) => statusStore.get(x) as DoneState).filter((p) => p !== undefined)
: []
return [states, doneStates]
}
async function deleteState ({ state }: { state: State | DoneState }) {
if (spaceInstance === undefined) {
return
}
const spaceClassInstance = client.getHierarchy().getClass(spaceInstance._class)
const spaceView = client.getHierarchy().as(spaceClassInstance, workbench.mixin.SpaceView)
const containingClass = spaceView.view.class
let query: DocumentQuery<Doc>
if (hierarchy.isDerived(state._class, task.class.DoneState)) {
query = { doneState: state._id }
query = { doneState: state._id, space: _id }
} else {
query = { status: state._id }
query = { status: state._id, space: _id }
}
const objectsInThisState = await client.findAll(containingClass, query)
const objectsInThisState = await client.findAll(task.class.Task, query)
if (objectsInThisState.length > 0) {
showPopup(MessageBox, {
label: task.string.CantStatusDelete,
message: task.string.CantStatusDeleteError
message: task.string.CantStatusDeleteError,
canSubmit: false
})
} else {
showPopup(
@@ -82,13 +85,43 @@
},
undefined,
async (result) => {
if (result && kanban !== undefined) {
client.removeDoc(state._class, state.space, state._id)
if (result !== undefined) {
if (hierarchy.isDerived(state._class, task.class.DoneState)) {
const index = doneStates.findIndex((x) => x._id === state._id)
if (index === -1) {
return
}
states.splice(index, 1)
if (spaceInstance) {
await client.update(spaceInstance, { doneStates: states.map((x) => x._id) })
}
} else {
const index = states.findIndex((x) => x._id === state._id)
if (index === -1) {
return
}
states.splice(index, 1)
if (spaceInstance) {
await client.update(spaceInstance, { states: states.map((x) => x._id) })
}
}
}
}
)
}
}
async function onMove (stateID: Ref<State>, position: number) {
const index = states.findIndex((x) => x._id === stateID)
if (index === -1) {
return
}
const elem = states.splice(index, 1)
states = [...states.slice(0, position), elem[0], ...states.slice(position)]
if (spaceInstance) {
await client.update(spaceInstance, { states: states.map((x) => x._id) })
}
}
</script>
<Panel
@@ -108,7 +141,7 @@
<div class="title-wrapper">
<span class="wrapped-title">
<Label label={task.string.ManageStatusesWithin} />
{#if spaceClassInstance}<Label label={spaceClassInstance?.label} />{:else}...{/if}
{#if spaceClass}<Label label={spaceClass?.label} />{:else}...{/if}
</span>
{#if spaceInstance?.name}<span class="wrapped-subtitle">{spaceInstance?.name}</span>{/if}
</div>
@@ -117,9 +150,13 @@
<Scroller>
<div class="popupPanel-body__main-content py-10 clear-mins">
{#if kanban !== undefined}
<KanbanEditor {kanban} on:delete={(e) => deleteState(e.detail)} />
{/if}
<StatesEditor
{states}
{wonStates}
{lostStates}
on:delete={(e) => deleteState(e.detail)}
on:move={(e) => onMove(e.detail.stateID, e.detail.position)}
/>
</div>
</Scroller>
</Panel>
@@ -15,10 +15,10 @@
-->
<script lang="ts">
import { Ref, Space } from '@hcengineering/core'
import task, { State } from '@hcengineering/task'
import { createQuery } from '@hcengineering/presentation'
import { State } from '@hcengineering/task'
import type { ButtonKind, ButtonSize } from '@hcengineering/ui'
import { showPopup, Button, eventToHTMLElement } from '@hcengineering/ui'
import { Button, eventToHTMLElement, showPopup } from '@hcengineering/ui'
import { statusStore } from '@hcengineering/view-resources'
import StatePresenter from './StatePresenter.svelte'
import StatesPopup from './StatesPopup.svelte'
@@ -32,18 +32,9 @@
export let shouldShowName: boolean = true
export let shrink: number = 0
let state: State
$: state = $statusStore.get(value)
let opened: boolean = false
const query = createQuery()
$: query.query(
task.class.State,
{ _id: value },
(res) => {
state = res[0]
},
{ limit: 1 }
)
const handleClick = (ev: MouseEvent) => {
if (!opened) {
opened = true
@@ -14,14 +14,15 @@
// limitations under the License.
-->
<script lang="ts">
import { Ref, Status, StatusValue } from '@hcengineering/core'
import type { ButtonKind, ButtonSize } from '@hcengineering/ui'
import { Ref, Space, Status, StatusValue } from '@hcengineering/core'
import { State } from '@hcengineering/task'
import type { ButtonKind, ButtonSize } from '@hcengineering/ui'
import { statusStore } from '@hcengineering/view-resources'
import StateEditor from './StateEditor.svelte'
import StatePresenter from './StatePresenter.svelte'
export let value: Ref<State> | StatusValue
export let space: Ref<Space>
export let onChange: ((value: Ref<State>) => void) | undefined = undefined
export let kind: ButtonKind = 'link'
export let size: ButtonSize = 'medium'
@@ -33,7 +34,7 @@
{#if value}
{#if onChange !== undefined && state !== undefined}
<StateEditor value={state._id} space={state.space} {onChange} {kind} {size} {shouldShowName} {shrink} />
<StateEditor value={state._id} {space} {onChange} {kind} {size} {shouldShowName} {shrink} />
{:else}
<StatePresenter value={state} {shouldShowName} on:accent-color />
{/if}
@@ -15,8 +15,8 @@
-->
<script lang="ts">
import { Ref } from '@hcengineering/core'
import { BreadcrumbsElement } from '@hcengineering/presentation'
import task, { SpaceWithStates, State } from '@hcengineering/task'
import { BreadcrumbsElement, createQuery } from '@hcengineering/presentation'
import task, { SpaceWithStates, State, getStates } from '@hcengineering/task'
import { ScrollerBar, getColorNumberByText, getPlatformColor, themeStore } from '@hcengineering/ui'
import { statusStore } from '@hcengineering/view-resources'
import { createEventDispatcher } from 'svelte'
@@ -26,7 +26,20 @@
export let state: Ref<State> | undefined = undefined
export let gap: 'none' | 'small' | 'big' = 'small'
$: states = $statusStore.filter((it) => it.space === space && it.ofAttribute === task.attribute.State)
let _space: SpaceWithStates | undefined = undefined
const spaceQuery = createQuery()
spaceQuery.query(
task.class.SpaceWithStates,
{
_id: space
},
(res) => {
_space = res[0]
}
)
$: states = getStates(_space, $statusStore)
let divScroll: HTMLElement
const dispatch = createEventDispatcher()
@@ -16,7 +16,7 @@
<script lang="ts">
import { Ref } from '@hcengineering/core'
import { getClient } from '@hcengineering/presentation'
import type { DoneState, Kanban, KanbanTemplate, KanbanTemplateSpace, State } from '@hcengineering/task'
import type { DoneState, KanbanTemplate, KanbanTemplateSpace, State } from '@hcengineering/task'
import {
CircleButton,
Component,
@@ -40,7 +40,7 @@
import StatusesPopup from './StatusesPopup.svelte'
export let template: KanbanTemplate | undefined = undefined
export let space: KanbanTemplateSpace | Kanban | undefined = undefined
export let space: KanbanTemplateSpace | undefined = undefined
export let states: State[] = []
export let wonStates: DoneState[] = []
export let lostStates: DoneState[] = []
@@ -14,28 +14,23 @@
// limitations under the License.
-->
<script lang="ts">
import { Ref, SortingOrder } from '@hcengineering/core'
import { Ref } from '@hcengineering/core'
import { createQuery } from '@hcengineering/presentation'
import task, { SpaceWithStates, State } from '@hcengineering/task'
import task, { SpaceWithStates, getStates } from '@hcengineering/task'
import { getColorNumberByText, getPlatformColorDef, resizeObserver, themeStore } from '@hcengineering/ui'
import { statusStore } from '@hcengineering/view-resources'
import { createEventDispatcher } from 'svelte'
export let space: Ref<SpaceWithStates>
let states: State[] = []
$: states = getStates(_space, $statusStore)
const dispatch = createEventDispatcher()
const statesQuery = createQuery()
statesQuery.query(
task.class.State,
{ space },
(res) => {
states = res
},
{
sort: {
rank: SortingOrder.Ascending
}
}
)
let _space: SpaceWithStates | undefined = undefined
const query = createQuery()
$: query.query(task.class.SpaceWithStates, { _id: space }, (res) => {
_space = res[0]
})
</script>
<div class="selectPopup" use:resizeObserver={() => dispatch('changeContent')}>
@@ -0,0 +1,313 @@
<!--
// 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 { Class, Ref } from '@hcengineering/core'
import { getClient } from '@hcengineering/presentation'
import type {
DoneStateTemplate,
KanbanTemplate,
KanbanTemplateSpace,
State,
StateTemplate
} from '@hcengineering/task'
import {
CircleButton,
Component,
IconAdd,
IconCircles,
IconMoreH,
Label,
PaletteColorIndexes,
defaultBackground,
eventToHTMLElement,
getColorNumberByText,
getPlatformColorDef,
showPopup,
themeStore
} from '@hcengineering/ui'
import { ColorsPopup, StringPresenter } from '@hcengineering/view-resources'
import { createEventDispatcher } from 'svelte'
import task from '../../plugin'
import Lost from '../icons/Lost.svelte'
import Won from '../icons/Won.svelte'
import StatusesPopup from './StatusesPopup.svelte'
export let template: KanbanTemplate
export let space: KanbanTemplateSpace
export let states: StateTemplate[] = []
export let wonStates: DoneStateTemplate[] = []
export let lostStates: DoneStateTemplate[] = []
const dispatch = createEventDispatcher()
const client = getClient()
const elements: HTMLElement[] = []
let selected: number | undefined
let dragState: Ref<State>
function dragswap (ev: MouseEvent, i: number): boolean {
const s = selected as number
if (i < s) {
return ev.offsetY < elements[i].offsetHeight / 2
} else if (i > s) {
return ev.offsetY > elements[i].offsetHeight / 2
}
return false
}
function dragover (ev: MouseEvent, i: number) {
const s = selected as number
if (dragswap(ev, i)) {
;[states[i], states[s]] = [states[s], states[i]]
selected = i
}
}
async function onMove (to: number) {
dispatch('move', {
stateID: dragState,
position: to
})
}
const onColorChange =
(state: State) =>
async (color: number | undefined): Promise<void> => {
if (color == null) {
return
}
await client.updateDoc(state._class, state.space, state._id, { color })
}
const spaceEditor = space.editor
function add (_class: Ref<Class<StateTemplate | DoneStateTemplate>>) {
showPopup(task.component.CreateStateTemplatePopup, {
space,
template,
_class
})
}
function edit (status: StateTemplate) {
showPopup(task.component.CreateStateTemplatePopup, { status, template, space })
}
</script>
{#if spaceEditor}
<Component is={spaceEditor} props={{ template }} />
{/if}
<div class="flex-no-shrink flex-between trans-title uppercase">
<Label label={task.string.ActiveStates} />
<CircleButton
icon={IconAdd}
size={'medium'}
on:click={() => {
add(task.class.StateTemplate)
}}
/>
</div>
<div class="flex-col mt-3">
{#each states as state, i}
{@const color = getPlatformColorDef(state.color ?? getColorNumberByText(state.name), $themeStore.dark)}
{#if state}
<div
bind:this={elements[i]}
class="flex-between states"
style:background={color.background ?? defaultBackground($themeStore.dark)}
draggable={true}
on:dragover|preventDefault={(ev) => {
dragover(ev, i)
}}
on:drop|preventDefault={() => {
onMove(i)
}}
on:dragstart={() => {
selected = i
dragState = states[i]._id
}}
on:dragend={() => {
selected = undefined
}}
>
<div class="bar"><IconCircles size={'small'} /></div>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div
class="color"
style:background-color={color.color}
on:click={() => {
showPopup(ColorsPopup, { selected: color.name }, elements[i], onColorChange(state))
}}
/>
<div class="flex-grow caption-color">
<StringPresenter value={state.name} oneLine />
</div>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div
class="tool hover-trans"
on:click={(ev) => {
showPopup(
StatusesPopup,
{
onDelete: () => dispatch('delete', { state }),
showDelete: states.length > 1,
onUpdate: () => edit(state)
},
eventToHTMLElement(ev),
() => {}
)
}}
>
<IconMoreH size={'medium'} />
</div>
</div>
{/if}
{/each}
</div>
<div class="flex-col mt-9">
<div class="flex-no-shrink flex-between trans-title uppercase">
<Label label={task.string.DoneStatesWon} />
<CircleButton
icon={IconAdd}
size={'medium'}
on:click={() => {
add(task.class.WonStateTemplate)
}}
/>
</div>
<div class="flex-col mt-4">
{#each wonStates as state}
{@const color = getPlatformColorDef(PaletteColorIndexes.Crocodile, $themeStore.dark)}
{#if state}
<div
class="states flex-row-center"
style:color={color.title}
style:background={color.background ?? defaultBackground($themeStore.dark)}
>
<div class="bar" />
<div class="mr-2">
<Won size={'medium'} />
</div>
<div class="flex-grow caption-color">
<StringPresenter value={state.name} oneLine />
<!-- <AttributeEditor maxWidth={'13rem'} _class={state._class} object={state} key="name" />-->
</div>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div
class="tool hover-trans"
on:click={(ev) => {
showPopup(
StatusesPopup,
{
onDelete: () => dispatch('delete', { state }),
showDelete: wonStates.length > 1,
onUpdate: () => edit(state)
},
eventToHTMLElement(ev),
() => {}
)
}}
>
<IconMoreH size={'medium'} />
</div>
</div>
{/if}
{/each}
</div>
</div>
<div class="mt-9">
<div class="flex-no-shrink flex-between trans-title uppercase">
<Label label={task.string.DoneStatesLost} />
<CircleButton
icon={IconAdd}
size={'medium'}
on:click={() => {
add(task.class.LostStateTemplate)
}}
/>
</div>
<div class="mt-4 mb-10">
{#each lostStates as state}
{@const color = getPlatformColorDef(PaletteColorIndexes.Firework, $themeStore.dark)}
{#if state}
<div
class="states flex-row-center"
style:color={color.title}
style:background={color.background ?? defaultBackground($themeStore.dark)}
>
<div class="bar" />
<div class="mr-2">
<Lost size={'medium'} />
</div>
<div class="flex-grow caption-color">
<StringPresenter value={state.name} oneLine />
<!-- <AttributeEditor maxWidth={'13rem'} _class={state._class} object={state} key="name" />-->
</div>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div
class="tool hover-trans"
on:click={(ev) => {
showPopup(
StatusesPopup,
{
onDelete: () => dispatch('delete', { state }),
showDelete: lostStates.length > 1,
onUpdate: () => edit(state)
},
eventToHTMLElement(ev),
() => {}
)
}}
>
<IconMoreH size={'medium'} />
</div>
</div>
{/if}
{/each}
</div>
</div>
<style lang="scss">
.states {
padding: 0.5rem 1rem 0.5rem 0.25rem;
color: var(--theme-caption-color);
background-color: var(--theme-button-default);
border: 1px solid var(--theme-button-border);
border-radius: 0.5rem;
user-select: none;
.bar {
margin-right: 0.25rem;
width: 1rem;
height: 1rem;
opacity: 0.4;
cursor: grabbing;
}
.color {
margin-right: 0.75rem;
width: 1rem;
height: 1rem;
border-radius: 0.25rem;
cursor: pointer;
}
.tool {
margin-left: 1rem;
}
}
.states + .states {
margin-top: 0.5rem;
}
</style>
+3 -1
View File
@@ -39,6 +39,7 @@ import DoneStateRefPresenter from './components/state/DoneStateRefPresenter.svel
import StateRefPresenter from './components/state/StateRefPresenter.svelte'
import DueDateEditor from './components/DueDateEditor.svelte'
import CreateStatePopup from './components/CreateStatePopup.svelte'
import CreateStateTemplatePopup from './components/CreateStateTemplatePopup.svelte'
export { default as AssigneePresenter } from './components/AssigneePresenter.svelte'
export { StateRefPresenter }
@@ -71,7 +72,8 @@ export default async (): Promise<Resources> => ({
StateRefPresenter,
TodoItemsPopup,
DueDateEditor,
CreateStatePopup
CreateStatePopup,
CreateStateTemplatePopup
},
actionImpl: {
EditStatuses: editStatuses
+10 -97
View File
@@ -25,15 +25,13 @@ import {
Ref,
Space,
Status,
Timestamp,
TxOperations
Timestamp
} 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 { Action, ViewletDescriptor } from '@hcengineering/view'
import { genRanks } from './utils'
/**
* @public
@@ -47,6 +45,8 @@ export interface DocWithRank extends Doc {
*/
export interface SpaceWithStates extends Space {
templateId?: Ref<KanbanTemplate>
states: Ref<State>[]
doneStates?: Ref<DoneState>[]
}
// S T A T E
@@ -61,9 +61,7 @@ export interface State extends Status {
/**
* @public
*/
export interface DoneState extends Status {
name: string
}
export interface DoneState extends Status {}
/**
* @public
@@ -108,13 +106,6 @@ export interface KanbanCard extends Class<Doc> {
card: AnyComponent
}
/**
* @public
*/
export interface Kanban extends Doc {
attachedTo: Ref<Space>
}
/**
* @public
*/
@@ -128,6 +119,7 @@ export interface Sequence extends Doc {
*/
export interface StateTemplate extends Doc, State {
attachedTo: Ref<KanbanTemplate>
rank: string
}
/**
@@ -135,6 +127,7 @@ export interface StateTemplate extends Doc, State {
*/
export interface DoneStateTemplate extends Doc, DoneState {
attachedTo: Ref<KanbanTemplate>
rank: string
}
/**
@@ -248,7 +241,6 @@ const task = plugin(taskId, {
LostState: '' as Ref<Class<LostState>>,
SpaceWithStates: '' as Ref<Class<SpaceWithStates>>,
Task: '' as Ref<Class<Task>>,
Kanban: '' as Ref<Class<Kanban>>,
Sequence: '' as Ref<Class<Sequence>>,
StateTemplate: '' as Ref<Class<StateTemplate>>,
DoneStateTemplate: '' as Ref<Class<DoneStateTemplate>>,
@@ -277,13 +269,15 @@ const task = plugin(taskId, {
Task: '' as Ref<Task>
},
space: {
Sequence: '' as Ref<Space>
Sequence: '' as Ref<Space>,
Statuses: '' as Ref<Space>
},
component: {
KanbanTemplateEditor: '' as AnyComponent,
KanbanTemplateSelector: '' as AnyComponent,
TodoItemsPopup: '' as AnyComponent,
CreateStatePopup: '' as AnyComponent
CreateStatePopup: '' as AnyComponent,
CreateStateTemplatePopup: '' as AnyComponent
},
ids: {
AssigneedNotification: '' as Ref<NotificationType>
@@ -292,84 +286,3 @@ const task = plugin(taskId, {
export default task
export * from './utils'
/**
* @public
*/
export async function createKanban (
client: TxOperations,
attachedTo: Ref<Space>,
templateId?: Ref<KanbanTemplate>
): Promise<Ref<Kanban>> {
if (templateId === undefined) {
await client.createDoc(task.class.State, attachedTo, {
ofAttribute: task.attribute.State,
name: 'New State',
color: 9,
rank: [...genRanks(1)][0]
})
const ranks = [...genRanks(2)]
await Promise.all([
client.createDoc(task.class.WonState, attachedTo, {
ofAttribute: task.attribute.DoneState,
name: 'Won',
rank: ranks[0]
}),
client.createDoc(task.class.LostState, attachedTo, {
ofAttribute: task.attribute.DoneState,
name: 'Lost',
rank: ranks[1]
})
])
return await client.createDoc(task.class.Kanban, attachedTo, {
attachedTo
})
}
const template = await client.findOne(task.class.KanbanTemplate, { _id: templateId })
if (template === undefined) {
throw Error(`Failed to find target kanban template: ${templateId}`)
}
const tmplStates = await client.findAll(task.class.StateTemplate, { attachedTo: template._id })
await Promise.all(
tmplStates.map(
async (state) =>
await client.createDoc(task.class.State, attachedTo, {
ofAttribute: task.attribute.State,
color: state.color,
description: state.description,
name: state.name,
rank: state.rank
})
)
)
const doneClassMap = new Map<Ref<Class<DoneStateTemplate>>, Ref<Class<DoneState>>>([
[task.class.WonStateTemplate, task.class.WonState],
[task.class.LostStateTemplate, task.class.LostState]
])
const tmplDoneStates = await client.findAll(task.class.DoneStateTemplate, { attachedTo: template._id })
await Promise.all(
tmplDoneStates.map(async (state) => {
const cl = doneClassMap.get(state._class)
if (cl === undefined) {
return
}
return await client.createDoc(cl, attachedTo, {
ofAttribute: task.attribute.DoneState,
description: state.description,
name: state.name,
rank: state.rank
})
})
)
return await client.createDoc(task.class.Kanban, attachedTo, {
attachedTo
})
}
+123 -1
View File
@@ -13,8 +13,10 @@
// limitations under the License.
//
import { LexoRank, LexoDecimal, LexoNumeralSystem36 } from 'lexorank'
import { Class, Data, DocumentQuery, IdMap, Ref, SortingOrder, Status, TxOperations } from '@hcengineering/core'
import { LexoDecimal, LexoNumeralSystem36, LexoRank } from 'lexorank'
import LexoRankBucket from 'lexorank/lib/lexoRank/lexoRankBucket'
import task, { DoneState, DoneStateTemplate, KanbanTemplate, SpaceWithStates, State } from '.'
/**
* @public
@@ -44,3 +46,123 @@ export const calcRank = (prev?: { rank: string }, next?: { rank: string }): stri
}
return a.between(b).toString()
}
/**
* @public
*/
export function getStates (space: SpaceWithStates | undefined, statusStore: IdMap<Status>): Status[] {
if (space === undefined) {
return []
}
const states = space.states.map((x) => statusStore.get(x) as Status).filter((p) => p !== undefined)
return states
}
/**
* @public
*/
export async function createState<T extends Status> (
client: TxOperations,
_class: Ref<Class<T>>,
data: Data<T>,
_id?: Ref<T>
): Promise<Ref<T>> {
const query: DocumentQuery<Status> = { name: data.name, ofAttribute: data.ofAttribute }
if (data.category !== undefined) {
query.category = data.category
}
const exists = await client.findOne(_class, query)
if (exists !== undefined) {
return exists._id as Ref<T>
}
const res = await client.createDoc(_class, task.space.Statuses, data, _id)
return res
}
/**
* @public
*/
export async function createStates (
client: TxOperations,
templateId?: Ref<KanbanTemplate>
): Promise<[Ref<Status>[], Ref<DoneState>[]]> {
if (templateId === undefined) {
const state = await createState(client, task.class.State, {
ofAttribute: task.attribute.State,
name: 'New State',
color: 9
})
const doneStates: Ref<DoneState>[] = []
doneStates.push(
await createState(client, task.class.WonState, {
ofAttribute: task.attribute.DoneState,
name: 'Won'
})
)
doneStates.push(
await createState(client, task.class.LostState, {
ofAttribute: task.attribute.DoneState,
name: 'Lost'
})
)
return [[state], doneStates]
}
const template = await client.findOne(task.class.KanbanTemplate, { _id: templateId })
if (template === undefined) {
throw Error(`Failed to find target kanban template: ${templateId}`)
}
const states: Ref<State>[] = []
const doneStates: Ref<DoneState>[] = []
const tmplStates = await client.findAll(
task.class.StateTemplate,
{ attachedTo: template._id },
{ sort: { rank: SortingOrder.Ascending } }
)
for (const state of tmplStates) {
states.push(
await createState(client, task.class.State, {
ofAttribute: task.attribute.State,
color: state.color,
description: state.description,
name: state.name
})
)
}
const doneClassMap = new Map<Ref<Class<DoneStateTemplate>>, Ref<Class<DoneState>>>([
[task.class.WonStateTemplate, task.class.WonState],
[task.class.LostStateTemplate, task.class.LostState]
])
const tmplDoneStates = await client.findAll(
task.class.DoneStateTemplate,
{ attachedTo: template._id },
{ sort: { rank: SortingOrder.Ascending } }
)
for (const state of tmplDoneStates) {
const cl = doneClassMap.get(state._class)
if (cl === undefined) {
continue
}
doneStates.push(
await createState(client, cl, {
ofAttribute: task.attribute.DoneState,
description: state.description,
name: state.name
})
)
}
return [states, doneStates]
}
@@ -102,7 +102,7 @@
<div class="flex-center clear-mins w-full h-9">
{#if issue?.$lookup?.status}
<div class="icon mr-4 h-8">
<IssueStatusIcon value={issue.$lookup.status} size="small" />
<IssueStatusIcon value={issue.$lookup.status} space={issue.space} size="small" />
</div>
{/if}
<span class="overflow-label flex-no-shrink mr-3">{issueId}</span>
@@ -13,19 +13,21 @@
// limitations under the License.
-->
<script lang="ts">
import { TxUpdateDoc } from '@hcengineering/core'
import { Issue } from '@hcengineering/tracker'
import { Ref, TxUpdateDoc } from '@hcengineering/core'
import { Issue, Project } from '@hcengineering/tracker'
import { statusStore } from '@hcengineering/view-resources'
import IssueStatusIcon from '../issues/IssueStatusIcon.svelte'
export let tx: TxUpdateDoc<Issue>
$: value = tx.operations.status
$: status = value && $statusStore.getIdMap().get(value)
$: status = value && $statusStore.get(value)
$: space = tx.objectSpace as Ref<Project>
</script>
<div class="icon">
{#if status}
<IssueStatusIcon value={status} size="small" on:accent-color />
<IssueStatusIcon value={status} {space} size="small" on:accent-color />
{/if}
</div>
@@ -105,6 +105,7 @@
<FilterBar
_class={tracker.class.Component}
{space}
query={searchQuery}
{viewOptions}
on:change={({ detail }) => (resultQuery = detail)}
@@ -29,7 +29,7 @@
<!-- <Icon icon={tracker.icon.TrackerApplication} size={'medium'} /> -->
<FixedColumn key="object-popup-issue-status">
{#if st}
<IssueStatusIcon value={st} size={'small'} />
<IssueStatusIcon value={st} size={'small'} space={value.space} />
{/if}
</FixedColumn>
<span class="ml-2 max-w-120 overflow-label">
@@ -1,8 +1,7 @@
<script lang="ts">
import presentation, { copyTextToClipboard, createQuery } from '@hcengineering/presentation'
import { getMetadata } from '@hcengineering/platform'
import presentation, { copyTextToClipboard, createQuery } from '@hcengineering/presentation'
import { Issue, IssueStatus } from '@hcengineering/tracker'
import view from '@hcengineering/view'
import {
AnySvelteComponent,
Button,
@@ -15,8 +14,10 @@
navigate,
parseLocation
} from '@hcengineering/ui'
import view from '@hcengineering/view'
import { fade } from 'svelte/transition'
import { statusStore } from '@hcengineering/view-resources'
import tracker from '../../plugin'
import IssuePresenter from './IssuePresenter.svelte'
import IssueStatusIcon from './IssueStatusIcon.svelte'
@@ -25,7 +26,6 @@
export let onRemove: () => void
const issueQuery = createQuery()
const statusQuery = createQuery()
let issue: Issue | undefined
let status: IssueStatus | undefined
@@ -42,14 +42,7 @@
)
$: if (issue?.status !== undefined) {
statusQuery.query(
tracker.class.IssueStatus,
{ _id: issue.status },
(res) => {
status = res[0]
},
{ limit: 1 }
)
status = $statusStore.get(issue.status)
}
const getIcon = (): AnySvelteComponent | undefined => {
@@ -107,8 +100,8 @@
</div>
<div class="content flex-row-center flex-wrap gap-2 reverse">
{#if status}
<IssueStatusIcon value={status} size="small" />
{#if status && issue}
<IssueStatusIcon value={status} space={issue.space} size="small" />
{/if}
{#if issue}
<IssuePresenter value={issue} />
@@ -84,7 +84,7 @@
displaySt = result
}
$: updateStatus(txes, $statusStore.getIdMap(), $ticker)
$: updateStatus(txes, $statusStore, $ticker)
</script>
<Row>
@@ -93,6 +93,6 @@
</span>
</Row>
{#each displaySt as st}
<StatusPresenter value={st.status} />
<StatusPresenter value={st.status} space={issue.space} />
<Duration value={st.duration} />
{/each}
@@ -13,9 +13,10 @@
// limitations under the License.
-->
<script lang="ts">
import core, { StatusCategory, WithLookup } from '@hcengineering/core'
import { getClient } from '@hcengineering/presentation'
import { IssueStatus } from '@hcengineering/tracker'
import core, { Ref, StatusCategory, WithLookup } from '@hcengineering/core'
import { createQuery, getClient } from '@hcengineering/presentation'
import { getStates } from '@hcengineering/task'
import { IssueStatus, Project } from '@hcengineering/tracker'
import { IconSize } from '@hcengineering/ui'
import { statusStore } from '@hcengineering/view-resources'
import tracker from '../../plugin'
@@ -23,6 +24,7 @@
export let value: WithLookup<IssueStatus>
export let size: IconSize
export let space: Ref<Project> | undefined
const dynamicFillCategories = [tracker.issueStatusCategory.Started]
@@ -35,17 +37,16 @@
count: number | undefined
} = { index: undefined, count: undefined }
const spaceQuery = createQuery()
let _space: Project | undefined = undefined
$: space
? spaceQuery.query(tracker.class.Project, { _id: space }, (res) => {
_space = res[0]
})
: (_space = undefined)
$: if (value.category === tracker.issueStatusCategory.Started) {
const _s = [
...$statusStore.filter(
(it) =>
it.ofAttribute === value.ofAttribute &&
it.category === tracker.issueStatusCategory.Started &&
it.space === value.space
)
]
_s.sort((a, b) => a.rank.localeCompare(b.rank))
statuses = _s
statuses = getStates(_space, $statusStore).filter((p) => p.category === tracker.issueStatusCategory.Started)
}
async function updateCategory (status: WithLookup<IssueStatus>, statuses: IssueStatus[]) {
@@ -42,8 +42,7 @@
$: activeStatusQuery.query(
tracker.class.IssueStatus,
{
category: { $in: [tracker.issueStatusCategory.Unstarted, tracker.issueStatusCategory.Started] },
...spaceQuery
category: { $in: [tracker.issueStatusCategory.Unstarted, tracker.issueStatusCategory.Started] }
},
(result) => {
active = { status: { $in: result.map(({ _id }) => _id) }, ...spaceQuery }
@@ -54,7 +53,7 @@
let backlog: DocumentQuery<Issue> = {}
$: backlogStatusQuery.query(
tracker.class.IssueStatus,
{ category: tracker.issueStatusCategory.Backlog, ...spaceQuery },
{ category: tracker.issueStatusCategory.Backlog },
(result) => {
backlog = { status: { $in: result.map(({ _id }) => _id) }, ...spaceQuery }
}
@@ -76,7 +76,13 @@
{/if}
</svelte:fragment>
</SpaceHeader>
<FilterBar _class={tracker.class.Issue} query={searchQuery} {viewOptions} on:change={(e) => (resultQuery = e.detail)} />
<FilterBar
_class={tracker.class.Issue}
{space}
query={searchQuery}
{viewOptions}
on:change={(e) => (resultQuery = e.detail)}
/>
<slot name="afterHeader" />
<div class="popupPanel rowContent">
{#if viewlet && viewOptions}
@@ -196,7 +196,7 @@
viewOptions: ViewOptions,
viewOptionsModel: ViewOptionModel[] | undefined
) {
categories = await getCategories(client, _class, docs, groupByKey, viewlet.descriptor)
categories = await getCategories(client, _class, space, docs, groupByKey, viewlet.descriptor)
for (const viewOption of viewOptionsModel ?? []) {
if (viewOption.actionTarget !== 'category') continue
const categoryFunc = viewOption as CategoryOption
@@ -210,6 +210,7 @@
const res = await categoryAction(
_class,
spaces.length > 0 ? { space: { $in: Array.from(spaces.values()) } } : {},
space,
groupByKey,
update,
queryId,
@@ -40,6 +40,7 @@
import StatusMovePresenter from './move/StatusMovePresenter.svelte'
import SelectReplacement from './move/SelectReplacement.svelte'
import PriorityEditor from './PriorityEditor.svelte'
import { getStates } from '@hcengineering/task'
export let selected: Issue | Issue[]
$: docs = Array.isArray(selected) ? selected : [selected]
@@ -48,7 +49,7 @@
const dispatch = createEventDispatcher()
const hierarchy = client.getHierarchy()
let currentSpace: Project | undefined
let targetProject: Project | undefined
let space: Ref<Project>
$: {
@@ -61,32 +62,22 @@
let processing = false
async function createMissingStatus (st: Ref<Status>): Promise<void> {
const cur = $statusStore.get(st)
const statuses = $statusStore.filter((it) => it.space === currentSpace?._id)
if (cur === undefined || currentSpace === undefined || statuses.find((s) => s.name === cur.name) !== undefined) {
return
if (targetProject) {
await client.update(targetProject, { $push: { states: st } })
}
await client.createDoc(cur._class, currentSpace._id, {
name: cur.name,
ofAttribute: cur.ofAttribute,
category: cur.category,
color: cur.color,
description: cur.description,
rank: cur.rank
})
}
async function createMissingComponent (c: Ref<Component>): Promise<void> {
const cur = $componentStore.get(c)
const components = $componentStore.filter((it) => it.space === currentSpace?._id)
const components = $componentStore.filter((it) => it.space === targetProject?._id)
if (
cur === undefined ||
currentSpace === undefined ||
targetProject === undefined ||
components.find((c) => c.label === cur.label) !== undefined
) {
return
}
await client.createDoc(cur._class, currentSpace._id, {
await client.createDoc(cur._class, targetProject._id, {
label: cur.label,
attachments: 0,
description: cur.description,
@@ -96,7 +87,7 @@
}
const moveAll = async () => {
if (currentSpace === undefined) {
if (targetProject === undefined) {
return
}
processing = true
@@ -133,7 +124,7 @@
issueToUpdate.set(issue._id, upd)
}
await moveIssueToSpace(client, docs, currentSpace, issueToUpdate)
await moveIssueToSpace(client, docs, targetProject, issueToUpdate)
processing = false
dispatch('close')
}
@@ -145,7 +136,7 @@
let componentToUpdate: Record<Ref<Component>, ComponentToUpdate | undefined> = {}
$: targetSpaceQuery.query(tracker.class.Project, { _id: space }, (res) => {
;[currentSpace] = res
;[targetProject] = res
})
let toMove: Issue[] = []
@@ -159,8 +150,8 @@
$: if (keepOriginalAttribytes) {
setOriginalAttributes()
} else if (currentSpace !== undefined) {
setReplacementAttributres(currentSpace)
} else if (targetProject !== undefined) {
setReplacementAttributres(targetProject)
}
const componentQuery = createQuery()
@@ -175,11 +166,11 @@
milestones = res
})
$: statuses = $statusStore.filter((it) => it.space === currentSpace?._id)
$: statuses = getStates(targetProject, $statusStore)
let keepOriginalAttribytes: boolean = false
let showManageAttributes: boolean = false
$: isManageAttributesAvailable = issueToUpdate.size > 0 && docs[0]?.space !== currentSpace?._id
$: isManageAttributesAvailable = issueToUpdate.size > 0 && docs[0]?.space !== targetProject?._id
function setOriginalAttributes () {
for (const issue of toMove) {
@@ -225,7 +216,7 @@
upd.status = undefined
}
if (upd.status === undefined) {
upd.status = findTargetStatus($statusStore, issue.status, space, true) ?? currentSpace.defaultIssueStatus
upd.status = findTargetStatus(issue.status, currentSpace, $statusStore, true) ?? currentSpace.defaultIssueStatus
}
if (issue.component !== undefined) {
@@ -268,7 +259,7 @@
label={showManageAttributes ? tracker.string.ManageAttributes : tracker.string.MoveIssues}
okLabel={view.string.Move}
okAction={moveAll}
canSave={docs[0]?.space !== currentSpace?._id}
canSave={docs[0]?.space !== targetProject?._id}
onCancel={() => dispatch('close')}
backAction={() => {
showManageAttributes = !showManageAttributes
@@ -281,7 +272,7 @@
hideAttachments
numberOfBlocks={showManageAttributes
? toMove.length
: currentSpace !== undefined && !keepOriginalAttribytes && docs[0]?.space !== currentSpace?._id
: targetProject !== undefined && !keepOriginalAttribytes && docs[0]?.space !== targetProject?._id
? 1
: 0}
on:changeContent
@@ -306,9 +297,9 @@
{#if !showManageAttributes}
<div class="flex-between">
{#if currentSpace !== undefined}
{#if targetProject !== undefined}
<SpaceSelector
_class={currentSpace._class}
_class={targetProject._class}
label={hierarchy.getClass(tracker.class.Project).label}
bind:space
kind={'regular'}
@@ -337,36 +328,37 @@
<svelte:fragment slot="blocks" let:block>
{#if !showManageAttributes}
{#if currentSpace !== undefined && !keepOriginalAttribytes}
{#if targetProject !== undefined && !keepOriginalAttribytes}
<SelectReplacement
currentProject={space}
{statuses}
{components}
targetProject={currentSpace}
{targetProject}
issues={toMove}
bind:statusToUpdate
bind:componentToUpdate
/>
{/if}
{:else if toMove.length > 0 && currentSpace}
{:else if toMove.length > 0 && targetProject}
{@const issue = toMove[block]}
{@const upd = issueToUpdate.get(issue._id) ?? {}}
{@const originalComponent = components.find((it) => it._id === issue.component)}
{@const targetComponent = components.find(
(it) => it.space === currentSpace?._id && it.label === originalComponent?.label
(it) => it.space === targetProject?._id && it.label === originalComponent?.label
)}
{#key keepOriginalAttribytes}
{#if issue.space !== currentSpace._id && (upd.status !== undefined || upd.component !== undefined)}
{#if issue.space !== targetProject._id && (upd.status !== undefined || upd.component !== undefined)}
<div class="flex-row-center min-h-9 gap-1-5 content-color">
<PriorityEditor value={issue} isEditable={false} kind={'list'} size={'small'} shouldShowLabel={false} />
<IssuePresenter value={issue} disabled kind={'list'} />
<TitlePresenter disabled value={issue} showParent={false} maxWidth={'7.5rem'} />
</div>
{#key upd.status}
<StatusMovePresenter {issue} bind:issueToUpdate targetProject={currentSpace} {statuses} />
<StatusMovePresenter currentProject={space} {issue} bind:issueToUpdate {targetProject} {statuses} />
{/key}
{#if targetComponent === undefined}
{#key upd.component}
<ComponentMovePresenter {issue} bind:issueToUpdate targetProject={currentSpace} {components} />
<ComponentMovePresenter {issue} bind:issueToUpdate {targetProject} {components} />
{/key}
{/if}
{/if}
@@ -13,8 +13,8 @@
// limitations under the License.
-->
<script lang="ts">
import { AttachedData, Ref, StatusManager, WithLookup } from '@hcengineering/core'
import { getClient } from '@hcengineering/presentation'
import { AttachedData, IdMap, Ref, Status, WithLookup } from '@hcengineering/core'
import { createQuery, getClient } from '@hcengineering/presentation'
import { Issue, IssueDraft, IssueStatus, Project } from '@hcengineering/tracker'
import {
ButtonKind,
@@ -31,7 +31,6 @@
import tracker from '../../plugin'
import IssueStatusIcon from './IssueStatusIcon.svelte'
import StatusPresenter from './StatusPresenter.svelte'
type ValueType = Issue | (AttachedData<Issue> & { space: Ref<Project> }) | IssueDraft
export let value: ValueType
@@ -82,11 +81,19 @@
)
}
function getStatuses (statusStore: StatusManager, value: ValueType): WithLookup<IssueStatus>[] {
return statusStore.filter((it) => it.space === value?.space)
let space: Project | undefined = undefined
const query = createQuery()
$: query.query(tracker.class.Project, { _id: value.space }, (res) => {
space = res[0]
})
function getStatuses (statuses: IdMap<Status>, space: Project | undefined): IssueStatus[] {
if (space === undefined) return []
return space.states.map((p) => statuses.get(p) as IssueStatus).filter((p) => p !== undefined)
}
$: statuses = getStatuses($statusStore, value)
$: statuses = getStatuses($statusStore, space)
function getSelectedStatus (
statuses: WithLookup<IssueStatus>[] | undefined,
@@ -127,7 +134,11 @@
on:click={handleStatusEditorOpened}
>
<div class="flex-center flex-no-shrink square-4">
{#if selectedStatus}<IssueStatusIcon value={selectedStatus} size={kind === 'list' ? 'small' : 'medium'} />{/if}
{#if selectedStatus}<IssueStatusIcon
value={selectedStatus}
size={kind === 'list' ? 'small' : 'medium'}
space={value.space}
/>{/if}
</div>
{#if selectedStatusLabel}
<span
@@ -152,7 +163,7 @@
>
<svelte:fragment slot="icon">
{#if selectedStatus}
<IssueStatusIcon value={selectedStatus} size={iconSize} />
<IssueStatusIcon value={selectedStatus} size={iconSize} space={value.space} />
{/if}
</svelte:fragment>
<svelte:fragment slot="content">
@@ -13,13 +13,14 @@
// limitations under the License.
-->
<script lang="ts">
import { IssueStatus } from '@hcengineering/tracker'
import { IssueStatus, Project } from '@hcengineering/tracker'
import IssueStatusIcon from './IssueStatusIcon.svelte'
import { createQuery } from '@hcengineering/presentation'
import core, { IdMap, Ref, StatusCategory, toIdMap } from '@hcengineering/core'
import { statusStore } from '@hcengineering/view-resources'
export let value: Ref<IssueStatus>[]
export let space: Ref<Project> | undefined
let statuses: IssueStatus[] = []
@@ -31,7 +32,6 @@
function sort (value: IssueStatus[], categories: IdMap<StatusCategory>): IssueStatus[] {
return value.sort((a, b) => {
if (a.category === b.category) return a.rank.localeCompare(b.rank)
if (a.category === undefined) return -1
if (b.category === undefined) return 1
const aCat = categories.get(a.category)
@@ -42,14 +42,14 @@
})
}
$: statuses = sort(value.map((p) => $statusStore.getIdMap().get(p)) as IssueStatus[], categories)
$: statuses = sort(value.map((p) => $statusStore.get(p)) as IssueStatus[], categories)
</script>
<div class="flex-presenter flex-gap-1-5">
{#each statuses as value, i}
{#if value && i < 5}
<div>
<IssueStatusIcon {value} size={'small'} />
<IssueStatusIcon {space} {value} size={'small'} />
</div>
{/if}
{/each}
@@ -13,10 +13,12 @@
// limitations under the License.
-->
<script lang="ts">
import { IssueStatus } from '@hcengineering/tracker'
import { Ref } from '@hcengineering/core'
import { IssueStatus, Project } from '@hcengineering/tracker'
import IssueStatusIcon from './IssueStatusIcon.svelte'
export let value: IssueStatus | undefined
export let space: Ref<Project>
export let size: 'small' | 'medium' = 'small'
export let kind: 'list-header' | undefined = undefined
export let colorInherit: boolean = false
@@ -27,7 +29,7 @@
{#if value}
<div class="flex-presenter cursor-default" style:color={'inherit'}>
{#if !inline}
<IssueStatusIcon {value} {size} on:accent-color />
<IssueStatusIcon {value} {size} {space} on:accent-color />
{/if}
<span
class="overflow-label"
@@ -14,10 +14,12 @@
-->
<script lang="ts">
import { Ref, Status, StatusValue } from '@hcengineering/core'
import { Project } from '@hcengineering/tracker'
import { statusStore } from '@hcengineering/view-resources'
import StatusPresenter from './StatusPresenter.svelte'
export let value: Ref<Status> | StatusValue | undefined
export let space: Ref<Project>
export let size: 'small' | 'medium' = 'medium'
export let kind: 'list-header' | undefined = undefined
export let colorInherit: boolean = false
@@ -27,5 +29,5 @@
</script>
{#if value}
<StatusPresenter value={statusValue} {size} {kind} {colorInherit} {accent} on:accent-color />
<StatusPresenter {space} value={statusValue} {size} {kind} {colorInherit} {accent} on:accent-color />
{/if}
@@ -118,7 +118,7 @@
subIssuesQeury.unsubscribe()
}
$: parentStatus = parentIssue ? $statusStore.getIdMap().get(parentIssue.status) : undefined
$: parentStatus = parentIssue ? $statusStore.get(parentIssue.status) : undefined
</script>
{#if parentIssue}
@@ -132,7 +132,7 @@
>
{#if parentStatus}
<div class="pr-2">
<IssueStatusIcon value={parentStatus} size="small" />
<IssueStatusIcon space={parentIssue.space} value={parentStatus} size="small" />
</div>
{/if}
{#if issue.$lookup?.space}
@@ -13,7 +13,7 @@
// limitations under the License.
-->
<script lang="ts">
import { Ref, SortingOrder, WithLookup } from '@hcengineering/core'
import { Ref, SortingOrder, Status, WithLookup } from '@hcengineering/core'
import { createQuery } from '@hcengineering/presentation'
import { Issue, Project } from '@hcengineering/tracker'
import {
@@ -79,13 +79,17 @@
}
$: if (subIssues) {
const doneStatuses = $statusStore
.getDocs()
.filter(
(s) =>
s.category === tracker.issueStatusCategory.Completed || s.category === tracker.issueStatusCategory.Canceled
)
.map((p) => p._id)
const doneStatuses = project
? project.states
.map((p) => $statusStore.get(p))
.filter((p) => p !== undefined)
.filter(
(s) =>
s?.category === tracker.issueStatusCategory.Completed ||
s?.category === tracker.issueStatusCategory.Canceled
)
.map((p) => (p as Status)._id)
: []
countComplete = subIssues.filter((si) => doneStatuses.includes(si.status)).length
}
$: hasSubIssues = (subIssues?.length ?? 0) > 0
@@ -19,7 +19,7 @@
import { statusStore } from '@hcengineering/view-resources'
import tracker from '../../../plugin'
import { ComponentToUpdate, StatusToUpdate, findTargetStatus } from '../../../utils'
import { ComponentToUpdate, StatusToUpdate } from '../../../utils'
import ComponentPresenter from '../../components/ComponentPresenter.svelte'
import ComponentRefPresenter from '../../components/ComponentRefPresenter.svelte'
import StatusRefPresenter from '../StatusRefPresenter.svelte'
@@ -27,6 +27,7 @@
import ComponentReplacementPopup from './ComponentReplacementPopup.svelte'
export let targetProject: Project
export let currentProject: Ref<Project>
export let issues: Issue[]
export let statuses: IssueStatus[] = []
export let components: Component[] = []
@@ -36,15 +37,17 @@
$: if (targetProject !== undefined) {
for (const i of issues) {
const status = statusToUpdate[i.status]
if (status !== undefined && !status.create) {
if ($statusStore.get(status.ref)?.space !== targetProject._id) {
statusToUpdate[i.status] = undefined
if (status?.create !== true) {
if (!targetProject.states.includes(i.status)) {
if (status?.ref === undefined) {
statusToUpdate[i.status] = { ref: targetProject.defaultIssueStatus }
} else if (!targetProject.states.includes(status.ref)) {
statusToUpdate[i.status] = { ref: targetProject.defaultIssueStatus }
}
} else {
delete statusToUpdate[i.status]
}
}
if (statusToUpdate[i.status] === undefined) {
const targetStatus = findTargetStatus($statusStore, i.status, targetProject._id, true)
statusToUpdate[i.status] = { ref: targetStatus ?? targetProject.defaultIssueStatus }
}
if (i.component !== undefined && i.component !== null) {
const cur = components.find((it) => it._id === i.component)
@@ -95,7 +98,7 @@
{#each Object.keys(statusToUpdate) as status}
{@const newStatus = statusToUpdate[status]}
<div class="flex-between min-h-11">
<StatusRefPresenter value={getStatusRef(status)} kind={'list-header'} />
<StatusRefPresenter value={getStatusRef(status)} space={currentProject} kind={'list-header'} />
<IconArrowRight size={'small'} fill={'var(--theme-halfcontent-color)'} />
</div>
<div class="flex-row-center min-h-11">
@@ -108,6 +111,7 @@
StatusReplacementPopup,
{
statuses,
space: targetProject._id,
original: $statusStore.get(getStatusRef(status)),
selected: getStatusRef(newStatus.ref)
},
@@ -123,7 +127,7 @@
}}
>
<span slot="content" class="flex-row-center pointer-events-none">
<StatusRefPresenter value={getStatusRef(newStatus.ref)} />
<StatusRefPresenter space={targetProject._id} value={getStatusRef(newStatus.ref)} />
</span>
</Button>
</div>
@@ -24,6 +24,7 @@
export let issue: Issue
export let targetProject: Project
export let currentProject: Ref<Project>
export let issueToUpdate: Map<Ref<Issue>, IssueToUpdate> = new Map()
export let statuses: IssueStatus[]
@@ -33,7 +34,7 @@
<Grid rowGap={0.25} topGap>
<div class="flex-between min-h-11">
<StatusRefPresenter value={issue.status} size={'small'} />
<StatusRefPresenter space={currentProject} value={issue.status} size={'small'} />
<IconArrowRight size={'small'} fill={'var(--theme-halfcontent-color)'} />
</div>
<div class="flex-row-center min-h-11">
@@ -62,7 +63,7 @@
}}
>
<span slot="content" class="flex-row-center pointer-events-none">
<StatusRefPresenter value={replace} />
<StatusRefPresenter space={targetProject._id} value={replace} />
</span>
</Button>
</div>
@@ -15,7 +15,7 @@
<script lang="ts">
import { createEventDispatcher } from 'svelte'
import { IssueStatus } from '@hcengineering/tracker'
import { IssueStatus, Project } from '@hcengineering/tracker'
import tracker from '../../../plugin'
import { StatusPresenter } from '@hcengineering/view-resources'
import { Ref } from '@hcengineering/core'
@@ -26,6 +26,7 @@
export let statuses: IssueStatus[] | undefined
export let original: IssueStatus | undefined
export let selected: Ref<IssueStatus>
export let space: Ref<Project>
const dispatch = createEventDispatcher()
</script>
@@ -50,7 +51,7 @@
<div class="flex-between w-full">
<div class="flex-row-center">
<div class="pr-2">
<IssueStatusIcon value={status} size={'small'} />
<IssueStatusIcon value={status} size={'small'} {space} />
</div>
<StatusPresenter value={status} />
</div>
@@ -83,7 +84,7 @@
<div class="flex-between w-full">
<div class="flex-row-center">
<div class="pr-2">
<IssueStatusIcon value={original} size={'small'} />
<IssueStatusIcon value={original} size={'small'} {space} />
</div>
<StatusPresenter value={original} />
</div>
@@ -23,7 +23,7 @@
export let issue: Issue
export let size: 'small' | 'medium' | 'large' = 'small'
$: status = $statusStore.getIdMap().get(issue.status)
$: status = $statusStore.get(issue.status)
$: huge = size === 'medium' || size === 'large'
$: text = project ? `${getIssueId(project, issue)} ${issue.title}` : issue.title
</script>
@@ -31,7 +31,7 @@
<div class="flex-row-center">
{#if status}
<div class="icon mr-2">
<IssueStatusIcon value={status} {size} />
<IssueStatusIcon value={status} {size} space={issue.space} />
</div>
{/if}
<span class="label" class:text-base={huge}>
@@ -70,8 +70,7 @@
}
$: if (subIssues) {
const doneStatuses = $statusStore
.getDocs()
const doneStatuses = Array.from($statusStore.values())
.filter((s) => s.category === tracker.issueStatusCategory.Completed)
.map((p) => p._id)
countComplete = subIssues.filter((si) => doneStatuses.includes(si.status)).length
@@ -30,13 +30,13 @@
$: noParents = docs?.filter((it) => !ids.has(it.attachedTo as Ref<Issue>))
$: rootNoBacklogIssues = noParents?.filter(
(it) => $statusStore.getIdMap().get(it.status)?.category !== tracker.issueStatusCategory.Backlog
(it) => $statusStore.get(it.status)?.category !== tracker.issueStatusCategory.Backlog
)
$: totalEstimation = floorFractionDigits(
(rootNoBacklogIssues ?? [{ estimation: 0, childInfo: [] } as unknown as Issue])
.map((it) => {
const cat = $statusStore.getIdMap().get(it.status)?.category
const cat = $statusStore.get(it.status)?.category
let retEst = it.estimation
if (it.childInfo?.length > 0) {
@@ -118,6 +118,7 @@
<FilterBar
_class={tracker.class.Milestone}
query={searchQuery}
{space}
{viewOptions}
on:change={(e) => (resultQuery = e.detail)}
/>
@@ -28,7 +28,7 @@
import { Asset } from '@hcengineering/platform'
import presentation, { Card, createQuery, getClient } from '@hcengineering/presentation'
import { StyledTextBox } from '@hcengineering/text-editor'
import { IssueStatus, Project, TimeReportDayType, createStatuses } from '@hcengineering/tracker'
import { Project, TimeReportDayType, createStatuses } from '@hcengineering/tracker'
import {
Button,
EditBox,
@@ -54,11 +54,9 @@
export let descriptionPlaceholder: string = ''
export let statusFactory: (
client: TxOperations | ApplyOperations,
spaceId: Status['space'],
statusClass: Status['_class'],
categoryOfAttribute: Status['ofAttribute'],
defaultStatusId: Status['_id']
) => Promise<void> = createStatuses
categoryOfAttribute: Status['ofAttribute']
) => Promise<Ref<Status>[]> = createStatuses
const client = getClient()
const hierarchy = client.getHierarchy()
@@ -92,8 +90,6 @@
let identifier: string = project?.identifier ?? 'TSK'
const defaultStatusId: Ref<IssueStatus> = generateId()
function getProjectData () {
return {
name,
@@ -103,7 +99,6 @@
archived: false,
identifier: identifier.toUpperCase(),
sequence: 0,
defaultIssueStatus: defaultStatusId,
defaultAssignee: defaultAssignee ?? undefined,
icon,
color,
@@ -116,7 +111,7 @@
return
}
const { sequence, defaultIssueStatus, ...projectData } = getProjectData()
const { sequence, ...projectData } = getProjectData()
const update: DocumentUpdate<Project> = {}
if (projectData.name !== project?.name) {
update.name = projectData.name
@@ -168,10 +163,15 @@
const ops = client
.apply(projectId)
.notMatch(tracker.class.Project, { identifier: projectData.identifier.toUpperCase() })
const statuses = await statusFactory(ops, tracker.class.IssueStatus, tracker.attribute.IssueStatus)
isSaving = true
await ops.createDoc(tracker.class.Project, core.space.Space, projectData, projectId)
await statusFactory(ops, projectId, tracker.class.IssueStatus, tracker.attribute.IssueStatus, defaultStatusId)
await ops.createDoc(
tracker.class.Project,
core.space.Space,
{ ...projectData, states: statuses, defaultIssueStatus: statuses[0] },
projectId
)
const succeeded = await ops.commit()
isSaving = false
@@ -86,6 +86,7 @@
<slot name="afterHeader" />
<FilterBar
_class={tracker.class.IssueTemplate}
{space}
{viewOptions}
query={searchQuery}
on:change={(e) => (resultQuery = e.detail)}
@@ -1,12 +1,13 @@
<script lang="ts">
import { Ref } from '@hcengineering/core'
import presentation, { createQuery, getClient } from '@hcengineering/presentation'
import { getStates } from '@hcengineering/task'
import { Issue, IssueStatus, Project } from '@hcengineering/tracker'
import { Button, Label, SelectPopup, eventToHTMLElement, showPopup } from '@hcengineering/ui'
import presentation, { getClient } from '@hcengineering/presentation'
import tracker from '../../plugin'
import { createEventDispatcher } from 'svelte'
import IssueStatusIcon from '../issues/IssueStatusIcon.svelte'
import { StatusPresenter, statusStore } from '@hcengineering/view-resources'
import { createEventDispatcher } from 'svelte'
import tracker from '../../plugin'
import IssueStatusIcon from '../issues/IssueStatusIcon.svelte'
export let projectId: Ref<Project>
export let issues: Issue[]
@@ -14,13 +15,18 @@
let processing = false
let _space: Project | undefined = undefined
const query = createQuery()
$: query.query(tracker.class.Project, { space: projectId }, (result) => {
_space = result[0]
})
const client = getClient()
let newStatus: IssueStatus =
$statusStore
.getDocs()
.find((s) => s._id !== status._id && s.category === status.category && s.space === projectId) ??
$statusStore.getDocs().find((s) => s._id !== status._id && s.space === projectId) ??
$: newStatus =
statuses.find((s) => s._id !== status._id && s.category === status.category && s.space === projectId) ??
statuses.find((s) => s._id !== status._id && s.space === projectId) ??
status
async function remove () {
@@ -32,12 +38,14 @@
})
})
)
await client.remove(status)
if (_space) {
await client.update(_space, { $pull: { states: status._id } })
}
processing = false
dispatch('close')
}
$: statuses = $statusStore.filter((it) => it.space === projectId && it._id !== status._id)
$: statuses = getStates(_space, $statusStore).filter((p) => p._id !== status._id)
$: statusesInfo = statuses?.map((s) => {
return {
id: s._id,
@@ -55,7 +63,7 @@
{ value: statusesInfo, placeholder: tracker.string.SetStatus, searchable: true },
eventToHTMLElement(event),
(val) => {
newStatus = $statusStore.getIdMap().get(val) ?? newStatus
newStatus = $statusStore.get(val) ?? newStatus
}
)
}
@@ -74,9 +82,7 @@
<Button kind={'link'} justify={'left'} width={'10rem'} on:click={handleStatusEditorOpened}>
<span slot="content" class="flex-row-center pointer-events-none">
{#if newStatus}
<IssueStatusIcon value={newStatus} size="inline" />
{/if}
{#if newStatus}
<IssueStatusIcon value={newStatus} space={projectId} size="inline" />
<span class="overflow-label disabled ml-1">
{newStatus.name}
</span>
@@ -13,13 +13,15 @@
// limitations under the License.
-->
<script lang="ts">
import { IssueStatus } from '@hcengineering/tracker'
import { Ref } from '@hcengineering/core'
import { IssueStatus, Project } from '@hcengineering/tracker'
import { Icon, IconCircles, IconDelete, IconEdit, Label, tooltip } from '@hcengineering/ui'
import { createEventDispatcher } from 'svelte'
import tracker from '../../plugin'
import IssueStatusIcon from '../issues/IssueStatusIcon.svelte'
export let value: IssueStatus
export let space: Ref<Project>
export let isDefault = false
export let isSingle = true
@@ -36,7 +38,7 @@
<IconCircles size={'small'} />
</div>
<div class="flex-no-shrink">
<IssueStatusIcon {value} size="small" />
<IssueStatusIcon {value} size="small" {space} />
</div>
<span class="caption-color ml-2">{value.name}</span>
{#if value.description}
@@ -14,16 +14,17 @@
-->
<script lang="ts">
import core, { Class, Data, Ref, SortingOrder, StatusCategory } from '@hcengineering/core'
import { createQuery, getClient, MessageBox, Card } from '@hcengineering/presentation'
import { calcRank, IssueStatus, Project } from '@hcengineering/tracker'
import { Button, closeTooltip, ExpandCollapse, IconAdd, Label, Loading, Scroller, showPopup } from '@hcengineering/ui'
import { Card, MessageBox, createQuery, getClient } from '@hcengineering/presentation'
import { createState, getStates } from '@hcengineering/task'
import { IssueStatus, Project } from '@hcengineering/tracker'
import { Button, ExpandCollapse, IconAdd, Label, Loading, Scroller, closeTooltip, showPopup } from '@hcengineering/ui'
import { statusStore } from '@hcengineering/view-resources'
import { createEventDispatcher } from 'svelte'
import { flip } from 'svelte/animate'
import tracker from '../../plugin'
import RemoveStatus from './RemoveStatus.svelte'
import StatusEditor from './StatusEditor.svelte'
import StatusPresenter from './StatusPresenter.svelte'
import RemoveStatus from './RemoveStatus.svelte'
import { statusStore } from '@hcengineering/view-resources'
export let projectId: Ref<Project>
export let projectClass: Ref<Class<Project>>
@@ -56,18 +57,16 @@
}
async function addStatus () {
if (editingStatus?.name && editingStatus?.category) {
const [prevStatus, nextStatus] = getSiblingsForNewStatus(editingStatus.category)
if (editingStatus?.name && editingStatus?.category && project) {
isSaving = true
await client.createDoc(tracker.class.IssueStatus, projectId, {
const id = await createState(client, tracker.class.IssueStatus, {
ofAttribute: tracker.attribute.IssueStatus,
name: editingStatus.name,
description: editingStatus.description,
color: editingStatus.color,
category: editingStatus.category,
rank: calcRank(prevStatus, nextStatus)
category: editingStatus.category
})
await client.update(project, { $push: { states: id } })
isSaving = false
}
@@ -77,16 +76,13 @@
async function editStatus () {
if (statusCategories && editingStatus?.name && editingStatus?.category && '_id' in editingStatus) {
const statusId = '_id' in editingStatus ? editingStatus._id : undefined
const status = statusId && $statusStore.getIdMap().get(statusId)
const status = statusId && $statusStore.get(statusId)
if (!status) {
return
}
const updates: Partial<Data<IssueStatus>> = {}
if (status.name !== editingStatus.name) {
updates.name = editingStatus.name
}
if (status.description !== editingStatus.description) {
updates.description = editingStatus.description
}
@@ -101,8 +97,29 @@
updates.color = editingStatus.color
}
}
if (Object.keys(updates).length > 0) {
if (status.name !== editingStatus.name) {
isSaving = true
const newId = await createState(client, tracker.class.IssueStatus, {
ofAttribute: tracker.attribute.IssueStatus,
name: editingStatus.name,
description: editingStatus.description,
color: editingStatus.color,
category: editingStatus.category
})
projectStatuses = projectStatuses.map((s) => (s._id === statusId ? { ...s, _id: newId } : s))
if (project) {
const issues = await client.findAll(tracker.class.Issue, { status: status._id, space: projectId })
await Promise.all(
issues.map(async (p) => {
await client.update(p, {
status: newId
})
})
)
await client.update(project, { states: projectStatuses.map((s) => s._id) })
}
isSaving = false
} else if (Object.keys(updates).length > 0) {
isSaving = true
await client.update(status, updates)
isSaving = false
@@ -136,7 +153,7 @@
async (result) => {
if (result && project) {
isSaving = true
await client.removeDoc(status._class, status.space, status._id)
await client.update(project, { $pull: { states: status._id } })
if (project.defaultIssueStatus === status._id) {
const newDefaultStatus = projectStatuses.find(
@@ -166,23 +183,22 @@
}
function handleDragOver (ev: DragEvent, status: IssueStatus) {
hoveringStatus = status
ev.preventDefault()
if (status.category === draggingStatus?.category) {
hoveringStatus = status
ev.preventDefault()
}
}
async function handleDrop (toItem: IssueStatus) {
if (draggingStatus != null && draggingStatus?._id !== toItem._id) {
const fromIndex = getStatusIndex(draggingStatus)
const toIndex = getStatusIndex(toItem)
const [prev, next] = [
projectStatuses[fromIndex < toIndex ? toIndex : toIndex - 1],
projectStatuses[fromIndex < toIndex ? toIndex + 1 : toIndex]
]
const item = projectStatuses.splice(fromIndex, 1)
isSaving = true
let newCategory = {}
if (draggingStatus?.category !== toItem.category) newCategory = { category: toItem.category }
await client.update(draggingStatus, { rank: calcRank(prev, next), ...newCategory })
projectStatuses = [...projectStatuses.slice(0, toIndex), ...item, ...projectStatuses.slice(toIndex)]
if (project) {
await client.update(project, { states: projectStatuses.map((s) => s._id) })
}
isSaving = false
}
@@ -193,33 +209,6 @@
return projectStatuses.findIndex(({ _id }) => _id === status._id) ?? -1
}
function getSiblingsForNewStatus (
categoryId: StatusCategory['_id']
): readonly [] | readonly [IssueStatus | undefined, IssueStatus | undefined] {
const categoryStatuses = projectStatuses.filter((s) => s.category === categoryId)
if (categoryStatuses.length > 0) {
const prev = categoryStatuses[categoryStatuses.length - 1]
const next = projectStatuses[getStatusIndex(prev) + 1]
return [prev, next]
}
const category = statusCategories?.find(({ _id }) => _id === categoryId)
if (!category) {
return []
}
const { order } = category
const prev = projectStatuses.findLast(
(s) => s.$lookup?.category?.order !== undefined && s.$lookup.category.order < order
)
const next = projectStatuses.find(
(s) => s.$lookup?.category?.order !== undefined && s.$lookup.category.order > order
)
return [prev, next]
}
function resetDrag () {
draggingStatus = null
hoveringStatus = null
@@ -227,7 +216,7 @@
$: projectQuery.query(projectClass, { _id: projectId }, (result) => ([project] = result), { limit: 1 })
$: updateStatusCategories()
$: projectStatuses = $statusStore.getDocs().filter((status) => status.space === projectId)
$: projectStatuses = getStates(project, $statusStore)
</script>
<Card
@@ -266,15 +255,17 @@
/>
</div>
<div class="flex-col">
{#each statuses as status, _ (status._id)}
{#each statuses as status (status._id)}
<div
class="row"
class:is-dragged-over-up={draggingStatus &&
status._id === hoveringStatus?._id &&
status.rank < draggingStatus.rank}
draggingStatus.category === status.category &&
getStatusIndex(status) < getStatusIndex(draggingStatus)}
class:is-dragged-over-down={draggingStatus &&
status._id === hoveringStatus?._id &&
status.rank > draggingStatus.rank}
draggingStatus.category === status.category &&
getStatusIndex(status) > getStatusIndex(draggingStatus)}
draggable={!isSingle}
animate:flip={{ duration: 200 }}
on:dragstart={(ev) => handleDragStart(ev, status)}
@@ -292,6 +283,7 @@
/>
{:else}
<StatusPresenter
space={projectId}
value={status}
isDefault={status._id === project.defaultIssueStatus}
{isSingle}
+56 -30
View File
@@ -23,13 +23,12 @@ import core, {
Doc,
DocumentQuery,
DocumentUpdate,
IdMap,
Ref,
SortingOrder,
Space,
Status,
StatusCategory,
StatusManager,
StatusValue,
toIdMap,
TxCollectionCUD,
TxOperations,
@@ -62,9 +61,16 @@ import {
PaletteColorIndexes
} from '@hcengineering/ui'
import { KeyFilter, ViewletDescriptor } from '@hcengineering/view'
import { CategoryQuery, groupBy, ListSelectionProvider, SelectDirection } from '@hcengineering/view-resources'
import {
CategoryQuery,
groupBy,
ListSelectionProvider,
SelectDirection,
statusStore
} from '@hcengineering/view-resources'
import { get } from 'svelte/store'
import tracker from './plugin'
import { defaultPriorities, defaultMilestoneStatuses } from './types'
import { defaultMilestoneStatuses, defaultPriorities } from './types'
export * from './types'
@@ -281,28 +287,51 @@ const listIssueKanbanStatusOrder = [
] as const
export async function issueStatusSort (
value: StatusValue[],
client: TxOperations,
value: Array<Ref<IssueStatus>>,
space: Ref<Project> | undefined,
viewletDescriptorId?: Ref<ViewletDescriptor>
): Promise<StatusValue[]> {
): Promise<Array<Ref<IssueStatus>>> {
let _space: Project | undefined
if (space !== undefined) {
_space = await client.findOne(tracker.class.Project, { _id: space })
}
const statuses = get(statusStore)
// TODO: How we track category updates.
if (viewletDescriptorId === tracker.viewlet.Kanban) {
value.sort((a, b) => {
const aVal = statuses.get(a) as IssueStatus
const bVal = statuses.get(b) as IssueStatus
const res =
listIssueKanbanStatusOrder.indexOf(a.values[0].category as Ref<StatusCategory>) -
listIssueKanbanStatusOrder.indexOf(b.values[0].category as Ref<StatusCategory>)
listIssueKanbanStatusOrder.indexOf(aVal?.category as Ref<StatusCategory>) -
listIssueKanbanStatusOrder.indexOf(bVal?.category as Ref<StatusCategory>)
if (res === 0) {
return a.values[0].getRank().localeCompare(b.values[0].getRank())
if (_space != null) {
const aIndex = _space.states.findIndex((s) => s === a)
const bIndex = _space.states.findIndex((s) => s === b)
return aIndex - bIndex
} else {
return aVal.name.localeCompare(bVal.name)
}
}
return res
})
} else {
value.sort((a, b) => {
const aVal = statuses.get(a) as IssueStatus
const bVal = statuses.get(b) as IssueStatus
const res =
listIssueStatusOrder.indexOf(a.values[0].category as Ref<StatusCategory>) -
listIssueStatusOrder.indexOf(b.values[0].category as Ref<StatusCategory>)
listIssueStatusOrder.indexOf(aVal?.category as Ref<StatusCategory>) -
listIssueStatusOrder.indexOf(bVal?.category as Ref<StatusCategory>)
if (res === 0) {
return a.values[0].getRank().localeCompare(b.values[0].getRank())
if (_space != null) {
const aIndex = _space.states.findIndex((s) => s === a)
const bIndex = _space.states.findIndex((s) => s === b)
return aIndex - bIndex
} else {
return aVal.name.localeCompare(bVal.name)
}
}
return res
})
@@ -310,7 +339,7 @@ export async function issueStatusSort (
return value
}
export async function issuePrioritySort (value: IssuePriority[]): Promise<IssuePriority[]> {
export async function issuePrioritySort (client: TxOperations, value: IssuePriority[]): Promise<IssuePriority[]> {
value.sort((a, b) => {
const i1 = defaultPriorities.indexOf(a)
const i2 = defaultPriorities.indexOf(b)
@@ -320,7 +349,10 @@ export async function issuePrioritySort (value: IssuePriority[]): Promise<IssueP
return value
}
export async function milestoneSort (value: Array<Ref<Milestone>>): Promise<Array<Ref<Milestone>>> {
export async function milestoneSort (
client: TxOperations,
value: Array<Ref<Milestone>>
): Promise<Array<Ref<Milestone>>> {
return await new Promise((resolve) => {
const query = createQuery(true)
query.query(tracker.class.Milestone, { _id: { $in: value } }, (res) => {
@@ -610,26 +642,20 @@ export async function collectIssues (client: TxOperations, docs: Doc[]): Promise
* @public
*/
export function findTargetStatus (
mgr: StatusManager,
status: Ref<Status>,
targetProject: Ref<Project>,
targetProject: Project,
statusStore: IdMap<Status>,
useCategory = false
): Ref<Status> | undefined {
const s = mgr.get(status)
let targetStatus = mgr
.filter(
(it) =>
it.space === targetProject &&
it.ofAttribute === s?.ofAttribute &&
(it.name ?? '').trim().toLowerCase() === (s?.name ?? '').trim().toLowerCase()
)
.shift()
if (targetStatus === undefined && useCategory) {
targetStatus = mgr
.filter((it) => it.space === targetProject && it.ofAttribute === s?.ofAttribute && s?.category === it.category)
.shift()
if (targetProject.states.includes(status)) return status
if (useCategory) {
const currentCategroy = statusStore.get(status)?.category
for (const status of targetProject.states) {
const st = statusStore.get(status)
if (st?.category === currentCategroy) return st?._id
}
}
return targetStatus?._id
}
/**
+12 -18
View File
@@ -13,8 +13,8 @@
// limitations under the License.
//
import core, { ApplyOperations, SortingOrder, Status, TxOperations, generateId } from '@hcengineering/core'
import { genRanks } from '@hcengineering/task'
import core, { ApplyOperations, Ref, SortingOrder, Status, TxOperations } from '@hcengineering/core'
import { createState } from '@hcengineering/task'
export { calcRank, genRanks } from '@hcengineering/task'
/**
@@ -24,32 +24,26 @@ export { calcRank, genRanks } from '@hcengineering/task'
*/
export async function createStatuses (
client: TxOperations | ApplyOperations,
spaceId: Status['space'],
statusClass: Status['_class'],
categoryOfAttribute: Status['ofAttribute'],
defaultStatusId: Status['_id']
): Promise<void> {
categoryOfAttribute: Status['ofAttribute']
): Promise<Ref<Status>[]> {
const categories = await client.findAll(
core.class.StatusCategory,
{ ofAttribute: categoryOfAttribute },
{ sort: { order: SortingOrder.Ascending } }
)
const ranks = [...genRanks(categories.length)]
for (const [i, category] of categories.entries()) {
const statusId = i === 0 ? defaultStatusId : generateId<Status>()
const rank = ranks[i]
const states: Ref<Status>[] = []
await client.createDoc(
statusClass,
spaceId,
{
for (const category of categories) {
states.push(
await createState(client, statusClass, {
ofAttribute: categoryOfAttribute,
name: category.defaultStatusName,
category: category._id,
rank
},
statusId
category: category._id
})
)
}
return states
}
@@ -194,7 +194,7 @@
if (attribute.attribute?.type._class === core.class.EnumOf) {
return { ...attribute.props, type: attribute.attribute.type }
}
return attribute.props
return { ...attribute.props, space: object.space }
}
function getValue (attribute: AttributeModel, object: Doc): any {
if (attribute.castRequest) {
@@ -13,7 +13,7 @@
// limitations under the License.
-->
<script lang="ts">
import { Class, Doc, DocumentQuery, Ref, getCurrentAccount } from '@hcengineering/core'
import { Class, Doc, DocumentQuery, Ref, Space, getCurrentAccount } from '@hcengineering/core'
import { getResource } from '@hcengineering/platform'
import { getClient } from '@hcengineering/presentation'
import { Button, IconAdd, eventToHTMLElement, getCurrentLocation, showPopup } from '@hcengineering/ui'
@@ -28,6 +28,7 @@
import { getViewOptions, viewOptionStore } from '../../viewOptions'
export let _class: Ref<Class<Doc>>
export let space: Ref<Space> | undefined
export let query: DocumentQuery<Doc>
export let viewOptions: ViewOptions | undefined = undefined
@@ -49,6 +50,7 @@
{
_class,
target,
space,
index: ++maxIndex,
onChange
},
@@ -172,6 +174,7 @@
<div class="filters">
{#each $filterStore as filter, i}
<FilterSection
{space}
{filter}
on:change={() => {
makeQuery(query, $filterStore)
@@ -13,7 +13,7 @@
// limitations under the License.
-->
<script lang="ts">
import { Class, Doc, Ref, RefTo } from '@hcengineering/core'
import { Class, Doc, Ref, RefTo, Space } from '@hcengineering/core'
import { translate } from '@hcengineering/platform'
import { getAttributePresenterClass, getClient } from '@hcengineering/presentation'
import type { State } from '@hcengineering/task'
@@ -34,6 +34,7 @@
import ModeSelector from './ModeSelector.svelte'
export let filter: Filter
export let space: Ref<Space> | undefined
let currentFilter = filter.nested ? filter.nested : filter
$: currentFilter = filter.nested ? filter.nested : filter
@@ -158,6 +159,7 @@
{
_class: currentFilter.key._class,
filter: currentFilter,
space,
onChange
},
eventToHTMLElement(e)
@@ -165,7 +167,10 @@
}}
>
{#if valueComponent}
<Component is={valueComponent} props={{ value: currentFilter.value, onChange, filter: currentFilter }} />
<Component
is={valueComponent}
props={{ value: currentFilter.value, onChange, filter: currentFilter, space }}
/>
{:else}
<span>{countLabel}</span>
{/if}
@@ -23,7 +23,7 @@
import view from '../../plugin'
export let _class: Ref<Class<Doc>>
export let space: Ref<Space> | undefined = undefined
export let space: Ref<Space> | undefined
export let target: HTMLElement
export let filter: Filter | undefined
export let index: number
@@ -0,0 +1,204 @@
<!--
// Copyright © 2022 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 core, { Doc, FindResult, IdMap, Ref, RefTo, Space, Status } from '@hcengineering/core'
import { translate } from '@hcengineering/platform'
import presentation, { createQuery, getClient } from '@hcengineering/presentation'
import task, { SpaceWithStates } from '@hcengineering/task'
import ui, {
addNotification,
deviceOptionsStore,
EditWithIcon,
Icon,
IconCheck,
IconSearch,
Label,
Loading,
resizeObserver,
themeStore
} from '@hcengineering/ui'
import { Filter } from '@hcengineering/view'
import { createEventDispatcher } from 'svelte'
import { buildConfigLookup, getPresenter } from '../../utils'
import view from '../../plugin'
import { FILTER_DEBOUNCE_MS, FilterRemovedNotification, sortFilterValues, statusStore } from '../..'
export let filter: Filter
export let space: Ref<Space> | undefined = undefined
export let onChange: (e: Filter) => void
const client = getClient()
const hierarchy = client.getHierarchy()
const tkey = '$lookup.' + filter.key.key
const key = { key: tkey }
const lookup = buildConfigLookup(hierarchy, filter.key._class, [tkey])
const promise = getPresenter(client, filter.key._class, key, key, lookup)
filter.modes = filter.modes === undefined ? [view.filter.FilterObjectIn, view.filter.FilterObjectNin] : filter.modes
filter.mode = filter.mode === undefined ? filter.modes[0] : filter.mode
let values: (Status | undefined | null)[] = []
let objectsPromise: Promise<FindResult<Status>> | undefined
const targets = new Set<any>()
$: targetClass = (filter.key.attribute.type as RefTo<Status>).to
$: clazz = hierarchy.getClass(targetClass)
let _space: SpaceWithStates | undefined = undefined
const query = createQuery()
if (space !== undefined) {
query.query(task.class.SpaceWithStates, { _id: space as Ref<SpaceWithStates> }, (res) => {
_space = res[0]
})
}
let filterUpdateTimeout: number | undefined
async function getValues (search: string, statusStore: IdMap<Status>): Promise<void> {
if (objectsPromise) {
await objectsPromise
}
targets.clear()
for (const object of filter.value) {
targets.add(object)
}
if (space !== undefined) {
const _space = await client.findOne(core.class.Space, { _id: space })
if (_space) {
values = (_space as any)[filter.key.key]
.map((p: Ref<Status>) => statusStore.get(p))
.filter((p: Status) => p !== undefined)
for (const value of values) {
targets.add(value?._id)
}
if (search !== '') {
values = values.filter((p) => p?.name.includes(search))
}
}
} else {
values = []
for (const status of statusStore.values()) {
if (hierarchy.isDerived(status._class, targetClass)) {
values.push(status)
targets.add(status._id)
}
}
}
if (targets.has(undefined)) {
values.unshift(undefined)
}
if (values.length !== targets.size) {
const oldSize = filter.value.length
filter.value = filter.value.filter((p) => targets.has(p))
const removed = oldSize - (filter.value.length ?? 0)
if (removed > 0) {
onChange(filter)
addNotification(
await translate(view.string.FilterUpdated, {}, $themeStore.language),
filter.key.label,
FilterRemovedNotification,
{
description: await translate(view.string.FilterRemoved, { count: removed }, $themeStore.language)
}
)
}
}
values = sortFilterValues(values, (v) => isSelected(v, filter.value))
objectsPromise = undefined
}
function isSelected (value: Doc | undefined | null, values: any[]): boolean {
return values.includes(value?._id ?? value)
}
function handleFilterToggle (value: any): void {
if (isSelected(value, filter.value)) {
filter.value = filter.value.filter((p) => (value ? p !== value._id : p != null))
} else {
if (value) {
filter.value = [...filter.value, value._id]
} else {
filter.value = [...filter.value, undefined]
}
}
updateFilter()
}
function updateFilter () {
clearTimeout(filterUpdateTimeout)
filterUpdateTimeout = setTimeout(() => onChange(filter), FILTER_DEBOUNCE_MS)
}
let search: string = ''
const dispatch = createEventDispatcher()
$: if (targetClass) getValues(search, $statusStore)
</script>
<div class="selectPopup" use:resizeObserver={() => dispatch('changeContent')}>
{#if clazz.filteringKey}
<div class="header">
<EditWithIcon
icon={IconSearch}
size={'large'}
width={'100%'}
autoFocus={!$deviceOptionsStore.isMobile}
bind:value={search}
placeholder={presentation.string.Search}
/>
</div>
{:else}
<div class="menu-space" />
{/if}
<div class="scroll">
<div class="box">
{#await promise then attribute}
{#if objectsPromise}
<Loading />
{:else}
{#each values as value}
<button
class="menu-item no-focus content-pointer-events-none"
on:click={() => {
handleFilterToggle(value)
}}
>
<div class="flex-between w-full">
<div class="flex-row-center">
{#if value}
{#key value._id}
<svelte:component this={attribute.presenter} {value} {...attribute.props} disabled oneLine />
{/key}
{:else}
<Label label={ui.string.NotSelected} />
{/if}
</div>
<div class="pointer-events-none">
{#if isSelected(value, filter.value)}
<Icon icon={IconCheck} size={'small'} />
{/if}
</div>
</div>
</button>
{/each}
{/if}
{/await}
</div>
</div>
<div class="menu-space" />
</div>
@@ -83,7 +83,7 @@
$: groupByKey = viewOptions.groupBy[level] ?? noCategory
let categories: CategoryType[] = []
$: updateCategories(_class, docs, groupByKey, viewOptions, viewOptionsConfig)
$: updateCategories(_class, space, docs, groupByKey, viewOptions, viewOptionsConfig)
$: groupByDocs = groupBy(docs, groupByKey, categories)
@@ -96,24 +96,25 @@
})
function update () {
updateCategories(_class, docs, groupByKey, viewOptions, viewOptionsConfig)
updateCategories(_class, space, docs, groupByKey, viewOptions, viewOptionsConfig)
}
async function updateCategories (
_class: Ref<Class<Doc>>,
space: Ref<Space> | undefined,
docs: Doc[],
groupByKey: string,
viewOptions: ViewOptions,
viewOptionsModel: ViewOptionModel[] | undefined
) {
categories = await getCategories(client, _class, docs, groupByKey)
categories = await getCategories(client, _class, space, docs, groupByKey)
if (level === 0) {
for (const viewOption of viewOptionsModel ?? []) {
if (viewOption.actionTarget !== 'category') continue
const categoryFunc = viewOption as CategoryOption
if (viewOptions[viewOption.key] ?? viewOption.defaultValue) {
const f = await getResource(categoryFunc.action)
const res = hierarchy.clone(await f(_class, query, groupByKey, update, queryId))
const res = hierarchy.clone(await f(_class, query, space, groupByKey, update, queryId))
if (res !== undefined) {
categories = concatCategories(res, categories)
return
@@ -17,8 +17,8 @@
import { Asset } from '@hcengineering/platform'
import { AnySvelteComponent } from '@hcengineering/ui'
import StatusPresenter from './StatusPresenter.svelte'
import { statusStore } from '../../status'
import StatusPresenter from './StatusPresenter.svelte'
export let value: Ref<Status> | StatusValue | undefined
export let size: 'small' | 'medium' = 'medium'
+2 -7
View File
@@ -39,6 +39,7 @@ import FilterTypePopup from './components/filter/FilterTypePopup.svelte'
import ObjectFilter from './components/filter/ObjectFilter.svelte'
import StringFilter from './components/filter/StringFilter.svelte'
import StringFilterPresenter from './components/filter/StringFilterPresenter.svelte'
import StatusFilter from './components/filter/StatusFilter.svelte'
import TimestampFilter from './components/filter/TimestampFilter.svelte'
import ValueFilter from './components/filter/ValueFilter.svelte'
import HTMLEditor from './components/HTMLEditor.svelte'
@@ -106,10 +107,8 @@ import {
} from './filter'
import { IndexedDocumentPreview } from '@hcengineering/presentation'
import { statusSort } from './utils'
import { showEmptyGroups } from './viewOptions'
import { AggregationMiddleware } from './middleware'
import { grouppingStatusManager, StatusAggregationManager } from './status'
export { getActions, invokeAction } from './actions'
export { default as ActionHandler } from './components/ActionHandler.svelte'
export { default as FilterButton } from './components/filter/FilterButton.svelte'
@@ -201,6 +200,7 @@ export default async (): Promise<Resources> => ({
DateFilter,
ValueFilter,
StringFilter,
StatusFilter,
TimestampFilter,
TableBrowser,
SpacePresenter,
@@ -262,7 +262,6 @@ export default async (): Promise<Resources> => ({
FilterNestedMatchResult: nestedMatchResult,
FilterNestedDontMatchResult: nestedDontMatchResult,
ShowEmptyGroups: showEmptyGroups,
StatusSort: statusSort,
FilterDateOutdated: dateOutdated,
FilterDateToday: dateToday,
FilterDateYesterday: dateYesterday,
@@ -273,9 +272,5 @@ export default async (): Promise<Resources> => ({
FilterDateNotSpecified: dateNotSpecified,
FilterDateCustom: dateCustom,
CreateDocMiddleware: AggregationMiddleware.create
},
aggregation: {
CreateStatusAggregationManager: StatusAggregationManager.create,
GrouppingStatusManager: grouppingStatusManager
}
})
+2 -6
View File
@@ -17,7 +17,7 @@
import { IntlString, Resource, mergeIds } from '@hcengineering/platform'
import { PresentationMiddlewareCreator } from '@hcengineering/presentation'
import { AnyComponent } from '@hcengineering/ui'
import view, { CreateAggregationManagerFunc, GrouppingManagerResource, SortFunc, viewId } from '@hcengineering/view'
import view, { viewId } from '@hcengineering/view'
export default mergeIds(viewId, view, {
component: {
@@ -25,6 +25,7 @@ export default mergeIds(viewId, view, {
DateFilter: '' as AnyComponent,
ValueFilter: '' as AnyComponent,
ArrayFilter: '' as AnyComponent,
StatusFilter: '' as AnyComponent,
StringFilter: '' as AnyComponent,
TimestampFilter: '' as AnyComponent,
FilterTypePopup: '' as AnyComponent,
@@ -96,11 +97,6 @@ export default mergeIds(viewId, view, {
Show: '' as IntlString
},
function: {
StatusSort: '' as SortFunc,
CreateDocMiddleware: '' as Resource<PresentationMiddlewareCreator>
},
aggregation: {
CreateStatusAggregationManager: '' as CreateAggregationManagerFunc,
GrouppingStatusManager: '' as GrouppingManagerResource
}
})
+13 -280
View File
@@ -13,291 +13,24 @@
// limitations under the License.
//
import core, {
AggregateValue,
AggregateValueData,
AnyAttribute,
Attribute,
Class,
Client,
Doc,
DocumentQuery,
FindOptions,
Hierarchy,
IdMap,
Ref,
SortingOrder,
SortingRules,
Space,
Status,
StatusCategory,
StatusManager,
StatusValue,
Tx,
WithLookup,
matchQuery,
toIdMap
} from '@hcengineering/core'
import { LiveQuery } from '@hcengineering/query'
import { AggregationManager, GrouppingManager } from '@hcengineering/view'
import { get, writable } from 'svelte/store'
import core, { IdMap, Status, toIdMap } from '@hcengineering/core'
import { createQuery, getClient } from '@hcengineering/presentation'
import { writable } from 'svelte/store'
// Issue status live query
export const statusStore = writable<StatusManager>(new StatusManager([]))
export const statusStore = writable<IdMap<Status>>()
/**
* @public
*/
export class StatusAggregationManager implements AggregationManager {
docs: Doc[] | undefined
function fillStores (): void {
const client = getClient()
docsByName: Map<string, Status[]> = new Map<string, Status[]>()
mgr: StatusManager | Promise<StatusManager> | undefined
statusCategory: IdMap<StatusCategory> = new Map()
query: (() => void) | undefined
categoryQuery: (() => void) | undefined
categoryPromise!: Promise<void>
lq: LiveQuery
lqCallback: () => void
private constructor (client: Client, lqCallback: () => void) {
this.lq = new LiveQuery(client)
this.lqCallback = lqCallback ?? (() => {})
}
static create (client: Client, lqCallback: () => void): StatusAggregationManager {
return new StatusAggregationManager(client, lqCallback)
}
private async getManager (): Promise<StatusManager> {
if (this.mgr !== undefined) {
if (this.mgr instanceof Promise) {
this.mgr = await this.mgr
}
return this.mgr
}
this.categoryPromise = new Promise((resolve) => {
this.categoryQuery = this.lq.query(core.class.StatusCategory, {}, (res) => {
this.statusCategory = toIdMap(res)
resolve()
})
if (client !== undefined) {
const query = createQuery(true)
query.query(core.class.Status, {}, (res) => {
statusStore.set(toIdMap(res))
})
this.mgr = new Promise<StatusManager>((resolve) => {
this.query = this.lq.query(
core.class.Status,
{},
(res) => {
const first = this.docs === undefined
this.docs = res
const newMap = new Map<string, Status[]>()
for (const d of this.docs as Array<WithLookup<Status>>) {
const n = d.name.toLowerCase().trim()
newMap.set(n, [...(newMap.get(n) ?? []), d])
}
this.mgr = new StatusManager(res)
this.docsByName = newMap
statusStore.set(this.mgr)
if (!first) {
this.lqCallback()
}
resolve(this.mgr)
},
{
sort: {
rank: SortingOrder.Ascending
}
}
)
})
return await this.mgr
}
close (): void {
this.query?.()
this.categoryQuery?.()
}
async notifyTx (tx: Tx): Promise<void> {
await this.lq.tx(tx)
}
getAttrClass (): Ref<Class<Doc>> {
return core.class.Status
}
async categorize (target: Array<Ref<Doc>>, attr: AnyAttribute): Promise<Array<Ref<Doc>>> {
const mgr = await this.getManager()
const idMap = mgr.getIdMap()
for (const sid of [...target]) {
const s = idMap.get(sid as Ref<Status>) as WithLookup<Status>
if (s !== undefined) {
const statuses = (this.docsByName.get(s.name.toLowerCase().trim()) ?? []).filter(
(it) => it.ofAttribute === attr._id && it._id !== s._id
)
target.push(...statuses.map((it) => it._id))
}
}
return target.filter((it, idx, arr) => arr.indexOf(it) === idx)
}
async updateLookup (resultDoc: WithLookup<Doc>, attr: Attribute<Doc>): Promise<void> {
const value = (resultDoc as any)[attr.name]
const doc = (await this.getManager()).getIdMap().get(value)
if (doc !== undefined) {
;(resultDoc.$lookup as any)[attr.name] = doc
}
}
async updateSorting<T extends Doc>(finalOptions: FindOptions<T>, attr: AnyAttribute): Promise<void> {
const attrSort = finalOptions.sort?.[attr.name]
if (attrSort !== undefined && typeof attrSort !== 'object') {
// Fill custom sorting.
let statuses = (await this.getManager()).getDocs()
statuses = statuses.filter((it) => it.ofAttribute === attr._id)
await this.categoryPromise
statuses.sort((a, b) => {
let ret = 0
if (a.category !== undefined && b.category !== undefined) {
ret = (this.statusCategory.get(a.category)?.order ?? 0) - (this.statusCategory.get(b.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
}
} else {
setTimeout(() => fillStores(), 50)
}
}
/**
* @public
*/
export const grouppingStatusManager: GrouppingManager = {
groupByCategories: groupByStatusCategories,
groupValues: groupStatusValues,
groupValuesWithEmpty: groupStatusValuesWithEmpty,
hasValue: hasStatusValue
}
/**
* @public
*/
export function groupByStatusCategories (categories: any[]): AggregateValue[] {
const mgr = get(statusStore)
const existingCategories: AggregateValue[] = []
const statusMap = new Map<string, AggregateValue>()
const usedSpaces = new Set<Ref<Space>>()
const statusesList: Array<WithLookup<Status>> = []
for (const v of categories) {
const status = mgr.getIdMap().get(v)
if (status !== undefined) {
statusesList.push(status)
usedSpaces.add(status.space)
}
}
for (const status of statusesList) {
if (status !== undefined) {
let fst = statusMap.get(status.name.toLowerCase().trim())
if (fst === undefined) {
const statuses = mgr
.getDocs()
.filter(
(it) =>
it.ofAttribute === status.ofAttribute &&
it.name.toLowerCase().trim() === status.name.toLowerCase().trim() &&
(categories.includes(it._id) || usedSpaces.has(it.space))
)
.sort((a, b) => a.rank.localeCompare(b.rank))
.map((it) => new AggregateValueData(it.name, it._id, it.space, it.rank, it.category))
fst = new StatusValue(status.name, status.color, statuses)
statusMap.set(status.name.toLowerCase().trim(), fst)
existingCategories.push(fst)
}
}
}
return existingCategories
}
/**
* @public
*/
export function groupStatusValues (val: Doc[], targets: Set<any>): Doc[] {
const values = val
const result: Doc[] = []
const unique = [...new Set(val.map((v) => (v as Status).name.trim().toLocaleLowerCase()))]
unique.forEach((label, i) => {
let exists = false
values.forEach((value) => {
if ((value as Status).name.trim().toLocaleLowerCase() === label) {
if (!exists) {
result[i] = value
exists = targets.has(value?._id)
}
}
})
})
return result
}
/**
* @public
*/
export function hasStatusValue (value: Doc | undefined | null, values: any[]): boolean {
const mgr = get(statusStore)
const statusSet = new Set(
mgr
.filter((it) => it.name.trim().toLocaleLowerCase() === (value as Status)?.name?.trim()?.toLocaleLowerCase())
.map((it) => it._id)
)
return values.some((it) => statusSet.has(it))
}
/**
* @public
*/
export function groupStatusValuesWithEmpty (
hierarchy: Hierarchy,
_class: Ref<Class<Doc>>,
key: string,
query: DocumentQuery<Doc> | undefined
): Array<Ref<Doc>> {
const mgr = get(statusStore)
const attr = hierarchy.getAttribute(_class, key)
// We do not need extensions for all status categories.
let statusList = mgr.filter((it) => {
return it.ofAttribute === attr._id
})
if (query !== undefined) {
const { [key]: st, space } = query
const resQuery: DocumentQuery<Doc> = {}
if (space !== undefined) {
resQuery.space = space
}
if (st !== undefined) {
resQuery._id = st
}
statusList = matchQuery<Doc>(statusList, resQuery, _class, hierarchy) as unknown as Array<WithLookup<Status>>
}
return statusList.map((it) => it._id)
}
fillStores()
+14 -59
View File
@@ -16,16 +16,14 @@
import core, {
AccountRole,
AttachedDoc,
AggregateValue,
AttachedDoc,
CategoryType,
Class,
Client,
Collection,
Doc,
DocumentUpdate,
getCurrentAccount,
getObjectValue,
Hierarchy,
Lookup,
Obj,
@@ -34,25 +32,20 @@ import core, {
ReverseLookup,
ReverseLookups,
Space,
Status,
TxOperations
TxOperations,
getCurrentAccount,
getObjectValue
} from '@hcengineering/core'
import type { IntlString } from '@hcengineering/platform'
import { getResource } from '@hcengineering/platform'
import {
AttributeCategory,
createQuery,
getAttributePresenterClass,
hasResource,
KeyedAttribute
} from '@hcengineering/presentation'
import { AttributeCategory, KeyedAttribute, getAttributePresenterClass, hasResource } from '@hcengineering/presentation'
import {
AnyComponent,
ErrorPresenter,
Location,
getCurrentResolvedLocation,
getPanelURI,
getPlatformColorForText,
Location,
locationToUrl
} from '@hcengineering/ui'
import type { BuildModelOptions, Viewlet, ViewletDescriptor } from '@hcengineering/view'
@@ -624,6 +617,7 @@ export function setGroupByValues (
export async function groupByCategory (
client: TxOperations,
_class: Ref<Class<Doc>>,
space: Ref<Space> | undefined,
key: string,
categories: CategoryType[],
viewletDescriptorId?: Ref<ViewletDescriptor>
@@ -649,12 +643,13 @@ export async function groupByCategory (
}
}
}
return await sortCategories(h, attrClass, existingCategories, viewletDescriptorId)
return await sortCategories(client, attrClass, space, existingCategories, viewletDescriptorId)
}
export async function getCategories (
client: TxOperations,
_class: Ref<Class<Doc>>,
space: Ref<Space> | undefined,
docs: Doc[],
key: string,
viewletDescriptorId?: Ref<ViewletDescriptor>
@@ -664,6 +659,7 @@ export async function getCategories (
return await groupByCategory(
client,
_class,
space,
key,
docs.map((it) => getObjectValue(key, it) ?? undefined),
viewletDescriptorId
@@ -713,30 +709,20 @@ export function concatCategories (arr1: CategoryType[], arr2: CategoryType[]): C
* @public
*/
export async function sortCategories (
hierarchy: Hierarchy,
client: TxOperations,
attrClass: Ref<Class<Doc>>,
space: Ref<Space> | undefined,
existingCategories: any[],
viewletDescriptorId?: Ref<ViewletDescriptor>
): Promise<any[]> {
const hierarchy = client.getHierarchy()
const clazz = hierarchy.getClass(attrClass)
const sortFunc = hierarchy.as(clazz, view.mixin.SortFuncs)
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)
return await f(client, existingCategories, space, viewletDescriptorId)
}
export function getKeyLabel<T extends Doc> (
@@ -876,37 +862,6 @@ export async function getObjectLinkFragment (
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
}
}
)
})
}
export function isAttachedDoc (doc: Doc | AttachedDoc): doc is AttachedDoc {
return 'attachedTo' in doc
}
+4 -3
View File
@@ -1,4 +1,4 @@
import { Class, Doc, DocumentQuery, Ref, SortingOrder } from '@hcengineering/core'
import { Class, Doc, DocumentQuery, Ref, SortingOrder, Space } from '@hcengineering/core'
import { getResource } from '@hcengineering/platform'
import { LiveQuery, createQuery, getAttributePresenterClass, getClient } from '@hcengineering/presentation'
import { locationToUrl, getCurrentResolvedLocation } from '@hcengineering/ui'
@@ -114,6 +114,7 @@ export function migrateViewOpttions (): void {
export async function showEmptyGroups (
_class: Ref<Class<Doc>>,
query: DocumentQuery<Doc> | undefined,
space: Ref<Space> | undefined,
key: string,
onUpdate: () => void,
queryId: Ref<Doc>,
@@ -139,7 +140,7 @@ export async function showEmptyGroups (
if (groupMixin?.grouppingManager !== undefined) {
const grouppingManager = await getResource(groupMixin.grouppingManager)
const docs = grouppingManager.groupValuesWithEmpty(hierarchy, _class, key, query)
return await groupByCategory(client, _class, key, docs, viewletDescriptorId)
return await groupByCategory(client, _class, space, key, docs, viewletDescriptorId)
}
const allValuesMixin = hierarchy.as(attributeClass, view.mixin.AllValuesFunc)
@@ -147,7 +148,7 @@ export async function showEmptyGroups (
const f = await getResource(allValuesMixin.func)
const res = await f(query, onUpdate, queryId)
if (res !== undefined) {
return await groupByCategory(client, _class, key, res, viewletDescriptorId)
return await groupByCategory(client, _class, space, key, res, viewletDescriptorId)
}
}
}
+8 -1
View File
@@ -36,6 +36,7 @@ import {
Space,
StatusValue,
Tx,
TxOperations,
Type,
UXObject,
WithLookup
@@ -290,7 +291,12 @@ export interface ListHeaderExtra extends Class<Doc> {
* @public
*/
export type SortFunc = Resource<
(values: (PrimitiveType | StatusValue)[], viewletDescriptorId?: Ref<ViewletDescriptor>) => Promise<any[]>
(
client: TxOperations,
values: (PrimitiveType | StatusValue)[],
space: Ref<Space> | undefined,
viewletDescriptorId?: Ref<ViewletDescriptor>
) => Promise<any[]>
>
/**
@@ -638,6 +644,7 @@ export interface ViewOption {
export type ViewCategoryActionFunc = (
_class: Ref<Class<Doc>>,
query: DocumentQuery<Doc> | undefined,
space: Ref<Space> | undefined,
key: string,
onUpdate: () => void,
queryId: Ref<Doc>,
@@ -146,7 +146,7 @@
</div>
</div>
{/if}
<FilterBar {_class} query={searchQuery} on:change={(e) => (resultQuery = e.detail)} />
<FilterBar {_class} query={searchQuery} space={undefined} on:change={(e) => (resultQuery = e.detail)} />
<Scroller padding={'2.5rem'}>
<div class="spaces-container">
{#each spaces as space (space._id)}
@@ -60,6 +60,7 @@
{:else}
<FilterBar
{_class}
{space}
query={searchQuery}
{viewOptions}
on:change={(e) => (resultQuery = { ...e.detail, ...query })}
@@ -93,6 +93,7 @@
{:else if viewOptions && viewlet}
<FilterBar
{_class}
{space}
query={searchQuery}
{viewOptions}
on:change={(e) => {
+2 -2
View File
@@ -47,7 +47,7 @@ export async function OnTemplateStateUpdate (tx: Tx, control: TriggerControl): P
await control.findAll(classToChange, { space: { $in: ids }, name: prevDoc.name })
) as Array<State | DoneState>
return statesToChange.map((it) => {
const newAttributes = it._class === task.class.State ? { color: newDoc.color, rank: newDoc.rank } : {}
const newAttributes = it._class === task.class.State ? { color: newDoc.color } : {}
return control.txFactory.createTxUpdateDoc(it._class, it.space, it._id, {
name: newDoc.name,
...newAttributes
@@ -75,7 +75,7 @@ export async function OnTemplateStateCreate (tx: Tx, control: TriggerControl): P
const doc = TxProcessor.createDoc2Doc(actualTx)
const ofAttribute = classToChange === task.class.State ? task.attribute.State : task.attribute.DoneState
return ids.map((it) => {
const newAttributes = classToChange === task.class.State ? { color: doc.color, rank: doc.rank } : {}
const newAttributes = classToChange === task.class.State ? { color: doc.color } : {}
return control.txFactory.createTxCreateDoc(classToChange, it, {
ofAttribute,
name: doc.name,