mirror of
https://github.com/hcengineering/platform.git
synced 2025-04-19 23:00:13 +00:00
UBER-963: Related issues (#3773)
Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
parent
152c858c1d
commit
b2435326fd
@ -213,6 +213,9 @@ export function createModel (builder: Builder): void {
|
||||
{
|
||||
key: '',
|
||||
presenter: tracker.component.RelatedIssueSelector,
|
||||
props: {
|
||||
kind: 'link'
|
||||
},
|
||||
label: tracker.string.Relations
|
||||
},
|
||||
'comments',
|
||||
@ -252,6 +255,9 @@ export function createModel (builder: Builder): void {
|
||||
{
|
||||
key: '',
|
||||
presenter: tracker.component.RelatedIssueSelector,
|
||||
props: {
|
||||
kind: 'link'
|
||||
},
|
||||
label: tracker.string.Issues
|
||||
},
|
||||
'status',
|
||||
@ -551,6 +557,20 @@ export function createModel (builder: Builder): void {
|
||||
filters: ['_class']
|
||||
})
|
||||
|
||||
builder.mixin(lead.mixin.Customer, core.class.Class, view.mixin.ObjectEditorFooter, {
|
||||
editor: tracker.component.RelatedIssuesSection,
|
||||
props: {
|
||||
label: tracker.string.RelatedIssues
|
||||
}
|
||||
})
|
||||
|
||||
builder.mixin(lead.class.Lead, core.class.Class, view.mixin.ObjectEditorFooter, {
|
||||
editor: tracker.component.RelatedIssuesSection,
|
||||
props: {
|
||||
label: tracker.string.RelatedIssues
|
||||
}
|
||||
})
|
||||
|
||||
createAction(builder, {
|
||||
action: workbench.actionImpl.Navigate,
|
||||
actionProps: {
|
||||
|
@ -407,6 +407,9 @@ export function createModel (builder: Builder): void {
|
||||
{
|
||||
key: '',
|
||||
presenter: tracker.component.RelatedIssueSelector,
|
||||
props: {
|
||||
kind: 'link'
|
||||
},
|
||||
label: tracker.string.Relations
|
||||
},
|
||||
'comments',
|
||||
@ -487,6 +490,14 @@ export function createModel (builder: Builder): void {
|
||||
label: recruit.string.Applications
|
||||
},
|
||||
'comments',
|
||||
{
|
||||
key: '',
|
||||
presenter: tracker.component.RelatedIssueSelector,
|
||||
props: {
|
||||
kind: 'link'
|
||||
},
|
||||
label: tracker.string.Issues
|
||||
},
|
||||
'$lookup.company',
|
||||
'$lookup.company.$lookup.channels',
|
||||
'location',
|
||||
@ -523,6 +534,12 @@ export function createModel (builder: Builder): void {
|
||||
label: recruit.string.Applications
|
||||
},
|
||||
'comments',
|
||||
{
|
||||
key: '',
|
||||
presenter: tracker.component.RelatedIssueSelector,
|
||||
label: tracker.string.Issues,
|
||||
props: { size: 'small', kind: 'link' }
|
||||
},
|
||||
'$lookup.channels',
|
||||
{
|
||||
key: '@applications.modifiedOn',
|
||||
@ -558,6 +575,9 @@ export function createModel (builder: Builder): void {
|
||||
{
|
||||
key: '',
|
||||
presenter: tracker.component.RelatedIssueSelector,
|
||||
props: {
|
||||
kind: 'link'
|
||||
},
|
||||
label: tracker.string.Issues
|
||||
},
|
||||
'status',
|
||||
@ -606,6 +626,9 @@ export function createModel (builder: Builder): void {
|
||||
{
|
||||
key: '',
|
||||
presenter: tracker.component.RelatedIssueSelector,
|
||||
props: {
|
||||
kind: 'link'
|
||||
},
|
||||
label: tracker.string.Issues
|
||||
},
|
||||
'status',
|
||||
@ -864,6 +887,12 @@ export function createModel (builder: Builder): void {
|
||||
props: { kind: 'list', size: 'small', shouldShowName: false }
|
||||
},
|
||||
{ key: 'comments', displayProps: { key: 'comments', suffix: true } },
|
||||
{
|
||||
key: '',
|
||||
presenter: tracker.component.RelatedIssueSelector,
|
||||
label: tracker.string.Issues,
|
||||
props: { size: 'small' }
|
||||
},
|
||||
{
|
||||
key: '$lookup.channels',
|
||||
label: contact.string.ContactInfo,
|
||||
@ -914,6 +943,11 @@ export function createModel (builder: Builder): void {
|
||||
},
|
||||
'description',
|
||||
{ key: 'comments', displayProps: { key: 'comments', suffix: true } },
|
||||
{
|
||||
key: '',
|
||||
presenter: tracker.component.RelatedIssueSelector,
|
||||
label: tracker.string.Issues
|
||||
},
|
||||
{ key: '', displayProps: { grow: true } },
|
||||
{
|
||||
key: '$lookup.company',
|
||||
@ -1601,19 +1635,19 @@ export function createModel (builder: Builder): void {
|
||||
builder.mixin(recruit.mixin.Candidate, core.class.Class, view.mixin.ObjectEditorFooter, {
|
||||
editor: tracker.component.RelatedIssuesSection,
|
||||
props: {
|
||||
label: recruit.string.RelatedIssues
|
||||
label: tracker.string.RelatedIssues
|
||||
}
|
||||
})
|
||||
builder.mixin(recruit.class.Vacancy, core.class.Class, view.mixin.ObjectEditorFooter, {
|
||||
editor: tracker.component.RelatedIssuesSection,
|
||||
props: {
|
||||
label: recruit.string.RelatedIssues
|
||||
label: tracker.string.RelatedIssues
|
||||
}
|
||||
})
|
||||
builder.mixin(recruit.class.Applicant, core.class.Class, view.mixin.ObjectEditorFooter, {
|
||||
editor: tracker.component.RelatedIssuesSection,
|
||||
props: {
|
||||
label: recruit.string.RelatedIssues
|
||||
label: tracker.string.RelatedIssues
|
||||
}
|
||||
})
|
||||
|
||||
|
@ -61,14 +61,14 @@
|
||||
|
||||
function update (stateObjects: Item[], limit: number | undefined, index: number): void {
|
||||
clearTimeout(loadingTimeout)
|
||||
if (limitedObjects.length > 0 || index * 2 === 0) {
|
||||
if (limitedObjects.length > 0 || index === 0) {
|
||||
limitedObjects = stateObjects.slice(0, limit)
|
||||
} else {
|
||||
loading = true
|
||||
loadingTimeout = setTimeout(() => {
|
||||
limitedObjects = stateObjects.slice(0, limit)
|
||||
loading = false
|
||||
}, index * 2)
|
||||
}, index)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -22,11 +22,18 @@
|
||||
min-width: 12.5rem;
|
||||
max-width: 17rem;
|
||||
max-height: 22rem;
|
||||
|
||||
background: var(--theme-popup-color);
|
||||
border: 1px solid var(--theme-popup-divider);
|
||||
border-radius: .5rem;
|
||||
box-shadow: var(--theme-popup-shadow);
|
||||
|
||||
&.noShadow {
|
||||
background: none;
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
&.full-width {
|
||||
flex-grow: 1;
|
||||
background: none;
|
||||
|
@ -2,27 +2,28 @@
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
import { lazyObserver } from '../lazy'
|
||||
import { lazyObserver, isLazyEnabled } from '../lazy'
|
||||
|
||||
let visible = false
|
||||
let visible = !isLazyEnabled()
|
||||
</script>
|
||||
|
||||
{#if !visible}
|
||||
<div
|
||||
use:lazyObserver={(val) => {
|
||||
use:lazyObserver={(val, unsubscribe) => {
|
||||
if (val) {
|
||||
visible = true
|
||||
dispatch('visible')
|
||||
unsubscribe?.()
|
||||
}
|
||||
}}
|
||||
>
|
||||
{#if visible}
|
||||
<slot />
|
||||
{:else}
|
||||
<!-- Zero-width space character -->
|
||||
{#if $$slots.loading}
|
||||
<slot name="loading" />
|
||||
{:else}
|
||||
​
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
{:else}
|
||||
<slot />
|
||||
{/if}
|
||||
|
@ -22,6 +22,7 @@
|
||||
import IconUpOutline from './icons/UpOutline.svelte'
|
||||
import IconDownOutline from './icons/DownOutline.svelte'
|
||||
import HalfUpDown from './icons/HalfUpDown.svelte'
|
||||
import { DelayedCaller } from '../utils'
|
||||
|
||||
export let padding: string | undefined = undefined
|
||||
export let autoscroll: boolean = false
|
||||
@ -249,20 +250,16 @@
|
||||
}
|
||||
}
|
||||
|
||||
let checkBarTimeout: any | undefined = undefined
|
||||
let checkHBarTimeout: any | undefined = undefined
|
||||
const delayedCaller = new DelayedCaller(25)
|
||||
|
||||
const delayCall = (op: () => void, h?: boolean) => {
|
||||
if (h) {
|
||||
clearTimeout(checkHBarTimeout)
|
||||
checkHBarTimeout = setTimeout(op, 5)
|
||||
} else {
|
||||
clearTimeout(checkBarTimeout)
|
||||
checkBarTimeout = setTimeout(op, 5)
|
||||
}
|
||||
const delayCall = (op: () => void) => {
|
||||
delayedCaller.call(op)
|
||||
}
|
||||
|
||||
const checkFade = (): void => {
|
||||
delayCall(_checkFade)
|
||||
}
|
||||
const _checkFade = (): void => {
|
||||
if (divScroll) {
|
||||
beforeContent = divScroll.scrollTop
|
||||
belowContent = divScroll.scrollHeight - divScroll.clientHeight - beforeContent
|
||||
@ -279,11 +276,18 @@
|
||||
else if (rightContent > 2) maskH = 'left'
|
||||
else maskH = 'none'
|
||||
}
|
||||
if (inter.size) checkIntersectionFade()
|
||||
if (inter.size) {
|
||||
checkIntersectionFade()
|
||||
}
|
||||
renderFade()
|
||||
}
|
||||
if (!isScrolling) delayCall(checkBar)
|
||||
if (!isScrolling && horizontal) delayCall(checkBarH, true)
|
||||
|
||||
if (!isScrolling) {
|
||||
checkBar()
|
||||
}
|
||||
if (!isScrolling && horizontal) {
|
||||
checkBarH()
|
||||
}
|
||||
}
|
||||
|
||||
function checkAutoScroll () {
|
||||
@ -383,8 +387,12 @@
|
||||
if (divScroll && divBox) {
|
||||
divScroll.addEventListener('wheel', wheelEvent)
|
||||
divScroll.addEventListener('scroll', checkFade)
|
||||
delayCall(checkBar)
|
||||
if (horizontal) delayCall(checkBarH, true)
|
||||
delayCall(() => {
|
||||
checkBar()
|
||||
if (horizontal) {
|
||||
checkBarH()
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
onDestroy(() => {
|
||||
|
@ -50,6 +50,8 @@
|
||||
export let value: Array<ValueType>
|
||||
export let width: 'medium' | 'large' | 'full' = 'medium'
|
||||
export let size: 'small' | 'medium' | 'large' = 'small'
|
||||
export let onSelect: ((value: ValueType['id']) => void) | undefined = undefined
|
||||
export let showShadow: boolean = true
|
||||
|
||||
let search: string = ''
|
||||
|
||||
@ -60,6 +62,14 @@
|
||||
let selection = 0
|
||||
let list: ListView
|
||||
|
||||
function sendSelect (id: ValueType['id']): void {
|
||||
if (onSelect) {
|
||||
onSelect(id)
|
||||
} else {
|
||||
dispatch('close', id)
|
||||
}
|
||||
}
|
||||
|
||||
function onKeydown (key: KeyboardEvent): void {
|
||||
if (key.code === 'ArrowUp') {
|
||||
key.stopPropagation()
|
||||
@ -74,7 +84,7 @@
|
||||
if (key.code === 'Enter') {
|
||||
key.preventDefault()
|
||||
key.stopPropagation()
|
||||
dispatch('close', value[selection].id)
|
||||
sendSelect(value[selection].id)
|
||||
}
|
||||
}
|
||||
const manager = createFocusManager()
|
||||
@ -88,6 +98,7 @@
|
||||
|
||||
<div
|
||||
class="selectPopup"
|
||||
class:noShadow={showShadow === false}
|
||||
class:full-width={width === 'full'}
|
||||
class:max-width-40={width === 'large'}
|
||||
use:resizeObserver={() => {
|
||||
@ -121,7 +132,7 @@
|
||||
>
|
||||
<svelte:fragment slot="item" let:item={itemId}>
|
||||
{@const item = filteredObjects[itemId]}
|
||||
<button class="menu-item withList w-full" on:click={() => dispatch('close', item.id)}>
|
||||
<button class="menu-item withList w-full" on:click={() => sendSelect(item.id)}>
|
||||
<div class="flex-row-center flex-grow pointer-events-none">
|
||||
{#if item.component}
|
||||
<div class="flex-grow clear-mins"><svelte:component this={item.component} {...item.props} /></div>
|
||||
|
@ -24,7 +24,7 @@
|
||||
let tooltipHTML: HTMLElement
|
||||
let nubHTML: HTMLElement
|
||||
let dir: TooltipAlignment
|
||||
let rect: DOMRect
|
||||
let rect: DOMRect | undefined
|
||||
let rectAnchor: DOMRect
|
||||
let tooltipSW: boolean // tooltipSW = true - Label; false - Component
|
||||
let nubDirection: 'top' | 'bottom' | 'left' | 'right' | undefined = undefined
|
||||
@ -37,18 +37,61 @@
|
||||
$: onUpdate = $tooltip.onUpdate
|
||||
$: kind = $tooltip.kind
|
||||
|
||||
const clearStyles = (): void => {
|
||||
shown = false
|
||||
tooltipHTML.style.top =
|
||||
tooltipHTML.style.bottom =
|
||||
tooltipHTML.style.left =
|
||||
tooltipHTML.style.right =
|
||||
tooltipHTML.style.height =
|
||||
''
|
||||
interface TooltipOptions {
|
||||
top: string
|
||||
bottom: string
|
||||
left: string
|
||||
right: string
|
||||
width: string
|
||||
height: string
|
||||
transform: string
|
||||
visibility: string
|
||||
classList: string
|
||||
}
|
||||
|
||||
const fitTooltip = (tooltipHTMLToCheck: HTMLElement): void => {
|
||||
let options: TooltipOptions = {
|
||||
top: '',
|
||||
bottom: '',
|
||||
left: '',
|
||||
right: '',
|
||||
width: '',
|
||||
height: '',
|
||||
transform: '',
|
||||
visibility: 'hidden',
|
||||
classList: ''
|
||||
}
|
||||
|
||||
const clearStyles = (): void => {
|
||||
shown = false
|
||||
options = {
|
||||
top: '',
|
||||
bottom: '',
|
||||
left: '',
|
||||
right: '',
|
||||
width: '',
|
||||
height: '',
|
||||
transform: '',
|
||||
visibility: 'hidden',
|
||||
classList: ''
|
||||
}
|
||||
}
|
||||
|
||||
const fitTooltip = (tooltipHTMLToCheck: HTMLElement, clWidth?: number): TooltipOptions => {
|
||||
const options: TooltipOptions = {
|
||||
top: '',
|
||||
bottom: '',
|
||||
left: '',
|
||||
right: '',
|
||||
width: '',
|
||||
height: '',
|
||||
transform: '',
|
||||
visibility: 'visible',
|
||||
classList: ''
|
||||
}
|
||||
if (($tooltip.label || $tooltip.component) && tooltipHTML && tooltipHTMLToCheck) {
|
||||
if (clWidth === undefined) {
|
||||
clWidth = tooltipHTML.clientWidth
|
||||
}
|
||||
if ($tooltip.element) {
|
||||
rect = $tooltip.element.getBoundingClientRect()
|
||||
rectAnchor = $tooltip.anchor
|
||||
@ -58,28 +101,28 @@
|
||||
if ($tooltip.component) {
|
||||
clearStyles()
|
||||
if (rect.bottom + tooltipHTMLToCheck.clientHeight + 28 < docHeight) {
|
||||
tooltipHTML.style.top = `calc(${rect.bottom}px + 5px + .25rem)`
|
||||
options.top = `calc(${rect.bottom}px + 5px + .25rem)`
|
||||
dir = 'bottom'
|
||||
} else if (rect.top > docHeight - rect.bottom) {
|
||||
tooltipHTML.style.bottom = `calc(${docHeight - rect.y}px + 5px + .25rem)`
|
||||
options.bottom = `calc(${docHeight - rect.y}px + 5px + .25rem)`
|
||||
if (tooltipHTML.clientHeight > rect.top - 28) {
|
||||
tooltipHTML.style.top = '1rem'
|
||||
tooltipHTML.style.height = `calc(${rect.top}px - 5px - 1.25rem)`
|
||||
options.top = '1rem'
|
||||
options.height = `calc(${rect.top}px - 5px - 1.25rem)`
|
||||
}
|
||||
dir = 'top'
|
||||
} else {
|
||||
tooltipHTML.style.top = `calc(${rect.bottom}px + 5px + .25rem)`
|
||||
options.top = `calc(${rect.bottom}px + 5px + .25rem)`
|
||||
if (tooltipHTMLToCheck.clientHeight > docHeight - rect.bottom - 28) {
|
||||
tooltipHTML.style.bottom = '1rem'
|
||||
tooltipHTML.style.height = `calc(${docHeight - rect.bottom}px - 5px - 1.25rem)`
|
||||
options.bottom = '1rem'
|
||||
options.height = `calc(${docHeight - rect.bottom}px - 5px - 1.25rem)`
|
||||
}
|
||||
dir = 'bottom'
|
||||
}
|
||||
|
||||
const tempLeft = rect.width / 2 + rect.left - clWidth / 2
|
||||
if (tempLeft + clWidth > docWidth - 8) tooltipHTML.style.right = '.5rem'
|
||||
else if (tempLeft < 8) tooltipHTML.style.left = '.5rem'
|
||||
else tooltipHTML.style.left = `${tempLeft}px`
|
||||
if (tempLeft + clWidth > docWidth - 8) options.right = '.5rem'
|
||||
else if (tempLeft < 8) options.left = '.5rem'
|
||||
else options.left = `${tempLeft}px`
|
||||
|
||||
if (nubHTML) {
|
||||
nubHTML.style.top = rect.top + 'px'
|
||||
@ -97,43 +140,53 @@
|
||||
} else dir = $tooltip.direction
|
||||
|
||||
if (dir === 'right') {
|
||||
tooltipHTML.style.top = rectAnchor.y + rectAnchor.height / 2 + 'px'
|
||||
tooltipHTML.style.left = `calc(${rectAnchor.right}px + .75rem)`
|
||||
tooltipHTML.style.transform = 'translateY(-50%)'
|
||||
options.top = rectAnchor.y + rectAnchor.height / 2 + 'px'
|
||||
options.left = `calc(${rectAnchor.right}px + .75rem)`
|
||||
options.transform = 'translateY(-50%)'
|
||||
} else if (dir === 'left') {
|
||||
tooltipHTML.style.top = rectAnchor.y + rectAnchor.height / 2 + 'px'
|
||||
tooltipHTML.style.right = `calc(${docWidth - rectAnchor.x}px + .75rem)`
|
||||
tooltipHTML.style.transform = 'translateY(-50%)'
|
||||
options.top = rectAnchor.y + rectAnchor.height / 2 + 'px'
|
||||
options.right = `calc(${docWidth - rectAnchor.x}px + .75rem)`
|
||||
options.transform = 'translateY(-50%)'
|
||||
} else if (dir === 'bottom') {
|
||||
tooltipHTML.style.top = `calc(${rectAnchor.bottom}px + .5rem)`
|
||||
tooltipHTML.style.left = rectAnchor.x + rectAnchor.width / 2 + 'px'
|
||||
tooltipHTML.style.transform = 'translateX(-50%)'
|
||||
options.top = `calc(${rectAnchor.bottom}px + .5rem)`
|
||||
options.left = rectAnchor.x + rectAnchor.width / 2 + 'px'
|
||||
options.transform = 'translateX(-50%)'
|
||||
} else if (dir === 'top') {
|
||||
tooltipHTML.style.bottom = `calc(${docHeight - rectAnchor.y}px + .75rem)`
|
||||
tooltipHTML.style.left = rectAnchor.x + rectAnchor.width / 2 + 'px'
|
||||
tooltipHTML.style.transform = 'translateX(-50%)'
|
||||
options.bottom = `calc(${docHeight - rectAnchor.y}px + .75rem)`
|
||||
options.left = rectAnchor.x + rectAnchor.width / 2 + 'px'
|
||||
options.transform = 'translateX(-50%)'
|
||||
}
|
||||
tooltipHTML.classList.remove('no-arrow')
|
||||
}
|
||||
} else {
|
||||
tooltipHTML.style.top = '50%'
|
||||
tooltipHTML.style.left = '50%'
|
||||
tooltipHTML.style.width = 'min-content'
|
||||
tooltipHTML.style.height = 'min-content'
|
||||
tooltipHTML.style.transform = 'translate(-50%, -50%)'
|
||||
tooltipHTML.classList.add('no-arrow')
|
||||
options.top = '50%'
|
||||
options.left = '50%'
|
||||
options.width = 'min-content'
|
||||
options.height = 'min-content'
|
||||
options.transform = 'translate(-50%, -50%)'
|
||||
options.classList = 'no-arrow'
|
||||
}
|
||||
tooltipHTML.style.visibility = 'visible'
|
||||
options.visibility = 'visible'
|
||||
shown = true
|
||||
} else if (tooltipHTML) {
|
||||
shown = false
|
||||
tooltipHTML.style.visibility = 'hidden'
|
||||
options.visibility = 'hidden'
|
||||
}
|
||||
return options
|
||||
}
|
||||
|
||||
const fitSubmenu = (): void => {
|
||||
const fitSubmenu = (): TooltipOptions => {
|
||||
const options: TooltipOptions = {
|
||||
top: '',
|
||||
bottom: '',
|
||||
left: '',
|
||||
right: '',
|
||||
width: '',
|
||||
height: '',
|
||||
visibility: 'visible',
|
||||
transform: '',
|
||||
classList: ''
|
||||
}
|
||||
if (($tooltip.label || $tooltip.component) && tooltipHTML) {
|
||||
clearStyles()
|
||||
if ($tooltip.element) {
|
||||
rect = $tooltip.element.getBoundingClientRect()
|
||||
const rectP = tooltipHTML.getBoundingClientRect()
|
||||
@ -145,23 +198,33 @@
|
||||
: rect.bottom > docHeight - rect.top
|
||||
? 'top'
|
||||
: 'bottom'
|
||||
if (dirH === 'right') tooltipHTML.style.left = rect.right - 4 + 'px'
|
||||
else tooltipHTML.style.right = docWidth - rect.left - 4 + 'px'
|
||||
if (dirV === 'bottom') tooltipHTML.style.top = rect.top - 4 + 'px'
|
||||
else tooltipHTML.style.bottom = docHeight - rect.bottom - 4 + 'px'
|
||||
tooltipHTML.style.visibility = 'visible'
|
||||
if (dirH === 'right') {
|
||||
options.left = rect.right - 4 + 'px'
|
||||
} else {
|
||||
options.right = docWidth - rect.left - 4 + 'px'
|
||||
}
|
||||
} else if (tooltipHTML) tooltipHTML.style.visibility = 'hidden'
|
||||
if (dirV === 'bottom') {
|
||||
options.top = rect.top - 4 + 'px'
|
||||
} else {
|
||||
options.bottom = docHeight - rect.bottom - 4 + 'px'
|
||||
}
|
||||
options.visibility = 'visible'
|
||||
}
|
||||
} else if (tooltipHTML) {
|
||||
options.visibility = 'hidden'
|
||||
}
|
||||
return options
|
||||
}
|
||||
|
||||
const hideTooltip = (): void => {
|
||||
if (tooltipHTML) tooltipHTML.style.visibility = 'hidden'
|
||||
if (tooltipHTML) options.visibility = 'hidden'
|
||||
closeTooltip()
|
||||
}
|
||||
|
||||
const whileShow = (ev: MouseEvent): void => {
|
||||
if ($tooltip.element && tooltipHTML) {
|
||||
const rectP = tooltipHTML.getBoundingClientRect()
|
||||
rect = $tooltip.element.getBoundingClientRect()
|
||||
const dT: number = dir === 'bottom' && $tooltip.kind !== 'submenu' ? 12 : 0
|
||||
const dB: number = dir === 'top' && $tooltip.kind !== 'submenu' ? 12 : 0
|
||||
const inTrigger: boolean = ev.x >= rect.left && ev.x <= rect.right && ev.y >= rect.top && ev.y <= rect.bottom
|
||||
@ -174,8 +237,18 @@
|
||||
}
|
||||
}
|
||||
|
||||
$: kind === 'submenu' ? fitSubmenu() : fitTooltip(tooltipHTML)
|
||||
afterUpdate(() => (kind === 'submenu' ? fitSubmenu() : fitTooltip(tooltipHTML)))
|
||||
$: if (kind === 'submenu') {
|
||||
options = fitSubmenu()
|
||||
} else {
|
||||
options = fitTooltip(tooltipHTML, clWidth)
|
||||
}
|
||||
afterUpdate(() => {
|
||||
if (kind === 'submenu') {
|
||||
options = fitSubmenu()
|
||||
} else {
|
||||
options = fitTooltip(tooltipHTML, clWidth)
|
||||
}
|
||||
})
|
||||
onDestroy(() => hideTooltip())
|
||||
</script>
|
||||
|
||||
@ -208,13 +281,20 @@
|
||||
/>
|
||||
{#if $tooltip.component && $tooltip.kind !== 'submenu'}
|
||||
<div
|
||||
class="popup-tooltip"
|
||||
class="popup-tooltip {options.classList}"
|
||||
class:shown
|
||||
class:doublePadding={$tooltip.label}
|
||||
use:resizeObserver={(element) => {
|
||||
clWidth = element.clientWidth
|
||||
fitTooltip(tooltipHTML)
|
||||
options = fitTooltip(tooltipHTML, clWidth)
|
||||
}}
|
||||
style:top={options.top}
|
||||
style:bottom={options.bottom}
|
||||
style:left={options.left}
|
||||
style:right={options.right}
|
||||
style:width={options.width}
|
||||
style:height={options.height}
|
||||
style:transform={options.transform}
|
||||
bind:this={tooltipHTML}
|
||||
>
|
||||
{#if $tooltip.label}
|
||||
@ -241,7 +321,17 @@
|
||||
</div>
|
||||
<div bind:this={nubHTML} class="nub {nubDirection ?? ''}" class:shown />
|
||||
{:else if $tooltip.label && $tooltip.kind !== 'submenu'}
|
||||
<div class="tooltip {dir ?? ''}" bind:this={tooltipHTML}>
|
||||
<div
|
||||
class="tooltip {dir ?? ''} {options.classList}"
|
||||
bind:this={tooltipHTML}
|
||||
style:top={options.top}
|
||||
style:bottom={options.bottom}
|
||||
style:left={options.left}
|
||||
style:right={options.right}
|
||||
style:width={options.width}
|
||||
style:height={options.height}
|
||||
style:transform={options.transform}
|
||||
>
|
||||
<Label label={$tooltip.label} params={$tooltip.props ?? {}} />
|
||||
{#if $tooltip.keys !== undefined}
|
||||
<div class="keys">
|
||||
@ -265,10 +355,17 @@
|
||||
</div>
|
||||
{:else if $tooltip.kind === 'submenu'}
|
||||
<div
|
||||
class="submenu-container {dir ?? ''}"
|
||||
class="submenu-container {dir ?? ''} {options.classList}"
|
||||
use:resizeObserver={(element) => {
|
||||
clWidth = element.clientWidth
|
||||
}}
|
||||
style:top={options.top}
|
||||
style:bottom={options.bottom}
|
||||
style:left={options.left}
|
||||
style:right={options.right}
|
||||
style:width={options.width}
|
||||
style:height={options.height}
|
||||
style:transform={options.transform}
|
||||
bind:this={tooltipHTML}
|
||||
>
|
||||
{#if typeof $tooltip.component === 'string'}
|
||||
|
@ -1,32 +1,41 @@
|
||||
const observers = new Map<string, IntersectionObserver>()
|
||||
const entryMap = new WeakMap<Element, { callback: (entry: IntersectionObserverEntry) => void }>()
|
||||
import { DelayedCaller } from './utils'
|
||||
|
||||
const observers = new Map<string, IntersectionObserver>()
|
||||
const entryMap = new WeakMap<Element, { callback: (isIntersecting: boolean) => void }>()
|
||||
|
||||
const delayedCaller = new DelayedCaller(5)
|
||||
function makeObserver (rootMargin: string): IntersectionObserver {
|
||||
return new IntersectionObserver(
|
||||
(entries, observer) => {
|
||||
for (const entry of entries) {
|
||||
const entryData = entryMap.get(entry.target)
|
||||
const entriesPending = new Map<Element, { isIntersecting: boolean }>()
|
||||
const notifyObservers = (observer: IntersectionObserver): void => {
|
||||
console.log('notifyObservers', entriesPending.size)
|
||||
for (const [target, entry] of entriesPending.entries()) {
|
||||
const entryData = entryMap.get(target)
|
||||
if (entryData == null) {
|
||||
observer.unobserve(entry.target)
|
||||
observer.unobserve(target)
|
||||
continue
|
||||
}
|
||||
|
||||
entryData.callback(entry)
|
||||
entryData.callback(entry.isIntersecting)
|
||||
if (entry.isIntersecting) {
|
||||
entryMap.delete(entry.target)
|
||||
observer.unobserve(entry.target)
|
||||
entryMap.delete(target)
|
||||
observer.unobserve(target)
|
||||
}
|
||||
}
|
||||
entriesPending.clear()
|
||||
}
|
||||
const observer = new IntersectionObserver(
|
||||
(entries, observer) => {
|
||||
for (const entry of entries) {
|
||||
entriesPending.set(entry.target, { isIntersecting: entry.isIntersecting })
|
||||
}
|
||||
delayedCaller.call(() => notifyObservers(observer))
|
||||
},
|
||||
{ rootMargin }
|
||||
)
|
||||
return observer
|
||||
}
|
||||
|
||||
function listen (
|
||||
rootMargin: string,
|
||||
element: Element,
|
||||
callback: (entry: IntersectionObserverEntry) => void
|
||||
): () => void {
|
||||
function listen (rootMargin: string, element: Element, callback: (isIntersecting: boolean) => void): () => void {
|
||||
let observer = observers.get(rootMargin)
|
||||
if (observer == null) {
|
||||
observer = makeObserver(rootMargin)
|
||||
@ -41,9 +50,14 @@ function listen (
|
||||
}
|
||||
}
|
||||
|
||||
export function lazyObserver (node: Element, onVisible: (value: boolean) => void): any {
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export const isLazyEnabled = (): boolean => (localStorage.getItem('#platform.lazy.loading') ?? 'true') === 'true'
|
||||
|
||||
export function lazyObserver (node: Element, onVisible: (value: boolean, unsubscribe?: () => void) => void): any {
|
||||
let visible = false
|
||||
const lazyEnabled = (localStorage.getItem('#platform.lazy.loading') ?? 'true') === 'true'
|
||||
const lazyEnabled = isLazyEnabled()
|
||||
if (!lazyEnabled) {
|
||||
visible = true
|
||||
onVisible(visible)
|
||||
@ -53,9 +67,9 @@ export function lazyObserver (node: Element, onVisible: (value: boolean) => void
|
||||
return {}
|
||||
}
|
||||
|
||||
const destroy = listen('20%', node, ({ isIntersecting }) => {
|
||||
const destroy = listen('20%', node, (isIntersecting) => {
|
||||
visible = isIntersecting
|
||||
onVisible(visible)
|
||||
onVisible(visible, destroy)
|
||||
})
|
||||
|
||||
return {
|
||||
|
@ -9,25 +9,40 @@
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
|
||||
import { DelayedCaller } from './utils'
|
||||
|
||||
// limitations under the License.
|
||||
let observer: ResizeObserver
|
||||
let callbacks: WeakMap<Element, (element: Element) => any>
|
||||
|
||||
const delayedCaller = new DelayedCaller(10)
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export function resizeObserver (element: Element, onResize: (element: Element) => any): { destroy: () => void } {
|
||||
if (observer === undefined) {
|
||||
callbacks = new WeakMap()
|
||||
observer = new ResizeObserver((entries) => {
|
||||
const entriesPending = new Set<Element>()
|
||||
|
||||
const notifyObservers = (): void => {
|
||||
window.requestAnimationFrame(() => {
|
||||
for (const entry of entries) {
|
||||
const onResize = callbacks.get(entry.target)
|
||||
for (const target of entriesPending.values()) {
|
||||
const onResize = callbacks.get(target)
|
||||
if (onResize != null) {
|
||||
onResize(entry.target)
|
||||
onResize(target)
|
||||
}
|
||||
}
|
||||
entriesPending.clear()
|
||||
})
|
||||
}
|
||||
|
||||
observer = new ResizeObserver((entries) => {
|
||||
for (const entry of entries) {
|
||||
entriesPending.add(entry.target)
|
||||
}
|
||||
delayedCaller.call(notifyObservers)
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -238,3 +238,21 @@ export function formatKey (key: string): string[][] {
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export class DelayedCaller {
|
||||
op?: () => void
|
||||
constructor (readonly delay: number = 10) {}
|
||||
call (op: () => void): void {
|
||||
const needTimer = this.op === undefined
|
||||
this.op = op
|
||||
if (needTimer) {
|
||||
setTimeout(() => {
|
||||
this.op?.()
|
||||
this.op = undefined
|
||||
}, this.delay)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -96,7 +96,6 @@
|
||||
"HasActiveApplicant":"Active Only",
|
||||
"HasNoActiveApplicant": "No Active",
|
||||
"NoneApplications": "None",
|
||||
"RelatedIssues": "Related issues",
|
||||
"VacancyList": "Vacancies",
|
||||
"MatchVacancy": "Match to vacancy",
|
||||
"VacancyMatching": "Match Talents to vacancy",
|
||||
|
@ -98,7 +98,6 @@
|
||||
"HasActiveApplicant":"Только активные",
|
||||
"HasNoActiveApplicant": "Не активные",
|
||||
"NoneApplications": "Отсутствуют",
|
||||
"RelatedIssues": "Связанные задачи",
|
||||
"VacancyList": "Вакансии",
|
||||
"MatchVacancy": "Проверить на вакансию",
|
||||
"VacancyMatching": "Подбор кандидатов на вакансию",
|
||||
|
@ -199,7 +199,7 @@
|
||||
<VacancyApplications objectId={object._id} />
|
||||
</div>
|
||||
<div class="w-full mt-6">
|
||||
<Component is={tracker.component.RelatedIssuesSection} props={{ object, label: recruit.string.RelatedIssues }} />
|
||||
<Component is={tracker.component.RelatedIssuesSection} props={{ object, label: tracker.string.RelatedIssues }} />
|
||||
</div>
|
||||
</Panel>
|
||||
{/if}
|
||||
|
@ -177,7 +177,8 @@
|
||||
|
||||
function createConfig (
|
||||
descr: Viewlet | undefined,
|
||||
preference: ViewletPreference | undefined
|
||||
preference: ViewletPreference | undefined,
|
||||
replacedKeys: Map<string, BuildModelKey>
|
||||
): (string | BuildModelKey)[] {
|
||||
const base = preference?.config ?? descr?.config ?? []
|
||||
const result: (string | BuildModelKey)[] = []
|
||||
@ -191,7 +192,7 @@
|
||||
return result
|
||||
}
|
||||
|
||||
$: finalConfig = createConfig(viewlet, preference)
|
||||
$: finalConfig = createConfig(viewlet, preference, replacedKeys)
|
||||
</script>
|
||||
|
||||
<div class="ac-header full divide">
|
||||
|
@ -66,7 +66,7 @@
|
||||
(applications?.get(b._id as Ref<Vacancy>)?.modifiedOn ?? b.modifiedOn) -
|
||||
(applications?.get(a._id as Ref<Vacancy>)?.modifiedOn ?? a.modifiedOn)
|
||||
|
||||
const replacedKeys: Map<string, BuildModelKey> = new Map<string, BuildModelKey>([
|
||||
$: replacedKeys = new Map<string, BuildModelKey>([
|
||||
[
|
||||
'@applications',
|
||||
{
|
||||
@ -100,7 +100,8 @@
|
||||
function createConfig (
|
||||
descr: Viewlet,
|
||||
preference: ViewletPreference | undefined,
|
||||
applications: Map<Ref<Vacancy>, ApplicationInfo>
|
||||
applications: Map<Ref<Vacancy>, ApplicationInfo>,
|
||||
replacedKeys: Map<string, BuildModelKey>
|
||||
): (string | BuildModelKey)[] {
|
||||
const base = preference?.config ?? descr.config
|
||||
const result: (string | BuildModelKey)[] = []
|
||||
@ -181,7 +182,7 @@
|
||||
props={{
|
||||
_class: recruit.class.Vacancy,
|
||||
options: viewlet.options,
|
||||
config: createConfig(viewlet, preference, applications),
|
||||
config: createConfig(viewlet, preference, applications, replacedKeys),
|
||||
viewlet,
|
||||
viewOptions,
|
||||
viewOptionsConfig: viewlet.viewOptions?.other,
|
||||
|
@ -69,7 +69,7 @@
|
||||
<Icon icon={recruit.icon.Issue} size={'small'} />
|
||||
</div>
|
||||
<span class="antiSection-header__title">
|
||||
<Label label={recruit.string.RelatedIssues} />
|
||||
<Label label={tracker.string.RelatedIssues} />
|
||||
</span>
|
||||
<div class="buttons-group small-gap">
|
||||
<Button
|
||||
|
@ -116,7 +116,6 @@ export default mergeIds(recruitId, recruit, {
|
||||
HasActiveApplicant: '' as IntlString,
|
||||
HasNoActiveApplicant: '' as IntlString,
|
||||
NoneApplications: '' as IntlString,
|
||||
RelatedIssues: '' as IntlString,
|
||||
|
||||
MatchVacancy: '' as IntlString,
|
||||
VacancyMatching: '' as IntlString,
|
||||
|
@ -65,7 +65,7 @@ export function getStates (space: SpaceWithStates | undefined, statusStore: IdMa
|
||||
return []
|
||||
}
|
||||
|
||||
const states = space.states.map((x) => statusStore.get(x) as Status).filter((p) => p !== undefined)
|
||||
const states = (space.states ?? []).map((x) => statusStore.get(x) as Status).filter((p) => p !== undefined)
|
||||
|
||||
return states
|
||||
}
|
||||
|
@ -173,6 +173,7 @@
|
||||
"RelatedIssueSearchPlaceholder": "Search for issue to reference...",
|
||||
"Blocks": "Blocks",
|
||||
"Related": "Related",
|
||||
"RelatedIssues": "Related issues",
|
||||
|
||||
"EditIssue": "Edit {title}",
|
||||
"EditWorkflowStatuses": "Edit issue statuses",
|
||||
|
@ -173,6 +173,7 @@
|
||||
"RelatedIssueSearchPlaceholder": "Поиск связанной задачи...",
|
||||
"Blocks": "Блокирует",
|
||||
"Related": "Связан",
|
||||
"RelatedIssues": "Связанные задачи",
|
||||
|
||||
"EditIssue": "Редактирование {title}",
|
||||
"EditWorkflowStatuses": "Редактировать статусы задач",
|
||||
|
@ -13,8 +13,8 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import core, { SortingOrder, WithLookup } from '@hcengineering/core'
|
||||
import { createQuery } from '@hcengineering/presentation'
|
||||
import core, { IdMap, SortingOrder, StatusCategory, WithLookup, toIdMap } from '@hcengineering/core'
|
||||
import { createQuery, getClient } from '@hcengineering/presentation'
|
||||
import { Issue, IssueStatus } from '@hcengineering/tracker'
|
||||
import {
|
||||
Icon,
|
||||
@ -25,7 +25,6 @@
|
||||
closeTooltip,
|
||||
getPlatformColorDef,
|
||||
navigate,
|
||||
showPopup,
|
||||
themeStore,
|
||||
tooltip
|
||||
} from '@hcengineering/ui'
|
||||
@ -33,6 +32,7 @@
|
||||
import { getIssueId, issueLinkFragmentProvider } from '../../../issues'
|
||||
import tracker from '../../../plugin'
|
||||
import IssueStatusIcon from '../IssueStatusIcon.svelte'
|
||||
import { listIssueStatusOrder } from '../../../utils'
|
||||
|
||||
export let issue: WithLookup<Issue>
|
||||
|
||||
@ -56,49 +56,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
function showSubIssues () {
|
||||
if (subIssues) {
|
||||
closeTooltip()
|
||||
showPopup(
|
||||
SelectPopup,
|
||||
{
|
||||
value: subIssues.map((iss) => {
|
||||
const project = iss.$lookup?.space
|
||||
const status = iss.$lookup?.status as WithLookup<IssueStatus>
|
||||
const icon = status.$lookup?.category?.icon
|
||||
const color = status.color ?? status.$lookup?.category?.color
|
||||
|
||||
return {
|
||||
id: iss._id,
|
||||
icon,
|
||||
isSelected: iss._id === issue._id,
|
||||
...(project !== undefined ? { text: `${getIssueId(project, iss)} ${iss.title}` } : undefined),
|
||||
...(color !== undefined ? { iconColor: getPlatformColorDef(color, $themeStore.dark).icon } : undefined)
|
||||
}
|
||||
}),
|
||||
width: 'large'
|
||||
},
|
||||
{
|
||||
getBoundingClientRect: () => {
|
||||
const rect = subIssuesElement.getBoundingClientRect()
|
||||
const offsetX = 5
|
||||
const offsetY = -1
|
||||
|
||||
return DOMRect.fromRect({ width: 1, height: 1, x: rect.right + offsetX, y: rect.top + offsetY })
|
||||
}
|
||||
},
|
||||
(selectedIssue) => {
|
||||
if (selectedIssue !== undefined) {
|
||||
const issue = subIssues?.find((p) => p._id === selectedIssue)
|
||||
if (issue) {
|
||||
openIssue(issue)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
$: areSubIssuesLoading = !subIssues
|
||||
$: parentIssue = issue.$lookup?.attachedTo ? (issue.$lookup?.attachedTo as Issue) : null
|
||||
$: if (parentIssue && parentIssue.subIssues > 0) {
|
||||
@ -119,6 +76,52 @@
|
||||
}
|
||||
|
||||
$: parentStatus = parentIssue ? $statusStore.get(parentIssue.status) : undefined
|
||||
|
||||
let categories: IdMap<StatusCategory> = new Map()
|
||||
|
||||
getClient()
|
||||
.findAll(core.class.StatusCategory, {})
|
||||
.then((res) => {
|
||||
categories = toIdMap(res)
|
||||
})
|
||||
|
||||
let sortedSubIssues: WithLookup<Issue>[] = []
|
||||
|
||||
$: {
|
||||
if (subIssues !== undefined) {
|
||||
subIssues.sort(
|
||||
(a, b) =>
|
||||
listIssueStatusOrder.indexOf($statusStore.get(a.status)?.category ?? tracker.issueStatusCategory.Backlog) -
|
||||
listIssueStatusOrder.indexOf($statusStore.get(b.status)?.category ?? tracker.issueStatusCategory.Backlog)
|
||||
)
|
||||
sortedSubIssues = subIssues ?? []
|
||||
}
|
||||
}
|
||||
|
||||
$: subIssueValue = sortedSubIssues.map((iss) => {
|
||||
const project = iss.$lookup?.space
|
||||
const status = iss.$lookup?.status as WithLookup<IssueStatus>
|
||||
const icon = status.$lookup?.category?.icon
|
||||
const color = status.color ?? status.$lookup?.category?.color
|
||||
|
||||
const c = $statusStore.get(iss.status)?.category
|
||||
const category = c !== undefined ? categories.get(c) : undefined
|
||||
|
||||
return {
|
||||
id: iss._id,
|
||||
icon,
|
||||
isSelected: iss._id === issue._id,
|
||||
...(project !== undefined ? { text: `${getIssueId(project, iss)} ${iss.title}` } : undefined),
|
||||
...(color !== undefined ? { iconColor: getPlatformColorDef(color, $themeStore.dark).icon } : undefined),
|
||||
category:
|
||||
category !== undefined
|
||||
? {
|
||||
label: category.label,
|
||||
icon: category.icon
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
{#if parentIssue}
|
||||
@ -152,8 +155,15 @@
|
||||
<div
|
||||
bind:this={subIssuesElement}
|
||||
class="flex-center sub-issues cursor-pointer"
|
||||
use:tooltip={{ label: tracker.string.OpenSubIssues, direction: 'bottom' }}
|
||||
on:click|preventDefault={showSubIssues}
|
||||
use:tooltip={{
|
||||
component: SelectPopup,
|
||||
props: {
|
||||
value: subIssueValue,
|
||||
onSelect: openIssue,
|
||||
showShadow: false,
|
||||
width: 'large'
|
||||
}
|
||||
}}
|
||||
>
|
||||
<span class="overflow-label">{subIssues?.length}</span>
|
||||
<div class="ml-2">
|
||||
|
@ -13,23 +13,14 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { Ref, SortingOrder, Status, WithLookup } from '@hcengineering/core'
|
||||
import { createQuery } from '@hcengineering/presentation'
|
||||
import core, { IdMap, Ref, SortingOrder, Status, StatusCategory, WithLookup, toIdMap } from '@hcengineering/core'
|
||||
import { createQuery, getClient } from '@hcengineering/presentation'
|
||||
import { Issue, Project } from '@hcengineering/tracker'
|
||||
import {
|
||||
Button,
|
||||
ButtonKind,
|
||||
ButtonSize,
|
||||
ProgressCircle,
|
||||
SelectPopup,
|
||||
closeTooltip,
|
||||
showPanel,
|
||||
showPopup
|
||||
} from '@hcengineering/ui'
|
||||
import { Button, ButtonKind, ButtonSize, ProgressCircle, SelectPopup, showPanel } from '@hcengineering/ui'
|
||||
import { statusStore } from '@hcengineering/view-resources'
|
||||
import { getIssueId } from '../../../issues'
|
||||
import tracker from '../../../plugin'
|
||||
import { subIssueListProvider } from '../../../utils'
|
||||
import { listIssueStatusOrder, subIssueListProvider } from '../../../utils'
|
||||
import IssueStatusIcon from '../IssueStatusIcon.svelte'
|
||||
|
||||
export let value: WithLookup<Issue>
|
||||
@ -46,6 +37,7 @@
|
||||
$: project = currentProject
|
||||
|
||||
let subIssues: Issue[] = []
|
||||
let _subIssues: Issue[] = []
|
||||
let countComplete: number = 0
|
||||
|
||||
const projectQuery = createQuery()
|
||||
@ -64,11 +56,18 @@
|
||||
|
||||
$: update(value)
|
||||
|
||||
let categories: IdMap<StatusCategory> = new Map()
|
||||
|
||||
getClient()
|
||||
.findAll(core.class.StatusCategory, {})
|
||||
.then((res) => {
|
||||
categories = toIdMap(res)
|
||||
})
|
||||
|
||||
function update (value: WithLookup<Issue>): void {
|
||||
if (value.$lookup?.subIssues !== undefined) {
|
||||
query.unsubscribe()
|
||||
subIssues = value.$lookup.subIssues as Issue[]
|
||||
subIssues.sort((a, b) => (a.rank ?? '').localeCompare(b.rank ?? ''))
|
||||
} else if (value.subIssues > 0) {
|
||||
query.query(tracker.class.Issue, { attachedTo: value._id }, (res) => (subIssues = res), {
|
||||
sort: { rank: SortingOrder.Ascending }
|
||||
@ -101,14 +100,19 @@
|
||||
}
|
||||
}
|
||||
|
||||
function showSubIssues () {
|
||||
if (subIssues) {
|
||||
closeTooltip()
|
||||
showPopup(
|
||||
SelectPopup,
|
||||
{
|
||||
value: subIssues.map((iss) => {
|
||||
$: {
|
||||
subIssues.sort(
|
||||
(a, b) =>
|
||||
listIssueStatusOrder.indexOf($statusStore.get(a.status)?.category ?? tracker.issueStatusCategory.Backlog) -
|
||||
listIssueStatusOrder.indexOf($statusStore.get(b.status)?.category ?? tracker.issueStatusCategory.Backlog)
|
||||
)
|
||||
_subIssues = subIssues
|
||||
}
|
||||
|
||||
$: subIssuesValue = _subIssues.map((iss) => {
|
||||
const text = project ? `${getIssueId(project, iss)} ${iss.title}` : iss.title
|
||||
const c = $statusStore.get(iss.status)?.category
|
||||
const category = c !== undefined ? categories.get(c) : undefined
|
||||
return {
|
||||
id: iss._id,
|
||||
text,
|
||||
@ -118,26 +122,16 @@
|
||||
value: $statusStore.get(iss.status),
|
||||
size: 'small',
|
||||
fill: undefined
|
||||
}
|
||||
}
|
||||
}),
|
||||
width: 'large'
|
||||
},
|
||||
{
|
||||
getBoundingClientRect: () => {
|
||||
const rect = btn.getBoundingClientRect()
|
||||
const offsetX = 0
|
||||
const offsetY = 0
|
||||
|
||||
return DOMRect.fromRect({ width: 1, height: 1, x: rect.left + offsetX, y: rect.bottom + offsetY })
|
||||
}
|
||||
},
|
||||
(selectedIssue) => {
|
||||
selectedIssue !== undefined && openIssue(selectedIssue)
|
||||
}
|
||||
)
|
||||
category:
|
||||
category !== undefined
|
||||
? {
|
||||
label: category.label,
|
||||
icon: category.icon
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
{#if hasSubIssues}
|
||||
@ -147,9 +141,14 @@
|
||||
{kind}
|
||||
{size}
|
||||
{justify}
|
||||
on:click={(ev) => {
|
||||
ev.stopPropagation()
|
||||
if (subIssues) showSubIssues()
|
||||
showTooltip={{
|
||||
component: SelectPopup,
|
||||
props: {
|
||||
value: subIssuesValue,
|
||||
onSelect: openIssue,
|
||||
showShadow: false,
|
||||
width: 'large'
|
||||
}
|
||||
}}
|
||||
>
|
||||
<svelte:fragment slot="content">
|
||||
|
@ -13,22 +13,13 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { Doc, Ref, SortingOrder, WithLookup } from '@hcengineering/core'
|
||||
import { createQuery } from '@hcengineering/presentation'
|
||||
import core, { Doc, IdMap, Ref, SortingOrder, StatusCategory, WithLookup, toIdMap } from '@hcengineering/core'
|
||||
import { createQuery, getClient } from '@hcengineering/presentation'
|
||||
import { Issue, Project } from '@hcengineering/tracker'
|
||||
import {
|
||||
Button,
|
||||
ButtonKind,
|
||||
ButtonSize,
|
||||
ProgressCircle,
|
||||
SelectPopup,
|
||||
closeTooltip,
|
||||
showPanel,
|
||||
showPopup
|
||||
} from '@hcengineering/ui'
|
||||
import { Button, ButtonKind, ButtonSize, ProgressCircle, SelectPopup, showPanel } from '@hcengineering/ui'
|
||||
import { statusStore } from '@hcengineering/view-resources'
|
||||
import tracker from '../../../plugin'
|
||||
import { subIssueListProvider } from '../../../utils'
|
||||
import { listIssueStatusOrder, subIssueListProvider } from '../../../utils'
|
||||
import RelatedIssuePresenter from './RelatedIssuePresenter.svelte'
|
||||
|
||||
export let object: WithLookup<Doc & { related: number }> | undefined
|
||||
@ -41,8 +32,7 @@
|
||||
export let width: string | undefined = 'min-contet'
|
||||
export let compactMode: boolean = false
|
||||
|
||||
let btn: HTMLElement
|
||||
|
||||
let _subIssues: Issue[] = []
|
||||
let subIssues: Issue[] = []
|
||||
let countComplete: number = 0
|
||||
|
||||
@ -55,13 +45,12 @@
|
||||
function update (value: WithLookup<Doc & { related: number }>): void {
|
||||
if (value.$lookup?.related !== undefined) {
|
||||
query.unsubscribe()
|
||||
subIssues = value.$lookup.related as Issue[]
|
||||
subIssues.sort((a, b) => (a.rank ?? '').localeCompare(b.rank ?? ''))
|
||||
_subIssues = value.$lookup.related as Issue[]
|
||||
} else {
|
||||
query.query(
|
||||
tracker.class.Issue,
|
||||
{ 'relations._id': value._id, 'relations._class': value._class },
|
||||
(res) => (subIssues = res),
|
||||
(res) => (_subIssues = res),
|
||||
{
|
||||
sort: { rank: SortingOrder.Ascending }
|
||||
}
|
||||
@ -69,9 +58,29 @@
|
||||
}
|
||||
}
|
||||
|
||||
let categories: IdMap<StatusCategory> = new Map()
|
||||
|
||||
getClient()
|
||||
.findAll(core.class.StatusCategory, {})
|
||||
.then((res) => {
|
||||
categories = toIdMap(res)
|
||||
})
|
||||
|
||||
$: {
|
||||
_subIssues.sort(
|
||||
(a, b) =>
|
||||
listIssueStatusOrder.indexOf($statusStore.get(a.status)?.category ?? tracker.issueStatusCategory.Backlog) -
|
||||
listIssueStatusOrder.indexOf($statusStore.get(b.status)?.category ?? tracker.issueStatusCategory.Backlog)
|
||||
)
|
||||
subIssues = _subIssues
|
||||
}
|
||||
|
||||
$: if (subIssues) {
|
||||
const doneStatuses = Array.from($statusStore.values())
|
||||
.filter((s) => s.category === tracker.issueStatusCategory.Completed)
|
||||
.filter(
|
||||
(s) =>
|
||||
s.category === tracker.issueStatusCategory.Completed || s.category === tracker.issueStatusCategory.Canceled
|
||||
)
|
||||
.map((p) => p._id)
|
||||
countComplete = subIssues.filter((si) => doneStatuses.includes(si.status)).length
|
||||
}
|
||||
@ -82,49 +91,40 @@
|
||||
showPanel(tracker.component.EditIssue, target, tracker.class.Issue, 'content')
|
||||
}
|
||||
|
||||
function showSubIssues () {
|
||||
if (subIssues) {
|
||||
closeTooltip()
|
||||
showPopup(
|
||||
SelectPopup,
|
||||
{
|
||||
value: subIssues.map((iss) => {
|
||||
$: selectValue = subIssues.map((iss) => {
|
||||
const c = $statusStore.get(iss.status)?.category
|
||||
const category = c !== undefined ? categories.get(c) : undefined
|
||||
return {
|
||||
id: iss._id,
|
||||
isSelected: false,
|
||||
component: RelatedIssuePresenter,
|
||||
props: { project: currentProject, issue: iss }
|
||||
}
|
||||
}),
|
||||
width: 'large'
|
||||
},
|
||||
{
|
||||
getBoundingClientRect: () => {
|
||||
const rect = btn.getBoundingClientRect()
|
||||
const offsetX = 0
|
||||
const offsetY = 0
|
||||
|
||||
return DOMRect.fromRect({ width: 1, height: 1, x: rect.left + offsetX, y: rect.bottom + offsetY })
|
||||
}
|
||||
},
|
||||
(selectedIssue) => {
|
||||
selectedIssue !== undefined && openIssue(selectedIssue)
|
||||
}
|
||||
)
|
||||
props: { project: currentProject, issue: iss },
|
||||
category:
|
||||
category !== undefined
|
||||
? {
|
||||
label: category.label,
|
||||
icon: category.icon
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
{#if hasSubIssues}
|
||||
<div class="flex-center flex-no-shrink" bind:this={btn}>
|
||||
<div class="flex-center flex-no-shrink">
|
||||
<Button
|
||||
{width}
|
||||
{kind}
|
||||
{size}
|
||||
{justify}
|
||||
on:click={(ev) => {
|
||||
ev.stopPropagation()
|
||||
if (subIssues) showSubIssues()
|
||||
showTooltip={{
|
||||
component: SelectPopup,
|
||||
props: {
|
||||
value: selectValue,
|
||||
onSelect: openIssue,
|
||||
showShadow: false,
|
||||
width: 'large'
|
||||
}
|
||||
}}
|
||||
>
|
||||
<svelte:fragment slot="content">
|
||||
|
@ -226,6 +226,7 @@ export default mergeIds(trackerId, tracker, {
|
||||
AddRelatedIssue: '' as IntlString,
|
||||
RelatedIssuesNotFound: '' as IntlString,
|
||||
RelatedIssue: '' as IntlString,
|
||||
RelatedIssues: '' as IntlString,
|
||||
BlockedIssue: '' as IntlString,
|
||||
BlockingIssue: '' as IntlString,
|
||||
BlockedBySearchPlaceholder: '' as IntlString,
|
||||
|
@ -270,7 +270,10 @@ export const milestoneTitleMap: Record<MilestoneViewMode, IntlString> = Object.f
|
||||
closed: tracker.string.ClosedMilestones
|
||||
})
|
||||
|
||||
const listIssueStatusOrder = [
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export const listIssueStatusOrder = [
|
||||
tracker.issueStatusCategory.Started,
|
||||
tracker.issueStatusCategory.Unstarted,
|
||||
tracker.issueStatusCategory.Backlog,
|
||||
@ -278,7 +281,10 @@ const listIssueStatusOrder = [
|
||||
tracker.issueStatusCategory.Canceled
|
||||
] as const
|
||||
|
||||
const listIssueKanbanStatusOrder = [
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export const listIssueKanbanStatusOrder = [
|
||||
tracker.issueStatusCategory.Backlog,
|
||||
tracker.issueStatusCategory.Unstarted,
|
||||
tracker.issueStatusCategory.Started,
|
||||
|
@ -159,19 +159,23 @@
|
||||
return { editor: editorMixin.editor, pinned: editorMixin?.pinned }
|
||||
}
|
||||
|
||||
function getEditorFooter (_class: Ref<Class<Doc>>): { footer: AnyComponent; props?: Record<string, any> } | undefined {
|
||||
const clazz = hierarchy.getClass(_class)
|
||||
const editorMixin = hierarchy.as(clazz, view.mixin.ObjectEditorFooter)
|
||||
if (editorMixin?.editor == null && clazz.extends != null) return getEditorFooter(clazz.extends)
|
||||
if (editorMixin.editor) {
|
||||
return { footer: editorMixin.editor, props: editorMixin?.props }
|
||||
function getEditorFooter (
|
||||
_class: Ref<Class<Doc>>,
|
||||
object?: Doc
|
||||
): { footer: AnyComponent; props?: Record<string, any> } | undefined {
|
||||
if (object !== undefined) {
|
||||
const footer = hierarchy.findClassOrMixinMixin(object, view.mixin.ObjectEditorFooter)
|
||||
if (footer !== undefined) {
|
||||
return { footer: footer.editor, props: footer.props }
|
||||
}
|
||||
}
|
||||
|
||||
return undefined
|
||||
}
|
||||
|
||||
let mainEditor: MixinEditor | undefined
|
||||
|
||||
$: editorFooter = getEditorFooter(_class)
|
||||
$: editorFooter = getEditorFooter(_class, object)
|
||||
|
||||
$: getEditorOrDefault(realObjectClass, _id)
|
||||
|
||||
|
@ -128,7 +128,7 @@
|
||||
limit: number,
|
||||
options?: FindOptions<Doc>
|
||||
) {
|
||||
q.query(
|
||||
loading += q.query(
|
||||
_class,
|
||||
query,
|
||||
(result) => {
|
||||
@ -139,10 +139,12 @@
|
||||
objects = result
|
||||
}
|
||||
objectsRecieved = true
|
||||
loading = loading === 1 ? 0 : -1
|
||||
loading = 0
|
||||
},
|
||||
{ sort: getSort(sortKey), limit, ...options, lookup, total: false }
|
||||
)
|
||||
? 1
|
||||
: 0
|
||||
}
|
||||
$: update(_class, query, _sortKey, sortOrder, lookup, limit, options)
|
||||
|
||||
@ -281,9 +283,15 @@
|
||||
}
|
||||
}
|
||||
|
||||
let buildIndex = 0
|
||||
|
||||
async function build (modelOptions: BuildModelOptions) {
|
||||
isBuildingModel = true
|
||||
model = await buildModel(modelOptions)
|
||||
const idx = ++buildIndex
|
||||
const res = await buildModel(modelOptions)
|
||||
if (buildIndex === idx) {
|
||||
model = res
|
||||
}
|
||||
isBuildingModel = false
|
||||
}
|
||||
|
||||
|
@ -22,9 +22,9 @@
|
||||
import { ActionContext, createQuery, getClient } from '@hcengineering/presentation'
|
||||
import setting from '@hcengineering/setting'
|
||||
import support, { SupportStatus } from '@hcengineering/support'
|
||||
import { locationStorageKeyId, Button } from '@hcengineering/ui'
|
||||
import {
|
||||
AnyComponent,
|
||||
Button,
|
||||
CompAndProps,
|
||||
Component,
|
||||
Label,
|
||||
@ -42,8 +42,9 @@
|
||||
closeTooltip,
|
||||
deviceOptionsStore as deviceInfo,
|
||||
getCurrentLocation,
|
||||
location,
|
||||
getLocation,
|
||||
location,
|
||||
locationStorageKeyId,
|
||||
navigate,
|
||||
openPanel,
|
||||
popupstore,
|
||||
@ -64,7 +65,7 @@
|
||||
import { getContext, onDestroy, onMount, tick } from 'svelte'
|
||||
import { subscribeMobile } from '../mobile'
|
||||
import workbench from '../plugin'
|
||||
import { buildNavModel, workspacesStore, signOut } from '../utils'
|
||||
import { buildNavModel, signOut, workspacesStore } from '../utils'
|
||||
import AccountPopup from './AccountPopup.svelte'
|
||||
import AppItem from './AppItem.svelte'
|
||||
import AppSwitcher from './AppSwitcher.svelte'
|
||||
@ -762,8 +763,8 @@
|
||||
<div
|
||||
class="antiPanel-component antiComponent"
|
||||
bind:this={contentPanel}
|
||||
use:resizeObserver={(element) => {
|
||||
componentWidth = element.clientWidth
|
||||
use:resizeObserver={() => {
|
||||
componentWidth = contentPanel.clientWidth
|
||||
}}
|
||||
>
|
||||
{#if currentApplication && currentApplication.component}
|
||||
|
@ -27,6 +27,10 @@
|
||||
{
|
||||
"name": "#platform.lazy.loading",
|
||||
"value": "false"
|
||||
},
|
||||
{
|
||||
"name": "flagOpenInDesktopApp",
|
||||
"value": "true"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -23,6 +23,10 @@
|
||||
{
|
||||
"name": "#platform.lazy.loading",
|
||||
"value": "false"
|
||||
},
|
||||
{
|
||||
"name": "flagOpenInDesktopApp",
|
||||
"value": "true"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user