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:
@ -1,28 +1,52 @@
|
|||||||
// @flow
|
// @flow
|
||||||
import { observable } from "mobx";
|
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
|
import useWindowSize from "hooks/useWindowSize";
|
||||||
|
|
||||||
type Props = {
|
type Props = {|
|
||||||
shadow?: boolean,
|
shadow?: boolean,
|
||||||
};
|
topShadow?: boolean,
|
||||||
|
bottomShadow?: boolean,
|
||||||
|
|};
|
||||||
|
|
||||||
@observer
|
function Scrollable({ shadow, topShadow, bottomShadow, ...rest }: Props) {
|
||||||
class Scrollable extends React.Component<Props> {
|
const ref = React.useRef<?HTMLDivElement>();
|
||||||
@observable shadow: boolean = false;
|
const [topShadowVisible, setTopShadow] = React.useState(false);
|
||||||
|
const [bottomShadowVisible, setBottomShadow] = React.useState(false);
|
||||||
|
const { height } = useWindowSize();
|
||||||
|
|
||||||
handleScroll = (ev: SyntheticMouseEvent<HTMLDivElement>) => {
|
const updateShadows = React.useCallback(() => {
|
||||||
this.shadow = !!(this.props.shadow && ev.currentTarget.scrollTop > 0);
|
const c = ref.current;
|
||||||
};
|
if (!c) return;
|
||||||
|
|
||||||
render() {
|
const scrollTop = c.scrollTop;
|
||||||
const { shadow, ...rest } = this.props;
|
const tsv = !!((shadow || topShadow) && scrollTop > 0);
|
||||||
|
if (tsv !== topShadowVisible) {
|
||||||
|
setTopShadow(tsv);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 (
|
return (
|
||||||
<Wrapper onScroll={this.handleScroll} shadow={this.shadow} {...rest} />
|
<Wrapper
|
||||||
|
ref={ref}
|
||||||
|
onScroll={updateShadows}
|
||||||
|
$topShadowVisible={topShadowVisible}
|
||||||
|
$bottomShadowVisible={bottomShadowVisible}
|
||||||
|
{...rest}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const Wrapper = styled.div`
|
const Wrapper = styled.div`
|
||||||
@ -31,9 +55,20 @@ const Wrapper = styled.div`
|
|||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
overscroll-behavior: none;
|
overscroll-behavior: none;
|
||||||
-webkit-overflow-scrolling: touch;
|
-webkit-overflow-scrolling: touch;
|
||||||
box-shadow: ${(props) =>
|
box-shadow: ${(props) => {
|
||||||
props.shadow ? "0 1px inset rgba(0,0,0,.1)" : "none"};
|
if (props.$topShadowVisible && props.$bottomShadowVisible) {
|
||||||
transition: all 250ms ease-in-out;
|
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);
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
// @flow
|
// @flow
|
||||||
import { observable } from "mobx";
|
import { observer } from "mobx-react";
|
||||||
import { observer, inject } from "mobx-react";
|
|
||||||
import {
|
import {
|
||||||
ArchiveIcon,
|
ArchiveIcon,
|
||||||
HomeIcon,
|
HomeIcon,
|
||||||
@ -10,14 +9,11 @@ import {
|
|||||||
ShapesIcon,
|
ShapesIcon,
|
||||||
TrashIcon,
|
TrashIcon,
|
||||||
PlusIcon,
|
PlusIcon,
|
||||||
|
SettingsIcon,
|
||||||
} from "outline-icons";
|
} from "outline-icons";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { withTranslation, type TFunction } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import styled from "styled-components";
|
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 CollectionNew from "scenes/CollectionNew";
|
||||||
import Invite from "scenes/Invite";
|
import Invite from "scenes/Invite";
|
||||||
import Flex from "components/Flex";
|
import Flex from "components/Flex";
|
||||||
@ -29,45 +25,49 @@ import Collections from "./components/Collections";
|
|||||||
import HeaderBlock from "./components/HeaderBlock";
|
import HeaderBlock from "./components/HeaderBlock";
|
||||||
import Section from "./components/Section";
|
import Section from "./components/Section";
|
||||||
import SidebarLink from "./components/SidebarLink";
|
import SidebarLink from "./components/SidebarLink";
|
||||||
|
import useStores from "hooks/useStores";
|
||||||
import AccountMenu from "menus/AccountMenu";
|
import AccountMenu from "menus/AccountMenu";
|
||||||
|
|
||||||
type Props = {
|
function MainSidebar() {
|
||||||
auth: AuthStore,
|
const { t } = useTranslation();
|
||||||
documents: DocumentsStore,
|
const { policies, auth, documents } = useStores();
|
||||||
policies: PoliciesStore,
|
const [inviteModalOpen, setInviteModalOpen] = React.useState(false);
|
||||||
t: TFunction,
|
const [
|
||||||
};
|
createCollectionModalOpen,
|
||||||
|
setCreateCollectionModalOpen,
|
||||||
|
] = React.useState(false);
|
||||||
|
|
||||||
@observer
|
React.useEffect(() => {
|
||||||
class MainSidebar extends React.Component<Props> {
|
documents.fetchDrafts();
|
||||||
@observable inviteModalOpen = false;
|
documents.fetchTemplates();
|
||||||
@observable createCollectionModalOpen = false;
|
}, [documents]);
|
||||||
|
|
||||||
componentDidMount() {
|
const handleCreateCollectionModalOpen = React.useCallback(
|
||||||
this.props.documents.fetchDrafts();
|
(ev: SyntheticEvent<>) => {
|
||||||
this.props.documents.fetchTemplates();
|
|
||||||
}
|
|
||||||
|
|
||||||
handleCreateCollectionModalOpen = (ev: SyntheticEvent<>) => {
|
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
this.createCollectionModalOpen = true;
|
setCreateCollectionModalOpen(true);
|
||||||
};
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
handleCreateCollectionModalClose = (ev: SyntheticEvent<>) => {
|
const handleCreateCollectionModalClose = React.useCallback(
|
||||||
this.createCollectionModalOpen = false;
|
(ev: SyntheticEvent<>) => {
|
||||||
};
|
|
||||||
|
|
||||||
handleInviteModalOpen = (ev: SyntheticEvent<>) => {
|
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
this.inviteModalOpen = true;
|
setCreateCollectionModalOpen(false);
|
||||||
};
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
handleInviteModalClose = () => {
|
const handleInviteModalOpen = React.useCallback((ev: SyntheticEvent<>) => {
|
||||||
this.inviteModalOpen = false;
|
ev.preventDefault();
|
||||||
};
|
setInviteModalOpen(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleInviteModalClose = React.useCallback((ev: SyntheticEvent<>) => {
|
||||||
|
ev.preventDefault();
|
||||||
|
setInviteModalOpen(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
render() {
|
|
||||||
const { auth, documents, policies, t } = this.props;
|
|
||||||
const { user, team } = auth;
|
const { user, team } = auth;
|
||||||
if (!user || !team) return null;
|
if (!user || !team) return null;
|
||||||
|
|
||||||
@ -115,9 +115,7 @@ class MainSidebar extends React.Component<Props> {
|
|||||||
icon={<ShapesIcon color="currentColor" />}
|
icon={<ShapesIcon color="currentColor" />}
|
||||||
exact={false}
|
exact={false}
|
||||||
label={t("Templates")}
|
label={t("Templates")}
|
||||||
active={
|
active={documents.active ? documents.active.template : undefined}
|
||||||
documents.active ? documents.active.template : undefined
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
<SidebarLink
|
<SidebarLink
|
||||||
to="/drafts"
|
to="/drafts"
|
||||||
@ -140,10 +138,10 @@ class MainSidebar extends React.Component<Props> {
|
|||||||
/>
|
/>
|
||||||
</Section>
|
</Section>
|
||||||
<Section>
|
<Section>
|
||||||
<Collections
|
<Collections onCreateCollection={handleCreateCollectionModalOpen} />
|
||||||
onCreateCollection={this.handleCreateCollectionModalOpen}
|
|
||||||
/>
|
|
||||||
</Section>
|
</Section>
|
||||||
|
</Scrollable>
|
||||||
|
<Secondary>
|
||||||
<Section>
|
<Section>
|
||||||
<SidebarLink
|
<SidebarLink
|
||||||
to="/archive"
|
to="/archive"
|
||||||
@ -161,44 +159,50 @@ class MainSidebar extends React.Component<Props> {
|
|||||||
icon={<TrashIcon color="currentColor" />}
|
icon={<TrashIcon color="currentColor" />}
|
||||||
exact={false}
|
exact={false}
|
||||||
label={t("Trash")}
|
label={t("Trash")}
|
||||||
active={
|
active={documents.active ? documents.active.isDeleted : undefined}
|
||||||
documents.active ? documents.active.isDeleted : undefined
|
/>
|
||||||
}
|
<SidebarLink
|
||||||
|
to="/settings"
|
||||||
|
icon={<SettingsIcon color="currentColor" />}
|
||||||
|
exact={false}
|
||||||
|
label={t("Settings")}
|
||||||
/>
|
/>
|
||||||
{can.invite && (
|
{can.invite && (
|
||||||
<SidebarLink
|
<SidebarLink
|
||||||
to="/settings/people"
|
to="/settings/people"
|
||||||
onClick={this.handleInviteModalOpen}
|
onClick={handleInviteModalOpen}
|
||||||
icon={<PlusIcon color="currentColor" />}
|
icon={<PlusIcon color="currentColor" />}
|
||||||
label={t("Invite people…")}
|
label={t("Invite people…")}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Section>
|
</Section>
|
||||||
</Scrollable>
|
</Secondary>
|
||||||
</Flex>
|
</Flex>
|
||||||
<Modal
|
<Modal
|
||||||
title={t("Invite people")}
|
title={t("Invite people")}
|
||||||
onRequestClose={this.handleInviteModalClose}
|
onRequestClose={handleInviteModalClose}
|
||||||
isOpen={this.inviteModalOpen}
|
isOpen={inviteModalOpen}
|
||||||
>
|
>
|
||||||
<Invite onSubmit={this.handleInviteModalClose} />
|
<Invite onSubmit={handleInviteModalClose} />
|
||||||
</Modal>
|
</Modal>
|
||||||
<Modal
|
<Modal
|
||||||
title={t("Create a collection")}
|
title={t("Create a collection")}
|
||||||
onRequestClose={this.handleCreateCollectionModalClose}
|
onRequestClose={handleCreateCollectionModalClose}
|
||||||
isOpen={this.createCollectionModalOpen}
|
isOpen={createCollectionModalOpen}
|
||||||
>
|
>
|
||||||
<CollectionNew onSubmit={this.handleCreateCollectionModalClose} />
|
<CollectionNew onSubmit={handleCreateCollectionModalClose} />
|
||||||
</Modal>
|
</Modal>
|
||||||
</Sidebar>
|
</Sidebar>
|
||||||
);
|
);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const Secondary = styled.div`
|
||||||
|
overflow-x: hidden;
|
||||||
|
flex-shrink: 0;
|
||||||
|
`;
|
||||||
|
|
||||||
const Drafts = styled(Flex)`
|
const Drafts = styled(Flex)`
|
||||||
height: 24px;
|
height: 24px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export default withTranslation()<MainSidebar>(
|
export default observer(MainSidebar);
|
||||||
inject("documents", "policies", "auth")(MainSidebar)
|
|
||||||
);
|
|
||||||
|
@ -58,7 +58,7 @@ function SettingsSidebar() {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<Flex auto column>
|
<Flex auto column>
|
||||||
<Scrollable shadow>
|
<Scrollable topShadow>
|
||||||
<Section>
|
<Section>
|
||||||
<Header>{t("Account")}</Header>
|
<Header>{t("Account")}</Header>
|
||||||
<SidebarLink
|
<SidebarLink
|
||||||
|
@ -7,6 +7,7 @@ const Section = styled(Flex)`
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
margin: 24px 8px;
|
margin: 24px 8px;
|
||||||
min-width: ${(props) => props.theme.sidebarMinWidth}px;
|
min-width: ${(props) => props.theme.sidebarMinWidth}px;
|
||||||
|
flex-shrink: 0;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export default Section;
|
export default Section;
|
||||||
|
31
app/hooks/useWindowSize.js
Normal file
31
app/hooks/useWindowSize.js
Normal 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;
|
||||||
|
}
|
@ -94,9 +94,11 @@
|
|||||||
"Invite people": "Invite people",
|
"Invite people": "Invite people",
|
||||||
"Create a collection": "Create a collection",
|
"Create a collection": "Create a collection",
|
||||||
"Return to App": "Return to App",
|
"Return to App": "Return to App",
|
||||||
|
"Account": "Account",
|
||||||
"Profile": "Profile",
|
"Profile": "Profile",
|
||||||
"Notifications": "Notifications",
|
"Notifications": "Notifications",
|
||||||
"API Tokens": "API Tokens",
|
"API Tokens": "API Tokens",
|
||||||
|
"Team": "Team",
|
||||||
"Details": "Details",
|
"Details": "Details",
|
||||||
"Security": "Security",
|
"Security": "Security",
|
||||||
"People": "People",
|
"People": "People",
|
||||||
@ -109,7 +111,6 @@
|
|||||||
"System": "System",
|
"System": "System",
|
||||||
"Light": "Light",
|
"Light": "Light",
|
||||||
"Dark": "Dark",
|
"Dark": "Dark",
|
||||||
"Account": "Account",
|
|
||||||
"Settings": "Settings",
|
"Settings": "Settings",
|
||||||
"API documentation": "API documentation",
|
"API documentation": "API documentation",
|
||||||
"Changelog": "Changelog",
|
"Changelog": "Changelog",
|
||||||
|
Reference in New Issue
Block a user