mirror of
https://github.com/haiwen/seahub.git
synced 2025-09-10 19:29:56 +00:00
feat: deleted repos ui (#7241)
* feat: deleted repos ui * feat: optimize code --------- Co-authored-by: 杨国璇 <ygx@Hello-word.local>
This commit is contained in:
@@ -1,11 +1,15 @@
|
|||||||
.my-deleted-repos-dialog {
|
.my-deleted-repos-dialog {
|
||||||
max-width: 720px;
|
max-width: 720px;
|
||||||
|
height: calc(100% - 56px);
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.my-deleted-repos-container {
|
.my-deleted-repos-dialog .modal-content {
|
||||||
min-height: 200px;
|
height: 100%;
|
||||||
max-height: 500px;
|
}
|
||||||
overflow: auto;
|
|
||||||
|
.my-deleted-repos-dialog .my-deleted-repos-container {
|
||||||
|
overflow-y: scroll;
|
||||||
}
|
}
|
||||||
|
|
||||||
.my-deleted-repos-empty-tip {
|
.my-deleted-repos-empty-tip {
|
||||||
@@ -25,5 +29,5 @@
|
|||||||
|
|
||||||
.my-deleted-repos-tip {
|
.my-deleted-repos-tip {
|
||||||
font-size: .8125rem;
|
font-size: .8125rem;
|
||||||
color: #9c9c9c;
|
color: #666;
|
||||||
}
|
}
|
@@ -0,0 +1,65 @@
|
|||||||
|
import React, { useCallback, useEffect, useState } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { Modal, ModalHeader, ModalBody } from 'reactstrap';
|
||||||
|
import { gettext, trashReposExpireDays } from '../../../utils/constants';
|
||||||
|
import { seafileAPI } from '../../../utils/seafile-api';
|
||||||
|
import { Utils } from '../../../utils/utils';
|
||||||
|
import toaster from '../../toast';
|
||||||
|
import Loading from '../../loading';
|
||||||
|
import EmptyTip from '../../empty-tip';
|
||||||
|
import Repos from './repos';
|
||||||
|
|
||||||
|
import './index.css';
|
||||||
|
|
||||||
|
const MyDeletedReposDialog = ({ toggleDialog }) => {
|
||||||
|
|
||||||
|
const [isLoading, setLoading] = useState(true);
|
||||||
|
const [deletedRepoList, setDeletedRepoList] = useState([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
seafileAPI.listDeletedRepo().then(res => {
|
||||||
|
setDeletedRepoList(res.data);
|
||||||
|
setLoading(false);
|
||||||
|
}).catch(error => {
|
||||||
|
const errMessage = Utils.getErrorMsg(error);
|
||||||
|
toaster.danger(errMessage);
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const filterRestoredRepo = useCallback((restoredRepoID) => {
|
||||||
|
const newDeletedRepoList = deletedRepoList.filter(item => {
|
||||||
|
return item.repo_id !== restoredRepoID;
|
||||||
|
});
|
||||||
|
setDeletedRepoList(newDeletedRepoList);
|
||||||
|
}, [deletedRepoList]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal isOpen={true} toggle={toggleDialog} className="my-deleted-repos-dialog">
|
||||||
|
<ModalHeader toggle={toggleDialog}>{gettext('Deleted Libraries')}</ModalHeader>
|
||||||
|
<ModalBody className="my-deleted-repos-container">
|
||||||
|
{isLoading ? (
|
||||||
|
<Loading />
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{deletedRepoList.length === 0 ? (
|
||||||
|
<EmptyTip
|
||||||
|
className="my-deleted-repos-empty-tip"
|
||||||
|
title={gettext('No deleted libraries')}
|
||||||
|
text={gettext('You have not deleted any libraries in the last {placeholder} days. A deleted library will be cleaned automatically after this period.').replace('{placeholder}', trashReposExpireDays)}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Repos repos={deletedRepoList} filterRestoredRepo={filterRestoredRepo} />
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</ModalBody>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
MyDeletedReposDialog.propTypes = {
|
||||||
|
toggleDialog: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MyDeletedReposDialog;
|
@@ -0,0 +1,71 @@
|
|||||||
|
import React, { useCallback, useMemo, useState } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
import relativeTime from 'dayjs/plugin/relativeTime';
|
||||||
|
import { seafileAPI } from '../../../utils/seafile-api';
|
||||||
|
import { gettext, lang } from '../../../utils/constants';
|
||||||
|
import toaster from '../../toast';
|
||||||
|
import { Utils } from '../../../utils/utils';
|
||||||
|
|
||||||
|
dayjs.locale(lang);
|
||||||
|
dayjs.extend(relativeTime);
|
||||||
|
|
||||||
|
const RepoItem = ({ repo, filterRestoredRepo }) => {
|
||||||
|
const repoID = useMemo(() => repo.repo_id, [repo]);
|
||||||
|
const repoName = useMemo(() => repo.repo_name, [repo]);
|
||||||
|
const localTime = useMemo(() => {
|
||||||
|
const timeDate = dayjs.utc(repo.del_time).toDate();
|
||||||
|
return dayjs(timeDate).fromNow();
|
||||||
|
}, [repo]);
|
||||||
|
const iconUrl = useMemo(() => Utils.getLibIconUrl(repo), [repo]);
|
||||||
|
|
||||||
|
const [highlight, setHighlight] = useState(false);
|
||||||
|
|
||||||
|
const onMouseEnter = useCallback(() => {
|
||||||
|
setHighlight(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const onMouseLeave = useCallback(() => {
|
||||||
|
setHighlight(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const restoreDeletedRepo = useCallback((event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
seafileAPI.restoreDeletedRepo(repoID).then(res => {
|
||||||
|
const message = gettext('Successfully restored the library {library_name}.').replace('{library_name}', repoName);
|
||||||
|
toaster.success(message);
|
||||||
|
filterRestoredRepo(repoID);
|
||||||
|
}).catch(error => {
|
||||||
|
const errMessage = Utils.getErrorMsg(error);
|
||||||
|
toaster.danger(errMessage);
|
||||||
|
});
|
||||||
|
}, [repoID, repoName, filterRestoredRepo]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<tr
|
||||||
|
className={highlight ? 'tr-highlight' : ''}
|
||||||
|
onMouseEnter={onMouseEnter}
|
||||||
|
onMouseLeave={onMouseLeave}
|
||||||
|
tabIndex="0"
|
||||||
|
onFocus={onMouseEnter}
|
||||||
|
>
|
||||||
|
<td className="text-center pl-2 pr-2"><img src={iconUrl} alt='' width="24" /></td>
|
||||||
|
<td className="name">{repoName}</td>
|
||||||
|
<td className="update">{localTime}</td>
|
||||||
|
<td>
|
||||||
|
<a href="#" onClick={restoreDeletedRepo} title={gettext('Restore')}
|
||||||
|
role="button" aria-label={gettext('Restore')}
|
||||||
|
className={`sf2-icon-reply action-icon ${highlight ? '' : 'vh'}`}>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
RepoItem.propTypes = {
|
||||||
|
repo: PropTypes.object.isRequired,
|
||||||
|
filterRestoredRepo: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default RepoItem;
|
@@ -0,0 +1,54 @@
|
|||||||
|
import React, { useState, useEffect, useRef } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import RepoItem from './repo-item';
|
||||||
|
import { gettext, trashReposExpireDays } from '../../../utils/constants';
|
||||||
|
|
||||||
|
const Repos = ({ repos, filterRestoredRepo }) => {
|
||||||
|
const [containerWidth, setContainerWidth] = useState(0);
|
||||||
|
|
||||||
|
const containerRef = useRef(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const container = containerRef.current;
|
||||||
|
const handleResize = () => {
|
||||||
|
if (!container) return;
|
||||||
|
setContainerWidth(container.offsetWidth);
|
||||||
|
};
|
||||||
|
const resizeObserver = new ResizeObserver(handleResize);
|
||||||
|
container && resizeObserver.observe(container);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
container && resizeObserver.unobserve(container);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={containerRef}>
|
||||||
|
<p className="tip my-deleted-repos-tip">{gettext('Tip: libraries deleted {placeholder} days ago will be cleaned automatically.').replace('{placeholder}', trashReposExpireDays)}</p>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th style={{ width: 40 }} className="pl-2 pr-2">{/* img*/}</th>
|
||||||
|
<th style={{ width: (containerWidth - 40) * 0.5 }}>{gettext('Name')}</th>
|
||||||
|
<th style={{ width: (containerWidth - 40) * 0.3 }}>{gettext('Deleted Time')}</th>
|
||||||
|
<th style={{ width: (containerWidth - 40) * 0.2 }}></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{repos.map((repo) => {
|
||||||
|
return (
|
||||||
|
<RepoItem key={repo.repo_id} repo={repo} filterRestoredRepo={filterRestoredRepo} />
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
Repos.propTypes = {
|
||||||
|
repos: PropTypes.array,
|
||||||
|
filterRestoredRepo: PropTypes.func,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Repos;
|
@@ -1,168 +0,0 @@
|
|||||||
import React, { Component } from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { Modal, ModalHeader, ModalBody } from 'reactstrap';
|
|
||||||
import dayjs from 'dayjs';
|
|
||||||
import relativeTime from 'dayjs/plugin/relativeTime';
|
|
||||||
import { gettext, lang, trashReposExpireDays } from '../../utils/constants';
|
|
||||||
import { seafileAPI } from '../../utils/seafile-api';
|
|
||||||
import { Utils } from '../../utils/utils';
|
|
||||||
import toaster from '../../components/toast';
|
|
||||||
import Loading from '../../components/loading';
|
|
||||||
import EmptyTip from '../../components/empty-tip';
|
|
||||||
|
|
||||||
import '../../css/my-deleted-repos.css';
|
|
||||||
|
|
||||||
dayjs.locale(lang);
|
|
||||||
dayjs.extend(relativeTime);
|
|
||||||
|
|
||||||
class MyLibsDeleted extends Component {
|
|
||||||
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
this.state = {
|
|
||||||
deletedRepoList: [],
|
|
||||||
isLoading: true,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
seafileAPI.listDeletedRepo().then(res => {
|
|
||||||
this.setState({
|
|
||||||
deletedRepoList: res.data,
|
|
||||||
isLoading: false,
|
|
||||||
});
|
|
||||||
}).catch(error => {
|
|
||||||
let errMessage = Utils.getErrorMsg(error);
|
|
||||||
toaster.danger(errMessage);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
refreshDeletedRepoList = (repoID) => {
|
|
||||||
let deletedRepoList = this.state.deletedRepoList.filter(item => {
|
|
||||||
return item.repo_id !== repoID;
|
|
||||||
});
|
|
||||||
this.setState({ deletedRepoList: deletedRepoList });
|
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { deletedRepoList: repos } = this.state;
|
|
||||||
return (
|
|
||||||
<Modal isOpen={true} toggle={this.props.toggleDialog} className="my-deleted-repos-dialog">
|
|
||||||
<ModalHeader toggle={this.props.toggleDialog}>
|
|
||||||
{gettext('Deleted Libraries')}
|
|
||||||
</ModalHeader>
|
|
||||||
<ModalBody className="my-deleted-repos-container">
|
|
||||||
{this.state.isLoading && <Loading />}
|
|
||||||
{(!this.state.isLoading && repos.length === 0) &&
|
|
||||||
<EmptyTip
|
|
||||||
className="my-deleted-repos-empty-tip"
|
|
||||||
title={gettext('No deleted libraries')}
|
|
||||||
text={gettext('You have not deleted any libraries in the last {placeholder} days. A deleted library will be cleaned automatically after this period.').replace('{placeholder}', trashReposExpireDays)}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
{repos.length !== 0 &&
|
|
||||||
<div>
|
|
||||||
<p className="tip my-deleted-repos-tip">{gettext('Tip: libraries deleted {placeholder} days ago will be cleaned automatically.').replace('{placeholder}', trashReposExpireDays)}</p>
|
|
||||||
<table>
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th width="10%">{/* img*/}</th>
|
|
||||||
<th width="50%">{gettext('Name')}</th>
|
|
||||||
<th width="28%">{gettext('Deleted Time')}</th>
|
|
||||||
<th width="12%"></th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{repos.map((item) => {
|
|
||||||
return (
|
|
||||||
<DeletedRepoItem
|
|
||||||
key={item.repo_id}
|
|
||||||
repo={item}
|
|
||||||
refreshDeletedRepoList={this.refreshDeletedRepoList}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</ModalBody>
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class DeletedRepoItem extends Component {
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
this.state = {
|
|
||||||
hideRestoreMenu: true,
|
|
||||||
highlight: false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
onMouseEnter = () => {
|
|
||||||
this.setState({
|
|
||||||
hideRestoreMenu: false,
|
|
||||||
highlight: true,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
onMouseLeave = () => {
|
|
||||||
this.setState({
|
|
||||||
hideRestoreMenu: true,
|
|
||||||
highlight: false,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
restoreDeletedRepo = (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
let repoID = this.props.repo.repo_id;
|
|
||||||
let repoName = this.props.repo.repo_name;
|
|
||||||
seafileAPI.restoreDeletedRepo(repoID).then(res => {
|
|
||||||
let message = gettext('Successfully restored the library {library_name}.').replace('{library_name}', repoName);
|
|
||||||
toaster.success(message);
|
|
||||||
this.props.refreshDeletedRepoList(repoID);
|
|
||||||
}).catch(error => {
|
|
||||||
const errMessage = Utils.getErrorMsg(error);
|
|
||||||
toaster.danger(errMessage);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
|
||||||
let localTime = dayjs.utc(this.props.repo.del_time).toDate();
|
|
||||||
localTime = dayjs(localTime).fromNow();
|
|
||||||
let iconUrl = Utils.getLibIconUrl(this.props.repo);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<tr
|
|
||||||
className={this.state.highlight ? 'tr-highlight' : ''}
|
|
||||||
onMouseEnter={this.onMouseEnter}
|
|
||||||
onMouseLeave={this.onMouseLeave}
|
|
||||||
tabIndex="0"
|
|
||||||
onFocus={this.onMouseEnter}
|
|
||||||
>
|
|
||||||
<td className="text-center"><img src={iconUrl} alt='' width="24" /></td>
|
|
||||||
<td className="name">{this.props.repo.repo_name}</td>
|
|
||||||
<td className="update">{localTime}</td>
|
|
||||||
<td>
|
|
||||||
<a href="#" onClick={this.restoreDeletedRepo} title={gettext('Restore')}
|
|
||||||
role="button" aria-label={gettext('Restore')}
|
|
||||||
className={`sf2-icon-reply action-icon ${this.state.highlight ? '' : 'vh'}`}>
|
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DeletedRepoItem.propTypes = {
|
|
||||||
repo: PropTypes.object.isRequired,
|
|
||||||
refreshDeletedRepoList: PropTypes.func.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
MyLibsDeleted.propTypes = {
|
|
||||||
toggleDialog: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default MyLibsDeleted;
|
|
@@ -14,7 +14,7 @@ import ReposSortMenu from '../../components/sort-menu';
|
|||||||
import SingleDropdownToolbar from '../../components/toolbar/single-dropdown-toolbar';
|
import SingleDropdownToolbar from '../../components/toolbar/single-dropdown-toolbar';
|
||||||
import SortOptionsDialog from '../../components/dialog/sort-options';
|
import SortOptionsDialog from '../../components/dialog/sort-options';
|
||||||
import CreateRepoDialog from '../../components/dialog/create-repo-dialog';
|
import CreateRepoDialog from '../../components/dialog/create-repo-dialog';
|
||||||
import DeletedReposDialog from '../../components/dialog/my-deleted-repos';
|
import DeletedReposDialog from '../../components/dialog/my-deleted-repos-dialog';
|
||||||
import { LIST_MODE } from '../../components/dir-view-mode/constants';
|
import { LIST_MODE } from '../../components/dir-view-mode/constants';
|
||||||
import MylibRepoListView from './mylib-repo-list-view';
|
import MylibRepoListView from './mylib-repo-list-view';
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user