TSK-960: move for issues (#2846)

* TSK-960: move for issues

Signed-off-by: Vyacheslav Tumanov <me@slavatumanov.me>

* TSK-960: remove validation and fix labels

Signed-off-by: Vyacheslav Tumanov <me@slavatumanov.me>

* TSK-960: apply changes in one transaction and get rid of text constant

Signed-off-by: Vyacheslav Tumanov <me@slavatumanov.me>

---------

Signed-off-by: Vyacheslav Tumanov <me@slavatumanov.me>
This commit is contained in:
Vyacheslav Tumanov 2023-03-29 18:04:28 +05:00 committed by GitHub
parent 85e8ddba72
commit eb8bd6634c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 216 additions and 5 deletions

View File

@ -1598,7 +1598,7 @@ export function createModel (builder: Builder): void {
createAction(
builder,
{
action: view.actionImpl.Move,
action: tracker.actionImpl.Move,
label: tracker.string.MoveToProject,
icon: view.icon.Move,
keyBinding: [],

View File

@ -58,6 +58,7 @@ export default mergeIds(trackerId, tracker, {
IssueCategory: '' as Ref<ObjectSearchCategory>
},
actionImpl: {
Move: '' as ViewAction,
CopyToClipboard: '' as ViewAction,
EditWorkflowStatuses: '' as ViewAction,
EditProject: '' as ViewAction,

View File

@ -157,6 +157,8 @@
"Assigned": "Assigned",
"Created": "Created",
"Subscribed": "Subscribed",
"MoveIssues": "Move issues",
"MoveIssuesDescription": "Select the project you want to move issues to.",
"Relations": "Relations",
"RemoveRelation": "Remove relation...",

View File

@ -157,6 +157,8 @@
"Assigned": "Назначенные",
"Created": "{value, plural, =1 {Создана} other {Созданные}}",
"Subscribed": "Отслеживаемые",
"MoveIssues": "Переместить задачи",
"MoveIssuesDescription": "Выберите проект, в который вы хотите переместить задачи.",
"Relations": "Зависимости",
"RemoveRelation": "Удалить зависимость...",

View File

@ -0,0 +1,121 @@
<!--
// Copyright © 2020, 2021 Anticrm Platform Contributors.
// Copyright © 2021, 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 { 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 view from '@hcengineering/view'
import ui from '@hcengineering/ui'
import tracker from '../../plugin'
import { Issue, Project } from '@hcengineering/tracker'
import { moveIssueToSpace } from '../../utils'
export let selected: Issue | Issue[]
$: docs = Array.isArray(selected) ? selected : [selected]
let currentSpace: Project | undefined
const client = getClient()
const dispatch = createEventDispatcher()
const hierarchy = client.getHierarchy()
let space: Ref<Project>
$: _class = hierarchy.getClass(tracker.class.Project).label
$: {
const doc = docs[0]
if (space === undefined) space = doc.space
}
const moveAll = async () => {
const spaceObject = await client.findOne(tracker.class.Project, { _id: space })
if (spaceObject === undefined) {
throw new Error('Move: state not found')
}
await moveIssueToSpace(client, docs, space, {
status: spaceObject.defaultIssueStatus
})
dispatch('close')
}
async function getSpace (): Promise<void> {
client.findOne(tracker.class.Project, { _id: space }).then((res) => (currentSpace = res))
}
</script>
<div class="container">
<div class="overflow-label fs-title">
<Label label={tracker.string.MoveIssues} />
</div>
<div class="content-accent-color mt-4 mb-4">
<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}
</div>
<div class="footer">
<Button
label={view.string.Move}
size={'small'}
disabled={space === currentSpace?._id}
kind={'primary'}
on:click={moveAll}
/>
<Button
size={'small'}
label={ui.string.Cancel}
on:click={() => {
dispatch('close')
}}
/>
</div>
</div>
<style lang="scss">
.container {
display: flex;
flex-direction: column;
padding: 2rem 1.75rem 1.75rem;
width: 25rem;
max-width: 40rem;
background: var(--popup-bg-color);
border-radius: 1.25rem;
user-select: none;
box-shadow: var(--popup-shadow);
.spaceSelect {
padding: 0.75rem;
background-color: var(--body-color);
border: 1px solid var(--popup-divider);
border-radius: 0.75rem;
}
.footer {
flex-shrink: 0;
display: grid;
grid-auto-flow: column;
direction: rtl;
justify-content: start;
align-items: center;
margin-top: 1rem;
column-gap: 0.5rem;
}
}
</style>

View File

@ -132,6 +132,7 @@ import IssueStatistics from './components/sprints/IssueStatistics.svelte'
import SprintRefPresenter from './components/sprints/SprintRefPresenter.svelte'
import CreateProject from './components/projects/CreateProject.svelte'
import ProjectPresenter from './components/projects/ProjectPresenter.svelte'
import MoveIssues from './components/issues/Move.svelte'
export { default as SubIssueList } from './components/issues/edit/SubIssueList.svelte'
@ -190,6 +191,10 @@ export async function queryIssue<D extends Issue> (
}))
}
async function move (issues: Issue | Issue[]): Promise<void> {
showPopup(MoveIssues, { selected: issues })
}
async function editWorkflowStatuses (project: Project | undefined): Promise<void> {
if (project !== undefined) {
showPopup(Statuses, { projectId: project._id, projectClass: project._class }, 'float')
@ -436,6 +441,7 @@ export default async (): Promise<Resources> => ({
GetAllSprints: getAllSprints
},
actionImpl: {
Move: move,
EditWorkflowStatuses: editWorkflowStatuses,
EditProject: editProject,
DeleteSprint: deleteSprint,

View File

@ -170,6 +170,8 @@ export default mergeIds(trackerId, tracker, {
NumberLabels: '' as IntlString,
MoveToProject: '' as IntlString,
Duplicate: '' as IntlString,
MoveIssues: '' as IntlString,
MoveIssuesDescription: '' as IntlString,
TypeIssuePriority: '' as IntlString,
IssueTitlePlaceholder: '' as IntlString,

View File

@ -15,9 +15,13 @@
import { Employee, getName } from '@hcengineering/contact'
import core, {
ApplyOperations,
AttachedDoc,
Class,
Collection,
Doc,
DocumentQuery,
DocumentUpdate,
IdMap,
Ref,
SortingOrder,
@ -58,6 +62,7 @@ import { CategoryQuery, ListSelectionProvider, SelectDirection } from '@hcengine
import { writable } from 'svelte/store'
import tracker from './plugin'
import { defaultComponentStatuses, defaultPriorities, defaultSprintStatuses, issuePriorities } from './types'
import { calcRank } from '@hcengineering/task'
export * from './types'
@ -685,6 +690,78 @@ export interface StatusStore {
byId: IdMap<WithLookup<IssueStatus>>
version: number
}
async function updateIssuesOnMove (
client: TxOperations,
applyOps: ApplyOperations,
doc: Doc,
space: Ref<Project>,
extra?: DocumentUpdate<any>
): Promise<void> {
const hierarchy = client.getHierarchy()
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) {
// 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,
{
$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 applyOps.update(doc, {
space,
...extra
})
}
/**
* @public
*/
export async function moveIssueToSpace (
client: TxOperations,
docs: Doc[],
space: Ref<Project>,
extra?: DocumentUpdate<any>
): Promise<void> {
const applyOps = client.apply(docs[0]._id)
for (const doc of docs) {
const lastOne = await client.findOne(tracker.class.Issue, {}, { sort: { rank: SortingOrder.Descending } })
const incResult = await client.updateDoc(
tracker.class.Project,
core.space.Space,
space,
{
$inc: { sequence: 1 }
},
true
)
await updateIssuesOnMove(client, applyOps, doc, space, {
...extra,
rank: calcRank(lastOne, undefined),
number: (incResult as any).object.sequence
})
}
await applyOps.commit()
}
// Issue status live query
export const statusStore = writable<StatusStore>({ statuses: [], byId: new Map(), version: 0 })

View File

@ -1,7 +1,7 @@
{
"string": {
"MoveClass": "Переместить {class}",
"SelectToMove": "Выберите {classLabel} который вы хотите переместить в {class}.",
"SelectToMove": "Выберите {classLabel}, в который вы хотите переместить {class}.",
"Delete": "Удалить",
"Move": "Переместить",
"Cancel": "Отменть",

View File

@ -28,9 +28,6 @@ export default mergeIds(viewId, view, {
ProxyPresenter: '' as AnyComponent
},
string: {
MoveClass: '' as IntlString,
SelectToMove: '' as IntlString,
Cancel: '' as IntlString,
LabelYes: '' as IntlString,
LabelNo: '' as IntlString,
ChooseAColor: '' as IntlString,

View File

@ -654,6 +654,9 @@ const view = plugin(viewId, {
NewFilteredView: '' as IntlString,
FilteredViewName: '' as IntlString,
Move: '' as IntlString,
MoveClass: '' as IntlString,
SelectToMove: '' as IntlString,
Cancel: '' as IntlString,
List: '' as IntlString,
Timeline: '' as IntlString
},