mirror of
https://github.com/hcengineering/platform.git
synced 2025-04-16 05:13:06 +00:00
UBERF-8316 Documents drag and drop (#6769)
Signed-off-by: Alexander Onnikov <Alexander.Onnikov@xored.com>
This commit is contained in:
parent
810a75067d
commit
8a8f56fda4
@ -61,6 +61,7 @@
|
||||
"@hcengineering/platform": "^0.6.11",
|
||||
"@hcengineering/server-tool": "^0.6.0",
|
||||
"@hcengineering/server-client": "^0.6.0",
|
||||
"@hcengineering/rank": "^0.6.4",
|
||||
"commander": "^8.1.0",
|
||||
"mime-types": "~2.1.34"
|
||||
}
|
||||
|
@ -22,7 +22,8 @@ import {
|
||||
collaborativeDocParse
|
||||
} from '@hcengineering/core'
|
||||
import { yDocToBuffer } from '@hcengineering/collaboration'
|
||||
import document, { type Document, type Teamspace } from '@hcengineering/document'
|
||||
import document, { type Document, type Teamspace, getFirstRank } from '@hcengineering/document'
|
||||
import { makeRank } from '@hcengineering/rank'
|
||||
import {
|
||||
MarkupMarkType,
|
||||
type MarkupNode,
|
||||
@ -328,6 +329,9 @@ async function createDBPageWithAttachments (
|
||||
|
||||
const parentId = parentMeta !== undefined ? (parentMeta.id as Ref<Document>) : document.ids.NoParent
|
||||
|
||||
const lastRank = await getFirstRank(client, space, parentId)
|
||||
const rank = makeRank(lastRank, undefined)
|
||||
|
||||
const object: AttachedData<Document> = {
|
||||
name: docMeta.name,
|
||||
content: collabId,
|
||||
@ -336,7 +340,8 @@ async function createDBPageWithAttachments (
|
||||
embeddings: 0,
|
||||
labels: 0,
|
||||
comments: 0,
|
||||
references: 0
|
||||
references: 0,
|
||||
rank
|
||||
}
|
||||
|
||||
await client.addCollection(
|
||||
@ -479,6 +484,9 @@ async function importPageDocument (
|
||||
|
||||
const parentId = parentMeta?.id ?? document.ids.NoParent
|
||||
|
||||
const lastRank = await getFirstRank(client, space, parentId as Ref<Document>)
|
||||
const rank = makeRank(lastRank, undefined)
|
||||
|
||||
const attachedData: AttachedData<Document> = {
|
||||
name: docMeta.name,
|
||||
content: collabId,
|
||||
@ -487,7 +495,8 @@ async function importPageDocument (
|
||||
embeddings: 0,
|
||||
labels: 0,
|
||||
comments: 0,
|
||||
references: 0
|
||||
references: 0,
|
||||
rank
|
||||
}
|
||||
|
||||
await client.addCollection(
|
||||
|
@ -50,6 +50,7 @@
|
||||
"@hcengineering/time": "^0.6.0",
|
||||
"@hcengineering/document": "^0.6.0",
|
||||
"@hcengineering/document-resources": "^0.6.0",
|
||||
"@hcengineering/collaboration": "^0.6.0"
|
||||
"@hcengineering/collaboration": "^0.6.0",
|
||||
"@hcengineering/rank": "^0.6.4"
|
||||
}
|
||||
}
|
||||
|
@ -14,7 +14,7 @@
|
||||
//
|
||||
|
||||
import activity from '@hcengineering/activity'
|
||||
import type { Class, CollaborativeDoc, CollectionSize, Domain, Role, RolesAssignment } from '@hcengineering/core'
|
||||
import type { Class, CollaborativeDoc, CollectionSize, Domain, Rank, Role, RolesAssignment } from '@hcengineering/core'
|
||||
import { IndexKind, Account, Ref, AccountRole } from '@hcengineering/core'
|
||||
import {
|
||||
type Document,
|
||||
@ -130,6 +130,10 @@ export class TDocument extends TAttachedDoc implements Document, Todoable {
|
||||
|
||||
@Prop(Collection(time.class.ToDo), getEmbeddedLabel('Action Items'))
|
||||
todos?: CollectionSize<ToDo>
|
||||
|
||||
@Index(IndexKind.Indexed)
|
||||
@Hidden()
|
||||
rank!: Rank
|
||||
}
|
||||
|
||||
@Model(document.class.DocumentSnapshot, core.class.AttachedDoc, DOMAIN_DOCUMENT)
|
||||
|
@ -13,16 +13,19 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import { DOMAIN_TX, MeasureMetricsContext } from '@hcengineering/core'
|
||||
import { DOMAIN_TX, MeasureMetricsContext, SortingOrder } from '@hcengineering/core'
|
||||
import { type Document, type Teamspace } from '@hcengineering/document'
|
||||
import {
|
||||
tryMigrate,
|
||||
type MigrateOperation,
|
||||
type MigrationClient,
|
||||
type MigrationUpgradeClient
|
||||
type MigrationUpgradeClient,
|
||||
type MigrateUpdate,
|
||||
type MigrationDocumentQuery,
|
||||
tryMigrate
|
||||
} from '@hcengineering/model'
|
||||
import core, { DOMAIN_SPACE } from '@hcengineering/model-core'
|
||||
import { type Asset } from '@hcengineering/platform'
|
||||
import { makeRank } from '@hcengineering/rank'
|
||||
|
||||
import document, { documentId, DOMAIN_DOCUMENT } from './index'
|
||||
import { loadCollaborativeDoc, saveCollaborativeDoc, yDocCopyXmlField } from '@hcengineering/collaboration'
|
||||
@ -127,6 +130,30 @@ async function migrateContentField (client: MigrationClient): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
async function migrateRank (client: MigrationClient): Promise<void> {
|
||||
const documents = await client.find<Document>(
|
||||
DOMAIN_DOCUMENT,
|
||||
{
|
||||
_class: document.class.Document,
|
||||
rank: { $exists: false }
|
||||
},
|
||||
{ sort: { name: SortingOrder.Ascending } }
|
||||
)
|
||||
|
||||
let rank = makeRank(undefined, undefined)
|
||||
const operations: { filter: MigrationDocumentQuery<Document>, update: MigrateUpdate<Document> }[] = []
|
||||
|
||||
for (const doc of documents) {
|
||||
operations.push({
|
||||
filter: { _id: doc._id },
|
||||
update: { $set: { rank } }
|
||||
})
|
||||
rank = makeRank(rank, undefined)
|
||||
}
|
||||
|
||||
await client.bulk(DOMAIN_DOCUMENT, operations)
|
||||
}
|
||||
|
||||
export const documentOperation: MigrateOperation = {
|
||||
async migrate (client: MigrationClient): Promise<void> {
|
||||
await tryMigrate(client, documentId, [
|
||||
@ -145,6 +172,10 @@ export const documentOperation: MigrateOperation = {
|
||||
{
|
||||
state: 'migrateContentField',
|
||||
func: migrateContentField
|
||||
},
|
||||
{
|
||||
state: 'migrateRank',
|
||||
func: migrateRank
|
||||
}
|
||||
])
|
||||
},
|
||||
|
@ -55,6 +55,7 @@
|
||||
export let showMenu: boolean = false
|
||||
export let shouldTooltip: boolean = false
|
||||
export let forciblyСollapsed: boolean = false
|
||||
export let draggable: boolean = false
|
||||
export let actions: Action[] = []
|
||||
export let _id: Ref<Doc> | string | undefined = undefined
|
||||
|
||||
@ -97,6 +98,10 @@
|
||||
class:selected
|
||||
class:showMenu={showMenu || pressed}
|
||||
on:click={toggle}
|
||||
{draggable}
|
||||
on:dragstart
|
||||
on:dragover
|
||||
on:drop
|
||||
>
|
||||
{#if isFold && !empty}
|
||||
<button class="hulyNavGroup-header__chevron" class:collapsed={!isOpen}>
|
||||
|
@ -18,7 +18,6 @@
|
||||
import {
|
||||
Icon,
|
||||
Label,
|
||||
IconOpenedArrow,
|
||||
IconDown,
|
||||
AnySvelteComponent,
|
||||
IconSize,
|
||||
@ -56,6 +55,8 @@
|
||||
export let level: number = 0
|
||||
export let _id: any = undefined
|
||||
|
||||
export let draggable: boolean = false
|
||||
|
||||
let labelEl: HTMLSpanElement
|
||||
let labelWidth: number
|
||||
let levelReset: boolean = false
|
||||
@ -85,7 +86,12 @@
|
||||
class:indent
|
||||
class:disabled
|
||||
class:showMenu
|
||||
{draggable}
|
||||
class:noActions={$$slots.actions === undefined}
|
||||
on:dragstart
|
||||
on:dragover
|
||||
on:dragend
|
||||
on:drop
|
||||
on:mouseover={mouseOver}
|
||||
on:mouseleave={() => {
|
||||
if (levelReset && !showMenu) levelReset = false
|
||||
|
@ -46,12 +46,8 @@
|
||||
|
||||
const id: Ref<Document> = generateId()
|
||||
|
||||
const object: Omit<AttachedData<Document>, 'content'> = {
|
||||
name: '',
|
||||
attachments: 0,
|
||||
labels: 0,
|
||||
comments: 0,
|
||||
references: 0
|
||||
const object: Pick<AttachedData<Document>, 'name' | 'icon' | 'color'> = {
|
||||
name: ''
|
||||
}
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
@ -21,6 +21,7 @@
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import document from '../plugin'
|
||||
import TeamspacePresenter from './teamspace/TeamspacePresenter.svelte'
|
||||
import { moveDocument } from '../utils'
|
||||
|
||||
export let value: Document
|
||||
|
||||
@ -40,10 +41,7 @@
|
||||
async function save (): Promise<void> {
|
||||
const ops = client.apply(value._id)
|
||||
|
||||
await ops.update(value, {
|
||||
space,
|
||||
attachedTo: parent ?? document.ids.NoParent
|
||||
})
|
||||
await moveDocument(value, space, parent ?? document.ids.NoParent)
|
||||
|
||||
if (space !== value.space) {
|
||||
const children = await findChildren(value)
|
||||
|
@ -25,6 +25,7 @@
|
||||
import document from '../../plugin'
|
||||
import { createEmptyDocument } from '../../utils'
|
||||
|
||||
import DropArea from './DropArea.svelte'
|
||||
import DocTreeElement from './DocTreeElement.svelte'
|
||||
|
||||
export let documents: Ref<Document>[]
|
||||
@ -34,11 +35,19 @@
|
||||
export let selected: Ref<Document> | undefined
|
||||
export let level: number = 0
|
||||
|
||||
export let onDragStart: (e: DragEvent, object: Ref<Document>) => void
|
||||
export let onDragOver: (e: DragEvent, object: Ref<Document>) => void
|
||||
export let onDragEnd: (e: DragEvent, object: Ref<Document>) => void
|
||||
export let onDrop: (e: DragEvent, object: Ref<Document>) => void
|
||||
|
||||
export let draggedItem: Ref<Document> | undefined
|
||||
export let draggedOver: Ref<Document> | undefined
|
||||
|
||||
const client = getClient()
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
function getDescendants (obj: Ref<Document>): Ref<Document>[] {
|
||||
return (descendants.get(obj) ?? []).sort((a, b) => a.name.localeCompare(b.name)).map((p) => p._id)
|
||||
return (descendants.get(obj) ?? []).sort((a, b) => a.rank.localeCompare(b.rank)).map((p) => p._id)
|
||||
}
|
||||
|
||||
function getActions (doc: Document): Action[] {
|
||||
@ -84,32 +93,63 @@
|
||||
</script>
|
||||
|
||||
{#each _documents as doc}
|
||||
{@const desc = _descendants.get(doc._id) ?? []}
|
||||
|
||||
{#if doc}
|
||||
<DocTreeElement
|
||||
{doc}
|
||||
icon={doc.icon === view.ids.IconWithEmoji ? IconWithEmoji : doc.icon ?? document.icon.Document}
|
||||
iconProps={doc.icon === view.ids.IconWithEmoji
|
||||
? { icon: doc.color }
|
||||
: {
|
||||
fill: doc.color !== undefined ? getPlatformColorDef(doc.color, $themeStore.dark).icon : 'currentColor'
|
||||
}}
|
||||
title={doc.name}
|
||||
selected={selected === doc._id}
|
||||
isFold
|
||||
{level}
|
||||
empty={desc.length === 0}
|
||||
actions={getActions(doc)}
|
||||
moreActions={() => getMoreActions(doc)}
|
||||
shouldTooltip
|
||||
on:click={() => {
|
||||
handleDocumentSelected(doc._id)
|
||||
}}
|
||||
>
|
||||
{#if desc.length}
|
||||
<svelte:self documents={desc} {descendants} {documentById} {selected} level={level + 1} on:selected />
|
||||
{@const desc = _descendants.get(doc._id) ?? []}
|
||||
{@const isDraggedOver = draggedOver === doc._id}
|
||||
<div class="flex-col relative">
|
||||
{#if isDraggedOver}
|
||||
<DropArea />
|
||||
{/if}
|
||||
</DocTreeElement>
|
||||
|
||||
<DocTreeElement
|
||||
{doc}
|
||||
icon={doc.icon === view.ids.IconWithEmoji ? IconWithEmoji : doc.icon ?? document.icon.Document}
|
||||
iconProps={doc.icon === view.ids.IconWithEmoji
|
||||
? { icon: doc.color }
|
||||
: {
|
||||
fill: doc.color !== undefined ? getPlatformColorDef(doc.color, $themeStore.dark).icon : 'currentColor'
|
||||
}}
|
||||
title={doc.name}
|
||||
selected={selected === doc._id && draggedItem === undefined}
|
||||
isFold
|
||||
{level}
|
||||
empty={desc.length === 0}
|
||||
actions={getActions(doc)}
|
||||
moreActions={() => getMoreActions(doc)}
|
||||
shouldTooltip
|
||||
on:click={() => {
|
||||
handleDocumentSelected(doc._id)
|
||||
}}
|
||||
on:dragstart={(evt) => {
|
||||
onDragStart(evt, doc._id)
|
||||
}}
|
||||
on:dragover={(evt) => {
|
||||
onDragOver(evt, doc._id)
|
||||
}}
|
||||
on:dragend={(evt) => {
|
||||
onDragEnd(evt, doc._id)
|
||||
}}
|
||||
on:drop={(evt) => {
|
||||
onDrop(evt, doc._id)
|
||||
}}
|
||||
>
|
||||
{#if desc.length}
|
||||
<svelte:self
|
||||
documents={desc}
|
||||
{descendants}
|
||||
{documentById}
|
||||
{selected}
|
||||
level={level + 1}
|
||||
{onDragStart}
|
||||
{onDragOver}
|
||||
{onDragEnd}
|
||||
{onDrop}
|
||||
{draggedItem}
|
||||
{draggedOver}
|
||||
on:selected
|
||||
/>
|
||||
{/if}
|
||||
</DocTreeElement>
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
|
@ -64,6 +64,11 @@
|
||||
showMenu={hovered}
|
||||
{shouldTooltip}
|
||||
{forciblyСollapsed}
|
||||
draggable
|
||||
on:dragstart
|
||||
on:dragover
|
||||
on:dragend
|
||||
on:drop
|
||||
on:click={() => {
|
||||
selectDocument()
|
||||
dispatch('click')
|
||||
@ -95,4 +100,5 @@
|
||||
<svelte:fragment slot="dropbox">
|
||||
<slot />
|
||||
</svelte:fragment>
|
||||
<slot name="extra" />
|
||||
</NavItem>
|
||||
|
@ -0,0 +1,29 @@
|
||||
<!--
|
||||
// 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.
|
||||
-->
|
||||
|
||||
<div class="drop-area" />
|
||||
|
||||
<style lang="scss">
|
||||
.drop-area {
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
left: 0.75rem;
|
||||
right: 0.75rem;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
background-color: var(--global-ui-highlight-BackgroundColor);
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
</style>
|
@ -0,0 +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">
|
||||
export let top: number
|
||||
</script>
|
||||
|
||||
<div class="drop-marker" style="top: {top}px;" />
|
||||
|
||||
<style lang="scss">
|
||||
.drop-marker {
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
z-index: 100;
|
||||
height: 0.125rem;
|
||||
background-color: var(--primary-button-focused);
|
||||
|
||||
left: 0.75rem;
|
||||
right: 0.75rem;
|
||||
top: 10rem;
|
||||
}
|
||||
</style>
|
@ -13,6 +13,7 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { Analytics } from '@hcengineering/analytics'
|
||||
import { Ref, SortingOrder, Space, generateId } from '@hcengineering/core'
|
||||
import { Document, DocumentEvents, Teamspace } from '@hcengineering/document'
|
||||
import { createQuery, getClient } from '@hcengineering/presentation'
|
||||
@ -23,7 +24,8 @@
|
||||
getPlatformColorForTextDef,
|
||||
themeStore,
|
||||
Action,
|
||||
IconAdd
|
||||
IconAdd,
|
||||
closeTooltip
|
||||
} from '@hcengineering/ui'
|
||||
import view from '@hcengineering/view'
|
||||
import { TreeNode, openDoc, getActions as getContributedActions } from '@hcengineering/view-resources'
|
||||
@ -31,10 +33,17 @@
|
||||
import { getResource } from '@hcengineering/platform'
|
||||
|
||||
import document from '../../plugin'
|
||||
import { getDocumentIdFromFragment, createEmptyDocument } from '../../utils'
|
||||
import {
|
||||
getDocumentIdFromFragment,
|
||||
createEmptyDocument,
|
||||
moveDocument,
|
||||
moveDocumentBefore,
|
||||
moveDocumentAfter
|
||||
} from '../../utils'
|
||||
import DocHierarchy from './DocHierarchy.svelte'
|
||||
import DocTreeElement from './DocTreeElement.svelte'
|
||||
import { Analytics } from '@hcengineering/analytics'
|
||||
import DropArea from './DropArea.svelte'
|
||||
import DropMarker from './DropMarker.svelte'
|
||||
|
||||
export let space: Teamspace
|
||||
export let model: SpacesNavModel
|
||||
@ -52,7 +61,24 @@
|
||||
let descendants: Map<Ref<Document>, Document[]> = new Map<Ref<Document>, Document[]>()
|
||||
|
||||
function getDescendants (obj: Ref<Document>): Ref<Document>[] {
|
||||
return (descendants.get(obj) ?? []).sort((a, b) => a.name.localeCompare(b.name)).map((p) => p._id)
|
||||
return (descendants.get(obj) ?? []).sort((a, b) => a.rank.localeCompare(b.rank)).map((p) => p._id)
|
||||
}
|
||||
|
||||
function getAllDescendants (obj: Ref<Document>): Ref<Document>[] {
|
||||
const result: Ref<Document>[] = []
|
||||
const queue: Ref<Document>[] = [obj]
|
||||
|
||||
while (queue.length > 0) {
|
||||
const next = queue.pop()
|
||||
if (next === undefined) break
|
||||
|
||||
const children = descendants.get(next) ?? []
|
||||
const childrenRefs = children.map((p) => p._id)
|
||||
result.push(...childrenRefs)
|
||||
queue.push(...childrenRefs)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
let selected: Ref<Document> | undefined
|
||||
@ -85,7 +111,7 @@
|
||||
},
|
||||
{
|
||||
sort: {
|
||||
name: SortingOrder.Ascending
|
||||
rank: SortingOrder.Ascending
|
||||
}
|
||||
}
|
||||
)
|
||||
@ -125,48 +151,180 @@
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
let parent: HTMLElement
|
||||
let draggedItem: Ref<Document> | undefined = undefined
|
||||
let draggedOver: Ref<Document> | undefined = undefined
|
||||
let draggedOverPos: 'before' | 'after' | undefined = undefined
|
||||
let draggedOverTop: number = 0
|
||||
let cannotDropTo: Ref<Document>[] = []
|
||||
|
||||
function canDrop (object: Ref<Document>, target: Ref<Document>): boolean {
|
||||
if (object === target) return false
|
||||
if (cannotDropTo.includes(target)) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
function onDragStart (event: DragEvent, object: Ref<Document>): void {
|
||||
// no prevent default to leverage default rendering
|
||||
// event.preventDefault()
|
||||
if (event.dataTransfer === null || event.target === null) {
|
||||
return
|
||||
}
|
||||
|
||||
cannotDropTo = [object, ...getAllDescendants(object)]
|
||||
|
||||
event.dataTransfer.effectAllowed = 'move'
|
||||
event.dataTransfer.dropEffect = 'move'
|
||||
draggedItem = object
|
||||
|
||||
closeTooltip()
|
||||
}
|
||||
|
||||
function getDropPosition (event: DragEvent): { pos: 'before' | 'after' | undefined, top: number } {
|
||||
const parentRect = parent.getBoundingClientRect()
|
||||
const targetRect = (event.target as HTMLElement).getBoundingClientRect()
|
||||
const dropPosition = event.clientY - targetRect.top
|
||||
|
||||
const before = dropPosition >= 0 && dropPosition < targetRect.height / 6
|
||||
const after = dropPosition <= targetRect.height && dropPosition > (5 * targetRect.height) / 6
|
||||
|
||||
const pos = before ? 'before' : after ? 'after' : undefined
|
||||
const top = pos === 'before' ? targetRect.top - parentRect.top - 1 : targetRect.bottom - parentRect.top - 1
|
||||
|
||||
return { pos, top }
|
||||
}
|
||||
|
||||
function onDragOver (event: DragEvent, object: Ref<Document>): void {
|
||||
event.preventDefault()
|
||||
// this is an ugly solution to control drop effect
|
||||
// we drag and drop elements that are in the depth of components hierarchy
|
||||
// so we cannot access them directly
|
||||
if (!(event.target as HTMLElement).draggable) return
|
||||
if (event.dataTransfer === null || event.target === null || draggedItem === object) {
|
||||
return
|
||||
}
|
||||
|
||||
if (draggedItem !== undefined && canDrop(draggedItem, object)) {
|
||||
event.dataTransfer.dropEffect = 'move'
|
||||
draggedOver = object
|
||||
|
||||
const { pos, top } = getDropPosition(event)
|
||||
draggedOverPos = pos
|
||||
draggedOverTop = top
|
||||
} else {
|
||||
event.dataTransfer.dropEffect = 'none'
|
||||
}
|
||||
}
|
||||
|
||||
function onDragEnd (event: DragEvent): void {
|
||||
event.preventDefault()
|
||||
draggedItem = undefined
|
||||
draggedOver = undefined
|
||||
draggedOverPos = undefined
|
||||
}
|
||||
|
||||
function onDrop (event: DragEvent, object: Ref<Document>): void {
|
||||
event.preventDefault()
|
||||
if (event.dataTransfer === null) {
|
||||
return
|
||||
}
|
||||
if (draggedItem !== undefined && canDrop(draggedItem, object)) {
|
||||
const doc = documentById.get(draggedItem)
|
||||
const target = documentById.get(object)
|
||||
|
||||
if (doc !== undefined && doc._id !== object) {
|
||||
if (object === document.ids.NoParent) {
|
||||
void moveDocument(doc, doc.space, document.ids.NoParent)
|
||||
} else if (target !== undefined) {
|
||||
const { pos } = getDropPosition(event)
|
||||
if (pos === 'before') {
|
||||
void moveDocumentBefore(doc, target)
|
||||
} else if (pos === 'after') {
|
||||
void moveDocumentAfter(doc, target)
|
||||
} else if (doc.attachedTo !== object) {
|
||||
void moveDocument(doc, target.space, target._id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
draggedItem = undefined
|
||||
draggedOver = undefined
|
||||
}
|
||||
</script>
|
||||
|
||||
<TreeNode
|
||||
_id={space?._id}
|
||||
icon={space?.icon === view.ids.IconWithEmoji ? IconWithEmoji : space?.icon ?? model.icon}
|
||||
iconProps={space?.icon === view.ids.IconWithEmoji
|
||||
? { icon: space.color }
|
||||
: {
|
||||
fill:
|
||||
space.color !== undefined
|
||||
? getPlatformColorDef(space.color, $themeStore.dark).icon
|
||||
: getPlatformColorForTextDef(space.name, $themeStore.dark).icon
|
||||
}}
|
||||
title={space.name}
|
||||
type={'nested'}
|
||||
highlighted={currentSpace === space._id}
|
||||
visible={currentSpace === space._id || forciblyСollapsed}
|
||||
actions={() => getActions(space)}
|
||||
{forciblyСollapsed}
|
||||
>
|
||||
<DocHierarchy {documents} {descendants} {documentById} {selected} />
|
||||
<div bind:this={parent} class="flex-col relative">
|
||||
{#if draggedOver === document.ids.NoParent}
|
||||
<DropArea />
|
||||
{/if}
|
||||
|
||||
<svelte:fragment slot="visible">
|
||||
{#if (selected || forciblyСollapsed) && visibleItem}
|
||||
{@const item = visibleItem}
|
||||
<DocTreeElement
|
||||
doc={item}
|
||||
icon={item.icon === view.ids.IconWithEmoji ? IconWithEmoji : item.icon ?? document.icon.Document}
|
||||
iconProps={item.icon === view.ids.IconWithEmoji
|
||||
? { icon: visibleItem.color }
|
||||
: {
|
||||
fill: item.color !== undefined ? getPlatformColorDef(item.color, $themeStore.dark).icon : 'currentColor'
|
||||
}}
|
||||
title={item.name}
|
||||
selected
|
||||
isFold
|
||||
empty
|
||||
shouldTooltip
|
||||
actions={getDocActions(item)}
|
||||
moreActions={() => getMoreActions(item)}
|
||||
forciblyСollapsed
|
||||
/>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
</TreeNode>
|
||||
{#if draggedOver && draggedOverPos}
|
||||
<DropMarker top={draggedOverTop} />
|
||||
{/if}
|
||||
|
||||
<TreeNode
|
||||
_id={space?._id}
|
||||
icon={space?.icon === view.ids.IconWithEmoji ? IconWithEmoji : space?.icon ?? model.icon}
|
||||
iconProps={space?.icon === view.ids.IconWithEmoji
|
||||
? { icon: space.color }
|
||||
: {
|
||||
fill:
|
||||
space.color !== undefined
|
||||
? getPlatformColorDef(space.color, $themeStore.dark).icon
|
||||
: getPlatformColorForTextDef(space.name, $themeStore.dark).icon
|
||||
}}
|
||||
title={space.name}
|
||||
type={'nested'}
|
||||
highlighted={currentSpace === space._id}
|
||||
visible={currentSpace === space._id || forciblyСollapsed}
|
||||
actions={() => getActions(space)}
|
||||
selected={draggedOver === document.ids.NoParent}
|
||||
{forciblyСollapsed}
|
||||
draggable
|
||||
on:drop={(evt) => {
|
||||
onDrop(evt, document.ids.NoParent)
|
||||
}}
|
||||
on:dragover={(evt) => {
|
||||
onDragOver(evt, document.ids.NoParent)
|
||||
}}
|
||||
on:dragstart={(evt) => {
|
||||
evt.preventDefault()
|
||||
}}
|
||||
>
|
||||
<DocHierarchy
|
||||
{documents}
|
||||
{descendants}
|
||||
{documentById}
|
||||
{selected}
|
||||
{onDragStart}
|
||||
{onDragEnd}
|
||||
{onDragOver}
|
||||
{onDrop}
|
||||
{draggedItem}
|
||||
{draggedOver}
|
||||
/>
|
||||
<svelte:fragment slot="visible">
|
||||
{#if (selected || forciblyСollapsed) && visibleItem}
|
||||
{@const item = visibleItem}
|
||||
<DocTreeElement
|
||||
doc={item}
|
||||
icon={item.icon === view.ids.IconWithEmoji ? IconWithEmoji : item.icon ?? document.icon.Document}
|
||||
iconProps={item.icon === view.ids.IconWithEmoji
|
||||
? { icon: visibleItem.color }
|
||||
: {
|
||||
fill: item.color !== undefined ? getPlatformColorDef(item.color, $themeStore.dark).icon : 'currentColor'
|
||||
}}
|
||||
title={item.name}
|
||||
selected
|
||||
isFold
|
||||
empty
|
||||
shouldTooltip
|
||||
actions={getDocActions(item)}
|
||||
moreActions={() => getMoreActions(item)}
|
||||
forciblyСollapsed
|
||||
/>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
</TreeNode>
|
||||
</div>
|
||||
|
@ -13,17 +13,57 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import { type AttachedData, type Client, type Ref, type TxOperations, makeCollaborativeDoc } from '@hcengineering/core'
|
||||
import { type Document, type Teamspace, documentId } from '@hcengineering/document'
|
||||
import {
|
||||
type AttachedData,
|
||||
type Client,
|
||||
type QuerySelector,
|
||||
type Ref,
|
||||
SortingOrder,
|
||||
type TxOperations,
|
||||
makeCollaborativeDoc
|
||||
} from '@hcengineering/core'
|
||||
import { type Document, type Teamspace, documentId, getFirstRank } from '@hcengineering/document'
|
||||
import { getMetadata, translate } from '@hcengineering/platform'
|
||||
import presentation, { getClient } from '@hcengineering/presentation'
|
||||
import { makeRank } from '@hcengineering/rank'
|
||||
import { getCurrentResolvedLocation, getPanelURI, type Location, type ResolvedLocation } from '@hcengineering/ui'
|
||||
import { accessDeniedStore } from '@hcengineering/view-resources'
|
||||
import { workbenchId } from '@hcengineering/workbench'
|
||||
import slugify from 'slugify'
|
||||
|
||||
import { accessDeniedStore } from '@hcengineering/view-resources'
|
||||
import document from './plugin'
|
||||
|
||||
export async function moveDocument (doc: Document, space: Ref<Teamspace>, parent: Ref<Document>): Promise<void> {
|
||||
const client = getClient()
|
||||
|
||||
const prevRank = await getFirstRank(client, space, parent)
|
||||
const rank = makeRank(prevRank, undefined)
|
||||
|
||||
await client.update(doc, { space, attachedTo: parent, rank })
|
||||
}
|
||||
|
||||
export async function moveDocumentBefore (doc: Document, before: Document): Promise<void> {
|
||||
const client = getClient()
|
||||
|
||||
const { space, attachedTo } = before
|
||||
const query = { rank: { $lt: before.rank } as unknown as QuerySelector<Document['rank']> }
|
||||
const lastRank = await getFirstRank(client, space, attachedTo, SortingOrder.Descending, query)
|
||||
const rank = makeRank(lastRank, before.rank)
|
||||
|
||||
await client.update(doc, { space, attachedTo, rank })
|
||||
}
|
||||
|
||||
export async function moveDocumentAfter (doc: Document, after: Document): Promise<void> {
|
||||
const client = getClient()
|
||||
|
||||
const { space, attachedTo } = after
|
||||
const query = { rank: { $gt: after.rank } as unknown as QuerySelector<Document['rank']> }
|
||||
const nextRank = await getFirstRank(client, space, attachedTo, SortingOrder.Ascending, query)
|
||||
const rank = makeRank(after.rank, nextRank)
|
||||
|
||||
await client.update(doc, { space, attachedTo, rank })
|
||||
}
|
||||
|
||||
export async function createEmptyDocument (
|
||||
client: TxOperations,
|
||||
id: Ref<Document>,
|
||||
@ -33,6 +73,9 @@ export async function createEmptyDocument (
|
||||
): Promise<void> {
|
||||
const name = await translate(document.string.Untitled, {})
|
||||
|
||||
const lastRank = await getFirstRank(client, space, parent)
|
||||
const rank = makeRank(lastRank, undefined)
|
||||
|
||||
const object: AttachedData<Document> = {
|
||||
name,
|
||||
content: makeCollaborativeDoc(id, 'content'),
|
||||
@ -42,6 +85,7 @@ export async function createEmptyDocument (
|
||||
labels: 0,
|
||||
comments: 0,
|
||||
references: 0,
|
||||
rank,
|
||||
...data
|
||||
}
|
||||
|
||||
|
@ -15,8 +15,9 @@
|
||||
|
||||
import { documentId, documentPlugin } from './plugin'
|
||||
|
||||
export * from './types'
|
||||
export * from './analytics'
|
||||
export * from './types'
|
||||
export * from './utils'
|
||||
export { documentId }
|
||||
|
||||
export default documentPlugin
|
||||
|
@ -14,7 +14,7 @@
|
||||
//
|
||||
|
||||
import { Attachment } from '@hcengineering/attachment'
|
||||
import { Account, AttachedDoc, Class, CollaborativeDoc, Ref, TypedSpace } from '@hcengineering/core'
|
||||
import { Account, AttachedDoc, Class, CollaborativeDoc, Rank, Ref, TypedSpace } from '@hcengineering/core'
|
||||
import { Preference } from '@hcengineering/preference'
|
||||
import { IconProps } from '@hcengineering/view'
|
||||
|
||||
@ -37,6 +37,8 @@ export interface Document extends AttachedDoc<Document, 'children', Teamspace>,
|
||||
embeddings?: number
|
||||
labels?: number
|
||||
references?: number
|
||||
|
||||
rank: Rank
|
||||
}
|
||||
|
||||
/** @public */
|
||||
|
35
plugins/document/src/utils.ts
Normal file
35
plugins/document/src/utils.ts
Normal file
@ -0,0 +1,35 @@
|
||||
//
|
||||
// Copyright © 2023, 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.
|
||||
//
|
||||
|
||||
import { type DocumentQuery, type Rank, type Ref, SortingOrder, TxOperations } from '@hcengineering/core'
|
||||
|
||||
import document from './plugin'
|
||||
import { type Document, type Teamspace } from './types'
|
||||
|
||||
export async function getFirstRank (
|
||||
client: TxOperations,
|
||||
space: Ref<Teamspace>,
|
||||
attachedTo: Ref<Document>,
|
||||
sort: SortingOrder = SortingOrder.Descending,
|
||||
extra: DocumentQuery<Document> = {}
|
||||
): Promise<Rank | undefined> {
|
||||
const doc = await client.findOne(
|
||||
document.class.Document,
|
||||
{ space, attachedTo, ...extra },
|
||||
{ sort: { rank: sort }, projection: { rank: 1 } }
|
||||
)
|
||||
|
||||
return doc?.rank
|
||||
}
|
@ -55,6 +55,7 @@
|
||||
export let showNotify: boolean = false
|
||||
export let forciblyСollapsed: boolean = false
|
||||
export let actions: (originalEvent?: MouseEvent) => Promise<Action[]> = async () => []
|
||||
export let draggable: boolean = false
|
||||
|
||||
let pressed: boolean = false
|
||||
let inlineActions: Action[] = []
|
||||
@ -103,7 +104,11 @@
|
||||
{shouldTooltip}
|
||||
showMenu={showMenu || pressed}
|
||||
{noDivider}
|
||||
{draggable}
|
||||
on:click
|
||||
on:dragstart
|
||||
on:dragover
|
||||
on:drop
|
||||
on:toggle={(ev) => {
|
||||
if (ev.detail !== undefined) collapsed = !ev.detail
|
||||
}}
|
||||
@ -166,8 +171,12 @@
|
||||
{forciblyСollapsed}
|
||||
{level}
|
||||
{shouldTooltip}
|
||||
{draggable}
|
||||
showMenu={showMenu || pressed}
|
||||
on:click
|
||||
on:dragstart
|
||||
on:dragover
|
||||
on:drop
|
||||
>
|
||||
<slot />
|
||||
<svelte:fragment slot="extra"><slot name="extra" /></svelte:fragment>
|
||||
|
@ -39,6 +39,7 @@
|
||||
export let noDivider: boolean = false
|
||||
export let shouldTooltip: boolean = false
|
||||
export let forciblyСollapsed: boolean = false
|
||||
export let draggable: boolean = false
|
||||
</script>
|
||||
|
||||
<TreeElement
|
||||
@ -63,7 +64,11 @@
|
||||
{showMenu}
|
||||
{noDivider}
|
||||
{forciblyСollapsed}
|
||||
{draggable}
|
||||
on:click
|
||||
on:dragstart
|
||||
on:dragover
|
||||
on:drop
|
||||
>
|
||||
<slot />
|
||||
<svelte:fragment slot="extra"><slot name="extra" /></svelte:fragment>
|
||||
|
Loading…
Reference in New Issue
Block a user