1
0
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:
杨国璇
2024-08-15 17:38:42 +08:00
committed by GitHub
parent 2d2a8458a2
commit ca5d33dd5a
53 changed files with 1209 additions and 362 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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