Issue Status fixes (#2482)

Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
Andrey Sobolev 2022-12-29 13:57:15 +07:00 committed by GitHub
parent 9daef6444d
commit 673eec8110
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 316 additions and 197 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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