Updated navigator layout (#5619)

Signed-off-by: Alexander Platov <alexander.platov@hardcoreeng.com>
This commit is contained in:
Alexander Platov 2024-06-05 08:50:13 +03:00 committed by GitHub
parent 07d35f0827
commit a437926f35
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
79 changed files with 1582 additions and 1012 deletions

View File

@ -360,6 +360,7 @@ input.search {
color: var(--theme-content-color);
}
}
&.colorInherit .label { color: inherit; }
}
.flex-presenter {
display: flex;

View File

@ -24,6 +24,7 @@
--spacing-1_5: 0.75rem;
--spacing-1_75: 0.875rem;
--spacing-2: 1rem;
--spacing-2_25: 1.125rem;
--spacing-2_5: 1.25rem;
--spacing-2_75: 1.375rem;
--spacing-3: 1.5rem;

View File

@ -315,7 +315,7 @@
}
.antiNav-divider {
flex-shrink: 0;
margin: .5rem 0;
margin: .75rem 0;
height: 1px;
&.line { background-color: var(--theme-navpanel-divider); }

View File

@ -747,7 +747,7 @@
&:is(.small, .large) .hulyAccordionItem-header__chevron > * {
transform: rotate(90deg);
}
}
}
}
.hulyAccordionItem-content {
overflow: hidden;

View File

@ -39,12 +39,10 @@
button.type-link { width: 100%; }
}
.hulyNavPanel-container .hulyNavItem-container,
.hulyNavPanel-container .hulyTaskNavLink-container {
.hulyNavPanel-container .hulyTaskNavLink-container,
.parentSelector .hulyNavItem-container {
margin: 0 0.75rem;
}
.hulyNavPanel-container .hulyNavItem-container + .hulyAccordionItem-container {
margin-top: 0.75rem;
}
.hulyNavPanel-header {
display: flex;
justify-content: space-between;
@ -65,6 +63,156 @@
}
&.small { padding-bottom: .75rem; }
}
.hulyNavGroup-container {
display: flex;
flex-direction: column;
flex-shrink: 0;
width: 100%;
min-width: 0;
min-height: 0;
.hulyNavGroup-header {
display: flex;
justify-content: space-between;
align-items: center;
flex-shrink: 0;
margin: 0;
padding: var(--spacing-1) var(--spacing-2_25) var(--spacing-1) var(--spacing-2);
min-height: var(--global-medium-Size);
border: none;
outline: none;
&.disabled { cursor: default; }
&__chevron,
&__arrow,
&__icon {
display: flex;
justify-content: center;
align-items: center;
flex-shrink: 0;
}
&__chevron {
margin: 0 0.125rem;
width: 1.25rem;
height: 1.25rem;
color: var(--global-disabled-TextColor);
border: 1px solid transparent;
border-radius: var(--extra-small-BorderRadius);
& > * {
transform-origin: center;
transform: rotate(0deg);
transition: transform 0.1s ease-in-out;
}
&.collapsed > * { transform: rotate(-90deg); }
}
&__icon {
margin: 0 0.125rem;
width: 1.5rem;
height: 1.5rem;
color: var(--global-primary-TextColor);
background-color: var(--theme-navpanel-selected);
border: 1px solid var(--global-subtle-ui-BorderColor);
border-radius: var(--extra-small-BorderRadius);
&.folder {
background-color: var(--theme-statusbar-color);
border-color: var(--global-surface-01-BorderColor);
}
}
&__label {
display: inline-flex;
align-items: center;
gap: var(--spacing-0_5);
padding: var(--spacing-0_25) var(--spacing-0_5);
min-width: 0;
text-transform: uppercase;
color: var(--global-tertiary-TextColor);
border-radius: var(--extra-small-BorderRadius);
}
&__tools {
display: none;
align-items: center;
flex-shrink: 0;
gap: var(--spacing-0_5);
margin-left: var(--spacing-1);
min-width: 0;
max-width: 50%;
}
&__arrow {
margin-left: var(--spacing-0_5);
width: var(--global-min-Size);
height: var(--global-min-Size);
color: var(--global-accent-IconColor);
}
&.showMenu,
&.highlighted,
&.selected,
&:hover {
.hulyNavGroup-header__tools { display: flex; }
}
&.showMenu,
&.highlighted,
&.selected {
.hulyNavGroup-header__label { background-color: var(--global-ui-BackgroundColor); }
.hulyNavGroup-header__chevron { color: var(--global-tertiary-TextColor); }
.hulyNavGroup-header__label { color: var(--global-secondary-TextColor); }
}
&.selected { background-color: var(--global-ui-highlight-BackgroundColor); }
&.isOpen + .hulyNavGroup-content {
max-height: 100%;
&:not(:has(.nested)) { margin-bottom: var(--spacing-1_5); }
}
}
&.nested .hulyNavGroup-header {
.hulyNavGroup-header__icon { margin: 0 var(--spacing-0_25) 0 0; }
.hulyNavGroup-header__label { padding: 0 var(--spacing-0_75) 0 0; }
&.isOpen .hulyNavGroup-header__label { background-color: var(--global-ui-BackgroundColor); }
}
.hulyNavGroup-header:hover,
&.nested .hulyNavGroup-header:hover {
.hulyNavGroup-header__chevron { background-color: var(--global-ui-BackgroundColor); }
.hulyNavGroup-header__label { background-color: var(--global-ui-hover-BackgroundColor); }
.hulyNavGroup-header__chevron { color: var(--button-subtle-IconColor); }
.hulyNavGroup-header__label { color: var(--global-primary-TextColor); }
}
&.nested.selectable .hulyNavGroup-header {
margin: 0 var(--spacing-1_5) var(--spacing-1_5);
padding: var(--spacing-0_5) var(--spacing-0_75) var(--spacing-0_5) var(--spacing-0_5);
min-height: var(--global-small-Size);
border-radius: var(--small-BorderRadius);
&.selected { padding-right: var(--spacing-0_75); }
&.isOpen { margin-bottom: 0; }
}
&:not(.nested, .noDivider),
&.noDivider + &.noDivider { border-top: 1px solid var(--theme-navpanel-divider); }
.hulyNavGroup-content {
overflow: hidden;
display: flex;
flex-direction: column;
padding: 0;
min-width: 0;
max-height: 0;
}
}
.hulyNavItem-container + .hulyNavGroup-container,
.hulyNavPanel-container a.noUnderline + .hulyNavGroup-container { margin-top: .75rem; }
.hulyNavItem-container .hulyNavItem-chevron:enabled {
& > * {
transform-origin: center;
transform: rotate(-90deg);
transition: transform 0.1s ease-in-out;
}
&.isOpen > * { transform: rotate(0deg); }
}
.hulySidePanel-container {
display: flex;
flex-direction: column;

View File

@ -49,6 +49,7 @@
} else if (type === 'type-button' && !hasMenu) {
actualIconSize = 'medium'
}
$: iconOnly = title === undefined && label === undefined && $$slots.default === undefined && icon !== undefined
export function focus () {
element?.focus()
@ -94,6 +95,7 @@
class:inheritColor
class:inheritFont
class:menu={hasMenu}
class:iconOnly
disabled={loading || disabled}
use:tp={tooltip}
on:click|stopPropagation
@ -153,9 +155,10 @@
height: var(--global-large-Size);
border-radius: var(--medium-BorderRadius);
&.type-button {
&.type-button:not(.iconOnly) {
padding: 0 var(--spacing-2);
}
&.iconOnly,
&.type-button-icon {
width: var(--global-large-Size);
}
@ -164,9 +167,10 @@
height: var(--global-medium-Size);
border-radius: var(--medium-BorderRadius);
&.type-button {
&.type-button:not(.iconOnly) {
padding: 0 var(--spacing-2);
}
&.iconOnly,
&.type-button-icon {
width: var(--global-medium-Size);
}
@ -176,9 +180,10 @@
gap: var(--spacing-0_5);
border-radius: var(--small-BorderRadius);
&.type-button {
&.type-button:not(.iconOnly) {
padding: 0 var(--spacing-1);
}
&.iconOnly,
&.type-button-icon {
width: var(--global-small-Size);
}
@ -187,9 +192,10 @@
height: var(--global-extra-small-Size);
border-radius: var(--extra-small-BorderRadius);
&.type-button {
&.type-button:not(.iconOnly) {
padding: 0 var(--spacing-1);
}
&.iconOnly,
&.type-button-icon {
width: var(--global-extra-small-Size);
}

View File

@ -30,6 +30,7 @@
export let inheritColor: boolean = false
export let tooltip: LabelAndProps | undefined = undefined
export let focusIndex = -1
export let id: string | undefined = undefined
let element: ButtonBase | undefined
@ -53,6 +54,7 @@
{hasMenu}
{tooltip}
{focusIndex}
{id}
on:click
on:keydown
/>

View File

@ -16,7 +16,7 @@
import type { Asset, IntlString } from '@hcengineering/platform'
import { deepEqual } from 'fast-equals'
import { ComponentType, createEventDispatcher } from 'svelte'
import { closePopup, showPopup } from '..'
import { closePopup, showPopup, type LabelAndProps, closeTooltip } from '..'
import { AnySvelteComponent, DropdownIntlItem } from '../types'
import ButtonBase from './ButtonBase.svelte'
import ModernPopup from './ModernPopup.svelte'
@ -32,6 +32,8 @@
export let loading: boolean = false
export let inheritColor: boolean = false
export let noSelection: boolean = false
export let autoSelectionIfOne: boolean = false
export let tooltip: LabelAndProps | undefined = undefined
export let items: DropdownIntlItem[]
export let params: Record<string, any> = {}
@ -46,7 +48,13 @@
function openPopup () {
if (!opened) {
if (autoSelectionIfOne && items.length === 1) {
selected = items[0].id
dispatch('selected', selected)
return
}
opened = true
closeTooltip()
showPopup(ModernPopup, { items, selected: noSelection ? undefined : selected, params }, element, (result) => {
if (result) {
selected = result
@ -86,5 +94,6 @@
pressed={opened}
{focusIndex}
{id}
{tooltip}
on:click={openPopup}
/>

View File

@ -15,10 +15,10 @@
<script lang="ts">
export let isOpen: boolean
export let empty: boolean = false
export let level: number = 1
export let level: number = 0
</script>
<div class="hulyFold-container" class:opened={isOpen && !empty} style:margin-left={`${(level - 1) * 1.5}rem`}>
<div class="hulyFold-container" class:opened={isOpen && !empty} style:margin-left={`${level * 1.5}rem`}>
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
{#if empty}
<path

View File

@ -23,6 +23,7 @@
export let inheritFont: boolean = false
export let focusIndex = -1
export let tooltip: LabelAndProps | undefined = undefined
export let id: string | undefined = undefined
</script>
<ButtonBase
@ -41,6 +42,7 @@
{focusIndex}
{tooltip}
{autoFocus}
{id}
on:click
>
<slot />

View File

@ -13,58 +13,119 @@
// limitations under the License.
-->
<script lang="ts">
import type { Doc, Ref } from '@hcengineering/core'
import { createEventDispatcher } from 'svelte'
import type { IntlString } from '@hcengineering/platform'
import type { AnyComponent } from '..'
import { showPopup, Menu, Action, Label, Component, IconOpenedArrow } from '..'
import type { Asset, IntlString } from '@hcengineering/platform'
import { getEmbeddedLabel } from '@hcengineering/platform'
import type { AnyComponent, IconSize, AnySvelteComponent } from '..'
import {
showPopup,
Menu,
Action,
Label,
Component,
IconDown,
IconDownOutline,
IconOpenedArrow,
IconFolderCollapsed,
IconFolderExpanded,
Icon,
getTreeCollapsed,
setTreeCollapsed,
tooltip
} from '..'
export let icon: Asset | AnySvelteComponent | undefined = undefined
export let folderIcon: boolean = false
export let iconProps: any | undefined = undefined
export let iconSize: IconSize = 'small'
export let label: IntlString | undefined = undefined
export let title: string | undefined = undefined
export let categoryName: string
export let tools: AnyComponent | undefined = undefined
export let isOpen: boolean = true
export let isFold: boolean = false
export let empty: boolean = false
export let collapsedPrefix: string = ''
export let visible: boolean = false
export let highlighted: boolean = false
export let selected: boolean = false
export let second: boolean = false
export let type: 'default' | 'nested' | 'nested-selectable' = 'default'
export let noDivider: boolean = false
export let showMenu: boolean = false
export let shouldTooltip: boolean = false
export let forciblyСollapsed: boolean = false
export let actions: Action[] = []
export let _id: Ref<Doc> | string | undefined = undefined
const dispatch = createEventDispatcher()
$: id = `navGroup-${categoryName}`
let pressed: boolean = false
const toggle = (): void => {
isOpen = !isOpen
dispatch('toggle', isOpen)
if ((!selected || empty) && type === 'nested-selectable') dispatch('click')
else if (!empty) {
isOpen = !isOpen
dispatch('toggle', isOpen)
}
}
function handleMenuClicked (ev: MouseEvent): void {
if (actions.length === 0) return
showPopup(Menu, { actions }, ev.target as HTMLElement)
ev.stopPropagation()
pressed = true
showPopup(Menu, { actions }, ev.target as HTMLElement, () => {
pressed = false
})
}
$: isOpen = !getTreeCollapsed(_id, collapsedPrefix)
$: setTreeCollapsed(_id, !isOpen, collapsedPrefix)
$: visibleIcon = folderIcon ? (isOpen && !empty ? IconFolderExpanded : IconFolderCollapsed) : icon
</script>
<div class="hulyAccordionItem-container" class:second>
<button class="hulyAccordionItem-header nav small" class:isOpen class:selected on:click={toggle}>
{#if isFold}
<button class="hulyAccordionItem-header__chevron" class:collapsed={!isOpen}>
<IconOpenedArrow size={'small'} />
<div
class="hulyNavGroup-container"
class:nested={type === 'nested' || type === 'nested-selectable'}
class:selectable={type === 'nested-selectable'}
class:noDivider
>
<button
class="hulyNavGroup-header"
class:isOpen={(isOpen || visible) && !empty}
class:highlighted
class:selected
class:showMenu={showMenu || pressed}
on:click={toggle}
>
{#if isFold && !empty}
<button class="hulyNavGroup-header__chevron" class:collapsed={!isOpen}>
<IconDown size={'small'} />
</button>
{/if}
<div class="hulyAccordionItem-header__label-wrapper font-medium-12">
<!-- svelte-ignore a11y-no-static-element-interactions -->
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div class="hulyNavGroup-header__label font-medium-12" on:click={handleMenuClicked}>
{#if visibleIcon}
<div class="hulyNavGroup-header__icon" class:folder={folderIcon}>
<Icon icon={visibleIcon} size={iconSize} {iconProps} />
</div>
{/if}
<span
class="hulyAccordionItem-header__label"
class:cursor-default={actions.length === 0}
on:click|stopPropagation={handleMenuClicked}
use:tooltip={shouldTooltip ? { label: label ?? getEmbeddedLabel(title ?? ''), direction: 'top' } : undefined}
class="overflow-label"
>
{#if label}<Label {label} />{/if}
{#if title}{title}{/if}
</span>
{#if actions.length > 0}
<IconDownOutline size={'tiny'} />
{/if}
</div>
{#if isFold}<div class="flex-grow" />{/if}
<div class="flex-grow" />
{#if $$slots.extra}<slot name="extra" />{/if}
{#if tools || $$slots.tools}
<div class="hulyAccordionItem-header__tools">
<div class="hulyNavGroup-header__tools">
{#if tools}
<Component
is={tools}
@ -77,8 +138,17 @@
<slot name="tools" />
</div>
{/if}
{#if selected && type === 'nested-selectable'}
<div class="hulyNavGroup-header__arrow"><IconOpenedArrow size={'small'} /></div>
{/if}
</button>
<div {id} class="hulyAccordionItem-content">
<slot />
</div>
{#if !empty}
<div {id} class="hulyNavGroup-content">
{#if (!isOpen && visible) || forciblyСollapsed}
<slot name="visible" {isOpen} />
{:else}
<slot />
{/if}
</div>
{/if}
</div>

View File

@ -14,9 +14,23 @@
-->
<script lang="ts">
import type { Asset, IntlString } from '@hcengineering/platform'
import { Icon, Label, IconOpenedArrow, Fold, AnySvelteComponent, IconSize } from '..'
import { getEmbeddedLabel } from '@hcengineering/platform'
import {
Icon,
Label,
IconOpenedArrow,
IconDown,
AnySvelteComponent,
IconSize,
getTreeCollapsed,
setTreeCollapsed,
tooltip,
IconFolderExpanded,
IconFolderCollapsed
} from '..'
export let icon: Asset | AnySvelteComponent | undefined = undefined
export let folderIcon: boolean = false
export let iconProps: any | undefined = undefined
export let iconSize: IconSize = 'small'
export let label: IntlString | undefined = undefined
@ -26,50 +40,101 @@
export let color: string | null = null
export let count: number | null = null
export let selected: boolean = false
export let indent: boolean = false
export let bold: boolean = false
export let disabled: boolean = false
export let isFold: boolean = false
export let isOpen: boolean = false
export let isSecondary: boolean = false
export let withBackground: boolean = false
export let showMenu: boolean = false
export let shouldTooltip: boolean = false
export let empty: boolean = false
export let level: number = 1
export let collapsedPrefix: string = ''
export let visible: boolean = false
export let forciblyСollapsed: boolean = false
export let level: number = 0
export let _id: any = undefined
let labelWidth: number
let levelReset: boolean = false
let hovered: boolean = false
$: showArrow = selected && (type === 'type-link' || type === 'type-object')
$: if (!showMenu && levelReset && !hovered) levelReset = false
$: isOpen = !getTreeCollapsed(_id, collapsedPrefix)
$: setTreeCollapsed(_id, !isOpen, collapsedPrefix)
$: visibleIcon = folderIcon ? (isOpen && !empty ? IconFolderExpanded : IconFolderCollapsed) : icon
</script>
<!-- svelte-ignore a11y-mouse-events-have-key-events -->
<button
class="hulyNavItem-container {type} {type === 'type-anchor-link' || isSecondary
? 'font-regular-12'
: 'font-regular-14'}"
class:fold={isFold}
class:selected
class:bold
class:indent
class:disabled
class:showMenu
class:noActions={$$slots.actions === undefined}
on:mouseover={() => {
if (!levelReset && labelWidth < 16 && level > 0) levelReset = true
if (!hovered) hovered = true
}}
on:mouseleave={() => {
if (levelReset && !showMenu) levelReset = false
hovered = false
}}
on:click={() => {
if (selected && isFold) isOpen = !isOpen
}}
on:click
on:contextmenu
>
{#if isFold}
<Fold {isOpen} {empty} {level} />
<button
class="hulyNavItem-chevron"
class:isOpen
style:margin-left={`${(levelReset ? 0 : level) * 1.25}rem`}
disabled={empty}
on:click|stopPropagation={() => {
if (!empty) isOpen = !isOpen
}}
>
{#if !empty}<IconDown size={'x-small'} />{/if}
</button>
{/if}
{#if icon || (type === 'type-tag' && color)}
{#if visibleIcon || (type === 'type-tag' && color)}
<div class="hulyNavItem-icon" class:withBackground class:w-auto={iconSize === 'x-small'}>
{#if type !== 'type-tag' && icon}
<Icon {icon} size={iconSize} {iconProps} />
{#if type !== 'type-tag' && visibleIcon}
<Icon icon={visibleIcon} size={iconSize} {iconProps} />
{:else if type === 'type-tag'}
<div style:background-color={color} class="hulyNavItem-icon__tag" />
{/if}
</div>
{/if}
<span
class="hulyNavItem-label"
class:font-medium-12={description}
class:flex-grow={!(type === 'type-anchor-link' || description)}
bind:clientWidth={labelWidth}
use:tooltip={shouldTooltip ? { label: label ?? getEmbeddedLabel(title ?? ''), direction: 'top' } : undefined}
class="{description ? 'hulyNavItem-wideLabel' : 'hulyNavItem-label'} overflow-label"
class:flex-grow={!(type === 'type-anchor-link')}
style:color={type === 'type-tag' && selected ? color : null}
>
{#if label}<Label {label} />{/if}
{#if title}{title}{/if}
<slot />
{#if description}
<span class="hulyNavItem-label font-medium-12 mr-0-5">
{#if label}<Label {label} />{/if}
{#if title}{title}{/if}
<slot />
</span>
{description}
{:else}
{#if label}<Label {label} />{/if}
{#if title}{title}{/if}
<slot />
{/if}
</span>
{#if description}
<span class="hulyNavItem-label description flex-grow">{description}</span>
{/if}
{#if $$slots.extra}<slot name="extra" />{/if}
{#if showMenu || $$slots.actions}
<div class="hulyNavItem-actions">
<slot name="actions" />
@ -81,13 +146,23 @@
</span>
{/if}
<slot name="notify" />
{#if selected && (type === 'type-link' || type === 'type-object')}
{#if showArrow}
<div class="hulyNavItem-icon right"><IconOpenedArrow size={'small'} /></div>
{/if}
</button>
{#if (isFold && (isOpen || (!isOpen && visible)) && !empty) || forciblyСollapsed}
<div class="hulyNavItem-dropbox">
{#if (!isOpen && visible) || forciblyСollapsed}
<slot name="visible" {isOpen} />
{:else}
<slot name="dropbox" />
{/if}
</div>
{/if}
<style lang="scss">
.hulyNavItem-container {
overflow: hidden;
display: flex;
justify-content: stretch;
align-items: center;
@ -98,11 +173,29 @@
border-radius: var(--small-BorderRadius);
outline: none;
.hulyNavItem-chevron,
.hulyNavItem-icon {
display: flex;
justify-content: center;
align-items: center;
flex-shrink: 0;
}
.hulyNavItem-chevron {
margin: 0;
margin-right: var(--spacing-0_75);
padding: 0;
width: 0.75rem;
height: 0.75rem;
color: var(--global-tertiary-TextColor);
border: none;
border-radius: var(--min-BorderRadius);
outline: none;
&:disabled {
pointer-events: none;
}
}
.hulyNavItem-icon {
width: var(--global-min-Size);
height: var(--global-min-Size);
color: var(--global-primary-TextColor);
@ -129,22 +222,13 @@
border-radius: var(--extra-small-BorderRadius);
}
}
.hulyNavItem-label {
white-space: nowrap;
word-break: break-all;
text-overflow: ellipsis;
overflow: hidden;
.hulyNavItem-label,
.hulyNavItem-wideLabel {
text-align: left;
min-width: 0;
color: var(--global-primary-TextColor);
&.description {
font-size: 0.875rem;
font-weight: 400;
}
}
.hulyNavItem-label + .hulyNavItem-label {
margin-left: var(--spacing-0_5);
.hulyNavItem-wideLabel {
font-size: 0.875rem;
}
.hulyNavItem-actions {
display: none;
@ -169,23 +253,25 @@
cursor: auto;
background-color: var(--global-ui-highlight-BackgroundColor);
&:not(.type-anchor-link) .hulyNavItem-label:not(.description) {
font-weight: 700;
}
// &:not(.type-anchor-link) .hulyNavItem-label:not(.description) {
// font-weight: 700;
// }
.hulyNavItem-count {
color: var(--global-secondary-TextColor);
}
}
// &.bold:not(.type-anchor-link) .hulyNavItem-label:not(.description) {
// font-weight: 700;
// }
&.type-link {
padding: 0 var(--spacing-1_25);
padding: 0 var(--spacing-0_5) 0 var(--spacing-1_25);
&.selected {
&:not(.fold) {
padding: 0 var(--spacing-0_75) 0 var(--spacing-1_25);
}
&.fold {
padding: 0 var(--spacing-0_75) 0 var(--spacing-0_5);
padding: 0 var(--spacing-0_75) 0 var(--spacing-1_25);
&.indent {
padding-left: var(--spacing-4);
}
.hulyNavItem-icon {
color: var(--global-accent-TextColor);
@ -200,6 +286,11 @@
}
&.type-tag {
padding: 0 var(--spacing-1_25);
.hulyNavItem-icon {
width: 0.75rem;
margin-right: 0.625rem;
}
}
&.type-object {
padding: 0 var(--spacing-0_5) 0 var(--spacing-0_5);
@ -244,17 +335,38 @@
color: var(--global-primary-TextColor);
}
}
&.fold {
padding-left: var(--spacing-0_5);
&.indent {
padding-left: var(--spacing-4);
}
&:hover .hulyNavItem-chevron:enabled {
color: var(--global-secondary-TextColor);
background-color: var(--button-tertiary-hover-BackgroundColor);
}
:global(.hulyFold-container) {
margin-right: var(--spacing-0_75);
&:not(.noActions):hover,
&:not(.noActions).showMenu {
.hulyNavItem-actions {
display: flex;
}
.hulyNavItem-icon.right {
display: none;
}
}
&.disabled {
cursor: not-allowed;
&:hover .hulyNavItem-actions,
&.showMenu .hulyNavItem-actions {
display: flex;
.hulyNavItem-icon {
opacity: 0.5;
}
.hulyNavItem-label {
color: rgb(var(--theme-caption-color) / 40%);
}
}
}
.hulyNavItem-dropbox {
display: flex;
flex-direction: column;
min-width: 0;
min-height: 0;
}
</style>

View File

@ -1,5 +1,5 @@
<script lang="ts">
export let size: 'small' | 'medium' | 'large'
export let size: 'x-small' | 'small' | 'medium' | 'large'
const fill: string = 'currentColor'
</script>

View File

@ -1,5 +1,5 @@
<script lang="ts">
export let size: 'x-small' | 'small' | 'medium' | 'large' | 'full'
export let size: 'tiny' | 'x-small' | 'small' | 'medium' | 'large' | 'full'
export let fill: string = 'currentColor'
</script>

View File

@ -0,0 +1,34 @@
<!--
// 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">
export let fill: string = 'currentColor'
export let size: 'small' | 'medium' | 'large' = 'small'
</script>
<svg class="svg-{size}" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" {fill}>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M1 4.5C1 3.11929 2.11929 2 3.5 2H5.33579C5.73361 2 6.11514 2.15804 6.39645 2.43934L7.81066 3.85355C7.90443 3.94732 8.03161 4 8.16421 4H12.5C13.8807 4 15 5.11929 15 6.5V11.5C15 12.8807 13.8807 14 12.5 14H3.5C2.11929 14 1 12.8807 1 11.5V4.5ZM3.5 3C2.67157 3 2 3.67157 2 4.5V11.5C2 12.3284 2.67157 13 3.5 13H12.5C13.3284 13 14 12.3284 14 11.5V6.5C14 5.67157 13.3284 5 12.5 5H8.16421C7.76639 5 7.38486 4.84197 7.10355 4.56066L5.68934 3.14645C5.59557 3.05268 5.46839 3 5.33579 3H3.5Z"
fill-opacity="0.6"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M3.5 3C2.67157 3 2 3.67157 2 4.5V11.5C2 12.3284 2.67157 13 3.5 13H12.5C13.3284 13 14 12.3284 14 11.5V6.5C14 5.67157 13.3284 5 12.5 5H8.16421C7.76639 5 7.38486 4.84197 7.10355 4.56066L5.68934 3.14645C5.59557 3.05268 5.46839 3 5.33579 3H3.5Z"
fill-opacity="0.1"
/>
</svg>

View File

@ -0,0 +1,34 @@
<!--
// 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">
export let fill: string = 'currentColor'
export let size: 'small' | 'medium' | 'large' = 'small'
</script>
<svg class="svg-{size}" xmlns="http://www.w3.org/2000/svg" width="17" height="16" viewBox="0 0 17 16" {fill}>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M14 7V6.5C14 5.67157 13.3284 5 12.5 5H8.16421C7.76639 5 7.38486 4.84197 7.10355 4.56066L5.68934 3.14645C5.59557 3.05268 5.46839 3 5.33579 3H3.5C2.67157 3 2 3.67157 2 4.5V10.5L2.58555 8.45056C2.83087 7.59196 3.61564 7 4.5086 7H14Z"
fill-opacity="0.1"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M3.5 2C2.11929 2 1 3.11929 1 4.5V11.5C1 12.8807 2.11929 14 3.5 14H13.4914C14.3844 14 15.1691 13.408 15.4144 12.5494L16.2716 9.54944C16.5735 8.49282 15.9696 7.44018 15 7.10762V6.5C15 5.11929 13.8807 4 12.5 4H8.16421C8.0316 4 7.90443 3.94732 7.81066 3.85355L6.39645 2.43934C6.11514 2.15804 5.73361 2 5.33579 2H3.5ZM14 7V6.5C14 5.67157 13.3284 5 12.5 5H8.16421C7.76639 5 7.38486 4.84197 7.10355 4.56066L5.68934 3.14645C5.59557 3.05268 5.46839 3 5.33579 3H3.5C2.67157 3 2 3.67157 2 4.5V10.5L2.58555 8.45056C2.83087 7.59196 3.61564 7 4.5086 7H14Z"
fill-opacity="0.6"
/>
</svg>

View File

@ -13,7 +13,7 @@
// limitations under the License.
-->
<script lang="ts">
export let size: 'small' | 'medium' | 'large'
export let size: 'x-small' | 'small' | 'medium' | 'large'
const fill: string = 'currentColor'
</script>

View File

@ -226,6 +226,8 @@ export { default as IconDropdownRight } from './components/icons/DropdownRight.s
export { default as IconKeyCommand } from './components/icons/KeyCommand.svelte'
export { default as IconKeyOption } from './components/icons/KeyOption.svelte'
export { default as IconKeyShift } from './components/icons/KeyShift.svelte'
export { default as IconFolderCollapsed } from './components/icons/FolderCollapsed.svelte'
export { default as IconFolderExpanded } from './components/icons/FolderExpanded.svelte'
export { default as IconCheckmark } from './components/icons/Checkmark.svelte'
export { default as PanelInstance } from './components/PanelInstance.svelte'

View File

@ -197,16 +197,17 @@ export function navigate (location: PlatformLocation, replace = false): boolean
}
const COLLAPSED = 'COLLAPSED'
export const getCollapsedKey = (_id: string): string => `${getCurrentLocation().path[1]}_${_id}_collapsed`
export const getCollapsedKey = (_id: string, prefix?: string): string =>
`${getCurrentLocation().path[1]}_${prefix !== undefined && prefix !== '' ? prefix + '_###_' + _id : _id}_collapsed`
export const getTreeCollapsed = (_id: any): boolean => {
export const getTreeCollapsed = (_id: any, prefix?: string): boolean => {
if (_id === undefined || _id === 'undefined') return false
return localStorage.getItem(getCollapsedKey(_id as string)) === COLLAPSED
return localStorage.getItem(getCollapsedKey(_id as string, prefix)) === COLLAPSED
}
export const setTreeCollapsed = (_id: any, collapsed: boolean): void => {
export const setTreeCollapsed = (_id: any, collapsed: boolean, prefix?: string): void => {
if (_id === undefined || _id === 'undefined') return
const key = getCollapsedKey(_id)
const key = getCollapsedKey(_id, prefix)
collapsed ? localStorage.setItem(key, COLLAPSED) : localStorage.removeItem(key)
}

View File

@ -107,6 +107,11 @@
}
function handleChannelSelected (event: CustomEvent): void {
if (event.detail === null) {
selectedData = undefined
return
}
const detail = (event.detail ?? {}) as SelectChannelEvent
selectedData = { _id: detail.object._id, _class: detail.object._class }

View File

@ -115,7 +115,7 @@
</script>
<NavItem
id={item.id}
_id={item.id}
icon={item.icon}
withIconBackground={item.withIconBackground}
isSecondary={item.isSecondary}

View File

@ -27,7 +27,6 @@
import chunter from '../../../plugin'
import { ChatNavItemModel, SortFnOptions } from '../types'
import { getObjectIcon, getChannelName } from '../../../utils'
import { navigatorStateStore, toggleSections } from '../utils'
export let id: string
export let header: IntlString
@ -45,12 +44,9 @@
let items: ChatNavItemModel[] = []
let visibleItems: ChatNavItemModel[] = []
let isCollapsed = false
let canShowMore = false
let isShownMore = false
$: isCollapsed = $navigatorStateStore.collapsedSections.includes(id)
$: void getChatNavItems(objects).then((res) => {
items = res
})
@ -137,49 +133,48 @@
return result
}
$: visibleItem = visibleItems.find(({ id }) => id === objectId)
</script>
{#if visibleItems.length > 0 && contexts.length > 0}
<NavGroup
_id={id}
label={header}
categoryName={id}
{actions}
isOpen={!isCollapsed}
highlighted={items.some((it) => it.id === objectId)}
isFold
second
on:toggle={() => {
toggleSections(id)
}}
empty={visibleItems.length === 0}
visible={visibleItem !== undefined}
noDivider
>
{#if !isCollapsed}
{#each visibleItems as item (item.id)}
{@const context = contexts.find(({ attachedTo }) => attachedTo === item.id)}
<ChatNavItem {context} isSelected={objectId === item.id} {item} type={'type-object'} on:select />
{/each}
{#if canShowMore}
<div class="showMore">
<ModernButton
label={isShownMore ? ui.string.ShowLess : ui.string.ShowMore}
kind="tertiary"
inheritFont
size="extra-small"
on:click={onShowMore}
/>
</div>
{/if}
{:else if objectId}
{@const item = visibleItems.find(({ id }) => id === objectId)}
{#if item}
{@const context = contexts.find(({ attachedTo }) => attachedTo === item.id)}
<ChatNavItem {context} isSelected {item} on:select />
{/if}
{#each visibleItems as item (item.id)}
{@const context = contexts.find(({ attachedTo }) => attachedTo === item.id)}
<ChatNavItem {context} isSelected={objectId === item.id} {item} type={'type-object'} on:select />
{/each}
{#if canShowMore}
<div class="showMore">
<ModernButton
label={isShownMore ? ui.string.ShowLess : ui.string.ShowMore}
kind="tertiary"
inheritFont
size="extra-small"
on:click={onShowMore}
/>
</div>
{/if}
<svelte:fragment slot="visible" let:isOpen>
{#if visibleItem !== undefined && !isOpen}
{@const context = contexts.find(({ attachedTo }) => attachedTo === visibleItem?.id)}
<ChatNavItem {context} isSelected item={visibleItem} type={'type-object'} on:select />
{/if}
</svelte:fragment>
</NavGroup>
{/if}
<style lang="scss">
.showMore {
margin-top: var(--spacing-1);
margin: var(--spacing-1);
font-size: 0.75rem;
}
</style>

View File

@ -18,7 +18,7 @@
import { DocNotifyContext } from '@hcengineering/notification'
import { SpecialNavModel } from '@hcengineering/workbench'
import { NavLink } from '@hcengineering/view-resources'
import { TreeSeparator } from '@hcengineering/workbench-resources'
import { TreeSeparator, NavFooter } from '@hcengineering/workbench-resources'
import { getResource } from '@hcengineering/platform'
import { InboxNotificationsClientImpl } from '@hcengineering/notification-resources'
@ -35,6 +35,7 @@
const notificationClient = InboxNotificationsClientImpl.getClient()
const contextsStore = notificationClient.contexts
let pressed: boolean = false
const globalActions = [
{
@ -66,16 +67,19 @@
}
function addButtonClicked (ev: MouseEvent): void {
showPopup(Menu, { actions: globalActions }, ev.target as HTMLElement)
pressed = true
showPopup(Menu, { actions: globalActions }, ev.target as HTMLElement, () => {
pressed = false
})
}
</script>
<div class="hulyNavPanel-header">
<div class="hulyNavPanel-header withButton">
<span class="overflow-label">
<Label label={chunter.string.Chat} />
</span>
{#if hasAccountRole(getCurrentAccount(), AccountRole.User)}
<ButtonIcon icon={IconAdd} kind={'primary'} size={'small'} on:click={addButtonClicked} />
<ButtonIcon icon={IconAdd} hasMenu {pressed} kind={'primary'} size={'small'} on:click={addButtonClicked} />
{/if}
</div>
@ -86,7 +90,7 @@
{#await isSpecialVisible(special, $contextsStore) then isVisible}
{#if isVisible}
<NavLink space={special.id}>
<ChatSpecialElement {special} {currentSpecial} />
<ChatSpecialElement {special} {currentSpecial} on:select />
</NavLink>
{/if}
{/await}
@ -106,16 +110,16 @@
}}
/>
</div>
<Scroller>
<Scroller shrink>
{#each chatNavGroupModels as model}
<ChatNavGroup {object} {objectId} {model} on:select />
{/each}
<div class="antiNav-space" />
</Scroller>
<NavFooter />
<style lang="scss">
.search {
padding: var(--spacing-3) var(--spacing-1_5) var(--spacing-1_5);
padding: var(--spacing-1_5);
border-bottom: 1px solid var(--theme-navpanel-divider);
}
</style>

View File

@ -13,6 +13,7 @@
// limitations under the License.
-->
<script lang="ts">
import { createEventDispatcher } from 'svelte'
import { SpecialNavModel } from '@hcengineering/workbench'
import { getResource } from '@hcengineering/platform'
import { InboxNotificationsClientImpl } from '@hcengineering/notification-resources'
@ -29,6 +30,8 @@
export let currentSpecial: SpecialNavModel | undefined = undefined
export let type: 'type-link' | 'type-tag' | 'type-anchor-link' | 'type-object' = 'type-link'
const dispatch = createEventDispatcher()
const notificationsClient = InboxNotificationsClientImpl.getClient()
const notificationsByContextStore = notificationsClient.inboxNotificationsByContext
@ -67,7 +70,7 @@
</script>
<NavItem
id={special.id}
_id={special.id}
icon={special.icon}
intlTitle={special.label}
withIconBackground={false}
@ -75,4 +78,5 @@
{elementsCount}
isSelected={special.id === currentSpecial?.id}
{type}
on:click={() => dispatch('select', null)}
/>

View File

@ -27,7 +27,7 @@
import { NotifyMarker } from '@hcengineering/notification-resources'
import { Asset, IntlString } from '@hcengineering/platform'
export let id: string
export let _id: string
export let icon: Asset | AnySvelteComponent | undefined
export let iconProps: any | undefined = undefined
export let iconSize: IconSize = 'small'
@ -50,7 +50,7 @@
$: menuActions = actions.filter(({ inline }) => inline !== true)
function handleMenuClicked (ev: MouseEvent): void {
showPopup(Menu, { actions: menuActions, ctx: id }, ev.target as HTMLElement, () => {
showPopup(Menu, { actions: menuActions, ctx: _id }, ev.target as HTMLElement, () => {
menuOpened = false
})
menuOpened = true
@ -62,6 +62,7 @@
</script>
<NavItem
{_id}
{icon}
{iconProps}
{iconSize}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -33,13 +33,7 @@ import presentation, { copyDocumentContent, getClient } from '@hcengineering/pre
import contact, { type Employee, type PersonAccount } from '@hcengineering/contact'
import request, { RequestStatus } from '@hcengineering/request'
import textEditor, { isEmptyMarkup } from '@hcengineering/text-editor'
import {
getEventPositionElement,
showPopup,
getTreeCollapsed,
setTreeCollapsed,
type Location
} from '@hcengineering/ui'
import { getEventPositionElement, showPopup, type Location } from '@hcengineering/ui'
import { type KeyFilter } from '@hcengineering/view'
import chunter from '@hcengineering/chunter'
import documents, {
@ -679,22 +673,6 @@ export async function canDeleteDocumentCategory (doc?: Doc | Doc[]): Promise<boo
return await checkPermission(client, documents.permission.DeleteDocumentCategory, (doc as DocumentCategory).space)
}
function getPrefixedTreeId (_id: string, prefix: string): string {
return `${prefix}_###_${_id}`
}
export function getPrefixedTreeCollapsed (_id: string, prefix: string): boolean {
if (_id === undefined || _id === 'undefined') return false
return getTreeCollapsed(prefix === '' ? _id : getPrefixedTreeId(_id, prefix))
}
export function setPrefixedTreeCollapsed (_id: string, prefix: string, collapsed: boolean): void {
if (_id === undefined || _id === 'undefined') return
setTreeCollapsed(prefix === '' ? _id : getPrefixedTreeId(_id, prefix), collapsed)
}
function getCurrentProjectId (space: Ref<DocumentSpace>): string {
return `${space}_###_project`
}

View File

@ -32,7 +32,7 @@
export let documentById: Map<Ref<Document>, Document>
export let selected: Ref<Document> | undefined
export let level = 1
export let level: number = 0
const client = getClient()
const dispatch = createEventDispatcher()
@ -97,10 +97,12 @@
}}
title={doc.name}
selected={selected === doc._id}
parent={desc.length > 0}
isFold
{level}
empty={desc.length === 0}
actions={getActions(doc)}
moreActions={() => getMoreActions(doc)}
{level}
shouldTooltip
on:click={() => {
handleDocumentSelected(doc._id)
}}

View File

@ -17,19 +17,7 @@
import { Document } from '@hcengineering/document'
import type { Asset, IntlString } from '@hcengineering/platform'
import type { Action, AnySvelteComponent } from '@hcengineering/ui'
import {
ActionIcon,
Icon,
IconChevronDown,
IconMoreH,
Label,
Menu,
navigate,
showPopup,
getTreeCollapsed,
setTreeCollapsed
} from '@hcengineering/ui'
import { IconMoreH, Menu, navigate, showPopup, NavItem, ButtonIcon } from '@hcengineering/ui'
import { getDocumentLink } from '../../utils'
export let doc: Document
@ -37,14 +25,16 @@
export let iconProps: Record<string, any> | undefined = undefined
export let label: IntlString | undefined = undefined
export let title: string | undefined = undefined
export let parent: boolean = false
export let collapsed: boolean = getTreeCollapsed(doc._id)
export let selected: boolean = false
export let isFold: boolean = false
export let empty: boolean = false
export let shouldTooltip: boolean = false
export let level: number = 0
export let actions: Action[] = []
export let moreActions: (originalEvent?: MouseEvent) => Promise<Action[]> | undefined = async () => []
export let forciblyСollapsed: boolean = false
let hovered = false
let hovered: boolean = false
async function onMenuClick (ev: MouseEvent): Promise<void> {
showPopup(Menu, { actions: await moreActions(ev), ctx: doc._id }, ev.target as HTMLElement, () => {
hovered = false
@ -58,59 +48,41 @@
}
const dispatch = createEventDispatcher()
$: collapsed = getTreeCollapsed(doc._id)
$: setTreeCollapsed(doc._id, collapsed)
</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`}
<NavItem
_id={doc._id}
{icon}
{iconProps}
{label}
{title}
{isFold}
{level}
{empty}
{selected}
showMenu={hovered}
{shouldTooltip}
{forciblyСollapsed}
on:click={() => {
selectDocument()
if (selected) {
collapsed = !collapsed
}
dispatch('click')
}}
>
<div
class="an-element__tool arrow"
class:empty={!parent}
on:click|preventDefault|stopPropagation={() => {
if (parent) collapsed = !collapsed
}}
>
{#if parent}<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">
{#if label}<Label {label} />{:else}{title}{/if}
</span>
<div class="an-element__grow" />
{#each actions as action}
{#if action.icon}
<div class="an-element__tool">
<ActionIcon label={action.label} icon={action.icon} size={'small'} action={(evt) => action.action({}, evt)} />
</div>
{/if}
{/each}
<div class="an-element__tool" class:pressed={hovered} on:click|preventDefault|stopPropagation={onMenuClick}>
<IconMoreH size={'small'} />
</div>
</div>
{#if parent && !collapsed}
<div class="antiNav-element__dropbox"><slot /></div>
{/if}
<svelte:fragment slot="actions">
{#each actions as action}
{#if action.icon}
<ButtonIcon
icon={action.icon}
kind={'tertiary'}
size={'extra-small'}
tooltip={{ label: action.label, direction: 'top' }}
on:click={(evt) => action.action({}, evt)}
/>
{/if}
{/each}
<ButtonIcon icon={IconMoreH} kind={'tertiary'} size={'extra-small'} pressed={hovered} on:click={onMenuClick} />
</svelte:fragment>
<svelte:fragment slot="dropbox">
<slot />
</svelte:fragment>
</NavItem>

View File

@ -13,25 +13,27 @@
// limitations under the License.
-->
<script lang="ts">
import { Ref, SortingOrder, Space } from '@hcengineering/core'
import { Ref, SortingOrder, Space, generateId } from '@hcengineering/core'
import { Document, Teamspace } from '@hcengineering/document'
import { createQuery } from '@hcengineering/presentation'
import { createQuery, getClient } from '@hcengineering/presentation'
import {
IconWithEmoji,
getTreeCollapsed,
setTreeCollapsed,
IconEdit,
getPlatformColorDef,
getPlatformColorForTextDef,
themeStore,
Action
Action,
IconAdd
} from '@hcengineering/ui'
import view from '@hcengineering/view'
import { TreeNode } from '@hcengineering/view-resources'
import { TreeNode, openDoc, getActions as getContributedActions } from '@hcengineering/view-resources'
import { SpacesNavModel } from '@hcengineering/workbench'
import { getResource } from '@hcengineering/platform'
import document from '../../plugin'
import { getDocumentIdFromFragment } from '../../utils'
import { getDocumentIdFromFragment, createEmptyDocument } from '../../utils'
import DocHierarchy from './DocHierarchy.svelte'
import DocTreeElement from './DocTreeElement.svelte'
export let space: Teamspace
export let model: SpacesNavModel
@ -40,9 +42,9 @@
export let currentFragment: string | undefined
export let getActions: (space: Space) => Promise<Action[]> = async () => []
export let deselect: boolean = false
export let forciblyСollapsed: boolean = false
let collapsed: boolean = getTreeCollapsed(space._id)
$: setTreeCollapsed(space._id, collapsed)
const client = getClient()
let documents: Ref<Document>[] = []
let documentById: Map<Ref<Document>, Document> = new Map<Ref<Document>, Document>()
@ -54,6 +56,8 @@
let selected: Ref<Document> | undefined
$: selected = getDocumentIdFromFragment(currentFragment ?? '')
let visibleItem: Document | undefined
$: visibleItem = selected !== undefined ? documentById.get(selected) : undefined
// TODO expand tree until the selected document ?
@ -84,10 +88,43 @@
}
}
)
function getDocActions (doc: Document): Action[] {
return [
{
icon: IconAdd,
label: document.string.CreateDocument,
action: async (ctx: any, evt: Event) => {
const id: Ref<Document> = generateId()
await createEmptyDocument(client, id, doc.space, doc._id, {})
const object = await client.findOne(document.class.Document, { _id: id })
if (object !== undefined) {
openDoc(client.getHierarchy(), object)
}
}
}
]
}
async function getMoreActions (obj: Document): Promise<Action[]> {
const result: Action[] = []
const extraActions = await getContributedActions(client, obj)
for (const act of extraActions) {
result.push({
icon: act.icon ?? IconEdit,
label: act.label,
action: async (ctx: any, evt: Event) => {
const impl = await getResource(act.action)
await impl(obj, evt, act.actionProps)
}
})
}
return result
}
</script>
<TreeNode
{collapsed}
_id={space?._id}
icon={space?.icon === view.ids.IconWithEmoji ? IconWithEmoji : space?.icon ?? model.icon}
iconProps={space?.icon === view.ids.IconWithEmoji
@ -99,10 +136,34 @@
: getPlatformColorForTextDef(space.name, $themeStore.dark).icon
}}
title={space.name}
folder
parent={descendants.size > 0}
type={'nested'}
highlighted={currentSpace === space._id}
visible={currentSpace === space._id || forciblyСollapsed}
actions={() => getActions(space)}
on:click={() => (collapsed = !collapsed)}
{forciblyСollapsed}
>
<DocHierarchy {documents} {descendants} {documentById} {selected} />
<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>

View File

@ -15,19 +15,24 @@
<script lang="ts">
import { Doc, Ref, SortingOrder, Space } from '@hcengineering/core'
import { Drive, Folder } from '@hcengineering/drive'
import { createQuery } from '@hcengineering/presentation'
import { Action, navigate } from '@hcengineering/ui'
import { createQuery, getClient } from '@hcengineering/presentation'
import { Action, navigate, IconEdit } from '@hcengineering/ui'
import { TreeNode, TreeItem, getActions as getContributedActions } from '@hcengineering/view-resources'
import { getResource } from '@hcengineering/platform'
import drive from '../plugin'
import { getDriveLink, getFolderIdFromFragment, getFolderLink } from '../navigation'
import FolderTreeLevel from './FolderTreeLevel.svelte'
import FolderTreeElement from './FolderTreeElement.svelte'
export let space: Drive
export let currentSpace: Ref<Space> | undefined
export let currentSpecial: string | undefined
export let currentFragment: string | undefined
export let getActions: (space: Space) => Promise<Action[]> = async () => []
export let forciblyСollapsed: boolean = false
export let deselect: boolean = false
const client = getClient()
let folders: Ref<Folder>[] = []
let folderById: Map<Ref<Folder>, Folder> = new Map<Ref<Folder>, Folder>()
@ -38,7 +43,9 @@
}
let selected: Ref<Doc> | undefined
let visibleItem: Folder | undefined
$: selected = getFolderIdFromFragment(currentFragment ?? '')
$: visibleItem = selected !== undefined ? folderById.get(selected as Ref<Folder>) : undefined
const query = createQuery()
query.query(
@ -75,16 +82,37 @@
function handleFolderSelected (_id: Ref<Folder>): void {
navigate(getFolderLink(_id))
}
async function getFolderActions (obj: Folder): Promise<Action[]> {
const result: Action[] = []
const extraActions = await getContributedActions(client, obj)
for (const act of extraActions) {
result.push({
icon: act.icon ?? IconEdit,
label: act.label,
action: async (ctx: any, evt: Event) => {
const impl = await getResource(act.action)
await impl(obj, evt, act.actionProps)
}
})
}
return result
}
</script>
{#if space}
<FolderTreeElement
<TreeNode
_id={space._id}
icon={drive.icon.Drive}
title={space.name}
selected={currentSpace === space._id && selected === space._id}
parent={descendants.size > 0}
highlighted={currentSpace === space._id && !deselect}
selected={currentSpace === space._id && selected === drive.ids.Root && currentFragment !== undefined && !deselect}
visible={(currentSpace === space._id && !deselect && descendants.size !== 0 && selected !== space._id) ||
(forciblyСollapsed && currentFragment !== undefined)}
type={'nested-selectable'}
empty={descendants.size === 0 || (forciblyСollapsed && selected === space._id && !deselect)}
actions={() => getActions(space)}
{forciblyСollapsed}
on:click={() => {
handleDriveSelected(space._id)
}}
@ -98,5 +126,23 @@
handleFolderSelected(ev.detail)
}}
/>
</FolderTreeElement>
<svelte:fragment slot="visible">
{#if (selected || forciblyСollapsed) && visibleItem}
{@const folder = visibleItem}
<TreeItem
_id={folder._id}
folderIcon
iconProps={{ fill: 'var(--global-accent-IconColor)' }}
title={folder.name}
selected
isFold
empty
actions={async () => await getFolderActions(folder)}
shouldTooltip
forciblyСollapsed
/>
{/if}
</svelte:fragment>
</TreeNode>
{/if}

View File

@ -40,7 +40,7 @@
<div class="flex-presenter" use:tooltip={{ label: getEmbeddedLabel(value.name) }}>
{#if shouldShowAvatar}
<div class="icon">
<Icon icon={FolderIcon} size={'small'} fill="#5195D7" />
<Icon icon={FolderIcon} size={'small'} fill="var(--global-accent-IconColor)" />
</div>
{/if}
<span class="label nowrap" class:no-underline={noUnderline || disabled} class:fs-bold={accent}>

View File

@ -1,101 +0,0 @@
<!--
// 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">
import { createEventDispatcher } from 'svelte'
import type { Doc, Ref } from '@hcengineering/core'
import { type Asset, type IntlString, getEmbeddedLabel } from '@hcengineering/platform'
import { type Action, type AnySvelteComponent, tooltip } from '@hcengineering/ui'
import {
Icon,
IconChevronDown,
IconMoreH,
Label,
Menu,
getTreeCollapsed,
setTreeCollapsed,
showPopup
} from '@hcengineering/ui'
export let _id: Ref<Doc>
export let icon: Asset | AnySvelteComponent | undefined = undefined
export let iconProps: Record<string, any> | undefined = undefined
export let label: IntlString | undefined = undefined
export let title: string | undefined = undefined
export let parent: boolean = false
export let collapsed: boolean = getTreeCollapsed(_id)
export let selected: boolean = false
export let level: number = 0
export let actions: (originalEvent?: MouseEvent) => Promise<Action[]> = async () => []
let hovered = false
async function onMenuClick (ev: MouseEvent): Promise<void> {
showPopup(Menu, { actions: await actions(ev), ctx: _id }, ev.target as HTMLElement, () => {
hovered = false
})
hovered = true
}
const dispatch = createEventDispatcher()
$: collapsed = getTreeCollapsed(_id)
$: setTreeCollapsed(_id, collapsed)
$: folder = level === 0
</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={() => {
if (selected) {
collapsed = !collapsed
}
dispatch('click')
}}
>
{#if !folder}
<div
class="an-element__tool arrow"
class:empty={!parent}
on:click|preventDefault|stopPropagation={() => {
if (parent) collapsed = !collapsed
}}
>
{#if parent}<IconChevronDown size={'small'} filled />{/if}
</div>
{/if}
{#if icon}
<div class="an-element__icon" class:folder>
<Icon {icon} {iconProps} size={'small'} />
</div>
{/if}
<span class="an-element__label" use:tooltip={{ label: label ?? getEmbeddedLabel(title ?? '') }}>
{#if label}<Label {label} />{:else}{title}{/if}
</span>
<div class="an-element__grow" />
<div class="an-element__tool" class:pressed={hovered} on:click|preventDefault|stopPropagation={onMenuClick}>
<IconMoreH size={'small'} />
</div>
</div>
{#if parent && !collapsed}
<div class="antiNav-element__dropbox"><slot /></div>
{/if}

View File

@ -19,17 +19,15 @@
import { getResource } from '@hcengineering/platform'
import { getClient } from '@hcengineering/presentation'
import { Action, IconEdit } from '@hcengineering/ui'
import { getActions as getContributedActions } from '@hcengineering/view-resources'
import FolderTreeElement from './FolderTreeElement.svelte'
import FolderIcon from './icons/Folder.svelte'
import { getActions as getContributedActions, TreeItem } from '@hcengineering/view-resources'
export let folders: Ref<Folder>[]
export let folderById: Map<Ref<Folder>, Folder>
export let descendants: Map<Ref<Folder>, Folder[]>
export let selected: Ref<Doc> | undefined
export let level = 1
export let level: number = 0
export let once: boolean = false
const client = getClient()
const dispatch = createEventDispatcher()
@ -66,24 +64,25 @@
{@const desc = _descendants.get(doc._id) ?? []}
{#if doc}
<FolderTreeElement
<TreeItem
_id={doc._id}
icon={FolderIcon}
iconProps={{
fill: '#5195D7'
}}
folderIcon
title={doc.name}
selected={selected === doc._id}
parent={desc.length > 0}
isFold
empty={desc.length === 0}
actions={async () => await getActions(doc)}
{level}
shouldTooltip
on:click={() => {
handleSelected(doc._id)
}}
>
{#if desc.length}
<svelte:self folders={desc} {descendants} {folderById} {selected} level={level + 1} on:selected />
{/if}
</FolderTreeElement>
<svelte:fragment slot="dropbox">
{#if desc.length > 0 && !once}
<svelte:self folders={desc} {descendants} {folderById} {selected} level={level + 1} on:selected />
{/if}
</svelte:fragment>
</TreeItem>
{/if}
{/each}

View File

@ -26,7 +26,7 @@
export let descendants: Map<Ref<Department>, Department[]>
export let departmentById: Map<Ref<Department>, Department>
export let selected: Ref<Department> | undefined
export let level = 1
export let level: number = 0
const client = getClient()
const dispatch = createEventDispatcher()
@ -68,16 +68,19 @@
icon={hr.icon.Department}
title={department.name}
selected={selected === department._id}
parent={desc.length > 0}
isFold
empty={desc.length === 0}
actions={() => getActions(department)}
{level}
on:click={() => {
handleDepartmentSelected(department._id)
}}
>
{#if desc.length}
<svelte:self departments={desc} {descendants} {departmentById} {selected} level={level + 1} on:selected />
{/if}
<svelte:fragment slot="dropbox">
{#if desc.length}
<svelte:self departments={desc} {descendants} {departmentById} {selected} level={level + 1} on:selected />
{/if}
</svelte:fragment>
</TreeElement>
{/if}
{/each}

View File

@ -15,7 +15,7 @@
<script lang="ts">
import { Ref } from '@hcengineering/core'
import { Department } from '@hcengineering/hr'
import { Scroller, Separator, EditBox } from '@hcengineering/ui'
import { Scroller, Separator } from '@hcengineering/ui'
import { TreeNode } from '@hcengineering/view-resources'
import { NavFooter, NavHeader } from '@hcengineering/workbench-resources'
@ -33,14 +33,18 @@
</script>
<div class="antiPanel-navigator {appsDirection === 'horizontal' ? 'portrait' : 'landscape'}">
<div class="antiPanel-wrap__content">
<div class="antiPanel-wrap__content hulyNavPanel-container">
<NavHeader label={hr.string.HRApplication} />
<Scroller shrink>
<TreeNode _id={'tree-hr'} label={hr.string.Departments} node>
<TreeNode
_id={'tree-hr'}
label={hr.string.Departments}
highlighted={department !== undefined}
isFold={department !== undefined}
>
<DepartmentsHierarchy {departments} {descendants} {departmentById} selected={department} on:selected />
</TreeNode>
<div class="antiNav-space" />
</Scroller>
<NavFooter />

View File

@ -23,7 +23,7 @@
export let classes: Ref<Class<Doc>>[] = ['contact:class:Contact' as Ref<Class<Doc>>]
export let _class: Ref<Class<Doc>> | undefined
export let ofClass: Ref<Class<Doc>> | undefined
export let level: number = 1
export let level: number = 0
const client = getClient()
const dispatch = createEventDispatcher()
@ -56,6 +56,7 @@
{@const clazz = client.getHierarchy().getClass(cl)}
{@const desc = getDescendants(cl)}
<NavItem
_id={clazz._id}
label={clazz.label}
isFold
empty

View File

@ -119,7 +119,13 @@
</div>
</div>
<Scroller>
<NavGroup label={setting.string.Classes} selected={_class !== undefined} categoryName={'classes'} second>
<NavGroup
label={setting.string.Classes}
highlighted={_class !== undefined}
categoryName={'classes'}
noDivider
isFold
>
<ClassHierarchy
{classes}
{_class}

View File

@ -135,9 +135,10 @@
{#each categories as _category}
{#if _category.extraComponents?.navigation && (_category.expandable ?? _category._id === setting.ids.Setting)}
<NavGroup
_id={_category._id}
label={_category.label}
categoryName={_category.name}
selected={_category.name === categoryId}
highlighted={_category.name === categoryId}
tools={_category.extraComponents?.tools}
>
<Component

View File

@ -14,7 +14,7 @@
// limitations under the License.
-->
<script lang="ts">
import { Button, IconAdd, showPopup } from '@hcengineering/ui'
import { ButtonIcon, IconAdd, showPopup } from '@hcengineering/ui'
import CreateSpaceType from './CreateSpaceType.svelte'
import { isOwnerOrMaintainer } from '@hcengineering/core'
@ -24,5 +24,5 @@
</script>
{#if isOwnerOrMaintainer()}
<Button id="new-space-type" icon={IconAdd} kind="link" size="small" on:click={handleAdd} />
<ButtonIcon id={'new-space-type'} icon={IconAdd} kind={'tertiary'} size={'extra-small'} on:click={handleAdd} />
{/if}

View File

@ -179,13 +179,18 @@
<div class="flex-col overflow-y-auto">
{#each spaces as space (space._id)}
<TreeNode label={getEmbeddedLabel(space.name)} actions={async () => await getSpaceActions(space)} parent>
{#each getTemplates(templates, space._id) as t (t._id)}
{@const getTemps = getTemplates(templates, space._id)}
<TreeNode
label={getEmbeddedLabel(space.name)}
actions={async () => await getSpaceActions(space)}
isFold
empty={getTemps.length === 0}
>
{#each getTemps as t (t._id)}
<TreeItem
_id={space._id}
title={t.title}
actions={async () => await getActions(t)}
indent
on:click={() => {
selected = t._id
title = t.title

View File

@ -5,6 +5,7 @@
"Agenda": "Agenda",
"Me": "Me",
"Team": "Team",
"TeamPlanner": "Team Planner",
"Today": "Today",
"TodayColon": "Today:",
"Tomorrow": "Tomorrow",

View File

@ -5,6 +5,7 @@
"Agenda": "Agenda",
"Me": "Yo",
"Team": "Equipo",
"TeamPlanner": "Planificador de equipos",
"Today": "Hoy",
"TodayColon": "Hoy:",
"Tomorrow": "Mañana",

View File

@ -5,6 +5,7 @@
"Agenda": "Agenda",
"Me": "Eu",
"Team": "Equipa",
"TeamPlanner": "Planificador de equipas",
"Today": "Hoje",
"TodayColon": "Hoje:",
"Tomorrow": "Amanhã",

View File

@ -5,6 +5,7 @@
"Agenda": "Agenda",
"Me": "Me",
"Team": "Команда",
"TeamPlanner": "Планировщик команды",
"Today": "Сегодня",
"TodayColon": "Сегодня:",
"Tomorrow": "Завтра",

View File

@ -63,6 +63,7 @@
"@hcengineering/time": "^0.6.0",
"fast-equals": "^5.0.1",
"@hcengineering/activity": "^0.6.0",
"@hcengineering/activity-resources": "^0.6.1"
"@hcengineering/activity-resources": "^0.6.1",
"@hcengineering/workbench-resources": "^0.6.1"
}
}

View File

@ -3,6 +3,7 @@
import { Ref, getCurrentAccount } from '@hcengineering/core'
import { Asset, IntlString } from '@hcengineering/platform'
import { createQuery } from '@hcengineering/presentation'
import { NavFooter } from '@hcengineering/workbench-resources'
import tagsPlugin, { TagElement as TagElementType } from '@hcengineering/tags'
import ui, {
Label,
@ -12,8 +13,7 @@
Scroller,
Month,
getPlatformColorDef,
themeStore,
areDatesEqual
themeStore
} from '@hcengineering/ui'
import { ToDosMode } from '..'
import time from '../plugin'
@ -126,8 +126,9 @@
}}
/>
{/each}
<div class="min-h-3 flex-no-shrink" />
<div class="hulyAccordionItem-container border pb-2">
<div class="hulyAccordionItem-container border" class:noBorder={tags.length === 0}>
<Month
currentDate={mode === 'date' ? currentDate : null}
on:update={(event) => {
@ -162,7 +163,15 @@
</div>
{#if tags.length > 0}
<NavGroup label={tagsPlugin.string.Tags} selected={mode === 'tag'} categoryName={'tags'} second>
<NavGroup
_id={'planner-tags'}
label={tagsPlugin.string.Tags}
highlighted={mode === 'tag'}
categoryName={'tags'}
noDivider
isFold
visible={tag !== undefined}
>
{#each tags as _tag}
{@const color = getPlatformColorDef(_tag.color ?? 0, $themeStore.dark)}
<NavItem
@ -178,9 +187,19 @@
}}
/>
{/each}
<svelte:fragment slot="visible" let:isOpen>
{#if !isOpen}
{@const visibleTag = tags.find((t) => t._id === tag)}
{#if visibleTag}
{@const color = getPlatformColorDef(visibleTag.color ?? 0, $themeStore.dark)}
<NavItem color={color.color} title={visibleTag.title} selected type={'type-tag'} />
{/if}
{/if}
</svelte:fragment>
</NavGroup>
{/if}
</Scroller>
<NavFooter />
</div>
<Separator
name={'time'}
@ -230,4 +249,7 @@
top: 0.1875rem;
right: 0.1875rem;
}
.noBorder {
border-bottom-color: transparent;
}
</style>

View File

@ -16,8 +16,9 @@
import { Ref, getCurrentAccount } from '@hcengineering/core'
import { createQuery } from '@hcengineering/presentation'
import task, { Project } from '@hcengineering/task'
import { Label, Separator } from '@hcengineering/ui'
import { Label, Separator, Scroller, NavItem } from '@hcengineering/ui'
import { ObjectPresenter, TreeNode } from '@hcengineering/view-resources'
import { NavFooter } from '@hcengineering/workbench-resources'
import time from '../../plugin'
export let navFloat: boolean = false
@ -37,30 +38,55 @@
projects = result
}
)
$: selectedItem = projects.find((pr) => pr._id === selected)
</script>
<div class="antiPanel-navigator {appsDirection === 'horizontal' ? 'portrait' : 'landscape'}">
<div class="antiPanel-wrap__content">
<div class="antiNav-header overflow-label">
<Label label={time.string.Team} />
<Label label={time.string.Planner} />
<div class="antiPanel-wrap__content hulyNavPanel-container">
<div class="hulyNavPanel-header">
<span class="overflow-label"><Label label={time.string.TeamPlanner} /></span>
</div>
<TreeNode _id={'projects-planning'} label={time.string.Team} node>
{#each projects as _project}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div
class="antiNav-element parent"
class:selected={selected === _project._id}
on:click={() => {
selected = _project._id
localStorage.setItem('team_last_mode', selected)
}}
>
<ObjectPresenter objectId={_project._id} _class={_project._class} value={_project} />
</div>
{/each}
</TreeNode>
<div class="antiNav-divider line" />
<Scroller shrink>
<TreeNode
_id={'projects-planning'}
label={time.string.Team}
isFold
empty={projects.length === 0}
highlighted={selected !== undefined}
visible={selected !== undefined}
>
{#each projects as _project}
<NavItem
selected={selected === _project._id}
on:click={() => {
selected = _project._id
localStorage.setItem('team_last_mode', selected)
}}
>
<ObjectPresenter
objectId={_project._id}
_class={_project._class}
value={_project}
colorInherit={selected === _project._id}
/>
</NavItem>
{/each}
<svelte:fragment slot="visible">
{#if selected && selectedItem}
<NavItem selected>
<ObjectPresenter
objectId={selectedItem._id}
_class={selectedItem._class}
value={selectedItem}
colorInherit
/>
</NavItem>
{/if}
</svelte:fragment>
</TreeNode>
</Scroller>
<NavFooter />
</div>
<Separator
name={'time'}

View File

@ -62,6 +62,7 @@ export default mergeIds(timeId, time, {
AddTitle: '' as IntlString,
MyWork: '' as IntlString,
WorkSchedule: '' as IntlString,
SummaryDuration: '' as IntlString
SummaryDuration: '' as IntlString,
TeamPlanner: '' as IntlString
}
})

View File

@ -21,21 +21,19 @@
Label,
getPlatformColorDef,
getPlatformColorForTextDef,
showPopup,
themeStore
} from '@hcengineering/ui'
import view from '@hcengineering/view'
import { canEditSpace } from '@hcengineering/view-resources'
import { CreateProject } from '../..'
import tracker from '../../plugin'
export let value: Project | undefined
export let inline: boolean = false
export let accent: boolean = false
export let colorInherit: boolean = false
</script>
{#if value}
<div class="flex-presenter cursor-default" class:inline-presenter={inline}>
<div class="flex-presenter cursor-default" class:inline-presenter={inline} class:colorInherit>
<div class="icon" class:emoji={value.icon === view.ids.IconWithEmoji}>
<Icon
icon={value.icon === view.ids.IconWithEmoji ? IconWithEmoji : value.icon ?? tracker.icon.Home}

View File

@ -20,8 +20,6 @@
IconWithEmoji,
getPlatformColorDef,
getPlatformColorForTextDef,
getTreeCollapsed,
setTreeCollapsed,
themeStore,
type Action
} from '@hcengineering/ui'
@ -36,9 +34,7 @@
export let currentSpecial: string | undefined
export let getActions: (space: Project) => Promise<Action[]> = async () => []
export let deselect: boolean = false
let collapsed: boolean = getTreeCollapsed(space._id)
$: setTreeCollapsed(space._id, collapsed)
export let forciblyСollapsed: boolean = false
let specials: SpecialNavModel[] = []
@ -60,11 +56,13 @@
}
$: updateSpecials(model, space)
$: visible =
(!deselect && currentSpace !== undefined && currentSpecial !== undefined && space._id === currentSpace) ||
forciblyСollapsed
</script>
{#if specials}
<TreeNode
{collapsed}
_id={space?._id}
icon={space?.icon === view.ids.IconWithEmoji ? IconWithEmoji : space?.icon ?? model.icon}
iconProps={space?.icon === view.ids.IconWithEmoji
@ -76,9 +74,11 @@
: getPlatformColorForTextDef(space.name, $themeStore.dark).icon
}}
title={space.name}
folder
type={'nested'}
highlighted={space._id === currentSpace}
{visible}
actions={() => getActions(space)}
on:click={() => (collapsed = !collapsed)}
{forciblyСollapsed}
>
{#each specials as special}
<NavLink space={space._id} special={special.id}>
@ -90,5 +90,16 @@
/>
</NavLink>
{/each}
<svelte:fragment slot="visible">
{#if visible}
{@const item = specials.find((sp) => sp.id === currentSpecial && currentSpace === space._id)}
{#if item}
<NavLink space={space._id} special={item.id}>
<SpecialElement indent label={item.label} icon={item.icon} selected forciblyСollapsed />
</NavLink>
{/if}
{/if}
</svelte:fragment>
</TreeNode>
{/if}

View File

@ -25,6 +25,7 @@
export let props: Record<string, any> = {}
export let inline: boolean = false
export let accent: boolean = false
export let colorInherit: boolean = false
export let shouldShowAvatar: boolean = true
export let noUnderline: boolean = false
export let disabled: boolean = false
@ -75,6 +76,7 @@
value={doc}
{inline}
{accent}
{colorInherit}
{shouldShowAvatar}
{shouldShowName}
{noUnderline}

View File

@ -19,36 +19,42 @@
import type { Action, AnySvelteComponent, IconSize } from '@hcengineering/ui'
import {
ActionIcon,
Icon,
IconChevronDown,
IconMoreH,
Label,
Menu,
showPopup,
getTreeCollapsed,
setTreeCollapsed
NavItem,
NavGroup,
ButtonIcon
} from '@hcengineering/ui'
import { createEventDispatcher } from 'svelte'
export let _id: Ref<Doc> | string | undefined = undefined
export let icon: Asset | AnySvelteComponent | undefined = undefined
export let folderIcon: boolean = false
export let iconProps: Record<string, any> | undefined = undefined
export let iconSize: IconSize = 'small'
export let label: IntlString | undefined = undefined
export let title: string | undefined = undefined
export let notifications = 0
export let parent: boolean = false
export let node: boolean = false
export let type: 'default' | 'nested' | 'nested-selectable' = 'default'
export let indent: boolean = false
export let folder: boolean = false
export let isFold: boolean = false
export let empty: boolean = false
export let visible: boolean = false
export let level: number = 0
export let collapsed: boolean = getTreeCollapsed(_id)
export let collapsedPrefix: string = ''
export let collapsed: boolean = getTreeCollapsed(_id, collapsedPrefix)
export let highlighted: boolean = false
export let selected: boolean = false
export let bold: boolean = false
export let shouldTooltip: boolean = false
export let showMenu: boolean = false
export let showNotify: boolean = false
export let forciblyСollapsed: boolean = false
export let actions: (originalEvent?: MouseEvent) => Promise<Action[]> = async () => []
let hovered = false
let pressed: boolean = false
let inlineActions: Action[] = []
let popupMenuActions: Action[] = []
@ -59,96 +65,132 @@
async function onMenuClick (ev: MouseEvent): Promise<void> {
// Read actual popup actions on open as visibility might have been changed
pressed = true
popupMenuActions = await actions().then((res) => res.filter((action) => action.inline !== true))
showPopup(Menu, { actions: popupMenuActions, ctx: _id }, ev.target as HTMLElement, () => {
hovered = false
pressed = false
})
hovered = true
}
async function onInlineClick (ev: MouseEvent, action: Action): Promise<void> {
action.action([], ev)
}
const dispatch = createEventDispatcher()
$: if (_id) 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 relative"
class:selected
class:hovered
class:parent
class:collapsed
style:padding-left={`${level > 0 ? level * 1.25 + 0.125 : indent ? 2.5 : 0.75}rem`}
on:click={() => {
collapsed = !collapsed
dispatch('click')
}}
>
{#if showNotify}
<div class="notify" />
{/if}
{#if icon && !node}
<div class="an-element__icon" class:folder>
<Icon {icon} {iconProps} size={iconSize} />
</div>
{/if}
<span class="an-element__label" class:title={node} class:bold>
{#if label}<Label {label} />{:else}{title}{/if}
</span>
{#if parent}
<div class="an-element__tool arrow hidden">
<IconChevronDown size={'small'} />
</div>
{/if}
<div class="an-element__grow" />
{#if inlineActions.length > 0}
{#each inlineActions as action}
<div class="an-element__tool" on:click|preventDefault|stopPropagation={(ev) => onInlineClick(ev, action)}>
<Icon icon={action.icon ?? ActionIcon} size={'small'} />
</div>
{/each}
{/if}
{#if popupMenuActions.length === 1 && popupMenuActions[0].icon}
<div id={_id} class="an-element__tool">
<ActionIcon
label={popupMenuActions[0].label}
icon={popupMenuActions[0].icon}
size={'small'}
action={async (ev) => {
void popupMenuActions[0].action(_id, ev)
}}
/>
</div>
{:else if popupMenuActions.length > 0}
<div class="an-element__tool" class:pressed={hovered} on:click|preventDefault|stopPropagation={onMenuClick}>
<IconMoreH size={'small'} />
</div>
{/if}
{#if notifications > 0 && collapsed}
<div class="an-element__counter">{notifications}</div>
{/if}
</div>
{#if parent && !collapsed}
<div class="antiNav-element__dropbox"><slot /></div>
{#if parent}
<NavGroup
{_id}
categoryName={_id ?? 'nav'}
{icon}
{folderIcon}
{iconProps}
{iconSize}
{label}
{title}
{highlighted}
{selected}
isOpen={!collapsed}
{collapsedPrefix}
{type}
{isFold}
{empty}
{visible}
{forciblyСollapsed}
{shouldTooltip}
showMenu={showMenu || pressed}
on:click
on:toggle={(ev) => {
if (ev.detail !== undefined) collapsed = !ev.detail
}}
>
<svelte:fragment slot="extra"><slot name="extra" /></svelte:fragment>
<svelte:fragment slot="tools">
{#if inlineActions.length > 0}
{#each inlineActions as action}
<ButtonIcon
icon={action.icon ?? ActionIcon}
size={'extra-small'}
kind={'tertiary'}
on:click={(ev) => onInlineClick(ev, action)}
/>
{/each}
{/if}
{#if popupMenuActions.length === 1 && popupMenuActions[0].icon}
<ButtonIcon
id={_id}
icon={popupMenuActions[0].icon}
size={'extra-small'}
kind={'tertiary'}
tooltip={{ label: popupMenuActions[0].label, direction: 'top' }}
on:click={async (ev) => {
void popupMenuActions[0].action(_id, ev)
}}
/>
{:else if popupMenuActions.length > 0}
<ButtonIcon icon={IconMoreH} size={'extra-small'} kind={'tertiary'} {pressed} on:click={onMenuClick} />
{/if}
</svelte:fragment>
<svelte:fragment slot="visible"><slot name="visible" /></svelte:fragment>
<slot />
</NavGroup>
{:else}
<NavItem
{_id}
{label}
{title}
{icon}
{folderIcon}
{iconProps}
{iconSize}
{selected}
{bold}
{indent}
isOpen={!collapsed}
{collapsedPrefix}
{isFold}
{empty}
{visible}
{forciblyСollapsed}
{level}
{shouldTooltip}
showMenu={showMenu || pressed}
on:click
>
<slot />
<svelte:fragment slot="extra"><slot name="extra" /></svelte:fragment>
<svelte:fragment slot="actions">
{#if $$slots.actions}<slot name="actions" />{/if}
{#if inlineActions.length > 0}
{#each inlineActions as action}
<ButtonIcon
icon={action.icon ?? ActionIcon}
size={'extra-small'}
kind={'tertiary'}
on:click={(ev) => onInlineClick(ev, action)}
/>
{/each}
{/if}
{#if popupMenuActions.length === 1 && popupMenuActions[0].icon}
<ButtonIcon
id={_id}
icon={popupMenuActions[0].icon}
size={'extra-small'}
kind={'tertiary'}
tooltip={{ label: popupMenuActions[0].label, direction: 'top' }}
on:click={async (ev) => {
void popupMenuActions[0].action(_id, ev)
}}
/>
{:else if popupMenuActions.length > 0}
<ButtonIcon icon={IconMoreH} size={'extra-small'} kind={'tertiary'} {pressed} on:click={onMenuClick} />
{/if}
</svelte:fragment>
<svelte:fragment slot="notify">
{#if $$slots.notify}<slot name="notify" />{/if}
</svelte:fragment>
<svelte:fragment slot="dropbox">
{#if isFold}<slot name="dropbox" />{/if}
</svelte:fragment>
<svelte:fragment slot="visible"><slot name="visible" /></svelte:fragment>
</NavItem>
{/if}
<style lang="scss">
.notify {
position: absolute;
top: 0.5rem;
left: 1.5rem;
height: 0.5rem;
width: 0.5rem;
background-color: var(--theme-inbox-notify);
border-radius: 50%;
}
</style>

View File

@ -19,31 +19,50 @@
import TreeElement from './TreeElement.svelte'
import { AnySvelteComponent } from '@hcengineering/ui'
export let _id: Ref<Doc>
export let _id: Ref<Doc> | undefined = undefined
export let icon: Asset | AnySvelteComponent | undefined = undefined
export let folderIcon: boolean = false
export let iconProps: Record<string, any> | undefined = undefined
export let iconSize: IconSize = 'small'
export let title: string
export let notifications = 0
export let actions: (originalEvent?: MouseEvent) => Promise<Action[]> = async () => []
export let selected: boolean = false
export let showMenu: boolean = false
export let empty: boolean = false
export let bold: boolean = false
export let indent: boolean = false
export let isFold: boolean = false
export let level: number = 0
export let shouldTooltip: boolean = false
export let showNotify: boolean = false
export let forciblyСollapsed: boolean = false
export let collapsedPrefix: string = ''
</script>
<TreeElement
{_id}
{icon}
{folderIcon}
{iconSize}
{iconProps}
{title}
{notifications}
{selected}
{actions}
{iconProps}
{bold}
{indent}
{showNotify}
collapsed
{shouldTooltip}
{isFold}
{empty}
{collapsedPrefix}
{level}
{showMenu}
{forciblyСollapsed}
on:click
/>
>
<svelte:fragment slot="extra"><slot name="extra" /></svelte:fragment>
<svelte:fragment slot="dropbox"><slot name="dropbox" /></svelte:fragment>
<svelte:fragment slot="visible"><slot name="visible" /></svelte:fragment>
</TreeElement>

View File

@ -22,13 +22,22 @@
export let title: string | undefined = undefined
export let label: IntlString | undefined = undefined
export let icon: Asset | AnySvelteComponent | undefined = undefined
export let folderIcon: boolean = false
export let iconProps: Record<string, any> | undefined = undefined
export let actions: () => Promise<Action[]> = async () => []
export let notifications: number = 0
export let parent: boolean = true
export let node: boolean = false
export let folder: boolean = false
export let type: 'default' | 'nested' | 'nested-selectable' = 'default'
export let isFold: boolean = false
export let empty: boolean = false
export let visible: boolean = false
export let collapsed: boolean = false
export let collapsedPrefix: string = ''
export let highlighted: boolean = false
export let selected: boolean = false
export let showMenu: boolean = false
export let shouldTooltip: boolean = false
export let forciblyСollapsed: boolean = false
</script>
<TreeElement
@ -37,13 +46,24 @@
{label}
{iconProps}
{icon}
{folderIcon}
{notifications}
{collapsed}
bind:collapsed
{collapsedPrefix}
{actions}
{node}
{folder}
{type}
{isFold}
{empty}
{visible}
{parent}
{highlighted}
{selected}
{shouldTooltip}
{showMenu}
{forciblyСollapsed}
on:click
>
<slot />
<svelte:fragment slot="extra"><slot name="extra" /></svelte:fragment>
<svelte:fragment slot="visible"><slot name="visible" /></svelte:fragment>
</TreeElement>

View File

@ -17,8 +17,10 @@
import { Label } from '@hcengineering/ui'
export let label: IntlString
export let withButton: boolean = false
</script>
<div class="antiNav-header overflow-label">
<Label {label} />
<div class="hulyNavPanel-header" class:withButton>
<span class="overflow-label"><Label {label} /></span>
<slot />
</div>

View File

@ -17,12 +17,11 @@
import { getResource } from '@hcengineering/platform'
import preference, { SpacePreference } from '@hcengineering/preference'
import { createQuery, getClient } from '@hcengineering/presentation'
import { Scroller } from '@hcengineering/ui'
import { Scroller, NavItem } from '@hcengineering/ui'
import { NavLink } from '@hcengineering/view-resources'
import type { Application, NavigatorModel, SpecialNavModel } from '@hcengineering/workbench'
import { getSpecialSpaceClass } from '../utils'
import SpacesNav from './navigator/SpacesNav.svelte'
import SpecialElement from './navigator/SpecialElement.svelte'
import StarredNav from './navigator/StarredNav.svelte'
import TreeSeparator from './navigator/TreeSeparator.svelte'
import SavedView from './SavedView.svelte'
@ -150,18 +149,20 @@
{/if}
{#await checkIsDisabled(special) then disabled}
<NavLink space={special.id} {disabled}>
<SpecialElement
<NavItem
label={special.label}
icon={special.icon}
selected={menuSelection ? false : special.id === currentSpecial}
selected={menuSelection
? false
: special.id === currentSpecial && (currentFragment === undefined || currentFragment === '')}
{disabled}
/>
</NavLink>
{/await}
{/each}
{/if}
<div class="min-h-3 flex-no-shrink" />
{#if specials.length > 0 && (starred.length > 0 || savedMenu)}<TreeSeparator line />{/if}
<SavedView
{currentApplication}
on:shown={(res) => (savedMenu = res.detail)}
@ -181,7 +182,6 @@
{/if}
{#each model.spaces as m, i (m.label)}
{#if (i === 0 && (specials.length > 0 || starred.length > 0 || savedMenu)) || i !== 0}<TreeSeparator line />{/if}
<SpacesNav
spaces={shownSpaces.filter((it) => hierarchy.isDerived(it._class, m.spaceClass))}
{currentSpace}
@ -190,9 +190,8 @@
on:open
{currentSpecial}
{currentFragment}
deselect={menuSelection}
deselect={menuSelection || starred.some((s) => s._id === currentSpace)}
/>
{/each}
<div class="antiNav-space" />
</Scroller>
{/if}

View File

@ -264,14 +264,18 @@
return []
}
}
$: visibleFilter = myFilteredViews.find((fv) => fv._id === selectedId)
</script>
{#if shown}
<TreeNode
_id={'tree-saved'}
label={view.string.FilteredViews}
node
highlighted={selectedId !== undefined}
actions={async () => await getActions(availableFilteredViews)}
isFold
empty={myFilteredViews.length === 0}
visible={selectedId !== undefined}
>
{#each myFilteredViews as fv}
<TreeItem
@ -284,5 +288,12 @@
actions={async (ov) => await viewAction(fv, ov)}
/>
{/each}
<svelte:fragment slot="visible">
{#if visibleFilter}
{@const item = visibleFilter}
<TreeItem _id={item._id} title={item.name} selected actions={async (ov) => await viewAction(item, ov)} />
{/if}
</svelte:fragment>
</TreeNode>
{/if}

View File

@ -260,7 +260,13 @@
loc.path[4] = (currentSpecial as string) ?? resolved.defaultLocation.path[4]
} else {
loc.path[3] = resolvedSpace
loc.path[4] = resolvedSpecial ?? currentSpecial ?? resolved.defaultLocation.path[4]
if (resolvedSpecial) {
loc.path[4] = resolvedSpecial
} else if (currentSpace && currentSpecial) {
loc.path[4] = currentSpecial
} else {
loc.path[4] = resolved.defaultLocation.path[4]
}
}
}
for (let index = 0; index < loc.path.length; index++) {
@ -728,7 +734,7 @@
<!-- svelte-ignore a11y-no-static-element-interactions -->
{#if navFloat}<div class="cover shown" on:click={() => (visibleNav = false)} />{/if}
<div class="antiPanel-navigator no-print {appsDirection === 'horizontal' ? 'portrait' : 'landscape'}">
<div class="antiPanel-wrap__content">
<div class="antiPanel-wrap__content hulyNavPanel-container">
{#if currentApplication}
<NavHeader label={currentApplication.label} />
{#if currentApplication.navHeaderComponent}

View File

@ -15,7 +15,7 @@
-->
<script lang="ts">
import type { Doc, Ref, Space } from '@hcengineering/core'
import core, { AccountRole, getCurrentAccount, hasAccountRole } from '@hcengineering/core'
import { AccountRole, getCurrentAccount, hasAccountRole } from '@hcengineering/core'
import { IntlString, getResource } from '@hcengineering/platform'
import preference from '@hcengineering/preference'
import { getClient } from '@hcengineering/presentation'
@ -23,28 +23,20 @@
Action,
AnyComponent,
IconAdd,
IconEdit,
IconSearch,
getCurrentResolvedLocation,
navigate,
showPopup
} from '@hcengineering/ui'
import {
NavLink,
TreeItem,
TreeNode,
getActions as getContributedActions,
getSpacePresenter,
classIcon
} from '@hcengineering/view-resources'
import { TreeNode } from '@hcengineering/view-resources'
import { SpacesNavModel } from '@hcengineering/workbench'
import { createEventDispatcher } from 'svelte'
import { InboxNotificationsClientImpl } from '@hcengineering/notification-resources'
import { DocNotifyContext, InboxNotification } from '@hcengineering/notification'
import plugin from '../../plugin'
import { getSpaceName } from '../../utils'
import TreeSeparator from './TreeSeparator.svelte'
import SpacesNavItem from './SpacesNavItem.svelte'
export let model: SpacesNavModel
export let currentSpace: Ref<Space> | undefined
@ -89,24 +81,6 @@
}
}
async function getActions (space: Space): Promise<Action[]> {
const result = [starSpace]
const extraActions = await getContributedActions(client, space, core.class.Space)
for (const act of extraActions) {
result.push({
icon: act.icon ?? IconEdit,
label: act.label,
inline: act.inline,
action: async (ctx: any, evt: Event) => {
const impl = await getResource(act.action)
await impl(space, evt, act.actionProps)
}
})
}
return result
}
const inboxClient = InboxNotificationsClientImpl.getClient()
const notifyContextByDocStore = inboxClient.contextByDoc
const inboxNotificationsByContextStore = inboxClient.inboxNotificationsByContext
@ -164,38 +138,51 @@
} else {
filteredSpaces = spaces
}
$: visibleSpace = filteredSpaces.find((fs) => fs._id === currentSpace)
$: empty = filteredSpaces.length === 0 || filteredSpaces === undefined
$: visible =
visibleSpace !== undefined &&
(currentSpecial !== undefined || currentFragment !== undefined || currentFragment !== '') &&
!deselect &&
!empty
</script>
<TreeNode _id={'tree-' + model.id} label={model.label} node actions={async () => getParentActions()}>
<TreeNode
_id={'tree-' + model.id}
label={model.label}
actions={async () => getParentActions()}
highlighted={visible}
isFold={!empty}
{empty}
{visible}
>
{#each filteredSpaces as space, i (space._id)}
{#await getSpacePresenter(client, space._class) then presenter}
{#if separate && model.specials && i !== 0}<TreeSeparator line />{/if}
{#if model.specials && presenter}
<svelte:component
this={presenter}
{space}
{model}
{currentSpace}
{currentSpecial}
{currentFragment}
{getActions}
{deselect}
/>
{:else}
<NavLink space={space._id}>
{#await getSpaceName(client, space) then name}
<TreeItem
_id={space._id}
title={name}
icon={classIcon(client, space._class)}
selected={deselect ? false : currentSpace === space._id}
actions={async () => await getActions(space)}
bold={isChanged(space, $notifyContextByDocStore, $inboxNotificationsByContextStore)}
indent
/>
{/await}
</NavLink>
{/if}
{/await}
{#if separate && model.specials && i !== 0}<TreeSeparator line />{/if}
<SpacesNavItem
{model}
{space}
{currentSpace}
{currentSpecial}
{currentFragment}
{deselect}
isChanged={isChanged(space, $notifyContextByDocStore, $inboxNotificationsByContextStore)}
spaceActions={[starSpace]}
/>
{/each}
<svelte:fragment slot="visible">
{#if visibleSpace}
<SpacesNavItem
{model}
space={visibleSpace}
{currentSpace}
{currentSpecial}
{currentFragment}
{deselect}
isChanged={isChanged(visibleSpace, $notifyContextByDocStore, $inboxNotificationsByContextStore)}
spaceActions={[starSpace]}
forciblyСollapsed
/>
{/if}
</svelte:fragment>
</TreeNode>

View File

@ -0,0 +1,93 @@
<!--
// Copyright © 2020 Anticrm Platform Contributors.
// 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, Space } from '@hcengineering/core'
import core from '@hcengineering/core'
import { getResource } from '@hcengineering/platform'
import { getClient } from '@hcengineering/presentation'
import { Action, IconEdit } from '@hcengineering/ui'
import {
NavLink,
TreeItem,
getActions as getContributedActions,
getSpacePresenter,
classIcon
} from '@hcengineering/view-resources'
import { SpacesNavModel } from '@hcengineering/workbench'
import { getSpaceName } from '../../utils'
export let model: SpacesNavModel
export let currentSpace: Ref<Space> | undefined
export let space: Space
export let currentSpecial: string | undefined
export let currentFragment: string | undefined
export let deselect: boolean = false
export let isChanged: boolean = false
export let spaceActions: Action[] | undefined
export let forciblyСollapsed: boolean = false
const client = getClient()
async function getActions (space: Space): Promise<Action[]> {
const result = [...(spaceActions ?? [])]
const extraActions = await getContributedActions(client, space, core.class.Space)
for (const act of extraActions) {
result.push({
icon: act.icon ?? IconEdit,
label: act.label,
inline: act.inline,
action: async (ctx: any, evt: Event) => {
const impl = await getResource(act.action)
await impl(space, evt, act.actionProps)
}
})
}
return result
}
</script>
{#await getSpacePresenter(client, space._class) then presenter}
{#if model.specials && presenter}
<svelte:component
this={presenter}
{space}
{model}
{currentSpace}
{currentSpecial}
{currentFragment}
{getActions}
{deselect}
{forciblyСollapsed}
selected={deselect ? false : currentSpace === space._id}
/>
{:else}
<NavLink space={space._id}>
{#await getSpaceName(client, space) then name}
<TreeItem
_id={space._id}
title={name}
icon={classIcon(client, space._class)}
selected={deselect ? false : currentSpace === space._id}
actions={async () => await getActions(space)}
bold={isChanged}
{forciblyСollapsed}
indent
/>
{/await}
</NavLink>
{/if}
{/await}

View File

@ -15,7 +15,7 @@
<script lang="ts">
import type { Asset, IntlString } from '@hcengineering/platform'
import type { Action } from '@hcengineering/ui'
import { ActionIcon, Icon, Label } from '@hcengineering/ui'
import { ButtonIcon, NavItem } from '@hcengineering/ui'
export let icon: Asset | undefined = undefined
export let iconProps: Record<string, any> | undefined = undefined
@ -25,35 +25,33 @@
export let selected: boolean = false
export let disabled: boolean = false
export let indent: boolean = false
export let forciblyСollapsed: boolean = false
</script>
<!-- svelte-ignore a11y-mouse-events-have-key-events -->
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div class="antiNav-element" class:selected class:disabled class:indent>
<div class="an-element__icon">
{#if icon}
<Icon {icon} size={'small'} {iconProps} />
{/if}
</div>
<span class="an-element__label">
{#if label}<Label {label} />{/if}
</span>
<div class="an-element__grow" />
{#each actions as action}
{#if action.icon}
<div class="an-element__tool">
<ActionIcon
label={action.label}
<NavItem
{icon}
{iconProps}
iconSize={'small'}
{label}
count={notifications > 0 ? notifications : null}
{selected}
{disabled}
{indent}
{forciblyСollapsed}
>
<svelte:fragment slot="actions">
{#each actions as action}
{#if action.icon}
<ButtonIcon
icon={action.icon}
size={'small'}
action={async (evt) => {
kind={'tertiary'}
size={'extra-small'}
tooltip={{ label: action.label }}
on:click={async (evt) => {
await action.action({}, evt)
}}
/>
</div>
{/if}
{/each}
{#if notifications > 0}
<div class="an-element__counter">{notifications}</div>
{/if}
</div>
{/if}
{/each}
</svelte:fragment>
</NavItem>

View File

@ -14,24 +14,16 @@
-->
<script lang="ts">
import type { Class, Doc, Ref, Space } from '@hcengineering/core'
import core from '@hcengineering/core'
import { DocNotifyContext, InboxNotification } from '@hcengineering/notification'
import { InboxNotificationsClientImpl } from '@hcengineering/notification-resources'
import { IntlString, getResource } from '@hcengineering/platform'
import { IntlString } from '@hcengineering/platform'
import preference from '@hcengineering/preference'
import { getClient } from '@hcengineering/presentation'
import { Action, IconEdit } from '@hcengineering/ui'
import { Action } from '@hcengineering/ui'
import view from '@hcengineering/view'
import {
NavLink,
TreeItem,
TreeNode,
getActions as getContributedActions,
getSpacePresenter,
classIcon
} from '@hcengineering/view-resources'
import { TreeNode } from '@hcengineering/view-resources'
import { SpacesNavModel } from '@hcengineering/workbench'
import { getSpaceName } from '../../utils'
import StarredNavItem from './StarredNavItem.svelte'
export let label: IntlString
export let spaces: Space[]
@ -43,17 +35,6 @@
const client = getClient()
const unStarSpace: Action = {
label: preference.string.Unstar,
icon: preference.icon.Star,
action: async (_id: Ref<Doc>): Promise<void> => {
const current = await client.findOne(preference.class.SpacePreference, { attachedTo: _id as Ref<Space> })
if (current !== undefined) {
await client.remove(current)
}
}
}
const unStarAll: Action = {
label: preference.string.DeleteStarred,
icon: view.icon.Delete,
@ -69,23 +50,6 @@
}
}
async function getActions (space: Space): Promise<Action[]> {
const result = [unStarSpace]
const extraActions = await getContributedActions(client, space, core.class.Space)
for (const act of extraActions) {
result.push({
icon: act.icon ?? IconEdit,
label: act.label,
action: async (ctx: any, evt: Event) => {
const impl = await getResource(act.action)
await impl(space, evt, act.actionProps)
}
})
}
return result
}
function getSpaceModel (space: Ref<Class<Space>>): SpacesNavModel | undefined {
const hierarchy = client.getHierarchy()
const ancestors = [space, ...[...hierarchy.getAncestors(space)].reverse()]
@ -109,37 +73,44 @@
if (notifyContext === undefined) return false
return !notifyContext.hidden && !!inboxNotificationsByContext.get(notifyContext._id)?.length
}
$: visibleSpace = spaces.find((space) => currentSpace === space._id)
</script>
<TreeNode _id={'tree-stared'} {label} node actions={async () => [unStarAll]}>
<TreeNode
_id={'tree-stared'}
{label}
actions={async () => [unStarAll]}
highlighted={spaces.some((s) => s._id === currentSpace) && !deselect}
isFold
empty={spaces.length === 0}
visible={visibleSpace !== undefined && !deselect}
>
{#each spaces as space (space._id)}
{@const model = getSpaceModel(space._class)}
{#await getSpacePresenter(client, space._class) then presenter}
{#if presenter && model}
<svelte:component
this={presenter}
{space}
{model}
{currentSpace}
{currentSpecial}
{currentFragment}
{getActions}
{deselect}
/>
{:else}
{#await getSpaceName(client, space) then name}
<NavLink space={space._id}>
<TreeItem
_id={space._id}
title={name}
icon={classIcon(client, space._class)}
selected={currentSpace === space._id}
actions={async () => await getActions(space)}
bold={isChanged(space, $notifyContextByDocStore, $inboxNotificationsByContextStore)}
/>
</NavLink>
{/await}
{/if}
{/await}
<StarredNavItem
{space}
{model}
{currentSpace}
{currentSpecial}
{currentFragment}
{deselect}
isChanged={isChanged(space, $notifyContextByDocStore, $inboxNotificationsByContextStore)}
/>
{/each}
<svelte:fragment slot="visible">
{#if visibleSpace}
{@const model = getSpaceModel(visibleSpace._class)}
<StarredNavItem
space={visibleSpace}
{model}
{currentSpace}
{currentSpecial}
{currentFragment}
{deselect}
isChanged={isChanged(visibleSpace, $notifyContextByDocStore, $inboxNotificationsByContextStore)}
forciblyСollapsed
/>
{/if}
</svelte:fragment>
</TreeNode>

View File

@ -0,0 +1,101 @@
<!--
// Copyright © 2022, 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, Ref, Space } from '@hcengineering/core'
import core from '@hcengineering/core'
import { getResource } from '@hcengineering/platform'
import preference from '@hcengineering/preference'
import { getClient } from '@hcengineering/presentation'
import { Action, IconEdit } from '@hcengineering/ui'
import {
NavLink,
TreeItem,
getActions as getContributedActions,
getSpacePresenter,
classIcon
} from '@hcengineering/view-resources'
import { SpacesNavModel } from '@hcengineering/workbench'
import { getSpaceName } from '../../utils'
export let space: Space
export let model: SpacesNavModel | undefined
export let currentSpace: Ref<Space> | undefined
export let currentSpecial: string | undefined
export let currentFragment: string | undefined
export let deselect: boolean = false
export let isChanged: boolean = false
export let forciblyСollapsed: boolean = false
const client = getClient()
const unStarSpace: Action = {
label: preference.string.Unstar,
icon: preference.icon.Star,
action: async (_id: Ref<Doc>): Promise<void> => {
const current = await client.findOne(preference.class.SpacePreference, { attachedTo: _id as Ref<Space> })
if (current !== undefined) {
await client.remove(current)
}
}
}
async function getActions (space: Space): Promise<Action[]> {
const result = [unStarSpace]
const extraActions = await getContributedActions(client, space, core.class.Space)
for (const act of extraActions) {
result.push({
icon: act.icon ?? IconEdit,
label: act.label,
action: async (ctx: any, evt: Event) => {
const impl = await getResource(act.action)
await impl(space, evt, act.actionProps)
}
})
}
return result
}
</script>
{#await getSpacePresenter(client, space._class) then presenter}
{#if presenter && model}
<svelte:component
this={presenter}
{space}
{model}
{currentSpace}
{currentSpecial}
{currentFragment}
{getActions}
{deselect}
{forciblyСollapsed}
type={'nested'}
/>
{:else}
{#await getSpaceName(client, space) then name}
<NavLink space={space._id}>
<TreeItem
_id={space._id}
title={name}
icon={classIcon(client, space._class)}
selected={currentSpace === space._id}
actions={async () => await getActions(space)}
bold={isChanged}
{forciblyСollapsed}
/>
</NavLink>
{/await}
{/if}
{/await}

View File

@ -20,9 +20,11 @@ export class ContractPage {
readonly appContact = (): Locator => this.page.locator('[id="app-contact\\:string\\:Contacts"]')
readonly employeeNavElement = (Employee: string): Locator =>
this.page.locator(`.antiNav-element:has-text("${Employee}")`)
this.page.locator(`.hulyNavItem-container:has-text("${Employee}")`)
readonly employeeButton = (Employee: string): Locator =>
this.page.locator(`button:not(.hulyNavItem-container):has-text("${Employee}")`)
readonly employeeButton = (Employee: string): Locator => this.page.locator(`button:has-text("${Employee}")`)
readonly firstNameInput = (): Locator => this.page.locator('[placeholder="First name"]')
readonly lastNameInput = (): Locator => this.page.locator('[placeholder="Last name"]')
readonly emailInput = (): Locator => this.page.locator('[placeholder="Email"]')
@ -54,8 +56,8 @@ export class ContractPage {
readonly personName = (person: string): Locator => this.page.locator(`text=${person}`)
readonly personTable = (): Locator => this.page.locator('.antiTable-body__row')
readonly personMarina = (): Locator => this.page.getByRole('link', { name: 'MM M. Marina' })
readonly comapnyTab = (): Locator => this.page.locator('.antiNav-element:has-text("Company")')
readonly addCompany = (): Locator => this.page.locator('button:has-text("Company")')
readonly comapnyTab = (): Locator => this.page.locator('.hulyNavItem-container:has-text("Company")')
readonly addCompany = (): Locator => this.page.locator('button.antiButton:has-text("Company")')
readonly companyName = (): Locator => this.page.locator('[placeholder="Company name"]')
readonly companyCreateButton = (): Locator => this.page.locator('button:has-text("Create")')
readonly companyByName = (company: string): Locator => this.page.locator(`text=${company}`)

View File

@ -20,8 +20,10 @@ export class DocumentsPage extends CommonPage {
readonly buttonCreateDocument = (): Locator =>
this.page.locator('div[data-float="navigator"] button[id="new-document"]')
readonly divTeamspacesParent = (): Locator => this.page.locator('div#tree-teamspaces').locator('xpath=..')
readonly buttonCreateTeamspace = (): Locator => this.page.locator('div#tree-teamspaces > button')
readonly divTeamspacesParent = (): Locator =>
this.page.locator('div#navGroup-tree-teamspaces').locator('xpath=../button[1]')
readonly buttonCreateTeamspace = (): Locator => this.page.locator('button#tree-teamspaces')
readonly formNewTeamspace = (): Locator => this.page.locator('form[id="document:string:NewTeamspace"]')
readonly formEditTeamspace = (): Locator => this.page.locator('form[id="document:string:EditTeamspace"]')
readonly inputModalNewTeamspaceTitle = (): Locator =>
@ -62,25 +64,31 @@ export class DocumentsPage extends CommonPage {
async openTeamspace (name: string): Promise<void> {
const classes = await this.page
.locator('div.antiNav-element span[class*="label"]', { hasText: name })
.locator('xpath=..')
.locator('button.hulyNavGroup-header span[class*="label"]', { hasText: name })
.locator('xpath=../..')
.getAttribute('class')
if (classes != null && classes.includes('collapsed')) {
await this.page.locator('div.antiNav-element span[class*="label"]', { hasText: name }).click()
if (classes != null && !classes.includes('isOpen')) {
await this.page.getByRole('button', { name }).click()
}
}
async checkTeamspaceExist (name: string): Promise<void> {
await expect(this.page.locator('div[class*="dropbox"] span[class*="label"]', { hasText: name })).toHaveCount(1)
await expect(
this.page.locator('div[class*="hulyNavGroup-content"] span[class*="label"]', { hasText: name })
).toHaveCount(1)
}
async checkTeamspaceNotExist (name: string): Promise<void> {
await expect(this.page.locator('div[class*="dropbox"] span[class*="label"]', { hasText: name })).toHaveCount(0)
await expect(
this.page.locator('button[class*="hulyNavGroup-header"] span[class*="label"]', { hasText: name })
).toHaveCount(0)
}
async moreActionTeamspace (name: string, action: string): Promise<void> {
await this.page.locator('div[class*="dropbox"] > div > span[class*="label"]', { hasText: name }).hover()
await this.page.locator(`xpath=//span[text()="${name}"]/../div[last()]`).click()
await this.page.locator('button.hulyNavGroup-header span[class*="label"]', { hasText: name }).hover()
await this.page
.locator(`xpath=//span[text()="${name}"]/../../div[@class="hulyNavGroup-header__tools"]/button[last()]`)
.click()
await this.selectFromDropdown(this.page, action)
}
@ -89,14 +97,14 @@ export class DocumentsPage extends CommonPage {
}
async openDocument (name: string): Promise<void> {
await this.page.locator('div.tree > span[class*="label"]', { hasText: name }).click()
await this.page.locator('button.hulyNavItem-container > span[class*="label"]', { hasText: name }).click()
}
async openDocumentForTeamspace (spaceName: string, documentName: string): Promise<void> {
await this.page
.locator('div.parent > span[class*="label"]', { hasText: spaceName })
.locator('xpath=../following-sibling::div[1]')
.locator('div.tree > span[class*="label"]', { hasText: documentName })
.locator('button.hulyNavGroup-header span[class*="label"]', { hasText: spaceName })
.locator('xpath=../../following-sibling::div[1]')
.locator('button.hulyNavItem-container span[class*="label"]', { hasText: documentName })
.click()
}
@ -120,10 +128,11 @@ export class DocumentsPage extends CommonPage {
}
async moreActionsOnDocument (documentName: string, action: string): Promise<void> {
await this.page.locator('button.hulyNavItem-container span[class*="label"]', { hasText: documentName }).hover()
await this.page
.locator('div.tree > span[class*="label"]', { hasText: documentName })
.locator('button.hulyNavItem-container > span[class*="label"]', { hasText: documentName })
.locator('xpath=..')
.locator('div[class*="tool"]:nth-child(6)')
.locator('div.hulyNavItem-actions > button:last-child')
.click()
await this.selectFromDropdown(this.page, action)
}

View File

@ -12,7 +12,9 @@ export class ApplicationsPage extends CommonRecruitingPage {
}
readonly pageHeader = (): Locator => this.page.locator('span[class*="header"]', { hasText: 'Applications' })
readonly buttonCreateApplication = (): Locator => this.page.locator('button > span', { hasText: 'Application' })
readonly buttonCreateApplication = (): Locator =>
this.page.locator('button.antiButton > span', { hasText: 'Application' })
readonly buttonTalentSelector = (): Locator => this.page.locator('div[id="vacancy.talant.selector"]')
readonly buttonSpaceSelector = (): Locator => this.page.locator('div[id="space.selector"]')
readonly buttonAssignedRecruiter = (): Locator =>

View File

@ -10,7 +10,7 @@ export class RecruitingPage {
recruitApplication = (): Locator => this.page.locator('[id="app-recruit\\:string\\:RecruitApplication"]')
talentsNavElement = (): Locator => this.page.locator('text=Talents')
reviews = (): Locator => this.page.locator('text=Reviews')
reviewButton = (): Locator => this.page.locator('button:has-text("Review")')
reviewButton = (): Locator => this.page.getByRole('button', { name: 'Review', exact: true })
frontendEngineerOption = (): Locator => this.page.locator('td:has-text("Frontend Engineer")')
searchOrRunCommandInput = (): Locator => this.page.locator('[placeholder="Search\\ or\\ run\\ a\\ command\\.\\.\\."]')
@ -35,7 +35,7 @@ export class RecruitingPage {
generalChatLink = (): Locator => this.page.locator('text=general')
contactsButton = (): Locator => this.page.locator('[id="app-contact\\:string\\:Contacts"]')
employeeSection = (): Locator => this.page.locator('.antiNav-element:has-text("Employee")')
employeeSection = (): Locator => this.page.getByRole('button', { name: 'Employee' })
johnAppleseed = (): Locator => this.page.locator('text=Appleseed John')
async clickRecruitApplication (): Promise<void> {

View File

@ -48,7 +48,9 @@ export class TalentsPage extends CommonRecruitingPage {
talentsLink = (): Locator => this.page.locator('text=Talents')
firstNameInput = (): Locator => this.page.locator('[placeholder="First name"]')
lastNameInput = (): Locator => this.page.locator('[placeholder="Last name"]')
skillsButton = (): Locator => this.page.locator('button:has-text("Skills")')
skillsButton = (): Locator =>
this.page.locator('[id="recruit\\:string\\:CreateTalent"]').getByRole('button', { name: 'Skills' })
addSkillButton = (): Locator => this.page.locator('.header > button:nth-child(3)')
skillTitleInput = (): Locator => this.page.getByPlaceholder('Please type title')
createSkillInput = (): Locator => this.page.getByPlaceholder('Please type skill title')
@ -58,7 +60,7 @@ export class TalentsPage extends CommonRecruitingPage {
createCandidateButton = (): Locator => this.page.locator('button:has-text("Create")')
openOtherSkills = (): Locator => this.page.getByText('Other')
skillsLink = (): Locator => this.page.locator('text=Skills')
newSkillButton = (): Locator => this.page.locator('button:has-text("Skill")')
newSkillButton = (): Locator => this.page.getByRole('button', { name: 'Skill', exact: true })
emailContact = (): Locator =>
this.page.locator('div[class^="popupPanel-body__header"] button[id="gmail:string:Email"]')

View File

@ -51,7 +51,7 @@ export class VacanciesPage extends CommonRecruitingPage {
readonly vacancyRow = (vacancyId: string): Locator =>
this.page.locator(`tr:has-text("${vacancyId}") > td:nth-child(3) >> .sm-tool-icon`)
readonly applicationButton = (): Locator => this.page.locator('button:has-text("Application")')
readonly applicationButton = (): Locator => this.page.getByRole('button', { name: 'Application', exact: true })
readonly talentSelector = (): Locator =>
this.page.locator('form[id="recruit:string:CreateApplication"] [id="vacancy.talant.selector"]')

View File

@ -81,7 +81,7 @@ export class CommonTrackerPage extends CalendarPage {
trackerApplicationButton = (): Locator => this.page.locator('[id="app-tracker\\:string\\:TrackerApplication"]')
componentsLink = (): Locator => this.page.locator('text=Components')
createComponentButton = (): Locator => this.page.locator('button:has-text("Component")')
createComponentButton = (): Locator => this.page.getByRole('button', { name: 'Component', exact: true })
componentNameInput = (): Locator => this.page.locator('[placeholder="Component\\ name"]')
createComponentConfirmButton = (): Locator => this.page.locator('button:has-text("Create component")')
newIssueButton = (): Locator => this.page.locator('button:has-text("New issue")')

View File

@ -3,7 +3,7 @@ import { CommonTrackerPage } from './common-tracker-page'
import { NewIssue } from './types'
export class TemplatePage extends CommonTrackerPage {
buttonNewTemplate = (): Locator => this.page.locator('button > span', { hasText: 'Template' })
buttonNewTemplate = (): Locator => this.page.getByRole('button', { name: 'Template', exact: true })
inputIssueTitle = (): Locator => this.page.locator('form[id$="NewProcess"] input[type="text"]')
inputIssueDescription = (): Locator => this.page.locator('form[id$="NewProcess"] div.tiptap')
buttonPopupCreateNewTemplatePriority = (): Locator =>

View File

@ -9,24 +9,23 @@ export class TrackerNavigationMenuPage extends CommonPage {
this.page = page
}
buttonCreateProject = (): Locator => this.page.locator('div#tree-projects').locator('xpath=..')
buttonProjectsParent = (): Locator => this.page.locator('div.parent > span')
buttonCreateProject = (): Locator => this.page.locator('div#navGroup-tree-projects').locator('xpath=../button[1]')
buttonProjectsParent = (): Locator => this.page.locator('button.hulyNavGroup-header span')
templateLinkForProject = (projectName: string): Locator =>
this.page.locator(`a[href$="templates"][href*="${projectName}"]`)
issuesLinkForProject = (projectName: string): Locator =>
this.page.locator(
`xpath=//div[contains(@class, "parent")]/span[text()="${projectName}"]/../following-sibling::div[1]/a[contains(@href, "issues")]`,
{ hasText: 'Issues' }
)
this.page
.getByRole('button', { name: projectName, exact: true })
.locator('xpath=following-sibling::div[1]/a[contains(@href, "issues")]', { hasText: 'Issues' })
milestonesLinkForProject = (projectName: string): Locator =>
this.page.locator(`div[class*="antiNav-element"] a[href$="milestones"][href*="${projectName}"]> div > span`, {
this.page.locator(`a[href$="milestones"][href*="${projectName}"] > button[class*="hulyNavItem-container"] > span`, {
hasText: 'Milestones'
})
componentsLinkForProject = (projectName: string): Locator =>
this.page.locator(`div[class*="antiNav-element"] a[href$="components"][href*="${projectName}"]> div > span`, {
this.page.locator(`a[href$="components"][href*="${projectName}"] > button[class*="hulyNavItem-container"] > span`, {
hasText: 'Components'
})
@ -39,7 +38,7 @@ export class TrackerNavigationMenuPage extends CommonPage {
milestone = (): Locator => this.page.getByRole('link', { name: 'Milestones' })
templates = (): Locator => this.page.getByRole('link', { name: 'Templates' })
createProjectButton = (): Locator => this.buttonCreateProject().locator('button.small')
createProjectButton = (): Locator => this.buttonCreateProject().locator('#tree-projects')
async pressCreateProjectButton (): Promise<void> {
await this.buttonCreateProject().hover()
@ -70,8 +69,8 @@ export class TrackerNavigationMenuPage extends CommonPage {
await this.buttonProjectsParent().filter({ hasText: projectName }).hover()
await this.buttonProjectsParent()
.filter({ hasText: projectName })
.locator('xpath=..')
.locator('div[class*="tool"]:not([class*="arrow"])')
.locator('xpath=../..')
.locator('div[class*="tools"] button')
.click()
await this.selectFromDropdown(this.page, action)
}

View File

@ -113,7 +113,7 @@ export async function createComponent (page: Page, componentName: string): Promi
await expect(page).toHaveURL(
`${PlatformURI}/workbench/sanity-ws/tracker/tracker%3Aproject%3ADefaultProject/components`
)
await page.click('button:has-text("Component")')
await page.getByRole('button', { name: 'Component', exact: true }).click()
await page.click('[placeholder="Component\\ name"]')
await page.fill('[placeholder="Component\\ name"]', componentName)
await page.click('button:has-text("Create component")')
@ -124,7 +124,7 @@ export async function createMilestone (page: Page, milestoneName: string): Promi
await expect(page).toHaveURL(
`${PlatformURI}/workbench/sanity-ws/tracker/tracker%3Aproject%3ADefaultProject/milestones`
)
await page.click('button:has-text("Milestone")')
await page.getByRole('button', { name: 'Milestone', exact: true }).click()
await page.click('[placeholder="Milestone\\ name"]')
await page.fill('[placeholder="Milestone\\ name"]', milestoneName)
await page.click('button:has-text("Create")')