mirror of
https://github.com/hcengineering/platform.git
synced 2026-09-06 09:47:43 +02:00
UBERF-9264: Fix set/unset parent issue (#7799)
Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
@@ -276,6 +276,30 @@ export function createActions (builder: Builder, issuesId: string, componentsId:
|
||||
},
|
||||
tracker.action.SetParent
|
||||
)
|
||||
createAction(
|
||||
builder,
|
||||
{
|
||||
action: view.actionImpl.UpdateDocument,
|
||||
actionProps: {
|
||||
key: 'attachedTo',
|
||||
value: tracker.ids.NoParent
|
||||
},
|
||||
query: {
|
||||
attachedTo: { $ne: tracker.ids.NoParent }
|
||||
},
|
||||
label: tracker.string.UnsetParentIssue,
|
||||
icon: tracker.icon.UnsetParent,
|
||||
input: 'none',
|
||||
category: tracker.category.Tracker,
|
||||
target: tracker.class.Issue,
|
||||
context: {
|
||||
mode: ['context'],
|
||||
application: tracker.app.Tracker,
|
||||
group: 'associate'
|
||||
}
|
||||
},
|
||||
tracker.action.UnsetParent
|
||||
)
|
||||
|
||||
createAction(
|
||||
builder,
|
||||
|
||||
@@ -15,8 +15,7 @@
|
||||
//
|
||||
import { type DocUpdateMessageViewlet } from '@hcengineering/activity'
|
||||
import { type ChatMessageViewlet } from '@hcengineering/chunter'
|
||||
import { type StatusCategory, type Doc, type Ref, type DocManager } from '@hcengineering/core'
|
||||
import { type ObjectSearchCategory, type ObjectSearchFactory } from '@hcengineering/model-presentation'
|
||||
import { type Doc, type DocManager, type Ref, type StatusCategory } from '@hcengineering/core'
|
||||
import { type NotificationGroup, type NotificationType } from '@hcengineering/notification'
|
||||
import { mergeIds, type IntlString, type Resource } from '@hcengineering/platform'
|
||||
import { type ProjectType } from '@hcengineering/task'
|
||||
@@ -87,10 +86,6 @@ export default mergeIds(trackerId, tracker, {
|
||||
MilestoneChatMessageViewlet: '' as Ref<ChatMessageViewlet>,
|
||||
DefaultProjectType: '' as Ref<ProjectType>
|
||||
},
|
||||
completion: {
|
||||
IssueQuery: '' as Resource<ObjectSearchFactory>,
|
||||
IssueCategory: '' as Ref<ObjectSearchCategory>
|
||||
},
|
||||
actionImpl: {
|
||||
Move: '' as ViewAction,
|
||||
CopyToClipboard: '' as ViewAction,
|
||||
|
||||
@@ -24,8 +24,8 @@
|
||||
import type { IntlString } from '@hcengineering/platform'
|
||||
import { Label } from '@hcengineering/ui'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import presentation from '..'
|
||||
import { ObjectCreate } from '../types'
|
||||
import presentation, { searchFor, type SearchItem } from '..'
|
||||
import { ObjectCreate, type ObjectSearchCategory } from '../types'
|
||||
import { createQuery } from '../utils'
|
||||
import DocPopup from './DocPopup.svelte'
|
||||
|
||||
@@ -46,7 +46,8 @@
|
||||
export let width: 'medium' | 'large' | 'full' | 'auto' = 'medium'
|
||||
export let size: 'small' | 'medium' | 'large' = 'large'
|
||||
|
||||
export let searchMode: 'field' | 'fulltext' | 'disabled' = 'field'
|
||||
export let searchMode: 'field' | 'fulltext' | 'disabled' | 'spotlight' = 'field'
|
||||
export let category: Ref<ObjectSearchCategory> | undefined = undefined
|
||||
export let searchField: string = 'name'
|
||||
export let groupBy = '_class'
|
||||
|
||||
@@ -74,21 +75,39 @@
|
||||
let search: string = ''
|
||||
let objects: Doc[] = []
|
||||
|
||||
let extraItems: Ref<Doc>[] = []
|
||||
|
||||
const query = createQuery()
|
||||
|
||||
$: noSearchField = searchMode === 'disabled'
|
||||
$: _idExtra = typeof docQuery?._id === 'object' ? docQuery?._id : {}
|
||||
$: if (searchMode === 'spotlight' && search !== '') {
|
||||
void searchSpotlight(search).then((items) => {
|
||||
extraItems = items.map((it) => it.item.id)
|
||||
})
|
||||
} else {
|
||||
extraItems = []
|
||||
}
|
||||
$: fquery = {
|
||||
...(docQuery ?? {}),
|
||||
...(() => {
|
||||
switch (searchMode) {
|
||||
case 'disabled':
|
||||
return { _id: { $nin: ignoreObjects, ..._idExtra } }
|
||||
case 'fulltext':
|
||||
return { $search: search, _id: { $nin: ignoreObjects, ..._idExtra } }
|
||||
case 'spotlight':
|
||||
return extraItems.length > 0
|
||||
? { _id: { $in: extraItems, $nin: ignoreObjects } }
|
||||
: { _id: { $nin: ignoreObjects, ..._idExtra } }
|
||||
default:
|
||||
return { [searchField]: { $like: '%' + search + '%' }, _id: { $nin: ignoreObjects, ..._idExtra } }
|
||||
}
|
||||
})()
|
||||
}
|
||||
$: query.query<Doc>(
|
||||
_class,
|
||||
{
|
||||
...(docQuery ?? {}),
|
||||
...(searchMode !== 'disabled' && search !== ''
|
||||
? searchMode === 'fulltext'
|
||||
? { $search: search }
|
||||
: { [searchField]: { $like: '%' + search + '%' } }
|
||||
: {}),
|
||||
_id: { $nin: ignoreObjects, ..._idExtra }
|
||||
},
|
||||
fquery,
|
||||
(result) => {
|
||||
result.sort(sort)
|
||||
if (created.length > 0) {
|
||||
@@ -100,6 +119,10 @@
|
||||
},
|
||||
{ ...(options ?? {}), limit: 200 }
|
||||
)
|
||||
|
||||
async function searchSpotlight (search: string): Promise<SearchItem[]> {
|
||||
return (await searchFor('spotlight', search, category, 50)).items
|
||||
}
|
||||
</script>
|
||||
|
||||
<DocPopup
|
||||
|
||||
@@ -65,7 +65,8 @@ async function searchCategory (
|
||||
client: TxOperations,
|
||||
cl: Ref<Class<Doc>>,
|
||||
query: string,
|
||||
categories: ObjectSearchCategory[]
|
||||
categories: ObjectSearchCategory[],
|
||||
limit?: number
|
||||
): Promise<SearchSection | undefined> {
|
||||
const r = await client.searchFulltext(
|
||||
{
|
||||
@@ -73,7 +74,7 @@ async function searchCategory (
|
||||
classes: [cl]
|
||||
},
|
||||
{
|
||||
limit: 5
|
||||
limit: limit ?? 5
|
||||
}
|
||||
)
|
||||
const category = findCategoryByClass(categories, cl)
|
||||
@@ -84,12 +85,13 @@ async function doFulltextSearch (
|
||||
client: TxOperations,
|
||||
classes: Array<Ref<Class<Doc>>>,
|
||||
query: string,
|
||||
categories: ObjectSearchCategory[]
|
||||
categories: ObjectSearchCategory[],
|
||||
limit?: number
|
||||
): Promise<SearchSection[]> {
|
||||
const sections: SearchSection[] = []
|
||||
const promises: Array<Promise<SearchSection | undefined>> = []
|
||||
for (const cl of classes) {
|
||||
promises.push(searchCategory(client, cl, query, categories))
|
||||
promises.push(searchCategory(client, cl, query, categories, limit))
|
||||
}
|
||||
|
||||
const resolvedSections = await Promise.all(promises)
|
||||
@@ -113,7 +115,9 @@ const categoriesByContext = new Map<string, ObjectSearchCategory[]>()
|
||||
|
||||
export async function searchFor (
|
||||
context: 'mention' | 'spotlight',
|
||||
query: string
|
||||
query: string,
|
||||
category?: Ref<ObjectSearchCategory>,
|
||||
limit?: number
|
||||
): Promise<{ items: SearchItem[], query: string }> {
|
||||
const client = getClient()
|
||||
let categories = categoriesByContext.get(context)
|
||||
@@ -129,12 +133,13 @@ export async function searchFor (
|
||||
}
|
||||
|
||||
const classesToSearch: Array<Ref<Class<Doc>>> = []
|
||||
for (const cat of categories) {
|
||||
const cats = category === undefined ? categories : categories.filter((it) => it._id === category)
|
||||
for (const cat of cats) {
|
||||
if (cat.classToSearch !== undefined) {
|
||||
classesToSearch.push(cat.classToSearch)
|
||||
}
|
||||
}
|
||||
|
||||
const sections = await doFulltextSearch(client, classesToSearch, query, categories)
|
||||
const sections = await doFulltextSearch(client, classesToSearch, query, categories, limit)
|
||||
return { items: packSearchResultsForListView(sections), query }
|
||||
}
|
||||
|
||||
@@ -27,6 +27,11 @@
|
||||
<symbol id="parent-issue" viewBox="0 0 16 16">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.73444 2.19183C8.27831 1.93589 7.72169 1.93589 7.26556 2.19183L1.25519 5.56425C1.09757 5.65269 1 5.81925 1 5.99988C1 6.1805 1.09757 6.34706 1.25519 6.4355L4.04323 7.99988L1.25519 9.56425C1.09757 9.65269 1 9.81925 1 9.99988C1 10.1805 1.09757 10.3471 1.25519 10.4355L7.26556 13.8079C7.72169 14.0639 8.27831 14.0639 8.73444 13.8079L14.7448 10.4355C14.9024 10.3471 15 10.1805 15 9.99988C15 9.81925 14.9024 9.65269 14.7448 9.56425L11.9568 7.99988L14.7448 6.4355C14.9024 6.34706 15 6.1805 15 5.99988C15 5.81925 14.9024 5.65269 14.7448 5.56425L8.73444 2.19183ZM5.06442 8.57287L7.26556 9.80793C7.72169 10.0639 8.27831 10.0639 8.73444 9.80793L10.9356 8.57287L13.4788 9.99988L8.24481 12.9367C8.09277 13.022 7.90723 13.022 7.75519 12.9367L2.52119 9.99988L5.06442 8.57287Z" />
|
||||
</symbol>
|
||||
<symbol id="unset-parent-issue" viewBox="0 0 16 16">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.73444 2.19183C8.27831 1.93589 7.72169 1.93589 7.26556 2.19183L1.25519 5.56425C1.09757 5.65269 1 5.81925 1 5.99988C1 6.1805 1.09757 6.34706 1.25519 6.4355L4.04323 7.99988L1.25519 9.56425C1.09757 9.65269 1 9.81925 1 9.99988C1 10.1805 1.09757 10.3471 1.25519 10.4355L7.26556 13.8079C7.72169 14.0639 8.27831 14.0639 8.73444 13.8079L14.7448 10.4355C14.9024 10.3471 15 10.1805 15 9.99988C15 9.81925 14.9024 9.65269 14.7448 9.56425L11.9568 7.99988L14.7448 6.4355C14.9024 6.34706 15 6.1805 15 5.99988C15 5.81925 14.9024 5.65269 14.7448 5.56425L8.73444 2.19183ZM5.06442 8.57287L7.26556 9.80793C7.72169 10.0639 8.27831 10.0639 8.73444 9.80793L10.9356 8.57287L13.4788 9.99988L8.24481 12.9367C8.09277 13.022 7.90723 13.022 7.75519 12.9367L2.52119 9.99988L5.06442 8.57287Z"/>
|
||||
<path d="M1 1L15 15" stroke="#000" stroke-width="0.5" />
|
||||
<path d="M1 15L15 1" stroke="#000" stroke-width="0.5" />
|
||||
</symbol>
|
||||
<symbol id="new-issue" viewBox="0 0 16 16">
|
||||
<path d="M8 14.5C8 14.2239 8.22386 14 8.5 14H14.5C14.7761 14 15 14.2239 15 14.5C15 14.7761 14.7761 15 14.5 15H8.5C8.22386 15 8 14.7761 8 14.5Z" />
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M12.5607 1.43934C11.9749 0.853553 11.0251 0.853553 10.4394 1.43934L1.43934 10.4394C1.15804 10.7207 1 11.1022 1 11.5V14.5C1 14.7761 1.22386 15 1.5 15H4.50002C4.89784 15 5.27937 14.842 5.56068 14.5607L14.5607 5.56066C15.1465 4.97487 15.1465 4.02513 14.5607 3.43934L12.5607 1.43934ZM11.1465 2.14645C11.3417 1.95118 11.6583 1.95118 11.8536 2.14645L13.8536 4.14645C14.0488 4.34171 14.0488 4.65829 13.8536 4.85355L11.9571 6.74998L9.25004 4.04287L11.1465 2.14645ZM8.54293 4.74998L2.14645 11.1465C2.05268 11.2402 2 11.3674 2 11.5V14H4.50002C4.63263 14 4.7598 13.9473 4.85357 13.8536L11.25 7.45708L8.54293 4.74998Z" />
|
||||
|
||||
|
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 36 KiB |
@@ -276,7 +276,8 @@
|
||||
"DefaultIssueStatus": "Výchozí stav úkolu",
|
||||
"IssueStatus": "Stav",
|
||||
"Extensions": "Rozšíření",
|
||||
"RoleLabel": "Role: {role}"
|
||||
"RoleLabel": "Role: {role}",
|
||||
"UnsetParentIssue": "Odebrat nadřazený úkol"
|
||||
},
|
||||
"status": {}
|
||||
}
|
||||
@@ -286,7 +286,8 @@
|
||||
"DefaultIssueStatus": "Standard-Aufgabenstatus",
|
||||
"IssueStatus": "Status",
|
||||
"Extensions": "Erweiterungen",
|
||||
"RoleLabel": "Rolle: {role}"
|
||||
"RoleLabel": "Rolle: {role}",
|
||||
"UnsetParentIssue": "Übergeordnete Aufgabe entfernen"
|
||||
},
|
||||
"status": {}
|
||||
}
|
||||
|
||||
@@ -286,7 +286,8 @@
|
||||
"DefaultIssueStatus": "Default issue status",
|
||||
"IssueStatus": "Status",
|
||||
"Extensions": "Extensions",
|
||||
"RoleLabel": "Role: {role}"
|
||||
"RoleLabel": "Role: {role}",
|
||||
"UnsetParentIssue": "Unset parent issue"
|
||||
},
|
||||
"status": {}
|
||||
}
|
||||
|
||||
@@ -269,7 +269,8 @@
|
||||
"DefaultIssueStatus": "Estado de problema predeterminado",
|
||||
"IssueStatus": "Estado",
|
||||
"Extensions": "Extensions",
|
||||
"RoleLabel": "Role: {role}"
|
||||
"RoleLabel": "Role: {role}",
|
||||
"UnsetParentIssue": "Unset parent issue"
|
||||
},
|
||||
"status": {}
|
||||
}
|
||||
@@ -269,7 +269,8 @@
|
||||
"DefaultIssueStatus": "Statut par défaut de l'issue",
|
||||
"IssueStatus": "Statut",
|
||||
"Extensions": "Extensions",
|
||||
"RoleLabel": "Rôle : {role}"
|
||||
"RoleLabel": "Rôle : {role}",
|
||||
"UnsetParentIssue": "Désélectionner l'issue parent"
|
||||
},
|
||||
"status": {}
|
||||
}
|
||||
@@ -269,7 +269,8 @@
|
||||
"DefaultIssueStatus": "Stato predefinito della issue",
|
||||
"IssueStatus": "Stato",
|
||||
"Extensions": "Estensioni",
|
||||
"RoleLabel": "Ruolo: {role}"
|
||||
"RoleLabel": "Ruolo: {role}",
|
||||
"UnsetParentIssue": "Annulla l'issue genitore"
|
||||
},
|
||||
"status": {}
|
||||
}
|
||||
|
||||
@@ -269,7 +269,8 @@
|
||||
"DefaultIssueStatus": "Estado padrão do problema",
|
||||
"IssueStatus": "Estado",
|
||||
"Extensions": "Extensions",
|
||||
"RoleLabel": "Cargo: {role}"
|
||||
"RoleLabel": "Cargo: {role}",
|
||||
"UnsetParentIssue": "Desmarcar problema pai"
|
||||
},
|
||||
"status": {}
|
||||
}
|
||||
@@ -286,7 +286,8 @@
|
||||
"DefaultIssueStatus": "Статус по умолчанию",
|
||||
"IssueStatus": "Статус",
|
||||
"Extensions": "Дополнительно",
|
||||
"RoleLabel": "Роль: {role}"
|
||||
"RoleLabel": "Роль: {role}",
|
||||
"UnsetParentIssue": "Снять родительскую задачу"
|
||||
},
|
||||
"status": {}
|
||||
}
|
||||
|
||||
@@ -286,7 +286,8 @@
|
||||
"DefaultIssueStatus": "默认问题状态",
|
||||
"IssueStatus": "状态",
|
||||
"Extensions": "扩展",
|
||||
"RoleLabel": "角色:{role}"
|
||||
"RoleLabel": "角色:{role}",
|
||||
"UnsetParentIssue": "取消父问题"
|
||||
},
|
||||
"status": {}
|
||||
}
|
||||
|
||||
@@ -36,6 +36,7 @@ loadMetadata(tracker.icon, {
|
||||
Labels: `${icons}#labels`,
|
||||
DueDate: `${icons}#dueDate`, // TODO: add icon
|
||||
Parent: `${icons}#parent-issue`, // TODO: add icon
|
||||
UnsetParent: `${icons}#unset-parent-issue`, // TODO: add icon
|
||||
Milestone: `${icons}#milestone`,
|
||||
IssueTemplates: `${icons}#issuetemplates`,
|
||||
Start: `${icons}#start`,
|
||||
|
||||
@@ -66,24 +66,13 @@
|
||||
|
||||
$: selected = !Array.isArray(value) ? ('attachedTo' in value ? value.attachedTo : undefined) : undefined
|
||||
$: ignoreObjects = !Array.isArray(value) ? ('_id' in value ? [value._id] : []) : undefined
|
||||
$: docQuery = {
|
||||
'parents.parentId': {
|
||||
$nin: [
|
||||
...new Set(
|
||||
(Array.isArray(value) ? value : [value])
|
||||
.map((issue) => ('_id' in issue ? issue._id : null))
|
||||
.filter((x): x is Ref<Issue> => x !== null)
|
||||
)
|
||||
]
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<ObjectPopup
|
||||
_class={tracker.class.Issue}
|
||||
{options}
|
||||
{docQuery}
|
||||
{selected}
|
||||
category={tracker.completion.IssueCategory}
|
||||
multiSelect={false}
|
||||
allowDeselect={true}
|
||||
placeholder={tracker.string.SetParent}
|
||||
@@ -91,7 +80,7 @@
|
||||
{ignoreObjects}
|
||||
shadows={true}
|
||||
{width}
|
||||
searchMode={'fulltext'}
|
||||
searchMode={'spotlight'}
|
||||
on:update
|
||||
on:close={onClose}
|
||||
>
|
||||
|
||||
@@ -175,6 +175,13 @@
|
||||
$: taskType = issue?.kind !== undefined ? $taskTypeStore.get(issue?.kind) : undefined
|
||||
|
||||
$: projectType = taskType?.parent !== undefined ? $typeStore.get(taskType.parent) : undefined
|
||||
|
||||
async function unsetParentIssue (): Promise<void> {
|
||||
if (issue === undefined || readonly) return
|
||||
|
||||
await client.update(issue, { attachedTo: tracker.ids.NoParent })
|
||||
Analytics.handleEvent(TrackerEvents.IssueParentUnset, { issue: issue.identifier ?? issue._id })
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if !embedded}
|
||||
@@ -276,8 +283,22 @@
|
||||
</svelte:fragment>
|
||||
|
||||
{#if hasParentIssue}
|
||||
<div class="mb-6">
|
||||
<div class="mb-6 flex-row-center">
|
||||
<SubIssueSelector {issue} />
|
||||
{#if !readonly}
|
||||
<div class="ml-2">
|
||||
<Button
|
||||
icon={tracker.icon.UnsetParent}
|
||||
iconProps={{ size: 'medium' }}
|
||||
kind={'regular'}
|
||||
showTooltip={{ label: tracker.string.UnsetParentIssue }}
|
||||
dataId={'btnUnsetParent'}
|
||||
on:click={() => {
|
||||
void unsetParentIssue()
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
<EditBox
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
import { type StatusCategory, type Client, type Doc, type Ref, type Space } from '@hcengineering/core'
|
||||
import type { Asset, IntlString, Metadata, Resource } from '@hcengineering/platform'
|
||||
import { mergeIds } from '@hcengineering/platform'
|
||||
import type { ObjectSearchCategory, ObjectSearchFactory } from '@hcengineering/presentation'
|
||||
import { type ProjectType } from '@hcengineering/task'
|
||||
import tracker, { trackerId, type IssueDraft, type Issue } from '@hcengineering/tracker'
|
||||
import { type AnyComponent, type Location } from '@hcengineering/ui'
|
||||
@@ -37,6 +38,10 @@ export default mergeIds(trackerId, tracker, {
|
||||
MilestoneIssuesList: '' as Ref<Viewlet>,
|
||||
ComponentIssuesList: '' as Ref<Viewlet>
|
||||
},
|
||||
completion: {
|
||||
IssueQuery: '' as Resource<ObjectSearchFactory>,
|
||||
IssueCategory: '' as Ref<ObjectSearchCategory>
|
||||
},
|
||||
string: {
|
||||
More: '' as IntlString,
|
||||
Delete: '' as IntlString,
|
||||
|
||||
@@ -22,5 +22,7 @@ export enum TrackerEvents {
|
||||
|
||||
ProjectCreated = 'tracker.project.Created',
|
||||
ProjectDeleted = 'tracker.project.Deleted',
|
||||
ProjectArchived = 'tracker.project.Archived'
|
||||
ProjectArchived = 'tracker.project.Archived',
|
||||
|
||||
IssueParentUnset = 'tracker.issue.ParentUnset'
|
||||
}
|
||||
|
||||
@@ -432,6 +432,7 @@ const pluginState = plugin(trackerId, {
|
||||
Labels: '' as Asset,
|
||||
DueDate: '' as Asset,
|
||||
Parent: '' as Asset,
|
||||
UnsetParent: '' as Asset,
|
||||
Milestone: '' as Asset,
|
||||
IssueTemplates: '' as Asset,
|
||||
Start: '' as Asset,
|
||||
@@ -495,7 +496,8 @@ const pluginState = plugin(trackerId, {
|
||||
EditProject: '' as Ref<Action>,
|
||||
SetMilestone: '' as Ref<Action<Doc, any>>,
|
||||
SetLabels: '' as Ref<Action<Doc, any>>,
|
||||
EditRelatedTargets: '' as Ref<Action<Doc, any>>
|
||||
EditRelatedTargets: '' as Ref<Action<Doc, any>>,
|
||||
UnsetParent: '' as Ref<Action<Doc, any>>
|
||||
},
|
||||
project: {
|
||||
DefaultProject: '' as Ref<Project>
|
||||
@@ -516,7 +518,8 @@ const pluginState = plugin(trackerId, {
|
||||
Project: '' as IntlString,
|
||||
RelatedIssues: '' as IntlString,
|
||||
Issue: '' as IntlString,
|
||||
NewProject: '' as IntlString
|
||||
NewProject: '' as IntlString,
|
||||
UnsetParentIssue: '' as IntlString
|
||||
},
|
||||
extensions: {
|
||||
IssueListHeader: '' as ComponentExtensionId,
|
||||
|
||||
Reference in New Issue
Block a user