mirror of
https://github.com/hcengineering/platform.git
synced 2024-11-22 11:42:30 +03:00
UBER-134: Back references (#3233)
Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
parent
14fbe03d9f
commit
d06f3316b3
@ -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" />
|
||||
|
@ -4,6 +4,7 @@
|
||||
"Cancel": "Cancel",
|
||||
"Ok": "Ok",
|
||||
"Save": "Save",
|
||||
"Saved": "Saved...",
|
||||
"Download": "Download",
|
||||
"Delete": "Delete",
|
||||
"Close": "Close",
|
||||
|
@ -4,6 +4,7 @@
|
||||
"Cancel": "Отменить",
|
||||
"Ok": "Ок",
|
||||
"Save": "Сохранить",
|
||||
"Saved": "Сохранено...",
|
||||
"Download": "Скачать",
|
||||
"Delete": "Удалить",
|
||||
"Close": "Закрыть",
|
||||
|
@ -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,
|
||||
|
@ -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)
|
||||
]
|
||||
}
|
||||
})
|
||||
|
@ -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>
|
||||
|
@ -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}
|
||||
|
@ -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
|
||||
|
@ -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) => {
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
292
packages/theme/styles/button.scss
Normal file
292
packages/theme/styles/button.scss
Normal 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;
|
||||
}
|
||||
}
|
@ -22,6 +22,7 @@
|
||||
@import "./mixins.scss";
|
||||
@import "./panel.scss";
|
||||
@import "./prose.scss";
|
||||
@import "./button.scss";
|
||||
|
||||
@font-face {
|
||||
font-family: 'IBM Plex Sans';
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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}
|
@ -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()
|
||||
|
@ -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,
|
||||
|
@ -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]
|
||||
|
@ -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>
|
||||
|
@ -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()
|
||||
|
@ -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>
|
||||
|
||||
|
@ -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}
|
||||
|
@ -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')}
|
||||
|
@ -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>
|
||||
|
@ -62,6 +62,7 @@
|
||||
bind:content={object.description}
|
||||
placeholder={tracker.string.ComponentDescriptionPlaceholder}
|
||||
emphasized
|
||||
showButtons={false}
|
||||
/>
|
||||
<svelte:fragment slot="pool">
|
||||
<EmployeeBox
|
||||
|
@ -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}
|
||||
|
@ -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>
|
||||
|
@ -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
|
||||
|
@ -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'}
|
||||
|
@ -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}
|
||||
|
@ -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}
|
||||
/>
|
||||
|
@ -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) {
|
||||
|
@ -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' })
|
||||
|
@ -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()
|
||||
|
@ -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}")`)
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user