### What problem does this PR solve? feat: Move files in file manager #1826 ### Type of change - [x] New Feature (non-breaking change which adds functionality)tags/v0.10.0
| <svg t="1722928702193" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="6094" | |||||
| width="200" height="200"> | |||||
| <path | |||||
| d="M572.330667 597.333333H298.666667v-85.333333h273.664l-77.994667-77.994667L554.666667 373.632 735.701333 554.666667l-60.373333 60.330666L554.666667 735.701333l-60.330667-60.373333L572.330667 597.333333zM533.333333 263.509333H853.333333a85.333333 85.333333 0 0 1 85.333334 85.333334V810.666667a85.333333 85.333333 0 0 1-85.333334 85.333333H170.666667a85.333333 85.333333 0 0 1-85.333334-85.333333V213.333333a85.333333 85.333333 0 0 1 85.333334-85.333333h241.493333a85.333333 85.333333 0 0 1 76.117333 46.72L533.333333 263.509333z m0 85.333334a85.333333 85.333333 0 0 1-76.117333-46.72L412.202667 213.333333H170.666667v597.333334h682.666666V348.842667h-320z" | |||||
| fill="#666666" p-id="6095"></path> | |||||
| </svg> |
| loading: boolean; | loading: boolean; | ||||
| } | } | ||||
| export const useFetchPureFileList = () => { | |||||
| const { mutateAsync, isPending: loading } = useMutation({ | |||||
| mutationKey: ['fetchPureFileList'], | |||||
| gcTime: 0, | |||||
| mutationFn: async (parentId: string) => { | |||||
| const { data } = await fileManagerService.listFile({ | |||||
| parent_id: parentId, | |||||
| }); | |||||
| return data; | |||||
| }, | |||||
| }); | |||||
| return { loading, fetchList: mutateAsync }; | |||||
| }; | |||||
| export const useFetchFileList = (): ResponseType<any> & IListResult => { | export const useFetchFileList = (): ResponseType<any> & IListResult => { | ||||
| const { searchString, handleInputChange } = useHandleSearchChange(); | const { searchString, handleInputChange } = useHandleSearchChange(); | ||||
| const { pagination, setPagination } = useGetPaginationWithRouter(); | const { pagination, setPagination } = useGetPaginationWithRouter(); | ||||
| return { data, loading, connectFileToKnowledge: mutateAsync }; | return { data, loading, connectFileToKnowledge: mutateAsync }; | ||||
| }; | }; | ||||
| export interface IMoveFileBody { | |||||
| src_file_ids: string[]; | |||||
| dest_file_id: string; // target folder id | |||||
| } | |||||
| export const useMoveFile = () => { | |||||
| const queryClient = useQueryClient(); | |||||
| const { t } = useTranslation(); | |||||
| const { | |||||
| data, | |||||
| isPending: loading, | |||||
| mutateAsync, | |||||
| } = useMutation({ | |||||
| mutationKey: ['moveFile'], | |||||
| mutationFn: async (params: IMoveFileBody) => { | |||||
| const { data } = await fileManagerService.moveFile(params); | |||||
| if (data.retcode === 0) { | |||||
| message.success(t('message.operated')); | |||||
| queryClient.invalidateQueries({ queryKey: ['fetchFileList'] }); | |||||
| } | |||||
| return data.retcode; | |||||
| }, | |||||
| }); | |||||
| return { data, loading, moveFile: mutateAsync }; | |||||
| }; |
| download: 'Download', | download: 'Download', | ||||
| close: 'Close', | close: 'Close', | ||||
| preview: 'Preview', | preview: 'Preview', | ||||
| move: 'Move', | |||||
| }, | }, | ||||
| login: { | login: { | ||||
| login: 'Sign in', | login: 'Sign in', | ||||
| fileError: 'File error', | fileError: 'File error', | ||||
| uploadLimit: | uploadLimit: | ||||
| 'The file size cannot exceed 10M, and the total number of files cannot exceed 128', | 'The file size cannot exceed 10M, and the total number of files cannot exceed 128', | ||||
| destinationFolder: 'Destination folder', | |||||
| }, | }, | ||||
| flow: { | flow: { | ||||
| cite: 'Cite', | cite: 'Cite', |
| download: '下載', | download: '下載', | ||||
| close: '關閉', | close: '關閉', | ||||
| preview: '預覽', | preview: '預覽', | ||||
| move: '移動', | |||||
| }, | }, | ||||
| login: { | login: { | ||||
| login: '登入', | login: '登入', | ||||
| preview: '預覽', | preview: '預覽', | ||||
| fileError: '文件錯誤', | fileError: '文件錯誤', | ||||
| uploadLimit: '文件大小不能超過10M,文件總數不超過128個', | uploadLimit: '文件大小不能超過10M,文件總數不超過128個', | ||||
| destinationFolder: '目標資料夾', | |||||
| }, | }, | ||||
| flow: { | flow: { | ||||
| cite: '引用', | cite: '引用', |
| download: '下载', | download: '下载', | ||||
| close: '关闭', | close: '关闭', | ||||
| preview: '预览', | preview: '预览', | ||||
| move: '移动', | |||||
| }, | }, | ||||
| login: { | login: { | ||||
| login: '登录', | login: '登录', | ||||
| preview: '预览', | preview: '预览', | ||||
| fileError: '文件错误', | fileError: '文件错误', | ||||
| uploadLimit: '文件大小不能超过10M,文件总数不超过128个', | uploadLimit: '文件大小不能超过10M,文件总数不超过128个', | ||||
| destinationFolder: '目标文件夹', | |||||
| }, | }, | ||||
| flow: { | flow: { | ||||
| flow: '工作流', | flow: '工作流', |
| import NewDocumentLink from '@/components/new-document-link'; | |||||
| import SvgIcon from '@/components/svg-icon'; | |||||
| import { useTranslate } from '@/hooks/common-hooks'; | import { useTranslate } from '@/hooks/common-hooks'; | ||||
| import { IFile } from '@/interfaces/database/file-manager'; | import { IFile } from '@/interfaces/database/file-manager'; | ||||
| import { api_host } from '@/utils/api'; | import { api_host } from '@/utils/api'; | ||||
| import { | |||||
| getExtension, | |||||
| isSupportedPreviewDocumentType, | |||||
| } from '@/utils/document-util'; | |||||
| import { downloadFile } from '@/utils/file-util'; | import { downloadFile } from '@/utils/file-util'; | ||||
| import { | import { | ||||
| DeleteOutlined, | DeleteOutlined, | ||||
| } from '@ant-design/icons'; | } from '@ant-design/icons'; | ||||
| import { Button, Space, Tooltip } from 'antd'; | import { Button, Space, Tooltip } from 'antd'; | ||||
| import { useHandleDeleteFile } from '../hooks'; | import { useHandleDeleteFile } from '../hooks'; | ||||
| import NewDocumentLink from '@/components/new-document-link'; | |||||
| import { | |||||
| getExtension, | |||||
| isSupportedPreviewDocumentType, | |||||
| } from '@/utils/document-util'; | |||||
| import styles from './index.less'; | import styles from './index.less'; | ||||
| interface IProps { | interface IProps { | ||||
| record: IFile; | record: IFile; | ||||
| setCurrentRecord: (record: any) => void; | setCurrentRecord: (record: any) => void; | ||||
| showRenameModal: (record: IFile) => void; | showRenameModal: (record: IFile) => void; | ||||
| showMoveFileModal: (ids: string[]) => void; | |||||
| showConnectToKnowledgeModal: (record: IFile) => void; | showConnectToKnowledgeModal: (record: IFile) => void; | ||||
| setSelectedRowKeys(keys: string[]): void; | setSelectedRowKeys(keys: string[]): void; | ||||
| } | } | ||||
| showRenameModal, | showRenameModal, | ||||
| showConnectToKnowledgeModal, | showConnectToKnowledgeModal, | ||||
| setSelectedRowKeys, | setSelectedRowKeys, | ||||
| showMoveFileModal, | |||||
| }: IProps) => { | }: IProps) => { | ||||
| const documentId = record.id; | const documentId = record.id; | ||||
| const beingUsed = false; | const beingUsed = false; | ||||
| showConnectToKnowledgeModal(record); | showConnectToKnowledgeModal(record); | ||||
| }; | }; | ||||
| const onShowMoveFileModal = () => { | |||||
| showMoveFileModal([documentId]); | |||||
| }; | |||||
| return ( | return ( | ||||
| <Space size={0}> | <Space size={0}> | ||||
| {isKnowledgeBase || ( | {isKnowledgeBase || ( | ||||
| </Button> | </Button> | ||||
| </Tooltip> | </Tooltip> | ||||
| )} | )} | ||||
| {isKnowledgeBase || ( | |||||
| <Tooltip title={t('move', { keyPrefix: 'common' })}> | |||||
| <Button | |||||
| type="text" | |||||
| disabled={beingUsed} | |||||
| onClick={onShowMoveFileModal} | |||||
| className={styles.iconButton} | |||||
| > | |||||
| <SvgIcon name={`move`} width={16}></SvgIcon> | |||||
| </Button> | |||||
| </Tooltip> | |||||
| )} | |||||
| {isKnowledgeBase || ( | {isKnowledgeBase || ( | ||||
| <Tooltip title={t('delete', { keyPrefix: 'common' })}> | <Tooltip title={t('delete', { keyPrefix: 'common' })}> | ||||
| <Button | <Button |
| MenuProps, | MenuProps, | ||||
| Space, | Space, | ||||
| } from 'antd'; | } from 'antd'; | ||||
| import { useMemo } from 'react'; | |||||
| import { useCallback, useMemo } from 'react'; | |||||
| import { | import { | ||||
| useHandleBreadcrumbClick, | useHandleBreadcrumbClick, | ||||
| useHandleDeleteFile, | useHandleDeleteFile, | ||||
| useSelectBreadcrumbItems, | useSelectBreadcrumbItems, | ||||
| } from './hooks'; | } from './hooks'; | ||||
| import SvgIcon from '@/components/svg-icon'; | |||||
| import { | import { | ||||
| IListResult, | IListResult, | ||||
| useFetchParentFolderList, | useFetchParentFolderList, | ||||
| showFolderCreateModal: () => void; | showFolderCreateModal: () => void; | ||||
| showFileUploadModal: () => void; | showFileUploadModal: () => void; | ||||
| setSelectedRowKeys: (keys: string[]) => void; | setSelectedRowKeys: (keys: string[]) => void; | ||||
| showMoveFileModal: (ids: string[]) => void; | |||||
| } | } | ||||
| const FileToolbar = ({ | const FileToolbar = ({ | ||||
| setSelectedRowKeys, | setSelectedRowKeys, | ||||
| searchString, | searchString, | ||||
| handleInputChange, | handleInputChange, | ||||
| showMoveFileModal, | |||||
| }: IProps) => { | }: IProps) => { | ||||
| const { t } = useTranslate('knowledgeDetails'); | const { t } = useTranslate('knowledgeDetails'); | ||||
| const breadcrumbItems = useSelectBreadcrumbItems(); | const breadcrumbItems = useSelectBreadcrumbItems(); | ||||
| setSelectedRowKeys, | setSelectedRowKeys, | ||||
| ); | ); | ||||
| const handleShowMoveFileModal = useCallback(() => { | |||||
| showMoveFileModal(selectedRowKeys); | |||||
| }, [selectedRowKeys, showMoveFileModal]); | |||||
| const disabled = selectedRowKeys.length === 0; | const disabled = selectedRowKeys.length === 0; | ||||
| const items: MenuProps['items'] = useMemo(() => { | const items: MenuProps['items'] = useMemo(() => { | ||||
| </Flex> | </Flex> | ||||
| ), | ), | ||||
| }, | }, | ||||
| { | |||||
| key: '5', | |||||
| onClick: handleShowMoveFileModal, | |||||
| label: ( | |||||
| <Flex gap={10}> | |||||
| <span className={styles.deleteIconWrapper}> | |||||
| <SvgIcon name={`move`} width={18}></SvgIcon> | |||||
| </span> | |||||
| <b>{t('move', { keyPrefix: 'common' })}</b> | |||||
| </Flex> | |||||
| ), | |||||
| }, | |||||
| ]; | ]; | ||||
| }, [handleRemoveFile, t]); | |||||
| }, [handleShowMoveFileModal, t, handleRemoveFile]); | |||||
| return ( | return ( | ||||
| <div className={styles.filter}> | <div className={styles.filter}> |
| return onOk(ret.name); | return onOk(ret.name); | ||||
| }; | }; | ||||
| const handleCancel = () => { | |||||
| hideModal(); | |||||
| }; | |||||
| const onFinish = (values: any) => { | |||||
| console.log('Success:', values); | |||||
| }; | |||||
| const onFinishFailed = (errorInfo: any) => { | |||||
| console.log('Failed:', errorInfo); | |||||
| }; | |||||
| return ( | return ( | ||||
| <Modal | <Modal | ||||
| title={t('newFolder', { keyPrefix: 'fileManager' })} | title={t('newFolder', { keyPrefix: 'fileManager' })} | ||||
| open={visible} | open={visible} | ||||
| onOk={handleOk} | onOk={handleOk} | ||||
| onCancel={handleCancel} | |||||
| onCancel={hideModal} | |||||
| okButtonProps={{ loading }} | okButtonProps={{ loading }} | ||||
| confirmLoading={loading} | confirmLoading={loading} | ||||
| > | > | ||||
| labelCol={{ span: 4 }} | labelCol={{ span: 4 }} | ||||
| wrapperCol={{ span: 20 }} | wrapperCol={{ span: 20 }} | ||||
| style={{ maxWidth: 600 }} | style={{ maxWidth: 600 }} | ||||
| onFinish={onFinish} | |||||
| onFinishFailed={onFinishFailed} | |||||
| autoComplete="off" | autoComplete="off" | ||||
| form={form} | form={form} | ||||
| > | > |
| useCreateFolder, | useCreateFolder, | ||||
| useDeleteFile, | useDeleteFile, | ||||
| useFetchParentFolderList, | useFetchParentFolderList, | ||||
| useMoveFile, | |||||
| useRenameFile, | useRenameFile, | ||||
| useUploadFile, | useUploadFile, | ||||
| } from '@/hooks/file-manager-hooks'; | } from '@/hooks/file-manager-hooks'; | ||||
| return { handleBreadcrumbClick }; | return { handleBreadcrumbClick }; | ||||
| }; | }; | ||||
| export const useHandleMoveFile = ( | |||||
| setSelectedRowKeys: (keys: string[]) => void, | |||||
| ) => { | |||||
| const { | |||||
| visible: moveFileVisible, | |||||
| hideModal: hideMoveFileModal, | |||||
| showModal: showMoveFileModal, | |||||
| } = useSetModalState(); | |||||
| const { moveFile, loading } = useMoveFile(); | |||||
| const [sourceFileIds, setSourceFileIds] = useState<string[]>([]); | |||||
| const onMoveFileOk = useCallback( | |||||
| async (targetFolderId: string) => { | |||||
| const ret = await moveFile({ | |||||
| src_file_ids: sourceFileIds, | |||||
| dest_file_id: targetFolderId, | |||||
| }); | |||||
| if (ret === 0) { | |||||
| setSelectedRowKeys([]); | |||||
| hideMoveFileModal(); | |||||
| } | |||||
| return ret; | |||||
| }, | |||||
| [moveFile, hideMoveFileModal, sourceFileIds, setSelectedRowKeys], | |||||
| ); | |||||
| const handleShowMoveFileModal = useCallback( | |||||
| (ids: string[]) => { | |||||
| setSourceFileIds(ids); | |||||
| showMoveFileModal(); | |||||
| }, | |||||
| [showMoveFileModal], | |||||
| ); | |||||
| return { | |||||
| initialValue: '', | |||||
| moveFileLoading: loading, | |||||
| onMoveFileOk, | |||||
| moveFileVisible, | |||||
| hideMoveFileModal, | |||||
| showMoveFileModal: handleShowMoveFileModal, | |||||
| }; | |||||
| }; |
| useGetRowSelection, | useGetRowSelection, | ||||
| useHandleConnectToKnowledge, | useHandleConnectToKnowledge, | ||||
| useHandleCreateFolder, | useHandleCreateFolder, | ||||
| useHandleMoveFile, | |||||
| useHandleUploadFile, | useHandleUploadFile, | ||||
| useNavigateToOtherFolder, | useNavigateToOtherFolder, | ||||
| useRenameCurrentFile, | useRenameCurrentFile, | ||||
| import ConnectToKnowledgeModal from './connect-to-knowledge-modal'; | import ConnectToKnowledgeModal from './connect-to-knowledge-modal'; | ||||
| import FolderCreateModal from './folder-create-modal'; | import FolderCreateModal from './folder-create-modal'; | ||||
| import styles from './index.less'; | import styles from './index.less'; | ||||
| import FileMovingModal from './move-file-modal'; | |||||
| const { Text } = Typography; | const { Text } = Typography; | ||||
| initialValue, | initialValue, | ||||
| connectToKnowledgeLoading, | connectToKnowledgeLoading, | ||||
| } = useHandleConnectToKnowledge(); | } = useHandleConnectToKnowledge(); | ||||
| // const { pagination } = useGetFilesPagination(); | |||||
| const { | |||||
| showMoveFileModal, | |||||
| moveFileVisible, | |||||
| onMoveFileOk, | |||||
| hideMoveFileModal, | |||||
| moveFileLoading, | |||||
| } = useHandleMoveFile(setSelectedRowKeys); | |||||
| const { pagination, data, searchString, handleInputChange, loading } = | const { pagination, data, searchString, handleInputChange, loading } = | ||||
| useFetchFileList(); | useFetchFileList(); | ||||
| const columns: ColumnsType<IFile> = [ | const columns: ColumnsType<IFile> = [ | ||||
| console.info(record); | console.info(record); | ||||
| }} | }} | ||||
| showRenameModal={showFileRenameModal} | showRenameModal={showFileRenameModal} | ||||
| showMoveFileModal={showMoveFileModal} | |||||
| showConnectToKnowledgeModal={showConnectToKnowledgeModal} | showConnectToKnowledgeModal={showConnectToKnowledgeModal} | ||||
| setSelectedRowKeys={setSelectedRowKeys} | setSelectedRowKeys={setSelectedRowKeys} | ||||
| ></ActionCell> | ></ActionCell> | ||||
| showFolderCreateModal={showFolderCreateModal} | showFolderCreateModal={showFolderCreateModal} | ||||
| showFileUploadModal={showFileUploadModal} | showFileUploadModal={showFileUploadModal} | ||||
| setSelectedRowKeys={setSelectedRowKeys} | setSelectedRowKeys={setSelectedRowKeys} | ||||
| showMoveFileModal={showMoveFileModal} | |||||
| ></FileToolbar> | ></FileToolbar> | ||||
| <Table | <Table | ||||
| dataSource={data?.files} | dataSource={data?.files} | ||||
| onOk={onConnectToKnowledgeOk} | onOk={onConnectToKnowledgeOk} | ||||
| loading={connectToKnowledgeLoading} | loading={connectToKnowledgeLoading} | ||||
| ></ConnectToKnowledgeModal> | ></ConnectToKnowledgeModal> | ||||
| {moveFileVisible && ( | |||||
| <FileMovingModal | |||||
| visible={moveFileVisible} | |||||
| hideModal={hideMoveFileModal} | |||||
| onOk={onMoveFileOk} | |||||
| loading={moveFileLoading} | |||||
| ></FileMovingModal> | |||||
| )} | |||||
| </section> | </section> | ||||
| ); | ); | ||||
| }; | }; |
| import { useFetchPureFileList } from '@/hooks/file-manager-hooks'; | |||||
| import { IFile } from '@/interfaces/database/file-manager'; | |||||
| import type { GetProp, TreeSelectProps } from 'antd'; | |||||
| import { TreeSelect } from 'antd'; | |||||
| import { useCallback, useEffect, useState } from 'react'; | |||||
| type DefaultOptionType = GetProp<TreeSelectProps, 'treeData'>[number]; | |||||
| interface IProps { | |||||
| value?: string; | |||||
| onChange?: (value: string) => void; | |||||
| } | |||||
| const AsyncTreeSelect = ({ value, onChange }: IProps) => { | |||||
| const { fetchList } = useFetchPureFileList(); | |||||
| const [treeData, setTreeData] = useState<Omit<DefaultOptionType, 'label'>[]>( | |||||
| [], | |||||
| ); | |||||
| const onLoadData: TreeSelectProps['loadData'] = useCallback( | |||||
| async ({ id }) => { | |||||
| const ret = await fetchList(id); | |||||
| if (ret.retcode === 0) { | |||||
| setTreeData((tree) => { | |||||
| return tree.concat( | |||||
| ret.data.files | |||||
| .filter((x: IFile) => x.type === 'folder') | |||||
| .map((x: IFile) => ({ | |||||
| id: x.id, | |||||
| pId: x.parent_id, | |||||
| value: x.id, | |||||
| title: x.name, | |||||
| isLeaf: false, | |||||
| })), | |||||
| ); | |||||
| }); | |||||
| } | |||||
| }, | |||||
| [fetchList], | |||||
| ); | |||||
| const handleChange = (newValue: string) => { | |||||
| onChange?.(newValue); | |||||
| }; | |||||
| useEffect(() => { | |||||
| onLoadData?.({ id: '', props: '' }); | |||||
| }, [onLoadData]); | |||||
| return ( | |||||
| <TreeSelect | |||||
| treeDataSimpleMode | |||||
| style={{ width: '100%' }} | |||||
| value={value} | |||||
| dropdownStyle={{ maxHeight: 400, overflow: 'auto' }} | |||||
| placeholder="Please select" | |||||
| onChange={handleChange} | |||||
| loadData={onLoadData} | |||||
| treeData={treeData} | |||||
| /> | |||||
| ); | |||||
| }; | |||||
| export default AsyncTreeSelect; |
| import { IModalManagerChildrenProps } from '@/components/modal-manager'; | |||||
| import { useTranslate } from '@/hooks/common-hooks'; | |||||
| import { Form, Modal } from 'antd'; | |||||
| import AsyncTreeSelect from './async-tree-select'; | |||||
| interface IProps extends Omit<IModalManagerChildrenProps, 'showModal'> { | |||||
| loading: boolean; | |||||
| onOk: (id: string) => void; | |||||
| } | |||||
| const FileMovingModal = ({ visible, hideModal, loading, onOk }: IProps) => { | |||||
| const [form] = Form.useForm(); | |||||
| const { t } = useTranslate('fileManager'); | |||||
| type FieldType = { | |||||
| name?: string; | |||||
| }; | |||||
| const handleOk = async () => { | |||||
| const ret = await form.validateFields(); | |||||
| return onOk(ret.name); | |||||
| }; | |||||
| return ( | |||||
| <Modal | |||||
| title={t('move', { keyPrefix: 'common' })} | |||||
| open={visible} | |||||
| onOk={handleOk} | |||||
| onCancel={hideModal} | |||||
| okButtonProps={{ loading }} | |||||
| confirmLoading={loading} | |||||
| width={600} | |||||
| > | |||||
| <Form | |||||
| name="basic" | |||||
| labelCol={{ span: 6 }} | |||||
| wrapperCol={{ span: 18 }} | |||||
| autoComplete="off" | |||||
| form={form} | |||||
| > | |||||
| <Form.Item<FieldType> | |||||
| label={t('destinationFolder')} | |||||
| name="name" | |||||
| rules={[{ required: true, message: t('pleaseSelect') }]} | |||||
| > | |||||
| <AsyncTreeSelect></AsyncTreeSelect> | |||||
| </Form.Item> | |||||
| </Form> | |||||
| </Modal> | |||||
| ); | |||||
| }; | |||||
| export default FileMovingModal; |
| connectFileToKnowledge, | connectFileToKnowledge, | ||||
| get_document_file, | get_document_file, | ||||
| getFile, | getFile, | ||||
| moveFile, | |||||
| } = api; | } = api; | ||||
| const methods = { | const methods = { | ||||
| method: 'get', | method: 'get', | ||||
| responseType: 'blob', | responseType: 'blob', | ||||
| }, | }, | ||||
| moveFile: { | |||||
| url: moveFile, | |||||
| method: 'post', | |||||
| }, | |||||
| } as const; | } as const; | ||||
| const fileManagerService = registerServer<keyof typeof methods>( | const fileManagerService = registerServer<keyof typeof methods>( |
| createFolder: `${api_host}/file/create`, | createFolder: `${api_host}/file/create`, | ||||
| connectFileToKnowledge: `${api_host}/file2document/convert`, | connectFileToKnowledge: `${api_host}/file2document/convert`, | ||||
| getFile: `${api_host}/file/get`, | getFile: `${api_host}/file/get`, | ||||
| moveFile: `${api_host}/file/mv`, | |||||
| // system | // system | ||||
| getSystemVersion: `${api_host}/system/version`, | getSystemVersion: `${api_host}/system/version`, |