UBERF-7126: Support rich editor blob resolve (#5727)

Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
Andrey Sobolev 2024-06-05 13:13:48 +07:00 committed by GitHub
parent a437926f35
commit cbc13c7b14
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 120 additions and 63 deletions

View File

@ -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:

View File

@ -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
})
]

View File

@ -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
}
}
}
})

View File

@ -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
View 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.