feat: Organize sidebar (#1834)

* chore: Flip chinese label in language select

* feat: Add settings to sidebar, organize secondary items to bottom
This commit is contained in:
Tom Moor
2021-01-21 07:22:20 -08:00
committed by GitHub
parent 993aad004e
commit 70626ffff0
6 changed files with 253 additions and 181 deletions

View File

@ -1,28 +1,52 @@
// @flow
import { observable } from "mobx";
import { observer } from "mobx-react";
import * as React from "react";
import styled from "styled-components";
import useWindowSize from "hooks/useWindowSize";
type Props = {
type Props = {|
shadow?: boolean,
};
topShadow?: boolean,
bottomShadow?: boolean,
|};
@observer
class Scrollable extends React.Component<Props> {
@observable shadow: boolean = false;
function Scrollable({ shadow, topShadow, bottomShadow, ...rest }: Props) {
const ref = React.useRef<?HTMLDivElement>();
const [topShadowVisible, setTopShadow] = React.useState(false);
const [bottomShadowVisible, setBottomShadow] = React.useState(false);
const { height } = useWindowSize();
handleScroll = (ev: SyntheticMouseEvent<HTMLDivElement>) => {
this.shadow = !!(this.props.shadow && ev.currentTarget.scrollTop > 0);
};
const updateShadows = React.useCallback(() => {
const c = ref.current;
if (!c) return;
render() {
const { shadow, ...rest } = this.props;
const scrollTop = c.scrollTop;
const tsv = !!((shadow || topShadow) && scrollTop > 0);
if (tsv !== topShadowVisible) {
setTopShadow(tsv);
}
return (
<Wrapper onScroll={this.handleScroll} shadow={this.shadow} {...rest} />
);
}
const wrapperHeight = c.scrollHeight - c.clientHeight;
const bsv = !!((shadow || bottomShadow) && wrapperHeight - scrollTop !== 0);
if (bsv !== bottomShadowVisible) {
setBottomShadow(bsv);
}
}, [shadow, topShadow, bottomShadow, topShadowVisible, bottomShadowVisible]);
React.useEffect(() => {
updateShadows();
}, [height, updateShadows]);
return (
<Wrapper
ref={ref}
onScroll={updateShadows}
$topShadowVisible={topShadowVisible}
$bottomShadowVisible={bottomShadowVisible}
{...rest}
/>
);
}
const Wrapper = styled.div`
@ -31,9 +55,20 @@ const Wrapper = styled.div`
overflow-x: hidden;
overscroll-behavior: none;
-webkit-overflow-scrolling: touch;
box-shadow: ${(props) =>
props.shadow ? "0 1px inset rgba(0,0,0,.1)" : "none"};
transition: all 250ms ease-in-out;
box-shadow: ${(props) => {
if (props.$topShadowVisible && props.$bottomShadowVisible) {
return "0 1px inset rgba(0,0,0,.1), 0 -1px inset rgba(0,0,0,.1)";
}
if (props.$topShadowVisible) {
return "0 1px inset rgba(0,0,0,.1)";
}
if (props.$bottomShadowVisible) {
return "0 -1px inset rgba(0,0,0,.1)";
}
return "none";
}};
transition: all 100ms ease-in-out;
`;
export default Scrollable;
export default observer(Scrollable);

View File

@ -1,6 +1,5 @@
// @flow
import { observable } from "mobx";
import { observer, inject } from "mobx-react";
import { observer } from "mobx-react";
import {
ArchiveIcon,
HomeIcon,
@ -10,14 +9,11 @@ import {
ShapesIcon,
TrashIcon,
PlusIcon,
SettingsIcon,
} from "outline-icons";
import * as React from "react";
import { withTranslation, type TFunction } from "react-i18next";
import { useTranslation } from "react-i18next";
import styled from "styled-components";
import AuthStore from "stores/AuthStore";
import DocumentsStore from "stores/DocumentsStore";
import PoliciesStore from "stores/PoliciesStore";
import CollectionNew from "scenes/CollectionNew";
import Invite from "scenes/Invite";
import Flex from "components/Flex";
@ -29,176 +25,184 @@ import Collections from "./components/Collections";
import HeaderBlock from "./components/HeaderBlock";
import Section from "./components/Section";
import SidebarLink from "./components/SidebarLink";
import useStores from "hooks/useStores";
import AccountMenu from "menus/AccountMenu";
type Props = {
auth: AuthStore,
documents: DocumentsStore,
policies: PoliciesStore,
t: TFunction,
};
function MainSidebar() {
const { t } = useTranslation();
const { policies, auth, documents } = useStores();
const [inviteModalOpen, setInviteModalOpen] = React.useState(false);
const [
createCollectionModalOpen,
setCreateCollectionModalOpen,
] = React.useState(false);
@observer
class MainSidebar extends React.Component<Props> {
@observable inviteModalOpen = false;
@observable createCollectionModalOpen = false;
React.useEffect(() => {
documents.fetchDrafts();
documents.fetchTemplates();
}, [documents]);
componentDidMount() {
this.props.documents.fetchDrafts();
this.props.documents.fetchTemplates();
}
const handleCreateCollectionModalOpen = React.useCallback(
(ev: SyntheticEvent<>) => {
ev.preventDefault();
setCreateCollectionModalOpen(true);
},
[]
);
handleCreateCollectionModalOpen = (ev: SyntheticEvent<>) => {
const handleCreateCollectionModalClose = React.useCallback(
(ev: SyntheticEvent<>) => {
ev.preventDefault();
setCreateCollectionModalOpen(false);
},
[]
);
const handleInviteModalOpen = React.useCallback((ev: SyntheticEvent<>) => {
ev.preventDefault();
this.createCollectionModalOpen = true;
};
setInviteModalOpen(true);
}, []);
handleCreateCollectionModalClose = (ev: SyntheticEvent<>) => {
this.createCollectionModalOpen = false;
};
handleInviteModalOpen = (ev: SyntheticEvent<>) => {
const handleInviteModalClose = React.useCallback((ev: SyntheticEvent<>) => {
ev.preventDefault();
this.inviteModalOpen = true;
};
setInviteModalOpen(false);
}, []);
handleInviteModalClose = () => {
this.inviteModalOpen = false;
};
const { user, team } = auth;
if (!user || !team) return null;
render() {
const { auth, documents, policies, t } = this.props;
const { user, team } = auth;
if (!user || !team) return null;
const can = policies.abilities(team.id);
const can = policies.abilities(team.id);
return (
<Sidebar>
<AccountMenu>
{(props) => (
<HeaderBlock
{...props}
subheading={user.name}
teamName={team.name}
logoUrl={team.avatarUrl}
showDisclosure
return (
<Sidebar>
<AccountMenu>
{(props) => (
<HeaderBlock
{...props}
subheading={user.name}
teamName={team.name}
logoUrl={team.avatarUrl}
showDisclosure
/>
)}
</AccountMenu>
<Flex auto column>
<Scrollable shadow>
<Section>
<SidebarLink
to="/home"
icon={<HomeIcon color="currentColor" />}
exact={false}
label={t("Home")}
/>
)}
</AccountMenu>
<Flex auto column>
<Scrollable shadow>
<Section>
<SidebarLink
to={{
pathname: "/search",
state: { fromMenu: true },
}}
icon={<SearchIcon color="currentColor" />}
label={t("Search")}
exact={false}
/>
<SidebarLink
to="/starred"
icon={<StarredIcon color="currentColor" />}
exact={false}
label={t("Starred")}
/>
<SidebarLink
to="/templates"
icon={<ShapesIcon color="currentColor" />}
exact={false}
label={t("Templates")}
active={documents.active ? documents.active.template : undefined}
/>
<SidebarLink
to="/drafts"
icon={<EditIcon color="currentColor" />}
label={
<Drafts align="center">
{t("Drafts")}
{documents.totalDrafts > 0 && (
<Bubble count={documents.totalDrafts} />
)}
</Drafts>
}
active={
documents.active
? !documents.active.publishedAt &&
!documents.active.isDeleted &&
!documents.active.isTemplate
: undefined
}
/>
</Section>
<Section>
<Collections onCreateCollection={handleCreateCollectionModalOpen} />
</Section>
</Scrollable>
<Secondary>
<Section>
<SidebarLink
to="/archive"
icon={<ArchiveIcon color="currentColor" />}
exact={false}
label={t("Archive")}
active={
documents.active
? documents.active.isArchived && !documents.active.isDeleted
: undefined
}
/>
<SidebarLink
to="/trash"
icon={<TrashIcon color="currentColor" />}
exact={false}
label={t("Trash")}
active={documents.active ? documents.active.isDeleted : undefined}
/>
<SidebarLink
to="/settings"
icon={<SettingsIcon color="currentColor" />}
exact={false}
label={t("Settings")}
/>
{can.invite && (
<SidebarLink
to="/home"
icon={<HomeIcon color="currentColor" />}
exact={false}
label={t("Home")}
to="/settings/people"
onClick={handleInviteModalOpen}
icon={<PlusIcon color="currentColor" />}
label={t("Invite people…")}
/>
<SidebarLink
to={{
pathname: "/search",
state: { fromMenu: true },
}}
icon={<SearchIcon color="currentColor" />}
label={t("Search")}
exact={false}
/>
<SidebarLink
to="/starred"
icon={<StarredIcon color="currentColor" />}
exact={false}
label={t("Starred")}
/>
<SidebarLink
to="/templates"
icon={<ShapesIcon color="currentColor" />}
exact={false}
label={t("Templates")}
active={
documents.active ? documents.active.template : undefined
}
/>
<SidebarLink
to="/drafts"
icon={<EditIcon color="currentColor" />}
label={
<Drafts align="center">
{t("Drafts")}
{documents.totalDrafts > 0 && (
<Bubble count={documents.totalDrafts} />
)}
</Drafts>
}
active={
documents.active
? !documents.active.publishedAt &&
!documents.active.isDeleted &&
!documents.active.isTemplate
: undefined
}
/>
</Section>
<Section>
<Collections
onCreateCollection={this.handleCreateCollectionModalOpen}
/>
</Section>
<Section>
<SidebarLink
to="/archive"
icon={<ArchiveIcon color="currentColor" />}
exact={false}
label={t("Archive")}
active={
documents.active
? documents.active.isArchived && !documents.active.isDeleted
: undefined
}
/>
<SidebarLink
to="/trash"
icon={<TrashIcon color="currentColor" />}
exact={false}
label={t("Trash")}
active={
documents.active ? documents.active.isDeleted : undefined
}
/>
{can.invite && (
<SidebarLink
to="/settings/people"
onClick={this.handleInviteModalOpen}
icon={<PlusIcon color="currentColor" />}
label={t("Invite people…")}
/>
)}
</Section>
</Scrollable>
</Flex>
<Modal
title={t("Invite people")}
onRequestClose={this.handleInviteModalClose}
isOpen={this.inviteModalOpen}
>
<Invite onSubmit={this.handleInviteModalClose} />
</Modal>
<Modal
title={t("Create a collection")}
onRequestClose={this.handleCreateCollectionModalClose}
isOpen={this.createCollectionModalOpen}
>
<CollectionNew onSubmit={this.handleCreateCollectionModalClose} />
</Modal>
</Sidebar>
);
}
)}
</Section>
</Secondary>
</Flex>
<Modal
title={t("Invite people")}
onRequestClose={handleInviteModalClose}
isOpen={inviteModalOpen}
>
<Invite onSubmit={handleInviteModalClose} />
</Modal>
<Modal
title={t("Create a collection")}
onRequestClose={handleCreateCollectionModalClose}
isOpen={createCollectionModalOpen}
>
<CollectionNew onSubmit={handleCreateCollectionModalClose} />
</Modal>
</Sidebar>
);
}
const Secondary = styled.div`
overflow-x: hidden;
flex-shrink: 0;
`;
const Drafts = styled(Flex)`
height: 24px;
`;
export default withTranslation()<MainSidebar>(
inject("documents", "policies", "auth")(MainSidebar)
);
export default observer(MainSidebar);

View File

@ -58,7 +58,7 @@ function SettingsSidebar() {
/>
<Flex auto column>
<Scrollable shadow>
<Scrollable topShadow>
<Section>
<Header>{t("Account")}</Header>
<SidebarLink

View File

@ -7,6 +7,7 @@ const Section = styled(Flex)`
flex-direction: column;
margin: 24px 8px;
min-width: ${(props) => props.theme.sidebarMinWidth}px;
flex-shrink: 0;
`;
export default Section;

View File

@ -0,0 +1,31 @@
// @flow
import { debounce } from "lodash";
import * as React from "react";
export default function useWindowSize() {
const [windowSize, setWindowSize] = React.useState({
width: undefined,
height: undefined,
});
React.useEffect(() => {
// Handler to call on window resize
const handleResize = debounce(() => {
// Set window width/height to state
setWindowSize({
width: window.innerWidth,
height: window.innerHeight,
});
}, 100);
// Add event listener
window.addEventListener("resize", handleResize);
// Call handler right away so state gets updated with initial window size
handleResize();
return () => window.removeEventListener("resize", handleResize);
}, []);
return windowSize;
}

View File

@ -94,9 +94,11 @@
"Invite people": "Invite people",
"Create a collection": "Create a collection",
"Return to App": "Return to App",
"Account": "Account",
"Profile": "Profile",
"Notifications": "Notifications",
"API Tokens": "API Tokens",
"Team": "Team",
"Details": "Details",
"Security": "Security",
"People": "People",
@ -109,7 +111,6 @@
"System": "System",
"Light": "Light",
"Dark": "Dark",
"Account": "Account",
"Settings": "Settings",
"API documentation": "API documentation",
"Changelog": "Changelog",