mirror of
https://github.com/hcengineering/platform.git
synced 2025-04-26 02:10:07 +00:00
UBER-807: Allow to customize create issue dialog (#3669)
Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
parent
f3c3541964
commit
2d5a38eed5
@ -34,6 +34,7 @@ import {
|
|||||||
Collection,
|
Collection,
|
||||||
Hidden,
|
Hidden,
|
||||||
Index,
|
Index,
|
||||||
|
Mixin,
|
||||||
Model,
|
Model,
|
||||||
Prop,
|
Prop,
|
||||||
ReadOnly,
|
ReadOnly,
|
||||||
@ -46,12 +47,12 @@ import {
|
|||||||
} from '@hcengineering/model'
|
} from '@hcengineering/model'
|
||||||
import attachment from '@hcengineering/model-attachment'
|
import attachment from '@hcengineering/model-attachment'
|
||||||
import chunter from '@hcengineering/model-chunter'
|
import chunter from '@hcengineering/model-chunter'
|
||||||
import core, { TAttachedDoc, TDoc, TStatus, TType } from '@hcengineering/model-core'
|
import core, { TAttachedDoc, TClass, TDoc, TStatus, TType } from '@hcengineering/model-core'
|
||||||
import task, { TSpaceWithStates, TTask } from '@hcengineering/model-task'
|
import task, { TSpaceWithStates, TTask } from '@hcengineering/model-task'
|
||||||
import view, { actionTemplates, classPresenter, createAction, showColorsViewOption } from '@hcengineering/model-view'
|
import view, { actionTemplates, classPresenter, createAction, showColorsViewOption } from '@hcengineering/model-view'
|
||||||
import workbench, { createNavigateAction } from '@hcengineering/model-workbench'
|
import workbench, { createNavigateAction } from '@hcengineering/model-workbench'
|
||||||
import notification from '@hcengineering/notification'
|
import notification from '@hcengineering/notification'
|
||||||
import { IntlString } from '@hcengineering/platform'
|
import { IntlString, Resource } from '@hcengineering/platform'
|
||||||
import setting from '@hcengineering/setting'
|
import setting from '@hcengineering/setting'
|
||||||
import tags, { TagElement } from '@hcengineering/tags'
|
import tags, { TagElement } from '@hcengineering/tags'
|
||||||
import { DoneState } from '@hcengineering/task'
|
import { DoneState } from '@hcengineering/task'
|
||||||
@ -64,9 +65,11 @@ import {
|
|||||||
IssueStatus,
|
IssueStatus,
|
||||||
IssueTemplate,
|
IssueTemplate,
|
||||||
IssueTemplateChild,
|
IssueTemplateChild,
|
||||||
|
IssueUpdateFunction,
|
||||||
Milestone,
|
Milestone,
|
||||||
MilestoneStatus,
|
MilestoneStatus,
|
||||||
Project,
|
Project,
|
||||||
|
ProjectIssueTargetOptions,
|
||||||
TimeReportDayType,
|
TimeReportDayType,
|
||||||
TimeSpendReport,
|
TimeSpendReport,
|
||||||
trackerId
|
trackerId
|
||||||
@ -77,6 +80,7 @@ import tracker from './plugin'
|
|||||||
import { generateClassNotificationTypes } from '@hcengineering/model-notification'
|
import { generateClassNotificationTypes } from '@hcengineering/model-notification'
|
||||||
import presentation from '@hcengineering/model-presentation'
|
import presentation from '@hcengineering/model-presentation'
|
||||||
import { defaultPriorities, issuePriorities } from '@hcengineering/tracker-resources/src/types'
|
import { defaultPriorities, issuePriorities } from '@hcengineering/tracker-resources/src/types'
|
||||||
|
import { AnyComponent } from '@hcengineering/ui'
|
||||||
import { PaletteColorIndexes } from '@hcengineering/ui/src/colors'
|
import { PaletteColorIndexes } from '@hcengineering/ui/src/colors'
|
||||||
|
|
||||||
export { trackerId } from '@hcengineering/tracker'
|
export { trackerId } from '@hcengineering/tracker'
|
||||||
@ -336,6 +340,14 @@ export class TComponent extends TDoc implements Component {
|
|||||||
declare space: Ref<Project>
|
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
|
* @public
|
||||||
*/
|
*/
|
||||||
@ -410,7 +422,8 @@ export function createModel (builder: Builder): void {
|
|||||||
TMilestone,
|
TMilestone,
|
||||||
TTypeMilestoneStatus,
|
TTypeMilestoneStatus,
|
||||||
TTimeSpendReport,
|
TTimeSpendReport,
|
||||||
TTypeReportedTime
|
TTypeReportedTime,
|
||||||
|
TProjectIssueTargetOptions
|
||||||
)
|
)
|
||||||
|
|
||||||
builder.createDoc(
|
builder.createDoc(
|
||||||
|
@ -92,6 +92,24 @@ export class Hierarchy {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
findClassOrMixinMixin<D extends Doc, M extends D>(doc: Doc, mixin: Ref<Mixin<M>>): M | undefined {
|
||||||
|
const cc = this.classHierarchyMixin(doc._class, mixin)
|
||||||
|
if (cc !== undefined) {
|
||||||
|
return cc
|
||||||
|
}
|
||||||
|
|
||||||
|
const _doc = _toDoc(doc)
|
||||||
|
// Find all potential mixins of doc
|
||||||
|
for (const [k, v] of Object.entries(_doc)) {
|
||||||
|
if (typeof v === 'object' && this.classifiers.has(k as Ref<Classifier>)) {
|
||||||
|
const cc = this.classHierarchyMixin(k as Ref<Mixin<Doc>>, mixin)
|
||||||
|
if (cc !== undefined) {
|
||||||
|
return cc
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
isMixin (_class: Ref<Class<Doc>>): boolean {
|
isMixin (_class: Ref<Class<Doc>>): boolean {
|
||||||
const data = this.classifiers.get(_class)
|
const data = this.classifiers.get(_class)
|
||||||
return data !== undefined && this._isMixin(data)
|
return data !== undefined && this._isMixin(data)
|
||||||
|
@ -289,7 +289,7 @@ export class TxOperations implements Omit<Client, 'notify'> {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
const dv = (doc as any)[k]
|
const dv = (doc as any)[k]
|
||||||
if (!deepEqual(dv, v) && v != null) {
|
if (!deepEqual(dv, v) && v !== undefined) {
|
||||||
;(documentUpdate as any)[k] = v
|
;(documentUpdate as any)[k] = v
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -111,7 +111,7 @@
|
|||||||
await client.update(targetPerson, update)
|
await client.update(targetPerson, update)
|
||||||
}
|
}
|
||||||
for (const channel of resultChannels.values()) {
|
for (const channel of resultChannels.values()) {
|
||||||
if (channel.attachedTo !== targetPerson._id) continue
|
if (channel.attachedTo === targetPerson._id) continue
|
||||||
await client.update(channel, { attachedTo: targetPerson._id })
|
await client.update(channel, { attachedTo: targetPerson._id })
|
||||||
}
|
}
|
||||||
for (const old of oldChannels) {
|
for (const old of oldChannels) {
|
||||||
|
@ -16,7 +16,7 @@
|
|||||||
import { AttachmentStyledBox } from '@hcengineering/attachment-resources'
|
import { 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, AttachedData, Doc, fillDefaults, generateId, Ref, SortingOrder } from '@hcengineering/core'
|
import core, { Account, DocData, Class, Doc, fillDefaults, generateId, Ref, SortingOrder } from '@hcengineering/core'
|
||||||
import { getResource, translate } from '@hcengineering/platform'
|
import { getResource, translate } from '@hcengineering/platform'
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
@ -38,7 +38,8 @@
|
|||||||
IssueStatus,
|
IssueStatus,
|
||||||
IssueTemplate,
|
IssueTemplate,
|
||||||
Milestone,
|
Milestone,
|
||||||
Project
|
Project,
|
||||||
|
ProjectIssueTargetOptions
|
||||||
} from '@hcengineering/tracker'
|
} from '@hcengineering/tracker'
|
||||||
import {
|
import {
|
||||||
addNotification,
|
addNotification,
|
||||||
@ -299,6 +300,18 @@
|
|||||||
currentProject = res.shift()
|
currentProject = res.shift()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
$: targetSettings =
|
||||||
|
currentProject !== undefined
|
||||||
|
? client
|
||||||
|
.getHierarchy()
|
||||||
|
.findClassOrMixinMixin<Class<Doc>, ProjectIssueTargetOptions>(
|
||||||
|
currentProject,
|
||||||
|
tracker.mixin.ProjectIssueTargetOptions
|
||||||
|
)
|
||||||
|
: undefined
|
||||||
|
|
||||||
|
let targetSettingOptions: Record<string, any> = {}
|
||||||
|
|
||||||
async function updateIssueStatusId (object: IssueDraft, currentProject: Project | undefined) {
|
async function updateIssueStatusId (object: IssueDraft, currentProject: Project | undefined) {
|
||||||
if (currentProject?.defaultIssueStatus && object.status === undefined) {
|
if (currentProject?.defaultIssueStatus && object.status === undefined) {
|
||||||
object.status = currentProject.defaultIssueStatus
|
object.status = currentProject.defaultIssueStatus
|
||||||
@ -340,7 +353,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function createIssue () {
|
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) {
|
||||||
return
|
return
|
||||||
@ -357,7 +370,7 @@
|
|||||||
true
|
true
|
||||||
)
|
)
|
||||||
|
|
||||||
const value: AttachedData<Issue> = {
|
const value: DocData<Issue> = {
|
||||||
doneState: null,
|
doneState: null,
|
||||||
title: getTitle(object.title),
|
title: getTitle(object.title),
|
||||||
description: object.description,
|
description: object.description,
|
||||||
@ -381,6 +394,11 @@
|
|||||||
childInfo: []
|
childInfo: []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (targetSettings !== undefined) {
|
||||||
|
const updateOp = await getResource(targetSettings.update)
|
||||||
|
updateOp?.(_id, _space, value, targetSettingOptions)
|
||||||
|
}
|
||||||
|
|
||||||
await client.addCollection(
|
await client.addCollection(
|
||||||
tracker.class.Issue,
|
tracker.class.Issue,
|
||||||
_space,
|
_space,
|
||||||
@ -569,6 +587,15 @@
|
|||||||
docProps={{ disabled: true, noUnderline: true }}
|
docProps={{ disabled: true, noUnderline: true }}
|
||||||
focusIndex={20000}
|
focusIndex={20000}
|
||||||
/>
|
/>
|
||||||
|
{#if targetSettings?.headerComponent && currentProject}
|
||||||
|
<Component
|
||||||
|
is={targetSettings.headerComponent}
|
||||||
|
props={{ targetSettingOptions, project: currentProject }}
|
||||||
|
on:change={(evt) => {
|
||||||
|
targetSettingOptions = evt.detail
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
</svelte:fragment>
|
</svelte:fragment>
|
||||||
<svelte:fragment slot="title" let:label>
|
<svelte:fragment slot="title" let:label>
|
||||||
<div class="flex-row-center gap-1">
|
<div class="flex-row-center gap-1">
|
||||||
@ -649,6 +676,15 @@
|
|||||||
component={object.component}
|
component={object.component}
|
||||||
bind:subIssues={object.subIssues}
|
bind:subIssues={object.subIssues}
|
||||||
/>
|
/>
|
||||||
|
{#if targetSettings?.bodyComponent && currentProject}
|
||||||
|
<Component
|
||||||
|
is={targetSettings.bodyComponent}
|
||||||
|
props={{ targetSettingOptions, project: currentProject }}
|
||||||
|
on:change={(evt) => {
|
||||||
|
targetSettingOptions = evt.detail
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
<svelte:fragment slot="pool">
|
<svelte:fragment slot="pool">
|
||||||
<div id="status-editor">
|
<div id="status-editor">
|
||||||
<StatusEditor
|
<StatusEditor
|
||||||
@ -759,6 +795,15 @@
|
|||||||
on:click={object.parentIssue ? clearParentIssue : setParentIssue}
|
on:click={object.parentIssue ? clearParentIssue : setParentIssue}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
{#if targetSettings?.poolComponent && currentProject}
|
||||||
|
<Component
|
||||||
|
is={targetSettings.poolComponent}
|
||||||
|
props={{ targetSettingOptions, project: currentProject }}
|
||||||
|
on:change={(evt) => {
|
||||||
|
targetSettingOptions = evt.detail
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
</svelte:fragment>
|
</svelte:fragment>
|
||||||
<svelte:fragment slot="attachments">
|
<svelte:fragment slot="attachments">
|
||||||
{#if attachments.size > 0}
|
{#if attachments.size > 0}
|
||||||
@ -785,5 +830,14 @@
|
|||||||
descriptionBox.attach()
|
descriptionBox.attach()
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
{#if targetSettings?.footerComponent && currentProject}
|
||||||
|
<Component
|
||||||
|
is={targetSettings.footerComponent}
|
||||||
|
props={{ targetSettingOptions, project: currentProject }}
|
||||||
|
on:change={(evt) => {
|
||||||
|
targetSettingOptions = evt.detail
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
</svelte:fragment>
|
</svelte:fragment>
|
||||||
</Card>
|
</Card>
|
||||||
|
@ -15,7 +15,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { PersonAccount } from '@hcengineering/contact'
|
import { PersonAccount } from '@hcengineering/contact'
|
||||||
import { EmployeeBox, personAccountByIdStore, personByIdStore } from '@hcengineering/contact-resources'
|
import { EmployeeBox, personAccountByIdStore, personByIdStore } from '@hcengineering/contact-resources'
|
||||||
import core, { ClassifierKind, Doc, Mixin, Ref } from '@hcengineering/core'
|
import core, { Class, ClassifierKind, Doc, Mixin, Ref } from '@hcengineering/core'
|
||||||
import { AttributeBarEditor, KeyedAttribute, createQuery, getClient } from '@hcengineering/presentation'
|
import { AttributeBarEditor, KeyedAttribute, createQuery, getClient } from '@hcengineering/presentation'
|
||||||
|
|
||||||
import tags from '@hcengineering/tags'
|
import tags from '@hcengineering/tags'
|
||||||
@ -68,7 +68,13 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getMixinKeys (mixin: Ref<Mixin<Doc>>): KeyedAttribute[] {
|
function getMixinKeys (mixin: Ref<Mixin<Doc>>): KeyedAttribute[] {
|
||||||
const filtredKeys = getFiltredKeys(hierarchy, mixin, [], issue._class)
|
const mixinClass = hierarchy.getClass(mixin)
|
||||||
|
const filtredKeys = getFiltredKeys(
|
||||||
|
hierarchy,
|
||||||
|
mixin,
|
||||||
|
[],
|
||||||
|
hierarchy.isMixin(mixinClass.extends as Ref<Class<Doc>>) ? mixinClass.extends : issue._class
|
||||||
|
)
|
||||||
const res = filtredKeys.filter((key) => !isCollectionAttr(hierarchy, key))
|
const res = filtredKeys.filter((key) => !isCollectionAttr(hierarchy, key))
|
||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
|
@ -19,11 +19,14 @@ import {
|
|||||||
Attribute,
|
Attribute,
|
||||||
Class,
|
Class,
|
||||||
Doc,
|
Doc,
|
||||||
|
DocData,
|
||||||
DocManager,
|
DocManager,
|
||||||
IdMap,
|
IdMap,
|
||||||
Markup,
|
Markup,
|
||||||
|
Mixin,
|
||||||
Ref,
|
Ref,
|
||||||
RelatedDocument,
|
RelatedDocument,
|
||||||
|
Space,
|
||||||
Status,
|
Status,
|
||||||
StatusCategory,
|
StatusCategory,
|
||||||
Timestamp,
|
Timestamp,
|
||||||
@ -52,6 +55,33 @@ export interface Project extends SpaceWithStates, IconProps {
|
|||||||
defaultTimeReportDay: TimeReportDayType
|
defaultTimeReportDay: TimeReportDayType
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
export type IssueUpdateFunction = (
|
||||||
|
id: Ref<Issue>,
|
||||||
|
space: Ref<Space>,
|
||||||
|
issue: DocData<Issue>,
|
||||||
|
data: Record<string, any>
|
||||||
|
) => Promise<void>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @public
|
||||||
|
*
|
||||||
|
* Customization mixin for project class.
|
||||||
|
*
|
||||||
|
* Allow to customize create issue/move issue dialogs, in case of selecting project of special kind.
|
||||||
|
*/
|
||||||
|
export interface ProjectIssueTargetOptions extends Class<Doc> {
|
||||||
|
// Component receiving project and context data.
|
||||||
|
headerComponent?: AnyComponent
|
||||||
|
bodyComponent?: AnyComponent
|
||||||
|
footerComponent?: AnyComponent
|
||||||
|
poolComponent?: AnyComponent
|
||||||
|
|
||||||
|
update: Resource<IssueUpdateFunction>
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
@ -452,5 +482,8 @@ export default plugin(trackerId, {
|
|||||||
string: {
|
string: {
|
||||||
ConfigLabel: '' as IntlString,
|
ConfigLabel: '' as IntlString,
|
||||||
NewRelatedIssue: '' as IntlString
|
NewRelatedIssue: '' as IntlString
|
||||||
|
},
|
||||||
|
mixin: {
|
||||||
|
ProjectIssueTargetOptions: '' as Ref<Mixin<ProjectIssueTargetOptions>>
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -26,6 +26,7 @@
|
|||||||
export let size: ButtonSize = 'small'
|
export let size: ButtonSize = 'small'
|
||||||
export let justify: 'left' | 'center' = 'center'
|
export let justify: 'left' | 'center' = 'center'
|
||||||
export let width: string | undefined = 'fit-content'
|
export let width: string | undefined = 'fit-content'
|
||||||
|
export let title: string | undefined
|
||||||
|
|
||||||
let shown: boolean = false
|
let shown: boolean = false
|
||||||
</script>
|
</script>
|
||||||
@ -53,10 +54,14 @@
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<svelte:fragment slot="content">
|
<svelte:fragment slot="content">
|
||||||
{#if value}
|
{#if title}
|
||||||
|
<span class="caption-color overflow-label pointer-events-none">{title}</span>
|
||||||
|
{:else if value}
|
||||||
<span class="caption-color overflow-label pointer-events-none">{value}</span>
|
<span class="caption-color overflow-label pointer-events-none">{value}</span>
|
||||||
{:else}
|
{:else}
|
||||||
<span class="content-dark-color pointer-events-none"><Label label={placeholder} /></span>
|
<span class="content-dark-color pointer-events-none">
|
||||||
|
<Label label={placeholder} />
|
||||||
|
</span>
|
||||||
{/if}
|
{/if}
|
||||||
</svelte:fragment>
|
</svelte:fragment>
|
||||||
</Button>
|
</Button>
|
||||||
|
@ -59,20 +59,20 @@
|
|||||||
<span class="content-dark-color"><Label label={placeholder} /></span>
|
<span class="content-dark-color"><Label label={placeholder} /></span>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<Button
|
|
||||||
focusIndex={2}
|
|
||||||
kind={'ghost'}
|
|
||||||
size={'small'}
|
|
||||||
icon={IconClose}
|
|
||||||
disabled={value === ''}
|
|
||||||
on:click={() => {
|
|
||||||
if (input) {
|
|
||||||
value = ''
|
|
||||||
input.focus()
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
{#if editable}
|
{#if editable}
|
||||||
|
<Button
|
||||||
|
focusIndex={2}
|
||||||
|
kind={'ghost'}
|
||||||
|
size={'small'}
|
||||||
|
icon={IconClose}
|
||||||
|
disabled={value === ''}
|
||||||
|
on:click={() => {
|
||||||
|
if (input) {
|
||||||
|
value = ''
|
||||||
|
input.focus()
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
<Button
|
<Button
|
||||||
id="channel-ok"
|
id="channel-ok"
|
||||||
focusIndex={3}
|
focusIndex={3}
|
||||||
@ -89,6 +89,7 @@
|
|||||||
kind={'ghost'}
|
kind={'ghost'}
|
||||||
size={'small'}
|
size={'small'}
|
||||||
icon={IconArrowRight}
|
icon={IconArrowRight}
|
||||||
|
showTooltip={{ label: view.string.Open }}
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
dispatch('update', value)
|
dispatch('update', value)
|
||||||
dispatch('close', 'open')
|
dispatch('close', 'open')
|
||||||
|
@ -183,7 +183,8 @@ export {
|
|||||||
StringPresenter,
|
StringPresenter,
|
||||||
EditBoxPopup,
|
EditBoxPopup,
|
||||||
SpaceHeader,
|
SpaceHeader,
|
||||||
ViewletContentView
|
ViewletContentView,
|
||||||
|
HyperlinkEditor
|
||||||
}
|
}
|
||||||
|
|
||||||
function PositionElementAlignment (e?: Event): PopupAlignment | undefined {
|
function PositionElementAlignment (e?: Event): PopupAlignment | undefined {
|
||||||
|
Loading…
Reference in New Issue
Block a user