mirror of
https://github.com/hcengineering/platform.git
synced 2026-08-31 20:29:55 +02:00
UBER-81: Fix move project (#3182)
Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
@@ -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": {}
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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')
|
||||
|
||||
Reference in New Issue
Block a user