From 2d102207ee8e97c25957076c4459d51aaac092e6 Mon Sep 17 00:00:00 2001 From: Kristina Date: Thu, 21 Nov 2024 20:03:27 +0400 Subject: [PATCH 01/10] Fix meetings (#7213) Signed-off-by: Kristina Fefelova --- common/scripts/each-diff.sh | 2 +- models/love/src/index.ts | 51 +++++-- models/view/src/plugin.ts | 1 + .../calendar/DateRangePresenter.svelte | 10 +- .../src/components/EditMeetingMinutes.svelte | 12 +- .../src/components/EditRoom.svelte | 8 +- .../components/MeetingMinutesDocEditor.svelte | 2 +- .../MeetingMinutesStatusPresenter.svelte | 2 +- .../components/RoomLanguageSelector.svelte | 1 + plugins/love-resources/src/utils.ts | 143 ++++++++++-------- .../src/components/DatePresenter.svelte | 9 +- .../src/components/DateTimePresenter.svelte | 38 +++++ .../src/components/EditDoc.svelte | 4 +- .../src/components/Table.svelte | 15 +- plugins/view-resources/src/index.ts | 2 + 15 files changed, 201 insertions(+), 99 deletions(-) create mode 100644 plugins/view-resources/src/components/DateTimePresenter.svelte diff --git a/common/scripts/each-diff.sh b/common/scripts/each-diff.sh index 35b367f18b..8fc1d9dada 100755 --- a/common/scripts/each-diff.sh +++ b/common/scripts/each-diff.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash -FILES=$(git diff origin/main --name-only --diff-filter=ACMR | sed 's| |\\ |g') +FILES=$(git diff origin/develop --name-only --diff-filter=ACMR | sed 's| |\\ |g') [ -z "$FILES" ] && exit 0 roots=$(rush list -p --json | grep "path" | cut -f 2 -d ':' | cut -f 2 -d '"') diff --git a/models/love/src/index.ts b/models/love/src/index.ts index 567edb65c4..0467e74658 100644 --- a/models/love/src/index.ts +++ b/models/love/src/index.ts @@ -16,14 +16,15 @@ import contact, { type Employee, type Person } from '@hcengineering/contact' import { AccountRole, + type CollaborativeDoc, + type CollectionSize, + DateRangeMode, + type Doc, type Domain, DOMAIN_TRANSIENT, IndexKind, type Ref, - type CollaborativeDoc, - type Doc, - type Timestamp, - type CollectionSize + type Timestamp } from '@hcengineering/core' import { type DevicesPreference, @@ -32,16 +33,16 @@ import { type JoinRequest, loveId, type Meeting, + type MeetingMinutes, + type MeetingStatus, type Office, type ParticipantInfo, type RequestStatus, type Room, type RoomAccess, type RoomInfo, - type RoomType, type RoomLanguage, - type MeetingMinutes, - type MeetingStatus + type RoomType } from '@hcengineering/love' import { type Builder, @@ -53,18 +54,18 @@ import { Model, Prop, ReadOnly, + TypeAny, TypeCollaborativeDoc, + TypeDate, TypeRef, TypeString, - TypeTimestamp, - UX, - TypeAny + UX } from '@hcengineering/model' import calendar, { TEvent } from '@hcengineering/model-calendar' import core, { TAttachedDoc, TDoc } from '@hcengineering/model-core' import preference, { TPreference } from '@hcengineering/model-preference' import presentation from '@hcengineering/model-presentation' -import view, { createAction } from '@hcengineering/model-view' +import view, { createAction, createAttributePresenter } from '@hcengineering/model-view' import notification from '@hcengineering/notification' import { getEmbeddedLabel } from '@hcengineering/platform' import setting from '@hcengineering/setting' @@ -228,12 +229,12 @@ export class TMeetingMinutes extends TAttachedDoc implements MeetingMinutes, Tod @Prop(PropCollection(chunter.class.ChatMessage), activity.string.Messages) messages?: number - @Prop(TypeTimestamp(), love.string.MeetingStart, { editor: view.component.TimestampPresenter }) + @Prop(TypeDate(DateRangeMode.DATETIME), love.string.MeetingStart, { editor: view.component.DateTimePresenter }) @ReadOnly() @Index(IndexKind.IndexedDsc) declare createdOn: Timestamp - @Prop(TypeTimestamp(), love.string.MeetingEnd) + @Prop(TypeDate(DateRangeMode.DATETIME), love.string.MeetingEnd, { editor: view.component.DateTimePresenter }) @ReadOnly() meetingEnd?: Timestamp @@ -506,10 +507,10 @@ export function createModel (builder: Builder): void { config: [ '', { key: 'status', presenter: love.component.MeetingMinutesStatusPresenter, label: love.string.Status }, - 'createdOn', - 'meetingEnd', { key: 'messages', displayProps: { key: 'messages', suffix: true } }, - { key: 'transcription', displayProps: { key: 'transcription', suffix: true } } + { key: 'transcription', displayProps: { key: 'transcription', suffix: true } }, + 'createdOn', + 'meetingEnd' ], configOptions: { hiddenKeys: ['description'], @@ -529,10 +530,13 @@ export function createModel (builder: Builder): void { config: [ '', { key: 'status', presenter: love.component.MeetingMinutesStatusPresenter, label: love.string.Status }, + { key: 'messages', displayProps: { key: 'messages', suffix: true } }, + { key: 'transcription', displayProps: { key: 'transcription', suffix: true } }, 'createdOn', 'meetingEnd' ], configOptions: { + hiddenKeys: ['description'], sortable: true }, variant: 'embedded' @@ -637,4 +641,19 @@ export function createModel (builder: Builder): void { builder.mixin(love.class.MeetingMinutes, core.class.Class, view.mixin.ObjectPanelFooter, { editor: love.component.PanelControlBar }) + + createAttributePresenter( + builder, + view.component.DateTimePresenter, + love.class.MeetingMinutes, + 'createdOn', + 'attribute' + ) + createAttributePresenter( + builder, + view.component.DateTimePresenter, + love.class.MeetingMinutes, + 'meetingEnd', + 'attribute' + ) } diff --git a/models/view/src/plugin.ts b/models/view/src/plugin.ts index c1f25d7a67..3c0b6a2bf7 100644 --- a/models/view/src/plugin.ts +++ b/models/view/src/plugin.ts @@ -64,6 +64,7 @@ export default mergeIds(viewId, view, { TimestampPresenter: '' as AnyComponent, DateEditor: '' as AnyComponent, DatePresenter: '' as AnyComponent, + DateTimePresenter: '' as AnyComponent, TableBrowser: '' as AnyComponent, RolePresenter: '' as AnyComponent, YoutubePresenter: '' as AnyComponent, diff --git a/packages/ui/src/components/calendar/DateRangePresenter.svelte b/packages/ui/src/components/calendar/DateRangePresenter.svelte index 1a5226be91..8f73ecc74f 100644 --- a/packages/ui/src/components/calendar/DateRangePresenter.svelte +++ b/packages/ui/src/components/calendar/DateRangePresenter.svelte @@ -478,9 +478,11 @@ {/if} {:else} -
- -
+ {#if shouldShowAvatar} +
+ +
+ {/if} {#if value !== undefined && value !== null && value.toString() !== ''} {#if withDate} {new Date(value).getDate()} @@ -524,7 +526,7 @@ border-radius: 0.375rem; transition-property: border, background-color, color, box-shadow; transition-duration: 0.15s; - cursor: pointer; + cursor: default; &.noPadding { padding: 0; diff --git a/plugins/love-resources/src/components/EditMeetingMinutes.svelte b/plugins/love-resources/src/components/EditMeetingMinutes.svelte index 4b9569a024..e99d438368 100644 --- a/plugins/love-resources/src/components/EditMeetingMinutes.svelte +++ b/plugins/love-resources/src/components/EditMeetingMinutes.svelte @@ -26,8 +26,16 @@ const client = getClient() const dispatch = createEventDispatcher() + let currentTitle = object.title + let newTitle = object.title + + $: if (object.title !== currentTitle) { + newTitle = object.title + currentTitle = object.title + } + async function changeTitle (): Promise { - await client.update(object, { title: object.title }) + await client.diffUpdate(object, { title: newTitle }) } onMount(() => { @@ -41,7 +49,7 @@ diff --git a/plugins/love-resources/src/components/EditRoom.svelte b/plugins/love-resources/src/components/EditRoom.svelte index 1483194cb5..8dbdc824b8 100644 --- a/plugins/love-resources/src/components/EditRoom.svelte +++ b/plugins/love-resources/src/components/EditRoom.svelte @@ -21,7 +21,7 @@ import { IntlString } from '@hcengineering/platform' import love from '../plugin' - import { getRoomName, tryConnect } from '../utils' + import { getRoomName, tryConnect, isConnected } from '../utils' import { infos, invites, myInfo, myRequests, selectedRoomPlace, myOffice, currentRoom } from '../stores' export let object: Room @@ -61,7 +61,11 @@ selectedRoomPlace.set(undefined) } - let connectLabel: IntlString = love.string.StartMeeting + $: connecting = connecting || ($currentRoom?._id === object._id && !$isConnected) + + let connectLabel: IntlString = $infos.some(({ room }) => room === object._id) + ? love.string.JoinMeeting + : love.string.StartMeeting $: if ($infos.some(({ room }) => room === object._id) && !connecting) { connectLabel = love.string.JoinMeeting diff --git a/plugins/love-resources/src/components/MeetingMinutesDocEditor.svelte b/plugins/love-resources/src/components/MeetingMinutesDocEditor.svelte index 0f33ec2e5d..d3ba1c66af 100644 --- a/plugins/love-resources/src/components/MeetingMinutesDocEditor.svelte +++ b/plugins/love-resources/src/components/MeetingMinutesDocEditor.svelte @@ -33,7 +33,7 @@ {#if doc} - + {#if data} - + {/if} diff --git a/plugins/love-resources/src/components/RoomLanguageSelector.svelte b/plugins/love-resources/src/components/RoomLanguageSelector.svelte index d21634b9c1..4e0e527c60 100644 --- a/plugins/love-resources/src/components/RoomLanguageSelector.svelte +++ b/plugins/love-resources/src/components/RoomLanguageSelector.svelte @@ -30,6 +30,7 @@ let container: HTMLElement let selectedItem: RoomLanguage = room.language + $: selectedItem = room.language let items: DropdownIntlItem[] = [] $: items = Object.entries(languagesDisplayData).map(([lang, data]) => ({ diff --git a/plugins/love-resources/src/utils.ts b/plugins/love-resources/src/utils.ts index 49ffd92888..f906e1bd98 100644 --- a/plugins/love-resources/src/utils.ts +++ b/plugins/love-resources/src/utils.ts @@ -12,7 +12,9 @@ import core, { makeCollaborativeDoc, type Ref, type Space, - type TxOperations + type TxOperations, + type Hierarchy, + type Doc } from '@hcengineering/core' import login from '@hcengineering/login' import { @@ -79,7 +81,7 @@ import { import { type Widget, type WidgetTab } from '@hcengineering/workbench' import view from '@hcengineering/view' import chunter from '@hcengineering/chunter' -import { openDoc } from '@hcengineering/view-resources' +import { getObjectLinkFragment } from '@hcengineering/view-resources' import { sendMessage } from './broadcast' import love from './plugin' @@ -494,6 +496,20 @@ function closeMeetingMinutes (): void { currentMeetingMinutes.set(undefined) } +function isRoomOpened (room: Room): boolean { + const loc = getCurrentLocation() + + if (loc.path[2] === loveId) { + const panel = get(panelstore).panel + const { _id } = panel ?? {} + + if (_id !== undefined && room._id !== undefined && _id === room._id) { + return true + } + } + return false +} + export async function setCam (value: boolean): Promise { if (value && get(currentRoom)?.type !== RoomType.Video) return if ($isCurrentInstanceConnected) { @@ -604,13 +620,9 @@ async function moveToRoom ( sessionId }) } - const loc = getCurrentLocation() - if (room.type === RoomType.Video && loc.path[2] !== loveId) { - loc.path[2] = loveId - loc.path.length = 3 - loc.fragment = undefined - loc.query = undefined - navigate(loc) + + if (!isRoomOpened(room)) { + await navigateToOfficeDoc(client.getHierarchy(), room) } } @@ -622,63 +634,70 @@ async function connectLK (currentPerson: Person, room: Room): Promise { ]) } +async function navigateToOfficeDoc (hierarchy: Hierarchy, object: Doc): Promise { + const panelComponent = hierarchy.classHierarchyMixin(object._class, view.mixin.ObjectPanel) + const comp = panelComponent?.component ?? view.component.EditDoc + const loc = await getObjectLinkFragment(hierarchy, object, {}, comp) + loc.path[2] = loveId + loc.path.length = 3 + loc.query = undefined + navigate(loc) +} + async function openMeetingMinutes (room: Room): Promise { const client = getClient() const sid = await lk.getSid() + const doc = await client.findOne(love.class.MeetingMinutes, { sid }) - if (sid !== undefined) { - const doc = await client.findOne(love.class.MeetingMinutes, { sid }) - - if (doc === undefined) { - const date = new Date() - .toLocaleDateString('en-GB', { - day: 'numeric', - month: 'long', - year: 'numeric', - hour: '2-digit', - minute: '2-digit', - hour12: false, - timeZone: 'UTC' - }) - .replace(',', ' at') - const _id = generateId() - const newDoc: MeetingMinutes = { - _id, - _class: love.class.MeetingMinutes, - sid, - attachedTo: room._id, - attachedToClass: room._class, - collection: 'meetings', - space: core.space.Workspace, - title: `${getRoomName(room, get(personByIdStore))} ${date}`, - description: makeCollaborativeDoc(_id, 'description'), - status: MeetingStatus.Active, - modifiedBy: getCurrentAccount()._id, - modifiedOn: Date.now() - } - await client.addCollection( - love.class.MeetingMinutes, - core.space.Workspace, - room._id, - room._class, - 'meetings', - { sid, title: newDoc.title, description: newDoc.description, status: newDoc.status }, - _id - ) - currentMeetingMinutes.set(newDoc) - const loc = getCurrentLocation() - if (loc.path[2] === loveId) { - await openDoc(client.getHierarchy(), newDoc) - } - } else { - currentMeetingMinutes.set(doc) - const loc = getCurrentLocation() - if (loc.path[2] === loveId) { - await openDoc(client.getHierarchy(), doc) - } - if (doc.status !== MeetingStatus.Active) { - void client.update(doc, { status: MeetingStatus.Active, meetingEnd: undefined }) - } + if (doc === undefined) { + const date = new Date() + .toLocaleDateString('en-GB', { + day: 'numeric', + month: 'long', + year: 'numeric', + hour: '2-digit', + minute: '2-digit', + hour12: false, + timeZone: 'UTC' + }) + .replace(',', ' at') + const _id = generateId() + const newDoc: MeetingMinutes = { + _id, + _class: love.class.MeetingMinutes, + sid, + attachedTo: room._id, + attachedToClass: room._class, + collection: 'meetings', + space: core.space.Workspace, + title: `${getRoomName(room, get(personByIdStore))} ${date}`, + description: makeCollaborativeDoc(_id, 'description'), + status: MeetingStatus.Active, + modifiedBy: getCurrentAccount()._id, + modifiedOn: Date.now() + } + await client.addCollection( + love.class.MeetingMinutes, + core.space.Workspace, + room._id, + room._class, + 'meetings', + { sid, title: newDoc.title, description: newDoc.description, status: newDoc.status }, + _id + ) + currentMeetingMinutes.set(newDoc) + const loc = getCurrentLocation() + if (loc.path[2] === loveId || room.type === RoomType.Video) { + await navigateToOfficeDoc(client.getHierarchy(), newDoc) + } + } else { + currentMeetingMinutes.set(doc) + const loc = getCurrentLocation() + if (loc.path[2] === loveId || room.type === RoomType.Video) { + await navigateToOfficeDoc(client.getHierarchy(), doc) + } + if (doc.status !== MeetingStatus.Active) { + void client.update(doc, { status: MeetingStatus.Active, meetingEnd: undefined }) } } } diff --git a/plugins/view-resources/src/components/DatePresenter.svelte b/plugins/view-resources/src/components/DatePresenter.svelte index 7890bc6d18..daa3d2854c 100644 --- a/plugins/view-resources/src/components/DatePresenter.svelte +++ b/plugins/view-resources/src/components/DatePresenter.svelte @@ -15,18 +15,23 @@ --> -{#if onChange !== undefined} +{#if onChange !== undefined && !readonly} onChange?.(e.detail)} {shouldShowAvatar} @@ -34,5 +39,5 @@ {inline} /> {:else} - + {/if} diff --git a/plugins/view-resources/src/components/DateTimePresenter.svelte b/plugins/view-resources/src/components/DateTimePresenter.svelte new file mode 100644 index 0000000000..da84c3e115 --- /dev/null +++ b/plugins/view-resources/src/components/DateTimePresenter.svelte @@ -0,0 +1,38 @@ + + + + diff --git a/plugins/view-resources/src/components/EditDoc.svelte b/plugins/view-resources/src/components/EditDoc.svelte index ab3082b50f..2e7758a0a5 100644 --- a/plugins/view-resources/src/components/EditDoc.svelte +++ b/plugins/view-resources/src/components/EditDoc.svelte @@ -98,7 +98,9 @@ $: if (_class !== oldClass) { oldClass = _class + realObjectClass = _class mainEditor = undefined + fieldEditors = [] } let keys: KeyedAttribute[] = [] @@ -289,7 +291,7 @@ - {#if headerEditor !== undefined} + {#if headerEditor !== undefined && object._id === _id} { - const readonlyParams = readonly - ? { - readonly: true, - editable: false, - disabled: true - } - : {} + const readonlyParams = + readonly || (attribute?.attribute?.readonly ?? false) + ? { + readonly: true, + editable: false, + disabled: true + } + : {} if (attribute.collectionAttr) { return { object, ...attribute.props, ...readonlyParams } } diff --git a/plugins/view-resources/src/index.ts b/plugins/view-resources/src/index.ts index bdc437bc11..bb099c3cc9 100644 --- a/plugins/view-resources/src/index.ts +++ b/plugins/view-resources/src/index.ts @@ -31,6 +31,7 @@ import CollaborativeHTMLEditor from './components/CollaborativeHTMLEditor.svelte import ColorsPopup from './components/ColorsPopup.svelte' import DateEditor from './components/DateEditor.svelte' import DatePresenter from './components/DatePresenter.svelte' +import DateTimePresenter from './components/DateTimePresenter.svelte' import DocAttributeBar from './components/DocAttributeBar.svelte' import DocNavLink from './components/DocNavLink.svelte' import DocReferencePresenter from './components/DocReferencePresenter.svelte' @@ -257,6 +258,7 @@ export default async (): Promise => ({ TimestampPresenter, DateEditor, DatePresenter, + DateTimePresenter, RolePresenter, ObjectPresenter, EditDoc, From 500a221cebf6b93ff52417ab53c6cd3eef87b12c Mon Sep 17 00:00:00 2001 From: Alexander Onnikov Date: Fri, 22 Nov 2024 01:19:33 +0700 Subject: [PATCH 02/10] fix: datalake issues (#7217) Signed-off-by: Alexander Onnikov --- server/datalake/src/client.ts | 53 ++++++++++++++++++++++------------- server/datalake/src/error.ts | 35 +++++++++++++++++++++++ server/datalake/src/index.ts | 8 +++--- workers/datalake/src/blob.ts | 2 +- workers/datalake/src/index.ts | 6 +++- 5 files changed, 79 insertions(+), 25 deletions(-) create mode 100644 server/datalake/src/error.ts diff --git a/server/datalake/src/client.ts b/server/datalake/src/client.ts index cadaeea1db..ae3defb46b 100644 --- a/server/datalake/src/client.ts +++ b/server/datalake/src/client.ts @@ -18,6 +18,8 @@ import FormData from 'form-data' import fetch, { type RequestInit, type Response } from 'node-fetch' import { Readable } from 'stream' +import { DatalakeError, NetworkError, NotFoundError } from './error' + /** @public */ export interface ObjectMetadata { lastModified: number @@ -62,14 +64,16 @@ export class Client { let response try { response = await fetchSafe(ctx, url) - } catch (err) { - console.error('failed to get object', { workspace, objectName, err }) + } catch (err: any) { + if (err.name !== 'NotFoundError') { + console.error('failed to get object', { workspace, objectName, err }) + } throw err } if (response.body == null) { ctx.error('bad datalake response', { objectName }) - throw new Error('Missing response body') + throw new DatalakeError('Missing response body') } return Readable.from(response.body) @@ -90,14 +94,16 @@ export class Client { let response try { response = await fetchSafe(ctx, url, { headers }) - } catch (err) { - console.error('failed to get partial object', { workspace, objectName, err }) + } catch (err: any) { + if (err.name !== 'NotFoundError') { + console.error('failed to get partial object', { workspace, objectName, err }) + } throw err } if (response.body == null) { ctx.error('bad datalake response', { objectName }) - throw new Error('Missing response body') + throw new DatalakeError('Missing response body') } return Readable.from(response.body) @@ -113,7 +119,10 @@ export class Client { let response: Response try { response = await fetchSafe(ctx, url, { method: 'HEAD' }) - } catch (err) { + } catch (err: any) { + if (err.name === 'NotFoundError') { + return + } console.error('failed to stat object', { workspace, objectName, err }) throw err } @@ -134,9 +143,11 @@ export class Client { const url = this.getObjectUrl(ctx, workspace, objectName) try { await fetchSafe(ctx, url, { method: 'DELETE' }) - } catch (err) { - console.error('failed to delete object', { workspace, objectName, err }) - throw err + } catch (err: any) { + if (err.name !== 'NotFoundError') { + console.error('failed to delete object', { workspace, objectName, err }) + throw err + } } } @@ -187,7 +198,7 @@ export class Client { const form = new FormData() const options: FormData.AppendOptions = { - filename: encodeURIComponent(objectName), + filename: objectName, contentType: metadata.type, knownLength: metadata.size, header: { @@ -200,13 +211,13 @@ export class Client { const result = (await response.json()) as BlobUploadResult[] if (result.length !== 1) { - throw new Error('Bad datalake response: ' + result.toString()) + throw new DatalakeError('Bad datalake response: ' + result.toString()) } const uploadResult = result[0] if ('error' in uploadResult) { - throw new Error('Upload failed: ' + uploadResult.error) + throw new DatalakeError('Upload failed: ' + uploadResult.error) } } @@ -232,7 +243,7 @@ export class Client { } catch (err) { ctx.error('failed to upload via signed url', { workspace, objectName, err }) await this.signObjectDelete(ctx, workspace, objectName) - throw new Error('Failed to upload via signed URL') + throw new DatalakeError('Failed to upload via signed URL') } await this.signObjectComplete(ctx, workspace, objectName) @@ -245,7 +256,7 @@ export class Client { return await response.text() } catch (err: any) { ctx.error('failed to sign object', { workspace, objectName, err }) - throw new Error('Failed to sign URL') + throw new DatalakeError('Failed to sign URL') } } @@ -255,7 +266,7 @@ export class Client { await fetchSafe(ctx, url, { method: 'PUT' }) } catch (err: any) { ctx.error('failed to complete signed url upload', { workspace, objectName, err }) - throw new Error('Failed to complete signed URL upload') + throw new DatalakeError('Failed to complete signed URL upload') } } @@ -265,7 +276,7 @@ export class Client { await fetchSafe(ctx, url, { method: 'DELETE' }) } catch (err: any) { ctx.error('failed to abort signed url upload', { workspace, objectName, err }) - throw new Error('Failed to abort signed URL upload') + throw new DatalakeError('Failed to abort signed URL upload') } } @@ -281,12 +292,16 @@ async function fetchSafe (ctx: MeasureContext, url: string, init?: RequestInit): response = await fetch(url, init) } catch (err: any) { ctx.error('network error', { err }) - throw new Error(`Network error ${err}`) + throw new NetworkError(`Network error ${err}`) } if (!response.ok) { const text = await response.text() - throw new Error(response.status === 404 ? 'Not Found' : 'HTTP error ' + response.status + ': ' + text) + if (response.status === 404) { + throw new NotFoundError(text) + } else { + throw new DatalakeError(text) + } } return response diff --git a/server/datalake/src/error.ts b/server/datalake/src/error.ts new file mode 100644 index 0000000000..b5b60262b1 --- /dev/null +++ b/server/datalake/src/error.ts @@ -0,0 +1,35 @@ +// +// Copyright © 2024 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. +// + +export class NetworkError extends Error { + constructor (message: string) { + super(message) + this.name = 'NetworkError' + } +} + +export class DatalakeError extends Error { + constructor (message: string) { + super(message) + this.name = 'DatalakeError' + } +} + +export class NotFoundError extends DatalakeError { + constructor (message = 'Not Found') { + super(message) + this.name = 'NotFoundError' + } +} diff --git a/server/datalake/src/index.ts b/server/datalake/src/index.ts index 7c2e88c884..c5f641dd11 100644 --- a/server/datalake/src/index.ts +++ b/server/datalake/src/index.ts @@ -35,9 +35,11 @@ export interface DatalakeConfig extends StorageConfig { */ export class DatalakeService implements StorageAdapter { static config = 'datalake' - client: Client + private readonly client: Client + constructor (readonly opt: DatalakeConfig) { - this.client = new Client(opt.endpoint) + const endpoint = Number.isInteger(opt.port) ? `${opt.endpoint}:${opt.port}` : opt.endpoint + this.client = new Client(endpoint) } async initialize (ctx: MeasureContext, workspaceId: WorkspaceId): Promise {} @@ -97,8 +99,6 @@ export class DatalakeService implements StorageAdapter { modifiedOn: result.lastModified, version: null } - } else { - ctx.error('no object found', { objectName, workspaceId: workspaceId.name }) } } catch (err) { ctx.error('failed to stat object', { error: err, objectName, workspaceId: workspaceId.name }) diff --git a/workers/datalake/src/blob.ts b/workers/datalake/src/blob.ts index 2c637b8ae5..b58c51d380 100644 --- a/workers/datalake/src/blob.ts +++ b/workers/datalake/src/blob.ts @@ -85,7 +85,7 @@ export async function handleBlobHead (request: BlobRequest, env: Env, ctx: Execu const { bucket } = selectStorage(env, workspace) const blob = await db.getBlob(sql, { workspace, name }) - if (blob === null) { + if (blob === null || blob.deleted) { return error(404) } diff --git a/workers/datalake/src/index.ts b/workers/datalake/src/index.ts index 83940825ac..bc27f83249 100644 --- a/workers/datalake/src/index.ts +++ b/workers/datalake/src/index.ts @@ -40,10 +40,14 @@ const withWorkspace: RequestHandler = (request: WorkspaceReque } const withBlob: RequestHandler = (request: BlobRequest) => { + if (request.params.workspace === undefined || request.params.workspace === '') { + return error(400, 'Missing workspace') + } if (request.params.name === undefined || request.params.name === '') { return error(400, 'Missing blob name') } - request.workspace = decodeURIComponent(request.params.name) + request.workspace = decodeURIComponent(request.params.workspace) + request.name = decodeURIComponent(request.params.name) } router From 1b415d6a6f751f6685db6bf3ca962f9baef5d820 Mon Sep 17 00:00:00 2001 From: Kristina Date: Thu, 21 Nov 2024 22:34:31 +0400 Subject: [PATCH 03/10] Meeting fixes (#7218) 1 Signed-off-by: Kristina Fefelova --- .../src/components/EditRoom.svelte | 21 +++++-- .../src/components/RoomPreview.svelte | 21 ++++--- .../components/widget/MeetingWidget.svelte | 20 ++++--- plugins/love-resources/src/utils.ts | 56 ++++++++++++++----- .../ai-bot/pod-ai-bot/src/workspace/love.ts | 5 +- 5 files changed, 87 insertions(+), 36 deletions(-) diff --git a/plugins/love-resources/src/components/EditRoom.svelte b/plugins/love-resources/src/components/EditRoom.svelte index 8dbdc824b8..4a5d3a0304 100644 --- a/plugins/love-resources/src/components/EditRoom.svelte +++ b/plugins/love-resources/src/components/EditRoom.svelte @@ -45,8 +45,10 @@ dispatch('open', { ignoreKeys: ['name'] }) }) + let tryConnecting = false + async function connect (): Promise { - connecting = true + tryConnecting = true const place = $selectedRoomPlace await tryConnect( $personByIdStore, @@ -57,11 +59,11 @@ $invites, place?._id === object._id ? { x: place.x, y: place.y } : undefined ) - connecting = false + tryConnecting = false selectedRoomPlace.set(undefined) } - $: connecting = connecting || ($currentRoom?._id === object._id && !$isConnected) + $: connecting = tryConnecting || ($currentRoom?._id === object._id && !$isConnected) let connectLabel: IntlString = $infos.some(({ room }) => room === object._id) ? love.string.JoinMeeting @@ -72,6 +74,17 @@ } else if (!connecting) { connectLabel = love.string.StartMeeting } + + function showConnectionButton (object: Room, isConnected: boolean, myOffice?: Room, currentRoom?: Room): boolean { + // Do not show connect button in my office + if (object._id === myOffice?._id) return false + // Show during connecting with spinner + if (connecting) return true + // Do not show connect button if we are already connected to the room + if (isConnected && currentRoom?._id === object._id) return false + + return true + }
@@ -85,7 +98,7 @@ focusIndex={1} />
- {#if object._id !== $myOffice?._id && ($currentRoom?._id !== object._id || connecting)} + {#if showConnectionButton(object, $isConnected, $myOffice, $currentRoom)} {/if} diff --git a/plugins/love-resources/src/components/RoomPreview.svelte b/plugins/love-resources/src/components/RoomPreview.svelte index a1783be146..4a5fd4297e 100644 --- a/plugins/love-resources/src/components/RoomPreview.svelte +++ b/plugins/love-resources/src/components/RoomPreview.svelte @@ -16,16 +16,15 @@ import { Person, type PersonAccount } from '@hcengineering/contact' import { Avatar, personByIdStore } from '@hcengineering/contact-resources' import { IdMap, getCurrentAccount } from '@hcengineering/core' - import { isOffice, ParticipantInfo, Room, RoomAccess, RoomType } from '@hcengineering/love' + import { isOffice, ParticipantInfo, Room, RoomAccess, RoomType, MeetingStatus } from '@hcengineering/love' import { Icon, Label, eventToHTMLElement, showPopup } from '@hcengineering/ui' import { createEventDispatcher } from 'svelte' import { getClient } from '@hcengineering/presentation' import { openDoc } from '@hcengineering/view-resources' - import { get } from 'svelte/store' import love from '../plugin' import { myInfo, selectedRoomPlace, currentRoom, currentMeetingMinutes } from '../stores' - import { getRoomLabel, lk } from '../utils' + import { getRoomLabel, lk, isConnected } from '../utils' import PersonActionPopup from './PersonActionPopup.svelte' import RoomLanguage from './RoomLanguage.svelte' @@ -67,14 +66,20 @@ async function openRoom (x: number, y: number): Promise { const client = getClient() const hierarchy = client.getHierarchy() - if ($currentRoom?._id === room._id) { + if ($isConnected && $currentRoom?._id === room._id) { const sid = await lk.getSid() - const meetingMinutes = - get(currentMeetingMinutes) ?? (await client.findOne(love.class.MeetingMinutes, { sid, attachedTo: room._id })) - if (meetingMinutes === undefined) { + let meeting = $currentMeetingMinutes + if (meeting?.sid !== sid || meeting?.attachedTo !== room._id || meeting?.status !== MeetingStatus.Active) { + meeting = await client.findOne(love.class.MeetingMinutes, { + sid, + attachedTo: room._id, + status: MeetingStatus.Active + }) + } + if (meeting === undefined) { await openDoc(hierarchy, room) } else { - await openDoc(hierarchy, meetingMinutes) + await openDoc(hierarchy, meeting) } } else { selectedRoomPlace.set({ _id: room._id, x, y }) diff --git a/plugins/love-resources/src/components/widget/MeetingWidget.svelte b/plugins/love-resources/src/components/widget/MeetingWidget.svelte index 91b854c640..efb67682a8 100644 --- a/plugins/love-resources/src/components/widget/MeetingWidget.svelte +++ b/plugins/love-resources/src/components/widget/MeetingWidget.svelte @@ -15,13 +15,13 @@ {#each stateObjects as objectRef, i (objectRef._id)} {@const dragged = isDragging && objectRef._id === dragCard?._id} - {@const object = limitedObjects.get(objectRef._id) ?? (objectRef._id === dragCard?._id ? dragCard : undefined)} + {@const object = getObject(objectRef._id, limitedObjects)} {#if object !== undefined}
Date: Fri, 22 Nov 2024 09:29:15 +0700 Subject: [PATCH 05/10] Drawing overlay for image preview dialog (#7216) Signed-off-by: Nikolay Chunosov --- models/attachment/src/index.ts | 26 +- packages/presentation/lang/cs.json | 3 +- packages/presentation/lang/en.json | 3 +- packages/presentation/lang/es.json | 3 +- packages/presentation/lang/fr.json | 3 +- packages/presentation/lang/it.json | 3 +- packages/presentation/lang/pt.json | 3 +- packages/presentation/lang/ru.json | 3 +- packages/presentation/lang/zh.json | 3 +- .../src/components/DrawingBoard.svelte | 147 +++++++ .../src/components/FilePreviewPopup.svelte | 36 +- .../src/components/icons/Eraser.svelte | 10 + packages/presentation/src/drawing.ts | 398 ++++++++++++++++++ packages/presentation/src/index.ts | 2 + packages/presentation/src/plugin.ts | 3 +- .../src/components/AttachmentActions.svelte | 27 +- .../AttachmentGalleryPresenter.svelte | 17 +- .../src/components/AttachmentPresenter.svelte | 17 +- .../src/components/AttachmentPreview.svelte | 11 +- .../src/components/Photos.svelte | 11 +- plugins/attachment-resources/src/utils.ts | 64 ++- plugins/attachment/src/index.ts | 14 +- .../src/components/viewer/ImageViewer.svelte | 40 +- 23 files changed, 749 insertions(+), 98 deletions(-) create mode 100644 packages/presentation/src/components/DrawingBoard.svelte create mode 100644 packages/presentation/src/components/icons/Eraser.svelte create mode 100644 packages/presentation/src/drawing.ts diff --git a/models/attachment/src/index.ts b/models/attachment/src/index.ts index f7bf8cf1a4..163ce99ac9 100644 --- a/models/attachment/src/index.ts +++ b/models/attachment/src/index.ts @@ -14,9 +14,10 @@ // import activity from '@hcengineering/activity' -import type { Attachment, AttachmentMetadata, Photo, SavedAttachments } from '@hcengineering/attachment' -import { IndexKind, type Blob, type Domain, type Ref } from '@hcengineering/core' +import type { Attachment, AttachmentMetadata, Drawing, Photo, SavedAttachments } from '@hcengineering/attachment' +import { IndexKind, type Blob, type Class, type Doc, type Domain, type Ref } from '@hcengineering/core' import { + Hidden, Index, Model, Prop, @@ -28,10 +29,11 @@ import { UX, type Builder } from '@hcengineering/model' -import core, { TAttachedDoc } from '@hcengineering/model-core' +import core, { TAttachedDoc, TDoc } from '@hcengineering/model-core' import preference, { TPreference } from '@hcengineering/model-preference' import view, { createAction } from '@hcengineering/model-view' import workbench, { WidgetType } from '@hcengineering/workbench' +import { getEmbeddedLabel } from '@hcengineering/platform' import presentation from '@hcengineering/model-presentation' import attachment from './plugin' @@ -80,8 +82,24 @@ export class TSavedAttachments extends TPreference implements SavedAttachments { declare attachedTo: Ref } +@Model(attachment.class.Drawing, core.class.Doc, DOMAIN_ATTACHMENT) +export class TDrawing extends TDoc implements Drawing { + @Prop(TypeRef(core.class.Doc), getEmbeddedLabel('Parent')) + @Index(IndexKind.Indexed) + @Hidden() + parent!: Ref + + @Prop(TypeRef(core.class.Class), getEmbeddedLabel('Parent class')) + @Index(IndexKind.Indexed) + @Hidden() + parentClass!: Ref> + + @Prop(TypeString(), getEmbeddedLabel('Content')) + content?: string +} + export function createModel (builder: Builder): void { - builder.createModel(TAttachment, TPhoto, TSavedAttachments) + builder.createModel(TAttachment, TDrawing, TPhoto, TSavedAttachments) builder.mixin(attachment.class.Attachment, core.class.Class, view.mixin.ObjectPresenter, { presenter: attachment.component.AttachmentPresenter diff --git a/packages/presentation/lang/cs.json b/packages/presentation/lang/cs.json index 166afee3a5..bfdee2bce8 100644 --- a/packages/presentation/lang/cs.json +++ b/packages/presentation/lang/cs.json @@ -33,7 +33,8 @@ "Next": "Další", "FailedToPreview": "Náhled se nezdařil", "ContentType": "Typ obsahu", - "ContentTypeNotSupported": "Náhled není dostupný pro tento typ obsahu" + "ContentTypeNotSupported": "Náhled není dostupný pro tento typ obsahu", + "StartDrawing": "Načmárejte" }, "status": { "FileTooLarge": "Soubor je příliš velký" diff --git a/packages/presentation/lang/en.json b/packages/presentation/lang/en.json index 6af5a21dd6..3bf9c961cd 100644 --- a/packages/presentation/lang/en.json +++ b/packages/presentation/lang/en.json @@ -33,7 +33,8 @@ "Next": "Next", "FailedToPreview": "Failed to preview", "ContentType": "Content type", - "ContentTypeNotSupported": "Preview is not available for this content type" + "ContentTypeNotSupported": "Preview is not available for this content type", + "StartDrawing": "Scribble over" }, "status": { "FileTooLarge": "File too large" diff --git a/packages/presentation/lang/es.json b/packages/presentation/lang/es.json index d313b42ced..3f22269188 100644 --- a/packages/presentation/lang/es.json +++ b/packages/presentation/lang/es.json @@ -33,7 +33,8 @@ "Next": "Siguiente", "FailedToPreview": "Error al previsualizar", "ContentType": "Tipo de contenido", - "ContentTypeNotSupported": "La vista previa no está disponible para este tipo de contenido" + "ContentTypeNotSupported": "La vista previa no está disponible para este tipo de contenido", + "StartDrawing": "Garabatear encima" }, "status": { "FileTooLarge": "Archivo demasiado grande" diff --git a/packages/presentation/lang/fr.json b/packages/presentation/lang/fr.json index 2db2936ecd..0d450533f9 100644 --- a/packages/presentation/lang/fr.json +++ b/packages/presentation/lang/fr.json @@ -33,7 +33,8 @@ "Next": "Suivant", "FailedToPreview": "Échec de l'aperçu", "ContentType": "Type de contenu", - "ContentTypeNotSupported": "L'aperçu n'est pas disponible pour ce type de contenu" + "ContentTypeNotSupported": "L'aperçu n'est pas disponible pour ce type de contenu", + "StartDrawing": "Gribouiller dessus" }, "status": { "FileTooLarge": "Fichier trop volumineux" diff --git a/packages/presentation/lang/it.json b/packages/presentation/lang/it.json index 2adafe0098..9c761f9d0b 100644 --- a/packages/presentation/lang/it.json +++ b/packages/presentation/lang/it.json @@ -33,7 +33,8 @@ "Next": "Successivo", "FailedToPreview": "Impossibile mostrare l'anteprima", "ContentType": "Tipo di contenuto", - "ContentTypeNotSupported": "Anteprima non disponibile per questo tipo di contenuto" + "ContentTypeNotSupported": "Anteprima non disponibile per questo tipo di contenuto", + "StartDrawing": "Scarabocchiare sopra" }, "status": { "FileTooLarge": "File troppo grande" diff --git a/packages/presentation/lang/pt.json b/packages/presentation/lang/pt.json index 3a9a13c216..be2daa978f 100644 --- a/packages/presentation/lang/pt.json +++ b/packages/presentation/lang/pt.json @@ -33,7 +33,8 @@ "Next": "Seguinte", "FailedToPreview": "Falha ao pré-visualizar", "ContentType": "Tipo de conteúdo", - "ContentTypeNotSupported": "A visualização não está disponível para este tipo de conteúdo" + "ContentTypeNotSupported": "A visualização não está disponível para este tipo de conteúdo", + "StartDrawing": "Scarabocchiare sopra" }, "status": { "FileTooLarge": "Ficheiro demasiado grande" diff --git a/packages/presentation/lang/ru.json b/packages/presentation/lang/ru.json index cd4d67aa8c..d34ca683a2 100644 --- a/packages/presentation/lang/ru.json +++ b/packages/presentation/lang/ru.json @@ -33,7 +33,8 @@ "Next": "Далее", "FailedToPreview": "Ошибка предпросмотра", "ContentType": "Тип контента", - "ContentTypeNotSupported": "Предварительный просмотр недоступен для этого типа контента" + "ContentTypeNotSupported": "Предварительный просмотр недоступен для этого типа контента", + "StartDrawing": "Сделать набросок" }, "status": { "FileTooLarge": "Файл слишком большой" diff --git a/packages/presentation/lang/zh.json b/packages/presentation/lang/zh.json index 1ebf4c7ffb..37e2d021f2 100644 --- a/packages/presentation/lang/zh.json +++ b/packages/presentation/lang/zh.json @@ -33,7 +33,8 @@ "Next": "下一步", "FailedToPreview": "预览失败", "ContentType": "内容类型", - "ContentTypeNotSupported": "此內容類型無法預覽" + "ContentTypeNotSupported": "此內容類型無法預覽", + "StartDrawing": "随意涂鸦" }, "status": { "FileTooLarge": "文件太大" diff --git a/packages/presentation/src/components/DrawingBoard.svelte b/packages/presentation/src/components/DrawingBoard.svelte new file mode 100644 index 0000000000..73692de942 --- /dev/null +++ b/packages/presentation/src/components/DrawingBoard.svelte @@ -0,0 +1,147 @@ + + + +{#if active} +
{ + updateToolbarPosition(readonly, board, toolbar) + }} + use:drawing={{ + readonly, + imageWidth, + imageHeight, + drawingData, + saveDrawing, + drawingTool, + penColor + }} + > + {#if !readonly} +
+ + {/each} +
+ {/if} + +
+{:else} + +{/if} + + diff --git a/packages/presentation/src/components/FilePreviewPopup.svelte b/packages/presentation/src/components/FilePreviewPopup.svelte index 4c0bc95191..5dfb00a309 100644 --- a/packages/presentation/src/components/FilePreviewPopup.svelte +++ b/packages/presentation/src/components/FilePreviewPopup.svelte @@ -15,7 +15,7 @@ @@ -65,6 +90,15 @@ + {#if props.drawingAvailable === true} +