mirror of
https://github.com/hcengineering/platform.git
synced 2026-10-01 22:15:04 +02:00
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:
@@ -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]
|
||||
|
||||
@@ -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,6 +1,7 @@
|
||||
{
|
||||
"string": {
|
||||
"Added": "Přidáno",
|
||||
"All": "Vše ({count})",
|
||||
"Attach": "Připojit",
|
||||
"Cancel": "Zrušit",
|
||||
"Edit": "Upravit",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"string": {
|
||||
"Added": "Hinzugefügt",
|
||||
"All": "Alle ({count})",
|
||||
"Attach": "Anhängen",
|
||||
"Cancel": "Abbrechen",
|
||||
"Edit": "Bearbeiten",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"string": {
|
||||
"Added": "Added",
|
||||
"All": "All ({count})",
|
||||
"Attach": "Attach",
|
||||
"Cancel": "Cancel",
|
||||
"Edit": "Edit",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"string": {
|
||||
"Added": "Añadido",
|
||||
"All": "Todo ({count})",
|
||||
"Attach": "Adjuntar",
|
||||
"Cancel": "Cancelar",
|
||||
"Edit": "Editar",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"string": {
|
||||
"Added": "Ajouté",
|
||||
"All": "Tout ({count})",
|
||||
"Attach": "Joindre",
|
||||
"Cancel": "Annuler",
|
||||
"Edit": "Modifier",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"string": {
|
||||
"Added": "Aggiunto" ,
|
||||
"All": "Tutto ({count})",
|
||||
"Attach": "Allega",
|
||||
"Cancel": "Annulla",
|
||||
"Edit": "Modifica",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"string": {
|
||||
"Added": "Adicionado",
|
||||
"All": "Todos ({count})",
|
||||
"Attach": "Anexar",
|
||||
"Cancel": "Cancelar",
|
||||
"Edit": "Editar",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"string": {
|
||||
"Added": "Добавлено",
|
||||
"All": "Всего ({count})",
|
||||
"Attach": "Прикрепить",
|
||||
"Cancel": "Отмена",
|
||||
"Edit": "Редактировать",
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user