Tracker: workflow statuses (#2171)

Signed-off-by: Sergei Ogorelkov <sergei.ogorelkov@xored.com>
This commit is contained in:
Sergei Ogorelkov 2022-07-01 22:24:58 +07:00 committed by GitHub
parent efc3583376
commit 69a8d16db4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 731 additions and 4 deletions

View File

@ -17,6 +17,7 @@ HR:
Tracker:
- Manual issues ordering
- Issue relations
- Issue status management
Workbench
- Use application aliases in URL

View File

@ -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,

View File

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

View File

@ -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>

View File

@ -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",

View File

@ -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": "Включить элементы, которые соответствуют",

View File

@ -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>

View File

@ -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>

View File

@ -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>&nbsp;·&nbsp;{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>

View File

@ -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>

View File

@ -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

View File

@ -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,

View File

@ -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>