UBER-963: Related issues (#3773)

Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
Andrey Sobolev 2023-10-03 01:20:37 +07:00 committed by GitHub
parent 152c858c1d
commit b2435326fd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 564 additions and 301 deletions

View File

@ -213,6 +213,9 @@ export function createModel (builder: Builder): void {
{ {
key: '', key: '',
presenter: tracker.component.RelatedIssueSelector, presenter: tracker.component.RelatedIssueSelector,
props: {
kind: 'link'
},
label: tracker.string.Relations label: tracker.string.Relations
}, },
'comments', 'comments',
@ -252,6 +255,9 @@ export function createModel (builder: Builder): void {
{ {
key: '', key: '',
presenter: tracker.component.RelatedIssueSelector, presenter: tracker.component.RelatedIssueSelector,
props: {
kind: 'link'
},
label: tracker.string.Issues label: tracker.string.Issues
}, },
'status', 'status',
@ -551,6 +557,20 @@ export function createModel (builder: Builder): void {
filters: ['_class'] 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, { createAction(builder, {
action: workbench.actionImpl.Navigate, action: workbench.actionImpl.Navigate,
actionProps: { actionProps: {

View File

@ -407,6 +407,9 @@ export function createModel (builder: Builder): void {
{ {
key: '', key: '',
presenter: tracker.component.RelatedIssueSelector, presenter: tracker.component.RelatedIssueSelector,
props: {
kind: 'link'
},
label: tracker.string.Relations label: tracker.string.Relations
}, },
'comments', 'comments',
@ -487,6 +490,14 @@ export function createModel (builder: Builder): void {
label: recruit.string.Applications label: recruit.string.Applications
}, },
'comments', 'comments',
{
key: '',
presenter: tracker.component.RelatedIssueSelector,
props: {
kind: 'link'
},
label: tracker.string.Issues
},
'$lookup.company', '$lookup.company',
'$lookup.company.$lookup.channels', '$lookup.company.$lookup.channels',
'location', 'location',
@ -523,6 +534,12 @@ export function createModel (builder: Builder): void {
label: recruit.string.Applications label: recruit.string.Applications
}, },
'comments', 'comments',
{
key: '',
presenter: tracker.component.RelatedIssueSelector,
label: tracker.string.Issues,
props: { size: 'small', kind: 'link' }
},
'$lookup.channels', '$lookup.channels',
{ {
key: '@applications.modifiedOn', key: '@applications.modifiedOn',
@ -558,6 +575,9 @@ export function createModel (builder: Builder): void {
{ {
key: '', key: '',
presenter: tracker.component.RelatedIssueSelector, presenter: tracker.component.RelatedIssueSelector,
props: {
kind: 'link'
},
label: tracker.string.Issues label: tracker.string.Issues
}, },
'status', 'status',
@ -606,6 +626,9 @@ export function createModel (builder: Builder): void {
{ {
key: '', key: '',
presenter: tracker.component.RelatedIssueSelector, presenter: tracker.component.RelatedIssueSelector,
props: {
kind: 'link'
},
label: tracker.string.Issues label: tracker.string.Issues
}, },
'status', 'status',
@ -864,6 +887,12 @@ export function createModel (builder: Builder): void {
props: { kind: 'list', size: 'small', shouldShowName: false } props: { kind: 'list', size: 'small', shouldShowName: false }
}, },
{ key: 'comments', displayProps: { key: 'comments', suffix: true } }, { key: 'comments', displayProps: { key: 'comments', suffix: true } },
{
key: '',
presenter: tracker.component.RelatedIssueSelector,
label: tracker.string.Issues,
props: { size: 'small' }
},
{ {
key: '$lookup.channels', key: '$lookup.channels',
label: contact.string.ContactInfo, label: contact.string.ContactInfo,
@ -914,6 +943,11 @@ export function createModel (builder: Builder): void {
}, },
'description', 'description',
{ key: 'comments', displayProps: { key: 'comments', suffix: true } }, { key: 'comments', displayProps: { key: 'comments', suffix: true } },
{
key: '',
presenter: tracker.component.RelatedIssueSelector,
label: tracker.string.Issues
},
{ key: '', displayProps: { grow: true } }, { key: '', displayProps: { grow: true } },
{ {
key: '$lookup.company', key: '$lookup.company',
@ -1601,19 +1635,19 @@ export function createModel (builder: Builder): void {
builder.mixin(recruit.mixin.Candidate, core.class.Class, view.mixin.ObjectEditorFooter, { builder.mixin(recruit.mixin.Candidate, core.class.Class, view.mixin.ObjectEditorFooter, {
editor: tracker.component.RelatedIssuesSection, editor: tracker.component.RelatedIssuesSection,
props: { props: {
label: recruit.string.RelatedIssues label: tracker.string.RelatedIssues
} }
}) })
builder.mixin(recruit.class.Vacancy, core.class.Class, view.mixin.ObjectEditorFooter, { builder.mixin(recruit.class.Vacancy, core.class.Class, view.mixin.ObjectEditorFooter, {
editor: tracker.component.RelatedIssuesSection, editor: tracker.component.RelatedIssuesSection,
props: { props: {
label: recruit.string.RelatedIssues label: tracker.string.RelatedIssues
} }
}) })
builder.mixin(recruit.class.Applicant, core.class.Class, view.mixin.ObjectEditorFooter, { builder.mixin(recruit.class.Applicant, core.class.Class, view.mixin.ObjectEditorFooter, {
editor: tracker.component.RelatedIssuesSection, editor: tracker.component.RelatedIssuesSection,
props: { props: {
label: recruit.string.RelatedIssues label: tracker.string.RelatedIssues
} }
}) })

View File

@ -61,14 +61,14 @@
function update (stateObjects: Item[], limit: number | undefined, index: number): void { function update (stateObjects: Item[], limit: number | undefined, index: number): void {
clearTimeout(loadingTimeout) clearTimeout(loadingTimeout)
if (limitedObjects.length > 0 || index * 2 === 0) { if (limitedObjects.length > 0 || index === 0) {
limitedObjects = stateObjects.slice(0, limit) limitedObjects = stateObjects.slice(0, limit)
} else { } else {
loading = true loading = true
loadingTimeout = setTimeout(() => { loadingTimeout = setTimeout(() => {
limitedObjects = stateObjects.slice(0, limit) limitedObjects = stateObjects.slice(0, limit)
loading = false loading = false
}, index * 2) }, index)
} }
} }

View File

@ -22,11 +22,18 @@
min-width: 12.5rem; min-width: 12.5rem;
max-width: 17rem; max-width: 17rem;
max-height: 22rem; max-height: 22rem;
background: var(--theme-popup-color); background: var(--theme-popup-color);
border: 1px solid var(--theme-popup-divider); border: 1px solid var(--theme-popup-divider);
border-radius: .5rem; border-radius: .5rem;
box-shadow: var(--theme-popup-shadow); box-shadow: var(--theme-popup-shadow);
&.noShadow {
background: none;
border: none;
box-shadow: none;
}
&.full-width { &.full-width {
flex-grow: 1; flex-grow: 1;
background: none; background: none;

View File

@ -2,27 +2,28 @@
import { createEventDispatcher } from 'svelte' import { createEventDispatcher } from 'svelte'
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
import { lazyObserver } from '../lazy' import { lazyObserver, isLazyEnabled } from '../lazy'
let visible = false let visible = !isLazyEnabled()
</script> </script>
<div {#if !visible}
use:lazyObserver={(val) => { <div
use:lazyObserver={(val, unsubscribe) => {
if (val) { if (val) {
visible = true visible = true
dispatch('visible') dispatch('visible')
unsubscribe?.()
} }
}} }}
> >
{#if visible}
<slot />
{:else}
<!-- Zero-width space character --> <!-- Zero-width space character -->
{#if $$slots.loading} {#if $$slots.loading}
<slot name="loading" /> <slot name="loading" />
{:else} {:else}
&#8203; &#8203;
{/if} {/if}
{/if} </div>
</div> {:else}
<slot />
{/if}

View File

@ -22,6 +22,7 @@
import IconUpOutline from './icons/UpOutline.svelte' import IconUpOutline from './icons/UpOutline.svelte'
import IconDownOutline from './icons/DownOutline.svelte' import IconDownOutline from './icons/DownOutline.svelte'
import HalfUpDown from './icons/HalfUpDown.svelte' import HalfUpDown from './icons/HalfUpDown.svelte'
import { DelayedCaller } from '../utils'
export let padding: string | undefined = undefined export let padding: string | undefined = undefined
export let autoscroll: boolean = false export let autoscroll: boolean = false
@ -249,20 +250,16 @@
} }
} }
let checkBarTimeout: any | undefined = undefined const delayedCaller = new DelayedCaller(25)
let checkHBarTimeout: any | undefined = undefined
const delayCall = (op: () => void, h?: boolean) => { const delayCall = (op: () => void) => {
if (h) { delayedCaller.call(op)
clearTimeout(checkHBarTimeout)
checkHBarTimeout = setTimeout(op, 5)
} else {
clearTimeout(checkBarTimeout)
checkBarTimeout = setTimeout(op, 5)
}
} }
const checkFade = (): void => { const checkFade = (): void => {
delayCall(_checkFade)
}
const _checkFade = (): void => {
if (divScroll) { if (divScroll) {
beforeContent = divScroll.scrollTop beforeContent = divScroll.scrollTop
belowContent = divScroll.scrollHeight - divScroll.clientHeight - beforeContent belowContent = divScroll.scrollHeight - divScroll.clientHeight - beforeContent
@ -279,11 +276,18 @@
else if (rightContent > 2) maskH = 'left' else if (rightContent > 2) maskH = 'left'
else maskH = 'none' else maskH = 'none'
} }
if (inter.size) checkIntersectionFade() if (inter.size) {
checkIntersectionFade()
}
renderFade() renderFade()
} }
if (!isScrolling) delayCall(checkBar)
if (!isScrolling && horizontal) delayCall(checkBarH, true) if (!isScrolling) {
checkBar()
}
if (!isScrolling && horizontal) {
checkBarH()
}
} }
function checkAutoScroll () { function checkAutoScroll () {
@ -383,8 +387,12 @@
if (divScroll && divBox) { if (divScroll && divBox) {
divScroll.addEventListener('wheel', wheelEvent) divScroll.addEventListener('wheel', wheelEvent)
divScroll.addEventListener('scroll', checkFade) divScroll.addEventListener('scroll', checkFade)
delayCall(checkBar) delayCall(() => {
if (horizontal) delayCall(checkBarH, true) checkBar()
if (horizontal) {
checkBarH()
}
})
} }
}) })
onDestroy(() => { onDestroy(() => {

View File

@ -50,6 +50,8 @@
export let value: Array<ValueType> export let value: Array<ValueType>
export let width: 'medium' | 'large' | 'full' = 'medium' export let width: 'medium' | 'large' | 'full' = 'medium'
export let size: 'small' | 'medium' | 'large' = 'small' export let size: 'small' | 'medium' | 'large' = 'small'
export let onSelect: ((value: ValueType['id']) => void) | undefined = undefined
export let showShadow: boolean = true
let search: string = '' let search: string = ''
@ -60,6 +62,14 @@
let selection = 0 let selection = 0
let list: ListView let list: ListView
function sendSelect (id: ValueType['id']): void {
if (onSelect) {
onSelect(id)
} else {
dispatch('close', id)
}
}
function onKeydown (key: KeyboardEvent): void { function onKeydown (key: KeyboardEvent): void {
if (key.code === 'ArrowUp') { if (key.code === 'ArrowUp') {
key.stopPropagation() key.stopPropagation()
@ -74,7 +84,7 @@
if (key.code === 'Enter') { if (key.code === 'Enter') {
key.preventDefault() key.preventDefault()
key.stopPropagation() key.stopPropagation()
dispatch('close', value[selection].id) sendSelect(value[selection].id)
} }
} }
const manager = createFocusManager() const manager = createFocusManager()
@ -88,6 +98,7 @@
<div <div
class="selectPopup" class="selectPopup"
class:noShadow={showShadow === false}
class:full-width={width === 'full'} class:full-width={width === 'full'}
class:max-width-40={width === 'large'} class:max-width-40={width === 'large'}
use:resizeObserver={() => { use:resizeObserver={() => {
@ -121,7 +132,7 @@
> >
<svelte:fragment slot="item" let:item={itemId}> <svelte:fragment slot="item" let:item={itemId}>
{@const item = filteredObjects[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"> <div class="flex-row-center flex-grow pointer-events-none">
{#if item.component} {#if item.component}
<div class="flex-grow clear-mins"><svelte:component this={item.component} {...item.props} /></div> <div class="flex-grow clear-mins"><svelte:component this={item.component} {...item.props} /></div>

View File

@ -24,7 +24,7 @@
let tooltipHTML: HTMLElement let tooltipHTML: HTMLElement
let nubHTML: HTMLElement let nubHTML: HTMLElement
let dir: TooltipAlignment let dir: TooltipAlignment
let rect: DOMRect let rect: DOMRect | undefined
let rectAnchor: DOMRect let rectAnchor: DOMRect
let tooltipSW: boolean // tooltipSW = true - Label; false - Component let tooltipSW: boolean // tooltipSW = true - Label; false - Component
let nubDirection: 'top' | 'bottom' | 'left' | 'right' | undefined = undefined let nubDirection: 'top' | 'bottom' | 'left' | 'right' | undefined = undefined
@ -37,18 +37,61 @@
$: onUpdate = $tooltip.onUpdate $: onUpdate = $tooltip.onUpdate
$: kind = $tooltip.kind $: kind = $tooltip.kind
const clearStyles = (): void => { interface TooltipOptions {
shown = false top: string
tooltipHTML.style.top = bottom: string
tooltipHTML.style.bottom = left: string
tooltipHTML.style.left = right: string
tooltipHTML.style.right = width: string
tooltipHTML.style.height = 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 (($tooltip.label || $tooltip.component) && tooltipHTML && tooltipHTMLToCheck) {
if (clWidth === undefined) {
clWidth = tooltipHTML.clientWidth
}
if ($tooltip.element) { if ($tooltip.element) {
rect = $tooltip.element.getBoundingClientRect() rect = $tooltip.element.getBoundingClientRect()
rectAnchor = $tooltip.anchor rectAnchor = $tooltip.anchor
@ -58,28 +101,28 @@
if ($tooltip.component) { if ($tooltip.component) {
clearStyles() clearStyles()
if (rect.bottom + tooltipHTMLToCheck.clientHeight + 28 < docHeight) { 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' dir = 'bottom'
} else if (rect.top > docHeight - rect.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) { if (tooltipHTML.clientHeight > rect.top - 28) {
tooltipHTML.style.top = '1rem' options.top = '1rem'
tooltipHTML.style.height = `calc(${rect.top}px - 5px - 1.25rem)` options.height = `calc(${rect.top}px - 5px - 1.25rem)`
} }
dir = 'top' dir = 'top'
} else { } 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) { if (tooltipHTMLToCheck.clientHeight > docHeight - rect.bottom - 28) {
tooltipHTML.style.bottom = '1rem' options.bottom = '1rem'
tooltipHTML.style.height = `calc(${docHeight - rect.bottom}px - 5px - 1.25rem)` options.height = `calc(${docHeight - rect.bottom}px - 5px - 1.25rem)`
} }
dir = 'bottom' dir = 'bottom'
} }
const tempLeft = rect.width / 2 + rect.left - clWidth / 2 const tempLeft = rect.width / 2 + rect.left - clWidth / 2
if (tempLeft + clWidth > docWidth - 8) tooltipHTML.style.right = '.5rem' if (tempLeft + clWidth > docWidth - 8) options.right = '.5rem'
else if (tempLeft < 8) tooltipHTML.style.left = '.5rem' else if (tempLeft < 8) options.left = '.5rem'
else tooltipHTML.style.left = `${tempLeft}px` else options.left = `${tempLeft}px`
if (nubHTML) { if (nubHTML) {
nubHTML.style.top = rect.top + 'px' nubHTML.style.top = rect.top + 'px'
@ -97,43 +140,53 @@
} else dir = $tooltip.direction } else dir = $tooltip.direction
if (dir === 'right') { if (dir === 'right') {
tooltipHTML.style.top = rectAnchor.y + rectAnchor.height / 2 + 'px' options.top = rectAnchor.y + rectAnchor.height / 2 + 'px'
tooltipHTML.style.left = `calc(${rectAnchor.right}px + .75rem)` options.left = `calc(${rectAnchor.right}px + .75rem)`
tooltipHTML.style.transform = 'translateY(-50%)' options.transform = 'translateY(-50%)'
} else if (dir === 'left') { } else if (dir === 'left') {
tooltipHTML.style.top = rectAnchor.y + rectAnchor.height / 2 + 'px' options.top = rectAnchor.y + rectAnchor.height / 2 + 'px'
tooltipHTML.style.right = `calc(${docWidth - rectAnchor.x}px + .75rem)` options.right = `calc(${docWidth - rectAnchor.x}px + .75rem)`
tooltipHTML.style.transform = 'translateY(-50%)' options.transform = 'translateY(-50%)'
} else if (dir === 'bottom') { } else if (dir === 'bottom') {
tooltipHTML.style.top = `calc(${rectAnchor.bottom}px + .5rem)` options.top = `calc(${rectAnchor.bottom}px + .5rem)`
tooltipHTML.style.left = rectAnchor.x + rectAnchor.width / 2 + 'px' options.left = rectAnchor.x + rectAnchor.width / 2 + 'px'
tooltipHTML.style.transform = 'translateX(-50%)' options.transform = 'translateX(-50%)'
} else if (dir === 'top') { } else if (dir === 'top') {
tooltipHTML.style.bottom = `calc(${docHeight - rectAnchor.y}px + .75rem)` options.bottom = `calc(${docHeight - rectAnchor.y}px + .75rem)`
tooltipHTML.style.left = rectAnchor.x + rectAnchor.width / 2 + 'px' options.left = rectAnchor.x + rectAnchor.width / 2 + 'px'
tooltipHTML.style.transform = 'translateX(-50%)' options.transform = 'translateX(-50%)'
} }
tooltipHTML.classList.remove('no-arrow')
} }
} else { } else {
tooltipHTML.style.top = '50%' options.top = '50%'
tooltipHTML.style.left = '50%' options.left = '50%'
tooltipHTML.style.width = 'min-content' options.width = 'min-content'
tooltipHTML.style.height = 'min-content' options.height = 'min-content'
tooltipHTML.style.transform = 'translate(-50%, -50%)' options.transform = 'translate(-50%, -50%)'
tooltipHTML.classList.add('no-arrow') options.classList = 'no-arrow'
} }
tooltipHTML.style.visibility = 'visible' options.visibility = 'visible'
shown = true shown = true
} else if (tooltipHTML) { } else if (tooltipHTML) {
shown = false 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) { if (($tooltip.label || $tooltip.component) && tooltipHTML) {
clearStyles()
if ($tooltip.element) { if ($tooltip.element) {
rect = $tooltip.element.getBoundingClientRect() rect = $tooltip.element.getBoundingClientRect()
const rectP = tooltipHTML.getBoundingClientRect() const rectP = tooltipHTML.getBoundingClientRect()
@ -145,23 +198,33 @@
: rect.bottom > docHeight - rect.top : rect.bottom > docHeight - rect.top
? 'top' ? 'top'
: 'bottom' : 'bottom'
if (dirH === 'right') tooltipHTML.style.left = rect.right - 4 + 'px' if (dirH === 'right') {
else tooltipHTML.style.right = docWidth - rect.left - 4 + 'px' options.left = rect.right - 4 + 'px'
if (dirV === 'bottom') tooltipHTML.style.top = rect.top - 4 + 'px' } else {
else tooltipHTML.style.bottom = docHeight - rect.bottom - 4 + 'px' options.right = docWidth - rect.left - 4 + 'px'
tooltipHTML.style.visibility = 'visible'
} }
} 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 => { const hideTooltip = (): void => {
if (tooltipHTML) tooltipHTML.style.visibility = 'hidden' if (tooltipHTML) options.visibility = 'hidden'
closeTooltip() closeTooltip()
} }
const whileShow = (ev: MouseEvent): void => { const whileShow = (ev: MouseEvent): void => {
if ($tooltip.element && tooltipHTML) { if ($tooltip.element && tooltipHTML) {
const rectP = tooltipHTML.getBoundingClientRect() const rectP = tooltipHTML.getBoundingClientRect()
rect = $tooltip.element.getBoundingClientRect()
const dT: number = dir === 'bottom' && $tooltip.kind !== 'submenu' ? 12 : 0 const dT: number = dir === 'bottom' && $tooltip.kind !== 'submenu' ? 12 : 0
const dB: number = dir === 'top' && $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 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) $: if (kind === 'submenu') {
afterUpdate(() => (kind === 'submenu' ? fitSubmenu() : fitTooltip(tooltipHTML))) options = fitSubmenu()
} else {
options = fitTooltip(tooltipHTML, clWidth)
}
afterUpdate(() => {
if (kind === 'submenu') {
options = fitSubmenu()
} else {
options = fitTooltip(tooltipHTML, clWidth)
}
})
onDestroy(() => hideTooltip()) onDestroy(() => hideTooltip())
</script> </script>
@ -208,13 +281,20 @@
/> />
{#if $tooltip.component && $tooltip.kind !== 'submenu'} {#if $tooltip.component && $tooltip.kind !== 'submenu'}
<div <div
class="popup-tooltip" class="popup-tooltip {options.classList}"
class:shown class:shown
class:doublePadding={$tooltip.label} class:doublePadding={$tooltip.label}
use:resizeObserver={(element) => { use:resizeObserver={(element) => {
clWidth = element.clientWidth 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} bind:this={tooltipHTML}
> >
{#if $tooltip.label} {#if $tooltip.label}
@ -241,7 +321,17 @@
</div> </div>
<div bind:this={nubHTML} class="nub {nubDirection ?? ''}" class:shown /> <div bind:this={nubHTML} class="nub {nubDirection ?? ''}" class:shown />
{:else if $tooltip.label && $tooltip.kind !== 'submenu'} {: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 ?? {}} /> <Label label={$tooltip.label} params={$tooltip.props ?? {}} />
{#if $tooltip.keys !== undefined} {#if $tooltip.keys !== undefined}
<div class="keys"> <div class="keys">
@ -265,10 +355,17 @@
</div> </div>
{:else if $tooltip.kind === 'submenu'} {:else if $tooltip.kind === 'submenu'}
<div <div
class="submenu-container {dir ?? ''}" class="submenu-container {dir ?? ''} {options.classList}"
use:resizeObserver={(element) => { use:resizeObserver={(element) => {
clWidth = element.clientWidth 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} bind:this={tooltipHTML}
> >
{#if typeof $tooltip.component === 'string'} {#if typeof $tooltip.component === 'string'}

View File

@ -1,32 +1,41 @@
const observers = new Map<string, IntersectionObserver>() import { DelayedCaller } from './utils'
const entryMap = new WeakMap<Element, { callback: (entry: IntersectionObserverEntry) => void }>()
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 { function makeObserver (rootMargin: string): IntersectionObserver {
return new IntersectionObserver( const entriesPending = new Map<Element, { isIntersecting: boolean }>()
(entries, observer) => { const notifyObservers = (observer: IntersectionObserver): void => {
for (const entry of entries) { console.log('notifyObservers', entriesPending.size)
const entryData = entryMap.get(entry.target) for (const [target, entry] of entriesPending.entries()) {
const entryData = entryMap.get(target)
if (entryData == null) { if (entryData == null) {
observer.unobserve(entry.target) observer.unobserve(target)
continue continue
} }
entryData.callback(entry) entryData.callback(entry.isIntersecting)
if (entry.isIntersecting) { if (entry.isIntersecting) {
entryMap.delete(entry.target) entryMap.delete(target)
observer.unobserve(entry.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 } { rootMargin }
) )
return observer
} }
function listen ( function listen (rootMargin: string, element: Element, callback: (isIntersecting: boolean) => void): () => void {
rootMargin: string,
element: Element,
callback: (entry: IntersectionObserverEntry) => void
): () => void {
let observer = observers.get(rootMargin) let observer = observers.get(rootMargin)
if (observer == null) { if (observer == null) {
observer = makeObserver(rootMargin) 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 let visible = false
const lazyEnabled = (localStorage.getItem('#platform.lazy.loading') ?? 'true') === 'true' const lazyEnabled = isLazyEnabled()
if (!lazyEnabled) { if (!lazyEnabled) {
visible = true visible = true
onVisible(visible) onVisible(visible)
@ -53,9 +67,9 @@ export function lazyObserver (node: Element, onVisible: (value: boolean) => void
return {} return {}
} }
const destroy = listen('20%', node, ({ isIntersecting }) => { const destroy = listen('20%', node, (isIntersecting) => {
visible = isIntersecting visible = isIntersecting
onVisible(visible) onVisible(visible, destroy)
}) })
return { return {

View File

@ -9,25 +9,40 @@
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// //
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
import { DelayedCaller } from './utils'
// limitations under the License. // limitations under the License.
let observer: ResizeObserver let observer: ResizeObserver
let callbacks: WeakMap<Element, (element: Element) => any> let callbacks: WeakMap<Element, (element: Element) => any>
const delayedCaller = new DelayedCaller(10)
/** /**
* @public * @public
*/ */
export function resizeObserver (element: Element, onResize: (element: Element) => any): { destroy: () => void } { export function resizeObserver (element: Element, onResize: (element: Element) => any): { destroy: () => void } {
if (observer === undefined) { if (observer === undefined) {
callbacks = new WeakMap() callbacks = new WeakMap()
observer = new ResizeObserver((entries) => { const entriesPending = new Set<Element>()
const notifyObservers = (): void => {
window.requestAnimationFrame(() => { window.requestAnimationFrame(() => {
for (const entry of entries) { for (const target of entriesPending.values()) {
const onResize = callbacks.get(entry.target) const onResize = callbacks.get(target)
if (onResize != null) { 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)
}) })
} }

View File

@ -238,3 +238,21 @@ export function formatKey (key: string): string[][] {
} }
return result 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)
}
}
}

View File

@ -96,7 +96,6 @@
"HasActiveApplicant":"Active Only", "HasActiveApplicant":"Active Only",
"HasNoActiveApplicant": "No Active", "HasNoActiveApplicant": "No Active",
"NoneApplications": "None", "NoneApplications": "None",
"RelatedIssues": "Related issues",
"VacancyList": "Vacancies", "VacancyList": "Vacancies",
"MatchVacancy": "Match to vacancy", "MatchVacancy": "Match to vacancy",
"VacancyMatching": "Match Talents to vacancy", "VacancyMatching": "Match Talents to vacancy",

View File

@ -98,7 +98,6 @@
"HasActiveApplicant":"Только активные", "HasActiveApplicant":"Только активные",
"HasNoActiveApplicant": "Не активные", "HasNoActiveApplicant": "Не активные",
"NoneApplications": "Отсутствуют", "NoneApplications": "Отсутствуют",
"RelatedIssues": "Связанные задачи",
"VacancyList": "Вакансии", "VacancyList": "Вакансии",
"MatchVacancy": "Проверить на вакансию", "MatchVacancy": "Проверить на вакансию",
"VacancyMatching": "Подбор кандидатов на вакансию", "VacancyMatching": "Подбор кандидатов на вакансию",

View File

@ -199,7 +199,7 @@
<VacancyApplications objectId={object._id} /> <VacancyApplications objectId={object._id} />
</div> </div>
<div class="w-full mt-6"> <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> </div>
</Panel> </Panel>
{/if} {/if}

View File

@ -177,7 +177,8 @@
function createConfig ( function createConfig (
descr: Viewlet | undefined, descr: Viewlet | undefined,
preference: ViewletPreference | undefined preference: ViewletPreference | undefined,
replacedKeys: Map<string, BuildModelKey>
): (string | BuildModelKey)[] { ): (string | BuildModelKey)[] {
const base = preference?.config ?? descr?.config ?? [] const base = preference?.config ?? descr?.config ?? []
const result: (string | BuildModelKey)[] = [] const result: (string | BuildModelKey)[] = []
@ -191,7 +192,7 @@
return result return result
} }
$: finalConfig = createConfig(viewlet, preference) $: finalConfig = createConfig(viewlet, preference, replacedKeys)
</script> </script>
<div class="ac-header full divide"> <div class="ac-header full divide">

View File

@ -66,7 +66,7 @@
(applications?.get(b._id as Ref<Vacancy>)?.modifiedOn ?? b.modifiedOn) - (applications?.get(b._id as Ref<Vacancy>)?.modifiedOn ?? b.modifiedOn) -
(applications?.get(a._id as Ref<Vacancy>)?.modifiedOn ?? a.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', '@applications',
{ {
@ -100,7 +100,8 @@
function createConfig ( function createConfig (
descr: Viewlet, descr: Viewlet,
preference: ViewletPreference | undefined, preference: ViewletPreference | undefined,
applications: Map<Ref<Vacancy>, ApplicationInfo> applications: Map<Ref<Vacancy>, ApplicationInfo>,
replacedKeys: Map<string, BuildModelKey>
): (string | BuildModelKey)[] { ): (string | BuildModelKey)[] {
const base = preference?.config ?? descr.config const base = preference?.config ?? descr.config
const result: (string | BuildModelKey)[] = [] const result: (string | BuildModelKey)[] = []
@ -181,7 +182,7 @@
props={{ props={{
_class: recruit.class.Vacancy, _class: recruit.class.Vacancy,
options: viewlet.options, options: viewlet.options,
config: createConfig(viewlet, preference, applications), config: createConfig(viewlet, preference, applications, replacedKeys),
viewlet, viewlet,
viewOptions, viewOptions,
viewOptionsConfig: viewlet.viewOptions?.other, viewOptionsConfig: viewlet.viewOptions?.other,

View File

@ -69,7 +69,7 @@
<Icon icon={recruit.icon.Issue} size={'small'} /> <Icon icon={recruit.icon.Issue} size={'small'} />
</div> </div>
<span class="antiSection-header__title"> <span class="antiSection-header__title">
<Label label={recruit.string.RelatedIssues} /> <Label label={tracker.string.RelatedIssues} />
</span> </span>
<div class="buttons-group small-gap"> <div class="buttons-group small-gap">
<Button <Button

View File

@ -116,7 +116,6 @@ export default mergeIds(recruitId, recruit, {
HasActiveApplicant: '' as IntlString, HasActiveApplicant: '' as IntlString,
HasNoActiveApplicant: '' as IntlString, HasNoActiveApplicant: '' as IntlString,
NoneApplications: '' as IntlString, NoneApplications: '' as IntlString,
RelatedIssues: '' as IntlString,
MatchVacancy: '' as IntlString, MatchVacancy: '' as IntlString,
VacancyMatching: '' as IntlString, VacancyMatching: '' as IntlString,

View File

@ -65,7 +65,7 @@ export function getStates (space: SpaceWithStates | undefined, statusStore: IdMa
return [] 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 return states
} }

View File

@ -173,6 +173,7 @@
"RelatedIssueSearchPlaceholder": "Search for issue to reference...", "RelatedIssueSearchPlaceholder": "Search for issue to reference...",
"Blocks": "Blocks", "Blocks": "Blocks",
"Related": "Related", "Related": "Related",
"RelatedIssues": "Related issues",
"EditIssue": "Edit {title}", "EditIssue": "Edit {title}",
"EditWorkflowStatuses": "Edit issue statuses", "EditWorkflowStatuses": "Edit issue statuses",

View File

@ -173,6 +173,7 @@
"RelatedIssueSearchPlaceholder": "Поиск связанной задачи...", "RelatedIssueSearchPlaceholder": "Поиск связанной задачи...",
"Blocks": "Блокирует", "Blocks": "Блокирует",
"Related": "Связан", "Related": "Связан",
"RelatedIssues": "Связанные задачи",
"EditIssue": "Редактирование {title}", "EditIssue": "Редактирование {title}",
"EditWorkflowStatuses": "Редактировать статусы задач", "EditWorkflowStatuses": "Редактировать статусы задач",

View File

@ -13,8 +13,8 @@
// limitations under the License. // limitations under the License.
--> -->
<script lang="ts"> <script lang="ts">
import core, { SortingOrder, WithLookup } from '@hcengineering/core' import core, { IdMap, SortingOrder, StatusCategory, WithLookup, toIdMap } from '@hcengineering/core'
import { createQuery } from '@hcengineering/presentation' import { createQuery, getClient } from '@hcengineering/presentation'
import { Issue, IssueStatus } from '@hcengineering/tracker' import { Issue, IssueStatus } from '@hcengineering/tracker'
import { import {
Icon, Icon,
@ -25,7 +25,6 @@
closeTooltip, closeTooltip,
getPlatformColorDef, getPlatformColorDef,
navigate, navigate,
showPopup,
themeStore, themeStore,
tooltip tooltip
} from '@hcengineering/ui' } from '@hcengineering/ui'
@ -33,6 +32,7 @@
import { getIssueId, issueLinkFragmentProvider } from '../../../issues' import { getIssueId, issueLinkFragmentProvider } from '../../../issues'
import tracker from '../../../plugin' import tracker from '../../../plugin'
import IssueStatusIcon from '../IssueStatusIcon.svelte' import IssueStatusIcon from '../IssueStatusIcon.svelte'
import { listIssueStatusOrder } from '../../../utils'
export let issue: WithLookup<Issue> 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 $: areSubIssuesLoading = !subIssues
$: parentIssue = issue.$lookup?.attachedTo ? (issue.$lookup?.attachedTo as Issue) : null $: parentIssue = issue.$lookup?.attachedTo ? (issue.$lookup?.attachedTo as Issue) : null
$: if (parentIssue && parentIssue.subIssues > 0) { $: if (parentIssue && parentIssue.subIssues > 0) {
@ -119,6 +76,52 @@
} }
$: parentStatus = parentIssue ? $statusStore.get(parentIssue.status) : undefined $: 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> </script>
{#if parentIssue} {#if parentIssue}
@ -152,8 +155,15 @@
<div <div
bind:this={subIssuesElement} bind:this={subIssuesElement}
class="flex-center sub-issues cursor-pointer" class="flex-center sub-issues cursor-pointer"
use:tooltip={{ label: tracker.string.OpenSubIssues, direction: 'bottom' }} use:tooltip={{
on:click|preventDefault={showSubIssues} component: SelectPopup,
props: {
value: subIssueValue,
onSelect: openIssue,
showShadow: false,
width: 'large'
}
}}
> >
<span class="overflow-label">{subIssues?.length}</span> <span class="overflow-label">{subIssues?.length}</span>
<div class="ml-2"> <div class="ml-2">

View File

@ -13,23 +13,14 @@
// limitations under the License. // limitations under the License.
--> -->
<script lang="ts"> <script lang="ts">
import { Ref, SortingOrder, Status, WithLookup } from '@hcengineering/core' import core, { IdMap, Ref, SortingOrder, Status, StatusCategory, WithLookup, toIdMap } from '@hcengineering/core'
import { createQuery } from '@hcengineering/presentation' import { createQuery, getClient } from '@hcengineering/presentation'
import { Issue, Project } from '@hcengineering/tracker' import { Issue, Project } from '@hcengineering/tracker'
import { import { Button, ButtonKind, ButtonSize, ProgressCircle, SelectPopup, showPanel } from '@hcengineering/ui'
Button,
ButtonKind,
ButtonSize,
ProgressCircle,
SelectPopup,
closeTooltip,
showPanel,
showPopup
} from '@hcengineering/ui'
import { statusStore } from '@hcengineering/view-resources' import { statusStore } from '@hcengineering/view-resources'
import { getIssueId } from '../../../issues' import { getIssueId } from '../../../issues'
import tracker from '../../../plugin' import tracker from '../../../plugin'
import { subIssueListProvider } from '../../../utils' import { listIssueStatusOrder, subIssueListProvider } from '../../../utils'
import IssueStatusIcon from '../IssueStatusIcon.svelte' import IssueStatusIcon from '../IssueStatusIcon.svelte'
export let value: WithLookup<Issue> export let value: WithLookup<Issue>
@ -46,6 +37,7 @@
$: project = currentProject $: project = currentProject
let subIssues: Issue[] = [] let subIssues: Issue[] = []
let _subIssues: Issue[] = []
let countComplete: number = 0 let countComplete: number = 0
const projectQuery = createQuery() const projectQuery = createQuery()
@ -64,11 +56,18 @@
$: update(value) $: update(value)
let categories: IdMap<StatusCategory> = new Map()
getClient()
.findAll(core.class.StatusCategory, {})
.then((res) => {
categories = toIdMap(res)
})
function update (value: WithLookup<Issue>): void { function update (value: WithLookup<Issue>): void {
if (value.$lookup?.subIssues !== undefined) { if (value.$lookup?.subIssues !== undefined) {
query.unsubscribe() query.unsubscribe()
subIssues = value.$lookup.subIssues as Issue[] subIssues = value.$lookup.subIssues as Issue[]
subIssues.sort((a, b) => (a.rank ?? '').localeCompare(b.rank ?? ''))
} else if (value.subIssues > 0) { } else if (value.subIssues > 0) {
query.query(tracker.class.Issue, { attachedTo: value._id }, (res) => (subIssues = res), { query.query(tracker.class.Issue, { attachedTo: value._id }, (res) => (subIssues = res), {
sort: { rank: SortingOrder.Ascending } sort: { rank: SortingOrder.Ascending }
@ -101,14 +100,19 @@
} }
} }
function showSubIssues () { $: {
if (subIssues) { subIssues.sort(
closeTooltip() (a, b) =>
showPopup( listIssueStatusOrder.indexOf($statusStore.get(a.status)?.category ?? tracker.issueStatusCategory.Backlog) -
SelectPopup, listIssueStatusOrder.indexOf($statusStore.get(b.status)?.category ?? tracker.issueStatusCategory.Backlog)
{ )
value: subIssues.map((iss) => { _subIssues = subIssues
}
$: subIssuesValue = _subIssues.map((iss) => {
const text = project ? `${getIssueId(project, iss)} ${iss.title}` : iss.title 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 { return {
id: iss._id, id: iss._id,
text, text,
@ -118,26 +122,16 @@
value: $statusStore.get(iss.status), value: $statusStore.get(iss.status),
size: 'small', size: 'small',
fill: undefined fill: undefined
}
}
}),
width: 'large'
}, },
{ category:
getBoundingClientRect: () => { category !== undefined
const rect = btn.getBoundingClientRect() ? {
const offsetX = 0 label: category.label,
const offsetY = 0 icon: category.icon
return DOMRect.fromRect({ width: 1, height: 1, x: rect.left + offsetX, y: rect.bottom + offsetY })
}
},
(selectedIssue) => {
selectedIssue !== undefined && openIssue(selectedIssue)
}
)
} }
: undefined
} }
})
</script> </script>
{#if hasSubIssues} {#if hasSubIssues}
@ -147,9 +141,14 @@
{kind} {kind}
{size} {size}
{justify} {justify}
on:click={(ev) => { showTooltip={{
ev.stopPropagation() component: SelectPopup,
if (subIssues) showSubIssues() props: {
value: subIssuesValue,
onSelect: openIssue,
showShadow: false,
width: 'large'
}
}} }}
> >
<svelte:fragment slot="content"> <svelte:fragment slot="content">

View File

@ -13,22 +13,13 @@
// limitations under the License. // limitations under the License.
--> -->
<script lang="ts"> <script lang="ts">
import { Doc, Ref, SortingOrder, WithLookup } from '@hcengineering/core' import core, { Doc, IdMap, Ref, SortingOrder, StatusCategory, WithLookup, toIdMap } from '@hcengineering/core'
import { createQuery } from '@hcengineering/presentation' import { createQuery, getClient } from '@hcengineering/presentation'
import { Issue, Project } from '@hcengineering/tracker' import { Issue, Project } from '@hcengineering/tracker'
import { import { Button, ButtonKind, ButtonSize, ProgressCircle, SelectPopup, showPanel } from '@hcengineering/ui'
Button,
ButtonKind,
ButtonSize,
ProgressCircle,
SelectPopup,
closeTooltip,
showPanel,
showPopup
} from '@hcengineering/ui'
import { statusStore } from '@hcengineering/view-resources' import { statusStore } from '@hcengineering/view-resources'
import tracker from '../../../plugin' import tracker from '../../../plugin'
import { subIssueListProvider } from '../../../utils' import { listIssueStatusOrder, subIssueListProvider } from '../../../utils'
import RelatedIssuePresenter from './RelatedIssuePresenter.svelte' import RelatedIssuePresenter from './RelatedIssuePresenter.svelte'
export let object: WithLookup<Doc & { related: number }> | undefined export let object: WithLookup<Doc & { related: number }> | undefined
@ -41,8 +32,7 @@
export let width: string | undefined = 'min-contet' export let width: string | undefined = 'min-contet'
export let compactMode: boolean = false export let compactMode: boolean = false
let btn: HTMLElement let _subIssues: Issue[] = []
let subIssues: Issue[] = [] let subIssues: Issue[] = []
let countComplete: number = 0 let countComplete: number = 0
@ -55,13 +45,12 @@
function update (value: WithLookup<Doc & { related: number }>): void { function update (value: WithLookup<Doc & { related: number }>): void {
if (value.$lookup?.related !== undefined) { if (value.$lookup?.related !== undefined) {
query.unsubscribe() query.unsubscribe()
subIssues = value.$lookup.related as Issue[] _subIssues = value.$lookup.related as Issue[]
subIssues.sort((a, b) => (a.rank ?? '').localeCompare(b.rank ?? ''))
} else { } else {
query.query( query.query(
tracker.class.Issue, tracker.class.Issue,
{ 'relations._id': value._id, 'relations._class': value._class }, { 'relations._id': value._id, 'relations._class': value._class },
(res) => (subIssues = res), (res) => (_subIssues = res),
{ {
sort: { rank: SortingOrder.Ascending } 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) { $: if (subIssues) {
const doneStatuses = Array.from($statusStore.values()) 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) .map((p) => p._id)
countComplete = subIssues.filter((si) => doneStatuses.includes(si.status)).length countComplete = subIssues.filter((si) => doneStatuses.includes(si.status)).length
} }
@ -82,49 +91,40 @@
showPanel(tracker.component.EditIssue, target, tracker.class.Issue, 'content') showPanel(tracker.component.EditIssue, target, tracker.class.Issue, 'content')
} }
function showSubIssues () { $: selectValue = subIssues.map((iss) => {
if (subIssues) { const c = $statusStore.get(iss.status)?.category
closeTooltip() const category = c !== undefined ? categories.get(c) : undefined
showPopup(
SelectPopup,
{
value: subIssues.map((iss) => {
return { return {
id: iss._id, id: iss._id,
isSelected: false, isSelected: false,
component: RelatedIssuePresenter, component: RelatedIssuePresenter,
props: { project: currentProject, issue: iss } props: { project: currentProject, issue: iss },
} category:
}), category !== undefined
width: 'large' ? {
}, label: category.label,
{ icon: category.icon
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)
}
)
} }
: undefined
} }
})
</script> </script>
{#if hasSubIssues} {#if hasSubIssues}
<div class="flex-center flex-no-shrink" bind:this={btn}> <div class="flex-center flex-no-shrink">
<Button <Button
{width} {width}
{kind} {kind}
{size} {size}
{justify} {justify}
on:click={(ev) => { showTooltip={{
ev.stopPropagation() component: SelectPopup,
if (subIssues) showSubIssues() props: {
value: selectValue,
onSelect: openIssue,
showShadow: false,
width: 'large'
}
}} }}
> >
<svelte:fragment slot="content"> <svelte:fragment slot="content">

View File

@ -226,6 +226,7 @@ export default mergeIds(trackerId, tracker, {
AddRelatedIssue: '' as IntlString, AddRelatedIssue: '' as IntlString,
RelatedIssuesNotFound: '' as IntlString, RelatedIssuesNotFound: '' as IntlString,
RelatedIssue: '' as IntlString, RelatedIssue: '' as IntlString,
RelatedIssues: '' as IntlString,
BlockedIssue: '' as IntlString, BlockedIssue: '' as IntlString,
BlockingIssue: '' as IntlString, BlockingIssue: '' as IntlString,
BlockedBySearchPlaceholder: '' as IntlString, BlockedBySearchPlaceholder: '' as IntlString,

View File

@ -270,7 +270,10 @@ export const milestoneTitleMap: Record<MilestoneViewMode, IntlString> = Object.f
closed: tracker.string.ClosedMilestones closed: tracker.string.ClosedMilestones
}) })
const listIssueStatusOrder = [ /**
* @public
*/
export const listIssueStatusOrder = [
tracker.issueStatusCategory.Started, tracker.issueStatusCategory.Started,
tracker.issueStatusCategory.Unstarted, tracker.issueStatusCategory.Unstarted,
tracker.issueStatusCategory.Backlog, tracker.issueStatusCategory.Backlog,
@ -278,7 +281,10 @@ const listIssueStatusOrder = [
tracker.issueStatusCategory.Canceled tracker.issueStatusCategory.Canceled
] as const ] as const
const listIssueKanbanStatusOrder = [ /**
* @public
*/
export const listIssueKanbanStatusOrder = [
tracker.issueStatusCategory.Backlog, tracker.issueStatusCategory.Backlog,
tracker.issueStatusCategory.Unstarted, tracker.issueStatusCategory.Unstarted,
tracker.issueStatusCategory.Started, tracker.issueStatusCategory.Started,

View File

@ -159,19 +159,23 @@
return { editor: editorMixin.editor, pinned: editorMixin?.pinned } return { editor: editorMixin.editor, pinned: editorMixin?.pinned }
} }
function getEditorFooter (_class: Ref<Class<Doc>>): { footer: AnyComponent; props?: Record<string, any> } | undefined { function getEditorFooter (
const clazz = hierarchy.getClass(_class) _class: Ref<Class<Doc>>,
const editorMixin = hierarchy.as(clazz, view.mixin.ObjectEditorFooter) object?: Doc
if (editorMixin?.editor == null && clazz.extends != null) return getEditorFooter(clazz.extends) ): { footer: AnyComponent; props?: Record<string, any> } | undefined {
if (editorMixin.editor) { if (object !== undefined) {
return { footer: editorMixin.editor, props: editorMixin?.props } const footer = hierarchy.findClassOrMixinMixin(object, view.mixin.ObjectEditorFooter)
if (footer !== undefined) {
return { footer: footer.editor, props: footer.props }
} }
}
return undefined return undefined
} }
let mainEditor: MixinEditor | undefined let mainEditor: MixinEditor | undefined
$: editorFooter = getEditorFooter(_class) $: editorFooter = getEditorFooter(_class, object)
$: getEditorOrDefault(realObjectClass, _id) $: getEditorOrDefault(realObjectClass, _id)

View File

@ -128,7 +128,7 @@
limit: number, limit: number,
options?: FindOptions<Doc> options?: FindOptions<Doc>
) { ) {
q.query( loading += q.query(
_class, _class,
query, query,
(result) => { (result) => {
@ -139,10 +139,12 @@
objects = result objects = result
} }
objectsRecieved = true objectsRecieved = true
loading = loading === 1 ? 0 : -1 loading = 0
}, },
{ sort: getSort(sortKey), limit, ...options, lookup, total: false } { sort: getSort(sortKey), limit, ...options, lookup, total: false }
) )
? 1
: 0
} }
$: update(_class, query, _sortKey, sortOrder, lookup, limit, options) $: update(_class, query, _sortKey, sortOrder, lookup, limit, options)
@ -281,9 +283,15 @@
} }
} }
let buildIndex = 0
async function build (modelOptions: BuildModelOptions) { async function build (modelOptions: BuildModelOptions) {
isBuildingModel = true isBuildingModel = true
model = await buildModel(modelOptions) const idx = ++buildIndex
const res = await buildModel(modelOptions)
if (buildIndex === idx) {
model = res
}
isBuildingModel = false isBuildingModel = false
} }

View File

@ -22,9 +22,9 @@
import { ActionContext, createQuery, getClient } from '@hcengineering/presentation' import { ActionContext, createQuery, getClient } from '@hcengineering/presentation'
import setting from '@hcengineering/setting' import setting from '@hcengineering/setting'
import support, { SupportStatus } from '@hcengineering/support' import support, { SupportStatus } from '@hcengineering/support'
import { locationStorageKeyId, Button } from '@hcengineering/ui'
import { import {
AnyComponent, AnyComponent,
Button,
CompAndProps, CompAndProps,
Component, Component,
Label, Label,
@ -42,8 +42,9 @@
closeTooltip, closeTooltip,
deviceOptionsStore as deviceInfo, deviceOptionsStore as deviceInfo,
getCurrentLocation, getCurrentLocation,
location,
getLocation, getLocation,
location,
locationStorageKeyId,
navigate, navigate,
openPanel, openPanel,
popupstore, popupstore,
@ -64,7 +65,7 @@
import { getContext, onDestroy, onMount, tick } from 'svelte' import { getContext, onDestroy, onMount, tick } from 'svelte'
import { subscribeMobile } from '../mobile' import { subscribeMobile } from '../mobile'
import workbench from '../plugin' import workbench from '../plugin'
import { buildNavModel, workspacesStore, signOut } from '../utils' import { buildNavModel, signOut, workspacesStore } from '../utils'
import AccountPopup from './AccountPopup.svelte' import AccountPopup from './AccountPopup.svelte'
import AppItem from './AppItem.svelte' import AppItem from './AppItem.svelte'
import AppSwitcher from './AppSwitcher.svelte' import AppSwitcher from './AppSwitcher.svelte'
@ -762,8 +763,8 @@
<div <div
class="antiPanel-component antiComponent" class="antiPanel-component antiComponent"
bind:this={contentPanel} bind:this={contentPanel}
use:resizeObserver={(element) => { use:resizeObserver={() => {
componentWidth = element.clientWidth componentWidth = contentPanel.clientWidth
}} }}
> >
{#if currentApplication && currentApplication.component} {#if currentApplication && currentApplication.component}

View File

@ -27,6 +27,10 @@
{ {
"name": "#platform.lazy.loading", "name": "#platform.lazy.loading",
"value": "false" "value": "false"
},
{
"name": "flagOpenInDesktopApp",
"value": "true"
} }
] ]
} }

View File

@ -23,6 +23,10 @@
{ {
"name": "#platform.lazy.loading", "name": "#platform.lazy.loading",
"value": "false" "value": "false"
},
{
"name": "flagOpenInDesktopApp",
"value": "true"
} }
] ]
} }