mirror of
https://github.com/hcengineering/platform.git
synced 2024-12-23 03:22:19 +03:00
Tracker: Add relation (#2174)
Signed-off-by: Dvinyanin Alexandr <dvinyanin.alexandr@gmail.com>
This commit is contained in:
parent
ce71d23f49
commit
25afe12cc2
@ -15,6 +15,7 @@ HR:
|
|||||||
|
|
||||||
Tracker:
|
Tracker:
|
||||||
- Manual issues ordering
|
- Manual issues ordering
|
||||||
|
- Issue relations
|
||||||
|
|
||||||
Workbench
|
Workbench
|
||||||
- Use application aliases in URL
|
- Use application aliases in URL
|
||||||
|
@ -853,7 +853,7 @@ export function createModel (builder: Builder): void {
|
|||||||
builder,
|
builder,
|
||||||
{
|
{
|
||||||
action: view.actionImpl.Move,
|
action: view.actionImpl.Move,
|
||||||
label: view.string.Move,
|
label: tracker.string.MoveToTeam,
|
||||||
icon: view.icon.Move,
|
icon: view.icon.Move,
|
||||||
keyBinding: [],
|
keyBinding: [],
|
||||||
input: 'none',
|
input: 'none',
|
||||||
@ -862,9 +862,32 @@ export function createModel (builder: Builder): void {
|
|||||||
context: {
|
context: {
|
||||||
mode: ['context', 'browser'],
|
mode: ['context', 'browser'],
|
||||||
application: tracker.app.Tracker,
|
application: tracker.app.Tracker,
|
||||||
group: 'edit'
|
group: 'associate'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
tracker.action.CopyIssueLink
|
tracker.action.MoveToTeam
|
||||||
|
)
|
||||||
|
// TODO: fix icon
|
||||||
|
createAction(
|
||||||
|
builder,
|
||||||
|
{
|
||||||
|
action: view.actionImpl.ValueSelector,
|
||||||
|
actionPopup: tracker.component.RelationsPopup,
|
||||||
|
actionProps: {
|
||||||
|
attribute: ''
|
||||||
|
},
|
||||||
|
label: tracker.string.Relations,
|
||||||
|
icon: tracker.icon.Document,
|
||||||
|
keyBinding: [],
|
||||||
|
input: 'focus',
|
||||||
|
category: tracker.category.Tracker,
|
||||||
|
target: tracker.class.Issue,
|
||||||
|
context: {
|
||||||
|
mode: ['context', 'browser'],
|
||||||
|
application: tracker.app.Tracker,
|
||||||
|
group: 'associate'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
tracker.action.Relations
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -34,6 +34,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@anticrm/platform": "~0.6.6",
|
"@anticrm/platform": "~0.6.6",
|
||||||
"@anticrm/theme": "~0.6.0",
|
"@anticrm/theme": "~0.6.0",
|
||||||
|
"@anticrm/core": "~0.6.16",
|
||||||
"svelte": "^3.47",
|
"svelte": "^3.47",
|
||||||
"@types/jest": "~28.1.0"
|
"@types/jest": "~28.1.0"
|
||||||
}
|
}
|
||||||
|
@ -14,6 +14,7 @@
|
|||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { afterUpdate, createEventDispatcher, onDestroy, onMount } from 'svelte'
|
import { afterUpdate, createEventDispatcher, onDestroy, onMount } from 'svelte'
|
||||||
|
import { generateId } from '@anticrm/core'
|
||||||
import ui from '../plugin'
|
import ui from '../plugin'
|
||||||
import { closePopup, showPopup } from '../popups'
|
import { closePopup, showPopup } from '../popups'
|
||||||
import { Action } from '../types'
|
import { Action } from '../types'
|
||||||
@ -27,6 +28,7 @@
|
|||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
const btns: HTMLElement[] = []
|
const btns: HTMLElement[] = []
|
||||||
let activeElement: HTMLElement
|
let activeElement: HTMLElement
|
||||||
|
const category = generateId()
|
||||||
|
|
||||||
const keyDown = (ev: KeyboardEvent): void => {
|
const keyDown = (ev: KeyboardEvent): void => {
|
||||||
if (ev.key === 'Tab') {
|
if (ev.key === 'Tab') {
|
||||||
@ -51,7 +53,7 @@
|
|||||||
}
|
}
|
||||||
if (ev.key === 'ArrowLeft') {
|
if (ev.key === 'ArrowLeft') {
|
||||||
dispatch('update', 'left')
|
dispatch('update', 'left')
|
||||||
closePopup('submenu')
|
closePopup(category)
|
||||||
ev.preventDefault()
|
ev.preventDefault()
|
||||||
ev.stopPropagation()
|
ev.stopPropagation()
|
||||||
}
|
}
|
||||||
@ -72,11 +74,11 @@
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
onDestroy(() => {
|
onDestroy(() => {
|
||||||
closePopup('submenu')
|
closePopup(category)
|
||||||
})
|
})
|
||||||
|
|
||||||
function showActionPopup (action: Action, target: HTMLElement): void {
|
function showActionPopup (action: Action, target: HTMLElement): void {
|
||||||
closePopup('submenu')
|
closePopup(category)
|
||||||
if (action.component !== undefined) {
|
if (action.component !== undefined) {
|
||||||
console.log(action.props)
|
console.log(action.props)
|
||||||
showPopup(
|
showPopup(
|
||||||
@ -87,7 +89,7 @@
|
|||||||
dispatch('close')
|
dispatch('close')
|
||||||
},
|
},
|
||||||
undefined,
|
undefined,
|
||||||
{ category: 'submenu', overlay: false }
|
{ category, overlay: false }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -123,6 +123,7 @@
|
|||||||
"ProjectLeadSearchPlaceholder": "Set project lead\u2026",
|
"ProjectLeadSearchPlaceholder": "Set project lead\u2026",
|
||||||
"ProjectMembersSearchPlaceholder": "Change project members\u2026",
|
"ProjectMembersSearchPlaceholder": "Change project members\u2026",
|
||||||
"Roadmap": "Roadmap",
|
"Roadmap": "Roadmap",
|
||||||
|
"MoveToTeam": "Move to team",
|
||||||
|
|
||||||
"GotoIssues": "Go to issues",
|
"GotoIssues": "Go to issues",
|
||||||
"GotoActive": "Go to active issues",
|
"GotoActive": "Go to active issues",
|
||||||
@ -141,6 +142,20 @@
|
|||||||
"Created": "Created",
|
"Created": "Created",
|
||||||
"Subscribed": "Subscribed",
|
"Subscribed": "Subscribed",
|
||||||
|
|
||||||
|
"Relations": "Relations",
|
||||||
|
"RemoveRelation": "Remove relation...",
|
||||||
|
"AddBlockedBy": "Mark as blocked by...",
|
||||||
|
"AddIsBlocking": "Mark as bloking...",
|
||||||
|
"AddRelatedIssue": "Reference another issue...",
|
||||||
|
"RelatedIssue": "Related issue {id} - {title}",
|
||||||
|
"BlockedIssue": "Blocked issue {id} - {title}",
|
||||||
|
"BlockingIssue": "Blocking issue {id} - {title}",
|
||||||
|
"BlockedBySearchPlaceholder": "Search for issue to mark blocked by...",
|
||||||
|
"IsBlockingSearchPlaceholder": "Search for issue to mark as blocking...",
|
||||||
|
"RelatedIssueSearchPlaceholder": "Search for issue to reference...",
|
||||||
|
"Blocks": "Blocks",
|
||||||
|
"Related": "Related",
|
||||||
|
|
||||||
"EditIssue": "Edit {title}",
|
"EditIssue": "Edit {title}",
|
||||||
|
|
||||||
"Save": "Save",
|
"Save": "Save",
|
||||||
|
@ -123,6 +123,7 @@
|
|||||||
"ProjectLeadSearchPlaceholder": "Назначьте руководителя проекта\u2026",
|
"ProjectLeadSearchPlaceholder": "Назначьте руководителя проекта\u2026",
|
||||||
"ProjectMembersSearchPlaceholder": "Измененить участников проекта\u2026",
|
"ProjectMembersSearchPlaceholder": "Измененить участников проекта\u2026",
|
||||||
"Roadmap": "Планирование",
|
"Roadmap": "Планирование",
|
||||||
|
"MoveToTeam": "Изменить команду",
|
||||||
|
|
||||||
"GotoIssues": "Перейти к задачам",
|
"GotoIssues": "Перейти к задачам",
|
||||||
"GotoActive": "Перейти к активным задачам",
|
"GotoActive": "Перейти к активным задачам",
|
||||||
@ -141,6 +142,20 @@
|
|||||||
"Created": "Созданные",
|
"Created": "Созданные",
|
||||||
"Subscribed": "Отслеживаемые",
|
"Subscribed": "Отслеживаемые",
|
||||||
|
|
||||||
|
"Relations": "Зависимости",
|
||||||
|
"RemoveRelation": "Удалить зависимость...",
|
||||||
|
"AddBlockedBy": "Отметить как блокируемую...",
|
||||||
|
"AddIsBlocking": "Отметить как блокирующую...",
|
||||||
|
"AddRelatedIssue": "Связать с задачей...",
|
||||||
|
"RelatedIssue": "Связанная задача {id} - {title}",
|
||||||
|
"BlockedIssue": "Блокируемая задача {id} - {title}",
|
||||||
|
"BlockingIssue": "Блокирующая {id} - {title}",
|
||||||
|
"BlockedBySearchPlaceholder": "Поиск блокирующей задачи...",
|
||||||
|
"IsBlockingSearchPlaceholder": "Поиск блокируемой задачи...",
|
||||||
|
"RelatedIssueSearchPlaceholder": "Поиск связанной задачи...",
|
||||||
|
"Blocks": "Блокирует",
|
||||||
|
"Related": "Связан",
|
||||||
|
|
||||||
"EditIssue": "Редактирование {title}",
|
"EditIssue": "Редактирование {title}",
|
||||||
|
|
||||||
"Save": "Сохранить",
|
"Save": "Сохранить",
|
||||||
|
@ -0,0 +1,90 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Ref } from '@anticrm/core'
|
||||||
|
import { createQuery, getClient } from '@anticrm/presentation'
|
||||||
|
import { Issue } from '@anticrm/tracker'
|
||||||
|
import { Action, closePopup, Menu, showPopup } from '@anticrm/ui'
|
||||||
|
import SelectIssuePopup from './SelectIssuePopup.svelte'
|
||||||
|
import SelectRelationPopup from './SelectRelationPopup.svelte'
|
||||||
|
import tracker from '../plugin'
|
||||||
|
import { updateIssueRelation } from '../issues'
|
||||||
|
import { IntlString } from '@anticrm/platform'
|
||||||
|
|
||||||
|
export let value: Issue
|
||||||
|
|
||||||
|
const client = getClient()
|
||||||
|
const query = createQuery()
|
||||||
|
$: relations = {
|
||||||
|
blockedBy: value.blockedBy ?? [],
|
||||||
|
relatedIssue: value.relatedIssue ?? [],
|
||||||
|
isBlocking: isBlocking ?? []
|
||||||
|
}
|
||||||
|
let isBlocking: Ref<Issue>[] = []
|
||||||
|
$: query.query(tracker.class.Issue, { blockedBy: value._id }, (result) => {
|
||||||
|
isBlocking = result.map(({ _id }) => _id)
|
||||||
|
})
|
||||||
|
$: hasRelation = Object.values(relations).some(({ length }) => length)
|
||||||
|
|
||||||
|
async function updateRelation (issue: Issue, type: keyof typeof relations, operation: '$push' | '$pull') {
|
||||||
|
const prop = type === 'isBlocking' ? 'blockedBy' : type
|
||||||
|
if (type !== 'isBlocking') {
|
||||||
|
await updateIssueRelation(client, value, issue._id, prop, operation)
|
||||||
|
}
|
||||||
|
if (type !== 'blockedBy') {
|
||||||
|
await updateIssueRelation(client, issue, value._id, prop, operation)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const makeAddAction = (type: keyof typeof relations, placeholder: IntlString) => async () => {
|
||||||
|
closePopup('popup')
|
||||||
|
showPopup(
|
||||||
|
SelectIssuePopup,
|
||||||
|
{ ignoreObjects: [value._id, ...relations[type]], placeholder },
|
||||||
|
undefined,
|
||||||
|
async (issue: Issue | undefined) => {
|
||||||
|
if (!issue) return
|
||||||
|
await updateRelation(issue, type, '$push')
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
async function removeRelation () {
|
||||||
|
closePopup('popup')
|
||||||
|
showPopup(
|
||||||
|
SelectRelationPopup,
|
||||||
|
relations,
|
||||||
|
undefined,
|
||||||
|
async (result: { type: keyof typeof relations; issue: Issue } | undefined) => {
|
||||||
|
if (!result) return
|
||||||
|
await updateRelation(result.issue, result.type, '$pull')
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const removeRelationAction: Action[] = [
|
||||||
|
{
|
||||||
|
action: removeRelation,
|
||||||
|
icon: tracker.icon.Issue,
|
||||||
|
label: tracker.string.RemoveRelation,
|
||||||
|
group: '1'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
$: actions = [
|
||||||
|
{
|
||||||
|
action: makeAddAction('blockedBy', tracker.string.BlockedBySearchPlaceholder),
|
||||||
|
icon: tracker.icon.Issue,
|
||||||
|
label: tracker.string.AddBlockedBy
|
||||||
|
},
|
||||||
|
{
|
||||||
|
action: makeAddAction('isBlocking', tracker.string.IsBlockingSearchPlaceholder),
|
||||||
|
icon: tracker.icon.Issue,
|
||||||
|
label: tracker.string.AddIsBlocking
|
||||||
|
},
|
||||||
|
{
|
||||||
|
action: makeAddAction('relatedIssue', tracker.string.RelatedIssueSearchPlaceholder),
|
||||||
|
icon: tracker.icon.Issue,
|
||||||
|
label: tracker.string.AddRelatedIssue
|
||||||
|
},
|
||||||
|
...(hasRelation ? removeRelationAction : [])
|
||||||
|
]
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Menu {actions} />
|
@ -0,0 +1,49 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { DocumentQuery, FindOptions, Ref, SortingOrder } from '@anticrm/core'
|
||||||
|
import { IntlString } from '@anticrm/platform'
|
||||||
|
import { ObjectPopup } from '@anticrm/presentation'
|
||||||
|
import { Issue } from '@anticrm/tracker'
|
||||||
|
import { Icon } from '@anticrm/ui'
|
||||||
|
import { getIssueId } from '../issues'
|
||||||
|
import tracker from '../plugin'
|
||||||
|
|
||||||
|
export let docQuery: DocumentQuery<Issue> | undefined = undefined
|
||||||
|
export let ignoreObjects: Ref<Issue>[] | undefined = undefined
|
||||||
|
export let placeholder: IntlString | undefined = undefined
|
||||||
|
export let width: 'medium' | 'large' | 'full' = 'large'
|
||||||
|
|
||||||
|
const options: FindOptions<Issue> = {
|
||||||
|
lookup: {
|
||||||
|
status: [tracker.class.IssueStatus, { category: tracker.class.IssueStatusCategory }],
|
||||||
|
space: tracker.class.Team
|
||||||
|
},
|
||||||
|
sort: { modifiedOn: SortingOrder.Descending }
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<ObjectPopup
|
||||||
|
_class={tracker.class.Issue}
|
||||||
|
{options}
|
||||||
|
{docQuery}
|
||||||
|
{placeholder}
|
||||||
|
{ignoreObjects}
|
||||||
|
{width}
|
||||||
|
searchField="title"
|
||||||
|
groupBy="space"
|
||||||
|
on:update
|
||||||
|
on:close
|
||||||
|
>
|
||||||
|
<svelte:fragment slot="item" let:item={issue}>
|
||||||
|
{@const { icon } = issue.$lookup?.status.$lookup?.category ?? {}}
|
||||||
|
{@const issueId = getIssueId(issue.$lookup.space, issue)}
|
||||||
|
{#if issueId && icon}
|
||||||
|
<div class="flex-center clear-mins w-full h-9">
|
||||||
|
<div class="icon mr-4 h-8">
|
||||||
|
<Icon {icon} size="small" />
|
||||||
|
</div>
|
||||||
|
<span class="overflow-label flex-no-shrink mr-3">{issueId}</span>
|
||||||
|
<span class="overflow-label w-full issue-title">{issue.title}</span>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</svelte:fragment>
|
||||||
|
</ObjectPopup>
|
@ -0,0 +1,70 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { createEventDispatcher } from 'svelte'
|
||||||
|
import { Ref } from '@anticrm/core'
|
||||||
|
import { getClient } from '@anticrm/presentation'
|
||||||
|
import { Issue } from '@anticrm/tracker'
|
||||||
|
import { SelectPopup, Loading } from '@anticrm/ui'
|
||||||
|
import { translate } from '@anticrm/platform'
|
||||||
|
import { getIssueId } from '../issues'
|
||||||
|
import tracker from '../plugin'
|
||||||
|
|
||||||
|
export let blockedBy: Ref<Issue>[] = []
|
||||||
|
export let isBlocking: Ref<Issue>[] = []
|
||||||
|
export let relatedIssue: Ref<Issue>[] = []
|
||||||
|
|
||||||
|
// TODO: fix icons
|
||||||
|
const config = {
|
||||||
|
blockedBy: {
|
||||||
|
label: tracker.string.BlockedIssue,
|
||||||
|
icon: tracker.icon.Issue
|
||||||
|
},
|
||||||
|
isBlocking: {
|
||||||
|
label: tracker.string.BlockingIssue,
|
||||||
|
icon: tracker.icon.Issues
|
||||||
|
},
|
||||||
|
relatedIssue: {
|
||||||
|
label: tracker.string.RelatedIssue,
|
||||||
|
icon: tracker.icon.Team
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const client = getClient()
|
||||||
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
|
async function getValue () {
|
||||||
|
const issues = await client.findAll(
|
||||||
|
tracker.class.Issue,
|
||||||
|
{ _id: { $in: [...blockedBy, ...isBlocking, ...relatedIssue] } },
|
||||||
|
{ lookup: { space: tracker.class.Team } }
|
||||||
|
)
|
||||||
|
const valueFactory = (type: keyof typeof config) => async (issueId: Ref<Issue>) => {
|
||||||
|
const issue = issues.find(({ _id }) => _id === issueId)
|
||||||
|
if (!issue?.$lookup?.space) return
|
||||||
|
const { label, icon } = config[type]
|
||||||
|
const text = await translate(label, { id: getIssueId(issue.$lookup.space, issue), title: issue.title })
|
||||||
|
return { text, icon, issue, type }
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
await Promise.all([
|
||||||
|
...blockedBy.map(valueFactory('blockedBy')),
|
||||||
|
...isBlocking.map(valueFactory('isBlocking')),
|
||||||
|
...relatedIssue.map(valueFactory('relatedIssue'))
|
||||||
|
])
|
||||||
|
).map((val, id) => ({ ...val, id }))
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#await getValue()}
|
||||||
|
<Loading />
|
||||||
|
{:then value}
|
||||||
|
<SelectPopup
|
||||||
|
{value}
|
||||||
|
width="large"
|
||||||
|
searchable
|
||||||
|
placeholder={tracker.string.RemoveRelation}
|
||||||
|
on:close={(e) => {
|
||||||
|
if (e.detail === undefined) dispatch('close')
|
||||||
|
else dispatch('close', value[e.detail])
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{/await}
|
@ -0,0 +1,83 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { WithLookup } from '@anticrm/core'
|
||||||
|
import { createQuery, getClient } from '@anticrm/presentation'
|
||||||
|
import { Issue } from '@anticrm/tracker'
|
||||||
|
import { Icon, IconClose } from '@anticrm/ui'
|
||||||
|
import { getIssueId, updateIssueRelation } from '../../issues'
|
||||||
|
import tracker from '../../plugin'
|
||||||
|
|
||||||
|
export let value: Issue
|
||||||
|
export let type: 'isBlocking' | 'blockedBy' | 'relatedIssue'
|
||||||
|
|
||||||
|
const client = getClient()
|
||||||
|
const issuesQuery = createQuery()
|
||||||
|
|
||||||
|
// TODO: fix icon
|
||||||
|
$: icon = tracker.icon.Issue
|
||||||
|
$: query = type === 'isBlocking' ? { blockedBy: value._id } : { _id: { $in: value[type] } }
|
||||||
|
let issues: WithLookup<Issue>[] = []
|
||||||
|
$: issuesQuery.query(
|
||||||
|
tracker.class.Issue,
|
||||||
|
query,
|
||||||
|
(result) => {
|
||||||
|
issues = result
|
||||||
|
},
|
||||||
|
{ lookup: { space: tracker.class.Team } }
|
||||||
|
)
|
||||||
|
|
||||||
|
async function handleClick (issue: Issue) {
|
||||||
|
const prop = type === 'isBlocking' ? 'blockedBy' : type
|
||||||
|
if (type !== 'isBlocking') {
|
||||||
|
await updateIssueRelation(client, value, issue._id, prop, '$pull')
|
||||||
|
}
|
||||||
|
if (type !== 'blockedBy') {
|
||||||
|
await updateIssueRelation(client, issue, value._id, prop, '$pull')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="flex-column">
|
||||||
|
{#each issues as issue}
|
||||||
|
{#if issue.$lookup?.space}
|
||||||
|
<div class="tag-container">
|
||||||
|
<Icon {icon} size={'small'} />
|
||||||
|
<span class="overflow-label ml-1-5 caption-color">{getIssueId(issue.$lookup.space, issue)}</span>
|
||||||
|
<button class="btn-close" on:click|stopPropagation={() => handleClick(issue)}>
|
||||||
|
<Icon icon={IconClose} size={'x-small'} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.tag-container {
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
padding-left: 0.5rem;
|
||||||
|
height: 1.5rem;
|
||||||
|
min-width: 0;
|
||||||
|
min-height: 0;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
width: fit-content;
|
||||||
|
&:hover {
|
||||||
|
border: 1px solid var(--divider-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-close {
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-left: 0.125rem;
|
||||||
|
padding: 0 0.25rem 0 0.125rem;
|
||||||
|
height: 1.75rem;
|
||||||
|
color: var(--content-color);
|
||||||
|
border-left: 1px solid transparent;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: var(--caption-color);
|
||||||
|
border-left-color: var(--divider-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
@ -15,6 +15,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { WithLookup } from '@anticrm/core'
|
import { WithLookup } from '@anticrm/core'
|
||||||
import type { Issue, IssueStatus } from '@anticrm/tracker'
|
import type { Issue, IssueStatus } from '@anticrm/tracker'
|
||||||
|
import { createQuery } from '@anticrm/presentation'
|
||||||
import { Component, Label } from '@anticrm/ui'
|
import { Component, Label } from '@anticrm/ui'
|
||||||
import tags from '@anticrm/tags'
|
import tags from '@anticrm/tags'
|
||||||
import tracker from '../../../plugin'
|
import tracker from '../../../plugin'
|
||||||
@ -23,9 +24,16 @@
|
|||||||
import ProjectEditor from '../../projects/ProjectEditor.svelte'
|
import ProjectEditor from '../../projects/ProjectEditor.svelte'
|
||||||
import AssigneeEditor from '../AssigneeEditor.svelte'
|
import AssigneeEditor from '../AssigneeEditor.svelte'
|
||||||
import DueDateEditor from '../DueDateEditor.svelte'
|
import DueDateEditor from '../DueDateEditor.svelte'
|
||||||
|
import RelationEditor from '../RelationEditor.svelte'
|
||||||
|
|
||||||
export let issue: Issue
|
export let issue: Issue
|
||||||
export let issueStatuses: WithLookup<IssueStatus>[]
|
export let issueStatuses: WithLookup<IssueStatus>[]
|
||||||
|
|
||||||
|
const query = createQuery()
|
||||||
|
let showIsBlocking = false
|
||||||
|
query.query(tracker.class.Issue, { blockedBy: issue._id }, (result) => {
|
||||||
|
showIsBlocking = result.length > 0
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="content">
|
<div class="content">
|
||||||
@ -34,6 +42,25 @@
|
|||||||
</span>
|
</span>
|
||||||
<StatusEditor value={issue} statuses={issueStatuses} shouldShowLabel />
|
<StatusEditor value={issue} statuses={issueStatuses} shouldShowLabel />
|
||||||
|
|
||||||
|
{#if issue.blockedBy?.length}
|
||||||
|
<span class="labelTop">
|
||||||
|
<Label label={tracker.string.BlockedBy} />
|
||||||
|
</span>
|
||||||
|
<RelationEditor value={issue} type="blockedBy" />
|
||||||
|
{/if}
|
||||||
|
{#if showIsBlocking}
|
||||||
|
<span class="labelTop">
|
||||||
|
<Label label={tracker.string.Blocks} />
|
||||||
|
</span>
|
||||||
|
<RelationEditor value={issue} type="isBlocking" />
|
||||||
|
{/if}
|
||||||
|
{#if issue.relatedIssue?.length}
|
||||||
|
<span class="labelTop">
|
||||||
|
<Label label={tracker.string.Related} />
|
||||||
|
</span>
|
||||||
|
<RelationEditor value={issue} type="relatedIssue" />
|
||||||
|
{/if}
|
||||||
|
|
||||||
<span class="label">
|
<span class="label">
|
||||||
<Label label={tracker.string.Priority} />
|
<Label label={tracker.string.Priority} />
|
||||||
</span>
|
</span>
|
||||||
|
@ -59,6 +59,7 @@ import KanbanView from './components/issues/KanbanView.svelte'
|
|||||||
import tracker from './plugin'
|
import tracker from './plugin'
|
||||||
import { copyToClipboard, getIssueId, getIssueTitle, resolveLocation } from './issues'
|
import { copyToClipboard, getIssueId, getIssueTitle, resolveLocation } from './issues'
|
||||||
import CreateIssue from './components/CreateIssue.svelte'
|
import CreateIssue from './components/CreateIssue.svelte'
|
||||||
|
import RelationsPopup from './components/RelationsPopup.svelte'
|
||||||
|
|
||||||
export async function queryIssue<D extends Issue> (
|
export async function queryIssue<D extends Issue> (
|
||||||
_class: Ref<Class<D>>,
|
_class: Ref<Class<D>>,
|
||||||
@ -149,6 +150,7 @@ export default async (): Promise<Resources> => ({
|
|||||||
TeamProjects,
|
TeamProjects,
|
||||||
Roadmap,
|
Roadmap,
|
||||||
IssuePreview,
|
IssuePreview,
|
||||||
|
RelationsPopup,
|
||||||
CreateIssue
|
CreateIssue
|
||||||
},
|
},
|
||||||
completion: {
|
completion: {
|
||||||
|
@ -95,3 +95,16 @@ export async function resolveLocation (loc: Location): Promise<Location | undefi
|
|||||||
|
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function updateIssueRelation (
|
||||||
|
client: TxOperations,
|
||||||
|
value: Issue,
|
||||||
|
id: Ref<Issue>,
|
||||||
|
prop: 'blockedBy' | 'relatedIssue',
|
||||||
|
operation: '$push' | '$pull'
|
||||||
|
): Promise<void> {
|
||||||
|
const update = Array.isArray(value[prop])
|
||||||
|
? { [operation]: { [prop]: id } }
|
||||||
|
: { [prop]: operation === '$push' ? [id] : [] }
|
||||||
|
await client.update(value, update)
|
||||||
|
}
|
||||||
|
@ -140,6 +140,7 @@ export default mergeIds(trackerId, tracker, {
|
|||||||
List: '' as IntlString,
|
List: '' as IntlString,
|
||||||
NumberLabels: '' as IntlString,
|
NumberLabels: '' as IntlString,
|
||||||
Roadmap: '' as IntlString,
|
Roadmap: '' as IntlString,
|
||||||
|
MoveToTeam: '' as IntlString,
|
||||||
|
|
||||||
IssueTitlePlaceholder: '' as IntlString,
|
IssueTitlePlaceholder: '' as IntlString,
|
||||||
IssueDescriptionPlaceholder: '' as IntlString,
|
IssueDescriptionPlaceholder: '' as IntlString,
|
||||||
@ -168,6 +169,20 @@ export default mergeIds(trackerId, tracker, {
|
|||||||
Created: '' as IntlString,
|
Created: '' as IntlString,
|
||||||
Subscribed: '' as IntlString,
|
Subscribed: '' as IntlString,
|
||||||
|
|
||||||
|
Relations: '' as IntlString,
|
||||||
|
RemoveRelation: '' as IntlString,
|
||||||
|
AddBlockedBy: '' as IntlString,
|
||||||
|
AddIsBlocking: '' as IntlString,
|
||||||
|
AddRelatedIssue: '' as IntlString,
|
||||||
|
RelatedIssue: '' as IntlString,
|
||||||
|
BlockedIssue: '' as IntlString,
|
||||||
|
BlockingIssue: '' as IntlString,
|
||||||
|
BlockedBySearchPlaceholder: '' as IntlString,
|
||||||
|
IsBlockingSearchPlaceholder: '' as IntlString,
|
||||||
|
RelatedIssueSearchPlaceholder: '' as IntlString,
|
||||||
|
Blocks: '' as IntlString,
|
||||||
|
Related: '' as IntlString,
|
||||||
|
|
||||||
DurMinutes: '' as IntlString,
|
DurMinutes: '' as IntlString,
|
||||||
DurHours: '' as IntlString,
|
DurHours: '' as IntlString,
|
||||||
DurDays: '' as IntlString,
|
DurDays: '' as IntlString,
|
||||||
@ -215,6 +230,7 @@ export default mergeIds(trackerId, tracker, {
|
|||||||
Roadmap: '' as AnyComponent,
|
Roadmap: '' as AnyComponent,
|
||||||
TeamProjects: '' as AnyComponent,
|
TeamProjects: '' as AnyComponent,
|
||||||
IssuePreview: '' as AnyComponent,
|
IssuePreview: '' as AnyComponent,
|
||||||
|
RelationsPopup: '' as AnyComponent,
|
||||||
CreateIssue: '' as AnyComponent
|
CreateIssue: '' as AnyComponent
|
||||||
},
|
},
|
||||||
function: {
|
function: {
|
||||||
|
@ -284,6 +284,8 @@ export default plugin(trackerId, {
|
|||||||
SetProject: '' as Ref<Action>,
|
SetProject: '' as Ref<Action>,
|
||||||
CopyIssueId: '' as Ref<Action>,
|
CopyIssueId: '' as Ref<Action>,
|
||||||
CopyIssueTitle: '' as Ref<Action>,
|
CopyIssueTitle: '' as Ref<Action>,
|
||||||
|
MoveToTeam: '' as Ref<Action>,
|
||||||
|
Relations: '' as Ref<Action>,
|
||||||
CopyIssueLink: '' as Ref<Action>
|
CopyIssueLink: '' as Ref<Action>
|
||||||
},
|
},
|
||||||
team: {
|
team: {
|
||||||
|
Loading…
Reference in New Issue
Block a user