mirror of
https://github.com/hcengineering/platform.git
synced 2025-04-15 12:55:59 +00:00
Tracker: workflow statuses (#2171)
Signed-off-by: Sergei Ogorelkov <sergei.ogorelkov@xored.com>
This commit is contained in:
parent
efc3583376
commit
69a8d16db4
@ -17,6 +17,7 @@ HR:
|
||||
Tracker:
|
||||
- Manual issues ordering
|
||||
- Issue relations
|
||||
- Issue status management
|
||||
|
||||
Workbench
|
||||
- Use application aliases in URL
|
||||
|
@ -560,6 +560,26 @@ export function createModel (builder: Builder): void {
|
||||
}
|
||||
})
|
||||
|
||||
createAction(
|
||||
builder,
|
||||
{
|
||||
action: tracker.actionImpl.EditWorkflowStatuses,
|
||||
label: tracker.string.EditWorkflowStatuses,
|
||||
icon: view.icon.Statuses,
|
||||
input: 'focus',
|
||||
category: tracker.category.Tracker,
|
||||
target: tracker.class.Team,
|
||||
query: {
|
||||
archived: false
|
||||
},
|
||||
context: {
|
||||
mode: ['context', 'browser'],
|
||||
group: 'edit'
|
||||
}
|
||||
},
|
||||
tracker.action.EditWorkflowStatuses
|
||||
)
|
||||
|
||||
builder.createDoc(
|
||||
view.class.ActionCategory,
|
||||
core.space.Model,
|
||||
|
@ -51,6 +51,7 @@ export default mergeIds(trackerId, tracker, {
|
||||
IssueCategory: '' as Ref<ObjectSearchCategory>
|
||||
},
|
||||
actionImpl: {
|
||||
CopyToClipboard: '' as ViewAction
|
||||
CopyToClipboard: '' as ViewAction,
|
||||
EditWorkflowStatuses: '' as ViewAction
|
||||
}
|
||||
})
|
||||
|
@ -21,6 +21,7 @@
|
||||
export let label: IntlString
|
||||
export let message: IntlString
|
||||
export let params: Record<string, any> = {}
|
||||
export let canSubmit = true
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
</script>
|
||||
@ -36,7 +37,9 @@
|
||||
kind={'primary'}
|
||||
on:click={() => dispatch('close', true)}
|
||||
/>
|
||||
<Button label={presentation.string.Cancel} size={'small'} on:click={() => dispatch('close', false)} />
|
||||
{#if canSubmit}
|
||||
<Button label={presentation.string.Cancel} size={'small'} on:click={() => dispatch('close', false)} />
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -3,6 +3,8 @@
|
||||
"TrackerApplication": "Tracker",
|
||||
"Teams": "Your teams",
|
||||
"More": "More",
|
||||
"Default": "Default",
|
||||
"MakeDefault": "Make default",
|
||||
"Delete": "Delete",
|
||||
"Open": "Open",
|
||||
"Members": "Members",
|
||||
@ -60,6 +62,7 @@
|
||||
"CategoryCanceled": "Canceled",
|
||||
|
||||
"Title": "Title",
|
||||
"Name": "Name",
|
||||
"Description": "Description",
|
||||
"Status": "Status",
|
||||
"Number": "Number",
|
||||
@ -157,6 +160,14 @@
|
||||
"Related": "Related",
|
||||
|
||||
"EditIssue": "Edit {title}",
|
||||
"EditWorkflowStatuses": "Edit issue statuses",
|
||||
"ManageWorkflowStatuses": "Manage issue statuses within team",
|
||||
"AddWorkflowStatus": "Add issue status",
|
||||
"EditWorkflowStatus": "Edit issue status",
|
||||
"DeleteWorkflowStatus": "Delete issue status",
|
||||
"DeleteWorkflowStatusConfirm": "Do you want to delete the \"{status}\" status?",
|
||||
"DeleteWorkflowStatusError": "Can't delete the issue status",
|
||||
"DeleteWorkflowStatusErrorDescription": "The \"{status}\" status has {count, plural, =1 {1 issue} other {# issues}} assigned. Please archive or move {count, plural, =1 {it} other {them}} before deleting this status.",
|
||||
|
||||
"Save": "Save",
|
||||
"IncludeItemsThatMatch": "Include items that match",
|
||||
|
@ -3,6 +3,8 @@
|
||||
"TrackerApplication": "Трекер",
|
||||
"Teams": "Команды",
|
||||
"More": "Больше",
|
||||
"Default": "По умолчанию",
|
||||
"MakeDefault": "Установить по умолчанию",
|
||||
"Delete": "Удалить",
|
||||
"Open": "Открыть",
|
||||
"Members": "Участиники",
|
||||
@ -60,6 +62,7 @@
|
||||
"CategoryCanceled": "Отменённые",
|
||||
|
||||
"Title": "Заголовок",
|
||||
"Name": "Имя",
|
||||
"Description": "Описание",
|
||||
"Status": "Статус",
|
||||
"Number": "Номер",
|
||||
@ -157,6 +160,14 @@
|
||||
"Related": "Связан",
|
||||
|
||||
"EditIssue": "Редактирование {title}",
|
||||
"EditWorkflowStatuses": "Редактировать статусы задач",
|
||||
"ManageWorkflowStatuses": "Управлять статусами задач для команды",
|
||||
"AddWorkflowStatus": "Добавить статус задачи",
|
||||
"EditWorkflowStatus": "Редактировать статус задачи",
|
||||
"DeleteWorkflowStatus": "Удалить статус задачи",
|
||||
"DeleteWorkflowStatusConfirm": "Вы действительно хотите удалить \"{status}\" статус?",
|
||||
"DeleteWorkflowStatusError": "Невозможно удалить статус задачи",
|
||||
"DeleteWorkflowStatusErrorDescription": "Статус \"{status}\" {count, plural, =1 {имеет 1 задача} other {имеют # задачи}}. Пожалуйста, архивируйте или переместите {count, plural, =1 {ее} other {их}} перед удалением этого статуса.",
|
||||
|
||||
"Save": "Сохранить",
|
||||
"IncludeItemsThatMatch": "Включить элементы, которые соответствуют",
|
||||
|
@ -0,0 +1,112 @@
|
||||
<!--
|
||||
// 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 { createEventDispatcher } from 'svelte'
|
||||
import { Data } from '@anticrm/core'
|
||||
import { IssueStatus } from '@anticrm/tracker'
|
||||
import { Button, eventToHTMLElement, getPlatformColor, showPopup } from '@anticrm/ui'
|
||||
import presentation from '@anticrm/presentation'
|
||||
import { ColorsPopup } from '@anticrm/view-resources'
|
||||
import tracker from '../../plugin'
|
||||
import Circles from '../icons/Circles.svelte'
|
||||
import StatusInput from './StatusInput.svelte'
|
||||
|
||||
export let value: Partial<Data<IssueStatus>>
|
||||
export let isSingle = true
|
||||
export let isSaving = false
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
function pickColor (evt: MouseEvent) {
|
||||
showPopup(ColorsPopup, {}, eventToHTMLElement(evt), (newColor) => (value.color = newColor))
|
||||
}
|
||||
|
||||
$: canSave = !isSaving && (value.name ?? '').length > 0
|
||||
</script>
|
||||
|
||||
<div class="flex-between background-button-bg-color border-radius-1 p-2 root">
|
||||
<div class="flex flex-grow items-center clear-mins inputs">
|
||||
<div class="flex-no-shrink draggable-mark">
|
||||
{#if !isSingle}<Circles />{/if}
|
||||
</div>
|
||||
<div class="flex-no-shrink ml-2 color" on:click={pickColor}>
|
||||
<div class="dot" style="background-color: {getPlatformColor(value.color ?? 0)}" />
|
||||
</div>
|
||||
<div class="ml-2 w-full name">
|
||||
<StatusInput bind:value={value.name} placeholder={tracker.string.Name} focus fill />
|
||||
</div>
|
||||
<div class="ml-2 w-full">
|
||||
<StatusInput bind:value={value.description} placeholder={tracker.string.Description} fill />
|
||||
</div>
|
||||
</div>
|
||||
<div class="buttons-group small-gap flex-no-shrink ml-2 mr-1">
|
||||
<Button label={presentation.string.Cancel} kind="secondary" on:click={() => dispatch('cancel')} />
|
||||
<Button label={presentation.string.Save} kind="primary" disabled={!canSave} on:click={() => dispatch('save')} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.root {
|
||||
line-height: 1.125rem;
|
||||
|
||||
&:hover {
|
||||
.draggable-mark {
|
||||
opacity: 0.4;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.inputs {
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
|
||||
.name {
|
||||
max-width: 10rem;
|
||||
}
|
||||
}
|
||||
|
||||
.draggable-mark {
|
||||
position: relative;
|
||||
opacity: 0;
|
||||
width: 0.375rem;
|
||||
height: 1rem;
|
||||
margin-left: 0.25rem;
|
||||
transition: opacity 0.1s;
|
||||
|
||||
&::before {
|
||||
position: absolute;
|
||||
content: '';
|
||||
inset: -0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.color {
|
||||
position: relative;
|
||||
width: 1.75rem;
|
||||
height: 1.75rem;
|
||||
background-color: var(--accent-bg-color);
|
||||
border: 1px solid transparent;
|
||||
border-radius: 0.25rem;
|
||||
cursor: pointer;
|
||||
|
||||
.dot {
|
||||
position: absolute;
|
||||
content: '';
|
||||
background: red;
|
||||
border-radius: 50%;
|
||||
inset: 30%;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -0,0 +1,80 @@
|
||||
<!--
|
||||
// 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 { IntlString, translate } from '@anticrm/platform'
|
||||
import { onMount } from 'svelte'
|
||||
|
||||
export let value: string | undefined = undefined
|
||||
export let placeholder: IntlString | undefined
|
||||
export let focus = false
|
||||
export let fill = false
|
||||
|
||||
let input: HTMLInputElement
|
||||
let placeholderTranslation = ''
|
||||
|
||||
async function updatePlaceholderTranslation (ph: IntlString | undefined) {
|
||||
if (ph) {
|
||||
placeholderTranslation = await translate(ph, {})
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
if (focus) {
|
||||
input.focus()
|
||||
focus = false
|
||||
}
|
||||
})
|
||||
|
||||
$: updatePlaceholderTranslation(placeholder)
|
||||
</script>
|
||||
|
||||
<div class="root" class:fill>
|
||||
<input
|
||||
bind:this={input}
|
||||
type="text"
|
||||
bind:value
|
||||
placeholder={placeholderTranslation}
|
||||
on:input
|
||||
on:change
|
||||
on:keydown
|
||||
on:keypress
|
||||
on:blur
|
||||
/>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.root {
|
||||
font-weight: 500;
|
||||
|
||||
&.fill {
|
||||
width: 100%;
|
||||
|
||||
input {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
input {
|
||||
padding: 0.25rem 0.5rem;
|
||||
background-color: var(--accent-bg-color);
|
||||
border: 1px solid transparent;
|
||||
border-radius: 0.25rem;
|
||||
|
||||
&:focus {
|
||||
border: 1px solid var(--primary-edit-border-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
@ -0,0 +1,125 @@
|
||||
<!--
|
||||
// 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 { createEventDispatcher } from 'svelte'
|
||||
import { Asset } from '@anticrm/platform'
|
||||
import { IssueStatus } from '@anticrm/tracker'
|
||||
import { Icon, Label, IconEdit, IconClose, tooltip } from '@anticrm/ui'
|
||||
import tracker from '../../plugin'
|
||||
import Circles from '../icons/Circles.svelte'
|
||||
|
||||
export let value: IssueStatus
|
||||
export let icon: Asset
|
||||
export let isDefault = false
|
||||
export let isSingle = true
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
function edit () {
|
||||
dispatch('edit', value)
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex-between background-button-bg-color border-radius-1 p-2 root" on:dblclick|preventDefault={edit}>
|
||||
<div class="flex flex-grow items-center">
|
||||
<div class="flex-no-shrink draggable-mark" class:draggable={!isSingle}>
|
||||
<Circles />
|
||||
</div>
|
||||
<div class="flex-no-shrink ml-2">
|
||||
<Icon {icon} size="small" />
|
||||
</div>
|
||||
<span class="content-accent-color ml-2">{value.name}</span>
|
||||
{#if value.description}
|
||||
<span> · {value.description}</span>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="buttons-group flex-no-shrink mr-1">
|
||||
{#if isDefault}
|
||||
<Label label={tracker.string.Default} />
|
||||
{:else if value.category === tracker.issueStatusCategory.Backlog || value.category === tracker.issueStatusCategory.Unstarted}
|
||||
<div class="btn" on:click|preventDefault={() => dispatch('default-update', value._id)}>
|
||||
<Label label={tracker.string.MakeDefault} />
|
||||
</div>
|
||||
{/if}
|
||||
<div
|
||||
class="btn"
|
||||
use:tooltip={{ label: tracker.string.EditWorkflowStatus, direction: 'bottom' }}
|
||||
on:click|preventDefault={edit}
|
||||
>
|
||||
<Icon icon={IconEdit} size="small" />
|
||||
</div>
|
||||
{#if !isSingle}
|
||||
<div
|
||||
class="btn"
|
||||
use:tooltip={{ label: tracker.string.DeleteWorkflowStatus, direction: 'bottom' }}
|
||||
on:click|preventDefault={() => dispatch('delete', value)}
|
||||
>
|
||||
<Icon icon={IconClose} size="small" />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.root {
|
||||
&:hover {
|
||||
.btn {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.draggable-mark.draggable {
|
||||
cursor: grab;
|
||||
opacity: 0.4;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.btn {
|
||||
position: relative;
|
||||
opacity: 0;
|
||||
cursor: pointer;
|
||||
color: var(--content-color);
|
||||
transition: color 0.15s;
|
||||
transition: opacity 0.15s;
|
||||
|
||||
&:hover {
|
||||
color: var(--caption-color);
|
||||
}
|
||||
|
||||
&::before {
|
||||
position: absolute;
|
||||
content: '';
|
||||
inset: -0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.draggable-mark {
|
||||
opacity: 0;
|
||||
position: relative;
|
||||
width: 0.375rem;
|
||||
height: 1rem;
|
||||
margin-left: 0.25rem;
|
||||
|
||||
&.draggable {
|
||||
transition: opacity 0.15s;
|
||||
|
||||
&::before {
|
||||
position: absolute;
|
||||
content: '';
|
||||
inset: -0.5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
@ -0,0 +1,342 @@
|
||||
<!--
|
||||
// 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 { createEventDispatcher } from 'svelte'
|
||||
import { flip } from 'svelte/animate'
|
||||
import { AttachedData, Class, Ref, SortingOrder } from '@anticrm/core'
|
||||
import { Button, Icon, Label, Panel, Scroller, IconAdd, Loading, closeTooltip, showPopup } from '@anticrm/ui'
|
||||
import { createQuery, getClient, MessageBox } from '@anticrm/presentation'
|
||||
import { calcRank, IssueStatus, IssueStatusCategory, Team } from '@anticrm/tracker'
|
||||
import tracker from '../../plugin'
|
||||
import StatusEditor from './StatusEditor.svelte'
|
||||
import StatusPresenter from './StatusPresenter.svelte'
|
||||
import ExpandCollapse from '@anticrm/ui/src/components/ExpandCollapse.svelte'
|
||||
|
||||
export let teamId: Ref<Team>
|
||||
export let teamClass: Ref<Class<Team>>
|
||||
|
||||
const client = getClient()
|
||||
const dispatch = createEventDispatcher()
|
||||
const teamQuery = createQuery()
|
||||
const statusesQuery = createQuery()
|
||||
|
||||
let team: Team | undefined
|
||||
let statusCategories: IssueStatusCategory[] | undefined
|
||||
let workflowStatuses: IssueStatus[] | undefined
|
||||
|
||||
let editingStatus: IssueStatus | Partial<AttachedData<IssueStatus>> | null = null
|
||||
let draggingStatus: IssueStatus | null = null
|
||||
let hoveringStatus: IssueStatus | null = null
|
||||
|
||||
let isSaving = false
|
||||
|
||||
async function updateStatusCategories () {
|
||||
statusCategories = await client.findAll(
|
||||
tracker.class.IssueStatusCategory,
|
||||
{},
|
||||
{ sort: { order: SortingOrder.Ascending } }
|
||||
)
|
||||
}
|
||||
|
||||
async function updateTeamDefaultStatus (statusId: Ref<IssueStatus>) {
|
||||
if (team) {
|
||||
await client.update(team, { defaultIssueStatus: statusId })
|
||||
}
|
||||
}
|
||||
|
||||
async function addStatus () {
|
||||
if (editingStatus?.name && editingStatus?.category && workflowStatuses) {
|
||||
const categoryStatuses = workflowStatuses.filter((s) => s.category === editingStatus!.category)
|
||||
const prevStatus = categoryStatuses[categoryStatuses.length - 1]
|
||||
const nextStatus = workflowStatuses[workflowStatuses.findIndex(({ _id }) => _id === prevStatus._id) + 1]
|
||||
|
||||
isSaving = true
|
||||
await client.addCollection(tracker.class.IssueStatus, teamId, teamId, tracker.class.Team, 'issueStatuses', {
|
||||
name: editingStatus.name,
|
||||
description: editingStatus.description,
|
||||
color: editingStatus.color,
|
||||
category: editingStatus.category,
|
||||
rank: calcRank(prevStatus, nextStatus)
|
||||
})
|
||||
isSaving = false
|
||||
}
|
||||
|
||||
cancelEditing()
|
||||
}
|
||||
|
||||
async function editStatus () {
|
||||
if (
|
||||
workflowStatuses &&
|
||||
statusCategories &&
|
||||
editingStatus?.name &&
|
||||
editingStatus?.category &&
|
||||
'_id' in editingStatus
|
||||
) {
|
||||
const statusId = '_id' in editingStatus ? editingStatus._id : undefined
|
||||
const status = statusId && workflowStatuses.find(({ _id }) => _id === statusId)
|
||||
|
||||
if (!status) {
|
||||
return
|
||||
}
|
||||
|
||||
const updates: Partial<AttachedData<IssueStatus>> = {}
|
||||
if (status.name !== editingStatus.name) {
|
||||
updates.name = editingStatus.name
|
||||
}
|
||||
if (status.description !== editingStatus.description) {
|
||||
updates.description = editingStatus.description
|
||||
}
|
||||
if (status.color !== editingStatus.color) {
|
||||
if (status.color === undefined) {
|
||||
const category = statusCategories.find((c) => c._id === editingStatus?.category)
|
||||
|
||||
if (category && editingStatus.color !== category.color) {
|
||||
updates.color = editingStatus.color
|
||||
}
|
||||
} else {
|
||||
updates.color = editingStatus.color
|
||||
}
|
||||
}
|
||||
|
||||
if (Object.keys(updates).length > 0) {
|
||||
isSaving = true
|
||||
await client.update(status, updates)
|
||||
isSaving = false
|
||||
}
|
||||
|
||||
cancelEditing()
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteStatus (event: CustomEvent<IssueStatus>) {
|
||||
closeTooltip()
|
||||
|
||||
const { detail: status } = event
|
||||
const issuesWithDeletingStatus = await client.findAll(
|
||||
tracker.class.Issue,
|
||||
{ status: status._id },
|
||||
{ projection: { _id: 1 } }
|
||||
)
|
||||
|
||||
if (issuesWithDeletingStatus.length > 0) {
|
||||
showPopup(MessageBox, {
|
||||
label: tracker.string.DeleteWorkflowStatusError,
|
||||
message: tracker.string.DeleteWorkflowStatusErrorDescription,
|
||||
params: { status: status.name, count: issuesWithDeletingStatus.length },
|
||||
canSubmit: false
|
||||
})
|
||||
} else {
|
||||
showPopup(
|
||||
MessageBox,
|
||||
{
|
||||
label: tracker.string.DeleteWorkflowStatus,
|
||||
message: tracker.string.DeleteWorkflowStatusConfirm,
|
||||
params: { status: status.name }
|
||||
},
|
||||
undefined,
|
||||
async (result) => {
|
||||
if (result && team && workflowStatuses) {
|
||||
isSaving = true
|
||||
await client.removeDoc(status._class, status.space, status._id)
|
||||
|
||||
if (team.defaultIssueStatus === status._id) {
|
||||
const newDefaultStatus = workflowStatuses.find(
|
||||
(s) => s._id !== status._id && s.category === status.category
|
||||
)
|
||||
if (newDefaultStatus?._id) {
|
||||
await updateTeamDefaultStatus(newDefaultStatus._id)
|
||||
}
|
||||
}
|
||||
isSaving = false
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
function cancelEditing () {
|
||||
editingStatus = null
|
||||
}
|
||||
|
||||
function handleDragStart (ev: DragEvent, status: IssueStatus) {
|
||||
if (ev.dataTransfer && ev.target) {
|
||||
ev.dataTransfer.effectAllowed = 'move'
|
||||
ev.dataTransfer.dropEffect = 'move'
|
||||
draggingStatus = status
|
||||
}
|
||||
}
|
||||
|
||||
function handleDragOver (ev: DragEvent, status: IssueStatus) {
|
||||
if (draggingStatus?.category === status.category) {
|
||||
hoveringStatus = status
|
||||
ev.preventDefault()
|
||||
} else {
|
||||
hoveringStatus = null
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDrop (toItem: IssueStatus) {
|
||||
if (workflowStatuses && draggingStatus?._id !== toItem._id && draggingStatus?.category === toItem.category) {
|
||||
const fromIndex = getStatusIndex(draggingStatus)
|
||||
const toIndex = getStatusIndex(toItem)
|
||||
const [prev, next] = [
|
||||
workflowStatuses[fromIndex < toIndex ? toIndex : toIndex - 1],
|
||||
workflowStatuses[fromIndex < toIndex ? toIndex + 1 : toIndex]
|
||||
]
|
||||
|
||||
isSaving = true
|
||||
await client.update(draggingStatus, { rank: calcRank(prev, next) })
|
||||
isSaving = false
|
||||
}
|
||||
|
||||
resetDrag()
|
||||
}
|
||||
|
||||
function getStatusIndex (status: IssueStatus) {
|
||||
return workflowStatuses?.findIndex(({ _id }) => _id === status._id) ?? -1
|
||||
}
|
||||
|
||||
function resetDrag () {
|
||||
draggingStatus = null
|
||||
hoveringStatus = null
|
||||
}
|
||||
|
||||
$: teamQuery.query(teamClass, { _id: teamId }, (result) => ([team] = result), { limit: 1 })
|
||||
$: statusesQuery.query(tracker.class.IssueStatus, { attachedTo: teamId }, (res) => (workflowStatuses = res), {
|
||||
sort: { rank: SortingOrder.Ascending }
|
||||
})
|
||||
$: updateStatusCategories()
|
||||
</script>
|
||||
|
||||
<Panel isHeader={false} isAside={false} isFullSize on:fullsize on:close={() => dispatch('close')}>
|
||||
<svelte:fragment slot="title">
|
||||
<div class="antiTitle icon-wrapper">
|
||||
<div class="wrapped-icon">
|
||||
<Icon icon={tracker.icon.Issue} size="small" />
|
||||
</div>
|
||||
<div class="title-wrapper">
|
||||
<span class="wrapped-title">
|
||||
<Label label={tracker.string.ManageWorkflowStatuses} />
|
||||
</span>
|
||||
{#if team}
|
||||
<span class="wrapped-subtitle">{team.name}</span>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
|
||||
{#if team === undefined || statusCategories === undefined || workflowStatuses === undefined}
|
||||
<Loading />
|
||||
{:else}
|
||||
<Scroller>
|
||||
<div class="popupPanel-body__main-content py-10 clear-mins">
|
||||
{#each statusCategories as category}
|
||||
{@const statuses = workflowStatuses?.filter((s) => s.category === category._id) ?? []}
|
||||
{@const isSingle = statuses.length === 1}
|
||||
<div class="flex-between category-name">
|
||||
<Label label={category.label} />
|
||||
<Button
|
||||
showTooltip={{ label: tracker.string.AddWorkflowStatus }}
|
||||
width="min-content"
|
||||
icon={IconAdd}
|
||||
size="small"
|
||||
kind="transparent"
|
||||
on:click={() => {
|
||||
closeTooltip()
|
||||
editingStatus = { category: category._id, color: category.color }
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div class="flex-col">
|
||||
{#each statuses as status, _ (status._id)}
|
||||
<div
|
||||
class="row"
|
||||
class:is-dragged-over-up={draggingStatus &&
|
||||
status._id === hoveringStatus?._id &&
|
||||
status.rank < draggingStatus.rank}
|
||||
class:is-dragged-over-down={draggingStatus &&
|
||||
status._id === hoveringStatus?._id &&
|
||||
status.rank > draggingStatus.rank}
|
||||
draggable={!isSingle}
|
||||
animate:flip={{ duration: 200 }}
|
||||
on:dragstart={(ev) => handleDragStart(ev, status)}
|
||||
on:dragover={(ev) => handleDragOver(ev, status)}
|
||||
on:drop={() => handleDrop(status)}
|
||||
on:dragend={resetDrag}
|
||||
>
|
||||
{#if editingStatus && '_id' in editingStatus && editingStatus._id === status._id}
|
||||
<StatusEditor
|
||||
value={editingStatus}
|
||||
on:cancel={cancelEditing}
|
||||
on:save={editStatus}
|
||||
{isSingle}
|
||||
{isSaving}
|
||||
/>
|
||||
{:else}
|
||||
<StatusPresenter
|
||||
value={status}
|
||||
icon={category.icon}
|
||||
isDefault={status._id === team.defaultIssueStatus}
|
||||
{isSingle}
|
||||
on:default-update={({ detail }) => updateTeamDefaultStatus(detail)}
|
||||
on:edit={({ detail }) => {
|
||||
closeTooltip()
|
||||
editingStatus = { ...detail }
|
||||
}}
|
||||
on:delete={deleteStatus}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
<ExpandCollapse duration={200} isExpanded>
|
||||
{#if editingStatus && !('_id' in editingStatus) && editingStatus.category === category._id}
|
||||
<StatusEditor value={editingStatus} on:cancel={cancelEditing} on:save={addStatus} {isSaving} isSingle />
|
||||
{/if}
|
||||
</ExpandCollapse>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</Scroller>
|
||||
{/if}
|
||||
</Panel>
|
||||
|
||||
<style lang="scss">
|
||||
.row {
|
||||
position: relative;
|
||||
margin-bottom: 0.25rem;
|
||||
|
||||
&.is-dragged-over-up::before {
|
||||
position: absolute;
|
||||
content: '';
|
||||
inset: 0;
|
||||
border-top: 1px solid var(--theme-bg-check);
|
||||
}
|
||||
|
||||
&.is-dragged-over-down::before {
|
||||
position: absolute;
|
||||
content: '';
|
||||
inset: 0;
|
||||
border-bottom: 1px solid var(--theme-bg-check);
|
||||
}
|
||||
}
|
||||
|
||||
.category-name {
|
||||
margin: 1rem 0 0.5rem 0;
|
||||
|
||||
&:first-child {
|
||||
margin: 0 0 0.5rem 0;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -16,6 +16,7 @@
|
||||
import { Class, Client, Ref } from '@anticrm/core'
|
||||
import { Resources } from '@anticrm/platform'
|
||||
import { ObjectSearchResult } from '@anticrm/presentation'
|
||||
import { showPopup } from '@anticrm/ui'
|
||||
import { Issue, Team } from '@anticrm/tracker'
|
||||
import Inbox from './components/inbox/Inbox.svelte'
|
||||
import Issues from './components/issues/Issues.svelte'
|
||||
@ -54,6 +55,7 @@ import ProjectTitlePresenter from './components/projects/ProjectTitlePresenter.s
|
||||
import TargetDatePresenter from './components/projects/TargetDatePresenter.svelte'
|
||||
import SetDueDateActionPopup from './components/SetDueDateActionPopup.svelte'
|
||||
import SetParentIssueActionPopup from './components/SetParentIssueActionPopup.svelte'
|
||||
import Statuses from './components/workflow/Statuses.svelte'
|
||||
import Views from './components/views/Views.svelte'
|
||||
import KanbanView from './components/issues/KanbanView.svelte'
|
||||
import tracker from './plugin'
|
||||
@ -110,6 +112,12 @@ export async function queryIssue<D extends Issue> (
|
||||
}))
|
||||
}
|
||||
|
||||
async function editWorkflowStatuses (team: Team | undefined): Promise<void> {
|
||||
if (team !== undefined) {
|
||||
showPopup(Statuses, { teamId: team._id, teamClass: team._class }, 'float')
|
||||
}
|
||||
}
|
||||
|
||||
export default async (): Promise<Resources> => ({
|
||||
component: {
|
||||
NopeComponent,
|
||||
@ -160,7 +168,8 @@ export default async (): Promise<Resources> => ({
|
||||
IssueTitleProvider: getIssueTitle
|
||||
},
|
||||
actionImpl: {
|
||||
CopyToClipboard: copyToClipboard
|
||||
CopyToClipboard: copyToClipboard,
|
||||
EditWorkflowStatuses: editWorkflowStatuses
|
||||
},
|
||||
resolver: {
|
||||
Location: resolveLocation
|
||||
|
@ -23,6 +23,8 @@ export default mergeIds(trackerId, tracker, {
|
||||
More: '' as IntlString,
|
||||
Delete: '' as IntlString,
|
||||
Open: '' as IntlString,
|
||||
Default: '' as IntlString,
|
||||
MakeDefault: '' as IntlString,
|
||||
Members: '' as IntlString,
|
||||
Inbox: '' as IntlString,
|
||||
MyIssues: '' as IntlString,
|
||||
@ -78,6 +80,15 @@ export default mergeIds(trackerId, tracker, {
|
||||
Status: '' as IntlString,
|
||||
DefaultIssueStatus: '' as IntlString,
|
||||
IssueStatuses: '' as IntlString,
|
||||
EditWorkflowStatuses: '' as IntlString,
|
||||
ManageWorkflowStatuses: '' as IntlString,
|
||||
AddWorkflowStatus: '' as IntlString,
|
||||
EditWorkflowStatus: '' as IntlString,
|
||||
DeleteWorkflowStatus: '' as IntlString,
|
||||
DeleteWorkflowStatusConfirm: '' as IntlString,
|
||||
DeleteWorkflowStatusError: '' as IntlString,
|
||||
DeleteWorkflowStatusErrorDescription: '' as IntlString,
|
||||
Name: '' as IntlString,
|
||||
StatusCategory: '' as IntlString,
|
||||
CategoryBacklog: '' as IntlString,
|
||||
CategoryUnstarted: '' as IntlString,
|
||||
|
@ -284,9 +284,10 @@ export default plugin(trackerId, {
|
||||
SetProject: '' as Ref<Action>,
|
||||
CopyIssueId: '' as Ref<Action>,
|
||||
CopyIssueTitle: '' as Ref<Action>,
|
||||
CopyIssueLink: '' as Ref<Action>,
|
||||
MoveToTeam: '' as Ref<Action>,
|
||||
Relations: '' as Ref<Action>,
|
||||
CopyIssueLink: '' as Ref<Action>
|
||||
EditWorkflowStatuses: '' as Ref<Action>
|
||||
},
|
||||
team: {
|
||||
DefaultTeam: '' as Ref<Team>
|
||||
|
Loading…
Reference in New Issue
Block a user