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

feat: metadata details settings (#7145)

* feat: metadata details settings

* feat: optimize code

* feat: optimize code

* feat: optimize code

* feat: optimize code

* feat: optimize code

* feat: optimize code

* feat: optimize code

---------

Co-authored-by: 杨国璇 <ygx@Hello-word.local>
Co-authored-by: 杨国璇 <ygx@192.168.1.6>
This commit is contained in:
杨国璇
2024-12-09 16:56:29 +08:00
committed by GitHub
parent 3f6c4f3d26
commit ac124d20ba
45 changed files with 677 additions and 409 deletions

View File

@@ -1 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1721786317709" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="22672" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M512 304c115.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-80z" p-id="22673"></path><path d="M432 57.6c-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 409.6 16 460.8 16 512c0 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 262.4c0-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 412.8 288 345.6 288 262.4z" p-id="22674"></path></svg>
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1733301494152" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="6527" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M512 320c105.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-96z" p-id="6528"></path><path d="M432 60.8c-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 240c0-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" p-id="6529"></path></svg>

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@@ -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 {

View File

@@ -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-header">
<div className="detail-title dirent-title">
<div className="detail-header-icon-container">
<img src={icon} width={iconSize} height={iconSize} alt="" />
<Title title={title} icon={icon} iconSize={iconSize} />
<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,
};

View File

@@ -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;
}

View File

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

View File

@@ -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 />
)}
</>
);

View File

@@ -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 />
)}
</>
);

View File

@@ -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>
);
}
}

View File

@@ -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) => {

View File

@@ -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,
};

View File

@@ -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>
);
};

View File

@@ -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}

View File

@@ -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 }>

View File

@@ -38,3 +38,8 @@ export const MODE_TYPE_MAP = {
RECENTLY_USED: 'recently_used',
SEARCH_RESULTS: 'search_results',
};
export const SYSTEM_FOLDERS = [
'/_Internal',
'/images'
];

View File

@@ -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}

View File

@@ -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 });

View File

@@ -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,
};

View File

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

View File

@@ -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>
)}

View File

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

View File

@@ -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 {

View File

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

View File

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

View File

@@ -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: '';

View File

@@ -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>

View File

@@ -1,2 +1,3 @@
export { MetadataProvider, useMetadata } from './metadata';
export { CollaboratorsProvider, useCollaborators } from './collaborators';
export { MetadataDetailsProvider, useMetadataDetails } from './metadata-details';

View File

@@ -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;
};

View File

@@ -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';

View File

@@ -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 : {

View File

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

View File

@@ -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: {

View File

@@ -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 } >