[UBER-155] Replace "Back" button with "Breadcrumbs" (#3239)

Signed-off-by: Sergei Ogorelkov <sergei.ogorelkov@icloud.com>
This commit is contained in:
Sergei Ogorelkov 2023-05-24 10:29:18 +04:00 committed by GitHub
parent 6773c0d5bb
commit e7ba92c764
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 273 additions and 45 deletions

View File

@ -109,7 +109,7 @@
>
<svelte:fragment slot="navigator">
{#if $$slots.navigator}
<div class="buttons-group xsmall-gap mx-2">
<div class="flex-row-center flex-gap-1-5 mx-2">
<slot name="navigator" />
</div>
{/if}

View File

@ -0,0 +1,67 @@
<!--
// 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 { Component, ScrollerBar, getPlatformColor } from '@hcengineering/ui'
import BreadcrumbsElement from './BreadcrumbsElement.svelte'
import { NavLink } from '../..'
import { BreadcrumbsModel } from './types'
import { hasComponent } from './utils'
export let models: readonly BreadcrumbsModel[]
export let gap: 'none' | 'small' | 'big' = 'small'
let scroller: HTMLElement
function getPosition (position: number): 'start' | 'end' | 'middle' {
if (position === 0) {
return 'start'
}
if (position === models.length - 1) {
return 'end'
}
return 'middle'
}
</script>
<ScrollerBar {gap} bind:scroller>
{#each models as model, i}
{@const { color } = model}
{#if hasComponent(model)}
{@const { component, props } = model}
<BreadcrumbsElement
position={getPosition(i)}
color={color !== undefined ? getPlatformColor(color) : 'var(--accent-bg-color)'}
>
{#if typeof component === 'string'}
<Component is={component} {props} />
{:else}
<svelte:component this={component} {...props} />
{/if}
</BreadcrumbsElement>
{:else}
{@const { title, href, onClick } = model}
<NavLink {href} noUnderline {onClick}>
<BreadcrumbsElement
label={title}
{title}
position={getPosition(i)}
color={color !== undefined ? getPlatformColor(color) : 'var(--accent-bg-color)'}
/>
</NavLink>
{/if}
{/each}
</ScrollerBar>

View File

@ -1,6 +1,5 @@
<!--
// Copyright © 2020, 2021 Anticrm Platform Contributors.
// Copyright © 2021 Hardcore Engineering Inc.
// 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
@ -14,37 +13,38 @@
// limitations under the License.
-->
<script lang="ts">
import { afterUpdate } from 'svelte'
import type { StatesBarPosition } from '../..'
import { resizeObserver } from '@hcengineering/ui'
export let label: string
export let position: StatesBarPosition = undefined
export let selected: boolean = false
export let color: string = 'var(--body-color)'
export let label: string | undefined = undefined
export let title: string | undefined = undefined
export let position: 'start' | 'middle' | 'end' | undefined = undefined
export let selected = false
export let color = 'var(--body-color)'
let lenght: number = 0
let text: HTMLElement
let divBar: HTMLElement
let svgBack: SVGElement
afterUpdate(() => {
if (text) lenght = text.clientWidth + 32 > 300 ? 300 : text.clientWidth + 32
})
let lenght = 0
</script>
<div class="hidden-text text-md font-medium" bind:this={text}>{label}</div>
<div
class="hidden-text text-md font-medium pointer-events-none content-pointer-events-none"
use:resizeObserver={(element) => (lenght = element.clientWidth + 32 > 300 ? 300 : element.clientWidth + 32)}
>
{#if $$slots.default}
<slot />
{:else}
{label}
{/if}
</div>
{#if lenght > 0}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div
bind:this={divBar}
class="asb-bar"
class:selected
class:cursor-pointer={!selected}
class:cursor-default={selected}
on:click|stopPropagation
class:cursor-pointer={!selected && !$$slots.default}
class:cursor-default={selected || $$slots.default}
{title}
on:click
>
<svg
bind:this={svgBack}
class="asb-bar__back"
viewBox="0 0 {lenght} 24"
xmlns="http://www.w3.org/2000/svg"
@ -53,6 +53,7 @@
{#if position === 'start'}
<path
class="asb-bar__{selected ? 'selected' : 'element'}"
class:asb-bar__disabled={$$slots.default}
style={selected ? `fill: ${color};` : ''}
d="M0,5.3C0,2.4,2.3,0,5.2,0h1.3h{lenght -
13}h1.2c0.5,0,1,0.3,1.2,0.9l4,10.7c0.1,0.3,0.1,0.7,0,0.9l-4,10.7c-0.2,0.5-0.7,0.9-1.2,0.9 l-1.2,0h-{lenght -
@ -61,6 +62,7 @@
{:else if position === 'middle'}
<path
class="asb-bar__{selected ? 'selected' : 'element'}"
class:asb-bar__disabled={$$slots.default}
style={selected ? `fill: ${color};` : ''}
d="M4,11.5L0.1,0.9C-0.1,0.5,0.2,0,0.6,0h5.8h{lenght -
13}h1.2c0.5,0,1,0.3,1.2,0.9l4,10.7c0.1,0.3,0.1,0.7,0,0.9l-4,10.7 c-0.2,0.5-0.7,0.9-1.2,0.9h-1.2h-{lenght -
@ -69,6 +71,7 @@
{:else if position === 'end'}
<path
class="asb-bar__{selected ? 'selected' : 'element'}"
class:asb-bar__disabled={$$slots.default}
style={selected ? `fill: ${color};` : ''}
d="M4.1,11.5l-4-10.6C-0.1,0.5,0.2,0,0.7,0h{lenght - 7}C{lenght -
3},0,{lenght},2.4,{lenght},5.3v13.3c0,2.9-2.4,5.3-5.3,5.3h-{lenght}H0.6c-0.5,0-0.8-0.5-0.6-0.9L4,12.5C4.1,12.2,4.1,11.8,4,11.5z"
@ -76,11 +79,20 @@
{:else}
<path
class="asb-bar__{selected ? 'selected' : 'element'}"
class:asb-bar__disabled={$$slots.default}
style={selected ? `fill: ${color};` : ''}
d="M0,5.3C0,2.4,2.3,0,5.2,0h1.3h{lenght}h1.3C49.7,0,52,2.4,52,5.3v13.3c0,2.9-2.3,5.3-5.2,5.3h-1.3h-{lenght}H5.2 C2.3,24,0,21.6,0,18.7V5.3z"
/>
{/if}
</svg>
<div class="asb-label__container" class:selected><div class="overflow-label">{label}</div></div>
<div class="asb-label__container" class:selected class:disabled={!$$slots.default}>
<div class="overflow-label">
{#if $$slots.default}
<slot />
{:else}
{label}
{/if}
</div>
</div>
</div>
{/if}

View File

@ -0,0 +1,30 @@
// 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.
import { AnyComponent, AnySvelteComponent } from '@hcengineering/ui'
interface BreadcrumbsProps {
readonly color?: number | undefined
}
type TextBreadcrumbsProps = { title: string } & (
| { readonly href: string, readonly onClick?: undefined }
| { readonly href?: undefined, readonly onClick: (event: MouseEvent) => void }
)
export interface ComponentBreadcrumbsProps {
readonly component: AnyComponent | AnySvelteComponent
readonly props: Record<string, any>
}
export type BreadcrumbsModel = BreadcrumbsProps & (TextBreadcrumbsProps | ComponentBreadcrumbsProps)

View File

@ -0,0 +1,18 @@
// 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.
import { BreadcrumbsModel, ComponentBreadcrumbsProps } from './types'
export function hasComponent (model: BreadcrumbsModel): model is ComponentBreadcrumbsProps {
return 'component' in model
}

View File

@ -39,6 +39,8 @@ export { default as IndexedDocumentCompare } from './components/IndexedDocumentC
export { default as DraggableList } from './components/DraggableList.svelte'
export { default as NavLink } from './components/NavLink.svelte'
export { default as IconForward } from './components/icons/Forward.svelte'
export { default as Breadcrumbs } from './components/breadcrumbs/Breadcrumbs.svelte'
export { default as BreadcrumbsElement } from './components/breadcrumbs/BreadcrumbsElement.svelte'
export { default } from './plugin'
export * from './types'
export * from './utils'

View File

@ -2,6 +2,8 @@ import { Client, Doc, RelatedDocument } from '@hcengineering/core'
import { Asset, IntlString, Resource } from '@hcengineering/platform'
import { AnyComponent, AnySvelteComponent } from '@hcengineering/ui'
export * from './components/breadcrumbs/types'
/**
* @public
*/

View File

@ -288,7 +288,6 @@ input.search {
color: var(--theme-content-color);
overflow: hidden;
visibility: visible;
user-select: none;
&:not(.nowrap) {

View File

@ -341,6 +341,7 @@
&:hover { fill: var(--theme-button-hovered); }
}
&__selected { fill: var(--theme-button-pressed); }
&__disabled { pointer-events: none; }
.asb-label__container {
position: absolute;
@ -356,7 +357,8 @@
font-weight: 500;
font-size: 0.8125rem;
color: var(--theme-dark-color);
pointer-events: none;
&.disabled { pointer-events: none; }
&.selected {
color: var(--theme-caption-color);

View File

@ -32,7 +32,13 @@
Label,
showPopup
} from '@hcengineering/ui'
import { ContextMenu, DocAttributeBar, invokeAction, UpDownNavigator } from '@hcengineering/view-resources'
import {
ContextMenu,
DocAttributeBar,
invokeAction,
ParentsNavigator,
UpDownNavigator
} from '@hcengineering/view-resources'
import { createEventDispatcher, onMount } from 'svelte'
import board from '../plugin'
import { getCardActions } from '../utils/CardActionUtils'
@ -116,6 +122,7 @@
>
<svelte:fragment slot="navigator">
<UpDownNavigator element={object} />
<ParentsNavigator element={object} />
</svelte:fragment>
<svelte:fragment slot="header">
<div class="flex fs-title flex-gap-1">

View File

@ -119,7 +119,6 @@
color: var(--theme-caption-color);
overflow: hidden;
visibility: visible;
display: -webkit-box;
/* autoprefixer: ignore next */
-webkit-box-orient: vertical;

View File

@ -47,7 +47,7 @@
SelectPopup,
showPopup
} from '@hcengineering/ui'
import { ClassAttributeBar, ContextMenu, UpDownNavigator } from '@hcengineering/view-resources'
import { ClassAttributeBar, ContextMenu, ParentsNavigator, UpDownNavigator } from '@hcengineering/view-resources'
import { createEventDispatcher, onDestroy, onMount } from 'svelte'
import document from '../plugin'
import DocumentEditor from './DocumentEditor.svelte'
@ -385,6 +385,7 @@
>
<svelte:fragment slot="navigator">
<UpDownNavigator element={documentObject} />
<ParentsNavigator element={documentObject} />
</svelte:fragment>
<svelte:fragment slot="header">
<span class="fs-title flex-row-center flex-shrink gap-1-5">

View File

@ -15,12 +15,11 @@
-->
<script lang="ts">
import { Ref } from '@hcengineering/core'
import { statusStore } from '@hcengineering/presentation'
import { BreadcrumbsElement, statusStore } from '@hcengineering/presentation'
import task, { SpaceWithStates, State } from '@hcengineering/task'
import { getColorNumberByText, getPlatformColor, ScrollerBar } from '@hcengineering/ui'
import { createEventDispatcher } from 'svelte'
import type { StatesBarPosition } from '../..'
import StatesBarElement from './StatesBarElement.svelte'
export let space: Ref<SpaceWithStates>
export let state: Ref<State> | undefined = undefined
@ -57,12 +56,13 @@
<ScrollerBar {gap} bind:scroller={divScroll}>
{#each states as item, i (item._id)}
<StatesBarElement
<BreadcrumbsElement
label={item.name}
position={getPosition(i)}
selected={item._id === state}
color={getPlatformColor(item.color ?? getColorNumberByText(item.name))}
on:click={(ev) => {
ev.stopPropagation()
if (item._id !== state) selectItem(ev, item)
}}
/>

View File

@ -34,14 +34,11 @@
const spaceQuery = createQuery()
let currentProject: Project | undefined = value?.$lookup?.space
$: if (projects === undefined) {
if (value && value?.$lookup?.space === undefined) {
spaceQuery.query(tracker.class.Project, { _id: value.space }, (res) => ([currentProject] = res))
} else {
spaceQuery.unsubscribe()
}
$: if (value?.$lookup?.space === undefined && !projects?.has(value.space)) {
spaceQuery.query(tracker.class.Project, { _id: value.space }, (res) => ([currentProject] = res))
} else {
currentProject = projects.get(value.space)
currentProject = value?.$lookup?.space ?? projects?.get(value.space)
spaceQuery.unsubscribe()
}
$: title = currentProject ? `${currentProject.identifier}-${value?.number}` : `${value?.number}`

View File

@ -20,7 +20,7 @@
import { getResource } from '@hcengineering/platform'
import presentation, { createQuery, getClient } from '@hcengineering/presentation'
import setting, { settingId } from '@hcengineering/setting'
import type { Issue, Project } from '@hcengineering/tracker'
import { Issue, Project } from '@hcengineering/tracker'
import {
Button,
EditBox,
@ -34,7 +34,14 @@
navigate,
showPopup
} from '@hcengineering/ui'
import { ActionContext, ContextMenu, DocNavLink, UpDownNavigator, contextStore } from '@hcengineering/view-resources'
import {
ActionContext,
ContextMenu,
DocNavLink,
ParentsNavigator,
UpDownNavigator,
contextStore
} from '@hcengineering/view-resources'
import { createEventDispatcher, onDestroy } from 'svelte'
import { generateIssueShortLink, getIssueId } from '../../../issues'
import tracker from '../../../plugin'
@ -158,6 +165,7 @@
<svelte:fragment slot="navigator">
{#if !embedded}
<UpDownNavigator element={issue} />
<ParentsNavigator element={issue} />
{/if}
<span class="ml-4 fs-title select-text-i">

View File

@ -16,7 +16,7 @@
import core, { Class, Doc, Ref, SortingOrder, TxCUD, WithLookup } from '@hcengineering/core'
import { createQuery, getClient } from '@hcengineering/presentation'
import type { Scrum, ScrumRecord } from '@hcengineering/tracker'
import { UpDownNavigator } from '@hcengineering/view-resources'
import { ParentsNavigator, UpDownNavigator } from '@hcengineering/view-resources'
import { Panel } from '@hcengineering/panel'
import { Button, closePanel, TabItem, TabList } from '@hcengineering/ui'
import tracker from '../../plugin'
@ -99,6 +99,7 @@
<Panel object={scrumRecord} isUtils={isRecording} isHeader={false} on:close>
<svelte:fragment slot="navigator">
<UpDownNavigator element={scrumRecord} />
<ParentsNavigator element={scrumRecord} />
</svelte:fragment>
<svelte:fragment slot="title">
<span class="fs-title select-text-i">

View File

@ -32,7 +32,7 @@
navigate,
showPopup
} from '@hcengineering/ui'
import { ContextMenu, UpDownNavigator } from '@hcengineering/view-resources'
import { ContextMenu, ParentsNavigator, UpDownNavigator } from '@hcengineering/view-resources'
import { createEventDispatcher, onDestroy, onMount } from 'svelte'
import tracker from '../../plugin'
@ -158,6 +158,7 @@
<svelte:fragment slot="navigator">
{#if !embedded}
<UpDownNavigator element={template} />
<ParentsNavigator element={template} />
{/if}
<div class="ml-2">

View File

@ -32,7 +32,7 @@
import { AnyComponent, Button, Component, IconMixin, IconMoreH, showPopup } from '@hcengineering/ui'
import view from '@hcengineering/view'
import { createEventDispatcher, onDestroy } from 'svelte'
import { ContextMenu } from '..'
import { ContextMenu, ParentsNavigator } from '..'
import { categorizeFields, getCollectionCounter, getFiltredKeys } from '../utils'
import ActionContext from './ActionContext.svelte'
import DocAttributeBar from './DocAttributeBar.svelte'
@ -302,6 +302,7 @@
<svelte:fragment slot="navigator">
{#if !embedded}
<UpDownNavigator element={object} />
<ParentsNavigator element={object} />
{/if}
</svelte:fragment>

View File

@ -0,0 +1,76 @@
<!--
// 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 { AttachedDoc, Doc } from '@hcengineering/core'
import { Breadcrumbs, BreadcrumbsModel, getClient } from '@hcengineering/presentation'
import { getObjectPresenter, isAttachedDoc } from '../utils'
import { AttributeModel } from '@hcengineering/view'
export let element: Doc | AttachedDoc
const client = getClient()
async function getParents (doc: AttachedDoc): Promise<readonly Doc[]> {
const parents: Doc[] = []
let currentDoc: Doc | undefined = doc
while (currentDoc && isAttachedDoc(currentDoc)) {
const parent: Doc | undefined = await client.findOne(currentDoc.attachedToClass, { _id: currentDoc.attachedTo })
if (parent) {
currentDoc = parent
parents.push(parent)
} else {
currentDoc = undefined
}
}
return parents.reverse()
}
async function getBreadcrumbsModels (doc: typeof element): Promise<readonly BreadcrumbsModel[]> {
if (!isAttachedDoc(doc)) {
return []
}
const parents = await getParents(doc)
if (parents.length === 0) {
return []
}
const models: BreadcrumbsModel[] = []
for (const parent of parents) {
const attributeModel: AttributeModel | undefined = await getObjectPresenter(client, parent._class, { key: '' })
if (attributeModel) {
const breadcrumbsModel: BreadcrumbsModel = {
component: attributeModel.presenter,
props: { inline: true, ...(attributeModel.props ?? {}), value: parent }
}
models.push(breadcrumbsModel)
}
}
return models
}
</script>
{#await getBreadcrumbsModels(element) then models}
{#if models.length > 0}
<Breadcrumbs {models} gap="none" />
{/if}
{/await}

View File

@ -123,6 +123,7 @@ export { default as StatusRefPresenter } from './components/status/StatusRefPres
export { default as TableBrowser } from './components/TableBrowser.svelte'
export { default as ValueSelector } from './components/ValueSelector.svelte'
export { default as FilterRemovedNotification } from './components/filter/FilterRemovedNotification.svelte'
export { default as ParentsNavigator } from './components/ParentsNavigator.svelte'
export * from './context'
export * from './filter'
export * from './selection'

View File

@ -949,3 +949,7 @@ export async function statusSort (
)
})
}
export function isAttachedDoc (doc: Doc | AttachedDoc): doc is AttachedDoc {
return 'attachedTo' in doc
}

View File

@ -25,6 +25,6 @@ test.describe('recruit tests', () => {
await page.click('[id="contact:string:AddMember"]')
await page.click('button:has-text("Chen Rosamund")')
await page.click('text=Chen Rosamund less than a minute ago >> span')
await page.click(`:nth-match(:text("${orgId}"), 2)`)
await page.click(`.card a:has-text("${orgId}")`)
})
})