Init home page (#9735)

Signed-off-by: Kristina Fefelova <kristin.fefelova@gmail.com>
This commit is contained in:
Kristina
2025-08-29 10:33:31 +04:00
committed by GitHub
parent 8246b96258
commit 5d9f62d178
59 changed files with 1168 additions and 82 deletions
+1 -5
View File
@@ -21,7 +21,7 @@ import { mergeIds, type Resource } from '@hcengineering/platform'
import { type Location, type ResolvedLocation } from '@hcengineering/ui/src/types'
import { type LocationData } from '@hcengineering/workbench'
import { type ActionCategory, type Action, type ViewAction } from '@hcengineering/view'
import { type TagCategory, type TagElement } from '@hcengineering/tags'
import { type TagCategory } from '@hcengineering/tags'
export default mergeIds(cardId, card, {
app: {
@@ -57,9 +57,5 @@ export default mergeIds(cardId, card, {
CardCustomLinkMatch: '' as Resource<(doc: Doc) => boolean>,
CardCustomLinkEncode: '' as Resource<(doc: Doc) => Location>,
CheckRelationsSectionVisibility: '' as Resource<(doc: Card) => Promise<boolean>>
},
label: {
Subscribed: '' as Ref<TagElement>,
NewMessages: '' as Ref<TagElement>
}
})
@@ -18,10 +18,11 @@
import LiteNode from './markup/lite/LiteNode.svelte'
export let message: Markup | MarkupNode
export let colorInherit: boolean = false
$: node = typeof message === 'string' ? markupToJSON(message) : message
</script>
<div class="text-markup-view">
<LiteNode {node} />
<div class="text-markup-view" class:colorInherit>
<LiteNode {node} {colorInherit} />
</div>
@@ -19,12 +19,13 @@
import NodeMarks from '../NodeMarks.svelte'
export let node: MarkupNode
export let colorInherit: boolean = false
</script>
{#if node}
{@const marks = node.marks ?? []}
<NodeMarks {marks}>
<LiteNodeContent {node} />
<LiteNodeContent {node} {colorInherit} />
</NodeMarks>
{/if}
@@ -22,6 +22,7 @@
import { getBlobRef } from '../../../preview'
export let node: MarkupNode
export let colorInherit: boolean = false
function toRefBlob (blobId: AttrValue): Ref<Blob> {
return blobId as Ref<Blob>
@@ -48,15 +49,15 @@
{@const nodes = node.content ?? []}
{#if node.type === MarkupNodeType.doc}
<LiteNodes {nodes} />
<LiteNodes {nodes} {colorInherit} />
{:else if node.type === MarkupNodeType.text}
{node.text}
{:else if node.type === MarkupNodeType.paragraph}
<p class="p-inline contrast" class:overflow-label={true} style:margin="0">
<LiteNodes {nodes} />
<p class="p-inline" class:overflow-label={true} style:margin="0" class:contrast={!colorInherit} class:colorInherit>
<LiteNodes {nodes} {colorInherit} />
</p>
{:else if node.type === MarkupNodeType.blockquote}
<LiteNodes {nodes} />
<LiteNodes {nodes} {colorInherit} />
{:else if node.type === MarkupNodeType.horizontal_rule}
<!-- nothing-->
{:else if node.type === MarkupNodeType.code_block}
@@ -69,7 +70,7 @@
}
]}
>
<LiteNodes {nodes} />
<LiteNodes {nodes} {colorInherit} />
</NodeMarks>
</p>
{:else if node.type === MarkupNodeType.reference}
@@ -80,7 +81,7 @@
{#if objectClass !== undefined && objectId !== undefined}
<ObjectNode _id={toRef(objectId)} _class={toClassRef(objectClass)} title={objectLabel} />
{:else}
<LiteNodes {nodes} />
<LiteNodes {nodes} {colorInherit} />
{/if}
{:else if node.type === MarkupNodeType.emoji}
<span class="emoji">
@@ -100,9 +101,15 @@
<!-- TODO not implemented -->
{:else if node.type === MarkupNodeType.subLink}
<sub>
<LiteNodes {nodes} />
<LiteNodes {nodes} {colorInherit} />
</sub>
{:else}
<LiteNodes {nodes} />
<LiteNodes {nodes} {colorInherit} />
{/if}
{/if}
<style lang="scss">
.colorInherit {
color: inherit;
}
</style>
@@ -18,10 +18,11 @@
import LiteNode from './LiteNode.svelte'
export let nodes: MarkupNode[]
export let colorInherit: boolean = false
</script>
{#if nodes}
{#each nodes as node}
<LiteNode {node} />
<LiteNode {node} {colorInherit} />
{/each}
{/if}
+6 -2
View File
@@ -13,6 +13,10 @@
color: var(--theme-text-primary-color);
margin: 0.25rem 0;
&.colorInherit {
color: inherit;
}
.suggestion {
padding-left: 0.25rem;
color: var(--theme-link-color);
@@ -544,7 +548,7 @@
}
}
&.embed-youtube {
&.embed-youtube {
iframe {
margin: 0;
padding: 0;
@@ -585,4 +589,4 @@
.select-text .text-editor-image-container {
user-select: all;
}
}
+5 -1
View File
@@ -99,6 +99,10 @@
"WeeksWOValue": "{weeks, plural, =1 {týden} other {týdny}}",
"MonthsWOValue": "{months, plural, =1 {měsíc} other {měsíce}}",
"Filter": "Filtr",
"Clear": "Vymazat"
"Clear": "Vymazat",
"Yesterday": "Včera",
"ThisWeek": "Tento týden",
"ThisMonth": "Tento měsíc",
"ThisYear": "Tento rok"
}
}
+5 -1
View File
@@ -100,6 +100,10 @@
"WeeksWOValue": "{weeks, plural, =1 {Woche} other {Wochen}}",
"MonthsWOValue": "{months, plural, =1 {Monat} other {Monate}}",
"Filter": "Filter",
"Clear": "Löschen"
"Clear": "Löschen",
"Yesterday": "Gestern",
"ThisWeek": "Diese Woche",
"ThisMonth": "Diesen Monat",
"ThisYear": "Dieses Jahr"
}
}
+5 -1
View File
@@ -100,6 +100,10 @@
"WeeksWOValue": "{weeks, plural, =1 {week} other {weeks}}",
"MonthsWOValue": "{months, plural, =1 {month} other {months}}",
"Filter": "Filter",
"Clear": "Clear"
"Clear": "Clear",
"Yesterday": "Yesterday",
"ThisWeek": "This week",
"ThisMonth": "This month",
"ThisYear": "This year"
}
}
+5 -1
View File
@@ -100,6 +100,10 @@
"WeeksWOValue": "{weeks, plural, =1 {semana} other {semanas}}",
"MonthsWOValue": "{months, plural, =1 {mes} other {meses}}",
"Filter": "Filtrar",
"Clear": "Limpiar"
"Clear": "Limpiar",
"Yesterday": "Ayer",
"ThisWeek": "Esta semana",
"ThisMonth": "Este mes",
"ThisYear": "Este año"
}
}
+5 -1
View File
@@ -100,6 +100,10 @@
"WeeksWOValue": "{weeks, plural, =1 {semaine} other {semaines}}",
"MonthsWOValue": "{months, plural, =1 {mois} other {mois}}",
"Filter": "Filtrer",
"Clear": "Effacer"
"Clear": "Effacer",
"Yesterday": "Hier",
"ThisWeek": "Cette semaine",
"ThisMonth": "Ce mois-ci",
"ThisYear": "Cette année"
}
}
+5 -1
View File
@@ -100,6 +100,10 @@
"WeeksWOValue": "{weeks, plural, =1 {settimana} other {settimane}}",
"MonthsWOValue": "{months, plural, =1 {mese} other {mesi}}",
"Filter": "Filtra",
"Clear": "Cancella"
"Clear": "Cancella",
"Yesterday": "Ieri",
"ThisWeek": "Questa settimana",
"ThisMonth": "Questo mese",
"ThisYear": "Quest'anno"
}
}
+5 -1
View File
@@ -100,6 +100,10 @@
"WeeksWOValue": "{weeks, plural, =1 {週} other {週}}",
"MonthsWOValue": "{months, plural, =1 {か月} other {か月}}",
"Filter": "フィルター",
"Clear": "クリア"
"Clear": "クリア",
"Yesterday": "昨日",
"ThisWeek": "今週",
"ThisMonth": "今月",
"ThisYear": "今年"
}
}
+5 -1
View File
@@ -100,6 +100,10 @@
"WeeksWOValue": "{weeks, plural, =1 {semana} other {semanas}}",
"MonthsWOValue": "{months, plural, =1 {mês} other {meses}}",
"Filter": "Filtrar",
"Clear": "Limpar"
"Clear": "Limpar",
"Yesterday": "Ontem",
"ThisWeek": "Esta semana",
"ThisMonth": "Este mês",
"ThisYear": "Este ano"
}
}
+5 -1
View File
@@ -100,6 +100,10 @@
"WeeksWOValue": "{weeks, plural, one {неделя} few {недели} other {недель}}",
"MonthsWOValue": "{months, plural, one {месяц} few {месяца} other {месяцев}}",
"Filter": "Фильтр",
"Clear": "Очистить"
"Clear": "Очистить",
"Yesterday": "Вчера",
"ThisWeek": "Эта неделя",
"ThisMonth": "Этот месяц",
"ThisYear": "Этот год"
}
}
+5 -1
View File
@@ -100,6 +100,10 @@
"WeeksWOValue": "{weeks, plural, =1 {周} other {周}}",
"MonthsWOValue": "{months, plural, =1 {月} other {月}}",
"Filter": "过滤",
"Clear": "清除"
"Clear": "清除",
"Yesterday": "昨天",
"ThisWeek": "本周",
"ThisMonth": "本月",
"ThisYear": "今年"
}
}
@@ -12,7 +12,7 @@
export let label: IntlString
export let value: string | undefined = undefined
export let kind: 'default' | 'ghost' = 'default'
export let kind: 'default' | 'ghost' | 'secondary' = 'default'
export let size: 'small' | 'medium' | 'large' = 'small'
export let disabled: boolean = false
export let error: boolean = false
@@ -149,6 +149,25 @@
height: var(--global-extra-large-Size);
}
}
&.secondary {
background-color: var(--input-BackgroundColor);
box-shadow: inset 0 0 0 1px var(--input-BorderColor);
&.small {
padding: var(--spacing-1) var(--spacing-1_5);
height: var(--global-small-Size);
}
&.medium {
padding: var(--spacing-1_5) var(--spacing-2);
height: var(--global-medium-Size);
}
&.large {
position: relative;
padding: 0 var(--spacing-2);
height: var(--global-extra-large-Size);
}
}
&.ghost {
&.small,
&.medium {
@@ -187,9 +206,27 @@
background-color: var(--input-hover-BackgroundColor);
}
}
&.secondary {
input::placeholder {
color: var(--input-PlaceholderColor);
}
&:active,
&:focus-within {
background-color: var(--input-BackgroundColor);
outline: 2px solid var(--global-focus-BorderColor);
outline-offset: 2px;
}
&:hover {
background-color: var(--input-hover-BackgroundColor);
}
}
&.ghost input::placeholder {
color: var(--input-PlaceholderColor);
}
&.secondary input::placeholder {
color: var(--input-PlaceholderColor);
}
&:hover input:not(:focus)::placeholder {
color: var(--input-hover-PlaceholderColor);
}
+4
View File
@@ -58,6 +58,10 @@ export const uis = plugin(uiId, {
None: '' as IntlString,
NotSelected: '' as IntlString,
Today: '' as IntlString,
Yesterday: '' as IntlString,
ThisWeek: '' as IntlString,
ThisMonth: '' as IntlString,
ThisYear: '' as IntlString,
NoDate: '' as IntlString,
StartDate: '' as IntlString,
TargetDate: '' as IntlString,
+3
View File
@@ -46,4 +46,7 @@
<symbol id="document" viewBox="0 0 32 32">
<path fill-rule="evenodd" clip-rule="evenodd" d="M9 4C7.89543 4 7 4.89543 7 6V26C7 27.1046 7.89543 28 9 28H23C24.1046 28 25 27.1046 25 26V12H21C18.7909 12 17 10.2091 17 8V4H9ZM19 4.41421V8C19 9.10457 19.8954 10 21 10H24.5858L19 4.41421ZM5 6C5 3.79086 6.79086 2 9 2H18.5858C19.1162 2 19.6249 2.21071 20 2.58579L26.4142 9C26.7893 9.37507 27 9.88378 27 10.4142V26C27 28.2091 25.2091 30 23 30H9C6.79086 30 5 28.2091 5 26V6ZM10 17C10 16.4477 10.4477 16 11 16H21C21.5523 16 22 16.4477 22 17C22 17.5523 21.5523 18 21 18H11C10.4477 18 10 17.5523 10 17ZM10 23C10 22.4477 10.4477 22 11 22H21C21.5523 22 22 22.4477 22 23C22 23.5523 21.5523 24 21 24H11C10.4477 24 10 23.5523 10 23Z" />
</symbol>
<symbol id="home" viewBox="0 0 32 32">
<path fill-rule="evenodd" clip-rule="evenodd" d="M15.2929 2.29302C15.6834 1.90249 16.3166 1.90249 16.7071 2.29302L29.7071 15.293C30.0976 15.6835 30.0976 16.3167 29.7071 16.7072C29.3166 17.0978 28.6834 17.0978 28.2929 16.7072L28 16.4143V26.0001C28 27.1047 27.1046 28.0001 26 28.0001H6C4.89543 28.0001 4 27.1047 4 26.0001V16.4143L3.70711 16.7072C3.31658 17.0978 2.68342 17.0978 2.29289 16.7072C1.90237 16.3167 1.90237 15.6835 2.29289 15.293L15.2929 2.29302ZM6 14.4143V26.0001H12V17.0001C12 16.4478 12.4477 16.0001 13 16.0001H19C19.5523 16.0001 20 16.4478 20 17.0001V26.0001H26V14.4143L16 4.41434L6 14.4143ZM18 26.0001V18.0001H14V26.0001H18Z" fill="currentColor"/>
</symbol>
</svg>

Before

Width:  |  Height:  |  Size: 6.4 KiB

After

Width:  |  Height:  |  Size: 7.1 KiB

+7 -1
View File
@@ -40,6 +40,12 @@
"Properties": "Vlastnosti",
"NoChildren": "Žádné děti",
"ConfigDescription": "Rozšíření pro správu znalostí ve stylu databáze.",
"AddCollaborators": "Přidat spolupracovníky"
"AddCollaborators": "Přidat spolupracovníky",
"Home": "Domů",
"WhatDoYouWantToShare": "Co chcete sdílet?",
"Share": "Sdílet",
"Compact": "Kompaktní",
"Comfortable": "Komfortní",
"Comfortable2": "Komfortní 2"
}
}
+7 -1
View File
@@ -40,6 +40,12 @@
"Properties": "Eigenschaften",
"NoChildren": "Keine Kinder",
"ConfigDescription": "Erweiterung für Datenbank-basiertes Wissensmanagement.",
"AddCollaborators": "Mitarbeiter hinzufügen"
"AddCollaborators": "Mitarbeiter hinzufügen",
"Home": "Startseite",
"WhatDoYouWantToShare": "Was möchten Sie teilen?",
"Share": "Teilen",
"Compact": "Kompakt",
"Comfortable": "Komfortabel",
"Comfortable2": "Komfortabel 2"
}
}
+7 -1
View File
@@ -40,6 +40,12 @@
"Properties": "Properties",
"NoChildren": "No children",
"ConfigDescription": "Extension for database-style knowledge management.",
"AddCollaborators": "Add collaborators"
"AddCollaborators": "Add collaborators",
"Home": "Home",
"WhatDoYouWantToShare": "What do you want to share?",
"Share": "Share",
"Compact": "Compact",
"Comfortable": "Comfortable",
"Comfortable2": "Comfortable 2"
}
}
+7 -1
View File
@@ -40,6 +40,12 @@
"Properties": "Propiedades",
"NoChildren": "No hay hijos",
"ConfigDescription": "Extensión para la gestión del conocimiento al estilo de una base de datos.",
"AddCollaborators": "Agregar colaboradores"
"AddCollaborators": "Agregar colaboradores",
"Home": "Inicio",
"WhatDoYouWantToShare": "¿Qué quieres compartir?",
"Share": "Compartir",
"Compact": "Compacto",
"Comfortable": "Confortable",
"Comfortable2": "Confortable 2"
}
}
+7 -1
View File
@@ -40,6 +40,12 @@
"Properties": "Propriétés",
"NoChildren": "Pas d'enfants",
"ConfigDescription": "Extension pour la gestion des connaissances de style base de données.",
"AddCollaborators": "Ajouter des collaborateurs"
"AddCollaborators": "Ajouter des collaborateurs",
"Home": "Accueil",
"WhatDoYouWantToShare": "Que voulez-vous partager ?",
"Share": "Partager",
"Compact": "Compact",
"Comfortable": "Confortable",
"Comfortable2": "Confortable 2"
}
}
+7 -1
View File
@@ -40,6 +40,12 @@
"NoChildren": "Nessun figlio",
"NumberTypes": "{count, plural, one {# tipo} other {# tipi}}",
"ConfigDescription": "Estensione per la gestione della conoscenza in stile database.",
"AddCollaborators": "Aggiungi collaboratori"
"AddCollaborators": "Aggiungi collaboratori",
"Home": "Home",
"WhatDoYouWantToShare": "Cosa vuoi condividere?",
"Share": "Condividi",
"Compact": "Compatto",
"Comfortable": "Confortabile",
"Comfortable2": "Confortabile 2"
}
}
+7 -1
View File
@@ -40,6 +40,12 @@
"Properties": "プロパティ",
"NoChildren": "子がありません",
"ConfigDescription": "データベーススタイルのナレッジマネジメント用の拡張機能。",
"AddCollaborators": "コラボレーターを追加"
"AddCollaborators": "コラボレーターを追加",
"Home": "ホーム",
"WhatDoYouWantToShare": "どうしますかを共有しますか?",
"Share": "共有",
"Compact": "コンパクト",
"Comfortable": "コンファンティブ",
"Comfortable2": "コンファンティブ 2"
}
}
+7 -1
View File
@@ -40,6 +40,12 @@
"Properties": "Propriedades",
"NoChildren": "Sem filhos",
"ConfigDescription": "Extensão para gerenciamento de conhecimento no estilo de banco de dados.",
"AddCollaborators": "Adicionar colaboradores"
"AddCollaborators": "Adicionar colaboradores",
"Home": "Início",
"WhatDoYouWantToShare": "O que você deseja compartilhar?",
"Share": "Compartilhar",
"Compact": "Compacto",
"Comfortable": "Confortável",
"Comfortable2": "Confortável 2"
}
}
+7 -1
View File
@@ -40,6 +40,12 @@
"Properties": "Свойства",
"NoChildren": "Нет потомков",
"ConfigDescription": "Расширение для управления знаниями в стиле базы данных.",
"AddCollaborators": "Добавить сотрудников"
"AddCollaborators": "Добавить сотрудников",
"Home": "Дом",
"WhatDoYouWantToShare": "Что вы хотите поделиться?",
"Share": "Поделиться",
"Compact": "Компактный",
"Comfortable": "Комфортный",
"Comfortable2": "Комфортный 2"
}
}
+7 -1
View File
@@ -40,6 +40,12 @@
"Properties": "属性",
"NoChildren": "没有子级",
"ConfigDescription": "用于数据库风格知识管理的扩展。",
"AddCollaborators": "添加协作者"
"AddCollaborators": "添加协作者",
"Home": "主页",
"WhatDoYouWantToShare": "您想分享什么?",
"Share": "分享",
"Compact": "紧凑",
"Comfortable": "舒适",
"Comfortable2": "舒适 2"
}
}
+2 -1
View File
@@ -23,5 +23,6 @@ loadMetadata(card.icon, {
Card: `${icons}#card`,
File: `${icons}#file`,
View: `${icons}#view`,
Document: `${icons}#document`
Document: `${icons}#document`,
Home: `${icons}#home`
})
@@ -23,7 +23,7 @@
import card from '../plugin'
import { getCardIconInfo } from '../utils'
export let value: Card | undefined = undefined
export let value: Card | undefined | null = undefined
export let _id: Ref<Card> | undefined = undefined
export let size: IconSize = 'small'
export let buttonSize: ButtonSize = 'medium'
@@ -33,14 +33,14 @@
const hierarchy = client.getHierarchy()
const query = createQuery()
let doc: Card | undefined = value
let doc: Card | undefined = value ?? undefined
$: if (_id !== undefined && doc === undefined) {
$: if (_id !== undefined && value == null) {
query.query(card.class.Card, { _id }, (res) => {
doc = res[0]
})
} else {
doc = value
doc = value ?? undefined
query.unsubscribe()
}
@@ -62,7 +62,7 @@
.divider {
border: 1px solid var(--theme-content-color);
width: 1px;
height: 100%;
height: 0.125rem;
margin: 0 0.125rem;
}
</style>
@@ -26,9 +26,12 @@
import { createCard } from '../utils'
import CardCollaborators from './CardCollaborators.svelte'
import { TypeSelector } from '../index'
export let type: Ref<MasterTag>
export let title: string = ''
export let type: Ref<MasterTag> = 'chat:masterTag:Thread' as Ref<MasterTag>
export let space: CardSpace | undefined = undefined
export let changeType: boolean = false
const dispatch = createEventDispatcher()
const client = getClient()
@@ -37,12 +40,14 @@
const me = getCurrentEmployee()
const _id = generateId<Card>()
const extension = client
.getModel()
.findAllSync(card.mixin.CreateCardExtension, {})
.find((it) => hierarchy.isDerived(type, it._id))
$: extension = type
? client
.getModel()
.findAllSync(card.mixin.CreateCardExtension, {})
.find((it) => hierarchy.isDerived(type, it._id))
: undefined
let data: Partial<Data<Card>> = {}
let data: Partial<Data<Card>> = { title }
let _space: Ref<CardSpace> | undefined = space?._id
let description: Markup = EmptyMarkup
let collaborators: Ref<Employee>[] = [me]
@@ -50,6 +55,7 @@
let creating = false
async function addCollaborators (): Promise<void> {
if (type == null) return
const accounts = collaborators
.filter((it) => it !== me)
.map((it) => $employeeByIdStore.get(it)?.personUuid)
@@ -61,7 +67,7 @@
}
async function okAction (): Promise<void> {
if (_space === undefined) return
if (_space === undefined || type == null) return
try {
creating = true
@@ -171,6 +177,12 @@
</div>
<div class="hulyModal-content__settingsSet">
{#if changeType}
<div class="hulyModal-content__settingsSet-line">
<span class="label"><Label label={card.string.MasterTag} /></span>
<TypeSelector bind:value={type} />
</div>
{/if}
{#if space == null && !(extension?.hideSpace ?? false)}
<div class="hulyModal-content__settingsSet-line">
<span class="label"><Label label={core.string.Space} /></span>
@@ -0,0 +1,220 @@
<!-- 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 { createQuery, getClient } from '@hcengineering/presentation'
import { Card } from '@hcengineering/card'
import { SortingOrder } from '@hcengineering/core'
import ui, {
eventToHTMLElement,
IconSettings,
Label,
ModernButton,
ModernEditbox,
Scroller,
showPopup
} from '@hcengineering/ui'
import { createEventDispatcher } from 'svelte'
import HomeCardPresenter from './HomeCardPresenter.svelte'
import HomeSettings from './HomeSettings.svelte'
import CreateCardPopup from './CreateCardPopup.svelte'
import card from '../plugin'
const cardsQuery = createQuery()
const limitStep = 50
const dispatch = createEventDispatcher()
let divScroll: HTMLDivElement
let limit = limitStep
let cards: Card[] = []
let total = -1
cardsQuery.query(
card.class.Card,
{},
(res) => {
cards = res
total = res.total
},
{
sort: { modifiedOn: SortingOrder.Descending },
limit,
total: true
}
)
$: hasNextPage = total > cards.length
function onScroll (): void {
if (divScroll != null && hasNextPage) {
const isAtBottom = divScroll.scrollTop + divScroll.clientHeight >= divScroll.scrollHeight - 400
if (isAtBottom) {
limit += limitStep
}
}
}
function getFormatDateId (timestamp: number): string {
const now = new Date()
const date = new Date(timestamp)
const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime()
const yesterdayStart = todayStart - 24 * 60 * 60 * 1000
const thisWeekStart = new Date(now)
const day = thisWeekStart.getDay() === 0 ? 7 : thisWeekStart.getDay()
thisWeekStart.setDate(thisWeekStart.getDate() - (day - 1))
thisWeekStart.setHours(0, 0, 0, 0)
const thisMonthStart = new Date(now.getFullYear(), now.getMonth(), 1).getTime()
const thisYearStart = new Date(now.getFullYear(), 0, 1).getTime()
if (date.getTime() >= todayStart) {
return 'today'
} else if (date.getTime() >= yesterdayStart) {
return 'yesterday'
} else if (date.getTime() >= thisWeekStart.getTime()) {
return 'thisWeek'
} else if (date.getTime() >= thisMonthStart) {
return 'thisMonth'
} else if (date.getTime() >= thisYearStart) {
return 'thisYear'
} else {
return date.getFullYear().toString()
}
}
function onSettings (e: MouseEvent): void {
showPopup(HomeSettings, {}, eventToHTMLElement(e))
}
function onShare (): void {
showPopup(CreateCardPopup, { title, changeType: true }, 'center', async (result) => {
if (result !== undefined) {
const doc = await getClient().findOne(card.class.Card, { _id: result })
if (doc === undefined) return
dispatch('selectCard', doc)
}
})
title = ''
}
let title: string = ''
</script>
<Scroller bind:divScroll {onScroll} padding="2rem 4rem">
<div class="home">
<div class="header flex-gap-2">
<div class="header__title">
<Label label={card.string.Home} />
</div>
<ModernButton icon={IconSettings} on:click={onSettings} size="small" iconSize="small" kind="tertiary" />
</div>
<div class="create-card">
<ModernEditbox
bind:value={title}
label={card.string.WhatDoYouWantToShare}
size="large"
kind="secondary"
width="100%"
disabled={false}
autoFocus={true}
>
<svelte:fragment slot="after">
<ModernButton
label={card.string.Share}
size="small"
kind="primary"
disabled={title.trim() === ''}
on:click={onShare}
/>
</svelte:fragment>
</ModernEditbox>
</div>
<div class="body flex-gap-2">
{#each cards as card, index}
{@const previousCard = cards[index - 1]}
{@const currentDate = card.modifiedOn}
{@const previousDate = previousCard?.modifiedOn}
{@const currentDateId = getFormatDateId(currentDate)}
{@const previousDateId = previousDate != null ? getFormatDateId(previousDate) : undefined}
{#if currentDateId !== previousDateId}
<div class="date">
{#if currentDateId === 'today'}
<Label label={ui.string.Today} />
{:else if currentDateId === 'yesterday'}
<Label label={ui.string.Yesterday} />
{:else if currentDateId === 'thisWeek'}
<Label label={ui.string.ThisWeek} />
{:else if currentDateId === 'thisMonth'}
<Label label={ui.string.ThisMonth} />
{:else if currentDateId === 'thisYear'}
<Label label={ui.string.ThisYear} />
{:else}
{currentDateId}
{/if}
</div>
{/if}
<HomeCardPresenter {card} />
{/each}
</div>
</div>
</Scroller>
<style lang="scss">
.home {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
flex: 1;
}
.create-card {
display: flex;
width: 100%;
margin: 1rem 0;
}
.header {
display: flex;
width: 100%;
padding: 1rem 0;
justify-content: space-between;
&__title {
font-size: 1.5rem;
font-weight: 600;
color: var(--global-primary-TextColor);
}
}
.body {
display: flex;
flex-direction: column;
overflow-x: hidden;
flex: 1;
width: 100%;
}
.date {
display: flex;
text-transform: uppercase;
font-size: 0.875rem;
font-weight: 500;
color: var(--global-secondary-TextColor);
}
</style>
@@ -0,0 +1,284 @@
<!-- 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 cardPlugin, { Card } from '@hcengineering/card'
import { createMessagesQuery } from '@hcengineering/presentation'
import { PersonId, SortingOrder } from '@hcengineering/core'
import { CardID, Message, Label as CardLabel } from '@hcengineering/communication-types'
import { Avatar, getPersonByPersonIdStore, PersonPreviewProvider } from '@hcengineering/contact-resources'
import { Person } from '@hcengineering/contact'
import { MessagePresenter, labelsStore, MessagePreview } from '@hcengineering/communication-resources'
import { Button, IconMoreH, tooltip } from '@hcengineering/ui'
import { showMenu } from '@hcengineering/view-resources'
import { getEmbeddedLabel } from '@hcengineering/platform'
import { isHomeSettingEnabled, compactSettingId, homeSettingsStore, comfortableSettingId2 } from '../home'
import { openCardInSidebar } from '../utils'
import CardTagsColored from './CardTagsColored.svelte'
import CardIcon from './CardIcon.svelte'
export let card: Card
const messagesQuery = createMessagesQuery()
let message: Message | undefined = undefined
let socialId: PersonId | undefined = undefined
let person: Person | undefined = undefined
$: isCompact = isHomeSettingEnabled($homeSettingsStore, compactSettingId)
$: isComfortable2 = isHomeSettingEnabled($homeSettingsStore, comfortableSettingId2)
$: messagesQuery.query(
{ card: card._id, strict: true, attachments: true, reactions: true, limit: 1, order: SortingOrder.Descending },
(res) => {
const msgs = res.getResult().reverse()
message = msgs[msgs.length - 1]
}
)
$: socialId = message?.creator ?? card.modifiedBy
$: personStore = getPersonByPersonIdStore([socialId])
$: person = $personStore.get(socialId)
function hasNewMessages (labels: CardLabel[], cardId: CardID): boolean {
return labels.some((it) => (it.labelId as string) === cardPlugin.label.NewMessages && it.cardId === cardId)
}
let isActionsOpened = false
</script>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div class="card" on:click|stopPropagation|preventDefault={() => openCardInSidebar(card._id, card)}>
<div class="card__avatar">
<PersonPreviewProvider value={person}>
<Avatar name={person?.name} {person} size="medium" />
</PersonPreviewProvider>
</div>
<div class="card__body">
<div class="card__header">
{#if hasNewMessages($labelsStore, card._id)}
<span class="notifyMarker" />
{/if}
<span class="card__title overflow-label" use:tooltip={{ label: getEmbeddedLabel(card.title), textAlign: 'left' }}>
{card.title}
</span>
{#if !isComfortable2}
<span class="card__tags">
<CardTagsColored value={card} />
</span>
{/if}
</div>
<div class="card__message">
{#if message}
{#if isCompact}
<MessagePreview {card} {message} colorInherit />
{:else}
<MessagePresenter {card} {message} hideHeader hideAvatar readonly padding="0" thread={false} />
{/if}
{/if}
</div>
<div class="card__parent" class:wrap={isComfortable2}>
{#if isComfortable2}
<span class="card__tags mr-2">
<CardTagsColored value={card} />
</span>
{/if}
{#if card.parent != null && !isCompact}
{@const info = card.parentInfo?.find((it) => it._id === card.parent)}
{#if info}
<span
class="parent"
use:tooltip={{ label: getEmbeddedLabel(info.title), textAlign: 'left' }}
on:click|stopPropagation|preventDefault={() => openCardInSidebar(info._id)}
>
<CardIcon size="x-small" _id={info._id} editable={false} />
<span class="overflow-label max-w-100">
{info.title}
</span>
</span>
{/if}
{/if}
</div>
</div>
{#if card.parent && isCompact}
{@const info = card.parentInfo?.find((it) => it._id === card.parent)}
{#if info}
<div class="card__parent column">
<span
class="parent"
use:tooltip={{ label: getEmbeddedLabel(info.title), textAlign: 'left' }}
on:click|stopPropagation|preventDefault={() => openCardInSidebar(info._id)}
>
<CardIcon size="x-small" _id={info._id} editable={false} />
<span class="overflow-label max-w-40">
{info.title}
</span>
</span>
</div>
{/if}
{/if}
{#if !isCompact}
<div class="card__actions" class:opened={isActionsOpened}>
<Button
icon={IconMoreH}
iconProps={{ size: 'medium' }}
kind="icon"
dataId="btnMoreActions"
on:click={(e) => {
isActionsOpened = true
showMenu(e, { object: card }, () => {
isActionsOpened = false
})
}}
/>
</div>
{/if}
</div>
<style lang="scss">
.card {
display: flex;
cursor: pointer;
border-radius: 0.5rem;
padding: 1rem;
padding-top: 0.375rem;
gap: 0.75rem;
min-height: 4.75rem;
width: 100%;
height: fit-content;
&:hover {
background-color: var(--global-ui-hover-BackgroundColor);
.card__actions {
visibility: visible;
}
}
&__body {
display: flex;
flex-direction: column;
gap: 0.25rem;
overflow: hidden;
}
&__header {
display: flex;
flex-direction: row;
overflow: hidden;
align-items: center;
height: 2rem;
gap: 0.5rem;
}
&__tags {
display: flex;
flex-direction: row;
height: 2rem;
}
&__title {
color: var(--global-primary-TextColor);
font-weight: 500;
font-size: 0.875rem;
white-space: nowrap;
}
&__avatar {
display: flex;
align-items: flex-start;
justify-content: center;
margin-top: 0.625rem;
}
&__message {
display: flex;
min-height: 1.375rem;
color: var(--global-secondary-TextColor);
}
&__parent {
display: flex;
align-items: center;
margin-top: 0.5rem;
&.wrap {
flex-wrap: wrap;
}
&.column {
flex-direction: column;
align-items: center;
justify-content: center;
margin-left: auto;
padding-left: 0.5rem;
}
}
&__actions {
display: flex;
flex-direction: column;
margin-left: auto;
padding-left: 0.5rem;
visibility: hidden;
&.opened {
visibility: visible;
}
&:hover {
visibility: visible;
}
}
.parent {
display: flex;
justify-content: center;
align-items: center;
padding: 0 0.5rem;
min-width: 2rem;
max-width: 25rem;
min-height: 1.5rem;
max-height: 1.5rem;
font-size: 0.75rem;
font-weight: 500;
border-radius: 1rem;
white-space: nowrap;
gap: 0.25rem;
background: var(--global-ui-hover-BackgroundColor);
border: var(--global-subtle-ui-BorderColor);
color: var(--global-secondary-TextColor);
cursor: pointer;
&:hover {
background: var(--global-ui-active-BackgroundColor);
}
}
.notifyMarker {
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
border-radius: 50%;
background-color: var(--global-higlight-Color);
min-width: 0.5rem;
height: 0.5rem;
}
}
</style>
@@ -0,0 +1,154 @@
<!--
// 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 { createEventDispatcher } from 'svelte'
import { createFocusManager, FocusHandler, Label, ListView, ModernToggle, resizeObserver } from '@hcengineering/ui'
import card from '../plugin'
import {
updateHomeSetting,
isHomeSettingEnabled,
comfortableSettingId,
compactSettingId,
comfortableSettingId2,
homeSettingsStore,
homeViewSettings
} from '../home'
const dispatch = createEventDispatcher()
let popupElement: HTMLDivElement | undefined = undefined
const items = [
{
id: compactSettingId,
label: card.string.Compact
},
{
id: comfortableSettingId,
label: card.string.Comfortable
},
{
id: comfortableSettingId2,
label: card.string.Comfortable2
}
]
let selection = 0
let list: ListView
export function onKeydown (key: KeyboardEvent): boolean {
if (key.code === 'Tab') {
dispatch('close')
key.preventDefault()
key.stopPropagation()
return true
}
if (key.code === 'ArrowUp') {
key.stopPropagation()
key.preventDefault()
list.select(selection - 1)
return true
}
if (key.code === 'ArrowDown') {
key.stopPropagation()
key.preventDefault()
list.select(selection + 1)
return true
}
if (key.code === 'Enter') {
key.preventDefault()
key.stopPropagation()
const item = items[selection]
if (item != null) {
onToggle(item.id)
}
return true
}
return false
}
const manager = createFocusManager()
$: if (popupElement) {
popupElement.focus()
}
function onToggle (id: string): void {
const isEnabled = !isHomeSettingEnabled($homeSettingsStore, id)
updateHomeSetting(id, isEnabled)
if (isEnabled) {
for (const setting of homeViewSettings) {
if (setting === id) continue
updateHomeSetting(setting, false)
}
} else {
const toEnable = homeViewSettings.find((it) => it !== id)
if (toEnable != null) {
updateHomeSetting(toEnable, true)
}
for (const setting of homeViewSettings) {
if (setting === toEnable) continue
updateHomeSetting(setting, false)
}
}
}
</script>
<FocusHandler {manager} />
<!-- svelte-ignore a11y-no-noninteractive-tabindex -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div
class="selectPopup"
bind:this={popupElement}
tabindex="0"
use:resizeObserver={() => {
dispatch('changeContent')
}}
on:keydown={onKeydown}
>
<div class="menu-space" />
<div class="scroll">
<div class="box">
<ListView bind:this={list} count={items.length} bind:selection on:changeContent={() => dispatch('changeContent')}>
<svelte:fragment slot="item" let:item={itemId}>
{@const item = items[itemId]}
<div class="menu-item withList w-full container">
<Label label={item.label} />
<ModernToggle
checked={isHomeSettingEnabled($homeSettingsStore, item.id)}
size="small"
on:change={() => {
onToggle(item.id)
}}
/>
</div>
</svelte:fragment>
</ListView>
</div>
</div>
<div class="menu-space" />
</div>
<style lang="scss">
.container {
display: flex;
align-items: center;
justify-content: space-between;
}
</style>
@@ -15,16 +15,19 @@
<script lang="ts">
import { Scroller } from '@hcengineering/ui'
import cardPlugin, { MasterTag, Card, CardSpace } from '@hcengineering/card'
import { MasterTag, Card, CardSpace } from '@hcengineering/card'
import { createQuery, getClient } from '@hcengineering/presentation'
import { createEventDispatcher } from 'svelte'
import { SavedView } from '@hcengineering/workbench-resources'
import { getCurrentAccount, SortingOrder, Ref } from '@hcengineering/core'
import { TreeItem } from '@hcengineering/view-resources'
import { type NavigatorConfig } from '../../types'
import NavigatorSpace from './NavigatorSpace.svelte'
import NavigatorVariant from './NavigatorVariant.svelte'
import cardPlugin from '../../plugin'
export let config: NavigatorConfig
export let applicationId: string
export let selectedType: Ref<MasterTag> | undefined = undefined
@@ -94,6 +97,13 @@
selectedSpecial = 'favorites'
dispatch('favorites')
}
function onHomeClick (): void {
selectedCard = undefined
selectedType = undefined
selectedSpecial = 'home'
dispatch('home')
}
</script>
<Scroller shrink>
@@ -101,6 +111,15 @@
{#if config.savedViews}
<SavedView alias={applicationId} />
{/if}
{#if config.home}
<TreeItem
_id={'home'}
label={cardPlugin.string.Home}
selected={selectedSpecial === 'home'}
icon={cardPlugin.icon.Home}
on:click={onHomeClick}
/>
{/if}
{#if config.groupBySpace}
{#each spaces as space (space._id)}
<NavigatorSpace
+95
View File
@@ -0,0 +1,95 @@
// 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.
import { get, writable } from 'svelte/store'
import { getCurrentAccount } from '@hcengineering/core'
import { location } from '@hcengineering/ui'
export interface HomeSetting {
id: string
on: boolean
}
export const compactSettingId = 'home-settings.compact'
export const comfortableSettingId = 'home-settings.comfortable'
export const comfortableSettingId2 = 'home-settings.comfortable2'
const defaultValues = {
[compactSettingId]: false,
[comfortableSettingId]: true,
[comfortableSettingId2]: false
} as const
export const homeViewSettings = [compactSettingId, comfortableSettingId, comfortableSettingId2]
export const homeSettingsStore = writable<HomeSetting[]>([])
export function isHomeSettingEnabled (settings: HomeSetting[], id: string): boolean {
const index = settings.findIndex((it) => it.id === id)
if (index === -1) return (defaultValues as any)[id] ?? false
return settings[index].on
}
export function updateHomeSetting (id: string, on: boolean): void {
const settings = get(homeSettingsStore)
const index = settings.findIndex((it) => it.id === id)
if (index === -1) {
homeSettingsStore.set([...settings, { id, on }])
} else {
homeSettingsStore.set(settings.map((it) => (it.id === id ? { ...it, on } : it)))
}
}
let prevWorkspace: string | undefined
location.subscribe((loc) => {
const workspace = loc.path[1]
if (prevWorkspace !== workspace) {
initHomeSettings()
prevWorkspace = workspace
}
})
homeSettingsStore.subscribe((settings) => {
const key = geLocalStorageKey()
if (key === undefined) return
localStorage.setItem(key, JSON.stringify(settings))
})
function geLocalStorageKey (): string | undefined {
const me = getCurrentAccount()
const loc = get(location)
const workspace = loc.path[1] ?? ''
if (me == null || workspace === '') return undefined
return `${workspace}.${me.uuid}.home.settings.v1`
}
function initHomeSettings (): void {
const key = geLocalStorageKey()
if (key === undefined) return
const stored = localStorage.getItem(key)
if (stored == null) {
homeSettingsStore.set([])
return
}
try {
const data = JSON.parse(stored)
Array.isArray(data) ? homeSettingsStore.set(data as HomeSetting[]) : homeSettingsStore.set([])
} catch (e) {
console.error(e)
homeSettingsStore.set([])
}
}
+1
View File
@@ -74,6 +74,7 @@ export { default as Favorites } from './components/Favorites.svelte'
export { default as CardPresenter } from './components/CardPresenter.svelte'
export { default as TypeSelector } from './components/TypeSelector.svelte'
export { default as AssociationsSelect } from './components/settings/view/AssociationsSelect.svelte'
export { default as Home } from './components/Home.svelte'
export * from './types'
export { getCardIconInfo } from './utils'
+7 -1
View File
@@ -100,6 +100,12 @@ export default mergeIds(cardId, card, {
NumberTypes: '' as IntlString,
Properties: '' as IntlString,
NoChildren: '' as IntlString,
AddCollaborators: '' as IntlString
AddCollaborators: '' as IntlString,
Home: '' as IntlString,
WhatDoYouWantToShare: '' as IntlString,
Share: '' as IntlString,
Compact: '' as IntlString,
Comfortable: '' as IntlString,
Comfortable2: '' as IntlString
}
})
+1
View File
@@ -18,6 +18,7 @@ import { type Heading } from '@hcengineering/text-editor'
interface BaseNavigatorConfig {
types: Array<Ref<MasterTag>>
home?: boolean
groupBySpace?: boolean
savedViews?: boolean
allowCreate?: boolean
+6 -1
View File
@@ -149,7 +149,8 @@ const cardPlugin = plugin(cardId, {
Card: '' as Asset,
File: '' as Asset,
View: '' as Asset,
Document: '' as Asset
Document: '' as Asset,
Home: '' as Asset
},
extensions: {
EditCardExtension: '' as ComponentExtensionId
@@ -184,6 +185,10 @@ const cardPlugin = plugin(cardId, {
},
function: {
OpenCardInSidebar: '' as Resource<(_id: Ref<Card>, card?: Card) => Promise<void>>
},
label: {
Subscribed: '' as Ref<Doc>,
NewMessages: '' as Ref<Doc>
}
})
@@ -30,7 +30,7 @@
import { chatId } from '@hcengineering/chat'
import { Ref } from '@hcengineering/core'
import view from '@hcengineering/view'
import { Favorites } from '@hcengineering/card-resources'
import { Favorites, Home } from '@hcengineering/card-resources'
import ChatNavigation from './ChatNavigation.svelte'
import {
@@ -39,7 +39,9 @@
navigateToType,
getTypeIdFromLocation,
isFavoritesLocation,
navigateToFavorites
navigateToFavorites,
navigateToHome,
isHomeLocation
} from '../location'
import ChatNavigationCategoryList from './ChatNavigationCategoryList.svelte'
@@ -55,6 +57,7 @@
doc: MasterTag
}
| { type: 'favorites' }
| { type: 'home' }
const client = getClient()
const hierarchy = client.getHierarchy()
@@ -74,12 +77,18 @@
const typeId = getTypeIdFromLocation(loc)
const cardId = getCardIdFromLocation(loc)
const isFavorites = isFavoritesLocation(loc)
const isHome = isHomeLocation(loc)
if (isFavorites) {
selection = { type: 'favorites' }
return
}
if (isHome) {
selection = { type: 'home' }
return
}
const type = typeId != null ? await client.findOne(cardPlugin.class.MasterTag, { _id: typeId }) : undefined
if (type != null) {
@@ -127,6 +136,13 @@
navigateToFavorites()
}
function selectHome (): void {
if (selection?.type === 'home') return
closePanel(false)
selection = { type: 'home' }
navigateToHome()
}
function getSelectedCard (selection: Selection | undefined): Card | undefined {
if (selection?.type !== 'card') return undefined
return selection.doc
@@ -164,10 +180,11 @@
<ChatNavigation
card={getSelectedCard(selection)}
type={getSelectedType(selection)}
isFavorites={selection?.type === 'favorites'}
special={selection?.type}
on:selectCard={selectCard}
on:selectType={selectType}
on:favorites={selectFavorites}
on:home={selectHome}
/>
</div>
{#if !($deviceInfo.isMobile && $deviceInfo.isPortrait && $deviceInfo.minWidth)}
@@ -188,6 +205,10 @@
{#key selection.type}
<Favorites application={chatId} />
{/key}
{:else if selection?.type === 'home'}
{#key selection.type}
<Home on:selectCard={selectCard} />
{/key}
{:else if selectedCard}
{@const panelComponent = hierarchy.classHierarchyMixin(selectedCard._class, view.mixin.ObjectPanel)}
{@const comp = panelComponent?.component ?? view.component.EditDoc}
@@ -23,7 +23,17 @@
export let card: Card | undefined = undefined
export let type: Ref<MasterTag> | undefined = undefined
export let isFavorites: boolean = false
export let special: 'home' | 'favorites' | string | undefined = undefined
function getSpecial (special: 'home' | 'favorites' | string | undefined): string | undefined {
if (special === 'favorites') {
return 'favorites'
}
if (special === 'home') {
return 'home'
}
return undefined
}
</script>
<Navigator
@@ -34,6 +44,7 @@
groupBySpace: false,
hideEmpty: true,
limit: 5,
home: true,
labelFilter: [SubscriptionLabelID],
preorder: [
{ type: chat.masterTag.Thread, order: 1 },
@@ -53,8 +64,9 @@
applicationId={chatId}
selectedType={type}
selectedCard={card?._id}
selectedSpecial={isFavorites ? 'favorites' : undefined}
selectedSpecial={getSpecial(special)}
on:selectType
on:selectCard
on:favorites
on:home
/>
+14
View File
@@ -26,6 +26,10 @@ export function isFavoritesLocation (loc: Location): boolean {
return loc.path[2] === chatId && loc.path[3] === 'favorites'
}
export function isHomeLocation (loc: Location): boolean {
return loc.path[2] === chatId && loc.path[3] === 'home'
}
export function getCardIdFromLocation (loc: Location): Ref<Card> | undefined {
if (loc.path[2] !== chatId) {
return undefined
@@ -78,6 +82,16 @@ export function navigateToFavorites (): void {
navigate(loc)
}
export function navigateToHome (): void {
const loc = getCurrentResolvedLocation()
loc.path[2] = chatId
loc.path[3] = 'home'
delete loc.query?.message
navigate(loc)
}
export async function resolveLocation (loc: Location): Promise<ResolvedLocation | undefined> {
if (loc.path[2] !== chatId) {
return undefined
@@ -53,7 +53,9 @@
let avatarSize = size
$: if (size === 'small') {
$: if (size === 'x-small') {
avatarSize = 'tiny'
} else if (size === 'small') {
avatarSize = 'x-small'
} else if (size === 'medium') {
avatarSize = 'small'
@@ -0,0 +1,60 @@
<!-- 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 { LiteMessageViewer } from '@hcengineering/presentation'
import { Card } from '@hcengineering/card'
import { type WithLookup } from '@hcengineering/core'
import { Message, SocialID } from '@hcengineering/communication-types'
import { Person } from '@hcengineering/contact'
import { getEmbeddedLabel, IntlString } from '@hcengineering/platform'
import { employeeByPersonIdStore, getPersonByPersonId } from '@hcengineering/contact-resources'
import { markdownToMarkup } from '@hcengineering/text-markdown'
import { jsonToMarkup, markupToText } from '@hcengineering/text'
import { tooltip } from '@hcengineering/ui'
import { isActivityMessage } from '../activity'
import ActivityMessageViewer from './message/ActivityMessageViewer.svelte'
import AttachmentsPreview from './AttachmentsPreview.svelte'
export let card: Card
export let message: Message
export let colorInherit: boolean = false
const tooltipLimit = 512
let person: WithLookup<Person> | undefined = undefined
$: void updatePerson(message.creator)
function getTooltipLabel (message: Message): IntlString {
const text = markupToText(jsonToMarkup(markdownToMarkup(message.content)))
if (text.length > tooltipLimit) {
return getEmbeddedLabel(text.substring(0, tooltipLimit) + '...')
}
return getEmbeddedLabel(text)
}
async function updatePerson (socialId: SocialID): Promise<void> {
person = $employeeByPersonIdStore.get(socialId) ?? (await getPersonByPersonId(socialId)) ?? undefined
}
</script>
<span class="message-preview overflow-label" use:tooltip={{ label: getTooltipLabel(message) }}>
{#if isActivityMessage(message)}
<ActivityMessageViewer {message} {card} author={person} />
{:else}
<LiteMessageViewer message={markdownToMarkup(message.content)} {colorInherit} />
{/if}
<AttachmentsPreview {message} />
</span>
@@ -33,6 +33,8 @@
export let isEditing = false
export let compact: boolean = false
export let hideAvatar: boolean = false
export let hideHeader: boolean = false
export let thread: boolean = true
function formatDate (date: Date): string {
return date.toLocaleTimeString('default', {
@@ -42,15 +44,17 @@
}
</script>
{#if compact}
{#if compact || hideHeader}
<div class="message__body">
<div class="time-container">
<div class="message__time message--time_hoverable">
<div class="message__date">
{formatDate(message.created)}
{#if !hideHeader}
<div class="time-container">
<div class="message__time message--time_hoverable">
<div class="message__date">
{formatDate(message.created)}
</div>
</div>
</div>
</div>
{/if}
<div class="message__content">
{#if !isEditing && message.content !== ''}
@@ -70,7 +74,7 @@
/>
{/if}
{#if !isEditing}
<MessageFooter {message} />
<MessageFooter {message} {thread} />
{/if}
</div>
</div>
@@ -132,7 +136,7 @@
/>
{/if}
{#if !isEditing}
<MessageFooter {message} />
<MessageFooter {message} {thread} />
{/if}
</div>
</div>
@@ -29,6 +29,7 @@
import communication from '../../plugin'
export let message: Message
export let thread: boolean = true
const me = getCurrentAccount()
const communicationClient = getCommunicationClient()
@@ -130,7 +131,7 @@
<ReactionsList reactions={message.reactions} on:click={handleReaction} />
</div>
{/if}
{#if message.thread && message.thread.threadId}
{#if thread && message.thread && message.thread.threadId}
<div class="message__replies overflow-label">
<MessageThread thread={message.thread} on:click={handleReply} />
</div>
@@ -41,7 +41,9 @@
export let padding: string | undefined = undefined
export let compact: boolean = false
export let hideAvatar: boolean = false
export let hideHeader: boolean = false
export let readonly: boolean = false
export let thread: boolean = true
let isEditing = false
let isDeleted = false
@@ -151,9 +153,18 @@
style:padding
>
{#if message.type === MessageType.Activity || (message.removed && message.thread?.threadId === undefined)}
<OneRowMessageBody {message} {card} {author} {hideAvatar} />
<OneRowMessageBody {message} {card} {author} {hideAvatar} {hideHeader} />
{:else}
<MessageBody {message} {card} {author} {isEditing} compact={compact && !isThread} {hideAvatar} />
<MessageBody
{message}
{card}
{author}
{isEditing}
compact={compact && !isThread}
{hideAvatar}
{hideHeader}
{thread}
/>
{/if}
{#if showActions}
@@ -27,6 +27,7 @@
export let author: Person | undefined
export let message: Message
export let hideAvatar: boolean = false
export let hideHeader: boolean = false
function formatDate (date: Date): string {
return date.toLocaleTimeString('default', {
@@ -51,7 +52,7 @@
{/if}
</div>
{/if}
{#if !isDeleted}
{#if !isDeleted && !hideHeader}
<div class="message__header">
<PersonPreviewProvider value={author}>
<div class="message__username">
@@ -85,7 +85,12 @@
}
.icon {
color: var(--global-primary-TextColor);
fill: var(--global-primary-TextColor);
color: var(--global-secondary-TextColor);
fill: var(--global-secondary-TextColor);
&:hover {
color: var(--global-secondary-TextColor);
fill: var(--global-secondary-TextColor);
}
}
</style>
@@ -48,6 +48,8 @@ export * from './stores'
export { default as MessagePresenter } from './components/message/MessagePresenter.svelte'
export { default as MessageInput } from './components/message/MessageInput.svelte'
export { default as ActivityMessageViewer } from './components/message/ActivityMessageViewer.svelte'
export { default as AttachmentsPreview } from './components/AttachmentsPreview.svelte'
export { default as MessagePreview } from './components/MessagePreview.svelte'
export default async (): Promise<Resources> => ({
component: {
@@ -21,9 +21,8 @@
import { employeeByPersonIdStore, getPersonByPersonId } from '@hcengineering/contact-resources'
import { markdownToMarkup } from '@hcengineering/text-markdown'
import { jsonToMarkup, markupToText } from '@hcengineering/text'
import { ActivityMessageViewer, isActivityMessage } from '@hcengineering/communication-resources'
import { ActivityMessageViewer, isActivityMessage, AttachmentsPreview } from '@hcengineering/communication-resources'
import AttachmentsPreview from './AttachmentsPreview.svelte'
import PreviewTemplate from './PreviewTemplate.svelte'
export let card: Card
@@ -14,17 +14,18 @@
-->
<script lang="ts">
import type { Doc, Ref } from '@hcengineering/core'
import type { Asset } from '@hcengineering/platform'
import type { Asset, IntlString } from '@hcengineering/platform'
import type { Action, IconSize } from '@hcengineering/ui'
import TreeElement from './TreeElement.svelte'
import { AnySvelteComponent } from '@hcengineering/ui'
export let _id: Ref<Doc> | undefined = undefined
export let _id: Ref<Doc> | string | undefined = undefined
export let icon: Asset | AnySvelteComponent | undefined = undefined
export let folderIcon: boolean = false
export let iconProps: Record<string, any> | undefined = undefined
export let iconSize: IconSize = 'small'
export let title: string
export let title: string | undefined = undefined
export let label: IntlString | undefined = undefined
export let notifications = 0
export let actions: (originalEvent?: MouseEvent) => Promise<Action[]> = async () => []
export let selected: boolean = false
@@ -49,6 +50,7 @@
{iconSize}
{iconProps}
{title}
{label}
{notifications}
{selected}
{actions}