mirror of
https://github.com/hcengineering/platform.git
synced 2025-03-22 07:28:39 +00:00
313 lines
9.0 KiB
Svelte
313 lines
9.0 KiB
Svelte
<!--
|
|
// 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 core, { AttachedData, Doc, Ref, SortingOrder, generateId, getCurrentAccount } from '@hcengineering/core'
|
|
import { Button, Component, EditBox, IconClose, Label, Scroller } from '@hcengineering/ui'
|
|
import { SpaceSelector, createQuery, getClient } from '@hcengineering/presentation'
|
|
import { ToDo, ToDoPriority, WorkSlot } from '@hcengineering/time'
|
|
import { Calendar, generateEventId } from '@hcengineering/calendar'
|
|
import tagsPlugin, { TagReference } from '@hcengineering/tags'
|
|
import { createEventDispatcher } from 'svelte'
|
|
import { VisibilityEditor } from '@hcengineering/calendar-resources'
|
|
import { StyledTextBox } from '@hcengineering/text-editor-resources'
|
|
import { PersonAccount } from '@hcengineering/contact'
|
|
import calendar from '@hcengineering/calendar-resources/src/plugin'
|
|
import task, { makeRank } from '@hcengineering/task'
|
|
import PriorityEditor from './PriorityEditor.svelte'
|
|
import DueDateEditor from './DueDateEditor.svelte'
|
|
import Workslots from './Workslots.svelte'
|
|
import time from '../plugin'
|
|
|
|
export let object: Doc | undefined
|
|
|
|
const acc = getCurrentAccount() as PersonAccount
|
|
const todo: AttachedData<ToDo> = {
|
|
workslots: 0,
|
|
title: '',
|
|
description: '',
|
|
priority: ToDoPriority.NoPriority,
|
|
attachedSpace: object?.space,
|
|
visibility: 'private',
|
|
user: acc.person,
|
|
rank: ''
|
|
}
|
|
|
|
const dispatch = createEventDispatcher()
|
|
const client = getClient()
|
|
|
|
export function canClose (): boolean {
|
|
return true
|
|
}
|
|
|
|
let loading = false
|
|
|
|
async function saveToDo (): Promise<void> {
|
|
loading = true
|
|
const ops = client.apply('todo-' + generateId())
|
|
const latestTodo = await ops.findOne(
|
|
time.class.ToDo,
|
|
{
|
|
user: acc.person,
|
|
doneOn: null
|
|
},
|
|
{
|
|
sort: { rank: SortingOrder.Ascending }
|
|
}
|
|
)
|
|
const id = await ops.addCollection(
|
|
time.class.ToDo,
|
|
time.space.ToDos,
|
|
object?._id ?? time.ids.NotAttached,
|
|
object?._class ?? time.class.ToDo,
|
|
'todos',
|
|
{
|
|
workslots: 0,
|
|
title: todo.title,
|
|
description: todo.description,
|
|
priority: todo.priority,
|
|
visibility: todo.visibility,
|
|
user: acc.person,
|
|
dueDate: todo.dueDate,
|
|
attachedSpace: todo.attachedSpace,
|
|
rank: makeRank(undefined, latestTodo?.rank)
|
|
}
|
|
)
|
|
for (const slot of slots) {
|
|
await ops.addCollection(time.class.WorkSlot, calendar.space.Calendar, id, time.class.ToDo, 'workslots', {
|
|
eventId: generateEventId(),
|
|
date: slot.date,
|
|
dueDate: slot.dueDate,
|
|
description: todo.description,
|
|
participants: [acc.person],
|
|
calendar: _calendar,
|
|
title: todo.title,
|
|
allDay: false,
|
|
access: 'owner',
|
|
visibility: todo.visibility === 'public' ? 'public' : 'freeBusy',
|
|
reminders: []
|
|
})
|
|
}
|
|
for (const tag of tags) {
|
|
await ops.addCollection(tagsPlugin.class.TagReference, time.space.ToDos, id, time.class.ToDo, 'labels', tag)
|
|
}
|
|
await ops.commit()
|
|
dispatch('close', true)
|
|
}
|
|
|
|
const currentUser = getCurrentAccount() as PersonAccount
|
|
let _calendar: Ref<Calendar> = `${currentUser._id}_calendar` as Ref<Calendar>
|
|
|
|
const q = createQuery()
|
|
q.query(calendar.class.ExternalCalendar, { default: true, hidden: false, createdBy: currentUser._id }, (res) => {
|
|
if (res.length > 0) {
|
|
_calendar = res[0]._id
|
|
}
|
|
})
|
|
|
|
let slots: WorkSlot[] = []
|
|
|
|
function removeSlot (e: CustomEvent<{ _id: Ref<WorkSlot> }>): void {
|
|
const index = slots.findIndex((p) => p._id === e.detail._id)
|
|
if (index !== -1) {
|
|
slots.splice(index, 1)
|
|
slots = slots
|
|
}
|
|
}
|
|
|
|
function createSlot (): void {
|
|
const defaultDuration = 30 * 60 * 1000
|
|
const now = Date.now()
|
|
const date = Math.ceil(now / (30 * 60 * 1000)) * (30 * 60 * 1000)
|
|
const dueDate = date + defaultDuration
|
|
slots.push({
|
|
eventId: generateEventId(),
|
|
date,
|
|
dueDate,
|
|
description: todo.description,
|
|
participants: [acc.person],
|
|
title: todo.title,
|
|
allDay: false,
|
|
access: 'owner',
|
|
visibility: todo.visibility,
|
|
reminders: [],
|
|
calendar: _calendar,
|
|
space: calendar.space.Calendar,
|
|
_id: generateId(),
|
|
_class: time.class.WorkSlot,
|
|
attachedTo: generateId(),
|
|
attachedToClass: time.class.ToDo,
|
|
collection: 'workslots',
|
|
modifiedOn: Date.now(),
|
|
modifiedBy: acc._id
|
|
})
|
|
slots = slots
|
|
}
|
|
|
|
function changeSlot (e: CustomEvent<{ startDate: number, dueDate: number, slot: Ref<WorkSlot> }>): void {
|
|
const { startDate, dueDate, slot } = e.detail
|
|
const workslot = slots.find((s) => s._id === slot)
|
|
if (workslot !== undefined) {
|
|
workslot.dueDate = dueDate
|
|
workslot.date = startDate
|
|
slots = slots
|
|
}
|
|
}
|
|
|
|
function changeDueSlot (e: CustomEvent<{ dueDate: number, slot: Ref<WorkSlot> }>): void {
|
|
const { dueDate, slot } = e.detail
|
|
const workslot = slots.find((s) => s._id === slot)
|
|
if (workslot !== undefined) {
|
|
workslot.dueDate = dueDate
|
|
slots = slots
|
|
}
|
|
}
|
|
|
|
let tags: AttachedData<TagReference>[] = []
|
|
</script>
|
|
|
|
<div class="eventPopup-container">
|
|
<div class="header flex-between">
|
|
<EditBox
|
|
bind:value={todo.title}
|
|
kind={'ghost-large'}
|
|
placeholder={time.string.AddTitle}
|
|
fullSize
|
|
focusable
|
|
autoFocus
|
|
focusIndex={10001}
|
|
/>
|
|
<div class="flex-row-center gap-1 flex-no-shrink ml-3">
|
|
<Button
|
|
id="card-close"
|
|
icon={IconClose}
|
|
kind={'ghost'}
|
|
size={'small'}
|
|
on:click={() => {
|
|
dispatch('close')
|
|
}}
|
|
/>
|
|
</div>
|
|
</div>
|
|
<Scroller>
|
|
<div class="block flex-no-shrink">
|
|
<div class="pb-4">
|
|
<StyledTextBox
|
|
alwaysEdit={true}
|
|
maxHeight="limited"
|
|
showButtons={false}
|
|
placeholder={calendar.string.Description}
|
|
bind:content={todo.description}
|
|
/>
|
|
</div>
|
|
<div class="flex-row-center gap-1-5 mb-1">
|
|
<PriorityEditor bind:value={todo.priority} />
|
|
<VisibilityEditor size="small" bind:value={todo.visibility} />
|
|
<DueDateEditor bind:value={todo.dueDate} />
|
|
</div>
|
|
</div>
|
|
<div class="block flex-no-shrink">
|
|
<div class="flex-row-center gap-1-5 mb-1">
|
|
<div>
|
|
<Label label={time.string.AddTo} />
|
|
</div>
|
|
<SpaceSelector
|
|
_class={task.class.Project}
|
|
query={{ archived: false, members: getCurrentAccount()._id }}
|
|
label={core.string.Space}
|
|
autoSelect={false}
|
|
allowDeselect
|
|
kind={'regular'}
|
|
size={'medium'}
|
|
focus={false}
|
|
bind:space={todo.attachedSpace}
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div class="block flex-no-shrink">
|
|
<div class="flex-row-center gap-1-5 mb-1">
|
|
<Component
|
|
is={tagsPlugin.component.DraftTagsEditor}
|
|
props={{ tags, targetClass: time.class.ToDo }}
|
|
on:update={(e) => {
|
|
tags = [...e.detail]
|
|
}}
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div class="block flex-gap-4 flex-no-shrink end">
|
|
<Workslots
|
|
bind:slots
|
|
shortcuts={false}
|
|
fixed={'createToDo'}
|
|
on:remove={removeSlot}
|
|
on:create={createSlot}
|
|
on:change={changeSlot}
|
|
on:dueChange={changeDueSlot}
|
|
/>
|
|
</div>
|
|
<div class="flex-row-reverse btn flex-no-shrink">
|
|
<Button
|
|
kind="primary"
|
|
{loading}
|
|
label={time.string.AddToDo}
|
|
on:click={saveToDo}
|
|
disabled={todo?.title === undefined || todo?.title === ''}
|
|
/>
|
|
</div>
|
|
</Scroller>
|
|
</div>
|
|
|
|
<style lang="scss">
|
|
.eventPopup-container {
|
|
display: flex;
|
|
flex-direction: column;
|
|
padding-top: 2rem;
|
|
padding-bottom: 2rem;
|
|
max-width: 40rem;
|
|
min-width: 40rem;
|
|
min-height: 0;
|
|
background: var(--theme-popup-color);
|
|
border-radius: 1rem;
|
|
box-shadow: var(--theme-popup-shadow);
|
|
|
|
.header {
|
|
flex-shrink: 0;
|
|
padding-right: 2rem;
|
|
padding-left: 2rem;
|
|
}
|
|
|
|
.btn {
|
|
padding-top: 1rem;
|
|
padding-right: 1rem;
|
|
padding-left: 1rem;
|
|
}
|
|
|
|
.block {
|
|
display: flex;
|
|
flex-direction: column;
|
|
min-width: 0;
|
|
min-height: 0;
|
|
padding-top: 1rem;
|
|
padding-bottom: 1rem;
|
|
padding-right: 2rem;
|
|
padding-left: 2rem;
|
|
|
|
&:not(.end) {
|
|
border-bottom: 1px solid var(--theme-divider-color);
|
|
}
|
|
}
|
|
}
|
|
</style>
|