UBER-953: Fix related issues (#3821)

Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
Andrey Sobolev 2023-10-11 13:20:39 +07:00 committed by GitHub
parent 8b9c90b388
commit bc3598ce15
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 2503 additions and 1890 deletions

View File

@ -189,6 +189,9 @@ export function createModel (builder: Builder): void {
createAction(builder, { createAction(builder, {
...viewTemplates.open, ...viewTemplates.open,
target: lead.class.Funnel, target: lead.class.Funnel,
query: {
archived: true
},
context: { context: {
mode: ['browser', 'context'], mode: ['browser', 'context'],
group: 'create' group: 'create'
@ -645,4 +648,26 @@ export function createModel (builder: Builder): void {
}, },
lead.action.CreateGlobalLead lead.action.CreateGlobalLead
) )
createAction(builder, {
action: view.actionImpl.ShowPopup,
actionProps: {
component: lead.component.CreateFunnel,
_id: 'customer',
element: 'top',
fillProps: {
_object: 'funnel'
}
},
label: lead.string.EditFunnel,
icon: lead.icon.Funnel,
input: 'focus',
category: lead.category.Lead,
target: lead.class.Funnel,
override: [view.action.Open],
context: {
mode: ['context', 'browser'],
group: 'associate'
}
})
} }

View File

@ -14,9 +14,11 @@
// //
import { Ref, TxOperations } from '@hcengineering/core' import { Ref, TxOperations } from '@hcengineering/core'
import { MigrateOperation, MigrationClient, MigrationUpgradeClient } from '@hcengineering/model' import { leadId } from '@hcengineering/lead'
import { MigrateOperation, MigrationClient, MigrationUpgradeClient, tryUpgrade } from '@hcengineering/model'
import core from '@hcengineering/model-core' import core from '@hcengineering/model-core'
import { createKanbanTemplate, createSequence } from '@hcengineering/model-task' import { createKanbanTemplate, createSequence } from '@hcengineering/model-task'
import tracker from '@hcengineering/model-tracker'
import task, { KanbanTemplate, createStates } from '@hcengineering/task' import task, { KanbanTemplate, createStates } from '@hcengineering/task'
import { PaletteColorIndexes } from '@hcengineering/ui/src/colors' import { PaletteColorIndexes } from '@hcengineering/ui/src/colors'
import lead from './plugin' import lead from './plugin'
@ -120,5 +122,20 @@ export const leadOperation: MigrateOperation = {
async upgrade (client: MigrationUpgradeClient): Promise<void> { async upgrade (client: MigrationUpgradeClient): Promise<void> {
const ops = new TxOperations(client, core.account.System) const ops = new TxOperations(client, core.account.System)
await createDefaults(ops) await createDefaults(ops)
await tryUpgrade(client, leadId, [
{
state: 'related-targets',
func: async (client): Promise<void> => {
const ops = new TxOperations(client, core.account.ConfigUser)
await ops.createDoc(tracker.class.RelatedIssueTarget, core.space.Configuration, {
rule: {
kind: 'classRule',
ofClass: lead.class.Lead
}
})
}
}
])
} }
} }

View File

@ -32,7 +32,8 @@ export default mergeIds(leadId, lead, {
Title: '' as IntlString, Title: '' as IntlString,
ManageFunnelStatuses: '' as IntlString, ManageFunnelStatuses: '' as IntlString,
GotoLeadApplication: '' as IntlString, GotoLeadApplication: '' as IntlString,
ConfigDescription: '' as IntlString ConfigDescription: '' as IntlString,
EditFunnel: '' as IntlString
}, },
component: { component: {
CreateLead: '' as AnyComponent, CreateLead: '' as AnyComponent,

View File

@ -88,8 +88,6 @@ export class TVacancy extends TSpaceWithStates implements Vacancy {
@Prop(Collection(chunter.class.Comment), chunter.string.Comments) @Prop(Collection(chunter.class.Comment), chunter.string.Comments)
comments?: number comments?: number
relations!: number
@Prop(TypeString(), recruit.string.Vacancy) @Prop(TypeString(), recruit.string.Vacancy)
@Index(IndexKind.FullText) @Index(IndexKind.FullText)
@Hidden() @Hidden()

View File

@ -15,12 +15,20 @@
import { getCategories } from '@anticrm/skillset' import { getCategories } from '@anticrm/skillset'
import core, { Doc, Ref, Space, TxOperations } from '@hcengineering/core' import core, { Doc, Ref, Space, TxOperations } from '@hcengineering/core'
import { MigrateOperation, MigrationClient, MigrationUpgradeClient, createOrUpdate } from '@hcengineering/model' import {
MigrateOperation,
MigrationClient,
MigrationUpgradeClient,
createOrUpdate,
tryUpgrade
} from '@hcengineering/model'
import tags, { TagCategory } from '@hcengineering/model-tags' import tags, { TagCategory } from '@hcengineering/model-tags'
import { createKanbanTemplate, createSequence } from '@hcengineering/model-task' import { createKanbanTemplate, createSequence } from '@hcengineering/model-task'
import task, { KanbanTemplate } from '@hcengineering/task' import task, { KanbanTemplate } 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'
import { recruitId } from '@hcengineering/recruit'
import tracker from '@hcengineering/model-tracker'
export const recruitOperation: MigrateOperation = { export const recruitOperation: MigrateOperation = {
async migrate (client: MigrationClient): Promise<void> {}, async migrate (client: MigrationClient): Promise<void> {},
@ -28,6 +36,28 @@ export const recruitOperation: MigrateOperation = {
const tx = new TxOperations(client, core.account.System) const tx = new TxOperations(client, core.account.System)
await createDefaults(tx) await createDefaults(tx)
await fixTemplateSpace(tx) await fixTemplateSpace(tx)
await tryUpgrade(client, recruitId, [
{
state: 'related-targets',
func: async (client): Promise<void> => {
const ops = new TxOperations(client, core.account.ConfigUser)
await ops.createDoc(tracker.class.RelatedIssueTarget, core.space.Configuration, {
rule: {
kind: 'classRule',
ofClass: recruit.class.Vacancy
}
})
await ops.createDoc(tracker.class.RelatedIssueTarget, core.space.Configuration, {
rule: {
kind: 'classRule',
ofClass: recruit.class.Applicant
}
})
}
}
])
} }
} }

View File

@ -0,0 +1,756 @@
//
// Copyright © 2023 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.
//
import contact from '@hcengineering/contact'
import { Builder } from '@hcengineering/model'
import core from '@hcengineering/model-core'
import task from '@hcengineering/model-task'
import view, { actionTemplates, createAction } from '@hcengineering/model-view'
import workbench, { createNavigateAction } from '@hcengineering/model-workbench'
import { IntlString } from '@hcengineering/platform'
import { trackerId } from '@hcengineering/tracker'
import { KeyBinding, ViewAction } from '@hcengineering/view'
import tracker from './plugin'
import tags from '@hcengineering/tags'
import { defaultPriorities, issuePriorities } from '@hcengineering/tracker-resources/src/types'
function createGotoSpecialAction (
builder: Builder,
id: string,
key: KeyBinding,
label: IntlString,
query?: Record<string, string | null>
): void {
createNavigateAction(builder, key, label, tracker.app.Tracker, {
application: trackerId,
mode: 'space',
spaceSpecial: id,
spaceClass: tracker.class.Project,
query
})
}
export function createActions (builder: Builder, issuesId: string, componentsId: string, myIssuesId: string): void {
createGotoSpecialAction(builder, issuesId, 'keyG->keyE', tracker.string.GotoIssues)
createGotoSpecialAction(builder, issuesId, 'keyG->keyA', tracker.string.GotoActive, { mode: 'active' })
createGotoSpecialAction(builder, issuesId, 'keyG->keyB', tracker.string.GotoBacklog, { mode: 'backlog' })
createGotoSpecialAction(builder, componentsId, 'keyG->keyC', tracker.string.GotoComponents)
createNavigateAction(builder, 'keyG->keyM', tracker.string.GotoMyIssues, tracker.app.Tracker, {
application: trackerId,
mode: 'special',
special: myIssuesId
})
createAction(builder, {
action: workbench.actionImpl.Navigate,
actionProps: {
mode: 'app',
application: trackerId
},
label: tracker.string.GotoTrackerApplication,
icon: view.icon.ArrowRight,
input: 'none',
category: view.category.Navigation,
target: core.class.Doc,
context: {
mode: ['workbench', 'browser', 'editor', 'panel', 'popup']
}
})
createAction(
builder,
{
action: tracker.actionImpl.EditWorkflowStatuses,
label: tracker.string.EditWorkflowStatuses,
icon: view.icon.Statuses,
input: 'focus',
category: tracker.category.Tracker,
target: tracker.class.Project,
override: [task.action.EditStatuses],
query: {},
context: {
mode: ['context', 'browser'],
group: 'edit'
}
},
tracker.action.EditWorkflowStatuses
)
createAction(
builder,
{
action: tracker.actionImpl.EditProject,
label: tracker.string.EditProject,
icon: contact.icon.Edit,
input: 'focus',
category: tracker.category.Tracker,
target: tracker.class.Project,
query: {},
context: {
mode: ['context', 'browser'],
group: 'edit'
}
},
tracker.action.EditProject
)
createAction(
builder,
{
action: tracker.actionImpl.DeleteProject,
label: workbench.string.Archive,
icon: view.icon.Archive,
input: 'focus',
category: tracker.category.Tracker,
target: tracker.class.Project,
query: {
archived: false
},
context: {
mode: ['context', 'browser'],
group: 'edit'
},
override: [view.action.Archive, view.action.Delete]
},
tracker.action.DeleteProject
)
createAction(
builder,
{
action: tracker.actionImpl.DeleteProject,
label: workbench.string.Delete,
icon: view.icon.Delete,
input: 'focus',
category: tracker.category.Tracker,
target: tracker.class.Project,
query: {
archived: true
},
context: {
mode: ['context', 'browser'],
group: 'edit'
},
override: [view.action.Archive, view.action.Delete]
},
tracker.action.DeleteProjectClean
)
createAction(builder, {
label: tracker.string.Unarchive,
icon: view.icon.Archive,
action: view.actionImpl.UpdateDocument as ViewAction,
actionProps: {
key: 'archived',
ask: true,
value: false,
label: tracker.string.Unarchive,
message: tracker.string.UnarchiveConfirm
},
input: 'any',
category: tracker.category.Tracker,
query: {
archived: true
},
context: {
mode: ['context', 'browser'],
group: 'tools'
},
target: tracker.class.Project
})
createAction(
builder,
{
action: tracker.actionImpl.DeleteIssue,
label: workbench.string.Delete,
icon: view.icon.Delete,
input: 'any',
category: tracker.category.Tracker,
target: tracker.class.Issue,
context: {
mode: ['context', 'browser'],
group: 'remove'
},
override: [view.action.Delete]
},
tracker.action.DeleteIssue
)
builder.createDoc(
view.class.ActionCategory,
core.space.Model,
{ label: tracker.string.TrackerApplication, visible: true },
tracker.category.Tracker
)
createAction(
builder,
{
action: view.actionImpl.ShowPopup,
actionProps: {
component: tracker.component.CreateIssue,
element: 'top'
},
label: tracker.string.NewIssue,
icon: tracker.icon.NewIssue,
keyBinding: ['keyC'],
input: 'none',
category: tracker.category.Tracker,
target: tracker.class.Issue,
context: {
mode: ['browser'],
application: tracker.app.Tracker,
group: 'create'
}
},
tracker.action.NewIssue
)
createAction(
builder,
{
action: view.actionImpl.ShowPopup,
actionProps: {
component: tracker.component.CreateIssue,
element: 'top',
fillProps: {
_object: 'parentIssue',
space: 'space'
}
},
label: tracker.string.NewSubIssue,
icon: tracker.icon.Subissue,
keyBinding: [],
input: 'focus',
category: tracker.category.Tracker,
target: tracker.class.Issue,
context: {
mode: ['context', 'browser'],
application: tracker.app.Tracker,
group: 'associate'
}
},
tracker.action.NewSubIssue
)
createAction(
builder,
{
action: view.actionImpl.ShowPopup,
actionProps: {
component: tracker.component.SetParentIssueActionPopup,
element: 'top',
fillProps: {
_objects: 'value'
}
},
label: tracker.string.SetParent,
icon: tracker.icon.Parent,
keyBinding: [],
input: 'none',
category: tracker.category.Tracker,
target: tracker.class.Issue,
context: {
mode: ['context'],
application: tracker.app.Tracker,
group: 'associate'
}
},
tracker.action.SetParent
)
createAction(
builder,
{
action: view.actionImpl.ShowPopup,
actionProps: {
component: tracker.component.CreateIssue,
element: 'top',
fillProps: {
_object: 'relatedTo',
space: 'space'
}
},
label: tracker.string.NewRelatedIssue,
icon: tracker.icon.NewIssue,
keyBinding: [],
input: 'focus',
category: tracker.category.Tracker,
target: core.class.Doc,
context: {
mode: ['context', 'browser', 'editor'],
group: 'associate'
}
},
tracker.action.NewRelatedIssue
)
createAction(builder, {
action: view.actionImpl.ShowPopup,
actionPopup: tracker.component.SetParentIssueActionPopup,
actionProps: {
component: tracker.component.SetParentIssueActionPopup,
element: 'top',
fillProps: {
_object: 'value'
}
},
label: tracker.string.SetParent,
icon: tracker.icon.Parent,
keyBinding: [],
input: 'none',
category: tracker.category.Tracker,
target: tracker.class.Issue,
override: [tracker.action.SetParent],
context: {
mode: ['browser'],
application: tracker.app.Tracker,
group: 'associate'
}
})
createAction(builder, {
...actionTemplates.open,
actionProps: {
component: tracker.component.EditIssue
},
target: tracker.class.Issue,
context: {
mode: ['browser', 'context'],
group: 'create'
},
override: [view.action.Open]
})
createAction(builder, {
...actionTemplates.open,
actionProps: {
component: tracker.component.EditIssueTemplate
},
target: tracker.class.IssueTemplate,
context: {
mode: ['browser', 'context'],
group: 'create'
},
override: [view.action.Open]
})
createAction(builder, {
action: view.actionImpl.ShowPopup,
actionProps: {
component: tracker.component.TimeSpendReportPopup,
fillProps: {
_object: 'issue'
}
},
label: tracker.string.TimeSpendReportAdd,
icon: tracker.icon.TimeReport,
input: 'focus',
keyBinding: ['keyT'],
category: tracker.category.Tracker,
target: tracker.class.Issue,
context: {
mode: ['context', 'browser'],
application: tracker.app.Tracker,
group: 'edit'
}
})
createAction(
builder,
{
action: task.actionImpl.SelectStatus,
actionPopup: task.component.StatusSelector,
actionProps: {
_class: tracker.class.IssueStatus,
ofAttribute: tracker.attribute.IssueStatus,
placeholder: tracker.string.Status
},
label: tracker.string.Status,
icon: tracker.icon.CategoryBacklog,
keyBinding: ['keyS->keyS'],
input: 'any',
category: tracker.category.Tracker,
target: tracker.class.Issue,
context: {
mode: ['context', 'browser'],
application: tracker.app.Tracker,
group: 'edit'
}
},
tracker.action.SetStatus
)
createAction(
builder,
{
action: view.actionImpl.ValueSelector,
actionPopup: view.component.ValueSelector,
actionProps: {
attribute: 'priority',
values: defaultPriorities.map((p) => ({ id: p, ...issuePriorities[p] })),
placeholder: tracker.string.SetPriority
},
label: tracker.string.Priority,
icon: tracker.icon.PriorityHigh,
keyBinding: ['keyP->keyR'],
input: 'any',
category: tracker.category.Tracker,
target: tracker.class.Issue,
context: {
mode: ['context', 'browser'],
application: tracker.app.Tracker,
group: 'edit'
}
},
tracker.action.SetPriority
)
createAction(
builder,
{
action: view.actionImpl.ValueSelector,
actionPopup: view.component.ValueSelector,
actionProps: {
attribute: 'assignee',
_class: contact.mixin.Employee,
query: {},
placeholder: tracker.string.AssignTo
},
label: tracker.string.Assignee,
icon: contact.icon.Person,
keyBinding: ['keyA'],
input: 'any',
category: tracker.category.Tracker,
target: tracker.class.Issue,
context: {
mode: ['context', 'browser'],
application: tracker.app.Tracker,
group: 'edit'
}
},
tracker.action.SetAssignee
)
createAction(
builder,
{
action: view.actionImpl.ValueSelector,
actionPopup: view.component.ValueSelector,
actionProps: {
attribute: 'component',
_class: tracker.class.Component,
query: {},
fillQuery: { space: 'space' },
docMatches: ['space'],
searchField: 'label',
placeholder: tracker.string.Component
},
label: tracker.string.Component,
icon: tracker.icon.Component,
keyBinding: ['keyM->keyT'],
input: 'any',
category: tracker.category.Tracker,
target: tracker.class.Issue,
context: {
mode: ['context', 'browser'],
application: tracker.app.Tracker,
group: 'edit'
}
},
tracker.action.SetComponent
)
createAction(
builder,
{
action: view.actionImpl.AttributeSelector,
actionPopup: tracker.component.MilestoneEditor,
actionProps: {
attribute: 'milestone',
isAction: true
},
label: tracker.string.Milestone,
icon: tracker.icon.Milestone,
keyBinding: ['keyS->keyP'],
input: 'any',
category: tracker.category.Tracker,
target: tracker.class.Issue,
context: {
mode: ['context', 'browser'],
application: tracker.app.Tracker,
group: 'edit'
}
},
tracker.action.SetMilestone
)
createAction(
builder,
{
action: view.actionImpl.ShowPopup,
actionProps: {
component: tags.component.TagsEditorPopup,
element: 'top',
fillProps: {
_object: 'object'
}
},
label: tracker.string.Labels,
icon: tags.icon.Tags,
keyBinding: ['keyL'],
input: 'focus',
category: tracker.category.Tracker,
target: tracker.class.Issue,
context: {
mode: ['context', 'browser'],
application: tracker.app.Tracker,
group: 'edit'
}
},
tracker.action.SetLabels
)
createAction(
builder,
{
action: view.actionImpl.ShowPopup,
actionProps: {
component: tags.component.ObjectsTagsEditorPopup,
element: 'top',
fillProps: {
_objects: 'value'
}
},
label: tracker.string.Labels,
icon: tags.icon.Tags,
keyBinding: ['keyL'],
input: 'selection',
category: tracker.category.Tracker,
target: tracker.class.Issue,
context: {
mode: ['context', 'browser'],
application: tracker.app.Tracker,
group: 'edit'
}
},
tracker.action.SetLabels
)
createAction(
builder,
{
action: view.actionImpl.ShowPopup,
actionProps: {
component: tracker.component.SetDueDateActionPopup,
props: { mondayStart: true, withTime: false },
element: 'top',
fillProps: {
_objects: 'value'
}
},
label: tracker.string.SetDueDate,
icon: tracker.icon.DueDate,
keyBinding: ['keyD'],
input: 'any',
category: tracker.category.Tracker,
target: tracker.class.Issue,
context: {
mode: ['context', 'browser'],
application: tracker.app.Tracker,
group: 'edit'
}
},
tracker.action.SetDueDate
)
createAction(
builder,
{
action: view.actionImpl.CopyTextToClipboard,
actionProps: {
textProvider: tracker.function.GetIssueId
},
label: tracker.string.CopyIssueId,
icon: view.icon.CopyId,
keyBinding: [],
input: 'focus',
category: tracker.category.Tracker,
target: tracker.class.Issue,
context: {
mode: ['context', 'browser'],
application: tracker.app.Tracker,
group: 'copy'
}
},
tracker.action.CopyIssueId
)
createAction(
builder,
{
action: view.actionImpl.CopyTextToClipboard,
actionProps: {
textProvider: tracker.function.GetIssueTitle
},
label: tracker.string.CopyIssueTitle,
icon: tracker.icon.CopyBranch,
keyBinding: [],
input: 'focus',
category: tracker.category.Tracker,
target: tracker.class.Issue,
context: {
mode: ['context', 'browser'],
application: tracker.app.Tracker,
group: 'copy'
}
},
tracker.action.CopyIssueTitle
)
createAction(
builder,
{
action: view.actionImpl.CopyTextToClipboard,
actionProps: {
textProvider: tracker.function.GetIssueLink
},
label: tracker.string.CopyIssueUrl,
icon: view.icon.CopyLink,
keyBinding: [],
input: 'focus',
category: tracker.category.Tracker,
target: tracker.class.Issue,
context: {
mode: ['context', 'browser'],
application: tracker.app.Tracker,
group: 'copy'
}
},
tracker.action.CopyIssueLink
)
createAction(
builder,
{
action: tracker.actionImpl.Move,
label: tracker.string.MoveToProject,
icon: view.icon.Move,
keyBinding: [],
input: 'any',
category: tracker.category.Tracker,
target: tracker.class.Issue,
context: {
mode: ['context', 'browser'],
application: tracker.app.Tracker,
group: 'associate'
},
override: [task.action.Move]
},
tracker.action.MoveToProject
)
createAction(
builder,
{
action: view.actionImpl.ValueSelector,
actionPopup: tracker.component.RelationsPopup,
actionProps: {
attribute: ''
},
label: tracker.string.Relations,
icon: tracker.icon.Relations,
keyBinding: [],
input: 'focus',
category: tracker.category.Tracker,
target: tracker.class.Issue,
context: {
mode: ['context', 'browser'],
application: tracker.app.Tracker,
group: 'associate'
}
},
tracker.action.Relations
)
createAction(
builder,
{
action: view.actionImpl.ShowPopup,
actionProps: {
component: tracker.component.CreateIssue,
element: 'top',
fillProps: {
_object: 'originalIssue',
space: 'space'
}
},
label: tracker.string.Duplicate,
icon: tracker.icon.Duplicate,
keyBinding: [],
input: 'focus',
category: tracker.category.Tracker,
target: tracker.class.Issue,
context: {
mode: ['context', 'browser'],
application: tracker.app.Tracker,
group: 'associate'
}
},
tracker.action.Duplicate
)
createAction(
builder,
{
action: tracker.actionImpl.DeleteMilestone,
label: view.string.Delete,
icon: view.icon.Delete,
keyBinding: ['Meta + Backspace'],
category: tracker.category.Tracker,
input: 'any',
target: tracker.class.Milestone,
context: { mode: ['context', 'browser'], group: 'remove' }
},
tracker.action.DeleteMilestone
)
builder.mixin(tracker.class.Project, core.class.Class, view.mixin.IgnoreActions, {
actions: [view.action.Open]
})
builder.mixin(tracker.class.Milestone, core.class.Class, view.mixin.IgnoreActions, {
actions: [view.action.Delete]
})
createAction(
builder,
{
action: view.actionImpl.ShowPopup,
actionProps: {
component: tracker.component.EditRelatedTargetsPopup,
element: 'top',
fillProps: {
_objects: 'value'
}
},
label: tracker.string.MapRelatedIssues,
icon: tracker.icon.Relations,
keyBinding: [],
input: 'none',
category: tracker.category.Tracker,
target: core.class.Space,
query: {
_class: { $nin: [tracker.class.Project] }
},
context: {
mode: ['context'],
application: tracker.app.Tracker,
group: 'associate'
}
},
tracker.action.EditRelatedTargets
)
}

File diff suppressed because it is too large Load Diff

View File

@ -34,7 +34,7 @@ import {
import { DOMAIN_TASK } from '@hcengineering/model-task' import { DOMAIN_TASK } from '@hcengineering/model-task'
import tags from '@hcengineering/tags' import tags from '@hcengineering/tags'
import { Issue, Project, TimeReportDayType, TimeSpendReport, createStatuses } from '@hcengineering/tracker' import { Issue, Project, TimeReportDayType, TimeSpendReport, createStatuses } from '@hcengineering/tracker'
import { DOMAIN_TRACKER } from '.' import { DOMAIN_TRACKER } from './types'
import tracker from './plugin' import tracker from './plugin'
import { DOMAIN_SPACE } from '@hcengineering/model-core' import { DOMAIN_SPACE } from '@hcengineering/model-core'
import view from '@hcengineering/view' import view from '@hcengineering/view'

View File

@ -43,7 +43,8 @@ export default mergeIds(trackerId, tracker, {
Unarchive: '' as IntlString, Unarchive: '' as IntlString,
UnarchiveConfirm: '' as IntlString, UnarchiveConfirm: '' as IntlString,
AllProjects: '' as IntlString, AllProjects: '' as IntlString,
RemainingTime: '' as IntlString RemainingTime: '' as IntlString,
MapRelatedIssues: '' as IntlString
}, },
activity: { activity: {
TxIssueCreated: '' as AnyComponent, TxIssueCreated: '' as AnyComponent,
@ -55,7 +56,9 @@ export default mergeIds(trackerId, tracker, {
IssueStatistics: '' as AnyComponent, IssueStatistics: '' as AnyComponent,
TimeSpendReportPopup: '' as AnyComponent, TimeSpendReportPopup: '' as AnyComponent,
NotificationIssuePresenter: '' as AnyComponent, NotificationIssuePresenter: '' as AnyComponent,
MilestoneFilter: '' as AnyComponent MilestoneFilter: '' as AnyComponent,
EditRelatedTargets: '' as AnyComponent,
EditRelatedTargetsPopup: '' as AnyComponent
}, },
app: { app: {
Tracker: '' as Ref<Application> Tracker: '' as Ref<Application>

View File

@ -0,0 +1,144 @@
//
// Copyright © 2023 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.
//
import { Builder } from '@hcengineering/model'
import core from '@hcengineering/model-core'
import view, { classPresenter } from '@hcengineering/model-view'
import notification from '@hcengineering/notification'
import tracker from './plugin'
/**
* Define presenters
*/
export function definePresenters (builder: Builder): void {
//
// Issue
//
builder.mixin(tracker.class.Issue, core.class.Class, view.mixin.ObjectPresenter, {
presenter: tracker.component.IssuePresenter
})
builder.mixin(tracker.class.Issue, core.class.Class, notification.mixin.NotificationObjectPresenter, {
presenter: tracker.component.NotificationIssuePresenter
})
builder.mixin(tracker.class.Issue, core.class.Class, view.mixin.PreviewPresenter, {
presenter: tracker.component.IssuePreview
})
//
// Issue Template
//
builder.mixin(tracker.class.IssueTemplate, core.class.Class, view.mixin.ObjectPresenter, {
presenter: tracker.component.IssueTemplatePresenter
})
//
// Issue Status
//
builder.mixin(tracker.class.IssueStatus, core.class.Class, view.mixin.ObjectPresenter, {
presenter: tracker.component.StatusPresenter
})
builder.mixin(tracker.class.IssueStatus, core.class.Class, view.mixin.AttributePresenter, {
presenter: tracker.component.StatusRefPresenter
})
//
// Time Spend Report
//
builder.mixin(tracker.class.TimeSpendReport, core.class.Class, view.mixin.ObjectPresenter, {
presenter: tracker.component.TimeSpendReport
})
//
// Type Milestone Status
//
builder.mixin(tracker.class.TypeMilestoneStatus, core.class.Class, view.mixin.AttributePresenter, {
presenter: tracker.component.MilestoneStatusPresenter
})
builder.mixin(tracker.class.TypeMilestoneStatus, core.class.Class, view.mixin.AttributeEditor, {
inlineEditor: tracker.component.MilestoneStatusEditor
})
//
// Type Issue Priority
//
builder.mixin(tracker.class.TypeIssuePriority, core.class.Class, view.mixin.ObjectPresenter, {
presenter: tracker.component.PriorityPresenter
})
builder.mixin(tracker.class.TypeIssuePriority, core.class.Class, view.mixin.AttributePresenter, {
presenter: tracker.component.PriorityRefPresenter
})
//
// Project
//
builder.mixin(tracker.class.Project, core.class.Class, view.mixin.ObjectPresenter, {
presenter: tracker.component.ProjectPresenter
})
builder.mixin(tracker.class.Project, core.class.Class, view.mixin.SpacePresenter, {
presenter: tracker.component.ProjectSpacePresenter
})
//
// Component
//
builder.mixin(tracker.class.Component, core.class.Class, view.mixin.ObjectEditor, {
editor: tracker.component.EditComponent
})
builder.mixin(tracker.class.Component, core.class.Class, view.mixin.ObjectPresenter, {
presenter: tracker.component.ComponentPresenter
})
classPresenter(
builder,
tracker.class.Component,
tracker.component.ComponentSelector,
tracker.component.ComponentSelector
)
builder.mixin(tracker.class.Component, core.class.Class, view.mixin.AttributeEditor, {
inlineEditor: tracker.component.ComponentSelector
})
builder.mixin(tracker.class.Component, core.class.Class, view.mixin.AttributePresenter, {
presenter: tracker.component.ComponentRefPresenter
})
/// Milestones
builder.mixin(tracker.class.Milestone, core.class.Class, view.mixin.ObjectPresenter, {
presenter: tracker.component.MilestonePresenter
})
builder.mixin(tracker.class.Milestone, core.class.Class, view.mixin.AttributePresenter, {
presenter: tracker.component.MilestoneRefPresenter
})
builder.mixin(tracker.class.Milestone, core.class.Class, view.mixin.ObjectEditor, {
editor: tracker.component.EditMilestone
})
classPresenter(
builder,
tracker.class.TypeReportedTime,
view.component.NumberPresenter,
tracker.component.ReportedTimeEditor
)
}

388
models/tracker/src/types.ts Normal file
View File

@ -0,0 +1,388 @@
//
// Copyright © 2023 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.
//
import contact, { Employee, Person } from '@hcengineering/contact'
import {
DOMAIN_MODEL,
DateRangeMode,
Domain,
IndexKind,
Markup,
Ref,
RelatedDocument,
Timestamp,
Type
} from '@hcengineering/core'
import {
ArrOf,
Collection,
Hidden,
Index,
Mixin,
Model,
Prop,
ReadOnly,
TypeDate,
TypeMarkup,
TypeNumber,
TypeRef,
TypeString,
UX
} from '@hcengineering/model'
import attachment from '@hcengineering/model-attachment'
import chunter from '@hcengineering/model-chunter'
import core, { TAttachedDoc, TClass, TDoc, TStatus, TType } from '@hcengineering/model-core'
import task, { TSpaceWithStates, TTask } from '@hcengineering/model-task'
import { IntlString, Resource } from '@hcengineering/platform'
import tags, { TagElement } from '@hcengineering/tags'
import { DoneState } from '@hcengineering/task'
import {
Component,
Issue,
IssueChildInfo,
IssueParentInfo,
IssuePriority,
IssueStatus,
IssueTemplate,
IssueTemplateChild,
IssueUpdateFunction,
Milestone,
MilestoneStatus,
Project,
ProjectIssueTargetOptions,
RelatedClassRule,
RelatedIssueTarget,
RelatedSpaceRule,
TimeReportDayType,
TimeSpendReport
} from '@hcengineering/tracker'
import { AnyComponent } from '@hcengineering/ui'
import tracker from './plugin'
export const DOMAIN_TRACKER = 'tracker' as Domain
/**
* @public
*/
@Model(tracker.class.IssueStatus, core.class.Status)
@UX(tracker.string.IssueStatuses, undefined, undefined, 'rank', 'name')
export class TIssueStatus extends TStatus implements IssueStatus {}
/**
* @public
*/
export function TypeIssuePriority (): Type<IssuePriority> {
return { _class: tracker.class.TypeIssuePriority, label: tracker.string.TypeIssuePriority }
}
/**
* @public
*/
@Model(tracker.class.TypeIssuePriority, core.class.Type, DOMAIN_MODEL)
export class TTypeIssuePriority extends TType {}
/**
* @public
*/
export function TypeMilestoneStatus (): Type<MilestoneStatus> {
return { _class: tracker.class.TypeMilestoneStatus, label: 'TypeMilestoneStatus' as IntlString }
}
/**
* @public
*/
@Model(tracker.class.TypeMilestoneStatus, core.class.Type, DOMAIN_MODEL)
export class TTypeMilestoneStatus extends TType {}
/**
* @public
*/
@Model(tracker.class.Project, task.class.SpaceWithStates)
@UX(tracker.string.Project, tracker.icon.Issues, 'Project', 'name')
export class TProject extends TSpaceWithStates implements Project {
@Prop(TypeString(), tracker.string.ProjectIdentifier)
@Index(IndexKind.FullText)
identifier!: IntlString
@Prop(TypeNumber(), tracker.string.Number)
@Hidden()
sequence!: number
@Prop(TypeRef(tracker.class.IssueStatus), tracker.string.DefaultIssueStatus)
defaultIssueStatus!: Ref<IssueStatus>
@Prop(TypeRef(contact.mixin.Employee), tracker.string.DefaultAssignee)
defaultAssignee!: Ref<Employee>
declare defaultTimeReportDay: TimeReportDayType
@Prop(Collection(tracker.class.RelatedIssueTarget), tracker.string.RelatedIssue)
relatedIssueTargets!: number
}
/**
* @public
*/
@Model(tracker.class.RelatedIssueTarget, core.class.Doc, DOMAIN_TRACKER)
@UX(tracker.string.RelatedIssues)
export class TRelatedIssueTarget extends TDoc implements RelatedIssueTarget {
@Prop(TypeRef(tracker.class.Project), tracker.string.Project)
target!: Ref<Project>
rule!: RelatedClassRule | RelatedSpaceRule
}
/**
* @public
*/
export function TypeReportedTime (): Type<number> {
return { _class: tracker.class.TypeReportedTime, label: core.string.Number }
}
/**
* @public
*/
@Model(tracker.class.Issue, task.class.Task)
@UX(tracker.string.Issue, tracker.icon.Issue, 'TSK', 'title')
export class TIssue extends TTask implements Issue {
@Prop(TypeRef(tracker.class.Issue), tracker.string.Parent)
declare attachedTo: Ref<Issue>
@Prop(TypeString(), tracker.string.Title)
@Index(IndexKind.FullText)
title!: string
@Prop(TypeMarkup(), tracker.string.Description)
@Index(IndexKind.FullText)
description!: Markup
@Prop(TypeRef(tracker.class.IssueStatus), tracker.string.Status, {
_id: tracker.attribute.IssueStatus,
iconComponent: tracker.activity.StatusIcon
})
@Index(IndexKind.Indexed)
declare status: Ref<IssueStatus>
@Prop(TypeIssuePriority(), tracker.string.Priority, {
iconComponent: tracker.activity.PriorityIcon
})
@Index(IndexKind.Indexed)
priority!: IssuePriority
@Prop(TypeNumber(), tracker.string.Number)
@Index(IndexKind.FullText)
@ReadOnly()
declare number: number
@Prop(TypeRef(contact.class.Person), tracker.string.Assignee)
@Index(IndexKind.Indexed)
declare assignee: Ref<Person> | null
@Prop(TypeRef(tracker.class.Component), tracker.string.Component, { icon: tracker.icon.Component })
@Index(IndexKind.Indexed)
component!: Ref<Component> | null
@Prop(Collection(tracker.class.Issue), tracker.string.SubIssues)
subIssues!: number
@Prop(ArrOf(TypeRef(core.class.TypeRelatedDocument)), tracker.string.BlockedBy)
blockedBy!: RelatedDocument[]
@Prop(ArrOf(TypeRef(core.class.TypeRelatedDocument)), tracker.string.RelatedTo)
@Index(IndexKind.Indexed)
relations!: RelatedDocument[]
parents!: IssueParentInfo[]
@Prop(Collection(tags.class.TagReference), tracker.string.Labels)
declare labels: number
@Prop(TypeRef(task.class.DoneState), task.string.TaskStateDone, { _id: task.attribute.DoneState })
@Hidden()
declare doneState: Ref<DoneState> | null
@Prop(TypeRef(tracker.class.Project), tracker.string.Project, { icon: tracker.icon.Issues })
@Index(IndexKind.Indexed)
@ReadOnly()
declare space: Ref<Project>
@Prop(TypeDate(DateRangeMode.DATETIME), tracker.string.DueDate)
declare dueDate: Timestamp | null
@Prop(TypeString(), tracker.string.Rank)
@Hidden()
declare rank: string
@Prop(TypeRef(tracker.class.Milestone), tracker.string.Milestone, { icon: tracker.icon.Milestone })
@Index(IndexKind.Indexed)
milestone!: Ref<Milestone> | null
@Prop(TypeNumber(), tracker.string.Estimation)
estimation!: number
@Prop(TypeReportedTime(), tracker.string.ReportedTime)
@ReadOnly()
reportedTime!: number
// A fully virtual property with calculated content.
// TODO: Add proper support for this kind of fields
@Prop(TypeNumber(), tracker.string.RemainingTime)
@ReadOnly()
@Hidden()
remainingTime!: number
@Prop(Collection(tracker.class.TimeSpendReport), tracker.string.TimeSpendReports)
reports!: number
declare childInfo: IssueChildInfo[]
}
/**
* @public
*/
@Model(tracker.class.IssueTemplate, core.class.Doc, DOMAIN_TRACKER)
@UX(tracker.string.IssueTemplate, tracker.icon.Issue, 'PROCESS')
export class TIssueTemplate extends TDoc implements IssueTemplate {
@Prop(TypeString(), tracker.string.Title)
@Index(IndexKind.FullText)
title!: string
@Prop(TypeMarkup(), tracker.string.Description)
@Index(IndexKind.FullText)
description!: Markup
@Prop(TypeIssuePriority(), tracker.string.Priority)
priority!: IssuePriority
@Prop(TypeRef(contact.class.Person), tracker.string.Assignee)
assignee!: Ref<Person> | null
@Prop(TypeRef(tracker.class.Component), tracker.string.Component)
component!: Ref<Component> | null
@Prop(ArrOf(TypeRef(tags.class.TagElement)), tracker.string.Labels)
labels?: Ref<TagElement>[]
declare space: Ref<Project>
@Prop(TypeDate(DateRangeMode.DATETIME), tracker.string.DueDate)
dueDate!: Timestamp | null
@Prop(TypeRef(tracker.class.Milestone), tracker.string.Milestone)
milestone!: Ref<Milestone> | null
@Prop(TypeNumber(), tracker.string.Estimation)
estimation!: number
@Prop(ArrOf(TypeRef(tracker.class.IssueTemplate)), tracker.string.IssueTemplate)
children!: IssueTemplateChild[]
@Prop(Collection(chunter.class.Comment), tracker.string.Comments)
comments!: number
@Prop(Collection(attachment.class.Attachment), tracker.string.Attachments)
attachments!: number
@Prop(ArrOf(TypeRef(core.class.TypeRelatedDocument)), tracker.string.RelatedTo)
relations!: RelatedDocument[]
}
/**
* @public
*/
@Model(tracker.class.TimeSpendReport, core.class.AttachedDoc, DOMAIN_TRACKER)
@UX(tracker.string.TimeSpendReport, tracker.icon.TimeReport)
export class TTimeSpendReport extends TAttachedDoc implements TimeSpendReport {
@Prop(TypeRef(tracker.class.Issue), tracker.string.Issue)
declare attachedTo: Ref<Issue>
@Prop(TypeRef(contact.mixin.Employee), contact.string.Employee)
employee!: Ref<Employee>
@Prop(TypeDate(), tracker.string.TimeSpendReportDate)
date!: Timestamp | null
@Prop(TypeNumber(), tracker.string.TimeSpendReportValue)
value!: number
@Prop(TypeString(), tracker.string.TimeSpendReportDescription)
description!: string
}
/**
* @public
*/
@Model(tracker.class.Component, core.class.Doc, DOMAIN_TRACKER)
@UX(tracker.string.Component, tracker.icon.Component, 'COMPONENT', 'label')
export class TComponent extends TDoc implements Component {
@Prop(TypeString(), tracker.string.Title)
@Index(IndexKind.FullText)
label!: string
@Prop(TypeMarkup(), tracker.string.Description)
description?: Markup
@Prop(TypeRef(contact.mixin.Employee), tracker.string.ComponentLead)
lead!: Ref<Employee> | null
@Prop(Collection(chunter.class.Comment), chunter.string.Comments)
comments!: number
@Prop(Collection(attachment.class.Attachment), attachment.string.Attachments, { shortLabel: attachment.string.Files })
attachments?: number
declare space: Ref<Project>
}
@Mixin(tracker.mixin.ProjectIssueTargetOptions, core.class.Class)
export class TProjectIssueTargetOptions extends TClass implements ProjectIssueTargetOptions {
headerComponent!: AnyComponent
bodyComponent!: AnyComponent
footerComponent!: AnyComponent
update!: Resource<IssueUpdateFunction>
}
/**
* @public
*/
@Model(tracker.class.Milestone, core.class.Doc, DOMAIN_TRACKER)
@UX(tracker.string.Milestone, tracker.icon.Milestone, '', 'label')
export class TMilestone extends TDoc implements Milestone {
@Prop(TypeString(), tracker.string.Title)
// @Index(IndexKind.FullText)
label!: string
@Prop(TypeMarkup(), tracker.string.Description)
description?: Markup
@Prop(TypeMilestoneStatus(), tracker.string.Status)
@Index(IndexKind.Indexed)
status!: MilestoneStatus
@Prop(Collection(chunter.class.Comment), chunter.string.Comments)
comments!: number
@Prop(Collection(attachment.class.Attachment), attachment.string.Attachments, { shortLabel: attachment.string.Files })
attachments?: number
@Prop(TypeDate(), tracker.string.TargetDate)
targetDate!: Timestamp
declare space: Ref<Project>
}
@UX(core.string.Number)
@Model(tracker.class.TypeReportedTime, core.class.Type)
export class TTypeReportedTime extends TType {}

View File

@ -0,0 +1,542 @@
//
// Copyright © 2023 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.
//
import contact from '@hcengineering/contact'
import { SortingOrder } from '@hcengineering/core'
import { Builder } from '@hcengineering/model'
import core from '@hcengineering/model-core'
import task from '@hcengineering/model-task'
import view, { showColorsViewOption } from '@hcengineering/model-view'
import { BuildModelKey, ViewOptionsModel } from '@hcengineering/view'
import tracker from './plugin'
import tags from '@hcengineering/tags'
export const issuesOptions = (kanban: boolean): ViewOptionsModel => ({
groupBy: ['status', 'assignee', 'priority', 'component', 'milestone', 'createdBy', 'modifiedBy'],
orderBy: [
['status', SortingOrder.Ascending],
['priority', SortingOrder.Descending],
['modifiedOn', SortingOrder.Descending],
['createdOn', SortingOrder.Descending],
['dueDate', SortingOrder.Ascending],
['rank', SortingOrder.Ascending]
],
other: [
{
key: 'shouldShowSubIssues',
type: 'toggle',
defaultValue: true,
actionTarget: 'query',
action: tracker.function.SubIssueQuery,
label: tracker.string.SubIssues
},
{
key: 'shouldShowAll',
type: 'toggle',
defaultValue: false,
actionTarget: 'category',
action: view.function.ShowEmptyGroups,
label: view.string.ShowEmptyGroups
},
...(!kanban ? [showColorsViewOption] : [])
]
})
export function issueConfig (
key: string = '',
compact: boolean = false,
milestone: boolean = true,
component: boolean = true
): (BuildModelKey | string)[] {
return [
{
key: '',
label: tracker.string.Priority,
presenter: tracker.component.PriorityEditor,
props: { type: 'priority', kind: 'list', size: 'small' },
displayProps: { key: 'priority' }
},
{
key: '',
label: tracker.string.Identifier,
presenter: tracker.component.IssuePresenter,
displayProps: { key: key + 'issue', fixed: 'left' }
},
{
key: '',
label: tracker.string.Status,
presenter: tracker.component.StatusEditor,
props: { kind: 'list', size: 'small', justify: 'center' },
displayProps: { key: key + 'status' }
},
{
key: '',
label: tracker.string.Title,
presenter: tracker.component.TitlePresenter,
props: compact ? { shouldUseMargin: true, showParent: false } : {},
displayProps: { key: key + 'title' }
},
{
key: '',
label: tracker.string.SubIssues,
presenter: tracker.component.SubIssuesSelector,
props: {}
},
{ key: 'comments', displayProps: { key: key + 'comments', suffix: true } },
{ key: 'attachments', displayProps: { key: key + 'attachments', suffix: true } },
{ key: '', displayProps: { grow: true } },
{
key: 'labels',
presenter: tags.component.LabelsPresenter,
displayProps: { compression: true },
props: { kind: 'list', full: false }
},
...(milestone
? [
{
key: '',
label: tracker.string.Milestone,
presenter: tracker.component.MilestoneEditor,
props: {
kind: 'list',
size: 'small',
shouldShowPlaceholder: false
},
displayProps: {
key: key + 'milestone',
excludeByKey: 'milestone',
compression: true
}
}
]
: []),
...(component
? [
{
key: '',
label: tracker.string.Component,
presenter: tracker.component.ComponentEditor,
props: {
kind: 'list',
size: 'small',
shouldShowPlaceholder: false
},
displayProps: {
key: key + 'component',
excludeByKey: 'component',
compression: true
}
}
]
: []),
{
key: '',
label: tracker.string.DueDate,
presenter: tracker.component.DueDatePresenter,
displayProps: { key: key + 'dueDate', compression: true },
props: { kind: 'list' }
},
{
key: '',
label: tracker.string.Estimation,
presenter: tracker.component.EstimationEditor,
props: { kind: 'list', size: 'small' },
displayProps: { key: key + 'estimation', fixed: 'left', dividerBefore: true, optional: true }
},
{
key: 'modifiedOn',
presenter: tracker.component.ModificationDatePresenter,
displayProps: { key: key + 'modified', fixed: 'left', dividerBefore: true }
},
{
key: 'assignee',
presenter: tracker.component.AssigneeEditor,
displayProps: { key: 'assignee', fixed: 'right' },
props: { kind: 'list', shouldShowName: false, avatarSize: 'x-small' }
}
]
}
export function defineViewlets (builder: Builder): void {
builder.createDoc(
view.class.ViewletDescriptor,
core.space.Model,
{
label: tracker.string.Board,
icon: task.icon.Kanban,
component: tracker.component.KanbanView
},
tracker.viewlet.Kanban
)
builder.createDoc(
view.class.Viewlet,
core.space.Model,
{
attachTo: tracker.class.Issue,
descriptor: view.viewlet.List,
viewOptions: issuesOptions(false),
configOptions: {
strict: true,
hiddenKeys: [
'title',
'blockedBy',
'relations',
'description',
'number',
'reportedTime',
'reports',
'priority',
'component',
'milestone',
'estimation',
'status',
'dueDate',
'attachedTo',
'createdBy',
'modifiedBy'
]
},
config: issueConfig()
},
tracker.viewlet.IssueList
)
const subIssuesOptions: ViewOptionsModel = {
groupBy: ['status', 'assignee', 'priority', 'milestone', 'createdBy', 'modifiedBy'],
orderBy: [
['rank', SortingOrder.Ascending],
['status', SortingOrder.Ascending],
['priority', SortingOrder.Ascending],
['modifiedOn', SortingOrder.Descending],
['createdOn', SortingOrder.Descending],
['dueDate', SortingOrder.Ascending]
],
groupDepth: 1,
other: [showColorsViewOption]
}
builder.createDoc(
view.class.Viewlet,
core.space.Model,
{
attachTo: tracker.class.Issue,
descriptor: view.viewlet.List,
viewOptions: subIssuesOptions,
variant: 'subissue',
configOptions: {
strict: true,
hiddenKeys: [
'priority',
'number',
'status',
'title',
'dueDate',
'milestone',
'estimation',
'createdBy',
'modifiedBy'
]
},
config: issueConfig('sub', true, true)
},
tracker.viewlet.SubIssues
)
const milestoneIssueOptions: ViewOptionsModel = {
groupBy: ['status', 'assignee', 'priority', 'component', 'createdBy', 'modifiedBy'],
orderBy: [
['rank', SortingOrder.Ascending],
['status', SortingOrder.Ascending],
['priority', SortingOrder.Ascending],
['modifiedOn', SortingOrder.Descending],
['createdOn', SortingOrder.Descending],
['dueDate', SortingOrder.Ascending]
],
groupDepth: 1,
other: [showColorsViewOption]
}
builder.createDoc(
view.class.Viewlet,
core.space.Model,
{
attachTo: tracker.class.Issue,
descriptor: view.viewlet.List,
viewOptions: milestoneIssueOptions,
variant: 'milestone',
configOptions: {
strict: true,
hiddenKeys: [
'priority',
'number',
'status',
'title',
'dueDate',
'milestone',
'estimation',
'createdBy',
'modifiedBy'
]
},
config: issueConfig('sub', true, false, true)
},
tracker.viewlet.MilestoneIssuesList
)
const componentIssueOptions: ViewOptionsModel = {
groupBy: ['status', 'assignee', 'priority', 'milestone', 'createdBy', 'modifiedBy'],
orderBy: [
['rank', SortingOrder.Ascending],
['status', SortingOrder.Ascending],
['priority', SortingOrder.Ascending],
['modifiedOn', SortingOrder.Descending],
['createdOn', SortingOrder.Descending],
['dueDate', SortingOrder.Ascending]
],
groupDepth: 1,
other: [showColorsViewOption]
}
builder.createDoc(
view.class.Viewlet,
core.space.Model,
{
attachTo: tracker.class.Issue,
descriptor: view.viewlet.List,
viewOptions: componentIssueOptions,
variant: 'component',
configOptions: {
strict: true,
hiddenKeys: [
'priority',
'number',
'status',
'title',
'dueDate',
'component',
'estimation',
'createdBy',
'modifiedBy'
]
},
config: issueConfig('sub', true, true, false)
},
tracker.viewlet.ComponentIssuesList
)
builder.createDoc(
view.class.Viewlet,
core.space.Model,
{
attachTo: tracker.class.IssueTemplate,
descriptor: view.viewlet.List,
viewOptions: {
groupBy: ['assignee', 'priority', 'component', 'milestone', 'createdBy', 'modifiedBy'],
orderBy: [
['priority', SortingOrder.Ascending],
['modifiedOn', SortingOrder.Descending],
['dueDate', SortingOrder.Ascending],
['rank', SortingOrder.Ascending]
],
other: [showColorsViewOption]
},
configOptions: {
strict: true,
hiddenKeys: ['milestone', 'estimation', 'component', 'title', 'description', 'createdBy', 'modifiedBy']
},
config: [
// { key: '', presenter: tracker.component.PriorityEditor, props: { kind: 'list', size: 'small' } },
{
key: '',
presenter: tracker.component.IssueTemplatePresenter,
props: { type: 'issue', shouldUseMargin: true }
},
// { key: '', presenter: tracker.component.DueDatePresenter, props: { kind: 'list' } },
{
key: '',
presenter: tracker.component.ComponentEditor,
label: tracker.string.Component,
props: {
kind: 'list',
size: 'small',
shouldShowPlaceholder: false
},
displayProps: { key: 'component', compression: true }
},
{
key: '',
label: tracker.string.Milestone,
presenter: tracker.component.MilestoneEditor,
props: {
kind: 'list',
size: 'small',
shouldShowPlaceholder: false
},
displayProps: { key: 'milestone', compression: true }
},
{
key: '',
label: tracker.string.Estimation,
presenter: tracker.component.TemplateEstimationEditor,
props: {
kind: 'list',
size: 'small'
},
displayProps: { key: 'estimation', compression: true }
},
{ key: '', displayProps: { grow: true } },
{
key: 'modifiedOn',
presenter: tracker.component.ModificationDatePresenter,
displayProps: { fixed: 'right', dividerBefore: true }
},
{
key: 'assignee',
presenter: tracker.component.AssigneeEditor,
props: { kind: 'list', shouldShowName: false, avatarSize: 'x-small' }
}
]
},
tracker.viewlet.IssueTemplateList
)
builder.createDoc(
view.class.Viewlet,
core.space.Model,
{
attachTo: tracker.class.Issue,
descriptor: tracker.viewlet.Kanban,
viewOptions: {
...issuesOptions(true),
groupDepth: 1
},
configOptions: {
strict: true
},
config: ['subIssues', 'priority', 'component', 'dueDate', 'labels', 'estimation', 'attachments', 'comments']
},
tracker.viewlet.IssueKanban
)
const componentListViewOptions: ViewOptionsModel = {
groupBy: ['lead', 'createdBy', 'modifiedBy'],
orderBy: [
['modifiedOn', SortingOrder.Descending],
['createdOn', SortingOrder.Descending]
],
other: [showColorsViewOption]
}
builder.createDoc(
view.class.Viewlet,
core.space.Model,
{
attachTo: tracker.class.Component,
descriptor: view.viewlet.List,
viewOptions: componentListViewOptions,
configOptions: {
strict: true,
hiddenKeys: ['label', 'description']
},
config: [
{
key: '',
presenter: tracker.component.ComponentPresenter,
props: { kind: 'list' }
},
{ key: '', displayProps: { grow: true } },
{
key: '$lookup.lead',
presenter: tracker.component.LeadPresenter,
displayProps: {
dividerBefore: true,
key: 'lead'
},
props: { _class: tracker.class.Component, defaultClass: contact.mixin.Employee, shouldShowLabel: false }
}
]
},
tracker.viewlet.ComponentList
)
builder.createDoc(
view.class.Viewlet,
core.space.Model,
{
attachTo: tracker.class.Project,
descriptor: view.viewlet.List,
viewOptions: {
groupBy: ['createdBy'],
orderBy: [
['modifiedOn', SortingOrder.Descending],
['createdOn', SortingOrder.Descending]
],
other: [showColorsViewOption]
},
configOptions: {
strict: true,
hiddenKeys: ['label', 'description']
},
config: [
{
key: '',
props: { kind: 'list' }
},
{ key: '', displayProps: { grow: true } }
]
},
tracker.viewlet.ProjectList
)
const milestoneOptions: ViewOptionsModel = {
groupBy: ['status', 'createdBy', 'modifiedBy'],
orderBy: [
['modifiedOn', SortingOrder.Descending],
['targetDate', SortingOrder.Descending],
['createdOn', SortingOrder.Descending]
],
other: [showColorsViewOption]
}
builder.createDoc(
view.class.Viewlet,
core.space.Model,
{
attachTo: tracker.class.Milestone,
descriptor: view.viewlet.List,
viewOptions: milestoneOptions,
configOptions: {
strict: true,
hiddenKeys: ['targetDate', 'label', 'description']
},
config: [
{
key: 'status',
props: { width: '1rem', kind: 'list', size: 'small', justify: 'center' }
},
{ key: '', presenter: tracker.component.MilestonePresenter, props: { shouldUseMargin: true } },
{ key: '', displayProps: { grow: true } },
{
key: '',
label: tracker.string.TargetDate,
presenter: tracker.component.MilestoneDatePresenter,
props: { field: 'targetDate' }
}
]
},
tracker.viewlet.MilestoneList
)
}

View File

@ -296,10 +296,15 @@ export class TxOperations implements Omit<Client, 'notify'> {
return new ApplyOperations(this, scope) return new ApplyOperations(this, scope)
} }
async diffUpdate (doc: Doc, raw: Doc | Data<Doc>, date: Timestamp, account?: Ref<Account>): Promise<Doc> { async diffUpdate<T extends Doc = Doc>(
doc: T,
update: T | Data<T> | DocumentUpdate<T>,
date?: Timestamp,
account?: Ref<Account>
): Promise<T> {
// We need to update fields if they are different. // We need to update fields if they are different.
const documentUpdate: DocumentUpdate<Doc> = {} const documentUpdate: DocumentUpdate<T> = {}
for (const [k, v] of Object.entries(raw)) { for (const [k, v] of Object.entries(update)) {
if (['_class', '_id', 'modifiedBy', 'modifiedOn', 'space', 'attachedTo', 'attachedToClass'].includes(k)) { if (['_class', '_id', 'modifiedBy', 'modifiedOn', 'space', 'attachedTo', 'attachedToClass'].includes(k)) {
continue continue
} }
@ -309,7 +314,7 @@ export class TxOperations implements Omit<Client, 'notify'> {
} }
} }
if (Object.keys(documentUpdate).length > 0) { if (Object.keys(documentUpdate).length > 0) {
await this.update(doc, documentUpdate, false, date, account ?? doc.modifiedBy) await this.update(doc, documentUpdate, false, date ?? Date.now(), account)
TxProcessor.applyUpdate(doc, documentUpdate) TxProcessor.applyUpdate(doc, documentUpdate)
} }
return doc return doc

View File

@ -22,6 +22,7 @@
export let label: IntlString export let label: IntlString
export let okAction: () => void export let okAction: () => void
export let okLabel: IntlString | undefined = undefined
export let canSave: boolean = false export let canSave: boolean = false
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
@ -40,7 +41,7 @@
<div class="antiCard-footer"> <div class="antiCard-footer">
<Button <Button
disabled={!canSave} disabled={!canSave}
label={presentation.string.Create} label={okLabel ?? presentation.string.Create}
kind={'accented'} kind={'accented'}
on:click={() => { on:click={() => {
okAction() okAction()

View File

@ -60,6 +60,7 @@
export let readonly = false export let readonly = false
export let iconWithEmoji: AnySvelteComponent | Asset | ComponentType | undefined = view.ids.IconWithEmoji export let iconWithEmoji: AnySvelteComponent | Asset | ComponentType | undefined = view.ids.IconWithEmoji
export let defaultIcon: AnySvelteComponent | Asset | ComponentType = IconFolder export let defaultIcon: AnySvelteComponent | Asset | ComponentType = IconFolder
export let findDefaultSpace: (() => Promise<Space | undefined>) | undefined = undefined
let selected: (Space & IconProps) | undefined let selected: (Space & IconProps) | undefined
@ -71,7 +72,7 @@
selected = value !== undefined ? await client.findOne(_class, { ...(spaceQuery ?? {}), _id: value }) : undefined selected = value !== undefined ? await client.findOne(_class, { ...(spaceQuery ?? {}), _id: value }) : undefined
if (selected === undefined && autoSelect) { if (selected === undefined && autoSelect) {
selected = await client.findOne(_class, { ...(spaceQuery ?? {}) }) selected = (await findDefaultSpace?.()) ?? (await client.findOne(_class, { ...(spaceQuery ?? {}) }))
if (selected !== undefined) { if (selected !== undefined) {
value = selected._id ?? undefined value = selected._id ?? undefined
dispatch('change', value) dispatch('change', value)

View File

@ -39,6 +39,7 @@
export let iconWithEmoji: AnySvelteComponent | Asset | ComponentType | undefined = undefined export let iconWithEmoji: AnySvelteComponent | Asset | ComponentType | undefined = undefined
export let defaultIcon: AnySvelteComponent | Asset | ComponentType | undefined = undefined export let defaultIcon: AnySvelteComponent | Asset | ComponentType | undefined = undefined
export let readonly: boolean = false export let readonly: boolean = false
export let findDefaultSpace: (() => Promise<Space | undefined>) | undefined = undefined
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
@ -71,4 +72,5 @@
dispatch('change', space) dispatch('change', space)
}} }}
on:space on:space
{findDefaultSpace}
/> />

View File

@ -31,6 +31,7 @@
"Members": "Members", "Members": "Members",
"UnAssign": "Unassign", "UnAssign": "Unassign",
"ConfigLabel": "CRM", "ConfigLabel": "CRM",
"ConfigDescription": "Extension for Customer relation management" "ConfigDescription": "Extension for Customer relation management",
"EditFunnel": "Edit Funnel"
} }
} }

View File

@ -31,6 +31,7 @@
"Members": "Пользователи", "Members": "Пользователи",
"UnAssign": "Отменить назначение", "UnAssign": "Отменить назначение",
"ConfigLabel": "CRM", "ConfigLabel": "CRM",
"ConfigDescription": "Расширение по работе с клиентами" "ConfigDescription": "Расширение по работе с клиентами",
"EditFunnel": "Редактировать воронку"
} }
} }

View File

@ -14,26 +14,35 @@
// limitations under the License. // limitations under the License.
--> -->
<script lang="ts"> <script lang="ts">
import core, { getCurrentAccount, Ref } from '@hcengineering/core' import { AccountArrayEditor } from '@hcengineering/contact-resources'
import core, { Account, getCurrentAccount, Ref } from '@hcengineering/core'
import { Funnel } from '@hcengineering/lead'
import presentation, { getClient, SpaceCreateCard } from '@hcengineering/presentation' import presentation, { getClient, SpaceCreateCard } from '@hcengineering/presentation'
import task, { createStates, KanbanTemplate } from '@hcengineering/task' import task, { createStates, KanbanTemplate } from '@hcengineering/task'
import { Component, EditBox, Grid, IconFolder, ToggleWithLabel } from '@hcengineering/ui' import ui, { Component, EditBox, Grid, IconFolder, Label, ToggleWithLabel } from '@hcengineering/ui'
import { createEventDispatcher } from 'svelte' import { createEventDispatcher } from 'svelte'
import lead from '../plugin' import lead from '../plugin'
export let funnel: Funnel | undefined = undefined
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
let name: string = '' const client = getClient()
const description: string = '' const hierarchy = client.getHierarchy()
$: isNew = !funnel
let name: string = funnel?.name ?? ''
const description: string = funnel?.description ?? ''
let templateId: Ref<KanbanTemplate> | undefined let templateId: Ref<KanbanTemplate> | undefined
let isPrivate: boolean = false let isPrivate: boolean = funnel?.private ?? false
let members: Ref<Account>[] =
funnel?.members !== undefined ? hierarchy.clone(funnel.members) : [getCurrentAccount()._id]
export function canClose (): boolean { export function canClose (): boolean {
return name === '' && templateId !== undefined return name === '' && templateId !== undefined
} }
const client = getClient()
async function createFunnel (): Promise<void> { async function createFunnel (): Promise<void> {
if ( if (
templateId !== undefined && templateId !== undefined &&
@ -49,17 +58,25 @@
description, description,
private: isPrivate, private: isPrivate,
archived: false, archived: false,
members: [getCurrentAccount()._id], members,
templateId, templateId,
states, states,
doneStates doneStates
}) })
} }
async function save (): Promise<void> {
if (isNew) {
await createFunnel()
} else if (funnel !== undefined) {
await client.diffUpdate<Funnel>(funnel, { name, description, members, private: isPrivate }, Date.now())
}
}
</script> </script>
<SpaceCreateCard <SpaceCreateCard
label={lead.string.CreateFunnel} label={lead.string.CreateFunnel}
okAction={createFunnel} okAction={save}
okLabel={!isNew ? ui.string.Save : undefined}
canSave={name.length > 0} canSave={name.length > 0}
on:close={() => { on:close={() => {
dispatch('close') dispatch('close')
@ -89,5 +106,17 @@
templateId = evt.detail templateId = evt.detail
}} }}
/> />
<div class="antiGrid-row">
<div class="antiGrid-row__header">
<Label label={lead.string.Members} />
</div>
<AccountArrayEditor
value={members}
label={lead.string.Members}
onChange={(refs) => (members = refs)}
kind={'regular'}
size={'large'}
/>
</div>
</Grid> </Grid>
</SpaceCreateCard> </SpaceCreateCard>

View File

@ -60,7 +60,7 @@
} }
} }
$: { $: if (_space === undefined) {
if (funnels.find((it) => it._id === _space) === undefined) { if (funnels.find((it) => it._id === _space) === undefined) {
_space = funnels[0]?._id _space = funnels[0]?._id
} }

View File

@ -66,7 +66,6 @@ export default mergeIds(taskId, task, {
CantStatusDeleteError: '' as IntlString, CantStatusDeleteError: '' as IntlString,
Archive: '' as IntlString, Archive: '' as IntlString,
Unarchive: '' as IntlString, Unarchive: '' as IntlString,
RelatedIssues: '' as IntlString,
Tasks: '' as IntlString, Tasks: '' as IntlString,
Task: '' as IntlString, Task: '' as IntlString,

View File

@ -283,7 +283,9 @@
"IssueNotificationChangedProperty": "{senderName} changed {property} to \"{newValue}\"", "IssueNotificationChangedProperty": "{senderName} changed {property} to \"{newValue}\"",
"IssueNotificationMessage": "{senderName}: {message}", "IssueNotificationMessage": "{senderName}: {message}",
"PreviousAssigned": "Previously assigned", "PreviousAssigned": "Previously assigned",
"IssueAssigneedToYou": "Assigned to you" "IssueAssigneedToYou": "Assigned to you",
"RelatedIssueTargetDescription": "Related issue project target for Class or Space",
"MapRelatedIssues": "Configure Related issue default projects"
}, },
"status": {} "status": {}
} }

