UBERF-5339 (#4566)

Signed-off-by: Denis Bykhov <bykhov.denis@gmail.com>
This commit is contained in:
Denis Bykhov 2024-02-07 21:27:24 +06:00 committed by GitHub
parent 7b61f3d652
commit b1268d6c17
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
48 changed files with 295 additions and 328 deletions

View File

@ -11,7 +11,7 @@ import core, {
TxOperations, TxOperations,
WorkspaceId WorkspaceId
} from '@hcengineering/core' } from '@hcengineering/core'
import tracker, { Issue, IssuePriority, IssueStatus } from '@hcengineering/tracker' import tracker, { Issue, IssuePriority, IssueStatus, Project } from '@hcengineering/tracker'
import { connect } from './connect' import { connect } from './connect'
import { calcRank } from '@hcengineering/task' import { calcRank } from '@hcengineering/task'
@ -38,6 +38,7 @@ const object: AttachedData<Issue> = {
estimation: 0, estimation: 0,
reports: 0, reports: 0,
childInfo: [], childInfo: [],
identifier: '',
kind: tracker.taskTypes.Issue kind: tracker.taskTypes.Issue
} }
@ -82,13 +83,15 @@ async function genIssue (client: TxOperations, statuses: Ref<IssueStatus>[]): Pr
}, },
true true
) )
const project = (incResult as any).object as Project
const number = project.sequence
const value: AttachedData<Issue> = { const value: AttachedData<Issue> = {
title: faker.commerce.productName(), title: faker.commerce.productName(),
description: faker.lorem.paragraphs(), description: faker.lorem.paragraphs(),
assignee: object.assignee, assignee: object.assignee,
component: object.component, component: object.component,
milestone: object.milestone, milestone: object.milestone,
number: (incResult as any).object.sequence, number,
status: faker.random.arrayElement(statuses), status: faker.random.arrayElement(statuses),
priority: faker.random.arrayElement(Object.values(IssuePriority)) as IssuePriority, priority: faker.random.arrayElement(Object.values(IssuePriority)) as IssuePriority,
rank: calcRank(lastOne, undefined), rank: calcRank(lastOne, undefined),
@ -102,6 +105,7 @@ async function genIssue (client: TxOperations, statuses: Ref<IssueStatus>[]): Pr
reports: 0, reports: 0,
relations: [], relations: [],
childInfo: [], childInfo: [],
identifier: `${project.identifier}-${number}`,
kind: tracker.taskTypes.Issue kind: tracker.taskTypes.Issue
} }
await client.addCollection( await client.addCollection(

View File

@ -178,14 +178,16 @@ async function genApplicant (
): Promise<void> { ): Promise<void> {
const applicantId = `vacancy-${vacancyId}-${candidateId}` as Ref<Applicant> const applicantId = `vacancy-${vacancyId}-${candidateId}` as Ref<Applicant>
const number = faker.datatype.number()
const applicant: AttachedData<Applicant> = { const applicant: AttachedData<Applicant> = {
number: faker.datatype.number(), number,
assignee: faker.random.arrayElement(emoloyeeIds), assignee: faker.random.arrayElement(emoloyeeIds),
status: faker.random.arrayElement(states), status: faker.random.arrayElement(states),
rank, rank,
startDate: null, startDate: null,
dueDate: null, dueDate: null,
kind: recruit.taskTypes.Applicant kind: recruit.taskTypes.Applicant,
identifier: `APP-${number}`
} }
// Update or create candidate // Update or create candidate

View File

@ -85,7 +85,7 @@ export class TCommonBoardPreference extends TPreference implements CommonBoardPr
} }
@Model(board.class.Card, task.class.Task) @Model(board.class.Card, task.class.Task)
@UX(board.string.Card, board.icon.Card, undefined, 'title') @UX(board.string.Card, board.icon.Card, 'CARD', 'title')
export class TCard extends TTask implements Card { export class TCard extends TTask implements Card {
@Prop(TypeString(), board.string.Title) @Prop(TypeString(), board.string.Title)
@Index(IndexKind.FullText) @Index(IndexKind.FullText)

View File

@ -13,7 +13,7 @@
// limitations under the License. // limitations under the License.
// //
import { boardId } from '@hcengineering/board' import { type Card, boardId } from '@hcengineering/board'
import { type Ref, TxOperations } from '@hcengineering/core' import { type Ref, TxOperations } from '@hcengineering/core'
import { import {
type MigrateOperation, type MigrateOperation,
@ -23,7 +23,7 @@ import {
tryMigrate tryMigrate
} from '@hcengineering/model' } from '@hcengineering/model'
import core, { DOMAIN_SPACE } from '@hcengineering/model-core' import core, { DOMAIN_SPACE } from '@hcengineering/model-core'
import { createProjectType, createSequence, fixTaskTypes } from '@hcengineering/model-task' import { DOMAIN_TASK, createProjectType, createSequence, fixTaskTypes } from '@hcengineering/model-task'
import tags from '@hcengineering/tags' import tags from '@hcengineering/tags'
import task, { type ProjectType } from '@hcengineering/task' import task, { type ProjectType } from '@hcengineering/task'
import { PaletteColorIndexes } from '@hcengineering/ui/src/colors' import { PaletteColorIndexes } from '@hcengineering/ui/src/colors'
@ -126,6 +126,19 @@ async function createDefaults (tx: TxOperations): Promise<void> {
) )
} }
async function migrateIdentifiers (client: MigrationClient): Promise<void> {
const docs = await client.find<Card>(DOMAIN_TASK, { _class: board.class.Card, identifier: { $exists: false } })
for (const doc of docs) {
await client.update(
DOMAIN_TASK,
{ _id: doc._id },
{
identifier: `CARD-${doc.number}`
}
)
}
}
export const boardOperation: MigrateOperation = { export const boardOperation: MigrateOperation = {
async migrate (client: MigrationClient): Promise<void> { async migrate (client: MigrationClient): Promise<void> {
await tryMigrate(client, boardId, [ await tryMigrate(client, boardId, [
@ -162,6 +175,10 @@ export const boardOperation: MigrateOperation = {
} }
]) ])
} }
},
{
state: 'identifier',
func: migrateIdentifiers
} }
]) ])
}, },

View File

@ -67,7 +67,7 @@ export class TFunnel extends TProject implements Funnel {
} }
@Model(lead.class.Lead, task.class.Task) @Model(lead.class.Lead, task.class.Task)
@UX(lead.string.Lead, lead.icon.Lead, undefined, 'title') @UX(lead.string.Lead, lead.icon.Lead, 'LEAD', 'title')
export class TLead extends TTask implements Lead { export class TLead extends TTask implements Lead {
@Prop(TypeRef(contact.class.Contact), lead.string.Customer) @Prop(TypeRef(contact.class.Contact), lead.string.Customer)
@ReadOnly() @ReadOnly()

View File

@ -14,7 +14,7 @@
// //
import { TxOperations } from '@hcengineering/core' import { TxOperations } from '@hcengineering/core'
import { leadId } from '@hcengineering/lead' import { type Lead, leadId } from '@hcengineering/lead'
import { import {
tryMigrate, tryMigrate,
tryUpgrade, tryUpgrade,
@ -23,7 +23,7 @@ import {
type MigrationUpgradeClient type MigrationUpgradeClient
} from '@hcengineering/model' } from '@hcengineering/model'
import core, { DOMAIN_SPACE } from '@hcengineering/model-core' import core, { DOMAIN_SPACE } from '@hcengineering/model-core'
import task, { createProjectType, createSequence, fixTaskTypes } from '@hcengineering/model-task' import task, { DOMAIN_TASK, createProjectType, createSequence, fixTaskTypes } from '@hcengineering/model-task'
import { PaletteColorIndexes } from '@hcengineering/ui/src/colors' import { PaletteColorIndexes } from '@hcengineering/ui/src/colors'
import lead from './plugin' import lead from './plugin'
@ -121,6 +121,19 @@ async function createDefaults (tx: TxOperations): Promise<void> {
await createSequence(tx, lead.class.Lead) await createSequence(tx, lead.class.Lead)
} }
async function migrateIdentifiers (client: MigrationClient): Promise<void> {
const docs = await client.find<Lead>(DOMAIN_TASK, { _class: lead.class.Lead, identifier: { $exists: false } })
for (const doc of docs) {
await client.update(
DOMAIN_TASK,
{ _id: doc._id },
{
identifier: `LEAD-${doc.number}`
}
)
}
}
export const leadOperation: MigrateOperation = { export const leadOperation: MigrateOperation = {
async migrate (client: MigrationClient): Promise<void> { async migrate (client: MigrationClient): Promise<void> {
await tryMigrate(client, leadId, [ await tryMigrate(client, leadId, [
@ -157,6 +170,10 @@ export const leadOperation: MigrateOperation = {
} }
]) ])
} }
},
{
state: 'identifier',
func: migrateIdentifiers
} }
]) ])
}, },

View File

@ -24,8 +24,8 @@ import {
type MigrationUpgradeClient type MigrationUpgradeClient
} from '@hcengineering/model' } from '@hcengineering/model'
import tags, { type TagCategory } from '@hcengineering/model-tags' import tags, { type TagCategory } from '@hcengineering/model-tags'
import { createProjectType, createSequence, fixTaskTypes } from '@hcengineering/model-task' import { DOMAIN_TASK, createProjectType, createSequence, fixTaskTypes } from '@hcengineering/model-task'
import { recruitId } from '@hcengineering/recruit' import { type Applicant, recruitId } from '@hcengineering/recruit'
import task, { type ProjectType } from '@hcengineering/task' import task, { type ProjectType } from '@hcengineering/task'
import { PaletteColorIndexes } from '@hcengineering/ui/src/colors' import { PaletteColorIndexes } from '@hcengineering/ui/src/colors'
import recruit from './plugin' import recruit from './plugin'
@ -67,6 +67,10 @@ export const recruitOperation: MigrateOperation = {
} }
]) ])
} }
},
{
state: 'identifier',
func: migrateIdentifiers
} }
]) ])
}, },
@ -89,6 +93,22 @@ export const recruitOperation: MigrateOperation = {
} }
} }
async function migrateIdentifiers (client: MigrationClient): Promise<void> {
const docs = await client.find<Applicant>(DOMAIN_TASK, {
_class: recruit.class.Applicant,
identifier: { $exists: false }
})
for (const doc of docs) {
await client.update(
DOMAIN_TASK,
{ _id: doc._id },
{
identifier: `APP-${doc.number}`
}
)
}
}
async function createDefaults (tx: TxOperations): Promise<void> { async function createDefaults (tx: TxOperations): Promise<void> {
await createSpaces(tx) await createSpaces(tx)

View File

@ -60,10 +60,7 @@ export function createModel (builder: Builder): void {
component: contact.component.Avatar, component: contact.component.Avatar,
props: [{ avatar: ['attachedTo', 'avatar'] }, { name: ['attachedTo', 'name'] }] props: [{ avatar: ['attachedTo', 'avatar'] }, { name: ['attachedTo', 'name'] }]
}, },
shortTitle: { shortTitle: 'identifier',
tmpl: 'APP-{number}',
props: ['number']
},
title: { title: {
tmpl: '{name} - {vacName}', tmpl: '{name} - {vacName}',
props: [{ _class: ['attachedTo', '_class'] }, { name: ['attachedTo', 'name'] }, { vacName: ['space', 'name'] }] props: [{ _class: ['attachedTo', '_class'] }, { name: ['attachedTo', 'name'] }, { vacName: ['space', 'name'] }]

View File

@ -42,10 +42,7 @@ export function createModel (builder: Builder): void {
component: tracker.component.IssueSearchIcon, component: tracker.component.IssueSearchIcon,
props: ['status', 'space'] props: ['status', 'space']
}, },
shortTitle: { shortTitle: 'identifier',
tmpl: '{identifier}-{number}',
props: [{ identifier: ['space', 'identifier'] }, 'number']
},
title: 'title' title: 'title'
} }
}) })

