Merge branch 'develop' of https://github.com/hcengineering/platform into staging-new

Signed-off-by: Artem Savchenko <armisav@gmail.com>
This commit is contained in:
Artem Savchenko
2025-09-08 09:54:20 +07:00
44 changed files with 561 additions and 118 deletions
+1 -1
View File
@@ -97,7 +97,7 @@
"MODEL_JSON": "${workspaceRoot}/models/all/bundle/model.json",
// "SERVER_PROVIDER":"uweb"
"SERVER_PROVIDER": "ws",
"MODEL_VERSION": "0.7.186",
"MODEL_VERSION": "0.7.230",
// "VERSION": "0.6.289",
"COMMUNICATION_API_ENABLED": "true",
"ELASTIC_INDEX_NAME": "local_storage_index",
+37 -1
View File
@@ -1675,6 +1675,9 @@ importers:
'@types/express-form-data':
specifier: ^2.0.5
version: 2.0.5
'@types/heic-decode':
specifier: ^1.1.2
version: 1.1.2
'@types/html-to-text':
specifier: ^8.1.1
version: 8.1.1
@@ -1813,6 +1816,9 @@ importers:
blurhash:
specifier: ^2.0.5
version: 2.0.5
bmp-ts:
specifier: ^1.0.9
version: 1.0.9
body-parser:
specifier: ^1.20.3
version: 1.20.3
@@ -2020,6 +2026,9 @@ importers:
graphql-binding:
specifier: ^2.5.2
version: 2.5.2
heic-decode:
specifier: ^2.1.0
version: 2.1.0
highlight.js:
specifier: ^11.11.1
version: 11.11.1
@@ -5569,7 +5578,7 @@ packages:
version: 0.0.0
'@rush-temp/pod-preview@file:projects/pod-preview.tgz':
resolution: {integrity: sha512-APb+wu9FGvuc2FCTz9nGyrGZwA4+o5uL6WWPdRhg1zeC9UniDdSA2InqNP03CxK8fayAwTMkTN9BF3C2g3w2lQ==, tarball: file:projects/pod-preview.tgz}
resolution: {integrity: sha512-67MM8HopF5FhImVD91kHzl4UuyYk8WXHYXG+4Wz+jO1py2l/nvW9kNj9A1vPHXfy6clmsAsUMdAlLLLJE7ilmg==, tarball: file:projects/pod-preview.tgz}
version: 0.0.0
'@rush-temp/pod-print@file:projects/pod-print.tgz':
@@ -7162,6 +7171,9 @@ packages:
'@types/hast@3.0.4':
resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==}
'@types/heic-decode@1.1.2':
resolution: {integrity: sha512-E1Dw+gpIuiHJcTpFBHm+V/6eYsPhtWook1Z6MIG0H1ONWBp9Z6l3FKydUxFw9GL9JqrWB1qa/uQHdbn/KUeXYw==}
'@types/html-minifier-terser@6.1.0':
resolution: {integrity: sha512-oh/6byDPnL1zeNXFrDXFLyZjkr1MsBG667IM792caf1L2UPOOMf65NFzjUH/ltyfwjAGfs1rsX1eftK0jC/KIg==}
@@ -8082,6 +8094,9 @@ packages:
bmp-js@0.1.0:
resolution: {integrity: sha512-vHdS19CnY3hwiNdkaqk93DvjVLfbEcI8mys4UjuWrlX1haDmroo8o4xCzh4wD6DGV6HxRCyauwhHRqMTfERtjw==}
bmp-ts@1.0.9:
resolution: {integrity: sha512-cTEHk2jLrPyi+12M3dhpEbnnPOsaZuq7C45ylbbQIiWgDFZq4UVYPEY5mlqjvsj/6gJv9qX5sa+ebDzLXT28Vw==}
bn.js@4.12.0:
resolution: {integrity: sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==}
@@ -10297,6 +10312,10 @@ packages:
resolution: {integrity: sha512-trFMIq3PATiFRiQmNNeHtsrkwYRByIXUbYNbotiY9RLVfMkdwZdd2eQ38mGt7BRiCKBaj1DyBAIHmm7mmXPuuw==}
engines: {node: '>=10.0.0'}
heic-decode@2.1.0:
resolution: {integrity: sha512-0fB3O3WMk38+PScbHLVp66jcNhsZ/ErtQ6u2lMYu/YxXgbBtl+oKOhGQHa4RpvE68k8IzbWkABzHnyAIjR758A==}
engines: {node: '>=8.0.0'}
hexoid@1.0.0:
resolution: {integrity: sha512-QFLV0taWQOZtvIRIAdBChesmogZrtuXvVWsFHZTk2SU+anspqZ2vMnoLg7IE1+Uk16N19APic1BuF8bC8c2m5g==}
engines: {node: '>=8'}
@@ -11285,6 +11304,10 @@ packages:
engines: {node: '>=16'}
hasBin: true
libheif-js@1.19.8:
resolution: {integrity: sha512-vQJWusIxO7wavpON1dusciL8Go9jsIQ+EUrckauFYAiSTjcmLAsuJh3SszLpvkwPci3JcL41ek2n+LUZGFpPIQ==}
engines: {node: '>=8.0.0'}
libphonenumber-js@1.10.56:
resolution: {integrity: sha512-d0GdKshNnyfl5gM7kZ9rXjGiAbxT/zCXp0k+EAzh8H4zrb2R7GXtMCrULrX7UQxtfx6CLy/vz/lomvW79FAFdA==}
@@ -25626,12 +25649,14 @@ snapshots:
dependencies:
'@types/cors': 2.8.17
'@types/express': 4.17.21
'@types/heic-decode': 1.1.2
'@types/jest': 29.5.12
'@types/morgan': 1.9.9
'@types/node': 22.15.29
'@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.8.3))(eslint@8.56.0)(typescript@5.8.3)
'@typescript-eslint/parser': 6.21.0(eslint@8.56.0)(typescript@5.8.3)
blurhash: 2.0.5
bmp-ts: 1.0.9
cors: 2.8.5
cross-env: 7.0.3
dotenv: 16.0.3
@@ -25643,6 +25668,7 @@ snapshots:
eslint-plugin-node: 11.1.0(eslint@8.56.0)
eslint-plugin-promise: 6.1.1(eslint@8.56.0)
express: 4.21.2
heic-decode: 2.1.0
jest: 29.7.0(@types/node@22.15.29)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.8.3))
lru-cache: 11.1.0
morgan: 1.10.0
@@ -32109,6 +32135,8 @@ snapshots:
dependencies:
'@types/unist': 2.0.10
'@types/heic-decode@1.1.2': {}
'@types/html-minifier-terser@6.1.0': {}
'@types/html-to-text@8.1.1': {}
@@ -33246,6 +33274,8 @@ snapshots:
bmp-js@0.1.0: {}
bmp-ts@1.0.9: {}
bn.js@4.12.0: {}
body-parser@1.20.3:
@@ -35944,6 +35974,10 @@ snapshots:
heap-js@2.6.0: {}
heic-decode@2.1.0:
dependencies:
libheif-js: 1.19.8
hexoid@1.0.0: {}
highlight.js@11.11.1: {}
@@ -37214,6 +37248,8 @@ snapshots:
dependencies:
isomorphic.js: 0.2.5
libheif-js@1.19.8: {}
libphonenumber-js@1.10.56: {}
lie@3.3.0:
+7 -3
View File
@@ -189,6 +189,10 @@ function defineTraining (builder: Builder): void {
component: training.component.TrainingPanel
})
builder.mixin(training.class.Training, core.class.Class, view.mixin.ObjectPresenter, {
presenter: training.component.TrainingPresenter
})
builder.createDoc(view.class.Viewlet, core.space.Model, {
attachTo: training.class.Training,
descriptor: view.viewlet.Table,
@@ -201,7 +205,8 @@ function defineTraining (builder: Builder): void {
},
{
...columns.trainingTitle,
key: 'title'
key: '',
presenter: training.component.TrainingTitlePresenter
},
{
...columns.trainingRevision,
@@ -890,8 +895,7 @@ const columns = {
trainingTitle: {
key: 'title',
label: training.string.TrainingTitle,
props: { accent: true },
displayProps: { grow: true, align: 'left' }
displayProps: { align: 'left' }
},
trainingRevision: {
key: 'revision',
+2 -1
View File
@@ -17,7 +17,7 @@ import type { Account, AccountRole, AccountUuid, Doc, Domain, PersonId, Ref } fr
import { type MeasureContext } from '@hcengineering/measurements'
import { type DocumentQuery, type FindOptions } from './storage'
import type { DocumentUpdate, Tx } from './tx'
import { type WorkspaceIds } from './utils'
import { PermissionsGrant, type WorkspaceIds } from './utils'
/**
* @public
@@ -72,6 +72,7 @@ export interface SessionData {
role: AccountRole
}
>
grant?: PermissionsGrant
asyncRequests?: ((ctx: MeasureContext, id?: string) => Promise<void>)[]
}
+4
View File
@@ -990,3 +990,7 @@ export function uniqueNotEmpty<T extends NonNullable<unknown>> (arr: Array<T | u
return unique(arr).filter(notEmpty)
}
export { platformNow, platformNowDiff } from '@hcengineering/measurements'
export interface PermissionsGrant {
spaces?: Ref<Space>[]
}
@@ -155,6 +155,10 @@
>
<Component is={presence.component.Presence} props={{ object }} />
<svelte:fragment slot="beforeTitle">
<slot name="beforeTitle" />
</svelte:fragment>
<svelte:fragment slot="title">
{#if !withoutTitle}
{#if $$slots.title}
@@ -17,7 +17,7 @@
import { createEventDispatcher } from 'svelte'
import ui from '../plugin'
import { showPopup } from '../popups'
import type { ButtonKind, DropdownIntlItem } from '../types'
import type { ButtonKind, ButtonSize, DropdownIntlItem } from '../types'
import Button from './Button.svelte'
import DropdownIcon from './icons/Dropdown.svelte'
import NestedMenu from './NestedMenu.svelte'
@@ -30,6 +30,9 @@
export let width: string | undefined = undefined
export let kind: ButtonKind = 'regular'
export let size: ButtonSize | undefined = undefined
export let withIcon: boolean = false
export let withSelectIcon: boolean = true
let container: HTMLElement
let opened: boolean = false
@@ -39,7 +42,7 @@
function openPopup (): void {
if (!opened) {
opened = true
showPopup(NestedMenu, { items }, container, (result) => {
showPopup(NestedMenu, { items, withIcon }, container, (result) => {
if (result !== undefined) {
selected = result
dispatch('selected', result.id)
@@ -51,12 +54,22 @@
</script>
<div bind:this={container}>
<Button width={width ?? 'min-content'} {kind} {disabled} on:click={openPopup}>
<Button
width={width ?? 'min-content'}
{kind}
{size}
{disabled}
on:click={openPopup}
icon={withIcon ? selected?.icon : undefined}
iconProps={selected?.iconProps}
>
<span slot="content" class="overflow-label disabled flex-grow text-left mr-2">
<Label label={selected !== undefined ? selected.label : label} />
</span>
<svelte:fragment slot="iconRight">
<DropdownIcon size={'small'} fill={!disabled ? 'var(--theme-content-color)' : 'var(--theme-dark-color)'} />
{#if withSelectIcon}
<DropdownIcon size={'small'} fill={!disabled ? 'var(--theme-content-color)' : 'var(--theme-dark-color)'} />
{/if}
</svelte:fragment>
</Button>
</div>
+16 -1
View File
@@ -18,10 +18,12 @@
import { resizeObserver } from '../resize'
import { DropdownIntlItem } from '../types'
import NestedMenu from './NestedMenu.svelte'
import Icon from './Icon.svelte'
export let items: [DropdownIntlItem, DropdownIntlItem[]][]
export let nestedFrom: DropdownIntlItem | undefined = undefined
export let onSelect: ((val: DropdownIntlItem) => void) | undefined = undefined
export let withIcon: boolean = false
const elements: HTMLElement[] = []
@@ -72,6 +74,11 @@
}
}}
>
{#if withIcon && nestedFrom.icon}
<div class="icon">
<Icon icon={nestedFrom.icon} iconProps={nestedFrom.iconProps} size={'small'} />
</div>
{/if}
<div class="overflow-label pr-1"><Label label={nestedFrom.label} /></div>
</button>
<div class="divider" />
@@ -87,12 +94,15 @@
elements[i]?.focus()
}}
label={item[0].label}
icon={withIcon ? item[0].icon : undefined}
iconProps={item[0].iconProps}
props={{
items: item[1].map((p) => {
return [p, []]
}),
nestedFrom: item[0],
onSelect: onNestedSelect
onSelect: onNestedSelect,
withIcon
}}
options={{ component: NestedMenu }}
/>
@@ -110,6 +120,11 @@
click(item[0])
}}
>
{#if withIcon && item[0].icon}
<div class="icon">
<Icon icon={item[0].icon} iconProps={item[0].iconProps} size={'small'} />
</div>
{/if}
<div class="overflow-label pr-1"><Label label={item[0].label} /></div>
</button>
{/if}
+2 -1
View File
@@ -27,6 +27,7 @@
export let focusIndex = -1
export let icon: Asset | AnySvelteComponent | undefined = undefined
export let iconProps: Record<string, any> = {}
export let text: string | undefined = undefined
export let label: IntlString | undefined = undefined
export let labelProps: Record<string, any> = {}
@@ -58,7 +59,7 @@
{/if}
{:else}
{#if icon}
<div class="icon"><Icon {icon} size={'small'} /></div>
<div class="icon"><Icon {icon} {iconProps} size={'small'} /></div>
{/if}
<span class="overflow-label pr-1">
{#if label}<Label {label} params={labelProps} />
+4
View File
@@ -52,4 +52,8 @@
<symbol id="space" viewBox="0 0 32 32">
<path fill-rule="evenodd" clip-rule="evenodd" d="M2 9C2 6.23858 4.23858 4 7 4H10.6716C11.4672 4 12.2303 4.31607 12.7929 4.87868L15.6213 7.70711C15.8089 7.89464 16.0632 8 16.3284 8H25C27.7614 8 30 10.2386 30 13V23C30 25.7614 27.7614 28 25 28H7C4.23858 28 2 25.7614 2 23V9ZM7 6C5.34315 6 4 7.34315 4 9V23C4 24.6569 5.34315 26 7 26H25C26.6569 26 28 24.6569 28 23V13C28 11.3431 26.6569 10 25 10H16.3284C15.5328 10 14.7697 9.68393 14.2071 9.12132L11.3787 6.29289C11.1911 6.10536 10.9368 6 10.6716 6H7Z" fill="currentColor"/>
</symbol>
<symbol id="expand" viewBox="0 0 32 32">
<path d="M20 2V4H26.586L18.293 12.2929C17.9024 12.6834 17.9024 13.3166 18.293 13.7071C18.6835 14.0976 19.3167 14.0976 19.7072 13.7071L28 5.414V12H30V2H20Z"/>
<path d="M13.7073 19.7071C14.0978 19.3166 14.0978 18.6834 13.7073 18.2929C13.3167 17.9024 12.6836 17.9024 12.293 18.2929L4 26.586V20H2V30H12V28H5.414L13.7073 19.7071Z"/>
</symbol>
</svg>

Before

Width:  |  Height:  |  Size: 7.6 KiB

After

Width:  |  Height:  |  Size: 8.0 KiB

+4 -3
View File
@@ -42,10 +42,11 @@
"ConfigDescription": "Rozšíření pro správu znalostí ve stylu databáze.",
"AddCollaborators": "Přidat spolupracovníky",
"Home": "Domů",
"WhatDoYouWantToShare": "Čím chcete sdílet?",
"Share": "Sdílet",
"CardTitle": "Zadejte název karty...",
"Post": "Přidat",
"Compact": "Kompaktní",
"Comfortable": "Komfortní",
"Comfortable2": "Komfortní 2"
"Comfortable2": "Komfortní 2",
"AdvancedCreateCard": "Přepnout na pokročilé vytváření karet"
}
}
+4 -3
View File
@@ -42,10 +42,11 @@
"ConfigDescription": "Erweiterung für Datenbank-basiertes Wissensmanagement.",
"AddCollaborators": "Mitarbeiter hinzufügen",
"Home": "Startseite",
"WhatDoYouWantToShare": "Was möchten Sie teilen?",
"Share": "Teilen",
"CardTitle": "Geben Sie einen Kartentitel ein...",
"Post": "Beitrag",
"Compact": "Kompakt",
"Comfortable": "Komfortabel",
"Comfortable2": "Komfortabel 2"
"Comfortable2": "Komfortabel 2",
"AdvancedCreateCard": "Zum erweiterten Kartenerstellungsmodus wechseln"
}
}
+4 -3
View File
@@ -42,10 +42,11 @@
"ConfigDescription": "Extension for database-style knowledge management.",
"AddCollaborators": "Add collaborators",
"Home": "Home",
"WhatDoYouWantToShare": "What do you want to share?",
"Share": "Share",
"CardTitle": "Enter a card title...",
"Post": "Post",
"Compact": "Compact",
"Comfortable": "Comfortable",
"Comfortable2": "Comfortable 2"
"Comfortable2": "Comfortable 2",
"AdvancedCreateCard": "Switch to advanced card creation"
}
}
+4 -3
View File
@@ -42,10 +42,11 @@
"ConfigDescription": "Extensión para la gestión del conocimiento al estilo de una base de datos.",
"AddCollaborators": "Agregar colaboradores",
"Home": "Inicio",
"WhatDoYouWantToShare": "¿Qué quieres compartir?",
"Share": "Compartir",
"CardTitle": "Introduce un título para la tarjeta...",
"Post": "Publicar",
"Compact": "Compacto",
"Comfortable": "Confortable",
"Comfortable2": "Confortable 2"
"Comfortable2": "Confortable 2",
"AdvancedCreateCard": "Cambiar a creación avanzada de tarjetas"
}
}
+4 -3
View File
@@ -42,10 +42,11 @@
"ConfigDescription": "Extension pour la gestion des connaissances de style base de données.",
"AddCollaborators": "Ajouter des collaborateurs",
"Home": "Accueil",
"WhatDoYouWantToShare": "Que voulez-vous partager ?",
"Share": "Partager",
"CardTitle": "Entrez un titre de carte...",
"Post": "Publier",
"Compact": "Compact",
"Comfortable": "Confortable",
"Comfortable2": "Confortable 2"
"Comfortable2": "Confortable 2",
"AdvancedCreateCard": "Passer à la création avancée de cartes"
}
}
+4 -3
View File
@@ -42,10 +42,11 @@
"ConfigDescription": "Estensione per la gestione della conoscenza in stile database.",
"AddCollaborators": "Aggiungi collaboratori",
"Home": "Home",
"WhatDoYouWantToShare": "Cosa vuoi condividere?",
"Share": "Condividi",
"CardTitle": "Inserisci un titolo per la carta...",
"Post": "Pubblica",
"Compact": "Compatto",
"Comfortable": "Confortabile",
"Comfortable2": "Confortabile 2"
"Comfortable2": "Confortabile 2",
"AdvancedCreateCard": "Passa alla creazione avanzata della carta"
}
}
+4 -3
View File
@@ -42,10 +42,11 @@
"ConfigDescription": "データベーススタイルのナレッジマネジメント用の拡張機能。",
"AddCollaborators": "コラボレーターを追加",
"Home": "ホーム",
"WhatDoYouWantToShare": "どうしますかを共有しますか?",
"Share": "共有",
"CardTitle": "カードのタイトルを入力...",
"Post": "投稿",
"Compact": "コンパクト",
"Comfortable": "コンファンティブ",
"Comfortable2": "コンファンティブ 2"
"Comfortable2": "コンファンティブ 2",
"AdvancedCreateCard": "高度なカード作成に切り替え"
}
}
+4 -3
View File
@@ -42,10 +42,11 @@
"ConfigDescription": "Extensão para gerenciamento de conhecimento no estilo de banco de dados.",
"AddCollaborators": "Adicionar colaboradores",
"Home": "Início",
"WhatDoYouWantToShare": "O que você deseja compartilhar?",
"Share": "Compartilhar",
"CardTitle": "Digite um título para o cartão...",
"Post": "Publicar",
"Compact": "Compacto",
"Comfortable": "Confortável",
"Comfortable2": "Confortável 2"
"Comfortable2": "Confortável 2",
"AdvancedCreateCard": "Mudar para criação avançada de cartão"
}
}
+4 -3
View File
@@ -42,10 +42,11 @@
"ConfigDescription": "Расширение для управления знаниями в стиле базы данных.",
"AddCollaborators": "Добавить сотрудников",
"Home": "Дом",
"WhatDoYouWantToShare": "Чем вы хотите поделиться?",
"Share": "Поделиться",
"CardTitle": "Введите название карты...",
"Post": "Опубликовать",
"Compact": "Компактный",
"Comfortable": "Комфортный",
"Comfortable2": "Комфортный 2"
"Comfortable2": "Комфортный 2",
"AdvancedCreateCard": "Переключиться на расширенное создание карты"
}
}
+4 -3
View File
@@ -42,10 +42,11 @@
"ConfigDescription": "用于数据库风格知识管理的扩展。",
"AddCollaborators": "添加协作者",
"Home": "主页",
"WhatDoYouWantToShare": "您想分享什么?",
"Share": "分享",
"CardTitle": "输入卡片标题...",
"Post": "发布",
"Compact": "紧凑",
"Comfortable": "舒适",
"Comfortable2": "舒适 2"
"Comfortable2": "舒适 2",
"AdvancedCreateCard": "切换到高级卡片创建"
}
}
+2 -1
View File
@@ -25,5 +25,6 @@ loadMetadata(card.icon, {
View: `${icons}#view`,
Document: `${icons}#document`,
Home: `${icons}#home`,
Space: `${icons}#space`
Space: `${icons}#space`,
Expand: `${icons}#expand`
})
@@ -32,6 +32,7 @@
export let type: Ref<MasterTag> = 'chat:masterTag:Thread' as Ref<MasterTag>
export let space: CardSpace | undefined = undefined
export let changeType: boolean = false
export let allowChangeSpace: boolean = true
const dispatch = createEventDispatcher()
const client = getClient()
@@ -183,7 +184,7 @@
<TypeSelector bind:value={type} />
</div>
{/if}
{#if space == null && !(extension?.hideSpace ?? false)}
{#if (space == null || allowChangeSpace) && !(extension?.hideSpace ?? false)}
<div class="hulyModal-content__settingsSet-line">
<span class="label"><Label label={core.string.Space} /></span>
<SpaceSelector
@@ -38,6 +38,7 @@
import { NotificationContext } from '@hcengineering/communication-types'
import card from '../plugin'
import CardIcon from './CardIcon.svelte'
import TagsEditor from './TagsEditor.svelte'
import EditCardNewContent from './EditCardNewContent.svelte'
import { openCardInSidebar } from '../utils'
@@ -165,6 +166,10 @@
<EditCardNewContent {_id} {doc} readonly={_readonly} {context} {isContextLoaded} />
</div>
<svelte:fragment slot="beforeTitle">
<CardIcon value={doc} />
</svelte:fragment>
<svelte:fragment slot="title">
{#if showParents}
<ParentsNavigator element={doc} maxWidth={'10rem'} />
@@ -37,6 +37,7 @@
import card from '../plugin'
import CardAttributeEditor from './CardAttributeEditor.svelte'
import CardIcon from './CardIcon.svelte'
import CardPresenter from './CardPresenter.svelte'
import Childs from './Childs.svelte'
import Content from './Content.svelte'
@@ -139,6 +140,10 @@
on:open
on:close={() => dispatch('close')}
>
<svelte:fragment slot="beforeTitle">
<CardIcon value={doc} />
</svelte:fragment>
<svelte:fragment slot="title">
<ParentsNavigator element={doc} />
<CardPresenter value={doc} noUnderline />
@@ -11,7 +11,7 @@
<!-- See the License for the specific language governing permissions and -->
<!-- limitations under the License. -->
<script lang="ts">
import { createQuery, getClient } from '@hcengineering/presentation'
import { createQuery } from '@hcengineering/presentation'
import { Card } from '@hcengineering/card'
import core, { SortingOrder } from '@hcengineering/core'
import ui, {
@@ -19,25 +19,21 @@
IconSettings,
Label,
ModernButton,
ModernEditbox,
Scroller,
SearchInput,
Loading,
showPopup
} from '@hcengineering/ui'
import { FilterBar, FilterButton } from '@hcengineering/view-resources'
import { createEventDispatcher } from 'svelte'
import HomeCardPresenter from './HomeCardPresenter.svelte'
import HomeSettings from './HomeSettings.svelte'
import CreateCardPopup from './CreateCardPopup.svelte'
import NewCardForm from './NewCardForm.svelte'
import card from '../plugin'
const cardsQuery = createQuery()
const limitStep = 50
const dispatch = createEventDispatcher()
let divScroll: HTMLDivElement
let limit = limitStep
let cards: Card[] = []
@@ -111,19 +107,6 @@
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">
@@ -145,28 +128,7 @@
space={undefined}
on:change={({ detail }) => (resultQuery = detail)}
/>
<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>
<NewCardForm />
<div class="body flex-gap-2">
{#each cards as card, index}
{@const previousCard = cards[index - 1]}
@@ -211,12 +173,6 @@
flex: 1;
}
.create-card {
display: flex;
width: 100%;
margin: 1rem 0;
}
.header {
display: flex;
width: 100%;
@@ -0,0 +1,178 @@
<script lang="ts">
import { getClient, SpaceSelector } from '@hcengineering/presentation'
import { ModernButton, ModernEditbox, ButtonIcon, IconSend, showPopup } from '@hcengineering/ui'
import { createEventDispatcher } from 'svelte'
import CreateCardPopup from './CreateCardPopup.svelte'
import card from '../plugin'
import { TypeSelector } from '../index'
import core, { Data, Ref } from '@hcengineering/core'
import { Card, type CardSpace, MasterTag } from '@hcengineering/card'
import { getResource } from '@hcengineering/platform'
import { EmptyMarkup } from '@hcengineering/text'
import { createCard } from '../utils'
const dispatch = createEventDispatcher()
let title: string = ''
let space: Ref<CardSpace> | undefined = undefined
let type: Ref<MasterTag> = 'chat:masterTag:Thread' as Ref<MasterTag>
let creating = false
$: applyDisabled = title.trim() === '' || space == null || type == null || creating
const client = getClient()
const hierarchy = client.getHierarchy()
$: extension =
type != null
? client
.getModel()
.findAllSync(card.mixin.CreateCardExtension, {})
.find((it) => hierarchy.isDerived(type, it._id))
: undefined
function onExtend (): void {
showPopup(
CreateCardPopup,
{ title, type, space, changeType: true, allowChangeSpace: 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 = ''
}
async function okAction (): Promise<void> {
if (space === undefined || type == null || title.trim() === '') return
try {
creating = true
const data: Partial<Data<Card>> = {
title
}
if (extension?.canCreate !== undefined) {
const fn = await getResource(extension.canCreate)
const res = await fn(space, data)
if (res === false) {
return
} else if (typeof res === 'string') {
return
}
}
const createdCard = await createCard(type, space, data, EmptyMarkup)
if (createdCard != null) {
dispatch('selectCard', createdCard)
}
title = ''
} finally {
creating = false
}
}
</script>
<div class="create-card rounded-form">
<div class="corner-container">
<ButtonIcon
icon={card.icon.Expand}
size="extra-small"
on:click={onExtend}
kind="tertiary"
disabled={creating}
tooltip={{ label: card.string.AdvancedCreateCard }}
/>
</div>
<div class="form-row">
<ModernEditbox
bind:value={title}
label={card.string.CardTitle}
size="medium"
kind="ghost"
width="100%"
disabled={creating}
autoFocus={true}
on:keydown={(evt) => {
if (evt.key === 'Enter') {
void okAction()
}
}}
/>
</div>
<div class="form-row form-row-bottom">
<div class="form-dropdowns">
<SpaceSelector
_class={card.class.CardSpace}
query={{ archived: false }}
label={core.string.Space}
bind:space
focus={false}
readonly={creating}
kind={'regular'}
size={'small'}
/>
<TypeSelector size={'small'} bind:value={type} disabled={creating} />
</div>
<ModernButton
label={card.string.Post}
icon={IconSend}
iconSize="small"
size="small"
kind="primary"
disabled={applyDisabled}
loading={creating}
on:click={okAction}
/>
</div>
</div>
<style lang="scss">
.corner-container {
position: absolute;
top: 0.75rem;
right: 1rem;
z-index: 1;
display: flex;
align-items: center;
justify-content: flex-end;
pointer-events: auto;
}
.create-card {
display: flex;
flex-direction: column;
width: 100%;
margin: 1rem 0;
border-radius: 1rem;
border: 1px solid var(--theme-divider-color);
background: var(--theme-surface-color);
box-shadow: 0 2px 8px 0 rgba(0, 0, 0, 0.04);
padding: 1rem;
position: relative;
}
.rounded-form {
border-radius: 1rem;
}
.form-row {
display: flex;
width: 100%;
margin-bottom: 1rem;
}
.form-row-bottom {
align-items: center;
justify-content: space-between;
margin-bottom: 0;
gap: 1rem;
}
.form-dropdowns {
display: flex;
gap: 0.75rem;
flex: 1;
}
</style>
@@ -15,13 +15,17 @@
<script lang="ts">
import { MasterTag } from '@hcengineering/card'
import { Class, ClassifierKind, Doc, Ref } from '@hcengineering/core'
import { getClient } from '@hcengineering/presentation'
import { DropdownIntlItem, NestedDropdown } from '@hcengineering/ui'
import { getClient, IconWithEmoji } from '@hcengineering/presentation'
import { type ButtonKind, type ButtonSize, DropdownIntlItem, NestedDropdown } from '@hcengineering/ui'
import { createEventDispatcher } from 'svelte'
import card from '../plugin'
import view from '@hcengineering/view'
export let value: Ref<MasterTag>
export let width: string | undefined = undefined
export let kind: ButtonKind | undefined = undefined
export let size: ButtonSize | undefined = undefined
export let disabled: boolean = false
const client = getClient()
const hierarchy = client.getHierarchy()
@@ -55,21 +59,31 @@
try {
const clazz = hierarchy.getClass(key)
result.push([
{ id: key, label: clazz.label },
value.map((it) => ({ id: it._id, label: it.label })).sort((a, b) => a.label.localeCompare(b.label))
{ id: key, label: clazz.label, ...getIconProps(clazz) },
value
.map((it) => ({ id: it._id, label: it.label, ...getIconProps(it) }))
.sort((a, b) => a.label.localeCompare(b.label))
])
} catch {}
}
return result
}
function getIconProps (tag: MasterTag): Pick<DropdownIntlItem, 'icon' | 'iconProps'> {
return {
icon: tag.icon === view.ids.IconWithEmoji ? IconWithEmoji : tag.icon ?? card.icon.MasterTag,
iconProps: tag.icon === view.ids.IconWithEmoji ? { icon: tag.color } : {}
}
}
const classes = filterClasses()
const dispatch = createEventDispatcher()
$: selectedClass = hierarchy.getClass(value)
$: selected = {
id: selectedClass._id,
label: selectedClass.label
label: selectedClass.label,
...getIconProps(selectedClass)
}
</script>
@@ -77,6 +91,11 @@
items={classes}
{width}
{selected}
{kind}
{size}
{disabled}
withIcon={true}
withSelectIcon={false}
on:selected={(e) => {
value = e.detail
dispatch('change', value)
+4 -3
View File
@@ -102,10 +102,11 @@ export default mergeIds(cardId, card, {
NoChildren: '' as IntlString,
AddCollaborators: '' as IntlString,
Home: '' as IntlString,
WhatDoYouWantToShare: '' as IntlString,
Share: '' as IntlString,
CardTitle: '' as IntlString,
Post: '' as IntlString,
Compact: '' as IntlString,
Comfortable: '' as IntlString,
Comfortable2: '' as IntlString
Comfortable2: '' as IntlString,
AdvancedCreateCard: '' as IntlString
}
})
+2 -1
View File
@@ -151,7 +151,8 @@ const cardPlugin = plugin(cardId, {
View: '' as Asset,
Document: '' as Asset,
Home: '' as Asset,
Space: '' as Asset
Space: '' as Asset,
Expand: '' as Asset
},
extensions: {
EditCardExtension: '' as ComponentExtensionId
@@ -85,7 +85,7 @@
<Card
okAction={save}
canSave={to !== undefined && trigger !== undefined && to !== from}
canSave={to !== undefined && trigger !== undefined}
label={plugin.string.AddTransition}
width={'medium'}
on:close
@@ -0,0 +1,22 @@
<!--
Copyright © 2023 Hardcore Engineering Inc.
-->
<script lang="ts">
import { type Training } from '@hcengineering/training'
import { DocNavLink } from '@hcengineering/view-resources'
export let value: Training
export let disabled: boolean = false
export let accent: boolean = false
</script>
{#if value}
<div class="content-halfcontent-color">
<DocNavLink object={value} {disabled} noOverflow accent>
<span class="whitespace-nowrap" class:fs-bold={accent}>
{value.title}
</span>
</DocNavLink>
</div>
{/if}
+4
View File
@@ -28,6 +28,7 @@ import TrainingCodePresenter from './components/TrainingCodePresenter.svelte'
import TrainingCreator from './components/TrainingCreator.svelte'
import TrainingPanel from './components/TrainingPanel.svelte'
import TrainingPassingScorePresenter from './components/TrainingPassingScorePresenter.svelte'
import TrainingPresenter from './components/TrainingPresenter.svelte'
import TrainingRequestDueDateEditor from './components/TrainingRequestDueDateEditor.svelte'
import TrainingRequestMaxAttemptsEditor from './components/TrainingRequestMaxAttemptsEditor.svelte'
import TrainingRequestMaxAttemptsPresenter from './components/TrainingRequestMaxAttemptsPresenter.svelte'
@@ -36,6 +37,7 @@ import TrainingRequestPanel from './components/TrainingRequestPanel.svelte'
import TrainingRequestTraineesEditor from './components/TrainingRequestTraineesEditor.svelte'
import TrainingStateFilterPresenter from './components/TrainingStateFilterPresenter.svelte'
import TrainingStatePresenter from './components/TrainingStatePresenter.svelte'
import TrainingTitlePresenter from './components/TrainingTitlePresenter.svelte'
import ViewAllTrainings from './components/ViewAllTrainings.svelte'
import ViewIncomingRequests from './components/ViewIncomingRequests.svelte'
import ViewMyResults from './components/ViewMyResults.svelte'
@@ -76,6 +78,7 @@ export default async (): Promise<Resources> => ({
TrainingCreator,
TrainingPanel,
TrainingPassingScorePresenter,
TrainingPresenter,
TrainingRequestDueDateEditor,
TrainingRequestMaxAttemptsEditor,
TrainingRequestMaxAttemptsPresenter,
@@ -84,6 +87,7 @@ export default async (): Promise<Resources> => ({
TrainingRequestTraineesEditor,
TrainingStateFilterPresenter,
TrainingStatePresenter,
TrainingTitlePresenter,
ViewAllTrainings,
ViewIncomingRequests,
ViewMyTrainings,
+4
View File
@@ -21,6 +21,7 @@ import type IncomingRequestStatePresenter from './components/IncomingRequestStat
import type SentRequestCompletionPresenter from './components/SentRequestCompletionPresenter.svelte'
import type SentRequestPresenter from './components/SentRequestPresenter.svelte'
import type SentRequestStatePresenter from './components/SentRequestStatePresenter.svelte'
import type TrainingPresenter from './components/TrainingPresenter.svelte'
import type TrainingAttemptNumberPresenter from './components/TrainingAttemptNumberPresenter.svelte'
import type TrainingAttemptPanel from './components/TrainingAttemptPanel.svelte'
import type TrainingAttemptPresenter from './components/TrainingAttemptPresenter.svelte'
@@ -40,6 +41,7 @@ import type TrainingRequestPanel from './components/TrainingRequestPanel.svelte'
import type TrainingRequestTraineesEditor from './components/TrainingRequestTraineesEditor.svelte'
import type TrainingStateFilterPresenter from './components/TrainingStateFilterPresenter.svelte'
import type TrainingStatePresenter from './components/TrainingStatePresenter.svelte'
import type TrainingTitlePresenter from './components/TrainingTitlePresenter.svelte'
import type ViewAllTrainings from './components/ViewAllTrainings.svelte'
import type ViewIncomingRequests from './components/ViewIncomingRequests.svelte'
import type ViewMyResults from './components/ViewMyResults.svelte'
@@ -56,6 +58,7 @@ export default mergeIds(trainingId, training, {
SentRequestCompletionPresenter: '' as Resource<ComponentType<SentRequestCompletionPresenter>>,
SentRequestPresenter: '' as Resource<ComponentType<SentRequestPresenter>>,
SentRequestStatePresenter: '' as Resource<ComponentType<SentRequestStatePresenter>>,
TrainingPresenter: '' as Resource<ComponentType<TrainingPresenter>>,
TrainingAttemptNumberPresenter: '' as Resource<ComponentType<TrainingAttemptNumberPresenter>>,
TrainingAttemptPanel: '' as Resource<ComponentType<TrainingAttemptPanel>>,
TrainingAttemptPresenter: '' as Resource<ComponentType<TrainingAttemptPresenter>>,
@@ -74,6 +77,7 @@ export default mergeIds(trainingId, training, {
TrainingRequestTraineesEditor: '' as Resource<ComponentType<TrainingRequestTraineesEditor>>,
TrainingStateFilterPresenter: '' as Resource<ComponentType<TrainingStateFilterPresenter>>,
TrainingStatePresenter: '' as Resource<ComponentType<TrainingStatePresenter>>,
TrainingTitlePresenter: '' as Resource<ComponentType<TrainingTitlePresenter>>,
ViewAllTrainings: '' as Resource<ComponentType<ViewAllTrainings>>,
ViewIncomingRequests: '' as Resource<ComponentType<ViewIncomingRequests>>,
ViewMyTrainings: '' as Resource<ComponentType<ViewMyTrainings>>,
+5 -2
View File
@@ -51,7 +51,8 @@
"typescript": "^5.8.3",
"@types/express": "^4.17.13",
"@types/cors": "^2.8.12",
"@types/morgan": "~1.9.9"
"@types/morgan": "~1.9.9",
"@types/heic-decode": "^1.1.2"
},
"dependencies": {
"@hcengineering/core": "^0.6.32",
@@ -69,6 +70,8 @@
"cors": "^2.8.5",
"morgan": "^1.10.0",
"node-poppler": "^8.0.4",
"lru-cache": "^11.1.0"
"lru-cache": "^11.1.0",
"heic-decode": "^2.1.0",
"bmp-ts": "^1.0.9"
}
}
+16
View File
@@ -22,6 +22,8 @@ import { pipeline } from 'stream/promises'
import { getImageMetadata } from '../metadata'
import { TemporaryDir } from '../tempdir'
import { type PreviewFile, type PreviewMetadata, type PreviewProvider } from '../types'
import { bmpToPng } from '../utils/bmp'
import { heicToPng } from '../utils/heic'
export class ImageProvider implements PreviewProvider {
constructor (
@@ -41,6 +43,20 @@ export class ImageProvider implements PreviewProvider {
await pipeline(stream, createWriteStream(path))
})
// Handle HEIC conversion
if (contentType === 'image/heic' || contentType === 'image/heif') {
const pngFile = await heicToPng(ctx, path)
this.tempDir.rm(path)
return { mimeType: 'image/png', filePath: pngFile }
}
// Handle BMP conversion
if (contentType === 'image/bmp') {
const pngFile = await bmpToPng(ctx, path)
this.tempDir.rm(path)
return { mimeType: 'image/png', filePath: pngFile }
}
return { mimeType: contentType, filePath: path }
}
+55
View File
@@ -0,0 +1,55 @@
//
// 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 { type MeasureContext } from '@hcengineering/core'
import { readFile } from 'fs/promises'
import { decode } from 'bmp-ts'
import sharp from 'sharp'
export async function bmpToPng (ctx: MeasureContext, bmpFile: string): Promise<string> {
const pngFile: string = bmpFile + '.png'
const buffer = await readFile(bmpFile)
const data = await ctx.with('bmp-decode', {}, () => {
return decode(Buffer.from(buffer), { toRGBA: false })
})
// Manually convert to RGBA as library does not seem to handle this properly
for (let i = 0; i < data.data.length; i += 4) {
// const a = data.data[i + 0]
const b = data.data[i + 1]
const g = data.data[i + 2]
const r = data.data[i + 3]
data.data[i + 0] = r
data.data[i + 1] = g
data.data[i + 2] = b
data.data[i + 3] = 0xff
}
await ctx.with('sharp', {}, async () => {
await sharp(data.data, {
raw: {
width: data.width,
height: data.height,
channels: 4
}
})
.png()
.toFile(pngFile)
})
return pngFile
}
+46
View File
@@ -0,0 +1,46 @@
//
// 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 { type MeasureContext } from '@hcengineering/core'
import { readFile } from 'fs/promises'
import decodeHeic from 'heic-decode'
import sharp from 'sharp'
export async function heicToPng (ctx: MeasureContext, heicFile: string): Promise<string> {
const pngFile: string = heicFile + '.png'
const buffer = await readFile(heicFile)
const { data, width, height } = await ctx.with('heic-decode', {}, () =>
decodeHeic({
// @ts-expect-error wrong types
buffer: new Uint8Array(buffer)
})
)
await ctx.with('sharp', {}, async () => {
await sharp(data, {
raw: {
width,
height,
channels: 4
}
})
.png()
.toFile(pngFile)
})
return pngFile
}
+21 -6
View File
@@ -111,16 +111,26 @@ export async function OnEmployeeCreate (_txes: Tx[], control: TriggerControl): P
const emp = control.hierarchy.as(person, contact.mixin.Employee)
if (emp.role === 'GUEST') {
let readOnlyGuestSpaces: Space[] = []
const readonlyEmployees = await control.findAll(control.ctx, contact.mixin.Employee, {
personUuid: readOnlyGuestAccountUuid
})
if (readonlyEmployees.length === 0) continue
if (readonlyEmployees.length !== 0) {
const readonlyEmployee = readonlyEmployees[0]
if (readonlyEmployee.active) {
readOnlyGuestSpaces = await control.findAll(control.ctx, core.class.Space, {
members: readOnlyGuestAccountUuid
})
}
}
const readonlyEmployee = readonlyEmployees[0]
if (!readonlyEmployee.active) continue
const grantSpacesRefs = control.ctx.contextData.grant?.spaces
const grantSpaces =
grantSpacesRefs !== undefined
? await control.findAll(control.ctx, core.class.Space, { _id: { $in: grantSpacesRefs } })
: []
const spaces = await control.findAll(control.ctx, core.class.Space, { members: readOnlyGuestAccountUuid })
for (const space of spaces) {
for (const space of [...readOnlyGuestSpaces, ...grantSpaces]) {
if (space._class === contact.class.PersonSpace || space.members.includes(account)) continue
const pushTx = systemTxFactory.createTxUpdateDoc(space._class, space.space, space._id, {
@@ -149,7 +159,12 @@ export async function OnEmployeeCreate (_txes: Tx[], control: TriggerControl): P
}
const spaces = await control.findAll(control.ctx, core.class.Space, { autoJoin: true })
for (const space of spaces) {
const grantSpacesRefs = control.ctx.contextData.grant?.spaces
const grantSpaces =
grantSpacesRefs !== undefined
? await control.findAll(control.ctx, core.class.Space, { _id: { $in: grantSpacesRefs } })
: []
for (const space of [...spaces, ...grantSpaces]) {
if (space.members.includes(account)) continue
const pushTx = systemTxFactory.createTxUpdateDoc(space._class, space.space, space._id, {
+4 -2
View File
@@ -611,6 +611,7 @@ export async function selectWorkspace (
let accountUuid: AccountUuid
let extra: Record<string, any> | undefined
let grant: PermissionsGrant | undefined
try {
const decodedToken = decodeTokenVerbose(ctx, token ?? '')
accountUuid = decodedToken.account
@@ -618,6 +619,7 @@ export async function selectWorkspace (
workspace = await getWorkspaceById(db, decodedToken.workspace)
}
extra = decodedToken.extra
grant = decodedToken.grant
} catch (e) {
if (workspace?.allowReadOnlyGuest === true) {
accountUuid = readOnlyGuestAccountUuid
@@ -666,7 +668,7 @@ export async function selectWorkspace (
if (accountUuid === systemAccountUuid) {
return {
account: accountUuid,
token: generateToken(accountUuid, workspace.uuid, extra),
token: generateToken(accountUuid, workspace.uuid, extra, undefined, grant),
endpoint: getEndpoint(workspace.uuid, workspace.region, getKind(workspace.region)),
workspace: workspace.uuid,
workspaceUrl: workspace.url,
@@ -723,7 +725,7 @@ export async function selectWorkspace (
return {
account: accountUuid,
token: generateToken(accountUuid, workspace.uuid, extra),
token: generateToken(accountUuid, workspace.uuid, extra, undefined, grant),
endpoint: getEndpoint(workspace.uuid, workspace.region, getKind(workspace.region)),
workspace: workspace.uuid,
workspaceUrl: workspace.url,
+1 -2
View File
@@ -101,8 +101,7 @@ export async function loadCollabJson (
}
if (!blob.contentType.includes('application/json')) {
ctx.error('invalid content type', { contentType: blob.contentType })
return undefined
ctx.warn('invalid content type', { contentType: blob.contentType })
}
const buffer = await storageAdapter.read(ctx, wsIds, blobId)
+4 -2
View File
@@ -31,7 +31,8 @@ import core, {
type Tx,
type TxResult,
type TxWorkspaceEvent,
type WorkspaceIds
type WorkspaceIds,
type PermissionsGrant
} from '@hcengineering/core'
import { PlatformError, unknownError } from '@hcengineering/platform'
import { createHash, type Hash } from 'crypto'
@@ -180,7 +181,8 @@ export class SessionDataImpl implements SessionData {
role: AccountRole
}
>,
readonly service: string
readonly service: string,
readonly grant?: PermissionsGrant
) {
this._removedMap = _removedMap
this._contextCache = _contextCache
+2 -1
View File
@@ -234,7 +234,8 @@ export class TriggersMiddleware extends BaseMiddleware implements Middleware {
sctx.contextCache,
this.context.modelDb,
sctx.socialStringsToUsers,
sctx.service
sctx.service,
sctx.grant
)
ctx.contextData = asyncContextData
const aresult = await this.triggers.apply(
+13 -1
View File
@@ -16,6 +16,7 @@
import type { LoginInfoWithWorkspaces } from '@hcengineering/account-client'
import {
generateId,
type PermissionsGrant,
TxProcessor,
type Account,
type AccountUuid,
@@ -141,6 +142,16 @@ export class ClientSession implements Session {
return await ctx.ctx.with('load-model', {}, (_ctx) => ctx.pipeline.loadModel(_ctx, lastModelTx, hash))
}
private getPermissionsGrant (): PermissionsGrant | undefined {
if (this.token.grant == null) {
return
}
return {
spaces: this.token.grant?.spaces
}
}
includeSessionContext (ctx: ClientSessionCtx): void {
const dataId = this.workspace.dataId ?? (this.workspace.uuid as unknown as WorkspaceDataId)
const contextData = new SessionDataImpl(
@@ -157,7 +168,8 @@ export class ClientSession implements Session {
undefined,
ctx.pipeline.context.modelDb,
ctx.socialStringsToUsers,
this.token.extra?.service ?? '🤦‍♂️user'
this.token.extra?.service ?? '🤦‍♂️user',
this.getPermissionsGrant()
)
ctx.ctx.contextData = contextData
}
+4 -1
View File
@@ -1,4 +1,4 @@
import { AccountRole, AccountUuid, MeasureContext, PersonUuid, WorkspaceUuid } from '@hcengineering/core'
import { AccountRole, AccountUuid, MeasureContext, PersonUuid, Ref, Space, WorkspaceUuid } from '@hcengineering/core'
import { getMetadata } from '@hcengineering/platform'
import { decode, encode } from 'jwt-simple'
import { validate } from 'uuid'
@@ -22,6 +22,8 @@ export interface PermissionsGrant {
firstName?: string
lastName?: string
spaces?: Ref<Space>[]
extra?: Record<string, any>
}
@@ -71,6 +73,7 @@ export function generateToken (
role: grant.role,
firstName: grant.firstName,
lastName: grant.lastName,
spaces: grant.spaces,
extra: grant.extra
}
: undefined