mirror of
https://github.com/hcengineering/platform.git
synced 2026-10-01 05:55:09 +02:00
Merge remote-tracking branch 'origin/develop' into staging
Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
@@ -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
|
||||
)
|
||||
|
||||
@@ -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<Doc>
|
||||
|
||||
@@ -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' },
|
||||
|
||||
@@ -81,6 +81,7 @@ export class TWidget extends TDoc implements Widget {
|
||||
|
||||
component!: AnyComponent
|
||||
tabComponent?: AnyComponent
|
||||
switcherComponent?: AnyComponent
|
||||
headerLabel?: IntlString
|
||||
|
||||
closeIfNoTabs?: boolean
|
||||
|
||||
@@ -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}
|
||||
<div class="divider buttons-divider" />
|
||||
{#if tool === 'pen'}
|
||||
<input type="range" min={2} max={20} step={2} bind:value={penWidth} on:change={updatePenWidth} />
|
||||
<input
|
||||
class="widthSelector"
|
||||
type="range"
|
||||
min={2}
|
||||
max={18}
|
||||
step={4}
|
||||
bind:value={penWidth}
|
||||
on:change={updatePenWidth}
|
||||
/>
|
||||
{:else}
|
||||
<input type="range" min={10} max={100} step={10} bind:value={eraserWidth} on:change={updateEraserWidth} />
|
||||
<input
|
||||
class="widthSelector"
|
||||
type="range"
|
||||
min={20}
|
||||
max={110}
|
||||
step={30}
|
||||
bind:value={eraserWidth}
|
||||
on:change={updateEraserWidth}
|
||||
/>
|
||||
{/if}
|
||||
<div class="divider buttons-divider" />
|
||||
{#each penColors as color}
|
||||
@@ -247,4 +263,8 @@
|
||||
width: 0;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.widthSelector {
|
||||
width: 80px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
</div>
|
||||
{#if showConnectionButton(object, $isConnected, $myOffice, $currentRoom)}
|
||||
{#if showConnectionButton(object, connecting, $isConnected, $myOffice, $currentRoom)}
|
||||
<ModernButton label={connectLabel} size="large" kind={'primary'} on:click={connect} loading={connecting} />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
<!--
|
||||
// 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.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import type { Asset, IntlString } from '@hcengineering/platform'
|
||||
import type { AnySvelteComponent } from '@hcengineering/ui'
|
||||
import { AppItem } from '@hcengineering/workbench-resources'
|
||||
import { isConnected, screenSharing } from '../../utils'
|
||||
|
||||
export let label: IntlString
|
||||
export let icon: Asset | AnySvelteComponent
|
||||
export let selected: boolean = false
|
||||
export let size: 'small' | 'medium' | 'large' = 'small'
|
||||
</script>
|
||||
|
||||
<AppItem
|
||||
{label}
|
||||
{icon}
|
||||
{selected}
|
||||
{size}
|
||||
kind={$screenSharing && !selected ? 'negative' : $isConnected && !selected ? 'positive' : 'default'}
|
||||
on:click
|
||||
/>
|
||||
@@ -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<Resources> => ({
|
||||
EditMeetingData,
|
||||
LoveWidget,
|
||||
MeetingWidget,
|
||||
WidgetSwitcher,
|
||||
MeetingMinutesPresenter,
|
||||
MeetingMinutesSection,
|
||||
EditMeetingMinutes,
|
||||
|
||||
@@ -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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
}
|
||||
}
|
||||
|
||||
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<void>, retries: number, delay: number): Promise<void> {
|
||||
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))
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "Результат",
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
"TestCase": "測試用例",
|
||||
"TestProject": "專案",
|
||||
"TestManagementApplication": "測試管理",
|
||||
"AllTestCases": "所有測試用例",
|
||||
"AllTestSuites": "所有測試套件",
|
||||
"AllProjects": "所有項目",
|
||||
"Projects": "專案",
|
||||
"CreateProject": "創建專案",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -16,7 +16,15 @@
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { Analytics } from '@hcengineering/analytics'
|
||||
import { type Doc, generateId, makeDocCollabId } from '@hcengineering/core'
|
||||
import {
|
||||
type Blob,
|
||||
Class,
|
||||
type CollaborativeDoc,
|
||||
type Doc,
|
||||
type Ref,
|
||||
generateId,
|
||||
makeDocCollabId
|
||||
} from '@hcengineering/core'
|
||||
import { IntlString, translate } from '@hcengineering/platform'
|
||||
import {
|
||||
getAttribute,
|
||||
@@ -24,7 +32,8 @@
|
||||
getFileUrl,
|
||||
getImageSize,
|
||||
imageSizeToRatio,
|
||||
KeyedAttribute
|
||||
KeyedAttribute,
|
||||
DrawingCmd
|
||||
} from '@hcengineering/presentation'
|
||||
import { markupToJSON } from '@hcengineering/text'
|
||||
import {
|
||||
@@ -45,7 +54,7 @@
|
||||
import CollaborationCursor from '@tiptap/extension-collaboration-cursor'
|
||||
import Placeholder from '@tiptap/extension-placeholder'
|
||||
import { createEventDispatcher, getContext, onDestroy, onMount } from 'svelte'
|
||||
import { Array as YArray, Doc as YDoc, Map as YMap } from 'yjs'
|
||||
import { Doc as YDoc } from 'yjs'
|
||||
|
||||
import { Completion } from '../Completion'
|
||||
import { deleteAttachment } from '../command/deleteAttachment'
|
||||
@@ -66,7 +75,7 @@
|
||||
import TextEditorToolbar from './TextEditorToolbar.svelte'
|
||||
import { noSelectionRender, renderCursor } from './editor/collaboration'
|
||||
import { defaultEditorAttributes } from './editor/editorProps'
|
||||
import { DrawingBoardExtension } from './extension/drawingBoard'
|
||||
import { DrawingBoardExtension, SavedBoard } from './extension/drawingBoard'
|
||||
import { EmojiExtension } from './extension/emoji'
|
||||
import { FileUploadExtension } from './extension/fileUploadExt'
|
||||
import { ImageUploadExtension } from './extension/imageUploadExt'
|
||||
@@ -364,7 +373,7 @@
|
||||
editor.commands.setHorizontalRule()
|
||||
break
|
||||
case 'drawing-board':
|
||||
makeNewDrawingBoard(pos)
|
||||
editor.commands.insertContentAt(pos, { type: 'drawingBoard', attrs: { id: generateId() } })
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -374,14 +383,39 @@
|
||||
remoteProvider.awareness?.setLocalStateField('lastUpdate', Date.now())
|
||||
}
|
||||
|
||||
function makeNewDrawingBoard (pos: number): void {
|
||||
const id = generateId()
|
||||
ydoc.getArray('drawing-board-registry').push([id])
|
||||
const drawing = ydoc.getMap(`drawing-board-${id}`)
|
||||
drawing.set('commands', new YArray())
|
||||
drawing.set('props', new YMap())
|
||||
editor.commands.insertContentAt(pos, { type: 'drawingBoard', attrs: { id } })
|
||||
editor.commands.showDrawingBoardPopup()
|
||||
interface SavedBoardRaw {
|
||||
ydoc: YDoc
|
||||
localProvider: Provider
|
||||
remoteProvider: Provider
|
||||
localSynced: boolean
|
||||
remoteSynced: boolean
|
||||
}
|
||||
const savedBoards: Record<string, SavedBoardRaw> = {}
|
||||
|
||||
function getSavedBoard (id: string): SavedBoard {
|
||||
let board = savedBoards[id]
|
||||
if (board === undefined) {
|
||||
const ydoc = new YDoc({ guid: id })
|
||||
// We don't have a real class for boards,
|
||||
// but collaborator only needs a string id
|
||||
// which is produced from such an id-object
|
||||
const collabId: CollaborativeDoc = {
|
||||
objectClass: 'DrawingBoard' as Ref<Class<Doc>>,
|
||||
objectId: id as Ref<Doc>,
|
||||
objectAttr: 'content'
|
||||
}
|
||||
const localProvider = createLocalProvider(ydoc, collabId)
|
||||
const remoteProvider = createRemoteProvider(ydoc, collabId, id as Ref<Blob>)
|
||||
savedBoards[id] = { ydoc, localProvider, remoteProvider, localSynced: false, remoteSynced: false }
|
||||
void localProvider.loaded.then(() => (savedBoards[id].localSynced = true))
|
||||
void remoteProvider.loaded.then(() => (savedBoards[id].remoteSynced = true))
|
||||
board = savedBoards[id]
|
||||
}
|
||||
return {
|
||||
props: board.ydoc.getMap('props'),
|
||||
commands: board.ydoc.getArray<DrawingCmd>('commands'),
|
||||
loading: !board.localSynced || !board.remoteSynced
|
||||
}
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
@@ -432,7 +466,7 @@
|
||||
}
|
||||
}),
|
||||
EmojiExtension,
|
||||
DrawingBoardExtension.configure({ ydoc }),
|
||||
DrawingBoardExtension.configure({ getSavedBoard }),
|
||||
...extensions
|
||||
],
|
||||
parseOptions: {
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
<!--
|
||||
// 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.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { DrawingBoardToolbar, DrawingCmd, DrawingTool, drawing } from '@hcengineering/presentation'
|
||||
import { Loading } from '@hcengineering/ui'
|
||||
import { onMount, onDestroy } from 'svelte'
|
||||
import { Array as YArray, Map as YMap } from 'yjs'
|
||||
|
||||
export let savedCmds: YArray<DrawingCmd>
|
||||
export let savedProps: YMap<any>
|
||||
export let grabFocus = false
|
||||
export let resizeable = false
|
||||
export let height: number | undefined = undefined
|
||||
export let readonly = false
|
||||
export let selected = false
|
||||
export let loading = false
|
||||
|
||||
let tool: DrawingTool
|
||||
let penColor: string
|
||||
let penWidth: number
|
||||
let eraserWidth: number
|
||||
let commandCount: number
|
||||
let commands: DrawingCmd[] = []
|
||||
let offset: { x: number, y: number }
|
||||
let toolbar: HTMLDivElement
|
||||
|
||||
function listenSavedCommands (): void {
|
||||
if (savedCmds.length === 0) {
|
||||
commands = []
|
||||
} else {
|
||||
for (let i = commands.length; i < savedCmds.length; i++) {
|
||||
commands.push(savedCmds.get(i))
|
||||
}
|
||||
}
|
||||
commandCount = savedCmds.length
|
||||
}
|
||||
|
||||
function listenSavedProps (): void {
|
||||
offset = savedProps.get('offset')
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
commands = savedCmds.toArray()
|
||||
offset = savedProps.get('offset')
|
||||
savedCmds.observe(listenSavedCommands)
|
||||
savedProps.observe(listenSavedProps)
|
||||
})
|
||||
|
||||
onDestroy(() => {
|
||||
savedCmds.unobserve(listenSavedCommands)
|
||||
savedProps.unobserve(listenSavedProps)
|
||||
})
|
||||
</script>
|
||||
|
||||
{#if savedCmds !== undefined && savedProps !== undefined}
|
||||
{#if loading}
|
||||
<div
|
||||
class="board"
|
||||
class:selected
|
||||
style:flex-grow={resizeable ? undefined : '1'}
|
||||
style:height={resizeable ? `${height}px` : undefined}
|
||||
>
|
||||
<Loading />
|
||||
<slot />
|
||||
</div>
|
||||
{:else}
|
||||
<div
|
||||
class="board"
|
||||
class:selected
|
||||
style:flex-grow={resizeable ? undefined : '1'}
|
||||
style:height={resizeable ? `${height}px` : undefined}
|
||||
use:drawing={{
|
||||
readonly,
|
||||
autoSize: true,
|
||||
commandCount,
|
||||
commands,
|
||||
offset,
|
||||
tool,
|
||||
penColor,
|
||||
penWidth,
|
||||
eraserWidth,
|
||||
cmdAdded: (cmd) => {
|
||||
savedCmds.push([cmd])
|
||||
},
|
||||
panned: (offset) => {
|
||||
savedProps.set('offset', offset)
|
||||
}
|
||||
}}
|
||||
>
|
||||
{#if grabFocus}
|
||||
<!-- grab focus from the editor -->
|
||||
<!-- svelte-ignore a11y-autofocus -->
|
||||
<input style:opacity="0" autoFocus />
|
||||
{/if}
|
||||
{#if !readonly}
|
||||
<DrawingBoardToolbar
|
||||
placeInside={true}
|
||||
showPanTool={true}
|
||||
bind:toolbar
|
||||
bind:tool
|
||||
bind:penColor
|
||||
bind:penWidth
|
||||
bind:eraserWidth
|
||||
on:clear={() => {
|
||||
savedCmds.delete(0, savedCmds.length)
|
||||
savedProps.set('offset', { x: 0, y: 0 })
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
<slot />
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
<style lang="scss">
|
||||
.board {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
background-color: var(--theme-navpanel-color);
|
||||
border: 1px solid var(--theme-navpanel-border);
|
||||
border-radius: var(--small-BorderRadius);
|
||||
|
||||
&.selected {
|
||||
border: 1px solid var(--theme-editbox-focus-border);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,176 @@
|
||||
<!--
|
||||
// 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.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { Button, IconScribble } from '@hcengineering/ui'
|
||||
import { Node } from '@tiptap/pm/model'
|
||||
import { Editor } from '@tiptap/core'
|
||||
import { onDestroy, onMount } from 'svelte'
|
||||
import { showBoardPopup, SavedBoard } from './extension/drawingBoard'
|
||||
import NodeViewWrapper from './node-view/NodeViewWrapper.svelte'
|
||||
import DrawingBoardEditor from './DrawingBoardEditor.svelte'
|
||||
|
||||
export let getSavedBoard: (id: string) => SavedBoard
|
||||
export let node: Node
|
||||
export let editor: Editor
|
||||
export let selected: boolean
|
||||
export let getPos: any
|
||||
|
||||
const defaultHeight = 500
|
||||
const maxHeight = 1000
|
||||
const minHeight = 100
|
||||
|
||||
let savedBoard: SavedBoard
|
||||
let resizer: HTMLElement
|
||||
let startY: number
|
||||
let resizedHeight: number | undefined
|
||||
let loading = true
|
||||
let loadingTimer: any
|
||||
|
||||
function onResizerPointerDown (e: PointerEvent): void {
|
||||
e.preventDefault()
|
||||
const height = node.attrs.height ?? defaultHeight
|
||||
startY = e.clientY - height
|
||||
resizedHeight = height
|
||||
resizer.setPointerCapture(e.pointerId)
|
||||
resizer.addEventListener('pointermove', onResizerPointerMove)
|
||||
resizer.addEventListener('pointerup', onResizerPointerUp)
|
||||
}
|
||||
|
||||
function onResizerPointerMove (e: PointerEvent): void {
|
||||
e.preventDefault()
|
||||
resizedHeight = Math.max(minHeight, e.clientY - startY)
|
||||
resizedHeight = Math.min(maxHeight, resizedHeight)
|
||||
}
|
||||
|
||||
function onResizerPointerUp (e: PointerEvent): void {
|
||||
e.preventDefault()
|
||||
resizer.releasePointerCapture(e.pointerId)
|
||||
resizer.removeEventListener('pointermove', onResizerPointerMove)
|
||||
resizer.removeEventListener('pointerup', onResizerPointerUp)
|
||||
if (typeof getPos === 'function') {
|
||||
const tr = editor.state.tr.setNodeMarkup(getPos(), undefined, { ...node.attrs, height: resizedHeight })
|
||||
editor.view.dispatch(tr)
|
||||
}
|
||||
resizedHeight = undefined
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
let delay = 100
|
||||
const getBoard = (): void => {
|
||||
loadingTimer = undefined
|
||||
savedBoard = getSavedBoard(node.attrs.id)
|
||||
loading = savedBoard.loading
|
||||
if (loading) {
|
||||
loadingTimer = setTimeout(getBoard, delay)
|
||||
delay *= 1.5
|
||||
}
|
||||
}
|
||||
getBoard()
|
||||
})
|
||||
|
||||
onDestroy(() => {
|
||||
if (loadingTimer !== undefined) {
|
||||
clearTimeout(loadingTimer)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
{#if savedBoard?.commands !== undefined && savedBoard?.props !== undefined}
|
||||
<NodeViewWrapper data-drag-handle="" data-type="drawingBoard" data-id={node.attrs.id}>
|
||||
<DrawingBoardEditor
|
||||
savedCmds={savedBoard.commands}
|
||||
savedProps={savedBoard.props}
|
||||
resizeable={true}
|
||||
readonly={!selected}
|
||||
{loading}
|
||||
{selected}
|
||||
height={resizedHeight ?? node.attrs.height ?? defaultHeight}
|
||||
>
|
||||
<div class="openButtonContainer">
|
||||
<Button
|
||||
kind={selected ? 'primary' : 'ghost'}
|
||||
icon={IconScribble}
|
||||
disabled={loading}
|
||||
on:click={() => {
|
||||
showBoardPopup(savedBoard, editor)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{#if selected}
|
||||
<div class="handle resizer" bind:this={resizer} on:pointerdown={onResizerPointerDown}>
|
||||
<svg viewBox="0 0 60 4" height="4" width="60" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="m60 2a2 2 0 0 1 -2 2 2 2 0 0 1 -2-2 2 2 0 0 1 2-2 2 2 0 0 1 2 2zm-8 0a2 2 0 0 1 -2 2 2 2 0 0 1 -2-2 2 2 0 0 1 2-2 2 2 0 0 1 2 2zm-8 0a2 2 0 0 1 -2 2 2 2 0 0 1 -2-2 2 2 0 0 1 2-2 2 2 0 0 1 2 2zm-8 0a2 2 0 0 1 -2 2 2 2 0 0 1 -2-2 2 2 0 0 1 2-2 2 2 0 0 1 2 2zm-8 0a2 2 0 0 1 -2 2 2 2 0 0 1 -2-2 2 2 0 0 1 2-2 2 2 0 0 1 2 2zm-8 0a2 2 0 0 1 -2 2 2 2 0 0 1 -2-2 2 2 0 0 1 2-2 2 2 0 0 1 2 2zm-8 0a2 2 0 0 1 -2 2 2 2 0 0 1 -2-2 2 2 0 0 1 2-2 2 2 0 0 1 2 2zm-8 0a2 2 0 0 1 -2 2 2 2 0 0 1 -2-2 2 2 0 0 1 2-2 2 2 0 0 1 2 2z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="handle drag">
|
||||
<svg viewBox="0 0 4 28" height="28" width="4" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="m2 28c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2zm0-8c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2zm0-8c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2zm0-8c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
{/if}
|
||||
</DrawingBoardEditor>
|
||||
</NodeViewWrapper>
|
||||
{/if}
|
||||
|
||||
<style lang="scss">
|
||||
.openButtonContainer {
|
||||
z-index: 1;
|
||||
position: absolute;
|
||||
top: 0.3rem;
|
||||
right: 0.3rem;
|
||||
}
|
||||
|
||||
.handle {
|
||||
z-index: 1;
|
||||
position: absolute;
|
||||
color: var(--global-on-accent-TextColor);
|
||||
background-color: var(--global-accent-IconColor);
|
||||
border: 1px solid var(--theme-editbox-focus-border);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
opacity: 0.5;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.resizer {
|
||||
bottom: 0;
|
||||
left: calc(50% - 4rem);
|
||||
width: 8rem;
|
||||
height: 0.6rem;
|
||||
cursor: row-resize;
|
||||
border-top-left-radius: var(--small-BorderRadius);
|
||||
border-top-right-radius: var(--small-BorderRadius);
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.drag {
|
||||
left: -0.6rem;
|
||||
top: calc(50% - 2rem);
|
||||
width: 0.6rem;
|
||||
height: 4rem;
|
||||
cursor: move;
|
||||
border-top-left-radius: var(--small-BorderRadius);
|
||||
border-bottom-left-radius: var(--small-BorderRadius);
|
||||
border-right: none;
|
||||
}
|
||||
</style>
|
||||
@@ -13,109 +13,36 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { DrawingBoardToolbar, DrawingCmd, DrawingTool, drawing } from '@hcengineering/presentation'
|
||||
import { DrawingCmd } from '@hcengineering/presentation'
|
||||
import textEditor from '@hcengineering/text-editor'
|
||||
import { Dialog, FocusHandler, createFocusManager } from '@hcengineering/ui'
|
||||
import { createEventDispatcher, onMount, onDestroy } from 'svelte'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import { Array as YArray, Map as YMap } from 'yjs'
|
||||
import DrawingBoardEditor from './DrawingBoardEditor.svelte'
|
||||
|
||||
export let savedCmds: YArray<DrawingCmd>
|
||||
export let savedProps: YMap<any>
|
||||
export let fullSize = false
|
||||
|
||||
const manager = createFocusManager()
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
let tool: DrawingTool
|
||||
let penColor: string
|
||||
let penWidth: number
|
||||
let eraserWidth: number
|
||||
let commandCount: number
|
||||
let commands: DrawingCmd[] = []
|
||||
let offset: { x: number, y: number }
|
||||
let toolbar: HTMLDivElement
|
||||
|
||||
function listenSavedCommands (): void {
|
||||
if (savedCmds.length === 0) {
|
||||
commands = []
|
||||
} else {
|
||||
for (let i = commands.length; i < savedCmds.length; i++) {
|
||||
commands.push(savedCmds.get(i))
|
||||
}
|
||||
}
|
||||
commandCount = savedCmds.length
|
||||
let dialog: Dialog
|
||||
$: if (dialog !== undefined) {
|
||||
dialog.maximize()
|
||||
}
|
||||
|
||||
function listenSavedProps (): void {
|
||||
offset = savedProps.get('offset')
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
if (fullSize) {
|
||||
dispatch('fullsize')
|
||||
}
|
||||
|
||||
commands = savedCmds.toArray()
|
||||
offset = savedProps.get('offset')
|
||||
savedCmds.observe(listenSavedCommands)
|
||||
savedProps.observe(listenSavedProps)
|
||||
})
|
||||
|
||||
onDestroy(() => {
|
||||
savedCmds.unobserve(listenSavedCommands)
|
||||
savedProps.unobserve(listenSavedProps)
|
||||
})
|
||||
</script>
|
||||
|
||||
{#if savedCmds !== undefined && savedProps !== undefined}
|
||||
<FocusHandler {manager} />
|
||||
<Dialog
|
||||
isFullSize
|
||||
label={textEditor.string.DrawingBoard}
|
||||
padding="0"
|
||||
bind:this={dialog}
|
||||
on:fullsize
|
||||
on:close={() => {
|
||||
dispatch('close')
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style:position="relative"
|
||||
style:flex-grow="1"
|
||||
use:drawing={{
|
||||
readonly: false,
|
||||
autoSize: true,
|
||||
commandCount,
|
||||
commands,
|
||||
offset,
|
||||
tool,
|
||||
penColor,
|
||||
penWidth,
|
||||
eraserWidth,
|
||||
cmdAdded: (cmd) => {
|
||||
savedCmds.push([cmd])
|
||||
},
|
||||
panned: (offset) => {
|
||||
savedProps.set('offset', offset)
|
||||
}
|
||||
}}
|
||||
>
|
||||
<!-- grab focus from the editor -->
|
||||
<!-- svelte-ignore a11y-autofocus -->
|
||||
<input style:opacity="0" autoFocus />
|
||||
|
||||
<DrawingBoardToolbar
|
||||
placeInside={true}
|
||||
showPanTool={true}
|
||||
bind:toolbar
|
||||
bind:tool
|
||||
bind:penColor
|
||||
bind:penWidth
|
||||
bind:eraserWidth
|
||||
on:clear={() => {
|
||||
savedCmds.delete(0, savedCmds.length)
|
||||
savedProps.set('offset', { x: 0, y: 0 })
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<DrawingBoardEditor {savedCmds} {savedProps} grabFocus />
|
||||
</Dialog>
|
||||
{/if}
|
||||
|
||||
@@ -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 = `<svg width="20" height="20" viewBox="0 0 15 15" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="m1.07 4.9-1.07-.54c2.22-4.44 8.42-5.88 7.08-2.5-.94 2.35-5.16 5.95-5.44 8.11-.2 1.51 2.63 2.18 5.69 1.33.47-4.95 4.09-7.4 6.01-6.27 2.17 1.28.85 5.17-4.83 7.15.38 2.86 3.93 1.06 5.53.29l.54 1.07c-6.13 3.07-7.1.29-7.24-1.01-3.72.93-7.21-.14-6.88-2.72.34-2.66 4.66-6.26 5.51-8.4.37-.87-3.43.54-4.9 3.49zm7.53 5.98c4.03-1.59 5.28-4.14 4.13-4.81-.64-.38-3.36.35-4.13 4.81z" />
|
||||
</svg>`
|
||||
const chevronSvg = `<svg height="4" viewBox="0 0 60 4" width="60" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="m60 2a2 2 0 0 1 -2 2 2 2 0 0 1 -2-2 2 2 0 0 1 2-2 2 2 0 0 1 2 2zm-8 0a2 2 0 0 1 -2 2 2 2 0 0 1 -2-2 2 2 0 0 1 2-2 2 2 0 0 1 2 2zm-8 0a2 2 0 0 1 -2 2 2 2 0 0 1 -2-2 2 2 0 0 1 2-2 2 2 0 0 1 2 2zm-8 0a2 2 0 0 1 -2 2 2 2 0 0 1 -2-2 2 2 0 0 1 2-2 2 2 0 0 1 2 2zm-8 0a2 2 0 0 1 -2 2 2 2 0 0 1 -2-2 2 2 0 0 1 2-2 2 2 0 0 1 2 2zm-8 0a2 2 0 0 1 -2 2 2 2 0 0 1 -2-2 2 2 0 0 1 2-2 2 2 0 0 1 2 2zm-8 0a2 2 0 0 1 -2 2 2 2 0 0 1 -2-2 2 2 0 0 1 2-2 2 2 0 0 1 2 2zm-8 0a2 2 0 0 1 -2 2 2 2 0 0 1 -2-2 2 2 0 0 1 2-2 2 2 0 0 1 2 2z" />
|
||||
</svg>`
|
||||
|
||||
interface SavedBoard {
|
||||
export interface SavedBoard {
|
||||
commands: YArray<DrawingCmd>
|
||||
props: YMap<any>
|
||||
loading: boolean
|
||||
}
|
||||
|
||||
function getSavedBoard (ydoc: YDoc | undefined, id: string): SavedBoard {
|
||||
const board = ydoc?.getMap(`drawing-board-${id}`)
|
||||
const commands = board?.get('commands') as YArray<DrawingCmd>
|
||||
const props = board?.get('props') as YMap<any>
|
||||
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<DrawingBoardOptions>({
|
||||
|
||||
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<DrawingBoardOptions>({
|
||||
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<DrawingBoardOptions>({
|
||||
},
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
@@ -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
|
||||
</script>
|
||||
|
||||
<button
|
||||
class="app {size}"
|
||||
class="app {size} {kind}"
|
||||
class:loading
|
||||
class:selected
|
||||
class:navigator
|
||||
@@ -104,6 +105,36 @@
|
||||
&.navigator {
|
||||
border-color: var(--theme-button-border);
|
||||
}
|
||||
&.positive {
|
||||
background-color: var(--positive-button-default);
|
||||
&:hover {
|
||||
background-color: var(--positive-button-hovered);
|
||||
}
|
||||
&:active,
|
||||
&.selected {
|
||||
background-color: var(--positive-button-pressed);
|
||||
}
|
||||
&.selected:hover {
|
||||
background-color: var(--positive-button-focused);
|
||||
}
|
||||
}
|
||||
&.negative {
|
||||
background-color: var(--negative-button-default);
|
||||
&:hover {
|
||||
background-color: var(--negative-button-hovered);
|
||||
}
|
||||
&:active,
|
||||
&.selected {
|
||||
background-color: var(--negative-button-pressed);
|
||||
}
|
||||
&.selected:hover {
|
||||
background-color: var(--negative-button-focused);
|
||||
}
|
||||
}
|
||||
&.positive .icon-container,
|
||||
&.negative .icon-container {
|
||||
color: var(--theme-caption-color);
|
||||
}
|
||||
}
|
||||
|
||||
.marker {
|
||||
|
||||
@@ -636,10 +636,10 @@
|
||||
|
||||
$deviceInfo.navigator.float = !(workbenchWidth < 1024)
|
||||
const checkWorkbenchWidth = (): void => {
|
||||
if (workbenchWidth <= 1024 && !$deviceInfo.navigator.float && $deviceInfo.navigator.visible) {
|
||||
if (workbenchWidth <= 720 && !$deviceInfo.navigator.float && $deviceInfo.navigator.visible) {
|
||||
$deviceInfo.navigator.visible = false
|
||||
$deviceInfo.navigator.float = true
|
||||
} else if (workbenchWidth > 1024 && $deviceInfo.navigator.float) {
|
||||
} else if (workbenchWidth > 720 && $deviceInfo.navigator.float) {
|
||||
if (getMetadata(workbench.metadata.NavigationExpandedDefault) === undefined) {
|
||||
$deviceInfo.navigator.float = false
|
||||
$deviceInfo.navigator.visible = true
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { Widget } from '@hcengineering/workbench'
|
||||
import { Label } from '@hcengineering/ui'
|
||||
import { Label, Component } from '@hcengineering/ui'
|
||||
|
||||
import AppItem from '../../AppItem.svelte'
|
||||
|
||||
@@ -24,7 +24,11 @@
|
||||
</script>
|
||||
|
||||
<div class="root">
|
||||
<AppItem label={widget.label} icon={widget.icon} selected={highlighted} size="small" on:click />
|
||||
<Component
|
||||
is={widget.switcherComponent ?? AppItem}
|
||||
props={{ label: widget.label, icon: widget.icon, selected: highlighted, size: 'small' }}
|
||||
on:click
|
||||
/>
|
||||
{#if withLabel}
|
||||
<Label label={widget.label} />
|
||||
{/if}
|
||||
|
||||
@@ -38,6 +38,7 @@ export { default as SpecialElement } from './components/navigator/SpecialElement
|
||||
export { default as SpaceView } from './components/SpaceView.svelte'
|
||||
export { default as TreeSeparator } from './components/navigator/TreeSeparator.svelte'
|
||||
export { default as WorkbenchTabs } from './components/WorkbenchTabs.svelte'
|
||||
export { default as AppItem } from './components/AppItem.svelte'
|
||||
export { SpecialView }
|
||||
|
||||
export * from './utils'
|
||||
|
||||
@@ -76,6 +76,7 @@ export interface Widget extends Doc {
|
||||
|
||||
component: AnyComponent
|
||||
tabComponent?: AnyComponent
|
||||
switcherComponent?: AnyComponent
|
||||
headerLabel?: IntlString
|
||||
|
||||
closeIfNoTabs?: boolean
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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<Doc>
|
||||
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
|
||||
}
|
||||
|
||||
@@ -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<string>
|
||||
}
|
||||
|
||||
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<string, SchemaAndFields>()
|
||||
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
|
||||
}
|
||||
|
||||
@@ -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<T>,
|
||||
operations: DocumentUpdate<T>,
|
||||
domainFields: Set<string>
|
||||
schemaFields: SchemaAndFields
|
||||
): Promise<void> {
|
||||
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<void> {
|
||||
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<TxResult> {
|
||||
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<Doc, Doc>, domainFields: Set<string>): Promise<TxResult> {
|
||||
private async txMixin (ctx: MeasureContext, tx: TxMixin<Doc, Doc>, schemaFields: SchemaAndFields): Promise<TxResult> {
|
||||
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<Doc>, domainFields)
|
||||
const converted = convertDoc(domain, doc, this.workspaceId.name, schemaFields)
|
||||
const { extractedFields } = parseUpdate(tx.attributes as Partial<Doc>, schemaFields)
|
||||
const columns = new Set<string>()
|
||||
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<Doc>[],
|
||||
domainFields: Set<string>
|
||||
schemaFields: SchemaAndFields
|
||||
): Promise<TxResult[]> {
|
||||
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<T>[],
|
||||
domainFields: Set<string>
|
||||
schemaFields: SchemaAndFields
|
||||
): Promise<TxResult[]> {
|
||||
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)
|
||||
|
||||
@@ -36,7 +36,9 @@ import {
|
||||
getDocFieldsByDomains,
|
||||
getIndex,
|
||||
getSchema,
|
||||
getSchemaAndFields,
|
||||
type Schema,
|
||||
type SchemaAndFields,
|
||||
translateDomain
|
||||
} from './schemas'
|
||||
|
||||
@@ -264,7 +266,7 @@ export function convertDoc<T extends Doc> (
|
||||
domain: string,
|
||||
doc: T,
|
||||
workspaceId: string,
|
||||
domainFields?: Set<string>
|
||||
schemaAndFields?: SchemaAndFields
|
||||
): DBDoc {
|
||||
const extractedFields: Doc & Record<string, any> = {
|
||||
_id: doc._id,
|
||||
@@ -280,19 +282,43 @@ export function convertDoc<T extends Doc> (
|
||||
|
||||
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<T extends Doc> (
|
||||
ops: DocumentUpdate<T> | MixinUpdate<Doc, T>,
|
||||
fields: Set<string>
|
||||
schemaFields: SchemaAndFields
|
||||
): {
|
||||
extractedFields: Partial<T>
|
||||
remainingData: Partial<T>
|
||||
@@ -340,14 +366,14 @@ export function parseUpdate<T extends Doc> (
|
||||
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
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -537,7 +537,7 @@ test.describe('Channel tests', () => {
|
||||
})
|
||||
|
||||
await test.step('Open general in sidebar too', async () => {
|
||||
await channelPage.makeActionWithChannelInMenu('general', 'Open in sidebar', true)
|
||||
await channelPage.makeActionWithChannelInMenu('general', 'Open in sidebar')
|
||||
await sidebarPage.checkIfSidebarHasVerticalTab(true, data.channelName)
|
||||
await sidebarPage.checkIfSidebarHasVerticalTab(true, 'general')
|
||||
await sidebarPage.checkIfChatSidebarTabIsOpen(true, 'general')
|
||||
@@ -593,7 +593,7 @@ test.describe('Channel tests', () => {
|
||||
})
|
||||
|
||||
await test.step('User go to another chat and Sidebar with tread disappears', async () => {
|
||||
await channelPage.appsMenuButton().click()
|
||||
await channelPage.openNavigator()
|
||||
await channelPage.clickChannel('random')
|
||||
await sidebarPage.checkIfSidebarIsOpen(false)
|
||||
})
|
||||
|
||||
@@ -302,12 +302,8 @@ export class ChannelPage extends CommonPage {
|
||||
await expect(this.buttonBreadcrumb(channel)).toBeVisible()
|
||||
}
|
||||
|
||||
async makeActionWithChannelInMenu (
|
||||
channelName: string,
|
||||
action: string,
|
||||
openNavigator: boolean = false
|
||||
): Promise<void> {
|
||||
if (openNavigator) await this.appsMenuButton().click()
|
||||
async makeActionWithChannelInMenu (channelName: string, action: string): Promise<void> {
|
||||
await this.openNavigator()
|
||||
await this.channelContainers().filter({ hasText: channelName }).hover()
|
||||
await this.channelContainers().filter({ hasText: channelName }).locator('.hulyNavItem-actions').click()
|
||||
await this.selectFromDropdown(this.page, action)
|
||||
|
||||
@@ -91,7 +91,12 @@ export class CommonPage {
|
||||
|
||||
submitButton = (): Locator => this.page.locator('div.date-popup-container button[type="submit"]')
|
||||
buttonBreadcrumb = (hasText?: string): Locator => this.page.locator('button.hulyBreadcrumb-container', { hasText })
|
||||
appsMenuButton = (): Locator => this.page.locator('[id="app-workbench\\:string\\:ShowMenu"]')
|
||||
appsShowMenuButton = (): Locator => this.page.locator('[id="app-workbench\\:string\\:ShowMenu"]')
|
||||
|
||||
async openNavigator (): Promise<void> {
|
||||
const needOpenNavigator = await this.appsShowMenuButton().isVisible()
|
||||
if (needOpenNavigator) await this.appsShowMenuButton().click()
|
||||
}
|
||||
|
||||
async selectMenuItem (page: Page, name: string, fullWordFilter: boolean = false): Promise<void> {
|
||||
if (name !== 'first') {
|
||||
|
||||
Reference in New Issue
Block a user