mirror of
https://github.com/haiwen/seahub.git
synced 2025-09-11 03:41:12 +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/sdoc-editor": "1.0.50",
|
||||||
"@seafile/seafile-calendar": "0.0.12",
|
"@seafile/seafile-calendar": "0.0.12",
|
||||||
"@seafile/seafile-editor": "1.0.109",
|
"@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/codemirror-extensions-langs": "^4.19.4",
|
||||||
"@uiw/react-codemirror": "^4.19.4",
|
"@uiw/react-codemirror": "^4.19.4",
|
||||||
"axios": "^1.7.3",
|
"axios": "^1.7.3",
|
||||||
@@ -5094,9 +5094,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@seafile/sf-metadata-ui-component": {
|
"node_modules/@seafile/sf-metadata-ui-component": {
|
||||||
"version": "0.0.18",
|
"version": "0.0.20",
|
||||||
"resolved": "https://registry.npmjs.org/@seafile/sf-metadata-ui-component/-/sf-metadata-ui-component-0.0.18.tgz",
|
"resolved": "https://registry.npmjs.org/@seafile/sf-metadata-ui-component/-/sf-metadata-ui-component-0.0.20.tgz",
|
||||||
"integrity": "sha512-jC4JL06dAntWIr/ZPA/zJ4I9NhDfw0ismvSQuGZsBMvdrdbBBGuFVxirohB9zzwzc0k/K2epyQ/1iARKv5wjow==",
|
"integrity": "sha512-nCeqHgwAkd05GKd3YROf3k7XEQuh6FicOeYVXcbCXjzH1pki8XxLcv1NW4D85LvWh2eS6m+utegcAlXfS1EsJQ==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@seafile/seafile-calendar": "0.0.24",
|
"@seafile/seafile-calendar": "0.0.24",
|
||||||
"@seafile/seafile-editor": "~1.0.102",
|
"@seafile/seafile-editor": "~1.0.102",
|
||||||
|
@@ -14,7 +14,7 @@
|
|||||||
"@seafile/sdoc-editor": "1.0.50",
|
"@seafile/sdoc-editor": "1.0.50",
|
||||||
"@seafile/seafile-calendar": "0.0.12",
|
"@seafile/seafile-calendar": "0.0.12",
|
||||||
"@seafile/seafile-editor": "1.0.109",
|
"@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/codemirror-extensions-langs": "^4.19.4",
|
||||||
"@uiw/react-codemirror": "^4.19.4",
|
"@uiw/react-codemirror": "^4.19.4",
|
||||||
"axios": "^1.7.3",
|
"axios": "^1.7.3",
|
||||||
|
@@ -12,12 +12,12 @@
|
|||||||
|
|
||||||
.dirent-detail-item .dirent-detail-item-name {
|
.dirent-detail-item .dirent-detail-item-name {
|
||||||
width: 160px;
|
width: 160px;
|
||||||
padding: 7px 6px;
|
padding: 6.5px 6px;
|
||||||
min-height: 34px;
|
min-height: 34px;
|
||||||
height: fit-content;
|
height: fit-content;
|
||||||
color: #666;
|
color: #666;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
line-height: 1.4;
|
line-height: 1.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dirent-detail-item .dirent-detail-item-name .sf-metadata-icon {
|
.dirent-detail-item .dirent-detail-item-name .sf-metadata-icon {
|
||||||
@@ -29,13 +29,8 @@
|
|||||||
.dirent-detail-item .dirent-detail-item-value {
|
.dirent-detail-item .dirent-detail-item-value {
|
||||||
width: 200px;
|
width: 200px;
|
||||||
display: flex;
|
display: flex;
|
||||||
padding: 7px 6px;
|
|
||||||
min-height: 34px;
|
|
||||||
height: fit-content;
|
height: fit-content;
|
||||||
}
|
min-height: 34px;
|
||||||
|
|
||||||
.dirent-detail-item .dirent-detail-item-value.editable:hover {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.dirent-detail-item .dirent-detail-item-name:hover,
|
.dirent-detail-item .dirent-detail-item-name:hover,
|
||||||
@@ -45,10 +40,48 @@
|
|||||||
cursor: default;
|
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 .text-formatter,
|
||||||
.dirent-detail-item .dirent-detail-item-value .ctime-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 .mtime-formatter,
|
||||||
.dirent-detail-item .dirent-detail-item-value .date-formatter {
|
.dirent-detail-item .dirent-detail-item-value .date-formatter {
|
||||||
|
padding: 6.5px 6px;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,28 +90,5 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.dirent-detail-item-value .creator-formatter {
|
.dirent-detail-item-value .creator-formatter {
|
||||||
height: 20px;
|
padding: 7px 6px;
|
||||||
}
|
|
||||||
|
|
||||||
.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;
|
|
||||||
}
|
}
|
||||||
|
@@ -1,40 +1,39 @@
|
|||||||
import React, { useMemo } from 'react';
|
import React, { useMemo } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { Formatter, Icon } from '@seafile/sf-metadata-ui-component';
|
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
|
import { Icon } from '@seafile/sf-metadata-ui-component';
|
||||||
import { CellType, COLUMNS_ICON_CONFIG } from '../../../metadata/metadata-view/_basic';
|
import { CellType, COLUMNS_ICON_CONFIG } from '../../../metadata/metadata-view/_basic';
|
||||||
import { gettext } from '../../../utils/constants';
|
|
||||||
|
|
||||||
import './index.css';
|
import './index.css';
|
||||||
|
|
||||||
const DetailItem = ({ field, value, valueId, valueClick, children, ...params }) => {
|
const DetailItem = ({ readonly, field, className, children }) => {
|
||||||
const icon = useMemo(() => {
|
const icon = useMemo(() => {
|
||||||
if (field.type === 'size') return COLUMNS_ICON_CONFIG[CellType.NUMBER];
|
if (field.type === 'size') return COLUMNS_ICON_CONFIG[CellType.NUMBER];
|
||||||
return COLUMNS_ICON_CONFIG[field.type];
|
return COLUMNS_ICON_CONFIG[field.type];
|
||||||
}, [field]);
|
}, [field]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="dirent-detail-item">
|
<div className={classnames('dirent-detail-item', className)}>
|
||||||
<div className="dirent-detail-item-name">
|
<div className="dirent-detail-item-name">
|
||||||
<Icon iconName={icon} />
|
<Icon iconName={icon} />
|
||||||
<span className="dirent-detail-item-name-value">{field.name}</span>
|
<span className="dirent-detail-item-name-value">{field.name}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className={classnames('dirent-detail-item-value', { 'editable': valueClick })} id={valueId} onClick={valueClick}>
|
<div className={classnames('dirent-detail-item-value', { 'editable': !readonly })} >
|
||||||
{children ? children : (<Formatter { ...params } field={field} value={value} />)}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
DetailItem.defaultProps = {
|
DetailItem.defaultProps = {
|
||||||
emptyTip: gettext('Empty')
|
readonly: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
DetailItem.propTypes = {
|
DetailItem.propTypes = {
|
||||||
|
readonly: PropTypes.bool,
|
||||||
field: PropTypes.object.isRequired,
|
field: PropTypes.object.isRequired,
|
||||||
value: PropTypes.any,
|
className: PropTypes.string,
|
||||||
children: PropTypes.any,
|
children: PropTypes.any,
|
||||||
valueId: PropTypes.string,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default DetailItem;
|
export default DetailItem;
|
||||||
|
@@ -1,20 +1,26 @@
|
|||||||
import React, { useMemo } from 'react';
|
import React, { useMemo } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
import { Formatter } from '@seafile/sf-metadata-ui-component';
|
||||||
import { getDirentPath } from './utils';
|
import { getDirentPath } from './utils';
|
||||||
import DetailItem from '../detail-item';
|
import DetailItem from '../detail-item';
|
||||||
import { CellType } from '../../../metadata/metadata-view/_basic';
|
import { CellType } from '../../../metadata/metadata-view/_basic';
|
||||||
import { gettext } from '../../../utils/constants';
|
import { gettext } from '../../../utils/constants';
|
||||||
import { MetadataDetails, useMetadata } from '../../../metadata';
|
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 direntPath = useMemo(() => getDirentPath(dirent, path), [dirent, path]);
|
||||||
const { enableMetadata } = useMetadata();
|
const { enableMetadata } = useMetadata();
|
||||||
|
const lastModifiedTimeField = useMemo(() => {
|
||||||
|
return { type: CellType.MTIME, name: gettext('Last modified time') };
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
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 && (
|
{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 React, { useCallback, useMemo, useState } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { v4 as uuidV4 } from 'uuid';
|
import { v4 as uuidV4 } from 'uuid';
|
||||||
|
import { Formatter } from '@seafile/sf-metadata-ui-component';
|
||||||
import { getDirentPath } from './utils';
|
import { getDirentPath } from './utils';
|
||||||
import DetailItem from '../detail-item';
|
import DetailItem from '../detail-item';
|
||||||
import { CellType } from '../../../metadata/metadata-view/_basic';
|
import { CellType } from '../../../metadata/metadata-view/_basic';
|
||||||
@@ -11,12 +12,16 @@ import { Utils } from '../../../utils/utils';
|
|||||||
import { MetadataDetails, useMetadata } from '../../../metadata';
|
import { MetadataDetails, useMetadata } from '../../../metadata';
|
||||||
import ObjectUtils from '../../../metadata/metadata-view/utils/object-utils';
|
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 [isEditFileTagShow, setEditFileTagShow] = useState(false);
|
||||||
const { enableMetadata } = useMetadata();
|
const { enableMetadata } = useMetadata();
|
||||||
|
|
||||||
const direntPath = useMemo(() => getDirentPath(dirent, path), [dirent, path]);
|
const direntPath = useMemo(() => getDirentPath(dirent, path), [dirent, path]);
|
||||||
const tagListTitleID = useMemo(() => `detail-list-view-tags-${uuidV4()}`, []);
|
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(() => {
|
const onEditFileTagToggle = useCallback(() => {
|
||||||
setEditFileTagShow(!isEditFileTagShow);
|
setEditFileTagShow(!isEditFileTagShow);
|
||||||
@@ -28,25 +33,37 @@ const FileDetails = React.memo(({ repoID, repoInfo, dirent, path, direntDetail,
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<DetailItem field={{ type: 'size', name: gettext('Size') }} value={Utils.bytesToSize(direntDetail.size)} />
|
<DetailItem field={sizeField} className="sf-metadata-property-detail-formatter">
|
||||||
<DetailItem field={{ type: CellType.LAST_MODIFIER, name: gettext('Last modifier') }} value={direntDetail.last_modifier_email} collaborators={[{
|
<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,
|
name: direntDetail.last_modifier_name,
|
||||||
contact_email: direntDetail.last_modifier_contact_email,
|
contact_email: direntDetail.last_modifier_contact_email,
|
||||||
email: direntDetail.last_modifier_email,
|
email: direntDetail.last_modifier_email,
|
||||||
avatar_url: direntDetail.last_modifier_avatar,
|
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 && (
|
{!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 ? (
|
{Array.isArray(fileTagList) && fileTagList.length > 0 ? (
|
||||||
<FileTagList fileTagList={fileTagList} />
|
<FileTagList fileTagList={fileTagList} />
|
||||||
) : (
|
) : (
|
||||||
<span className="empty-tip-text">{gettext('Empty')}</span>
|
<span className="empty-tip-text">{gettext('Empty')}</span>
|
||||||
)}
|
)}
|
||||||
|
</div>
|
||||||
</DetailItem>
|
</DetailItem>
|
||||||
)}
|
)}
|
||||||
{window.app.pageOptions.enableMetadataManagement && (
|
{window.app.pageOptions.enableMetadataManagement && (
|
||||||
<MetadataDetails repoID={repoID} filePath={direntPath} direntType="file" { ...params } />
|
<MetadataDetails repoID={repoID} filePath={direntPath} repoInfo={repoInfo} direntType="file" />
|
||||||
)}
|
)}
|
||||||
{isEditFileTagShow &&
|
{isEditFileTagShow &&
|
||||||
<EditFileTagPopover
|
<EditFileTagPopover
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { siteRoot, mediaUrl } from '../../../utils/constants';
|
import { siteRoot } from '../../../utils/constants';
|
||||||
import { seafileAPI } from '../../../utils/seafile-api';
|
import { seafileAPI } from '../../../utils/seafile-api';
|
||||||
import { Utils } from '../../../utils/utils';
|
import { Utils } from '../../../utils/utils';
|
||||||
import toaster from '../../toast';
|
import toaster from '../../toast';
|
||||||
@@ -9,9 +9,6 @@ import { Detail, Header, Body } from '../detail';
|
|||||||
import DirDetails from './dir-details';
|
import DirDetails from './dir-details';
|
||||||
import FileDetails from './file-details';
|
import FileDetails from './file-details';
|
||||||
import ObjectUtils from '../../../metadata/metadata-view/utils/object-utils';
|
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';
|
import './index.css';
|
||||||
|
|
||||||
@@ -22,26 +19,9 @@ class DirentDetails extends React.Component {
|
|||||||
this.state = {
|
this.state = {
|
||||||
direntDetail: '',
|
direntDetail: '',
|
||||||
dirent: null,
|
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) => {
|
updateDetail = (repoID, dirent, direntPath) => {
|
||||||
const apiName = dirent.type === 'file' ? 'getFileInfo' : 'getDirInfo';
|
const apiName = dirent.type === 'file' ? 'getFileInfo' : 'getDirInfo';
|
||||||
seafileAPI[apiName](repoID, direntPath).then(res => {
|
seafileAPI[apiName](repoID, direntPath).then(res => {
|
||||||
@@ -73,7 +53,6 @@ class DirentDetails extends React.Component {
|
|||||||
};
|
};
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.loadCollaborators();
|
|
||||||
this.loadDetail(this.props.repoID, this.props.dirent, this.props.path);
|
this.loadDetail(this.props.repoID, this.props.dirent, this.props.path);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -108,7 +87,7 @@ class DirentDetails extends React.Component {
|
|||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { dirent, direntDetail, collaborators, collaboratorsCache } = this.state;
|
const { dirent, direntDetail } = this.state;
|
||||||
const { repoID, path, fileTags } = this.props;
|
const { repoID, path, fileTags } = this.props;
|
||||||
const direntName = dirent?.name || '';
|
const direntName = dirent?.name || '';
|
||||||
const smallIconUrl = Utils.getDirentIcon(dirent);
|
const smallIconUrl = Utils.getDirentIcon(dirent);
|
||||||
@@ -127,10 +106,6 @@ class DirentDetails extends React.Component {
|
|||||||
dirent={dirent}
|
dirent={dirent}
|
||||||
direntDetail={direntDetail}
|
direntDetail={direntDetail}
|
||||||
path={this.props.dirent ? path + '/' + dirent.name : path}
|
path={this.props.dirent ? path + '/' + dirent.name : path}
|
||||||
collaborators={collaborators}
|
|
||||||
collaboratorsCache={collaboratorsCache}
|
|
||||||
updateCollaboratorsCache={this.updateCollaboratorsCache}
|
|
||||||
queryUserAPI={this.userService?.queryUser}
|
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<FileDetails
|
<FileDetails
|
||||||
@@ -142,10 +117,6 @@ class DirentDetails extends React.Component {
|
|||||||
repoTags={this.props.repoTags}
|
repoTags={this.props.repoTags}
|
||||||
fileTagList={dirent ? dirent.file_tags : fileTags}
|
fileTagList={dirent ? dirent.file_tags : fileTags}
|
||||||
onFileTagChanged={this.props.onFileTagChanged}
|
onFileTagChanged={this.props.onFileTagChanged}
|
||||||
collaborators={collaborators}
|
|
||||||
collaboratorsCache={collaboratorsCache}
|
|
||||||
updateCollaboratorsCache={this.updateCollaboratorsCache}
|
|
||||||
queryUserAPI={this.userService?.queryUser}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
@@ -1,11 +1,24 @@
|
|||||||
import React from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import LibDetail from './lib-details';
|
import LibDetail from './lib-details';
|
||||||
import DirentDetail from './dirent-details';
|
import DirentDetail from './dirent-details';
|
||||||
import ObjectUtils from '../../metadata/metadata-view/utils/object-utils';
|
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 }) => {
|
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) {
|
if (path === '/' && !dirent) {
|
||||||
return (
|
return (
|
||||||
<LibDetail currentRepoInfo={currentRepoInfo} onClose={onClose} />
|
<LibDetail currentRepoInfo={currentRepoInfo} onClose={onClose} />
|
||||||
|
@@ -1,5 +1,6 @@
|
|||||||
import React, { useEffect, useMemo, useState } from 'react';
|
import React, { useEffect, useMemo, useState } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
import { Formatter } from '@seafile/sf-metadata-ui-component';
|
||||||
import { Utils } from '../../../utils/utils';
|
import { Utils } from '../../../utils/utils';
|
||||||
import { gettext } from '../../../utils/constants';
|
import { gettext } from '../../../utils/constants';
|
||||||
import { seafileAPI } from '../../../utils/seafile-api';
|
import { seafileAPI } from '../../../utils/seafile-api';
|
||||||
@@ -14,6 +15,10 @@ const LibDetail = React.memo(({ currentRepoInfo, onClose }) => {
|
|||||||
const [isLoading, setLoading] = useState(true);
|
const [isLoading, setLoading] = useState(true);
|
||||||
const [repo, setRepo] = useState({});
|
const [repo, setRepo] = useState({});
|
||||||
const smallIconUrl = useMemo(() => Utils.getLibIconUrl(currentRepoInfo), [currentRepoInfo]);
|
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(() => {
|
useEffect(() => {
|
||||||
setLoading(true);
|
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="w-100 h-100 d-flex algin-items-center justify-content-center"><Loading /></div>
|
||||||
) : (
|
) : (
|
||||||
<div className="detail-content">
|
<div className="detail-content">
|
||||||
<DetailItem field={{ type: CellType.NUMBER, name: gettext('Files') }} value={repo.file_count} />
|
<DetailItem field={filesField} value={repo.file_count || 0} className="sf-metadata-property-detail-formatter">
|
||||||
<DetailItem field={{ type: 'size', name: gettext('Size') }} value={repo.size} />
|
<Formatter field={filesField} value={repo.file_count || 0} />
|
||||||
<DetailItem field={{ type: CellType.CREATOR, name: gettext('Creator') }} value={repo.owner_email} collaborators={[{
|
</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,
|
name: repo.owner_name,
|
||||||
contact_email: repo.owner_contact_email,
|
contact_email: repo.owner_contact_email,
|
||||||
email: repo.owner_email,
|
email: repo.owner_email,
|
||||||
avatar_url: repo.owner_avatar,
|
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>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Body>
|
</Body>
|
||||||
|
@@ -1,25 +1,39 @@
|
|||||||
/* eslint-disable react/prop-types */
|
/* eslint-disable react/prop-types */
|
||||||
import React, { useContext, useState, useCallback, useEffect } from 'react';
|
import React, { useContext, useState, useCallback, useEffect, useMemo } from 'react';
|
||||||
import { useMetadata } from './metadata';
|
import { mediaUrl } from '../../utils/constants';
|
||||||
import { mediaUrl } from '../../../utils/constants';
|
import { isValidEmail, UserService } from '../metadata-view/_basic';
|
||||||
import { isValidEmail } from '../_basic';
|
import User from '../metadata-view/model/user';
|
||||||
|
import metadataAPI from '../api';
|
||||||
|
|
||||||
const CollaboratorsContext = React.createContext(null);
|
const CollaboratorsContext = React.createContext(null);
|
||||||
|
|
||||||
export const CollaboratorsProvider = ({
|
export const CollaboratorsProvider = ({ repoID, children }) => {
|
||||||
children,
|
|
||||||
}) => {
|
|
||||||
const [collaboratorsCache, setCollaboratorsCache] = useState({});
|
const [collaboratorsCache, setCollaboratorsCache] = useState({});
|
||||||
const [collaborators, setCollaborators] = useState([]);
|
const [collaborators, setCollaborators] = useState([]);
|
||||||
|
const queryUser = useMemo(() => {
|
||||||
const { store } = useMetadata();
|
const userService = new UserService({ mediaUrl, api: metadataAPI.listUserInfo });
|
||||||
|
const queryUserAPI = userService.queryUser;
|
||||||
|
window.queryUser = queryUserAPI;
|
||||||
|
return queryUserAPI;
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setCollaborators(store?.collaborators || []);
|
metadataAPI.getCollaborators(repoID).then(res => {
|
||||||
}, [store?.collaborators]);
|
const collaborators = Array.isArray(res?.data?.user_list) ? res.data.user_list.map(user => new User(user)) : [];
|
||||||
|
setCollaborators(collaborators);
|
||||||
|
});
|
||||||
|
}, [repoID]);
|
||||||
|
|
||||||
useEffect(() => {
|
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.collaborators = collaborators;
|
||||||
window.sfMetadata.collaboratorsCache = collaboratorsCache;
|
window.sfMetadata.collaboratorsCache = collaboratorsCache;
|
||||||
}, [collaborators, collaboratorsCache]);
|
}, [collaborators, collaboratorsCache]);
|
||||||
@@ -54,7 +68,7 @@ export const CollaboratorsProvider = ({
|
|||||||
}, [collaborators, collaboratorsCache]);
|
}, [collaborators, collaboratorsCache]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CollaboratorsContext.Provider value={{ collaborators, collaboratorsCache, updateCollaboratorsCache, getCollaborator }}>
|
<CollaboratorsContext.Provider value={{ collaborators, collaboratorsCache, updateCollaboratorsCache, getCollaborator, queryUser }}>
|
||||||
{children}
|
{children}
|
||||||
</CollaboratorsContext.Provider>
|
</CollaboratorsContext.Provider>
|
||||||
);
|
);
|
||||||
@@ -65,6 +79,6 @@ export const useCollaborators = () => {
|
|||||||
if (!context) {
|
if (!context) {
|
||||||
throw new Error('\'CollaboratorsContext\' is null');
|
throw new Error('\'CollaboratorsContext\' is null');
|
||||||
}
|
}
|
||||||
const { collaborators, collaboratorsCache, updateCollaboratorsCache, getCollaborator } = context;
|
const { collaborators, collaboratorsCache, updateCollaboratorsCache, getCollaborator, queryUser } = context;
|
||||||
return { collaborators, collaboratorsCache, updateCollaboratorsCache, getCollaborator };
|
return { collaborators, collaboratorsCache, updateCollaboratorsCache, getCollaborator, queryUser };
|
||||||
};
|
};
|
@@ -1 +1,2 @@
|
|||||||
export { MetadataProvider, useMetadata } from './metadata';
|
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 [navigation, setNavigation] = useState([]);
|
||||||
const viewsMap = useRef({});
|
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(() => {
|
useEffect(() => {
|
||||||
if (!enableMetadataManagement) {
|
if (!enableMetadataManagement) {
|
||||||
setEnableExtendedProperties(false);
|
cancelURLView();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
metadataAPI.getMetadataStatus(repoID).then(res => {
|
metadataAPI.getMetadataStatus(repoID).then(res => {
|
||||||
setEnableExtendedProperties(res.data.enabled);
|
const enableMetadata = res.data.enabled;
|
||||||
|
if (!enableMetadata) {
|
||||||
|
cancelURLView();
|
||||||
|
}
|
||||||
|
setEnableExtendedProperties(enableMetadata);
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
const errorMsg = Utils.getErrorMsg(error, true);
|
const errorMsg = Utils.getErrorMsg(error, true);
|
||||||
toaster.danger(errorMsg);
|
toaster.danger(errorMsg);
|
||||||
setEnableExtendedProperties(false);
|
setEnableExtendedProperties(false);
|
||||||
});
|
});
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [repoID, enableMetadataManagement]);
|
}, [repoID, enableMetadataManagement]);
|
||||||
|
|
||||||
const updateEnableMetadata = useCallback((newValue) => {
|
const updateEnableMetadata = useCallback((newValue) => {
|
||||||
if (newValue === enableMetadata) return;
|
if (newValue === enableMetadata) return;
|
||||||
if (!newValue) {
|
if (!newValue) {
|
||||||
hideMetadataView && hideMetadataView();
|
hideMetadataView && hideMetadataView();
|
||||||
|
cancelURLView();
|
||||||
} else {
|
} else {
|
||||||
setShowFirstView(true);
|
setShowFirstView(true);
|
||||||
}
|
}
|
||||||
setEnableExtendedProperties(newValue);
|
setEnableExtendedProperties(newValue);
|
||||||
}, [enableMetadata, hideMetadataView]);
|
}, [enableMetadata, hideMetadataView, cancelURLView]);
|
||||||
|
|
||||||
// views
|
// views
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -61,14 +78,6 @@ export const MetadataProvider = ({ repoID, hideMetadataView, selectMetadataView,
|
|||||||
return;
|
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 = {};
|
viewsMap.current = {};
|
||||||
setNavigation([]);
|
setNavigation([]);
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// 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 MetadataStatusManagementDialog from './metadata-status-manage-dialog';
|
||||||
import MetadataTreeView from './metadata-tree-view';
|
import MetadataTreeView from './metadata-tree-view';
|
||||||
import MetadataDetails from './metadata-details';
|
import MetadataDetails from './metadata-details';
|
||||||
@@ -8,6 +8,7 @@ export * from './hooks';
|
|||||||
|
|
||||||
export {
|
export {
|
||||||
metadataAPI,
|
metadataAPI,
|
||||||
|
MetadataContext,
|
||||||
SeafileMetadata,
|
SeafileMetadata,
|
||||||
MetadataStatusManagementDialog,
|
MetadataStatusManagementDialog,
|
||||||
MetadataTreeView,
|
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 PropTypes from 'prop-types';
|
||||||
import { Utils } from '../../utils/utils';
|
import { Utils } from '../../utils/utils';
|
||||||
import metadataAPI from '../api';
|
import metadataAPI from '../api';
|
||||||
@@ -6,12 +6,17 @@ import Column from '../metadata-view/model/metadata/column';
|
|||||||
import { normalizeFields, getCellValueByColumn } from './utils';
|
import { normalizeFields, getCellValueByColumn } from './utils';
|
||||||
import DetailItem from '../../components/dirent-detail/detail-item';
|
import DetailItem from '../../components/dirent-detail/detail-item';
|
||||||
import toaster from '../../components/toast';
|
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';
|
import './index.css';
|
||||||
|
|
||||||
const MetadataDetails = ({ repoID, filePath, direntType, emptyTip, ...params }) => {
|
const MetadataDetails = ({ repoID, filePath, repoInfo, direntType, emptyTip }) => {
|
||||||
const [isLoading, setLoading] = useState(true);
|
const [isLoading, setLoading] = useState(true);
|
||||||
const [metadata, setMetadata] = useState({ record: {}, fields: [] });
|
const [metadata, setMetadata] = useState({ record: {}, fields: [] });
|
||||||
|
const permission = useMemo(() => repoInfo.permission !== 'admin' && repoInfo.permission !== 'rw' ? 'r' : 'rw', [repoInfo]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
@@ -34,18 +39,77 @@ const MetadataDetails = ({ repoID, filePath, direntType, emptyTip, ...params })
|
|||||||
});
|
});
|
||||||
}, [repoID, filePath, direntType]);
|
}, [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;
|
if (isLoading) return null;
|
||||||
const { fields, record } = metadata;
|
const { fields, record } = metadata;
|
||||||
if (!record._id) return null;
|
if (!record._id) return null;
|
||||||
return fields.map(field => {
|
return (
|
||||||
|
<>
|
||||||
|
{fields.map(field => {
|
||||||
|
const canEdit = permission === 'rw' && field.editable;
|
||||||
const value = getCellValueByColumn(record, field);
|
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 = {
|
MetadataDetails.propTypes = {
|
||||||
repoID: PropTypes.string,
|
repoID: PropTypes.string,
|
||||||
filePath: PropTypes.string,
|
filePath: PropTypes.string,
|
||||||
|
repoInfo: PropTypes.object,
|
||||||
direntType: PropTypes.string,
|
direntType: PropTypes.string,
|
||||||
direntDetail: PropTypes.object,
|
direntDetail: PropTypes.object,
|
||||||
};
|
};
|
||||||
|
@@ -51,7 +51,7 @@ export const EDITABLE_PRIVATE_COLUMN_KEYS = [
|
|||||||
PRIVATE_COLUMN_KEY.FILE_COLLABORATORS,
|
PRIVATE_COLUMN_KEY.FILE_COLLABORATORS,
|
||||||
PRIVATE_COLUMN_KEY.FILE_EXPIRE_TIME,
|
PRIVATE_COLUMN_KEY.FILE_EXPIRE_TIME,
|
||||||
PRIVATE_COLUMN_KEY.FILE_KEYWORDS,
|
PRIVATE_COLUMN_KEY.FILE_KEYWORDS,
|
||||||
PRIVATE_COLUMN_KEY.FILE_SUMMARY,
|
// PRIVATE_COLUMN_KEY.FILE_SUMMARY,
|
||||||
PRIVATE_COLUMN_KEY.FILE_EXPIRED,
|
PRIVATE_COLUMN_KEY.FILE_EXPIRED,
|
||||||
PRIVATE_COLUMN_KEY.FILE_STATUS,
|
PRIVATE_COLUMN_KEY.FILE_STATUS,
|
||||||
];
|
];
|
||||||
|
@@ -150,4 +150,6 @@ export {
|
|||||||
isRegExpression,
|
isRegExpression,
|
||||||
getGeolocationDisplayString,
|
getGeolocationDisplayString,
|
||||||
getGeolocationByGranularity,
|
getGeolocationByGranularity,
|
||||||
|
getFloatNumber,
|
||||||
|
isNumber,
|
||||||
} from './utils';
|
} from './utils';
|
||||||
|
@@ -5,6 +5,8 @@ export {
|
|||||||
replaceNumberNotAllowInput,
|
replaceNumberNotAllowInput,
|
||||||
formatStringToNumber,
|
formatStringToNumber,
|
||||||
formatTextToNumber,
|
formatTextToNumber,
|
||||||
|
getFloatNumber,
|
||||||
|
isNumber,
|
||||||
} from './number';
|
} from './number';
|
||||||
export {
|
export {
|
||||||
getOption,
|
getOption,
|
||||||
|
@@ -21,4 +21,6 @@ export {
|
|||||||
getLongtextDisplayString,
|
getLongtextDisplayString,
|
||||||
getGeolocationDisplayString,
|
getGeolocationDisplayString,
|
||||||
getGeolocationByGranularity,
|
getGeolocationByGranularity,
|
||||||
|
getFloatNumber,
|
||||||
|
isNumber,
|
||||||
} from './column';
|
} from './column';
|
||||||
|
@@ -21,6 +21,8 @@ export {
|
|||||||
getLongtextDisplayString,
|
getLongtextDisplayString,
|
||||||
getGeolocationDisplayString,
|
getGeolocationDisplayString,
|
||||||
getGeolocationByGranularity,
|
getGeolocationByGranularity,
|
||||||
|
getFloatNumber,
|
||||||
|
isNumber,
|
||||||
} from './cell';
|
} from './cell';
|
||||||
export {
|
export {
|
||||||
getColumnType,
|
getColumnType,
|
||||||
|
@@ -45,3 +45,9 @@
|
|||||||
fill: #666;
|
fill: #666;
|
||||||
font-size: 12px;
|
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 React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { IconBtn } from '@seafile/sf-metadata-ui-component';
|
import { IconBtn } from '@seafile/sf-metadata-ui-component';
|
||||||
import { useCollaborators } from '../../../../hooks';
|
import { useCollaborators } from '../../../../../hooks';
|
||||||
|
|
||||||
import './index.css';
|
import './index.css';
|
||||||
|
|
||||||
@@ -20,7 +20,7 @@ const DeleteCollaborator = ({ value, onDelete }) => {
|
|||||||
<img className="collaborator-avatar m-0" alt={name} src={avatar_url} />
|
<img className="collaborator-avatar m-0" alt={name} src={avatar_url} />
|
||||||
</span>
|
</span>
|
||||||
<span className="collaborator-name text-truncate" title={name} aria-label={name}>{name}</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>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
@@ -5,12 +5,13 @@ import { SearchInput, Icon } from '@seafile/sf-metadata-ui-component';
|
|||||||
import { isFunction } from '../../../_basic';
|
import { isFunction } from '../../../_basic';
|
||||||
import { KeyCodes } from '../../../../../constants';
|
import { KeyCodes } from '../../../../../constants';
|
||||||
import { gettext } from '../../../../../utils/constants';
|
import { gettext } from '../../../../../utils/constants';
|
||||||
import { useCollaborators } from '../../../hooks';
|
import { useCollaborators } from '../../../../hooks';
|
||||||
import DeleteCollaborator from './delete-collaborator';
|
import DeleteCollaborator from './delete-collaborator';
|
||||||
|
|
||||||
import './index.css';
|
import './index.css';
|
||||||
|
|
||||||
const CollaboratorEditor = forwardRef(({
|
const CollaboratorEditor = forwardRef(({
|
||||||
|
saveImmediately = false,
|
||||||
column,
|
column,
|
||||||
value: oldValue,
|
value: oldValue,
|
||||||
onCommit,
|
onCommit,
|
||||||
@@ -22,7 +23,6 @@ const CollaboratorEditor = forwardRef(({
|
|||||||
const [highlightIndex, setHighlightIndex] = useState(-1);
|
const [highlightIndex, setHighlightIndex] = useState(-1);
|
||||||
const [maxItemNum, setMaxItemNum] = useState(0);
|
const [maxItemNum, setMaxItemNum] = useState(0);
|
||||||
const [itemHeight, setItemHeight] = useState(0);
|
const [itemHeight, setItemHeight] = useState(0);
|
||||||
const timerRef = useRef(null);
|
|
||||||
const editorContainerRef = useRef(null);
|
const editorContainerRef = useRef(null);
|
||||||
const editorRef = useRef(null);
|
const editorRef = useRef(null);
|
||||||
const collaboratorItemRef = useRef(null);
|
const collaboratorItemRef = useRef(null);
|
||||||
@@ -70,7 +70,10 @@ const CollaboratorEditor = forwardRef(({
|
|||||||
newValue.push(email);
|
newValue.push(email);
|
||||||
}
|
}
|
||||||
setValue(newValue);
|
setValue(newValue);
|
||||||
}, [value]);
|
if (saveImmediately) {
|
||||||
|
onCommit && onCommit(newValue);
|
||||||
|
}
|
||||||
|
}, [saveImmediately, value, onCommit]);
|
||||||
|
|
||||||
const onDeleteCollaborator = useCallback((email) => {
|
const onDeleteCollaborator = useCallback((email) => {
|
||||||
const newValue = value.slice(0);
|
const newValue = value.slice(0);
|
||||||
@@ -79,7 +82,10 @@ const CollaboratorEditor = forwardRef(({
|
|||||||
newValue.splice(collaboratorIndex, 1);
|
newValue.splice(collaboratorIndex, 1);
|
||||||
}
|
}
|
||||||
setValue(newValue);
|
setValue(newValue);
|
||||||
}, [value]);
|
if (saveImmediately) {
|
||||||
|
onCommit && onCommit(newValue);
|
||||||
|
}
|
||||||
|
}, [saveImmediately, value, onCommit]);
|
||||||
|
|
||||||
const onMenuMouseEnter = useCallback((highlightIndex) => {
|
const onMenuMouseEnter = useCallback((highlightIndex) => {
|
||||||
setHighlightIndex(highlightIndex);
|
setHighlightIndex(highlightIndex);
|
||||||
@@ -182,8 +188,6 @@ const CollaboratorEditor = forwardRef(({
|
|||||||
document.addEventListener('keydown', onHotKey, true);
|
document.addEventListener('keydown', onHotKey, true);
|
||||||
return () => {
|
return () => {
|
||||||
document.removeEventListener('keydown', onHotKey, true);
|
document.removeEventListener('keydown', onHotKey, true);
|
||||||
timerRef.current && clearTimeout(timerRef.current);
|
|
||||||
timerRef.current = null;
|
|
||||||
};
|
};
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [onHotKey]);
|
}, [onHotKey]);
|
||||||
@@ -214,7 +218,7 @@ const CollaboratorEditor = forwardRef(({
|
|||||||
<div key={collaborator.email} className="sf-metadata-collaborator-item" ref={collaboratorItemRef}>
|
<div key={collaborator.email} className="sf-metadata-collaborator-item" ref={collaboratorItemRef}>
|
||||||
<div
|
<div
|
||||||
className={classnames('collaborator-container', { 'collaborator-container-highlight': i === highlightIndex })}
|
className={classnames('collaborator-container', { 'collaborator-container-highlight': i === highlightIndex })}
|
||||||
onMouseDown={() => onSelectCollaborator(isSelected ? null : collaborator.email)}
|
onMouseDown={() => onSelectCollaborator(collaborator.email)}
|
||||||
onMouseEnter={() => onMenuMouseEnter(i)}
|
onMouseEnter={() => onMenuMouseEnter(i)}
|
||||||
onMouseLeave={() => onMenuMouseLeave(i)}
|
onMouseLeave={() => onMenuMouseLeave(i)}
|
||||||
>
|
>
|
||||||
@@ -238,7 +242,7 @@ const CollaboratorEditor = forwardRef(({
|
|||||||
<div className="sf-metadata-collaborator-editor" style={{ top: -38 }} ref={editorRef}>
|
<div className="sf-metadata-collaborator-editor" style={{ top: -38 }} ref={editorRef}>
|
||||||
<DeleteCollaborator value={value} onDelete={onDeleteCollaborator} />
|
<DeleteCollaborator value={value} onDelete={onDeleteCollaborator} />
|
||||||
<div className="sf-metadata-search-collaborator-options">
|
<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>
|
||||||
<div className="sf-metadata-collaborator-editor-container" ref={editorContainerRef}>
|
<div className="sf-metadata-collaborator-editor-container" ref={editorContainerRef}>
|
||||||
{renderCollaborators()}
|
{renderCollaborators()}
|
||||||
@@ -248,6 +252,7 @@ const CollaboratorEditor = forwardRef(({
|
|||||||
});
|
});
|
||||||
|
|
||||||
CollaboratorEditor.propTypes = {
|
CollaboratorEditor.propTypes = {
|
||||||
|
saveImmediately: PropTypes.bool,
|
||||||
column: PropTypes.object,
|
column: PropTypes.object,
|
||||||
value: PropTypes.array,
|
value: PropTypes.array,
|
||||||
onCommit: PropTypes.func,
|
onCommit: PropTypes.func,
|
||||||
|
@@ -8,7 +8,7 @@ import { isCellValueChanged } from '../../../utils/cell-comparer';
|
|||||||
import { EVENT_BUS_TYPE } from '../../../constants';
|
import { EVENT_BUS_TYPE } from '../../../constants';
|
||||||
import { getEventClassName } from '../../../utils';
|
import { getEventClassName } from '../../../utils';
|
||||||
import Editor from '../editor';
|
import Editor from '../editor';
|
||||||
import { canEdit } from '../../../utils/column-utils';
|
import { canEditCell } from '../../../utils/column-utils';
|
||||||
|
|
||||||
class NormalEditorContainer extends React.Component {
|
class NormalEditorContainer extends React.Component {
|
||||||
|
|
||||||
@@ -83,7 +83,7 @@ class NormalEditorContainer extends React.Component {
|
|||||||
const { column, record, openEditorMode, columns, modifyColumnData } = this.props;
|
const { column, record, openEditorMode, columns, modifyColumnData } = this.props;
|
||||||
const editorProps = {
|
const editorProps = {
|
||||||
ref: this.setEditorRef,
|
ref: this.setEditorRef,
|
||||||
readOnly: !canEdit(column, record, true),
|
readOnly: !canEditCell(column, record, true),
|
||||||
columns,
|
columns,
|
||||||
column: this.props.column,
|
column: this.props.column,
|
||||||
value: this.getInitialValue(),
|
value: this.getInitialValue(),
|
||||||
|
@@ -6,7 +6,7 @@ import { CellType, isFunction, Z_INDEX, getCellValueByColumn, getColumnOptionNam
|
|||||||
import { isCellValueChanged } from '../../../utils/cell-comparer';
|
import { isCellValueChanged } from '../../../utils/cell-comparer';
|
||||||
import { EVENT_BUS_TYPE } from '../../../constants';
|
import { EVENT_BUS_TYPE } from '../../../constants';
|
||||||
import Editor from '../editor';
|
import Editor from '../editor';
|
||||||
import { canEdit } from '../../../utils/column-utils';
|
import { canEditCell } from '../../../utils/column-utils';
|
||||||
|
|
||||||
const NOT_SUPPORT_EDITOR_COLUMN_TYPES = [
|
const NOT_SUPPORT_EDITOR_COLUMN_TYPES = [
|
||||||
CellType.CTIME, CellType.MTIME, CellType.CREATOR, CellType.LAST_MODIFIER,
|
CellType.CTIME, CellType.MTIME, CellType.CREATOR, CellType.LAST_MODIFIER,
|
||||||
@@ -58,7 +58,7 @@ class PopupEditorContainer extends React.Component {
|
|||||||
|
|
||||||
createEditor = () => {
|
createEditor = () => {
|
||||||
const { column, record, height, onPressTab, editorPosition, columns, modifyColumnData } = this.props;
|
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);
|
const value = this.getInitialValue(readOnly);
|
||||||
|
|
||||||
let editorProps = {
|
let editorProps = {
|
||||||
|
@@ -63,6 +63,7 @@
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
|
margin-right: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.single-select-avatar {
|
.single-select-avatar {
|
||||||
@@ -82,12 +83,13 @@
|
|||||||
padding: 0px 10px;
|
padding: 0px 10px;
|
||||||
height: 20px;
|
height: 20px;
|
||||||
line-height: 20px;
|
line-height: 20px;
|
||||||
text-align: center;
|
|
||||||
margin-left: 0;
|
margin-left: 0;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
margin-top: 5px;
|
margin-top: 5px;
|
||||||
|
white-space: nowrap;
|
||||||
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sf-metadata-single-select-editor .single-select-check-icon {
|
.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 PropTypes from 'prop-types';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
import { SearchInput, CustomizeAddTool, Icon } from '@seafile/sf-metadata-ui-component';
|
import { SearchInput, CustomizeAddTool, Icon } from '@seafile/sf-metadata-ui-component';
|
||||||
import { getCellValueByColumn, getColumnByKey, isFunction } from '../../../_basic';
|
import { getCellValueByColumn, getColumnByKey, isFunction, getColumnOptions } from '../../../_basic';
|
||||||
import { generateNewOption, getSelectColumnOptions } from '../../../utils/select-utils';
|
import { generateNewOption } from '../../../utils/select-utils';
|
||||||
import { KeyCodes } from '../../../../../constants';
|
import { KeyCodes } from '../../../../../constants';
|
||||||
import { gettext } from '../../../../../utils/constants';
|
import { gettext } from '../../../../../utils/constants';
|
||||||
|
|
||||||
@@ -23,15 +23,14 @@ const SingleSelectEditor = forwardRef(({
|
|||||||
const [searchValue, setSearchValue] = useState('');
|
const [searchValue, setSearchValue] = useState('');
|
||||||
const [highlightIndex, setHighlightIndex] = useState(-1);
|
const [highlightIndex, setHighlightIndex] = useState(-1);
|
||||||
const [maxItemNum, setMaxItemNum] = useState(0);
|
const [maxItemNum, setMaxItemNum] = useState(0);
|
||||||
const [itemHeight, setItemHeight] = useState(0);
|
const itemHeight = 30;
|
||||||
const timerRef = useRef(null);
|
|
||||||
const editorContainerRef = useRef(null);
|
const editorContainerRef = useRef(null);
|
||||||
const editorRef = useRef(null);
|
const editorRef = useRef(null);
|
||||||
const selectItemRef = useRef(null);
|
const selectItemRef = useRef(null);
|
||||||
const canEditData = window.sfMetadataContext.canModifyColumnData(column);
|
const canEditData = window.sfMetadataContext.canModifyColumnData(column);
|
||||||
|
|
||||||
const options = useMemo(() => {
|
const options = useMemo(() => {
|
||||||
const options = getSelectColumnOptions(column);
|
const options = getColumnOptions(column);
|
||||||
const { data } = column;
|
const { data } = column;
|
||||||
const { cascade_column_key, cascade_settings } = data || {};
|
const { cascade_column_key, cascade_settings } = data || {};
|
||||||
if (cascade_column_key) {
|
if (cascade_column_key) {
|
||||||
@@ -64,8 +63,8 @@ const SingleSelectEditor = forwardRef(({
|
|||||||
}, [column, height]);
|
}, [column, height]);
|
||||||
|
|
||||||
const blur = useCallback(() => {
|
const blur = useCallback(() => {
|
||||||
onCommit && onCommit();
|
onCommit && onCommit(value);
|
||||||
}, [onCommit]);
|
}, [value, onCommit]);
|
||||||
|
|
||||||
const onChangeSearch = useCallback((newSearchValue) => {
|
const onChangeSearch = useCallback((newSearchValue) => {
|
||||||
if (searchValue === newSearchValue) return;
|
if (searchValue === newSearchValue) return;
|
||||||
@@ -76,7 +75,7 @@ const SingleSelectEditor = forwardRef(({
|
|||||||
if (optionId === value) return;
|
if (optionId === value) return;
|
||||||
setValue(optionId);
|
setValue(optionId);
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
onCommit && onCommit();
|
onCommit && onCommit(optionId);
|
||||||
}, 1);
|
}, 1);
|
||||||
}, [value, onCommit]);
|
}, [value, onCommit]);
|
||||||
|
|
||||||
@@ -89,6 +88,7 @@ const SingleSelectEditor = forwardRef(({
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const createOption = useCallback((event) => {
|
const createOption = useCallback((event) => {
|
||||||
|
event && event.stopPropagation();
|
||||||
event && event.nativeEvent.stopImmediatePropagation();
|
event && event.nativeEvent.stopImmediatePropagation();
|
||||||
const newOption = generateNewOption(options, searchValue?.trim() || '');
|
const newOption = generateNewOption(options, searchValue?.trim() || '');
|
||||||
let newOptions = options.slice(0);
|
let newOptions = options.slice(0);
|
||||||
@@ -180,13 +180,10 @@ const SingleSelectEditor = forwardRef(({
|
|||||||
}
|
}
|
||||||
if (editorContainerRef.current && selectItemRef.current) {
|
if (editorContainerRef.current && selectItemRef.current) {
|
||||||
setMaxItemNum(getMaxItemNum());
|
setMaxItemNum(getMaxItemNum());
|
||||||
setItemHeight(parseInt(getComputedStyle(selectItemRef.current, null).height));
|
|
||||||
}
|
}
|
||||||
document.addEventListener('keydown', onHotKey, true);
|
document.addEventListener('keydown', onHotKey, true);
|
||||||
return () => {
|
return () => {
|
||||||
document.removeEventListener('keydown', onHotKey, true);
|
document.removeEventListener('keydown', onHotKey, true);
|
||||||
timerRef.current && clearTimeout(timerRef.current);
|
|
||||||
timerRef.current = null;
|
|
||||||
};
|
};
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [onHotKey]);
|
}, [onHotKey]);
|
||||||
@@ -211,12 +208,8 @@ const SingleSelectEditor = forwardRef(({
|
|||||||
return (<span className="none-search-result">{noOptionsTip}</span>);
|
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) => {
|
return displayOptions.map((option, i) => {
|
||||||
const isSelected = value === option.name;
|
const isSelected = value === option.id || value === option.name;
|
||||||
return (
|
return (
|
||||||
<div key={option.id} className="sf-metadata-single-select-item" ref={selectItemRef}>
|
<div key={option.id} className="sf-metadata-single-select-item" ref={selectItemRef}>
|
||||||
<div
|
<div
|
||||||
@@ -228,7 +221,7 @@ const SingleSelectEditor = forwardRef(({
|
|||||||
<div className="single-select">
|
<div className="single-select">
|
||||||
<span
|
<span
|
||||||
className="single-select-name"
|
className="single-select-name"
|
||||||
style={{ backgroundColor: option.color, color: option.textColor || null, maxWidth }}
|
style={{ backgroundColor: option.color, color: option.textColor || null }}
|
||||||
title={option.name}
|
title={option.name}
|
||||||
aria-label={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 (
|
return (
|
||||||
<div className="sf-metadata-single-select-editor" style={style} ref={editorRef}>
|
<div className="sf-metadata-single-select-editor" style={style} ref={editorRef}>
|
||||||
@@ -253,6 +246,7 @@ const SingleSelectEditor = forwardRef(({
|
|||||||
onKeyDown={onKeyDown}
|
onKeyDown={onKeyDown}
|
||||||
onChange={onChangeSearch}
|
onChange={onChangeSearch}
|
||||||
autoFocus={true}
|
autoFocus={true}
|
||||||
|
className="sf-metadata-search-options"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="sf-metadata-single-select-editor-container" ref={editorContainerRef}>
|
<div className="sf-metadata-single-select-editor-container" ref={editorContainerRef}>
|
||||||
@@ -280,4 +274,3 @@ SingleSelectEditor.propTypes = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default SingleSelectEditor;
|
export default SingleSelectEditor;
|
||||||
|
|
||||||
|
@@ -1,11 +1,11 @@
|
|||||||
import React, { useMemo } from 'react';
|
import React, { useMemo } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { Formatter } from '@seafile/sf-metadata-ui-component';
|
import { Formatter } from '@seafile/sf-metadata-ui-component';
|
||||||
import { useCollaborators } from '../../hooks';
|
import { useCollaborators } from '../../../hooks';
|
||||||
import { Utils } from '../../../../utils/utils';
|
import { Utils } from '../../../../utils/utils';
|
||||||
|
|
||||||
const CellFormatter = ({ readonly, value, field, ...params }) => {
|
const CellFormatter = ({ readonly, value, field, ...params }) => {
|
||||||
const { collaborators, collaboratorsCache, updateCollaboratorsCache } = useCollaborators();
|
const { collaborators, collaboratorsCache, updateCollaboratorsCache, queryUser } = useCollaborators();
|
||||||
const props = useMemo(() => {
|
const props = useMemo(() => {
|
||||||
return {
|
return {
|
||||||
collaborators,
|
collaborators,
|
||||||
@@ -14,11 +14,11 @@ const CellFormatter = ({ readonly, value, field, ...params }) => {
|
|||||||
readonly,
|
readonly,
|
||||||
value,
|
value,
|
||||||
field,
|
field,
|
||||||
queryUserAPI: window.sfMetadataContext.userService.queryUser,
|
queryUserAPI: queryUser,
|
||||||
getFileIconUrl: Utils.getFileIconUrl,
|
getFileIconUrl: Utils.getFileIconUrl,
|
||||||
getFolderIconUrl: Utils.getFolderIconUrl,
|
getFolderIconUrl: Utils.getFolderIconUrl,
|
||||||
};
|
};
|
||||||
}, [readonly, value, field, collaborators, collaboratorsCache, updateCollaboratorsCache]);
|
}, [readonly, value, field, collaborators, collaboratorsCache, updateCollaboratorsCache, queryUser]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Formatter { ...props } { ...params } />
|
<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 DeleteConfirmDialog from './delete-confirm-dialog';
|
||||||
import Table from './table';
|
import Table from './table';
|
||||||
|
import DetailEditor from './detail-editor';
|
||||||
|
import CellFormatter from './cell-formatter';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
DeleteConfirmDialog,
|
DeleteConfirmDialog,
|
||||||
Table,
|
Table,
|
||||||
|
DetailEditor,
|
||||||
|
CellFormatter,
|
||||||
};
|
};
|
||||||
|
@@ -9,8 +9,8 @@ import {
|
|||||||
filterTermModifierIsWithin,
|
filterTermModifierIsWithin,
|
||||||
isDateColumn,
|
isDateColumn,
|
||||||
FILTER_ERR_MSG,
|
FILTER_ERR_MSG,
|
||||||
|
getColumnOptions as getSelectColumnOptions,
|
||||||
} from '../../../../../_basic';
|
} from '../../../../../_basic';
|
||||||
import { getSelectColumnOptions } from '../../../../../utils/select-utils';
|
|
||||||
import CollaboratorFilter from './collaborator-filter';
|
import CollaboratorFilter from './collaborator-filter';
|
||||||
import FilterCalendar from '../filter-calendar';
|
import FilterCalendar from '../filter-calendar';
|
||||||
import FilterItemUtils from '../filter-item-utils';
|
import FilterItemUtils from '../filter-item-utils';
|
||||||
|
@@ -5,7 +5,7 @@
|
|||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
border-right: 1px solid #eee;
|
border-right: 1px solid #eee;
|
||||||
padding: 4px 8px;
|
padding: 6px 8px;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
}
|
}
|
||||||
@@ -71,7 +71,7 @@
|
|||||||
/* cell formatter */
|
/* cell formatter */
|
||||||
.sf-metadata-result-table-cell .sf-metadata-ui.cell-formatter-container {
|
.sf-metadata-result-table-cell .sf-metadata-ui.cell-formatter-container {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
line-height: 24px;
|
line-height: 20px;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -303,3 +303,17 @@
|
|||||||
padding-top: 5.5px;
|
padding-top: 5.5px;
|
||||||
padding-bottom: 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 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';
|
EDITABLE_PRIVATE_COLUMN_KEYS, PREDEFINED_COLUMN_KEYS } from './_basic';
|
||||||
import EventBus from '../../components/common/event-bus';
|
import EventBus from '../../components/common/event-bus';
|
||||||
import { username } from '../../utils/constants';
|
import { username } from '../../utils/constants';
|
||||||
import User from './model/user';
|
|
||||||
|
|
||||||
class Context {
|
class Context {
|
||||||
|
|
||||||
@@ -11,29 +10,26 @@ class Context {
|
|||||||
this.settings = {};
|
this.settings = {};
|
||||||
this.metadataAPI = null;
|
this.metadataAPI = null;
|
||||||
this.localStorage = null;
|
this.localStorage = null;
|
||||||
this.userService = null;
|
|
||||||
this.eventBus = null;
|
this.eventBus = null;
|
||||||
this.hasInit = false;
|
this.hasInit = false;
|
||||||
this.permission = 'r';
|
this.permission = 'r';
|
||||||
this.collaboratorsCache = {};
|
this.collaboratorsCache = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
async init({ otherSettings }) {
|
async init(settings) {
|
||||||
if (this.hasInit) return;
|
if (this.hasInit) return;
|
||||||
|
|
||||||
// init settings
|
// init settings
|
||||||
this.settings = otherSettings || {};
|
this.settings = settings || {};
|
||||||
|
|
||||||
// init metadataAPI
|
// init metadataAPI
|
||||||
const { mediaUrl, repoInfo } = this.settings;
|
const { repoInfo } = this.settings;
|
||||||
this.metadataAPI = metadataAPI;
|
this.metadataAPI = metadataAPI;
|
||||||
|
|
||||||
// init localStorage
|
// init localStorage
|
||||||
const { repoID, viewID } = this.settings;
|
const { repoID, viewID } = this.settings;
|
||||||
this.localStorage = new LocalStorage(`sf-metadata-${repoID}-${viewID}`);
|
const localStorageName = viewID ? `sf-metadata-${repoID}-${viewID}` : `sf-metadata-${repoID}`;
|
||||||
|
this.localStorage = new LocalStorage(localStorageName);
|
||||||
// init userService
|
|
||||||
this.userService = new UserService({ mediaUrl, api: this.metadataAPI.listUserInfo });
|
|
||||||
|
|
||||||
const eventBus = new EventBus();
|
const eventBus = new EventBus();
|
||||||
this.eventBus = eventBus;
|
this.eventBus = eventBus;
|
||||||
@@ -47,7 +43,6 @@ class Context {
|
|||||||
this.settings = {};
|
this.settings = {};
|
||||||
this.metadataAPI = null;
|
this.metadataAPI = null;
|
||||||
this.localStorage = null;
|
this.localStorage = null;
|
||||||
this.userService = null;
|
|
||||||
this.eventBus = null;
|
this.eventBus = null;
|
||||||
this.hasInit = false;
|
this.hasInit = false;
|
||||||
this.permission = 'r';
|
this.permission = 'r';
|
||||||
@@ -135,23 +130,6 @@ class Context {
|
|||||||
return true;
|
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 = () => {
|
restoreRows = () => {
|
||||||
// todo
|
// todo
|
||||||
};
|
};
|
||||||
|
@@ -1,7 +1,5 @@
|
|||||||
import { CollaboratorsProvider, useCollaborators } from './collaborators';
|
|
||||||
import { MetadataProvider, useMetadata } from './metadata';
|
import { MetadataProvider, useMetadata } from './metadata';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
CollaboratorsProvider, useCollaborators,
|
|
||||||
MetadataProvider, useMetadata,
|
MetadataProvider, useMetadata,
|
||||||
};
|
};
|
||||||
|
@@ -47,9 +47,8 @@ export const MetadataProvider = ({
|
|||||||
setLoading(true);
|
setLoading(true);
|
||||||
// init context
|
// init context
|
||||||
const context = new Context();
|
const context = new Context();
|
||||||
window.sfMetadata = {};
|
|
||||||
window.sfMetadataContext = context;
|
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 });
|
storeRef.current = new Store({ context: window.sfMetadataContext, repoId: repoID, viewId: viewID });
|
||||||
window.sfMetadataStore = storeRef.current;
|
window.sfMetadataStore = storeRef.current;
|
||||||
storeRef.current.initStartIndex();
|
storeRef.current.initStartIndex();
|
||||||
@@ -68,7 +67,6 @@ export const MetadataProvider = ({
|
|||||||
const unsubscribeReloadData = eventBus.subscribe(EVENT_BUS_TYPE.RELOAD_DATA, reloadMetadata);
|
const unsubscribeReloadData = eventBus.subscribe(EVENT_BUS_TYPE.RELOAD_DATA, reloadMetadata);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
window.sfMetadata = {};
|
|
||||||
window.sfMetadataContext.destroy();
|
window.sfMetadataContext.destroy();
|
||||||
window.sfMetadataStore.destroy();
|
window.sfMetadataStore.destroy();
|
||||||
unsubscribeServerTableChanged();
|
unsubscribeServerTableChanged();
|
||||||
|
@@ -1,15 +1,13 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { MetadataProvider, CollaboratorsProvider } from './hooks/index';
|
import { MetadataProvider } from './hooks';
|
||||||
import { Table } from './components/index';
|
import { Table, DetailEditor, CellFormatter } from './components';
|
||||||
|
import Context from './context';
|
||||||
|
|
||||||
const SeafileMetadata = ({ ...params }) => {
|
const SeafileMetadata = ({ ...params }) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MetadataProvider { ...params }>
|
<MetadataProvider { ...params }>
|
||||||
<CollaboratorsProvider >
|
|
||||||
<Table />
|
<Table />
|
||||||
</CollaboratorsProvider>
|
|
||||||
</MetadataProvider>
|
</MetadataProvider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -21,3 +19,8 @@ SeafileMetadata.propTypes = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default SeafileMetadata;
|
export default SeafileMetadata;
|
||||||
|
export {
|
||||||
|
Context,
|
||||||
|
DetailEditor,
|
||||||
|
CellFormatter,
|
||||||
|
};
|
||||||
|
@@ -327,7 +327,7 @@ export const normalizeColumns = (columns) => {
|
|||||||
return displayColumns;
|
return displayColumns;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function canEdit(col, record, enableCellSelect) {
|
export function canEditCell(col, record, enableCellSelect) {
|
||||||
if (!col) return false;
|
if (!col) return false;
|
||||||
if (window.sfMetadataContext.canModifyColumn(col) === false) return false;
|
if (window.sfMetadataContext.canModifyColumn(col) === false) return false;
|
||||||
if (col.editable != null && typeof (col.editable) === 'function') {
|
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 { formatTextToDate } from './date';
|
||||||
import { getFloatNumber, getNumberDisplayString, formatStringToNumber, isNumber } from '../_basic/utils/cell/column/number';
|
|
||||||
|
|
||||||
const SUPPORT_PASTE_FROM_COLUMN = {
|
const SUPPORT_PASTE_FROM_COLUMN = {
|
||||||
[CellType.NUMBER]: [CellType.TEXT, CellType.NUMBER],
|
[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})日$/;
|
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) {
|
function convertCellValue(cellValue, oldCellValue, targetColumn, fromColumn) {
|
||||||
const { type: fromColumnType, data: fromColumnData } = fromColumn;
|
const { type: fromColumnType, data: fromColumnData } = fromColumn;
|
||||||
const { type: targetColumnType, data: targetColumnData } = targetColumn;
|
const { type: targetColumnType, data: targetColumnData } = targetColumn;
|
||||||
@@ -140,7 +134,7 @@ function convert2SingleSelect(cellValue, oldCellValue, fromColumn, targetColumn)
|
|||||||
let fromOptionName;
|
let fromOptionName;
|
||||||
switch (fromColumnType) {
|
switch (fromColumnType) {
|
||||||
case CellType.SINGLE_SELECT: {
|
case CellType.SINGLE_SELECT: {
|
||||||
const fromOptions = getSelectColumnOptions(fromColumn);
|
const fromOptions = getColumnOptions(fromColumn);
|
||||||
fromOptionName = getOptionName(fromOptions, cellValue) || '';
|
fromOptionName = getOptionName(fromOptions, cellValue) || '';
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -156,7 +150,7 @@ function convert2SingleSelect(cellValue, oldCellValue, fromColumn, targetColumn)
|
|||||||
return oldCellValue;
|
return oldCellValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentOptions = getSelectColumnOptions(targetColumn);
|
const currentOptions = getColumnOptions(targetColumn);
|
||||||
const newOption = generatorCellOption(currentOptions, fromOptionName);
|
const newOption = generatorCellOption(currentOptions, fromOptionName);
|
||||||
return PREDEFINED_COLUMN_KEYS.includes(targetColumn.key) ? newOption.id : newOption.name;
|
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);
|
return getDateDisplayString(cellValue, fromColumnData.format || DEFAULT_DATE_FORMAT);
|
||||||
}
|
}
|
||||||
case CellType.SINGLE_SELECT: {
|
case CellType.SINGLE_SELECT: {
|
||||||
const options = getSelectColumnOptions(fromColumn);
|
const options = getColumnOptions(fromColumn);
|
||||||
return getOptionName(options, cellValue) || null;
|
return getOptionName(options, cellValue) || null;
|
||||||
}
|
}
|
||||||
case CellType.COLLABORATOR: {
|
case CellType.COLLABORATOR: {
|
||||||
const collaborators = window.sfMetadata.collaborators || window.sfMetadataContext.getCollaboratorsFromCache();
|
const collaborators = window.sfMetadata.getCollaborators();
|
||||||
return getCollaboratorsName(collaborators, cellValue);
|
return getCollaboratorsName(collaborators, cellValue);
|
||||||
}
|
}
|
||||||
case CellType.CREATOR:
|
case CellType.CREATOR:
|
||||||
@@ -242,7 +236,7 @@ function convert2Text(cellValue, oldCellValue, fromColumn) {
|
|||||||
if (!cellValue) {
|
if (!cellValue) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const collaborators = window.sfMetadata.collaborators || window.sfMetadataContext.getCollaboratorsFromCache();
|
const collaborators = window.sfMetadata.getCollaborators();
|
||||||
return getCollaboratorsName(collaborators, [cellValue]);
|
return getCollaboratorsName(collaborators, [cellValue]);
|
||||||
}
|
}
|
||||||
default: {
|
default: {
|
||||||
@@ -257,7 +251,7 @@ function convert2Collaborator(cellValue, oldCellValue, fromColumnType) {
|
|||||||
if (!Array.isArray(cellValue) || cellValue.length === 0) {
|
if (!Array.isArray(cellValue) || cellValue.length === 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const collaborators = window.sfMetadata.collaborators || window.sfMetadataContext.getCollaboratorsFromCache();
|
const collaborators = window.sfMetadata.getCollaborators();
|
||||||
let validEmailMap = {};
|
let validEmailMap = {};
|
||||||
collaborators.forEach(collaborator => validEmailMap[collaborator.email] = true);
|
collaborators.forEach(collaborator => validEmailMap[collaborator.email] = true);
|
||||||
return cellValue.filter(email => !!validEmailMap[email]);
|
return cellValue.filter(email => !!validEmailMap[email]);
|
||||||
@@ -270,7 +264,7 @@ function convert2Collaborator(cellValue, oldCellValue, fromColumnType) {
|
|||||||
if (userNames.length === 0) {
|
if (userNames.length === 0) {
|
||||||
return oldCellValue;
|
return oldCellValue;
|
||||||
}
|
}
|
||||||
const collaborators = window.sfMetadata.collaborators || window.sfMetadataContext.getCollaboratorsFromCache();
|
const collaborators = window.sfMetadata.getCollaborators();
|
||||||
let nameCollaboratorMap = {};
|
let nameCollaboratorMap = {};
|
||||||
collaborators.forEach(collaborator => nameCollaboratorMap[collaborator.name] = collaborator);
|
collaborators.forEach(collaborator => nameCollaboratorMap[collaborator.name] = collaborator);
|
||||||
const emails = userNames.map(name => {
|
const emails = userNames.map(name => {
|
||||||
@@ -284,7 +278,7 @@ function convert2Collaborator(cellValue, oldCellValue, fromColumnType) {
|
|||||||
}
|
}
|
||||||
case CellType.CREATOR:
|
case CellType.CREATOR:
|
||||||
case CellType.LAST_MODIFIER: {
|
case CellType.LAST_MODIFIER: {
|
||||||
const collaborators = window.sfMetadata.collaborators || window.sfMetadataContext.getCollaboratorsFromCache();
|
const collaborators = window.sfMetadata.getCollaborators();
|
||||||
let validEmailMap = {};
|
let validEmailMap = {};
|
||||||
collaborators.forEach(collaborator => validEmailMap[collaborator.email] = true);
|
collaborators.forEach(collaborator => validEmailMap[collaborator.email] = true);
|
||||||
if (!cellValue || !validEmailMap[cellValue]) {
|
if (!cellValue || !validEmailMap[cellValue]) {
|
||||||
|
@@ -1,13 +1,6 @@
|
|||||||
import { SELECT_OPTION_COLORS } from '../_basic/constants/select-option';
|
import { SELECT_OPTION_COLORS } from '../_basic/constants/select-option';
|
||||||
import { generateOptionID } from '../_basic/utils/column/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 getNotDuplicateOption = (options) => {
|
||||||
const defaultOptions = SELECT_OPTION_COLORS.slice(12, 24);
|
const defaultOptions = SELECT_OPTION_COLORS.slice(12, 24);
|
||||||
let defaultOption = defaultOptions[Math.floor(Math.random() * defaultOptions.length)];
|
let defaultOption = defaultOptions[Math.floor(Math.random() * defaultOptions.length)];
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import { Z_INDEX, getGroupByPath, isFunction, getCellValueByColumn } from '../_basic';
|
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 { SUPPORT_PREVIEW_COLUMN_TYPES } from '../constants';
|
||||||
import { getGroupRecordByIndex } from './group-metrics';
|
import { getGroupRecordByIndex } from './group-metrics';
|
||||||
|
|
||||||
@@ -44,7 +44,7 @@ export const isSelectedCellEditable = ({ enableCellSelect, selectedPosition, col
|
|||||||
const row = getSelectedRow({ selectedPosition, isGroupView, recordGetterByIndex });
|
const row = getSelectedRow({ selectedPosition, isGroupView, recordGetterByIndex });
|
||||||
if (!window.sfMetadataContext.canModifyRow(row)) return false;
|
if (!window.sfMetadataContext.canModifyRow(row)) return false;
|
||||||
const isCellEditable = isFunction(onCheckCellIsEditable) ? onCheckCellIsEditable({ row, column, ...selectedPosition }) : true;
|
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) {
|
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 DeleteFolderDialog from '../../components/dialog/delete-folder-dialog';
|
||||||
import { EVENT_BUS_TYPE } from '../../components/common/event-bus-type';
|
import { EVENT_BUS_TYPE } from '../../components/common/event-bus-type';
|
||||||
import { PRIVATE_FILE_TYPE } from '../../constants';
|
import { PRIVATE_FILE_TYPE } from '../../constants';
|
||||||
import { MetadataProvider } from '../../metadata/hooks';
|
import { MetadataProvider, CollaboratorsProvider } from '../../metadata/hooks';
|
||||||
|
|
||||||
const propTypes = {
|
const propTypes = {
|
||||||
eventBus: PropTypes.object,
|
eventBus: PropTypes.object,
|
||||||
@@ -2174,6 +2174,7 @@ class LibContentView extends React.Component {
|
|||||||
renameMetadataView={this.renameMetadataView}
|
renameMetadataView={this.renameMetadataView}
|
||||||
hideMetadataView={this.hideFileMetadata}
|
hideMetadataView={this.hideFileMetadata}
|
||||||
>
|
>
|
||||||
|
<CollaboratorsProvider repoID={this.props.repoID}>
|
||||||
<div className="main-panel-center flex-row">
|
<div className="main-panel-center flex-row">
|
||||||
<LibContentContainer
|
<LibContentContainer
|
||||||
isSidePanelFolded={this.props.isSidePanelFolded}
|
isSidePanelFolded={this.props.isSidePanelFolded}
|
||||||
@@ -2291,6 +2292,7 @@ class LibContentView extends React.Component {
|
|||||||
<MediaQuery query="(max-width: 767.8px)">
|
<MediaQuery query="(max-width: 767.8px)">
|
||||||
<Modal zIndex="1030" isOpen={!Utils.isDesktop() && this.state.isTreePanelShown} toggle={this.toggleTreePanel} contentClassName="d-none"></Modal>
|
<Modal zIndex="1030" isOpen={!Utils.isDesktop() && this.state.isTreePanelShown} toggle={this.toggleTreePanel} contentClassName="d-none"></Modal>
|
||||||
</MediaQuery>
|
</MediaQuery>
|
||||||
|
</CollaboratorsProvider>
|
||||||
</MetadataProvider>
|
</MetadataProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user