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: '',
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: {

View File

@ -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
}
})

View File

@ -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)
}
}

View File

@ -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;

View File

@ -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>
<div
use:lazyObserver={(val) => {
if (val) {
visible = true
dispatch('visible')
}
}}
>
{#if visible}
<slot />
{:else}
{#if !visible}
<div
use:lazyObserver={(val, unsubscribe) => {
if (val) {
visible = true
dispatch('visible')
unsubscribe?.()
}
}}
>
<!-- Zero-width space character -->
{#if $$slots.loading}
<slot name="loading" />
{:else}
&#8203;
{/if}
{/if}
</div>
</div>
{:else}
<slot />
{/if}

View File

@ -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(() => {

View File

@ -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>

View File

@ -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'
}
if (dirV === 'bottom') {
options.top = rect.top - 4 + 'px'
} else {
options.bottom = docHeight - rect.bottom - 4 + 'px'
}
options.visibility = 'visible'
}
} else if (tooltipHTML) tooltipHTML.style.visibility = 'hidden'
} 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'}

View File

@ -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(
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(target)
continue
}
entryData.callback(entry.isIntersecting)
if (entry.isIntersecting) {
entryMap.delete(target)
observer.unobserve(target)
}
}
entriesPending.clear()
}
const observer = new IntersectionObserver(
(entries, observer) => {
for (const entry of entries) {
const entryData = entryMap.get(entry.target)
if (entryData == null) {
observer.unobserve(entry.target)
continue
}
entryData.callback(entry)
if (entry.isIntersecting) {
entryMap.delete(entry.target)
observer.unobserve(entry.target)
}
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 {

View File

@ -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)
})
}

View File

@ -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)
}
}
}

View File

@ -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",

View File

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

View File

@ -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}

View File

@ -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">

View File

@ -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,

View File

@ -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

View File

@ -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,

View File

@ -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
}

View File

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

View File

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

View File

@ -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">

View File

@ -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,43 +100,38 @@
}
}
function showSubIssues () {
if (subIssues) {
closeTooltip()
showPopup(
SelectPopup,
{
value: subIssues.map((iss) => {
const text = project ? `${getIssueId(project, iss)} ${iss.title}` : iss.title
return {
id: iss._id,
text,
isSelected: iss._id === value._id,
icon: IssueStatusIcon,
iconProps: {
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)
}
)
}
$: {
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,
isSelected: iss._id === value._id,
icon: IssueStatusIcon,
iconProps: {
value: $statusStore.get(iss.status),
size: 'small',
fill: undefined
},
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">

View File

@ -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) => {
return {
id: iss._id,
isSelected: false,
component: RelatedIssuePresenter,
props: { project: currentProject, issue: 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 },
category:
category !== undefined
? {
label: category.label,
icon: category.icon
}
}),
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)
}
)
: 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">

View File

@ -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,

View File

@ -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,

View File

@ -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)

View File

@ -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
}

View File

@ -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}

View File

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

View File

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