mirror of
https://github.com/hcengineering/platform.git
synced 2026-09-27 12:04:56 +02:00
+10
-31
@@ -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]
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
})
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
@@ -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
@@ -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
@@ -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>
|
||||
|
||||
+2
-2
@@ -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}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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
|
||||
}
|
||||
})
|
||||
|
||||
@@ -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,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()
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user