mirror of
https://github.com/haiwen/seahub.git
synced 2025-09-09 02:42:47 +00:00
Side nav (#6309)
* [user panel] combined 7 commits into 1: [user side nav] added 'fold/unfold' for the sidebar [redesign] redesigned toolbar for 'my libs' & 'shared with all'; redesigned 'top bar' for 'help', 'clients' and other pages [Shared with all] 'share existing libraries' dialog: added the 'close' icon back, enabled clicking outside to close the dialog ['Invite Guest' page] redesigned the toolbar ['Share Admin' - 'Links'] 'Share Links' page: redesigned the toolbar ['Share Admin' - 'Links'] 'Upload Links' page: redesigned the toolbar cleaned up code * [user side panel] update * [user panel] update * [code style] update: remove an eslint warning
This commit is contained in:
@@ -6,6 +6,8 @@ import { Modal } from 'reactstrap';
|
|||||||
import { siteRoot } from './utils/constants';
|
import { siteRoot } from './utils/constants';
|
||||||
import { Utils } from './utils/utils';
|
import { Utils } from './utils/utils';
|
||||||
import SystemNotification from './components/system-notification';
|
import SystemNotification from './components/system-notification';
|
||||||
|
//import Header from './components/header';
|
||||||
|
import Logo from './components/logo';
|
||||||
import SidePanel from './components/side-panel';
|
import SidePanel from './components/side-panel';
|
||||||
import MainPanel from './components/main-panel';
|
import MainPanel from './components/main-panel';
|
||||||
import FilesActivities from './pages/dashboard/files-activities';
|
import FilesActivities from './pages/dashboard/files-activities';
|
||||||
@@ -42,6 +44,7 @@ const LinkedDevicesWrapper = MainContentWrapper(LinkedDevices);
|
|||||||
const SharedLibrariesWrapper = MainContentWrapper(SharedLibraries);
|
const SharedLibrariesWrapper = MainContentWrapper(SharedLibraries);
|
||||||
const SharedWithOCMWrapper = MainContentWrapper(ShareWithOCM);
|
const SharedWithOCMWrapper = MainContentWrapper(ShareWithOCM);
|
||||||
const OCMViaWebdavWrapper = MainContentWrapper(OCMViaWebdav);
|
const OCMViaWebdavWrapper = MainContentWrapper(OCMViaWebdav);
|
||||||
|
const InvitationsViewWrapper = MainContentWrapper(InvitationsView);
|
||||||
const ShareAdminLibrariesWrapper = MainContentWrapper(ShareAdminLibraries);
|
const ShareAdminLibrariesWrapper = MainContentWrapper(ShareAdminLibraries);
|
||||||
const ShareAdminFoldersWrapper = MainContentWrapper(ShareAdminFolders);
|
const ShareAdminFoldersWrapper = MainContentWrapper(ShareAdminFolders);
|
||||||
|
|
||||||
@@ -50,8 +53,8 @@ class App extends Component {
|
|||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = {
|
this.state = {
|
||||||
isOpen: false,
|
|
||||||
isSidePanelClosed: false,
|
isSidePanelClosed: false,
|
||||||
|
isSidePanelFolded: localStorage.getItem('sf_user_side_nav_folded') == 'true' || false,
|
||||||
currentTab: '',
|
currentTab: '',
|
||||||
pathPrefix: [],
|
pathPrefix: [],
|
||||||
};
|
};
|
||||||
@@ -203,14 +206,38 @@ class App extends Component {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
toggleFoldSideNav = () => {
|
||||||
|
this.setState({
|
||||||
|
isSidePanelFolded: !this.state.isSidePanelFolded
|
||||||
|
}, () => {
|
||||||
|
localStorage.setItem('sf_user_side_nav_folded', this.state.isSidePanelFolded);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
let { currentTab, isSidePanelClosed } = this.state;
|
const { currentTab, isSidePanelClosed, isSidePanelFolded } = this.state;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<SystemNotification />
|
<SystemNotification />
|
||||||
<div id="main">
|
{/*<Header
|
||||||
<SidePanel isSidePanelClosed={this.state.isSidePanelClosed} onCloseSidePanel={this.onCloseSidePanel} currentTab={currentTab} tabItemClick={this.tabItemClick} />
|
isSidePanelClosed={isSidePanelClosed}
|
||||||
|
onCloseSidePanel={this.onCloseSidePanel}
|
||||||
|
onShowSidePanel={this.onShowSidePanel}
|
||||||
|
onSearchedClick={this.onSearchedClick}
|
||||||
|
/>
|
||||||
|
*/}
|
||||||
|
<div id="main" className="user-panel">
|
||||||
|
<Logo onCloseSidePanel={this.onCloseSidePanel} positioned={true} />
|
||||||
|
<SidePanel
|
||||||
|
isSidePanelClosed={isSidePanelClosed}
|
||||||
|
isSidePanelFolded={isSidePanelFolded}
|
||||||
|
onCloseSidePanel={this.onCloseSidePanel}
|
||||||
|
currentTab={currentTab}
|
||||||
|
tabItemClick={this.tabItemClick}
|
||||||
|
showLogoOnlyInMobile={true}
|
||||||
|
toggleFoldSideNav={this.toggleFoldSideNav}
|
||||||
|
/>
|
||||||
<MainPanel>
|
<MainPanel>
|
||||||
<Router className="reach-router">
|
<Router className="reach-router">
|
||||||
<Libraries path={ siteRoot } onShowSidePanel={this.onShowSidePanel} onSearchedClick={this.onSearchedClick} />
|
<Libraries path={ siteRoot } onShowSidePanel={this.onShowSidePanel} onSearchedClick={this.onSearchedClick} />
|
||||||
@@ -239,7 +266,7 @@ class App extends Component {
|
|||||||
/>
|
/>
|
||||||
<Wikis path={siteRoot + 'published'} onShowSidePanel={this.onShowSidePanel} onSearchedClick={this.onSearchedClick}/>
|
<Wikis path={siteRoot + 'published'} onShowSidePanel={this.onShowSidePanel} onSearchedClick={this.onSearchedClick}/>
|
||||||
<PublicSharedView path={siteRoot + 'org/'} onShowSidePanel={this.onShowSidePanel} onSearchedClick={this.onSearchedClick} onTabNavClick={this.tabItemClick}/>
|
<PublicSharedView path={siteRoot + 'org/'} onShowSidePanel={this.onShowSidePanel} onSearchedClick={this.onSearchedClick} onTabNavClick={this.tabItemClick}/>
|
||||||
<InvitationsView path={siteRoot + 'invitations/'} onShowSidePanel={this.onShowSidePanel} onSearchedClick={this.onSearchedClick} />
|
<InvitationsViewWrapper path={siteRoot + 'invitations/'} onShowSidePanel={this.onShowSidePanel} onSearchedClick={this.onSearchedClick} />
|
||||||
</Router>
|
</Router>
|
||||||
</MainPanel>
|
</MainPanel>
|
||||||
<MediaQuery query="(max-width: 767.8px)">
|
<MediaQuery query="(max-width: 767.8px)">
|
||||||
|
@@ -108,10 +108,10 @@ class ShareRepoDialog extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.props.onRepoSelectedHandler(this.state.selectedRepoList);
|
this.props.onRepoSelectedHandler(this.state.selectedRepoList);
|
||||||
this.onCloseDialog();
|
this.toggle();
|
||||||
};
|
};
|
||||||
|
|
||||||
onCloseDialog = () => {
|
toggle = () => {
|
||||||
this.props.onShareRepoDialogClose();
|
this.props.onShareRepoDialogClose();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -146,7 +146,7 @@ class ShareRepoDialog extends React.Component {
|
|||||||
</ModalBody>
|
</ModalBody>
|
||||||
{this.state.errMessage && <Alert color="danger" className="mt-2">{this.state.errMessage}</Alert>}
|
{this.state.errMessage && <Alert color="danger" className="mt-2">{this.state.errMessage}</Alert>}
|
||||||
<ModalFooter>
|
<ModalFooter>
|
||||||
<Button color="secondary" onClick={this.onCloseDialog}>{gettext('Close')}</Button>
|
<Button color="secondary" onClick={this.toggle}>{gettext('Close')}</Button>
|
||||||
<Button color="primary" onClick={this.handleSubmit}>{gettext('Submit')}</Button>
|
<Button color="primary" onClick={this.handleSubmit}>{gettext('Submit')}</Button>
|
||||||
</ModalFooter>
|
</ModalFooter>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
42
frontend/src/components/header.js
Normal file
42
frontend/src/components/header.js
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import Logo from './logo';
|
||||||
|
import CommonToolbar from './toolbar/common-toolbar';
|
||||||
|
|
||||||
|
const propTypes = {
|
||||||
|
isSidePanelClosed: PropTypes.bool,
|
||||||
|
onCloseSidePanel: PropTypes.func,
|
||||||
|
onShowSidePanel: PropTypes.func,
|
||||||
|
onSearchedClick: PropTypes.func,
|
||||||
|
searchPlaceholder: PropTypes.string,
|
||||||
|
showSearch: PropTypes.bool
|
||||||
|
};
|
||||||
|
|
||||||
|
class Header extends React.Component {
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { onShowSidePanel, onSearchedClick, showSearch } = this.props;
|
||||||
|
return (
|
||||||
|
<div id="header" className="d-flex justify-content-between py-2 px-4">
|
||||||
|
<div className={'flex-shrink-0 d-none d-md-flex'}>
|
||||||
|
<Logo onCloseSidePanel={this.props.onCloseSidePanel} />
|
||||||
|
</div>
|
||||||
|
<div className="flex-shrink-0 d-flex flex-fill">
|
||||||
|
<div className="cur-view-toolbar">
|
||||||
|
<span title="Side Nav Menu" onClick={onShowSidePanel} className="sf2-icon-menu side-nav-toggle hidden-md-up d-md-none">
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<CommonToolbar
|
||||||
|
showSearch={showSearch}
|
||||||
|
searchPlaceholder={this.props.searchPlaceholder}
|
||||||
|
onSearchedClick={onSearchedClick}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Header.propTypes = propTypes;
|
||||||
|
|
||||||
|
export default Header;
|
@@ -5,6 +5,7 @@ import { siteRoot, mediaUrl, logoPath, logoWidth, logoHeight, siteTitle } from '
|
|||||||
const propTypes = {
|
const propTypes = {
|
||||||
onCloseSidePanel: PropTypes.func,
|
onCloseSidePanel: PropTypes.func,
|
||||||
showCloseSidePanelIcon: PropTypes.bool,
|
showCloseSidePanelIcon: PropTypes.bool,
|
||||||
|
positioned: PropTypes.bool
|
||||||
};
|
};
|
||||||
|
|
||||||
class Logo extends React.Component {
|
class Logo extends React.Component {
|
||||||
@@ -14,8 +15,9 @@ class Logo extends React.Component {
|
|||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
const { positioned } = this.props;
|
||||||
return (
|
return (
|
||||||
<div className="top-logo">
|
<div className={`top-logo ${positioned ? 'd-none d-md-block positioned-top-logo' : ''}`}>
|
||||||
<a href={siteRoot} id="logo">
|
<a href={siteRoot} id="logo">
|
||||||
<img src={logoPath.indexOf('image-view') != -1 ? logoPath : mediaUrl + logoPath} height={logoHeight} width={logoWidth} title={siteTitle} alt="logo" />
|
<img src={logoPath.indexOf('image-view') != -1 ? logoPath : mediaUrl + logoPath} height={logoHeight} width={logoWidth} title={siteTitle} alt="logo" />
|
||||||
</a>
|
</a>
|
||||||
|
@@ -5,7 +5,7 @@ import {
|
|||||||
gettext, siteRoot, canAddGroup, canAddRepo, canShareRepo,
|
gettext, siteRoot, canAddGroup, canAddRepo, canShareRepo,
|
||||||
canGenerateShareLink, canGenerateUploadLink, canInvitePeople,
|
canGenerateShareLink, canGenerateUploadLink, canInvitePeople,
|
||||||
enableTC, sideNavFooterCustomHtml, additionalAppBottomLinks,
|
enableTC, sideNavFooterCustomHtml, additionalAppBottomLinks,
|
||||||
canViewOrg, isDocs, isPro, isDBSqlite3, customNavItems
|
canViewOrg, isDocs, isPro, isDBSqlite3, customNavItems, mediaUrl
|
||||||
} from '../utils/constants';
|
} from '../utils/constants';
|
||||||
import { seafileAPI } from '../utils/seafile-api';
|
import { seafileAPI } from '../utils/seafile-api';
|
||||||
import { Utils } from '../utils/utils';
|
import { Utils } from '../utils/utils';
|
||||||
@@ -16,6 +16,8 @@ import CreateGroupDialog from '../components/dialog/create-group-dialog';
|
|||||||
const propTypes = {
|
const propTypes = {
|
||||||
currentTab: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
currentTab: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
||||||
tabItemClick: PropTypes.func.isRequired,
|
tabItemClick: PropTypes.func.isRequired,
|
||||||
|
isSidePanelFolded: PropTypes.bool,
|
||||||
|
toggleFoldSideNav: PropTypes.func
|
||||||
};
|
};
|
||||||
|
|
||||||
const SUB_NAV_ITEM_HEIGHT = 28;
|
const SUB_NAV_ITEM_HEIGHT = 28;
|
||||||
@@ -23,12 +25,13 @@ const SUB_NAV_ITEM_HEIGHT = 28;
|
|||||||
class MainSideNav extends React.Component {
|
class MainSideNav extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
filesNavUnfolded: false,
|
filesNavUnfolded: false,
|
||||||
sharedExtended: false,
|
sharedExtended: false,
|
||||||
closeSideBar:false,
|
closeSideBar:false,
|
||||||
groupItems: [],
|
groupItems: [],
|
||||||
isCreateGroupDialogOpen: false
|
isCreateGroupDialogOpen: false,
|
||||||
};
|
};
|
||||||
this.adminHeight = 0;
|
this.adminHeight = 0;
|
||||||
this.filesNavHeight = 0;
|
this.filesNavHeight = 0;
|
||||||
@@ -214,12 +217,22 @@ class MainSideNav extends React.Component {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
toggleMinimize = () => {
|
||||||
|
this.setState({
|
||||||
|
isMinimized: !this.state.isMinimized
|
||||||
|
}, () => {
|
||||||
|
localStorage.setItem('sf_user_side_nav_minimized', this.state.isMinimized);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
let showActivity = isDocs || isPro || !isDBSqlite3;
|
let showActivity = isDocs || isPro || !isDBSqlite3;
|
||||||
const { filesNavUnfolded } = this.state;
|
const { filesNavUnfolded } = this.state;
|
||||||
|
const { isSidePanelFolded } = this.props;
|
||||||
return (
|
return (
|
||||||
<div className="side-nav">
|
<div className="side-nav">
|
||||||
<div className="side-nav-con">
|
<div className={'side-nav-con d-flex flex-column'}>
|
||||||
<h2 className="mb-2 px-2 font-weight-normal heading">{gettext('Workspace')}</h2>
|
<h2 className="mb-2 px-2 font-weight-normal heading">{gettext('Workspace')}</h2>
|
||||||
<ul className="nav nav-pills flex-column nav-container">
|
<ul className="nav nav-pills flex-column nav-container">
|
||||||
<li id="files" className={`nav-item flex-column ${this.getActiveClass('libraries')}`}>
|
<li id="files" className={`nav-item flex-column ${this.getActiveClass('libraries')}`}>
|
||||||
@@ -334,6 +347,16 @@ class MainSideNav extends React.Component {
|
|||||||
</ul>
|
</ul>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
<div className="side-nav-bottom-toolbar d-none d-md-flex mt-auto px-2 rounded flex-shrink-0 align-items-center" onClick={this.props.toggleFoldSideNav}>
|
||||||
|
{isSidePanelFolded ? <img src={`${mediaUrl}img/open-sidebar.svg`} width="20" alt="" title={gettext('Unfold the sidebar')} /> : (
|
||||||
|
<>
|
||||||
|
<img className="mr-2" src={`${mediaUrl}img/close-sidebar.svg`} width="20" alt="" />
|
||||||
|
<span>{gettext('Fold the sidebar')}</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@@ -1,5 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
import { Utils } from '../utils/utils';
|
||||||
import Logo from './logo';
|
import Logo from './logo';
|
||||||
import MainSideNav from './main-side-nav';
|
import MainSideNav from './main-side-nav';
|
||||||
|
|
||||||
@@ -8,23 +9,30 @@ const propTypes = {
|
|||||||
currentTab: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
currentTab: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
||||||
onCloseSidePanel: PropTypes.func,
|
onCloseSidePanel: PropTypes.func,
|
||||||
tabItemClick: PropTypes.func,
|
tabItemClick: PropTypes.func,
|
||||||
children: PropTypes.object
|
children: PropTypes.object,
|
||||||
|
showLogoOnlyInMobile: PropTypes.bool,
|
||||||
|
isSidePanelFolded: PropTypes.bool,
|
||||||
|
toggleFoldSideNav: PropTypes.func
|
||||||
};
|
};
|
||||||
|
|
||||||
class SidePanel extends React.Component {
|
class SidePanel extends React.Component {
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { children } = this.props;
|
const { children, isSidePanelFolded, showLogoOnlyInMobile = false } = this.props;
|
||||||
return (
|
return (
|
||||||
<div className={`side-panel ${this.props.isSidePanelClosed ? '' : 'left-zero'}`}>
|
<div className={`side-panel ${isSidePanelFolded ? 'side-panel-folded' : ''} ${this.props.isSidePanelClosed ? '' : 'left-zero'}`}>
|
||||||
<div className="side-panel-north">
|
<div className={'side-panel-north'}>
|
||||||
<Logo onCloseSidePanel={this.props.onCloseSidePanel}/>
|
{showLogoOnlyInMobile && !Utils.isDesktop() && <Logo onCloseSidePanel={this.props.onCloseSidePanel} />}
|
||||||
</div>
|
</div>
|
||||||
<div className="side-panel-center">
|
<div className="side-panel-center">
|
||||||
{children ?
|
{children ? children : (
|
||||||
children :
|
<MainSideNav
|
||||||
<MainSideNav tabItemClick={this.props.tabItemClick} currentTab={this.props.currentTab} />
|
tabItemClick={this.props.tabItemClick}
|
||||||
}
|
currentTab={this.props.currentTab}
|
||||||
|
isSidePanelFolded={isSidePanelFolded}
|
||||||
|
toggleFoldSideNav={this.props.toggleFoldSideNav}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@@ -1,46 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import CommonToolbar from './common-toolbar';
|
|
||||||
import { Button } from 'reactstrap';
|
|
||||||
import { gettext } from '../../utils/constants';
|
|
||||||
import { Utils } from '../../utils/utils';
|
|
||||||
|
|
||||||
const propTypes = {
|
|
||||||
onShowSidePanel: PropTypes.func.isRequired,
|
|
||||||
onSearchedClick: PropTypes.func.isRequired,
|
|
||||||
toggleInvitePeopleDialog: PropTypes.func.isRequired,
|
|
||||||
searchPlaceholder: PropTypes.string.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
class InvitationsToolbar extends React.Component {
|
|
||||||
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
let { onShowSidePanel, onSearchedClick, toggleInvitePeopleDialog } = this.props;
|
|
||||||
return (
|
|
||||||
<div className="main-panel-north border-left-show">
|
|
||||||
<div className="cur-view-toolbar">
|
|
||||||
<span title="Side Nav Menu" onClick={onShowSidePanel} className="sf2-icon-menu side-nav-toggle hidden-md-up d-md-none">
|
|
||||||
</span>
|
|
||||||
{Utils.isDesktop() ? (
|
|
||||||
<div className="operation">
|
|
||||||
<Button color="btn btn-secondary operation-item" onClick={toggleInvitePeopleDialog}>
|
|
||||||
<i className="sf3-font sf3-font-enlarge text-secondary mr-1"></i>{gettext('Invite Guest')}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<span className="sf2-icon-plus mobile-toolbar-icon" title={gettext('Invite Guest')} onClick={toggleInvitePeopleDialog}></span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<CommonToolbar searchPlaceholder={this.props.searchPlaceholder} onSearchedClick={onSearchedClick}/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
InvitationsToolbar.propTypes = propTypes;
|
|
||||||
|
|
||||||
export default InvitationsToolbar;
|
|
@@ -1,91 +0,0 @@
|
|||||||
import React, { Fragment } from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { DropdownToggle, Dropdown, DropdownMenu, DropdownItem } from 'reactstrap';
|
|
||||||
import { Link, navigate } from '@gatsbyjs/reach-router';
|
|
||||||
import { Utils } from '../../utils/utils';
|
|
||||||
import { siteRoot, gettext } from '../../utils/constants';
|
|
||||||
import ModalPortal from '../modal-portal';
|
|
||||||
import CreateRepoDialog from '../dialog/create-repo-dialog';
|
|
||||||
|
|
||||||
const propTypes = {
|
|
||||||
onCreateRepo: PropTypes.func.isRequired,
|
|
||||||
moreShown: PropTypes.bool
|
|
||||||
};
|
|
||||||
|
|
||||||
class MyLibsToolbar extends React.Component {
|
|
||||||
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
this.state = {
|
|
||||||
isCreateRepoDialogOpen: false,
|
|
||||||
isOpen: false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
onCreateRepo = (repo) => {
|
|
||||||
this.props.onCreateRepo(repo);
|
|
||||||
this.onCreateToggle();
|
|
||||||
};
|
|
||||||
|
|
||||||
onCreateToggle = () => {
|
|
||||||
this.setState({isCreateRepoDialogOpen: !this.state.isCreateRepoDialogOpen});
|
|
||||||
};
|
|
||||||
|
|
||||||
toggleMore = () => {
|
|
||||||
this.setState({ isOpen: !this.state.isOpen });
|
|
||||||
};
|
|
||||||
|
|
||||||
onDropdownToggleKeyDown = (e) => {
|
|
||||||
if (e.key == 'Enter' || e.key == 'Space') {
|
|
||||||
this.toggleMore();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
visitDeletedviaKey = (e) => {
|
|
||||||
if (e.key == 'Enter' || e.key == 'Space') {
|
|
||||||
navigate(`${siteRoot}my-libs/deleted/`);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { moreShown = false } = this.props;
|
|
||||||
return (
|
|
||||||
<Fragment>
|
|
||||||
{Utils.isDesktop() ? (
|
|
||||||
<div className="operation">
|
|
||||||
<button className="btn btn-secondary operation-item" title={gettext('New Library')} onClick={this.onCreateToggle}>
|
|
||||||
<i className="sf3-font sf3-font-enlarge text-secondary mr-1"></i>{gettext('New Library')}
|
|
||||||
</button>
|
|
||||||
{moreShown &&
|
|
||||||
<Dropdown isOpen={this.state.isOpen} toggle={this.toggleMore}>
|
|
||||||
<DropdownToggle className='btn btn-secondary operation-item' onKeyDown={this.onDropdownToggleKeyDown}>
|
|
||||||
{gettext('More')}
|
|
||||||
</DropdownToggle>
|
|
||||||
<DropdownMenu>
|
|
||||||
<DropdownItem className="link-dropdown-container" onKeyDown={this.visitDeletedviaKey}>
|
|
||||||
<Link className="link-dropdown-item" to={siteRoot + 'my-libs/deleted/'}>{gettext('Deleted Libraries')}</Link>
|
|
||||||
</DropdownItem>
|
|
||||||
</DropdownMenu>
|
|
||||||
</Dropdown>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<span className="sf2-icon-plus mobile-toolbar-icon" title={gettext('New Library')} onClick={this.onCreateToggle}></span>
|
|
||||||
)}
|
|
||||||
{this.state.isCreateRepoDialogOpen && (
|
|
||||||
<ModalPortal>
|
|
||||||
<CreateRepoDialog
|
|
||||||
libraryType='mine'
|
|
||||||
onCreateRepo={this.onCreateRepo}
|
|
||||||
onCreateToggle={this.onCreateToggle}
|
|
||||||
/>
|
|
||||||
</ModalPortal>
|
|
||||||
)}
|
|
||||||
</Fragment>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MyLibsToolbar.propTypes = propTypes;
|
|
||||||
|
|
||||||
export default MyLibsToolbar;
|
|
@@ -7,7 +7,10 @@
|
|||||||
|
|
||||||
/* for top bottom layout*/
|
/* for top bottom layout*/
|
||||||
#header {
|
#header {
|
||||||
display: flex;
|
background: #fff;
|
||||||
|
border-bottom: 1px solid #eee;
|
||||||
|
box-shadow: 0 2px 4px rgba(0,0,0, 10%);
|
||||||
|
z-index: 100;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* for left right layout */
|
/* for left right layout */
|
||||||
@@ -23,6 +26,22 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.side-panel-folded {
|
||||||
|
flex-basis: 69px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-panel .side-panel-north {
|
||||||
|
height: 49px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.positioned-top-logo {
|
||||||
|
position: fixed;
|
||||||
|
top: .5rem;
|
||||||
|
left: 1rem;
|
||||||
|
z-index: 101;
|
||||||
}
|
}
|
||||||
|
|
||||||
.main-panel {
|
.main-panel {
|
||||||
@@ -34,12 +53,12 @@
|
|||||||
@media (max-width: 767px) {
|
@media (max-width: 767px) {
|
||||||
.side-panel {
|
.side-panel {
|
||||||
position:fixed;
|
position:fixed;
|
||||||
|
top: 0;
|
||||||
left:-300px;
|
left:-300px;
|
||||||
z-index: 1031;
|
z-index: 1031;
|
||||||
width: 300px;
|
width: 300px;
|
||||||
max-width: calc(100% - 40px);
|
max-width: calc(100% - 40px);
|
||||||
height:100%;
|
height:100%;
|
||||||
background:#f8f8f8;
|
|
||||||
-webkit-transition: all 0.3s ease;
|
-webkit-transition: all 0.3s ease;
|
||||||
-moz-transition: all 0.3s ease;
|
-moz-transition: all 0.3s ease;
|
||||||
transition: all 0.3s ease;
|
transition: all 0.3s ease;
|
||||||
@@ -57,16 +76,6 @@
|
|||||||
z-index: 100;
|
z-index: 100;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 767px) {
|
|
||||||
.side-panel-north {
|
|
||||||
border-right: 1px solid #eee;
|
|
||||||
}
|
|
||||||
|
|
||||||
.main-panel-north {
|
|
||||||
padding-bottom: 0.25rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.side-panel-center,
|
.side-panel-center,
|
||||||
.main-panel-center {
|
.main-panel-center {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@@ -5,7 +5,7 @@ import moment from 'moment';
|
|||||||
import { gettext } from '../../utils/constants';
|
import { gettext } from '../../utils/constants';
|
||||||
import { Utils } from '../../utils/utils';
|
import { Utils } from '../../utils/utils';
|
||||||
import { seafileAPI } from '../../utils/seafile-api';
|
import { seafileAPI } from '../../utils/seafile-api';
|
||||||
import InvitationsToolbar from '../../components/toolbar/invitations-toolbar';
|
import SingleDropdownToolbar from '../../components/toolbar/single-dropdown-toolbar';
|
||||||
import InvitePeopleDialog from '../../components/dialog/invite-people-dialog';
|
import InvitePeopleDialog from '../../components/dialog/invite-people-dialog';
|
||||||
import InvitationRevokeDialog from '../../components/dialog/invitation-revoke-dialog';
|
import InvitationRevokeDialog from '../../components/dialog/invitation-revoke-dialog';
|
||||||
import Loading from '../../components/loading';
|
import Loading from '../../components/loading';
|
||||||
@@ -159,7 +159,6 @@ class Item extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const ItemPropTypes = {
|
const ItemPropTypes = {
|
||||||
data: PropTypes.object.isRequired,
|
|
||||||
invitation: PropTypes.object.isRequired,
|
invitation: PropTypes.object.isRequired,
|
||||||
isDesktop: PropTypes.bool.isRequired,
|
isDesktop: PropTypes.bool.isRequired,
|
||||||
};
|
};
|
||||||
@@ -271,15 +270,15 @@ class InvitationsView extends React.Component {
|
|||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<InvitationsToolbar
|
|
||||||
onShowSidePanel={this.props.onShowSidePanel}
|
|
||||||
onSearchedClick={this.props.onSearchedClick}
|
|
||||||
toggleInvitePeopleDialog={this.toggleInvitePeopleDialog}
|
|
||||||
/>
|
|
||||||
<div className="main-panel-center flex-row">
|
<div className="main-panel-center flex-row">
|
||||||
<div className="cur-view-container">
|
<div className="cur-view-container">
|
||||||
<div className="cur-view-path">
|
<div className="cur-view-path">
|
||||||
<h3 className="sf-heading">{gettext('Invite Guest')}</h3>
|
<h3 className="sf-heading">
|
||||||
|
{gettext('Invite Guest')}
|
||||||
|
<SingleDropdownToolbar
|
||||||
|
opList={[{'text': gettext('Invite Guest'), 'onClick': this.toggleInvitePeopleDialog}]}
|
||||||
|
/>
|
||||||
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
<div className="cur-view-content">
|
<div className="cur-view-content">
|
||||||
<Content data={this.state} />
|
<Content data={this.state} />
|
||||||
|
@@ -242,8 +242,7 @@ class Libraries extends Component {
|
|||||||
<TopToolbar
|
<TopToolbar
|
||||||
onShowSidePanel={this.props.onShowSidePanel}
|
onShowSidePanel={this.props.onShowSidePanel}
|
||||||
onSearchedClick={this.props.onSearchedClick}
|
onSearchedClick={this.props.onSearchedClick}
|
||||||
>
|
/>
|
||||||
</TopToolbar>
|
|
||||||
<div className="main-panel-center flex-row">
|
<div className="main-panel-center flex-row">
|
||||||
<div className="cur-view-container">
|
<div className="cur-view-container">
|
||||||
<div className="cur-view-path">
|
<div className="cur-view-path">
|
||||||
|
@@ -1,17 +1,21 @@
|
|||||||
import React, { Component, Fragment } from 'react';
|
import React, { Component, Fragment } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import cookie from 'react-cookies';
|
import cookie from 'react-cookies';
|
||||||
|
import { navigate } from '@gatsbyjs/reach-router';
|
||||||
|
import { DropdownToggle, Dropdown, DropdownMenu, DropdownItem } from 'reactstrap';
|
||||||
import { seafileAPI } from '../../utils/seafile-api';
|
import { seafileAPI } from '../../utils/seafile-api';
|
||||||
import { gettext } from '../../utils/constants';
|
import { gettext, siteRoot } from '../../utils/constants';
|
||||||
import { Utils } from '../../utils/utils';
|
import { Utils } from '../../utils/utils';
|
||||||
import toaster from '../../components/toast';
|
import toaster from '../../components/toast';
|
||||||
import Repo from '../../models/repo';
|
import Repo from '../../models/repo';
|
||||||
import Loading from '../../components/loading';
|
import Loading from '../../components/loading';
|
||||||
import EmptyTip from '../../components/empty-tip';
|
import EmptyTip from '../../components/empty-tip';
|
||||||
import TopToolbar from '../../components/toolbar/top-toolbar';
|
import TopToolbar from '../../components/toolbar/top-toolbar';
|
||||||
import MyLibsToolbar from '../../components/toolbar/my-libs-toolbar';
|
|
||||||
import MylibRepoListView from './mylib-repo-list-view';
|
import MylibRepoListView from './mylib-repo-list-view';
|
||||||
import SortOptionsDialog from '../../components/dialog/sort-options';
|
import SortOptionsDialog from '../../components/dialog/sort-options';
|
||||||
|
import SingleDropdownToolbar from '../../components/toolbar/single-dropdown-toolbar';
|
||||||
|
import ModalPortal from '../../components/modal-portal';
|
||||||
|
import CreateRepoDialog from '../../components/dialog/create-repo-dialog';
|
||||||
|
|
||||||
const propTypes = {
|
const propTypes = {
|
||||||
onShowSidePanel: PropTypes.func.isRequired,
|
onShowSidePanel: PropTypes.func.isRequired,
|
||||||
@@ -25,6 +29,8 @@ class MyLibraries extends Component {
|
|||||||
errorMsg: '',
|
errorMsg: '',
|
||||||
isLoading: true,
|
isLoading: true,
|
||||||
repoList: [],
|
repoList: [],
|
||||||
|
isCreateRepoDialogOpen: false,
|
||||||
|
isDropdownMenuOpen: false,
|
||||||
isSortOptionsDialogOpen: false,
|
isSortOptionsDialogOpen: false,
|
||||||
sortBy: cookie.load('seafile-repo-dir-sort-by') || 'name', // 'name' or 'time' or 'size'
|
sortBy: cookie.load('seafile-repo-dir-sort-by') || 'name', // 'name' or 'time' or 'size'
|
||||||
sortOrder: cookie.load('seafile-repo-dir-sort-order') || 'asc', // 'asc' or 'desc'
|
sortOrder: cookie.load('seafile-repo-dir-sort-order') || 'asc', // 'asc' or 'desc'
|
||||||
@@ -62,6 +68,7 @@ class MyLibraries extends Component {
|
|||||||
};
|
};
|
||||||
|
|
||||||
onCreateRepo = (repo) => {
|
onCreateRepo = (repo) => {
|
||||||
|
this.toggleCreateRepoDialog();
|
||||||
seafileAPI.createMineRepo(repo).then((res) => {
|
seafileAPI.createMineRepo(repo).then((res) => {
|
||||||
const newRepo = new Repo({
|
const newRepo = new Repo({
|
||||||
repo_id: res.data.repo_id,
|
repo_id: res.data.repo_id,
|
||||||
@@ -125,20 +132,61 @@ class MyLibraries extends Component {
|
|||||||
this.setState({repoList: repoList});
|
this.setState({repoList: repoList});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
toggleCreateRepoDialog = () => {
|
||||||
|
this.setState({isCreateRepoDialogOpen: !this.state.isCreateRepoDialogOpen});
|
||||||
|
};
|
||||||
|
|
||||||
|
toggleDropdownMenu = () => {
|
||||||
|
this.setState({
|
||||||
|
isDropdownMenuOpen: !this.state.isDropdownMenuOpen
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
visitDeleted = () => {
|
||||||
|
navigate(`${siteRoot}my-libs/deleted/`);
|
||||||
|
};
|
||||||
|
|
||||||
|
visitDeletedviaKey = (e) => {
|
||||||
|
if (e.key == 'Enter' || e.key == 'Space') {
|
||||||
|
this.visiteDeleted();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
const { isDropdownMenuOpen } = this.state;
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<TopToolbar
|
<TopToolbar
|
||||||
onShowSidePanel={this.props.onShowSidePanel}
|
onShowSidePanel={this.props.onShowSidePanel}
|
||||||
onSearchedClick={this.props.onSearchedClick}
|
onSearchedClick={this.props.onSearchedClick}
|
||||||
>
|
>
|
||||||
<MyLibsToolbar onCreateRepo={this.onCreateRepo} moreShown={true} />
|
|
||||||
</TopToolbar>
|
</TopToolbar>
|
||||||
<div className="main-panel-center flex-row">
|
<div className="main-panel-center flex-row">
|
||||||
<div className="cur-view-container">
|
<div className="cur-view-container">
|
||||||
<div className="cur-view-path">
|
<div className="cur-view-path">
|
||||||
<h3 className="sf-heading m-0">{gettext('My Libraries')}</h3>
|
<h3 className="sf-heading m-0">
|
||||||
{(!Utils.isDesktop() && this.state.repoList.length > 0) && <span className="sf3-font sf3-font-sort action-icon" onClick={this.toggleSortOptionsDialog}></span>}
|
{gettext('My Libraries')}
|
||||||
|
<SingleDropdownToolbar
|
||||||
|
opList={[{'text': gettext('New Library'), 'onClick': this.toggleCreateRepoDialog}]}
|
||||||
|
/>
|
||||||
|
</h3>
|
||||||
|
<div>
|
||||||
|
{(!Utils.isDesktop() && this.state.repoList.length > 0) && <span className="sf3-font sf3-font-sort action-icon" onClick={this.toggleSortOptionsDialog}></span>}
|
||||||
|
<Dropdown isOpen={isDropdownMenuOpen} toggle={this.toggleDropdownMenu}>
|
||||||
|
<DropdownToggle
|
||||||
|
tag="i"
|
||||||
|
className={'cur-view-path-btn sf3-font-more sf3-font ml-2'}
|
||||||
|
data-toggle="dropdown"
|
||||||
|
title={gettext('More operations')}
|
||||||
|
aria-label={gettext('More operations')}
|
||||||
|
aria-expanded={isDropdownMenuOpen}
|
||||||
|
>
|
||||||
|
</DropdownToggle>
|
||||||
|
<DropdownMenu right={true}>
|
||||||
|
<DropdownItem onClick={this.visitDeleted} onKeyDown={this.visitDeletedviaKey}>{gettext('Deleted Libraries')}</DropdownItem>
|
||||||
|
</DropdownMenu>
|
||||||
|
</Dropdown>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="cur-view-content">
|
<div className="cur-view-content">
|
||||||
{this.state.isLoading && <Loading />}
|
{this.state.isLoading && <Loading />}
|
||||||
@@ -166,6 +214,15 @@ class MyLibraries extends Component {
|
|||||||
sortItems={this.sortRepoList}
|
sortItems={this.sortRepoList}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
|
{this.state.isCreateRepoDialogOpen && (
|
||||||
|
<ModalPortal>
|
||||||
|
<CreateRepoDialog
|
||||||
|
libraryType='mine'
|
||||||
|
onCreateRepo={this.onCreateRepo}
|
||||||
|
onCreateToggle={this.toggleCreateRepoDialog}
|
||||||
|
/>
|
||||||
|
</ModalPortal>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
|
@@ -2,7 +2,7 @@ import React, { Component, Fragment } from 'react';
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { Link } from '@gatsbyjs/reach-router';
|
import { Link } from '@gatsbyjs/reach-router';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import { Dropdown, DropdownToggle, DropdownItem, Button } from 'reactstrap';
|
import { Dropdown, DropdownToggle, DropdownItem } from 'reactstrap';
|
||||||
import { seafileAPI } from '../../utils/seafile-api';
|
import { seafileAPI } from '../../utils/seafile-api';
|
||||||
import { Utils } from '../../utils/utils';
|
import { Utils } from '../../utils/utils';
|
||||||
import { isPro, gettext, siteRoot, canGenerateUploadLink } from '../../utils/constants';
|
import { isPro, gettext, siteRoot, canGenerateUploadLink } from '../../utils/constants';
|
||||||
@@ -16,6 +16,7 @@ import SortOptionsDialog from '../../components/dialog/sort-options';
|
|||||||
import CommonOperationConfirmationDialog from '../../components/dialog/common-operation-confirmation-dialog';
|
import CommonOperationConfirmationDialog from '../../components/dialog/common-operation-confirmation-dialog';
|
||||||
import TopToolbar from '../../components/toolbar/top-toolbar';
|
import TopToolbar from '../../components/toolbar/top-toolbar';
|
||||||
import Selector from '../../components/single-selector';
|
import Selector from '../../components/single-selector';
|
||||||
|
import SingleDropdownToolbar from '../../components/toolbar/single-dropdown-toolbar';
|
||||||
|
|
||||||
const contentPropTypes = {
|
const contentPropTypes = {
|
||||||
loading: PropTypes.bool.isRequired,
|
loading: PropTypes.bool.isRequired,
|
||||||
@@ -543,15 +544,18 @@ class ShareAdminShareLinks extends Component {
|
|||||||
onShowSidePanel={this.props.onShowSidePanel}
|
onShowSidePanel={this.props.onShowSidePanel}
|
||||||
onSearchedClick={this.props.onSearchedClick}
|
onSearchedClick={this.props.onSearchedClick}
|
||||||
>
|
>
|
||||||
|
|
||||||
<Button className="operation-item d-none d-md-block" onClick={this.toggleCleanInvalidShareLinksDialog}>{gettext('Clean invalid share links')}</Button>
|
|
||||||
</TopToolbar>
|
</TopToolbar>
|
||||||
<div className="main-panel-center">
|
<div className="main-panel-center">
|
||||||
<div className="cur-view-container">
|
<div className="cur-view-container">
|
||||||
<div className="cur-view-path share-upload-nav">
|
<div className="cur-view-path share-upload-nav">
|
||||||
<ul className="nav">
|
<ul className="nav">
|
||||||
<li className="nav-item">
|
<li className="nav-item">
|
||||||
<Link to={`${siteRoot}share-admin-share-links/`} className="nav-link active">{gettext('Share Links')}</Link>
|
<Link to={`${siteRoot}share-admin-share-links/`} className="nav-link active">
|
||||||
|
{gettext('Share Links')}
|
||||||
|
<SingleDropdownToolbar
|
||||||
|
opList={[{'text': gettext('Clean invalid share links'), 'onClick': this.toggleCleanInvalidShareLinksDialog}]}
|
||||||
|
/>
|
||||||
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
{canGenerateUploadLink && (
|
{canGenerateUploadLink && (
|
||||||
<li className="nav-item"><Link to={`${siteRoot}share-admin-upload-links/`} className="nav-link">{gettext('Upload Links')}</Link></li>
|
<li className="nav-item"><Link to={`${siteRoot}share-admin-upload-links/`} className="nav-link">{gettext('Upload Links')}</Link></li>
|
||||||
|
@@ -2,7 +2,7 @@ import React, { Component, Fragment } from 'react';
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { Link } from '@gatsbyjs/reach-router';
|
import { Link } from '@gatsbyjs/reach-router';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import { Dropdown, DropdownToggle, DropdownItem, Button } from 'reactstrap';
|
import { Dropdown, DropdownToggle, DropdownItem } from 'reactstrap';
|
||||||
import { gettext, siteRoot, canGenerateShareLink } from '../../utils/constants';
|
import { gettext, siteRoot, canGenerateShareLink } from '../../utils/constants';
|
||||||
import { seafileAPI } from '../../utils/seafile-api';
|
import { seafileAPI } from '../../utils/seafile-api';
|
||||||
import { Utils } from '../../utils/utils';
|
import { Utils } from '../../utils/utils';
|
||||||
@@ -13,6 +13,7 @@ import UploadLink from '../../models/upload-link';
|
|||||||
import ShareAdminLink from '../../components/dialog/share-admin-link';
|
import ShareAdminLink from '../../components/dialog/share-admin-link';
|
||||||
import CommonOperationConfirmationDialog from '../../components/dialog/common-operation-confirmation-dialog';
|
import CommonOperationConfirmationDialog from '../../components/dialog/common-operation-confirmation-dialog';
|
||||||
import TopToolbar from '../../components/toolbar/top-toolbar';
|
import TopToolbar from '../../components/toolbar/top-toolbar';
|
||||||
|
import SingleDropdownToolbar from '../../components/toolbar/single-dropdown-toolbar';
|
||||||
|
|
||||||
const contentPropTypes = {
|
const contentPropTypes = {
|
||||||
loading: PropTypes.bool.isRequired,
|
loading: PropTypes.bool.isRequired,
|
||||||
@@ -275,9 +276,7 @@ class ShareAdminUploadLinks extends Component {
|
|||||||
<TopToolbar
|
<TopToolbar
|
||||||
onShowSidePanel={this.props.onShowSidePanel}
|
onShowSidePanel={this.props.onShowSidePanel}
|
||||||
onSearchedClick={this.props.onSearchedClick}
|
onSearchedClick={this.props.onSearchedClick}
|
||||||
>
|
/>
|
||||||
<Button className="operation-item d-none d-md-block" onClick={this.toggleCleanInvalidUploadLinksDialog}>{gettext('Clean invalid upload links')}</Button>
|
|
||||||
</TopToolbar>
|
|
||||||
<div className="main-panel-center">
|
<div className="main-panel-center">
|
||||||
<div className="cur-view-container">
|
<div className="cur-view-container">
|
||||||
<div className="cur-view-path share-upload-nav">
|
<div className="cur-view-path share-upload-nav">
|
||||||
@@ -285,7 +284,14 @@ class ShareAdminUploadLinks extends Component {
|
|||||||
{canGenerateShareLink && (
|
{canGenerateShareLink && (
|
||||||
<li className="nav-item"><Link to={`${siteRoot}share-admin-share-links/`} className="nav-link">{gettext('Share Links')}</Link></li>
|
<li className="nav-item"><Link to={`${siteRoot}share-admin-share-links/`} className="nav-link">{gettext('Share Links')}</Link></li>
|
||||||
)}
|
)}
|
||||||
<li className="nav-item"><Link to={`${siteRoot}share-admin-upload-links/`} className="nav-link active">{gettext('Upload Links')}</Link></li>
|
<li className="nav-item">
|
||||||
|
<Link to={`${siteRoot}share-admin-upload-links/`} className="nav-link active">
|
||||||
|
{gettext('Upload Links')}
|
||||||
|
<SingleDropdownToolbar
|
||||||
|
opList={[{'text': gettext('Clean invalid upload links'), 'onClick': this.toggleCleanInvalidUploadLinksDialog}]}
|
||||||
|
/>
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div className="cur-view-content">
|
<div className="cur-view-content">
|
||||||
|
@@ -8,10 +8,13 @@ import Repo from '../../models/repo';
|
|||||||
import toaster from '../../components/toast';
|
import toaster from '../../components/toast';
|
||||||
import Loading from '../../components/loading';
|
import Loading from '../../components/loading';
|
||||||
import EmptyTip from '../../components/empty-tip';
|
import EmptyTip from '../../components/empty-tip';
|
||||||
import CommonToolbar from '../../components/toolbar/common-toolbar';
|
|
||||||
import SharedRepoListView from '../../components/shared-repo-list-view/shared-repo-list-view';
|
import SharedRepoListView from '../../components/shared-repo-list-view/shared-repo-list-view';
|
||||||
import SortOptionsDialog from '../../components/dialog/sort-options';
|
import SortOptionsDialog from '../../components/dialog/sort-options';
|
||||||
import TopToolbar from './top-toolbar';
|
import SingleDropdownToolbar from '../../components/toolbar/single-dropdown-toolbar';
|
||||||
|
import ModalPortal from '../../components/modal-portal';
|
||||||
|
import TopToolbar from '../../components/toolbar/top-toolbar';
|
||||||
|
import CreateRepoDialog from '../../components/dialog/create-repo-dialog';
|
||||||
|
import ShareRepoDialog from '../../components/dialog/share-repo-dialog';
|
||||||
|
|
||||||
const propTypes = {
|
const propTypes = {
|
||||||
onShowSidePanel: PropTypes.func,
|
onShowSidePanel: PropTypes.func,
|
||||||
@@ -29,6 +32,8 @@ class PublicSharedView extends React.Component {
|
|||||||
isLoading: true,
|
isLoading: true,
|
||||||
errMessage: '',
|
errMessage: '',
|
||||||
repoList: [],
|
repoList: [],
|
||||||
|
isCreateRepoDialogOpen: false,
|
||||||
|
isSelectRepoDialogOpen: false,
|
||||||
sortBy: cookie.load('seafile-repo-dir-sort-by') || 'name', // 'name' or 'time' or 'size'
|
sortBy: cookie.load('seafile-repo-dir-sort-by') || 'name', // 'name' or 'time' or 'size'
|
||||||
sortOrder: cookie.load('seafile-repo-dir-sort-order') || 'asc', // 'asc' or 'desc'
|
sortOrder: cookie.load('seafile-repo-dir-sort-order') || 'asc', // 'asc' or 'desc'
|
||||||
isSortOptionsDialogOpen: false,
|
isSortOptionsDialogOpen: false,
|
||||||
@@ -152,12 +157,51 @@ class PublicSharedView extends React.Component {
|
|||||||
renderSortIconInMobile = () => {
|
renderSortIconInMobile = () => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|
||||||
{(!Utils.isDesktop() && this.state.repoList.length > 0) && <span className="sf3-font sf3-font-sort action-icon" onClick={this.toggleSortOptionsDialog}></span>}
|
{(!Utils.isDesktop() && this.state.repoList.length > 0) && <span className="sf3-font sf3-font-sort action-icon" onClick={this.toggleSortOptionsDialog}></span>}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
onCreateRepoToggle = () => {
|
||||||
|
this.setState({isCreateRepoDialogOpen: !this.state.isCreateRepoDialogOpen});
|
||||||
|
};
|
||||||
|
|
||||||
|
onSelectRepoToggle = () => {
|
||||||
|
this.setState({isSelectRepoDialogOpen: !this.state.isSelectRepoDialogOpen});
|
||||||
|
};
|
||||||
|
|
||||||
|
onCreateRepo = (repo) => {
|
||||||
|
this.onCreateRepoToggle();
|
||||||
|
seafileAPI.createPublicRepo(repo).then(res => {
|
||||||
|
let object = {
|
||||||
|
repo_id: res.data.id,
|
||||||
|
repo_name: res.data.name,
|
||||||
|
permission: res.data.permission,
|
||||||
|
size: res.data.size,
|
||||||
|
owner_name: res.data.owner_name,
|
||||||
|
owner_email: res.data.owner,
|
||||||
|
mtime: res.data.mtime,
|
||||||
|
encrypted: res.data.encrypted,
|
||||||
|
};
|
||||||
|
let repo = new Repo(object);
|
||||||
|
this.addRepoItem(repo);
|
||||||
|
}).catch((error) => {
|
||||||
|
let errMessage = Utils.getErrorMsg(error);
|
||||||
|
toaster.danger(errMessage);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
onRepoSelectedHandler = (selectedRepoList) => {
|
||||||
|
selectedRepoList.forEach(repo => {
|
||||||
|
seafileAPI.selectOwnedRepoToPublic(repo.repo_id, {share_type: 'public', permission: repo.sharePermission}).then(() => {
|
||||||
|
this.addRepoItem(repo);
|
||||||
|
}).catch((error) => {
|
||||||
|
let errMessage = Utils.getErrorMsg(error);
|
||||||
|
toaster.danger(errMessage);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { inAllLibs = false, currentViewMode = 'list' } = this.props; // inAllLibs: in 'All Libs'('Files') page
|
const { inAllLibs = false, currentViewMode = 'list' } = this.props; // inAllLibs: in 'All Libs'('Files') page
|
||||||
|
|
||||||
@@ -178,14 +222,25 @@ class PublicSharedView extends React.Component {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<div className="main-panel-north border-left-show">
|
<TopToolbar
|
||||||
{canAddPublicRepo && <TopToolbar onShowSidePanel={this.props.onShowSidePanel} addRepoItem={this.addRepoItem} />}
|
onShowSidePanel={this.props.onShowSidePanel}
|
||||||
<CommonToolbar onSearchedClick={this.props.onSearchedClick} />
|
onSearchedClick={this.props.onSearchedClick}
|
||||||
</div>
|
>
|
||||||
|
</TopToolbar>
|
||||||
<div className="main-panel-center">
|
<div className="main-panel-center">
|
||||||
<div className="cur-view-container">
|
<div className="cur-view-container">
|
||||||
<div className="cur-view-path">
|
<div className="cur-view-path">
|
||||||
<h3 className="sf-heading m-0">{gettext('Shared with all')}</h3>
|
<h3 className="sf-heading m-0">
|
||||||
|
{gettext('Shared with all')}
|
||||||
|
{canAddPublicRepo &&
|
||||||
|
<SingleDropdownToolbar
|
||||||
|
opList={[
|
||||||
|
{'text': gettext('Share existing libraries'), 'onClick': this.onSelectRepoToggle},
|
||||||
|
{'text': gettext('New Library'), 'onClick': this.onCreateRepoToggle}
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
</h3>
|
||||||
{this.renderSortIconInMobile()}
|
{this.renderSortIconInMobile()}
|
||||||
</div>
|
</div>
|
||||||
<div className="cur-view-content">
|
<div className="cur-view-content">
|
||||||
@@ -201,6 +256,23 @@ class PublicSharedView extends React.Component {
|
|||||||
sortItems={this.sortItems}
|
sortItems={this.sortItems}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
|
{this.state.isCreateRepoDialogOpen && (
|
||||||
|
<ModalPortal>
|
||||||
|
<CreateRepoDialog
|
||||||
|
libraryType={this.state.libraryType}
|
||||||
|
onCreateToggle={this.onCreateRepoToggle}
|
||||||
|
onCreateRepo={this.onCreateRepo}
|
||||||
|
/>
|
||||||
|
</ModalPortal>
|
||||||
|
)}
|
||||||
|
{this.state.isSelectRepoDialogOpen && (
|
||||||
|
<ModalPortal>
|
||||||
|
<ShareRepoDialog
|
||||||
|
onRepoSelectedHandler={this.onRepoSelectedHandler}
|
||||||
|
onShareRepoDialogClose={this.onSelectRepoToggle}
|
||||||
|
/>
|
||||||
|
</ModalPortal>
|
||||||
|
)}
|
||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@@ -1,125 +0,0 @@
|
|||||||
import React, { Fragment } from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import MediaQuery from 'react-responsive';
|
|
||||||
import { Dropdown, DropdownToggle, DropdownMenu, DropdownItem } from 'reactstrap';
|
|
||||||
import { seafileAPI } from '../../utils/seafile-api';
|
|
||||||
import { gettext } from '../../utils/constants';
|
|
||||||
import { Utils } from '../../utils/utils';
|
|
||||||
import Repo from '../../models/repo';
|
|
||||||
import toaster from '../../components/toast';
|
|
||||||
import ModalPortal from '../../components/modal-portal';
|
|
||||||
import CreateRepoDialog from '../../components/dialog/create-repo-dialog';
|
|
||||||
import ShareRepoDialog from '../../components/dialog/share-repo-dialog';
|
|
||||||
|
|
||||||
const propTypes = {
|
|
||||||
onShowSidePanel: PropTypes.func.isRequired,
|
|
||||||
addRepoItem: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
class TopToolbar extends React.Component {
|
|
||||||
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
this.state = {
|
|
||||||
libraryType: 'public',
|
|
||||||
isCreateMenuShow: false,
|
|
||||||
isCreateRepoDialogShow: false,
|
|
||||||
isSelectRepoDialpgShow: false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
onCreateRepo = (repo) => {
|
|
||||||
seafileAPI.createPublicRepo(repo).then(res => {
|
|
||||||
let object = {
|
|
||||||
repo_id: res.data.id,
|
|
||||||
repo_name: res.data.name,
|
|
||||||
permission: res.data.permission,
|
|
||||||
size: res.data.size,
|
|
||||||
owner_name: res.data.owner_name,
|
|
||||||
owner_email: res.data.owner,
|
|
||||||
mtime: res.data.mtime,
|
|
||||||
encrypted: res.data.encrypted,
|
|
||||||
};
|
|
||||||
let repo = new Repo(object);
|
|
||||||
this.props.addRepoItem(repo);
|
|
||||||
this.onCreateRepoToggle();
|
|
||||||
}).catch((error) => {
|
|
||||||
let errMessage = Utils.getErrorMsg(error);
|
|
||||||
toaster.danger(errMessage);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
onRepoSelectedHandler = (selectedRepoList) => {
|
|
||||||
selectedRepoList.forEach(repo => {
|
|
||||||
seafileAPI.selectOwnedRepoToPublic(repo.repo_id, {share_type: 'public', permission: repo.sharePermission}).then(() => {
|
|
||||||
this.props.addRepoItem(repo);
|
|
||||||
}).catch((error) => {
|
|
||||||
let errMessage = Utils.getErrorMsg(error);
|
|
||||||
toaster.danger(errMessage);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
onAddRepoToggle = () => {
|
|
||||||
this.setState({isCreateMenuShow: !this.state.isCreateMenuShow});
|
|
||||||
};
|
|
||||||
|
|
||||||
onCreateRepoToggle = () => {
|
|
||||||
this.setState({isCreateRepoDialogShow: !this.state.isCreateRepoDialogShow});
|
|
||||||
};
|
|
||||||
|
|
||||||
onSelectRepoToggle = () => {
|
|
||||||
this.setState({isSelectRepoDialpgShow: !this.state.isSelectRepoDialpgShow});
|
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<Fragment>
|
|
||||||
<div className="cur-view-toolbar">
|
|
||||||
<span className="sf2-icon-menu side-nav-toggle hidden-md-up d-md-none" title="Side Nav Menu" onClick={this.props.onShowSidePanel}></span>
|
|
||||||
<div className="operation">
|
|
||||||
<Dropdown isOpen={this.state.isCreateMenuShow} toggle={this.onAddRepoToggle}>
|
|
||||||
<MediaQuery query="(min-width: 768px)">
|
|
||||||
<DropdownToggle className='btn btn-secondary operation-item'>
|
|
||||||
<i className="sf3-font sf3-font-enlarge text-secondary mr-1"></i>{gettext('Add Library')}
|
|
||||||
</DropdownToggle>
|
|
||||||
</MediaQuery>
|
|
||||||
<MediaQuery query="(max-width: 767.8px)">
|
|
||||||
<DropdownToggle
|
|
||||||
tag="span"
|
|
||||||
className="sf2-icon-plus mobile-toolbar-icon"
|
|
||||||
title={gettext('Add Library')}
|
|
||||||
/>
|
|
||||||
</MediaQuery>
|
|
||||||
<DropdownMenu>
|
|
||||||
<DropdownItem onClick={this.onSelectRepoToggle}>{gettext('Share existing libraries')}</DropdownItem>
|
|
||||||
<DropdownItem onClick={this.onCreateRepoToggle}>{gettext('New Library')}</DropdownItem>
|
|
||||||
</DropdownMenu>
|
|
||||||
</Dropdown>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{this.state.isCreateRepoDialogShow && (
|
|
||||||
<ModalPortal>
|
|
||||||
<CreateRepoDialog
|
|
||||||
libraryType={this.state.libraryType}
|
|
||||||
onCreateToggle={this.onCreateRepoToggle}
|
|
||||||
onCreateRepo={this.onCreateRepo}
|
|
||||||
/>
|
|
||||||
</ModalPortal>
|
|
||||||
)}
|
|
||||||
{this.state.isSelectRepoDialpgShow && (
|
|
||||||
<ModalPortal>
|
|
||||||
<ShareRepoDialog
|
|
||||||
onRepoSelectedHandler={this.onRepoSelectedHandler}
|
|
||||||
onShareRepoDialogClose={this.onSelectRepoToggle}
|
|
||||||
/>
|
|
||||||
</ModalPortal>
|
|
||||||
)}
|
|
||||||
</Fragment>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
TopToolbar.propTypes = propTypes;
|
|
||||||
|
|
||||||
export default TopToolbar;
|
|
@@ -1188,12 +1188,15 @@ a.sf-popover-item {
|
|||||||
}
|
}
|
||||||
/********** Container ***********/
|
/********** Container ***********/
|
||||||
#header {
|
#header {
|
||||||
background: #F8FAFD;
|
background: #fff;
|
||||||
|
border-bottom: 1px solid #eee;
|
||||||
|
box-shadow: 0 2px 4px rgba(0,0,0, 10%);
|
||||||
|
z-index: 100;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 53px;
|
height: 49px;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
border-bottom: 1px solid #e8e8e8;
|
padding: .5rem 1rem;
|
||||||
padding: 8px 16px 4px;
|
/*display:flex;*/
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
@@ -662,6 +662,29 @@ a, a:hover { color: #ec8000; }
|
|||||||
padding-left: 0.25rem;
|
padding-left: 0.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* for folded 'side nav' */
|
||||||
|
.side-panel-folded .nav {
|
||||||
|
margin: 0!important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.side-panel-folded .heading,
|
||||||
|
.side-panel-folded .nav-item#share-admin-nav,
|
||||||
|
.side-panel-folded .nav-item .nav-text,
|
||||||
|
.side-panel-folded .nav-item .toggle-icon,
|
||||||
|
.side-panel-folded .nav-item .sub-nav {
|
||||||
|
display: none!important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.side-nav-bottom-toolbar {
|
||||||
|
height: 38px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.side-nav-bottom-toolbar:hover {
|
||||||
|
background: #f0f0f0;
|
||||||
|
}
|
||||||
|
/* folded 'side nav' ends */
|
||||||
|
|
||||||
.side-nav-footer {
|
.side-nav-footer {
|
||||||
display:flex;
|
display:flex;
|
||||||
flex-shrink:0;
|
flex-shrink:0;
|
||||||
@@ -705,6 +728,11 @@ a, a:hover { color: #ec8000; }
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.side-panel-folded .nav .nav-item .nav-link {
|
||||||
|
height: 36px;
|
||||||
|
}
|
||||||
|
|
||||||
.side-nav-con .active .sharp,
|
.side-nav-con .active .sharp,
|
||||||
.side-nav-con .active .nav-text {
|
.side-nav-con .active .nav-text {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
@@ -1483,12 +1511,6 @@ a.table-sort-op:hover {
|
|||||||
border: none;
|
border: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.side-panel .side-nav .side-nav-con .nav-item {
|
|
||||||
background-color: #f8f8f8;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.rotate-90 {
|
.rotate-90 {
|
||||||
transform: rotate(90deg);
|
transform: rotate(90deg);
|
||||||
}
|
}
|
||||||
|
15
media/img/close-sidebar.svg
Normal file
15
media/img/close-sidebar.svg
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Generator: Adobe Illustrator 21.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||||
|
<svg version="1.1" id="图层_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||||
|
viewBox="0 0 32 32" style="enable-background:new 0 0 32 32;" xml:space="preserve">
|
||||||
|
<style type="text/css">
|
||||||
|
.st0{fill:#999999;}
|
||||||
|
</style>
|
||||||
|
<title>close-sidebar</title>
|
||||||
|
<g id="close-sidebar">
|
||||||
|
<path id="形状结合" class="st0" d="M17.5,2.4c0.6,0.6,0.6,1.5,0,2.1L5.8,16l11.6,11.5c0.6,0.6,0.6,1.5,0,2.1
|
||||||
|
c-0.3,0.3-0.7,0.4-1.1,0.4c-0.4,0-0.8-0.2-1.1-0.4L2.5,17C2.2,16.8,2,16.4,2,16c0-0.4,0.2-0.8,0.5-1.1L15.3,2.4
|
||||||
|
C15.9,1.9,16.8,1.9,17.5,2.4z M29.5,2.4c0.6,0.6,0.6,1.5,0,2.1L17.9,16l11.6,11.5c0.6,0.6,0.6,1.5,0,2.1c-0.3,0.3-0.7,0.4-1.1,0.4
|
||||||
|
c-0.4,0-0.8-0.2-1.1-0.4L14.5,17c-0.3-0.3-0.5-0.7-0.5-1.1c0-0.4,0.2-0.8,0.5-1.1L27.3,2.4C28,1.9,28.9,1.9,29.5,2.4z"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 932 B |
15
media/img/open-sidebar.svg
Normal file
15
media/img/open-sidebar.svg
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Generator: Adobe Illustrator 21.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||||
|
<svg version="1.1" id="图层_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||||
|
viewBox="0 0 32 32" style="enable-background:new 0 0 32 32;" xml:space="preserve">
|
||||||
|
<style type="text/css">
|
||||||
|
.st0{fill:#999999;}
|
||||||
|
</style>
|
||||||
|
<title>open-sidebar</title>
|
||||||
|
<g id="open-sidebar">
|
||||||
|
<path id="形状结合" class="st0" d="M14.5,2.4c-0.6,0.6-0.6,1.5,0,2.1L26.2,16L14.5,27.5C14,28,14,29,14.5,29.6
|
||||||
|
c0.3,0.3,0.7,0.4,1.1,0.4c0.4,0,0.8-0.2,1.1-0.4L29.5,17c0.3-0.3,0.5-0.7,0.5-1.1c0-0.4-0.2-0.8-0.5-1.1L16.7,2.4
|
||||||
|
C16.1,1.9,15.2,1.9,14.5,2.4z M2.5,2.4C1.8,3,1.8,4,2.5,4.6L14.1,16L2.5,27.5c-0.6,0.6-0.6,1.5,0,2.1C2.7,29.8,3.1,30,3.6,30
|
||||||
|
c0.4,0,0.8-0.2,1.1-0.4L17.5,17c0.3-0.3,0.5-0.7,0.5-1.1c0-0.4-0.2-0.8-0.5-1.1L4.7,2.4C4,1.9,3.1,1.9,2.5,2.4z"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 923 B |
Reference in New Issue
Block a user