diff --git a/.flowconfig b/.flowconfig
index 23473ecb..37acb6c0 100644
--- a/.flowconfig
+++ b/.flowconfig
@@ -32,6 +32,7 @@ module.file_ext=.json
esproposal.decorators=ignore
esproposal.class_static_fields=enable
esproposal.class_instance_fields=enable
+esproposal.optional_chaining=enable
suppress_comment=\\(.\\|\n\\)*\\$FlowFixMe
suppress_comment=\\(.\\|\n\\)*\\$FlowIssue
diff --git a/app/components/Actions.js b/app/components/Actions.js
index 2e3fbf41..77ebe565 100644
--- a/app/components/Actions.js
+++ b/app/components/Actions.js
@@ -11,11 +11,6 @@ export const Action = styled(Flex)`
font-size: 15px;
flex-shrink: 0;
- a {
- color: ${(props) => props.theme.text};
- height: 24px;
- }
-
&:empty {
display: none;
}
diff --git a/app/components/Breadcrumb.js b/app/components/Breadcrumb.js
index 16f5d9e0..b6130a16 100644
--- a/app/components/Breadcrumb.js
+++ b/app/components/Breadcrumb.js
@@ -4,7 +4,6 @@ import {
ArchiveIcon,
EditIcon,
GoToIcon,
- MoreIcon,
PadlockIcon,
ShapesIcon,
TrashIcon,
@@ -14,18 +13,15 @@ import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
import styled from "styled-components";
import breakpoint from "styled-components-breakpoint";
-
-import CollectionsStore from "stores/CollectionsStore";
import Document from "models/Document";
import CollectionIcon from "components/CollectionIcon";
import Flex from "components/Flex";
-import BreadcrumbMenu from "./BreadcrumbMenu";
import useStores from "hooks/useStores";
+import BreadcrumbMenu from "menus/BreadcrumbMenu";
import { collectionUrl } from "utils/routeHelpers";
type Props = {
document: Document,
- collections: CollectionsStore,
onlyText: boolean,
};
@@ -133,7 +129,7 @@ const Breadcrumb = ({ document, onlyText }: Props) => {
{isNestedDocument && (
<>
- } path={menuPath} />
+
>
)}
{lastPath && (
@@ -148,6 +144,11 @@ const Breadcrumb = ({ document, onlyText }: Props) => {
);
};
+export const Slash = styled(GoToIcon)`
+ flex-shrink: 0;
+ fill: ${(props) => props.theme.divider};
+`;
+
const Wrapper = styled(Flex)`
display: none;
@@ -168,22 +169,6 @@ const SmallSlash = styled(GoToIcon)`
opacity: 0.25;
`;
-export const Slash = styled(GoToIcon)`
- flex-shrink: 0;
- fill: ${(props) => props.theme.divider};
-`;
-
-const Overflow = styled(MoreIcon)`
- flex-shrink: 0;
- transition: opacity 100ms ease-in-out;
- fill: ${(props) => props.theme.divider};
-
- &:active,
- &:hover {
- fill: ${(props) => props.theme.text};
- }
-`;
-
const Crumb = styled(Link)`
color: ${(props) => props.theme.text};
font-size: 15px;
@@ -199,12 +184,17 @@ const Crumb = styled(Link)`
const CollectionName = styled(Link)`
display: flex;
- flex-shrink: 0;
+ flex-shrink: 1;
color: ${(props) => props.theme.text};
font-size: 15px;
font-weight: 500;
white-space: nowrap;
overflow: hidden;
+ min-width: 0;
+
+ svg {
+ flex-shrink: 0;
+ }
`;
export default observer(Breadcrumb);
diff --git a/app/components/BreadcrumbMenu.js b/app/components/BreadcrumbMenu.js
deleted file mode 100644
index fa7df3f4..00000000
--- a/app/components/BreadcrumbMenu.js
+++ /dev/null
@@ -1,22 +0,0 @@
-// @flow
-import * as React from "react";
-import { DropdownMenu } from "components/DropdownMenu";
-import DropdownMenuItems from "components/DropdownMenu/DropdownMenuItems";
-
-type Props = {
- label: React.Node,
- path: Array,
-};
-
-export default function BreadcrumbMenu({ label, path }: Props) {
- return (
-
- ({
- title: item.title,
- to: item.url,
- }))}
- />
-
- );
-}
diff --git a/app/components/Button.js b/app/components/Button.js
index d978887b..14b878a9 100644
--- a/app/components/Button.js
+++ b/app/components/Button.js
@@ -22,9 +22,13 @@ const RealButton = styled.button`
cursor: pointer;
user-select: none;
- svg {
- fill: ${(props) => props.iconColor || props.theme.buttonText};
- }
+ ${(props) =>
+ !props.borderOnHover &&
+ `
+ svg {
+ fill: ${props.iconColor || props.theme.buttonText};
+ }
+ `}
&::-moz-focus-inner {
padding: 0;
@@ -42,7 +46,7 @@ const RealButton = styled.button`
}
${(props) =>
- props.neutral &&
+ props.$neutral &&
`
background: ${props.theme.buttonNeutralBackground};
color: ${props.theme.buttonNeutralText};
@@ -52,9 +56,14 @@ const RealButton = styled.button`
: `rgba(0, 0, 0, 0.07) 0px 1px 2px, ${props.theme.buttonNeutralBorder} 0 0 0 1px inset`
};
- svg {
+ ${
+ props.borderOnHover
+ ? ""
+ : `svg {
fill: ${props.iconColor || props.theme.buttonNeutralText};
+ }`
}
+
&:hover {
background: ${darken(0.05, props.theme.buttonNeutralBackground)};
@@ -72,9 +81,9 @@ const RealButton = styled.button`
background: ${props.theme.danger};
color: ${props.theme.white};
- &:hover {
- background: ${darken(0.05, props.theme.danger)};
- }
+ &:hover {
+ background: ${darken(0.05, props.theme.danger)};
+ }
`};
`;
@@ -108,6 +117,7 @@ export type Props = {
children?: React.Node,
innerRef?: React.ElementRef,
disclosure?: boolean,
+ neutral?: boolean,
fullwidth?: boolean,
borderOnHover?: boolean,
};
@@ -119,13 +129,14 @@ function Button({
value,
disclosure,
innerRef,
+ neutral,
...rest
}: Props) {
const hasText = children !== undefined || value !== undefined;
const hasIcon = icon !== undefined;
return (
-
+
{hasIcon && icon}
{hasText && }
diff --git a/app/components/ContextMenu/Header.js b/app/components/ContextMenu/Header.js
new file mode 100644
index 00000000..775991eb
--- /dev/null
+++ b/app/components/ContextMenu/Header.js
@@ -0,0 +1,13 @@
+// @flow
+import styled from "styled-components";
+
+const Header = styled.h3`
+ font-size: 11px;
+ font-weight: 600;
+ text-transform: uppercase;
+ color: ${(props) => props.theme.sidebarText};
+ letter-spacing: 0.04em;
+ margin: 1em 12px 0.5em;
+`;
+
+export default Header;
diff --git a/app/components/DropdownMenu/DropdownMenuItem.js b/app/components/ContextMenu/MenuItem.js
similarity index 68%
rename from app/components/DropdownMenu/DropdownMenuItem.js
rename to app/components/ContextMenu/MenuItem.js
index 95269e1d..089144ce 100644
--- a/app/components/DropdownMenu/DropdownMenuItem.js
+++ b/app/components/ContextMenu/MenuItem.js
@@ -1,6 +1,7 @@
// @flow
import { CheckmarkIcon } from "outline-icons";
import * as React from "react";
+import { MenuItem as BaseMenuItem } from "reakit/Menu";
import styled from "styled-components";
type Props = {
@@ -8,31 +9,35 @@ type Props = {
children?: React.Node,
selected?: boolean,
disabled?: boolean,
+ as?: string | React.ComponentType<*>,
};
-const DropdownMenuItem = ({
+const MenuItem = ({
onClick,
children,
selected,
disabled,
+ as,
...rest
}: Props) => {
return (
-
+
);
};
@@ -41,13 +46,14 @@ const Spacer = styled.div`
height: 24px;
`;
-const MenuItem = styled.a`
+export const MenuAnchor = styled.a`
display: flex;
margin: 0;
+ border: 0;
padding: 6px 12px;
width: 100%;
min-height: 32px;
-
+ background: none;
color: ${(props) =>
props.disabled ? props.theme.textTertiary : props.theme.textSecondary};
justify-content: left;
@@ -61,6 +67,7 @@ const MenuItem = styled.a`
}
svg {
+ flex-shrink: 0;
opacity: ${(props) => (props.disabled ? ".5" : 1)};
}
@@ -69,7 +76,8 @@ const MenuItem = styled.a`
? "pointer-events: none;"
: `
- &:hover {
+ &:hover,
+ &.focus-visible {
color: ${props.theme.white};
background: ${props.theme.primary};
box-shadow: none;
@@ -87,4 +95,4 @@ const MenuItem = styled.a`
`};
`;
-export default DropdownMenuItem;
+export default MenuItem;
diff --git a/app/components/ContextMenu/OverflowMenuButton.js b/app/components/ContextMenu/OverflowMenuButton.js
new file mode 100644
index 00000000..419b63b5
--- /dev/null
+++ b/app/components/ContextMenu/OverflowMenuButton.js
@@ -0,0 +1,21 @@
+// @flow
+import { MoreIcon } from "outline-icons";
+import * as React from "react";
+import { MenuButton } from "reakit/Menu";
+import NudeButton from "components/NudeButton";
+
+export default function OverflowMenuButton({
+ iconColor,
+ className,
+ ...rest
+}: any) {
+ return (
+
+ {(props) => (
+
+
+
+ )}
+
+ );
+}
diff --git a/app/components/ContextMenu/Separator.js b/app/components/ContextMenu/Separator.js
new file mode 100644
index 00000000..5dbc29c5
--- /dev/null
+++ b/app/components/ContextMenu/Separator.js
@@ -0,0 +1,16 @@
+// @flow
+import * as React from "react";
+import { MenuSeparator } from "reakit/Menu";
+import styled from "styled-components";
+
+export default function Separator(rest: {}) {
+ return (
+
+ {(props) => }
+
+ );
+}
+
+const HorizontalRule = styled.hr`
+ margin: 0.5em 12px;
+`;
diff --git a/app/components/DropdownMenu/DropdownMenuItems.js b/app/components/ContextMenu/Template.js
similarity index 64%
rename from app/components/DropdownMenu/DropdownMenuItems.js
rename to app/components/ContextMenu/Template.js
index 3752c4ec..d1ab6b70 100644
--- a/app/components/DropdownMenu/DropdownMenuItems.js
+++ b/app/components/ContextMenu/Template.js
@@ -1,13 +1,19 @@
// @flow
import { ExpandedIcon } from "outline-icons";
import * as React from "react";
+import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
+import {
+ useMenuState,
+ MenuButton,
+ MenuItem as BaseMenuItem,
+} from "reakit/Menu";
import styled from "styled-components";
-import Flex from "components/Flex";
-import DropdownMenu from "./DropdownMenu";
-import DropdownMenuItem from "./DropdownMenuItem";
+import MenuItem, { MenuAnchor } from "./MenuItem";
+import Separator from "./Separator";
+import ContextMenu from ".";
-type MenuItem =
+type TMenuItem =
| {|
title: React.Node,
to: string,
@@ -35,7 +41,7 @@ type MenuItem =
disabled?: boolean,
style?: Object,
hover?: boolean,
- items: MenuItem[],
+ items: TMenuItem[],
|}
| {|
type: "separator",
@@ -48,14 +54,35 @@ type MenuItem =
|};
type Props = {|
- items: MenuItem[],
+ items: TMenuItem[],
|};
const Disclosure = styled(ExpandedIcon)`
transform: rotate(270deg);
+ justify-self: flex-end;
`;
-export default function DropdownMenuItems({ items }: Props): React.Node {
+const Submenu = React.forwardRef(({ templateItems, title, ...rest }, ref) => {
+ const { t } = useTranslation();
+ const menu = useMenuState({ modal: true });
+
+ return (
+ <>
+
+ {(props) => (
+
+ {title}
+
+ )}
+
+
+
+
+ >
+ );
+});
+
+function Template({ items, ...menu }: Props): React.Node {
let filtered = items.filter((item) => item.visible !== false);
// this block literally just trims unneccessary separators
@@ -76,69 +103,67 @@ export default function DropdownMenuItems({ items }: Props): React.Node {
return filtered.map((item, index) => {
if (item.to) {
return (
-
{item.title}
-
+
);
}
if (item.href) {
return (
-
{item.title}
-
+
);
}
if (item.onClick) {
return (
-
{item.title}
-
+
);
}
if (item.items) {
return (
-
-
- {item.title}
-
-
-
- }
- hover={item.hover}
+
-
-
+ as={Submenu}
+ templateItems={item.items}
+ title={item.title}
+ {...menu}
+ />
);
}
if (item.type === "separator") {
- return
;
+ return ;
}
return null;
});
}
+
+export default React.memo(Template);
diff --git a/app/components/ContextMenu/index.js b/app/components/ContextMenu/index.js
new file mode 100644
index 00000000..88f700c5
--- /dev/null
+++ b/app/components/ContextMenu/index.js
@@ -0,0 +1,77 @@
+// @flow
+import { rgba } from "polished";
+import * as React from "react";
+import { Menu } from "reakit/Menu";
+import styled from "styled-components";
+import { fadeAndScaleIn } from "shared/styles/animations";
+import usePrevious from "hooks/usePrevious";
+
+type Props = {|
+ "aria-label": string,
+ visible?: boolean,
+ animating?: boolean,
+ children: React.Node,
+ onOpen?: () => void,
+ onClose?: () => void,
+|};
+
+export default function ContextMenu({
+ children,
+ onOpen,
+ onClose,
+ ...rest
+}: Props) {
+ const previousVisible = usePrevious(rest.visible);
+
+ React.useEffect(() => {
+ if (rest.visible && !previousVisible) {
+ if (onOpen) {
+ onOpen();
+ }
+ }
+ if (!rest.visible && previousVisible) {
+ if (onClose) {
+ onClose();
+ }
+ }
+ }, [onOpen, onClose, previousVisible, rest.visible]);
+
+ return (
+
+ );
+}
+
+const Position = styled.div`
+ position: absolute;
+ z-index: ${(props) => props.theme.depths.menu};
+`;
+
+const Background = styled.div`
+ animation: ${fadeAndScaleIn} 200ms ease;
+ transform-origin: ${(props) => (props.left !== undefined ? "25%" : "75%")} 0;
+ background: ${(props) => rgba(props.theme.menuBackground, 0.95)};
+ border: ${(props) =>
+ props.theme.menuBorder ? `1px solid ${props.theme.menuBorder}` : "none"};
+ border-radius: 2px;
+ padding: 0.5em 0;
+ min-width: 180px;
+ overflow: hidden;
+ overflow-y: auto;
+ max-height: 75vh;
+ max-width: 276px;
+ box-shadow: ${(props) => props.theme.menuShadow};
+ pointer-events: all;
+ font-weight: normal;
+
+ @media print {
+ display: none;
+ }
+`;
diff --git a/app/components/DocumentHistory/components/Revision.js b/app/components/DocumentHistory/components/Revision.js
index dbb228a4..2c29cb2e 100644
--- a/app/components/DocumentHistory/components/Revision.js
+++ b/app/components/DocumentHistory/components/Revision.js
@@ -1,6 +1,5 @@
// @flow
import format from "date-fns/format";
-import { MoreIcon } from "outline-icons";
import * as React from "react";
import { NavLink } from "react-router-dom";
import styled, { withTheme } from "styled-components";
@@ -45,9 +44,7 @@ class RevisionListItem extends React.Component {
- }
+ iconColor={selected ? theme.white : theme.textTertiary}
/>
)}
diff --git a/app/components/DocumentListItem.js b/app/components/DocumentListItem.js
index d292e99b..e940e6d0 100644
--- a/app/components/DocumentListItem.js
+++ b/app/components/DocumentListItem.js
@@ -3,8 +3,9 @@ import { observer } from "mobx-react";
import { PlusIcon } from "outline-icons";
import * as React from "react";
import { useTranslation } from "react-i18next";
-import { Link, useHistory } from "react-router-dom";
+import { Link } from "react-router-dom";
import styled, { css } from "styled-components";
+import breakpoint from "styled-components-breakpoint";
import Document from "models/Document";
import Badge from "components/Badge";
import Button from "components/Button";
@@ -18,7 +19,7 @@ import useCurrentUser from "hooks/useCurrentUser";
import DocumentMenu from "menus/DocumentMenu";
import { newDocumentUrl } from "utils/routeHelpers";
-type Props = {
+type Props = {|
document: Document,
highlight?: ?string,
context?: ?string,
@@ -27,7 +28,7 @@ type Props = {
showPin?: boolean,
showDraft?: boolean,
showTemplate?: boolean,
-};
+|};
const SEARCH_RESULT_REGEX = /]*>(.*?)<\/b>/gi;
@@ -40,7 +41,6 @@ function replaceResultMarks(tag: string) {
function DocumentListItem(props: Props) {
const { t } = useTranslation();
const currentUser = useCurrentUser();
- const history = useHistory();
const [menuOpen, setMenuOpen] = React.useState(false);
const {
document,
@@ -53,23 +53,11 @@ function DocumentListItem(props: Props) {
context,
} = props;
- const handleNewFromTemplate = React.useCallback(
- (ev: SyntheticEvent<>) => {
- ev.preventDefault();
- ev.stopPropagation();
-
- history.push(
- newDocumentUrl(document.collectionId, {
- templateId: document.id,
- })
- );
- },
- [history, document]
- );
-
const queryIsInTitle =
!!highlight &&
!!document.title.toLowerCase().includes(highlight.toLowerCase());
+ const canStar =
+ !document.isDraft && !document.isArchived && !document.isTemplate;
return (
-
-
- {document.isNew && document.createdBy.id !== currentUser.id && (
- {t("New")}
+
+
+
+ {document.isNew && document.createdBy.id !== currentUser.id && (
+ {t("New")}
+ )}
+ {canStar && (
+
+
+
+ )}
+ {document.isDraft && showDraft && (
+
+ {t("Draft")}
+
+ )}
+ {document.isTemplate && showTemplate && (
+ {t("Template")}
+ )}
+
+
+ {!queryIsInTitle && (
+
)}
- {!document.isDraft && !document.isArchived && !document.isTemplate && (
-
-
-
- )}
- {document.isDraft && showDraft && (
-
- {t("Draft")}
-
- )}
- {document.isTemplate && showTemplate && (
- {t("Template")}
- )}
-
- {document.isTemplate && !document.isArchived && !document.isDeleted && (
- } neutral>
+
+
+
+ {document.isTemplate && !document.isArchived && !document.isDeleted && (
+ <>
+ }
+ neutral
+ >
{t("New doc")}
- )}
-
-
- setMenuOpen(true)}
- onClose={() => setMenuOpen(false)}
- />
-
-
-
-
- {!queryIsInTitle && (
-
+ )}
+ setMenuOpen(true)}
+ onClose={() => setMenuOpen(false)}
+ modal={false}
/>
- )}
-
+
);
}
-const SecondaryActions = styled(Flex)`
+const Content = styled.div`
+ flex-grow: 1;
+ flex-shrink: 1;
+ min-width: 0;
+`;
+
+const Actions = styled(EventBoundary)`
+ display: none;
align-items: center;
- position: absolute;
- right: 16px;
- top: 50%;
- transform: translateY(-50%);
+ margin: 8px;
+ flex-shrink: 0;
+ flex-grow: 0;
+
+ ${breakpoint("tablet")`
+ display: flex;
+ `};
`;
const DocumentLink = styled(Link)`
- display: block;
+ display: flex;
+ align-items: center;
margin: 10px -8px;
padding: 6px 8px;
border-radius: 8px;
max-height: 50vh;
min-width: 100%;
max-width: calc(100vw - 40px);
- overflow: hidden;
- position: relative;
- ${SecondaryActions} {
+ ${Actions} {
opacity: 0;
}
@@ -166,10 +173,11 @@ const DocumentLink = styled(Link)`
&:hover,
&:active,
- &:focus {
+ &:focus,
+ &:focus-within {
background: ${(props) => props.theme.listItemHoverBackground};
- ${SecondaryActions} {
+ ${Actions} {
opacity: 1;
}
@@ -187,7 +195,7 @@ const DocumentLink = styled(Link)`
css`
background: ${(props) => props.theme.listItemHoverBackground};
- ${SecondaryActions} {
+ ${Actions} {
opacity: 1;
}
@@ -210,7 +218,7 @@ const Heading = styled.h3`
Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
`;
-const Actions = styled(Flex)`
+const StarPositioner = styled(Flex)`
margin-left: 4px;
align-items: center;
`;
diff --git a/app/components/DocumentMeta.js b/app/components/DocumentMeta.js
index 52c42ea4..10a94154 100644
--- a/app/components/DocumentMeta.js
+++ b/app/components/DocumentMeta.js
@@ -15,6 +15,7 @@ const Container = styled(Flex)`
font-size: 13px;
white-space: nowrap;
overflow: hidden;
+ min-width: 0;
`;
const Modified = styled.span`
diff --git a/app/components/DropdownMenu/DropdownMenu.js b/app/components/DropdownMenu/DropdownMenu.js
deleted file mode 100644
index bb5b8eaf..00000000
--- a/app/components/DropdownMenu/DropdownMenu.js
+++ /dev/null
@@ -1,289 +0,0 @@
-// @flow
-import invariant from "invariant";
-import { observable } from "mobx";
-import { observer } from "mobx-react";
-import { MoreIcon } from "outline-icons";
-import { rgba } from "polished";
-import * as React from "react";
-import { withTranslation, type TFunction } from "react-i18next";
-import { PortalWithState } from "react-portal";
-import styled from "styled-components";
-import { fadeAndScaleIn } from "shared/styles/animations";
-import Flex from "components/Flex";
-import NudeButton from "components/NudeButton";
-
-let previousClosePortal;
-let counter = 0;
-
-type Children =
- | React.Node
- | ((options: { closePortal: () => void }) => React.Node);
-
-type Props = {|
- label?: React.Node,
- onOpen?: () => void,
- onClose?: () => void,
- children?: Children,
- className?: string,
- hover?: boolean,
- style?: Object,
- position?: "left" | "right" | "center",
- t: TFunction,
-|};
-
-@observer
-class DropdownMenu extends React.Component {
- id: string = `menu${counter++}`;
- closeTimeout: TimeoutID;
-
- @observable top: ?number;
- @observable bottom: ?number;
- @observable right: ?number;
- @observable left: ?number;
- @observable position: "left" | "right" | "center";
- @observable fixed: ?boolean;
- @observable bodyRect: ClientRect;
- @observable labelRect: ClientRect;
- @observable dropdownRef: { current: null | HTMLElement } = React.createRef();
- @observable menuRef: { current: null | HTMLElement } = React.createRef();
-
- handleOpen = (
- openPortal: (SyntheticEvent<>) => void,
- closePortal: () => void
- ) => {
- return (ev: SyntheticMouseEvent) => {
- ev.preventDefault();
- const currentTarget = ev.currentTarget;
- invariant(document.body, "why you not here");
-
- if (currentTarget instanceof HTMLDivElement) {
- this.bodyRect = document.body.getBoundingClientRect();
- this.labelRect = currentTarget.getBoundingClientRect();
- this.top = this.labelRect.bottom - this.bodyRect.top;
- this.bottom = undefined;
- this.position = this.props.position || "left";
-
- if (currentTarget.parentElement) {
- const triggerParentStyle = getComputedStyle(
- currentTarget.parentElement
- );
-
- if (triggerParentStyle.position === "static") {
- this.fixed = true;
- this.top = this.labelRect.bottom;
- }
- }
-
- this.initPosition();
-
- // attempt to keep only one flyout menu open at once
- if (previousClosePortal && !this.props.hover) {
- previousClosePortal();
- }
- previousClosePortal = closePortal;
- openPortal(ev);
- }
- };
- };
-
- initPosition() {
- if (this.position === "left") {
- this.right =
- this.bodyRect.width - this.labelRect.left - this.labelRect.width;
- } else if (this.position === "center") {
- this.left = this.labelRect.left + this.labelRect.width / 2;
- } else {
- this.left = this.labelRect.left;
- }
- }
-
- onOpen = () => {
- if (typeof this.props.onOpen === "function") {
- this.props.onOpen();
- }
- this.fitOnTheScreen();
- };
-
- fitOnTheScreen() {
- if (!this.dropdownRef || !this.dropdownRef.current) return;
- const el = this.dropdownRef.current;
-
- const sticksOutPastBottomEdge =
- el.clientHeight + this.top > window.innerHeight;
- if (sticksOutPastBottomEdge) {
- this.top = undefined;
- this.bottom = this.fixed ? 0 : -1 * window.pageYOffset;
- } else {
- this.bottom = undefined;
- }
-
- if (this.position === "left" || this.position === "right") {
- const totalWidth =
- Math.sign(this.position === "left" ? -1 : 1) * el.offsetLeft +
- el.scrollWidth;
- const isVisible = totalWidth < window.innerWidth;
-
- if (!isVisible) {
- if (this.position === "right") {
- this.position = "left";
- this.left = undefined;
- } else if (this.position === "left") {
- this.position = "right";
- this.right = undefined;
- }
- }
- }
-
- this.initPosition();
- this.forceUpdate();
- }
-
- closeAfterTimeout = (closePortal: () => void) => () => {
- if (this.closeTimeout) {
- clearTimeout(this.closeTimeout);
- }
- this.closeTimeout = setTimeout(closePortal, 500);
- };
-
- clearCloseTimeout = () => {
- if (this.closeTimeout) {
- clearTimeout(this.closeTimeout);
- }
- };
-
- render() {
- const { className, hover, label, children, t } = this.props;
-
- return (
-
-
- {({ closePortal, openPortal, isOpen, portal }) => (
- <>
-
- {portal(
-
-
-
- )}
- >
- )}
-
-
- );
- }
-}
-
-const Label = styled(Flex).attrs({
- justify: "center",
- align: "center",
-})`
- z-index: ${(props) => props.theme.depths.menu};
- cursor: pointer;
-`;
-
-const Position = styled.div`
- position: ${({ fixed }) => (fixed ? "fixed" : "absolute")};
- display: flex;
- ${({ left }) => (left !== undefined ? `left: ${left}px` : "")};
- ${({ right }) => (right !== undefined ? `right: ${right}px` : "")};
- ${({ top }) => (top !== undefined ? `top: ${top}px` : "")};
- ${({ bottom }) => (bottom !== undefined ? `bottom: ${bottom}px` : "")};
- max-height: 75%;
- z-index: ${(props) => props.theme.depths.menu};
- transform: ${(props) =>
- props.position === "center" ? "translateX(-50%)" : "initial"};
- pointer-events: none;
-`;
-
-const Menu = styled.div`
- animation: ${fadeAndScaleIn} 200ms ease;
- transform-origin: ${(props) => (props.left !== undefined ? "25%" : "75%")} 0;
- backdrop-filter: blur(10px);
- background: ${(props) => rgba(props.theme.menuBackground, 0.8)};
- border: ${(props) =>
- props.theme.menuBorder ? `1px solid ${props.theme.menuBorder}` : "none"};
- border-radius: 2px;
- padding: 0.5em 0;
- min-width: 180px;
- overflow: hidden;
- overflow-y: auto;
- box-shadow: ${(props) => props.theme.menuShadow};
- pointer-events: all;
-
- hr {
- margin: 0.5em 12px;
- }
-
- @media print {
- display: none;
- }
-`;
-
-export const Header = styled.h3`
- font-size: 11px;
- font-weight: 600;
- text-transform: uppercase;
- color: ${(props) => props.theme.sidebarText};
- letter-spacing: 0.04em;
- margin: 1em 12px 0.5em;
-`;
-
-export default withTranslation()(DropdownMenu);
diff --git a/app/components/DropdownMenu/index.js b/app/components/DropdownMenu/index.js
deleted file mode 100644
index 38b1b84e..00000000
--- a/app/components/DropdownMenu/index.js
+++ /dev/null
@@ -1,3 +0,0 @@
-// @flow
-export { default as DropdownMenu, Header } from "./DropdownMenu";
-export { default as DropdownMenuItem } from "./DropdownMenuItem";
diff --git a/app/components/EventBoundary.js b/app/components/EventBoundary.js
index 16b64809..bbe28f46 100644
--- a/app/components/EventBoundary.js
+++ b/app/components/EventBoundary.js
@@ -4,13 +4,18 @@ import * as React from "react";
type Props = {
children: React.Node,
+ className?: string,
};
-export default function EventBoundary({ children }: Props) {
+export default function EventBoundary({ children, className }: Props) {
const handleClick = React.useCallback((event: SyntheticEvent<>) => {
event.preventDefault();
event.stopPropagation();
}, []);
- return {children};
+ return (
+
+ {children}
+
+ );
}
diff --git a/app/components/IconPicker.js b/app/components/IconPicker.js
index 279c9233..1c297b40 100644
--- a/app/components/IconPicker.js
+++ b/app/components/IconPicker.js
@@ -1,6 +1,4 @@
// @flow
-import { observable } from "mobx";
-import { observer } from "mobx-react";
import {
CollectionIcon,
CoinsIcon,
@@ -22,14 +20,17 @@ import {
VehicleIcon,
} from "outline-icons";
import * as React from "react";
-import { withTranslation, type TFunction } from "react-i18next";
+import { useTranslation } from "react-i18next";
+import { useMenuState, MenuButton, MenuItem } from "reakit/Menu";
import styled from "styled-components";
-import { DropdownMenu } from "components/DropdownMenu";
+import ContextMenu from "components/ContextMenu";
import Flex from "components/Flex";
import HelpText from "components/HelpText";
import { LabelText } from "components/Input";
import NudeButton from "components/NudeButton";
+const style = { width: 30, height: 30 };
+
const TwitterPicker = React.lazy(() =>
import("react-color/lib/components/twitter/Twitter")
);
@@ -122,107 +123,77 @@ const colors = [
"#2F362F",
];
-type Props = {
+type Props = {|
onOpen?: () => void,
onChange: (color: string, icon: string) => void,
icon: string,
color: string,
- t: TFunction,
-};
+|};
-function preventEventBubble(event) {
- event.stopPropagation();
+function IconPicker({ onOpen, icon, color, onChange }: Props) {
+ const { t } = useTranslation();
+ const menu = useMenuState({
+ modal: true,
+ placement: "bottom-end",
+ });
+ const Component = icons[icon || "collection"].component;
+
+ return (
+
+
+
+ {(props) => (
+
+ )}
+
+
+
+ {Object.keys(icons).map((name) => {
+ const Component = icons[name].component;
+ return (
+
+ );
+ })}
+
+
+ {t("Loading")}…}>
+ onChange(color.hex, icon)}
+ colors={colors}
+ triangle="hide"
+ />
+
+
+
+
+ );
}
-@observer
-class IconPicker extends React.Component {
- @observable isOpen: boolean = false;
- node: ?HTMLElement;
-
- componentDidMount() {
- window.addEventListener("click", this.handleClickOutside);
- }
-
- componentWillUnmount() {
- window.removeEventListener("click", this.handleClickOutside);
- }
-
- handleClose = () => {
- this.isOpen = false;
- };
-
- handleOpen = () => {
- this.isOpen = true;
-
- if (this.props.onOpen) {
- this.props.onOpen();
- }
- };
-
- handleClickOutside = (ev: SyntheticMouseEvent<>) => {
- // $FlowFixMe
- if (ev.target && this.node && this.node.contains(ev.target)) {
- return;
- }
-
- this.handleClose();
- };
-
- render() {
- const { t } = this.props;
- const Component = icons[this.props.icon || "collection"].component;
-
- return (
- (this.node = ref)}>
-
-
-
-
- }
- >
-
- {Object.keys(icons).map((name) => {
- const Component = icons[name].component;
- return (
- this.props.onChange(this.props.color, name)}
- style={{ width: 30, height: 30 }}
- >
-
-
- );
- })}
-
-
- {t("Loading")}…}>
-
- this.props.onChange(color.hex, this.props.icon)
- }
- colors={colors}
- triangle="hide"
- />
-
-
-
-
- );
- }
-}
+const Label = styled.label`
+ display: block;
+`;
const Icons = styled.div`
padding: 15px 9px 9px 15px;
width: 276px;
`;
-const LabelButton = styled(NudeButton)`
+const Button = styled(NudeButton)`
border: 1px solid ${(props) => props.theme.inputBorder};
width: 32px;
height: 32px;
@@ -249,4 +220,4 @@ const Wrapper = styled("div")`
position: relative;
`;
-export default withTranslation()(IconPicker);
+export default IconPicker;
diff --git a/app/components/Sidebar/Main.js b/app/components/Sidebar/Main.js
index 872ba576..44b3794c 100644
--- a/app/components/Sidebar/Main.js
+++ b/app/components/Sidebar/Main.js
@@ -75,16 +75,17 @@ class MainSidebar extends React.Component {
return (
-
+ {(props) => (
- }
- />
+ )}
+
diff --git a/app/components/Sidebar/components/CollectionLink.js b/app/components/Sidebar/components/CollectionLink.js
index c22334be..afa40ab3 100644
--- a/app/components/Sidebar/components/CollectionLink.js
+++ b/app/components/Sidebar/components/CollectionLink.js
@@ -100,14 +100,12 @@ function CollectionLink({
<>
{can.update && (
setMenuOpen(true)}
onClose={() => setMenuOpen(false)}
/>
)}
setMenuOpen(true)}
onClose={() => setMenuOpen(false)}
diff --git a/app/components/Sidebar/components/DocumentLink.js b/app/components/Sidebar/components/DocumentLink.js
index c245e926..70546706 100644
--- a/app/components/Sidebar/components/DocumentLink.js
+++ b/app/components/Sidebar/components/DocumentLink.js
@@ -242,7 +242,6 @@ function DocumentLink({
document && !isMoving ? (
setMenuOpen(true)}
onClose={() => setMenuOpen(false)}
diff --git a/app/components/Sidebar/components/HeaderBlock.js b/app/components/Sidebar/components/HeaderBlock.js
index 22ba64df..d0cd1e12 100644
--- a/app/components/Sidebar/components/HeaderBlock.js
+++ b/app/components/Sidebar/components/HeaderBlock.js
@@ -12,26 +12,22 @@ type Props = {
logoUrl: string,
};
-function HeaderBlock({
- showDisclosure,
- teamName,
- subheading,
- logoUrl,
- ...rest
-}: Props) {
- return (
-
-
-
-
- {teamName}{" "}
- {showDisclosure && }
-
- {subheading}
-
-
- );
-}
+const HeaderBlock = React.forwardRef(
+ ({ showDisclosure, teamName, subheading, logoUrl, ...rest }: Props, ref) => {
+ return (
+
+
+
+
+ {teamName}{" "}
+ {showDisclosure && }
+
+ {subheading}
+
+
+ );
+ }
+);
const StyledExpandedIcon = styled(ExpandedIcon)`
position: absolute;
diff --git a/app/components/Sidebar/components/SidebarLink.js b/app/components/Sidebar/components/SidebarLink.js
index 970bbe9a..06e4efab 100644
--- a/app/components/Sidebar/components/SidebarLink.js
+++ b/app/components/Sidebar/components/SidebarLink.js
@@ -1,7 +1,13 @@
// @flow
import * as React from "react";
-import { withRouter, NavLink } from "react-router-dom";
+import {
+ withRouter,
+ NavLink,
+ type RouterHistory,
+ type Match,
+} from "react-router-dom";
import styled, { withTheme } from "styled-components";
+import EventBoundary from "components/EventBoundary";
import { type Theme } from "types";
type Props = {
@@ -10,6 +16,7 @@ type Props = {
innerRef?: (?HTMLElement) => void,
onClick?: (SyntheticEvent<>) => void,
onMouseEnter?: (SyntheticEvent<>) => void,
+ className?: string,
children?: React.Node,
icon?: React.Node,
label?: React.Node,
@@ -18,6 +25,8 @@ type Props = {
iconColor?: string,
active?: boolean,
isActiveDrop?: boolean,
+ history: RouterHistory,
+ match: Match,
theme: Theme,
exact?: boolean,
depth?: number,
@@ -39,7 +48,9 @@ function SidebarLink({
href,
innerRef,
depth,
- ...rest
+ history,
+ match,
+ className,
}: Props) {
const style = React.useMemo(() => {
return {
@@ -70,7 +81,7 @@ function SidebarLink({
as={to ? undefined : href ? "a" : "div"}
href={href}
ref={innerRef}
- {...rest}
+ className={className}
>
{icon && {icon}}
@@ -84,9 +95,10 @@ const IconWrapper = styled.span`
margin-left: -4px;
margin-right: 4px;
height: 24px;
+ overflow: hidden;
`;
-const Actions = styled.span`
+const Actions = styled(EventBoundary)`
display: ${(props) => (props.showActions ? "inline-flex" : "none")};
position: absolute;
top: 4px;
@@ -110,7 +122,6 @@ const Actions = styled.span`
const StyledNavLink = styled(NavLink)`
display: flex;
position: relative;
- overflow: hidden;
text-overflow: ellipsis;
padding: 4px 16px;
border-radius: 4px;
@@ -121,6 +132,7 @@ const StyledNavLink = styled(NavLink)`
props.$isActiveDrop ? props.theme.white : props.theme.sidebarText};
font-size: 15px;
cursor: pointer;
+ overflow: hidden;
svg {
${(props) => (props.$isActiveDrop ? `fill: ${props.theme.white};` : "")}
diff --git a/app/components/Toasts/components/Toast.js b/app/components/Toasts/components/Toast.js
index f8468556..21fe38cb 100644
--- a/app/components/Toasts/components/Toast.js
+++ b/app/components/Toasts/components/Toast.js
@@ -8,7 +8,7 @@ import type { Toast as TToast } from "types";
type Props = {
onRequestClose: () => void,
- closeAfterMs: number,
+ closeAfterMs?: number,
toast: TToast,
};
diff --git a/app/menus/AccountMenu.js b/app/menus/AccountMenu.js
index 5df567e5..319192a0 100644
--- a/app/menus/AccountMenu.js
+++ b/app/menus/AccountMenu.js
@@ -1,134 +1,128 @@
// @flow
-import { observable } from "mobx";
-import { inject, observer } from "mobx-react";
+import { observer } from "mobx-react";
import { SunIcon, MoonIcon } from "outline-icons";
import * as React from "react";
-import { withTranslation, type TFunction } from "react-i18next";
+import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
+import { useMenuState, MenuButton } from "reakit/Menu";
import styled from "styled-components";
-import AuthStore from "stores/AuthStore";
-import UiStore from "stores/UiStore";
-import KeyboardShortcuts from "scenes/KeyboardShortcuts";
-import { DropdownMenu, DropdownMenuItem } from "components/DropdownMenu";
-import Flex from "components/Flex";
-import Modal from "components/Modal";
import {
developers,
changelog,
githubIssuesUrl,
mailToUrl,
settings,
-} from "../../shared/utils/routeHelpers";
+} from "shared/utils/routeHelpers";
+import KeyboardShortcuts from "scenes/KeyboardShortcuts";
+import ContextMenu from "components/ContextMenu";
+import MenuItem, { MenuAnchor } from "components/ContextMenu/MenuItem";
+import Separator from "components/ContextMenu/Separator";
+import Flex from "components/Flex";
+import Modal from "components/Modal";
+import useStores from "hooks/useStores";
-type Props = {
- label: React.Node,
- ui: UiStore,
- auth: AuthStore,
- t: TFunction,
-};
+type Props = {|
+ children: (props: any) => React.Node,
+|};
-@observer
-class AccountMenu extends React.Component {
- @observable keyboardShortcutsOpen: boolean = false;
+const AppearanceMenu = React.forwardRef((props, ref) => {
+ const { ui } = useStores();
+ const { t } = useTranslation();
+ const menu = useMenuState();
- handleLogout = () => {
- this.props.auth.logout();
- };
-
- handleOpenKeyboardShortcuts = () => {
- this.keyboardShortcutsOpen = true;
- };
-
- handleCloseKeyboardShortcuts = () => {
- this.keyboardShortcutsOpen = false;
- };
-
- render() {
- const { ui, t } = this.props;
-
- return (
- <>
-
+
+ {(props) => (
+
+
+ {t("Appearance")}
+ {ui.resolvedTheme === "light" ? : }
+
+
+ )}
+
+
+
-
+
- >
- );
- }
+ {t("Light")}
+
+
+
+ >
+ );
+});
+
+function AccountMenu(props: Props) {
+ const menu = useMenuState({
+ placement: "bottom-start",
+ modal: true,
+ });
+ const { auth } = useStores();
+ const { t } = useTranslation();
+ const [keyboardShortcutsOpen, setKeyboardShortcutsOpen] = React.useState(
+ false
+ );
+
+ return (
+ <>
+ setKeyboardShortcutsOpen(false)}
+ title={t("Keyboard shortcuts")}
+ >
+
+
+ {props.children}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+ );
}
const ChangeTheme = styled(Flex)`
width: 100%;
`;
-export default withTranslation()(
- inject("ui", "auth")(AccountMenu)
-);
+export default observer(AccountMenu);
diff --git a/app/menus/BreadcrumbMenu.js b/app/menus/BreadcrumbMenu.js
new file mode 100644
index 00000000..5e5a584f
--- /dev/null
+++ b/app/menus/BreadcrumbMenu.js
@@ -0,0 +1,34 @@
+// @flow
+import * as React from "react";
+import { useTranslation } from "react-i18next";
+import { useMenuState } from "reakit/Menu";
+import ContextMenu from "components/ContextMenu";
+import OverflowMenuButton from "components/ContextMenu/OverflowMenuButton";
+import Template from "components/ContextMenu/Template";
+
+type Props = {
+ path: Array,
+};
+
+export default function BreadcrumbMenu({ path }: Props) {
+ const { t } = useTranslation();
+ const menu = useMenuState({
+ modal: true,
+ placement: "bottom",
+ });
+
+ return (
+ <>
+
+
+ ({
+ title: item.title,
+ to: item.url,
+ }))}
+ />
+
+ >
+ );
+}
diff --git a/app/menus/CollectionGroupMemberMenu.js b/app/menus/CollectionGroupMemberMenu.js
new file mode 100644
index 00000000..a8df3c7b
--- /dev/null
+++ b/app/menus/CollectionGroupMemberMenu.js
@@ -0,0 +1,44 @@
+// @flow
+import { observer } from "mobx-react";
+import * as React from "react";
+import { useTranslation } from "react-i18next";
+import { useMenuState } from "reakit/Menu";
+import ContextMenu from "components/ContextMenu";
+import OverflowMenuButton from "components/ContextMenu/OverflowMenuButton";
+import Template from "components/ContextMenu/Template";
+
+type Props = {|
+ onMembers: () => void,
+ onRemove: () => void,
+|};
+
+function CollectionGroupMemberMenu({ onMembers, onRemove }: Props) {
+ const { t } = useTranslation();
+ const menu = useMenuState({ modal: true });
+
+ return (
+ <>
+
+
+
+
+ >
+ );
+}
+
+export default observer(CollectionGroupMemberMenu);
diff --git a/app/menus/CollectionMenu.js b/app/menus/CollectionMenu.js
index fae20725..c0253e9a 100644
--- a/app/menus/CollectionMenu.js
+++ b/app/menus/CollectionMenu.js
@@ -1,229 +1,221 @@
// @flow
-import { observable } from "mobx";
-import { inject, observer } from "mobx-react";
+import { observer } from "mobx-react";
import * as React from "react";
-import { withTranslation, type TFunction } from "react-i18next";
-import { withRouter, type RouterHistory } from "react-router-dom";
-import DocumentsStore from "stores/DocumentsStore";
-import PoliciesStore from "stores/PoliciesStore";
-import UiStore from "stores/UiStore";
+import { useTranslation } from "react-i18next";
+import { useHistory } from "react-router-dom";
+import { useMenuState, MenuButton } from "reakit/Menu";
import Collection from "models/Collection";
import CollectionDelete from "scenes/CollectionDelete";
import CollectionEdit from "scenes/CollectionEdit";
import CollectionExport from "scenes/CollectionExport";
import CollectionMembers from "scenes/CollectionMembers";
-import { DropdownMenu } from "components/DropdownMenu";
-import DropdownMenuItems from "components/DropdownMenu/DropdownMenuItems";
+import ContextMenu from "components/ContextMenu";
+import OverflowMenuButton from "components/ContextMenu/OverflowMenuButton";
+import Template from "components/ContextMenu/Template";
import Modal from "components/Modal";
import VisuallyHidden from "components/VisuallyHidden";
+import useStores from "hooks/useStores";
import getDataTransferFiles from "utils/getDataTransferFiles";
import { newDocumentUrl } from "utils/routeHelpers";
-type Props = {
- position?: "left" | "right" | "center",
- ui: UiStore,
- policies: PoliciesStore,
- documents: DocumentsStore,
+type Props = {|
collection: Collection,
- history: RouterHistory,
+ placement?: string,
+ modal?: boolean,
+ label?: (any) => React.Node,
onOpen?: () => void,
onClose?: () => void,
- t: TFunction,
-};
+|};
-@observer
-class CollectionMenu extends React.Component {
- file: ?HTMLInputElement;
- @observable showCollectionMembers = false;
- @observable showCollectionEdit = false;
- @observable showCollectionDelete = false;
- @observable showCollectionExport = false;
+function CollectionMenu({
+ collection,
+ label,
+ modal = true,
+ placement,
+ onOpen,
+ onClose,
+}: Props) {
+ const menu = useMenuState({ modal, placement });
+ const [renderModals, setRenderModals] = React.useState(false);
+ const { ui, documents, policies } = useStores();
+ const { t } = useTranslation();
+ const history = useHistory();
- onNewDocument = (ev: SyntheticEvent<>) => {
- ev.preventDefault();
- const { collection } = this.props;
- this.props.history.push(newDocumentUrl(collection.id));
- };
+ const file = React.useRef();
+ const [showCollectionMembers, setShowCollectionMembers] = React.useState(
+ false
+ );
+ const [showCollectionEdit, setShowCollectionEdit] = React.useState(false);
+ const [showCollectionDelete, setShowCollectionDelete] = React.useState(false);
+ const [showCollectionExport, setShowCollectionExport] = React.useState(false);
- onImportDocument = (ev: SyntheticEvent<>) => {
- ev.preventDefault();
- ev.stopPropagation();
-
- // simulate a click on the file upload input element
- if (this.file) this.file.click();
- };
-
- onFilePicked = async (ev: SyntheticEvent<>) => {
- const files = getDataTransferFiles(ev);
-
- try {
- const file = files[0];
- const document = await this.props.documents.import(
- file,
- null,
- this.props.collection.id,
- { publish: true }
- );
- this.props.history.push(document.url);
- } catch (err) {
- this.props.ui.showToast(err.message, {
- type: "error",
- });
+ const handleOpen = React.useCallback(() => {
+ setRenderModals(true);
+ if (onOpen) {
+ onOpen();
}
- };
+ }, [onOpen]);
- handleEditCollectionOpen = (ev: SyntheticEvent<>) => {
- ev.preventDefault();
- this.showCollectionEdit = true;
- };
+ const handleNewDocument = React.useCallback(
+ (ev: SyntheticEvent<>) => {
+ ev.preventDefault();
+ history.push(newDocumentUrl(collection.id));
+ },
+ [history, collection.id]
+ );
- handleEditCollectionClose = () => {
- this.showCollectionEdit = false;
- };
+ const handleImportDocument = React.useCallback(
+ (ev: SyntheticEvent<>) => {
+ ev.preventDefault();
+ ev.stopPropagation();
- handleDeleteCollectionOpen = (ev: SyntheticEvent<>) => {
- ev.preventDefault();
- this.showCollectionDelete = true;
- };
+ // simulate a click on the file upload input element
+ if (file.current) {
+ file.current.click();
+ }
+ },
+ [file]
+ );
- handleDeleteCollectionClose = () => {
- this.showCollectionDelete = false;
- };
+ const handleFilePicked = React.useCallback(
+ async (ev: SyntheticEvent<>) => {
+ const files = getDataTransferFiles(ev);
- handleExportCollectionOpen = (ev: SyntheticEvent<>) => {
- ev.preventDefault();
- this.showCollectionExport = true;
- };
+ try {
+ const file = files[0];
+ const document = await documents.import(
+ file,
+ null,
+ this.props.collection.id,
+ { publish: true }
+ );
+ history.push(document.url);
+ } catch (err) {
+ ui.showToast(err.message, {
+ type: "error",
+ });
+ }
+ },
+ [history, ui, documents]
+ );
- handleExportCollectionClose = () => {
- this.showCollectionExport = false;
- };
+ const can = policies.abilities(collection.id);
- handleMembersModalOpen = (ev: SyntheticEvent<>) => {
- ev.preventDefault();
- this.showCollectionMembers = true;
- };
-
- handleMembersModalClose = () => {
- this.showCollectionMembers = false;
- };
-
- render() {
- const {
- policies,
- documents,
- collection,
- position,
- onOpen,
- onClose,
- t,
- } = this.props;
- const can = policies.abilities(collection.id);
-
- return (
- <>
-
- (this.file = ref)}
- onChange={this.onFilePicked}
- onClick={(ev) => ev.stopPropagation()}
- accept={documents.importFileTypes.join(", ")}
- />
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- >
- );
- }
+ return (
+ <>
+
+ ev.stopPropagation()}
+ accept={documents.importFileTypes.join(", ")}
+ tabIndex="-1"
+ />
+
+ {label ? (
+ {label}
+ ) : (
+
+ )}
+
+ setShowCollectionEdit(true),
+ },
+ {
+ title: `${t("Permissions")}…`,
+ visible: can.update,
+ onClick: () => setShowCollectionMembers(true),
+ },
+ {
+ title: `${t("Export")}…`,
+ visible: !!(collection && can.export),
+ onClick: () => setShowCollectionExport(true),
+ },
+ {
+ type: "separator",
+ },
+ {
+ type: "separator",
+ },
+ {
+ title: `${t("Delete")}…`,
+ visible: !!(collection && can.delete),
+ onClick: () => setShowCollectionDelete(true),
+ },
+ ]}
+ />
+
+ {renderModals && (
+ <>
+ setShowCollectionMembers(false)}
+ isOpen={showCollectionMembers}
+ >
+ setShowCollectionMembers(false)}
+ onEdit={() => setShowCollectionEdit(true)}
+ />
+
+ setShowCollectionEdit(false)}
+ >
+ setShowCollectionEdit(false)}
+ collection={collection}
+ />
+
+ setShowCollectionDelete(false)}
+ >
+ setShowCollectionDelete(false)}
+ collection={collection}
+ />
+
+ setShowCollectionExport(false)}
+ >
+ setShowCollectionExport(false)}
+ collection={collection}
+ />
+
+ >
+ )}
+ >
+ );
}
-export default withTranslation()(
- inject("ui", "documents", "policies")(withRouter(CollectionMenu))
-);
+export default observer(CollectionMenu);
diff --git a/app/menus/CollectionSortMenu.js b/app/menus/CollectionSortMenu.js
index d2930108..fa1b531c 100644
--- a/app/menus/CollectionSortMenu.js
+++ b/app/menus/CollectionSortMenu.js
@@ -3,29 +3,25 @@ import { observer } from "mobx-react";
import { AlphabeticalSortIcon, ManualSortIcon } from "outline-icons";
import * as React from "react";
import { useTranslation } from "react-i18next";
+import { useMenuState, MenuButton } from "reakit/Menu";
import Collection from "models/Collection";
-import { DropdownMenu } from "components/DropdownMenu";
-import DropdownMenuItems from "components/DropdownMenu/DropdownMenuItems";
+import ContextMenu from "components/ContextMenu";
+import Template from "components/ContextMenu/Template";
import NudeButton from "components/NudeButton";
-type Props = {
- position?: "left" | "right" | "center",
+type Props = {|
collection: Collection,
onOpen?: () => void,
onClose?: () => void,
-};
+|};
-function CollectionSortMenu({
- collection,
- position,
- onOpen,
- onClose,
- ...rest
-}: Props) {
+function CollectionSortMenu({ collection, onOpen, onClose, ...rest }: Props) {
const { t } = useTranslation();
+ const menu = useMenuState({ modal: true });
const handleChangeSort = React.useCallback(
(field: string) => {
+ menu.hide();
return collection.save({
sort: {
field,
@@ -33,38 +29,43 @@ function CollectionSortMenu({
},
});
},
- [collection]
+ [collection, menu]
);
const alphabeticalSort = collection.sort.field === "title";
return (
-
- {alphabeticalSort ? : }
-
- }
- position={position}
- {...rest}
- >
- handleChangeSort("title"),
- selected: alphabeticalSort,
- },
- {
- title: t("Manual sort"),
- onClick: () => handleChangeSort("index"),
- selected: !alphabeticalSort,
- },
- ]}
- />
-
+ <>
+
+ {(props) => (
+
+ {alphabeticalSort ? : }
+
+ )}
+
+
+ handleChangeSort("title"),
+ selected: alphabeticalSort,
+ },
+ {
+ title: t("Manual sort"),
+ onClick: () => handleChangeSort("index"),
+ selected: !alphabeticalSort,
+ },
+ ]}
+ />
+
+ >
);
}
diff --git a/app/menus/DocumentMenu.js b/app/menus/DocumentMenu.js
index 3ab8aa6c..5602cf04 100644
--- a/app/menus/DocumentMenu.js
+++ b/app/menus/DocumentMenu.js
@@ -1,21 +1,21 @@
// @flow
-import { observable } from "mobx";
-import { inject, observer } from "mobx-react";
+import { observer } from "mobx-react";
import * as React from "react";
-import { withTranslation, type TFunction } from "react-i18next";
-import { Redirect } from "react-router-dom";
-import AuthStore from "stores/AuthStore";
-import CollectionStore from "stores/CollectionsStore";
-import PoliciesStore from "stores/PoliciesStore";
-import UiStore from "stores/UiStore";
+import { useTranslation } from "react-i18next";
+import { useHistory } from "react-router-dom";
+import { useMenuState, MenuButton } from "reakit/Menu";
+import styled from "styled-components";
import Document from "models/Document";
import DocumentDelete from "scenes/DocumentDelete";
import DocumentShare from "scenes/DocumentShare";
import DocumentTemplatize from "scenes/DocumentTemplatize";
import CollectionIcon from "components/CollectionIcon";
-import { DropdownMenu } from "components/DropdownMenu";
-import DropdownMenuItems from "components/DropdownMenu/DropdownMenuItems";
+import ContextMenu from "components/ContextMenu";
+import OverflowMenuButton from "components/ContextMenu/OverflowMenuButton";
+import Template from "components/ContextMenu/Template";
+import Flex from "components/Flex";
import Modal from "components/Modal";
+import useStores from "hooks/useStores";
import {
documentHistoryUrl,
documentMoveUrl,
@@ -24,348 +24,319 @@ import {
newDocumentUrl,
} from "utils/routeHelpers";
-type Props = {
- ui: UiStore,
- auth: AuthStore,
- position?: "left" | "right" | "center",
+type Props = {|
document: Document,
- collections: CollectionStore,
- policies: PoliciesStore,
className: string,
isRevision?: boolean,
showPrint?: boolean,
+ modal?: boolean,
showToggleEmbeds?: boolean,
showPin?: boolean,
- label?: React.Node,
+ label?: (any) => React.Node,
onOpen?: () => void,
onClose?: () => void,
- t: TFunction,
-};
+|};
-@observer
-class DocumentMenu extends React.Component {
- @observable redirectTo: ?string;
- @observable showDeleteModal = false;
- @observable showTemplateModal = false;
- @observable showShareModal = false;
+function DocumentMenu({
+ document,
+ isRevision,
+ className,
+ modal = true,
+ showToggleEmbeds,
+ showPrint,
+ showPin,
+ label,
+ onOpen,
+ onClose,
+}: Props) {
+ const { policies, collections, auth, ui } = useStores();
+ const menu = useMenuState({ modal });
+ const history = useHistory();
+ const { t } = useTranslation();
+ const [renderModals, setRenderModals] = React.useState(false);
+ const [showDeleteModal, setShowDeleteModal] = React.useState(false);
+ const [showTemplateModal, setShowTemplateModal] = React.useState(false);
+ const [showShareModal, setShowShareModal] = React.useState(false);
- componentDidUpdate() {
- this.redirectTo = undefined;
- }
-
- handleNewChild = (ev: SyntheticEvent<>) => {
- const { document } = this.props;
- this.redirectTo = newDocumentUrl(document.collectionId, {
- parentDocumentId: document.id,
- });
- };
-
- handleDelete = (ev: SyntheticEvent<>) => {
- this.showDeleteModal = true;
- };
-
- handleDocumentHistory = () => {
- if (this.props.isRevision) {
- this.redirectTo = documentUrl(this.props.document);
- } else {
- this.redirectTo = documentHistoryUrl(this.props.document);
+ const handleOpen = React.useCallback(() => {
+ setRenderModals(true);
+ if (onOpen) {
+ onOpen();
}
- };
+ }, [onOpen]);
- handleMove = (ev: SyntheticEvent<>) => {
- this.redirectTo = documentMoveUrl(this.props.document);
- };
+ const handleDuplicate = React.useCallback(
+ async (ev: SyntheticEvent<>) => {
+ const duped = await document.duplicate();
- handleEdit = (ev: SyntheticEvent<>) => {
- this.redirectTo = editDocumentUrl(this.props.document);
- };
+ // when duplicating, go straight to the duplicated document content
+ history.push(duped.url);
+ ui.showToast(t("Document duplicated"), { type: "success" });
+ },
+ [ui, t, history, document]
+ );
- handleDuplicate = async (ev: SyntheticEvent<>) => {
- const duped = await this.props.document.duplicate();
+ const handleArchive = React.useCallback(
+ async (ev: SyntheticEvent<>) => {
+ await document.archive();
+ ui.showToast(t("Document archived"), { type: "success" });
+ },
+ [ui, t, document]
+ );
- // when duplicating, go straight to the duplicated document content
- this.redirectTo = duped.url;
- const { t } = this.props;
- this.props.ui.showToast(t("Document duplicated"), { type: "success" });
- };
+ const handleRestore = React.useCallback(
+ async (ev: SyntheticEvent<>, options?: { collectionId: string }) => {
+ await document.restore(options);
+ ui.showToast(t("Document restored"), { type: "success" });
+ },
+ [ui, t, document]
+ );
- handleOpenTemplateModal = () => {
- this.showTemplateModal = true;
- };
+ const handleUnpublish = React.useCallback(
+ async (ev: SyntheticEvent<>) => {
+ await document.unpublish();
+ ui.showToast(t("Document unpublished"), { type: "success" });
+ },
+ [ui, t, document]
+ );
- handleCloseTemplateModal = () => {
- this.showTemplateModal = false;
- };
+ const handlePrint = React.useCallback((ev: SyntheticEvent<>) => {
+ window.print();
+ }, []);
- handleCloseDeleteModal = () => {
- this.showDeleteModal = false;
- };
+ const handleStar = React.useCallback(
+ (ev: SyntheticEvent<>) => {
+ ev.stopPropagation();
+ document.star();
+ },
+ [document]
+ );
- handleArchive = async (ev: SyntheticEvent<>) => {
- await this.props.document.archive();
- const { t } = this.props;
- this.props.ui.showToast(t("Document archived"), { type: "success" });
- };
+ const handleUnstar = React.useCallback(
+ (ev: SyntheticEvent<>) => {
+ ev.stopPropagation();
+ document.unstar();
+ },
+ [document]
+ );
- handleRestore = async (
- ev: SyntheticEvent<>,
- options?: { collectionId: string }
- ) => {
- await this.props.document.restore(options);
- const { t } = this.props;
- this.props.ui.showToast(t("Document restored"), { type: "success" });
- };
+ const handleShareLink = React.useCallback(
+ async (ev: SyntheticEvent<>) => {
+ await document.share();
+ setShowShareModal(true);
+ },
+ [document]
+ );
- handleUnpublish = async (ev: SyntheticEvent<>) => {
- await this.props.document.unpublish();
- const { t } = this.props;
- this.props.ui.showToast(t("Document unpublished"), { type: "success" });
- };
+ const can = policies.abilities(document.id);
+ const canShareDocuments = !!(can.share && auth.team && auth.team.sharing);
+ const canViewHistory = can.read && !can.restore;
+ const collection = collections.get(document.collectionId);
- handlePin = (ev: SyntheticEvent<>) => {
- this.props.document.pin();
- };
-
- handleUnpin = (ev: SyntheticEvent<>) => {
- this.props.document.unpin();
- };
-
- handleStar = (ev: SyntheticEvent<>) => {
- ev.stopPropagation();
- this.props.document.star();
- };
-
- handleUnstar = (ev: SyntheticEvent<>) => {
- ev.stopPropagation();
- this.props.document.unstar();
- };
-
- handleExport = (ev: SyntheticEvent<>) => {
- this.props.document.download();
- };
-
- handleShareLink = async (ev: SyntheticEvent<>) => {
- const { document } = this.props;
- await document.share();
- this.showShareModal = true;
- };
-
- handleCloseShareModal = () => {
- this.showShareModal = false;
- };
-
- render() {
- if (this.redirectTo) return ;
-
- const {
- policies,
- document,
- position,
- className,
- showToggleEmbeds,
- showPrint,
- showPin,
- auth,
- collections,
- label,
- onOpen,
- onClose,
- t,
- } = this.props;
-
- const can = policies.abilities(document.id);
- const canShareDocuments = !!(can.share && auth.team && auth.team.sharing);
- const canViewHistory = can.read && !can.restore;
- const collection = collections.get(document.collectionId);
-
- return (
- <>
-
-
+ {label ? (
+ {label}
+ ) : (
+
+ )}
+
+ {
- const can = policies.abilities(collection.id);
+ ...collections.orderedData.map((collection) => {
+ const can = policies.abilities(collection.id);
- return {
- title: (
- <>
-
- {collection.name}
- >
- ),
- onClick: (ev) =>
- this.handleRestore(ev, { collectionId: collection.id }),
- disabled: !can.update,
- };
- }),
- ],
- },
- {
- title: t("Unpin"),
- onClick: this.handleUnpin,
- visible: !!(showPin && document.pinned && can.unpin),
- },
- {
- title: t("Pin to collection"),
- onClick: this.handlePin,
- visible: !!(showPin && !document.pinned && can.pin),
- },
- {
- title: t("Unstar"),
- onClick: this.handleUnstar,
- visible: document.isStarred && !!can.unstar,
- },
- {
- title: t("Star"),
- onClick: this.handleStar,
- visible: !document.isStarred && !!can.star,
- },
- {
- title: `${t("Share link")}…`,
- onClick: this.handleShareLink,
- visible: canShareDocuments,
- },
- {
- title: t("Enable embeds"),
- onClick: document.enableEmbeds,
- visible: !!showToggleEmbeds && document.embedsDisabled,
- },
- {
- title: t("Disable embeds"),
- onClick: document.disableEmbeds,
- visible: !!showToggleEmbeds && !document.embedsDisabled,
- },
- {
- type: "separator",
- },
- {
- title: t("New nested document"),
- onClick: this.handleNewChild,
- visible: !!can.createChildDocument,
- },
- {
- title: `${t("Create template")}…`,
- onClick: this.handleOpenTemplateModal,
- visible: !!can.update && !document.isTemplate,
- },
- {
- title: t("Edit"),
- onClick: this.handleEdit,
- visible: !!can.update,
- },
- {
- title: t("Duplicate"),
- onClick: this.handleDuplicate,
- visible: !!can.update,
- },
- {
- title: t("Unpublish"),
- onClick: this.handleUnpublish,
- visible: !!can.unpublish,
- },
- {
- title: t("Archive"),
- onClick: this.handleArchive,
- visible: !!can.archive,
- },
- {
- title: `${t("Delete")}…`,
- onClick: this.handleDelete,
- visible: !!can.delete,
- },
- {
- title: `${t("Move")}…`,
- onClick: this.handleMove,
- visible: !!can.move,
- },
- {
- type: "separator",
- },
- {
- title: t("History"),
- onClick: this.handleDocumentHistory,
- visible: canViewHistory,
- },
- {
- title: t("Download"),
- onClick: this.handleExport,
- visible: !!can.download,
- },
- {
- title: t("Print"),
- onClick: window.print,
- visible: !!showPrint,
- },
- ]}
- />
-
-
-
-
-
-
-
-
-
-
- >
- );
- }
+ return {
+ title: (
+
+
+ {collection.name}
+
+ ),
+ onClick: (ev) =>
+ handleRestore(ev, { collectionId: collection.id }),
+ disabled: !can.update,
+ };
+ }),
+ ],
+ },
+ {
+ title: t("Unpin"),
+ onClick: document.unpin,
+ visible: !!(showPin && document.pinned && can.unpin),
+ },
+ {
+ title: t("Pin to collection"),
+ onClick: document.pin,
+ visible: !!(showPin && !document.pinned && can.pin),
+ },
+ {
+ title: t("Unstar"),
+ onClick: handleUnstar,
+ visible: document.isStarred && !!can.unstar,
+ },
+ {
+ title: t("Star"),
+ onClick: handleStar,
+ visible: !document.isStarred && !!can.star,
+ },
+ {
+ title: `${t("Share link")}…`,
+ onClick: handleShareLink,
+ visible: canShareDocuments,
+ },
+ {
+ title: t("Enable embeds"),
+ onClick: document.enableEmbeds,
+ visible: !!showToggleEmbeds && document.embedsDisabled,
+ },
+ {
+ title: t("Disable embeds"),
+ onClick: document.disableEmbeds,
+ visible: !!showToggleEmbeds && !document.embedsDisabled,
+ },
+ {
+ type: "separator",
+ },
+ {
+ title: t("New nested document"),
+ to: newDocumentUrl(document.collectionId, {
+ parentDocumentId: document.id,
+ }),
+ visible: !!can.createChildDocument,
+ },
+ {
+ title: `${t("Create template")}…`,
+ onClick: () => setShowTemplateModal(true),
+ visible: !!can.update && !document.isTemplate,
+ },
+ {
+ title: t("Edit"),
+ to: editDocumentUrl(document),
+ visible: !!can.update,
+ },
+ {
+ title: t("Duplicate"),
+ onClick: handleDuplicate,
+ visible: !!can.update,
+ },
+ {
+ title: t("Unpublish"),
+ onClick: handleUnpublish,
+ visible: !!can.unpublish,
+ },
+ {
+ title: t("Archive"),
+ onClick: handleArchive,
+ visible: !!can.archive,
+ },
+ {
+ title: `${t("Delete")}…`,
+ onClick: () => setShowDeleteModal(true),
+ visible: !!can.delete,
+ },
+ {
+ title: `${t("Move")}…`,
+ to: documentMoveUrl(document),
+ visible: !!can.move,
+ },
+ {
+ type: "separator",
+ },
+ {
+ title: t("History"),
+ to: isRevision
+ ? documentUrl(document)
+ : documentHistoryUrl(document),
+ visible: canViewHistory,
+ },
+ {
+ title: t("Download"),
+ onClick: document.download,
+ visible: !!can.download,
+ },
+ {
+ title: t("Print"),
+ onClick: handlePrint,
+ visible: !!showPrint,
+ },
+ ]}
+ />
+
+ {renderModals && (
+ <>
+ setShowDeleteModal(false)}
+ isOpen={showDeleteModal}
+ >
+ setShowDeleteModal(false)}
+ />
+
+ setShowTemplateModal(false)}
+ isOpen={showTemplateModal}
+ >
+ setShowTemplateModal(false)}
+ />
+
+ setShowShareModal(false)}
+ isOpen={showShareModal}
+ >
+ setShowShareModal(false)}
+ />
+
+ >
+ )}
+ >
+ );
}
-export default withTranslation()(
- inject("ui", "auth", "collections", "policies")(DocumentMenu)
-);
+const CollectionName = styled.div`
+ overflow: hidden;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+`;
+
+export default observer(DocumentMenu);
diff --git a/app/menus/GroupMemberMenu.js b/app/menus/GroupMemberMenu.js
new file mode 100644
index 00000000..8e2d28a7
--- /dev/null
+++ b/app/menus/GroupMemberMenu.js
@@ -0,0 +1,36 @@
+// @flow
+import { observer } from "mobx-react";
+import * as React from "react";
+import { useTranslation } from "react-i18next";
+import { useMenuState } from "reakit/Menu";
+import ContextMenu from "components/ContextMenu";
+import OverflowMenuButton from "components/ContextMenu/OverflowMenuButton";
+import Template from "components/ContextMenu/Template";
+
+type Props = {|
+ onRemove: () => void,
+|};
+
+function GroupMemberMenu({ onRemove }: Props) {
+ const { t } = useTranslation();
+ const menu = useMenuState({ modal: true });
+
+ return (
+ <>
+
+
+
+
+ >
+ );
+}
+
+export default observer(GroupMemberMenu);
diff --git a/app/menus/GroupMenu.js b/app/menus/GroupMenu.js
index 6197bb79..415e2159 100644
--- a/app/menus/GroupMenu.js
+++ b/app/menus/GroupMenu.js
@@ -1,108 +1,74 @@
// @flow
-import { observable } from "mobx";
-import { inject, observer } from "mobx-react";
+import { observer } from "mobx-react";
import * as React from "react";
-import { withTranslation, type TFunction } from "react-i18next";
-import { withRouter, type RouterHistory } from "react-router-dom";
-import PoliciesStore from "stores/PoliciesStore";
-import UiStore from "stores/UiStore";
+import { useTranslation } from "react-i18next";
+import { useMenuState } from "reakit/Menu";
import Group from "models/Group";
import GroupDelete from "scenes/GroupDelete";
import GroupEdit from "scenes/GroupEdit";
-import { DropdownMenu } from "components/DropdownMenu";
-import DropdownMenuItems from "components/DropdownMenu/DropdownMenuItems";
+import ContextMenu from "components/ContextMenu";
+import OverflowMenuButton from "components/ContextMenu/OverflowMenuButton";
+import Template from "components/ContextMenu/Template";
import Modal from "components/Modal";
+import useStores from "hooks/useStores";
-type Props = {
- ui: UiStore,
- policies: PoliciesStore,
+type Props = {|
group: Group,
- history: RouterHistory,
onMembers: () => void,
- onOpen?: () => void,
- onClose?: () => void,
- t: TFunction,
-};
+|};
-@observer
-class GroupMenu extends React.Component {
- @observable editModalOpen: boolean = false;
- @observable deleteModalOpen: boolean = false;
+function GroupMenu({ group, onMembers }: Props) {
+ const { t } = useTranslation();
+ const { policies } = useStores();
+ const menu = useMenuState({ modal: true });
+ const [editModalOpen, setEditModalOpen] = React.useState(false);
+ const [deleteModalOpen, setDeleteModalOpen] = React.useState(false);
+ const can = policies.abilities(group.id);
- onEdit = (ev: SyntheticEvent<>) => {
- ev.preventDefault();
- this.editModalOpen = true;
- };
-
- onDelete = (ev: SyntheticEvent<>) => {
- ev.preventDefault();
- this.deleteModalOpen = true;
- };
-
- handleEditModalClose = () => {
- this.editModalOpen = false;
- };
-
- handleDeleteModalClose = () => {
- this.deleteModalOpen = false;
- };
-
- render() {
- const { policies, group, onOpen, onClose, t } = this.props;
- const can = policies.abilities(group.id);
-
- return (
- <>
-
-
-
-
-
-
-
-
-
-
- >
- );
- }
+ return (
+ <>
+ setEditModalOpen(false)}
+ isOpen={editModalOpen}
+ >
+ setEditModalOpen(false)} />
+
+ setDeleteModalOpen(false)}
+ isOpen={deleteModalOpen}
+ >
+ setDeleteModalOpen(false)} />
+
+
+
+ setEditModalOpen(true),
+ visible: !!(group && can.update),
+ },
+ {
+ title: `${t("Delete")}…`,
+ onClick: () => setDeleteModalOpen(true),
+ visible: !!(group && can.delete),
+ },
+ ]}
+ />
+
+ >
+ );
}
-export default withTranslation()(
- inject("policies")(withRouter(GroupMenu))
-);
+export default observer(GroupMenu);
diff --git a/app/menus/MemberMenu.js b/app/menus/MemberMenu.js
new file mode 100644
index 00000000..efe7e1b3
--- /dev/null
+++ b/app/menus/MemberMenu.js
@@ -0,0 +1,36 @@
+// @flow
+import { observer } from "mobx-react";
+import * as React from "react";
+import { useTranslation } from "react-i18next";
+import { useMenuState } from "reakit/Menu";
+import ContextMenu from "components/ContextMenu";
+import OverflowMenuButton from "components/ContextMenu/OverflowMenuButton";
+import Template from "components/ContextMenu/Template";
+
+type Props = {|
+ onRemove: () => void,
+|};
+
+function MemberMenu({ onRemove }: Props) {
+ const { t } = useTranslation();
+ const menu = useMenuState({ modal: true });
+
+ return (
+ <>
+
+
+
+
+ >
+ );
+}
+
+export default observer(MemberMenu);
diff --git a/app/menus/NewChildDocumentMenu.js b/app/menus/NewChildDocumentMenu.js
index 5b0f11c5..260b998c 100644
--- a/app/menus/NewChildDocumentMenu.js
+++ b/app/menus/NewChildDocumentMenu.js
@@ -1,53 +1,32 @@
// @flow
-import { observable } from "mobx";
-import { observer, inject } from "mobx-react";
+import { observer } from "mobx-react";
import * as React from "react";
-import { Trans, withTranslation, type TFunction } from "react-i18next";
-import { Redirect } from "react-router-dom";
-
-import CollectionsStore from "stores/CollectionsStore";
+import { useTranslation, Trans } from "react-i18next";
+import { useMenuState, MenuButton } from "reakit/Menu";
import Document from "models/Document";
-import { DropdownMenu } from "components/DropdownMenu";
-import DropdownMenuItems from "components/DropdownMenu/DropdownMenuItems";
+import ContextMenu from "components/ContextMenu";
+import Template from "components/ContextMenu/Template";
+import useStores from "hooks/useStores";
import { newDocumentUrl } from "utils/routeHelpers";
type Props = {
- label?: React.Node,
+ label?: (any) => React.Node,
document: Document,
- collections: CollectionsStore,
- t: TFunction,
};
-@observer
-class NewChildDocumentMenu extends React.Component {
- @observable redirectTo: ?string;
+function NewChildDocumentMenu({ document, label }: Props) {
+ const menu = useMenuState({ modal: true });
+ const { collections } = useStores();
+ const { t } = useTranslation();
+ const collection = collections.get(document.collectionId);
+ const collectionName = collection ? collection.name : t("collection");
- componentDidUpdate() {
- this.redirectTo = undefined;
- }
-
- handleNewDocument = () => {
- const { document } = this.props;
- this.redirectTo = newDocumentUrl(document.collectionId);
- };
-
- handleNewChild = () => {
- const { document } = this.props;
- this.redirectTo = newDocumentUrl(document.collectionId, {
- parentDocumentId: document.id,
- });
- };
-
- render() {
- if (this.redirectTo) return ;
-
- const { label, document, collections, t } = this.props;
- const collection = collections.get(document.collectionId);
- const collectionName = collection ? collection.name : t("collection");
-
- return (
-
-
+ {label}
+
+ {
),
- onClick: this.handleNewDocument,
+ to: newDocumentUrl(document.collectionId),
},
{
title: t("New nested document"),
- onClick: this.handleNewChild,
+ to: newDocumentUrl(document.collectionId, {
+ parentDocumentId: document.id,
+ }),
},
]}
/>
-
- );
- }
+
+ >
+ );
}
-export default withTranslation()(
- inject("collections")(NewChildDocumentMenu)
-);
+export default observer(NewChildDocumentMenu);
diff --git a/app/menus/NewDocumentMenu.js b/app/menus/NewDocumentMenu.js
index 65aef421..1de530f3 100644
--- a/app/menus/NewDocumentMenu.js
+++ b/app/menus/NewDocumentMenu.js
@@ -1,92 +1,72 @@
// @flow
-import { observable } from "mobx";
-import { inject, observer } from "mobx-react";
+import { observer } from "mobx-react";
import { PlusIcon } from "outline-icons";
import * as React from "react";
-import { withTranslation, type TFunction } from "react-i18next";
-import { Redirect } from "react-router-dom";
-
-import CollectionsStore from "stores/CollectionsStore";
-import DocumentsStore from "stores/DocumentsStore";
-import PoliciesStore from "stores/PoliciesStore";
+import { useTranslation } from "react-i18next";
+import { Link } from "react-router-dom";
+import { MenuButton, useMenuState } from "reakit/Menu";
+import styled from "styled-components";
import Button from "components/Button";
import CollectionIcon from "components/CollectionIcon";
-import { DropdownMenu, Header } from "components/DropdownMenu";
-import DropdownMenuItems from "components/DropdownMenu/DropdownMenuItems";
+import ContextMenu from "components/ContextMenu";
+import Header from "components/ContextMenu/Header";
+import Template from "components/ContextMenu/Template";
+import Flex from "components/Flex";
+import useStores from "hooks/useStores";
import { newDocumentUrl } from "utils/routeHelpers";
-type Props = {
- label?: React.Node,
- documents: DocumentsStore,
- collections: CollectionsStore,
- policies: PoliciesStore,
- t: TFunction,
-};
+function NewDocumentMenu() {
+ const menu = useMenuState();
+ const { t } = useTranslation();
+ const { collections, policies } = useStores();
+ const singleCollection = collections.orderedData.length === 1;
-@observer
-class NewDocumentMenu extends React.Component {
- @observable redirectTo: ?string;
-
- componentDidUpdate() {
- this.redirectTo = undefined;
+ if (singleCollection) {
+ return (
+ }
+ small
+ >
+ {t("New doc")}
+
+ );
}
- handleNewDocument = (
- collectionId: string,
- options?: {
- parentDocumentId?: string,
- template?: boolean,
- templateId?: string,
- }
- ) => {
- this.redirectTo = newDocumentUrl(collectionId, options);
- };
-
- onOpen = () => {
- const { collections } = this.props;
-
- if (collections.orderedData.length === 1) {
- this.handleNewDocument(collections.orderedData[0].id);
- }
- };
-
- render() {
- if (this.redirectTo) return ;
-
- const { collections, documents, policies, label, t, ...rest } = this.props;
- const singleCollection = collections.orderedData.length === 1;
-
- return (
- } small>
- {t("New doc")}
- {singleCollection ? "" : "…"}
-
- )
- }
- onOpen={this.onOpen}
- {...rest}
- >
+ return (
+ <>
+
+ {(props) => (
+ } {...props} small>
+ {`${t("New doc")}…`}
+
+ )}
+
+
{t("Choose a collection")}
- ({
- onClick: () => this.handleNewDocument(collection.id),
+ to: newDocumentUrl(collection.id),
disabled: !policies.abilities(collection.id).update,
title: (
- <>
+
- {collection.name}
- >
+ {collection.name}
+
),
}))}
/>
-
- );
- }
+
+ >
+ );
}
-export default withTranslation()(
- inject("collections", "documents", "policies")(NewDocumentMenu)
-);
+const CollectionName = styled.div`
+ overflow: hidden;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+`;
+
+export default observer(NewDocumentMenu);
diff --git a/app/menus/NewTemplateMenu.js b/app/menus/NewTemplateMenu.js
index a63e31c7..610fce1f 100644
--- a/app/menus/NewTemplateMenu.js
+++ b/app/menus/NewTemplateMenu.js
@@ -1,74 +1,59 @@
// @flow
-import { observable } from "mobx";
-import { inject, observer } from "mobx-react";
+import { observer } from "mobx-react";
import { PlusIcon } from "outline-icons";
import * as React from "react";
-import { withTranslation, type TFunction } from "react-i18next";
-import { Redirect } from "react-router-dom";
-
-import CollectionsStore from "stores/CollectionsStore";
-import PoliciesStore from "stores/PoliciesStore";
+import { useTranslation } from "react-i18next";
+import { useMenuState, MenuButton } from "reakit/Menu";
+import styled from "styled-components";
import Button from "components/Button";
import CollectionIcon from "components/CollectionIcon";
-import { DropdownMenu, Header } from "components/DropdownMenu";
-import DropdownMenuItems from "components/DropdownMenu/DropdownMenuItems";
+import ContextMenu from "components/ContextMenu";
+import Header from "components/ContextMenu/Header";
+import Template from "components/ContextMenu/Template";
+import Flex from "components/Flex";
+import useStores from "hooks/useStores";
import { newDocumentUrl } from "utils/routeHelpers";
-type Props = {
- label?: React.Node,
- collections: CollectionsStore,
- policies: PoliciesStore,
- t: TFunction,
-};
+function NewTemplateMenu() {
+ const menu = useMenuState();
+ const { t } = useTranslation();
+ const { collections, policies } = useStores();
-@observer
-class NewTemplateMenu extends React.Component {
- @observable redirectTo: ?string;
-
- componentDidUpdate() {
- this.redirectTo = undefined;
- }
-
- handleNewDocument = (collectionId: string) => {
- this.redirectTo = newDocumentUrl(collectionId, {
- template: true,
- });
- };
-
- render() {
- if (this.redirectTo) return ;
-
- const { collections, policies, label, t, ...rest } = this.props;
-
- return (
- } small>
- {t("New template")}…
-
- )
- }
- {...rest}
- >
+ return (
+ <>
+
+ {(props) => (
+ } {...props} small>
+ {t("New template")}…
+
+ )}
+
+
{t("Choose a collection")}
- ({
- onClick: () => this.handleNewDocument(collection.id),
+ to: newDocumentUrl(collection.id, {
+ template: true,
+ }),
disabled: !policies.abilities(collection.id).update,
title: (
- <>
+
- {collection.name}
- >
+ {collection.name}
+
),
}))}
/>
-
- );
- }
+
+ >
+ );
}
-export default withTranslation()(
- inject("collections", "policies")(NewTemplateMenu)
-);
+const CollectionName = styled.div`
+ overflow: hidden;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+`;
+
+export default observer(NewTemplateMenu);
diff --git a/app/menus/RevisionMenu.js b/app/menus/RevisionMenu.js
index 5a2ceae3..ee82da0a 100644
--- a/app/menus/RevisionMenu.js
+++ b/app/menus/RevisionMenu.js
@@ -1,68 +1,69 @@
// @flow
-import { inject } from "mobx-react";
+import { observer } from "mobx-react";
import * as React from "react";
-import { withTranslation, type TFunction } from "react-i18next";
-import { withRouter, type RouterHistory } from "react-router-dom";
-
-import UiStore from "stores/UiStore";
+import { useTranslation } from "react-i18next";
+import { useHistory } from "react-router-dom";
+import { useMenuState } from "reakit/Menu";
import Document from "models/Document";
import Revision from "models/Revision";
+import ContextMenu from "components/ContextMenu";
+import MenuItem from "components/ContextMenu/MenuItem";
+import OverflowMenuButton from "components/ContextMenu/OverflowMenuButton";
+import Separator from "components/ContextMenu/Separator";
import CopyToClipboard from "components/CopyToClipboard";
-import { DropdownMenu, DropdownMenuItem } from "components/DropdownMenu";
+import useStores from "hooks/useStores";
import { documentHistoryUrl } from "utils/routeHelpers";
-type Props = {
- onOpen?: () => void,
- onClose: () => void,
- history: RouterHistory,
+type Props = {|
document: Document,
revision: Revision,
+ iconColor?: string,
className?: string,
- label: React.Node,
- ui: UiStore,
- t: TFunction,
-};
+|};
-class RevisionMenu extends React.Component {
- handleRestore = async (ev: SyntheticEvent<>) => {
- ev.preventDefault();
- await this.props.document.restore({ revisionId: this.props.revision.id });
- const { t } = this.props;
- this.props.ui.showToast(t("Document restored"), { type: "success" });
- this.props.history.push(this.props.document.url);
- };
+function RevisionMenu({ document, revision, className, iconColor }: Props) {
+ const { ui } = useStores();
+ const menu = useMenuState({ modal: true });
+ const { t } = useTranslation();
+ const history = useHistory();
- handleCopy = () => {
- const { t } = this.props;
- this.props.ui.showToast(t("Link copied"), { type: "info" });
- };
+ const handleRestore = React.useCallback(
+ async (ev: SyntheticEvent<>) => {
+ ev.preventDefault();
+ await document.restore({ revisionId: revision.id });
+ ui.showToast(t("Document restored"), { type: "success" });
+ history.push(document.url);
+ },
+ [history, ui, t, document, revision]
+ );
- render() {
- const { className, label, onOpen, onClose, t } = this.props;
- const url = `${window.location.origin}${documentHistoryUrl(
- this.props.document,
- this.props.revision.id
- )}`;
+ const handleCopy = React.useCallback(() => {
+ ui.showToast(t("Link copied"), { type: "info" });
+ }, [ui, t]);
- return (
-
+
-
+ iconColor={iconColor}
+ {...menu}
+ />
+
+
-
-
- {t("Copy link")}
+
+
+
+
-
- );
- }
+
+ >
+ );
}
-export default withTranslation()(
- withRouter(inject("ui")(RevisionMenu))
-);
+export default observer(RevisionMenu);
diff --git a/app/menus/ShareMenu.js b/app/menus/ShareMenu.js
index c4cbbb0b..b94684fc 100644
--- a/app/menus/ShareMenu.js
+++ b/app/menus/ShareMenu.js
@@ -1,75 +1,69 @@
// @flow
-import { observable } from "mobx";
-import { inject, observer } from "mobx-react";
+import { observer } from "mobx-react";
import * as React from "react";
-import { withTranslation, type TFunction } from "react-i18next";
-import { Redirect } from "react-router-dom";
-
-import SharesStore from "stores/SharesStore";
-import UiStore from "stores/UiStore";
+import { useTranslation } from "react-i18next";
+import { useHistory } from "react-router-dom";
+import { useMenuState } from "reakit/Menu";
import Share from "models/Share";
+import ContextMenu from "components/ContextMenu";
+import MenuItem from "components/ContextMenu/MenuItem";
+import OverflowMenuButton from "components/ContextMenu/OverflowMenuButton";
import CopyToClipboard from "components/CopyToClipboard";
-import { DropdownMenu, DropdownMenuItem } from "components/DropdownMenu";
+import useStores from "hooks/useStores";
type Props = {
- onOpen?: () => void,
- onClose: () => void,
- shares: SharesStore,
- ui: UiStore,
share: Share,
- t: TFunction,
};
-@observer
-class ShareMenu extends React.Component {
- @observable redirectTo: ?string;
+function ShareMenu({ share }: Props) {
+ const menu = useMenuState({ modal: true });
+ const { ui, shares } = useStores();
+ const { t } = useTranslation();
+ const history = useHistory();
- componentDidUpdate() {
- this.redirectTo = undefined;
- }
+ const handleGoToDocument = React.useCallback(
+ (ev: SyntheticEvent<>) => {
+ ev.preventDefault();
+ history.push(share.documentUrl);
+ },
+ [history, share]
+ );
- handleGoToDocument = (ev: SyntheticEvent<>) => {
- ev.preventDefault();
- this.redirectTo = this.props.share.documentUrl;
- };
+ const handleRevoke = React.useCallback(
+ async (ev: SyntheticEvent<>) => {
+ ev.preventDefault();
- handleRevoke = async (ev: SyntheticEvent<>) => {
- ev.preventDefault();
+ try {
+ await shares.revoke(share);
+ ui.showToast(t("Share link revoked"), { type: "info" });
+ } catch (err) {
+ ui.showToast(err.message, { type: "error" });
+ }
+ },
+ [t, shares, share, ui]
+ );
- try {
- await this.props.shares.revoke(this.props.share);
- const { t } = this.props;
- this.props.ui.showToast(t("Share link revoked"), { type: "info" });
- } catch (err) {
- this.props.ui.showToast(err.message, { type: "error" });
- }
- };
+ const handleCopy = React.useCallback(() => {
+ ui.showToast(t("Share link copied"), { type: "info" });
+ }, [t, ui]);
- handleCopy = () => {
- const { t } = this.props;
- this.props.ui.showToast(t("Share link copied"), { type: "info" });
- };
-
- render() {
- if (this.redirectTo) return ;
-
- const { share, onOpen, onClose, t } = this.props;
-
- return (
-
-
- {t("Copy link")}
+ return (
+ <>
+
+
+
+
-
+
+
-
+
-
- );
- }
+
+
+ >
+ );
}
-export default withTranslation()(inject("shares", "ui")(ShareMenu));
+export default observer(ShareMenu);
diff --git a/app/menus/TemplatesMenu.js b/app/menus/TemplatesMenu.js
index fe899358..33066475 100644
--- a/app/menus/TemplatesMenu.js
+++ b/app/menus/TemplatesMenu.js
@@ -1,42 +1,42 @@
// @flow
-import { observer, inject } from "mobx-react";
+import { observer } from "mobx-react";
import { DocumentIcon } from "outline-icons";
import * as React from "react";
-import { withTranslation, type TFunction } from "react-i18next";
+import { useTranslation } from "react-i18next";
+import { MenuButton, useMenuState } from "reakit/Menu";
import styled from "styled-components";
-import DocumentsStore from "stores/DocumentsStore";
import Document from "models/Document";
import Button from "components/Button";
-import { DropdownMenu, DropdownMenuItem } from "components/DropdownMenu";
+import ContextMenu from "components/ContextMenu";
+import MenuItem from "components/ContextMenu/MenuItem";
+import useStores from "hooks/useStores";
-type Props = {
+type Props = {|
document: Document,
- documents: DocumentsStore,
- t: TFunction,
-};
+|};
-@observer
-class TemplatesMenu extends React.Component {
- render() {
- const { documents, document, t, ...rest } = this.props;
- const templates = documents.templatesInCollection(document.collectionId);
+function TemplatesMenu({ document }: Props) {
+ const menu = useMenuState({ modal: true });
+ const { documents } = useStores();
+ const { t } = useTranslation();
+ const templates = documents.templatesInCollection(document.collectionId);
- if (!templates.length) {
- return null;
- }
+ if (!templates.length) {
+ return null;
+ }
- return (
-
+ return (
+ <>
+
+ {(props) => (
+
- }
- {...rest}
- >
+ )}
+
+
{templates.map((template) => (
- document.updateFromTemplate(template)}
>
@@ -48,17 +48,15 @@ class TemplatesMenu extends React.Component {
{t("By {{ author }}", { author: template.createdBy.name })}
-
+
))}
-
- );
- }
+
+ >
+ );
}
const Author = styled.div`
font-size: 13px;
`;
-export default withTranslation()(
- inject("documents")(TemplatesMenu)
-);
+export default observer(TemplatesMenu);
diff --git a/app/menus/UserMenu.js b/app/menus/UserMenu.js
index 901499c1..420ea8c4 100644
--- a/app/menus/UserMenu.js
+++ b/app/menus/UserMenu.js
@@ -1,98 +1,110 @@
// @flow
-import { inject, observer } from "mobx-react";
+import { observer } from "mobx-react";
import * as React from "react";
-
-import { withTranslation, type TFunction } from "react-i18next";
-import UsersStore from "stores/UsersStore";
+import { useTranslation } from "react-i18next";
+import { useMenuState } from "reakit/Menu";
import User from "models/User";
-import { DropdownMenu } from "components/DropdownMenu";
-import DropdownMenuItems from "components/DropdownMenu/DropdownMenuItems";
+import ContextMenu from "components/ContextMenu";
+import OverflowMenuButton from "components/ContextMenu/OverflowMenuButton";
+import Template from "components/ContextMenu/Template";
+import useStores from "hooks/useStores";
-type Props = {
+type Props = {|
user: User,
- users: UsersStore,
- t: TFunction,
-};
+|};
-@observer
-class UserMenu extends React.Component {
- handlePromote = (ev: SyntheticEvent<>) => {
- ev.preventDefault();
- const { user, users, t } = this.props;
- if (
- !window.confirm(
- t(
- "Are you sure you want to make {{ userName }} an admin? Admins can modify team and billing information.",
- { userName: user.name }
+function UserMenu({ user }: Props) {
+ const { users } = useStores();
+ const { t } = useTranslation();
+ const menu = useMenuState({ modal: true });
+
+ const handlePromote = React.useCallback(
+ (ev: SyntheticEvent<>) => {
+ ev.preventDefault();
+ if (
+ !window.confirm(
+ t(
+ "Are you sure you want to make {{ userName }} an admin? Admins can modify team and billing information.",
+ { userName: user.name }
+ )
)
- )
- ) {
- return;
- }
- users.promote(user);
- };
+ ) {
+ return;
+ }
+ users.promote(user);
+ },
+ [users, user, t]
+ );
- handleDemote = (ev: SyntheticEvent<>) => {
- ev.preventDefault();
- const { user, users, t } = this.props;
- if (
- !window.confirm(
- t("Are you sure you want to make {{ userName }} a member?", {
- userName: user.name,
- })
- )
- ) {
- return;
- }
- users.demote(user);
- };
-
- handleSuspend = (ev: SyntheticEvent<>) => {
- ev.preventDefault();
- const { user, users, t } = this.props;
- if (
- !window.confirm(
- t(
- "Are you sure you want to suspend this account? Suspended users will be prevented from logging in."
+ const handleDemote = React.useCallback(
+ (ev: SyntheticEvent<>) => {
+ ev.preventDefault();
+ if (
+ !window.confirm(
+ t("Are you sure you want to make {{ userName }} a member?", {
+ userName: user.name,
+ })
)
- )
- ) {
- return;
- }
- users.suspend(user);
- };
+ ) {
+ return;
+ }
+ users.demote(user);
+ },
+ [users, user, t]
+ );
- handleRevoke = (ev: SyntheticEvent<>) => {
- ev.preventDefault();
- const { user, users } = this.props;
- users.delete(user, { confirmation: true });
- };
+ const handleSuspend = React.useCallback(
+ (ev: SyntheticEvent<>) => {
+ ev.preventDefault();
+ if (
+ !window.confirm(
+ t(
+ "Are you sure you want to suspend this account? Suspended users will be prevented from logging in."
+ )
+ )
+ ) {
+ return;
+ }
+ users.suspend(user);
+ },
+ [users, user, t]
+ );
- handleActivate = (ev: SyntheticEvent<>) => {
- ev.preventDefault();
- const { user, users } = this.props;
- users.activate(user);
- };
+ const handleRevoke = React.useCallback(
+ (ev: SyntheticEvent<>) => {
+ ev.preventDefault();
+ users.delete(user, { confirmation: true });
+ },
+ [users, user]
+ );
- render() {
- const { user, t } = this.props;
+ const handleActivate = React.useCallback(
+ (ev: SyntheticEvent<>) => {
+ ev.preventDefault();
+ users.activate(user);
+ },
+ [users, user]
+ );
- return (
-
-
+
+
+ {
},
{
title: `${t("Revoke invite")}…`,
- onClick: this.handleRevoke,
+ onClick: handleRevoke,
visible: user.isInvited,
},
{
title: t("Activate account"),
- onClick: this.handleActivate,
+ onClick: handleActivate,
visible: !user.isInvited && user.isSuspended,
},
{
title: `${t("Suspend account")}…`,
- onClick: this.handleSuspend,
+ onClick: handleSuspend,
visible: !user.isInvited && !user.isSuspended,
},
]}
/>
-
- );
- }
+
+ >
+ );
}
-export default withTranslation()(inject("users")(UserMenu));
+export default observer(UserMenu);
diff --git a/app/scenes/Collection.js b/app/scenes/Collection.js
index 69e3b07e..3fa9034b 100644
--- a/app/scenes/Collection.js
+++ b/app/scenes/Collection.js
@@ -2,7 +2,7 @@
import { observable } from "mobx";
import { observer, inject } from "mobx-react";
-import { NewDocumentIcon, PlusIcon, PinIcon } from "outline-icons";
+import { NewDocumentIcon, PlusIcon, PinIcon, MoreIcon } from "outline-icons";
import * as React from "react";
import { withTranslation, Trans, type TFunction } from "react-i18next";
import { Redirect, Link, Switch, Route, type Match } from "react-router-dom";
@@ -164,7 +164,20 @@ class CollectionScene extends React.Component {
>
)}
-
+ (
+ }
+ {...props}
+ borderOnHover
+ neutral
+ small
+ />
+ )}
+ />
);
diff --git a/app/scenes/CollectionMembers/components/CollectionGroupMemberListItem.js b/app/scenes/CollectionMembers/components/CollectionGroupMemberListItem.js
index 6d9f46e5..ce74910b 100644
--- a/app/scenes/CollectionMembers/components/CollectionGroupMemberListItem.js
+++ b/app/scenes/CollectionMembers/components/CollectionGroupMemberListItem.js
@@ -4,9 +4,9 @@ import { useTranslation } from "react-i18next";
import styled from "styled-components";
import CollectionGroupMembership from "models/CollectionGroupMembership";
import Group from "models/Group";
-import { DropdownMenu, DropdownMenuItem } from "components/DropdownMenu";
import GroupListItem from "components/GroupListItem";
import InputSelect from "components/InputSelect";
+import CollectionGroupMemberMenu from "menus/CollectionGroupMemberMenu";
type Props = {
group: Group,
@@ -50,15 +50,10 @@ const MemberListItem = ({
labelHidden
/>
-
-
- {t("Members")}…
-
-
-
- {t("Remove")}
-
-
+
>
)}
diff --git a/app/scenes/CollectionMembers/components/MemberListItem.js b/app/scenes/CollectionMembers/components/MemberListItem.js
index 2d55ab8e..a5e4f31a 100644
--- a/app/scenes/CollectionMembers/components/MemberListItem.js
+++ b/app/scenes/CollectionMembers/components/MemberListItem.js
@@ -7,11 +7,11 @@ import User from "models/User";
import Avatar from "components/Avatar";
import Badge from "components/Badge";
import Button from "components/Button";
-import { DropdownMenu, DropdownMenuItem } from "components/DropdownMenu";
import Flex from "components/Flex";
import InputSelect from "components/InputSelect";
import ListItem from "components/List/Item";
import Time from "components/Time";
+import MemberMenu from "menus/MemberMenu";
type Props = {
user: User,
@@ -69,13 +69,7 @@ const MemberListItem = ({
/>
)}
- {canEdit && onRemove && (
-
-
- {t("Remove")}
-
-
- )}
+ {canEdit && onRemove && }
{canEdit && onAdd && (
- }
+ )}
/>
)}
@@ -343,15 +343,16 @@ class Header extends React.Component {
(
}
iconColor="currentColor"
+ {...props}
borderOnHover
neutral
small
/>
- }
+ )}
showToggleEmbeds={canToggleEmbeds}
showPrint
/>
diff --git a/app/scenes/GroupMembers/AddPeopleToGroup.js b/app/scenes/GroupMembers/AddPeopleToGroup.js
index 441543b6..14b3f998 100644
--- a/app/scenes/GroupMembers/AddPeopleToGroup.js
+++ b/app/scenes/GroupMembers/AddPeopleToGroup.js
@@ -112,7 +112,6 @@ class AddPeopleToGroup extends React.Component {
key={item.id}
user={item}
onAdd={() => this.handleAddUser(item)}
- canEdit
/>
)}
/>
diff --git a/app/scenes/GroupMembers/GroupMembers.js b/app/scenes/GroupMembers/GroupMembers.js
index 3a6cd480..aa573c60 100644
--- a/app/scenes/GroupMembers/GroupMembers.js
+++ b/app/scenes/GroupMembers/GroupMembers.js
@@ -103,7 +103,6 @@ class GroupMembers extends React.Component {
this.handleRemoveUser(item) : undefined
}
diff --git a/app/scenes/GroupMembers/components/GroupMemberListItem.js b/app/scenes/GroupMembers/components/GroupMemberListItem.js
index be4a255b..a5ca3376 100644
--- a/app/scenes/GroupMembers/components/GroupMemberListItem.js
+++ b/app/scenes/GroupMembers/components/GroupMemberListItem.js
@@ -1,28 +1,21 @@
// @flow
import * as React from "react";
-import GroupMembership from "models/GroupMembership";
import User from "models/User";
import Avatar from "components/Avatar";
import Badge from "components/Badge";
import Button from "components/Button";
-import { DropdownMenu, DropdownMenuItem } from "components/DropdownMenu";
import Flex from "components/Flex";
import ListItem from "components/List/Item";
import Time from "components/Time";
+import GroupMemberMenu from "menus/GroupMemberMenu";
-type Props = {
+type Props = {|
user: User,
- groupMembership?: ?GroupMembership,
onAdd?: () => Promise,
onRemove?: () => Promise,
-};
+|};
-const GroupMemberListItem = ({
- user,
- groupMembership,
- onRemove,
- onAdd,
-}: Props) => {
+const GroupMemberListItem = ({ user, onRemove, onAdd }: Props) => {
return (
}
actions={
- {onRemove && (
-
- Remove
-
- )}
+ {onRemove && }
{onAdd && (
Add
diff --git a/app/scenes/Search/Search.js b/app/scenes/Search/Search.js
index 6a5cf8a5..645682a5 100644
--- a/app/scenes/Search/Search.js
+++ b/app/scenes/Search/Search.js
@@ -361,6 +361,7 @@ class Search extends React.Component {
highlight={this.query}
context={result.context}
showCollection
+ showTemplate
/>
);
})}
diff --git a/app/scenes/Search/components/FilterOption.js b/app/scenes/Search/components/FilterOption.js
index 27c6c539..45265d55 100644
--- a/app/scenes/Search/components/FilterOption.js
+++ b/app/scenes/Search/components/FilterOption.js
@@ -1,45 +1,61 @@
// @flow
import { CheckmarkIcon } from "outline-icons";
import * as React from "react";
+import { MenuItem } from "reakit/Menu";
import styled from "styled-components";
import Flex from "components/Flex";
import HelpText from "components/HelpText";
-type Props = {
+type Props = {|
label: string,
note?: string,
onSelect: () => void,
active: boolean,
-};
+|};
-const FilterOption = ({ label, note, onSelect, active }: Props) => {
+const FilterOption = ({ label, note, onSelect, active, ...rest }: Props) => {
return (
-
-
-
-
- {label}
- {note && {note}}
-
- {active && }
-
-
-
+
);
};
+const Description = styled(HelpText)`
+ margin-bottom: 0;
+`;
+
const Checkmark = styled(CheckmarkIcon)`
flex-shrink: 0;
padding-left: 4px;
fill: ${(props) => props.theme.text};
`;
-const Anchor = styled("a")`
+const Button = styled.button`
display: flex;
flex-direction: column;
font-size: 15px;
padding: 4px 8px;
+ margin: 0;
+ border: 0;
+ background: none;
color: ${(props) => props.theme.text};
+ text-align: left;
+ font-weight: ${(props) => (props.active ? "600" : "normal")};
+ justify-content: center;
+ width: 100%;
min-height: 32px;
${HelpText} {
@@ -54,7 +70,7 @@ const Anchor = styled("a")`
const ListItem = styled("li")`
list-style: none;
- font-weight: ${(props) => (props.active ? "600" : "normal")};
+ max-width: 250px;
`;
export default FilterOption;
diff --git a/app/scenes/Search/components/FilterOptions.js b/app/scenes/Search/components/FilterOptions.js
index 68f31d5e..d83a22f7 100644
--- a/app/scenes/Search/components/FilterOptions.js
+++ b/app/scenes/Search/components/FilterOptions.js
@@ -1,63 +1,74 @@
// @flow
import { find } from "lodash";
import * as React from "react";
+import { useMenuState, MenuButton } from "reakit/Menu";
import styled from "styled-components";
import Button, { Inner } from "components/Button";
-import { DropdownMenu } from "components/DropdownMenu";
+import ContextMenu from "components/ContextMenu";
import FilterOption from "./FilterOption";
-type Props = {
- options: {
- key: string,
- label: string,
- note?: string,
- }[],
+type TFilterOption = {|
+ key: string,
+ label: string,
+ note?: string,
+|};
+
+type Props = {|
+ options: TFilterOption[],
activeKey: ?string,
defaultLabel?: string,
selectedPrefix?: string,
+ className?: string,
onSelect: (key: ?string) => void,
-};
+|};
const FilterOptions = ({
options,
activeKey = "",
defaultLabel,
selectedPrefix = "",
+ className,
onSelect,
}: Props) => {
+ const menu = useMenuState();
const selected = find(options, { key: activeKey }) || options[0];
const selectedLabel = selected ? `${selectedPrefix} ${selected.label}` : "";
return (
-
- {({ closeMenu }) => (
+
+
+ {(props) => (
+
+ {activeKey ? selectedLabel : defaultLabel}
+
+ )}
+
+
{options.map((option) => (
{
onSelect(option.key);
- closeMenu();
+ menu.hide();
}}
active={option.key === activeKey}
{...option}
+ {...menu}
/>
))}
- )}
-
+
+
);
};
-const Content = styled("div")`
- padding: 0 8px;
- width: 250px;
-
- p {
- margin-bottom: 0;
- }
-`;
-
const StyledButton = styled(Button)`
box-shadow: none;
text-transform: none;
@@ -73,32 +84,14 @@ const StyledButton = styled(Button)`
}
`;
-const SearchFilter = (props) => {
- return (
-
- {props.label}
-
- }
- position="right"
- >
- {({ closePortal }) => (
- {props.children({ closeMenu: closePortal })}
- )}
-
- );
-};
-
-const DropdownButton = styled(SearchFilter)`
+const SearchFilter = styled.div`
margin-right: 8px;
`;
const List = styled("ol")`
list-style: none;
margin: 0;
- padding: 0;
+ padding: 0 8px;
`;
export default FilterOptions;
diff --git a/app/utils/compressImage.js b/app/utils/compressImage.js
index cc363a46..d378cc00 100644
--- a/app/utils/compressImage.js
+++ b/app/utils/compressImage.js
@@ -1,7 +1,7 @@
// @flow
import Compressor from "compressorjs";
-type Options = Omit;
+type Options = { maxWidth?: number, maxHeight?: number };
export const compressImage = async (
file: File | Blob,
diff --git a/package.json b/package.json
index 3e48cc0c..990dca85 100644
--- a/package.json
+++ b/package.json
@@ -152,6 +152,7 @@
"react-virtualized-auto-sizer": "^1.0.2",
"react-waypoint": "^9.0.2",
"react-window": "^1.8.6",
+ "reakit": "^1.3.4",
"rich-markdown-editor": "^11.0.11",
"semver": "^7.3.2",
"sequelize": "^6.3.4",
diff --git a/shared/i18n/locales/en_US/translation.json b/shared/i18n/locales/en_US/translation.json
index 257927be..b0c41d23 100644
--- a/shared/i18n/locales/en_US/translation.json
+++ b/shared/i18n/locales/en_US/translation.json
@@ -8,6 +8,7 @@
"Drafts": "Drafts",
"Templates": "Templates",
"Deleted Collection": "Deleted Collection",
+ "Submenu": "Submenu",
"New": "New",
"Only visible to you": "Only visible to you",
"Draft": "Draft",
@@ -22,7 +23,6 @@
"Never viewed": "Never viewed",
"Viewed": "Viewed",
"in": "in",
- "More options": "More options",
"Insert column after": "Insert column after",
"Insert column before": "Insert column before",
"Insert row after": "Insert row after",
@@ -76,6 +76,7 @@
"Warning": "Warning",
"Warning notice": "Warning notice",
"Icon": "Icon",
+ "Choose icon": "Choose icon",
"Loading": "Loading",
"Search": "Search",
"Outline is available in your language {{optionLabel}}, would you like to change?": "Outline is available in your language {{optionLabel}}, would you like to change?",
@@ -104,23 +105,29 @@
"Export Data": "Export Data",
"Integrations": "Integrations",
"Installation": "Installation",
+ "Appearance": "Appearance",
+ "System": "System",
+ "Light": "Light",
+ "Dark": "Dark",
+ "Account": "Account",
"Settings": "Settings",
"API documentation": "API documentation",
"Changelog": "Changelog",
"Send us feedback": "Send us feedback",
"Report a bug": "Report a bug",
- "Appearance": "Appearance",
- "System": "System",
- "Light": "Light",
- "Dark": "Dark",
"Log out": "Log out",
- "Collection permissions": "Collection permissions",
+ "Path to document": "Path to document",
+ "Group member options": "Group member options",
+ "Members": "Members",
+ "Remove": "Remove",
+ "Collection": "Collection",
"New document": "New document",
"Import document": "Import document",
"Edit": "Edit",
"Permissions": "Permissions",
"Export": "Export",
"Delete": "Delete",
+ "Collection permissions": "Collection permissions",
"Edit collection": "Edit collection",
"Delete collection": "Delete collection",
"Export collection": "Export collection",
@@ -131,6 +138,7 @@
"Document archived": "Document archived",
"Document restored": "Document restored",
"Document unpublished": "Document unpublished",
+ "Document options": "Document options",
"Restore": "Restore",
"Choose a collection": "Choose a collection",
"Unpin": "Unpin",
@@ -152,21 +160,26 @@
"Share document": "Share document",
"Edit group": "Edit group",
"Delete group": "Delete group",
- "Members": "Members",
+ "Group options": "Group options",
+ "Member options": "Member options",
"collection": "collection",
+ "New child document": "New child document",
"New document in <1>{{collectionName}}1>": "New document in <1>{{collectionName}}1>",
"New template": "New template",
"Link copied": "Link copied",
+ "Revision options": "Revision options",
"Restore version": "Restore version",
"Copy link": "Copy link",
"Share link revoked": "Share link revoked",
"Share link copied": "Share link copied",
+ "Share options": "Share options",
"Go to document": "Go to document",
"Revoke link": "Revoke link",
"By {{ author }}": "By {{ author }}",
"Are you sure you want to make {{ userName }} an admin? Admins can modify team and billing information.": "Are you sure you want to make {{ userName }} an admin? Admins can modify team and billing information.",
"Are you sure you want to make {{ userName }} a member?": "Are you sure you want to make {{ userName }} a member?",
"Are you sure you want to suspend this account? Suspended users will be prevented from logging in.": "Are you sure you want to suspend this account? Suspended users will be prevented from logging in.",
+ "User options": "User options",
"Make {{ userName }} a member…": "Make {{ userName }} a member…",
"Make {{ userName }} an admin…": "Make {{ userName }} an admin…",
"Revoke invite": "Revoke invite",
@@ -212,7 +225,6 @@
"No people left to add": "No people left to add",
"Read only": "Read only",
"Read & Edit": "Read & Edit",
- "Remove": "Remove",
"Active <1>1> ago": "Active <1>1> ago",
"Never signed in": "Never signed in",
"Invited": "Invited",
diff --git a/yarn.lock b/yarn.lock
index db2d9556..5932e347 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1365,6 +1365,11 @@
dependencies:
"@types/node" ">= 8"
+"@popperjs/core@^2.5.4":
+ version "2.6.0"
+ resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.6.0.tgz#f022195afdfc942e088ee2101285a1d31c7d727f"
+ integrity sha512-cPqjjzuFWNK3BSKLm0abspP0sp/IGOli4p5I5fKFAzdS8fvjdOwDCfZqAaIiXd9lPkOWi3SUUfZof3hEb7J/uw==
+
"@react-dnd/asap@^4.0.0":
version "4.0.0"
resolved "https://registry.yarnpkg.com/@react-dnd/asap/-/asap-4.0.0.tgz#b300eeed83e9801f51bd66b0337c9a6f04548651"
@@ -2604,6 +2609,11 @@ bn.js@^5.0.0, bn.js@^5.1.1:
resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-5.1.3.tgz#beca005408f642ebebea80b042b4d18d2ac0ee6b"
integrity sha512-GkTiFpjFtUzU9CbMeJ5iazkCzGL3jrhzerzZIuqLABjbwRaFt33I9tUdSNryIptM+RxDet6OKm2WnLXzW51KsQ==
+body-scroll-lock@^3.1.5:
+ version "3.1.5"
+ resolved "https://registry.yarnpkg.com/body-scroll-lock/-/body-scroll-lock-3.1.5.tgz#c1392d9217ed2c3e237fee1e910f6cdd80b7aaec"
+ integrity sha512-Yi1Xaml0EvNA0OYWxXiYNqY24AfWkbA6w5vxE7GWxtKfzIbZM+Qw+aSmkgsbWzbHiy/RCSkUZBplVxTA+E4jJg==
+
boolbase@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e"
@@ -10078,6 +10088,36 @@ readdirp@~3.5.0:
dependencies:
picomatch "^2.2.1"
+reakit-system@^0.15.1:
+ version "0.15.1"
+ resolved "https://registry.yarnpkg.com/reakit-system/-/reakit-system-0.15.1.tgz#bf5cc7a03f60a817373bc9cbb4a689c1f4100547"
+ integrity sha512-PkqfAyEohtcEu/gUvKriCv42NywDtUgvocEN3147BI45dOFAB89nrT7wRIbIcKJiUT598F+JlPXAZZVLWhc1Kg==
+ dependencies:
+ reakit-utils "^0.15.1"
+
+reakit-utils@^0.15.1:
+ version "0.15.1"
+ resolved "https://registry.yarnpkg.com/reakit-utils/-/reakit-utils-0.15.1.tgz#797f0a43f6a1dbc22d161224d5d2272e287dbfe3"
+ integrity sha512-6cZgKGvOkAMQgkwU9jdYbHfkuIN1Pr+vwcB19plLvcTfVN0Or10JhIuj9X+JaPZyI7ydqTDFaKNdUcDP69o/+Q==
+
+reakit-warning@^0.6.1:
+ version "0.6.1"
+ resolved "https://registry.yarnpkg.com/reakit-warning/-/reakit-warning-0.6.1.tgz#dba33bb8866aebe30e67ac433ead707d16d38a36"
+ integrity sha512-poFUV0EyxB+CcV9uTNBAFmcgsnR2DzAbOTkld4Ul+QOKSeEHZB3b3+MoZQgcYHmbvG19Na1uWaM7ES+/Eyr8tQ==
+ dependencies:
+ reakit-utils "^0.15.1"
+
+reakit@^1.3.4:
+ version "1.3.4"
+ resolved "https://registry.yarnpkg.com/reakit/-/reakit-1.3.4.tgz#1c83614130fbcee624ba510547db79c90b3435a4"
+ integrity sha512-aLR0GBOc9vELTk4PKK/zndBbIs4RSw4B7GSGY6yoMHQ/vna0iLNd5BMhjtXspD/B7hhkYERlT6di8pIyD3HSPw==
+ dependencies:
+ "@popperjs/core" "^2.5.4"
+ body-scroll-lock "^3.1.5"
+ reakit-system "^0.15.1"
+ reakit-utils "^0.15.1"
+ reakit-warning "^0.6.1"
+
rechoir@^0.6.2:
version "0.6.2"
resolved "https://registry.yarnpkg.com/rechoir/-/rechoir-0.6.2.tgz#85204b54dba82d5742e28c96756ef43af50e3384"