diff --git a/apps/meteor/app/webdav/client/actionButton.ts b/apps/meteor/app/webdav/client/actionButton.ts index f3f24b6232a9..ed6e764d9a47 100644 --- a/apps/meteor/app/webdav/client/actionButton.ts +++ b/apps/meteor/app/webdav/client/actionButton.ts @@ -1,10 +1,12 @@ import { Meteor } from 'meteor/meteor'; -import { t, getURL } from '../../utils/client'; +import { getURL } from '../../utils/client'; import { WebdavAccounts } from '../../models/client'; import { settings } from '../../settings/client'; -import { MessageAction, modal } from '../../ui-utils/client'; +import { MessageAction } from '../../ui-utils/client'; import { messageArgs } from '../../../client/lib/utils/messageArgs'; +import { imperativeModal } from '../../../client/lib/imperativeModal'; +import SaveToWebdav from '../../../client/views/room/webdav/SaveToWebdavModal'; Meteor.startup(function () { MessageAction.addButton({ @@ -27,21 +29,16 @@ Meteor.startup(function () { action(_, props) { const { message = messageArgs(this).msg } = props; const [attachment] = message.attachments || []; - const { file } = message; const url = getURL(attachment.title_link, { full: true }); - modal.open({ - data: { - message, - attachment, - file, - url, + imperativeModal.open({ + component: SaveToWebdav, + props: { + data: { + attachment, + url, + }, + onClose: imperativeModal.close, }, - title: t('Save_To_Webdav'), - content: 'selectWebdavAccount', - showCancelButton: true, - showConfirmButton: false, - closeOnCancel: true, - html: true, }); }, order: 100, diff --git a/apps/meteor/app/webdav/client/index.js b/apps/meteor/app/webdav/client/index.js index 48268131aca3..2ff38da5fd71 100644 --- a/apps/meteor/app/webdav/client/index.js +++ b/apps/meteor/app/webdav/client/index.js @@ -15,7 +15,5 @@ Meteor.startup(() => { import('./webdavFilePicker.html'); import('./webdavFilePicker.css'); import('./webdavFilePicker'); - import('./selectWebdavAccount.html'); - import('./selectWebdavAccount'); }); }); diff --git a/apps/meteor/app/webdav/client/selectWebdavAccount.html b/apps/meteor/app/webdav/client/selectWebdavAccount.html deleted file mode 100644 index 69b58eb2d909..000000000000 --- a/apps/meteor/app/webdav/client/selectWebdavAccount.html +++ /dev/null @@ -1,13 +0,0 @@ - - - - {{#each webdavAccounts}} - - - {{usernamePlusServer this}} - - - {{/each}} - - - diff --git a/apps/meteor/app/webdav/client/selectWebdavAccount.js b/apps/meteor/app/webdav/client/selectWebdavAccount.js deleted file mode 100644 index 0bfbaca77fb5..000000000000 --- a/apps/meteor/app/webdav/client/selectWebdavAccount.js +++ /dev/null @@ -1,44 +0,0 @@ -import { Meteor } from 'meteor/meteor'; -import { Template } from 'meteor/templating'; - -import { modal } from '../../ui-utils'; -import { t } from '../../utils'; -import { WebdavAccounts } from '../../models/client'; -import { dispatchToastMessage } from '../../../client/lib/toast'; - -Template.selectWebdavAccount.helpers({ - webdavAccounts() { - return WebdavAccounts.find().fetch(); - }, - usernamePlusServer(account) { - return account.name || `${account.username}@${account.server_url.replace(/^https?\:\/\//i, '')}`; - }, -}); -Template.selectWebdavAccount.events({ - 'click .webdav-account'() { - modal.close(); - const accountId = this._id; - const { url } = Template.instance().data; - const name = Template.instance().data.attachment.title; - - const fileRequest = new XMLHttpRequest(); - fileRequest.open('GET', url, true); - fileRequest.responseType = 'arraybuffer'; - fileRequest.onload = function () { - const arrayBuffer = fileRequest.response; - if (arrayBuffer) { - const fileData = new Uint8Array(arrayBuffer); - Meteor.call('uploadFileToWebdav', accountId, fileData, name, (error, response) => { - if (error) { - return dispatchToastMessage({ type: 'error', message: t(error.error) }); - } - if (!response.success) { - return dispatchToastMessage({ type: 'error', message: t(response.message) }); - } - return dispatchToastMessage({ type: 'success', message: t('File_uploaded') }); - }); - } - }; - fileRequest.send(null); - }, -}); diff --git a/apps/meteor/app/webdav/server/methods/getWebdavCredentials.ts b/apps/meteor/app/webdav/server/lib/getWebdavCredentials.ts similarity index 78% rename from apps/meteor/app/webdav/server/methods/getWebdavCredentials.ts rename to apps/meteor/app/webdav/server/lib/getWebdavCredentials.ts index 873050ccd2db..0b0526a07fbd 100644 --- a/apps/meteor/app/webdav/server/methods/getWebdavCredentials.ts +++ b/apps/meteor/app/webdav/server/lib/getWebdavCredentials.ts @@ -1,4 +1,4 @@ -import { ServerCredentials } from '../lib/webdavClientAdapter'; +import { ServerCredentials } from './webdavClientAdapter'; export function getWebdavCredentials(account: ServerCredentials): ServerCredentials { const cred = account.token diff --git a/apps/meteor/app/webdav/server/lib/uploadFileToWebdav.ts b/apps/meteor/app/webdav/server/lib/uploadFileToWebdav.ts new file mode 100644 index 000000000000..c7033894c8e4 --- /dev/null +++ b/apps/meteor/app/webdav/server/lib/uploadFileToWebdav.ts @@ -0,0 +1,21 @@ +import { WebdavAccounts } from '@rocket.chat/models'; +import type { IWebdavAccount } from '@rocket.chat/core-typings'; + +import { getWebdavCredentials } from './getWebdavCredentials'; +import { WebdavClientAdapter } from './webdavClientAdapter'; + +export const uploadFileToWebdav = async (accountId: IWebdavAccount['_id'], fileData: string | Buffer, name: string): Promise => { + const account = await WebdavAccounts.findOneById(accountId); + if (!account) { + throw new Error('error-invalid-account'); + } + + const uploadFolder = 'Rocket.Chat Uploads/'; + const buffer = Buffer.from(fileData); + + const cred = getWebdavCredentials(account); + const client = new WebdavClientAdapter(account.serverURL, cred); + // eslint-disable-next-line @typescript-eslint/no-empty-function + await client.createDirectory(uploadFolder).catch(() => {}); + await client.putFileContents(`${uploadFolder}/${name}`, buffer, { overwrite: false }); +}; diff --git a/apps/meteor/app/webdav/server/methods/getFileFromWebdav.ts b/apps/meteor/app/webdav/server/methods/getFileFromWebdav.ts index ea753de9d670..a6a5711f6480 100644 --- a/apps/meteor/app/webdav/server/methods/getFileFromWebdav.ts +++ b/apps/meteor/app/webdav/server/methods/getFileFromWebdav.ts @@ -2,7 +2,7 @@ import { Meteor } from 'meteor/meteor'; import { WebdavAccounts } from '@rocket.chat/models'; import { settings } from '../../../settings/server'; -import { getWebdavCredentials } from './getWebdavCredentials'; +import { getWebdavCredentials } from '../lib/getWebdavCredentials'; import { WebdavClientAdapter } from '../lib/webdavClientAdapter'; Meteor.methods({ diff --git a/apps/meteor/app/webdav/server/methods/getWebdavFileList.ts b/apps/meteor/app/webdav/server/methods/getWebdavFileList.ts index e1d980132a93..e7e6b1ba63f7 100644 --- a/apps/meteor/app/webdav/server/methods/getWebdavFileList.ts +++ b/apps/meteor/app/webdav/server/methods/getWebdavFileList.ts @@ -2,7 +2,7 @@ import { Meteor } from 'meteor/meteor'; import { WebdavAccounts } from '@rocket.chat/models'; import { settings } from '../../../settings/server'; -import { getWebdavCredentials } from './getWebdavCredentials'; +import { getWebdavCredentials } from '../lib/getWebdavCredentials'; import { WebdavClientAdapter } from '../lib/webdavClientAdapter'; Meteor.methods({ diff --git a/apps/meteor/app/webdav/server/methods/getWebdavFilePreview.ts b/apps/meteor/app/webdav/server/methods/getWebdavFilePreview.ts index 735be207431c..98fb402efc78 100644 --- a/apps/meteor/app/webdav/server/methods/getWebdavFilePreview.ts +++ b/apps/meteor/app/webdav/server/methods/getWebdavFilePreview.ts @@ -3,7 +3,7 @@ import { createClient } from 'webdav'; import { WebdavAccounts } from '@rocket.chat/models'; import { settings } from '../../../settings/server'; -import { getWebdavCredentials } from './getWebdavCredentials'; +import { getWebdavCredentials } from '../lib/getWebdavCredentials'; Meteor.methods({ async getWebdavFilePreview(accountId, path) { diff --git a/apps/meteor/app/webdav/server/methods/uploadFileToWebdav.ts b/apps/meteor/app/webdav/server/methods/uploadFileToWebdav.ts index 2c7c7cc8a386..efc30558498e 100644 --- a/apps/meteor/app/webdav/server/methods/uploadFileToWebdav.ts +++ b/apps/meteor/app/webdav/server/methods/uploadFileToWebdav.ts @@ -1,45 +1,36 @@ import { Meteor } from 'meteor/meteor'; -import { WebdavAccounts } from '@rocket.chat/models'; +import { MeteorError } from '../../../../server/sdk/errors'; import { settings } from '../../../settings/server'; import { Logger } from '../../../logger/server'; -import { getWebdavCredentials } from './getWebdavCredentials'; -import { WebdavClientAdapter } from '../lib/webdavClientAdapter'; +import { uploadFileToWebdav } from '../lib/uploadFileToWebdav'; const logger = new Logger('WebDAV_Upload'); Meteor.methods({ async uploadFileToWebdav(accountId, fileData, name) { if (!Meteor.userId()) { - throw new Meteor.Error('error-invalid-user', 'Invalid User', { + throw new MeteorError('error-invalid-user', 'Invalid User', { method: 'uploadFileToWebdav', }); } if (!settings.get('Webdav_Integration_Enabled')) { - throw new Meteor.Error('error-not-allowed', 'WebDAV Integration Not Allowed', { + throw new MeteorError('error-not-allowed', 'WebDAV Integration Not Allowed', { method: 'uploadFileToWebdav', }); } - const account = await WebdavAccounts.findOneById(accountId); - if (!account) { - throw new Meteor.Error('error-invalid-account', 'Invalid WebDAV Account', { - method: 'uploadFileToWebdav', - }); - } - - const uploadFolder = 'Rocket.Chat Uploads/'; - const buffer = Buffer.from(fileData); - try { - const cred = getWebdavCredentials(account); - const client = new WebdavClientAdapter(account.serverURL, cred); - // eslint-disable-next-line @typescript-eslint/no-empty-function - await client.createDirectory(uploadFolder).catch(() => {}); - await client.putFileContents(`${uploadFolder}/${name}`, buffer, { overwrite: false }); + await uploadFileToWebdav(accountId, fileData, name); return { success: true }; } catch (error: any) { + if (typeof error === 'object' && error instanceof Error && error.name === 'error-invalid-account') { + throw new MeteorError(error.name, 'Invalid WebDAV Account', { + method: 'uploadFileToWebdav', + }); + } + // @ts-ignore logger.error(error); diff --git a/apps/meteor/client/lib/getWebdavServerName.ts b/apps/meteor/client/lib/getWebdavServerName.ts new file mode 100644 index 000000000000..7126a08f0bc7 --- /dev/null +++ b/apps/meteor/client/lib/getWebdavServerName.ts @@ -0,0 +1,4 @@ +import { IWebdavAccountIntegration } from '@rocket.chat/core-typings'; + +export const getWebdavServerName = ({ name, serverURL, username }: Omit): string => + name || `${username}@${serverURL?.replace(/^https?\:\/\//i, '')}`; diff --git a/apps/meteor/client/views/account/integrations/AccountIntegrationsPage.tsx b/apps/meteor/client/views/account/integrations/AccountIntegrationsPage.tsx index cfb044cfb4af..ea9e621cbce4 100644 --- a/apps/meteor/client/views/account/integrations/AccountIntegrationsPage.tsx +++ b/apps/meteor/client/views/account/integrations/AccountIntegrationsPage.tsx @@ -1,4 +1,4 @@ -import type { IWebdavAccount } from '@rocket.chat/core-typings'; +import type { IWebdavAccountIntegration } from '@rocket.chat/core-typings'; import { Box, Select, SelectOption, Field, Button } from '@rocket.chat/fuselage'; import { useToastMessageDispatch, useMethod, useTranslation } from '@rocket.chat/ui-contexts'; import React, { useMemo, useCallback, ReactElement } from 'react'; @@ -7,13 +7,9 @@ import { WebdavAccounts } from '../../../../app/models/client'; import Page from '../../../components/Page'; import { useForm } from '../../../hooks/useForm'; import { useReactiveValue } from '../../../hooks/useReactiveValue'; +import { getWebdavServerName } from '../../../lib/getWebdavServerName'; -type WebdavAccountIntegration = Omit; - -const getWebdavAccounts = (): Array => WebdavAccounts.find().fetch(); - -const getServerName = ({ name, serverURL, username }: Omit): string => - name || `${username}@${serverURL?.replace(/^https?\:\/\//i, '')}`; +const getWebdavAccounts = (): IWebdavAccountIntegration[] => WebdavAccounts.find().fetch(); const AccountIntegrationsPage = (): ReactElement => { const t = useTranslation(); @@ -26,7 +22,7 @@ const AccountIntegrationsPage = (): ReactElement => { handlers: { handleSelected }, } = useForm({ selected: [] }); - const options: SelectOption[] = useMemo(() => accounts.map(({ _id, ...current }) => [_id, getServerName(current)]), [accounts]); + const options: SelectOption[] = useMemo(() => accounts?.map(({ _id, ...current }) => [_id, getWebdavServerName(current)]), [accounts]); const handleClickRemove = useCallback(() => { try { diff --git a/apps/meteor/client/views/room/webdav/SaveToWebdavModal.tsx b/apps/meteor/client/views/room/webdav/SaveToWebdavModal.tsx new file mode 100644 index 000000000000..7fbd291c8e36 --- /dev/null +++ b/apps/meteor/client/views/room/webdav/SaveToWebdavModal.tsx @@ -0,0 +1,120 @@ +import { MessageAttachment, IWebdavAccount } from '@rocket.chat/core-typings'; +import { Modal, Box, ButtonGroup, Button, FieldGroup, Field, Select, SelectOption, Throbber } from '@rocket.chat/fuselage'; +import { useUniqueId } from '@rocket.chat/fuselage-hooks'; +import { useMethod, useToastMessageDispatch, useTranslation } from '@rocket.chat/ui-contexts'; +import React, { ReactElement, useState, useMemo, useEffect, useRef } from 'react'; +import { useForm, Controller } from 'react-hook-form'; + +import { useEndpointData } from '../../../hooks/useEndpointData'; +import { getWebdavServerName } from '../../../lib/getWebdavServerName'; + +type SaveToWebdavModalProps = { + onClose: () => void; + data: { + attachment: MessageAttachment; + url: string; + }; +}; + +const SaveToWebdavModal = ({ onClose, data }: SaveToWebdavModalProps): ReactElement => { + const t = useTranslation(); + const [isLoading, setIsLoading] = useState(false); + const dispatchToastMessage = useToastMessageDispatch(); + const uploadFileToWebdav = useMethod('uploadFileToWebdav'); + const fileRequest = useRef(null); + const accountIdField = useUniqueId(); + + const { + control, + handleSubmit, + formState: { errors }, + } = useForm<{ accountId: string }>(); + + const { value } = useEndpointData('/v1/webdav.getMyAccounts'); + + const accountsOptions: SelectOption[] = useMemo(() => { + if (value?.accounts) { + return value.accounts.map(({ _id, ...current }) => [_id, getWebdavServerName(current)]); + } + + return []; + }, [value?.accounts]); + + useEffect(() => fileRequest.current?.abort, []); + + const handleSaveFile = ({ accountId }: { accountId: IWebdavAccount['_id'] }): void => { + setIsLoading(true); + + const { + url, + attachment: { title }, + } = data; + + fileRequest.current = new XMLHttpRequest(); + fileRequest.current.open('GET', url, true); + fileRequest.current.responseType = 'arraybuffer'; + fileRequest.current.onload = async (): Promise => { + const arrayBuffer = fileRequest.current?.response; + if (arrayBuffer) { + const fileData = new Uint8Array(arrayBuffer); + + try { + const response = await uploadFileToWebdav(accountId, fileData, title); + if (!response.success) { + return dispatchToastMessage({ type: 'error', message: t(response.message) }); + } + return dispatchToastMessage({ type: 'success', message: t('File_uploaded') }); + } catch (error) { + return dispatchToastMessage({ type: 'error', message: error as Error }); + } finally { + setIsLoading(false); + onClose(); + } + } + }; + fileRequest.current.send(null); + }; + + return ( + + + {t('Save_To_Webdav')} + + + + {isLoading && ( + + + + )} + {!isLoading && ( + + + {t('Select_a_webdav_server')} + + ( + + )} + /> + + {errors.accountId && {t('Field_required')}} + + + )} + + + + {t('Cancel')} + + {isLoading ? t('Please_wait') : t('Save_To_Webdav')} + + + + + ); +}; +export default SaveToWebdavModal; diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json index 18858486d0ff..390329f6acbe 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json @@ -4041,6 +4041,7 @@ "Select_a_department": "Select a department", "Select_a_room": "Select a room", "Select_a_user": "Select a user", + "Select_a_webdav_server": "Select a WebDAV server", "Select_an_avatar": "Select an avatar", "Select_an_option": "Select an option", "Select_at_least_one_user": "Select at least one user", diff --git a/packages/core-typings/src/IWebdavAccount.ts b/packages/core-typings/src/IWebdavAccount.ts index f1a834be7dc5..f45dd992a746 100644 --- a/packages/core-typings/src/IWebdavAccount.ts +++ b/packages/core-typings/src/IWebdavAccount.ts @@ -8,4 +8,6 @@ export interface IWebdavAccount extends IRocketChatRecord { name: string; } +export type IWebdavAccountIntegration = Pick; + export type IWebdavAccountPayload = Pick; diff --git a/packages/rest-typings/src/v1/webdav.ts b/packages/rest-typings/src/v1/webdav.ts index a4ee811c546a..d048fa6a0324 100644 --- a/packages/rest-typings/src/v1/webdav.ts +++ b/packages/rest-typings/src/v1/webdav.ts @@ -1,9 +1,9 @@ -import type { IWebdavAccount } from '@rocket.chat/core-typings'; +import type { IWebdavAccountIntegration } from '@rocket.chat/core-typings'; export type WebdavEndpoints = { '/v1/webdav.getMyAccounts': { GET: () => { - accounts: Pick[]; + accounts: IWebdavAccountIntegration[]; }; }; }; diff --git a/packages/ui-contexts/src/ServerContext/methods.ts b/packages/ui-contexts/src/ServerContext/methods.ts index 9ae0a253adb3..f94bd8d96a0d 100644 --- a/packages/ui-contexts/src/ServerContext/methods.ts +++ b/packages/ui-contexts/src/ServerContext/methods.ts @@ -11,6 +11,7 @@ import type { SaveRoomSettingsMethod } from './methods/saveRoomSettings'; import type { SaveSettingsMethod } from './methods/saveSettings'; import type { SaveUserPreferencesMethod } from './methods/saveUserPreferences'; import type { UnfollowMessageMethod } from './methods/unfollowMessage'; +import type { UploadFileToWebdav } from './methods/uploadFileToWebdav'; // TODO: frontend chapter day - define methods @@ -117,6 +118,7 @@ export interface ServerMethods { 'updateOAuthApp': (...args: any[]) => any; 'updateOutgoingIntegration': (...args: any[]) => any; 'uploadCustomSound': (...args: any[]) => any; + 'uploadFileToWebdav': UploadFileToWebdav; 'Mailer:unsubscribe': MailerUnsubscribeMethod; 'getRoomById': (rid: IRoom['_id']) => IRoom; 'getReadReceipts': GetReadReceiptsMethod; diff --git a/packages/ui-contexts/src/ServerContext/methods/uploadFileToWebdav.ts b/packages/ui-contexts/src/ServerContext/methods/uploadFileToWebdav.ts new file mode 100644 index 000000000000..98ced8c70181 --- /dev/null +++ b/packages/ui-contexts/src/ServerContext/methods/uploadFileToWebdav.ts @@ -0,0 +1,9 @@ +import type { IWebdavAccount } from '@rocket.chat/core-typings'; + +import type { TranslationKey } from '../../TranslationContext'; + +export type UploadFileToWebdav = ( + accountId: IWebdavAccount['_id'], + fileData: Uint8Array, + name?: string, +) => { success: boolean; message: TranslationKey };