View File

@ -283,7 +283,9 @@
"IssueNotificationChangedProperty": "{senderName} изменил {property} на \"{newValue}\"", "IssueNotificationChangedProperty": "{senderName} изменил {property} на \"{newValue}\"",
"IssueNotificationMessage": "{senderName}: {message}", "IssueNotificationMessage": "{senderName}: {message}",
"PreviousAssigned": "Ранее назначенные", "PreviousAssigned": "Ранее назначенные",
"IssueAssigneedToYou": "Назначено вам" "IssueAssigneedToYou": "Назначено вам",
"RelatedIssueTargetDescription": "Настройка проекта по умолчанию для Класса или пространства",
"MapRelatedIssues": "Настроить проекты по умолчанию для связанных задач"
}, },
"status": {} "status": {}
} }

View File

@ -64,6 +64,7 @@
"@hcengineering/workbench-resources": "^0.6.1", "@hcengineering/workbench-resources": "^0.6.1",
"@hcengineering/activity-resources": "^0.6.1", "@hcengineering/activity-resources": "^0.6.1",
"@hcengineering/activity": "^0.6.0", "@hcengineering/activity": "^0.6.0",
"@hcengineering/query": "^0.6.7" "@hcengineering/query": "^0.6.7",
"@hcengineering/preference": "^0.6.8"
} }
} }

