2022-03-31 08:32:42 +00:00
|
|
|
//
|
2022-04-23 16:59:57 +00:00
|
|
|
// Copyright © 2022 Hardcore Engineering Inc.
|
2022-03-31 08:32:42 +00:00
|
|
|
//
|
|
|
|
// 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.
|
|
|
|
//
|
|
|
|
|
2023-04-04 06:11:49 +00:00
|
|
|
import { Employee } from '@hcengineering/contact'
|
2023-01-19 07:49:13 +00:00
|
|
|
import core, {
|
2023-03-29 13:04:28 +00:00
|
|
|
ApplyOperations,
|
2023-05-15 17:16:41 +00:00
|
|
|
AttachedData,
|
2023-03-29 13:04:28 +00:00
|
|
|
AttachedDoc,
|
2023-02-08 15:37:50 +00:00
|
|
|
Class,
|
2023-03-29 13:04:28 +00:00
|
|
|
Collection,
|
2023-01-19 07:49:13 +00:00
|
|
|
Doc,
|
|
|
|
DocumentQuery,
|
2023-03-29 13:04:28 +00:00
|
|
|
DocumentUpdate,
|
2023-01-19 07:49:13 +00:00
|
|
|
Ref,
|
|
|
|
SortingOrder,
|
2023-05-15 17:16:41 +00:00
|
|
|
Status,
|
2023-04-04 06:11:49 +00:00
|
|
|
StatusCategory,
|
2023-05-15 17:16:41 +00:00
|
|
|
StatusManager,
|
2023-04-04 06:11:49 +00:00
|
|
|
StatusValue,
|
2023-01-19 07:49:13 +00:00
|
|
|
toIdMap,
|
|
|
|
TxCollectionCUD,
|
|
|
|
TxOperations,
|
2023-04-11 11:21:13 +00:00
|
|
|
TxResult,
|
2023-04-04 06:11:49 +00:00
|
|
|
TxUpdateDoc
|
2023-01-19 07:49:13 +00:00
|
|
|
} from '@hcengineering/core'
|
2023-04-04 06:11:49 +00:00
|
|
|
import { Asset, IntlString } from '@hcengineering/platform'
|
2023-04-17 06:08:41 +00:00
|
|
|
import { createQuery } from '@hcengineering/presentation'
|
2023-04-04 06:11:49 +00:00
|
|
|
import { calcRank } from '@hcengineering/task'
|
2022-04-23 16:59:57 +00:00
|
|
|
import {
|
|
|
|
Issue,
|
2023-01-14 10:54:54 +00:00
|
|
|
IssuePriority,
|
2022-04-27 08:18:59 +00:00
|
|
|
IssuesDateModificationPeriod,
|
2022-06-24 12:36:08 +00:00
|
|
|
IssuesGrouping,
|
|
|
|
IssuesOrdering,
|
2023-05-16 11:56:29 +00:00
|
|
|
Milestone,
|
|
|
|
MilestoneStatus,
|
2023-05-29 18:17:14 +00:00
|
|
|
Project,
|
2022-12-07 09:40:19 +00:00
|
|
|
TimeReportDayType
|
2022-09-21 08:08:25 +00:00
|
|
|
} from '@hcengineering/tracker'
|
|
|
|
import {
|
|
|
|
AnyComponent,
|
|
|
|
AnySvelteComponent,
|
2022-11-24 16:50:55 +00:00
|
|
|
areDatesEqual,
|
2022-09-21 08:08:25 +00:00
|
|
|
getMillisecondsInMonth,
|
|
|
|
isWeekend,
|
2023-05-29 18:17:14 +00:00
|
|
|
MILLISECONDS_IN_WEEK,
|
|
|
|
PaletteColorIndexes
|
2022-09-21 08:08:25 +00:00
|
|
|
} from '@hcengineering/ui'
|
2023-03-23 05:41:27 +00:00
|
|
|
import { ViewletDescriptor } from '@hcengineering/view'
|
2023-04-04 06:11:49 +00:00
|
|
|
import { CategoryQuery, groupBy, ListSelectionProvider, SelectDirection } from '@hcengineering/view-resources'
|
2022-04-08 06:27:02 +00:00
|
|
|
import tracker from './plugin'
|
2023-05-29 18:17:14 +00:00
|
|
|
import { defaultMilestoneStatuses, defaultPriorities } from './types'
|
2022-06-24 12:36:08 +00:00
|
|
|
|
|
|
|
export * from './types'
|
2022-03-31 08:32:42 +00:00
|
|
|
|
2022-07-08 17:42:23 +00:00
|
|
|
export const UNSET_COLOR = -1
|
|
|
|
|
2022-03-31 08:32:42 +00:00
|
|
|
export interface NavigationItem {
|
|
|
|
id: string
|
|
|
|
label: IntlString
|
|
|
|
icon: Asset
|
|
|
|
component: AnyComponent
|
|
|
|
componentProps?: Record<string, string>
|
|
|
|
top: boolean
|
|
|
|
}
|
|
|
|
|
|
|
|
export interface Selection {
|
2023-03-17 06:17:53 +00:00
|
|
|
currentProject?: Ref<Project>
|
2022-03-31 08:32:42 +00:00
|
|
|
currentSpecial?: string
|
|
|
|
}
|
|
|
|
|
2023-05-16 11:56:29 +00:00
|
|
|
export type IssuesGroupByKeys = keyof Pick<Issue, 'status' | 'priority' | 'assignee' | 'component' | 'milestone'>
|
2022-06-28 06:53:39 +00:00
|
|
|
export type IssuesOrderByKeys = keyof Pick<Issue, 'status' | 'priority' | 'modifiedOn' | 'dueDate' | 'rank'>
|
2022-04-20 06:06:05 +00:00
|
|
|
|
|
|
|
export const issuesGroupKeyMap: Record<IssuesGrouping, IssuesGroupByKeys | undefined> = {
|
|
|
|
[IssuesGrouping.Status]: 'status',
|
|
|
|
[IssuesGrouping.Priority]: 'priority',
|
|
|
|
[IssuesGrouping.Assignee]: 'assignee',
|
2023-03-17 06:17:53 +00:00
|
|
|
[IssuesGrouping.Component]: 'component',
|
2023-05-16 11:56:29 +00:00
|
|
|
[IssuesGrouping.Milestone]: 'milestone',
|
2022-04-20 06:06:05 +00:00
|
|
|
[IssuesGrouping.NoGrouping]: undefined
|
|
|
|
}
|
|
|
|
|
|
|
|
export const issuesOrderKeyMap: Record<IssuesOrdering, IssuesOrderByKeys> = {
|
|
|
|
[IssuesOrdering.Status]: 'status',
|
|
|
|
[IssuesOrdering.Priority]: 'priority',
|
|
|
|
[IssuesOrdering.LastUpdated]: 'modifiedOn',
|
2022-06-28 06:53:39 +00:00
|
|
|
[IssuesOrdering.DueDate]: 'dueDate',
|
|
|
|
[IssuesOrdering.Manual]: 'rank'
|
2022-04-20 06:06:05 +00:00
|
|
|
}
|
|
|
|
|
2023-05-16 11:56:29 +00:00
|
|
|
export const issuesGroupEditorMap: Record<'status' | 'priority' | 'component' | 'milestone', AnyComponent | undefined> =
|
|
|
|
{
|
|
|
|
status: tracker.component.StatusEditor,
|
|
|
|
priority: tracker.component.PriorityEditor,
|
|
|
|
component: tracker.component.ComponentEditor,
|
|
|
|
milestone: tracker.component.MilestoneEditor
|
|
|
|
}
|
2022-04-22 06:13:57 +00:00
|
|
|
|
2022-04-29 05:27:17 +00:00
|
|
|
export const getIssuesModificationDatePeriodTime = (period: IssuesDateModificationPeriod | null): number => {
|
2022-04-22 06:13:57 +00:00
|
|
|
const today = new Date(Date.now())
|
|
|
|
|
|
|
|
switch (period) {
|
|
|
|
case IssuesDateModificationPeriod.PastWeek: {
|
|
|
|
return today.getTime() - MILLISECONDS_IN_WEEK
|
|
|
|
}
|
|
|
|
case IssuesDateModificationPeriod.PastMonth: {
|
|
|
|
return today.getTime() - getMillisecondsInMonth(today)
|
|
|
|
}
|
|
|
|
default: {
|
|
|
|
return 0
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2022-04-26 08:46:35 +00:00
|
|
|
|
2022-05-05 06:35:26 +00:00
|
|
|
export interface FilterAction {
|
|
|
|
icon?: Asset | AnySvelteComponent
|
|
|
|
label?: IntlString
|
|
|
|
onSelect: (event: MouseEvent | KeyboardEvent) => void
|
|
|
|
}
|
|
|
|
|
|
|
|
export interface FilterSectionElement extends Omit<FilterAction, 'label'> {
|
|
|
|
title?: string
|
|
|
|
count?: number
|
|
|
|
isSelected?: boolean
|
|
|
|
}
|
|
|
|
|
2022-05-11 17:15:29 +00:00
|
|
|
export interface IssueFilter {
|
|
|
|
mode: '$in' | '$nin'
|
|
|
|
query: DocumentQuery<Issue>
|
|
|
|
}
|
|
|
|
|
2022-05-05 06:35:26 +00:00
|
|
|
export const getGroupedIssues = (
|
|
|
|
key: IssuesGroupByKeys | undefined,
|
|
|
|
elements: Issue[],
|
|
|
|
orderedCategories?: any[]
|
|
|
|
): { [p: string]: Issue[] } => {
|
|
|
|
if (key === undefined) {
|
|
|
|
return { [undefined as any]: elements }
|
|
|
|
}
|
|
|
|
|
|
|
|
const unorderedIssues = groupBy(elements, key)
|
|
|
|
|
|
|
|
if (orderedCategories === undefined || orderedCategories.length === 0) {
|
|
|
|
return unorderedIssues
|
|
|
|
}
|
|
|
|
|
|
|
|
return Object.keys(unorderedIssues)
|
|
|
|
.sort((o1, o2) => {
|
|
|
|
const key1 = o1 === 'null' ? null : o1
|
|
|
|
const key2 = o2 === 'null' ? null : o2
|
|
|
|
|
|
|
|
const i1 = orderedCategories.findIndex((x) => x === key1)
|
|
|
|
const i2 = orderedCategories.findIndex((x) => x === key2)
|
|
|
|
|
|
|
|
return i1 - i2
|
|
|
|
})
|
|
|
|
.reduce((obj: { [p: string]: any[] }, objKey) => {
|
|
|
|
obj[objKey] = unorderedIssues[objKey]
|
|
|
|
return obj
|
|
|
|
}, {})
|
|
|
|
}
|
|
|
|
|
|
|
|
export const getIssueFilterAssetsByType = (type: string): { icon: Asset, label: IntlString } | undefined => {
|
|
|
|
switch (type) {
|
|
|
|
case 'status': {
|
|
|
|
return {
|
|
|
|
icon: tracker.icon.CategoryBacklog,
|
|
|
|
label: tracker.string.Status
|
|
|
|
}
|
|
|
|
}
|
2022-05-11 17:15:29 +00:00
|
|
|
case 'priority': {
|
|
|
|
return {
|
|
|
|
icon: tracker.icon.PriorityHigh,
|
|
|
|
label: tracker.string.Priority
|
|
|
|
}
|
|
|
|
}
|
2023-03-17 06:17:53 +00:00
|
|
|
case 'component': {
|
2022-06-01 16:08:22 +00:00
|
|
|
return {
|
2023-03-17 06:17:53 +00:00
|
|
|
icon: tracker.icon.Component,
|
|
|
|
label: tracker.string.Component
|
2022-06-01 16:08:22 +00:00
|
|
|
}
|
|
|
|
}
|
2023-05-16 11:56:29 +00:00
|
|
|
case 'milestone': {
|
2022-08-08 06:54:36 +00:00
|
|
|
return {
|
2023-05-16 11:56:29 +00:00
|
|
|
icon: tracker.icon.Milestone,
|
|
|
|
label: tracker.string.Milestone
|
2022-08-08 06:54:36 +00:00
|
|
|
}
|
|
|
|
}
|
2022-05-05 06:35:26 +00:00
|
|
|
default: {
|
|
|
|
return undefined
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2022-05-11 17:15:29 +00:00
|
|
|
|
|
|
|
export const getArraysIntersection = (a: any[], b: any[]): any[] => {
|
|
|
|
const setB = new Set(b)
|
|
|
|
const intersection = new Set(a.filter((x) => setB.has(x)))
|
|
|
|
|
|
|
|
return Array.from(intersection)
|
|
|
|
}
|
|
|
|
|
|
|
|
export const getArraysUnion = (a: any[], b: any[]): any[] => {
|
|
|
|
const setB = new Set(b)
|
|
|
|
const union = new Set(a)
|
|
|
|
|
|
|
|
for (const element of setB) {
|
|
|
|
union.add(element)
|
|
|
|
}
|
|
|
|
|
|
|
|
return Array.from(union)
|
|
|
|
}
|
2022-05-18 04:04:54 +00:00
|
|
|
|
2023-04-28 09:35:20 +00:00
|
|
|
export type ComponentsFilterMode = 'all' | 'backlog' | 'active' | 'closed'
|
2022-05-18 15:57:11 +00:00
|
|
|
|
2023-05-16 11:56:29 +00:00
|
|
|
export type MilestoneViewMode = 'all' | 'planned' | 'active' | 'closed'
|
2022-08-03 07:05:19 +00:00
|
|
|
|
2023-02-03 05:47:25 +00:00
|
|
|
export type ScrumRecordViewMode = 'timeReports' | 'objects'
|
|
|
|
|
2023-05-16 11:56:29 +00:00
|
|
|
export const getIncludedMilestoneStatuses = (mode: MilestoneViewMode): MilestoneStatus[] => {
|
2022-08-03 07:05:19 +00:00
|
|
|
switch (mode) {
|
|
|
|
case 'all': {
|
2023-05-16 11:56:29 +00:00
|
|
|
return defaultMilestoneStatuses
|
2022-08-03 07:05:19 +00:00
|
|
|
}
|
|
|
|
case 'active': {
|
2023-05-16 11:56:29 +00:00
|
|
|
return [MilestoneStatus.InProgress]
|
2022-08-03 07:05:19 +00:00
|
|
|
}
|
|
|
|
case 'planned': {
|
2023-05-16 11:56:29 +00:00
|
|
|
return [MilestoneStatus.Planned]
|
2022-08-03 07:05:19 +00:00
|
|
|
}
|
|
|
|
case 'closed': {
|
2023-05-16 11:56:29 +00:00
|
|
|
return [MilestoneStatus.Completed, MilestoneStatus.Canceled]
|
2022-08-03 07:05:19 +00:00
|
|
|
}
|
|
|
|
default: {
|
|
|
|
return []
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-04-28 09:35:20 +00:00
|
|
|
export const componentsTitleMap: Record<ComponentsFilterMode, IntlString> = Object.freeze({
|
2023-03-17 06:17:53 +00:00
|
|
|
all: tracker.string.AllComponents,
|
|
|
|
backlog: tracker.string.BacklogComponents,
|
|
|
|
active: tracker.string.ActiveComponents,
|
|
|
|
closed: tracker.string.ClosedComponents
|
2022-05-19 10:37:41 +00:00
|
|
|
})
|
2022-06-08 14:03:26 +00:00
|
|
|
|
2023-05-16 11:56:29 +00:00
|
|
|
export const milestoneTitleMap: Record<MilestoneViewMode, IntlString> = Object.freeze({
|
|
|
|
all: tracker.string.AllMilestones,
|
|
|
|
planned: tracker.string.PlannedMilestones,
|
|
|
|
active: tracker.string.ActiveMilestones,
|
|
|
|
closed: tracker.string.ClosedMilestones
|
2022-08-03 07:05:19 +00:00
|
|
|
})
|
|
|
|
|
2023-02-03 05:47:25 +00:00
|
|
|
export const scrumRecordTitleMap: Record<ScrumRecordViewMode, IntlString> = Object.freeze({
|
|
|
|
timeReports: tracker.string.ScrumRecordTimeReports,
|
|
|
|
objects: tracker.string.ScrumRecordObjects
|
|
|
|
})
|
|
|
|
|
2022-06-09 14:51:45 +00:00
|
|
|
const listIssueStatusOrder = [
|
|
|
|
tracker.issueStatusCategory.Started,
|
|
|
|
tracker.issueStatusCategory.Unstarted,
|
|
|
|
tracker.issueStatusCategory.Backlog,
|
|
|
|
tracker.issueStatusCategory.Completed,
|
|
|
|
tracker.issueStatusCategory.Canceled
|
|
|
|
] as const
|
|
|
|
|
2023-03-23 05:41:27 +00:00
|
|
|
const listIssueKanbanStatusOrder = [
|
|
|
|
tracker.issueStatusCategory.Backlog,
|
|
|
|
tracker.issueStatusCategory.Unstarted,
|
|
|
|
tracker.issueStatusCategory.Started,
|
|
|
|
tracker.issueStatusCategory.Completed,
|
|
|
|
tracker.issueStatusCategory.Canceled
|
|
|
|
] as const
|
|
|
|
|
|
|
|
export async function issueStatusSort (
|
2023-04-04 06:11:49 +00:00
|
|
|
value: StatusValue[],
|
2023-03-23 05:41:27 +00:00
|
|
|
viewletDescriptorId?: Ref<ViewletDescriptor>
|
2023-04-04 06:11:49 +00:00
|
|
|
): Promise<StatusValue[]> {
|
|
|
|
// TODO: How we track category updates.
|
|
|
|
|
|
|
|
if (viewletDescriptorId === tracker.viewlet.Kanban) {
|
|
|
|
value.sort((a, b) => {
|
|
|
|
const res =
|
|
|
|
listIssueKanbanStatusOrder.indexOf(a.values[0].category as Ref<StatusCategory>) -
|
|
|
|
listIssueKanbanStatusOrder.indexOf(b.values[0].category as Ref<StatusCategory>)
|
|
|
|
if (res === 0) {
|
|
|
|
return a.values[0].rank.localeCompare(b.values[0].rank)
|
2023-03-23 05:41:27 +00:00
|
|
|
}
|
2023-04-04 06:11:49 +00:00
|
|
|
return res
|
2022-08-16 10:19:33 +00:00
|
|
|
})
|
2023-04-04 06:11:49 +00:00
|
|
|
} else {
|
|
|
|
value.sort((a, b) => {
|
|
|
|
const res =
|
|
|
|
listIssueStatusOrder.indexOf(a.values[0].category as Ref<StatusCategory>) -
|
|
|
|
listIssueStatusOrder.indexOf(b.values[0].category as Ref<StatusCategory>)
|
|
|
|
if (res === 0) {
|
|
|
|
return a.values[0].rank.localeCompare(b.values[0].rank)
|
|
|
|
}
|
|
|
|
return res
|
|
|
|
})
|
|
|
|
}
|
|
|
|
return value
|
2023-01-14 10:54:54 +00:00
|
|
|
}
|
2022-06-08 14:03:26 +00:00
|
|
|
|
2023-01-14 10:54:54 +00:00
|
|
|
export async function issuePrioritySort (value: IssuePriority[]): Promise<IssuePriority[]> {
|
|
|
|
value.sort((a, b) => {
|
|
|
|
const i1 = defaultPriorities.indexOf(a)
|
|
|
|
const i2 = defaultPriorities.indexOf(b)
|
2022-06-08 14:03:26 +00:00
|
|
|
|
2023-05-29 18:17:14 +00:00
|
|
|
return i2 - i1
|
2023-01-14 10:54:54 +00:00
|
|
|
})
|
|
|
|
return value
|
|
|
|
}
|
2022-06-08 14:03:26 +00:00
|
|
|
|
2023-05-16 11:56:29 +00:00
|
|
|
export async function milestoneSort (value: Array<Ref<Milestone>>): Promise<Array<Ref<Milestone>>> {
|
2023-01-14 10:54:54 +00:00
|
|
|
return await new Promise((resolve) => {
|
|
|
|
const query = createQuery(true)
|
2023-05-16 11:56:29 +00:00
|
|
|
query.query(tracker.class.Milestone, { _id: { $in: value } }, (res) => {
|
|
|
|
const milestones = toIdMap(res)
|
|
|
|
value.sort((a, b) => (milestones.get(b)?.targetDate ?? 0) - (milestones.get(a)?.targetDate ?? 0))
|
2023-01-15 10:26:30 +00:00
|
|
|
resolve(value)
|
2023-01-14 10:54:54 +00:00
|
|
|
query.unsubscribe()
|
2022-06-08 14:03:26 +00:00
|
|
|
})
|
2023-01-14 10:54:54 +00:00
|
|
|
})
|
2022-06-08 14:03:26 +00:00
|
|
|
}
|
2022-06-09 14:49:57 +00:00
|
|
|
|
2023-05-16 11:56:29 +00:00
|
|
|
export async function moveIssuesToAnotherMilestone (
|
2022-11-21 05:44:46 +00:00
|
|
|
client: TxOperations,
|
2023-05-16 11:56:29 +00:00
|
|
|
oldMilestone: Milestone,
|
|
|
|
newMilestone: Milestone | undefined
|
2022-11-21 05:44:46 +00:00
|
|
|
): Promise<boolean> {
|
|
|
|
try {
|
2023-05-16 11:56:29 +00:00
|
|
|
// Find all Issues by Milestone
|
|
|
|
const movedIssues = await client.findAll(tracker.class.Issue, { milestone: oldMilestone._id })
|
2022-11-21 05:44:46 +00:00
|
|
|
|
2023-05-16 11:56:29 +00:00
|
|
|
// Update Issues by new Milestone
|
2023-04-11 11:21:13 +00:00
|
|
|
const awaitedUpdates: Array<Promise<TxResult>> = []
|
2022-11-21 05:44:46 +00:00
|
|
|
for (const issue of movedIssues) {
|
2023-05-16 11:56:29 +00:00
|
|
|
awaitedUpdates.push(client.update(issue, { milestone: newMilestone?._id ?? null }))
|
2022-11-21 05:44:46 +00:00
|
|
|
}
|
|
|
|
await Promise.all(awaitedUpdates)
|
|
|
|
|
|
|
|
return true
|
|
|
|
} catch (error) {
|
|
|
|
console.error(
|
2023-05-16 11:56:29 +00:00
|
|
|
`Error happened while moving issues between milestones from ${oldMilestone.label} to ${
|
|
|
|
newMilestone?.label ?? 'No Milestone'
|
2022-11-21 05:44:46 +00:00
|
|
|
}: `,
|
|
|
|
error
|
|
|
|
)
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
}
|
2022-11-24 16:50:55 +00:00
|
|
|
|
2022-12-07 09:40:19 +00:00
|
|
|
export function getTimeReportDate (type: TimeReportDayType): number {
|
2022-11-24 16:50:55 +00:00
|
|
|
const date = new Date(Date.now())
|
2022-12-07 09:40:19 +00:00
|
|
|
|
|
|
|
if (type === TimeReportDayType.PreviousWorkDay) {
|
2022-11-24 16:50:55 +00:00
|
|
|
date.setDate(date.getDate() - 1)
|
|
|
|
}
|
|
|
|
|
2022-12-07 09:40:19 +00:00
|
|
|
// if date is day off then set date to last working day
|
2022-11-24 16:50:55 +00:00
|
|
|
while (isWeekend(date)) {
|
|
|
|
date.setDate(date.getDate() - 1)
|
|
|
|
}
|
|
|
|
|
|
|
|
return date.valueOf()
|
|
|
|
}
|
|
|
|
|
2022-12-07 09:40:19 +00:00
|
|
|
export function getTimeReportDayType (timestamp: number): TimeReportDayType | undefined {
|
2022-11-24 16:50:55 +00:00
|
|
|
const date = new Date(timestamp)
|
2022-12-07 09:40:19 +00:00
|
|
|
const currentWorkDate = new Date(getTimeReportDate(TimeReportDayType.CurrentWorkDay))
|
|
|
|
const previousWorkDate = new Date(getTimeReportDate(TimeReportDayType.PreviousWorkDay))
|
2022-11-24 16:50:55 +00:00
|
|
|
|
|
|
|
if (areDatesEqual(date, currentWorkDate)) {
|
2022-12-07 09:40:19 +00:00
|
|
|
return TimeReportDayType.CurrentWorkDay
|
2022-11-24 16:50:55 +00:00
|
|
|
} else if (areDatesEqual(date, previousWorkDate)) {
|
2022-12-07 09:40:19 +00:00
|
|
|
return TimeReportDayType.PreviousWorkDay
|
2022-11-24 16:50:55 +00:00
|
|
|
}
|
|
|
|
}
|
2023-01-14 10:54:54 +00:00
|
|
|
|
|
|
|
export function subIssueQuery (value: boolean, query: DocumentQuery<Issue>): DocumentQuery<Issue> {
|
|
|
|
return value ? query : { ...query, attachedTo: tracker.ids.NoParent }
|
|
|
|
}
|
|
|
|
|
2023-02-08 15:37:50 +00:00
|
|
|
async function getAllSomething (
|
|
|
|
_class: Ref<Class<Doc>>,
|
2023-04-05 12:23:55 +00:00
|
|
|
query: DocumentQuery<Doc> | undefined,
|
2023-02-08 15:37:50 +00:00
|
|
|
onUpdate: () => void,
|
|
|
|
queryId: Ref<Doc>
|
|
|
|
): Promise<any[] | undefined> {
|
|
|
|
const promise = new Promise<Array<Ref<Doc>>>((resolve, reject) => {
|
|
|
|
let refresh: boolean = false
|
|
|
|
const lq = CategoryQuery.getLiveQuery(queryId)
|
2023-04-05 12:23:55 +00:00
|
|
|
refresh = lq.query(_class, query ?? {}, (res) => {
|
|
|
|
const result = res.map((p) => p._id)
|
|
|
|
CategoryQuery.results.set(queryId, result)
|
|
|
|
resolve(result)
|
|
|
|
onUpdate()
|
|
|
|
})
|
2023-02-08 15:37:50 +00:00
|
|
|
|
|
|
|
if (!refresh) {
|
|
|
|
resolve(CategoryQuery.results.get(queryId) ?? [])
|
|
|
|
}
|
2023-02-08 06:42:26 +00:00
|
|
|
})
|
2023-02-08 15:37:50 +00:00
|
|
|
return await promise
|
2023-02-08 06:42:26 +00:00
|
|
|
}
|
|
|
|
|
2023-02-08 15:37:50 +00:00
|
|
|
export async function getAllPriority (
|
2023-04-05 12:23:55 +00:00
|
|
|
query: DocumentQuery<Doc> | undefined,
|
2023-02-08 15:37:50 +00:00
|
|
|
onUpdate: () => void,
|
|
|
|
queryId: Ref<Doc>
|
|
|
|
): Promise<any[] | undefined> {
|
2023-02-08 06:42:26 +00:00
|
|
|
return defaultPriorities
|
|
|
|
}
|
|
|
|
|
2023-03-17 06:17:53 +00:00
|
|
|
export async function getAllComponents (
|
2023-04-05 12:23:55 +00:00
|
|
|
query: DocumentQuery<Doc> | undefined,
|
2023-02-08 15:37:50 +00:00
|
|
|
onUpdate: () => void,
|
|
|
|
queryId: Ref<Doc>
|
|
|
|
): Promise<any[] | undefined> {
|
2023-04-05 12:23:55 +00:00
|
|
|
return await getAllSomething(tracker.class.Component, query, onUpdate, queryId)
|
2023-02-08 06:42:26 +00:00
|
|
|
}
|
|
|
|
|
2023-05-16 11:56:29 +00:00
|
|
|
export async function getAllMilestones (
|
2023-04-05 12:23:55 +00:00
|
|
|
query: DocumentQuery<Doc> | undefined,
|
2023-02-08 15:37:50 +00:00
|
|
|
onUpdate: () => void,
|
|
|
|
queryId: Ref<Doc>
|
|
|
|
): Promise<any[] | undefined> {
|
2023-05-16 11:56:29 +00:00
|
|
|
return await getAllSomething(tracker.class.Milestone, query, onUpdate, queryId)
|
2023-02-08 06:42:26 +00:00
|
|
|
}
|
|
|
|
|
2023-01-14 10:54:54 +00:00
|
|
|
export function subIssueListProvider (subIssues: Issue[], target: Ref<Issue>): void {
|
|
|
|
const listProvider = new ListSelectionProvider((offset: 1 | -1 | 0, of?: Doc, dir?: SelectDirection) => {
|
|
|
|
if (dir === 'vertical') {
|
|
|
|
let pos = subIssues.findIndex((p) => p._id === of?._id)
|
|
|
|
pos += offset
|
|
|
|
if (pos < 0) {
|
|
|
|
pos = 0
|
|
|
|
}
|
|
|
|
if (pos >= subIssues.length) {
|
|
|
|
pos = subIssues.length - 1
|
|
|
|
}
|
|
|
|
listProvider.updateFocus(subIssues[pos])
|
|
|
|
}
|
|
|
|
}, false)
|
|
|
|
listProvider.update(subIssues)
|
|
|
|
const selectedIssue = subIssues.find((p) => p._id === target)
|
|
|
|
if (selectedIssue != null) {
|
|
|
|
listProvider.updateFocus(selectedIssue)
|
|
|
|
}
|
|
|
|
}
|
2023-01-19 07:49:13 +00:00
|
|
|
|
2023-06-01 16:38:53 +00:00
|
|
|
export async function getPreviousAssignees (objectId: Ref<Doc>): Promise<Array<Ref<Employee>>> {
|
2023-01-19 07:49:13 +00:00
|
|
|
return await new Promise((resolve) => {
|
|
|
|
const query = createQuery(true)
|
|
|
|
query.query(
|
|
|
|
core.class.Tx,
|
|
|
|
{
|
2023-06-01 16:38:53 +00:00
|
|
|
'tx.objectId': objectId,
|
2023-01-19 07:49:13 +00:00
|
|
|
'tx.operations.assignee': { $exists: true }
|
|
|
|
},
|
|
|
|
(res) => {
|
|
|
|
const prevAssignee = res
|
|
|
|
.map((t) => ((t as TxCollectionCUD<Doc, Issue>).tx as TxUpdateDoc<Issue>).operations.assignee)
|
|
|
|
.filter((p) => !(p == null)) as Array<Ref<Employee>>
|
|
|
|
resolve(prevAssignee)
|
|
|
|
query.unsubscribe()
|
|
|
|
}
|
|
|
|
)
|
|
|
|
})
|
|
|
|
}
|
2023-02-13 08:18:31 +00:00
|
|
|
|
2023-03-29 13:04:28 +00:00
|
|
|
async function updateIssuesOnMove (
|
|
|
|
client: TxOperations,
|
|
|
|
applyOps: ApplyOperations,
|
|
|
|
doc: Doc,
|
2023-05-15 17:16:41 +00:00
|
|
|
space: Project,
|
|
|
|
extra: DocumentUpdate<any>,
|
|
|
|
updates: Map<Ref<Issue>, DocumentUpdate<Issue>>
|
2023-03-29 13:04:28 +00:00
|
|
|
): Promise<void> {
|
|
|
|
const hierarchy = client.getHierarchy()
|
|
|
|
const attributes = hierarchy.getAllAttributes(doc._class)
|
|
|
|
for (const attribute of attributes.values()) {
|
|
|
|
if (hierarchy.isDerived(attribute.type._class, core.class.Collection)) {
|
|
|
|
const collection = attribute.type as Collection<AttachedDoc>
|
|
|
|
const allAttached = await client.findAll(collection.of, { attachedTo: doc._id })
|
|
|
|
for (const attached of allAttached) {
|
|
|
|
if (hierarchy.isDerived(collection.of, tracker.class.Issue)) {
|
|
|
|
const lastOne = await client.findOne(tracker.class.Issue, {}, { sort: { rank: SortingOrder.Descending } })
|
|
|
|
const incResult = await client.updateDoc(
|
|
|
|
tracker.class.Project,
|
|
|
|
core.space.Space,
|
2023-05-15 17:16:41 +00:00
|
|
|
space._id,
|
2023-03-29 13:04:28 +00:00
|
|
|
{
|
|
|
|
$inc: { sequence: 1 }
|
|
|
|
},
|
|
|
|
true
|
|
|
|
)
|
2023-05-15 17:16:41 +00:00
|
|
|
await updateIssuesOnMove(
|
|
|
|
client,
|
|
|
|
applyOps,
|
|
|
|
attached,
|
|
|
|
space,
|
|
|
|
{
|
|
|
|
...updates.get(attached._id as Ref<Issue>),
|
|
|
|
rank: calcRank(lastOne, undefined),
|
|
|
|
number: (incResult as any).object.sequence
|
|
|
|
},
|
|
|
|
updates
|
|
|
|
)
|
|
|
|
} else {
|
|
|
|
await updateIssuesOnMove(client, applyOps, attached, space, {}, updates)
|
|
|
|
}
|
2023-03-29 13:04:28 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
await applyOps.update(doc, {
|
2023-05-15 17:16:41 +00:00
|
|
|
space: space._id,
|
2023-03-29 13:04:28 +00:00
|
|
|
...extra
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @public
|
|
|
|
*/
|
|
|
|
export async function moveIssueToSpace (
|
|
|
|
client: TxOperations,
|
2023-05-15 17:16:41 +00:00
|
|
|
docs: Issue[],
|
|
|
|
space: Project,
|
|
|
|
updates: Map<Ref<Issue>, DocumentUpdate<Issue>>
|
2023-03-29 13:04:28 +00:00
|
|
|
): Promise<void> {
|
|
|
|
const applyOps = client.apply(docs[0]._id)
|
|
|
|
for (const doc of docs) {
|
|
|
|
const lastOne = await client.findOne(tracker.class.Issue, {}, { sort: { rank: SortingOrder.Descending } })
|
|
|
|
const incResult = await client.updateDoc(
|
|
|
|
tracker.class.Project,
|
|
|
|
core.space.Space,
|
2023-05-15 17:16:41 +00:00
|
|
|
space._id,
|
2023-03-29 13:04:28 +00:00
|
|
|
{
|
|
|
|
$inc: { sequence: 1 }
|
|
|
|
},
|
|
|
|
true
|
|
|
|
)
|
2023-05-15 17:16:41 +00:00
|
|
|
await updateIssuesOnMove(
|
|
|
|
client,
|
|
|
|
applyOps,
|
|
|
|
doc,
|
|
|
|
space,
|
|
|
|
{
|
|
|
|
...updates.get(doc._id),
|
|
|
|
rank: calcRank(lastOne, undefined),
|
|
|
|
number: (incResult as any).object.sequence
|
|
|
|
},
|
|
|
|
updates
|
|
|
|
)
|
2023-03-29 13:04:28 +00:00
|
|
|
}
|
|
|
|
await applyOps.commit()
|
|
|
|
}
|
2023-05-15 17:16:41 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* @public
|
|
|
|
*
|
|
|
|
* Will collect all issues to be moved.
|
|
|
|
*/
|
|
|
|
export async function collectIssues (client: TxOperations, docs: Doc[]): Promise<Issue[]> {
|
|
|
|
const result: Issue[] = []
|
|
|
|
const hierarchy = client.getHierarchy()
|
|
|
|
for (const doc of docs) {
|
|
|
|
if (hierarchy.isDerived(doc._class, tracker.class.Issue)) {
|
|
|
|
result.push(doc as Issue)
|
|
|
|
}
|
|
|
|
|
|
|
|
const attributes = hierarchy.getAllAttributes(doc._class)
|
|
|
|
for (const attribute of attributes.values()) {
|
|
|
|
if (hierarchy.isDerived(attribute.type._class, core.class.Collection)) {
|
|
|
|
const collection = attribute.type as Collection<AttachedDoc>
|
|
|
|
const allAttached = await client.findAll(collection.of, { attachedTo: doc._id })
|
|
|
|
for (const attached of allAttached) {
|
|
|
|
if (hierarchy.isDerived(collection.of, tracker.class.Issue)) {
|
|
|
|
if (result.find((it) => it._id === attached._id) === undefined) {
|
|
|
|
result.push(attached as Issue)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
const subIssues = await collectIssues(client, [attached])
|
|
|
|
if (subIssues.length > 0) {
|
|
|
|
for (const s of subIssues) {
|
|
|
|
if (result.find((it) => it._id === s._id) === undefined) {
|
|
|
|
result.push(s)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return result
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @public
|
|
|
|
*/
|
|
|
|
export function findTargetStatus (
|
|
|
|
mgr: StatusManager,
|
|
|
|
status: Ref<Status>,
|
|
|
|
targetProject: Ref<Project>,
|
|
|
|
useCategory = false
|
|
|
|
): Ref<Status> | undefined {
|
|
|
|
const s = mgr.get(status)
|
|
|
|
let targetStatus = mgr
|
|
|
|
.filter(
|
|
|
|
(it) =>
|
|
|
|
it.space === targetProject &&
|
|
|
|
it.ofAttribute === s?.ofAttribute &&
|
|
|
|
(it.name ?? '').trim().toLowerCase() === (s?.name ?? '').trim().toLowerCase()
|
|
|
|
)
|
|
|
|
.shift()
|
|
|
|
if (targetStatus === undefined && useCategory) {
|
|
|
|
targetStatus = mgr
|
|
|
|
.filter((it) => it.space === targetProject && it.ofAttribute === s?.ofAttribute && s?.category === it.category)
|
|
|
|
.shift()
|
|
|
|
}
|
|
|
|
return targetStatus?._id
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @public
|
|
|
|
*/
|
|
|
|
export function issueToAttachedData (issue: Issue): AttachedData<Issue> {
|
|
|
|
const { _id, _class, space, ...data } = issue
|
|
|
|
return { ...data }
|
|
|
|
}
|
2023-05-29 18:17:14 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* @public
|
|
|
|
*/
|
|
|
|
export const IssuePriorityColor = {
|
|
|
|
[IssuePriority.NoPriority]: PaletteColorIndexes.Blueberry,
|
|
|
|
[IssuePriority.Urgent]: PaletteColorIndexes.Orange,
|
|
|
|
[IssuePriority.High]: PaletteColorIndexes.Sunshine,
|
|
|
|
[IssuePriority.Medium]: PaletteColorIndexes.Ocean,
|
|
|
|
[IssuePriority.Low]: PaletteColorIndexes.Cloud
|
|
|
|
}
|