1
0
mirror of https://github.com/haiwen/seahub.git synced 2025-09-25 14:50:29 +00:00

Shared from other servers (#8099)

* [Shared from other servers('shared-with-ocm/')] added nav items for it in the side nav panel; added content for it in the 'Files' page; a new version of its independent page)

* [Shared from other servers] side panel: highlight the current nav item for the page

* [Shared from other servers('shared-with-ocm/')] added 'sort' & 'star/unstar'

* [Shared from other servers('shared-with-ocm/')] removed 'star/unstar'

* [Shared from other servers('shared-with-ocm/')] update for 'sort' and the side panel

* [Shared from other servers('shared-with-ocm/')] dir view: modified the text for the root element of the path

* [Shared from other servers('shared-with-ocm/')] dir view: fixed the icons for the file items

* [Shared from other servers('shared-with-ocm/')] dir view: improved 'download' & 'hover', removed the code for 'delete'(not supported)

* [Shared from other servers('shared-with-ocm/')] dir view: added 'upload'(as a dropdown menu); redesigned the path bar

* [Shared from other servers('/ocm-via-webdav/')] redesigned it

* [Shared from other servers('shared-with-ocm/')] update

* [Shared from other servers('shared-with-ocm/')] i18n: changed some text to the existing text as required
This commit is contained in:
llj
2025-08-04 16:28:37 +08:00
committed by GitHub
parent aafe8120ad
commit 32f0a29fca
11 changed files with 618 additions and 143 deletions

View File

@@ -2,7 +2,7 @@ import React from 'react';
import PropTypes from 'prop-types';
import { Link } from '@gatsbyjs/reach-router';
import {
gettext, siteRoot, canAddRepo, canViewOrg
gettext, siteRoot, canAddRepo, canViewOrg, enableOCM, enableOCMViaWebdav
} from '../utils/constants';
const propTypes = {
@@ -72,6 +72,32 @@ class FilesSubNav extends React.Component {
</Link>
</li>
}
{enableOCM &&
<li className={`nav-item ${this.getActiveClass('shared-with-ocm')}`}>
<Link
to={siteRoot + 'shared-with-ocm/'}
className={`nav-link ellipsis ${this.getActiveClass('shared-with-ocm')}`}
title={gettext('Shared from other servers')}
onClick={(e) => this.tabItemClick(e, 'shared-with-ocm')}
>
<span className="sf3-font-share-with-me sf3-font nav-icon" aria-hidden="true"></span>
<span className="nav-text">{gettext('Shared from other servers')}</span>
</Link>
</li>
}
{enableOCMViaWebdav &&
<li className={`nav-item ${this.getActiveClass('ocm-via-webdav')}`}>
<Link
to={siteRoot + 'ocm-via-webdav/'}
className={`nav-link ellipsis ${this.getActiveClass('ocm-via-webdav')}`}
title={gettext('Shared from other servers')}
onClick={(e) => this.tabItemClick(e, 'ocm-via-webdav')}
>
<span className="sf3-font-share-with-me sf3-font nav-icon" aria-hidden="true"></span>
<span className="nav-text">{gettext('Shared from other servers')}</span>
</Link>
</li>
}
{this.renderSharedGroups()}
</>
);

View File

