Signed-off-by: Denis Bykhov <bykhov.denis@gmail.com>
This commit is contained in:
Denis Bykhov 2023-08-14 18:35:07 +06:00 committed by GitHub
parent e92918c6c7
commit 30f255fa7c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 249 additions and 54 deletions

View File

@ -50,8 +50,8 @@ import view, { createAction } from '@hcengineering/model-view'
import workbench from '@hcengineering/model-workbench'
import notification from '@hcengineering/notification'
import setting from '@hcengineering/setting'
import calendar from './plugin'
import { AnyComponent } from '@hcengineering/ui'
import calendar from './plugin'
export * from '@hcengineering/calendar'
export { calendarId } from '@hcengineering/calendar'
@ -62,13 +62,16 @@ export const DOMAIN_CALENDAR = 'calendar' as Domain
@Model(calendar.class.Calendar, core.class.Space)
@UX(calendar.string.Calendar, calendar.icon.Calendar)
export class TCalendar extends TSpaceWithStates implements Calendar {
@Prop(TypeString(), calendar.string.HideDetails)
hideDetails?: boolean
visibility!: 'public' | 'freeBusy' | 'private'
sync?: boolean
}
@Model(calendar.class.Event, core.class.AttachedDoc, DOMAIN_CALENDAR)
@UX(calendar.string.Event, calendar.icon.Calendar)
export class TEvent extends TAttachedDoc implements Event {
declare space: Ref<Calendar>
eventId!: string
@Prop(TypeString(), calendar.string.Title)
@ -106,6 +109,8 @@ export class TEvent extends TAttachedDoc implements Event {
externalParticipants?: string[]
access!: 'freeBusyReader' | 'reader' | 'writer' | 'owner'
visibility?: 'public' | 'freeBusy' | 'private'
}
@Model(calendar.class.ReccuringEvent, calendar.class.Event)
@ -174,7 +179,8 @@ export function createModel (builder: Builder): void {
icon: calendar.component.CalendarIntegrationIcon,
createComponent: calendar.component.IntegrationConnect,
onDisconnect: calendar.handler.DisconnectHandler,
reconnectComponent: calendar.component.IntegrationConnect
reconnectComponent: calendar.component.IntegrationConnect,
configureComponent: calendar.component.IntegrationConfigure
},
calendar.integrationType.Calendar
)

View File

@ -33,7 +33,8 @@ async function migrateCalendars (tx: TxOperations): Promise<void> {
description: '',
archived: false,
private: false,
members: [user._id]
members: [user._id],
visibility: 'public'
},
`${user._id}_calendar` as Ref<Calendar>,
undefined,
@ -49,6 +50,11 @@ async function migrateCalendars (tx: TxOperations): Promise<void> {
if (space !== undefined) {
await tx.remove(space)
}
const calendars = await tx.findAll(calendar.class.Calendar, { visibility: { $exists: false } })
for (const calendar of calendars) {
await tx.update(calendar, { visibility: 'public' })
}
}
async function fixEventDueDate (client: MigrationClient): Promise<void> {

View File

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

View File

@ -73,6 +73,8 @@
"SundayShort": "Su",
"OnUntil": "On",
"Times": "{count, plural, one {time} other {times}}",
"AddParticipants": "Add participants"
"AddParticipants": "Add participants",
"Sync": "Synchronization",
"Busy": "Busy"
}
}

View File

@ -73,7 +73,8 @@
"SundayShort": "Вс",
"OnUntil": "До",
"Times": "{count, plural, one {раз} few {раза} other {раз}}",
"AddParticipants": "Добавить участников"
"AddParticipants": "Добавить участников",
"Sync": "Синхронизация",
"Busy": "Занято"
}
}

View File

