1
0
mirror of https://github.com/haiwen/seahub.git synced 2025-09-07 01:41:39 +00:00

feat: metadata detail editor (#6562)

* feat: metadata detail editor

* feat: update code

* feat: update code

---------

Co-authored-by: 杨国璇 <ygx@Hello-word.local>
This commit is contained in:
杨国璇
2024-08-15 17:38:42 +08:00
committed by GitHub
parent 2d2a8458a2
commit ca5d33dd5a
53 changed files with 1209 additions and 362 deletions

View File

@@ -19,7 +19,7 @@
"@seafile/sdoc-editor": "1.0.50",
"@seafile/seafile-calendar": "0.0.12",
"@seafile/seafile-editor": "1.0.109",
"@seafile/sf-metadata-ui-component": "0.0.18",
"@seafile/sf-metadata-ui-component": "0.0.20",
"@uiw/codemirror-extensions-langs": "^4.19.4",
"@uiw/react-codemirror": "^4.19.4",
"axios": "^1.7.3",
@@ -5094,9 +5094,9 @@
}
},
"node_modules/@seafile/sf-metadata-ui-component": {
"version": "0.0.18",
"resolved": "https://registry.npmjs.org/@seafile/sf-metadata-ui-component/-/sf-metadata-ui-component-0.0.18.tgz",
"integrity": "sha512-jC4JL06dAntWIr/ZPA/zJ4I9NhDfw0ismvSQuGZsBMvdrdbBBGuFVxirohB9zzwzc0k/K2epyQ/1iARKv5wjow==",
"version": "0.0.20",
"resolved": "https://registry.npmjs.org/@seafile/sf-metadata-ui-component/-/sf-metadata-ui-component-0.0.20.tgz",
"integrity": "sha512-nCeqHgwAkd05GKd3YROf3k7XEQuh6FicOeYVXcbCXjzH1pki8XxLcv1NW4D85LvWh2eS6m+utegcAlXfS1EsJQ==",
"dependencies": {
"@seafile/seafile-calendar": "0.0.24",
"@seafile/seafile-editor": "~1.0.102",

View File

@@ -14,7 +14,7 @@
"@seafile/sdoc-editor": "1.0.50",
"@seafile/seafile-calendar": "0.0.12",
"@seafile/seafile-editor": "1.0.109",
"@seafile/sf-metadata-ui-component": "0.0.18",
"@seafile/sf-metadata-ui-component": "0.0.20",
"@uiw/codemirror-extensions-langs": "^4.19.4",
"@uiw/react-codemirror": "^4.19.4",
"axios": "^1.7.3",

View File

@@ -12,12 +12,12 @@
.dirent-detail-item .dirent-detail-item-name {
width: 160px;
padding: 7px 6px;
padding: 6.5px 6px;
min-height: 34px;
height: fit-content;
color: #666;
font-size: 14px;
line-height: 1.4;
line-height: 1.5;
}
.dirent-detail-item .dirent-detail-item-name .sf-metadata-icon {
@@ -29,13 +29,8 @@
.dirent-detail-item .dirent-detail-item-value {
width: 200px;
display: flex;
padding: 7px 6px;
min-height: 34px;
height: fit-content;
}
.dirent-detail-item .dirent-detail-item-value.editable:hover {
cursor: pointer;
min-height: 34px;
}
.dirent-detail-item .dirent-detail-item-name:hover,
@@ -45,10 +40,48 @@
cursor: default;
}
.dirent-detail-item .dirent-detail-item-value.editable:hover {
cursor: pointer;
}
/* media */
.cur-view-detail-small .dirent-detail-item .dirent-detail-item-name {
width: calc((100% - 8px) * 0.44);
}
.cur-view-detail-small .dirent-detail-item .dirent-detail-item-value {
width: calc((100% - 8px) * 0.56);
}
.cur-view-detail-large .dirent-detail-item .dirent-detail-item-name {
width: 160px;
margin-right: 8px;
}
.cur-view-detail-large .dirent-detail-item .dirent-detail-item-value {
flex: 1;
}
.dirent-detail-item .dirent-detail-item-value:not(.editable) .sf-metadata-record-cell-empty {
display: inline-block;
height: 34px;
width: 100%;
line-height: 1.5;
padding: 6.5px 6px;
}
.dirent-detail-item .dirent-detail-item-value:not(.editable) .sf-metadata-record-cell-empty:empty::before {
content: attr(placeholder);
color: #666;
font-size: 14px;
}
/* formatter */
.dirent-detail-item .dirent-detail-item-value .text-formatter,
.dirent-detail-item .dirent-detail-item-value .ctime-formatter,
.dirent-detail-item .dirent-detail-item-value .mtime-formatter,
.dirent-detail-item .dirent-detail-item-value .date-formatter {
padding: 6.5px 6px;
line-height: 1.5;
}
@@ -57,28 +90,5 @@
}
.dirent-detail-item-value .creator-formatter {
height: 20px;
}
.dirent-detail-item-value .sf-metadata-record-cell-empty::before {
content: attr(placeholder);
color: #666;
font-size: 14px;
}
/* */
.cur-view-detail-small .dirent-detail-item .dirent-detail-item-name {
width: 44%;
}
.cur-view-detail-small .dirent-detail-item .dirent-detail-item-value {
width: 56%;
}
.cur-view-detail-large .dirent-detail-item .dirent-detail-item-name {
width: 160px;
}
.cur-view-detail-large .dirent-detail-item .dirent-detail-item-value {
flex: 1;
padding: 7px 6px;
}

View File

@@ -1,40 +1,39 @@
import React, { useMemo } from 'react';
import PropTypes from 'prop-types';
import { Formatter, Icon } from '@seafile/sf-metadata-ui-component';
import classnames from 'classnames';
import { Icon } from '@seafile/sf-metadata-ui-component';
import { CellType, COLUMNS_ICON_CONFIG } from '../../../metadata/metadata-view/_basic';
import { gettext } from '../../../utils/constants';
import './index.css';
const DetailItem = ({ field, value, valueId, valueClick, children, ...params }) => {
const DetailItem = ({ readonly, field, className, children }) => {
const icon = useMemo(() => {
if (field.type === 'size') return COLUMNS_ICON_CONFIG[CellType.NUMBER];
return COLUMNS_ICON_CONFIG[field.type];
}, [field]);
return (
<div className="dirent-detail-item">
<div className={classnames('dirent-detail-item', className)}>
<div className="dirent-detail-item-name">
<Icon iconName={icon} />
<span className="dirent-detail-item-name-value">{field.name}</span>
</div>
<div className={classnames('dirent-detail-item-value', { 'editable': valueClick })} id={valueId} onClick={valueClick}>
{children ? children : (<Formatter { ...params } field={field} value={value} />)}
<div className={classnames('dirent-detail-item-value', { 'editable': !readonly })} >
{children}
</div>
</div>
);
};
DetailItem.defaultProps = {
emptyTip: gettext('Empty')
readonly: true,
};
DetailItem.propTypes = {
readonly: PropTypes.bool,
field: PropTypes.object.isRequired,
value: PropTypes.any,
className: PropTypes.string,
children: PropTypes.any,
valueId: PropTypes.string,
};
export default DetailItem;

View File

@@ -1,20 +1,26 @@
import React, { useMemo } from 'react';
import PropTypes from 'prop-types';
import { Formatter } from '@seafile/sf-metadata-ui-component';
import { getDirentPath } from './utils';
import DetailItem from '../detail-item';
import { CellType } from '../../../metadata/metadata-view/_basic';
import { gettext } from '../../../utils/constants';
import { MetadataDetails, useMetadata } from '../../../metadata';
const DirDetails = ({ repoID, repoInfo, dirent, path, direntDetail, ...params }) => {
const DirDetails = ({ repoID, repoInfo, dirent, path, direntDetail }) => {
const direntPath = useMemo(() => getDirentPath(dirent, path), [dirent, path]);
const { enableMetadata } = useMetadata();
const lastModifiedTimeField = useMemo(() => {
return { type: CellType.MTIME, name: gettext('Last modified time') };
}, []);
return (
<>
<DetailItem field={{ type: CellType.MTIME, name: gettext('Last modified time') }} value={direntDetail.mtime} />
<DetailItem field={lastModifiedTimeField} className="sf-metadata-property-detail-formatter">
<Formatter field={lastModifiedTimeField} value={direntDetail.mtime} />
</DetailItem>
{window.app.pageOptions.enableMetadataManagement && enableMetadata && (
<MetadataDetails repoID={repoID} filePath={direntPath} direntType="dir" { ...params } />
<MetadataDetails repoID={repoID} repoInfo={repoInfo} filePath={direntPath} direntType="dir" />
)}
</>
);

View File

@@ -1,6 +1,7 @@
import React, { useCallback, useMemo, useState } from 'react';
import PropTypes from 'prop-types';
import { v4 as uuidV4 } from 'uuid';
import { Formatter } from '@seafile/sf-metadata-ui-component';
import { getDirentPath } from './utils';
import DetailItem from '../detail-item';
import { CellType } from '../../../metadata/metadata-view/_basic';
@@ -11,12 +12,16 @@ import { Utils } from '../../../utils/utils';
import { MetadataDetails, useMetadata } from '../../../metadata';
import ObjectUtils from '../../../metadata/metadata-view/utils/object-utils';
const FileDetails = React.memo(({ repoID, repoInfo, dirent, path, direntDetail, onFileTagChanged, repoTags, fileTagList, ...params }) => {
const FileDetails = React.memo(({ repoID, repoInfo, dirent, path, direntDetail, onFileTagChanged, repoTags, fileTagList }) => {
const [isEditFileTagShow, setEditFileTagShow] = useState(false);
const { enableMetadata } = useMetadata();
const direntPath = useMemo(() => getDirentPath(dirent, path), [dirent, path]);
const tagListTitleID = useMemo(() => `detail-list-view-tags-${uuidV4()}`, []);
const sizeField = useMemo(() => ({ type: 'size', name: gettext('Size') }), []);
const lastModifierField = useMemo(() => ({ type: CellType.LAST_MODIFIER, name: gettext('Last modifier') }), []);
const lastModifiedTimeField = useMemo(() => ({ type: CellType.MTIME, name: gettext('Last modified time') }), []);
const tagsField = useMemo(() => ({ type: CellType.SINGLE_SELECT, name: gettext('Tags') }), []);
const onEditFileTagToggle = useCallback(() => {
setEditFileTagShow(!isEditFileTagShow);
@@ -28,25 +33,37 @@ const FileDetails = React.memo(({ repoID, repoInfo, dirent, path, direntDetail,
return (
<>
<DetailItem field={{ type: 'size', name: gettext('Size') }} value={Utils.bytesToSize(direntDetail.size)} />
<DetailItem field={{ type: CellType.LAST_MODIFIER, name: gettext('Last modifier') }} value={direntDetail.last_modifier_email} collaborators={[{
<DetailItem field={sizeField} className="sf-metadata-property-detail-formatter">
<Formatter field={sizeField} value={Utils.bytesToSize(direntDetail.size)} />
</DetailItem>
<DetailItem field={lastModifierField} className="sf-metadata-property-detail-formatter">
<Formatter
field={lastModifierField}
value={direntDetail.last_modifier_email}
collaborators={[{
name: direntDetail.last_modifier_name,
contact_email: direntDetail.last_modifier_contact_email,
email: direntDetail.last_modifier_email,
avatar_url: direntDetail.last_modifier_avatar,
}]} />
<DetailItem field={{ type: CellType.MTIME, name: gettext('Last modified time') }} value={direntDetail.last_modified} />
}]}
/>
</DetailItem >
<DetailItem field={lastModifiedTimeField} className="sf-metadata-property-detail-formatter">
<Formatter field={lastModifiedTimeField} value={direntDetail.last_modified}/>
</DetailItem>
{!window.app.pageOptions.enableMetadataManagement && enableMetadata && (
<DetailItem field={{ type: CellType.SINGLE_SELECT, name: gettext('Tags') }} valueId={tagListTitleID} valueClick={onEditFileTagToggle} >
<DetailItem field={tagsField} className="sf-metadata-property-detail-formatter">
<div className="" id={tagListTitleID} onClick={onEditFileTagToggle}>
{Array.isArray(fileTagList) && fileTagList.length > 0 ? (
<FileTagList fileTagList={fileTagList} />
) : (
<span className="empty-tip-text">{gettext('Empty')}</span>
)}
</div>
</DetailItem>
)}
{window.app.pageOptions.enableMetadataManagement && (
<MetadataDetails repoID={repoID} filePath={direntPath} direntType="file" { ...params } />
<MetadataDetails repoID={repoID} filePath={direntPath} repoInfo={repoInfo} direntType="file" />
)}
{isEditFileTagShow &&
<EditFileTagPopover

View File

@@ -1,6 +1,6 @@
import React from 'react';
import PropTypes from 'prop-types';
import { siteRoot, mediaUrl } from '../../../utils/constants';
import { siteRoot } from '../../../utils/constants';
import { seafileAPI } from '../../../utils/seafile-api';
import { Utils } from '../../../utils/utils';
import toaster from '../../toast';
@@ -9,9 +9,6 @@ import { Detail, Header, Body } from '../detail';
import DirDetails from './dir-details';
import FileDetails from './file-details';
import ObjectUtils from '../../../metadata/metadata-view/utils/object-utils';
import metadataAPI from '../../../metadata/api';
import { User } from '../../../metadata/metadata-view/model';
import { UserService } from '../../../metadata/metadata-view/_basic';
import './index.css';
@@ -22,26 +19,9 @@ class DirentDetails extends React.Component {
this.state = {
direntDetail: '',
dirent: null,
collaborators: [],
collaboratorsCache: {},
};
this.userService = new UserService({ mediaUrl, api: metadataAPI.listUserInfo });
}
updateCollaboratorsCache = (user) => {
const newCollaboratorsCache = { ...this.state.collaboratorsCache, [user.email]: user };
this.setState({ collaboratorsCache: newCollaboratorsCache });
};
loadCollaborators = () => {
metadataAPI.getCollaborators(this.props.repoID).then(res => {
const collaborators = Array.isArray(res?.data?.user_list) ? res.data.user_list.map(user => new User(user)) : [];
this.setState({ collaborators });
}).catch(error => {
this.setState({ collaborators: [] });
});
};
updateDetail = (repoID, dirent, direntPath) => {
const apiName = dirent.type === 'file' ? 'getFileInfo' : 'getDirInfo';
seafileAPI[apiName](repoID, direntPath).then(res => {
@@ -73,7 +53,6 @@ class DirentDetails extends React.Component {
};
componentDidMount() {
this.loadCollaborators();
this.loadDetail(this.props.repoID, this.props.dirent, this.props.path);
}
@@ -108,7 +87,7 @@ class DirentDetails extends React.Component {
};
render() {
const { dirent, direntDetail, collaborators, collaboratorsCache } = this.state;
const { dirent, direntDetail } = this.state;
const { repoID, path, fileTags } = this.props;
const direntName = dirent?.name || '';
const smallIconUrl = Utils.getDirentIcon(dirent);
@@ -127,10 +106,6 @@ class DirentDetails extends React.Component {
dirent={dirent}
direntDetail={direntDetail}
path={this.props.dirent ? path + '/' + dirent.name : path}
collaborators={collaborators}
collaboratorsCache={collaboratorsCache}
updateCollaboratorsCache={this.updateCollaboratorsCache}
queryUserAPI={this.userService?.queryUser}
/>
) : (
<FileDetails
@@ -142,10 +117,6 @@ class DirentDetails extends React.Component {
repoTags={this.props.repoTags}
fileTagList={dirent ? dirent.file_tags : fileTags}
onFileTagChanged={this.props.onFileTagChanged}
collaborators={collaborators}
collaboratorsCache={collaboratorsCache}
updateCollaboratorsCache={this.updateCollaboratorsCache}
queryUserAPI={this.userService?.queryUser}
/>
)}
</div>

View File

@@ -1,11 +1,24 @@
import React from 'react';
import React, { useEffect } from 'react';
import PropTypes from 'prop-types';
import LibDetail from './lib-details';
import DirentDetail from './dirent-details';
import ObjectUtils from '../../metadata/metadata-view/utils/object-utils';
import { MetadataContext } from '../../metadata';
import { mediaUrl } from '../../utils/constants';
const Index = React.memo(({ repoID, path, dirent, currentRepoInfo, repoTags, fileTags, onClose, onFileTagChanged }) => {
useEffect(() => {
// init context
const context = new MetadataContext();
window.sfMetadataContext = context;
window.sfMetadataContext.init({ repoID, mediaUrl, repoInfo: currentRepoInfo });
return () => {
window.sfMetadataContext.destroy();
delete window['sfMetadataContext'];
};
}, [repoID, currentRepoInfo]);
if (path === '/' && !dirent) {
return (
<LibDetail currentRepoInfo={currentRepoInfo} onClose={onClose} />

View File

@@ -1,5 +1,6 @@
import React, { useEffect, useMemo, useState } from 'react';
import PropTypes from 'prop-types';
import { Formatter } from '@seafile/sf-metadata-ui-component';
import { Utils } from '../../../utils/utils';
import { gettext } from '../../../utils/constants';
import { seafileAPI } from '../../../utils/seafile-api';
@@ -14,6 +15,10 @@ const LibDetail = React.memo(({ currentRepoInfo, onClose }) => {
const [isLoading, setLoading] = useState(true);
const [repo, setRepo] = useState({});
const smallIconUrl = useMemo(() => Utils.getLibIconUrl(currentRepoInfo), [currentRepoInfo]);
const filesField = useMemo(() => ({ type: CellType.NUMBER, name: gettext('Files') }), []);
const sizeField = useMemo(() => ({ type: 'size', name: gettext('Size') }), []);
const creatorField = useMemo(() => ({ type: CellType.CREATOR, name: gettext('Creator') }), []);
const mtimeField = useMemo(() => ({ type: CellType.MTIME, name: gettext('Last modified time') }), []);
useEffect(() => {
setLoading(true);
@@ -35,15 +40,27 @@ const LibDetail = React.memo(({ currentRepoInfo, onClose }) => {
<div className="w-100 h-100 d-flex algin-items-center justify-content-center"><Loading /></div>
) : (
<div className="detail-content">
<DetailItem field={{ type: CellType.NUMBER, name: gettext('Files') }} value={repo.file_count} />
<DetailItem field={{ type: 'size', name: gettext('Size') }} value={repo.size} />
<DetailItem field={{ type: CellType.CREATOR, name: gettext('Creator') }} value={repo.owner_email} collaborators={[{
<DetailItem field={filesField} value={repo.file_count || 0} className="sf-metadata-property-detail-formatter">
<Formatter field={filesField} value={repo.file_count || 0} />
</DetailItem>
<DetailItem field={sizeField} value={repo.size} className="sf-metadata-property-detail-formatter">
<Formatter field={sizeField} value={repo.size} />
</DetailItem>
<DetailItem field={creatorField} className="sf-metadata-property-detail-formatter">
<Formatter
field={creatorField}
value={repo.owner_email}
collaborators={[{
name: repo.owner_name,
contact_email: repo.owner_contact_email,
email: repo.owner_email,
avatar_url: repo.owner_avatar,
}]} />
<DetailItem field={{ type: CellType.MTIME, name: gettext('Last modified time') }} value={repo.last_modified} />
}]}
/>
</DetailItem>
<DetailItem field={mtimeField} className="sf-metadata-property-detail-formatter">
<Formatter field={mtimeField} value={repo.last_modified} />
</DetailItem>
</div>
)}
</Body>

View File

@@ -1,25 +1,39 @@
/* eslint-disable react/prop-types */
import React, { useContext, useState, useCallback, useEffect } from 'react';
import { useMetadata } from './metadata';
import { mediaUrl } from '../../../utils/constants';
import { isValidEmail } from '../_basic';
import React, { useContext, useState, useCallback, useEffect, useMemo } from 'react';
import { mediaUrl } from '../../utils/constants';
import { isValidEmail, UserService } from '../metadata-view/_basic';
import User from '../metadata-view/model/user';
import metadataAPI from '../api';
const CollaboratorsContext = React.createContext(null);
export const CollaboratorsProvider = ({
children,
}) => {
export const CollaboratorsProvider = ({ repoID, children }) => {
const [collaboratorsCache, setCollaboratorsCache] = useState({});
const [collaborators, setCollaborators] = useState([]);
const { store } = useMetadata();
const queryUser = useMemo(() => {
const userService = new UserService({ mediaUrl, api: metadataAPI.listUserInfo });
const queryUserAPI = userService.queryUser;
window.queryUser = queryUserAPI;
return queryUserAPI;
}, []);
useEffect(() => {
setCollaborators(store?.collaborators || []);
}, [store?.collaborators]);
metadataAPI.getCollaborators(repoID).then(res => {
const collaborators = Array.isArray(res?.data?.user_list) ? res.data.user_list.map(user => new User(user)) : [];
setCollaborators(collaborators);
});
}, [repoID]);
useEffect(() => {
if (!window.sfMetadata) return;
if (!window.sfMetadata) {
window.sfMetadata = {};
window.sfMetadata.getCollaboratorsFromCache = () => {
return Object.values(window.sfMetadata.collaboratorsCache || {}) || [];
};
window.sfMetadata.getCollaborators = () => {
return [...window.sfMetadata.collaborators, ...(Object.values(window.sfMetadata.collaboratorsCache || {}) || [])];
};
}
window.sfMetadata.collaborators = collaborators;
window.sfMetadata.collaboratorsCache = collaboratorsCache;
}, [collaborators, collaboratorsCache]);
@@ -54,7 +68,7 @@ export const CollaboratorsProvider = ({
}, [collaborators, collaboratorsCache]);
return (
<CollaboratorsContext.Provider value={{ collaborators, collaboratorsCache, updateCollaboratorsCache, getCollaborator }}>
<CollaboratorsContext.Provider value={{ collaborators, collaboratorsCache, updateCollaboratorsCache, getCollaborator, queryUser }}>
{children}
</CollaboratorsContext.Provider>
);
@@ -65,6 +79,6 @@ export const useCollaborators = () => {
if (!context) {
throw new Error('\'CollaboratorsContext\' is null');
}
const { collaborators, collaboratorsCache, updateCollaboratorsCache, getCollaborator } = context;
return { collaborators, collaboratorsCache, updateCollaboratorsCache, getCollaborator };
const { collaborators, collaboratorsCache, updateCollaboratorsCache, getCollaborator, queryUser } = context;
return { collaborators, collaboratorsCache, updateCollaboratorsCache, getCollaborator, queryUser };
};

View File

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

View File

@@ -19,29 +19,46 @@ export const MetadataProvider = ({ repoID, hideMetadataView, selectMetadataView,
const [navigation, setNavigation] = useState([]);
const viewsMap = useRef({});
const cancelURLView = useCallback(() => {
// If attribute extension is turned off, unmark the URL
const { origin, pathname, search } = window.location;
const urlParams = new URLSearchParams(search);
const viewID = urlParams.get('view');
if (viewID) {
const url = `${origin}${pathname}`;
window.history.pushState({ url: url, path: '' }, '', url);
}
}, []);
useEffect(() => {
if (!enableMetadataManagement) {
setEnableExtendedProperties(false);
cancelURLView();
return;
}
metadataAPI.getMetadataStatus(repoID).then(res => {
setEnableExtendedProperties(res.data.enabled);
const enableMetadata = res.data.enabled;
if (!enableMetadata) {
cancelURLView();
}
setEnableExtendedProperties(enableMetadata);
}).catch(error => {
const errorMsg = Utils.getErrorMsg(error, true);
toaster.danger(errorMsg);
setEnableExtendedProperties(false);
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [repoID, enableMetadataManagement]);
const updateEnableMetadata = useCallback((newValue) => {
if (newValue === enableMetadata) return;
if (!newValue) {
hideMetadataView && hideMetadataView();
cancelURLView();
} else {
setShowFirstView(true);
}
setEnableExtendedProperties(newValue);
}, [enableMetadata, hideMetadataView]);
}, [enableMetadata, hideMetadataView, cancelURLView]);
// views
useEffect(() => {
@@ -61,14 +78,6 @@ export const MetadataProvider = ({ repoID, hideMetadataView, selectMetadataView,
return;
}
// If attribute extension is turned off, unmark the URL
const { origin, pathname, search } = window.location;
const urlParams = new URLSearchParams(search);
const viewID = urlParams.get('view');
if (viewID) {
const url = `${origin}${pathname}`;
window.history.pushState({ url: url, path: '' }, '', url);
}
viewsMap.current = {};
setNavigation([]);
// eslint-disable-next-line react-hooks/exhaustive-deps

View File

@@ -1,4 +1,4 @@
import SeafileMetadata from './metadata-view';
import SeafileMetadata, { Context as MetadataContext } from './metadata-view';
import MetadataStatusManagementDialog from './metadata-status-manage-dialog';
import MetadataTreeView from './metadata-tree-view';
import MetadataDetails from './metadata-details';
@@ -8,6 +8,7 @@ export * from './hooks';
export {
metadataAPI,
MetadataContext,
SeafileMetadata,
MetadataStatusManagementDialog,
MetadataTreeView,

View File

@@ -1,4 +1,4 @@
import React, { useEffect, useState } from 'react';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import PropTypes from 'prop-types';
import { Utils } from '../../utils/utils';
import metadataAPI from '../api';
@@ -6,12 +6,17 @@ import Column from '../metadata-view/model/metadata/column';
import { normalizeFields, getCellValueByColumn } from './utils';
import DetailItem from '../../components/dirent-detail/detail-item';
import toaster from '../../components/toast';
import { gettext } from '../../utils/constants';
import { DetailEditor, CellFormatter } from '../metadata-view';
import { getColumnOriginName } from '../metadata-view/utils/column-utils';
import { CellType, getColumnOptions, getOptionName, PREDEFINED_COLUMN_KEYS } from '../metadata-view/_basic';
import './index.css';
const MetadataDetails = ({ repoID, filePath, direntType, emptyTip, ...params }) => {
const MetadataDetails = ({ repoID, filePath, repoInfo, direntType, emptyTip }) => {
const [isLoading, setLoading] = useState(true);
const [metadata, setMetadata] = useState({ record: {}, fields: [] });
const permission = useMemo(() => repoInfo.permission !== 'admin' && repoInfo.permission !== 'rw' ? 'r' : 'rw', [repoInfo]);
useEffect(() => {
setLoading(true);
@@ -34,18 +39,77 @@ const MetadataDetails = ({ repoID, filePath, direntType, emptyTip, ...params })
});
}, [repoID, filePath, direntType]);
const onChange = useCallback((fieldKey, newValue) => {
const { record, fields } = metadata;
const field = fields.find(f => f.key === fieldKey);
const fileName = getColumnOriginName(field);
let update = { [fileName]: newValue };
if (!PREDEFINED_COLUMN_KEYS.includes(field.key) && field.type === CellType.SINGLE_SELECT) {
const options = getColumnOptions(field);
update = { [fileName]: getOptionName(options, newValue) };
}
metadataAPI.modifyRecord(repoID, record._id, update).then(res => {
const newMetadata = { ...metadata, record: { ...record, ...update } };
setMetadata(newMetadata);
}).catch(error => {
const errorMsg = Utils.getErrorMsg(error);
toaster.danger(errorMsg);
});
}, [repoID, metadata]);
const modifyColumnData = useCallback((fieldKey, newData) => {
const { fields, record } = metadata;
let newFields = fields.slice(0);
let update;
metadataAPI.modifyColumnData(repoID, fieldKey, newData).then(res => {
const newField = new Column(res.data.column);
const fieldIndex = fields.findIndex(f => f.key === fieldKey);
newFields[fieldIndex] = newField;
return newField;
}).then((newField) => {
const fileName = getColumnOriginName(newField);
const options = getColumnOptions(newField);
const newOption = options[options.length - 1];
update = { [fileName]: newOption.id };
if (!PREDEFINED_COLUMN_KEYS.includes(fieldKey) && newField.type === CellType.SINGLE_SELECT) {
update = { [fileName]: getOptionName(options, newOption.id) };
}
return metadataAPI.modifyRecord(repoID, record._id, update);
}).then(res => {
const newMetadata = { ...metadata, record: { ...record, ...update }, fields: newFields };
setMetadata(newMetadata);
}).catch(error => {
const errorMsg = Utils.getErrorMsg(error);
toaster.danger(errorMsg);
});
}, [repoID, metadata]);
if (isLoading) return null;
const { fields, record } = metadata;
if (!record._id) return null;
return fields.map(field => {
return (
<>
{fields.map(field => {
const canEdit = permission === 'rw' && field.editable;
const value = getCellValueByColumn(record, field);
return (<DetailItem key={field.key} field={field} value={value} emptyTip={emptyTip} { ...params } />);
});
return (
<DetailItem key={field.key} field={field} readonly={!canEdit}>
{canEdit ? (
<DetailEditor field={field} value={value} onChange={onChange} fields={fields} record={record} modifyColumnData={modifyColumnData} />
) : (
<CellFormatter field={field} value={value} emptyTip={gettext('Empty')} className="sf-metadata-property-detail-formatter" />
)}
</DetailItem>
);
})}
</>
);
};
MetadataDetails.propTypes = {
repoID: PropTypes.string,
filePath: PropTypes.string,
repoInfo: PropTypes.object,
direntType: PropTypes.string,
direntDetail: PropTypes.object,
};

View File

@@ -51,7 +51,7 @@ export const EDITABLE_PRIVATE_COLUMN_KEYS = [
PRIVATE_COLUMN_KEY.FILE_COLLABORATORS,
PRIVATE_COLUMN_KEY.FILE_EXPIRE_TIME,
PRIVATE_COLUMN_KEY.FILE_KEYWORDS,
PRIVATE_COLUMN_KEY.FILE_SUMMARY,
// PRIVATE_COLUMN_KEY.FILE_SUMMARY,
PRIVATE_COLUMN_KEY.FILE_EXPIRED,
PRIVATE_COLUMN_KEY.FILE_STATUS,
];

View File

@@ -150,4 +150,6 @@ export {
isRegExpression,
getGeolocationDisplayString,
getGeolocationByGranularity,
getFloatNumber,
isNumber,
} from './utils';

View File

@@ -5,6 +5,8 @@ export {
replaceNumberNotAllowInput,
formatStringToNumber,
formatTextToNumber,
getFloatNumber,
isNumber,
} from './number';
export {
getOption,

View File

@@ -21,4 +21,6 @@ export {
getLongtextDisplayString,
getGeolocationDisplayString,
getGeolocationByGranularity,
getFloatNumber,
isNumber,
} from './column';

View File

@@ -21,6 +21,8 @@ export {
getLongtextDisplayString,
getGeolocationDisplayString,
getGeolocationByGranularity,
getFloatNumber,
isNumber,
} from './cell';
export {
getColumnType,

View File

@@ -45,3 +45,9 @@
fill: #666;
font-size: 12px;
}
.sf-metadata-delete-collaborator .collaborator .collaborator-remove {
height: 14px;
width: 14px;
margin-left: 2px;
}

View File

@@ -1,7 +1,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import { IconBtn } from '@seafile/sf-metadata-ui-component';
import { useCollaborators } from '../../../../hooks';
import { useCollaborators } from '../../../../../hooks';
import './index.css';
@@ -20,7 +20,7 @@ const DeleteCollaborator = ({ value, onDelete }) => {
<img className="collaborator-avatar m-0" alt={name} src={avatar_url} />
</span>
<span className="collaborator-name text-truncate" title={name} aria-label={name}>{name}</span>
<IconBtn className="collaborator-remove" onClick={() => onDelete(email)} iconName="x-01" />
<IconBtn className="collaborator-remove" onClick={(event) => onDelete(email, event)} iconName="x-01" />
</div>
);
})}

View File

@@ -5,12 +5,13 @@ import { SearchInput, Icon } from '@seafile/sf-metadata-ui-component';
import { isFunction } from '../../../_basic';
import { KeyCodes } from '../../../../../constants';
import { gettext } from '../../../../../utils/constants';
import { useCollaborators } from '../../../hooks';
import { useCollaborators } from '../../../../hooks';
import DeleteCollaborator from './delete-collaborator';
import './index.css';
const CollaboratorEditor = forwardRef(({
saveImmediately = false,
column,
value: oldValue,
onCommit,
@@ -22,7 +23,6 @@ const CollaboratorEditor = forwardRef(({
const [highlightIndex, setHighlightIndex] = useState(-1);
const [maxItemNum, setMaxItemNum] = useState(0);
const [itemHeight, setItemHeight] = useState(0);
const timerRef = useRef(null);
const editorContainerRef = useRef(null);
const editorRef = useRef(null);
const collaboratorItemRef = useRef(null);
@@ -70,7 +70,10 @@ const CollaboratorEditor = forwardRef(({
newValue.push(email);
}
setValue(newValue);
}, [value]);
if (saveImmediately) {
onCommit && onCommit(newValue);
}
}, [saveImmediately, value, onCommit]);
const onDeleteCollaborator = useCallback((email) => {
const newValue = value.slice(0);
@@ -79,7 +82,10 @@ const CollaboratorEditor = forwardRef(({
newValue.splice(collaboratorIndex, 1);
}
setValue(newValue);
}, [value]);
if (saveImmediately) {
onCommit && onCommit(newValue);
}
}, [saveImmediately, value, onCommit]);
const onMenuMouseEnter = useCallback((highlightIndex) => {
setHighlightIndex(highlightIndex);
@@ -182,8 +188,6 @@ const CollaboratorEditor = forwardRef(({
document.addEventListener('keydown', onHotKey, true);
return () => {
document.removeEventListener('keydown', onHotKey, true);
timerRef.current && clearTimeout(timerRef.current);
timerRef.current = null;
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [onHotKey]);
@@ -214,7 +218,7 @@ const CollaboratorEditor = forwardRef(({
<div key={collaborator.email} className="sf-metadata-collaborator-item" ref={collaboratorItemRef}>
<div
className={classnames('collaborator-container', { 'collaborator-container-highlight': i === highlightIndex })}
onMouseDown={() => onSelectCollaborator(isSelected ? null : collaborator.email)}
onMouseDown={() => onSelectCollaborator(collaborator.email)}
onMouseEnter={() => onMenuMouseEnter(i)}
onMouseLeave={() => onMenuMouseLeave(i)}
>
@@ -238,7 +242,7 @@ const CollaboratorEditor = forwardRef(({
<div className="sf-metadata-collaborator-editor" style={{ top: -38 }} ref={editorRef}>
<DeleteCollaborator value={value} onDelete={onDeleteCollaborator} />
<div className="sf-metadata-search-collaborator-options">
<SearchInput placeholder={gettext('Search collaborators')} onKeyDown={onKeyDown} onChange={onChangeSearch} autoFocus={true} />
<SearchInput placeholder={gettext('Search collaborators')} onKeyDown={onKeyDown} onChange={onChangeSearch} autoFocus={true} className="sf-metadata-search-collaborators" />
</div>
<div className="sf-metadata-collaborator-editor-container" ref={editorContainerRef}>
{renderCollaborators()}
@@ -248,6 +252,7 @@ const CollaboratorEditor = forwardRef(({
});
CollaboratorEditor.propTypes = {
saveImmediately: PropTypes.bool,
column: PropTypes.object,
value: PropTypes.array,
onCommit: PropTypes.func,

View File

@@ -8,7 +8,7 @@ import { isCellValueChanged } from '../../../utils/cell-comparer';
import { EVENT_BUS_TYPE } from '../../../constants';
import { getEventClassName } from '../../../utils';
import Editor from '../editor';
import { canEdit } from '../../../utils/column-utils';
import { canEditCell } from '../../../utils/column-utils';
class NormalEditorContainer extends React.Component {
@@ -83,7 +83,7 @@ class NormalEditorContainer extends React.Component {
const { column, record, openEditorMode, columns, modifyColumnData } = this.props;
const editorProps = {
ref: this.setEditorRef,
readOnly: !canEdit(column, record, true),
readOnly: !canEditCell(column, record, true),
columns,
column: this.props.column,
value: this.getInitialValue(),

View File

@@ -6,7 +6,7 @@ import { CellType, isFunction, Z_INDEX, getCellValueByColumn, getColumnOptionNam
import { isCellValueChanged } from '../../../utils/cell-comparer';
import { EVENT_BUS_TYPE } from '../../../constants';
import Editor from '../editor';
import { canEdit } from '../../../utils/column-utils';
import { canEditCell } from '../../../utils/column-utils';
const NOT_SUPPORT_EDITOR_COLUMN_TYPES = [
CellType.CTIME, CellType.MTIME, CellType.CREATOR, CellType.LAST_MODIFIER,
@@ -58,7 +58,7 @@ class PopupEditorContainer extends React.Component {
createEditor = () => {
const { column, record, height, onPressTab, editorPosition, columns, modifyColumnData } = this.props;
const readOnly = canEdit(column, record, true) || NOT_SUPPORT_EDITOR_COLUMN_TYPES.includes(column.type);
const readOnly = canEditCell(column, record, true) || NOT_SUPPORT_EDITOR_COLUMN_TYPES.includes(column.type);
const value = this.getInitialValue(readOnly);
let editorProps = {

View File

@@ -63,6 +63,7 @@
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
margin-right: 10px;
}
.single-select-avatar {
@@ -82,12 +83,13 @@
padding: 0px 10px;
height: 20px;
line-height: 20px;
text-align: center;
margin-left: 0;
border-radius: 10px;
overflow: hidden;
text-overflow: ellipsis;
margin-top: 5px;
white-space: nowrap;
max-width: 100%;
}
.sf-metadata-single-select-editor .single-select-check-icon {

View File

@@ -2,8 +2,8 @@ import React, { forwardRef, useMemo, useImperativeHandle, useCallback, useState,
import PropTypes from 'prop-types';
import classnames from 'classnames';
import { SearchInput, CustomizeAddTool, Icon } from '@seafile/sf-metadata-ui-component';
import { getCellValueByColumn, getColumnByKey, isFunction } from '../../../_basic';
import { generateNewOption, getSelectColumnOptions } from '../../../utils/select-utils';
import { getCellValueByColumn, getColumnByKey, isFunction, getColumnOptions } from '../../../_basic';
import { generateNewOption } from '../../../utils/select-utils';
import { KeyCodes } from '../../../../../constants';
import { gettext } from '../../../../../utils/constants';
@@ -23,15 +23,14 @@ const SingleSelectEditor = forwardRef(({
const [searchValue, setSearchValue] = useState('');
const [highlightIndex, setHighlightIndex] = useState(-1);
const [maxItemNum, setMaxItemNum] = useState(0);
const [itemHeight, setItemHeight] = useState(0);
const timerRef = useRef(null);
const itemHeight = 30;
const editorContainerRef = useRef(null);
const editorRef = useRef(null);
const selectItemRef = useRef(null);
const canEditData = window.sfMetadataContext.canModifyColumnData(column);
const options = useMemo(() => {
const options = getSelectColumnOptions(column);
const options = getColumnOptions(column);
const { data } = column;
const { cascade_column_key, cascade_settings } = data || {};
if (cascade_column_key) {
@@ -64,8 +63,8 @@ const SingleSelectEditor = forwardRef(({
}, [column, height]);
const blur = useCallback(() => {
onCommit && onCommit();
}, [onCommit]);
onCommit && onCommit(value);
}, [value, onCommit]);
const onChangeSearch = useCallback((newSearchValue) => {
if (searchValue === newSearchValue) return;
@@ -76,7 +75,7 @@ const SingleSelectEditor = forwardRef(({
if (optionId === value) return;
setValue(optionId);
setTimeout(() => {
onCommit && onCommit();
onCommit && onCommit(optionId);
}, 1);
}, [value, onCommit]);
@@ -89,6 +88,7 @@ const SingleSelectEditor = forwardRef(({
}, []);
const createOption = useCallback((event) => {
event && event.stopPropagation();
event && event.nativeEvent.stopImmediatePropagation();
const newOption = generateNewOption(options, searchValue?.trim() || '');
let newOptions = options.slice(0);
@@ -180,13 +180,10 @@ const SingleSelectEditor = forwardRef(({
}
if (editorContainerRef.current && selectItemRef.current) {
setMaxItemNum(getMaxItemNum());
setItemHeight(parseInt(getComputedStyle(selectItemRef.current, null).height));
}
document.addEventListener('keydown', onHotKey, true);
return () => {
document.removeEventListener('keydown', onHotKey, true);
timerRef.current && clearTimeout(timerRef.current);
timerRef.current = null;
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [onHotKey]);
@@ -211,12 +208,8 @@ const SingleSelectEditor = forwardRef(({
return (<span className="none-search-result">{noOptionsTip}</span>);
}
// maxWidth = single-selects-container's width - single-selects-container's padding-left and padding-right - single-select-container's padding-left - single-select-check-icon's width - The gap between the single-select-check-icon and single-select-name or scroll's width
// maxWidth = column.width > 200 ? column.width - 20 - 12 - 20 - 10 : 200 - 20 - 12 - 20 - 10
// maxWidth = column.width > 200 ? column.width - 62 : 200 - 62
const maxWidth = column.width > 200 ? column.width - 62 : 200 - 62;
return displayOptions.map((option, i) => {
const isSelected = value === option.name;
const isSelected = value === option.id || value === option.name;
return (
<div key={option.id} className="sf-metadata-single-select-item" ref={selectItemRef}>
<div
@@ -228,7 +221,7 @@ const SingleSelectEditor = forwardRef(({
<div className="single-select">
<span
className="single-select-name"
style={{ backgroundColor: option.color, color: option.textColor || null, maxWidth }}
style={{ backgroundColor: option.color, color: option.textColor || null }}
title={option.name}
aria-label={option.name}
>
@@ -243,7 +236,7 @@ const SingleSelectEditor = forwardRef(({
);
});
}, [displayOptions, searchValue, column, value, highlightIndex, onMenuMouseEnter, onMenuMouseLeave, onSelectOption]);
}, [displayOptions, searchValue, value, highlightIndex, onMenuMouseEnter, onMenuMouseLeave, onSelectOption]);
return (
<div className="sf-metadata-single-select-editor" style={style} ref={editorRef}>
@@ -253,6 +246,7 @@ const SingleSelectEditor = forwardRef(({
onKeyDown={onKeyDown}
onChange={onChangeSearch}
autoFocus={true}
className="sf-metadata-search-options"
/>
</div>
<div className="sf-metadata-single-select-editor-container" ref={editorContainerRef}>
@@ -280,4 +274,3 @@ SingleSelectEditor.propTypes = {
};
export default SingleSelectEditor;

View File

@@ -1,11 +1,11 @@
import React, { useMemo } from 'react';
import PropTypes from 'prop-types';
import { Formatter } from '@seafile/sf-metadata-ui-component';
import { useCollaborators } from '../../hooks';
import { useCollaborators } from '../../../hooks';
import { Utils } from '../../../../utils/utils';
const CellFormatter = ({ readonly, value, field, ...params }) => {
const { collaborators, collaboratorsCache, updateCollaboratorsCache } = useCollaborators();
const { collaborators, collaboratorsCache, updateCollaboratorsCache, queryUser } = useCollaborators();
const props = useMemo(() => {
return {
collaborators,
@@ -14,11 +14,11 @@ const CellFormatter = ({ readonly, value, field, ...params }) => {
readonly,
value,
field,
queryUserAPI: window.sfMetadataContext.userService.queryUser,
queryUserAPI: queryUser,
getFileIconUrl: Utils.getFileIconUrl,
getFolderIconUrl: Utils.getFolderIconUrl,
};
}, [readonly, value, field, collaborators, collaboratorsCache, updateCollaboratorsCache]);
}, [readonly, value, field, collaborators, collaboratorsCache, updateCollaboratorsCache, queryUser]);
return (
<Formatter { ...props } { ...params } />

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,7 +1,11 @@
import DeleteConfirmDialog from './delete-confirm-dialog';
import Table from './table';
import DetailEditor from './detail-editor';
import CellFormatter from './cell-formatter';
export {
DeleteConfirmDialog,
Table,
DetailEditor,
CellFormatter,
};

View File

@@ -9,8 +9,8 @@ import {
filterTermModifierIsWithin,
isDateColumn,
FILTER_ERR_MSG,
getColumnOptions as getSelectColumnOptions,
} from '../../../../../_basic';
import { getSelectColumnOptions } from '../../../../../utils/select-utils';
import CollaboratorFilter from './collaborator-filter';
import FilterCalendar from '../filter-calendar';
import FilterItemUtils from '../filter-item-utils';

View File

@@ -5,7 +5,7 @@
text-overflow: ellipsis;
white-space: nowrap;
border-right: 1px solid #eee;
padding: 4px 8px;
padding: 6px 8px;
display: flex;
justify-content: flex-start;
}
@@ -71,7 +71,7 @@
/* cell formatter */
.sf-metadata-result-table-cell .sf-metadata-ui.cell-formatter-container {
height: 100%;
line-height: 24px;
line-height: 20px;
font-size: 14px;
}
@@ -303,3 +303,17 @@
padding-top: 5.5px;
padding-bottom: 5.5px;
}
/* file name */
.sf-metadata-result-table-cell.sf-metadata-result-table-file-name-cell {
padding-top: 4px;
padding-bottom: 4px;
}
.sf-metadata-result-table-cell.sf-metadata-result-table-file-name-cell .sf-metadata-ui.cell-formatter-container {
line-height: 24px;
}
.sf-metadata-result-table-file-name-cell .sf-metadata-ui.file-name-formatter .sf-metadata-file-icon {
transform: translateY(-1px);
}

View File

@@ -1,9 +1,8 @@
import metadataAPI from '../api';
import { UserService, LocalStorage, PRIVATE_COLUMN_KEYS, EDITABLE_DATA_PRIVATE_COLUMN_KEYS,
import { LocalStorage, PRIVATE_COLUMN_KEYS, EDITABLE_DATA_PRIVATE_COLUMN_KEYS,
EDITABLE_PRIVATE_COLUMN_KEYS, PREDEFINED_COLUMN_KEYS } from './_basic';
import EventBus from '../../components/common/event-bus';
import { username } from '../../utils/constants';
import User from './model/user';
class Context {
@@ -11,29 +10,26 @@ class Context {
this.settings = {};
this.metadataAPI = null;
this.localStorage = null;
this.userService = null;
this.eventBus = null;
this.hasInit = false;
this.permission = 'r';
this.collaboratorsCache = {};
}
async init({ otherSettings }) {
async init(settings) {
if (this.hasInit) return;
// init settings
this.settings = otherSettings || {};
this.settings = settings || {};
// init metadataAPI
const { mediaUrl, repoInfo } = this.settings;
const { repoInfo } = this.settings;
this.metadataAPI = metadataAPI;
// init localStorage
const { repoID, viewID } = this.settings;
this.localStorage = new LocalStorage(`sf-metadata-${repoID}-${viewID}`);
// init userService
this.userService = new UserService({ mediaUrl, api: this.metadataAPI.listUserInfo });
const localStorageName = viewID ? `sf-metadata-${repoID}-${viewID}` : `sf-metadata-${repoID}`;
this.localStorage = new LocalStorage(localStorageName);
const eventBus = new EventBus();
this.eventBus = eventBus;
@@ -47,7 +43,6 @@ class Context {
this.settings = {};
this.metadataAPI = null;
this.localStorage = null;
this.userService = null;
this.eventBus = null;
this.hasInit = false;
this.permission = 'r';
@@ -135,23 +130,6 @@ class Context {
return true;
};
getCollaboratorFromCache(email) {
return this.collaboratorsCache[email];
}
getCollaboratorsFromCache() {
const collaboratorsCache = this.collaboratorsCache;
return Object.values(collaboratorsCache).filter(item => item.email !== 'anonymous');
}
updateCollaboratorsCache(email, collaborator) {
if (collaborator instanceof User) {
this.collaboratorsCache[email] = collaborator;
return;
}
this.collaboratorsCache[email] = new User(collaborator);
}
restoreRows = () => {
// todo
};

View File

@@ -1,7 +1,5 @@
import { CollaboratorsProvider, useCollaborators } from './collaborators';
import { MetadataProvider, useMetadata } from './metadata';
export {
CollaboratorsProvider, useCollaborators,
MetadataProvider, useMetadata,
};

View File

@@ -47,9 +47,8 @@ export const MetadataProvider = ({
setLoading(true);
// init context
const context = new Context();
window.sfMetadata = {};
window.sfMetadataContext = context;
window.sfMetadataContext.init({ otherSettings: { ...params, repoID, viewID } });
window.sfMetadataContext.init({ ...params, repoID, viewID });
storeRef.current = new Store({ context: window.sfMetadataContext, repoId: repoID, viewId: viewID });
window.sfMetadataStore = storeRef.current;
storeRef.current.initStartIndex();
@@ -68,7 +67,6 @@ export const MetadataProvider = ({
const unsubscribeReloadData = eventBus.subscribe(EVENT_BUS_TYPE.RELOAD_DATA, reloadMetadata);
return () => {
window.sfMetadata = {};
window.sfMetadataContext.destroy();
window.sfMetadataStore.destroy();
unsubscribeServerTableChanged();

View File

@@ -1,15 +1,13 @@
import React from 'react';
import PropTypes from 'prop-types';
import { MetadataProvider, CollaboratorsProvider } from './hooks/index';
import { Table } from './components/index';
import { MetadataProvider } from './hooks';
import { Table, DetailEditor, CellFormatter } from './components';
import Context from './context';
const SeafileMetadata = ({ ...params }) => {
return (
<MetadataProvider { ...params }>
<CollaboratorsProvider >
<Table />
</CollaboratorsProvider>
</MetadataProvider>
);
};
@@ -21,3 +19,8 @@ SeafileMetadata.propTypes = {
};
export default SeafileMetadata;
export {
Context,
DetailEditor,
CellFormatter,
};

View File

@@ -327,7 +327,7 @@ export const normalizeColumns = (columns) => {
return displayColumns;
};
export function canEdit(col, record, enableCellSelect) {
export function canEditCell(col, record, enableCellSelect) {
if (!col) return false;
if (window.sfMetadataContext.canModifyColumn(col) === false) return false;
if (col.editable != null && typeof (col.editable) === 'function') {

View File

@@ -1,6 +1,7 @@
import { CellType, DEFAULT_DATE_FORMAT, generatorCellOption, getCollaboratorsName, getOptionName, getDateDisplayString, PREDEFINED_COLUMN_KEYS } from '../_basic';
import { CellType, DEFAULT_DATE_FORMAT, generatorCellOption, getCollaboratorsName, getOptionName, getDateDisplayString,
PREDEFINED_COLUMN_KEYS, getFloatNumber, getNumberDisplayString, formatStringToNumber, isNumber, getColumnOptions,
} from '../_basic';
import { formatTextToDate } from './date';
import { getFloatNumber, getNumberDisplayString, formatStringToNumber, isNumber } from '../_basic/utils/cell/column/number';
const SUPPORT_PASTE_FROM_COLUMN = {
[CellType.NUMBER]: [CellType.TEXT, CellType.NUMBER],
@@ -8,13 +9,6 @@ const SUPPORT_PASTE_FROM_COLUMN = {
const reg_chinese_date_format = /(\d{4})年(\d{1,2})月(\d{1,2})日$/;
export function getSelectColumnOptions(column) {
if (!column || !column.data || !Array.isArray(column.data.options)) {
return [];
}
return column.data.options;
}
function convertCellValue(cellValue, oldCellValue, targetColumn, fromColumn) {
const { type: fromColumnType, data: fromColumnData } = fromColumn;
const { type: targetColumnType, data: targetColumnData } = targetColumn;
@@ -140,7 +134,7 @@ function convert2SingleSelect(cellValue, oldCellValue, fromColumn, targetColumn)
let fromOptionName;
switch (fromColumnType) {
case CellType.SINGLE_SELECT: {
const fromOptions = getSelectColumnOptions(fromColumn);
const fromOptions = getColumnOptions(fromColumn);
fromOptionName = getOptionName(fromOptions, cellValue) || '';
break;
}
@@ -156,7 +150,7 @@ function convert2SingleSelect(cellValue, oldCellValue, fromColumn, targetColumn)
return oldCellValue;
}
const currentOptions = getSelectColumnOptions(targetColumn);
const currentOptions = getColumnOptions(targetColumn);
const newOption = generatorCellOption(currentOptions, fromOptionName);
return PREDEFINED_COLUMN_KEYS.includes(targetColumn.key) ? newOption.id : newOption.name;
}
@@ -230,11 +224,11 @@ function convert2Text(cellValue, oldCellValue, fromColumn) {
return getDateDisplayString(cellValue, fromColumnData.format || DEFAULT_DATE_FORMAT);
}
case CellType.SINGLE_SELECT: {
const options = getSelectColumnOptions(fromColumn);
const options = getColumnOptions(fromColumn);
return getOptionName(options, cellValue) || null;
}
case CellType.COLLABORATOR: {
const collaborators = window.sfMetadata.collaborators || window.sfMetadataContext.getCollaboratorsFromCache();
const collaborators = window.sfMetadata.getCollaborators();
return getCollaboratorsName(collaborators, cellValue);
}
case CellType.CREATOR:
@@ -242,7 +236,7 @@ function convert2Text(cellValue, oldCellValue, fromColumn) {
if (!cellValue) {
return null;
}
const collaborators = window.sfMetadata.collaborators || window.sfMetadataContext.getCollaboratorsFromCache();
const collaborators = window.sfMetadata.getCollaborators();
return getCollaboratorsName(collaborators, [cellValue]);
}
default: {
@@ -257,7 +251,7 @@ function convert2Collaborator(cellValue, oldCellValue, fromColumnType) {
if (!Array.isArray(cellValue) || cellValue.length === 0) {
return null;
}
const collaborators = window.sfMetadata.collaborators || window.sfMetadataContext.getCollaboratorsFromCache();
const collaborators = window.sfMetadata.getCollaborators();
let validEmailMap = {};
collaborators.forEach(collaborator => validEmailMap[collaborator.email] = true);
return cellValue.filter(email => !!validEmailMap[email]);
@@ -270,7 +264,7 @@ function convert2Collaborator(cellValue, oldCellValue, fromColumnType) {
if (userNames.length === 0) {
return oldCellValue;
}
const collaborators = window.sfMetadata.collaborators || window.sfMetadataContext.getCollaboratorsFromCache();
const collaborators = window.sfMetadata.getCollaborators();
let nameCollaboratorMap = {};
collaborators.forEach(collaborator => nameCollaboratorMap[collaborator.name] = collaborator);
const emails = userNames.map(name => {
@@ -284,7 +278,7 @@ function convert2Collaborator(cellValue, oldCellValue, fromColumnType) {
}
case CellType.CREATOR:
case CellType.LAST_MODIFIER: {
const collaborators = window.sfMetadata.collaborators || window.sfMetadataContext.getCollaboratorsFromCache();
const collaborators = window.sfMetadata.getCollaborators();
let validEmailMap = {};
collaborators.forEach(collaborator => validEmailMap[collaborator.email] = true);
if (!cellValue || !validEmailMap[cellValue]) {

View File

@@ -1,13 +1,6 @@
import { SELECT_OPTION_COLORS } from '../_basic/constants/select-option';
import { generateOptionID } from '../_basic/utils/column/option';
export function getSelectColumnOptions(column) {
if (!column || !column.data || !Array.isArray(column.data.options)) {
return [];
}
return column.data.options;
}
const getNotDuplicateOption = (options) => {
const defaultOptions = SELECT_OPTION_COLORS.slice(12, 24);
let defaultOption = defaultOptions[Math.floor(Math.random() * defaultOptions.length)];

View File

@@ -1,5 +1,5 @@
import { Z_INDEX, getGroupByPath, isFunction, getCellValueByColumn } from '../_basic';
import { getColumnByIndex, canEdit } from './column-utils';
import { getColumnByIndex, canEditCell } from './column-utils';
import { SUPPORT_PREVIEW_COLUMN_TYPES } from '../constants';
import { getGroupRecordByIndex } from './group-metrics';
@@ -44,7 +44,7 @@ export const isSelectedCellEditable = ({ enableCellSelect, selectedPosition, col
const row = getSelectedRow({ selectedPosition, isGroupView, recordGetterByIndex });
if (!window.sfMetadataContext.canModifyRow(row)) return false;
const isCellEditable = isFunction(onCheckCellIsEditable) ? onCheckCellIsEditable({ row, column, ...selectedPosition }) : true;
return isCellEditable && canEdit(column, row, enableCellSelect);
return isCellEditable && canEditCell(column, row, enableCellSelect);
};
export function selectedRangeIsSingleCell(selectedRange) {

View File

@@ -24,7 +24,7 @@ import CopyMoveDirentProgressDialog from '../../components/dialog/copy-move-dire
import DeleteFolderDialog from '../../components/dialog/delete-folder-dialog';
import { EVENT_BUS_TYPE } from '../../components/common/event-bus-type';
import { PRIVATE_FILE_TYPE } from '../../constants';
import { MetadataProvider } from '../../metadata/hooks';
import { MetadataProvider, CollaboratorsProvider } from '../../metadata/hooks';
const propTypes = {
eventBus: PropTypes.object,
@@ -2174,6 +2174,7 @@ class LibContentView extends React.Component {
renameMetadataView={this.renameMetadataView}
hideMetadataView={this.hideFileMetadata}
>
<CollaboratorsProvider repoID={this.props.repoID}>
<div className="main-panel-center flex-row">
<LibContentContainer
isSidePanelFolded={this.props.isSidePanelFolded}
@@ -2291,6 +2292,7 @@ class LibContentView extends React.Component {
<MediaQuery query="(max-width: 767.8px)">
<Modal zIndex="1030" isOpen={!Utils.isDesktop() && this.state.isTreePanelShown} toggle={this.toggleTreePanel} contentClassName="d-none"></Modal>
</MediaQuery>
</CollaboratorsProvider>
</MetadataProvider>
);
}