Fix statuses (#3666)

Signed-off-by: Denis Bykhov <bykhov.denis@gmail.com>
This commit is contained in:
Denis Bykhov 2023-09-07 00:49:53 +06:00 committed by GitHub
parent 47e39eae75
commit f3c3541964
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
32 changed files with 390 additions and 125 deletions

View File

@ -35,12 +35,13 @@ import contact from '@hcengineering/model-contact'
import core, { TDoc, TType } from '@hcengineering/model-core'
import preference, { TPreference } from '@hcengineering/model-preference'
import tags from '@hcengineering/model-tags'
import task, { TSpaceWithStates, TTask } from '@hcengineering/model-task'
import task, { TSpaceWithStates, TTask, actionTemplates as taskActionTemplates } from '@hcengineering/model-task'
import view, { actionTemplates, createAction, actionTemplates as viewTemplates } from '@hcengineering/model-view'
import workbench, { Application } from '@hcengineering/model-workbench'
import { IntlString } from '@hcengineering/platform'
import type { AnyComponent } from '@hcengineering/ui'
import board from './plugin'
import { State } from '@hcengineering/task'
export { boardId } from '@hcengineering/board'
export { boardOperation } from './migration'
@ -99,6 +100,9 @@ export class TCard extends TTask implements Card {
@Prop(TypeDate(), task.string.StartDate)
startDate!: Timestamp | null
@Prop(TypeRef(task.class.State), task.string.TaskState, { _id: board.attribute.State })
declare status: Ref<State>
}
@Model(board.class.MenuPage, core.class.Doc, DOMAIN_MODEL)
@ -167,6 +171,27 @@ export function createModel (builder: Builder): void {
board.app.Board
)
createAction(
builder,
{
...taskActionTemplates.editStatus,
target: board.class.Board,
actionProps: {
ofAttribute: board.attribute.State,
doneOfAttribute: board.attribute.DoneState
},
query: {
archived: false
},
context: {
mode: ['context', 'browser'],
group: 'edit'
},
override: [task.action.EditStatuses]
},
board.action.EditStatuses
)
// const leadLookup: Lookup<Card> =
// {
// state: task.class.State,

View File

@ -22,7 +22,7 @@ import task, { KanbanTemplate, createStates } from '@hcengineering/task'
import board from './plugin'
async function createSpace (tx: TxOperations): Promise<void> {
const currentTemplate = await tx.findOne(core.class.Space, {
const currentTemplate = await tx.findOne(task.class.KanbanTemplateSpace, {
_id: board.space.BoardTemplates
})
if (currentTemplate === undefined) {
@ -36,10 +36,17 @@ async function createSpace (tx: TxOperations): Promise<void> {
private: false,
archived: false,
members: [],
attachedToClass: board.class.Board
attachedToClass: board.class.Board,
ofAttribute: board.attribute.State,
doneAttribute: board.attribute.DoneState
},
board.space.BoardTemplates
)
} else if (currentTemplate.ofAttribute === undefined) {
await tx.update(currentTemplate, {
ofAttribute: board.attribute.State,
doneAttribute: board.attribute.DoneState
})
}
const current = await tx.findOne(core.class.Space, {
@ -47,7 +54,7 @@ async function createSpace (tx: TxOperations): Promise<void> {
})
if (current === undefined) {
const defaultTmpl = await createDefaultKanbanTemplate(tx)
const [states, doneStates] = await createStates(tx, defaultTmpl)
const [states, doneStates] = await createStates(tx, board.attribute.State, board.attribute.DoneState, defaultTmpl)
await tx.createDoc(
board.class.Board,
core.space.Space,
@ -77,13 +84,18 @@ async function createDefaultKanbanTemplate (tx: TxOperations): Promise<Ref<Kanba
]
}
return await createKanbanTemplate(tx, {
kanbanId: board.template.DefaultBoard,
space: board.space.BoardTemplates,
title: 'Default board',
states: defaultKanban.states,
doneStates: defaultKanban.doneStates
})
return await createKanbanTemplate(
tx,
{
kanbanId: board.template.DefaultBoard,
space: board.space.BoardTemplates,
title: 'Default board',
states: defaultKanban.states,
doneStates: defaultKanban.doneStates
},
board.attribute.State,
board.attribute.DoneState
)
}
async function createDefaults (tx: TxOperations): Promise<void> {
await createSpace(tx)

View File

@ -62,6 +62,7 @@ export default mergeIds(boardId, board, {
ConfigDescription: '' as IntlString
},
action: {
EditStatuses: '' as Ref<Action>,
ConvertToCard: '' as Ref<Action>
},
actionImpl: {

View File

@ -81,7 +81,7 @@ export class TLead extends TTask implements Lead {
@Prop(TypeRef(contact.mixin.Employee), lead.string.Assignee)
declare assignee: Ref<Employee> | null
@Prop(TypeRef(task.class.State), task.string.TaskState, { _id: task.attribute.State })
@Prop(TypeRef(task.class.State), task.string.TaskState, { _id: lead.attribute.State })
declare status: Ref<State>
declare space: Ref<Funnel>
@ -365,6 +365,27 @@ export function createModel (builder: Builder): void {
}
}
createAction(
builder,
{
...actionTemplates.editStatus,
target: lead.class.Funnel,
actionProps: {
ofAttribute: lead.attribute.State,
doneOfAttribute: lead.attribute.DoneState
},
query: {
archived: false
},
context: {
mode: ['context', 'browser'],
group: 'edit'
},
override: [task.action.EditStatuses]
},
lead.action.EditStatuses
)
builder.createDoc(
notification.class.NotificationGroup,
core.space.Model,

View File

@ -22,7 +22,7 @@ import { PaletteColorIndexes } from '@hcengineering/ui/src/colors'
import lead from './plugin'
async function createSpace (tx: TxOperations): Promise<void> {
const currentTemplate = await tx.findOne(core.class.Space, {
const currentTemplate = await tx.findOne(task.class.KanbanTemplateSpace, {
_id: lead.space.FunnelTemplates
})
if (currentTemplate === undefined) {
@ -36,10 +36,17 @@ async function createSpace (tx: TxOperations): Promise<void> {
private: false,
members: [],
archived: false,
attachedToClass: lead.class.Funnel
attachedToClass: lead.class.Funnel,
ofAttribute: lead.attribute.State,
doneAttribute: lead.attribute.DoneState
},
lead.space.FunnelTemplates
)
} else if (currentTemplate.ofAttribute === undefined) {
await tx.update(currentTemplate, {
ofAttribute: lead.attribute.State,
doneAttribute: lead.attribute.DoneState
})
}
const current = await tx.findOne(core.class.Space, {
@ -47,7 +54,7 @@ async function createSpace (tx: TxOperations): Promise<void> {
})
if (current === undefined) {
const defaultTmpl = await createDefaultKanbanTemplate(tx)
const [states, doneStates] = await createStates(tx, defaultTmpl)
const [states, doneStates] = await createStates(tx, lead.attribute.State, lead.attribute.DoneState, defaultTmpl)
await tx.createDoc(
lead.class.Funnel,
core.space.Space,
@ -81,13 +88,18 @@ async function createDefaultKanbanTemplate (tx: TxOperations): Promise<Ref<Kanba
]
}
return await createKanbanTemplate(tx, {
kanbanId: lead.template.DefaultFunnel,
space: lead.space.FunnelTemplates,
title: 'Default funnel',
states: defaultKanban.states,
doneStates: defaultKanban.doneStates
})
return await createKanbanTemplate(
tx,
{
kanbanId: lead.template.DefaultFunnel,
space: lead.space.FunnelTemplates,
title: 'Default funnel',
states: defaultKanban.states,
doneStates: defaultKanban.doneStates
},
lead.attribute.State,
lead.attribute.DoneState
)
}
async function fixTemplateSpace (tx: TxOperations): Promise<void> {

View File

@ -60,6 +60,7 @@ export default mergeIds(leadId, lead, {
Lead: '' as Ref<ActionCategory>
},
action: {
EditStatuses: '' as Ref<Action>,
CreateGlobalLead: '' as Ref<Action>
},
ids: {

View File

@ -165,7 +165,7 @@ export class TApplicant extends TTask implements Applicant {
@Prop(TypeRef(contact.mixin.Employee), recruit.string.AssignedRecruiter)
declare assignee: Ref<Employee> | null
@Prop(TypeRef(task.class.State), task.string.TaskState, { _id: task.attribute.State })
@Prop(TypeRef(task.class.State), task.string.TaskState, { _id: recruit.attribute.State })
declare status: Ref<State>
}
@ -368,6 +368,27 @@ export function createModel (builder: Builder): void {
recruit.app.Recruit
)
createAction(
builder,
{
...actionTemplates.editStatus,
target: recruit.class.Vacancy,
actionProps: {
ofAttribute: recruit.attribute.State,
doneOfAttribute: recruit.attribute.DoneState
},
query: {
archived: false
},
context: {
mode: ['context', 'browser'],
group: 'edit'
},
override: [task.action.EditStatuses]
},
recruit.action.EditStatuses
)
builder.createDoc(
view.class.Viewlet,
core.space.Model,

View File

@ -92,15 +92,20 @@ async function createDefaultKanbanTemplate (tx: TxOperations): Promise<Ref<Kanba
]
}
return await createKanbanTemplate(tx, {
kanbanId: recruit.template.DefaultVacancy,
space: recruit.space.VacancyTemplates as Ref<Doc> as Ref<Space>,
title: 'Default vacancy',
description: '',
shortDescription: '',
states: defaultKanban.states,
doneStates: defaultKanban.doneStates
})
return await createKanbanTemplate(
tx,
{
kanbanId: recruit.template.DefaultVacancy,
space: recruit.space.VacancyTemplates as Ref<Doc> as Ref<Space>,
title: 'Default vacancy',
description: '',
shortDescription: '',
states: defaultKanban.states,
doneStates: defaultKanban.doneStates
},
recruit.attribute.State,
recruit.attribute.DoneState
)
}
async function createSpaces (tx: TxOperations): Promise<void> {
@ -142,7 +147,7 @@ async function createSpaces (tx: TxOperations): Promise<void> {
await tx.update(currentReviews, { private: false })
}
const currentTemplate = await tx.findOne(core.class.Space, {
const currentTemplate = await tx.findOne(task.class.KanbanTemplateSpace, {
_id: recruit.space.VacancyTemplates
})
if (currentTemplate === undefined) {
@ -157,9 +162,16 @@ async function createSpaces (tx: TxOperations): Promise<void> {
private: false,
members: [],
archived: false,
attachedToClass: recruit.class.Vacancy
attachedToClass: recruit.class.Vacancy,
ofAttribute: recruit.attribute.State,
doneAttribute: recruit.attribute.DoneState
},
recruit.space.VacancyTemplates
)
} else if (currentTemplate.ofAttribute === undefined) {
await tx.update(currentTemplate, {
ofAttribute: recruit.attribute.State,
doneAttribute: recruit.attribute.DoneState
})
}
}

View File

@ -31,7 +31,8 @@ export default mergeIds(recruitId, recruit, {
CopyApplicationLink: '' as Ref<Action>,
CopyCandidateLink: '' as Ref<Action>,
MoveApplicant: '' as Ref<Action>,
GetTalentIds: '' as Ref<Action>
GetTalentIds: '' as Ref<Action>,
EditStatuses: '' as Ref<Action>
},
actionImpl: {
CreateOpinion: '' as ViewAction,

View File

@ -158,6 +158,8 @@ export class TKanbanTemplateSpace extends TSpace implements KanbanTemplateSpace
description!: IntlString
icon!: AnyComponent
editor!: AnyComponent
ofAttribute!: Ref<Attribute<State>>
doneAttribute!: Ref<Attribute<DoneState>>
attachedToClass!: Ref<Class<Doc>>
}
@ -337,6 +339,10 @@ export function createModel (builder: Builder): void {
{
...actionTemplates.editStatus,
target: task.class.SpaceWithStates,
actionProps: {
ofAttribute: task.attribute.State,
doneOfAttribute: task.attribute.DoneState
},
query: {
archived: false
},

View File

@ -14,6 +14,7 @@
//
import {
Attribute,
Class,
DOMAIN_STATUS,
DOMAIN_TX,
@ -25,13 +26,14 @@ import {
TxCollectionCUD,
TxCreateDoc,
TxOperations,
TxUpdateDoc
TxUpdateDoc,
toIdMap
} from '@hcengineering/core'
import { MigrateOperation, MigrationClient, MigrationUpgradeClient, createOrUpdate } from '@hcengineering/model'
import core, { DOMAIN_SPACE } from '@hcengineering/model-core'
import tags from '@hcengineering/model-tags'
import { DOMAIN_VIEW } from '@hcengineering/model-view'
import { DoneStateTemplate, KanbanTemplate, StateTemplate, Task, genRanks } from '@hcengineering/task'
import { DoneState, DoneStateTemplate, KanbanTemplate, State, StateTemplate, Task, genRanks } from '@hcengineering/task'
import view, { Filter, FilteredView } from '@hcengineering/view'
import { DOMAIN_TASK } from '.'
import task from './plugin'
@ -73,7 +75,9 @@ export async function createSequence (tx: TxOperations, _class: Ref<Class<Doc>>)
*/
export async function createKanbanTemplate (
client: TxOperations,
data: KanbanTemplateData
data: KanbanTemplateData,
ofAttribute: Ref<Attribute<Status>>,
doneAtrtribute?: Ref<Attribute<DoneState>>
): Promise<Ref<KanbanTemplate>> {
const current = await client.findOne(task.class.KanbanTemplate, { _id: data.kanbanId })
if (current !== undefined) {
@ -96,7 +100,7 @@ export async function createKanbanTemplate (
data.doneStates.map((st, i) =>
client.createDoc(st.isWon ? task.class.WonStateTemplate : task.class.LostStateTemplate, data.space, {
rank: doneStateRanks[i],
ofAttribute: task.attribute.DoneState,
ofAttribute: doneAtrtribute ?? ofAttribute,
name: st.name,
attachedTo: data.kanbanId
})
@ -108,7 +112,7 @@ export async function createKanbanTemplate (
data.states.map((st, i) =>
client.createDoc(task.class.StateTemplate, data.space, {
attachedTo: data.kanbanId,
ofAttribute: task.attribute.State,
ofAttribute,
rank: stateRanks[i],
name: st.name,
color: st.color
@ -226,7 +230,46 @@ async function renameStatePrefs (client: MigrationUpgradeClient): Promise<void>
}
}
async function fixStatusAttributes (client: MigrationClient): Promise<void> {
const spaces = await client.find<Space>(DOMAIN_SPACE, {})
const map = toIdMap(spaces)
const oldStatuses = await client.find<OldStatus>(DOMAIN_STATUS, { space: { $ne: task.space.Statuses } })
for (const oldStatus of oldStatuses) {
const space = map.get(oldStatus.space)
if (space !== undefined) {
try {
const isDone = oldStatus._class === task.class.DoneState
let ofAttribute = task.attribute.State
if (space._class === ('recruit:class:Vacancy' as Ref<Class<Space>>)) {
ofAttribute = isDone
? ('recruit.attribute.DoneState' as Ref<Attribute<State>>)
: ('recruit:attribute:State' as Ref<Attribute<State>>)
}
if (space._class === ('lead:class:Funnel' as Ref<Class<Space>>)) {
ofAttribute = isDone
? ('lead.attribute.DoneState' as Ref<Attribute<State>>)
: ('lead:attribute:State' as Ref<Attribute<State>>)
}
if (space._class === ('board:class:Board' as Ref<Class<Space>>)) {
ofAttribute = isDone
? ('board.attribute.DoneState' as Ref<Attribute<State>>)
: ('board:attribute:State' as Ref<Attribute<State>>)
}
if (space._class === ('tracker:class:Project' as Ref<Class<Space>>)) {
ofAttribute = 'tracker:attribute:IssueStatus' as Ref<Attribute<State>>
}
if (ofAttribute !== oldStatus.ofAttribute) {
await client.update(DOMAIN_STATUS, { _id: oldStatus._id }, { ofAttribute })
}
} catch (err) {
console.log(err)
}
}
}
}
async function migrateStatuses (client: MigrationClient): Promise<void> {
await fixStatusAttributes(client)
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>>()

View File

@ -23,7 +23,12 @@ export async function createVacancy (
const incResult = await client.update(sequence, { $inc: { sequence: 1 } }, true)
const [states, doneStates] = await createStates(client, templateId)
const [states, doneStates] = await createStates(
client,
recruit.attribute.State,
recruit.attribute.DoneState,
templateId
)
const id = await client.createDoc(recruit.class.Vacancy, core.space.Space, {
name,

View File

@ -25,7 +25,7 @@ export async function createBoard (
description: string,
templateId?: Ref<KanbanTemplate>
): Promise<Ref<Board>> {
const [states, doneStates] = await createStates(client, templateId)
const [states, doneStates] = await createStates(client, board.attribute.State, board.attribute.DoneState, templateId)
const boardRef = await client.createDoc(board.class.Board, core.space.Space, {
name,

View File

@ -15,7 +15,7 @@
//
import { Employee } from '@hcengineering/contact'
import type { Class, Doc, Markup, Ref, Timestamp, Type } from '@hcengineering/core'
import type { Attribute, Class, Doc, Markup, Ref, Timestamp, Type } from '@hcengineering/core'
import type { Asset, IntlString, Plugin } from '@hcengineering/platform'
import { plugin } from '@hcengineering/platform'
import type { Preference } from '@hcengineering/preference'
@ -120,6 +120,10 @@ const boards = plugin(boardId, {
string: {
ConfigLabel: '' as IntlString
},
attribute: {
State: '' as Ref<Attribute<State>>,
DoneState: '' as Ref<Attribute<DoneState>>
},
icon: {
Board: '' as Asset,
Card: '' as Asset

View File

@ -15,11 +15,11 @@
//
import type { Contact } from '@hcengineering/contact'
import type { Class, Doc, Ref, Timestamp } from '@hcengineering/core'
import type { Attribute, Class, Doc, Ref, Timestamp } from '@hcengineering/core'
import { Mixin } from '@hcengineering/core'
import type { Asset, IntlString, Plugin } from '@hcengineering/platform'
import { plugin } from '@hcengineering/platform'
import type { KanbanTemplateSpace, SpaceWithStates, State, Task } from '@hcengineering/task'
import type { DoneState, KanbanTemplateSpace, SpaceWithStates, State, Task } from '@hcengineering/task'
/**
* @public
@ -73,6 +73,10 @@ const lead = plugin(leadId, {
Lead: '' as IntlString,
ConfigLabel: '' as IntlString
},
attribute: {
State: '' as Ref<Attribute<State>>,
DoneState: '' as Ref<Attribute<DoneState>>
},
icon: {
Funnel: '' as Asset,
Lead: '' as Asset,

View File

@ -174,7 +174,12 @@
const incResult = await client.update(sequence, { $inc: { sequence: 1 } }, true)
const [states, doneStates] = await createStates(client, templateId)
const [states, doneStates] = await createStates(
client,
recruit.attribute.State,
recruit.attribute.DoneState,
templateId
)
const id = await client.createDoc(
recruit.class.Vacancy,

View File

@ -15,11 +15,21 @@
import { Event } from '@hcengineering/calendar'
import type { Channel, Organization, Person } from '@hcengineering/contact'
import type { AttachedData, AttachedDoc, Class, Doc, Mixin, Ref, Space, Timestamp } from '@hcengineering/core'
import type {
AttachedData,
AttachedDoc,
Attribute,
Class,
Doc,
Mixin,
Ref,
Space,
Timestamp
} from '@hcengineering/core'
import type { Asset, IntlString, Plugin, Resource } from '@hcengineering/platform'
import { plugin } from '@hcengineering/platform'
import { TagReference } from '@hcengineering/tags'
import type { KanbanTemplateSpace, SpaceWithStates, State, Task } from '@hcengineering/task'
import type { DoneState, KanbanTemplateSpace, SpaceWithStates, State, Task } from '@hcengineering/task'
import { AnyComponent, ResolvedLocation } from '@hcengineering/ui'
/**
@ -155,6 +165,10 @@ const recruit = plugin(recruitId, {
Candidate: '' as Ref<Mixin<Candidate>>,
VacancyList: '' as Ref<Mixin<VacancyList>>
},
attribute: {
State: '' as Ref<Attribute<State>>,
DoneState: '' as Ref<Attribute<DoneState>>
},
component: {
EditVacancy: '' as AnyComponent
},

View File

@ -28,7 +28,7 @@
let templateMap = new Map<Ref<KanbanTemplate>, KanbanTemplate>()
const templatesQ = createQuery()
$: if (folder !== undefined) {
templatesQ.query(task.class.KanbanTemplate, { space: folder._id as Ref<Doc> as Ref<Space> }, (result) => {
templatesQ.query(task.class.KanbanTemplate, { space: folder._id }, (result) => {
templates = result
})
} else {
@ -81,7 +81,7 @@
await Promise.all(
doneStates.map(async (ds) => {
await client.createDoc(ds.class, space as Ref<Doc> as Ref<Space>, {
await client.createDoc(ds.class, space, {
attachedTo: template,
ofAttribute: task.attribute.DoneState,
name: ds.name,

View File

@ -13,7 +13,7 @@
// limitations under the License.
-->
<script lang="ts">
import { Class, Data, Ref } from '@hcengineering/core'
import { Attribute, Class, Data, Ref, Status } 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'
@ -25,6 +25,7 @@
const hierarchy = client.getHierarchy()
export let status: State | undefined = undefined
export let _class: Ref<Class<State | DoneState>> | undefined = status?._class
export let ofAttribute: Ref<Attribute<Status>>
export let value = status?.name ?? ''
export let space: Ref<SpaceWithStates>
@ -40,7 +41,7 @@
if (status === undefined) {
if (!hierarchy.isDerived(_class, task.class.DoneState)) {
const newDoc: Data<State> = {
ofAttribute: task.attribute.State,
ofAttribute,
name: value.trim(),
color: 9
}
@ -48,7 +49,7 @@
await client.update(_space, { $push: { states: id } })
} else {
const newDoc: Data<DoneState> = {
ofAttribute: task.attribute.DoneState,
ofAttribute,
name: value.trim()
}
const id = await createState(client, _class, newDoc)

View File

@ -13,7 +13,7 @@
// limitations under the License.
-->
<script lang="ts">
import { Class, Data, Ref, SortingOrder } from '@hcengineering/core'
import { Attribute, Class, Data, Ref, SortingOrder, Status } 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'
@ -26,6 +26,7 @@
export let status: StateTemplate | undefined = undefined
export let _class: Ref<Class<StateTemplate | DoneStateTemplate>> | undefined = status?._class
export let template: KanbanTemplate
export let ofAttribute: Ref<Attribute<Status>>
export let space: KanbanTemplateSpace
export let value = status?.name ?? ''
@ -36,14 +37,14 @@
if (_class !== undefined && status === undefined) {
const lastOne = await client.findOne(_class, attachedTo, { sort: { rank: SortingOrder.Descending } })
let newDoc: Data<StateTemplate> = {
ofAttribute: task.attribute.State,
ofAttribute,
name: value.trim(),
rank: calcRank(lastOne, undefined),
...attachedTo
}
if (!hierarchy.isDerived(_class, task.class.DoneState)) {
newDoc = {
ofAttribute: task.attribute.State,
ofAttribute,
name: value.trim(),
color: 9,
rank: calcRank(lastOne, undefined),

View File

@ -14,7 +14,7 @@
// limitations under the License.
-->
<script lang="ts">
import type { Doc, DocumentQuery, IdMap, Ref, Status } from '@hcengineering/core'
import type { Attribute, 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'
@ -24,6 +24,8 @@
import StatesEditor from './StatesEditor.svelte'
export let _id: Ref<SpaceWithStates>
export let ofAttribute: Ref<Attribute<Status>>
export let doneOfAttribute: Ref<Attribute<Status>>
let spaceInstance: SpaceWithStates | undefined
@ -150,13 +152,18 @@
<Scroller>
<div class="popupPanel-body__main-content py-10 clear-mins">
<StatesEditor
{states}
{wonStates}
{lostStates}
on:delete={(e) => deleteState(e.detail)}
on:move={(e) => onMove(e.detail.stateID, e.detail.position)}
/>
{#if spaceInstance}
<StatesEditor
space={_id}
{ofAttribute}
{doneOfAttribute}
{states}
{wonStates}
{lostStates}
on:delete={(e) => deleteState(e.detail)}
on:move={(e) => onMove(e.detail.stateID, e.detail.position)}
/>
{/if}
</div>
</Scroller>
</Panel>

View File

@ -14,12 +14,11 @@
// limitations under the License.
-->
<script lang="ts">
import { Ref } from '@hcengineering/core'
import { Attribute, Ref, Status } from '@hcengineering/core'
import { getClient } from '@hcengineering/presentation'
import type { DoneState, KanbanTemplate, KanbanTemplateSpace, State } from '@hcengineering/task'
import type { DoneState, SpaceWithStates, State } from '@hcengineering/task'
import {
CircleButton,
Component,
IconAdd,
IconCircles,
IconMoreH,
@ -39,8 +38,9 @@
import Won from '../icons/Won.svelte'
import StatusesPopup from './StatusesPopup.svelte'
export let template: KanbanTemplate | undefined = undefined
export let space: KanbanTemplateSpace | undefined = undefined
export let space: Ref<SpaceWithStates>
export let ofAttribute: Ref<Attribute<Status>> = task.attribute.State
export let doneOfAttribute: Ref<Attribute<Status>> = task.attribute.DoneState
export let states: State[] = []
export let wonStates: DoneState[] = []
export let lostStates: DoneState[] = []
@ -86,12 +86,8 @@
await client.updateDoc(state._class, state.space, state._id, { color })
}
const spaceEditor = (space as KanbanTemplateSpace)?.editor
</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
@ -102,15 +98,15 @@
task.component.CreateStatePopup,
{
space,
template,
_class: template !== undefined ? task.class.StateTemplate : task.class.State
ofAttribute,
_class: task.class.State
},
undefined
)
}}
/>
</div>
<div class="flex-col mt-3">
<div class="flex-col flex-no-shrink mt-3">
{#each states as state, i}
{@const color = getPlatformColorDef(state.color ?? getColorNumberByText(state.name), $themeStore.dark)}
{#if state}
@ -155,7 +151,7 @@
onDelete: () => dispatch('delete', { state }),
showDelete: states.length > 1,
onUpdate: () => {
showPopup(task.component.CreateStatePopup, { status: state, template }, undefined)
showPopup(task.component.CreateStatePopup, { status: state, space, ofAttribute }, undefined)
}
},
eventToHTMLElement(ev),
@ -169,7 +165,7 @@
{/if}
{/each}
</div>
<div class="flex-col mt-9">
<div class="flex-col flex-no-shrink mt-9">
<div class="flex-no-shrink flex-between trans-title uppercase">
<Label label={task.string.DoneStatesWon} />
<CircleButton
@ -180,8 +176,8 @@
task.component.CreateStatePopup,
{
space,
template,
_class: template !== undefined ? task.class.WonStateTemplate : task.class.WonState
ofAttribute: doneOfAttribute,
_class: task.class.WonState
},
undefined
)
@ -215,7 +211,11 @@
onDelete: () => dispatch('delete', { state }),
showDelete: wonStates.length > 1,
onUpdate: () => {
showPopup(task.component.CreateStatePopup, { status: state, template }, undefined)
showPopup(
task.component.CreateStatePopup,
{ status: state, space, ofAttribute: doneOfAttribute },
undefined
)
}
},
eventToHTMLElement(ev),
@ -230,7 +230,7 @@
{/each}
</div>
</div>
<div class="mt-9">
<div class="mt-9 flex-no-shrink">
<div class="flex-no-shrink flex-between trans-title uppercase">
<Label label={task.string.DoneStatesLost} />
<CircleButton
@ -241,8 +241,8 @@
task.component.CreateStatePopup,
{
space,
template,
_class: template !== undefined ? task.class.LostStateTemplate : task.class.LostState
ofAttribute: doneOfAttribute,
_class: task.class.LostState
},
undefined
)
@ -276,7 +276,11 @@
onDelete: () => dispatch('delete', { state }),
showDelete: lostStates.length > 1,
onUpdate: () => {
showPopup(task.component.CreateStatePopup, { status: state, template }, undefined)
showPopup(
task.component.CreateStatePopup,
{ status: state, space, ofAttribute: doneOfAttribute },
undefined
)
}
},
eventToHTMLElement(ev),

View File

@ -53,6 +53,7 @@
const dispatch = createEventDispatcher()
const client = getClient()
const hierarchy = client.getHierarchy()
const elements: HTMLElement[] = []
let selected: number | undefined
@ -95,7 +96,11 @@
const spaceEditor = space.editor
function add (_class: Ref<Class<StateTemplate | DoneStateTemplate>>) {
const ofAttribute = hierarchy.isDerived(_class, task.class.DoneStateTemplate)
? space.doneAttribute
: space.ofAttribute
showPopup(task.component.CreateStateTemplatePopup, {
ofAttribute,
space,
template,
_class
@ -103,7 +108,10 @@
}
function edit (status: StateTemplate) {
showPopup(task.component.CreateStateTemplatePopup, { status, template, space })
const ofAttribute = hierarchy.isDerived(status._class, task.class.DoneStateTemplate)
? space.doneAttribute
: space.ofAttribute
showPopup(task.component.CreateStateTemplatePopup, { status, template, space, ofAttribute })
}
</script>

View File

@ -14,38 +14,50 @@
// limitations under the License.
//
import { Attribute, Ref, Status } from '@hcengineering/core'
import { Resources } from '@hcengineering/platform'
import { SpaceWithStates } from '@hcengineering/task'
import { showPopup } from '@hcengineering/ui'
import AssignedTasks from './components/AssignedTasks.svelte'
import CreateStatePopup from './components/CreateStatePopup.svelte'
import CreateStateTemplatePopup from './components/CreateStateTemplatePopup.svelte'
import Dashboard from './components/Dashboard.svelte'
import DueDateEditor from './components/DueDateEditor.svelte'
import KanbanTemplatePresenter from './components/KanbanTemplatePresenter.svelte'
import StatusTableView from './components/StatusTableView.svelte'
import TaskHeader from './components/TaskHeader.svelte'
import TaskPresenter from './components/TaskPresenter.svelte'
import KanbanTemplateEditor from './components/kanban/KanbanTemplateEditor.svelte'
import KanbanTemplateSelector from './components/kanban/KanbanTemplateSelector.svelte'
import KanbanView from './components/kanban/KanbanView.svelte'
import DoneStateEditor from './components/state/DoneStateEditor.svelte'
import DoneStatePresenter from './components/state/DoneStatePresenter.svelte'
import DoneStateRefPresenter from './components/state/DoneStateRefPresenter.svelte'
import EditStatuses from './components/state/EditStatuses.svelte'
import StateEditor from './components/state/StateEditor.svelte'
import StatePresenter from './components/state/StatePresenter.svelte'
import StatusTableView from './components/StatusTableView.svelte'
import TaskHeader from './components/TaskHeader.svelte'
import TaskPresenter from './components/TaskPresenter.svelte'
import KanbanTemplatePresenter from './components/KanbanTemplatePresenter.svelte'
import StateRefPresenter from './components/state/StateRefPresenter.svelte'
import TodoItemPresenter from './components/todos/TodoItemPresenter.svelte'
import TodoItemsPopup from './components/todos/TodoItemsPopup.svelte'
import Todos from './components/todos/Todos.svelte'
import TodoStatePresenter from './components/todos/TodoStatePresenter.svelte'
import Dashboard from './components/Dashboard.svelte'
import DoneStateRefPresenter from './components/state/DoneStateRefPresenter.svelte'
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'
import Todos from './components/todos/Todos.svelte'
export { default as AssigneePresenter } from './components/AssigneePresenter.svelte'
export { StateRefPresenter }
async function editStatuses (object: SpaceWithStates): Promise<void> {
showPopup(EditStatuses, { _id: object._id, spaceClass: object._class }, 'float')
async function editStatuses (
object: SpaceWithStates,
ev: Event,
props: {
ofAttribute: Ref<Attribute<Status>>
doneOfAttribute: Ref<Attribute<Status>>
}
): Promise<void> {
showPopup(
EditStatuses,
{ _id: object._id, ofAttribute: props.ofAttribute, doneOfAttribute: props.doneOfAttribute },
'float'
)
}
export type StatesBarPosition = 'start' | 'middle' | 'end' | undefined

View File

@ -158,6 +158,8 @@ export interface KanbanTemplateSpace extends Space {
name: IntlString
description: IntlString
icon: AnyComponent
ofAttribute: Ref<Attribute<State>>
doneAttribute?: Ref<Attribute<DoneState>>
editor?: AnyComponent
attachedToClass: Ref<Class<Doc>>
}

View File

@ -13,7 +13,17 @@
// limitations under the License.
//
import { Class, Data, DocumentQuery, IdMap, Ref, SortingOrder, Status, TxOperations } from '@hcengineering/core'
import {
Attribute,
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 '.'
@ -86,11 +96,13 @@ export async function createState<T extends Status> (
*/
export async function createStates (
client: TxOperations,
ofAttribute: Ref<Attribute<Status>>,
doneAtrtribute?: Ref<Attribute<DoneState>>,
templateId?: Ref<KanbanTemplate>
): Promise<[Ref<Status>[], Ref<DoneState>[]]> {
if (templateId === undefined) {
const state = await createState(client, task.class.State, {
ofAttribute: task.attribute.State,
ofAttribute,
name: 'New State',
color: 9
})
@ -99,13 +111,13 @@ export async function createStates (
doneStates.push(
await createState(client, task.class.WonState, {
ofAttribute: task.attribute.DoneState,
ofAttribute: doneAtrtribute ?? ofAttribute,
name: 'Won'
})
)
doneStates.push(
await createState(client, task.class.LostState, {
ofAttribute: task.attribute.DoneState,
ofAttribute: doneAtrtribute ?? ofAttribute,
name: 'Lost'
})
)
@ -131,7 +143,7 @@ export async function createStates (
for (const state of tmplStates) {
states.push(
await createState(client, task.class.State, {
ofAttribute: task.attribute.State,
ofAttribute,
color: state.color,
description: state.description,
name: state.name
@ -157,7 +169,7 @@ export async function createStates (
doneStates.push(
await createState(client, cl, {
ofAttribute: task.attribute.DoneState,
ofAttribute: doneAtrtribute ?? ofAttribute,
description: state.description,
name: state.name
})

View File

@ -55,7 +55,6 @@
clip-rule="evenodd"
d="M8 14C11.3137 14 14 11.3137 14 8C14 4.68629 11.3137 2 8 2C4.68629 2 2 4.68629 2 8C2 11.3137 4.68629 14 8 14ZM8 15C11.866 15 15 11.866 15 8C15 4.13401 11.866 1 8 1C4.13401 1 1 4.13401 1 8C1 11.866 4.13401 15 8 15Z"
/>
{#if statusIcon.count && statusIcon.index}
<path
d="M 4.5 4.5 L 9 4.5 A 4.5 4.5 0 {statusIcon.index > (statusIcon.count - 1) / 2 ? 1 : 0} 1 {Math.cos(
@ -66,7 +65,11 @@
transform="translate(3.5,3.5)"
/>
{:else}
<circle cx="8" cy="8" r="4" fill="var(--theme-error-color)" opacity=".15" />
<path
d="M 4.5 4.5 L 9 4.5 A 4.5 4.5 0 1 1 {Math.cos(Math.PI - 0.01) * 4.5 + 4.5} {Math.sin(Math.PI - 0.01) * 4.5 +
4.5} Z"
transform="translate(3.5,3.5)"
/>
{/if}
{:else if category._id === tracker.issueStatusCategory.Completed}
<path

View File

@ -49,7 +49,7 @@
statuses = getStates(_space, $statusStore).filter((p) => p.category === tracker.issueStatusCategory.Started)
}
async function updateCategory (status: WithLookup<IssueStatus>, statuses: IssueStatus[]) {
async function updateCategory (_space: Project | undefined, status: WithLookup<IssueStatus>, statuses: IssueStatus[]) {
if (status.$lookup?.category) {
category = status.$lookup.category
}
@ -68,7 +68,7 @@
}
}
$: updateCategory(value, statuses)
$: updateCategory(_space, value, statuses)
$: icon = category?.icon
$: color = value.color !== undefined ? value.color : category !== undefined ? category.color : -1
</script>

View File

@ -117,7 +117,7 @@
return {
id: s._id,
component: StatusPresenter,
props: { value: s, size: 'small' },
props: { value: s, size: 'small', space: value.space },
isSelected: selectedStatus?._id === s._id ?? false
}
})

View File

@ -46,7 +46,7 @@
</script>
<div class="flex-presenter flex-gap-1-5">
{#each statuses as value, i}
{#each statuses as value, i (value._id)}
{#if value && i < 5}
<div>
<IssueStatusIcon {space} {value} size={'small'} />

View File

@ -13,27 +13,27 @@
// limitations under the License.
-->
<script lang="ts">
import core, { Doc, FindResult, IdMap, Ref, RefTo, Space, Status } from '@hcengineering/core'
import core, { Doc, FindResult, IdMap, Ref, RefTo, Space, Status, toIdMap } 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,
addNotification,
deviceOptionsStore,
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 '../..'
import view from '../../plugin'
import { buildConfigLookup, getPresenter } from '../../utils'
export let filter: Filter
export let space: Ref<Space> | undefined = undefined
@ -76,11 +76,11 @@
}
if (space !== undefined) {
const _space = await client.findOne(core.class.Space, { _id: space })
const _space = await client.findOne(task.class.SpaceWithStates, { _id: space as Ref<SpaceWithStates> })
if (_space) {
values = (_space as any)[filter.key.key]
.map((p: Ref<Status>) => statusStore.get(p))
.filter((p: Status) => p !== undefined)
const targetClass = (filter.key.attribute.type as RefTo<Status>).to
const key = hierarchy.isDerived(targetClass, task.class.DoneState) ? 'doneStates' : 'states'
values = (_space as any)[key].map((p: Ref<Status>) => statusStore.get(p)).filter((p: Status) => p !== undefined)
for (const value of values) {
targets.add(value?._id)
}
@ -89,13 +89,14 @@
}
}
} else {
values = []
const statuses: Status[] = []
for (const status of statusStore.values()) {
if (hierarchy.isDerived(status._class, targetClass)) {
values.push(status)
if (hierarchy.isDerived(status._class, targetClass) && status.ofAttribute === filter.key.attribute._id) {
statuses.push(status)
targets.add(status._id)
}
}
values = await sort(statuses)
}
if (targets.has(undefined)) {
values.unshift(undefined)
@ -120,6 +121,26 @@
objectsPromise = undefined
}
async function sort (statuses: Status[]): Promise<Status[]> {
const categories = toIdMap(await client.findAll(core.class.StatusCategory, {}))
statuses.sort((a, b) => {
if (a.category !== undefined && b.category !== undefined && a.category !== b.category) {
const aCat = categories.get(a.category)
const bCat = categories.get(b.category)
if (aCat !== undefined && bCat !== undefined) {
return aCat.order - bCat.order
}
}
if (_space != null) {
const aIndex = _space.states.findIndex((s) => s === a._id)
const bIndex = _space.states.findIndex((s) => s === b._id)
return aIndex - bIndex
}
return a.name.localeCompare(b.name)
})
return statuses
}
function isSelected (value: Doc | undefined | null, values: any[]): boolean {
return values.includes(value?._id ?? value)
}
@ -182,7 +203,14 @@
<div class="flex-row-center">
{#if value}
{#key value._id}
<svelte:component this={attribute.presenter} {value} {...attribute.props} disabled oneLine />
<svelte:component
this={attribute.presenter}
{value}
{...attribute.props}
{space}
disabled
oneLine
/>
{/key}
{:else}
<Label label={ui.string.NotSelected} />

View File

@ -71,7 +71,7 @@
<SearchEdit bind:value={search} on:change={() => dispatch('search', search)} />
<!-- <ActionIcon icon={IconMoreH} size={'small'} /> -->
<div class="buttons-divider" />
<FilterButton {_class} />
<FilterButton {_class} space={spaceId} />
</div>
<div class="ac-header-full medium-gap">
<ViewletSettingButton bind:viewOptions bind:viewlet />