diff --git a/models/tracker/src/index.ts b/models/tracker/src/index.ts index 893db84c2e..011fdda752 100644 --- a/models/tracker/src/index.ts +++ b/models/tracker/src/index.ts @@ -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, diff --git a/models/tracker/src/plugin.ts b/models/tracker/src/plugin.ts index d4185cc315..5331b7250a 100644 --- a/models/tracker/src/plugin.ts +++ b/models/tracker/src/plugin.ts @@ -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>> + NewRelatedIssue: '' as Ref>>, + DeleteSprint: '' as Ref>> } }) diff --git a/packages/presentation/src/components/ObjectPopup.svelte b/packages/presentation/src/components/ObjectPopup.svelte index c560a1509e..889bc942d2 100644 --- a/packages/presentation/src/components/ObjectPopup.svelte +++ b/packages/presentation/src/components/ObjectPopup.svelte @@ -41,6 +41,7 @@ export let docQuery: DocumentQuery | 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) } diff --git a/packages/ui/src/components/EditBox.svelte b/packages/ui/src/components/EditBox.svelte index 0edac73390..f2f032ac6d 100644 --- a/packages/ui/src/components/EditBox.svelte +++ b/packages/ui/src/components/EditBox.svelte @@ -105,8 +105,7 @@
{ input.focus() }} diff --git a/plugins/tracker-assets/lang/en.json b/plugins/tracker-assets/lang/en.json index 05a098b119..e1d24b0253 100644 --- a/plugins/tracker-assets/lang/en.json +++ b/plugins/tracker-assets/lang/en.json @@ -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", diff --git a/plugins/tracker-assets/lang/ru.json b/plugins/tracker-assets/lang/ru.json index b3cd055194..70148bfc1a 100644 --- a/plugins/tracker-assets/lang/ru.json +++ b/plugins/tracker-assets/lang/ru.json @@ -209,6 +209,9 @@ "NewSprint": "Новый Спринт", "CreateSprint": "Создать", + "MoveAndDeleteSprint": "Переместить Задачи в {newSprint} и Удалить {deleteSprint}", + "MoveAndDeleteSprintConfirm": "Вы действительно хотите удалить спринт и перенести задачи в другой спринт?", + "Estimation": "Оценка", "ReportedTime": "Использовано", "TimeSpendReports": "Отчеты по времени", diff --git a/plugins/tracker-resources/src/components/sprints/EditSprint.svelte b/plugins/tracker-resources/src/components/sprints/EditSprint.svelte index 8550e9c6c3..415a76bd2d 100644 --- a/plugins/tracker-resources/src/components/sprints/EditSprint.svelte +++ b/plugins/tracker-resources/src/components/sprints/EditSprint.svelte @@ -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}
diff --git a/plugins/tracker-resources/src/components/sprints/MoveAndDeleteSprintPopup.svelte b/plugins/tracker-resources/src/components/sprints/MoveAndDeleteSprintPopup.svelte new file mode 100644 index 0000000000..9658f80244 --- /dev/null +++ b/plugins/tracker-resources/src/components/sprints/MoveAndDeleteSprintPopup.svelte @@ -0,0 +1,51 @@ + + + + moveAndDeleteSprint(selectedSprint)} + on:close +> + (selectedSprint = detail)} + /> + diff --git a/plugins/tracker-resources/src/components/sprints/SprintPopup.svelte b/plugins/tracker-resources/src/components/sprints/SprintPopup.svelte index d6c02c37d2..b29846f2f0 100644 --- a/plugins/tracker-resources/src/components/sprints/SprintPopup.svelte +++ b/plugins/tracker-resources/src/components/sprints/SprintPopup.svelte @@ -20,11 +20,16 @@ import { sprintStatusAssets } from '../../utils' import SprintTitlePresenter from './SprintTitlePresenter.svelte' export let _class: Ref> - export let selected: Ref | undefined + export let selected: Ref | undefined = undefined export let sprintQuery: DocumentQuery = {} 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 @@ { } } +async function moveAndDeleteSprint (client: TxOperations, oldSprint: Sprint, newSprint?: Sprint): Promise { + 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 { + 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 => ({ component: { NopeComponent, @@ -234,7 +279,8 @@ export default async (): Promise => ({ GetIssueTitle: issueTitleProvider }, actionImpl: { - EditWorkflowStatuses: editWorkflowStatuses + EditWorkflowStatuses: editWorkflowStatuses, + DeleteSprint: deleteSprint }, resolver: { Location: resolveLocation diff --git a/plugins/tracker-resources/src/plugin.ts b/plugins/tracker-resources/src/plugin.ts index f34c515eb6..3afe281d60 100644 --- a/plugins/tracker-resources/src/plugin.ts +++ b/plugins/tracker-resources/src/plugin.ts @@ -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, diff --git a/plugins/tracker-resources/src/utils.ts b/plugins/tracker-resources/src/utils.ts index ba9157cefa..ab23c92e9a 100644 --- a/plugins/tracker-resources/src/utils.ts +++ b/plugins/tracker-resources/src/utils.ts @@ -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 { + 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 */