UBER-413: Allow extensible navigator model (#3477)

Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
Andrey Sobolev 2023-07-04 08:50:24 +07:00 committed by GitHub
parent 03b9be571b
commit 2869a8e189
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 214 additions and 44 deletions

View File

@ -149,6 +149,7 @@ export function createModel (builder: Builder): void {
navigatorModel: { navigatorModel: {
spaces: [ spaces: [
{ {
id: 'boards',
label: board.string.MyBoards, label: board.string.MyBoards,
spaceClass: board.class.Board, spaceClass: board.class.Board,
addSpaceLabel: board.string.BoardCreateLabel, addSpaceLabel: board.string.BoardCreateLabel,

View File

@ -421,12 +421,14 @@ export function createModel (builder: Builder, options = { addApplication: true
], ],
spaces: [ spaces: [
{ {
id: 'channels',
label: chunter.string.Channels, label: chunter.string.Channels,
spaceClass: chunter.class.Channel, spaceClass: chunter.class.Channel,
addSpaceLabel: chunter.string.CreateChannel, addSpaceLabel: chunter.string.CreateChannel,
createComponent: chunter.component.CreateChannel createComponent: chunter.component.CreateChannel
}, },
{ {
id: 'directMessages',
label: chunter.string.DirectMessages, label: chunter.string.DirectMessages,
spaceClass: chunter.class.DirectMessage, spaceClass: chunter.class.DirectMessage,
addSpaceLabel: chunter.string.NewDirectMessage, addSpaceLabel: chunter.string.NewDirectMessage,

View File

@ -163,6 +163,7 @@ export function createModel (builder: Builder): void {
], ],
spaces: [ spaces: [
{ {
id: 'funnels',
label: lead.string.Funnels, label: lead.string.Funnels,
spaceClass: lead.class.Funnel, spaceClass: lead.class.Funnel,
addSpaceLabel: lead.string.CreateFunnel, addSpaceLabel: lead.string.CreateFunnel,

View File

@ -543,7 +543,7 @@ export function createModel (builder: Builder): void {
{ {
key: 'assignee', key: 'assignee',
presenter: tracker.component.AssigneeEditor, presenter: tracker.component.AssigneeEditor,
displayProps: { key: 'assigee', fixed: 'right' }, displayProps: { key: 'assignee', fixed: 'right' },
props: { kind: 'list', shouldShowName: false, avatarSize: 'x-small' } props: { kind: 'list', shouldShowName: false, avatarSize: 'x-small' }
} }
], ],
@ -1084,6 +1084,7 @@ export function createModel (builder: Builder): void {
], ],
spaces: [ spaces: [
{ {
id: 'projects',
label: tracker.string.Projects, label: tracker.string.Projects,
spaceClass: tracker.class.Project, spaceClass: tracker.class.Project,
addSpaceLabel: tracker.string.CreateProject, addSpaceLabel: tracker.string.CreateProject,

View File

@ -19,7 +19,13 @@ import preference, { TPreference } from '@hcengineering/model-preference'
import { createAction } from '@hcengineering/model-view' import { createAction } from '@hcengineering/model-view'
import type { Asset, IntlString } from '@hcengineering/platform' import type { Asset, IntlString } from '@hcengineering/platform'
import view, { KeyBinding } from '@hcengineering/view' import view, { KeyBinding } from '@hcengineering/view'
import type { Application, HiddenApplication, SpaceView, ViewConfiguration } from '@hcengineering/workbench' import type {
Application,
ApplicationNavModel,
HiddenApplication,
SpaceView,
ViewConfiguration
} from '@hcengineering/workbench'
import core, { TClass, TDoc } from '@hcengineering/model-core' import core, { TClass, TDoc } from '@hcengineering/model-core'
import workbench from './plugin' import workbench from './plugin'
@ -37,6 +43,12 @@ export class TApplication extends TDoc implements Application {
hidden!: boolean hidden!: boolean
} }
@Model(workbench.class.ApplicationNavModel, core.class.Doc, DOMAIN_MODEL)
@UX(workbench.string.Application)
export class TApplicationNavModel extends TDoc implements ApplicationNavModel {
extends!: Ref<Application>
}
@Model(workbench.class.HiddenApplication, preference.class.Preference) @Model(workbench.class.HiddenApplication, preference.class.Preference)
export class THiddenApplication extends TPreference implements HiddenApplication { export class THiddenApplication extends TPreference implements HiddenApplication {
@Prop(TypeRef(workbench.class.Application), workbench.string.HiddenApplication) @Prop(TypeRef(workbench.class.Application), workbench.string.HiddenApplication)
@ -49,7 +61,7 @@ export class TSpaceView extends TClass implements SpaceView {
} }
export function createModel (builder: Builder): void { export function createModel (builder: Builder): void {
builder.createModel(TApplication, TSpaceView, THiddenApplication) builder.createModel(TApplication, TSpaceView, THiddenApplication, TApplicationNavModel)
builder.mixin(workbench.class.Application, core.class.Class, view.mixin.ObjectPresenter, { builder.mixin(workbench.class.Application, core.class.Class, view.mixin.ObjectPresenter, {
presenter: workbench.component.ApplicationPresenter presenter: workbench.component.ApplicationPresenter
}) })

View File

@ -1,3 +1,4 @@
import { deepEqual } from 'fast-equals'
import { DocumentUpdate, Hierarchy, MixinData, MixinUpdate, ModelDb, toFindResult } from '.' import { DocumentUpdate, Hierarchy, MixinData, MixinUpdate, ModelDb, toFindResult } from '.'
import type { import type {
Account, Account,
@ -15,7 +16,7 @@ import type {
import { Client } from './client' import { Client } from './client'
import core from './component' import core from './component'
import type { DocumentQuery, FindOptions, FindResult, TxResult, WithLookup } from './storage' import type { DocumentQuery, FindOptions, FindResult, TxResult, WithLookup } from './storage'
import { DocumentClassQuery, Tx, TxCUD, TxFactory } from './tx' import { DocumentClassQuery, Tx, TxCUD, TxFactory, TxProcessor } from './tx'
/** /**
* @public * @public
@ -269,6 +270,57 @@ export class TxOperations implements Omit<Client, 'notify'> {
apply (scope: string): ApplyOperations { apply (scope: string): ApplyOperations {
return new ApplyOperations(this, scope) return new ApplyOperations(this, scope)
} }
async diffUpdate (doc: Doc, raw: Doc | Data<Doc>, date: Timestamp): Promise<Doc> {
// We need to update fields if they are different.
const documentUpdate: DocumentUpdate<Doc> = {}
for (const [k, v] of Object.entries(raw)) {
if (['_class', '_id', 'modifiedBy', 'modifiedOn', 'space', 'attachedTo', 'attachedToClass'].includes(k)) {
continue
}
const dv = (doc as any)[k]
if (!deepEqual(dv, v) && v != null) {
;(documentUpdate as any)[k] = v
}
}
if (Object.keys(documentUpdate).length > 0) {
await this.update(doc, documentUpdate, false, date, doc.modifiedBy)
TxProcessor.applyUpdate(doc, documentUpdate)
}
return doc
}
async mixinDiffUpdate (
doc: Doc,
raw: Doc | Data<Doc>,
mixin: Ref<Class<Mixin<Doc>>>,
modifiedBy: Ref<Account>,
modifiedOn: Timestamp
): Promise<Doc> {
// We need to update fields if they are different.
if (!this.getHierarchy().hasMixin(doc, mixin)) {
await this.createMixin(doc._id, doc._class, doc.space, mixin, raw as MixinData<Doc, Doc>, modifiedOn, modifiedBy)
TxProcessor.applyUpdate(this.getHierarchy().as(doc, mixin), raw)
return doc
}
const documentUpdate: MixinUpdate<Doc, Doc> = {}
for (const [k, v] of Object.entries(raw)) {
if (['_class', '_id', 'modifiedBy', 'modifiedOn', 'space', 'attachedTo', 'attachedToClass'].includes(k)) {
continue
}
const dv = (doc as any)[k]
if (!deepEqual(dv, v) && v != null) {
;(documentUpdate as any)[k] = v
}
}
if (Object.keys(documentUpdate).length > 0) {
await this.updateMixin(doc._id, doc._class, doc.space, mixin, documentUpdate, modifiedOn, modifiedBy)
TxProcessor.applyUpdate(this.getHierarchy().as(doc, mixin), documentUpdate)
}
return doc
}
} }
/** /**

View File

@ -14,6 +14,8 @@
import view, { Viewlet } from '@hcengineering/view' import view, { Viewlet } from '@hcengineering/view'
import { import {
FilterBar, FilterBar,
SpaceHeader,
ViewletContentView,
ViewletSettingButton, ViewletSettingButton,
activeViewlet, activeViewlet,
getViewOptions, getViewOptions,
@ -23,8 +25,7 @@
} from '@hcengineering/view-resources' } from '@hcengineering/view-resources'
import { onDestroy } from 'svelte' import { onDestroy } from 'svelte'
import tracker from '../../plugin' import tracker from '../../plugin'
import IssuesContent from './IssuesContent.svelte' import CreateIssue from '../CreateIssue.svelte'
import IssuesHeader from './IssuesHeader.svelte'
export let space: Ref<Space> | undefined = undefined export let space: Ref<Space> | undefined = undefined
export let query: DocumentQuery<Issue> = {} export let query: DocumentQuery<Issue> = {}
@ -88,7 +89,8 @@
$: viewOptions = getViewOptions(viewlet, $viewOptionStore) $: viewOptions = getViewOptions(viewlet, $viewOptionStore)
</script> </script>
<IssuesHeader <SpaceHeader
_class={tracker.class.Issue}
bind:viewlet bind:viewlet
bind:search bind:search
showLabelSelector={$$slots.label_selector} showLabelSelector={$$slots.label_selector}
@ -117,12 +119,21 @@
/> />
{/if} {/if}
</svelte:fragment> </svelte:fragment>
</IssuesHeader> </SpaceHeader>
<FilterBar _class={tracker.class.Issue} query={searchQuery} {viewOptions} on:change={(e) => (resultQuery = e.detail)} /> <FilterBar _class={tracker.class.Issue} query={searchQuery} {viewOptions} on:change={(e) => (resultQuery = e.detail)} />
<slot name="afterHeader" /> <slot name="afterHeader" />
<div class="popupPanel rowContent"> <div class="popupPanel rowContent">
{#if viewlet} {#if viewlet}
<IssuesContent {viewlet} query={resultQuery} {space} {viewOptions} /> <ViewletContentView
_class={tracker.class.Issue}
{viewlet}
query={resultQuery}
{space}
{viewOptions}
createItemDialog={CreateIssue}
createItemLabel={tracker.string.AddIssueTooltip}
createItemDialogProps={{ shouldSaveDraft: true }}
/>
{/if} {/if}
{#if $$slots.aside !== undefined && asideShown} {#if $$slots.aside !== undefined && asideShown}
<div class="popupPanel-body__aside" class:shown={asideShown}> <div class="popupPanel-body__aside" class:shown={asideShown}>

View File

@ -23,9 +23,10 @@
themeStore themeStore
} from '@hcengineering/ui' } from '@hcengineering/ui'
import { NavLink, TreeNode } from '@hcengineering/view-resources' import { NavLink, TreeNode } from '@hcengineering/view-resources'
import { SpacesNavModel } from '@hcengineering/workbench' import { SpacesNavModel, SpecialNavModel } from '@hcengineering/workbench'
import { SpecialElement } from '@hcengineering/workbench-resources' import { SpecialElement } from '@hcengineering/workbench-resources'
import tracker from '../../plugin' import tracker from '../../plugin'
import { getResource } from '@hcengineering/platform'
export let space: Project export let space: Project
export let model: SpacesNavModel export let model: SpacesNavModel
@ -38,9 +39,30 @@
const getSpaceCollapsedKey = () => `${getCurrentLocation().path[1]}_${space._id}_collapsed` const getSpaceCollapsedKey = () => `${getCurrentLocation().path[1]}_${space._id}_collapsed`
$: collapsed = localStorage.getItem(getSpaceCollapsedKey()) === COLLAPSED $: collapsed = localStorage.getItem(getSpaceCollapsedKey()) === COLLAPSED
let specials: SpecialNavModel[] = []
async function updateSpecials (model: SpacesNavModel, space: Project): Promise<void> {
const newSpecials: SpecialNavModel[] = []
for (const sp of model.specials ?? []) {
let shouldAdd = true
if (sp.visibleIf !== undefined) {
const visibleIf = await getResource(sp.visibleIf)
if (visibleIf !== undefined) {
shouldAdd = await visibleIf([space])
}
}
if (shouldAdd) {
newSpecials.push(sp)
}
}
specials = newSpecials
}
$: updateSpecials(model, space)
</script> </script>
{#if model.specials} {#if specials}
<TreeNode <TreeNode
{collapsed} {collapsed}
icon={space?.icon === tracker.component.IconWithEmoji ? IconWithEmoji : space?.icon ?? model.icon} icon={space?.icon === tracker.component.IconWithEmoji ? IconWithEmoji : space?.icon ?? model.icon}
@ -56,7 +78,7 @@
actions={() => getActions(space)} actions={() => getActions(space)}
on:click={() => localStorage.setItem(getSpaceCollapsedKey(), collapsed ? '' : COLLAPSED)} on:click={() => localStorage.setItem(getSpaceCollapsedKey(), collapsed ? '' : COLLAPSED)}
> >
{#each model.specials as special} {#each specials as special}
<NavLink space={space._id} special={special.id}> <NavLink space={space._id} special={special.id}>
<SpecialElement <SpecialElement
indent={'ml-2'} indent={'ml-2'}

View File

@ -15,6 +15,7 @@
import view, { Viewlet } from '@hcengineering/view' import view, { Viewlet } from '@hcengineering/view'
import { import {
FilterBar, FilterBar,
SpaceHeader,
ViewletSettingButton, ViewletSettingButton,
activeViewlet, activeViewlet,
getViewOptions, getViewOptions,
@ -24,7 +25,6 @@
} from '@hcengineering/view-resources' } from '@hcengineering/view-resources'
import { onDestroy } from 'svelte' import { onDestroy } from 'svelte'
import tracker from '../../plugin' import tracker from '../../plugin'
import IssuesHeader from '../issues/IssuesHeader.svelte'
import CreateIssueTemplate from './CreateIssueTemplate.svelte' import CreateIssueTemplate from './CreateIssueTemplate.svelte'
import IssueTemplatesContent from './IssueTemplatesContent.svelte' import IssueTemplatesContent from './IssueTemplatesContent.svelte'
@ -91,7 +91,15 @@
$: viewOptions = getViewOptions(viewlet, $viewOptionStore) $: viewOptions = getViewOptions(viewlet, $viewOptionStore)
</script> </script>
<IssuesHeader {space} {viewlets} {label} bind:viewlet bind:search showLabelSelector={$$slots.label_selector}> <SpaceHeader
_class={tracker.class.IssueTemplate}
{space}
{viewlets}
{label}
bind:viewlet
bind:search
showLabelSelector={$$slots.label_selector}
>
<svelte:fragment slot="label_selector"> <svelte:fragment slot="label_selector">
<slot name="label_selector" /> <slot name="label_selector" />
</svelte:fragment> </svelte:fragment>
@ -115,7 +123,7 @@
<ViewletSettingButton bind:viewOptions {viewlet} /> <ViewletSettingButton bind:viewOptions {viewlet} />
{/if} {/if}
</svelte:fragment> </svelte:fragment>
</IssuesHeader> </SpaceHeader>
<slot name="afterHeader" /> <slot name="afterHeader" />
<FilterBar <FilterBar
_class={tracker.class.IssueTemplate} _class={tracker.class.IssueTemplate}

View File

@ -1,12 +1,13 @@
<script lang="ts"> <script lang="ts">
import { Ref, Space } from '@hcengineering/core' import { Class, Doc, Ref, Space } from '@hcengineering/core'
import { TabList, SearchEdit, IModeSelector, ModeSelector } from '@hcengineering/ui' import { TabList, SearchEdit, IModeSelector, ModeSelector } from '@hcengineering/ui'
import { Viewlet } from '@hcengineering/view' import { Viewlet } from '@hcengineering/view'
import { FilterButton, setActiveViewletId } from '@hcengineering/view-resources'
import tracker from '../../plugin'
import { WithLookup } from '@hcengineering/core' import { WithLookup } from '@hcengineering/core'
import { setActiveViewletId } from '../utils'
import FilterButton from './filter/FilterButton.svelte'
export let space: Ref<Space> | undefined = undefined export let space: Ref<Space> | undefined = undefined
export let _class: Ref<Class<Doc>>
export let viewlet: WithLookup<Viewlet> | undefined export let viewlet: WithLookup<Viewlet> | undefined
export let viewlets: WithLookup<Viewlet>[] = [] export let viewlets: WithLookup<Viewlet>[] = []
export let label: string export let label: string
@ -65,7 +66,7 @@
<SearchEdit bind:value={search} on:change={() => {}} /> <SearchEdit bind:value={search} on:change={() => {}} />
<!-- <ActionIcon icon={IconMoreH} size={'small'} /> --> <!-- <ActionIcon icon={IconMoreH} size={'small'} /> -->
<div class="buttons-divider" /> <div class="buttons-divider" />
<FilterButton _class={tracker.class.Issue} {space} /> <FilterButton {_class} {space} />
</div> </div>
<div class="ac-header-full medium-gap"> <div class="ac-header-full medium-gap">
<slot name="extra" /> <slot name="extra" />

View File

@ -1,18 +1,21 @@
<script lang="ts"> <script lang="ts">
import { DocumentQuery, Ref, Space, WithLookup } from '@hcengineering/core' import { Class, Doc, DocumentQuery, Ref, Space, WithLookup } from '@hcengineering/core'
import { Issue } from '@hcengineering/tracker' import { AnySvelteComponent, Component, Loading } from '@hcengineering/ui'
import { Component, Loading } from '@hcengineering/ui'
import view, { Viewlet, ViewletPreference, ViewOptions } from '@hcengineering/view' import view, { Viewlet, ViewletPreference, ViewOptions } from '@hcengineering/view'
import tracker from '../../plugin'
import CreateIssue from '../CreateIssue.svelte'
import { createQuery } from '@hcengineering/presentation' import { createQuery } from '@hcengineering/presentation'
import { IntlString } from '@hcengineering/platform'
export let viewlet: WithLookup<Viewlet> export let viewlet: WithLookup<Viewlet>
export let query: DocumentQuery<Issue> = {} export let _class: Ref<Class<Doc>>
export let query: DocumentQuery<Doc> = {}
export let space: Ref<Space> | undefined export let space: Ref<Space> | undefined
export let viewOptions: ViewOptions export let viewOptions: ViewOptions
export let createItemDialog: AnySvelteComponent | undefined = undefined
export let createItemLabel: IntlString | undefined = undefined
export let createItemDialogProps = { shouldSaveDraft: true }
const preferenceQuery = createQuery() const preferenceQuery = createQuery()
let preference: ViewletPreference | undefined let preference: ViewletPreference | undefined
let loading = true let loading = true
@ -29,10 +32,6 @@
}, },
{ limit: 1 } { limit: 1 }
) )
const createItemDialog = CreateIssue
const createItemLabel = tracker.string.AddIssueTooltip
const createItemDialogProps = { shouldSaveDraft: true }
</script> </script>
{#if viewlet?.$lookup?.descriptor?.component} {#if viewlet?.$lookup?.descriptor?.component}
@ -42,7 +41,7 @@
<Component <Component
is={viewlet.$lookup.descriptor.component} is={viewlet.$lookup.descriptor.component}
props={{ props={{
_class: tracker.class.Issue, _class,
config: preference?.config ?? viewlet.config, config: preference?.config ?? viewlet.config,
options: viewlet.options, options: viewlet.options,
createItemDialog, createItemDialog,

View File

@ -77,6 +77,8 @@ import ValueSelector from './components/ValueSelector.svelte'
import ViewletSettingButton from './components/ViewletSettingButton.svelte' import ViewletSettingButton from './components/ViewletSettingButton.svelte'
import DateFilterPresenter from './components/filter/DateFilterPresenter.svelte' import DateFilterPresenter from './components/filter/DateFilterPresenter.svelte'
import ArrayFilter from './components/filter/ArrayFilter.svelte' import ArrayFilter from './components/filter/ArrayFilter.svelte'
import SpaceHeader from './components/SpaceHeader.svelte'
import ViewletContentView from './components/ViewletContentView.svelte'
import { import {
afterResult, afterResult,
@ -175,7 +177,9 @@ export {
DocNavLink, DocNavLink,
EnumEditor, EnumEditor,
StringPresenter, StringPresenter,
EditBoxPopup EditBoxPopup,
SpaceHeader,
ViewletContentView
} }
function PositionElementAlignment (e?: Event): PopupAlignment | undefined { function PositionElementAlignment (e?: Event): PopupAlignment | undefined {

View File

@ -63,7 +63,7 @@
import { getContext, onDestroy, onMount, tick } from 'svelte' import { getContext, onDestroy, onMount, tick } from 'svelte'
import { subscribeMobile } from '../mobile' import { subscribeMobile } from '../mobile'
import workbench from '../plugin' import workbench from '../plugin'
import { workspacesStore } from '../utils' import { buildNavModel, workspacesStore } from '../utils'
import AccountPopup from './AccountPopup.svelte' import AccountPopup from './AccountPopup.svelte'
import AppItem from './AppItem.svelte' import AppItem from './AppItem.svelte'
import AppSwitcher from './AppSwitcher.svelte' import AppSwitcher from './AppSwitcher.svelte'
@ -326,7 +326,7 @@
clear(1) clear(1)
currentAppAlias = app currentAppAlias = app
currentApplication = await client.findOne(workbench.class.Application, { alias: app }) currentApplication = await client.findOne(workbench.class.Application, { alias: app })
navigatorModel = currentApplication?.navigatorModel navigatorModel = await buildNavModel(client, currentApplication)
} }
if ( if (

View File

@ -17,11 +17,12 @@
import core from '@hcengineering/core' import core from '@hcengineering/core'
import { DocUpdates } from '@hcengineering/notification' import { DocUpdates } from '@hcengineering/notification'
import { NotificationClientImpl } from '@hcengineering/notification-resources' import { NotificationClientImpl } from '@hcengineering/notification-resources'
import { getResource } from '@hcengineering/platform' import { IntlString, getResource } from '@hcengineering/platform'
import preference from '@hcengineering/preference' import preference from '@hcengineering/preference'
import { getClient } from '@hcengineering/presentation' import { getClient } from '@hcengineering/presentation'
import { import {
Action, Action,
AnyComponent,
AnySvelteComponent, AnySvelteComponent,
IconAdd, IconAdd,
IconEdit, IconEdit,
@ -49,14 +50,14 @@
const client = getClient() const client = getClient()
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
const addSpace: Action = { const addSpace = (addSpaceLabel: IntlString, createComponent: AnyComponent): Action => ({
label: model.addSpaceLabel, label: addSpaceLabel,
icon: IconAdd, icon: IconAdd,
action: async (_id: Ref<Doc>): Promise<void> => { action: async (_id: Ref<Doc>): Promise<void> => {
dispatch('open') dispatch('open')
showPopup(model.createComponent, {}, 'top') showPopup(createComponent, {}, 'top')
}
} }
})
const browseSpaces: Action = { const browseSpaces: Action = {
label: plugin.string.BrowseSpaces, label: plugin.string.BrowseSpaces,
@ -108,7 +109,11 @@
} }
function getParentActions (): Action[] { function getParentActions (): Action[] {
return hasSpaceBrowser ? [browseSpaces, addSpace] : [addSpace] const result = hasSpaceBrowser ? [browseSpaces] : []
if (model.addSpaceLabel !== undefined && model.createComponent !== undefined) {
result.push(addSpace(model.addSpaceLabel, model.createComponent))
}
return result
} }
async function getPresenter (_class: Ref<Class<Doc>>): Promise<AnySvelteComponent | undefined> { async function getPresenter (_class: Ref<Class<Doc>>): Promise<AnySvelteComponent | undefined> {

View File

@ -14,7 +14,7 @@
// limitations under the License. // limitations under the License.
// //
import type { Class, Client, Doc, Obj, Ref, Space } from '@hcengineering/core' import type { Class, Client, Doc, Obj, Ref, Space, TxOperations } from '@hcengineering/core'
import core from '@hcengineering/core' import core from '@hcengineering/core'
import type { Workspace } from '@hcengineering/login' import type { Workspace } from '@hcengineering/login'
import type { Asset } from '@hcengineering/platform' import type { Asset } from '@hcengineering/platform'
@ -146,3 +146,38 @@ export async function showApplication (app: Application): Promise<void> {
} }
export const workspacesStore = writable<Workspace[]>([]) export const workspacesStore = writable<Workspace[]>([])
/**
* @public
*/
export async function buildNavModel (
client: TxOperations,
currentApplication?: Application
): Promise<NavigatorModel | undefined> {
let newNavModel = currentApplication?.navigatorModel
if (currentApplication !== undefined) {
const models = await client.findAll(workbench.class.ApplicationNavModel, { extends: currentApplication._id })
for (const nm of models) {
const spaces = newNavModel?.spaces ?? []
// Check for extending
for (const sp of spaces) {
const extend = (nm.spaces ?? []).find((p) => p.id === sp.id)
if (extend !== undefined) {
sp.label = sp.label ?? extend.label
sp.createComponent = sp.createComponent ?? extend.createComponent
sp.addSpaceLabel = sp.addSpaceLabel ?? extend.addSpaceLabel
sp.icon = sp.icon ?? extend.icon
sp.visibleIf = sp.visibleIf ?? extend.visibleIf
sp.specials = [...(sp.specials ?? []), ...(extend.specials ?? [])]
}
}
const newSpaces = (nm.spaces ?? []).filter((it) => !spaces.some((sp) => sp.id === it.id))
newNavModel = {
spaces: [...spaces, ...newSpaces],
specials: [...(newNavModel?.specials ?? []), ...(nm.specials ?? [])],
aside: newNavModel?.aside ?? nm?.aside
}
}
}
return newNavModel
}

View File

@ -16,9 +16,9 @@
import type { Class, Doc, Mixin, Obj, Ref, Space } from '@hcengineering/core' import type { Class, Doc, Mixin, Obj, Ref, Space } from '@hcengineering/core'
import type { Asset, IntlString, Metadata, Plugin, Resource } from '@hcengineering/platform' import type { Asset, IntlString, Metadata, Plugin, Resource } from '@hcengineering/platform'
import { plugin } from '@hcengineering/platform' import { plugin } from '@hcengineering/platform'
import type { Preference } from '@hcengineering/preference'
import { AnyComponent, Location, ResolvedLocation } from '@hcengineering/ui' import { AnyComponent, Location, ResolvedLocation } from '@hcengineering/ui'
import { ViewAction } from '@hcengineering/view' import { ViewAction } from '@hcengineering/view'
import type { Preference } from '@hcengineering/preference'
/** /**
* @public * @public
@ -28,7 +28,10 @@ export interface Application extends Doc {
alias: string alias: string
icon: Asset icon: Asset
hidden: boolean hidden: boolean
// Also attached ApplicationNavModel will be joined after this one main.
navigatorModel?: NavigatorModel navigatorModel?: NavigatorModel
locationResolver?: Resource<(loc: Location) => Promise<ResolvedLocation | undefined>> locationResolver?: Resource<(loc: Location) => Promise<ResolvedLocation | undefined>>
// Component will be displayed in case navigator model is not defined, or nothing is selected in navigator model // Component will be displayed in case navigator model is not defined, or nothing is selected in navigator model
@ -40,6 +43,17 @@ export interface Application extends Doc {
navFooterComponent?: AnyComponent navFooterComponent?: AnyComponent
} }
/**
* @public
*/
export interface ApplicationNavModel extends Doc {
extends: Ref<Application>
spaces?: SpacesNavModel[]
specials?: SpecialNavModel[]
aside?: AnyComponent
}
/** /**
* @public * @public
*/ */
@ -51,10 +65,11 @@ export interface HiddenApplication extends Preference {
* @public * @public
*/ */
export interface SpacesNavModel { export interface SpacesNavModel {
label: IntlString id: string // Id could be used for extending of navigation model
label?: IntlString
spaceClass: Ref<Class<Space>> spaceClass: Ref<Class<Space>>
addSpaceLabel: IntlString addSpaceLabel?: IntlString
createComponent: AnyComponent createComponent?: AnyComponent
icon?: Asset icon?: Asset
// Child special items. // Child special items.
@ -118,6 +133,7 @@ export const workbenchId = 'workbench' as Plugin
export default plugin(workbenchId, { export default plugin(workbenchId, {
class: { class: {
Application: '' as Ref<Class<Application>>, Application: '' as Ref<Class<Application>>,
ApplicationNavModel: '' as Ref<Class<ApplicationNavModel>>,
HiddenApplication: '' as Ref<Class<HiddenApplication>> HiddenApplication: '' as Ref<Class<HiddenApplication>>
}, },
mixin: { mixin: {