UBER-81: Fix move project (#3182)

Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
Andrey Sobolev
2023-05-16 00:16:41 +07:00
committed by GitHub
parent ceec07d528
commit 3d4e19270b
17 changed files with 580 additions and 78 deletions
+5 -1
View File
@@ -294,7 +294,11 @@
"ConfigDescription": "Extension to manage work items and do all jobs done.",
"ProjectColor": "Choose project icon color",
"ProjectIconCategory": "Icon",
"ProjectEmojiiCategory": "Emojii"
"ProjectEmojiiCategory": "Emojii",
"NoStatusFound": "No matching status found",
"CreateMissingStatus": "Create missing status",
"UnsetParent": "Parent issue will be unset"
},
"status": {}
}
+4 -1
View File
@@ -294,7 +294,10 @@
"ConfigDescription": "Расширение по управлению задачами, чтобы все было в срок.",
"ProjectColor": "Выберете цвет проекта",
"ProjectIconCategory": "Иконка",
"ProjectEmojiiCategory": "Эмодзи"
"ProjectEmojiiCategory": "Эмодзи",
"NoStatusFound": "Статус не найдет",
"CreateMissingStatus": "Создать отсутствущий статус",
"UnsetParent": "Родительская задача будет убрана"
},
"status": {}
}
@@ -14,11 +14,11 @@
-->
<script lang="ts">
import { Ref, SortingOrder } from '@hcengineering/core'
import { getEmbeddedLabel, IntlString, translate } from '@hcengineering/platform'
import { IntlString, getEmbeddedLabel, translate } from '@hcengineering/platform'
import { createQuery } from '@hcengineering/presentation'
import { Component } from '@hcengineering/tracker'
import { Component, Project } from '@hcengineering/tracker'
import type { ButtonKind, ButtonSize } from '@hcengineering/ui'
import { Button, ButtonShape, eventToHTMLElement, SelectPopup, showPopup, Label } from '@hcengineering/ui'
import { Button, ButtonShape, Label, SelectPopup, eventToHTMLElement, showPopup } from '@hcengineering/ui'
import tracker from '../plugin'
export let value: Ref<Component> | null | undefined
@@ -35,6 +35,7 @@
export let enlargedText: boolean = false
export let short: boolean = false
export let focusIndex: number | undefined = undefined
export let space: Ref<Project> | undefined = undefined
let selectedComponent: Component | undefined
let defaultComponentLabel = ''
@@ -42,9 +43,9 @@
const query = createQuery()
let rawComponents: Component[] = []
let loading = true
query.query(
$: query.query(
tracker.class.Component,
{},
space !== undefined ? { space } : {},
(res) => {
rawComponents = res
loading = false
@@ -13,16 +13,17 @@
// limitations under the License.
-->
<script lang="ts">
import { Ref } from '@hcengineering/core'
import { AttachedData, Ref } from '@hcengineering/core'
import { IntlString } from '@hcengineering/platform'
import { getClient } from '@hcengineering/presentation'
import { Component, Issue, IssueTemplate } from '@hcengineering/tracker'
import { Component, Issue, IssueTemplate, Project } from '@hcengineering/tracker'
import { ButtonKind, ButtonShape, ButtonSize, tooltip } from '@hcengineering/ui'
import { createEventDispatcher } from 'svelte'
import { activeComponent } from '../../issues'
import tracker from '../../plugin'
import ComponentSelector from '../ComponentSelector.svelte'
export let value: Issue | IssueTemplate
export let value: Issue | IssueTemplate | AttachedData<Issue>
export let isEditable: boolean = true
export let shouldShowLabel: boolean = true
export let popupPlaceholder: IntlString = tracker.string.MoveToComponent
@@ -36,15 +37,20 @@
export let groupBy: string | undefined = undefined
export let enlargedText = false
export let compression: boolean = false
export let space: Ref<Project> | undefined = undefined
const client = getClient()
const dispatch = createEventDispatcher()
const handleComponentIdChanged = async (newComponentId: Ref<Component> | null | undefined) => {
if (!isEditable || newComponentId === undefined || value.component === newComponentId) {
return
}
await client.update(value, { component: newComponentId })
dispatch('change', newComponentId)
if ('_class' in value) {
await client.update(value, { component: newComponentId })
}
}
</script>
@@ -61,6 +67,7 @@
{shape}
width={compression ? 'min-content' : width}
{justify}
{space}
{isEditable}
{shouldShowLabel}
{popupPlaceholder}
@@ -20,7 +20,7 @@
export let value: WithLookup<Component>
export let shouldShowAvatar = true
export let onClick: () => void | undefined
export let onClick: (() => void) | undefined = undefined
export let disabled = false
export let inline: boolean = false
@@ -14,16 +14,20 @@
// limitations under the License.
-->
<script lang="ts">
import { getClient } from '@hcengineering/presentation'
import { Button, Label } from '@hcengineering/ui'
import { Ref } from '@hcengineering/core'
import { SpaceSelect } from '@hcengineering/presentation'
import { createEventDispatcher } from 'svelte'
import { DocumentUpdate, Ref } from '@hcengineering/core'
import { SpaceSelect, createQuery, getClient, statusStore } from '@hcengineering/presentation'
import { Component, Issue, Project } from '@hcengineering/tracker'
import ui, { Button, Label, Spinner } from '@hcengineering/ui'
import view from '@hcengineering/view'
import ui from '@hcengineering/ui'
import { createEventDispatcher } from 'svelte'
import tracker from '../../plugin'
import { Issue, Project } from '@hcengineering/tracker'
import { moveIssueToSpace } from '../../utils'
import { collectIssues, findTargetStatus, moveIssueToSpace } from '../../utils'
import IssuePresenter from './IssuePresenter.svelte'
import TitlePresenter from './TitlePresenter.svelte'
import ComponentMove from './move/ComponentMove.svelte'
import ComponentMovePresenter from './move/ComponentMovePresenter.svelte'
import StatusMove from './move/StatusMove.svelte'
import StatusMovePresenter from './move/StatusMovePresenter.svelte'
export let selected: Issue | Issue[]
$: docs = Array.isArray(selected) ? selected : [selected]
@@ -37,23 +41,82 @@
$: _class = hierarchy.getClass(tracker.class.Project).label
$: {
const doc = docs[0]
if (space === undefined) space = doc.space
if (space === undefined) {
space = doc.space
}
}
let processing = false
const moveAll = async () => {
const spaceObject = await client.findOne(tracker.class.Project, { _id: space })
if (spaceObject === undefined) {
throw new Error('Move: state not found')
if (currentSpace === undefined) {
return
}
await moveIssueToSpace(client, docs, space, {
status: spaceObject.defaultIssueStatus
})
processing = true
await moveIssueToSpace(client, docs, currentSpace, issueToUpdate)
processing = false
dispatch('close')
}
async function getSpace (): Promise<void> {
client.findOne(tracker.class.Project, { _id: space }).then((res) => (currentSpace = res))
const targetSpaceQuery = createQuery()
let issueToUpdate: Map<Ref<Issue>, DocumentUpdate<Issue>> = new Map()
$: targetSpaceQuery.query(tracker.class.Project, { _id: space }, (res) => {
;[currentSpace] = res
})
let toMove: Issue[] = []
let loading = true
$: {
collectIssues(client, docs).then((res) => {
toMove = res
loading = false
})
}
$: if (currentSpace !== undefined) {
for (const c of toMove) {
const upd = issueToUpdate.get(c._id) ?? {}
// In case of target space change
if (upd.status !== undefined && $statusStore.get(upd.status)?.space !== currentSpace._id) {
upd.status = undefined
}
if (upd.status === undefined) {
upd.status = findTargetStatus($statusStore, c.status, space, true) ?? currentSpace.defaultIssueStatus
}
if (c.component !== undefined) {
const cur = components.find((it) => it._id === c.component)
if (cur !== undefined) {
if (
upd.component !== undefined &&
components.find((it) => it._id === upd.component)?.space !== currentSpace._id
) {
upd.component = undefined
}
if (upd.component === undefined) {
upd.component = components.find((it) => it.space === currentSpace?._id && it.label === cur.label)?._id
}
}
}
if (c.attachedTo !== tracker.ids.NoParent && toMove.find((it) => it._id === c.attachedTo) === undefined) {
upd.attachedTo = tracker.ids.NoParent
upd.attachedToClass = tracker.class.Issue
}
issueToUpdate.set(c._id, upd)
}
issueToUpdate = issueToUpdate
}
const componentQuery = createQuery()
let components: Component[] = []
$: componentQuery.query(tracker.class.Component, {}, (res) => {
components = res
})
</script>
<div class="container">
@@ -64,19 +127,57 @@
<Label label={tracker.string.MoveIssuesDescription} />
</div>
<div class="spaceSelect">
{#await getSpace() then}
{#if currentSpace && _class}
<SpaceSelect _class={currentSpace._class} label={_class} bind:value={space} />
{/if}
{/await}
{#if currentSpace && _class}
<SpaceSelect _class={currentSpace._class} label={_class} bind:value={space} />
{/if}
</div>
<div class="mt-2">
<Label label={tracker.string.Issues} />
</div>
<div class="issues-move flex-col">
{#if loading}
<Spinner />
{:else if toMove.length > 0 && currentSpace}
{#each toMove as issue}
{@const upd = issueToUpdate.get(issue._id) ?? {}}
<div class="issue-move p-3">
<div class="flex-row-center p-1">
<IssuePresenter value={issue} disabled kind={'list'} />
<div class="ml-2 max-w-30">
<TitlePresenter disabled value={issue} showParent={false} />
</div>
</div>
{#if issue.space !== currentSpace._id}
{#key upd.status}
<StatusMovePresenter {issue} {issueToUpdate} currentProject={currentSpace} />
{/key}
{#key upd.component}
<ComponentMovePresenter {issue} {issueToUpdate} currentProject={currentSpace} {components} />
{/key}
{#if upd.attachedTo === tracker.ids.NoParent && issue.attachedTo !== tracker.ids.NoParent}
<div class="p-1 unset-parent">
<Label label={tracker.string.SetParent} />
</div>
{/if}
{/if}
</div>
{/each}
{/if}
</div>
{#if currentSpace !== undefined}
<StatusMove issues={toMove} targetProject={currentSpace} />
<ComponentMove issues={toMove} targetProject={currentSpace} {components} />
{/if}
<div class="footer">
<Button
label={view.string.Move}
size={'small'}
disabled={space === currentSpace?._id}
disabled={docs[0]?.space === currentSpace?._id}
kind={'primary'}
on:click={moveAll}
loading={processing}
/>
<Button
size={'small'}
@@ -84,6 +185,7 @@
on:click={() => {
dispatch('close')
}}
disabled={processing}
/>
</div>
</div>
@@ -93,7 +195,7 @@
display: flex;
flex-direction: column;
padding: 2rem 1.75rem 1.75rem;
width: 25rem;
width: 55rem;
max-width: 40rem;
background: var(--popup-bg-color);
border-radius: 1.25rem;
@@ -117,5 +219,20 @@
margin-top: 1rem;
column-gap: 0.5rem;
}
.issues-move {
height: 30rem;
overflow: auto;
}
.issue-move {
border: 1px solid var(--popup-divider);
}
.status-option {
border: 1px solid var(--popup-divider);
}
}
.unset-parent {
background-color: var(--accent-bg-color);
}
</style>
@@ -14,13 +14,12 @@
-->
<script lang="ts">
import { AttachedData, Ref, StatusManager, WithLookup } from '@hcengineering/core'
import { getClient } from '@hcengineering/presentation'
import { getClient, statusStore } from '@hcengineering/presentation'
import { Issue, IssueDraft, IssueStatus, Project } from '@hcengineering/tracker'
import type { ButtonKind, ButtonSize } from '@hcengineering/ui'
import { Button, eventToHTMLElement, SelectPopup, showPopup, TooltipAlignment } from '@hcengineering/ui'
import type { ButtonKind, ButtonSize, IconSize } from '@hcengineering/ui'
import { Button, SelectPopup, TooltipAlignment, eventToHTMLElement, showPopup } from '@hcengineering/ui'
import { createEventDispatcher } from 'svelte'
import tracker from '../../plugin'
import { statusStore } from '@hcengineering/presentation'
import IssueStatusIcon from './IssueStatusIcon.svelte'
import StatusPresenter from './StatusPresenter.svelte'
@@ -36,6 +35,7 @@
export let kind: ButtonKind = 'link'
export let size: ButtonSize = 'large'
export let iconSize: IconSize = 'inline'
export let justify: 'left' | 'center' = 'left'
export let width: string | undefined = undefined
export let defaultIssueStatus: Ref<IssueStatus> | undefined = undefined
@@ -138,7 +138,7 @@
>
<span slot="content" class="flex-row-center pointer-events-none">
{#if selectedStatus}
<IssueStatusIcon value={selectedStatus} size="inline" />
<IssueStatusIcon value={selectedStatus} size={iconSize} />
{/if}
{#if selectedStatusLabel}
<span
@@ -24,6 +24,7 @@
export let showParent = true
export let kind: 'list' | undefined = undefined
export let onClick: (() => void) | undefined = undefined
export let disabled = false
</script>
{#if value}
@@ -34,7 +35,15 @@
style:max-width={showParent ? `${value.parents.length !== 0 ? 95 : 100}%` : '100%'}
title={value.title}
>
<DocNavLink object={value} {onClick} component={tracker.component.EditIssue} inline shrink={1} colorInherit>
<DocNavLink
object={value}
{disabled}
{onClick}
component={tracker.component.EditIssue}
inline
shrink={1}
colorInherit
>
{value.title}
</DocNavLink>
</span>
@@ -0,0 +1,73 @@
<!--
// Copyright © 2023 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 { EmployeeAccount } from '@hcengineering/contact'
import { getCurrentAccount } from '@hcengineering/core'
import { getClient } from '@hcengineering/presentation'
import { Component, Issue, Project } from '@hcengineering/tracker'
import { Button, Label } from '@hcengineering/ui'
import tracker from '../../../plugin'
import ComponentPresenter from '../../components/ComponentPresenter.svelte'
export let issues: Issue[]
export let targetProject: Project
export let components: Component[]
const client = getClient()
$: missingComponents = (
issues
.map((it) => it.component)
.filter((it) => it != null)
.map((it) => components.find((cit) => cit._id === it)) as Component[]
).filter((it, idx, arr) => {
const targetComponent = components.find((it2) => it2.space === targetProject._id && it2.label === it.label)
return targetComponent === undefined && arr.indexOf(it) === idx
})
async function createMissingComponent (cur: Component): Promise<void> {
await client.createDoc(cur._class, targetProject._id, {
label: cur.label,
members: [(getCurrentAccount() as EmployeeAccount).employee],
status: cur.status,
startDate: cur.startDate,
attachments: 0,
description: cur.description,
targetDate: cur.targetDate,
comments: 0,
lead: cur.lead
})
}
</script>
{#if missingComponents.length > 0}
<div class="mt-2">
<Label label={tracker.string.Component} />
</div>
<div class="flex-col">
{#each missingComponents as comp}
<div class="status-option p-1 flex-row-center flex-between">
<div class="flex-row-center">
<div class="mr-2">
<Label label={tracker.string.NoComponent} />
</div>
<ComponentPresenter value={comp} disabled />
</div>
<Button label={tracker.string.CreateComponent} on:click={() => createMissingComponent(comp)} />
</div>
{/each}
</div>
{/if}
@@ -0,0 +1,69 @@
<!--
// Copyright © 2023 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 { DocumentUpdate, Ref } from '@hcengineering/core'
import { Component, Issue, Project } from '@hcengineering/tracker'
import { Label } from '@hcengineering/ui'
import tracker from '../../../plugin'
import { issueToAttachedData } from '../../../utils'
import ComponentEditor from '../../components/ComponentEditor.svelte'
import ComponentPresenter from '../../components/ComponentPresenter.svelte'
export let issue: Issue
export let currentProject: Project
export let issueToUpdate: Map<Ref<Issue>, DocumentUpdate<Issue>> = new Map()
export let components: Component[]
$: currentComponent = components.find((it) => it._id === issue.component)
$: targetComponent = components.find((it) => it.space === currentProject._id && it.label === currentComponent?.label)
</script>
{#if currentComponent !== undefined}
<div class="flex-row-center p-1" class:no-component={targetComponent === undefined}>
<div class="p-1">
<ComponentPresenter value={currentComponent} />
</div>
<div class="p-1 flex-row-center">
<span class="p-1"> => </span>
<!--Find appropriate status in target Project -->
{#if targetComponent === undefined}
<div class="flex-row-center">
<div class="mr-2">
<Label label={tracker.string.NoComponent} />
</div>
<span class="p-1"> => </span>
</div>
{/if}
<ComponentEditor
shouldShowLabel={true}
space={currentProject._id}
value={{
...issueToAttachedData(issue),
component: issueToUpdate.get(issue._id)?.component || null,
space: currentProject._id
}}
on:change={(evt) => issueToUpdate.set(issue._id, { ...issueToUpdate.get(issue._id), status: evt.detail })}
/>
</div>
</div>
{/if}
<style lang="scss">
.no-component {
background-color: var(--accent-bg-color);
}
</style>
@@ -0,0 +1,70 @@
<!--
// Copyright © 2023 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, Status } from '@hcengineering/core'
import { getClient, statusStore } from '@hcengineering/presentation'
import { Issue, Project } from '@hcengineering/tracker'
import { Button, Label } from '@hcengineering/ui'
import tracker from '../../../plugin'
import { findTargetStatus } from '../../../utils'
import StatusRefPresenter from '../StatusRefPresenter.svelte'
export let issues: Issue[]
export let targetProject: Project
const client = getClient()
$: missingStatuses = issues
.map((it) => it.status)
.filter((it, idx, arr) => {
const targetStatus = findTargetStatus($statusStore, it, targetProject._id)
return targetStatus === undefined && arr.indexOf(it) === idx
})
async function createMissingStatus (st: Ref<Status>): Promise<void> {
const cur = $statusStore.get(st)
if (cur === undefined) {
return
}
await client.createDoc(cur._class, targetProject._id, {
name: cur.name,
ofAttribute: cur.ofAttribute,
category: cur.category,
color: cur.color,
description: cur.description,
rank: cur.rank
})
}
</script>
{#if missingStatuses.length > 0}
<div class="mt-2">
<Label label={tracker.string.Status} />
</div>
<div class="flex-col">
{#each missingStatuses as st}
<div class="status-option p-1 flex-row-center flex-between">
<div class="flex-row-center">
<div class="mr-2">
<Label label={tracker.string.NoStatusFound} />
</div>
<StatusRefPresenter value={st} kind={'list-header'} />
</div>
<Button label={tracker.string.CreateMissingStatus} on:click={() => createMissingStatus(st)} />
</div>
{/each}
</div>
{/if}
@@ -0,0 +1,63 @@
<!--
// Copyright © 2023 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 { DocumentUpdate, Ref } from '@hcengineering/core'
import { statusStore } from '@hcengineering/presentation'
import { Issue, Project } from '@hcengineering/tracker'
import { Label } from '@hcengineering/ui'
import tracker from '../../../plugin'
import { findTargetStatus, issueToAttachedData } from '../../../utils'
import StatusEditor from '../StatusEditor.svelte'
import StatusRefPresenter from '../StatusRefPresenter.svelte'
export let issue: Issue
export let currentProject: Project
export let issueToUpdate: Map<Ref<Issue>, DocumentUpdate<Issue>> = new Map()
$: targetStatus = findTargetStatus($statusStore, issue.status, currentProject._id)
</script>
<div class="flex-row-center p-1" class:no-status={targetStatus === undefined}>
<div class="p-1">
<StatusRefPresenter value={issue.status} size={'small'} />
</div>
<div class="p-1 flex-row-center">
<span class="p-1"> => </span>
<!--Find appropriate status in target Project -->
{#if targetStatus === undefined}
<div class="flex-row-center">
<Label label={tracker.string.NoStatusFound} />
<span class="p-1"> => </span>
</div>
{/if}
<StatusEditor
iconSize={'small'}
shouldShowLabel={true}
value={{
...issueToAttachedData(issue),
status: issueToUpdate.get(issue._id)?.status ?? currentProject.defaultIssueStatus,
space: currentProject._id
}}
on:change={(evt) => issueToUpdate.set(issue._id, { ...issueToUpdate.get(issue._id), status: evt.detail })}
/>
</div>
</div>
<style lang="scss">
.no-status {
background-color: var(--accent-bg-color);
}
</style>
@@ -17,7 +17,7 @@
import { translate } from '@hcengineering/platform'
import presentation, { getClient } from '@hcengineering/presentation'
import { Project, Sprint, SprintStatus } from '@hcengineering/tracker'
import ui, { deviceOptionsStore, Icon, Label, CheckBox, Loading, resizeObserver } from '@hcengineering/ui'
import ui, { CheckBox, Icon, Label, Loading, deviceOptionsStore, resizeObserver } from '@hcengineering/ui'
import view, { Filter } from '@hcengineering/view'
import { createEventDispatcher, onMount } from 'svelte'
import tracker from '../../plugin'
+5 -1
View File
@@ -314,7 +314,11 @@ export default mergeIds(trackerId, tracker, {
ProjectColor: '' as IntlString,
ProjectIconCategory: '' as IntlString,
ProjectEmojiiCategory: '' as IntlString
ProjectEmojiiCategory: '' as IntlString,
NoStatusFound: '' as IntlString,
CreateMissingStatus: '' as IntlString,
UnsetParent: '' as IntlString
},
component: {
NopeComponent: '' as AnyComponent,
+113 -20
View File
@@ -16,6 +16,7 @@
import { Employee } from '@hcengineering/contact'
import core, {
ApplyOperations,
AttachedData,
AttachedDoc,
Class,
Collection,
@@ -24,7 +25,9 @@ import core, {
DocumentUpdate,
Ref,
SortingOrder,
Status,
StatusCategory,
StatusManager,
StatusValue,
toIdMap,
TxCollectionCUD,
@@ -535,8 +538,9 @@ async function updateIssuesOnMove (
client: TxOperations,
applyOps: ApplyOperations,
doc: Doc,
space: Ref<Project>,
extra?: DocumentUpdate<any>
space: Project,
extra: DocumentUpdate<any>,
updates: Map<Ref<Issue>, DocumentUpdate<Issue>>
): Promise<void> {
const hierarchy = client.getHierarchy()
const attributes = hierarchy.getAllAttributes(doc._class)
@@ -545,29 +549,37 @@ async function updateIssuesOnMove (
const collection = attribute.type as Collection<AttachedDoc>
const allAttached = await client.findAll(collection.of, { attachedTo: doc._id })
for (const attached of allAttached) {
// Do not use extra for childs.
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,
space,
space._id,
{
$inc: { sequence: 1 }
},
true
)
await updateIssuesOnMove(client, applyOps, attached, space, {
...extra,
rank: calcRank(lastOne, undefined),
number: (incResult as any).object.sequence
})
} else await updateIssuesOnMove(client, applyOps, attached, space)
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)
}
}
}
}
await applyOps.update(doc, {
space,
space: space._id,
...extra
})
}
@@ -577,9 +589,9 @@ async function updateIssuesOnMove (
*/
export async function moveIssueToSpace (
client: TxOperations,
docs: Doc[],
space: Ref<Project>,
extra?: DocumentUpdate<any>
docs: Issue[],
space: Project,
updates: Map<Ref<Issue>, DocumentUpdate<Issue>>
): Promise<void> {
const applyOps = client.apply(docs[0]._id)
for (const doc of docs) {
@@ -587,17 +599,98 @@ export async function moveIssueToSpace (
const incResult = await client.updateDoc(
tracker.class.Project,
core.space.Space,
space,
space._id,
{
$inc: { sequence: 1 }
},
true
)
await updateIssuesOnMove(client, applyOps, doc, space, {
...extra,
rank: calcRank(lastOne, undefined),
number: (incResult as any).object.sequence
})
await updateIssuesOnMove(
client,
applyOps,
doc,
space,
{
...updates.get(doc._id),
rank: calcRank(lastOne, undefined),
number: (incResult as any).object.sequence
},
updates
)
}
await applyOps.commit()
}
/**
* @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 }
}
-1
View File
@@ -44,7 +44,6 @@
"@hcengineering/server-token": "^0.6.4",
"@hcengineering/server-core": "^0.6.1",
"lib0": "~0.2.52",
"lodash.debounce": "~4.0.8",
"y-protocols": "^1.0.5",
"ws": "^8.10.0",
"yjs": "^13.5.52",
+2 -12
View File
@@ -257,7 +257,6 @@ async function doIssueUpdate (
{ limit: 1 }
)
const updatedComponent = newParent !== undefined ? newParent.component : null
const updatedParents =
newParent !== undefined ? [{ parentId: newParent._id, parentTitle: newParent.title }, ...newParent.parents] : []
@@ -268,13 +267,12 @@ async function doIssueUpdate (
? {}
: { parents: [...issue.parents].slice(0, parentInfoIndex + 1).concat(updatedParents) }
return { ...parentsUpdate, component: updatedComponent }
return { ...parentsUpdate }
}
res.push(
control.txFactory.createTxUpdateDoc(updateTx.objectClass, updateTx.objectSpace, updateTx.objectId, {
parents: updatedParents,
component: updatedComponent
parents: updatedParents
}),
...(await updateSubIssues(updateTx, control, update))
)
@@ -284,14 +282,6 @@ async function doIssueUpdate (
updateIssueParentEstimations(issue, res, control, issue.parents, updatedParents)
}
if (Object.prototype.hasOwnProperty.call(updateTx.operations, 'component')) {
res.push(
...(await updateSubIssues(updateTx, control, {
component: updateTx.operations.component
}))
)
}
if (
Object.prototype.hasOwnProperty.call(updateTx.operations, 'estimation') ||
Object.prototype.hasOwnProperty.call(updateTx.operations, 'reportedTime')