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

Signed-off-by: Victor Ilyushchenko <alt13ri@gmail.com>
This commit is contained in:
Victor Ilyushchenko 2025-01-15 12:29:30 +03:00 committed by GitHub
parent ff5b57dd47
commit 0c6feb646e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 562 additions and 151 deletions

View File

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

View File

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

View File

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

View File

@ -101,6 +101,7 @@
{draggable}
on:dragstart
on:dragover
on:dragend
on:drop
>
{#if isFold && !empty}

View File

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

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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