mirror of
https://github.com/hcengineering/platform.git
synced 2025-05-10 17:30:51 +00:00
Merge branch 'staging' into develop
Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
commit
329d1f175b
@ -13,60 +13,60 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
//
|
//
|
||||||
|
|
||||||
import documentsPlugin, {
|
|
||||||
documentsId,
|
|
||||||
type Document,
|
|
||||||
type DocumentSpace,
|
|
||||||
DocumentState
|
|
||||||
} from '@hcengineering/controlled-documents'
|
|
||||||
import activity from '@hcengineering/activity'
|
import activity from '@hcengineering/activity'
|
||||||
import contact from '@hcengineering/contact'
|
import contact from '@hcengineering/contact'
|
||||||
|
import documentsPlugin, {
|
||||||
|
documentsId,
|
||||||
|
DocumentState,
|
||||||
|
type Document,
|
||||||
|
type DocumentSpace
|
||||||
|
} from '@hcengineering/controlled-documents'
|
||||||
import { type Builder } from '@hcengineering/model'
|
import { type Builder } from '@hcengineering/model'
|
||||||
import chunter from '@hcengineering/model-chunter'
|
import chunter from '@hcengineering/model-chunter'
|
||||||
import core from '@hcengineering/model-core'
|
import core from '@hcengineering/model-core'
|
||||||
|
import { generateClassNotificationTypes } from '@hcengineering/model-notification'
|
||||||
|
import presentation from '@hcengineering/model-presentation'
|
||||||
|
import print from '@hcengineering/model-print'
|
||||||
import request from '@hcengineering/model-request'
|
import request from '@hcengineering/model-request'
|
||||||
import tracker from '@hcengineering/model-tracker'
|
import tracker from '@hcengineering/model-tracker'
|
||||||
import { generateClassNotificationTypes } from '@hcengineering/model-notification'
|
|
||||||
import view, { classPresenter, createAction } from '@hcengineering/model-view'
|
import view, { classPresenter, createAction } from '@hcengineering/model-view'
|
||||||
import presentation from '@hcengineering/model-presentation'
|
|
||||||
import workbench from '@hcengineering/model-workbench'
|
import workbench from '@hcengineering/model-workbench'
|
||||||
import notification from '@hcengineering/notification'
|
import notification from '@hcengineering/notification'
|
||||||
import setting from '@hcengineering/setting'
|
import setting from '@hcengineering/setting'
|
||||||
import tags from '@hcengineering/tags'
|
import tags from '@hcengineering/tags'
|
||||||
import print from '@hcengineering/model-print'
|
|
||||||
import textEditor from '@hcengineering/text-editor'
|
import textEditor from '@hcengineering/text-editor'
|
||||||
|
|
||||||
import documents from './plugin'
|
import { type Class, type Doc, type Ref } from '@hcengineering/core'
|
||||||
|
import { type Action } from '@hcengineering/view'
|
||||||
import { definePermissions } from './permissions'
|
import { definePermissions } from './permissions'
|
||||||
|
import documents from './plugin'
|
||||||
|
import { defineSpaceType } from './spaceType'
|
||||||
import {
|
import {
|
||||||
|
TChangeControl,
|
||||||
|
TControlledDocument,
|
||||||
|
TControlledDocumentSnapshot,
|
||||||
|
TDocument,
|
||||||
|
TDocumentApprovalRequest,
|
||||||
|
TDocumentCategory,
|
||||||
|
TDocumentComment,
|
||||||
|
TDocumentMeta,
|
||||||
|
TDocumentRequest,
|
||||||
|
TDocumentReviewRequest,
|
||||||
|
TDocumentSnapshot,
|
||||||
TDocumentSpace,
|
TDocumentSpace,
|
||||||
TDocumentSpaceType,
|
TDocumentSpaceType,
|
||||||
TDocumentSpaceTypeDescriptor,
|
TDocumentSpaceTypeDescriptor,
|
||||||
TExternalSpace,
|
|
||||||
TOrgSpace,
|
|
||||||
TDocumentMeta,
|
|
||||||
TProjectDocument,
|
|
||||||
TProjectMeta,
|
|
||||||
TProject,
|
|
||||||
TDocument,
|
|
||||||
TDocumentSnapshot,
|
|
||||||
TControlledDocumentSnapshot,
|
|
||||||
THierarchyDocument,
|
|
||||||
TDocumentTemplate,
|
TDocumentTemplate,
|
||||||
TDocumentTraining,
|
TDocumentTraining,
|
||||||
TDocumentCategory,
|
TExternalSpace,
|
||||||
TControlledDocument,
|
THierarchyDocument,
|
||||||
TChangeControl,
|
TOrgSpace,
|
||||||
TDocumentRequest,
|
TProject,
|
||||||
TDocumentReviewRequest,
|
TProjectDocument,
|
||||||
TDocumentApprovalRequest,
|
TProjectMeta,
|
||||||
TTypeDocumentState,
|
|
||||||
TTypeControlledDocumentState,
|
TTypeControlledDocumentState,
|
||||||
TDocumentComment
|
TTypeDocumentState
|
||||||
} from './types'
|
} from './types'
|
||||||
import { defineSpaceType } from './spaceType'
|
|
||||||
import { type Class, type Doc, type Ref } from '@hcengineering/core'
|
|
||||||
import { type Action } from '@hcengineering/view'
|
|
||||||
|
|
||||||
export { documentsId } from '@hcengineering/controlled-documents/src/index'
|
export { documentsId } from '@hcengineering/controlled-documents/src/index'
|
||||||
export * from './types'
|
export * from './types'
|
||||||
@ -105,6 +105,10 @@ export function createModel (builder: Builder): void {
|
|||||||
titleProvider: documents.function.ControlledDocumentTitleProvider
|
titleProvider: documents.function.ControlledDocumentTitleProvider
|
||||||
})
|
})
|
||||||
|
|
||||||
|
builder.mixin(documents.class.DocumentMeta, core.class.Class, view.mixin.ObjectTitle, {
|
||||||
|
titleProvider: documents.function.ControlledDocumentTitleProvider
|
||||||
|
})
|
||||||
|
|
||||||
builder.mixin(documents.class.DocumentApprovalRequest, core.class.Class, view.mixin.ObjectPresenter, {
|
builder.mixin(documents.class.DocumentApprovalRequest, core.class.Class, view.mixin.ObjectPresenter, {
|
||||||
presenter: documents.component.DocumentApprovalRequestPresenter
|
presenter: documents.component.DocumentApprovalRequestPresenter
|
||||||
})
|
})
|
||||||
|
@ -594,6 +594,13 @@ export function createModel (builder: Builder): void {
|
|||||||
key: 'status',
|
key: 'status',
|
||||||
props: { kind: 'list', size: 'small', shouldShowName: false }
|
props: { kind: 'list', size: 'small', shouldShowName: false }
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: 'kind',
|
||||||
|
label: task.string.TaskType,
|
||||||
|
presenter: task.component.TaskTypeListPresenter,
|
||||||
|
props: { kind: 'list', size: 'small', justify: 'center' },
|
||||||
|
displayProps: { key: 'applicant_kind' }
|
||||||
|
},
|
||||||
{
|
{
|
||||||
key: '$lookup.attachedTo',
|
key: '$lookup.attachedTo',
|
||||||
presenter: contact.component.PersonPresenter,
|
presenter: contact.component.PersonPresenter,
|
||||||
|
@ -513,11 +513,6 @@ export function createModel (builder: Builder): void {
|
|||||||
id: 'automations',
|
id: 'automations',
|
||||||
label: setting.string.Automations,
|
label: setting.string.Automations,
|
||||||
component: task.component.ProjectTypeAutomationsSectionEditor
|
component: task.component.ProjectTypeAutomationsSectionEditor
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'collections',
|
|
||||||
label: setting.string.Collections,
|
|
||||||
component: task.component.ProjectTypeCollectionsSectionEditor
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
subEditors: {
|
subEditors: {
|
||||||
|
@ -55,6 +55,7 @@ export default mergeIds(taskId, task, {
|
|||||||
TypesView: '' as AnyComponent,
|
TypesView: '' as AnyComponent,
|
||||||
StateIconPresenter: '' as AnyComponent,
|
StateIconPresenter: '' as AnyComponent,
|
||||||
TaskTypePresenter: '' as AnyComponent,
|
TaskTypePresenter: '' as AnyComponent,
|
||||||
|
TaskTypeListPresenter: '' as AnyComponent,
|
||||||
ProjectTypePresenter: '' as AnyComponent,
|
ProjectTypePresenter: '' as AnyComponent,
|
||||||
TaskTypeClassPresenter: '' as AnyComponent,
|
TaskTypeClassPresenter: '' as AnyComponent,
|
||||||
ProjectTypeClassPresenter: '' as AnyComponent
|
ProjectTypeClassPresenter: '' as AnyComponent
|
||||||
|
@ -741,7 +741,8 @@ function defineSpaceType (builder: Builder): void {
|
|||||||
description: tracker.string.Issue,
|
description: tracker.string.Issue,
|
||||||
icon: tracker.icon.Issue,
|
icon: tracker.icon.Issue,
|
||||||
name: tracker.string.Issue,
|
name: tracker.string.Issue,
|
||||||
statusCategoriesFunc: tracker.function.GetIssueStatusCategories
|
statusCategoriesFunc: tracker.function.GetIssueStatusCategories,
|
||||||
|
openTasks: tracker.function.OpenIssuesOfTaskType
|
||||||
},
|
},
|
||||||
tracker.descriptors.Issue
|
tracker.descriptors.Issue
|
||||||
)
|
)
|
||||||
|
@ -105,13 +105,13 @@ export function issueConfig (
|
|||||||
props: { kind: 'list', size: 'small', justify: 'center' },
|
props: { kind: 'list', size: 'small', justify: 'center' },
|
||||||
displayProps: { key: key + 'status' }
|
displayProps: { key: key + 'status' }
|
||||||
},
|
},
|
||||||
// {
|
{
|
||||||
// key: 'kind',
|
key: 'kind',
|
||||||
// label: task.string.TaskType,
|
label: task.string.TaskType,
|
||||||
// presenter: task.component.TaskTypePresenter,
|
presenter: task.component.TaskTypeListPresenter,
|
||||||
// props: { kind: 'list', size: 'small', justify: 'center' },
|
props: { kind: 'list', size: 'small', justify: 'center' },
|
||||||
// displayProps: { key: key + 'kind' }
|
displayProps: { key: key + 'kind' }
|
||||||
// },
|
},
|
||||||
{
|
{
|
||||||
key: '',
|
key: '',
|
||||||
label: tracker.string.Title,
|
label: tracker.string.Title,
|
||||||
|
@ -21,7 +21,7 @@ describe('dsl', () => {
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
expect(jsonToHTML(doc)).toEqual(
|
expect(jsonToHTML(doc)).toEqual(
|
||||||
'<p>Hello, <span data-type="reference" data-id="123" data-objectclass="world" data-label="World">@World</span></p><p>Check out <a target="_blank" rel="noopener noreferrer" class="cursor-pointer" href="https://example.com"><u>this link</u></a>.</p>'
|
'<p>Hello, <span data-type="reference" class="antiMention" data-id="123" data-objectclass="world" data-label="World">@World</span></p><p>Check out <a target="_blank" rel="noopener noreferrer" class="cursor-pointer" href="https://example.com"><u>this link</u></a>.</p>'
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -15,10 +15,18 @@
|
|||||||
|
|
||||||
import { Node, mergeAttributes } from '@tiptap/core'
|
import { Node, mergeAttributes } from '@tiptap/core'
|
||||||
import { getDataAttribute } from './utils'
|
import { getDataAttribute } from './utils'
|
||||||
|
import { Class, Doc, Ref } from '@hcengineering/core'
|
||||||
|
|
||||||
|
export interface ReferenceNodeProps {
|
||||||
|
id: Ref<Doc>
|
||||||
|
objectclass: Ref<Class<Doc>>
|
||||||
|
label: string
|
||||||
|
}
|
||||||
|
|
||||||
export interface ReferenceOptions {
|
export interface ReferenceOptions {
|
||||||
renderLabel: (props: { options: ReferenceOptions, node: any }) => string
|
renderLabel: (props: { options: ReferenceOptions, props: ReferenceNodeProps }) => string
|
||||||
suggestion: { char: string }
|
suggestion: { char?: string }
|
||||||
|
HTMLAttributes: Record<string, any>
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -28,23 +36,26 @@ export const ReferenceNode = Node.create<ReferenceOptions>({
|
|||||||
name: 'reference',
|
name: 'reference',
|
||||||
group: 'inline',
|
group: 'inline',
|
||||||
inline: true,
|
inline: true,
|
||||||
|
selectable: true,
|
||||||
|
atom: true,
|
||||||
|
draggable: true,
|
||||||
|
|
||||||
addAttributes () {
|
addAttributes () {
|
||||||
return {
|
return {
|
||||||
id: getDataAttribute('id'),
|
id: getDataAttribute('id'),
|
||||||
objectclass: getDataAttribute('objectclass'),
|
objectclass: getDataAttribute('objectclass'),
|
||||||
label: getDataAttribute('label'),
|
label: getDataAttribute('label')
|
||||||
class: { default: null }
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
addOptions () {
|
addOptions () {
|
||||||
return {
|
return {
|
||||||
renderLabel ({ options, node }) {
|
renderLabel ({ options, props }) {
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
return `${options.suggestion.char}${node.attrs.label ?? node.attrs.id}`
|
return `${options.suggestion.char}${props.label ?? props.id}`
|
||||||
},
|
},
|
||||||
suggestion: { char: '@' }
|
suggestion: { char: '@' },
|
||||||
|
HTMLAttributes: {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -72,21 +83,25 @@ export const ReferenceNode = Node.create<ReferenceOptions>({
|
|||||||
},
|
},
|
||||||
|
|
||||||
renderHTML ({ node, HTMLAttributes }) {
|
renderHTML ({ node, HTMLAttributes }) {
|
||||||
const options = this.options
|
|
||||||
return [
|
return [
|
||||||
'span',
|
'span',
|
||||||
mergeAttributes(
|
mergeAttributes(
|
||||||
{
|
{
|
||||||
'data-type': this.name
|
'data-type': this.name,
|
||||||
|
class: 'antiMention'
|
||||||
},
|
},
|
||||||
|
this.options.HTMLAttributes,
|
||||||
HTMLAttributes
|
HTMLAttributes
|
||||||
),
|
),
|
||||||
this.options.renderLabel({ options, node })
|
this.options.renderLabel({
|
||||||
|
options: this.options,
|
||||||
|
props: node.attrs as ReferenceNodeProps
|
||||||
|
})
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
||||||
renderText ({ node }) {
|
renderText ({ node }) {
|
||||||
const options = this.options
|
const options = this.options
|
||||||
return options.renderLabel({ options, node })
|
return options.renderLabel({ options, props: node.attrs as ReferenceNodeProps })
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -39,6 +39,8 @@
|
|||||||
export let labelDirection: TooltipAlignment | undefined = undefined
|
export let labelDirection: TooltipAlignment | undefined = undefined
|
||||||
export let shouldUpdateUndefined: boolean = true
|
export let shouldUpdateUndefined: boolean = true
|
||||||
export let minW0 = true
|
export let minW0 = true
|
||||||
|
export let focusIndex: number = -1
|
||||||
|
export let dataId: string | undefined = undefined
|
||||||
|
|
||||||
let container: HTMLElement
|
let container: HTMLElement
|
||||||
let opened: boolean = false
|
let opened: boolean = false
|
||||||
@ -78,6 +80,8 @@
|
|||||||
|
|
||||||
<div bind:this={container} class:min-w-0={minW0}>
|
<div bind:this={container} class:min-w-0={minW0}>
|
||||||
<Button
|
<Button
|
||||||
|
{focusIndex}
|
||||||
|
{dataId}
|
||||||
{icon}
|
{icon}
|
||||||
{iconProps}
|
{iconProps}
|
||||||
width={width ?? 'min-content'}
|
width={width ?? 'min-content'}
|
||||||
|
@ -90,7 +90,7 @@
|
|||||||
const employeeRef = (existingPerson?._id as Ref<Employee>) ?? id
|
const employeeRef = (existingPerson?._id as Ref<Employee>) ?? id
|
||||||
|
|
||||||
await client.createMixin(id, contact.class.Person, contact.space.Contacts, contact.mixin.Employee, {
|
await client.createMixin(id, contact.class.Person, contact.space.Contacts, contact.mixin.Employee, {
|
||||||
active: true
|
active: false
|
||||||
})
|
})
|
||||||
|
|
||||||
await client.addCollection(
|
await client.addCollection(
|
||||||
@ -148,7 +148,7 @@
|
|||||||
function changeEmail (): void {
|
function changeEmail (): void {
|
||||||
const index = channels.findIndex((p) => p.provider === contact.channelProvider.Email)
|
const index = channels.findIndex((p) => p.provider === contact.channelProvider.Email)
|
||||||
if (index !== -1) {
|
if (index !== -1) {
|
||||||
channels[index].value = email.trim()
|
channels[index].value = email.trim().toLowerCase()
|
||||||
} else {
|
} else {
|
||||||
channels.push({
|
channels.push({
|
||||||
provider: contact.channelProvider.Email,
|
provider: contact.channelProvider.Email,
|
||||||
|
@ -108,6 +108,7 @@ import {
|
|||||||
getAllDocumentStates,
|
getAllDocumentStates,
|
||||||
getControlledDocumentTitle,
|
getControlledDocumentTitle,
|
||||||
getDocumentMetaLinkFragment,
|
getDocumentMetaLinkFragment,
|
||||||
|
getDocumentMetaTitle,
|
||||||
getVisibleFilters,
|
getVisibleFilters,
|
||||||
isFolder,
|
isFolder,
|
||||||
renameFolder,
|
renameFolder,
|
||||||
@ -456,6 +457,7 @@ export default async (): Promise<Resources> => ({
|
|||||||
CanPrintDocument: canPrintDocument,
|
CanPrintDocument: canPrintDocument,
|
||||||
DocumentIdentifierProvider: documentIdentifierProvider,
|
DocumentIdentifierProvider: documentIdentifierProvider,
|
||||||
ControlledDocumentTitleProvider: getControlledDocumentTitle,
|
ControlledDocumentTitleProvider: getControlledDocumentTitle,
|
||||||
|
DocumentMetaTitleProvider: getDocumentMetaTitle,
|
||||||
Comment: comment,
|
Comment: comment,
|
||||||
IsCommentVisible: isCommentVisible
|
IsCommentVisible: isCommentVisible
|
||||||
},
|
},
|
||||||
|
@ -246,6 +246,7 @@ export default mergeIds(documentsId, documents, {
|
|||||||
CanOpenDocument: '' as Resource<(doc?: Doc | Doc[]) => Promise<boolean>>,
|
CanOpenDocument: '' as Resource<(doc?: Doc | Doc[]) => Promise<boolean>>,
|
||||||
CanPrintDocument: '' as Resource<(doc?: Doc | Doc[]) => Promise<boolean>>,
|
CanPrintDocument: '' as Resource<(doc?: Doc | Doc[]) => Promise<boolean>>,
|
||||||
CanTransferDocument: '' as Resource<(doc?: Doc | Doc[]) => Promise<boolean>>,
|
CanTransferDocument: '' as Resource<(doc?: Doc | Doc[]) => Promise<boolean>>,
|
||||||
ControlledDocumentTitleProvider: '' as Resource<(client: Client, ref: Ref<Doc>, doc?: Doc) => Promise<string>>
|
ControlledDocumentTitleProvider: '' as Resource<(client: Client, ref: Ref<Doc>, doc?: Doc) => Promise<string>>,
|
||||||
|
DocumentMetaTitleProvider: '' as Resource<(client: Client, ref: Ref<Doc>, doc?: Doc) => Promise<string>>
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -16,6 +16,7 @@
|
|||||||
import { Ref, SpaceType, WithLookup } from '@hcengineering/core'
|
import { Ref, SpaceType, WithLookup } from '@hcengineering/core'
|
||||||
import { Icon, Label, IconOpenedArrow } from '@hcengineering/ui'
|
import { Icon, Label, IconOpenedArrow } from '@hcengineering/ui'
|
||||||
import { createEventDispatcher } from 'svelte'
|
import { createEventDispatcher } from 'svelte'
|
||||||
|
import setting from '../../plugin'
|
||||||
|
|
||||||
export let selectedTypeId: Ref<SpaceType> | undefined
|
export let selectedTypeId: Ref<SpaceType> | undefined
|
||||||
export let types: WithLookup<SpaceType>[] = []
|
export let types: WithLookup<SpaceType>[] = []
|
||||||
@ -31,6 +32,7 @@
|
|||||||
|
|
||||||
{#each types as type}
|
{#each types as type}
|
||||||
{@const descriptor = type.$lookup?.descriptor}
|
{@const descriptor = type.$lookup?.descriptor}
|
||||||
|
{@const dIcon = descriptor?.icon === '' || descriptor?.icon == null ? setting.icon.Setting : descriptor.icon}
|
||||||
<button
|
<button
|
||||||
class="hulyTaskNavLink-container font-regular-14"
|
class="hulyTaskNavLink-container font-regular-14"
|
||||||
class:selected={type._id === selectedTypeId}
|
class:selected={type._id === selectedTypeId}
|
||||||
@ -39,12 +41,12 @@
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div class="hulyTaskNavLink-avatar">
|
<div class="hulyTaskNavLink-avatar">
|
||||||
{#if descriptor?.icon}
|
|
||||||
<div class="hulyTaskNavLink-icon">
|
<div class="hulyTaskNavLink-icon">
|
||||||
<Icon icon={descriptor?.icon} size="small" fill="currentColor" />
|
{#if dIcon}
|
||||||
</div>
|
<Icon icon={dIcon} size="small" fill="currentColor" />
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
{#if descriptor}
|
{#if descriptor}
|
||||||
<div class="hulyTaskNavLink-content">
|
<div class="hulyTaskNavLink-content">
|
||||||
<span class="hulyTaskNavLink-content__title">{type.name}</span>
|
<span class="hulyTaskNavLink-content__title">{type.name}</span>
|
||||||
|
@ -28,7 +28,7 @@
|
|||||||
TextArea,
|
TextArea,
|
||||||
Toggle
|
Toggle
|
||||||
} from '@hcengineering/ui'
|
} from '@hcengineering/ui'
|
||||||
import { deleteObjects } from '@hcengineering/view-resources'
|
import { deleteObjects, iconsLibrary } from '@hcengineering/view-resources'
|
||||||
|
|
||||||
import settingRes from '../../../plugin'
|
import settingRes from '../../../plugin'
|
||||||
|
|
||||||
@ -117,10 +117,11 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if descriptor !== undefined}
|
{#if descriptor !== undefined}
|
||||||
|
{@const dIcon = descriptor.icon === '' ? settingRes.icon.Setting : descriptor.icon}
|
||||||
<div class="hulyComponent-content__column-group">
|
<div class="hulyComponent-content__column-group">
|
||||||
<div class="hulyComponent-content__header">
|
<div class="hulyComponent-content__header">
|
||||||
<div class="flex gap-1">
|
<div class="flex gap-1">
|
||||||
<ButtonIcon icon={descriptor.icon} size={'large'} kind={'secondary'} dataId={'btnSelectIcon'} />
|
<ButtonIcon icon={dIcon} size={'large'} kind={'secondary'} dataId={'btnSelectIcon'} />
|
||||||
<ModernEditbox
|
<ModernEditbox
|
||||||
kind="ghost"
|
kind="ghost"
|
||||||
size="large"
|
size="large"
|
||||||
|
@ -1,31 +0,0 @@
|
|||||||
<!--
|
|
||||||
// Copyright © 2024 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.
|
|
||||||
-->
|
|
||||||
<script lang="ts">
|
|
||||||
import { ButtonIcon, IconAdd, IconFolder, Label } from '@hcengineering/ui'
|
|
||||||
import { ProjectType, ProjectTypeDescriptor } from '@hcengineering/task'
|
|
||||||
import task from '../../plugin'
|
|
||||||
|
|
||||||
export let type: ProjectType | undefined
|
|
||||||
export let descriptor: ProjectTypeDescriptor | undefined
|
|
||||||
export let disabled: boolean = true
|
|
||||||
</script>
|
|
||||||
|
|
||||||
{#if descriptor !== undefined}
|
|
||||||
<div class="hulyTableAttr-header font-medium-12">
|
|
||||||
<IconFolder size="small" />
|
|
||||||
<span><Label label={task.string.Collections} /></span>
|
|
||||||
<ButtonIcon kind="primary" icon={IconAdd} size="small" dataId={'btnAdd'} {disabled} on:click={() => {}} />
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
@ -1,10 +1,13 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Class, Doc, Ref, toIdMap } from '@hcengineering/core'
|
import { Class, Doc, Ref, toIdMap } from '@hcengineering/core'
|
||||||
|
import { getEmbeddedLabel } from '@hcengineering/platform'
|
||||||
import { getClient } from '@hcengineering/presentation'
|
import { getClient } from '@hcengineering/presentation'
|
||||||
import task, { ProjectType, TaskType } from '@hcengineering/task'
|
import task, { ProjectType, TaskType } from '@hcengineering/task'
|
||||||
import { ButtonKind, ButtonSize, DropdownLabels } from '@hcengineering/ui'
|
import { ButtonKind, ButtonSize, DropdownLabelsIntl, type DropdownIntlItem } from '@hcengineering/ui'
|
||||||
import { createEventDispatcher, onDestroy } from 'svelte'
|
import { createEventDispatcher, onDestroy } from 'svelte'
|
||||||
import { selectedTaskTypeStore, taskTypeStore } from '../..'
|
import { selectedTaskTypeStore, taskTypeStore } from '../..'
|
||||||
|
import TaskTypeListPresenter from './TaskTypeListPresenter.svelte'
|
||||||
|
import TaskTypeIcon from './TaskTypeIcon.svelte'
|
||||||
|
|
||||||
export let value: Ref<TaskType> | undefined
|
export let value: Ref<TaskType> | undefined
|
||||||
export let projectType: Ref<ProjectType> | undefined
|
export let projectType: Ref<ProjectType> | undefined
|
||||||
@ -28,13 +31,18 @@
|
|||||||
(taskTypeDescriptors.get(it.descriptor)?.allowCreate ?? false) &&
|
(taskTypeDescriptors.get(it.descriptor)?.allowCreate ?? false) &&
|
||||||
(baseClass === undefined || client.getHierarchy().isDerived(it.targetClass, baseClass))
|
(baseClass === undefined || client.getHierarchy().isDerived(it.targetClass, baseClass))
|
||||||
)
|
)
|
||||||
.map((it) => ({ id: it._id, label: it.name }))
|
.map((it) => ({
|
||||||
|
id: it._id,
|
||||||
|
label: getEmbeddedLabel(it.name),
|
||||||
|
icon: TaskTypeIcon,
|
||||||
|
iconProps: { value: it }
|
||||||
|
})) as DropdownIntlItem[]
|
||||||
|
|
||||||
$: if (
|
$: if (
|
||||||
(value === undefined && items.length > 0) ||
|
(value === undefined && items.length > 0) ||
|
||||||
(items.length > 0 && items.find((it) => it.id === value) === undefined)
|
(items.length > 0 && items.find((it) => it.id === value) === undefined)
|
||||||
) {
|
) {
|
||||||
value = items[0].id
|
value = items[0].id as Ref<TaskType>
|
||||||
change()
|
change()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -50,16 +58,17 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if projectType !== undefined && (items.length > 1 || showAlways)}
|
{#if projectType !== undefined && (items.length > 1 || showAlways)}
|
||||||
<DropdownLabels
|
<DropdownLabelsIntl
|
||||||
{focusIndex}
|
{focusIndex}
|
||||||
{kind}
|
{kind}
|
||||||
{size}
|
{size}
|
||||||
{items}
|
{items}
|
||||||
{justify}
|
{justify}
|
||||||
{width}
|
{width}
|
||||||
|
icon={TaskTypeIcon}
|
||||||
|
iconProps={value !== undefined ? { value: $taskTypeStore.get(value) } : {}}
|
||||||
dataId={'btnSelectTaskType'}
|
dataId={'btnSelectTaskType'}
|
||||||
bind:selected={value}
|
bind:selected={value}
|
||||||
enableSearch={false}
|
|
||||||
on:selected={change}
|
on:selected={change}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
|
@ -14,28 +14,31 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { AttributeEditor, createQuery, getClient } from '@hcengineering/presentation'
|
|
||||||
import task, { ProjectType, TaskType, calculateStatuses, findStatusAttr } from '@hcengineering/task'
|
|
||||||
import { Ref, SortingOrder, Status } from '@hcengineering/core'
|
import { Ref, SortingOrder, Status } from '@hcengineering/core'
|
||||||
import { Asset, getEmbeddedLabel } from '@hcengineering/platform'
|
import { Asset, getEmbeddedLabel, getResource } from '@hcengineering/platform'
|
||||||
import {
|
import { AttributeEditor, MessageBox, createQuery, getClient } from '@hcengineering/presentation'
|
||||||
Label,
|
|
||||||
showPopup,
|
|
||||||
ButtonIcon,
|
|
||||||
ModernButton,
|
|
||||||
IconSquareExpand,
|
|
||||||
IconAdd,
|
|
||||||
Icon,
|
|
||||||
Scroller
|
|
||||||
} from '@hcengineering/ui'
|
|
||||||
import { IconPicker, statusStore } from '@hcengineering/view-resources'
|
|
||||||
import { ClassAttributes, settingsStore } from '@hcengineering/setting-resources'
|
import { ClassAttributes, settingsStore } from '@hcengineering/setting-resources'
|
||||||
|
import task, { ProjectType, TaskType, calculateStatuses, findStatusAttr } from '@hcengineering/task'
|
||||||
|
import {
|
||||||
|
ButtonIcon,
|
||||||
|
Icon,
|
||||||
|
IconAdd,
|
||||||
|
IconDelete,
|
||||||
|
IconSquareExpand,
|
||||||
|
Label,
|
||||||
|
ModernButton,
|
||||||
|
Scroller,
|
||||||
|
getCurrentLocation,
|
||||||
|
navigate,
|
||||||
|
showPopup
|
||||||
|
} from '@hcengineering/ui'
|
||||||
|
import { IconPicker, deleteObjects, statusStore } from '@hcengineering/view-resources'
|
||||||
import { taskTypeStore } from '../..'
|
import { taskTypeStore } from '../..'
|
||||||
|
import plugin from '../../plugin'
|
||||||
import StatesProjectEditor from '../state/StatesProjectEditor.svelte'
|
import StatesProjectEditor from '../state/StatesProjectEditor.svelte'
|
||||||
import TaskTypeKindEditor from '../taskTypes/TaskTypeKindEditor.svelte'
|
import TaskTypeKindEditor from '../taskTypes/TaskTypeKindEditor.svelte'
|
||||||
import TaskTypeRefEditor from '../taskTypes/TaskTypeRefEditor.svelte'
|
import TaskTypeRefEditor from '../taskTypes/TaskTypeRefEditor.svelte'
|
||||||
import TaskTypeIcon from './TaskTypeIcon.svelte'
|
import TaskTypeIcon from './TaskTypeIcon.svelte'
|
||||||
import plugin from '../../plugin'
|
|
||||||
|
|
||||||
export let spaceType: ProjectType
|
export let spaceType: ProjectType
|
||||||
export let objectId: Ref<TaskType>
|
export let objectId: Ref<TaskType>
|
||||||
@ -65,13 +68,15 @@
|
|||||||
$: states = (taskType?.statuses.map((p) => $statusStore.byId.get(p)).filter((p) => p !== undefined) as Status[]) ?? []
|
$: states = (taskType?.statuses.map((p) => $statusStore.byId.get(p)).filter((p) => p !== undefined) as Status[]) ?? []
|
||||||
|
|
||||||
let tasksCounter: number = 0
|
let tasksCounter: number = 0
|
||||||
|
let loading: boolean = true
|
||||||
const tasksCounterQuery = createQuery()
|
const tasksCounterQuery = createQuery()
|
||||||
$: if (taskType !== undefined) {
|
$: if (taskType !== undefined) {
|
||||||
tasksCounterQuery.query(
|
loading = tasksCounterQuery.query(
|
||||||
task.class.Task,
|
task.class.Task,
|
||||||
{ kind: taskType._id },
|
{ kind: taskType._id },
|
||||||
(res) => {
|
(res) => {
|
||||||
tasksCounter = res.total
|
tasksCounter = res.total
|
||||||
|
loading = false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
total: true,
|
total: true,
|
||||||
@ -87,6 +92,7 @@
|
|||||||
if (readonly) {
|
if (readonly) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
// TODO: Be aware icon equal to descriptor one will not be shown in lists.
|
||||||
const icons: Asset[] = [descriptor[0].icon]
|
const icons: Asset[] = [descriptor[0].icon]
|
||||||
|
|
||||||
showPopup(
|
showPopup(
|
||||||
@ -125,6 +131,41 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$: canDelete = !loading && tasksCounter === 0
|
||||||
|
|
||||||
|
async function handleDelete (): Promise<void> {
|
||||||
|
if (!canDelete || readonly || taskType == null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
showPopup(MessageBox, {
|
||||||
|
label: plugin.string.Delete,
|
||||||
|
message: plugin.string.Delete,
|
||||||
|
action: async () => {
|
||||||
|
if (taskType == null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
await deleteObjects(client, [taskType])
|
||||||
|
|
||||||
|
const loc = getCurrentLocation()
|
||||||
|
loc.path.length -= 2
|
||||||
|
navigate(loc)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
async function showIssuesOfTaskType (): Promise<void> {
|
||||||
|
if (taskType == null) return
|
||||||
|
const descriptor = client
|
||||||
|
.getModel()
|
||||||
|
.findAllSync(task.class.TaskTypeDescriptor, { _id: taskType?.descriptor })
|
||||||
|
.shift()
|
||||||
|
if (descriptor?.openTasks !== undefined) {
|
||||||
|
const f = await getResource(descriptor.openTasks)
|
||||||
|
await f?.(taskType)
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if taskType !== undefined}
|
{#if taskType !== undefined}
|
||||||
@ -157,6 +198,7 @@
|
|||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
<div class="flex-row">
|
||||||
<ModernButton
|
<ModernButton
|
||||||
icon={IconSquareExpand}
|
icon={IconSquareExpand}
|
||||||
label={plugin.string.CountTasks}
|
label={plugin.string.CountTasks}
|
||||||
@ -165,7 +207,20 @@
|
|||||||
kind={'tertiary'}
|
kind={'tertiary'}
|
||||||
size={'medium'}
|
size={'medium'}
|
||||||
hasMenu
|
hasMenu
|
||||||
|
on:click={() => {
|
||||||
|
showIssuesOfTaskType()
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
|
{#if canDelete}
|
||||||
|
<ButtonIcon
|
||||||
|
icon={IconDelete}
|
||||||
|
size="small"
|
||||||
|
kind="secondary"
|
||||||
|
disabled={readonly}
|
||||||
|
on:click={handleDelete}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="name" class:editable={!readonly}>
|
<div class="name" class:editable={!readonly}>
|
||||||
|
@ -0,0 +1,35 @@
|
|||||||
|
<!--
|
||||||
|
// Copyright © 2025 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.
|
||||||
|
-->
|
||||||
|
<script lang="ts">
|
||||||
|
import { Ref } from '@hcengineering/core'
|
||||||
|
import type { TaskType } from '@hcengineering/task'
|
||||||
|
import view from '@hcengineering/view'
|
||||||
|
import { taskTypeStore } from '../../'
|
||||||
|
import TaskTypeIcon from './TaskTypeIcon.svelte'
|
||||||
|
import { getClient } from '@hcengineering/presentation'
|
||||||
|
import task from '@hcengineering/task'
|
||||||
|
|
||||||
|
export let value: TaskType | Ref<TaskType> | undefined
|
||||||
|
|
||||||
|
$: _value = typeof value === 'string' ? $taskTypeStore.get(value) : value
|
||||||
|
$: descriptor = getClient().getModel().findAllSync(task.class.TaskTypeDescriptor, { _id: _value?.descriptor }).shift()
|
||||||
|
$: visible = !(_value?.icon === descriptor?.icon)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if _value !== undefined && visible}
|
||||||
|
<span class="label flex-row-center gap-1 ml-3">
|
||||||
|
<TaskTypeIcon value={_value} size={'small'} />
|
||||||
|
</span>
|
||||||
|
{/if}
|
@ -64,10 +64,10 @@ import ProjectTypeClassPresenter from './components/taskTypes/ProjectTypeClassPr
|
|||||||
import TaskKindSelector from './components/taskTypes/TaskKindSelector.svelte'
|
import TaskKindSelector from './components/taskTypes/TaskKindSelector.svelte'
|
||||||
import TaskTypeClassPresenter from './components/taskTypes/TaskTypeClassPresenter.svelte'
|
import TaskTypeClassPresenter from './components/taskTypes/TaskTypeClassPresenter.svelte'
|
||||||
import TaskTypePresenter from './components/taskTypes/TaskTypePresenter.svelte'
|
import TaskTypePresenter from './components/taskTypes/TaskTypePresenter.svelte'
|
||||||
|
import TaskTypeListPresenter from './components/taskTypes/TaskTypeListPresenter.svelte'
|
||||||
|
|
||||||
import CreateProjectType from './components/projectTypes/CreateProjectType.svelte'
|
import CreateProjectType from './components/projectTypes/CreateProjectType.svelte'
|
||||||
import ProjectTypeAutomationsSectionEditor from './components/projectTypes/ProjectTypeAutomationsSectionEditor.svelte'
|
import ProjectTypeAutomationsSectionEditor from './components/projectTypes/ProjectTypeAutomationsSectionEditor.svelte'
|
||||||
import ProjectTypeCollectionsSectionEditor from './components/projectTypes/ProjectTypeCollectionsSectionEditor.svelte'
|
|
||||||
import ProjectTypeGeneralSectionEditor from './components/projectTypes/ProjectTypeGeneralSectionEditor.svelte'
|
import ProjectTypeGeneralSectionEditor from './components/projectTypes/ProjectTypeGeneralSectionEditor.svelte'
|
||||||
import ProjectTypeSelector from './components/projectTypes/ProjectTypeSelector.svelte'
|
import ProjectTypeSelector from './components/projectTypes/ProjectTypeSelector.svelte'
|
||||||
import ProjectTypeTasksTypeSectionEditor from './components/projectTypes/ProjectTypeTasksTypeSectionEditor.svelte'
|
import ProjectTypeTasksTypeSectionEditor from './components/projectTypes/ProjectTypeTasksTypeSectionEditor.svelte'
|
||||||
@ -124,6 +124,7 @@ export default async (): Promise<Resources> => ({
|
|||||||
StateIconPresenter,
|
StateIconPresenter,
|
||||||
StatusFilter,
|
StatusFilter,
|
||||||
TaskTypePresenter,
|
TaskTypePresenter,
|
||||||
|
TaskTypeListPresenter,
|
||||||
TaskTypeClassPresenter,
|
TaskTypeClassPresenter,
|
||||||
ProjectTypeClassPresenter,
|
ProjectTypeClassPresenter,
|
||||||
ProjectTypePresenter,
|
ProjectTypePresenter,
|
||||||
@ -132,7 +133,6 @@ export default async (): Promise<Resources> => ({
|
|||||||
ProjectTypeGeneralSectionEditor,
|
ProjectTypeGeneralSectionEditor,
|
||||||
ProjectTypeTasksTypeSectionEditor,
|
ProjectTypeTasksTypeSectionEditor,
|
||||||
ProjectTypeAutomationsSectionEditor,
|
ProjectTypeAutomationsSectionEditor,
|
||||||
ProjectTypeCollectionsSectionEditor,
|
|
||||||
TaskTypeEditor
|
TaskTypeEditor
|
||||||
},
|
},
|
||||||
actionImpl: {
|
actionImpl: {
|
||||||
|
@ -104,7 +104,6 @@ export default mergeIds(taskId, task, {
|
|||||||
ProjectTypeGeneralSectionEditor: '' as AnyComponent,
|
ProjectTypeGeneralSectionEditor: '' as AnyComponent,
|
||||||
ProjectTypeTasksTypeSectionEditor: '' as AnyComponent,
|
ProjectTypeTasksTypeSectionEditor: '' as AnyComponent,
|
||||||
ProjectTypeAutomationsSectionEditor: '' as AnyComponent,
|
ProjectTypeAutomationsSectionEditor: '' as AnyComponent,
|
||||||
ProjectTypeCollectionsSectionEditor: '' as AnyComponent,
|
|
||||||
TaskTypeEditor: '' as AnyComponent
|
TaskTypeEditor: '' as AnyComponent
|
||||||
},
|
},
|
||||||
function: {
|
function: {
|
||||||
|
@ -94,6 +94,8 @@ export interface TaskTypeDescriptor extends Doc {
|
|||||||
// If specified, will allow to be created by users, system type overwise
|
// If specified, will allow to be created by users, system type overwise
|
||||||
allowCreate: boolean
|
allowCreate: boolean
|
||||||
statusCategoriesFunc?: Resource<(project: ProjectType) => Ref<StatusCategory>[]>
|
statusCategoriesFunc?: Resource<(project: ProjectType) => Ref<StatusCategory>[]>
|
||||||
|
|
||||||
|
openTasks?: Resource<(value: TaskType) => Promise<void>>
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1,179 +0,0 @@
|
|||||||
import { Node, mergeAttributes } from '@tiptap/core'
|
|
||||||
import { Plugin, PluginKey } from '@tiptap/pm/state'
|
|
||||||
import { getDataAttribute } from './utils'
|
|
||||||
|
|
||||||
import Suggestion, { type SuggestionOptions } from './components/extension/suggestion'
|
|
||||||
|
|
||||||
export interface CompletionOptions {
|
|
||||||
HTMLAttributes: Record<string, any>
|
|
||||||
renderLabel: (props: { options: CompletionOptions, node: any }) => string
|
|
||||||
suggestion: Omit<SuggestionOptions, 'editor'>
|
|
||||||
showDoc?: (event: MouseEvent, _id: string, _class: string) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
export function clickHandler (opt: CompletionOptions): Plugin {
|
|
||||||
return new Plugin({
|
|
||||||
key: new PluginKey('completion-handleClickLink'),
|
|
||||||
props: {
|
|
||||||
handleClick: (view, pos, event) => {
|
|
||||||
if (event.button !== 0) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
const link = (event.target as HTMLElement)?.closest('span')
|
|
||||||
if (link != null) {
|
|
||||||
const _class = link.getAttribute('data-objectclass')
|
|
||||||
const _id = link.getAttribute('data-id')
|
|
||||||
if (_id != null && _class != null) {
|
|
||||||
opt.showDoc?.(event, _id, _class)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export const Completion = Node.create<CompletionOptions>({
|
|
||||||
name: 'reference',
|
|
||||||
|
|
||||||
addOptions () {
|
|
||||||
return {
|
|
||||||
HTMLAttributes: {},
|
|
||||||
renderLabel ({ options, node }) {
|
|
||||||
// eslint-disable-next-line
|
|
||||||
return `${options.suggestion.char}${node.attrs.label ?? node.attrs.id}`
|
|
||||||
},
|
|
||||||
suggestion: {
|
|
||||||
char: '@',
|
|
||||||
allowSpaces: true,
|
|
||||||
// pluginKey: CompletionPluginKey,
|
|
||||||
command: ({ editor, range, props }) => {
|
|
||||||
// increase range.to by one when the next node is of type "text"
|
|
||||||
// and starts with a space character
|
|
||||||
const nodeAfter = editor.view.state.selection.$to.nodeAfter
|
|
||||||
const overrideSpace = nodeAfter?.text?.startsWith(' ')
|
|
||||||
|
|
||||||
if (overrideSpace !== undefined && overrideSpace) {
|
|
||||||
// eslint-disable-next-line
|
|
||||||
range.to += 1
|
|
||||||
}
|
|
||||||
|
|
||||||
if (props !== null) {
|
|
||||||
editor
|
|
||||||
.chain()
|
|
||||||
.focus()
|
|
||||||
.insertContentAt(range, [
|
|
||||||
{
|
|
||||||
type: this.name,
|
|
||||||
attrs: props
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'text',
|
|
||||||
text: ' '
|
|
||||||
}
|
|
||||||
])
|
|
||||||
.run()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
allow: ({ editor, range }) => {
|
|
||||||
if (range.from > editor.state.doc.content.size) return false
|
|
||||||
const $from = editor.state.doc.resolve(range.from)
|
|
||||||
const type = editor.schema.nodes[this.name]
|
|
||||||
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
|
|
||||||
return !!$from.parent.type.contentMatch.matchType(type)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
group: 'inline',
|
|
||||||
|
|
||||||
inline: true,
|
|
||||||
|
|
||||||
selectable: true,
|
|
||||||
|
|
||||||
atom: true,
|
|
||||||
|
|
||||||
draggable: true,
|
|
||||||
|
|
||||||
addAttributes () {
|
|
||||||
return {
|
|
||||||
id: getDataAttribute('id'),
|
|
||||||
label: getDataAttribute('label'),
|
|
||||||
objectclass: getDataAttribute('objectclass')
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
parseHTML () {
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
tag: `span[data-type="${this.name}"]`
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
|
|
||||||
renderHTML ({ node, HTMLAttributes }) {
|
|
||||||
return [
|
|
||||||
'span',
|
|
||||||
mergeAttributes(
|
|
||||||
{
|
|
||||||
'data-type': this.name,
|
|
||||||
class: 'antiMention'
|
|
||||||
},
|
|
||||||
this.options.HTMLAttributes,
|
|
||||||
HTMLAttributes
|
|
||||||
),
|
|
||||||
this.options.renderLabel({
|
|
||||||
options: this.options,
|
|
||||||
node
|
|
||||||
})
|
|
||||||
]
|
|
||||||
},
|
|
||||||
|
|
||||||
renderText ({ node }) {
|
|
||||||
return this.options.renderLabel({
|
|
||||||
options: this.options,
|
|
||||||
node
|
|
||||||
})
|
|
||||||
},
|
|
||||||
|
|
||||||
addKeyboardShortcuts () {
|
|
||||||
return {
|
|
||||||
Backspace: () =>
|
|
||||||
this.editor.commands.command(({ tr, state }) => {
|
|
||||||
let isMention = false
|
|
||||||
const { selection } = state
|
|
||||||
const { empty, anchor } = selection
|
|
||||||
|
|
||||||
if (!empty) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
state.doc.nodesBetween(anchor - 1, anchor, (node, pos) => {
|
|
||||||
if (node.type.name === this.name) {
|
|
||||||
isMention = true
|
|
||||||
|
|
||||||
// eslint-disable-next-line
|
|
||||||
tr.insertText(this.options.suggestion.char || '', pos, pos + node.nodeSize)
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return isMention
|
|
||||||
})
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
addProseMirrorPlugins () {
|
|
||||||
return [
|
|
||||||
Suggestion({
|
|
||||||
editor: this.editor,
|
|
||||||
...this.options.suggestion
|
|
||||||
}),
|
|
||||||
clickHandler(this.options)
|
|
||||||
]
|
|
||||||
}
|
|
||||||
})
|
|
@ -66,7 +66,6 @@
|
|||||||
TextEditorHandler
|
TextEditorHandler
|
||||||
} from '@hcengineering/text-editor'
|
} from '@hcengineering/text-editor'
|
||||||
import { EditorKitOptions, getEditorKit } from '../../src/kits/editor-kit'
|
import { EditorKitOptions, getEditorKit } from '../../src/kits/editor-kit'
|
||||||
import { Completion } from '../Completion'
|
|
||||||
import { deleteAttachment } from '../command/deleteAttachment'
|
import { deleteAttachment } from '../command/deleteAttachment'
|
||||||
import { textEditorCommandHandler } from '../commands'
|
import { textEditorCommandHandler } from '../commands'
|
||||||
import { Provider } from '../provider/types'
|
import { Provider } from '../provider/types'
|
||||||
@ -84,8 +83,9 @@
|
|||||||
import { InlineCommentCollaborationExtension } from './extension/inlineComment'
|
import { InlineCommentCollaborationExtension } from './extension/inlineComment'
|
||||||
import { LeftMenuExtension } from './extension/leftMenu'
|
import { LeftMenuExtension } from './extension/leftMenu'
|
||||||
import { mermaidOptions } from './extension/mermaid'
|
import { mermaidOptions } from './extension/mermaid'
|
||||||
|
import { ReferenceExtension, referenceConfig } from './extension/reference'
|
||||||
import { type FileAttachFunction } from './extension/types'
|
import { type FileAttachFunction } from './extension/types'
|
||||||
import { completionConfig, inlineCommandsConfig } from './extensions'
|
import { inlineCommandsConfig } from './extensions'
|
||||||
|
|
||||||
export let object: Doc
|
export let object: Doc
|
||||||
export let attribute: KeyedAttribute
|
export let attribute: KeyedAttribute
|
||||||
@ -495,8 +495,8 @@
|
|||||||
render: renderCursor,
|
render: renderCursor,
|
||||||
selectionRender: noSelectionRender
|
selectionRender: noSelectionRender
|
||||||
}),
|
}),
|
||||||
Completion.configure({
|
ReferenceExtension.configure({
|
||||||
...completionConfig,
|
...referenceConfig,
|
||||||
showDoc (event: MouseEvent, _id: string, _class: string) {
|
showDoc (event: MouseEvent, _id: string, _class: string) {
|
||||||
dispatch('open-document', { event, _id, _class })
|
dispatch('open-document', { event, _id, _class })
|
||||||
}
|
}
|
||||||
|
@ -15,23 +15,13 @@
|
|||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { SearchResultDoc } from '@hcengineering/core'
|
import { SearchResultDoc } from '@hcengineering/core'
|
||||||
import { getResource } from '@hcengineering/platform'
|
import presentation, { SearchResult, reduceCalls, searchFor, type SearchItem } from '@hcengineering/presentation'
|
||||||
import presentation, {
|
|
||||||
SearchResult,
|
|
||||||
getClient,
|
|
||||||
reduceCalls,
|
|
||||||
searchFor,
|
|
||||||
type SearchItem
|
|
||||||
} from '@hcengineering/presentation'
|
|
||||||
import { Label, ListView, resizeObserver } from '@hcengineering/ui'
|
import { Label, ListView, resizeObserver } from '@hcengineering/ui'
|
||||||
import view from '@hcengineering/view'
|
|
||||||
import { createEventDispatcher } from 'svelte'
|
import { createEventDispatcher } from 'svelte'
|
||||||
|
import { getReferenceLabel } from './extension/reference'
|
||||||
|
|
||||||
export let query: string = ''
|
export let query: string = ''
|
||||||
|
|
||||||
const client = getClient()
|
|
||||||
const hierarchy = client.getHierarchy()
|
|
||||||
|
|
||||||
let items: SearchItem[] = []
|
let items: SearchItem[] = []
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
@ -40,21 +30,11 @@
|
|||||||
let scrollContainer: HTMLElement
|
let scrollContainer: HTMLElement
|
||||||
let selection = 0
|
let selection = 0
|
||||||
|
|
||||||
async function getIdentifier (item: SearchResultDoc): Promise<string | undefined> {
|
|
||||||
const identifierProvider = hierarchy.classHierarchyMixin(item.doc._class, view.mixin.ObjectIdentifier)
|
|
||||||
if (identifierProvider === undefined) {
|
|
||||||
return item.shortTitle ?? item.title
|
|
||||||
}
|
|
||||||
|
|
||||||
const resource = await getResource(identifierProvider.provider)
|
|
||||||
return await resource(client, item.id)
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleSelectItem (item: SearchResultDoc): Promise<void> {
|
async function handleSelectItem (item: SearchResultDoc): Promise<void> {
|
||||||
const identifier = await getIdentifier(item)
|
const label = await getReferenceLabel(item.doc._class, item.doc._id)
|
||||||
dispatch('close', {
|
dispatch('close', {
|
||||||
id: item.id,
|
id: item.id,
|
||||||
label: identifier,
|
label,
|
||||||
objectclass: item.doc._class
|
objectclass: item.doc._class
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -16,28 +16,27 @@
|
|||||||
import { Markup } from '@hcengineering/core'
|
import { Markup } from '@hcengineering/core'
|
||||||
import { Asset, IntlString } from '@hcengineering/platform'
|
import { Asset, IntlString } from '@hcengineering/platform'
|
||||||
import { EmptyMarkup, isEmptyMarkup } from '@hcengineering/text'
|
import { EmptyMarkup, isEmptyMarkup } from '@hcengineering/text'
|
||||||
|
import textEditor, { RefAction, TextEditorHandler } from '@hcengineering/text-editor'
|
||||||
import {
|
import {
|
||||||
AnySvelteComponent,
|
AnySvelteComponent,
|
||||||
Button,
|
Button,
|
||||||
ButtonKind,
|
ButtonKind,
|
||||||
handler,
|
|
||||||
registerFocus,
|
|
||||||
deviceOptionsStore as deviceInfo,
|
|
||||||
checkAdaptiveMatching,
|
checkAdaptiveMatching,
|
||||||
IconClose
|
deviceOptionsStore as deviceInfo,
|
||||||
|
handler,
|
||||||
|
IconClose,
|
||||||
|
registerFocus
|
||||||
} from '@hcengineering/ui'
|
} from '@hcengineering/ui'
|
||||||
import { createEventDispatcher } from 'svelte'
|
|
||||||
import { FocusPosition } from '@tiptap/core'
|
import { FocusPosition } from '@tiptap/core'
|
||||||
import { EditorView } from '@tiptap/pm/view'
|
import { EditorView } from '@tiptap/pm/view'
|
||||||
import textEditor, { RefAction, TextEditorHandler } from '@hcengineering/text-editor'
|
import { createEventDispatcher } from 'svelte'
|
||||||
|
|
||||||
import { Completion } from '../Completion'
|
import view from '@hcengineering/view'
|
||||||
import TextEditor from './TextEditor.svelte'
|
import TextEditor from './TextEditor.svelte'
|
||||||
import { defaultRefActions, getModelRefActions } from './editor/actions'
|
import { defaultRefActions, getModelRefActions } from './editor/actions'
|
||||||
import { completionConfig } from './extensions'
|
|
||||||
import { EmojiExtension } from './extension/emoji'
|
import { EmojiExtension } from './extension/emoji'
|
||||||
import { IsEmptyContentExtension } from './extension/isEmptyContent'
|
import { IsEmptyContentExtension } from './extension/isEmptyContent'
|
||||||
import view from '@hcengineering/view'
|
import { referenceConfig, ReferenceExtension } from './extension/reference'
|
||||||
import Send from './icons/Send.svelte'
|
import Send from './icons/Send.svelte'
|
||||||
|
|
||||||
export let content: Markup = EmptyMarkup
|
export let content: Markup = EmptyMarkup
|
||||||
@ -141,8 +140,8 @@
|
|||||||
focusManager?.setFocus(idx)
|
focusManager?.setFocus(idx)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const completionPlugin = Completion.configure({
|
const completionPlugin = ReferenceExtension.configure({
|
||||||
...completionConfig,
|
...referenceConfig,
|
||||||
showDoc (event: MouseEvent, _id: string, _class: string) {
|
showDoc (event: MouseEvent, _id: string, _class: string) {
|
||||||
dispatch('open-document', { event, _id, _class })
|
dispatch('open-document', { event, _id, _class })
|
||||||
}
|
}
|
||||||
|
@ -2,14 +2,13 @@
|
|||||||
import { Markup } from '@hcengineering/core'
|
import { Markup } from '@hcengineering/core'
|
||||||
import { IntlString } from '@hcengineering/platform'
|
import { IntlString } from '@hcengineering/platform'
|
||||||
import { EmptyMarkup } from '@hcengineering/text'
|
import { EmptyMarkup } from '@hcengineering/text'
|
||||||
|
import textEditor from '@hcengineering/text-editor'
|
||||||
import { ButtonSize, Label } from '@hcengineering/ui'
|
import { ButtonSize, Label } from '@hcengineering/ui'
|
||||||
import { AnyExtension } from '@tiptap/core'
|
import { AnyExtension } from '@tiptap/core'
|
||||||
import { createEventDispatcher } from 'svelte'
|
import { createEventDispatcher } from 'svelte'
|
||||||
import textEditor from '@hcengineering/text-editor'
|
|
||||||
|
|
||||||
|
import { referenceConfig, ReferenceExtension } from './extension/reference'
|
||||||
import StyledTextEditor from './StyledTextEditor.svelte'
|
import StyledTextEditor from './StyledTextEditor.svelte'
|
||||||
import { Completion } from '../Completion'
|
|
||||||
import { completionConfig } from './extensions'
|
|
||||||
|
|
||||||
export let label: IntlString | undefined = undefined
|
export let label: IntlString | undefined = undefined
|
||||||
export let content: Markup | undefined
|
export let content: Markup | undefined
|
||||||
@ -53,8 +52,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function configureExtensions (): AnyExtension[] {
|
function configureExtensions (): AnyExtension[] {
|
||||||
const completionPlugin = Completion.configure({
|
const completionPlugin = ReferenceExtension.configure({
|
||||||
...completionConfig,
|
...referenceConfig,
|
||||||
showDoc (event: MouseEvent, _id: string, _class: string) {
|
showDoc (event: MouseEvent, _id: string, _class: string) {
|
||||||
dispatch('open-document', { event, _id, _class })
|
dispatch('open-document', { event, _id, _class })
|
||||||
}
|
}
|
||||||
|
@ -21,7 +21,6 @@
|
|||||||
import type { AnyExtension } from '@tiptap/core'
|
import type { AnyExtension } from '@tiptap/core'
|
||||||
import { createEventDispatcher } from 'svelte'
|
import { createEventDispatcher } from 'svelte'
|
||||||
|
|
||||||
import { Completion } from '../Completion'
|
|
||||||
import StyledTextEditor from './StyledTextEditor.svelte'
|
import StyledTextEditor from './StyledTextEditor.svelte'
|
||||||
|
|
||||||
import { addTableHandler } from '../utils'
|
import { addTableHandler } from '../utils'
|
||||||
@ -29,8 +28,9 @@
|
|||||||
import { FocusExtension } from './extension/focus'
|
import { FocusExtension } from './extension/focus'
|
||||||
import { ImageUploadExtension } from './extension/imageUploadExt'
|
import { ImageUploadExtension } from './extension/imageUploadExt'
|
||||||
import { InlineCommandsExtension } from './extension/inlineCommands'
|
import { InlineCommandsExtension } from './extension/inlineCommands'
|
||||||
|
import { ReferenceExtension, referenceConfig } from './extension/reference'
|
||||||
import { type FileAttachFunction } from './extension/types'
|
import { type FileAttachFunction } from './extension/types'
|
||||||
import { completionConfig, InlineCommandId, inlineCommandsConfig } from './extensions'
|
import { InlineCommandId, inlineCommandsConfig } from './extensions'
|
||||||
|
|
||||||
export let label: IntlString | undefined = undefined
|
export let label: IntlString | undefined = undefined
|
||||||
export let content: Markup
|
export let content: Markup
|
||||||
@ -179,8 +179,8 @@
|
|||||||
getFileUrl
|
getFileUrl
|
||||||
})
|
})
|
||||||
|
|
||||||
const completionPlugin = Completion.configure({
|
const completionPlugin = ReferenceExtension.configure({
|
||||||
...completionConfig,
|
...referenceConfig,
|
||||||
showDoc (event: MouseEvent, _id: string, _class: string) {
|
showDoc (event: MouseEvent, _id: string, _class: string) {
|
||||||
dispatch('open-document', { event, _id, _class })
|
dispatch('open-document', { event, _id, _class })
|
||||||
}
|
}
|
||||||
|
@ -13,14 +13,8 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
//
|
//
|
||||||
|
|
||||||
import { type Class, type Doc, type Ref } from '@hcengineering/core'
|
import { showPopup } from '@hcengineering/ui'
|
||||||
import { getMetadata, getResource } from '@hcengineering/platform'
|
import { Extension } from '@tiptap/core'
|
||||||
import presentation, { getClient } from '@hcengineering/presentation'
|
|
||||||
import { parseLocation, showPopup } from '@hcengineering/ui'
|
|
||||||
import view from '@hcengineering/view'
|
|
||||||
import workbench, { type Application } from '@hcengineering/workbench'
|
|
||||||
import { type Editor, Extension } from '@tiptap/core'
|
|
||||||
import { Plugin, PluginKey } from '@tiptap/pm/state'
|
|
||||||
import LinkPopup from '../LinkPopup.svelte'
|
import LinkPopup from '../LinkPopup.svelte'
|
||||||
|
|
||||||
export const LinkUtilsExtension = Extension.create<any>({
|
export const LinkUtilsExtension = Extension.create<any>({
|
||||||
@ -48,153 +42,6 @@ export const LinkUtilsExtension = Extension.create<any>({
|
|||||||
},
|
},
|
||||||
|
|
||||||
addProseMirrorPlugins () {
|
addProseMirrorPlugins () {
|
||||||
return [ResolveReferenceUrlsPlugin(this.editor)]
|
return []
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
export interface LinkToReferencePluginState {
|
|
||||||
references: Map<string, { id: Ref<Doc>, objectclass: Ref<Class<Doc>>, label: string }>
|
|
||||||
queue: Set<string>
|
|
||||||
}
|
|
||||||
|
|
||||||
const resolveReferencePluginKey = new PluginKey<LinkToReferencePluginState>('linkToReference')
|
|
||||||
|
|
||||||
interface ReferenceProps {
|
|
||||||
id: Ref<Doc>
|
|
||||||
objectclass: Ref<Class<Doc>>
|
|
||||||
label: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ResolveReferenceUrlsPlugin (editor: Editor): Plugin<LinkToReferencePluginState> {
|
|
||||||
return new Plugin<LinkToReferencePluginState>({
|
|
||||||
key: resolveReferencePluginKey,
|
|
||||||
|
|
||||||
appendTransaction: (transactions, oldState, newState) => {
|
|
||||||
if (transactions[0]?.getMeta('linkToReference') === undefined) return undefined
|
|
||||||
if (editor.schema.nodes.reference === undefined) return
|
|
||||||
|
|
||||||
const references = resolveReferencePluginKey.getState(newState)?.references ?? new Map()
|
|
||||||
|
|
||||||
const { tr } = newState
|
|
||||||
tr.doc.descendants((node, pos) => {
|
|
||||||
if (!node.isText || !node.marks.some((m) => m.type.name === 'link')) return
|
|
||||||
|
|
||||||
const url = node.textContent
|
|
||||||
const mapping = references.get(url)
|
|
||||||
if (mapping === undefined) return
|
|
||||||
|
|
||||||
const replacementNode = editor.schema.nodes.reference.create(mapping)
|
|
||||||
const mpos = tr.mapping.map(pos)
|
|
||||||
tr.replaceWith(mpos, mpos + node.nodeSize, replacementNode)
|
|
||||||
})
|
|
||||||
|
|
||||||
if (tr.steps.length > 0) return tr
|
|
||||||
},
|
|
||||||
|
|
||||||
state: {
|
|
||||||
init () {
|
|
||||||
return {
|
|
||||||
references: new Map(),
|
|
||||||
queue: new Set()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
apply (tr, prev, oldState, newState) {
|
|
||||||
if (tr.getMeta('linkToReference') !== undefined) {
|
|
||||||
const references = tr.getMeta('linkToReference').references as LinkToReferencePluginState['references']
|
|
||||||
const urls = new Set(references.keys())
|
|
||||||
return {
|
|
||||||
queue: new Set(Array.from(prev.queue).filter((url) => !urls.has(url))),
|
|
||||||
references: new Map([...prev.references, ...references])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!tr.docChanged || oldState.doc.eq(newState.doc)) return prev
|
|
||||||
|
|
||||||
const urls: string[] = []
|
|
||||||
tr.doc.descendants((node) => {
|
|
||||||
if (!node.isText || !node.marks.some((m) => m.type.name === 'link')) return
|
|
||||||
const url = node.textContent
|
|
||||||
|
|
||||||
const hasNoMapping = prev.references.has(url) && prev.references.get(url) === undefined
|
|
||||||
if (prev.queue.has(url) || hasNoMapping) return
|
|
||||||
|
|
||||||
urls.push(url)
|
|
||||||
})
|
|
||||||
|
|
||||||
const promises = urls.map(async (url) => {
|
|
||||||
try {
|
|
||||||
return [url, await getReferenceFromUrl(url)] as const
|
|
||||||
} catch {
|
|
||||||
return [url, undefined] as const
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
if (promises.length > 0) {
|
|
||||||
void Promise.all(promises).then((references) => {
|
|
||||||
editor.view.dispatch(editor.state.tr.setMeta('linkToReference', { references: new Map(references) }))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
references: prev.references,
|
|
||||||
queue: new Set([...prev.queue, ...urls])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getReferenceFromUrl (text: string): Promise<ReferenceProps | undefined> {
|
|
||||||
const client = getClient()
|
|
||||||
const hierarchy = client.getHierarchy()
|
|
||||||
|
|
||||||
const url = new URL(text)
|
|
||||||
|
|
||||||
const frontUrl = getMetadata(presentation.metadata.FrontUrl) ?? window.location.origin
|
|
||||||
if (url.origin !== frontUrl) return
|
|
||||||
|
|
||||||
const location = parseLocation(url)
|
|
||||||
|
|
||||||
const appAlias = (location.path[2] ?? '').trim()
|
|
||||||
if (!(appAlias.length > 0)) return
|
|
||||||
|
|
||||||
const excludedApps = getMetadata(workbench.metadata.ExcludedApplications) ?? []
|
|
||||||
const apps: Application[] = client
|
|
||||||
.getModel()
|
|
||||||
.findAllSync<Application>(workbench.class.Application, { hidden: false, _id: { $nin: excludedApps } })
|
|
||||||
|
|
||||||
const app = apps.find((p) => p.alias === appAlias)
|
|
||||||
|
|
||||||
if (app?.locationResolver === undefined) return
|
|
||||||
const locationResolverFn = await getResource(app.locationResolver)
|
|
||||||
const resolvedLocation = await locationResolverFn(location)
|
|
||||||
|
|
||||||
const locationParts = decodeURIComponent(resolvedLocation?.loc?.fragment ?? '').split('|')
|
|
||||||
const id = locationParts[1] as Ref<Doc>
|
|
||||||
const objectclass = locationParts[2] as Ref<Class<Doc>>
|
|
||||||
if (id === undefined || objectclass === undefined) return
|
|
||||||
|
|
||||||
const linkProviders = client.getModel().findAllSync(view.mixin.LinkIdProvider, {})
|
|
||||||
const linkProvider = linkProviders.find(({ _id }) => hierarchy.isDerived(objectclass, _id))
|
|
||||||
const _id: Ref<Doc> | undefined =
|
|
||||||
linkProvider !== undefined ? (await (await getResource(linkProvider.decode))(id)) ?? id : id
|
|
||||||
|
|
||||||
let label = ''
|
|
||||||
const labelProvider = hierarchy.classHierarchyMixin(objectclass, view.mixin.ObjectIdentifier)
|
|
||||||
if (labelProvider !== undefined) {
|
|
||||||
const resource = await getResource(labelProvider.provider)
|
|
||||||
label = await resource(client, _id)
|
|
||||||
} else {
|
|
||||||
const titleMixin = hierarchy.classHierarchyMixin(objectclass, view.mixin.ObjectTitle)
|
|
||||||
if (titleMixin === undefined) return
|
|
||||||
|
|
||||||
const titleProviderFn = await getResource(titleMixin.titleProvider)
|
|
||||||
label = await titleProviderFn(client, _id)
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: _id,
|
|
||||||
objectclass,
|
|
||||||
label
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -0,0 +1,374 @@
|
|||||||
|
import { mergeAttributes, type Editor } from '@tiptap/core'
|
||||||
|
import { Plugin, PluginKey } from '@tiptap/pm/state'
|
||||||
|
import MentionList from '../MentionList.svelte'
|
||||||
|
import { SvelteRenderer } from '../node-view'
|
||||||
|
|
||||||
|
import { ReferenceNode, type ReferenceNodeProps, type ReferenceOptions } from '@hcengineering/text'
|
||||||
|
import Suggestion, { type SuggestionKeyDownProps, type SuggestionOptions, type SuggestionProps } from './suggestion'
|
||||||
|
|
||||||
|
import { type Class, type Doc, type Ref } from '@hcengineering/core'
|
||||||
|
import { getMetadata, getResource } from '@hcengineering/platform'
|
||||||
|
import presentation, { createQuery, getClient } from '@hcengineering/presentation'
|
||||||
|
import { parseLocation } from '@hcengineering/ui'
|
||||||
|
import view from '@hcengineering/view'
|
||||||
|
import workbench, { type Application } from '@hcengineering/workbench'
|
||||||
|
|
||||||
|
export interface ReferenceExtensionOptions extends ReferenceOptions {
|
||||||
|
suggestion: Omit<SuggestionOptions, 'editor'>
|
||||||
|
showDoc?: (event: MouseEvent, _id: string, _class: string) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ReferenceExtension = ReferenceNode.extend<ReferenceExtensionOptions>({
|
||||||
|
addOptions () {
|
||||||
|
return {
|
||||||
|
HTMLAttributes: {},
|
||||||
|
renderLabel ({ options, props }) {
|
||||||
|
// eslint-disable-next-line
|
||||||
|
return `${options.suggestion.char}${props.label ?? props.id}`
|
||||||
|
},
|
||||||
|
suggestion: {
|
||||||
|
char: '@',
|
||||||
|
allowSpaces: true,
|
||||||
|
command: ({ editor, range, props }) => {
|
||||||
|
// increase range.to by one when the next node is of type "text"
|
||||||
|
// and starts with a space character
|
||||||
|
const nodeAfter = editor.view.state.selection.$to.nodeAfter
|
||||||
|
const overrideSpace = nodeAfter?.text?.startsWith(' ')
|
||||||
|
|
||||||
|
if (overrideSpace !== undefined && overrideSpace) {
|
||||||
|
// eslint-disable-next-line
|
||||||
|
range.to += 1
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props !== null) {
|
||||||
|
editor
|
||||||
|
.chain()
|
||||||
|
.focus()
|
||||||
|
.insertContentAt(range, [
|
||||||
|
{
|
||||||
|
type: this.name,
|
||||||
|
attrs: props
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: ' '
|
||||||
|
}
|
||||||
|
])
|
||||||
|
.run()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
allow: ({ editor, range }) => {
|
||||||
|
if (range.from > editor.state.doc.content.size) return false
|
||||||
|
const $from = editor.state.doc.resolve(range.from)
|
||||||
|
const type = editor.schema.nodes[this.name]
|
||||||
|
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
|
||||||
|
return !!$from.parent.type.contentMatch.matchType(type)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
addNodeView () {
|
||||||
|
return ({ node, HTMLAttributes }) => {
|
||||||
|
const span = document.createElement('span')
|
||||||
|
span.setAttribute('data-type', this.name)
|
||||||
|
span.className = 'antimention'
|
||||||
|
const attributes = mergeAttributes(
|
||||||
|
{
|
||||||
|
'data-type': this.name,
|
||||||
|
class: 'antiMention'
|
||||||
|
},
|
||||||
|
this.options.HTMLAttributes,
|
||||||
|
HTMLAttributes
|
||||||
|
)
|
||||||
|
|
||||||
|
span.addEventListener('click', (event) => {
|
||||||
|
if (event.button !== 0) return
|
||||||
|
|
||||||
|
const link = (event.target as HTMLElement)?.closest('span')
|
||||||
|
if (link != null) {
|
||||||
|
const _class = link.getAttribute('data-objectclass')
|
||||||
|
const _id = link.getAttribute('data-id')
|
||||||
|
if (_id != null && _class != null) {
|
||||||
|
this.options.showDoc?.(event, _id, _class)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
Object.entries(attributes).forEach(([key, value]) => {
|
||||||
|
span.setAttribute(key, value)
|
||||||
|
})
|
||||||
|
|
||||||
|
const query = createQuery(true)
|
||||||
|
const options = this.options
|
||||||
|
|
||||||
|
const renderLabel = (props: ReferenceNodeProps): void => {
|
||||||
|
span.setAttribute('data-label', props.label)
|
||||||
|
span.innerText = options.renderLabel({ options, props: props ?? node.attrs })
|
||||||
|
}
|
||||||
|
|
||||||
|
const id = node.attrs.id
|
||||||
|
const objectclass: Ref<Class<Doc>> = node.attrs.objectclass
|
||||||
|
|
||||||
|
renderLabel({ id, objectclass, label: node.attrs.label })
|
||||||
|
|
||||||
|
if (id !== undefined && objectclass !== undefined) {
|
||||||
|
query.query(objectclass, { _id: id }, async (result) => {
|
||||||
|
const obj = result[0]
|
||||||
|
if (obj === undefined) return
|
||||||
|
|
||||||
|
const label = await getReferenceLabel(objectclass, id, obj)
|
||||||
|
if (label === '') return
|
||||||
|
|
||||||
|
renderLabel({ id, objectclass, label })
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
dom: span,
|
||||||
|
update (node, decorations) {
|
||||||
|
renderLabel({ id, objectclass, label: node.attrs.label })
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
destroy () {
|
||||||
|
query.unsubscribe()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
addKeyboardShortcuts () {
|
||||||
|
return {
|
||||||
|
Backspace: () =>
|
||||||
|
this.editor.commands.command(({ tr, state }) => {
|
||||||
|
let isMention = false
|
||||||
|
const { selection } = state
|
||||||
|
const { empty, anchor } = selection
|
||||||
|
|
||||||
|
if (!empty) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
state.doc.nodesBetween(anchor - 1, anchor, (node, pos) => {
|
||||||
|
if (node.type.name === this.name) {
|
||||||
|
isMention = true
|
||||||
|
|
||||||
|
// eslint-disable-next-line
|
||||||
|
tr.insertText(this.options.suggestion.char || '', pos, pos + node.nodeSize)
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return isMention
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
addProseMirrorPlugins () {
|
||||||
|
return [
|
||||||
|
Suggestion({
|
||||||
|
editor: this.editor,
|
||||||
|
...this.options.suggestion
|
||||||
|
}),
|
||||||
|
// ReferenceClickHandler(this.options),
|
||||||
|
ResolveReferenceUrlsPlugin(this.editor)
|
||||||
|
]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
export const referenceConfig: Partial<ReferenceExtensionOptions> = {
|
||||||
|
HTMLAttributes: {
|
||||||
|
class: 'reference'
|
||||||
|
},
|
||||||
|
suggestion: {
|
||||||
|
items: async () => {
|
||||||
|
return []
|
||||||
|
},
|
||||||
|
render: () => {
|
||||||
|
let component: any
|
||||||
|
|
||||||
|
return {
|
||||||
|
onStart: (props: SuggestionProps) => {
|
||||||
|
component = new SvelteRenderer(MentionList, {
|
||||||
|
element: document.body,
|
||||||
|
props: {
|
||||||
|
...props,
|
||||||
|
close: () => {
|
||||||
|
component?.destroy()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
onUpdate (props: SuggestionProps) {
|
||||||
|
component?.updateProps(props)
|
||||||
|
},
|
||||||
|
onKeyDown (props: SuggestionKeyDownProps) {
|
||||||
|
if (props.event.key === 'Escape') {
|
||||||
|
props.event.stopPropagation()
|
||||||
|
}
|
||||||
|
return component?.onKeyDown(props)
|
||||||
|
},
|
||||||
|
onExit () {
|
||||||
|
component?.destroy()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ResolveReferenceUrlsPluginState {
|
||||||
|
references: Map<string, { id: Ref<Doc>, objectclass: Ref<Class<Doc>>, label: string }>
|
||||||
|
queue: Set<string>
|
||||||
|
}
|
||||||
|
|
||||||
|
const resolveReferencePluginKey = new PluginKey<ResolveReferenceUrlsPluginState>('linkToReference')
|
||||||
|
|
||||||
|
export function ResolveReferenceUrlsPlugin (editor: Editor): Plugin<ResolveReferenceUrlsPluginState> {
|
||||||
|
return new Plugin<ResolveReferenceUrlsPluginState>({
|
||||||
|
key: resolveReferencePluginKey,
|
||||||
|
|
||||||
|
appendTransaction: (transactions, oldState, newState) => {
|
||||||
|
if (transactions[0]?.getMeta('linkToReference') === undefined) return undefined
|
||||||
|
if (editor.schema.nodes.reference === undefined) return
|
||||||
|
|
||||||
|
const references = resolveReferencePluginKey.getState(newState)?.references ?? new Map()
|
||||||
|
|
||||||
|
const { tr } = newState
|
||||||
|
tr.doc.descendants((node, pos) => {
|
||||||
|
if (!node.isText || !node.marks.some((m) => m.type.name === 'link')) return
|
||||||
|
|
||||||
|
const url = node.textContent
|
||||||
|
const mapping = references.get(url)
|
||||||
|
if (mapping === undefined) return
|
||||||
|
|
||||||
|
const replacementNode = editor.schema.nodes.reference.create(mapping)
|
||||||
|
const mpos = tr.mapping.map(pos)
|
||||||
|
tr.replaceWith(mpos, mpos + node.nodeSize, replacementNode)
|
||||||
|
})
|
||||||
|
|
||||||
|
if (tr.steps.length > 0) return tr
|
||||||
|
},
|
||||||
|
|
||||||
|
state: {
|
||||||
|
init () {
|
||||||
|
return {
|
||||||
|
references: new Map(),
|
||||||
|
queue: new Set()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
apply (tr, prev, oldState, newState) {
|
||||||
|
if (tr.getMeta('linkToReference') !== undefined) {
|
||||||
|
const references = tr.getMeta('linkToReference').references as ResolveReferenceUrlsPluginState['references']
|
||||||
|
const urls = new Set(references.keys())
|
||||||
|
return {
|
||||||
|
queue: new Set(Array.from(prev.queue).filter((url) => !urls.has(url))),
|
||||||
|
references: new Map([...prev.references, ...references])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!tr.docChanged || oldState.doc.eq(newState.doc)) return prev
|
||||||
|
|
||||||
|
const urls: string[] = []
|
||||||
|
tr.doc.descendants((node) => {
|
||||||
|
if (!node.isText || !node.marks.some((m) => m.type.name === 'link')) return
|
||||||
|
const url = node.textContent
|
||||||
|
|
||||||
|
const hasNoMapping = prev.references.has(url) && prev.references.get(url) === undefined
|
||||||
|
if (prev.queue.has(url) || hasNoMapping) return
|
||||||
|
|
||||||
|
urls.push(url)
|
||||||
|
})
|
||||||
|
|
||||||
|
const promises = urls.map(async (url) => {
|
||||||
|
try {
|
||||||
|
return [url, await getReferenceFromUrl(url)] as const
|
||||||
|
} catch {
|
||||||
|
return [url, undefined] as const
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (promises.length > 0) {
|
||||||
|
void Promise.all(promises).then((references) => {
|
||||||
|
editor.view.dispatch(editor.state.tr.setMeta('linkToReference', { references: new Map(references) }))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
references: prev.references,
|
||||||
|
queue: new Set([...prev.queue, ...urls])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getReferenceLabel<T extends Doc> (
|
||||||
|
objectclass: Ref<Class<T>>,
|
||||||
|
id: Ref<T>,
|
||||||
|
doc?: T
|
||||||
|
): Promise<string> {
|
||||||
|
const client = getClient()
|
||||||
|
const hierarchy = client.getHierarchy()
|
||||||
|
|
||||||
|
const labelProvider = hierarchy.classHierarchyMixin(objectclass as Ref<Class<Doc>>, view.mixin.ObjectIdentifier)
|
||||||
|
const labelProviderFn = labelProvider !== undefined ? await getResource(labelProvider.provider) : undefined
|
||||||
|
|
||||||
|
const titleMixin = hierarchy.classHierarchyMixin(objectclass as Ref<Class<Doc>>, view.mixin.ObjectTitle)
|
||||||
|
const titleProviderFn = titleMixin !== undefined ? await getResource(titleMixin.titleProvider) : undefined
|
||||||
|
|
||||||
|
const identifier = (await labelProviderFn?.(client, id, doc)) ?? ''
|
||||||
|
const title = (await titleProviderFn?.(client, id, doc)) ?? ''
|
||||||
|
|
||||||
|
const label = identifier !== '' && title !== '' && identifier !== title ? `${identifier} ${title}` : title ?? ''
|
||||||
|
|
||||||
|
return label
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getReferenceFromUrl (text: string): Promise<ReferenceNodeProps | undefined> {
|
||||||
|
const client = getClient()
|
||||||
|
const hierarchy = client.getHierarchy()
|
||||||
|
|
||||||
|
const url = new URL(text)
|
||||||
|
|
||||||
|
const frontUrl = getMetadata(presentation.metadata.FrontUrl) ?? window.location.origin
|
||||||
|
if (url.origin !== frontUrl) return
|
||||||
|
|
||||||
|
const location = parseLocation(url)
|
||||||
|
|
||||||
|
const appAlias = (location.path[2] ?? '').trim()
|
||||||
|
if (!(appAlias.length > 0)) return
|
||||||
|
|
||||||
|
const excludedApps = getMetadata(workbench.metadata.ExcludedApplications) ?? []
|
||||||
|
const apps: Application[] = client
|
||||||
|
.getModel()
|
||||||
|
.findAllSync<Application>(workbench.class.Application, { hidden: false, _id: { $nin: excludedApps } })
|
||||||
|
|
||||||
|
const app = apps.find((p) => p.alias === appAlias)
|
||||||
|
|
||||||
|
if (app?.locationResolver === undefined) return
|
||||||
|
const locationResolverFn = await getResource(app.locationResolver)
|
||||||
|
const resolvedLocation = await locationResolverFn(location)
|
||||||
|
|
||||||
|
const locationParts = decodeURIComponent(resolvedLocation?.loc?.fragment ?? '').split('|')
|
||||||
|
const id = locationParts[1] as Ref<Doc>
|
||||||
|
const objectclass = locationParts[2] as Ref<Class<Doc>>
|
||||||
|
if (id === undefined || objectclass === undefined) return
|
||||||
|
|
||||||
|
const linkProviders = client.getModel().findAllSync(view.mixin.LinkIdProvider, {})
|
||||||
|
const linkProvider = linkProviders.find(({ _id }) => hierarchy.isDerived(objectclass, _id))
|
||||||
|
const _id: Ref<Doc> | undefined =
|
||||||
|
linkProvider !== undefined ? (await (await getResource(linkProvider.decode))(id)) ?? id : id
|
||||||
|
|
||||||
|
const label = await getReferenceLabel(objectclass, id)
|
||||||
|
if (label === '') return
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: _id,
|
||||||
|
objectclass,
|
||||||
|
label
|
||||||
|
}
|
||||||
|
}
|
@ -13,16 +13,15 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
//
|
//
|
||||||
|
|
||||||
import view from '@hcengineering/view'
|
|
||||||
import { type Editor, type Range } from '@tiptap/core'
|
|
||||||
import textEditor from '@hcengineering/text-editor'
|
import textEditor from '@hcengineering/text-editor'
|
||||||
import { IconScribble } from '@hcengineering/ui'
|
import { IconScribble } from '@hcengineering/ui'
|
||||||
|
import view from '@hcengineering/view'
|
||||||
|
import { type Editor, type Range } from '@tiptap/core'
|
||||||
|
|
||||||
import { type CompletionOptions } from '../Completion'
|
import { type InlineCommandsOptions } from './extension/inlineCommands'
|
||||||
import MentionList from './MentionList.svelte'
|
|
||||||
import { SvelteRenderer } from './node-view'
|
|
||||||
import type { SuggestionKeyDownProps, SuggestionProps } from './extension/suggestion'
|
import type { SuggestionKeyDownProps, SuggestionProps } from './extension/suggestion'
|
||||||
import InlineCommandsList from './InlineCommandsList.svelte'
|
import InlineCommandsList from './InlineCommandsList.svelte'
|
||||||
|
import { SvelteRenderer } from './node-view'
|
||||||
|
|
||||||
export const mInsertTable = [
|
export const mInsertTable = [
|
||||||
{
|
{
|
||||||
@ -87,49 +86,6 @@ export const mInsertTable = [
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
/**
|
|
||||||
* @public
|
|
||||||
*/
|
|
||||||
export const completionConfig: Partial<CompletionOptions> = {
|
|
||||||
HTMLAttributes: {
|
|
||||||
class: 'reference'
|
|
||||||
},
|
|
||||||
suggestion: {
|
|
||||||
items: async () => {
|
|
||||||
return []
|
|
||||||
},
|
|
||||||
render: () => {
|
|
||||||
let component: any
|
|
||||||
|
|
||||||
return {
|
|
||||||
onStart: (props: SuggestionProps) => {
|
|
||||||
component = new SvelteRenderer(MentionList, {
|
|
||||||
element: document.body,
|
|
||||||
props: {
|
|
||||||
...props,
|
|
||||||
close: () => {
|
|
||||||
component?.destroy()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
},
|
|
||||||
onUpdate (props: SuggestionProps) {
|
|
||||||
component?.updateProps(props)
|
|
||||||
},
|
|
||||||
onKeyDown (props: SuggestionKeyDownProps) {
|
|
||||||
if (props.event.key === 'Escape') {
|
|
||||||
props.event.stopPropagation()
|
|
||||||
}
|
|
||||||
return component?.onKeyDown(props)
|
|
||||||
},
|
|
||||||
onExit () {
|
|
||||||
component?.destroy()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const inlineCommandsIds = [
|
const inlineCommandsIds = [
|
||||||
'image',
|
'image',
|
||||||
'table',
|
'table',
|
||||||
@ -147,7 +103,7 @@ export type InlineCommandId = (typeof inlineCommandsIds)[number]
|
|||||||
export function inlineCommandsConfig (
|
export function inlineCommandsConfig (
|
||||||
handleSelect: (id: string, pos: number, targetItem?: MouseEvent | HTMLElement) => Promise<void>,
|
handleSelect: (id: string, pos: number, targetItem?: MouseEvent | HTMLElement) => Promise<void>,
|
||||||
excludedCommands: InlineCommandId[] = []
|
excludedCommands: InlineCommandId[] = []
|
||||||
): Partial<CompletionOptions> {
|
): Partial<InlineCommandsOptions> {
|
||||||
return {
|
return {
|
||||||
suggestion: {
|
suggestion: {
|
||||||
items: () => {
|
items: () => {
|
||||||
|
@ -32,7 +32,14 @@ import { includesAny } from '@hcengineering/contact'
|
|||||||
import { type Resources, type Status, translate } from '@hcengineering/platform'
|
import { type Resources, type Status, translate } from '@hcengineering/platform'
|
||||||
import { getClient, MessageBox, type ObjectSearchResult } from '@hcengineering/presentation'
|
import { getClient, MessageBox, type ObjectSearchResult } from '@hcengineering/presentation'
|
||||||
import { type Component, type Issue, type Milestone, type Project } from '@hcengineering/tracker'
|
import { type Component, type Issue, type Milestone, type Project } from '@hcengineering/tracker'
|
||||||
import { closePanel, getCurrentLocation, navigate, showPopup, themeStore } from '@hcengineering/ui'
|
import {
|
||||||
|
closePanel,
|
||||||
|
getCurrentLocation,
|
||||||
|
getCurrentResolvedLocation,
|
||||||
|
navigate,
|
||||||
|
showPopup,
|
||||||
|
themeStore
|
||||||
|
} from '@hcengineering/ui'
|
||||||
import ComponentEditor from './components/components/ComponentEditor.svelte'
|
import ComponentEditor from './components/components/ComponentEditor.svelte'
|
||||||
import ComponentFilterValuePresenter from './components/components/ComponentFilterValuePresenter.svelte'
|
import ComponentFilterValuePresenter from './components/components/ComponentFilterValuePresenter.svelte'
|
||||||
import ComponentPresenter from './components/components/ComponentPresenter.svelte'
|
import ComponentPresenter from './components/components/ComponentPresenter.svelte'
|
||||||
@ -118,7 +125,13 @@ import ComponentSelector from './components/components/ComponentSelector.svelte'
|
|||||||
import IssueTemplatePresenter from './components/templates/IssueTemplatePresenter.svelte'
|
import IssueTemplatePresenter from './components/templates/IssueTemplatePresenter.svelte'
|
||||||
import IssueTemplates from './components/templates/IssueTemplates.svelte'
|
import IssueTemplates from './components/templates/IssueTemplates.svelte'
|
||||||
|
|
||||||
import { AggregationManager, deleteObject, deleteObjects } from '@hcengineering/view-resources'
|
import {
|
||||||
|
AggregationManager,
|
||||||
|
buildFilterKey,
|
||||||
|
deleteObject,
|
||||||
|
deleteObjects,
|
||||||
|
setFilters
|
||||||
|
} from '@hcengineering/view-resources'
|
||||||
import MoveAndDeleteMilestonePopup from './components/milestones/MoveAndDeleteMilestonePopup.svelte'
|
import MoveAndDeleteMilestonePopup from './components/milestones/MoveAndDeleteMilestonePopup.svelte'
|
||||||
import EditIssueTemplate from './components/templates/EditIssueTemplate.svelte'
|
import EditIssueTemplate from './components/templates/EditIssueTemplate.svelte'
|
||||||
import TemplateEstimationEditor from './components/templates/EstimationEditor.svelte'
|
import TemplateEstimationEditor from './components/templates/EstimationEditor.svelte'
|
||||||
@ -160,6 +173,8 @@ import { settingId } from '@hcengineering/setting'
|
|||||||
import { getAllStates } from '@hcengineering/task-resources'
|
import { getAllStates } from '@hcengineering/task-resources'
|
||||||
import EstimationValueEditor from './components/issues/timereport/EstimationValueEditor.svelte'
|
import EstimationValueEditor from './components/issues/timereport/EstimationValueEditor.svelte'
|
||||||
import TimePresenter from './components/issues/timereport/TimePresenter.svelte'
|
import TimePresenter from './components/issues/timereport/TimePresenter.svelte'
|
||||||
|
import type { TaskType } from '@hcengineering/task'
|
||||||
|
import view, { type Filter } from '@hcengineering/view'
|
||||||
|
|
||||||
export { default as AssigneeEditor } from './components/issues/AssigneeEditor.svelte'
|
export { default as AssigneeEditor } from './components/issues/AssigneeEditor.svelte'
|
||||||
export { default as SubIssueList } from './components/issues/edit/SubIssueList.svelte'
|
export { default as SubIssueList } from './components/issues/edit/SubIssueList.svelte'
|
||||||
@ -346,6 +361,33 @@ function setStore (manager: DocManager<Component>): void {
|
|||||||
componentStore.set(manager)
|
componentStore.set(manager)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function openIssuesOfTaskType (taskType: TaskType): Promise<void> {
|
||||||
|
function setFilterTag (taskType: TaskType): void {
|
||||||
|
const client = getClient()
|
||||||
|
const hierarchy = client.getHierarchy()
|
||||||
|
const attribute = hierarchy.getAttribute(tracker.class.Issue, 'kind')
|
||||||
|
const key = buildFilterKey(hierarchy, tracker.class.Issue, 'kind', attribute)
|
||||||
|
const filter = {
|
||||||
|
key,
|
||||||
|
value: [taskType._id],
|
||||||
|
props: { level: 0 },
|
||||||
|
modes: [view.filter.FilterObjectIn, view.filter.FilterObjectNin],
|
||||||
|
mode: view.filter.FilterObjectIn,
|
||||||
|
index: 1
|
||||||
|
} as unknown as Filter
|
||||||
|
setFilters([filter])
|
||||||
|
}
|
||||||
|
if (taskType === undefined) return
|
||||||
|
const loc = getCurrentResolvedLocation()
|
||||||
|
loc.path[2] = 'tracker'
|
||||||
|
loc.path[3] = 'all-issues'
|
||||||
|
loc.path.length = 4
|
||||||
|
navigate(loc)
|
||||||
|
setTimeout(() => {
|
||||||
|
setFilterTag(taskType)
|
||||||
|
}, 200)
|
||||||
|
}
|
||||||
|
|
||||||
export default async (): Promise<Resources> => ({
|
export default async (): Promise<Resources> => ({
|
||||||
activity: {
|
activity: {
|
||||||
PriorityIcon,
|
PriorityIcon,
|
||||||
@ -467,7 +509,8 @@ export default async (): Promise<Resources> => ({
|
|||||||
IsProjectJoined: async (project: Project) => includesAny(project.members, getCurrentAccount().socialIds),
|
IsProjectJoined: async (project: Project) => includesAny(project.members, getCurrentAccount().socialIds),
|
||||||
GetIssueStatusCategories: getIssueStatusCategories,
|
GetIssueStatusCategories: getIssueStatusCategories,
|
||||||
SetComponentStore: setStore,
|
SetComponentStore: setStore,
|
||||||
ComponentFilterFunction: filterComponents
|
ComponentFilterFunction: filterComponents,
|
||||||
|
OpenIssuesOfTaskType: openIssuesOfTaskType
|
||||||
},
|
},
|
||||||
actionImpl: {
|
actionImpl: {
|
||||||
Move: move,
|
Move: move,
|
||||||
|
@ -16,7 +16,7 @@ import { type StatusCategory, type Client, type Doc, type Ref, type Space } from
|
|||||||
import type { Asset, IntlString, Metadata, Resource } from '@hcengineering/platform'
|
import type { Asset, IntlString, Metadata, Resource } from '@hcengineering/platform'
|
||||||
import { mergeIds } from '@hcengineering/platform'
|
import { mergeIds } from '@hcengineering/platform'
|
||||||
import type { ObjectSearchCategory, ObjectSearchFactory } from '@hcengineering/presentation'
|
import type { ObjectSearchCategory, ObjectSearchFactory } from '@hcengineering/presentation'
|
||||||
import { type ProjectType } from '@hcengineering/task'
|
import { type ProjectType, type TaskType } from '@hcengineering/task'
|
||||||
import tracker, { trackerId, type IssueDraft, type Issue } from '@hcengineering/tracker'
|
import tracker, { trackerId, type IssueDraft, type Issue } from '@hcengineering/tracker'
|
||||||
import { type AnyComponent, type Location } from '@hcengineering/ui'
|
import { type AnyComponent, type Location } from '@hcengineering/ui'
|
||||||
import {
|
import {
|
||||||
@ -398,7 +398,8 @@ export default mergeIds(trackerId, tracker, {
|
|||||||
IsProjectJoined: '' as Resource<(space: Space) => Promise<boolean>>,
|
IsProjectJoined: '' as Resource<(space: Space) => Promise<boolean>>,
|
||||||
IssueChatTitleProvider: '' as Resource<(object: Doc) => string>,
|
IssueChatTitleProvider: '' as Resource<(object: Doc) => string>,
|
||||||
GetIssueStatusCategories: '' as Resource<(project: ProjectType) => Array<Ref<StatusCategory>>>,
|
GetIssueStatusCategories: '' as Resource<(project: ProjectType) => Array<Ref<StatusCategory>>>,
|
||||||
GetIssueIdByIdentifier: '' as Resource<(id: string) => Promise<Ref<Issue> | undefined>>
|
GetIssueIdByIdentifier: '' as Resource<(id: string) => Promise<Ref<Issue> | undefined>>,
|
||||||
|
OpenIssuesOfTaskType: '' as Resource<(taskType: TaskType) => Promise<void>>
|
||||||
},
|
},
|
||||||
aggregation: {
|
aggregation: {
|
||||||
CreateComponentAggregationManager: '' as CreateAggregationManagerFunc,
|
CreateComponentAggregationManager: '' as CreateAggregationManagerFunc,
|
||||||
|
@ -19,7 +19,7 @@
|
|||||||
import { AnyComponent, LabelAndProps, themeStore, tooltip } from '@hcengineering/ui'
|
import { AnyComponent, LabelAndProps, themeStore, tooltip } from '@hcengineering/ui'
|
||||||
import view from '@hcengineering/view'
|
import view from '@hcengineering/view'
|
||||||
|
|
||||||
import { getDocIdentifier } from '../utils'
|
import { getReferenceLabel } from '@hcengineering/text-editor-resources/src/components/extension/reference'
|
||||||
import DocNavLink from './DocNavLink.svelte'
|
import DocNavLink from './DocNavLink.svelte'
|
||||||
|
|
||||||
export let _id: Ref<Doc> | undefined = undefined
|
export let _id: Ref<Doc> | undefined = undefined
|
||||||
@ -91,7 +91,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function updateDocTitle (doc: Doc | undefined): Promise<void> {
|
async function updateDocTitle (doc: Doc | undefined): Promise<void> {
|
||||||
docTitle = doc ? await getDocIdentifier(client, doc._id, doc._class, doc) : undefined
|
docTitle = doc ? await getReferenceLabel(doc._class, doc._id, doc) : undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
async function updateDocTooltip (doc?: Doc): Promise<void> {
|
async function updateDocTooltip (doc?: Doc): Promise<void> {
|
||||||
|
@ -83,7 +83,8 @@ test.describe('contact tests', () => {
|
|||||||
await contractPage.fillEmailInput(mail)
|
await contractPage.fillEmailInput(mail)
|
||||||
await contractPage.clickCreateButton()
|
await contractPage.clickCreateButton()
|
||||||
await contractPage.waitForFormAntiCardDetached()
|
await contractPage.waitForFormAntiCardDetached()
|
||||||
await contractPage.kickEmployee(first, last)
|
// employee already inactive
|
||||||
|
// await contractPage.kickEmployee(first, last)
|
||||||
// In non refactored code, the last assert just checks if the employee does exist, not if it has inactive status
|
// In non refactored code, the last assert just checks if the employee does exist, not if it has inactive status
|
||||||
await contractPage.expectKickEmployeeShowsInactiveStatus(first, last)
|
await contractPage.expectKickEmployeeShowsInactiveStatus(first, last)
|
||||||
})
|
})
|
||||||
|
@ -194,7 +194,7 @@ test.describe('Documents tests', () => {
|
|||||||
await documentContentPage.checkLinkInTheText(contentLink, 'http://test/link/123456')
|
await documentContentPage.checkLinkInTheText(contentLink, 'http://test/link/123456')
|
||||||
})
|
})
|
||||||
|
|
||||||
test('Check Link to Reference conversion', async () => {
|
test('Check Link to Reference conversion and sync', async () => {
|
||||||
const sourceDocument: NewDocument = {
|
const sourceDocument: NewDocument = {
|
||||||
title: `Reference Document Title-${generateId()}`,
|
title: `Reference Document Title-${generateId()}`,
|
||||||
space: 'Default'
|
space: 'Default'
|
||||||
@ -204,6 +204,7 @@ test.describe('Documents tests', () => {
|
|||||||
space: 'Default'
|
space: 'Default'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await test.step('Create two documents and reference one from the other', async () => {
|
||||||
await leftSideMenuPage.clickDocuments()
|
await leftSideMenuPage.clickDocuments()
|
||||||
|
|
||||||
await documentsPage.clickOnButtonCreateDocument()
|
await documentsPage.clickOnButtonCreateDocument()
|
||||||
@ -221,6 +222,25 @@ test.describe('Documents tests', () => {
|
|||||||
await documentContentPage.addContentToTheNewLine(targetDocumentUrl)
|
await documentContentPage.addContentToTheNewLine(targetDocumentUrl)
|
||||||
await documentContentPage.addRandomLines(5)
|
await documentContentPage.addRandomLines(5)
|
||||||
await documentContentPage.checkReferenceInTheText(targetDocument.title)
|
await documentContentPage.checkReferenceInTheText(targetDocument.title)
|
||||||
|
|
||||||
|
await leftSideMenuPage.clickDocuments()
|
||||||
|
})
|
||||||
|
|
||||||
|
await test.step('Rename the document and check sync', async () => {
|
||||||
|
await leftSideMenuPage.clickDocuments()
|
||||||
|
|
||||||
|
const targetDocumentSecondTitle = `Updated Reference Document Title-${generateId()}`
|
||||||
|
|
||||||
|
await documentsPage.openDocument(targetDocument.title)
|
||||||
|
await documentContentPage.checkDocumentTitle(targetDocument.title)
|
||||||
|
await documentContentPage.updateDocumentTitle(targetDocumentSecondTitle)
|
||||||
|
await documentContentPage.checkDocumentTitle(targetDocumentSecondTitle)
|
||||||
|
|
||||||
|
await documentsPage.openDocument(sourceDocument.title)
|
||||||
|
await documentContentPage.checkDocumentTitle(sourceDocument.title)
|
||||||
|
|
||||||
|
await documentContentPage.checkReferenceInTheText(targetDocumentSecondTitle)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
test('Locked document and checking URL', async ({ page, context }) => {
|
test('Locked document and checking URL', async ({ page, context }) => {
|
||||||
|
Loading…
Reference in New Issue
Block a user