1
0
mirror of https://github.com/haiwen/seahub.git synced 2025-09-06 17:33:18 +00:00

feat: sdoc metadata (#6687)

* feat: sdoc metadata

* feat: update code

---------

Co-authored-by: 杨国璇 <ygx@Hello-word.local>
This commit is contained in:
杨国璇
2024-09-03 10:46:53 +08:00
committed by GitHub
parent 4a2b160c23
commit 5672cf51b6
20 changed files with 286 additions and 84 deletions

View File

@@ -16,7 +16,7 @@
"@gatsbyjs/reach-router": "1.3.9",
"@seafile/react-image-lightbox": "2.0.8",
"@seafile/resumablejs": "1.1.16",
"@seafile/sdoc-editor": "^1.0.66",
"@seafile/sdoc-editor": "^1.0.70",
"@seafile/seafile-calendar": "0.0.12",
"@seafile/seafile-editor": "^1.0.115",
"@seafile/sf-metadata-ui-component": "^0.0.29",
@@ -4783,9 +4783,9 @@
"integrity": "sha512-8rBbmAEuuwOAGHYGCtEzpx+bxAcGS+V30otMmhRe7bPAdh4E57RWgCa8x7pkzHGFlY1t5d+ILz1gojvPVMYQig=="
},
"node_modules/@seafile/sdoc-editor": {
"version": "1.0.66",
"resolved": "https://registry.npmjs.org/@seafile/sdoc-editor/-/sdoc-editor-1.0.66.tgz",
"integrity": "sha512-7v3mynzapsUKNwb5AHadXsR0/RzTmFAJNeJSe5ncjNgB0Ys+q1guM3iLARi96Tw7pEoTutzezUN/vI7qIZNWUw==",
"version": "1.0.70",
"resolved": "https://registry.npmjs.org/@seafile/sdoc-editor/-/sdoc-editor-1.0.70.tgz",
"integrity": "sha512-lPOj07d3uVXIwUdKgdMsAFMPS3eejkGDMSR6V12tjYwahIXy64ApPALiWimF7rG49p9nsjqKRBbygrhrgCeNuA==",
"dependencies": {
"@seafile/print-js": "1.6.6",
"@seafile/react-image-lightbox": "2.0.4",

View File

@@ -11,7 +11,7 @@
"@gatsbyjs/reach-router": "1.3.9",
"@seafile/react-image-lightbox": "2.0.8",
"@seafile/resumablejs": "1.1.16",
"@seafile/sdoc-editor": "^1.0.66",
"@seafile/sdoc-editor": "^1.0.70",
"@seafile/seafile-calendar": "0.0.12",
"@seafile/seafile-editor": "^1.0.115",
"@seafile/sf-metadata-ui-component": "^0.0.29",

View File

@@ -0,0 +1,51 @@
import React, { useMemo } from 'react';
import PropTypes from 'prop-types';
import { Formatter } from '@seafile/sf-metadata-ui-component';
import DetailItem from '../detail-item';
import { CellType } from '../../../metadata/metadata-view/_basic';
import { gettext } from '../../../utils/constants';
import { Utils } from '../../../utils/utils';
import { MetadataDetails, useEnableMetadata } from '../../../metadata';
const FileDetails = ({ repoID, repoInfo, path, direntDetail }) => {
const { enableMetadata } = useEnableMetadata();
const sizeField = useMemo(() => ({ type: 'size', name: gettext('Size') }), []);
const lastModifierField = useMemo(() => ({ type: CellType.LAST_MODIFIER, name: gettext('Last modifier') }), []);
const lastModifiedTimeField = useMemo(() => ({ type: CellType.MTIME, name: gettext('Last modified time') }), []);
return (
<>
<DetailItem field={sizeField} className="sf-metadata-property-detail-formatter">
<Formatter field={sizeField} value={Utils.bytesToSize(direntDetail.size)} />
</DetailItem>
<DetailItem field={lastModifierField} className="sf-metadata-property-detail-formatter">
<Formatter
field={lastModifierField}
value={direntDetail.last_modifier_email}
collaborators={[{
name: direntDetail.last_modifier_name,
contact_email: direntDetail.last_modifier_contact_email,
email: direntDetail.last_modifier_email,
avatar_url: direntDetail.last_modifier_avatar,
}]}
/>
</DetailItem >
<DetailItem field={lastModifiedTimeField} className="sf-metadata-property-detail-formatter">
<Formatter field={lastModifiedTimeField} value={direntDetail.last_modified}/>
</DetailItem>
{window.app.pageOptions.enableMetadataManagement && enableMetadata && (
<MetadataDetails repoID={repoID} filePath={path} repoInfo={repoInfo} direntType="file" />
)}
</>
);
};
FileDetails.propTypes = {
repoID: PropTypes.string,
repoInfo: PropTypes.object,
path: PropTypes.string,
direntDetail: PropTypes.object,
};
export default FileDetails;

View File

@@ -0,0 +1,3 @@
.detail-body .empty-tip-text {
color: #666;
}

View File

@@ -0,0 +1,72 @@
import React, { useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import { seafileAPI } from '../../../utils/seafile-api';
import { Utils } from '../../../utils/utils';
import toaster from '../../toast';
import { Header, Body } from '../detail';
import FileDetails from './file-details';
import { MetadataContext } from '../../../metadata';
import './index.css';
const EmbeddedFileDetails = ({ repoID, repoInfo, dirent, path, onClose, width = 300, className }) => {
const [direntDetail, setDirentDetail] = useState('');
useEffect(() => {
// init context
const context = new MetadataContext();
window.sfMetadataContext = context;
window.sfMetadataContext.init({ repoID, repoInfo });
seafileAPI.getFileInfo(repoID, path).then(res => {
setDirentDetail(res.data);
}).catch(error => {
const errMessage = Utils.getErrorMsg(error);
toaster.danger(errMessage);
});
return () => {
window.sfMetadataContext.destroy();
delete window['sfMetadataContext'];
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const direntName = dirent?.name || '';
const smallIconUrl = Utils.getDirentIcon(dirent);
return (
<div
className={classnames('cur-view-detail', className, {
'cur-view-detail-small': width < 400,
'cur-view-detail-large': width > 400
})}
style={{ width }}
>
<Header title={direntName} icon={smallIconUrl} onClose={onClose} />
<Body>
{dirent && direntDetail && (
<div className="detail-content">
<FileDetails
repoID={repoID}
repoInfo={repoInfo}
path={path}
direntDetail={direntDetail}
/>
</div>
)}
</Body>
</div>
);
};
EmbeddedFileDetails.propTypes = {
repoID: PropTypes.string.isRequired,
dirent: PropTypes.object,
path: PropTypes.string.isRequired,
repoInfo: PropTypes.object.isRequired,
onClose: PropTypes.func.isRequired,
};
export default EmbeddedFileDetails;

View File

@@ -7,7 +7,7 @@ import { Utils } from '../../utils/utils';
import toaster from '../toast';
import FileInfo from './file-info';
import FileToolbar from './file-toolbar';
import FileDetails from '../dirent-detail/file-details';
import FileDetails from '../dirent-detail/old-file-details';
import '../../css/file-view.css';

View File

@@ -0,0 +1,45 @@
import React, { useContext, useEffect, useMemo, useState } from 'react';
import metadataAPI from '../api';
import { Utils } from '../../utils/utils';
import toaster from '../../components/toast';
// This hook provides content related to seahub interaction, such as whether to enable extended attributes
const EnableMetadataContext = React.createContext(null);
export const EnableMetadataProvider = ({ repoID, children }) => {
const enableMetadataManagement = useMemo(() => {
return window.app.pageOptions.enableMetadataManagement;
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [window.app.pageOptions.enableMetadataManagement]);
const [enableMetadata, setEnableExtendedProperties] = useState(false);
useEffect(() => {
if (!enableMetadataManagement) {
return;
}
metadataAPI.getMetadataStatus(repoID).then(res => {
const enableMetadata = res.data.enabled;
setEnableExtendedProperties(enableMetadata);
}).catch(error => {
const errorMsg = Utils.getErrorMsg(error, true);
toaster.danger(errorMsg);
setEnableExtendedProperties(false);
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [repoID, enableMetadataManagement]);
return (
<EnableMetadataContext.Provider value={{ enableMetadata }}>
{children}
</EnableMetadataContext.Provider>
);
};
export const useEnableMetadata = () => {
const context = useContext(EnableMetadataContext);
if (!context) {
throw new Error('\'EnableMetadataContext\' is null');
}
return context;
};

View File

@@ -1,2 +1,3 @@
export { MetadataProvider, useMetadata } from './metadata';
export { EnableMetadataProvider, useEnableMetadata } from './enable-metadata';
export { CollaboratorsProvider, useCollaborators } from './collaborators';

View File

@@ -2,7 +2,8 @@ class User {
constructor(object) {
this.avatar_url = object.avatar_url || '';
this.contact_email = object.contact_email || '';
this.email = object.email || '';
this.username = object.email || object.username || '';
this.email = this.username;
this.name = object.name || '';
this.name_pinyin = object.name_pinyin || '';
this.id = object.id_in_org || '';

View File

@@ -1,81 +1,90 @@
import React, { Fragment } from 'react';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { SimpleEditor } from '@seafile/sdoc-editor';
import ExternalOperations from './external-operations';
import { seafileAPI } from '../../../utils/seafile-api';
import Dirent from '../../../models/dirent';
import { Utils } from '../../../utils/utils';
import { useCollaborators } from '../../../metadata';
import EmbeddedFileDetails from '../../../components/dirent-detail/embedded-file-details';
export default class SdocEditor extends React.Component {
constructor(props) {
super(props);
const { isStarred, isSdocDraft } = window.app.pageOptions;
this.state = {
isStarred: isStarred,
isDraft: isSdocDraft,
direntList: []
};
const SdocEditor = () => {
const [isStarred, setStarted] = useState(window.app.pageOptions.isStarred);
const [isDraft] = useState(window.app.pageOptions.isSdocDraft);
const [direntList, setDirentList] = useState([]);
const [currentDirent, setCurrentDirent] = useState(null);
const { collaborators } = useCollaborators();
const plugins = useMemo(() => {
const { repoID, docPath, docPerm } = window.seafile;
return [
{
name: 'sdoc-info',
icon: 'sdoc-info',
resizable_width: true,
display_type: 'right-panel',
component: ({ onClose, width }) => {
return (<EmbeddedFileDetails repoID={repoID} onClose={onClose} path={docPath} dirent={currentDirent} repoInfo={{ permission: docPerm }} width={width} />);
},
}
];
}, [currentDirent]);
componentDidMount() {
this.onSetFavicon();
this.getDirentList();
}
toggleStar = (isStarred) => {
this.setState({ isStarred: isStarred });
};
onSetFavicon = (suffix) => {
let { docName } = window.seafile;
if (suffix) {
docName = docName + suffix;
}
const fileIcon = Utils.getFileIconUrl(docName);
document.getElementById('favicon').href = fileIcon;
};
onNewNotification = () => {
this.onSetFavicon('_notification');
};
onClearNotification = () => {
this.onSetFavicon();
};
getDirPath = () => {
const dirPath = useMemo(() => {
const { docPath } = window.seafile;
const index = docPath.lastIndexOf('/');
if (index) {
return docPath.slice(0, index);
}
return '/';
}, []);
const onSetFavicon = useCallback((suffix) => {
let { docName } = window.seafile;
if (suffix) {
docName = docName + suffix;
}
const fileIcon = Utils.getFileIconUrl(docName);
document.getElementById('favicon').href = fileIcon;
}, []);
const toggleStar = (isStarred) => {
setStarted(isStarred);
};
getDirentList = () => {
const { repoID } = window.seafile;
const path = this.getDirPath();
seafileAPI.listDir(repoID, path, { 'with_thumbnail': true }).then(res => {
const onNewNotification = useCallback(() => {
onSetFavicon('_notification');
}, [onSetFavicon]);
const onClearNotification = useCallback(() => {
onSetFavicon();
}, [onSetFavicon]);
const getDirentList = () => {
const { repoID, docPath } = window.seafile;
seafileAPI.listDir(repoID, dirPath, { 'with_thumbnail': true }).then(res => {
let direntList = [];
res.data.dirent_list.forEach(item => {
let dirent = new Dirent(item);
const dirent = new Dirent(item);
if (Utils.joinPath(item.parent_dir, item.name) === docPath) {
setCurrentDirent(dirent);
}
direntList.push(dirent);
});
this.setState({
direntList: direntList
});
setDirentList(direntList);
}).catch((err) => {
Utils.getErrorMsg(err, true);
});
};
render() {
useEffect(() => {
onSetFavicon();
getDirentList();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const { repoID, docPath, docName, docPerm } = window.seafile;
const { isStarred, isDraft, direntList } = this.state;
const dirPath = this.getDirPath();
return (
<Fragment>
<SimpleEditor isStarred={isStarred} isDraft={isDraft} />
<>
<SimpleEditor isStarred={isStarred} isDraft={isDraft} plugins={plugins} collaborators={collaborators} showComment={true} />
<ExternalOperations
repoID={repoID}
docPath={docPath}
@@ -84,11 +93,12 @@ export default class SdocEditor extends React.Component {
isStarred={isStarred}
direntList={direntList}
dirPath={dirPath}
toggleStar={this.toggleStar}
onNewNotification={this.onNewNotification}
onClearNotification={this.onClearNotification}
toggleStar={toggleStar}
onNewNotification={onNewNotification}
onClearNotification={onClearNotification}
/>
</Fragment>
</>
);
}
}
};
export default SdocEditor;

View File

@@ -5,6 +5,7 @@ import i18n from './_i18n/i18n-sdoc-editor';
import { Utils } from './utils/utils';
import Loading from './components/loading';
import SdocEditor from './pages/sdoc/sdoc-editor';
import { CollaboratorsProvider, EnableMetadataProvider } from './metadata';
const { serviceURL, avatarURL, siteRoot, lang, mediaUrl, isPro } = window.app.config;
const { username, name } = window.app.userInfo;
@@ -51,7 +52,11 @@ window.seafile = {
ReactDom.render(
<I18nextProvider i18n={ i18n } >
<Suspense fallback={<Loading />}>
<EnableMetadataProvider repoID={repoID} >
<CollaboratorsProvider repoID={repoID}>
<SdocEditor />
</CollaboratorsProvider>
</EnableMetadataProvider>
</Suspense>
</I18nextProvider>,
document.getElementById('wrapper')

View File

@@ -1,11 +1,11 @@
@font-face {
font-family: "sdocfont"; /* Project id 4097705 */
src: url('./sdoc-editor-font/iconfont.eot?t=1722915152576'); /* IE9 */
src: url('./sdoc-editor-font/iconfont.eot?t=1722915152576#iefix') format('embedded-opentype'), /* IE6-IE8 */
url('./sdoc-editor-font/iconfont.woff2?t=1722915152576') format('woff2'),
url('./sdoc-editor-font/iconfont.woff?t=1722915152576') format('woff'),
url('./sdoc-editor-font/iconfont.ttf?t=1722915152576') format('truetype'),
url('./sdoc-editor-font/iconfont.svg?t=1722915152576#sdocfont') format('svg');
src: url('./sdoc-editor-font/iconfont.eot?t=1725002301707'); /* IE9 */
src: url('./sdoc-editor-font/iconfont.eot?t=1725002301707#iefix') format('embedded-opentype'), /* IE6-IE8 */
url('./sdoc-editor-font/iconfont.woff2?t=1725002301707') format('woff2'),
url('./sdoc-editor-font/iconfont.woff?t=1725002301707') format('woff'),
url('./sdoc-editor-font/iconfont.ttf?t=1725002301707') format('truetype'),
url('./sdoc-editor-font/iconfont.svg?t=1725002301707#sdocfont') format('svg');
}
.sdocfont {
@@ -16,6 +16,14 @@
-moz-osx-font-smoothing: grayscale;
}
.sdoc-info:before {
content: "\e677";
}
.sdoc-inline-code:before {
content: "\e676";
}
.sdoc-sort:before {
content: "\e674";
}

View File

@@ -14,6 +14,10 @@
/>
<missing-glyph />
<glyph glyph-name="sdoc-info" unicode="&#58999;" d="M512 896c281.6 0 512-230.4 512-512s-230.4-512-512-512S0 102.4 0 384 230.4 896 512 896z m0-96C281.6 800 96 614.4 96 384s185.6-416 416-416 416 185.6 416 416c0 108.8-44.8 217.6-121.6 294.4S620.8 800 512 800z m32-352c19.2 0 32-12.8 32-32v-256c0-19.2-12.8-32-32-32h-64c-19.2 0-32 12.8-32 32V416c0 19.2 12.8 32 32 32h64z m-19.2 192c9.6 0 16-3.2 22.4-9.6 3.2-3.2 6.4-6.4 9.6-6.4 25.6-25.6 25.6-64 0-89.6-3.2-3.2-6.4-6.4-9.6-6.4-6.4-3.2-16-6.4-22.4-9.6h-25.6c-9.6 0-16 3.2-22.4 9.6-3.2 3.2-6.4 6.4-9.6 6.4-25.6 25.6-25.6 64 0 89.6 3.2 3.2 6.4 6.4 9.6 6.4 6.4 3.2 16 6.4 22.4 9.6h25.6z" horiz-adv-x="1024" />
<glyph glyph-name="sdoc-inline-code" unicode="&#58998;" d="M1024 384l-272-288-67.2 70.4 204.8 217.6-204.8 217.6L752 672l272-288zM0 384l272-288 67.2 70.4L134.4 384l204.8 217.6L272 672 0 384z m451.2-512l-99.2 3.2L579.2 896h99.2l-227.2-1024z" horiz-adv-x="1024" />
<glyph glyph-name="sdoc-sort" unicode="&#58996;" d="M294.4 790.4l201.6-204.8c19.2-19.2 19.2-51.2 0-73.6-19.2-19.2-51.2-19.2-70.4 0L320 620.8v-604.8c0-25.6-22.4-48-48-48S224-9.6 224 16V620.8L118.4 512c-19.2-19.2-48-19.2-67.2-3.2l-3.2 3.2c-19.2 19.2-19.2 51.2 0 73.6l201.6 204.8c12.8 12.8 32 12.8 44.8 0zM752 800c25.6 0 48-22.4 48-48v-604.8l105.6 108.8c19.2 19.2 51.2 19.2 70.4 0 19.2-19.2 19.2-51.2 0-73.6l-201.6-204.8c-12.8-12.8-32-12.8-44.8 0l-201.6 204.8c-19.2 19.2-19.2 51.2 0 73.6l3.2 3.2c19.2 16 48 16 67.2-3.2l105.6-108.8V752c0 25.6 22.4 48 48 48z" horiz-adv-x="1024" />
<glyph glyph-name="sdoc-set-up" unicode="&#58997;" d="M598.52384 809.6l12.8-92.8 6.4-38.4 35.2-16c12.8-6.4 28.8-16 41.6-25.6l32-19.2 35.2 12.8 92.8 35.2 86.4-140.8-76.8-54.4-32-25.6 3.2-38.4v-44.8l-3.2-38.4 32-25.6 76.8-57.6-83.2-140.8-92.8 35.2-35.2 12.8-32-19.2c-12.8-9.6-28.8-19.2-41.6-25.6l-35.2-16-6.4-38.4-12.8-92.8h-172.8l-12.8 92.8-6.4 38.4-35.2 16c-12.8 6.4-28.8 16-41.6 25.6l-32 19.2-35.2-12.8-92.8-35.2-86.4 140.8L160.12384 297.6l32 25.6-3.2 38.4V384v22.4l3.2 38.4-32 25.6-76.8 57.6 83.2 140.8 92.8-35.2 35.2-12.8 32 19.2c12.8 9.6 28.8 19.2 41.6 25.6l35.2 16 6.4 38.4 12.8 92.8h176m12.8 83.2H416.12384c-32 0-57.6-22.4-60.8-51.2l-16-105.6c-19.2-9.6-35.2-19.2-54.4-32L182.52384 748.8c-9.6 3.2-16 3.2-22.4 3.2-22.4 0-41.6-9.6-54.4-28.8L6.52384 560c-12.8-28.8-6.4-60.8 16-80l86.4-67.2c0-9.6-3.2-22.4-3.2-28.8 0-9.6 0-19.2 3.2-28.8L22.52384 288c-22.4-19.2-28.8-51.2-16-76.8l96-163.2c9.6-19.2 32-32 54.4-32 6.4 0 12.8 0 22.4 3.2l102.4 41.6c16-12.8 35.2-22.4 54.4-28.8l16-105.6c3.2-28.8 28.8-51.2 60.8-51.2H608.12384c32 0 57.6 22.4 60.8 51.2l16 105.6c19.2 9.6 35.2 19.2 54.4 28.8l102.4-41.6c6.4-3.2 16-3.2 22.4-3.2 22.4 0 41.6 9.6 54.4 28.8l99.2 166.4c16 25.6 9.6 57.6-16 76.8l-86.4 67.2c0 9.6 3.2 19.2 3.2 28.8s0 22.4-3.2 28.8l86.4 67.2c22.4 19.2 28.8 51.2 16 76.8l-96 163.2c-9.6 19.2-32 32-54.4 32-6.4 0-12.8 0-22.4-3.2l-102.4-41.6c-16 12.8-35.2 22.4-54.4 28.8L672.12384 844.8C665.72384 873.6 640.12384 896 611.32384 896zM512.12384 512c70.4 0 128-57.6 128-128s-57.6-128-128-128-128 57.6-128 128 57.6 128 128 128m0 86.4a214.4 214.4 0 1 1 0-428.8 214.4 214.4 0 0 1 0 428.8z" horiz-adv-x="1026" />

Before

Width:  |  Height:  |  Size: 65 KiB

After

Width:  |  Height:  |  Size: 66 KiB

View File

@@ -42,6 +42,7 @@ window.app.pageOptions = {
canEditFile: {% if can_edit_file %}true{% else %}false{% endif %}, // only for some file types
canDownloadFile: {% if can_download_file %}true{% else %}false{% endif %},
enableWatermark: {% if enable_watermark %}true{% else %}false{% endif %},
enableMetadataManagement: {% if enable_metadata_management %} true {% else %} false {% endif %},
// for {{filetype}} file
{% block extra_data %}

View File

@@ -571,6 +571,7 @@ def view_lib_file(request, repo_id, path):
'share_link_expire_days_max': SHARE_LINK_EXPIRE_DAYS_MAX,
'can_download_file': parse_repo_perm(permission).can_download,
'seafile_collab_server': SEAFILE_COLLAB_SERVER,
'enable_metadata_management': settings.ENABLE_METADATA_MANAGEMENT,
}
# check whether file is starred