Skip to content

Commit

Permalink
Tracker: move "IssueStatus" enum into model (#1449)
Browse files Browse the repository at this point in the history
Signed-off-by: Sergei Ogorelkov <sergei.ogorelkov@xored.com>
  • Loading branch information
Sergei Ogorelkov authored Apr 23, 2022
1 parent 51687d1 commit d663e38
Show file tree
Hide file tree
Showing 25 changed files with 5,864 additions and 3,151 deletions.
8,203 changes: 5,257 additions & 2,946 deletions common/config/rush/pnpm-lock.yaml

Large diffs are not rendered by default.

4 changes: 0 additions & 4 deletions models/task/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -468,10 +468,6 @@ export function createModel (builder: Builder): void {
task.viewlet.Kanban
)

builder.mixin(task.class.DoneState, core.class.Class, view.mixin.AttributePresenter, {
presenter: task.component.DoneStatePresenter
})

builder.mixin(task.class.TodoItem, core.class.Class, view.mixin.AttributeEditor, {
editor: task.component.Todos
})
Expand Down
114 changes: 107 additions & 7 deletions models/tracker/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@

import type { Employee } from '@anticrm/contact'
import contact from '@anticrm/contact'
import { Domain, IndexKind, Markup, Ref, Timestamp } from '@anticrm/core'
import { Domain, DOMAIN_MODEL, IndexKind, Markup, Ref, Timestamp } from '@anticrm/core'
import {
Builder,
Collection,
Expand All @@ -32,9 +32,9 @@ import {
} from '@anticrm/model'
import attachment from '@anticrm/model-attachment'
import chunter from '@anticrm/model-chunter'
import core, { DOMAIN_SPACE, TDoc, TSpace } from '@anticrm/model-core'
import { IntlString } from '@anticrm/platform'
import { Document, Issue, IssuePriority, IssueStatus, Team } from '@anticrm/tracker'
import core, { DOMAIN_SPACE, TAttachedDoc, TDoc, TSpace } from '@anticrm/model-core'
import { Asset, IntlString } from '@anticrm/platform'
import { Document, Issue, IssuePriority, IssueStatus, IssueStatusCategory, Team } from '@anticrm/tracker'
import tracker from './plugin'

import workbench from '@anticrm/model-workbench'
Expand All @@ -44,6 +44,35 @@ export { default } from './plugin'

export const DOMAIN_TRACKER = 'tracker' as Domain

/**
* @public
*/
@Model(tracker.class.IssueStatus, core.class.AttachedDoc, DOMAIN_TRACKER)
export class TIssueStatus extends TAttachedDoc implements IssueStatus {
name!: string
description?: string
color?: number

@Prop(TypeRef(tracker.class.IssueStatusCategory), tracker.string.StatusCategory)
category!: Ref<IssueStatusCategory>

@Prop(TypeString(), tracker.string.Rank)
@Hidden()
rank!: string
}

/**
* @public
*/
@Model(tracker.class.IssueStatusCategory, core.class.Doc, DOMAIN_MODEL)
export class TIssueStatusCategory extends TDoc implements IssueStatusCategory {
label!: IntlString
icon!: Asset
color!: number
defaultStatusName!: string
order!: number
}

/**
* @public
*/
Expand All @@ -61,6 +90,12 @@ export class TTeam extends TSpace implements Team {
@Prop(TypeNumber(), tracker.string.Number)
@Hidden()
sequence!: number

@Prop(Collection(tracker.class.IssueStatus), tracker.string.IssueStatuses)
issueStatuses!: number

@Prop(TypeRef(tracker.class.IssueStatus), tracker.string.DefaultIssueStatus)
defaultIssueStatus!: Ref<IssueStatus>
}

/**
Expand All @@ -77,8 +112,8 @@ export class TIssue extends TDoc implements Issue {
@Index(IndexKind.FullText)
description!: Markup

@Prop(TypeNumber(), tracker.string.Status)
status!: IssueStatus
@Prop(TypeRef(tracker.class.IssueStatus), tracker.string.Status)
status!: Ref<IssueStatus>

@Prop(TypeNumber(), tracker.string.Priority)
priority!: IssuePriority
Expand Down Expand Up @@ -141,7 +176,72 @@ export class TDocument extends TDoc implements Document {
}

export function createModel (builder: Builder): void {
builder.createModel(TTeam, TIssue)
builder.createModel(TTeam, TIssue, TIssueStatus, TIssueStatusCategory)

builder.createDoc(
tracker.class.IssueStatusCategory,
core.space.Model,
{
label: tracker.string.CategoryBacklog,
icon: tracker.icon.CategoryBacklog,
color: 0,
defaultStatusName: 'Backlog',
order: 0
},
tracker.issueStatusCategory.Backlog
)

builder.createDoc(
tracker.class.IssueStatusCategory,
core.space.Model,
{
label: tracker.string.CategoryUnstarted,
icon: tracker.icon.CategoryUnstarted,
color: 1,
defaultStatusName: 'Todo',
order: 1
},
tracker.issueStatusCategory.Unstarted
)

builder.createDoc(
tracker.class.IssueStatusCategory,
core.space.Model,
{
label: tracker.string.CategoryStarted,
icon: tracker.icon.CategoryStarted,
color: 2,
defaultStatusName: 'In Progress',
order: 2
},
tracker.issueStatusCategory.Started
)

builder.createDoc(
tracker.class.IssueStatusCategory,
core.space.Model,
{
label: tracker.string.CategoryCompleted,
icon: tracker.icon.CategoryCompleted,
color: 3,
defaultStatusName: 'Done',
order: 3
},
tracker.issueStatusCategory.Completed
)

builder.createDoc(
tracker.class.IssueStatusCategory,
core.space.Model,
{
label: tracker.string.CategoryCanceled,
icon: tracker.icon.CategoryCanceled,
color: 4,
defaultStatusName: 'Canceled',
order: 4
},
tracker.issueStatusCategory.Canceled
)

builder.createDoc(
workbench.class.Application,
Expand Down
118 changes: 115 additions & 3 deletions models/tracker/src/migration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,60 @@
// limitations under the License.
//

import core, { TxOperations } from '@anticrm/core'
import core, { generateId, Ref, TxOperations } from '@anticrm/core'
import { MigrateOperation, MigrationClient, MigrationUpgradeClient } from '@anticrm/model'
import { Team } from '@anticrm/tracker'
import { IssueStatus, IssueStatusCategory, Team, genRanks } from '@anticrm/tracker'
import tracker from './plugin'

enum DeprecatedIssueStatus {
Backlog,
Todo,
InProgress,
Done,
Canceled
}

interface CreateTeamIssueStatusesArgs {
tx: TxOperations
teamId: Ref<Team>
categories: IssueStatusCategory[]
defaultStatusId?: Ref<IssueStatus>
defaultCategoryId?: Ref<IssueStatusCategory>
}

const categoryByDeprecatedIssueStatus = {
[DeprecatedIssueStatus.Backlog]: tracker.issueStatusCategory.Backlog,
[DeprecatedIssueStatus.Todo]: tracker.issueStatusCategory.Unstarted,
[DeprecatedIssueStatus.InProgress]: tracker.issueStatusCategory.Started,
[DeprecatedIssueStatus.Done]: tracker.issueStatusCategory.Completed,
[DeprecatedIssueStatus.Canceled]: tracker.issueStatusCategory.Canceled
} as const

async function createTeamIssueStatuses ({
tx,
teamId: attachedTo,
categories,
defaultStatusId,
defaultCategoryId = tracker.issueStatusCategory.Backlog
}: CreateTeamIssueStatusesArgs): Promise<void> {
const issueStatusRanks = [...genRanks(categories.length)]

for (const [i, statusCategory] of categories.entries()) {
const { _id: category, defaultStatusName } = statusCategory
const rank = issueStatusRanks[i]

await tx.addCollection(
tracker.class.IssueStatus,
attachedTo,
attachedTo,
tracker.class.Team,
'issueStatuses',
{ name: defaultStatusName, category, rank },
category === defaultCategoryId ? defaultStatusId : undefined
)
}
}

async function createDefaultTeam (tx: TxOperations): Promise<void> {
const current = await tx.findOne(tracker.class.Team, {
_id: tracker.team.DefaultTeam
Expand All @@ -29,6 +78,9 @@ async function createDefaultTeam (tx: TxOperations): Promise<void> {

// Create new if not deleted by customers.
if (current === undefined && currentDeleted === undefined) {
const defaultStatusId: Ref<IssueStatus> = generateId()
const categories = await tx.findAll(tracker.class.IssueStatusCategory, {})

await tx.createDoc<Team>(
tracker.class.Team,
core.space.Space,
Expand All @@ -39,21 +91,81 @@ async function createDefaultTeam (tx: TxOperations): Promise<void> {
members: [],
archived: false,
identifier: 'TSK',
sequence: 0
sequence: 0,
issueStatuses: 0,
defaultIssueStatus: defaultStatusId
},
tracker.team.DefaultTeam
)
await createTeamIssueStatuses({ tx, teamId: tracker.team.DefaultTeam, categories, defaultStatusId })
}
}

async function upgradeTeamIssueStatuses (tx: TxOperations): Promise<void> {
const teams = await tx.findAll(tracker.class.Team, { issueStatuses: undefined })

if (teams.length > 0) {
const categories = await tx.findAll(tracker.class.IssueStatusCategory, {})

for (const team of teams) {
const defaultStatusId: Ref<IssueStatus> = generateId()

await tx.update(team, { issueStatuses: 0, defaultIssueStatus: defaultStatusId })
await createTeamIssueStatuses({ tx, teamId: team._id, categories, defaultStatusId })
}
}
}

async function upgradeIssueStatuses (tx: TxOperations): Promise<void> {
const deprecatedStatuses = [
DeprecatedIssueStatus.Backlog,
DeprecatedIssueStatus.Canceled,
DeprecatedIssueStatus.Done,
DeprecatedIssueStatus.InProgress,
DeprecatedIssueStatus.Todo
]
const issues = await tx.findAll(tracker.class.Issue, { status: { $in: deprecatedStatuses as any } })

if (issues.length > 0) {
const statusByDeprecatedStatus = new Map<DeprecatedIssueStatus, Ref<IssueStatus>>()

for (const issue of issues) {
const deprecatedStatus = issue.status as unknown as DeprecatedIssueStatus

if (!statusByDeprecatedStatus.has(deprecatedStatus)) {
const category = categoryByDeprecatedIssueStatus[deprecatedStatus]
const issueStatus = await tx.findOne(tracker.class.IssueStatus, { category })

if (issueStatus === undefined) {
throw new Error(`Could not find a new status for "${DeprecatedIssueStatus[deprecatedStatus]}"`)
}

statusByDeprecatedStatus.set(deprecatedStatus, issueStatus._id)
}

await tx.update(issue, { status: statusByDeprecatedStatus.get(deprecatedStatus) })
}
}
}

async function createDefaults (tx: TxOperations): Promise<void> {
await createDefaultTeam(tx)
}

async function upgradeTeams (tx: TxOperations): Promise<void> {
await upgradeTeamIssueStatuses(tx)
}

async function upgradeIssues (tx: TxOperations): Promise<void> {
await upgradeIssueStatuses(tx)
}

export const trackerOperation: MigrateOperation = {
async migrate (client: MigrationClient): Promise<void> {},
async upgrade (client: MigrationUpgradeClient): Promise<void> {
const tx = new TxOperations(client, core.account.System)
await createDefaults(tx)
await upgradeTeams(tx)
await upgradeIssues(tx)
}
}
6 changes: 2 additions & 4 deletions packages/kanban/src/components/Kanban.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,12 @@
import core, { AttachedDoc, Class, Doc, DocumentQuery, DocumentUpdate, FindOptions, Ref, Space } from '@anticrm/core'
import { createQuery, getClient } from '@anticrm/presentation'
import { getPlatformColor, ScrollBox } from '@anticrm/ui'
import { createEventDispatcher, tick } from 'svelte'
import { createEventDispatcher } from 'svelte'
import { slide } from 'svelte/transition'
import { DocWithRank } from '../types'
import { DocWithRank, StateType, TypeState } from '../types'
import { calcRank } from '../utils'
type StateType = any
type Item = DocWithRank & { state: StateType; doneState: StateType | null }
type TypeState = { _id: StateType; title: string; color: number }
type ExtItem = { prev?: Item; it: Item; next?: Item, pos: number }
type CardDragEvent = DragEvent & { currentTarget: EventTarget & HTMLDivElement }
Expand Down
11 changes: 11 additions & 0 deletions packages/kanban/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,14 @@ import { Doc } from '@anticrm/core'
export interface DocWithRank extends Doc {
rank: string
}

export type StateType = any

/**
* @public
*/
export interface TypeState {
_id: StateType
title: string
color: number
}
12 changes: 9 additions & 3 deletions packages/ui/src/components/SelectPopup.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
export let placeholder: IntlString | undefined = undefined
export let placeholderParam: any | undefined = undefined
export let searchable: boolean = false
export let value: Array<{id: number | string, icon: Asset, label: IntlString}>
export let value: Array<{id: number | string, icon: Asset, label?: IntlString, text?: string}>
let search: string = ''
Expand All @@ -39,10 +39,16 @@
{/if}
<div class="scroll">
<div class="box">
{#each value.filter(el => el.label.toLowerCase().includes(search.toLowerCase())) as item}
{#each value.filter(el => (el.label ?? el.text ?? '').toLowerCase().includes(search.toLowerCase())) as item}
<button class="menu-item" on:click={() => { dispatch('close', item.id) }}>
<div class="icon"><Icon icon={item.icon} size={'small'} /></div>
<span class="label"><Label label={item.label} /></span>
<span class="label">
{#if item.label}
<Label label={item.label} />
{:else if item.text}
<span>{item.text}</span>
{/if}
</span>
</button>
{/each}
</div>
Expand Down
Loading

0 comments on commit d663e38

Please sign in to comment.