[UBER-180] Fix status order (#3297)

Signed-off-by: Sergei Ogorelkov <sergei.ogorelkov@icloud.com>
This commit is contained in:
Sergei Ogorelkov 2023-05-31 10:53:23 +04:00 committed by GitHub
parent 0d1139f385
commit 1568c4b437
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 89 additions and 96 deletions

View File

@ -21,7 +21,6 @@ import core, {
DocumentUpdate, DocumentUpdate,
Ref, Ref,
SortingOrder, SortingOrder,
StatusCategory,
TxCreateDoc, TxCreateDoc,
TxOperations, TxOperations,
TxResult, TxResult,
@ -41,6 +40,7 @@ import {
Project, Project,
TimeReportDayType, TimeReportDayType,
calcRank, calcRank,
createStatuses,
genRanks genRanks
} from '@hcengineering/tracker' } from '@hcengineering/tracker'
import { DOMAIN_TRACKER } from '.' import { DOMAIN_TRACKER } from '.'
@ -54,14 +54,6 @@ enum DeprecatedIssueStatus {
Canceled Canceled
} }
interface CreateProjectIssueStatusesArgs {
tx: TxOperations
projectId: Ref<Project>
categories: StatusCategory[]
defaultStatusId?: Ref<IssueStatus>
defaultCategoryId?: Ref<StatusCategory>
}
const categoryByDeprecatedIssueStatus = { const categoryByDeprecatedIssueStatus = {
[DeprecatedIssueStatus.Backlog]: tracker.issueStatusCategory.Backlog, [DeprecatedIssueStatus.Backlog]: tracker.issueStatusCategory.Backlog,
[DeprecatedIssueStatus.Todo]: tracker.issueStatusCategory.Unstarted, [DeprecatedIssueStatus.Todo]: tracker.issueStatusCategory.Unstarted,
@ -70,30 +62,6 @@ const categoryByDeprecatedIssueStatus = {
[DeprecatedIssueStatus.Canceled]: tracker.issueStatusCategory.Canceled [DeprecatedIssueStatus.Canceled]: tracker.issueStatusCategory.Canceled
} as const } as const
async function createProjectIssueStatuses ({
tx,
projectId: attachedTo,
categories,
defaultStatusId,
defaultCategoryId = tracker.issueStatusCategory.Backlog
}: CreateProjectIssueStatusesArgs): Promise<void> {
const issueStatusRanks = [...genRanks(categories.length)]
for (const [i, statusCategory] of categories.entries()) {
const { _id: category, defaultStatusName } = statusCategory
const rank = issueStatusRanks[i]
if (defaultStatusName !== undefined) {
await tx.createDoc(
tracker.class.IssueStatus,
attachedTo,
{ ofAttribute: tracker.attribute.IssueStatus, name: defaultStatusName, category, rank },
category === defaultCategoryId ? defaultStatusId : undefined
)
}
}
}
async function createDefaultProject (tx: TxOperations): Promise<void> { async function createDefaultProject (tx: TxOperations): Promise<void> {
const current = await tx.findOne(tracker.class.Project, { const current = await tx.findOne(tracker.class.Project, {
_id: tracker.project.DefaultProject _id: tracker.project.DefaultProject
@ -106,7 +74,6 @@ async function createDefaultProject (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 defaultStatusId: Ref<IssueStatus> = generateId()
const categories = await tx.findAll(core.class.StatusCategory, {}, { sort: { order: SortingOrder.Ascending } })
await tx.createDoc<Project>( await tx.createDoc<Project>(
tracker.class.Project, tracker.class.Project,
@ -126,14 +93,20 @@ async function createDefaultProject (tx: TxOperations): Promise<void> {
}, },
tracker.project.DefaultProject tracker.project.DefaultProject
) )
await createProjectIssueStatuses({ tx, projectId: tracker.project.DefaultProject, categories, defaultStatusId }) await createStatuses(
tx,
tracker.project.DefaultProject,
tracker.class.IssueStatus,
tracker.attribute.IssueStatus,
defaultStatusId
)
} }
} }
async function fixProjectIssueStatusesOrder (tx: TxOperations, project: Project): Promise<TxResult> { async function fixProjectIssueStatusesOrder (tx: TxOperations, project: Project): Promise<TxResult> {
const statuses = await tx.findAll( const statuses = await tx.findAll(
tracker.class.IssueStatus, tracker.class.IssueStatus,
{ attachedTo: project._id }, { space: project._id },
{ lookup: { category: core.class.StatusCategory } } { lookup: { category: core.class.StatusCategory } }
) )
statuses.sort((a, b) => (a.$lookup?.category?.order ?? 0) - (b.$lookup?.category?.order ?? 0)) statuses.sort((a, b) => (a.$lookup?.category?.order ?? 0) - (b.$lookup?.category?.order ?? 0))
@ -167,13 +140,11 @@ async function upgradeProjectIssueStatuses (tx: TxOperations): Promise<void> {
const projects = await tx.findAll(tracker.class.Project, { issueStatuses: undefined }) const projects = await tx.findAll(tracker.class.Project, { issueStatuses: undefined })
if (projects.length > 0) { if (projects.length > 0) {
const categories = await tx.findAll(core.class.StatusCategory, {}, { sort: { order: SortingOrder.Ascending } })
for (const project of projects) { for (const project of projects) {
const defaultStatusId: Ref<IssueStatus> = generateId() const defaultStatusId: Ref<IssueStatus> = generateId()
await tx.update(project, { issueStatuses: 0, defaultIssueStatus: defaultStatusId }) await tx.update(project, { issueStatuses: 0, defaultIssueStatus: defaultStatusId })
await createProjectIssueStatuses({ tx, projectId: project._id, categories, defaultStatusId }) await createStatuses(tx, project._id, tracker.class.IssueStatus, tracker.attribute.IssueStatus, defaultStatusId)
} }
} }
} }

View File

@ -26,7 +26,7 @@ export interface StatusCategory extends Doc {
icon: Asset icon: Asset
label: IntlString label: IntlString
color: number color: number
defaultStatusName?: string defaultStatusName: string
order: number // category order order: number // category order
} }
/** /**

View File

@ -67,7 +67,7 @@
showPopup( showPopup(
SelectPopup, SelectPopup,
{ value: statusesInfo, placeholder: tracker.string.SetStatus, searchable: true }, { value: statusesInfo, placeholder: tracker.string.SetStatus },
eventToHTMLElement(event), eventToHTMLElement(event),
changeStatus changeStatus
) )

View File

@ -15,19 +15,11 @@
<script lang="ts"> <script lang="ts">
import { Employee } from '@hcengineering/contact' import { Employee } from '@hcengineering/contact'
import { AccountArrayEditor, AssigneeBox } from '@hcengineering/contact-resources' import { AccountArrayEditor, AssigneeBox } from '@hcengineering/contact-resources'
import core, { import core, { Account, DocumentUpdate, Ref, generateId, getCurrentAccount } from '@hcengineering/core'
Account,
ApplyOperations,
DocumentUpdate,
Ref,
SortingOrder,
generateId,
getCurrentAccount
} from '@hcengineering/core'
import { Asset } from '@hcengineering/platform' import { Asset } from '@hcengineering/platform'
import presentation, { Card, createQuery, getClient } from '@hcengineering/presentation' import presentation, { Card, createQuery, getClient } from '@hcengineering/presentation'
import { StyledTextBox } from '@hcengineering/text-editor' import { StyledTextBox } from '@hcengineering/text-editor'
import { IssueStatus, Project, TimeReportDayType, genRanks } from '@hcengineering/tracker' import { IssueStatus, Project, TimeReportDayType, createStatuses } from '@hcengineering/tracker'
import { import {
Button, Button,
EditBox, EditBox,
@ -159,7 +151,7 @@
isSaving = true isSaving = true
await ops.createDoc(tracker.class.Project, core.space.Space, projectData, projectId) await ops.createDoc(tracker.class.Project, core.space.Space, projectData, projectId)
await createProjectIssueStatuses(ops, projectId, defaultStatusId) await createStatuses(ops, projectId, tracker.class.IssueStatus, tracker.attribute.IssueStatus, defaultStatusId)
const succeeded = await ops.commit() const succeeded = await ops.commit()
isSaving = false isSaving = false
@ -170,39 +162,6 @@
} }
} }
async function createProjectIssueStatuses (
ops: ApplyOperations,
projectId: Ref<Project>,
defaultStatusId: Ref<IssueStatus>,
defaultCategoryId = tracker.issueStatusCategory.Backlog
): Promise<void> {
const categories = await ops.findAll(
core.class.StatusCategory,
{ ofAttribute: tracker.attribute.IssueStatus },
{ sort: { order: SortingOrder.Ascending } }
)
const issueStatusRanks = [...genRanks(categories.length)]
for (const [i, statusCategory] of categories.entries()) {
const { _id: category, defaultStatusName } = statusCategory
const rank = issueStatusRanks[i]
if (defaultStatusName !== undefined) {
await ops.createDoc(
tracker.class.IssueStatus,
projectId,
{
ofAttribute: tracker.attribute.IssueStatus,
name: defaultStatusName,
category,
rank
},
category === defaultCategoryId ? defaultStatusId : undefined
)
}
}
}
function chooseIcon (ev: MouseEvent) { function chooseIcon (ev: MouseEvent) {
showPopup(ProjectIconChooser, { icon, color }, 'top', (result) => { showPopup(ProjectIconChooser, { icon, color }, 'top', (result) => {
if (result !== undefined && result !== null) { if (result !== undefined && result !== null) {

View File

@ -68,9 +68,7 @@
async function addStatus () { async function addStatus () {
if (editingStatus?.name && editingStatus?.category) { if (editingStatus?.name && editingStatus?.category) {
const categoryStatuses = $statusStore.statuses.filter((s) => s.category === editingStatus!.category) const [prevStatus, nextStatus] = getSiblingsForNewStatus(editingStatus.category)
const prevStatus = categoryStatuses[categoryStatuses.length - 1]
const nextStatus = $statusStore.statuses[$statusStore.statuses.findIndex(({ _id }) => _id === prevStatus._id) + 1]
isSaving = true isSaving = true
await client.createDoc(tracker.class.IssueStatus, projectId, { await client.createDoc(tracker.class.IssueStatus, projectId, {
@ -152,8 +150,8 @@
await client.removeDoc(status._class, status.space, status._id) await client.removeDoc(status._class, status.space, status._id)
if (project.defaultIssueStatus === status._id) { if (project.defaultIssueStatus === status._id) {
const newDefaultStatus = $statusStore.statuses.find( const newDefaultStatus = projectStatuses.find(
(s) => s._id !== status._id && s.category === status.category && s.space === status.space (s) => s._id !== status._id && s.category === status.category
) )
if (newDefaultStatus?._id) { if (newDefaultStatus?._id) {
await updateProjectDefaultStatus(newDefaultStatus._id) await updateProjectDefaultStatus(newDefaultStatus._id)
@ -188,8 +186,8 @@
const fromIndex = getStatusIndex(draggingStatus) const fromIndex = getStatusIndex(draggingStatus)
const toIndex = getStatusIndex(toItem) const toIndex = getStatusIndex(toItem)
const [prev, next] = [ const [prev, next] = [
$statusStore.statuses[fromIndex < toIndex ? toIndex : toIndex - 1], projectStatuses[fromIndex < toIndex ? toIndex : toIndex - 1],
$statusStore.statuses[fromIndex < toIndex ? toIndex + 1 : toIndex] projectStatuses[fromIndex < toIndex ? toIndex + 1 : toIndex]
] ]
isSaving = true isSaving = true
@ -203,7 +201,34 @@
} }
function getStatusIndex (status: IssueStatus) { function getStatusIndex (status: IssueStatus) {
return $statusStore.statuses.findIndex(({ _id }) => _id === status._id) ?? -1 return projectStatuses.findIndex(({ _id }) => _id === status._id) ?? -1
}
function getSiblingsForNewStatus (
categoryId: StatusCategory['_id']
): readonly [] | readonly [IssueStatus | undefined, IssueStatus | undefined] {
const categoryStatuses = projectStatuses.filter((s) => s.category === categoryId)
if (categoryStatuses.length > 0) {
const prev = categoryStatuses[categoryStatuses.length - 1]
const next = projectStatuses[getStatusIndex(prev) + 1]
return [prev, next]
}
const category = statusCategories?.find(({ _id }) => _id === categoryId)
if (!category) {
return []
}
const { order } = category
const prev = projectStatuses.findLast(
(s) => s.$lookup?.category?.order !== undefined && s.$lookup.category.order < order
)
const next = projectStatuses.find(
(s) => s.$lookup?.category?.order !== undefined && s.$lookup.category.order > order
)
return [prev, next]
} }
function resetDrag () { function resetDrag () {
@ -213,6 +238,7 @@
$: projectQuery.query(projectClass, { _id: projectId }, (result) => ([project] = result), { limit: 1 }) $: projectQuery.query(projectClass, { _id: projectId }, (result) => ([project] = result), { limit: 1 })
$: updateStatusCategories() $: updateStatusCategories()
$: projectStatuses = $statusStore.statuses.filter((status) => status.space === projectId)
</script> </script>
<Panel isHeader={false} isAside={false} on:fullsize on:close={() => dispatch('close')}> <Panel isHeader={false} isAside={false} on:fullsize on:close={() => dispatch('close')}>
@ -232,14 +258,13 @@
</div> </div>
</svelte:fragment> </svelte:fragment>
{#if project === undefined || statusCategories === undefined || $statusStore.statuses.length === 0} {#if project === undefined || statusCategories === undefined || projectStatuses.length === 0}
<Loading /> <Loading />
{:else} {:else}
<Scroller> <Scroller>
<div class="popupPanel-body__main-content py-10 clear-mins flex-no-shrink"> <div class="popupPanel-body__main-content py-10 clear-mins flex-no-shrink">
{#each statusCategories as category} {#each statusCategories as category}
{@const statuses = {@const statuses = projectStatuses.filter((s) => s.category === category._id) ?? []}
$statusStore.statuses.filter((s) => s.space === projectId && s.category === category._id) ?? []}
{@const isSingle = statuses.length === 1} {@const isSingle = statuses.length === 1}
<div class="flex-between category-name"> <div class="flex-between category-name">
<Label label={category.label} /> <Label label={category.label} />

View File

@ -13,6 +13,7 @@
// limitations under the License. // limitations under the License.
// //
import core, { ApplyOperations, SortingOrder, Status, TxOperations, generateId } from '@hcengineering/core'
import { LexoRank, LexoDecimal, LexoNumeralSystem36 } from 'lexorank' import { LexoRank, LexoDecimal, LexoNumeralSystem36 } from 'lexorank'
import LexoRankBucket from 'lexorank/lib/lexoRank/lexoRankBucket' import LexoRankBucket from 'lexorank/lib/lexoRank/lexoRankBucket'
@ -44,3 +45,40 @@ export const calcRank = (prev?: { rank: string }, next?: { rank: string }): stri
} }
return a.between(b).toString() return a.between(b).toString()
} }
/**
* Generates statuses for provided space.
*
* @public
*/
export async function createStatuses (
client: TxOperations | ApplyOperations,
spaceId: Status['space'],
statusClass: Status['_class'],
categoryOfAttribute: Status['ofAttribute'],
defaultStatusId: Status['_id']
): Promise<void> {
const categories = await client.findAll(
core.class.StatusCategory,
{ ofAttribute: categoryOfAttribute },
{ sort: { order: SortingOrder.Ascending } }
)
const ranks = [...genRanks(categories.length)]
for (const [i, category] of categories.entries()) {
const statusId = i === 0 ? defaultStatusId : generateId<Status>()
const rank = ranks[i]
await client.createDoc(
statusClass,
spaceId,
{
ofAttribute: categoryOfAttribute,
name: category.defaultStatusName,
category: category._id,
rank
},
statusId
)
}
}