mirror of
https://github.com/hcengineering/platform.git
synced 2025-04-15 21:03:30 +00:00
Tracker: move "IssueStatus" enum into model (#1449)
Signed-off-by: Sergei Ogorelkov <sergei.ogorelkov@xored.com>
This commit is contained in:
parent
51687d1a2b
commit
d663e383e6
File diff suppressed because it is too large
Load Diff
@ -468,10 +468,6 @@ export function createModel (builder: Builder): void {
|
|||||||
task.viewlet.Kanban
|
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, {
|
builder.mixin(task.class.TodoItem, core.class.Class, view.mixin.AttributeEditor, {
|
||||||
editor: task.component.Todos
|
editor: task.component.Todos
|
||||||
})
|
})
|
||||||
|
@ -15,7 +15,7 @@
|
|||||||
|
|
||||||
import type { Employee } from '@anticrm/contact'
|
import type { Employee } from '@anticrm/contact'
|
||||||
import contact 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 {
|
import {
|
||||||
Builder,
|
Builder,
|
||||||
Collection,
|
Collection,
|
||||||
@ -32,9 +32,9 @@ import {
|
|||||||
} from '@anticrm/model'
|
} from '@anticrm/model'
|
||||||
import attachment from '@anticrm/model-attachment'
|
import attachment from '@anticrm/model-attachment'
|
||||||
import chunter from '@anticrm/model-chunter'
|
import chunter from '@anticrm/model-chunter'
|
||||||
import core, { DOMAIN_SPACE, TDoc, TSpace } from '@anticrm/model-core'
|
import core, { DOMAIN_SPACE, TAttachedDoc, TDoc, TSpace } from '@anticrm/model-core'
|
||||||
import { IntlString } from '@anticrm/platform'
|
import { Asset, IntlString } from '@anticrm/platform'
|
||||||
import { Document, Issue, IssuePriority, IssueStatus, Team } from '@anticrm/tracker'
|
import { Document, Issue, IssuePriority, IssueStatus, IssueStatusCategory, Team } from '@anticrm/tracker'
|
||||||
import tracker from './plugin'
|
import tracker from './plugin'
|
||||||
|
|
||||||
import workbench from '@anticrm/model-workbench'
|
import workbench from '@anticrm/model-workbench'
|
||||||
@ -44,6 +44,35 @@ export { default } from './plugin'
|
|||||||
|
|
||||||
export const DOMAIN_TRACKER = 'tracker' as Domain
|
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
|
* @public
|
||||||
*/
|
*/
|
||||||
@ -61,6 +90,12 @@ export class TTeam extends TSpace implements Team {
|
|||||||
@Prop(TypeNumber(), tracker.string.Number)
|
@Prop(TypeNumber(), tracker.string.Number)
|
||||||
@Hidden()
|
@Hidden()
|
||||||
sequence!: number
|
sequence!: number
|
||||||
|
|
||||||
|
@Prop(Collection(tracker.class.IssueStatus), tracker.string.IssueStatuses)
|
||||||
|
issueStatuses!: number
|
||||||
|
|
||||||
|
@Prop(TypeRef(tracker.class.IssueStatus), tracker.string.DefaultIssueStatus)
|
||||||
|
defaultIssueStatus!: Ref<IssueStatus>
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -77,8 +112,8 @@ export class TIssue extends TDoc implements Issue {
|
|||||||
@Index(IndexKind.FullText)
|
@Index(IndexKind.FullText)
|
||||||
description!: Markup
|
description!: Markup
|
||||||
|
|
||||||
@Prop(TypeNumber(), tracker.string.Status)
|
@Prop(TypeRef(tracker.class.IssueStatus), tracker.string.Status)
|
||||||
status!: IssueStatus
|
status!: Ref<IssueStatus>
|
||||||
|
|
||||||
@Prop(TypeNumber(), tracker.string.Priority)
|
@Prop(TypeNumber(), tracker.string.Priority)
|
||||||
priority!: IssuePriority
|
priority!: IssuePriority
|
||||||
@ -141,7 +176,72 @@ export class TDocument extends TDoc implements Document {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function createModel (builder: Builder): void {
|
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(
|
builder.createDoc(
|
||||||
workbench.class.Application,
|
workbench.class.Application,
|
||||||
|
@ -13,11 +13,60 @@
|
|||||||
// limitations under the License.
|
// 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 { MigrateOperation, MigrationClient, MigrationUpgradeClient } from '@anticrm/model'
|
||||||
import { Team } from '@anticrm/tracker'
|
import { IssueStatus, IssueStatusCategory, Team, genRanks } from '@anticrm/tracker'
|
||||||
import tracker from './plugin'
|
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> {
|
async function createDefaultTeam (tx: TxOperations): Promise<void> {
|
||||||
const current = await tx.findOne(tracker.class.Team, {
|
const current = await tx.findOne(tracker.class.Team, {
|
||||||
_id: tracker.team.DefaultTeam
|
_id: tracker.team.DefaultTeam
|
||||||
@ -29,6 +78,9 @@ async function createDefaultTeam (tx: TxOperations): Promise<void> {
|
|||||||
|
|
||||||
// Create new if not deleted by customers.
|
// Create new if not deleted by customers.
|
||||||
if (current === undefined && currentDeleted === undefined) {
|
if (current === undefined && currentDeleted === undefined) {
|
||||||
|
const defaultStatusId: Ref<IssueStatus> = generateId()
|
||||||
|
const categories = await tx.findAll(tracker.class.IssueStatusCategory, {})
|
||||||
|
|
||||||
await tx.createDoc<Team>(
|
await tx.createDoc<Team>(
|
||||||
tracker.class.Team,
|
tracker.class.Team,
|
||||||
core.space.Space,
|
core.space.Space,
|
||||||
@ -39,10 +91,60 @@ async function createDefaultTeam (tx: TxOperations): Promise<void> {
|
|||||||
members: [],
|
members: [],
|
||||||
archived: false,
|
archived: false,
|
||||||
identifier: 'TSK',
|
identifier: 'TSK',
|
||||||
sequence: 0
|
sequence: 0,
|
||||||
|
issueStatuses: 0,
|
||||||
|
defaultIssueStatus: defaultStatusId
|
||||||
},
|
},
|
||||||
tracker.team.DefaultTeam
|
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) })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -50,10 +152,20 @@ async function createDefaults (tx: TxOperations): Promise<void> {
|
|||||||
await createDefaultTeam(tx)
|
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 = {
|
export const trackerOperation: MigrateOperation = {
|
||||||
async migrate (client: MigrationClient): Promise<void> {},
|
async migrate (client: MigrationClient): Promise<void> {},
|
||||||
async upgrade (client: MigrationUpgradeClient): Promise<void> {
|
async upgrade (client: MigrationUpgradeClient): Promise<void> {
|
||||||
const tx = new TxOperations(client, core.account.System)
|
const tx = new TxOperations(client, core.account.System)
|
||||||
await createDefaults(tx)
|
await createDefaults(tx)
|
||||||
|
await upgradeTeams(tx)
|
||||||
|
await upgradeIssues(tx)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -16,14 +16,12 @@
|
|||||||
import core, { AttachedDoc, Class, Doc, DocumentQuery, DocumentUpdate, FindOptions, Ref, Space } from '@anticrm/core'
|
import core, { AttachedDoc, Class, Doc, DocumentQuery, DocumentUpdate, FindOptions, Ref, Space } from '@anticrm/core'
|
||||||
import { createQuery, getClient } from '@anticrm/presentation'
|
import { createQuery, getClient } from '@anticrm/presentation'
|
||||||
import { getPlatformColor, ScrollBox } from '@anticrm/ui'
|
import { getPlatformColor, ScrollBox } from '@anticrm/ui'
|
||||||
import { createEventDispatcher, tick } from 'svelte'
|
import { createEventDispatcher } from 'svelte'
|
||||||
import { slide } from 'svelte/transition'
|
import { slide } from 'svelte/transition'
|
||||||
import { DocWithRank } from '../types'
|
import { DocWithRank, StateType, TypeState } from '../types'
|
||||||
import { calcRank } from '../utils'
|
import { calcRank } from '../utils'
|
||||||
|
|
||||||
type StateType = any
|
|
||||||
type Item = DocWithRank & { state: StateType; doneState: StateType | null }
|
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 ExtItem = { prev?: Item; it: Item; next?: Item, pos: number }
|
||||||
type CardDragEvent = DragEvent & { currentTarget: EventTarget & HTMLDivElement }
|
type CardDragEvent = DragEvent & { currentTarget: EventTarget & HTMLDivElement }
|
||||||
|
|
||||||
|
@ -6,3 +6,14 @@ import { Doc } from '@anticrm/core'
|
|||||||
export interface DocWithRank extends Doc {
|
export interface DocWithRank extends Doc {
|
||||||
rank: string
|
rank: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type StateType = any
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
export interface TypeState {
|
||||||
|
_id: StateType
|
||||||
|
title: string
|
||||||
|
color: number
|
||||||
|
}
|
||||||
|
@ -21,7 +21,7 @@
|
|||||||
export let placeholder: IntlString | undefined = undefined
|
export let placeholder: IntlString | undefined = undefined
|
||||||
export let placeholderParam: any | undefined = undefined
|
export let placeholderParam: any | undefined = undefined
|
||||||
export let searchable: boolean = false
|
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 = ''
|
let search: string = ''
|
||||||
|
|
||||||
@ -39,10 +39,16 @@
|
|||||||
{/if}
|
{/if}
|
||||||
<div class="scroll">
|
<div class="scroll">
|
||||||
<div class="box">
|
<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) }}>
|
<button class="menu-item" on:click={() => { dispatch('close', item.id) }}>
|
||||||
<div class="icon"><Icon icon={item.icon} size={'small'} /></div>
|
<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>
|
</button>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
@ -21,10 +21,6 @@
|
|||||||
"AddIssue": "Add Issue",
|
"AddIssue": "Add Issue",
|
||||||
"NewIssue": "New issue",
|
"NewIssue": "New issue",
|
||||||
"SaveIssue": "Save issue",
|
"SaveIssue": "Save issue",
|
||||||
"Todo": "Todo",
|
|
||||||
"InProgress": "In Progress",
|
|
||||||
"Done": "Done",
|
|
||||||
"Canceled": "Canceled",
|
|
||||||
"SetPriority": "Set priority\u2026",
|
"SetPriority": "Set priority\u2026",
|
||||||
"SetStatus": "Set status\u2026",
|
"SetStatus": "Set status\u2026",
|
||||||
"SelectIssue": "Select issue",
|
"SelectIssue": "Select issue",
|
||||||
@ -36,6 +32,12 @@
|
|||||||
"Low": "Low",
|
"Low": "Low",
|
||||||
"Unassigned": "Unassigned",
|
"Unassigned": "Unassigned",
|
||||||
|
|
||||||
|
"CategoryBacklog": "Backlog",
|
||||||
|
"CategoryUnstarted": "Unstarted",
|
||||||
|
"CategoryStarted": "Started",
|
||||||
|
"CategoryCompleted": "Completed",
|
||||||
|
"CategoryCanceled": "Canceled",
|
||||||
|
|
||||||
"Title": "Title",
|
"Title": "Title",
|
||||||
"Description": "",
|
"Description": "",
|
||||||
"Status": "Status",
|
"Status": "Status",
|
||||||
|
@ -36,11 +36,11 @@ loadMetadata(tracker.icon, {
|
|||||||
DueDate: `${icons}#inbox`, // TODO: add icon
|
DueDate: `${icons}#inbox`, // TODO: add icon
|
||||||
Parent: `${icons}#myissues`, // TODO: add icon
|
Parent: `${icons}#myissues`, // TODO: add icon
|
||||||
|
|
||||||
StatusBacklog: `${icons}#status-backlog`,
|
CategoryBacklog: `${icons}#status-backlog`,
|
||||||
StatusTodo: `${icons}#status-todo`,
|
CategoryUnstarted: `${icons}#status-todo`,
|
||||||
StatusInProgress: `${icons}#status-inprogress`,
|
CategoryStarted: `${icons}#status-inprogress`,
|
||||||
StatusDone: `${icons}#status-done`,
|
CategoryCompleted: `${icons}#status-done`,
|
||||||
StatusCanceled: `${icons}#status-canceled`,
|
CategoryCanceled: `${icons}#status-canceled`,
|
||||||
PriorityNoPriority: `${icons}#priority-nopriority`,
|
PriorityNoPriority: `${icons}#priority-nopriority`,
|
||||||
PriorityUrgent: `${icons}#priority-urgent`,
|
PriorityUrgent: `${icons}#priority-urgent`,
|
||||||
PriorityHigh: `${icons}#priority-high`,
|
PriorityHigh: `${icons}#priority-high`,
|
||||||
|
@ -47,7 +47,6 @@
|
|||||||
"@anticrm/notification-resources": "~0.6.0",
|
"@anticrm/notification-resources": "~0.6.0",
|
||||||
"@anticrm/contact": "~0.6.5",
|
"@anticrm/contact": "~0.6.5",
|
||||||
"@anticrm/view-resources": "~0.6.0",
|
"@anticrm/view-resources": "~0.6.0",
|
||||||
"lexorank": "~1.0.4",
|
|
||||||
"@anticrm/text-editor": "~0.6.0",
|
"@anticrm/text-editor": "~0.6.0",
|
||||||
"@anticrm/panel": "~0.6.0",
|
"@anticrm/panel": "~0.6.0",
|
||||||
"@anticrm/kanban": "~0.6.0"
|
"@anticrm/kanban": "~0.6.0"
|
||||||
|
@ -14,28 +14,25 @@
|
|||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import contact, { Employee } from '@anticrm/contact'
|
import contact, { Employee } from '@anticrm/contact'
|
||||||
import core, { Data, generateId, Ref, SortingOrder } from '@anticrm/core'
|
import core, { Data, generateId, Ref, SortingOrder, WithLookup } from '@anticrm/core'
|
||||||
import { Asset, IntlString } from '@anticrm/platform'
|
import { Asset, IntlString } from '@anticrm/platform'
|
||||||
import presentation, { getClient, UserBox, Card, MessageBox } from '@anticrm/presentation'
|
import presentation, { getClient, UserBox, Card, createQuery } from '@anticrm/presentation'
|
||||||
import { Issue, IssuePriority, IssueStatus, Team } from '@anticrm/tracker'
|
import { Issue, IssuePriority, IssueStatus, Team, calcRank } from '@anticrm/tracker'
|
||||||
import { StyledTextBox } from '@anticrm/text-editor'
|
import { StyledTextBox } from '@anticrm/text-editor'
|
||||||
import { EditBox, Button, showPopup, DatePresenter, SelectPopup, IconAttachment, eventToHTMLElement } from '@anticrm/ui'
|
import { EditBox, Button, showPopup, DatePresenter, SelectPopup, IconAttachment, eventToHTMLElement } from '@anticrm/ui'
|
||||||
import { createEventDispatcher } from 'svelte'
|
import { createEventDispatcher } from 'svelte'
|
||||||
import tracker from '../plugin'
|
import tracker from '../plugin'
|
||||||
import { calcRank } from '../utils'
|
|
||||||
import StatusSelector from './StatusSelector.svelte'
|
import StatusSelector from './StatusSelector.svelte'
|
||||||
import PrioritySelector from './PrioritySelector.svelte'
|
import PrioritySelector from './PrioritySelector.svelte'
|
||||||
|
|
||||||
export let space: Ref<Team>
|
export let space: Ref<Team>
|
||||||
export let parent: Ref<Issue> | undefined
|
export let parent: Ref<Issue> | undefined
|
||||||
export let status: IssueStatus = IssueStatus.Backlog
|
export let issueStatus: Ref<IssueStatus> | undefined = undefined
|
||||||
export let priority: IssuePriority = IssuePriority.NoPriority
|
export let priority: IssuePriority = IssuePriority.NoPriority
|
||||||
export let assignee: Ref<Employee> | null = null
|
export let assignee: Ref<Employee> | null = null
|
||||||
|
|
||||||
$: _space = space
|
|
||||||
$: _parent = parent
|
|
||||||
|
|
||||||
let currentAssignee: Ref<Employee> | null = assignee
|
let currentAssignee: Ref<Employee> | null = assignee
|
||||||
|
let issueStatuses: WithLookup<IssueStatus>[] = []
|
||||||
|
|
||||||
const object: Data<Issue> = {
|
const object: Data<Issue> = {
|
||||||
title: '',
|
title: '',
|
||||||
@ -43,7 +40,7 @@
|
|||||||
assignee: null,
|
assignee: null,
|
||||||
number: 0,
|
number: 0,
|
||||||
rank: '',
|
rank: '',
|
||||||
status: status,
|
status: '' as Ref<IssueStatus>,
|
||||||
priority: priority,
|
priority: priority,
|
||||||
dueDate: null,
|
dueDate: null,
|
||||||
comments: 0
|
comments: 0
|
||||||
@ -51,8 +48,37 @@
|
|||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
const client = getClient()
|
const client = getClient()
|
||||||
|
const statusesQuery = createQuery()
|
||||||
const taskId: Ref<Issue> = generateId()
|
const taskId: Ref<Issue> = generateId()
|
||||||
|
|
||||||
|
$: _space = space
|
||||||
|
$: _parent = parent
|
||||||
|
$: updateIssueStatusId(space, issueStatus)
|
||||||
|
$: statusesQuery.query(tracker.class.IssueStatus, { attachedTo: space }, (statuses) => {
|
||||||
|
issueStatuses = statuses
|
||||||
|
}, {
|
||||||
|
lookup: { category: tracker.class.IssueStatusCategory },
|
||||||
|
sort: { rank: SortingOrder.Ascending }
|
||||||
|
})
|
||||||
|
|
||||||
|
async function updateIssueStatusId (teamId: Ref<Team>, status?: Ref<IssueStatus>) {
|
||||||
|
if (status !== undefined) {
|
||||||
|
object.status = status
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const team = await client.findOne(
|
||||||
|
tracker.class.Team,
|
||||||
|
{ _id: teamId },
|
||||||
|
{ lookup: { defaultIssueStatus: tracker.class.IssueStatus } }
|
||||||
|
)
|
||||||
|
const teamDefaultIssueStatusId = team?.$lookup?.defaultIssueStatus?._id
|
||||||
|
|
||||||
|
if (teamDefaultIssueStatusId) {
|
||||||
|
object.status = teamDefaultIssueStatusId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function canClose (): boolean {
|
export function canClose (): boolean {
|
||||||
// if (object.title !== undefined) {
|
// if (object.title !== undefined) {
|
||||||
// showPopup(
|
// showPopup(
|
||||||
@ -72,6 +98,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function createIssue () {
|
async function createIssue () {
|
||||||
|
if (!object.status) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const lastOne = await client.findOne<Issue>(
|
const lastOne = await client.findOne<Issue>(
|
||||||
tracker.class.Issue,
|
tracker.class.Issue,
|
||||||
{ status: object.status },
|
{ status: object.status },
|
||||||
@ -116,12 +146,10 @@
|
|||||||
object.priority = newPriority
|
object.priority = newPriority
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleStatusChanged = (newStatus: IssueStatus | undefined) => {
|
const handleStatusChanged = (statusId: Ref<IssueStatus> | undefined) => {
|
||||||
if (newStatus === undefined) {
|
if (statusId !== undefined) {
|
||||||
return
|
object.status = statusId
|
||||||
}
|
}
|
||||||
|
|
||||||
object.status = newStatus
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@ -164,8 +192,8 @@
|
|||||||
placeholder={tracker.string.IssueDescriptionPlaceholder}
|
placeholder={tracker.string.IssueDescriptionPlaceholder}
|
||||||
/>
|
/>
|
||||||
<svelte:fragment slot="pool">
|
<svelte:fragment slot="pool">
|
||||||
<StatusSelector bind:status={object.status} onStatusChange={handleStatusChanged} />
|
<StatusSelector selectedStatusId={object.status} statuses={issueStatuses} onStatusChange={handleStatusChanged} />
|
||||||
<PrioritySelector bind:priority={object.priority} onPriorityChange={handlePriorityChanged} />
|
<PrioritySelector priority={object.priority} onPriorityChange={handlePriorityChanged} />
|
||||||
<UserBox
|
<UserBox
|
||||||
_class={contact.class.Employee}
|
_class={contact.class.Employee}
|
||||||
label={tracker.string.Assignee}
|
label={tracker.string.Assignee}
|
||||||
|
@ -1,3 +1,17 @@
|
|||||||
|
<!--
|
||||||
|
// 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">
|
<script lang="ts">
|
||||||
import { Ref, Space } from '@anticrm/core'
|
import { Ref, Space } from '@anticrm/core'
|
||||||
import { Button, showPopup } from '@anticrm/ui'
|
import { Button, showPopup } from '@anticrm/ui'
|
||||||
|
@ -13,24 +13,23 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { Ref, WithLookup } from '@anticrm/core'
|
||||||
|
|
||||||
import { IssueStatus } from '@anticrm/tracker'
|
import { IssueStatus } from '@anticrm/tracker'
|
||||||
import { Button, Icon, Label, showPopup, SelectPopup, eventToHTMLElement } from '@anticrm/ui'
|
import { Button, Icon, showPopup, SelectPopup, eventToHTMLElement } from '@anticrm/ui'
|
||||||
import { issueStatuses } from '../utils'
|
|
||||||
import tracker from '../plugin'
|
import tracker from '../plugin'
|
||||||
|
|
||||||
export let status: IssueStatus
|
export let selectedStatusId: Ref<IssueStatus>
|
||||||
|
export let statuses: WithLookup<IssueStatus>[]
|
||||||
export let kind: 'button' | 'icon' = 'button'
|
export let kind: 'button' | 'icon' = 'button'
|
||||||
export let shouldShowLabel: boolean = true
|
export let shouldShowLabel: boolean = true
|
||||||
export let onStatusChange: ((newStatus: IssueStatus | undefined) => void) | undefined = undefined
|
export let onStatusChange: ((newStatus: Ref<IssueStatus> | undefined) => void) | undefined = undefined
|
||||||
export let isEditable: boolean = true
|
export let isEditable: boolean = true
|
||||||
|
|
||||||
const statusesInfo = [
|
$: selectedStatus = statuses.find((status) => status._id === selectedStatusId) ?? statuses[0]
|
||||||
IssueStatus.Backlog,
|
$: selectedStatusIcon = selectedStatus?.$lookup?.category?.icon
|
||||||
IssueStatus.Todo,
|
$: selectedStatusLabel = shouldShowLabel ? selectedStatus?.name : undefined
|
||||||
IssueStatus.InProgress,
|
$: statusesInfo = statuses.map((s) => ({ id: s._id, text: s.name, color: s.color, icon: s.$lookup?.category?.icon }))
|
||||||
IssueStatus.Done,
|
|
||||||
IssueStatus.Canceled
|
|
||||||
].map((s) => ({ id: s, ...issueStatuses[s] }))
|
|
||||||
|
|
||||||
const handleStatusEditorOpened = (event: MouseEvent) => {
|
const handleStatusEditorOpened = (event: MouseEvent) => {
|
||||||
if (!isEditable) {
|
if (!isEditable) {
|
||||||
@ -47,23 +46,28 @@
|
|||||||
|
|
||||||
{#if kind === 'button'}
|
{#if kind === 'button'}
|
||||||
<Button
|
<Button
|
||||||
label={shouldShowLabel ? issueStatuses[status].label : undefined}
|
icon={selectedStatusIcon}
|
||||||
icon={issueStatuses[status].icon}
|
|
||||||
width="min-content"
|
width="min-content"
|
||||||
size="small"
|
size="small"
|
||||||
kind="no-border"
|
kind="no-border"
|
||||||
on:click={handleStatusEditorOpened}
|
on:click={handleStatusEditorOpened}
|
||||||
/>
|
>
|
||||||
|
<svelte:fragment slot="content">
|
||||||
|
{#if selectedStatusLabel}
|
||||||
|
<span class="nowrap">{selectedStatusLabel}</span>
|
||||||
|
{/if}
|
||||||
|
</svelte:fragment>
|
||||||
|
</Button>
|
||||||
{:else if kind === 'icon'}
|
{:else if kind === 'icon'}
|
||||||
<div class={isEditable ? 'flex-presenter' : 'presenter'} on:click={handleStatusEditorOpened}>
|
<div class={isEditable ? 'flex-presenter' : 'presenter'} on:click={handleStatusEditorOpened}>
|
||||||
<div class="statusIcon">
|
{#if selectedStatusIcon}
|
||||||
<Icon icon={issueStatuses[status].icon} size={'small'} />
|
<div class="statusIcon">
|
||||||
</div>
|
<Icon icon={selectedStatusIcon} size={'small'} />
|
||||||
{#if shouldShowLabel}
|
|
||||||
<div class="label nowrap ml-2">
|
|
||||||
<Label label={issueStatuses[status].label} />
|
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
{#if selectedStatusLabel}
|
||||||
|
<div class="label nowrap ml-2">{selectedStatusLabel}</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
@ -1,8 +1,22 @@
|
|||||||
|
<!--
|
||||||
|
// 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">
|
<script lang="ts">
|
||||||
import { Ref } from '@anticrm/core'
|
import { Ref } from '@anticrm/core'
|
||||||
import { IssueStatus, Team, IssuesDateModificationPeriod } from '@anticrm/tracker'
|
import { Team, IssuesDateModificationPeriod } from '@anticrm/tracker'
|
||||||
import Issues from './Issues.svelte'
|
|
||||||
import tracker from '../../plugin'
|
import tracker from '../../plugin'
|
||||||
|
import Issues from './Issues.svelte'
|
||||||
|
|
||||||
export let currentSpace: Ref<Team>
|
export let currentSpace: Ref<Team>
|
||||||
|
|
||||||
@ -12,6 +26,6 @@
|
|||||||
<Issues
|
<Issues
|
||||||
{currentSpace}
|
{currentSpace}
|
||||||
{completedIssuesPeriod}
|
{completedIssuesPeriod}
|
||||||
includedGroups={{ status: [IssueStatus.InProgress, IssueStatus.Todo] }}
|
includedGroups={{ status: [tracker.issueStatusCategory.Unstarted, tracker.issueStatusCategory.Started] }}
|
||||||
title={tracker.string.ActiveIssues}
|
title={tracker.string.ActiveIssues}
|
||||||
/>
|
/>
|
||||||
|
@ -1,8 +1,22 @@
|
|||||||
|
<!--
|
||||||
|
// 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">
|
<script lang="ts">
|
||||||
import { Ref } from '@anticrm/core'
|
import { Ref } from '@anticrm/core'
|
||||||
import { IssueStatus, Team, IssuesDateModificationPeriod } from '@anticrm/tracker'
|
import { Team, IssuesDateModificationPeriod } from '@anticrm/tracker'
|
||||||
import Issues from './Issues.svelte'
|
|
||||||
import tracker from '../../plugin'
|
import tracker from '../../plugin'
|
||||||
|
import Issues from './Issues.svelte'
|
||||||
|
|
||||||
export let currentSpace: Ref<Team>
|
export let currentSpace: Ref<Team>
|
||||||
|
|
||||||
@ -13,5 +27,5 @@
|
|||||||
title={tracker.string.BacklogIssues}
|
title={tracker.string.BacklogIssues}
|
||||||
{currentSpace}
|
{currentSpace}
|
||||||
{completedIssuesPeriod}
|
{completedIssuesPeriod}
|
||||||
includedGroups={{ status: [IssueStatus.Backlog] }}
|
includedGroups={{ status: [tracker.issueStatusCategory.Backlog] }}
|
||||||
/>
|
/>
|
||||||
|
@ -1,65 +1,59 @@
|
|||||||
|
<!--
|
||||||
|
// 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">
|
<script lang="ts">
|
||||||
import contact from '@anticrm/contact'
|
import contact from '@anticrm/contact'
|
||||||
import { Class, Doc, FindOptions, Ref, WithLookup } from '@anticrm/core'
|
import { Class, Doc, FindOptions, Ref, SortingOrder, WithLookup } from '@anticrm/core'
|
||||||
import { Kanban } from '@anticrm/kanban'
|
import { Kanban, TypeState } from '@anticrm/kanban'
|
||||||
import { createQuery } from '@anticrm/presentation'
|
import { createQuery } from '@anticrm/presentation'
|
||||||
import { Issue, IssueStatus, Team } from '@anticrm/tracker'
|
import { Issue, Team } from '@anticrm/tracker'
|
||||||
import { Button, Component, eventToHTMLElement, Icon, IconAdd, IconMoreH, showPopup, Tooltip } from '@anticrm/ui'
|
import { Button, eventToHTMLElement, Icon, IconAdd, showPopup, Tooltip } from '@anticrm/ui'
|
||||||
import view from '@anticrm/view'
|
|
||||||
import { focusStore, ListSelectionProvider, selectionStore } from '@anticrm/view-resources'
|
import { focusStore, ListSelectionProvider, selectionStore } from '@anticrm/view-resources'
|
||||||
import ActionContext from '@anticrm/view-resources/src/components/ActionContext.svelte'
|
import ActionContext from '@anticrm/view-resources/src/components/ActionContext.svelte'
|
||||||
import Menu from '@anticrm/view-resources/src/components/Menu.svelte'
|
import Menu from '@anticrm/view-resources/src/components/Menu.svelte'
|
||||||
import { onMount } from 'svelte'
|
import { onMount } from 'svelte'
|
||||||
import tracker from '../../plugin'
|
import tracker from '../../plugin'
|
||||||
import CreateIssue from '../CreateIssue.svelte'
|
import CreateIssue from '../CreateIssue.svelte'
|
||||||
import AssigneePresenter from './AssigneePresenter.svelte';
|
import AssigneePresenter from './AssigneePresenter.svelte'
|
||||||
import IssuePresenter from './IssuePresenter.svelte'
|
import IssuePresenter from './IssuePresenter.svelte'
|
||||||
import PriorityPresenter from './PriorityPresenter.svelte';
|
import PriorityPresenter from './PriorityPresenter.svelte'
|
||||||
|
|
||||||
export let currentSpace: Ref<Team>
|
export let currentSpace: Ref<Team>
|
||||||
export let baseMenuClass: Ref<Class<Doc>> | undefined = undefined
|
export let baseMenuClass: Ref<Class<Doc>> | undefined = undefined
|
||||||
|
|
||||||
const states = [
|
|
||||||
{
|
|
||||||
_id: IssueStatus.Backlog,
|
|
||||||
title: 'Backlog',
|
|
||||||
color: 0,
|
|
||||||
icon: tracker.icon.StatusBacklog
|
|
||||||
},
|
|
||||||
{
|
|
||||||
_id: IssueStatus.InProgress,
|
|
||||||
title: 'In progress',
|
|
||||||
color: 1,
|
|
||||||
icon: tracker.icon.StatusInProgress
|
|
||||||
},
|
|
||||||
{
|
|
||||||
_id: IssueStatus.Todo,
|
|
||||||
title: 'To do',
|
|
||||||
color: 2,
|
|
||||||
icon: tracker.icon.StatusTodo
|
|
||||||
},
|
|
||||||
{
|
|
||||||
_id: IssueStatus.Done,
|
|
||||||
title: 'Done',
|
|
||||||
color: 3,
|
|
||||||
icon: tracker.icon.StatusDone
|
|
||||||
},
|
|
||||||
{
|
|
||||||
_id: IssueStatus.Canceled,
|
|
||||||
title: 'Canceled',
|
|
||||||
color: 4,
|
|
||||||
icon: tracker.icon.StatusCanceled
|
|
||||||
}
|
|
||||||
]
|
|
||||||
/* eslint-disable no-undef */
|
/* eslint-disable no-undef */
|
||||||
|
|
||||||
const spaceQuery = createQuery()
|
const spaceQuery = createQuery()
|
||||||
|
const statusesQuery = createQuery()
|
||||||
|
|
||||||
let currentTeam: Team | undefined
|
let currentTeam: Team | undefined
|
||||||
|
|
||||||
$: spaceQuery.query(tracker.class.Team, { _id: currentSpace }, (res) => {
|
$: spaceQuery.query(tracker.class.Team, { _id: currentSpace }, (res) => {
|
||||||
currentTeam = res.shift()
|
currentTeam = res.shift()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
let states: TypeState[] | undefined
|
||||||
|
$: statusesQuery.query(tracker.class.IssueStatus, { attachedTo: currentSpace }, (issueStatuses) => {
|
||||||
|
states = issueStatuses.map((status) => ({
|
||||||
|
_id: status._id,
|
||||||
|
title: status.name,
|
||||||
|
color: status.color ?? status.$lookup?.category?.color ?? 0
|
||||||
|
}))
|
||||||
|
}, {
|
||||||
|
lookup: { category: tracker.class.IssueStatusCategory },
|
||||||
|
sort: { rank: SortingOrder.Ascending }
|
||||||
|
})
|
||||||
|
|
||||||
/* eslint-disable prefer-const */
|
/* eslint-disable prefer-const */
|
||||||
/* eslint-disable no-unused-vars */
|
/* eslint-disable no-unused-vars */
|
||||||
let issue: Issue
|
let issue: Issue
|
||||||
@ -107,7 +101,7 @@ import PriorityPresenter from './PriorityPresenter.svelte';
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if currentTeam}
|
{#if currentTeam && states}
|
||||||
<ActionContext
|
<ActionContext
|
||||||
context={{
|
context={{
|
||||||
mode: 'browser'
|
mode: 'browser'
|
||||||
@ -141,7 +135,7 @@ import PriorityPresenter from './PriorityPresenter.svelte';
|
|||||||
on:contextmenu={(evt) => showMenu(evt.detail.evt, evt.detail.objects)}
|
on:contextmenu={(evt) => showMenu(evt.detail.evt, evt.detail.objects)}
|
||||||
>
|
>
|
||||||
<svelte:fragment slot="header" let:state let:count>
|
<svelte:fragment slot="header" let:state let:count>
|
||||||
<div class="header flex-col">
|
<div class="header flex-col">
|
||||||
<div class="flex-between label font-medium w-full h-full mb-4">
|
<div class="flex-between label font-medium w-full h-full mb-4">
|
||||||
<div class="flex-row-center gap-2">
|
<div class="flex-row-center gap-2">
|
||||||
<Icon icon={state.icon} size={'small'} />
|
<Icon icon={state.icon} size={'small'} />
|
||||||
@ -188,10 +182,6 @@ import PriorityPresenter from './PriorityPresenter.svelte';
|
|||||||
min-height: 6rem;
|
min-height: 6rem;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
|
|
||||||
.filter {
|
|
||||||
border-bottom: 1px solid var(--divider-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.label {
|
.label {
|
||||||
color: var(--theme-caption-color);
|
color: var(--theme-caption-color);
|
||||||
border-bottom: 1px solid var(--divider-color);
|
border-bottom: 1px solid var(--divider-color);
|
||||||
|
@ -1,7 +1,21 @@
|
|||||||
|
<!--
|
||||||
|
// 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">
|
<script lang="ts">
|
||||||
import contact from '@anticrm/contact'
|
import contact from '@anticrm/contact'
|
||||||
import { DocumentQuery, FindOptions, Ref } from '@anticrm/core'
|
import { DocumentQuery, FindOptions, Ref, WithLookup } from '@anticrm/core'
|
||||||
import { Issue, Team } from '@anticrm/tracker'
|
import { Issue, IssueStatus, Team } from '@anticrm/tracker'
|
||||||
import { Component, Button, eventToHTMLElement, IconAdd, Scroller, showPopup, Tooltip } from '@anticrm/ui'
|
import { Component, Button, eventToHTMLElement, IconAdd, Scroller, showPopup, Tooltip } from '@anticrm/ui'
|
||||||
import { createEventDispatcher } from 'svelte'
|
import { createEventDispatcher } from 'svelte'
|
||||||
import tracker from '../../plugin'
|
import tracker from '../../plugin'
|
||||||
@ -12,13 +26,15 @@
|
|||||||
export let query: DocumentQuery<Issue>
|
export let query: DocumentQuery<Issue>
|
||||||
export let groupBy: { key: IssuesGroupByKeys | undefined; group: Issue[IssuesGroupByKeys] | undefined }
|
export let groupBy: { key: IssuesGroupByKeys | undefined; group: Issue[IssuesGroupByKeys] | undefined }
|
||||||
export let orderBy: IssuesOrderByKeys
|
export let orderBy: IssuesOrderByKeys
|
||||||
|
export let statuses: WithLookup<IssueStatus>[]
|
||||||
export let currentSpace: Ref<Team> | undefined = undefined
|
export let currentSpace: Ref<Team> | undefined = undefined
|
||||||
export let currentTeam: Team
|
export let currentTeam: Team
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
const options: FindOptions<Issue> = {
|
const options: FindOptions<Issue> = {
|
||||||
lookup: {
|
lookup: {
|
||||||
assignee: contact.class.Employee
|
assignee: contact.class.Employee,
|
||||||
|
status: tracker.class.IssueStatus
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -46,7 +62,8 @@
|
|||||||
isEditable: false,
|
isEditable: false,
|
||||||
shouldShowLabel: true,
|
shouldShowLabel: true,
|
||||||
value: grouping,
|
value: grouping,
|
||||||
defaultName: groupBy.key === 'assignee' ? tracker.string.NoAssignee : undefined
|
defaultName: groupBy.key === 'assignee' ? tracker.string.NoAssignee : undefined,
|
||||||
|
statuses: groupBy.key === 'status' ? statuses : undefined
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<span class="eLabelCounter ml-2">{issuesAmount}</span>
|
<span class="eLabelCounter ml-2">{issuesAmount}</span>
|
||||||
@ -64,7 +81,7 @@
|
|||||||
itemsConfig={[
|
itemsConfig={[
|
||||||
{ key: '', presenter: tracker.component.PriorityPresenter, props: { currentSpace } },
|
{ key: '', presenter: tracker.component.PriorityPresenter, props: { currentSpace } },
|
||||||
{ key: '', presenter: tracker.component.IssuePresenter, props: { currentTeam } },
|
{ key: '', presenter: tracker.component.IssuePresenter, props: { currentTeam } },
|
||||||
{ key: '', presenter: tracker.component.StatusPresenter, props: { currentSpace } },
|
{ key: '', presenter: tracker.component.StatusPresenter, props: { currentSpace, statuses } },
|
||||||
{ key: '', presenter: tracker.component.TitlePresenter, props: { shouldUseMargin: true } },
|
{ key: '', presenter: tracker.component.TitlePresenter, props: { shouldUseMargin: true } },
|
||||||
{ key: '', presenter: tracker.component.DueDatePresenter, props: { currentSpace } },
|
{ key: '', presenter: tracker.component.DueDatePresenter, props: { currentSpace } },
|
||||||
{ key: 'modifiedOn', presenter: tracker.component.ModificationDatePresenter },
|
{ key: 'modifiedOn', presenter: tracker.component.ModificationDatePresenter },
|
||||||
|
@ -13,14 +13,14 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Ref, Timestamp } from '@anticrm/core'
|
import { Ref, Timestamp, WithLookup } from '@anticrm/core'
|
||||||
import { Issue, IssueStatus, Team } from '@anticrm/tracker'
|
import { Issue, Team } from '@anticrm/tracker'
|
||||||
import { DatePresenter, Tooltip, getDaysDifference } from '@anticrm/ui'
|
import { DatePresenter, Tooltip, getDaysDifference } from '@anticrm/ui'
|
||||||
import { getClient } from '@anticrm/presentation'
|
import { getClient } from '@anticrm/presentation'
|
||||||
import DueDatePopup from './DueDatePopup.svelte'
|
import DueDatePopup from './DueDatePopup.svelte'
|
||||||
import tracker from '../../plugin'
|
import tracker from '../../plugin'
|
||||||
|
|
||||||
export let value: Issue
|
export let value: WithLookup<Issue>
|
||||||
export let currentSpace: Ref<Team> | undefined = undefined
|
export let currentSpace: Ref<Team> | undefined = undefined
|
||||||
|
|
||||||
const WARNING_DAYS = 7
|
const WARNING_DAYS = 7
|
||||||
@ -64,7 +64,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$: shouldRenderPresenter = dueDateMs && value.status !== IssueStatus.Done && value.status !== IssueStatus.Canceled
|
$: shouldRenderPresenter =
|
||||||
|
dueDateMs &&
|
||||||
|
value.$lookup?.status?.category !== tracker.issueStatusCategory.Completed &&
|
||||||
|
value.$lookup?.status?.category !== tracker.issueStatusCategory.Canceled
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if shouldRenderPresenter}
|
{#if shouldRenderPresenter}
|
||||||
|
@ -14,7 +14,7 @@
|
|||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import contact from '@anticrm/contact'
|
import contact from '@anticrm/contact'
|
||||||
import type { DocumentQuery, Ref } from '@anticrm/core'
|
import { DocumentQuery, Ref, SortingOrder, WithLookup } from '@anticrm/core'
|
||||||
import { createQuery } from '@anticrm/presentation'
|
import { createQuery } from '@anticrm/presentation'
|
||||||
import {
|
import {
|
||||||
Issue,
|
Issue,
|
||||||
@ -22,7 +22,8 @@
|
|||||||
IssuesGrouping,
|
IssuesGrouping,
|
||||||
IssuesOrdering,
|
IssuesOrdering,
|
||||||
IssuesDateModificationPeriod,
|
IssuesDateModificationPeriod,
|
||||||
IssueStatus
|
IssueStatus,
|
||||||
|
IssueStatusCategory
|
||||||
} from '@anticrm/tracker'
|
} from '@anticrm/tracker'
|
||||||
import { Button, Label, ScrollBox, IconOptions, showPopup, eventToHTMLElement } from '@anticrm/ui'
|
import { Button, Label, ScrollBox, IconOptions, showPopup, eventToHTMLElement } from '@anticrm/ui'
|
||||||
import CategoryPresenter from './CategoryPresenter.svelte'
|
import CategoryPresenter from './CategoryPresenter.svelte'
|
||||||
@ -50,9 +51,11 @@
|
|||||||
const ENTRIES_LIMIT = 200
|
const ENTRIES_LIMIT = 200
|
||||||
const spaceQuery = createQuery()
|
const spaceQuery = createQuery()
|
||||||
const issuesQuery = createQuery()
|
const issuesQuery = createQuery()
|
||||||
|
const statusesQuery = createQuery()
|
||||||
const issuesMap: { [status: string]: number } = {}
|
const issuesMap: { [status: string]: number } = {}
|
||||||
let currentTeam: Team | undefined
|
let currentTeam: Team | undefined
|
||||||
let issues: Issue[] = []
|
let issues: Issue[] = []
|
||||||
|
let statusesById: ReadonlyMap<Ref<IssueStatus>, WithLookup<IssueStatus>> = new Map()
|
||||||
|
|
||||||
$: totalIssues = getTotalIssues(issuesMap)
|
$: totalIssues = getTotalIssues(issuesMap)
|
||||||
|
|
||||||
@ -71,25 +74,41 @@
|
|||||||
|
|
||||||
$: groupByKey = issuesGroupKeyMap[groupingKey]
|
$: groupByKey = issuesGroupKeyMap[groupingKey]
|
||||||
$: categories = getCategories(groupByKey, issues, !!shouldShowEmptyGroups)
|
$: categories = getCategories(groupByKey, issues, !!shouldShowEmptyGroups)
|
||||||
$: displayedCategories = (categories as any[]).filter((x: ReturnType<typeof getCategories>) => {
|
$: displayedCategories = (categories as any[]).filter((x) => {
|
||||||
return (
|
if (groupByKey === undefined || includedGroups[groupByKey] === undefined) {
|
||||||
groupByKey === undefined || includedGroups[groupByKey] === undefined || includedGroups[groupByKey]?.includes(x)
|
return true
|
||||||
)
|
}
|
||||||
})
|
|
||||||
$: includedIssuesQuery = getIncludedIssuesQuery(includedGroups)
|
|
||||||
$: filteredIssuesQuery = getModifiedOnIssuesFilterQuery(issues, completedIssuesPeriod)
|
|
||||||
|
|
||||||
const getIncludedIssuesQuery = (groups: Partial<Record<IssuesGroupByKeys, Array<any>>>) => {
|
if (groupByKey === 'status') {
|
||||||
|
const category = statusesById.get(x as Ref<IssueStatus>)?.category
|
||||||
|
|
||||||
|
return !!(category && includedGroups.status?.includes(category))
|
||||||
|
}
|
||||||
|
|
||||||
|
return includedGroups[groupByKey]?.includes(x)
|
||||||
|
})
|
||||||
|
$: includedIssuesQuery = getIncludedIssuesQuery(includedGroups, statuses)
|
||||||
|
$: filteredIssuesQuery = getModifiedOnIssuesFilterQuery(issues, completedIssuesPeriod)
|
||||||
|
$: statuses = [...statusesById.values()]
|
||||||
|
|
||||||
|
const getIncludedIssuesQuery = (
|
||||||
|
groups: Partial<Record<IssuesGroupByKeys, Array<any>>>,
|
||||||
|
issueStatuses: IssueStatus[]
|
||||||
|
) => {
|
||||||
const resultMap: { [p: string]: { $in: any[] } } = {}
|
const resultMap: { [p: string]: { $in: any[] } } = {}
|
||||||
|
|
||||||
for (const [key, value] of Object.entries(groups)) {
|
for (const [key, value] of Object.entries(groups)) {
|
||||||
resultMap[key] = { $in: value }
|
const includedCategories = key === 'status' ? filterIssueStatuses(issueStatuses, value) : value
|
||||||
|
resultMap[key] = { $in: includedCategories }
|
||||||
}
|
}
|
||||||
|
|
||||||
return resultMap
|
return resultMap
|
||||||
}
|
}
|
||||||
|
|
||||||
const getModifiedOnIssuesFilterQuery = (currentIssues: Issue[], period: IssuesDateModificationPeriod | null) => {
|
const getModifiedOnIssuesFilterQuery = (
|
||||||
|
currentIssues: WithLookup<Issue>[],
|
||||||
|
period: IssuesDateModificationPeriod | null
|
||||||
|
) => {
|
||||||
const filter: { _id: { $in: Array<Ref<Issue>> } } = { _id: { $in: [] } }
|
const filter: { _id: { $in: Array<Ref<Issue>> } } = { _id: { $in: [] } }
|
||||||
|
|
||||||
if (!period || period === IssuesDateModificationPeriod.All) {
|
if (!period || period === IssuesDateModificationPeriod.All) {
|
||||||
@ -97,7 +116,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (const issue of currentIssues) {
|
for (const issue of currentIssues) {
|
||||||
if (issue.status === IssueStatus.Done && issue.modifiedOn < getIssuesModificationDatePeriodTime(period)) {
|
if (
|
||||||
|
issue.$lookup?.status?.category === tracker.issueStatusCategory.Completed &&
|
||||||
|
issue.modifiedOn < getIssuesModificationDatePeriodTime(period)
|
||||||
|
) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -116,6 +138,18 @@
|
|||||||
{ limit: ENTRIES_LIMIT, lookup: { assignee: contact.class.Employee } }
|
{ limit: ENTRIES_LIMIT, lookup: { assignee: contact.class.Employee } }
|
||||||
)
|
)
|
||||||
|
|
||||||
|
$: statusesQuery.query(
|
||||||
|
tracker.class.IssueStatus,
|
||||||
|
{ attachedTo: currentSpace },
|
||||||
|
(issueStatuses) => {
|
||||||
|
statusesById = new Map(issueStatuses.map((status) => [status._id, status]))
|
||||||
|
},
|
||||||
|
{
|
||||||
|
lookup: { category: tracker.class.IssueStatusCategory },
|
||||||
|
sort: { rank: SortingOrder.Ascending }
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
const getCategories = (key: IssuesGroupByKeys | undefined, elements: Issue[], shouldShowAll: boolean) => {
|
const getCategories = (key: IssuesGroupByKeys | undefined, elements: Issue[], shouldShowAll: boolean) => {
|
||||||
if (!key) {
|
if (!key) {
|
||||||
return [undefined] // No grouping
|
return [undefined] // No grouping
|
||||||
@ -132,6 +166,15 @@
|
|||||||
return shouldShowAll ? defaultIssueCategories[key] ?? existingCategories : existingCategories
|
return shouldShowAll ? defaultIssueCategories[key] ?? existingCategories : existingCategories
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function filterIssueStatuses (
|
||||||
|
issueStatuses: IssueStatus[],
|
||||||
|
issueStatusCategories: Ref<IssueStatusCategory>[]
|
||||||
|
): Ref<IssueStatus>[] {
|
||||||
|
const statusCategories = new Set(issueStatusCategories)
|
||||||
|
|
||||||
|
return issueStatuses.filter((status) => statusCategories.has(status.category)).map((s) => s._id)
|
||||||
|
}
|
||||||
|
|
||||||
const getTotalIssues = (map: { [status: string]: number }) => {
|
const getTotalIssues = (map: { [status: string]: number }) => {
|
||||||
let total = 0
|
let total = 0
|
||||||
|
|
||||||
@ -197,6 +240,7 @@
|
|||||||
groupBy={{ key: groupByKey, group: category }}
|
groupBy={{ key: groupByKey, group: category }}
|
||||||
orderBy={issuesOrderKeyMap[orderingKey]}
|
orderBy={issuesOrderKeyMap[orderingKey]}
|
||||||
query={resultQuery}
|
query={resultQuery}
|
||||||
|
{statuses}
|
||||||
{currentSpace}
|
{currentSpace}
|
||||||
{currentTeam}
|
{currentTeam}
|
||||||
on:content={(event) => {
|
on:content={(event) => {
|
||||||
|
@ -13,7 +13,7 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Ref } from '@anticrm/core'
|
import { Ref, WithLookup } from '@anticrm/core'
|
||||||
import { Issue, IssueStatus, Team } from '@anticrm/tracker'
|
import { Issue, IssueStatus, Team } from '@anticrm/tracker'
|
||||||
import { getClient } from '@anticrm/presentation'
|
import { getClient } from '@anticrm/presentation'
|
||||||
import { Tooltip } from '@anticrm/ui'
|
import { Tooltip } from '@anticrm/ui'
|
||||||
@ -21,13 +21,14 @@
|
|||||||
import StatusSelector from '../StatusSelector.svelte'
|
import StatusSelector from '../StatusSelector.svelte'
|
||||||
|
|
||||||
export let value: Issue
|
export let value: Issue
|
||||||
|
export let statuses: WithLookup<IssueStatus>[]
|
||||||
export let currentSpace: Ref<Team> | undefined = undefined
|
export let currentSpace: Ref<Team> | undefined = undefined
|
||||||
export let isEditable: boolean = true
|
export let isEditable: boolean = true
|
||||||
export let shouldShowLabel: boolean = false
|
export let shouldShowLabel: boolean = false
|
||||||
|
|
||||||
const client = getClient()
|
const client = getClient()
|
||||||
|
|
||||||
const handleStatusChanged = async (newStatus: IssueStatus | undefined) => {
|
const handleStatusChanged = async (newStatus: Ref<IssueStatus> | undefined) => {
|
||||||
if (!isEditable || newStatus === undefined) {
|
if (!isEditable || newStatus === undefined) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -49,7 +50,8 @@
|
|||||||
kind={'icon'}
|
kind={'icon'}
|
||||||
{isEditable}
|
{isEditable}
|
||||||
{shouldShowLabel}
|
{shouldShowLabel}
|
||||||
status={value.status}
|
{statuses}
|
||||||
|
selectedStatusId={value.status}
|
||||||
onStatusChange={handleStatusChanged}
|
onStatusChange={handleStatusChanged}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
@ -58,7 +60,8 @@
|
|||||||
kind={'icon'}
|
kind={'icon'}
|
||||||
{isEditable}
|
{isEditable}
|
||||||
{shouldShowLabel}
|
{shouldShowLabel}
|
||||||
status={value.status}
|
{statuses}
|
||||||
|
selectedStatusId={value.status}
|
||||||
onStatusChange={handleStatusChanged}
|
onStatusChange={handleStatusChanged}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
//
|
//
|
||||||
// Copyright © 2020 Anticrm Platform Contributors.
|
// Copyright © 2022 Hardcore Engineering Inc.
|
||||||
//
|
//
|
||||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
// 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
|
// you may not use this file except in compliance with the License. You may
|
||||||
@ -59,6 +59,14 @@ export default mergeIds(trackerId, tracker, {
|
|||||||
Identifier: '' as IntlString,
|
Identifier: '' as IntlString,
|
||||||
Description: '' as IntlString,
|
Description: '' as IntlString,
|
||||||
Status: '' as IntlString,
|
Status: '' as IntlString,
|
||||||
|
DefaultIssueStatus: '' as IntlString,
|
||||||
|
IssueStatuses: '' as IntlString,
|
||||||
|
StatusCategory: '' as IntlString,
|
||||||
|
CategoryBacklog: '' as IntlString,
|
||||||
|
CategoryUnstarted: '' as IntlString,
|
||||||
|
CategoryStarted: '' as IntlString,
|
||||||
|
CategoryCompleted: '' as IntlString,
|
||||||
|
CategoryCanceled: '' as IntlString,
|
||||||
Number: '' as IntlString,
|
Number: '' as IntlString,
|
||||||
Assignee: '' as IntlString,
|
Assignee: '' as IntlString,
|
||||||
AssignTo: '' as IntlString,
|
AssignTo: '' as IntlString,
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
//
|
//
|
||||||
// Copyright © 2020, 2021 Anticrm Platform Contributors.
|
// Copyright © 2022 Hardcore Engineering Inc.
|
||||||
// Copyright © 2021 Hardcore Engineering Inc.
|
|
||||||
//
|
//
|
||||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
// 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
|
// you may not use this file except in compliance with the License. You may
|
||||||
@ -16,10 +15,15 @@
|
|||||||
|
|
||||||
import { Ref, SortingOrder } from '@anticrm/core'
|
import { Ref, SortingOrder } from '@anticrm/core'
|
||||||
import type { Asset, IntlString } from '@anticrm/platform'
|
import type { Asset, IntlString } from '@anticrm/platform'
|
||||||
import { IssuePriority, IssueStatus, Team, IssuesGrouping, IssuesOrdering, Issue, IssuesDateModificationPeriod } from '@anticrm/tracker'
|
import {
|
||||||
|
IssuePriority,
|
||||||
|
Team,
|
||||||
|
IssuesGrouping,
|
||||||
|
IssuesOrdering,
|
||||||
|
Issue,
|
||||||
|
IssuesDateModificationPeriod
|
||||||
|
} from '@anticrm/tracker'
|
||||||
import { AnyComponent, getMillisecondsInMonth, MILLISECONDS_IN_WEEK } from '@anticrm/ui'
|
import { AnyComponent, getMillisecondsInMonth, MILLISECONDS_IN_WEEK } from '@anticrm/ui'
|
||||||
import { LexoDecimal, LexoNumeralSystem36, LexoRank } from 'lexorank'
|
|
||||||
import LexoRankBucket from 'lexorank/lib/lexoRank/lexoRankBucket'
|
|
||||||
import tracker from './plugin'
|
import tracker from './plugin'
|
||||||
|
|
||||||
export interface NavigationItem {
|
export interface NavigationItem {
|
||||||
@ -36,41 +40,6 @@ export interface Selection {
|
|||||||
currentSpecial?: string
|
currentSpecial?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @public
|
|
||||||
*/
|
|
||||||
export const genRanks = (count: number): Generator<string, void, unknown> =>
|
|
||||||
(function * () {
|
|
||||||
const sys = new LexoNumeralSystem36()
|
|
||||||
const base = 36
|
|
||||||
const max = base ** 6
|
|
||||||
const gap = LexoDecimal.parse(Math.trunc(max / (count + 2)).toString(base), sys)
|
|
||||||
let cur = LexoDecimal.parse('0', sys)
|
|
||||||
|
|
||||||
for (let i = 0; i < count; i++) {
|
|
||||||
cur = cur.add(gap)
|
|
||||||
yield new LexoRank(LexoRankBucket.BUCKET_0, cur).toString()
|
|
||||||
}
|
|
||||||
})()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @public
|
|
||||||
*/
|
|
||||||
export const calcRank = (prev?: { rank: string }, next?: { rank: string }): string => {
|
|
||||||
const a = prev?.rank !== undefined ? LexoRank.parse(prev.rank) : LexoRank.min()
|
|
||||||
const b = next?.rank !== undefined ? LexoRank.parse(next.rank) : LexoRank.max()
|
|
||||||
|
|
||||||
return a.between(b).toString()
|
|
||||||
}
|
|
||||||
|
|
||||||
export const issueStatuses: Record<IssueStatus, { icon: Asset, label: IntlString }> = {
|
|
||||||
[IssueStatus.Backlog]: { icon: tracker.icon.StatusBacklog, label: tracker.string.Backlog },
|
|
||||||
[IssueStatus.Todo]: { icon: tracker.icon.StatusTodo, label: tracker.string.Todo },
|
|
||||||
[IssueStatus.InProgress]: { icon: tracker.icon.StatusInProgress, label: tracker.string.InProgress },
|
|
||||||
[IssueStatus.Done]: { icon: tracker.icon.StatusDone, label: tracker.string.Done },
|
|
||||||
[IssueStatus.Canceled]: { icon: tracker.icon.StatusCanceled, label: tracker.string.Canceled }
|
|
||||||
}
|
|
||||||
|
|
||||||
export const issuePriorities: Record<IssuePriority, { icon: Asset, label: IntlString }> = {
|
export const issuePriorities: Record<IssuePriority, { icon: Asset, label: IntlString }> = {
|
||||||
[IssuePriority.NoPriority]: { icon: tracker.icon.PriorityNoPriority, label: tracker.string.NoPriority },
|
[IssuePriority.NoPriority]: { icon: tracker.icon.PriorityNoPriority, label: tracker.string.NoPriority },
|
||||||
[IssuePriority.Urgent]: { icon: tracker.icon.PriorityUrgent, label: tracker.string.Urgent },
|
[IssuePriority.Urgent]: { icon: tracker.icon.PriorityUrgent, label: tracker.string.Urgent },
|
||||||
@ -99,7 +68,7 @@ export const issuesDateModificationPeriodOptions: Record<IssuesDateModificationP
|
|||||||
[IssuesDateModificationPeriod.PastMonth]: tracker.string.PastMonth
|
[IssuesDateModificationPeriod.PastMonth]: tracker.string.PastMonth
|
||||||
}
|
}
|
||||||
|
|
||||||
export type IssuesGroupByKeys = keyof Pick<Issue, 'status' | 'priority' | 'assignee' >
|
export type IssuesGroupByKeys = keyof Pick<Issue, 'status' | 'priority' | 'assignee'>
|
||||||
export type IssuesOrderByKeys = keyof Pick<Issue, 'status' | 'priority' | 'modifiedOn' | 'dueDate'>
|
export type IssuesOrderByKeys = keyof Pick<Issue, 'status' | 'priority' | 'modifiedOn' | 'dueDate'>
|
||||||
|
|
||||||
export const issuesGroupKeyMap: Record<IssuesGrouping, IssuesGroupByKeys | undefined> = {
|
export const issuesGroupKeyMap: Record<IssuesGrouping, IssuesGroupByKeys | undefined> = {
|
||||||
@ -130,7 +99,6 @@ export const issuesGroupPresenterMap: Record<IssuesGroupByKeys, AnyComponent | u
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const defaultIssueCategories: Partial<Record<IssuesGroupByKeys, Array<Issue[IssuesGroupByKeys]> | undefined>> = {
|
export const defaultIssueCategories: Partial<Record<IssuesGroupByKeys, Array<Issue[IssuesGroupByKeys]> | undefined>> = {
|
||||||
status: [IssueStatus.InProgress, IssueStatus.Todo, IssueStatus.Backlog, IssueStatus.Done, IssueStatus.Canceled],
|
|
||||||
priority: [IssuePriority.NoPriority, IssuePriority.Urgent, IssuePriority.High, IssuePriority.Medium, IssuePriority.Low]
|
priority: [IssuePriority.NoPriority, IssuePriority.Urgent, IssuePriority.High, IssuePriority.Medium, IssuePriority.Low]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -32,6 +32,7 @@
|
|||||||
"@anticrm/contact": "~0.6.5",
|
"@anticrm/contact": "~0.6.5",
|
||||||
"@anticrm/chunter": "~0.6.1",
|
"@anticrm/chunter": "~0.6.1",
|
||||||
"@anticrm/attachment": "~0.6.1",
|
"@anticrm/attachment": "~0.6.1",
|
||||||
"@anticrm/task": "~0.6.0"
|
"@anticrm/task": "~0.6.0",
|
||||||
|
"lexorank": "~1.0.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -14,11 +14,33 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
import { Employee } from '@anticrm/contact'
|
import { Employee } from '@anticrm/contact'
|
||||||
import type { Class, Doc, Markup, Ref, Space, Timestamp } from '@anticrm/core'
|
import type { AttachedDoc, Class, Doc, Markup, Ref, Space, Timestamp } from '@anticrm/core'
|
||||||
import type { Asset, IntlString, Plugin } from '@anticrm/platform'
|
import type { Asset, IntlString, Plugin } from '@anticrm/platform'
|
||||||
import { plugin } from '@anticrm/platform'
|
import { plugin } from '@anticrm/platform'
|
||||||
import { AnyComponent } from '@anticrm/ui'
|
import { AnyComponent } from '@anticrm/ui'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
export interface IssueStatus extends AttachedDoc {
|
||||||
|
name: string
|
||||||
|
description?: string
|
||||||
|
color?: number
|
||||||
|
category: Ref<IssueStatusCategory>
|
||||||
|
rank: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
export interface IssueStatusCategory extends Doc {
|
||||||
|
icon: Asset
|
||||||
|
label: IntlString
|
||||||
|
color: number
|
||||||
|
defaultStatusName: string
|
||||||
|
order: number
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
@ -26,16 +48,8 @@ export interface Team extends Space {
|
|||||||
teamLogo?: string | null
|
teamLogo?: string | null
|
||||||
identifier: string // Team identifier
|
identifier: string // Team identifier
|
||||||
sequence: number
|
sequence: number
|
||||||
}
|
issueStatuses: number
|
||||||
/**
|
defaultIssueStatus: Ref<IssueStatus>
|
||||||
* @public
|
|
||||||
*/
|
|
||||||
export enum IssueStatus {
|
|
||||||
Backlog,
|
|
||||||
Todo,
|
|
||||||
InProgress,
|
|
||||||
Done,
|
|
||||||
Canceled
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -84,7 +98,7 @@ export enum IssuesDateModificationPeriod {
|
|||||||
export interface Issue extends Doc {
|
export interface Issue extends Doc {
|
||||||
title: string
|
title: string
|
||||||
description: Markup
|
description: Markup
|
||||||
status: IssueStatus
|
status: Ref<IssueStatus>
|
||||||
priority: IssuePriority
|
priority: IssuePriority
|
||||||
|
|
||||||
number: number
|
number: number
|
||||||
@ -153,17 +167,28 @@ export interface Project extends Doc {
|
|||||||
*/
|
*/
|
||||||
export const trackerId = 'tracker' as Plugin
|
export const trackerId = 'tracker' as Plugin
|
||||||
|
|
||||||
|
export * from './utils'
|
||||||
|
|
||||||
export default plugin(trackerId, {
|
export default plugin(trackerId, {
|
||||||
class: {
|
class: {
|
||||||
Team: '' as Ref<Class<Team>>,
|
Team: '' as Ref<Class<Team>>,
|
||||||
Issue: '' as Ref<Class<Issue>>,
|
Issue: '' as Ref<Class<Issue>>,
|
||||||
Document: '' as Ref<Class<Document>>,
|
Document: '' as Ref<Class<Document>>,
|
||||||
Project: '' as Ref<Class<Project>>
|
Project: '' as Ref<Class<Project>>,
|
||||||
|
IssueStatus: '' as Ref<Class<IssueStatus>>,
|
||||||
|
IssueStatusCategory: '' as Ref<Class<IssueStatusCategory>>
|
||||||
},
|
},
|
||||||
component: {
|
component: {
|
||||||
Tracker: '' as AnyComponent,
|
Tracker: '' as AnyComponent,
|
||||||
TrackerApp: '' as AnyComponent
|
TrackerApp: '' as AnyComponent
|
||||||
},
|
},
|
||||||
|
issueStatusCategory: {
|
||||||
|
Backlog: '' as Ref<IssueStatusCategory>,
|
||||||
|
Unstarted: '' as Ref<IssueStatusCategory>,
|
||||||
|
Started: '' as Ref<IssueStatusCategory>,
|
||||||
|
Completed: '' as Ref<IssueStatusCategory>,
|
||||||
|
Canceled: '' as Ref<IssueStatusCategory>
|
||||||
|
},
|
||||||
icon: {
|
icon: {
|
||||||
TrackerApplication: '' as Asset,
|
TrackerApplication: '' as Asset,
|
||||||
Project: '' as Asset,
|
Project: '' as Asset,
|
||||||
@ -183,11 +208,12 @@ export default plugin(trackerId, {
|
|||||||
DueDate: '' as Asset,
|
DueDate: '' as Asset,
|
||||||
Parent: '' as Asset,
|
Parent: '' as Asset,
|
||||||
|
|
||||||
StatusBacklog: '' as Asset,
|
CategoryBacklog: '' as Asset,
|
||||||
StatusTodo: '' as Asset,
|
CategoryUnstarted: '' as Asset,
|
||||||
StatusInProgress: '' as Asset,
|
CategoryStarted: '' as Asset,
|
||||||
StatusDone: '' as Asset,
|
CategoryCompleted: '' as Asset,
|
||||||
StatusCanceled: '' as Asset,
|
CategoryCanceled: '' as Asset,
|
||||||
|
|
||||||
PriorityNoPriority: '' as Asset,
|
PriorityNoPriority: '' as Asset,
|
||||||
PriorityUrgent: '' as Asset,
|
PriorityUrgent: '' as Asset,
|
||||||
PriorityHigh: '' as Asset,
|
PriorityHigh: '' as Asset,
|
||||||
|
44
plugins/tracker/src/utils.ts
Normal file
44
plugins/tracker/src/utils.ts
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
//
|
||||||
|
// 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.
|
||||||
|
//
|
||||||
|
|
||||||
|
import { LexoRank, LexoDecimal, LexoNumeralSystem36 } from 'lexorank'
|
||||||
|
import LexoRankBucket from 'lexorank/lib/lexoRank/lexoRankBucket'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
export const genRanks = (count: number): Generator<string, void, unknown> =>
|
||||||
|
(function * () {
|
||||||
|
const sys = new LexoNumeralSystem36()
|
||||||
|
const base = 36
|
||||||
|
const max = base ** 6
|
||||||
|
const gap = LexoDecimal.parse(Math.trunc(max / (count + 2)).toString(base), sys)
|
||||||
|
let cur = LexoDecimal.parse('0', sys)
|
||||||
|
|
||||||
|
for (let i = 0; i < count; i++) {
|
||||||
|
cur = cur.add(gap)
|
||||||
|
yield new LexoRank(LexoRankBucket.BUCKET_0, cur).toString()
|
||||||
|
}
|
||||||
|
})()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
export const calcRank = (prev?: { rank: string }, next?: { rank: string }): string => {
|
||||||
|
const a = prev?.rank !== undefined ? LexoRank.parse(prev.rank) : LexoRank.min()
|
||||||
|
const b = next?.rank !== undefined ? LexoRank.parse(next.rank) : LexoRank.max()
|
||||||
|
|
||||||
|
return a.between(b).toString()
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user