1
0
mirror of https://github.com/haiwen/seahub.git synced 2025-09-06 17:33:18 +00:00

fix: metadata status toggle (#6530)

* fix: metadata status toggle

* feat: optimize code

---------

Co-authored-by: 杨国璇 <ygx@Hello-word.local>
This commit is contained in:
杨国璇
2024-08-12 17:15:56 +08:00
committed by GitHub
parent 0ec12c0948
commit 27bf8792da
17 changed files with 426 additions and 348 deletions

View File

@@ -23,7 +23,7 @@ const propTypes = {
sortBy: PropTypes.string, sortBy: PropTypes.string,
sortOrder: PropTypes.string, sortOrder: PropTypes.string,
sortItems: PropTypes.func, sortItems: PropTypes.func,
metadataViewId: PropTypes.string, viewId: PropTypes.string,
}; };
class DirTool extends React.Component { class DirTool extends React.Component {
@@ -100,7 +100,7 @@ class DirTool extends React.Component {
render() { render() {
const menuItems = this.getMenu(); const menuItems = this.getMenu();
const { isDropdownMenuOpen } = this.state; const { isDropdownMenuOpen } = this.state;
const { repoID, currentMode, currentPath, sortBy, sortOrder, metadataViewId } = this.props; const { repoID, currentMode, currentPath, sortBy, sortOrder, viewId } = this.props;
const propertiesText = TextTranslation.PROPERTIES.value; const propertiesText = TextTranslation.PROPERTIES.value;
const isFileExtended = currentPath.startsWith('/' + PRIVATE_FILE_TYPE.FILE_EXTENDED_PROPERTIES + '/'); const isFileExtended = currentPath.startsWith('/' + PRIVATE_FILE_TYPE.FILE_EXTENDED_PROPERTIES + '/');
@@ -114,7 +114,7 @@ class DirTool extends React.Component {
if (isFileExtended) { if (isFileExtended) {
return ( return (
<div className="d-flex"> <div className="d-flex">
<MetadataViewToolBar metadataViewId={metadataViewId} /> <MetadataViewToolBar viewId={viewId} />
</div> </div>
); );
} }

View File

@@ -38,7 +38,7 @@ const propTypes = {
filePermission: PropTypes.string, filePermission: PropTypes.string,
repoTags: PropTypes.array.isRequired, repoTags: PropTypes.array.isRequired,
onFileTagChanged: PropTypes.func.isRequired, onFileTagChanged: PropTypes.func.isRequired,
metadataViewId: PropTypes.string, viewId: PropTypes.string,
onItemMove: PropTypes.func.isRequired, onItemMove: PropTypes.func.isRequired,
}; };
@@ -101,7 +101,7 @@ class CurDirPath extends React.Component {
sortBy={this.props.sortBy} sortBy={this.props.sortBy}
sortOrder={this.props.sortOrder} sortOrder={this.props.sortOrder}
sortItems={this.props.sortItems} sortItems={this.props.sortItems}
metadataViewId={this.props.metadataViewId} viewId={this.props.viewId}
/>} />}
{!isDesktop && this.props.direntList.length > 0 && {!isDesktop && this.props.direntList.length > 0 &&
<span className="sf3-font sf3-font-sort action-icon" onClick={this.toggleSortOptionsDialog}></span>} <span className="sf3-font sf3-font-sort action-icon" onClick={this.toggleSortOptionsDialog}></span>}

View File

@@ -13,7 +13,7 @@ const propTypes = {
isFileLoadedErr: PropTypes.bool.isRequired, isFileLoadedErr: PropTypes.bool.isRequired,
filePermission: PropTypes.string, filePermission: PropTypes.string,
content: PropTypes.string, content: PropTypes.string,
metadataViewId: PropTypes.string, viewId: PropTypes.string,
lastModified: PropTypes.string, lastModified: PropTypes.string,
latestContributor: PropTypes.string, latestContributor: PropTypes.string,
onLinkClick: PropTypes.func.isRequired, onLinkClick: PropTypes.func.isRequired,
@@ -53,9 +53,9 @@ class DirColumnFile extends React.Component {
} }
if (this.props.content === '__sf-metadata') { if (this.props.content === '__sf-metadata') {
const { repoID, currentRepoInfo, metadataViewId } = this.props; const { repoID, currentRepoInfo, viewId } = this.props;
return (<SeafileMetadata mediaUrl={mediaUrl} repoID={repoID} repoInfo={currentRepoInfo} viewID={metadataViewId} />); return (<SeafileMetadata mediaUrl={mediaUrl} repoID={repoID} repoInfo={currentRepoInfo} viewID={viewId} />);
} }
return ( return (

View File

@@ -312,12 +312,7 @@ class DirColumnNav extends React.Component {
getMenuContainerSize={getMenuContainerSize} getMenuContainerSize={getMenuContainerSize}
/> />
</TreeSection> </TreeSection>
<DirViews <DirViews repoID={repoID} currentPath={currentPath} userPerm={userPerm} />
repoID={repoID}
currentPath={currentPath}
userPerm={userPerm}
onNodeClick={this.onNodeClick}
/>
<DirOthers <DirOthers
repoID={repoID} repoID={repoID}
userPerm={userPerm} userPerm={userPerm}

View File

@@ -37,7 +37,7 @@ const propTypes = {
hash: PropTypes.string, hash: PropTypes.string,
filePermission: PropTypes.string, filePermission: PropTypes.string,
content: PropTypes.string, content: PropTypes.string,
metadataViewId: PropTypes.string, viewId: PropTypes.string,
lastModified: PropTypes.string, lastModified: PropTypes.string,
latestContributor: PropTypes.string, latestContributor: PropTypes.string,
onLinkClick: PropTypes.func.isRequired, onLinkClick: PropTypes.func.isRequired,
@@ -194,7 +194,7 @@ class DirColumnView extends React.Component {
isFileLoadedErr={this.props.isFileLoadedErr} isFileLoadedErr={this.props.isFileLoadedErr}
filePermission={this.props.filePermission} filePermission={this.props.filePermission}
content={this.props.content} content={this.props.content}
metadataViewId={this.props.metadataViewId} viewId={this.props.viewId}
currentRepoInfo={this.props.currentRepoInfo} currentRepoInfo={this.props.currentRepoInfo}
lastModified={this.props.lastModified} lastModified={this.props.lastModified}
latestContributor={this.props.latestContributor} latestContributor={this.props.latestContributor}

View File

@@ -2,16 +2,16 @@ import React, { useCallback, useMemo, useState } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { gettext } from '../../utils/constants'; import { gettext } from '../../utils/constants';
import TreeSection from '../tree-section'; import TreeSection from '../tree-section';
import { MetadataStatusManagementDialog, MetadataTreeView, useMetadataStatus } from '../../metadata'; import { MetadataStatusManagementDialog, MetadataTreeView, useMetadata } from '../../metadata';
const DirViews = ({ userPerm, repoID, currentPath, onNodeClick }) => { const DirViews = ({ userPerm, repoID, currentPath }) => {
const enableMetadataManagement = useMemo(() => { const enableMetadataManagement = useMemo(() => {
return window.app.pageOptions.enableMetadataManagement; return window.app.pageOptions.enableMetadataManagement;
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [window.app.pageOptions.enableMetadataManagement]); }, [window.app.pageOptions.enableMetadataManagement]);
const [showMetadataStatusManagementDialog, setShowMetadataStatusManagementDialog] = useState(false); const [showMetadataStatusManagementDialog, setShowMetadataStatusManagementDialog] = useState(false);
const { enableExtendedProperties, updateEnableExtendedProperties } = useMetadataStatus(); const { enableMetadata, updateEnableMetadata, navigation } = useMetadata();
const moreOperations = useMemo(() => { const moreOperations = useMemo(() => {
if (!enableMetadataManagement) return []; if (!enableMetadataManagement) return [];
if (userPerm !== 'rw' && userPerm !== 'admin') return []; if (userPerm !== 'rw' && userPerm !== 'admin') return [];
@@ -32,8 +32,8 @@ const DirViews = ({ userPerm, repoID, currentPath, onNodeClick }) => {
}, []); }, []);
const toggleMetadataStatus = useCallback((value) => { const toggleMetadataStatus = useCallback((value) => {
updateEnableExtendedProperties(value); updateEnableMetadata(value);
}, [updateEnableExtendedProperties]); }, [updateEnableMetadata]);
return ( return (
<> <>
@@ -43,11 +43,13 @@ const DirViews = ({ userPerm, repoID, currentPath, onNodeClick }) => {
moreOperations={moreOperations} moreOperations={moreOperations}
moreOperationClick={moreOperationClick} moreOperationClick={moreOperationClick}
> >
{enableExtendedProperties && (<MetadataTreeView userPerm={userPerm} repoID={repoID} currentPath={currentPath} onNodeClick={onNodeClick} />)} {enableMetadata && Array.isArray(navigation) && navigation.length > 0 && (
<MetadataTreeView userPerm={userPerm} currentPath={currentPath} />
)}
</TreeSection> </TreeSection>
{showMetadataStatusManagementDialog && ( {showMetadataStatusManagementDialog && (
<MetadataStatusManagementDialog <MetadataStatusManagementDialog
value={enableExtendedProperties} value={enableMetadata}
repoID={repoID} repoID={repoID}
toggle={closeMetadataManagementDialog} toggle={closeMetadataManagementDialog}
submit={toggleMetadataStatus} submit={toggleMetadataStatus}

View File

@@ -4,16 +4,16 @@ 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, useMetadataStatus } from '../../../metadata'; import { MetadataDetails, useMetadata } from '../../../metadata';
const DirDetails = ({ repoID, repoInfo, dirent, path, direntDetail, ...params }) => { const DirDetails = ({ repoID, repoInfo, dirent, path, direntDetail, ...params }) => {
const direntPath = useMemo(() => getDirentPath(dirent, path), [dirent, path]); const direntPath = useMemo(() => getDirentPath(dirent, path), [dirent, path]);
const { enableExtendedProperties } = useMetadataStatus(); const { enableMetadata } = useMetadata();
return ( return (
<> <>
<DetailItem field={{ type: CellType.MTIME, name: gettext('Last modified time') }} value={direntDetail.mtime} /> <DetailItem field={{ type: CellType.MTIME, name: gettext('Last modified time') }} value={direntDetail.mtime} />
{window.app.pageOptions.enableMetadataManagement && enableExtendedProperties && ( {window.app.pageOptions.enableMetadataManagement && enableMetadata && (
<MetadataDetails repoID={repoID} filePath={direntPath} direntType="dir" { ...params } /> <MetadataDetails repoID={repoID} filePath={direntPath} direntType="dir" { ...params } />
)} )}
</> </>

View File

@@ -8,12 +8,12 @@ import { gettext } from '../../../utils/constants';
import EditFileTagPopover from '../../popover/edit-filetag-popover'; import EditFileTagPopover from '../../popover/edit-filetag-popover';
import FileTagList from '../../file-tag-list'; import FileTagList from '../../file-tag-list';
import { Utils } from '../../../utils/utils'; import { Utils } from '../../../utils/utils';
import { MetadataDetails, useMetadataStatus } 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, ...params }) => {
const [isEditFileTagShow, setEditFileTagShow] = useState(false); const [isEditFileTagShow, setEditFileTagShow] = useState(false);
const { enableExtendedProperties } = useMetadataStatus(); 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()}`, []);
@@ -36,7 +36,7 @@ const FileDetails = React.memo(({ repoID, repoInfo, dirent, path, direntDetail,
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 field={{ type: CellType.MTIME, name: gettext('Last modified time') }} value={direntDetail.last_modified} />
{!window.app.pageOptions.enableMetadataManagement && enableExtendedProperties && ( {!window.app.pageOptions.enableMetadataManagement && enableMetadata && (
<DetailItem field={{ type: CellType.SINGLE_SELECT, name: gettext('Tags') }} valueId={tagListTitleID} valueClick={onEditFileTagToggle} > <DetailItem field={{ type: CellType.SINGLE_SELECT, name: gettext('Tags') }} valueId={tagListTitleID} valueClick={onEditFileTagToggle} >
{Array.isArray(fileTagList) && fileTagList.length > 0 ? ( {Array.isArray(fileTagList) && fileTagList.length > 0 ? (
<FileTagList fileTagList={fileTagList} /> <FileTagList fileTagList={fileTagList} />

View File

@@ -1 +1 @@
export { MetadataStatusProvider, useMetadataStatus } from './metadata-status'; export { MetadataProvider, useMetadata } from './metadata';

View File

@@ -1,50 +0,0 @@
import React, { useCallback, useContext, useEffect, useMemo, useState } from 'react';
import metadataAPI from '../api';
import { Utils } from '../../utils/utils';
import toaster from '../../components/toast';
const MetadataStatusContext = React.createContext(null);
export const MetadataStatusProvider = ({ repoID, children }) => {
const enableMetadataManagement = useMemo(() => {
return window.app.pageOptions.enableMetadataManagement;
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [window.app.pageOptions.enableMetadataManagement]);
const [enableExtendedProperties, setEnableExtendedProperties] = useState(false);
useEffect(() => {
if (!enableMetadataManagement) {
setEnableExtendedProperties(false);
return;
}
metadataAPI.getMetadataStatus(repoID).then(res => {
setEnableExtendedProperties(res.data.enabled);
}).catch(error => {
const errorMsg = Utils.getErrorMsg(error, true);
toaster.danger(errorMsg);
setEnableExtendedProperties(false);
});
}, [repoID, enableMetadataManagement]);
const updateEnableExtendedProperties = useCallback((newValue) => {
if (newValue === enableExtendedProperties) return;
setEnableExtendedProperties(newValue);
}, [enableExtendedProperties]);
return (
<MetadataStatusContext.Provider value={{ enableExtendedProperties, updateEnableExtendedProperties }}>
{children}
</MetadataStatusContext.Provider>
);
};
export const useMetadataStatus = () => {
const context = useContext(MetadataStatusContext);
if (!context) {
throw new Error('\'MetadataStatusContext\' is null');
}
const { enableExtendedProperties, updateEnableExtendedProperties } = context;
return { enableExtendedProperties, updateEnableExtendedProperties };
};

View File

@@ -0,0 +1,179 @@
import React, { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
import metadataAPI from '../api';
import { Utils } from '../../utils/utils';
import toaster from '../../components/toast';
import { gettext } from '../../utils/constants';
import { PRIVATE_FILE_TYPE } from '../../constants';
// This hook provides content related to seahub interaction, such as whether to enable extended attributes, views data, etc.
const MetadataContext = React.createContext(null);
export const MetadataProvider = ({ repoID, hideMetadataView, selectMetadataView, renameMetadataView, children }) => {
const enableMetadataManagement = useMemo(() => {
return window.app.pageOptions.enableMetadataManagement;
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [window.app.pageOptions.enableMetadataManagement]);
const [enableMetadata, setEnableExtendedProperties] = useState(false);
const [showFirstView, setShowFirstView] = useState(false);
const [navigation, setNavigation] = useState([]);
const viewsMap = useRef({});
useEffect(() => {
if (!enableMetadataManagement) {
setEnableExtendedProperties(false);
return;
}
metadataAPI.getMetadataStatus(repoID).then(res => {
setEnableExtendedProperties(res.data.enabled);
}).catch(error => {
const errorMsg = Utils.getErrorMsg(error, true);
toaster.danger(errorMsg);
setEnableExtendedProperties(false);
});
}, [repoID, enableMetadataManagement]);
const updateEnableMetadata = useCallback((newValue) => {
if (newValue === enableMetadata) return;
if (!newValue) {
hideMetadataView && hideMetadataView();
} else {
setShowFirstView(true);
}
setEnableExtendedProperties(newValue);
}, [enableMetadata, hideMetadataView]);
// views
useEffect(() => {
if (enableMetadata) {
metadataAPI.listViews(repoID).then(res => {
const { navigation, views } = res.data;
if (Array.isArray(views)) {
views.forEach(view => {
viewsMap.current[view._id] = view;
});
}
setNavigation(navigation);
}).catch(error => {
const errorMsg = Utils.getErrorMsg(error);
toaster.danger(errorMsg);
});
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
}, [repoID, enableMetadata]);
const selectView = useCallback((view, isSelected) => {
if (isSelected) return;
const node = {
children: [],
path: '/' + PRIVATE_FILE_TYPE.FILE_EXTENDED_PROPERTIES + '/' + view.name,
isExpanded: false,
isLoaded: true,
isPreload: true,
object: {
file_tags: [],
id: PRIVATE_FILE_TYPE.FILE_EXTENDED_PROPERTIES,
name: gettext('File extended properties'),
type: PRIVATE_FILE_TYPE.FILE_EXTENDED_PROPERTIES,
isDir: () => false,
},
parentNode: {},
key: repoID,
view_id: view._id,
};
selectMetadataView(node);
setShowFirstView(false);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [repoID, selectMetadataView]);
const addView = useCallback((name, successCallback, failCallback) => {
metadataAPI.addView(repoID, name).then(res => {
const view = res.data.view;
let newNavigation = navigation.slice(0);
newNavigation.push({ _id: view._id, type: 'view' });
viewsMap.current[view._id] = view;
setNavigation(newNavigation);
selectView(view);
successCallback && successCallback();
}).catch(error => {
failCallback && failCallback(error);
});
}, [navigation, repoID, viewsMap, selectView]);
const deleteView = useCallback((viewId, isSelected) => {
metadataAPI.deleteView(repoID, viewId).then(res => {
const newNavigation = navigation.filter(item => item._id !== viewId);
delete viewsMap.current[viewId];
setNavigation(newNavigation);
if (isSelected) {
const currentViewIndex = navigation.findIndex(item => item._id === viewId);
const lastViewId = navigation[currentViewIndex - 1]._id;
const lastView = viewsMap.current[lastViewId];
selectView(lastView);
}
}).catch((error => {
const errorMsg = Utils.getErrorMsg(error);
toaster.danger(errorMsg);
}));
}, [repoID, navigation, selectView, viewsMap]);
const updateView = useCallback((viewId, update, successCallback, failCallback) => {
metadataAPI.modifyView(repoID, viewId, update).then(res => {
const currentView = viewsMap.current[viewId];
viewsMap.current[viewId] = { ...currentView, ...update };
if (Object.prototype.hasOwnProperty.call(update, 'name')) {
renameMetadataView(viewId, '/' + PRIVATE_FILE_TYPE.FILE_EXTENDED_PROPERTIES + '/' + update['name']);
}
successCallback && successCallback();
}).catch(error => {
failCallback && failCallback(error);
});
}, [repoID, viewsMap, renameMetadataView]);
const moveView = useCallback((sourceViewId, targetViewId) => {
metadataAPI.moveView(repoID, sourceViewId, targetViewId).then(res => {
const { navigation } = res.data;
setNavigation(navigation);
}).catch(error => {
const errorMsg = Utils.getErrorMsg(error);
toaster.danger(errorMsg);
});
}, [repoID]);
return (
<MetadataContext.Provider value={{
enableMetadata,
updateEnableMetadata,
showFirstView,
navigation,
viewsMap: viewsMap.current,
selectView,
addView,
deleteView,
updateView,
moveView,
}}>
{children}
</MetadataContext.Provider>
);
};
export const useMetadata = () => {
const context = useContext(MetadataContext);
if (!context) {
throw new Error('\'MetadataContext\' is null');
}
return context;
};

View File

@@ -1,65 +1,54 @@
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import React, { useCallback, useEffect, useMemo, useState } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { CustomizeAddTool } from '@seafile/sf-metadata-ui-component';
import { gettext } from '../../utils/constants'; import { gettext } from '../../utils/constants';
import { PRIVATE_FILE_TYPE } from '../../constants'; import { PRIVATE_FILE_TYPE } from '../../constants';
import metadataAPI from '../api';
import { Utils } from '../../utils/utils';
import toaster from '../../components/toast';
import ViewItem from './view-item'; import ViewItem from './view-item';
import NameDialog from './name-dialog'; import NameDialog from './name-dialog';
import { useMetadata } from '../hooks';
import './index.css'; import './index.css';
import { CustomizeAddTool } from '@seafile/sf-metadata-ui-component';
const MetadataTreeView = ({ userPerm, repoID, currentPath, onNodeClick }) => { const MetadataTreeView = ({ userPerm, currentPath }) => {
const canAdd = useMemo(() => { const canAdd = useMemo(() => {
if (userPerm !== 'rw' && userPerm !== 'admin') return false; if (userPerm !== 'rw' && userPerm !== 'admin') return false;
return true; return true;
}, [userPerm]); }, [userPerm]);
const [views, setViews] = useState([]);
const [showAddViewDialog, setSowAddViewDialog] = useState(false); const [showAddViewDialog, setSowAddViewDialog] = useState(false);
const [, setState] = useState(0); const [, setState] = useState(0);
const viewsMap = useRef({}); const {
showFirstView,
navigation,
viewsMap,
selectView,
addView,
deleteView,
updateView,
moveView
} = useMetadata();
useEffect(() => { useEffect(() => {
metadataAPI.listViews(repoID).then(res => { const { origin, pathname, search } = window.location;
const { navigation, views } = res.data; const urlParams = new URLSearchParams(search);
if (Array.isArray(views)) { const viewID = urlParams.get('view');
views.forEach(view => { if (viewID) {
viewsMap.current[view._id] = view; const lastOpenedView = viewsMap[viewID] || '';
}); if (lastOpenedView) {
selectView(lastOpenedView);
return;
}
const url = `${origin}${pathname}`;
window.history.pushState({ url: url, path: '' }, '', url);
}
const firstViewObject = navigation.find(item => item.type === 'view');
const firstView = firstViewObject ? viewsMap[firstViewObject._id] : '';
if (showFirstView && firstView) {
selectView(firstView);
} }
setViews(navigation);
}).catch(error => {
const errorMsg = Utils.getErrorMsg(error);
toaster.danger(errorMsg);
});
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, []); }, []);
const onClick = useCallback((view, isSelected) => {
if (isSelected) return;
const node = {
children: [],
path: '/' + PRIVATE_FILE_TYPE.FILE_EXTENDED_PROPERTIES + '/' + view.name,
isExpanded: false,
isLoaded: true,
isPreload: true,
object: {
file_tags: [],
id: PRIVATE_FILE_TYPE.FILE_EXTENDED_PROPERTIES,
name: gettext('File extended properties'),
type: PRIVATE_FILE_TYPE.FILE_EXTENDED_PROPERTIES,
isDir: () => false,
},
parentNode: {},
key: repoID,
view_id: view._id,
view_name: view.name,
};
onNodeClick(node);
}, [repoID, onNodeClick]);
const openAddView = useCallback(() => { const openAddView = useCallback(() => {
setSowAddViewDialog(true); setSowAddViewDialog(true);
}, []); }, []);
@@ -68,66 +57,20 @@ const MetadataTreeView = ({ userPerm, repoID, currentPath, onNodeClick }) => {
setSowAddViewDialog(false); setSowAddViewDialog(false);
}, []); }, []);
const addView = useCallback((name, failCallback) => {
metadataAPI.addView(repoID, name).then(res => {
const view = res.data.view;
let newViews = views.slice(0);
newViews.push({ _id: view._id, type: 'view' });
viewsMap.current[view._id] = view;
setSowAddViewDialog(false);
setViews(newViews);
onClick(view);
}).catch(error => {
failCallback && failCallback(error);
});
}, [views, repoID, viewsMap, onClick]);
const onDeleteView = useCallback((viewId, isSelected) => {
metadataAPI.deleteView(repoID, viewId).then(res => {
const newViews = views.filter(item => item._id !== viewId);
delete viewsMap.current[viewId];
setViews(newViews);
if (isSelected) {
const currentViewIndex = views.findIndex(item => item._id === viewId);
const lastViewId = views[currentViewIndex - 1]._id;
const lastView = viewsMap.current[lastViewId];
onClick(lastView);
}
}).catch((error => {
const errorMsg = Utils.getErrorMsg(error);
toaster.danger(errorMsg);
}));
}, [repoID, views, onClick, viewsMap]);
const onUpdateView = useCallback((viewId, update, successCallback, failCallback) => { const onUpdateView = useCallback((viewId, update, successCallback, failCallback) => {
metadataAPI.modifyView(repoID, viewId, update).then(res => { updateView(viewId, update, () => {
successCallback && successCallback();
const currentView = viewsMap.current[viewId];
viewsMap.current[viewId] = { ...currentView, ...update };
setState(n => n + 1); setState(n => n + 1);
}).catch(error => { successCallback && successCallback();
failCallback && failCallback(error); }, failCallback);
}); }, [updateView]);
}, [repoID, viewsMap]);
const onMoveView = useCallback((sourceViewId, targetViewId) => {
metadataAPI.moveView(repoID, sourceViewId, targetViewId).then(res => {
const { navigation } = res.data;
setViews(navigation);
}).catch(error => {
const errorMsg = Utils.getErrorMsg(error);
toaster.danger(errorMsg);
});
}, [repoID]);
return ( return (
<> <>
<div className="tree-view tree metadata-tree-view"> <div className="tree-view tree metadata-tree-view">
<div className="tree-node"> <div className="tree-node">
<div className="children"> <div className="children">
{views.map((item, index) => { {navigation.map((item, index) => {
if (item.type !== 'view') return null; const view = viewsMap[item._id];
const view = viewsMap.current[item._id];
const viewPath = '/' + PRIVATE_FILE_TYPE.FILE_EXTENDED_PROPERTIES + '/' + view.name; const viewPath = '/' + PRIVATE_FILE_TYPE.FILE_EXTENDED_PROPERTIES + '/' + view.name;
const isSelected = currentPath === viewPath; const isSelected = currentPath === viewPath;
return ( return (
@@ -137,10 +80,10 @@ const MetadataTreeView = ({ userPerm, repoID, currentPath, onNodeClick }) => {
isSelected={isSelected} isSelected={isSelected}
userPerm={userPerm} userPerm={userPerm}
view={view} view={view}
onClick={(view) => onClick(view, isSelected)} onClick={(view) => selectView(view, isSelected)}
onDelete={() => onDeleteView(view._id, isSelected)} onDelete={() => deleteView(view._id, isSelected)}
onUpdate={(update, successCallback, failCallback) => onUpdateView(view._id, update, successCallback, failCallback)} onUpdate={(update, successCallback, failCallback) => onUpdateView(view._id, update, successCallback, failCallback)}
onMove={onMoveView} onMove={moveView}
/>); />);
})} })}
{canAdd && {canAdd &&
@@ -161,9 +104,7 @@ const MetadataTreeView = ({ userPerm, repoID, currentPath, onNodeClick }) => {
MetadataTreeView.propTypes = { MetadataTreeView.propTypes = {
userPerm: PropTypes.string, userPerm: PropTypes.string,
repoID: PropTypes.string.isRequired,
currentPath: PropTypes.string, currentPath: PropTypes.string,
onNodeClick: PropTypes.func,
}; };
export default MetadataTreeView; export default MetadataTreeView;

View File

@@ -45,7 +45,9 @@ const NameDialog = ({ value: oldName, title, onSubmit, onToggle }) => {
onToggle(); onToggle();
return; return;
} }
onSubmit(message, (error) => { onSubmit(message, () => {
onToggle();
}, (error) => {
const errorMsg = Utils.getErrorMsg(error); const errorMsg = Utils.getErrorMsg(error);
setErrorMessage(errorMsg); setErrorMessage(errorMsg);
setSubmitting(false); setSubmitting(false);

View File

@@ -92,7 +92,7 @@ const ViewItem = ({
if (!canDrop) return false; if (!canDrop) return false;
const dragData = JSON.stringify({ type: 'sf-metadata-view', view_id: view._id }); const dragData = JSON.stringify({ type: 'sf-metadata-view', view_id: view._id });
event.dataTransfer.effectAllowed = 'move'; event.dataTransfer.effectAllowed = 'move';
event.dataTransfer.setData('application/drag-sf-metadata-view-info', dragData); event.dataTransfer.setData('application/drag-sf-metadata-view', dragData);
}, [canDrop, view]); }, [canDrop, view]);
const onDragEnter = useCallback((event) => { const onDragEnter = useCallback((event) => {
@@ -105,8 +105,10 @@ const ViewItem = ({
setDropShow(false); setDropShow(false);
}, [canDrop]); }, [canDrop]);
const onDragMove = useCallback(() => { const onDragMove = useCallback((event) => {
if (!canDrop) return false; if (!canDrop) return false;
event.preventDefault();
event.dataTransfer.dropEffect = 'move';
}, [canDrop]); }, [canDrop]);
const onDrop = useCallback((event) => { const onDrop = useCallback((event) => {
@@ -114,7 +116,7 @@ const ViewItem = ({
event.stopPropagation(); event.stopPropagation();
setDropShow(false); setDropShow(false);
let dragData = event.dataTransfer.getData('application/drag-sf-metadata-view-info'); let dragData = event.dataTransfer.getData('application/drag-sf-metadata-view');
if (!dragData) return; if (!dragData) return;
dragData = JSON.parse(dragData); dragData = JSON.parse(dragData);
if (dragData.type !== 'sf-metadata-view') return false; if (dragData.type !== 'sf-metadata-view') return false;

View File

@@ -5,7 +5,7 @@ import { EVENT_BUS_TYPE } from '../../constants';
import './index.css'; import './index.css';
const ViewToolBar = ({ metadataViewId }) => { const ViewToolBar = ({ viewId }) => {
const [view, setView] = useState(null); const [view, setView] = useState(null);
const [collaborators, setCollaborators] = useState([]); const [collaborators, setCollaborators] = useState([]);
@@ -59,7 +59,7 @@ const ViewToolBar = ({ metadataViewId }) => {
unsubscribeViewChange && unsubscribeViewChange(); unsubscribeViewChange && unsubscribeViewChange();
}; };
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [metadataViewId]); }, [viewId]);
if (!view) return null; if (!view) return null;
@@ -116,7 +116,7 @@ const ViewToolBar = ({ metadataViewId }) => {
}; };
ViewToolBar.propTypes = { ViewToolBar.propTypes = {
metadataViewId: PropTypes.string, viewId: PropTypes.string,
}; };
export default ViewToolBar; export default ViewToolBar;

View File

@@ -6,7 +6,6 @@ import CurDirPath from '../../components/cur-dir-path';
import Detail from '../../components/dirent-detail'; import Detail from '../../components/dirent-detail';
import DirColumnView from '../../components/dir-view-mode/dir-column-view'; import DirColumnView from '../../components/dir-view-mode/dir-column-view';
import ToolbarForSelectedDirents from '../../components/toolbar/selected-dirents-toolbar'; import ToolbarForSelectedDirents from '../../components/toolbar/selected-dirents-toolbar';
import { MetadataStatusProvider } from '../../metadata/hooks';
import '../../css/lib-content-view.css'; import '../../css/lib-content-view.css';
@@ -40,7 +39,7 @@ const propTypes = {
isFileLoading: PropTypes.bool.isRequired, isFileLoading: PropTypes.bool.isRequired,
filePermission: PropTypes.string, filePermission: PropTypes.string,
content: PropTypes.string, content: PropTypes.string,
metadataViewId: PropTypes.string, viewId: PropTypes.string,
lastModified: PropTypes.string, lastModified: PropTypes.string,
latestContributor: PropTypes.string, latestContributor: PropTypes.string,
onLinkClick: PropTypes.func.isRequired, onLinkClick: PropTypes.func.isRequired,
@@ -183,7 +182,6 @@ class LibContentContainer extends React.Component {
} }
return ( return (
<MetadataStatusProvider repoID={repoID}>
<div className="cur-view-container"> <div className="cur-view-container">
{this.props.currentRepoInfo.status === 'read-only' && {this.props.currentRepoInfo.status === 'read-only' &&
<div className="readonly-tip-message"> <div className="readonly-tip-message">
@@ -224,7 +222,7 @@ class LibContentContainer extends React.Component {
filePermission={this.props.filePermission} filePermission={this.props.filePermission}
onFileTagChanged={this.props.onToolbarFileTagChanged} onFileTagChanged={this.props.onToolbarFileTagChanged}
repoTags={this.props.repoTags} repoTags={this.props.repoTags}
metadataViewId={this.props.metadataViewId} viewId={this.props.viewId}
onItemMove={this.props.onItemMove} onItemMove={this.props.onItemMove}
/> />
<ToolbarForSelectedDirents <ToolbarForSelectedDirents
@@ -284,7 +282,7 @@ class LibContentContainer extends React.Component {
hash={this.props.hash} hash={this.props.hash}
filePermission={this.props.filePermission} filePermission={this.props.filePermission}
content={this.props.content} content={this.props.content}
metadataViewId={this.props.metadataViewId} viewId={this.props.viewId}
lastModified={this.props.lastModified} lastModified={this.props.lastModified}
latestContributor={this.props.latestContributor} latestContributor={this.props.latestContributor}
onLinkClick={this.props.onLinkClick} onLinkClick={this.props.onLinkClick}
@@ -336,7 +334,6 @@ class LibContentContainer extends React.Component {
)} )}
</div> </div>
</div> </div>
</MetadataStatusProvider>
); );
} }
} }

View File

@@ -24,6 +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';
const propTypes = { const propTypes = {
eventBus: PropTypes.object, eventBus: PropTypes.object,
@@ -89,7 +90,7 @@ class LibContentView extends React.Component {
asyncOperationType: 'move', asyncOperationType: 'move',
asyncOperationProgress: 0, asyncOperationProgress: 0,
asyncOperatedFilesLength: 0, asyncOperatedFilesLength: 0,
metadataViewId: '0000', viewId: '0000',
}; };
this.oldonpopstate = window.onpopstate; this.oldonpopstate = window.onpopstate;
@@ -148,12 +149,7 @@ class LibContentView extends React.Component {
const { repoID, eventBus } = props; const { repoID, eventBus } = props;
this.unsubscribeEvent = eventBus.subscribe(EVENT_BUS_TYPE.SEARCH_LIBRARY_CONTENT, this.onSearchedClick); this.unsubscribeEvent = eventBus.subscribe(EVENT_BUS_TYPE.SEARCH_LIBRARY_CONTENT, this.onSearchedClick);
const urlParams = new URLSearchParams(window.location.search); const path = this.getPathFromLocation(repoID);
const viewID = urlParams.get('view');
const viewName = JSON.parse(localStorage.getItem('last_visited_view'))?.viewName;
const path = viewID
? `/${PRIVATE_FILE_TYPE.FILE_EXTENDED_PROPERTIES}/${viewName}`
: this.getPathFromLocation(repoID);
try { try {
const repoInfo = await this.fetchRepoInfo(repoID); const repoInfo = await this.fetchRepoInfo(repoID);
@@ -410,19 +406,11 @@ class LibContentView extends React.Component {
this.loadSidePanel(path); this.loadSidePanel(path);
} }
if (path.includes(PRIVATE_FILE_TYPE.FILE_EXTENDED_PROPERTIES)) { if (!path.includes(PRIVATE_FILE_TYPE.FILE_EXTENDED_PROPERTIES)) {
this.handleFileExtendedProperties(path);
} else {
this.showDir(path); this.showDir(path);
} }
}; };
handleFileExtendedProperties = (path) => {
const lastVisitedView = localStorage.getItem('last_visited_view');
const { viewID, viewName } = lastVisitedView ? JSON.parse(lastVisitedView) : {};
this.showFileMetadata(path, viewID, viewName);
};
loadSidePanel = (path) => { loadSidePanel = (path) => {
let repoID = this.props.repoID; let repoID = this.props.repoID;
if (path === '/' || path.includes(PRIVATE_FILE_TYPE.FILE_EXTENDED_PROPERTIES)) { if (path === '/' || path.includes(PRIVATE_FILE_TYPE.FILE_EXTENDED_PROPERTIES)) {
@@ -523,7 +511,7 @@ class LibContentView extends React.Component {
window.history.pushState({ url: url, path: filePath }, filePath, url); window.history.pushState({ url: url, path: filePath }, filePath, url);
}; };
showFileMetadata = (filePath, viewId, viewName) => { showFileMetadata = (filePath, viewId) => {
const repoID = this.props.repoID; const repoID = this.props.repoID;
const repoInfo = this.state.currentRepoInfo; const repoInfo = this.state.currentRepoInfo;
@@ -533,16 +521,33 @@ class LibContentView extends React.Component {
isFileLoading: false, isFileLoading: false,
isFileLoadedErr: false, isFileLoadedErr: false,
content: '__sf-metadata', content: '__sf-metadata',
metadataViewId: viewId, viewId: viewId,
isDirentDetailShow: false isDirentDetailShow: false
}); });
const url = `${siteRoot}library/${repoID}/${encodeURIComponent(repoInfo.repo_name)}?view=${encodeURIComponent(viewId)}`; const url = `${siteRoot}library/${repoID}/${encodeURIComponent(repoInfo.repo_name)}?view=${encodeURIComponent(viewId)}`;
localStorage.setItem('last_visited_view', JSON.stringify({ viewID: viewId, viewName: viewName }));
window.history.pushState({ url: url, path: '' }, '', url); window.history.pushState({ url: url, path: '' }, '', url);
}; };
hideFileMetadata = () => {
this.setState({
path: '',
isViewFile: false,
isFileLoading: false,
isFileLoadedErr: false,
content: '',
viewId: '',
isDirentDetailShow: false
});
};
renameMetadataView = (renamedViewId, newPath) => {
const { viewId, content } = this.state;
if (content !== '__sf-metadata') return;
if (viewId !== renamedViewId) return;
this.setState({ path: newPath });
};
loadDirentList = (path) => { loadDirentList = (path) => {
let repoID = this.props.repoID; let repoID = this.props.repoID;
seafileAPI.listDir(repoID, path, { 'with_thumbnail': true }).then(res => { seafileAPI.listDir(repoID, path, { 'with_thumbnail': true }).then(res => {
@@ -1827,7 +1832,7 @@ class LibContentView extends React.Component {
} }
} else if (Utils.isFileMetadata(node?.object?.type)) { } else if (Utils.isFileMetadata(node?.object?.type)) {
if (node.path !== this.state.path) { if (node.path !== this.state.path) {
this.showFileMetadata(node.path, node.view_id || '0000', node.view_name); this.showFileMetadata(node.path, node.view_id || '0000');
} }
} else { } else {
let url = siteRoot + 'lib/' + repoID + '/file' + Utils.encodePath(node.path); let url = siteRoot + 'lib/' + repoID + '/file' + Utils.encodePath(node.path);
@@ -2163,7 +2168,12 @@ class LibContentView extends React.Component {
} }
return ( return (
<Fragment> <MetadataProvider
repoID={this.props.repoID}
selectMetadataView={this.onTreeNodeClick}
renameMetadataView={this.renameMetadataView}
hideMetadataView={this.hideFileMetadata}
>
<div className="main-panel-center flex-row"> <div className="main-panel-center flex-row">
<LibContentContainer <LibContentContainer
isSidePanelFolded={this.props.isSidePanelFolded} isSidePanelFolded={this.props.isSidePanelFolded}
@@ -2193,7 +2203,7 @@ class LibContentView extends React.Component {
isFileLoadedErr={this.state.isFileLoadedErr} isFileLoadedErr={this.state.isFileLoadedErr}
filePermission={this.state.filePermission} filePermission={this.state.filePermission}
content={this.state.content} content={this.state.content}
metadataViewId={this.state.metadataViewId} viewId={this.state.viewId}
lastModified={this.state.lastModified} lastModified={this.state.lastModified}
latestContributor={this.state.latestContributor} latestContributor={this.state.latestContributor}
onLinkClick={this.onLinkClick} onLinkClick={this.onLinkClick}
@@ -2281,7 +2291,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>
</Fragment> </MetadataProvider>
); );
} }
} }