mirror of
https://github.com/haiwen/seahub.git
synced 2025-09-05 17:02:47 +00:00
Add tldraw file editor (#7273)
* add tldr file type * add tldraw editor render * optimize code * add save mode * update version * update dependence * update dependence * optimize code * optimize code --------- Co-authored-by: 杨顺强 <978987373@qq.com>
This commit is contained in:
@@ -105,6 +105,7 @@ const excludedChunkNames = [
|
||||
'sharedFileViewMarkdown',
|
||||
'markdownEditor',
|
||||
'plainMarkdownEditor',
|
||||
'tldrawEditor',
|
||||
];
|
||||
|
||||
// This is the production and development configuration.
|
||||
|
@@ -1,6 +1,7 @@
|
||||
const paths = require('./paths');
|
||||
|
||||
const entryFiles = {
|
||||
tldrawEditor: '/tldrawEditor.js',
|
||||
markdownEditor: '/index.js',
|
||||
plainMarkdownEditor: '/pages/plain-markdown-editor/index.js',
|
||||
TCAccept: '/tc-accept.js',
|
||||
|
13552
frontend/package-lock.json
generated
13552
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -15,6 +15,7 @@
|
||||
"@seafile/seafile-calendar": "0.0.28",
|
||||
"@seafile/seafile-editor": "1.0.133",
|
||||
"@seafile/sf-metadata-ui-component": "^0.0.62",
|
||||
"@seafile/stldraw-editor": "0.1.5",
|
||||
"@uiw/codemirror-extensions-langs": "^4.19.4",
|
||||
"@uiw/codemirror-themes": "^4.23.5",
|
||||
"@uiw/react-codemirror": "^4.19.4",
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import React, { Fragment } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { siteRoot, username, enableSeadoc, thumbnailDefaultSize, thumbnailSizeForOriginal, gettext, fileServerRoot } from '../../utils/constants';
|
||||
import { siteRoot, username, enableSeadoc, thumbnailDefaultSize, thumbnailSizeForOriginal, gettext, fileServerRoot, enableTldraw } from '../../utils/constants';
|
||||
import { Utils } from '../../utils/utils';
|
||||
import { seafileAPI } from '../../utils/seafile-api';
|
||||
import URLDecorator from '../../utils/url-decorator';
|
||||
@@ -409,6 +409,9 @@ class DirentGridView extends React.Component {
|
||||
case 'New Word File':
|
||||
this.onCreateFileToggle('.docx');
|
||||
break;
|
||||
case 'New Whiteboard File':
|
||||
this.onCreateFileToggle('.draw');
|
||||
break;
|
||||
case 'New SeaDoc File':
|
||||
this.onCreateFileToggle('.sdoc');
|
||||
break;
|
||||
@@ -724,7 +727,8 @@ class DirentGridView extends React.Component {
|
||||
NEW_EXCEL_FILE,
|
||||
NEW_POWERPOINT_FILE,
|
||||
NEW_WORD_FILE,
|
||||
NEW_SEADOC_FILE
|
||||
NEW_SEADOC_FILE,
|
||||
NEW_TLDRAW_FILE
|
||||
} = TextTranslation;
|
||||
|
||||
let direntsContainerMenuList = [
|
||||
@@ -743,6 +747,10 @@ class DirentGridView extends React.Component {
|
||||
NEW_WORD_FILE
|
||||
);
|
||||
|
||||
if (enableTldraw) {
|
||||
direntsContainerMenuList.push(NEW_TLDRAW_FILE);
|
||||
}
|
||||
|
||||
if (selectedDirentList.length === 0) {
|
||||
if (!hasCustomPermission('create')) return;
|
||||
this.handleContextClick(event, DIRENT_GRID_CONTAINER_MENU_ID, direntsContainerMenuList);
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import React, { Fragment } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classnames from 'classnames';
|
||||
import { siteRoot, gettext, username, enableSeadoc, thumbnailSizeForOriginal, thumbnailDefaultSize, fileServerRoot } from '../../utils/constants';
|
||||
import { siteRoot, gettext, username, enableSeadoc, thumbnailSizeForOriginal, thumbnailDefaultSize, fileServerRoot, enableTldraw } from '../../utils/constants';
|
||||
import { Utils } from '../../utils/utils';
|
||||
import TextTranslation from '../../utils/text-translation';
|
||||
import URLDecorator from '../../utils/url-decorator';
|
||||
@@ -418,7 +418,8 @@ class DirentListView extends React.Component {
|
||||
NEW_EXCEL_FILE,
|
||||
NEW_POWERPOINT_FILE,
|
||||
NEW_WORD_FILE,
|
||||
NEW_SEADOC_FILE
|
||||
NEW_SEADOC_FILE,
|
||||
NEW_TLDRAW_FILE
|
||||
} = TextTranslation;
|
||||
|
||||
const direntsContainerMenuList = [
|
||||
@@ -432,9 +433,13 @@ class DirentListView extends React.Component {
|
||||
NEW_MARKDOWN_FILE,
|
||||
NEW_EXCEL_FILE,
|
||||
NEW_POWERPOINT_FILE,
|
||||
NEW_WORD_FILE
|
||||
NEW_WORD_FILE,
|
||||
);
|
||||
|
||||
if (enableTldraw) {
|
||||
direntsContainerMenuList.push(NEW_TLDRAW_FILE);
|
||||
}
|
||||
|
||||
if (this.props.selectedDirentList.length === 0) {
|
||||
let id = 'dirent-container-menu';
|
||||
|
||||
@@ -509,6 +514,9 @@ class DirentListView extends React.Component {
|
||||
case 'New Word File':
|
||||
this.onCreateFileToggle('.docx');
|
||||
break;
|
||||
case 'New Whiteboard File':
|
||||
this.onCreateFileToggle('.draw');
|
||||
break;
|
||||
case 'New SeaDoc File':
|
||||
this.onCreateFileToggle('.sdoc');
|
||||
break;
|
||||
|
@@ -2,7 +2,7 @@ import React, { Fragment } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Dropdown, DropdownToggle, DropdownMenu, DropdownItem } from 'reactstrap';
|
||||
import { Utils } from '../../utils/utils';
|
||||
import { enableSeadoc, gettext } from '../../utils/constants';
|
||||
import { enableSeadoc, enableTldraw, gettext } from '../../utils/constants';
|
||||
import ModalPortal from '../modal-portal';
|
||||
import CreateFolder from '../../components/dialog/create-folder-dialog';
|
||||
import CreateFile from '../../components/dialog/create-file-dialog';
|
||||
@@ -108,6 +108,13 @@ class DirOperationToolbar extends React.Component {
|
||||
});
|
||||
};
|
||||
|
||||
onCreateTldrawToggle = () => {
|
||||
this.setState({
|
||||
isCreateFileDialogShow: !this.state.isCreateFileDialogShow,
|
||||
fileType: '.draw'
|
||||
});
|
||||
};
|
||||
|
||||
onCreateSeaDocToggle = () => {
|
||||
this.setState({
|
||||
isCreateFileDialogShow: !this.state.isCreateFileDialogShow,
|
||||
@@ -246,8 +253,11 @@ class DirOperationToolbar extends React.Component {
|
||||
{ 'text': gettext('New Markdown File'), 'onClick': this.onCreateMarkdownToggle },
|
||||
{ 'text': gettext('New Excel File'), 'onClick': this.onCreateExcelToggle },
|
||||
{ 'text': gettext('New PowerPoint File'), 'onClick': this.onCreatePPTToggle },
|
||||
{ 'text': gettext('New Word File'), 'onClick': this.onCreateWordToggle }
|
||||
{ 'text': gettext('New Word File'), 'onClick': this.onCreateWordToggle },
|
||||
);
|
||||
if (enableTldraw) {
|
||||
newSubOpList.push({ 'text': gettext('New Whiteboard File'), 'onClick': this.onCreateTldrawToggle });
|
||||
}
|
||||
opList.push({
|
||||
'icon': 'new',
|
||||
'text': gettext('New'),
|
||||
|
1
frontend/src/pages/tldraw-editor/constants/index.js
Normal file
1
frontend/src/pages/tldraw-editor/constants/index.js
Normal file
@@ -0,0 +1 @@
|
||||
export const SAVE_INTERVAL_TIME = 3 * 60 * 1000;
|
26
frontend/src/pages/tldraw-editor/editor-api.js
Normal file
26
frontend/src/pages/tldraw-editor/editor-api.js
Normal file
@@ -0,0 +1,26 @@
|
||||
import { seafileAPI } from '../../utils/seafile-api';
|
||||
import { Utils } from '../../utils/utils';
|
||||
|
||||
const { repoID, filePath, fileName } = window.app.pageOptions;
|
||||
let dirPath = Utils.getDirName(filePath);
|
||||
|
||||
class EditorApi {
|
||||
|
||||
saveContent(content) {
|
||||
return seafileAPI.getUpdateLink(repoID, dirPath).then((res) => {
|
||||
const uploadLink = res.data;
|
||||
return seafileAPI.updateFile(uploadLink, filePath, fileName, content);
|
||||
});
|
||||
}
|
||||
|
||||
getFileContent = () => {
|
||||
return seafileAPI.getFileDownloadLink(repoID, filePath).then(res => {
|
||||
const downLoadUrl = res.data;
|
||||
return seafileAPI.getFileContent(downLoadUrl);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
const editorApi = new EditorApi();
|
||||
|
||||
export default editorApi;
|
89
frontend/src/pages/tldraw-editor/index.js
Normal file
89
frontend/src/pages/tldraw-editor/index.js
Normal file
@@ -0,0 +1,89 @@
|
||||
import React, { useCallback, useState, useEffect, useRef } from 'react';
|
||||
import { SimpleEditor } from '@seafile/stldraw-editor';
|
||||
import isHotkey from 'is-hotkey';
|
||||
import editorApi from './editor-api';
|
||||
import { gettext } from '../../utils/constants';
|
||||
import toaster from '../../components/toast';
|
||||
import { SAVE_INTERVAL_TIME } from './constants';
|
||||
|
||||
const TldrawEditor = () => {
|
||||
const editorRef = useRef(null);
|
||||
const isChangedRef = useRef(false);
|
||||
const [fileContent, setFileContent] = useState({});
|
||||
const [isFetching, setIsFetching] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
editorApi.getFileContent().then(res => {
|
||||
setFileContent(res.data);
|
||||
setIsFetching(false);
|
||||
});
|
||||
}, []);
|
||||
|
||||
const saveDocument = useCallback(async () => {
|
||||
if (isChangedRef.current) {
|
||||
try {
|
||||
await editorApi.saveContent(JSON.stringify(editorRef.current));
|
||||
isChangedRef.current = false;
|
||||
toaster.success(gettext('Successfully saved'), { duration: 2 });
|
||||
} catch {
|
||||
toaster.danger(gettext('Failed to save'), { duration: 2 });
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const handleHotkeySave = (event) => {
|
||||
if (isHotkey('mod+s')(event)) {
|
||||
event.preventDefault();
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('keydown', handleHotkeySave);
|
||||
return () => {
|
||||
document.removeEventListener('keydown', handleHotkeySave);
|
||||
};
|
||||
}, [saveDocument]);
|
||||
|
||||
useEffect(() => {
|
||||
const saveInterval = setInterval(() => {
|
||||
if (isChangedRef.current) {
|
||||
editorApi.saveContent(JSON.stringify(editorRef.current)).then(res => {
|
||||
isChangedRef.current = false;
|
||||
});
|
||||
}
|
||||
}, SAVE_INTERVAL_TIME);
|
||||
|
||||
const handleBeforeUnload = (event) => {
|
||||
if (isChangedRef.current) {
|
||||
event.preventDefault();
|
||||
event.returnValue = '';
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('beforeunload', handleBeforeUnload);
|
||||
return () => {
|
||||
clearInterval(saveInterval);
|
||||
window.removeEventListener('beforeunload', handleBeforeUnload);
|
||||
};
|
||||
}, [saveDocument]);
|
||||
|
||||
const onContentChanged = useCallback((docContent) => {
|
||||
editorRef.current = docContent;
|
||||
isChangedRef.current = true;
|
||||
}, []);
|
||||
|
||||
const onSave = useCallback(() => {
|
||||
saveDocument();
|
||||
}, [saveDocument]);
|
||||
|
||||
return (
|
||||
<SimpleEditor
|
||||
isFetching={isFetching}
|
||||
document={fileContent}
|
||||
onContentChanged={onContentChanged}
|
||||
onSave={onSave}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default TldrawEditor;
|
11
frontend/src/tldrawEditor.js
Normal file
11
frontend/src/tldrawEditor.js
Normal file
@@ -0,0 +1,11 @@
|
||||
import React, { Suspense } from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import TldrawEditor from './pages/tldraw-editor';
|
||||
import Loading from './components/loading';
|
||||
|
||||
ReactDOM.render(
|
||||
<Suspense fallback={<Loading />}>
|
||||
<TldrawEditor />
|
||||
</Suspense>,
|
||||
document.getElementById('wrapper')
|
||||
);
|
@@ -90,6 +90,7 @@ export const ocmRemoteServers = window.app.pageOptions.ocmRemoteServers;
|
||||
export const enableOCMViaWebdav = window.app.pageOptions.enableOCMViaWebdav;
|
||||
export const enableSSOToThirdpartWebsite = window.app.pageOptions.enableSSOToThirdpartWebsite;
|
||||
export const enableSeadoc = window.app.pageOptions.enableSeadoc;
|
||||
export const enableTldraw = window.app.pageOptions.enableTldraw;
|
||||
export const enableMultipleOfficeSuite = window.app.pageOptions.enableMultipleOfficeSuite;
|
||||
|
||||
export const curNoteMsg = window.app.pageOptions.curNoteMsg;
|
||||
|
@@ -27,6 +27,10 @@ const TextTranslation = {
|
||||
key: 'New Word File',
|
||||
value: gettext('New Word File')
|
||||
},
|
||||
NEW_TLDRAW_FILE: {
|
||||
key: 'New Whiteboard File',
|
||||
value: gettext('New Whiteboard File')
|
||||
},
|
||||
NEW_SEADOC_FILE: {
|
||||
key: 'New SeaDoc File',
|
||||
value: gettext('New SeaDoc File')
|
||||
|
@@ -25,7 +25,7 @@ from seahub.settings import SEAFILE_VERSION, SITE_DESCRIPTION, \
|
||||
ENABLE_SEAFILE_DOCS, LOGIN_BG_IMAGE_PATH, THUMBNAIL_DEFAULT_SIZE, \
|
||||
CUSTOM_LOGIN_BG_PATH, ENABLE_SHARE_LINK_REPORT_ABUSE, \
|
||||
PRIVACY_POLICY_LINK, TERMS_OF_SERVICE_LINK, ENABLE_SEADOC, THUMBNAIL_SIZE_FOR_GRID, \
|
||||
FILE_SERVER_ROOT
|
||||
FILE_SERVER_ROOT, ENABLE_TLDRAW
|
||||
|
||||
from seahub.organizations.models import OrgAdminSettings
|
||||
from seahub.organizations.settings import ORG_ENABLE_ADMIN_CUSTOM_LOGO
|
||||
@@ -173,7 +173,8 @@ def base(request):
|
||||
'side_nav_footer_custom_html': SIDE_NAV_FOOTER_CUSTOM_HTML,
|
||||
'about_dialog_custom_html': ABOUT_DIALOG_CUSTOM_HTML,
|
||||
'enable_repo_auto_del': ENABLE_REPO_AUTO_DEL,
|
||||
'enable_seadoc': ENABLE_SEADOC
|
||||
'enable_seadoc': ENABLE_SEADOC,
|
||||
'enable_tldraw': ENABLE_TLDRAW,
|
||||
}
|
||||
|
||||
if request.user.is_staff:
|
||||
|
@@ -64,6 +64,7 @@ FILEEXT_ICON_MAP = {
|
||||
'docx': 'word.png',
|
||||
'odt': 'word.png',
|
||||
'fodt': 'word.png',
|
||||
'tldr': 'word.png',
|
||||
|
||||
'ppt': 'ppt.png',
|
||||
'pptx': 'ppt.png',
|
||||
|
@@ -917,6 +917,11 @@ SEADOC_PRIVATE_KEY = ''
|
||||
SEADOC_SERVER_URL = 'http://127.0.0.1:7070'
|
||||
FILE_CONVERTER_SERVER_URL = 'http://127.0.0.1:8888'
|
||||
|
||||
##########################
|
||||
# Settings for tldraw #
|
||||
##########################
|
||||
|
||||
ENABLE_TLDRAW = False
|
||||
|
||||
############################
|
||||
# Settings for Seahub Priv #
|
||||
|
@@ -154,6 +154,7 @@
|
||||
enableOnlyoffice: {% if enableOnlyoffice %} true {% else %} false {% endif %},
|
||||
onlyofficeConverterExtensions: {% if onlyofficeConverterExtensions %} {{onlyofficeConverterExtensions|safe}} {% else %} null {% endif %},
|
||||
enableSeadoc: {% if enable_seadoc %} true {% else %} false {% endif %},
|
||||
enableTldraw: {% if enable_tldraw %} true {% else %} false {% endif %},
|
||||
isOrgContext: {% if org is not None %} true {% else %} false {% endif %},
|
||||
enableMetadataManagement: {% if enable_metadata_management %} true {% else %} false {% endif %},
|
||||
enableFileTags: {% if enable_file_tags %} true {% else %} false {% endif %},
|
||||
|
19
seahub/templates/tldraw_file_view_react.html
Normal file
19
seahub/templates/tldraw_file_view_react.html
Normal file
@@ -0,0 +1,19 @@
|
||||
{% extends 'file_view_react.html' %}
|
||||
{% load render_bundle from webpack_loader %}
|
||||
|
||||
|
||||
{% block extra_style %}
|
||||
{% render_bundle 'tldrawEditor' 'css' %}
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_data %}
|
||||
docPath: '{{ path|escapejs }}',
|
||||
docName: '{{ filename|escapejs }}',
|
||||
docUuid: '{{ file_uuid }}',
|
||||
lang: '{{ lang }}',
|
||||
rawPath: '{{ raw_path|escapejs }}',
|
||||
{% endblock %}
|
||||
|
||||
{% block render_bundle %}
|
||||
{% render_bundle 'tldrawEditor' 'js' %}
|
||||
{% endblock %}
|
@@ -143,6 +143,7 @@ PREVIEW_FILEEXT = {
|
||||
#'3D': ('stl', 'obj'),
|
||||
XMIND: ('xmind',),
|
||||
SEADOC: ('sdoc',),
|
||||
TLDRAW: ('draw',),
|
||||
}
|
||||
|
||||
def get_non_sdoc_file_exts():
|
||||
|
@@ -10,6 +10,7 @@ AUDIO = 'Audio'
|
||||
SPREADSHEET = 'SpreadSheet'
|
||||
XMIND = 'XMind'
|
||||
SEADOC = 'SDoc'
|
||||
TLDRAW = 'Tldraw'
|
||||
|
||||
|
||||
MARKDOWN_SUPPORT_CONVERT_TYPES = ['sdoc']
|
||||
|
@@ -61,7 +61,7 @@ from seahub.utils import render_error, is_org_context, \
|
||||
from seahub.utils.ip import get_remote_ip
|
||||
from seahub.utils.file_types import (IMAGE, PDF, SVG,
|
||||
DOCUMENT, SPREADSHEET, AUDIO,
|
||||
MARKDOWN, TEXT, VIDEO, XMIND, SEADOC)
|
||||
MARKDOWN, TEXT, VIDEO, XMIND, SEADOC, TLDRAW)
|
||||
from seahub.utils.star import is_file_starred
|
||||
from seahub.utils.http import json_response, \
|
||||
BadRequestException
|
||||
@@ -816,6 +816,34 @@ def view_lib_file(request, repo_id, path):
|
||||
return_dict['share_link_expire_days_max'] = SHARE_LINK_EXPIRE_DAYS_MAX
|
||||
return_dict['raw_path'] = raw_path
|
||||
|
||||
can_edit_file = True
|
||||
if parse_repo_perm(permission).can_edit_on_web is False:
|
||||
can_edit_file = False
|
||||
elif is_locked and not locked_by_me:
|
||||
can_edit_file = False
|
||||
return_dict['can_edit_file'] = can_edit_file
|
||||
|
||||
return render(request, template, return_dict)
|
||||
|
||||
if filetype == TLDRAW:
|
||||
|
||||
mode = request.GET.get('mode', '')
|
||||
if mode not in ('edit', 'viewer', 'plain'):
|
||||
mode = 'viewer'
|
||||
if mode == 'plain':
|
||||
template = 'plain_' + template
|
||||
|
||||
return_dict['protocol'] = request.is_secure() and 'https' or 'http'
|
||||
return_dict['domain'] = get_current_site(request).domain
|
||||
return_dict['serviceUrl'] = get_service_url().rstrip('/')
|
||||
return_dict['language_code'] = get_language()
|
||||
return_dict['mode'] = mode
|
||||
return_dict['share_link_expire_days_Default'] = SHARE_LINK_EXPIRE_DAYS_DEFAULT
|
||||
return_dict['share_link_expire_days_min'] = SHARE_LINK_EXPIRE_DAYS_MIN
|
||||
return_dict['share_link_expire_days_max'] = SHARE_LINK_EXPIRE_DAYS_MAX
|
||||
return_dict['raw_path'] = raw_path
|
||||
|
||||
|
||||
can_edit_file = True
|
||||
if parse_repo_perm(permission).can_edit_on_web is False:
|
||||
can_edit_file = False
|
||||
|
Reference in New Issue
Block a user