Tracker: Issue List - DueDate presenter (#1393)

This commit is contained in:
Artyom Grigorovich
2022-04-14 13:07:07 +07:00
committed by GitHub
parent 4ba579b517
commit e83ffd3804
24 changed files with 416 additions and 129 deletions
@@ -49,11 +49,14 @@
<Scroller>
<IssuesList
_class={tracker.class.Issue}
config={[
leftItemsConfig={[
{ key: '', presenter: tracker.component.PriorityPresenter, props: { currentSpace } },
{ key: '', presenter: tracker.component.IssuePresenter, props: { currentTeam } },
{ key: '', presenter: tracker.component.StatusPresenter, props: { currentSpace } },
{ key: '', presenter: tracker.component.TitlePresenter },
{ key: '', presenter: tracker.component.TitlePresenter }
]}
rightItemsConfig={[
{ key: '', presenter: tracker.component.DueDatePresenter, props: { currentSpace } },
{ key: 'modifiedOn', presenter: tracker.component.ModificationDatePresenter },
{ key: '', presenter: tracker.component.AssigneePresenter, props: { currentSpace } }
]}
@@ -0,0 +1,85 @@
<!--
// 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 { Icon, Label, IconDPCalendarOver, IconDPCalendar } from '@anticrm/ui'
import tracker from '../../plugin'
export let formattedDate: string = ''
export let daysDifference: number = 0
export let isOverdue: boolean = false
export let iconModifier: 'warning' | 'critical' | 'overdue' | undefined = undefined
</script>
{#if formattedDate}
<div class="root">
<div
class="iconContainer"
class:mIconContainerWarning={iconModifier === 'warning'}
class:mIconContainerCritical={iconModifier === 'critical' || iconModifier === 'overdue'}
>
<Icon icon={isOverdue ? IconDPCalendarOver : IconDPCalendar} size={'small'} />
</div>
<div class="messageContainer">
<div class="title">
<Label
label={isOverdue ? tracker.string.DueDatePopupOverdueTitle : tracker.string.DueDatePopupTitle}
params={{ value: formattedDate }}
/>
</div>
<div class="description">
<Label
label={isOverdue ? tracker.string.DueDatePopupOverdueDescription : tracker.string.DueDatePopupDescription}
params={{ value: daysDifference }}
/>
</div>
</div>
</div>
{/if}
<style lang="scss">
.root {
display: flex;
width: 15rem;
}
.iconContainer {
color: var(--content-color);
margin-right: 1rem;
&.mIconContainerWarning {
color: var(--warning-color);
}
&.mIconContainerCritical {
color: var(--error-color);
}
}
.messageContainer {
display: flex;
flex-direction: column;
justify-content: center;
}
.title {
color: var(--theme-caption-color);
font-weight: 500;
}
.description {
margin-top: 0.25rem;
color: var(--theme-content-dark-color);
}
</style>
@@ -0,0 +1,89 @@
<!--
// 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, Timestamp } from '@anticrm/core'
import { Issue, IssueStatus, Team } from '@anticrm/tracker'
import { DatePresenter, Tooltip, getDaysDifference } from '@anticrm/ui'
import { getClient } from '@anticrm/presentation'
import DueDatePopup from './DueDatePopup.svelte'
import tracker from '../../plugin'
export let value: Issue
export let currentSpace: Ref<Team> | undefined = undefined
const WARNING_DAYS = 7
const client = getClient()
$: today = new Date(new Date(Date.now()).setHours(0, 0, 0, 0))
$: dueDateMs = value.dueDate
$: isOverdue = dueDateMs !== null && dueDateMs < today.getTime()
$: dueDate = dueDateMs === null ? null : new Date(dueDateMs)
$: daysDifference = dueDate === null ? null : getDaysDifference(today, dueDate)
$: iconModifier = getIconModifier(isOverdue, daysDifference)
$: formattedDate = !dueDateMs ? '' : new Date(dueDateMs).toLocaleString('default', { month: 'short', day: 'numeric' })
const handleDueDateChanged = async (event: CustomEvent<Timestamp>) => {
const newDate = event.detail
if (newDate === undefined) {
return
}
const currentIssue = await client.findOne(tracker.class.Issue, { space: currentSpace, _id: value._id })
if (currentIssue === undefined) {
return
}
await client.update(currentIssue, { dueDate: newDate })
}
const getIconModifier = (isOverdue: boolean, daysDifference: number | null) => {
if (isOverdue) {
return 'overdue' as 'overdue' // Fixes `DatePresenter` icon type issue
}
if (daysDifference === 0) {
return 'critical' as 'critical'
}
if (daysDifference !== null && daysDifference <= WARNING_DAYS) {
return 'warning' as 'warning'
}
}
$: shouldRenderPresenter = dueDateMs && value.status !== IssueStatus.Done && value.status !== IssueStatus.Canceled
</script>
{#if shouldRenderPresenter}
<Tooltip
direction={'top'}
component={DueDatePopup}
props={{
formattedDate: formattedDate,
daysDifference: daysDifference,
isOverdue: isOverdue,
iconModifier: iconModifier
}}
>
<DatePresenter
value={dueDateMs}
editable={true}
shouldShowLabel={false}
icon={iconModifier}
on:change={handleDueDateChanged}
/>
</Tooltip>
{/if}
@@ -16,14 +16,16 @@
import { Class, Doc, DocumentQuery, FindOptions, Ref, getObjectValue } from '@anticrm/core'
import { SortingOrder } from '@anticrm/core'
import { createQuery, getClient } from '@anticrm/presentation'
import { CheckBox, Loading, showPopup, Spinner, IconMoreV } from '@anticrm/ui'
import { CheckBox, Loading, showPopup, Spinner, IconMoreV, Tooltip } from '@anticrm/ui'
import { BuildModelKey } from '@anticrm/view'
import { createEventDispatcher } from 'svelte'
import { buildModel, LoadingProps, Menu } from '@anticrm/view-resources'
import tracker from '../../plugin'
export let _class: Ref<Class<Doc>>
export let baseMenuClass: Ref<Class<Doc>> | undefined = undefined
export let config: (BuildModelKey | string)[]
export let leftItemsConfig: (BuildModelKey | string)[]
export let rightItemsConfig: (BuildModelKey | string)[] | undefined = undefined
export let options: FindOptions<Doc> | undefined = undefined
export let query: DocumentQuery<Doc>
@@ -92,13 +94,20 @@
return props.length
}
const buildItemModels = async () => {
const leftModels = await buildModel({ client, _class, keys: leftItemsConfig, options })
const rightModels = rightItemsConfig && (await buildModel({ client, _class, keys: rightItemsConfig, options }))
return { leftModels, rightModels }
}
</script>
{#await buildModel({ client, _class, keys: config, options })}
{#await buildItemModels()}
{#if !isLoading}
<Loading />
{/if}
{:then attributeModels}
{:then itemModels}
<div class="listRoot">
{#if docObjects}
{#each docObjects as docObject, rowIndex (docObject._id)}
@@ -107,65 +116,80 @@
class:mListGridChecked={selectedIssueIds.has(docObject._id)}
class:mListGridFixed={rowIndex === selectedRowIndex}
>
{#each attributeModels as attributeModel, attributeModelIndex}
{#if attributeModelIndex === 0}
<div class="gridElement">
<div class="eListGridCheckBox ml-2">
<CheckBox
checked={selectedIssueIds.has(docObject._id)}
on:value={(event) => {
handleIssueSelected(docObject._id, event)
}}
/>
<div class="modelsContainer">
{#each itemModels.leftModels as attributeModel, attributeModelIndex}
{#if attributeModelIndex === 0}
<div class="gridElement">
<Tooltip direction={'bottom'} label={tracker.string.SelectIssue}>
<div class="eListGridCheckBox ml-2">
<CheckBox
checked={selectedIssueIds.has(docObject._id)}
on:value={(event) => {
handleIssueSelected(docObject._id, event)
}}
/>
</div>
</Tooltip>
<div class="priorityPresenter">
<svelte:component
this={attributeModel.presenter}
value={getObjectValue(attributeModel.key, docObject) ?? ''}
{...attributeModel.props}
/>
</div>
</div>
<div class="priorityPresenter">
{:else if attributeModelIndex === 1}
<div class="issuePresenter">
<svelte:component
this={attributeModel.presenter}
value={getObjectValue(attributeModel.key, docObject) ?? ''}
{...attributeModel.props}
/>
<div
id="context-menu"
class="eIssuePresenterContextMenu"
on:click={(event) => showMenu(event, docObject, rowIndex)}
>
<IconMoreV size={'small'} />
</div>
</div>
{:else}
<div class="gridElement">
<svelte:component
this={attributeModel.presenter}
value={getObjectValue(attributeModel.key, docObject) ?? ''}
{...attributeModel.props}
/>
</div>
</div>
{:else if attributeModelIndex === 1}
<div class="issuePresenter">
<svelte:component
this={attributeModel.presenter}
value={getObjectValue(attributeModel.key, docObject) ?? ''}
{...attributeModel.props}
/>
<div
id="context-menu"
class="eIssuePresenterContextMenu"
on:click={(event) => showMenu(event, docObject, rowIndex)}
>
<IconMoreV size={'small'} />
{/if}
{/each}
</div>
{#if itemModels.rightModels}
<div class="modelsContainer">
{#each itemModels.rightModels as attributeModel}
<div class="gridElement">
<svelte:component
this={attributeModel.presenter}
value={getObjectValue(attributeModel.key, docObject) ?? ''}
{...attributeModel.props}
/>
</div>
</div>
{:else}
<div class="gridElement">
<svelte:component
this={attributeModel.presenter}
value={getObjectValue(attributeModel.key, docObject) ?? ''}
{...attributeModel.props}
/>
</div>
{/if}
{/each}
{/each}
</div>
{/if}
</div>
{/each}
{:else if loadingProps !== undefined}
{#each Array(getLoadingElementsLength(loadingProps, options)) as _, rowIndex}
<div class="listGrid mListGridIsLoading" class:fixed={rowIndex === selectedRowIndex}>
{#each attributeModels as _, attributeModelIndex}
{#if attributeModelIndex === 0}
<div class="gridElement">
<CheckBox checked={false} />
<div class="ml-4">
<Spinner size="small" />
</div>
<div class="modelsContainer">
<div class="gridElement">
<CheckBox checked={false} />
<div class="ml-4">
<Spinner size="small" />
</div>
{/if}
{/each}
</div>
</div>
</div>
{/each}
{/if}
@@ -182,8 +206,9 @@
}
.listGrid {
display: grid;
grid-template-columns: 4rem 5rem 2rem auto 4rem 2rem;
display: flex;
align-items: center;
justify-content: space-between;
height: 3.25rem;
color: var(--theme-caption-color);
border-bottom: 1px solid var(--theme-button-border-hovered);
@@ -203,7 +228,7 @@
}
&.mListGridIsLoading {
grid-template-columns: auto;
justify-content: flex-start;
}
&:hover {
@@ -225,24 +250,20 @@
}
}
.checkBox {
.modelsContainer {
display: flex;
align-items: center;
justify-content: center;
padding: 0.03rem;
border-radius: 0.25rem;
background-color: rgba(247, 248, 248, 0.5);
opacity: 0;
&:hover {
opacity: 1;
}
}
.gridElement {
display: flex;
align-items: center;
justify-content: start;
margin-left: 0.5rem;
&:first-child {
margin-left: 0;
}
}
.priorityPresenter {
@@ -252,7 +273,7 @@
.issuePresenter {
display: flex;
align-items: center;
padding-right: 1rem;
margin-left: 0.5rem;
.eIssuePresenterContextMenu {
visibility: hidden;
@@ -16,6 +16,7 @@
import { Ref } from '@anticrm/core'
import { Issue, IssuePriority, Team } from '@anticrm/tracker'
import { getClient } from '@anticrm/presentation'
import { Tooltip } from '@anticrm/ui'
import tracker from '../../plugin'
import PrioritySelector from '../PrioritySelector.svelte'
@@ -40,10 +41,12 @@
</script>
{#if value}
<PrioritySelector
kind={'icon'}
shouldShowLabel={false}
priority={value.priority}
onPriorityChange={handlePriorityChanged}
/>
<Tooltip direction={'bottom'} label={tracker.string.SetPriority}>
<PrioritySelector
kind={'icon'}
shouldShowLabel={false}
priority={value.priority}
onPriorityChange={handlePriorityChanged}
/>
</Tooltip>
{/if}
@@ -16,6 +16,7 @@
import { Ref } from '@anticrm/core'
import { Issue, IssueStatus, Team } from '@anticrm/tracker'
import { getClient } from '@anticrm/presentation'
import { Tooltip } from '@anticrm/ui'
import tracker from '../../plugin'
import StatusSelector from '../StatusSelector.svelte'
@@ -40,5 +41,7 @@
</script>
{#if value}
<StatusSelector kind={'icon'} shouldShowLabel={false} status={value.status} onStatusChange={handleStatusChanged} />
<Tooltip direction={'bottom'} label={tracker.string.SetStatus}>
<StatusSelector kind={'icon'} shouldShowLabel={false} status={value.status} onStatusChange={handleStatusChanged} />
</Tooltip>
{/if}