@ -14,9 +14,11 @@
-->
<script lang="ts">
import { Event } from '@hcengineering/calendar'
import { addZero } from '@hcengineering/ui'
import { Label, addZero } from '@hcengineering/ui'
import calendar from '../plugin'
export let event: Event
export let hideDetails: boolean = false
export let oneRow: boolean = false
export let narrow: boolean = false
export let size: { width: number; height: number }
@ -30,7 +32,13 @@
</script>
{#if !narrow}
<b class="overflow-label">{event.title}</b>
{#if !hideDetails}
<b class="overflow-label">
{event.title}
</b>
{:else}
<span class="content-dark-color"><Label label={calendar.string.Busy} /></span>
{/if}
{/if}
{#if !oneRow}
<span class="overflow-label text-sm">{getTime(startDate)}-{getTime(endDate)}</span>

View File

@ -39,7 +39,7 @@
getMonday,
showPopup
} from '@hcengineering/ui'
import { CalendarMode, DayCalendar } from '../index'
import { CalendarMode, DayCalendar, calendarStore, hidePrivateEvents } from '../index'
import calendar from '../plugin'
import Day from './Day.svelte'
@ -66,6 +66,7 @@
let selectedDate: Date = new Date()
let raw: Event[] = []
let visible: Event[] = []
let objects: Event[] = []
function getFrom (date: Date, mode: CalendarMode): Timestamp {
@ -116,7 +117,7 @@
let calendars: Calendar[] = []
calendarsQuery.query(calendar.class.Calendar, { createdBy: me._id }, (res) => {
calendarsQuery.query(calendar.class.Calendar, { members: me._id }, (res) => {
calendars = res
})
@ -138,7 +139,8 @@
)
}
$: update(_class, query, calendars, options)
$: objects = getAllEvents(raw, from, to)
$: visible = hidePrivateEvents(raw, $calendarStore)
$: objects = getAllEvents(visible, from, to)
function inRange (start: Date, end: Date, startPeriod: Date, period: 'day' | 'hour'): boolean {
const endPeriod =
@ -242,7 +244,7 @@
current.dueDate = new Date(e.detail.date).setMinutes(new Date(e.detail.date).getMinutes() + 30)
} else {
const me = getCurrentAccount() as PersonAccount
raw.push({
const temp: Event = {
_id: dragItemId,
allDay: false,
eventId: generateEventId(),
@ -253,13 +255,14 @@
attachedToClass: dragItem._class,
_class: dragEventClass,
collection: 'events',
space: dragItem.space,
space: `${me._id}_calendar` as Ref<Calendar>,
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.push(temp)
}
raw = raw
}
@ -270,7 +273,8 @@
function clear (dragItem: Doc | undefined) {
if (dragItem === undefined) {
raw = raw.filter((p) => p._id !== dragItemId)
objects = getAllEvents(raw, from, to)
visible = hidePrivateEvents(raw, $calendarStore)
objects = getAllEvents(visible, from, to)
}
}

View File

@ -20,7 +20,7 @@
import { Button, CheckBox, DAY, EditBox, Icon, IconClose, Label, closePopup, showPopup } from '@hcengineering/ui'
import { createEventDispatcher } from 'svelte'
import calendar from '../plugin'
import { saveUTC } from '../utils'
import { isReadOnly, saveUTC } from '../utils'
import EventParticipants from './EventParticipants.svelte'
import EventTimeEditor from './EventTimeEditor.svelte'
import RRulePresenter from './RRulePresenter.svelte'
@ -29,6 +29,8 @@
export let object: Event
$: readOnly = isReadOnly(object)
let title = object.title
const defaultDuration = 60 * 60 * 1000
@ -54,6 +56,9 @@
}
async function saveEvent () {
if (readOnly) {
return
}
const update: DocumentUpdate<Event> = {}
if (object.title !== title) {
update.title = title.trim()
@ -97,6 +102,9 @@
}
function setRecurrance () {
if (readOnly) {
return
}
showPopup(ReccurancePopup, { rules }, undefined, (res) => {
if (res) {
rules = res
@ -196,7 +204,7 @@
<div class="container">
<div class="header flex-between">
{#if object.attachedTo === calendar.ids.NoAttached}
<EditBox bind:value={title} placeholder={calendar.string.NewEvent} />
<EditBox bind:value={title} placeholder={calendar.string.NewEvent} disabled={readOnly} />
{:else}
<div />
{/if}
@ -213,7 +221,7 @@
/>
</div>
<div class="time">
<EventTimeEditor {allDay} bind:startDate bind:dueDate />
<EventTimeEditor {allDay} bind:startDate bind:dueDate disabled={readOnly} />
<div>
{#if !allDay && rules.length === 0}
<div class="flex-row-center flex-gap-3 ext">
@ -232,7 +240,7 @@
{:else}
<div>
<div class="flex-row-center flex-gap-2 mt-1">
<CheckBox bind:checked={allDay} accented on:value={allDayChangeHandler} />
<CheckBox bind:checked={allDay} accented on:value={allDayChangeHandler} readonly={readOnly} />
<Label label={calendar.string.AllDay} />
</div>
<div class="flex-row-center flex-gap-2 mt-1">
@ -255,24 +263,19 @@
</div>
<div class="divider" />
<div>
<EventParticipants bind:participants bind:externalParticipants />
<EventParticipants bind:participants bind:externalParticipants disabled={readOnly} />
</div>
<div class="divider" />
<div class="block">
<div class="flex-row-center flex-gap-2">
<Icon icon={calendar.icon.Description} size="small" />
<EditBox bind:value={description} placeholder={calendar.string.Description} />
<EditBox bind:value={description} placeholder={calendar.string.Description} disabled={readOnly} />
</div>
</div>
<div class="divider" />
<div class="flex-between pool">
<div />
<Button
kind="accented"
label={presentation.string.Save}
on:click={saveEvent}
disabled={title === '' && object.attachedTo === calendar.ids.NoAttached}
/>
<Button kind="accented" label={presentation.string.Save} disabled={readOnly} on:click={saveEvent} />
</div>
</div>

View File

@ -25,9 +25,10 @@
tooltip
} from '@hcengineering/ui'
import view, { ObjectEditor } from '@hcengineering/view'
import { createEventDispatcher } from 'svelte'
import EventPresenter from './EventPresenter.svelte'
import { Menu } from '@hcengineering/view-resources'
import { createEventDispatcher } from 'svelte'
import { calendarStore, isReadOnly, isVisible } from '../utils'
import EventPresenter from './EventPresenter.svelte'
export let event: Event
export let hourHeight: number
@ -38,9 +39,11 @@
$: empty = size.width < 44
function click () {
const editor = hierarchy.classHierarchyMixin<Doc, ObjectEditor>(event._class, view.mixin.ObjectEditor)
if (editor?.editor !== undefined) {
showPopup(editor.editor, { object: event })
if (visible) {
const editor = hierarchy.classHierarchyMixin<Doc, ObjectEditor>(event._class, view.mixin.ObjectEditor)
if (editor?.editor !== undefined) {
showPopup(editor.editor, { object: event })
}
}
}
@ -58,6 +61,7 @@
$: fontSize = $deviceOptionsStore.fontSize
function dragStart (e: DragEvent) {
if (readOnly) return
if (event.allDay) return
originDate = event.date
originDueDate = event.dueDate
@ -87,6 +91,7 @@
let dragDirection: 'bottom' | 'mid' | 'top' | undefined
function drag (e: DragEvent) {
if (readOnly) return
if (event.allDay) return
if (dragInitY !== undefined) {
const diff = Math.floor((e.y - dragInitY) / pixelPer15Min)
@ -135,6 +140,9 @@
ev.preventDefault()
showPopup(Menu, { object: event }, getEventPositionElement(ev))
}
$: visible = isVisible(event, $calendarStore)
$: readOnly = isReadOnly(event)
</script>
{#if event}
@ -145,7 +153,7 @@
class:oneRow
class:empty
draggable={!event.allDay}
use:tooltip={{ component: EventPresenter, props: { value: event } }}
use:tooltip={{ component: EventPresenter, props: { value: event, hideDetails: !visible } }}
on:click|stopPropagation={click}
on:contextmenu={showMenu}
on:dragstart={dragStart}
@ -154,7 +162,7 @@
on:drop
>
{#if !empty && presenter?.presenter}
<Component is={presenter.presenter} props={{ event, narrow, oneRow }} />
<Component is={presenter.presenter} props={{ event, narrow, oneRow, hideDetails: !visible }} />
{/if}
</div>
{/if}

View File

@ -22,6 +22,7 @@
export let participants: Ref<Person>[]
export let externalParticipants: string[]
export let disabled: boolean = false
$: placeholder =
participants.length > 0 || externalParticipants.length > 0

View File

@ -14,10 +14,11 @@
-->
<script lang="ts">
import { Event } from '@hcengineering/calendar'
import { DatePresenter, DateTimeRangePresenter, showPopup } from '@hcengineering/ui'
import { DatePresenter, DateTimeRangePresenter, Label, showPopup } from '@hcengineering/ui'
import calendar from '../plugin'
export let value: Event
export let hideDetails: boolean = false
export let inline: boolean = false
function click (): void {
@ -29,7 +30,11 @@
<div class="antiSelect w-full cursor-pointer flex-center flex-between" on:click={click}>
{#if value}
<div class="mr-4">
{value.title}
{#if !hideDetails}
{value.title}
{:else}
<Label label={calendar.string.Busy} />
{/if}
</div>
{#if !inline}
{#if value.allDay}

View File

@ -20,6 +20,7 @@
export let startDate: number
export let dueDate: number
export let allDay: boolean
export let disabled: boolean = false
$: sameDate = areDatesEqual(new Date(startDate), new Date(dueDate))
@ -46,7 +47,7 @@
<div class="flex-row-center flex-gap-1 mt-2 mb-2">
<Icon icon={calendar.icon.Watch} size="small" />
{#if sameDate}
<DateEditor bind:date={startDate} direction="horizontal" withoutTime={allDay} on:update={dateChange} />
<DateEditor bind:date={startDate} direction="horizontal" withoutTime={allDay} on:update={dateChange} {disabled} />
<div class="content-dark-color">
<IconArrowRight size="small" />
</div>
@ -55,13 +56,14 @@
direction="horizontal"
withoutTime={allDay}
showDate={false}
{disabled}
on:update={dueChange}
/>
{:else}
<DateEditor bind:date={startDate} direction="vertical" withoutTime={allDay} on:update={dateChange} />
<DateEditor bind:date={startDate} direction="vertical" withoutTime={allDay} on:update={dateChange} {disabled} />
<div class="content-dark-color">
<IconArrowRight size="small" />
</div>
<DateEditor bind:date={dueDate} direction="vertical" withoutTime={allDay} on:update={dueChange} />
<DateEditor bind:date={dueDate} direction="vertical" withoutTime={allDay} on:update={dueChange} {disabled} />
{/if}
</div>

View File

@ -0,0 +1,71 @@
<!--
// 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 { Calendar } from '@hcengineering/calendar'
import { getCurrentAccount } from '@hcengineering/core'
import presentation, { Card, createQuery, getClient } from '@hcengineering/presentation'
import { Grid, Label, Toggle } from '@hcengineering/ui'
import { createEventDispatcher } from 'svelte'
import calendar from '../plugin'
const client = getClient()
let calendars: Calendar[] = []
const query = createQuery()
query.query(
calendar.class.Calendar,
{
createdBy: getCurrentAccount()._id
},
(res) => (calendars = res)
)
async function update (calendar: Calendar, value: boolean) {
await client.update(calendar, {
sync: value
})
}
const dispatch = createEventDispatcher()
</script>
<Card
label={calendar.string.Calendars}
okAction={() => {
dispatch('close')
}}
canSave={true}
fullSize
okLabel={presentation.string.Ok}
on:close={() => dispatch('close')}
on:changeContent
>
<div style="width: 25rem;">
<Grid rowGap={1}>
<div>
<Label label={calendar.string.Calendar} />
</div>
<div>
<Label label={calendar.string.Sync} />
</div>
{#each calendars as calendar}
<div>{calendar.name}</div>
<div>
<Toggle bind:on={calendar.sync} on:change={(res) => update(calendar, res.detail)} />
</div>
{/each}
</Grid>
</div>
</Card>

View File

@ -34,12 +34,15 @@ import CalendarIntegrationIcon from './components/icons/Calendar.svelte'
import EventElement from './components/EventElement.svelte'
import CalendarEventPresenter from './components/CalendarEventPresenter.svelte'
import DayCalendar from './components/DayCalendar.svelte'
import IntegrationConfigure from './components/IntegrationConfigure.svelte'
import calendar from './plugin'
import contact from '@hcengineering/contact'
import { deleteObjects } from '@hcengineering/view-resources'
export { EventElement, CalendarView, DayCalendar }
export * from './utils'
async function saveEventReminder (object: Doc): Promise<void> {
showPopup(SaveEventReminder, { objectId: object._id, objectClass: object._class })
}
@ -70,7 +73,8 @@ async function deleteRecHandler (res: any, object: ReccuringInstance): Promise<v
rdate: object.rdate,
rules: object.rules,
exdate: object.exdate,
access: 'owner'
visibility: object.visibility,
access: object.access
},
object._id
)
@ -152,7 +156,8 @@ export default async (): Promise<Resources> => ({
CreateEvent,
IntegrationConnect,
CalendarIntegrationIcon,
CalendarEventPresenter
CalendarEventPresenter,
IntegrationConfigure
},
activity: {
ReminderViewlet

View File

@ -75,6 +75,8 @@ export default mergeIds(calendarId, calendar, {
SaturdayShort: '' as IntlString,
SundayShort: '' as IntlString,
Times: '' as IntlString,
AddParticipants: '' as IntlString
AddParticipants: '' as IntlString,
Sync: '' as IntlString,
Busy: '' as IntlString
}
})

View File

@ -1,4 +1,8 @@
import { Timestamp } from '@hcengineering/core'
import { Calendar, Event } from '@hcengineering/calendar'
import { IdMap, Timestamp, getCurrentAccount, toIdMap } from '@hcengineering/core'
import { createQuery, getClient } from '@hcengineering/presentation'
import { writable } from 'svelte/store'
import calendar from './plugin'
export function saveUTC (date: Timestamp): Timestamp {
const utcdate = new Date(date)
@ -12,3 +16,63 @@ export function saveUTC (date: Timestamp): Timestamp {
utcdate.getMilliseconds()
)
}
export function hidePrivateEvents (events: Event[], calendars: IdMap<Calendar>): Event[] {
const me = getCurrentAccount()._id
const res: Event[] = []
for (const event of events) {
if ((event.createdBy ?? event.modifiedBy) === me) {
res.push(event)
} else {
if (event.visibility !== undefined) {
if (event.visibility !== 'private') {
res.push(event)
}
} else {
const space = calendars.get(event.space)
if (space != null && space.visibility !== 'private') {
res.push(event)
}
}
}
}
return res
}
export function isReadOnly (value: Event): boolean {
const me = getCurrentAccount()._id
if (value.createdBy !== me) return true
if (['owner', 'writer'].includes(value.access)) return false
return true
}
export function isVisible (value: Event, calendars: IdMap<Calendar>): boolean {
const me = getCurrentAccount()._id
if (value.createdBy === me) return true
if (value.visibility === 'freeBusy') {
return false
}
const space = calendars.get(value.space)
if (space == null) {
return true
} else {
return space.visibility === 'public'
}
}
export const calendarStore = writable<IdMap<Calendar>>(new Map())
function fillStores (): void {
const client = getClient()
if (client !== undefined) {
const query = createQuery(true)
query.query(calendar.class.Calendar, {}, (res) => {
calendarStore.set(toIdMap(res))
})
} else {
setTimeout(() => fillStores(), 50)
}
}
fillStores()

View File

@ -22,7 +22,10 @@ import { AnyComponent } from '@hcengineering/ui'
/**
* @public
*/
export interface Calendar extends Space {}
export interface Calendar extends Space {
visibility: 'public' | 'freeBusy' | 'private'
sync?: boolean
}
/**
* @public
@ -58,6 +61,7 @@ export interface ReccuringEvent extends Event {
* @public
*/
export interface Event extends AttachedDoc {
space: Ref<Calendar>
eventId: string
title: string
description: Markup
@ -80,6 +84,8 @@ export interface Event extends AttachedDoc {
reminders?: Timestamp[]
visibility?: 'public' | 'freeBusy' | 'private'
access: 'freeBusyReader' | 'reader' | 'writer' | 'owner'
}
@ -132,7 +138,7 @@ const calendarPlugin = plugin(calendarId, {
},
space: {
// deprecated
PersonalEvents: '' as Ref<Space>
PersonalEvents: '' as Ref<Calendar>
},
app: {
Calendar: '' as Ref<Doc>

View File

@ -94,7 +94,8 @@ export async function OnPersonAccountCreate (tx: Tx, control: TriggerControl): P
description: '',
archived: false,
private: false,
members: [user._id]
members: [user._id],
visibility: 'public'
},
`${user._id}_calendar` as Ref<Calendar>,
undefined,
@ -108,11 +109,10 @@ async function onEventCreate (tx: Tx, control: TriggerControl): Promise<Tx[]> {
const ev = TxProcessor.createDoc2Doc(ctx)
const res: Tx[] = []
const accounts = await control.modelDb.findAll(contact.class.PersonAccount, {})
const participants = accounts.filter(
(p) => (p._id !== ev.createdBy ?? ev.modifiedBy) && ev.participants.includes(p.person)
)
for (const acc of participants) {
for (const participant of ev.participants) {
const acc = (await control.modelDb.findAll(contact.class.PersonAccount, { person: participant }))[0]
if (acc === undefined) continue
if (acc._id === ev.createdBy ?? ev.modifiedBy) continue
const { _id, _class, space, modifiedBy, modifiedOn, ...data } = ev
const innerTx = control.txFactory.createTxCreateDoc(_class, `${acc._id}_calendar` as Ref<Calendar>, {
...data,