mirror of
https://github.com/hcengineering/platform.git
synced 2024-12-26 04:54:40 +03:00
UBERF-7126: Support rich editor blob resolve (#5727)
Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
parent
a437926f35
commit
cbc13c7b14
@ -15,7 +15,7 @@
|
||||
import { FilePreviewPopup } from '@hcengineering/presentation'
|
||||
import { ImageNode, type ImageOptions as ImageNodeOptions } from '@hcengineering/text'
|
||||
import { showPopup } from '@hcengineering/ui'
|
||||
import { mergeAttributes, nodeInputRule } from '@tiptap/core'
|
||||
import { nodeInputRule } from '@tiptap/core'
|
||||
import { Plugin, PluginKey } from '@tiptap/pm/state'
|
||||
|
||||
/**
|
||||
@ -69,8 +69,7 @@ export const ImageExtension = ImageNode.extend<ImageOptions>({
|
||||
return {
|
||||
inline: true,
|
||||
HTMLAttributes: {},
|
||||
getFileUrl: () => '',
|
||||
getFileUrlSrcSet: () => ''
|
||||
getBlobRef: async () => ({ src: '', srcset: '' })
|
||||
}
|
||||
},
|
||||
|
||||
@ -85,53 +84,6 @@ export const ImageExtension = ImageNode.extend<ImageOptions>({
|
||||
]
|
||||
},
|
||||
|
||||
renderHTML ({ node, HTMLAttributes }) {
|
||||
const divAttributes = {
|
||||
class: 'text-editor-image-container',
|
||||
'data-type': this.name,
|
||||
'data-align': node.attrs.align
|
||||
}
|
||||
|
||||
const imgAttributes = mergeAttributes(
|
||||
{
|
||||
'data-type': this.name
|
||||
},
|
||||
this.options.HTMLAttributes,
|
||||
HTMLAttributes
|
||||
)
|
||||
const getFileUrl = this.options.getFileUrl
|
||||
const getFileUrlSrcSet = this.options.getFileUrlSrcSet
|
||||
|
||||
const id = imgAttributes['file-id']
|
||||
if (id != null) {
|
||||
imgAttributes.src = getFileUrl(id)
|
||||
let width: number | undefined
|
||||
// TODO: Use max width of component may be?
|
||||
switch (imgAttributes.width) {
|
||||
case '32px':
|
||||
width = 32
|
||||
break
|
||||
case '64px':
|
||||
width = 64
|
||||
break
|
||||
case '128px':
|
||||
width = 128
|
||||
break
|
||||
case '256px':
|
||||
width = 256
|
||||
break
|
||||
case '512px':
|
||||
width = 512
|
||||
break
|
||||
}
|
||||
imgAttributes.srcset = getFileUrlSrcSet(id, width)
|
||||
imgAttributes.class = 'text-editor-image'
|
||||
imgAttributes.contentEditable = false
|
||||
}
|
||||
|
||||
return ['div', divAttributes, ['img', imgAttributes]]
|
||||
},
|
||||
|
||||
addCommands () {
|
||||
return {
|
||||
setImage:
|
||||
|
@ -22,7 +22,7 @@ import TaskList from '@tiptap/extension-task-list'
|
||||
|
||||
import { DefaultKit, type DefaultKitOptions } from './default-kit'
|
||||
|
||||
import { getFileUrl, getFileSrcSet } from '@hcengineering/presentation'
|
||||
import { getBlobRef } from '@hcengineering/presentation'
|
||||
import { CodeBlockExtension, codeBlockOptions } from '@hcengineering/text'
|
||||
import { CodemarkExtension } from '../components/extension/codemark'
|
||||
import { FileExtension, type FileOptions } from '../components/extension/fileExt'
|
||||
@ -104,8 +104,9 @@ export const EditorKit = Extension.create<EditorKitOptions>({
|
||||
? [
|
||||
ImageExtension.configure({
|
||||
inline: true,
|
||||
getFileUrl,
|
||||
getFileUrlSrcSet: getFileSrcSet,
|
||||
loadingImgSrc:
|
||||
'data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4NCjxzdmcgd2lkdGg9IjMycHgiIGhlaWdodD0iMzJweCIgdmlld0JveD0iMCAwIDE2IDE2IiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPg0KICAgIDxwYXRoIGQ9Im0gNCAxIGMgLTEuNjQ0NTMxIDAgLTMgMS4zNTU0NjkgLTMgMyB2IDEgaCAxIHYgLTEgYyAwIC0xLjEwOTM3NSAwLjg5MDYyNSAtMiAyIC0yIGggMSB2IC0xIHogbSAyIDAgdiAxIGggNCB2IC0xIHogbSA1IDAgdiAxIGggMSBjIDEuMTA5Mzc1IDAgMiAwLjg5MDYyNSAyIDIgdiAxIGggMSB2IC0xIGMgMCAtMS42NDQ1MzEgLTEuMzU1NDY5IC0zIC0zIC0zIHogbSAtNSA0IGMgLTAuNTUwNzgxIDAgLTEgMC40NDkyMTkgLTEgMSBzIDAuNDQ5MjE5IDEgMSAxIHMgMSAtMC40NDkyMTkgMSAtMSBzIC0wLjQ0OTIxOSAtMSAtMSAtMSB6IG0gLTUgMSB2IDQgaCAxIHYgLTQgeiBtIDEzIDAgdiA0IGggMSB2IC00IHogbSAtNC41IDIgbCAtMiAyIGwgLTEuNSAtMSBsIC0yIDIgdiAwLjUgYyAwIDAuNSAwLjUgMC41IDAuNSAwLjUgaCA3IHMgMC40NzI2NTYgLTAuMDM1MTU2IDAuNSAtMC41IHYgLTEgeiBtIC04LjUgMyB2IDEgYyAwIDEuNjQ0NTMxIDEuMzU1NDY5IDMgMyAzIGggMSB2IC0xIGggLTEgYyAtMS4xMDkzNzUgMCAtMiAtMC44OTA2MjUgLTIgLTIgdiAtMSB6IG0gMTMgMCB2IDEgYyAwIDEuMTA5Mzc1IC0wLjg5MDYyNSAyIC0yIDIgaCAtMSB2IDEgaCAxIGMgMS42NDQ1MzEgMCAzIC0xLjM1NTQ2OSAzIC0zIHYgLTEgeiBtIC04IDMgdiAxIGggNCB2IC0xIHogbSAwIDAiIGZpbGw9IiMyZTM0MzQiIGZpbGwtb3BhY2l0eT0iMC4zNDkwMiIvPg0KPC9zdmc+DQo=',
|
||||
getBlobRef: async (file, name, size) => await getBlobRef(undefined, file, name, size),
|
||||
...this.options.image
|
||||
})
|
||||
]
|
||||
|
@ -12,9 +12,9 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
import type { Blob, Ref } from '@hcengineering/core'
|
||||
import { Node, mergeAttributes } from '@tiptap/core'
|
||||
import { getDataAttribute } from './utils'
|
||||
import type { Ref, Blob } from '@hcengineering/core'
|
||||
|
||||
/**
|
||||
* @public
|
||||
@ -22,8 +22,9 @@ import type { Ref, Blob } from '@hcengineering/core'
|
||||
export interface ImageOptions {
|
||||
inline: boolean
|
||||
HTMLAttributes: Record<string, any>
|
||||
getFileUrl: (fileId: Ref<Blob>, filename?: string) => string
|
||||
getFileUrlSrcSet: (fileId: Ref<Blob>, size?: number) => string
|
||||
|
||||
loadingImgSrc?: string
|
||||
getBlobRef: (fileId: Ref<Blob>, filename?: string, size?: number) => Promise<{ src: string, srcset: string }>
|
||||
}
|
||||
|
||||
/**
|
||||
@ -36,8 +37,7 @@ export const ImageNode = Node.create<ImageOptions>({
|
||||
return {
|
||||
inline: true,
|
||||
HTMLAttributes: {},
|
||||
getFileUrl: () => '',
|
||||
getFileUrlSrcSet: () => ''
|
||||
getBlobRef: async () => ({ src: '', srcset: '' })
|
||||
}
|
||||
},
|
||||
|
||||
@ -102,13 +102,58 @@ export const ImageNode = Node.create<ImageOptions>({
|
||||
this.options.HTMLAttributes,
|
||||
HTMLAttributes
|
||||
)
|
||||
|
||||
const fileId = imgAttributes['file-id']
|
||||
if (fileId != null) {
|
||||
imgAttributes.src = this.options.getFileUrl(fileId)
|
||||
imgAttributes.srcset = this.options.getFileUrlSrcSet(fileId)
|
||||
imgAttributes.src = `platform://platform/files/workspace/?file=${fileId}`
|
||||
}
|
||||
|
||||
return ['div', divAttributes, ['img', imgAttributes]]
|
||||
},
|
||||
addNodeView () {
|
||||
return ({ node, HTMLAttributes }) => {
|
||||
const container = document.createElement('div')
|
||||
const imgElement = document.createElement('img')
|
||||
container.append(imgElement)
|
||||
|
||||
const divAttributes = {
|
||||
class: 'text-editor-image-container',
|
||||
'data-type': this.name,
|
||||
'data-align': node.attrs.align
|
||||
}
|
||||
|
||||
for (const [k, v] of Object.entries(divAttributes)) {
|
||||
container.setAttribute(k, v)
|
||||
}
|
||||
|
||||
const imgAttributes = mergeAttributes(
|
||||
{
|
||||
'data-type': this.name
|
||||
},
|
||||
this.options.HTMLAttributes,
|
||||
HTMLAttributes
|
||||
)
|
||||
for (const [k, v] of Object.entries(imgAttributes)) {
|
||||
if (k !== 'src' && k !== 'srcset' && v !== null) {
|
||||
imgElement.setAttribute(k, v)
|
||||
}
|
||||
}
|
||||
const fileId = imgAttributes['file-id']
|
||||
if (fileId != null) {
|
||||
const setBrokenImg = setTimeout(() => {
|
||||
imgElement.src = this.options.loadingImgSrc ?? `platform://platform/files/workspace/?file=${fileId}`
|
||||
}, 200)
|
||||
if (fileId != null) {
|
||||
void this.options.getBlobRef(fileId).then((val) => {
|
||||
clearTimeout(setBrokenImg)
|
||||
imgElement.src = val.src
|
||||
imgElement.srcset = val.srcset
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
dom: container
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
@ -77,9 +77,12 @@ export async function start (
|
||||
const extensions = [
|
||||
ServerKit.configure({
|
||||
image: {
|
||||
getFileUrl: (fileId, size) => {
|
||||
getBlobRef: async (fileId, name, size) => {
|
||||
const sz = size !== undefined ? `&size=${size}` : ''
|
||||
return `${config.UploadUrl}?file=${fileId}${sz}`
|
||||
return {
|
||||
src: `${config.UploadUrl}?file=${fileId}`,
|
||||
srcset: `${config.UploadUrl}?file=${fileId}${sz}`
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
56
server/front/readme.md
Normal file
56
server/front/readme.md
Normal file
@ -0,0 +1,56 @@
|
||||
# Overview
|
||||
|
||||
Front service is suited to deliver application bundles and resource assets, it also work as resize/recode service for previews, perform blob storage front operations.
|
||||
|
||||
## Configuration
|
||||
|
||||
* SERVER_PORT: Specifies the port number on which the server will listen.
|
||||
* TRANSACTOR_URL: Specifies the URL of the transactor.
|
||||
* MONGO_URL: Specifies the URL of the MongoDB database.
|
||||
* ELASTIC_URL: Specifies the URL of the Elasticsearch service.
|
||||
* ACCOUNTS_URL: Specifies the URL of the accounts service.
|
||||
* UPLOAD_URL: Specifies the URL for uploading files.
|
||||
* GMAIL_URL: Specifies the URL of the Gmail service.
|
||||
* CALENDAR_URL: Specifies the URL of the calendar service.
|
||||
* TELEGRAM_URL: Specifies the URL of the Telegram service.
|
||||
* REKONI_URL: Specifies the URL of the Rekoni service.
|
||||
* COLLABORATOR_URL: Specifies the URL of the collaborator service.
|
||||
* COLLABORATOR_API_URL: Specifies the URL of the collaborator API.
|
||||
* MODEL_VERSION: Specifies the required model version.
|
||||
* SERVER_SECRET: Specifies the server secret.
|
||||
* PREVIEW_CONFIG: Specifies the preview configuration.
|
||||
* BRANDING_URL: Specifies the URL of the branding service.
|
||||
|
||||
## Preview service configuration
|
||||
|
||||
PREVIEW_CONFIG env variable foremat.
|
||||
|
||||
A `;` separated list of triples, providerName|previewUrl|supportedFormats.
|
||||
|
||||
- providerName - a provider name should be same as in Storage configuration.
|
||||
- previewUrl - an Url with :workspace, :blobId, :downloadFile, :size, :format placeholders, they will be replaced in UI with an appropriate blob values.
|
||||
- supportedFormats - a `,` separated list of file extensions.
|
||||
|
||||
PREVIEW_CONFIG=*|https://front.hc.engineering/files/:workspace/api/preview/?format=:format&width=:size&image=:downloadFile
|
||||
|
||||
## Variables
|
||||
|
||||
- :workspace - a current workspacw public url name segment.
|
||||
- :blobId - an uniq blob _id identifier.
|
||||
- :size - a numeric value to determine required size of the image, image will not be upscaled, only downscaled. If -1 is passed, original image size value will be used.
|
||||
- :downloadFile - an URI encoded component value of full download URI, could be presigned uri to S3 storage.
|
||||
- :format - an a conversion file format, `avif`,`webp` etc.
|
||||
|
||||
## Passing default variant.
|
||||
|
||||
providerName could be set to `*` in this case it will be default preview provider.
|
||||
|
||||
## Default variant.
|
||||
|
||||
If no preview config are specified, a default one targating a front service preview/resize functionality will be used.
|
||||
|
||||
`/files/${getCurrentWorkspaceUrl()}?file=:blobId.:format&size=:size`
|
||||
|
||||
## Testing with dev-production/etc.
|
||||
|
||||
Only a downloadFile variant of URIs will work, since app is hosted on localhost and token should be valid to use preview on production environment.
|
Loading…
Reference in New Issue
Block a user