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
|
||||
)
|
||||
|
||||
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
|
||||
})
|
||||
|
@ -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,
|
||||
@ -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'
|
||||
@ -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
|
||||
*/
|
||||
@ -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>
|
||||
}
|
||||
|
||||
/**
|
||||
@ -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
|
||||
@ -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,
|
||||
|
@ -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
|
||||
@ -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,
|
||||
@ -39,10 +91,60 @@ 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) })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -50,10 +152,20 @@ 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)
|
||||
}
|
||||
}
|
||||
|
@ -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 }
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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 = ''
|
||||
|
||||
@ -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>
|
||||
|
@ -21,10 +21,6 @@
|
||||
"AddIssue": "Add Issue",
|
||||
"NewIssue": "New issue",
|
||||
"SaveIssue": "Save issue",
|
||||
"Todo": "Todo",
|
||||
"InProgress": "In Progress",
|
||||
"Done": "Done",
|
||||
"Canceled": "Canceled",
|
||||
"SetPriority": "Set priority\u2026",
|
||||
"SetStatus": "Set status\u2026",
|
||||
"SelectIssue": "Select issue",
|
||||
@ -36,6 +32,12 @@
|
||||
"Low": "Low",
|
||||
"Unassigned": "Unassigned",
|
||||
|
||||
"CategoryBacklog": "Backlog",
|
||||
"CategoryUnstarted": "Unstarted",
|
||||
"CategoryStarted": "Started",
|
||||
"CategoryCompleted": "Completed",
|
||||
"CategoryCanceled": "Canceled",
|
||||
|
||||
"Title": "Title",
|
||||
"Description": "",
|
||||
"Status": "Status",
|
||||
|
@ -36,11 +36,11 @@ loadMetadata(tracker.icon, {
|
||||
DueDate: `${icons}#inbox`, // TODO: add icon
|
||||
Parent: `${icons}#myissues`, // TODO: add icon
|
||||
|
||||
StatusBacklog: `${icons}#status-backlog`,
|
||||
StatusTodo: `${icons}#status-todo`,
|
||||
StatusInProgress: `${icons}#status-inprogress`,
|
||||
StatusDone: `${icons}#status-done`,
|
||||
StatusCanceled: `${icons}#status-canceled`,
|
||||
CategoryBacklog: `${icons}#status-backlog`,
|
||||
CategoryUnstarted: `${icons}#status-todo`,
|
||||
CategoryStarted: `${icons}#status-inprogress`,
|
||||
CategoryCompleted: `${icons}#status-done`,
|
||||
CategoryCanceled: `${icons}#status-canceled`,
|
||||
PriorityNoPriority: `${icons}#priority-nopriority`,
|
||||
PriorityUrgent: `${icons}#priority-urgent`,
|
||||
PriorityHigh: `${icons}#priority-high`,
|
||||
|
@ -47,7 +47,6 @@
|
||||
"@anticrm/notification-resources": "~0.6.0",
|
||||
"@anticrm/contact": "~0.6.5",
|
||||
"@anticrm/view-resources": "~0.6.0",
|
||||
"lexorank": "~1.0.4",
|
||||
"@anticrm/text-editor": "~0.6.0",
|
||||
"@anticrm/panel": "~0.6.0",
|
||||
"@anticrm/kanban": "~0.6.0"
|
||||
|
@ -14,28 +14,25 @@
|
||||
-->
|
||||
<script lang="ts">
|
||||
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 presentation, { getClient, UserBox, Card, MessageBox } from '@anticrm/presentation'
|
||||
import { Issue, IssuePriority, IssueStatus, Team } from '@anticrm/tracker'
|
||||
import presentation, { getClient, UserBox, Card, createQuery } from '@anticrm/presentation'
|
||||
import { Issue, IssuePriority, IssueStatus, Team, calcRank } from '@anticrm/tracker'
|
||||
import { StyledTextBox } from '@anticrm/text-editor'
|
||||
import { EditBox, Button, showPopup, DatePresenter, SelectPopup, IconAttachment, eventToHTMLElement } from '@anticrm/ui'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import tracker from '../plugin'
|
||||
import { calcRank } from '../utils'
|
||||
import StatusSelector from './StatusSelector.svelte'
|
||||
import PrioritySelector from './PrioritySelector.svelte'
|
||||
|
||||
export let space: Ref<Team>
|
||||
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 assignee: Ref<Employee> | null = null
|
||||
|
||||
$: _space = space
|
||||
$: _parent = parent
|
||||
|
||||
let currentAssignee: Ref<Employee> | null = assignee
|
||||
let issueStatuses: WithLookup<IssueStatus>[] = []
|
||||
|
||||
const object: Data<Issue> = {
|
||||
title: '',
|
||||
@ -43,7 +40,7 @@
|
||||
assignee: null,
|
||||
number: 0,
|
||||
rank: '',
|
||||
status: status,
|
||||
status: '' as Ref<IssueStatus>,
|
||||
priority: priority,
|
||||
dueDate: null,
|
||||
comments: 0
|
||||
@ -51,8 +48,37 @@
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
const client = getClient()
|
||||
const statusesQuery = createQuery()
|
||||
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 {
|
||||
// if (object.title !== undefined) {
|
||||
// showPopup(
|
||||
@ -72,6 +98,10 @@
|
||||
}
|
||||
|
||||
async function createIssue () {
|
||||
if (!object.status) {
|
||||
return
|
||||
}
|
||||
|
||||
const lastOne = await client.findOne<Issue>(
|
||||
tracker.class.Issue,
|
||||
{ status: object.status },
|
||||
@ -116,12 +146,10 @@
|
||||
object.priority = newPriority
|
||||
}
|
||||
|
||||
const handleStatusChanged = (newStatus: IssueStatus | undefined) => {
|
||||
if (newStatus === undefined) {
|
||||
return
|
||||
const handleStatusChanged = (statusId: Ref<IssueStatus> | undefined) => {
|
||||
if (statusId !== undefined) {
|
||||
object.status = statusId
|
||||
}
|
||||
|
||||
object.status = newStatus
|
||||
}
|
||||
</script>
|
||||
|
||||
@ -164,8 +192,8 @@
|
||||
placeholder={tracker.string.IssueDescriptionPlaceholder}
|
||||
/>
|
||||
<svelte:fragment slot="pool">
|
||||
<StatusSelector bind:status={object.status} onStatusChange={handleStatusChanged} />
|
||||
<PrioritySelector bind:priority={object.priority} onPriorityChange={handlePriorityChanged} />
|
||||
<StatusSelector selectedStatusId={object.status} statuses={issueStatuses} onStatusChange={handleStatusChanged} />
|
||||
<PrioritySelector priority={object.priority} onPriorityChange={handlePriorityChanged} />
|
||||
<UserBox
|
||||
_class={contact.class.Employee}
|
||||
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">
|
||||
import { Ref, Space } from '@anticrm/core'
|
||||
import { Button, showPopup } from '@anticrm/ui'
|
||||
|
@ -13,24 +13,23 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { Ref, WithLookup } from '@anticrm/core'
|
||||
|
||||
import { IssueStatus } from '@anticrm/tracker'
|
||||
import { Button, Icon, Label, showPopup, SelectPopup, eventToHTMLElement } from '@anticrm/ui'
|
||||
import { issueStatuses } from '../utils'
|
||||
import { Button, Icon, showPopup, SelectPopup, eventToHTMLElement } from '@anticrm/ui'
|
||||
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 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
|
||||
|
||||
const statusesInfo = [
|
||||
IssueStatus.Backlog,
|
||||
IssueStatus.Todo,
|
||||
IssueStatus.InProgress,
|
||||
IssueStatus.Done,
|
||||
IssueStatus.Canceled
|
||||
].map((s) => ({ id: s, ...issueStatuses[s] }))
|
||||
$: selectedStatus = statuses.find((status) => status._id === selectedStatusId) ?? statuses[0]
|
||||
$: selectedStatusIcon = selectedStatus?.$lookup?.category?.icon
|
||||
$: selectedStatusLabel = shouldShowLabel ? selectedStatus?.name : undefined
|
||||
$: statusesInfo = statuses.map((s) => ({ id: s._id, text: s.name, color: s.color, icon: s.$lookup?.category?.icon }))
|
||||
|
||||
const handleStatusEditorOpened = (event: MouseEvent) => {
|
||||
if (!isEditable) {
|
||||
@ -47,23 +46,28 @@
|
||||
|
||||
{#if kind === 'button'}
|
||||
<Button
|
||||
label={shouldShowLabel ? issueStatuses[status].label : undefined}
|
||||
icon={issueStatuses[status].icon}
|
||||
icon={selectedStatusIcon}
|
||||
width="min-content"
|
||||
size="small"
|
||||
kind="no-border"
|
||||
on:click={handleStatusEditorOpened}
|
||||
/>
|
||||
>
|
||||
<svelte:fragment slot="content">
|
||||
{#if selectedStatusLabel}
|
||||
<span class="nowrap">{selectedStatusLabel}</span>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
</Button>
|
||||
{:else if kind === 'icon'}
|
||||
<div class={isEditable ? 'flex-presenter' : 'presenter'} on:click={handleStatusEditorOpened}>
|
||||
<div class="statusIcon">
|
||||
<Icon icon={issueStatuses[status].icon} size={'small'} />
|
||||
</div>
|
||||
{#if shouldShowLabel}
|
||||
<div class="label nowrap ml-2">
|
||||
<Label label={issueStatuses[status].label} />
|
||||
{#if selectedStatusIcon}
|
||||
<div class="statusIcon">
|
||||
<Icon icon={selectedStatusIcon} size={'small'} />
|
||||
</div>
|
||||
{/if}
|
||||
{#if selectedStatusLabel}
|
||||
<div class="label nowrap ml-2">{selectedStatusLabel}</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/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">
|
||||
import { Ref } from '@anticrm/core'
|
||||
import { IssueStatus, Team, IssuesDateModificationPeriod } from '@anticrm/tracker'
|
||||
import Issues from './Issues.svelte'
|
||||
import { Team, IssuesDateModificationPeriod } from '@anticrm/tracker'
|
||||
import tracker from '../../plugin'
|
||||
import Issues from './Issues.svelte'
|
||||
|
||||
export let currentSpace: Ref<Team>
|
||||
|
||||
@ -12,6 +26,6 @@
|
||||
<Issues
|
||||
{currentSpace}
|
||||
{completedIssuesPeriod}
|
||||
includedGroups={{ status: [IssueStatus.InProgress, IssueStatus.Todo] }}
|
||||
includedGroups={{ status: [tracker.issueStatusCategory.Unstarted, tracker.issueStatusCategory.Started] }}
|
||||
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">
|
||||
import { Ref } from '@anticrm/core'
|
||||
import { IssueStatus, Team, IssuesDateModificationPeriod } from '@anticrm/tracker'
|
||||
import Issues from './Issues.svelte'
|
||||
import { Team, IssuesDateModificationPeriod } from '@anticrm/tracker'
|
||||
import tracker from '../../plugin'
|
||||
import Issues from './Issues.svelte'
|
||||
|
||||
export let currentSpace: Ref<Team>
|
||||
|
||||
@ -13,5 +27,5 @@
|
||||
title={tracker.string.BacklogIssues}
|
||||
{currentSpace}
|
||||
{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">
|
||||
import contact from '@anticrm/contact'
|
||||
import { Class, Doc, FindOptions, Ref, WithLookup } from '@anticrm/core'
|
||||
import { Kanban } from '@anticrm/kanban'
|
||||
import { Class, Doc, FindOptions, Ref, SortingOrder, WithLookup } from '@anticrm/core'
|
||||
import { Kanban, TypeState } from '@anticrm/kanban'
|
||||
import { createQuery } from '@anticrm/presentation'
|
||||
import { Issue, IssueStatus, Team } from '@anticrm/tracker'
|
||||
import { Button, Component, eventToHTMLElement, Icon, IconAdd, IconMoreH, showPopup, Tooltip } from '@anticrm/ui'
|
||||
import view from '@anticrm/view'
|
||||
import { Issue, Team } from '@anticrm/tracker'
|
||||
import { Button, eventToHTMLElement, Icon, IconAdd, showPopup, Tooltip } from '@anticrm/ui'
|
||||
import { focusStore, ListSelectionProvider, selectionStore } from '@anticrm/view-resources'
|
||||
import ActionContext from '@anticrm/view-resources/src/components/ActionContext.svelte'
|
||||
import Menu from '@anticrm/view-resources/src/components/Menu.svelte'
|
||||
import { onMount } from 'svelte'
|
||||
import tracker from '../../plugin'
|
||||
import CreateIssue from '../CreateIssue.svelte'
|
||||
import AssigneePresenter from './AssigneePresenter.svelte';
|
||||
import AssigneePresenter from './AssigneePresenter.svelte'
|
||||
import IssuePresenter from './IssuePresenter.svelte'
|
||||
import PriorityPresenter from './PriorityPresenter.svelte';
|
||||
import PriorityPresenter from './PriorityPresenter.svelte'
|
||||
|
||||
export let currentSpace: Ref<Team>
|
||||
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 */
|
||||
|
||||
const spaceQuery = createQuery()
|
||||
const statusesQuery = createQuery()
|
||||
|
||||
let currentTeam: Team | undefined
|
||||
|
||||
$: spaceQuery.query(tracker.class.Team, { _id: currentSpace }, (res) => {
|
||||
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 no-unused-vars */
|
||||
let issue: Issue
|
||||
@ -107,7 +101,7 @@ import PriorityPresenter from './PriorityPresenter.svelte';
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if currentTeam}
|
||||
{#if currentTeam && states}
|
||||
<ActionContext
|
||||
context={{
|
||||
mode: 'browser'
|
||||
@ -141,7 +135,7 @@ import PriorityPresenter from './PriorityPresenter.svelte';
|
||||
on:contextmenu={(evt) => showMenu(evt.detail.evt, evt.detail.objects)}
|
||||
>
|
||||
<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-row-center gap-2">
|
||||
<Icon icon={state.icon} size={'small'} />
|
||||
@ -188,10 +182,6 @@ import PriorityPresenter from './PriorityPresenter.svelte';
|
||||
min-height: 6rem;
|
||||
user-select: none;
|
||||
|
||||
.filter {
|
||||
border-bottom: 1px solid var(--divider-color);
|
||||
}
|
||||
|
||||
.label {
|
||||
color: var(--theme-caption-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">
|
||||
import contact from '@anticrm/contact'
|
||||
import { DocumentQuery, FindOptions, Ref } from '@anticrm/core'
|
||||
import { Issue, Team } from '@anticrm/tracker'
|
||||
import { DocumentQuery, FindOptions, Ref, WithLookup } from '@anticrm/core'
|
||||
import { Issue, IssueStatus, Team } from '@anticrm/tracker'
|
||||
import { Component, Button, eventToHTMLElement, IconAdd, Scroller, showPopup, Tooltip } from '@anticrm/ui'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import tracker from '../../plugin'
|
||||
@ -12,13 +26,15 @@
|
||||
export let query: DocumentQuery<Issue>
|
||||
export let groupBy: { key: IssuesGroupByKeys | undefined; group: Issue[IssuesGroupByKeys] | undefined }
|
||||
export let orderBy: IssuesOrderByKeys
|
||||
export let statuses: WithLookup<IssueStatus>[]
|
||||
export let currentSpace: Ref<Team> | undefined = undefined
|
||||
export let currentTeam: Team
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
const options: FindOptions<Issue> = {
|
||||
lookup: {
|
||||
assignee: contact.class.Employee
|
||||
assignee: contact.class.Employee,
|
||||
status: tracker.class.IssueStatus
|
||||
}
|
||||
}
|
||||
|
||||
@ -46,7 +62,8 @@
|
||||
isEditable: false,
|
||||
shouldShowLabel: true,
|
||||
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>
|
||||
@ -64,7 +81,7 @@
|
||||
itemsConfig={[
|
||||
{ key: '', presenter: tracker.component.PriorityPresenter, props: { currentSpace } },
|
||||
{ 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.DueDatePresenter, props: { currentSpace } },
|
||||
{ key: 'modifiedOn', presenter: tracker.component.ModificationDatePresenter },
|
||||
|
@ -13,14 +13,14 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { Ref, Timestamp } from '@anticrm/core'
|
||||
import { Issue, IssueStatus, Team } from '@anticrm/tracker'
|
||||
import { Ref, Timestamp, WithLookup } from '@anticrm/core'
|
||||
import { Issue, Team } from '@anticrm/tracker'
|
||||
import { DatePresenter, Tooltip, getDaysDifference } from '@anticrm/ui'
|
||||
import { getClient } from '@anticrm/presentation'
|
||||
import DueDatePopup from './DueDatePopup.svelte'
|
||||
import tracker from '../../plugin'
|
||||
|
||||
export let value: Issue
|
||||
export let value: WithLookup<Issue>
|
||||
export let currentSpace: Ref<Team> | undefined = undefined
|
||||
|
||||
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>
|
||||
|
||||
{#if shouldRenderPresenter}
|
||||
|
@ -14,7 +14,7 @@
|
||||
-->
|
||||
<script lang="ts">
|
||||
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 {
|
||||
Issue,
|
||||
@ -22,7 +22,8 @@
|
||||
IssuesGrouping,
|
||||
IssuesOrdering,
|
||||
IssuesDateModificationPeriod,
|
||||
IssueStatus
|
||||
IssueStatus,
|
||||
IssueStatusCategory
|
||||
} from '@anticrm/tracker'
|
||||
import { Button, Label, ScrollBox, IconOptions, showPopup, eventToHTMLElement } from '@anticrm/ui'
|
||||
import CategoryPresenter from './CategoryPresenter.svelte'
|
||||
@ -50,9 +51,11 @@
|
||||
const ENTRIES_LIMIT = 200
|
||||
const spaceQuery = createQuery()
|
||||
const issuesQuery = createQuery()
|
||||
const statusesQuery = createQuery()
|
||||
const issuesMap: { [status: string]: number } = {}
|
||||
let currentTeam: Team | undefined
|
||||
let issues: Issue[] = []
|
||||
let statusesById: ReadonlyMap<Ref<IssueStatus>, WithLookup<IssueStatus>> = new Map()
|
||||
|
||||
$: totalIssues = getTotalIssues(issuesMap)
|
||||
|
||||
@ -71,25 +74,41 @@
|
||||
|
||||
$: groupByKey = issuesGroupKeyMap[groupingKey]
|
||||
$: categories = getCategories(groupByKey, issues, !!shouldShowEmptyGroups)
|
||||
$: displayedCategories = (categories as any[]).filter((x: ReturnType<typeof getCategories>) => {
|
||||
return (
|
||||
groupByKey === undefined || includedGroups[groupByKey] === undefined || includedGroups[groupByKey]?.includes(x)
|
||||
)
|
||||
})
|
||||
$: includedIssuesQuery = getIncludedIssuesQuery(includedGroups)
|
||||
$: filteredIssuesQuery = getModifiedOnIssuesFilterQuery(issues, completedIssuesPeriod)
|
||||
$: displayedCategories = (categories as any[]).filter((x) => {
|
||||
if (groupByKey === undefined || includedGroups[groupByKey] === undefined) {
|
||||
return true
|
||||
}
|
||||
|
||||
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[] } } = {}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
const getModifiedOnIssuesFilterQuery = (currentIssues: Issue[], period: IssuesDateModificationPeriod | null) => {
|
||||
const getModifiedOnIssuesFilterQuery = (
|
||||
currentIssues: WithLookup<Issue>[],
|
||||
period: IssuesDateModificationPeriod | null
|
||||
) => {
|
||||
const filter: { _id: { $in: Array<Ref<Issue>> } } = { _id: { $in: [] } }
|
||||
|
||||
if (!period || period === IssuesDateModificationPeriod.All) {
|
||||
@ -97,7 +116,10 @@
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@ -116,6 +138,18 @@
|
||||
{ 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) => {
|
||||
if (!key) {
|
||||
return [undefined] // No grouping
|
||||
@ -132,6 +166,15 @@
|
||||
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 }) => {
|
||||
let total = 0
|
||||
|
||||
@ -197,6 +240,7 @@
|
||||
groupBy={{ key: groupByKey, group: category }}
|
||||
orderBy={issuesOrderKeyMap[orderingKey]}
|
||||
query={resultQuery}
|
||||
{statuses}
|
||||
{currentSpace}
|
||||
{currentTeam}
|
||||
on:content={(event) => {
|
||||
|
@ -13,7 +13,7 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { Ref } from '@anticrm/core'
|
||||
import { Ref, WithLookup } from '@anticrm/core'
|
||||
import { Issue, IssueStatus, Team } from '@anticrm/tracker'
|
||||
import { getClient } from '@anticrm/presentation'
|
||||
import { Tooltip } from '@anticrm/ui'
|
||||
@ -21,13 +21,14 @@
|
||||
import StatusSelector from '../StatusSelector.svelte'
|
||||
|
||||
export let value: Issue
|
||||
export let statuses: WithLookup<IssueStatus>[]
|
||||
export let currentSpace: Ref<Team> | undefined = undefined
|
||||
export let isEditable: boolean = true
|
||||
export let shouldShowLabel: boolean = false
|
||||
|
||||
const client = getClient()
|
||||
|
||||
const handleStatusChanged = async (newStatus: IssueStatus | undefined) => {
|
||||
const handleStatusChanged = async (newStatus: Ref<IssueStatus> | undefined) => {
|
||||
if (!isEditable || newStatus === undefined) {
|
||||
return
|
||||
}
|
||||
@ -49,7 +50,8 @@
|
||||
kind={'icon'}
|
||||
{isEditable}
|
||||
{shouldShowLabel}
|
||||
status={value.status}
|
||||
{statuses}
|
||||
selectedStatusId={value.status}
|
||||
onStatusChange={handleStatusChanged}
|
||||
/>
|
||||
</Tooltip>
|
||||
@ -58,7 +60,8 @@
|
||||
kind={'icon'}
|
||||
{isEditable}
|
||||
{shouldShowLabel}
|
||||
status={value.status}
|
||||
{statuses}
|
||||
selectedStatusId={value.status}
|
||||
onStatusChange={handleStatusChanged}
|
||||
/>
|
||||
{/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");
|
||||
// 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,
|
||||
Description: '' 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,
|
||||
Assignee: '' as IntlString,
|
||||
AssignTo: '' as IntlString,
|
||||
|
@ -1,6 +1,5 @@
|
||||
//
|
||||
// Copyright © 2020, 2021 Anticrm Platform Contributors.
|
||||
// Copyright © 2021 Hardcore Engineering Inc.
|
||||
// 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
|
||||
@ -16,10 +15,15 @@
|
||||
|
||||
import { Ref, SortingOrder } from '@anticrm/core'
|
||||
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 { LexoDecimal, LexoNumeralSystem36, LexoRank } from 'lexorank'
|
||||
import LexoRankBucket from 'lexorank/lib/lexoRank/lexoRankBucket'
|
||||
import tracker from './plugin'
|
||||
|
||||
export interface NavigationItem {
|
||||
@ -36,41 +40,6 @@ export interface Selection {
|
||||
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 }> = {
|
||||
[IssuePriority.NoPriority]: { icon: tracker.icon.PriorityNoPriority, label: tracker.string.NoPriority },
|
||||
[IssuePriority.Urgent]: { icon: tracker.icon.PriorityUrgent, label: tracker.string.Urgent },
|
||||
@ -99,7 +68,7 @@ export const issuesDateModificationPeriodOptions: Record<IssuesDateModificationP
|
||||
[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 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>> = {
|
||||
status: [IssueStatus.InProgress, IssueStatus.Todo, IssueStatus.Backlog, IssueStatus.Done, IssueStatus.Canceled],
|
||||
priority: [IssuePriority.NoPriority, IssuePriority.Urgent, IssuePriority.High, IssuePriority.Medium, IssuePriority.Low]
|
||||
}
|
||||
|
||||
|
@ -32,6 +32,7 @@
|
||||
"@anticrm/contact": "~0.6.5",
|
||||
"@anticrm/chunter": "~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 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 { plugin } from '@anticrm/platform'
|
||||
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
|
||||
*/
|
||||
@ -26,16 +48,8 @@ export interface Team extends Space {
|
||||
teamLogo?: string | null
|
||||
identifier: string // Team identifier
|
||||
sequence: number
|
||||
}
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export enum IssueStatus {
|
||||
Backlog,
|
||||
Todo,
|
||||
InProgress,
|
||||
Done,
|
||||
Canceled
|
||||
issueStatuses: number
|
||||
defaultIssueStatus: Ref<IssueStatus>
|
||||
}
|
||||
|
||||
/**
|
||||
@ -84,7 +98,7 @@ export enum IssuesDateModificationPeriod {
|
||||
export interface Issue extends Doc {
|
||||
title: string
|
||||
description: Markup
|
||||
status: IssueStatus
|
||||
status: Ref<IssueStatus>
|
||||
priority: IssuePriority
|
||||
|
||||
number: number
|
||||
@ -153,17 +167,28 @@ export interface Project extends Doc {
|
||||
*/
|
||||
export const trackerId = 'tracker' as Plugin
|
||||
|
||||
export * from './utils'
|
||||
|
||||
export default plugin(trackerId, {
|
||||
class: {
|
||||
Team: '' as Ref<Class<Team>>,
|
||||
Issue: '' as Ref<Class<Issue>>,
|
||||
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: {
|
||||
Tracker: '' 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: {
|
||||
TrackerApplication: '' as Asset,
|
||||
Project: '' as Asset,
|
||||
@ -183,11 +208,12 @@ export default plugin(trackerId, {
|
||||
DueDate: '' as Asset,
|
||||
Parent: '' as Asset,
|
||||
|
||||
StatusBacklog: '' as Asset,
|
||||
StatusTodo: '' as Asset,
|
||||
StatusInProgress: '' as Asset,
|
||||
StatusDone: '' as Asset,
|
||||
StatusCanceled: '' as Asset,
|
||||
CategoryBacklog: '' as Asset,
|
||||
CategoryUnstarted: '' as Asset,
|
||||
CategoryStarted: '' as Asset,
|
||||
CategoryCompleted: '' as Asset,
|
||||
CategoryCanceled: '' as Asset,
|
||||
|
||||
PriorityNoPriority: '' as Asset,
|
||||
PriorityUrgent: '' 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