mirror of
https://github.com/hcengineering/platform.git
synced 2025-04-25 01:39:53 +00:00
Tracker: parent issues name (#2136)
Signed-off-by: Sergei Ogorelkov <sergei.ogorelkov@xored.com>
This commit is contained in:
parent
a74ae0ee89
commit
6f0bd0a6fe
@ -10,9 +10,10 @@ Tracker:
|
||||
|
||||
- Remember view options
|
||||
- My issues
|
||||
- Names of parent issues
|
||||
- Roadmap
|
||||
- Remember view options
|
||||
- Context menus (Priority/Status/Assignee)
|
||||
-
|
||||
|
||||
Chunter:
|
||||
|
||||
|
@ -20,6 +20,6 @@ import serverTracker from '@anticrm/server-tracker'
|
||||
|
||||
export function createModel (builder: Builder): void {
|
||||
builder.createDoc(serverCore.class.Trigger, core.space.Model, {
|
||||
trigger: serverTracker.trigger.OnIssueProjectUpdate
|
||||
trigger: serverTracker.trigger.OnIssueUpdate
|
||||
})
|
||||
}
|
||||
|
@ -43,6 +43,7 @@ import task from '@anticrm/task'
|
||||
import {
|
||||
Document,
|
||||
Issue,
|
||||
IssueParentInfo,
|
||||
IssuePriority,
|
||||
IssueStatus,
|
||||
IssueStatusCategory,
|
||||
@ -180,6 +181,8 @@ export class TIssue extends TAttachedDoc implements Issue {
|
||||
@Prop(ArrOf(TypeRef(tracker.class.Issue)), tracker.string.RelatedTo)
|
||||
relatedIssue!: Ref<Issue>[]
|
||||
|
||||
parents!: IssueParentInfo[]
|
||||
|
||||
@Prop(Collection(chunter.class.Comment), tracker.string.Comments)
|
||||
comments!: number
|
||||
|
||||
|
@ -225,6 +225,36 @@ async function migrateParentIssues (client: MigrationClient): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
async function updateIssueParentInfo (client: MigrationClient, parentIssue: Issue | null): Promise<void> {
|
||||
const parents =
|
||||
parentIssue === null ? [] : [{ parentId: parentIssue._id, parentTitle: parentIssue.title }, ...parentIssue.parents]
|
||||
const migrationResult = await client.update<Issue>(
|
||||
DOMAIN_TRACKER,
|
||||
{
|
||||
_class: tracker.class.Issue,
|
||||
attachedTo: parentIssue?._id ?? tracker.ids.NoParent,
|
||||
parents: { $exists: false }
|
||||
},
|
||||
{ parents }
|
||||
)
|
||||
|
||||
if (migrationResult.matched > 0) {
|
||||
const subIssues = await client.find<Issue>(DOMAIN_TRACKER, {
|
||||
_class: tracker.class.Issue,
|
||||
attachedTo: parentIssue?._id ?? tracker.ids.NoParent,
|
||||
subIssues: { $gt: 0 }
|
||||
})
|
||||
|
||||
for (const issue of subIssues) {
|
||||
await updateIssueParentInfo(client, issue)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function migrateIssueParentInfo (client: MigrationClient): Promise<void> {
|
||||
await updateIssueParentInfo(client, null)
|
||||
}
|
||||
|
||||
async function migrateIssueProjects (client: MigrationClient): Promise<void> {
|
||||
const issues = await client.find(DOMAIN_TRACKER, { _class: tracker.class.Issue, project: { $exists: false } })
|
||||
|
||||
@ -288,6 +318,7 @@ async function upgradeProjects (tx: TxOperations): Promise<void> {
|
||||
export const trackerOperation: MigrateOperation = {
|
||||
async migrate (client: MigrationClient): Promise<void> {
|
||||
await Promise.all([migrateIssueProjects(client), migrateParentIssues(client)])
|
||||
await migrateIssueParentInfo(client)
|
||||
},
|
||||
async upgrade (client: MigrationUpgradeClient): Promise<void> {
|
||||
const tx = new TxOperations(client, core.account.System)
|
||||
|
@ -64,7 +64,8 @@
|
||||
priority: priority,
|
||||
dueDate: null,
|
||||
comments: 0,
|
||||
subIssues: 0
|
||||
subIssues: 0,
|
||||
parents: []
|
||||
}
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
@ -143,7 +144,10 @@
|
||||
rank: calcRank(lastOne, undefined),
|
||||
comments: 0,
|
||||
subIssues: 0,
|
||||
dueDate: object.dueDate
|
||||
dueDate: object.dueDate,
|
||||
parents: parentIssue
|
||||
? [{ parentId: parentIssue._id, parentTitle: parentIssue.title }, ...parentIssue.parents]
|
||||
: []
|
||||
}
|
||||
|
||||
await client.addCollection(
|
||||
|
@ -13,7 +13,7 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { AttachedData, FindOptions, SortingOrder } from '@anticrm/core'
|
||||
import { AttachedData, FindOptions, Ref, SortingOrder } from '@anticrm/core'
|
||||
import { getClient, ObjectPopup } from '@anticrm/presentation'
|
||||
import { calcRank, Issue, IssueStatusCategory } from '@anticrm/tracker'
|
||||
import { Icon } from '@anticrm/ui'
|
||||
@ -25,7 +25,6 @@
|
||||
export let width: 'medium' | 'large' | 'full' = 'large'
|
||||
|
||||
const client = getClient()
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
const options: FindOptions<Issue> = {
|
||||
lookup: { status: tracker.class.IssueStatus, space: tracker.class.Team },
|
||||
@ -43,7 +42,12 @@
|
||||
async function onClose ({ detail: parentIssue }: CustomEvent<Issue | undefined | null>) {
|
||||
const vv = Array.isArray(value) ? value : [value]
|
||||
for (const docValue of vv) {
|
||||
if ('_id' in docValue && parentIssue !== undefined && parentIssue?._id !== docValue.attachedTo) {
|
||||
if (
|
||||
'_id' in docValue &&
|
||||
parentIssue !== undefined &&
|
||||
parentIssue?._id !== docValue.attachedTo &&
|
||||
parentIssue?._id !== docValue._id
|
||||
) {
|
||||
let rank: string | null = null
|
||||
|
||||
if (parentIssue) {
|
||||
@ -68,12 +72,24 @@
|
||||
|
||||
$: 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)
|
||||
)
|
||||
]
|
||||
}
|
||||
}
|
||||
$: updateIssueStatusCategories()
|
||||
</script>
|
||||
|
||||
<ObjectPopup
|
||||
_class={tracker.class.Issue}
|
||||
{options}
|
||||
{docQuery}
|
||||
{selected}
|
||||
multiSelect={false}
|
||||
allowDeselect={true}
|
||||
|
@ -31,6 +31,7 @@
|
||||
import AssigneePresenter from './AssigneePresenter.svelte'
|
||||
import SubIssuesSelector from './edit/SubIssuesSelector.svelte'
|
||||
import IssuePresenter from './IssuePresenter.svelte'
|
||||
import ParentNamesPresenter from './ParentNamesPresenter.svelte'
|
||||
import PriorityEditor from './PriorityEditor.svelte'
|
||||
|
||||
export let currentSpace: Ref<Team> = tracker.team.DefaultTeam
|
||||
@ -172,8 +173,11 @@
|
||||
showPanel(tracker.component.EditIssue, object._id, object._class, 'content')
|
||||
}}
|
||||
>
|
||||
<div class="flex-col mr-6">
|
||||
<div class="flex-col mr-8">
|
||||
<div class="flex clear-mins names">
|
||||
<IssuePresenter value={issue} />
|
||||
<ParentNamesPresenter value={issue} />
|
||||
</div>
|
||||
<span class="fs-bold caption-color mt-1 lines-limit-2">
|
||||
{object.title}
|
||||
</span>
|
||||
@ -210,6 +214,10 @@
|
||||
{/await}
|
||||
|
||||
<style lang="scss">
|
||||
.names {
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
|
||||
.header {
|
||||
padding-bottom: 0.75rem;
|
||||
border-bottom: 1px solid var(--divider-color);
|
||||
|
@ -0,0 +1,48 @@
|
||||
<!--
|
||||
// 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 type { Issue } from '@anticrm/tracker'
|
||||
|
||||
export let value: Issue | undefined
|
||||
</script>
|
||||
|
||||
{#if value}
|
||||
<div class="root">
|
||||
<span class="names">
|
||||
{#each value.parents as parentInfo}
|
||||
<span class="name">{parentInfo.parentTitle}</span>
|
||||
{/each}
|
||||
</span>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style lang="scss">
|
||||
.root {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
|
||||
.names {
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
color: var(--content-color);
|
||||
}
|
||||
|
||||
.name::before {
|
||||
content: '›';
|
||||
padding: 0 0.25rem;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -14,25 +14,29 @@
|
||||
-->
|
||||
<script lang="ts">
|
||||
import type { Issue } from '@anticrm/tracker'
|
||||
import ParentNamesPresenter from './ParentNamesPresenter.svelte'
|
||||
|
||||
export let value: Issue
|
||||
export let shouldUseMargin: boolean = false
|
||||
</script>
|
||||
|
||||
{#if value}
|
||||
<span class="titleLabel flex-grow" class:mTitleLabelWithMargin={shouldUseMargin} title={value.title}
|
||||
>{value.title}</span
|
||||
>
|
||||
<span class="root" class:with-margin={shouldUseMargin} title={value.title}>
|
||||
<span class="name">{value.title}</span>
|
||||
<ParentNamesPresenter {value} />
|
||||
</span>
|
||||
{/if}
|
||||
|
||||
<style lang="scss">
|
||||
.titleLabel {
|
||||
flex-shrink: 10;
|
||||
overflow: hidden;
|
||||
.root {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
min-width: 0;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
flex-shrink: 10;
|
||||
|
||||
&.mTitleLabelWithMargin {
|
||||
&.with-margin {
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
@ -54,7 +54,8 @@
|
||||
priority: IssuePriority.NoPriority,
|
||||
dueDate: null,
|
||||
comments: 0,
|
||||
subIssues: 0
|
||||
subIssues: 0,
|
||||
parents: []
|
||||
}
|
||||
}
|
||||
|
||||
@ -94,7 +95,8 @@
|
||||
...newIssue,
|
||||
title: getTitle(newIssue.title),
|
||||
number: (incResult as any).object.sequence,
|
||||
rank: calcRank(lastOne, undefined)
|
||||
rank: calcRank(lastOne, undefined),
|
||||
parents: [{ parentId: parentIssue._id, parentTitle: parentIssue.title }, ...parentIssue.parents]
|
||||
}
|
||||
|
||||
const objectId = await client.addCollection(
|
||||
|
@ -112,6 +112,7 @@ export interface Issue extends AttachedDoc {
|
||||
subIssues: number
|
||||
blockedBy?: Ref<Issue>[]
|
||||
relatedIssue?: Ref<Issue>[]
|
||||
parents: IssueParentInfo[]
|
||||
|
||||
comments: number
|
||||
attachments?: number
|
||||
@ -124,6 +125,14 @@ export interface Issue extends AttachedDoc {
|
||||
rank: string
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export interface IssueParentInfo {
|
||||
parentId: Ref<Issue>
|
||||
parentTitle: string
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
|
@ -13,37 +13,27 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import core, { AttachedData, Tx, TxUpdateDoc } from '@anticrm/core'
|
||||
import core, { DocumentUpdate, Ref, Tx, TxUpdateDoc } from '@anticrm/core'
|
||||
import { extractTx, TriggerControl } from '@anticrm/server-core'
|
||||
import tracker, { Issue } from '@anticrm/tracker'
|
||||
|
||||
async function updateSubIssues (
|
||||
updateTx: TxUpdateDoc<Issue>,
|
||||
control: TriggerControl,
|
||||
node: Issue,
|
||||
update: Partial<AttachedData<Issue>>,
|
||||
shouldSkip = false
|
||||
update: DocumentUpdate<Issue> | ((node: Issue) => DocumentUpdate<Issue>)
|
||||
): Promise<TxUpdateDoc<Issue>[]> {
|
||||
let txes: TxUpdateDoc<Issue>[] = []
|
||||
const subIssues = await control.findAll(tracker.class.Issue, { 'parents.parentId': updateTx.objectId })
|
||||
|
||||
if (!shouldSkip && Object.entries(update).some(([key, value]) => value !== node[key as keyof Issue])) {
|
||||
txes.push(control.txFactory.createTxUpdateDoc(node._class, node.space, node._id, update))
|
||||
}
|
||||
|
||||
if (node.subIssues > 0) {
|
||||
const subIssues = await control.findAll(tracker.class.Issue, { attachedTo: node._id })
|
||||
|
||||
for (const subIssue of subIssues) {
|
||||
txes = txes.concat(await updateSubIssues(control, subIssue, update))
|
||||
}
|
||||
}
|
||||
|
||||
return txes
|
||||
return subIssues.map((issue) => {
|
||||
const docUpdate = typeof update === 'function' ? update(issue) : update
|
||||
return control.txFactory.createTxUpdateDoc(issue._class, issue.space, issue._id, docUpdate)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export async function OnIssueProjectUpdate (tx: Tx, control: TriggerControl): Promise<Tx[]> {
|
||||
export async function OnIssueUpdate (tx: Tx, control: TriggerControl): Promise<Tx[]> {
|
||||
const actualTx = extractTx(tx)
|
||||
if (actualTx._class !== core.class.TxUpdateDoc) {
|
||||
return []
|
||||
@ -54,22 +44,56 @@ export async function OnIssueProjectUpdate (tx: Tx, control: TriggerControl): Pr
|
||||
return []
|
||||
}
|
||||
|
||||
if (!Object.prototype.hasOwnProperty.call(updateTx.operations, 'project')) {
|
||||
return []
|
||||
}
|
||||
const res: Tx[] = []
|
||||
|
||||
const update: Partial<AttachedData<Issue>> = { project: updateTx.operations.project ?? null }
|
||||
const [node] = await control.findAll(
|
||||
updateTx.objectClass,
|
||||
{ _id: updateTx.objectId, subIssues: { $gt: 0 } },
|
||||
if (Object.prototype.hasOwnProperty.call(updateTx.operations, 'attachedTo')) {
|
||||
const [newParent] = await control.findAll(
|
||||
tracker.class.Issue,
|
||||
{ _id: updateTx.operations.attachedTo as Ref<Issue> },
|
||||
{ limit: 1 }
|
||||
)
|
||||
return node !== undefined ? await updateSubIssues(control, node, update, true) : []
|
||||
const updatedParents =
|
||||
newParent !== undefined ? [{ parentId: newParent._id, parentTitle: newParent.title }, ...newParent.parents] : []
|
||||
|
||||
function update (issue: Issue): DocumentUpdate<Issue> {
|
||||
const parentInfoIndex = issue.parents.findIndex(({ parentId }) => parentId === updateTx.objectId)
|
||||
return parentInfoIndex === -1
|
||||
? {}
|
||||
: { parents: [...issue.parents].slice(0, parentInfoIndex + 1).concat(updatedParents) }
|
||||
}
|
||||
|
||||
res.push(
|
||||
control.txFactory.createTxUpdateDoc(updateTx.objectClass, updateTx.objectSpace, updateTx.objectId, {
|
||||
parents: updatedParents
|
||||
}),
|
||||
...(await updateSubIssues(updateTx, control, update))
|
||||
)
|
||||
}
|
||||
|
||||
if (Object.prototype.hasOwnProperty.call(updateTx.operations, 'project')) {
|
||||
res.push(...(await updateSubIssues(updateTx, control, { project: updateTx.operations.project })))
|
||||
}
|
||||
|
||||
if (Object.prototype.hasOwnProperty.call(updateTx.operations, 'title')) {
|
||||
function update (issue: Issue): DocumentUpdate<Issue> {
|
||||
const parentInfoIndex = issue.parents.findIndex(({ parentId }) => parentId === updateTx.objectId)
|
||||
const updatedParentInfo = { ...issue.parents[parentInfoIndex], parentTitle: updateTx.operations.title as string }
|
||||
const updatedParents = [...issue.parents]
|
||||
|
||||
updatedParents[parentInfoIndex] = updatedParentInfo
|
||||
|
||||
return { parents: updatedParents }
|
||||
}
|
||||
|
||||
res.push(...(await updateSubIssues(updateTx, control, update)))
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
|
||||
export default async () => ({
|
||||
trigger: {
|
||||
OnIssueProjectUpdate
|
||||
OnIssueUpdate
|
||||
}
|
||||
})
|
||||
|
26
server-plugins/tracker-resources/src/utils.ts
Normal file
26
server-plugins/tracker-resources/src/utils.ts
Normal file
@ -0,0 +1,26 @@
|
||||
//
|
||||
// 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.
|
||||
//
|
||||
|
||||
export function arrayEquals<T> (first: Array<T>, second: Array<T>): boolean {
|
||||
if (first === second) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (first.length !== second.length) {
|
||||
return false
|
||||
}
|
||||
|
||||
return first.every((val, index) => val === second[index])
|
||||
}
|
@ -27,6 +27,6 @@ export const serverTrackerId = 'server-tracker' as Plugin
|
||||
*/
|
||||
export default plugin(serverTrackerId, {
|
||||
trigger: {
|
||||
OnIssueProjectUpdate: '' as Resource<TriggerFunc>
|
||||
OnIssueUpdate: '' as Resource<TriggerFunc>
|
||||
}
|
||||
})
|
||||
|
Loading…
Reference in New Issue
Block a user