UBERF-8316 Documents drag and drop (#6769)

Signed-off-by: Alexander Onnikov <Alexander.Onnikov@xored.com>
This commit is contained in:
Alexander Onnikov 2024-10-02 18:00:38 +07:00 committed by GitHub
parent 810a75067d
commit 8a8f56fda4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
20 changed files with 510 additions and 97 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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