mirror of
https://github.com/hcengineering/platform.git
synced 2025-05-22 07:26:19 +00:00
Markup editor updates and few more little fixes (#2502)
Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
parent
8fa7c938da
commit
0bcd74fa25
2
.vscode/launch.json
vendored
2
.vscode/launch.json
vendored
@ -21,7 +21,7 @@
|
||||
"MINIO_SECRET_KEY": "minioadmin",
|
||||
"SERVER_SECRET": "secret",
|
||||
"REKONI_URL": "http://localhost:4004",
|
||||
"OPENAI_TOKEN": "",
|
||||
// "OPENAI_TOKEN": "",
|
||||
// "RETRANSLATE_URL": "http://127.0.0.1:4500",
|
||||
//"RETRANSLATE_URL": "https://208.167.249.201",
|
||||
// "RETRANSLATE_TOKEN": ""
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -317,13 +317,15 @@ export function createModel (builder: Builder): void {
|
||||
classPresenter(
|
||||
builder,
|
||||
core.class.TypeMarkup,
|
||||
view.component.HTMLPresenter,
|
||||
view.component.StringEditor,
|
||||
view.component.StringEditorPopup
|
||||
view.component.MarkupPresenter,
|
||||
view.component.MarkupEditor,
|
||||
view.component.MarkupEditorPopup
|
||||
)
|
||||
|
||||
builder.mixin(core.class.TypeMarkup, core.class.Class, view.mixin.InlineAttributEditor, {
|
||||
editor: view.component.HTMLEditor
|
||||
})
|
||||
|
||||
classPresenter(builder, core.class.TypeBoolean, view.component.BooleanPresenter, view.component.BooleanEditor)
|
||||
classPresenter(builder, core.class.TypeTimestamp, view.component.TimestampPresenter)
|
||||
classPresenter(builder, core.class.TypeDate, view.component.DatePresenter, view.component.DateEditor)
|
||||
|
@ -47,7 +47,7 @@ export default mergeIds(viewId, view, {
|
||||
IntlStringPresenter: '' as AnyComponent,
|
||||
NumberEditor: '' as AnyComponent,
|
||||
NumberPresenter: '' as AnyComponent,
|
||||
HTMLPresenter: '' as AnyComponent,
|
||||
MarkupPresenter: '' as AnyComponent,
|
||||
BooleanPresenter: '' as AnyComponent,
|
||||
BooleanEditor: '' as AnyComponent,
|
||||
TimestampPresenter: '' as AnyComponent,
|
||||
@ -59,7 +59,9 @@ export default mergeIds(viewId, view, {
|
||||
GithubPresenter: '' as AnyComponent,
|
||||
ClassPresenter: '' as AnyComponent,
|
||||
EnumEditor: '' as AnyComponent,
|
||||
HTMLEditor: '' as AnyComponent
|
||||
HTMLEditor: '' as AnyComponent,
|
||||
MarkupEditor: '' as AnyComponent,
|
||||
MarkupEditorPopup: '' as AnyComponent
|
||||
},
|
||||
string: {
|
||||
Table: '' as IntlString,
|
||||
|
@ -34,6 +34,7 @@
|
||||
|
||||
<style lang="scss">
|
||||
.attributes-bar-container {
|
||||
flex-shrink: 0;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 2fr;
|
||||
grid-auto-flow: row;
|
||||
|
126
packages/presentation/src/components/DraggableList.svelte
Normal file
126
packages/presentation/src/components/DraggableList.svelte
Normal file
@ -0,0 +1,126 @@
|
||||
<!--
|
||||
// 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 { flip } from 'svelte/animate'
|
||||
|
||||
import { Doc } from '@hcengineering/core'
|
||||
|
||||
import { IconMoreV } from '@hcengineering/ui'
|
||||
import { getClient } from '../utils'
|
||||
|
||||
type DocWithRank = Doc & { rank: string }
|
||||
|
||||
export let objects: Doc[] | DocWithRank[]
|
||||
export let handleMove: ((fromIndex: number, toIndex: number) => void) | undefined = undefined
|
||||
export let calcRank: (doc: DocWithRank, next: DocWithRank) => string
|
||||
export let showContextMenu: ((evt: MouseEvent, doc: Doc) => void) | undefined = undefined
|
||||
|
||||
const client = getClient()
|
||||
|
||||
let draggingIndex: number | null = null
|
||||
let hoveringIndex: number | null = null
|
||||
let dragOverIndex: number = -1
|
||||
|
||||
function resetDrag () {
|
||||
draggingIndex = null
|
||||
hoveringIndex = null
|
||||
dragOverIndex = -1
|
||||
}
|
||||
|
||||
function handleDragStart (ev: DragEvent, index: number) {
|
||||
if (ev.dataTransfer) {
|
||||
ev.dataTransfer.effectAllowed = 'move'
|
||||
ev.dataTransfer.dropEffect = 'move'
|
||||
draggingIndex = index
|
||||
}
|
||||
}
|
||||
|
||||
function checkHasRank (objects: Doc[] | (Doc & { rank: string })[]): objects is (Doc & { rank: string })[] {
|
||||
return 'rank' in objects[0]
|
||||
}
|
||||
|
||||
async function handleDrop (ev: DragEvent, toIndex: number) {
|
||||
if (ev.dataTransfer && draggingIndex !== null && toIndex !== draggingIndex) {
|
||||
ev.dataTransfer.dropEffect = 'move'
|
||||
|
||||
if (handleMove) {
|
||||
handleMove(draggingIndex, toIndex)
|
||||
return
|
||||
}
|
||||
if (!checkHasRank(objects)) {
|
||||
return
|
||||
}
|
||||
const [prev, next] = [
|
||||
objects[draggingIndex < toIndex ? toIndex : toIndex - 1],
|
||||
objects[draggingIndex < toIndex ? toIndex + 1 : toIndex]
|
||||
]
|
||||
const object = objects[draggingIndex]
|
||||
|
||||
await client.update(object, { rank: calcRank(prev, next) })
|
||||
}
|
||||
resetDrag()
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="antiAccordion">
|
||||
{#each objects as object, index (object._id)}
|
||||
<div class="description pb-1" animate:flip={{ duration: 400 }}>
|
||||
<div
|
||||
class:is-dragging={index === draggingIndex}
|
||||
class:is-dragged-over-up={draggingIndex !== null && index < draggingIndex && index === hoveringIndex}
|
||||
class:is-dragged-over-down={draggingIndex !== null && index > draggingIndex && index === hoveringIndex}
|
||||
class:drag-over-highlight={index === dragOverIndex}
|
||||
draggable={true}
|
||||
on:contextmenu|preventDefault={(ev) => showContextMenu?.(ev, object)}
|
||||
on:dragstart={(ev) => handleDragStart(ev, index)}
|
||||
on:dragover|preventDefault={() => {
|
||||
dragOverIndex = index
|
||||
return false
|
||||
}}
|
||||
on:dragenter={() => (hoveringIndex = index)}
|
||||
on:drop|preventDefault={(ev) => handleDrop(ev, index)}
|
||||
on:dragend={resetDrag}
|
||||
>
|
||||
<div class="draggable-container">
|
||||
<div class="caption mb-0 flex flex-grow flex-row-center">
|
||||
<div class="draggable-mark fs-title dark-color whitespace-nowrap mr-2"><IconMoreV size={'small'} /></div>
|
||||
<div class="fs-title dark-color whitespace-nowrap mr-2">
|
||||
{`${index + 1}.`}
|
||||
</div>
|
||||
<slot name="object" {index} />
|
||||
</div>
|
||||
<slot name="object-footer" {index} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.drag-over-highlight {
|
||||
opacity: 0.2;
|
||||
}
|
||||
.description {
|
||||
.draggable-container {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.draggable-mark {
|
||||
opacity: 0.4;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
@ -44,6 +44,7 @@ export { default as IconMembers } from './components/icons/Members.svelte'
|
||||
export { default as IconMembersOutline } from './components/icons/MembersOutline.svelte'
|
||||
export { default as ObjectSearchPopup } from './components/ObjectSearchPopup.svelte'
|
||||
export { default as IndexedDocumentPreview } from './components/IndexedDocumentPreview.svelte'
|
||||
export { default as DraggableList } from './components/DraggableList.svelte'
|
||||
export { connect, versionError } from './connect'
|
||||
export { default } from './plugin'
|
||||
export * from './types'
|
||||
|
@ -37,36 +37,42 @@
|
||||
"@hcengineering/core": "^0.6.20",
|
||||
"@hcengineering/ui": "^0.6.3",
|
||||
"svelte": "^3.47",
|
||||
"@tiptap/core": "~2.0.0-beta.199",
|
||||
"@tiptap/starter-kit": "~2.0.0-beta.199",
|
||||
"@tiptap/extension-highlight": "~2.0.0-beta.199",
|
||||
"@tiptap/extension-placeholder": "~2.0.0-beta.199",
|
||||
"@tiptap/extension-mention": "~2.0.0-beta.199",
|
||||
"@tiptap/extension-typography": "~2.0.0-beta.199",
|
||||
"@tiptap/extension-link": "~2.0.0-beta.199",
|
||||
"@tiptap/suggestion": "~2.0.0-beta.199",
|
||||
"@tiptap/extension-task-list": "~2.0.0-beta.199",
|
||||
"@tiptap/extension-task-item": "~2.0.0-beta.199",
|
||||
"@tiptap/extension-collaboration": "~2.0.0-beta.199",
|
||||
"@tiptap/extension-collaboration-cursor": "~2.0.0-beta.199",
|
||||
"@tiptap/core": "~2.0.0-beta.209",
|
||||
"@tiptap/starter-kit": "~2.0.0-beta.209",
|
||||
"@tiptap/extension-highlight": "~2.0.0-beta.209",
|
||||
"@tiptap/extension-placeholder": "~2.0.0-beta.209",
|
||||
"@tiptap/extension-mention": "~2.0.0-beta.209",
|
||||
"@tiptap/extension-typography": "~2.0.0-beta.209",
|
||||
"@tiptap/extension-link": "~2.0.0-beta.209",
|
||||
"@tiptap/suggestion": "~2.0.0-beta.209",
|
||||
"@tiptap/extension-task-list": "~2.0.0-beta.209",
|
||||
"@tiptap/extension-task-item": "~2.0.0-beta.209",
|
||||
"@tiptap/extension-collaboration": "~2.0.0-beta.209",
|
||||
"@tiptap/extension-collaboration-cursor": "~2.0.0-beta.209",
|
||||
"@tiptap/prosemirror-tables": "^1.1.4",
|
||||
"emoji-regex": "^10.1.0",
|
||||
"prosemirror-collab": "~1.3.0",
|
||||
"prosemirror-state": "~1.4.1",
|
||||
"prosemirror-transform": "~1.7.0",
|
||||
"yjs": "^13.5.42",
|
||||
"prosemirror-gapcursor": "^1.3.1",
|
||||
"prosemirror-dropcursor": "^1.6.1",
|
||||
"prosemirror-collab": "^1.3.0",
|
||||
"prosemirror-state": "^1.4.2",
|
||||
"prosemirror-transform": "^1.7.0",
|
||||
"prosemirror-schema-list": "^1.2.2",
|
||||
"prosemirror-commands": "^1.5.0",
|
||||
"yjs": "^13.5.44",
|
||||
"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",
|
||||
"rfc6902": "~5.0.1",
|
||||
"diff": "~5.1.0",
|
||||
"@tiptap/extension-code-block": "~2.0.0-beta.200",
|
||||
"@tiptap/extension-gapcursor": "~2.0.0-beta.200",
|
||||
"@tiptap/extension-heading": "~2.0.0-beta.200",
|
||||
"@tiptap/extension-table": "~2.0.0-beta.202",
|
||||
"@tiptap/extension-table-cell": "~2.0.0-beta.202",
|
||||
"@tiptap/extension-table-header": "~2.0.0-beta.202",
|
||||
"@tiptap/extension-table-row": "~2.0.0-beta.202"
|
||||
"y-prosemirror": "^1.2.0",
|
||||
"prosemirror-changeset": "^2.2.0",
|
||||
"prosemirror-model": "^1.18.3",
|
||||
"prosemirror-view": "^1.29.1",
|
||||
"prosemirror-history": "^1.3.0",
|
||||
"rfc6902": "^5.0.1",
|
||||
"diff": "^5.1.0",
|
||||
"@tiptap/extension-code-block": "~2.0.0-beta.209",
|
||||
"@tiptap/extension-gapcursor": "~2.0.0-beta.209",
|
||||
"@tiptap/extension-heading": "~2.0.0-beta.209",
|
||||
"@tiptap/extension-table": "~2.0.0-beta.209",
|
||||
"@tiptap/extension-table-cell": "~2.0.0-beta.209",
|
||||
"@tiptap/extension-table-header": "~2.0.0-beta.209",
|
||||
"@tiptap/extension-table-row": "~2.0.0-beta.209"
|
||||
}
|
||||
}
|
||||
|
@ -45,14 +45,15 @@
|
||||
setContext(CollaborationIds.Doc, ydoc)
|
||||
setContext(CollaborationIds.Provider, wsProvider)
|
||||
wsProvider.on('status', (event: any) => {
|
||||
console.log(documentId, event.status) // logs "connected" or "disconnected"
|
||||
console.log('Collaboration:', documentId, event.status) // logs "connected" or "disconnected"
|
||||
})
|
||||
wsProvider.on('synched', (event: any) => {
|
||||
console.log('Collaboration:', event) // logs "connected" or "disconnected"
|
||||
})
|
||||
}
|
||||
|
||||
onDestroy(() => {
|
||||
setTimeout(() => {
|
||||
wsProvider?.disconnect()
|
||||
}, 100)
|
||||
wsProvider?.disconnect()
|
||||
})
|
||||
</script>
|
||||
|
||||
|
@ -30,6 +30,7 @@
|
||||
export let previewUnlimit: boolean = false
|
||||
export let focusable: boolean = false
|
||||
export let enableFormatting = false
|
||||
export let autofocus = false
|
||||
|
||||
const Mode = {
|
||||
View: 1,
|
||||
@ -118,6 +119,7 @@
|
||||
{maxHeight}
|
||||
{focusable}
|
||||
{enableFormatting}
|
||||
{autofocus}
|
||||
bind:content={rawValue}
|
||||
bind:this={textEditor}
|
||||
on:attach
|
||||
|
@ -67,6 +67,7 @@
|
||||
export let maxHeight: 'max' | 'card' | 'limited' | string | undefined = undefined
|
||||
export let withoutTopBorder = false
|
||||
export let enableFormatting = false
|
||||
export let autofocus = false
|
||||
|
||||
let textEditor: TextEditor
|
||||
|
||||
@ -201,7 +202,7 @@
|
||||
a.action(evt?.target as HTMLElement, editorHandler)
|
||||
}
|
||||
|
||||
let needFocus = false
|
||||
let needFocus = autofocus
|
||||
const focused = false
|
||||
|
||||
$: if (textEditor && needFocus) {
|
||||
|
@ -419,6 +419,7 @@ input.search {
|
||||
.mt-9 { margin-top: 2.25rem; }
|
||||
.mt-10 { margin-top: 2.5rem; }
|
||||
.mt-14 { margin-top: 3.5rem; }
|
||||
.mb-0 { margin-bottom: 0 !important; }
|
||||
.mb-1 { margin-bottom: .25rem; }
|
||||
.mb-2 { margin-bottom: .5rem; }
|
||||
.mb-3 { margin-bottom: .75rem; }
|
||||
@ -452,7 +453,7 @@ input.search {
|
||||
.pt-3 { padding-top: .75rem; }
|
||||
.pt-4 { padding-top: 1rem; }
|
||||
.pt-6 { padding-top: 1.5rem; }
|
||||
.pb-1 { padding-bottom: .25rem; }
|
||||
.pb-1 { padding-bottom: .25rem !important; }
|
||||
.pb-2 { padding-bottom: .5rem; }
|
||||
.pb-3 { padding-bottom: .75rem; }
|
||||
.pb-4 { padding-bottom: 1rem; }
|
||||
|
@ -33,6 +33,7 @@
|
||||
export let disabled: boolean = false
|
||||
export let loading: boolean = false
|
||||
export let width: string | undefined = undefined
|
||||
export let height: string | undefined = undefined
|
||||
export let resetIconSize: boolean = false
|
||||
export let highlight: boolean = false
|
||||
export let selected: boolean = false
|
||||
@ -95,6 +96,7 @@
|
||||
class:notSelected
|
||||
disabled={disabled || loading}
|
||||
style:width
|
||||
style:height
|
||||
{title}
|
||||
type={kind === 'primary' ? 'submit' : 'button'}
|
||||
on:click|stopPropagation|preventDefault
|
||||
|
@ -117,6 +117,7 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<div
|
||||
class="editbox-container"
|
||||
class:flex-grow={fullSize}
|
||||
|
@ -16,6 +16,7 @@
|
||||
import type { TabModel } from '../types'
|
||||
import Label from './Label.svelte'
|
||||
import Component from './Component.svelte'
|
||||
import { Icon } from '..'
|
||||
|
||||
export let model: TabModel
|
||||
export let selected = 0
|
||||
@ -23,6 +24,7 @@
|
||||
|
||||
<div class="flex-stretch container">
|
||||
{#each model as tab, i}
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<div
|
||||
class="flex-row-center tab"
|
||||
class:selected={i === selected}
|
||||
@ -30,6 +32,11 @@
|
||||
selected = i
|
||||
}}
|
||||
>
|
||||
{#if tab.icon !== undefined}
|
||||
<div class="mr-2">
|
||||
<Icon icon={tab.icon} size={'small'} />
|
||||
</div>
|
||||
{/if}
|
||||
<Label label={tab.label} />
|
||||
</div>
|
||||
{/each}
|
||||
|
@ -48,6 +48,7 @@
|
||||
"AllFilters": "all filters",
|
||||
"MatchCriteria": "Match criteria",
|
||||
"DontMatchCriteria": "Don't match criteria",
|
||||
"View": "View"
|
||||
"View": "View",
|
||||
"MarkupEditor": "Edit of rich content field"
|
||||
}
|
||||
}
|
||||
|
@ -46,6 +46,7 @@
|
||||
"AllFilters": "всем фильтрам",
|
||||
"MatchCriteria": "Соответсвует условию",
|
||||
"DontMatchCriteria": "Не соответвует условию",
|
||||
"View": "Вид"
|
||||
"View": "Вид",
|
||||
"MarkupEditor": "Изменение форматированного поля"
|
||||
}
|
||||
}
|
||||
|
71
plugins/view-resources/src/components/MarkupEditor.svelte
Normal file
71
plugins/view-resources/src/components/MarkupEditor.svelte
Normal file
@ -0,0 +1,71 @@
|
||||
<!--
|
||||
// Copyright © 2020, 2021 Anticrm Platform Contributors.
|
||||
// Copyright © 2021 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 { IntlString } from '@hcengineering/platform'
|
||||
import { MessageViewer } from '@hcengineering/presentation'
|
||||
import { Button, eventToHTMLElement, Label, showPopup } from '@hcengineering/ui'
|
||||
import MarkupEditorPopup from './MarkupEditorPopup.svelte'
|
||||
|
||||
// export let label: IntlString
|
||||
export let placeholder: IntlString
|
||||
export let value: string
|
||||
export let focus: boolean = false
|
||||
export let onChange: (value: string) => void
|
||||
export let kind: 'no-border' | 'link' = 'no-border'
|
||||
export let readonly = false
|
||||
// export let size: ButtonSize = 'x-large'
|
||||
export let justify: 'left' | 'center' = 'center'
|
||||
export let width: string | undefined = 'fit-content'
|
||||
|
||||
let shown: boolean = false
|
||||
</script>
|
||||
|
||||
{#if kind === 'link'}
|
||||
<Button
|
||||
{kind}
|
||||
size={'x-large'}
|
||||
{justify}
|
||||
{width}
|
||||
height={value ? 'auto' : undefined}
|
||||
on:click={(ev) => {
|
||||
if (!shown && !readonly) {
|
||||
showPopup(MarkupEditorPopup, { value }, eventToHTMLElement(ev), (res) => {
|
||||
if (res != null) {
|
||||
value = res
|
||||
onChange(value)
|
||||
}
|
||||
shown = false
|
||||
})
|
||||
}
|
||||
}}
|
||||
>
|
||||
<svelte:fragment slot="content">
|
||||
<div class="lines-limit-4" style:text-align={'left'} class:dark-color={readonly}>
|
||||
{#if value}
|
||||
<MessageViewer message={value} />
|
||||
{:else}
|
||||
<span class="dark-color"><Label label={placeholder} /></span>
|
||||
{/if}
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
</Button>
|
||||
{:else if readonly}
|
||||
{#if value}
|
||||
<span class="overflow-label">{value}</span>
|
||||
{:else}
|
||||
<span class="dark-color"><Label label={placeholder} /></span>
|
||||
{/if}
|
||||
{/if}
|
@ -0,0 +1,48 @@
|
||||
<!--
|
||||
// Copyright © 2020, 2021 Anticrm Platform Contributors.
|
||||
// Copyright © 2021 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 { Card } from '@hcengineering/presentation'
|
||||
import { StyledTextBox } from '@hcengineering/text-editor'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import view from '../plugin'
|
||||
|
||||
export let value: string
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
export let maxHeight: string = '40vh'
|
||||
const checkValue = (evt: CustomEvent): void => {
|
||||
const res: string | undefined = evt.detail === null ? undefined : evt.detail
|
||||
if (value !== res && res != null) {
|
||||
dispatch('change', res)
|
||||
value = res
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Card
|
||||
label={view.string.MarkupEditor}
|
||||
canSave={true}
|
||||
okLabel={view.string.Save}
|
||||
okAction={() => {
|
||||
dispatch('close', value)
|
||||
}}
|
||||
on:close={() => dispatch('close', null)}
|
||||
on:changeContent
|
||||
>
|
||||
<div class="flex-grow mt-4">
|
||||
<StyledTextBox autofocus content={value} alwaysEdit mode={2} hideExtraButtons {maxHeight} on:value={checkValue} />
|
||||
</div>
|
||||
</Card>
|
@ -54,6 +54,8 @@ import UpDownNavigator from './components/UpDownNavigator.svelte'
|
||||
import ViewletSettingButton from './components/ViewletSettingButton.svelte'
|
||||
import ValueSelector from './components/ValueSelector.svelte'
|
||||
import HTMLEditor from './components/HTMLEditor.svelte'
|
||||
import MarkupEditor from './components/MarkupEditor.svelte'
|
||||
import MarkupEditorPopup from './components/MarkupEditorPopup.svelte'
|
||||
import SortableList from './components/list/SortableList.svelte'
|
||||
import SortableListItem from './components/list/SortableListItem.svelte'
|
||||
import {
|
||||
@ -118,7 +120,8 @@ export {
|
||||
NumberPresenter,
|
||||
TimestampPresenter,
|
||||
SortableList,
|
||||
SortableListItem
|
||||
SortableListItem,
|
||||
MarkupEditor
|
||||
}
|
||||
|
||||
export default async (): Promise<Resources> => ({
|
||||
@ -149,6 +152,8 @@ export default async (): Promise<Resources> => ({
|
||||
YoutubePresenter,
|
||||
ActionsPopup,
|
||||
StringEditorPopup: EditBoxPopup,
|
||||
MarkupEditor,
|
||||
MarkupEditorPopup,
|
||||
BooleanTruePresenter,
|
||||
EnumEditor,
|
||||
FilterTypePopup,
|
||||
|
@ -54,6 +54,7 @@ export default mergeIds(viewId, view, {
|
||||
AnyFilter: '' as IntlString,
|
||||
AllFilters: '' as IntlString,
|
||||
MatchCriteria: '' as IntlString,
|
||||
DontMatchCriteria: '' as IntlString
|
||||
DontMatchCriteria: '' as IntlString,
|
||||
MarkupEditor: '' as IntlString
|
||||
}
|
||||
})
|
||||
|
@ -47,7 +47,7 @@
|
||||
"lodash.debounce": "~4.0.8",
|
||||
"y-protocols": "~1.0.5",
|
||||
"ws": "^8.10.0",
|
||||
"yjs": "^13.5.42",
|
||||
"yjs": "^13.5.44",
|
||||
"@hcengineering/minio": "^0.6.0"
|
||||
}
|
||||
}
|
||||
|
@ -271,7 +271,6 @@ async function doIssueUpdate (
|
||||
)
|
||||
|
||||
const updatedProject = newParent !== undefined ? newParent.project : null
|
||||
const updatedSprint = newParent !== undefined ? newParent.sprint : null
|
||||
const updatedParents =
|
||||
newParent !== undefined ? [{ parentId: newParent._id, parentTitle: newParent.title }, ...newParent.parents] : []
|
||||
|
||||
@ -282,14 +281,13 @@ async function doIssueUpdate (
|
||||
? {}
|
||||
: { parents: [...issue.parents].slice(0, parentInfoIndex + 1).concat(updatedParents) }
|
||||
|
||||
return { ...parentsUpdate, project: updatedProject, sprint: updatedSprint }
|
||||
return { ...parentsUpdate, project: updatedProject }
|
||||
}
|
||||
|
||||
res.push(
|
||||
control.txFactory.createTxUpdateDoc(updateTx.objectClass, updateTx.objectSpace, updateTx.objectId, {
|
||||
parents: updatedParents,
|
||||
project: updatedProject,
|
||||
sprint: updatedSprint
|
||||
project: updatedProject
|
||||
}),
|
||||
...(await updateSubIssues(updateTx, control, update))
|
||||
)
|
||||
|
@ -172,76 +172,113 @@ export async function backup (transactorUrl: string, workspaceId: WorkspaceId, s
|
||||
let addedDocuments = 0
|
||||
|
||||
// update digest tar
|
||||
const needRetrieveChunks: Ref<Doc>[][] = []
|
||||
|
||||
// Load all digest from collection.
|
||||
while (true) {
|
||||
const it = await connection.loadChunk(c, idx)
|
||||
idx = it.idx
|
||||
try {
|
||||
const it = await connection.loadChunk(c, idx)
|
||||
idx = it.idx
|
||||
console.log(needRetrieveChunks.length)
|
||||
|
||||
const needRetrieve: Ref<Doc>[] = []
|
||||
const needRetrieve: Ref<Doc>[] = []
|
||||
|
||||
for (const [k, v] of Object.entries(it.docs)) {
|
||||
const kHash = digest.get(k as Ref<Doc>)
|
||||
if (kHash !== undefined) {
|
||||
digest.delete(k as Ref<Doc>)
|
||||
if (kHash !== v) {
|
||||
changes.updated[k as Ref<Doc>] = v
|
||||
for (const [k, v] of Object.entries(it.docs)) {
|
||||
const kHash = digest.get(k as Ref<Doc>)
|
||||
if (kHash !== undefined) {
|
||||
digest.delete(k as Ref<Doc>)
|
||||
if (kHash !== v) {
|
||||
changes.updated[k as Ref<Doc>] = v
|
||||
needRetrieve.push(k as Ref<Doc>)
|
||||
changed++
|
||||
}
|
||||
} else {
|
||||
changes.added[k as Ref<Doc>] = v
|
||||
needRetrieve.push(k as Ref<Doc>)
|
||||
changed++
|
||||
}
|
||||
}
|
||||
if (needRetrieve.length > 0) {
|
||||
needRetrieveChunks.push(needRetrieve)
|
||||
}
|
||||
if (it.finished) {
|
||||
await connection.closeChunk(idx)
|
||||
break
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.error(err)
|
||||
if (idx !== undefined) {
|
||||
await connection.closeChunk(idx)
|
||||
}
|
||||
// Try again
|
||||
idx = undefined
|
||||
}
|
||||
}
|
||||
while (needRetrieveChunks.length > 0) {
|
||||
const needRetrieve = needRetrieveChunks.shift() as Ref<Doc>[]
|
||||
|
||||
console.log('Retrieve chunk:', needRetrieve.length)
|
||||
let docs: Doc[] = []
|
||||
try {
|
||||
docs = await connection.loadDocs(c, needRetrieve)
|
||||
} catch (err: any) {
|
||||
console.log(err)
|
||||
// Put back.
|
||||
needRetrieveChunks.push(needRetrieve)
|
||||
continue
|
||||
}
|
||||
|
||||
// Chunk data into small pieces
|
||||
if (addedDocuments > dataBlobSize && _pack !== undefined) {
|
||||
_pack.finalize()
|
||||
_pack = undefined
|
||||
addedDocuments = 0
|
||||
|
||||
if (changed > 0) {
|
||||
snapshot.domains[c] = domainInfo
|
||||
domainInfo.added = Object.keys(changes.added).length
|
||||
domainInfo.updated = Object.keys(changes.updated).length
|
||||
domainInfo.removed = changes.removed.length
|
||||
await storage.writeFile(domainInfo.snapshot, gzipSync(JSON.stringify(changes)))
|
||||
// This will allow to retry in case of critical error.
|
||||
await storage.writeFile(infoFile, gzipSync(JSON.stringify(backupInfo, undefined, 2)))
|
||||
}
|
||||
}
|
||||
if (_pack === undefined) {
|
||||
_pack = pack()
|
||||
stIndex++
|
||||
const storageFile = join(backupIndex, `${c}-data-${snapshot.date}-${stIndex}.tar.gz`)
|
||||
console.log('storing from domain', c, storageFile)
|
||||
domainInfo.storage.push(storageFile)
|
||||
const dataStream = await storage.write(storageFile)
|
||||
const storageZip = createGzip()
|
||||
|
||||
_pack.pipe(storageZip)
|
||||
storageZip.pipe(dataStream)
|
||||
}
|
||||
|
||||
for (const d of docs) {
|
||||
if (d._class === core.class.BlobData) {
|
||||
const blob = d as BlobData
|
||||
const data = Buffer.from(blob.base64Data, 'base64')
|
||||
blob.base64Data = ''
|
||||
const descrJson = JSON.stringify(d)
|
||||
addedDocuments += descrJson.length
|
||||
addedDocuments += data.length
|
||||
_pack.entry({ name: d._id + '.json' }, descrJson, function (err) {
|
||||
if (err != null) throw err
|
||||
})
|
||||
_pack.entry({ name: d._id }, data, function (err) {
|
||||
if (err != null) throw err
|
||||
})
|
||||
} else {
|
||||
changes.added[k as Ref<Doc>] = v
|
||||
needRetrieve.push(k as Ref<Doc>)
|
||||
changed++
|
||||
const data = JSON.stringify(d)
|
||||
addedDocuments += data.length
|
||||
_pack.entry({ name: d._id + '.json' }, data, function (err) {
|
||||
if (err != null) throw err
|
||||
})
|
||||
}
|
||||
}
|
||||
if (needRetrieve.length > 0) {
|
||||
const docs = await connection.loadDocs(c, needRetrieve)
|
||||
|
||||
// Chunk data into small pieces
|
||||
if (addedDocuments > dataBlobSize && _pack !== undefined) {
|
||||
_pack.finalize()
|
||||
_pack = undefined
|
||||
addedDocuments = 0
|
||||
}
|
||||
if (_pack === undefined) {
|
||||
_pack = pack()
|
||||
stIndex++
|
||||
const storageFile = join(backupIndex, `${c}-data-${snapshot.date}-${stIndex}.tar.gz`)
|
||||
console.log('storing from domain', c, storageFile)
|
||||
domainInfo.storage.push(storageFile)
|
||||
const dataStream = await storage.write(storageFile)
|
||||
const storageZip = createGzip()
|
||||
|
||||
_pack.pipe(storageZip)
|
||||
storageZip.pipe(dataStream)
|
||||
}
|
||||
|
||||
for (const d of docs) {
|
||||
if (d._class === core.class.BlobData) {
|
||||
const blob = d as BlobData
|
||||
const data = Buffer.from(blob.base64Data, 'base64')
|
||||
blob.base64Data = ''
|
||||
const descrJson = JSON.stringify(d)
|
||||
addedDocuments += descrJson.length
|
||||
addedDocuments += data.length
|
||||
_pack.entry({ name: d._id + '.json' }, descrJson, function (err) {
|
||||
if (err != null) throw err
|
||||
})
|
||||
_pack.entry({ name: d._id }, data, function (err) {
|
||||
if (err != null) throw err
|
||||
})
|
||||
} else {
|
||||
const data = JSON.stringify(d)
|
||||
addedDocuments += data.length
|
||||
_pack.entry({ name: d._id + '.json' }, data, function (err) {
|
||||
if (err != null) throw err
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (it.finished) {
|
||||
break
|
||||
}
|
||||
}
|
||||
changes.removed = Array.from(digest.keys())
|
||||
if (changes.removed.length > 0) {
|
||||
@ -255,6 +292,8 @@ export async function backup (transactorUrl: string, workspaceId: WorkspaceId, s
|
||||
domainInfo.removed = changes.removed.length
|
||||
await storage.writeFile(domainInfo.snapshot, gzipSync(JSON.stringify(changes)))
|
||||
_pack?.finalize()
|
||||
// This will allow to retry in case of critical error.
|
||||
await storage.writeFile(infoFile, gzipSync(JSON.stringify(backupInfo, undefined, 2)))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -307,17 +307,13 @@ export function start (
|
||||
perMessageDeflate: {
|
||||
zlibDeflateOptions: {
|
||||
// See zlib defaults.
|
||||
chunkSize: 1024,
|
||||
chunkSize: 10 * 1024,
|
||||
memLevel: 7,
|
||||
level: 3
|
||||
},
|
||||
zlibInflateOptions: {
|
||||
chunkSize: 10 * 1024
|
||||
},
|
||||
// Below options specified as default values.
|
||||
concurrencyLimit: 10, // Limits zlib concurrency for perf.
|
||||
threshold: 1024 // Size (in bytes) below which messages
|
||||
// should not be compressed if context takeover is disabled.
|
||||
}
|
||||
}
|
||||
})
|
||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||
|
Loading…
Reference in New Issue
Block a user