UBERF-9264: Fix set/unset parent issue (#7799)

Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
Andrey Sobolev
2025-01-27 15:14:54 +05:00
committed by GitHub
parent 83a912d716
commit 331aa7f78f
20 changed files with 133 additions and 51 deletions
+24
View File
@@ -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,
+1 -6
View File
@@ -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
+12 -7
View File
@@ -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 }
}
+5
View File
@@ -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

+2 -1
View File
@@ -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": {}
}
+2 -1
View File
@@ -286,7 +286,8 @@
"DefaultIssueStatus": "Standard-Aufgabenstatus",
"IssueStatus": "Status",
"Extensions": "Erweiterungen",
"RoleLabel": "Rolle: {role}"
"RoleLabel": "Rolle: {role}",
"UnsetParentIssue": "Übergeordnete Aufgabe entfernen"
},
"status": {}
}
+2 -1
View File
@@ -286,7 +286,8 @@
"DefaultIssueStatus": "Default issue status",
"IssueStatus": "Status",
"Extensions": "Extensions",
"RoleLabel": "Role: {role}"
"RoleLabel": "Role: {role}",
"UnsetParentIssue": "Unset parent issue"
},
"status": {}
}
+2 -1
View File
@@ -269,7 +269,8 @@
"DefaultIssueStatus": "Estado de problema predeterminado",
"IssueStatus": "Estado",
"Extensions": "Extensions",
"RoleLabel": "Role: {role}"
"RoleLabel": "Role: {role}",
"UnsetParentIssue": "Unset parent issue"
},
"status": {}
}
+2 -1
View File
@@ -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": {}
}
+2 -1
View File
@@ -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": {}
}
+2 -1
View File
@@ -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": {}
}
+2 -1
View File
@@ -286,7 +286,8 @@
"DefaultIssueStatus": "Статус по умолчанию",
"IssueStatus": "Статус",
"Extensions": "Дополнительно",
"RoleLabel": "Роль: {role}"
"RoleLabel": "Роль: {role}",
"UnsetParentIssue": "Снять родительскую задачу"
},
"status": {}
}
+2 -1
View File
@@ -286,7 +286,8 @@
"DefaultIssueStatus": "默认问题状态",
"IssueStatus": "状态",
"Extensions": "扩展",
"RoleLabel": "角色:{role}"
"RoleLabel": "角色:{role}",
"UnsetParentIssue": "取消父问题"
},
"status": {}
}
+1
View File
@@ -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
+5
View File
@@ -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,
+3 -1
View File
@@ -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'
}
+5 -2
View File
@@ -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,