1
0
mirror of https://github.com/haiwen/seahub.git synced 2025-09-23 12:27:48 +00:00
* [top bar, side panel] modified UI of the top bar; redesigned the side panel(moved the content in the bottom to the side nav)

* [font icons] updated the 'department' & 'invite Guest' icons

* [linked devices] moved it from the 'home' side panel to the 'settings' page

* ['settings' page] redesigned the empty tip for 'linked devices'
This commit is contained in:
llj
2024-05-15 17:58:18 +08:00
committed by GitHub
parent 514200164e
commit 7bfc7bae0c
16 changed files with 346 additions and 84 deletions

View File

@@ -1,7 +1,9 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Link } from '@gatsbyjs/reach-router';
import { gettext, siteRoot, canAddRepo, canGenerateShareLink, canGenerateUploadLink, canInvitePeople } from '../utils/constants';
import { gettext, siteRoot, canAddRepo, canGenerateShareLink, canGenerateUploadLink, canInvitePeople,
enableTC, sideNavFooterCustomHtml, additionalAppBottomLinks
} from '../utils/constants';
import { seafileAPI } from '../utils/seafile-api';
import { Utils } from '../utils/utils';
import toaster from './toast';
@@ -83,7 +85,7 @@ class MainSideNav extends React.Component {
className={`nav-link ellipsis ${this.getActiveClass(item.name)}`}
onClick={(e) => this.tabItemClick(e, item.name, item.id)}
>
<span className={`${item.parent_group_id == 0 ? 'sf3-font-group sf3-font' : 'fas fa-building'} nav-icon`} aria-hidden="true"></span>
<span className={`${item.parent_group_id == 0 ? 'sf3-font-group' : 'sf3-font-department'} sf3-font nav-icon`} aria-hidden="true"></span>
<span className="nav-text">{item.name}</span>
</Link>
</li>
@@ -181,6 +183,7 @@ class MainSideNav extends React.Component {
return (
<div className="side-nav">
<div className="side-nav-con">
<h2 className="mb-2 px-2 font-weight-normal heading">{gettext('Workspace')}</h2>
<ul className="nav nav-pills flex-column nav-container">
<li className="nav-item flex-column" id="files">
<Link to={ siteRoot + 'libraries/' } className={`nav-link ellipsis ${this.getActiveClass('libraries')}`} title={gettext('Files')} onClick={(e) => this.tabItemClick(e, 'libraries')}>
@@ -235,19 +238,13 @@ class MainSideNav extends React.Component {
<span className="nav-text">{gettext('Wikis')}</span>
</Link>
</li>
{canInvitePeople &&
<li className="nav-item">
<Link className={`nav-link ellipsis ${this.getActiveClass('linked-devices')}`} to={siteRoot + 'linked-devices/'} title={gettext('Linked Devices')} onClick={(e) => this.tabItemClick(e, 'linked-devices')}>
<span className="sf3-font-devices sf3-font" aria-hidden="true"></span>
<span className="nav-text">{gettext('Linked Devices')}</span>
<Link className={`nav-link ellipsis ${this.getActiveClass('invitations')}`} to={siteRoot + 'invitations/'} title={gettext('Invite Guest')} onClick={(e) => this.tabItemClick(e, 'invitations')}>
<span className="sf3-font-invite-visitors sf3-font" aria-hidden="true"></span>
<span className="nav-text">{gettext('Invite Guest')}</span>
</Link>
</li>
{canInvitePeople &&
<li className="nav-item">
<Link className={`nav-link ellipsis ${this.getActiveClass('invitations')}`} to={siteRoot + 'invitations/'} title={gettext('Invite Guest')} onClick={(e) => this.tabItemClick(e, 'invitations')}>
<span className="sf2-icon-invite" aria-hidden="true"></span>
<span className="nav-text">{gettext('Invite Guest')}</span>
</Link>
</li>
}
<li className="nav-item flex-column" id="share-admin-nav">
<a className="nav-link ellipsis" title={gettext('Share Admin')} onClick={this.shExtend}>
@@ -259,6 +256,47 @@ class MainSideNav extends React.Component {
</li>
{customNavItems && this.renderCustomNavItems()}
</ul>
<h2 className="mb-2 pt-1 px-2 font-weight-normal heading">{gettext('Help and resources')}</h2>
{sideNavFooterCustomHtml ? (
<div className='side-nav-footer' dangerouslySetInnerHTML={{__html: sideNavFooterCustomHtml}}></div>
) : (
<ul className="nav nav-pills flex-column nav-container">
<li className="nav-item">
<a className={'nav-link'} href={siteRoot + 'help/'} title={gettext('Help')}>
<span className="sf3-font-help sf3-font" aria-hidden="true"></span>
<span className="nav-text">{gettext('Help')}</span>
</a>
</li>
{enableTC &&
<li className="nav-item">
<a href={`${siteRoot}terms/`} className="nav-link">
<span className="sf3-font-terms sf3-font" aria-hidden="true"></span>
<span className="nav-text">{gettext('Terms')}</span>
</a>
</li>
}
{additionalAppBottomLinks && (
<>
{Object.keys(additionalAppBottomLinks).map((key, index) => {
return (
<a className="nav-link" href={additionalAppBottomLinks[key]}>
<span className="sf3-font-terms sf3-font" aria-hidden="true"></span>
<span className="nav-text">{key}</span>
</a>
);
})}
</>
)}
<li className="nav-item">
<a href={siteRoot + 'download_client_program/'} className="nav-link">
<span className="sf3-font-devices sf3-font" aria-hidden="true"></span>
<span className="nav-text">{gettext('Clients')}</span>
</a>
</li>
</ul>
)
}
</div>
</div>
);

View File

@@ -1,57 +0,0 @@
import React, { Fragment } from 'react';
import { gettext, siteRoot, enableTC, sideNavFooterCustomHtml, additionalAppBottomLinks } from '../utils/constants';
import ModalPortal from './modal-portal';
import AboutDialog from './dialog/about-dialog';
class SideNavFooter extends React.Component {
constructor(props) {
super(props);
this.state = {
isAboutDialogShow: false,
};
}
onAboutDialogToggle = (e) => {
e.preventDefault();
this.setState({isAboutDialogShow: !this.state.isAboutDialogShow});
};
renderExternalAppLinks = () => {
if (additionalAppBottomLinks && (typeof additionalAppBottomLinks) === 'object') {
let keys = Object.keys(additionalAppBottomLinks);
return keys.map((key, index) => {
return <a key={index} className="item" href={additionalAppBottomLinks[key]}>{key}</a>;
});
}
return null;
};
render() {
if (sideNavFooterCustomHtml) {
return (<div className='side-nav-footer' dangerouslySetInnerHTML={{__html: sideNavFooterCustomHtml}}></div>);
}
return (
<Fragment>
<div className="side-nav-footer flex-wrap">
<a href={siteRoot + 'help/'} target="_blank" rel="noopener noreferrer" className="item">{gettext('Help')}</a>
<a href="#" className="item" onClick={this.onAboutDialogToggle}>{gettext('About')}</a>
{enableTC && <a href={`${siteRoot}terms/`} className="item">{gettext('Terms')}</a>}
{this.renderExternalAppLinks()}
<a href={siteRoot + 'download_client_program/'} className={`item ${additionalAppBottomLinks ? '' : 'last-item'}`}>
<span aria-hidden="true" className="sf2-icon-monitor vam"></span>{' '}
<span className="vam">{gettext('Clients')}</span>
</a>
</div>
{this.state.isAboutDialogShow && (
<ModalPortal>
<AboutDialog onCloseAboutDialog={this.onAboutDialogToggle} />
</ModalPortal>
)}
</Fragment>
);
}
}
export default SideNavFooter;

View File

@@ -2,7 +2,6 @@ import React from 'react';
import PropTypes from 'prop-types';
import Logo from './logo';
import MainSideNav from './main-side-nav';
import SideNavFooter from './side-nav-footer';
const propTypes = {
isSidePanelClosed: PropTypes.bool.isRequired,
@@ -22,9 +21,6 @@ class SidePanel extends React.Component {
<div className="side-panel-center">
<MainSideNav tabItemClick={this.props.tabItemClick} currentTab={this.props.currentTab} />
</div>
<div className="side-panel-footer">
<SideNavFooter />
</div>
</div>
);
}

View File

@@ -0,0 +1,240 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import moment from 'moment';
import { Dropdown, DropdownToggle, DropdownItem } from 'reactstrap';
import { seafileAPI } from '../../utils/seafile-api';
import { gettext } from '../../utils/constants';
import { Utils } from '../../utils/utils';
import toaster from '../../components/toast';
import ConfirmUnlinkDeviceDialog from '../../components/dialog/confirm-unlink-device';
class Content extends Component {
render() {
const { loading, errorMsg, items } = this.props;
if (loading) {
return <span className="loading-icon loading-tip"></span>;
} else if (errorMsg) {
return <p className="error text-center">{errorMsg}</p>;
} else {
const emptyTip = (
<p>{gettext('No linked devices. You have not accessed your files with any client (desktop or mobile) yet. Configure clients on your devices to access your data more comfortably.')}</p>
);
const desktopThead = (
<thead>
<tr>
<th width="13%">{gettext('Platform')}</th>
<th width="30%">{gettext('Device Name')}</th>
<th width="30%">{gettext('IP')}</th>
<th width="17%">{gettext('Last Access')}</th>
<th width="10%"></th>
</tr>
</thead>
);
const mobileThead = (
<thead>
<tr>
<th width="92%"></th>
<th width="8%"></th>
</tr>
</thead>
);
const isDesktop = Utils.isDesktop();
return items.length ? (
<table className={`table-hover ${isDesktop ? '': 'table-thead-hidden'}`}>
{isDesktop ? desktopThead : mobileThead}
<tbody>
{items.map((item, index) => {
return <Item key={index} data={item} isDesktop={isDesktop} />;
})}
</tbody>
</table>
): emptyTip;
}
}
}
Content.propTypes = {
loading: PropTypes.bool.isRequired,
errorMsg: PropTypes.string.isRequired,
items: PropTypes.array.isRequired
};
class Item extends Component {
constructor(props) {
super(props);
this.state = {
isOpMenuOpen: false, // for mobile
isOpIconShown: false,
unlinked: false,
isConfirmUnlinkDialogOpen: false
};
}
toggleOpMenu = () => {
this.setState({
isOpMenuOpen: !this.state.isOpMenuOpen
});
};
handleMouseOver = () => {
this.setState({
isOpIconShown: true
});
};
handleMouseOut = () => {
this.setState({
isOpIconShown: false
});
};
toggleDialog = () => {
this.setState({
isConfirmUnlinkDialogOpen: !this.state.isConfirmUnlinkDialogOpen
});
};
handleClick = (e) => {
e.preventDefault();
const data = this.props.data;
if (data.is_desktop_client) {
this.toggleDialog();
} else {
const wipeDevice = true;
this.unlinkDevice(wipeDevice);
}
};
unlinkDevice = (wipeDevice) => {
const data = this.props.data;
seafileAPI.unlinkDevice(data.platform, data.device_id, wipeDevice).then((res) => {
this.setState({
unlinked: true
});
let msg = gettext('Successfully unlinked %(name)s.');
msg = msg.replace('%(name)s', data.device_name);
toaster.success(msg);
}).catch((error) => {
let errMessage = Utils.getErrorMsg(error);
toaster.danger(errMessage);
});
};
render() {
if (this.state.unlinked) {
return null;
}
const data = this.props.data;
let opClasses = 'sf2-icon-delete unlink-device action-icon';
opClasses += this.state.isOpIconShown ? '' : ' invisible';
const desktopItem = (
<tr onMouseOver={this.handleMouseOver} onMouseOut={this.handleMouseOut} onFocus={this.handleMouseOver}>
<td>{data.platform}</td>
<td>{data.device_name}</td>
<td>{data.last_login_ip}</td>
<td>{moment(data.last_accessed).fromNow()}</td>
<td>
<a href="#" className={opClasses} title={gettext('Unlink')} role="button" aria-label={gettext('Unlink')} onClick={this.handleClick}></a>
</td>
</tr>
);
const mobileItem = (
<tr>
<td>
{data.device_name}<br />
<span className="item-meta-info">{data.last_login_ip}</span>
<span className="item-meta-info">{moment(data.last_accessed).fromNow()}</span>
<span className="item-meta-info">{data.platform}</span>
</td>
<td>
<Dropdown isOpen={this.state.isOpMenuOpen} toggle={this.toggleOpMenu}>
<DropdownToggle
tag="i"
className="sf-dropdown-toggle fa fa-ellipsis-v ml-0"
title={gettext('More operations')}
aria-label={gettext('More operations')}
data-toggle="dropdown"
aria-expanded={this.state.isOpMenuOpen}
/>
<div className={this.state.isOpMenuOpen ? '' : 'd-none'} onClick={this.toggleOpMenu}>
<div className="mobile-operation-menu-bg-layer"></div>
<div className="mobile-operation-menu">
<DropdownItem className="mobile-menu-item" onClick={this.handleClick}>{gettext('Unlink')}</DropdownItem>
</div>
</div>
</Dropdown>
</td>
</tr>
);
return (
<React.Fragment>
{this.props.isDesktop ? desktopItem : mobileItem}
{this.state.isConfirmUnlinkDialogOpen &&
<ConfirmUnlinkDeviceDialog
executeOperation={this.unlinkDevice}
toggleDialog={this.toggleDialog}
/>
}
</React.Fragment>
);
}
}
Item.propTypes = {
isDesktop: PropTypes.bool.isRequired,
data: PropTypes.object.isRequired,
};
class LinkedDevices extends Component {
constructor(props) {
super(props);
this.state = {
loading: true,
errorMsg: '',
items: []
};
}
componentDidMount() {
seafileAPI.listLinkedDevices().then((res) => {
this.setState({
loading: false,
items: res.data
});
}).catch((error) => {
this.setState({
loading: false,
errorMsg: Utils.getErrorMsg(error, true) // true: show login tip if 403
});
});
}
render() {
const { loading, errorMsg, items } = this.state;
return (
<div className="setting-item" id="linked-devices">
<h3 className="setting-item-heading">{gettext('Linked Devices')}</h3>
<div className="cur-view-content">
<Content
loading={loading}
errorMsg={errorMsg}
items={items}
/>
</div>
</div>
);
}
}
export default LinkedDevices;