diff --git a/packages/text-editor/src/components/extension/imageExt.ts b/packages/text-editor/src/components/extension/imageExt.ts index 1be30e424b..4c193f444c 100644 --- a/packages/text-editor/src/components/extension/imageExt.ts +++ b/packages/text-editor/src/components/extension/imageExt.ts @@ -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({ return { inline: true, HTMLAttributes: {}, - getFileUrl: () => '', - getFileUrlSrcSet: () => '' + getBlobRef: async () => ({ src: '', srcset: '' }) } }, @@ -85,53 +84,6 @@ export const ImageExtension = ImageNode.extend({ ] }, - 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: diff --git a/packages/text-editor/src/kits/editor-kit.ts b/packages/text-editor/src/kits/editor-kit.ts index 7490b05e50..1ae159cb6a 100644 --- a/packages/text-editor/src/kits/editor-kit.ts +++ b/packages/text-editor/src/kits/editor-kit.ts @@ -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({ ? [ 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 }) ] diff --git a/packages/text/src/nodes/image.ts b/packages/text/src/nodes/image.ts index 6517859f10..371702000c 100644 --- a/packages/text/src/nodes/image.ts +++ b/packages/text/src/nodes/image.ts @@ -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 - getFileUrl: (fileId: Ref, filename?: string) => string - getFileUrlSrcSet: (fileId: Ref, size?: number) => string + + loadingImgSrc?: string + getBlobRef: (fileId: Ref, filename?: string, size?: number) => Promise<{ src: string, srcset: string }> } /** @@ -36,8 +37,7 @@ export const ImageNode = Node.create({ return { inline: true, HTMLAttributes: {}, - getFileUrl: () => '', - getFileUrlSrcSet: () => '' + getBlobRef: async () => ({ src: '', srcset: '' }) } }, @@ -102,13 +102,58 @@ export const ImageNode = Node.create({ 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 + } + } } }) diff --git a/server/collaborator/src/server.ts b/server/collaborator/src/server.ts index 3be5be9337..2cfdcb428e 100644 --- a/server/collaborator/src/server.ts +++ b/server/collaborator/src/server.ts @@ -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}` + } } } }) diff --git a/server/front/readme.md b/server/front/readme.md new file mode 100644 index 0000000000..660304b37b --- /dev/null +++ b/server/front/readme.md @@ -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.