mirror of
https://github.com/haiwen/seahub.git
synced 2025-09-14 06:11:16 +00:00
feat: metadata detail editor (#6562)
* feat: metadata detail editor * feat: update code * feat: update code --------- Co-authored-by: 杨国璇 <ygx@Hello-word.local>
This commit is contained in:
@@ -12,12 +12,12 @@
|
||||
|
||||
.dirent-detail-item .dirent-detail-item-name {
|
||||
width: 160px;
|
||||
padding: 7px 6px;
|
||||
padding: 6.5px 6px;
|
||||
min-height: 34px;
|
||||
height: fit-content;
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
line-height: 1.4;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.dirent-detail-item .dirent-detail-item-name .sf-metadata-icon {
|
||||
@@ -29,13 +29,8 @@
|
||||
.dirent-detail-item .dirent-detail-item-value {
|
||||
width: 200px;
|
||||
display: flex;
|
||||
padding: 7px 6px;
|
||||
min-height: 34px;
|
||||
height: fit-content;
|
||||
}
|
||||
|
||||
.dirent-detail-item .dirent-detail-item-value.editable:hover {
|
||||
cursor: pointer;
|
||||
min-height: 34px;
|
||||
}
|
||||
|
||||
.dirent-detail-item .dirent-detail-item-name:hover,
|
||||
@@ -45,10 +40,48 @@
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.dirent-detail-item .dirent-detail-item-value.editable:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* media */
|
||||
.cur-view-detail-small .dirent-detail-item .dirent-detail-item-name {
|
||||
width: calc((100% - 8px) * 0.44);
|
||||
}
|
||||
|
||||
.cur-view-detail-small .dirent-detail-item .dirent-detail-item-value {
|
||||
width: calc((100% - 8px) * 0.56);
|
||||
}
|
||||
|
||||
.cur-view-detail-large .dirent-detail-item .dirent-detail-item-name {
|
||||
width: 160px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.cur-view-detail-large .dirent-detail-item .dirent-detail-item-value {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.dirent-detail-item .dirent-detail-item-value:not(.editable) .sf-metadata-record-cell-empty {
|
||||
display: inline-block;
|
||||
height: 34px;
|
||||
width: 100%;
|
||||
line-height: 1.5;
|
||||
padding: 6.5px 6px;
|
||||
}
|
||||
|
||||
.dirent-detail-item .dirent-detail-item-value:not(.editable) .sf-metadata-record-cell-empty:empty::before {
|
||||
content: attr(placeholder);
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* formatter */
|
||||
.dirent-detail-item .dirent-detail-item-value .text-formatter,
|
||||
.dirent-detail-item .dirent-detail-item-value .ctime-formatter,
|
||||
.dirent-detail-item .dirent-detail-item-value .mtime-formatter,
|
||||
.dirent-detail-item .dirent-detail-item-value .date-formatter {
|
||||
padding: 6.5px 6px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
@@ -57,28 +90,5 @@
|
||||
}
|
||||
|
||||
.dirent-detail-item-value .creator-formatter {
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.dirent-detail-item-value .sf-metadata-record-cell-empty::before {
|
||||
content: attr(placeholder);
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* */
|
||||
.cur-view-detail-small .dirent-detail-item .dirent-detail-item-name {
|
||||
width: 44%;
|
||||
}
|
||||
|
||||
.cur-view-detail-small .dirent-detail-item .dirent-detail-item-value {
|
||||
width: 56%;
|
||||
}
|
||||
|
||||
.cur-view-detail-large .dirent-detail-item .dirent-detail-item-name {
|
||||
width: 160px;
|
||||
}
|
||||
|
||||
.cur-view-detail-large .dirent-detail-item .dirent-detail-item-value {
|
||||
flex: 1;
|
||||
padding: 7px 6px;
|
||||
}
|
||||
|
@@ -1,40 +1,39 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Formatter, Icon } from '@seafile/sf-metadata-ui-component';
|
||||
import classnames from 'classnames';
|
||||
import { Icon } from '@seafile/sf-metadata-ui-component';
|
||||
import { CellType, COLUMNS_ICON_CONFIG } from '../../../metadata/metadata-view/_basic';
|
||||
import { gettext } from '../../../utils/constants';
|
||||
|
||||
import './index.css';
|
||||
|
||||
const DetailItem = ({ field, value, valueId, valueClick, children, ...params }) => {
|
||||
const DetailItem = ({ readonly, field, className, children }) => {
|
||||
const icon = useMemo(() => {
|
||||
if (field.type === 'size') return COLUMNS_ICON_CONFIG[CellType.NUMBER];
|
||||
return COLUMNS_ICON_CONFIG[field.type];
|
||||
}, [field]);
|
||||
|
||||
return (
|
||||
<div className="dirent-detail-item">
|
||||
<div className={classnames('dirent-detail-item', className)}>
|
||||
<div className="dirent-detail-item-name">
|
||||
<Icon iconName={icon} />
|
||||
<span className="dirent-detail-item-name-value">{field.name}</span>
|
||||
</div>
|
||||
<div className={classnames('dirent-detail-item-value', { 'editable': valueClick })} id={valueId} onClick={valueClick}>
|
||||
{children ? children : (<Formatter { ...params } field={field} value={value} />)}
|
||||
<div className={classnames('dirent-detail-item-value', { 'editable': !readonly })} >
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
DetailItem.defaultProps = {
|
||||
emptyTip: gettext('Empty')
|
||||
readonly: true,
|
||||
};
|
||||
|
||||
DetailItem.propTypes = {
|
||||
readonly: PropTypes.bool,
|
||||
field: PropTypes.object.isRequired,
|
||||
value: PropTypes.any,
|
||||
className: PropTypes.string,
|
||||
children: PropTypes.any,
|
||||
valueId: PropTypes.string,
|
||||
};
|
||||
|
||||
export default DetailItem;
|
||||
|
@@ -1,20 +1,26 @@
|
||||
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/metadata-view/_basic';
|
||||
import { gettext } from '../../../utils/constants';
|
||||
import { MetadataDetails, useMetadata } from '../../../metadata';
|
||||
|
||||
const DirDetails = ({ repoID, repoInfo, dirent, path, direntDetail, ...params }) => {
|
||||
const DirDetails = ({ repoID, repoInfo, dirent, path, direntDetail }) => {
|
||||
const direntPath = useMemo(() => getDirentPath(dirent, path), [dirent, path]);
|
||||
const { enableMetadata } = useMetadata();
|
||||
const lastModifiedTimeField = useMemo(() => {
|
||||
return { type: CellType.MTIME, name: gettext('Last modified time') };
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<DetailItem field={{ type: CellType.MTIME, name: gettext('Last modified time') }} value={direntDetail.mtime} />
|
||||
<DetailItem field={lastModifiedTimeField} className="sf-metadata-property-detail-formatter">
|
||||
<Formatter field={lastModifiedTimeField} value={direntDetail.mtime} />
|
||||
</DetailItem>
|
||||
{window.app.pageOptions.enableMetadataManagement && enableMetadata && (
|
||||
<MetadataDetails repoID={repoID} filePath={direntPath} direntType="dir" { ...params } />
|
||||
<MetadataDetails repoID={repoID} repoInfo={repoInfo} filePath={direntPath} direntType="dir" />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
@@ -1,6 +1,7 @@
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { v4 as uuidV4 } from 'uuid';
|
||||
import { Formatter } from '@seafile/sf-metadata-ui-component';
|
||||
import { getDirentPath } from './utils';
|
||||
import DetailItem from '../detail-item';
|
||||
import { CellType } from '../../../metadata/metadata-view/_basic';
|
||||
@@ -11,12 +12,16 @@ import { Utils } from '../../../utils/utils';
|
||||
import { MetadataDetails, useMetadata } from '../../../metadata';
|
||||
import ObjectUtils from '../../../metadata/metadata-view/utils/object-utils';
|
||||
|
||||
const FileDetails = React.memo(({ repoID, repoInfo, dirent, path, direntDetail, onFileTagChanged, repoTags, fileTagList, ...params }) => {
|
||||
const FileDetails = React.memo(({ repoID, repoInfo, dirent, path, direntDetail, onFileTagChanged, repoTags, fileTagList }) => {
|
||||
const [isEditFileTagShow, setEditFileTagShow] = useState(false);
|
||||
const { enableMetadata } = useMetadata();
|
||||
|
||||
const direntPath = useMemo(() => getDirentPath(dirent, path), [dirent, path]);
|
||||
const tagListTitleID = useMemo(() => `detail-list-view-tags-${uuidV4()}`, []);
|
||||
const sizeField = useMemo(() => ({ type: 'size', name: gettext('Size') }), []);
|
||||
const lastModifierField = useMemo(() => ({ type: CellType.LAST_MODIFIER, name: gettext('Last modifier') }), []);
|
||||
const lastModifiedTimeField = useMemo(() => ({ type: CellType.MTIME, name: gettext('Last modified time') }), []);
|
||||
const tagsField = useMemo(() => ({ type: CellType.SINGLE_SELECT, name: gettext('Tags') }), []);
|
||||
|
||||
const onEditFileTagToggle = useCallback(() => {
|
||||
setEditFileTagShow(!isEditFileTagShow);
|
||||
@@ -28,25 +33,37 @@ const FileDetails = React.memo(({ repoID, repoInfo, dirent, path, direntDetail,
|
||||
|
||||
return (
|
||||
<>
|
||||
<DetailItem field={{ type: 'size', name: gettext('Size') }} value={Utils.bytesToSize(direntDetail.size)} />
|
||||
<DetailItem field={{ type: CellType.LAST_MODIFIER, name: gettext('Last modifier') }} value={direntDetail.last_modifier_email} collaborators={[{
|
||||
name: direntDetail.last_modifier_name,
|
||||
contact_email: direntDetail.last_modifier_contact_email,
|
||||
email: direntDetail.last_modifier_email,
|
||||
avatar_url: direntDetail.last_modifier_avatar,
|
||||
}]} />
|
||||
<DetailItem field={{ type: CellType.MTIME, name: gettext('Last modified time') }} value={direntDetail.last_modified} />
|
||||
<DetailItem field={sizeField} className="sf-metadata-property-detail-formatter">
|
||||
<Formatter field={sizeField} value={Utils.bytesToSize(direntDetail.size)} />
|
||||
</DetailItem>
|
||||
<DetailItem field={lastModifierField} className="sf-metadata-property-detail-formatter">
|
||||
<Formatter
|
||||
field={lastModifierField}
|
||||
value={direntDetail.last_modifier_email}
|
||||
collaborators={[{
|
||||
name: direntDetail.last_modifier_name,
|
||||
contact_email: direntDetail.last_modifier_contact_email,
|
||||
email: direntDetail.last_modifier_email,
|
||||
avatar_url: direntDetail.last_modifier_avatar,
|
||||
}]}
|
||||
/>
|
||||
</DetailItem >
|
||||
<DetailItem field={lastModifiedTimeField} className="sf-metadata-property-detail-formatter">
|
||||
<Formatter field={lastModifiedTimeField} value={direntDetail.last_modified}/>
|
||||
</DetailItem>
|
||||
{!window.app.pageOptions.enableMetadataManagement && enableMetadata && (
|
||||
<DetailItem field={{ type: CellType.SINGLE_SELECT, name: gettext('Tags') }} valueId={tagListTitleID} valueClick={onEditFileTagToggle} >
|
||||
{Array.isArray(fileTagList) && fileTagList.length > 0 ? (
|
||||
<FileTagList fileTagList={fileTagList} />
|
||||
) : (
|
||||
<span className="empty-tip-text">{gettext('Empty')}</span>
|
||||
)}
|
||||
<DetailItem field={tagsField} className="sf-metadata-property-detail-formatter">
|
||||
<div className="" id={tagListTitleID} onClick={onEditFileTagToggle}>
|
||||
{Array.isArray(fileTagList) && fileTagList.length > 0 ? (
|
||||
<FileTagList fileTagList={fileTagList} />
|
||||
) : (
|
||||
<span className="empty-tip-text">{gettext('Empty')}</span>
|
||||
)}
|
||||
</div>
|
||||
</DetailItem>
|
||||
)}
|
||||
{window.app.pageOptions.enableMetadataManagement && (
|
||||
<MetadataDetails repoID={repoID} filePath={direntPath} direntType="file" { ...params } />
|
||||
<MetadataDetails repoID={repoID} filePath={direntPath} repoInfo={repoInfo} direntType="file" />
|
||||
)}
|
||||
{isEditFileTagShow &&
|
||||
<EditFileTagPopover
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { siteRoot, mediaUrl } from '../../../utils/constants';
|
||||
import { siteRoot } from '../../../utils/constants';
|
||||
import { seafileAPI } from '../../../utils/seafile-api';
|
||||
import { Utils } from '../../../utils/utils';
|
||||
import toaster from '../../toast';
|
||||
@@ -9,9 +9,6 @@ import { Detail, Header, Body } from '../detail';
|
||||
import DirDetails from './dir-details';
|
||||
import FileDetails from './file-details';
|
||||
import ObjectUtils from '../../../metadata/metadata-view/utils/object-utils';
|
||||
import metadataAPI from '../../../metadata/api';
|
||||
import { User } from '../../../metadata/metadata-view/model';
|
||||
import { UserService } from '../../../metadata/metadata-view/_basic';
|
||||
|
||||
import './index.css';
|
||||
|
||||
@@ -22,26 +19,9 @@ class DirentDetails extends React.Component {
|
||||
this.state = {
|
||||
direntDetail: '',
|
||||
dirent: null,
|
||||
collaborators: [],
|
||||
collaboratorsCache: {},
|
||||
};
|
||||
this.userService = new UserService({ mediaUrl, api: metadataAPI.listUserInfo });
|
||||
}
|
||||
|
||||
updateCollaboratorsCache = (user) => {
|
||||
const newCollaboratorsCache = { ...this.state.collaboratorsCache, [user.email]: user };
|
||||
this.setState({ collaboratorsCache: newCollaboratorsCache });
|
||||
};
|
||||
|
||||
loadCollaborators = () => {
|
||||
metadataAPI.getCollaborators(this.props.repoID).then(res => {
|
||||
const collaborators = Array.isArray(res?.data?.user_list) ? res.data.user_list.map(user => new User(user)) : [];
|
||||
this.setState({ collaborators });
|
||||
}).catch(error => {
|
||||
this.setState({ collaborators: [] });
|
||||
});
|
||||
};
|
||||
|
||||
updateDetail = (repoID, dirent, direntPath) => {
|
||||
const apiName = dirent.type === 'file' ? 'getFileInfo' : 'getDirInfo';
|
||||
seafileAPI[apiName](repoID, direntPath).then(res => {
|
||||
@@ -73,7 +53,6 @@ class DirentDetails extends React.Component {
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
this.loadCollaborators();
|
||||
this.loadDetail(this.props.repoID, this.props.dirent, this.props.path);
|
||||
}
|
||||
|
||||
@@ -108,7 +87,7 @@ class DirentDetails extends React.Component {
|
||||
};
|
||||
|
||||
render() {
|
||||
const { dirent, direntDetail, collaborators, collaboratorsCache } = this.state;
|
||||
const { dirent, direntDetail } = this.state;
|
||||
const { repoID, path, fileTags } = this.props;
|
||||
const direntName = dirent?.name || '';
|
||||
const smallIconUrl = Utils.getDirentIcon(dirent);
|
||||
@@ -127,10 +106,6 @@ class DirentDetails extends React.Component {
|
||||
dirent={dirent}
|
||||
direntDetail={direntDetail}
|
||||
path={this.props.dirent ? path + '/' + dirent.name : path}
|
||||
collaborators={collaborators}
|
||||
collaboratorsCache={collaboratorsCache}
|
||||
updateCollaboratorsCache={this.updateCollaboratorsCache}
|
||||
queryUserAPI={this.userService?.queryUser}
|
||||
/>
|
||||
) : (
|
||||
<FileDetails
|
||||
@@ -142,10 +117,6 @@ class DirentDetails extends React.Component {
|
||||
repoTags={this.props.repoTags}
|
||||
fileTagList={dirent ? dirent.file_tags : fileTags}
|
||||
onFileTagChanged={this.props.onFileTagChanged}
|
||||
collaborators={collaborators}
|
||||
collaboratorsCache={collaboratorsCache}
|
||||
updateCollaboratorsCache={this.updateCollaboratorsCache}
|
||||
queryUserAPI={this.userService?.queryUser}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
@@ -1,11 +1,24 @@
|
||||
import React from 'react';
|
||||
import React, { useEffect } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import LibDetail from './lib-details';
|
||||
import DirentDetail from './dirent-details';
|
||||
import ObjectUtils from '../../metadata/metadata-view/utils/object-utils';
|
||||
import { MetadataContext } from '../../metadata';
|
||||
import { mediaUrl } from '../../utils/constants';
|
||||
|
||||
const Index = React.memo(({ repoID, path, dirent, currentRepoInfo, repoTags, fileTags, onClose, onFileTagChanged }) => {
|
||||
|
||||
useEffect(() => {
|
||||
// init context
|
||||
const context = new MetadataContext();
|
||||
window.sfMetadataContext = context;
|
||||
window.sfMetadataContext.init({ repoID, mediaUrl, repoInfo: currentRepoInfo });
|
||||
return () => {
|
||||
window.sfMetadataContext.destroy();
|
||||
delete window['sfMetadataContext'];
|
||||
};
|
||||
}, [repoID, currentRepoInfo]);
|
||||
|
||||
if (path === '/' && !dirent) {
|
||||
return (
|
||||
<LibDetail currentRepoInfo={currentRepoInfo} onClose={onClose} />
|
||||
|
@@ -1,5 +1,6 @@
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Formatter } from '@seafile/sf-metadata-ui-component';
|
||||
import { Utils } from '../../../utils/utils';
|
||||
import { gettext } from '../../../utils/constants';
|
||||
import { seafileAPI } from '../../../utils/seafile-api';
|
||||
@@ -14,6 +15,10 @@ const LibDetail = React.memo(({ currentRepoInfo, onClose }) => {
|
||||
const [isLoading, setLoading] = useState(true);
|
||||
const [repo, setRepo] = useState({});
|
||||
const smallIconUrl = useMemo(() => Utils.getLibIconUrl(currentRepoInfo), [currentRepoInfo]);
|
||||
const filesField = useMemo(() => ({ type: CellType.NUMBER, name: gettext('Files') }), []);
|
||||
const sizeField = useMemo(() => ({ type: 'size', name: gettext('Size') }), []);
|
||||
const creatorField = useMemo(() => ({ type: CellType.CREATOR, name: gettext('Creator') }), []);
|
||||
const mtimeField = useMemo(() => ({ type: CellType.MTIME, name: gettext('Last modified time') }), []);
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(true);
|
||||
@@ -35,15 +40,27 @@ const LibDetail = React.memo(({ currentRepoInfo, onClose }) => {
|
||||
<div className="w-100 h-100 d-flex algin-items-center justify-content-center"><Loading /></div>
|
||||
) : (
|
||||
<div className="detail-content">
|
||||
<DetailItem field={{ type: CellType.NUMBER, name: gettext('Files') }} value={repo.file_count} />
|
||||
<DetailItem field={{ type: 'size', name: gettext('Size') }} value={repo.size} />
|
||||
<DetailItem field={{ type: CellType.CREATOR, name: gettext('Creator') }} value={repo.owner_email} collaborators={[{
|
||||
name: repo.owner_name,
|
||||
contact_email: repo.owner_contact_email,
|
||||
email: repo.owner_email,
|
||||
avatar_url: repo.owner_avatar,
|
||||
}]} />
|
||||
<DetailItem field={{ type: CellType.MTIME, name: gettext('Last modified time') }} value={repo.last_modified} />
|
||||
<DetailItem field={filesField} value={repo.file_count || 0} className="sf-metadata-property-detail-formatter">
|
||||
<Formatter field={filesField} value={repo.file_count || 0} />
|
||||
</DetailItem>
|
||||
<DetailItem field={sizeField} value={repo.size} className="sf-metadata-property-detail-formatter">
|
||||
<Formatter field={sizeField} value={repo.size} />
|
||||
</DetailItem>
|
||||
<DetailItem field={creatorField} className="sf-metadata-property-detail-formatter">
|
||||
<Formatter
|
||||
field={creatorField}
|
||||
value={repo.owner_email}
|
||||
collaborators={[{
|
||||
name: repo.owner_name,
|
||||
contact_email: repo.owner_contact_email,
|
||||
email: repo.owner_email,
|
||||
avatar_url: repo.owner_avatar,
|
||||
}]}
|
||||
/>
|
||||
</DetailItem>
|
||||
<DetailItem field={mtimeField} className="sf-metadata-property-detail-formatter">
|
||||
<Formatter field={mtimeField} value={repo.last_modified} />
|
||||
</DetailItem>
|
||||
</div>
|
||||
)}
|
||||
</Body>
|
||||
|
Reference in New Issue
Block a user