TSK-399: Allow to delete sprints (#2386)

Signed-off-by: Anton Brechka <anton.brechka@xored.com>
This commit is contained in:
mrsadman99 2022-11-21 08:44:46 +03:00 committed by GitHub
parent 4878dae539
commit 3ae814cf21
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 185 additions and 15 deletions

View File

@ -1286,6 +1286,25 @@ export function createModel (builder: Builder): void {
tracker.action.Duplicate
)
createAction(
builder,
{
action: tracker.actionImpl.DeleteSprint,
label: view.string.Delete,
icon: view.icon.Delete,
keyBinding: ['Meta + Backspace', 'Ctrl + Backspace'],
category: tracker.category.Tracker,
input: 'any',
target: tracker.class.Sprint,
context: { mode: ['context', 'browser'], group: 'tools' }
},
tracker.action.DeleteSprint
)
builder.mixin(tracker.class.Sprint, core.class.Class, view.mixin.IgnoreActions, {
actions: [view.action.Delete]
})
classPresenter(
builder,
tracker.class.TypeReportedTime,

View File

@ -55,9 +55,11 @@ export default mergeIds(trackerId, tracker, {
},
actionImpl: {
CopyToClipboard: '' as ViewAction,
EditWorkflowStatuses: '' as ViewAction
EditWorkflowStatuses: '' as ViewAction,
DeleteSprint: '' as ViewAction
},
action: {
NewRelatedIssue: '' as Ref<Action<Doc, Record<string, any>>>
NewRelatedIssue: '' as Ref<Action<Doc, Record<string, any>>>,
DeleteSprint: '' as Ref<Action<Doc, Record<string, any>>>
}
})

View File

@ -41,6 +41,7 @@
export let docQuery: DocumentQuery<Doc> | undefined = undefined
export let multiSelect: boolean = false
export let closeAfterSelect: boolean = true
export let allowDeselect: boolean = false
export let titleDeselect: IntlString | undefined = undefined
export let placeholder: IntlString = presentation.string.Search
@ -110,7 +111,7 @@
} else {
selected = person._id
}
dispatch('close', selected !== undefined ? person : undefined)
dispatch(closeAfterSelect ? 'close' : 'update', selected !== undefined ? person : undefined)
} else {
checkSelected(person, objects)
}

View File

@ -105,8 +105,7 @@
</script>
<div
class="editbox-container"
class:w-full={focusable}
class="editbox-container w-full"
on:click={() => {
input.focus()
}}

View File

@ -209,6 +209,9 @@
"NewSprint": "New Sprint",
"CreateSprint": "Create",
"MoveAndDeleteSprint": "Move Issues to {newSprint} and Delete {deleteSprint}",
"MoveAndDeleteSprintConfirm": "Do you want to delete sprint and move issues to another sprint?",
"Estimation": "Estimation",
"ReportedTime": "Reported Time",
"TimeSpendReports": "Time spend reports",

View File

@ -209,6 +209,9 @@
"NewSprint": "Новый Спринт",
"CreateSprint": "Создать",
"MoveAndDeleteSprint": "Переместить Задачи в {newSprint} и Удалить {deleteSprint}",
"MoveAndDeleteSprintConfirm": "Вы действительно хотите удалить спринт и перенести задачи в другой спринт?",
"Estimation": "Оценка",
"ReportedTime": "Использовано",
"TimeSpendReports": "Отчеты по времени",

View File

