mirror of
https://github.com/hcengineering/platform.git
synced 2024-12-22 19:11:33 +03:00
TSK-1411 Доработка фильтров с датами (#3151)
Signed-off-by: Denis Bykhov <bykhov.denis@gmail.com>
This commit is contained in:
parent
b0e68ed880
commit
16da75f6d8
@ -914,10 +914,6 @@ export function createModel (builder: Builder): void {
|
||||
component: view.component.ValueFilter
|
||||
})
|
||||
|
||||
builder.mixin(core.class.TypeDate, core.class.Class, view.mixin.AttributeFilter, {
|
||||
component: view.component.DateFilter
|
||||
})
|
||||
|
||||
builder.mixin(tracker.class.TypeIssuePriority, core.class.Class, view.mixin.AttributePresenter, {
|
||||
presenter: tracker.component.PriorityRefPresenter
|
||||
})
|
||||
|
@ -669,7 +669,7 @@ export function createModel (builder: Builder): void {
|
||||
})
|
||||
|
||||
builder.mixin(core.class.TypeDate, core.class.Class, view.mixin.AttributeFilter, {
|
||||
component: view.component.ValueFilter
|
||||
component: view.component.DateFilter
|
||||
})
|
||||
|
||||
builder.mixin(core.class.EnumOf, core.class.Class, view.mixin.AttributeFilter, {
|
||||
@ -681,7 +681,7 @@ export function createModel (builder: Builder): void {
|
||||
})
|
||||
|
||||
builder.mixin(core.class.TypeTimestamp, core.class.Class, view.mixin.AttributeFilter, {
|
||||
component: view.component.TimestampFilter
|
||||
component: view.component.DateFilter
|
||||
})
|
||||
|
||||
builder.createDoc(
|
||||
@ -764,6 +764,93 @@ export function createModel (builder: Builder): void {
|
||||
view.filter.FilterNestedDontMatch
|
||||
)
|
||||
|
||||
builder.createDoc(
|
||||
view.class.FilterMode,
|
||||
core.space.Model,
|
||||
{
|
||||
label: view.string.Overdue,
|
||||
result: view.function.FilterDateOutdated,
|
||||
disableValueSelector: true
|
||||
},
|
||||
view.filter.FilterDateOutdated
|
||||
)
|
||||
|
||||
builder.createDoc(
|
||||
view.class.FilterMode,
|
||||
core.space.Model,
|
||||
{
|
||||
label: view.string.Today,
|
||||
result: view.function.FilterDateToday,
|
||||
disableValueSelector: true
|
||||
},
|
||||
view.filter.FilterDateToday
|
||||
)
|
||||
|
||||
builder.createDoc(
|
||||
view.class.FilterMode,
|
||||
core.space.Model,
|
||||
{
|
||||
label: view.string.ThisWeek,
|
||||
result: view.function.FilterDateWeek,
|
||||
disableValueSelector: true
|
||||
},
|
||||
view.filter.FilterDateWeek
|
||||
)
|
||||
|
||||
builder.createDoc(
|
||||
view.class.FilterMode,
|
||||
core.space.Model,
|
||||
{
|
||||
label: view.string.NextWeek,
|
||||
result: view.function.FilterDateNextWeek,
|
||||
disableValueSelector: true
|
||||
},
|
||||
view.filter.FilterDateNextW
|
||||
)
|
||||
|
||||
builder.createDoc(
|
||||
view.class.FilterMode,
|
||||
core.space.Model,
|
||||
{
|
||||
label: view.string.ThisMonth,
|
||||
result: view.function.FilterDateMonth,
|
||||
disableValueSelector: true
|
||||
},
|
||||
view.filter.FilterDateM
|
||||
)
|
||||
|
||||
builder.createDoc(
|
||||
view.class.FilterMode,
|
||||
core.space.Model,
|
||||
{
|
||||
label: view.string.NextMonth,
|
||||
result: view.function.FilterDateNextMonth,
|
||||
disableValueSelector: true
|
||||
},
|
||||
view.filter.FilterDateNextM
|
||||
)
|
||||
|
||||
builder.createDoc(
|
||||
view.class.FilterMode,
|
||||
core.space.Model,
|
||||
{
|
||||
label: view.string.CustomDate,
|
||||
result: view.function.FilterDateCustom
|
||||
},
|
||||
view.filter.FilterDateCustom
|
||||
)
|
||||
|
||||
builder.createDoc(
|
||||
view.class.FilterMode,
|
||||
core.space.Model,
|
||||
{
|
||||
label: view.string.NotSpecified,
|
||||
result: view.function.FilterDateNotSpecified,
|
||||
disableValueSelector: true
|
||||
},
|
||||
view.filter.FilterDateNotSpecified
|
||||
)
|
||||
|
||||
classPresenter(builder, core.class.EnumOf, view.component.EnumPresenter, view.component.EnumEditor)
|
||||
|
||||
// createAction(
|
||||
|
@ -104,6 +104,14 @@ export default mergeIds(viewId, view, {
|
||||
FilterAfterResult: '' as FilterFunction,
|
||||
FilterNestedMatchResult: '' as FilterFunction,
|
||||
FilterNestedDontMatchResult: '' as FilterFunction,
|
||||
FilterDateOutdated: '' as FilterFunction,
|
||||
FilterDateToday: '' as FilterFunction,
|
||||
FilterDateWeek: '' as FilterFunction,
|
||||
FilterDateNextWeek: '' as FilterFunction,
|
||||
FilterDateMonth: '' as FilterFunction,
|
||||
FilterDateNextMonth: '' as FilterFunction,
|
||||
FilterDateNotSpecified: '' as FilterFunction,
|
||||
FilterDateCustom: '' as FilterFunction,
|
||||
ShowEmptyGroups: '' as ViewCategoryAction
|
||||
}
|
||||
})
|
||||
|
366
packages/ui/src/components/calendar/DateInputBox.svelte
Normal file
366
packages/ui/src/components/calendar/DateInputBox.svelte
Normal file
@ -0,0 +1,366 @@
|
||||
<!--
|
||||
// Copyright © 2023 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 { afterUpdate, createEventDispatcher } from 'svelte'
|
||||
import ui from '../../plugin'
|
||||
import Icon from '../Icon.svelte'
|
||||
import Label from '../Label.svelte'
|
||||
import IconClose from '../icons/Close.svelte'
|
||||
import { daysInMonth } from './internal/DateUtils'
|
||||
|
||||
export let currentDate: Date | null
|
||||
export let withTime: boolean = false
|
||||
|
||||
type TEdits = 'day' | 'month' | 'year' | 'hour' | 'min'
|
||||
interface IEdits {
|
||||
id: TEdits
|
||||
value: number
|
||||
el?: HTMLElement
|
||||
}
|
||||
const editsType: TEdits[] = ['day', 'month', 'year', 'hour', 'min']
|
||||
const getIndex = (id: TEdits): number => editsType.indexOf(id)
|
||||
let edits: IEdits[] = editsType.map((edit) => {
|
||||
return { id: edit, value: -1 }
|
||||
})
|
||||
let selected: TEdits | null = 'day'
|
||||
let startTyping: boolean = false
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
const setValue = (val: number, date: Date | null, id: TEdits): Date => {
|
||||
if (date == null) date = new Date()
|
||||
switch (id) {
|
||||
case 'day':
|
||||
date.setDate(val)
|
||||
break
|
||||
case 'month':
|
||||
date.setMonth(val - 1)
|
||||
break
|
||||
case 'year':
|
||||
date.setFullYear(val)
|
||||
break
|
||||
case 'hour':
|
||||
date.setHours(val)
|
||||
break
|
||||
case 'min':
|
||||
date.setMinutes(val)
|
||||
break
|
||||
}
|
||||
return date
|
||||
}
|
||||
|
||||
const getMaxValue = (date: Date | null, id: TEdits): number => {
|
||||
if (date == null) date = new Date()
|
||||
switch (id) {
|
||||
case 'day':
|
||||
return daysInMonth(date)
|
||||
case 'month':
|
||||
return 12
|
||||
case 'year':
|
||||
return 3000
|
||||
case 'hour':
|
||||
return 23
|
||||
case 'min':
|
||||
return 59
|
||||
}
|
||||
}
|
||||
|
||||
const getValue = (date: Date, id: TEdits): number => {
|
||||
switch (id) {
|
||||
case 'day':
|
||||
return date.getDate()
|
||||
case 'month':
|
||||
return date.getMonth() + 1
|
||||
case 'year':
|
||||
return date.getFullYear()
|
||||
case 'hour':
|
||||
return date.getHours()
|
||||
case 'min':
|
||||
return date.getMinutes()
|
||||
}
|
||||
}
|
||||
|
||||
const dateToEdits = (currentDate: Date | null): void => {
|
||||
if (currentDate == null) {
|
||||
edits.forEach((edit) => {
|
||||
edit.value = -1
|
||||
})
|
||||
} else {
|
||||
for (const edit of edits) {
|
||||
edit.value = getValue(currentDate, edit.id)
|
||||
}
|
||||
}
|
||||
edits = edits
|
||||
}
|
||||
|
||||
export const isNull = (currentDate: Date | null, full: boolean = false): boolean => {
|
||||
dateToEdits(currentDate)
|
||||
let result: boolean = false
|
||||
edits.forEach((edit, i) => {
|
||||
if (edit.value === -1 && full && i > 2) result = true
|
||||
if (edit.value === -1 && !full && i < 3) result = true
|
||||
if (i === 0 && edit.value === 0) result = true
|
||||
if (i === 2 && (edit.value < 1970 || edit.value > 3000)) result = true
|
||||
})
|
||||
return result
|
||||
}
|
||||
|
||||
const keyDown = (ev: KeyboardEvent, ed: TEdits): void => {
|
||||
if (selected === ed) {
|
||||
const index = getIndex(ed)
|
||||
if (ev.key >= '0' && ev.key <= '9') {
|
||||
const shouldNext = !startTyping && selected !== 'year'
|
||||
const num: number = parseInt(ev.key, 10)
|
||||
if (startTyping) {
|
||||
if (num === 0) edits[index].value = 0
|
||||
else {
|
||||
edits[index].value = num
|
||||
}
|
||||
startTyping = false
|
||||
} else if (edits[index].value * 10 + num > getMaxValue(currentDate, ed)) {
|
||||
edits[index].value = getMaxValue(currentDate, ed)
|
||||
} else {
|
||||
edits[index].value = edits[index].value * 10 + num
|
||||
}
|
||||
if (!isNull(currentDate, false) && !startTyping) {
|
||||
fixEdits()
|
||||
currentDate = setValue(edits[index].value, currentDate, ed)
|
||||
dateToEdits(currentDate)
|
||||
}
|
||||
edits = edits
|
||||
|
||||
if (selected === 'day' && (shouldNext || edits[0].value > getMaxValue(currentDate, 'day') / 10)) {
|
||||
selected = 'month'
|
||||
} else if (selected === 'month' && (shouldNext || edits[1].value > 1)) selected = 'year'
|
||||
else if (selected === 'year' && withTime && (shouldNext || edits[2].value > 999)) selected = 'hour'
|
||||
else if (selected === 'hour' && (shouldNext || edits[3].value > 2)) selected = 'min'
|
||||
}
|
||||
if (ev.code === 'Enter') {
|
||||
if (!isNull(currentDate, false)) dispatch('close')
|
||||
}
|
||||
if (ev.code === 'Backspace') {
|
||||
edits[index].value = -1
|
||||
startTyping = true
|
||||
}
|
||||
if (ev.code === 'ArrowUp' || (ev.code === 'ArrowDown' && edits[index].el)) {
|
||||
if (edits[index].value !== -1) {
|
||||
const val = ev.code === 'ArrowUp' ? edits[index].value + 1 : edits[index].value - 1
|
||||
if (currentDate) {
|
||||
currentDate = setValue(val, currentDate, ed)
|
||||
dateToEdits(currentDate)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (ev.code === 'ArrowLeft' && edits[index].el) {
|
||||
selected = index === 0 ? edits[withTime ? 4 : 2].id : edits[index - 1].id
|
||||
}
|
||||
if (ev.code === 'ArrowRight' && edits[index].el) {
|
||||
selected = index === (withTime ? 4 : 2) ? edits[0].id : edits[index + 1].id
|
||||
}
|
||||
if (ev.code === 'Tab') {
|
||||
if ((ed === 'year' && !withTime) || (ed === 'min' && withTime)) dispatch('save')
|
||||
}
|
||||
}
|
||||
}
|
||||
const focused = (ed: TEdits): void => {
|
||||
selected = ed
|
||||
startTyping = true
|
||||
}
|
||||
const clearEdits = (): void => {
|
||||
edits.forEach((edit) => (edit.value = -1))
|
||||
if (edits[0].el) edits[0].el.focus()
|
||||
dispatch('save')
|
||||
}
|
||||
const fixEdits = (): void => {
|
||||
const h: number = edits[3].value === -1 ? 0 : edits[3].value
|
||||
const m: number = edits[4].value === -1 ? 0 : edits[4].value
|
||||
currentDate = new Date(edits[2].value, edits[1].value - 1, edits[0].value, h, m)
|
||||
dispatch('save')
|
||||
}
|
||||
|
||||
$: dateToEdits(currentDate)
|
||||
$: if (selected && edits[getIndex(selected)].el) edits[getIndex(selected)].el?.focus()
|
||||
|
||||
afterUpdate(() => {
|
||||
if (selected) edits[getIndex(selected)].el?.focus()
|
||||
})
|
||||
</script>
|
||||
|
||||
<div class="datetime-input">
|
||||
<div class="flex-row-center">
|
||||
<span
|
||||
bind:this={edits[0].el}
|
||||
class="digit"
|
||||
tabindex="0"
|
||||
on:keydown={(ev) => keyDown(ev, edits[0].id)}
|
||||
on:focus={() => focused(edits[0].id)}
|
||||
on:blur={() => (selected = null)}
|
||||
>
|
||||
{#if edits[0].value > -1}
|
||||
{edits[0].value.toString().padStart(2, '0')}
|
||||
{:else}<Label label={ui.string.DD} />{/if}
|
||||
</span>
|
||||
<span class="separator">.</span>
|
||||
<span
|
||||
bind:this={edits[1].el}
|
||||
class="digit"
|
||||
tabindex="0"
|
||||
on:keydown={(ev) => keyDown(ev, edits[1].id)}
|
||||
on:focus={() => focused(edits[1].id)}
|
||||
on:blur={() => (selected = null)}
|
||||
>
|
||||
{#if edits[1].value > -1}
|
||||
{edits[1].value.toString().padStart(2, '0')}
|
||||
{:else}<Label label={ui.string.MM} />{/if}
|
||||
</span>
|
||||
<span class="separator">.</span>
|
||||
<span
|
||||
bind:this={edits[2].el}
|
||||
class="digit"
|
||||
tabindex="0"
|
||||
on:keydown={(ev) => keyDown(ev, edits[2].id)}
|
||||
on:focus={() => focused(edits[2].id)}
|
||||
on:blur={() => (selected = null)}
|
||||
>
|
||||
{#if edits[2].value > -1}
|
||||
{edits[2].value.toString().padStart(4, '0')}
|
||||
{:else}<Label label={ui.string.YYYY} />{/if}
|
||||
</span>
|
||||
{#if withTime}
|
||||
<div class="time-divider" />
|
||||
<span
|
||||
bind:this={edits[3].el}
|
||||
class="digit"
|
||||
tabindex="0"
|
||||
on:keydown={(ev) => keyDown(ev, edits[3].id)}
|
||||
on:focus={() => focused(edits[3].id)}
|
||||
on:blur={() => (selected = null)}
|
||||
>
|
||||
{#if edits[3].value > -1}
|
||||
{edits[3].value.toString().padStart(2, '0')}
|
||||
{:else}<Label label={ui.string.HH} />{/if}
|
||||
</span>
|
||||
<span class="separator">:</span>
|
||||
<span
|
||||
bind:this={edits[4].el}
|
||||
class="digit"
|
||||
tabindex="0"
|
||||
on:keydown={(ev) => keyDown(ev, edits[4].id)}
|
||||
on:focus={() => focused(edits[4].id)}
|
||||
on:blur={() => (selected = null)}
|
||||
>
|
||||
{#if edits[4].value > -1}
|
||||
{edits[4].value.toString().padStart(2, '0')}
|
||||
{:else}<Label label={ui.string.MM} />{/if}
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
{#if currentDate}
|
||||
<div
|
||||
class="close-btn"
|
||||
tabindex="0"
|
||||
on:click={() => {
|
||||
selected = 'day'
|
||||
startTyping = true
|
||||
currentDate = null
|
||||
clearEdits()
|
||||
}}
|
||||
on:blur={() => (selected = null)}
|
||||
>
|
||||
<Icon icon={IconClose} size={'x-small'} />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.datetime-input {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
margin: 0;
|
||||
padding: 0.75rem;
|
||||
height: 3rem;
|
||||
font-family: inherit;
|
||||
font-size: 1rem;
|
||||
color: var(--theme-content-color);
|
||||
background-color: var(--theme-bg-color);
|
||||
border: 1px solid var(--theme-button-border);
|
||||
border-radius: 0.25rem;
|
||||
transition: border-color 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
border-color: var(--theme-button-enabled);
|
||||
}
|
||||
&:focus-within {
|
||||
color: var(--theme-caption-color);
|
||||
border-color: var(--primary-edit-border-color);
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin: 0 0.25rem;
|
||||
width: 0.75rem;
|
||||
height: 0.75rem;
|
||||
color: var(--theme-content-color);
|
||||
background-color: var(--theme-button-enabled);
|
||||
outline: none;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: var(--theme-caption-color);
|
||||
background-color: var(--theme-button-hovered);
|
||||
}
|
||||
}
|
||||
|
||||
.digit {
|
||||
position: relative;
|
||||
padding: 0 0.125rem;
|
||||
height: 1.5rem;
|
||||
line-height: 1.5rem;
|
||||
color: var(--theme-caption-color);
|
||||
outline: none;
|
||||
border-radius: 0.125rem;
|
||||
|
||||
&:focus {
|
||||
color: var(--primary-button-color);
|
||||
background-color: var(--primary-button-enabled);
|
||||
}
|
||||
&::after {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 11000;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
.time-divider {
|
||||
flex-shrink: 0;
|
||||
margin: 0 0.25rem;
|
||||
width: 1px;
|
||||
min-width: 1px;
|
||||
height: 0.75rem;
|
||||
background-color: var(--theme-button-border);
|
||||
}
|
||||
.separator {
|
||||
margin: 0 0.1rem;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -1,5 +1,5 @@
|
||||
<!--
|
||||
// Copyright © 2020 Anticrm Platform Contributors.
|
||||
// Copyright © 2023 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
|
||||
@ -13,18 +13,17 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { DateRangeMode } from '@hcengineering/core'
|
||||
import { IntlString } from '@hcengineering/platform'
|
||||
import { afterUpdate, createEventDispatcher } from 'svelte'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import ui from '../../plugin'
|
||||
import ActionIcon from '../ActionIcon.svelte'
|
||||
import Button from '../Button.svelte'
|
||||
import Icon from '../Icon.svelte'
|
||||
import Label from '../Label.svelte'
|
||||
import IconClose from '../icons/Close.svelte'
|
||||
import DateInputBox from './DateInputBox.svelte'
|
||||
import MonthSquare from './MonthSquare.svelte'
|
||||
import { daysInMonth } from './internal/DateUtils'
|
||||
import Shifts from './Shifts.svelte'
|
||||
import { DateRangeMode } from '@hcengineering/core'
|
||||
|
||||
export let currentDate: Date | null
|
||||
export let withTime: boolean = false
|
||||
@ -35,203 +34,49 @@
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
type TEdits = 'day' | 'month' | 'year' | 'hour' | 'min'
|
||||
interface IEdits {
|
||||
id: TEdits
|
||||
value: number
|
||||
el?: HTMLElement
|
||||
}
|
||||
const editsType: TEdits[] = ['day', 'month', 'year', 'hour', 'min']
|
||||
const getIndex = (id: TEdits): number => editsType.indexOf(id)
|
||||
const today: Date = new Date(Date.now())
|
||||
let selected: TEdits | null = 'day'
|
||||
let startTyping: boolean = false
|
||||
let edits: IEdits[] = editsType.map((edit) => {
|
||||
return { id: edit, value: -1 }
|
||||
})
|
||||
|
||||
let viewDate: Date = currentDate ?? today
|
||||
let viewDateSec: Date
|
||||
|
||||
const getValue = (date: Date | null | undefined, id: TEdits): number => {
|
||||
if (date == null) date = today
|
||||
switch (id) {
|
||||
case 'day':
|
||||
return date.getDate()
|
||||
case 'month':
|
||||
return date.getMonth() + 1
|
||||
case 'year':
|
||||
return date.getFullYear()
|
||||
case 'hour':
|
||||
return date.getHours()
|
||||
case 'min':
|
||||
return date.getMinutes()
|
||||
}
|
||||
}
|
||||
const setValue = (val: number, date: Date, id: TEdits): Date => {
|
||||
switch (id) {
|
||||
case 'day':
|
||||
date.setDate(val)
|
||||
break
|
||||
case 'month':
|
||||
date.setMonth(val - 1)
|
||||
break
|
||||
case 'year':
|
||||
date.setFullYear(val)
|
||||
break
|
||||
case 'hour':
|
||||
date.setHours(val)
|
||||
break
|
||||
case 'min':
|
||||
date.setMinutes(val)
|
||||
break
|
||||
}
|
||||
return date
|
||||
}
|
||||
const getMaxValue = (date: Date, id: TEdits): number => {
|
||||
switch (id) {
|
||||
case 'day':
|
||||
return daysInMonth(date)
|
||||
case 'month':
|
||||
return 12
|
||||
case 'year':
|
||||
return 3000
|
||||
case 'hour':
|
||||
return 23
|
||||
case 'min':
|
||||
return 59
|
||||
}
|
||||
}
|
||||
|
||||
const dateToEdits = (): void => {
|
||||
edits.forEach((edit) => {
|
||||
edit.value = getValue(currentDate, edit.id)
|
||||
})
|
||||
edits = edits
|
||||
}
|
||||
const clearEdits = (): void => {
|
||||
edits.forEach((edit) => (edit.value = -1))
|
||||
if (edits[0].el) edits[0].el.focus()
|
||||
}
|
||||
const fixEdits = (): void => {
|
||||
const h: number = edits[3].value === -1 ? 0 : edits[3].value
|
||||
const m: number = edits[4].value === -1 ? 0 : edits[4].value
|
||||
viewDate = currentDate = new Date(edits[2].value, edits[1].value - 1, edits[0].value, h, m)
|
||||
}
|
||||
const isNull = (full: boolean = false): boolean => {
|
||||
let result: boolean = false
|
||||
edits.forEach((edit, i) => {
|
||||
if (edit.value === -1 && full && i > 2) result = true
|
||||
if (edit.value === -1 && !full && i < 3) result = true
|
||||
if (i === 0 && edit.value === 0) result = true
|
||||
if (i === 2 && (edit.value < 1970 || edit.value > 3000)) result = true
|
||||
})
|
||||
return result
|
||||
}
|
||||
let dateInput: DateInputBox
|
||||
|
||||
const saveDate = (withTime: boolean = false): void => {
|
||||
if (currentDate) {
|
||||
if (!withTime) {
|
||||
currentDate.setHours(edits[3].value > 0 ? edits[3].value : 0)
|
||||
currentDate.setMinutes(edits[4].value > 0 ? edits[4].value : 0)
|
||||
currentDate.setHours(0)
|
||||
currentDate.setMinutes(0)
|
||||
}
|
||||
currentDate.setSeconds(0, 0)
|
||||
viewDate = currentDate = currentDate
|
||||
dateToEdits()
|
||||
dispatch('update', currentDate)
|
||||
}
|
||||
}
|
||||
const closeDP = (withTime: boolean = false): void => {
|
||||
if (!isNull()) saveDate(withTime)
|
||||
if (!dateInput.isNull(currentDate, withTime)) saveDate(withTime)
|
||||
else {
|
||||
currentDate = null
|
||||
dispatch('update', null)
|
||||
}
|
||||
dispatch('close')
|
||||
dispatch('close', currentDate)
|
||||
}
|
||||
|
||||
const keyDown = (ev: KeyboardEvent, ed: TEdits): void => {
|
||||
if (selected === ed) {
|
||||
const index = getIndex(ed)
|
||||
if (ev.key >= '0' && ev.key <= '9') {
|
||||
const num: number = parseInt(ev.key, 10)
|
||||
if (startTyping) {
|
||||
if (num === 0) edits[index].value = 0
|
||||
else {
|
||||
edits[index].value = num
|
||||
startTyping = false
|
||||
}
|
||||
} else if (edits[index].value * 10 + num > getMaxValue(viewDate, ed)) {
|
||||
edits[index].value = getMaxValue(viewDate, ed)
|
||||
} else {
|
||||
edits[index].value = edits[index].value * 10 + num
|
||||
}
|
||||
if (!isNull(false) && !startTyping) {
|
||||
fixEdits()
|
||||
currentDate = setValue(edits[index].value, viewDate, ed)
|
||||
dateToEdits()
|
||||
}
|
||||
edits = edits
|
||||
|
||||
if (selected === 'day' && edits[0].value > getMaxValue(viewDate, 'day') / 10) selected = 'month'
|
||||
else if (selected === 'month' && edits[1].value > 1) selected = 'year'
|
||||
else if (selected === 'year' && withTime && edits[2].value > 999) selected = 'hour'
|
||||
else if (selected === 'hour' && edits[3].value > 2) selected = 'min'
|
||||
}
|
||||
if (ev.code === 'Enter') {
|
||||
if (!isNull(false)) closeDP()
|
||||
}
|
||||
if (ev.code === 'Backspace') {
|
||||
edits[index].value = -1
|
||||
startTyping = true
|
||||
}
|
||||
if (ev.code === 'ArrowUp' || (ev.code === 'ArrowDown' && edits[index].el)) {
|
||||
if (edits[index].value !== -1) {
|
||||
const val = ev.code === 'ArrowUp' ? edits[index].value + 1 : edits[index].value - 1
|
||||
if (currentDate) {
|
||||
currentDate = setValue(val, currentDate, ed)
|
||||
dateToEdits()
|
||||
}
|
||||
}
|
||||
}
|
||||
if (ev.code === 'ArrowLeft' && edits[index].el) {
|
||||
selected = index === 0 ? edits[withTime ? 4 : 2].id : edits[index - 1].id
|
||||
}
|
||||
if (ev.code === 'ArrowRight' && edits[index].el) {
|
||||
selected = index === (withTime ? 4 : 2) ? edits[0].id : edits[index + 1].id
|
||||
}
|
||||
if (ev.code === 'Tab') {
|
||||
if ((ed === 'year' && !withTime) || (ed === 'min' && withTime)) saveDate()
|
||||
}
|
||||
}
|
||||
}
|
||||
const focused = (ed: TEdits): void => {
|
||||
selected = ed
|
||||
startTyping = true
|
||||
}
|
||||
const updateDate = (date: Date | null): void => {
|
||||
if (date) {
|
||||
currentDate = date
|
||||
dateToEdits()
|
||||
closeDP()
|
||||
}
|
||||
}
|
||||
const navigateMonth = (result: any): void => {
|
||||
if (result) {
|
||||
if (result.charAt(1) === 'm') viewDate.setMonth(viewDate.getMonth() + (result === '-m' ? -1 : 1))
|
||||
viewDate.setMonth(viewDate.getMonth() + result)
|
||||
viewDate = viewDate
|
||||
}
|
||||
}
|
||||
const changeMonth = (date: Date, up: boolean): Date => {
|
||||
return new Date(date.getFullYear(), date.getMonth() + (up ? 1 : -1), date.getDate())
|
||||
const changeMonth = (date: Date): Date => {
|
||||
return new Date(date.getFullYear(), date.getMonth() + 1, 1)
|
||||
}
|
||||
|
||||
if (currentDate) dateToEdits()
|
||||
$: if (selected && edits[getIndex(selected)].el) edits[getIndex(selected)].el?.focus()
|
||||
$: if (viewDate) viewDateSec = changeMonth(viewDate, true)
|
||||
|
||||
afterUpdate(() => {
|
||||
if (selected) edits[getIndex(selected)].el?.focus()
|
||||
})
|
||||
$: if (viewDate) viewDateSec = changeMonth(viewDate)
|
||||
</script>
|
||||
|
||||
<div class="date-popup-container">
|
||||
@ -254,91 +99,13 @@
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="datetime-input">
|
||||
<div class="flex-row-center">
|
||||
<span
|
||||
bind:this={edits[0].el}
|
||||
class="digit"
|
||||
tabindex="0"
|
||||
on:keydown={(ev) => keyDown(ev, edits[0].id)}
|
||||
on:focus={() => focused(edits[0].id)}
|
||||
on:blur={() => (selected = null)}
|
||||
>
|
||||
{#if edits[0].value > -1}
|
||||
{edits[0].value.toString().padStart(2, '0')}
|
||||
{:else}<Label label={ui.string.DD} />{/if}
|
||||
</span>
|
||||
<span class="separator">.</span>
|
||||
<span
|
||||
bind:this={edits[1].el}
|
||||
class="digit"
|
||||
tabindex="0"
|
||||
on:keydown={(ev) => keyDown(ev, edits[1].id)}
|
||||
on:focus={() => focused(edits[1].id)}
|
||||
on:blur={() => (selected = null)}
|
||||
>
|
||||
{#if edits[1].value > -1}
|
||||
{edits[1].value.toString().padStart(2, '0')}
|
||||
{:else}<Label label={ui.string.MM} />{/if}
|
||||
</span>
|
||||
<span class="separator">.</span>
|
||||
<span
|
||||
bind:this={edits[2].el}
|
||||
class="digit"
|
||||
tabindex="0"
|
||||
on:keydown={(ev) => keyDown(ev, edits[2].id)}
|
||||
on:focus={() => focused(edits[2].id)}
|
||||
on:blur={() => (selected = null)}
|
||||
>
|
||||
{#if edits[2].value > -1}
|
||||
{edits[2].value.toString().padStart(4, '0')}
|
||||
{:else}<Label label={ui.string.YYYY} />{/if}
|
||||
</span>
|
||||
{#if withTime}
|
||||
<div class="time-divider" />
|
||||
<span
|
||||
bind:this={edits[3].el}
|
||||
class="digit"
|
||||
tabindex="0"
|
||||
on:keydown={(ev) => keyDown(ev, edits[3].id)}
|
||||
on:focus={() => focused(edits[3].id)}
|
||||
on:blur={() => (selected = null)}
|
||||
>
|
||||
{#if edits[3].value > -1}
|
||||
{edits[3].value.toString().padStart(2, '0')}
|
||||
{:else}<Label label={ui.string.HH} />{/if}
|
||||
</span>
|
||||
<span class="separator">:</span>
|
||||
<span
|
||||
bind:this={edits[4].el}
|
||||
class="digit"
|
||||
tabindex="0"
|
||||
on:keydown={(ev) => keyDown(ev, edits[4].id)}
|
||||
on:focus={() => focused(edits[4].id)}
|
||||
on:blur={() => (selected = null)}
|
||||
>
|
||||
{#if edits[4].value > -1}
|
||||
{edits[4].value.toString().padStart(2, '0')}
|
||||
{:else}<Label label={ui.string.MM} />{/if}
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
{#if currentDate}
|
||||
<div
|
||||
class="close-btn"
|
||||
tabindex="0"
|
||||
on:click={() => {
|
||||
selected = 'day'
|
||||
startTyping = true
|
||||
currentDate = null
|
||||
clearEdits()
|
||||
}}
|
||||
on:blur={() => (selected = null)}
|
||||
>
|
||||
<Icon icon={IconClose} size={'x-small'} />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<DateInputBox
|
||||
bind:this={dateInput}
|
||||
bind:currentDate
|
||||
{withTime}
|
||||
on:close={() => closeDP(withTime)}
|
||||
on:save={() => saveDate(withTime)}
|
||||
/>
|
||||
|
||||
<div class="month-group">
|
||||
<MonthSquare
|
||||
@ -437,83 +204,4 @@
|
||||
border-top: 1px solid var(--theme-popup-divider);
|
||||
}
|
||||
}
|
||||
|
||||
.datetime-input {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
margin: 0;
|
||||
padding: 0.75rem;
|
||||
height: 3rem;
|
||||
font-family: inherit;
|
||||
font-size: 1rem;
|
||||
color: var(--theme-content-color);
|
||||
background-color: var(--theme-bg-color);
|
||||
border: 1px solid var(--theme-button-border);
|
||||
border-radius: 0.25rem;
|
||||
transition: border-color 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
border-color: var(--theme-button-enabled);
|
||||
}
|
||||
&:focus-within {
|
||||
color: var(--theme-caption-color);
|
||||
border-color: var(--primary-edit-border-color);
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin: 0 0.25rem;
|
||||
width: 0.75rem;
|
||||
height: 0.75rem;
|
||||
color: var(--theme-content-color);
|
||||
background-color: var(--theme-button-enabled);
|
||||
outline: none;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: var(--theme-caption-color);
|
||||
background-color: var(--theme-button-hovered);
|
||||
}
|
||||
}
|
||||
|
||||
.digit {
|
||||
position: relative;
|
||||
padding: 0 0.125rem;
|
||||
height: 1.5rem;
|
||||
line-height: 1.5rem;
|
||||
color: var(--theme-caption-color);
|
||||
outline: none;
|
||||
border-radius: 0.125rem;
|
||||
|
||||
&:focus {
|
||||
color: var(--primary-button-color);
|
||||
background-color: var(--primary-button-enabled);
|
||||
}
|
||||
&::after {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 11000;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
.time-divider {
|
||||
flex-shrink: 0;
|
||||
margin: 0 0.25rem;
|
||||
width: 1px;
|
||||
min-width: 1px;
|
||||
height: 0.75rem;
|
||||
background-color: var(--theme-button-border);
|
||||
}
|
||||
.separator {
|
||||
margin: 0 0.1rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@ -25,6 +25,7 @@
|
||||
export let hideNavigator: boolean = false
|
||||
export let viewUpdate: boolean = true
|
||||
export let displayedWeeksCount = 6
|
||||
export let selectedTo: Date | null | undefined = undefined
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
@ -33,14 +34,24 @@
|
||||
const today: Date = new Date(Date.now())
|
||||
const capitalizeFirstLetter = (str: string): string => str.charAt(0).toUpperCase() + str.slice(1)
|
||||
|
||||
if (viewDate === undefined) viewDate = currentDate ?? today
|
||||
afterUpdate(() => {
|
||||
if (currentDate && viewUpdate) viewDate = currentDate
|
||||
if (viewDate) {
|
||||
monthYear = capitalizeFirstLetter(getMonthName(viewDate)) + ' ' + viewDate.getFullYear()
|
||||
firstDayOfCurrentMonth = firstDay(viewDate, mondayStart)
|
||||
}
|
||||
monthYear = capitalizeFirstLetter(getMonthName(viewDate)) + ' ' + viewDate.getFullYear()
|
||||
firstDayOfCurrentMonth = firstDay(viewDate, mondayStart)
|
||||
})
|
||||
|
||||
function inRange (currentDate: Date | null, selectedTo: Date | null | undefined, target: Date): boolean {
|
||||
if (currentDate == null || selectedTo == null) return false
|
||||
if (areDatesEqual(currentDate, selectedTo)) return false
|
||||
const startDate = currentDate < selectedTo ? currentDate : selectedTo
|
||||
const endDate = currentDate > selectedTo ? currentDate : selectedTo
|
||||
return target > startDate && target < endDate
|
||||
}
|
||||
|
||||
function isSelected (currentDate: Date | null, selectedTo: Date | null | undefined, target: Date): boolean {
|
||||
if (currentDate != null && areDatesEqual(currentDate, target)) return true
|
||||
if (selectedTo != null && areDatesEqual(selectedTo, target)) return true
|
||||
return false
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="month-container">
|
||||
@ -52,7 +63,7 @@
|
||||
class="btn"
|
||||
on:click={() => {
|
||||
if (viewUpdate) viewDate.setMonth(viewDate.getMonth() - 1)
|
||||
dispatch('navigation', '-m')
|
||||
dispatch('navigation', -1)
|
||||
}}
|
||||
>
|
||||
<div class="icon-btn"><Icon icon={IconNavPrev} size={'full'} /></div>
|
||||
@ -61,7 +72,7 @@
|
||||
class="btn"
|
||||
on:click={() => {
|
||||
if (viewUpdate) viewDate.setMonth(viewDate.getMonth() + 1)
|
||||
dispatch('navigation', '+m')
|
||||
dispatch('navigation', 1)
|
||||
}}
|
||||
>
|
||||
<div class="icon-btn"><Icon icon={IconNavNext} size={'full'} /></div>
|
||||
@ -80,14 +91,14 @@
|
||||
|
||||
{#each [...Array(displayedWeeksCount).keys()] as weekIndex}
|
||||
{#each [...Array(7).keys()] as dayOfWeek}
|
||||
{@const wrongM = weekday(firstDayOfCurrentMonth, weekIndex, dayOfWeek).getMonth() !== viewDate.getMonth()}
|
||||
{@const date = weekday(firstDayOfCurrentMonth, weekIndex, dayOfWeek)}
|
||||
{@const wrongM = date.getMonth() !== viewDate.getMonth()}
|
||||
<div
|
||||
class="day"
|
||||
class:weekend={isWeekend(weekday(firstDayOfCurrentMonth, weekIndex, dayOfWeek))}
|
||||
class:today={areDatesEqual(today, weekday(firstDayOfCurrentMonth, weekIndex, dayOfWeek))}
|
||||
class:selected={currentDate &&
|
||||
weekday(firstDayOfCurrentMonth, weekIndex, dayOfWeek).getMonth() === currentDate.getMonth() &&
|
||||
areDatesEqual(currentDate, weekday(firstDayOfCurrentMonth, weekIndex, dayOfWeek))}
|
||||
class:weekend={isWeekend(date)}
|
||||
class:today={areDatesEqual(today, date)}
|
||||
class:selected={isSelected(currentDate, selectedTo, date)}
|
||||
class:range={inRange(currentDate, selectedTo, date)}
|
||||
class:wrongMonth={wrongM}
|
||||
style={`grid-column-start: ${dayOfWeek + 1}; grid-row-start: ${weekIndex + 2};`}
|
||||
on:click|stopPropagation={(ev) => {
|
||||
@ -95,7 +106,7 @@
|
||||
ev.preventDefault()
|
||||
return
|
||||
}
|
||||
viewDate = weekday(firstDayOfCurrentMonth, weekIndex, dayOfWeek)
|
||||
viewDate = new Date(date)
|
||||
if (currentDate) {
|
||||
viewDate.setHours(currentDate.getHours())
|
||||
viewDate.setMinutes(currentDate.getMinutes())
|
||||
@ -103,7 +114,7 @@
|
||||
dispatch('update', viewDate)
|
||||
}}
|
||||
>
|
||||
{weekday(firstDayOfCurrentMonth, weekIndex, dayOfWeek).getDate()}
|
||||
{date.getDate()}
|
||||
</div>
|
||||
{/each}
|
||||
{/each}
|
||||
@ -213,6 +224,11 @@
|
||||
background-color: var(--primary-bg-color);
|
||||
}
|
||||
|
||||
&.range:not(.wrongMonth) {
|
||||
color: var(--caption-color);
|
||||
background-color: var(--primary-button-disabled);
|
||||
}
|
||||
|
||||
&:before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
|
209
packages/ui/src/components/calendar/RangeDatePopup.svelte
Normal file
209
packages/ui/src/components/calendar/RangeDatePopup.svelte
Normal file
@ -0,0 +1,209 @@
|
||||
<!--
|
||||
// Copyright © 2023 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 { IntlString } from '@hcengineering/platform'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import ui from '../../plugin'
|
||||
import ActionIcon from '../ActionIcon.svelte'
|
||||
import Button from '../Button.svelte'
|
||||
import Label from '../Label.svelte'
|
||||
import IconClose from '../icons/Close.svelte'
|
||||
import DateInputBox from './DateInputBox.svelte'
|
||||
import MonthSquare from './MonthSquare.svelte'
|
||||
|
||||
export let startDate: Date | null
|
||||
export let endDate: Date | null
|
||||
export let label: IntlString
|
||||
export let mondayStart: boolean = true
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
const today: Date = new Date(Date.now())
|
||||
|
||||
let viewDate: Date = startDate ?? today
|
||||
let dateInput: DateInputBox
|
||||
let endDateInput: DateInputBox
|
||||
|
||||
const saveDate = (): void => {
|
||||
if (startDate) {
|
||||
startDate.setHours(0, 0, 0, 0)
|
||||
if (endDate) {
|
||||
endDate.setHours(0, 0, 0, 0)
|
||||
if (endDate < startDate) {
|
||||
const swap = endDate
|
||||
endDate = startDate
|
||||
startDate = swap
|
||||
}
|
||||
}
|
||||
viewDate = startDate
|
||||
dispatch('update', {
|
||||
startDate,
|
||||
endDate
|
||||
})
|
||||
} else if (endDate) {
|
||||
startDate = endDate
|
||||
startDate.setHours(0, 0, 0, 0)
|
||||
endDate = null
|
||||
viewDate = startDate
|
||||
dispatch('update', {
|
||||
startDate,
|
||||
endDate
|
||||
})
|
||||
}
|
||||
viewDateSec = changeMonth(startDate, endDate)
|
||||
}
|
||||
const closeDP = (): void => {
|
||||
if (!dateInput.isNull(startDate, false)) saveDate()
|
||||
else {
|
||||
startDate = null
|
||||
endDate = null
|
||||
dispatch('update', {
|
||||
startDate,
|
||||
endDate
|
||||
})
|
||||
}
|
||||
dispatch('close', {
|
||||
startDate,
|
||||
endDate
|
||||
})
|
||||
}
|
||||
|
||||
const updateDate = (date: Date | null): void => {
|
||||
if (date) {
|
||||
if (startDate == null) {
|
||||
startDate = date
|
||||
} else if (endDate == null) {
|
||||
if (date < startDate) {
|
||||
endDate = startDate
|
||||
startDate = date
|
||||
} else {
|
||||
endDate = date
|
||||
}
|
||||
} else {
|
||||
startDate = date
|
||||
endDate = null
|
||||
}
|
||||
}
|
||||
}
|
||||
const navigateMonth = (result: any): void => {
|
||||
if (result) {
|
||||
viewDate.setMonth(viewDate.getMonth() + result)
|
||||
viewDate = viewDate
|
||||
viewDateSec.setMonth(viewDateSec.getMonth() + result)
|
||||
viewDateSec = viewDateSec
|
||||
}
|
||||
}
|
||||
const changeMonth = (date: Date | null, endDate: Date | null): Date => {
|
||||
if (date == null) {
|
||||
date = new Date()
|
||||
}
|
||||
if (endDate == null || (date.getMonth() === endDate.getMonth() && date.getFullYear() === endDate.getFullYear())) {
|
||||
return new Date(date.getFullYear(), date.getMonth() + 1, 1)
|
||||
}
|
||||
return new Date(endDate)
|
||||
}
|
||||
|
||||
let viewDateSec: Date = changeMonth(startDate, endDate)
|
||||
</script>
|
||||
|
||||
<div class="date-popup-container">
|
||||
<div class="header">
|
||||
<span class="fs-title overflow-label"><Label {label} /></span>
|
||||
<ActionIcon
|
||||
icon={IconClose}
|
||||
size={'small'}
|
||||
action={() => {
|
||||
dispatch('close', {})
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div class="content">
|
||||
<div class="flex-between">
|
||||
<div class="w-60">
|
||||
<DateInputBox bind:this={dateInput} bind:currentDate={startDate} on:close={closeDP} on:save={saveDate} />
|
||||
</div>
|
||||
<div class="w-60">
|
||||
<DateInputBox bind:this={endDateInput} bind:currentDate={endDate} on:close={closeDP} on:save={saveDate} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="month-group">
|
||||
<MonthSquare
|
||||
bind:currentDate={startDate}
|
||||
selectedTo={endDate}
|
||||
{viewDate}
|
||||
{mondayStart}
|
||||
viewUpdate={false}
|
||||
hideNavigator
|
||||
on:update={(result) => updateDate(result.detail)}
|
||||
/>
|
||||
<MonthSquare
|
||||
bind:currentDate={endDate}
|
||||
selectedTo={startDate}
|
||||
viewDate={viewDateSec}
|
||||
{mondayStart}
|
||||
viewUpdate={false}
|
||||
on:update={(result) => updateDate(result.detail)}
|
||||
on:navigation={(result) => navigateMonth(result.detail)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="footer">
|
||||
<Button kind={'primary'} label={ui.string.Save} size={'x-large'} width={'100%'} on:click={() => closeDP()} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.date-popup-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
max-width: calc(100vw - 2rem);
|
||||
max-height: calc(100vh - 2rem);
|
||||
width: max-content;
|
||||
height: max-content;
|
||||
color: var(--caption-color);
|
||||
background: var(--theme-popup-color);
|
||||
border-radius: 0.5rem;
|
||||
box-shadow: var(--theme-popup-shadow);
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 1rem 1.5rem 1rem 2rem;
|
||||
border-bottom: 1px solid var(--theme-popup-divider);
|
||||
}
|
||||
|
||||
.content {
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 1.5rem 2rem;
|
||||
min-height: 0;
|
||||
|
||||
.month-group {
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
margin: 0.5rem -0.5rem 0;
|
||||
}
|
||||
}
|
||||
|
||||
.footer {
|
||||
padding: 1rem 2rem;
|
||||
border-top: 1px solid var(--theme-popup-divider);
|
||||
}
|
||||
}
|
||||
</style>
|
@ -67,6 +67,7 @@ export { default as Section } from './components/Section.svelte'
|
||||
export { default as DatePicker } from './components/calendar/DatePicker.svelte'
|
||||
export { default as DateRangePicker } from './components/calendar/DateRangePicker.svelte'
|
||||
export { default as DatePopup } from './components/calendar/DatePopup.svelte'
|
||||
export { default as RangeDatePopup } from './components/calendar/RangeDatePopup.svelte'
|
||||
export { default as DateRangePopup } from './components/calendar/DateRangePopup.svelte'
|
||||
export { default as TimePopup } from './components/calendar/TimePopup.svelte'
|
||||
export { default as DateRangePresenter } from './components/calendar/DateRangePresenter.svelte'
|
||||
|
@ -67,6 +67,14 @@
|
||||
"ShowPreviewOnClick": "Please click to show document index preview...",
|
||||
"Shown": "Shown",
|
||||
"Total": "Total",
|
||||
"ShowEmptyGroups": "Show empty groups"
|
||||
"ShowEmptyGroups": "Show empty groups",
|
||||
"Overdue": "Overdue",
|
||||
"Today": "Today",
|
||||
"ThisWeek": "This week",
|
||||
"NextWeek": "Next week",
|
||||
"ThisMonth": "This month",
|
||||
"NextMonth": "Next month",
|
||||
"NotSpecified": "Not specified",
|
||||
"CustomDate": "Custom date"
|
||||
}
|
||||
}
|
||||
|
@ -64,6 +64,14 @@
|
||||
"ShowPreviewOnClick": "Пожалуйста нажмите чтобы увидеть предпросмотр...",
|
||||
"Shown": "Показано",
|
||||
"Total": "Всего",
|
||||
"ShowEmptyGroups": "Показывать пустые группы"
|
||||
"ShowEmptyGroups": "Показывать пустые группы",
|
||||
"Overdue": "Overdue",
|
||||
"Today": "Сегодня",
|
||||
"ThisWeek": "Эта неделя",
|
||||
"NextWeek": "Следующая неделя",
|
||||
"ThisMonth": "Этот месяц",
|
||||
"NextMonth": "Следующий месяц",
|
||||
"NotSpecified": "Не указана",
|
||||
"CustomDate": "Выбранная дата"
|
||||
}
|
||||
}
|
||||
|
@ -13,138 +13,82 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { Class, Doc, FindResult, getObjectValue, Ref, SortingOrder, Space } from '@hcengineering/core'
|
||||
import core, { Class, Doc, Ref, Space } from '@hcengineering/core'
|
||||
import { getClient } from '@hcengineering/presentation'
|
||||
import ui, { Button, CheckBox, Label, Loading, resizeObserver } from '@hcengineering/ui'
|
||||
import { Filter } from '@hcengineering/view'
|
||||
import { getPresenter } from '../../utils'
|
||||
import { RangeDatePopup, SelectPopup, showPopup } from '@hcengineering/ui'
|
||||
import { Filter, FilterMode } from '@hcengineering/view'
|
||||
import { createEventDispatcher, onMount } from 'svelte'
|
||||
import view from '../../plugin'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
|
||||
export let _class: Ref<Class<Doc>>
|
||||
export let space: Ref<Space> | undefined = undefined
|
||||
export let filter: Filter
|
||||
export let onChange: (e: Filter) => void
|
||||
|
||||
filter.modes = [view.filter.FilterValueIn, view.filter.FilterValueNin]
|
||||
const isDate = filter.key.attribute.type._class === core.class.TypeDate
|
||||
|
||||
filter.modes = isDate
|
||||
? [
|
||||
view.filter.FilterDateOutdated,
|
||||
view.filter.FilterDateToday,
|
||||
view.filter.FilterDateWeek,
|
||||
view.filter.FilterDateNextW,
|
||||
view.filter.FilterDateM,
|
||||
view.filter.FilterDateNextM,
|
||||
view.filter.FilterDateCustom,
|
||||
view.filter.FilterDateNotSpecified
|
||||
]
|
||||
: [view.filter.FilterDateToday, view.filter.FilterDateWeek, view.filter.FilterDateM, view.filter.FilterDateCustom]
|
||||
filter.mode = filter.mode === undefined ? filter.modes[0] : filter.mode
|
||||
|
||||
const client = getClient()
|
||||
const key = { key: filter.key.key }
|
||||
const promise = getPresenter(client, filter.key._class, key, key)
|
||||
|
||||
let values = new Map<any, number>()
|
||||
let selectedValues: Set<any> = new Set<any>(filter.value.map((p) => p[0]))
|
||||
const realValues = new Map<any, Set<any>>()
|
||||
|
||||
let objectsPromise: Promise<FindResult<Doc>> | undefined
|
||||
|
||||
async function getValues (): Promise<void> {
|
||||
if (objectsPromise) {
|
||||
await objectsPromise
|
||||
}
|
||||
values.clear()
|
||||
realValues.clear()
|
||||
let prefix = ''
|
||||
const hieararchy = client.getHierarchy()
|
||||
const attr = hieararchy.getAttribute(filter.key._class, filter.key.key)
|
||||
if (hieararchy.isMixin(attr.attributeOf)) {
|
||||
prefix = attr.attributeOf + '.'
|
||||
}
|
||||
objectsPromise = client.findAll(
|
||||
_class,
|
||||
{ ...(space ? { space } : {}) },
|
||||
{
|
||||
sort: { [filter.key.key]: SortingOrder.Ascending },
|
||||
projection: { [prefix + filter.key.key]: 1, space: 1 }
|
||||
}
|
||||
)
|
||||
const res = await objectsPromise
|
||||
|
||||
for (const object of res) {
|
||||
let asDoc = object
|
||||
if (hieararchy.isMixin(filter.key._class)) {
|
||||
asDoc = hieararchy.as(object, filter.key._class)
|
||||
}
|
||||
const realValue = getObjectValue(filter.key.key, asDoc)
|
||||
const d = realValue ? new Date(realValue as number).setHours(0, 0, 0, 0) : undefined
|
||||
values.set(d, (values.get(d) ?? 0) + 1)
|
||||
realValues.set(d, (realValues.get(d) ?? new Set()).add(realValue))
|
||||
}
|
||||
for (const object of filter.value.map((p) => p[0])) {
|
||||
if (!values.has(object)) values.set(object, 0)
|
||||
}
|
||||
values = values
|
||||
objectsPromise = undefined
|
||||
}
|
||||
|
||||
function isSelected (value: any, values: Set<any>): boolean {
|
||||
return values.has(value)
|
||||
}
|
||||
|
||||
function toggle (value: any): void {
|
||||
if (isSelected(value, selectedValues)) {
|
||||
selectedValues.delete(value)
|
||||
} else {
|
||||
selectedValues.add(value)
|
||||
}
|
||||
selectedValues = selectedValues
|
||||
}
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
getValues()
|
||||
let modes: FilterMode[] = []
|
||||
|
||||
client.findAll(view.class.FilterMode, { _id: { $in: filter.modes } }).then((res) => {
|
||||
modes = res
|
||||
})
|
||||
|
||||
function showPicker () {
|
||||
showPopup(
|
||||
RangeDatePopup,
|
||||
{ label: filter.key.attribute.label, startDate: filter.value[0], endDate: filter.value[1] },
|
||||
undefined,
|
||||
(res) => {
|
||||
const value: Date[] = []
|
||||
if (res.startDate) {
|
||||
value.push(res.startDate)
|
||||
}
|
||||
if (res.endDate && res.startDate !== res.endDate) {
|
||||
value.push(res.endDate)
|
||||
}
|
||||
filter.value = value
|
||||
onChange(filter)
|
||||
dispatch('close')
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
if (filter.mode === view.filter.FilterDateCustom) {
|
||||
showPicker()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<div class="selectPopup" use:resizeObserver={() => dispatch('changeContent')}>
|
||||
<div class="scroll">
|
||||
<div class="box">
|
||||
{#await promise then attribute}
|
||||
{#if objectsPromise}
|
||||
<Loading />
|
||||
{:else}
|
||||
{#each Array.from(values.keys()) as value}
|
||||
{@const realValue = [...(realValues.get(value) ?? [])][0]}
|
||||
<button
|
||||
class="menu-item"
|
||||
on:click={() => {
|
||||
toggle(value)
|
||||
}}
|
||||
>
|
||||
<div class="flex-between w-full">
|
||||
<div class="flex clear-mins">
|
||||
<div class="check pointer-events-none">
|
||||
<CheckBox checked={isSelected(value, selectedValues)} primary />
|
||||
</div>
|
||||
{#if value !== undefined}
|
||||
<svelte:component
|
||||
this={attribute.presenter}
|
||||
value={typeof value === 'string' ? realValue : value}
|
||||
{...attribute.props}
|
||||
/>
|
||||
{:else}
|
||||
<Label label={ui.string.NotSelected} />
|
||||
{/if}
|
||||
</div>
|
||||
<div class="content-dark-color ml-2">
|
||||
{values.get(value)}
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
{/each}
|
||||
{/if}
|
||||
{/await}
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
shape={'round'}
|
||||
label={view.string.Apply}
|
||||
on:click={() => {
|
||||
filter.value = Array.from(selectedValues.values()).map((p) => {
|
||||
return [p, Array.from(realValues.get(p) ?? [])]
|
||||
})
|
||||
onChange(filter)
|
||||
dispatch('close')
|
||||
{#if filter.mode !== view.filter.FilterDateCustom}
|
||||
<SelectPopup
|
||||
value={modes.map((it) => ({ ...it, id: it._id }))}
|
||||
on:close={(evt) => {
|
||||
filter.mode = evt.detail
|
||||
if (filter.mode === view.filter.FilterDateCustom) {
|
||||
showPicker()
|
||||
} else {
|
||||
onChange(filter)
|
||||
dispatch('close')
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
|
@ -22,6 +22,7 @@
|
||||
import { Filter, FilterMode } from '@hcengineering/view'
|
||||
import { createEventDispatcher, onDestroy } from 'svelte'
|
||||
import view from '../../plugin'
|
||||
import ModeSelector from './ModeSelector.svelte'
|
||||
|
||||
export let filter: Filter
|
||||
|
||||
@ -89,6 +90,22 @@
|
||||
|
||||
$: modeValuePromise = getMode(filter.mode)
|
||||
$: nestedModeValuePromise = filter.nested ? getMode(filter.nested.mode) : undefined
|
||||
|
||||
function clickHandler (e: MouseEvent, nested: boolean) {
|
||||
const curr = nested && filter.nested ? filter.nested : filter
|
||||
if (curr.modes.length <= 2) {
|
||||
toggle()
|
||||
} else {
|
||||
showPopup(ModeSelector, { filter: curr }, eventToHTMLElement(e), (res) => {
|
||||
if (nested && filter.nested) {
|
||||
filter.nested.mode = res
|
||||
} else {
|
||||
filter.mode = res
|
||||
}
|
||||
dispatch('change')
|
||||
})
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="filter-section">
|
||||
@ -102,8 +119,8 @@
|
||||
</button>
|
||||
<button
|
||||
class="filter-button"
|
||||
on:click={() => {
|
||||
toggle()
|
||||
on:click={(e) => {
|
||||
clickHandler(e, false)
|
||||
}}
|
||||
>
|
||||
{#await modeValuePromise then mode}
|
||||
|
@ -0,0 +1,40 @@
|
||||
<!--
|
||||
// Copyright © 2023 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 { getClient } from '@hcengineering/presentation'
|
||||
import { SelectPopup } from '@hcengineering/ui'
|
||||
import { Filter, FilterMode } from '@hcengineering/view'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import view from '../../plugin'
|
||||
|
||||
export let filter: Filter
|
||||
|
||||
const client = getClient()
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
let modes: FilterMode[] = []
|
||||
|
||||
client.findAll(view.class.FilterMode, { _id: { $in: filter.modes } }).then((res) => {
|
||||
modes = res
|
||||
})
|
||||
</script>
|
||||
|
||||
<SelectPopup
|
||||
value={modes.map((it) => ({ ...it, id: it._id }))}
|
||||
on:close={(evt) => {
|
||||
dispatch('close', evt.detail)
|
||||
}}
|
||||
/>
|
@ -81,6 +81,68 @@ export async function nestedDontMatchResult (filter: Filter, onUpdate: () => voi
|
||||
return { $nin: result }
|
||||
}
|
||||
|
||||
export async function dateOutdated (filter: Filter): Promise<ObjQueryType<any>> {
|
||||
return { $lt: new Date() }
|
||||
}
|
||||
|
||||
export async function dateToday (filter: Filter): Promise<ObjQueryType<any>> {
|
||||
const todayStart = new Date().setUTCHours(0, 0, 0, 0)
|
||||
const todayEnd = new Date().setUTCHours(23, 59, 59, 999)
|
||||
return { $gte: todayStart, $lte: todayEnd }
|
||||
}
|
||||
|
||||
export async function dateWeek (filter: Filter): Promise<ObjQueryType<any>> {
|
||||
const day = new Date().getDay()
|
||||
const startDayDiff = day === 0 ? 6 : day - 1
|
||||
const endDayDiff = 7 - startDayDiff
|
||||
const weekStart = new Date(new Date().setUTCDate(new Date().getUTCDate() - startDayDiff)).setUTCHours(0, 0, 0, 0)
|
||||
const weekEnd = new Date(new Date().setUTCDate(new Date().getUTCDate() + endDayDiff)).setUTCHours(23, 59, 59, 999)
|
||||
return { $gte: weekStart, $lte: weekEnd }
|
||||
}
|
||||
|
||||
export async function dateNextWeek (filter: Filter): Promise<ObjQueryType<any>> {
|
||||
const day = new Date().getDay()
|
||||
const startDayDiff = day === 0 ? 6 : day - 1
|
||||
const endDayDiff = 7 - startDayDiff
|
||||
const weekStart = new Date(new Date().setUTCDate(new Date().getUTCDate() - startDayDiff + 7)).setUTCHours(0, 0, 0, 0)
|
||||
const weekEnd = new Date(new Date().setUTCDate(new Date().getUTCDate() + endDayDiff + 7)).setUTCHours(23, 59, 59, 999)
|
||||
return { $gte: weekStart, $lte: weekEnd }
|
||||
}
|
||||
|
||||
export async function dateMonth (filter: Filter): Promise<ObjQueryType<any>> {
|
||||
const today = new Date()
|
||||
const lastDayOfMonth = new Date(today.getFullYear(), today.getMonth() + 1, 0)
|
||||
const monthStart = new Date(new Date().setUTCDate(1)).setUTCHours(0, 0, 0, 0)
|
||||
const monthEnd = lastDayOfMonth.setUTCHours(23, 59, 59, 999)
|
||||
return { $gte: monthStart, $lte: monthEnd }
|
||||
}
|
||||
|
||||
export async function dateNextMonth (filter: Filter): Promise<ObjQueryType<any>> {
|
||||
const today = new Date()
|
||||
const lastDayOfMonth = new Date(today.getFullYear(), today.getMonth() + 2, 0)
|
||||
const monthStart = new Date(new Date().setUTCMonth(new Date().getUTCMonth() + 1, 1)).setUTCHours(0, 0, 0, 0)
|
||||
const monthEnd = lastDayOfMonth.setUTCHours(23, 59, 59, 999)
|
||||
return { $gte: monthStart, $lte: monthEnd }
|
||||
}
|
||||
|
||||
export async function dateNotSpecified (filter: Filter): Promise<ObjQueryType<any>> {
|
||||
return { $in: [undefined, null] }
|
||||
}
|
||||
|
||||
export async function dateCustom (filter: Filter): Promise<ObjQueryType<any>> {
|
||||
if (filter.value.length === 1) {
|
||||
const todayStart = new Date(filter.value[0]).setUTCHours(0, 0, 0, 0)
|
||||
const todayEnd = new Date(filter.value[0]).setUTCHours(23, 59, 59, 999)
|
||||
return { $gte: todayStart, $lte: todayEnd }
|
||||
}
|
||||
if (filter.value.length === 2) {
|
||||
const todayStart = new Date(filter.value[0]).setUTCHours(0, 0, 0, 0)
|
||||
const todayEnd = new Date(filter.value[1]).setUTCHours(23, 59, 59, 999)
|
||||
return { $gte: todayStart, $lte: todayEnd }
|
||||
}
|
||||
return await dateNotSpecified(filter)
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
|
@ -77,6 +77,14 @@ import ViewletSettingButton from './components/ViewletSettingButton.svelte'
|
||||
import {
|
||||
afterResult,
|
||||
beforeResult,
|
||||
dateCustom,
|
||||
dateMonth,
|
||||
dateNextMonth,
|
||||
dateNextWeek,
|
||||
dateNotSpecified,
|
||||
dateOutdated,
|
||||
dateToday,
|
||||
dateWeek,
|
||||
nestedDontMatchResult,
|
||||
nestedMatchResult,
|
||||
objectInResult,
|
||||
@ -224,6 +232,14 @@ export default async (): Promise<Resources> => ({
|
||||
FilterNestedMatchResult: nestedMatchResult,
|
||||
FilterNestedDontMatchResult: nestedDontMatchResult,
|
||||
ShowEmptyGroups: showEmptyGroups,
|
||||
StatusSort: statusSort
|
||||
StatusSort: statusSort,
|
||||
FilterDateOutdated: dateOutdated,
|
||||
FilterDateToday: dateToday,
|
||||
FilterDateWeek: dateWeek,
|
||||
FilterDateNextWeek: dateNextWeek,
|
||||
FilterDateMonth: dateMonth,
|
||||
FilterDateNextMonth: dateNextMonth,
|
||||
FilterDateNotSpecified: dateNotSpecified,
|
||||
FilterDateCustom: dateCustom
|
||||
}
|
||||
})
|
||||
|
@ -64,7 +64,15 @@ export default mergeIds(viewId, view, {
|
||||
ShowPreviewOnClick: '' as IntlString,
|
||||
Shown: '' as IntlString,
|
||||
ShowEmptyGroups: '' as IntlString,
|
||||
Total: '' as IntlString
|
||||
Total: '' as IntlString,
|
||||
Overdue: '' as IntlString,
|
||||
Today: '' as IntlString,
|
||||
ThisWeek: '' as IntlString,
|
||||
NextWeek: '' as IntlString,
|
||||
ThisMonth: '' as IntlString,
|
||||
NextMonth: '' as IntlString,
|
||||
NotSpecified: '' as IntlString,
|
||||
CustomDate: '' as IntlString
|
||||
},
|
||||
function: {
|
||||
StatusSort: '' as SortFunc
|
||||
|
@ -732,7 +732,15 @@ const view = plugin(viewId, {
|
||||
FilterBefore: '' as Ref<FilterMode>,
|
||||
FilterAfter: '' as Ref<FilterMode>,
|
||||
FilterNestedMatch: '' as Ref<FilterMode>,
|
||||
FilterNestedDontMatch: '' as Ref<FilterMode>
|
||||
FilterNestedDontMatch: '' as Ref<FilterMode>,
|
||||
FilterDateOutdated: '' as Ref<FilterMode>,
|
||||
FilterDateToday: '' as Ref<FilterMode>,
|
||||
FilterDateWeek: '' as Ref<FilterMode>,
|
||||
FilterDateNextW: '' as Ref<FilterMode>,
|
||||
FilterDateM: '' as Ref<FilterMode>,
|
||||
FilterDateNextM: '' as Ref<FilterMode>,
|
||||
FilterDateNotSpecified: '' as Ref<FilterMode>,
|
||||
FilterDateCustom: '' as Ref<FilterMode>
|
||||
},
|
||||
popup: {
|
||||
PositionElementAlignment: '' as Resource<(e?: Event) => PopupAlignment | undefined>
|
||||
|
Loading…
Reference in New Issue
Block a user