View File

@ -133,7 +133,14 @@ export class TTask extends TAttachedDoc implements Task {
@Prop(Collection(attachment.class.Attachment), attachment.string.Attachments, { shortLabel: attachment.string.Files }) @Prop(Collection(attachment.class.Attachment), attachment.string.Attachments, { shortLabel: attachment.string.Files })
attachments?: number attachments?: number
isDone?: boolean @Prop(TypeBoolean(), getEmbeddedLabel('isDone'))
@Hidden()
isDone?: boolean
@Prop(TypeString(), task.string.Identifier)
@ReadOnly()
@Index(IndexKind.Indexed)
identifier!: string
} }
@Mixin(task.mixin.KanbanCard, core.class.Class) @Mixin(task.mixin.KanbanCard, core.class.Class)

View File

@ -42,7 +42,9 @@ import {
baseIssueTaskStatuses, baseIssueTaskStatuses,
classicIssueTaskStatuses, classicIssueTaskStatuses,
createStatesData, createStatesData,
type IssueStatus type IssueStatus,
type Issue,
type Project
} from '@hcengineering/tracker' } from '@hcengineering/tracker'
import { PaletteColorIndexes } from '@hcengineering/ui/src/colors' import { PaletteColorIndexes } from '@hcengineering/ui/src/colors'
import tracker from './plugin' import tracker from './plugin'
@ -333,6 +335,20 @@ async function restoreToDoCategory (client: MigrationClient): Promise<void> {
} }
} }
async function migrateIdentifiers (client: MigrationClient): Promise<void> {
const classes = client.hierarchy.getDescendants(tracker.class.Issue)
const issues = await client.find<Issue>(DOMAIN_TASK, { _class: { $in: classes }, identifier: { $exists: false } })
if (issues.length === 0) return
const projects = await client.find<Project>(DOMAIN_SPACE, { _class: tracker.class.Project })
const projectsMap = toIdMap(projects)
for (const issue of issues) {
const project = projectsMap.get(issue.space)
if (project === undefined) continue
const identifier = project.identifier + '-' + issue.number
await client.update(DOMAIN_TASK, { _id: issue._id }, { $set: { identifier } })
}
}
export const trackerOperation: MigrateOperation = { export const trackerOperation: MigrateOperation = {
async migrate (client: MigrationClient): Promise<void> { async migrate (client: MigrationClient): Promise<void> {
await tryMigrate(client, 'tracker', [ await tryMigrate(client, 'tracker', [
@ -464,6 +480,10 @@ export const trackerOperation: MigrateOperation = {
{ {
state: 'restoreToDoCategory', state: 'restoreToDoCategory',
func: restoreToDoCategory func: restoreToDoCategory
},
{
state: 'identifier',
func: migrateIdentifiers
} }
]) ])
}, },

View File

@ -17,11 +17,12 @@
import type { Board, Card as BoardCard } from '@hcengineering/board' import type { Board, Card as BoardCard } from '@hcengineering/board'
import core, { AttachedData, Ref, SortingOrder, Space, generateId } from '@hcengineering/core' import core, { AttachedData, Ref, SortingOrder, Space, generateId } from '@hcengineering/core'
import { OK, Status } from '@hcengineering/platform' import { OK, Status } from '@hcengineering/platform'
import { Card, SpaceSelector, getClient } from '@hcengineering/presentation' import { Card, SpaceSelector, createQuery, getClient } from '@hcengineering/presentation'
import task, { calcRank } from '@hcengineering/task' import task, { TaskType, calcRank } from '@hcengineering/task'
import { EditBox, Grid, Status as StatusControl } from '@hcengineering/ui' import { EditBox, Grid, Status as StatusControl } from '@hcengineering/ui'
import { createEventDispatcher } from 'svelte' import { createEventDispatcher } from 'svelte'
import board from '../plugin' import board from '../plugin'
import { TaskKindSelector } from '@hcengineering/task-resources'
export let space: Ref<Space> export let space: Ref<Space>
@ -32,12 +33,14 @@
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
const client = getClient() const client = getClient()
const cardId = generateId() const cardId = generateId<BoardCard>()
export function canClose (): boolean { export function canClose (): boolean {
return title !== '' return title !== ''
} }
let kind: Ref<TaskType> | undefined = undefined
async function createCard () { async function createCard () {
const sp = await client.findOne(board.class.Board, { _id: _space as Ref<Board> }) const sp = await client.findOne(board.class.Board, { _id: _space as Ref<Board> })
if (sp === undefined) { if (sp === undefined) {
@ -51,6 +54,9 @@
if (status === undefined) { if (status === undefined) {
throw new Error('Status not found') throw new Error('Status not found')
} }
if (kind === undefined) {
throw new Error('kind is not specified')
}
const sequence = await client.findOne(task.class.Sequence, { attachedTo: board.class.Card }) const sequence = await client.findOne(task.class.Sequence, { attachedTo: board.class.Card })
if (sequence === undefined) { if (sequence === undefined) {
throw new Error('sequence object not found') throw new Error('sequence object not found')
@ -59,10 +65,14 @@
const lastOne = await client.findOne(board.class.Card, {}, { sort: { rank: SortingOrder.Descending } }) const lastOne = await client.findOne(board.class.Card, {}, { sort: { rank: SortingOrder.Descending } })
const incResult = await client.update(sequence, { $inc: { sequence: 1 } }, true) const incResult = await client.update(sequence, { $inc: { sequence: 1 } }, true)
const number = (incResult as any).object.sequence
const value: AttachedData<BoardCard> = { const value: AttachedData<BoardCard> = {
status: status._id, status: status._id,
number: (incResult as any).object.sequence, number,
title, title,
kind,
identifier: `CARD-${number}`,
rank: calcRank(lastOne, undefined), rank: calcRank(lastOne, undefined),
assignee: null, assignee: null,
description: '', description: '',
@ -75,6 +85,14 @@
await client.addCollection(board.class.Card, _space, _space, board.class.Board, 'cards', value, cardId) await client.addCollection(board.class.Card, _space, _space, board.class.Board, 'cards', value, cardId)
dispatch('close') dispatch('close')
} }
let boards: Board[] = []
const boardQuery = createQuery()
boardQuery.query(board.class.Board, {}, (res) => {
boards = res
})
$: currentBoard = boards.find((it) => it._id === _space)
</script> </script>
<Card <Card
@ -88,6 +106,7 @@
> >
<svelte:fragment slot="header"> <svelte:fragment slot="header">
<SpaceSelector _class={board.class.Board} label={board.string.BoardName} bind:space={_space} /> <SpaceSelector _class={board.class.Board} label={board.string.BoardName} bind:space={_space} />
<TaskKindSelector projectType={currentBoard?.type} bind:value={kind} baseClass={board.class.Card} />
</svelte:fragment> </svelte:fragment>
<StatusControl slot="error" {status} /> <StatusControl slot="error" {status} />
<Grid column={1} rowGap={1.5}> <Grid column={1} rowGap={1.5}>

View File

@ -52,7 +52,7 @@
const mixins: Mixin<Doc>[] = [] const mixins: Mixin<Doc>[] = []
const allowedCollections = ['labels'] const allowedCollections = ['labels']
const ignoreKeys = ['isArchived', 'location', 'title', 'description', 'status', 'number', 'assignee'] const ignoreKeys = ['isArchived', 'location', 'title', 'description', 'status', 'number', 'assignee', 'identifier']
function change (field: string, value: any) { function change (field: string, value: any) {
if (object) { if (object) {

View File

@ -27,15 +27,17 @@ export async function createCard (
const lastOne = await client.findOne(board.class.Card, {}, { sort: { rank: SortingOrder.Descending } }) const lastOne = await client.findOne(board.class.Card, {}, { sort: { rank: SortingOrder.Descending } })
const incResult = await client.update(sequence, { $inc: { sequence: 1 } }, true) const incResult = await client.update(sequence, { $inc: { sequence: 1 } }, true)
const number = (incResult as any).object.sequence
const value: AttachedData<Card> = { const value: AttachedData<Card> = {
title: '', title: '',
status, status,
startDate: null, startDate: null,
dueDate: null, dueDate: null,
number: (incResult as any).object.sequence, number,
rank: calcRank(lastOne, undefined), rank: calcRank(lastOne, undefined),
assignee: null, assignee: null,
identifier: `CARD-${number}`,
description: '', description: '',
...attribues ...attribues
} }

View File

@ -20,12 +20,12 @@
import type { Customer, Funnel, Lead } from '@hcengineering/lead' import type { Customer, Funnel, Lead } from '@hcengineering/lead'
import { OK, Status } from '@hcengineering/platform' import { OK, Status } from '@hcengineering/platform'
import { Card, createQuery, getClient, InlineAttributeBar, SpaceSelector } from '@hcengineering/presentation' import { Card, createQuery, getClient, InlineAttributeBar, SpaceSelector } from '@hcengineering/presentation'
import task, { calcRank, getStates } from '@hcengineering/task' import task, { calcRank, getStates, TaskType } from '@hcengineering/task'
import { TaskKindSelector, typeStore } from '@hcengineering/task-resources'
import { Button, createFocusManager, EditBox, FocusHandler, Label, Status as StatusControl } from '@hcengineering/ui' import { Button, createFocusManager, EditBox, FocusHandler, Label, Status as StatusControl } from '@hcengineering/ui'
import { statusStore } from '@hcengineering/view-resources' import { statusStore } from '@hcengineering/view-resources'
import { createEventDispatcher } from 'svelte' import { createEventDispatcher } from 'svelte'
import lead from '../plugin' import lead from '../plugin'
import { typeStore } from '@hcengineering/task-resources'
export let space: Ref<Funnel> export let space: Ref<Funnel>
export let customer: Ref<Contact> | null = null export let customer: Ref<Contact> | null = null
@ -71,19 +71,27 @@
$: funnel = funnels.find((it) => it._id === _space) $: funnel = funnels.find((it) => it._id === _space)
let kind: Ref<TaskType> | undefined = undefined
async function createLead () { async function createLead () {
const sequence = await client.findOne(task.class.Sequence, { attachedTo: lead.class.Lead }) const sequence = await client.findOne(task.class.Sequence, { attachedTo: lead.class.Lead })
if (sequence === undefined || customer == null) { if (sequence === undefined || customer == null) {
throw new Error('Lead creation failed') throw new Error('Lead creation failed')
} }
if (kind === undefined) {
throw new Error('kind is not specified')
}
const lastOne = await client.findOne(lead.class.Lead, {}, { sort: { rank: SortingOrder.Descending } }) const lastOne = await client.findOne(lead.class.Lead, {}, { sort: { rank: SortingOrder.Descending } })
const incResult = await client.update(sequence, { $inc: { sequence: 1 } }, true) const incResult = await client.update(sequence, { $inc: { sequence: 1 } }, true)
const number = (incResult as any).object.sequence
const value: AttachedData<Lead> = { const value: AttachedData<Lead> = {
status: state, status: state,
number: (incResult as any).object.sequence, number,
identifier: `LEAD-${number}`,
title, title,
kind,
rank: calcRank(lastOne, undefined), rank: calcRank(lastOne, undefined),
assignee: null, assignee: null,
startDate: null, startDate: null,
@ -137,6 +145,7 @@
label: lead.string.CreateFunnel label: lead.string.CreateFunnel
}} }}
/> />
<TaskKindSelector projectType={funnel?.type} bind:value={kind} baseClass={lead.class.Lead} />
</svelte:fragment> </svelte:fragment>
<svelte:fragment slot="title"> <svelte:fragment slot="title">
<div class="flex-row-center gap-2"> <div class="flex-row-center gap-2">

View File

@ -39,7 +39,7 @@
} }
onMount(() => { onMount(() => {
dispatch('open', { ignoreKeys: ['comments', 'number', 'title', 'customer'] }) dispatch('open', { ignoreKeys: ['comments', 'number', 'title', 'customer', 'identifier'] })
}) })
</script> </script>

View File

@ -25,7 +25,7 @@
import { ActionIcon, Component, DueDatePresenter, IconMoreH, showPopup } from '@hcengineering/ui' import { ActionIcon, Component, DueDatePresenter, IconMoreH, showPopup } from '@hcengineering/ui'
import { BuildModelKey } from '@hcengineering/view' import { BuildModelKey } from '@hcengineering/view'
import { ContextMenu, enabledConfig, openDoc, statusStore } from '@hcengineering/view-resources' import { ContextMenu, enabledConfig, openDoc, statusStore } from '@hcengineering/view-resources'
import { ChatMessagesPresenter } from '@hcengineering/notification-resources' import { ChatMessagesPresenter } from '@hcengineering/chunter-resources'
import task from '@hcengineering/task' import task from '@hcengineering/task'
import lead from '../plugin' import lead from '../plugin'

View File

@ -31,14 +31,14 @@
{#if value} {#if value}
<DocNavLink object={value} {inline} {disabled} {noUnderline} {accent}> <DocNavLink object={value} {inline} {disabled} {noUnderline} {accent}>
{#if inline} {#if inline}
<span class="antiMention" use:tooltip={{ label: lead.string.Lead }}>@LEAD-{value.number}</span> <span class="antiMention" use:tooltip={{ label: lead.string.Lead }}>@{value.identifier}</span>
{:else} {:else}
<div class="flex-presenter"> <div class="flex-presenter">
{#if shouldShowAvatar} {#if shouldShowAvatar}
<div class="icon"><Icon icon={lead.icon.Lead} size={'small'} /></div> <div class="icon"><Icon icon={lead.icon.Lead} size={'small'} /></div>
{/if} {/if}
<span class="label nowrap" class:no-underline={noUnderline || disabled} class:fs-bold={accent} <span class="label nowrap" class:no-underline={noUnderline || disabled} class:fs-bold={accent}
>LEAD-{value.number}</span >{value.identifier}</span
> >
</div> </div>
{/if} {/if}

View File

@ -89,6 +89,7 @@
attachedToClass: recruit.mixin.Candidate, attachedToClass: recruit.mixin.Candidate,
_class: recruit.class.Applicant, _class: recruit.class.Applicant,
space, space,
identifier: '',
_id: generateId(), _id: generateId(),
collection: 'applications', collection: 'applications',
modifiedOn: Date.now(), modifiedOn: Date.now(),
@ -137,6 +138,8 @@
) )
} }
const number = (incResult as any).object.sequence
await client.addCollection( await client.addCollection(
recruit.class.Applicant, recruit.class.Applicant,
_space, _space,
@ -146,7 +149,8 @@
{ {
...doc, ...doc,
status: selectedState._id, status: selectedState._id,
number: (incResult as any).object.sequence, number,
identifier: `APP-${number}`,
assignee: doc.assignee, assignee: doc.assignee,
rank: calcRank(lastOne, undefined), rank: calcRank(lastOne, undefined),
startDate: null, startDate: null,

View File

@ -31,11 +31,11 @@
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
const sendOpen = () => { const sendOpen = () => {
if (object?.number !== undefined) { if (object !== undefined) {
dispatch('open', { dispatch('open', {
ignoreKeys: ['comments', 'number'], ignoreKeys: ['comments', 'number', 'identifier'],
allowedCollections: ['labels'], allowedCollections: ['labels'],
title: `APP-${object.number}` title: object.identifier
}) })
} }
} }

View File

@ -1,16 +1,16 @@
import contact, { getName } from '@hcengineering/contact' import contact, { getName } from '@hcengineering/contact'
import { type Class, type Client, type Doc, Hierarchy, type Ref } from '@hcengineering/core' import { Hierarchy, type Class, type Client, type Doc, type Ref } from '@hcengineering/core'
import presentation, { getClient } from '@hcengineering/presentation'
import { getMetadata } from '@hcengineering/platform' import { getMetadata } from '@hcengineering/platform'
import presentation, { getClient } from '@hcengineering/presentation'
import { import {
recruitId,
type Applicant, type Applicant,
type Candidate, type Candidate,
type Review, type Review,
type Vacancy, type Vacancy,
type VacancyList, type VacancyList
recruitId
} from '@hcengineering/recruit' } from '@hcengineering/recruit'
import { type Location, type ResolvedLocation, getCurrentResolvedLocation, getPanelURI } from '@hcengineering/ui' import { getCurrentResolvedLocation, getPanelURI, type Location, type ResolvedLocation } from '@hcengineering/ui'
import view from '@hcengineering/view' import view from '@hcengineering/view'
import { workbenchId } from '@hcengineering/workbench' import { workbenchId } from '@hcengineering/workbench'
import recruit from './plugin' import recruit from './plugin'
@ -191,7 +191,7 @@ export async function getAppIdentifier (client: Client, ref: Ref<Applicant>, doc
return '' return ''
} }
return `APP-${applicant.number}` return applicant.identifier
} }
export async function getRevTitle (client: Client, ref: Ref<Review>): Promise<string> { export async function getRevTitle (client: Client, ref: Ref<Review>): Promise<string> {
@ -201,6 +201,9 @@ export async function getRevTitle (client: Client, ref: Ref<Review>): Promise<st
export async function getSequenceId (doc: RecruitDocument): Promise<string> { export async function getSequenceId (doc: RecruitDocument): Promise<string> {
const client = getClient() const client = getClient()
const hierarchy = client.getHierarchy() const hierarchy = client.getHierarchy()
if (hierarchy.isDerived(doc._class, recruit.class.Applicant)) {
return (doc as Applicant).identifier
}
let clazz = hierarchy.getClass(doc._class) let clazz = hierarchy.getClass(doc._class)
let label = clazz.shortLabel let label = clazz.shortLabel
while (label === undefined && clazz.extends !== undefined) { while (label === undefined && clazz.extends !== undefined) {

View File

@ -94,6 +94,7 @@
"ProcessStates": "Process states", "ProcessStates": "Process states",
"Type": "Type", "Type": "Type",
"Group": "Group", "Group": "Group",
"Color": "Color" "Color": "Color",
"Identifier": "Identifier"
} }
} }

View File

@ -94,6 +94,7 @@
"ProcessStates": "Состояния процесса", "ProcessStates": "Состояния процесса",
"Type": "Тип", "Type": "Тип",
"Group": "Группа", "Group": "Группа",
"Color": "Цвет" "Color": "Цвет",
"Identifier": "Идентификатор"
} }
} }

View File

@ -56,6 +56,7 @@ export interface Task extends AttachedDoc, DocWithRank {
comments?: number comments?: number
attachments?: number attachments?: number
labels?: number labels?: number
identifier: string
} }
/** /**
@ -244,7 +245,8 @@ const task = plugin(taskId, {
Dashboard: '' as IntlString, Dashboard: '' as IntlString,
ProjectTypes: '' as IntlString, ProjectTypes: '' as IntlString,
TaskType: '' as IntlString, TaskType: '' as IntlString,
ProjectType: '' as IntlString ProjectType: '' as IntlString,
Identifier: '' as IntlString
}, },
class: { class: {
Sequence: '' as Ref<Class<Sequence>>, Sequence: '' as Ref<Class<Sequence>>,

View File

@ -61,7 +61,7 @@
import view from '@hcengineering/view' import view from '@hcengineering/view'
import { ObjectBox } from '@hcengineering/view-resources' import { ObjectBox } from '@hcengineering/view-resources'
import { createEventDispatcher, onDestroy } from 'svelte' import { createEventDispatcher, onDestroy } from 'svelte'
import { activeComponent, activeMilestone, generateIssueShortLink, getIssueId, updateIssueRelation } from '../issues' import { activeComponent, activeMilestone, generateIssueShortLink, updateIssueRelation } from '../issues'
import tracker from '../plugin' import tracker from '../plugin'
import SetParentIssueActionPopup from './SetParentIssueActionPopup.svelte' import SetParentIssueActionPopup from './SetParentIssueActionPopup.svelte'
import ComponentSelector from './components/ComponentSelector.svelte' import ComponentSelector from './components/ComponentSelector.svelte'
@ -300,7 +300,7 @@
$: spaceQuery.query(tracker.class.Project, { _id: _space }, (res) => { $: spaceQuery.query(tracker.class.Project, { _id: _space }, (res) => {
resetDefaultAssigneeId() resetDefaultAssigneeId()
currentProject = res.shift() currentProject = res[0]
}) })
const docCreateManager = DocCreateExtensionManager.create(tracker.class.Issue) const docCreateManager = DocCreateExtensionManager.create(tracker.class.Issue)
@ -370,13 +370,15 @@
true true
) )
const number = (incResult as any).object.sequence
const value: DocData<Issue> = { const value: DocData<Issue> = {
title: getTitle(object.title), title: getTitle(object.title),
description: object.description, description: object.description,
assignee: object.assignee, assignee: object.assignee,
component: object.component, component: object.component,
milestone: object.milestone, milestone: object.milestone,
number: (incResult as any).object.sequence, number,
status: object.status, status: object.status,
priority: object.priority, priority: object.priority,
rank: calcRank(lastOne, undefined), rank: calcRank(lastOne, undefined),
@ -396,7 +398,8 @@
reports: 0, reports: 0,
relations: relatedTo !== undefined ? [{ _id: relatedTo._id, _class: relatedTo._class }] : [], relations: relatedTo !== undefined ? [{ _id: relatedTo._id, _class: relatedTo._class }] : [],
childInfo: [], childInfo: [],
kind kind,
identifier: `${currentProject?.identifier}-${number}`
} }
await docCreateManager.commit(operations, _id, _space, value) await docCreateManager.commit(operations, _id, _space, value)
@ -439,7 +442,7 @@
{ {
issueId: _id, issueId: _id,
subTitlePostfix: (await translate(tracker.string.CreatedOne, {}, $themeStore.language)).toLowerCase(), subTitlePostfix: (await translate(tracker.string.CreatedOne, {}, $themeStore.language)).toLowerCase(),
issueUrl: currentProject != null && generateIssueShortLink(getIssueId(currentProject, value as Issue)) issueUrl: currentProject != null && generateIssueShortLink(value.identifier)
} }
) )

View File

@ -15,12 +15,11 @@
<script lang="ts"> <script lang="ts">
import core, { AttachedData, FindOptions, Ref, SortingOrder } from '@hcengineering/core' import core, { AttachedData, FindOptions, Ref, SortingOrder } from '@hcengineering/core'
import { ObjectPopup, getClient } from '@hcengineering/presentation' import { ObjectPopup, getClient } from '@hcengineering/presentation'
import { calcRank } from '@hcengineering/task'
import { Issue, IssueDraft } from '@hcengineering/tracker' import { Issue, IssueDraft } from '@hcengineering/tracker'
import { createEventDispatcher } from 'svelte' import { createEventDispatcher } from 'svelte'
import { getIssueId } from '../issues'
import tracker from '../plugin' import tracker from '../plugin'
import IssueStatusIcon from './issues/IssueStatusIcon.svelte' import IssueStatusIcon from './issues/IssueStatusIcon.svelte'
import { calcRank } from '@hcengineering/task'
export let value: Issue | AttachedData<Issue> | Issue[] | IssueDraft export let value: Issue | AttachedData<Issue> | Issue[] | IssueDraft
export let width: 'medium' | 'large' | 'full' = 'large' export let width: 'medium' | 'large' | 'full' = 'large'
@ -29,7 +28,6 @@
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
const options: FindOptions<Issue> = { const options: FindOptions<Issue> = {
lookup: { lookup: {
space: tracker.class.Project,
status: [tracker.class.IssueStatus, { category: core.class.StatusCategory }] status: [tracker.class.IssueStatus, { category: core.class.StatusCategory }]
}, },
sort: { modifiedOn: SortingOrder.Descending } sort: { modifiedOn: SortingOrder.Descending }
@ -98,17 +96,14 @@
on:close={onClose} on:close={onClose}
> >
<svelte:fragment slot="item" let:item={issue}> <svelte:fragment slot="item" let:item={issue}>
{@const issueId = getIssueId(issue.$lookup.space, issue)} <div class="flex-center clear-mins w-full h-9">
{#if issueId} {#if issue?.$lookup?.status}
<div class="flex-center clear-mins w-full h-9"> <div class="icon mr-4 h-8">
{#if issue?.$lookup?.status} <IssueStatusIcon value={issue.$lookup.status} space={issue.space} size="small" />
<div class="icon mr-4 h-8"> </div>
<IssueStatusIcon value={issue.$lookup.status} space={issue.space} size="small" /> {/if}
</div> <span class="overflow-label flex-no-shrink mr-3">{issue.identifier}</span>
{/if} <span class="overflow-label w-full content-color">{issue.title}</span>
<span class="overflow-label flex-no-shrink mr-3">{issueId}</span> </div>
<span class="overflow-label w-full content-color">{issue.title}</span>
</div>
{/if}
</svelte:fragment> </svelte:fragment>
</ObjectPopup> </ObjectPopup>

View File

@ -15,13 +15,11 @@
--> -->
<script lang="ts"> <script lang="ts">
import { WithLookup } from '@hcengineering/core' import { WithLookup } from '@hcengineering/core'
import type { Issue, Project } from '@hcengineering/tracker' import type { Issue } from '@hcengineering/tracker'
import { FixedColumn, statusStore } from '@hcengineering/view-resources' import { FixedColumn, statusStore } from '@hcengineering/view-resources'
import { getIssueId } from '../../issues'
import IssueStatusIcon from './IssueStatusIcon.svelte' import IssueStatusIcon from './IssueStatusIcon.svelte'
export let value: WithLookup<Issue> export let value: WithLookup<Issue>
$: title = getIssueId(value.$lookup?.space as Project, value)
$: st = $statusStore.byId.get(value.status) $: st = $statusStore.byId.get(value.status)
</script> </script>
@ -33,6 +31,6 @@
{/if} {/if}
</FixedColumn> </FixedColumn>
<span class="ml-2 max-w-120 overflow-label"> <span class="ml-2 max-w-120 overflow-label">
{title} - {value.title} {value.identifier} - {value.title}
</span> </span>
</div> </div>

View File

@ -16,14 +16,13 @@
import { WithLookup } from '@hcengineering/core' import { WithLookup } from '@hcengineering/core'
import { Asset } from '@hcengineering/platform' import { Asset } from '@hcengineering/platform'
import { getClient } from '@hcengineering/presentation' import { getClient } from '@hcengineering/presentation'
import type { Issue, Project } from '@hcengineering/tracker' import { taskTypeStore } from '@hcengineering/task-resources'
import TaskTypeIcon from '@hcengineering/task-resources/src/components/taskTypes/TaskTypeIcon.svelte'
import type { Issue } from '@hcengineering/tracker'
import { AnySvelteComponent, Component, Icon, tooltip } from '@hcengineering/ui' import { AnySvelteComponent, Component, Icon, tooltip } from '@hcengineering/ui'
import view from '@hcengineering/view' import view from '@hcengineering/view'
import { DocNavLink } from '@hcengineering/view-resources' import { DocNavLink } from '@hcengineering/view-resources'
import tracker from '../../plugin' import tracker from '../../plugin'
import { activeProjects } from '../../utils'
import { taskTypeStore } from '@hcengineering/task-resources'
import TaskTypeIcon from '@hcengineering/task-resources/src/components/taskTypes/TaskTypeIcon.svelte'
export let value: WithLookup<Issue> | undefined export let value: WithLookup<Issue> | undefined
export let disabled: boolean = false export let disabled: boolean = false
@ -35,14 +34,6 @@
export let kind: 'list' | undefined = undefined export let kind: 'list' | undefined = undefined
export let icon: Asset | AnySvelteComponent | undefined = undefined export let icon: Asset | AnySvelteComponent | undefined = undefined
let currentProject: Project | undefined = value?.$lookup?.space
$: if (value !== undefined) {
currentProject = $activeProjects.get(value?.space) as Project
}
$: title = currentProject ? `${currentProject.identifier}-${value?.number}` : `${value?.number}`
$: presenters = $: presenters =
value !== undefined ? getClient().getHierarchy().findMixinMixins(value, view.mixin.ObjectPresenter) : [] value !== undefined ? getClient().getHierarchy().findMixinMixins(value, view.mixin.ObjectPresenter) : []
@ -61,7 +52,7 @@
shrink={0} shrink={0}
> >
{#if inline} {#if inline}
<span class="antiMention" use:tooltip={{ label: tracker.string.Issue }}>@{title}</span> <span class="antiMention" use:tooltip={{ label: tracker.string.Issue }}>@{value.identifier}</span>
{:else} {:else}
<span class="issuePresenterRoot" class:list={kind === 'list'} class:cursor-pointer={!disabled}> <span class="issuePresenterRoot" class:list={kind === 'list'} class:cursor-pointer={!disabled}>
{#if shouldShowAvatar} {#if shouldShowAvatar}
@ -74,7 +65,7 @@
</div> </div>
{/if} {/if}
<span class="overflow-label" class:select-text={!noSelect} title={value?.title}> <span class="overflow-label" class:select-text={!noSelect} title={value?.title}>
{title} {value.identifier}
<slot name="details" /> <slot name="details" />
</span> </span>
</span> </span>

View File

@ -45,11 +45,6 @@
{ limit: 1 } { limit: 1 }
) )
let currentProject: Project | undefined
$: currentProject = $activeProjects.get(space) as Project
$: issueName = currentProject && issue && `${currentProject.identifier}-${issue.number}`
const limit: number = 350 const limit: number = 350
let cHeight: number = 0 let cHeight: number = 0
@ -74,7 +69,7 @@
<span class="overflow-label content-color">{parent.title}</span> <span class="overflow-label content-color">{parent.title}</span>
<IconForward size={'x-small'} /> <IconForward size={'x-small'} />
{/if} {/if}
<span class="content-dark-color">{issueName}</span> <span class="content-dark-color">{issue.identifier}</span>
</div> </div>
<span class="overflow-label text-xl caption-color">{issue.title}</span> <span class="overflow-label text-xl caption-color">{issue.title}</span>
</div> </div>

View File

@ -21,14 +21,12 @@
let currentProject: Project | undefined = undefined let currentProject: Project | undefined = undefined
$: currentProject = $activeProjects.get(value.space) as Project $: currentProject = $activeProjects.get(value.space) as Project
$: title = currentProject ? `${currentProject.identifier}-${value?.number}` : `${value?.number}`
</script> </script>
{#if value} {#if value}
<div class="flex-col"> <div class="flex-col">
<div class="flex-row-center crop-presenter"> <div class="flex-row-center crop-presenter">
<span class="font-medium mr-2 whitespace-nowrap clear-mins">{title}</span> <span class="font-medium mr-2 whitespace-nowrap clear-mins">{value.identifier}</span>
<span class="overflow-label"> <span class="overflow-label">
{currentProject?.name} {currentProject?.name}
</span> </span>

View File

@ -13,33 +13,22 @@
// limitations under the License. // limitations under the License.
--> -->
<script lang="ts"> <script lang="ts">
import { Issue, Project } from '@hcengineering/tracker' import { Issue } from '@hcengineering/tracker'
import { Button, IconClose, Spinner } from '@hcengineering/ui' import { Button, IconClose } from '@hcengineering/ui'
import { createEventDispatcher } from 'svelte' import { createEventDispatcher } from 'svelte'
import { getIssueId } from '../../issues'
import tracker from '../../plugin' import tracker from '../../plugin'
import { activeProjects } from '../../utils'
import PriorityRefPresenter from './PriorityRefPresenter.svelte' import PriorityRefPresenter from './PriorityRefPresenter.svelte'
export let issue: Issue export let issue: Issue
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
let project: Project | undefined
$: project = $activeProjects.get(issue.space)
$: issueId = project && getIssueId(project, issue)
</script> </script>
<div class="parentIssue-container"> <div class="parentIssue-container">
<div class="flex-no-shrink mr-1-5"> <div class="flex-no-shrink mr-1-5">
<PriorityRefPresenter value={issue.priority} shouldShowLabel={false} /> <PriorityRefPresenter value={issue.priority} shouldShowLabel={false} />
</div> </div>
{#if issueId} <span class="overflow-label flex-no-shrink content-dark-color">{issue.identifier}</span>
<span class="overflow-label flex-no-shrink content-dark-color">{issueId}</span>
{:else}
<Spinner size="small" />
{/if}
<span class="overflow-label issue-title">{issue.title}</span> <span class="overflow-label issue-title">{issue.title}</span>
<Button <Button
icon={IconClose} icon={IconClose}

View File

@ -6,7 +6,7 @@
import { Issue } from '@hcengineering/tracker' import { Issue } from '@hcengineering/tracker'
import { Component, Icon, IconClose, navigate } from '@hcengineering/ui' import { Component, Icon, IconClose, navigate } from '@hcengineering/ui'
import view from '@hcengineering/view' import view from '@hcengineering/view'
import { getIssueId, issueLinkFragmentProvider, updateIssueRelation } from '../../issues' import { issueLinkFragmentProvider, updateIssueRelation } from '../../issues'
import tracker from '../../plugin' import tracker from '../../plugin'
export let value: Issue export let value: Issue
@ -24,14 +24,9 @@
$: isIssue = client.getHierarchy().isDerived(_class, tracker.class.Issue) $: isIssue = client.getHierarchy().isDerived(_class, tracker.class.Issue)
let documents: WithLookup<Doc>[] = [] let documents: WithLookup<Doc>[] = []
$: issuesQuery.query( $: issuesQuery.query(_class, query, (result) => {
_class, documents = result
query, })
(result) => {
documents = result
},
{ lookup: isIssue ? { space: tracker.class.Project } : {} }
)
async function handleClick (issue: RelatedDocument) { async function handleClick (issue: RelatedDocument) {
const prop = type === 'isBlocking' ? 'blockedBy' : type const prop = type === 'isBlocking' ? 'blockedBy' : type
@ -82,19 +77,17 @@
{#each documents as doc} {#each documents as doc}
{#if isIssue} {#if isIssue}
{@const issue = asIssue(doc)} {@const issue = asIssue(doc)}
{#if issue.$lookup?.space} <div class="tag-container">
<div class="tag-container"> <Icon {icon} size={'small'} />
<Icon {icon} size={'small'} /> <div class="flex-grow">
<div class="flex-grow"> <button on:click|stopPropagation={() => handleRedirect(issue)}>
<button on:click|stopPropagation={() => handleRedirect(issue)}> <span class="overflow-label ml-1-5 content-color">{issue.identifier}</span>
<span class="overflow-label ml-1-5 content-color">{getIssueId(issue.$lookup.space, issue)}</span>
</button>
</div>
<button class="btn-close" on:click|stopPropagation={() => handleClick(issue)}>
<Icon icon={IconClose} size={'x-small'} />
</button> </button>
</div> </div>
{/if} <button class="btn-close" on:click|stopPropagation={() => handleClick(issue)}>
<Icon icon={IconClose} size={'x-small'} />
</button>
</div>
{:else} {:else}
<div class="tag-container between"> <div class="tag-container between">
<Component <Component

View File

@ -90,7 +90,8 @@
'dueDate', 'dueDate',
'milestone', 'milestone',
'relations', 'relations',
'blockedBy' 'blockedBy',
'identifier'
]) ])
let account: PersonAccount | undefined let account: PersonAccount | undefined

View File

@ -46,7 +46,7 @@
import view from '@hcengineering/view' import view from '@hcengineering/view'
import { ContextMenu, DocNavLink, ParentsNavigator } from '@hcengineering/view-resources' import { ContextMenu, DocNavLink, ParentsNavigator } from '@hcengineering/view-resources'
import { createEventDispatcher, onDestroy } from 'svelte' import { createEventDispatcher, onDestroy } from 'svelte'
import { generateIssueShortLink, getIssueId } from '../../../issues' import { generateIssueShortLink } from '../../../issues'
import tracker from '../../../plugin' import tracker from '../../../plugin'
import IssueStatusActivity from '../IssueStatusActivity.svelte' import IssueStatusActivity from '../IssueStatusActivity.svelte'
import ControlPanel from './ControlPanel.svelte' import ControlPanel from './ControlPanel.svelte'
@ -104,7 +104,6 @@
{ lookup: { attachedTo: tracker.class.Issue, space: tracker.class.Project } } { lookup: { attachedTo: tracker.class.Issue, space: tracker.class.Project } }
) )
$: issueId = currentProject !== undefined && issue !== undefined && getIssueId(currentProject, issue)
$: canSave = title.trim().length > 0 $: canSave = title.trim().length > 0
$: parentIssue = issue?.$lookup?.attachedTo $: parentIssue = issue?.$lookup?.attachedTo
@ -195,12 +194,12 @@
{#if !embedded} {#if !embedded}
<ParentsNavigator element={issue} /> <ParentsNavigator element={issue} />
{/if} {/if}
{#if embedded && issueId} {#if embedded}
<DocNavLink noUnderline object={issue}> <DocNavLink noUnderline object={issue}>
<div class="title">{issueId}</div> <div class="title">{issue.identifier}</div>
</DocNavLink> </DocNavLink>
{:else if issueId} {:else}
<div class="title not-active">{issueId}</div> <div class="title not-active">{issue.identifier}</div>
{/if} {/if}
{#if (projectType?.tasks.length ?? 0) > 1 && taskType !== undefined} {#if (projectType?.tasks.length ?? 0) > 1 && taskType !== undefined}
@ -223,9 +222,7 @@
<svelte:fragment slot="utils"> <svelte:fragment slot="utils">
<Button icon={IconMoreH} iconProps={{ size: 'medium' }} kind={'icon'} on:click={showMenu} /> <Button icon={IconMoreH} iconProps={{ size: 'medium' }} kind={'icon'} on:click={showMenu} />
{#if issueId} <CopyToClipboard issueUrl={generateIssueShortLink(issue.identifier)} />
<CopyToClipboard issueUrl={generateIssueShortLink(issueId)} />
{/if}
<Button <Button
icon={setting.icon.Setting} icon={setting.icon.Setting}
kind={'icon'} kind={'icon'}
@ -298,7 +295,7 @@
{/if} {/if}
<span slot="actions-label" class="select-text"> <span slot="actions-label" class="select-text">
{#if issueId}{issueId}{/if} {issue.identifier}
</span> </span>
<svelte:fragment slot="custom-attributes"> <svelte:fragment slot="custom-attributes">

View File

@ -30,7 +30,7 @@
tooltip tooltip
} from '@hcengineering/ui' } from '@hcengineering/ui'
import { statusStore } from '@hcengineering/view-resources' import { statusStore } from '@hcengineering/view-resources'
import { getIssueId, issueLinkFragmentProvider } from '../../../issues' import { issueLinkFragmentProvider } from '../../../issues'
import tracker from '../../../plugin' import tracker from '../../../plugin'
import { listIssueStatusOrder } from '../../../utils' import { listIssueStatusOrder } from '../../../utils'
import IssueStatusIcon from '../IssueStatusIcon.svelte' import IssueStatusIcon from '../IssueStatusIcon.svelte'
@ -75,7 +75,6 @@
{ {
sort: { modifiedOn: SortingOrder.Descending }, sort: { modifiedOn: SortingOrder.Descending },
lookup: { lookup: {
space: tracker.class.Project,
status: [tracker.class.IssueStatus, { category: core.class.StatusCategory }] status: [tracker.class.IssueStatus, { category: core.class.StatusCategory }]
} }
} }
@ -123,7 +122,7 @@
id: iss._id, id: iss._id,
icon, icon,
isSelected: iss._id === issue._id, isSelected: iss._id === issue._id,
...(project !== undefined ? { text: `${getIssueId(project, iss)} ${iss.title}` } : undefined), text: `${iss.identifier} ${iss.title}`,
...(color !== undefined ? { iconColor: getPlatformColorDef(color, $themeStore.dark).icon } : undefined), ...(color !== undefined ? { iconColor: getPlatformColorDef(color, $themeStore.dark).icon } : undefined),
category: category:
category !== undefined category !== undefined
@ -151,9 +150,7 @@
<IssueStatusIcon space={parentIssue.space} value={parentStatus} size="small" /> <IssueStatusIcon space={parentIssue.space} value={parentStatus} size="small" />
</div> </div>
{/if} {/if}
{#if issue.$lookup?.space} <span class="overflow-label flex-no-shrink mr-2">{parentIssue.identifier}</span>
<span class="overflow-label flex-no-shrink mr-2">{getIssueId(issue.$lookup.space, parentIssue)}</span>
{/if}
<span class="overflow-label issue-title">{parentIssue.title}</span> <span class="overflow-label issue-title">{parentIssue.title}</span>
</div> </div>
</div> </div>

View File

@ -16,14 +16,13 @@
import core, { IdMap, Ref, SortingOrder, StatusCategory, WithLookup, toIdMap } from '@hcengineering/core' import core, { IdMap, Ref, SortingOrder, StatusCategory, WithLookup, toIdMap } from '@hcengineering/core'
import { createQuery, getClient } from '@hcengineering/presentation' import { createQuery, getClient } from '@hcengineering/presentation'
import task, { getStates } from '@hcengineering/task' import task, { getStates } from '@hcengineering/task'
import { typeStore } from '@hcengineering/task-resources'
import { Issue, Project } from '@hcengineering/tracker' import { Issue, Project } from '@hcengineering/tracker'
import { Button, ButtonKind, ButtonSize, ProgressCircle, SelectPopup, showPanel } from '@hcengineering/ui' import { Button, ButtonKind, ButtonSize, ProgressCircle, SelectPopup, showPanel } from '@hcengineering/ui'
import { statusStore } from '@hcengineering/view-resources' import { statusStore } from '@hcengineering/view-resources'
import { getIssueId } from '../../../issues'
import tracker from '../../../plugin' import tracker from '../../../plugin'
import { listIssueStatusOrder } from '../../../utils' import { listIssueStatusOrder } from '../../../utils'
import IssueStatusIcon from '../IssueStatusIcon.svelte' import IssueStatusIcon from '../IssueStatusIcon.svelte'
import { typeStore } from '@hcengineering/task-resources'
export let value: WithLookup<Issue> export let value: WithLookup<Issue>
export let currentProject: Project | undefined = undefined export let currentProject: Project | undefined = undefined
@ -116,7 +115,7 @@
} }
$: subIssuesValue = _subIssues.map((iss) => { $: subIssuesValue = _subIssues.map((iss) => {
const text = project != null ? `${getIssueId(project, iss)} ${iss.title}` : iss.title const text = `${iss.identifier} ${iss.title}`
const c = $statusStore.byId.get(iss.status)?.category const c = $statusStore.byId.get(iss.status)?.category
const category = c !== undefined ? categories.get(c) : undefined const category = c !== undefined ? categories.get(c) : undefined
return { return {

View File

@ -13,19 +13,17 @@
// limitations under the License. // limitations under the License.
--> -->
<script lang="ts"> <script lang="ts">
import { Issue, Project } from '@hcengineering/tracker' import { Issue } from '@hcengineering/tracker'
import { statusStore } from '@hcengineering/view-resources' import { statusStore } from '@hcengineering/view-resources'
import IssueStatusIcon from '../IssueStatusIcon.svelte' import IssueStatusIcon from '../IssueStatusIcon.svelte'
import { getIssueId } from '../../../issues'
export let project: Project | undefined
export let issue: Issue export let issue: Issue
export let size: 'small' | 'medium' | 'large' = 'small' export let size: 'small' | 'medium' | 'large' = 'small'
$: status = $statusStore.byId.get(issue.status) $: status = $statusStore.byId.get(issue.status)
$: huge = size === 'medium' || size === 'large' $: huge = size === 'medium' || size === 'large'
$: text = project ? `${getIssueId(project, issue)} ${issue.title}` : issue.title $: text = `${issue.identifier} ${issue.title}`
</script> </script>
<div class="flex-row-center"> <div class="flex-row-center">

View File

@ -18,7 +18,6 @@
import { Issue } from '@hcengineering/tracker' import { Issue } from '@hcengineering/tracker'
import { ListView, deviceOptionsStore as deviceInfo, getEventPositionElement, showPopup } from '@hcengineering/ui' import { ListView, deviceOptionsStore as deviceInfo, getEventPositionElement, showPopup } from '@hcengineering/ui'
import { ContextMenu, FixedColumn, ListSelectionProvider } from '@hcengineering/view-resources' import { ContextMenu, FixedColumn, ListSelectionProvider } from '@hcengineering/view-resources'
import { getIssueId } from '../../../issues'
import tracker from '../../../plugin' import tracker from '../../../plugin'
import { activeProjects } from '../../../utils' import { activeProjects } from '../../../utils'
import EstimationEditor from './EstimationEditor.svelte' import EstimationEditor from './EstimationEditor.svelte'
@ -52,9 +51,7 @@
> >
<div class="flex-row-center clear-mins gap-2 flex-grow mr-4" class:p-text={twoRows}> <div class="flex-row-center clear-mins gap-2 flex-grow mr-4" class:p-text={twoRows}>
<FixedColumn key={'estimation_issue'} justify={'left'} addClass={'fs-bold'}> <FixedColumn key={'estimation_issue'} justify={'left'} addClass={'fs-bold'}>
{#if currentProject} {issue.identifier}
{getIssueId(currentProject, issue)}
{/if}
</FixedColumn> </FixedColumn>
<span class="overflow-label fs-bold caption-color" title={issue.title}> <span class="overflow-label fs-bold caption-color" title={issue.title}>
{issue.title} {issue.title}

View File

@ -14,23 +14,22 @@
--> -->
<script lang="ts"> <script lang="ts">
import contact from '@hcengineering/contact' import contact from '@hcengineering/contact'
import { Ref, Space, WithLookup } from '@hcengineering/core'
import { UserBox } from '@hcengineering/contact-resources' import { UserBox } from '@hcengineering/contact-resources'
import { Ref, Space, WithLookup } from '@hcengineering/core'
import { Project, TimeReportDayType, TimeSpendReport } from '@hcengineering/tracker' import { Project, TimeReportDayType, TimeSpendReport } from '@hcengineering/tracker'
import { import {
DatePresenter,
ListView,
deviceOptionsStore as deviceInfo, deviceOptionsStore as deviceInfo,
eventToHTMLElement, eventToHTMLElement,
getEventPositionElement, getEventPositionElement,
ListView, showPopup
showPopup,
DatePresenter
} from '@hcengineering/ui' } from '@hcengineering/ui'
import { ContextMenu, FixedColumn, ListSelectionProvider } from '@hcengineering/view-resources' import { ContextMenu, FixedColumn, ListSelectionProvider } from '@hcengineering/view-resources'
import { getIssueId } from '../../../issues'
import tracker from '../../../plugin' import tracker from '../../../plugin'
import { activeProjects } from '../../../utils'
import TimePresenter from './TimePresenter.svelte' import TimePresenter from './TimePresenter.svelte'
import TimeSpendReportPopup from './TimeSpendReportPopup.svelte' import TimeSpendReportPopup from './TimeSpendReportPopup.svelte'
import { activeProjects } from '../../../utils'
export let reports: WithLookup<TimeSpendReport>[] export let reports: WithLookup<TimeSpendReport>[]
@ -86,8 +85,8 @@
> >
<div class="flex-row-center clear-mins gap-2 flex-grow mr-4" class:p-text={twoRows}> <div class="flex-row-center clear-mins gap-2 flex-grow mr-4" class:p-text={twoRows}>
<FixedColumn key={'timespend_issue'} justify={'left'} addClass={'fs-bold'}> <FixedColumn key={'timespend_issue'} justify={'left'} addClass={'fs-bold'}>
{#if currentProject && report.$lookup?.attachedTo} {#if report.$lookup?.attachedTo}
{getIssueId(currentProject, report.$lookup?.attachedTo)} {report.$lookup?.attachedTo?.identifier}
{/if} {/if}
</FixedColumn> </FixedColumn>
{#if report.$lookup?.attachedTo?.title} {#if report.$lookup?.attachedTo?.title}

View File

@ -1,34 +0,0 @@
<script lang="ts">
import presentation, { Card } from '@hcengineering/presentation'
import EditBox from '@hcengineering/ui/src/components/EditBox.svelte'
import { createEventDispatcher } from 'svelte'
import tracker from '../../plugin'
export let identifier: string
export let projectsIdentifiers: Set<string>
let newIdentifier = identifier
const dispatch = createEventDispatcher()
function save () {
dispatch('close', newIdentifier)
}
</script>
<Card
label={projectsIdentifiers.has(newIdentifier) ? tracker.string.IdentifierExists : tracker.string.ProjectIdentifier}
okLabel={presentation.string.Save}
okAction={save}
canSave={!!newIdentifier && newIdentifier !== identifier && !projectsIdentifiers.has(newIdentifier)}
on:close={() => {
dispatch('close')
}}
on:changeContent
>
<div class="float-left-box">
<div class="float-left p-2">
<EditBox bind:value={newIdentifier} uppercase />
</div>
</div>
</Card>

View File

@ -130,9 +130,6 @@
if (projectData.defaultTimeReportDay !== project?.defaultTimeReportDay) { if (projectData.defaultTimeReportDay !== project?.defaultTimeReportDay) {
update.defaultTimeReportDay = projectData.defaultTimeReportDay update.defaultTimeReportDay = projectData.defaultTimeReportDay
} }
if (projectData.identifier.toUpperCase() !== project?.identifier) {
update.identifier = projectData.identifier.toUpperCase()
}
if (projectData.members.length !== project?.members.length) { if (projectData.members.length !== project?.members.length) {
update.members = projectData.members update.members = projectData.members
} else { } else {
@ -180,7 +177,6 @@
close(projectId) close(projectId)
} else { } else {
isSaving = false isSaving = false
changeIdentity(changeIdentityRef)
} }
} }
} }
@ -196,13 +192,6 @@
} }
showPopup(IconPicker, { icon, color, icons }, 'top', update, update) showPopup(IconPicker, { icon, color, icons }, 'top', update, update)
} }
function changeIdentity (element: HTMLElement): void {
showPopup(ChangeIdentity, { identifier, projectsIdentifiers }, element, (result) => {
if (result != null) {
identifier = result.toLocaleUpperCase()
}
})
}
function close (id?: Ref<Project>): void { function close (id?: Ref<Project>): void {
dispatch('close', id) dispatch('close', id)
@ -286,17 +275,7 @@
kind={'large-style'} kind={'large-style'}
uppercase uppercase
/> />
{#if !isNew} {#if !isSaving && projectsIdentifiers.has(identifier.toUpperCase())}
<div class="ml-1">
<Button
size={'small'}
icon={IconEdit}
on:click={(ev) => {
changeIdentity(eventToHTMLElement(ev))
}}
/>
</div>
{:else if !isSaving && projectsIdentifiers.has(identifier.toUpperCase())}
<div class="absolute overflow-label duplicated-identifier"> <div class="absolute overflow-label duplicated-identifier">
<Label label={tracker.string.IdentifierExists} /> <Label label={tracker.string.IdentifierExists} />
</div> </div>

View File

@ -14,21 +14,21 @@
// //
import core, { import core, {
ClassifierKind,
DOMAIN_CONFIGURATION,
DOMAIN_MODEL,
getCurrentAccount,
toIdMap,
type AttachedDoc, type AttachedDoc,
type Class, type Class,
ClassifierKind,
type Client, type Client,
type Doc, type Doc,
type DocumentQuery, type DocumentQuery,
DOMAIN_MODEL,
getCurrentAccount,
type Ref, type Ref,
type RelatedDocument, type RelatedDocument,
toIdMap, type TxOperations
type TxOperations,
DOMAIN_CONFIGURATION
} from '@hcengineering/core' } from '@hcengineering/core'
import { type Resources, translate } from '@hcengineering/platform' import { translate, type Resources } from '@hcengineering/platform'
import { getClient, MessageBox, type ObjectSearchResult } from '@hcengineering/presentation' import { getClient, MessageBox, type ObjectSearchResult } from '@hcengineering/presentation'
import { type Issue, type Milestone, type Project } from '@hcengineering/tracker' import { type Issue, type Milestone, type Project } from '@hcengineering/tracker'
import { getCurrentLocation, navigate, showPopup, themeStore } from '@hcengineering/ui' import { getCurrentLocation, navigate, showPopup, themeStore } from '@hcengineering/ui'
@ -45,23 +45,22 @@ import ProjectComponents from './components/components/ProjectComponents.svelte'
import CreateIssue from './components/CreateIssue.svelte' import CreateIssue from './components/CreateIssue.svelte'
import EditRelatedTargets from './components/EditRelatedTargets.svelte' import EditRelatedTargets from './components/EditRelatedTargets.svelte'
import EditRelatedTargetsPopup from './components/EditRelatedTargetsPopup.svelte' import EditRelatedTargetsPopup from './components/EditRelatedTargetsPopup.svelte'
import SettingsRelatedTargets from './components/SettingsRelatedTargets.svelte'
import Inbox from './components/inbox/Inbox.svelte' import Inbox from './components/inbox/Inbox.svelte'
import AssigneeEditor from './components/issues/AssigneeEditor.svelte' import AssigneeEditor from './components/issues/AssigneeEditor.svelte'
import DueDatePresenter from './components/issues/DueDatePresenter.svelte' import DueDatePresenter from './components/issues/DueDatePresenter.svelte'
import EditIssue from './components/issues/edit/EditIssue.svelte' import EditIssue from './components/issues/edit/EditIssue.svelte'
import IssueItem from './components/issues/IssueItem.svelte' import IssueItem from './components/issues/IssueItem.svelte'
import IssueSearchIcon from './components/issues/IssueSearchIcon.svelte'
import IssuePresenter from './components/issues/IssuePresenter.svelte' import IssuePresenter from './components/issues/IssuePresenter.svelte'
import IssuePreview from './components/issues/IssuePreview.svelte' import IssuePreview from './components/issues/IssuePreview.svelte'
import Issues from './components/issues/Issues.svelte' import Issues from './components/issues/Issues.svelte'
import IssueSearchIcon from './components/issues/IssueSearchIcon.svelte'
import IssuesView from './components/issues/IssuesView.svelte' import IssuesView from './components/issues/IssuesView.svelte'
import KanbanView from './components/issues/KanbanView.svelte' import KanbanView from './components/issues/KanbanView.svelte'
import ModificationDatePresenter from './components/issues/ModificationDatePresenter.svelte' import ModificationDatePresenter from './components/issues/ModificationDatePresenter.svelte'
import NotificationIssuePresenter from './components/issues/NotificationIssuePresenter.svelte' import NotificationIssuePresenter from './components/issues/NotificationIssuePresenter.svelte'
import PriorityEditor from './components/issues/PriorityEditor.svelte' import PriorityEditor from './components/issues/PriorityEditor.svelte'
import PriorityInlineEditor from './components/issues/PriorityInlineEditor.svelte'
import PriorityFilterValuePresenter from './components/issues/PriorityFilterValuePresenter.svelte' import PriorityFilterValuePresenter from './components/issues/PriorityFilterValuePresenter.svelte'
import PriorityInlineEditor from './components/issues/PriorityInlineEditor.svelte'
import PriorityPresenter from './components/issues/PriorityPresenter.svelte' import PriorityPresenter from './components/issues/PriorityPresenter.svelte'
import PriorityRefPresenter from './components/issues/PriorityRefPresenter.svelte' import PriorityRefPresenter from './components/issues/PriorityRefPresenter.svelte'
import RelatedIssueSelector from './components/issues/related/RelatedIssueSelector.svelte' import RelatedIssueSelector from './components/issues/related/RelatedIssueSelector.svelte'
@ -75,21 +74,21 @@ import MilestoneDatePresenter from './components/milestones/MilestoneDatePresent
import MyIssues from './components/myissues/MyIssues.svelte' import MyIssues from './components/myissues/MyIssues.svelte'
import NewIssueHeader from './components/NewIssueHeader.svelte' import NewIssueHeader from './components/NewIssueHeader.svelte'
import NopeComponent from './components/NopeComponent.svelte' import NopeComponent from './components/NopeComponent.svelte'
import MembersArrayEditor from './components/projects/MembersArrayEditor.svelte'
import ProjectFilterValuePresenter from './components/projects/ProjectFilterValuePresenter.svelte' import ProjectFilterValuePresenter from './components/projects/ProjectFilterValuePresenter.svelte'
import RelationsPopup from './components/RelationsPopup.svelte' import RelationsPopup from './components/RelationsPopup.svelte'
import SetDueDateActionPopup from './components/SetDueDateActionPopup.svelte' import SetDueDateActionPopup from './components/SetDueDateActionPopup.svelte'
import SetParentIssueActionPopup from './components/SetParentIssueActionPopup.svelte' import SetParentIssueActionPopup from './components/SetParentIssueActionPopup.svelte'
import SettingsRelatedTargets from './components/SettingsRelatedTargets.svelte'
import CreateIssueTemplate from './components/templates/CreateIssueTemplate.svelte' import CreateIssueTemplate from './components/templates/CreateIssueTemplate.svelte'
import MembersArrayEditor from './components/projects/MembersArrayEditor.svelte'
import { import {
getIssueId, getIssueTitle,
getTitle,
issueIdentifierProvider, issueIdentifierProvider,
issueIdProvider,
issueLinkFragmentProvider, issueLinkFragmentProvider,
issueLinkProvider, issueLinkProvider,
getIssueTitle, issueTitleProvider,
resolveLocation, resolveLocation
issueTitleProvider
} from './issues' } from './issues'
import tracker from './plugin' import tracker from './plugin'
@ -98,9 +97,9 @@ import MilestonePresenter from './components/milestones/MilestonePresenter.svelt
import Milestones from './components/milestones/Milestones.svelte' import Milestones from './components/milestones/Milestones.svelte'
import MilestoneSelector from './components/milestones/MilestoneSelector.svelte' import MilestoneSelector from './components/milestones/MilestoneSelector.svelte'
import MilestoneStatusEditor from './components/milestones/MilestoneStatusEditor.svelte' import MilestoneStatusEditor from './components/milestones/MilestoneStatusEditor.svelte'
import MilestoneStatusIcon from './components/milestones/MilestoneStatusIcon.svelte'
import MilestoneStatusPresenter from './components/milestones/MilestoneStatusPresenter.svelte' import MilestoneStatusPresenter from './components/milestones/MilestoneStatusPresenter.svelte'
import MilestoneTitlePresenter from './components/milestones/MilestoneTitlePresenter.svelte' import MilestoneTitlePresenter from './components/milestones/MilestoneTitlePresenter.svelte'
import MilestoneStatusIcon from './components/milestones/MilestoneStatusIcon.svelte'
import SubIssuesSelector from './components/issues/edit/SubIssuesSelector.svelte' import SubIssuesSelector from './components/issues/edit/SubIssuesSelector.svelte'
import EstimationEditor from './components/issues/timereport/EstimationEditor.svelte' import EstimationEditor from './components/issues/timereport/EstimationEditor.svelte'
@ -125,9 +124,9 @@ import {
getAllMilestones, getAllMilestones,
getAllPriority, getAllPriority,
getComponentTitle, getComponentTitle,
getIssueChatTitle,
getIssueStatusCategories, getIssueStatusCategories,
getMilestoneTitle, getMilestoneTitle,
getIssueChatTitle,
getVisibleFilters, getVisibleFilters,
issuePrioritySort, issuePrioritySort,
issueStatusSort, issueStatusSort,
@ -141,7 +140,9 @@ import PriorityIcon from './components/activity/PriorityIcon.svelte'
import StatusIcon from './components/activity/StatusIcon.svelte' import StatusIcon from './components/activity/StatusIcon.svelte'
import TxIssueCreated from './components/activity/TxIssueCreated.svelte' import TxIssueCreated from './components/activity/TxIssueCreated.svelte'
import DeleteComponentPresenter from './components/components/DeleteComponentPresenter.svelte' import DeleteComponentPresenter from './components/components/DeleteComponentPresenter.svelte'
import IssueStatusIcon from './components/issues/IssueStatusIcon.svelte'
import MoveIssues from './components/issues/Move.svelte' import MoveIssues from './components/issues/Move.svelte'
import PriorityIconPresenter from './components/issues/PriorityIconPresenter.svelte'
import StatusRefPresenter from './components/issues/StatusRefPresenter.svelte' import StatusRefPresenter from './components/issues/StatusRefPresenter.svelte'
import TimeSpendReportPopup from './components/issues/timereport/TimeSpendReportPopup.svelte' import TimeSpendReportPopup from './components/issues/timereport/TimeSpendReportPopup.svelte'
import IssueStatistics from './components/milestones/IssueStatistics.svelte' import IssueStatistics from './components/milestones/IssueStatistics.svelte'
@ -150,21 +151,19 @@ import MilestoneRefPresenter from './components/milestones/MilestoneRefPresenter
import CreateProject from './components/projects/CreateProject.svelte' import CreateProject from './components/projects/CreateProject.svelte'
import ProjectPresenter from './components/projects/ProjectPresenter.svelte' import ProjectPresenter from './components/projects/ProjectPresenter.svelte'
import ProjectSpacePresenter from './components/projects/ProjectSpacePresenter.svelte' import ProjectSpacePresenter from './components/projects/ProjectSpacePresenter.svelte'
import IssueStatusIcon from './components/issues/IssueStatusIcon.svelte'
import PriorityIconPresenter from './components/issues/PriorityIconPresenter.svelte'
import { get } from 'svelte/store' import { get } from 'svelte/store'
import { settingId } from '@hcengineering/setting'
import EstimationValueEditor from './components/issues/timereport/EstimationValueEditor.svelte' import EstimationValueEditor from './components/issues/timereport/EstimationValueEditor.svelte'
import TimePresenter from './components/issues/timereport/TimePresenter.svelte' import TimePresenter from './components/issues/timereport/TimePresenter.svelte'
import { settingId } from '@hcengineering/setting'
export { default as AssigneeEditor } from './components/issues/AssigneeEditor.svelte' export { default as AssigneeEditor } from './components/issues/AssigneeEditor.svelte'
export { default as SubIssueList } from './components/issues/edit/SubIssueList.svelte' export { default as SubIssueList } from './components/issues/edit/SubIssueList.svelte'
export { default as IssueStatusIcon } from './components/issues/IssueStatusIcon.svelte' export { default as IssueStatusIcon } from './components/issues/IssueStatusIcon.svelte'
export { default as StatusPresenter } from './components/issues/StatusPresenter.svelte' export { default as StatusPresenter } from './components/issues/StatusPresenter.svelte'
export { CreateProject, IssuePresenter, PriorityEditor, StatusEditor, TitlePresenter, activeProjects } export { activeProjects, CreateProject, IssuePresenter, PriorityEditor, StatusEditor, TitlePresenter }
export async function queryIssue<D extends Issue> ( export async function queryIssue<D extends Issue> (
_class: Ref<Class<D>>, _class: Ref<Class<D>>,
@ -172,9 +171,7 @@ export async function queryIssue<D extends Issue> (
search: string, search: string,
filter?: { in?: RelatedDocument[], nin?: RelatedDocument[] } filter?: { in?: RelatedDocument[], nin?: RelatedDocument[] }
): Promise<ObjectSearchResult[]> { ): Promise<ObjectSearchResult[]> {
const projects = await client.findAll<Project>(tracker.class.Project, {}) const q: DocumentQuery<Issue> = { identifier: { $like: `%${search}%` } }
const q: DocumentQuery<Issue> = { title: { $like: `%${search}%` } }
if (filter?.in !== undefined || filter?.nin !== undefined) { if (filter?.in !== undefined || filter?.nin !== undefined) {
q._id = {} q._id = {}
if (filter.in !== undefined) { if (filter.in !== undefined) {
@ -185,40 +182,28 @@ export async function queryIssue<D extends Issue> (
} }
} }
const named = toIdMap( const numbered = toIdMap(
await client.findAll<Issue>(_class, q, { await client.findAll<Issue>(_class, q, {
limit: 200, limit: 200
lookup: { space: tracker.class.Project }
}) })
) )
for (const currentProject of projects) {
const nids: number[] = [] const q2: DocumentQuery<Issue> = { title: { $like: `%${search}%` } }
for (let n = 0; n <= currentProject.sequence; n++) { if (q._id !== undefined) {
const v = `${currentProject.identifier}-${n}` q2._id = q._id
if (v.includes(search)) { }
nids.push(n) const named = await client.findAll<Issue>(_class, q2, { limit: 200 })
} for (const d of named) {
} if (d.identifier.includes(search) || d.title.includes(search)) {
if (nids.length > 0) { if (!numbered.has(d._id)) {
const q2: DocumentQuery<Issue> = { number: { $in: nids } } numbered.set(d._id, d)
if (q._id !== undefined) {
q2._id = q._id
}
const numbered = await client.findAll<Issue>(_class, q2, { limit: 200, lookup: { space: tracker.class.Project } })
for (const d of numbered) {
const shortId = `${projects.find((it) => it._id === d.space)?.identifier ?? ''}-${d.number}`
if (shortId.includes(search) || d.title.includes(search)) {
if (!named.has(d._id)) {
named.set(d._id, d)
}
}
} }
} }
} }
return Array.from(named.values()).map((e) => ({ return Array.from(numbered.values()).map((e) => ({
doc: e, doc: e,
title: getIssueId(e.$lookup?.space as Project, e), title: e.identifier,
icon: tracker.icon.TrackerApplication, icon: tracker.icon.TrackerApplication,
component: IssueItem component: IssueItem
})) }))
@ -533,7 +518,7 @@ export default async (): Promise<Resources> => ({
IssueTitleProvider: issueTitleProvider, IssueTitleProvider: issueTitleProvider,
ComponentTitleProvider: getComponentTitle, ComponentTitleProvider: getComponentTitle,
MilestoneTitleProvider: getMilestoneTitle, MilestoneTitleProvider: getMilestoneTitle,
GetIssueId: issueIdProvider, GetIssueId: getTitle,
GetIssueLink: issueLinkProvider, GetIssueLink: issueLinkProvider,
GetIssueLinkFragment: issueLinkFragmentProvider, GetIssueLinkFragment: issueLinkFragmentProvider,
GetIssueTitle: getIssueTitle, GetIssueTitle: getIssueTitle,

View File

@ -1,8 +1,8 @@
import { type Doc, type DocumentUpdate, type Ref, type RelatedDocument, type TxOperations } from '@hcengineering/core' import { type Doc, type DocumentUpdate, type Ref, type RelatedDocument, type TxOperations } from '@hcengineering/core'
import presentation, { getClient } from '@hcengineering/presentation'
import { getMetadata } from '@hcengineering/platform' import { getMetadata } from '@hcengineering/platform'
import { type Component, type Issue, type Project, type Milestone, trackerId } from '@hcengineering/tracker' import presentation, { getClient } from '@hcengineering/presentation'
import { type Location, type ResolvedLocation, getPanelURI, getCurrentResolvedLocation } from '@hcengineering/ui' import { trackerId, type Component, type Issue, type Milestone } from '@hcengineering/tracker'
import { getCurrentResolvedLocation, getPanelURI, type Location, type ResolvedLocation } from '@hcengineering/ui'
import { workbenchId } from '@hcengineering/workbench' import { workbenchId } from '@hcengineering/workbench'
import { writable } from 'svelte/store' import { writable } from 'svelte/store'
import tracker from './plugin' import tracker from './plugin'
@ -10,22 +10,14 @@ import tracker from './plugin'
export const activeComponent = writable<Ref<Component> | undefined>(undefined) export const activeComponent = writable<Ref<Component> | undefined>(undefined)
export const activeMilestone = writable<Ref<Milestone> | undefined>(undefined) export const activeMilestone = writable<Ref<Milestone> | undefined>(undefined)
export function getIssueId (project: Project, issue: Issue): string {
return `${project.identifier}-${issue.number}`
}
export function isIssueId (shortLink: string): boolean { export function isIssueId (shortLink: string): boolean {
return /^\S+-\d+$/.test(shortLink) return /^\S+-\d+$/.test(shortLink)
} }
export async function issueIdentifierProvider (client: TxOperations, ref: Ref<Doc>): Promise<string> { export async function issueIdentifierProvider (client: TxOperations, ref: Ref<Doc>): Promise<string> {
const object = await client.findOne( const object = await client.findOne(tracker.class.Issue, { _id: ref as Ref<Issue> })
tracker.class.Issue, if (object === undefined) throw new Error(`Issue project not found, _id: ${ref}`)
{ _id: ref as Ref<Issue> }, return object.identifier
{ lookup: { space: tracker.class.Project } }
)
if (object?.$lookup?.space === undefined) throw new Error(`Issue project not found, _id: ${ref}`)
return getIssueId(object.$lookup.space, object)
} }
export async function issueTitleProvider (client: TxOperations, ref: Ref<Doc>): Promise<string> { export async function issueTitleProvider (client: TxOperations, ref: Ref<Doc>): Promise<string> {
@ -42,22 +34,15 @@ export async function issueTitleProvider (client: TxOperations, ref: Ref<Doc>):
return await getIssueTitle(object) return await getIssueTitle(object)
} }
async function getTitle (doc: Doc): Promise<string> { export async function getTitle (doc: Doc): Promise<string> {
const client = getClient()
const issue = doc as Issue const issue = doc as Issue
const object = await client.findOne(tracker.class.Project, { _id: issue.space }) return issue.identifier
if (object === undefined) return `?-${issue.number}`
return getIssueId(object, issue)
} }
export function generateIssuePanelUri (issue: Issue): string { export function generateIssuePanelUri (issue: Issue): string {
return getPanelURI(tracker.component.EditIssue, issue._id, issue._class, 'content') return getPanelURI(tracker.component.EditIssue, issue._id, issue._class, 'content')
} }
export async function issueIdProvider (doc: Doc): Promise<string> {
return await getTitle(doc)
}
export async function issueLinkFragmentProvider (doc: Doc): Promise<Location> { export async function issueLinkFragmentProvider (doc: Doc): Promise<Location> {
const loc = getCurrentResolvedLocation() const loc = getCurrentResolvedLocation()
loc.path.length = 2 loc.path.length = 2
@ -85,21 +70,8 @@ export function generateIssueShortLink (issueId: string): string {
} }
export async function generateIssueLocation (loc: Location, issueId: string): Promise<ResolvedLocation | undefined> { export async function generateIssueLocation (loc: Location, issueId: string): Promise<ResolvedLocation | undefined> {
const tokens = issueId.split('-')
if (tokens.length < 2) {
return undefined
}
const projectId = tokens[0]
const issueNumber = Number(tokens[1])
const client = getClient() const client = getClient()
const project = await client.findOne(tracker.class.Project, { identifier: projectId }) const issue = await client.findOne(tracker.class.Issue, { identifier: issueId })
if (project === undefined) {
console.error(
`Could not find project ${projectId}. Make sure you are in correct workspace and the project was not deleted or renamed.`
)
return undefined
}
const issue = await client.findOne(tracker.class.Issue, { number: issueNumber, space: project._id })
if (issue === undefined) { if (issue === undefined) {
console.error(`Could not find issue ${issueId}.`) console.error(`Could not find issue ${issueId}.`)
return undefined return undefined
@ -112,7 +84,7 @@ export async function generateIssueLocation (loc: Location, issueId: string): Pr
fragment: generateIssuePanelUri(issue) fragment: generateIssuePanelUri(issue)
}, },
defaultLocation: { defaultLocation: {
path: [appComponent, workspace, trackerId, project._id, 'issues'], path: [appComponent, workspace, trackerId, issue.space, 'issues'],
fragment: generateIssuePanelUri(issue) fragment: generateIssuePanelUri(issue)
} }
} }

View File

@ -612,6 +612,7 @@ export async function moveIssueToSpace (
}, },
true true
) )
const number = (incResult as any).object.sequence
await updateIssuesOnMove( await updateIssuesOnMove(
client, client,
applyOps, applyOps,
@ -620,7 +621,8 @@ export async function moveIssueToSpace (
{ {
...updates.get(doc._id), ...updates.get(doc._id),
rank: calcRank(lastOne, undefined), rank: calcRank(lastOne, undefined),
number: (incResult as any).object.sequence number,
identifier: `${space.identifier}-${number}`
}, },
updates updates
) )

View File

@ -57,11 +57,10 @@ async function updateSubIssues (
*/ */
export async function issueHTMLPresenter (doc: Doc, control: TriggerControl): Promise<string> { export async function issueHTMLPresenter (doc: Doc, control: TriggerControl): Promise<string> {
const issue = doc as Issue const issue = doc as Issue
const issueId = await getIssueId(issue, control)
const front = getMetadata(serverCore.metadata.FrontUrl) ?? '' const front = getMetadata(serverCore.metadata.FrontUrl) ?? ''
const path = `${workbenchId}/${control.workspace.workspaceUrl}/${trackerId}/${issueId}` const path = `${workbenchId}/${control.workspace.workspaceUrl}/${trackerId}/${issue.identifier}`
const link = concatLink(front, path) const link = concatLink(front, path)
return `<a href="${link}">${issueId}</a> ${issue.title}` return `<a href="${link}">${issue.identifier}</a> ${issue.title}`
} }
/** /**
@ -76,11 +75,9 @@ export async function getIssueId (doc: Issue, control: TriggerControl): Promise<
/** /**
* @public * @public
*/ */
export async function issueTextPresenter (doc: Doc, control: TriggerControl): Promise<string> { export async function issueTextPresenter (doc: Doc): Promise<string> {
const issue = doc as Issue const issue = doc as Issue
const issueId = await getIssueId(issue, control) return `${issue.identifier} ${issue.title}`
return `${issueId} ${issue.title}`
} }
function isSamePerson (control: TriggerControl, assignee: Ref<Person>, target: Ref<Account>): boolean { function isSamePerson (control: TriggerControl, assignee: Ref<Person>, target: Ref<Account>): boolean {
@ -100,7 +97,7 @@ export async function getIssueNotificationContent (
): Promise<NotificationContent> { ): Promise<NotificationContent> {
const issue = doc as Issue const issue = doc as Issue
const issueShortName = await issueTextPresenter(doc, control) const issueShortName = await issueTextPresenter(doc)
const issueTitle = `${issueShortName}: ${issue.title}` const issueTitle = `${issueShortName}: ${issue.title}`
const title = tracker.string.IssueNotificationTitle const title = tracker.string.IssueNotificationTitle

View File

@ -102,9 +102,9 @@ export const contentStageId = 'cnt-v2b'
/** /**
* @public * @public
*/ */
export const fieldStateId = 'fld-v11' export const fieldStateId = 'fld-v12'
/** /**
* @public * @public
*/ */
export const fullTextPushStageId = 'fts-v9_2' export const fullTextPushStageId = 'fts-v10b'

View File

@ -90,11 +90,6 @@ export class EditProjectPage extends CommonTrackerPage {
if (data.title != null) { if (data.title != null) {
await this.inputTitle.fill(data.title) await this.inputTitle.fill(data.title)
} }
if (data.identifier != null) {
await this.buttonEditIdentifier.click()
await this.inputEditProjectIdentifier.fill(data.identifier)
await this.buttonEditProjectIdentifier.click()
}
if (data.description != null) { if (data.description != null) {
await this.inputDescription.fill(data.description) await this.inputDescription.fill(data.description)
} }

View File

@ -48,7 +48,6 @@ test.describe('Tracker Projects tests', () => {
} }
const updateProjectData: NewProject = { const updateProjectData: NewProject = {
title: 'UpdateProject', title: 'UpdateProject',
identifier: 'UPDAT',
description: 'Updated Project description', description: 'Updated Project description',
private: true, private: true,
defaultAssigneeForIssues: 'Chen Rosamund', defaultAssigneeForIssues: 'Chen Rosamund',