diff --git a/.vscode/launch.json b/.vscode/launch.json index e7ad3953d3..3c237f20d0 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -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", diff --git a/common/config/rush/pnpm-lock.yaml b/common/config/rush/pnpm-lock.yaml index 7ee56d3b07..4a798d6f9d 100644 --- a/common/config/rush/pnpm-lock.yaml +++ b/common/config/rush/pnpm-lock.yaml @@ -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: diff --git a/models/training/src/index.ts b/models/training/src/index.ts index cb539af8e0..82980172d8 100644 --- a/models/training/src/index.ts +++ b/models/training/src/index.ts @@ -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', diff --git a/packages/core/src/server.ts b/packages/core/src/server.ts index 2af3be288d..a1515008ed 100644 --- a/packages/core/src/server.ts +++ b/packages/core/src/server.ts @@ -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)[] } diff --git a/packages/core/src/utils.ts b/packages/core/src/utils.ts index c06a476409..21809cfe2f 100644 --- a/packages/core/src/utils.ts +++ b/packages/core/src/utils.ts @@ -990,3 +990,7 @@ export function uniqueNotEmpty> (arr: Array[] +} diff --git a/packages/panel/src/components/Panel.svelte b/packages/panel/src/components/Panel.svelte index 53d931e913..e87244e632 100644 --- a/packages/panel/src/components/Panel.svelte +++ b/packages/panel/src/components/Panel.svelte @@ -155,6 +155,10 @@ > + + + + {#if !withoutTitle} {#if $$slots.title} diff --git a/packages/ui/src/components/NestedDropdown.svelte b/packages/ui/src/components/NestedDropdown.svelte index 1bcadc5764..b13771f5d2 100644 --- a/packages/ui/src/components/NestedDropdown.svelte +++ b/packages/ui/src/components/NestedDropdown.svelte @@ -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 @@
-
diff --git a/packages/ui/src/components/NestedMenu.svelte b/packages/ui/src/components/NestedMenu.svelte index 2029044340..40e3f50042 100644 --- a/packages/ui/src/components/NestedMenu.svelte +++ b/packages/ui/src/components/NestedMenu.svelte @@ -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} +
+ +
+ {/if}
@@ -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} +
+ +
+ {/if}
{/if} diff --git a/packages/ui/src/components/Submenu.svelte b/packages/ui/src/components/Submenu.svelte index af3bfa8ef8..cd8ad7e2f9 100644 --- a/packages/ui/src/components/Submenu.svelte +++ b/packages/ui/src/components/Submenu.svelte @@ -27,6 +27,7 @@ export let focusIndex = -1 export let icon: Asset | AnySvelteComponent | undefined = undefined + export let iconProps: Record = {} export let text: string | undefined = undefined export let label: IntlString | undefined = undefined export let labelProps: Record = {} @@ -58,7 +59,7 @@ {/if} {:else} {#if icon} -
+
{/if} {#if label}
{/if} - {#if space == null && !(extension?.hideSpace ?? false)} + {#if (space == null || allowChangeSpace) && !(extension?.hideSpace ?? false)}
+ + + + {#if showParents} diff --git a/plugins/card-resources/src/components/EditCardOld.svelte b/plugins/card-resources/src/components/EditCardOld.svelte index dfa4cc2a8f..fa76e6a2d0 100644 --- a/plugins/card-resources/src/components/EditCardOld.svelte +++ b/plugins/card-resources/src/components/EditCardOld.svelte @@ -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')} > + + + + diff --git a/plugins/card-resources/src/components/Home.svelte b/plugins/card-resources/src/components/Home.svelte index e75a91bc1a..cbcefc89a4 100644 --- a/plugins/card-resources/src/components/Home.svelte +++ b/plugins/card-resources/src/components/Home.svelte @@ -11,7 +11,7 @@ @@ -145,28 +128,7 @@ space={undefined} on:change={({ detail }) => (resultQuery = detail)} /> -
- - - - - -
- +
{#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%; diff --git a/plugins/card-resources/src/components/NewCardForm.svelte b/plugins/card-resources/src/components/NewCardForm.svelte new file mode 100644 index 0000000000..68b2a17edb --- /dev/null +++ b/plugins/card-resources/src/components/NewCardForm.svelte @@ -0,0 +1,178 @@ + + +
+
+ +
+
+ { + if (evt.key === 'Enter') { + void okAction() + } + }} + /> +
+
+
+ + +
+ +
+
+ + diff --git a/plugins/card-resources/src/components/TypeSelector.svelte b/plugins/card-resources/src/components/TypeSelector.svelte index b6417be59a..2d461fadb0 100644 --- a/plugins/card-resources/src/components/TypeSelector.svelte +++ b/plugins/card-resources/src/components/TypeSelector.svelte @@ -15,13 +15,17 @@ @@ -77,6 +91,11 @@ items={classes} {width} {selected} + {kind} + {size} + {disabled} + withIcon={true} + withSelectIcon={false} on:selected={(e) => { value = e.detail dispatch('change', value) diff --git a/plugins/card-resources/src/plugin.ts b/plugins/card-resources/src/plugin.ts index 1d35b665cf..84ce31fd8d 100644 --- a/plugins/card-resources/src/plugin.ts +++ b/plugins/card-resources/src/plugin.ts @@ -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 } }) diff --git a/plugins/card/src/index.ts b/plugins/card/src/index.ts index 21c7699455..8002996f0e 100644 --- a/plugins/card/src/index.ts +++ b/plugins/card/src/index.ts @@ -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 diff --git a/plugins/process-resources/src/components/settings/AddTransitionPopup.svelte b/plugins/process-resources/src/components/settings/AddTransitionPopup.svelte index 0fe072b29b..3da14b0bd0 100644 --- a/plugins/process-resources/src/components/settings/AddTransitionPopup.svelte +++ b/plugins/process-resources/src/components/settings/AddTransitionPopup.svelte @@ -85,7 +85,7 @@ + + + +{#if value} +
+ + + {value.title} + + +
+{/if} diff --git a/plugins/training-resources/src/index.ts b/plugins/training-resources/src/index.ts index 53a920bcd2..5532886810 100644 --- a/plugins/training-resources/src/index.ts +++ b/plugins/training-resources/src/index.ts @@ -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 => ({ TrainingCreator, TrainingPanel, TrainingPassingScorePresenter, + TrainingPresenter, TrainingRequestDueDateEditor, TrainingRequestMaxAttemptsEditor, TrainingRequestMaxAttemptsPresenter, @@ -84,6 +87,7 @@ export default async (): Promise => ({ TrainingRequestTraineesEditor, TrainingStateFilterPresenter, TrainingStatePresenter, + TrainingTitlePresenter, ViewAllTrainings, ViewIncomingRequests, ViewMyTrainings, diff --git a/plugins/training-resources/src/plugin.ts b/plugins/training-resources/src/plugin.ts index 5488a0ac99..5286717704 100644 --- a/plugins/training-resources/src/plugin.ts +++ b/plugins/training-resources/src/plugin.ts @@ -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>, SentRequestPresenter: '' as Resource>, SentRequestStatePresenter: '' as Resource>, + TrainingPresenter: '' as Resource>, TrainingAttemptNumberPresenter: '' as Resource>, TrainingAttemptPanel: '' as Resource>, TrainingAttemptPresenter: '' as Resource>, @@ -74,6 +77,7 @@ export default mergeIds(trainingId, training, { TrainingRequestTraineesEditor: '' as Resource>, TrainingStateFilterPresenter: '' as Resource>, TrainingStatePresenter: '' as Resource>, + TrainingTitlePresenter: '' as Resource>, ViewAllTrainings: '' as Resource>, ViewIncomingRequests: '' as Resource>, ViewMyTrainings: '' as Resource>, diff --git a/pods/preview/package.json b/pods/preview/package.json index 863bb9d913..314c4ae5ec 100644 --- a/pods/preview/package.json +++ b/pods/preview/package.json @@ -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" } } diff --git a/pods/preview/src/providers/image.ts b/pods/preview/src/providers/image.ts index d686760efb..f3359e01ef 100644 --- a/pods/preview/src/providers/image.ts +++ b/pods/preview/src/providers/image.ts @@ -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 } } diff --git a/pods/preview/src/utils/bmp.ts b/pods/preview/src/utils/bmp.ts new file mode 100644 index 0000000000..3af66ab80d --- /dev/null +++ b/pods/preview/src/utils/bmp.ts @@ -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 { + 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 +} diff --git a/pods/preview/src/utils/heic.ts b/pods/preview/src/utils/heic.ts new file mode 100644 index 0000000000..4661dc6e79 --- /dev/null +++ b/pods/preview/src/utils/heic.ts @@ -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 { + 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 +} diff --git a/server-plugins/contact-resources/src/index.ts b/server-plugins/contact-resources/src/index.ts index dc09805e59..04b2c6bf47 100644 --- a/server-plugins/contact-resources/src/index.ts +++ b/server-plugins/contact-resources/src/index.ts @@ -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, { diff --git a/server/account/src/utils.ts b/server/account/src/utils.ts index 7946a46191..d79535c09f 100644 --- a/server/account/src/utils.ts +++ b/server/account/src/utils.ts @@ -611,6 +611,7 @@ export async function selectWorkspace ( let accountUuid: AccountUuid let extra: Record | 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, diff --git a/server/collaboration/src/storage.ts b/server/collaboration/src/storage.ts index 20a84d4b32..e0dc833f3d 100644 --- a/server/collaboration/src/storage.ts +++ b/server/collaboration/src/storage.ts @@ -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) diff --git a/server/core/src/utils.ts b/server/core/src/utils.ts index 9d02bcc1e7..a3a59eb8ff 100644 --- a/server/core/src/utils.ts +++ b/server/core/src/utils.ts @@ -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 diff --git a/server/middleware/src/triggers.ts b/server/middleware/src/triggers.ts index cd4a8be867..51fb2b95f2 100644 --- a/server/middleware/src/triggers.ts +++ b/server/middleware/src/triggers.ts @@ -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( diff --git a/server/server/src/client.ts b/server/server/src/client.ts index 6381ef9d0a..5acace68b3 100644 --- a/server/server/src/client.ts +++ b/server/server/src/client.ts @@ -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 } diff --git a/server/token/src/token.ts b/server/token/src/token.ts index 32f6364815..b3f7914c24 100644 --- a/server/token/src/token.ts +++ b/server/token/src/token.ts @@ -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[] + extra?: Record } @@ -71,6 +73,7 @@ export function generateToken ( role: grant.role, firstName: grant.firstName, lastName: grant.lastName, + spaces: grant.spaces, extra: grant.extra } : undefined