Add limit for types in navigator and add list view for types (#8489)

Signed-off-by: Kristina Fefelova <kristin.fefelova@gmail.com>
This commit is contained in:
Kristina
2025-04-08 09:42:04 +07:00
committed by GitHub
parent 5571dc0ee1
commit feaf4cb1cc
27 changed files with 397 additions and 136 deletions
+3 -3
View File
@@ -161,7 +161,7 @@ export function createSystemType (
attachTo: type,
descriptor: view.viewlet.List,
viewOptions: {
groupBy: ['_class', 'createdBy', 'modifiedBy'],
groupBy: ['_class', 'createdBy', 'modifiedBy', 'parent'],
orderBy: [
['modifiedOn', SortingOrder.Descending],
['rank', SortingOrder.Ascending]
@@ -281,7 +281,7 @@ export function createModel (builder: Builder): void {
attachTo: card.class.Card,
descriptor: view.viewlet.List,
viewOptions: {
groupBy: ['_class', 'createdBy', 'modifiedBy'],
groupBy: ['_class', 'createdBy', 'modifiedBy', 'parent'],
orderBy: [
['modifiedOn', SortingOrder.Descending],
['rank', SortingOrder.Ascending]
@@ -304,7 +304,7 @@ export function createModel (builder: Builder): void {
descriptor: view.viewlet.List,
variant: 'child',
viewOptions: {
groupBy: ['_class', 'createdBy', 'modifiedBy'],
groupBy: ['_class', 'createdBy', 'modifiedBy', 'parent'],
orderBy: [
['modifiedOn', SortingOrder.Descending],
['rank', SortingOrder.Ascending]
-1
View File
@@ -72,7 +72,6 @@ export default mergeIds(viewId, view, {
DatePresenter: '' as AnyComponent,
DateTimePresenter: '' as AnyComponent,
TableBrowser: '' as AnyComponent,
RolePresenter: '' as AnyComponent,
YoutubePresenter: '' as AnyComponent,
GithubPresenter: '' as AnyComponent,
ClassPresenter: '' as AnyComponent,
+1
View File
@@ -1,6 +1,7 @@
{
"string": {
"Added": "Přidáno",
"All": "Vše ({count})",
"Attach": "Připojit",
"Cancel": "Zrušit",
"Edit": "Upravit",
+1
View File
@@ -1,6 +1,7 @@
{
"string": {
"Added": "Hinzugefügt",
"All": "Alle ({count})",
"Attach": "Anhängen",
"Cancel": "Abbrechen",
"Edit": "Bearbeiten",
+1
View File
@@ -1,6 +1,7 @@
{
"string": {
"Added": "Added",
"All": "All ({count})",
"Attach": "Attach",
"Cancel": "Cancel",
"Edit": "Edit",
+1
View File
@@ -1,6 +1,7 @@
{
"string": {
"Added": "Añadido",
"All": "Todo ({count})",
"Attach": "Adjuntar",
"Cancel": "Cancelar",
"Edit": "Editar",
+1
View File
@@ -1,6 +1,7 @@
{
"string": {
"Added": "Ajouté",
"All": "Tout ({count})",
"Attach": "Joindre",
"Cancel": "Annuler",
"Edit": "Modifier",
+1
View File
@@ -1,6 +1,7 @@
{
"string": {
"Added": "Aggiunto" ,
"All": "Tutto ({count})",
"Attach": "Allega",
"Cancel": "Annulla",
"Edit": "Modifica",
+1
View File
@@ -1,6 +1,7 @@
{
"string": {
"Added": "Adicionado",
"All": "Todos ({count})",
"Attach": "Anexar",
"Cancel": "Cancelar",
"Edit": "Editar",
+1
View File
@@ -1,6 +1,7 @@
{
"string": {
"Added": "Добавлено",
"All": "Всего ({count})",
"Attach": "Прикрепить",
"Cancel": "Отмена",
"Edit": "Редактировать",
+1
View File
@@ -1,6 +1,7 @@
{
"string": {
"Added": "已添加",
"All": "全部 ({count})",
"Attach": "附加",
"Cancel": "取消",
"Edit": "编辑",
@@ -14,31 +14,53 @@
-->
<script lang="ts">
import { NavigationSection } from '../types'
import { Section } from '../index'
import NavItem from './NavItem.svelte'
import { createEventDispatcher } from 'svelte'
import { ButtonVariant, NavigationSection } from '../types'
import { Section } from '../index'
import NavItem from './NavItem.svelte'
import uiNext from '../plugin'
import Button from './Button.svelte'
export let sections: NavigationSection[] = []
export let selected: string | undefined = undefined
export let selectedItem: string | undefined = undefined
export let selectedSection: string | undefined = undefined
const dispatch = createEventDispatcher()
</script>
<div class="navigation-list">
{#each sections as section (section.id)}
<Section id={section.id} title={section.title} expanded={section.expanded} on:toggle>
<Section
id={section.id}
title={section.title}
selected={selectedSection === section.id}
expanded={section.expanded}
on:toggle
on:click={() => {
dispatch('select', section.id)
}}
>
{#each section.items as item (item.id)}
<NavItem
label={item.label}
icon={item.icon}
selected={selected === item.id}
selected={selectedItem === item.id}
notificationsCount={item.notificationsCount}
on:click={() => {
dispatch('click', item.id)
dispatch('selectItem', item.id)
}}
/>
{/each}
{#if section.total > section.items.length}
<Button
labelIntl={uiNext.string.All}
labelParams={{ count: section.total }}
variant={ButtonVariant.Ghost}
on:click={() => dispatch('all', section.id)}
/>
{/if}
</Section>
{/each}
</div>
+18 -7
View File
@@ -26,12 +26,15 @@
export let title: IntlString
export let withHeaderDivider = false
export let expanded = true
export let selected = false
const dispatch = createEventDispatcher()
let _expanded = expanded
function toggleExpanded (): void {
function toggleExpanded (event: MouseEvent): void {
event.stopPropagation()
event.preventDefault()
_expanded = !_expanded
dispatch('toggle', id)
}
@@ -40,17 +43,19 @@
<div class="section">
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div class="section__header" on:click={toggleExpanded}>
<div class="section__title next-label-overflow">
<Label label={title} />
</div>
<div class="section__arrow">
<div class="section__header" class:selected on:click>
<div class="section__arrow" on:click={toggleExpanded}>
{#if _expanded}
<IconArrowChevronDown />
{:else}
<IconArrowChevronRight />
{/if}
</div>
<div class="section__title next-label-overflow">
<Label label={title} />
</div>
{#if withHeaderDivider}
<Divider />
{/if}
@@ -75,11 +80,17 @@
.section__header {
display: flex;
width: 100%;
height: 1.5rem;
min-height: 1.5rem;
align-items: center;
gap: 0.375rem;
flex-shrink: 0;
cursor: pointer;
border-radius: 0.5rem;
padding: 0.5rem;
&.selected {
background: var(--next-surface-color-fg-inv);
}
}
.section__title {
+1
View File
@@ -20,6 +20,7 @@ export const uiNextId = 'ui-next' as Plugin
export const uiNext = plugin(uiNextId, {
string: {
Added: '' as IntlString,
All: '' as IntlString,
Attach: '' as IntlString,
Cancel: '' as IntlString,
Edit: '' as IntlString,
+1
View File
@@ -22,6 +22,7 @@ export interface NavigationSection {
id: string
title: IntlString
expanded: boolean
total: number
items: NavigationSectionItem[]
}
@@ -14,7 +14,7 @@
-->
<script lang="ts">
import cardPlugin, { Card } from '@hcengineering/card'
import cardPlugin, { Card, MasterTag } from '@hcengineering/card'
import {
defineSeparators,
Separator,
@@ -24,33 +24,32 @@
restoreLocation
} from '@hcengineering/ui'
import { onDestroy } from 'svelte'
import { createNotificationContextsQuery, createQuery, getClient } from '@hcengineering/presentation'
import { createNotificationContextsQuery, getClient } from '@hcengineering/presentation'
import { chatId } from '@hcengineering/chat'
import { SortingOrder } from '@hcengineering/core'
import { Ref, SortingOrder } from '@hcengineering/core'
import { NotificationContext } from '@hcengineering/communication-types'
import ChatPanel from './ChatPanel.svelte'
import ChatNavigation from './ChatNavigation.svelte'
import { navigateToCard, getCardIdFromLocation } from '../location'
import { navigateToCard, getCardIdFromLocation, navigateToType, getTypeIdFromLocation } from '../location'
import ChatNavigationCategoryList from './ChatNavigationCategoryList.svelte'
type Selection = { type: 'card', card: Card } | { type: 'type', ref: Ref<MasterTag> }
const client = getClient()
const query = createQuery()
const notificationContextsQuery = createNotificationContextsQuery()
let replacedPanelElement: HTMLElement
let card: Card | undefined = undefined
let selection: Selection | undefined = undefined
let needRestoreLoc = true
let cards: Card[] = []
let contexts: NotificationContext[] = []
// TODO: only subscribed cards
query.query(cardPlugin.class.Card, {}, (res) => {
cards = res
})
$: selectedCard = getSelectedCard(selection)
$: selectedType = getSelectedType(selection)
// TODO: only for subscribed cards
// TODO: only for subscribed/loaded cards
notificationContextsQuery.query(
{
notifications: {
@@ -69,10 +68,17 @@
return
}
const typeId = getTypeIdFromLocation(loc)
if (typeId != null && typeId !== '') {
selection = { type: 'type', ref: typeId }
return
}
const cardId = getCardIdFromLocation(loc)
if (cardId == null || cardId === '') {
card = undefined
selection = undefined
if (needRestoreLoc) {
needRestoreLoc = false
restoreLocation(loc, chatId)
@@ -82,16 +88,36 @@
needRestoreLoc = false
if (cardId !== card?._id) {
card = await client.findOne(cardPlugin.class.Card, { _id: cardId })
if (cardId !== selectedCard?._id) {
const card = await client.findOne(cardPlugin.class.Card, { _id: cardId })
selection = card != null ? { type: 'card', card } : undefined
}
}
function selectCard (event: CustomEvent<Card>): void {
card = event.detail
const card = event.detail
selection = { type: 'card', card }
navigateToCard(card._id)
}
function selectType (event: CustomEvent<Ref<MasterTag>>): void {
const type = event.detail
selection = { type: 'type', ref: type }
navigateToType(type)
}
function getSelectedCard (selection: Selection | undefined): Card | undefined {
if (selection == null) return undefined
if (selection.type !== 'card') return undefined
return selection.card
}
function getSelectedType (selection: Selection | undefined): Ref<MasterTag> | undefined {
if (selection == null) return undefined
if (selection.type !== 'type') return undefined
return selection.ref
}
onDestroy(
resolvedLocationStore.subscribe((loc) => {
void syncLocation(loc)
@@ -116,7 +142,13 @@
class:fly={$deviceInfo.navigator.float}
>
<div class="antiPanel-wrap__content hulyNavPanel-container">
<ChatNavigation {card} {cards} {contexts} on:select={selectCard} />
<ChatNavigation
card={getSelectedCard(selection)}
type={getSelectedType(selection)}
{contexts}
on:selectCard={selectCard}
on:selectType={selectType}
/>
</div>
{#if !($deviceInfo.isMobile && $deviceInfo.isPortrait && $deviceInfo.minWidth)}
<Separator name="new-chat" float={$deviceInfo.navigator.float ? 'navigator' : true} index={0} />
@@ -132,11 +164,11 @@
/>
{/if}
<div bind:this={replacedPanelElement} class="hulyComponent chat__panel">
{#if card}
{@const context = contexts.find((c) => c.card === card?._id)}
{#key card._id}
<ChatPanel {card} {context} />
{/key}
{#if selectedCard}
{@const context = contexts.find((c) => c.card === selectedCard?._id)}
<ChatPanel card={selectedCard} {context} />
{:else if selectedType}
<ChatNavigationCategoryList type={selectedType} />
{/if}
</div>
</div>
@@ -14,26 +14,71 @@
-->
<script lang="ts">
import { Card } from '@hcengineering/card'
import cardPlugin, { Card, MasterTag } from '@hcengineering/card'
import { createEventDispatcher } from 'svelte'
import { NavigationList, NavigationSection } from '@hcengineering/ui-next'
import { languageStore, Scroller } from '@hcengineering/ui'
import { Ref } from '@hcengineering/core'
import { Ref, SortingOrder, Class } from '@hcengineering/core'
import { NotificationContext } from '@hcengineering/communication-types'
import { SavedView } from '@hcengineering/workbench-resources'
import { chatId } from '@hcengineering/chat'
import { createQuery, LiveQuery } from '@hcengineering/presentation'
import { cardsToChatSections, NavigatorState, navigatorStateStore, toggleSection } from '../navigator'
export let card: Card | undefined = undefined
export let cards: Card[] = []
export let type: Ref<MasterTag> | undefined = undefined
export let contexts: NotificationContext[] = []
const cardsLimit = 8
const dispatch = createEventDispatcher()
const cardsQueryByType = new Map<Ref<MasterTag>, { query: LiveQuery, limit: number }>()
const typesQuery = createQuery()
let cardsByType = new Map<Ref<MasterTag>, { cards: Card[], total: number }>()
let types: MasterTag[] = []
let sections: NavigationSection[] = []
$: void updateSections(cards, contexts, $languageStore, $navigatorStateStore)
$: typesQuery.query(cardPlugin.class.MasterTag, {}, (result) => {
types = result.filter((p) => p.removed !== true)
})
$: loadObjects(types)
function loadObjects (types: MasterTag[]): void {
if (types.length === 0) return
for (const type of types) {
const { query, limit } = cardsQueryByType.get(type._id) ?? {
query: createQuery(),
limit: cardsLimit
}
cardsQueryByType.set(type._id, { query, limit })
query.query(
type._id as Ref<Class<Card>>,
{},
(res) => {
if (res.length === 0) {
cardsByType.delete(type._id)
} else {
cardsByType.set(type._id, { cards: res, total: res.total })
}
cardsByType = cardsByType
},
{ total: true, limit, sort: { modifiedOn: SortingOrder.Descending } }
)
}
cardsByType = cardsByType
}
$: void updateSections(cardsByType, contexts, $languageStore, $navigatorStateStore)
async function updateSections (
cards: Card[],
cards: Map<Ref<MasterTag>, { cards: Card[], total: number }>,
contexts: NotificationContext[],
_lang: string,
state: NavigatorState
@@ -45,24 +90,43 @@
toggleSection(event.detail)
}
function handleClick (event: CustomEvent<Ref<Card>>): void {
function handleSelectCard (event: CustomEvent<Ref<Card>>): void {
const cardId = event.detail
if (card != null && cardId === card._id) {
return
}
const newCard = cards.find((it) => it._id === cardId)
const newCard = Array.from(cardsByType.values())
.flatMap((it) => it.cards)
.find((it) => it._id === cardId)
if (newCard !== undefined) {
dispatch('select', newCard)
dispatch('selectCard', newCard)
}
}
function handleAll (event: CustomEvent<Ref<MasterTag>>): void {
const query = cardsQueryByType.get(event.detail)
if (query !== undefined) {
query.limit += cardsLimit
loadObjects(types)
}
}
</script>
<Scroller shrink>
<div class="chat-navigation">
<NavigationList {sections} selected={card?._id} on:toggle={handleSectionToggle} on:click={handleClick} />
<SavedView alias={chatId} />
<NavigationList
{sections}
selectedItem={card?._id}
selectedSection={type}
on:toggle={handleSectionToggle}
on:selectItem={handleSelectCard}
on:select={(ev) => dispatch('selectType', ev.detail)}
on:all={handleAll}
/>
</div>
</Scroller>
@@ -72,6 +136,6 @@
flex-direction: column;
width: 100%;
padding: 0 8px;
margin-top: 3rem;
margin-top: 1rem;
}
</style>
@@ -0,0 +1,98 @@
<!--
// Copyright © 2025 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 card, { MasterTag } from '@hcengineering/card'
import { Ref, SortingOrder } from '@hcengineering/core'
import { SpecialView } from '@hcengineering/workbench-resources'
import { getClient } from '@hcengineering/presentation'
import view, { BuildModelKey, type ViewOptions } from '@hcengineering/view'
export let type: Ref<MasterTag>
const client = getClient()
const hierarchy = client.getHierarchy()
$: clazz = hierarchy.hasClass(type) ? hierarchy.getClass(type) : undefined
const defaultOptions: ViewOptions = {
groupBy: ['parent'],
orderBy: ['modifiedBy', SortingOrder.Descending]
}
const defaultConfig: (BuildModelKey | string)[] = [
{
displayProps: {
fixed: 'left',
key: 'createdBy'
},
key: 'createdBy'
},
{
displayProps: {
fixed: 'left',
key: 'card'
},
key: '',
props: {
showParent: false
}
},
{
displayProps: {
grow: true
},
key: ''
},
{
displayProps: {
fixed: 'left',
key: 'tags'
},
key: '',
label: card.string.Tags,
presenter: view.component.RolePresenter,
props: {
fullSize: true
}
},
{
displayProps: {
fixed: 'right',
key: 'parent'
},
key: 'parent'
},
{
displayProps: {
fixed: 'right',
key: 'createdOn'
},
key: 'createdOn'
}
]
</script>
{#if clazz}
<SpecialView
_class={type}
icon={clazz.icon ?? card.icon.Card}
label={clazz.label}
defaultViewletDescriptor={view.viewlet.List}
actionVisible={true}
defaultViewOptions={defaultOptions}
{defaultConfig}
/>
{/if}
@@ -101,22 +101,23 @@
async function handleSubmit (markdown: string, files: UploadedFile[]): Promise<void> {
if (message === undefined || data === undefined) return
const { card } = data
if (card === undefined) return
if (card === undefined || parentCard === undefined) return
let threadId: CardID | undefined = message.thread?.thread
if (threadId == null) {
const markup = jsonToMarkup(markdownToMarkup(message.content))
const title = markupToText(markup).trim()
const messageText = markupToText(markup).trim()
const lastOne = await client.findOne(cardPlugin.class.Card, {}, { sort: { rank: SortingOrder.Descending } })
const titleFromMessage = `${messageText.slice(0, 100)}${messageText.length > 100 ? '...' : ''}`
const title = titleFromMessage.length > 0 ? titleFromMessage : `Thread from ${parentCard.title}`
const data = fillDefaults(
hierarchy,
{
title: `Thread: ${title.slice(0, 100)}${title.length > 100 ? '...' : ''}`,
title,
rank: makeRank(lastOne?.rank, undefined),
content: '' as MarkupBlobRef,
parentInfo: [],
blobs: {}
parent: parentCard._id
},
chat.masterTag.Thread
)
+36 -7
View File
@@ -13,8 +13,8 @@
// limitations under the License.
//
import { type Card } from '@hcengineering/card'
import type { Ref } from '@hcengineering/core'
import { type Card, type MasterTag } from '@hcengineering/card'
import type { Doc, Ref } from '@hcengineering/core'
import { navigate, type Location, getCurrentResolvedLocation } from '@hcengineering/ui'
import { chatId } from '@hcengineering/chat'
import { getClient } from '@hcengineering/presentation'
@@ -25,22 +25,51 @@ import { openWidget } from '@hcengineering/workbench-resources'
import chat from './plugin'
import { type ChatWidgetData } from './types'
// Url: /chat/{cardId}/{threadId}?message={messageId}
export function decodeURI (value: string): ['type', Ref<MasterTag>] | ['card', Ref<Card>] {
return decodeURIComponent(value).split('|') as any
}
export function encodeURI (type: 'type' | 'card', ref: Ref<Doc>): string {
return [type, ref].join('|')
}
export function getCardIdFromLocation (loc: Location): Ref<Card> | undefined {
if (loc.path[2] !== chatId) {
return undefined
}
return loc.path[3] as Ref<Card>
const [type, ref] = decodeURI(loc.path[3])
if (type !== 'card') {
return undefined
}
return ref
}
export function getTypeIdFromLocation (loc: Location): Ref<MasterTag> | undefined {
if (loc.path[2] !== chatId) {
return undefined
}
const [type, ref] = decodeURI(loc.path[3])
if (type !== 'type') {
return undefined
}
return ref
}
export function navigateToCard (_id: Ref<Card>): void {
const loc = getCurrentResolvedLocation()
loc.path[2] = chatId
loc.path[3] = _id
loc.path[4] = ''
loc.path.length = 4
loc.path[3] = encodeURI('card', _id)
delete loc.query?.message
navigate(loc)
}
export function navigateToType (_id: Ref<MasterTag>): void {
const loc = getCurrentResolvedLocation()
loc.path[2] = chatId
loc.path[3] = encodeURI('type', _id)
delete loc.query?.message
navigate(loc)
+23 -48
View File
@@ -15,7 +15,7 @@
import { type Card, type MasterTag } from '@hcengineering/card'
import { type NavigationSection } from '@hcengineering/ui-next'
import { groupByArray, type Hierarchy, type Ref } from '@hcengineering/core'
import { type Hierarchy, type Ref } from '@hcengineering/core'
import chat from '@hcengineering/chat'
import { translate } from '@hcengineering/platform'
import { getClient } from '@hcengineering/presentation'
@@ -59,28 +59,30 @@ export function toggleSection (id: string): void {
}
export async function cardsToChatSections (
cards: Card[],
cardsByType: Map<Ref<MasterTag>, { cards: Card[], total: number }>,
contexts: NotificationContext[],
state: NavigatorState
): Promise<NavigationSection[]> {
const client = getClient()
const hierarchy = client.getHierarchy()
const { threads, channels, other } = splitCards(cards, hierarchy)
const cardByClass = groupByArray(other, (it) => it._class)
const threads = cardsByType.get(chat.masterTag.Thread)
const channels = cardsByType.get(chat.masterTag.Channel)
const result: NavigationSection[] = []
if (threads.length > 0) {
result.push(getSection(chat.masterTag.Thread, threads, contexts, state, hierarchy))
if (threads != null && threads.cards.length > 0) {
result.push(getSection(chat.masterTag.Thread, threads.cards, threads.total, contexts, state, hierarchy))
}
if (channels.length > 0) {
result.push(getSection(chat.masterTag.Channel, channels, contexts, state, hierarchy))
if (channels != null && channels.cards.length > 0) {
result.push(getSection(chat.masterTag.Channel, channels.cards, channels.total, contexts, state, hierarchy))
}
const cardSessions: Array<[string, NavigationSection]> = []
for (const [_class, cards] of cardByClass.entries()) {
const section = getSection(_class, cards, contexts, state, hierarchy)
for (const [type, cards] of cardsByType.entries()) {
if (type === chat.masterTag.Thread || type === chat.masterTag.Channel) continue
if (cards.cards.length === 0) continue
const section = getSection(type, cards.cards, cards.total, contexts, state, hierarchy)
const label = await translate(section.title, {})
cardSessions.push([label, section])
}
@@ -95,6 +97,7 @@ export async function cardsToChatSections (
function getSection (
_class: Ref<MasterTag>,
cards: Card[],
total: number,
contexts: NotificationContext[],
state: NavigatorState,
hierarchy: Hierarchy
@@ -105,43 +108,15 @@ function getSection (
id: _class,
title: clazz.pluralLabel ?? clazz.label,
expanded: !state.collapsedSections.includes(_class),
items: cards
.map((card) => {
const context = contexts.find((it) => it.card === card._id)
return {
id: card._id,
label: card.title,
icon: clazz.icon ?? chat.icon.Thread,
notificationsCount: context?.notifications?.length ?? 0
}
})
.sort((c1, c2) => c1.label.toLowerCase().localeCompare(c2.label.toLowerCase()))
total,
items: cards.map((card) => {
const context = contexts.find((it) => it.card === card._id)
return {
id: card._id,
label: card.title,
icon: clazz.icon ?? chat.icon.Thread,
notificationsCount: context?.notifications?.length ?? 0
}
})
}
}
function splitCards (
cards: Card[],
hierarchy: Hierarchy
): {
threads: Card[]
channels: Card[]
other: Card[]
} {
return cards.reduce<{
threads: Card[]
channels: Card[]
other: Card[]
}>(
(acc, it) => {
if (hierarchy.isDerived(it._class, chat.masterTag.Thread)) {
acc.threads.push(it)
} else if (hierarchy.isDerived(it._class, chat.masterTag.Channel)) {
acc.channels.push(it)
} else {
acc.other.push(it)
}
return acc
},
{ threads: [], channels: [], other: [] }
)
}
@@ -2,7 +2,7 @@
import { createEventDispatcher, onDestroy } from 'svelte'
import { activeViewlet, makeViewletKey, setActiveViewletId } from '../utils'
import { resolvedLocationStore, Switcher } from '@hcengineering/ui'
import view, { Viewlet, ViewletPreference } from '@hcengineering/view'
import view, { Viewlet, ViewletDescriptor, ViewletPreference } from '@hcengineering/view'
import core, { DocumentQuery, Ref, WithLookup } from '@hcengineering/core'
import { deepEqual } from 'fast-equals'
import { createQuery } from '@hcengineering/presentation'
@@ -14,6 +14,7 @@
export let loading = true
export let hidden = false
export let ignoreFragment = false
export let defaultViewletDescriptor: Ref<ViewletDescriptor> | undefined = undefined
const query = createQuery()
@@ -47,9 +48,12 @@
viewlets: Array<WithLookup<Viewlet>>,
activeViewlet: Record<string, Ref<Viewlet> | null>,
key: string
) {
): void {
if (viewlets == null || viewlets.length === 0) return
const newViewlet = viewlets.find((viewlet) => viewlet?._id === activeViewlet[key]) ?? viewlets[0]
const newViewlet =
viewlets.find((viewlet) => viewlet?._id === activeViewlet[key]) ??
viewlets.find((viewlet) => viewlet.descriptor === defaultViewletDescriptor) ??
viewlets[0]
if (viewlet?._id !== newViewlet?._id || !deepEqual(viewlet.config, newViewlet.config)) {
viewlet = newViewlet
setActiveViewletId(newViewlet._id)
@@ -26,6 +26,7 @@
import ViewletClassSettings from './ViewletClassSettings.svelte'
export let viewlet: Viewlet
export let defaultConfig: (BuildModelKey | string)[] | undefined = undefined
const dispatch = createEventDispatcher()
@@ -98,11 +99,13 @@
}
function getBaseConfig (viewlet: Viewlet): Config[] {
const lookup = buildConfigLookup(hierarchy, viewlet.attachTo, viewlet.config, viewlet.options?.lookup)
const config = defaultConfig ?? viewlet.config
const lookup = buildConfigLookup(hierarchy, viewlet.attachTo, config, viewlet.options?.lookup)
const result: Config[] = []
const clazz = hierarchy.getClass(viewlet.attachTo)
let wasOptional = false
for (const param of viewlet.config) {
for (const param of config) {
if (typeof param === 'string') {
if (viewlet.configOptions?.hiddenKeys?.includes(param)) continue
if (param.length === 0) {
@@ -14,7 +14,7 @@
-->
<script lang="ts">
import { ButtonIcon, showPopup, closeTooltip } from '@hcengineering/ui'
import { ViewOptionModel, ViewOptions, Viewlet } from '@hcengineering/view'
import { ViewOptionModel, ViewOptions, Viewlet, type ViewOptionsModel, BuildModelKey } from '@hcengineering/view'
import view from '../plugin'
import { getViewOptions, viewOptionStore, defaultOptions } from '../viewOptions'
import ViewOptionsButton from './ViewOptionsButton.svelte'
@@ -26,6 +26,8 @@
export let viewlet: Viewlet | undefined = undefined
export let disabled: boolean = false
export let viewOptionsConfig: ViewOptionModel[] | undefined = undefined
export let defaultViewOptions: ViewOptions | undefined = undefined
export let defaultConfig: (BuildModelKey | string)[] | undefined = undefined
let btn: HTMLButtonElement
let pressed: boolean = false
@@ -33,12 +35,29 @@
function clickHandler () {
pressed = true
closeTooltip()
showPopup(ViewletSetting, { viewlet }, btn, () => {
showPopup(ViewletSetting, { viewlet, defaultConfig }, btn, () => {
pressed = false
})
}
$: viewOptions = getViewOptions(viewlet, $viewOptionStore, defaultOptions)
function getDefaults (viewOptions: ViewOptionsModel): ViewOptions {
const res: ViewOptions = {
groupBy: [viewOptions.groupBy[0] ?? defaultOptions.groupBy[0]],
orderBy: viewOptions.orderBy?.[0] ?? defaultOptions.orderBy
}
for (const opt of viewOptions.other) {
res[opt.key] = opt.defaultValue
}
return res
}
function getDefaultOptions (): ViewOptions {
if (defaultViewOptions != null) return defaultViewOptions
return viewlet?.viewOptions != null ? getDefaults(viewlet.viewOptions) : defaultOptions
}
$: viewOptions = getViewOptions(viewlet, $viewOptionStore, getDefaultOptions())
$: disabled = $restrictionStore.readonly
</script>
+1 -13
View File
@@ -19,7 +19,6 @@ import {
type ToggleViewOption,
type ViewOptionModel,
type ViewOptions,
type ViewOptionsModel,
type Viewlet,
type ViewletDescriptor
} from '@hcengineering/view'
@@ -84,17 +83,6 @@ function _getViewOptions (viewlet: Viewlet, viewOptionStore: Map<string, ViewOpt
return res
}
function getDefaults (viewOptions: ViewOptionsModel): ViewOptions {
const res: ViewOptions = {
groupBy: [viewOptions.groupBy[0] ?? defaultOptions.groupBy[0]],
orderBy: viewOptions.orderBy?.[0] ?? defaultOptions.orderBy
}
for (const opt of viewOptions.other) {
res[opt.key] = opt.defaultValue
}
return res
}
export function getViewOptions (
viewlet: Viewlet | undefined,
viewOptionStore: Map<string, ViewOptions>,
@@ -105,7 +93,7 @@ export function getViewOptions (
}
const res = _getViewOptions(viewlet, viewOptionStore)
if (res !== null) return res
return viewlet.viewOptions != null ? getDefaults(viewlet.viewOptions) : defaults
return defaults
}
export function migrateViewOpttions (): void {
+2 -1
View File
@@ -174,7 +174,8 @@ const view = plugin(viewId, {
ObjectMention: '' as AnyComponent,
SearchSelector: '' as AnyComponent,
FoldersBrowser: '' as AnyComponent,
PersonIdPresenter: '' as AnyComponent
PersonIdPresenter: '' as AnyComponent,
RolePresenter: '' as AnyComponent
},
ids: {
IconWithEmoji: '' as Asset
@@ -29,7 +29,7 @@
SearchInput,
showPopup
} from '@hcengineering/ui'
import { Viewlet, ViewletDescriptor, ViewletPreference, ViewOptions } from '@hcengineering/view'
import { Viewlet, ViewletDescriptor, ViewletPreference, ViewOptions, BuildModelKey } from '@hcengineering/view'
import {
FilterBar,
FilterButton,
@@ -58,6 +58,9 @@
export let queryBuilder: Resource<() => Promise<DocumentQuery<Doc>>> | undefined = undefined
export let actionConfig: Record<string, any> = {}
export let actionVisible: boolean = false
export let defaultViewletDescriptor: Ref<ViewletDescriptor> | undefined = undefined
export let defaultViewOptions: ViewOptions | undefined = undefined
export let defaultConfig: (BuildModelKey | string)[] | undefined = undefined
const client = getClient()
const hierarchy = client.getHierarchy()
@@ -130,6 +133,7 @@
bind:viewlet
bind:preference
bind:viewlets
{defaultViewletDescriptor}
ignoreFragment
viewletQuery={{
attachTo: _class,
@@ -137,7 +141,7 @@
...(descriptors !== undefined ? { descriptor: { $in: descriptors } } : {})
}}
/>
<ViewletSettingButton bind:viewOptions bind:viewlet />
<ViewletSettingButton bind:viewOptions bind:viewlet {defaultViewOptions} {defaultConfig} />
</svelte:fragment>
<Breadcrumb {icon} {label} size={'large'} isCurrent />
@@ -198,7 +202,7 @@
_class,
space,
options,
config: preference?.config ?? viewlet.config,
config: preference?.config ?? defaultConfig ?? viewlet.config,
viewlet,
viewOptions,
viewOptionsConfig: viewlet.viewOptions?.other,
@@ -218,7 +222,7 @@
_class,
space,
options,
config: preference?.config ?? viewlet.config,
config: preference?.config ?? defaultConfig ?? viewlet.config,
viewlet,
viewOptions,
viewOptionsConfig: viewlet.viewOptions?.other,