From 6af28c9655b02de7453203e693da976684b908db Mon Sep 17 00:00:00 2001 From: Artyom Savchenko Date: Tue, 3 Dec 2024 16:52:35 +0700 Subject: [PATCH 1/5] UBERF-8613: Rename all test cases and fix list viewlet (#7255) Signed-off-by: Artem Savchenko --- models/test-management/src/index.ts | 33 +++++++++++---------- plugins/test-management-assets/lang/cs.json | 2 +- plugins/test-management-assets/lang/en.json | 2 +- plugins/test-management-assets/lang/es.json | 2 +- plugins/test-management-assets/lang/fr.json | 2 +- plugins/test-management-assets/lang/it.json | 2 +- plugins/test-management-assets/lang/pt.json | 2 +- plugins/test-management-assets/lang/ru.json | 8 ++--- plugins/test-management-assets/lang/zh.json | 2 +- plugins/test-management/src/plugin.ts | 2 +- 10 files changed, 29 insertions(+), 28 deletions(-) diff --git a/models/test-management/src/index.ts b/models/test-management/src/index.ts index 43e288745a..811a4a3c52 100644 --- a/models/test-management/src/index.ts +++ b/models/test-management/src/index.ts @@ -93,7 +93,7 @@ function defineApplication (builder: Builder): void { parentKey: 'parent', noParentId: testManagement.ids.NoParent, getFolderLink: testManagement.function.GetTestSuiteLink, - allObjectsLabel: testManagement.string.AllTestCases, + allObjectsLabel: testManagement.string.AllTestSuites, allObjectsIcon: testManagement.icon.TestSuites }, syncWithLocationQuery: true @@ -335,20 +335,6 @@ function defineTestCase (builder: Builder): void { ignoreKeys: ['createdBy', 'modifiedBy', 'createdOn', 'modifiedOn', 'name'] }) - builder.createDoc( - view.class.Viewlet, - core.space.Model, - { - attachTo: testManagement.class.TestCase, - descriptor: view.viewlet.Table, - config: ['', { key: 'attachedTo', label: testManagement.string.TestSuite }, 'status', 'assignee'], - configOptions: { - strict: true - } - }, - testManagement.viewlet.TableTestCase - ) - const viewOptions: ViewOptionsModel = { groupBy: ['attachedTo'], orderBy: [ @@ -385,7 +371,6 @@ function defineTestCase (builder: Builder): void { props: { kind: 'list', size: 'small', shouldShowName: false }, displayProps: { key: 'status', fixed: 'left' } }, - { key: '', displayProps: { fixed: 'left', key: 'lead' } }, { key: '', displayProps: { grow: true } }, { key: 'modifiedOn', displayProps: { key: 'modified', fixed: 'right', dividerBefore: true } }, { @@ -398,6 +383,20 @@ function defineTestCase (builder: Builder): void { }, testManagement.viewlet.ListTestCase ) + + builder.createDoc( + view.class.Viewlet, + core.space.Model, + { + attachTo: testManagement.class.TestCase, + descriptor: view.viewlet.Table, + config: ['', { key: 'attachedTo', label: testManagement.string.TestSuite }, 'status', 'assignee'], + configOptions: { + strict: true + } + }, + testManagement.viewlet.TableTestCase + ) } function defineTestRun (builder: Builder): void { @@ -493,6 +492,8 @@ function defineTestResult (builder: Builder): void { key: 'status', props: { kind: 'list', size: 'small', shouldShowName: false } }, + { key: '', displayProps: { grow: true } }, + { key: 'modifiedOn', displayProps: { key: 'modified', fixed: 'right', dividerBefore: true } }, { key: 'assignee', props: { kind: 'list', shouldShowName: false, avatarSize: 'x-small' }, diff --git a/plugins/test-management-assets/lang/cs.json b/plugins/test-management-assets/lang/cs.json index 9f075c90e7..154069f81f 100644 --- a/plugins/test-management-assets/lang/cs.json +++ b/plugins/test-management-assets/lang/cs.json @@ -21,7 +21,7 @@ "TestCase": "Testovací případ", "TestProject": "Projekt", "TestManagementApplication": "Správa testů", - "AllTestCases": "Všechny testovací případy", + "AllTestSuites": "Všechny testovací sady", "AllProjects": "Všechny projekty", "Projects": "Projekty", "CreateProject": "Vytvořit projekt", diff --git a/plugins/test-management-assets/lang/en.json b/plugins/test-management-assets/lang/en.json index 943bbaa95a..d318793798 100644 --- a/plugins/test-management-assets/lang/en.json +++ b/plugins/test-management-assets/lang/en.json @@ -21,7 +21,7 @@ "TestCase": "Test case", "TestProject": "Project", "TestManagementApplication": "Test Management", - "AllTestCases": "All test cases", + "AllTestSuites": "All test suites", "AllProjects": "All projects", "Projects": "Projects", "CreateProject": "Create project", diff --git a/plugins/test-management-assets/lang/es.json b/plugins/test-management-assets/lang/es.json index f0fdb2bc09..8a0a249378 100644 --- a/plugins/test-management-assets/lang/es.json +++ b/plugins/test-management-assets/lang/es.json @@ -21,7 +21,7 @@ "TestCase": "Caso de prueba", "TestProject": "Proyecto", "TestManagementApplication": "Gestión de Pruebas", - "AllTestCases": "Todos los casos de prueba", + "AllTestSuites": "Todos los conjuntos de pruebas", "AllProjects": "Todos los proyectos", "Projects": "Proyectos", "CreateProject": "Crear proyecto", diff --git a/plugins/test-management-assets/lang/fr.json b/plugins/test-management-assets/lang/fr.json index 3fd4f7c877..8ab2c8135f 100644 --- a/plugins/test-management-assets/lang/fr.json +++ b/plugins/test-management-assets/lang/fr.json @@ -21,7 +21,7 @@ "TestCase": "Cas de test", "TestProject": "Projet", "TestManagementApplication": "Gestion des tests", - "AllTestCases": "Tous les cas de tests", + "AllTestSuites": "Tous les ensembles de tests", "AllProjects": "Tous les projets", "Projects": "Projets", "CreateProject": "Créer un projet", diff --git a/plugins/test-management-assets/lang/it.json b/plugins/test-management-assets/lang/it.json index a1a56b4ab7..195bfb66f9 100644 --- a/plugins/test-management-assets/lang/it.json +++ b/plugins/test-management-assets/lang/it.json @@ -21,7 +21,7 @@ "TestCase": "Caso di test", "TestProject": "Progetto", "TestManagementApplication": "Gestione Test", - "AllTestCases": "Tutti i casi di test", + "AllTestSuites": "Tutti i set di test", "AllProjects": "Tutti i progetti", "Projects": "Progetti", "CreateProject": "Crea progetto", diff --git a/plugins/test-management-assets/lang/pt.json b/plugins/test-management-assets/lang/pt.json index c6a71d734c..a470584a46 100644 --- a/plugins/test-management-assets/lang/pt.json +++ b/plugins/test-management-assets/lang/pt.json @@ -21,7 +21,7 @@ "TestCase": "Caso de teste", "TestProject": "Projeto", "TestManagementApplication": "Gestão de Testes", - "AllTestCases": "Todos os casos de teste", + "AllTestSuites": "Todos os conjuntos de testes", "AllProjects": "Todos os projetos", "Projects": "Projetos", "CreateProject": "Criar projeto", diff --git a/plugins/test-management-assets/lang/ru.json b/plugins/test-management-assets/lang/ru.json index e18849137c..aace7962d2 100644 --- a/plugins/test-management-assets/lang/ru.json +++ b/plugins/test-management-assets/lang/ru.json @@ -21,7 +21,7 @@ "TestCase": "Тест-кейс", "TestProject": "Проект", "TestManagementApplication": "Управление тестированием", - "AllTestCases": "Все тест-кейсы", + "AllTestSuites": "Все категории тестов", "AllProjects": "Все проекты", "Projects": "Проекты", "CreateProject": "Создать проект", @@ -36,14 +36,14 @@ "RoleLabel": "Роль", "ProjectMembers": "Участники проекта", "TestSuites": "Тестовые наборы", - "CreateTestSuite": "Создать тестовый набор", + "CreateTestSuite": "Создать новую категорию тестов", "NamePlaceholder": "Имя тестового набора", "DescriptionPlaceholder": "Описание (опционально)", "TestRuns": "Выполнение тестов", "TestRun": "Тест план", "TestNamePlaceholder": "Имя тест кейса", "ChooseIcon": "Выберите иконку", - "NoTestSuite": "Тестовый набор не задан", + "NoTestSuite": "Категория тестов не задана", "StatusDraft": "В прогрессе", "StatusReview": "Готов для ревью", "StatusReviewComments": "Требует исправлений", @@ -58,7 +58,7 @@ "NoTestCases": "Тест-кейсы отсутствует", "CreateTestRun": "Создать тест план", "TestRunNamePlaceholder": "Название тест плана", - "SelectTestSuites": "Выбрать наборы тестов", + "SelectTestSuites": "Выбрать категории тестов", "SelectTestCases": "Выбрать тест-кейсы", "TestLibrary": "Библиотека тестов", "TestResult": "Результат", diff --git a/plugins/test-management-assets/lang/zh.json b/plugins/test-management-assets/lang/zh.json index 3854292f46..5eb4833e29 100644 --- a/plugins/test-management-assets/lang/zh.json +++ b/plugins/test-management-assets/lang/zh.json @@ -21,7 +21,7 @@ "TestCase": "測試用例", "TestProject": "專案", "TestManagementApplication": "測試管理", - "AllTestCases": "所有測試用例", + "AllTestSuites": "所有測試套件", "AllProjects": "所有項目", "Projects": "專案", "CreateProject": "創建專案", diff --git a/plugins/test-management/src/plugin.ts b/plugins/test-management/src/plugin.ts index fc13a88910..1ebfde133d 100644 --- a/plugins/test-management/src/plugin.ts +++ b/plugins/test-management/src/plugin.ts @@ -128,7 +128,7 @@ export const testManagementPlugin = plugin(testManagementId, { TestCase: '' as IntlString, TestProject: '' as IntlString, TestManagementApplication: '' as IntlString, - AllTestCases: '' as IntlString, + AllTestSuites: '' as IntlString, AllProjects: '' as IntlString, Projects: '' as IntlString, CreateProject: '' as IntlString, From 6d31873779b122e90b21e44d49d0efb96b4533c0 Mon Sep 17 00:00:00 2001 From: Kristina Date: Tue, 3 Dec 2024 18:14:57 +0400 Subject: [PATCH 2/5] Fix double meeting initialization (#7256) Signed-off-by: Kristina Fefelova --- .../src/components/EditRoom.svelte | 10 +++- plugins/love-resources/src/utils.ts | 54 +++++++++---------- 2 files changed, 34 insertions(+), 30 deletions(-) diff --git a/plugins/love-resources/src/components/EditRoom.svelte b/plugins/love-resources/src/components/EditRoom.svelte index 4a5d3a0304..970cd09657 100644 --- a/plugins/love-resources/src/components/EditRoom.svelte +++ b/plugins/love-resources/src/components/EditRoom.svelte @@ -75,7 +75,13 @@ connectLabel = love.string.StartMeeting } - function showConnectionButton (object: Room, isConnected: boolean, myOffice?: Room, currentRoom?: Room): boolean { + function showConnectionButton ( + object: Room, + connecting: boolean, + 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 @@ -98,7 +104,7 @@ focusIndex={1} /> - {#if showConnectionButton(object, $isConnected, $myOffice, $currentRoom)} + {#if showConnectionButton(object, connecting, $isConnected, $myOffice, $currentRoom)} {/if} diff --git a/plugins/love-resources/src/utils.ts b/plugins/love-resources/src/utils.ts index 1c3681f244..00c4a9e363 100644 --- a/plugins/love-resources/src/utils.ts +++ b/plugins/love-resources/src/utils.ts @@ -374,16 +374,12 @@ lk.on(RoomEvent.RecordingStatusChanged, (evt) => { isRecording.set(evt) }) lk.on(RoomEvent.RoomMetadataChanged, (metadata) => { - try { - const data = JSON.parse(metadata) as RoomMetadata - if (data.recording !== undefined) { - isRecording.set(data.recording) - } - if (data.transcription !== undefined) { - isTranscription.set(data.transcription === TranscriptionStatus.InProgress) - } - } catch (err: any) { - Analytics.handleError(err) + const data = parseMetadata(metadata) + if (data.recording !== undefined) { + isRecording.set(data.recording) + } + if (data.transcription !== undefined) { + isTranscription.set(data.transcription === TranscriptionStatus.InProgress) } }) @@ -392,7 +388,7 @@ lk.on(RoomEvent.Connected, () => { sendMessage({ type: 'connect', value: true }) isCurrentInstanceConnected.set(true) isRecording.set(lk.isRecording) - void initRoomMetadata(lk.metadata) + void initRoom() Analytics.handleEvent(LoveEvents.ConnectedToRoom) }) lk.on(RoomEvent.Disconnected, () => { @@ -402,25 +398,19 @@ lk.on(RoomEvent.Disconnected, () => { Analytics.handleEvent(LoveEvents.DisconnectedFromRoom) }) -async function initRoomMetadata (metadata: string | undefined): Promise { - let data: RoomMetadata - try { - data = metadata == null || metadata === '' ? {} : JSON.parse(metadata) - } catch (err: any) { - data = {} - Analytics.handleError(err) - } - - isTranscription.set(data.transcription === TranscriptionStatus.InProgress) - +async function initRoom (): Promise { const room = get(currentRoom) - const meetingMinutes = get(currentMeetingMinutes) - const isValidMeeting = - meetingMinutes != null && meetingMinutes.attachedTo === room?._id && meetingMinutes.status === MeetingStatus.Active - - if (room != null && !isValidMeeting) { + if (room !== undefined) { await initMeetingMinutes(room) } + await initRoomMetadata(lk.metadata) +} + +async function initRoomMetadata (metadata: string | undefined): Promise { + const room = get(currentRoom) + const data: RoomMetadata = parseMetadata(metadata) + + isTranscription.set(data.transcription === TranscriptionStatus.InProgress) if ( (data.transcription == null || data.transcription === TranscriptionStatus.Idle) && @@ -434,6 +424,15 @@ async function initRoomMetadata (metadata: string | undefined): Promise { } } +function parseMetadata (metadata: string | undefined): RoomMetadata { + try { + return metadata == null || metadata === '' ? {} : JSON.parse(metadata) + } catch (err: any) { + Analytics.handleError(err) + return {} + } +} + async function withRetries (fn: () => Promise, retries: number, delay: number): Promise { for (let attempt = 0; attempt < retries; attempt++) { try { @@ -738,7 +737,6 @@ export async function connectRoom ( 3, 1000 ) - await initMeetingMinutes(room) } catch (err) { console.error(err) await leaveRoom(currentInfo, get(myOffice)) From f69d5ede0e841b8410592bc0af6ad91fafa89ee9 Mon Sep 17 00:00:00 2001 From: Chunosov Date: Wed, 4 Dec 2024 02:56:59 +0700 Subject: [PATCH 3/5] Save drawing boards separately from document (#7257) Signed-off-by: Nikolay Chunosov --- .../src/components/DrawingBoardToolbar.svelte | 30 ++- packages/presentation/src/drawing.ts | 4 +- packages/ui/src/components/Dialog.svelte | 4 + .../components/CollaborativeTextEditor.svelte | 62 ++++-- .../src/components/DrawingBoardEditor.svelte | 139 ++++++++++++ .../components/DrawingBoardNodeView.svelte | 176 +++++++++++++++ .../src/components/DrawingBoardPopup.svelte | 89 +------- .../src/components/extension/drawingBoard.ts | 209 +++--------------- 8 files changed, 435 insertions(+), 278 deletions(-) create mode 100644 plugins/text-editor-resources/src/components/DrawingBoardEditor.svelte create mode 100644 plugins/text-editor-resources/src/components/DrawingBoardNodeView.svelte diff --git a/packages/presentation/src/components/DrawingBoardToolbar.svelte b/packages/presentation/src/components/DrawingBoardToolbar.svelte index 1d5f793615..856b96405e 100644 --- a/packages/presentation/src/components/DrawingBoardToolbar.svelte +++ b/packages/presentation/src/components/DrawingBoardToolbar.svelte @@ -80,7 +80,7 @@ case 'add-color': { if (colorSelector !== undefined) { colorSelector.value = penColor - colorSelector.click() + colorSelector.showPicker() } break } @@ -130,8 +130,8 @@ if (!penColors.includes(penColor)) { penColor = penColors[0] ?? defaultColor } - penWidth = parseInt(localStorage.getItem(storageKey.penWidth) ?? '4') - eraserWidth = parseInt(localStorage.getItem(storageKey.eraserWidth) ?? '30') + penWidth = parseInt(localStorage.getItem(storageKey.penWidth) ?? '6') + eraserWidth = parseInt(localStorage.getItem(storageKey.eraserWidth) ?? '50') }) function updatePenWidth (): void { @@ -181,9 +181,25 @@ {/if}
{#if tool === 'pen'} - + {:else} - + {/if}
{#each penColors as color} @@ -247,4 +263,8 @@ width: 0; opacity: 0; } + + .widthSelector { + width: 80px; + } diff --git a/packages/presentation/src/drawing.ts b/packages/presentation/src/drawing.ts index cc9a97b402..fd459fda00 100644 --- a/packages/presentation/src/drawing.ts +++ b/packages/presentation/src/drawing.ts @@ -295,7 +295,6 @@ export function drawing ( } }) prevPos = { x, y } - props.panned?.(draw.offset) }) } } @@ -310,6 +309,8 @@ export function drawing ( if (draw.isDrawingTool()) { draw.drawLive(e.offsetX, e.offsetY, true) storeCommand() + } else if (draw.tool === 'pan') { + props.panned?.(draw.offset) } draw.on = false } @@ -347,6 +348,7 @@ export function drawing ( canvas.style.cursor = props.defaultCursor ?? 'default' } else if (draw.isDrawingTool()) { canvas.style.cursor = 'none' + canvasCursor.style.visibility = 'visible' const erasing = draw.tool === 'erase' const w = draw.cursorWidth() canvasCursor.style.background = erasing ? 'none' : draw.penColor diff --git a/packages/ui/src/components/Dialog.svelte b/packages/ui/src/components/Dialog.svelte index ef8f85087d..9679461d9f 100644 --- a/packages/ui/src/components/Dialog.svelte +++ b/packages/ui/src/components/Dialog.svelte @@ -35,6 +35,10 @@ const dispatch = createEventDispatcher() + export function maximize (): void { + toggleFullSize = true + } + let fullSize: boolean = false let toggleFullSize: boolean = fullSize $: needFullSize = checkAdaptiveMatching($deviceInfo.size, 'md') diff --git a/plugins/text-editor-resources/src/components/CollaborativeTextEditor.svelte b/plugins/text-editor-resources/src/components/CollaborativeTextEditor.svelte index 938e5d7714..711a7c4b34 100644 --- a/plugins/text-editor-resources/src/components/CollaborativeTextEditor.svelte +++ b/plugins/text-editor-resources/src/components/CollaborativeTextEditor.svelte @@ -16,7 +16,15 @@ --> + +{#if savedCmds !== undefined && savedProps !== undefined} + {#if loading} +
+ + +
+ {:else} +
{ + savedCmds.push([cmd]) + }, + panned: (offset) => { + savedProps.set('offset', offset) + } + }} + > + {#if grabFocus} + + + + {/if} + {#if !readonly} + { + savedCmds.delete(0, savedCmds.length) + savedProps.set('offset', { x: 0, y: 0 }) + }} + /> + {/if} + +
+ {/if} +{/if} + + diff --git a/plugins/text-editor-resources/src/components/DrawingBoardNodeView.svelte b/plugins/text-editor-resources/src/components/DrawingBoardNodeView.svelte new file mode 100644 index 0000000000..6c8d20b80e --- /dev/null +++ b/plugins/text-editor-resources/src/components/DrawingBoardNodeView.svelte @@ -0,0 +1,176 @@ + + + +{#if savedBoard?.commands !== undefined && savedBoard?.props !== undefined} + + +
+
+ {#if selected} +
+ + + +
+
+ + + +
+ {/if} +
+
+{/if} + + diff --git a/plugins/text-editor-resources/src/components/DrawingBoardPopup.svelte b/plugins/text-editor-resources/src/components/DrawingBoardPopup.svelte index 75348ad5e2..2d894be680 100644 --- a/plugins/text-editor-resources/src/components/DrawingBoardPopup.svelte +++ b/plugins/text-editor-resources/src/components/DrawingBoardPopup.svelte @@ -13,109 +13,36 @@ // limitations under the License. --> {#if savedCmds !== undefined && savedProps !== undefined} { dispatch('close') }} > -
{ - savedCmds.push([cmd]) - }, - panned: (offset) => { - savedProps.set('offset', offset) - } - }} - > - - - - - { - savedCmds.delete(0, savedCmds.length) - savedProps.set('offset', { x: 0, y: 0 }) - }} - /> -
+
{/if} diff --git a/plugins/text-editor-resources/src/components/extension/drawingBoard.ts b/plugins/text-editor-resources/src/components/extension/drawingBoard.ts index c1476599cb..e01d31a569 100644 --- a/plugins/text-editor-resources/src/components/extension/drawingBoard.ts +++ b/plugins/text-editor-resources/src/components/extension/drawingBoard.ts @@ -13,41 +13,26 @@ // limitations under the License. // -import { type DrawingCmd, type DrawingProps, drawing } from '@hcengineering/presentation' +import { type DrawingCmd } from '@hcengineering/presentation' import { showPopup } from '@hcengineering/ui' import { type Editor, mergeAttributes, Node } from '@tiptap/core' import { NodeSelection } from '@tiptap/pm/state' -import type { Array as YArray, Doc as YDoc, Map as YMap } from 'yjs' +import type { Array as YArray, Map as YMap } from 'yjs' +import DrawingBoardNodeView from '../DrawingBoardNodeView.svelte' import DrawingBoardPopup from '../DrawingBoardPopup.svelte' - -const defaultHeight = 500 -const maxHeight = 1000 -const minHeight = 100 +import { SvelteNodeViewRenderer } from '../node-view' export interface DrawingBoardOptions { - ydoc?: YDoc + getSavedBoard: (id: string) => SavedBoard } -const scribbleSvg = ` - -` -const chevronSvg = ` - -` - -interface SavedBoard { +export interface SavedBoard { commands: YArray props: YMap + loading: boolean } -function getSavedBoard (ydoc: YDoc | undefined, id: string): SavedBoard { - const board = ydoc?.getMap(`drawing-board-${id}`) - const commands = board?.get('commands') as YArray - const props = board?.get('props') as YMap - return { commands, props } -} - -function showBoardPopup (board: SavedBoard, editor: Editor): void { +export function showBoardPopup (board: SavedBoard, editor: Editor): void { if (board.commands !== undefined && board.props !== undefined) { showPopup( DrawingBoardPopup, @@ -76,23 +61,35 @@ export const DrawingBoardExtension = Node.create({ group: 'block', + draggable: true, + renderHTML ({ HTMLAttributes }) { return ['div', mergeAttributes({ 'data-type': this.name }, HTMLAttributes)] }, + parseHTML () { + return [ + { + tag: 'div[data-type="drawingBoard"]' + } + ] + }, + addOptions () { return { - ydoc: undefined + getSavedBoard: (id) => ({}) as any as SavedBoard } }, addAttributes () { return { id: { - renderHTML: (attrs) => ({ 'data-id': attrs.id }) + renderHTML: (attrs) => ({ 'data-id': attrs.id }), + parseHTML: (element) => element.getAttribute('data-id') }, height: { - renderHTML: (attrs) => ({ 'data-height': attrs.height }) + renderHTML: (attrs) => ({ 'data-height': attrs.height }), + parseHTML: (element) => parseInt(element.getAttribute('data-height') ?? '500') } } }, @@ -105,8 +102,10 @@ export const DrawingBoardExtension = Node.create({ if (state.selection instanceof NodeSelection) { const node = state.selection.node if (node?.type.name === this.name) { - const board = getSavedBoard(this.options.ydoc, node.attrs.id) - showBoardPopup(board, this.editor) + const board = this.options.getSavedBoard(node.attrs.id) + if (!board.loading) { + showBoardPopup(board, this.editor) + } return true } } @@ -122,155 +121,11 @@ export const DrawingBoardExtension = Node.create({ }, addNodeView () { - return ({ node, getPos }) => { - const board = getSavedBoard(this.options.ydoc, node.attrs.id) - if (board.commands === undefined || board.props === undefined) { - return {} + return SvelteNodeViewRenderer(DrawingBoardNodeView, { + contentAs: 'div', + componentProps: { + getSavedBoard: this.options.getSavedBoard } - - const normalBorder = '1px solid var(--theme-navpanel-border)' - const selectedBorder = '1px solid var(--theme-editbox-focus-border)' - - const dom = document.createElement('div') - dom.id = node.attrs.id - dom.contentEditable = 'false' - dom.style.width = '100%' - dom.style.position = 'relative' - dom.style.height = `${node.attrs.height ?? defaultHeight}px` - dom.style.border = normalBorder - dom.style.borderRadius = 'var(--small-BorderRadius)' - dom.style.backgroundColor = 'var(--theme-navpanel-color)' - dom.ondblclick = () => { - showBoardPopup(board, this.editor) - } - - const drawingProps: DrawingProps = { - readonly: true, - autoSize: true, - commands: board.commands.toArray(), - commandCount: board.commands.length, - offset: board.props.get('offset') - } - - const { canvas, update: updateDrawing } = drawing(dom, drawingProps) - if (canvas === undefined || updateDrawing === undefined) { - return {} - } - dom.appendChild(canvas) - - const listenSavedCommands = (): void => { - let update = false - if (board.commands.length === 0) { - update = true - drawingProps.commands = [] - } else if (board.commands.length > drawingProps.commands.length) { - update = true - for (let i = drawingProps.commands.length; i < board.commands.length; i++) { - drawingProps.commands.push(board.commands.get(i)) - } - } - if (update) { - drawingProps.commandCount = board.commands.length - updateDrawing(drawingProps) - } - } - const listenSavedProps = (): void => { - drawingProps.offset = board.props.get('offset') - updateDrawing(drawingProps) - } - board.commands.observe(listenSavedCommands) - board.props.observe(listenSavedProps) - - const button = document.createElement('div') - button.style.visibility = 'hidden' - button.style.cursor = 'pointer' - button.style.position = 'absolute' - button.style.top = '0.3rem' - button.style.right = '0.3rem' - button.style.width = '2.2rem' - button.style.height = '2.2rem' - button.style.color = 'var(--primary-button-color)' - button.style.backgroundColor = 'var(--primary-button-default)' - button.style.borderRadius = 'var(--extra-small-BorderRadius)' - button.style.border = normalBorder - button.style.display = 'flex' - button.style.alignItems = 'center' - button.style.justifyContent = 'center' - button.innerHTML = scribbleSvg - button.onclick = () => { - showBoardPopup(board, this.editor) - } - dom.appendChild(button) - - const resizer = document.createElement('div') - resizer.style.position = 'absolute' - resizer.style.bottom = '0' - resizer.style.left = 'calc(50% - 4rem)' - resizer.style.width = '8rem' - resizer.style.height = '0.6rem' - resizer.style.cursor = 'row-resize' - resizer.style.color = 'var(--global-on-accent-TextColor)' - resizer.style.backgroundColor = 'var(--global-accent-IconColor)' - resizer.style.border = selectedBorder - resizer.style.display = 'flex' - resizer.style.alignItems = 'center' - resizer.style.justifyContent = 'center' - resizer.style.visibility = 'hidden' - resizer.style.borderTopLeftRadius = 'var(--small-BorderRadius)' - resizer.style.borderTopRightRadius = 'var(--small-BorderRadius)' - resizer.style.borderBottom = 'none' - resizer.style.opacity = '0.5' - resizer.innerHTML = chevronSvg - resizer.onpointerenter = () => { - resizer.style.opacity = '1' - } - resizer.onpointerleave = () => { - resizer.style.opacity = '0.5' - } - resizer.onpointerdown = (e: PointerEvent): void => { - e.preventDefault() - resizer.setPointerCapture(e.pointerId) - const { offsetHeight } = dom - const startY = e.clientY - offsetHeight - let height = node.attrs.height ?? defaultHeight - const onPointerMove = (e: PointerEvent): void => { - e.preventDefault() - height = Math.max(minHeight, e.clientY - startY) - height = Math.min(maxHeight, height) - dom.style.height = `${height}px` - } - const onPointerUp = (e: PointerEvent): void => { - e.preventDefault() - resizer.releasePointerCapture(e.pointerId) - if (typeof getPos === 'function') { - const tr = this.editor.state.tr.setNodeMarkup(getPos(), undefined, { ...node.attrs, height }) - this.editor.view.dispatch(tr) - } - dom.removeEventListener('pointermove', onPointerMove) - dom.removeEventListener('pointerup', onPointerUp) - } - dom.addEventListener('pointermove', onPointerMove) - dom.addEventListener('pointerup', onPointerUp) - } - dom.appendChild(resizer) - - return { - dom, - selectNode: () => { - dom.style.border = selectedBorder - button.style.visibility = 'visible' - resizer.style.visibility = 'visible' - }, - deselectNode: () => { - dom.style.border = normalBorder - button.style.visibility = 'hidden' - resizer.style.visibility = 'hidden' - }, - destroy: () => { - board.commands.unobserve(listenSavedCommands) - board.props.unobserve(listenSavedProps) - } - } - } + }) } }) From 26e93fefdb1bbdecb24bba6d85092c92fa9b9cf3 Mon Sep 17 00:00:00 2001 From: Andrey Sobolev Date: Wed, 4 Dec 2024 14:10:36 +0700 Subject: [PATCH 4/5] UBERF-8615: Backup/restore fixes (#7258) Signed-off-by: Andrey Sobolev --- pods/server/src/__start.ts | 6 ++- server/backup/src/backup.ts | 18 +++++++- server/postgres/src/schemas.ts | 24 ++++++++++ server/postgres/src/storage.ts | 71 ++++++++++++++++------------- server/postgres/src/utils.ts | 38 ++++++++++++--- server/server/src/sessionManager.ts | 3 +- 6 files changed, 118 insertions(+), 42 deletions(-) diff --git a/pods/server/src/__start.ts b/pods/server/src/__start.ts index c52052600f..f89bac6965 100644 --- a/pods/server/src/__start.ts +++ b/pods/server/src/__start.ts @@ -122,7 +122,11 @@ const close = (): void => { } process.on('unhandledRejection', (reason, promise) => { - console.log('Unhandled Rejection at:', promise, 'reason:', reason) + metricsContext.error('Unhandled Rejection at:', { origin, promise }) +}) + +global.process.on('uncaughtException', (error, origin) => { + metricsContext.error('Uncaught Exception at:', { origin, error }) }) process.on('SIGINT', close) diff --git a/server/backup/src/backup.ts b/server/backup/src/backup.ts index ed1e7d323a..302962e046 100644 --- a/server/backup/src/backup.ts +++ b/server/backup/src/backup.ts @@ -32,11 +32,13 @@ import core, { Ref, SortingOrder, systemAccountEmail, + TxProcessor, WorkspaceId, type BackupStatus, type Blob, type DocIndexState, - type Tx + type Tx, + type TxCUD } from '@hcengineering/core' import { BlobClient, createClient } from '@hcengineering/server-client' import { type StorageAdapter } from '@hcengineering/server-core' @@ -1768,8 +1770,20 @@ export async function restore ( d.space = core.space.Workspace ;(d as any)['%hash%'] = '' } + + if (TxProcessor.isExtendsCUD(d._class)) { + const tx = d as TxCUD + if (tx.objectSpace == null) { + tx.objectSpace = core.space.Workspace + ;(tx as any)['%hash%'] = '' + } + } + } + try { + await connection.upload(c, docs) + } catch (err: any) { + ctx.error('error during upload', { err, docs: JSON.stringify(docs) }) } - await connection.upload(c, docs) docs.length = 0 sendSize = 0 } diff --git a/server/postgres/src/schemas.ts b/server/postgres/src/schemas.ts index 253de0bfca..ffdd50d90b 100644 --- a/server/postgres/src/schemas.ts +++ b/server/postgres/src/schemas.ts @@ -241,3 +241,27 @@ export function getDocFieldsByDomains (domain: string): string[] { const schema = domainSchemas[translateDomain(domain)] ?? defaultSchema return Object.keys(schema) } + +export interface SchemaAndFields { + schema: Schema + + fields: string[] + domainFields: Set +} + +function createSchemaFields (schema: Schema): SchemaAndFields { + const fields = Object.keys(schema) + const domainFields = new Set(Object.keys(schema)) + return { schema, fields, domainFields } +} + +const defaultSchemaFields: SchemaAndFields = createSchemaFields(defaultSchema) + +const domainSchemaFields = new Map() +for (const [domain, _schema] of Object.entries(domainSchemas)) { + domainSchemaFields.set(domain, createSchemaFields(_schema)) +} + +export function getSchemaAndFields (domain: string): SchemaAndFields { + return domainSchemaFields.get(translateDomain(domain)) ?? defaultSchemaFields +} diff --git a/server/postgres/src/storage.ts b/server/postgres/src/storage.ts index ccce30a4f6..5d56814511 100644 --- a/server/postgres/src/storage.ts +++ b/server/postgres/src/storage.ts @@ -65,7 +65,14 @@ import { type TxAdapter } from '@hcengineering/server-core' import type postgres from 'postgres' -import { getDocFieldsByDomains, getSchema, type Schema, translateDomain } from './schemas' +import { + getDocFieldsByDomains, + getSchema, + getSchemaAndFields, + type Schema, + type SchemaAndFields, + translateDomain +} from './schemas' import { type ValueType } from './types' import { convertDoc, @@ -442,22 +449,21 @@ abstract class PostgresAdapterBase implements DbAdapter { if ((operations as any)['%hash%'] === undefined) { ;(operations as any)['%hash%'] = null } - const domainFields = new Set(getDocFieldsByDomains(domain)) + const schemaFields = getSchemaAndFields(domain) if (isOps) { await this.mgr.write(undefined, async (client) => { const res = await client.unsafe(`SELECT * FROM ${translateDomain(domain)} WHERE ${translatedQuery} FOR UPDATE`) - const schema = getSchema(domain) - const docs = res.map((p) => parseDoc(p as any, schema)) + const docs = res.map((p) => parseDoc(p as any, schemaFields.schema)) for (const doc of docs) { if (doc === undefined) continue const prevAttachedTo = (doc as any).attachedTo TxProcessor.applyUpdate(doc, operations) ;(doc as any)['%hash%'] = null - const converted = convertDoc(domain, doc, this.workspaceId.name, domainFields) + const converted = convertDoc(domain, doc, this.workspaceId.name, schemaFields) const params: any[] = [doc._id, this.workspaceId.name] let paramsIndex = params.length + 1 const updates: string[] = [] - const { extractedFields, remainingData } = parseUpdate(operations, domainFields) + const { extractedFields, remainingData } = parseUpdate(operations, schemaFields) const newAttachedTo = (doc as any).attachedTo if (Object.keys(extractedFields).length > 0) { for (const key in extractedFields) { @@ -482,7 +488,7 @@ abstract class PostgresAdapterBase implements DbAdapter { } }) } else { - await this.rawUpdateDoc(domain, query, operations, domainFields) + await this.rawUpdateDoc(domain, query, operations, schemaFields) } } @@ -490,13 +496,13 @@ abstract class PostgresAdapterBase implements DbAdapter { domain: Domain, query: DocumentQuery, operations: DocumentUpdate, - domainFields: Set + schemaFields: SchemaAndFields ): Promise { const translatedQuery = this.buildRawQuery(domain, query) const updates: string[] = [] const params: any[] = [] let paramsIndex = params.length + 1 - const { extractedFields, remainingData } = parseUpdate(operations, domainFields) + const { extractedFields, remainingData } = parseUpdate(operations, schemaFields) const { space, attachedTo, ...ops } = operations as any for (const key in extractedFields) { updates.push(`"${key}" = $${paramsIndex++}`) @@ -1397,8 +1403,8 @@ abstract class PostgresAdapterBase implements DbAdapter { upload (ctx: MeasureContext, domain: Domain, docs: Doc[]): Promise { return ctx.with('upload', { domain }, async (ctx) => { - const fields = getDocFieldsByDomains(domain) - const filedsWithData = [...fields, 'data'] + const schemaFields = getSchemaAndFields(domain) + const filedsWithData = [...schemaFields.fields, 'data'] const insertFields: string[] = [] const onConflict: string[] = [] for (const field of filedsWithData) { @@ -1409,7 +1415,6 @@ abstract class PostgresAdapterBase implements DbAdapter { const onConflictStr = onConflict.join(', ') try { - const domainFields = new Set(getDocFieldsByDomains(domain)) const toUpload = [...docs] const tdomain = translateDomain(domain) while (toUpload.length > 0) { @@ -1427,11 +1432,11 @@ abstract class PostgresAdapterBase implements DbAdapter { } const size = digest != null ? estimateDocSize(doc) : 0 ;(doc as any)['%hash%'] = digest == null ? null : `${digest}|${size.toString(16)}` - const d = convertDoc(domain, doc, this.workspaceId.name, domainFields) + const d = convertDoc(domain, doc, this.workspaceId.name, schemaFields) values.push(d.workspaceId) variables.push(`$${index++}`) - for (const field of fields) { + for (const field of schemaFields.fields) { values.push(d[field]) variables.push(`$${index++}`) } @@ -1504,7 +1509,7 @@ abstract class PostgresAdapterBase implements DbAdapter { const schema = getSchema(domain) const docs = res.map((p) => parseDoc(p, schema)) const map = new Map(docs.map((d) => [d._id, d])) - const domainFields = new Set(getDocFieldsByDomains(domain)) + const schemaFields = getSchemaAndFields(domain) for (const [_id, ops] of operations) { const doc = map.get(_id) if (doc === undefined) continue @@ -1513,10 +1518,10 @@ abstract class PostgresAdapterBase implements DbAdapter { ;(op as any)['%hash%'] = null } TxProcessor.applyUpdate(doc, op) - const converted = convertDoc(domain, doc, this.workspaceId.name, domainFields) + const converted = convertDoc(domain, doc, this.workspaceId.name, schemaFields) const columns: string[] = [] - const { extractedFields, remainingData } = parseUpdate(op, domainFields) + const { extractedFields, remainingData } = parseUpdate(op, schemaFields) for (const key in extractedFields) { columns.push(key) } @@ -1539,20 +1544,18 @@ abstract class PostgresAdapterBase implements DbAdapter { @withContext('insert') async insert (ctx: MeasureContext, domain: string, docs: Doc[]): Promise { - const schema = getSchema(domain) - const fields = Object.keys(schema) - const filedsWithData = [...fields, 'data'] + const schemaFields = getSchemaAndFields(domain) + const filedsWithData = [...schemaFields.fields, 'data'] const columns: string[] = ['workspaceId'] for (const field of filedsWithData) { columns.push(field) } - const domainFields = new Set(fields) while (docs.length > 0) { const part = docs.splice(0, 500) const values: DBDoc[] = [] for (let i = 0; i < part.length; i++) { const doc = part[i] - const d = convertDoc(domain, doc, this.workspaceId.name, domainFields) + const d = convertDoc(domain, doc, this.workspaceId.name, schemaFields) values.push(d) } await this.mgr.write(ctx.id, async (client) => { @@ -1608,7 +1611,7 @@ class PostgresAdapter extends PostgresAdapterBase { } } - private async txMixin (ctx: MeasureContext, tx: TxMixin, domainFields: Set): Promise { + private async txMixin (ctx: MeasureContext, tx: TxMixin, schemaFields: SchemaAndFields): Promise { await ctx.with('tx-mixin', { _class: tx.objectClass, mixin: tx.mixin }, async (ctx) => { await this.mgr.write(ctx.id, async (client) => { const doc = await this.findDoc(ctx, client, tx.objectClass, tx.objectId, true) @@ -1616,8 +1619,8 @@ class PostgresAdapter extends PostgresAdapterBase { TxProcessor.updateMixin4Doc(doc, tx) ;(doc as any)['%hash%'] = null const domain = this.hierarchy.getDomain(tx.objectClass) - const converted = convertDoc(domain, doc, this.workspaceId.name, domainFields) - const { extractedFields } = parseUpdate(tx.attributes as Partial, domainFields) + const converted = convertDoc(domain, doc, this.workspaceId.name, schemaFields) + const { extractedFields } = parseUpdate(tx.attributes as Partial, schemaFields) const columns = new Set() for (const key in extractedFields) { columns.add(key) @@ -1625,6 +1628,7 @@ class PostgresAdapter extends PostgresAdapterBase { columns.add('modifiedBy') columns.add('modifiedOn') columns.add('data') + columns.add('%hash%') await client`UPDATE ${client(translateDomain(domain))} SET ${client(converted, Array.from(columns))} WHERE _id = ${tx.objectId} AND "workspaceId" = ${this.workspaceId.name}` }) }) @@ -1655,7 +1659,7 @@ class PostgresAdapter extends PostgresAdapterBase { this.process(ops, tx) } - const domainFields = new Set(getDocFieldsByDomains(domain)) + const domainFields = getSchemaAndFields(domain) if (ops.add.length > 0) { const res = await this.insert(ctx, domain, ops.add) if (Object.keys(res).length > 0) { @@ -1694,7 +1698,7 @@ class PostgresAdapter extends PostgresAdapterBase { ctx: MeasureContext, domain: Domain, txes: TxUpdateDoc[], - domainFields: Set + schemaFields: SchemaAndFields ): Promise { const byOperator = groupByArray(txes, (it) => isOperator(it.operations)) @@ -1715,15 +1719,18 @@ class PostgresAdapter extends PostgresAdapterBase { ops.modifiedOn = tx.modifiedOn TxProcessor.applyUpdate(doc, ops) ;(doc as any)['%hash%'] = null - const converted = convertDoc(domain, doc, this.workspaceId.name, domainFields) + const converted = convertDoc(domain, doc, this.workspaceId.name, schemaFields) const columns: string[] = [] - const { extractedFields, remainingData } = parseUpdate(ops, domainFields) + const { extractedFields, remainingData } = parseUpdate(ops, schemaFields) for (const key in extractedFields) { columns.push(key) } if (Object.keys(remainingData).length > 0) { columns.push('data') } + if (!columns.includes('%hash%')) { + columns.push('%hash%') + } await client`UPDATE ${client(translateDomain(domain))} SET ${client(converted, columns)} WHERE _id = ${tx.objectId} AND "workspaceId" = ${this.workspaceId.name}` }) if (tx.retrieve === true && doc !== undefined) { @@ -1734,7 +1741,7 @@ class PostgresAdapter extends PostgresAdapterBase { ) } if ((withoutOperator ?? [])?.length > 0) { - result.push(...(await this.updateDoc(ctx, domain, withoutOperator ?? [], domainFields))) + result.push(...(await this.updateDoc(ctx, domain, withoutOperator ?? [], schemaFields))) } return result } @@ -1743,7 +1750,7 @@ class PostgresAdapter extends PostgresAdapterBase { ctx: MeasureContext, domain: Domain, txes: TxUpdateDoc[], - domainFields: Set + schemaFields: SchemaAndFields ): Promise { return ctx.with('update jsonb_set', {}, async (_ctx) => { const operations: { @@ -1760,7 +1767,7 @@ class PostgresAdapter extends PostgresAdapterBase { const updates: string[] = ['"modifiedBy" = $1', '"modifiedOn" = $2', '"%hash%" = $3'] const params: any[] = [tx.modifiedBy, tx.modifiedOn, null] let paramsIndex = params.length - const { extractedFields, remainingData } = parseUpdate(tx.operations, domainFields) + const { extractedFields, remainingData } = parseUpdate(tx.operations, schemaFields) const { space, attachedTo, ...ops } = tx.operations as any for (const key in extractedFields) { fields.push(key) diff --git a/server/postgres/src/utils.ts b/server/postgres/src/utils.ts index 60e821de15..1d1e9cc70f 100644 --- a/server/postgres/src/utils.ts +++ b/server/postgres/src/utils.ts @@ -36,7 +36,9 @@ import { getDocFieldsByDomains, getIndex, getSchema, + getSchemaAndFields, type Schema, + type SchemaAndFields, translateDomain } from './schemas' @@ -264,7 +266,7 @@ export function convertDoc ( domain: string, doc: T, workspaceId: string, - domainFields?: Set + schemaAndFields?: SchemaAndFields ): DBDoc { const extractedFields: Doc & Record = { _id: doc._id, @@ -280,19 +282,43 @@ export function convertDoc ( const extractedFieldsKeys = new Set(Object.keys(extractedFields)) - domainFields = domainFields ?? new Set(getDocFieldsByDomains(domain)) + schemaAndFields = schemaAndFields ?? getSchemaAndFields(domain) for (const key in doc) { if (extractedFieldsKeys.has(key)) { continue } - if (domainFields.has(key)) { + if (schemaAndFields.domainFields.has(key)) { extractedFields[key] = doc[key] } else { remainingData[key] = doc[key] } } + // Check if some fields are missing + for (const [key, _type] of Object.entries(schemaAndFields.schema)) { + if (!(key in doc)) { + // We missing required field, and we need to add a dummy value for it. + if (_type.notNull) { + // Null value is not allowed + switch (_type.type) { + case 'bigint': + extractedFields[key] = 0 + break + case 'bool': + extractedFields[key] = false + break + case 'text': + extractedFields[key] = '' + break + case 'text[]': + extractedFields[key] = [] + break + } + } + } + } + const res: any = { ...extractedFields, workspaceId, @@ -328,7 +354,7 @@ export function inferType (val: any): string { export function parseUpdate ( ops: DocumentUpdate | MixinUpdate, - fields: Set + schemaFields: SchemaAndFields ): { extractedFields: Partial remainingData: Partial @@ -340,14 +366,14 @@ export function parseUpdate ( const val = (ops as any)[key] if (key.startsWith('$')) { for (const k in val) { - if (fields.has(k)) { + if (schemaFields.domainFields.has(k)) { ;(extractedFields as any)[k] = val[key] } else { ;(remainingData as any)[k] = val[key] } } } else { - if (fields.has(key)) { + if (schemaFields.domainFields.has(key)) { ;(extractedFields as any)[key] = val } else { ;(remainingData as any)[key] = val diff --git a/server/server/src/sessionManager.ts b/server/server/src/sessionManager.ts index 01120bbdda..3daf92f423 100644 --- a/server/server/src/sessionManager.ts +++ b/server/server/src/sessionManager.ts @@ -17,6 +17,7 @@ import { Analytics } from '@hcengineering/analytics' import core, { TxFactory, WorkspaceEvent, + cutObjectArray, generateId, isWorkspaceCreating, systemAccountEmail, @@ -249,7 +250,7 @@ class TSessionManager implements SessionManager { sec, wsId, user: s[1].session.getUser(), - ...r.params + ...cutObjectArray(r.params) }) } } From eb5bcdf07f76177ad58a4b6c7734dcd5def40c9c Mon Sep 17 00:00:00 2001 From: Alexander Platov Date: Thu, 5 Dec 2024 08:33:39 +0300 Subject: [PATCH 5/5] The alarm button in the sidebar for the Meeting (#7260) Signed-off-by: Alexander Platov --- models/love/src/index.ts | 3 +- models/love/src/plugin.ts | 3 +- models/workbench/src/index.ts | 1 + .../components/widget/WidgetSwitcher.svelte | 34 +++++++++++++++++++ plugins/love-resources/src/index.ts | 2 ++ .../src/components/AppItem.svelte | 33 +++++++++++++++++- .../src/components/Workbench.svelte | 4 +-- .../sidebar/widgets/WidgetPresenter.svelte | 8 +++-- plugins/workbench-resources/src/index.ts | 1 + plugins/workbench/src/index.ts | 1 + tests/sanity/tests/chat/chat.spec.ts | 4 +-- tests/sanity/tests/model/channel-page.ts | 8 ++--- tests/sanity/tests/model/common-page.ts | 7 +++- 13 files changed, 93 insertions(+), 16 deletions(-) create mode 100644 plugins/love-resources/src/components/widget/WidgetSwitcher.svelte diff --git a/models/love/src/index.ts b/models/love/src/index.ts index b25c90400b..a5222eeef7 100644 --- a/models/love/src/index.ts +++ b/models/love/src/index.ts @@ -288,7 +288,8 @@ export function createModel (builder: Builder): void { label: love.string.Meeting, type: WidgetType.Flexible, icon: love.icon.Cam, - component: love.component.MeetingWidget + component: love.component.MeetingWidget, + switcherComponent: love.component.WidgetSwitcher }, love.ids.MeetingWidget ) diff --git a/models/love/src/plugin.ts b/models/love/src/plugin.ts index c7a5c088cf..ef026ba0cf 100644 --- a/models/love/src/plugin.ts +++ b/models/love/src/plugin.ts @@ -27,7 +27,8 @@ export default mergeIds(loveId, love, { WorkbenchExtension: '' as AnyComponent, Settings: '' as AnyComponent, LoveWidget: '' as AnyComponent, - MeetingWidget: '' as AnyComponent + MeetingWidget: '' as AnyComponent, + WidgetSwitcher: '' as AnyComponent }, app: { Love: '' as Ref diff --git a/models/workbench/src/index.ts b/models/workbench/src/index.ts index 729bf39a56..96dae1a6f1 100644 --- a/models/workbench/src/index.ts +++ b/models/workbench/src/index.ts @@ -81,6 +81,7 @@ export class TWidget extends TDoc implements Widget { component!: AnyComponent tabComponent?: AnyComponent + switcherComponent?: AnyComponent headerLabel?: IntlString closeIfNoTabs?: boolean diff --git a/plugins/love-resources/src/components/widget/WidgetSwitcher.svelte b/plugins/love-resources/src/components/widget/WidgetSwitcher.svelte new file mode 100644 index 0000000000..d0e7718bb5 --- /dev/null +++ b/plugins/love-resources/src/components/widget/WidgetSwitcher.svelte @@ -0,0 +1,34 @@ + + + + diff --git a/plugins/love-resources/src/index.ts b/plugins/love-resources/src/index.ts index 0a32f9b44f..c06f50ae41 100644 --- a/plugins/love-resources/src/index.ts +++ b/plugins/love-resources/src/index.ts @@ -11,6 +11,7 @@ import Settings from './components/Settings.svelte' import WorkbenchExtension from './components/WorkbenchExtension.svelte' import LoveWidget from './components/LoveWidget.svelte' import MeetingWidget from './components/widget/MeetingWidget.svelte' +import WidgetSwitcher from './components/widget/WidgetSwitcher.svelte' import MeetingMinutesPresenter from './components/MeetingMinutesPresenter.svelte' import MeetingMinutesSection from './components/MeetingMinutesSection.svelte' import EditMeetingMinutes from './components/EditMeetingMinutes.svelte' @@ -47,6 +48,7 @@ export default async (): Promise => ({ EditMeetingData, LoveWidget, MeetingWidget, + WidgetSwitcher, MeetingMinutesPresenter, MeetingMinutesSection, EditMeetingMinutes, diff --git a/plugins/workbench-resources/src/components/AppItem.svelte b/plugins/workbench-resources/src/components/AppItem.svelte index 6ac0e969dc..02c26f6b3b 100644 --- a/plugins/workbench-resources/src/components/AppItem.svelte +++ b/plugins/workbench-resources/src/components/AppItem.svelte @@ -21,13 +21,14 @@ export let icon: Asset | AnySvelteComponent export let selected: boolean = false export let size: 'small' | 'medium' | 'large' = 'large' + export let kind: 'default' | 'positive' | 'negative' = 'default' export let loading: boolean = false export let notify: boolean = false export let navigator: boolean = false