diff --git a/frontend/src/assets/icons/set-up.svg b/frontend/src/assets/icons/set-up.svg index 95d4c3e52f2..f9bdefe0aa6 100644 --- a/frontend/src/assets/icons/set-up.svg +++ b/frontend/src/assets/icons/set-up.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/frontend/src/components/dirent-detail/detail/header/index.css b/frontend/src/components/dirent-detail/detail/header/index.css index 16a28182a93..4d61a1b4fa1 100644 --- a/frontend/src/components/dirent-detail/detail/header/index.css +++ b/frontend/src/components/dirent-detail/detail/header/index.css @@ -9,28 +9,11 @@ padding: 8px 16px; } -.detail-header .detail-title { - display: flex; - flex: 1; - align-items: center; - width: 0; /* prevent strut flex layout */ -} - -.detail-header .detail-title .detail-header-icon-container { - height: 32px; - width: 32px; +.detail-header .detail-control-container { + flex-shrink: 0; display: flex; align-items: center; justify-content: center; - flex-shrink: 0; -} - -.detail-header .detail-title .name { - margin: 0 0.5rem 0 6px; - line-height: 1.5rem; - vertical-align: middle; - font-size: 1rem; - color: #212529; } .detail-header .detail-control { diff --git a/frontend/src/components/dirent-detail/detail/header/index.js b/frontend/src/components/dirent-detail/detail/header/index.js index c152f5c842b..1ff83603a89 100644 --- a/frontend/src/components/dirent-detail/detail/header/index.js +++ b/frontend/src/components/dirent-detail/detail/header/index.js @@ -1,21 +1,20 @@ import React from 'react'; import PropTypes from 'prop-types'; import Icon from '../../../icon'; +import Title from './title'; import './index.css'; -const Header = ({ title, icon, iconSize = 32, onClose, component = {} }) => { +const Header = ({ title, icon, iconSize = 32, onClose, children, component = {} }) => { const { closeIcon } = component; return (
-
-
- + + <div className="detail-control-container"> + {children} + <div className="detail-control" onClick={onClose}> + {closeIcon ? closeIcon : <Icon symbol="close" className="detail-control-close" />} </div> - <span className="name ellipsis" title={title}>{title}</span> - </div> - <div className="detail-control" onClick={onClose}> - {closeIcon ? closeIcon : <Icon symbol="close" className="detail-control-close" />} </div> </div> ); @@ -26,6 +25,7 @@ Header.propTypes = { icon: PropTypes.string.isRequired, iconSize: PropTypes.number, component: PropTypes.object, + children: PropTypes.any, onClose: PropTypes.func.isRequired, }; diff --git a/frontend/src/components/dirent-detail/detail/header/title/index.css b/frontend/src/components/dirent-detail/detail/header/title/index.css new file mode 100644 index 00000000000..8367432a87a --- /dev/null +++ b/frontend/src/components/dirent-detail/detail/header/title/index.css @@ -0,0 +1,23 @@ +.detail-header .detail-title { + display: flex; + flex: 1; + align-items: center; + width: 0; /* prevent strut flex layout */ +} + +.detail-header .detail-title .detail-header-icon-container { + height: 32px; + width: 32px; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; +} + +.detail-header .detail-title .name { + margin: 0 0.5rem 0 6px; + line-height: 1.5rem; + vertical-align: middle; + font-size: 1rem; + color: #212529; +} diff --git a/frontend/src/components/dirent-detail/detail/header/title/index.js b/frontend/src/components/dirent-detail/detail/header/title/index.js new file mode 100644 index 00000000000..a00f754583a --- /dev/null +++ b/frontend/src/components/dirent-detail/detail/header/title/index.js @@ -0,0 +1,24 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +import './index.css'; + +const Title = ({ icon, iconSize, title }) => { + + return ( + <div className="detail-title dirent-title"> + <div className="detail-header-icon-container"> + <img src={icon} width={iconSize} height={iconSize} alt="" /> + </div> + <span className="name ellipsis" title={title}>{title}</span> + </div> + ); +}; + +Title.propTypes = { + icon: PropTypes.string, + iconSize: PropTypes.number, + title: PropTypes.string, +}; + +export default Title; diff --git a/frontend/src/components/dirent-detail/dirent-details/dir-details.js b/frontend/src/components/dirent-detail/dirent-details/dir-details.js index 7b0ea7244ac..dca1e3ac000 100644 --- a/frontend/src/components/dirent-detail/dirent-details/dir-details.js +++ b/frontend/src/components/dirent-detail/dirent-details/dir-details.js @@ -1,16 +1,14 @@ import React, { useMemo } from 'react'; import PropTypes from 'prop-types'; import { Formatter } from '@seafile/sf-metadata-ui-component'; -import { getDirentPath } from './utils'; import DetailItem from '../detail-item'; import { CellType } from '../../../metadata/constants'; import { gettext } from '../../../utils/constants'; import { MetadataDetails } from '../../../metadata'; import { useMetadataStatus } from '../../../hooks'; -const DirDetails = ({ repoID, repoInfo, dirent, path, direntDetail }) => { - const direntPath = useMemo(() => getDirentPath(dirent, path), [dirent, path]); - const { enableMetadata } = useMetadataStatus(); +const DirDetails = ({ direntDetail }) => { + const { enableMetadata, enableMetadataManagement } = useMetadataStatus(); const lastModifiedTimeField = useMemo(() => { return { type: CellType.MTIME, name: gettext('Last modified time') }; }, []); @@ -20,8 +18,8 @@ const DirDetails = ({ repoID, repoInfo, dirent, path, direntDetail }) => { <DetailItem field={lastModifiedTimeField} className="sf-metadata-property-detail-formatter"> <Formatter field={lastModifiedTimeField} value={direntDetail.mtime} /> </DetailItem> - {window.app.pageOptions.enableMetadataManagement && enableMetadata && ( - <MetadataDetails repoID={repoID} repoInfo={repoInfo} filePath={direntPath} direntType="dir" /> + {enableMetadataManagement && enableMetadata && ( + <MetadataDetails /> )} </> ); diff --git a/frontend/src/components/dirent-detail/dirent-details/file-details/index.js b/frontend/src/components/dirent-detail/dirent-details/file-details/index.js index e11454bf3ab..eb06faa56a2 100644 --- a/frontend/src/components/dirent-detail/dirent-details/file-details/index.js +++ b/frontend/src/components/dirent-detail/dirent-details/file-details/index.js @@ -10,7 +10,7 @@ import { gettext } from '../../../../utils/constants'; import EditFileTagPopover from '../../../popover/edit-filetag-popover'; import FileTagList from '../../../file-tag-list'; import { Utils } from '../../../../utils/utils'; -import { MetadataDetails } from '../../../../metadata'; +import { MetadataDetails, useMetadataDetails } from '../../../../metadata'; import ObjectUtils from '../../../../metadata/utils/object-utils'; import { getCellValueByColumn, getDateDisplayString, decimalToExposureTime } from '../../../../metadata/utils/cell'; import Collapse from './collapse'; @@ -57,10 +57,10 @@ const getImageInfoValue = (key, value) => { } }; -const FileDetails = React.memo(({ repoID, repoInfo, dirent, path, direntDetail, onFileTagChanged, repoTags, fileTagList }) => { +const FileDetails = React.memo(({ repoID, dirent, path, direntDetail, onFileTagChanged, repoTags, fileTagList }) => { const [isEditFileTagShow, setEditFileTagShow] = useState(false); - const { enableMetadata } = useMetadataStatus(); - const [record, setRecord] = useState(null); + const { enableMetadataManagement, enableMetadata } = useMetadataStatus(); + const { record } = useMetadataDetails(); const direntPath = useMemo(() => getDirentPath(dirent, path), [dirent, path]); const tagListTitleID = useMemo(() => `detail-list-view-tags-${uuidV4()}`, []); @@ -77,10 +77,6 @@ const FileDetails = React.memo(({ repoID, repoInfo, dirent, path, direntDetail, onFileTagChanged(dirent, direntPath); }, [dirent, direntPath, onFileTagChanged]); - const updateRecord = useCallback((record) => { - setRecord(record); - }, []); - const dom = ( <> <DetailItem field={sizeField} className="sf-metadata-property-detail-formatter"> @@ -116,8 +112,8 @@ const FileDetails = React.memo(({ repoID, repoInfo, dirent, path, direntDetail, </div> </DetailItem> )} - {window.app.pageOptions.enableMetadataManagement && enableMetadata && ( - <MetadataDetails repoID={repoID} filePath={direntPath} repoInfo={repoInfo} direntType="file" updateRecord={updateRecord} /> + {enableMetadataManagement && enableMetadata && ( + <MetadataDetails /> )} </> ); diff --git a/frontend/src/components/dirent-detail/dirent-details/index.js b/frontend/src/components/dirent-detail/dirent-details/index.js index 931f6b2dd9c..ace84a97a59 100644 --- a/frontend/src/components/dirent-detail/dirent-details/index.js +++ b/frontend/src/components/dirent-detail/dirent-details/index.js @@ -9,6 +9,9 @@ import { Detail, Header, Body } from '../detail'; import DirDetails from './dir-details'; import FileDetails from './file-details'; import ObjectUtils from '../../../metadata/utils/object-utils'; +import { MetadataDetailsProvider } from '../../../metadata/hooks'; +import Settings from '../../../metadata/components/metadata-details/settings'; +import { getDirentPath } from './utils'; import './index.css'; @@ -95,38 +98,59 @@ class DirentDetails extends React.Component { render() { const { dirent, direntDetail } = this.state; - const { repoID, path, fileTags } = this.props; + const { repoID, fileTags } = this.props; + + if (!dirent || !direntDetail) { + return ( + <Detail> + <Header title={dirent?.name || ''} icon={Utils.getDirentIcon(dirent, true)} onClose={this.props.onClose} /> + <Body> + {this.renderImage()} + </Body> + </Detail> + ); + } + + let path = this.props.path; + if (dirent?.type !== 'file') { + path = this.props.dirent ? Utils.joinPath(path, dirent.name) : path; + } + return ( - <Detail> - <Header title={dirent?.name || ''} icon={Utils.getDirentIcon(dirent, true)} onClose={this.props.onClose} /> - <Body> - {this.renderImage()} - {dirent && direntDetail && ( - <div className="detail-content"> - {dirent.type !== 'file' ? - <DirDetails - repoID={repoID} - repoInfo={this.props.currentRepoInfo} - dirent={dirent} - direntDetail={direntDetail} - path={this.props.dirent ? Utils.joinPath(path, dirent.name) : path} - /> - : - <FileDetails - repoID={repoID} - repoInfo={this.props.currentRepoInfo} - dirent={dirent} - path={path} - direntDetail={direntDetail} - repoTags={this.props.repoTags} - fileTagList={dirent ? dirent.file_tags : fileTags} - onFileTagChanged={this.props.onFileTagChanged} - /> - } - </div> - )} - </Body> - </Detail> + <MetadataDetailsProvider + repoID={repoID} + repoInfo={this.props.currentRepoInfo} + path={getDirentPath(dirent, path)} + dirent={dirent} + direntDetail={direntDetail} + direntType={dirent?.type !== 'file' ? 'dir' : 'file'} + > + <Detail> + <Header title={dirent?.name || ''} icon={Utils.getDirentIcon(dirent, true)} onClose={this.props.onClose} > + <Settings /> + </Header> + <Body> + {this.renderImage()} + {dirent && direntDetail && ( + <div className="detail-content"> + {dirent.type !== 'file' ? ( + <DirDetails direntDetail={direntDetail} /> + ) : ( + <FileDetails + repoID={repoID} + dirent={dirent} + path={path} + direntDetail={direntDetail} + repoTags={this.props.repoTags} + fileTagList={dirent ? dirent.file_tags : fileTags} + onFileTagChanged={this.props.onFileTagChanged} + /> + )} + </div> + )} + </Body> + </Detail> + </MetadataDetailsProvider> ); } } diff --git a/frontend/src/components/dirent-detail/dirent-details/utils.js b/frontend/src/components/dirent-detail/dirent-details/utils.js index 2f7af6b1c02..2336cea886a 100644 --- a/frontend/src/components/dirent-detail/dirent-details/utils.js +++ b/frontend/src/components/dirent-detail/dirent-details/utils.js @@ -2,7 +2,7 @@ import { Utils } from '../../../utils/utils'; export const getDirentPath = (dirent, path) => { if (Utils.isMarkdownFile(path)) return path; // column mode: view file - return Utils.joinPath(path, dirent.name); + return Utils.joinPath(path, dirent?.name); }; export const getFileParent = (dirent, path) => { diff --git a/frontend/src/components/dirent-detail/embedded-file-details/file-details.js b/frontend/src/components/dirent-detail/embedded-file-details/file-details.js index 300d36f7f27..2ed2ae58946 100644 --- a/frontend/src/components/dirent-detail/embedded-file-details/file-details.js +++ b/frontend/src/components/dirent-detail/embedded-file-details/file-details.js @@ -8,7 +8,7 @@ import { Utils } from '../../../utils/utils'; import { MetadataDetails } from '../../../metadata'; import { useMetadataStatus } from '../../../hooks'; -const FileDetails = ({ repoID, repoInfo, path, direntDetail }) => { +const FileDetails = ({ direntDetail }) => { const { enableMetadata } = useMetadataStatus(); const sizeField = useMemo(() => ({ type: 'size', name: gettext('Size') }), []); @@ -36,16 +36,13 @@ const FileDetails = ({ repoID, repoInfo, path, direntDetail }) => { <Formatter field={lastModifiedTimeField} value={direntDetail.last_modified}/> </DetailItem> {enableMetadata && ( - <MetadataDetails repoID={repoID} filePath={path} repoInfo={repoInfo} direntType="file" /> + <MetadataDetails /> )} </> ); }; FileDetails.propTypes = { - repoID: PropTypes.string, - repoInfo: PropTypes.object, - path: PropTypes.string, direntDetail: PropTypes.object, }; diff --git a/frontend/src/components/dirent-detail/embedded-file-details/index.js b/frontend/src/components/dirent-detail/embedded-file-details/index.js index f7a560f4cf8..ea94d317f62 100644 --- a/frontend/src/components/dirent-detail/embedded-file-details/index.js +++ b/frontend/src/components/dirent-detail/embedded-file-details/index.js @@ -7,6 +7,8 @@ import toaster from '../../toast'; import { Header, Body } from '../detail'; import FileDetails from './file-details'; import { MetadataContext } from '../../../metadata'; +import { MetadataDetailsProvider } from '../../../metadata/hooks'; +import Settings from '../../../metadata/components/metadata-details/settings'; import './index.css'; @@ -36,27 +38,33 @@ const EmbeddedFileDetails = ({ repoID, repoInfo, dirent, path, onClose, width = }, []); return ( - <div - className={classnames('cur-view-detail', className, { - 'cur-view-detail-small': width < 400, - 'cur-view-detail-large': width > 400 - })} - style={{ width }} + <MetadataDetailsProvider + repoID={repoID} + repoInfo={repoInfo} + path={path} + dirent={dirent} + direntDetail={direntDetail} + direntType="file" > - <Header title={dirent?.name || ''} icon={Utils.getDirentIcon(dirent, true)} onClose={onClose} component={headerComponent} /> - <Body> - {dirent && direntDetail && ( - <div className="detail-content"> - <FileDetails - repoID={repoID} - repoInfo={repoInfo} - path={path} - direntDetail={direntDetail} - /> - </div> - )} - </Body> - </div> + <div + className={classnames('cur-view-detail', className, { + 'cur-view-detail-small': width < 400, + 'cur-view-detail-large': width > 400 + })} + style={{ width }} + > + <Header title={dirent?.name || ''} icon={Utils.getDirentIcon(dirent, true)} onClose={onClose} component={headerComponent} > + <Settings /> + </Header> + <Body> + {dirent && direntDetail && ( + <div className="detail-content"> + <FileDetails direntDetail={direntDetail} /> + </div> + )} + </Body> + </div> + </MetadataDetailsProvider> ); }; diff --git a/frontend/src/components/dirent-detail/index.js b/frontend/src/components/dirent-detail/index.js index 8fabb6c6f91..838de2f95ed 100644 --- a/frontend/src/components/dirent-detail/index.js +++ b/frontend/src/components/dirent-detail/index.js @@ -31,13 +31,9 @@ const Detail = React.memo(({ repoID, path, dirent, currentRepoInfo, repoTags, fi } if (path === '/' && !dirent) { - return ( - <LibDetail - currentRepoInfo={currentRepoInfo} - onClose={onClose} - /> - ); + return (<LibDetail currentRepoInfo={currentRepoInfo} onClose={onClose} />); } + return ( <DirentDetail repoID={repoID} diff --git a/frontend/src/components/file-view/file-view.js b/frontend/src/components/file-view/file-view.js index 8992a431add..aebb6f9a7c2 100644 --- a/frontend/src/components/file-view/file-view.js +++ b/frontend/src/components/file-view/file-view.js @@ -31,7 +31,7 @@ const propTypes = { const { isStarred, isLocked, lockedByMe, repoID, filePath, filePerm, enableWatermark, userNickName, - fileName, repoEncrypted + fileName, repoEncrypted, isRepoAdmin } = window.app.pageOptions; class FileView extends React.Component { @@ -116,7 +116,8 @@ class FileView extends React.Component { const { isDetailsPanelOpen, isHeaderShown } = this.state; const repoInfo = { permission: filePerm, - encrypted: repoEncrypted + encrypted: repoEncrypted, + is_admin: isRepoAdmin, }; return ( <I18nextProvider i18n={ i18n }> diff --git a/frontend/src/constants/index.js b/frontend/src/constants/index.js index f369c58f785..1168a1ee234 100644 --- a/frontend/src/constants/index.js +++ b/frontend/src/constants/index.js @@ -38,3 +38,8 @@ export const MODE_TYPE_MAP = { RECENTLY_USED: 'recently_used', SEARCH_RESULTS: 'search_results', }; + +export const SYSTEM_FOLDERS = [ + '/_Internal', + '/images' +]; diff --git a/frontend/src/hooks/metadata-status.js b/frontend/src/hooks/metadata-status.js index 1aad544533d..687f660da6d 100644 --- a/frontend/src/hooks/metadata-status.js +++ b/frontend/src/hooks/metadata-status.js @@ -17,6 +17,7 @@ export const MetadataStatusProvider = ({ repoID, currentRepoInfo, hideMetadataVi const [enableMetadata, setEnableMetadata] = useState(false); const [enableTags, setEnableTags] = useState(false); const [tagsLang, setTagsLang] = useState('en'); + const [detailsSettings, setDetailsSettings] = useState({}); const [isBeingBuilt, setIsBeingBuilt] = useState(false); const cancelMetadataURL = useCallback(() => { @@ -37,12 +38,13 @@ export const MetadataStatusProvider = ({ repoID, currentRepoInfo, hideMetadataVi return; } metadataAPI.getMetadataStatus(repoID).then(res => { - const { enabled: enableMetadata, tags_enabled: enableTags, tags_lang: tagsLang } = res.data; + const { enabled: enableMetadata, tags_enabled: enableTags, tags_lang: tagsLang, details_settings: detailsSettings } = res.data; if (!enableMetadata) { cancelMetadataURL(); } setEnableTags(enableTags); setTagsLang(tagsLang || 'en'); + setDetailsSettings(JSON.parse(detailsSettings)); setEnableMetadata(enableMetadata); setLoading(false); }).catch(error => { @@ -60,6 +62,7 @@ export const MetadataStatusProvider = ({ repoID, currentRepoInfo, hideMetadataVi cancelMetadataURL(); setEnableTags(false); } + setDetailsSettings({}); setIsBeingBuilt(newValue); setEnableMetadata(newValue); }, [enableMetadata, cancelMetadataURL]); @@ -74,6 +77,16 @@ export const MetadataStatusProvider = ({ repoID, currentRepoInfo, hideMetadataVi setTagsLang(lang); }, [enableTags, tagsLang, cancelMetadataURL, hideMetadataView]); + const modifyDetailsSettings = useCallback((update) => { + metadataAPI.modifyMetadataDetailsSettings(repoID, update).then(res => { + const newDetailsSettings = { ...detailsSettings, ...update }; + setDetailsSettings(newDetailsSettings); + }).catch(error => { + const newDetailsSettings = { ...detailsSettings, ...update }; + setDetailsSettings(newDetailsSettings); + }); + }, [repoID, detailsSettings]); + return ( <EnableMetadataContext.Provider value={{ @@ -85,6 +98,8 @@ export const MetadataStatusProvider = ({ repoID, currentRepoInfo, hideMetadataVi enableTags, tagsLang, updateEnableTags, + detailsSettings, + modifyDetailsSettings, }} > {!isLoading && children} diff --git a/frontend/src/metadata/api.js b/frontend/src/metadata/api.js index 8c9f5419b0d..53d8f0669db 100644 --- a/frontend/src/metadata/api.js +++ b/frontend/src/metadata/api.js @@ -65,6 +65,12 @@ class MetadataManagerAPI { return this.req.delete(url); } + modifyMetadataDetailsSettings(repoID, settings) { + const url = this.server + '/api/v2.1/repos/' + repoID + '/metadata/details-settings/'; + const data = { settings: settings }; + return this.req.put(url, data); + } + getMetadata(repoID, params) { const url = this.server + '/api/v2.1/repos/' + repoID + '/metadata/records/'; return this.req.get(url, { params: params }); diff --git a/frontend/src/metadata/components/metadata-details/constants.js b/frontend/src/metadata/components/metadata-details/constants.js index 12d7876bf66..1b811db4eed 100644 --- a/frontend/src/metadata/components/metadata-details/constants.js +++ b/frontend/src/metadata/components/metadata-details/constants.js @@ -1,4 +1,4 @@ -import { PRIVATE_COLUMN_KEY } from '../../constants'; +import { PRIVATE_COLUMN_KEY, CellType } from '../../constants'; export const NOT_DISPLAY_COLUMN_KEYS = [ PRIVATE_COLUMN_KEY.ID, @@ -23,11 +23,7 @@ export const NOT_DISPLAY_COLUMN_KEYS = [ PRIVATE_COLUMN_KEY.FACE_VECTORS, ]; -export const SYSTEM_FOLDERS = [ - '/_Internal', - '/images' -]; - export { PRIVATE_COLUMN_KEY, + CellType, }; diff --git a/frontend/src/metadata/components/metadata-details/index.js b/frontend/src/metadata/components/metadata-details/index.js index db7db7e68ef..9705e53fb69 100644 --- a/frontend/src/metadata/components/metadata-details/index.js +++ b/frontend/src/metadata/components/metadata-details/index.js @@ -1,159 +1,45 @@ -import React, { useCallback, useEffect, useMemo, useState } from 'react'; -import PropTypes from 'prop-types'; -import toaster from '../../../components/toast'; +import React, { useMemo } from 'react'; import CellFormatter from '../cell-formatter'; import DetailEditor from '../detail-editor'; import DetailItem from '../../../components/dirent-detail/detail-item'; import { Utils } from '../../../utils/utils'; -import metadataAPI from '../../api'; -import Column from '../../model/column'; -import { getCellValueByColumn, getOptionName, getColumnOptionNamesByIds, getColumnOptionNameById, getFileNameFromRecord, getRecordIdFromRecord, getFileObjIdFromRecord } from '../../utils/cell'; -import { normalizeFields } from './utils'; +import { getCellValueByColumn, getFileNameFromRecord } from '../../utils/cell'; import { gettext } from '../../../utils/constants'; -import { CellType, EVENT_BUS_TYPE, PREDEFINED_COLUMN_KEYS, PRIVATE_COLUMN_KEY } from '../../constants'; -import { getColumnByKey, getColumnOptions, getColumnOriginName } from '../../utils/column'; -import { SYSTEM_FOLDERS } from './constants'; +import { PRIVATE_COLUMN_KEY } from '../../constants'; import Location from './location'; +import { useMetadataDetails } from '../../hooks'; import { checkIsDir } from '../../utils/row'; -import tagsAPI from '../../../tag/api'; import './index.css'; -const MetadataDetails = ({ repoID, filePath, repoInfo, direntType, updateRecord }) => { - const [isLoading, setLoading] = useState(true); - const [metadata, setMetadata] = useState({ record: {}, fields: [] }); - const permission = useMemo(() => repoInfo.permission !== 'admin' && repoInfo.permission !== 'rw' ? 'r' : 'rw', [repoInfo]); +const MetadataDetails = () => { + const { isLoading, canModifyRecord, record, columns, onChange, modifyColumnData, updateFileTags } = useMetadataDetails(); - const onChange = useCallback((fieldKey, newValue) => { - const { record, fields } = metadata; - const field = getColumnByKey(fields, fieldKey); - const fileName = getColumnOriginName(field); - const recordId = getRecordIdFromRecord(record); - const fileObjId = getFileObjIdFromRecord(record); - let update = { [fileName]: newValue }; - if (field.type === CellType.SINGLE_SELECT) { - update = { [fileName]: getColumnOptionNameById(field, newValue) }; - } else if (field.type === CellType.MULTIPLE_SELECT) { - update = { [fileName]: newValue ? getColumnOptionNamesByIds(field, newValue) : [] }; - } - metadataAPI.modifyRecord(repoID, recordId, update, fileObjId).then(res => { - const newMetadata = { ...metadata, record: { ...record, ...update } }; - setMetadata(newMetadata); - if (window?.sfMetadataContext?.eventBus) { - window.sfMetadataContext.eventBus.dispatch(EVENT_BUS_TYPE.LOCAL_RECORD_CHANGED, recordId, update); - } - }).catch(error => { - const errorMsg = Utils.getErrorMsg(error); - toaster.danger(errorMsg); - }); - }, [repoID, metadata]); - - const modifyColumnData = useCallback((fieldKey, newData) => { - const { fields, record } = metadata; - let newFields = fields.slice(0); - let update; - metadataAPI.modifyColumnData(repoID, fieldKey, newData).then(res => { - const newField = new Column(res.data.column); - const fieldIndex = fields.findIndex(f => f.key === fieldKey); - newFields[fieldIndex] = newField; - return newField; - }).then((newField) => { - const fileName = getColumnOriginName(newField); - const options = getColumnOptions(newField); - const newOption = options[options.length - 1]; - update = { [fileName]: newOption.id }; - if (!PREDEFINED_COLUMN_KEYS.includes(fieldKey) && newField.type === CellType.SINGLE_SELECT) { - update = { [fileName]: getOptionName(options, newOption.id) }; - } else if (newField.type === CellType.MULTIPLE_SELECT) { - const oldValue = getCellValueByColumn(record, newField) || []; - update = { [fileName]: [...oldValue, newOption.name] }; - } - return metadataAPI.modifyRecord(repoID, record._id, update, record._obj_id); - }).then(res => { - const newMetadata = { ...metadata, record: { ...record, ...update }, fields: newFields }; - setMetadata(newMetadata); - }).catch(error => { - const errorMsg = Utils.getErrorMsg(error); - toaster.danger(errorMsg); - }); - }, [repoID, metadata]); - - const localRecordChanged = useCallback((recordId, updates) => { - if (getRecordIdFromRecord(metadata?.record) !== recordId) return; - const newMetadata = { ...metadata, record: { ...metadata.record, ...updates } }; - setMetadata(newMetadata); - }, [metadata]); - - const updateFileTags = useCallback((updateRecords) => { - const { record } = metadata; - const { record_id, tags } = updateRecords[0]; - - tagsAPI.updateFileTags(repoID, [{ record_id, tags }]).then(res => { - const newValue = tags ? tags.map(id => ({ row_id: id, display_value: id })) : []; - const update = { [PRIVATE_COLUMN_KEY.TAGS]: newValue }; - const newMetadata = { ...metadata, record: { ...record, ...update } }; - setMetadata(newMetadata); - if (window?.sfMetadataContext?.eventBus) { - window.sfMetadataContext.eventBus.dispatch(EVENT_BUS_TYPE.LOCAL_RECORD_CHANGED, record_id, update); - } - }).catch(error => { - const errorMsg = Utils.getErrorMsg(error); - toaster.danger(errorMsg); - }); - }, [repoID, metadata]); - - useEffect(() => { - setLoading(true); - if (SYSTEM_FOLDERS.find(folderPath => filePath.startsWith(folderPath))) { - setLoading(false); - return; - } - - const dirName = Utils.getDirName(filePath); - const fileName = Utils.getFileName(filePath); - let parentDir = direntType === 'file' ? dirName : dirName.slice(0, dirName.length - fileName.length - 1); - if (!parentDir.startsWith('/')) { - parentDir = '/' + parentDir; - } - metadataAPI.getMetadataRecordInfo(repoID, parentDir, fileName).then(res => { - const { results, metadata } = res.data; - const record = Array.isArray(results) && results.length > 0 ? results[0] : {}; - let fields = normalizeFields(metadata).map(field => new Column(field)); - const isDir = checkIsDir(record); - if (isDir) { - fields = fields.filter(field => field.type !== CellType.TAGS); - } - updateRecord && updateRecord(record); - setMetadata({ record, fields }); - setLoading(false); - }).catch(error => { - const errMessage = Utils.getErrorMsg(error); - toaster.danger(errMessage); - setLoading(false); - }); - }, [repoID, filePath, direntType, updateRecord]); - - useEffect(() => { - const eventBus = window?.sfMetadataContext?.eventBus; - if (!eventBus) return; - const unsubscribeLocalRecordChanged = eventBus.subscribe(EVENT_BUS_TYPE.LOCAL_RECORD_DETAIL_CHANGED, localRecordChanged); - return () => { - unsubscribeLocalRecordChanged(); - }; - }, [localRecordChanged]); + const displayColumns = useMemo(() => columns.filter(c => c.shown), [columns]); if (isLoading) return null; - const { fields, record } = metadata; + if (!record) return null; if (!record._id) return null; + const fileName = getFileNameFromRecord(record); const isImage = record && (Utils.imageCheck(fileName) || Utils.videoCheck(fileName)); + const isDir = record && checkIsDir(record); + return ( <> - {fields.map(field => { - let canEdit = permission === 'rw' && field.editable; + {displayColumns.map(field => { + if (field.key === PRIVATE_COLUMN_KEY.LOCATION) { + if (!isImage) return null; + return (<Location key={field.key} position={getCellValueByColumn(record, field)} />); + } + + if (field.key === PRIVATE_COLUMN_KEY.TAGS && isDir) return null; + + let canEdit = canModifyRecord && field.editable; if (!isImage && canEdit && field.key === PRIVATE_COLUMN_KEY.CAPTURE_TIME) { canEdit = false; } + const value = getCellValueByColumn(record, field); return ( <DetailItem key={field.key} field={field} readonly={!canEdit}> @@ -161,7 +47,7 @@ const MetadataDetails = ({ repoID, filePath, repoInfo, direntType, updateRecord <DetailEditor field={field} value={value} - fields={fields} + fields={columns} record={record} modifyColumnData={modifyColumnData} onChange={onChange} @@ -173,18 +59,8 @@ const MetadataDetails = ({ repoID, filePath, repoInfo, direntType, updateRecord </DetailItem> ); })} - {isImage && (<Location position={getCellValueByColumn(record, { key: PRIVATE_COLUMN_KEY.LOCATION })} />)} </> ); }; -MetadataDetails.propTypes = { - repoID: PropTypes.string, - filePath: PropTypes.string, - repoInfo: PropTypes.object, - direntType: PropTypes.string, - direntDetail: PropTypes.object, - updateRecord: PropTypes.func, -}; - export default MetadataDetails; diff --git a/frontend/src/metadata/components/metadata-details/location/index.js b/frontend/src/metadata/components/metadata-details/location/index.js index 9bc380b5d9a..16ab8582862 100644 --- a/frontend/src/metadata/components/metadata-details/location/index.js +++ b/frontend/src/metadata/components/metadata-details/location/index.js @@ -175,7 +175,7 @@ class Location extends React.Component { )} </DetailItem> {isLoading ? (<Loading />) : this.mapType && ( - <div className={classnames('dirent-detail-item-value-map', { 'd-none': !isValid })}> + <div className={classnames('dirent-detail-item dirent-detail-item-value-map', { 'd-none': !isValid })}> <div className="w-100 h-100" ref={ref => this.ref = ref} id="sf-geolocation-map-container"></div> </div> )} diff --git a/frontend/src/metadata/components/metadata-details/settings/index.css b/frontend/src/metadata/components/metadata-details/settings/index.css new file mode 100644 index 00000000000..e69de29bb2d diff --git a/frontend/src/metadata/components/metadata-details/settings/index.js b/frontend/src/metadata/components/metadata-details/settings/index.js new file mode 100644 index 00000000000..a3f99b8464f --- /dev/null +++ b/frontend/src/metadata/components/metadata-details/settings/index.js @@ -0,0 +1,46 @@ +import React, { useMemo, useCallback, useState } from 'react'; +import Icon from '../../../../components/icon'; +import HideColumnPopover from '../../popover/hidden-column-popover'; +import { useMetadataDetails } from '../../../hooks'; +import { useMetadataStatus } from '../../../../hooks'; + +import './index.css'; + +const Settings = () => { + const [isShowSetter, setShowSetter] = useState(false); + + const { enableMetadata } = useMetadataStatus(); + const { modifyColumnOrder, modifyHiddenColumns, columns, canModifyDetails } = useMetadataDetails(); + const hiddenColumns = useMemo(() => columns.filter(c => !c.shown).map(c => c.key), [columns]); + + const onSetterToggle = useCallback(() => { + setShowSetter(!isShowSetter); + }, [isShowSetter]); + const target = useMemo(() => 'detail-control-settings-btn', []); + + if (!enableMetadata) return null; + if (!canModifyDetails) return null; + + return ( + <> + <div className="detail-control mr-2" id={target} onClick={onSetterToggle}> + <Icon symbol="set-up" className="detail-control-close" /> + </div> + {isShowSetter && ( + <HideColumnPopover + readOnly={false} + hiddenColumns={hiddenColumns} + target={target} + placement="bottom-end" + columns={columns} + hidePopover={onSetterToggle} + onChange={modifyHiddenColumns} + modifyColumnOrder={modifyColumnOrder} + /> + )} + </> + ); + +}; + +export default Settings; diff --git a/frontend/src/metadata/components/metadata-details/utils.js b/frontend/src/metadata/components/metadata-details/utils.js index 050c5c30c8b..0721f694f09 100644 --- a/frontend/src/metadata/components/metadata-details/utils.js +++ b/frontend/src/metadata/components/metadata-details/utils.js @@ -1,10 +1,10 @@ import { getNormalizedColumnType } from '../../utils/column'; import { getCellValueByColumn } from '../../utils/cell'; -import { NOT_DISPLAY_COLUMN_KEYS } from './constants'; +import { NOT_DISPLAY_COLUMN_KEYS, CellType, PRIVATE_COLUMN_KEY } from './constants'; export const normalizeFields = (fields) => { if (!Array.isArray(fields) || fields.length === 0) return []; - const validFields = fields.map((field) => { + let validFields = fields.map((field) => { const { type, key, ...params } = field; return { ...params, @@ -13,11 +13,8 @@ export const normalizeFields = (fields) => { width: 200, }; }).filter(field => !NOT_DISPLAY_COLUMN_KEYS.includes(field.key)); - let displayFields = []; - validFields.forEach(field => { - displayFields.push(field); - }); - return displayFields; + validFields.push({ key: PRIVATE_COLUMN_KEY.LOCATION, type: CellType.GEOLOCATION, width: 200 }); + return validFields; }; export { diff --git a/frontend/src/metadata/components/popover/hidden-column-popover/hidden-columns/hide-column.js b/frontend/src/metadata/components/popover/hidden-column-popover/hidden-columns/hide-column.js index e0c9fe6e2c7..c3b554c3201 100644 --- a/frontend/src/metadata/components/popover/hidden-column-popover/hidden-columns/hide-column.js +++ b/frontend/src/metadata/components/popover/hidden-column-popover/hidden-columns/hide-column.js @@ -1,110 +1,105 @@ -import React, { useCallback } from 'react'; +import React, { useCallback, useRef } from 'react'; import PropTypes from 'prop-types'; import classNames from 'classnames'; -import { DragSource, DropTarget } from 'react-dnd'; import { Icon, Switch } from '@seafile/sf-metadata-ui-component'; import { COLUMNS_ICON_CONFIG } from '../../../../constants'; -const dragSource = { - beginDrag: props => { - return { key: props.column.key, column: props.column }; - }, - endDrag(props, monitor) { - const source = monitor.getItem(); - const didDrop = monitor.didDrop(); - let target = {}; - if (!didDrop) { - return { source, target }; - } - }, - isDragging(props) { - const { columnIndex, currentIndex } = props; - return currentIndex > columnIndex; - } -}; -const dropTarget = { - drop(props, monitor) { - const source = monitor.getItem(); - const { column: targetColumn } = props; - if (targetColumn.key !== source.key && source.column.frozen === targetColumn.frozen) { - const target = { key: targetColumn.key }; - props.onMove(source.key, target.key); - } - } -}; - -const dragCollect = (connect, monitor) => ({ - connectDragSource: connect.dragSource(), - connectDragPreview: connect.dragPreview(), - isDragging: monitor.isDragging(), -}); - -const dropCollect = (connect, monitor) => ({ - connectDropTarget: connect.dropTarget(), - isOver: monitor.isOver(), - canDrop: monitor.canDrop(), - dragged: monitor.getItem(), -}); - const HideColumnItem = ({ - isOver, - isDragging, - canDrop, - connectDragSource, - connectDragPreview, - connectDropTarget, readOnly, + isHidden, column, columnIndex, - isHidden, + draggingColumnKey, + draggingColumnIndex, + dragOverColumnKey, + updateDraggingKey, + updateDragOverKey, onChange, - onMouseEnter, - onMouseLeave, + onMove, }) => { + const ref = useRef(null); + + const onDragStart = useCallback((event) => { + const dragData = JSON.stringify({ type: 'sf-metadata-filed-display-setting', column_key: column.key }); + event.dataTransfer.setDragImage(ref.current, 10, 10); + event.dataTransfer.effectAllowed = 'move'; + event.dataTransfer.setData('application/drag-sf-metadata-filed-display-setting', dragData); + updateDraggingKey(column.key); + }, [column, updateDraggingKey]); + + const onDragEnter = useCallback(() => { + if (!draggingColumnKey) return; + updateDragOverKey(column.key); + }, [column, updateDragOverKey, draggingColumnKey]); + + const onDragLeave = useCallback(() => { + if (!draggingColumnKey) return; + updateDragOverKey(null); + }, [updateDragOverKey, draggingColumnKey]); + + const onDragOver = useCallback((event) => { + if (!draggingColumnKey) return; + event.preventDefault(); + event.dataTransfer.dropEffect = 'move'; + updateDragOverKey(column.key); + }, [column, updateDragOverKey, draggingColumnKey]); + + const onDrop = useCallback((event) => { + event.stopPropagation(); + let dragData = event.dataTransfer.getData('application/drag-sf-metadata-filed-display-setting'); + if (!dragData) return false; + dragData = JSON.parse(dragData); + if (dragData.type !== 'sf-metadata-filed-display-setting' || !dragData.column_key) return false; + if (dragData.column_key !== column.key) { + onMove && onMove(dragData.column_key, column.key); + } + }, [column, onMove]); + + const onDragEnd = useCallback(() => { + updateDraggingKey(null); + updateDragOverKey(null); + }, [updateDraggingKey, updateDragOverKey]); const update = useCallback(() => { if (readOnly) return; onChange(column.key); }, [readOnly, column, onChange]); + const isOver = dragOverColumnKey === column.key; + return ( - <> - {connectDropTarget( - connectDragPreview( - <div - className={classNames('hide-column-item', { - 'disabled': readOnly, - 'hide-column-can-drop-top': isOver && canDrop && isDragging, - 'hide-column-can-drop': isOver && canDrop && !isDragging - })} - onMouseEnter={() => onMouseEnter(columnIndex)} - onMouseLeave={onMouseLeave} - > - {!readOnly && ( - <> - {connectDragSource( - <div className="drag-hide-column-handle"> - <Icon iconName="drag" /> - </div> - )} - </> - )} - <Switch - disabled={readOnly} - checked={isHidden} - placeholder={( - <> - <Icon iconName={COLUMNS_ICON_CONFIG[column.type]} /> - <span className="text-truncate">{column.name}</span> - </> - )} - onChange={update} - switchClassName="hide-column-item-switch" - /> - </div> - ) + <div + ref={ref} + className={classNames('hide-column-item', { + 'disabled': readOnly, + 'hide-column-can-drop-top': isOver && draggingColumnIndex >= columnIndex, + 'hide-column-can-drop': isOver && draggingColumnIndex < columnIndex, + 'dragging': draggingColumnKey === column.key + })} + onDrop={onDrop} + onDragEnter={onDragEnter} + onDragOver={onDragOver} + onDragLeave={onDragLeave} + onDragEnd={onDragEnd} + > + {!readOnly && ( + <div className="drag-hide-column-handle" draggable="true" onDragStart={onDragStart}> + <Icon iconName="drag" /> + </div> )} - </> + <Switch + disabled={readOnly} + checked={isHidden} + placeholder={( + <> + <Icon iconName={COLUMNS_ICON_CONFIG[column.type]} /> + <span className="text-truncate">{column.name}</span> + </> + )} + onChange={update} + switchClassName="hide-column-item-switch" + /> + </div> ); }; @@ -112,14 +107,14 @@ HideColumnItem.propTypes = { readOnly: PropTypes.bool, isHidden: PropTypes.bool, columnIndex: PropTypes.number, - currentIndex: PropTypes.number, column: PropTypes.object.isRequired, + draggingColumnKey: PropTypes.string, + draggingColumnIndex: PropTypes.number, + dragOverColumnKey: PropTypes.string, + updateDraggingKey: PropTypes.func, + updateDragOverKey: PropTypes.func, onChange: PropTypes.func.isRequired, onMove: PropTypes.func, - onMouseEnter: PropTypes.func, - onMouseLeave: PropTypes.func, }; -export default DropTarget('sfMetadataHiddenColumns', dropTarget, dropCollect)( - DragSource('sfMetadataHiddenColumns', dragSource, dragCollect)(HideColumnItem) -); +export default HideColumnItem; diff --git a/frontend/src/metadata/components/popover/hidden-column-popover/hidden-columns/index.js b/frontend/src/metadata/components/popover/hidden-column-popover/hidden-columns/index.js index d1210be77dd..c4609101a00 100644 --- a/frontend/src/metadata/components/popover/hidden-column-popover/hidden-columns/index.js +++ b/frontend/src/metadata/components/popover/hidden-column-popover/hidden-columns/index.js @@ -1,27 +1,29 @@ import React, { useCallback, useMemo, useState } from 'react'; import PropTypes from 'prop-types'; import classnames from 'classnames'; -import { DropTarget } from 'react-dnd'; import HideColumn from './hide-column'; -import html5DragDropContext from '../../../../../pages/wiki2/wiki-nav/html5DragDropContext'; import { gettext } from '../../../../../utils/constants'; const HiddenColumns = ({ readOnly, columns, hiddenColumns, onChange, modifyColumnOrder }) => { - const [currentIndex, setCurrentIndex] = useState(-1); + const [draggingColumnKey, setDraggingCellKey] = useState(null); + const [dragOverColumnKey, setDragOverCellKey] = useState(null); const isEmpty = useMemo(() => { if (!Array.isArray(columns) || columns.length === 0) return true; return false; }, [columns]); - const onMouseEnter = useCallback((columnIndex) => { - if (currentIndex === columnIndex) return; - setCurrentIndex(columnIndex); - }, [currentIndex]); + const updateDraggingKey = useCallback((cellKey) => { + if (cellKey === draggingColumnKey) return; + setDraggingCellKey(cellKey); + }, [draggingColumnKey]); - const onMouseLeave = useCallback(() => { - setCurrentIndex(-1); - }, []); + const updateDragOverKey = useCallback((cellKey) => { + if (cellKey === dragOverColumnKey) return; + setDragOverCellKey(cellKey); + }, [dragOverColumnKey]); + + const draggingColumnIndex = draggingColumnKey ? columns.findIndex(c => c.key === draggingColumnKey) : -1; return ( <div className={classnames('hide-columns-list', { 'empty-hide-columns-container': isEmpty })}> @@ -32,13 +34,15 @@ const HiddenColumns = ({ readOnly, columns, hiddenColumns, onChange, modifyColum key={column.key} readOnly={readOnly} columnIndex={columnIndex} - currentIndex={currentIndex} isHidden={!hiddenColumns.includes(column.key)} column={column} + draggingColumnKey={draggingColumnKey} + draggingColumnIndex={draggingColumnIndex} + dragOverColumnKey={dragOverColumnKey} onChange={onChange} onMove={modifyColumnOrder} - onMouseEnter={onMouseEnter} - onMouseLeave={onMouseLeave} + updateDraggingKey={updateDraggingKey} + updateDragOverKey={updateDragOverKey} /> ); })} @@ -54,8 +58,4 @@ HiddenColumns.propTypes = { modifyColumnOrder: PropTypes.func, }; -const DndHiddenColumns = DropTarget('sfMetadataHiddenColumns', {}, connect => ({ - connectDropTarget: connect.dropTarget() -}))(HiddenColumns); - -export default html5DragDropContext(DndHiddenColumns); +export default HiddenColumns; diff --git a/frontend/src/metadata/components/popover/hidden-column-popover/index.css b/frontend/src/metadata/components/popover/hidden-column-popover/index.css index 00438adf5ab..039f8259d2e 100644 --- a/frontend/src/metadata/components/popover/hidden-column-popover/index.css +++ b/frontend/src/metadata/components/popover/hidden-column-popover/index.css @@ -145,6 +145,10 @@ display: none; } +.sf-metadata-hide-columns-popover .hide-column-item.dragging { + background-color: #f5f5f5; +} + .sf-metadata-hide-columns-popover .hide-column-item.hide-column-can-drop::after, .sf-metadata-hide-columns-popover .hide-column-item.hide-column-can-drop-top::before { content: ''; diff --git a/frontend/src/metadata/components/view-toolbar/kanban-view-toolbar/index.js b/frontend/src/metadata/components/view-toolbar/kanban-view-toolbar/index.js index 3176fbccd79..9835eb6575a 100644 --- a/frontend/src/metadata/components/view-toolbar/kanban-view-toolbar/index.js +++ b/frontend/src/metadata/components/view-toolbar/kanban-view-toolbar/index.js @@ -67,13 +67,14 @@ const KanbanViewToolBar = ({ className="sf-metadata-view-tool-operation-btn sf-metadata-view-tool-setting" size={24} role="button" - aria-label="Setting" + aria-label={gettext('Settings')} + title={gettext('Settings')} tabIndex={0} onClick={onToggleKanbanSetting} /> {!isCustomPermission && ( - <div className="cur-view-path-btn ml-2" onClick={toggleDetails}> - <span className="sf3-font sf3-font-info" aria-label={gettext('Properties')} title={gettext('Properties')}></span> + <div className="cur-view-path-btn ml-2" onClick={toggleDetails} aria-label={gettext('Properties')} title={gettext('Properties')}> + <span className="sf3-font sf3-font-info"></span> </div> )} </div> diff --git a/frontend/src/metadata/hooks/index.js b/frontend/src/metadata/hooks/index.js index a00eced1d9a..ca990d0368c 100644 --- a/frontend/src/metadata/hooks/index.js +++ b/frontend/src/metadata/hooks/index.js @@ -1,2 +1,3 @@ export { MetadataProvider, useMetadata } from './metadata'; export { CollaboratorsProvider, useCollaborators } from './collaborators'; +export { MetadataDetailsProvider, useMetadataDetails } from './metadata-details'; diff --git a/frontend/src/metadata/hooks/metadata-details.js b/frontend/src/metadata/hooks/metadata-details.js new file mode 100644 index 00000000000..45ecccb5eee --- /dev/null +++ b/frontend/src/metadata/hooks/metadata-details.js @@ -0,0 +1,211 @@ +import React, { useContext, useEffect, useCallback, useState, useMemo, useRef } from 'react'; +import metadataAPI from '../api'; +import { Utils } from '../../utils/utils'; +import toaster from '../../components/toast'; +import { useMetadataStatus } from '../../hooks/metadata-status'; +import { SYSTEM_FOLDERS } from '../../constants'; +import Column from '../model/column'; +import { normalizeFields } from '../components/metadata-details/utils'; +import { CellType, EVENT_BUS_TYPE, PREDEFINED_COLUMN_KEYS, PRIVATE_COLUMN_KEY } from '../constants'; +import { getCellValueByColumn, getOptionName, getColumnOptionNamesByIds, getColumnOptionNameById, getRecordIdFromRecord, + getFileObjIdFromRecord +} from '../utils/cell'; +import tagsAPI from '../../tag/api'; +import { getColumnByKey, getColumnOptions, getColumnOriginName } from '../utils/column'; + +const MetadataDetailsContext = React.createContext(null); + +export const MetadataDetailsProvider = ({ repoID, repoInfo, path, dirent, direntDetail, direntType, children }) => { + const { enableMetadata, detailsSettings, modifyDetailsSettings } = useMetadataStatus(); + + const [isLoading, setLoading] = useState(true); + const [record, setRecord] = useState(null); + const [originColumns, setOriginColumns] = useState([]); + + const canModifyRecord = useMemo(() => repoInfo.permission !== 'admin' && repoInfo.permission !== 'rw' ? false : true, [repoInfo]); + const canModifyDetails = useMemo(() => repoInfo.is_admin, [repoInfo]); + + const allColumnsRef = useRef([]); + + const columns = useMemo(() => { + const orderAndHiddenColumns = detailsSettings?.columns || []; + if (!Array.isArray(orderAndHiddenColumns) || orderAndHiddenColumns.length === 0) { + return originColumns.map(c => ({ ...c, shown: true })); + } + const oldColumnsMap = orderAndHiddenColumns.reduce((pre, cur) => { + pre[cur.key] = true; + return pre; + }, {}); + const columnsMap = originColumns.reduce((pre, cur) => { + pre[cur.key] = cur; + return pre; + }, {}); + const exitColumnsOrder = orderAndHiddenColumns.map(c => { + const column = columnsMap[c.key]; + if (column) return { ...c, ...column }; + return null; + }).filter(c => c); + const newColumns = originColumns.filter(c => !oldColumnsMap[c.key]).map(c => ({ ...c, shown: false })); + return [...exitColumnsOrder, ...newColumns]; + }, [originColumns, detailsSettings]); + + const localRecordChanged = useCallback((recordId, updates) => { + if (getRecordIdFromRecord(record) !== recordId) return; + const newRecord = { ...record, ...updates }; + setRecord(newRecord); + }, [record]); + + const onChange = useCallback((fieldKey, newValue) => { + const field = getColumnByKey(originColumns, fieldKey); + const fileName = getColumnOriginName(field); + const recordId = getRecordIdFromRecord(record); + const fileObjId = getFileObjIdFromRecord(record); + let update = { [fileName]: newValue }; + if (field.type === CellType.SINGLE_SELECT) { + update = { [fileName]: getColumnOptionNameById(field, newValue) }; + } else if (field.type === CellType.MULTIPLE_SELECT) { + update = { [fileName]: newValue ? getColumnOptionNamesByIds(field, newValue) : [] }; + } + metadataAPI.modifyRecord(repoID, recordId, update, fileObjId).then(res => { + setRecord({ ...record, ...update }); + if (window?.sfMetadataContext?.eventBus) { + window.sfMetadataContext.eventBus.dispatch(EVENT_BUS_TYPE.LOCAL_RECORD_CHANGED, recordId, update); + } + }).catch(error => { + const errorMsg = Utils.getErrorMsg(error); + toaster.danger(errorMsg); + }); + }, [repoID, record, originColumns]); + + const modifyColumnData = useCallback((fieldKey, newData) => { + let newColumns = originColumns.slice(0); + let update; + metadataAPI.modifyColumnData(repoID, fieldKey, newData).then(res => { + const newColumn = new Column(res.data.column); + const fieldIndex = originColumns.findIndex(f => f.key === fieldKey); + newColumns[fieldIndex] = newColumn; + return newColumn; + }).then((newField) => { + const fileName = getColumnOriginName(newField); + const options = getColumnOptions(newField); + const newOption = options[options.length - 1]; + update = { [fileName]: newOption.id }; + if (!PREDEFINED_COLUMN_KEYS.includes(fieldKey) && newField.type === CellType.SINGLE_SELECT) { + update = { [fileName]: getOptionName(options, newOption.id) }; + } else if (newField.type === CellType.MULTIPLE_SELECT) { + const oldValue = getCellValueByColumn(record, newField) || []; + update = { [fileName]: [...oldValue, newOption.name] }; + } + return metadataAPI.modifyRecord(repoID, record._id, update, record._obj_id); + }).then(res => { + setOriginColumns(newColumns); + setRecord({ ...record, ...update }); + }).catch(error => { + const errorMsg = Utils.getErrorMsg(error); + toaster.danger(errorMsg); + }); + }, [repoID, record, originColumns]); + + const updateFileTags = useCallback((updateRecords) => { + const { record_id, tags } = updateRecords[0]; + + tagsAPI.updateFileTags(repoID, [{ record_id, tags }]).then(res => { + const newValue = tags ? tags.map(id => ({ row_id: id, display_value: id })) : []; + const update = { [PRIVATE_COLUMN_KEY.TAGS]: newValue }; + setRecord({ ...record, ...update }); + if (window?.sfMetadataContext?.eventBus) { + window.sfMetadataContext.eventBus.dispatch(EVENT_BUS_TYPE.LOCAL_RECORD_CHANGED, record_id, update); + } + }).catch(error => { + const errorMsg = Utils.getErrorMsg(error); + toaster.danger(errorMsg); + }); + }, [repoID, record]); + + const saveColumns = useCallback((columns) => { + modifyDetailsSettings({ columns: columns.map(c => ({ key: c.key, shown: c.shown })) }); + }, [modifyDetailsSettings]); + + const modifyHiddenColumns = useCallback((hiddenColumns) => { + let newColumns = columns.slice(0); + newColumns = newColumns.map(c => ({ ...c, shown: !hiddenColumns.includes(c.key) })); + saveColumns(newColumns); + }, [columns, saveColumns]); + + const modifyColumnOrder = useCallback((sourceColumnKey, targetColumnKey) => { + const targetColumnIndex = columns.findIndex(c => c.key === targetColumnKey); + const sourceColumn = columns.find(c => c.key === sourceColumnKey); + let newColumns = columns.slice(0); + newColumns = newColumns.filter(c => c.key !== sourceColumnKey); + newColumns.splice(targetColumnIndex, 0, sourceColumn); + saveColumns(newColumns); + }, [columns, saveColumns]); + + useEffect(() => { + setLoading(true); + if (!dirent || !direntDetail || !enableMetadata || SYSTEM_FOLDERS.find(folderPath => path.startsWith(folderPath))) { + setRecord(null); + setOriginColumns([]); + setLoading(false); + return; + } + + const dirName = Utils.getDirName(path); + const fileName = Utils.getFileName(path); + let parentDir = direntType === 'file' ? dirName : dirName.slice(0, dirName.length - fileName.length - 1); + + if (!parentDir.startsWith('/')) { + parentDir = '/' + parentDir; + } + metadataAPI.getMetadataRecordInfo(repoID, parentDir, fileName).then(res => { + const { results, metadata } = res.data; + const record = Array.isArray(results) && results.length > 0 ? results[0] : {}; + const columns = normalizeFields(metadata).map(field => new Column(field)); + allColumnsRef.current = columns; + setRecord(record); + setOriginColumns(columns); + setLoading(false); + }).catch(error => { + const errMessage = Utils.getErrorMsg(error); + toaster.danger(errMessage); + setLoading(false); + }); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [enableMetadata, repoID, path, direntType, dirent, direntDetail]); + + useEffect(() => { + const eventBus = window?.sfMetadataContext?.eventBus; + if (!eventBus) return; + const unsubscribeLocalRecordChanged = eventBus.subscribe(EVENT_BUS_TYPE.LOCAL_RECORD_DETAIL_CHANGED, localRecordChanged); + return () => { + unsubscribeLocalRecordChanged(); + }; + }, [localRecordChanged]); + + return ( + <MetadataDetailsContext.Provider + value={{ + isLoading, + canModifyRecord, + canModifyDetails, + record, + columns, + onChange, + modifyColumnData, + updateFileTags, + modifyHiddenColumns, + modifyColumnOrder, + }} + > + {children} + </MetadataDetailsContext.Provider> + ); +}; + +export const useMetadataDetails = () => { + const context = useContext(MetadataDetailsContext); + if (!context) { + throw new Error('\'MetadataDetailsContext\' is null'); + } + return context; +}; diff --git a/frontend/src/metadata/utils/row/core.js b/frontend/src/metadata/utils/row/core.js index 145cfcc471c..f69e806ee6d 100644 --- a/frontend/src/metadata/utils/row/core.js +++ b/frontend/src/metadata/utils/row/core.js @@ -27,6 +27,7 @@ const updateTableRowsWithRowsData = (tables, tableId, recordsData = []) => { }; export const checkIsDir = (record) => { + if (!record) return false; const isDir = record[PRIVATE_COLUMN_KEY.IS_DIR]; if (typeof isDir === 'string') { return isDir.toUpperCase() === 'TRUE'; diff --git a/frontend/src/pages/markdown-editor/header-toolbar/header-toolbar.js b/frontend/src/pages/markdown-editor/header-toolbar/header-toolbar.js index 690120e6e8d..7f23efd2859 100644 --- a/frontend/src/pages/markdown-editor/header-toolbar/header-toolbar.js +++ b/frontend/src/pages/markdown-editor/header-toolbar/header-toolbar.js @@ -17,7 +17,7 @@ import Dirent from '../../../../src/models/dirent'; import '../css/header-toolbar.css'; const { seafileCollabServer } = window.app.config; -const { canDownloadFile, repoID, filePath } = window.app.pageOptions; +const { canDownloadFile, repoID, filePath, isRepoAdmin } = window.app.pageOptions; const propTypes = { editorApi: PropTypes.object.isRequired, @@ -95,7 +95,7 @@ class HeaderToolbar extends React.Component { }; onArticleInfoToggle = () => { - const repoInfo = { permission: this.currentDirent.permission }; + const repoInfo = { permission: this.currentDirent.permission, is_admin: isRepoAdmin, }; const eventBus = EventBus.getInstance(); eventBus.dispatch(EXTERNAL_EVENTS.ON_ARTICLE_INFO_TOGGLE, this.isFileInfoShow ? null : { diff --git a/frontend/src/pages/sdoc/sdoc-editor/index.css b/frontend/src/pages/sdoc/sdoc-editor/index.css index 66584c35223..d152a3dd188 100644 --- a/frontend/src/pages/sdoc/sdoc-editor/index.css +++ b/frontend/src/pages/sdoc/sdoc-editor/index.css @@ -15,6 +15,16 @@ background-color: inherit; } +.sdoc-content-right-panel .detail-header .seafile-multicolor-icon-set-up { + fill: #999; + font-weight: 700; + font-size: 16px; +} + +.sdoc-content-right-panel .detail-header .detail-control:hover .seafile-multicolor-icon-set-up { + fill: #5a5a5a; +} + .sdoc-content-right-panel .detail-header .sdoc-sm-close { color: #999; font-weight: 700; diff --git a/frontend/src/pages/sdoc/sdoc-editor/index.js b/frontend/src/pages/sdoc/sdoc-editor/index.js index ea6beab5d63..5742a7d223e 100644 --- a/frontend/src/pages/sdoc/sdoc-editor/index.js +++ b/frontend/src/pages/sdoc/sdoc-editor/index.js @@ -16,7 +16,7 @@ const SdocEditor = () => { const [currentDirent, setCurrentDirent] = useState(null); const { collaborators } = useCollaborators(); const plugins = useMemo(() => { - const { repoID, docPath, docPerm } = window.seafile; + const { repoID, docPath, docPerm, isRepoAdmin } = window.seafile; return [ { name: 'sdoc-info', @@ -29,7 +29,7 @@ const SdocEditor = () => { repoID={repoID} path={docPath} dirent={currentDirent} - repoInfo={{ permission: docPerm }} + repoInfo={{ permission: docPerm, is_admin: isRepoAdmin }} width={width - 1} component={{ headerComponent: { diff --git a/frontend/src/view-file-sdoc.js b/frontend/src/view-file-sdoc.js index e30a45fa9a5..c1087f17b22 100644 --- a/frontend/src/view-file-sdoc.js +++ b/frontend/src/view-file-sdoc.js @@ -15,11 +15,12 @@ const { repoID, repoName, repoEncrypted, parentDir, filePerm, docPath, docName, docUuid, seadocAccessToken, seadocServerUrl, assetsUrl, isSdocRevision, isPublished, originFilename, revisionCreatedAt, originFileVersion, - originFilePath, originDocUuid, revisionId, isFreezed, mobileLogin + originFilePath, originDocUuid, revisionId, isFreezed, mobileLogin, isRepoAdmin } = window.app.pageOptions; window.seafile = { repoID, + isRepoAdmin, docPath, docName, docUuid, @@ -52,7 +53,7 @@ window.seafile = { mobileLogin, }; -const repoInfo = { encrypted: repoEncrypted, permission: filePerm }; +const repoInfo = { encrypted: repoEncrypted, permission: filePerm, is_admin: isRepoAdmin }; ReactDom.render( <I18nextProvider i18n={ i18n } > diff --git a/media/css/sf_font3/iconfont.css b/media/css/sf_font3/iconfont.css index d0684766eaf..def6c7fe635 100644 --- a/media/css/sf_font3/iconfont.css +++ b/media/css/sf_font3/iconfont.css @@ -1,11 +1,11 @@ @font-face { font-family: "sf3-font"; /* Project id 1230969 */ - src: url('iconfont.eot?t=1732614348756'); /* IE9 */ - src: url('iconfont.eot?t=1732614348756#iefix') format('embedded-opentype'), /* IE6-IE8 */ - url('iconfont.woff2?t=1732614348756') format('woff2'), - url('iconfont.woff?t=1732614348756') format('woff'), - url('iconfont.ttf?t=1732614348756') format('truetype'), - url('iconfont.svg?t=1732614348756#sf3-font') format('svg'); + src: url('./iconfont.eot?t=1733301127109'); /* IE9 */ + src: url('./iconfont.eot?t=1733301127109#iefix') format('embedded-opentype'), /* IE6-IE8 */ + url('./iconfont.woff2?t=1733301127109') format('woff2'), + url('./iconfont.woff?t=1733301127109') format('woff'), + url('./iconfont.ttf?t=1733301127109') format('truetype'), + url('./iconfont.svg?t=1733301127109#sf3-font') format('svg'); } .sf3-font { diff --git a/media/css/sf_font3/iconfont.eot b/media/css/sf_font3/iconfont.eot index d4fb6fdc870..4bb9fed7ba7 100644 Binary files a/media/css/sf_font3/iconfont.eot and b/media/css/sf_font3/iconfont.eot differ diff --git a/media/css/sf_font3/iconfont.svg b/media/css/sf_font3/iconfont.svg index afa771ec951..d40bdac5381 100644 --- a/media/css/sf_font3/iconfont.svg +++ b/media/css/sf_font3/iconfont.svg @@ -150,7 +150,7 @@ <glyph glyph-name="copy1" unicode="" d="M704 640c35.2 0 64-28.8 64-64v-352h96c16 0 28.8 12.8 32 28.8V736c0 19.2-12.8 32-32 32H384c-19.2 0-32-12.8-32-32v-96h352z m-44.8-96H140.8c-6.4 0-12.8-6.4-12.8-12.8v-518.4c0-6.4 6.4-12.8 12.8-12.8h518.4c9.6 0 12.8 6.4 12.8 12.8V531.2c0 6.4-6.4 12.8-12.8 12.8zM256 800c0 35.2 28.8 64 64 64h608c35.2 0 64-28.8 64-64v-608c0-35.2-28.8-64-64-64h-160v-160c0-35.2-25.6-60.8-60.8-64H96c-35.2 0-64 28.8-64 64V576c0 35.2 28.8 64 64 64h160V800z" horiz-adv-x="1024" /> - <glyph glyph-name="trash" unicode="" d="M595.2 73.6v38.4c0 16-12.8 28.8-28.8 28.8s-28.8-6.4-35.2-12.8l-96-96c57.6-67.2 89.6-105.6 96-108.8 9.6-9.6 19.2-9.6 32-9.6s32 9.6 32 28.8v41.6H928c35.2 0 64 28.8 64 64 0 9.6-3.2 22.4-6.4 32l-156.8 278.4-12.8-67.2-67.2 22.4 128-236.8h-281.6zM288 320l35.2-19.2c12.8-6.4 32-3.2 41.6 6.4s9.6 25.6 6.4 38.4c-3.2 9.6-16 54.4-35.2 128L192 448c-16-3.2-25.6-9.6-32-22.4-6.4-12.8 0-32 12.8-41.6l35.2-19.2-166.4-288c-19.2-28.8-9.6-67.2 22.4-86.4 9.6-6.4 19.2-9.6 28.8-9.6l320-6.4-54.4 44.8 54.4 44.8-268.8 9.6L288 320z m371.2 147.2l-35.2-19.2c-16-9.6-19.2-28.8-12.8-44.8 3.2-6.4 16-16 25.6-19.2l140.8-35.2 48 128c6.4 16 9.6 35.2-9.6 51.2-12.8 12.8-32 6.4-38.4 3.2l-38.4-19.2-166.4 288c-16 28.8-54.4 41.6-86.4 22.4-9.6-6.4-16-12.8-22.4-22.4l-163.2-275.2 67.2 22.4 12.8-67.2 140.8 227.2 137.6-240z" horiz-adv-x="1025" /> + <glyph glyph-name="trash" unicode="" d="M601.8512 54.4V96c0 19.2-12.8 32-32 32s-28.8-6.4-38.4-12.8l-102.4-102.4c60.8-73.6 96-112 102.4-118.4 9.6-9.6 19.2-12.8 35.2-12.8 12.8 0 35.2 9.6 35.2 28.8v44.8h355.2c38.4 0 67.2 28.8 67.2 67.2 0 12.8-3.2 22.4-9.6 32L848.2512 352l-12.8-73.6-70.4 25.6 134.4-252.8h-297.6zM272.2512 320l35.2-19.2c12.8-6.4 35.2-3.2 44.8 9.6 9.6 12.8 9.6 25.6 6.4 41.6-3.2 9.6-16 57.6-38.4 137.6l-153.6-28.8c-16-3.2-28.8-9.6-32-22.4s-3.2-35.2 16-44.8l38.4-22.4L9.8512 64c-19.2-38.4-9.6-76.8 22.4-96 9.6-6.4 22.4-9.6 32-9.6l342.4-6.4-54.4 51.2 57.6 48-284.8 9.6L272.2512 320z m396.8 153.6l-35.2-22.4c-16-9.6-22.4-32-12.8-48 3.2-6.4 16-16 28.8-19.2l147.2-38.4 51.2 137.6c6.4 16 9.6 38.4-9.6 57.6-16 12.8-35.2 6.4-41.6 3.2l-41.6-22.4L576.2512 832c-19.2 35.2-57.6 48-89.6 28.8-9.6-6.4-19.2-16-25.6-25.6l-176-294.4 70.4 25.6 12.8-73.6L518.6512 736l150.4-262.4z" horiz-adv-x="1025" /> <glyph glyph-name="history" unicode="" d="M512 864C350 864 209 786 122 663V744c0 24-21 45-45 45S32 768 32 744v-180c0-24 21-45 45-45H272c15 0 33 9 39 21 9 15 9 33 0 45-6 15-24 24-39 24H194C263 708 380 774 512 774c216 0 390-174 390-390s-174-390-390-390c-183 0-333 123-378 291-6 18-21 30-42 30-12 0-24-6-33-12s-12-21-12-33c0-3 0-9 3-12v-6C107 51 293-96 512-96c264 0 480 216 480 480S776 864 512 864z m0-201c-12 0-24-6-33-12-9-9-12-21-12-33v-270c0-12 6-24 12-33 9-9 21-12 33-12h195c15 0 30 9 39 21s9 30 0 45-24 24-39 21h-150V618c0 12-6 24-12 33-9 6-21 12-33 12z" horiz-adv-x="1024" /> @@ -190,7 +190,7 @@ <glyph glyph-name="search" unicode="" d="M448.099844 896c246.015601 0 447.301092-201.285491 447.301092-447.301092 0-95.850234-28.75507-182.115445-79.875195-255.600624l182.115445-182.115445c31.950078-31.950078 31.950078-83.070203 0-115.02028s-83.070203-31.950078-115.020281 0L703.700468 78.078003c-73.485179-51.120125-159.75039-79.875195-255.600624-79.875195-246.015601 0-447.301092 201.285491-447.301092 447.301092C0.798752 694.714509 202.084243 896 448.099844 896z m0-127.800312C272.374415 768.199688 128.599064 624.424337 128.599064 448.698908s143.775351-319.50078 319.50078-319.50078 319.50078 143.775351 319.50078 319.50078S623.825273 768.199688 448.099844 768.199688z" horiz-adv-x="1024" /> - <glyph glyph-name="set-up" unicode="" d="M512 592c115.2 0 208-92.8 208-208s-92.8-208-208-208-208 92.8-208 208 92.8 208 208 208z m0-128c-44.8 0-80-35.2-80-80s35.2-80 80-80 80 35.2 80 80-35.2 80-80 80zM432 838.4c-9.6 19.2-32 28.8-51.2 22.4-80-19.2-150.4-60.8-208-112-12.8-16-16-35.2-9.6-54.4 6.4-12.8 9.6-25.6 9.6-41.6 0-51.2-41.6-92.8-89.6-92.8-19.2 0-38.4-12.8-41.6-32C22.4 486.4 16 435.2 16 384c0-35.2 3.2-67.2 9.6-99.2 3.2-22.4 25.6-38.4 48-35.2h9.6c51.2 0 89.6-41.6 89.6-92.8 0-22.4-9.6-41.6-22.4-57.6-16-16-12.8-41.6 3.2-60.8 57.6-60.8 131.2-105.6 214.4-131.2 22.4-6.4 48 6.4 54.4 28.8 12.8 38.4 44.8 64 86.4 64s73.6-25.6 86.4-64c6.4-22.4 32-35.2 54.4-28.8 83.2 25.6 156.8 70.4 214.4 131.2 16 16 16 41.6 3.2 60.8-12.8 16-22.4 35.2-22.4 57.6 0 51.2 41.6 92.8 89.6 92.8h9.6c22.4-3.2 44.8 12.8 48 35.2 6.4 32 9.6 67.2 9.6 99.2 0 51.2-6.4 102.4-22.4 150.4-6.4 19.2-22.4 32-41.6 32-51.2 0-89.6 41.6-89.6 92.8 0 16 3.2 28.8 9.6 41.6 9.6 16 3.2 38.4-9.6 51.2-57.6 54.4-128 92.8-204.8 115.2-19.2 0-41.6-9.6-51.2-28.8-16-28.8-44.8-51.2-80-51.2s-67.2 22.4-80 51.2zM288 633.6c0 16-3.2 28.8-6.4 41.6 32 25.6 67.2 44.8 105.6 60.8 28.8-38.4 73.6-60.8 124.8-60.8s96 22.4 124.8 60.8c38.4-12.8 73.6-35.2 105.6-60.8-3.2-12.8-6.4-28.8-6.4-41.6 0-80 57.6-150.4 134.4-163.2 6.4-28.8 9.6-57.6 9.6-86.4 0-16 0-28.8-3.2-44.8-80-9.6-140.8-80-140.8-163.2 0-25.6 6.4-51.2 16-73.6-32-28.8-67.2-51.2-105.6-67.2-28.8 48-80 76.8-137.6 76.8s-108.8-32-137.6-76.8c-38.4 16-73.6 38.4-105.6 67.2 9.6 22.4 16 48 16 73.6 0 83.2-60.8 153.6-140.8 163.2 3.2 12.8 3.2 28.8 3.2 41.6 0 28.8 3.2 57.6 9.6 86.4C230.4 483.2 288 550.4 288 633.6z" horiz-adv-x="1024" /> + <glyph glyph-name="set-up" unicode="" d="M512 576c105.6 0 192-86.4 192-192s-86.4-192-192-192-192 86.4-192 192 86.4 192 192 192z m0-96c-54.4 0-96-41.6-96-96s41.6-96 96-96 96 41.6 96 96-41.6 96-96 96zM432 835.2c-9.6 19.2-32 28.8-54.4 22.4-83.2-19.2-153.6-60.8-214.4-112-12.8-16-16-35.2-9.6-54.4 6.4-12.8 9.6-25.6 9.6-41.6 0-51.2-44.8-92.8-92.8-92.8-19.2 0-38.4-12.8-44.8-32-19.2-41.6-25.6-92.8-25.6-144 0-35.2 3.2-67.2 9.6-99.2 3.2-22.4 25.6-38.4 51.2-35.2h9.6c54.4 0 92.8-41.6 92.8-92.8 0-22.4-9.6-41.6-22.4-57.6-16-16-12.8-41.6 3.2-60.8 60.8-60.8 137.6-105.6 224-131.2 22.4-6.4 51.2 6.4 57.6 28.8 12.8 38.4 48 64 89.6 64s76.8-25.6 89.6-64c6.4-22.4 32-35.2 57.6-28.8 86.4 25.6 163.2 70.4 224 131.2 16 16 16 41.6 3.2 60.8-12.8 16-22.4 35.2-22.4 57.6 0 51.2 44.8 92.8 92.8 92.8h9.6c22.4-3.2 48 12.8 51.2 35.2 6.4 32 9.6 67.2 9.6 99.2 0 51.2-6.4 102.4-22.4 150.4-6.4 19.2-22.4 32-44.8 32-54.4 0-92.8 41.6-92.8 92.8 0 16 3.2 28.8 9.6 41.6 3.2 16-3.2 38.4-16 51.2-60.8 54.4-134.4 92.8-211.2 115.2-19.2 0-44.8-9.6-54.4-28.8-16-28.8-48-51.2-83.2-51.2s-70.4 22.4-83.2 51.2zM262.4 656c0 16-3.2 32-6.4 44.8 35.2 28.8 76.8 48 118.4 67.2 32-41.6 83.2-67.2 140.8-67.2 57.6 0 108.8 25.6 140.8 67.2 44.8-12.8 83.2-38.4 118.4-67.2-3.2-12.8-6.4-32-6.4-44.8 0-86.4 64-166.4 150.4-179.2 6.4-32 9.6-64 9.6-96 0-16 0-32-3.2-48-89.6-9.6-160-86.4-160-179.2 0-28.8 6.4-57.6 19.2-80-35.2-32-76.8-57.6-118.4-73.6-32 51.2-89.6 83.2-153.6 83.2s-121.6-35.2-153.6-83.2c-44.8 16-83.2 41.6-118.4 73.6 6.4 25.6 16 51.2 16 80 0 89.6-67.2 169.6-160 179.2 3.2 12.8 3.2 32 3.2 44.8 0 32 3.2 64 9.6 96 89.6 16 153.6 89.6 153.6 182.4z" horiz-adv-x="1024" /> <glyph glyph-name="language" unicode="" d="M982.4 582.4C905.6 768 723.2 896 512 896 230.4 896 0 665.6 0 384s230.4-512 512-512 512 230.4 512 512c0 96-41.6 198.4-41.6 198.4zM76.8 384c0 54.4 9.6 105.6 28.8 153.6 22.4-12.8 54.4-16 54.4-41.6 0-86.4 3.2-179.2 86.4-179.2 3.2 0 44.8-16 67.2-67.2 6.4-19.2 35.2 0 67.2 0 16 0 0-25.6 0-80s121.6-134.4 121.6-134.4c0-35.2 0-64 3.2-86.4C265.6-48 76.8 147.2 76.8 384z m540.8-422.4c16 70.4 25.6 108.8 60.8 137.6 51.2 41.6 6.4 89.6-32 83.2-32-3.2-12.8 35.2-38.4 38.4-28.8 3.2-64 54.4-102.4 73.6-22.4 9.6-41.6 35.2-73.6 35.2-28.8 0-70.4-22.4-70.4-3.2 0 60.8-6.4 102.4-6.4 121.6 0 12.8-9.6 3.2 28.8 3.2 22.4 0 9.6 41.6 32 41.6 19.2 0 70.4-19.2 83.2-9.6 12.8 6.4 86.4-185.6 86.4-32 0 19.2-9.6 48 0 67.2 38.4 67.2 76.8 124.8 73.6 131.2-3.2 3.2-41.6 9.6-70.4 0-9.6-3.2 3.2-19.2-12.8-22.4-57.6-12.8-105.6 12.8-86.4 38.4 16 25.6 83.2 9.6 86.4 60.8 3.2 28.8 6.4 64 6.4 86.4 156.8-22.4 284.8-128 339.2-272-147.2-108.8-108.8-182.4-60.8-227.2 25.6-22.4 51.2-57.6 67.2-83.2-57.6-128-169.6-233.6-310.4-268.8z" horiz-adv-x="1024" /> diff --git a/media/css/sf_font3/iconfont.ttf b/media/css/sf_font3/iconfont.ttf index 62b77ed106f..0a1dae1a5d6 100644 Binary files a/media/css/sf_font3/iconfont.ttf and b/media/css/sf_font3/iconfont.ttf differ diff --git a/media/css/sf_font3/iconfont.woff b/media/css/sf_font3/iconfont.woff index fe823786dc8..77a4a0b7a7f 100644 Binary files a/media/css/sf_font3/iconfont.woff and b/media/css/sf_font3/iconfont.woff differ diff --git a/media/css/sf_font3/iconfont.woff2 b/media/css/sf_font3/iconfont.woff2 index 5a753e5c8fa..48dbc69dbe0 100644 Binary files a/media/css/sf_font3/iconfont.woff2 and b/media/css/sf_font3/iconfont.woff2 differ diff --git a/seahub/repo_metadata/apis.py b/seahub/repo_metadata/apis.py index 215fb007b40..d8564d6835b 100644 --- a/seahub/repo_metadata/apis.py +++ b/seahub/repo_metadata/apis.py @@ -50,10 +50,14 @@ def get(self, request, repo_id): is_enabled = False is_tags_enabled = False tags_lang = '' + details_settings = '{}' try: record = RepoMetadata.objects.filter(repo_id=repo_id).first() if record and record.enabled: is_enabled = True + details_settings = record.details_settings + if not details_settings: + details_settings = '{}' if record and record.tags_enabled: is_tags_enabled = True tags_lang = record.tags_lang @@ -66,6 +70,7 @@ def get(self, request, repo_id): 'enabled': is_enabled, 'tags_enabled': is_tags_enabled, 'tags_lang': tags_lang, + 'details_settings': details_settings }) def put(self, request, repo_id): @@ -148,6 +153,7 @@ def delete(self, request, repo_id): record.enabled = False record.face_recognition_enabled = False record.tags_enabled = False + record.details_settings = '{}' record.save() RepoMetadataViews.objects.filter(repo_id=repo_id).delete() except Exception as e: @@ -157,6 +163,48 @@ def delete(self, request, repo_id): return Response({'success': True}) + +class MetadataDetailsSettingsView(APIView): + authentication_classes = (TokenAuthentication, SessionAuthentication) + permission_classes = (IsAuthenticated, ) + throttle_classes = (UserRateThrottle, ) + + def put(self, request, repo_id): + settings = request.data.get('settings', {}) + if not settings: + error_msg = 'settings invalid.' + return api_error(status.HTTP_400_BAD_REQUEST, error_msg) + + # resource check + repo = seafile_api.get_repo(repo_id) + if not repo: + error_msg = f'Library {repo_id} not found.' + return api_error(status.HTTP_404_NOT_FOUND, error_msg) + + if not is_repo_admin(request.user.username, repo_id): + error_msg = 'Permission denied.' + return api_error(status.HTTP_403_FORBIDDEN, error_msg) + + metadata = RepoMetadata.objects.filter(repo_id=repo_id).first() + if not metadata or not metadata.enabled: + error_msg = f'The metadata module is not enabled for repo {repo_id}.' + return api_error(status.HTTP_404_NOT_FOUND, error_msg) + + old_details_settings = metadata.details_settings if metadata.details_settings else '{}' + old_details_settings = json.loads(old_details_settings) + if not old_details_settings: + old_details_settings = {} + + old_details_settings.update(settings) + try: + metadata.details_settings = json.dumps(old_details_settings) + metadata.save() + except Exception as e: + logger.exception(e) + return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, 'Internal Server Error') + + return Response({'success': True}) + class MetadataRecords(APIView): authentication_classes = (TokenAuthentication, SessionAuthentication) permission_classes = (IsAuthenticated, ) @@ -831,6 +879,7 @@ def post(self, request, repo_id): try: results = RepoMetadataViews.objects.move_view(repo_id, view_id, target_view_id) except Exception as e: + logger.error(e) error_msg = 'Internal Server Error' return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg) @@ -1184,7 +1233,7 @@ def post(self, request, repo_id): return Response({'task_id': task_id}) def delete(self, request, repo_id): - # recource check + # resource check repo = seafile_api.get_repo(repo_id) if not repo: error_msg = 'Library %s not found.' % repo_id @@ -1393,13 +1442,11 @@ def get(self, request, repo_id): return Response(query_result) - - def post(self, request, repo_id): tags_data = request.data.get('tags_data', []) if not tags_data: - error_msg = f'Tags data is required.' + error_msg = 'Tags data is required.' return api_error(status.HTTP_400_BAD_REQUEST, error_msg) metadata = RepoMetadata.objects.filter(repo_id=repo_id).first() @@ -1546,7 +1593,7 @@ def delete(self, request, repo_id): tag_ids = request.data.get('tag_ids', []) if not tag_ids: - error_msg = f'Tag ids is required.' + error_msg = 'Tag ids is required.' return api_error(status.HTTP_400_BAD_REQUEST, error_msg) metadata = RepoMetadata.objects.filter(repo_id=repo_id).first() @@ -1722,13 +1769,13 @@ def get(self, request, repo_id, tag_id): tag_files_records = tag_query.get('results', []) if not tag_files_records: - return Response({ 'metadata': [], 'results': [] }) + return Response({'metadata': [], 'results': []}) tag_files_record = tag_files_records[0] tag_files_record_ids = tag_files_record.get(TAGS_TABLE.columns.file_links.name , []) if not tag_files_record_ids: - return Response({ 'metadata': [], 'results': [] }) + return Response({'metadata': [], 'results': []}) tag_files_sql = 'SELECT `%s`, `%s`, `%s`, `%s`, `%s`, `%s` FROM %s WHERE `%s` IN (%s)' % (METADATA_TABLE.columns.id.name, METADATA_TABLE.columns.file_name.name, \ METADATA_TABLE.columns.parent_dir.name, METADATA_TABLE.columns.size.name, \ @@ -1743,4 +1790,3 @@ def get(self, request, repo_id, tag_id): return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg) return Response(tag_files_query) - diff --git a/seahub/repo_metadata/models.py b/seahub/repo_metadata/models.py index 078f33f8867..5f0dbf9f930 100644 --- a/seahub/repo_metadata/models.py +++ b/seahub/repo_metadata/models.py @@ -68,6 +68,7 @@ class RepoMetadata(models.Model): tags_enabled = models.BooleanField(db_index=True) tags_lang = models.CharField(max_length=36) last_face_cluster_time = models.DateTimeField(db_index=True, blank=True, null=True) + details_settings = models.TextField() objects = RepoMetadataManager() diff --git a/seahub/repo_metadata/urls.py b/seahub/repo_metadata/urls.py index d44ddabbdb4..62d8f88e2ed 100644 --- a/seahub/repo_metadata/urls.py +++ b/seahub/repo_metadata/urls.py @@ -2,7 +2,7 @@ from .apis import MetadataRecords, MetadataManage, MetadataColumns, MetadataRecordInfo, \ MetadataViews, MetadataViewsMoveView, MetadataViewsDetailView, MetadataViewsDuplicateView, FacesRecords, \ FaceRecognitionManage, FacesRecord, MetadataExtractFileDetails, PeoplePhotos, MetadataTagsStatusManage, MetadataTags, \ - MetadataFileTags, MetadataTagFiles + MetadataFileTags, MetadataTagFiles, MetadataDetailsSettingsView urlpatterns = [ re_path(r'^$', MetadataManage.as_view(), name='api-v2.1-metadata'), @@ -24,6 +24,9 @@ re_path(r'^extract-file-details/$', MetadataExtractFileDetails.as_view(), name='api-v2.1-metadata-extract-file-details'), + # details settings + re_path(r'^details-settings/', MetadataDetailsSettingsView.as_view(), name='api-v2.1-metadata-details-settings'), + # tags api re_path(r'^tags-status/$', MetadataTagsStatusManage.as_view(), name='api-v2.1-metadata-tags-status'), re_path(r'^tags/$', MetadataTags.as_view(), name='api-v2.1-metadata-tags'), diff --git a/seahub/templates/file_view_react.html b/seahub/templates/file_view_react.html index 9f1270036f8..678ad5100a6 100644 --- a/seahub/templates/file_view_react.html +++ b/seahub/templates/file_view_react.html @@ -30,6 +30,7 @@ repoID: '{{ repo.id }}', repoName: '{{ repo.name|escapejs }}', repoEncrypted: {% if repo.encrypted %}true{% else %}false{% endif %}, + isRepoAdmin: {% if is_repo_admin %}true{% else %}false{% endif %}, filePath: '{{ path|escapejs }}', filePerm: '{{ file_perm }}', fileType: '{{ filetype }}', diff --git a/seahub/templates/markdown_file_view_react.html b/seahub/templates/markdown_file_view_react.html index d18e7a64a80..334dba81502 100644 --- a/seahub/templates/markdown_file_view_react.html +++ b/seahub/templates/markdown_file_view_react.html @@ -39,6 +39,7 @@ repoID: '{{ repo.id }}', repoName: '{{ repo.name|escapejs }}', repoEncrypted: {% if repo.encrypted %}true{% else %}false{% endif %}, + isRepoAdmin: {% if is_repo_admin %}true{% else %}false{% endif %}, filePath: '{{ path|escapejs }}', fileName: '{{ filename|escapejs }}', filePerm: '{{ file_perm }}', diff --git a/seahub/views/file.py b/seahub/views/file.py index 740fb559cb9..2dc89fcaef6 100644 --- a/seahub/views/file.py +++ b/seahub/views/file.py @@ -68,7 +68,7 @@ ONLINE_OFFICE_LOCK_OWNER, if_locked_by_online_office from seahub.views import check_folder_permission, \ get_unencry_rw_repos_by_user -from seahub.utils.repo import is_repo_owner, parse_repo_perm +from seahub.utils.repo import is_repo_owner, parse_repo_perm, is_repo_admin from seahub.group.utils import is_group_member from seahub.thumbnail.utils import extract_xmind_image, get_thumbnail_src, \ XMIND_IMAGE_SIZE, get_share_link_thumbnail_src, get_thumbnail_image_path @@ -567,6 +567,7 @@ def view_lib_file(request, repo_id, path): 'file_id': file_id, 'last_commit_id': repo.head_cmmt_id, 'is_repo_owner': is_repo_owner(request, repo_id, username), + 'is_repo_admin': is_repo_admin(username, repo_id), 'path': path, 'parent_dir': parent_dir, 'filename': filename,