mirror of
https://github.com/hcengineering/platform.git
synced 2024-12-23 11:31:57 +03:00
Issue Status fixes (#2482)
Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
parent
9daef6444d
commit
673eec8110
@ -130,7 +130,16 @@
|
|||||||
<slot name="content" />
|
<slot name="content" />
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<Button {focusIndex} width={width ?? 'min-content'} {size} {kind} {justify} {showTooltip} on:click={_click}>
|
<Button
|
||||||
|
{focusIndex}
|
||||||
|
disabled={readonly}
|
||||||
|
width={width ?? 'min-content'}
|
||||||
|
{size}
|
||||||
|
{kind}
|
||||||
|
{justify}
|
||||||
|
{showTooltip}
|
||||||
|
on:click={_click}
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
slot="content"
|
slot="content"
|
||||||
class="overflow-label flex-row-center"
|
class="overflow-label flex-row-center"
|
||||||
|
@ -41,6 +41,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<span class="an-element__label title">
|
<span class="an-element__label title">
|
||||||
{#if label}<Label {label} />{/if}
|
{#if label}<Label {label} />{/if}
|
||||||
|
<slot name="title" />
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<slot name="tools" />
|
<slot name="tools" />
|
||||||
|
@ -21,7 +21,7 @@
|
|||||||
|
|
||||||
export let value: WithLookup<Issue>
|
export let value: WithLookup<Issue>
|
||||||
export let disableClick = false
|
export let disableClick = false
|
||||||
export let onClick: () => void
|
export let onClick: (() => void) | undefined = undefined
|
||||||
|
|
||||||
function handleIssueEditorOpened () {
|
function handleIssueEditorOpened () {
|
||||||
if (disableClick) {
|
if (disableClick) {
|
||||||
|
@ -32,12 +32,20 @@
|
|||||||
Spinner
|
Spinner
|
||||||
} from '@hcengineering/ui'
|
} from '@hcengineering/ui'
|
||||||
import { AttributeModel, BuildModelKey } from '@hcengineering/view'
|
import { AttributeModel, BuildModelKey } from '@hcengineering/view'
|
||||||
import { buildModel, filterStore, getObjectPresenter, LoadingProps, Menu } from '@hcengineering/view-resources'
|
import {
|
||||||
|
buildModel,
|
||||||
|
filterStore,
|
||||||
|
FixedColumn,
|
||||||
|
getObjectPresenter,
|
||||||
|
LoadingProps,
|
||||||
|
Menu
|
||||||
|
} from '@hcengineering/view-resources'
|
||||||
import { onDestroy } from 'svelte'
|
import { onDestroy } from 'svelte'
|
||||||
import { createEventDispatcher } from 'svelte'
|
import { createEventDispatcher } from 'svelte'
|
||||||
import tracker from '../../plugin'
|
import tracker from '../../plugin'
|
||||||
import { IssuesGroupByKeys, issuesGroupEditorMap, IssuesOrderByKeys, issuesSortOrderMap } from '../../utils'
|
import { IssuesGroupByKeys, issuesGroupEditorMap, IssuesOrderByKeys, issuesSortOrderMap } from '../../utils'
|
||||||
import CreateIssue from '../CreateIssue.svelte'
|
import CreateIssue from '../CreateIssue.svelte'
|
||||||
|
import IssueStatistics from '../sprints/IssueStatistics.svelte'
|
||||||
import IssuesListItem from './IssuesListItem.svelte'
|
import IssuesListItem from './IssuesListItem.svelte'
|
||||||
|
|
||||||
export let _class: Ref<Class<Doc>>
|
export let _class: Ref<Class<Doc>>
|
||||||
@ -79,7 +87,7 @@
|
|||||||
let personPresenter: AttributeModel
|
let personPresenter: AttributeModel
|
||||||
let isCollapsedMap: Record<any, boolean> = {}
|
let isCollapsedMap: Record<any, boolean> = {}
|
||||||
let varsStyle: string = ''
|
let varsStyle: string = ''
|
||||||
let propsWidth: Record<string, number> = {}
|
let propsWidth: Record<string, number> = { groupBy: 0 }
|
||||||
let itemModels: AttributeModel[]
|
let itemModels: AttributeModel[]
|
||||||
let isFilterUpdate = false
|
let isFilterUpdate = false
|
||||||
let groupedIssuesBeforeFilter = groupedIssues
|
let groupedIssuesBeforeFilter = groupedIssues
|
||||||
@ -122,7 +130,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function toCat (category: any): any {
|
function toCat (category: any): any {
|
||||||
return category ?? noCategory
|
return 'cat-' + (category ?? noCategory)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleCollapseCategory = (category: any) => {
|
const handleCollapseCategory = (category: any) => {
|
||||||
@ -155,7 +163,7 @@
|
|||||||
onDestroy(unsubscribeFilter)
|
onDestroy(unsubscribeFilter)
|
||||||
|
|
||||||
$: {
|
$: {
|
||||||
if (isFilterUpdate && groupedIssuesBeforeFilter !== groupedIssues) {
|
if (isFilterUpdate && groupedIssuesBeforeFilter !== groupedIssues && groupByKey) {
|
||||||
isCollapsedMap = {}
|
isCollapsedMap = {}
|
||||||
|
|
||||||
categories.forEach((category) => (isCollapsedMap[toCat(category)] = getInitCollapseValue(category)))
|
categories.forEach((category) => (isCollapsedMap[toCat(category)] = getInitCollapseValue(category)))
|
||||||
@ -195,6 +203,10 @@
|
|||||||
varsStyle = ''
|
varsStyle = ''
|
||||||
for (const key in propsWidth) varsStyle += `--fixed-${key}: ${propsWidth[key]}px;`
|
for (const key in propsWidth) varsStyle += `--fixed-${key}: ${propsWidth[key]}px;`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const checkWidth = (key: string, result: CustomEvent): void => {
|
||||||
|
if (result !== undefined) propsWidth[key] = result.detail
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="issueslist-container" style={varsStyle}>
|
<div class="issueslist-container" style={varsStyle}>
|
||||||
@ -202,8 +214,15 @@
|
|||||||
{@const items = groupedIssues[category] ?? []}
|
{@const items = groupedIssues[category] ?? []}
|
||||||
{@const limited = limitGroup(category, groupedIssues, categoryLimit) ?? []}
|
{@const limited = limitGroup(category, groupedIssues, categoryLimit) ?? []}
|
||||||
{#if headerComponent || groupByKey === 'assignee' || category === undefined}
|
{#if headerComponent || groupByKey === 'assignee' || category === undefined}
|
||||||
|
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||||
<div class="flex-between categoryHeader row" on:click={() => handleCollapseCategory(toCat(category))}>
|
<div class="flex-between categoryHeader row" on:click={() => handleCollapseCategory(toCat(category))}>
|
||||||
<div class="flex-row-center gap-2 clear-mins">
|
<div class="flex-row-center gap-2 clear-mins">
|
||||||
|
<FixedColumn
|
||||||
|
width={propsWidth.groupBy}
|
||||||
|
key={'groupBy'}
|
||||||
|
justify={'left'}
|
||||||
|
on:update={(result) => checkWidth('groupBy', result)}
|
||||||
|
>
|
||||||
{#if groupByKey === 'assignee' && personPresenter}
|
{#if groupByKey === 'assignee' && personPresenter}
|
||||||
<svelte:component
|
<svelte:component
|
||||||
this={personPresenter.presenter}
|
this={personPresenter.presenter}
|
||||||
@ -236,6 +255,15 @@
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
|
</FixedColumn>
|
||||||
|
<FixedColumn
|
||||||
|
width={propsWidth.statistics}
|
||||||
|
key={'statistics'}
|
||||||
|
justify={'left'}
|
||||||
|
on:update={(result) => checkWidth('statistics', result)}
|
||||||
|
>
|
||||||
|
<IssueStatistics issues={groupedIssues[category]} />
|
||||||
|
</FixedColumn>
|
||||||
{#if limited.length < items.length}
|
{#if limited.length < items.length}
|
||||||
<div class="counter">
|
<div class="counter">
|
||||||
{limited.length}
|
{limited.length}
|
||||||
|
@ -17,7 +17,8 @@
|
|||||||
import { SortingOrder, WithLookup } from '@hcengineering/core'
|
import { SortingOrder, WithLookup } from '@hcengineering/core'
|
||||||
import presentation, { Card, createQuery } from '@hcengineering/presentation'
|
import presentation, { Card, createQuery } from '@hcengineering/presentation'
|
||||||
import { Issue, IssueStatus, Team } from '@hcengineering/tracker'
|
import { Issue, IssueStatus, Team } from '@hcengineering/tracker'
|
||||||
import { Button, EditBox, EditStyle, IconAdd, Label, showPopup } from '@hcengineering/ui'
|
import { Button, EditStyle, eventToHTMLElement, IconAdd, Label, showPopup } from '@hcengineering/ui'
|
||||||
|
import EditBoxPopup from '@hcengineering/view-resources/src/components/EditBoxPopup.svelte'
|
||||||
import { createEventDispatcher } from 'svelte'
|
import { createEventDispatcher } from 'svelte'
|
||||||
import tracker from '../../../plugin'
|
import tracker from '../../../plugin'
|
||||||
import IssuePresenter from '../IssuePresenter.svelte'
|
import IssuePresenter from '../IssuePresenter.svelte'
|
||||||
@ -26,7 +27,7 @@
|
|||||||
import TimeSpendReportPopup from './TimeSpendReportPopup.svelte'
|
import TimeSpendReportPopup from './TimeSpendReportPopup.svelte'
|
||||||
import TimeSpendReports from './TimeSpendReports.svelte'
|
import TimeSpendReports from './TimeSpendReports.svelte'
|
||||||
|
|
||||||
export let value: string | number | undefined
|
export let value: number
|
||||||
export let format: 'text' | 'password' | 'number'
|
export let format: 'text' | 'password' | 'number'
|
||||||
export let kind: EditStyle = 'search-style'
|
export let kind: EditStyle = 'search-style'
|
||||||
export let object: Issue
|
export let object: Issue
|
||||||
@ -35,10 +36,6 @@
|
|||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
function _onkeypress (ev: KeyboardEvent) {
|
|
||||||
if (ev.key === 'Enter') dispatch('close', _value)
|
|
||||||
}
|
|
||||||
|
|
||||||
$: childIds = Array.from((object.childInfo ?? []).map((it) => it.childId))
|
$: childIds = Array.from((object.childInfo ?? []).map((it) => it.childId))
|
||||||
|
|
||||||
const query = createQuery()
|
const query = createQuery()
|
||||||
@ -78,13 +75,14 @@
|
|||||||
)
|
)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||||
<Card
|
<Card
|
||||||
label={tracker.string.Estimation}
|
label={tracker.string.Estimation}
|
||||||
canSave={true}
|
canSave={true}
|
||||||
okAction={() => {
|
okAction={() => {
|
||||||
dispatch('close', _value)
|
dispatch('close', _value)
|
||||||
}}
|
}}
|
||||||
okLabel={presentation.string.Save}
|
okLabel={_value !== value ? presentation.string.Save : presentation.string.Close}
|
||||||
on:close={() => {
|
on:close={() => {
|
||||||
dispatch('close', null)
|
dispatch('close', null)
|
||||||
}}
|
}}
|
||||||
@ -92,8 +90,28 @@
|
|||||||
<svelte:fragment slot="title">
|
<svelte:fragment slot="title">
|
||||||
<div class="flex-row-center">
|
<div class="flex-row-center">
|
||||||
<Label label={tracker.string.Estimation} />
|
<Label label={tracker.string.Estimation} />
|
||||||
<div class="ml-2">
|
<div
|
||||||
<EstimationStatsPresenter value={object} />
|
class="ml-2 mr-4"
|
||||||
|
on:click={(evt) => {
|
||||||
|
showPopup(
|
||||||
|
EditBoxPopup,
|
||||||
|
{
|
||||||
|
value: _value === 0 ? undefined : _value,
|
||||||
|
format,
|
||||||
|
kind,
|
||||||
|
placeholder: tracker.string.Estimation,
|
||||||
|
maxDigitsAfterPoint: 3
|
||||||
|
},
|
||||||
|
eventToHTMLElement(evt),
|
||||||
|
(res) => {
|
||||||
|
if (typeof res === 'number') {
|
||||||
|
_value = res
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<EstimationStatsPresenter value={object} estimation={_value} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</svelte:fragment>
|
</svelte:fragment>
|
||||||
@ -101,23 +119,9 @@
|
|||||||
<svelte:fragment slot="header">
|
<svelte:fragment slot="header">
|
||||||
<IssuePresenter value={object} disableClick />
|
<IssuePresenter value={object} disableClick />
|
||||||
</svelte:fragment>
|
</svelte:fragment>
|
||||||
|
|
||||||
<div class="header no-border flex-col p-1">
|
<div class="header no-border flex-col p-1">
|
||||||
<div class="flex-row-center flex-between">
|
<div class="flex-row-center flex-between" />
|
||||||
<EditBox
|
|
||||||
bind:value={_value}
|
|
||||||
{format}
|
|
||||||
{kind}
|
|
||||||
placeholder={tracker.string.Estimation}
|
|
||||||
focus
|
|
||||||
maxDigitsAfterPoint={3}
|
|
||||||
on:keypress={_onkeypress}
|
|
||||||
on:change={() => {
|
|
||||||
if (typeof _value === 'number') {
|
|
||||||
object.estimation = _value
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
{#if currentTeam && issueStatuses}
|
{#if currentTeam && issueStatuses}
|
||||||
<SubIssuesEstimations
|
<SubIssuesEstimations
|
||||||
|
@ -21,6 +21,9 @@
|
|||||||
import TimePresenter from './TimePresenter.svelte'
|
import TimePresenter from './TimePresenter.svelte'
|
||||||
|
|
||||||
export let value: Issue | AttachedData<Issue>
|
export let value: Issue | AttachedData<Issue>
|
||||||
|
export let estimation: number | undefined = undefined
|
||||||
|
|
||||||
|
$: _estimation = estimation ?? value.estimation
|
||||||
|
|
||||||
$: workDayLength = value.workDayLength
|
$: workDayLength = value.workDayLength
|
||||||
$: childReportTime = floorFractionDigits(
|
$: childReportTime = floorFractionDigits(
|
||||||
@ -30,11 +33,12 @@
|
|||||||
$: childEstimationTime = (value.childInfo ?? []).map((it) => it.estimation).reduce((a, b) => a + b, 0)
|
$: childEstimationTime = (value.childInfo ?? []).map((it) => it.estimation).reduce((a, b) => a + b, 0)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||||
<div class="estimation-container" on:click>
|
<div class="estimation-container" on:click>
|
||||||
<div class="icon">
|
<div class="icon">
|
||||||
<EstimationProgressCircle
|
<EstimationProgressCircle
|
||||||
value={Math.max(value.reportedTime, childReportTime)}
|
value={Math.max(value.reportedTime, childReportTime)}
|
||||||
max={childEstimationTime || value.estimation}
|
max={childEstimationTime || _estimation}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<span class="overflow-label label flex-row-center flex-nowrap text-md">
|
<span class="overflow-label label flex-row-center flex-nowrap text-md">
|
||||||
@ -61,21 +65,21 @@
|
|||||||
{/if}
|
{/if}
|
||||||
{#if childEstimationTime}
|
{#if childEstimationTime}
|
||||||
{@const childEstTime = Math.round(childEstimationTime)}
|
{@const childEstTime = Math.round(childEstimationTime)}
|
||||||
{@const estimationDiff = childEstTime - Math.round(value.estimation)}
|
{@const estimationDiff = childEstTime - Math.round(_estimation)}
|
||||||
{#if estimationDiff !== 0}
|
{#if estimationDiff !== 0}
|
||||||
<div class="flex flex-nowrap mr-1" class:showWarning={estimationDiff !== 0}>
|
<div class="flex flex-nowrap mr-1" class:showWarning={estimationDiff !== 0}>
|
||||||
<TimePresenter value={childEstTime} {workDayLength} />
|
<TimePresenter value={childEstTime} {workDayLength} />
|
||||||
</div>
|
</div>
|
||||||
{#if value.estimation !== 0}
|
{#if _estimation !== 0}
|
||||||
<div class="romColor">
|
<div class="romColor">
|
||||||
(<TimePresenter value={value.estimation} {workDayLength} />)
|
(<TimePresenter value={_estimation} {workDayLength} />)
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{:else}
|
{:else}
|
||||||
<TimePresenter value={value.estimation} {workDayLength} />
|
<TimePresenter value={_estimation} {workDayLength} />
|
||||||
{/if}
|
{/if}
|
||||||
{:else}
|
{:else}
|
||||||
<TimePresenter value={value.estimation} {workDayLength} />
|
<TimePresenter value={_estimation} {workDayLength} />
|
||||||
{/if}
|
{/if}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
@ -34,7 +34,7 @@
|
|||||||
const listProvider = new ListSelectionProvider((offset: 1 | -1 | 0, of?: Doc, dir?: SelectDirection) => {})
|
const listProvider = new ListSelectionProvider((offset: 1 | -1 | 0, of?: Doc, dir?: SelectDirection) => {})
|
||||||
|
|
||||||
let varsStyle: string = ''
|
let varsStyle: string = ''
|
||||||
const propsWidth: Record<string, number> = { issue: 0 }
|
const propsWidth: Record<string, number> = { issue: 0, estimation: 0, assignee: 0 }
|
||||||
$: if (propsWidth) {
|
$: if (propsWidth) {
|
||||||
varsStyle = ''
|
varsStyle = ''
|
||||||
for (const key in propsWidth) varsStyle += `--fixed-${key}: ${propsWidth[key]}px;`
|
for (const key in propsWidth) varsStyle += `--fixed-${key}: ${propsWidth[key]}px;`
|
||||||
@ -59,7 +59,7 @@
|
|||||||
listProvider.updateFocus(issue)
|
listProvider.updateFocus(issue)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div class="flex-row-center clear-mins gap-2 p-2">
|
<div class="flex-row-center clear-mins gap-2 p-2 flex-grow">
|
||||||
<span class="issuePresenter">
|
<span class="issuePresenter">
|
||||||
<FixedColumn
|
<FixedColumn
|
||||||
width={propsWidth.issue}
|
width={propsWidth.issue}
|
||||||
@ -76,16 +76,30 @@
|
|||||||
{issue.title}
|
{issue.title}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-center flex-no-shrink gap-2">
|
|
||||||
|
<FixedColumn
|
||||||
|
width={propsWidth.assignee}
|
||||||
|
key={'assignee'}
|
||||||
|
justify={'right'}
|
||||||
|
on:update={(result) => checkWidth('assignee', result)}
|
||||||
|
>
|
||||||
<UserBox
|
<UserBox
|
||||||
|
width={'100%'}
|
||||||
label={tracker.string.Assignee}
|
label={tracker.string.Assignee}
|
||||||
_class={contact.class.Employee}
|
_class={contact.class.Employee}
|
||||||
value={issue.assignee}
|
value={issue.assignee}
|
||||||
readonly
|
readonly
|
||||||
showNavigate={false}
|
showNavigate={false}
|
||||||
/>
|
/>
|
||||||
|
</FixedColumn>
|
||||||
|
<FixedColumn
|
||||||
|
width={propsWidth.estimation}
|
||||||
|
key={'estimation'}
|
||||||
|
justify={'left'}
|
||||||
|
on:update={(result) => checkWidth('estimation', result)}
|
||||||
|
>
|
||||||
<EstimationEditor value={issue} kind={'list'} />
|
<EstimationEditor value={issue} kind={'list'} />
|
||||||
</div>
|
</FixedColumn>
|
||||||
</div>
|
</div>
|
||||||
</svelte:fragment>
|
</svelte:fragment>
|
||||||
</ListView>
|
</ListView>
|
||||||
|
@ -16,7 +16,8 @@
|
|||||||
import { Ref, SortingOrder, WithLookup } from '@hcengineering/core'
|
import { Ref, SortingOrder, WithLookup } from '@hcengineering/core'
|
||||||
import { createQuery } from '@hcengineering/presentation'
|
import { createQuery } from '@hcengineering/presentation'
|
||||||
import { Issue, IssueStatus, Team } from '@hcengineering/tracker'
|
import { Issue, IssueStatus, Team } from '@hcengineering/tracker'
|
||||||
import { Label, Scroller, Spinner } from '@hcengineering/ui'
|
import { Scroller, Spinner } from '@hcengineering/ui'
|
||||||
|
import Expandable from '@hcengineering/ui/src/components/Expandable.svelte'
|
||||||
import tracker from '../../../plugin'
|
import tracker from '../../../plugin'
|
||||||
import EstimationSubIssueList from './EstimationSubIssueList.svelte'
|
import EstimationSubIssueList from './EstimationSubIssueList.svelte'
|
||||||
|
|
||||||
@ -37,12 +38,16 @@
|
|||||||
|
|
||||||
{#if subIssues && issueStatuses}
|
{#if subIssues && issueStatuses}
|
||||||
{#if hasSubIssues}
|
{#if hasSubIssues}
|
||||||
<Label label={tracker.string.ChildEstimation} />: {total}
|
<Expandable label={tracker.string.ChildEstimation}>
|
||||||
|
<svelte:fragment slot="title">
|
||||||
|
: {total}
|
||||||
|
</svelte:fragment>
|
||||||
<div class="h-50">
|
<div class="h-50">
|
||||||
<Scroller>
|
<Scroller>
|
||||||
<EstimationSubIssueList issues={subIssues} {teams} />
|
<EstimationSubIssueList issues={subIssues} {teams} />
|
||||||
</Scroller>
|
</Scroller>
|
||||||
</div>
|
</div>
|
||||||
|
</Expandable>
|
||||||
{/if}
|
{/if}
|
||||||
{:else}
|
{:else}
|
||||||
<div class="flex-center pt-3">
|
<div class="flex-center pt-3">
|
||||||
|
@ -30,7 +30,7 @@
|
|||||||
|
|
||||||
export let value: TimeSpendReport | undefined
|
export let value: TimeSpendReport | undefined
|
||||||
export let placeholder: IntlString = tracker.string.TimeSpendReportValue
|
export let placeholder: IntlString = tracker.string.TimeSpendReportValue
|
||||||
export let defaultTimeReportDay = TimeReportDayType.PreviousWorkDay
|
export let defaultTimeReportDay: TimeReportDayType = TimeReportDayType.PreviousWorkDay
|
||||||
|
|
||||||
const data = {
|
const data = {
|
||||||
date: value?.date ?? getTimeReportDate(defaultTimeReportDay),
|
date: value?.date ?? getTimeReportDate(defaultTimeReportDay),
|
||||||
|
@ -16,7 +16,7 @@
|
|||||||
import { DocumentQuery, Ref, SortingOrder } from '@hcengineering/core'
|
import { DocumentQuery, Ref, SortingOrder } from '@hcengineering/core'
|
||||||
import { createQuery } from '@hcengineering/presentation'
|
import { createQuery } from '@hcengineering/presentation'
|
||||||
import { Issue, Team, TimeSpendReport } from '@hcengineering/tracker'
|
import { Issue, Team, TimeSpendReport } from '@hcengineering/tracker'
|
||||||
import { floorFractionDigits, Label, Scroller, Spinner } from '@hcengineering/ui'
|
import { Expandable, floorFractionDigits, Label, Scroller, Spinner } from '@hcengineering/ui'
|
||||||
import tracker from '../../../plugin'
|
import tracker from '../../../plugin'
|
||||||
import TimePresenter from './TimePresenter.svelte'
|
import TimePresenter from './TimePresenter.svelte'
|
||||||
import TimeSpendReportsList from './TimeSpendReportsList.svelte'
|
import TimeSpendReportsList from './TimeSpendReportsList.svelte'
|
||||||
@ -42,15 +42,19 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if reports}
|
{#if reports}
|
||||||
|
<Expandable expanded={true}>
|
||||||
|
<svelte:fragment slot="title">
|
||||||
<span class="overflow-label flex-nowrap">
|
<span class="overflow-label flex-nowrap">
|
||||||
<Label label={tracker.string.ReportedTime} />: <TimePresenter value={reportedTime} {workDayLength} />
|
<Label label={tracker.string.ReportedTime} />: <TimePresenter value={reportedTime} {workDayLength} />
|
||||||
<Label label={tracker.string.TimeSpendReports} />: <TimePresenter value={total} {workDayLength} />
|
<Label label={tracker.string.TimeSpendReports} />: <TimePresenter value={total} {workDayLength} />
|
||||||
</span>
|
</span>
|
||||||
|
</svelte:fragment>
|
||||||
<div class="h-50">
|
<div class="h-50">
|
||||||
<Scroller>
|
<Scroller>
|
||||||
<TimeSpendReportsList {reports} {teams} />
|
<TimeSpendReportsList {reports} {teams} />
|
||||||
</Scroller>
|
</Scroller>
|
||||||
</div>
|
</div>
|
||||||
|
</Expandable>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="flex-center pt-3">
|
<div class="flex-center pt-3">
|
||||||
<Spinner />
|
<Spinner />
|
||||||
|
@ -36,7 +36,7 @@
|
|||||||
const listProvider = new ListSelectionProvider((offset: 1 | -1 | 0, of?: Doc, dir?: SelectDirection) => {})
|
const listProvider = new ListSelectionProvider((offset: 1 | -1 | 0, of?: Doc, dir?: SelectDirection) => {})
|
||||||
|
|
||||||
let varsStyle: string = ''
|
let varsStyle: string = ''
|
||||||
const propsWidth: Record<string, number> = { issue: 0 }
|
const propsWidth: Record<string, number> = { issue: 0, assignee: 0, reported: 0, date: 0 }
|
||||||
$: if (propsWidth) {
|
$: if (propsWidth) {
|
||||||
varsStyle = ''
|
varsStyle = ''
|
||||||
for (const key in propsWidth) varsStyle += `--fixed-${key}: ${propsWidth[key]}px;`
|
for (const key in propsWidth) varsStyle += `--fixed-${key}: ${propsWidth[key]}px;`
|
||||||
@ -82,7 +82,7 @@
|
|||||||
}}
|
}}
|
||||||
on:click={(evt) => editSpendReport(evt, report, currentTeam?.defaultTimeReportDay)}
|
on:click={(evt) => editSpendReport(evt, report, currentTeam?.defaultTimeReportDay)}
|
||||||
>
|
>
|
||||||
<div class="flex-row-center clear-mins gap-2 p-2">
|
<div class="flex-row-center clear-mins gap-2 p-2 flex-grow">
|
||||||
<span class="issuePresenter">
|
<span class="issuePresenter">
|
||||||
<FixedColumn
|
<FixedColumn
|
||||||
width={propsWidth.issue}
|
width={propsWidth.issue}
|
||||||
@ -101,17 +101,40 @@
|
|||||||
</span>
|
</span>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-center flex-no-shrink gap-2">
|
<FixedColumn
|
||||||
|
width={propsWidth.assignee}
|
||||||
|
key={'assignee'}
|
||||||
|
justify={'left'}
|
||||||
|
on:update={(result) => checkWidth('assignee', result)}
|
||||||
|
>
|
||||||
<UserBox
|
<UserBox
|
||||||
|
width={'100%'}
|
||||||
label={tracker.string.Assignee}
|
label={tracker.string.Assignee}
|
||||||
_class={contact.class.Employee}
|
_class={contact.class.Employee}
|
||||||
value={report.employee}
|
value={report.employee}
|
||||||
readonly
|
readonly
|
||||||
showNavigate={false}
|
showNavigate={false}
|
||||||
/>
|
/>
|
||||||
|
</FixedColumn>
|
||||||
|
|
||||||
|
<FixedColumn
|
||||||
|
width={propsWidth.reported}
|
||||||
|
key={'reported'}
|
||||||
|
justify={'center'}
|
||||||
|
on:update={(result) => checkWidth('reported', result)}
|
||||||
|
>
|
||||||
|
<div class="p-1">
|
||||||
<TimePresenter value={report.value} workDayLength={currentTeam?.workDayLength} />
|
<TimePresenter value={report.value} workDayLength={currentTeam?.workDayLength} />
|
||||||
<DatePresenter value={report.date} />
|
|
||||||
</div>
|
</div>
|
||||||
|
</FixedColumn>
|
||||||
|
<FixedColumn
|
||||||
|
width={propsWidth.date}
|
||||||
|
key={'date'}
|
||||||
|
justify={'left'}
|
||||||
|
on:update={(result) => checkWidth('date', result)}
|
||||||
|
>
|
||||||
|
<DatePresenter value={report.date} />
|
||||||
|
</FixedColumn>
|
||||||
</div>
|
</div>
|
||||||
</svelte:fragment>
|
</svelte:fragment>
|
||||||
</ListView>
|
</ListView>
|
||||||
|
@ -0,0 +1,108 @@
|
|||||||
|
<!--
|
||||||
|
// Copyright © 2022 Hardcore Engineering Inc.
|
||||||
|
//
|
||||||
|
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License. You may
|
||||||
|
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
//
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
-->
|
||||||
|
<script lang="ts">
|
||||||
|
import { Ref, WithLookup } from '@hcengineering/core'
|
||||||
|
import { createQuery } from '@hcengineering/presentation'
|
||||||
|
import { Issue, IssueStatus, WorkDayLength } from '@hcengineering/tracker'
|
||||||
|
import { floorFractionDigits, Label } from '@hcengineering/ui'
|
||||||
|
import tracker from '../../plugin'
|
||||||
|
import EstimationProgressCircle from '../issues/timereport/EstimationProgressCircle.svelte'
|
||||||
|
import TimePresenter from '../issues/timereport/TimePresenter.svelte'
|
||||||
|
export let issues: Issue[] | undefined = undefined
|
||||||
|
export let capacity: number | undefined = undefined
|
||||||
|
export let workDayLength: WorkDayLength = WorkDayLength.EIGHT_HOURS
|
||||||
|
|
||||||
|
$: ids = new Set(issues?.map((it) => it._id) ?? [])
|
||||||
|
|
||||||
|
$: noParents = issues?.filter((it) => !ids.has(it.attachedTo as Ref<Issue>))
|
||||||
|
|
||||||
|
$: rootNoBacklogIssues = noParents?.filter(
|
||||||
|
(it) => issueStatuses.get(it.status)?.category !== tracker.issueStatusCategory.Backlog
|
||||||
|
)
|
||||||
|
|
||||||
|
const statuses = createQuery()
|
||||||
|
let issueStatuses: Map<Ref<IssueStatus>, WithLookup<IssueStatus>> = new Map()
|
||||||
|
$: if (noParents !== undefined) {
|
||||||
|
statuses.query(tracker.class.IssueStatus, { _id: { $in: Array.from(noParents.map((it) => it.status)) } }, (res) => {
|
||||||
|
issueStatuses = new Map(res.map((it) => [it._id, it]))
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
statuses.unsubscribe()
|
||||||
|
}
|
||||||
|
|
||||||
|
$: totalEstimation = floorFractionDigits(
|
||||||
|
(rootNoBacklogIssues ?? [{ estimation: 0, childInfo: [] } as unknown as Issue])
|
||||||
|
.map((it) => {
|
||||||
|
const cat = issueStatuses.get(it.status)?.category
|
||||||
|
|
||||||
|
let retEst = it.estimation
|
||||||
|
if (it.childInfo?.length > 0) {
|
||||||
|
const cEstimation = it.childInfo.map((ct) => ct.estimation).reduce((a, b) => a + b, 0)
|
||||||
|
const cReported = it.childInfo.map((ct) => ct.reportedTime).reduce((a, b) => a + b, 0)
|
||||||
|
if (cEstimation !== 0) {
|
||||||
|
retEst = cEstimation
|
||||||
|
if (cat === tracker.issueStatusCategory.Completed || cat === tracker.issueStatusCategory.Canceled) {
|
||||||
|
if (cReported < cEstimation) {
|
||||||
|
retEst = cReported
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (cat === tracker.issueStatusCategory.Completed || cat === tracker.issueStatusCategory.Canceled) {
|
||||||
|
if (it.reportedTime < it.estimation) {
|
||||||
|
return it.reportedTime
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return retEst
|
||||||
|
})
|
||||||
|
.reduce((it, cur) => {
|
||||||
|
return it + cur
|
||||||
|
}, 0),
|
||||||
|
3
|
||||||
|
)
|
||||||
|
$: totalReported = floorFractionDigits(
|
||||||
|
(noParents ?? [{ reportedTime: 0, childInfo: [] } as unknown as Issue])
|
||||||
|
.map((it) => {
|
||||||
|
if (it.childInfo?.length > 0) {
|
||||||
|
const cReported = it.childInfo.map((ct) => ct.reportedTime).reduce((a, b) => a + b, 0)
|
||||||
|
if (cReported !== 0) {
|
||||||
|
return cReported + it.reportedTime
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return it.reportedTime
|
||||||
|
})
|
||||||
|
.reduce((it, cur) => {
|
||||||
|
return it + cur
|
||||||
|
}),
|
||||||
|
3
|
||||||
|
)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if issues}
|
||||||
|
<!-- <Label label={tracker.string.SprintDay} value={}/> -->
|
||||||
|
<div class="flex-row-center flex-no-shrink h-6" class:showWarning={totalEstimation > (capacity ?? 0)}>
|
||||||
|
<EstimationProgressCircle value={totalReported} max={totalEstimation} />
|
||||||
|
<div class="w-2 min-w-2" />
|
||||||
|
{#if totalReported > 0}
|
||||||
|
<TimePresenter value={totalReported} {workDayLength} />
|
||||||
|
/
|
||||||
|
{/if}
|
||||||
|
<TimePresenter value={totalEstimation} {workDayLength} />
|
||||||
|
{#if capacity}
|
||||||
|
<Label label={tracker.string.CapacityValue} params={{ value: capacity }} />
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/if}
|
@ -13,17 +13,15 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Ref, WithLookup } from '@hcengineering/core'
|
import { Ref } from '@hcengineering/core'
|
||||||
import { IntlString } from '@hcengineering/platform'
|
import { IntlString } from '@hcengineering/platform'
|
||||||
import { createQuery, getClient } from '@hcengineering/presentation'
|
import { createQuery, getClient } from '@hcengineering/presentation'
|
||||||
import { Issue, IssueStatus, IssueTemplate, Sprint, Team } from '@hcengineering/tracker'
|
import { Issue, IssueTemplate, Sprint, Team } from '@hcengineering/tracker'
|
||||||
import { ButtonKind, ButtonSize, ButtonShape, floorFractionDigits } from '@hcengineering/ui'
|
import { ButtonKind, ButtonShape, ButtonSize, deviceOptionsStore as deviceInfo } from '@hcengineering/ui'
|
||||||
import { Label, deviceOptionsStore as deviceInfo } from '@hcengineering/ui'
|
|
||||||
import DatePresenter from '@hcengineering/ui/src/components/calendar/DatePresenter.svelte'
|
import DatePresenter from '@hcengineering/ui/src/components/calendar/DatePresenter.svelte'
|
||||||
import { activeSprint } from '../../issues'
|
import { activeSprint } from '../../issues'
|
||||||
import tracker from '../../plugin'
|
import tracker from '../../plugin'
|
||||||
import { getDayOfSprint } from '../../utils'
|
import { getDayOfSprint } from '../../utils'
|
||||||
import EstimationProgressCircle from '../issues/timereport/EstimationProgressCircle.svelte'
|
|
||||||
import TimePresenter from '../issues/timereport/TimePresenter.svelte'
|
import TimePresenter from '../issues/timereport/TimePresenter.svelte'
|
||||||
import SprintSelector from './SprintSelector.svelte'
|
import SprintSelector from './SprintSelector.svelte'
|
||||||
|
|
||||||
@ -38,7 +36,7 @@
|
|||||||
export let justify: 'left' | 'center' = 'left'
|
export let justify: 'left' | 'center' = 'left'
|
||||||
export let width: string | undefined = '100%'
|
export let width: string | undefined = '100%'
|
||||||
export let onlyIcon: boolean = false
|
export let onlyIcon: boolean = false
|
||||||
export let issues: Issue[] | undefined = undefined
|
|
||||||
export let groupBy: string | undefined = undefined
|
export let groupBy: string | undefined = undefined
|
||||||
export let enlargedText: boolean = false
|
export let enlargedText: boolean = false
|
||||||
|
|
||||||
@ -59,75 +57,9 @@
|
|||||||
await client.update(value, { sprint: newSprintId })
|
await client.update(value, { sprint: newSprintId })
|
||||||
}
|
}
|
||||||
|
|
||||||
$: ids = new Set(issues?.map((it) => it._id) ?? [])
|
|
||||||
|
|
||||||
$: noParents = issues?.filter((it) => !ids.has(it.attachedTo as Ref<Issue>))
|
|
||||||
|
|
||||||
$: rootNoBacklogIssues = noParents?.filter(
|
|
||||||
(it) => issueStatuses.get(it.status)?.category !== tracker.issueStatusCategory.Backlog
|
|
||||||
)
|
|
||||||
|
|
||||||
const statuses = createQuery()
|
|
||||||
let issueStatuses: Map<Ref<IssueStatus>, WithLookup<IssueStatus>> = new Map()
|
|
||||||
$: if (noParents !== undefined) {
|
|
||||||
statuses.query(tracker.class.IssueStatus, { _id: { $in: Array.from(noParents.map((it) => it.status)) } }, (res) => {
|
|
||||||
issueStatuses = new Map(res.map((it) => [it._id, it]))
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
statuses.unsubscribe()
|
|
||||||
}
|
|
||||||
|
|
||||||
$: totalEstimation = floorFractionDigits(
|
|
||||||
(rootNoBacklogIssues ?? [{ estimation: 0, childInfo: [] } as unknown as Issue])
|
|
||||||
.map((it) => {
|
|
||||||
const cat = issueStatuses.get(it.status)?.category
|
|
||||||
|
|
||||||
let retEst = it.estimation
|
|
||||||
if (it.childInfo?.length > 0) {
|
|
||||||
const cEstimation = it.childInfo.map((ct) => ct.estimation).reduce((a, b) => a + b, 0)
|
|
||||||
const cReported = it.childInfo.map((ct) => ct.reportedTime).reduce((a, b) => a + b, 0)
|
|
||||||
if (cEstimation !== 0) {
|
|
||||||
retEst = cEstimation
|
|
||||||
if (cat === tracker.issueStatusCategory.Completed || cat === tracker.issueStatusCategory.Canceled) {
|
|
||||||
if (cReported < cEstimation) {
|
|
||||||
retEst = cReported
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (cat === tracker.issueStatusCategory.Completed || cat === tracker.issueStatusCategory.Canceled) {
|
|
||||||
if (it.reportedTime < it.estimation) {
|
|
||||||
return it.reportedTime
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return retEst
|
|
||||||
})
|
|
||||||
.reduce((it, cur) => {
|
|
||||||
return it + cur
|
|
||||||
}, 0),
|
|
||||||
3
|
|
||||||
)
|
|
||||||
$: totalReported = floorFractionDigits(
|
|
||||||
(noParents ?? [{ reportedTime: 0, childInfo: [] } as unknown as Issue])
|
|
||||||
.map((it) => {
|
|
||||||
if (it.childInfo?.length > 0) {
|
|
||||||
const cReported = it.childInfo.map((ct) => ct.reportedTime).reduce((a, b) => a + b, 0)
|
|
||||||
if (cReported !== 0) {
|
|
||||||
return cReported + it.reportedTime
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return it.reportedTime
|
|
||||||
})
|
|
||||||
.reduce((it, cur) => {
|
|
||||||
return it + cur
|
|
||||||
}),
|
|
||||||
3
|
|
||||||
)
|
|
||||||
|
|
||||||
const sprintQuery = createQuery()
|
const sprintQuery = createQuery()
|
||||||
let sprint: Sprint | undefined
|
let sprint: Sprint | undefined
|
||||||
$: if (issues !== undefined && value.sprint) {
|
$: if (value.sprint) {
|
||||||
sprintQuery.query(tracker.class.Sprint, { _id: value.sprint }, (res) => {
|
sprintQuery.query(tracker.class.Sprint, { _id: value.sprint }, (res) => {
|
||||||
sprint = res.shift()
|
sprint = res.shift()
|
||||||
})
|
})
|
||||||
@ -161,7 +93,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if sprint || issues}
|
{#if sprint}
|
||||||
<div class="flex-row-center" class:minus-margin-space={kind === 'list-header'} class:text-sm={twoRows}>
|
<div class="flex-row-center" class:minus-margin-space={kind === 'list-header'} class:text-sm={twoRows}>
|
||||||
{#if sprint}
|
{#if sprint}
|
||||||
{@const now = Date.now()}
|
{@const now = Date.now()}
|
||||||
@ -181,26 +113,6 @@
|
|||||||
/
|
/
|
||||||
<TimePresenter value={sprintDaysTo} {workDayLength} />
|
<TimePresenter value={sprintDaysTo} {workDayLength} />
|
||||||
{/if}
|
{/if}
|
||||||
{#if issues}
|
|
||||||
<!-- <Label label={tracker.string.SprintDay} value={}/> -->
|
|
||||||
<div
|
|
||||||
class="flex-row-center flex-no-shrink h-6"
|
|
||||||
class:ml-2={sprint}
|
|
||||||
class:ml-0-5={!sprint}
|
|
||||||
class:showWarning={totalEstimation > (sprint?.capacity ?? 0)}
|
|
||||||
>
|
|
||||||
<EstimationProgressCircle value={totalReported} max={totalEstimation} />
|
|
||||||
<div class="w-2 min-w-2" />
|
|
||||||
{#if totalReported > 0}
|
|
||||||
<TimePresenter value={totalReported} {workDayLength} />
|
|
||||||
/
|
|
||||||
{/if}
|
|
||||||
<TimePresenter value={totalEstimation} {workDayLength} />
|
|
||||||
{#if sprint?.capacity}
|
|
||||||
<Label label={tracker.string.CapacityValue} params={{ value: sprint?.capacity }} />
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
@ -16,7 +16,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { IntlString } from '@hcengineering/platform'
|
import type { IntlString } from '@hcengineering/platform'
|
||||||
import { createEventDispatcher } from 'svelte'
|
import { createEventDispatcher } from 'svelte'
|
||||||
import { EditBox, resizeObserver } from '@hcengineering/ui'
|
import { Button, EditBox, IconCheck, resizeObserver } from '@hcengineering/ui'
|
||||||
import type { EditStyle } from '@hcengineering/ui'
|
import type { EditStyle } from '@hcengineering/ui'
|
||||||
|
|
||||||
export let value: string | number | undefined
|
export let value: string | number | undefined
|
||||||
@ -32,7 +32,12 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="selectPopup" use:resizeObserver={() => dispatch('changeContent')}>
|
<div class="selectPopup" use:resizeObserver={() => dispatch('changeContent')}>
|
||||||
<div class="header no-border">
|
<div class="header no-border flex-row-center">
|
||||||
|
<div class="flex-grow">
|
||||||
<EditBox bind:value {placeholder} {format} {kind} focus on:keypress={_onkeypress} />
|
<EditBox bind:value {placeholder} {format} {kind} focus on:keypress={_onkeypress} />
|
||||||
</div>
|
</div>
|
||||||
|
<div class="p-1">
|
||||||
|
<Button icon={IconCheck} size={'small'} on:click={() => dispatch('close', value)} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -17,14 +17,14 @@
|
|||||||
import { resizeObserver } from '@hcengineering/ui'
|
import { resizeObserver } from '@hcengineering/ui'
|
||||||
import { createEventDispatcher } from 'svelte'
|
import { createEventDispatcher } from 'svelte'
|
||||||
|
|
||||||
export let width: number = 0
|
export let width: number | undefined = 0
|
||||||
export let key: string
|
export let key: string
|
||||||
export let justify: string = ''
|
export let justify: string = ''
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
let cWidth: number
|
let cWidth: number = 0
|
||||||
$: if (cWidth > width) {
|
$: if (cWidth > (width ?? 0)) {
|
||||||
width = cWidth
|
width = cWidth
|
||||||
dispatch('update', cWidth)
|
dispatch('update', cWidth)
|
||||||
}
|
}
|
||||||
@ -34,7 +34,9 @@
|
|||||||
class="flex-no-shrink"
|
class="flex-no-shrink"
|
||||||
style="{justify !== '' ? `text-align: ${justify}; ` : ''} min-width: var(--fixed-{key});"
|
style="{justify !== '' ? `text-align: ${justify}; ` : ''} min-width: var(--fixed-{key});"
|
||||||
use:resizeObserver={(element) => {
|
use:resizeObserver={(element) => {
|
||||||
|
if (element.clientWidth > cWidth) {
|
||||||
cWidth = element.clientWidth
|
cWidth = element.clientWidth
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<slot />
|
<slot />
|
||||||
|
Loading…
Reference in New Issue
Block a user