UBER-676 UBER-716 UBER-717 UBER-719 (#3582)

Signed-off-by: Denis Bykhov <bykhov.denis@gmail.com>
This commit is contained in:
Denis Bykhov 2023-08-13 22:28:12 +06:00 committed by GitHub
parent de03ecb895
commit ccaf66f179
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
34 changed files with 1975 additions and 668 deletions

View File

@ -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
})

View File

@ -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">

View File

@ -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)'} />

View File

@ -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>

View 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(' ', '&nbsp;')
.replaceAll('<', '&lt;')
.replaceAll('>', '&gt;')
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>

View 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>

View 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>

View File

@ -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'

View File

@ -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

View File

@ -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"
}
}

View File

@ -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": "Добавить участников"
}
}

View File

@ -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`))

View File

@ -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",

View 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(' ', '&nbsp;')
.replaceAll('<', '&lt;')
.replaceAll('>', '&gt;')
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>

View File

@ -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)}

View File

@ -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>

View 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>

View File

@ -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)}

View File

@ -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}

View File

@ -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) =>

View File

@ -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>

View File

@ -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>

View File

@ -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}

View File

@ -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>

View File

@ -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>

View File

@ -0,0 +1,67 @@
<!--
// Copyright © 2023 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<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>

View File

@ -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}

View 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>

View File

@ -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} />

View File

@ -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,

View File

@ -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
}
})

View File

@ -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

View File

@ -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
}

View File

@ -112,6 +112,7 @@ export { employeeByIdStore, employeesStore } from './utils'
export {
Channels,
ChannelsEditor,
ContactRefPresenter,
ContactPresenter,
ChannelsView,
ChannelsDropdown,