UBER-134: Back references (#3233)

Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
Andrey Sobolev 2023-05-23 17:42:39 +07:00 committed by GitHub
parent 14fbe03d9f
commit d06f3316b3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
36 changed files with 835 additions and 671 deletions

View File

@ -141,6 +141,7 @@
</svelte:fragment>
<svelte:fragment slot="utils">
<slot name="pre-utils" />
<Component is={calendar.component.DocReminder} props={{ value: object, title, focusIndex: 9000 }} />
{#if isUtils && $$slots.utils}
<div class="buttons-divider" />

View File

@ -4,6 +4,7 @@
"Cancel": "Cancel",
"Ok": "Ok",
"Save": "Save",
"Saved": "Saved...",
"Download": "Download",
"Delete": "Delete",
"Close": "Close",

View File

@ -4,6 +4,7 @@
"Cancel": "Отменить",
"Ok": "Ок",
"Save": "Сохранить",
"Saved": "Сохранено...",
"Download": "Скачать",
"Delete": "Удалить",
"Close": "Закрыть",

View File

@ -33,6 +33,7 @@ export default plugin(presentationId, {
Cancel: '' as IntlString,
Ok: '' as IntlString,
Save: '' as IntlString,
Saved: '' as IntlString,
Download: '' as IntlString,
Delete: '' as IntlString,
Close: '' as IntlString,

View File

@ -1,13 +1,38 @@
import { Node, mergeAttributes } from '@tiptap/core'
import Suggestion, { SuggestionOptions } from '@tiptap/suggestion'
import { Plugin, PluginKey } from '@tiptap/pm/state'
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 const CompletionPluginKey = new PluginKey('completion')
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',
@ -63,10 +88,16 @@ export const Completion = Node.create<CompletionOptions>({
inline: true,
selectable: false,
selectable: true,
atom: true,
draggable: true,
onFocus () {
console.log('focus')
},
addAttributes () {
return {
id: {
@ -126,7 +157,15 @@ export const Completion = Node.create<CompletionOptions>({
renderHTML ({ node, HTMLAttributes }) {
return [
'span',
mergeAttributes({ 'data-type': this.name }, this.options.HTMLAttributes, HTMLAttributes),
mergeAttributes(
{
'data-type': this.name,
class: 'antiButton secondary cursor-pointer',
style: 'width: fit-content;display: inline-flex;'
},
this.options.HTMLAttributes,
HTMLAttributes
),
this.options.renderLabel({
options: this.options,
node
@ -174,7 +213,8 @@ export const Completion = Node.create<CompletionOptions>({
Suggestion({
editor: this.editor,
...this.options.suggestion
})
}),
clickHandler(this.options)
]
}
})

View File

@ -26,6 +26,7 @@
export let icon: Asset | AnySvelteComponent = IconDescription
export let content: string = ''
export let maxHeight: string = '40vh'
export let enableBackReferences = false
const dispatch = createEventDispatcher()
@ -44,5 +45,14 @@
<Label {label} />
</span>
</div>
<StyledTextBox {content} alwaysEdit focusable mode={2} hideExtraButtons {maxHeight} on:value={checkValue} />
<StyledTextBox
{content}
alwaysEdit
focusable
mode={2}
hideExtraButtons
{maxHeight}
on:value={checkValue}
{enableBackReferences}
/>
</div>

View File

@ -27,32 +27,30 @@
showPopup,
tooltip
} from '@hcengineering/ui'
import { AnyExtension } from '@tiptap/core'
import { createEventDispatcher } from 'svelte'
import { Completion } from '../Completion'
import textEditorPlugin from '../plugin'
import { FORMAT_MODES, FormatMode, RefAction, RefInputActionItem, TextEditorHandler } from '../types'
import LinkPopup from './LinkPopup.svelte'
import MentionList from './MentionList.svelte'
import { SvelteRenderer } from './SvelteRenderer'
import TextEditor from './TextEditor.svelte'
import { completionConfig } from './extensions'
import Attach from './icons/Attach.svelte'
import Bold from './icons/Bold.svelte'
import RIBold from './icons/RIBold.svelte'
import Code from './icons/Code.svelte'
import RICode from './icons/RICode.svelte'
import CodeBlock from './icons/CodeBlock.svelte'
import RILink from './icons/RILink.svelte'
import RIMention from './icons/RIMention.svelte'
import Italic from './icons/Italic.svelte'
import RIItalic from './icons/RIItalic.svelte'
import Link from './icons/Link.svelte'
import ListBullet from './icons/ListBullet.svelte'
import ListNumber from './icons/ListNumber.svelte'
import Quote from './icons/Quote.svelte'
import RIBold from './icons/RIBold.svelte'
import RICode from './icons/RICode.svelte'
import RIItalic from './icons/RIItalic.svelte'
import RILink from './icons/RILink.svelte'
import RIMention from './icons/RIMention.svelte'
import RIStrikethrough from './icons/RIStrikethrough.svelte'
import Send from './icons/Send.svelte'
import Strikethrough from './icons/Strikethrough.svelte'
import RIStrikethrough from './icons/RIStrikethrough.svelte'
import TextStyle from './icons/TextStyle.svelte'
const dispatch = createEventDispatcher()
@ -185,42 +183,6 @@
textEditor.submit()
}
const editorExtensions: AnyExtension[] = [
Completion.configure({
HTMLAttributes: {
class: 'reference'
},
suggestion: {
items: async (query: { query: string }) => {
return []
},
render: () => {
let component: any
return {
onStart: (props: any) => {
component = new SvelteRenderer(MentionList, {
...props,
close: () => {
component.destroy()
}
})
},
onUpdate (props: any) {
component.updateProps(props)
},
onKeyDown (props: any) {
return component.onKeyDown(props)
},
onExit () {
component.destroy()
}
}
}
}
})
]
const editorHandler: TextEditorHandler = {
insertText: (text) => {
textEditor.insertText(text)
@ -277,6 +239,12 @@
focusManager?.setFocus(idx)
}
}
const completionPlugin = Completion.configure({
...completionConfig,
showDoc (event: MouseEvent, _id: string, _class: string) {
dispatch('open-document', { event, _id, _class })
}
})
</script>
<div class="ref-container">
@ -380,7 +348,7 @@
focused = true
updateFocus()
}}
extensions={editorExtensions}
extensions={[completionPlugin]}
on:selection-update={updateFormattingState}
on:update
placeholder={placeholder ?? textEditorPlugin.string.EditorPlaceholder}

View File

@ -13,8 +13,10 @@
resizeObserver
} from '@hcengineering/ui'
import { createEventDispatcher } from 'svelte'
import { Completion } from '../Completion'
import textEditorPlugin from '../plugin'
import StyledTextEditor from './StyledTextEditor.svelte'
import { completionConfig } from './extensions'
export let label: IntlString | undefined = undefined
export let content: string
@ -33,6 +35,7 @@
export let focusable: boolean = false
export let enableFormatting = false
export let autofocus = false
export let enableBackReferences: boolean = false
const Mode = {
View: 1,
@ -119,6 +122,12 @@
focusManager?.setFocus(idx)
}
}
const completionPlugin = Completion.configure({
...completionConfig,
showDoc (event: MouseEvent, _id: string, _class: string) {
dispatch('open-document', { event, _id, _class })
}
})
</script>
<!-- svelte-ignore a11y-click-events-have-key-events -->
@ -149,6 +158,7 @@
{focusable}
{enableFormatting}
{autofocus}
extensions={enableBackReferences ? [completionPlugin] : []}
bind:content={rawValue}
bind:this={textEditor}
on:attach

View File

@ -32,22 +32,23 @@
import { headingLevels, mInsertTable } from './extensions'
import Attach from './icons/Attach.svelte'
import Bold from './icons/Bold.svelte'
import RIBold from './icons/RIBold.svelte'
import Code from './icons/Code.svelte'
import RICode from './icons/RICode.svelte'
import CodeBlock from './icons/CodeBlock.svelte'
import Header from './icons/Header.svelte'
import IconTable from './icons/IconTable.svelte'
import Italic from './icons/Italic.svelte'
import RIItalic from './icons/RIItalic.svelte'
import Link from './icons/Link.svelte'
import RILink from './icons/RILink.svelte'
import ListBullet from './icons/ListBullet.svelte'
import ListNumber from './icons/ListNumber.svelte'
import Quote from './icons/Quote.svelte'
import Strikethrough from './icons/Strikethrough.svelte'
import RIBold from './icons/RIBold.svelte'
import RICode from './icons/RICode.svelte'
import RIItalic from './icons/RIItalic.svelte'
import RILink from './icons/RILink.svelte'
import RIStrikethrough from './icons/RIStrikethrough.svelte'
import Strikethrough from './icons/Strikethrough.svelte'
// import RIMention from './icons/RIMention.svelte'
import { AnyExtension } from '@tiptap/core'
import AddColAfter from './icons/table/AddColAfter.svelte'
import AddColBefore from './icons/table/AddColBefore.svelte'
import AddRowAfter from './icons/table/AddRowAfter.svelte'
@ -75,6 +76,7 @@
export let enableFormatting = false
export let autofocus = false
export let full = false
export let extensions: AnyExtension[] = []
let textEditor: TextEditor
let isEmpty = true
@ -555,6 +557,7 @@
<TextEditor
bind:content
{placeholder}
{extensions}
bind:this={textEditor}
bind:isEmpty
on:value
@ -573,7 +576,7 @@
<TextEditor
bind:content
{placeholder}
bind:this={textEditor}
{extensions}
bind:isEmpty
on:value
on:content={(ev) => {

View File

@ -14,6 +14,9 @@ import TipTapCodeBlock from '@tiptap/extension-code-block'
import Gapcursor from '@tiptap/extension-gapcursor'
import Link from '@tiptap/extension-link'
import { CompletionOptions } from '../Completion'
import MentionList from './MentionList.svelte'
import { SvelteRenderer } from './SvelteRenderer'
export const tableExtensions = [
Table.configure({
@ -126,3 +129,40 @@ export const mInsertTable = [
header: true
}
]
/**
* @public
*/
export const completionConfig: Partial<CompletionOptions> = {
HTMLAttributes: {
class: 'reference'
},
suggestion: {
items: async (query: { query: string }) => {
return []
},
render: () => {
let component: any
return {
onStart: (props: any) => {
component = new SvelteRenderer(MentionList, {
...props,
close: () => {
component.destroy()
}
})
},
onUpdate (props: any) {
component.updateProps(props)
},
onKeyDown (props: any) {
return component.onKeyDown(props)
},
onExit () {
component.destroy()
}
}
}
}
}

View File

@ -0,0 +1,292 @@
.antiButton {
display: flex;
align-items: center;
flex-shrink: 0;
padding: 0 0.625rem;
min-width: 1.375rem;
white-space: nowrap;
color: var(--theme-caption-color);
background-color: transparent;
border: 1px solid transparent;
transition-property: border, background-color, color, box-shadow;
transition-duration: 0.15s;
&.accent {
font-weight: 500;
}
.btn-icon {
color: var(--theme-content-color);
transition: color 0.15s;
pointer-events: none;
}
.btn-right-icon {
margin-left: 0.5rem;
color: var(--theme-halfcontent-color);
transition: color 0.15s;
pointer-events: none;
}
&:not(.only-icon) .btn-icon:not(.spinner) {
margin-right: 0.5rem;
}
&:not(.only-icon) .btn-right-icon {
margin-left: 0.5rem;
}
&.no-border:not(.only-icon) .btn-icon,
&.link-bordered:not(.only-icon) .btn-icon,
&.list:not(.only-icon) .btn-icon,
&.sh-circle:not(.only-icon) .btn-icon {
margin-right: 0.25rem;
}
&.short {
max-width: 7rem;
}
&.sh-no-shape {
border-radius: 0.25rem;
}
&.sh-round {
border-radius: 0.5rem;
}
&.sh-circle {
border-radius: 1rem;
&.link {
padding: 0 0.5rem 0 0.25rem;
}
}
&.sh-rectangle-right {
border-top-left-radius: 0.25rem;
border-bottom-left-radius: 0.25rem;
}
&.sh-rectangle-left {
border-top-right-radius: 0.25rem;
border-bottom-right-radius: 0.25rem;
}
&.sh-filter {
border-radius: 0 0 0.5rem 0.5rem;
}
&.bs-solid {
border-style: solid;
}
&.bs-dashed {
border-style: dashed;
}
&.highlight {
box-shadow: inset 0 0 1px 1px var(--primary-button-enabled);
&:hover {
box-shadow: inset 0 0 1px 1px var(--primary-button-hovered);
}
}
&:hover {
.btn-icon {
color: var(--theme-caption-color);
}
}
&:focus {
&:not(.sh-filter) {
box-shadow: 0 0 0 2px var(--primary-button-focused-border);
}
&.sh-filter {
border-color: var(--primary-button-focused-border);
}
}
&:disabled {
color: var(--theme-dark-color);
cursor: not-allowed;
.btn-icon {
opacity: 0.5;
}
}
&.jf-left {
justify-content: flex-start;
}
&.jf-center {
justify-content: center;
}
&.only-icon {
padding: 0;
}
&.secondary {
background-color: var(--theme-button-enabled);
border-color: var(--theme-button-border);
// &.medium:not(.only-icon) {
// padding: 0 0.75rem;
// }
&:hover {
background-color: var(--theme-button-hovered);
}
&:active {
background-color: var(--theme-button-pressed);
}
&:focus {
background-color: var(--theme-button-focused);
}
&:disabled {
background-color: var(--theme-button-disabled);
}
&.selected {
background-color: var(--theme-button-hovered);
.btn-icon {
color: var(--theme-caption-color);
}
}
}
&.no-border {
font-weight: 400;
color: var(--theme-content-color);
background-color: var(--theme-button-enabled);
box-shadow: var(--button-shadow);
&:hover {
color: var(--theme-caption-color);
background-color: var(--theme-button-hovered);
.btn-icon {
color: var(--theme-caption-color);
}
}
&:disabled {
color: var(--theme-trans-color);
background-color: var(--theme-list-button-color);
cursor: default;
.btn-icon {
color: var(--theme-trans-color);
}
&:hover {
color: var(--theme-trans-color);
.btn-icon {
color: var(--theme-trans-color);
}
}
}
}
&.transparent {
&:hover {
background-color: var(--theme-button-hovered);
}
&.selected {
background-color: var(--highlight-select);
}
&.selected:hover {
background-color: var(--highlight-select-hover);
}
}
&.link {
padding: 0 0.875rem;
&:hover {
color: var(--theme-caption-color);
background-color: var(--theme-bg-color);
border-color: var(--theme-divider-color);
.btn-icon {
color: var(--theme-content-color);
}
}
&:disabled {
color: var(--theme-dark-color);
background-color: transparent;
border-color: transparent;
cursor: auto;
.btn-icon {
color: var(--theme-content-color);
}
}
}
&.link-bordered {
padding: 0 0.5rem;
color: var(--theme-content-color);
background-color: var(--theme-link-button-color);
border-color: var(--theme-button-border);
&:hover {
color: var(--theme-caption-color);
background-color: var(--theme-link-button-hover);
border-color: var(--theme-list-divider-color);
.btn-icon {
color: var(--theme-caption-color);
}
}
&.small {
padding: 0 0.25rem;
}
}
&.list {
padding: 0 0.625em;
min-height: 1.75rem;
color: var(--theme-content-color);
background-color: var(--theme-button-enabled);
border: 1px solid var(--theme-button-border);
border-radius: 1.5rem;
// transition-property: border, color, background-color;
// transition-duration: 0.15s;
.btn-icon {
color: var(--theme-dark-color);
}
&:hover {
color: var(--theme-caption-color);
background-color: var(--theme-button-hovered);
border-color: var(--theme-button-border);
}
&:focus {
box-shadow: none;
}
}
&.primary {
padding: 0 0.75rem;
font-weight: 500;
color: var(--primary-button-color);
background-color: var(--primary-button-enabled);
border-color: var(--primary-button-border);
.btn-icon {
color: var(--white-color);
}
&:hover {
background-color: var(--primary-button-hovered);
}
&:active {
background-color: var(--primary-button-pressed);
}
&:focus {
background-color: var(--primary-button-focused);
}
&:disabled {
background-color: var(--primary-button-disabled);
}
}
&.notSelected {
color: var(--theme-dark-color);
.btn-icon {
color: var(--theme-darker-color);
}
&:hover,
&:hover .btn-icon {
color: var(--theme-content-color);
}
}
&.dangerous {
color: var(--white-color);
background-color: var(--dangerous-bg-color);
border-color: var(--dangerous-bg-color);
&:hover {
background-color: var(--dangerous-bg-hover);
}
&:focus {
box-shadow: var(--dangerous-shadow);
}
}
.resetIconSize {
font-size: 16px;
}
}

View File

@ -22,6 +22,7 @@
@import "./mixins.scss";
@import "./panel.scss";
@import "./prose.scss";
@import "./button.scss";
@font-face {
font-family: 'IBM Plex Sans';

View File

@ -96,7 +96,7 @@
<button
use:tooltip={showTooltip}
bind:this={input}
class="button {kind} {size} jf-{justify} sh-{shape ?? 'no-shape'} bs-{borderStyle}"
class="antiButton {kind} {size} jf-{justify} sh-{shape ?? 'no-shape'} bs-{borderStyle}"
class:only-icon={iconOnly}
class:accent
class:highlight
@ -174,297 +174,4 @@
width: 2.75rem;
}
}
.button {
display: flex;
align-items: center;
flex-shrink: 0;
padding: 0 0.625rem;
min-width: 1.375rem;
white-space: nowrap;
color: var(--theme-caption-color);
background-color: transparent;
border: 1px solid transparent;
transition-property: border, background-color, color, box-shadow;
transition-duration: 0.15s;
&.accent {
font-weight: 500;
}
.btn-icon {
color: var(--theme-content-color);
transition: color 0.15s;
pointer-events: none;
}
.btn-right-icon {
margin-left: 0.5rem;
color: var(--theme-halfcontent-color);
transition: color 0.15s;
pointer-events: none;
}
&:not(.only-icon) .btn-icon:not(.spinner) {
margin-right: 0.5rem;
}
&:not(.only-icon) .btn-right-icon {
margin-left: 0.5rem;
}
&.no-border:not(.only-icon) .btn-icon,
&.link-bordered:not(.only-icon) .btn-icon,
&.list:not(.only-icon) .btn-icon,
&.sh-circle:not(.only-icon) .btn-icon {
margin-right: 0.25rem;
}
&.short {
max-width: 7rem;
}
&.sh-no-shape {
border-radius: 0.25rem;
}
&.sh-round {
border-radius: 0.5rem;
}
&.sh-circle {
border-radius: 1rem;
&.link {
padding: 0 0.5rem 0 0.25rem;
}
}
&.sh-rectangle-right {
border-top-left-radius: 0.25rem;
border-bottom-left-radius: 0.25rem;
}
&.sh-rectangle-left {
border-top-right-radius: 0.25rem;
border-bottom-right-radius: 0.25rem;
}
&.sh-filter {
border-radius: 0 0 0.5rem 0.5rem;
}
&.bs-solid {
border-style: solid;
}
&.bs-dashed {
border-style: dashed;
}
&.highlight {
box-shadow: inset 0 0 1px 1px var(--primary-button-enabled);
&:hover {
box-shadow: inset 0 0 1px 1px var(--primary-button-hovered);
}
}
&:hover {
.btn-icon {
color: var(--theme-caption-color);
}
}
&:focus {
&:not(.sh-filter) {
box-shadow: 0 0 0 2px var(--primary-button-focused-border);
}
&.sh-filter {
border-color: var(--primary-button-focused-border);
}
}
&:disabled {
color: var(--theme-dark-color);
cursor: not-allowed;
.btn-icon {
opacity: 0.5;
}
}
&.jf-left {
justify-content: flex-start;
}
&.jf-center {
justify-content: center;
}
&.only-icon {
padding: 0;
}
&.secondary {
background-color: var(--theme-button-enabled);
border-color: var(--theme-button-border);
// &.medium:not(.only-icon) {
// padding: 0 0.75rem;
// }
&:hover {
background-color: var(--theme-button-hovered);
}
&:active {
background-color: var(--theme-button-pressed);
}
&:focus {
background-color: var(--theme-button-focused);
}
&:disabled {
background-color: var(--theme-button-disabled);
}
&.selected {
background-color: var(--theme-button-hovered);
.btn-icon {
color: var(--theme-caption-color);
}
}
}
&.no-border {
font-weight: 400;
color: var(--theme-content-color);
background-color: var(--theme-button-enabled);
box-shadow: var(--button-shadow);
&:hover {
color: var(--theme-caption-color);
background-color: var(--theme-button-hovered);
.btn-icon {
color: var(--theme-caption-color);
}
}
&:disabled {
color: var(--theme-trans-color);
background-color: var(--theme-list-button-color);
cursor: default;
.btn-icon {
color: var(--theme-trans-color);
}
&:hover {
color: var(--theme-trans-color);
.btn-icon {
color: var(--theme-trans-color);
}
}
}
}
&.transparent {
&:hover {
background-color: var(--theme-button-hovered);
}
&.selected {
background-color: var(--highlight-select);
}
&.selected:hover {
background-color: var(--highlight-select-hover);
}
}
&.link {
padding: 0 0.875rem;
&:hover {
color: var(--theme-caption-color);
background-color: var(--theme-bg-color);
border-color: var(--theme-divider-color);
.btn-icon {
color: var(--theme-content-color);
}
}
&:disabled {
color: var(--theme-dark-color);
background-color: transparent;
border-color: transparent;
cursor: auto;
.btn-icon {
color: var(--theme-content-color);
}
}
}
&.link-bordered {
padding: 0 0.5rem;
color: var(--theme-content-color);
background-color: var(--theme-link-button-color);
border-color: var(--theme-button-border);
&:hover {
color: var(--theme-caption-color);
background-color: var(--theme-link-button-hover);
border-color: var(--theme-list-divider-color);
.btn-icon {
color: var(--theme-caption-color);
}
}
&.small {
padding: 0 0.25rem;
}
}
&.list {
padding: 0 0.625em;
min-height: 1.75rem;
color: var(--theme-content-color);
background-color: var(--theme-button-enabled);
border: 1px solid var(--theme-button-border);
border-radius: 1.5rem;
// transition-property: border, color, background-color;
// transition-duration: 0.15s;
.btn-icon {
color: var(--theme-dark-color);
}
&:hover {
color: var(--theme-caption-color);
background-color: var(--theme-button-hovered);
border-color: var(--theme-button-border);
}
&:focus {
box-shadow: none;
}
}
&.primary {
padding: 0 0.75rem;
font-weight: 500;
color: var(--primary-button-color);
background-color: var(--primary-button-enabled);
border-color: var(--primary-button-border);
.btn-icon {
color: var(--white-color);
}
&:hover {
background-color: var(--primary-button-hovered);
}
&:active {
background-color: var(--primary-button-pressed);
}
&:focus {
background-color: var(--primary-button-focused);
}
&:disabled {
background-color: var(--primary-button-disabled);
}
}
&.notSelected {
color: var(--theme-dark-color);
.btn-icon {
color: var(--theme-darker-color);
}
&:hover,
&:hover .btn-icon {
color: var(--theme-content-color);
}
}
&.dangerous {
color: var(--white-color);
background-color: var(--dangerous-bg-color);
border-color: var(--dangerous-bg-color);
&:hover {
background-color: var(--dangerous-bg-hover);
}
&:focus {
box-shadow: var(--dangerous-shadow);
}
}
.resetIconSize {
font-size: 16px;
}
}
</style>

View File

@ -102,9 +102,7 @@
}}
/>
{/if}
{#if !embedded}
{#if $$slots.navigator}<slot name="navigator" />{/if}
{/if}
{#if $$slots.navigator}<slot name="navigator" />{/if}
<div class="popupPanel-title__content">
{#if !twoRows && !withoutTitle}<slot name="title" />{/if}
</div>

View File

@ -298,7 +298,7 @@ class ActivityImpl implements Activity {
// Check mixin classes for desired attribute
for (const cl of this.hierarchy.getDescendants(cltx.objectClass)) {
try {
collectionAttribute = this.hierarchy.getAttribute(cl, cltx.collection) as Attribute<Collection<AttachedDoc>>
collectionAttribute = this.hierarchy.findAttribute(cl, cltx.collection) as Attribute<Collection<AttachedDoc>>
if (collectionAttribute !== undefined) {
break
}

View File

@ -0,0 +1,109 @@
<!--
// Copyright © 2023 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
import { Doc, Markup, updateAttribute } from '@hcengineering/core'
import { IntlString } from '@hcengineering/platform'
import { createQuery, getAttribute, getClient, KeyedAttribute } from '@hcengineering/presentation'
import { navigate } from '@hcengineering/ui'
import view from '@hcengineering/view'
import { getObjectLinkFragment } from '@hcengineering/view-resources'
import { createEventDispatcher } from 'svelte'
import AttachmentStyledBox from './AttachmentStyledBox.svelte'
export let object: Doc
export let key: KeyedAttribute
export let placeholder: IntlString
export let focusIndex = -1
export let updateBacklinks: ((doc: Doc, description: Markup) => void) | undefined = undefined
const client = getClient()
const queryClient = createQuery()
let description = ''
let doc: Doc | undefined
$: queryClient.query(object._class, { _id: object._id }, async (result) => {
;[doc] = result
if (doc) {
description = getAttribute(client, object, key)
}
})
const dispatch = createEventDispatcher()
let descriptionBox: AttachmentStyledBox
async function save () {
clearTimeout(saveTrigger)
if (!object) {
return
}
const old = getAttribute(client, object, key)
if (description !== old) {
updateAttribute(client, object, object._class, key, description)
dispatch('saved', true)
setTimeout(() => {
dispatch('saved', false)
}, 2500)
updateBacklinks?.(object, description)
}
await descriptionBox.createAttachments()
}
let saveTrigger: any
function triggerSave (): void {
clearTimeout(saveTrigger)
saveTrigger = setTimeout(save, 2500)
}
export function isFocused (): boolean {
return descriptionBox.isFocused()
}
</script>
{#key doc?._id}
<AttachmentStyledBox
{focusIndex}
enableBackReferences={true}
bind:this={descriptionBox}
useAttachmentPreview={true}
objectId={object._id}
_class={object._class}
space={object.space}
alwaysEdit
on:attached={(e) => descriptionBox.saveNewAttachment(e.detail)}
on:detached={(e) => descriptionBox.removeAttachmentById(e.detail)}
showButtons
on:blur={save}
on:changeContent={triggerSave}
maxHeight={'card'}
focusable
bind:content={description}
{placeholder}
on:open-document={async (event) => {
save()
const doc = await client.findOne(event.detail._class, { _id: event.detail._id })
if (doc != null) {
const location = await getObjectLinkFragment(client.getHierarchy(), doc, {}, view.component.EditDoc)
navigate(location)
}
}}
/>
{/key}

View File

@ -42,6 +42,7 @@
export let shouldSaveDraft: boolean = false
export let useAttachmentPreview = false
export let focusIndex: number | undefined = -1
export let enableBackReferences: boolean = false
let draftKey = objectId ? `${objectId}_attachments` : undefined
$: draftKey = objectId ? `${objectId}_attachments` : undefined
@ -337,10 +338,13 @@
{maxHeight}
{focusable}
{emphasized}
{enableBackReferences}
on:changeSize
on:changeContent
on:blur
on:focus
on:open-document
on:open-document
on:attach={() => {
if (fakeAttach === 'fake') dispatch('attach', { action: 'add' })
else if (fakeAttach === 'normal') attach()

View File

@ -32,6 +32,7 @@ import FileBrowser from './components/FileBrowser.svelte'
import FileDownload from './components/icons/FileDownload.svelte'
import Photos from './components/Photos.svelte'
import AttachmentStyledBox from './components/AttachmentStyledBox.svelte'
import AttachmentStyleBoxEditor from './components/AttachmentStyleBoxEditor.svelte'
import AccordionEditor from './components/AccordionEditor.svelte'
import IconUploadDuo from './components/icons/UploadDuo.svelte'
import IconAttachment from './components/icons/Attachment.svelte'
@ -52,6 +53,7 @@ export {
FileDownload,
FileBrowser,
AttachmentStyledBox,
AttachmentStyleBoxEditor,
AccordionEditor,
IconUploadDuo,
IconAttachment,

View File

@ -58,14 +58,14 @@ function extractBacklinks (
const ato = el.getAttribute('data-id') as Ref<Doc>
const atoClass = el.getAttribute('data-objectclass') as Ref<Class<Doc>>
const e = result.find((e) => e.attachedTo === ato && e.attachedToClass === atoClass)
if (e === undefined) {
if (e === undefined && ato !== attachedDocId && ato !== backlinkId) {
result.push({
attachedTo: ato,
attachedToClass: atoClass,
collection: 'backlinks',
backlinkId,
backlinkClass,
message,
message: el.parentElement?.innerHTML ?? '',
attachedDocId
})
}
@ -83,7 +83,7 @@ export function getBacklinks (
content: string
): Array<Data<Backlink>> {
const parser = new DOMParser()
const doc = parser.parseFromString(content, 'application/xhtml+xml')
const doc = parser.parseFromString(content, 'text/html')
return extractBacklinks(backlinkId, backlinkClass, attachedDocId, content, doc.childNodes as NodeListOf<HTMLElement>)
}
@ -107,6 +107,10 @@ export async function createBacklinks (
)
}
}
/**
* @public
*/
export async function updateBacklinks (
client: TxOperations,
backlinkId: Ref<Doc>,
@ -135,7 +139,9 @@ export async function updateBacklinksList (
// We need to find ones we need to remove, and ones we need to update.
for (const c of current) {
// Find existing and check if we need to update message.
const pos = backlinks.findIndex((b) => b.backlinkId === c.backlinkId && b.backlinkClass === c.backlinkClass)
const pos = backlinks.findIndex(
(b) => b.backlinkId === c.backlinkId && b.backlinkClass === c.backlinkClass && b.attachedTo === c.attachedTo
)
if (pos !== -1) {
// We need to check and update if required.
const data = backlinks[pos]

View File

@ -21,13 +21,10 @@
import { getObjectPresenter } from '@hcengineering/view-resources'
import chunter from '../../plugin'
// export let tx: TxCreateDoc<Backlink>
export let value: Backlink
// export let edit: boolean = false
const client = getClient()
let presenter: AttributeModel | undefined
let targetPresenter: AttributeModel | undefined
const docQuery = createQuery()
const targetQuery = createQuery()
@ -43,12 +40,6 @@
target = r.shift()
})
$: if (target !== undefined) {
getObjectPresenter(client, target._class, { key: '' }).then((p) => {
targetPresenter = p
})
}
$: if (doc !== undefined) {
getObjectPresenter(client, doc._class, { key: '' }).then((p) => {
presenter = p
@ -58,9 +49,6 @@
{#if presenter}
<span class="labels-row">
{#if targetPresenter}
<svelte:component this={targetPresenter.presenter} value={target} inline />
{/if}
<span style:text-transform={'lowercase'}><Label label={chunter.string.In} /></span>
<svelte:component this={presenter.presenter} value={doc} inline />
</span>

View File

@ -64,6 +64,7 @@ import { getDmName, getLink, getTitle, resolveLocation } from './utils'
export { default as Header } from './components/Header.svelte'
export { classIcon } from './utils'
export { CommentPopup, CommentsPresenter }
export { createBacklinks, updateBacklinks } from './backlinks'
async function MarkUnread (object: Message): Promise<void> {
const client = NotificationClientImpl.getClient()

View File

@ -19,9 +19,10 @@
import { Scroller } from '@hcengineering/ui'
import { createEventDispatcher, onMount } from 'svelte'
import CandidateCard from './CandidateCard.svelte'
import ExpandRightDouble from './icons/ExpandRightDouble.svelte'
import VacancyCard from './VacancyCard.svelte'
import ExpandRightDouble from './icons/ExpandRightDouble.svelte'
import { getName } from '@hcengineering/contact'
import { Ref } from '@hcengineering/core'
import recruit from '../plugin'
import Reviews from './review/Reviews.svelte'
@ -30,10 +31,20 @@
let candidate: Candidate
let vacancy: Vacancy
const dispatch = createEventDispatcher()
const sendOpen = () => {
dispatch('open', {
ignoreKeys: ['comments', 'number'],
allowedCollections: ['labels'],
title: `APP-${object.number} ${candidate !== undefined ? '- ' + getName(candidate) : ''}`
})
}
const candidateQuery = createQuery()
$: if (object !== undefined) {
candidateQuery.query(recruit.mixin.Candidate, { _id: object.attachedTo as Ref<Candidate> }, (result) => {
candidate = result[0]
sendOpen()
})
}
@ -44,10 +55,8 @@
})
}
const dispatch = createEventDispatcher()
onMount(() => {
dispatch('open', { ignoreKeys: ['comments', 'number'], allowedCollections: ['labels'] })
sendOpen()
})
</script>

View File

@ -14,21 +14,20 @@
// limitations under the License.
-->
<script lang="ts">
import { Attachments } from '@hcengineering/attachment-resources'
import core, { ClassifierKind, Doc, Mixin, Ref } from '@hcengineering/core'
import { AttachmentStyleBoxEditor } from '@hcengineering/attachment-resources'
import { updateBacklinks } from '@hcengineering/chunter-resources'
import core, { ClassifierKind, Data, Doc, Mixin, Ref } from '@hcengineering/core'
import notification from '@hcengineering/notification'
import { Panel } from '@hcengineering/panel'
import { createQuery, getClient } from '@hcengineering/presentation'
import { getResource } from '@hcengineering/platform'
import presentation, { createQuery, getClient } from '@hcengineering/presentation'
import { Vacancy } from '@hcengineering/recruit'
import { FullDescriptionBox } from '@hcengineering/text-editor'
import tracker from '@hcengineering/tracker'
import { Button, Component, EditBox, Grid, IconMixin, IconMoreH, LinkWrapper, showPopup } from '@hcengineering/ui'
import { Button, Component, EditBox, IconMixin, IconMoreH, Label, LinkWrapper, showPopup } from '@hcengineering/ui'
import { ContextMenu, DocAttributeBar } from '@hcengineering/view-resources'
import { createEventDispatcher } from 'svelte'
import { createEventDispatcher, onDestroy } from 'svelte'
import recruit from '../plugin'
import VacancyApplications from './VacancyApplications.svelte'
import { getResource } from '@hcengineering/platform'
import notification from '@hcengineering/notification'
import { onDestroy } from 'svelte'
export let _id: Ref<Vacancy>
export let embedded = false
@ -36,6 +35,7 @@
let object: Required<Vacancy>
let rawName: string = ''
let rawDesc: string = ''
let rawFullDesc: string = ''
let lastId: Ref<Vacancy> | undefined = undefined
let showAllMixins = false
@ -63,16 +63,13 @@
object = result[0] as Required<Vacancy>
rawName = object.name
rawDesc = object.description
rawFullDesc = object.fullDescription
})
}
}
$: updateObject(_id)
function onChange (key: string, value: any): void {
client.updateDoc(object._class, object.space, object._id, { [key]: value })
}
function showMenu (ev?: Event): void {
if (object !== undefined) {
showPopup(ContextMenu, { object }, (ev as MouseEvent).target as HTMLElement)
@ -97,13 +94,41 @@
}
$: getMixins(object, showAllMixins)
let descriptionBox: AttachmentStyleBoxEditor
$: descriptionKey = client.getHierarchy().getAttribute(recruit.class.Vacancy, 'fullDescription')
let saved = false
async function save () {
if (!object) {
return
}
const updates: Partial<Data<Vacancy>> = {}
const trimmedName = rawName.trim()
if (trimmedName.length > 0 && trimmedName !== object.name) {
updates.name = trimmedName
}
if (rawDesc !== object.description) {
updates.description = rawDesc
}
if (rawFullDesc !== object.fullDescription) {
updates.fullDescription = rawFullDesc
}
if (Object.keys(updates).length > 0) {
await client.update(object, updates)
}
}
</script>
{#if object}
<Panel
icon={clazz.icon}
title={object.name}
isHeader={true}
isHeader={false}
isSub={false}
isAside={true}
{embedded}
{object}
@ -121,69 +146,67 @@
{/if}
</svelte:fragment>
<svelte:fragment slot="actions">
<Button
icon={IconMixin}
kind={'transparent'}
shape={'round'}
selected={showAllMixins}
on:click={() => {
showAllMixins = !showAllMixins
}}
/>
</svelte:fragment>
<svelte:fragment slot="attributes" let:direction={dir}>
{#if dir === 'column'}
<DocAttributeBar
{object}
{mixins}
ignoreKeys={['name', 'description', 'fullDescription', 'private', 'archived']}
/>
<DocAttributeBar {object} {mixins} ignoreKeys={['name', 'fullDescription', 'private', 'archived']} />
{/if}
</svelte:fragment>
<svelte:fragment slot="subheader">
<span class="fs-title flex-grow">
<EditBox
bind:value={object.name}
placeholder={recruit.string.VacancyPlaceholder}
kind={'large-style'}
focusable
on:blur={() => {
if (rawName !== object.name) onChange('name', object.name)
}}
/>
</span>
<span class="fs-title flex-grow">
<EditBox
bind:value={object.name}
placeholder={recruit.string.VacancyPlaceholder}
kind={'large-style'}
focusable
focus={!embedded}
on:blur={save}
/>
</span>
<svelte:fragment slot="pre-utils">
{#if saved}
<Label label={presentation.string.Saved} />
{/if}
</svelte:fragment>
<svelte:fragment slot="utils">
<div class="p-1">
<Button icon={IconMoreH} kind={'transparent'} size={'medium'} on:click={showMenu} />
</div>
<div class="p-1">
<Button
icon={IconMixin}
kind={'transparent'}
shape={'round'}
selected={showAllMixins}
on:click={() => {
showAllMixins = !showAllMixins
}}
/>
</div>
</svelte:fragment>
<Grid column={1} rowGap={1.5}>
<EditBox
bind:value={object.description}
placeholder={recruit.string.VacancyDescription}
focusable
on:blur={() => {
if (rawDesc !== object.description) onChange('description', object.description)
<!-- <EditBox bind:value={object.description} placeholder={recruit.string.VacancyDescription} focusable on:blur={save} /> -->
<div class="w-full mt-6">
<AttachmentStyleBoxEditor
focusIndex={30}
{object}
key={{ key: 'fullDescription', attr: descriptionKey }}
bind:this={descriptionBox}
placeholder={recruit.string.FullDescription}
on:saved={(evt) => {
saved = evt.detail
}}
updateBacklinks={(doc, description) => {
updateBacklinks(client, doc._id, doc._class, doc._id, description)
}}
/>
<FullDescriptionBox
content={object.fullDescription}
on:save={(res) => {
onChange('fullDescription', res.detail)
}}
/>
<Attachments
objectId={object._id}
_class={object._class}
space={object.space}
attachments={object.attachments ?? 0}
/>
</div>
<div class="w-full mt-6">
<VacancyApplications objectId={object._id} />
</div>
<div class="w-full mt-6">
<Component is={tracker.component.RelatedIssuesSection} props={{ object, label: recruit.string.RelatedIssues }} />
</Grid>
</div>
</Panel>
{/if}

View File

@ -71,6 +71,7 @@
import SetDueDateActionPopup from './SetDueDateActionPopup.svelte'
import SetParentIssueActionPopup from './SetParentIssueActionPopup.svelte'
import SubIssues from './SubIssues.svelte'
import { createBacklinks } from '@hcengineering/chunter-resources'
export let space: Ref<Project>
export let status: Ref<IssueStatus> | undefined = undefined
@ -400,6 +401,9 @@
issueUrl: currentProject && generateIssueShortLink(getIssueId(currentProject, value as Issue))
})
// Create an backlink to document
await createBacklinks(client, _id, tracker.class.Issue, _id, object.description)
draftController.remove()
resetObject()
descriptionBox?.removeDraft(false)
@ -607,6 +611,7 @@
alwaysEdit
showButtons={false}
emphasized
enableBackReferences={true}
bind:content={object.description}
placeholder={tracker.string.IssueDescriptionPlaceholder}
on:changeSize={() => dispatch('changeContent')}

View File

@ -1,10 +1,11 @@
<script lang="ts">
import { AttachmentStyleBoxEditor } from '@hcengineering/attachment-resources'
import { updateBacklinks } from '@hcengineering/chunter-resources'
import { getClient } from '@hcengineering/presentation'
import { Component } from '@hcengineering/tracker'
import { EditBox } from '@hcengineering/ui'
import { createEventDispatcher } from 'svelte'
import { createEventDispatcher, onMount } from 'svelte'
import tracker from '../../plugin'
import { onMount } from 'svelte'
export let object: Component
@ -23,14 +24,16 @@
rawLabel = object.label
}
onMount(() => dispatch('open', { ignoreKeys: ['label'] }))
onMount(() => dispatch('open', { ignoreKeys: ['label', 'description', 'attachments'] }))
$: descriptionKey = client.getHierarchy().getAttribute(tracker.class.Component, 'description')
let descriptionBox: AttachmentStyleBoxEditor
</script>
<EditBox
bind:value={rawLabel}
placeholder={tracker.string.Component}
kind="large-style"
focusable
on:blur={() => {
const trimmedLabel = rawLabel.trim()
@ -41,3 +44,16 @@
}
}}
/>
<div class="w-full mt-6">
<AttachmentStyleBoxEditor
focusIndex={30}
{object}
key={{ key: 'description', attr: descriptionKey }}
bind:this={descriptionBox}
placeholder={tracker.string.IssueDescriptionPlaceholder}
updateBacklinks={(doc, description) => {
updateBacklinks(client, doc._id, doc._class, doc._id, description)
}}
/>
</div>

View File

@ -62,6 +62,7 @@
bind:content={object.description}
placeholder={tracker.string.ComponentDescriptionPlaceholder}
emphasized
showButtons={false}
/>
<svelte:fragment slot="pool">
<EmployeeBox

View File

@ -13,12 +13,12 @@
// limitations under the License.
-->
<script lang="ts">
import { AttachmentStyledBox } from '@hcengineering/attachment-resources'
import { Class, Data, Doc, Ref, WithLookup } from '@hcengineering/core'
import { AttachmentStyleBoxEditor } from '@hcengineering/attachment-resources'
import { Class, Doc, Ref, WithLookup } from '@hcengineering/core'
import notification from '@hcengineering/notification'
import { Panel } from '@hcengineering/panel'
import { getResource } from '@hcengineering/platform'
import { createQuery, getClient } from '@hcengineering/presentation'
import presentation, { createQuery, getClient } from '@hcengineering/presentation'
import setting, { settingId } from '@hcengineering/setting'
import type { Issue, Project } from '@hcengineering/tracker'
import {
@ -43,6 +43,7 @@
import CopyToClipboard from './CopyToClipboard.svelte'
import SubIssueSelector from './SubIssueSelector.svelte'
import SubIssues from './SubIssues.svelte'
import { updateBacklinks } from '@hcengineering/chunter-resources'
export let _id: Ref<Issue>
export let _class: Ref<Class<Issue>>
@ -58,7 +59,7 @@
let title = ''
let description = ''
let innerWidth: number
let descriptionBox: AttachmentStyledBox
let descriptionBox: AttachmentStyleBoxEditor
let showAllMixins: boolean
const notificationClient = getResource(notification.function.GetNotificationClient).then((res) => res())
@ -82,8 +83,7 @@
_class,
{ _id },
async (result) => {
if (saveTrigger !== undefined && lastId !== _id) {
clearTimeout(saveTrigger)
if (lastId !== _id) {
await save()
}
;[issue] = result
@ -102,36 +102,15 @@
let saved = false
async function save () {
clearTimeout(saveTrigger)
if (!issue || !canSave) {
return
}
const updates: Partial<Data<Issue>> = {}
const trimmedTitle = title.trim()
if (trimmedTitle.length > 0 && trimmedTitle !== issue.title) {
updates.title = trimmedTitle
await client.update(issue, { title: trimmedTitle })
}
if (description !== issue.description) {
updates.description = description
}
if (Object.keys(updates).length > 0) {
await client.update(issue, updates)
saved = true
setTimeout(() => {
saved = false
}, 5000)
}
await descriptionBox.createAttachments()
}
let saveTrigger: any
function triggerSave (): void {
clearTimeout(saveTrigger)
saveTrigger = setTimeout(save, 5000)
}
function showMenu (ev?: Event): void {
@ -151,6 +130,8 @@
// If it is embedded
$: lastCtx = $contextStore.getLastContext()
$: isContextEnabled = lastCtx?.mode === 'editor' || lastCtx?.mode === 'browser'
$: descriptionKey = client.getHierarchy().getAttribute(tracker.class.Issue, 'description')
</script>
{#if !embedded}
@ -165,21 +146,21 @@
{#if issue !== undefined}
<Panel
object={issue}
isHeader
isHeader={false}
isAside={true}
isSub={false}
withoutActivity={false}
{embedded}
withoutTitle
bind:innerWidth
on:open
on:close={() => dispatch('close')}
>
<svelte:fragment slot="navigator">
<UpDownNavigator element={issue} />
</svelte:fragment>
<svelte:fragment slot="header">
<span class="fs-title select-text-i">
{#if !embedded}
<UpDownNavigator element={issue} />
{/if}
<span class="ml-4 fs-title select-text-i">
{#if embedded}
<DocNavLink object={issue}>
{#if issueId}{issueId}{/if}
@ -187,11 +168,10 @@
{:else if issueId}{issueId}{/if}
</span>
</svelte:fragment>
<svelte:fragment slot="tools">
<svelte:fragment slot="pre-utils">
{#if saved}
<Label label={tracker.string.Saved} />
<Label label={presentation.string.Saved} />
{/if}
<Button icon={IconMoreH} kind={'transparent'} size={'medium'} on:click={showMenu} />
</svelte:fragment>
{#if parentIssue}
@ -212,26 +192,19 @@
focus={!embedded}
/>
<div class="w-full mt-6">
{#key issue._id}
<AttachmentStyledBox
focusIndex={30}
bind:this={descriptionBox}
useAttachmentPreview={true}
objectId={_id}
_class={tracker.class.Issue}
space={issue.space}
alwaysEdit
on:attached={(e) => descriptionBox.saveNewAttachment(e.detail)}
on:detached={(e) => descriptionBox.removeAttachmentById(e.detail)}
showButtons
on:blur={save}
on:changeContent={triggerSave}
maxHeight={'card'}
focusable
bind:content={description}
placeholder={tracker.string.IssueDescriptionPlaceholder}
/>
{/key}
<AttachmentStyleBoxEditor
focusIndex={30}
object={issue}
key={{ key: 'description', attr: descriptionKey }}
bind:this={descriptionBox}
placeholder={tracker.string.IssueDescriptionPlaceholder}
on:saved={(evt) => {
saved = evt.detail
}}
updateBacklinks={(doc, description) => {
updateBacklinks(client, doc._id, doc._class, doc._id, description)
}}
/>
</div>
<div class="mt-6">
@ -250,8 +223,8 @@
<span slot="actions-label" class="select-text">
{#if issueId}{issueId}{/if}
</span>
<svelte:fragment slot="actions">
<div class="flex-grow" />
<svelte:fragment slot="utils">
<Button icon={IconMoreH} kind={'transparent'} size={'medium'} on:click={showMenu} />
{#if issueId}
<CopyToClipboard issueUrl={generateIssueShortLink(issueId)} {issueId} />
{/if}

View File

@ -18,6 +18,8 @@
import { EditBox } from '@hcengineering/ui'
import { createEventDispatcher, onMount } from 'svelte'
import tracker from '../../plugin'
import { AttachmentStyleBoxEditor } from '@hcengineering/attachment-resources'
import { updateBacklinks } from '@hcengineering/chunter-resources'
export let object: Milestone
@ -36,14 +38,15 @@
rawLabel = object.label
}
onMount(() => dispatch('open', { ignoreKeys: ['label'] }))
onMount(() => dispatch('open', { ignoreKeys: ['label', 'description', 'attachments'] }))
$: descriptionKey = client.getHierarchy().getAttribute(tracker.class.Component, 'description')
let descriptionBox: AttachmentStyleBoxEditor
</script>
<EditBox
bind:value={rawLabel}
placeholder={tracker.string.MilestoneNamePlaceholder}
kind="large-style"
focusable
on:blur={async () => {
const trimmedLabel = rawLabel.trim()
@ -54,3 +57,16 @@
}
}}
/>
<div class="w-full mt-6">
<AttachmentStyleBoxEditor
focusIndex={30}
{object}
key={{ key: 'description', attr: descriptionKey }}
bind:this={descriptionBox}
placeholder={tracker.string.IssueDescriptionPlaceholder}
updateBacklinks={(doc, description) => {
updateBacklinks(client, doc._id, doc._class, doc._id, description)
}}
/>
</div>

View File

@ -22,6 +22,7 @@
import { createEventDispatcher } from 'svelte'
import tracker from '../../plugin'
import MilestoneStatusSelector from './MilestoneStatusSelector.svelte'
import { createBacklinks } from '@hcengineering/chunter-resources'
export let space: Ref<Project>
const dispatch = createEventDispatcher()
@ -37,7 +38,9 @@
}
async function onSave () {
await client.createDoc(tracker.class.Milestone, space, object)
const _id = await client.createDoc(tracker.class.Milestone, space, object)
// Create an backlink to document
await createBacklinks(client, _id, tracker.class.Milestone, _id, object.description ?? '')
}
const handleComponentStatusChanged = (newMilestoneStatus: MilestoneStatus | undefined) => {
@ -71,6 +74,7 @@
bind:content={object.description}
placeholder={tracker.string.ComponentDescriptionPlaceholder}
emphasized
showButtons={false}
/>
<svelte:fragment slot="pool">
<MilestoneStatusSelector

View File

@ -13,23 +13,21 @@
// limitations under the License.
-->
<script lang="ts">
import { AttachmentDocList } from '@hcengineering/attachment-resources'
import { Class, Data, Doc, Ref, WithLookup } from '@hcengineering/core'
import { AttachmentStyleBoxEditor } from '@hcengineering/attachment-resources'
import { Class, Doc, Ref, WithLookup } from '@hcengineering/core'
import notification from '@hcengineering/notification'
import { Panel } from '@hcengineering/panel'
import { getResource } from '@hcengineering/platform'
import presentation, { MessageViewer, createQuery, getClient } from '@hcengineering/presentation'
import { createQuery, getClient } from '@hcengineering/presentation'
import setting, { settingId } from '@hcengineering/setting'
import tags from '@hcengineering/tags'
import type { IssueTemplate, IssueTemplateChild, Project } from '@hcengineering/tracker'
import {
Button,
EditBox,
IconAttachment,
IconEdit,
Icon,
IconMoreH,
Label,
Scroller,
getCurrentResolvedLocation,
navigate,
showPopup
@ -38,12 +36,13 @@
import { createEventDispatcher, onDestroy, onMount } from 'svelte'
import tracker from '../../plugin'
import { StyledTextBox } from '@hcengineering/text-editor'
import { updateBacklinks } from '@hcengineering/chunter-resources'
import SubIssueTemplates from './IssueTemplateChilds.svelte'
import TemplateControlPanel from './TemplateControlPanel.svelte'
export let _id: Ref<IssueTemplate>
export let _class: Ref<Class<IssueTemplate>>
export let embedded = false
let lastId: Ref<Doc> = _id
const query = createQuery()
@ -55,8 +54,8 @@
let title = ''
let description = ''
let innerWidth: number
let isEditing = false
let descriptionBox: StyledTextBox
let descriptionBox: AttachmentStyleBoxEditor
const notificationClient = getResource(notification.function.GetNotificationClient).then((res) => res())
@ -88,48 +87,19 @@
)
$: canSave = title.trim().length > 0
$: isDescriptionEmpty = !new DOMParser().parseFromString(description, 'text/html').documentElement.innerText?.trim()
function edit (ev: MouseEvent) {
ev.preventDefault()
isEditing = true
}
function cancelEditing (ev: MouseEvent) {
ev.preventDefault()
isEditing = false
if (template) {
title = template.title
description = template.description
}
}
async function save (ev: MouseEvent) {
ev.preventDefault()
let saved = false
async function save () {
if (!template || !canSave) {
return
}
const updates: Partial<Data<IssueTemplate>> = {}
const trimmedTitle = title.trim()
if (trimmedTitle.length > 0 && trimmedTitle !== template.title) {
updates.title = trimmedTitle
await client.update(template, { title: trimmedTitle })
}
if (description !== template.description) {
updates.description = description
}
if (Object.keys(updates).length > 0) {
await client.update(template, updates)
}
await descriptionBox.createAttachments()
isEditing = false
}
function showMenu (ev?: Event): void {
@ -170,106 +140,72 @@
children: evt.detail
})
}
$: descriptionKey = client.getHierarchy().getAttribute(tracker.class.IssueTemplate, 'description')
</script>
{#if template !== undefined}
<Panel
object={template}
isHeader
isHeader={false}
isAside={true}
isSub={false}
withoutActivity={isEditing}
withoutActivity={false}
{embedded}
bind:innerWidth
on:open
on:close={() => dispatch('close')}
>
<svelte:fragment slot="navigator">
<UpDownNavigator element={template} />
</svelte:fragment>
<svelte:fragment slot="header">
<span class="fs-title">
{#if !embedded}
<UpDownNavigator element={template} />
{/if}
<div class="ml-2">
<Icon icon={tracker.icon.IssueTemplates} size={'small'} />
</div>
<span class="fs-title flex-row-center">
{template.title}
</span>
</svelte:fragment>
<svelte:fragment slot="tools">
{#if isEditing}
<Button kind={'transparent'} label={presentation.string.Cancel} on:click={cancelEditing} />
<Button disabled={!canSave} label={presentation.string.Save} on:click={save} />
{:else}
<Button icon={IconEdit} kind={'transparent'} size={'medium'} on:click={edit} />
<Button icon={IconMoreH} kind={'transparent'} size={'medium'} on:click={showMenu} />
<EditBox bind:value={title} placeholder={tracker.string.IssueTitlePlaceholder} kind="large-style" on:blur={save} />
<div class="w-full mt-6">
<AttachmentStyleBoxEditor
focusIndex={30}
object={template}
key={{ key: 'description', attr: descriptionKey }}
bind:this={descriptionBox}
placeholder={tracker.string.IssueDescriptionPlaceholder}
on:saved={(evt) => {
saved = evt.detail
}}
updateBacklinks={(doc, description) => {
updateBacklinks(client, doc._id, doc._class, doc._id, description)
}}
/>
</div>
<div class="mt-6">
{#key template._id && currentProject !== undefined}
{#if currentProject !== undefined}
<SubIssueTemplates
maxHeight="limited"
project={template.space}
bind:children={template.children}
on:create-issue={createIssue}
on:update-issue={updateIssue}
on:update-issues={updateIssues}
/>
{/if}
{/key}
</div>
<svelte:fragment slot="pre-utils">
{#if saved}
<Label label={tracker.string.Saved} />
{/if}
</svelte:fragment>
{#if isEditing}
<Scroller>
<div class="popupPanel-body__main-content py-10 clear-mins content">
<EditBox
bind:value={title}
maxWidth="53.75rem"
placeholder={tracker.string.IssueTitlePlaceholder}
kind="large-style"
/>
<div class="flex-between mt-6">
<div class="flex-grow">
<StyledTextBox
bind:this={descriptionBox}
alwaysEdit
showButtons
maxHeight={'card'}
bind:content={description}
placeholder={tracker.string.IssueDescriptionPlaceholder}
/>
</div>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div
class="tool"
on:click={() => {
descriptionBox.attach()
}}
>
<IconAttachment size={'large'} />
</div>
</div>
</div>
</Scroller>
{:else}
<span class="title select-text">{title}</span>
<div class="mt-6 description-preview select-text">
{#if isDescriptionEmpty}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div class="placeholder" on:click={edit}>
<Label label={tracker.string.IssueDescriptionPlaceholder} />
</div>
{:else}
<MessageViewer message={description} />
{/if}
</div>
<div class="mt-6">
{#key template._id && currentProject !== undefined}
{#if currentProject !== undefined}
<SubIssueTemplates
maxHeight="limited"
project={template.space}
bind:children={template.children}
on:create-issue={createIssue}
on:update-issue={updateIssue}
on:update-issues={updateIssues}
/>
{/if}
{/key}
</div>
<div class="mt-6">
<AttachmentDocList value={template} />
</div>
{/if}
<span slot="actions-label"> ID </span>
<svelte:fragment slot="actions">
<div class="flex-grow" />
<!-- {#if issueId}
<CopyToClipboard issueUrl={generateIssueShortLink(issueId)} {issueId} />
{/if} -->
<svelte:fragment slot="utils">
<Button icon={IconMoreH} kind={'transparent'} size={'medium'} on:click={showMenu} />
<Button
icon={setting.icon.Setting}
kind={'transparent'}

View File

@ -23,11 +23,11 @@
AttributeCategory,
AttributeCategoryOrder,
AttributesBar,
KeyedAttribute,
createQuery,
getAttributePresenterClass,
getClient,
hasResource,
KeyedAttribute
hasResource
} from '@hcengineering/presentation'
import { AnyComponent, Button, Component, IconMixin, IconMoreH, showPopup } from '@hcengineering/ui'
import view from '@hcengineering/view'
@ -203,20 +203,21 @@
if (clazz.icon !== undefined) return clazz.icon
while (clazz.extends !== undefined) {
clazz = hierarchy.getClass(clazz.extends)
if (clazz.icon !== undefined) {
if (clazz.icon != null) {
return clazz.icon
}
}
throw new Error(`Icon not found for ${_class}`)
// throw new Error(`Icon not found for ${_class}`)
}
$: icon = getIcon(realObjectClass)
let title: string = ''
let rawTitle: string = ''
$: if (object !== undefined) {
getTitle(object).then((t) => {
title = t
rawTitle = t
})
}
@ -266,9 +267,12 @@
ignoreMixins = new Set(ev.detail.ignoreMixins)
allowedCollections = ev.detail.allowedCollections ?? []
collectionArrays = ev.detail.collectionArrays ?? []
title = ev.detail.title
getMixins(object, showAllMixins)
updateKeys()
}
$: finalTitle = title ?? rawTitle
</script>
<ActionContext
@ -277,10 +281,10 @@
}}
/>
{#if object !== undefined && title !== undefined}
{#if object !== undefined && finalTitle !== undefined}
<Panel
{icon}
{title}
title={finalTitle}
{object}
{embedded}
isHeader={mainEditor?.pinned ?? false}
@ -296,26 +300,28 @@
withoutInput={!activityOptions.showInput}
>
<svelte:fragment slot="navigator">
<UpDownNavigator element={object} />
{#if !embedded}
<UpDownNavigator element={object} />
{/if}
</svelte:fragment>
<svelte:fragment slot="utils">
<div class="p-1">
<Button icon={IconMoreH} kind={'transparent'} size={'medium'} on:click={showMenu} />
</div>
<div class="p-1">
<Button
icon={IconMixin}
kind={'transparent'}
shape={'round'}
selected={showAllMixins}
on:click={() => {
showAllMixins = !showAllMixins
}}
/>
</div>
</svelte:fragment>
<svelte:fragment slot="actions">
<Button
icon={IconMixin}
kind={'transparent'}
shape={'round'}
selected={showAllMixins}
on:click={() => {
showAllMixins = !showAllMixins
}}
/>
</svelte:fragment>
<svelte:fragment slot="attributes" let:direction={dir}>
{#if !headerLoading}
{#if headerEditor !== undefined}

View File

@ -1,7 +1,7 @@
<script lang="ts">
import { Doc } from '@hcengineering/core'
import { getClient } from '@hcengineering/presentation'
import ui, { Button, closeTooltip, IconDownOutline, IconNavPrev, IconUpOutline, navigate } from '@hcengineering/ui'
import { Button, IconDownOutline, IconUpOutline, navigate } from '@hcengineering/ui'
import { tick } from 'svelte'
import { select } from '../actionImpl'
import view from '../plugin'
@ -30,11 +30,6 @@
}
}
function goBack () {
closeTooltip()
history.back()
}
$: select(undefined, 0, element, 'vertical')
</script>
@ -54,11 +49,3 @@
on:click={(evt) => next(evt, false)}
/>
{/if}
<Button
focusIndex={10007}
showTooltip={{ label: ui.string.Back, direction: 'bottom' }}
icon={IconNavPrev}
kind={'secondary'}
size={'medium'}
on:click={goBack}
/>

View File

@ -81,16 +81,17 @@ export async function getObjectPresenter (
preserveKey: BuildModelKey,
isCollectionAttr: boolean = false,
checkResource = false
): Promise<AttributeModel> {
): Promise<AttributeModel | undefined> {
const hierarchy = client.getHierarchy()
const mixin = isCollectionAttr ? view.mixin.CollectionPresenter : view.mixin.ObjectPresenter
const clazz = hierarchy.getClass(_class)
const presenterMixin = hierarchy.classHierarchyMixin(_class, mixin, (m) => !checkResource || hasResource(m.presenter))
if (presenterMixin?.presenter === undefined) {
throw new Error(
console.error(
`object presenter not found for class=${_class}, mixin=${mixin}, preserve key ${JSON.stringify(preserveKey)}`
)
return undefined
}
const presenter = await getResource(presenterMixin.presenter)
const key = preserveKey.sortingKey ?? preserveKey.key
@ -195,7 +196,11 @@ export async function getPresenter<T extends Doc> (
}
}
if (key.key.length === 0) {
return await getObjectPresenter(client, _class, preserveKey, isCollectionAttr)
const p = await getObjectPresenter(client, _class, preserveKey, isCollectionAttr)
if (p === undefined) {
throw new Error(`object presenter not found for class=${_class}, preserve key ${JSON.stringify(preserveKey)}`)
}
return p
} else {
if (key.key.startsWith('$lookup')) {
if (lookup === undefined) {

View File

@ -42,7 +42,7 @@ test.describe('recruit tests', () => {
await page.locator('.antiPopup').locator('text=Email').click()
const emailInput = page.locator('[placeholder="john\\.appleseed@apple\\.com"]')
await emailInput.fill(email)
await page.locator('#channel-ok.button').click()
await page.locator('#channel-ok.antiButton').click()
await page.locator('.antiCard button:has-text("Create")').click()
await page.waitForSelector('form.antiCard', { state: 'detached' })

View File

@ -175,7 +175,7 @@ test('report-time-from-main-view', async ({ page }) => {
await createIssue(page, { name, assignee, status })
// await page.click('.close-button > .button')
// await page.click('.close-button > .antiButton')
// We need to fait for indexer to complete indexing.
await fillSearch(page, name)
@ -243,7 +243,7 @@ test('create-issue-draft', async ({ page }) => {
await page.locator('[placeholder="Type text\\.\\.\\."]').click()
// Fill [placeholder="Type text\.\.\."]
await page.locator('[placeholder="Type text\\.\\.\\."]').fill('1')
await page.locator('.ml-2 > .button').click()
await page.locator('.ml-2 > .antiButton').click()
// Click button:nth-child(8)
await page.locator('#more-actions').click()

View File

@ -68,22 +68,22 @@ export async function fillIssueForm (page: Page, props: IssueProps): Promise<voi
await page.click(`.selectPopup button:has-text("${priority}")`)
}
if (labels !== undefined) {
await page.click(af + '.button:has-text("Labels")')
await page.click(af + '.antiButton:has-text("Labels")')
for (const label of labels) {
await page.click(`.selectPopup button:has-text("${label}") >> nth=0`)
}
await page.keyboard.press('Escape')
}
if (assignee !== undefined) {
await page.click(af + '.button:has-text("Assignee")')
await page.click(af + '.antiButton:has-text("Assignee")')
await page.click(`.selectPopup button:has-text("${assignee}")`)
}
if (component !== undefined) {
await page.click(af + 'button:has-text("Component")')
await page.click(af + '.antiButton:has-text("Component")')
await page.click(`.selectPopup button:has-text("${component}")`)
}
if (milestone !== undefined) {
await page.click(af + '.button:has-text("No Milestone")')
await page.click(af + '.antiButton:has-text("No Milestone")')
await page.click(`.selectPopup button:has-text("${milestone}")`)
}
}