View File

@ -13,24 +13,25 @@
// limitations under the License. // limitations under the License.
--> -->
<script lang="ts"> <script lang="ts">
import { AttachmentStyledBox } from '@hcengineering/attachment-resources' import { Attachment } from '@hcengineering/attachment'
import { AttachmentPresenter, AttachmentStyledBox } from '@hcengineering/attachment-resources'
import chunter from '@hcengineering/chunter' import chunter from '@hcengineering/chunter'
import { Employee } from '@hcengineering/contact' import { Employee } from '@hcengineering/contact'
import core, { Account, DocData, Class, Doc, fillDefaults, generateId, Ref, SortingOrder } from '@hcengineering/core' import core, { Account, Class, Doc, DocData, Ref, SortingOrder, fillDefaults, generateId } from '@hcengineering/core'
import { getResource, translate } from '@hcengineering/platform' import { getResource, translate } from '@hcengineering/platform'
import preference, { SpacePreference } from '@hcengineering/preference'
import { import {
Card, Card,
createQuery,
DraftController, DraftController,
getClient,
KeyedAttribute, KeyedAttribute,
MessageBox, MessageBox,
MultipleDraftController, MultipleDraftController,
SpaceSelector SpaceSelector,
createQuery,
getClient
} from '@hcengineering/presentation' } from '@hcengineering/presentation'
import tags, { TagElement, TagReference } from '@hcengineering/tags' import tags, { TagElement, TagReference } from '@hcengineering/tags'
import { import {
calcRank,
Component as ComponentType, Component as ComponentType,
Issue, Issue,
IssueDraft, IssueDraft,
@ -39,29 +40,30 @@
IssueTemplate, IssueTemplate,
Milestone, Milestone,
Project, Project,
ProjectIssueTargetOptions ProjectIssueTargetOptions,
calcRank
} from '@hcengineering/tracker' } from '@hcengineering/tracker'
import { import {
addNotification,
Button, Button,
Component, Component,
createFocusManager,
DatePresenter, DatePresenter,
EditBox, EditBox,
FocusHandler, FocusHandler,
IconAttachment, IconAttachment,
Label, Label,
addNotification,
createFocusManager,
showPopup, showPopup,
themeStore themeStore
} from '@hcengineering/ui' } from '@hcengineering/ui'
import { Attachment } from '@hcengineering/attachment'
import { AttachmentPresenter } from '@hcengineering/attachment-resources'
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, getIssueId, updateIssueRelation } from '../issues'
import tracker from '../plugin' import tracker from '../plugin'
import ComponentSelector from './ComponentSelector.svelte' import ComponentSelector from './ComponentSelector.svelte'
import SetParentIssueActionPopup from './SetParentIssueActionPopup.svelte'
import SubIssues from './SubIssues.svelte'
import AssigneeEditor from './issues/AssigneeEditor.svelte' import AssigneeEditor from './issues/AssigneeEditor.svelte'
import IssueNotification from './issues/IssueNotification.svelte' import IssueNotification from './issues/IssueNotification.svelte'
import ParentIssue from './issues/ParentIssue.svelte' import ParentIssue from './issues/ParentIssue.svelte'
@ -69,11 +71,9 @@
import StatusEditor from './issues/StatusEditor.svelte' import StatusEditor from './issues/StatusEditor.svelte'
import EstimationEditor from './issues/timereport/EstimationEditor.svelte' import EstimationEditor from './issues/timereport/EstimationEditor.svelte'
import MilestoneSelector from './milestones/MilestoneSelector.svelte' import MilestoneSelector from './milestones/MilestoneSelector.svelte'
import SetParentIssueActionPopup from './SetParentIssueActionPopup.svelte'
import SubIssues from './SubIssues.svelte'
import ProjectPresenter from './projects/ProjectPresenter.svelte' import ProjectPresenter from './projects/ProjectPresenter.svelte'
export let space: Ref<Project> export let space: Ref<Project> | undefined
export let status: Ref<IssueStatus> | undefined = undefined export let status: Ref<IssueStatus> | undefined = undefined
export let priority: IssuePriority | undefined = undefined export let priority: IssuePriority | undefined = undefined
export let assignee: Ref<Employee> | null = null export let assignee: Ref<Employee> | null = null
@ -150,7 +150,7 @@
title: '', title: '',
description: '', description: '',
priority: priority ?? IssuePriority.NoPriority, priority: priority ?? IssuePriority.NoPriority,
space: _space, space: _space as Ref<Project>,
component: component ?? $activeComponent ?? null, component: component ?? $activeComponent ?? null,
dueDate: null, dueDate: null,
attachments: 0, attachments: 0,
@ -209,7 +209,7 @@
space: _space space: _space
} }
$: if (object.space !== _space) { $: if (_space !== undefined && object.space !== _space) {
object.space = _space object.space = _space
} }
@ -261,7 +261,7 @@
return { return {
...p, ...p,
_id: p.id, _id: p.id,
space: _space, space: _space as Ref<Project>,
subIssues: [], subIssues: [],
dueDate: null, dueDate: null,
labels: [], labels: [],
@ -355,7 +355,7 @@
async function createIssue (): Promise<void> { async function createIssue (): Promise<void> {
const _id: Ref<Issue> = generateId() const _id: Ref<Issue> = generateId()
if (!canSave || object.status === undefined) { if (!canSave || object.status === undefined || _space === undefined) {
return return
} }
@ -363,7 +363,7 @@
const incResult = await client.updateDoc( const incResult = await client.updateDoc(
tracker.class.Project, tracker.class.Project,
core.space.Space, core.space.Space,
_space, _space as Ref<Project>,
{ {
$inc: { sequence: 1 } $inc: { sequence: 1 }
}, },
@ -396,7 +396,7 @@
if (targetSettings !== undefined) { if (targetSettings !== undefined) {
const updateOp = await getResource(targetSettings.update) const updateOp = await getResource(targetSettings.update)
updateOp?.(_id, _space, value, targetSettingOptions) updateOp?.(_id, _space as Ref<Project>, value, targetSettingOptions)
} }
await client.addCollection( await client.addCollection(
@ -543,6 +543,47 @@
const manager = createFocusManager() const manager = createFocusManager()
let attachments: Map<Ref<Attachment>, Attachment> = new Map<Ref<Attachment>, Attachment>() let attachments: Map<Ref<Attachment>, Attachment> = new Map<Ref<Attachment>, Attachment>()
async function findDefaultSpace (): Promise<Project | undefined> {
let targetRef: Ref<Project> | undefined
if (relatedTo !== undefined) {
const targets = await client.findAll(tracker.class.RelatedIssueTarget, {})
// Find a space target first
targetRef =
targets.find((t) => t.rule.kind === 'spaceRule' && t.rule.space === relatedTo?.space && t.target !== undefined)
?.target ?? undefined
// Find a class target as second
targetRef =
targetRef ??
targets.find(
(t) =>
t.rule.kind === 'classRule' &&
client.getHierarchy().isDerived(relatedTo?._class as Ref<Class<Doc>>, t.rule.ofClass)
)?.target ??
undefined
}
// Find first starred project
if (targetRef === undefined) {
const prefs = await client.findAll<SpacePreference>(
preference.class.SpacePreference,
{},
{ sort: { modifiedOn: SortingOrder.Ascending } }
)
const projects = await client.findAll<Project>(tracker.class.Project, {
_id: {
$in: Array.from(prefs.map((it) => it.attachedTo as Ref<Project>).filter((it) => it != null))
}
})
if (projects.length > 0) {
return projects[0]
}
}
if (targetRef !== undefined) {
return client.findOne(tracker.class.Project, { _id: targetRef })
}
}
</script> </script>
<FocusHandler {manager} /> <FocusHandler {manager} />
@ -568,6 +609,7 @@
size={'small'} size={'small'}
component={ProjectPresenter} component={ProjectPresenter}
defaultIcon={tracker.icon.Home} defaultIcon={tracker.icon.Home}
{findDefaultSpace}
/> />
<ObjectBox <ObjectBox
_class={tracker.class.IssueTemplate} _class={tracker.class.IssueTemplate}
@ -667,6 +709,7 @@
/> />
{/key} {/key}
</div> </div>
{#if _space}
<SubIssues <SubIssues
bind:this={subIssuesComponent} bind:this={subIssuesComponent}
projectId={_space} projectId={_space}
@ -675,6 +718,7 @@
component={object.component} component={object.component}
bind:subIssues={object.subIssues} bind:subIssues={object.subIssues}
/> />
{/if}
{#if targetSettings?.bodyComponent && currentProject} {#if targetSettings?.bodyComponent && currentProject}
<Component <Component
is={targetSettings.bodyComponent} is={targetSettings.bodyComponent}

View File

@ -0,0 +1,137 @@
<script lang="ts">
import core, { Space, WithLookup } from '@hcengineering/core'
import { SpaceSelector, createQuery, getClient } from '@hcengineering/presentation'
import { RelatedIssueTarget } from '@hcengineering/tracker'
import { Button, Icon, IconArrowRight, IconDelete, Label } from '@hcengineering/ui'
import { FixedColumn } from '@hcengineering/view-resources'
import tracker from '../plugin'
export let value: Space | undefined
const targetQuery = createQuery()
let targets: WithLookup<RelatedIssueTarget>[] = []
$: targetQuery.query(
tracker.class.RelatedIssueTarget,
{},
(res) => {
targets = res.toSorted((a, b) => a.rule.kind.localeCompare(b.rule.kind))
},
{
lookup: {
target: tracker.class.Project
}
}
)
const client = getClient()
$: showCreate =
value !== undefined &&
targets.find((it) => it.rule.kind === 'spaceRule' && it.rule.space === value?._id) === undefined
</script>
<div class="flex-col" class:p-4={value === undefined}>
<div class="p-3">
<Label label={tracker.string.RelatedIssueTargetDescription} />
</div>
{#each targets as target, i}
<div class="flex-row p-1">
<div class="flex-row-center bordered">
<FixedColumn key={'rule-name'}>
{#if target.rule.kind === 'classRule'}
{@const documentClass = client.getHierarchy().getClass(target.rule.ofClass)}
<div class="flex-row-center">
<Button
label={documentClass.label}
icon={documentClass.icon}
disabled={true}
size={'medium'}
kind={'link'}
/>
</div>
{:else if target.rule.kind === 'spaceRule'}
<SpaceSelector
label={core.string.Space}
_class={core.class.Space}
space={target.rule.space}
readonly={true}
kind={'link'}
size={'medium'}
/>
{/if}
</FixedColumn>
<span class="p-1 mr-2"> <Icon icon={IconArrowRight} size={'medium'} /> </span>
<FixedColumn key={'space-value'}>
<SpaceSelector
label={tracker.string.Project}
_class={tracker.class.Project}
space={target.target ?? undefined}
autoSelect={false}
allowDeselect
kind={'list'}
on:change={(evt) => {
client.update(target, { target: evt.detail || null })
}}
/>
</FixedColumn>
{#if target.rule.kind === 'spaceRule'}
<div class="flex-grow flex flex-reverse">
<Button
icon={IconDelete}
on:click={() => {
client.remove(target)
}}
/>
</div>
{/if}
</div>
</div>
{/each}
{#if showCreate && value !== undefined}
{@const space = value._id}
<div class="flex-row-center bordered">
<FixedColumn key={'rule-name'}>
<SpaceSelector
label={core.string.Space}
_class={core.class.Space}
space={value._id}
readonly={true}
kind={'link'}
size={'medium'}
/>
</FixedColumn>
<span class="p-1"> => </span>
<FixedColumn key={'space-value'}>
<SpaceSelector
label={tracker.string.Project}
_class={tracker.class.Project}
space={undefined}
autoSelect={false}
allowDeselect
kind={'list'}
on:change={(evt) => {
client.createDoc(tracker.class.RelatedIssueTarget, space, {
target: evt.detail || null,
rule: {
kind: 'spaceRule',
space
}
})
}}
/>
</FixedColumn>
</div>
{/if}
</div>
<style lang="scss">
.bordered {
padding: 0.25rem 0.5rem;
background-color: var(--theme-comp-header-color);
border: 1px solid var(--theme-divider-color);
border-radius: 0.25rem;
}
</style>

View File

@ -0,0 +1,23 @@
<script lang="ts">
import { Space } from '@hcengineering/core'
import { Card } from '@hcengineering/presentation'
import ui from '@hcengineering/ui'
import { createEventDispatcher } from 'svelte'
import tracker from '../plugin'
import EditRelatedTargets from './EditRelatedTargets.svelte'
export let value: Space
const dispatch = createEventDispatcher()
</script>
<Card
label={tracker.string.RelatedIssues}
canSave={true}
okAction={() => {
dispatch('close')
}}
okLabel={ui.string.Ok}
on:close
>
<EditRelatedTargets {value} on:close />
</Card>

View File

@ -75,6 +75,8 @@ import SetDueDateActionPopup from './components/SetDueDateActionPopup.svelte'
import SetParentIssueActionPopup from './components/SetParentIssueActionPopup.svelte' import SetParentIssueActionPopup from './components/SetParentIssueActionPopup.svelte'
import CreateIssueTemplate from './components/templates/CreateIssueTemplate.svelte' import CreateIssueTemplate from './components/templates/CreateIssueTemplate.svelte'
import Statuses from './components/workflow/Statuses.svelte' import Statuses from './components/workflow/Statuses.svelte'
import EditRelatedTargets from './components/EditRelatedTargets.svelte'
import EditRelatedTargetsPopup from './components/EditRelatedTargetsPopup.svelte'
import { import {
getIssueId, getIssueId,
getIssueTitle, getIssueTitle,
@ -475,7 +477,9 @@ export default async (): Promise<Resources> => ({
PriorityFilterValuePresenter, PriorityFilterValuePresenter,
StatusFilterValuePresenter, StatusFilterValuePresenter,
ProjectFilterValuePresenter, ProjectFilterValuePresenter,
ComponentFilterValuePresenter ComponentFilterValuePresenter,
EditRelatedTargets,
EditRelatedTargetsPopup
}, },
completion: { completion: {
IssueQuery: async (client: Client, query: string, filter?: { in?: RelatedDocument[], nin?: RelatedDocument[] }) => IssueQuery: async (client: Client, query: string, filter?: { in?: RelatedDocument[], nin?: RelatedDocument[] }) =>

View File

@ -226,7 +226,6 @@ export default mergeIds(trackerId, tracker, {
AddRelatedIssue: '' as IntlString, AddRelatedIssue: '' as IntlString,
RelatedIssuesNotFound: '' as IntlString, RelatedIssuesNotFound: '' as IntlString,
RelatedIssue: '' as IntlString, RelatedIssue: '' as IntlString,
RelatedIssues: '' as IntlString,
BlockedIssue: '' as IntlString, BlockedIssue: '' as IntlString,
BlockingIssue: '' as IntlString, BlockingIssue: '' as IntlString,
BlockedBySearchPlaceholder: '' as IntlString, BlockedBySearchPlaceholder: '' as IntlString,
@ -301,7 +300,9 @@ export default mergeIds(trackerId, tracker, {
NoStatusFound: '' as IntlString, NoStatusFound: '' as IntlString,
CreateMissingStatus: '' as IntlString, CreateMissingStatus: '' as IntlString,
UnsetParent: '' as IntlString, UnsetParent: '' as IntlString,
PreviousAssigned: '' as IntlString PreviousAssigned: '' as IntlString,
EditRelatedTargets: '' as IntlString,
RelatedIssueTargetDescription: '' as IntlString
}, },
component: { component: {
NopeComponent: '' as AnyComponent, NopeComponent: '' as AnyComponent,

View File

@ -55,6 +55,29 @@ export interface Project extends SpaceWithStates, IconProps {
defaultTimeReportDay: TimeReportDayType defaultTimeReportDay: TimeReportDayType
} }
export type RelatedIssueKind = 'classRule' | 'spaceRule'
export interface RelatedClassRule {
kind: 'classRule'
ofClass: Ref<Class<Doc>>
}
export interface RelatedSpaceRule {
kind: 'spaceRule'
space: Ref<Space>
}
/**
* @public
*
* If defined, will be used to set a default project for this kind of document's related issues.
*/
export interface RelatedIssueTarget extends Doc {
// Attached to project.
target?: Ref<Project> | null
rule: RelatedClassRule | RelatedSpaceRule
}
/** /**
* @public * @public
*/ */
@ -369,7 +392,8 @@ export default plugin(trackerId, {
Milestone: '' as Ref<Class<Milestone>>, Milestone: '' as Ref<Class<Milestone>>,
TypeMilestoneStatus: '' as Ref<Class<Type<MilestoneStatus>>>, TypeMilestoneStatus: '' as Ref<Class<Type<MilestoneStatus>>>,
TimeSpendReport: '' as Ref<Class<TimeSpendReport>>, TimeSpendReport: '' as Ref<Class<TimeSpendReport>>,
TypeReportedTime: '' as Ref<Class<Type<number>>> TypeReportedTime: '' as Ref<Class<Type<number>>>,
RelatedIssueTarget: '' as Ref<Class<RelatedIssueTarget>>
}, },
ids: { ids: {
NoParent: '' as Ref<Issue>, NoParent: '' as Ref<Issue>,
@ -471,7 +495,8 @@ export default plugin(trackerId, {
EditWorkflowStatuses: '' as Ref<Action>, EditWorkflowStatuses: '' as Ref<Action>,
EditProject: '' as Ref<Action>, EditProject: '' as Ref<Action>,
SetMilestone: '' as Ref<Action>, SetMilestone: '' as Ref<Action>,
SetLabels: '' as Ref<Action> SetLabels: '' as Ref<Action>,
EditRelatedTargets: '' as Ref<Action>
}, },
project: { project: {
DefaultProject: '' as Ref<Project> DefaultProject: '' as Ref<Project>
@ -487,7 +512,8 @@ export default plugin(trackerId, {
IssueNotificationChanged: '' as IntlString, IssueNotificationChanged: '' as IntlString,
IssueNotificationChangedProperty: '' as IntlString, IssueNotificationChangedProperty: '' as IntlString,
IssueNotificationMessage: '' as IntlString, IssueNotificationMessage: '' as IntlString,
IssueAssigneedToYou: '' as IntlString IssueAssigneedToYou: '' as IntlString,
RelatedIssues: '' as IntlString
}, },
mixin: { mixin: {
ProjectIssueTargetOptions: '' as Ref<Mixin<ProjectIssueTargetOptions>> ProjectIssueTargetOptions: '' as Ref<Mixin<ProjectIssueTargetOptions>>