feat(planner): drag-n-drop (#5031)

Signed-off-by: Eduard Aksamitov <e@euaaaio.ru>
This commit is contained in:
Eduard Aksamitov 2024-03-22 11:37:24 +03:00 committed by GitHub
parent 5fd60d1096
commit 9d52ddbbea
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
19 changed files with 534 additions and 142 deletions

View File

@ -50,6 +50,7 @@
"@hcengineering/notification": "^0.6.16",
"@hcengineering/model-tracker": "^0.6.0",
"@hcengineering/time": "^0.6.0",
"@hcengineering/time-resources": "^0.6.0"
"@hcengineering/time-resources": "^0.6.0",
"@hcengineering/rank": "^0.6.0"
}
}

View File

@ -26,10 +26,23 @@ import {
type Space,
type Timestamp,
type Type,
DateRangeMode
DateRangeMode,
IndexKind
} from '@hcengineering/core'
import lead from '@hcengineering/lead'
import { Collection, Mixin, Model, Prop, TypeRef, TypeString, UX, type Builder, TypeDate } from '@hcengineering/model'
import {
Collection,
Mixin,
Model,
Prop,
TypeRef,
TypeString,
UX,
type Builder,
TypeDate,
Hidden,
Index
} from '@hcengineering/model'
import { TEvent } from '@hcengineering/model-calendar'
import core, { TAttachedDoc, TClass, TDoc, TType } from '@hcengineering/model-core'
import tracker from '@hcengineering/model-tracker'
@ -51,7 +64,8 @@ import {
type WorkSlot
} from '@hcengineering/time'
import { type Resource } from '@hcengineering/platform'
import type { Resource } from '@hcengineering/platform'
import type { Rank } from '@hcengineering/task'
import time from './plugin'
import task from '@hcengineering/task'
@ -107,6 +121,10 @@ export class TToDO extends TAttachedDoc implements ToDo {
@Prop(Collection(tags.class.TagReference, tags.string.TagLabel), tags.string.Tags)
labels?: number | undefined
@Index(IndexKind.Indexed)
@Hidden()
rank!: Rank
}
@Model(time.class.ProjectToDo, time.class.ToDo)

View File

