mirror of
https://github.com/hcengineering/platform.git
synced 2025-05-31 04:38:02 +00:00
EZQMS-1234: ability to relocate and reorder controlled documents within the space (#7668)
Some checks are pending
CI / build (push) Waiting to run
CI / svelte-check (push) Blocked by required conditions
CI / formatting (push) Blocked by required conditions
CI / test (push) Blocked by required conditions
CI / uitest (push) Waiting to run
CI / uitest-pg (push) Waiting to run
CI / uitest-qms (push) Waiting to run
CI / docker-build (push) Blocked by required conditions
CI / dist-build (push) Blocked by required conditions
Some checks are pending
CI / build (push) Waiting to run
CI / svelte-check (push) Blocked by required conditions
CI / formatting (push) Blocked by required conditions
CI / test (push) Blocked by required conditions
CI / uitest (push) Waiting to run
CI / uitest-pg (push) Waiting to run
CI / uitest-qms (push) Waiting to run
CI / docker-build (push) Blocked by required conditions
CI / dist-build (push) Blocked by required conditions
Signed-off-by: Victor Ilyushchenko <alt13ri@gmail.com>
This commit is contained in:
parent
ff5b57dd47
commit
0c6feb646e
@ -58,6 +58,7 @@
|
||||
"@hcengineering/chunter": "^0.6.20",
|
||||
"@hcengineering/text-editor": "^0.6.0",
|
||||
"@hcengineering/collaboration": "^0.6.0",
|
||||
"@hcengineering/time": "^0.6.0"
|
||||
"@hcengineering/time": "^0.6.0",
|
||||
"@hcengineering/rank": "^0.6.4"
|
||||
}
|
||||
}
|
||||
|
@ -4,12 +4,12 @@
|
||||
|
||||
import attachment, { type Attachment } from '@hcengineering/attachment'
|
||||
import {
|
||||
YXmlElement,
|
||||
YXmlText,
|
||||
YAbstractType,
|
||||
yXmlElementClone,
|
||||
loadCollabYdoc,
|
||||
saveCollabYdoc
|
||||
saveCollabYdoc,
|
||||
YAbstractType,
|
||||
YXmlElement,
|
||||
yXmlElementClone,
|
||||
YXmlText
|
||||
} from '@hcengineering/collaboration'
|
||||
import {
|
||||
type ChangeControl,
|
||||
@ -17,8 +17,10 @@ import {
|
||||
createChangeControl,
|
||||
createDocumentTemplate,
|
||||
type DocumentCategory,
|
||||
type DocumentMeta,
|
||||
documentsId,
|
||||
DocumentState
|
||||
DocumentState,
|
||||
type ProjectMeta
|
||||
} from '@hcengineering/controlled-documents'
|
||||
import {
|
||||
type Class,
|
||||
@ -48,6 +50,7 @@ import { DOMAIN_ATTACHMENT } from '@hcengineering/model-attachment'
|
||||
import core from '@hcengineering/model-core'
|
||||
import tags from '@hcengineering/tags'
|
||||
|
||||
import { makeRank } from '@hcengineering/rank'
|
||||
import documents, { DOMAIN_DOCUMENTS } from './index'
|
||||
|
||||
async function createTemplatesSpace (tx: TxOperations): Promise<void> {
|
||||
@ -364,6 +367,42 @@ async function migrateDocSections (client: MigrationClient): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
async function migrateProjectMetaRank (client: MigrationClient): Promise<void> {
|
||||
const projectMeta = await client.find<ProjectMeta>(DOMAIN_DOCUMENTS, {
|
||||
_class: documents.class.ProjectMeta,
|
||||
rank: { $exists: false }
|
||||
})
|
||||
|
||||
const docMeta = await client.find<DocumentMeta>(DOMAIN_DOCUMENTS, {
|
||||
_class: documents.class.ProjectDocument,
|
||||
_id: { $in: projectMeta.map((p) => p.meta) }
|
||||
})
|
||||
|
||||
const docMetaById = new Map<Ref<DocumentMeta>, DocumentMeta>()
|
||||
for (const doc of docMeta) {
|
||||
docMetaById.set(doc._id, doc)
|
||||
}
|
||||
|
||||
projectMeta.sort((a, b) => {
|
||||
const docA = docMetaById.get(a.meta)
|
||||
const docB = docMetaById.get(b.meta)
|
||||
return (docA?.title ?? '').localeCompare(docB?.title ?? '', undefined, { numeric: true })
|
||||
})
|
||||
|
||||
let rank = makeRank(undefined, undefined)
|
||||
const operations: { filter: MigrationDocumentQuery<ProjectMeta>, update: MigrateUpdate<ProjectMeta> }[] = []
|
||||
|
||||
for (const doc of projectMeta) {
|
||||
operations.push({
|
||||
filter: { _id: doc._id },
|
||||
update: { $set: { rank } }
|
||||
})
|
||||
rank = makeRank(rank, undefined)
|
||||
}
|
||||
|
||||
await client.bulk(DOMAIN_DOCUMENTS, operations)
|
||||
}
|
||||
|
||||
export const documentsOperation: MigrateOperation = {
|
||||
async migrate (client: MigrationClient): Promise<void> {
|
||||
await tryMigrate(client, documentsId, [
|
||||
@ -374,6 +413,10 @@ export const documentsOperation: MigrateOperation = {
|
||||
{
|
||||
state: 'migrateDocSections',
|
||||
func: migrateDocSections
|
||||
},
|
||||
{
|
||||
state: 'migrateProjectMetaRank',
|
||||
func: migrateProjectMetaRank
|
||||
}
|
||||
])
|
||||
},
|
||||
|
@ -58,7 +58,8 @@ import {
|
||||
type Role,
|
||||
type TypedSpace,
|
||||
type Account,
|
||||
type RolesAssignment
|
||||
type RolesAssignment,
|
||||
type Rank
|
||||
} from '@hcengineering/core'
|
||||
import {
|
||||
ArrOf,
|
||||
@ -181,6 +182,10 @@ export class TProjectMeta extends TDoc implements ProjectMeta {
|
||||
|
||||
@Prop(Collection(documents.class.ProjectDocument), documents.string.Documents)
|
||||
documents!: CollectionSize<ProjectDocument>
|
||||
|
||||
@Index(IndexKind.Indexed)
|
||||
@Hidden()
|
||||
rank!: Rank
|
||||
}
|
||||
|
||||
@Model(documents.class.ProjectDocument, core.class.AttachedDoc, DOMAIN_DOCUMENTS)
|
||||
|
@ -101,6 +101,7 @@
|
||||
{draggable}
|
||||
on:dragstart
|
||||
on:dragover
|
||||
on:dragend
|
||||
on:drop
|
||||
>
|
||||
{#if isFold && !empty}
|
||||
|
@ -70,6 +70,7 @@
|
||||
"effector": "~22.8.7",
|
||||
"svelte": "^4.2.19",
|
||||
"slugify": "^1.6.6",
|
||||
"fast-equals": "^5.0.1"
|
||||
"fast-equals": "^5.0.1",
|
||||
"@hcengineering/rank": "^0.6.4"
|
||||
}
|
||||
}
|
||||
|
@ -27,7 +27,6 @@
|
||||
DocumentState
|
||||
} from '@hcengineering/controlled-documents'
|
||||
import { TreeItem } from '@hcengineering/view-resources'
|
||||
import { compareDocs } from '../../utils'
|
||||
|
||||
export let projectMeta: ProjectMeta[] = []
|
||||
export let childrenByParent: Record<Ref<DocumentMeta>, Array<ProjectMeta>>
|
||||
@ -36,13 +35,39 @@
|
||||
export let getMoreActions: ((obj: Doc, originalEvent?: MouseEvent) => Promise<Action[]>) | undefined = undefined
|
||||
export let collapsedPrefix: string = ''
|
||||
|
||||
export let onDragStart: ((e: DragEvent, object: Ref<DocumentMeta>) => void) | undefined = undefined
|
||||
export let onDragOver: ((e: DragEvent, object: Ref<DocumentMeta>) => void) | undefined = undefined
|
||||
export let onDragEnd: ((e: DragEvent, object: Ref<DocumentMeta>) => void) | undefined = undefined
|
||||
export let onDrop: ((e: DragEvent, object: Ref<DocumentMeta>) => void) | undefined = undefined
|
||||
|
||||
export let draggedItem: Ref<DocumentMeta> | undefined = undefined
|
||||
export let draggedOver: Ref<DocumentMeta> | undefined = undefined
|
||||
|
||||
import DropArea from './DropArea.svelte'
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
const currentUser = getCurrentAccount() as PersonAccount
|
||||
const currentPerson = currentUser.person
|
||||
|
||||
const docsQuery = createQuery()
|
||||
let docs: WithLookup<ProjectDocument>[] = []
|
||||
|
||||
function sortDocs (meta: ProjectMeta[]): void {
|
||||
const metaById = new Map(meta.map((p) => [p._id, p]))
|
||||
docs = docs.slice().sort((a, b) => {
|
||||
const metaA = metaById.get(a.attachedTo)
|
||||
const metaB = metaById.get(b.attachedTo)
|
||||
|
||||
if (metaA !== undefined && metaB !== undefined) {
|
||||
return metaA.rank.localeCompare(metaB.rank)
|
||||
}
|
||||
return 0
|
||||
})
|
||||
}
|
||||
|
||||
$: sortDocs(projectMeta)
|
||||
|
||||
const docsQuery = createQuery()
|
||||
|
||||
$: docsQuery.query(
|
||||
documents.class.ProjectDocument,
|
||||
{
|
||||
@ -79,12 +104,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
docs.sort((a, b) => {
|
||||
if (a.$lookup?.document !== undefined && b.$lookup?.document !== undefined) {
|
||||
return compareDocs(a.$lookup.document, b.$lookup.document)
|
||||
}
|
||||
return 0
|
||||
})
|
||||
sortDocs(projectMeta)
|
||||
},
|
||||
{
|
||||
lookup: {
|
||||
@ -110,37 +130,62 @@
|
||||
|
||||
{#if doc}
|
||||
{@const children = childrenByParent[doc.attachedTo] ?? []}
|
||||
<TreeItem
|
||||
_id={doc._id}
|
||||
icon={documents.icon.Document}
|
||||
iconProps={{
|
||||
fill: 'currentColor'
|
||||
}}
|
||||
title={getDocumentName(doc)}
|
||||
selected={selected === doc._id || selected === prjdoc._id}
|
||||
isFold
|
||||
empty={children.length === 0 || children === undefined}
|
||||
actions={getMoreActions !== undefined ? () => getDocMoreActions(prjdoc) : undefined}
|
||||
{level}
|
||||
{collapsedPrefix}
|
||||
shouldTooltip
|
||||
on:click={() => {
|
||||
dispatch('selected', prjdoc)
|
||||
}}
|
||||
>
|
||||
<svelte:fragment slot="dropbox">
|
||||
{#if children.length}
|
||||
<svelte:self
|
||||
projectMeta={children}
|
||||
{childrenByParent}
|
||||
{selected}
|
||||
{collapsedPrefix}
|
||||
{getMoreActions}
|
||||
level={level + 1}
|
||||
on:selected
|
||||
/>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
</TreeItem>
|
||||
{@const isDraggedOver = draggedOver === doc.attachedTo}
|
||||
<div class="flex-col relative">
|
||||
{#if isDraggedOver}
|
||||
<DropArea />
|
||||
{/if}
|
||||
<TreeItem
|
||||
_id={doc._id}
|
||||
icon={documents.icon.Document}
|
||||
iconProps={{
|
||||
fill: 'currentColor'
|
||||
}}
|
||||
title={getDocumentName(doc)}
|
||||
selected={selected === doc._id || selected === prjdoc._id}
|
||||
isFold
|
||||
empty={children.length === 0 || children === undefined}
|
||||
actions={getMoreActions !== undefined ? () => getDocMoreActions(prjdoc) : undefined}
|
||||
{level}
|
||||
{collapsedPrefix}
|
||||
shouldTooltip
|
||||
on:click={() => {
|
||||
dispatch('selected', prjdoc)
|
||||
}}
|
||||
draggable={onDragStart !== undefined}
|
||||
on:dragstart={(evt) => {
|
||||
onDragStart?.(evt, doc.attachedTo)
|
||||
}}
|
||||
on:dragover={(evt) => {
|
||||
onDragOver?.(evt, doc.attachedTo)
|
||||
}}
|
||||
on:dragend={(evt) => {
|
||||
onDragEnd?.(evt, doc.attachedTo)
|
||||
}}
|
||||
on:drop={(evt) => {
|
||||
onDrop?.(evt, doc.attachedTo)
|
||||
}}
|
||||
>
|
||||
<svelte:fragment slot="dropbox">
|
||||
{#if children.length}
|
||||
<svelte:self
|
||||
projectMeta={children}
|
||||
{childrenByParent}
|
||||
{selected}
|
||||
{collapsedPrefix}
|
||||
{getMoreActions}
|
||||
level={level + 1}
|
||||
{onDragStart}
|
||||
{onDragOver}
|
||||
{onDragEnd}
|
||||
{onDrop}
|
||||
{draggedItem}
|
||||
{draggedOver}
|
||||
on:selected
|
||||
/>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
</TreeItem>
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
|
@ -16,7 +16,15 @@
|
||||
import { WithLookup, type Doc, type Ref, type Space } from '@hcengineering/core'
|
||||
import { createQuery, getClient } from '@hcengineering/presentation'
|
||||
import { getResource } from '@hcengineering/platform'
|
||||
import { type Action, getPlatformColorForTextDef, themeStore, navigate, IconEdit, Label } from '@hcengineering/ui'
|
||||
import {
|
||||
type Action,
|
||||
getPlatformColorForTextDef,
|
||||
themeStore,
|
||||
navigate,
|
||||
IconEdit,
|
||||
Label,
|
||||
closeTooltip
|
||||
} from '@hcengineering/ui'
|
||||
import { getActions as getContributedActions, TreeNode, TreeItem } from '@hcengineering/view-resources'
|
||||
import { ActionGroup } from '@hcengineering/view'
|
||||
import {
|
||||
@ -40,11 +48,17 @@
|
||||
getProjectDocsHierarchy,
|
||||
isEditableProject,
|
||||
createDocument,
|
||||
canCreateChildDocument
|
||||
canCreateChildDocument,
|
||||
moveDocument,
|
||||
moveDocumentBefore,
|
||||
moveDocumentAfter
|
||||
} from '../../utils'
|
||||
|
||||
import documents from '../../plugin'
|
||||
|
||||
import DropArea from './DropArea.svelte'
|
||||
import DropMarker from './DropMarker.svelte'
|
||||
|
||||
export let space: DocumentSpace
|
||||
export let currentSpace: Ref<Space> | undefined
|
||||
export let currentFragment: string | undefined
|
||||
@ -67,6 +81,7 @@
|
||||
let project: Ref<Project> = documents.ids.NoProject
|
||||
$: void selectProject(space)
|
||||
|
||||
let docsByMeta = new Map<Ref<DocumentMeta>, WithLookup<ProjectMeta>>()
|
||||
let rootDocs: Array<WithLookup<ProjectMeta>> = []
|
||||
let childrenByParent: Record<Ref<DocumentMeta>, Array<WithLookup<ProjectMeta>>> = {}
|
||||
|
||||
@ -78,6 +93,7 @@
|
||||
project
|
||||
},
|
||||
(result) => {
|
||||
docsByMeta = new Map(result.map((r) => [r.meta, r]))
|
||||
;({ rootDocs, childrenByParent } = getProjectDocsHierarchy(result))
|
||||
},
|
||||
{
|
||||
@ -116,6 +132,23 @@
|
||||
selectedControlledDoc = undefined
|
||||
}
|
||||
|
||||
function getAllDescendants (obj: Ref<DocumentMeta>): Ref<DocumentMeta>[] {
|
||||
const result: Ref<DocumentMeta>[] = []
|
||||
const queue: Ref<DocumentMeta>[] = [obj]
|
||||
|
||||
while (queue.length > 0) {
|
||||
const next = queue.pop()
|
||||
if (next === undefined) break
|
||||
|
||||
const children = childrenByParent[next] ?? []
|
||||
const childrenRefs = children.map((p) => p.meta)
|
||||
result.push(...childrenRefs)
|
||||
queue.push(...childrenRefs)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
async function selectProject (space: DocumentSpace): Promise<void> {
|
||||
project = getCurrentProject(space._id) ?? (await getLatestProjectId(space._id, true)) ?? documents.ids.NoProject
|
||||
}
|
||||
@ -181,75 +214,202 @@
|
||||
|
||||
return actions
|
||||
}
|
||||
|
||||
let parent: HTMLElement
|
||||
let draggedItem: Ref<DocumentMeta> | undefined = undefined
|
||||
let draggedOver: Ref<DocumentMeta> | undefined = undefined
|
||||
let draggedOverPos: 'before' | 'after' | undefined = undefined
|
||||
let draggedOverTop: number = 0
|
||||
let cannotDropTo: Ref<DocumentMeta>[] = []
|
||||
|
||||
function canDrop (object: Ref<DocumentMeta>, target: Ref<DocumentMeta>): boolean {
|
||||
if (object === target) return false
|
||||
if (cannotDropTo.includes(target)) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
function onDragStart (event: DragEvent, object: Ref<DocumentMeta>): 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<DocumentMeta>): 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<DocumentMeta>): void {
|
||||
event.preventDefault()
|
||||
if (event.dataTransfer === null) {
|
||||
return
|
||||
}
|
||||
if (draggedItem !== undefined && canDrop(draggedItem, object)) {
|
||||
const doc = docsByMeta.get(draggedItem)
|
||||
const target = docsByMeta.get(object)
|
||||
|
||||
if (doc !== undefined && target !== undefined && doc._id !== target._id) {
|
||||
if (object === documents.ids.NoParent) {
|
||||
void moveDocument(doc, doc.space)
|
||||
} 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.parent !== object) {
|
||||
void moveDocument(doc, target.space, target)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
draggedItem = undefined
|
||||
draggedOver = undefined
|
||||
}
|
||||
</script>
|
||||
|
||||
<TreeNode
|
||||
_id={space?._id}
|
||||
folderIcon
|
||||
iconProps={{
|
||||
fill: getPlatformColorForTextDef(space.name, $themeStore.dark).icon
|
||||
}}
|
||||
title={space.name}
|
||||
highlighted={space._id === currentSpace && currentFragment !== undefined && !deselect}
|
||||
visible={(space._id === currentSpace && currentFragment !== undefined && !deselect) || forciblyСollapsed}
|
||||
showMenu={pressed}
|
||||
{forciblyСollapsed}
|
||||
actions={() => getSpaceActions(space)}
|
||||
type={'nested'}
|
||||
>
|
||||
<svelte:fragment slot="extra">
|
||||
{#if spaceType?.projects === true}
|
||||
<ProjectSelector
|
||||
value={project}
|
||||
space={space?._id}
|
||||
maxWidth={'6rem'}
|
||||
kind={'ghost'}
|
||||
size={'x-small'}
|
||||
showDropdownIcon
|
||||
bind:pressed
|
||||
on:change={(evt) => {
|
||||
project = evt.detail
|
||||
setCurrentProject(space._id, project)
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
|
||||
{#if rootDocs.length > 0}
|
||||
<DocHierarchyLevel
|
||||
projectMeta={rootDocs}
|
||||
{childrenByParent}
|
||||
{selected}
|
||||
getMoreActions={getDocumentActions}
|
||||
on:selected={(e) => {
|
||||
handleDocumentSelected(e.detail)
|
||||
}}
|
||||
/>
|
||||
{:else}
|
||||
<div class="pseudo-element flex-row-center content-dark-color text-md nowrap">
|
||||
<Label label={documents.string.NoDocuments} />
|
||||
</div>
|
||||
<div bind:this={parent} class="flex-col relative">
|
||||
{#if draggedOver === documents.ids.NoParent}
|
||||
<DropArea />
|
||||
{/if}
|
||||
|
||||
<svelte:fragment slot="visible">
|
||||
{#if (selected || forciblyСollapsed) && selectedControlledDoc}
|
||||
{@const doc = selectedControlledDoc}
|
||||
<TreeItem
|
||||
_id={doc._id}
|
||||
icon={documents.icon.Document}
|
||||
iconProps={{
|
||||
fill: 'currentColor'
|
||||
{#if draggedOver && draggedOverPos}
|
||||
<DropMarker top={draggedOverTop} />
|
||||
{/if}
|
||||
|
||||
<TreeNode
|
||||
_id={space?._id}
|
||||
folderIcon
|
||||
iconProps={{
|
||||
fill: getPlatformColorForTextDef(space.name, $themeStore.dark).icon
|
||||
}}
|
||||
title={space.name}
|
||||
highlighted={space._id === currentSpace && currentFragment !== undefined && !deselect}
|
||||
visible={(space._id === currentSpace && currentFragment !== undefined && !deselect) || forciblyСollapsed}
|
||||
showMenu={pressed}
|
||||
{forciblyСollapsed}
|
||||
actions={() => getSpaceActions(space)}
|
||||
type={'nested'}
|
||||
draggable
|
||||
on:drop={(evt) => {
|
||||
onDrop(evt, documents.ids.NoParent)
|
||||
}}
|
||||
on:dragover={(evt) => {
|
||||
onDragOver(evt, documents.ids.NoParent)
|
||||
}}
|
||||
on:dragstart={(evt) => {
|
||||
evt.preventDefault()
|
||||
}}
|
||||
>
|
||||
<svelte:fragment slot="extra">
|
||||
{#if spaceType?.projects === true}
|
||||
<ProjectSelector
|
||||
value={project}
|
||||
space={space?._id}
|
||||
maxWidth={'6rem'}
|
||||
kind={'ghost'}
|
||||
size={'x-small'}
|
||||
showDropdownIcon
|
||||
bind:pressed
|
||||
on:change={(evt) => {
|
||||
project = evt.detail
|
||||
setCurrentProject(space._id, project)
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
|
||||
{#if rootDocs.length > 0}
|
||||
<DocHierarchyLevel
|
||||
projectMeta={rootDocs}
|
||||
{childrenByParent}
|
||||
{selected}
|
||||
getMoreActions={getDocumentActions}
|
||||
on:selected={(e) => {
|
||||
handleDocumentSelected(e.detail)
|
||||
}}
|
||||
title={getDocumentName(doc)}
|
||||
actions={() => getDocumentActions(doc)}
|
||||
selected
|
||||
isFold
|
||||
empty
|
||||
forciblyСollapsed
|
||||
{onDragStart}
|
||||
{onDragEnd}
|
||||
{onDragOver}
|
||||
{onDrop}
|
||||
{draggedItem}
|
||||
{draggedOver}
|
||||
/>
|
||||
{:else}
|
||||
<div class="pseudo-element flex-row-center content-dark-color text-md nowrap">
|
||||
<Label label={documents.string.NoDocuments} />
|
||||
</div>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
</TreeNode>
|
||||
|
||||
<svelte:fragment slot="visible">
|
||||
{#if (selected || forciblyСollapsed) && selectedControlledDoc}
|
||||
{@const doc = selectedControlledDoc}
|
||||
<TreeItem
|
||||
_id={doc._id}
|
||||
icon={documents.icon.Document}
|
||||
iconProps={{
|
||||
fill: 'currentColor'
|
||||
}}
|
||||
title={getDocumentName(doc)}
|
||||
actions={() => getDocumentActions(doc)}
|
||||
selected
|
||||
isFold
|
||||
empty
|
||||
forciblyСollapsed
|
||||
/>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
</TreeNode>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.pseudo-element {
|
||||
|
@ -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>
|
@ -11,51 +11,53 @@
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
import core, {
|
||||
type Class,
|
||||
type Doc,
|
||||
type DocumentQuery,
|
||||
type Hierarchy,
|
||||
type Ref,
|
||||
type Tx,
|
||||
type TxOperations,
|
||||
type Space,
|
||||
type Markup,
|
||||
type Client,
|
||||
type WithLookup,
|
||||
SortingOrder,
|
||||
getCurrentAccount,
|
||||
checkPermission
|
||||
} from '@hcengineering/core'
|
||||
import { type IntlString, translate } from '@hcengineering/platform'
|
||||
import { getClient } from '@hcengineering/presentation'
|
||||
import { type Person, type Employee, type PersonAccount } from '@hcengineering/contact'
|
||||
import request, { RequestStatus } from '@hcengineering/request'
|
||||
import { isEmptyMarkup } from '@hcengineering/text'
|
||||
import { showPopup, getUserTimezone, type Location } from '@hcengineering/ui'
|
||||
import { type KeyFilter } from '@hcengineering/view'
|
||||
import chunter from '@hcengineering/chunter'
|
||||
import { type Employee, type Person, type PersonAccount } from '@hcengineering/contact'
|
||||
import documents, {
|
||||
type ControlledDocument,
|
||||
type Document,
|
||||
type DocumentRequest,
|
||||
type DocumentTemplate,
|
||||
type DocumentSpace,
|
||||
type DocumentCategory,
|
||||
type DocumentMeta,
|
||||
type DocumentComment,
|
||||
type DocumentMeta,
|
||||
type DocumentRequest,
|
||||
type DocumentSpace,
|
||||
type DocumentTemplate,
|
||||
type OrgSpace,
|
||||
type Project,
|
||||
type ProjectDocument,
|
||||
type ProjectMeta,
|
||||
ControlledDocumentState,
|
||||
DocumentState,
|
||||
getDocumentName
|
||||
getDocumentName,
|
||||
getFirstRank
|
||||
} from '@hcengineering/controlled-documents'
|
||||
import { type Request } from '@hcengineering/request'
|
||||
import core, {
|
||||
type Class,
|
||||
type Client,
|
||||
type Doc,
|
||||
type DocumentQuery,
|
||||
type Hierarchy,
|
||||
type Markup,
|
||||
type QuerySelector,
|
||||
type Ref,
|
||||
type Space,
|
||||
type Tx,
|
||||
type TxOperations,
|
||||
type WithLookup,
|
||||
SortingOrder,
|
||||
checkPermission,
|
||||
getCurrentAccount
|
||||
} from '@hcengineering/core'
|
||||
import { type IntlString, translate } from '@hcengineering/platform'
|
||||
import { getClient } from '@hcengineering/presentation'
|
||||
import request, { type Request, RequestStatus } from '@hcengineering/request'
|
||||
import { isEmptyMarkup } from '@hcengineering/text'
|
||||
import { type Location, getUserTimezone, showPopup } from '@hcengineering/ui'
|
||||
import { type KeyFilter } from '@hcengineering/view'
|
||||
|
||||
import documentsResources from './plugin'
|
||||
import { makeRank } from '@hcengineering/rank'
|
||||
import { getProjectDocumentLink } from './navigation'
|
||||
import documentsResources from './plugin'
|
||||
import { wizardOpened } from './stores/wizards/create-document'
|
||||
|
||||
export type TranslatedDocumentStates = Readonly<Record<DocumentState, string>>
|
||||
@ -654,3 +656,41 @@ export function formatSignatureDate (date: number): string {
|
||||
second: 'numeric'
|
||||
})
|
||||
}
|
||||
|
||||
export async function moveDocument (doc: ProjectMeta, space: Ref<Space>, target?: ProjectMeta): Promise<void> {
|
||||
const client = getClient()
|
||||
|
||||
let parent = documents.ids.NoParent
|
||||
let path: Array<Ref<DocumentMeta>> = []
|
||||
if (target !== undefined) {
|
||||
parent = target.meta
|
||||
path = [target.meta, ...target.path]
|
||||
}
|
||||
|
||||
const prevRank = await getFirstRank(client, space, doc.project, parent)
|
||||
const rank = makeRank(prevRank, undefined)
|
||||
|
||||
await client.update(doc, { parent, path, rank })
|
||||
}
|
||||
|
||||
export async function moveDocumentBefore (doc: ProjectMeta, before: ProjectMeta): Promise<void> {
|
||||
const client = getClient()
|
||||
|
||||
const { space, parent, path } = before
|
||||
const query = { rank: { $lt: before.rank } as unknown as QuerySelector<ProjectMeta['rank']> }
|
||||
const lastRank = await getFirstRank(client, space, doc.project, parent, SortingOrder.Descending, query)
|
||||
const rank = makeRank(lastRank, before.rank)
|
||||
|
||||
await client.update(doc, { parent, path, rank })
|
||||
}
|
||||
|
||||
export async function moveDocumentAfter (doc: ProjectMeta, after: ProjectMeta): Promise<void> {
|
||||
const client = getClient()
|
||||
|
||||
const { space, parent, path } = after
|
||||
const query = { rank: { $gt: after.rank } as unknown as QuerySelector<ProjectMeta['rank']> }
|
||||
const nextRank = await getFirstRank(client, space, doc.project, parent, SortingOrder.Ascending, query)
|
||||
const rank = makeRank(after.rank, nextRank)
|
||||
|
||||
await client.update(doc, { parent, path, rank })
|
||||
}
|
||||
|
@ -49,7 +49,8 @@
|
||||
"@hcengineering/training": "^0.1.0",
|
||||
"lexorank": "~1.0.4",
|
||||
"@hcengineering/activity": "^0.6.0",
|
||||
"@hcengineering/chunter": "^0.6.20"
|
||||
"@hcengineering/chunter": "^0.6.20",
|
||||
"@hcengineering/rank": "^0.6.4"
|
||||
},
|
||||
"publishConfig": {
|
||||
"@hcengineering:registry": "https://npm.pkg.github.com"
|
||||
|
@ -27,9 +27,10 @@ import {
|
||||
type ProjectDocument,
|
||||
DocumentState
|
||||
} from './types'
|
||||
import { makeRank } from '@hcengineering/rank'
|
||||
|
||||
import documents from './plugin'
|
||||
import { TEMPLATE_PREFIX } from './utils'
|
||||
import { getFirstRank, TEMPLATE_PREFIX } from './utils'
|
||||
|
||||
async function getParentPath (client: TxOperations, parent: Ref<ProjectDocument>): Promise<Array<Ref<DocumentMeta>>> {
|
||||
const parentDocObj = await client.findOne(documents.class.ProjectDocument, {
|
||||
@ -175,12 +176,16 @@ export async function createControlledDocMetadata (
|
||||
path = await getParentPath(client, parent)
|
||||
}
|
||||
|
||||
const parentMeta = path[0] ?? documents.ids.NoParent
|
||||
const lastRank = await getFirstRank(client, space, projectId, parentMeta)
|
||||
|
||||
const projectMetaId = await ops.createDoc(documents.class.ProjectMeta, space, {
|
||||
project: projectId,
|
||||
meta: documentMetaId,
|
||||
path,
|
||||
parent: path[0] ?? documents.ids.NoParent,
|
||||
documents: 0
|
||||
parent: parentMeta,
|
||||
documents: 0,
|
||||
rank: makeRank(lastRank, undefined)
|
||||
})
|
||||
|
||||
const projectDocumentId = await client.addCollection(
|
||||
@ -323,12 +328,16 @@ export async function createDocumentTemplateMetadata (
|
||||
metaId
|
||||
)
|
||||
|
||||
const parentMeta = path[0] ?? documents.ids.NoParent
|
||||
const lastRank = await getFirstRank(client, space, projectId, parentMeta)
|
||||
|
||||
const projectMetaId = await ops.createDoc(documents.class.ProjectMeta, space, {
|
||||
project: projectId,
|
||||
meta: documentMetaId,
|
||||
path,
|
||||
parent: path[0] ?? documents.ids.NoParent,
|
||||
documents: 0
|
||||
documents: 0,
|
||||
rank: makeRank(lastRank, undefined)
|
||||
})
|
||||
|
||||
const projectDocumentId = await client.addCollection(
|
||||
|
@ -16,7 +16,8 @@ import {
|
||||
type TypedSpace,
|
||||
type Timestamp,
|
||||
SpaceType,
|
||||
SpaceTypeDescriptor
|
||||
SpaceTypeDescriptor,
|
||||
Rank
|
||||
} from '@hcengineering/core'
|
||||
import { type TagReference } from '@hcengineering/tags'
|
||||
import { Request } from '@hcengineering/request'
|
||||
@ -91,6 +92,8 @@ export interface ProjectMeta extends Doc {
|
||||
// head: Ref<HierarchyDocument>
|
||||
|
||||
documents: CollectionSize<ProjectDocument>
|
||||
|
||||
rank: Rank
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -12,7 +12,17 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
import { ApplyOperations, Data, DocumentUpdate, Ref, TxOperations } from '@hcengineering/core'
|
||||
import {
|
||||
ApplyOperations,
|
||||
Data,
|
||||
DocumentQuery,
|
||||
DocumentUpdate,
|
||||
Rank,
|
||||
Ref,
|
||||
SortingOrder,
|
||||
Space,
|
||||
TxOperations
|
||||
} from '@hcengineering/core'
|
||||
import { LexoDecimal, LexoNumeralSystem36, LexoRank } from 'lexorank'
|
||||
import LexoRankBucket from 'lexorank/lib/lexoRank/lexoRankBucket'
|
||||
|
||||
@ -22,6 +32,7 @@ import {
|
||||
ChangeControl,
|
||||
ControlledDocument,
|
||||
Document,
|
||||
DocumentMeta,
|
||||
DocumentSpace,
|
||||
DocumentState,
|
||||
Project,
|
||||
@ -143,7 +154,8 @@ export async function copyProjectDocuments (
|
||||
meta: meta.meta,
|
||||
path: meta.path,
|
||||
parent: meta.parent,
|
||||
documents: meta.documents
|
||||
documents: meta.documents,
|
||||
rank: meta.rank
|
||||
})
|
||||
|
||||
// copy project docs attached to meta
|
||||
@ -165,6 +177,26 @@ export async function copyProjectDocuments (
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export async function getFirstRank (
|
||||
client: TxOperations,
|
||||
space: Ref<Space>,
|
||||
project: Ref<Project>,
|
||||
parent: Ref<DocumentMeta>,
|
||||
sort: SortingOrder = SortingOrder.Descending,
|
||||
extra: DocumentQuery<ProjectMeta> = {}
|
||||
): Promise<Rank | undefined> {
|
||||
const doc = await client.findOne(
|
||||
documents.class.ProjectMeta,
|
||||
{ space, project, parent, ...extra },
|
||||
{ sort: { rank: sort }, projection: { rank: 1 } }
|
||||
)
|
||||
|
||||
return doc?.rank
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
|
@ -108,6 +108,7 @@
|
||||
on:click
|
||||
on:dragstart
|
||||
on:dragover
|
||||
on:dragend
|
||||
on:drop
|
||||
on:toggle={(ev) => {
|
||||
if (ev.detail !== undefined) collapsed = !ev.detail
|
||||
@ -176,6 +177,7 @@
|
||||
on:click
|
||||
on:dragstart
|
||||
on:dragover
|
||||
on:dragend
|
||||
on:drop
|
||||
>
|
||||
<slot />
|
||||
|
@ -39,6 +39,7 @@
|
||||
export let showNotify: boolean = false
|
||||
export let forciblyСollapsed: boolean = false
|
||||
export let collapsedPrefix: string = ''
|
||||
export let draggable: boolean = false
|
||||
</script>
|
||||
|
||||
<TreeElement
|
||||
@ -62,7 +63,12 @@
|
||||
{showMenu}
|
||||
{noDivider}
|
||||
{forciblyСollapsed}
|
||||
{draggable}
|
||||
on:click
|
||||
on:dragstart
|
||||
on:dragover
|
||||
on:dragend
|
||||
on:drop
|
||||
>
|
||||
<svelte:fragment slot="extra"><slot name="extra" /></svelte:fragment>
|
||||
<svelte:fragment slot="dropbox"><slot name="dropbox" /></svelte:fragment>
|
||||
|
Loading…
Reference in New Issue
Block a user