Updated navigator layout (#5619)

Signed-off-by: Alexander Platov <alexander.platov@hardcoreeng.com>
This commit is contained in:
Alexander Platov
2024-06-05 12:50:13 +07:00
committed by GitHub
parent 07d35f0827
commit a437926f35
79 changed files with 1582 additions and 1012 deletions
@@ -26,14 +26,13 @@
getDocumentName,
DocumentState
} from '@hcengineering/controlled-documents'
import DocHierarchyTreeElement from './DocHierarchyTreeElement.svelte'
import { TreeItem } from '@hcengineering/view-resources'
import { compareDocs } from '../../utils'
export let projectMeta: ProjectMeta[] = []
export let childrenByParent: Record<Ref<DocumentMeta>, Array<ProjectMeta>>
export let selected: Ref<Doc> | undefined
export let level = 1
export let level: number = 0
export let getMoreActions: ((obj: Doc, originalEvent?: MouseEvent) => Promise<Action[]>) | undefined = undefined
export let collapsedPrefix: string = ''
@@ -111,33 +110,36 @@
{#if doc}
{@const children = childrenByParent[doc.attachedTo] ?? []}
<DocHierarchyTreeElement
docId={doc._id}
<TreeItem
_id={doc._id}
icon={documents.icon.Document}
iconProps={{
fill: 'currentColor'
}}
title={getDocumentName(doc)}
selected={selected === prjdoc._id}
parent={children.length > 0}
getMoreActions={getMoreActions !== undefined ? () => getDocMoreActions(prjdoc) : undefined}
selected={selected === doc._id || selected === prjdoc._id}
isFold
empty={children.length === 0 || children === undefined}
actions={getMoreActions !== undefined ? () => getDocMoreActions(prjdoc) : undefined}
{level}
{collapsedPrefix}
on:click={() => {
dispatch('selected', prjdoc)
}}
>
{#if children.length}
<svelte:self
projectMeta={children}
{childrenByParent}
{selected}
{collapsedPrefix}
{getMoreActions}
level={level + 1}
on:selected
/>
{/if}
</DocHierarchyTreeElement>
<svelte:fragment slot="dropbox">
{#if children.length}
<svelte:self
projectMeta={children}
{childrenByParent}
{selected}
{collapsedPrefix}
{getMoreActions}
level={level + 1}
on:selected
/>
{/if}
</svelte:fragment>
</TreeItem>
{/if}
{/each}
@@ -1,102 +0,0 @@
<!--
// Copyright © 2023 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
import { type Ref } from '@hcengineering/core'
import { type Asset, getEmbeddedLabel } from '@hcengineering/platform'
import { type Action, type AnySvelteComponent, tooltip } from '@hcengineering/ui'
import {
Icon,
IconChevronDown,
IconMoreH,
Menu,
getTreeCollapsed,
setTreeCollapsed,
showPopup
} from '@hcengineering/ui'
import { DocumentSpace } from '@hcengineering/controlled-documents'
import { createEventDispatcher } from 'svelte'
export let _id: Ref<DocumentSpace>
export let icon: Asset | AnySvelteComponent | undefined = undefined
export let iconProps: Record<string, any> | undefined = undefined
export let title: string
export let selected: boolean = false
export let getMoreActions: ((originalEvent?: MouseEvent) => Promise<Action[]>) | undefined = undefined
export let collapsed: boolean = getTreeCollapsed(_id)
const dispatch = createEventDispatcher()
let hovered = false
async function onMenuClick (ev: MouseEvent): Promise<void> {
if (getMoreActions === undefined) {
return
}
hovered = true
const actions = await getMoreActions(ev)
showPopup(Menu, { actions, ctx: _id }, ev.target as HTMLElement, () => {
hovered = false
})
}
$: if (_id !== undefined) collapsed = getTreeCollapsed(_id)
$: setTreeCollapsed(_id, collapsed)
</script>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div
class="antiNav-element tree parent"
class:selected
class:hovered
class:collapsed
on:click={() => {
collapsed = !collapsed
dispatch('click')
}}
>
{#if icon}
<div class="an-element__icon folder">
<Icon {icon} {iconProps} size={'small'} />
</div>
{/if}
<span class="an-element__label" use:tooltip={{ label: getEmbeddedLabel(title) }}>
{title}
</span>
<div
class="an-element__tool arrow hidden"
on:click|preventDefault|stopPropagation={() => {
collapsed = !collapsed
}}
>
<IconChevronDown size={'small'} />
</div>
<div class="an-element__grow" />
<slot name="extra" />
<div class="w-6">
<div class="an-element__tool" class:pressed={hovered} on:click|preventDefault|stopPropagation={onMenuClick}>
<IconMoreH size={'small'} />
</div>
</div>
</div>
{#if !collapsed}
<div class="antiNav-element__dropbox"><slot /></div>
{/if}
@@ -1,103 +0,0 @@
<!--
// Copyright © 2023 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
import { type Doc, type Ref } from '@hcengineering/core'
import { type Asset, getEmbeddedLabel } from '@hcengineering/platform'
import type { Action, AnySvelteComponent } from '@hcengineering/ui'
import { Icon, IconChevronDown, IconMoreH, Menu, showPopup, tooltip } from '@hcengineering/ui'
import { getPrefixedTreeCollapsed, setPrefixedTreeCollapsed } from '../../utils'
export let docId: Ref<Doc>
export let icon: Asset | AnySvelteComponent | undefined = undefined
export let iconProps: Record<string, any> | undefined = undefined
export let title: string
export let parent: boolean = false
export let selected: boolean = false
export let folder: boolean = false
export let level: number = 0
export let getMoreActions: ((originalEvent?: MouseEvent) => Promise<Action[]>) | undefined = undefined
export let collapsedPrefix: string = ''
export let collapsed: boolean = getPrefixedTreeCollapsed(docId, collapsedPrefix)
let hovered = false
async function onMenuClick (ev: MouseEvent): Promise<void> {
if (getMoreActions === undefined) {
return
}
showPopup(Menu, { actions: await getMoreActions(ev), ctx: docId }, ev.target as HTMLElement, () => {
hovered = false
})
hovered = true
}
$: if (docId !== undefined) collapsed = getPrefixedTreeCollapsed(docId, collapsedPrefix)
$: setPrefixedTreeCollapsed(docId, collapsedPrefix, collapsed)
$: hasLeftCollapser = parent && !folder
$: hasActions = getMoreActions !== undefined
</script>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div
class="antiNav-element tree"
class:selected
class:hovered
class:parent
class:collapsed
style:padding-left={`${level > 0 ? level * 1.25 + 0.125 : 0.75}rem`}
on:click
>
<div
class="an-element__tool arrow"
class:empty={!hasLeftCollapser}
on:click|preventDefault|stopPropagation={() => {
if (hasLeftCollapser) collapsed = !collapsed
}}
>
{#if hasLeftCollapser}<IconChevronDown size={'small'} filled />{/if}
</div>
{#if icon}
<div class="an-element__icon" class:folder={level === 0}>
<Icon {icon} {iconProps} size={'small'} />
</div>
{/if}
<span class="an-element__label" use:tooltip={{ label: getEmbeddedLabel(title) }}>
{title}
</span>
{#if parent && folder}
<div
class="an-element__tool arrow hidden"
on:click|preventDefault|stopPropagation={() => {
if (parent) collapsed = !collapsed
}}
>
<IconChevronDown size={'small'} />
</div>
{/if}
<div class="an-element__grow" />
{#if hasActions}
<div class="an-element__tool" class:pressed={hovered} on:click|preventDefault|stopPropagation={onMenuClick}>
<IconMoreH size={'small'} />
</div>
{/if}
</div>
{#if parent && !collapsed}
<div class="antiNav-element__dropbox"><slot /></div>
{/if}
@@ -16,19 +16,17 @@
import { createEventDispatcher } from 'svelte'
import { WithLookup, type Doc, type Ref } from '@hcengineering/core'
import { createQuery } from '@hcengineering/presentation'
import { getPlatformColorForTextDef, themeStore } from '@hcengineering/ui'
import { getPlatformColorForTextDef, themeStore, getTreeCollapsed } from '@hcengineering/ui'
import documents, {
type DocumentMeta,
type DocumentSpace,
type Project,
type ProjectMeta
} from '@hcengineering/controlled-documents'
import { TreeNode } from '@hcengineering/view-resources'
import FolderCollapsed from '../icons/FolderCollapsed.svelte'
import FolderExpanded from '../icons/FolderExpanded.svelte'
import DocHierarchyLevel from './DocHierarchyLevel.svelte'
import DocHierarchyTreeElement from './DocHierarchyTreeElement.svelte'
import { getPrefixedTreeCollapsed, setPrefixedTreeCollapsed, getProjectDocsHierarchy } from '../../utils'
import { getProjectDocsHierarchy } from '../../utils'
export let space: DocumentSpace
export let project: Ref<Project> | undefined
@@ -36,9 +34,9 @@
export let collapsedPrefix: string = ''
const dispatch = createEventDispatcher()
let collapsed: boolean = getPrefixedTreeCollapsed(space._id, collapsedPrefix)
$: setPrefixedTreeCollapsed(space._id, collapsedPrefix, collapsed)
$: folderIcon = collapsed ? FolderCollapsed : FolderExpanded
// let collapsed: boolean = getPrefixedTreeCollapsed(space._id, collapsedPrefix)
// $: setPrefixedTreeCollapsed(space._id, collapsedPrefix, collapsed)
// $: folderIcon = collapsed ? FolderCollapsed : FolderExpanded
let rootDocs: Array<WithLookup<ProjectMeta>> = []
let childrenByParent: Record<Ref<DocumentMeta>, Array<WithLookup<ProjectMeta>>> = {}
@@ -56,29 +54,21 @@
)
</script>
<div class="root">
<DocHierarchyTreeElement
bind:collapsed
docId={space?._id}
icon={folderIcon}
iconProps={{
fill: getPlatformColorForTextDef(space.name, $themeStore.dark).icon
}}
title={space.name}
folder
selected={selected === undefined}
parent={rootDocs.length > 0}
{collapsedPrefix}
on:click={() => {
dispatch('selected', space)
}}
>
<DocHierarchyLevel projectMeta={rootDocs} {childrenByParent} {selected} {collapsedPrefix} on:selected />
</DocHierarchyTreeElement>
</div>
<style lang="scss">
:global(.root .antiNav-element) {
margin: 0;
}
</style>
<TreeNode
_id={space?._id}
folderIcon
iconProps={{
fill: getPlatformColorForTextDef(space.name, $themeStore.dark).icon
}}
title={space.name}
highlighted={selected !== undefined}
selected={selected === undefined}
empty={rootDocs.length === 0}
{collapsedPrefix}
type={'nested-selectable'}
on:click={() => {
dispatch('selected', space)
}}
>
<DocHierarchyLevel projectMeta={rootDocs} {childrenByParent} {selected} {collapsedPrefix} on:selected />
</TreeNode>
@@ -16,32 +16,22 @@
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,
getTreeCollapsed,
setTreeCollapsed,
getPlatformColorForTextDef,
themeStore,
navigate,
IconEdit,
Label
} from '@hcengineering/ui'
import { getActions as getContributedActions } from '@hcengineering/view-resources'
import { type Action, getPlatformColorForTextDef, themeStore, navigate, IconEdit, Label } from '@hcengineering/ui'
import { getActions as getContributedActions, TreeNode, TreeItem } from '@hcengineering/view-resources'
import { ActionGroup } from '@hcengineering/view'
import {
type ControlledDocument,
type DocumentMeta,
type DocumentSpace,
type DocumentSpaceType,
type Project,
type ProjectDocument,
type ProjectMeta
type ProjectMeta,
getDocumentName
} from '@hcengineering/controlled-documents'
import FolderCollapsed from '../icons/FolderCollapsed.svelte'
import FolderExpanded from '../icons/FolderExpanded.svelte'
import ProjectSelector from '../project/ProjectSelector.svelte'
import DocHierarchyLevel from './DocHierarchyLevel.svelte'
import DocHierarchyRootElement from './DocHierarchyRootElement.svelte'
import { getDocumentIdFromFragment, getProjectDocumentLink } from '../../navigation'
import {
getCurrentProject,
@@ -55,12 +45,16 @@
import documents from '../../plugin'
export let space: DocumentSpace
export let currentSpace: Ref<Space> | undefined
export let currentFragment: string | undefined
export let getActions: (space: Space) => Promise<Action[]> = async () => []
export let deselect: boolean = false
export let forciblyСollapsed: boolean = false
const client = getClient()
let spaceType: DocumentSpaceType | undefined
let pressed: boolean = false
const spaceTypeQuery = createQuery()
$: spaceTypeQuery.query(documents.class.DocumentSpaceType, { _id: space.type }, (result) => {
@@ -69,10 +63,6 @@
$: selected = getDocumentIdFromFragment(currentFragment ?? '')
let collapsed: boolean = getTreeCollapsed(space._id)
$: setTreeCollapsed(space._id, collapsed)
$: folderIcon = collapsed ? FolderCollapsed : FolderExpanded
let project: Ref<Project> = documents.ids.NoProject
$: void selectProject(space)
@@ -96,6 +86,35 @@
}
)
let selectedControlledDoc: ControlledDocument | undefined = undefined
$: if (selected !== undefined) {
void client
.findOne(
documents.class.ProjectDocument,
{ _id: selected },
{
lookup: {
document: documents.class.ControlledDocument
}
}
)
.then((result) => {
if (result !== undefined) {
selectedControlledDoc = result.$lookup?.document as ControlledDocument
} else {
// There's some issue with resolving which needs to be fixed later
void client
.findOne(documents.class.ControlledDocument, { _id: selected as unknown as Ref<ControlledDocument> })
.then((result) => {
selectedControlledDoc = result
})
}
})
} else {
selectedControlledDoc = undefined
}
async function selectProject (space: DocumentSpace): Promise<void> {
project = getCurrentProject(space._id) ?? (await getLatestProjectId(space._id)) ?? documents.ids.NoProject
}
@@ -161,17 +180,21 @@
}
</script>
<DocHierarchyRootElement
bind:collapsed
<TreeNode
_id={space?._id}
icon={folderIcon}
folderIcon
iconProps={{
fill: getPlatformColorForTextDef(space.name, $themeStore.dark).icon
}}
title={space.name}
getMoreActions={() => getSpaceActions(space)}
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'}
>
<div slot="extra">
<svelte:fragment slot="extra">
{#if spaceType?.projects === true}
<ProjectSelector
value={project}
@@ -180,13 +203,14 @@
kind={'ghost'}
size={'x-small'}
showDropdownIcon
bind:pressed
on:change={(evt) => {
project = evt.detail
setCurrentProject(space._id, project)
}}
/>
{/if}
</div>
</svelte:fragment>
{#if rootDocs.length > 0}
<DocHierarchyLevel
@@ -203,7 +227,26 @@
<Label label={documents.string.NoDocuments} />
</div>
{/if}
</DocHierarchyRootElement>
<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>
<style lang="scss">
.pseudo-element {
@@ -26,7 +26,8 @@
export let value: Ref<Project> | undefined
export let space: Ref<Space>
export let disabled = false
export let disabled: boolean = false
export let pressed: boolean = false
export let focusIndex = -1
export let maxWidth = ''
export let kind: ButtonKind = 'link'
@@ -48,11 +49,13 @@
function showVersionsPopup (): void {
if (disabled) return
pressed = true
showPopup(ProjectSelectorPopup, { space, showReadonly, selected: value }, container, (result) => {
if (result !== undefined) {
value = result
dispatch('change', result)
}
pressed = false
})
}
@@ -68,6 +71,7 @@
{size}
{justify}
{disabled}
stopPropagation
on:click={showVersionsPopup}
showTooltip={{
label: project !== undefined ? getEmbeddedLabel(project.name) : documents.string.Project