mirror of
https://github.com/hcengineering/platform.git
synced 2025-05-28 10:57:36 +00:00
UBER-676 UBER-716 UBER-717 UBER-719 (#3582)
Signed-off-by: Denis Bykhov <bykhov.denis@gmail.com>
This commit is contained in:
parent
de03ecb895
commit
ccaf66f179
@ -118,7 +118,7 @@ export class TReccuringEvent extends TEvent implements ReccuringEvent {
|
||||
|
||||
@Model(calendar.class.ReccuringInstance, calendar.class.Event)
|
||||
@UX(calendar.string.Event, calendar.icon.Calendar)
|
||||
export class TReccuringInstance extends TEvent implements ReccuringInstance {
|
||||
export class TReccuringInstance extends TReccuringEvent implements ReccuringInstance {
|
||||
recurringEventId!: Ref<ReccuringEvent>
|
||||
originalStartTime!: number
|
||||
isCancelled?: boolean
|
||||
@ -288,10 +288,6 @@ export function createModel (builder: Builder): void {
|
||||
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
|
||||
})
|
||||
|
@ -17,7 +17,7 @@
|
||||
import type { AnySvelteComponent, ButtonSize } from '../types'
|
||||
import Icon from './Icon.svelte'
|
||||
|
||||
export let icon: Asset | AnySvelteComponent | undefined
|
||||
export let icon: Asset | AnySvelteComponent | undefined = undefined
|
||||
export let size: ButtonSize = 'large'
|
||||
export let ghost: boolean = false
|
||||
export let selected: boolean = false
|
||||
@ -35,13 +35,13 @@
|
||||
on:click|stopPropagation
|
||||
on:mousemove
|
||||
>
|
||||
<div class="content">
|
||||
{#if $$slots.content}
|
||||
<slot name="content" />
|
||||
{:else if icon}
|
||||
{#if $$slots.content}
|
||||
<slot name="content" />
|
||||
{:else if icon}
|
||||
<div class="content">
|
||||
<Icon {icon} size={'full'} />
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
|
@ -53,7 +53,7 @@
|
||||
function openPopup () {
|
||||
if (!opened) {
|
||||
opened = true
|
||||
showPopup(DropdownLabelsPopupIntl, { items, selected }, container, (result) => {
|
||||
showPopup(DropdownLabelsPopupIntl, { items, selected, params }, container, (result) => {
|
||||
if (result) {
|
||||
selected = result
|
||||
dispatch('selected', result)
|
||||
@ -87,7 +87,10 @@
|
||||
on:click={openPopup}
|
||||
>
|
||||
<span slot="content" class="overflow-label disabled flex-grow text-left mr-2">
|
||||
<Label label={selectedItem ? selectedItem.label : label} params={selectedItem ? selectedItem.params : params} />
|
||||
<Label
|
||||
label={selectedItem ? selectedItem.label : label}
|
||||
params={selectedItem ? selectedItem.params ?? params : params}
|
||||
/>
|
||||
</span>
|
||||
<svelte:fragment slot="iconRight">
|
||||
<DropdownIcon size={'small'} fill={'var(--theme-dark-color)'} />
|
||||
|
@ -21,6 +21,7 @@
|
||||
|
||||
export let items: DropdownIntlItem[]
|
||||
export let selected: DropdownIntlItem['id'] | undefined = undefined
|
||||
export let params: Record<string, any> = {}
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
const btns: HTMLButtonElement[] = []
|
||||
@ -50,7 +51,7 @@
|
||||
dispatch('close', item.id)
|
||||
}}
|
||||
>
|
||||
<div class="flex-grow caption-color nowrap"><Label label={item.label} params={item.params} /></div>
|
||||
<div class="flex-grow caption-color nowrap"><Label label={item.label} params={item.params ?? params} /></div>
|
||||
<div class="check">
|
||||
{#if item.id === selected}<IconCheck size={'small'} />{/if}
|
||||
</div>
|
||||
|
195
packages/ui/src/components/NumberInput.svelte
Normal file
195
packages/ui/src/components/NumberInput.svelte
Normal file
@ -0,0 +1,195 @@
|
||||
<!--
|
||||
// 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 type { IntlString } from '@hcengineering/platform'
|
||||
import { translate } from '@hcengineering/platform'
|
||||
import { themeStore } from '@hcengineering/theme'
|
||||
import { afterUpdate, createEventDispatcher, onMount } from 'svelte'
|
||||
import { registerFocus } from '../focus'
|
||||
import plugin from '../plugin'
|
||||
import { resizeObserver } from '../resize'
|
||||
import { floorFractionDigits } from '../utils'
|
||||
import DownOutline from './icons/DownOutline.svelte'
|
||||
import UpOutline from './icons/UpOutline.svelte'
|
||||
|
||||
export let maxWidth: string | undefined = undefined
|
||||
export let value: number = 0
|
||||
export let minValue: number | undefined = undefined
|
||||
export let placeholder: IntlString = plugin.string.EditBoxPlaceholder
|
||||
export let placeholderParam: any | undefined = undefined
|
||||
export let maxDigitsAfterPoint: number | undefined = undefined
|
||||
export let disabled: boolean = false
|
||||
export let autoFocus: boolean = false
|
||||
export let select: boolean = false
|
||||
export let focusable: boolean = false
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
let text: HTMLElement
|
||||
let input: HTMLInputElement
|
||||
let style: string
|
||||
let phTraslate: string = ''
|
||||
let parentWidth: number | undefined
|
||||
|
||||
$: {
|
||||
if (
|
||||
maxDigitsAfterPoint !== undefined &&
|
||||
value &&
|
||||
!value.toString().match(`^\\d+\\.?\\d{0,${maxDigitsAfterPoint}}$`)
|
||||
) {
|
||||
value = floorFractionDigits(Number(value), maxDigitsAfterPoint)
|
||||
}
|
||||
}
|
||||
|
||||
$: {
|
||||
if (minValue !== undefined && value < minValue) value = minValue
|
||||
}
|
||||
$: style = `max-width: ${maxWidth || (parentWidth ? `${parentWidth}px` : 'max-content')};`
|
||||
$: translate(placeholder, placeholderParam ?? {}, $themeStore.language).then((res) => {
|
||||
phTraslate = res
|
||||
})
|
||||
|
||||
function computeSize (t: HTMLInputElement | EventTarget | null) {
|
||||
if (t == null) {
|
||||
return
|
||||
}
|
||||
const target = t as HTMLInputElement
|
||||
const value = target.value
|
||||
text.innerHTML = (value === '' ? phTraslate : value)
|
||||
.replaceAll(' ', ' ')
|
||||
.replaceAll('<', '<')
|
||||
.replaceAll('>', '>')
|
||||
target.style.width = maxWidth ?? '5rem'
|
||||
dispatch('input')
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
if (autoFocus) {
|
||||
input.focus()
|
||||
autoFocus = false
|
||||
}
|
||||
if (select) {
|
||||
input.select()
|
||||
select = false
|
||||
}
|
||||
computeSize(input)
|
||||
})
|
||||
|
||||
afterUpdate(() => {
|
||||
computeSize(input)
|
||||
})
|
||||
|
||||
export function focusInput () {
|
||||
input?.focus()
|
||||
}
|
||||
export function selectInput () {
|
||||
input?.select()
|
||||
}
|
||||
|
||||
// Focusable control with index
|
||||
export let focusIndex = -1
|
||||
const { idx, focusManager } = registerFocus(focusIndex, {
|
||||
focus: () => {
|
||||
focusInput()
|
||||
return input != null
|
||||
},
|
||||
isFocus: () => document.activeElement === input
|
||||
})
|
||||
const updateFocus = () => {
|
||||
focusManager?.setFocus(idx)
|
||||
}
|
||||
$: if (input) {
|
||||
input.addEventListener('focus', updateFocus, { once: true })
|
||||
}
|
||||
|
||||
export function focus (): void {
|
||||
input.focus()
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<div
|
||||
class="editbox-container"
|
||||
on:click={() => {
|
||||
input.focus()
|
||||
}}
|
||||
use:resizeObserver={(element) => {
|
||||
parentWidth = element.parentElement?.getBoundingClientRect().width
|
||||
}}
|
||||
>
|
||||
<div class="hidden-text" bind:this={text} />
|
||||
<div class="flex-row-center clear-mins" class:focusable>
|
||||
<input
|
||||
{disabled}
|
||||
bind:this={input}
|
||||
type="number"
|
||||
class="number"
|
||||
bind:value
|
||||
placeholder={phTraslate}
|
||||
{style}
|
||||
on:input={(ev) => ev.target && computeSize(ev.target)}
|
||||
on:change
|
||||
on:keydown
|
||||
on:keypress
|
||||
on:blur
|
||||
/>
|
||||
<div class="flex-col-center">
|
||||
<div on:click={() => value++}>
|
||||
<UpOutline size="small" />
|
||||
</div>
|
||||
<div on:click={() => value--}>
|
||||
<DownOutline size="small" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.editbox-container {
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
border-radius: 0.375rem;
|
||||
border: 1px solid var(--theme-table-border-color);
|
||||
padding: 0rem 0.5rem;
|
||||
|
||||
input {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
min-width: 0;
|
||||
color: var(--theme-caption-color);
|
||||
|
||||
&::-webkit-contacts-auto-fill-button,
|
||||
&::-webkit-credentials-auto-fill-button {
|
||||
visibility: hidden;
|
||||
display: none !important;
|
||||
pointer-events: none;
|
||||
height: 0;
|
||||
width: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
&.number::-webkit-outer-spin-button,
|
||||
&.number::-webkit-inner-spin-button {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
}
|
||||
|
||||
input[type='number'] {
|
||||
-moz-appearance: textfield;
|
||||
}
|
||||
}
|
||||
</style>
|
41
packages/ui/src/components/calendar/SimpleTimePopup.svelte
Normal file
41
packages/ui/src/components/calendar/SimpleTimePopup.svelte
Normal file
@ -0,0 +1,41 @@
|
||||
<!--
|
||||
// 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 { createEventDispatcher } from 'svelte'
|
||||
import TimeInputBox from './TimeInputBox.svelte'
|
||||
|
||||
export let currentDate: Date
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
function close (): void {
|
||||
currentDate.setSeconds(0, 0)
|
||||
dispatch('close', { value: currentDate })
|
||||
}
|
||||
|
||||
export function canClose (): boolean {
|
||||
return true
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="antiPopup popup">
|
||||
<TimeInputBox bind:currentDate on:close={close} />
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.popup {
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
</style>
|
275
packages/ui/src/components/calendar/TimeInputBox.svelte
Normal file
275
packages/ui/src/components/calendar/TimeInputBox.svelte
Normal file
@ -0,0 +1,275 @@
|
||||
<!--
|
||||
// Copyright © 2023 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { afterUpdate, createEventDispatcher } from 'svelte'
|
||||
import ui from '../../plugin'
|
||||
import Label from '../Label.svelte'
|
||||
|
||||
export let currentDate: Date
|
||||
|
||||
type TEdits = 'hour' | 'min'
|
||||
interface IEdits {
|
||||
id: TEdits
|
||||
value: number
|
||||
el?: HTMLElement
|
||||
}
|
||||
const editsType: TEdits[] = ['hour', 'min']
|
||||
const getIndex = (id: TEdits): number => editsType.indexOf(id)
|
||||
let edits: IEdits[] = editsType.map((edit) => {
|
||||
return { id: edit, value: -1 }
|
||||
})
|
||||
let selected: TEdits | null = 'hour'
|
||||
let startTyping: boolean = false
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
const setValue = (val: number, date: Date | null, id: TEdits): Date => {
|
||||
if (date == null) date = new Date()
|
||||
switch (id) {
|
||||
case 'hour':
|
||||
date.setHours(val)
|
||||
break
|
||||
case 'min':
|
||||
date.setMinutes(val)
|
||||
break
|
||||
}
|
||||
return date
|
||||
}
|
||||
|
||||
const getMaxValue = (date: Date | null, id: TEdits): number => {
|
||||
if (date == null) date = new Date()
|
||||
switch (id) {
|
||||
case 'hour':
|
||||
return 23
|
||||
case 'min':
|
||||
return 59
|
||||
}
|
||||
}
|
||||
|
||||
const getValue = (date: Date, id: TEdits): number => {
|
||||
switch (id) {
|
||||
case 'hour':
|
||||
return date.getHours()
|
||||
case 'min':
|
||||
return date.getMinutes()
|
||||
}
|
||||
}
|
||||
|
||||
const dateToEdits = (currentDate: Date | null): void => {
|
||||
if (currentDate == null) {
|
||||
edits.forEach((edit) => {
|
||||
edit.value = -1
|
||||
})
|
||||
} else {
|
||||
for (const edit of edits) {
|
||||
edit.value = getValue(currentDate, edit.id)
|
||||
}
|
||||
}
|
||||
edits = edits
|
||||
}
|
||||
|
||||
export const isNull = (currentDate?: Date): boolean => {
|
||||
if (currentDate !== undefined) {
|
||||
dateToEdits(currentDate)
|
||||
}
|
||||
let result: boolean = false
|
||||
edits.forEach((edit, i) => {
|
||||
if (edit.value === -1) result = true
|
||||
})
|
||||
return result
|
||||
}
|
||||
|
||||
const keyDown = (ev: KeyboardEvent, ed: TEdits): void => {
|
||||
if (selected === ed) {
|
||||
const index = getIndex(ed)
|
||||
if (ev.key >= '0' && ev.key <= '9') {
|
||||
const shouldNext = !startTyping
|
||||
const num: number = parseInt(ev.key, 10)
|
||||
if (startTyping) {
|
||||
if (num === 0) edits[index].value = 0
|
||||
else {
|
||||
edits[index].value = num
|
||||
}
|
||||
startTyping = false
|
||||
} else if (edits[index].value * 10 + num > getMaxValue(currentDate, ed)) {
|
||||
edits[index].value = getMaxValue(currentDate, ed)
|
||||
} else {
|
||||
edits[index].value = edits[index].value * 10 + num
|
||||
}
|
||||
if (!isNull() && !startTyping) {
|
||||
fixEdits()
|
||||
currentDate = setValue(edits[index].value, currentDate, ed)
|
||||
dateToEdits(currentDate)
|
||||
}
|
||||
edits = edits
|
||||
|
||||
if (selected === 'hour' && (shouldNext || edits[0].value > 2)) selected = 'min'
|
||||
}
|
||||
if (ev.code === 'Enter') {
|
||||
dispatch('close', currentDate)
|
||||
}
|
||||
if (ev.code === 'Backspace') {
|
||||
edits[index].value = -1
|
||||
startTyping = true
|
||||
}
|
||||
if (ev.code === 'ArrowUp' || (ev.code === 'ArrowDown' && edits[index].el)) {
|
||||
if (edits[index].value !== -1) {
|
||||
const val = ev.code === 'ArrowUp' ? edits[index].value + 1 : edits[index].value - 1
|
||||
if (currentDate) {
|
||||
currentDate = setValue(val, currentDate, ed)
|
||||
dateToEdits(currentDate)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (ev.code === 'ArrowLeft' && edits[index].el) {
|
||||
selected = index === 0 ? edits[1].id : edits[index - 1].id
|
||||
}
|
||||
if (ev.code === 'ArrowRight' && edits[index].el) {
|
||||
selected = index === 4 ? edits[0].id : edits[index + 1].id
|
||||
}
|
||||
if (ev.code === 'Tab') {
|
||||
if (ed === 'min') dispatch('save')
|
||||
}
|
||||
}
|
||||
}
|
||||
const focused = (ed: TEdits): void => {
|
||||
selected = ed
|
||||
startTyping = true
|
||||
}
|
||||
const fixEdits = (): void => {
|
||||
const h: number = edits[0].value === -1 ? 0 : edits[0].value
|
||||
const m: number = edits[1].value === -1 ? 0 : edits[1].value
|
||||
currentDate = new Date(currentDate.getFullYear(), currentDate.getMonth(), currentDate.getDate(), h, m)
|
||||
dispatch('save')
|
||||
}
|
||||
|
||||
$: dateToEdits(currentDate)
|
||||
$: if (selected && edits[getIndex(selected)].el) edits[getIndex(selected)].el?.focus()
|
||||
|
||||
afterUpdate(() => {
|
||||
if (selected) edits[getIndex(selected)].el?.focus()
|
||||
})
|
||||
</script>
|
||||
|
||||
<div class="datetime-input">
|
||||
<div class="flex-row-center">
|
||||
<span
|
||||
bind:this={edits[0].el}
|
||||
class="digit"
|
||||
tabindex="0"
|
||||
on:keydown={(ev) => keyDown(ev, edits[0].id)}
|
||||
on:focus={() => focused(edits[0].id)}
|
||||
on:blur={() => (selected = null)}
|
||||
>
|
||||
{#if edits[0].value > -1}
|
||||
{edits[0].value.toString().padStart(2, '0')}
|
||||
{:else}<Label label={ui.string.HH} />{/if}
|
||||
</span>
|
||||
<span class="separator">:</span>
|
||||
<span
|
||||
bind:this={edits[1].el}
|
||||
class="digit"
|
||||
tabindex="0"
|
||||
on:keydown={(ev) => keyDown(ev, edits[1].id)}
|
||||
on:focus={() => focused(edits[1].id)}
|
||||
on:blur={() => (selected = null)}
|
||||
>
|
||||
{#if edits[1].value > -1}
|
||||
{edits[1].value.toString().padStart(2, '0')}
|
||||
{:else}<Label label={ui.string.MM} />{/if}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.datetime-input {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
margin: 0;
|
||||
padding: 0.75rem;
|
||||
height: 3rem;
|
||||
font-family: inherit;
|
||||
font-size: 1rem;
|
||||
color: var(--theme-content-color);
|
||||
background-color: var(--theme-bg-color);
|
||||
border: 1px solid var(--theme-button-border);
|
||||
border-radius: 0.25rem;
|
||||
transition: border-color 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
border-color: var(--theme-button-default);
|
||||
}
|
||||
&:focus-within {
|
||||
color: var(--theme-caption-color);
|
||||
border-color: var(--primary-edit-border-color);
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin: 0 0.25rem;
|
||||
width: 0.75rem;
|
||||
height: 0.75rem;
|
||||
color: var(--theme-content-color);
|
||||
background-color: var(--theme-button-default);
|
||||
outline: none;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: var(--theme-caption-color);
|
||||
background-color: var(--theme-button-hovered);
|
||||
}
|
||||
}
|
||||
|
||||
.digit {
|
||||
position: relative;
|
||||
padding: 0 0.125rem;
|
||||
height: 1.5rem;
|
||||
line-height: 1.5rem;
|
||||
color: var(--theme-caption-color);
|
||||
outline: none;
|
||||
border-radius: 0.125rem;
|
||||
|
||||
&:focus {
|
||||
color: var(--accented-button-color);
|
||||
background-color: var(--accented-button-default);
|
||||
}
|
||||
&::after {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 11000;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
.time-divider {
|
||||
flex-shrink: 0;
|
||||
margin: 0 0.25rem;
|
||||
width: 1px;
|
||||
min-width: 1px;
|
||||
height: 0.75rem;
|
||||
background-color: var(--theme-button-border);
|
||||
}
|
||||
.separator {
|
||||
margin: 0 0.1rem;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -190,6 +190,8 @@ export { default as StepsDialog } from './components/StepsDialog.svelte'
|
||||
export { default as EmojiPopup } from './components/EmojiPopup.svelte'
|
||||
export { default as IconWithEmoji } from './components/IconWithEmoji.svelte'
|
||||
export { default as ModeSelector } from './components/ModeSelector.svelte'
|
||||
export { default as SimpleTimePopup } from './components/calendar/SimpleTimePopup.svelte'
|
||||
export { default as NumberInput } from './components/NumberInput.svelte'
|
||||
|
||||
export * from './types'
|
||||
export * from './location'
|
||||
|
@ -15,4 +15,26 @@
|
||||
<symbol id="notifications" viewBox="0 0 32 32">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M17 3C17 2.44772 16.5523 2 16 2C15.4477 2 15 2.44772 15 3V4.05493C10.5 4.55237 7 8.36745 7 13V14.8953C7 16.0307 6.6136 17.1322 5.90434 18.0188L4.18007 20.1741C3.41618 21.129 3 22.3154 3 23.5383C3 24.8978 4.10216 26 5.46174 26H12C12 26.5253 12.1035 27.0454 12.3045 27.5307C12.5055 28.016 12.8001 28.457 13.1716 28.8284C13.543 29.1999 13.984 29.4945 14.4693 29.6955C14.9546 29.8965 15.4747 30 16 30C16.5253 30 17.0454 29.8965 17.5307 29.6955C18.016 29.4945 18.457 29.1999 18.8284 28.8284C19.1999 28.457 19.4945 28.016 19.6955 27.5307C19.8965 27.0454 20 26.5253 20 26H26.5383C27.8978 26 29 24.8978 29 23.5383C29 22.3154 28.5838 21.129 27.8199 20.1741L26.0957 18.0188C25.3864 17.1322 25 16.0307 25 14.8953V13C25 8.36745 21.5 4.55237 17 4.05493V3ZM18 26H14C14 26.2626 14.0517 26.5227 14.1522 26.7654C14.2528 27.008 14.4001 27.2285 14.5858 27.4142C14.7715 27.5999 14.992 27.7472 15.2346 27.8478C15.4773 27.9483 15.7374 28 16 28C16.2626 28 16.5227 27.9483 16.7654 27.8478C17.008 27.7472 17.2285 27.5999 17.4142 27.4142C17.5999 27.2285 17.7472 27.008 17.8478 26.7654C17.9483 26.5227 18 26.2626 18 26ZM5 23.5383C5 23.7933 5.20673 24 5.46174 24H26.5383C26.7933 24 27 23.7933 27 23.5383C27 22.7696 26.7384 22.0238 26.2582 21.4235L24.5339 19.2682C23.541 18.027 23 16.4848 23 14.8953V13C23 9.13401 19.866 6 16 6C12.134 6 9 9.13401 9 13V14.8953C9 16.4848 8.45903 18.027 7.46608 19.2682L5.74181 21.4235C5.26161 22.0238 5 22.7696 5 23.5383Z" />
|
||||
</symbol>
|
||||
<symbol id="watch" viewBox="0 0 16 16">
|
||||
<path d="M8 4C8 3.72386 7.77614 3.5 7.5 3.5C7.22386 3.5 7 3.72386 7 4V8.5C7 8.77614 7.22386 9 7.5 9H10.5C10.7761 9 11 8.77614 11 8.5C11 8.22386 10.7761 8 10.5 8L8 8V4Z" />
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M15 8C15 11.866 11.866 15 8 15C4.13401 15 1 11.866 1 8C1 4.13401 4.13401 1 8 1C11.866 1 15 4.13401 15 8ZM14 8C14 11.3137 11.3137 14 8 14C4.68629 14 2 11.3137 2 8C2 4.68629 4.68629 2 8 2C11.3137 2 14 4.68629 14 8Z" />
|
||||
</symbol>
|
||||
<symbol id="description" viewBox="0 0 16 16">
|
||||
<path d="M2 4.5C2 4.22386 2.22386 4 2.5 4H13.5C13.7761 4 14 4.22386 14 4.5C14 4.77614 13.7761 5 13.5 5H2.5C2.22386 5 2 4.77614 2 4.5Z" fill-opacity="0.6"/>
|
||||
<path d="M2 8C2 7.72386 2.22386 7.5 2.5 7.5H9.5C9.77614 7.5 10 7.72386 10 8C10 8.27614 9.77614 8.5 9.5 8.5H2.5C2.22386 8.5 2 8.27614 2 8Z" fill-opacity="0.6"/>
|
||||
<path d="M2 11.5C2 11.2239 2.22386 11 2.5 11H11.5C11.7761 11 12 11.2239 12 11.5C12 11.7761 11.7761 12 11.5 12H2.5C2.22386 12 2 11.7761 2 11.5Z" fill-opacity="0.6"/>
|
||||
</symbol>
|
||||
<symbol id="participants" viewBox="0 0 16 16">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M2 5.5C2 3.567 3.567 2 5.5 2C7.433 2 9 3.567 9 5.5C9 7.433 7.433 9 5.5 9C3.567 9 2 7.433 2 5.5ZM5.5 3C4.11929 3 3 4.11929 3 5.5C3 6.88071 4.11929 8 5.5 8C6.88071 8 8 6.88071 8 5.5C8 4.11929 6.88071 3 5.5 3Z" fill-opacity="0.6"/>
|
||||
<path d="M10.5 10C10.2239 10 10 10.2239 10 10.5C10 10.7761 10.2239 11 10.5 11H11.5C12.8807 11 14 12.1193 14 13.5C14 13.7761 14.2239 14 14.5 14C14.7761 14 15 13.7761 15 13.5C15 11.567 13.433 10 11.5 10H10.5Z" fill-opacity="0.6"/>
|
||||
<path d="M4.5 11C3.11929 11 2 12.1193 2 13.5C2 13.7761 1.77614 14 1.5 14C1.22386 14 1 13.7761 1 13.5C1 11.567 2.567 10 4.5 10H6.5C8.433 10 10 11.567 10 13.5C10 13.7761 9.77614 14 9.5 14C9.22386 14 9 13.7761 9 13.5C9 12.1193 7.88071 11 6.5 11H4.5Z" fill-opacity="0.6"/>
|
||||
<path d="M10.5 2.99988C10.2238 2.99988 9.99995 3.22374 9.99995 3.49988C9.99995 3.77602 10.2238 3.99988 10.5 3.99988C10.7846 3.99988 11.0661 4.06066 11.3254 4.17815C11.5847 4.29564 11.8159 4.46714 12.0036 4.68119C12.1913 4.89523 12.3312 5.14688 12.4138 5.41931C12.4965 5.69174 12.52 5.97868 12.4828 6.26093C12.4457 6.54318 12.3487 6.81425 12.1984 7.05601C12.048 7.29777 11.8478 7.50465 11.6111 7.66282C11.3744 7.82098 11.1066 7.92679 10.8257 7.97316C10.5449 8.01954 10.2573 8.00541 9.98232 7.93173C9.71558 7.86026 9.44141 8.01855 9.36994 8.28528C9.29847 8.55202 9.45677 8.82618 9.7235 8.89766C10.136 9.00818 10.5673 9.02937 10.9886 8.95981C11.41 8.89025 11.8116 8.73153 12.1667 8.49429C12.5217 8.25704 12.8221 7.94672 13.0476 7.58408C13.2731 7.22144 13.4186 6.81484 13.4743 6.39146C13.53 5.96807 13.4947 5.53767 13.3708 5.12902C13.2468 4.72038 13.037 4.3429 12.7555 4.02184C12.4739 3.70078 12.127 3.44353 11.7381 3.26729C11.3491 3.09105 10.927 2.99988 10.5 2.99988Z" fill-opacity="0.6"/>
|
||||
</symbol>
|
||||
<symbol id="repeat" viewBox="0 0 16 16">
|
||||
<path d="M10.0003 4.99986H12.6103C11.9636 4.00602 11.0131 3.24777 9.90035 2.83815C8.78765 2.42852 7.57236 2.38944 6.43564 2.72675C5.29892 3.06405 4.30164 3.75967 3.59247 4.70992C2.98332 5.52613 2.61475 6.49266 2.52293 7.50049C2.49788 7.7755 2.27614 7.99986 1.99998 7.99986C1.72404 7.99986 1.49802 7.77598 1.51892 7.50083C1.60709 6.33998 2.00581 5.22075 2.67752 4.26241C3.44506 3.16736 4.53166 2.33568 5.78917 1.88079C7.04668 1.4259 8.41389 1.36994 9.70437 1.72053C10.9948 2.07112 12.1458 2.8112 13.0003 3.83986V1.99986H14.0003V5.99986H10.0003V4.99986Z" fill-opacity="0.6"/>
|
||||
<path d="M6.00026 10.9999H3.39026C4.03693 11.9937 4.98746 12.7519 6.10016 13.1616C7.21287 13.5712 8.42816 13.6103 9.56487 13.273C10.7016 12.9357 11.6989 12.24 12.408 11.2898C13.0172 10.4736 13.3858 9.50706 13.4776 8.49923C13.5026 8.22421 13.7244 7.99986 14.0005 7.99986C14.2765 7.99986 14.5025 8.22373 14.4816 8.49888C14.3934 9.65973 13.9947 10.779 13.323 11.7373C12.5555 12.8324 11.4689 13.664 10.2113 14.1189C8.95384 14.5738 7.58663 14.6298 6.29615 14.2792C5.00567 13.9286 3.85472 13.1885 3.00026 12.1599V13.9999H2.00026V9.99986H6.00026V10.9999Z" fill-opacity="0.6"/>
|
||||
</symbol>
|
||||
<symbol id="globe" viewBox="0 0 16 16">
|
||||
<path d="M8 1C6.61553 1 5.26216 1.41054 4.11101 2.17971C2.95987 2.94888 2.06266 4.04213 1.53285 5.32122C1.00303 6.6003 0.86441 8.00776 1.13451 9.36563C1.4046 10.7235 2.07129 11.9708 3.05026 12.9497C4.02922 13.9287 5.2765 14.5954 6.63437 14.8655C7.99224 15.1356 9.3997 14.997 10.6788 14.4672C11.9579 13.9373 13.0511 13.0401 13.8203 11.889C14.5895 10.7378 15 9.38447 15 8C15 6.14348 14.2625 4.36301 12.9497 3.05025C11.637 1.7375 9.85652 1 8 1ZM14 7.5H11C10.9416 5.65854 10.4646 3.85458 9.605 2.225C10.7893 2.54895 11.8457 3.22842 12.6316 4.17171C13.4175 5.115 13.8952 6.27669 14 7.5ZM8 14C7.88846 14.0075 7.77654 14.0075 7.665 14C6.62915 12.3481 6.05426 10.4491 6 8.5H10C9.95026 10.4477 9.38058 12.3466 8.35 14C8.23348 14.0082 8.11653 14.0082 8 14ZM6 7.5C6.04975 5.55234 6.61942 3.65341 7.65 2C7.87264 1.97498 8.09737 1.97498 8.32 2C9.36114 3.6504 9.94124 5.54953 10 7.5H6ZM6.38 2.225C5.52565 3.85582 5.05373 5.65972 5 7.5H2C2.10485 6.27669 2.58247 5.115 3.3684 4.17171C4.15432 3.22842 5.21072 2.54895 6.395 2.225H6.38ZM2.025 8.5H5.025C5.07718 10.3399 5.54739 12.1438 6.4 13.775C5.21943 13.4476 4.16739 12.7666 3.38528 11.8236C2.60317 10.8806 2.12848 9.72076 2.025 8.5ZM9.605 13.775C10.4646 12.1454 10.9416 10.3415 11 8.5H14C13.8952 9.72331 13.4175 10.885 12.6316 11.8283C11.8457 12.7716 10.7893 13.4511 9.605 13.775Z" fill-opacity="0.6"/>
|
||||
</symbol>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 5.0 KiB After Width: | Height: | Size: 10 KiB |
@ -51,6 +51,28 @@
|
||||
"RemoveRecEvent": "Remove recurring event",
|
||||
"ThisEvent": "This event",
|
||||
"ThisAndNext": "This and following events",
|
||||
"AllEvents": "All events"
|
||||
"AllEvents": "All events",
|
||||
"NewEvent": "New event",
|
||||
"TimeZone": "Time zone",
|
||||
"Repeat": "Repeat",
|
||||
"On": "On",
|
||||
"Every": "Every",
|
||||
"After": "After",
|
||||
"Ends": "Ends",
|
||||
"Never": "Never",
|
||||
"Day": "Day",
|
||||
"Week": "Week",
|
||||
"Month": "Month",
|
||||
"Year": "Year",
|
||||
"MondayShort": "Mo",
|
||||
"TuesdayShort": "Tu",
|
||||
"WednesdayShort": "We",
|
||||
"ThursdayShort": "Th",
|
||||
"FridayShort": "Fr",
|
||||
"SaturdayShort": "Sa",
|
||||
"SundayShort": "Su",
|
||||
"OnUntil": "On",
|
||||
"Times": "{count, plural, one {time} other {times}}",
|
||||
"AddParticipants": "Add participants"
|
||||
}
|
||||
}
|
@ -51,6 +51,29 @@
|
||||
"RemoveRecEvent": "Удаление повторяющегося события",
|
||||
"ThisEvent": "Только это событие",
|
||||
"ThisAndNext": "Это и последующие события",
|
||||
"AllEvents": "Все события"
|
||||
"AllEvents": "Все события",
|
||||
"NewEvent": "Новое событие",
|
||||
"TimeZone": "Часовой пояс",
|
||||
"Repeat": "Повтор",
|
||||
"On": "В",
|
||||
"Every": "Кажд.",
|
||||
"After": "После",
|
||||
"Ends": "Окончание",
|
||||
"Never": "Никогда",
|
||||
"Day": "{count, plural, one {день} few {дня} other {дней}}",
|
||||
"Week": "{count, plural, one {неделю} few {недели} other {недель}}",
|
||||
"Month": "{count, plural, one {месяц} few {месяца} other {месяцев}}",
|
||||
"Year": "{count, plural, one {год} few {года} other {лет}}",
|
||||
"MondayShort": "Пн",
|
||||
"TuesdayShort": "Вт",
|
||||
"WednesdayShort": "Ср",
|
||||
"ThursdayShort": "Чт",
|
||||
"FridayShort": "Пт",
|
||||
"SaturdayShort": "Сб",
|
||||
"SundayShort": "Вс",
|
||||
"OnUntil": "До",
|
||||
"Times": "{count, plural, one {раз} few {раза} other {раз}}",
|
||||
"AddParticipants": "Добавить участников"
|
||||
|
||||
}
|
||||
}
|
@ -21,7 +21,12 @@ loadMetadata(calendar.icon, {
|
||||
Calendar: `${icons}#calendar`,
|
||||
Reminder: `${icons}#reminder`,
|
||||
Notifications: `${icons}#notifications`,
|
||||
Location: `${icons}#location`
|
||||
Location: `${icons}#location`,
|
||||
Watch: `${icons}#watch`,
|
||||
Description: `${icons}#description`,
|
||||
Participants: `${icons}#participants`,
|
||||
Repeat: `${icons}#repeat`,
|
||||
Globe: `${icons}#globe`
|
||||
})
|
||||
|
||||
addStringsLoader(calendarId, async (lang: string) => await import(`../lang/${lang}.json`))
|
||||
|
@ -37,6 +37,7 @@
|
||||
"@hcengineering/ui": "^0.6.10",
|
||||
"@hcengineering/presentation": "^0.6.2",
|
||||
"@hcengineering/calendar": "^0.6.13",
|
||||
"@hcengineering/theme": "^0.6.3",
|
||||
"svelte": "3.55.1",
|
||||
"@hcengineering/text-editor": "^0.6.0",
|
||||
"@hcengineering/contact": "^0.6.19",
|
||||
|
176
plugins/calendar-resources/src/components/AddParticipant.svelte
Normal file
176
plugins/calendar-resources/src/components/AddParticipant.svelte
Normal file
@ -0,0 +1,176 @@
|
||||
<!--
|
||||
// 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 contact, { Person } from '@hcengineering/contact'
|
||||
import { IntlString, translate } from '@hcengineering/platform'
|
||||
import { createQuery } from '@hcengineering/presentation'
|
||||
import { themeStore } from '@hcengineering/theme'
|
||||
import { registerFocus, resizeObserver } from '@hcengineering/ui'
|
||||
import { afterUpdate, createEventDispatcher, onMount } from 'svelte'
|
||||
|
||||
export let maxWidth: string | undefined = undefined
|
||||
export let value: string | number | undefined = undefined
|
||||
export let placeholder: IntlString
|
||||
export let autoFocus: boolean = false
|
||||
export let select: boolean = false
|
||||
export let focusable: boolean = false
|
||||
export let disabled: boolean = false
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
let text: HTMLElement
|
||||
let input: HTMLInputElement
|
||||
let style: string
|
||||
let phTraslate: string = ''
|
||||
let parentWidth: number | undefined
|
||||
|
||||
$: style = `max-width: ${maxWidth || (parentWidth ? `${parentWidth}px` : 'max-content')};`
|
||||
$: translate(placeholder, {}, $themeStore.language).then((res) => {
|
||||
phTraslate = res
|
||||
})
|
||||
|
||||
function computeSize (t: HTMLInputElement | EventTarget | null) {
|
||||
if (t == null) {
|
||||
return
|
||||
}
|
||||
const target = t as HTMLInputElement
|
||||
const value = target.value
|
||||
text.innerHTML = (value === '' ? phTraslate : value)
|
||||
.replaceAll(' ', ' ')
|
||||
.replaceAll('<', '<')
|
||||
.replaceAll('>', '>')
|
||||
target.style.width = Math.max(text.clientWidth, 50) + 'px'
|
||||
dispatch('input')
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
if (autoFocus) {
|
||||
input.focus()
|
||||
autoFocus = false
|
||||
}
|
||||
if (select) {
|
||||
input.select()
|
||||
select = false
|
||||
}
|
||||
computeSize(input)
|
||||
})
|
||||
|
||||
afterUpdate(() => {
|
||||
computeSize(input)
|
||||
})
|
||||
|
||||
export function focusInput () {
|
||||
input?.focus()
|
||||
}
|
||||
export function selectInput () {
|
||||
input?.select()
|
||||
}
|
||||
|
||||
// Focusable control with index
|
||||
export let focusIndex = -1
|
||||
const { idx, focusManager } = registerFocus(focusIndex, {
|
||||
focus: () => {
|
||||
focusInput()
|
||||
return input != null
|
||||
},
|
||||
isFocus: () => document.activeElement === input
|
||||
})
|
||||
const updateFocus = () => {
|
||||
focusManager?.setFocus(idx)
|
||||
}
|
||||
$: if (input) {
|
||||
input.addEventListener('focus', updateFocus, { once: true })
|
||||
}
|
||||
|
||||
export function focus (): void {
|
||||
input.focus()
|
||||
}
|
||||
|
||||
const query = createQuery()
|
||||
|
||||
let persons: Person[] = []
|
||||
|
||||
$: query.query(contact.class.Person, {}, (res) => {
|
||||
persons = res
|
||||
})
|
||||
</script>
|
||||
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<div
|
||||
class="editbox-container"
|
||||
class:w-full={focusable}
|
||||
on:click={() => {
|
||||
input.focus()
|
||||
}}
|
||||
use:resizeObserver={(element) => {
|
||||
parentWidth = element.parentElement?.getBoundingClientRect().width
|
||||
}}
|
||||
>
|
||||
<div class="hidden-text" bind:this={text} />
|
||||
<div class="flex-row-center clear-mins" class:focusable>
|
||||
<input
|
||||
{disabled}
|
||||
bind:this={input}
|
||||
type="text"
|
||||
bind:value
|
||||
placeholder={phTraslate}
|
||||
{style}
|
||||
on:input={(ev) => ev.target && computeSize(ev.target)}
|
||||
on:change
|
||||
on:keydown
|
||||
on:keypress
|
||||
on:blur
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.editbox-container {
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
|
||||
.focusable {
|
||||
margin: 0 -0.75rem;
|
||||
padding: 0.625rem 0.75rem;
|
||||
width: calc(100% + 1.5rem);
|
||||
border-radius: 0.25rem;
|
||||
transition: border-color 0.15s ease-in-out;
|
||||
|
||||
&:focus-within {
|
||||
box-shadow: 0 0 0 1px var(--theme-editbox-focus-border);
|
||||
}
|
||||
}
|
||||
|
||||
input {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
min-width: 0;
|
||||
color: var(--theme-caption-color);
|
||||
border: none;
|
||||
border-radius: 2px;
|
||||
|
||||
&::-webkit-contacts-auto-fill-button,
|
||||
&::-webkit-credentials-auto-fill-button {
|
||||
visibility: hidden;
|
||||
display: none !important;
|
||||
pointer-events: none;
|
||||
height: 0;
|
||||
width: 0;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
@ -80,10 +80,10 @@
|
||||
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)
|
||||
return new Date(new Date(date).setDate(-7)).setHours(0, 0, 0, 0)
|
||||
}
|
||||
case CalendarMode.Year: {
|
||||
return new Date(new Date(date).setMonth(0, 1)).setHours(0, 0, 0, 0)
|
||||
return new Date(new Date(date).setMonth(0, -7)).setHours(0, 0, 0, 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -101,10 +101,10 @@
|
||||
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)
|
||||
return new Date(new Date(date).setMonth(date.getMonth() + 1, 14)).setHours(0, 0, 0, 0)
|
||||
}
|
||||
case CalendarMode.Year: {
|
||||
return new Date(new Date(date).setMonth(12, 1)).setHours(0, 0, 0, 0)
|
||||
return new Date(new Date(date).setMonth(12, 14)).setHours(0, 0, 0, 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -378,7 +378,7 @@
|
||||
events={objects}
|
||||
{mondayStart}
|
||||
displayedDaysCount={7}
|
||||
startFromWeekStart={false}
|
||||
startFromWeekStart
|
||||
bind:selectedDate
|
||||
bind:currentDate
|
||||
on:create={(e) => showCreateDialog(e.detail.date, e.detail.withTime)}
|
||||
|
@ -13,15 +13,18 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { Calendar, generateEventId } from '@hcengineering/calendar'
|
||||
import { Employee, PersonAccount } from '@hcengineering/contact'
|
||||
import { UserBoxList } from '@hcengineering/contact-resources'
|
||||
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, RecurringRule, generateEventId } from '@hcengineering/calendar'
|
||||
import { Person, PersonAccount } from '@hcengineering/contact'
|
||||
import { Class, Doc, Ref, getCurrentAccount } from '@hcengineering/core'
|
||||
import presentation, { getClient } from '@hcengineering/presentation'
|
||||
import { Button, CheckBox, EditBox, Icon, IconClose, Label, showPopup } from '@hcengineering/ui'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import calendar from '../plugin'
|
||||
import { saveUTC } from '../utils'
|
||||
import EventParticipants from './EventParticipants.svelte'
|
||||
import EventTimeEditor from './EventTimeEditor.svelte'
|
||||
import RRulePresenter from './RRulePresenter.svelte'
|
||||
import ReccurancePopup from './ReccurancePopup.svelte'
|
||||
|
||||
export let attachedTo: Ref<Doc> = calendar.ids.NoAttached
|
||||
export let attachedToClass: Ref<Class<Doc>> = calendar.class.Event
|
||||
@ -35,13 +38,17 @@
|
||||
|
||||
let startDate =
|
||||
date === undefined ? now.getTime() : withTime ? date.getTime() : date.setHours(now.getHours(), now.getMinutes())
|
||||
let duration = defaultDuration
|
||||
const duration = defaultDuration
|
||||
let dueDate = startDate + duration
|
||||
let dueDateRef: DateRangePresenter
|
||||
let allDay = false
|
||||
|
||||
let description: string = ''
|
||||
|
||||
let rules: RecurringRule[] = []
|
||||
|
||||
const currentUser = getCurrentAccount() as PersonAccount
|
||||
let participants: Ref<Employee>[] = [currentUser.person as Ref<Employee>]
|
||||
let participants: Ref<Person>[] = [currentUser.person]
|
||||
let externalParticipants: string[] = []
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
const client = getClient()
|
||||
@ -54,40 +61,37 @@
|
||||
let date: number | undefined
|
||||
if (startDate != null) date = startDate
|
||||
if (date === undefined) return
|
||||
if (title === '') return
|
||||
const space = `${getCurrentAccount()._id}_calendar` as Ref<Calendar>
|
||||
await client.addCollection(calendar.class.Event, space, attachedTo, attachedToClass, 'events', {
|
||||
eventId: generateEventId(),
|
||||
date: allDay ? saveUTC(date) : date,
|
||||
dueDate: allDay ? saveUTC(dueDate) : dueDate,
|
||||
description: '',
|
||||
participants,
|
||||
title,
|
||||
allDay,
|
||||
access: 'owner'
|
||||
})
|
||||
}
|
||||
|
||||
const handleNewStartDate = async (newStartDate: number | null) => {
|
||||
if (newStartDate !== null) {
|
||||
startDate = allDay ? new Date(newStartDate).setHours(0, 0, 0, 0) : newStartDate
|
||||
dueDate = startDate + (allDay ? allDayDuration : duration)
|
||||
await tick()
|
||||
dueDateRef.adaptValue()
|
||||
}
|
||||
}
|
||||
|
||||
const handleNewDueDate = async (newDueDate: number | null) => {
|
||||
if (newDueDate !== null) {
|
||||
const diff = newDueDate - startDate
|
||||
if (diff > 0) {
|
||||
dueDate = allDay ? new Date(newDueDate).setHours(23, 59, 59, 999) : newDueDate
|
||||
duration = dueDate - startDate
|
||||
} else {
|
||||
dueDate = startDate + (allDay ? allDayDuration : duration)
|
||||
}
|
||||
await tick()
|
||||
dueDateRef.adaptValue()
|
||||
if (rules.length > 0) {
|
||||
await client.addCollection(calendar.class.ReccuringEvent, space, attachedTo, attachedToClass, 'events', {
|
||||
eventId: generateEventId(),
|
||||
date: allDay ? saveUTC(date) : date,
|
||||
dueDate: allDay ? saveUTC(dueDate) : dueDate,
|
||||
externalParticipants,
|
||||
rdate: [],
|
||||
exdate: [],
|
||||
rules,
|
||||
description,
|
||||
participants,
|
||||
title,
|
||||
allDay,
|
||||
access: 'owner'
|
||||
})
|
||||
} else {
|
||||
await client.addCollection(calendar.class.Event, space, attachedTo, attachedToClass, 'events', {
|
||||
eventId: generateEventId(),
|
||||
date: allDay ? saveUTC(date) : date,
|
||||
dueDate: allDay ? saveUTC(dueDate) : dueDate,
|
||||
externalParticipants,
|
||||
description,
|
||||
participants,
|
||||
title,
|
||||
allDay,
|
||||
access: 'owner'
|
||||
})
|
||||
}
|
||||
dispatch('close')
|
||||
}
|
||||
|
||||
async function allDayChangeHandler () {
|
||||
@ -98,44 +102,125 @@
|
||||
} else {
|
||||
dueDate = startDate + defaultDuration
|
||||
}
|
||||
await tick()
|
||||
dueDateRef.adaptValue()
|
||||
}
|
||||
|
||||
$: mode = allDay ? DateRangeMode.DATE : DateRangeMode.DATETIME
|
||||
function setRecurrance () {
|
||||
showPopup(ReccurancePopup, { rules }, undefined, (res) => {
|
||||
if (res) {
|
||||
rules = res
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<Card
|
||||
label={calendar.string.CreateEvent}
|
||||
okAction={saveEvent}
|
||||
canSave={title !== undefined && title.trim().length > 0 && participants.length > 0 && startDate !== undefined}
|
||||
on:close={() => {
|
||||
dispatch('close')
|
||||
}}
|
||||
on:changeContent
|
||||
>
|
||||
<EditBox bind:value={title} placeholder={calendar.string.Title} kind={'large-style'} autoFocus />
|
||||
<svelte:fragment slot="pool">
|
||||
<ToggleWithLabel bind:on={allDay} label={calendar.string.AllDay} on:change={allDayChangeHandler} />
|
||||
<DateRangePresenter
|
||||
value={startDate}
|
||||
labelNull={ui.string.SelectDate}
|
||||
on:change={async (event) => await handleNewStartDate(event.detail)}
|
||||
kind={'regular'}
|
||||
{mode}
|
||||
size={'large'}
|
||||
editable
|
||||
<div class="container">
|
||||
<div class="header flex-between">
|
||||
<EditBox bind:value={title} placeholder={calendar.string.NewEvent} />
|
||||
<Button
|
||||
id="card-close"
|
||||
focusIndex={10002}
|
||||
icon={IconClose}
|
||||
iconProps={{ size: 'medium', fill: 'var(--theme-dark-color)' }}
|
||||
kind={'ghost'}
|
||||
size={'small'}
|
||||
on:click={() => {
|
||||
dispatch('close')
|
||||
}}
|
||||
/>
|
||||
<DateRangePresenter
|
||||
bind:this={dueDateRef}
|
||||
value={dueDate}
|
||||
labelNull={calendar.string.DueTo}
|
||||
on:change={async (event) => await handleNewDueDate(event.detail)}
|
||||
kind={'regular'}
|
||||
{mode}
|
||||
size={'large'}
|
||||
editable
|
||||
/>
|
||||
<UserBoxList bind:items={participants} label={calendar.string.Participants} kind={'regular'} size={'large'} />
|
||||
</svelte:fragment>
|
||||
</Card>
|
||||
</div>
|
||||
<div class="time">
|
||||
<EventTimeEditor {allDay} bind:startDate bind:dueDate />
|
||||
<div>
|
||||
{#if !allDay && rules.length === 0}
|
||||
<div class="flex-row-center flex-gap-3 ext">
|
||||
<div class="cursor-pointer" on:click={() => (allDay = true)}>
|
||||
<Label label={calendar.string.AllDay} />
|
||||
</div>
|
||||
<div>
|
||||
<Label label={calendar.string.TimeZone} />
|
||||
</div>
|
||||
<div class="cursor-pointer" on:click={setRecurrance}>
|
||||
<Label label={calendar.string.Repeat} />
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<div>
|
||||
<div class="flex-row-center flex-gap-2 mt-1">
|
||||
<CheckBox bind:checked={allDay} accented on:value={allDayChangeHandler} />
|
||||
<Label label={calendar.string.AllDay} />
|
||||
</div>
|
||||
<div class="flex-row-center flex-gap-2 mt-1">
|
||||
<Icon size="small" icon={calendar.icon.Globe} />
|
||||
<Label label={calendar.string.TimeZone} />
|
||||
</div>
|
||||
<div class="flex-row-center flex-gap-2 mt-1" on:click={setRecurrance}>
|
||||
<Icon size="small" icon={calendar.icon.Repeat} />
|
||||
{#if rules.length > 0}
|
||||
<RRulePresenter {rules} />
|
||||
{:else}
|
||||
<Label label={calendar.string.Repeat} />
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<div class="divider" />
|
||||
<div>
|
||||
<EventParticipants bind:participants bind:externalParticipants />
|
||||
</div>
|
||||
<div class="divider" />
|
||||
<div class="block">
|
||||
<div class="flex-row-center flex-gap-2">
|
||||
<Icon icon={calendar.icon.Description} size="small" />
|
||||
<EditBox bind:value={description} placeholder={calendar.string.Description} />
|
||||
</div>
|
||||
</div>
|
||||
<div class="divider" />
|
||||
<div class="flex-between pool">
|
||||
<div />
|
||||
<Button kind="accented" label={presentation.string.Create} on:click={saveEvent} disabled={title === ''} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
background: var(--theme-popup-color);
|
||||
box-shadow: var(--theme-popup-shadow);
|
||||
width: 25rem;
|
||||
border-radius: 1rem;
|
||||
|
||||
.header {
|
||||
margin: 0.75rem 0.75rem 0 0.75rem;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.block {
|
||||
padding-left: 1.25rem;
|
||||
padding-right: 1.25rem;
|
||||
}
|
||||
|
||||
.divider {
|
||||
margin-top: 0.75rem;
|
||||
margin-bottom: 0.75rem;
|
||||
border-bottom: 1px solid var(--border-color, rgba(0, 0, 0, 0.1));
|
||||
}
|
||||
|
||||
.pool {
|
||||
margin-top: 0.5rem;
|
||||
margin: 1.25rem;
|
||||
}
|
||||
|
||||
.time {
|
||||
margin-left: 1.25rem;
|
||||
margin-right: 1.25rem;
|
||||
|
||||
.ext {
|
||||
color: var(--theme-dark-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
100
plugins/calendar-resources/src/components/DateEditor.svelte
Normal file
100
plugins/calendar-resources/src/components/DateEditor.svelte
Normal file
@ -0,0 +1,100 @@
|
||||
<!--
|
||||
// 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 ui, {
|
||||
Button,
|
||||
ButtonKind,
|
||||
DatePopup,
|
||||
SimpleDatePopup,
|
||||
SimpleTimePopup,
|
||||
eventToHTMLElement,
|
||||
showPopup
|
||||
} from '@hcengineering/ui'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import DateLocalePresenter from './DateLocalePresenter.svelte'
|
||||
|
||||
export let date: number
|
||||
export let direction: 'vertical' | 'horizontal' = 'vertical'
|
||||
export let showDate: boolean = true
|
||||
export let withoutTime: boolean
|
||||
export let kind: ButtonKind = 'ghost'
|
||||
export let disabled: boolean = false
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
function timeClick (e: MouseEvent) {
|
||||
if (showDate) {
|
||||
showPopup(SimpleTimePopup, { currentDate: new Date(date) }, eventToHTMLElement(e), (res) => {
|
||||
if (res.value) {
|
||||
date = res.value.getTime()
|
||||
dispatch('update', date)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
showPopup(
|
||||
DatePopup,
|
||||
{ currentDate: new Date(date), withTime: !withoutTime, label: ui.string.SelectDate, noShift: true },
|
||||
undefined,
|
||||
(res) => {
|
||||
if (res) {
|
||||
date = res.value.getTime()
|
||||
dispatch('update', date)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
function dateClick (e: MouseEvent) {
|
||||
showPopup(SimpleDatePopup, { currentDate: new Date(date) }, eventToHTMLElement(e), (res) => {
|
||||
if (res) {
|
||||
date = res.getTime()
|
||||
dispatch('update', date)
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="container" class:vertical={direction === 'vertical'} class:horizontal={direction === 'horizontal'}>
|
||||
{#if showDate || withoutTime}
|
||||
<Button {kind} on:click={dateClick} {disabled}>
|
||||
<div slot="content">
|
||||
<DateLocalePresenter {date} />
|
||||
</div>
|
||||
</Button>
|
||||
{/if}
|
||||
{#if !withoutTime}
|
||||
<Button {kind} on:click={timeClick} {disabled}>
|
||||
<div slot="content">
|
||||
{new Date(date).toLocaleTimeString('default', { hour: 'numeric', minute: '2-digit' })}
|
||||
</div>
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.vertical {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.horizontal {
|
||||
flex-direction: row;
|
||||
}
|
||||
</style>
|
@ -0,0 +1,31 @@
|
||||
<!--
|
||||
// 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">
|
||||
export let date: number
|
||||
const current = new Date()
|
||||
let options: Intl.DateTimeFormatOptions = {
|
||||
day: 'numeric',
|
||||
weekday: 'short',
|
||||
month: 'short'
|
||||
}
|
||||
if (current.getFullYear() !== new Date(date).getFullYear()) {
|
||||
options = {
|
||||
...options,
|
||||
year: '2-digit'
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
{new Date(date).toLocaleDateString('default', options)}
|
@ -70,13 +70,7 @@
|
||||
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'
|
||||
)
|
||||
showPopup(calendar.component.EditEvent, { object: e }, 'content')
|
||||
}}
|
||||
>
|
||||
{e.title}
|
||||
|
@ -106,7 +106,7 @@
|
||||
return result
|
||||
}
|
||||
|
||||
$: calendarEvents = toCalendar(events, currentDate, displayedDaysCount, startHour, displayedHours + startHour)
|
||||
$: calendarEvents = toCalendar(events, weekMonday, displayedDaysCount, startHour, displayedHours + startHour)
|
||||
|
||||
$: fontSize = $deviceInfo.fontSize
|
||||
$: docHeight = $deviceInfo.docHeight
|
||||
@ -325,7 +325,7 @@
|
||||
event: CalendarItem
|
||||
): { top: number; bottom: number; left: number; right: number; width: number; visibility: number } => {
|
||||
const result = { top: 0, bottom: 0, left: 0, right: 0, width: 0, visibility: 1 }
|
||||
const checkDate = new Date(currentDate.getTime() + MILLISECONDS_IN_DAY * event.day)
|
||||
const checkDate = new Date(weekMonday.getTime() + MILLISECONDS_IN_DAY * event.day)
|
||||
const startDay = checkDate.setHours(startHour, 0, 0, 0)
|
||||
const endDay = checkDate.setHours(displayedHours - 1, 59, 59, 999)
|
||||
const startTime = event.date < startDay ? { hours: 0, mins: 0 } : convertToTime(event.date)
|
||||
@ -343,7 +343,7 @@
|
||||
cellHeight * (displayedHours - startHour - endTime.hours - 1) +
|
||||
((60 - endTime.mins) / 60) * cellHeight +
|
||||
getGridOffset(endTime.mins, true) +
|
||||
(showHeader ? 0 : rem(2.5))
|
||||
(showHeader ? 0 : rem(3.5))
|
||||
let cols = 1
|
||||
let index: number = 0
|
||||
grid[event.day].columns.forEach((col, i) =>
|
||||
|
@ -13,193 +13,307 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { Event } from '@hcengineering/calendar'
|
||||
import { DateRangeMode, Doc } 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,
|
||||
showPopup
|
||||
} from '@hcengineering/ui'
|
||||
import view from '@hcengineering/view'
|
||||
import { ContextMenu, ObjectPresenter, getObjectPreview } from '@hcengineering/view-resources'
|
||||
import { createEventDispatcher, tick } from 'svelte'
|
||||
import { Event, ReccuringEvent, ReccuringInstance, RecurringRule, generateEventId } from '@hcengineering/calendar'
|
||||
import { Person } from '@hcengineering/contact'
|
||||
import { DocumentUpdate, Ref } from '@hcengineering/core'
|
||||
import presentation, { getClient } from '@hcengineering/presentation'
|
||||
import { Button, CheckBox, DAY, EditBox, Icon, IconClose, Label, closePopup, showPopup } from '@hcengineering/ui'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import calendar from '../plugin'
|
||||
import { saveUTC } from '../utils'
|
||||
import EventParticipants from './EventParticipants.svelte'
|
||||
import EventTimeEditor from './EventTimeEditor.svelte'
|
||||
import RRulePresenter from './RRulePresenter.svelte'
|
||||
import ReccurancePopup from './ReccurancePopup.svelte'
|
||||
import UpdateRecInstancePopup from './UpdateRecInstancePopup.svelte'
|
||||
|
||||
export let object: Event
|
||||
|
||||
let title = object.title
|
||||
|
||||
const defaultDuration = 60 * 60 * 1000
|
||||
const allDayDuration = 24 * 60 * 60 * 1000 - 1
|
||||
|
||||
let startDate = object.date
|
||||
const duration = object.dueDate - object.date
|
||||
let dueDate = startDate + duration
|
||||
let allDay = object.allDay
|
||||
|
||||
let description = object.description
|
||||
|
||||
let rules: RecurringRule[] = (object as ReccuringEvent).rules ?? []
|
||||
|
||||
let participants: Ref<Person>[] = object.participants
|
||||
let externalParticipants: string[] = object.externalParticipants ?? []
|
||||
|
||||
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]
|
||||
})
|
||||
export function canClose (): boolean {
|
||||
return title !== undefined && title.trim().length === 0 && participants.length === 0
|
||||
}
|
||||
|
||||
let presenter: AnyComponent | undefined
|
||||
async function updatePreviewPresenter (doc?: Doc): Promise<void> {
|
||||
if (doc === undefined) {
|
||||
return
|
||||
async function saveEvent () {
|
||||
const update: DocumentUpdate<Event> = {}
|
||||
if (object.title !== title) {
|
||||
update.title = title.trim()
|
||||
}
|
||||
presenter = doc !== undefined ? await getObjectPreview(client, doc._class) : undefined
|
||||
}
|
||||
|
||||
$: updatePreviewPresenter(doc)
|
||||
|
||||
$: mode = object.allDay ? DateRangeMode.DATE : DateRangeMode.DATETIME
|
||||
|
||||
const defaultDuration = 60 * 60 * 1000
|
||||
const allDayDuration = 24 * 60 * 60 * 1000 - 1
|
||||
let duration = object.dueDate - object.date
|
||||
|
||||
async function updateDate () {
|
||||
await client.update(object, {
|
||||
date: object.allDay ? saveUTC(object.date) : object.date,
|
||||
dueDate: object.allDay ? saveUTC(object.dueDate) : object.dueDate,
|
||||
allDay: object.allDay
|
||||
})
|
||||
}
|
||||
|
||||
async function handleNewStartDate (newStartDate: number | null) {
|
||||
if (newStartDate !== null) {
|
||||
object.date = object.allDay ? new Date(newStartDate).setHours(0, 0, 0, 0) : newStartDate
|
||||
object.dueDate = object.date + (object.allDay ? allDayDuration : duration)
|
||||
await tick()
|
||||
dueDateRef.adaptValue()
|
||||
await updateDate()
|
||||
if (object.description !== description) {
|
||||
update.description = description.trim()
|
||||
}
|
||||
}
|
||||
|
||||
async function handleNewDueDate (newDueDate: number | null) {
|
||||
if (newDueDate !== null) {
|
||||
const diff = newDueDate - object.date
|
||||
if (diff > 0) {
|
||||
object.dueDate = object.allDay ? new Date(newDueDate).setHours(23, 59, 59, 999) : newDueDate
|
||||
duration = object.dueDate - object.date
|
||||
} else {
|
||||
object.dueDate = object.date + (object.allDay ? allDayDuration : duration)
|
||||
if (allDay !== object.allDay) {
|
||||
update.date = allDay ? saveUTC(startDate) : startDate
|
||||
update.dueDate = allDay ? saveUTC(dueDate) : dueDate
|
||||
update.allDay = allDay
|
||||
} else {
|
||||
if (object.date !== startDate) {
|
||||
update.date = allDay ? saveUTC(startDate) : startDate
|
||||
}
|
||||
if (object.dueDate !== dueDate) {
|
||||
update.dueDate = allDay ? saveUTC(dueDate) : dueDate
|
||||
}
|
||||
await tick()
|
||||
dueDateRef.adaptValue()
|
||||
await updateDate()
|
||||
}
|
||||
if (rules !== (object as ReccuringEvent).rules) {
|
||||
;(update as DocumentUpdate<ReccuringEvent>).rules = rules
|
||||
}
|
||||
if (Object.keys(update).length > 0) {
|
||||
if (object._class === calendar.class.ReccuringInstance) {
|
||||
await updateHandler(update)
|
||||
} else {
|
||||
await client.update(object, update)
|
||||
}
|
||||
}
|
||||
dispatch('close')
|
||||
}
|
||||
|
||||
async function allDayChangeHandler () {
|
||||
if (object.allDay) {
|
||||
object.date = new Date(object.date).setHours(0, 0, 0, 0)
|
||||
if (object.dueDate - object.date < allDayDuration) object.dueDate = allDayDuration + object.date
|
||||
else object.dueDate = new Date(object.dueDate).setHours(23, 59, 59, 999)
|
||||
if (allDay) {
|
||||
startDate = new Date(startDate).setHours(0, 0, 0, 0)
|
||||
if (dueDate - startDate < allDayDuration) dueDate = allDayDuration + startDate
|
||||
else dueDate = new Date(dueDate).setHours(23, 59, 59, 999)
|
||||
} else {
|
||||
object.dueDate = object.date + defaultDuration
|
||||
dueDate = startDate + defaultDuration
|
||||
}
|
||||
await tick()
|
||||
dueDateRef.adaptValue()
|
||||
await updateDate()
|
||||
}
|
||||
|
||||
let dueDateRef: DateRangePresenter
|
||||
function setRecurrance () {
|
||||
showPopup(ReccurancePopup, { rules }, undefined, (res) => {
|
||||
if (res) {
|
||||
rules = res
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function showMenu (ev?: MouseEvent): void {
|
||||
if (object !== undefined) {
|
||||
showPopup(ContextMenu, { object, excludedActions: [view.action.Open] }, ev?.target as HTMLElement)
|
||||
async function updatePast (ops: DocumentUpdate<Event>) {
|
||||
const obj = object as ReccuringInstance
|
||||
const origin = await client.findOne(calendar.class.ReccuringEvent, {
|
||||
eventId: obj.recurringEventId,
|
||||
space: obj.space
|
||||
})
|
||||
if (origin !== undefined) {
|
||||
await client.addCollection(
|
||||
calendar.class.ReccuringEvent,
|
||||
origin.space,
|
||||
origin.attachedTo,
|
||||
origin.attachedToClass,
|
||||
origin.collection,
|
||||
{
|
||||
...origin,
|
||||
date: obj.date,
|
||||
dueDate: obj.dueDate,
|
||||
...ops,
|
||||
eventId: generateEventId()
|
||||
}
|
||||
)
|
||||
const targetDate = ops.date ?? obj.date
|
||||
await client.update(origin, {
|
||||
rules: [{ ...origin.rules[0], endDate: targetDate - DAY }],
|
||||
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<ReccuringEvent>) {
|
||||
const obj = object as ReccuringInstance
|
||||
if (obj.virtual !== true) {
|
||||
await client.update(object, ops)
|
||||
} else {
|
||||
showPopup(UpdateRecInstancePopup, { currentAvailable: ops.rules === undefined }, undefined, async (res) => {
|
||||
if (res !== null) {
|
||||
if (res.mode === 'current') {
|
||||
await client.addCollection(
|
||||
obj._class,
|
||||
obj.space,
|
||||
obj.attachedTo,
|
||||
obj.attachedToClass,
|
||||
obj.collection,
|
||||
{
|
||||
title: obj.title,
|
||||
description: obj.description,
|
||||
date: obj.date,
|
||||
dueDate: obj.dueDate,
|
||||
allDay: obj.allDay,
|
||||
participants: obj.participants,
|
||||
externalParticipants: obj.externalParticipants,
|
||||
originalStartTime: obj.originalStartTime,
|
||||
recurringEventId: obj.recurringEventId,
|
||||
reminders: obj.reminders,
|
||||
location: obj.location,
|
||||
eventId: obj.eventId,
|
||||
access: 'owner',
|
||||
rules: obj.rules,
|
||||
exdate: obj.exdate,
|
||||
rdate: obj.rdate,
|
||||
...ops
|
||||
},
|
||||
obj._id
|
||||
)
|
||||
} else if (res.mode === 'all') {
|
||||
const base = await client.findOne(calendar.class.ReccuringEvent, {
|
||||
space: obj.space,
|
||||
eventId: obj.recurringEventId
|
||||
})
|
||||
if (base !== undefined) {
|
||||
await client.update(base, ops)
|
||||
}
|
||||
} else if (res.mode === 'next') {
|
||||
await updatePast(ops)
|
||||
}
|
||||
}
|
||||
closePopup()
|
||||
})
|
||||
}
|
||||
}
|
||||
</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 class="container">
|
||||
<div class="header flex-between">
|
||||
{#if object.attachedTo === calendar.ids.NoAttached}
|
||||
<EditBox bind:value={title} placeholder={calendar.string.NewEvent} />
|
||||
{:else}
|
||||
<div />
|
||||
{/if}
|
||||
<Button
|
||||
id="card-close"
|
||||
focusIndex={10002}
|
||||
icon={IconClose}
|
||||
iconProps={{ size: 'medium', fill: 'var(--theme-dark-color)' }}
|
||||
kind={'ghost'}
|
||||
size={'small'}
|
||||
on:click={() => {
|
||||
dispatch('close')
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div class="time">
|
||||
<EventTimeEditor {allDay} bind:startDate bind:dueDate />
|
||||
<div>
|
||||
{#if !allDay && rules.length === 0}
|
||||
<div class="flex-row-center flex-gap-3 ext">
|
||||
<div class="cursor-pointer" on:click={() => (allDay = true)}>
|
||||
<Label label={calendar.string.AllDay} />
|
||||
</div>
|
||||
<div>
|
||||
<Label label={calendar.string.TimeZone} />
|
||||
</div>
|
||||
{#if rules.length > 0}
|
||||
<div class="cursor-pointer" on:click={setRecurrance}>
|
||||
<Label label={calendar.string.Repeat} />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{#if presenter !== undefined && doc}
|
||||
<div class="antiPanel p-4">
|
||||
<Component is={presenter} props={{ object: doc }} />
|
||||
{:else}
|
||||
<div>
|
||||
<div class="flex-row-center flex-gap-2 mt-1">
|
||||
<CheckBox bind:checked={allDay} accented on:value={allDayChangeHandler} />
|
||||
<Label label={calendar.string.AllDay} />
|
||||
</div>
|
||||
<div class="flex-row-center flex-gap-2 mt-1">
|
||||
<Icon size="small" icon={calendar.icon.Globe} />
|
||||
<Label label={calendar.string.TimeZone} />
|
||||
</div>
|
||||
{#if rules.length > 0}
|
||||
<div class="flex-row-center flex-gap-2 mt-1" on:click={setRecurrance}>
|
||||
<Icon size="small" icon={calendar.icon.Repeat} />
|
||||
{#if rules.length > 0}
|
||||
<RRulePresenter {rules} />
|
||||
{:else}
|
||||
<Label label={calendar.string.Repeat} />
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
<div class="mb-2">
|
||||
<div class="mb-4">
|
||||
<EditBox
|
||||
label={calendar.string.Title}
|
||||
bind:value={object.title}
|
||||
on:change={() => client.update(object, { title: object.title })}
|
||||
/>
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<StyledTextBox
|
||||
kind={'emphasized'}
|
||||
content={object.description}
|
||||
on:value={(evt) => {
|
||||
client.update(object, { description: evt.detail })
|
||||
}}
|
||||
label={calendar.string.Description}
|
||||
placeholder={calendar.string.Description}
|
||||
/>
|
||||
</div>
|
||||
<div class="divider" />
|
||||
<div>
|
||||
<EventParticipants bind:participants bind:externalParticipants />
|
||||
</div>
|
||||
<div class="divider" />
|
||||
<div class="block">
|
||||
<div class="flex-row-center flex-gap-2">
|
||||
<Icon icon={calendar.icon.Description} size="small" />
|
||||
<EditBox bind:value={description} placeholder={calendar.string.Description} />
|
||||
</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 class="divider" />
|
||||
<div class="flex-between pool">
|
||||
<div />
|
||||
<Button
|
||||
kind="accented"
|
||||
label={presentation.string.Save}
|
||||
on:click={saveEvent}
|
||||
disabled={title === '' && object.attachedTo === calendar.ids.NoAttached}
|
||||
/>
|
||||
</div>
|
||||
</Panel>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
background: var(--theme-popup-color);
|
||||
box-shadow: var(--theme-popup-shadow);
|
||||
width: 25rem;
|
||||
border-radius: 1rem;
|
||||
|
||||
.header {
|
||||
margin: 0.75rem 0.75rem 0 0.75rem;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.block {
|
||||
padding-left: 1.25rem;
|
||||
padding-right: 1.25rem;
|
||||
}
|
||||
|
||||
.divider {
|
||||
margin-top: 0.75rem;
|
||||
margin-bottom: 0.75rem;
|
||||
border-bottom: 1px solid var(--border-color, rgba(0, 0, 0, 0.1));
|
||||
}
|
||||
|
||||
.pool {
|
||||
margin-top: 0.5rem;
|
||||
margin: 1.25rem;
|
||||
}
|
||||
|
||||
.time {
|
||||
margin-left: 1.25rem;
|
||||
margin-right: 1.25rem;
|
||||
|
||||
.ext {
|
||||
color: var(--theme-dark-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@ -1,295 +0,0 @@
|
||||
<!--
|
||||
// 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>
|
@ -16,10 +16,18 @@
|
||||
import calendar, { CalendarEventPresenter, Event } from '@hcengineering/calendar'
|
||||
import { Doc, DocumentUpdate } from '@hcengineering/core'
|
||||
import { getClient } from '@hcengineering/presentation'
|
||||
import { Component, MILLISECONDS_IN_MINUTE, deviceOptionsStore, showPopup, tooltip } from '@hcengineering/ui'
|
||||
import {
|
||||
Component,
|
||||
MILLISECONDS_IN_MINUTE,
|
||||
deviceOptionsStore,
|
||||
getEventPositionElement,
|
||||
showPopup,
|
||||
tooltip
|
||||
} from '@hcengineering/ui'
|
||||
import view, { ObjectEditor } from '@hcengineering/view'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import EventPresenter from './EventPresenter.svelte'
|
||||
import { Menu } from '@hcengineering/view-resources'
|
||||
|
||||
export let event: Event
|
||||
export let hourHeight: number
|
||||
@ -122,6 +130,11 @@
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function showMenu (ev: MouseEvent) {
|
||||
ev.preventDefault()
|
||||
showPopup(Menu, { object: event }, getEventPositionElement(ev))
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if event}
|
||||
@ -134,6 +147,7 @@
|
||||
draggable={!event.allDay}
|
||||
use:tooltip={{ component: EventPresenter, props: { value: event } }}
|
||||
on:click|stopPropagation={click}
|
||||
on:contextmenu={showMenu}
|
||||
on:dragstart={dragStart}
|
||||
on:drag={drag}
|
||||
on:dragend={drop}
|
||||
|
@ -0,0 +1,110 @@
|
||||
<!--
|
||||
// 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 { Person } from '@hcengineering/contact'
|
||||
import { Ref } from '@hcengineering/core'
|
||||
import { Button, Icon, IconClose } from '@hcengineering/ui'
|
||||
import calendar from '../plugin'
|
||||
import AddParticipant from './AddParticipant.svelte'
|
||||
import { ContactRefPresenter } from '@hcengineering/contact-resources'
|
||||
|
||||
export let participants: Ref<Person>[]
|
||||
export let externalParticipants: string[]
|
||||
|
||||
$: placeholder =
|
||||
participants.length > 0 || externalParticipants.length > 0
|
||||
? calendar.string.AddParticipants
|
||||
: calendar.string.Participants
|
||||
|
||||
function removeParticipant (_id: Ref<Person>): void {
|
||||
const index = participants.findIndex((p) => p === _id)
|
||||
if (index !== -1) {
|
||||
participants.splice(index, 1)
|
||||
participants = participants
|
||||
}
|
||||
}
|
||||
|
||||
function removeExtParticipant (val: string): void {
|
||||
const index = externalParticipants.findIndex((p) => p === val)
|
||||
if (index !== -1) {
|
||||
externalParticipants.splice(index, 1)
|
||||
externalParticipants = externalParticipants
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="container flex-col">
|
||||
<div class="header flex-row-center flex-gap-3">
|
||||
<Icon icon={calendar.icon.Participants} size="small" />
|
||||
<AddParticipant {placeholder} />
|
||||
</div>
|
||||
<div class="content">
|
||||
{#each participants as participant}
|
||||
<div class="flex-between item">
|
||||
<ContactRefPresenter disabled value={participant} />
|
||||
<div class="tool">
|
||||
<Button
|
||||
icon={IconClose}
|
||||
iconProps={{ size: 'medium', fill: 'var(--theme-dark-color)' }}
|
||||
kind={'ghost'}
|
||||
size={'small'}
|
||||
on:click={() => {
|
||||
removeParticipant(participant)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
{#each externalParticipants as participant}
|
||||
<div class="flex-between item">
|
||||
{participant}
|
||||
<div class="tool">
|
||||
<Button
|
||||
icon={IconClose}
|
||||
iconProps={{ size: 'medium', fill: 'var(--theme-dark-color)' }}
|
||||
kind={'ghost'}
|
||||
size={'small'}
|
||||
on:click={() => {
|
||||
removeExtParticipant(participant)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.container {
|
||||
margin: 0 1.25rem;
|
||||
|
||||
.content {
|
||||
margin-top: 0.25rem;
|
||||
margin-left: 1.75rem;
|
||||
}
|
||||
|
||||
.item {
|
||||
.tool {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.tool {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
@ -21,13 +21,7 @@
|
||||
export let inline: boolean = false
|
||||
|
||||
function click (): void {
|
||||
showPopup(
|
||||
value._class === calendar.class.ReccuringInstance
|
||||
? calendar.component.EditRecEvent
|
||||
: calendar.component.EditEvent,
|
||||
{ object: value },
|
||||
'content'
|
||||
)
|
||||
showPopup(calendar.component.EditEvent, { object: value }, 'content')
|
||||
}
|
||||
</script>
|
||||
|
||||
|
@ -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.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { Icon, areDatesEqual, IconArrowRight } from '@hcengineering/ui'
|
||||
import calendar from '../plugin'
|
||||
import DateEditor from './DateEditor.svelte'
|
||||
|
||||
export let startDate: number
|
||||
export let dueDate: number
|
||||
export let allDay: boolean
|
||||
|
||||
$: sameDate = areDatesEqual(new Date(startDate), new Date(dueDate))
|
||||
|
||||
let diff = dueDate - startDate
|
||||
const allDayDuration = 24 * 60 * 60 * 1000 - 1
|
||||
|
||||
function dateChange () {
|
||||
startDate = allDay ? new Date(startDate).setHours(0, 0, 0, 0) : startDate
|
||||
dueDate = startDate + (allDay ? allDayDuration : diff)
|
||||
}
|
||||
|
||||
function dueChange () {
|
||||
const newDiff = dueDate - startDate
|
||||
if (newDiff > 0) {
|
||||
dueDate = allDay ? new Date(dueDate).setHours(23, 59, 59, 999) : dueDate
|
||||
diff = dueDate - startDate
|
||||
} else {
|
||||
dueDate = startDate + (allDay ? allDayDuration : diff)
|
||||
}
|
||||
diff = dueDate - startDate
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex-row-center flex-gap-1 mt-2 mb-2">
|
||||
<Icon icon={calendar.icon.Watch} size="small" />
|
||||
{#if sameDate}
|
||||
<DateEditor bind:date={startDate} direction="horizontal" withoutTime={allDay} on:update={dateChange} />
|
||||
<div class="content-dark-color">
|
||||
<IconArrowRight size="small" />
|
||||
</div>
|
||||
<DateEditor
|
||||
bind:date={dueDate}
|
||||
direction="horizontal"
|
||||
withoutTime={allDay}
|
||||
showDate={false}
|
||||
on:update={dueChange}
|
||||
/>
|
||||
{:else}
|
||||
<DateEditor bind:date={startDate} direction="vertical" withoutTime={allDay} on:update={dateChange} />
|
||||
<div class="content-dark-color">
|
||||
<IconArrowRight size="small" />
|
||||
</div>
|
||||
<DateEditor bind:date={dueDate} direction="vertical" withoutTime={allDay} on:update={dueChange} />
|
||||
{/if}
|
||||
</div>
|
@ -0,0 +1,74 @@
|
||||
<!--
|
||||
// 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 { RecurringRule } from '@hcengineering/calendar'
|
||||
import { Label, themeStore } from '@hcengineering/ui'
|
||||
import calendar from '../plugin'
|
||||
import { IntlString, translate } from '@hcengineering/platform'
|
||||
import DateLocalePresenter from './DateLocalePresenter.svelte'
|
||||
|
||||
export let rules: RecurringRule[] = []
|
||||
$: rule = rules[0]
|
||||
|
||||
const periods: Record<string, IntlString> = {
|
||||
DAILY: calendar.string.Day,
|
||||
WEEKLY: calendar.string.Week,
|
||||
MONTHLY: calendar.string.Month,
|
||||
YEARLY: calendar.string.Year
|
||||
}
|
||||
|
||||
const weekdays: Record<string, IntlString> = {
|
||||
MO: calendar.string.MondayShort,
|
||||
TU: calendar.string.TuesdayShort,
|
||||
WE: calendar.string.WednesdayShort,
|
||||
TH: calendar.string.ThursdayShort,
|
||||
FR: calendar.string.FridayShort,
|
||||
SA: calendar.string.SaturdayShort,
|
||||
SU: calendar.string.SundayShort
|
||||
}
|
||||
|
||||
async function getDays (days: string[], lang: string): Promise<string> {
|
||||
let result = ''
|
||||
for (const day of Object.keys(weekdays)) {
|
||||
if (days.includes(day)) {
|
||||
const curr = await translate(weekdays[day], {}, lang)
|
||||
result += curr
|
||||
result += ', '
|
||||
}
|
||||
}
|
||||
return result.slice(0, -2)
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if rule}
|
||||
<Label label={calendar.string.Every} />
|
||||
{rule.interval ?? 1}
|
||||
<Label label={periods[rule.freq]} params={{ count: rule.interval ?? 1 }} />
|
||||
<span class="content-darker-color">
|
||||
{#if rule.freq === 'WEEKLY' && rule.byDay}
|
||||
{#await getDays(rule.byDay ?? [], $themeStore.language) then str}
|
||||
{str}
|
||||
{/await}
|
||||
{/if}
|
||||
{#if rule.endDate}
|
||||
<Label label={calendar.string.OnUntil} />
|
||||
<DateLocalePresenter date={rule.endDate} />
|
||||
{/if}
|
||||
{#if rule.count}
|
||||
{rule.count}
|
||||
<Label label={calendar.string.Times} params={{ count: rule.count }} />
|
||||
{/if}
|
||||
</span>
|
||||
{/if}
|
221
plugins/calendar-resources/src/components/ReccurancePopup.svelte
Normal file
221
plugins/calendar-resources/src/components/ReccurancePopup.svelte
Normal file
@ -0,0 +1,221 @@
|
||||
<!--
|
||||
// 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 { RecurringRule, getWeekday } from '@hcengineering/calendar'
|
||||
import ui, {
|
||||
Button,
|
||||
CircleButton,
|
||||
DropdownIntlItem,
|
||||
DropdownLabelsIntl,
|
||||
Grid,
|
||||
Label,
|
||||
NumberInput,
|
||||
RadioButton
|
||||
} from '@hcengineering/ui'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import calendar from '../plugin'
|
||||
import DateEditor from './DateEditor.svelte'
|
||||
|
||||
export let rules: RecurringRule[]
|
||||
|
||||
type Freq = 'DAILY' | 'WEEKLY' | 'MONTHLY' | 'YEARLY'
|
||||
let periodType: Freq = (rules[0]?.freq as Freq) ?? 'WEEKLY'
|
||||
let interval: number = rules[0]?.interval ?? 1
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
let selected = rules[0]?.endDate !== undefined ? 'on' : rules[0]?.count ? 'after' : 'never'
|
||||
|
||||
const ddItems = [
|
||||
{
|
||||
id: 'DAILY',
|
||||
label: calendar.string.Day
|
||||
},
|
||||
{
|
||||
id: 'WEEKLY',
|
||||
label: calendar.string.Week
|
||||
},
|
||||
{
|
||||
id: 'MONTHLY',
|
||||
label: calendar.string.Month
|
||||
},
|
||||
{
|
||||
id: 'YEARLY',
|
||||
label: calendar.string.Year
|
||||
}
|
||||
] as DropdownIntlItem[]
|
||||
|
||||
function save () {
|
||||
const res: RecurringRule = {
|
||||
freq: periodType,
|
||||
interval
|
||||
}
|
||||
if (selected === 'on') {
|
||||
res.endDate = until
|
||||
} else if (selected === 'after') {
|
||||
res.count = count
|
||||
}
|
||||
if (periodType === 'WEEKLY') {
|
||||
res.byDay = selectedWeekdays
|
||||
}
|
||||
rules = [res]
|
||||
dispatch('close', rules)
|
||||
}
|
||||
|
||||
let count: number = rules[0]?.count ?? 1
|
||||
|
||||
let selectedWeekdays: string[] = rules[0]?.byDay ?? [getWeekday(new Date())]
|
||||
|
||||
function weekdayClick (day: string) {
|
||||
const index = selectedWeekdays.findIndex((p) => p === day)
|
||||
if (index !== -1) {
|
||||
selectedWeekdays.splice(index, 1)
|
||||
} else {
|
||||
selectedWeekdays.push(day)
|
||||
}
|
||||
selectedWeekdays = selectedWeekdays
|
||||
}
|
||||
|
||||
function isActive (day: string, selected: string[]): boolean {
|
||||
return selectedWeekdays.includes(day)
|
||||
}
|
||||
|
||||
const weekdays = [
|
||||
{ id: 'MO', label: calendar.string.MondayShort },
|
||||
{ id: 'TU', label: calendar.string.TuesdayShort },
|
||||
{ id: 'WE', label: calendar.string.WednesdayShort },
|
||||
{ id: 'TH', label: calendar.string.ThursdayShort },
|
||||
{ id: 'FR', label: calendar.string.FridayShort },
|
||||
{ id: 'SA', label: calendar.string.SaturdayShort },
|
||||
{ id: 'SU', label: calendar.string.SundayShort }
|
||||
]
|
||||
|
||||
let until: number = rules[0]?.endDate ?? Date.now()
|
||||
</script>
|
||||
|
||||
<div class="container">
|
||||
<div class="header fs-title">
|
||||
<Label label={calendar.string.Repeat} />
|
||||
{selected}
|
||||
</div>
|
||||
<div class="content">
|
||||
<div class="flex-row-center flex-gap-2">
|
||||
<Label label={calendar.string.Every} />
|
||||
<NumberInput bind:value={interval} maxWidth={'3rem'} maxDigitsAfterPoint={0} minValue={1} />
|
||||
<DropdownLabelsIntl bind:selected={periodType} items={ddItems} size="medium" params={{ count: interval }} />
|
||||
</div>
|
||||
{#if periodType === 'WEEKLY'}
|
||||
<div class="flex-row-center mt-4">
|
||||
<Label label={calendar.string.On} />
|
||||
<div class="flex-row-center flex-gap-2 ml-6">
|
||||
{#each weekdays as day}
|
||||
<CircleButton
|
||||
size="medium"
|
||||
accented={isActive(day.id, selectedWeekdays)}
|
||||
on:click={() => weekdayClick(day.id)}
|
||||
>
|
||||
<div class="flex-row-center weekday" slot="content">
|
||||
<Label label={day.label} />
|
||||
</div>
|
||||
</CircleButton>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="mt-4 mb-6">
|
||||
<Label label={calendar.string.Ends} />
|
||||
</div>
|
||||
<Grid columnGap={1} rowGap={1}>
|
||||
<RadioButton
|
||||
labelIntl={calendar.string.Never}
|
||||
value={'never'}
|
||||
group={selected}
|
||||
action={() => {
|
||||
selected = 'never'
|
||||
}}
|
||||
/>
|
||||
<div />
|
||||
<RadioButton
|
||||
labelIntl={calendar.string.OnUntil}
|
||||
value={'on'}
|
||||
group={selected}
|
||||
action={() => {
|
||||
selected = 'on'
|
||||
}}
|
||||
/>
|
||||
<div class="flex">
|
||||
<DateEditor bind:date={until} withoutTime kind="regular" disabled={selected !== 'on'} />
|
||||
</div>
|
||||
<RadioButton
|
||||
labelIntl={calendar.string.After}
|
||||
value={'after'}
|
||||
group={selected}
|
||||
action={() => {
|
||||
selected = 'after'
|
||||
}}
|
||||
/>
|
||||
<div class="flex-row-center flex-gap-2">
|
||||
<NumberInput
|
||||
bind:value={count}
|
||||
maxWidth={'3rem'}
|
||||
maxDigitsAfterPoint={0}
|
||||
minValue={1}
|
||||
disabled={selected !== 'after'}
|
||||
/>
|
||||
<Label label={calendar.string.Times} params={{ count }} />
|
||||
</div>
|
||||
</Grid>
|
||||
</div>
|
||||
<div class="pool flex-row-reverse flex-gap-3">
|
||||
<Button
|
||||
label={ui.string.Save}
|
||||
on:click={save}
|
||||
kind="accented"
|
||||
disabled={periodType === 'MONTHLY' && selectedWeekdays.length === 0}
|
||||
/>
|
||||
<Button label={ui.string.Cancel} on:click={() => dispatch('close')} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
background: var(--theme-popup-color);
|
||||
box-shadow: var(--theme-popup-shadow);
|
||||
width: 25rem;
|
||||
border-radius: 1rem;
|
||||
|
||||
.header {
|
||||
margin: 1.25rem;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.content {
|
||||
margin: 1.25rem;
|
||||
}
|
||||
|
||||
.weekday {
|
||||
overflow: hidden;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.pool {
|
||||
border-top: 1px solid var(--border-color, rgba(0, 0, 0, 0.1));
|
||||
padding: 1.25rem;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -13,41 +13,46 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { IntlString } from '@hcengineering/platform'
|
||||
import presentation from '@hcengineering/presentation'
|
||||
import {
|
||||
Button,
|
||||
DropdownIntlItem,
|
||||
DropdownLabelsIntl,
|
||||
FocusHandler,
|
||||
Label,
|
||||
createFocusManager
|
||||
} from '@hcengineering/ui'
|
||||
import { Button, 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
|
||||
export let currentAvailable: boolean = true
|
||||
|
||||
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
|
||||
}
|
||||
]
|
||||
const items = currentAvailable
|
||||
? [
|
||||
{
|
||||
id: 'current',
|
||||
label: calendar.string.ThisEvent
|
||||
},
|
||||
{
|
||||
id: 'next',
|
||||
label: calendar.string.ThisAndNext
|
||||
},
|
||||
{
|
||||
id: 'all',
|
||||
label: calendar.string.AllEvents
|
||||
}
|
||||
]
|
||||
: [
|
||||
{
|
||||
id: 'next',
|
||||
label: calendar.string.ThisAndNext
|
||||
},
|
||||
{
|
||||
id: 'all',
|
||||
label: calendar.string.AllEvents
|
||||
}
|
||||
]
|
||||
|
||||
let selected = 'current'
|
||||
let selected = items[0].id
|
||||
</script>
|
||||
|
||||
<FocusHandler {manager} />
|
||||
|
@ -23,7 +23,6 @@ import CreateEvent from './components/CreateEvent.svelte'
|
||||
import DateTimePresenter from './components/DateTimePresenter.svelte'
|
||||
import DocReminder from './components/DocReminder.svelte'
|
||||
import EditEvent from './components/EditEvent.svelte'
|
||||
import EditRecEvent from './components/EditRecEvent.svelte'
|
||||
import EventPresenter from './components/EventPresenter.svelte'
|
||||
import Events from './components/Events.svelte'
|
||||
import IntegrationConnect from './components/IntegrationConnect.svelte'
|
||||
@ -68,6 +67,9 @@ async function deleteRecHandler (res: any, object: ReccuringInstance): Promise<v
|
||||
reminders: object.reminders,
|
||||
location: object.location,
|
||||
isCancelled: true,
|
||||
rdate: object.rdate,
|
||||
rules: object.rules,
|
||||
exdate: object.exdate,
|
||||
access: 'owner'
|
||||
},
|
||||
object._id
|
||||
@ -141,7 +143,6 @@ export enum CalendarMode {
|
||||
export default async (): Promise<Resources> => ({
|
||||
component: {
|
||||
EditEvent,
|
||||
EditRecEvent,
|
||||
PersonsPresenter,
|
||||
CalendarView,
|
||||
Events,
|
||||
|
@ -20,8 +20,7 @@ import { AnyComponent } from '@hcengineering/ui'
|
||||
export default mergeIds(calendarId, calendar, {
|
||||
component: {
|
||||
CreateEvent: '' as AnyComponent,
|
||||
EditEvent: '' as AnyComponent,
|
||||
EditRecEvent: '' as AnyComponent
|
||||
EditEvent: '' as AnyComponent
|
||||
},
|
||||
activity: {
|
||||
ReminderViewlet: '' as AnyComponent
|
||||
@ -54,6 +53,28 @@ export default mergeIds(calendarId, calendar, {
|
||||
RemoveRecEvent: '' as IntlString,
|
||||
ThisEvent: '' as IntlString,
|
||||
ThisAndNext: '' as IntlString,
|
||||
AllEvents: '' as IntlString
|
||||
AllEvents: '' as IntlString,
|
||||
NewEvent: '' as IntlString,
|
||||
TimeZone: '' as IntlString,
|
||||
Repeat: '' as IntlString,
|
||||
Every: '' as IntlString,
|
||||
On: '' as IntlString,
|
||||
OnUntil: '' as IntlString,
|
||||
Ends: '' as IntlString,
|
||||
Never: '' as IntlString,
|
||||
After: '' as IntlString,
|
||||
Day: '' as IntlString,
|
||||
Week: '' as IntlString,
|
||||
Month: '' as IntlString,
|
||||
Year: '' as IntlString,
|
||||
MondayShort: '' as IntlString,
|
||||
TuesdayShort: '' as IntlString,
|
||||
WednesdayShort: '' as IntlString,
|
||||
ThursdayShort: '' as IntlString,
|
||||
FridayShort: '' as IntlString,
|
||||
SaturdayShort: '' as IntlString,
|
||||
SundayShort: '' as IntlString,
|
||||
Times: '' as IntlString,
|
||||
AddParticipants: '' as IntlString
|
||||
}
|
||||
})
|
||||
|
@ -87,7 +87,7 @@ export interface Event extends AttachedDoc {
|
||||
* @public
|
||||
* use for an instance of a recurring event
|
||||
*/
|
||||
export interface ReccuringInstance extends Event {
|
||||
export interface ReccuringInstance extends ReccuringEvent {
|
||||
recurringEventId: string
|
||||
originalStartTime: number
|
||||
isCancelled?: boolean
|
||||
@ -123,7 +123,12 @@ const calendarPlugin = plugin(calendarId, {
|
||||
Calendar: '' as Asset,
|
||||
Location: '' as Asset,
|
||||
Reminder: '' as Asset,
|
||||
Notifications: '' as Asset
|
||||
Notifications: '' as Asset,
|
||||
Watch: '' as Asset,
|
||||
Description: '' as Asset,
|
||||
Participants: '' as Asset,
|
||||
Repeat: '' as Asset,
|
||||
Globe: '' as Asset
|
||||
},
|
||||
space: {
|
||||
// deprecated
|
||||
|
@ -24,7 +24,6 @@ function generateRecurringValues (
|
||||
): Timestamp[] {
|
||||
const values: Timestamp[] = []
|
||||
const currentDate = new Date(startDate)
|
||||
|
||||
switch (rule.freq) {
|
||||
case 'DAILY':
|
||||
generateDailyValues(rule, currentDate, values, from, to)
|
||||
@ -41,7 +40,6 @@ function generateRecurringValues (
|
||||
default:
|
||||
throw new Error('Invalid recurring rule frequency')
|
||||
}
|
||||
|
||||
return values
|
||||
}
|
||||
|
||||
@ -53,16 +51,11 @@ function generateDailyValues (
|
||||
to: Timestamp
|
||||
): void {
|
||||
const { count, endDate, interval } = rule
|
||||
const { bySecond, byMinute, byHour, bySetPos } = rule
|
||||
const { 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)))
|
||||
) {
|
||||
if (bySetPos == null || bySetPos.includes(getSetPos(currentDate))) {
|
||||
const res = currentDate.getTime()
|
||||
if (res > from) {
|
||||
values.push(res)
|
||||
@ -85,28 +78,31 @@ function generateWeeklyValues (
|
||||
to: Timestamp
|
||||
): void {
|
||||
const { count, endDate, interval } = rule
|
||||
const { bySecond, byMinute, byHour, byDay, wkst, bySetPos } = rule
|
||||
const { 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)
|
||||
const next = new Date(currentDate).setDate(currentDate.getDate() + (interval ?? 1) * 7)
|
||||
const end = new Date(new Date(currentDate).setDate(currentDate.getDate() + 7))
|
||||
let date = currentDate
|
||||
while (date < end) {
|
||||
if (
|
||||
(byDay == null || byDay.includes(getWeekday(date, wkst))) &&
|
||||
(bySetPos == null || bySetPos.includes(getSetPos(date)))
|
||||
) {
|
||||
const res = date.getTime()
|
||||
if (res > from) {
|
||||
values.push(res)
|
||||
}
|
||||
i++
|
||||
}
|
||||
i++
|
||||
date = new Date(date.setDate(date.getDate() + 1))
|
||||
if (count !== undefined && i === count) return
|
||||
if (endDate !== undefined && date.getTime() > endDate) return
|
||||
if (date.getTime() > to) return
|
||||
}
|
||||
|
||||
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
|
||||
currentDate = new Date(next)
|
||||
}
|
||||
}
|
||||
|
||||
@ -118,29 +114,32 @@ function generateMonthlyValues (
|
||||
to: Timestamp
|
||||
): void {
|
||||
const { count, endDate, interval } = rule
|
||||
const { bySecond, byMinute, byHour, byDay, byMonthDay, bySetPos, wkst } = rule
|
||||
const { 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)
|
||||
const next = new Date(currentDate).setMonth(currentDate.getMonth() + (interval ?? 1))
|
||||
const end = new Date(new Date(currentDate).setMonth(currentDate.getMonth() + 1))
|
||||
let date = currentDate
|
||||
while (date < end) {
|
||||
if (
|
||||
(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++
|
||||
}
|
||||
i++
|
||||
}
|
||||
date = new Date(date.setDate(date.getDate() + 1))
|
||||
|
||||
currentDate.setMonth(currentDate.getMonth() + (interval ?? 1))
|
||||
if (count !== undefined && i === count) break
|
||||
if (endDate !== undefined && currentDate.getTime() > endDate) break
|
||||
if (currentDate.getTime() > to) break
|
||||
if (count !== undefined && i === count) return
|
||||
if (endDate !== undefined && date.getTime() > endDate) return
|
||||
if (date.getTime() > to) return
|
||||
}
|
||||
currentDate = new Date(next)
|
||||
}
|
||||
}
|
||||
|
||||
@ -152,32 +151,34 @@ function generateYearlyValues (
|
||||
to: Timestamp
|
||||
): void {
|
||||
const { count, endDate, interval } = rule
|
||||
const { bySecond, byMinute, byHour, byDay, byMonthDay, byYearDay, byWeekNo, byMonth, bySetPos, wkst } = rule
|
||||
const { 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)
|
||||
const next = new Date(currentDate).setFullYear(currentDate.getFullYear() + (interval ?? 1))
|
||||
const end = new Date(new Date(currentDate).setFullYear(currentDate.getFullYear() + 1))
|
||||
let date = currentDate
|
||||
while (date < end) {
|
||||
if (
|
||||
(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++
|
||||
}
|
||||
i++
|
||||
date = new Date(date.setDate(date.getDate() + 1))
|
||||
if (count !== undefined && i === count) return
|
||||
if (endDate !== undefined && date.getTime() > endDate) return
|
||||
if (date.getTime() > to) return
|
||||
}
|
||||
|
||||
currentDate.setFullYear(currentDate.getFullYear() + (interval ?? 1))
|
||||
if (count !== undefined && i === count) break
|
||||
if (endDate !== undefined && currentDate.getTime() > endDate) break
|
||||
if (currentDate.getTime() > to) break
|
||||
currentDate = new Date(next)
|
||||
}
|
||||
}
|
||||
|
||||
@ -191,7 +192,10 @@ function getSetPos (date: Date): number {
|
||||
return Math.ceil((day + daysOffset) / 7)
|
||||
}
|
||||
|
||||
function getWeekday (date: Date, wkst?: string): string {
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export function getWeekday (date: Date, wkst?: string): string {
|
||||
const weekdays = ['SU', 'MO', 'TU', 'WE', 'TH', 'FR', 'SA']
|
||||
const weekday = weekdays[date.getDay()]
|
||||
|
||||
@ -247,7 +251,6 @@ function getReccuringEventInstances (
|
||||
const override = instances.find((p) => p.originalStartTime === i.date)
|
||||
return override === undefined
|
||||
})
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
|
@ -112,6 +112,7 @@ export { employeeByIdStore, employeesStore } from './utils'
|
||||
export {
|
||||
Channels,
|
||||
ChannelsEditor,
|
||||
ContactRefPresenter,
|
||||
ContactPresenter,
|
||||
ChannelsView,
|
||||
ChannelsDropdown,
|
||||
|
Loading…
Reference in New Issue
Block a user