UBER-807: Allow to customize create issue dialog (#3669)

Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
Andrey Sobolev 2023-09-08 11:33:23 +07:00 committed by GitHub
parent f3c3541964
commit 2d5a38eed5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 158 additions and 27 deletions

View File

@ -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(

View File

@ -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)

View File

@ -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
}
}

View File

@ -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) {

View File

@ -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>

View File

@ -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
}

View File

@ -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>>
}
})

View File

@ -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>

View File

@ -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')

View File

@ -183,7 +183,8 @@ export {
StringPresenter,
EditBoxPopup,
SpaceHeader,
ViewletContentView
ViewletContentView,
HyperlinkEditor
}
function PositionElementAlignment (e?: Event): PopupAlignment | undefined {