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
models/tracker/src
packages/core/src
plugins
contact-resources/src/components
tracker-resources/src/components
tracker/src
view-resources/src
@ -34,6 +34,7 @@ import {
|
||||
Collection,
|
||||
Hidden,
|
||||
Index,
|
||||
Mixin,
|
||||
Model,
|
||||
Prop,
|
||||
ReadOnly,
|
||||
@ -46,12 +47,12 @@ import {
|
||||
} from '@hcengineering/model'
|
||||
import attachment from '@hcengineering/model-attachment'
|
||||
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 view, { actionTemplates, classPresenter, createAction, showColorsViewOption } from '@hcengineering/model-view'
|
||||
import workbench, { createNavigateAction } from '@hcengineering/model-workbench'
|
||||
import notification from '@hcengineering/notification'
|
||||
import { IntlString } from '@hcengineering/platform'
|
||||
import { IntlString, Resource } from '@hcengineering/platform'
|
||||
import setting from '@hcengineering/setting'
|
||||
import tags, { TagElement } from '@hcengineering/tags'
|
||||
import { DoneState } from '@hcengineering/task'
|
||||
@ -64,9 +65,11 @@ import {
|
||||
IssueStatus,
|
||||
IssueTemplate,
|
||||
IssueTemplateChild,
|
||||
IssueUpdateFunction,
|
||||
Milestone,
|
||||
MilestoneStatus,
|
||||
Project,
|
||||
ProjectIssueTargetOptions,
|
||||
TimeReportDayType,
|
||||
TimeSpendReport,
|
||||
trackerId
|
||||
@ -77,6 +80,7 @@ import tracker from './plugin'
|
||||
import { generateClassNotificationTypes } from '@hcengineering/model-notification'
|
||||
import presentation from '@hcengineering/model-presentation'
|
||||
import { defaultPriorities, issuePriorities } from '@hcengineering/tracker-resources/src/types'
|
||||
import { AnyComponent } from '@hcengineering/ui'
|
||||
import { PaletteColorIndexes } from '@hcengineering/ui/src/colors'
|
||||
|
||||
export { trackerId } from '@hcengineering/tracker'
|
||||
@ -336,6 +340,14 @@ export class TComponent extends TDoc implements Component {
|
||||
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
|
||||
*/
|
||||
@ -410,7 +422,8 @@ export function createModel (builder: Builder): void {
|
||||
TMilestone,
|
||||
TTypeMilestoneStatus,
|
||||
TTimeSpendReport,
|
||||
TTypeReportedTime
|
||||
TTypeReportedTime,
|
||||
TProjectIssueTargetOptions
|
||||
)
|
||||
|
||||
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 {
|
||||
const data = this.classifiers.get(_class)
|
||||
return data !== undefined && this._isMixin(data)
|
||||
|
@ -289,7 +289,7 @@ export class TxOperations implements Omit<Client, 'notify'> {
|
||||
continue
|
||||
}
|
||||
const dv = (doc as any)[k]
|
||||
if (!deepEqual(dv, v) && v != null) {
|
||||
if (!deepEqual(dv, v) && v !== undefined) {
|
||||
;(documentUpdate as any)[k] = v
|
||||
}
|
||||
}
|
||||
|
@ -111,7 +111,7 @@
|
||||
await client.update(targetPerson, update)
|
||||
}
|
||||
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 })
|
||||
}
|
||||
for (const old of oldChannels) {
|
||||
|
@ -16,7 +16,7 @@
|
||||
import { AttachmentStyledBox } from '@hcengineering/attachment-resources'
|
||||
import chunter from '@hcengineering/chunter'
|
||||
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 {
|
||||
Card,
|
||||
@ -38,7 +38,8 @@
|
||||
IssueStatus,
|
||||
IssueTemplate,
|
||||
Milestone,
|
||||
Project
|
||||
Project,
|
||||
ProjectIssueTargetOptions
|
||||
} from '@hcengineering/tracker'
|
||||
import {
|
||||
addNotification,
|
||||
@ -299,6 +300,18 @@
|
||||
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) {
|
||||
if (currentProject?.defaultIssueStatus && object.status === undefined) {
|
||||
object.status = currentProject.defaultIssueStatus
|
||||
@ -340,7 +353,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function createIssue () {
|
||||
async function createIssue (): Promise<void> {
|
||||
const _id: Ref<Issue> = generateId()
|
||||
if (!canSave || object.status === undefined) {
|
||||
return
|
||||
@ -357,7 +370,7 @@
|
||||
true
|
||||
)
|
||||
|
||||
const value: AttachedData<Issue> = {
|
||||
const value: DocData<Issue> = {
|
||||
doneState: null,
|
||||
title: getTitle(object.title),
|
||||
description: object.description,
|
||||
@ -381,6 +394,11 @@
|
||||
childInfo: []
|
||||
}
|
||||
|
||||
if (targetSettings !== undefined) {
|
||||
const updateOp = await getResource(targetSettings.update)
|
||||
updateOp?.(_id, _space, value, targetSettingOptions)
|
||||
}
|
||||
|
||||
await client.addCollection(
|
||||
tracker.class.Issue,
|
||||
_space,
|
||||
@ -569,6 +587,15 @@
|
||||
docProps={{ disabled: true, noUnderline: true }}
|
||||
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 slot="title" let:label>
|
||||
<div class="flex-row-center gap-1">
|
||||
@ -649,6 +676,15 @@
|
||||
component={object.component}
|
||||
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">
|
||||
<div id="status-editor">
|
||||
<StatusEditor
|
||||
@ -759,6 +795,15 @@
|
||||
on:click={object.parentIssue ? clearParentIssue : setParentIssue}
|
||||
/>
|
||||
</div>
|
||||
{#if targetSettings?.poolComponent && currentProject}
|
||||
<Component
|
||||
is={targetSettings.poolComponent}
|
||||
props={{ targetSettingOptions, project: currentProject }}
|
||||
on:change={(evt) => {
|
||||
targetSettingOptions = evt.detail
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="attachments">
|
||||
{#if attachments.size > 0}
|
||||
@ -785,5 +830,14 @@
|
||||
descriptionBox.attach()
|
||||
}}
|
||||
/>
|
||||
{#if targetSettings?.footerComponent && currentProject}
|
||||
<Component
|
||||
is={targetSettings.footerComponent}
|
||||
props={{ targetSettingOptions, project: currentProject }}
|
||||
on:change={(evt) => {
|
||||
targetSettingOptions = evt.detail
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
</Card>
|
||||
|
@ -15,7 +15,7 @@
|
||||
<script lang="ts">
|
||||
import { PersonAccount } from '@hcengineering/contact'
|
||||
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 tags from '@hcengineering/tags'
|
||||
@ -68,7 +68,13 @@
|
||||
}
|
||||
|
||||
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))
|
||||
return res
|
||||
}
|
||||
|
@ -19,11 +19,14 @@ import {
|
||||
Attribute,
|
||||
Class,
|
||||
Doc,
|
||||
DocData,
|
||||
DocManager,
|
||||
IdMap,
|
||||
Markup,
|
||||
Mixin,
|
||||
Ref,
|
||||
RelatedDocument,
|
||||
Space,
|
||||
Status,
|
||||
StatusCategory,
|
||||
Timestamp,
|
||||
@ -52,6 +55,33 @@ export interface Project extends SpaceWithStates, IconProps {
|
||||
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
|
||||
*/
|
||||
@ -452,5 +482,8 @@ export default plugin(trackerId, {
|
||||
string: {
|
||||
ConfigLabel: '' as IntlString,
|
||||
NewRelatedIssue: '' as IntlString
|
||||
},
|
||||
mixin: {
|
||||
ProjectIssueTargetOptions: '' as Ref<Mixin<ProjectIssueTargetOptions>>
|
||||
}
|
||||
})
|
||||
|
@ -26,6 +26,7 @@
|
||||
export let size: ButtonSize = 'small'
|
||||
export let justify: 'left' | 'center' = 'center'
|
||||
export let width: string | undefined = 'fit-content'
|
||||
export let title: string | undefined
|
||||
|
||||
let shown: boolean = false
|
||||
</script>
|
||||
@ -53,10 +54,14 @@
|
||||
}}
|
||||
>
|
||||
<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>
|
||||
{: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}
|
||||
</svelte:fragment>
|
||||
</Button>
|
||||
|
@ -59,20 +59,20 @@
|
||||
<span class="content-dark-color"><Label label={placeholder} /></span>
|
||||
{/if}
|
||||
</div>
|
||||
<Button
|
||||
focusIndex={2}
|
||||
kind={'ghost'}
|
||||
size={'small'}
|
||||
icon={IconClose}
|
||||
disabled={value === ''}
|
||||
on:click={() => {
|
||||
if (input) {
|
||||
value = ''
|
||||
input.focus()
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{#if editable}
|
||||
<Button
|
||||
focusIndex={2}
|
||||
kind={'ghost'}
|
||||
size={'small'}
|
||||
icon={IconClose}
|
||||
disabled={value === ''}
|
||||
on:click={() => {
|
||||
if (input) {
|
||||
value = ''
|
||||
input.focus()
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
id="channel-ok"
|
||||
focusIndex={3}
|
||||
@ -89,6 +89,7 @@
|
||||
kind={'ghost'}
|
||||
size={'small'}
|
||||
icon={IconArrowRight}
|
||||
showTooltip={{ label: view.string.Open }}
|
||||
on:click={() => {
|
||||
dispatch('update', value)
|
||||
dispatch('close', 'open')
|
||||
|
@ -183,7 +183,8 @@ export {
|
||||
StringPresenter,
|
||||
EditBoxPopup,
|
||||
SpaceHeader,
|
||||
ViewletContentView
|
||||
ViewletContentView,
|
||||
HyperlinkEditor
|
||||
}
|
||||
|
||||
function PositionElementAlignment (e?: Event): PopupAlignment | undefined {
|
||||
|
Loading…
Reference in New Issue
Block a user