Signed-off-by: Denis Bykhov <bykhov.denis@gmail.com>
This commit is contained in:
Denis Bykhov 2023-07-10 21:50:29 +06:00 committed by GitHub
parent f1d0aa54f9
commit 5e3bfacf8a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
71 changed files with 1924 additions and 362 deletions

View File

@ -47,7 +47,7 @@
"@hcengineering/client": "^0.6.13",
"@hcengineering/platform": "^0.6.9",
"@hcengineering/model": "^0.6.6",
"@hcengineering/recruit": "^0.6.15",
"@hcengineering/recruit": "^0.6.16",
"faker": "~5.5.3",
"@hcengineering/model-recruit": "^0.6.0",
"@hcengineering/chunter": "^0.6.9",

View File

@ -66,8 +66,8 @@
"@hcengineering/chunter": "^0.6.9",
"@hcengineering/chunter-assets": "^0.6.7",
"@hcengineering/chunter-resources": "^0.6.0",
"@hcengineering/recruit": "^0.6.15",
"@hcengineering/recruit-assets": "^0.6.9",
"@hcengineering/recruit": "^0.6.16",
"@hcengineering/recruit-assets": "^0.6.10",
"@hcengineering/recruit-resources": "^0.6.0",
"@hcengineering/setting": "^0.6.9",
"@hcengineering/setting-assets": "^0.6.7",
@ -133,8 +133,8 @@
"@hcengineering/server-tags-resources": "^0.6.0",
"@hcengineering/server-task": "^0.6.0",
"@hcengineering/server-task-resources": "^0.6.0",
"@hcengineering/calendar": "^0.6.11",
"@hcengineering/calendar-assets": "^0.6.9",
"@hcengineering/calendar": "^0.6.12",
"@hcengineering/calendar-assets": "^0.6.10",
"@hcengineering/calendar-resources": "^0.6.0",
"@hcengineering/server-calendar": "^0.6.0",
"@hcengineering/server-calendar-resources": "^0.6.0",
@ -160,7 +160,7 @@
"@hcengineering/document": "^0.6.0",
"@hcengineering/document-assets": "^0.6.0",
"@hcengineering/document-resources": "^0.6.0",
"@hcengineering/bitrix": "^0.6.38",
"@hcengineering/bitrix": "^0.6.39",
"@hcengineering/bitrix-assets": "^0.6.0",
"@hcengineering/bitrix-resources": "^0.6.0",
"@hcengineering/request": "^0.6.3",

View File

@ -19,7 +19,7 @@ import { activityId } from '@hcengineering/activity'
import { attachmentId } from '@hcengineering/attachment'
import { automationId } from '@hcengineering/automation'
import { boardId } from '@hcengineering/board'
import { calendarId } from '@hcengineering/calendar'
import calendar, { calendarId } from '@hcengineering/calendar'
import { chunterId } from '@hcengineering/chunter'
import client, { clientId } from '@hcengineering/client'
import { contactId } from '@hcengineering/contact'
@ -88,6 +88,7 @@ interface Config {
REKONI_URL: string
TELEGRAM_URL: string
GMAIL_URL: string
CALENDAR_URL: string
TITLE?: string
DEFAULT_LANGUAGE?: string
}
@ -108,6 +109,7 @@ export async function configurePlatform() {
}
setMetadata(telegram.metadata.TelegramURL, config.TELEGRAM_URL ?? 'http://localhost:8086')
setMetadata(gmail.metadata.GmailURL, config.GMAIL_URL ?? 'http://localhost:8087')
setMetadata(calendar.metadata.CalendarServiceURL, config.CALENDAR_URL ?? 'http://localhost:8095')
setMetadata(login.metadata.OverrideEndpoint, process.env.LOGIN_ENDPOINT)

View File

@ -69,7 +69,7 @@
"@hcengineering/server-attachment-resources": "^0.6.0",
"xml2js": "~0.4.23",
"@hcengineering/model-recruit": "^0.6.0",
"@hcengineering/recruit": "^0.6.15",
"@hcengineering/recruit": "^0.6.16",
"@hcengineering/task": "^0.6.10",
"@hcengineering/chunter": "^0.6.9",
"mime-types": "~2.1.34",

View File

@ -35,7 +35,7 @@
"@hcengineering/view": "^0.6.8",
"@hcengineering/model-view": "^0.6.0",
"@hcengineering/contact": "^0.6.18",
"@hcengineering/bitrix": "^0.6.38",
"@hcengineering/bitrix": "^0.6.39",
"@hcengineering/bitrix-resources": "^0.6.0",
"@hcengineering/preference": "^0.6.8",
"@hcengineering/model-preference": "^0.6.0",

View File

@ -30,8 +30,9 @@
"@hcengineering/ui": "^0.6.10",
"@hcengineering/view": "^0.6.8",
"@hcengineering/model-attachment": "^0.6.0",
"@hcengineering/setting": "^0.6.9",
"@hcengineering/model-task": "^0.6.0",
"@hcengineering/calendar": "^0.6.11",
"@hcengineering/calendar": "^0.6.12",
"@hcengineering/calendar-resources": "^0.6.0",
"@hcengineering/platform": "^0.6.9",
"@hcengineering/notification": "^0.6.14",

View File