@@ -6,7 +6,8 @@ import {
gettext, siteRoot, canAddGroup, canAddRepo, canShareRepo,
canGenerateShareLink, canGenerateUploadLink, canInvitePeople,
enableTC, sideNavFooterCustomHtml, enableShowAbout, showWechatSupportGroup,
canViewOrg, isPro, isDBSqlite3, customNavItems, mediaUrl
canViewOrg, enableOCM, enableOCMViaWebdav,
isPro, isDBSqlite3, customNavItems, mediaUrl
} from '../utils/constants';
import { seafileAPI } from '../utils/seafile-api';
import { Utils } from '../utils/utils';
@@ -62,7 +63,7 @@ class MainSideNav extends React.Component {
return group;
});
this.filesNavHeight = (groupList.length + (canAddGroup ? 1 : 0) + (canAddRepo ? 1 : 0) + (canViewOrg ? 1 : 0) + 1) * SUB_NAV_ITEM_HEIGHT;
this.filesNavHeight = (groupList.length + (canAddGroup ? 1 : 0) + (canAddRepo ? 1 : 0) + (canViewOrg ? 1 : 0) + (enableOCM ? 1 : 0) + (enableOCMViaWebdav ? 1 : 0) + 1) * SUB_NAV_ITEM_HEIGHT;
this.setState({
groupItems: groupList.sort((a, b) => {
return a.name.toLowerCase() < b.name.toLowerCase() ? -1 : 1;

View File

@@ -13,7 +13,7 @@ class SortMenu extends React.Component {
constructor(props) {
super(props);
this.sortOptions = [
this.sortOptions = this.props.sortOptions || [
{ value: 'name-asc', text: gettext('By name ascending') },
{ value: 'name-desc', text: gettext('By name descending') },
{ value: 'size-asc', text: gettext('By size ascending') },

View File

@@ -2,7 +2,7 @@ import React, { Component } from 'react';
import cookie from 'react-cookies';
import { Utils } from '../../utils/utils';
import { seafileAPI } from '../../utils/seafile-api';
import { gettext, canAddRepo, canViewOrg } from '../../utils/constants';
import { gettext, canAddRepo, canViewOrg, enableOCM } from '../../utils/constants';
import Repo from '../../models/repo';
import Group from '../../models/group';
import toaster from '../../components/toast';
@@ -16,6 +16,7 @@ import CreateRepoDialog from '../../components/dialog/create-repo-dialog';
import MylibRepoListView from '../../pages/my-libs/mylib-repo-list-view';
import SharedLibraries from '../../pages/shared-libs';
import SharedWithAll from '../../pages/shared-with-all';
import SharedWithOCM from '../../pages/share-with-ocm/shared-with-ocm';
import GroupItem from '../../pages/groups/group-item';
import { GroupsReposManager } from './groups-repos-manager';
import EventBus from '../../components/common/event-bus';
@@ -62,7 +63,6 @@ class Libraries extends Component {
this.unsubscribeUnsharedRepoToGroup();
}
initLibraries = () => {
const promiseListRepos = seafileAPI.listRepos({ 'type': ['mine', 'shared', 'public'] });
const promiseListGroups = seafileAPI.listGroups(true);
@@ -515,6 +515,17 @@ class Libraries extends Component {
</div>
}
{enableOCM &&
<div className="pb-3">
<SharedWithOCM
inAllLibs={true}
currentViewMode={currentViewMode}
sortBy={this.state.sortBy}
sortOrder={this.state.sortOrder}
/>
</div>
}
{groupList.length > 0 && groupList.map((group) => {
return (
<GroupItem

View File

@@ -9,6 +9,8 @@ import toaster from '../../components/toast';
import Loading from '../../components/loading';
import EmptyTip from '../../components/empty-tip';
import '../../css/lib-content-view.css';
dayjs.extend(relativeTime);
class OCMViaWebdav extends Component {
@@ -31,11 +33,12 @@ class OCMViaWebdav extends Component {
getAllReceivedShares = () => {
const url = seafileAPI.server + '/ocm-via-webdav/received-shares/';
seafileAPI.req.get(url).then((res) => {
const { received_share_list } = res.data;
this.setState({
loading: false,
shareID: '',
path: '',
items: res.data.received_share_list,
items: this.sortItems(received_share_list)
});
}).catch(error => {
let errMessage = Utils.getErrorMsg(error);
@@ -59,18 +62,18 @@ class OCMViaWebdav extends Component {
};
openFolder = (item) => {
this.setState({
loading: true,
});
const url = seafileAPI.server + '/ocm-via-webdav/received-shares/' + item.id + '/?path=' + item.path;
seafileAPI.req.get(url).then((res) => {
const { received_share_list, parent_dir } = res.data;
this.setState({
loading: false,
shareID: item.id,
path: res.data.parent_dir,
items: res.data.received_share_list,
path: parent_dir,
items: this.sortItems(received_share_list)
});
}).catch(error => {
let errMessage = Utils.getErrorMsg(error);
@@ -79,17 +82,17 @@ class OCMViaWebdav extends Component {
};
onPathClick = (path) => {
this.setState({
loading: true,
loading: true
});
const url = seafileAPI.server + '/ocm-via-webdav/received-shares/' + this.state.shareID + '/?path=' + path;
seafileAPI.req.get(url).then((res) => {
const { received_share_list, parent_dir } = res.data;
this.setState({
loading: false,
items: res.data.received_share_list,
path: res.data.parent_dir,
path: parent_dir,
items: this.sortItems(received_share_list)
});
}).catch(error => {
let errMessage = Utils.getErrorMsg(error);
@@ -97,25 +100,33 @@ class OCMViaWebdav extends Component {
});
};
sortItems = (items) => {
return items.sort((a, b) => {
return a.is_dir ? -1 : 1;
});
};
render() {
const { loading, errorMsg, items, shareID, path } = this.state;
return (
<Fragment>
<div className="main-panel-center">
<div className="cur-view-container">
<div className="cur-view-path align-items-center">
<DirPath
shareID={this.state.shareID}
currentPath={this.state.path}
shareID={shareID}
currentPath={path}
onPathClick={this.onPathClick}
getAllReceivedShares={this.getAllReceivedShares}
/>
</div>
<div className="cur-view-content">
<Content
loading={this.state.loading}
errorMsg={this.state.errorMsg}
items={this.state.items}
path={this.state.path}
loading={loading}
errorMsg={errorMsg}
items={items}
path={path}
leaveShare={this.leaveShare}
openFolder={this.openFolder}
/>
@@ -143,29 +154,29 @@ class Content extends Component {
render() {
const { loading, errorMsg, items, path } = this.props;
const emptyTip = (
<EmptyTip
title={gettext('No libraries have been shared with you')}
text={gettext('No libraries have been shared with you from other servers.')}
>
</EmptyTip>
);
if (loading) {
return <Loading />;
} else if (errorMsg) {
return <p className="error text-center">{errorMsg}</p>;
} else {
const emptyTip = (
<EmptyTip
title={gettext('No files or folders have been shared with you')}
text={gettext('No files or folders have been shared with you from other servers.')}
>
</EmptyTip>
);
const table = (
<table>
<thead>
<tr>
<th width="5%"></th>
<th width="30%">{gettext('Name')}</th>
<th width="35%">{gettext('Shared By')}</th>
<th width="20%">{gettext('Time')}</th>
<th width="5%">{/* operations */}</th>
<th width="5%">{/* operations */}</th>
<th width="40%">{gettext('Name')}</th>
<th width="10%">{/* operations */}</th>
<th width="30%">{gettext('Shared By')}</th>
<th width="15%">{gettext('Sharing Time')}</th>
</tr>
</thead>
<tbody>
@@ -188,7 +199,6 @@ class Content extends Component {
}
Content.propTypes = {
data: PropTypes.object.isRequired,
loading: PropTypes.bool.isRequired,
errorMsg: PropTypes.string.isRequired,
items: PropTypes.array.isRequired,
@@ -206,18 +216,21 @@ class Item extends Component {
constructor(props) {
super(props);
this.state = {
isHighlighted: false,
isOpIconShown: false
};
}
handleMouseOver = () => {
this.setState({
isHighlighted: true,
isOpIconShown: true
});
};
handleMouseOut = () => {
this.setState({
isHighlighted: false,
isOpIconShown: false
});
};
@@ -227,8 +240,7 @@ class Item extends Component {
window.location.href = downloadUrl;
};
leaveShare = (e) => {
e.preventDefault();
leaveShare = () => {
this.props.leaveShare(this.props.item);
};
@@ -238,8 +250,8 @@ class Item extends Component {
};
render() {
const item = this.props.item;
const { isOpIconShown } = this.state;
const { item, path } = this.props;
const { isHighlighted, isOpIconShown } = this.state;
if (item.is_dir) {
item.icon_url = Utils.getFolderIconUrl();
@@ -247,17 +259,27 @@ class Item extends Component {
item.icon_url = Utils.getFileIconUrl(item.name);
}
return (
<tr onMouseOver={this.handleMouseOver} onMouseOut={this.handleMouseOut}>
<td><img src={item.icon_url} width="24" alt="" /></td>
<tr
className={isHighlighted ? 'tr-highlight' : ''}
onMouseOver={this.handleMouseOver}
onMouseOut={this.handleMouseOut}
onFocus={this.handleMouseOver}
>
<td className="text-center">
<img src={item.icon_url} width="24" alt="" />
</td>
<td>
{item.is_dir ? <a href="#" onClick={this.openFolder}>{item.name}</a> : item.name}
{item.is_dir
? <a href="#" onClick={this.openFolder}>{item.name}</a>
: item.name
}
</td>
<td>
{item.is_dir ? '' : <i className={`op-icon sf3-font sf3-font-download1 ${isOpIconShown ? '' : 'invisible'}`} title={gettext('Download')} onClick={this.downloadFile}></i>}
{path ? '' : <i className={`op-icon sf2-icon-x3 ${isOpIconShown ? '' : 'invisible'}`} title={gettext('Leave Share')} onClick={this.leaveShare}></i>}
</td>
<td>{item.shared_by}</td>
<td title={dayjs(item.last_modified).format('dddd, MMMM D, YYYY h:mm:ss A')}>{dayjs(item.ctime).fromNow()}</td>
<td>{item.is_dir ? '' : <a href="#" className={`action-icon sf2-icon-download ${isOpIconShown ? '' : 'invisible'}`} title={gettext('Download')} onClick={this.downloadFile}></a>}
</td>
<td>{this.props.path ? '' : <a href="#" className={`action-icon sf2-icon-x3 ${isOpIconShown ? '' : 'invisible'}`} title={gettext('Leave Share')} onClick={this.leaveShare}></a>}
</td>
<td title={dayjs(item.ctime).format('dddd, MMMM D, YYYY h:mm:ss A')}>{dayjs(item.ctime).fromNow()}</td>
</tr>
);
}
@@ -298,7 +320,7 @@ class DirPath extends React.Component {
return (
<Fragment key={index}>
<span className="path-split">/</span>
<span className="path-file-name">{item}</span>
<span className="last-path-item" title={item}>{item}</span>
</Fragment>
);
} else {
@@ -310,7 +332,14 @@ class DirPath extends React.Component {
return (
<Fragment key={index} >
<span className="path-split">/</span>
<a className="path-link" data-path={nodePath} onClick={this.onPathClick}>{item}</a>
<span
className="path-item"
data-path={nodePath}
onClick={this.onPathClick}
title={item}
>
{item}
</span>
</Fragment>
);
}
@@ -319,11 +348,17 @@ class DirPath extends React.Component {
};
render() {
let pathElem = this.turnPathToLink(this.props.currentPath);
const { currentPath } = this.props;
return (
<div className="path-container">
<a href="#" onClick={this.props.getAllReceivedShares}>{gettext('All')}</a>
{pathElem}
<div className="path-container dir-view-path">
<span
className="path-item mw-100"
onClick={this.props.getAllReceivedShares}
title={gettext('Shared from other servers')}
>
{gettext('Shared from other servers')}
</span>
{currentPath && this.turnPathToLink(currentPath)}
</div>
);
}

View File

@@ -0,0 +1,87 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Dropdown, DropdownToggle, DropdownMenu, DropdownItem } from 'reactstrap';
import { gettext } from '../../utils/constants';
const propTypes = {
userPerm: PropTypes.string.isRequired,
openFileInput: PropTypes.func.isRequired
};
class LastPathItemWrapper extends React.Component {
constructor(props) {
super(props);
this.state = {
isDesktopMenuOpen: false
};
}
toggleDesktopOpMenu = () => {
this.setState({ isDesktopMenuOpen: !this.state.isDesktopMenuOpen });
};
onDropdownToggleKeyDown = (e) => {
if (e.key == 'Enter' || e.key == 'Space') {
this.toggleDesktopOpMenu();
}
};
onMenuItemKeyDown = (item, e) => {
if (e.key == 'Enter' || e.key == 'Space') {
item.onClick();
}
};
render() {
const { userPerm } = this.props;
let dropdownMenu = null;
if (userPerm == 'rw') {
const opList = [
{
'icon': 'upload-files',
'text': gettext('Upload'),
'onClick': this.props.openFileInput
}
];
dropdownMenu = (
<Dropdown isOpen={this.state.isDesktopMenuOpen} toggle={this.toggleDesktopOpMenu}>
<DropdownToggle
tag="div"
role="button"
className="path-item"
onClick={this.toggleDesktopOpMenu}
onKeyDown={this.onDropdownToggleKeyDown}
data-toggle="dropdown"
>
<i className="sf3-font-new sf3-font"></i>
<i className="sf3-font-down sf3-font path-item-dropdown-toggle"></i>
</DropdownToggle>
<DropdownMenu positionFixed={true}>
{opList.map((item, index) => {
return (
<DropdownItem key={index} onClick={item.onClick} onKeyDown={this.onMenuItemKeyDown.bind(this, item)}>
<i className={`sf3-font-${item.icon} sf3-font mr-2 dropdown-item-icon`}></i>
{item.text}
</DropdownItem>
);
})}
</DropdownMenu>
</Dropdown>
);
}
return (
<div className="dir-operation">
{this.props.children}
{userPerm == 'rw' && dropdownMenu}
</div>
);
}
}
LastPathItemWrapper.propTypes = propTypes;
export default LastPathItemWrapper;

View File

@@ -1,6 +1,7 @@
import React, { Fragment } from 'react';
import PropTypes from 'prop-types';
import { Link } from '@gatsbyjs/reach-router';
import classnames from 'classnames';
import dayjs from 'dayjs';
import relativeTime from 'dayjs/plugin/relativeTime';
import { gettext } from '../../utils/constants';
@@ -14,18 +15,21 @@ class DirentItem extends React.Component {
constructor(props) {
super(props);
this.state = {
isHighlighted: false,
isOpIconShown: false
};
}
handleMouseOver = () => {
this.setState({
isHighlighted: true,
isOpIconShown: true
});
};
handleMouseOut = () => {
this.setState({
isHighlighted: false,
isOpIconShown: false
});
};
@@ -34,19 +38,22 @@ class DirentItem extends React.Component {
this.props.openFolder(this.props.dirent);
};
downloadDirent = (e) => {
e.preventDefault();
downloadDirent = () => {
this.props.downloadDirent(this.props.dirent);
};
render() {
let { isOpIconShown } = this.state;
let { isHighlighted, isOpIconShown } = this.state;
let { dirent } = this.props;
let iconUrl = Utils.getDirentIcon(dirent);
return (
<Fragment>
<tr onMouseEnter={this.handleMouseOver} onMouseLeave={this.handleMouseOut}>
<tr
className={classnames({ 'tr-highlight': isHighlighted })}
onMouseEnter={this.handleMouseOver}
onMouseLeave={this.handleMouseOut}
>
<td className="text-center"><img src={iconUrl} width="24" alt='' /></td>
<td>
{dirent.is_file ?
@@ -56,7 +63,7 @@ class DirentItem extends React.Component {
</td>
<td>
{isOpIconShown && dirent.is_file &&
<a href="#" className="op-icon sf3-font sf3-font-download1" title={gettext('Download')} onClick={this.downloadDirent}></a>
<i role="button" className="op-icon sf3-font sf3-font-download1" title={gettext('Download')} onClick={this.downloadDirent}></i>
}
</td>
<td>{Utils.bytesToSize(dirent.size)}</td>
@@ -70,7 +77,6 @@ class DirentItem extends React.Component {
DirentItem.propTypes = {
dirent: PropTypes.object.isRequired,
openFolder: PropTypes.func.isRequired,
deleteDirent: PropTypes.func.isRequired,
downloadDirent: PropTypes.func.isRequired,
};
@@ -93,7 +99,7 @@ class DirContent extends React.Component {
return (
<Fragment>
<table className="table-hover">
<table>
<thead>
<tr>
<th width="5%">{/* icon*/}</th>
@@ -109,7 +115,6 @@ class DirContent extends React.Component {
key={index}
dirent={dirent}
openFolder={this.props.openFolder}
deleteDirent={this.props.deleteDirent}
downloadDirent={this.props.downloadDirent}
/>;
})}
@@ -125,7 +130,6 @@ DirContent.propTypes = {
errorMsg: PropTypes.string.isRequired,
direntList: PropTypes.array.isRequired,
openFolder: PropTypes.func.isRequired,
deleteDirent: PropTypes.func.isRequired,
downloadDirent: PropTypes.func.isRequired,
};

View File

@@ -3,13 +3,16 @@ import PropTypes from 'prop-types';
import { Link } from '@gatsbyjs/reach-router';
import { siteRoot, gettext } from '../../utils/constants';
import { Utils } from '../../utils/utils';
import LastPathItemWrapper from './last-path-item-wrapper';
const propTypes = {
repoID: PropTypes.string.isRequired,
repoName: PropTypes.string.isRequired,
currentPath: PropTypes.string.isRequired,
onPathClick: PropTypes.func.isRequired,
onTabNavClick: PropTypes.func.isRequired,
repoID: PropTypes.string.isRequired,
userPerm: PropTypes.string.isRequired,
openFileInput: PropTypes.func.isRequired
};
class DirPath extends React.Component {
@@ -31,7 +34,12 @@ class DirPath extends React.Component {
return (
<Fragment key={index}>
<span className="path-split">/</span>
<span className="path-file-name">{item}</span>
<LastPathItemWrapper
userPerm={this.props.userPerm}
openFileInput={this.props.openFileInput}
>
<span className="last-path-item" title={item}>{item}</span>
</LastPathItemWrapper>
</Fragment>
);
} else {
@@ -39,7 +47,7 @@ class DirPath extends React.Component {
return (
<Fragment key={index} >
<span className="path-split">/</span>
<a className="path-link" data-path={nodePath} onClick={this.onPathClick}>{item}</a>
<span className="path-item" role="button" data-path={nodePath} onClick={this.onPathClick} title={item}>{item}</span>
</Fragment>
);
}
@@ -48,16 +56,23 @@ class DirPath extends React.Component {
};
render() {
let { currentPath, repoName } = this.props;
let pathElem = this.turnPathToLink(currentPath);
const { currentPath, repoName } = this.props;
const pathElem = this.turnPathToLink(currentPath);
return (
<div className="path-container">
<Link to={siteRoot + 'shared-with-ocm/'} className="normal" onClick={(e) => this.props.onTabNavClick('shared-with-ocm')}>{gettext('All')}</Link>
<div className="path-container dir-view-path">
<Link to={siteRoot + 'shared-with-ocm/'} className="path-item normal mw-100" onClick={(e) => this.props.onTabNavClick('shared-with-ocm')} title={gettext('Shared from other servers')}>{gettext('Shared from other servers')}</Link>
<span className="path-split">/</span>
{(currentPath === '/' || currentPath === '') ?
<span className="path-repo-name">{repoName}</span> :
<a className="path-link" data-path="/" onClick={this.onPathClick}>{repoName}</a>
{(currentPath === '/' || currentPath === '')
? (
<LastPathItemWrapper
userPerm={this.props.userPerm}
openFileInput={this.props.openFileInput}
>
<span className="last-path-item" title={repoName}>{repoName}</span>
</LastPathItemWrapper>
)
: <span role="button" className="path-item" data-path="/" onClick={this.onPathClick} title={repoName}>{repoName}</span>
}
{pathElem}
</div>

View File

@@ -3,16 +3,19 @@ import PropTypes from 'prop-types';
import axios from 'axios';
import { Utils } from '../../utils/utils';
import { seafileAPI } from '../../utils/seafile-api';
import { siteRoot } from '../../utils/constants';
import { siteRoot, gettext } from '../../utils/constants';
import toaster from '../../components/toast';
import DirPathBar from './remote-dir-path';
import DirContent from './remote-dir-content';
import '../../css/lib-content-view.css';
class Dirent {
constructor(obj) {
this.name = obj.name;
this.mtime = obj.mtime;
this.size = obj.size;
this.type = obj.type;
this.is_file = obj.type === 'file';
}
@@ -119,6 +122,9 @@ class DirView extends Component {
this.setState({
direntList: direntList
});
const msg = gettext('Successfully added the file.');
toaster.success(msg);
});
}).catch((err) => {
let errMessage = Utils.getErrorMsg(err);
@@ -127,15 +133,11 @@ class DirView extends Component {
};
render() {
const { loading, errorMsg, repoName, direntList, path } = this.state;
const { loading, errorMsg, repoName, direntList, path, userPerm } = this.state;
const { repoID } = this.props;
return (
<Fragment>
{/*
<input className="d-none" type="file" onChange={this.onFileInputChange} ref={this.fileInput} />
{userPerm === 'rw' && <Button className="operation-item" onClick={this.openFileInput}>{gettext('Upload')}</Button>}
*/}
<div className="main-panel-center flex-row">
<div className="cur-view-container">
<div className="cur-view-path align-items-center">
@@ -145,7 +147,10 @@ class DirView extends Component {
currentPath={path}
onPathClick={this.onPathClick}
onTabNavClick={this.props.onTabNavClick}
userPerm={userPerm}
openFileInput={this.openFileInput}
/>
<input className="d-none" type="file" onChange={this.onFileInputChange} ref={this.fileInput} />
</div>
<div className="cur-view-content">
<DirContent
@@ -153,7 +158,6 @@ class DirView extends Component {
errorMsg={errorMsg}
direntList={direntList}
openFolder={this.openFolder}
deleteDirent={this.deleteDirent}
downloadDirent={this.downloadDirent}
/>
</div>

View File

@@ -2,59 +2,133 @@ import React, { Component, Fragment } from 'react';
import PropTypes from 'prop-types';
import dayjs from 'dayjs';
import relativeTime from 'dayjs/plugin/relativeTime';
import { Link } from '@gatsbyjs/reach-router';
import cookie from 'react-cookies';
import classnames from 'classnames';
import { Link, navigate } from '@gatsbyjs/reach-router';
import { Dropdown, DropdownToggle, DropdownItem } from 'reactstrap';
import { gettext, siteRoot } 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 ViewModes from '../../components/view-modes';
import ReposSortMenu from '../../components/sort-menu';
import SortOptionsDialog from '../../components/dialog/sort-options';
import LibsMobileThead from '../../components/libs-mobile-thead';
import { LIST_MODE } from '../../components/dir-view-mode/constants';
const propTypes = {
currentViewMode: PropTypes.string,
inAllLibs: PropTypes.bool
};
dayjs.extend(relativeTime);
class Content extends Component {
render() {
const { loading, errorMsg, items } = this.props;
sortByName = (e) => {
e.preventDefault();
const sortBy = 'name';
const sortOrder = this.props.sortOrder == 'asc' ? 'desc' : 'asc';
this.props.sortItems(sortBy, sortOrder);
};
const emptyTip = (
<EmptyTip
title={gettext('No libraries have been shared with you')}
text={gettext('No libraries have been shared with you from other servers.')}
>
</EmptyTip>
renderItems = () => {
const { items, currentViewMode, inAllLibs } = this.props;
const isDesktop = Utils.isDesktop();
return (
<>
{items.map((item, index) => {
return (
<Item
key={index}
item={item}
currentViewMode={currentViewMode}
inAllLibs={inAllLibs}
isDesktop={isDesktop}
leaveShare={this.props.leaveShare}
/>
);
})}
</>
);
};
render() {
const { loading, errorMsg, items, sortOrder, currentViewMode, inAllLibs } = this.props;
if (loading) {
return <Loading />;
} else if (errorMsg) {
return <p className="error text-center">{errorMsg}</p>;
} else {
const table = (
<table>
<thead>
<tr>
<th width="4%"></th>
<th width="20%">{gettext('Name')}</th>
<th width="20%">{gettext('Shared by')}</th>
<th width="26%">{gettext('At server')}</th>
<th width="20%">{gettext('Time')}</th>
<th width="10%">{/* operations */}</th>
</tr>
</thead>
<tbody>
{items.map((item, index) => {
return <Item
key={index}
item={item}
leaveShare={this.props.leaveShare}
/>;
})}
</tbody>
</table>
);
if (items.length == 0) {
const emptyTipTitle = gettext('No libraries have been shared with you');
const emptyTip = inAllLibs
? <p className={`libraries-empty-tip-in-${currentViewMode}-mode`}>{emptyTipTitle}</p>
: (
<EmptyTip
title={emptyTipTitle}
text={gettext('No libraries have been shared with you from other servers.')}
>
</EmptyTip>
);
return emptyTip;
}
const isDesktop = Utils.isDesktop();
if (isDesktop) {
const sortIcon = sortOrder === 'asc'
? <span className="sf3-font sf3-font-down rotate-180 d-inline-block"></span>
: <span className="sf3-font sf3-font-down"></span>;
return currentViewMode == LIST_MODE
? (
<table className={classnames({ 'table-thead-hidden': inAllLibs })}>
<thead>
<tr>
<th width="4%"></th>
<th width="3%"><span className="sr-only">{gettext('Library Type')}</span></th>
<th width="35%"><a className="d-block table-sort-op" href="#" onClick={this.sortByName}>{gettext('Name')} {this.props.sortBy === 'name' && sortIcon}</a></th>
<th width="10%"><span className="sr-only">{gettext('Actions')}</span></th>
{inAllLibs
? (
<>
<th width="14%">{gettext('Size')}</th>
<th width="17%">{gettext('Last Update')}</th>
</>
)
: (
<>
<th width="31%">{gettext('At server')}</th>
</>
)}
<th width="17%">{gettext('Owner')}</th>
</tr>
</thead>
<tbody>
{this.renderItems()}
</tbody>
</table>
)
: (
<div className="d-flex justify-content-between flex-wrap">
{this.renderItems()}
</div>
);
} else { // mobile
return (
<table className="table-thead-hidden">
<LibsMobileThead inAllLibs={inAllLibs} />
<tbody>
{this.renderItems()}
</tbody>
</table>
);
}
return items.length ? table : emptyTip;
}
}
}
@@ -71,18 +145,22 @@ class Item extends Component {
constructor(props) {
super(props);
this.state = {
isOpIconShown: false
isHighlighted: false,
isOpIconShown: false,
isItemMenuShow: false // for mobile
};
}
handleMouseOver = () => {
this.setState({
isHighlighted: true,
isOpIconShown: true
});
};
handleMouseOut = () => {
this.setState({
isHighlighted: false,
isOpIconShown: false
});
};
@@ -92,26 +170,113 @@ class Item extends Component {
this.props.leaveShare(this.props.item);
};
toggleOperationMenu = () => {
this.setState({ isItemMenuShow: !this.state.isItemMenuShow });
};
visitRepo = () => {
navigate(this.repoURL);
};
render() {
const item = this.props.item;
const { isOpIconShown } = this.state;
const { item, isDesktop, currentViewMode, inAllLibs } = this.props;
const { isHighlighted, isOpIconShown } = this.state;
item.icon_url = Utils.getLibIconUrl(item);
item.icon_title = Utils.getLibIconTitle(item);
let shareRepoUrl = `${siteRoot}remote-library/${this.props.item.provider_id}/${this.props.item.repo_id}/${Utils.encodePath(this.props.item.repo_name)}/`;
return (
<tr onMouseOver={this.handleMouseOver} onMouseOut={this.handleMouseOut} onFocus={this.handleMouseOver}>
<td><img src={item.icon_url} title={item.icon_title} alt={item.icon_title} width="24" /></td>
<td><Link to={shareRepoUrl}>{item.repo_name}</Link></td>
<td>{item.from_user}</td>
<td>{item.from_server_url}</td>
<td title={dayjs(item.last_modified).format('dddd, MMMM D, YYYY h:mm:ss A')}>{dayjs(item.ctime).fromNow()}</td>
<td>
<a href="#" role="button" className={`action-icon sf2-icon-x3 ${isOpIconShown ? '' : 'invisible'}`} title={gettext('Leave Share')} aria-label={gettext('Leave Share')} onClick={this.leaveShare}></a>
</td>
</tr>
);
this.repoURL = shareRepoUrl;
if (isDesktop) {
return currentViewMode == LIST_MODE
? (
<tr
className={isHighlighted ? 'tr-highlight' : ''}
onMouseOver={this.handleMouseOver}
onMouseOut={this.handleMouseOut}
onFocus={this.handleMouseOver}
>
<td></td>
<td><img src={item.icon_url} title={item.icon_title} alt={item.icon_title} width="24" /></td>
<td><Link to={shareRepoUrl}>{item.repo_name}</Link></td>
<td>
<i role="button" className={`op-icon sf2-icon-x3 ${isOpIconShown ? '' : 'invisible'}`} title={gettext('Leave Share')} aria-label={gettext('Leave Share')} onClick={this.leaveShare}></i>
</td>
{inAllLibs
? (
<>
<td></td>
<td></td>
</>
)
: (
<>
<td>{item.from_server_url}</td>
</>
)}
<td>{item.from_user}</td>
</tr>
)
: (
<div
className="library-grid-item px-3 d-flex justify-content-between align-items-center"
onMouseOver={this.handleMouseOver}
onMouseOut={this.handleMouseOut}
onFocus={this.handleMouseOver}
>
<div className="d-flex align-items-center text-truncate">
<img src={item.icon_url} title={item.icon_title} alt={item.icon_title} width="36" className="mr-2" />
<Link to={shareRepoUrl} className="library-name text-truncate" title={item.repo_name}>{item.repo_name}</Link>
</div>
<div className="flex-shrink-0">
<i role="button" className={`op-icon sf2-icon-x3 ${isOpIconShown ? '' : 'invisible'}`} title={gettext('Leave Share')} aria-label={gettext('Leave Share')} onClick={this.leaveShare}></i>
</div>
</div>
);
} else {
// mobile
return (
<tr
className={isHighlighted ? 'tr-highlight' : ''}
onMouseOver={this.handleMouseOver}
onMouseOut={this.handleMouseOut}
>
<td onClick={this.visitRepo}>
<img src={item.icon_url} title={item.icon_title} alt={item.icon_title} width="24" />
</td>
<td onClick={this.visitRepo}>
{item.repo_name && (
<div>
<Link to={shareRepoUrl}>{item.repo_name}</Link>
</div>
)}
<span className="item-meta-info">{item.from_user}</span>
<span className="item-meta-info">{item.from_server_url}</span>
</td>
<td>
<Dropdown isOpen={this.state.isItemMenuShow} toggle={this.toggleOperationMenu}>
<DropdownToggle
tag="i"
className="sf-dropdown-toggle sf3-font sf3-font-more-vertical ml-0"
title={gettext('More operations')}
aria-label={gettext('More operations')}
data-toggle="dropdown"
aria-expanded={this.state.isItemMenuShow}
/>
<div className={`${this.state.isItemMenuShow ? '' : 'd-none'}`} onClick={this.toggleOperationMenu}>
<div className="mobile-operation-menu-bg-layer"></div>
<div className="mobile-operation-menu">
<DropdownItem className="mobile-menu-item" onClick={this.leaveShare}>{gettext('Leave Share')}</DropdownItem>
</div>
</div>
</Dropdown>
</td>
</tr>
);
}
}
}
@@ -123,18 +288,30 @@ Item.propTypes = {
class SharedWithOCM extends Component {
constructor(props) {
super(props);
this.sortOptions = [
{ value: 'name-asc', text: gettext('By name ascending') },
{ value: 'name-desc', text: gettext('By name descending') }
];
this.state = {
loading: true,
errorMsg: '',
items: []
items: [],
currentViewMode: localStorage.getItem('sf_repo_list_view_mode') || LIST_MODE,
sortBy: 'name',
sortOrder: this.props.sortOrder || cookie.load('seafile-repo-dir-sort-order') || 'asc', // 'asc' or 'desc'
isSortOptionsDialogOpen: false
};
}
componentDidMount() {
seafileAPI.listOCMSharesReceived().then((res) => {
const { ocm_share_received_list } = res.data;
this.setState({
loading: false,
items: res.data.ocm_share_received_list
items: ocm_share_received_list
}, () => {
const { sortBy, sortOrder } = this.state;
this.sortItems(sortBy, sortOrder);
});
}).catch((error) => {
this.setState({
@@ -144,6 +321,18 @@ class SharedWithOCM extends Component {
});
}
static getDerivedStateFromProps(props, state) {
if (props.sortBy == 'name' && props.sortOrder != state.sortOrder) {
cookie.save('seafile-repo-dir-sort-order', props.sortOrder);
return {
...state,
sortOrder: props.sortOrder,
items: Utils.sortRepos(state.items, props.sortBy, props.sortOrder)
};
}
return null;
}
leaveShare = (item) => {
const { id, repo_name } = item;
seafileAPI.deleteOCMShareReceived(id).then((res) => {
@@ -158,27 +347,131 @@ class SharedWithOCM extends Component {
});
};
renderSortIconInMobile = () => {
return (
<>
{(!Utils.isDesktop() && this.state.items.length > 0) &&
<span
className="sf3-font sf3-font-sort action-icon"
onClick={this.toggleSortOptionsDialog}
>
</span>
}
</>
);
};
switchViewMode = (newMode) => {
this.setState({
currentViewMode: newMode
}, () => {
localStorage.setItem('sf_repo_list_view_mode', newMode);
});
};
onSelectSortOption = (sortOption) => {
const [sortBy, sortOrder] = sortOption.value.split('-');
this.setState({ sortBy, sortOrder }, () => {
this.sortItems(sortBy, sortOrder);
});
};
sortItems = (sortBy, sortOrder) => {
cookie.save('seafile-repo-dir-sort-by', sortBy);
cookie.save('seafile-repo-dir-sort-order', sortOrder);
this.setState({
sortBy: sortBy,
sortOrder: sortOrder,
items: Utils.sortRepos(this.state.items, sortBy, sortOrder)
});
};
toggleSortOptionsDialog = () => {
this.setState({
isSortOptionsDialogOpen: !this.state.isSortOptionsDialogOpen
});
};
renderContent = (currentViewMode) => {
return (
<Content
inAllLibs={this.props.inAllLibs}
currentViewMode={currentViewMode}
loading={this.state.loading}
errorMsg={this.state.errorMsg}
items={this.state.items}
sortBy={this.state.sortBy}
sortOrder={this.state.sortOrder}
sortItems={this.sortItems}
leaveShare={this.leaveShare}
/>
);
};
render() {
const { inAllLibs = false, currentViewMode: propCurrentViewMode } = this.props; // inAllLibs: in 'All Libs'('Files') page
const { sortBy, sortOrder, currentViewMode: stateCurrentViewMode } = this.state;
const currentViewMode = inAllLibs ? propCurrentViewMode : stateCurrentViewMode;
return (
<Fragment>
<div className="main-panel-center">
<div className="cur-view-container">
<div className="cur-view-path">
<h3 className="sf-heading m-0">{gettext('Shared from other servers')}</h3>
{inAllLibs
? (
<>
<div className={`d-flex justify-content-between mt-3 py-1 ${currentViewMode == LIST_MODE ? 'sf-border-bottom' : ''}`}>
<h4 className="sf-heading m-0">
<span className="sf3-font-share-with-me sf3-font nav-icon" aria-hidden="true"></span>
{gettext('Shared from other servers')}
</h4>
{/* this.renderSortIconInMobile() */}
</div>
{this.renderContent(currentViewMode)}
</>
)
: (
<div className="main-panel-center">
<div className="cur-view-container">
<div className="cur-view-path">
<h3 className="sf-heading m-0">{gettext('Shared from other servers')}</h3>
{Utils.isDesktop() && (
<div className="d-flex align-items-center">
<div className="mr-2">
<ViewModes
currentViewMode={currentViewMode}
switchViewMode={this.switchViewMode}
/>
</div>
<ReposSortMenu
sortOptions={this.sortOptions}
sortBy={sortBy}
sortOrder={sortOrder}
onSelectSortOption={this.onSelectSortOption}
/>
</div>
)}
{this.renderSortIconInMobile()}
</div>
<div className={classnames('cur-view-content', 'repos-container', { 'pt-3': currentViewMode != LIST_MODE })}>
{this.renderContent(currentViewMode)}
</div>
</div>
</div>
<div className="cur-view-content">
<Content
loading={this.state.loading}
errorMsg={this.state.errorMsg}
items={this.state.items}
leaveShare={this.leaveShare}
/>
</div>
</div>
</div>
)}
{this.state.isSortOptionsDialogOpen &&
<SortOptionsDialog
toggleDialog={this.toggleSortOptionsDialog}
sortOptions={this.sortOptions}
sortBy={this.state.sortBy}
sortOrder={this.state.sortOrder}
sortItems={this.sortItems}
/>
}
</Fragment>
);
}
}
SharedWithOCM.propTypes = propTypes;
export default SharedWithOCM;

View File

@@ -93,8 +93,7 @@ class Item extends Component {
this.setState({ isShowSharedDialog: false });
};
onToggleStarRepo = (e) => {
e.preventDefault();
onToggleStarRepo = () => {
const repoName = this.props.data.repo_name;
if (this.state.isStarred) {
seafileAPI.unstarItem(this.props.data.repo_id, '/').then(() => {