Kanban due date (#3034)

Signed-off-by: Denis Bykhov <bykhov.denis@gmail.com>
This commit is contained in:
Denis Bykhov 2023-04-21 16:29:47 +06:00 committed by GitHub
parent cb8996fc1f
commit cc5e7adb41
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 168 additions and 158 deletions

View File

@ -27,7 +27,11 @@
"AddDueDate": "Add due date",
"EditDueDate": "Edit due date",
"SaveDueDate": "Save due date",
"IssueNeedsToBeCompletedByThisDate": "Issue needs to be completed by this date",
"NeedsToBeCompletedByThisDate": "Needs to be completed by this date",
"DueDatePopupTitle": "Due on {value}",
"DueDatePopupOverdueTitle": "Was due on {value}",
"DueDatePopupDescription": "{value, plural, =0 {Today} =1 {Tomorrow} other {# days remaining}}",
"DueDatePopupOverdueDescription": "{value, plural, =1 {1 day overdue} other {# days overdue}}",
"English": "English",
"Russian": "Russian",
"MinutesBefore": "{minutes, plural, =1 {a minute before} other {# minutes before}}",

View File

@ -27,7 +27,11 @@
"AddDueDate": "Установить дату",
"EditDueDate": "Изменить дату",
"SaveDueDate": "Сохранить дату",
"IssueNeedsToBeCompletedByThisDate": "Задача должна быть завершена к этой дате",
"NeedsToBeCompletedByThisDate": "Должно быть завершено к этой дате",
"DueDatePopupTitle": "Срок {value}",
"DueDatePopupOverdueTitle": "Должно было завершится {value}",
"DueDatePopupDescription": "{value, plural, =0 {Сегодня} =1 {Завтра} other {# дней осталось}}",
"DueDatePopupOverdueDescription": "{value, plural, =1 {1 день опоздания} other {# дней опаздания}}",
"English": "Английский",
"Russian": "Русский",
"MinutesBefore": "{minutes, plural, =1 {за минуту} other {за # минут}}",

View File

@ -13,21 +13,22 @@
// limitations under the License.
-->
<script lang="ts">
import { IntlString } from '@hcengineering/platform'
import { afterUpdate, createEventDispatcher } from 'svelte'
import ui from '../../plugin'
import ActionIcon from '../ActionIcon.svelte'
import Button from '../Button.svelte'
import Icon from '../Icon.svelte'
import IconClose from '../icons/Close.svelte'
import Label from '../Label.svelte'
import { daysInMonth } from './internal/DateUtils'
import IconClose from '../icons/Close.svelte'
import MonthSquare from './MonthSquare.svelte'
import { daysInMonth } from './internal/DateUtils'
export let currentDate: Date | null
export let withTime: boolean = false
export let mondayStart: boolean = true
export let label = currentDate != null ? ui.string.EditDueDate : ui.string.AddDueDate
export let detail = ui.string.IssueNeedsToBeCompletedByThisDate
export let detail: IntlString | undefined = undefined
const dispatch = createEventDispatcher()
@ -242,8 +243,10 @@
<div class="content">
<div class="label">
<span class="bold"><Label {label} /></span>
<span class="divider">-</span>
<Label label={detail} />
{#if detail}
<span class="divider">-</span>
<Label label={detail} />
{/if}
</div>
<div class="datetime-input">

View File

@ -16,15 +16,16 @@
import type { IntlString } from '@hcengineering/platform'
import { createEventDispatcher } from 'svelte'
import { DateRangeMode } from '@hcengineering/core'
import ui from '../../plugin'
import { showPopup } from '../../popups'
import { ButtonKind } from '../../types'
import Icon from '../Icon.svelte'
import Label from '../Label.svelte'
import DatePopup from './DatePopup.svelte'
import DPCalendar from './icons/DPCalendar.svelte'
import DPCalendarOver from './icons/DPCalendarOver.svelte'
import { getMonthName } from './internal/DateUtils'
import DatePopup from './DatePopup.svelte'
import { DateRangeMode } from '@hcengineering/core'
export let value: number | null | undefined
export let mode: DateRangeMode = DateRangeMode.DATE
@ -36,9 +37,9 @@
export let showIcon = true
export let shouldShowLabel: boolean = true
export let size: 'x-small' | 'small' = 'small'
export let kind: 'transparent' | 'primary' | 'link' | 'list' = 'primary'
export let kind: ButtonKind = 'link'
export let label = ui.string.DueDate
export let detail = ui.string.IssueNeedsToBeCompletedByThisDate
export let detail = ui.string.NeedsToBeCompletedByThisDate
const dispatch = createEventDispatcher()
@ -70,8 +71,10 @@
class:h-6={size === 'small'}
class:h-3={size === 'x-small'}
class:text-xs={size === 'x-small'}
on:click={() => {
on:click={(e) => {
if (editable && !opened) {
e.stopPropagation()
e.preventDefault()
opened = true
showPopup(
DatePopup,
@ -251,6 +254,19 @@
border-color: var(--button-border-color);
}
}
&.link-bordered {
padding: 0 0.375rem;
color: var(--accent-color);
border-color: var(--divider-color);
&:hover {
color: var(--accent-color);
background-color: var(--button-bg-hover);
border-color: var(--button-border-hover);
.btn-icon {
color: var(--accent-color);
}
}
}
.time-divider {
flex-shrink: 0;

View File

@ -13,8 +13,11 @@
// limitations under the License.
-->
<script lang="ts">
import { Icon, Label, IconDPCalendarOver, IconDPCalendar } from '@hcengineering/ui'
import tracker from '../plugin'
import DPCalendar from './icons/DPCalendar.svelte'
import DPCalendarOver from './icons/DPCalendarOver.svelte'
import ui from '../../plugin'
import Icon from '../Icon.svelte'
import Label from '../Label.svelte'
export let formattedDate: string = ''
export let daysDifference: number = 0
@ -29,18 +32,18 @@
class:mIconContainerWarning={iconModifier === 'warning'}
class:mIconContainerCritical={iconModifier === 'critical' || iconModifier === 'overdue'}
>
<Icon icon={isOverdue ? IconDPCalendarOver : IconDPCalendar} size={'small'} />
<Icon icon={isOverdue ? DPCalendarOver : DPCalendar} size={'small'} />
</div>
<div class="messageContainer">
<div class="title">
<Label
label={isOverdue ? tracker.string.DueDatePopupOverdueTitle : tracker.string.DueDatePopupTitle}
label={isOverdue ? ui.string.DueDatePopupOverdueTitle : ui.string.DueDatePopupTitle}
params={{ value: formattedDate }}
/>
</div>
<div class="description">
<Label
label={isOverdue ? tracker.string.DueDatePopupOverdueDescription : tracker.string.DueDatePopupDescription}
label={isOverdue ? ui.string.DueDatePopupOverdueDescription : ui.string.DueDatePopupDescription}
params={{ value: daysDifference }}
/>
</div>

View File

@ -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 { Timestamp } from '@hcengineering/core'
import DueDatePopup from './DueDatePopup.svelte'
import { tooltip } from '../../tooltips'
import DatePresenter from './DatePresenter.svelte'
import { getDaysDifference } from './internal/DateUtils'
import { ButtonKind } from '../../types'
export let value: number | null = null
export let shouldRender: boolean = true
export let onChange: (newDate: number | null) => void
export let kind: ButtonKind = 'link'
export let editable: boolean = true
const today = new Date(new Date(Date.now()).setHours(0, 0, 0, 0))
$: isOverdue = value !== null && value < today.getTime()
$: dueDate = value === null ? null : new Date(value)
$: daysDifference = dueDate === null ? null : getDaysDifference(today, dueDate)
$: iconModifier = getDueDateIconModifier(isOverdue, daysDifference)
let formattedDate = getFormattedDate(value)
$: formattedDate = getFormattedDate(value)
function getFormattedDate (value: number | null): string {
return !value ? '' : new Date(value).toLocaleString('default', { month: 'short', day: 'numeric' })
}
const handleDueDateChanged = async (event: CustomEvent<Timestamp>) => {
const newDate = event.detail
if (newDate === undefined || value === newDate || !editable) {
return
}
onChange(newDate)
}
const WARNING_DAYS = 7
const getDueDateIconModifier = (
isOverdue: boolean,
daysDifference: number | null
): 'overdue' | 'critical' | 'warning' | undefined => {
if (isOverdue) {
return 'overdue'
}
if (daysDifference === 0) {
return 'critical'
}
if (daysDifference !== null && daysDifference <= WARNING_DAYS) {
return 'warning'
}
}
</script>
{#if shouldRender}
<div
class="clear-mins"
use:tooltip={formattedDate
? {
direction: 'top',
component: DueDatePopup,
props: {
formattedDate,
daysDifference,
isOverdue,
iconModifier
}
}
: undefined}
>
<DatePresenter {value} {editable} icon={iconModifier} {kind} on:change={handleDueDateChanged} />
</div>
{/if}

View File

@ -72,6 +72,7 @@ export { default as TimePopup } from './components/calendar/TimePopup.svelte'
export { default as DateRangePresenter } from './components/calendar/DateRangePresenter.svelte'
export { default as DateTimeRangePresenter } from './components/calendar/DateTimeRangePresenter.svelte'
export { default as DatePresenter } from './components/calendar/DatePresenter.svelte'
export { default as DueDatePresenter } from './components/calendar/DueDatePresenter.svelte'
export { default as DateTimePresenter } from './components/calendar/DateTimePresenter.svelte'
export { default as StylishEdit } from './components/StylishEdit.svelte'
export { default as Grid } from './components/Grid.svelte'

View File

@ -52,7 +52,7 @@ export const uis = plugin(uiId, {
AddDueDate: '' as IntlString,
EditDueDate: '' as IntlString,
SaveDueDate: '' as IntlString,
IssueNeedsToBeCompletedByThisDate: '' as IntlString,
NeedsToBeCompletedByThisDate: '' as IntlString,
English: '' as IntlString,
Russian: '' as IntlString,
MinutesBefore: '' as IntlString,
@ -70,7 +70,11 @@ export const uis = plugin(uiId, {
DD: '' as IntlString,
MM: '' as IntlString,
YYYY: '' as IntlString,
HH: '' as IntlString
HH: '' as IntlString,
DueDatePopupTitle: '' as IntlString,
DueDatePopupOverdueTitle: '' as IntlString,
DueDatePopupDescription: '' as IntlString,
DueDatePopupOverdueDescription: '' as IntlString
},
metadata: {
DefaultApplication: '' as Metadata<AnyComponent>,

View File

@ -24,7 +24,7 @@
import recruit from '@hcengineering/recruit'
import { AssigneePresenter, StateRefPresenter } from '@hcengineering/task-resources'
import tracker from '@hcengineering/tracker'
import { Component, showPanel } from '@hcengineering/ui'
import { Component, DueDatePresenter, showPanel } from '@hcengineering/ui'
import view from '@hcengineering/view'
import { ObjectPresenter } from '@hcengineering/view-resources'
import ApplicationPresenter from './ApplicationPresenter.svelte'
@ -90,6 +90,13 @@
</div>
<Component is={tracker.component.RelatedIssueSelector} props={{ object }} />
</div>
<DueDatePresenter
value={object.dueDate}
shouldRender={object.dueDate !== null}
onChange={async (e) => {
await client.update(object, { dueDate: e })
}}
/>
{#if (object.attachments ?? 0) > 0}
<div class="step-lr75">
<AttachmentsPresenter value={object.attachments} {object} />

View File

@ -113,10 +113,6 @@
"NewIssueDialogCloseNote": "All changes will be lost",
"RemoveComponentDialogClose": "Delete the component?",
"RemoveComponentDialogCloseNote": "Are you sure you want to delete this component? This operation cannot be undone",
"DueDatePopupTitle": "Due on {value}",
"DueDatePopupOverdueTitle": "Was due on {value}",
"DueDatePopupDescription": "{value, plural, =0 {Today} =1 {Tomorrow} other {# days remaining}}",
"DueDatePopupOverdueDescription": "{value, plural, =1 {1 day overdue} other {# days overdue}}",
"Grouping": "Grouping",
"Ordering": "Ordering",
"CompletedIssues": "Completed issues",

View File

@ -113,10 +113,6 @@
"NewIssueDialogCloseNote": "Все внесенные изменения будут потеряны",
"RemoveComponentDialogClose": "Удалить компонент?",
"RemoveComponentDialogCloseNote": "Уверены, что хотите удалить этот компонент? Эта операция не может быть отменена",
"DueDatePopupTitle": "Срок {value}",
"DueDatePopupOverdueTitle": "Должна была завершится {value}",
"DueDatePopupDescription": "{value, plural, =0 {Сегодня} =1 {Завтра} other {# дней осталось}}",
"DueDatePopupOverdueDescription": "{value, plural, =1 {1 день опозщдания} other {# дней опазданий}}",
"Grouping": "Группировка",
"Ordering": "Сортировка",
"CompletedIssues": "Завершённые задачи",

View File

@ -1,79 +0,0 @@
<!--
// 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 { Timestamp } from '@hcengineering/core'
import { DatePresenter, tooltip, getDaysDifference } from '@hcengineering/ui'
import DueDatePopup from './DueDatePopup.svelte'
import { getDueDateIconModifier } from '../utils'
export let dateMs: number | null = null
export let shouldRender: boolean = true
export let onDateChange: (newDate: number | null) => void
export let kind: 'transparent' | 'primary' | 'link' | 'list' = 'primary'
export let editable: boolean = true
$: today = new Date(new Date(Date.now()).setHours(0, 0, 0, 0))
$: isOverdue = dateMs !== null && dateMs < today.getTime()
$: dueDate = dateMs === null ? null : new Date(dateMs)
$: daysDifference = dueDate === null ? null : getDaysDifference(today, dueDate)
$: iconModifier = getDueDateIconModifier(isOverdue, daysDifference)
$: formattedDate = !dateMs ? '' : new Date(dateMs).toLocaleString('default', { month: 'short', day: 'numeric' })
const handleDueDateChanged = async (event: CustomEvent<Timestamp>) => {
const newDate = event.detail
if (newDate === undefined || dateMs === newDate || !editable) {
return
}
onDateChange(newDate)
}
</script>
{#if shouldRender}
{#if formattedDate}
<div
class="clear-mins"
use:tooltip={{
direction: 'top',
component: DueDatePopup,
props: {
formattedDate,
daysDifference,
isOverdue,
iconModifier
}
}}
>
<DatePresenter
value={dateMs}
{editable}
shouldShowLabel={false}
icon={iconModifier}
{kind}
on:change={handleDueDateChanged}
/>
</div>
{:else}
<DatePresenter
value={dateMs}
{editable}
shouldShowLabel={false}
icon={iconModifier}
{kind}
on:change={handleDueDateChanged}
/>
{/if}
{/if}

View File

@ -15,7 +15,7 @@
<script lang="ts">
import { Component } from '@hcengineering/tracker'
import { getClient } from '@hcengineering/presentation'
import CommonTrackerDatePresenter from '../CommonTrackerDatePresenter.svelte'
import { DueDatePresenter } from '@hcengineering/ui'
export let value: Component
@ -28,4 +28,4 @@
}
</script>
<CommonTrackerDatePresenter dateMs={dueDateMs} shouldRender={true} onDateChange={handleDueDateChanged} />
<DueDatePresenter value={dueDateMs} shouldRender={true} onChange={handleDueDateChanged} />

View File

@ -15,14 +15,13 @@
<script lang="ts">
import { getClient } from '@hcengineering/presentation'
import { Issue } from '@hcengineering/tracker'
import { DatePresenter, getDaysDifference } from '@hcengineering/ui'
import { getDueDateIconModifier } from '../../utils'
import { DueDatePresenter } from '@hcengineering/ui'
export let value: Issue
const client = getClient()
const handleDueDateChanged = async (newDueDate: number | undefined) => {
const handleDueDateChanged = async (newDueDate: number | undefined | null) => {
if (newDueDate === undefined || value.dueDate === newDueDate) {
return
}
@ -37,21 +36,8 @@
{ dueDate: newDueDate }
)
}
$: today = new Date(new Date(Date.now()).setHours(0, 0, 0, 0))
$: isOverdue = value.dueDate !== null && value.dueDate < today.getTime()
$: dueDate = value.dueDate === null ? null : new Date(value.dueDate)
$: daysDifference = dueDate === null ? null : getDaysDifference(today, dueDate)
$: iconModifier = getDueDateIconModifier(isOverdue, daysDifference)
</script>
{#if value}
<!-- TODO: fix button style and alignment -->
<DatePresenter
kind={'link'}
value={value.dueDate}
icon={iconModifier}
editable
on:change={({ detail }) => handleDueDateChanged(detail)}
/>
<DueDatePresenter kind={'link'} value={value.dueDate} editable onChange={(e) => handleDueDateChanged(e)} />
{/if}

View File

@ -16,11 +16,11 @@
import { WithLookup } from '@hcengineering/core'
import { Issue } from '@hcengineering/tracker'
import { getClient } from '@hcengineering/presentation'
import CommonTrackerDatePresenter from '../CommonTrackerDatePresenter.svelte'
import tracker from '../../plugin'
import { ButtonKind, DueDatePresenter } from '@hcengineering/ui'
export let value: WithLookup<Issue>
export let kind: 'transparent' | 'primary' | 'link' | 'list' = 'primary'
export let kind: ButtonKind = 'link'
export let isEditable = true
const client = getClient()
@ -45,10 +45,10 @@
value.$lookup?.status?.category !== tracker.issueStatusCategory.Canceled
</script>
<CommonTrackerDatePresenter
dateMs={dueDateMs}
<DueDatePresenter
value={dueDateMs}
shouldRender={shouldRenderPresenter}
onDateChange={handleDueDateChanged}
onChange={handleDueDateChanged}
editable={isEditable}
{kind}
/>

View File

@ -78,6 +78,7 @@
import PriorityEditor from './PriorityEditor.svelte'
import StatusEditor from './StatusEditor.svelte'
import EstimationEditor from './timereport/EstimationEditor.svelte'
import DueDatePresenter from './DueDatePresenter.svelte'
export let space: Ref<Project> | undefined = undefined
export let baseMenuClass: Ref<Class<Doc>> | undefined = undefined
@ -341,7 +342,7 @@
<Component is={notification.component.NotificationPresenter} props={{ value: object }} />
</div>
</div>
<div class="buttons-group xsmall-gap states-bar">
<div class="xsmall-gap states-bar">
{#if issue && issue.subIssues > 0}
<SubIssuesSelector value={issue} {currentProject} />
{/if}
@ -355,6 +356,7 @@
width={''}
bind:onlyIcon={fullFilled[issueId]}
/>
<DueDatePresenter value={issue} kind={'link-bordered'} />
<EstimationEditor kind={'list'} size={'small'} value={issue} />
<div
class="clear-mins"
@ -403,8 +405,9 @@
min-height: 6.5rem;
}
.states-bar {
flex-shrink: 10;
width: fit-content;
margin: 0.625rem 1rem 0;
display: flex;
flex-wrap: wrap;
gap: 0.375rem;
margin: 0.625rem 1rem;
}
</style>

View File

@ -141,10 +141,6 @@ export default mergeIds(trackerId, tracker, {
DocumentIcon: '' as IntlString,
DocumentColor: '' as IntlString,
Rank: '' as IntlString,
DueDatePopupTitle: '' as IntlString,
DueDatePopupOverdueTitle: '' as IntlString,
DueDatePopupDescription: '' as IntlString,
DueDatePopupOverdueDescription: '' as IntlString,
Grouping: '' as IntlString,
Ordering: '' as IntlString,
CompletedIssues: '' as IntlString,

View File

@ -227,25 +227,6 @@ export const getArraysUnion = (a: any[], b: any[]): any[] => {
return Array.from(union)
}
const WARNING_DAYS = 7
export const getDueDateIconModifier = (
isOverdue: boolean,
daysDifference: number | null
): 'overdue' | 'critical' | 'warning' | undefined => {
if (isOverdue) {
return 'overdue'
}
if (daysDifference === 0) {
return 'critical'
}
if (daysDifference !== null && daysDifference <= WARNING_DAYS) {
return 'warning'
}
}
export type ComponentsViewMode = 'all' | 'backlog' | 'active' | 'closed'
export type SprintViewMode = 'all' | 'planned' | 'active' | 'closed'