UBER-668 UBER-669 UBER-670 UBER-671 (#3566)

Signed-off-by: Denis Bykhov <bykhov.denis@gmail.com>
This commit is contained in:
Denis Bykhov 2023-08-07 18:39:04 +06:00 committed by GitHub
parent 32fd80c971
commit afc881a7c6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 406 additions and 181 deletions

View File

@ -14,7 +14,15 @@
// //
import activity from '@hcengineering/activity' import activity from '@hcengineering/activity'
import { Calendar, Event, ReccuringEvent, ReccuringInstance, RecurringRule, calendarId } from '@hcengineering/calendar' import {
Calendar,
CalendarEventPresenter,
Event,
ReccuringEvent,
ReccuringInstance,
RecurringRule,
calendarId
} from '@hcengineering/calendar'
import { Contact } from '@hcengineering/contact' import { Contact } from '@hcengineering/contact'
import { DateRangeMode, Domain, IndexKind, Markup, Ref, Timestamp } from '@hcengineering/core' import { DateRangeMode, Domain, IndexKind, Markup, Ref, Timestamp } from '@hcengineering/core'
import { import {
@ -22,6 +30,7 @@ import {
Builder, Builder,
Collection, Collection,
Index, Index,
Mixin,
Model, Model,
Prop, Prop,
ReadOnly, ReadOnly,
@ -35,13 +44,14 @@ import {
} from '@hcengineering/model' } from '@hcengineering/model'
import attachment from '@hcengineering/model-attachment' import attachment from '@hcengineering/model-attachment'
import contact from '@hcengineering/model-contact' import contact from '@hcengineering/model-contact'
import core, { TAttachedDoc } from '@hcengineering/model-core' import core, { TAttachedDoc, TClass } from '@hcengineering/model-core'
import { TSpaceWithStates } from '@hcengineering/model-task' import { TSpaceWithStates } from '@hcengineering/model-task'
import view, { createAction } from '@hcengineering/model-view' import view, { createAction } from '@hcengineering/model-view'
import workbench from '@hcengineering/model-workbench' import workbench from '@hcengineering/model-workbench'
import notification from '@hcengineering/notification' import notification from '@hcengineering/notification'
import setting from '@hcengineering/setting' import setting from '@hcengineering/setting'
import calendar from './plugin' import calendar from './plugin'
import { AnyComponent } from '@hcengineering/ui'
export * from '@hcengineering/calendar' export * from '@hcengineering/calendar'
export { calendarId } from '@hcengineering/calendar' export { calendarId } from '@hcengineering/calendar'
@ -115,8 +125,13 @@ export class TReccuringInstance extends TEvent implements ReccuringInstance {
virtual?: boolean virtual?: boolean
} }
@Mixin(calendar.mixin.CalendarEventPresenter, core.class.Class)
export class TCalendarEventPresenter extends TClass implements CalendarEventPresenter {
presenter!: AnyComponent
}
export function createModel (builder: Builder): void { export function createModel (builder: Builder): void {
builder.createModel(TCalendar, TReccuringEvent, TReccuringInstance, TEvent) builder.createModel(TCalendar, TReccuringEvent, TReccuringInstance, TEvent, TCalendarEventPresenter)
builder.createDoc( builder.createDoc(
workbench.class.Application, workbench.class.Application,
@ -131,6 +146,10 @@ export function createModel (builder: Builder): void {
calendar.app.Calendar calendar.app.Calendar
) )
builder.mixin(calendar.class.Event, core.class.Class, calendar.mixin.CalendarEventPresenter, {
presenter: calendar.component.CalendarEventPresenter
})
builder.createDoc( builder.createDoc(
view.class.Viewlet, view.class.Viewlet,
core.space.Model, core.space.Model,

View File

@ -28,7 +28,8 @@ export default mergeIds(calendarId, calendar, {
IntegrationConnect: '' as AnyComponent, IntegrationConnect: '' as AnyComponent,
CreateCalendar: '' as AnyComponent, CreateCalendar: '' as AnyComponent,
EventPresenter: '' as AnyComponent, EventPresenter: '' as AnyComponent,
CalendarIntegrationIcon: '' as AnyComponent CalendarIntegrationIcon: '' as AnyComponent,
CalendarEventPresenter: '' as AnyComponent
}, },
action: { action: {
SaveEventReminder: '' as Ref<Action>, SaveEventReminder: '' as Ref<Action>,

View File

@ -172,7 +172,6 @@ export { default as Panel } from './components/Panel.svelte'
export { default as MonthCalendar } from './components/calendar/MonthCalendar.svelte' export { default as MonthCalendar } from './components/calendar/MonthCalendar.svelte'
export { default as YearCalendar } from './components/calendar/YearCalendar.svelte' export { default as YearCalendar } from './components/calendar/YearCalendar.svelte'
export { default as WeekCalendar } from './components/calendar/WeekCalendar.svelte' export { default as WeekCalendar } from './components/calendar/WeekCalendar.svelte'
export { default as DayCalendar } from './components/calendar/DayCalendar.svelte'
export { default as FocusHandler } from './components/FocusHandler.svelte' export { default as FocusHandler } from './components/FocusHandler.svelte'
export { default as ListView } from './components/ListView.svelte' export { default as ListView } from './components/ListView.svelte'

View File

@ -0,0 +1,37 @@
<!--
// 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 { Event } from '@hcengineering/calendar'
import { addZero } from '@hcengineering/ui'
export let event: Event
export let oneRow: boolean = false
export let narrow: boolean = false
export let size: { width: number; height: number }
$: startDate = new Date(event.date)
$: endDate = new Date(event.dueDate)
const getTime = (date: Date): string => {
return `${addZero(date.getHours())}:${addZero(date.getMinutes())}`
}
</script>
{#if !narrow}
<b class="overflow-label">{event.title}</b>
{/if}
{#if !oneRow}
<span class="overflow-label text-sm">{getTime(startDate)}-{getTime(endDate)}</span>
{/if}

View File

@ -13,7 +13,7 @@
// limitations under the License. // limitations under the License.
--> -->
<script lang="ts"> <script lang="ts">
import { Calendar, Event, getAllEvents } from '@hcengineering/calendar' import { Calendar, Event, generateEventId, getAllEvents } from '@hcengineering/calendar'
import { PersonAccount } from '@hcengineering/contact' import { PersonAccount } from '@hcengineering/contact'
import { import {
Class, Class,
@ -30,27 +30,25 @@
import { import {
AnyComponent, AnyComponent,
Button, Button,
CalendarItem,
DayCalendar,
DropdownLabelsIntl, DropdownLabelsIntl,
IconBack, IconBack,
IconForward, IconForward,
MILLISECONDS_IN_DAY,
MonthCalendar, MonthCalendar,
YearCalendar, YearCalendar,
areDatesEqual, areDatesEqual,
getMonday, getMonday,
showPopup showPopup
} from '@hcengineering/ui' } from '@hcengineering/ui'
import { CalendarMode } from '../index' import { CalendarMode, DayCalendar } from '../index'
import calendar from '../plugin' import calendar from '../plugin'
import Day from './Day.svelte' import Day from './Day.svelte'
import EventElement from './EventElement.svelte'
export let _class: Ref<Class<Doc>> = calendar.class.Event export let _class: Ref<Class<Doc>> = calendar.class.Event
export let query: DocumentQuery<Event> | undefined = undefined export let query: DocumentQuery<Event> | undefined = undefined
export let options: FindOptions<Event> | undefined = undefined export let options: FindOptions<Event> | undefined = undefined
export let createComponent: AnyComponent | undefined = calendar.component.CreateEvent export let createComponent: AnyComponent | undefined = calendar.component.CreateEvent
export let dragItem: Doc | undefined = undefined
export let dragEventClass: Ref<Class<Event>> = calendar.class.Event
export let allowedModes: CalendarMode[] = [ export let allowedModes: CalendarMode[] = [
CalendarMode.Days, CalendarMode.Days,
CalendarMode.Week, CalendarMode.Week,
@ -117,7 +115,6 @@
const calendarsQuery = createQuery() const calendarsQuery = createQuery()
let calendars: Calendar[] = [] let calendars: Calendar[] = []
const offsetTZ = new Date().getTimezoneOffset() * 60 * 1000
calendarsQuery.query(calendar.class.Calendar, { createdBy: me._id }, (res) => { calendarsQuery.query(calendar.class.Calendar, { createdBy: me._id }, (res) => {
calendars = res calendars = res
@ -233,6 +230,50 @@
ddItems = ddItems ddItems = ddItems
} }
const dragItemId = 'drag_item' as Ref<Event>
function dragEnter (e: CustomEvent<any>) {
if (dragItem !== undefined) {
const current = raw.find((p) => p._id === dragItemId)
if (current !== undefined) {
current.attachedTo = dragItem._id
current.attachedToClass = dragItem._class
current.date = e.detail.date.getTime()
current.dueDate = new Date(e.detail.date).setMinutes(new Date(e.detail.date).getMinutes() + 30)
} else {
const me = getCurrentAccount() as PersonAccount
raw.push({
_id: dragItemId,
allDay: false,
eventId: generateEventId(),
title: '',
description: '',
access: 'owner',
attachedTo: dragItem._id,
attachedToClass: dragItem._class,
_class: dragEventClass,
collection: 'events',
space: dragItem.space,
modifiedBy: me._id,
participants: [me.person],
modifiedOn: Date.now(),
date: e.detail.date.getTime(),
dueDate: new Date(e.detail.date).setMinutes(new Date(e.detail.date).getMinutes() + 30)
})
}
raw = raw
}
}
$: clear(dragItem)
function clear (dragItem: Doc | undefined) {
if (dragItem === undefined) {
raw = raw.filter((p) => p._id !== dragItemId)
objects = getAllEvents(raw, from, to)
}
}
$: getDdItems(allowedModes) $: getDdItems(allowedModes)
let ddItems: { let ddItems: {
@ -247,55 +288,6 @@
{ id: 'month', label: calendar.string.ModeMonth, mode: CalendarMode.Month }, { id: 'month', label: calendar.string.ModeMonth, mode: CalendarMode.Month },
{ id: 'year', label: calendar.string.ModeYear, mode: CalendarMode.Year } { id: 'year', label: calendar.string.ModeYear, mode: CalendarMode.Year }
] ]
const toCalendar = (
events: Event[],
date: Date,
days: number = 1,
startHour: number = 0,
endHour: number = 24
): CalendarItem[] => {
const result: CalendarItem[] = []
for (let day = 0; day < days; day++) {
const startDay = new Date(MILLISECONDS_IN_DAY * day + date.getTime()).setHours(0, 0, 0, 0)
const startDate = new Date(MILLISECONDS_IN_DAY * day + date.getTime()).setHours(startHour, 0, 0, 0)
const lastDate = new Date(MILLISECONDS_IN_DAY * day + date.getTime()).setHours(endHour, 0, 0, 0)
events.forEach((event) => {
const eventStart = event.allDay ? event.date + offsetTZ : event.date
const eventEnd = event.allDay ? event.dueDate + offsetTZ : event.dueDate
if ((eventStart < lastDate && eventEnd > startDate) || (eventStart === eventEnd && eventStart === startDay)) {
result.push({
_id: event._id,
allDay: event.allDay,
date: eventStart,
dueDate: eventEnd,
day,
access: event.access
})
}
})
}
const sd = date.setHours(0, 0, 0, 0)
const ld = new Date(MILLISECONDS_IN_DAY * (days - 1) + date.getTime()).setHours(23, 59, 59, 999)
events
.filter((ev) => ev.allDay)
.sort((a, b) => b.dueDate - b.date - (a.dueDate - a.date))
.forEach((event) => {
const eventStart = event.date + offsetTZ
const eventEnd = event.dueDate + offsetTZ
if ((eventStart < ld && eventEnd > sd) || (eventStart === eventEnd && eventStart === sd)) {
result.push({
_id: event._id,
allDay: event.allDay,
date: eventStart,
dueDate: eventEnd,
day: -1,
access: event.access
})
}
})
return result
}
</script> </script>
<div class="calendar-header"> <div class="calendar-header">
@ -383,7 +375,7 @@
</MonthCalendar> </MonthCalendar>
{:else if mode === CalendarMode.Week} {:else if mode === CalendarMode.Week}
<DayCalendar <DayCalendar
events={toCalendar(objects, currentDate, 7)} events={objects}
{mondayStart} {mondayStart}
displayedDaysCount={7} displayedDaysCount={7}
startFromWeekStart={false} startFromWeekStart={false}
@ -391,18 +383,12 @@
bind:currentDate bind:currentDate
on:create={(e) => showCreateDialog(e.detail.date, e.detail.withTime)} on:create={(e) => showCreateDialog(e.detail.date, e.detail.withTime)}
on:drop on:drop
> on:dragenter={dragEnter}
<svelte:fragment slot="event" let:id let:size> />
{@const event = objects.find((event) => event._id === id)}
{#if event}
<EventElement {event} {size} />
{/if}
</svelte:fragment>
</DayCalendar>
{:else if mode === CalendarMode.Day || mode === CalendarMode.Days} {:else if mode === CalendarMode.Day || mode === CalendarMode.Days}
{#key mode} {#key mode}
<DayCalendar <DayCalendar
events={toCalendar(objects, currentDate, mode === CalendarMode.Days ? 3 : 1)} events={objects}
{mondayStart} {mondayStart}
displayedDaysCount={mode === CalendarMode.Days ? 3 : 1} displayedDaysCount={mode === CalendarMode.Days ? 3 : 1}
startFromWeekStart={false} startFromWeekStart={false}
@ -410,14 +396,8 @@
bind:currentDate bind:currentDate
on:create={(e) => showCreateDialog(e.detail.date, e.detail.withTime)} on:create={(e) => showCreateDialog(e.detail.date, e.detail.withTime)}
on:drop on:drop
> on:dragenter={dragEnter}
<svelte:fragment slot="event" let:id let:size> />
{@const event = objects.find((event) => event._id === id)}
{#if event}
<EventElement {event} {size} />
{/if}
</svelte:fragment>
</DayCalendar>
{/key} {/key}
{/if} {/if}

View File

@ -13,28 +13,29 @@
// limitations under the License. // limitations under the License.
--> -->
<script lang="ts"> <script lang="ts">
import { Event } from '@hcengineering/calendar'
import { Timestamp } from '@hcengineering/core' import { Timestamp } from '@hcengineering/core'
import { createEventDispatcher, onMount } from 'svelte'
import ui, { import ui, {
resizeObserver,
deviceOptionsStore as deviceInfo,
Scroller,
Label,
ActionIcon, ActionIcon,
CalendarItem,
IconDownOutline,
IconUpOutline, IconUpOutline,
IconDownOutline Label,
} from '../..'
import {
MILLISECONDS_IN_DAY, MILLISECONDS_IN_DAY,
Scroller,
addZero, addZero,
areDatesEqual,
deviceOptionsStore as deviceInfo,
day as getDay, day as getDay,
getMonday, getMonday,
getWeekDayName, getWeekDayName,
areDatesEqual resizeObserver
} from './internal/DateUtils' } from '@hcengineering/ui'
import { CalendarItem } from '../../types' import { createEventDispatcher, onMount } from 'svelte'
import calendar from '../plugin'
import EventElement from './EventElement.svelte'
export let events: CalendarItem[] export let events: Event[]
export let mondayStart = true export let mondayStart = true
export let selectedDate: Date = new Date() export let selectedDate: Date = new Date()
export let currentDate: Date = selectedDate export let currentDate: Date = selectedDate
@ -54,6 +55,59 @@
} }
const rem = (n: number): number => n * fontSize const rem = (n: number): number => n * fontSize
const offsetTZ = new Date().getTimezoneOffset() * 60 * 1000
const toCalendar = (
events: Event[],
date: Date,
days: number = 1,
startHour: number = 0,
endHour: number = 24
): CalendarItem[] => {
const result: CalendarItem[] = []
for (let day = 0; day < days; day++) {
const startDay = new Date(MILLISECONDS_IN_DAY * day + date.getTime()).setHours(0, 0, 0, 0)
const startDate = new Date(MILLISECONDS_IN_DAY * day + date.getTime()).setHours(startHour, 0, 0, 0)
const lastDate = new Date(MILLISECONDS_IN_DAY * day + date.getTime()).setHours(endHour, 0, 0, 0)
events.forEach((event) => {
const eventStart = event.allDay ? event.date + offsetTZ : event.date
const eventEnd = event.allDay ? event.dueDate + offsetTZ : event.dueDate
if ((eventStart < lastDate && eventEnd > startDate) || (eventStart === eventEnd && eventStart === startDay)) {
result.push({
_id: event._id,
allDay: event.allDay,
date: eventStart,
dueDate: eventEnd,
day,
access: event.access
})
}
})
}
const sd = date.setHours(0, 0, 0, 0)
const ld = new Date(MILLISECONDS_IN_DAY * (days - 1) + date.getTime()).setHours(23, 59, 59, 999)
events
.filter((ev) => ev.allDay)
.sort((a, b) => b.dueDate - b.date - (a.dueDate - a.date))
.forEach((event) => {
const eventStart = event.date + offsetTZ
const eventEnd = event.dueDate + offsetTZ
if ((eventStart < ld && eventEnd > sd) || (eventStart === eventEnd && eventStart === sd)) {
result.push({
_id: event._id,
allDay: event.allDay,
date: eventStart,
dueDate: eventEnd,
day: -1,
access: event.access
})
}
})
return result
}
$: calendarEvents = toCalendar(events, currentDate, displayedDaysCount, startHour, displayedHours + startHour)
$: fontSize = $deviceInfo.fontSize $: fontSize = $deviceInfo.fontSize
$: docHeight = $deviceInfo.docHeight $: docHeight = $deviceInfo.docHeight
$: cellHeight = 4 * fontSize $: cellHeight = 4 * fontSize
@ -88,7 +142,7 @@
let calendarWidth: number = 0 let calendarWidth: number = 0
let calendarRect: DOMRect let calendarRect: DOMRect
let colWidth: number = 0 let colWidth: number = 0
let newEvents = events let newEvents = calendarEvents
let grid: CalendarGrid[] = Array<CalendarGrid>(displayedDaysCount) let grid: CalendarGrid[] = Array<CalendarGrid>(displayedDaysCount)
let alldays: CalendarItem[] = [] let alldays: CalendarItem[] = []
let alldaysGrid: CalendarADGrid[] = Array<CalendarADGrid>(displayedDaysCount) let alldaysGrid: CalendarADGrid[] = Array<CalendarADGrid>(displayedDaysCount)
@ -105,8 +159,8 @@
let shortAlldays: { id: string; day: number; fixRow?: boolean }[] = [] let shortAlldays: { id: string; day: number; fixRow?: boolean }[] = []
let moreCounts: number[] = Array<number>(displayedDaysCount) let moreCounts: number[] = Array<number>(displayedDaysCount)
$: if (newEvents !== events) { $: if (newEvents !== calendarEvents) {
newEvents = events newEvents = calendarEvents
grid = new Array<CalendarGrid>(displayedDaysCount) grid = new Array<CalendarGrid>(displayedDaysCount)
alldaysGrid = new Array<CalendarADGrid>(displayedDaysCount) alldaysGrid = new Array<CalendarADGrid>(displayedDaysCount)
alldays = [] alldays = []
@ -187,12 +241,12 @@
adMaxRow++ adMaxRow++
} }
const prepareAllDays = () => { const prepareAllDays = () => {
alldays = events.filter((ev) => ev.day === -1) alldays = calendarEvents.filter((ev) => ev.allDay)
adRows = [] adRows = []
for (let i = 0; i < displayedDaysCount; i++) alldaysGrid[i] = { alldays: [null] } for (let i = 0; i < displayedDaysCount; i++) alldaysGrid[i] = { alldays: [null] }
adMaxRow = 1 adMaxRow = 1
alldays.forEach((event) => { alldays.forEach((event) => {
const days = events const days = calendarEvents
.filter((ev) => ev.allDay && ev.day !== -1 && event._id === ev._id) .filter((ev) => ev.allDay && ev.day !== -1 && event._id === ev._id)
.map((ev) => { .map((ev) => {
return ev.day return ev.day
@ -288,7 +342,8 @@
result.bottom = result.bottom =
cellHeight * (displayedHours - startHour - endTime.hours - 1) + cellHeight * (displayedHours - startHour - endTime.hours - 1) +
((60 - endTime.mins) / 60) * cellHeight + ((60 - endTime.mins) / 60) * cellHeight +
getGridOffset(endTime.mins, true) getGridOffset(endTime.mins, true) +
(showHeader ? 0 : rem(2.5))
let cols = 1 let cols = 1
let index: number = 0 let index: number = 0
grid[event.day].columns.forEach((col, i) => grid[event.day].columns.forEach((col, i) =>
@ -414,7 +469,7 @@
{/each} {/each}
<div class="sticky-header allday-header text-sm content-dark-color"> <div class="sticky-header allday-header text-sm content-dark-color">
All day <Label label={calendar.string.AllDay} />
{#if (!minimizedAD && adMaxRow > maxAD) || (minimizedAD && adMaxRow > minAD)} {#if (!minimizedAD && adMaxRow > maxAD) || (minimizedAD && adMaxRow > minAD)}
<ActionIcon <ActionIcon
icon={shownAD ? IconUpOutline : IconDownOutline} icon={shownAD ? IconUpOutline : IconDownOutline}
@ -432,19 +487,26 @@
<div style:min-height={`${shownHeightAD - cellBorder * 2}px`} /> <div style:min-height={`${shownHeightAD - cellBorder * 2}px`} />
{#each alldays as event, i} {#each alldays as event, i}
{@const rect = getADRect(event._id)} {@const rect = getADRect(event._id)}
<!-- svelte-ignore a11y-no-noninteractive-tabindex --> {@const ev = events.find((p) => p._id === event._id)}
<div {#if ev}
class="calendar-element" <!-- svelte-ignore a11y-no-noninteractive-tabindex -->
style:top={`${rect.top}px`} <div
style:height={`${rect.height}px`} class="calendar-element"
style:left={`${rect.left}px`} style:top={`${rect.top}px`}
style:width={`${rect.width}px`} style:height={`${rect.height}px`}
style:opacity={rect.visibility === 0 ? 0.4 : 1} style:left={`${rect.left}px`}
style:--mask-image={getMask(rect.visibility)} style:width={`${rect.width}px`}
tabindex={500 + i} style:opacity={rect.visibility === 0 ? 0.4 : 1}
> style:--mask-image={getMask(rect.visibility)}
<slot name="event" id={event._id} size={{ width: rect.width, height: rect.height }} /> tabindex={500 + i}
</div> >
<EventElement
hourHeight={cellHeight}
event={ev}
size={{ width: rect.width, height: rect.height }}
/>
</div>
{/if}
{/each} {/each}
{/key}{/key}{/key} {/key}{/key}{/key}
</Scroller> </Scroller>
@ -452,38 +514,44 @@
{#key [styleAD, calendarWidth, displayedDaysCount]} {#key [styleAD, calendarWidth, displayedDaysCount]}
{#each alldays as event, i} {#each alldays as event, i}
{@const rect = getADRect(event._id)} {@const rect = getADRect(event._id)}
<!-- svelte-ignore a11y-no-noninteractive-tabindex --> {@const ev = events.find((p) => p._id === event._id)}
<div {#if ev}
class="calendar-element" <!-- svelte-ignore a11y-no-noninteractive-tabindex -->
style:top={`${rect.top}px`} <div
style:height={`${rect.height}px`} class="calendar-element"
style:left={`${rect.left}px`} style:top={`${rect.top}px`}
style:width={`${rect.width}px`} style:height={`${rect.height}px`}
style:opacity={rect.visibility === 0 ? 0.4 : 1} style:left={`${rect.left}px`}
style:--mask-image={getMask(rect.visibility)} style:width={`${rect.width}px`}
tabindex={500 + i} style:opacity={rect.visibility === 0 ? 0.4 : 1}
> style:--mask-image={getMask(rect.visibility)}
<slot name="event" id={event._id} size={{ width: rect.width, height: rect.height }} /> tabindex={500 + i}
</div> >
<EventElement hourHeight={cellHeight} event={ev} size={{ width: rect.width, height: rect.height }} />
</div>
{/if}
{/each} {/each}
{/key} {/key}
{:else} {:else}
{#key [styleAD, calendarWidth, displayedDaysCount]} {#key [styleAD, calendarWidth, displayedDaysCount]}
{#each shortAlldays as event, i} {#each shortAlldays as event, i}
{@const rect = getADRect(event.id, event.day, event.fixRow)} {@const rect = getADRect(event.id, event.day, event.fixRow)}
<!-- svelte-ignore a11y-no-noninteractive-tabindex --> {@const ev = events.find((p) => p._id === event.id)}
<div {#if ev}
class="calendar-element" <!-- svelte-ignore a11y-no-noninteractive-tabindex -->
style:top={`${rect.top}px`} <div
style:height={`${rect.height}px`} class="calendar-element"
style:left={`${rect.left}px`} style:top={`${rect.top}px`}
style:width={`${rect.width}px`} style:height={`${rect.height}px`}
style:opacity={rect.visibility === 0 ? 0.4 : 1} style:left={`${rect.left}px`}
style:--mask-image={getMask(rect.visibility)} style:width={`${rect.width}px`}
tabindex={500 + i} style:opacity={rect.visibility === 0 ? 0.4 : 1}
> style:--mask-image={getMask(rect.visibility)}
<slot name="event" id={event.id} size={{ width: rect.width, height: rect.height }} /> tabindex={500 + i}
</div> >
<EventElement hourHeight={cellHeight} event={ev} size={{ width: rect.width, height: rect.height }} />
</div>
{/if}
{/each} {/each}
{#each moreCounts as more, day} {#each moreCounts as more, day}
{@const addon = shortAlldays.length} {@const addon = shortAlldays.length}
@ -530,7 +598,11 @@
style:width={`${colWidth}px`} style:width={`${colWidth}px`}
style:grid-column={`col-start ${dayOfWeek + 1} / ${dayOfWeek + 2}`} style:grid-column={`col-start ${dayOfWeek + 1} / ${dayOfWeek + 2}`}
style:grid-row={`row-start ${hourOfDay * 2 + 1} / row-start ${hourOfDay * 2 + 3}`} style:grid-row={`row-start ${hourOfDay * 2 + 1} / row-start ${hourOfDay * 2 + 3}`}
on:dragover|preventDefault on:dragenter={(e) => {
dispatch('dragenter', {
date: new Date(day.setHours(hourOfDay + startHour, 0, 0, 0))
})
}}
on:drop|preventDefault={(e) => { on:drop|preventDefault={(e) => {
dispatch('drop', { dispatch('drop', {
day, day,
@ -552,26 +624,35 @@
{#key [styleAD, calendarWidth, displayedDaysCount]} {#key [styleAD, calendarWidth, displayedDaysCount]}
{#each newEvents.filter((ev) => !ev.allDay) as event, i} {#each newEvents.filter((ev) => !ev.allDay) as event, i}
{@const rect = getRect(event)} {@const rect = getRect(event)}
<!-- svelte-ignore a11y-no-noninteractive-tabindex --> {@const ev = events.find((p) => p._id === event._id)}
<div {#if ev}
class="calendar-element" <!-- svelte-ignore a11y-no-noninteractive-tabindex -->
style:top={`${rect.top}px`} <div
style:bottom={`${rect.bottom}px`} class="calendar-element"
style:left={`${rect.left}px`} style:top={`${rect.top}px`}
style:right={`${rect.right}px`} style:bottom={`${rect.bottom}px`}
style:opacity={rect.visibility === 0 ? 0.4 : 1} style:left={`${rect.left}px`}
style:--mask-image={'none'} style:right={`${rect.right}px`}
tabindex={1000 + i} style:opacity={rect.visibility === 0 ? 0.4 : 1}
> style:--mask-image={'none'}
<slot tabindex={1000 + i}
name="event" >
id={event._id} <EventElement
size={{ event={ev}
width: rect.width, hourHeight={cellHeight}
height: (calendarRect?.height ?? rect.top + rect.bottom) - rect.top - rect.bottom size={{
}} width: rect.width,
/> height: (calendarRect?.height ?? rect.top + rect.bottom) - rect.top - rect.bottom
</div> }}
on:drop={(e) => {
dispatch('drop', {
date: new Date(event.date)
})
}}
on:resize={() => (events = events)}
/>
</div>
{/if}
{/each} {/each}
{/key} {/key}
</div> </div>
@ -596,6 +677,7 @@
mask-image: var(--mask-image, none); mask-image: var(--mask-image, none);
--webkit-mask-image: var(--mask-image, none); --webkit-mask-image: var(--mask-image, none);
border-radius: 0.25rem; border-radius: 0.25rem;
pointer-events: none;
} }
.sticky-header { .sticky-header {
position: sticky; position: sticky;

View File

@ -13,47 +13,141 @@
// limitations under the License. // limitations under the License.
--> -->
<script lang="ts"> <script lang="ts">
import { Event } from '@hcengineering/calendar' import calendar, { CalendarEventPresenter, Event } from '@hcengineering/calendar'
import { MILLISECONDS_IN_MINUTE, addZero, showPanel, tooltip } from '@hcengineering/ui' import { Doc, DocumentUpdate } from '@hcengineering/core'
import view from '@hcengineering/view' import { getClient } from '@hcengineering/presentation'
import { Component, MILLISECONDS_IN_MINUTE, deviceOptionsStore, showPopup, tooltip } from '@hcengineering/ui'
import view, { ObjectEditor } from '@hcengineering/view'
import { createEventDispatcher } from 'svelte'
import EventPresenter from './EventPresenter.svelte' import EventPresenter from './EventPresenter.svelte'
export let event: Event export let event: Event
export let hourHeight: number
export let size: { width: number; height: number } export let size: { width: number; height: number }
$: startDate = new Date(event.date)
$: endDate = new Date(event.dueDate)
$: oneRow = size.height < 42 || event.allDay $: oneRow = size.height < 42 || event.allDay
$: narrow = event.dueDate - event.date < MILLISECONDS_IN_MINUTE * 25 $: narrow = event.dueDate - event.date < MILLISECONDS_IN_MINUTE * 25
$: empty = size.width < 44 $: empty = size.width < 44
const getTime = (date: Date): string => { function click () {
return `${addZero(date.getHours())}:${addZero(date.getMinutes())}` const editor = hierarchy.classHierarchyMixin<Doc, ObjectEditor>(event._class, view.mixin.ObjectEditor)
if (editor?.editor !== undefined) {
showPopup(editor.editor, { object: event })
}
}
const client = getClient()
const hierarchy = client.getHierarchy()
$: presenter = hierarchy.classHierarchyMixin<Doc, CalendarEventPresenter>(
event._class,
calendar.mixin.CalendarEventPresenter
)
let div: HTMLDivElement
const dispatch = createEventDispatcher()
$: fontSize = $deviceOptionsStore.fontSize
function dragStart (e: DragEvent) {
if (event.allDay) return
originDate = event.date
originDueDate = event.dueDate
const rect = div.getBoundingClientRect()
const topThreshold = rect.y + fontSize / 2
if (e.dataTransfer) {
e.dataTransfer.effectAllowed = 'move'
e.dataTransfer.dropEffect = 'move'
}
dragInitY = e.y
if (e.y < topThreshold) {
dragDirection = 'top'
} else {
const bottomThreshold = rect.y + rect.height - fontSize / 2
if (e.y > bottomThreshold) {
dragDirection = 'bottom'
} else {
dragDirection = 'mid'
}
}
}
let originDate = event.date
let originDueDate = event.dueDate
$: pixelPer15Min = hourHeight / 4
let dragInitY: number | undefined
let dragDirection: 'bottom' | 'mid' | 'top' | undefined
function drag (e: DragEvent) {
if (event.allDay) return
if (dragInitY !== undefined) {
const diff = Math.floor((e.y - dragInitY) / pixelPer15Min)
if (diff) {
if (dragDirection !== 'bottom') {
const newValue = new Date(originDate).setMinutes(new Date(originDate).getMinutes() + 15 * diff)
if (dragDirection === 'top') {
if (newValue < event.dueDate) {
event.date = newValue
dispatch('resize')
}
} else {
const newDue = new Date(originDueDate).setMinutes(new Date(originDueDate).getMinutes() + 15 * diff)
event.date = newValue
event.dueDate = newDue
dispatch('resize')
}
} else {
const newDue = new Date(originDueDate).setMinutes(new Date(originDueDate).getMinutes() + 15 * diff)
if (newDue > event.date) {
event.dueDate = newDue
dispatch('resize')
}
}
}
}
}
async function drop () {
const update: DocumentUpdate<Event> = {}
if (originDate !== event.date) {
update.date = event.date
}
if (originDueDate !== event.dueDate) {
update.dueDate = event.dueDate
}
if (Object.keys(update).length > 0) {
await client.update(event, {
dueDate: event.dueDate,
date: event.date
})
}
} }
</script> </script>
{#if event} {#if event}
<!-- svelte-ignore a11y-click-events-have-key-events --> <!-- svelte-ignore a11y-click-events-have-key-events -->
<div <div
bind:this={div}
class="event-container" class="event-container"
class:oneRow class:oneRow
class:empty class:empty
draggable={!event.allDay}
use:tooltip={{ component: EventPresenter, props: { value: event } }} use:tooltip={{ component: EventPresenter, props: { value: event } }}
on:click|stopPropagation={() => { on:click|stopPropagation={click}
if (event) showPanel(view.component.EditDoc, event._id, event._class, 'content') on:dragstart={dragStart}
}} on:drag={drag}
on:dragend={drop}
on:drop
> >
{#if !narrow && !empty} {#if !empty && presenter?.presenter}
<b class="overflow-label">{event.title}</b> <Component is={presenter.presenter} props={{ event, narrow, oneRow }} />
{/if}
{#if !oneRow && !empty}
<span class="overflow-label text-sm">{getTime(startDate)}-{getTime(endDate)}</span>
{/if} {/if}
</div> </div>
{/if} {/if}
<style lang="scss"> <style lang="scss">
.event-container { .event-container {
pointer-events: auto;
overflow: hidden; overflow: hidden;
display: flex; display: flex;
flex-direction: column; flex-direction: column;

View File

@ -33,11 +33,13 @@ import UpdateRecInstancePopup from './components/UpdateRecInstancePopup.svelte'
import ReminderViewlet from './components/activity/ReminderViewlet.svelte' import ReminderViewlet from './components/activity/ReminderViewlet.svelte'
import CalendarIntegrationIcon from './components/icons/Calendar.svelte' import CalendarIntegrationIcon from './components/icons/Calendar.svelte'
import EventElement from './components/EventElement.svelte' import EventElement from './components/EventElement.svelte'
import CalendarEventPresenter from './components/CalendarEventPresenter.svelte'
import DayCalendar from './components/DayCalendar.svelte'
import calendar from './plugin' import calendar from './plugin'
import contact from '@hcengineering/contact' import contact from '@hcengineering/contact'
import { deleteObjects } from '@hcengineering/view-resources' import { deleteObjects } from '@hcengineering/view-resources'
export { EventElement, CalendarView } export { EventElement, CalendarView, DayCalendar }
async function saveEventReminder (object: Doc): Promise<void> { async function saveEventReminder (object: Doc): Promise<void> {
showPopup(SaveEventReminder, { objectId: object._id, objectClass: object._class }) showPopup(SaveEventReminder, { objectId: object._id, objectClass: object._class })
@ -148,7 +150,8 @@ export default async (): Promise<Resources> => ({
EventPresenter, EventPresenter,
CreateEvent, CreateEvent,
IntegrationConnect, IntegrationConnect,
CalendarIntegrationIcon CalendarIntegrationIcon,
CalendarEventPresenter
}, },
activity: { activity: {
ReminderViewlet ReminderViewlet

View File

@ -12,7 +12,7 @@
// limitations under the License. // limitations under the License.
import { Contact } from '@hcengineering/contact' import { Contact } from '@hcengineering/contact'
import type { AttachedDoc, Class, Doc, Markup, Ref, Space, Timestamp } from '@hcengineering/core' import type { AttachedDoc, Class, Doc, Markup, Mixin, Ref, Space, Timestamp } from '@hcengineering/core'
import { NotificationType } from '@hcengineering/notification' import { NotificationType } from '@hcengineering/notification'
import type { Asset, IntlString, Metadata, Plugin } from '@hcengineering/platform' import type { Asset, IntlString, Metadata, Plugin } from '@hcengineering/platform'
import { plugin } from '@hcengineering/platform' import { plugin } from '@hcengineering/platform'
@ -94,6 +94,13 @@ export interface ReccuringInstance extends Event {
virtual?: boolean virtual?: boolean
} }
/**
* @public
*/
export interface CalendarEventPresenter extends Class<Event> {
presenter: AnyComponent
}
/** /**
* @public * @public
*/ */
@ -109,6 +116,9 @@ const calendarPlugin = plugin(calendarId, {
ReccuringEvent: '' as Ref<Class<ReccuringEvent>>, ReccuringEvent: '' as Ref<Class<ReccuringEvent>>,
ReccuringInstance: '' as Ref<Class<ReccuringInstance>> ReccuringInstance: '' as Ref<Class<ReccuringInstance>>
}, },
mixin: {
CalendarEventPresenter: '' as Ref<Mixin<CalendarEventPresenter>>
},
icon: { icon: {
Calendar: '' as Asset, Calendar: '' as Asset,
Location: '' as Asset, Location: '' as Asset,

View File

@ -261,11 +261,7 @@ export function getAllEvents (events: Event[], from: Timestamp, to: Timestamp):
const recurData: ReccuringInstance[] = [] const recurData: ReccuringInstance[] = []
const instancesMap: Map<string, ReccuringInstance[]> = new Map() const instancesMap: Map<string, ReccuringInstance[]> = new Map()
for (const event of events) { for (const event of events) {
if (event._class === calendar.class.Event) { if (event._class === calendar.class.ReccuringEvent) {
if (from > event.dueDate) continue
if (event.date > to) continue
base.push(event)
} else if (event._class === calendar.class.ReccuringEvent) {
recur.push(event as ReccuringEvent) recur.push(event as ReccuringEvent)
} else if (event._class === calendar.class.ReccuringInstance) { } else if (event._class === calendar.class.ReccuringInstance) {
const instance = event as ReccuringInstance const instance = event as ReccuringInstance
@ -273,6 +269,10 @@ export function getAllEvents (events: Event[], from: Timestamp, to: Timestamp):
const arr = instancesMap.get(instance.recurringEventId) ?? [] const arr = instancesMap.get(instance.recurringEventId) ?? []
arr.push(instance) arr.push(instance)
instancesMap.set(instance.recurringEventId, arr) instancesMap.set(instance.recurringEventId, arr)
} else {
if (from > event.dueDate) continue
if (event.date > to) continue
base.push(event)
} }
} }
for (const rec of recur) { for (const rec of recur) {