mirror of
https://github.com/haiwen/seahub.git
synced 2025-09-07 01:41:39 +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:
8
frontend/package-lock.json
generated
8
frontend/package-lock.json
generated
@@ -19,7 +19,7 @@
|
||||
"@seafile/sdoc-editor": "1.0.50",
|
||||
"@seafile/seafile-calendar": "0.0.12",
|
||||
"@seafile/seafile-editor": "1.0.109",
|
||||
"@seafile/sf-metadata-ui-component": "0.0.18",
|
||||
"@seafile/sf-metadata-ui-component": "0.0.20",
|
||||
"@uiw/codemirror-extensions-langs": "^4.19.4",
|
||||
"@uiw/react-codemirror": "^4.19.4",
|
||||
"axios": "^1.7.3",
|
||||
@@ -5094,9 +5094,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@seafile/sf-metadata-ui-component": {
|
||||
"version": "0.0.18",
|
||||
"resolved": "https://registry.npmjs.org/@seafile/sf-metadata-ui-component/-/sf-metadata-ui-component-0.0.18.tgz",
|
||||
"integrity": "sha512-jC4JL06dAntWIr/ZPA/zJ4I9NhDfw0ismvSQuGZsBMvdrdbBBGuFVxirohB9zzwzc0k/K2epyQ/1iARKv5wjow==",
|
||||
"version": "0.0.20",
|
||||
"resolved": "https://registry.npmjs.org/@seafile/sf-metadata-ui-component/-/sf-metadata-ui-component-0.0.20.tgz",
|
||||
"integrity": "sha512-nCeqHgwAkd05GKd3YROf3k7XEQuh6FicOeYVXcbCXjzH1pki8XxLcv1NW4D85LvWh2eS6m+utegcAlXfS1EsJQ==",
|
||||
"dependencies": {
|
||||
"@seafile/seafile-calendar": "0.0.24",
|
||||
"@seafile/seafile-editor": "~1.0.102",
|
||||
|
@@ -14,7 +14,7 @@
|
||||
"@seafile/sdoc-editor": "1.0.50",
|
||||
"@seafile/seafile-calendar": "0.0.12",
|
||||
"@seafile/seafile-editor": "1.0.109",
|
||||
"@seafile/sf-metadata-ui-component": "0.0.18",
|
||||
"@seafile/sf-metadata-ui-component": "0.0.20",
|
||||
"@uiw/codemirror-extensions-langs": "^4.19.4",
|
||||
"@uiw/react-codemirror": "^4.19.4",
|
||||
"axios": "^1.7.3",
|
||||
|
@@ -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={[{
|
||||
<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 field={{ type: CellType.MTIME, name: gettext('Last modified time') }} value={direntDetail.last_modified} />
|
||||
}]}
|
||||
/>
|
||||
</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} >
|
||||
<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={[{
|
||||
<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 field={{ type: CellType.MTIME, name: gettext('Last modified time') }} value={repo.last_modified} />
|
||||
}]}
|
||||
/>
|
||||
</DetailItem>
|
||||
<DetailItem field={mtimeField} className="sf-metadata-property-detail-formatter">
|
||||
<Formatter field={mtimeField} value={repo.last_modified} />
|
||||
</DetailItem>
|
||||
</div>
|
||||
)}
|
||||
</Body>
|
||||
|
@@ -1,25 +1,39 @@
|
||||
/* eslint-disable react/prop-types */
|
||||
import React, { useContext, useState, useCallback, useEffect } from 'react';
|
||||
import { useMetadata } from './metadata';
|
||||
import { mediaUrl } from '../../../utils/constants';
|
||||
import { isValidEmail } from '../_basic';
|
||||
import React, { useContext, useState, useCallback, useEffect, useMemo } from 'react';
|
||||
import { mediaUrl } from '../../utils/constants';
|
||||
import { isValidEmail, UserService } from '../metadata-view/_basic';
|
||||
import User from '../metadata-view/model/user';
|
||||
import metadataAPI from '../api';
|
||||
|
||||
const CollaboratorsContext = React.createContext(null);
|
||||
|
||||
export const CollaboratorsProvider = ({
|
||||
children,
|
||||
}) => {
|
||||
export const CollaboratorsProvider = ({ repoID, children }) => {
|
||||
const [collaboratorsCache, setCollaboratorsCache] = useState({});
|
||||
const [collaborators, setCollaborators] = useState([]);
|
||||
|
||||
const { store } = useMetadata();
|
||||
const queryUser = useMemo(() => {
|
||||
const userService = new UserService({ mediaUrl, api: metadataAPI.listUserInfo });
|
||||
const queryUserAPI = userService.queryUser;
|
||||
window.queryUser = queryUserAPI;
|
||||
return queryUserAPI;
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
setCollaborators(store?.collaborators || []);
|
||||
}, [store?.collaborators]);
|
||||
metadataAPI.getCollaborators(repoID).then(res => {
|
||||
const collaborators = Array.isArray(res?.data?.user_list) ? res.data.user_list.map(user => new User(user)) : [];
|
||||
setCollaborators(collaborators);
|
||||
});
|
||||
}, [repoID]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!window.sfMetadata) return;
|
||||
if (!window.sfMetadata) {
|
||||
window.sfMetadata = {};
|
||||
window.sfMetadata.getCollaboratorsFromCache = () => {
|
||||
return Object.values(window.sfMetadata.collaboratorsCache || {}) || [];
|
||||
};
|
||||
window.sfMetadata.getCollaborators = () => {
|
||||
return [...window.sfMetadata.collaborators, ...(Object.values(window.sfMetadata.collaboratorsCache || {}) || [])];
|
||||
};
|
||||
}
|
||||
window.sfMetadata.collaborators = collaborators;
|
||||
window.sfMetadata.collaboratorsCache = collaboratorsCache;
|
||||
}, [collaborators, collaboratorsCache]);
|
||||
@@ -54,7 +68,7 @@ export const CollaboratorsProvider = ({
|
||||
}, [collaborators, collaboratorsCache]);
|
||||
|
||||
return (
|
||||
<CollaboratorsContext.Provider value={{ collaborators, collaboratorsCache, updateCollaboratorsCache, getCollaborator }}>
|
||||
<CollaboratorsContext.Provider value={{ collaborators, collaboratorsCache, updateCollaboratorsCache, getCollaborator, queryUser }}>
|
||||
{children}
|
||||
</CollaboratorsContext.Provider>
|
||||
);
|
||||
@@ -65,6 +79,6 @@ export const useCollaborators = () => {
|
||||
if (!context) {
|
||||
throw new Error('\'CollaboratorsContext\' is null');
|
||||
}
|
||||
const { collaborators, collaboratorsCache, updateCollaboratorsCache, getCollaborator } = context;
|
||||
return { collaborators, collaboratorsCache, updateCollaboratorsCache, getCollaborator };
|
||||
const { collaborators, collaboratorsCache, updateCollaboratorsCache, getCollaborator, queryUser } = context;
|
||||
return { collaborators, collaboratorsCache, updateCollaboratorsCache, getCollaborator, queryUser };
|
||||
};
|
@@ -1 +1,2 @@
|
||||
export { MetadataProvider, useMetadata } from './metadata';
|
||||
export { CollaboratorsProvider, useCollaborators } from './collaborators';
|
||||
|
@@ -19,29 +19,46 @@ export const MetadataProvider = ({ repoID, hideMetadataView, selectMetadataView,
|
||||
const [navigation, setNavigation] = useState([]);
|
||||
const viewsMap = useRef({});
|
||||
|
||||
const cancelURLView = useCallback(() => {
|
||||
// If attribute extension is turned off, unmark the URL
|
||||
const { origin, pathname, search } = window.location;
|
||||
const urlParams = new URLSearchParams(search);
|
||||
const viewID = urlParams.get('view');
|
||||
if (viewID) {
|
||||
const url = `${origin}${pathname}`;
|
||||
window.history.pushState({ url: url, path: '' }, '', url);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!enableMetadataManagement) {
|
||||
setEnableExtendedProperties(false);
|
||||
cancelURLView();
|
||||
return;
|
||||
}
|
||||
metadataAPI.getMetadataStatus(repoID).then(res => {
|
||||
setEnableExtendedProperties(res.data.enabled);
|
||||
const enableMetadata = res.data.enabled;
|
||||
if (!enableMetadata) {
|
||||
cancelURLView();
|
||||
}
|
||||
setEnableExtendedProperties(enableMetadata);
|
||||
}).catch(error => {
|
||||
const errorMsg = Utils.getErrorMsg(error, true);
|
||||
toaster.danger(errorMsg);
|
||||
setEnableExtendedProperties(false);
|
||||
});
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [repoID, enableMetadataManagement]);
|
||||
|
||||
const updateEnableMetadata = useCallback((newValue) => {
|
||||
if (newValue === enableMetadata) return;
|
||||
if (!newValue) {
|
||||
hideMetadataView && hideMetadataView();
|
||||
cancelURLView();
|
||||
} else {
|
||||
setShowFirstView(true);
|
||||
}
|
||||
setEnableExtendedProperties(newValue);
|
||||
}, [enableMetadata, hideMetadataView]);
|
||||
}, [enableMetadata, hideMetadataView, cancelURLView]);
|
||||
|
||||
// views
|
||||
useEffect(() => {
|
||||
@@ -61,14 +78,6 @@ export const MetadataProvider = ({ repoID, hideMetadataView, selectMetadataView,
|
||||
return;
|
||||
}
|
||||
|
||||
// If attribute extension is turned off, unmark the URL
|
||||
const { origin, pathname, search } = window.location;
|
||||
const urlParams = new URLSearchParams(search);
|
||||
const viewID = urlParams.get('view');
|
||||
if (viewID) {
|
||||
const url = `${origin}${pathname}`;
|
||||
window.history.pushState({ url: url, path: '' }, '', url);
|
||||
}
|
||||
viewsMap.current = {};
|
||||
setNavigation([]);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import SeafileMetadata from './metadata-view';
|
||||
import SeafileMetadata, { Context as MetadataContext } from './metadata-view';
|
||||
import MetadataStatusManagementDialog from './metadata-status-manage-dialog';
|
||||
import MetadataTreeView from './metadata-tree-view';
|
||||
import MetadataDetails from './metadata-details';
|
||||
@@ -8,6 +8,7 @@ export * from './hooks';
|
||||
|
||||
export {
|
||||
metadataAPI,
|
||||
MetadataContext,
|
||||
SeafileMetadata,
|
||||
MetadataStatusManagementDialog,
|
||||
MetadataTreeView,
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Utils } from '../../utils/utils';
|
||||
import metadataAPI from '../api';
|
||||
@@ -6,12 +6,17 @@ import Column from '../metadata-view/model/metadata/column';
|
||||
import { normalizeFields, getCellValueByColumn } from './utils';
|
||||
import DetailItem from '../../components/dirent-detail/detail-item';
|
||||
import toaster from '../../components/toast';
|
||||
import { gettext } from '../../utils/constants';
|
||||
import { DetailEditor, CellFormatter } from '../metadata-view';
|
||||
import { getColumnOriginName } from '../metadata-view/utils/column-utils';
|
||||
import { CellType, getColumnOptions, getOptionName, PREDEFINED_COLUMN_KEYS } from '../metadata-view/_basic';
|
||||
|
||||
import './index.css';
|
||||
|
||||
const MetadataDetails = ({ repoID, filePath, direntType, emptyTip, ...params }) => {
|
||||
const MetadataDetails = ({ repoID, filePath, repoInfo, direntType, emptyTip }) => {
|
||||
const [isLoading, setLoading] = useState(true);
|
||||
const [metadata, setMetadata] = useState({ record: {}, fields: [] });
|
||||
const permission = useMemo(() => repoInfo.permission !== 'admin' && repoInfo.permission !== 'rw' ? 'r' : 'rw', [repoInfo]);
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(true);
|
||||
@@ -34,18 +39,77 @@ const MetadataDetails = ({ repoID, filePath, direntType, emptyTip, ...params })
|
||||
});
|
||||
}, [repoID, filePath, direntType]);
|
||||
|
||||
const onChange = useCallback((fieldKey, newValue) => {
|
||||
const { record, fields } = metadata;
|
||||
const field = fields.find(f => f.key === fieldKey);
|
||||
const fileName = getColumnOriginName(field);
|
||||
let update = { [fileName]: newValue };
|
||||
if (!PREDEFINED_COLUMN_KEYS.includes(field.key) && field.type === CellType.SINGLE_SELECT) {
|
||||
const options = getColumnOptions(field);
|
||||
update = { [fileName]: getOptionName(options, newValue) };
|
||||
}
|
||||
metadataAPI.modifyRecord(repoID, record._id, update).then(res => {
|
||||
const newMetadata = { ...metadata, record: { ...record, ...update } };
|
||||
setMetadata(newMetadata);
|
||||
}).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) };
|
||||
}
|
||||
return metadataAPI.modifyRecord(repoID, record._id, update);
|
||||
}).then(res => {
|
||||
const newMetadata = { ...metadata, record: { ...record, ...update }, fields: newFields };
|
||||
setMetadata(newMetadata);
|
||||
}).catch(error => {
|
||||
const errorMsg = Utils.getErrorMsg(error);
|
||||
toaster.danger(errorMsg);
|
||||
});
|
||||
}, [repoID, metadata]);
|
||||
|
||||
if (isLoading) return null;
|
||||
const { fields, record } = metadata;
|
||||
if (!record._id) return null;
|
||||
return fields.map(field => {
|
||||
return (
|
||||
<>
|
||||
{fields.map(field => {
|
||||
const canEdit = permission === 'rw' && field.editable;
|
||||
const value = getCellValueByColumn(record, field);
|
||||
return (<DetailItem key={field.key} field={field} value={value} emptyTip={emptyTip} { ...params } />);
|
||||
});
|
||||
return (
|
||||
<DetailItem key={field.key} field={field} readonly={!canEdit}>
|
||||
{canEdit ? (
|
||||
<DetailEditor field={field} value={value} onChange={onChange} fields={fields} record={record} modifyColumnData={modifyColumnData} />
|
||||
) : (
|
||||
<CellFormatter field={field} value={value} emptyTip={gettext('Empty')} className="sf-metadata-property-detail-formatter" />
|
||||
)}
|
||||
</DetailItem>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
MetadataDetails.propTypes = {
|
||||
repoID: PropTypes.string,
|
||||
filePath: PropTypes.string,
|
||||
repoInfo: PropTypes.object,
|
||||
direntType: PropTypes.string,
|
||||
direntDetail: PropTypes.object,
|
||||
};
|
||||
|
@@ -51,7 +51,7 @@ export const EDITABLE_PRIVATE_COLUMN_KEYS = [
|
||||
PRIVATE_COLUMN_KEY.FILE_COLLABORATORS,
|
||||
PRIVATE_COLUMN_KEY.FILE_EXPIRE_TIME,
|
||||
PRIVATE_COLUMN_KEY.FILE_KEYWORDS,
|
||||
PRIVATE_COLUMN_KEY.FILE_SUMMARY,
|
||||
// PRIVATE_COLUMN_KEY.FILE_SUMMARY,
|
||||
PRIVATE_COLUMN_KEY.FILE_EXPIRED,
|
||||
PRIVATE_COLUMN_KEY.FILE_STATUS,
|
||||
];
|
||||
|
@@ -150,4 +150,6 @@ export {
|
||||
isRegExpression,
|
||||
getGeolocationDisplayString,
|
||||
getGeolocationByGranularity,
|
||||
getFloatNumber,
|
||||
isNumber,
|
||||
} from './utils';
|
||||
|
@@ -5,6 +5,8 @@ export {
|
||||
replaceNumberNotAllowInput,
|
||||
formatStringToNumber,
|
||||
formatTextToNumber,
|
||||
getFloatNumber,
|
||||
isNumber,
|
||||
} from './number';
|
||||
export {
|
||||
getOption,
|
||||
|
@@ -21,4 +21,6 @@ export {
|
||||
getLongtextDisplayString,
|
||||
getGeolocationDisplayString,
|
||||
getGeolocationByGranularity,
|
||||
getFloatNumber,
|
||||
isNumber,
|
||||
} from './column';
|
||||
|
@@ -21,6 +21,8 @@ export {
|
||||
getLongtextDisplayString,
|
||||
getGeolocationDisplayString,
|
||||
getGeolocationByGranularity,
|
||||
getFloatNumber,
|
||||
isNumber,
|
||||
} from './cell';
|
||||
export {
|
||||
getColumnType,
|
||||
|
@@ -45,3 +45,9 @@
|
||||
fill: #666;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.sf-metadata-delete-collaborator .collaborator .collaborator-remove {
|
||||
height: 14px;
|
||||
width: 14px;
|
||||
margin-left: 2px;
|
||||
}
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { IconBtn } from '@seafile/sf-metadata-ui-component';
|
||||
import { useCollaborators } from '../../../../hooks';
|
||||
import { useCollaborators } from '../../../../../hooks';
|
||||
|
||||
import './index.css';
|
||||
|
||||
@@ -20,7 +20,7 @@ const DeleteCollaborator = ({ value, onDelete }) => {
|
||||
<img className="collaborator-avatar m-0" alt={name} src={avatar_url} />
|
||||
</span>
|
||||
<span className="collaborator-name text-truncate" title={name} aria-label={name}>{name}</span>
|
||||
<IconBtn className="collaborator-remove" onClick={() => onDelete(email)} iconName="x-01" />
|
||||
<IconBtn className="collaborator-remove" onClick={(event) => onDelete(email, event)} iconName="x-01" />
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
@@ -5,12 +5,13 @@ import { SearchInput, Icon } from '@seafile/sf-metadata-ui-component';
|
||||
import { isFunction } from '../../../_basic';
|
||||
import { KeyCodes } from '../../../../../constants';
|
||||
import { gettext } from '../../../../../utils/constants';
|
||||
import { useCollaborators } from '../../../hooks';
|
||||
import { useCollaborators } from '../../../../hooks';
|
||||
import DeleteCollaborator from './delete-collaborator';
|
||||
|
||||
import './index.css';
|
||||
|
||||
const CollaboratorEditor = forwardRef(({
|
||||
saveImmediately = false,
|
||||
column,
|
||||
value: oldValue,
|
||||
onCommit,
|
||||
@@ -22,7 +23,6 @@ const CollaboratorEditor = forwardRef(({
|
||||
const [highlightIndex, setHighlightIndex] = useState(-1);
|
||||
const [maxItemNum, setMaxItemNum] = useState(0);
|
||||
const [itemHeight, setItemHeight] = useState(0);
|
||||
const timerRef = useRef(null);
|
||||
const editorContainerRef = useRef(null);
|
||||
const editorRef = useRef(null);
|
||||
const collaboratorItemRef = useRef(null);
|
||||
@@ -70,7 +70,10 @@ const CollaboratorEditor = forwardRef(({
|
||||
newValue.push(email);
|
||||
}
|
||||
setValue(newValue);
|
||||
}, [value]);
|
||||
if (saveImmediately) {
|
||||
onCommit && onCommit(newValue);
|
||||
}
|
||||
}, [saveImmediately, value, onCommit]);
|
||||
|
||||
const onDeleteCollaborator = useCallback((email) => {
|
||||
const newValue = value.slice(0);
|
||||
@@ -79,7 +82,10 @@ const CollaboratorEditor = forwardRef(({
|
||||
newValue.splice(collaboratorIndex, 1);
|
||||
}
|
||||
setValue(newValue);
|
||||
}, [value]);
|
||||
if (saveImmediately) {
|
||||
onCommit && onCommit(newValue);
|
||||
}
|
||||
}, [saveImmediately, value, onCommit]);
|
||||
|
||||
const onMenuMouseEnter = useCallback((highlightIndex) => {
|
||||
setHighlightIndex(highlightIndex);
|
||||
@@ -182,8 +188,6 @@ const CollaboratorEditor = forwardRef(({
|
||||
document.addEventListener('keydown', onHotKey, true);
|
||||
return () => {
|
||||
document.removeEventListener('keydown', onHotKey, true);
|
||||
timerRef.current && clearTimeout(timerRef.current);
|
||||
timerRef.current = null;
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [onHotKey]);
|
||||
@@ -214,7 +218,7 @@ const CollaboratorEditor = forwardRef(({
|
||||
<div key={collaborator.email} className="sf-metadata-collaborator-item" ref={collaboratorItemRef}>
|
||||
<div
|
||||
className={classnames('collaborator-container', { 'collaborator-container-highlight': i === highlightIndex })}
|
||||
onMouseDown={() => onSelectCollaborator(isSelected ? null : collaborator.email)}
|
||||
onMouseDown={() => onSelectCollaborator(collaborator.email)}
|
||||
onMouseEnter={() => onMenuMouseEnter(i)}
|
||||
onMouseLeave={() => onMenuMouseLeave(i)}
|
||||
>
|
||||
@@ -238,7 +242,7 @@ const CollaboratorEditor = forwardRef(({
|
||||
<div className="sf-metadata-collaborator-editor" style={{ top: -38 }} ref={editorRef}>
|
||||
<DeleteCollaborator value={value} onDelete={onDeleteCollaborator} />
|
||||
<div className="sf-metadata-search-collaborator-options">
|
||||
<SearchInput placeholder={gettext('Search collaborators')} onKeyDown={onKeyDown} onChange={onChangeSearch} autoFocus={true} />
|
||||
<SearchInput placeholder={gettext('Search collaborators')} onKeyDown={onKeyDown} onChange={onChangeSearch} autoFocus={true} className="sf-metadata-search-collaborators" />
|
||||
</div>
|
||||
<div className="sf-metadata-collaborator-editor-container" ref={editorContainerRef}>
|
||||
{renderCollaborators()}
|
||||
@@ -248,6 +252,7 @@ const CollaboratorEditor = forwardRef(({
|
||||
});
|
||||
|
||||
CollaboratorEditor.propTypes = {
|
||||
saveImmediately: PropTypes.bool,
|
||||
column: PropTypes.object,
|
||||
value: PropTypes.array,
|
||||
onCommit: PropTypes.func,
|
||||
|
@@ -8,7 +8,7 @@ import { isCellValueChanged } from '../../../utils/cell-comparer';
|
||||
import { EVENT_BUS_TYPE } from '../../../constants';
|
||||
import { getEventClassName } from '../../../utils';
|
||||
import Editor from '../editor';
|
||||
import { canEdit } from '../../../utils/column-utils';
|
||||
import { canEditCell } from '../../../utils/column-utils';
|
||||
|
||||
class NormalEditorContainer extends React.Component {
|
||||
|
||||
@@ -83,7 +83,7 @@ class NormalEditorContainer extends React.Component {
|
||||
const { column, record, openEditorMode, columns, modifyColumnData } = this.props;
|
||||
const editorProps = {
|
||||
ref: this.setEditorRef,
|
||||
readOnly: !canEdit(column, record, true),
|
||||
readOnly: !canEditCell(column, record, true),
|
||||
columns,
|
||||
column: this.props.column,
|
||||
value: this.getInitialValue(),
|
||||
|
@@ -6,7 +6,7 @@ import { CellType, isFunction, Z_INDEX, getCellValueByColumn, getColumnOptionNam
|
||||
import { isCellValueChanged } from '../../../utils/cell-comparer';
|
||||
import { EVENT_BUS_TYPE } from '../../../constants';
|
||||
import Editor from '../editor';
|
||||
import { canEdit } from '../../../utils/column-utils';
|
||||
import { canEditCell } from '../../../utils/column-utils';
|
||||
|
||||
const NOT_SUPPORT_EDITOR_COLUMN_TYPES = [
|
||||
CellType.CTIME, CellType.MTIME, CellType.CREATOR, CellType.LAST_MODIFIER,
|
||||
@@ -58,7 +58,7 @@ class PopupEditorContainer extends React.Component {
|
||||
|
||||
createEditor = () => {
|
||||
const { column, record, height, onPressTab, editorPosition, columns, modifyColumnData } = this.props;
|
||||
const readOnly = canEdit(column, record, true) || NOT_SUPPORT_EDITOR_COLUMN_TYPES.includes(column.type);
|
||||
const readOnly = canEditCell(column, record, true) || NOT_SUPPORT_EDITOR_COLUMN_TYPES.includes(column.type);
|
||||
const value = this.getInitialValue(readOnly);
|
||||
|
||||
let editorProps = {
|
||||
|
@@ -63,6 +63,7 @@
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.single-select-avatar {
|
||||
@@ -82,12 +83,13 @@
|
||||
padding: 0px 10px;
|
||||
height: 20px;
|
||||
line-height: 20px;
|
||||
text-align: center;
|
||||
margin-left: 0;
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
margin-top: 5px;
|
||||
white-space: nowrap;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.sf-metadata-single-select-editor .single-select-check-icon {
|
||||
|
@@ -2,8 +2,8 @@ import React, { forwardRef, useMemo, useImperativeHandle, useCallback, useState,
|
||||
import PropTypes from 'prop-types';
|
||||
import classnames from 'classnames';
|
||||
import { SearchInput, CustomizeAddTool, Icon } from '@seafile/sf-metadata-ui-component';
|
||||
import { getCellValueByColumn, getColumnByKey, isFunction } from '../../../_basic';
|
||||
import { generateNewOption, getSelectColumnOptions } from '../../../utils/select-utils';
|
||||
import { getCellValueByColumn, getColumnByKey, isFunction, getColumnOptions } from '../../../_basic';
|
||||
import { generateNewOption } from '../../../utils/select-utils';
|
||||
import { KeyCodes } from '../../../../../constants';
|
||||
import { gettext } from '../../../../../utils/constants';
|
||||
|
||||
@@ -23,15 +23,14 @@ const SingleSelectEditor = forwardRef(({
|
||||
const [searchValue, setSearchValue] = useState('');
|
||||
const [highlightIndex, setHighlightIndex] = useState(-1);
|
||||
const [maxItemNum, setMaxItemNum] = useState(0);
|
||||
const [itemHeight, setItemHeight] = useState(0);
|
||||
const timerRef = useRef(null);
|
||||
const itemHeight = 30;
|
||||
const editorContainerRef = useRef(null);
|
||||
const editorRef = useRef(null);
|
||||
const selectItemRef = useRef(null);
|
||||
const canEditData = window.sfMetadataContext.canModifyColumnData(column);
|
||||
|
||||
const options = useMemo(() => {
|
||||
const options = getSelectColumnOptions(column);
|
||||
const options = getColumnOptions(column);
|
||||
const { data } = column;
|
||||
const { cascade_column_key, cascade_settings } = data || {};
|
||||
if (cascade_column_key) {
|
||||
@@ -64,8 +63,8 @@ const SingleSelectEditor = forwardRef(({
|
||||
}, [column, height]);
|
||||
|
||||
const blur = useCallback(() => {
|
||||
onCommit && onCommit();
|
||||
}, [onCommit]);
|
||||
onCommit && onCommit(value);
|
||||
}, [value, onCommit]);
|
||||
|
||||
const onChangeSearch = useCallback((newSearchValue) => {
|
||||
if (searchValue === newSearchValue) return;
|
||||
@@ -76,7 +75,7 @@ const SingleSelectEditor = forwardRef(({
|
||||
if (optionId === value) return;
|
||||
setValue(optionId);
|
||||
setTimeout(() => {
|
||||
onCommit && onCommit();
|
||||
onCommit && onCommit(optionId);
|
||||
}, 1);
|
||||
}, [value, onCommit]);
|
||||
|
||||
@@ -89,6 +88,7 @@ const SingleSelectEditor = forwardRef(({
|
||||
}, []);
|
||||
|
||||
const createOption = useCallback((event) => {
|
||||
event && event.stopPropagation();
|
||||
event && event.nativeEvent.stopImmediatePropagation();
|
||||
const newOption = generateNewOption(options, searchValue?.trim() || '');
|
||||
let newOptions = options.slice(0);
|
||||
@@ -180,13 +180,10 @@ const SingleSelectEditor = forwardRef(({
|
||||
}
|
||||
if (editorContainerRef.current && selectItemRef.current) {
|
||||
setMaxItemNum(getMaxItemNum());
|
||||
setItemHeight(parseInt(getComputedStyle(selectItemRef.current, null).height));
|
||||
}
|
||||
document.addEventListener('keydown', onHotKey, true);
|
||||
return () => {
|
||||
document.removeEventListener('keydown', onHotKey, true);
|
||||
timerRef.current && clearTimeout(timerRef.current);
|
||||
timerRef.current = null;
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [onHotKey]);
|
||||
@@ -211,12 +208,8 @@ const SingleSelectEditor = forwardRef(({
|
||||
return (<span className="none-search-result">{noOptionsTip}</span>);
|
||||
}
|
||||
|
||||
// maxWidth = single-selects-container's width - single-selects-container's padding-left and padding-right - single-select-container's padding-left - single-select-check-icon's width - The gap between the single-select-check-icon and single-select-name or scroll's width
|
||||
// maxWidth = column.width > 200 ? column.width - 20 - 12 - 20 - 10 : 200 - 20 - 12 - 20 - 10
|
||||
// maxWidth = column.width > 200 ? column.width - 62 : 200 - 62
|
||||
const maxWidth = column.width > 200 ? column.width - 62 : 200 - 62;
|
||||
return displayOptions.map((option, i) => {
|
||||
const isSelected = value === option.name;
|
||||
const isSelected = value === option.id || value === option.name;
|
||||
return (
|
||||
<div key={option.id} className="sf-metadata-single-select-item" ref={selectItemRef}>
|
||||
<div
|
||||
@@ -228,7 +221,7 @@ const SingleSelectEditor = forwardRef(({
|
||||
<div className="single-select">
|
||||
<span
|
||||
className="single-select-name"
|
||||
style={{ backgroundColor: option.color, color: option.textColor || null, maxWidth }}
|
||||
style={{ backgroundColor: option.color, color: option.textColor || null }}
|
||||
title={option.name}
|
||||
aria-label={option.name}
|
||||
>
|
||||
@@ -243,7 +236,7 @@ const SingleSelectEditor = forwardRef(({
|
||||
);
|
||||
});
|
||||
|
||||
}, [displayOptions, searchValue, column, value, highlightIndex, onMenuMouseEnter, onMenuMouseLeave, onSelectOption]);
|
||||
}, [displayOptions, searchValue, value, highlightIndex, onMenuMouseEnter, onMenuMouseLeave, onSelectOption]);
|
||||
|
||||
return (
|
||||
<div className="sf-metadata-single-select-editor" style={style} ref={editorRef}>
|
||||
@@ -253,6 +246,7 @@ const SingleSelectEditor = forwardRef(({
|
||||
onKeyDown={onKeyDown}
|
||||
onChange={onChangeSearch}
|
||||
autoFocus={true}
|
||||
className="sf-metadata-search-options"
|
||||
/>
|
||||
</div>
|
||||
<div className="sf-metadata-single-select-editor-container" ref={editorContainerRef}>
|
||||
@@ -280,4 +274,3 @@ SingleSelectEditor.propTypes = {
|
||||
};
|
||||
|
||||
export default SingleSelectEditor;
|
||||
|
||||
|
@@ -1,11 +1,11 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Formatter } from '@seafile/sf-metadata-ui-component';
|
||||
import { useCollaborators } from '../../hooks';
|
||||
import { useCollaborators } from '../../../hooks';
|
||||
import { Utils } from '../../../../utils/utils';
|
||||
|
||||
const CellFormatter = ({ readonly, value, field, ...params }) => {
|
||||
const { collaborators, collaboratorsCache, updateCollaboratorsCache } = useCollaborators();
|
||||
const { collaborators, collaboratorsCache, updateCollaboratorsCache, queryUser } = useCollaborators();
|
||||
const props = useMemo(() => {
|
||||
return {
|
||||
collaborators,
|
||||
@@ -14,11 +14,11 @@ const CellFormatter = ({ readonly, value, field, ...params }) => {
|
||||
readonly,
|
||||
value,
|
||||
field,
|
||||
queryUserAPI: window.sfMetadataContext.userService.queryUser,
|
||||
queryUserAPI: queryUser,
|
||||
getFileIconUrl: Utils.getFileIconUrl,
|
||||
getFolderIconUrl: Utils.getFolderIconUrl,
|
||||
};
|
||||
}, [readonly, value, field, collaborators, collaboratorsCache, updateCollaboratorsCache]);
|
||||
}, [readonly, value, field, collaborators, collaboratorsCache, updateCollaboratorsCache, queryUser]);
|
||||
|
||||
return (
|
||||
<Formatter { ...props } { ...params } />
|
||||
|
@@ -0,0 +1,22 @@
|
||||
.sf-metadata-checkbox-property-detail-editor {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
padding: 7px 6px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.sf-metadata-checkbox-property-detail-editor .sf-metadata-checkbox-property-detail-editor-content {
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 2px solid #e0e0e0;
|
||||
border-radius: 3px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.sf-metadata-checkbox-property-detail-editor .sf-metadata-checkbox-property-detail-editor-content .sf-metadata-icon-check-mark {
|
||||
fill: #20c933;
|
||||
}
|
@@ -0,0 +1,28 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Icon } from '@seafile/sf-metadata-ui-component';
|
||||
|
||||
import './index.css';
|
||||
|
||||
const CheckboxEditor = ({ value, onChange: onChangeAPI }) => {
|
||||
|
||||
const onChange = useCallback((event) => {
|
||||
event && event.stopPropagation();
|
||||
onChangeAPI(!value);
|
||||
}, [value, onChangeAPI]);
|
||||
|
||||
return (
|
||||
<div className="sf-metadata-property-detail-editor sf-metadata-checkbox-property-detail-editor">
|
||||
<div className="sf-metadata-checkbox-property-detail-editor-content" onClick={onChange}>
|
||||
{value && (<Icon iconName="check-mark" />)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
CheckboxEditor.propTypes = {
|
||||
value: PropTypes.bool,
|
||||
onChange: PropTypes.func,
|
||||
};
|
||||
|
||||
export default CheckboxEditor;
|
@@ -0,0 +1,36 @@
|
||||
.sf-metadata-collaborator-property-detail-editor {
|
||||
min-height: 34px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.sf-metadata-collaborator-property-detail-editor .sf-metadata-delete-collaborator {
|
||||
border-bottom: none;
|
||||
background-color: inherit;
|
||||
border-radius: unset;
|
||||
padding: 2px 6px;
|
||||
}
|
||||
|
||||
.sf-metadata-collaborator-property-detail-editor .sf-metadata-delete-collaborator .collaborator {
|
||||
margin: 5px 10px 5px 0;
|
||||
}
|
||||
|
||||
.sf-metadata-collaborator-property-editor-popover .popover {
|
||||
max-width: unset;
|
||||
}
|
||||
|
||||
.sf-metadata-collaborator-property-editor-popover .sf-metadata-collaborator-editor {
|
||||
width: 100%;
|
||||
position: unset;
|
||||
min-width: 200px;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
opacity: 1;
|
||||
background-color: #ffffff;
|
||||
border: none;
|
||||
border-radius: unset;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.sf-metadata-collaborator-property-editor-popover .sf-metadata-delete-collaborator {
|
||||
display: none;
|
||||
}
|
@@ -0,0 +1,102 @@
|
||||
import React, { useCallback, useState, useRef, useEffect } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Popover } from 'reactstrap';
|
||||
import { KeyCodes } from '../../../_basic';
|
||||
import { getEventClassName, gettext } from '../../../utils';
|
||||
import Editor from '../../cell-editor/collaborator-editor';
|
||||
import DeleteCollaborator from '../../cell-editor/collaborator-editor/delete-collaborator';
|
||||
|
||||
import './index.css';
|
||||
|
||||
const CollaboratorEditor = ({ field, value, onChange }) => {
|
||||
const ref = useRef(null);
|
||||
const [showEditor, setShowEditor] = useState(false);
|
||||
|
||||
const onClick = useCallback((event) => {
|
||||
if (!showEditor) return;
|
||||
if (!event.target) return;
|
||||
const className = getEventClassName(event);
|
||||
if (className.indexOf('sf-metadata-search-collaborators') > -1) return;
|
||||
const editor = document.querySelector('.sf-metadata-collaborator-editor');
|
||||
if ((editor && editor.contains(event.target)) || ref.current.contains(event.target)) return;
|
||||
setShowEditor(false);
|
||||
}, [showEditor]);
|
||||
|
||||
const onHotKey = useCallback((event) => {
|
||||
if (event.keyCode === KeyCodes.Esc) {
|
||||
if (showEditor) {
|
||||
setShowEditor(false);
|
||||
}
|
||||
}
|
||||
}, [showEditor]);
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener('mousedown', onClick);
|
||||
document.addEventListener('keydown', onHotKey, true);
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', onClick);
|
||||
document.removeEventListener('keydown', onHotKey, true);
|
||||
};
|
||||
}, [onClick, onHotKey]);
|
||||
|
||||
const openEditor = useCallback(() => {
|
||||
setShowEditor(true);
|
||||
}, []);
|
||||
|
||||
const onCommit = useCallback((newValue) => {
|
||||
onChange(newValue);
|
||||
}, [onChange]);
|
||||
|
||||
const deleteCollaborator = useCallback((email, event) => {
|
||||
event && event.stopPropagation();
|
||||
event && event.nativeEvent && event.nativeEvent.stopImmediatePropagation();
|
||||
const newValue = value.filter(c => c !== email);
|
||||
onChange(newValue);
|
||||
}, [value, onChange]);
|
||||
|
||||
const renderEditor = useCallback(() => {
|
||||
if (!showEditor) return null;
|
||||
const { width } = ref.current.getBoundingClientRect();
|
||||
return (
|
||||
<Popover
|
||||
target={ref}
|
||||
isOpen={true}
|
||||
placement="bottom-end"
|
||||
hideArrow={true}
|
||||
fade={false}
|
||||
className="sf-metadata-property-editor-popover sf-metadata-collaborator-property-editor-popover"
|
||||
boundariesElement={document.body}
|
||||
style={{ width: Math.max(width - 2, 200) }}
|
||||
>
|
||||
<Editor
|
||||
saveImmediately={true}
|
||||
value={value}
|
||||
column={field}
|
||||
height={2}
|
||||
onCommit={onCommit}
|
||||
/>
|
||||
</Popover>
|
||||
);
|
||||
}, [showEditor, onCommit, value, field]);
|
||||
|
||||
|
||||
return (
|
||||
<div
|
||||
className="sf-metadata-property-detail-editor sf-metadata-collaborator-property-detail-editor"
|
||||
placeholder={gettext('Empty')}
|
||||
ref={ref}
|
||||
onClick={openEditor}
|
||||
>
|
||||
{<DeleteCollaborator value={value} onDelete={deleteCollaborator} />}
|
||||
{renderEditor()}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
CollaboratorEditor.propTypes = {
|
||||
field: PropTypes.object.isRequired,
|
||||
value: PropTypes.array,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default CollaboratorEditor;
|
@@ -0,0 +1,9 @@
|
||||
.sf-metadata-date-property-detail-editor {
|
||||
width: 100%;
|
||||
height: 34px;
|
||||
padding: 6.5px 6px;
|
||||
}
|
||||
|
||||
.dirent-detail-item-value.editable .ant-calendar-picker-input {
|
||||
display: none;
|
||||
}
|
@@ -0,0 +1,54 @@
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import { SfCalendar } from '@seafile/sf-metadata-ui-component';
|
||||
import PropTypes from 'prop-types';
|
||||
import { DEFAULT_DATE_FORMAT, getDateDisplayString } from '../../../_basic';
|
||||
import { gettext } from '../../../utils';
|
||||
|
||||
import './index.css';
|
||||
|
||||
const DateEditor = ({ value, field, onChange: onChangeAPI, lang }) => {
|
||||
const [showEditor, setShowEditor] = useState(false);
|
||||
const format = useMemo(() => field?.data?.format || DEFAULT_DATE_FORMAT, [field]);
|
||||
|
||||
const openEditor = useCallback(() => {
|
||||
setShowEditor(true);
|
||||
}, []);
|
||||
|
||||
const onChange = useCallback((newValue) => {
|
||||
onChangeAPI(newValue);
|
||||
}, [onChangeAPI]);
|
||||
|
||||
const onClear = useCallback(() => {
|
||||
onChangeAPI(null);
|
||||
setShowEditor(false);
|
||||
}, [onChangeAPI]);
|
||||
|
||||
const closeEditor = useCallback(() => {
|
||||
setShowEditor(false);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className="sf-metadata-property-detail-editor sf-metadata-date-property-detail-editor"
|
||||
placeholder={gettext('Empty')}
|
||||
onClick={openEditor}
|
||||
>
|
||||
{getDateDisplayString(value, format)}
|
||||
</div>
|
||||
{showEditor && (
|
||||
<SfCalendar lang={lang} format={format} value={value} onChange={onChange} onClose={closeEditor} onClear={onClear} />
|
||||
)}
|
||||
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
DateEditor.propTypes = {
|
||||
value: PropTypes.string,
|
||||
field: PropTypes.object.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
lang: PropTypes.string,
|
||||
};
|
||||
|
||||
export default DateEditor;
|
@@ -0,0 +1,43 @@
|
||||
.sf-metadata-property-editor-popover .popover[x-placement^="bottom"] {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.sf-metadata-property-editor-popover .popover[x-placement^="top"] {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.sf-metadata-property-editor-popover .sf-metadata-property-editor-popover-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.sf-metadata-property-editor-popover .add-search-result {
|
||||
height: 30px;
|
||||
padding: 0 10px;
|
||||
overflow: hidden;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.sf-metadata-property-editor-popover .add-search-result:hover{
|
||||
cursor: pointer;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.sf-metadata-property-editor-popover .add-search-result .add-new-option {
|
||||
display: inline-block;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.sf-metadata-property-detail-editor {
|
||||
border-radius: inherit;
|
||||
}
|
||||
|
||||
.sf-metadata-property-detail-editor:empty::before {
|
||||
content: attr(placeholder);
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
}
|
@@ -0,0 +1,51 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { CellType } from '../../_basic';
|
||||
import CheckboxEditor from './checkbox-editor';
|
||||
import TextEditor from './text-editor';
|
||||
import NumberEditor from './number-editor';
|
||||
import SingleSelectEditor from './single-select-editor';
|
||||
import CollaboratorEditor from './collaborator-editor';
|
||||
import DateEditor from './date-editor';
|
||||
import { lang } from '../../../../utils/constants';
|
||||
|
||||
import './index.css';
|
||||
|
||||
const DetailEditor = ({ field, onChange: onChangeAPI, ...props }) => {
|
||||
const onChange = useCallback((newValue) => {
|
||||
onChangeAPI(field.key, newValue);
|
||||
}, [field, onChangeAPI]);
|
||||
|
||||
|
||||
switch (field.type) {
|
||||
case CellType.CHECKBOX: {
|
||||
return (<CheckboxEditor { ...props } field={field} onChange={onChange} />);
|
||||
}
|
||||
case CellType.TEXT: {
|
||||
return (<TextEditor { ...props } field={field} onChange={onChange} />);
|
||||
}
|
||||
case CellType.NUMBER: {
|
||||
return (<NumberEditor { ...props } field={field} onChange={onChange} />);
|
||||
}
|
||||
case CellType.DATE: {
|
||||
return (<DateEditor { ...props } field={field} onChange={onChange} lang={lang} />);
|
||||
}
|
||||
case CellType.SINGLE_SELECT: {
|
||||
return (<SingleSelectEditor { ...props } field={field} onChange={onChange} />);
|
||||
}
|
||||
case CellType.COLLABORATOR: {
|
||||
return (<CollaboratorEditor { ...props } field={field} onChange={onChange} />);
|
||||
}
|
||||
default: {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
};
|
||||
|
||||
DetailEditor.propTypes = {
|
||||
field: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
export default DetailEditor;
|
@@ -0,0 +1,24 @@
|
||||
.sf-metadata-number-property-detail-editor {
|
||||
min-height: 34px;
|
||||
height: 34px;
|
||||
padding: 6.5px 6px;
|
||||
line-height: 1.5;
|
||||
border: none !important;
|
||||
background: inherit;
|
||||
}
|
||||
|
||||
.sf-metadata-number-property-detail-editor::placeholder {
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.sf-metadata-number-property-detail-editor:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.sf-metadata-number-property-detail-editor:focus {
|
||||
background-color: #fff;
|
||||
box-shadow: rgba(15, 15, 15, 0.05) 0px 0px 0px 1px, rgba(15, 15, 15, 0.1) 0px 3px 6px, rgba(15, 15, 15, 0.2) 0px 9px 24px;
|
||||
border-radius: 3px;
|
||||
cursor: auto;
|
||||
}
|
@@ -0,0 +1,100 @@
|
||||
import React, { useCallback, useRef, useState, useEffect } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { KeyCodes } from '../../../../../constants';
|
||||
import { isCellValueChanged } from '../../../utils/cell-comparer';
|
||||
import { gettext } from '../../../utils';
|
||||
import ObjectUtils from '../../../utils/object-utils';
|
||||
import { getNumberDisplayString, DEFAULT_NUMBER_FORMAT, formatStringToNumber,
|
||||
replaceNumberNotAllowInput, isMac,
|
||||
} from '../../../_basic';
|
||||
|
||||
import './index.css';
|
||||
|
||||
const NumberEditor = React.memo(({ value: oldValue, field, onChange: onChangeAPI }) => {
|
||||
const [value, setValue] = useState('');
|
||||
const ref = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
const validValue = oldValue || oldValue === 0 ? oldValue : '';
|
||||
const data = field?.data || {};
|
||||
const value = getNumberDisplayString(validValue, data) || '';
|
||||
setValue(value);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [oldValue, field]);
|
||||
|
||||
const valueChange = useCallback((event) => {
|
||||
const format = field?.data?.format || DEFAULT_NUMBER_FORMAT;
|
||||
const currency_symbol = format === 'custom_currency' ? field.data['currency_symbol'] : null;
|
||||
const initValue = event.target.value.trim();
|
||||
// Prevent the repetition of periods bug in the Chinese input method of the Windows system
|
||||
if (!isMac() && initValue.indexOf('.。') > -1) return;
|
||||
const newValue = replaceNumberNotAllowInput(initValue, format, currency_symbol);
|
||||
if (newValue === value) return;
|
||||
setValue(newValue);
|
||||
}, [field, value]);
|
||||
|
||||
const onBlur = useCallback(() => {
|
||||
const newValue = formatStringToNumber(value, field.data);
|
||||
if (newValue === oldValue) return;
|
||||
onChangeAPI(newValue);
|
||||
}, [oldValue, value, field, onChangeAPI]);
|
||||
|
||||
const onPaste = useCallback((event) => {
|
||||
event.stopPropagation();
|
||||
}, []);
|
||||
|
||||
const onCut = useCallback((event) => {
|
||||
event.stopPropagation();
|
||||
}, []);
|
||||
|
||||
const onKeyDown = useCallback((event) => {
|
||||
const { selectionStart, selectionEnd, value } = event.currentTarget;
|
||||
if (event.keyCode === KeyCodes.Enter) {
|
||||
event.preventDefault();
|
||||
ref.current && ref.current.blur();
|
||||
} else if (
|
||||
(event.keyCode === KeyCodes.ChineseInputMethod) ||
|
||||
(event.keyCode === KeyCodes.LeftArrow && selectionStart === 0) ||
|
||||
(event.keyCode === KeyCodes.RightArrow && selectionEnd === value.length)
|
||||
) {
|
||||
event.stopPropagation();
|
||||
}
|
||||
}, []);
|
||||
|
||||
const onCompositionEnd = useCallback((event) => {
|
||||
valueChange(event);
|
||||
}, [valueChange]);
|
||||
|
||||
return (
|
||||
<input
|
||||
ref={ref}
|
||||
type="text"
|
||||
className="sf-metadata-number-property-detail-editor form-control"
|
||||
placeholder={gettext('Empty') || ''}
|
||||
onBlur={onBlur}
|
||||
onCut={onCut}
|
||||
onPaste={onPaste}
|
||||
value={value}
|
||||
name={field.name}
|
||||
title={field.name}
|
||||
aria-label={field.name}
|
||||
onChange={valueChange}
|
||||
onKeyDown={onKeyDown}
|
||||
onCompositionEnd={onCompositionEnd}
|
||||
/>
|
||||
);
|
||||
|
||||
|
||||
}, (props, nextProps) => {
|
||||
const isChanged = isCellValueChanged(props.value, nextProps.value, nextProps.field.type) ||
|
||||
!ObjectUtils.isSameObject(props.field, nextProps.field);
|
||||
return !isChanged;
|
||||
});
|
||||
|
||||
NumberEditor.propTypes = {
|
||||
value: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
|
||||
field: PropTypes.object.isRequired,
|
||||
onChange: PropTypes.func,
|
||||
};
|
||||
|
||||
export default NumberEditor;
|
@@ -0,0 +1,40 @@
|
||||
.sf-metadata-single-select-property-detail-editor {
|
||||
height: 34px;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 6px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.sf-metadata-single-select-property-detail-editor .sf-metadata-single-select-property-value {
|
||||
border-radius: 10px;
|
||||
font-size: 13px;
|
||||
line-height: 20px;
|
||||
margin: 6px 0;
|
||||
max-width: 250px;
|
||||
overflow: hidden;
|
||||
padding: 0 10px;
|
||||
text-align: center;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
width: -webkit-min-content;
|
||||
width: min-content;
|
||||
}
|
||||
|
||||
.sf-metadata-single-select-property-editor-popover .popover {
|
||||
max-width: unset;
|
||||
}
|
||||
|
||||
.sf-metadata-single-select-property-editor-popover .sf-metadata-single-select-editor {
|
||||
position: unset;
|
||||
min-width: 200px;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
opacity: 1;
|
||||
background-color: #ffffff;
|
||||
border: none;
|
||||
border-radius: none;
|
||||
box-shadow: none;
|
||||
}
|
@@ -0,0 +1,111 @@
|
||||
import React, { useMemo, useCallback, useState, useRef, useEffect } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Popover } from 'reactstrap';
|
||||
import { getColumnOptions, getOption, KeyCodes } from '../../../_basic';
|
||||
import { getEventClassName, gettext } from '../../../utils';
|
||||
import Editor from '../../cell-editor/single-select-editor';
|
||||
|
||||
import './index.css';
|
||||
|
||||
const SingleSelectEditor = ({ field, value, record, fields, onChange, modifyColumnData }) => {
|
||||
const ref = useRef(null);
|
||||
const [showEditor, setShowEditor] = useState(false);
|
||||
const options = useMemo(() => getColumnOptions(field), [field]);
|
||||
|
||||
const onClick = useCallback((event) => {
|
||||
if (!event.target) return;
|
||||
const className = getEventClassName(event);
|
||||
if (className.indexOf('sf-metadata-search-options') > -1) return;
|
||||
const dom = document.querySelector('.sf-metadata-single-select-editor');
|
||||
if (!dom) return;
|
||||
if (dom.contains(event.target)) return;
|
||||
if (ref.current && !ref.current.contains(event.target) && showEditor) {
|
||||
setShowEditor(false);
|
||||
}
|
||||
}, [showEditor]);
|
||||
|
||||
const onHotKey = useCallback((event) => {
|
||||
if (event.keyCode === KeyCodes.Esc) {
|
||||
if (showEditor) {
|
||||
setShowEditor(false);
|
||||
}
|
||||
}
|
||||
}, [showEditor]);
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener('mousedown', onClick);
|
||||
document.addEventListener('keydown', onHotKey, true);
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', onClick);
|
||||
document.removeEventListener('keydown', onHotKey, true);
|
||||
};
|
||||
}, [onClick, onHotKey]);
|
||||
|
||||
const openEditor = useCallback(() => {
|
||||
setShowEditor(true);
|
||||
}, []);
|
||||
|
||||
const onCommit = useCallback((newValue) => {
|
||||
if (newValue && !getOption(options, newValue)) {
|
||||
setShowEditor(false);
|
||||
return;
|
||||
}
|
||||
onChange(newValue);
|
||||
setShowEditor(false);
|
||||
}, [options, onChange]);
|
||||
|
||||
const option = value ? getOption(options, value) : null;
|
||||
|
||||
const renderEditor = useCallback(() => {
|
||||
if (!showEditor) return null;
|
||||
const { width } = ref.current.getBoundingClientRect();
|
||||
return (
|
||||
<Popover
|
||||
target={ref}
|
||||
isOpen={true}
|
||||
placement="bottom-end"
|
||||
hideArrow={true}
|
||||
fade={false}
|
||||
className="sf-metadata-property-editor-popover sf-metadata-single-select-property-editor-popover"
|
||||
boundariesElement={document.body}
|
||||
>
|
||||
<Editor
|
||||
value={value}
|
||||
column={{ ...field, width: Math.max(width - 2, 200) }}
|
||||
columns={fields}
|
||||
modifyColumnData={modifyColumnData}
|
||||
record={record}
|
||||
height={2}
|
||||
onCommit={onCommit}
|
||||
/>
|
||||
</Popover>
|
||||
);
|
||||
}, [showEditor, onCommit, record, value, modifyColumnData, fields, field]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="sf-metadata-property-detail-editor sf-metadata-single-select-property-detail-editor"
|
||||
placeholder={gettext('Empty')}
|
||||
ref={ref}
|
||||
onClick={openEditor}
|
||||
>
|
||||
{option && (
|
||||
<div
|
||||
className="sf-metadata-single-select-property-value"
|
||||
style={{ backgroundColor: option.color, color: option.textColor || null }}
|
||||
>
|
||||
{option.name}
|
||||
</div>
|
||||
)}
|
||||
{renderEditor()}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
SingleSelectEditor.propTypes = {
|
||||
field: PropTypes.object.isRequired,
|
||||
value: PropTypes.string,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default SingleSelectEditor;
|
@@ -0,0 +1,17 @@
|
||||
.sf-metadata-text-property-detail-editor {
|
||||
padding: 6.5px 6px;
|
||||
min-height: 34px;
|
||||
width: 100%;
|
||||
border: none !important;
|
||||
outline: none !important;
|
||||
}
|
||||
|
||||
.sf-metadata-text-property-detail-editor.formatter {
|
||||
background-color: inherit;
|
||||
}
|
||||
|
||||
.sf-metadata-text-property-detail-editor:not(.formatter) {
|
||||
background-color: #fff;
|
||||
box-shadow: rgba(15, 15, 15, 0.05) 0px 0px 0px 1px, rgba(15, 15, 15, 0.1) 0px 3px 6px, rgba(15, 15, 15, 0.2) 0px 9px 24px;
|
||||
border-radius: 3px;
|
||||
}
|
@@ -0,0 +1,90 @@
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classnames from 'classnames';
|
||||
import { ClickOutside } from '@seafile/sf-metadata-ui-component';
|
||||
import { KeyCodes } from '../../../../../constants';
|
||||
import { isCellValueChanged } from '../../../utils/cell-comparer';
|
||||
import { gettext, getTrimmedString } from '../../../utils';
|
||||
import ObjectUtils from '../../../utils/object-utils';
|
||||
|
||||
import './index.css';
|
||||
|
||||
const TextEditor = React.memo(({ value: oldValue, onChange: onChangeAPI }) => {
|
||||
const [showEditor, setShowEditor] = useState(false);
|
||||
const ref = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
ref.current.innerText = oldValue || '';
|
||||
}, [oldValue]);
|
||||
|
||||
const closeEditor = useCallback(() => {
|
||||
const value = ref.current.innerText;
|
||||
if (value !== oldValue) {
|
||||
onChangeAPI(getTrimmedString(value) || null);
|
||||
}
|
||||
setShowEditor(false);
|
||||
}, [oldValue, onChangeAPI]);
|
||||
|
||||
const onPaste = useCallback((event) => {
|
||||
event.stopPropagation();
|
||||
}, []);
|
||||
|
||||
const onCut = useCallback((event) => {
|
||||
event.stopPropagation();
|
||||
}, []);
|
||||
|
||||
const onKeyDown = useCallback((event) => {
|
||||
const { selectionStart, selectionEnd, value } = event.currentTarget;
|
||||
if (event.keyCode === KeyCodes.Enter) {
|
||||
event.preventDefault();
|
||||
closeEditor();
|
||||
} else if (
|
||||
(event.keyCode === KeyCodes.ChineseInputMethod) ||
|
||||
(event.keyCode === KeyCodes.LeftArrow && selectionStart === 0) ||
|
||||
(event.keyCode === KeyCodes.RightArrow && selectionEnd === value.length)
|
||||
) {
|
||||
event.stopPropagation();
|
||||
}
|
||||
}, [closeEditor]);
|
||||
|
||||
const displayEditor = useCallback(() => {
|
||||
if (showEditor) return;
|
||||
setShowEditor(true);
|
||||
setTimeout(() => {
|
||||
ref.current.focus();
|
||||
const range = document.createRange();
|
||||
range.selectNodeContents(ref.current);
|
||||
range.collapse(false);
|
||||
const selection = window.getSelection();
|
||||
selection.removeAllRanges();
|
||||
selection.addRange(range);
|
||||
}, 1);
|
||||
}, [showEditor]);
|
||||
|
||||
return (
|
||||
<ClickOutside onClickOutside={closeEditor}>
|
||||
<div
|
||||
className={classnames('sf-metadata-property-detail-editor sf-metadata-text-property-detail-editor', { 'formatter': !showEditor })}
|
||||
onClick={displayEditor}
|
||||
ref={ref}
|
||||
onKeyDown={onKeyDown}
|
||||
onCut={onCut}
|
||||
onPaste={onPaste}
|
||||
placeholder={gettext('Empty')}
|
||||
contentEditable={showEditor}
|
||||
/>
|
||||
</ClickOutside>
|
||||
);
|
||||
}, (props, nextProps) => {
|
||||
const isChanged = isCellValueChanged(props.value, nextProps.value, nextProps.field.type) ||
|
||||
!ObjectUtils.isSameObject(props.field, nextProps.field);
|
||||
return !isChanged;
|
||||
});
|
||||
|
||||
TextEditor.propTypes = {
|
||||
value: PropTypes.string,
|
||||
field: PropTypes.object.isRequired,
|
||||
onChange: PropTypes.func,
|
||||
};
|
||||
|
||||
export default TextEditor;
|
@@ -1,7 +1,11 @@
|
||||
import DeleteConfirmDialog from './delete-confirm-dialog';
|
||||
import Table from './table';
|
||||
import DetailEditor from './detail-editor';
|
||||
import CellFormatter from './cell-formatter';
|
||||
|
||||
export {
|
||||
DeleteConfirmDialog,
|
||||
Table,
|
||||
DetailEditor,
|
||||
CellFormatter,
|
||||
};
|
||||
|
@@ -9,8 +9,8 @@ import {
|
||||
filterTermModifierIsWithin,
|
||||
isDateColumn,
|
||||
FILTER_ERR_MSG,
|
||||
getColumnOptions as getSelectColumnOptions,
|
||||
} from '../../../../../_basic';
|
||||
import { getSelectColumnOptions } from '../../../../../utils/select-utils';
|
||||
import CollaboratorFilter from './collaborator-filter';
|
||||
import FilterCalendar from '../filter-calendar';
|
||||
import FilterItemUtils from '../filter-item-utils';
|
||||
|
@@ -5,7 +5,7 @@
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
border-right: 1px solid #eee;
|
||||
padding: 4px 8px;
|
||||
padding: 6px 8px;
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
@@ -71,7 +71,7 @@
|
||||
/* cell formatter */
|
||||
.sf-metadata-result-table-cell .sf-metadata-ui.cell-formatter-container {
|
||||
height: 100%;
|
||||
line-height: 24px;
|
||||
line-height: 20px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
@@ -303,3 +303,17 @@
|
||||
padding-top: 5.5px;
|
||||
padding-bottom: 5.5px;
|
||||
}
|
||||
|
||||
/* file name */
|
||||
.sf-metadata-result-table-cell.sf-metadata-result-table-file-name-cell {
|
||||
padding-top: 4px;
|
||||
padding-bottom: 4px;
|
||||
}
|
||||
|
||||
.sf-metadata-result-table-cell.sf-metadata-result-table-file-name-cell .sf-metadata-ui.cell-formatter-container {
|
||||
line-height: 24px;
|
||||
}
|
||||
|
||||
.sf-metadata-result-table-file-name-cell .sf-metadata-ui.file-name-formatter .sf-metadata-file-icon {
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
@@ -1,9 +1,8 @@
|
||||
import metadataAPI from '../api';
|
||||
import { UserService, LocalStorage, PRIVATE_COLUMN_KEYS, EDITABLE_DATA_PRIVATE_COLUMN_KEYS,
|
||||
import { LocalStorage, PRIVATE_COLUMN_KEYS, EDITABLE_DATA_PRIVATE_COLUMN_KEYS,
|
||||
EDITABLE_PRIVATE_COLUMN_KEYS, PREDEFINED_COLUMN_KEYS } from './_basic';
|
||||
import EventBus from '../../components/common/event-bus';
|
||||
import { username } from '../../utils/constants';
|
||||
import User from './model/user';
|
||||
|
||||
class Context {
|
||||
|
||||
@@ -11,29 +10,26 @@ class Context {
|
||||
this.settings = {};
|
||||
this.metadataAPI = null;
|
||||
this.localStorage = null;
|
||||
this.userService = null;
|
||||
this.eventBus = null;
|
||||
this.hasInit = false;
|
||||
this.permission = 'r';
|
||||
this.collaboratorsCache = {};
|
||||
}
|
||||
|
||||
async init({ otherSettings }) {
|
||||
async init(settings) {
|
||||
if (this.hasInit) return;
|
||||
|
||||
// init settings
|
||||
this.settings = otherSettings || {};
|
||||
this.settings = settings || {};
|
||||
|
||||
// init metadataAPI
|
||||
const { mediaUrl, repoInfo } = this.settings;
|
||||
const { repoInfo } = this.settings;
|
||||
this.metadataAPI = metadataAPI;
|
||||
|
||||
// init localStorage
|
||||
const { repoID, viewID } = this.settings;
|
||||
this.localStorage = new LocalStorage(`sf-metadata-${repoID}-${viewID}`);
|
||||
|
||||
// init userService
|
||||
this.userService = new UserService({ mediaUrl, api: this.metadataAPI.listUserInfo });
|
||||
const localStorageName = viewID ? `sf-metadata-${repoID}-${viewID}` : `sf-metadata-${repoID}`;
|
||||
this.localStorage = new LocalStorage(localStorageName);
|
||||
|
||||
const eventBus = new EventBus();
|
||||
this.eventBus = eventBus;
|
||||
@@ -47,7 +43,6 @@ class Context {
|
||||
this.settings = {};
|
||||
this.metadataAPI = null;
|
||||
this.localStorage = null;
|
||||
this.userService = null;
|
||||
this.eventBus = null;
|
||||
this.hasInit = false;
|
||||
this.permission = 'r';
|
||||
@@ -135,23 +130,6 @@ class Context {
|
||||
return true;
|
||||
};
|
||||
|
||||
getCollaboratorFromCache(email) {
|
||||
return this.collaboratorsCache[email];
|
||||
}
|
||||
|
||||
getCollaboratorsFromCache() {
|
||||
const collaboratorsCache = this.collaboratorsCache;
|
||||
return Object.values(collaboratorsCache).filter(item => item.email !== 'anonymous');
|
||||
}
|
||||
|
||||
updateCollaboratorsCache(email, collaborator) {
|
||||
if (collaborator instanceof User) {
|
||||
this.collaboratorsCache[email] = collaborator;
|
||||
return;
|
||||
}
|
||||
this.collaboratorsCache[email] = new User(collaborator);
|
||||
}
|
||||
|
||||
restoreRows = () => {
|
||||
// todo
|
||||
};
|
||||
|
@@ -1,7 +1,5 @@
|
||||
import { CollaboratorsProvider, useCollaborators } from './collaborators';
|
||||
import { MetadataProvider, useMetadata } from './metadata';
|
||||
|
||||
export {
|
||||
CollaboratorsProvider, useCollaborators,
|
||||
MetadataProvider, useMetadata,
|
||||
};
|
||||
|
@@ -47,9 +47,8 @@ export const MetadataProvider = ({
|
||||
setLoading(true);
|
||||
// init context
|
||||
const context = new Context();
|
||||
window.sfMetadata = {};
|
||||
window.sfMetadataContext = context;
|
||||
window.sfMetadataContext.init({ otherSettings: { ...params, repoID, viewID } });
|
||||
window.sfMetadataContext.init({ ...params, repoID, viewID });
|
||||
storeRef.current = new Store({ context: window.sfMetadataContext, repoId: repoID, viewId: viewID });
|
||||
window.sfMetadataStore = storeRef.current;
|
||||
storeRef.current.initStartIndex();
|
||||
@@ -68,7 +67,6 @@ export const MetadataProvider = ({
|
||||
const unsubscribeReloadData = eventBus.subscribe(EVENT_BUS_TYPE.RELOAD_DATA, reloadMetadata);
|
||||
|
||||
return () => {
|
||||
window.sfMetadata = {};
|
||||
window.sfMetadataContext.destroy();
|
||||
window.sfMetadataStore.destroy();
|
||||
unsubscribeServerTableChanged();
|
||||
|
@@ -1,15 +1,13 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { MetadataProvider, CollaboratorsProvider } from './hooks/index';
|
||||
import { Table } from './components/index';
|
||||
import { MetadataProvider } from './hooks';
|
||||
import { Table, DetailEditor, CellFormatter } from './components';
|
||||
import Context from './context';
|
||||
|
||||
const SeafileMetadata = ({ ...params }) => {
|
||||
|
||||
return (
|
||||
<MetadataProvider { ...params }>
|
||||
<CollaboratorsProvider >
|
||||
<Table />
|
||||
</CollaboratorsProvider>
|
||||
</MetadataProvider>
|
||||
);
|
||||
};
|
||||
@@ -21,3 +19,8 @@ SeafileMetadata.propTypes = {
|
||||
};
|
||||
|
||||
export default SeafileMetadata;
|
||||
export {
|
||||
Context,
|
||||
DetailEditor,
|
||||
CellFormatter,
|
||||
};
|
||||
|
@@ -327,7 +327,7 @@ export const normalizeColumns = (columns) => {
|
||||
return displayColumns;
|
||||
};
|
||||
|
||||
export function canEdit(col, record, enableCellSelect) {
|
||||
export function canEditCell(col, record, enableCellSelect) {
|
||||
if (!col) return false;
|
||||
if (window.sfMetadataContext.canModifyColumn(col) === false) return false;
|
||||
if (col.editable != null && typeof (col.editable) === 'function') {
|
||||
|
@@ -1,6 +1,7 @@
|
||||
import { CellType, DEFAULT_DATE_FORMAT, generatorCellOption, getCollaboratorsName, getOptionName, getDateDisplayString, PREDEFINED_COLUMN_KEYS } from '../_basic';
|
||||
import { CellType, DEFAULT_DATE_FORMAT, generatorCellOption, getCollaboratorsName, getOptionName, getDateDisplayString,
|
||||
PREDEFINED_COLUMN_KEYS, getFloatNumber, getNumberDisplayString, formatStringToNumber, isNumber, getColumnOptions,
|
||||
} from '../_basic';
|
||||
import { formatTextToDate } from './date';
|
||||
import { getFloatNumber, getNumberDisplayString, formatStringToNumber, isNumber } from '../_basic/utils/cell/column/number';
|
||||
|
||||
const SUPPORT_PASTE_FROM_COLUMN = {
|
||||
[CellType.NUMBER]: [CellType.TEXT, CellType.NUMBER],
|
||||
@@ -8,13 +9,6 @@ const SUPPORT_PASTE_FROM_COLUMN = {
|
||||
|
||||
const reg_chinese_date_format = /(\d{4})年(\d{1,2})月(\d{1,2})日$/;
|
||||
|
||||
export function getSelectColumnOptions(column) {
|
||||
if (!column || !column.data || !Array.isArray(column.data.options)) {
|
||||
return [];
|
||||
}
|
||||
return column.data.options;
|
||||
}
|
||||
|
||||
function convertCellValue(cellValue, oldCellValue, targetColumn, fromColumn) {
|
||||
const { type: fromColumnType, data: fromColumnData } = fromColumn;
|
||||
const { type: targetColumnType, data: targetColumnData } = targetColumn;
|
||||
@@ -140,7 +134,7 @@ function convert2SingleSelect(cellValue, oldCellValue, fromColumn, targetColumn)
|
||||
let fromOptionName;
|
||||
switch (fromColumnType) {
|
||||
case CellType.SINGLE_SELECT: {
|
||||
const fromOptions = getSelectColumnOptions(fromColumn);
|
||||
const fromOptions = getColumnOptions(fromColumn);
|
||||
fromOptionName = getOptionName(fromOptions, cellValue) || '';
|
||||
break;
|
||||
}
|
||||
@@ -156,7 +150,7 @@ function convert2SingleSelect(cellValue, oldCellValue, fromColumn, targetColumn)
|
||||
return oldCellValue;
|
||||
}
|
||||
|
||||
const currentOptions = getSelectColumnOptions(targetColumn);
|
||||
const currentOptions = getColumnOptions(targetColumn);
|
||||
const newOption = generatorCellOption(currentOptions, fromOptionName);
|
||||
return PREDEFINED_COLUMN_KEYS.includes(targetColumn.key) ? newOption.id : newOption.name;
|
||||
}
|
||||
@@ -230,11 +224,11 @@ function convert2Text(cellValue, oldCellValue, fromColumn) {
|
||||
return getDateDisplayString(cellValue, fromColumnData.format || DEFAULT_DATE_FORMAT);
|
||||
}
|
||||
case CellType.SINGLE_SELECT: {
|
||||
const options = getSelectColumnOptions(fromColumn);
|
||||
const options = getColumnOptions(fromColumn);
|
||||
return getOptionName(options, cellValue) || null;
|
||||
}
|
||||
case CellType.COLLABORATOR: {
|
||||
const collaborators = window.sfMetadata.collaborators || window.sfMetadataContext.getCollaboratorsFromCache();
|
||||
const collaborators = window.sfMetadata.getCollaborators();
|
||||
return getCollaboratorsName(collaborators, cellValue);
|
||||
}
|
||||
case CellType.CREATOR:
|
||||
@@ -242,7 +236,7 @@ function convert2Text(cellValue, oldCellValue, fromColumn) {
|
||||
if (!cellValue) {
|
||||
return null;
|
||||
}
|
||||
const collaborators = window.sfMetadata.collaborators || window.sfMetadataContext.getCollaboratorsFromCache();
|
||||
const collaborators = window.sfMetadata.getCollaborators();
|
||||
return getCollaboratorsName(collaborators, [cellValue]);
|
||||
}
|
||||
default: {
|
||||
@@ -257,7 +251,7 @@ function convert2Collaborator(cellValue, oldCellValue, fromColumnType) {
|
||||
if (!Array.isArray(cellValue) || cellValue.length === 0) {
|
||||
return null;
|
||||
}
|
||||
const collaborators = window.sfMetadata.collaborators || window.sfMetadataContext.getCollaboratorsFromCache();
|
||||
const collaborators = window.sfMetadata.getCollaborators();
|
||||
let validEmailMap = {};
|
||||
collaborators.forEach(collaborator => validEmailMap[collaborator.email] = true);
|
||||
return cellValue.filter(email => !!validEmailMap[email]);
|
||||
@@ -270,7 +264,7 @@ function convert2Collaborator(cellValue, oldCellValue, fromColumnType) {
|
||||
if (userNames.length === 0) {
|
||||
return oldCellValue;
|
||||
}
|
||||
const collaborators = window.sfMetadata.collaborators || window.sfMetadataContext.getCollaboratorsFromCache();
|
||||
const collaborators = window.sfMetadata.getCollaborators();
|
||||
let nameCollaboratorMap = {};
|
||||
collaborators.forEach(collaborator => nameCollaboratorMap[collaborator.name] = collaborator);
|
||||
const emails = userNames.map(name => {
|
||||
@@ -284,7 +278,7 @@ function convert2Collaborator(cellValue, oldCellValue, fromColumnType) {
|
||||
}
|
||||
case CellType.CREATOR:
|
||||
case CellType.LAST_MODIFIER: {
|
||||
const collaborators = window.sfMetadata.collaborators || window.sfMetadataContext.getCollaboratorsFromCache();
|
||||
const collaborators = window.sfMetadata.getCollaborators();
|
||||
let validEmailMap = {};
|
||||
collaborators.forEach(collaborator => validEmailMap[collaborator.email] = true);
|
||||
if (!cellValue || !validEmailMap[cellValue]) {
|
||||
|
@@ -1,13 +1,6 @@
|
||||
import { SELECT_OPTION_COLORS } from '../_basic/constants/select-option';
|
||||
import { generateOptionID } from '../_basic/utils/column/option';
|
||||
|
||||
export function getSelectColumnOptions(column) {
|
||||
if (!column || !column.data || !Array.isArray(column.data.options)) {
|
||||
return [];
|
||||
}
|
||||
return column.data.options;
|
||||
}
|
||||
|
||||
const getNotDuplicateOption = (options) => {
|
||||
const defaultOptions = SELECT_OPTION_COLORS.slice(12, 24);
|
||||
let defaultOption = defaultOptions[Math.floor(Math.random() * defaultOptions.length)];
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import { Z_INDEX, getGroupByPath, isFunction, getCellValueByColumn } from '../_basic';
|
||||
import { getColumnByIndex, canEdit } from './column-utils';
|
||||
import { getColumnByIndex, canEditCell } from './column-utils';
|
||||
import { SUPPORT_PREVIEW_COLUMN_TYPES } from '../constants';
|
||||
import { getGroupRecordByIndex } from './group-metrics';
|
||||
|
||||
@@ -44,7 +44,7 @@ export const isSelectedCellEditable = ({ enableCellSelect, selectedPosition, col
|
||||
const row = getSelectedRow({ selectedPosition, isGroupView, recordGetterByIndex });
|
||||
if (!window.sfMetadataContext.canModifyRow(row)) return false;
|
||||
const isCellEditable = isFunction(onCheckCellIsEditable) ? onCheckCellIsEditable({ row, column, ...selectedPosition }) : true;
|
||||
return isCellEditable && canEdit(column, row, enableCellSelect);
|
||||
return isCellEditable && canEditCell(column, row, enableCellSelect);
|
||||
};
|
||||
|
||||
export function selectedRangeIsSingleCell(selectedRange) {
|
||||
|
@@ -24,7 +24,7 @@ import CopyMoveDirentProgressDialog from '../../components/dialog/copy-move-dire
|
||||
import DeleteFolderDialog from '../../components/dialog/delete-folder-dialog';
|
||||
import { EVENT_BUS_TYPE } from '../../components/common/event-bus-type';
|
||||
import { PRIVATE_FILE_TYPE } from '../../constants';
|
||||
import { MetadataProvider } from '../../metadata/hooks';
|
||||
import { MetadataProvider, CollaboratorsProvider } from '../../metadata/hooks';
|
||||
|
||||
const propTypes = {
|
||||
eventBus: PropTypes.object,
|
||||
@@ -2174,6 +2174,7 @@ class LibContentView extends React.Component {
|
||||
renameMetadataView={this.renameMetadataView}
|
||||
hideMetadataView={this.hideFileMetadata}
|
||||
>
|
||||
<CollaboratorsProvider repoID={this.props.repoID}>
|
||||
<div className="main-panel-center flex-row">
|
||||
<LibContentContainer
|
||||
isSidePanelFolded={this.props.isSidePanelFolded}
|
||||
@@ -2291,6 +2292,7 @@ class LibContentView extends React.Component {
|
||||
<MediaQuery query="(max-width: 767.8px)">
|
||||
<Modal zIndex="1030" isOpen={!Utils.isDesktop() && this.state.isTreePanelShown} toggle={this.toggleTreePanel} contentClassName="d-none"></Modal>
|
||||
</MediaQuery>
|
||||
</CollaboratorsProvider>
|
||||
</MetadataProvider>
|
||||
);
|
||||
}
|
||||
|
Reference in New Issue
Block a user