1
0
mirror of https://github.com/haiwen/seahub.git synced 2025-04-27 11:01:14 +00:00

Add excl draw module 2 (#7658)

* init exceldraw module

* excalidraw demo

* i18n fixed bug

* exceldraw change to excalidraw

* lang setting

---------

Co-authored-by: 杨顺强 <978987373@qq.com>
Co-authored-by: First <first@FirstdeMacBook-Pro.local>
This commit is contained in:
zhichaona 2025-03-24 14:11:58 +08:00 committed by GitHub
parent ef9be3fed1
commit ff7fd0f0d5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
23 changed files with 1597 additions and 32 deletions

View File

@ -627,6 +627,12 @@ module.exports = function (webpackEnv) {
// Make sure to add the new loader(s) before the "file" loader.
],
},
{
test: /\.m?js$/,
resolve: {
fullySpecified: false
}
}
].filter(Boolean),
},
plugins: [

View File

@ -2,6 +2,7 @@ const paths = require('./paths');
const entryFiles = {
tldrawEditor: '/tldrawEditor.js',
excalidrawEditor: '/excalidraw-editor.js',
markdownEditor: '/index.js',
plainMarkdownEditor: '/pages/plain-markdown-editor/index.js',
TCAccept: '/tc-accept.js',

File diff suppressed because it is too large Load Diff

View File

@ -8,6 +8,7 @@
"@codemirror/view": "^6.34.1",
"@emoji-mart/data": "^1.2.1",
"@emoji-mart/react": "^1.1.1",
"@excalidraw/excalidraw": "^0.18.0",
"@gatsbyjs/reach-router": "2.0.1",
"@seafile/react-image-lightbox": "4.0.2",
"@seafile/resumablejs": "1.1.16",

View File

@ -1,6 +1,6 @@
import React, { Fragment } from 'react';
import PropTypes from 'prop-types';
import { siteRoot, username, enableSeadoc, thumbnailDefaultSize, thumbnailSizeForOriginal, gettext, fileServerRoot, enableWhiteboard, useGoFileserver } from '../../utils/constants';
import { siteRoot, username, enableSeadoc, thumbnailDefaultSize, thumbnailSizeForOriginal, gettext, fileServerRoot, enableWhiteboard, useGoFileserver, enableExcalidraw } from '../../utils/constants';
import { Utils } from '../../utils/utils';
import { seafileAPI } from '../../utils/seafile-api';
import URLDecorator from '../../utils/url-decorator';
@ -412,6 +412,9 @@ class DirentGridView extends React.Component {
case 'New Whiteboard File':
this.onCreateFileToggle('.draw');
break;
case 'New Excalidraw File':
this.onCreateFileToggle('.exdraw');
break;
case 'New SeaDoc File':
this.onCreateFileToggle('.sdoc');
break;
@ -734,13 +737,15 @@ class DirentGridView extends React.Component {
if (!['admin', 'rw'].includes(this.props.userPerm)) return;
const {
NEW_FOLDER, NEW_FILE,
NEW_FOLDER,
NEW_FILE,
NEW_MARKDOWN_FILE,
NEW_EXCEL_FILE,
NEW_POWERPOINT_FILE,
NEW_WORD_FILE,
NEW_SEADOC_FILE,
NEW_TLDRAW_FILE
NEW_TLDRAW_FILE,
NEW_EXCALIDRAW_FILE
} = TextTranslation;
let direntsContainerMenuList = [
@ -763,6 +768,10 @@ class DirentGridView extends React.Component {
direntsContainerMenuList.push(NEW_TLDRAW_FILE);
}
if (enableExcalidraw) {
direntsContainerMenuList.push(NEW_EXCALIDRAW_FILE);
}
if (selectedDirentList.length === 0) {
if (!hasCustomPermission('create')) return;
this.handleContextClick(event, DIRENT_GRID_CONTAINER_MENU_ID, direntsContainerMenuList);

View File

@ -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, enableWhiteboard, useGoFileserver } from '../../utils/constants';
import { siteRoot, gettext, username, enableSeadoc, thumbnailSizeForOriginal, thumbnailDefaultSize, fileServerRoot, enableWhiteboard, useGoFileserver, enableExcalidraw } from '../../utils/constants';
import { Utils } from '../../utils/utils';
import TextTranslation from '../../utils/text-translation';
import URLDecorator from '../../utils/url-decorator';
@ -431,7 +431,8 @@ class DirentListView extends React.Component {
NEW_POWERPOINT_FILE,
NEW_WORD_FILE,
NEW_SEADOC_FILE,
NEW_TLDRAW_FILE
NEW_TLDRAW_FILE,
NEW_EXCALIDRAW_FILE,
} = TextTranslation;
const direntsContainerMenuList = [
@ -452,6 +453,10 @@ class DirentListView extends React.Component {
direntsContainerMenuList.push(NEW_TLDRAW_FILE);
}
if (enableExcalidraw) {
direntsContainerMenuList.push(NEW_EXCALIDRAW_FILE);
}
if (this.props.selectedDirentList.length === 0) {
let id = 'dirent-container-menu';
@ -529,6 +534,9 @@ class DirentListView extends React.Component {
case 'New Whiteboard File':
this.onCreateFileToggle('.draw');
break;
case 'New Excalidraw File':
this.onCreateFileToggle('.exdraw');
break;
case 'New SeaDoc File':
this.onCreateFileToggle('.sdoc');
break;

View File

@ -1,6 +1,6 @@
import React from 'react';
import PropTypes from 'prop-types';
import { enableSeadoc, gettext, enableWhiteboard } from '../../utils/constants';
import { enableSeadoc, gettext, enableWhiteboard, enableExcalidraw } from '../../utils/constants';
import Loading from '../loading';
import ModalPortal from '../modal-portal';
import CreateFile from '../../components/dialog/create-file-dialog';
@ -74,7 +74,8 @@ class DirentNoneView extends React.Component {
NEW_POWERPOINT_FILE,
NEW_WORD_FILE,
NEW_SEADOC_FILE,
NEW_TLDRAW_FILE
NEW_TLDRAW_FILE,
NEW_EXCALIDRAW_FILE
} = TextTranslation;
const direntsContainerMenuList = [
NEW_FOLDER, NEW_FILE, 'Divider',
@ -92,6 +93,9 @@ class DirentNoneView extends React.Component {
if (enableWhiteboard) {
direntsContainerMenuList.push(NEW_TLDRAW_FILE);
}
if (enableExcalidraw) {
direntsContainerMenuList.push(NEW_EXCALIDRAW_FILE);
}
let id = 'dirent-container-menu';
if (isCustomPermission) {
const { create: canCreate } = customPermission.permission;

View File

@ -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, enableWhiteboard, gettext } from '../../utils/constants';
import { enableExcalidraw, enableSeadoc, enableWhiteboard, 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';
@ -115,6 +115,13 @@ class DirOperationToolbar extends React.Component {
});
};
onCreateExcalidrawToggle = () => {
this.setState({
isCreateFileDialogShow: !this.state.isCreateFileDialogShow,
fileType: '.exdraw'
});
};
onCreateSeaDocToggle = () => {
this.setState({
isCreateFileDialogShow: !this.state.isCreateFileDialogShow,
@ -266,6 +273,9 @@ class DirOperationToolbar extends React.Component {
if (enableWhiteboard) {
newSubOpList.push({ 'text': gettext('New Whiteboard File'), 'onClick': this.onCreateTldrawToggle });
}
if (enableExcalidraw) {
newSubOpList.push({ 'text': gettext('New Excalidraw File'), 'onClick': this.onCreateExcalidrawToggle });
}
opList.push({
'icon': 'new',
'text': gettext('New'),

View File

@ -0,0 +1,11 @@
import React, { Suspense } from 'react';
import { createRoot } from 'react-dom/client';
import Loading from './components/loading';
import ExcaliEditor from './pages/excalidraw-editor';
const root = createRoot(document.getElementById('wrapper'));
root.render(
<Suspense fallback={<Loading />}>
<ExcaliEditor />
</Suspense>
);

View File

@ -0,0 +1,15 @@
export const SAVE_INTERVAL_TIME = 3 * 60 * 1000;
export const langList = {
'zh-cn': 'zh-CN',
'en': 'en',
'zh-tw': 'zh-TW',
'ru': 'ru-RU',
'it': 'it-IT',
'fr': 'fr-FR',
'es-ms': 'en',
'es-ar': 'en',
'es': 'es-ES',
'de': 'de-DE',
'cs': 'cs-CZ',
};

View 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;

View File

@ -0,0 +1,8 @@
.dropdown-menu {
display: block !important;
}
.excalidraw .dropdown-menu {
--bs-dropdown-padding-x: auto;
border: unset;
}

View File

@ -0,0 +1,86 @@
import React, { useCallback, useState, useEffect, useRef } from 'react';
import SimpleEditor from './simple-editor';
import editorApi from './editor-api';
import isHotkey from 'is-hotkey';
import { gettext } from '../../utils/constants';
import toaster from '../../components/toast';
import { SAVE_INTERVAL_TIME } from './constants';
import './index.css';
const ExcaliEditor = () => {
const [fileContent, setFileContent] = useState(null);
const editorRef = useRef(null);
const isChangedRef = useRef(false);
const [isFetching, setIsFetching] = useState(true);
useEffect(() => {
editorApi.getFileContent().then(res => {
if (res.data?.appState?.collaborators && !Array.isArray(res.data.appState.collaborators)) {
// collaborators.forEach is not a function
res.data['appState']['collaborators'] = [];
}
setFileContent(res.data);
setIsFetching(false);
});
}, []);
const saveSceneContent = 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, true);
return () => {
document.removeEventListener('keydown', handleHotkeySave, true);
};
}, [saveSceneContent]);
useEffect(() => {
const saveInterval = setInterval(() => {
if (isChangedRef.current) {
editorApi.saveContent(JSON.stringify(editorRef.current)).then(res => {
isChangedRef.current = false;
});
}
}, SAVE_INTERVAL_TIME);
return () => {
clearInterval(saveInterval);
};
}, [saveSceneContent]);
const onSaveContent = useCallback(() => {
saveSceneContent();
}, [saveSceneContent]);
const onChangeContent = useCallback((elements) => {
editorRef.current = { elements };
isChangedRef.current = true;
}, []);
return (
<SimpleEditor
isFetching={isFetching}
sceneContent={fileContent}
onSaveContent={onSaveContent}
onChangeContent={onChangeContent}
/>
);
};
export default ExcaliEditor;

View File

@ -0,0 +1,75 @@
import React, { useEffect, useState } from 'react';
import { Excalidraw, MainMenu } from '@excalidraw/excalidraw';
import isHotkey from 'is-hotkey';
import CodeMirrorLoading from '../../components/code-mirror-loading';
import { langList } from './constants';
import '@excalidraw/excalidraw/index.css';
const SimpleEditor = ({
sceneContent = null,
onChangeContent,
onSaveContent,
isFetching
}) => {
const [excalidrawAPI, setExcalidrawAPI] = useState(null);
const UIOptions = {
canvasActions: {
saveToActiveFile: false,
LoadScene: false
},
tools: { image: false },
};
const handleChange = () => {
const elements = excalidrawAPI.getSceneElements();
onChangeContent(elements);
};
useEffect(() => {
const handleHotkeySave = (event) => {
if (isHotkey('mod+s', event)) {
event.preventDefault();
onSaveContent(excalidrawAPI.getSceneElements());
}
};
document.addEventListener('keydown', handleHotkeySave, true);
return () => {
document.removeEventListener('keydown', handleHotkeySave, true);
};
}, [excalidrawAPI, onSaveContent]);
if (isFetching) {
return (
<div className='excali-container'>
<CodeMirrorLoading />
</div>
);
}
return (
<>
<div className='excali-container' style={{ height: '100vh', width: '100vw' }}>
<Excalidraw
initialData={sceneContent}
excalidrawAPI={(api) => setExcalidrawAPI(api)}
onChange={handleChange}
UIOptions={UIOptions}
langCode={langList[window.app.config.lang] || 'en'}
>
<MainMenu>
<MainMenu.DefaultItems.Export />
<MainMenu.DefaultItems.SaveAsImage />
<MainMenu.DefaultItems.Help />
<MainMenu.DefaultItems.ClearCanvas />
<MainMenu.DefaultItems.ToggleTheme />
<MainMenu.DefaultItems.ChangeCanvasBackground />
</MainMenu>
</Excalidraw>
</div>
</>
);
};
export default SimpleEditor;

View File

@ -99,6 +99,7 @@ export const enableSSOToThirdpartWebsite = window.app.pageOptions.enableSSOToThi
export const enableSeadoc = window.app.pageOptions.enableSeadoc;
export const enableSeafileAI = window.app.pageOptions.enableSeafileAI;
export const enableWhiteboard = window.app.pageOptions.enableWhiteboard;
export const enableExcalidraw = window.app.pageOptions.enableExcalidraw;
export const enableMultipleOfficeSuite = window.app.pageOptions.enableMultipleOfficeSuite;
export const curNoteMsg = window.app.pageOptions.curNoteMsg;

View File

@ -31,6 +31,10 @@ const TextTranslation = {
key: 'New Whiteboard File',
value: gettext('New Whiteboard File')
},
NEW_EXCALIDRAW_FILE: {
key: 'New Excalidraw File',
value: gettext('New Excalidraw File')
},
NEW_SEADOC_FILE: {
key: 'New SeaDoc File',
value: gettext('New SeaDoc File')

View File

@ -25,7 +25,7 @@ from seahub.settings import SEAFILE_VERSION, SITE_DESCRIPTION, \
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, ENABLE_WHITEBOARD, ENABLE_SEAFILE_AI
FILE_SERVER_ROOT, ENABLE_WHITEBOARD, ENABLE_SEAFILE_AI, ENABLE_EXCALIDRAW
from seahub.organizations.models import OrgAdminSettings
from seahub.organizations.settings import ORG_ENABLE_ADMIN_CUSTOM_LOGO
@ -181,6 +181,7 @@ def base(request):
'enable_seadoc': ENABLE_SEADOC,
'enable_seafile_ai': ENABLE_SEAFILE_AI,
'enable_whiteboard': ENABLE_WHITEBOARD,
'enable_excalidraw': ENABLE_EXCALIDRAW,
}
if request.user.is_staff:

View File

@ -961,6 +961,12 @@ FILE_CONVERTER_SERVER_URL = 'http://127.0.0.1:8888'
ENABLE_WHITEBOARD = False
##########################
# Settings for excalidraw #
##########################
ENABLE_EXCALIDRAW = False
############################
# Settings for Seahub Priv #
############################

View File

@ -159,6 +159,7 @@
officeWebAppEditFileExtension: {% if office_web_app_edit_file_extension %} {{office_web_app_edit_file_extension|safe}} {% else %} [] {% endif %},
enableSeadoc: {% if enable_seadoc %} true {% else %} false {% endif %},
enableWhiteboard: {% if enable_whiteboard %} true {% else %} false {% endif %},
enableExcalidraw: {% if enable_excalidraw %} true {% else %} false {% endif %},
isOrgContext: {% if org is not None %} true {% else %} false {% endif %},
enableMetadataManagement: {% if enable_metadata_management %} true {% else %} false {% endif %},
isMultiTenacy: {% if multi_tenancy %} true {% else %} false {% endif %},

View File

@ -0,0 +1,19 @@
{% extends 'file_view_react.html' %}
{% load render_bundle from webpack_loader %}
{% block extra_style %}
{% render_bundle 'excalidrawEditor' '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 'excalidrawEditor' 'js' %}
{% endblock %}

View File

@ -144,6 +144,7 @@ PREVIEW_FILEEXT = {
XMIND: ('xmind',),
SEADOC: ('sdoc',),
TLDRAW: ('draw',),
EXCALIDRAW: ('exdraw',),
}
def get_non_sdoc_file_exts():

View File

@ -11,6 +11,7 @@ SPREADSHEET = 'SpreadSheet'
XMIND = 'XMind'
SEADOC = 'SDoc'
TLDRAW = 'Tldraw'
EXCALIDRAW = 'Excalidraw'
MARKDOWN_SUPPORT_CONVERT_TYPES = ['sdoc']

View File

@ -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, TLDRAW)
MARKDOWN, TEXT, VIDEO, XMIND, SEADOC, TLDRAW, EXCALIDRAW)
from seahub.utils.timeutils import timestamp_to_isoformat_timestr
from seahub.utils.star import is_file_starred
from seahub.utils.http import json_response, \
@ -850,6 +850,27 @@ def view_lib_file(request, repo_id, path):
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 == EXCALIDRAW:
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['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