Fixes to collaborator and documents (#2394)

Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
Andrey Sobolev 2022-11-23 01:13:26 +07:00 committed by GitHub
parent dcaa0ea9b0
commit afe44eb034
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 243 additions and 124 deletions

2
.vscode/launch.json vendored
View File

@ -153,7 +153,7 @@
"type": "node",
"request": "launch",
"args": [
"src/index.ts"
"src/__start.ts"
],
"env": {
"SECRET": "secret",

View File

@ -295,7 +295,7 @@ specifiers:
mime-types: ~2.1.34
mini-css-extract-plugin: ^2.2.0
minio: ^7.0.26
mongodb: ^4.9.0
mongodb: ^4.11.0
pdfkit: ~0.13.0
postcss: ^8.3.4
postcss-load-config: ^3.1.0
@ -332,6 +332,7 @@ specifiers:
webpack-dev-server: ^4.7.4
ws: ^8.10.0
xml2js: ~0.4.23
y-prosemirror: 1.0.20
y-protocols: ~1.0.5
y-websocket: ^1.4.5
yjs: ^13.5.42
@ -668,6 +669,7 @@ dependencies:
webpack-dev-server: 4.11.1_78c1cd1c404fc7ed0a3af68b1f6f4aa1
ws: 8.11.0
xml2js: 0.4.23
y-prosemirror: 1.0.20_3b523a098b7386dd759c4c2b4e06fb42
y-protocols: 1.0.5
y-websocket: 1.4.5_yjs@13.5.42
yjs: 13.5.42
@ -11137,7 +11139,7 @@ packages:
dev: false
file:projects/account.tgz:
resolution: {integrity: sha512-8yjt4NrdmP1f+lfABJvi/UMWdJyh7o5ozzLsBxAOoNPPJdv2hfWGkQRlaix24WJY69UMXug587gbLi8kvl1JDQ==, tarball: file:projects/account.tgz}
resolution: {integrity: sha512-eATySKTuYgYy7bhtyRcIqpHJwH5XNAhrGqfb7xytdE+Mou2AEV6I7s8LtFWLFGnXUJQEakfNk3sPTLtORivw+w==, tarball: file:projects/account.tgz}
name: '@rush-temp/account'
version: 0.0.0
dependencies:
@ -11818,7 +11820,7 @@ packages:
dev: false
file:projects/dev-account.tgz:
resolution: {integrity: sha512-hwScHxHPdx48SHu+nFqc3XRvNabSYK6jL9xUc8uXxgRjkxI5jwqe6EAPczHdFo0KzPSivFP0WEpjaMkUDzH4WQ==, tarball: file:projects/dev-account.tgz}
resolution: {integrity: sha512-wgzK5UaTPJua2WnS9J1gbpsCPZcR+OZn+1Lotkjc8fojELP8HbT8MHwNHTYEx14nPO8cYUFGgMwSKMLYeIOkRA==, tarball: file:projects/dev-account.tgz}
name: '@rush-temp/dev-account'
version: 0.0.0
dependencies:
@ -12064,7 +12066,7 @@ packages:
dev: false
file:projects/front.tgz:
resolution: {integrity: sha512-leV9tNxFFE/Z/vwbqsNGdp5OE8DaditWbMLnToMBAinA9vFc3qQi4y8zx2MX4Ohn0D/D0Xymha3xE/9mvXeUTw==, tarball: file:projects/front.tgz}
resolution: {integrity: sha512-hjAySYAWM8LKqnfBRXxesUumH6MmZEbazSbEYp4s67um6rGUATr9kUAppfHm2VCuI2WWTmSn52catVFQhRNCqg==, tarball: file:projects/front.tgz}
name: '@rush-temp/front'
version: 0.0.0
dependencies:
@ -12105,7 +12107,7 @@ packages:
dev: false
file:projects/generator.tgz:
resolution: {integrity: sha512-knz6FRYXSNtm1MU/tTKpG1PwQvePi1uaNqNwKmWRURDg/59PESdFMQadueRbIdgJJ15tlJU4ki8yCHmO6l+n0Q==, tarball: file:projects/generator.tgz}
resolution: {integrity: sha512-GBIEqPVyMK0nBCG6rFfwmnXuusGg+6R1xHEUp4Q/6aweSkb1mjz5FeycHN2zGs9j45RYwisAwmZCkFICUOaaNQ==, tarball: file:projects/generator.tgz}
name: '@rush-temp/generator'
version: 0.0.0
dependencies:
@ -12638,7 +12640,7 @@ packages:
dev: false
file:projects/minio.tgz:
resolution: {integrity: sha512-VoxVx9EB3UAH22AGrPedW7GCOMvELT35uTIjYoJM91wAg2AeN2yJBTpgoStYCZVIWT9kmCbgGE6+L2Vi0PT7fA==, tarball: file:projects/minio.tgz}
resolution: {integrity: sha512-Q7+heaBlBIIsy/JvFJpRnEjwq5dmItX2YK/ID98hd6C/Vaix0DUvwc5VuSadDNC/ugonds1KIfkGqm16mPgiCA==, tarball: file:projects/minio.tgz}
name: '@rush-temp/minio'
version: 0.0.0
dependencies:
@ -13633,7 +13635,7 @@ packages:
dev: false
file:projects/mongo.tgz:
resolution: {integrity: sha512-b5/QncRlKkm/JFot8N/JPXLYjKjKTkBIh8g0tS4PWkA/ENdCIDulcpmvi1Q6UM+ZhP6uSGr08JHnVpPmPPj46g==, tarball: file:projects/mongo.tgz}
resolution: {integrity: sha512-Td9qjRbNnbet8a7jMYOTEgaHPL5B6L01ny0wolWKPXtGJMcOD9vkWdC+UJuCJ1OSbMv6vo7hOqXdQXocn5G4Cw==, tarball: file:projects/mongo.tgz}
name: '@rush-temp/mongo'
version: 0.0.0
dependencies:
@ -13831,7 +13833,7 @@ packages:
dev: false
file:projects/pod-account.tgz:
resolution: {integrity: sha512-n507oMGA9w0oyJHDG4Ct65c2q/reFMbeOYkqd8GLrV15ggrNObeMt21vKuGmNzcoa275qIUeUdOY5xbDuR4FQg==, tarball: file:projects/pod-account.tgz}
resolution: {integrity: sha512-57U5JccEZbAk81q+pwaLqNK7VJAF+soBtVsIVRdKgnlgSuI/zjSVEXyKJT7OyXCQPsrJ4fR/TpP8c+JoLaenDg==, tarball: file:projects/pod-account.tgz}
name: '@rush-temp/pod-account'
version: 0.0.0
dependencies:
@ -13867,7 +13869,7 @@ packages:
dev: false
file:projects/pod-backup.tgz:
resolution: {integrity: sha512-lNuroyMRN27WmKY8SOxXDi4crPhzNhS10u4XL6D3LHtip2b3tebu04AdGVloiUOHdKjufcVzGqGQ33ZL/976QA==, tarball: file:projects/pod-backup.tgz}
resolution: {integrity: sha512-Olz5vF5waH9Hbb3l39zF4DvevJeCxykLR41PzrVb2CYmfvrF8i34V7HvJ2dXzxUXm+EClkbj31S02adwyfT7kA==, tarball: file:projects/pod-backup.tgz}
name: '@rush-temp/pod-backup'
version: 0.0.0
dependencies:
@ -13899,7 +13901,7 @@ packages:
dev: false
file:projects/pod-collaborator.tgz:
resolution: {integrity: sha512-WF2SZB/a+C7SK3N3gC0rKcFvdEy6NSFYTUnY1Xv8fjanS/d0VEjpmEjpgyCBvRy7B9fKSMIN90yrkhf6F1p/uA==, tarball: file:projects/pod-collaborator.tgz}
resolution: {integrity: sha512-FM+/tmFMqqJpRV8nfJ3f2Cuz7sLrApZ7I9fpIjB3yPcLwA/UawwR6ew/zBQMEdazCbe6s/NjNBETquptO8f7lg==, tarball: file:projects/pod-collaborator.tgz}
name: '@rush-temp/pod-collaborator'
version: 0.0.0
dependencies:
@ -13935,7 +13937,7 @@ packages:
dev: false
file:projects/pod-front.tgz:
resolution: {integrity: sha512-QDIGhzVOhCUdd122OEVtsi7ELilODbEDWKSr78OTdHelqV8CcsgWIWU8RsYBIqAAIIMRoyu1rXQqDzEgwYfSpw==, tarball: file:projects/pod-front.tgz}
resolution: {integrity: sha512-oxfotaMR88yF2NtkdbtsEvYVCe+GdtuaqvKrEtDMZlk2gcEMCZge1tWTES+HSIxHtWB4pIH1kfzirk+v7eB5Jw==, tarball: file:projects/pod-front.tgz}
name: '@rush-temp/pod-front'
version: 0.0.0
dependencies:
@ -13977,7 +13979,7 @@ packages:
dev: false
file:projects/pod-server.tgz:
resolution: {integrity: sha512-IpXQo5LRXdiCB4ir/T+i+qWMhCaYaHSiGnrE+w7QYf85tS1NmhNS2PWEvU0CTwQYlTiD5e7PNgok6orsU8pexQ==, tarball: file:projects/pod-server.tgz}
resolution: {integrity: sha512-rfkv0eygy8Jmv8uw2KEdIfuP0TrGIgXAIUv6Y/uUn/lvPQR9FBCuDfZ9/sEJW1d7MRaBLeFkmVr0xzivBYZHZA==, tarball: file:projects/pod-server.tgz}
name: '@rush-temp/pod-server'
version: 0.0.0
dependencies:
@ -14295,7 +14297,7 @@ packages:
dev: false
file:projects/server-backup.tgz:
resolution: {integrity: sha512-snkRz4+41T/IJEpLmAnc9lUqo+K1xa0jkpcB5wdg1YSG+XQupGOuJ4Lu6V63+KrHEbWpircaTNL9gFEHQsWl5w==, tarball: file:projects/server-backup.tgz}
resolution: {integrity: sha512-RCwlP7QyaEr5W6rpqf8/cTOR7cH56704M5+w5ZRbX28SrTWoeFssfEmYMEuqLVxQvVcwSySq5wGegV/VHrwSVQ==, tarball: file:projects/server-backup.tgz}
name: '@rush-temp/server-backup'
version: 0.0.0
dependencies:
@ -14402,7 +14404,7 @@ packages:
dev: false
file:projects/server-contact-resources.tgz:
resolution: {integrity: sha512-RmqLUSQSZYSc1UFyU0RmZ7b7IBMT0rrbF3B1KpGcnFbu+sduTPW5byef5DnRLIDX2YlNvejYsfXssBCNk0LU/w==, tarball: file:projects/server-contact-resources.tgz}
resolution: {integrity: sha512-kqkkjCxfo3L3lB6yzXWlGedWYGyi31TQcKR0ZIqYxZxakeB7zUTxSfayEGIxxRvIeJ8K2fXtnkwjk5AbaRKi7Q==, tarball: file:projects/server-contact-resources.tgz}
name: '@rush-temp/server-contact-resources'
version: 0.0.0
dependencies:
@ -14443,7 +14445,7 @@ packages:
dev: false
file:projects/server-core.tgz:
resolution: {integrity: sha512-CihDLjYbiz3luqKzAoF181km4CZxkxqVzQoxaJ24YPgvv4Fw0ztT3o1fw3f/68mNya4DbE9lrKvETKL3jeLpmw==, tarball: file:projects/server-core.tgz}
resolution: {integrity: sha512-dBZNtVbslOApBtMTuf+X50jpjLsCoLmPje51lAr6UgKIaeL9kjhaVWz9AjNA6bCQgBBM+MYe6fbRIPaXCCfmEA==, tarball: file:projects/server-core.tgz}
name: '@rush-temp/server-core'
version: 0.0.0
dependencies:
@ -14897,7 +14899,7 @@ packages:
dev: false
file:projects/server-token.tgz:
resolution: {integrity: sha512-2obpSEchu1xIse/mgmk2GLor2XtUmF+1CULwme1I1T1ZU50nPcpu8AZrOO7cGK9d34CTJonLDk+wscGA4pt4hQ==, tarball: file:projects/server-token.tgz}
resolution: {integrity: sha512-8KGPm5Wes48jUlz/RyGE3rIsQ8nd8S4otlVnZvUB8x1hSRtMngc1DNQVmgCy3iUwNYuyN9w7r0zZN7c3F+Vn5Q==, tarball: file:projects/server-token.tgz}
name: '@rush-temp/server-token'
version: 0.0.0
dependencies:
@ -14921,7 +14923,7 @@ packages:
dev: false
file:projects/server-tool.tgz:
resolution: {integrity: sha512-VxPpII/+/u5gj40wFyU/wiV9EevgwadwNF3FFZGsnG4SJmWf34aSvvH4Cz8WilEAzeA4LXlUS20stY+9hFFycg==, tarball: file:projects/server-tool.tgz}
resolution: {integrity: sha512-Xm9jJEzgWPgN4FefhPDKMv6MYMW7vAfW74FQ1E3xyli3zNW6UiZje+y46ooDu1MREOFyfTYyIAJkhAx7r1lBdw==, tarball: file:projects/server-tool.tgz}
name: '@rush-temp/server-tool'
version: 0.0.0
dependencies:
@ -15015,7 +15017,7 @@ packages:
dev: false
file:projects/server.tgz:
resolution: {integrity: sha512-1BkV1SV80DpRxzAKpNIcL0yHhybay4GRjjaMU8QGD4PipOn6rO3u0x/iELRA8hHnPJzFDZQcqI2/+hm5h5buGQ==, tarball: file:projects/server.tgz}
resolution: {integrity: sha512-L3P1x5JppF1RTeUhn+oM9/8Udx33+C5rKVu3he3DWjbXd5XTvfY8XsiSkZwoBTsYlXsevTZnd5aYA8ZjqPmGRQ==, tarball: file:projects/server.tgz}
name: '@rush-temp/server'
version: 0.0.0
dependencies:
@ -15451,7 +15453,7 @@ packages:
dev: false
file:projects/text-editor.tgz_89204ec304a9fe9c91bbfc5394a172bd:
resolution: {integrity: sha512-jGIekdEoT82BTFc4UVhbLOuQZJkVp4NSb7P3DOw5JJwJ0KvuU0mQZcA7AGKTI7eAuPBG6xjtFR2f4ykt+QcjmA==, tarball: file:projects/text-editor.tgz}
resolution: {integrity: sha512-y/a6ITp7adtktGttSf9Fqvxz60+EN3SPpWK/KLeezMmTVdeejIBiNYSsMnJGjWhuJ8PV3WDQVLsbXSRZBwAwLA==, tarball: file:projects/text-editor.tgz}
id: file:projects/text-editor.tgz
name: '@rush-temp/text-editor'
version: 0.0.0
@ -15501,6 +15503,7 @@ packages:
svelte-loader: 3.1.4_svelte@3.53.1
svelte-preprocess: 4.10.7_1cd24d71cb02643c0a6ca17ff2edd158
typescript: 4.8.4
y-prosemirror: 1.0.20_3b523a098b7386dd759c4c2b4e06fb42
y-websocket: 1.4.5_yjs@13.5.42
yjs: 13.5.42
transitivePeerDependencies:
@ -15553,7 +15556,7 @@ packages:
dev: false
file:projects/tool.tgz:
resolution: {integrity: sha512-lPQdPlcd6ajld4cEO1nodKIc4avevkjqB1qAYMfrLfN9X+F7WWv+bf3r0OLkDpXurUiufMgCo/KAMhypIeLdJw==, tarball: file:projects/tool.tgz}
resolution: {integrity: sha512-zhdyLwe6Bv8T3Pnby/tdJxQZljdHmJqT6w5qBnF+TyoOEPknKxbheB/I/u6IOSq3IJn5ti12Nz02IBReeEsnWQ==, tarball: file:projects/tool.tgz}
name: '@rush-temp/tool'
version: 0.0.0
dependencies:
@ -15621,7 +15624,7 @@ packages:
dev: false
file:projects/tracker-resources.tgz_49b4785992daa3b61a639b2b31601e76:
resolution: {integrity: sha512-mAMoLXcGbvCBFiy0w3I2qDd//xQHlzzNsjkDY9h/xREiAzkdzgusZNu6J/tlYEK0gJGKKXki46a1sH9dBNgIzQ==, tarball: file:projects/tracker-resources.tgz}
resolution: {integrity: sha512-ZY4HQHk3DoAHrswKl30DwbWhgun3qmqwN4L4fiG/iu7P0hnKNQpFO9XGgyZrarteHamCj2LayFuhQM/eTU5IQw==, tarball: file:projects/tracker-resources.tgz}
id: file:projects/tracker-resources.tgz
name: '@rush-temp/tracker-resources'
version: 0.0.0

View File

@ -35,6 +35,7 @@
export let isAside: boolean = true
export let isCustomAttr: boolean = true
export let floatAside = false
export let allowClose = true
</script>
<Panel
@ -44,6 +45,7 @@
bind:innerWidth
bind:withoutTitle
on:close
{allowClose}
{floatAside}
>
<svelte:fragment slot="navigator">

View File

@ -55,6 +55,7 @@
"prosemirror-transform": "~1.7.0",
"yjs": "^13.5.42",
"y-websocket": "^1.4.5",
"y-prosemirror": "1.0.20",
"prosemirror-changeset": "~2.2.0",
"prosemirror-model": "~1.18.1",
"prosemirror-view": "~1.29.0",

View File

@ -0,0 +1,61 @@
<!--
//
// Copyright © 2022 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 { onDestroy, setContext } from 'svelte'
import { WebsocketProvider } from 'y-websocket'
import * as Y from 'yjs'
import { CollaborationIds } from '../types'
export let documentId: string
export let token: string
export let collaboratorURL: string
export let initialContentId: string | undefined = undefined
let _documentId = ''
let wsProvider: WebsocketProvider | undefined
$: if (_documentId !== documentId) {
_documentId = documentId
if (wsProvider !== undefined) {
wsProvider.disconnect()
}
const ydoc: Y.Doc = new Y.Doc()
wsProvider = new WebsocketProvider(collaboratorURL, documentId, ydoc, {
params: {
token,
documentId,
initialContentId: initialContentId ?? ''
}
})
setContext(CollaborationIds.Doc, ydoc)
setContext(CollaborationIds.Provider, wsProvider)
wsProvider.on('status', (event: any) => {
console.log(documentId, event.status) // logs "connected" or "disconnected"
})
}
onDestroy(() => {
setTimeout(() => {
wsProvider?.disconnect()
}, 100)
})
</script>
{#key _documentId}
<slot />
{/key}

View File

@ -124,7 +124,8 @@
.ProseMirror {
flex-grow: 1;
overflow: auto;
max-height: 60vh;
min-height: 3rem;
max-height: inherit !important;
outline: none;
line-height: 150%;
color: var(--accent-color);

View File

@ -37,7 +37,7 @@
import { DecorationSet } from 'prosemirror-view'
import textEditorPlugin from '../plugin'
import { FormatMode, FORMAT_MODES } from '../types'
import { CollaborationIds, FormatMode, FORMAT_MODES } from '../types'
import Bold from './icons/Bold.svelte'
import Code from './icons/Code.svelte'
import CodeBlock from './icons/CodeBlock.svelte'
@ -61,22 +61,29 @@
import DeleteRow from './icons/table/DeleteRow.svelte'
import DeleteCol from './icons/table/DeleteCol.svelte'
import DeleteTable from './icons/table/DeleteTable.svelte'
import { getContext } from 'svelte'
export let documentId: string
export let readonly = false
export let token: string
export let collaboratorURL: string
export let isFormatting = true
export let buttonSize: IconSize = 'small'
export let focusable: boolean = false
export let placeholder: IntlString = textEditorPlugin.string.EditorPlaceholder
export let initialContentId: string | undefined = undefined
export let suggestMode = false
export let comparedVersion: Markup | undefined = undefined
// export let suggestMode = false
export let comparedVersion: Markup | ArrayBuffer | undefined = undefined
const ydoc = new Y.Doc()
const wsProvider = new WebsocketProvider(collaboratorURL, documentId, ydoc, {
export let field: string | undefined = undefined
const ydoc = (getContext(CollaborationIds.Doc) as Y.Doc | undefined) ?? new Y.Doc()
const contextProvider = getContext(CollaborationIds.Provider) as WebsocketProvider | undefined
const wsProvider =
contextProvider ??
new WebsocketProvider(collaboratorURL, documentId, ydoc, {
params: {
token,
documentId,
@ -84,9 +91,11 @@
}
})
wsProvider.on('status', (event: any) => {
if (contextProvider === undefined) {
wsProvider?.on('status', (event: any) => {
console.log(documentId, event.status) // logs "connected" or "disconnected"
})
}
const currentUser = getCurrentAccount() as EmployeeAccount
@ -173,8 +182,8 @@
let _decoration = DecorationSet.empty
let oldContent = ''
function updateEditor (editor?: Editor, comparedVersion?: Markup): void {
const r = calculateDecorations(editor, oldContent, comparedVersion)
function updateEditor (editor?: Editor, field?: string, comparedVersion?: Markup | ArrayBuffer): void {
const r = calculateDecorations(editor, oldContent, field, comparedVersion)
if (r !== undefined) {
oldContent = r.oldContent
_decoration = r.decorations
@ -183,7 +192,7 @@
const updateDecorations = () => {
if (editor && editor.schema) {
updateEditor(editor, comparedVersion)
updateEditor(editor, field, comparedVersion)
}
}
@ -206,7 +215,7 @@
}
})
$: updateEditor(editor, comparedVersion)
$: updateEditor(editor, field, comparedVersion)
onMount(() => {
ph.then(() => {
@ -219,7 +228,8 @@
Placeholder.configure({ placeholder: placeHolderStr }),
Collaboration.configure({
document: ydoc
document: ydoc,
field
}),
CollaborationCursor.configure({
provider: wsProvider,
@ -256,9 +266,13 @@
onDestroy(() => {
if (editor) {
try {
editor.destroy()
} catch (err: any) {}
if (contextProvider === undefined) {
wsProvider.disconnect()
}
}
})
let activeModes = new Set<FormatMode>()
@ -573,8 +587,8 @@
<style lang="scss" global>
.ProseMirror {
flex-grow: 1;
overflow: auto;
max-height: 60vh;
min-height: 3rem;
max-height: inherit !important;
outline: none;
line-height: 150%;
color: var(--accent-color);

View File

@ -29,6 +29,7 @@
export let previewLimit: number = 240
export let previewUnlimit: boolean = false
export let focusable: boolean = false
export let enableFormatting = false
const Mode = {
View: 1,
@ -104,6 +105,7 @@
{buttonSize}
{maxHeight}
{focusable}
{enableFormatting}
bind:content={rawValue}
bind:this={textEditor}
on:attach
@ -119,6 +121,9 @@
}}
on:value={(evt) => {
rawValue = evt.detail
if (alwaysEdit) {
content = evt.detail
}
dispatch('changeContent')
}}
>

View File

@ -66,6 +66,7 @@
export let focusable: boolean = false
export let maxHeight: 'max' | 'card' | 'limited' | string | undefined = undefined
export let withoutTopBorder = false
export let enableFormatting = false
let textEditor: TextEditor
@ -85,7 +86,7 @@
? 'max-content'
: maxHeight
let isFormatting = false
let isFormatting = enableFormatting
let activeModes = new Set<FormatMode>()
let isSelectionEmpty = true

View File

@ -216,7 +216,8 @@
<style lang="scss" global>
.ProseMirror {
overflow-y: auto;
max-height: 40vh;
min-height: 3rem;
max-height: inherit !important;
outline: none;
line-height: 150%;
color: var(--accent-color);

View File

@ -19,16 +19,32 @@ import { ChangeSet } from 'prosemirror-changeset'
import { DOMParser, Node, Schema } from 'prosemirror-model'
import { Decoration, DecorationSet } from 'prosemirror-view'
import { recreateTransform } from './recreate'
import { yDocToProsemirrorJSON } from 'y-prosemirror'
import { Doc, applyUpdate } from 'yjs'
/**
* @public
*/
export function createDocument (schema: Schema, content: Markup): Node {
export function createDocument (schema: Schema, content: Markup | ArrayBuffer, field?: string): Node {
if (typeof content === 'string') {
const wrappedValue = `<body>${content}</body>`
const body = new window.DOMParser().parseFromString(wrappedValue, 'text/html').body
return DOMParser.fromSchema(schema).parse(body)
} else {
try {
const ydoc = new Doc()
const uint8arr = new Uint8Array(content)
applyUpdate(ydoc, uint8arr)
const body = yDocToProsemirrorJSON(ydoc, field)
return schema.nodeFromJSON(body)
} catch (err: any) {
console.error(err)
return schema.node(schema.topNodeType)
}
}
}
/**
@ -37,7 +53,8 @@ export function createDocument (schema: Schema, content: Markup): Node {
export function calculateDecorations (
editor?: Editor,
oldContent?: string,
comparedVersion?: Markup
field?: string,
comparedVersion?: Markup | ArrayBuffer
):
| {
decorations: DecorationSet
@ -52,7 +69,7 @@ export function calculateDecorations (
return
}
const schema = editor.schema
const docOld = createDocument(schema, comparedVersion)
const docOld = createDocument(schema, comparedVersion, field)
const docNew = editor.state.doc
const c = editor.getHTML()
@ -93,6 +110,7 @@ export function calculateDecorations (
if (decorations.length > 0) {
return { decorations: DecorationSet.empty.add(docNew, decorations), oldContent: c }
}
return { decorations: DecorationSet.empty, oldContent: c }
} catch (error: any) {
console.error(error)
}

View File

@ -29,6 +29,7 @@ export { default as CollaboratorEditor } from './components/CollaboratorEditor.s
export { default as CollaborationDiffViewer } from './components/CollaborationDiffViewer.svelte'
export { default } from './plugin'
export * from './types'
export { default as Collaboration } from './components/Collaboration.svelte'
addStringsLoader(textEditorId, async (lang: string) => {
return await import(`../lang/${lang}.json`)

View File

@ -25,6 +25,9 @@ export interface RefInputActionItem extends Doc {
order?: number
}
/**
* @public
*/
export const FORMAT_MODES = [
'bold',
'italic',
@ -39,4 +42,15 @@ export const FORMAT_MODES = [
'table'
] as const
/**
* @public
*/
export type FormatMode = typeof FORMAT_MODES[number]
/**
* @public
*/
export const CollaborationIds = {
Doc: 'text-editor.collaborator.document',
Provider: 'text-editor.collaborator.provider'
}

View File

@ -525,6 +525,7 @@ input.search {
.min-h-0 { min-height: 0; }
.min-h-2 { min-height: .5rem; }
.min-h-7 { min-height: 1.75rem; }
.min-h-30 { min-height: 7.5rem; }
.min-h-60 { min-height: 15rem; }
.max-w-9 { max-width: 2.25rem; }
.max-w-30 { max-width: 7.5rem; }

View File

@ -139,7 +139,7 @@
&-content {
display: flex;
flex-direction: column;
flex-shrink: 0;
// flex-shrink: 0;
margin-left: auto;
margin-right: auto;
width: calc(100% - 7.5rem);
@ -171,6 +171,7 @@
align-items: center;
width: 100%;
min-width: 0;
flex-wrap: wrap;
&.between { justify-content: space-between; }
}

View File

@ -24,7 +24,7 @@
export let direction: TooltipAlignment | undefined = undefined
export let icon: Asset | AnySvelteComponent
export let iconProps: any | undefined = undefined
export let size: 'small' | 'medium' | 'large'
export let size: 'x-small' | 'small' | 'medium' | 'large'
export let action: (ev: MouseEvent) => Promise<void> | void = async () => {}
export let invisible: boolean = false
</script>

View File

@ -30,6 +30,7 @@
export let isFullSize: boolean = false
export let withoutTitle: boolean = false
export let floatAside = false
export let allowClose = true
const dispatch = createEventDispatcher()
@ -61,6 +62,7 @@
>
<div class="popupPanel-title__bordered {twoRows && !withoutTitle ? 'flex-col flex-no-shrink' : 'flex-row-center'}">
<div class="popupPanel-title {twoRows && !withoutTitle ? 'row-top' : 'row'}">
{#if allowClose}
<Button
icon={IconClose}
kind={'transparent'}
@ -69,6 +71,7 @@
dispatch('close')
}}
/>
{/if}
{#if $$slots.navigator}<slot name="navigator" />{/if}
<div class="popupPanel-title__content">
{#if !twoRows && !withoutTitle}<slot name="title" />{/if}

View File

@ -14,19 +14,20 @@
// limitations under the License.
-->
<script lang="ts">
import { Class, Doc, Ref, Space } from '@hcengineering/core'
import { Label, Spinner, Icon } from '@hcengineering/ui'
import { Class, Doc, DocumentQuery, Ref, Space } from '@hcengineering/core'
import { Icon, Label, Spinner } from '@hcengineering/ui'
import view from '@hcengineering/view'
import { Table } from '@hcengineering/view-resources'
import attachment from '../plugin'
import AddAttachment from './AddAttachment.svelte'
import IconAttachment from './icons/Attachment.svelte'
import AttachmentDroppable from './AttachmentDroppable.svelte'
import IconAttachment from './icons/Attachment.svelte'
import UploadDuo from './icons/UploadDuo.svelte'
export let objectId: Ref<Doc>
export let space: Ref<Space>
export let _class: Ref<Class<Doc>>
export let query: DocumentQuery<Doc> = {}
export let attachments: number | undefined = undefined
@ -59,6 +60,7 @@
<div class="text-sm dark-color" style:pointer-events="none">
<Label label={attachment.string.NoAttachments} />
</div>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div
class="over-underline text-sm content-accent-color"
style:pointer-events={dragover ? 'none' : 'all'}
@ -83,8 +85,11 @@
'lastModified'
]}
options={{ sort: { pinned: -1 } }}
query={{ attachedTo: objectId }}
query={{ ...query, attachedTo: objectId }}
loadingProps={{ length: attachments ?? 0 }}
on:content={(evt) => {
attachments = evt.detail.length
}}
/>
{/if}
</div>

View File

@ -27,7 +27,6 @@
export let object: DocumentVersion
export let readonly = false
export let initialContentId: string | undefined = undefined
export let suggestMode = false
export let comparedVersion: Markup | undefined = undefined
const token = getMetadata(login.metadata.LoginToken) ?? ''
@ -42,7 +41,6 @@
<CollaboratorEditor
documentId={object.contentAttachmentId}
{token}
{suggestMode}
{collaboratorURL}
{readonly}
on:content

View File

@ -144,7 +144,7 @@
}
onMount(() => {
dispatch('open', { ignoreKeys: ['comments', 'name'] })
dispatch('open', { ignoreKeys: ['comments', 'name', 'reviewers'] })
})
const versionQuery = createQuery()
@ -230,34 +230,6 @@
let autoSelect = true
type ModelType = 'view' | 'edit' // | 'suggest'
let mode: ModelType = 'view'
const modeLabels = {
view: document.string.ViewMode,
edit: document.string.EditMode
// suggest: document.string.SuggestMode
}
function selectMode (event: MouseEvent): void {
showPopup(
SelectPopup,
{
value: Object.entries(modeLabels).map(([mode, label]) => ({
id: mode,
label
})),
placeholder: document.string.Version,
searchable: false
},
eventToHTMLElement(event),
(res) => {
if (res != null) {
mode = res
}
}
)
}
async function doEdit (documentObject: Document): Promise<void> {
processing = true
// Looking for a draft version
@ -319,7 +291,6 @@
autoSelect = true
}
}
mode = 'edit'
processing = false
}
@ -358,8 +329,6 @@
})
}
mode = 'view'
processing = false
}
let editor: DocumentEditor
@ -479,7 +448,7 @@
size={'medium'}
disabled={documentObject?.approvers?.length === 0}
/>
<Button
<!-- <Button
loading={processing}
kind={'link-bordered'}
label={document.string.SendForReview}
@ -487,7 +456,7 @@
icon={IconShare}
size={'medium'}
disabled={documentObject?.reviewers?.length === 0}
/>
/> -->
{/if}
{#if version?.state === DocumentVersionState.Draft && approveRequest}
<Button
@ -517,13 +486,6 @@
/>
{/if}
{/if}
{#if !readonly && version?.state === DocumentVersionState.Draft && approveRequest === undefined}
<Button loading={processing} kind={'link-bordered'} on:click={selectMode} icon={IconEdit} size={'medium'}>
<svelte:fragment slot="content">
<Label label={modeLabels[mode]} />
</svelte:fragment>
</Button>
{/if}
<Button icon={IconMoreH} kind={'transparent'} size={'medium'} on:click={showMenu} />
</svelte:fragment>
@ -535,7 +497,7 @@
object={version}
initialContentId={version.initialContentId}
comparedVersion={compareTo?.content ?? versions[versions.length - 2]?.content}
readonly={mode === 'view'}
readonly={false}
bind:this={editor}
/>
{/key}
@ -547,7 +509,12 @@
</div>
<div class="p-1 mt-6">
<Attachments objectId={documentObject._id} space={documentObject.space} _class={documentObject._class} />
<Attachments
objectId={documentObject._id}
space={documentObject.space}
_class={documentObject._class}
attachments={documentObject.attachments ?? 0}
/>
</div>
<svelte:fragment slot="custom-attributes">
@ -555,7 +522,7 @@
object={documentObject}
_class={documentObject._class}
to={core.class.Doc}
ignoreKeys={['name']}
ignoreKeys={['name', 'reviewers']}
{readonly}
/>
@ -586,7 +553,7 @@
.description-preview {
color: var(--theme-content-color);
line-height: 150%;
overflow: auto;
// overflow: auto;
}
.tab-content {

View File

@ -333,7 +333,7 @@
{/if}
{#each fieldEditors as collection}
{#if collection.editor}
<div class="mt-6 clear-mins">
<div class="mt-6">
<Component
is={collection.editor}
props={{

View File

@ -61,6 +61,7 @@
let loading = 0
let objects: Doc[] = []
let objectsRecieved = false
const refs: HTMLElement[] = []
$: refs.length = objects.length
@ -91,6 +92,7 @@
query,
(result) => {
objects = result
objectsRecieved = true
if (sortingFunction !== undefined) {
const sf = sortingFunction
objects.sort((a, b) => -1 * sortOrder * sf(a, b))
@ -230,7 +232,7 @@
</tr>
</thead>
{/if}
{#if objects.length}
{#if objects.length || objectsRecieved}
<tbody>
{#each objects as object, row (object._id)}
<tr

View File

@ -42,14 +42,15 @@ export function start (ctx: MeasureContext, port: number, minio: MinioService, h
)
const server = createServer()
server.on('upgrade', (request: IncomingMessage, socket: any, head: Buffer) => {
try {
const parsedUrl = new URL('http://host' + (request.url ?? ''))
const token = parsedUrl.searchParams.get('token')
const payload = decodeToken(token ?? '')
console.log('client connected with payload', payload)
const documentId = parsedUrl.searchParams.get('documentId') as string
console.log('client connected with payload', payload, documentId)
const initialContentId = parsedUrl.searchParams.get('initialContentId') as string
wss.handleUpgrade(request, socket, head, (ws) =>
wss.emit('connection', ws, request, payload, documentId, initialContentId)

View File

@ -52,9 +52,20 @@ persistence = {
let minioDocument: Buffer | undefined
try {
minioDocument = Buffer.concat(await minio.read(token.workspace, documentId))
console.log('bind for document', documentId, token.email)
} catch (err: any) {
if (initialContentId !== undefined && initialContentId.length > 0) {
// Try first take existing document.
const existingDoc = getYDoc(initialContentId, token, true, minio, initialContentId, false)
if (existingDoc !== undefined) {
const newUpdates = encodeStateAsUpdate(existingDoc)
minioDocument = Buffer.from(newUpdates.buffer)
console.log('bind for existing document', documentId, token.email)
} else {
minioDocument = Buffer.concat(await minio.read(token.workspace, initialContentId))
console.log('bind for initial document', documentId, token.email, initialContentId)
}
}
}
@ -76,6 +87,8 @@ persistence = {
const buffer = Buffer.from(newUpdates.buffer)
await ydoc?.minio?.put(token.workspace, documentId, buffer)
console.log('state written for', documentId, token.email)
} catch (err: any) {
console.error(err)
}
@ -180,10 +193,11 @@ export function getYDoc (
token: Token,
gc = true,
minio: MinioService,
initialContentId: string
): WSSharedDoc {
initialContentId: string,
allowBind = true
): WSSharedDoc | undefined {
let doc = docs.get(docId)
if (doc === undefined) {
if (doc === undefined && allowBind) {
doc = new WSSharedDoc(docId)
doc.gc = gc
docs.set(docId, doc)
@ -241,10 +255,10 @@ const closeConn = (doc: WSSharedDoc, conn: any): void => {
// if persisted, we store state and destroy ydocument
if (controlledIds !== undefined) {
void persistence.writeState(doc.name, doc, controlledIds?.token).then(() => {
docs.delete(doc.name)
doc.destroy()
})
}
docs.delete(doc.name)
}
}
conn.close()
@ -285,6 +299,10 @@ export function setupWSConnection (
conn.binaryType = 'arraybuffer'
// get doc, initialize if it does not exist yet
const doc = getYDoc(documentId, token, gc, minio, initialContentId)
if (doc === undefined) {
conn.close()
return
}
doc.conns.set(conn, { ids: new Set(), token })
// listen and reply to events
conn.on('message', (message: ArrayBuffer) => messageListener(conn, doc, new Uint8Array(message)))

View File

@ -273,12 +273,13 @@ export function start (
const payload = decodeToken(token ?? '')
console.log('client connected with payload', payload)
if (productId !== '' && payload.workspace.productId !== productId) {
if (payload.workspace.productId !== productId) {
throw new Error('Invalid workspace product')
}
wss.handleUpgrade(request, socket, head, (ws) => wss.emit('connection', ws, request, payload))
} catch (err) {
console.error('invalid token', err)
wss.handleUpgrade(request, socket, head, (ws) => {
const resp: Response<any> = {
id: -1,