@ -14,32 +14,33 @@
//
import activity from '@hcengineering/activity'
import { Calendar, Event, Reminder, calendarId } from '@hcengineering/calendar'
import { Employee } from '@hcengineering/contact'
import { Calendar, Event, ReccuringEvent, ReccuringInstance, RecurringRule, calendarId } from '@hcengineering/calendar'
import { Contact } from '@hcengineering/contact'
import { DateRangeMode, Domain, IndexKind, Markup, Ref, Timestamp } from '@hcengineering/core'
import {
ArrOf,
Builder,
Collection,
Hidden,
Index,
Mixin,
Model,
Prop,
ReadOnly,
TypeBoolean,
TypeDate,
TypeMarkup,
TypeRef,
TypeString,
TypeTimestamp,
UX
} from '@hcengineering/model'
import attachment from '@hcengineering/model-attachment'
import chunter from '@hcengineering/model-chunter'
import contact from '@hcengineering/model-contact'
import core, { TAttachedDoc } from '@hcengineering/model-core'
import { TSpaceWithStates } from '@hcengineering/model-task'
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'
export * from '@hcengineering/calendar'
@ -50,11 +51,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 {}
export class TCalendar extends TSpaceWithStates implements Calendar {
@Prop(TypeString(), calendar.string.HideDetails)
hideDetails?: boolean
}
@Model(calendar.class.Event, core.class.AttachedDoc, DOMAIN_CALENDAR)
@UX(calendar.string.Event, calendar.icon.Calendar)
export class TEvent extends TAttachedDoc implements Event {
eventId!: string
@Prop(TypeString(), calendar.string.Title)
@Index(IndexKind.FullText)
title!: string
@ -67,6 +73,10 @@ export class TEvent extends TAttachedDoc implements Event {
@Index(IndexKind.FullText)
location?: string
@Prop(TypeBoolean(), calendar.string.AllDay)
@ReadOnly()
allDay!: boolean
@Prop(TypeDate(DateRangeMode.DATETIME), calendar.string.Date)
date!: Timestamp
@ -76,28 +86,37 @@ export class TEvent extends TAttachedDoc implements Event {
@Prop(Collection(attachment.class.Attachment), attachment.string.Attachments, { shortLabel: attachment.string.Files })
attachments?: number
@Prop(Collection(chunter.class.Comment), chunter.string.Comments)
comments?: number
@Prop(ArrOf(TypeRef(contact.class.Contact)), calendar.string.Participants)
participants!: Ref<Contact>[]
@Prop(ArrOf(TypeRef(contact.class.Employee)), calendar.string.Participants)
participants!: Ref<Employee>[]
@Prop(ArrOf(TypeTimestamp()), calendar.string.Reminders)
reminders?: number[]
@Prop(ArrOf(TypeString()), calendar.string.ExternalParticipants)
externalParticipants?: string[]
access!: 'freeBusyReader' | 'reader' | 'writer' | 'owner'
}
@Mixin(calendar.mixin.Reminder, calendar.class.Event)
@UX(calendar.string.Reminder, calendar.icon.Calendar)
export class TReminder extends TEvent implements Reminder {
@Prop(TypeDate(DateRangeMode.DATETIME), calendar.string.Shift)
@Hidden()
shift!: Timestamp
@Model(calendar.class.ReccuringEvent, calendar.class.Event)
@UX(calendar.string.ReccuringEvent, calendar.icon.Calendar)
export class TReccuringEvent extends TEvent implements ReccuringEvent {
rules!: RecurringRule[]
exdate!: Timestamp[]
rdate!: Timestamp[]
}
@Prop(TypeString(), calendar.string.State)
@Index(IndexKind.Indexed)
@Hidden()
state!: 'active' | 'done'
@Model(calendar.class.ReccuringInstance, calendar.class.Event)
@UX(calendar.string.Event, calendar.icon.Calendar)
export class TReccuringInstance extends TEvent implements ReccuringInstance {
recurringEventId!: Ref<ReccuringEvent>
originalStartTime!: number
isCancelled?: boolean
virtual?: boolean
}
export function createModel (builder: Builder): void {
builder.createModel(TCalendar, TEvent, TReminder)
builder.createModel(TCalendar, TReccuringEvent, TReccuringInstance, TEvent)
builder.createDoc(
workbench.class.Application,
@ -127,6 +146,20 @@ export function createModel (builder: Builder): void {
calendar.viewlet.CalendarEvent
)
builder.createDoc(
setting.class.IntegrationType,
core.space.Model,
{
label: calendar.string.Calendar,
description: calendar.string.IntegrationDescr,
icon: calendar.component.CalendarIntegrationIcon,
createComponent: calendar.component.IntegrationConnect,
onDisconnect: calendar.handler.DisconnectHandler,
reconnectComponent: calendar.component.IntegrationConnect
},
calendar.integrationType.Calendar
)
builder.createDoc(
notification.class.NotificationGroup,
core.space.Model,
@ -149,10 +182,8 @@ export function createModel (builder: Builder): void {
generated: false,
label: calendar.string.Reminder,
group: calendar.ids.CalendarNotificationGroup,
txClasses: [core.class.TxMixin],
field: 'state',
txMatch: { 'attributes.state': 'done' },
objectClass: calendar.mixin.Reminder,
txClasses: [],
objectClass: calendar.class.Event,
allowedForAuthor: true,
templates: {
textTemplate: 'Reminder: {doc}',
@ -171,9 +202,9 @@ export function createModel (builder: Builder): void {
activity.class.TxViewlet,
core.space.Model,
{
objectClass: calendar.mixin.Reminder,
objectClass: calendar.class.Event,
icon: calendar.icon.Reminder,
txClass: core.class.TxMixin,
txClass: core.class.TxUpdateDoc,
label: calendar.string.Reminder,
component: calendar.activity.ReminderViewlet,
display: 'emphasized',
@ -218,14 +249,30 @@ export function createModel (builder: Builder): void {
calendar.action.SaveEventReminder
)
builder.mixin(calendar.mixin.Reminder, core.class.Class, view.mixin.ObjectPresenter, {
presenter: calendar.component.ReminderPresenter
})
createAction(
builder,
{
action: calendar.actionImpl.DeleteRecEvent,
override: [view.action.Delete],
label: view.string.Delete,
icon: view.icon.Delete,
keyBinding: ['Meta + Backspace'],
category: view.category.General,
input: 'any',
target: calendar.class.ReccuringInstance,
context: { mode: ['context', 'browser'], group: 'tools' }
},
calendar.action.DeleteRecEvent
)
builder.mixin(calendar.class.Event, core.class.Class, view.mixin.ObjectEditor, {
editor: calendar.component.EditEvent
})
builder.mixin(calendar.class.ReccuringInstance, core.class.Class, view.mixin.ObjectEditor, {
editor: calendar.component.EditRecEvent
})
builder.mixin(calendar.class.Event, core.class.Class, view.mixin.ObjectPresenter, {
presenter: calendar.component.EventPresenter
})

View File

@ -13,34 +13,70 @@
// limitations under the License.
//
import core, { TxOperations } from '@hcengineering/core'
import { Calendar, Event } from '@hcengineering/calendar'
import core, { Ref, TxOperations } from '@hcengineering/core'
import { MigrateOperation, MigrationClient, MigrationUpgradeClient } from '@hcengineering/model'
import calendar from './plugin'
import { DOMAIN_CALENDAR } from '.'
import contact from '@hcengineering/contact'
async function createSpace (tx: TxOperations): Promise<void> {
const current = await tx.findOne(core.class.Space, {
_id: calendar.space.PersonalEvents
async function migrateCalendars (tx: TxOperations): Promise<void> {
const existCalendars = new Set((await tx.findAll(calendar.class.Calendar, {})).map((p) => p._id))
const users = await tx.findAll(contact.class.EmployeeAccount, {})
for (const user of users) {
if (!existCalendars.has(`${user._id}_calendar` as Ref<Calendar>)) {
await tx.createDoc(
calendar.class.Calendar,
core.space.Space,
{
name: user.email,
description: '',
archived: false,
private: false,
members: [user._id]
},
`${user._id}_calendar` as Ref<Calendar>,
undefined,
user._id
)
}
}
const events = await tx.findAll(calendar.class.Event, { space: calendar.space.PersonalEvents })
for (const event of events) {
await tx.update(event, { space: (event.createdBy ?? event.modifiedBy) as string as Ref<Calendar> })
}
const space = await tx.findOne(calendar.class.Calendar, { _id: calendar.space.PersonalEvents })
if (space !== undefined) {
await tx.remove(space)
}
}
async function fixEventDueDate (client: MigrationClient): Promise<void> {
const events = await client.find<Event>(DOMAIN_CALENDAR, {
_class: calendar.class.Event,
dueDate: { $exists: false }
})
if (current === undefined) {
await tx.createDoc(
core.class.Space,
core.space.Space,
{
name: 'Personal Events',
description: 'Personal Events',
private: false,
archived: false,
members: []
},
calendar.space.PersonalEvents
)
for (const event of events) {
await client.update(DOMAIN_CALENDAR, { _id: event._id }, { dueDate: event.date })
}
}
async function migrateReminders (client: MigrationClient): Promise<void> {
const events = await client.find(DOMAIN_CALENDAR, { 'calendar:mixin:Reminder': { $exists: true } })
for (const event of events) {
const shift = (event as any)['calendar:mixin:Reminder'].shift
await client.update(DOMAIN_CALENDAR, { _id: event._id }, { reminders: [shift] })
await client.update(DOMAIN_CALENDAR, { _id: event._id }, { $unset: { 'calendar:mixin:Reminder': true } })
}
}
export const calendarOperation: MigrateOperation = {
async migrate (client: MigrationClient): Promise<void> {},
async migrate (client: MigrationClient): Promise<void> {
await fixEventDueDate(client)
await migrateReminders(client)
},
async upgrade (client: MigrationUpgradeClient): Promise<void> {
const tx = new TxOperations(client, core.account.System)
await createSpace(tx)
await migrateCalendars(tx)
}
}

View File

@ -25,20 +25,22 @@ import { Action, ActionCategory, ViewAction, Viewlet, ViewletDescriptor } from '
export default mergeIds(calendarId, calendar, {
component: {
IntegrationConnect: '' as AnyComponent,
CreateCalendar: '' as AnyComponent,
CalendarView: '' as AnyComponent,
EditEvent: '' as AnyComponent,
ReminderPresenter: '' as AnyComponent,
EventPresenter: '' as AnyComponent
EventPresenter: '' as AnyComponent,
CalendarIntegrationIcon: '' as AnyComponent
},
action: {
SaveEventReminder: '' as Ref<Action>
SaveEventReminder: '' as Ref<Action>,
DeleteRecEvent: '' as Ref<Action>
},
category: {
Calendar: '' as Ref<ActionCategory>
},
actionImpl: {
SaveEventReminder: '' as ViewAction
SaveEventReminder: '' as ViewAction,
DeleteRecEvent: '' as ViewAction
},
string: {
ApplicationLabelCalendar: '' as IntlString,
@ -48,7 +50,8 @@ export default mergeIds(calendarId, calendar, {
State: '' as IntlString,
CreatedReminder: '' as IntlString,
ConfigLabel: '' as IntlString,
ConfigDescription: '' as IntlString
ConfigDescription: '' as IntlString,
IntegrationDescr: '' as IntlString
},
viewlet: {
Calendar: '' as Ref<ViewletDescriptor>,

View File

@ -34,7 +34,7 @@
"@hcengineering/model-view": "^0.6.0",
"@hcengineering/model-workbench": "^0.6.1",
"@hcengineering/model-contact": "^0.6.1",
"@hcengineering/recruit": "^0.6.15",
"@hcengineering/recruit": "^0.6.16",
"@hcengineering/recruit-resources": "^0.6.0",
"@hcengineering/chunter": "^0.6.9",
"@hcengineering/notification": "^0.6.14",

View File

@ -28,9 +28,10 @@
"@hcengineering/core": "^0.6.27",
"@hcengineering/model": "^0.6.6",
"@hcengineering/platform": "^0.6.9",
"@hcengineering/calendar": "^0.6.12",
"@hcengineering/server-notification": "^0.6.1",
"@hcengineering/calendar": "^0.6.11",
"@hcengineering/server-calendar": "^0.6.0",
"@hcengineering/contact": "^0.6.18",
"@hcengineering/server-core": "^0.6.1"
}
}

View File

@ -20,16 +20,33 @@ import core, { Class, Doc } from '@hcengineering/core'
import serverNotification from '@hcengineering/server-notification'
import serverCalendar from '@hcengineering/server-calendar'
import serverCore, { ObjectDDParticipant } from '@hcengineering/server-core'
import contact from '@hcengineering/contact'
export { serverCalendarId } from '@hcengineering/server-calendar'
export function createModel (builder: Builder): void {
builder.mixin(calendar.class.Event, core.class.Class, serverNotification.mixin.HTMLPresenter, {
presenter: serverCalendar.function.EventHTMLPresenter
presenter: serverCalendar.function.ReminderHTMLPresenter
})
builder.mixin(calendar.class.Event, core.class.Class, serverNotification.mixin.TextPresenter, {
presenter: serverCalendar.function.EventTextPresenter
presenter: serverCalendar.function.ReminderTextPresenter
})
builder.createDoc(serverCore.class.Trigger, core.space.Model, {
trigger: serverCalendar.trigger.OnEmployeeAccountCreate,
txMatch: {
_class: core.class.TxCreateDoc,
objectClass: contact.class.EmployeeAccount
}
})
builder.createDoc(serverCore.class.Trigger, core.space.Model, {
trigger: serverCalendar.trigger.OnEvent,
txMatch: {
_class: core.class.TxCollectionCUD,
objectClass: calendar.class.Event
}
})
builder.mixin<Class<Doc>, ObjectDDParticipant>(

View File

@ -1,6 +1,6 @@
{
"name": "@hcengineering/panel",
"version": "0.6.9",
"version": "0.6.10",
"main": "src/index.ts",
"author": "Anticrm Platform Contributors",
"license": "EPL-2.0",
@ -40,7 +40,7 @@
"@hcengineering/chunter": "^0.6.9",
"@hcengineering/presentation": "^0.6.2",
"@hcengineering/activity": "^0.6.0",
"@hcengineering/calendar": "^0.6.11"
"@hcengineering/calendar": "^0.6.12"
},
"repository": "https://github.com/hcenginneing/anticrm",
"publishConfig": {

View File

@ -36,7 +36,7 @@
: new Date(new Date(currentDate).setHours(0, 0, 0, 0))
</script>
<table class="antiTable">
<table>
<thead class="scroller-thead">
<tr class="scroller-thead__tr">
<th><Label label={ui.string.HoursLabel} /></th>
@ -45,7 +45,7 @@
<th>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div
class="antiTable-cells cursor-pointer uppercase flex-col-center"
class="cursor-pointer uppercase flex-col-center"
class:today={areDatesEqual(todayDate, day)}
on:click={() => {
dispatch('select', day)
@ -59,18 +59,15 @@
</tr>
</thead>
<tbody>
{#if $$slots.cell}
<slot name="header" date={getDay(weekMonday, 0)} days={displayedDaysCount} />
{/if}
{#each [...Array(displayedHours).keys()] as hourOfDay}
<tr class="antiTable-body__row">
<td style="width: 50px;" class="calendar-td">
<div class="antiTable-cells__firstCell">
<tr>
<td style="width: 50px;" class="calendar-td first">
{#if hourOfDay !== 0}
{addZero(hourOfDay)}:00
</div>
{/if}
</td>
{#each [...Array(displayedDaysCount).keys()] as dayIndex}
<td class="antiTable-body__border calendar-td cell" style={`height: ${cellHeight};`}>
<td class="calendar-td cell" style={`height: ${cellHeight};`}>
{#if $$slots.cell}
<slot name="cell" date={getDay(weekMonday, dayIndex, hourOfDay * 60)} />
{/if}
@ -92,14 +89,21 @@
color: var(--theme-caption-color);
}
.calendar-td {
&:not(.first) {
border: 1px solid var(--theme-table-border-color);
}
padding: 0;
margin: 0;
&.first {
display: flex;
margin-top: -0.5rem;
}
}
.cell {
overflow: hidden;
padding: 2px;
width: calc(calc(100% - 50px) / 7);
}
.cell:hover:not(.wrongMonth) {
.cell:hover {
color: var(--accented-button-color);
background-color: var(--highlight-hover);
}

View File

@ -41,7 +41,7 @@
"@hcengineering/core": "^0.6.27",
"@hcengineering/view": "^0.6.8",
"@hcengineering/view-resources": "^0.6.0",
"@hcengineering/panel": "^0.6.9",
"@hcengineering/panel": "^0.6.10",
"@hcengineering/text-editor": "^0.6.0",
"@hcengineering/login": "^0.6.7",
"filesize": "^8.0.3",

View File

@ -30,6 +30,6 @@
},
"dependencies": {
"@hcengineering/platform": "^0.6.9",
"@hcengineering/bitrix": "^0.6.38"
"@hcengineering/bitrix": "^0.6.39"
}
}

View File

@ -34,7 +34,7 @@
"dependencies": {
"@hcengineering/platform": "^0.6.9",
"svelte": "3.55.1",
"@hcengineering/bitrix": "^0.6.38",
"@hcengineering/bitrix": "^0.6.39",
"@hcengineering/ui": "^0.6.10",
"@hcengineering/presentation": "^0.6.2",
"@hcengineering/text-editor": "^0.6.0",
@ -54,7 +54,7 @@
"@hcengineering/tags": "^0.6.11",
"@hcengineering/tags-resources": "^0.6.0",
"fast-equals": "^2.0.3",
"@hcengineering/recruit": "^0.6.15",
"@hcengineering/recruit": "^0.6.16",
"@hcengineering/task": "^0.6.10"
},
"repository": "https://github.com/hcengineering/anticrm",

View File

@ -1,6 +1,6 @@
{
"name": "@hcengineering/bitrix",
"version": "0.6.38",
"version": "0.6.39",
"main": "lib/index.js",
"author": "Anticrm Platform Contributors",
"license": "EPL-2.0",
@ -37,7 +37,7 @@
"fast-equals": "^2.0.3",
"qs": "~6.11.0",
"@hcengineering/gmail": "^0.6.12",
"@hcengineering/recruit": "^0.6.15",
"@hcengineering/recruit": "^0.6.16",
"@hcengineering/task": "^0.6.10"
},
"repository": "https://github.com/hcengineering/anticrm",

View File

@ -35,7 +35,7 @@
"@hcengineering/attachment": "^0.6.8",
"@hcengineering/attachment-resources": "^0.6.0",
"@hcengineering/board": "^0.6.9",
"@hcengineering/calendar": "^0.6.11",
"@hcengineering/calendar": "^0.6.12",
"@hcengineering/chunter": "^0.6.9",
"@hcengineering/chunter-resources": "^0.6.0",
"@hcengineering/contact": "^0.6.18",
@ -44,7 +44,7 @@
"@hcengineering/core": "^0.6.27",
"@hcengineering/notification": "^0.6.14",
"@hcengineering/notification-resources": "^0.6.0",
"@hcengineering/panel": "^0.6.9",
"@hcengineering/panel": "^0.6.10",
"@hcengineering/platform": "^0.6.9",
"@hcengineering/presentation": "^0.6.2",
"@hcengineering/task": "^0.6.10",

View File

@ -39,6 +39,18 @@
"CreateEvent": "Create event",
"EventFor": "Event for: ",
"ConfigLabel": "Calendar",
"ConfigDescription": "Extension to see calendar with Events"
"ConfigDescription": "Extension to see calendar with Events",
"HideDetails": "Hide details",
"ReccuringEvent": "Reccuring event",
"ExternalParticipants": "External participants",
"IntegrationDescr": "Use Google Calendar integration",
"Connect": "Connect",
"RedirectGoogle": "You will be redirect to google auth page",
"ConnectCalendar": "Connect Google Calendar account",
"EditRecEvent": "Edit recurring event",
"RemoveRecEvent": "Remove recurring event",
"ThisEvent": "This event",
"ThisAndNext": "This and following events",
"AllEvents": "All events"
}
}

View File

@ -39,6 +39,18 @@
"CreateEvent": "Создать событие",
"EventFor": "Событие для: ",
"ConfigLabel": "Календарь",
"ConfigDescription": "Расширение для календаря с событиями"
"ConfigDescription": "Расширение для календаря с событиями",
"ReccuringEvent": "Повторяющиеся событие",
"HideDetails": "Скрыть детали",
"ExternalParticipants": "Внешние участники",
"IntegrationDescr": "Подключить Google Каледнарь",
"Connect": "Подключить",
"RedirectGoogle": "Вы будете перенаправлены на страницу авторизации Google",
"ConnectCalendar": "Подключить Google Каледнарь",
"EditRecEvent": "Изменение повторяющегося события",
"RemoveRecEvent": "Удаление повторяющегося события",
"ThisEvent": "Только это событие",
"ThisAndNext": "Это и последующие события",
"AllEvents": "Все события"
}
}

View File

@ -1,6 +1,6 @@
{
"name": "@hcengineering/calendar-assets",
"version": "0.6.9",
"version": "0.6.10",
"main": "lib/index.js",
"author": "Anticrm Platform Contributors",
"template": "@hcengineering/assets-package",
@ -30,7 +30,7 @@
},
"dependencies": {
"@hcengineering/platform": "^0.6.9",
"@hcengineering/calendar": "^0.6.11"
"@hcengineering/calendar": "^0.6.12"
},
"repository": "https://github.com/hcenginneing/anticrm",
"publishConfig": {

View File

@ -36,10 +36,11 @@
"@hcengineering/platform": "^0.6.9",
"@hcengineering/ui": "^0.6.10",
"@hcengineering/presentation": "^0.6.2",
"@hcengineering/calendar": "^0.6.11",
"@hcengineering/calendar": "^0.6.12",
"svelte": "3.55.1",
"@hcengineering/text-editor": "^0.6.0",
"@hcengineering/contact": "^0.6.18",
"@hcengineering/panel": "^0.6.10",
"@hcengineering/contact-resources": "^0.6.0",
"@hcengineering/view-resources": "^0.6.0",
"@hcengineering/view": "^0.6.8",

View File

@ -13,21 +13,32 @@
// limitations under the License.
-->
<script lang="ts">
import { Event } from '@hcengineering/calendar'
import { Class, Doc, DocumentQuery, FindOptions, Ref, SortingOrder, Space } from '@hcengineering/core'
import { Calendar, Event, getAllEvents } from '@hcengineering/calendar'
import {
Class,
Doc,
DocumentQuery,
FindOptions,
Ref,
SortingOrder,
Space,
Timestamp,
getCurrentAccount
} from '@hcengineering/core'
import { createQuery } from '@hcengineering/presentation'
import {
AnyComponent,
areDatesEqual,
Button,
IconBack,
IconForward,
MonthCalendar,
Scroller,
showPopup,
WeekCalendar,
YearCalendar,
defaultSP
areDatesEqual,
defaultSP,
getMonday,
showPopup
} from '@hcengineering/ui'
import { BuildModelKey } from '@hcengineering/view'
import { CalendarMode } from '../index'
@ -51,53 +62,98 @@
let objects: Event[] = []
function getFrom (date: Date, mode: CalendarMode): Timestamp {
switch (mode) {
case CalendarMode.Day: {
return new Date(date).setHours(0, 0, 0, 0)
}
case CalendarMode.Week: {
return getMonday(date, mondayStart).setHours(0, 0, 0, 0)
}
case CalendarMode.Month: {
return new Date(new Date(date).setDate(1)).setHours(0, 0, 0, 0)
}
case CalendarMode.Year: {
return new Date(new Date(date).setMonth(0, 1)).setHours(0, 0, 0, 0)
}
}
}
function getTo (date: Date, mode: CalendarMode): Timestamp {
switch (mode) {
case CalendarMode.Day: {
return new Date(date).setDate(date.getDate() + 1)
}
case CalendarMode.Week: {
const monday = getMonday(date, mondayStart)
return new Date(monday.setDate(monday.getDate() + 7)).setHours(0, 0, 0, 0)
}
case CalendarMode.Month: {
return new Date(new Date(date).setMonth(date.getMonth() + 1, 1)).setHours(0, 0, 0, 0)
}
case CalendarMode.Year: {
return new Date(new Date(date).setMonth(12, 1)).setHours(0, 0, 0, 0)
}
}
}
$: from = getFrom(currentDate, mode)
$: to = getTo(currentDate, mode)
const calendarsQuery = createQuery()
let calendars: Calendar[] = []
calendarsQuery.query(calendar.class.Calendar, { createdBy: getCurrentAccount()._id }, (res) => {
calendars = res
})
const q = createQuery()
async function update (_class: Ref<Class<Event>>, query: DocumentQuery<Event>, options?: FindOptions<Event>) {
async function update (
_class: Ref<Class<Event>>,
query: DocumentQuery<Event>,
from: Timestamp,
to: Timestamp,
calendars: Calendar[],
options?: FindOptions<Event>
) {
q.query<Event>(
_class,
query,
{
space: { $in: calendars.map((p) => p._id) },
...query
},
(result) => {
objects = result
objects = getAllEvents(result, from, to)
},
{ sort: { date: SortingOrder.Ascending }, ...options }
)
}
$: update(_class, query, options)
$: update(_class, query, from, to, calendars, options)
function areDatesLess (firstDate: Date, secondDate: Date): boolean {
return (
firstDate.getFullYear() <= secondDate.getFullYear() &&
firstDate.getMonth() <= secondDate.getMonth() &&
firstDate.getDate() <= secondDate.getDate()
)
function inRange (start: Date, end: Date, startPeriod: Date, period: 'day' | 'hour'): boolean {
const endPeriod =
period === 'day'
? new Date(startPeriod).setDate(startPeriod.getDate() + 1)
: new Date(startPeriod).setHours(startPeriod.getHours() + 1)
if (end.getTime() - 1 <= startPeriod.getTime()) return false
if (start.getTime() >= endPeriod) return false
return true
}
function findEvents (events: Event[], date: Date, minutes = false): Event[] {
return events.filter((it) => {
const d1 = new Date(it.date)
const d2 = new Date(it.dueDate ?? it.date)
const inDays = areDatesLess(d1, date) && areDatesLess(date, d2)
if (minutes) {
if (areDatesEqual(d1, date)) {
return (
(date.getTime() <= d1.getTime() && d1.getTime() < date.getTime() + 60 * 60 * 1000) ||
(d1.getTime() <= date.getTime() && date.getTime() < d2.getTime())
)
}
if (areDatesEqual(d2, date)) {
return (
(date.getTime() < d2.getTime() && d2.getTime() < date.getTime() + 60 * 60 * 1000) ||
(d1.getTime() <= date.getTime() && date.getTime() < d2.getTime())
)
}
// Somethere in middle
return inDays
let d1 = new Date(it.date)
let d2 = new Date(it.dueDate ?? it.date)
if (it.allDay) {
if (minutes) return false
d1 = new Date(d1.getTime() + new Date().getTimezoneOffset() * 60 * 1000)
d2 = new Date(d2.getTime() + new Date().getTimezoneOffset() * 60 * 1000)
return inRange(d1, d2, date, minutes ? 'hour' : 'day')
}
return inDays
return inRange(d1, d2, date, minutes ? 'hour' : 'day')
})
}

View File

@ -13,13 +13,13 @@
// limitations under the License.
-->
<script lang="ts">
import { Calendar, generateEventId } from '@hcengineering/calendar'
import { Employee, EmployeeAccount } from '@hcengineering/contact'
import { Class, DateRangeMode, Doc, getCurrentAccount, Ref } from '@hcengineering/core'
import { Card, getClient } from '@hcengineering/presentation'
import { UserBoxList } from '@hcengineering/contact-resources'
import ui, { EditBox, DateRangePresenter } from '@hcengineering/ui'
import { tick } from 'svelte'
import { createEventDispatcher } from 'svelte'
import { Class, DateRangeMode, Doc, Ref, getCurrentAccount } from '@hcengineering/core'
import { Card, getClient } from '@hcengineering/presentation'
import ui, { DateRangePresenter, EditBox, ToggleWithLabel } from '@hcengineering/ui'
import { createEventDispatcher, tick } from 'svelte'
import calendar from '../plugin'
export let attachedTo: Ref<Doc> = calendar.ids.NoAttached
@ -30,16 +30,17 @@
const now = new Date()
const defaultDuration = 30 * 60 * 1000
const allDayDuration = 24 * 60 * 60 * 1000
let startDate =
date === undefined ? now.getTime() : withTime ? date.getTime() : date.setHours(now.getHours(), now.getMinutes())
let duration = defaultDuration
let dueDate = startDate + duration
let dueDateRef: DateRangePresenter
let allDay = false
const currentUser = getCurrentAccount() as EmployeeAccount
let participants: Ref<Employee>[] = [currentUser.employee]
const space = calendar.space.PersonalEvents
const dispatch = createEventDispatcher()
const client = getClient()
@ -52,22 +53,23 @@
let date: number | undefined
if (startDate != null) date = startDate
if (date === undefined) return
await client.createDoc(calendar.class.Event, space, {
attachedTo,
attachedToClass,
collection: 'events',
date,
dueDate,
const space = `${getCurrentAccount()._id}_calendar` as Ref<Calendar>
await client.addCollection(calendar.class.Event, space, attachedTo, attachedToClass, 'events', {
eventId: generateEventId(),
date: new Date(date).setUTCHours(0, 0, 0, 0),
dueDate: new Date(dueDate).setUTCHours(0, 0, 0, 0),
description: '',
participants,
title
title,
allDay,
access: 'owner'
})
}
const handleNewStartDate = async (newStartDate: number | null) => {
if (newStartDate !== null) {
startDate = newStartDate
dueDate = startDate + duration
dueDate = startDate + (allDay ? allDayDuration : duration)
await tick()
dueDateRef.adaptValue()
}
@ -80,12 +82,14 @@
dueDate = newDueDate
duration = diff
} else {
dueDate = startDate + duration
dueDate = startDate + (allDay ? allDayDuration : duration)
}
await tick()
dueDateRef.adaptValue()
}
}
$: mode = allDay ? DateRangeMode.DATE : DateRangeMode.DATETIME
</script>
<Card
@ -99,12 +103,13 @@
>
<EditBox bind:value={title} placeholder={calendar.string.Title} kind={'large-style'} autoFocus />
<svelte:fragment slot="pool">
<ToggleWithLabel bind:on={allDay} label={calendar.string.AllDay} />
<DateRangePresenter
value={startDate}
labelNull={ui.string.SelectDate}
on:change={async (event) => await handleNewStartDate(event.detail)}
mode={DateRangeMode.DATETIME}
kind={'regular'}
{mode}
size={'large'}
editable
/>
@ -113,8 +118,8 @@
value={dueDate}
labelNull={calendar.string.DueTo}
on:change={async (event) => await handleNewDueDate(event.detail)}
mode={DateRangeMode.DATETIME}
kind={'regular'}
{mode}
size={'large'}
editable
/>

View File

@ -13,23 +13,25 @@
// limitations under the License.
-->
<script lang="ts">
import { Calendar, Event, generateEventId } from '@hcengineering/calendar'
import { Employee, EmployeeAccount } from '@hcengineering/contact'
import { Class, DateRangeMode, Doc, getCurrentAccount, Ref } from '@hcengineering/core'
import { Card, getClient } from '@hcengineering/presentation'
import { UserBoxList } from '@hcengineering/contact-resources'
import ui, { EditBox, DateRangePresenter } from '@hcengineering/ui'
import { Class, DateRangeMode, Doc, Ref, getCurrentAccount } from '@hcengineering/core'
import { Card, getClient } from '@hcengineering/presentation'
import ui, { DateRangePresenter, EditBox } from '@hcengineering/ui'
import { createEventDispatcher } from 'svelte'
import calendar from '../plugin'
export let attachedTo: Ref<Doc>
export let attachedToClass: Ref<Class<Doc>>
export let event: Event | undefined = undefined
export let title: string = ''
let _title = title
let value: number | null | undefined = null
const currentUser = getCurrentAccount() as EmployeeAccount
let participants: Ref<Employee>[] = [currentUser.employee]
const space = calendar.space.PersonalEvents
const defaultDuration = 30 * 60 * 1000
const dispatch = createEventDispatcher()
const client = getClient()
@ -41,22 +43,18 @@
async function saveReminder () {
let date: number | undefined
if (value != null) date = value
// if (value.date !== undefined) {
// date = new Date(value.date).getTime()
// } else if (value.shift !== undefined) {
// date = new Date().getTime() + value.shift
// }
if (date === undefined) return
const _id = await client.addCollection(calendar.class.Event, space, attachedTo, attachedToClass, 'reminders', {
const space = `${getCurrentAccount()._id}_calendar` as Ref<Calendar>
await client.addCollection(calendar.class.Event, space, attachedTo, attachedToClass, 'events', {
eventId: generateEventId(),
date,
dueDate: date + defaultDuration,
description: '',
participants,
title: _title
})
await client.createMixin(_id, calendar.class.Event, space, calendar.mixin.Reminder, {
shift: 0,
state: 'active'
title: _title,
allDay: false,
reminders: [0],
access: 'owner'
})
}
</script>
@ -72,7 +70,6 @@
>
<EditBox bind:value={_title} placeholder={calendar.string.Title} kind={'large-style'} autoFocus />
<svelte:fragment slot="pool">
<!-- <TimeShiftPicker title={calendar.string.Date} bind:value direction="after" /> -->
<DateRangePresenter
bind:value
mode={DateRangeMode.DATETIME}

View File

@ -15,7 +15,7 @@
<script lang="ts">
import { Event } from '@hcengineering/calendar'
import { Class, Doc, DocumentQuery, FindOptions, Ref } from '@hcengineering/core'
import { Label, addZero, getPlatformColorForTextDef, themeStore, tooltip } from '@hcengineering/ui'
import { Label, addZero, getPlatformColorForTextDef, showPopup, themeStore, tooltip } from '@hcengineering/ui'
import { BuildModelKey } from '@hcengineering/view'
import { createEventDispatcher } from 'svelte'
import calendar from '../plugin'
@ -38,14 +38,11 @@
const dispatch = createEventDispatcher()
const eventCount = 3
$: tip =
events.length > 0
? {
label: calendar.string.Events,
component: EventsPopup,
props: { value: events, _class, query, options, baseMenuClass, config }
}
: undefined
$: tip = {
label: calendar.string.Events,
component: EventsPopup,
props: { value: events }
}
</script>
{#if size === 'huge'}
@ -73,13 +70,24 @@
>
{#each events.slice(0, eventCount) as e}
<div
class="overflow-label mt-1 py-1 flex flex-between event"
style="background-color: {getPlatformColorForTextDef(e._class, $themeStore.dark).background};"
class="overflow-label mt-1 py-1 flex flex-between event cursor-pointer"
style="background-color: {getPlatformColorForTextDef(e.space, $themeStore.dark).color};"
on:click|stopPropagation|preventDefault={() => {
showPopup(
e._class === calendar.class.ReccuringInstance
? calendar.component.EditRecEvent
: calendar.component.EditEvent,
{ object: e },
'content'
)
}}
>
{e.title}
<div>
{addZero(new Date(e.date).getHours())}:{addZero(new Date(e.date).getMinutes())}
</div>
{#if !e.allDay}
<div>
{addZero(new Date(e.date).getHours())}:{addZero(new Date(e.date).getMinutes())}
</div>
{/if}
</div>
{/each}
{#if events.length > eventCount}
@ -92,15 +100,12 @@
{:else}
<div class="w-full h-full relative flex-center cell" class:today class:selected class:wrongMonth use:tooltip={tip}>
{date.getDate()}
{#if events.length > 0}
<div class="marker" />
{/if}
</div>
{/if}
<style lang="scss">
.event {
border-radius: 0.5rem;
border-radius: 0.25rem;
padding: 0rem 0.5rem;
color: var(--accent-color);
}
@ -111,7 +116,7 @@
padding: 0.25rem;
}
.cell {
border-radius: 0.5rem;
border-radius: 0.25rem;
border: 1px solid transparent;
}
.today {

View File

@ -35,9 +35,8 @@
showPopup(SaveEventReminder, { objectId: value._id, objectClass: value._class }, ev.target as HTMLElement)
} else {
const currentUser = getCurrentAccount() as EmployeeAccount
const current = await client.findOne(calendar.mixin.Reminder, {
const current = await client.findOne(calendar.class.Event, {
attachedTo: value._id,
state: 'active',
participants: currentUser.employee
})
if (current === undefined) {

View File

@ -13,22 +13,37 @@
// limitations under the License.
-->
<script lang="ts">
import { Event } from '@hcengineering/calendar'
import { EmployeeAccount } from '@hcengineering/contact'
import { Class, Doc, getCurrentAccount, Ref } from '@hcengineering/core'
import { Button, showPopup, Label, Scroller, IconAdd, deviceOptionsStore as deviceInfo } from '@hcengineering/ui'
import { Table } from '@hcengineering/view-resources'
import { createQuery } from '@hcengineering/presentation'
import { Button, deviceOptionsStore as deviceInfo, IconAdd, Label, Scroller, showPopup } from '@hcengineering/ui'
import calendar from '../plugin'
import CreateReminder from './CreateReminder.svelte'
import ReminderPresenter from './ReminderPresenter.svelte'
export let attachedTo: Ref<Doc>
export let attachedToClass: Ref<Class<Doc>>
export let title: string | undefined
function click (ev: Event): void {
const currentUser = getCurrentAccount() as EmployeeAccount
let events: Event[] = []
const query = createQuery()
$: query.query(
calendar.class.Event,
{
attachedTo,
participants: currentUser.employee
},
(res) => {
events = res.filter((p) => p.reminders !== undefined && p.reminders.length > 0)
}
)
function click (ev: MouseEvent): void {
showPopup(CreateReminder, { attachedTo, attachedToClass, title }, ev.target as HTMLElement)
}
const currentUser = getCurrentAccount() as EmployeeAccount
$: isMobile = $deviceInfo.isMobile
</script>
@ -39,13 +54,9 @@
</div>
<Scroller>
<div class="px-4 clear-mins">
<Table
_class={calendar.mixin.Reminder}
config={['']}
options={{}}
query={{ attachedTo, state: 'active', participants: currentUser.employee }}
hiddenHeader
/>
{#each events as event}
<ReminderPresenter value={event} />
{/each}
</div>
</Scroller>
</div>

View File

@ -14,32 +14,37 @@
-->
<script lang="ts">
import { Event } from '@hcengineering/calendar'
import { Doc, WithLookup } from '@hcengineering/core'
import { DateRangeMode, Doc } from '@hcengineering/core'
import { Panel } from '@hcengineering/panel'
import { createQuery, getClient } from '@hcengineering/presentation'
import { StyledTextBox } from '@hcengineering/text-editor'
import { AnyComponent, Component, Label, StylishEdit } from '@hcengineering/ui'
import { getObjectPreview, ObjectPresenter } from '@hcengineering/view-resources'
import { createEventDispatcher, onMount } from 'svelte'
import ui, {
AnyComponent,
Button,
Component,
DateRangePresenter,
EditBox,
IconMoreH,
Label,
ToggleWithLabel,
showPopup
} from '@hcengineering/ui'
import view from '@hcengineering/view'
import { ContextMenu, ObjectPresenter, getObjectPreview } from '@hcengineering/view-resources'
import { createEventDispatcher, tick } from 'svelte'
import calendar from '../plugin'
export let object: WithLookup<Event>
export let object: Event
const dispatch = createEventDispatcher()
const client = getClient()
onMount(() => {
dispatch('open', {
ignoreKeys: ['comments', 'title', 'description'],
ignoreMixins: [calendar.mixin.Reminder]
})
})
const query = createQuery()
let doc: Doc | undefined
$: if (object.attachedTo !== undefined && object.attachedToClass !== undefined) {
$: if (object.attachedTo !== calendar.ids.NoAttached) {
query.query(object.attachedToClass, { _id: object.attachedTo }, (res) => {
doc = res.shift()
doc = res[0]
})
}
@ -52,10 +57,95 @@
}
$: updatePreviewPresenter(doc)
$: mode = object.allDay ? DateRangeMode.DATE : DateRangeMode.DATETIME
const defaultDuration = 30 * 60 * 1000
const allDayDuration = 24 * 60 * 60 * 1000
let duration = object.dueDate - object.date
async function updateDate () {
await client.update(object, {
date: object.date,
dueDate: object.dueDate,
allDay: object.allDay
})
}
async function handleNewStartDate (newStartDate: number | null) {
if (newStartDate !== null) {
object.date = newStartDate
object.dueDate = object.date + (object.allDay ? allDayDuration : duration)
if (object.allDay) {
object.date = new Date(object.date).setUTCHours(0, 0, 0, 0)
object.dueDate = new Date(object.dueDate).setUTCHours(0, 0, 0, 0)
}
await tick()
dueDateRef.adaptValue()
await updateDate()
}
}
async function handleNewDueDate (newDueDate: number | null) {
if (newDueDate !== null) {
const diff = newDueDate - object.date
if (diff > 0) {
object.dueDate = newDueDate
duration = diff
} else {
object.dueDate = object.date + (object.allDay ? allDayDuration : duration)
}
if (object.allDay) {
object.date = new Date(object.date).setUTCHours(0, 0, 0, 0)
object.dueDate = new Date(object.dueDate).setUTCHours(0, 0, 0, 0)
}
await tick()
dueDateRef.adaptValue()
await updateDate()
}
}
async function allDayChangeHandler () {
if (object.allDay) {
object.date = new Date(object.date).setUTCHours(0, 0, 0, 0)
object.dueDate = new Date(object.dueDate).setUTCHours(0, 0, 0, 0)
} else {
object.dueDate = object.date + defaultDuration
}
await tick()
dueDateRef.adaptValue()
await updateDate()
}
let dueDateRef: DateRangePresenter
function showMenu (ev?: MouseEvent): void {
if (object !== undefined) {
showPopup(ContextMenu, { object, excludedActions: [view.action.Open] }, ev?.target as HTMLElement)
}
}
</script>
{#if object !== undefined}
{#if object.attachedTo && object.attachedToClass}
<Panel
icon={calendar.icon.Calendar}
title={object.title}
{object}
isAside={false}
isHeader={false}
on:open
on:close={() => {
dispatch('close')
}}
withoutActivity={true}
withoutInput={true}
>
<svelte:fragment slot="utils">
<div class="p-1">
<Button icon={IconMoreH} kind={'ghost'} size={'medium'} on:click={showMenu} />
</div>
</svelte:fragment>
{#if doc}
<div class="mb-4">
<div class="flex-row-center p-1">
<Label label={calendar.string.EventFor} />
@ -72,7 +162,7 @@
{/if}
<div class="mb-2">
<div class="mb-4">
<StylishEdit
<EditBox
label={calendar.string.Title}
bind:value={object.title}
on:change={() => client.update(object, { title: object.title })}
@ -90,4 +180,32 @@
/>
</div>
</div>
{/if}
<div class="flex-row-center flex-gap-2">
<div>
<ToggleWithLabel bind:on={object.allDay} label={calendar.string.AllDay} on:change={allDayChangeHandler} />
</div>
<div>
<DateRangePresenter
value={object.date}
labelNull={ui.string.SelectDate}
on:change={async (event) => await handleNewStartDate(event.detail)}
{mode}
kind={'regular'}
size={'large'}
editable
/>
</div>
<div>
<DateRangePresenter
bind:this={dueDateRef}
value={object.dueDate}
labelNull={calendar.string.DueTo}
on:change={async (event) => await handleNewDueDate(event.detail)}
{mode}
kind={'regular'}
size={'large'}
editable
/>
</div>
</div>
</Panel>

View File

@ -0,0 +1,295 @@
<!--
// 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, ReccuringInstance, generateEventId } from '@hcengineering/calendar'
import { DateRangeMode, Doc, DocumentUpdate, WithLookup } from '@hcengineering/core'
import { Panel } from '@hcengineering/panel'
import { createQuery, getClient } from '@hcengineering/presentation'
import { StyledTextBox } from '@hcengineering/text-editor'
import ui, {
AnyComponent,
Button,
Component,
DateRangePresenter,
EditBox,
IconMoreH,
Label,
ToggleWithLabel,
closePopup,
showPopup
} from '@hcengineering/ui'
import view from '@hcengineering/view'
import { ContextMenu, ObjectPresenter, getObjectPreview } from '@hcengineering/view-resources'
import { createEventDispatcher, tick } from 'svelte'
import calendar from '../plugin'
import UpdateRecInstancePopup from './UpdateRecInstancePopup.svelte'
export let object: WithLookup<ReccuringInstance>
const dispatch = createEventDispatcher()
const client = getClient()
const query = createQuery()
let doc: Doc | undefined
$: if (object.attachedTo !== calendar.ids.NoAttached) {
query.query(object.attachedToClass, { _id: object.attachedTo }, (res) => {
doc = res[0]
})
}
let presenter: AnyComponent | undefined
async function updatePreviewPresenter (doc?: Doc): Promise<void> {
if (doc === undefined) {
return
}
presenter = doc !== undefined ? await getObjectPreview(client, doc._class) : undefined
}
$: updatePreviewPresenter(doc)
$: mode = object.allDay ? DateRangeMode.DATE : DateRangeMode.DATETIME
const defaultDuration = 30 * 60 * 1000
const allDayDuration = 24 * 60 * 60 * 1000
let duration = object.dueDate - object.date
async function updatePast (ops: DocumentUpdate<Event>) {
const origin = await client.findOne(calendar.class.ReccuringEvent, {
eventId: object.recurringEventId,
space: object.space
})
if (origin !== undefined) {
await client.addCollection(
calendar.class.ReccuringEvent,
origin.space,
origin.attachedTo,
origin.attachedToClass,
origin.collection,
{
...origin,
date: object.date,
dueDate: object.dueDate,
...ops,
eventId: generateEventId()
}
)
const targetDate = ops.date ?? object.date
await client.update(origin, {
rules: [{ ...origin.rules[0], endDate: targetDate - 1 }],
rdate: origin.rdate.filter((p) => p < targetDate)
})
const instances = await client.findAll(calendar.class.ReccuringInstance, {
recurringEventId: origin.eventId,
date: { $gte: targetDate }
})
for (const instance of instances) {
await client.remove(instance)
}
}
}
async function updateHandler (ops: DocumentUpdate<Event>) {
if (object.virtual !== true) {
await client.update(object, ops)
} else {
showPopup(UpdateRecInstancePopup, {}, undefined, async (res) => {
if (res !== null) {
if (res.mode === 'current') {
await client.addCollection(
object._class,
object.space,
object.attachedTo,
object.attachedToClass,
object.collection,
{
title: object.title,
description: object.description,
date: object.date,
dueDate: object.dueDate,
allDay: object.allDay,
participants: object.participants,
externalParticipants: object.externalParticipants,
originalStartTime: object.originalStartTime,
recurringEventId: object.recurringEventId,
reminders: object.reminders,
location: object.location,
eventId: object.eventId,
access: 'owner'
},
object._id
)
} else if (res.mode === 'all') {
const base = await client.findOne(calendar.class.ReccuringEvent, {
space: object.space,
eventId: object.recurringEventId
})
if (base !== undefined) {
await client.update(base, ops)
}
} else if (res.mode === 'next') {
await updatePast(ops)
}
}
closePopup()
})
}
}
async function updateDate () {
await updateHandler({
date: object.date,
dueDate: object.dueDate,
allDay: object.allDay
})
}
async function handleNewStartDate (newStartDate: number | null) {
if (newStartDate !== null) {
object.date = newStartDate
object.dueDate = object.date + (object.allDay ? allDayDuration : duration)
if (object.allDay) {
object.date = new Date(object.date).setUTCHours(0, 0, 0, 0)
object.dueDate = new Date(object.dueDate).setUTCHours(0, 0, 0, 0)
}
await tick()
dueDateRef.adaptValue()
await updateDate()
}
}
async function handleNewDueDate (newDueDate: number | null) {
if (newDueDate !== null) {
const diff = newDueDate - object.date
if (diff > 0) {
object.dueDate = newDueDate
duration = diff
} else {
object.dueDate = object.date + (object.allDay ? allDayDuration : duration)
}
if (object.allDay) {
object.date = new Date(object.date).setUTCHours(0, 0, 0, 0)
object.dueDate = new Date(object.dueDate).setUTCHours(0, 0, 0, 0)
}
await tick()
dueDateRef.adaptValue()
await updateDate()
}
}
async function allDayChangeHandler () {
if (object.allDay) {
object.date = new Date(object.date).setUTCHours(0, 0, 0, 0)
object.dueDate = new Date(object.dueDate).setUTCHours(0, 0, 0, 0)
} else {
object.dueDate = object.date + defaultDuration
}
await tick()
dueDateRef.adaptValue()
await updateDate()
}
let dueDateRef: DateRangePresenter
function showMenu (ev?: MouseEvent): void {
if (object !== undefined) {
showPopup(ContextMenu, { object, excludedActions: [view.action.Open] }, ev?.target as HTMLElement)
}
}
</script>
<Panel
icon={calendar.icon.Calendar}
title={object.title}
{object}
isAside={false}
isHeader={false}
on:open
on:close={() => {
dispatch('close')
}}
withoutActivity={true}
withoutInput={true}
>
<svelte:fragment slot="utils">
<div class="p-1">
<Button icon={IconMoreH} kind={'ghost'} size={'medium'} on:click={showMenu} />
</div>
</svelte:fragment>
{#if doc}
<div class="mb-4">
<div class="flex-row-center p-1">
<Label label={calendar.string.EventFor} />
<div class="ml-2">
<ObjectPresenter _class={object.attachedToClass} objectId={object.attachedTo} value={doc} />
</div>
</div>
{#if presenter !== undefined && doc}
<div class="antiPanel p-4">
<Component is={presenter} props={{ object: doc }} />
</div>
{/if}
</div>
{/if}
<div class="mb-2">
<div class="mb-4">
<EditBox
label={calendar.string.Title}
bind:value={object.title}
on:change={() => updateHandler({ title: object.title })}
/>
</div>
<div class="mb-2">
<StyledTextBox
kind={'emphasized'}
content={object.description}
on:value={(evt) => {
updateHandler({ description: evt.detail })
}}
label={calendar.string.Description}
placeholder={calendar.string.Description}
/>
</div>
</div>
<div class="flex-row-center flex-gap-2">
<div>
<ToggleWithLabel bind:on={object.allDay} label={calendar.string.AllDay} on:change={allDayChangeHandler} />
</div>
<div>
<DateRangePresenter
value={object.date}
labelNull={ui.string.SelectDate}
on:change={async (event) => await handleNewStartDate(event.detail)}
{mode}
kind={'regular'}
size={'large'}
editable
/>
</div>
<div>
<DateRangePresenter
bind:this={dueDateRef}
value={object.dueDate}
labelNull={calendar.string.DueTo}
on:change={async (event) => await handleNewDueDate(event.detail)}
{mode}
kind={'regular'}
size={'large'}
editable
/>
</div>
</div>
</Panel>

View File

@ -14,24 +14,35 @@
-->
<script lang="ts">
import { Event } from '@hcengineering/calendar'
import { DateTimeRangePresenter, showPanel } from '@hcengineering/ui'
import view from '@hcengineering/view'
import { DatePresenter, DateTimeRangePresenter, showPopup } from '@hcengineering/ui'
import calendar from '../plugin'
export let value: Event
export let inline: boolean = false
function click (): void {
showPanel(view.component.EditDoc, value._id, value._class, 'content')
showPopup(
value._class === calendar.class.ReccuringInstance
? calendar.component.EditRecEvent
: calendar.component.EditEvent,
{ object: value },
'content'
)
}
</script>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div class="antiSelect w-full cursor-pointer flex-center flex-between" on:click={click}>
{#if value}
<div class="mr-4">
{value.title}
</div>
{#if !inline}
<DateTimeRangePresenter value={value.date} />
{#if value.allDay}
<DatePresenter value={value.date} />
{:else}
<DateTimeRangePresenter value={value.date} />
{/if}
{/if}
{/if}
</div>

View File

@ -14,23 +14,11 @@
-->
<script lang="ts">
import type { Event } from '@hcengineering/calendar'
import { Class, Doc, DocumentQuery, FindOptions, Ref } from '@hcengineering/core'
import { Table } from '@hcengineering/view-resources'
import EventPresenter from './EventPresenter.svelte'
export let value: Event[]
export let _class: Ref<Class<Doc>>
export let query: DocumentQuery<Event> = {}
export let options: FindOptions<Event> | undefined = undefined
export let baseMenuClass: Ref<Class<Event>> | undefined = undefined
export let config: string[]
</script>
<Table
{_class}
{config}
{baseMenuClass}
{options}
query={{ ...query, _id: { $in: value.map((it) => it._id) } }}
loadingProps={{ length: value.length ?? 0 }}
/>
{#each value as val}
<EventPresenter value={val} />
{/each}

View File

@ -47,7 +47,7 @@
}
function getHeight (e: Event, date: Date): string {
if (e.dueDate !== undefined && new Date(e.dueDate).getHours() === date.getHours()) {
if (new Date(e.dueDate).getHours() === date.getHours()) {
return `${(new Date(e.dueDate).getMinutes() / 60) * 100}%`
}
return '100%'
@ -75,9 +75,10 @@
return total
}
function getStyle (events: Event[], i: number, date: Date): string {
function getStyle (events: Event[], i: number, date: Date, dark: boolean): string {
const e = events[i]
let res = `background-color: ${getPlatformColorForTextDef(e._class, $themeStore.dark).background};`
const color = getPlatformColorForTextDef(e.space, dark)
let res = `background: ${color.color}33; border: 1px solid ${color.color}99; border-left: 0.25rem solid ${color.color}; `
res += `left: ${getShift(events, i)}rem;`
if (startCell(e.date, date)) {
res += ` top: ${getTop(e)};`
@ -110,7 +111,7 @@
class:isStart={startCell(e.date, date)}
class:isEnd={endCell(e.dueDate ?? e.date, date)}
class:wide
style={getStyle(events, i, date)}
style={getStyle(events, i, date, $themeStore.dark)}
on:click|stopPropagation={() => {
showPanel(view.component.EditDoc, e._id, e._class, 'content')
}}
@ -130,13 +131,10 @@
padding: 0 0.5rem;
height: 100%;
border: 1px solid #00000033;
width: 1rem;
&.wide {
width: 5rem;
&.isStart {
.title {
visibility: visible;
}
width: 100%;
&.isStart {
.title {
visibility: visible;
}
}
@ -145,12 +143,12 @@
}
&.isStart {
border-top-left-radius: 0.5rem;
border-top-right-radius: 0.5rem;
border-top-left-radius: 0.25rem;
border-top-right-radius: 0.25rem;
}
&.isEnd {
border-bottom-left-radius: 0.5rem;
border-bottom-right-radius: 0.5rem;
border-bottom-left-radius: 0.25rem;
border-bottom-right-radius: 0.25rem;
}
}
</style>

View File

@ -0,0 +1,111 @@
<!--
// 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 { getMetadata } from '@hcengineering/platform'
import { Button, IconClose, Label } from '@hcengineering/ui'
import { createEventDispatcher } from 'svelte'
import calendar from '../plugin'
import { concatLink } from '@hcengineering/core'
import presentation from '@hcengineering/presentation'
const dispatch = createEventDispatcher()
let connecting = false
const calendarUrl = getMetadata(calendar.metadata.CalendarServiceURL) ?? ''
async function sendRequest (): Promise<void> {
connecting = true
const link = concatLink(calendarUrl, '/signin')
const url = new URL(link)
url.search = new URLSearchParams({
redirectURL: window.location.href
}).toString()
const res = await fetch(url.toString(), {
method: 'GET',
headers: {
Authorization: 'Bearer ' + getMetadata(presentation.metadata.Token),
'Content-Type': 'application/json'
}
})
const redirectTo = await res.text()
window.open(redirectTo, '_self')
}
</script>
<div class="card">
<div class="flex-between header">
<div class="overflow-label fs-title"><Label label={calendar.string.ConnectCalendar} /></div>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div
class="tool"
on:click={() => {
dispatch('close')
}}
>
<IconClose size={'small'} />
</div>
</div>
<div class="content">
<Label label={calendar.string.RedirectGoogle} />
<div class="footer">
<Button label={calendar.string.Connect} kind={'accented'} disabled={connecting} on:click={sendRequest} />
</div>
</div>
</div>
<style lang="scss">
.card {
position: relative;
display: flex;
flex-direction: column;
width: 20rem;
min-width: 20rem;
max-width: 20rem;
background-color: var(--popup-bg-hover);
border-radius: 0.75rem;
box-shadow: var(--popup-shadow);
.header {
flex-shrink: 0;
margin: 1.75rem 1.75rem 1.25rem;
.tool {
cursor: pointer;
&:hover {
color: var(--caption-color);
}
&:active {
color: var(--accent-color);
}
}
}
.content {
flex-shrink: 0;
flex-grow: 1;
height: fit-content;
margin: 0 1.75rem 0.5rem;
}
.footer {
display: flex;
flex-direction: row-reverse;
justify-content: space-between;
align-items: center;
padding: 1rem 0;
}
}
</style>

View File

@ -13,12 +13,12 @@
// limitations under the License.
-->
<script lang="ts">
import { Reminder } from '@hcengineering/calendar'
import { Event } from '@hcengineering/calendar'
import { getResource } from '@hcengineering/platform'
import { DateTimeRangePresenter, showPanel, tooltip } from '@hcengineering/ui'
import view from '@hcengineering/view'
export let value: Reminder
export let value: Event
function click (): void {
showPanel(view.component.EditDoc, value._id, value._class, 'content')
@ -34,6 +34,10 @@
{value.title}
</div>
{/await}
<DateTimeRangePresenter value={value.date + value.shift} />
<div class="flex-row-center">
{#each value.reminders ?? [] as reminder}
<DateTimeRangePresenter value={value.date + reminder} />
{/each}
</div>
{/if}
</div>

View File

@ -44,9 +44,10 @@
return
}
await client.updateMixin(event._id, event._class, event.space, calendar.mixin.Reminder, {
shift,
state: 'active'
await client.update(event, {
$push: {
reminders: shift
}
})
}
</script>

View File

@ -0,0 +1,108 @@
<!--
// 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 presentation from '@hcengineering/presentation'
import {
Button,
DropdownIntlItem,
DropdownLabelsIntl,
FocusHandler,
Label,
createFocusManager
} from '@hcengineering/ui'
import { createEventDispatcher } from 'svelte'
import calendar from '../plugin'
import { IntlString } from '@hcengineering/platform'
export let label: IntlString = calendar.string.EditRecEvent
const dispatch = createEventDispatcher()
const manager = createFocusManager()
const items: DropdownIntlItem[] = [
{
id: 'current',
label: calendar.string.ThisEvent
},
{
id: 'next',
label: calendar.string.ThisAndNext
},
{
id: 'all',
label: calendar.string.AllEvents
}
]
let selected = 'current'
</script>
<FocusHandler {manager} />
<div class="msgbox-container">
<div class="overflow-label fs-title mb-4"><Label {label} /></div>
<div>
<DropdownLabelsIntl {items} bind:selected />
</div>
<div class="footer">
<Button
focus
focusIndex={1}
label={presentation.string.Ok}
size={'large'}
kind={'accented'}
on:click={() => {
dispatch('close', { mode: selected })
}}
/>
<Button
focusIndex={2}
label={presentation.string.Cancel}
size={'large'}
on:click={() => {
dispatch('close')
}}
/>
</div>
</div>
<style lang="scss">
.msgbox-container {
display: flex;
flex-direction: column;
padding: 2rem 1.75rem 1.75rem;
width: 30rem;
max-width: 40rem;
background: var(--theme-popup-color);
border-radius: 0.5rem;
user-select: none;
box-shadow: var(--theme-popup-shadow);
.message {
margin-bottom: 1.75rem;
color: var(--theme-content-color);
}
.footer {
flex-shrink: 0;
display: grid;
grid-auto-flow: column;
direction: rtl;
justify-content: flex-start;
align-items: center;
column-gap: 0.5rem;
}
}
</style>

View File

@ -13,14 +13,14 @@
// limitations under the License.
-->
<script lang="ts">
import { Event, Reminder } from '@hcengineering/calendar'
import { Ref, TxMixin } from '@hcengineering/core'
import { Event } from '@hcengineering/calendar'
import { Ref, TxUpdateDoc } from '@hcengineering/core'
import { getClient } from '@hcengineering/presentation'
import { DateTimePresenter, showPanel } from '@hcengineering/ui'
import view from '@hcengineering/view'
import calendar from '../../plugin'
export let tx: TxMixin<Event, Reminder>
export let tx: TxUpdateDoc<Event>
const client = getClient()

View File

@ -0,0 +1,67 @@
<!--
// 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.
-->
<svg
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
x="0px"
y="0px"
viewBox="0 0 36 36"
style="enable-background:new 0 0 36 36;"
xml:space="preserve"
>
<style type="text/css">
.st0 {
fill: #f1bf42;
}
.st1 {
fill: #3265cb;
}
.st2 {
fill: #5383ec;
}
.st3 {
fill: #3c7e40;
}
.st4 {
fill: #58a65c;
}
.st5 {
fill: #e34134;
}
</style>
<g>
<rect x="25.5" y="10.6" class="st0" width="6.6" height="14.8" />
<path class="st1" d="M32.1,10.6v-4C32.1,5.1,31,4,29.5,4h-4v6.6H32.1z" />
<path class="st2" d="M10.6,10.6h14.9V4H6.6C5.1,4,4,5.1,4,6.6v18.8h6.6V10.6z" />
<path class="st3" d="M4,25.4v3.9C4,30.9,5.1,32,6.6,32h4v-6.6" />
<rect x="10.6" y="25.4" class="st4" width="14.9" height="6.6" />
<polygon class="st5" points="25.5,25.4 25.5,32 25.5,32 32.1,25.4 32.1,25.4 " />
<g>
<path
class="st2"
d="M12.5,20l1.6-0.2c0.1,0.4,0.2,0.7,0.4,0.9S15,21,15.3,21c0.3,0,0.6-0.1,0.9-0.4s0.3-0.6,0.3-1
c0-0.4-0.1-0.7-0.3-1s-0.5-0.4-0.8-0.4c-0.2,0-0.5,0-0.8,0.1l0.2-1.3c0.4,0,0.8-0.1,1-0.3s0.4-0.5,0.4-0.8c0-0.3-0.1-0.5-0.3-0.7
S15.5,15,15.3,15c-0.3,0-0.5,0.1-0.7,0.3s-0.3,0.5-0.4,0.9l-1.5-0.3c0.1-0.5,0.3-0.9,0.5-1.2s0.5-0.6,0.9-0.7s0.8-0.3,1.3-0.3
c0.8,0,1.5,0.3,1.9,0.8c0.4,0.4,0.6,0.9,0.6,1.4c0,0.8-0.4,1.4-1.2,1.8c0.5,0.1,0.9,0.3,1.2,0.7s0.4,0.8,0.4,1.3
c0,0.8-0.3,1.4-0.8,1.9s-1.2,0.8-2.1,0.8c-0.8,0-1.4-0.2-1.9-0.7S12.6,20.7,12.5,20z"
/>
<path
class="st2"
d="M23.5,22.2h-1.6V16c-0.6,0.6-1.3,1-2.1,1.2v-1.5c0.4-0.1,0.9-0.4,1.4-0.8s0.8-0.9,1-1.4h1.3V22.2z"
/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@ -13,25 +13,118 @@
// limitations under the License.
//
import { Doc } from '@hcengineering/core'
import { Resources } from '@hcengineering/platform'
import { showPopup } from '@hcengineering/ui'
import { ReccuringInstance } from '@hcengineering/calendar'
import { Doc, TxOperations, concatLink } from '@hcengineering/core'
import { Resources, getMetadata } from '@hcengineering/platform'
import presentation, { getClient } from '@hcengineering/presentation'
import { closePopup, showPopup } from '@hcengineering/ui'
import CalendarView from './components/CalendarView.svelte'
import SaveEventReminder from './components/SaveEventReminder.svelte'
import CreateEvent from './components/CreateEvent.svelte'
import DateTimePresenter from './components/DateTimePresenter.svelte'
import DocReminder from './components/DocReminder.svelte'
import PersonsPresenter from './components/PersonsPresenter.svelte'
import Events from './components/Events.svelte'
import ReminderPresenter from './components/ReminderPresenter.svelte'
import ReminderViewlet from './components/activity/ReminderViewlet.svelte'
import EditEvent from './components/EditEvent.svelte'
import EditRecEvent from './components/EditRecEvent.svelte'
import EventPresenter from './components/EventPresenter.svelte'
import CreateEvent from './components/CreateEvent.svelte'
import Events from './components/Events.svelte'
import IntegrationConnect from './components/IntegrationConnect.svelte'
import PersonsPresenter from './components/PersonsPresenter.svelte'
import SaveEventReminder from './components/SaveEventReminder.svelte'
import UpdateRecInstancePopup from './components/UpdateRecInstancePopup.svelte'
import ReminderViewlet from './components/activity/ReminderViewlet.svelte'
import CalendarIntegrationIcon from './components/icons/Calendar.svelte'
import calendar from './plugin'
import contact from '@hcengineering/contact'
import { deleteObjects } from '@hcengineering/view-resources'
async function saveEventReminder (object: Doc): Promise<void> {
showPopup(SaveEventReminder, { objectId: object._id, objectClass: object._class })
}
async function deleteRecHandler (res: any, object: ReccuringInstance): Promise<void> {
const client = getClient()
if (res.mode === 'current') {
await client.addCollection(
object._class,
object.space,
object.attachedTo,
object.attachedToClass,
object.collection,
{
eventId: object.eventId,
title: object.title,
description: object.description,
date: object.date,
dueDate: object.dueDate,
allDay: object.allDay,
participants: object.participants,
externalParticipants: object.externalParticipants,
originalStartTime: object.originalStartTime,
recurringEventId: object.recurringEventId,
reminders: object.reminders,
location: object.location,
isCancelled: true,
access: 'owner'
},
object._id
)
} else if (res.mode === 'all') {
const base = await client.findOne(calendar.class.ReccuringEvent, {
space: object.space,
eventId: object.recurringEventId
})
if (base !== undefined) {
await client.remove(base)
}
} else if (res.mode === 'next') {
await removePast(client, object)
}
}
async function deleteRecEvent (object: ReccuringInstance): Promise<void> {
if (object.virtual === true) {
showPopup(UpdateRecInstancePopup, { label: calendar.string.RemoveRecEvent }, undefined, async (res) => {
if (res !== null) {
await deleteRecHandler(res, object)
closePopup()
}
})
} else {
showPopup(
contact.component.DeleteConfirmationPopup,
{
object,
deleteAction: async () => {
const objs = Array.isArray(object) ? object : [object]
await deleteObjects(getClient(), objs).catch((err) => console.error(err))
closePopup()
}
},
undefined
)
}
}
async function removePast (client: TxOperations, object: ReccuringInstance): Promise<void> {
const origin = await client.findOne(calendar.class.ReccuringEvent, {
eventId: object.recurringEventId,
space: object.space
})
if (origin !== undefined) {
const target = object.date
await client.update(origin, {
rules: [{ ...origin.rules[0], endDate: target - 1 }],
rdate: origin.rdate.filter((p) => p < target)
})
const instances = await client.findAll(calendar.class.ReccuringInstance, {
eventId: origin.eventId,
date: { $gte: target }
})
for (const instance of instances) {
await client.remove(instance)
}
}
}
export enum CalendarMode {
Day,
Week,
@ -42,19 +135,36 @@ export enum CalendarMode {
export default async (): Promise<Resources> => ({
component: {
EditEvent,
ReminderPresenter,
EditRecEvent,
PersonsPresenter,
CalendarView,
Events,
DateTimePresenter,
DocReminder,
EventPresenter,
CreateEvent
CreateEvent,
IntegrationConnect,
CalendarIntegrationIcon
},
activity: {
ReminderViewlet
},
actionImpl: {
SaveEventReminder: saveEventReminder
SaveEventReminder: saveEventReminder,
DeleteRecEvent: deleteRecEvent
},
handler: {
DisconnectHandler: async () => {
const url = getMetadata(calendar.metadata.CalendarServiceURL)
const token = getMetadata(presentation.metadata.Token)
if (url === undefined || token === undefined) return
await fetch(concatLink(url, '/signout'), {
method: 'GET',
headers: {
Authorization: 'Bearer ' + token,
'Content-Type': 'application/json'
}
})
}
}
})

View File

@ -19,7 +19,9 @@ import { AnyComponent } from '@hcengineering/ui'
export default mergeIds(calendarId, calendar, {
component: {
CreateEvent: '' as AnyComponent
CreateEvent: '' as AnyComponent,
EditEvent: '' as AnyComponent,
EditRecEvent: '' as AnyComponent
},
activity: {
ReminderViewlet: '' as AnyComponent
@ -42,6 +44,17 @@ export default mergeIds(calendarId, calendar, {
AllDay: '' as IntlString,
AndMore: '' as IntlString,
CreateEvent: '' as IntlString,
EventFor: '' as IntlString
EventFor: '' as IntlString,
ReccuringEvent: '' as IntlString,
HideDetails: '' as IntlString,
ExternalParticipants: '' as IntlString,
RedirectGoogle: '' as IntlString,
Connect: '' as IntlString,
ConnectCalendar: '' as IntlString,
EditRecEvent: '' as IntlString,
RemoveRecEvent: '' as IntlString,
ThisEvent: '' as IntlString,
ThisAndNext: '' as IntlString,
AllEvents: '' as IntlString
}
})

View File

@ -1,6 +1,6 @@
{
"name": "@hcengineering/calendar",
"version": "0.6.11",
"version": "0.6.12",
"main": "lib/index.js",
"author": "Anticrm Platform Contributors",
"license": "EPL-2.0",
@ -30,7 +30,8 @@
"@hcengineering/ui": "^0.6.10",
"@hcengineering/notification": "^0.6.14",
"@hcengineering/core": "^0.6.27",
"@hcengineering/contact": "^0.6.18"
"@hcengineering/contact": "^0.6.18",
"@hcengineering/setting": "^0.6.9"
},
"repository": "https://github.com/hcengineering/anticrm",
"publishConfig": {

View File

@ -11,12 +11,13 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import { Employee } from '@hcengineering/contact'
import type { AttachedDoc, Class, Doc, Markup, Mixin, Ref, Space, Timestamp } from '@hcengineering/core'
import type { Asset, IntlString, Plugin } from '@hcengineering/platform'
import { plugin } from '@hcengineering/platform'
import { AnyComponent } from '@hcengineering/ui'
import { Contact } from '@hcengineering/contact'
import type { AttachedDoc, Class, Doc, Markup, Ref, Space, Timestamp } from '@hcengineering/core'
import { NotificationType } from '@hcengineering/notification'
import type { Asset, IntlString, Metadata, Plugin } from '@hcengineering/platform'
import { plugin } from '@hcengineering/platform'
import type { Handler, IntegrationType } from '@hcengineering/setting'
import { AnyComponent } from '@hcengineering/ui'
/**
* @public
@ -25,31 +26,72 @@ export interface Calendar extends Space {}
/**
* @public
* RFC5545
*/
export interface Event extends AttachedDoc {
title: string
description: Markup
location?: string
// Event scheduled date
date: Timestamp
// Event due date for long events.
dueDate?: Timestamp
attachments?: number
comments?: number
participants?: Ref<Employee>[]
export interface RecurringRule {
freq: 'SECONDLY' | 'MINUTELY' | 'HOURLY' | 'DAILY' | 'WEEKLY' | 'MONTHLY' | 'YEARLY'
endDate?: Timestamp
count?: number
interval?: number
bySecond?: number[]
byMinute?: number[]
byHour?: number[]
byDay?: string[]
byMonthDay?: number[]
byYearDay?: number[]
byWeekNo?: number[]
byMonth?: number[]
bySetPos?: number[]
wkst?: 'SU' | 'MO' | 'TU' | 'WE' | 'TH' | 'FR' | 'SA'
}
/**
* @public
*/
export interface Reminder extends Event {
shift: Timestamp
state: 'active' | 'done'
export interface ReccuringEvent extends Event {
rules: RecurringRule[]
exdate: Timestamp[]
rdate: Timestamp[]
}
/**
* @public
*/
export interface Event extends AttachedDoc {
eventId: string
title: string
description: Markup
location?: string
allDay: boolean
// Event scheduled date
date: Timestamp
// Event due date for long events.
dueDate: Timestamp
attachments?: number
participants: Ref<Contact>[]
externalParticipants?: string[]
reminders?: Timestamp[]
access: 'freeBusyReader' | 'reader' | 'writer' | 'owner'
}
/**
* @public
* use for an instance of a recurring event
*/
export interface ReccuringInstance extends Event {
recurringEventId: string
originalStartTime: number
isCancelled?: boolean
virtual?: boolean
}
/**
@ -63,10 +105,9 @@ export const calendarId = 'calendar' as Plugin
const calendarPlugin = plugin(calendarId, {
class: {
Calendar: '' as Ref<Class<Calendar>>,
Event: '' as Ref<Class<Event>>
},
mixin: {
Reminder: '' as Ref<Mixin<Reminder>>
Event: '' as Ref<Class<Event>>,
ReccuringEvent: '' as Ref<Class<ReccuringEvent>>,
ReccuringInstance: '' as Ref<Class<ReccuringInstance>>
},
icon: {
Calendar: '' as Asset,
@ -75,7 +116,7 @@ const calendarPlugin = plugin(calendarId, {
Notifications: '' as Asset
},
space: {
// Space for all personal events.
// deprecated
PersonalEvents: '' as Ref<Space>
},
app: {
@ -102,6 +143,15 @@ const calendarPlugin = plugin(calendarId, {
EventNumber: '' as IntlString,
Reminders: '' as IntlString
},
handler: {
DisconnectHandler: '' as Handler
},
integrationType: {
Calendar: '' as Ref<IntegrationType>
},
metadata: {
CalendarServiceURL: '' as Metadata<string>
},
ids: {
ReminderNotification: '' as Ref<NotificationType>,
NoAttached: '' as Ref<Event>
@ -109,3 +159,4 @@ const calendarPlugin = plugin(calendarId, {
})
export default calendarPlugin
export * from './utils'

View File

@ -0,0 +1,316 @@
import { Timestamp, generateId } from '@hcengineering/core'
import calendar, { Event, ReccuringEvent, ReccuringInstance, RecurringRule } from '.'
function getInstance (event: ReccuringEvent, date: Timestamp): ReccuringInstance {
const diff = event.dueDate - event.date
return {
...event,
recurringEventId: event.eventId,
date,
dueDate: date + diff,
originalStartTime: date,
_class: calendar.class.ReccuringInstance,
eventId: generateEventId(),
_id: generateId(),
virtual: true
}
}
function generateRecurringValues (
rule: RecurringRule,
startDate: Timestamp,
from: Timestamp,
to: Timestamp
): Timestamp[] {
const values: Timestamp[] = []
const currentDate = new Date(startDate)
switch (rule.freq) {
case 'DAILY':
generateDailyValues(rule, currentDate, values, from, to)
break
case 'WEEKLY':
generateWeeklyValues(rule, currentDate, values, from, to)
break
case 'MONTHLY':
generateMonthlyValues(rule, currentDate, values, from, to)
break
case 'YEARLY':
generateYearlyValues(rule, currentDate, values, from, to)
break
default:
throw new Error('Invalid recurring rule frequency')
}
return values
}
function generateDailyValues (
rule: RecurringRule,
currentDate: Date,
values: Timestamp[],
from: Timestamp,
to: Timestamp
): void {
const { count, endDate, interval } = rule
const { bySecond, byMinute, byHour, bySetPos } = rule
let i = 0
while (true) {
if (
(bySecond == null || bySecond.includes(currentDate.getSeconds())) &&
(byMinute == null || byMinute.includes(currentDate.getMinutes())) &&
(byHour == null || byHour.includes(currentDate.getHours())) &&
(bySetPos == null || bySetPos.includes(getSetPos(currentDate)))
) {
const res = currentDate.getTime()
if (res > from) {
values.push(res)
}
i++
}
currentDate.setDate(currentDate.getDate() + (interval ?? 1))
if (count !== undefined && i === count) break
if (endDate !== undefined && currentDate.getTime() > endDate) break
if (currentDate.getTime() > to) break
}
}
function generateWeeklyValues (
rule: RecurringRule,
currentDate: Date,
values: Timestamp[],
from: Timestamp,
to: Timestamp
): void {
const { count, endDate, interval } = rule
const { bySecond, byMinute, byHour, byDay, wkst, bySetPos } = rule
let i = 0
while (true) {
if (
(bySecond == null || bySecond.includes(currentDate.getSeconds())) &&
(byMinute == null || byMinute.includes(currentDate.getMinutes())) &&
(byHour == null || byHour.includes(currentDate.getHours())) &&
(byDay == null || byDay.includes(getWeekday(currentDate, wkst))) &&
(bySetPos == null || bySetPos.includes(getSetPos(currentDate)))
) {
const res = currentDate.getTime()
if (res > from) {
values.push(res)
}
i++
}
currentDate.setDate(currentDate.getDate() + (interval ?? 1) * 7)
if (count !== undefined && i === count) break
if (endDate !== undefined && currentDate.getTime() > endDate) break
if (currentDate.getTime() > to) break
}
}
function generateMonthlyValues (
rule: RecurringRule,
currentDate: Date,
values: Timestamp[],
from: Timestamp,
to: Timestamp
): void {
const { count, endDate, interval } = rule
const { bySecond, byMinute, byHour, byDay, byMonthDay, bySetPos, wkst } = rule
let i = 0
while (true) {
if (
(bySecond == null || bySecond.includes(currentDate.getSeconds())) &&
(byMinute == null || byMinute.includes(currentDate.getMinutes())) &&
(byHour == null || byHour.includes(currentDate.getHours())) &&
(byDay == null || byDay.includes(getWeekday(currentDate, wkst))) &&
(byMonthDay == null || byMonthDay.includes(new Date(currentDate).getDate())) &&
(bySetPos == null || bySetPos.includes(getSetPos(currentDate)))
) {
const res = currentDate.getTime()
if (res > from) {
values.push(res)
}
i++
}
currentDate.setMonth(currentDate.getMonth() + (interval ?? 1))
if (count !== undefined && i === count) break
if (endDate !== undefined && currentDate.getTime() > endDate) break
if (currentDate.getTime() > to) break
}
}
function generateYearlyValues (
rule: RecurringRule,
currentDate: Date,
values: Timestamp[],
from: Timestamp,
to: Timestamp
): void {
const { count, endDate, interval } = rule
const { bySecond, byMinute, byHour, byDay, byMonthDay, byYearDay, byWeekNo, byMonth, bySetPos, wkst } = rule
let i = 0
while (true) {
if (
(bySecond == null || bySecond.includes(currentDate.getSeconds())) &&
(byMinute == null || byMinute.includes(currentDate.getMinutes())) &&
(byHour == null || byHour.includes(currentDate.getHours())) &&
(byDay == null || byDay.includes(getWeekday(currentDate, wkst))) &&
(byMonthDay == null || byMonthDay.includes(currentDate.getDate())) &&
(byYearDay == null || byYearDay.includes(getYearDay(currentDate))) &&
(byWeekNo == null || byWeekNo.includes(getWeekNumber(currentDate))) &&
(byMonth == null || byMonth.includes(currentDate.getMonth())) &&
(bySetPos == null || bySetPos.includes(getSetPos(currentDate)))
) {
const res = currentDate.getTime()
if (res > from) {
values.push(res)
}
i++
}
currentDate.setFullYear(currentDate.getFullYear() + (interval ?? 1))
if (count !== undefined && i === count) break
if (endDate !== undefined && currentDate.getTime() > endDate) break
if (currentDate.getTime() > to) break
}
}
function getSetPos (date: Date): number {
const month = date.getMonth()
const year = date.getFullYear()
const firstOfMonth = new Date(year, month, 1)
const daysOffset = firstOfMonth.getDay()
const day = date.getDate()
return Math.ceil((day + daysOffset) / 7)
}
function getWeekday (date: Date, wkst?: string): string {
const weekdays = ['SU', 'MO', 'TU', 'WE', 'TH', 'FR', 'SA']
const weekday = weekdays[date.getDay()]
if (wkst !== undefined && wkst !== 'MO') {
const wkstIndex = weekdays.indexOf(wkst)
const offset = wkstIndex > 0 ? wkstIndex - 1 : 6
return weekdays[(date.getDay() + offset) % 7]
}
return weekday
}
function getWeekNumber (date: Date): number {
const firstDayOfYear = new Date(date.getFullYear(), 0, 1)
const daysOffset = firstDayOfYear.getDay()
const diff = (date.getTime() - firstDayOfYear.getTime()) / 86400000
return Math.floor((diff + daysOffset + 1) / 7)
}
function getYearDay (date: Date): number {
const startOfYear = new Date(date.getFullYear(), 0, 0)
const diff = date.getTime() - startOfYear.getTime()
return Math.floor(diff / 86400000)
}
function getReccuringEventInstances (
event: ReccuringEvent,
instances: ReccuringInstance[],
from: Timestamp,
to: Timestamp
): ReccuringInstance[] {
let res: ReccuringInstance[] = []
for (const rule of event.rules) {
const values = generateRecurringValues(rule, event.date, from, to)
for (const val of values) {
const instance = getInstance(event, val)
res.push(instance)
}
}
for (const date of event.rdate) {
if (date < from || date > to) continue
const instance = getInstance(event, date)
const exists = res.find((p) => p.date === instance.date)
if (exists === undefined) res.push(instance)
}
const excludes = new Set(event.exdate)
res = res.filter((p) => !excludes.has(p.date))
res = res.filter((i) => {
const override = instances.find((p) => p.originalStartTime === i.date)
return override === undefined
})
return res
}
/**
* @public
*/
export function getAllEvents (events: Event[], from: Timestamp, to: Timestamp): Event[] {
const base: Event[] = []
const recur: ReccuringEvent[] = []
const instances: ReccuringInstance[] = []
const recurData: ReccuringInstance[] = []
const instancesMap: Map<string, ReccuringInstance[]> = new Map()
for (const event of events) {
if (event._class === calendar.class.Event) {
base.push(event)
} else if (event._class === calendar.class.ReccuringEvent) {
recur.push(event as ReccuringEvent)
} else if (event._class === calendar.class.ReccuringInstance) {
const instance = event as ReccuringInstance
instances.push(instance)
const arr = instancesMap.get(instance.recurringEventId) ?? []
arr.push(instance)
instancesMap.set(instance.recurringEventId, arr)
}
}
for (const rec of recur) {
recurData.push(...getReccuringEventInstances(rec, instancesMap.get(rec.eventId) ?? [], from, to))
}
const res = [...base, ...recurData, ...instances]
res.sort((a, b) => a.date - b.date)
return res
}
/**
* @public
*/
export function generateEventId (): string {
const id = generateId()
return encodeToBase32Hex(id)
}
function encodeToBase32Hex (input: string): string {
const encoder = new TextEncoder()
const bytes = encoder.encode(input)
const base32HexDigits = '0123456789abcdefghijklmnopqrstuv'
let result = ''
for (let i = 0; i < bytes.length; i += 5) {
const octets = [
(bytes[i] ?? 0) >> 3,
((bytes[i] & 0x07) << 2) | (bytes[i + 1] >> 6),
(bytes[i + 1] >> 1) & 0x1f,
((bytes[i + 1] & 0x01) << 4) | (bytes[i + 2] >> 4),
((bytes[i + 2] & 0x0f) << 1) | (bytes[i + 3] >> 7),
(bytes[i + 3] >> 2) & 0x1f,
((bytes[i + 3] & 0x03) << 3) | (bytes[i + 4] >> 5),
bytes[i + 4] & 0x1f
]
for (let j = 0; j < 8; j++) {
result += base32HexDigits.charAt(octets[j])
}
}
return result
}

View File

@ -44,7 +44,7 @@
"@hcengineering/core": "^0.6.27",
"@hcengineering/view": "^0.6.8",
"@hcengineering/attachment-resources": "^0.6.0",
"@hcengineering/panel": "^0.6.9",
"@hcengineering/panel": "^0.6.10",
"@hcengineering/view-resources": "^0.6.0",
"@hcengineering/attachment": "^0.6.8",
"@hcengineering/login": "^0.6.7",

View File

@ -41,7 +41,7 @@
"@hcengineering/core": "^0.6.27",
"@hcengineering/view": "^0.6.8",
"@hcengineering/attachment-resources": "^0.6.0",
"@hcengineering/panel": "^0.6.9",
"@hcengineering/panel": "^0.6.10",
"@hcengineering/view-resources": "^0.6.0",
"@hcengineering/attachment": "^0.6.8",
"@hcengineering/notification": "^0.6.14",

View File

@ -48,7 +48,7 @@
"@hcengineering/attachment-resources": "^0.6.0",
"@hcengineering/login": "^0.6.7",
"@hcengineering/core": "^0.6.27",
"@hcengineering/panel": "^0.6.9",
"@hcengineering/panel": "^0.6.10",
"@hcengineering/templates": "^0.6.6"
}
}

View File

@ -91,8 +91,7 @@ export default plugin(gmailId, {
Gmail: '' as Ref<IntegrationType>
},
handler: {
DisconnectHandler: '' as Handler,
ReconnectHandler: '' as Handler
DisconnectHandler: '' as Handler
},
class: {
Message: '' as Ref<Class<Message>>,

View File

@ -33,13 +33,13 @@
"dependencies": {
"@hcengineering/platform": "^0.6.9",
"svelte": "3.55.1",
"@hcengineering/calendar": "^0.6.11",
"@hcengineering/calendar": "^0.6.12",
"@hcengineering/calendar-resources": "^0.6.0",
"@hcengineering/hr": "^0.6.9",
"@hcengineering/ui": "^0.6.10",
"@hcengineering/presentation": "^0.6.2",
"@hcengineering/core": "^0.6.27",
"@hcengineering/panel": "^0.6.9",
"@hcengineering/panel": "^0.6.10",
"@hcengineering/contact": "^0.6.18",
"@hcengineering/view": "^0.6.8",
"@hcengineering/view-resources": "^0.6.0",

View File

@ -36,7 +36,7 @@
"svelte": "3.55.1",
"@hcengineering/inventory": "^0.6.6",
"@hcengineering/ui": "^0.6.10",
"@hcengineering/panel": "^0.6.9",
"@hcengineering/panel": "^0.6.10",
"@hcengineering/presentation": "^0.6.2",
"@hcengineering/view": "^0.6.8",
"@hcengineering/view-resources": "^0.6.0",

View File

@ -37,7 +37,7 @@
"@hcengineering/ui": "^0.6.10",
"@hcengineering/presentation": "^0.6.2",
"@hcengineering/core": "^0.6.27",
"@hcengineering/panel": "^0.6.9",
"@hcengineering/panel": "^0.6.10",
"@hcengineering/contact": "^0.6.18",
"@hcengineering/view": "^0.6.8",
"@hcengineering/task": "^0.6.10",

View File

@ -1,6 +1,6 @@
{
"name": "@hcengineering/recruit-assets",
"version": "0.6.9",
"version": "0.6.10",
"main": "lib/index.js",
"author": "Anticrm Platform Contributors",
"template": "@hcengineering/assets-package",
@ -30,7 +30,7 @@
},
"dependencies": {
"@hcengineering/platform": "^0.6.9",
"@hcengineering/recruit": "^0.6.15"
"@hcengineering/recruit": "^0.6.16"
},
"repository": "https://github.com/hcenginneing/anticrm",
"publishConfig": {

View File

@ -34,7 +34,7 @@
"@hcengineering/platform": "^0.6.9",
"@hcengineering/core": "^0.6.27",
"svelte": "3.55.1",
"@hcengineering/recruit": "^0.6.15",
"@hcengineering/recruit": "^0.6.16",
"@hcengineering/ui": "^0.6.10",
"@hcengineering/presentation": "^0.6.2",
"@hcengineering/text-editor": "^0.6.0",
@ -42,7 +42,7 @@
"@hcengineering/contact": "^0.6.18",
"@hcengineering/login": "^0.6.7",
"@hcengineering/workbench": "^0.6.8",
"@hcengineering/panel": "^0.6.9",
"@hcengineering/panel": "^0.6.10",
"@hcengineering/activity": "^0.6.0",
"@hcengineering/attachment": "^0.6.8",
"@hcengineering/attachment-resources": "^0.6.0",
@ -55,7 +55,7 @@
"@hcengineering/rekoni": "^0.6.0",
"@hcengineering/notification": "^0.6.14",
"@hcengineering/tags": "^0.6.11",
"@hcengineering/calendar": "^0.6.11",
"@hcengineering/calendar": "^0.6.12",
"@hcengineering/tracker": "^0.6.10"
}
}

View File

@ -14,7 +14,6 @@
// limitations under the License.
-->
<script lang="ts">
import calendar from '@hcengineering/calendar'
import contact, { Contact } from '@hcengineering/contact'
import { UserBox } from '@hcengineering/contact-resources'
import { Hierarchy } from '@hcengineering/core'
@ -41,8 +40,7 @@
onMount(() => {
dispatch('open', {
ignoreKeys: ['number', 'comments', 'title', 'description', 'verdict'],
ignoreMixins: [calendar.mixin.Reminder]
ignoreKeys: ['number', 'comments', 'title', 'description', 'verdict']
})
})

View File

@ -1,6 +1,6 @@
{
"name": "@hcengineering/recruit",
"version": "0.6.15",
"version": "0.6.16",
"main": "lib/index.js",
"author": "Anticrm Platform Contributors",
"license": "EPL-2.0",
@ -31,7 +31,7 @@
"@hcengineering/contact": "^0.6.18",
"@hcengineering/chunter": "^0.6.9",
"@hcengineering/task": "^0.6.10",
"@hcengineering/calendar": "^0.6.11",
"@hcengineering/calendar": "^0.6.12",
"@hcengineering/ui": "^0.6.10",
"@hcengineering/tags": "^0.6.11"
},

View File

@ -40,7 +40,7 @@
"@hcengineering/attachment": "^0.6.8",
"@hcengineering/ui": "^0.6.10",
"@hcengineering/presentation": "^0.6.2",
"@hcengineering/panel": "^0.6.9",
"@hcengineering/panel": "^0.6.10",
"@hcengineering/view": "^0.6.8",
"@hcengineering/view-resources": "^0.6.0",
"@hcengineering/task": "^0.6.10",

View File

@ -41,7 +41,7 @@
"@hcengineering/core": "^0.6.27",
"@hcengineering/chunter": "^0.6.9",
"@hcengineering/attachment": "^0.6.8",
"@hcengineering/panel": "^0.6.9",
"@hcengineering/panel": "^0.6.10",
"@hcengineering/view": "^0.6.8",
"@hcengineering/view-resources": "^0.6.0",
"@hcengineering/login": "^0.6.7",

View File

@ -46,7 +46,7 @@
"@hcengineering/notification-resources": "^0.6.0",
"@hcengineering/attachment": "^0.6.8",
"@hcengineering/attachment-resources": "^0.6.0",
"@hcengineering/panel": "^0.6.9",
"@hcengineering/panel": "^0.6.10",
"@hcengineering/templates": "^0.6.6"
}
}

View File

@ -43,7 +43,7 @@
"@hcengineering/presentation": "^0.6.2",
"@hcengineering/login": "^0.6.7",
"@hcengineering/setting": "^0.6.9",
"@hcengineering/calendar": "^0.6.11",
"@hcengineering/calendar": "^0.6.12",
"@hcengineering/tags": "^0.6.11",
"@hcengineering/task": "^0.6.10",
"@hcengineering/chunter": "^0.6.9",
@ -52,7 +52,7 @@
"@hcengineering/contact-resources": "^0.6.0",
"@hcengineering/view-resources": "^0.6.0",
"@hcengineering/text-editor": "^0.6.0",
"@hcengineering/panel": "^0.6.9",
"@hcengineering/panel": "^0.6.10",
"@hcengineering/kanban": "^0.6.0",
"@hcengineering/attachment-resources": "^0.6.0",
"@hcengineering/workbench": "^0.6.8",

View File

@ -36,7 +36,7 @@
"@hcengineering/platform": "^0.6.9",
"@hcengineering/contact": "^0.6.18",
"@hcengineering/model": "^0.6.6",
"@hcengineering/panel": "^0.6.9",
"@hcengineering/panel": "^0.6.10",
"@hcengineering/core": "^0.6.27",
"@hcengineering/view": "^0.6.8",
"@hcengineering/ui": "^0.6.10",

View File

@ -41,7 +41,7 @@
"@hcengineering/presentation": "^0.6.2",
"@hcengineering/login": "^0.6.7",
"@hcengineering/setting": "^0.6.9",
"@hcengineering/calendar": "^0.6.11",
"@hcengineering/calendar": "^0.6.12",
"@hcengineering/request": "^0.6.3",
"@hcengineering/notification": "^0.6.14",
"@hcengineering/notification-resources": "^0.6.0",

View File

@ -90,7 +90,7 @@
"@hcengineering/view-assets": "^0.6.6",
"@hcengineering/task-assets": "^0.6.9",
"@hcengineering/chunter-assets": "^0.6.7",
"@hcengineering/recruit-assets": "^0.6.9",
"@hcengineering/recruit-assets": "^0.6.10",
"@hcengineering/setting-assets": "^0.6.7",
"@hcengineering/contact-assets": "^0.6.7",
"@hcengineering/activity-assets": "^0.6.1",
@ -105,7 +105,7 @@
"@hcengineering/notification-assets": "^0.6.8",
"@hcengineering/preference-assets": "^0.6.0",
"@hcengineering/tags-assets": "^0.6.0",
"@hcengineering/calendar-assets": "^0.6.9",
"@hcengineering/calendar-assets": "^0.6.10",
"@hcengineering/tracker-assets": "^0.6.0",
"@hcengineering/board-assets": "^0.6.9",
"@hcengineering/hr-assets": "^0.6.9",
@ -116,7 +116,7 @@
"@hcengineering/view": "^0.6.8",
"@hcengineering/task": "^0.6.10",
"@hcengineering/chunter": "^0.6.9",
"@hcengineering/recruit": "^0.6.15",
"@hcengineering/recruit": "^0.6.16",
"@hcengineering/setting": "^0.6.9",
"@hcengineering/contact": "^0.6.18",
"@hcengineering/activity": "^0.6.0",
@ -131,12 +131,12 @@
"@hcengineering/notification": "^0.6.14",
"@hcengineering/preference": "^0.6.8",
"@hcengineering/tags": "^0.6.11",
"@hcengineering/calendar": "^0.6.11",
"@hcengineering/calendar": "^0.6.12",
"@hcengineering/tracker": "^0.6.10",
"@hcengineering/board": "^0.6.9",
"@hcengineering/hr": "^0.6.9",
"@hcengineering/document": "^0.6.0",
"@hcengineering/bitrix": "^0.6.38",
"@hcengineering/bitrix": "^0.6.39",
"@hcengineering/request": "^0.6.3"
}
}

View File

@ -28,7 +28,8 @@
"dependencies": {
"@hcengineering/core": "^0.6.27",
"@hcengineering/platform": "^0.6.9",
"@hcengineering/calendar": "^0.6.11",
"@hcengineering/calendar": "^0.6.12",
"@hcengineering/contact": "^0.6.18",
"@hcengineering/server-core": "^0.6.1",
"@hcengineering/server-notification-resources": "^0.6.0"
}

View File

@ -13,11 +13,26 @@
// limitations under the License.
//
import calendar, { Event } from '@hcengineering/calendar'
import { Class, Doc, DocumentQuery, FindOptions, FindResult, Hierarchy, Ref } from '@hcengineering/core'
import calendar, { Calendar, Event, ReccuringEvent } from '@hcengineering/calendar'
import core, {
Class,
Doc,
DocumentQuery,
FindOptions,
FindResult,
Hierarchy,
Ref,
Tx,
TxCUD,
TxCreateDoc,
TxProcessor,
TxRemoveDoc,
TxUpdateDoc
} from '@hcengineering/core'
import { getResource } from '@hcengineering/platform'
import { TriggerControl } from '@hcengineering/server-core'
import { getHTMLPresenter, getTextPresenter } from '@hcengineering/server-notification-resources'
import contact, { EmployeeAccount } from '@hcengineering/contact'
/**
* @public
@ -31,14 +46,14 @@ export async function FindReminders (
options?: FindOptions<T>
) => Promise<FindResult<T>>
): Promise<Doc[]> {
const result = await findAll(calendar.mixin.Reminder, { attachedTo: doc._id })
return result
const events = await findAll(calendar.class.Event, { attachedTo: doc._id })
return events
}
/**
* @public
*/
export async function EventHTMLPresenter (doc: Doc, control: TriggerControl): Promise<string | undefined> {
export async function ReminderHTMLPresenter (doc: Doc, control: TriggerControl): Promise<string | undefined> {
const event = doc as Event
const target = (await control.findAll(event.attachedToClass, { _id: event.attachedTo }, { limit: 1 }))[0]
if (target !== undefined) {
@ -52,7 +67,7 @@ export async function EventHTMLPresenter (doc: Doc, control: TriggerControl): Pr
/**
* @public
*/
export async function EventTextPresenter (doc: Doc, control: TriggerControl): Promise<string | undefined> {
export async function ReminderTextPresenter (doc: Doc, control: TriggerControl): Promise<string | undefined> {
const event = doc as Event
const target = (await control.findAll(event.attachedToClass, { _id: event.attachedTo }, { limit: 1 }))[0]
if (target !== undefined) {
@ -64,11 +79,154 @@ export async function EventTextPresenter (doc: Doc, control: TriggerControl): Pr
}
}
/**
* @public
*/
export async function OnEmployeeAccountCreate (tx: Tx, control: TriggerControl): Promise<Tx[]> {
const ctx = TxProcessor.extractTx(tx) as TxCreateDoc<EmployeeAccount>
const user = TxProcessor.createDoc2Doc(ctx)
const res: TxCreateDoc<Calendar> = control.txFactory.createTxCreateDoc(
calendar.class.Calendar,
core.space.Space,
{
name: user.email,
description: '',
archived: false,
private: false,
members: [user._id]
},
`${user._id}_calendar` as Ref<Calendar>,
undefined,
user._id
)
return [res]
}
async function onEventCreate (tx: Tx, control: TriggerControl): Promise<Tx[]> {
const ctx = TxProcessor.extractTx(tx) as TxCreateDoc<Event>
const ev = TxProcessor.createDoc2Doc(ctx)
const res: Tx[] = []
const accounts = await control.modelDb.findAll(contact.class.EmployeeAccount, {})
const participants = accounts.filter(
(p) => (p._id !== ev.createdBy ?? ev.modifiedBy) && ev.participants.includes(p.employee)
)
for (const acc of participants) {
const innerTx = control.txFactory.createTxCreateDoc(ev._class, `${acc._id}_calendar` as Ref<Calendar>, {
eventId: ev.eventId,
participants: ev.participants,
externalParticipants: ev.externalParticipants,
title: ev.title,
description: ev.description,
allDay: ev.allDay,
attachedTo: ev.attachedTo,
attachedToClass: ev.attachedToClass,
collection: ev.collection,
date: ev.date,
dueDate: ev.dueDate,
reminders: ev.reminders,
location: ev.location,
access: 'reader'
})
res.push(
control.txFactory.createTxCollectionCUD(ev.attachedToClass, ev.attachedTo, ev.space, ev.collection, innerTx)
)
}
return res
}
async function onEventUpdate (tx: Tx, control: TriggerControl): Promise<Tx[]> {
const ctx = TxProcessor.extractTx(tx) as TxUpdateDoc<Event>
const ev = (await control.findAll(calendar.class.Event, { _id: ctx.objectId }))[0]
const res: Tx[] = []
if (ev !== undefined) {
const events = await control.findAll(calendar.class.Event, { eventId: ev.eventId, _id: { $ne: ev._id } })
for (const event of events) {
const innerTx = control.txFactory.createTxUpdateDoc(event._class, event.space, event._id, ctx.operations)
res.push(
control.txFactory.createTxCollectionCUD(
event.attachedToClass,
event.attachedTo,
event.space,
event.collection,
innerTx
)
)
}
}
return res
}
async function onEventRemove (tx: Tx, control: TriggerControl): Promise<Tx[]> {
const ctx = TxProcessor.extractTx(tx) as TxRemoveDoc<Event>
const event = control.removedMap.get(ctx.objectId)
const res: Tx[] = []
if (event !== undefined) {
const events = await control.findAll(calendar.class.Event, { eventId: (event as Event).eventId })
for (const event of events) {
const innerTx = control.txFactory.createTxRemoveDoc(event._class, event.space, event._id)
res.push(
control.txFactory.createTxCollectionCUD(
event.attachedToClass,
event.attachedTo,
event.space,
event.collection,
innerTx
)
)
}
if (event._class === calendar.class.ReccuringEvent) {
const childs = await control.findAll(calendar.class.ReccuringInstance, {
recurringEventId: (event as ReccuringEvent).eventId
})
for (const child of childs) {
const innerTx = control.txFactory.createTxRemoveDoc(child._class, child.space, child._id)
res.push(
control.txFactory.createTxCollectionCUD(
child.attachedToClass,
child.attachedTo,
child.space,
child.collection,
innerTx
)
)
}
}
}
return res
}
/**
* @public
*/
export async function OnEvent (tx: Tx, control: TriggerControl): Promise<Tx[]> {
if (tx.space === core.space.DerivedTx) return []
const ctx = TxProcessor.extractTx(tx) as TxCUD<Event>
if (!control.hierarchy.isDerived(ctx.objectClass, calendar.class.Event)) return []
switch (ctx._class) {
case core.class.TxCreateDoc:
return await onEventCreate(ctx, control)
case core.class.TxUpdateDoc:
return await onEventUpdate(ctx, control)
case core.class.TxRemoveDoc:
return await onEventRemove(ctx, control)
}
return []
}
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
export default async () => ({
function: {
EventHTMLPresenter,
EventTextPresenter,
ReminderHTMLPresenter,
ReminderTextPresenter,
FindReminders
},
trigger: {
OnEvent,
OnEmployeeAccountCreate
}
})

View File

@ -28,6 +28,7 @@
},
"dependencies": {
"@hcengineering/core": "^0.6.27",
"@hcengineering/server-core": "^0.6.1",
"@hcengineering/server-notification": "^0.6.1",
"@hcengineering/platform": "^0.6.9"
}

View File

@ -18,6 +18,7 @@ import { Class, Doc, DocumentQuery, FindOptions, FindResult, Hierarchy, Ref } fr
import type { Plugin, Resource } from '@hcengineering/platform'
import { plugin } from '@hcengineering/platform'
import { Presenter } from '@hcengineering/server-notification'
import type { TriggerFunc } from '@hcengineering/server-core'
/**
* @public
@ -29,8 +30,8 @@ export const serverCalendarId = 'server-calendar' as Plugin
*/
export default plugin(serverCalendarId, {
function: {
EventHTMLPresenter: '' as Resource<Presenter>,
EventTextPresenter: '' as Resource<Presenter>,
ReminderHTMLPresenter: '' as Resource<Presenter>,
ReminderTextPresenter: '' as Resource<Presenter>,
FindReminders: '' as Resource<
(
doc: Doc,
@ -42,5 +43,9 @@ export default plugin(serverCalendarId, {
) => Promise<FindResult<T>>
) => Promise<Doc[]>
>
},
trigger: {
OnEvent: '' as Resource<TriggerFunc>,
OnEmployeeAccountCreate: '' as Resource<TriggerFunc>
}
})

View File

@ -33,7 +33,7 @@
"@hcengineering/server-core": "^0.6.1",
"@hcengineering/server": "^0.6.4",
"@hcengineering/chunter": "^0.6.9",
"@hcengineering/recruit": "^0.6.15",
"@hcengineering/recruit": "^0.6.16",
"got": "^11.8.3",
"fast-equals": "^2.0.3",
"html-to-text": "^9.0.3"

View File

@ -29,7 +29,7 @@
"@hcengineering/core": "^0.6.27",
"@hcengineering/platform": "^0.6.9",
"@hcengineering/server-core": "^0.6.1",
"@hcengineering/recruit": "^0.6.15",
"@hcengineering/recruit": "^0.6.16",
"@hcengineering/view": "^0.6.8",
"@hcengineering/login": "^0.6.7",
"@hcengineering/workbench": "^0.6.8",

View File

@ -26,7 +26,8 @@ import core, {
Ref,
ServerStorage,
Tx,
TxCUD
TxCUD,
systemAccountEmail
} from '@hcengineering/core'
import platform, { PlatformError, Severity, Status } from '@hcengineering/platform'
import { BroadcastFunc, Middleware, SessionContext, TxMiddlewareResult } from '@hcengineering/server-core'
@ -63,7 +64,7 @@ export class PrivateMiddleware extends BaseMiddleware implements Middleware {
if (account !== tx.modifiedBy && account !== core.account.System) {
throw new PlatformError(new Status(Severity.ERROR, platform.status.Forbidden, {}))
}
target = [ctx.userEmail]
target = [ctx.userEmail, systemAccountEmail]
}
}
const res = await this.provideTx(ctx, tx)

View File

@ -30,6 +30,7 @@ import core, {
Ref,
ServerStorage,
Space,
systemAccountEmail,
Tx,
TxCreateDoc,
TxCUD,
@ -265,10 +266,11 @@ export class SpaceSecurityMiddleware extends BaseMiddleware implements Middlewar
}
}
async getTargets (accounts: Ref<Account>[] | undefined): Promise<string[] | undefined> {
if (accounts === undefined) return
async getTargets (accounts: Ref<Account>[]): Promise<string[]> {
const users = await this.storage.modelDb.findAll(core.class.Account, { _id: { $in: accounts } })
return users.map((p) => p.email)
const res = users.map((p) => p.email)
res.push(systemAccountEmail)
return res
}
private async getTxTargets (ctx: SessionContext, tx: Tx): Promise<string[] | undefined> {
@ -278,7 +280,7 @@ export class SpaceSecurityMiddleware extends BaseMiddleware implements Middlewar
if (h.isDerived(tx._class, core.class.TxCUD)) {
const account = await getUser(this.storage, ctx)
if (tx.objectSpace === (account._id as string)) {
targets = [account.email]
targets = [account.email, systemAccountEmail]
} else {
const space = this.privateSpaces[tx.objectSpace]
if (space !== undefined) {
@ -290,12 +292,8 @@ export class SpaceSecurityMiddleware extends BaseMiddleware implements Middlewar
if (allowed === undefined || !allowed.includes(isSpace ? (cudTx.objectId as Ref<Space>) : tx.objectSpace)) {
throw new PlatformError(new Status(Severity.ERROR, platform.status.Forbidden, {}))
}
} else {
if (targets === undefined) {
targets = [account.email]
} else if (!targets.includes(account.email)) {
targets.push(account.email)
}
} else if (!targets.includes(account.email)) {
targets.push(account.email)
}
}
}