@ -2,8 +2,8 @@
import { getClient } from '@hcengineering/presentation'
import { StyledTextBox } from '@hcengineering/text-editor'
import { Sprint } from '@hcengineering/tracker'
import { Button, DatePresenter, EditBox, Icon, Label, showPopup } from '@hcengineering/ui'
import { DocAttributeBar } from '@hcengineering/view-resources'
import { Button, DatePresenter, EditBox, Icon, IconMoreH, Label, showPopup } from '@hcengineering/ui'
import { ContextMenu, DocAttributeBar } from '@hcengineering/view-resources'
import { onDestroy } from 'svelte'
import { activeSprint } from '../../issues'
import tracker from '../../plugin'
@ -28,6 +28,12 @@
})
}
function showMenu (ev?: Event): void {
if (sprint) {
showPopup(ContextMenu, { object: sprint }, (ev as MouseEvent).target as HTMLElement)
}
}
$: $activeSprint = sprint?._id
onDestroy(() => {
@ -71,6 +77,7 @@
{#if sprint?.capacity}
<Label label={tracker.string.CapacityValue} params={{ value: sprint?.capacity }} />
{/if}
<Button icon={IconMoreH} kind={'transparent'} size={'medium'} on:click={showMenu} />
</div>
</div>
</svelte:fragment>

View File

@ -0,0 +1,51 @@
<!--
//
// Copyright © 2022 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
//
-->
<script lang="ts">
import tracker from '../../plugin'
import { Card } from '@hcengineering/presentation'
import { translate } from '@hcengineering/platform'
import SprintPopup from './SprintPopup.svelte'
import { Sprint } from '@hcengineering/tracker'
export let sprint: Sprint
export let moveAndDeleteSprint: (selectedSprint?: Sprint) => Promise<void>
let selectedSprint: Sprint | undefined
let noSprintLabel: string
$: translate(tracker.string.NoSprint, {}).then((label) => (noSprintLabel = label))
$: selectedSprintLabel = selectedSprint?.label ?? noSprintLabel
</script>
<Card
canSave
label={tracker.string.MoveAndDeleteSprint}
labelProps={{ newSprint: selectedSprintLabel, deleteSprint: sprint.label }}
okLabel={tracker.string.Delete}
okAction={() => moveAndDeleteSprint(selectedSprint)}
on:close
>
<SprintPopup
_class={tracker.class.Sprint}
ignoreSprints={[sprint]}
allowDeselect
closeAfterSelect={false}
shadows={false}
width="full"
on:update={({ detail }) => (selectedSprint = detail)}
/>
</Card>

View File

@ -20,11 +20,16 @@
import { sprintStatusAssets } from '../../utils'
import SprintTitlePresenter from './SprintTitlePresenter.svelte'
export let _class: Ref<Class<Sprint>>
export let selected: Ref<Sprint> | undefined
export let selected: Ref<Sprint> | undefined = undefined
export let sprintQuery: DocumentQuery<Sprint> = {}
export let create: ObjectCreate | undefined = undefined
export let allowDeselect = false
export let closeAfterSelect: boolean = true
export let shadows = true
export let width: 'medium' | 'large' | 'full' = 'medium'
export let ignoreSprints: Sprint[] | undefined = undefined
$: ignoreObjects = ignoreSprints?.filter((s) => '_id' in s).map((s) => s._id)
$: _create =
create !== undefined
? {
@ -38,11 +43,14 @@
<ObjectPopup
{_class}
{selected}
{ignoreObjects}
bind:docQuery={sprintQuery}
searchField={'label'}
multiSelect={false}
{allowDeselect}
shadows={true}
{closeAfterSelect}
{shadows}
{width}
create={_create}
on:update
on:close

View File

@ -13,10 +13,10 @@
// limitations under the License.
//
import { Class, Client, DocumentQuery, Ref, RelatedDocument } from '@hcengineering/core'
import { Resources } from '@hcengineering/platform'
import { ObjectSearchResult } from '@hcengineering/presentation'
import { Issue, Team } from '@hcengineering/tracker'
import { Class, Client, DocumentQuery, Ref, RelatedDocument, TxOperations } from '@hcengineering/core'
import { Resources, translate } from '@hcengineering/platform'
import { getClient, MessageBox, ObjectSearchResult } from '@hcengineering/presentation'
import { Issue, Sprint, Team } from '@hcengineering/tracker'
import { showPopup } from '@hcengineering/ui'
import CreateIssue from './components/CreateIssue.svelte'
import CreateIssueTemplate from './components/templates/CreateIssueTemplate.svelte'
@ -93,6 +93,9 @@ import IssueTemplates from './components/templates/IssueTemplates.svelte'
import EditIssueTemplate from './components/templates/EditIssueTemplate.svelte'
import TemplateEstimationEditor from './components/templates/EstimationEditor.svelte'
import MoveAndDeleteSprintPopup from './components/sprints/MoveAndDeleteSprintPopup.svelte'
import { moveIssuesToAnotherSprint } from './utils'
import { deleteObject } from '@hcengineering/view-resources/src/utils'
import CreateTeam from './components/teams/CreateTeam.svelte'
import TeamPresenter from './components/teams/TeamPresenter.svelte'
@ -160,6 +163,48 @@ async function editWorkflowStatuses (team: Team | undefined): Promise<void> {
}
}
async function moveAndDeleteSprint (client: TxOperations, oldSprint: Sprint, newSprint?: Sprint): Promise<void> {
const noSprintLabel = await translate(tracker.string.NoSprint, {})
showPopup(
MessageBox,
{
label: tracker.string.MoveAndDeleteSprint,
message: tracker.string.MoveAndDeleteSprintConfirm,
labelProps: { newSprint: newSprint?.label ?? noSprintLabel, deleteSprint: oldSprint.label }
},
undefined,
(result?: boolean) => {
if (result === true) {
void moveIssuesToAnotherSprint(client, oldSprint, newSprint).then((succes) => {
if (succes) {
void deleteObject(client, oldSprint)
}
})
}
}
)
}
async function deleteSprint (sprint: Sprint): Promise<void> {
const client = getClient()
// Check if available to move issues to another sprint
const firstSearchedSprint = await client.findOne(tracker.class.Sprint, { _id: { $nin: [sprint._id] } })
if (firstSearchedSprint !== undefined) {
showPopup(
MoveAndDeleteSprintPopup,
{
sprint,
moveAndDeleteSprint: async (selectedSprint?: Sprint) =>
await moveAndDeleteSprint(client, sprint, selectedSprint)
},
'top'
)
} else {
await moveAndDeleteSprint(client, sprint)
}
}
export default async (): Promise<Resources> => ({
component: {
NopeComponent,
@ -234,7 +279,8 @@ export default async (): Promise<Resources> => ({
GetIssueTitle: issueTitleProvider
},
actionImpl: {
EditWorkflowStatuses: editWorkflowStatuses
EditWorkflowStatuses: editWorkflowStatuses,
DeleteSprint: deleteSprint
},
resolver: {
Location: resolveLocation

View File

@ -228,6 +228,9 @@ export default mergeIds(trackerId, tracker, {
MoveToSprint: '' as IntlString,
NoSprint: '' as IntlString,
MoveAndDeleteSprint: '' as IntlString,
MoveAndDeleteSprintConfirm: '' as IntlString,
Estimation: '' as IntlString,
ReportedTime: '' as IntlString,
TimeSpendReport: '' as IntlString,

View File

@ -14,7 +14,7 @@
//
import { Employee, formatName } from '@hcengineering/contact'
import { DocumentQuery, Ref, SortingOrder, WithLookup } from '@hcengineering/core'
import { DocumentQuery, Ref, SortingOrder, TxOperations, WithLookup } from '@hcengineering/core'
import { TypeState } from '@hcengineering/kanban'
import { Asset, IntlString, translate } from '@hcengineering/platform'
import {
@ -614,6 +614,34 @@ export function getDayOfSprint (startDate: number, now: number): number {
return ds.filter((it) => !isWeekend(new Date(new Date(stTime).setDate(it)))).length
}
export async function moveIssuesToAnotherSprint (
client: TxOperations,
oldSprint: Sprint,
newSprint: Sprint | undefined
): Promise<boolean> {
try {
// Find all Issues by Sprint
const movedIssues = await client.findAll(tracker.class.Issue, { sprint: oldSprint._id })
// Update Issues by new Sprint
const awaitedUpdates = []
for (const issue of movedIssues) {
awaitedUpdates.push(client.update(issue, { sprint: newSprint?._id ?? undefined }))
}
await Promise.all(awaitedUpdates)
return true
} catch (error) {
console.error(
`Error happened while moving issues between sprints from ${oldSprint.label} to ${
newSprint?.label ?? 'No Sprint'
}: `,
error
)
return false
}
}
/**
* @public
*/