@ -14,13 +14,14 @@
//
import { type PersonAccount } from '@hcengineering/contact'
import { type Account, type Doc, type Ref, TxOperations } from '@hcengineering/core'
import { type Account, type Doc, type Ref, SortingOrder, TxOperations } from '@hcengineering/core'
import {
type MigrateOperation,
type MigrationClient,
type MigrationUpgradeClient,
createOrUpdate
} from '@hcengineering/model'
import { makeRank } from '@hcengineering/rank'
import core from '@hcengineering/model-core'
import task from '@hcengineering/task'
import tags from '@hcengineering/tags'
@ -37,12 +38,14 @@ export async function migrateWorkSlots (client: TxOperations): Promise<void> {
const now = Date.now()
const todos = new Map<Ref<Doc>, Ref<ToDo>>()
const count = new Map<Ref<ToDo>, number>()
let rank = makeRank(undefined, undefined)
for (const oldWorkSlot of oldWorkSlots) {
const todo = todos.get(oldWorkSlot.attachedTo)
if (todo === undefined) {
const acc = oldWorkSlot.space.replace('_calendar', '') as Ref<Account>
const account = (await client.findOne(core.class.Account, { _id: acc })) as PersonAccount
if (account.person !== undefined) {
rank = makeRank(undefined, rank)
const todo = await client.addCollection(
time.class.ProjectToDo,
time.space.ToDos,
@ -57,7 +60,8 @@ export async function migrateWorkSlots (client: TxOperations): Promise<void> {
workslots: 0,
priority: ToDoPriority.NoPriority,
user: account.person,
visibility: 'public'
visibility: 'public',
rank
}
)
await client.update(oldWorkSlot, {
@ -105,6 +109,44 @@ async function migrateTodosSpace (client: TxOperations): Promise<void> {
}
}
async function migrateTodosRanks (client: TxOperations): Promise<void> {
const doneTodos = await client.findAll(
time.class.ToDo,
{
rank: { $exists: false },
doneOn: null
},
{
sort: { modifiedOn: SortingOrder.Ascending }
}
)
let doneTodoRank = makeRank(undefined, undefined)
for (const todo of doneTodos) {
await client.update(todo, {
rank: doneTodoRank
})
doneTodoRank = makeRank(undefined, doneTodoRank)
}
const undoneTodos = await client.findAll(
time.class.ToDo,
{
rank: { $exists: false },
doneOn: { $ne: null }
},
{
sort: { doneOn: SortingOrder.Ascending }
}
)
let undoneTodoRank = makeRank(undefined, undefined)
for (const todo of undoneTodos) {
await client.update(todo, {
rank: undoneTodoRank
})
undoneTodoRank = makeRank(undefined, undoneTodoRank)
}
}
async function createDefaultSpace (tx: TxOperations): Promise<void> {
const current = await tx.findOne(core.class.Space, {
_id: time.space.ToDos
@ -161,5 +203,6 @@ export const timeOperation: MigrateOperation = {
)
await migrateWorkSlots(tx)
await migrateTodosSpace(tx)
await migrateTodosRanks(tx)
}
}

View File

@ -457,7 +457,7 @@
}
}
&.large {
padding: var(--spacing-2) var(--spacing-1_5) var(--spacing-1) var(--spacing-2);
padding: var(--spacing-2) var(--spacing-1_5) var(--spacing-2) var(--spacing-2);
min-height: var(--global-extra-large-Size);
.hulyAccordionItem-header__label-wrapper {
@ -526,6 +526,23 @@
}
}
.hulyToDoLine-draggable {
position: relative;
&.is-dragging-over-up::before {
position: absolute;
content: '';
inset: 0;
border-top: 1px solid var(--global-focus-BorderColor);
}
&.is-dragging-over-down::before {
position: absolute;
content: '';
inset: 0;
border-bottom: 1px solid var(--global-focus-BorderColor);
}
}
/* ToDo Line */
.hulyToDoLine-container {
display: flex;
@ -567,6 +584,7 @@
color: inherit;
border: none;
outline: none;
cursor: grab;
&.isNew::after {
position: absolute;

View File

@ -60,6 +60,7 @@
"@hcengineering/workbench": "^0.6.9",
"@hcengineering/document": "^0.6.0",
"@hcengineering/time": "^0.6.0",
"@hcengineering/rank": "^0.6.0",
"@tiptap/core": "^2.1.12",
"slugify": "^1.6.6"
}

View File

@ -1,11 +1,12 @@
<script lang="ts">
import { Person } from '@hcengineering/contact'
import { AssigneePopup, EmployeePresenter } from '@hcengineering/contact-resources'
import { Doc, Ref } from '@hcengineering/core'
import { Doc, Ref, SortingOrder } from '@hcengineering/core'
import { MessageBox, createQuery, getClient } from '@hcengineering/presentation'
import { NodeViewContent, NodeViewProps, NodeViewWrapper } from '@hcengineering/text-editor'
import { CheckBox, getEventPositionElement, showPopup } from '@hcengineering/ui'
import time, { ToDo, ToDoPriority } from '@hcengineering/time'
import { makeRank } from '@hcengineering/rank'
import document from '../../plugin'
@ -75,6 +76,20 @@
await ops.remove(todo)
}
const doneOn = node.attrs.checked === true ? Date.now() : null
const latestTodoItem = await ops.findOne(
time.class.ToDo,
{
user,
doneOn: doneOn === null ? null : { $ne: null }
},
{
sort: { rank: SortingOrder.Ascending }
}
)
const rank = makeRank(undefined, latestTodoItem?.rank)
const id = await ops.addCollection(time.class.ProjectToDo, time.space.ToDos, object._id, object._class, 'todos', {
attachedSpace: object.space,
title,
@ -83,7 +98,8 @@
workslots: 0,
priority: ToDoPriority.NoPriority,
visibility: 'public',
doneOn: node.attrs.checked === true ? Date.now() : null
doneOn,
rank
})
await ops.commit()

View File

@ -1,30 +1,45 @@
<script lang="ts">
import { ActionIcon, IconAdd, showPopup, ModernEditbox } from '@hcengineering/ui'
import { SortingOrder, getCurrentAccount } from '@hcengineering/core'
import { PersonAccount } from '@hcengineering/contact'
import { getCurrentAccount } from '@hcengineering/core'
import { getClient } from '@hcengineering/presentation'
import { ActionIcon, EditBox, IconAdd, showPopup, ModernEditbox } from '@hcengineering/ui'
import { ToDoPriority } from '@hcengineering/time'
import time from '../plugin'
import { getClient } from '@hcengineering/presentation'
import CreateToDoPopup from './CreateToDoPopup.svelte'
import time from '../plugin'
import { makeRank } from '@hcengineering/task'
export let fullSize: boolean = false
let value: string = ''
async function save () {
const client = getClient()
const acc = getCurrentAccount() as PersonAccount
async function save (): Promise<void> {
let [name, description] = value.split('//')
name = name.trim()
if (name.length === 0) return
description = description?.trim() ?? ''
const client = getClient()
const acc = getCurrentAccount() as PersonAccount
await client.addCollection(time.class.ToDo, time.space.ToDos, time.ids.NotAttached, time.class.ToDo, 'todos', {
const ops = client.apply('todo')
const latestTodo = await ops.findOne(
time.class.ToDo,
{
user: acc.person,
doneOn: null
},
{
sort: { rank: SortingOrder.Ascending }
}
)
await ops.addCollection(time.class.ToDo, time.space.ToDos, time.ids.NotAttached, time.class.ToDo, 'todos', {
title: name,
description,
user: acc.person,
workslots: 0,
priority: ToDoPriority.NoPriority,
visibility: 'private'
visibility: 'private',
rank: makeRank(undefined, latestTodo?.rank)
})
await ops.commit()
clear()
}

View File

@ -13,7 +13,7 @@
// limitations under the License.
-->
<script lang="ts">
import core, { AttachedData, Doc, Ref, generateId, getCurrentAccount } from '@hcengineering/core'
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'
@ -24,7 +24,7 @@
import { StyledTextBox } from '@hcengineering/text-editor'
import { PersonAccount } from '@hcengineering/contact'
import calendar from '@hcengineering/calendar-resources/src/plugin'
import task from '@hcengineering/task'
import task, { makeRank } from '@hcengineering/task'
import PriorityEditor from './PriorityEditor.svelte'
import DueDateEditor from './DueDateEditor.svelte'
import Workslots from './Workslots.svelte'
@ -40,7 +40,8 @@
priority: ToDoPriority.NoPriority,
attachedSpace: object?.space,
visibility: 'private',
user: acc.person
user: acc.person,
rank: ''
}
const dispatch = createEventDispatcher()
@ -54,7 +55,18 @@
async function saveToDo (): Promise<void> {
loading = true
const id = await client.addCollection(
const ops = client.apply('todo')
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,
@ -68,12 +80,13 @@
visibility: todo.visibility,
user: acc.person,
dueDate: todo.dueDate,
attachedSpace: todo.attachedSpace
attachedSpace: todo.attachedSpace,
rank: makeRank(undefined, latestTodo?.rank)
}
)
const space = `${acc._id}_calendar` as Ref<Calendar>
for (const slot of slots) {
await client.addCollection(time.class.WorkSlot, space, id, time.class.ToDo, 'workslots', {
await ops.addCollection(time.class.WorkSlot, space, id, time.class.ToDo, 'workslots', {
eventId: generateEventId(),
date: slot.date,
dueDate: slot.dueDate,
@ -87,8 +100,9 @@
})
}
for (const tag of tags) {
await client.addCollection(tagsPlugin.class.TagReference, time.space.ToDos, id, time.class.ToDo, 'labels', tag)
await ops.addCollection(tagsPlugin.class.TagReference, time.space.ToDos, id, time.class.ToDo, 'labels', tag)
}
await ops.commit()
dispatch('close', true)
}

View File

@ -1,3 +1,18 @@
<!--
// Copyright © 2024 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, afterUpdate } from 'svelte'
import calendar, { Calendar, generateEventId } from '@hcengineering/calendar'
@ -6,13 +21,13 @@
import { getClient } from '@hcengineering/presentation'
import { TagElement } from '@hcengineering/tags'
import { Separator, defineSeparators } from '@hcengineering/ui'
import { ToDo } from '@hcengineering/time'
import { ToDosMode } from '..'
import time from '../plugin'
import { timeSeparators } from '../utils'
import PlanningCalendar from './PlanningCalendar.svelte'
import ToDos from './ToDos.svelte'
import ToDosNavigator from './ToDosNavigator.svelte'
import ToDos from './ToDos.svelte'
import { timeSeparators } from '../utils'
import { dragging } from '../dragging'
import time from '../plugin'
export let visibleNav: boolean = true
export let navFloat: boolean = false
@ -26,12 +41,12 @@
let currentDate: Date = new Date()
let dragItem: ToDo | undefined = undefined
$: dragItem = $dragging.item
const client = getClient()
async function drop (e: CustomEvent<any>) {
if (dragItem === undefined) return
if (dragItem === null) return
const doc = dragItem
const date = e.detail.date.getTime()
const currentUser = getCurrentAccount() as PersonAccount
@ -79,14 +94,7 @@
/>
{/if}
<div class="flex-col clear-mins">
<ToDos
{mode}
{tag}
bind:isVisiblePlannerNav
bind:currentDate
on:dragstart={(e) => (dragItem = e.detail)}
on:dragend={() => (dragItem = undefined)}
/>
<ToDos {mode} {tag} bind:isVisiblePlannerNav bind:currentDate />
</div>
<Separator name={'time'} float={navFloat} index={1} color={'transparent'} separatorSize={0} short />
{/if}

View File

@ -25,7 +25,7 @@
import time from '../plugin'
import IconSun from './icons/Sun.svelte'
export let dragItem: ToDo | undefined = undefined
export let dragItem: ToDo | null = null
export let currentDate: Date = new Date()
export let displayedDaysCount = 1
export let createComponent: AnyComponent | undefined = calendar.component.CreateEvent
@ -151,8 +151,8 @@
}
}
function clear (dragItem: ToDo | undefined) {
if (dragItem === undefined) {
function clear (dragItem: ToDo | null) {
if (dragItem === null) {
raw = raw.filter((p) => p._id !== dragItemId)
all = getAllEvents(raw, from, to)
objects = hidePrivateEvents(all, $calendarStore)
@ -216,7 +216,7 @@
events={objects}
{displayedDaysCount}
startFromWeekStart={false}
clearCells={dragItem !== undefined}
clearCells={dragItem !== null}
{dragItemId}
on:dragEnter={dragEnter}
on:dragleave={dragLeave}

View File

@ -0,0 +1,113 @@
<!--
// Copyright © 2024 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 type { WithLookup } from '@hcengineering/core'
import type { ToDo } from '@hcengineering/time'
import { createEventDispatcher } from 'svelte'
import { dragging } from '../dragging'
import time from '../plugin'
export let todo: WithLookup<ToDo>
export let index: number
export let groupName: IntlString | null
export let projectId: string | false | null
const dispatch = createEventDispatcher()
let isDragging: boolean = false
let draggingOverClass: string = ''
$: draggingItemIndex = $dragging.itemIndex
$: draggingGroupName = $dragging.groupName
$: draggingProjectId = $dragging.projectId
$: draggingOverIndex = $dragging.overItemIndex
$: draggingOverGroupName = $dragging.overGroupName
$: draggingOverProjectId = $dragging.overProjectId
$: isDraggable = groupName !== time.string.Done
$: {
if (
isDraggable &&
draggingItemIndex !== null &&
draggingOverGroupName === groupName &&
draggingGroupName === groupName &&
draggingOverProjectId === projectId &&
draggingProjectId === projectId &&
index === draggingOverIndex
) {
draggingOverClass = index < draggingItemIndex ? 'is-dragging-over-up' : 'is-dragging-over-down'
} else {
draggingOverClass = ''
}
}
function handleDragStart (event: DragEvent): void {
if (event.dataTransfer) {
event.dataTransfer.effectAllowed = 'all'
}
isDragging = true
dragging.update((state) => ({
...state,
item: todo,
itemIndex: index,
groupName,
projectId
}))
}
function handleDragEnd (): void {
isDragging = false
dragging.set({
item: null,
itemIndex: null,
groupName: null,
projectId: null,
overItemIndex: null,
overGroupName: null,
overProjectId: null
})
}
function handleDragOver (): void {
if (!isDraggable) return
dragging.update((state) => ({
...state,
overItemIndex: index,
overGroupName: groupName,
overProjectId: projectId
}))
}
function handleDrop (event: DragEvent): void {
if (!isDraggable) return
dispatch('drop', { event, index })
}
</script>
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div
class="hulyToDoLine-draggable {draggingOverClass} step-tb125"
class:dragging={isDragging}
draggable={true}
on:dragstart={handleDragStart}
on:dragend={handleDragEnd}
on:dragover|preventDefault={handleDragOver}
on:drop={handleDrop}
>
<slot />
</div>

View File

@ -1,3 +1,17 @@
<!--
// Copyright © 2024 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 { SortingOrder, WithLookup } from '@hcengineering/core'
import { createQuery, getClient } from '@hcengineering/presentation'
@ -5,7 +19,6 @@
import { Component, IconMoreV2, Spinner, showPanel, Icon } from '@hcengineering/ui'
import { showMenu } from '@hcengineering/view-resources'
import time, { ToDo, ToDoPriority, WorkSlot } from '@hcengineering/time'
import { createEventDispatcher } from 'svelte'
import plugin from '../plugin'
import ToDoDuration from './ToDoDuration.svelte'
import WorkItemPresenter from './WorkItemPresenter.svelte'
@ -15,13 +28,10 @@
export let todo: WithLookup<ToDo>
export let size: 'small' | 'large' = 'small'
export let planned: boolean = true
export let draggable: boolean = true
export let isNew: boolean = false
const dispatch = createEventDispatcher()
const client = getClient()
let updating: Promise<any> | undefined = undefined
let isDrag: boolean = false
async function markDone (): Promise<void> {
await updating
@ -51,17 +61,6 @@
})
}
function dragStart (todo: ToDo, event: DragEvent & { currentTarget: EventTarget & HTMLButtonElement }): void {
event.currentTarget.classList.add('dragged')
if (event.dataTransfer) event.dataTransfer.effectAllowed = 'all'
dispatch('dragstart', todo)
}
function dragEnd (event: DragEvent & { currentTarget: EventTarget & HTMLButtonElement }): void {
event.currentTarget.classList.remove('dragged')
dispatch('dragend')
}
function open (e: MouseEvent): void {
showPanel(time.component.EditToDo, todo._id, todo._class, 'content')
}
@ -73,20 +72,10 @@
class="hulyToDoLine-container {size}"
class:hovered
class:isDone
class:isDrag
on:click|stopPropagation={open}
on:contextmenu={(e) => {
showMenu(e, { object: todo })
}}
{draggable}
on:dragstart={(e) => {
isDrag = true
dragStart(todo, e)
}}
on:dragend={(e) => {
isDrag = false
dragEnd(e)
}}
>
<div class="flex-row-top flex-grow flex-gap-2">
<div class="flex-row-center flex-no-shrink">

View File

@ -1,21 +1,33 @@
<!--
// Copyright © 2024 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 { WithLookup, IdMap, Ref, Space } from '@hcengineering/core'
import { IntlString } from '@hcengineering/platform'
import { ToDo, WorkSlot } from '@hcengineering/time'
import time from '../plugin'
import { createEventDispatcher } from 'svelte'
import type { WithLookup, IdMap, Ref, Space } from '@hcengineering/core'
import type { ToDo, WorkSlot } from '@hcengineering/time'
import type { IntlString } from '@hcengineering/platform'
import type { Project } from '@hcengineering/tracker'
import type { ToDosMode } from '..'
import { AccordionItem } from '@hcengineering/ui'
import ToDoDraggable from './ToDoDraggable.svelte'
import ToDoDuration from './ToDoDuration.svelte'
import ToDoElement from './ToDoElement.svelte'
import {
AccordionItem,
IconWithEmoji,
getPlatformColorDef,
getPlatformColorForTextDef,
themeStore
} from '@hcengineering/ui'
import { ToDosMode } from '..'
import tracker, { Project } from '@hcengineering/tracker'
import view from '@hcengineering/view'
import time from '../plugin'
import { dragging } from '../dragging'
import ToDoProjectGroup from './ToDoProjectGroup.svelte'
import { getClient } from '@hcengineering/presentation'
import { makeRank } from '@hcengineering/task'
export let mode: ToDosMode
export let title: IntlString
@ -25,8 +37,6 @@
export let largeSize: boolean = false
export let projects: IdMap<Project>
const dispatch = createEventDispatcher()
function getAllWorkslots (todos: WithLookup<ToDo>[]): WorkSlot[] {
const workslots: WorkSlot[] = []
for (const todo of todos) {
@ -56,9 +66,28 @@
withoutProject = wp
return _groups
}
const hasProject = (proj: Ref<Space> | undefined): boolean => {
return (proj && projects.has(proj as Ref<Project>)) ?? false
}
const client = getClient()
$: draggingItem = $dragging.item
$: draggingItemIndex = $dragging.itemIndex
async function handleDrop (event: CustomEvent<{ event: DragEvent, index: number }>): Promise<void> {
if (draggingItem === null || draggingItemIndex === null) return
const droppingIndex = event.detail.index
const [previousItem, nextItem] = [
todos[draggingItemIndex < droppingIndex ? droppingIndex : droppingIndex - 1],
todos[draggingItemIndex < droppingIndex ? droppingIndex + 1 : droppingIndex]
]
const newRank = makeRank(previousItem?.rank, nextItem?.rank)
await client.update(draggingItem, { rank: newRank })
}
</script>
{#if showTitle}
@ -68,7 +97,7 @@
bottomSpace={false}
counter={todos.length}
duration={showDuration}
isOpen
isOpen={title !== time.string.Done}
fixHeader
background={'var(--theme-navpanel-color)'}
>
@ -77,48 +106,31 @@
</svelte:fragment>
{#if groups}
{#each groups as group}
<AccordionItem
icon={group.icon === view.ids.IconWithEmoji ? IconWithEmoji : group.icon ?? tracker.icon.Home}
iconProps={group.icon === view.ids.IconWithEmoji
? { icon: group.color }
: {
fill:
group.color !== undefined
? getPlatformColorDef(group.color, $themeStore.dark).icon
: getPlatformColorForTextDef(group.name, $themeStore.dark).icon
}}
title={group.name}
size={'medium'}
isOpen
nested
>
{#each todos.filter((td) => td.attachedSpace === group._id) as todo}
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div class="step-tb125" draggable={true} on:dragend on:dragstart={() => dispatch('dragstart', todo)}>
<ToDoElement {todo} size={largeSize ? 'large' : 'small'} planned={mode !== 'unplanned'} />
</div>
{/each}
</AccordionItem>
<ToDoProjectGroup
todos={todos.filter((td) => td.attachedSpace === group._id)}
project={group}
groupName={title}
{largeSize}
{mode}
/>
{/each}
{/if}
{#if withoutProject}
<AccordionItem label={time.string.WithoutProject} size={'medium'} isOpen nested>
{#each todos.filter((td) => !hasProject(td.attachedSpace)) as todo}
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div class="step-tb125" draggable={true} on:dragend on:dragstart={() => dispatch('dragstart', todo)}>
<ToDoElement {todo} size={largeSize ? 'large' : 'small'} planned={mode !== 'unplanned'} />
</div>
{/each}
</AccordionItem>
<ToDoProjectGroup
todos={todos.filter((td) => !hasProject(td.attachedSpace))}
project={false}
groupName={title}
{largeSize}
{mode}
/>
{/if}
</AccordionItem>
{:else}
<div class="flex-col p-4 w-full">
{#each todos as todo}
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div class="step-tb125" draggable={true} on:dragend on:dragstart={() => dispatch('dragstart', todo)}>
{#each todos as todo, index}
<ToDoDraggable {todo} {index} groupName={title} projectId={false} on:drop={handleDrop}>
<ToDoElement {todo} size={largeSize ? 'large' : 'small'} planned={mode !== 'unplanned'} />
</div>
</ToDoDraggable>
{/each}
</div>
{/if}

View File

@ -0,0 +1,90 @@
<!--
// Copyright © 2024 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 type { Project } from '@hcengineering/tracker'
import type { ToDo } from '@hcengineering/time'
import type { ToDosMode } from '..'
import {
AccordionItem,
IconWithEmoji,
getPlatformColorDef,
getPlatformColorForTextDef,
themeStore
} from '@hcengineering/ui'
import { getClient } from '@hcengineering/presentation'
import { makeRank } from '@hcengineering/task'
import tracker from '@hcengineering/tracker'
import view from '@hcengineering/view'
import ToDoDraggable from './ToDoDraggable.svelte'
import ToDoElement from './ToDoElement.svelte'
import time from '../plugin'
import { dragging } from '../dragging'
export let todos: ToDo[]
export let project: Project | false | undefined = undefined
export let mode: ToDosMode
export let groupName: IntlString
export let largeSize: boolean = false
const client = getClient()
let projectId: string | false
$: icon = project
? project.icon === view.ids.IconWithEmoji
? IconWithEmoji
: project.icon ?? tracker.icon.Home
: undefined
$: iconProps = project
? project.icon === view.ids.IconWithEmoji
? { icon: project.color }
: {
fill:
project.color !== undefined
? getPlatformColorDef(project.color, $themeStore.dark).icon
: getPlatformColorForTextDef(project.name, $themeStore.dark).icon
}
: undefined
$: title = project ? project.name : undefined
$: label = title ? undefined : time.string.WithoutProject
$: projectId = project ? project._id : false
$: draggingItem = $dragging.item
$: draggingItemIndex = $dragging.itemIndex
async function handleDrop (event: CustomEvent<{ event: DragEvent, index: number }>): Promise<void> {
if (draggingItem === null || draggingItemIndex === null) return
const droppingIndex = event.detail.index
const [previousItem, nextItem] = [
todos[draggingItemIndex < droppingIndex ? droppingIndex : droppingIndex - 1],
todos[draggingItemIndex < droppingIndex ? droppingIndex + 1 : droppingIndex]
]
const newRank = makeRank(previousItem?.rank, nextItem?.rank)
await client.update(draggingItem, { rank: newRank })
}
</script>
<AccordionItem {icon} {iconProps} {title} {label} size={'medium'} isOpen nested>
{#each todos as todo, index}
<ToDoDraggable {todo} {index} {groupName} {projectId} on:drop={handleDrop}>
<ToDoElement {todo} size={largeSize ? 'large' : 'small'} planned={mode !== 'unplanned'} />
</ToDoDraggable>
{/each}
</AccordionItem>

View File

@ -1,21 +1,39 @@
<!--
// Copyright © 2024 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 { PersonAccount } from '@hcengineering/contact'
import { DocumentQuery, Ref, SortingOrder, WithLookup, getCurrentAccount, IdMap, toIdMap } from '@hcengineering/core'
import { IntlString } from '@hcengineering/platform'
import { createQuery } from '@hcengineering/presentation'
import type { DocumentQuery, Ref, WithLookup, IdMap } from '@hcengineering/core'
import type { ToDo, WorkSlot } from '@hcengineering/time'
import type { PersonAccount } from '@hcengineering/contact'
import type { IntlString } from '@hcengineering/platform'
import type { TagElement } from '@hcengineering/tags'
import type { Project } from '@hcengineering/tracker'
import type { ToDosMode } from '..'
import { Scroller, areDatesEqual, todosSP, defaultSP, Header, ButtonIcon, Label } from '@hcengineering/ui'
import { ToDo, WorkSlot } from '@hcengineering/time'
import { ToDosMode } from '..'
import { getCurrentAccount, toIdMap, SortingOrder } from '@hcengineering/core'
import { createQuery } from '@hcengineering/presentation'
import tracker from '@hcengineering/tracker'
import tags from '@hcengineering/tags'
import view from '@hcengineering/view-resources/src/plugin'
import { getNearest } from '../utils'
import MenuClose from './icons/MenuClose.svelte'
import MenuOpen from './icons/MenuOpen.svelte'
import CreateToDo from './CreateToDo.svelte'
import ToDoGroup from './ToDoGroup.svelte'
import MenuClose from './icons/MenuClose.svelte'
import MenuOpen from './icons/MenuOpen.svelte'
import IconDiff from './icons/Diff.svelte'
import time from '../plugin'
import tags, { TagElement } from '@hcengineering/tags'
import tracker, { Project } from '@hcengineering/tracker'
import view from '@hcengineering/view-resources/src/plugin'
export let mode: ToDosMode
export let tag: Ref<TagElement> | undefined
@ -29,7 +47,6 @@
const doneQuery = createQuery()
const inboxQuery = createQuery()
const activeQuery = createQuery()
const tagsQuery = createQuery()
const projectsQuery = createQuery()
@ -129,7 +146,7 @@
},
{
limit: 200,
sort: { modifiedOn: SortingOrder.Ascending },
sort: { rank: SortingOrder.Ascending },
lookup: { _id: { workslots: time.class.WorkSlot } }
}
)
@ -147,7 +164,7 @@
},
{
limit: 200,
sort: { modifiedOn: SortingOrder.Ascending }
sort: { rank: SortingOrder.Ascending }
}
)
} else {
@ -245,8 +262,6 @@
})
}
}
todos.sort((a, b) => (a.nearest?.date ?? 0) - (b.nearest?.date ?? 0))
scheduled.sort((a, b) => (a.nearest?.date ?? 0) - (b.nearest?.date ?? 0))
groups.set(
time.string.ToDos,
todos.map((p) => p.todo)
@ -309,8 +324,6 @@
{mode}
{projects}
{largeSize}
on:dragstart
on:dragend
/>
{/each}
</Scroller>
@ -322,7 +335,6 @@
display: flex;
flex-direction: column;
width: 100%;
// height: 100%;
min-width: 0;
min-height: 0;
}

View File

@ -0,0 +1,23 @@
import type { IntlString } from '@hcengineering/platform'
import type { ToDo } from '@hcengineering/time'
import { writable } from 'svelte/store'
interface ToDoDragging {
item: ToDo | null
itemIndex: number | null
groupName: IntlString | null
projectId: string | false | null
overItemIndex: number | null
overGroupName: IntlString | null
overProjectId: string | false | null
}
export const dragging = writable<ToDoDragging>({
item: null,
itemIndex: null,
groupName: null,
projectId: null,
overItemIndex: null,
overGroupName: null,
overProjectId: null
})

View File

@ -37,6 +37,7 @@
"@hcengineering/calendar": "^0.6.17",
"@hcengineering/task": "^0.6.13",
"@hcengineering/platform": "^0.6.9",
"@hcengineering/ui": "^0.6.11"
"@hcengineering/ui": "^0.6.11",
"@hcengineering/rank": "^0.6.0"
}
}

View File

@ -19,6 +19,7 @@ import { IntlString, plugin } from '@hcengineering/platform'
import { Event, Visibility } from '@hcengineering/calendar'
import { AnyComponent } from '@hcengineering/ui'
import { Person } from '@hcengineering/contact'
import type { Rank } from '@hcengineering/rank'
/**
* @public
@ -49,6 +50,7 @@ export interface ToDo extends AttachedDoc {
user: Ref<Person>
attachedSpace?: Ref<Space>
labels?: number
rank: Rank
}
/**

View File

@ -21,6 +21,7 @@ import core, {
Doc,
DocumentUpdate,
Ref,
SortingOrder,
Status,
Tx,
TxCUD,
@ -38,7 +39,7 @@ import {
getNotificationContent,
isShouldNotifyTx
} from '@hcengineering/server-notification-resources'
import task from '@hcengineering/task'
import task, { makeRank } from '@hcengineering/task'
import tracker, { Issue, IssueStatus, Project, TimeSpendReport } from '@hcengineering/tracker'
import serverTime, { OnToDo, ToDoFactory } from '@hcengineering/server-time'
import time, { ProjectToDo, ToDo, ToDoPriority, TodoAutomationHelper, WorkSlot } from '@hcengineering/time'
@ -444,6 +445,20 @@ async function getIssueToDoData (
): Promise<AttachedData<ProjectToDo> | undefined> {
const acc = await getPersonAccount(user, control)
if (acc === undefined) return
const firstTodoItem = (
await control.findAll(
time.class.ToDo,
{
user: acc.person,
doneOn: null
},
{
limit: 1,
sort: { rank: SortingOrder.Ascending }
}
)
)[0]
const rank = makeRank(undefined, firstTodoItem?.rank)
const data: AttachedData<ProjectToDo> = {
attachedSpace: issue.space,
workslots: 0,
@ -451,7 +466,8 @@ async function getIssueToDoData (
priority: ToDoPriority.NoPriority,
visibility: 'public',
title: issue.title,
user: acc.person
user: acc.person,
rank
}
return data
}