fix: Keyboard accessible context menus (#1768)
- Makes menus fully accessible and keyboard driven - Currently adds 2.8% to initial bundle size due to the inclusion of Reakit and its dependency, popperjs. - Converts all menus to functional components - Remove old custom menu system - Various layout and flow improvements around the menus closes #1766
This commit is contained in:
parent
47369dd968
commit
e8b7782f5e
|
@ -32,6 +32,7 @@ module.file_ext=.json
|
||||||
esproposal.decorators=ignore
|
esproposal.decorators=ignore
|
||||||
esproposal.class_static_fields=enable
|
esproposal.class_static_fields=enable
|
||||||
esproposal.class_instance_fields=enable
|
esproposal.class_instance_fields=enable
|
||||||
|
esproposal.optional_chaining=enable
|
||||||
|
|
||||||
suppress_comment=\\(.\\|\n\\)*\\$FlowFixMe
|
suppress_comment=\\(.\\|\n\\)*\\$FlowFixMe
|
||||||
suppress_comment=\\(.\\|\n\\)*\\$FlowIssue
|
suppress_comment=\\(.\\|\n\\)*\\$FlowIssue
|
||||||
|
|
|
@ -11,11 +11,6 @@ export const Action = styled(Flex)`
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
|
||||||
a {
|
|
||||||
color: ${(props) => props.theme.text};
|
|
||||||
height: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:empty {
|
&:empty {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,6 @@ import {
|
||||||
ArchiveIcon,
|
ArchiveIcon,
|
||||||
EditIcon,
|
EditIcon,
|
||||||
GoToIcon,
|
GoToIcon,
|
||||||
MoreIcon,
|
|
||||||
PadlockIcon,
|
PadlockIcon,
|
||||||
ShapesIcon,
|
ShapesIcon,
|
||||||
TrashIcon,
|
TrashIcon,
|
||||||
|
@ -14,18 +13,15 @@ import { useTranslation } from "react-i18next";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
import breakpoint from "styled-components-breakpoint";
|
import breakpoint from "styled-components-breakpoint";
|
||||||
|
|
||||||
import CollectionsStore from "stores/CollectionsStore";
|
|
||||||
import Document from "models/Document";
|
import Document from "models/Document";
|
||||||
import CollectionIcon from "components/CollectionIcon";
|
import CollectionIcon from "components/CollectionIcon";
|
||||||
import Flex from "components/Flex";
|
import Flex from "components/Flex";
|
||||||
import BreadcrumbMenu from "./BreadcrumbMenu";
|
|
||||||
import useStores from "hooks/useStores";
|
import useStores from "hooks/useStores";
|
||||||
|
import BreadcrumbMenu from "menus/BreadcrumbMenu";
|
||||||
import { collectionUrl } from "utils/routeHelpers";
|
import { collectionUrl } from "utils/routeHelpers";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
document: Document,
|
document: Document,
|
||||||
collections: CollectionsStore,
|
|
||||||
onlyText: boolean,
|
onlyText: boolean,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -133,7 +129,7 @@ const Breadcrumb = ({ document, onlyText }: Props) => {
|
||||||
</CollectionName>
|
</CollectionName>
|
||||||
{isNestedDocument && (
|
{isNestedDocument && (
|
||||||
<>
|
<>
|
||||||
<Slash /> <BreadcrumbMenu label={<Overflow />} path={menuPath} />
|
<Slash /> <BreadcrumbMenu path={menuPath} />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{lastPath && (
|
{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)`
|
const Wrapper = styled(Flex)`
|
||||||
display: none;
|
display: none;
|
||||||
|
|
||||||
|
@ -168,22 +169,6 @@ const SmallSlash = styled(GoToIcon)`
|
||||||
opacity: 0.25;
|
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)`
|
const Crumb = styled(Link)`
|
||||||
color: ${(props) => props.theme.text};
|
color: ${(props) => props.theme.text};
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
|
@ -199,12 +184,17 @@ const Crumb = styled(Link)`
|
||||||
|
|
||||||
const CollectionName = styled(Link)`
|
const CollectionName = styled(Link)`
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-shrink: 0;
|
flex-shrink: 1;
|
||||||
color: ${(props) => props.theme.text};
|
color: ${(props) => props.theme.text};
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
min-width: 0;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export default observer(Breadcrumb);
|
export default observer(Breadcrumb);
|
||||||
|
|
|
@ -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<any>,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function BreadcrumbMenu({ label, path }: Props) {
|
|
||||||
return (
|
|
||||||
<DropdownMenu label={label} position="center">
|
|
||||||
<DropdownMenuItems
|
|
||||||
items={path.map((item) => ({
|
|
||||||
title: item.title,
|
|
||||||
to: item.url,
|
|
||||||
}))}
|
|
||||||
/>
|
|
||||||
</DropdownMenu>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -22,9 +22,13 @@ const RealButton = styled.button`
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
|
|
||||||
|
${(props) =>
|
||||||
|
!props.borderOnHover &&
|
||||||
|
`
|
||||||
svg {
|
svg {
|
||||||
fill: ${(props) => props.iconColor || props.theme.buttonText};
|
fill: ${props.iconColor || props.theme.buttonText};
|
||||||
}
|
}
|
||||||
|
`}
|
||||||
|
|
||||||
&::-moz-focus-inner {
|
&::-moz-focus-inner {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
@ -42,7 +46,7 @@ const RealButton = styled.button`
|
||||||
}
|
}
|
||||||
|
|
||||||
${(props) =>
|
${(props) =>
|
||||||
props.neutral &&
|
props.$neutral &&
|
||||||
`
|
`
|
||||||
background: ${props.theme.buttonNeutralBackground};
|
background: ${props.theme.buttonNeutralBackground};
|
||||||
color: ${props.theme.buttonNeutralText};
|
color: ${props.theme.buttonNeutralText};
|
||||||
|
@ -52,10 +56,15 @@ const RealButton = styled.button`
|
||||||
: `rgba(0, 0, 0, 0.07) 0px 1px 2px, ${props.theme.buttonNeutralBorder} 0 0 0 1px inset`
|
: `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};
|
fill: ${props.iconColor || props.theme.buttonNeutralText};
|
||||||
|
}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background: ${darken(0.05, props.theme.buttonNeutralBackground)};
|
background: ${darken(0.05, props.theme.buttonNeutralBackground)};
|
||||||
box-shadow: rgba(0, 0, 0, 0.07) 0px 1px 2px, ${
|
box-shadow: rgba(0, 0, 0, 0.07) 0px 1px 2px, ${
|
||||||
|
@ -108,6 +117,7 @@ export type Props = {
|
||||||
children?: React.Node,
|
children?: React.Node,
|
||||||
innerRef?: React.ElementRef<any>,
|
innerRef?: React.ElementRef<any>,
|
||||||
disclosure?: boolean,
|
disclosure?: boolean,
|
||||||
|
neutral?: boolean,
|
||||||
fullwidth?: boolean,
|
fullwidth?: boolean,
|
||||||
borderOnHover?: boolean,
|
borderOnHover?: boolean,
|
||||||
};
|
};
|
||||||
|
@ -119,13 +129,14 @@ function Button({
|
||||||
value,
|
value,
|
||||||
disclosure,
|
disclosure,
|
||||||
innerRef,
|
innerRef,
|
||||||
|
neutral,
|
||||||
...rest
|
...rest
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const hasText = children !== undefined || value !== undefined;
|
const hasText = children !== undefined || value !== undefined;
|
||||||
const hasIcon = icon !== undefined;
|
const hasIcon = icon !== undefined;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RealButton type={type} ref={innerRef} {...rest}>
|
<RealButton type={type} ref={innerRef} $neutral={neutral} {...rest}>
|
||||||
<Inner hasIcon={hasIcon} hasText={hasText} disclosure={disclosure}>
|
<Inner hasIcon={hasIcon} hasText={hasText} disclosure={disclosure}>
|
||||||
{hasIcon && icon}
|
{hasIcon && icon}
|
||||||
{hasText && <Label hasIcon={hasIcon}>{children || value}</Label>}
|
{hasText && <Label hasIcon={hasIcon}>{children || value}</Label>}
|
||||||
|
|
|
@ -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;
|
|
@ -1,6 +1,7 @@
|
||||||
// @flow
|
// @flow
|
||||||
import { CheckmarkIcon } from "outline-icons";
|
import { CheckmarkIcon } from "outline-icons";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
|
import { MenuItem as BaseMenuItem } from "reakit/Menu";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
@ -8,23 +9,25 @@ type Props = {
|
||||||
children?: React.Node,
|
children?: React.Node,
|
||||||
selected?: boolean,
|
selected?: boolean,
|
||||||
disabled?: boolean,
|
disabled?: boolean,
|
||||||
|
as?: string | React.ComponentType<*>,
|
||||||
};
|
};
|
||||||
|
|
||||||
const DropdownMenuItem = ({
|
const MenuItem = ({
|
||||||
onClick,
|
onClick,
|
||||||
children,
|
children,
|
||||||
selected,
|
selected,
|
||||||
disabled,
|
disabled,
|
||||||
|
as,
|
||||||
...rest
|
...rest
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
return (
|
return (
|
||||||
<MenuItem
|
<BaseMenuItem
|
||||||
onClick={disabled ? undefined : onClick}
|
onClick={disabled ? undefined : onClick}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
role="menuitem"
|
|
||||||
tabIndex="-1"
|
|
||||||
{...rest}
|
{...rest}
|
||||||
>
|
>
|
||||||
|
{(props) => (
|
||||||
|
<MenuAnchor as={onClick ? "button" : as} {...props}>
|
||||||
{selected !== undefined && (
|
{selected !== undefined && (
|
||||||
<>
|
<>
|
||||||
{selected ? <CheckmarkIcon /> : <Spacer />}
|
{selected ? <CheckmarkIcon /> : <Spacer />}
|
||||||
|
@ -32,7 +35,9 @@ const DropdownMenuItem = ({
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{children}
|
{children}
|
||||||
</MenuItem>
|
</MenuAnchor>
|
||||||
|
)}
|
||||||
|
</BaseMenuItem>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -41,13 +46,14 @@ const Spacer = styled.div`
|
||||||
height: 24px;
|
height: 24px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const MenuItem = styled.a`
|
export const MenuAnchor = styled.a`
|
||||||
display: flex;
|
display: flex;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
border: 0;
|
||||||
padding: 6px 12px;
|
padding: 6px 12px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
min-height: 32px;
|
min-height: 32px;
|
||||||
|
background: none;
|
||||||
color: ${(props) =>
|
color: ${(props) =>
|
||||||
props.disabled ? props.theme.textTertiary : props.theme.textSecondary};
|
props.disabled ? props.theme.textTertiary : props.theme.textSecondary};
|
||||||
justify-content: left;
|
justify-content: left;
|
||||||
|
@ -61,6 +67,7 @@ const MenuItem = styled.a`
|
||||||
}
|
}
|
||||||
|
|
||||||
svg {
|
svg {
|
||||||
|
flex-shrink: 0;
|
||||||
opacity: ${(props) => (props.disabled ? ".5" : 1)};
|
opacity: ${(props) => (props.disabled ? ".5" : 1)};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -69,7 +76,8 @@ const MenuItem = styled.a`
|
||||||
? "pointer-events: none;"
|
? "pointer-events: none;"
|
||||||
: `
|
: `
|
||||||
|
|
||||||
&:hover {
|
&:hover,
|
||||||
|
&.focus-visible {
|
||||||
color: ${props.theme.white};
|
color: ${props.theme.white};
|
||||||
background: ${props.theme.primary};
|
background: ${props.theme.primary};
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
|
@ -87,4 +95,4 @@ const MenuItem = styled.a`
|
||||||
`};
|
`};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export default DropdownMenuItem;
|
export default MenuItem;
|
|
@ -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 (
|
||||||
|
<MenuButton {...rest}>
|
||||||
|
{(props) => (
|
||||||
|
<NudeButton className={className} {...props}>
|
||||||
|
<MoreIcon color={iconColor} />
|
||||||
|
</NudeButton>
|
||||||
|
)}
|
||||||
|
</MenuButton>
|
||||||
|
);
|
||||||
|
}
|
|
@ -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 (
|
||||||
|
<MenuSeparator {...rest}>
|
||||||
|
{(props) => <HorizontalRule {...props} />}
|
||||||
|
</MenuSeparator>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const HorizontalRule = styled.hr`
|
||||||
|
margin: 0.5em 12px;
|
||||||
|
`;
|
|
@ -1,13 +1,19 @@
|
||||||
// @flow
|
// @flow
|
||||||
import { ExpandedIcon } from "outline-icons";
|
import { ExpandedIcon } from "outline-icons";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
|
import {
|
||||||
|
useMenuState,
|
||||||
|
MenuButton,
|
||||||
|
MenuItem as BaseMenuItem,
|
||||||
|
} from "reakit/Menu";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
import Flex from "components/Flex";
|
import MenuItem, { MenuAnchor } from "./MenuItem";
|
||||||
import DropdownMenu from "./DropdownMenu";
|
import Separator from "./Separator";
|
||||||
import DropdownMenuItem from "./DropdownMenuItem";
|
import ContextMenu from ".";
|
||||||
|
|
||||||
type MenuItem =
|
type TMenuItem =
|
||||||
| {|
|
| {|
|
||||||
title: React.Node,
|
title: React.Node,
|
||||||
to: string,
|
to: string,
|
||||||
|
@ -35,7 +41,7 @@ type MenuItem =
|
||||||
disabled?: boolean,
|
disabled?: boolean,
|
||||||
style?: Object,
|
style?: Object,
|
||||||
hover?: boolean,
|
hover?: boolean,
|
||||||
items: MenuItem[],
|
items: TMenuItem[],
|
||||||
|}
|
|}
|
||||||
| {|
|
| {|
|
||||||
type: "separator",
|
type: "separator",
|
||||||
|
@ -48,14 +54,35 @@ type MenuItem =
|
||||||
|};
|
|};
|
||||||
|
|
||||||
type Props = {|
|
type Props = {|
|
||||||
items: MenuItem[],
|
items: TMenuItem[],
|
||||||
|};
|
|};
|
||||||
|
|
||||||
const Disclosure = styled(ExpandedIcon)`
|
const Disclosure = styled(ExpandedIcon)`
|
||||||
transform: rotate(270deg);
|
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 (
|
||||||
|
<>
|
||||||
|
<MenuButton ref={ref} {...menu} {...rest}>
|
||||||
|
{(props) => (
|
||||||
|
<MenuAnchor {...props}>
|
||||||
|
{title} <Disclosure color="currentColor" />
|
||||||
|
</MenuAnchor>
|
||||||
|
)}
|
||||||
|
</MenuButton>
|
||||||
|
<ContextMenu {...menu} aria-label={t("Submenu")}>
|
||||||
|
<Template {...menu} items={templateItems} />
|
||||||
|
</ContextMenu>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
function Template({ items, ...menu }: Props): React.Node {
|
||||||
let filtered = items.filter((item) => item.visible !== false);
|
let filtered = items.filter((item) => item.visible !== false);
|
||||||
|
|
||||||
// this block literally just trims unneccessary separators
|
// this block literally just trims unneccessary separators
|
||||||
|
@ -76,69 +103,67 @@ export default function DropdownMenuItems({ items }: Props): React.Node {
|
||||||
return filtered.map((item, index) => {
|
return filtered.map((item, index) => {
|
||||||
if (item.to) {
|
if (item.to) {
|
||||||
return (
|
return (
|
||||||
<DropdownMenuItem
|
<MenuItem
|
||||||
as={Link}
|
as={Link}
|
||||||
to={item.to}
|
to={item.to}
|
||||||
key={index}
|
key={index}
|
||||||
disabled={item.disabled}
|
disabled={item.disabled}
|
||||||
selected={item.selected}
|
selected={item.selected}
|
||||||
|
{...menu}
|
||||||
>
|
>
|
||||||
{item.title}
|
{item.title}
|
||||||
</DropdownMenuItem>
|
</MenuItem>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (item.href) {
|
if (item.href) {
|
||||||
return (
|
return (
|
||||||
<DropdownMenuItem
|
<MenuItem
|
||||||
href={item.href}
|
href={item.href}
|
||||||
key={index}
|
key={index}
|
||||||
disabled={item.disabled}
|
disabled={item.disabled}
|
||||||
selected={item.selected}
|
selected={item.selected}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
|
{...menu}
|
||||||
>
|
>
|
||||||
{item.title}
|
{item.title}
|
||||||
</DropdownMenuItem>
|
</MenuItem>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (item.onClick) {
|
if (item.onClick) {
|
||||||
return (
|
return (
|
||||||
<DropdownMenuItem
|
<MenuItem
|
||||||
|
as="button"
|
||||||
onClick={item.onClick}
|
onClick={item.onClick}
|
||||||
disabled={item.disabled}
|
disabled={item.disabled}
|
||||||
selected={item.selected}
|
selected={item.selected}
|
||||||
key={index}
|
key={index}
|
||||||
|
{...menu}
|
||||||
>
|
>
|
||||||
{item.title}
|
{item.title}
|
||||||
</DropdownMenuItem>
|
</MenuItem>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (item.items) {
|
if (item.items) {
|
||||||
return (
|
return (
|
||||||
<DropdownMenu
|
<BaseMenuItem
|
||||||
style={item.style}
|
|
||||||
label={
|
|
||||||
<DropdownMenuItem disabled={item.disabled}>
|
|
||||||
<Flex justify="space-between" align="center" auto>
|
|
||||||
{item.title}
|
|
||||||
<Disclosure color="currentColor" />
|
|
||||||
</Flex>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
}
|
|
||||||
hover={item.hover}
|
|
||||||
key={index}
|
key={index}
|
||||||
>
|
as={Submenu}
|
||||||
<DropdownMenuItems items={item.items} />
|
templateItems={item.items}
|
||||||
</DropdownMenu>
|
title={item.title}
|
||||||
|
{...menu}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (item.type === "separator") {
|
if (item.type === "separator") {
|
||||||
return <hr key={index} />;
|
return <Separator key={index} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default React.memo<Props>(Template);
|
|
@ -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 (
|
||||||
|
<Menu {...rest}>
|
||||||
|
{(props) => (
|
||||||
|
<Position {...props}>
|
||||||
|
<Background>
|
||||||
|
{rest.visible || rest.animating ? children : null}
|
||||||
|
</Background>
|
||||||
|
</Position>
|
||||||
|
)}
|
||||||
|
</Menu>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
`;
|
|
@ -1,6 +1,5 @@
|
||||||
// @flow
|
// @flow
|
||||||
import format from "date-fns/format";
|
import format from "date-fns/format";
|
||||||
import { MoreIcon } from "outline-icons";
|
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { NavLink } from "react-router-dom";
|
import { NavLink } from "react-router-dom";
|
||||||
import styled, { withTheme } from "styled-components";
|
import styled, { withTheme } from "styled-components";
|
||||||
|
@ -45,9 +44,7 @@ class RevisionListItem extends React.Component<Props> {
|
||||||
<StyledRevisionMenu
|
<StyledRevisionMenu
|
||||||
document={document}
|
document={document}
|
||||||
revision={revision}
|
revision={revision}
|
||||||
label={
|
iconColor={selected ? theme.white : theme.textTertiary}
|
||||||
<MoreIcon color={selected ? theme.white : theme.textTertiary} />
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</StyledNavLink>
|
</StyledNavLink>
|
||||||
|
|
|
@ -3,8 +3,9 @@ import { observer } from "mobx-react";
|
||||||
import { PlusIcon } from "outline-icons";
|
import { PlusIcon } from "outline-icons";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
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 styled, { css } from "styled-components";
|
||||||
|
import breakpoint from "styled-components-breakpoint";
|
||||||
import Document from "models/Document";
|
import Document from "models/Document";
|
||||||
import Badge from "components/Badge";
|
import Badge from "components/Badge";
|
||||||
import Button from "components/Button";
|
import Button from "components/Button";
|
||||||
|
@ -18,7 +19,7 @@ import useCurrentUser from "hooks/useCurrentUser";
|
||||||
import DocumentMenu from "menus/DocumentMenu";
|
import DocumentMenu from "menus/DocumentMenu";
|
||||||
import { newDocumentUrl } from "utils/routeHelpers";
|
import { newDocumentUrl } from "utils/routeHelpers";
|
||||||
|
|
||||||
type Props = {
|
type Props = {|
|
||||||
document: Document,
|
document: Document,
|
||||||
highlight?: ?string,
|
highlight?: ?string,
|
||||||
context?: ?string,
|
context?: ?string,
|
||||||
|
@ -27,7 +28,7 @@ type Props = {
|
||||||
showPin?: boolean,
|
showPin?: boolean,
|
||||||
showDraft?: boolean,
|
showDraft?: boolean,
|
||||||
showTemplate?: boolean,
|
showTemplate?: boolean,
|
||||||
};
|
|};
|
||||||
|
|
||||||
const SEARCH_RESULT_REGEX = /<b\b[^>]*>(.*?)<\/b>/gi;
|
const SEARCH_RESULT_REGEX = /<b\b[^>]*>(.*?)<\/b>/gi;
|
||||||
|
|
||||||
|
@ -40,7 +41,6 @@ function replaceResultMarks(tag: string) {
|
||||||
function DocumentListItem(props: Props) {
|
function DocumentListItem(props: Props) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const currentUser = useCurrentUser();
|
const currentUser = useCurrentUser();
|
||||||
const history = useHistory();
|
|
||||||
const [menuOpen, setMenuOpen] = React.useState(false);
|
const [menuOpen, setMenuOpen] = React.useState(false);
|
||||||
const {
|
const {
|
||||||
document,
|
document,
|
||||||
|
@ -53,23 +53,11 @@ function DocumentListItem(props: Props) {
|
||||||
context,
|
context,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const handleNewFromTemplate = React.useCallback(
|
|
||||||
(ev: SyntheticEvent<>) => {
|
|
||||||
ev.preventDefault();
|
|
||||||
ev.stopPropagation();
|
|
||||||
|
|
||||||
history.push(
|
|
||||||
newDocumentUrl(document.collectionId, {
|
|
||||||
templateId: document.id,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
},
|
|
||||||
[history, document]
|
|
||||||
);
|
|
||||||
|
|
||||||
const queryIsInTitle =
|
const queryIsInTitle =
|
||||||
!!highlight &&
|
!!highlight &&
|
||||||
!!document.title.toLowerCase().includes(highlight.toLowerCase());
|
!!document.title.toLowerCase().includes(highlight.toLowerCase());
|
||||||
|
const canStar =
|
||||||
|
!document.isDraft && !document.isArchived && !document.isTemplate;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DocumentLink
|
<DocumentLink
|
||||||
|
@ -80,15 +68,16 @@ function DocumentListItem(props: Props) {
|
||||||
state: { title: document.titleWithDefault },
|
state: { title: document.titleWithDefault },
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
<Content>
|
||||||
<Heading>
|
<Heading>
|
||||||
<Title text={document.titleWithDefault} highlight={highlight} />
|
<Title text={document.titleWithDefault} highlight={highlight} />
|
||||||
{document.isNew && document.createdBy.id !== currentUser.id && (
|
{document.isNew && document.createdBy.id !== currentUser.id && (
|
||||||
<Badge yellow>{t("New")}</Badge>
|
<Badge yellow>{t("New")}</Badge>
|
||||||
)}
|
)}
|
||||||
{!document.isDraft && !document.isArchived && !document.isTemplate && (
|
{canStar && (
|
||||||
<Actions>
|
<StarPositioner>
|
||||||
<StarButton document={document} />
|
<StarButton document={document} />
|
||||||
</Actions>
|
</StarPositioner>
|
||||||
)}
|
)}
|
||||||
{document.isDraft && showDraft && (
|
{document.isDraft && showDraft && (
|
||||||
<Tooltip
|
<Tooltip
|
||||||
|
@ -102,22 +91,6 @@ function DocumentListItem(props: Props) {
|
||||||
{document.isTemplate && showTemplate && (
|
{document.isTemplate && showTemplate && (
|
||||||
<Badge primary>{t("Template")}</Badge>
|
<Badge primary>{t("Template")}</Badge>
|
||||||
)}
|
)}
|
||||||
<SecondaryActions>
|
|
||||||
{document.isTemplate && !document.isArchived && !document.isDeleted && (
|
|
||||||
<Button onClick={handleNewFromTemplate} icon={<PlusIcon />} neutral>
|
|
||||||
{t("New doc")}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<EventBoundary>
|
|
||||||
<DocumentMenu
|
|
||||||
document={document}
|
|
||||||
showPin={showPin}
|
|
||||||
onOpen={() => setMenuOpen(true)}
|
|
||||||
onClose={() => setMenuOpen(false)}
|
|
||||||
/>
|
|
||||||
</EventBoundary>
|
|
||||||
</SecondaryActions>
|
|
||||||
</Heading>
|
</Heading>
|
||||||
|
|
||||||
{!queryIsInTitle && (
|
{!queryIsInTitle && (
|
||||||
|
@ -133,30 +106,64 @@ function DocumentListItem(props: Props) {
|
||||||
showPublished={showPublished}
|
showPublished={showPublished}
|
||||||
showLastViewed
|
showLastViewed
|
||||||
/>
|
/>
|
||||||
|
</Content>
|
||||||
|
<Actions>
|
||||||
|
{document.isTemplate && !document.isArchived && !document.isDeleted && (
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
as={Link}
|
||||||
|
to={newDocumentUrl(document.collectionId, {
|
||||||
|
templateId: document.id,
|
||||||
|
})}
|
||||||
|
icon={<PlusIcon />}
|
||||||
|
neutral
|
||||||
|
>
|
||||||
|
{t("New doc")}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<DocumentMenu
|
||||||
|
document={document}
|
||||||
|
showPin={showPin}
|
||||||
|
onOpen={() => setMenuOpen(true)}
|
||||||
|
onClose={() => setMenuOpen(false)}
|
||||||
|
modal={false}
|
||||||
|
/>
|
||||||
|
</Actions>
|
||||||
</DocumentLink>
|
</DocumentLink>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
align-items: center;
|
||||||
position: absolute;
|
margin: 8px;
|
||||||
right: 16px;
|
flex-shrink: 0;
|
||||||
top: 50%;
|
flex-grow: 0;
|
||||||
transform: translateY(-50%);
|
|
||||||
|
${breakpoint("tablet")`
|
||||||
|
display: flex;
|
||||||
|
`};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const DocumentLink = styled(Link)`
|
const DocumentLink = styled(Link)`
|
||||||
display: block;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
margin: 10px -8px;
|
margin: 10px -8px;
|
||||||
padding: 6px 8px;
|
padding: 6px 8px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
max-height: 50vh;
|
max-height: 50vh;
|
||||||
min-width: 100%;
|
min-width: 100%;
|
||||||
max-width: calc(100vw - 40px);
|
max-width: calc(100vw - 40px);
|
||||||
overflow: hidden;
|
|
||||||
position: relative;
|
|
||||||
|
|
||||||
${SecondaryActions} {
|
${Actions} {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -166,10 +173,11 @@ const DocumentLink = styled(Link)`
|
||||||
|
|
||||||
&:hover,
|
&:hover,
|
||||||
&:active,
|
&:active,
|
||||||
&:focus {
|
&:focus,
|
||||||
|
&:focus-within {
|
||||||
background: ${(props) => props.theme.listItemHoverBackground};
|
background: ${(props) => props.theme.listItemHoverBackground};
|
||||||
|
|
||||||
${SecondaryActions} {
|
${Actions} {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -187,7 +195,7 @@ const DocumentLink = styled(Link)`
|
||||||
css`
|
css`
|
||||||
background: ${(props) => props.theme.listItemHoverBackground};
|
background: ${(props) => props.theme.listItemHoverBackground};
|
||||||
|
|
||||||
${SecondaryActions} {
|
${Actions} {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -210,7 +218,7 @@ const Heading = styled.h3`
|
||||||
Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
|
Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const Actions = styled(Flex)`
|
const StarPositioner = styled(Flex)`
|
||||||
margin-left: 4px;
|
margin-left: 4px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
`;
|
`;
|
||||||
|
|
|
@ -15,6 +15,7 @@ const Container = styled(Flex)`
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
min-width: 0;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const Modified = styled.span`
|
const Modified = styled.span`
|
||||||
|
|
|
@ -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<Props> {
|
|
||||||
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<HTMLElement>) => {
|
|
||||||
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 (
|
|
||||||
<div className={className}>
|
|
||||||
<PortalWithState
|
|
||||||
onOpen={this.onOpen}
|
|
||||||
onClose={this.props.onClose}
|
|
||||||
closeOnOutsideClick
|
|
||||||
closeOnEsc
|
|
||||||
>
|
|
||||||
{({ closePortal, openPortal, isOpen, portal }) => (
|
|
||||||
<>
|
|
||||||
<Label
|
|
||||||
onMouseMove={hover ? this.clearCloseTimeout : undefined}
|
|
||||||
onMouseOut={
|
|
||||||
hover ? this.closeAfterTimeout(closePortal) : undefined
|
|
||||||
}
|
|
||||||
onMouseEnter={
|
|
||||||
hover ? this.handleOpen(openPortal, closePortal) : undefined
|
|
||||||
}
|
|
||||||
onClick={
|
|
||||||
hover ? undefined : this.handleOpen(openPortal, closePortal)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{label || (
|
|
||||||
<NudeButton
|
|
||||||
id={`${this.id}button`}
|
|
||||||
aria-label={t("More options")}
|
|
||||||
aria-haspopup="true"
|
|
||||||
aria-expanded={isOpen ? "true" : "false"}
|
|
||||||
aria-controls={this.id}
|
|
||||||
>
|
|
||||||
<MoreIcon />
|
|
||||||
</NudeButton>
|
|
||||||
)}
|
|
||||||
</Label>
|
|
||||||
{portal(
|
|
||||||
<Position
|
|
||||||
ref={this.dropdownRef}
|
|
||||||
position={this.position}
|
|
||||||
fixed={this.fixed}
|
|
||||||
top={this.top}
|
|
||||||
bottom={this.bottom}
|
|
||||||
left={this.left}
|
|
||||||
right={this.right}
|
|
||||||
>
|
|
||||||
<Menu
|
|
||||||
ref={this.menuRef}
|
|
||||||
onMouseMove={hover ? this.clearCloseTimeout : undefined}
|
|
||||||
onMouseOut={
|
|
||||||
hover ? this.closeAfterTimeout(closePortal) : undefined
|
|
||||||
}
|
|
||||||
onClick={
|
|
||||||
typeof children === "function"
|
|
||||||
? undefined
|
|
||||||
: (ev) => {
|
|
||||||
ev.stopPropagation();
|
|
||||||
closePortal();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
style={this.props.style}
|
|
||||||
id={this.id}
|
|
||||||
aria-labelledby={`${this.id}button`}
|
|
||||||
role="menu"
|
|
||||||
>
|
|
||||||
{typeof children === "function"
|
|
||||||
? children({ closePortal })
|
|
||||||
: children}
|
|
||||||
</Menu>
|
|
||||||
</Position>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</PortalWithState>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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>(DropdownMenu);
|
|
|
@ -1,3 +0,0 @@
|
||||||
// @flow
|
|
||||||
export { default as DropdownMenu, Header } from "./DropdownMenu";
|
|
||||||
export { default as DropdownMenuItem } from "./DropdownMenuItem";
|
|
|
@ -4,13 +4,18 @@ import * as React from "react";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
children: React.Node,
|
children: React.Node,
|
||||||
|
className?: string,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function EventBoundary({ children }: Props) {
|
export default function EventBoundary({ children, className }: Props) {
|
||||||
const handleClick = React.useCallback((event: SyntheticEvent<>) => {
|
const handleClick = React.useCallback((event: SyntheticEvent<>) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return <span onClick={handleClick}>{children}</span>;
|
return (
|
||||||
|
<span onClick={handleClick} className={className}>
|
||||||
|
{children}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,4 @@
|
||||||
// @flow
|
// @flow
|
||||||
import { observable } from "mobx";
|
|
||||||
import { observer } from "mobx-react";
|
|
||||||
import {
|
import {
|
||||||
CollectionIcon,
|
CollectionIcon,
|
||||||
CoinsIcon,
|
CoinsIcon,
|
||||||
|
@ -22,14 +20,17 @@ import {
|
||||||
VehicleIcon,
|
VehicleIcon,
|
||||||
} from "outline-icons";
|
} from "outline-icons";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { withTranslation, type TFunction } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { useMenuState, MenuButton, MenuItem } from "reakit/Menu";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
import { DropdownMenu } from "components/DropdownMenu";
|
import ContextMenu from "components/ContextMenu";
|
||||||
import Flex from "components/Flex";
|
import Flex from "components/Flex";
|
||||||
import HelpText from "components/HelpText";
|
import HelpText from "components/HelpText";
|
||||||
import { LabelText } from "components/Input";
|
import { LabelText } from "components/Input";
|
||||||
import NudeButton from "components/NudeButton";
|
import NudeButton from "components/NudeButton";
|
||||||
|
|
||||||
|
const style = { width: 30, height: 30 };
|
||||||
|
|
||||||
const TwitterPicker = React.lazy(() =>
|
const TwitterPicker = React.lazy(() =>
|
||||||
import("react-color/lib/components/twitter/Twitter")
|
import("react-color/lib/components/twitter/Twitter")
|
||||||
);
|
);
|
||||||
|
@ -122,107 +123,77 @@ const colors = [
|
||||||
"#2F362F",
|
"#2F362F",
|
||||||
];
|
];
|
||||||
|
|
||||||
type Props = {
|
type Props = {|
|
||||||
onOpen?: () => void,
|
onOpen?: () => void,
|
||||||
onChange: (color: string, icon: string) => void,
|
onChange: (color: string, icon: string) => void,
|
||||||
icon: string,
|
icon: string,
|
||||||
color: string,
|
color: string,
|
||||||
t: TFunction,
|
|};
|
||||||
};
|
|
||||||
|
|
||||||
function preventEventBubble(event) {
|
function IconPicker({ onOpen, icon, color, onChange }: Props) {
|
||||||
event.stopPropagation();
|
const { t } = useTranslation();
|
||||||
}
|
const menu = useMenuState({
|
||||||
|
modal: true,
|
||||||
@observer
|
placement: "bottom-end",
|
||||||
class IconPicker extends React.Component<Props> {
|
});
|
||||||
@observable isOpen: boolean = false;
|
const Component = icons[icon || "collection"].component;
|
||||||
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 (
|
return (
|
||||||
<Wrapper ref={(ref) => (this.node = ref)}>
|
<Wrapper>
|
||||||
<label>
|
<Label>
|
||||||
<LabelText>{t("Icon")}</LabelText>
|
<LabelText>{t("Icon")}</LabelText>
|
||||||
</label>
|
</Label>
|
||||||
<DropdownMenu
|
<MenuButton {...menu}>
|
||||||
onOpen={this.handleOpen}
|
{(props) => (
|
||||||
label={
|
<Button {...props}>
|
||||||
<LabelButton>
|
<Component role="button" color={color} size={30} />
|
||||||
<Component role="button" color={this.props.color} size={30} />
|
</Button>
|
||||||
</LabelButton>
|
)}
|
||||||
}
|
</MenuButton>
|
||||||
>
|
<ContextMenu {...menu} onOpen={onOpen} aria-label={t("Choose icon")}>
|
||||||
<Icons onClick={preventEventBubble}>
|
<Icons>
|
||||||
{Object.keys(icons).map((name) => {
|
{Object.keys(icons).map((name) => {
|
||||||
const Component = icons[name].component;
|
const Component = icons[name].component;
|
||||||
return (
|
return (
|
||||||
<IconButton
|
<MenuItem
|
||||||
key={name}
|
key={name}
|
||||||
onClick={() => this.props.onChange(this.props.color, name)}
|
onClick={() => onChange(color, name)}
|
||||||
style={{ width: 30, height: 30 }}
|
{...menu}
|
||||||
>
|
>
|
||||||
<Component color={this.props.color} size={30} />
|
{(props) => (
|
||||||
|
<IconButton style={style} {...props}>
|
||||||
|
<Component color={color} size={30} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
|
)}
|
||||||
|
</MenuItem>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</Icons>
|
</Icons>
|
||||||
<Flex onClick={preventEventBubble}>
|
<Flex>
|
||||||
<React.Suspense fallback={<Loading>{t("Loading")}…</Loading>}>
|
<React.Suspense fallback={<Loading>{t("Loading")}…</Loading>}>
|
||||||
<ColorPicker
|
<ColorPicker
|
||||||
color={this.props.color}
|
color={color}
|
||||||
onChange={(color) =>
|
onChange={(color) => onChange(color.hex, icon)}
|
||||||
this.props.onChange(color.hex, this.props.icon)
|
|
||||||
}
|
|
||||||
colors={colors}
|
colors={colors}
|
||||||
triangle="hide"
|
triangle="hide"
|
||||||
/>
|
/>
|
||||||
</React.Suspense>
|
</React.Suspense>
|
||||||
</Flex>
|
</Flex>
|
||||||
</DropdownMenu>
|
</ContextMenu>
|
||||||
</Wrapper>
|
</Wrapper>
|
||||||
);
|
);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const Label = styled.label`
|
||||||
|
display: block;
|
||||||
|
`;
|
||||||
|
|
||||||
const Icons = styled.div`
|
const Icons = styled.div`
|
||||||
padding: 15px 9px 9px 15px;
|
padding: 15px 9px 9px 15px;
|
||||||
width: 276px;
|
width: 276px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const LabelButton = styled(NudeButton)`
|
const Button = styled(NudeButton)`
|
||||||
border: 1px solid ${(props) => props.theme.inputBorder};
|
border: 1px solid ${(props) => props.theme.inputBorder};
|
||||||
width: 32px;
|
width: 32px;
|
||||||
height: 32px;
|
height: 32px;
|
||||||
|
@ -249,4 +220,4 @@ const Wrapper = styled("div")`
|
||||||
position: relative;
|
position: relative;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export default withTranslation()<IconPicker>(IconPicker);
|
export default IconPicker;
|
||||||
|
|
|
@ -75,16 +75,17 @@ class MainSidebar extends React.Component<Props> {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Sidebar>
|
<Sidebar>
|
||||||
<AccountMenu
|
<AccountMenu>
|
||||||
label={
|
{(props) => (
|
||||||
<HeaderBlock
|
<HeaderBlock
|
||||||
|
{...props}
|
||||||
subheading={user.name}
|
subheading={user.name}
|
||||||
teamName={team.name}
|
teamName={team.name}
|
||||||
logoUrl={team.avatarUrl}
|
logoUrl={team.avatarUrl}
|
||||||
showDisclosure
|
showDisclosure
|
||||||
/>
|
/>
|
||||||
}
|
)}
|
||||||
/>
|
</AccountMenu>
|
||||||
<Flex auto column>
|
<Flex auto column>
|
||||||
<Scrollable shadow>
|
<Scrollable shadow>
|
||||||
<Section>
|
<Section>
|
||||||
|
|
|
@ -100,14 +100,12 @@ function CollectionLink({
|
||||||
<>
|
<>
|
||||||
{can.update && (
|
{can.update && (
|
||||||
<CollectionSortMenuWithMargin
|
<CollectionSortMenuWithMargin
|
||||||
position="right"
|
|
||||||
collection={collection}
|
collection={collection}
|
||||||
onOpen={() => setMenuOpen(true)}
|
onOpen={() => setMenuOpen(true)}
|
||||||
onClose={() => setMenuOpen(false)}
|
onClose={() => setMenuOpen(false)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<CollectionMenu
|
<CollectionMenu
|
||||||
position="right"
|
|
||||||
collection={collection}
|
collection={collection}
|
||||||
onOpen={() => setMenuOpen(true)}
|
onOpen={() => setMenuOpen(true)}
|
||||||
onClose={() => setMenuOpen(false)}
|
onClose={() => setMenuOpen(false)}
|
||||||
|
|
|
@ -242,7 +242,6 @@ function DocumentLink({
|
||||||
document && !isMoving ? (
|
document && !isMoving ? (
|
||||||
<Fade>
|
<Fade>
|
||||||
<DocumentMenu
|
<DocumentMenu
|
||||||
position="right"
|
|
||||||
document={document}
|
document={document}
|
||||||
onOpen={() => setMenuOpen(true)}
|
onOpen={() => setMenuOpen(true)}
|
||||||
onClose={() => setMenuOpen(false)}
|
onClose={() => setMenuOpen(false)}
|
||||||
|
|
|
@ -12,15 +12,10 @@ type Props = {
|
||||||
logoUrl: string,
|
logoUrl: string,
|
||||||
};
|
};
|
||||||
|
|
||||||
function HeaderBlock({
|
const HeaderBlock = React.forwardRef<Props, any>(
|
||||||
showDisclosure,
|
({ showDisclosure, teamName, subheading, logoUrl, ...rest }: Props, ref) => {
|
||||||
teamName,
|
|
||||||
subheading,
|
|
||||||
logoUrl,
|
|
||||||
...rest
|
|
||||||
}: Props) {
|
|
||||||
return (
|
return (
|
||||||
<Header justify="flex-start" align="center" {...rest}>
|
<Header justify="flex-start" align="center" ref={ref} {...rest}>
|
||||||
<TeamLogo alt={`${teamName} logo`} src={logoUrl} size="38px" />
|
<TeamLogo alt={`${teamName} logo`} src={logoUrl} size="38px" />
|
||||||
<Flex align="flex-start" column>
|
<Flex align="flex-start" column>
|
||||||
<TeamName showDisclosure>
|
<TeamName showDisclosure>
|
||||||
|
@ -31,7 +26,8 @@ function HeaderBlock({
|
||||||
</Flex>
|
</Flex>
|
||||||
</Header>
|
</Header>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const StyledExpandedIcon = styled(ExpandedIcon)`
|
const StyledExpandedIcon = styled(ExpandedIcon)`
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
|
|
@ -1,7 +1,13 @@
|
||||||
// @flow
|
// @flow
|
||||||
import * as React from "react";
|
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 styled, { withTheme } from "styled-components";
|
||||||
|
import EventBoundary from "components/EventBoundary";
|
||||||
import { type Theme } from "types";
|
import { type Theme } from "types";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
@ -10,6 +16,7 @@ type Props = {
|
||||||
innerRef?: (?HTMLElement) => void,
|
innerRef?: (?HTMLElement) => void,
|
||||||
onClick?: (SyntheticEvent<>) => void,
|
onClick?: (SyntheticEvent<>) => void,
|
||||||
onMouseEnter?: (SyntheticEvent<>) => void,
|
onMouseEnter?: (SyntheticEvent<>) => void,
|
||||||
|
className?: string,
|
||||||
children?: React.Node,
|
children?: React.Node,
|
||||||
icon?: React.Node,
|
icon?: React.Node,
|
||||||
label?: React.Node,
|
label?: React.Node,
|
||||||
|
@ -18,6 +25,8 @@ type Props = {
|
||||||
iconColor?: string,
|
iconColor?: string,
|
||||||
active?: boolean,
|
active?: boolean,
|
||||||
isActiveDrop?: boolean,
|
isActiveDrop?: boolean,
|
||||||
|
history: RouterHistory,
|
||||||
|
match: Match,
|
||||||
theme: Theme,
|
theme: Theme,
|
||||||
exact?: boolean,
|
exact?: boolean,
|
||||||
depth?: number,
|
depth?: number,
|
||||||
|
@ -39,7 +48,9 @@ function SidebarLink({
|
||||||
href,
|
href,
|
||||||
innerRef,
|
innerRef,
|
||||||
depth,
|
depth,
|
||||||
...rest
|
history,
|
||||||
|
match,
|
||||||
|
className,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const style = React.useMemo(() => {
|
const style = React.useMemo(() => {
|
||||||
return {
|
return {
|
||||||
|
@ -70,7 +81,7 @@ function SidebarLink({
|
||||||
as={to ? undefined : href ? "a" : "div"}
|
as={to ? undefined : href ? "a" : "div"}
|
||||||
href={href}
|
href={href}
|
||||||
ref={innerRef}
|
ref={innerRef}
|
||||||
{...rest}
|
className={className}
|
||||||
>
|
>
|
||||||
{icon && <IconWrapper>{icon}</IconWrapper>}
|
{icon && <IconWrapper>{icon}</IconWrapper>}
|
||||||
<Label>{label}</Label>
|
<Label>{label}</Label>
|
||||||
|
@ -84,9 +95,10 @@ const IconWrapper = styled.span`
|
||||||
margin-left: -4px;
|
margin-left: -4px;
|
||||||
margin-right: 4px;
|
margin-right: 4px;
|
||||||
height: 24px;
|
height: 24px;
|
||||||
|
overflow: hidden;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const Actions = styled.span`
|
const Actions = styled(EventBoundary)`
|
||||||
display: ${(props) => (props.showActions ? "inline-flex" : "none")};
|
display: ${(props) => (props.showActions ? "inline-flex" : "none")};
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 4px;
|
top: 4px;
|
||||||
|
@ -110,7 +122,6 @@ const Actions = styled.span`
|
||||||
const StyledNavLink = styled(NavLink)`
|
const StyledNavLink = styled(NavLink)`
|
||||||
display: flex;
|
display: flex;
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
padding: 4px 16px;
|
padding: 4px 16px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
|
@ -121,6 +132,7 @@ const StyledNavLink = styled(NavLink)`
|
||||||
props.$isActiveDrop ? props.theme.white : props.theme.sidebarText};
|
props.$isActiveDrop ? props.theme.white : props.theme.sidebarText};
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
svg {
|
svg {
|
||||||
${(props) => (props.$isActiveDrop ? `fill: ${props.theme.white};` : "")}
|
${(props) => (props.$isActiveDrop ? `fill: ${props.theme.white};` : "")}
|
||||||
|
|
|
@ -8,7 +8,7 @@ import type { Toast as TToast } from "types";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
onRequestClose: () => void,
|
onRequestClose: () => void,
|
||||||
closeAfterMs: number,
|
closeAfterMs?: number,
|
||||||
toast: TToast,
|
toast: TToast,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,134 +1,128 @@
|
||||||
// @flow
|
// @flow
|
||||||
import { observable } from "mobx";
|
import { observer } from "mobx-react";
|
||||||
import { inject, observer } from "mobx-react";
|
|
||||||
import { SunIcon, MoonIcon } from "outline-icons";
|
import { SunIcon, MoonIcon } from "outline-icons";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { withTranslation, type TFunction } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
|
import { useMenuState, MenuButton } from "reakit/Menu";
|
||||||
import styled from "styled-components";
|
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 {
|
import {
|
||||||
developers,
|
developers,
|
||||||
changelog,
|
changelog,
|
||||||
githubIssuesUrl,
|
githubIssuesUrl,
|
||||||
mailToUrl,
|
mailToUrl,
|
||||||
settings,
|
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 = {
|
type Props = {|
|
||||||
label: React.Node,
|
children: (props: any) => React.Node,
|
||||||
ui: UiStore,
|
|};
|
||||||
auth: AuthStore,
|
|
||||||
t: TFunction,
|
|
||||||
};
|
|
||||||
|
|
||||||
@observer
|
const AppearanceMenu = React.forwardRef((props, ref) => {
|
||||||
class AccountMenu extends React.Component<Props> {
|
const { ui } = useStores();
|
||||||
@observable keyboardShortcutsOpen: boolean = false;
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<Modal
|
<MenuButton ref={ref} {...menu} {...props}>
|
||||||
isOpen={this.keyboardShortcutsOpen}
|
{(props) => (
|
||||||
onRequestClose={this.handleCloseKeyboardShortcuts}
|
<MenuAnchor {...props}>
|
||||||
title={t("Keyboard shortcuts")}
|
|
||||||
>
|
|
||||||
<KeyboardShortcuts />
|
|
||||||
</Modal>
|
|
||||||
<DropdownMenu
|
|
||||||
style={{ marginRight: 10, marginTop: -10 }}
|
|
||||||
label={this.props.label}
|
|
||||||
>
|
|
||||||
<DropdownMenuItem as={Link} to={settings()}>
|
|
||||||
{t("Settings")}
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuItem onClick={this.handleOpenKeyboardShortcuts}>
|
|
||||||
{t("Keyboard shortcuts")}
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuItem href={developers()} target="_blank">
|
|
||||||
{t("API documentation")}
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<hr />
|
|
||||||
<DropdownMenuItem href={changelog()} target="_blank">
|
|
||||||
{t("Changelog")}
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuItem href={mailToUrl()} target="_blank">
|
|
||||||
{t("Send us feedback")}
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuItem href={githubIssuesUrl()} target="_blank">
|
|
||||||
{t("Report a bug")}
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<hr />
|
|
||||||
<DropdownMenu
|
|
||||||
position="right"
|
|
||||||
style={{
|
|
||||||
left: 170,
|
|
||||||
position: "relative",
|
|
||||||
top: -40,
|
|
||||||
}}
|
|
||||||
label={
|
|
||||||
<DropdownMenuItem>
|
|
||||||
<ChangeTheme justify="space-between">
|
<ChangeTheme justify="space-between">
|
||||||
{t("Appearance")}
|
{t("Appearance")}
|
||||||
{ui.resolvedTheme === "light" ? <SunIcon /> : <MoonIcon />}
|
{ui.resolvedTheme === "light" ? <SunIcon /> : <MoonIcon />}
|
||||||
</ChangeTheme>
|
</ChangeTheme>
|
||||||
</DropdownMenuItem>
|
</MenuAnchor>
|
||||||
}
|
)}
|
||||||
hover
|
</MenuButton>
|
||||||
>
|
<ContextMenu {...menu} aria-label={t("Appearance")}>
|
||||||
<DropdownMenuItem
|
<MenuItem
|
||||||
|
{...menu}
|
||||||
onClick={() => ui.setTheme("system")}
|
onClick={() => ui.setTheme("system")}
|
||||||
selected={ui.theme === "system"}
|
selected={ui.theme === "system"}
|
||||||
>
|
>
|
||||||
{t("System")}
|
{t("System")}
|
||||||
</DropdownMenuItem>
|
</MenuItem>
|
||||||
<DropdownMenuItem
|
<MenuItem
|
||||||
|
{...menu}
|
||||||
onClick={() => ui.setTheme("light")}
|
onClick={() => ui.setTheme("light")}
|
||||||
selected={ui.theme === "light"}
|
selected={ui.theme === "light"}
|
||||||
>
|
>
|
||||||
{t("Light")}
|
{t("Light")}
|
||||||
</DropdownMenuItem>
|
</MenuItem>
|
||||||
<DropdownMenuItem
|
<MenuItem
|
||||||
|
{...menu}
|
||||||
onClick={() => ui.setTheme("dark")}
|
onClick={() => ui.setTheme("dark")}
|
||||||
selected={ui.theme === "dark"}
|
selected={ui.theme === "dark"}
|
||||||
>
|
>
|
||||||
{t("Dark")}
|
{t("Dark")}
|
||||||
</DropdownMenuItem>
|
</MenuItem>
|
||||||
</DropdownMenu>
|
</ContextMenu>
|
||||||
<hr />
|
</>
|
||||||
<DropdownMenuItem onClick={this.handleLogout}>
|
);
|
||||||
{t("Log out")}
|
});
|
||||||
</DropdownMenuItem>
|
|
||||||
</DropdownMenu>
|
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 (
|
||||||
|
<>
|
||||||
|
<Modal
|
||||||
|
isOpen={keyboardShortcutsOpen}
|
||||||
|
onRequestClose={() => setKeyboardShortcutsOpen(false)}
|
||||||
|
title={t("Keyboard shortcuts")}
|
||||||
|
>
|
||||||
|
<KeyboardShortcuts />
|
||||||
|
</Modal>
|
||||||
|
<MenuButton {...menu}>{props.children}</MenuButton>
|
||||||
|
<ContextMenu {...menu} aria-label={t("Account")}>
|
||||||
|
<MenuItem {...menu} as={Link} to={settings()}>
|
||||||
|
{t("Settings")}
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem {...menu} onClick={() => setKeyboardShortcutsOpen(true)}>
|
||||||
|
{t("Keyboard shortcuts")}
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem {...menu} href={developers()} target="_blank">
|
||||||
|
{t("API documentation")}
|
||||||
|
</MenuItem>
|
||||||
|
<Separator {...menu} />
|
||||||
|
<MenuItem {...menu} href={changelog()} target="_blank">
|
||||||
|
{t("Changelog")}
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem {...menu} href={mailToUrl()} target="_blank">
|
||||||
|
{t("Send us feedback")}
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem {...menu} href={githubIssuesUrl()} target="_blank">
|
||||||
|
{t("Report a bug")}
|
||||||
|
</MenuItem>
|
||||||
|
<Separator {...menu} />
|
||||||
|
<MenuItem {...menu} as={AppearanceMenu} />
|
||||||
|
<Separator {...menu} />
|
||||||
|
<MenuItem {...menu} onClick={auth.logout}>
|
||||||
|
{t("Log out")}
|
||||||
|
</MenuItem>
|
||||||
|
</ContextMenu>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const ChangeTheme = styled(Flex)`
|
const ChangeTheme = styled(Flex)`
|
||||||
width: 100%;
|
width: 100%;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export default withTranslation()<AccountMenu>(
|
export default observer(AccountMenu);
|
||||||
inject("ui", "auth")(AccountMenu)
|
|
||||||
);
|
|
||||||
|
|
|
@ -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<any>,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function BreadcrumbMenu({ path }: Props) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const menu = useMenuState({
|
||||||
|
modal: true,
|
||||||
|
placement: "bottom",
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<OverflowMenuButton {...menu} />
|
||||||
|
<ContextMenu {...menu} aria-label={t("Path to document")}>
|
||||||
|
<Template
|
||||||
|
{...menu}
|
||||||
|
items={path.map((item) => ({
|
||||||
|
title: item.title,
|
||||||
|
to: item.url,
|
||||||
|
}))}
|
||||||
|
/>
|
||||||
|
</ContextMenu>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
|
@ -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 (
|
||||||
|
<>
|
||||||
|
<OverflowMenuButton {...menu} />
|
||||||
|
<ContextMenu {...menu} aria-label={t("Group member options")}>
|
||||||
|
<Template
|
||||||
|
{...menu}
|
||||||
|
items={[
|
||||||
|
{
|
||||||
|
title: t("Members"),
|
||||||
|
onClick: onMembers,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "separator",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("Remove"),
|
||||||
|
onClick: onRemove,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</ContextMenu>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default observer(CollectionGroupMemberMenu);
|
|
@ -1,123 +1,104 @@
|
||||||
// @flow
|
// @flow
|
||||||
import { observable } from "mobx";
|
import { observer } from "mobx-react";
|
||||||
import { inject, observer } from "mobx-react";
|
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { withTranslation, type TFunction } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { withRouter, type RouterHistory } from "react-router-dom";
|
import { useHistory } from "react-router-dom";
|
||||||
import DocumentsStore from "stores/DocumentsStore";
|
import { useMenuState, MenuButton } from "reakit/Menu";
|
||||||
import PoliciesStore from "stores/PoliciesStore";
|
|
||||||
import UiStore from "stores/UiStore";
|
|
||||||
import Collection from "models/Collection";
|
import Collection from "models/Collection";
|
||||||
import CollectionDelete from "scenes/CollectionDelete";
|
import CollectionDelete from "scenes/CollectionDelete";
|
||||||
import CollectionEdit from "scenes/CollectionEdit";
|
import CollectionEdit from "scenes/CollectionEdit";
|
||||||
import CollectionExport from "scenes/CollectionExport";
|
import CollectionExport from "scenes/CollectionExport";
|
||||||
import CollectionMembers from "scenes/CollectionMembers";
|
import CollectionMembers from "scenes/CollectionMembers";
|
||||||
import { DropdownMenu } from "components/DropdownMenu";
|
import ContextMenu from "components/ContextMenu";
|
||||||
import DropdownMenuItems from "components/DropdownMenu/DropdownMenuItems";
|
import OverflowMenuButton from "components/ContextMenu/OverflowMenuButton";
|
||||||
|
import Template from "components/ContextMenu/Template";
|
||||||
import Modal from "components/Modal";
|
import Modal from "components/Modal";
|
||||||
import VisuallyHidden from "components/VisuallyHidden";
|
import VisuallyHidden from "components/VisuallyHidden";
|
||||||
|
import useStores from "hooks/useStores";
|
||||||
import getDataTransferFiles from "utils/getDataTransferFiles";
|
import getDataTransferFiles from "utils/getDataTransferFiles";
|
||||||
import { newDocumentUrl } from "utils/routeHelpers";
|
import { newDocumentUrl } from "utils/routeHelpers";
|
||||||
|
|
||||||
type Props = {
|
type Props = {|
|
||||||
position?: "left" | "right" | "center",
|
|
||||||
ui: UiStore,
|
|
||||||
policies: PoliciesStore,
|
|
||||||
documents: DocumentsStore,
|
|
||||||
collection: Collection,
|
collection: Collection,
|
||||||
history: RouterHistory,
|
placement?: string,
|
||||||
|
modal?: boolean,
|
||||||
|
label?: (any) => React.Node,
|
||||||
onOpen?: () => void,
|
onOpen?: () => void,
|
||||||
onClose?: () => void,
|
onClose?: () => void,
|
||||||
t: TFunction,
|
|};
|
||||||
};
|
|
||||||
|
|
||||||
@observer
|
function CollectionMenu({
|
||||||
class CollectionMenu extends React.Component<Props> {
|
collection,
|
||||||
file: ?HTMLInputElement;
|
label,
|
||||||
@observable showCollectionMembers = false;
|
modal = true,
|
||||||
@observable showCollectionEdit = false;
|
placement,
|
||||||
@observable showCollectionDelete = false;
|
onOpen,
|
||||||
@observable showCollectionExport = false;
|
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<>) => {
|
const file = React.useRef<?HTMLInputElement>();
|
||||||
|
const [showCollectionMembers, setShowCollectionMembers] = React.useState(
|
||||||
|
false
|
||||||
|
);
|
||||||
|
const [showCollectionEdit, setShowCollectionEdit] = React.useState(false);
|
||||||
|
const [showCollectionDelete, setShowCollectionDelete] = React.useState(false);
|
||||||
|
const [showCollectionExport, setShowCollectionExport] = React.useState(false);
|
||||||
|
|
||||||
|
const handleOpen = React.useCallback(() => {
|
||||||
|
setRenderModals(true);
|
||||||
|
if (onOpen) {
|
||||||
|
onOpen();
|
||||||
|
}
|
||||||
|
}, [onOpen]);
|
||||||
|
|
||||||
|
const handleNewDocument = React.useCallback(
|
||||||
|
(ev: SyntheticEvent<>) => {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
const { collection } = this.props;
|
history.push(newDocumentUrl(collection.id));
|
||||||
this.props.history.push(newDocumentUrl(collection.id));
|
},
|
||||||
};
|
[history, collection.id]
|
||||||
|
);
|
||||||
|
|
||||||
onImportDocument = (ev: SyntheticEvent<>) => {
|
const handleImportDocument = React.useCallback(
|
||||||
|
(ev: SyntheticEvent<>) => {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
|
|
||||||
// simulate a click on the file upload input element
|
// simulate a click on the file upload input element
|
||||||
if (this.file) this.file.click();
|
if (file.current) {
|
||||||
};
|
file.current.click();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[file]
|
||||||
|
);
|
||||||
|
|
||||||
onFilePicked = async (ev: SyntheticEvent<>) => {
|
const handleFilePicked = React.useCallback(
|
||||||
|
async (ev: SyntheticEvent<>) => {
|
||||||
const files = getDataTransferFiles(ev);
|
const files = getDataTransferFiles(ev);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const file = files[0];
|
const file = files[0];
|
||||||
const document = await this.props.documents.import(
|
const document = await documents.import(
|
||||||
file,
|
file,
|
||||||
null,
|
null,
|
||||||
this.props.collection.id,
|
this.props.collection.id,
|
||||||
{ publish: true }
|
{ publish: true }
|
||||||
);
|
);
|
||||||
this.props.history.push(document.url);
|
history.push(document.url);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.props.ui.showToast(err.message, {
|
ui.showToast(err.message, {
|
||||||
type: "error",
|
type: "error",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
},
|
||||||
|
[history, ui, documents]
|
||||||
|
);
|
||||||
|
|
||||||
handleEditCollectionOpen = (ev: SyntheticEvent<>) => {
|
|
||||||
ev.preventDefault();
|
|
||||||
this.showCollectionEdit = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
handleEditCollectionClose = () => {
|
|
||||||
this.showCollectionEdit = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
handleDeleteCollectionOpen = (ev: SyntheticEvent<>) => {
|
|
||||||
ev.preventDefault();
|
|
||||||
this.showCollectionDelete = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
handleDeleteCollectionClose = () => {
|
|
||||||
this.showCollectionDelete = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
handleExportCollectionOpen = (ev: SyntheticEvent<>) => {
|
|
||||||
ev.preventDefault();
|
|
||||||
this.showCollectionExport = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
handleExportCollectionClose = () => {
|
|
||||||
this.showCollectionExport = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
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);
|
const can = policies.abilities(collection.id);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -125,37 +106,36 @@ class CollectionMenu extends React.Component<Props> {
|
||||||
<VisuallyHidden>
|
<VisuallyHidden>
|
||||||
<input
|
<input
|
||||||
type="file"
|
type="file"
|
||||||
ref={(ref) => (this.file = ref)}
|
ref={file}
|
||||||
onChange={this.onFilePicked}
|
onChange={handleFilePicked}
|
||||||
onClick={(ev) => ev.stopPropagation()}
|
onClick={(ev) => ev.stopPropagation()}
|
||||||
accept={documents.importFileTypes.join(", ")}
|
accept={documents.importFileTypes.join(", ")}
|
||||||
|
tabIndex="-1"
|
||||||
/>
|
/>
|
||||||
</VisuallyHidden>
|
</VisuallyHidden>
|
||||||
|
{label ? (
|
||||||
<Modal
|
<MenuButton {...menu}>{label}</MenuButton>
|
||||||
title={t("Collection permissions")}
|
) : (
|
||||||
onRequestClose={this.handleMembersModalClose}
|
<OverflowMenuButton {...menu} />
|
||||||
isOpen={this.showCollectionMembers}
|
)}
|
||||||
|
<ContextMenu
|
||||||
|
{...menu}
|
||||||
|
onOpen={handleOpen}
|
||||||
|
onClose={onClose}
|
||||||
|
aria-label={t("Collection")}
|
||||||
>
|
>
|
||||||
<CollectionMembers
|
<Template
|
||||||
collection={collection}
|
{...menu}
|
||||||
onSubmit={this.handleMembersModalClose}
|
|
||||||
handleEditCollectionOpen={this.handleEditCollectionOpen}
|
|
||||||
onEdit={this.handleEditCollectionOpen}
|
|
||||||
/>
|
|
||||||
</Modal>
|
|
||||||
<DropdownMenu onOpen={onOpen} onClose={onClose} position={position}>
|
|
||||||
<DropdownMenuItems
|
|
||||||
items={[
|
items={[
|
||||||
{
|
{
|
||||||
title: t("New document"),
|
title: t("New document"),
|
||||||
visible: can.update,
|
visible: can.update,
|
||||||
onClick: this.onNewDocument,
|
onClick: handleNewDocument,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: t("Import document"),
|
title: t("Import document"),
|
||||||
visible: can.update,
|
visible: can.update,
|
||||||
onClick: this.onImportDocument,
|
onClick: handleImportDocument,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "separator",
|
type: "separator",
|
||||||
|
@ -163,17 +143,17 @@ class CollectionMenu extends React.Component<Props> {
|
||||||
{
|
{
|
||||||
title: `${t("Edit")}…`,
|
title: `${t("Edit")}…`,
|
||||||
visible: can.update,
|
visible: can.update,
|
||||||
onClick: this.handleEditCollectionOpen,
|
onClick: () => setShowCollectionEdit(true),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: `${t("Permissions")}…`,
|
title: `${t("Permissions")}…`,
|
||||||
visible: can.update,
|
visible: can.update,
|
||||||
onClick: this.handleMembersModalOpen,
|
onClick: () => setShowCollectionMembers(true),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: `${t("Export")}…`,
|
title: `${t("Export")}…`,
|
||||||
visible: !!(collection && can.export),
|
visible: !!(collection && can.export),
|
||||||
onClick: this.handleExportCollectionOpen,
|
onClick: () => setShowCollectionExport(true),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "separator",
|
type: "separator",
|
||||||
|
@ -184,46 +164,58 @@ class CollectionMenu extends React.Component<Props> {
|
||||||
{
|
{
|
||||||
title: `${t("Delete")}…`,
|
title: `${t("Delete")}…`,
|
||||||
visible: !!(collection && can.delete),
|
visible: !!(collection && can.delete),
|
||||||
onClick: this.handleDeleteCollectionOpen,
|
onClick: () => setShowCollectionDelete(true),
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
</DropdownMenu>
|
</ContextMenu>
|
||||||
|
{renderModals && (
|
||||||
|
<>
|
||||||
|
<Modal
|
||||||
|
title={t("Collection permissions")}
|
||||||
|
onRequestClose={() => setShowCollectionMembers(false)}
|
||||||
|
isOpen={showCollectionMembers}
|
||||||
|
>
|
||||||
|
<CollectionMembers
|
||||||
|
collection={collection}
|
||||||
|
onSubmit={() => setShowCollectionMembers(false)}
|
||||||
|
onEdit={() => setShowCollectionEdit(true)}
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
<Modal
|
<Modal
|
||||||
title={t("Edit collection")}
|
title={t("Edit collection")}
|
||||||
isOpen={this.showCollectionEdit}
|
isOpen={showCollectionEdit}
|
||||||
onRequestClose={this.handleEditCollectionClose}
|
onRequestClose={() => setShowCollectionEdit(false)}
|
||||||
>
|
>
|
||||||
<CollectionEdit
|
<CollectionEdit
|
||||||
onSubmit={this.handleEditCollectionClose}
|
onSubmit={() => setShowCollectionEdit(false)}
|
||||||
collection={collection}
|
collection={collection}
|
||||||
/>
|
/>
|
||||||
</Modal>
|
</Modal>
|
||||||
<Modal
|
<Modal
|
||||||
title={t("Delete collection")}
|
title={t("Delete collection")}
|
||||||
isOpen={this.showCollectionDelete}
|
isOpen={showCollectionDelete}
|
||||||
onRequestClose={this.handleDeleteCollectionClose}
|
onRequestClose={() => setShowCollectionDelete(false)}
|
||||||
>
|
>
|
||||||
<CollectionDelete
|
<CollectionDelete
|
||||||
onSubmit={this.handleDeleteCollectionClose}
|
onSubmit={() => setShowCollectionDelete(false)}
|
||||||
collection={collection}
|
collection={collection}
|
||||||
/>
|
/>
|
||||||
</Modal>
|
</Modal>
|
||||||
<Modal
|
<Modal
|
||||||
title={t("Export collection")}
|
title={t("Export collection")}
|
||||||
isOpen={this.showCollectionExport}
|
isOpen={showCollectionExport}
|
||||||
onRequestClose={this.handleExportCollectionClose}
|
onRequestClose={() => setShowCollectionExport(false)}
|
||||||
>
|
>
|
||||||
<CollectionExport
|
<CollectionExport
|
||||||
onSubmit={this.handleExportCollectionClose}
|
onSubmit={() => setShowCollectionExport(false)}
|
||||||
collection={collection}
|
collection={collection}
|
||||||
/>
|
/>
|
||||||
</Modal>
|
</Modal>
|
||||||
</>
|
</>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withTranslation()<CollectionMenu>(
|
export default observer(CollectionMenu);
|
||||||
inject("ui", "documents", "policies")(withRouter(CollectionMenu))
|
|
||||||
);
|
|
||||||
|
|
|
@ -3,29 +3,25 @@ import { observer } from "mobx-react";
|
||||||
import { AlphabeticalSortIcon, ManualSortIcon } from "outline-icons";
|
import { AlphabeticalSortIcon, ManualSortIcon } from "outline-icons";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { useMenuState, MenuButton } from "reakit/Menu";
|
||||||
import Collection from "models/Collection";
|
import Collection from "models/Collection";
|
||||||
import { DropdownMenu } from "components/DropdownMenu";
|
import ContextMenu from "components/ContextMenu";
|
||||||
import DropdownMenuItems from "components/DropdownMenu/DropdownMenuItems";
|
import Template from "components/ContextMenu/Template";
|
||||||
import NudeButton from "components/NudeButton";
|
import NudeButton from "components/NudeButton";
|
||||||
|
|
||||||
type Props = {
|
type Props = {|
|
||||||
position?: "left" | "right" | "center",
|
|
||||||
collection: Collection,
|
collection: Collection,
|
||||||
onOpen?: () => void,
|
onOpen?: () => void,
|
||||||
onClose?: () => void,
|
onClose?: () => void,
|
||||||
};
|
|};
|
||||||
|
|
||||||
function CollectionSortMenu({
|
function CollectionSortMenu({ collection, onOpen, onClose, ...rest }: Props) {
|
||||||
collection,
|
|
||||||
position,
|
|
||||||
onOpen,
|
|
||||||
onClose,
|
|
||||||
...rest
|
|
||||||
}: Props) {
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const menu = useMenuState({ modal: true });
|
||||||
|
|
||||||
const handleChangeSort = React.useCallback(
|
const handleChangeSort = React.useCallback(
|
||||||
(field: string) => {
|
(field: string) => {
|
||||||
|
menu.hide();
|
||||||
return collection.save({
|
return collection.save({
|
||||||
sort: {
|
sort: {
|
||||||
field,
|
field,
|
||||||
|
@ -33,24 +29,28 @@ function CollectionSortMenu({
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[collection]
|
[collection, menu]
|
||||||
);
|
);
|
||||||
|
|
||||||
const alphabeticalSort = collection.sort.field === "title";
|
const alphabeticalSort = collection.sort.field === "title";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DropdownMenu
|
<>
|
||||||
onOpen={onOpen}
|
<MenuButton {...menu}>
|
||||||
onClose={onClose}
|
{(props) => (
|
||||||
label={
|
<NudeButton {...props}>
|
||||||
<NudeButton aria-label={t("Sort in sidebar")} aria-haspopup="true">
|
|
||||||
{alphabeticalSort ? <AlphabeticalSortIcon /> : <ManualSortIcon />}
|
{alphabeticalSort ? <AlphabeticalSortIcon /> : <ManualSortIcon />}
|
||||||
</NudeButton>
|
</NudeButton>
|
||||||
}
|
)}
|
||||||
position={position}
|
</MenuButton>
|
||||||
{...rest}
|
<ContextMenu
|
||||||
|
{...menu}
|
||||||
|
onOpen={onOpen}
|
||||||
|
onClose={onClose}
|
||||||
|
aria-label={t("Sort in sidebar")}
|
||||||
>
|
>
|
||||||
<DropdownMenuItems
|
<Template
|
||||||
|
{...menu}
|
||||||
items={[
|
items={[
|
||||||
{
|
{
|
||||||
title: t("Alphabetical sort"),
|
title: t("Alphabetical sort"),
|
||||||
|
@ -64,7 +64,8 @@ function CollectionSortMenu({
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
</DropdownMenu>
|
</ContextMenu>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,21 +1,21 @@
|
||||||
// @flow
|
// @flow
|
||||||
import { observable } from "mobx";
|
import { observer } from "mobx-react";
|
||||||
import { inject, observer } from "mobx-react";
|
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { withTranslation, type TFunction } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Redirect } from "react-router-dom";
|
import { useHistory } from "react-router-dom";
|
||||||
import AuthStore from "stores/AuthStore";
|
import { useMenuState, MenuButton } from "reakit/Menu";
|
||||||
import CollectionStore from "stores/CollectionsStore";
|
import styled from "styled-components";
|
||||||
import PoliciesStore from "stores/PoliciesStore";
|
|
||||||
import UiStore from "stores/UiStore";
|
|
||||||
import Document from "models/Document";
|
import Document from "models/Document";
|
||||||
import DocumentDelete from "scenes/DocumentDelete";
|
import DocumentDelete from "scenes/DocumentDelete";
|
||||||
import DocumentShare from "scenes/DocumentShare";
|
import DocumentShare from "scenes/DocumentShare";
|
||||||
import DocumentTemplatize from "scenes/DocumentTemplatize";
|
import DocumentTemplatize from "scenes/DocumentTemplatize";
|
||||||
import CollectionIcon from "components/CollectionIcon";
|
import CollectionIcon from "components/CollectionIcon";
|
||||||
import { DropdownMenu } from "components/DropdownMenu";
|
import ContextMenu from "components/ContextMenu";
|
||||||
import DropdownMenuItems from "components/DropdownMenu/DropdownMenuItems";
|
import OverflowMenuButton from "components/ContextMenu/OverflowMenuButton";
|
||||||
|
import Template from "components/ContextMenu/Template";
|
||||||
|
import Flex from "components/Flex";
|
||||||
import Modal from "components/Modal";
|
import Modal from "components/Modal";
|
||||||
|
import useStores from "hooks/useStores";
|
||||||
import {
|
import {
|
||||||
documentHistoryUrl,
|
documentHistoryUrl,
|
||||||
documentMoveUrl,
|
documentMoveUrl,
|
||||||
|
@ -24,154 +24,109 @@ import {
|
||||||
newDocumentUrl,
|
newDocumentUrl,
|
||||||
} from "utils/routeHelpers";
|
} from "utils/routeHelpers";
|
||||||
|
|
||||||
type Props = {
|
type Props = {|
|
||||||
ui: UiStore,
|
|
||||||
auth: AuthStore,
|
|
||||||
position?: "left" | "right" | "center",
|
|
||||||
document: Document,
|
document: Document,
|
||||||
collections: CollectionStore,
|
|
||||||
policies: PoliciesStore,
|
|
||||||
className: string,
|
className: string,
|
||||||
isRevision?: boolean,
|
isRevision?: boolean,
|
||||||
showPrint?: boolean,
|
showPrint?: boolean,
|
||||||
|
modal?: boolean,
|
||||||
showToggleEmbeds?: boolean,
|
showToggleEmbeds?: boolean,
|
||||||
showPin?: boolean,
|
showPin?: boolean,
|
||||||
label?: React.Node,
|
label?: (any) => React.Node,
|
||||||
onOpen?: () => void,
|
onOpen?: () => void,
|
||||||
onClose?: () => void,
|
onClose?: () => void,
|
||||||
t: TFunction,
|
|};
|
||||||
};
|
|
||||||
|
|
||||||
@observer
|
function DocumentMenu({
|
||||||
class DocumentMenu extends React.Component<Props> {
|
|
||||||
@observable redirectTo: ?string;
|
|
||||||
@observable showDeleteModal = false;
|
|
||||||
@observable showTemplateModal = false;
|
|
||||||
@observable showShareModal = 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);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
handleMove = (ev: SyntheticEvent<>) => {
|
|
||||||
this.redirectTo = documentMoveUrl(this.props.document);
|
|
||||||
};
|
|
||||||
|
|
||||||
handleEdit = (ev: SyntheticEvent<>) => {
|
|
||||||
this.redirectTo = editDocumentUrl(this.props.document);
|
|
||||||
};
|
|
||||||
|
|
||||||
handleDuplicate = async (ev: SyntheticEvent<>) => {
|
|
||||||
const duped = await this.props.document.duplicate();
|
|
||||||
|
|
||||||
// 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" });
|
|
||||||
};
|
|
||||||
|
|
||||||
handleOpenTemplateModal = () => {
|
|
||||||
this.showTemplateModal = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
handleCloseTemplateModal = () => {
|
|
||||||
this.showTemplateModal = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
handleCloseDeleteModal = () => {
|
|
||||||
this.showDeleteModal = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
handleArchive = async (ev: SyntheticEvent<>) => {
|
|
||||||
await this.props.document.archive();
|
|
||||||
const { t } = this.props;
|
|
||||||
this.props.ui.showToast(t("Document archived"), { type: "success" });
|
|
||||||
};
|
|
||||||
|
|
||||||
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" });
|
|
||||||
};
|
|
||||||
|
|
||||||
handleUnpublish = async (ev: SyntheticEvent<>) => {
|
|
||||||
await this.props.document.unpublish();
|
|
||||||
const { t } = this.props;
|
|
||||||
this.props.ui.showToast(t("Document unpublished"), { type: "success" });
|
|
||||||
};
|
|
||||||
|
|
||||||
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 <Redirect to={this.redirectTo} push />;
|
|
||||||
|
|
||||||
const {
|
|
||||||
policies,
|
|
||||||
document,
|
document,
|
||||||
position,
|
isRevision,
|
||||||
className,
|
className,
|
||||||
|
modal = true,
|
||||||
showToggleEmbeds,
|
showToggleEmbeds,
|
||||||
showPrint,
|
showPrint,
|
||||||
showPin,
|
showPin,
|
||||||
auth,
|
|
||||||
collections,
|
|
||||||
label,
|
label,
|
||||||
onOpen,
|
onOpen,
|
||||||
onClose,
|
onClose,
|
||||||
t,
|
}: Props) {
|
||||||
} = this.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);
|
||||||
|
|
||||||
|
const handleOpen = React.useCallback(() => {
|
||||||
|
setRenderModals(true);
|
||||||
|
if (onOpen) {
|
||||||
|
onOpen();
|
||||||
|
}
|
||||||
|
}, [onOpen]);
|
||||||
|
|
||||||
|
const handleDuplicate = React.useCallback(
|
||||||
|
async (ev: SyntheticEvent<>) => {
|
||||||
|
const duped = await document.duplicate();
|
||||||
|
|
||||||
|
// when duplicating, go straight to the duplicated document content
|
||||||
|
history.push(duped.url);
|
||||||
|
ui.showToast(t("Document duplicated"), { type: "success" });
|
||||||
|
},
|
||||||
|
[ui, t, history, document]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleArchive = React.useCallback(
|
||||||
|
async (ev: SyntheticEvent<>) => {
|
||||||
|
await document.archive();
|
||||||
|
ui.showToast(t("Document archived"), { type: "success" });
|
||||||
|
},
|
||||||
|
[ui, t, document]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleRestore = React.useCallback(
|
||||||
|
async (ev: SyntheticEvent<>, options?: { collectionId: string }) => {
|
||||||
|
await document.restore(options);
|
||||||
|
ui.showToast(t("Document restored"), { type: "success" });
|
||||||
|
},
|
||||||
|
[ui, t, document]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleUnpublish = React.useCallback(
|
||||||
|
async (ev: SyntheticEvent<>) => {
|
||||||
|
await document.unpublish();
|
||||||
|
ui.showToast(t("Document unpublished"), { type: "success" });
|
||||||
|
},
|
||||||
|
[ui, t, document]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handlePrint = React.useCallback((ev: SyntheticEvent<>) => {
|
||||||
|
window.print();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleStar = React.useCallback(
|
||||||
|
(ev: SyntheticEvent<>) => {
|
||||||
|
ev.stopPropagation();
|
||||||
|
document.star();
|
||||||
|
},
|
||||||
|
[document]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleUnstar = React.useCallback(
|
||||||
|
(ev: SyntheticEvent<>) => {
|
||||||
|
ev.stopPropagation();
|
||||||
|
document.unstar();
|
||||||
|
},
|
||||||
|
[document]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleShareLink = React.useCallback(
|
||||||
|
async (ev: SyntheticEvent<>) => {
|
||||||
|
await document.share();
|
||||||
|
setShowShareModal(true);
|
||||||
|
},
|
||||||
|
[document]
|
||||||
|
);
|
||||||
|
|
||||||
const can = policies.abilities(document.id);
|
const can = policies.abilities(document.id);
|
||||||
const canShareDocuments = !!(can.share && auth.team && auth.team.sharing);
|
const canShareDocuments = !!(can.share && auth.team && auth.team.sharing);
|
||||||
|
@ -180,24 +135,29 @@ class DocumentMenu extends React.Component<Props> {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<DropdownMenu
|
{label ? (
|
||||||
className={className}
|
<MenuButton {...menu}>{label}</MenuButton>
|
||||||
position={position}
|
) : (
|
||||||
onOpen={onOpen}
|
<OverflowMenuButton className={className} {...menu} />
|
||||||
|
)}
|
||||||
|
<ContextMenu
|
||||||
|
{...menu}
|
||||||
|
aria-label={t("Document options")}
|
||||||
|
onOpen={handleOpen}
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
label={label}
|
|
||||||
>
|
>
|
||||||
<DropdownMenuItems
|
<Template
|
||||||
|
{...menu}
|
||||||
items={[
|
items={[
|
||||||
{
|
{
|
||||||
title: t("Restore"),
|
title: t("Restore"),
|
||||||
visible: !!can.unarchive,
|
visible: !!can.unarchive,
|
||||||
onClick: this.handleRestore,
|
onClick: handleRestore,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: t("Restore"),
|
title: t("Restore"),
|
||||||
visible: !!(collection && can.restore),
|
visible: !!(collection && can.restore),
|
||||||
onClick: this.handleRestore,
|
onClick: handleRestore,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: t("Restore"),
|
title: t("Restore"),
|
||||||
|
@ -218,13 +178,13 @@ class DocumentMenu extends React.Component<Props> {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
title: (
|
title: (
|
||||||
<>
|
<Flex align="center">
|
||||||
<CollectionIcon collection={collection} />
|
<CollectionIcon collection={collection} />
|
||||||
{collection.name}
|
<CollectionName>{collection.name}</CollectionName>
|
||||||
</>
|
</Flex>
|
||||||
),
|
),
|
||||||
onClick: (ev) =>
|
onClick: (ev) =>
|
||||||
this.handleRestore(ev, { collectionId: collection.id }),
|
handleRestore(ev, { collectionId: collection.id }),
|
||||||
disabled: !can.update,
|
disabled: !can.update,
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
|
@ -232,27 +192,27 @@ class DocumentMenu extends React.Component<Props> {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: t("Unpin"),
|
title: t("Unpin"),
|
||||||
onClick: this.handleUnpin,
|
onClick: document.unpin,
|
||||||
visible: !!(showPin && document.pinned && can.unpin),
|
visible: !!(showPin && document.pinned && can.unpin),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: t("Pin to collection"),
|
title: t("Pin to collection"),
|
||||||
onClick: this.handlePin,
|
onClick: document.pin,
|
||||||
visible: !!(showPin && !document.pinned && can.pin),
|
visible: !!(showPin && !document.pinned && can.pin),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: t("Unstar"),
|
title: t("Unstar"),
|
||||||
onClick: this.handleUnstar,
|
onClick: handleUnstar,
|
||||||
visible: document.isStarred && !!can.unstar,
|
visible: document.isStarred && !!can.unstar,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: t("Star"),
|
title: t("Star"),
|
||||||
onClick: this.handleStar,
|
onClick: handleStar,
|
||||||
visible: !document.isStarred && !!can.star,
|
visible: !document.isStarred && !!can.star,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: `${t("Share link")}…`,
|
title: `${t("Share link")}…`,
|
||||||
onClick: this.handleShareLink,
|
onClick: handleShareLink,
|
||||||
visible: canShareDocuments,
|
visible: canShareDocuments,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -270,42 +230,44 @@ class DocumentMenu extends React.Component<Props> {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: t("New nested document"),
|
title: t("New nested document"),
|
||||||
onClick: this.handleNewChild,
|
to: newDocumentUrl(document.collectionId, {
|
||||||
|
parentDocumentId: document.id,
|
||||||
|
}),
|
||||||
visible: !!can.createChildDocument,
|
visible: !!can.createChildDocument,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: `${t("Create template")}…`,
|
title: `${t("Create template")}…`,
|
||||||
onClick: this.handleOpenTemplateModal,
|
onClick: () => setShowTemplateModal(true),
|
||||||
visible: !!can.update && !document.isTemplate,
|
visible: !!can.update && !document.isTemplate,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: t("Edit"),
|
title: t("Edit"),
|
||||||
onClick: this.handleEdit,
|
to: editDocumentUrl(document),
|
||||||
visible: !!can.update,
|
visible: !!can.update,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: t("Duplicate"),
|
title: t("Duplicate"),
|
||||||
onClick: this.handleDuplicate,
|
onClick: handleDuplicate,
|
||||||
visible: !!can.update,
|
visible: !!can.update,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: t("Unpublish"),
|
title: t("Unpublish"),
|
||||||
onClick: this.handleUnpublish,
|
onClick: handleUnpublish,
|
||||||
visible: !!can.unpublish,
|
visible: !!can.unpublish,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: t("Archive"),
|
title: t("Archive"),
|
||||||
onClick: this.handleArchive,
|
onClick: handleArchive,
|
||||||
visible: !!can.archive,
|
visible: !!can.archive,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: `${t("Delete")}…`,
|
title: `${t("Delete")}…`,
|
||||||
onClick: this.handleDelete,
|
onClick: () => setShowDeleteModal(true),
|
||||||
visible: !!can.delete,
|
visible: !!can.delete,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: `${t("Move")}…`,
|
title: `${t("Move")}…`,
|
||||||
onClick: this.handleMove,
|
to: documentMoveUrl(document),
|
||||||
visible: !!can.move,
|
visible: !!can.move,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -313,59 +275,68 @@ class DocumentMenu extends React.Component<Props> {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: t("History"),
|
title: t("History"),
|
||||||
onClick: this.handleDocumentHistory,
|
to: isRevision
|
||||||
|
? documentUrl(document)
|
||||||
|
: documentHistoryUrl(document),
|
||||||
visible: canViewHistory,
|
visible: canViewHistory,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: t("Download"),
|
title: t("Download"),
|
||||||
onClick: this.handleExport,
|
onClick: document.download,
|
||||||
visible: !!can.download,
|
visible: !!can.download,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: t("Print"),
|
title: t("Print"),
|
||||||
onClick: window.print,
|
onClick: handlePrint,
|
||||||
visible: !!showPrint,
|
visible: !!showPrint,
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
</DropdownMenu>
|
</ContextMenu>
|
||||||
|
{renderModals && (
|
||||||
|
<>
|
||||||
<Modal
|
<Modal
|
||||||
title={t("Delete {{ documentName }}", {
|
title={t("Delete {{ documentName }}", {
|
||||||
documentName: this.props.document.noun,
|
documentName: document.noun,
|
||||||
})}
|
})}
|
||||||
onRequestClose={this.handleCloseDeleteModal}
|
onRequestClose={() => setShowDeleteModal(false)}
|
||||||
isOpen={this.showDeleteModal}
|
isOpen={showDeleteModal}
|
||||||
>
|
>
|
||||||
<DocumentDelete
|
<DocumentDelete
|
||||||
document={this.props.document}
|
document={document}
|
||||||
onSubmit={this.handleCloseDeleteModal}
|
onSubmit={() => setShowDeleteModal(false)}
|
||||||
/>
|
/>
|
||||||
</Modal>
|
</Modal>
|
||||||
<Modal
|
<Modal
|
||||||
title={t("Create template")}
|
title={t("Create template")}
|
||||||
onRequestClose={this.handleCloseTemplateModal}
|
onRequestClose={() => setShowTemplateModal(false)}
|
||||||
isOpen={this.showTemplateModal}
|
isOpen={showTemplateModal}
|
||||||
>
|
>
|
||||||
<DocumentTemplatize
|
<DocumentTemplatize
|
||||||
document={this.props.document}
|
document={document}
|
||||||
onSubmit={this.handleCloseTemplateModal}
|
onSubmit={() => setShowTemplateModal(false)}
|
||||||
/>
|
/>
|
||||||
</Modal>
|
</Modal>
|
||||||
<Modal
|
<Modal
|
||||||
title={t("Share document")}
|
title={t("Share document")}
|
||||||
onRequestClose={this.handleCloseShareModal}
|
onRequestClose={() => setShowShareModal(false)}
|
||||||
isOpen={this.showShareModal}
|
isOpen={showShareModal}
|
||||||
>
|
>
|
||||||
<DocumentShare
|
<DocumentShare
|
||||||
document={this.props.document}
|
document={document}
|
||||||
onSubmit={this.handleCloseShareModal}
|
onSubmit={() => setShowShareModal(false)}
|
||||||
/>
|
/>
|
||||||
</Modal>
|
</Modal>
|
||||||
</>
|
</>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withTranslation()<DocumentMenu>(
|
const CollectionName = styled.div`
|
||||||
inject("ui", "auth", "collections", "policies")(DocumentMenu)
|
overflow: hidden;
|
||||||
);
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export default observer(DocumentMenu);
|
||||||
|
|
|
@ -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 (
|
||||||
|
<>
|
||||||
|
<OverflowMenuButton {...menu} />
|
||||||
|
<ContextMenu {...menu} aria-label={t("Group member options")}>
|
||||||
|
<Template
|
||||||
|
{...menu}
|
||||||
|
items={[
|
||||||
|
{
|
||||||
|
title: t("Remove"),
|
||||||
|
onClick: onRemove,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</ContextMenu>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default observer(GroupMemberMenu);
|
|
@ -1,85 +1,54 @@
|
||||||
// @flow
|
// @flow
|
||||||
import { observable } from "mobx";
|
import { observer } from "mobx-react";
|
||||||
import { inject, observer } from "mobx-react";
|
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { withTranslation, type TFunction } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { withRouter, type RouterHistory } from "react-router-dom";
|
import { useMenuState } from "reakit/Menu";
|
||||||
import PoliciesStore from "stores/PoliciesStore";
|
|
||||||
import UiStore from "stores/UiStore";
|
|
||||||
import Group from "models/Group";
|
import Group from "models/Group";
|
||||||
import GroupDelete from "scenes/GroupDelete";
|
import GroupDelete from "scenes/GroupDelete";
|
||||||
import GroupEdit from "scenes/GroupEdit";
|
import GroupEdit from "scenes/GroupEdit";
|
||||||
import { DropdownMenu } from "components/DropdownMenu";
|
import ContextMenu from "components/ContextMenu";
|
||||||
import DropdownMenuItems from "components/DropdownMenu/DropdownMenuItems";
|
import OverflowMenuButton from "components/ContextMenu/OverflowMenuButton";
|
||||||
|
import Template from "components/ContextMenu/Template";
|
||||||
import Modal from "components/Modal";
|
import Modal from "components/Modal";
|
||||||
|
import useStores from "hooks/useStores";
|
||||||
|
|
||||||
type Props = {
|
type Props = {|
|
||||||
ui: UiStore,
|
|
||||||
policies: PoliciesStore,
|
|
||||||
group: Group,
|
group: Group,
|
||||||
history: RouterHistory,
|
|
||||||
onMembers: () => void,
|
onMembers: () => void,
|
||||||
onOpen?: () => void,
|
|};
|
||||||
onClose?: () => void,
|
|
||||||
t: TFunction,
|
|
||||||
};
|
|
||||||
|
|
||||||
@observer
|
function GroupMenu({ group, onMembers }: Props) {
|
||||||
class GroupMenu extends React.Component<Props> {
|
const { t } = useTranslation();
|
||||||
@observable editModalOpen: boolean = false;
|
const { policies } = useStores();
|
||||||
@observable deleteModalOpen: boolean = false;
|
const menu = useMenuState({ modal: true });
|
||||||
|
const [editModalOpen, setEditModalOpen] = React.useState(false);
|
||||||
onEdit = (ev: SyntheticEvent<>) => {
|
const [deleteModalOpen, setDeleteModalOpen] = React.useState(false);
|
||||||
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);
|
const can = policies.abilities(group.id);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Modal
|
<Modal
|
||||||
title={t("Edit group")}
|
title={t("Edit group")}
|
||||||
onRequestClose={this.handleEditModalClose}
|
onRequestClose={() => setEditModalOpen(false)}
|
||||||
isOpen={this.editModalOpen}
|
isOpen={editModalOpen}
|
||||||
>
|
>
|
||||||
<GroupEdit
|
<GroupEdit group={group} onSubmit={() => setEditModalOpen(false)} />
|
||||||
group={this.props.group}
|
|
||||||
onSubmit={this.handleEditModalClose}
|
|
||||||
/>
|
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
<Modal
|
<Modal
|
||||||
title={t("Delete group")}
|
title={t("Delete group")}
|
||||||
onRequestClose={this.handleDeleteModalClose}
|
onRequestClose={() => setDeleteModalOpen(false)}
|
||||||
isOpen={this.deleteModalOpen}
|
isOpen={deleteModalOpen}
|
||||||
>
|
>
|
||||||
<GroupDelete
|
<GroupDelete group={group} onSubmit={() => setDeleteModalOpen(false)} />
|
||||||
group={this.props.group}
|
|
||||||
onSubmit={this.handleDeleteModalClose}
|
|
||||||
/>
|
|
||||||
</Modal>
|
</Modal>
|
||||||
<DropdownMenu onOpen={onOpen} onClose={onClose}>
|
<OverflowMenuButton {...menu} />
|
||||||
<DropdownMenuItems
|
<ContextMenu {...menu} aria-label={t("Group options")}>
|
||||||
|
<Template
|
||||||
|
{...menu}
|
||||||
items={[
|
items={[
|
||||||
{
|
{
|
||||||
title: `${t("Members")}…`,
|
title: `${t("Members")}…`,
|
||||||
onClick: this.props.onMembers,
|
onClick: onMembers,
|
||||||
visible: !!(group && can.read),
|
visible: !!(group && can.read),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -87,22 +56,19 @@ class GroupMenu extends React.Component<Props> {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: `${t("Edit")}…`,
|
title: `${t("Edit")}…`,
|
||||||
onClick: this.onEdit,
|
onClick: () => setEditModalOpen(true),
|
||||||
visible: !!(group && can.update),
|
visible: !!(group && can.update),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: `${t("Delete")}…`,
|
title: `${t("Delete")}…`,
|
||||||
onClick: this.onDelete,
|
onClick: () => setDeleteModalOpen(true),
|
||||||
visible: !!(group && can.delete),
|
visible: !!(group && can.delete),
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
</DropdownMenu>
|
</ContextMenu>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withTranslation()<GroupMenu>(
|
export default observer(GroupMenu);
|
||||||
inject("policies")(withRouter(GroupMenu))
|
|
||||||
);
|
|
||||||
|
|
|
@ -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 (
|
||||||
|
<>
|
||||||
|
<OverflowMenuButton {...menu} />
|
||||||
|
<ContextMenu {...menu} aria-label={t("Member options")}>
|
||||||
|
<Template
|
||||||
|
{...menu}
|
||||||
|
items={[
|
||||||
|
{
|
||||||
|
title: t("Remove"),
|
||||||
|
onClick: onRemove,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</ContextMenu>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default observer(MemberMenu);
|
|
@ -1,53 +1,32 @@
|
||||||
// @flow
|
// @flow
|
||||||
import { observable } from "mobx";
|
import { observer } from "mobx-react";
|
||||||
import { observer, inject } from "mobx-react";
|
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { Trans, withTranslation, type TFunction } from "react-i18next";
|
import { useTranslation, Trans } from "react-i18next";
|
||||||
import { Redirect } from "react-router-dom";
|
import { useMenuState, MenuButton } from "reakit/Menu";
|
||||||
|
|
||||||
import CollectionsStore from "stores/CollectionsStore";
|
|
||||||
import Document from "models/Document";
|
import Document from "models/Document";
|
||||||
import { DropdownMenu } from "components/DropdownMenu";
|
import ContextMenu from "components/ContextMenu";
|
||||||
import DropdownMenuItems from "components/DropdownMenu/DropdownMenuItems";
|
import Template from "components/ContextMenu/Template";
|
||||||
|
import useStores from "hooks/useStores";
|
||||||
import { newDocumentUrl } from "utils/routeHelpers";
|
import { newDocumentUrl } from "utils/routeHelpers";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
label?: React.Node,
|
label?: (any) => React.Node,
|
||||||
document: Document,
|
document: Document,
|
||||||
collections: CollectionsStore,
|
|
||||||
t: TFunction,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
@observer
|
function NewChildDocumentMenu({ document, label }: Props) {
|
||||||
class NewChildDocumentMenu extends React.Component<Props> {
|
const menu = useMenuState({ modal: true });
|
||||||
@observable redirectTo: ?string;
|
const { collections } = useStores();
|
||||||
|
const { t } = useTranslation();
|
||||||
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 <Redirect to={this.redirectTo} push />;
|
|
||||||
|
|
||||||
const { label, document, collections, t } = this.props;
|
|
||||||
const collection = collections.get(document.collectionId);
|
const collection = collections.get(document.collectionId);
|
||||||
const collectionName = collection ? collection.name : t("collection");
|
const collectionName = collection ? collection.name : t("collection");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DropdownMenu label={label}>
|
<>
|
||||||
<DropdownMenuItems
|
<MenuButton {...menu}>{label}</MenuButton>
|
||||||
|
<ContextMenu {...menu} aria-label={t("New child document")}>
|
||||||
|
<Template
|
||||||
|
{...menu}
|
||||||
items={[
|
items={[
|
||||||
{
|
{
|
||||||
title: (
|
title: (
|
||||||
|
@ -57,19 +36,19 @@ class NewChildDocumentMenu extends React.Component<Props> {
|
||||||
</Trans>
|
</Trans>
|
||||||
</span>
|
</span>
|
||||||
),
|
),
|
||||||
onClick: this.handleNewDocument,
|
to: newDocumentUrl(document.collectionId),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: t("New nested document"),
|
title: t("New nested document"),
|
||||||
onClick: this.handleNewChild,
|
to: newDocumentUrl(document.collectionId, {
|
||||||
|
parentDocumentId: document.id,
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
</DropdownMenu>
|
</ContextMenu>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withTranslation()<NewChildDocumentMenu>(
|
export default observer(NewChildDocumentMenu);
|
||||||
inject("collections")(NewChildDocumentMenu)
|
|
||||||
);
|
|
||||||
|
|
|
@ -1,92 +1,72 @@
|
||||||
// @flow
|
// @flow
|
||||||
import { observable } from "mobx";
|
import { observer } from "mobx-react";
|
||||||
import { inject, observer } from "mobx-react";
|
|
||||||
import { PlusIcon } from "outline-icons";
|
import { PlusIcon } from "outline-icons";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { withTranslation, type TFunction } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Redirect } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
|
import { MenuButton, useMenuState } from "reakit/Menu";
|
||||||
import CollectionsStore from "stores/CollectionsStore";
|
import styled from "styled-components";
|
||||||
import DocumentsStore from "stores/DocumentsStore";
|
|
||||||
import PoliciesStore from "stores/PoliciesStore";
|
|
||||||
import Button from "components/Button";
|
import Button from "components/Button";
|
||||||
import CollectionIcon from "components/CollectionIcon";
|
import CollectionIcon from "components/CollectionIcon";
|
||||||
import { DropdownMenu, Header } from "components/DropdownMenu";
|
import ContextMenu from "components/ContextMenu";
|
||||||
import DropdownMenuItems from "components/DropdownMenu/DropdownMenuItems";
|
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";
|
import { newDocumentUrl } from "utils/routeHelpers";
|
||||||
|
|
||||||
type Props = {
|
function NewDocumentMenu() {
|
||||||
label?: React.Node,
|
const menu = useMenuState();
|
||||||
documents: DocumentsStore,
|
const { t } = useTranslation();
|
||||||
collections: CollectionsStore,
|
const { collections, policies } = useStores();
|
||||||
policies: PoliciesStore,
|
|
||||||
t: TFunction,
|
|
||||||
};
|
|
||||||
|
|
||||||
@observer
|
|
||||||
class NewDocumentMenu extends React.Component<Props> {
|
|
||||||
@observable redirectTo: ?string;
|
|
||||||
|
|
||||||
componentDidUpdate() {
|
|
||||||
this.redirectTo = undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
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 <Redirect to={this.redirectTo} push />;
|
|
||||||
|
|
||||||
const { collections, documents, policies, label, t, ...rest } = this.props;
|
|
||||||
const singleCollection = collections.orderedData.length === 1;
|
const singleCollection = collections.orderedData.length === 1;
|
||||||
|
|
||||||
|
if (singleCollection) {
|
||||||
return (
|
return (
|
||||||
<DropdownMenu
|
<Button
|
||||||
label={
|
as={Link}
|
||||||
label || (
|
to={newDocumentUrl(collections.orderedData[0].id)}
|
||||||
<Button icon={<PlusIcon />} small>
|
icon={<PlusIcon />}
|
||||||
{t("New doc")}
|
small
|
||||||
{singleCollection ? "" : "…"}
|
|
||||||
</Button>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
onOpen={this.onOpen}
|
|
||||||
{...rest}
|
|
||||||
>
|
>
|
||||||
|
{t("New doc")}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<MenuButton {...menu}>
|
||||||
|
{(props) => (
|
||||||
|
<Button icon={<PlusIcon />} {...props} small>
|
||||||
|
{`${t("New doc")}…`}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</MenuButton>
|
||||||
|
<ContextMenu {...menu} aria-label={t("New document")}>
|
||||||
<Header>{t("Choose a collection")}</Header>
|
<Header>{t("Choose a collection")}</Header>
|
||||||
<DropdownMenuItems
|
<Template
|
||||||
|
{...menu}
|
||||||
items={collections.orderedData.map((collection) => ({
|
items={collections.orderedData.map((collection) => ({
|
||||||
onClick: () => this.handleNewDocument(collection.id),
|
to: newDocumentUrl(collection.id),
|
||||||
disabled: !policies.abilities(collection.id).update,
|
disabled: !policies.abilities(collection.id).update,
|
||||||
title: (
|
title: (
|
||||||
<>
|
<Flex align="center">
|
||||||
<CollectionIcon collection={collection} />
|
<CollectionIcon collection={collection} />
|
||||||
{collection.name}
|
<CollectionName>{collection.name}</CollectionName>
|
||||||
</>
|
</Flex>
|
||||||
),
|
),
|
||||||
}))}
|
}))}
|
||||||
/>
|
/>
|
||||||
</DropdownMenu>
|
</ContextMenu>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withTranslation()<NewDocumentMenu>(
|
const CollectionName = styled.div`
|
||||||
inject("collections", "documents", "policies")(NewDocumentMenu)
|
overflow: hidden;
|
||||||
);
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export default observer(NewDocumentMenu);
|
||||||
|
|
|
@ -1,74 +1,59 @@
|
||||||
// @flow
|
// @flow
|
||||||
import { observable } from "mobx";
|
import { observer } from "mobx-react";
|
||||||
import { inject, observer } from "mobx-react";
|
|
||||||
import { PlusIcon } from "outline-icons";
|
import { PlusIcon } from "outline-icons";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { withTranslation, type TFunction } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Redirect } from "react-router-dom";
|
import { useMenuState, MenuButton } from "reakit/Menu";
|
||||||
|
import styled from "styled-components";
|
||||||
import CollectionsStore from "stores/CollectionsStore";
|
|
||||||
import PoliciesStore from "stores/PoliciesStore";
|
|
||||||
import Button from "components/Button";
|
import Button from "components/Button";
|
||||||
import CollectionIcon from "components/CollectionIcon";
|
import CollectionIcon from "components/CollectionIcon";
|
||||||
import { DropdownMenu, Header } from "components/DropdownMenu";
|
import ContextMenu from "components/ContextMenu";
|
||||||
import DropdownMenuItems from "components/DropdownMenu/DropdownMenuItems";
|
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";
|
import { newDocumentUrl } from "utils/routeHelpers";
|
||||||
|
|
||||||
type Props = {
|
function NewTemplateMenu() {
|
||||||
label?: React.Node,
|
const menu = useMenuState();
|
||||||
collections: CollectionsStore,
|
const { t } = useTranslation();
|
||||||
policies: PoliciesStore,
|
const { collections, policies } = useStores();
|
||||||
t: TFunction,
|
|
||||||
};
|
|
||||||
|
|
||||||
@observer
|
|
||||||
class NewTemplateMenu extends React.Component<Props> {
|
|
||||||
@observable redirectTo: ?string;
|
|
||||||
|
|
||||||
componentDidUpdate() {
|
|
||||||
this.redirectTo = undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
handleNewDocument = (collectionId: string) => {
|
|
||||||
this.redirectTo = newDocumentUrl(collectionId, {
|
|
||||||
template: true,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
|
||||||
if (this.redirectTo) return <Redirect to={this.redirectTo} push />;
|
|
||||||
|
|
||||||
const { collections, policies, label, t, ...rest } = this.props;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DropdownMenu
|
<>
|
||||||
label={
|
<MenuButton {...menu}>
|
||||||
label || (
|
{(props) => (
|
||||||
<Button icon={<PlusIcon />} small>
|
<Button icon={<PlusIcon />} {...props} small>
|
||||||
{t("New template")}…
|
{t("New template")}…
|
||||||
</Button>
|
</Button>
|
||||||
)
|
)}
|
||||||
}
|
</MenuButton>
|
||||||
{...rest}
|
<ContextMenu aria-label={t("New template")} {...menu}>
|
||||||
>
|
|
||||||
<Header>{t("Choose a collection")}</Header>
|
<Header>{t("Choose a collection")}</Header>
|
||||||
<DropdownMenuItems
|
<Template
|
||||||
|
{...menu}
|
||||||
items={collections.orderedData.map((collection) => ({
|
items={collections.orderedData.map((collection) => ({
|
||||||
onClick: () => this.handleNewDocument(collection.id),
|
to: newDocumentUrl(collection.id, {
|
||||||
|
template: true,
|
||||||
|
}),
|
||||||
disabled: !policies.abilities(collection.id).update,
|
disabled: !policies.abilities(collection.id).update,
|
||||||
title: (
|
title: (
|
||||||
<>
|
<Flex align="center">
|
||||||
<CollectionIcon collection={collection} />
|
<CollectionIcon collection={collection} />
|
||||||
{collection.name}
|
<CollectionName>{collection.name}</CollectionName>
|
||||||
</>
|
</Flex>
|
||||||
),
|
),
|
||||||
}))}
|
}))}
|
||||||
/>
|
/>
|
||||||
</DropdownMenu>
|
</ContextMenu>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withTranslation()<NewTemplateMenu>(
|
const CollectionName = styled.div`
|
||||||
inject("collections", "policies")(NewTemplateMenu)
|
overflow: hidden;
|
||||||
);
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export default observer(NewTemplateMenu);
|
||||||
|
|
|
@ -1,68 +1,69 @@
|
||||||
// @flow
|
// @flow
|
||||||
import { inject } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { withTranslation, type TFunction } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { withRouter, type RouterHistory } from "react-router-dom";
|
import { useHistory } from "react-router-dom";
|
||||||
|
import { useMenuState } from "reakit/Menu";
|
||||||
import UiStore from "stores/UiStore";
|
|
||||||
import Document from "models/Document";
|
import Document from "models/Document";
|
||||||
import Revision from "models/Revision";
|
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 CopyToClipboard from "components/CopyToClipboard";
|
||||||
import { DropdownMenu, DropdownMenuItem } from "components/DropdownMenu";
|
import useStores from "hooks/useStores";
|
||||||
import { documentHistoryUrl } from "utils/routeHelpers";
|
import { documentHistoryUrl } from "utils/routeHelpers";
|
||||||
|
|
||||||
type Props = {
|
type Props = {|
|
||||||
onOpen?: () => void,
|
|
||||||
onClose: () => void,
|
|
||||||
history: RouterHistory,
|
|
||||||
document: Document,
|
document: Document,
|
||||||
revision: Revision,
|
revision: Revision,
|
||||||
|
iconColor?: string,
|
||||||
className?: string,
|
className?: string,
|
||||||
label: React.Node,
|
|};
|
||||||
ui: UiStore,
|
|
||||||
t: TFunction,
|
|
||||||
};
|
|
||||||
|
|
||||||
class RevisionMenu extends React.Component<Props> {
|
function RevisionMenu({ document, revision, className, iconColor }: Props) {
|
||||||
handleRestore = async (ev: SyntheticEvent<>) => {
|
const { ui } = useStores();
|
||||||
|
const menu = useMenuState({ modal: true });
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const history = useHistory();
|
||||||
|
|
||||||
|
const handleRestore = React.useCallback(
|
||||||
|
async (ev: SyntheticEvent<>) => {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
await this.props.document.restore({ revisionId: this.props.revision.id });
|
await document.restore({ revisionId: revision.id });
|
||||||
const { t } = this.props;
|
ui.showToast(t("Document restored"), { type: "success" });
|
||||||
this.props.ui.showToast(t("Document restored"), { type: "success" });
|
history.push(document.url);
|
||||||
this.props.history.push(this.props.document.url);
|
},
|
||||||
};
|
[history, ui, t, document, revision]
|
||||||
|
);
|
||||||
|
|
||||||
handleCopy = () => {
|
const handleCopy = React.useCallback(() => {
|
||||||
const { t } = this.props;
|
ui.showToast(t("Link copied"), { type: "info" });
|
||||||
this.props.ui.showToast(t("Link copied"), { type: "info" });
|
}, [ui, t]);
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { className, label, onOpen, onClose, t } = this.props;
|
|
||||||
const url = `${window.location.origin}${documentHistoryUrl(
|
const url = `${window.location.origin}${documentHistoryUrl(
|
||||||
this.props.document,
|
document,
|
||||||
this.props.revision.id
|
revision.id
|
||||||
)}`;
|
)}`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DropdownMenu
|
<>
|
||||||
onOpen={onOpen}
|
<OverflowMenuButton
|
||||||
onClose={onClose}
|
|
||||||
className={className}
|
className={className}
|
||||||
label={label}
|
iconColor={iconColor}
|
||||||
>
|
{...menu}
|
||||||
<DropdownMenuItem onClick={this.handleRestore}>
|
/>
|
||||||
|
<ContextMenu {...menu} aria-label={t("Revision options")}>
|
||||||
|
<MenuItem {...menu} onClick={handleRestore}>
|
||||||
{t("Restore version")}
|
{t("Restore version")}
|
||||||
</DropdownMenuItem>
|
</MenuItem>
|
||||||
<hr />
|
<Separator />
|
||||||
<CopyToClipboard text={url} onCopy={this.handleCopy}>
|
<CopyToClipboard text={url} onCopy={handleCopy}>
|
||||||
<DropdownMenuItem>{t("Copy link")}</DropdownMenuItem>
|
<MenuItem {...menu}>{t("Copy link")}</MenuItem>
|
||||||
</CopyToClipboard>
|
</CopyToClipboard>
|
||||||
</DropdownMenu>
|
</ContextMenu>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withTranslation()<RevisionMenu>(
|
export default observer(RevisionMenu);
|
||||||
withRouter(inject("ui")(RevisionMenu))
|
|
||||||
);
|
|
||||||
|
|
|
@ -1,75 +1,69 @@
|
||||||
// @flow
|
// @flow
|
||||||
import { observable } from "mobx";
|
import { observer } from "mobx-react";
|
||||||
import { inject, observer } from "mobx-react";
|
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { withTranslation, type TFunction } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Redirect } from "react-router-dom";
|
import { useHistory } from "react-router-dom";
|
||||||
|
import { useMenuState } from "reakit/Menu";
|
||||||
import SharesStore from "stores/SharesStore";
|
|
||||||
import UiStore from "stores/UiStore";
|
|
||||||
import Share from "models/Share";
|
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 CopyToClipboard from "components/CopyToClipboard";
|
||||||
import { DropdownMenu, DropdownMenuItem } from "components/DropdownMenu";
|
import useStores from "hooks/useStores";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
onOpen?: () => void,
|
|
||||||
onClose: () => void,
|
|
||||||
shares: SharesStore,
|
|
||||||
ui: UiStore,
|
|
||||||
share: Share,
|
share: Share,
|
||||||
t: TFunction,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
@observer
|
function ShareMenu({ share }: Props) {
|
||||||
class ShareMenu extends React.Component<Props> {
|
const menu = useMenuState({ modal: true });
|
||||||
@observable redirectTo: ?string;
|
const { ui, shares } = useStores();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const history = useHistory();
|
||||||
|
|
||||||
componentDidUpdate() {
|
const handleGoToDocument = React.useCallback(
|
||||||
this.redirectTo = undefined;
|
(ev: SyntheticEvent<>) => {
|
||||||
}
|
|
||||||
|
|
||||||
handleGoToDocument = (ev: SyntheticEvent<>) => {
|
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
this.redirectTo = this.props.share.documentUrl;
|
history.push(share.documentUrl);
|
||||||
};
|
},
|
||||||
|
[history, share]
|
||||||
|
);
|
||||||
|
|
||||||
handleRevoke = async (ev: SyntheticEvent<>) => {
|
const handleRevoke = React.useCallback(
|
||||||
|
async (ev: SyntheticEvent<>) => {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.props.shares.revoke(this.props.share);
|
await shares.revoke(share);
|
||||||
const { t } = this.props;
|
ui.showToast(t("Share link revoked"), { type: "info" });
|
||||||
this.props.ui.showToast(t("Share link revoked"), { type: "info" });
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.props.ui.showToast(err.message, { type: "error" });
|
ui.showToast(err.message, { type: "error" });
|
||||||
}
|
}
|
||||||
};
|
},
|
||||||
|
[t, shares, share, ui]
|
||||||
|
);
|
||||||
|
|
||||||
handleCopy = () => {
|
const handleCopy = React.useCallback(() => {
|
||||||
const { t } = this.props;
|
ui.showToast(t("Share link copied"), { type: "info" });
|
||||||
this.props.ui.showToast(t("Share link copied"), { type: "info" });
|
}, [t, ui]);
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
|
||||||
if (this.redirectTo) return <Redirect to={this.redirectTo} push />;
|
|
||||||
|
|
||||||
const { share, onOpen, onClose, t } = this.props;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DropdownMenu onOpen={onOpen} onClose={onClose}>
|
<>
|
||||||
<CopyToClipboard text={share.url} onCopy={this.handleCopy}>
|
<OverflowMenuButton {...menu} />
|
||||||
<DropdownMenuItem>{t("Copy link")}</DropdownMenuItem>
|
<ContextMenu {...menu} aria-label={t("Share options")}>
|
||||||
|
<CopyToClipboard text={share.url} onCopy={handleCopy}>
|
||||||
|
<MenuItem {...menu}>{t("Copy link")}</MenuItem>
|
||||||
</CopyToClipboard>
|
</CopyToClipboard>
|
||||||
<DropdownMenuItem onClick={this.handleGoToDocument}>
|
<MenuItem {...menu} onClick={handleGoToDocument}>
|
||||||
{t("Go to document")}
|
{t("Go to document")}
|
||||||
</DropdownMenuItem>
|
</MenuItem>
|
||||||
<hr />
|
<hr />
|
||||||
<DropdownMenuItem onClick={this.handleRevoke}>
|
<MenuItem {...menu} onClick={handleRevoke}>
|
||||||
{t("Revoke link")}
|
{t("Revoke link")}
|
||||||
</DropdownMenuItem>
|
</MenuItem>
|
||||||
</DropdownMenu>
|
</ContextMenu>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withTranslation()<ShareMenu>(inject("shares", "ui")(ShareMenu));
|
export default observer(ShareMenu);
|
||||||
|
|
|
@ -1,24 +1,24 @@
|
||||||
// @flow
|
// @flow
|
||||||
import { observer, inject } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { DocumentIcon } from "outline-icons";
|
import { DocumentIcon } from "outline-icons";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { withTranslation, type TFunction } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { MenuButton, useMenuState } from "reakit/Menu";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
import DocumentsStore from "stores/DocumentsStore";
|
|
||||||
import Document from "models/Document";
|
import Document from "models/Document";
|
||||||
import Button from "components/Button";
|
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,
|
document: Document,
|
||||||
documents: DocumentsStore,
|
|};
|
||||||
t: TFunction,
|
|
||||||
};
|
|
||||||
|
|
||||||
@observer
|
function TemplatesMenu({ document }: Props) {
|
||||||
class TemplatesMenu extends React.Component<Props> {
|
const menu = useMenuState({ modal: true });
|
||||||
render() {
|
const { documents } = useStores();
|
||||||
const { documents, document, t, ...rest } = this.props;
|
const { t } = useTranslation();
|
||||||
const templates = documents.templatesInCollection(document.collectionId);
|
const templates = documents.templatesInCollection(document.collectionId);
|
||||||
|
|
||||||
if (!templates.length) {
|
if (!templates.length) {
|
||||||
|
@ -26,17 +26,17 @@ class TemplatesMenu extends React.Component<Props> {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DropdownMenu
|
<>
|
||||||
position="left"
|
<MenuButton {...menu}>
|
||||||
label={
|
{(props) => (
|
||||||
<Button disclosure neutral>
|
<Button {...props} disclosure neutral>
|
||||||
{t("Templates")}
|
{t("Templates")}
|
||||||
</Button>
|
</Button>
|
||||||
}
|
)}
|
||||||
{...rest}
|
</MenuButton>
|
||||||
>
|
<ContextMenu {...menu} aria-label={t("Templates")}>
|
||||||
{templates.map((template) => (
|
{templates.map((template) => (
|
||||||
<DropdownMenuItem
|
<MenuItem
|
||||||
key={template.id}
|
key={template.id}
|
||||||
onClick={() => document.updateFromTemplate(template)}
|
onClick={() => document.updateFromTemplate(template)}
|
||||||
>
|
>
|
||||||
|
@ -48,17 +48,15 @@ class TemplatesMenu extends React.Component<Props> {
|
||||||
{t("By {{ author }}", { author: template.createdBy.name })}
|
{t("By {{ author }}", { author: template.createdBy.name })}
|
||||||
</Author>
|
</Author>
|
||||||
</div>
|
</div>
|
||||||
</DropdownMenuItem>
|
</MenuItem>
|
||||||
))}
|
))}
|
||||||
</DropdownMenu>
|
</ContextMenu>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const Author = styled.div`
|
const Author = styled.div`
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export default withTranslation()<TemplatesMenu>(
|
export default observer(TemplatesMenu);
|
||||||
inject("documents")(TemplatesMenu)
|
|
||||||
);
|
|
||||||
|
|
|
@ -1,24 +1,26 @@
|
||||||
// @flow
|
// @flow
|
||||||
import { inject, observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
import { withTranslation, type TFunction } from "react-i18next";
|
import { useMenuState } from "reakit/Menu";
|
||||||
import UsersStore from "stores/UsersStore";
|
|
||||||
import User from "models/User";
|
import User from "models/User";
|
||||||
import { DropdownMenu } from "components/DropdownMenu";
|
import ContextMenu from "components/ContextMenu";
|
||||||
import DropdownMenuItems from "components/DropdownMenu/DropdownMenuItems";
|
import OverflowMenuButton from "components/ContextMenu/OverflowMenuButton";
|
||||||
|
import Template from "components/ContextMenu/Template";
|
||||||
|
import useStores from "hooks/useStores";
|
||||||
|
|
||||||
type Props = {
|
type Props = {|
|
||||||
user: User,
|
user: User,
|
||||||
users: UsersStore,
|
|};
|
||||||
t: TFunction,
|
|
||||||
};
|
|
||||||
|
|
||||||
@observer
|
function UserMenu({ user }: Props) {
|
||||||
class UserMenu extends React.Component<Props> {
|
const { users } = useStores();
|
||||||
handlePromote = (ev: SyntheticEvent<>) => {
|
const { t } = useTranslation();
|
||||||
|
const menu = useMenuState({ modal: true });
|
||||||
|
|
||||||
|
const handlePromote = React.useCallback(
|
||||||
|
(ev: SyntheticEvent<>) => {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
const { user, users, t } = this.props;
|
|
||||||
if (
|
if (
|
||||||
!window.confirm(
|
!window.confirm(
|
||||||
t(
|
t(
|
||||||
|
@ -30,11 +32,13 @@ class UserMenu extends React.Component<Props> {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
users.promote(user);
|
users.promote(user);
|
||||||
};
|
},
|
||||||
|
[users, user, t]
|
||||||
|
);
|
||||||
|
|
||||||
handleDemote = (ev: SyntheticEvent<>) => {
|
const handleDemote = React.useCallback(
|
||||||
|
(ev: SyntheticEvent<>) => {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
const { user, users, t } = this.props;
|
|
||||||
if (
|
if (
|
||||||
!window.confirm(
|
!window.confirm(
|
||||||
t("Are you sure you want to make {{ userName }} a member?", {
|
t("Are you sure you want to make {{ userName }} a member?", {
|
||||||
|
@ -45,11 +49,13 @@ class UserMenu extends React.Component<Props> {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
users.demote(user);
|
users.demote(user);
|
||||||
};
|
},
|
||||||
|
[users, user, t]
|
||||||
|
);
|
||||||
|
|
||||||
handleSuspend = (ev: SyntheticEvent<>) => {
|
const handleSuspend = React.useCallback(
|
||||||
|
(ev: SyntheticEvent<>) => {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
const { user, users, t } = this.props;
|
|
||||||
if (
|
if (
|
||||||
!window.confirm(
|
!window.confirm(
|
||||||
t(
|
t(
|
||||||
|
@ -60,39 +66,45 @@ class UserMenu extends React.Component<Props> {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
users.suspend(user);
|
users.suspend(user);
|
||||||
};
|
},
|
||||||
|
[users, user, t]
|
||||||
|
);
|
||||||
|
|
||||||
handleRevoke = (ev: SyntheticEvent<>) => {
|
const handleRevoke = React.useCallback(
|
||||||
|
(ev: SyntheticEvent<>) => {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
const { user, users } = this.props;
|
|
||||||
users.delete(user, { confirmation: true });
|
users.delete(user, { confirmation: true });
|
||||||
};
|
},
|
||||||
|
[users, user]
|
||||||
|
);
|
||||||
|
|
||||||
handleActivate = (ev: SyntheticEvent<>) => {
|
const handleActivate = React.useCallback(
|
||||||
|
(ev: SyntheticEvent<>) => {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
const { user, users } = this.props;
|
|
||||||
users.activate(user);
|
users.activate(user);
|
||||||
};
|
},
|
||||||
|
[users, user]
|
||||||
render() {
|
);
|
||||||
const { user, t } = this.props;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DropdownMenu>
|
<>
|
||||||
<DropdownMenuItems
|
<OverflowMenuButton {...menu} />
|
||||||
|
<ContextMenu {...menu} aria-label={t("User options")}>
|
||||||
|
<Template
|
||||||
|
{...menu}
|
||||||
items={[
|
items={[
|
||||||
{
|
{
|
||||||
title: t("Make {{ userName }} a member…", {
|
title: t("Make {{ userName }} a member…", {
|
||||||
userName: user.name,
|
userName: user.name,
|
||||||
}),
|
}),
|
||||||
onClick: this.handleDemote,
|
onClick: handleDemote,
|
||||||
visible: user.isAdmin,
|
visible: user.isAdmin,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: t("Make {{ userName }} an admin…", {
|
title: t("Make {{ userName }} an admin…", {
|
||||||
userName: user.name,
|
userName: user.name,
|
||||||
}),
|
}),
|
||||||
onClick: this.handlePromote,
|
onClick: handlePromote,
|
||||||
visible: !user.isAdmin && !user.isSuspended,
|
visible: !user.isAdmin && !user.isSuspended,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -100,24 +112,24 @@ class UserMenu extends React.Component<Props> {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: `${t("Revoke invite")}…`,
|
title: `${t("Revoke invite")}…`,
|
||||||
onClick: this.handleRevoke,
|
onClick: handleRevoke,
|
||||||
visible: user.isInvited,
|
visible: user.isInvited,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: t("Activate account"),
|
title: t("Activate account"),
|
||||||
onClick: this.handleActivate,
|
onClick: handleActivate,
|
||||||
visible: !user.isInvited && user.isSuspended,
|
visible: !user.isInvited && user.isSuspended,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: `${t("Suspend account")}…`,
|
title: `${t("Suspend account")}…`,
|
||||||
onClick: this.handleSuspend,
|
onClick: handleSuspend,
|
||||||
visible: !user.isInvited && !user.isSuspended,
|
visible: !user.isInvited && !user.isSuspended,
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
</DropdownMenu>
|
</ContextMenu>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withTranslation()<UserMenu>(inject("users")(UserMenu));
|
export default observer(UserMenu);
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
import { observable } from "mobx";
|
import { observable } from "mobx";
|
||||||
import { observer, inject } from "mobx-react";
|
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 * as React from "react";
|
||||||
import { withTranslation, Trans, type TFunction } from "react-i18next";
|
import { withTranslation, Trans, type TFunction } from "react-i18next";
|
||||||
import { Redirect, Link, Switch, Route, type Match } from "react-router-dom";
|
import { Redirect, Link, Switch, Route, type Match } from "react-router-dom";
|
||||||
|
@ -164,7 +164,20 @@ class CollectionScene extends React.Component<Props> {
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<Action>
|
<Action>
|
||||||
<CollectionMenu collection={this.collection} />
|
<CollectionMenu
|
||||||
|
collection={this.collection}
|
||||||
|
placement="bottom-end"
|
||||||
|
modal={false}
|
||||||
|
label={(props) => (
|
||||||
|
<Button
|
||||||
|
icon={<MoreIcon />}
|
||||||
|
{...props}
|
||||||
|
borderOnHover
|
||||||
|
neutral
|
||||||
|
small
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
</Action>
|
</Action>
|
||||||
</Actions>
|
</Actions>
|
||||||
);
|
);
|
||||||
|
|
|
@ -4,9 +4,9 @@ import { useTranslation } from "react-i18next";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
import CollectionGroupMembership from "models/CollectionGroupMembership";
|
import CollectionGroupMembership from "models/CollectionGroupMembership";
|
||||||
import Group from "models/Group";
|
import Group from "models/Group";
|
||||||
import { DropdownMenu, DropdownMenuItem } from "components/DropdownMenu";
|
|
||||||
import GroupListItem from "components/GroupListItem";
|
import GroupListItem from "components/GroupListItem";
|
||||||
import InputSelect from "components/InputSelect";
|
import InputSelect from "components/InputSelect";
|
||||||
|
import CollectionGroupMemberMenu from "menus/CollectionGroupMemberMenu";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
group: Group,
|
group: Group,
|
||||||
|
@ -50,15 +50,10 @@ const MemberListItem = ({
|
||||||
labelHidden
|
labelHidden
|
||||||
/>
|
/>
|
||||||
<ButtonWrap>
|
<ButtonWrap>
|
||||||
<DropdownMenu>
|
<CollectionGroupMemberMenu
|
||||||
<DropdownMenuItem onClick={openMembersModal}>
|
onMembers={openMembersModal}
|
||||||
{t("Members")}…
|
onRemove={onRemove}
|
||||||
</DropdownMenuItem>
|
/>
|
||||||
<hr />
|
|
||||||
<DropdownMenuItem onClick={onRemove}>
|
|
||||||
{t("Remove")}
|
|
||||||
</DropdownMenuItem>
|
|
||||||
</DropdownMenu>
|
|
||||||
</ButtonWrap>
|
</ButtonWrap>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -7,11 +7,11 @@ import User from "models/User";
|
||||||
import Avatar from "components/Avatar";
|
import Avatar from "components/Avatar";
|
||||||
import Badge from "components/Badge";
|
import Badge from "components/Badge";
|
||||||
import Button from "components/Button";
|
import Button from "components/Button";
|
||||||
import { DropdownMenu, DropdownMenuItem } from "components/DropdownMenu";
|
|
||||||
import Flex from "components/Flex";
|
import Flex from "components/Flex";
|
||||||
import InputSelect from "components/InputSelect";
|
import InputSelect from "components/InputSelect";
|
||||||
import ListItem from "components/List/Item";
|
import ListItem from "components/List/Item";
|
||||||
import Time from "components/Time";
|
import Time from "components/Time";
|
||||||
|
import MemberMenu from "menus/MemberMenu";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
user: User,
|
user: User,
|
||||||
|
@ -69,13 +69,7 @@ const MemberListItem = ({
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{canEdit && onRemove && (
|
{canEdit && onRemove && <MemberMenu onRemove={onRemove} />}
|
||||||
<DropdownMenu>
|
|
||||||
<DropdownMenuItem onClick={onRemove}>
|
|
||||||
{t("Remove")}
|
|
||||||
</DropdownMenuItem>
|
|
||||||
</DropdownMenu>
|
|
||||||
)}
|
|
||||||
{canEdit && onAdd && (
|
{canEdit && onAdd && (
|
||||||
<Button onClick={onAdd} neutral>
|
<Button onClick={onAdd} neutral>
|
||||||
{t("Add")}
|
{t("Add")}
|
||||||
|
|
|
@ -290,18 +290,18 @@ class Header extends React.Component<Props> {
|
||||||
<Action>
|
<Action>
|
||||||
<NewChildDocumentMenu
|
<NewChildDocumentMenu
|
||||||
document={document}
|
document={document}
|
||||||
label={
|
label={(props) => (
|
||||||
<Tooltip
|
<Tooltip
|
||||||
tooltip={t("New document")}
|
tooltip={t("New document")}
|
||||||
shortcut="n"
|
shortcut="n"
|
||||||
delay={500}
|
delay={500}
|
||||||
placement="bottom"
|
placement="bottom"
|
||||||
>
|
>
|
||||||
<Button icon={<PlusIcon />} neutral>
|
<Button icon={<PlusIcon />} {...props} neutral>
|
||||||
{t("New doc")}
|
{t("New doc")}
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
}
|
)}
|
||||||
/>
|
/>
|
||||||
</Action>
|
</Action>
|
||||||
)}
|
)}
|
||||||
|
@ -343,15 +343,16 @@ class Header extends React.Component<Props> {
|
||||||
<DocumentMenu
|
<DocumentMenu
|
||||||
document={document}
|
document={document}
|
||||||
isRevision={isRevision}
|
isRevision={isRevision}
|
||||||
label={
|
label={(props) => (
|
||||||
<Button
|
<Button
|
||||||
icon={<MoreIcon />}
|
icon={<MoreIcon />}
|
||||||
iconColor="currentColor"
|
iconColor="currentColor"
|
||||||
|
{...props}
|
||||||
borderOnHover
|
borderOnHover
|
||||||
neutral
|
neutral
|
||||||
small
|
small
|
||||||
/>
|
/>
|
||||||
}
|
)}
|
||||||
showToggleEmbeds={canToggleEmbeds}
|
showToggleEmbeds={canToggleEmbeds}
|
||||||
showPrint
|
showPrint
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -112,7 +112,6 @@ class AddPeopleToGroup extends React.Component<Props> {
|
||||||
key={item.id}
|
key={item.id}
|
||||||
user={item}
|
user={item}
|
||||||
onAdd={() => this.handleAddUser(item)}
|
onAdd={() => this.handleAddUser(item)}
|
||||||
canEdit
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -103,7 +103,6 @@ class GroupMembers extends React.Component<Props> {
|
||||||
<GroupMemberListItem
|
<GroupMemberListItem
|
||||||
key={item.id}
|
key={item.id}
|
||||||
user={item}
|
user={item}
|
||||||
membership={groupMemberships.get(`${item.id}-${group.id}`)}
|
|
||||||
onRemove={
|
onRemove={
|
||||||
can.update ? () => this.handleRemoveUser(item) : undefined
|
can.update ? () => this.handleRemoveUser(item) : undefined
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,28 +1,21 @@
|
||||||
// @flow
|
// @flow
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import GroupMembership from "models/GroupMembership";
|
|
||||||
import User from "models/User";
|
import User from "models/User";
|
||||||
import Avatar from "components/Avatar";
|
import Avatar from "components/Avatar";
|
||||||
import Badge from "components/Badge";
|
import Badge from "components/Badge";
|
||||||
import Button from "components/Button";
|
import Button from "components/Button";
|
||||||
import { DropdownMenu, DropdownMenuItem } from "components/DropdownMenu";
|
|
||||||
import Flex from "components/Flex";
|
import Flex from "components/Flex";
|
||||||
import ListItem from "components/List/Item";
|
import ListItem from "components/List/Item";
|
||||||
import Time from "components/Time";
|
import Time from "components/Time";
|
||||||
|
import GroupMemberMenu from "menus/GroupMemberMenu";
|
||||||
|
|
||||||
type Props = {
|
type Props = {|
|
||||||
user: User,
|
user: User,
|
||||||
groupMembership?: ?GroupMembership,
|
|
||||||
onAdd?: () => Promise<void>,
|
onAdd?: () => Promise<void>,
|
||||||
onRemove?: () => Promise<void>,
|
onRemove?: () => Promise<void>,
|
||||||
};
|
|};
|
||||||
|
|
||||||
const GroupMemberListItem = ({
|
const GroupMemberListItem = ({ user, onRemove, onAdd }: Props) => {
|
||||||
user,
|
|
||||||
groupMembership,
|
|
||||||
onRemove,
|
|
||||||
onAdd,
|
|
||||||
}: Props) => {
|
|
||||||
return (
|
return (
|
||||||
<ListItem
|
<ListItem
|
||||||
title={user.name}
|
title={user.name}
|
||||||
|
@ -42,11 +35,7 @@ const GroupMemberListItem = ({
|
||||||
image={<Avatar src={user.avatarUrl} size={40} />}
|
image={<Avatar src={user.avatarUrl} size={40} />}
|
||||||
actions={
|
actions={
|
||||||
<Flex align="center">
|
<Flex align="center">
|
||||||
{onRemove && (
|
{onRemove && <GroupMemberMenu onRemove={onRemove} />}
|
||||||
<DropdownMenu>
|
|
||||||
<DropdownMenuItem onClick={onRemove}>Remove</DropdownMenuItem>
|
|
||||||
</DropdownMenu>
|
|
||||||
)}
|
|
||||||
{onAdd && (
|
{onAdd && (
|
||||||
<Button onClick={onAdd} neutral>
|
<Button onClick={onAdd} neutral>
|
||||||
Add
|
Add
|
||||||
|
|
|
@ -361,6 +361,7 @@ class Search extends React.Component<Props> {
|
||||||
highlight={this.query}
|
highlight={this.query}
|
||||||
context={result.context}
|
context={result.context}
|
||||||
showCollection
|
showCollection
|
||||||
|
showTemplate
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
|
@ -1,45 +1,61 @@
|
||||||
// @flow
|
// @flow
|
||||||
import { CheckmarkIcon } from "outline-icons";
|
import { CheckmarkIcon } from "outline-icons";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
|
import { MenuItem } from "reakit/Menu";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
import Flex from "components/Flex";
|
import Flex from "components/Flex";
|
||||||
import HelpText from "components/HelpText";
|
import HelpText from "components/HelpText";
|
||||||
|
|
||||||
type Props = {
|
type Props = {|
|
||||||
label: string,
|
label: string,
|
||||||
note?: string,
|
note?: string,
|
||||||
onSelect: () => void,
|
onSelect: () => void,
|
||||||
active: boolean,
|
active: boolean,
|
||||||
};
|
|};
|
||||||
|
|
||||||
const FilterOption = ({ label, note, onSelect, active }: Props) => {
|
const FilterOption = ({ label, note, onSelect, active, ...rest }: Props) => {
|
||||||
return (
|
return (
|
||||||
<ListItem active={active}>
|
<MenuItem onClick={active ? undefined : onSelect} {...rest}>
|
||||||
<Anchor onClick={active ? undefined : onSelect}>
|
{(props) => (
|
||||||
|
<ListItem>
|
||||||
|
<Button active={active} {...props}>
|
||||||
<Flex align="center" justify="space-between">
|
<Flex align="center" justify="space-between">
|
||||||
<span>
|
<span>
|
||||||
{label}
|
{label}
|
||||||
{note && <HelpText small>{note}</HelpText>}
|
{note && <Description small>{note}</Description>}
|
||||||
</span>
|
</span>
|
||||||
{active && <Checkmark />}
|
{active && <Checkmark />}
|
||||||
</Flex>
|
</Flex>
|
||||||
</Anchor>
|
</Button>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
|
)}
|
||||||
|
</MenuItem>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const Description = styled(HelpText)`
|
||||||
|
margin-bottom: 0;
|
||||||
|
`;
|
||||||
|
|
||||||
const Checkmark = styled(CheckmarkIcon)`
|
const Checkmark = styled(CheckmarkIcon)`
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
padding-left: 4px;
|
padding-left: 4px;
|
||||||
fill: ${(props) => props.theme.text};
|
fill: ${(props) => props.theme.text};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const Anchor = styled("a")`
|
const Button = styled.button`
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
padding: 4px 8px;
|
padding: 4px 8px;
|
||||||
|
margin: 0;
|
||||||
|
border: 0;
|
||||||
|
background: none;
|
||||||
color: ${(props) => props.theme.text};
|
color: ${(props) => props.theme.text};
|
||||||
|
text-align: left;
|
||||||
|
font-weight: ${(props) => (props.active ? "600" : "normal")};
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
min-height: 32px;
|
min-height: 32px;
|
||||||
|
|
||||||
${HelpText} {
|
${HelpText} {
|
||||||
|
@ -54,7 +70,7 @@ const Anchor = styled("a")`
|
||||||
|
|
||||||
const ListItem = styled("li")`
|
const ListItem = styled("li")`
|
||||||
list-style: none;
|
list-style: none;
|
||||||
font-weight: ${(props) => (props.active ? "600" : "normal")};
|
max-width: 250px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export default FilterOption;
|
export default FilterOption;
|
||||||
|
|
|
@ -1,63 +1,74 @@
|
||||||
// @flow
|
// @flow
|
||||||
import { find } from "lodash";
|
import { find } from "lodash";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
|
import { useMenuState, MenuButton } from "reakit/Menu";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
import Button, { Inner } from "components/Button";
|
import Button, { Inner } from "components/Button";
|
||||||
import { DropdownMenu } from "components/DropdownMenu";
|
import ContextMenu from "components/ContextMenu";
|
||||||
import FilterOption from "./FilterOption";
|
import FilterOption from "./FilterOption";
|
||||||
|
|
||||||
type Props = {
|
type TFilterOption = {|
|
||||||
options: {
|
|
||||||
key: string,
|
key: string,
|
||||||
label: string,
|
label: string,
|
||||||
note?: string,
|
note?: string,
|
||||||
}[],
|
|};
|
||||||
|
|
||||||
|
type Props = {|
|
||||||
|
options: TFilterOption[],
|
||||||
activeKey: ?string,
|
activeKey: ?string,
|
||||||
defaultLabel?: string,
|
defaultLabel?: string,
|
||||||
selectedPrefix?: string,
|
selectedPrefix?: string,
|
||||||
|
className?: string,
|
||||||
onSelect: (key: ?string) => void,
|
onSelect: (key: ?string) => void,
|
||||||
};
|
|};
|
||||||
|
|
||||||
const FilterOptions = ({
|
const FilterOptions = ({
|
||||||
options,
|
options,
|
||||||
activeKey = "",
|
activeKey = "",
|
||||||
defaultLabel,
|
defaultLabel,
|
||||||
selectedPrefix = "",
|
selectedPrefix = "",
|
||||||
|
className,
|
||||||
onSelect,
|
onSelect,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
|
const menu = useMenuState();
|
||||||
const selected = find(options, { key: activeKey }) || options[0];
|
const selected = find(options, { key: activeKey }) || options[0];
|
||||||
const selectedLabel = selected ? `${selectedPrefix} ${selected.label}` : "";
|
const selectedLabel = selected ? `${selectedPrefix} ${selected.label}` : "";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DropdownButton label={activeKey ? selectedLabel : defaultLabel}>
|
<SearchFilter>
|
||||||
{({ closeMenu }) => (
|
<MenuButton {...menu}>
|
||||||
|
{(props) => (
|
||||||
|
<StyledButton
|
||||||
|
{...props}
|
||||||
|
className={className}
|
||||||
|
neutral
|
||||||
|
disclosure
|
||||||
|
small
|
||||||
|
>
|
||||||
|
{activeKey ? selectedLabel : defaultLabel}
|
||||||
|
</StyledButton>
|
||||||
|
)}
|
||||||
|
</MenuButton>
|
||||||
|
<ContextMenu aria-label={defaultLabel} {...menu}>
|
||||||
<List>
|
<List>
|
||||||
{options.map((option) => (
|
{options.map((option) => (
|
||||||
<FilterOption
|
<FilterOption
|
||||||
key={option.key}
|
key={option.key}
|
||||||
onSelect={() => {
|
onSelect={() => {
|
||||||
onSelect(option.key);
|
onSelect(option.key);
|
||||||
closeMenu();
|
menu.hide();
|
||||||
}}
|
}}
|
||||||
active={option.key === activeKey}
|
active={option.key === activeKey}
|
||||||
{...option}
|
{...option}
|
||||||
|
{...menu}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</List>
|
</List>
|
||||||
)}
|
</ContextMenu>
|
||||||
</DropdownButton>
|
</SearchFilter>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const Content = styled("div")`
|
|
||||||
padding: 0 8px;
|
|
||||||
width: 250px;
|
|
||||||
|
|
||||||
p {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const StyledButton = styled(Button)`
|
const StyledButton = styled(Button)`
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
text-transform: none;
|
text-transform: none;
|
||||||
|
@ -73,32 +84,14 @@ const StyledButton = styled(Button)`
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const SearchFilter = (props) => {
|
const SearchFilter = styled.div`
|
||||||
return (
|
|
||||||
<DropdownMenu
|
|
||||||
className={props.className}
|
|
||||||
label={
|
|
||||||
<StyledButton neutral disclosure small>
|
|
||||||
{props.label}
|
|
||||||
</StyledButton>
|
|
||||||
}
|
|
||||||
position="right"
|
|
||||||
>
|
|
||||||
{({ closePortal }) => (
|
|
||||||
<Content>{props.children({ closeMenu: closePortal })}</Content>
|
|
||||||
)}
|
|
||||||
</DropdownMenu>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const DropdownButton = styled(SearchFilter)`
|
|
||||||
margin-right: 8px;
|
margin-right: 8px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const List = styled("ol")`
|
const List = styled("ol")`
|
||||||
list-style: none;
|
list-style: none;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0 8px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export default FilterOptions;
|
export default FilterOptions;
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
// @flow
|
// @flow
|
||||||
import Compressor from "compressorjs";
|
import Compressor from "compressorjs";
|
||||||
|
|
||||||
type Options = Omit<Compressor.Options, "success" | "error">;
|
type Options = { maxWidth?: number, maxHeight?: number };
|
||||||
|
|
||||||
export const compressImage = async (
|
export const compressImage = async (
|
||||||
file: File | Blob,
|
file: File | Blob,
|
||||||
|
|
|
@ -152,6 +152,7 @@
|
||||||
"react-virtualized-auto-sizer": "^1.0.2",
|
"react-virtualized-auto-sizer": "^1.0.2",
|
||||||
"react-waypoint": "^9.0.2",
|
"react-waypoint": "^9.0.2",
|
||||||
"react-window": "^1.8.6",
|
"react-window": "^1.8.6",
|
||||||
|
"reakit": "^1.3.4",
|
||||||
"rich-markdown-editor": "^11.0.11",
|
"rich-markdown-editor": "^11.0.11",
|
||||||
"semver": "^7.3.2",
|
"semver": "^7.3.2",
|
||||||
"sequelize": "^6.3.4",
|
"sequelize": "^6.3.4",
|
||||||
|
|
|
@ -8,6 +8,7 @@
|
||||||
"Drafts": "Drafts",
|
"Drafts": "Drafts",
|
||||||
"Templates": "Templates",
|
"Templates": "Templates",
|
||||||
"Deleted Collection": "Deleted Collection",
|
"Deleted Collection": "Deleted Collection",
|
||||||
|
"Submenu": "Submenu",
|
||||||
"New": "New",
|
"New": "New",
|
||||||
"Only visible to you": "Only visible to you",
|
"Only visible to you": "Only visible to you",
|
||||||
"Draft": "Draft",
|
"Draft": "Draft",
|
||||||
|
@ -22,7 +23,6 @@
|
||||||
"Never viewed": "Never viewed",
|
"Never viewed": "Never viewed",
|
||||||
"Viewed": "Viewed",
|
"Viewed": "Viewed",
|
||||||
"in": "in",
|
"in": "in",
|
||||||
"More options": "More options",
|
|
||||||
"Insert column after": "Insert column after",
|
"Insert column after": "Insert column after",
|
||||||
"Insert column before": "Insert column before",
|
"Insert column before": "Insert column before",
|
||||||
"Insert row after": "Insert row after",
|
"Insert row after": "Insert row after",
|
||||||
|
@ -76,6 +76,7 @@
|
||||||
"Warning": "Warning",
|
"Warning": "Warning",
|
||||||
"Warning notice": "Warning notice",
|
"Warning notice": "Warning notice",
|
||||||
"Icon": "Icon",
|
"Icon": "Icon",
|
||||||
|
"Choose icon": "Choose icon",
|
||||||
"Loading": "Loading",
|
"Loading": "Loading",
|
||||||
"Search": "Search",
|
"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?",
|
"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",
|
"Export Data": "Export Data",
|
||||||
"Integrations": "Integrations",
|
"Integrations": "Integrations",
|
||||||
"Installation": "Installation",
|
"Installation": "Installation",
|
||||||
|
"Appearance": "Appearance",
|
||||||
|
"System": "System",
|
||||||
|
"Light": "Light",
|
||||||
|
"Dark": "Dark",
|
||||||
|
"Account": "Account",
|
||||||
"Settings": "Settings",
|
"Settings": "Settings",
|
||||||
"API documentation": "API documentation",
|
"API documentation": "API documentation",
|
||||||
"Changelog": "Changelog",
|
"Changelog": "Changelog",
|
||||||
"Send us feedback": "Send us feedback",
|
"Send us feedback": "Send us feedback",
|
||||||
"Report a bug": "Report a bug",
|
"Report a bug": "Report a bug",
|
||||||
"Appearance": "Appearance",
|
|
||||||
"System": "System",
|
|
||||||
"Light": "Light",
|
|
||||||
"Dark": "Dark",
|
|
||||||
"Log out": "Log out",
|
"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",
|
"New document": "New document",
|
||||||
"Import document": "Import document",
|
"Import document": "Import document",
|
||||||
"Edit": "Edit",
|
"Edit": "Edit",
|
||||||
"Permissions": "Permissions",
|
"Permissions": "Permissions",
|
||||||
"Export": "Export",
|
"Export": "Export",
|
||||||
"Delete": "Delete",
|
"Delete": "Delete",
|
||||||
|
"Collection permissions": "Collection permissions",
|
||||||
"Edit collection": "Edit collection",
|
"Edit collection": "Edit collection",
|
||||||
"Delete collection": "Delete collection",
|
"Delete collection": "Delete collection",
|
||||||
"Export collection": "Export collection",
|
"Export collection": "Export collection",
|
||||||
|
@ -131,6 +138,7 @@
|
||||||
"Document archived": "Document archived",
|
"Document archived": "Document archived",
|
||||||
"Document restored": "Document restored",
|
"Document restored": "Document restored",
|
||||||
"Document unpublished": "Document unpublished",
|
"Document unpublished": "Document unpublished",
|
||||||
|
"Document options": "Document options",
|
||||||
"Restore": "Restore",
|
"Restore": "Restore",
|
||||||
"Choose a collection": "Choose a collection",
|
"Choose a collection": "Choose a collection",
|
||||||
"Unpin": "Unpin",
|
"Unpin": "Unpin",
|
||||||
|
@ -152,21 +160,26 @@
|
||||||
"Share document": "Share document",
|
"Share document": "Share document",
|
||||||
"Edit group": "Edit group",
|
"Edit group": "Edit group",
|
||||||
"Delete group": "Delete group",
|
"Delete group": "Delete group",
|
||||||
"Members": "Members",
|
"Group options": "Group options",
|
||||||
|
"Member options": "Member options",
|
||||||
"collection": "collection",
|
"collection": "collection",
|
||||||
|
"New child document": "New child document",
|
||||||
"New document in <1>{{collectionName}}</1>": "New document in <1>{{collectionName}}</1>",
|
"New document in <1>{{collectionName}}</1>": "New document in <1>{{collectionName}}</1>",
|
||||||
"New template": "New template",
|
"New template": "New template",
|
||||||
"Link copied": "Link copied",
|
"Link copied": "Link copied",
|
||||||
|
"Revision options": "Revision options",
|
||||||
"Restore version": "Restore version",
|
"Restore version": "Restore version",
|
||||||
"Copy link": "Copy link",
|
"Copy link": "Copy link",
|
||||||
"Share link revoked": "Share link revoked",
|
"Share link revoked": "Share link revoked",
|
||||||
"Share link copied": "Share link copied",
|
"Share link copied": "Share link copied",
|
||||||
|
"Share options": "Share options",
|
||||||
"Go to document": "Go to document",
|
"Go to document": "Go to document",
|
||||||
"Revoke link": "Revoke link",
|
"Revoke link": "Revoke link",
|
||||||
"By {{ author }}": "By {{ author }}",
|
"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 }} 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 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.",
|
"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 }} a member…": "Make {{ userName }} a member…",
|
||||||
"Make {{ userName }} an admin…": "Make {{ userName }} an admin…",
|
"Make {{ userName }} an admin…": "Make {{ userName }} an admin…",
|
||||||
"Revoke invite": "Revoke invite",
|
"Revoke invite": "Revoke invite",
|
||||||
|
@ -212,7 +225,6 @@
|
||||||
"No people left to add": "No people left to add",
|
"No people left to add": "No people left to add",
|
||||||
"Read only": "Read only",
|
"Read only": "Read only",
|
||||||
"Read & Edit": "Read & Edit",
|
"Read & Edit": "Read & Edit",
|
||||||
"Remove": "Remove",
|
|
||||||
"Active <1></1> ago": "Active <1></1> ago",
|
"Active <1></1> ago": "Active <1></1> ago",
|
||||||
"Never signed in": "Never signed in",
|
"Never signed in": "Never signed in",
|
||||||
"Invited": "Invited",
|
"Invited": "Invited",
|
||||||
|
|
40
yarn.lock
40
yarn.lock
|
@ -1365,6 +1365,11 @@
|
||||||
dependencies:
|
dependencies:
|
||||||
"@types/node" ">= 8"
|
"@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":
|
"@react-dnd/asap@^4.0.0":
|
||||||
version "4.0.0"
|
version "4.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/@react-dnd/asap/-/asap-4.0.0.tgz#b300eeed83e9801f51bd66b0337c9a6f04548651"
|
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"
|
resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-5.1.3.tgz#beca005408f642ebebea80b042b4d18d2ac0ee6b"
|
||||||
integrity sha512-GkTiFpjFtUzU9CbMeJ5iazkCzGL3jrhzerzZIuqLABjbwRaFt33I9tUdSNryIptM+RxDet6OKm2WnLXzW51KsQ==
|
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:
|
boolbase@~1.0.0:
|
||||||
version "1.0.0"
|
version "1.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e"
|
resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e"
|
||||||
|
@ -10078,6 +10088,36 @@ readdirp@~3.5.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
picomatch "^2.2.1"
|
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:
|
rechoir@^0.6.2:
|
||||||
version "0.6.2"
|
version "0.6.2"
|
||||||
resolved "https://registry.yarnpkg.com/rechoir/-/rechoir-0.6.2.tgz#85204b54dba82d5742e28c96756ef43af50e3384"
|
resolved "https://registry.yarnpkg.com/rechoir/-/rechoir-0.6.2.tgz#85204b54dba82d5742e28c96756ef43af50e3384"
|
||||||
|
|
Reference in New Issue