Assorted cleanup, minor bug fixes, styling fixes, eslint rules (#5165

* fix: Logic error in toast
fix: Remove useless component

* fix: Logout not clearing all stores

* Add icons to notification settings

* Add eslint rule to enforce spaced comment

* Add eslint rule for arrow-body-style

* Add eslint rule to enforce self-closing components

* Add menu to api key settings
Fix: Deleting webhook subscription does not remove from UI
Split webhook subscriptions into active and inactive
Styling updates
This commit is contained in:
Tom Moor
2023-04-08 08:25:20 -04:00
committed by GitHub
parent 422bdc32d9
commit db73879918
116 changed files with 792 additions and 1088 deletions

View File

@@ -25,10 +25,16 @@
"rules": { "rules": {
"eqeqeq": 2, "eqeqeq": 2,
"curly": 2, "curly": 2,
"arrow-body-style": ["error", "as-needed"],
"spaced-comment": "error",
"object-shorthand": "error", "object-shorthand": "error",
"no-mixed-operators": "off", "no-mixed-operators": "off",
"no-useless-escape": "off", "no-useless-escape": "off",
"es/no-regexp-lookbehind-assertions": "error", "es/no-regexp-lookbehind-assertions": "error",
"react/self-closing-comp": ["error", {
"component": true,
"html": true
}],
"@typescript-eslint/no-unused-vars": [ "@typescript-eslint/no-unused-vars": [
"error", "error",
{ {

View File

@@ -17,9 +17,9 @@ import { createAction } from "~/actions";
import { CollectionSection } from "~/actions/sections"; import { CollectionSection } from "~/actions/sections";
import history from "~/utils/history"; import history from "~/utils/history";
const ColorCollectionIcon = ({ collection }: { collection: Collection }) => { const ColorCollectionIcon = ({ collection }: { collection: Collection }) => (
return <DynamicCollectionIcon collection={collection} />; <DynamicCollectionIcon collection={collection} />
}; );
export const openCollection = createAction({ export const openCollection = createAction({
name: ({ t }) => t("Open collection"), name: ({ t }) => t("Open collection"),

View File

@@ -9,32 +9,28 @@ import { createAction } from "~/actions";
import { ActionContext } from "~/types"; import { ActionContext } from "~/types";
import { TeamSection } from "../sections"; import { TeamSection } from "../sections";
export const createTeamsList = ({ stores }: { stores: RootStore }) => { export const createTeamsList = ({ stores }: { stores: RootStore }) =>
return ( stores.auth.availableTeams?.map((session) => ({
stores.auth.availableTeams?.map((session) => ({ id: `switch-${session.id}`,
id: `switch-${session.id}`, name: session.name,
name: session.name, analyticsName: "Switch workspace",
analyticsName: "Switch workspace", section: TeamSection,
section: TeamSection, keywords: "change switch workspace organization team",
keywords: "change switch workspace organization team", icon: () => (
icon: () => ( <StyledTeamLogo
<StyledTeamLogo alt={session.name}
alt={session.name} model={{
model={{ initial: session.name[0],
initial: session.name[0], avatarUrl: session.avatarUrl,
avatarUrl: session.avatarUrl, id: session.id,
id: session.id, color: stringToColor(session.id),
color: stringToColor(session.id), }}
}} size={24}
size={24} />
/> ),
), visible: ({ currentTeamId }: ActionContext) => currentTeamId !== session.id,
visible: ({ currentTeamId }: ActionContext) => perform: () => (window.location.href = session.url),
currentTeamId !== session.id, })) ?? [];
perform: () => (window.location.href = session.url),
})) ?? []
);
};
export const switchTeam = createAction({ export const switchTeam = createAction({
name: ({ t }) => t("Switch workspace"), name: ({ t }) => t("Switch workspace"),
@@ -53,9 +49,8 @@ export const createTeam = createAction({
keywords: "create change switch workspace organization team", keywords: "create change switch workspace organization team",
section: TeamSection, section: TeamSection,
icon: <PlusIcon />, icon: <PlusIcon />,
visible: ({ stores, currentTeamId }) => { visible: ({ stores, currentTeamId }) =>
return stores.policies.abilities(currentTeamId ?? "").createTeam; stores.policies.abilities(currentTeamId ?? "").createTeam,
},
perform: ({ t, event, stores }) => { perform: ({ t, event, stores }) => {
event?.preventDefault(); event?.preventDefault();
event?.stopPropagation(); event?.stopPropagation();

View File

@@ -26,12 +26,10 @@ const Content = styled.div`
`}; `};
`; `;
const CenteredContent: React.FC<Props> = ({ children, ...rest }) => { const CenteredContent: React.FC<Props> = ({ children, ...rest }) => (
return ( <Container {...rest}>
<Container {...rest}> <Content>{children}</Content>
<Content>{children}</Content> </Container>
</Container> );
);
};
export default CenteredContent; export default CenteredContent;

View File

@@ -42,7 +42,7 @@ const Circle = ({
style={{ style={{
transition: "stroke-dashoffset 0.6s ease 0s", transition: "stroke-dashoffset 0.6s ease 0s",
}} }}
></circle> />
); );
}; };

View File

@@ -37,7 +37,7 @@ export type Placement =
| "left-start"; | "left-start";
type Props = MenuStateReturn & { type Props = MenuStateReturn & {
"aria-label": string; "aria-label"?: string;
/** The parent menu state if this is a submenu. */ /** The parent menu state if this is a submenu. */
parentMenuState?: MenuStateReturn; parentMenuState?: MenuStateReturn;
/** Called when the context menu is opened. */ /** Called when the context menu is opened. */

View File

@@ -131,7 +131,7 @@ const SmallSlash = styled(GoToIcon)`
vertical-align: middle; vertical-align: middle;
flex-shrink: 0; flex-shrink: 0;
fill: ${(props) => props.theme.slate}; fill: ${(props) => props.theme.textTertiary};
opacity: 0.5; opacity: 0.5;
`; `;

View File

@@ -63,11 +63,13 @@ function DocumentExplorer({ onSubmit, onSelect, items }: Props) {
const VERTICAL_PADDING = 6; const VERTICAL_PADDING = 6;
const HORIZONTAL_PADDING = 24; const HORIZONTAL_PADDING = 24;
const searchIndex = React.useMemo(() => { const searchIndex = React.useMemo(
return new FuzzySearch(items, ["title"], { () =>
caseSensitive: false, new FuzzySearch(items, ["title"], {
}); caseSensitive: false,
}, [items]); }),
[items]
);
React.useEffect(() => { React.useEffect(() => {
if (searchTerm) { if (searchTerm) {
@@ -119,9 +121,7 @@ function DocumentExplorer({ onSubmit, onSelect, items }: Props) {
setSearchTerm(ev.target.value); setSearchTerm(ev.target.value);
}; };
const isExpanded = (node: number) => { const isExpanded = (node: number) => includes(expandedNodes, nodes[node].id);
return includes(expandedNodes, nodes[node].id);
};
const calculateInitialScrollOffset = (itemCount: number) => { const calculateInitialScrollOffset = (itemCount: number) => {
if (listRef.current) { if (listRef.current) {
@@ -169,9 +169,7 @@ function DocumentExplorer({ onSubmit, onSelect, items }: Props) {
return selectedNodeId === nodeId; return selectedNodeId === nodeId;
}; };
const hasChildren = (node: number) => { const hasChildren = (node: number) => nodes[node].children.length > 0;
return nodes[node].children.length > 0;
};
const toggleCollapse = (node: number) => { const toggleCollapse = (node: number) => {
if (!hasChildren(node)) { if (!hasChildren(node)) {
@@ -275,13 +273,9 @@ function DocumentExplorer({ onSubmit, onSelect, items }: Props) {
inputSearchRef.current?.focus(); inputSearchRef.current?.focus();
}; };
const next = () => { const next = () => Math.min(activeNode + 1, nodes.length - 1);
return Math.min(activeNode + 1, nodes.length - 1);
};
const prev = () => { const prev = () => Math.max(activeNode - 1, 0);
return Math.max(activeNode - 1, 0);
};
const handleKeyDown = (ev: React.KeyboardEvent<HTMLDivElement>) => { const handleKeyDown = (ev: React.KeyboardEvent<HTMLDivElement>) => {
switch (ev.key) { switch (ev.key) {

View File

@@ -116,13 +116,11 @@ function Editor(props: Props, ref: React.RefObject<SharedEditor> | null) {
const results = await documents.searchTitles(term); const results = await documents.searchTitles(term);
return sortBy( return sortBy(
results.map((document: Document) => { results.map((document: Document) => ({
return { title: document.title,
title: document.title, subtitle: <DocumentBreadcrumb document={document} onlyText />,
subtitle: <DocumentBreadcrumb document={document} onlyText />, url: document.url,
url: document.url, })),
};
}),
(document) => (document) =>
deburr(document.title) deburr(document.title)
.toLowerCase() .toLowerCase()

View File

@@ -240,29 +240,27 @@ function IconPicker({ onOpen, onClose, icon, color, onChange }: Props) {
aria-label={t("Choose icon")} aria-label={t("Choose icon")}
> >
<Icons> <Icons>
{Object.keys(icons).map((name, index) => { {Object.keys(icons).map((name, index) => (
return ( <MenuItem
<MenuItem key={name}
key={name} onClick={() => onChange(color, name)}
onClick={() => onChange(color, name)} {...menu}
{...menu} >
> {(props) => (
{(props) => ( <IconButton
<IconButton style={
style={ {
{ ...style,
...style, "--delay": `${index * 8}ms`,
"--delay": `${index * 8}ms`, } as React.CSSProperties
} as React.CSSProperties }
} {...props}
{...props} >
> <Icon as={icons[name].component} color={color} size={30} />
<Icon as={icons[name].component} color={color} size={30} /> </IconButton>
</IconButton> )}
)} </MenuItem>
</MenuItem> ))}
);
})}
</Icons> </Icons>
<Colors> <Colors>
<React.Suspense <React.Suspense

View File

@@ -46,9 +46,8 @@ export type Props = {
onChange?: (value: string | null) => void; onChange?: (value: string | null) => void;
}; };
const getOptionFromValue = (options: Option[], value: string | null) => { const getOptionFromValue = (options: Option[], value: string | null) =>
return options.find((option) => option.value === value); options.find((option) => option.value === value);
};
const InputSelect = (props: Props) => { const InputSelect = (props: Props) => {
const { const {

View File

@@ -14,18 +14,16 @@ type Props = {
body?: PlaceholderTextProps; body?: PlaceholderTextProps;
}; };
const Placeholder = ({ count, className, header, body }: Props) => { const Placeholder = ({ count, className, header, body }: Props) => (
return ( <Fade>
<Fade> {times(count || 2, (index) => (
{times(count || 2, (index) => ( <Item key={index} className={className} column auto>
<Item key={index} className={className} column auto> <PlaceholderText {...header} header delay={0.2 * index} />
<PlaceholderText {...header} header delay={0.2 * index} /> <PlaceholderText {...body} delay={0.2 * index} />
<PlaceholderText {...body} delay={0.2 * index} /> </Item>
</Item> ))}
))} </Fade>
</Fade> );
);
};
const Item = styled(Flex)` const Item = styled(Flex)`
padding: 10px 0; padding: 10px 0;

View File

@@ -2,13 +2,11 @@ import * as React from "react";
import styled, { keyframes } from "styled-components"; import styled, { keyframes } from "styled-components";
import { depths, s } from "@shared/styles"; import { depths, s } from "@shared/styles";
const LoadingIndicatorBar = () => { const LoadingIndicatorBar = () => (
return ( <Container>
<Container> <Loader />
<Loader /> </Container>
</Container> );
);
};
const loadingFrame = keyframes` const loadingFrame = keyframes`
from { margin-left: -100%; } from { margin-left: -100%; }

View File

@@ -9,24 +9,22 @@ type Props = {
description?: JSX.Element; description?: JSX.Element;
}; };
const Notice: React.FC<Props> = ({ children, icon, description }) => { const Notice: React.FC<Props> = ({ children, icon, description }) => (
return ( <Container>
<Container> <Flex as="span" gap={8}>
<Flex as="span" gap={8}> {icon}
{icon} <span>
<span> <Title>{children}</Title>
<Title>{children}</Title> {description && (
{description && ( <>
<> <br />
<br /> {description}
{description} </>
</> )}
)} </span>
</span> </Flex>
</Flex> </Container>
</Container> );
);
};
const Title = styled.span` const Title = styled.span`
font-weight: 500; font-weight: 500;

View File

@@ -1,29 +0,0 @@
import * as React from "react";
import Notice from "~/components/Notice";
const NoticeAlert: React.FC = ({ children }) => {
return (
<Notice>
<svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
style={{
position: "relative",
top: "2px",
marginRight: "4px",
}}
>
<path
d="M15.6676 11.5372L10.0155 1.14735C9.10744 -0.381434 6.89378 -0.383465 5.98447 1.14735L0.332715 11.5372C-0.595598 13.0994 0.528309 15.0776 2.34778 15.0776H13.652C15.47 15.0776 16.5959 13.101 15.6676 11.5372ZM8 13.2026C7.48319 13.2026 7.0625 12.7819 7.0625 12.2651C7.0625 11.7483 7.48319 11.3276 8 11.3276C8.51681 11.3276 8.9375 11.7483 8.9375 12.2651C8.9375 12.7819 8.51681 13.2026 8 13.2026ZM8.9375 9.45257C8.9375 9.96938 8.51681 10.3901 8 10.3901C7.48319 10.3901 7.0625 9.96938 7.0625 9.45257V4.76507C7.0625 4.24826 7.48319 3.82757 8 3.82757C8.51681 3.82757 8.9375 4.24826 8.9375 4.76507V9.45257Z"
fill="currentColor"
/>
</svg>{" "}
{children}
</Notice>
);
};
export default NoticeAlert;

View File

@@ -21,32 +21,30 @@ const Scene: React.FC<Props> = ({
left, left,
children, children,
centered, centered,
}) => { }) => (
return ( <FillWidth>
<FillWidth> <PageTitle title={textTitle || title} />
<PageTitle title={textTitle || title} /> <Header
<Header hasSidebar
hasSidebar title={
title={ icon ? (
icon ? ( <>
<> {icon}&nbsp;{title}
{icon}&nbsp;{title} </>
</> ) : (
) : ( title
title )
) }
} actions={actions}
actions={actions} left={left}
left={left} />
/> {centered !== false ? (
{centered !== false ? ( <CenteredContent withStickyHeader>{children}</CenteredContent>
<CenteredContent withStickyHeader>{children}</CenteredContent> ) : (
) : ( children
children )}
)} </FillWidth>
</FillWidth> );
);
};
const FillWidth = styled.div` const FillWidth = styled.div`
width: 100%; width: 100%;

View File

@@ -158,21 +158,19 @@ function SearchPopover({ shareId }: Props) {
return ( return (
<> <>
<PopoverDisclosure {...popover}> <PopoverDisclosure {...popover}>
{(props) => { {(props) => (
// props assumes the disclosure is a button, but we want a type-ahead // props assumes the disclosure is a button, but we want a type-ahead
// so we take the aria props, and ref and ignore the event handlers // so we take the aria props, and ref and ignore the event handlers
return ( <StyledInputSearch
<StyledInputSearch aria-controls={props["aria-controls"]}
aria-controls={props["aria-controls"]} aria-expanded={props["aria-expanded"]}
aria-expanded={props["aria-expanded"]} aria-haspopup={props["aria-haspopup"]}
aria-haspopup={props["aria-haspopup"]} ref={props.ref}
ref={props.ref} onChange={handleSearchInputChange}
onChange={handleSearchInputChange} onFocus={handleSearchInputFocus}
onFocus={handleSearchInputFocus} onKeyDown={handleKeyDown}
onKeyDown={handleKeyDown} />
/> )}
);
}}
</PopoverDisclosure> </PopoverDisclosure>
<Popover <Popover
{...popover} {...popover}

View File

@@ -34,9 +34,7 @@ function Collections() {
fractionalIndex(null, orderedCollections[0].index) fractionalIndex(null, orderedCollections[0].index)
); );
}, },
canDrop: (item) => { canDrop: (item) => item.id !== orderedCollections[0].id,
return item.id !== orderedCollections[0].id;
},
collect: (monitor) => ({ collect: (monitor) => ({
isCollectionDropping: monitor.isOver(), isCollectionDropping: monitor.isOver(),
isDraggingAnyCollection: monitor.getItemType() === "collection", isDraggingAnyCollection: monitor.getItemType() === "collection",

View File

@@ -76,18 +76,20 @@ function InnerDocumentLink(
[collection, node] [collection, node]
); );
const showChildren = React.useMemo(() => { const showChildren = React.useMemo(
return !!( () =>
hasChildDocuments && !!(
activeDocument && hasChildDocuments &&
collection && activeDocument &&
(collection collection &&
.pathToDocument(activeDocument.id) (collection
.map((entry) => entry.id) .pathToDocument(activeDocument.id)
.includes(node.id) || .map((entry) => entry.id)
isActiveDocument) .includes(node.id) ||
); isActiveDocument)
}, [hasChildDocuments, activeDocument, isActiveDocument, node, collection]); ),
[hasChildDocuments, activeDocument, isActiveDocument, node, collection]
);
const [expanded, setExpanded] = React.useState(showChildren); const [expanded, setExpanded] = React.useState(showChildren);

View File

@@ -56,12 +56,9 @@ function DraggableCollectionLink({
fractionalIndex(collection.index, belowCollectionIndex) fractionalIndex(collection.index, belowCollectionIndex)
); );
}, },
canDrop: (item) => { canDrop: (item) =>
return ( collection.id !== item.id &&
collection.id !== item.id && (!belowCollection || item.id !== belowCollection.id),
(!belowCollection || item.id !== belowCollection.id)
);
},
collect: (monitor: DropTargetMonitor<Collection, Collection>) => ({ collect: (monitor: DropTargetMonitor<Collection, Collection>) => ({
isCollectionDropping: monitor.isOver(), isCollectionDropping: monitor.isOver(),
isDraggingAnyCollection: monitor.canDrop(), isDraggingAnyCollection: monitor.canDrop(),

View File

@@ -21,15 +21,13 @@ const resolveToLocation = (
const normalizeToLocation = ( const normalizeToLocation = (
to: LocationDescriptor, to: LocationDescriptor,
currentLocation: Location currentLocation: Location
) => { ) =>
return typeof to === "string" typeof to === "string"
? createLocation(to, null, undefined, currentLocation) ? createLocation(to, null, undefined, currentLocation)
: to; : to;
};
const joinClassnames = (...classnames: (string | undefined)[]) => { const joinClassnames = (...classnames: (string | undefined)[]) =>
return classnames.filter((i) => i).join(" "); classnames.filter((i) => i).join(" ");
};
export type Props = React.AnchorHTMLAttributes<HTMLAnchorElement> & { export type Props = React.AnchorHTMLAttributes<HTMLAnchorElement> & {
activeClassName?: string; activeClassName?: string;
@@ -103,16 +101,13 @@ const NavLink = ({
}, [linkRef, scrollIntoViewIfNeeded, isActive]); }, [linkRef, scrollIntoViewIfNeeded, isActive]);
const shouldFastClick = React.useCallback( const shouldFastClick = React.useCallback(
(event: React.MouseEvent<HTMLAnchorElement>): boolean => { (event: React.MouseEvent<HTMLAnchorElement>): boolean =>
return ( event.button === 0 && // Only intercept left clicks
event.button === 0 && // Only intercept left clicks !event.defaultPrevented &&
!event.defaultPrevented && !rest.target &&
!rest.target && !event.altKey &&
!event.altKey && !event.metaKey &&
!event.metaKey && !event.ctrlKey,
!event.ctrlKey
);
},
[rest.target] [rest.target]
); );
@@ -153,7 +148,7 @@ const NavLink = ({
<Link <Link
key={isActive ? "active" : "inactive"} key={isActive ? "active" : "inactive"}
ref={linkRef} ref={linkRef}
//onMouseDown={handleClick} // onMouseDown={handleClick}
onKeyDown={(event) => { onKeyDown={(event) => {
if (["Enter", " "].includes(event.key)) { if (["Enter", " "].includes(event.key)) {
navigateTo(); navigateTo();

View File

@@ -42,9 +42,9 @@ function DocumentLink(
!!node.children.length || activeDocument?.parentDocumentId === node.id; !!node.children.length || activeDocument?.parentDocumentId === node.id;
const document = documents.get(node.id); const document = documents.get(node.id);
const showChildren = React.useMemo(() => { const showChildren = React.useMemo(() => !!hasChildDocuments, [
return !!hasChildDocuments; hasChildDocuments,
}, [hasChildDocuments]); ]);
const [expanded, setExpanded] = React.useState(showChildren); const [expanded, setExpanded] = React.useState(showChildren);
@@ -111,9 +111,7 @@ function DocumentLink(
scrollIntoViewIfNeeded={!document?.isStarred} scrollIntoViewIfNeeded={!document?.isStarred}
isDraft={isDraft} isDraft={isDraft}
ref={ref} ref={ref}
isActive={() => { isActive={() => !!isActiveDocument}
return !!isActiveDocument;
}}
/> />
{expanded && {expanded &&
nodeChildren.map((childNode, index) => ( nodeChildren.map((childNode, index) => (

View File

@@ -6,7 +6,6 @@ import { useTranslation } from "react-i18next";
import Star from "~/models/Star"; import Star from "~/models/Star";
import Flex from "~/components/Flex"; import Flex from "~/components/Flex";
import useStores from "~/hooks/useStores"; import useStores from "~/hooks/useStores";
import useToasts from "~/hooks/useToasts";
import DropCursor from "./DropCursor"; import DropCursor from "./DropCursor";
import Header from "./Header"; import Header from "./Header";
import PlaceholderCollections from "./PlaceholderCollections"; import PlaceholderCollections from "./PlaceholderCollections";
@@ -22,7 +21,6 @@ function Starred() {
const [displayedStarsCount, setDisplayedStarsCount] = React.useState( const [displayedStarsCount, setDisplayedStarsCount] = React.useState(
STARRED_PAGINATION_LIMIT STARRED_PAGINATION_LIMIT
); );
const { showToast } = useToasts();
const { stars } = useStores(); const { stars } = useStores();
const { t } = useTranslation(); const { t } = useTranslation();
@@ -34,13 +32,10 @@ function Starred() {
offset, offset,
}); });
} catch (error) { } catch (error) {
showToast(t("Starred documents could not be loaded"), {
type: "error",
});
setFetchError(error); setFetchError(error);
} }
}, },
[stars, showToast, t] [stars]
); );
React.useEffect(() => { React.useEffect(() => {

View File

@@ -22,7 +22,7 @@ export default function Spinner({ color, ...props }: Props) {
cx="8" cx="8"
cy="8" cy="8"
r="6" r="6"
></Circle> />
</SVG> </SVG>
); );
} }

View File

@@ -7,23 +7,21 @@ type Props = {
color?: string; color?: string;
}; };
const Squircle: React.FC<Props> = ({ color, size = 28, children }) => { const Squircle: React.FC<Props> = ({ color, size = 28, children }) => (
return ( <Wrapper
<Wrapper style={{ width: size, height: size }}
style={{ width: size, height: size }} align="center"
align="center" justify="center"
justify="center" >
> <svg width={size} height={size} viewBox="0 0 28 28">
<svg width={size} height={size} viewBox="0 0 28 28"> <path
<path fill={color}
fill={color} d="M0 11.1776C0 1.97285 1.97285 0 11.1776 0H16.8224C26.0272 0 28 1.97285 28 11.1776V16.8224C28 26.0272 26.0272 28 16.8224 28H11.1776C1.97285 28 0 26.0272 0 16.8224V11.1776Z"
d="M0 11.1776C0 1.97285 1.97285 0 11.1776 0H16.8224C26.0272 0 28 1.97285 28 11.1776V16.8224C28 26.0272 26.0272 28 16.8224 28H11.1776C1.97285 28 0 26.0272 0 16.8224V11.1776Z" />
/> </svg>
</svg> <Content>{children}</Content>
<Content>{children}</Content> </Wrapper>
</Wrapper> );
);
};
const Wrapper = styled(Flex)` const Wrapper = styled(Flex)`
position: relative; position: relative;

View File

@@ -34,14 +34,12 @@ const Background = styled.div<{ sticky?: boolean }>`
z-index: 1; z-index: 1;
`; `;
const Subheading: React.FC<Props> = ({ children, sticky, ...rest }) => { const Subheading: React.FC<Props> = ({ children, sticky, ...rest }) => (
return ( <Background sticky={sticky}>
<Background sticky={sticky}> <H3 {...rest}>
<H3 {...rest}> <Underline>{children}</Underline>
<Underline>{children}</Underline> </H3>
</H3> </Background>
</Background> );
);
};
export default Subheading; export default Subheading;

View File

@@ -17,6 +17,7 @@ const TabLink = styled(NavLink)`
font-size: 14px; font-size: 14px;
cursor: var(--pointer); cursor: var(--pointer);
color: ${s("textTertiary")}; color: ${s("textTertiary")};
user-select: none;
margin-right: 24px; margin-right: 24px;
padding: 6px 0; padding: 6px 0;

View File

@@ -195,23 +195,21 @@ export const Placeholder = ({
}: { }: {
columns: number; columns: number;
rows?: number; rows?: number;
}) => { }) => (
return ( <DelayedMount>
<DelayedMount> <tbody>
<tbody> {new Array(rows).fill(1).map((_, row) => (
{new Array(rows).fill(1).map((_, row) => ( <Row key={row}>
<Row key={row}> {new Array(columns).fill(1).map((_, col) => (
{new Array(columns).fill(1).map((_, col) => ( <Cell key={col}>
<Cell key={col}> <PlaceholderText minWidth={25} maxWidth={75} />
<PlaceholderText minWidth={25} maxWidth={75} /> </Cell>
</Cell> ))}
))} </Row>
</Row> ))}
))} </tbody>
</tbody> </DelayedMount>
</DelayedMount> );
);
};
const Anchor = styled.div` const Anchor = styled.div`
top: -32px; top: -32px;

View File

@@ -61,8 +61,9 @@ function Toast({ closeAfterMs = 3000, onRequestClose, toast }: Props) {
{type === "loading" && <Spinner color="currentColor" />} {type === "loading" && <Spinner color="currentColor" />}
{type === "info" && <InfoIcon color="currentColor" />} {type === "info" && <InfoIcon color="currentColor" />}
{type === "success" && <CheckboxIcon checked color="currentColor" />} {type === "success" && <CheckboxIcon checked color="currentColor" />}
{type === "warning" || {(type === "warning" || type === "error") && (
(type === "error" && <WarningIcon color="currentColor" />)} <WarningIcon color="currentColor" />
)}
<Message>{toast.message}</Message> <Message>{toast.message}</Message>
{action && <Action onClick={action.onClick}>{action.text}</Action>} {action && <Action onClick={action.onClick}>{action.text}</Action>}
</Container> </Container>

View File

@@ -30,7 +30,7 @@ function BlockMenu(props: Props) {
return ( return (
<SuggestionsMenu <SuggestionsMenu
{...props} {...props}
filterable={true} filterable
onClearSearch={clearSearch} onClearSearch={clearSearch}
renderMenuItem={(item, _index, options) => ( renderMenuItem={(item, _index, options) => (
<SuggestionsMenuItem <SuggestionsMenuItem

View File

@@ -25,10 +25,9 @@ function LinkSearchResult({
scrollIntoView(node, { scrollIntoView(node, {
scrollMode: "if-needed", scrollMode: "if-needed",
block: "center", block: "center",
boundary: (parent) => { boundary: (parent) =>
// Prevents body and other parent elements from being scrolled // Prevents body and other parent elements from being scrolled
return parent !== containerRef.current; parent !== containerRef.current,
},
}); });
} }
}, },

View File

@@ -397,11 +397,9 @@ function SuggestionsMenu<T extends MenuItem>(props: Props<T>) {
}); });
return filterExcessSeparators( return filterExcessSeparators(
filtered.sort((item) => { filtered.sort((item) =>
return searchInput && item.title searchInput && item.title ? commandScore(item.title, searchInput) : 0
? commandScore(item.title, searchInput) )
: 0;
})
); );
}, [commands, props]); }, [commands, props]);

View File

@@ -28,12 +28,11 @@ function SuggestionsMenuItem({
scrollIntoView(node, { scrollIntoView(node, {
scrollMode: "if-needed", scrollMode: "if-needed",
block: "nearest", block: "nearest",
boundary: (parent) => { boundary: (parent) =>
// All the parent elements of your target are checked until they // All the parent elements of your target are checked until they
// reach the portal context. Prevents body and other parent // reach the portal context. Prevents body and other parent
// elements from being scrolled // elements from being scrolled
return parent !== portal; parent !== portal,
},
}); });
} }
}, },

View File

@@ -355,8 +355,8 @@ export class Editor extends React.PureComponent<
decorations: Decoration<{ decorations: Decoration<{
[key: string]: any; [key: string]: any;
}>[] }>[]
) => { ) =>
return new ComponentView(extension.component, { new ComponentView(extension.component, {
editor: this, editor: this,
extension, extension,
node, node,
@@ -364,7 +364,6 @@ export class Editor extends React.PureComponent<
getPos, getPos,
decorations, decorations,
}); });
};
return { return {
...nodeViews, ...nodeViews,
@@ -449,13 +448,12 @@ export class Editor extends React.PureComponent<
throw new Error("createView called before ref available"); throw new Error("createView called before ref available");
} }
const isEditingCheckbox = (tr: Transaction) => { const isEditingCheckbox = (tr: Transaction) =>
return tr.steps.some( tr.steps.some(
(step: any) => (step: any) =>
step.slice?.content?.firstChild?.type.name === step.slice?.content?.firstChild?.type.name ===
this.schema.nodes.checkbox_item.name this.schema.nodes.checkbox_item.name
); );
};
const self = this; // eslint-disable-line const self = this; // eslint-disable-line
const view = new EditorView(this.elementRef.current, { const view = new EditorView(this.elementRef.current, {
@@ -579,36 +577,28 @@ export class Editor extends React.PureComponent<
* *
* @returns True if the editor is empty * @returns True if the editor is empty
*/ */
public isEmpty = () => { public isEmpty = () => ProsemirrorHelper.isEmpty(this.view.state.doc);
return ProsemirrorHelper.isEmpty(this.view.state.doc);
};
/** /**
* Return the headings in the current editor. * Return the headings in the current editor.
* *
* @returns A list of headings in the document * @returns A list of headings in the document
*/ */
public getHeadings = () => { public getHeadings = () => ProsemirrorHelper.getHeadings(this.view.state.doc);
return ProsemirrorHelper.getHeadings(this.view.state.doc);
};
/** /**
* Return the tasks/checkmarks in the current editor. * Return the tasks/checkmarks in the current editor.
* *
* @returns A list of tasks in the document * @returns A list of tasks in the document
*/ */
public getTasks = () => { public getTasks = () => ProsemirrorHelper.getTasks(this.view.state.doc);
return ProsemirrorHelper.getTasks(this.view.state.doc);
};
/** /**
* Return the comments in the current editor. * Return the comments in the current editor.
* *
* @returns A list of comments in the document * @returns A list of comments in the document
*/ */
public getComments = () => { public getComments = () => ProsemirrorHelper.getComments(this.view.state.doc);
return ProsemirrorHelper.getComments(this.view.state.doc);
};
/** /**
* Remove a specific comment mark from the document. * Remove a specific comment mark from the document.
@@ -661,9 +651,9 @@ export class Editor extends React.PureComponent<
return; return;
} }
this.props.onChange((asString = true, trim = false) => { this.props.onChange((asString = true, trim = false) =>
return this.view ? this.value(asString, trim) : undefined; this.view ? this.value(asString, trim) : undefined
}); );
}; };
private handleEditorBlur = () => { private handleEditorBlur = () => {
@@ -835,13 +825,11 @@ const EditorContainer = styled(Styles)<{ focusedCommentId?: string }>`
`; `;
const LazyLoadedEditor = React.forwardRef<Editor, Props>( const LazyLoadedEditor = React.forwardRef<Editor, Props>(
(props: Props, ref) => { (props: Props, ref) => (
return ( <WithTheme>
<WithTheme> {(theme) => <Editor theme={theme} {...props} ref={ref} />}
{(theme) => <Editor theme={theme} {...props} ref={ref} />} </WithTheme>
</WithTheme> )
);
}
); );
export default LazyLoadedEditor; export default LazyLoadedEditor;

View File

@@ -21,17 +21,19 @@ export default function useBuildTheme(customTheme: Partial<CustomTheme> = {}) {
const isMobile = useMediaQuery(`(max-width: ${breakpoints.tablet}px)`); const isMobile = useMediaQuery(`(max-width: ${breakpoints.tablet}px)`);
const isPrinting = useMediaQuery("print"); const isPrinting = useMediaQuery("print");
const theme = React.useMemo(() => { const theme = React.useMemo(
return isPrinting () =>
? buildLightTheme(customTheme) isPrinting
: isMobile ? buildLightTheme(customTheme)
? ui.resolvedTheme === "dark" : isMobile
? buildPitchBlackTheme(customTheme) ? ui.resolvedTheme === "dark"
: buildLightTheme(customTheme) ? buildPitchBlackTheme(customTheme)
: ui.resolvedTheme === "dark" : buildLightTheme(customTheme)
? buildDarkTheme(customTheme) : ui.resolvedTheme === "dark"
: buildLightTheme(customTheme); ? buildDarkTheme(customTheme)
}, [customTheme, isMobile, isPrinting, ui.resolvedTheme]); : buildLightTheme(customTheme),
[customTheme, isMobile, isPrinting, ui.resolvedTheme]
);
return theme; return theme;
} }

View File

@@ -4,8 +4,8 @@ import { useTranslation } from "react-i18next";
export default function useDictionary() { export default function useDictionary() {
const { t } = useTranslation(); const { t } = useTranslation();
return React.useMemo(() => { return React.useMemo(
return { () => ({
addColumnAfter: t("Insert column after"), addColumnAfter: t("Insert column after"),
addColumnBefore: t("Insert column before"), addColumnBefore: t("Insert column before"),
addRowAfter: t("Insert row after"), addRowAfter: t("Insert row after"),
@@ -79,8 +79,9 @@ export default function useDictionary() {
insertDate: t("Current date"), insertDate: t("Current date"),
insertTime: t("Current time"), insertTime: t("Current time"),
insertDateTime: t("Current date and time"), insertDateTime: t("Current date and time"),
}; }),
}, [t]); [t]
);
} }
export type Dictionary = ReturnType<typeof useDictionary>; export type Dictionary = ReturnType<typeof useDictionary>;

View File

@@ -7,18 +7,20 @@ import useSettingsConfig from "./useSettingsConfig";
const useSettingsActions = () => { const useSettingsActions = () => {
const config = useSettingsConfig(); const config = useSettingsConfig();
const actions = React.useMemo(() => { const actions = React.useMemo(
return config.map((item) => { () =>
const Icon = item.icon; config.map((item) => {
return { const Icon = item.icon;
id: item.path, return {
name: item.name, id: item.path,
icon: <Icon color="currentColor" />, name: item.name,
section: NavigationSection, icon: <Icon color="currentColor" />,
perform: () => history.push(item.path), section: NavigationSection,
}; perform: () => history.push(item.path),
}); };
}, [config]); }),
[config]
);
const navigateToSettings = React.useMemo( const navigateToSettings = React.useMemo(
() => () =>

View File

@@ -160,16 +160,18 @@ const useSettingsConfig = () => {
icon: ExportIcon, icon: ExportIcon,
}, },
// Integrations // Integrations
...mapValues(PluginLoader.plugins, (plugin) => { ...mapValues(
return { PluginLoader.plugins,
name: plugin.config.name, (plugin) =>
path: integrationSettingsPath(plugin.id), ({
group: t("Integrations"), name: plugin.config.name,
component: plugin.settings, path: integrationSettingsPath(plugin.id),
enabled: !!plugin.settings && can.update, group: t("Integrations"),
icon: plugin.icon, component: plugin.settings,
} as ConfigItem; enabled: !!plugin.settings && can.update,
}), icon: plugin.icon,
} as ConfigItem)
),
SelfHosted: { SelfHosted: {
name: t("Self Hosted"), name: t("Self Hosted"),
path: integrationSettingsPath("self-hosted"), path: integrationSettingsPath("self-hosted"),

View File

@@ -4,11 +4,12 @@ const useUnmount = (callback: (...args: Array<any>) => any) => {
const ref = React.useRef(callback); const ref = React.useRef(callback);
ref.current = callback; ref.current = callback;
React.useEffect(() => { React.useEffect(
return () => { () => () => {
ref.current(); ref.current();
}; },
}, []); []
);
}; };
export default useUnmount; export default useUnmount;

View File

@@ -36,8 +36,8 @@ const AccountMenu: React.FC = ({ children }) => {
} }
}, [menu, theme, previousTheme]); }, [menu, theme, previousTheme]);
const actions = React.useMemo(() => { const actions = React.useMemo(
return [ () => [
openKeyboardShortcuts, openKeyboardShortcuts,
downloadApp, downloadApp,
openAPIDocumentation, openAPIDocumentation,
@@ -50,8 +50,9 @@ const AccountMenu: React.FC = ({ children }) => {
navigateToAccountPreferences, navigateToAccountPreferences,
separator(), separator(),
logout, logout,
]; ],
}, []); []
);
return ( return (
<> <>

52
app/menus/ApiKeyMenu.tsx Normal file
View File

@@ -0,0 +1,52 @@
import { observer } from "mobx-react";
import * as React from "react";
import { useTranslation } from "react-i18next";
import { useMenuState } from "reakit/Menu";
import ApiKey from "~/models/ApiKey";
import TokenRevokeDialog from "~/scenes/Settings/components/TokenRevokeDialog";
import ContextMenu from "~/components/ContextMenu";
import MenuItem from "~/components/ContextMenu/MenuItem";
import OverflowMenuButton from "~/components/ContextMenu/OverflowMenuButton";
import useStores from "~/hooks/useStores";
type Props = {
/** The apiKey to associate with the menu */
token: ApiKey;
/** CSS class name */
className?: string;
};
function ApiKeyMenu({ token, className }: Props) {
const menu = useMenuState({
modal: true,
});
const { dialogs } = useStores();
const { t } = useTranslation();
const handleRevoke = React.useCallback(() => {
dialogs.openModal({
title: t("Revoke token"),
isCentered: true,
content: (
<TokenRevokeDialog onSubmit={dialogs.closeAllModals} token={token} />
),
});
}, [t, dialogs, token]);
return (
<>
<OverflowMenuButton
aria-label={t("Show menu")}
className={className}
{...menu}
/>
<ContextMenu {...menu}>
<MenuItem {...menu} onClick={handleRevoke} dangerous>
{t("Revoke")}
</MenuItem>
</ContextMenu>
</>
);
}
export default observer(ApiKeyMenu);

View File

@@ -31,15 +31,16 @@ const OrganizationMenu: React.FC = ({ children }) => {
// NOTE: it's useful to memoize on the team id and session because the action // NOTE: it's useful to memoize on the team id and session because the action
// menu is not cached at all. // menu is not cached at all.
const actions = React.useMemo(() => { const actions = React.useMemo(
return [ () => [
...createTeamsList(context), ...createTeamsList(context),
createTeam, createTeam,
separator(), separator(),
navigateToSettings, navigateToSettings,
logout, logout,
]; ],
}, [context]); [context]
);
return ( return (
<> <>

View File

@@ -59,20 +59,17 @@ export default abstract class BaseModel {
}; };
updateFromJson = (data: any) => { updateFromJson = (data: any) => {
//const isNew = !data.id && !this.id && this.isNew; // const isNew = !data.id && !this.id && this.isNew;
set(this, { ...data, isNew: false }); set(this, { ...data, isNew: false });
this.persistedAttributes = this.toAPI(); this.persistedAttributes = this.toAPI();
}; };
fetch = (options?: any) => { fetch = (options?: any) => this.store.fetch(this.id, options);
return this.store.fetch(this.id, options);
};
refresh = () => { refresh = () =>
return this.fetch({ this.fetch({
force: true, force: true,
}); });
};
delete = async () => { delete = async () => {
this.isSaving = true; this.isSaving = true;

View File

@@ -209,19 +209,14 @@ export default class Collection extends ParanoidModel {
} }
@action @action
star = async () => { star = async () => this.store.star(this);
return this.store.star(this);
};
@action @action
unstar = async () => { unstar = async () => this.store.unstar(this);
return this.store.unstar(this);
};
export = (format: FileOperationFormat) => { export = (format: FileOperationFormat) =>
return client.post("/collections.export", { client.post("/collections.export", {
id: this.id, id: this.id,
format, format,
}); });
};
} }

View File

@@ -238,23 +238,17 @@ export default class Document extends ParanoidModel {
} }
@action @action
share = async () => { share = async () =>
return this.store.rootStore.shares.create({ this.store.rootStore.shares.create({
documentId: this.id, documentId: this.id,
}); });
};
archive = () => { archive = () => this.store.archive(this);
return this.store.archive(this);
};
restore = (options?: { revisionId?: string; collectionId?: string }) => { restore = (options?: { revisionId?: string; collectionId?: string }) =>
return this.store.restore(this, options); this.store.restore(this, options);
};
unpublish = () => { unpublish = () => this.store.unpublish(this);
return this.store.unpublish(this);
};
@action @action
enableEmbeds = () => { enableEmbeds = () => {
@@ -267,12 +261,11 @@ export default class Document extends ParanoidModel {
}; };
@action @action
pin = (collectionId?: string) => { pin = (collectionId?: string) =>
return this.store.rootStore.pins.create({ this.store.rootStore.pins.create({
documentId: this.id, documentId: this.id,
...(collectionId ? { collectionId } : {}), ...(collectionId ? { collectionId } : {}),
}); });
};
@action @action
unpin = (collectionId?: string) => { unpin = (collectionId?: string) => {
@@ -287,14 +280,10 @@ export default class Document extends ParanoidModel {
}; };
@action @action
star = () => { star = () => this.store.star(this);
return this.store.star(this);
};
@action @action
unstar = () => { unstar = () => this.store.unstar(this);
return this.store.unstar(this);
};
/** /**
* Subscribes the current user to this document. * Subscribes the current user to this document.
@@ -302,9 +291,7 @@ export default class Document extends ParanoidModel {
* @returns A promise that resolves when the subscription is created. * @returns A promise that resolves when the subscription is created.
*/ */
@action @action
subscribe = () => { subscribe = () => this.store.subscribe(this);
return this.store.subscribe(this);
};
/** /**
* Unsubscribes the current user to this document. * Unsubscribes the current user to this document.
@@ -312,9 +299,7 @@ export default class Document extends ParanoidModel {
* @returns A promise that resolves when the subscription is destroyed. * @returns A promise that resolves when the subscription is destroyed.
*/ */
@action @action
unsubscribe = (userId: string) => { unsubscribe = (userId: string) => this.store.unsubscribe(userId, this);
return this.store.unsubscribe(userId, this);
};
@action @action
view = () => { view = () => {
@@ -336,9 +321,7 @@ export default class Document extends ParanoidModel {
}; };
@action @action
templatize = () => { templatize = () => this.store.templatize(this.id);
return this.store.templatize(this.id);
};
@action @action
save = async (options?: SaveOptions | undefined) => { save = async (options?: SaveOptions | undefined) => {
@@ -359,13 +342,10 @@ export default class Document extends ParanoidModel {
} }
}; };
move = (collectionId: string, parentDocumentId?: string | undefined) => { move = (collectionId: string, parentDocumentId?: string | undefined) =>
return this.store.move(this.id, collectionId, parentDocumentId); this.store.move(this.id, collectionId, parentDocumentId);
};
duplicate = () => { duplicate = () => this.store.duplicate(this);
return this.store.duplicate(this);
};
getSummary = (paragraphs = 4) => { getSummary = (paragraphs = 4) => {
const result = this.text.trim().split("\n").slice(0, paragraphs).join("\n"); const result = this.text.trim().split("\n").slice(0, paragraphs).join("\n");
@@ -405,8 +385,8 @@ export default class Document extends ParanoidModel {
}; };
} }
download = (contentType: ExportContentType) => { download = (contentType: ExportContentType) =>
return client.post( client.post(
`/documents.export`, `/documents.export`,
{ {
id: this.id, id: this.id,
@@ -418,5 +398,4 @@ export default class Document extends ParanoidModel {
}, },
} }
); );
};
} }

View File

@@ -90,13 +90,8 @@ class User extends ParanoidModel {
* @param type The type of notification event * @param type The type of notification event
* @returns The current preference * @returns The current preference
*/ */
public subscribedToEventType = (type: NotificationEventType) => { public subscribedToEventType = (type: NotificationEventType) =>
return ( this.notificationSettings[type] ?? NotificationEventDefaults[type] ?? false;
this.notificationSettings[type] ??
NotificationEventDefaults[type] ??
false
);
};
/** /**
* Sets a preference for the users notification settings on the model and * Sets a preference for the users notification settings on the model and

View File

@@ -1,8 +1,7 @@
const fields = new Map(); const fields = new Map();
export const getFieldsForModel = (target: any) => { export const getFieldsForModel = (target: any) =>
return fields.get(target.constructor.name); fields.get(target.constructor.name);
};
/** /**
* A decorator that records this key as a serializable field on the model. * A decorator that records this key as a serializable field on the model.

View File

@@ -8,8 +8,6 @@ const extensions = withComments(basicExtensions);
const CommentEditor = ( const CommentEditor = (
props: EditorProps, props: EditorProps,
ref: React.RefObject<SharedEditor> ref: React.RefObject<SharedEditor>
) => { ) => <Editor extensions={extensions} {...props} ref={ref} />;
return <Editor extensions={extensions} {...props} ref={ref} />;
};
export default React.forwardRef(CommentEditor); export default React.forwardRef(CommentEditor);

View File

@@ -114,10 +114,9 @@ function CommentThread({
scrollMode: "if-needed", scrollMode: "if-needed",
behavior: "smooth", behavior: "smooth",
block: "start", block: "start",
boundary: (parent) => { boundary: (parent) =>
// Prevents body and other parent elements from being scrolled // Prevents body and other parent elements from being scrolled
return parent.id !== "comments"; parent.id !== "comments",
},
}); });
}, },
isVisible ? 0 : sidebarAppearDuration isVisible ? 0 : sidebarAppearDuration

View File

@@ -185,13 +185,14 @@ function MultiplayerEditor({ onSynced, ...props }: Props, ref: any) {
isMounted, isMounted,
]); ]);
const user = React.useMemo(() => { const user = React.useMemo(
return { () => ({
id: currentUser.id, id: currentUser.id,
name: currentUser.name, name: currentUser.name,
color: currentUser.color, color: currentUser.color,
}; }),
}, [currentUser.id, currentUser.color, currentUser.name]); [currentUser.id, currentUser.color, currentUser.name]
);
const extensions = React.useMemo(() => { const extensions = React.useMemo(() => {
if (!remoteProvider) { if (!remoteProvider) {

View File

@@ -49,13 +49,11 @@ const PublicBreadcrumb: React.FC<Props> = ({
() => () =>
pathToDocument(sharedTree, documentId) pathToDocument(sharedTree, documentId)
.slice(0, -1) .slice(0, -1)
.map((item) => { .map((item) => ({
return { ...item,
...item, type: "route",
type: "route", to: sharedDocumentPath(shareId, item.url),
to: sharedDocumentPath(shareId, item.url), })),
};
}),
[sharedTree, shareId, documentId] [sharedTree, shareId, documentId]
); );

View File

@@ -30,9 +30,7 @@ export default function DocumentScene(props: Props) {
setLastVisitedPath(currentPath); setLastVisitedPath(currentPath);
}, [currentPath, setLastVisitedPath]); }, [currentPath, setLastVisitedPath]);
React.useEffect(() => { React.useEffect(() => () => ui.clearActiveDocument(), [ui]);
return () => ui.clearActiveDocument();
}, [ui]);
// the urlId portion of the url does not include the slugified title // the urlId portion of the url does not include the slugified title
// we only want to force a re-mount of the document component when the // we only want to force a re-mount of the document component when the

View File

@@ -1,6 +1,7 @@
import { WarningIcon } from "outline-icons";
import * as React from "react"; import * as React from "react";
import { Trans } from "react-i18next"; import { Trans } from "react-i18next";
import NoticeAlert from "~/components/NoticeAlert"; import Notice from "~/components/Notice";
import useQuery from "~/hooks/useQuery"; import useQuery from "~/hooks/useQuery";
export default function Notices() { export default function Notices() {
@@ -13,7 +14,7 @@ export default function Notices() {
} }
return ( return (
<NoticeAlert> <Notice icon={<WarningIcon color="currentcolor" />}>
{notice === "domain-required" && ( {notice === "domain-required" && (
<Trans> <Trans>
Unable to sign-in. Please navigate to your team's custom URL, then try Unable to sign-in. Please navigate to your team's custom URL, then try
@@ -103,6 +104,6 @@ export default function Notices() {
team domain. team domain.
</Trans> </Trans>
)} )}
</NoticeAlert> </Notice>
); );
} }

View File

@@ -12,7 +12,6 @@ import Heading from "~/components/Heading";
import Modal from "~/components/Modal"; import Modal from "~/components/Modal";
import PaginatedList from "~/components/PaginatedList"; import PaginatedList from "~/components/PaginatedList";
import Scene from "~/components/Scene"; import Scene from "~/components/Scene";
import Subheading from "~/components/Subheading";
import Text from "~/components/Text"; import Text from "~/components/Text";
import useBoolean from "~/hooks/useBoolean"; import useBoolean from "~/hooks/useBoolean";
import useCurrentTeam from "~/hooks/useCurrentTeam"; import useCurrentTeam from "~/hooks/useCurrentTeam";
@@ -57,11 +56,15 @@ function Groups() {
Groups can be used to organize and manage the people on your team. Groups can be used to organize and manage the people on your team.
</Trans> </Trans>
</Text> </Text>
<Subheading>{t("All groups")}</Subheading>
<PaginatedList <PaginatedList
items={groups.orderedData} items={groups.orderedData}
empty={<Empty>{t("No groups have been created yet")}</Empty>} empty={<Empty>{t("No groups have been created yet")}</Empty>}
fetch={groups.fetchPage} fetch={groups.fetchPage}
heading={
<h2>
<Trans>All</Trans>
</h2>
}
renderItem={(item: Group) => ( renderItem={(item: Group) => (
<GroupListItem <GroupListItem
key={item.id} key={item.id}

View File

@@ -1,9 +1,20 @@
import { debounce } from "lodash"; import { debounce } from "lodash";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { EmailIcon } from "outline-icons"; import {
AcademicCapIcon,
CheckboxIcon,
CollectionIcon,
CommentIcon,
EditIcon,
EmailIcon,
PublishIcon,
StarredIcon,
UserIcon,
} from "outline-icons";
import * as React from "react"; import * as React from "react";
import { useTranslation, Trans } from "react-i18next"; import { useTranslation, Trans } from "react-i18next";
import { NotificationEventType } from "@shared/types"; import { NotificationEventType } from "@shared/types";
import Flex from "~/components/Flex";
import Heading from "~/components/Heading"; import Heading from "~/components/Heading";
import Input from "~/components/Input"; import Input from "~/components/Input";
import Notice from "~/components/Notice"; import Notice from "~/components/Notice";
@@ -24,6 +35,7 @@ function Notifications() {
const options = [ const options = [
{ {
event: NotificationEventType.PublishDocument, event: NotificationEventType.PublishDocument,
icon: <PublishIcon color="currentColor" />,
title: t("Document published"), title: t("Document published"),
description: t( description: t(
"Receive a notification whenever a new document is published" "Receive a notification whenever a new document is published"
@@ -31,6 +43,7 @@ function Notifications() {
}, },
{ {
event: NotificationEventType.UpdateDocument, event: NotificationEventType.UpdateDocument,
icon: <EditIcon color="currentColor" />,
title: t("Document updated"), title: t("Document updated"),
description: t( description: t(
"Receive a notification when a document you are subscribed to is edited" "Receive a notification when a document you are subscribed to is edited"
@@ -38,6 +51,7 @@ function Notifications() {
}, },
{ {
event: NotificationEventType.CreateComment, event: NotificationEventType.CreateComment,
icon: <CommentIcon color="currentColor" />,
title: t("Comment posted"), title: t("Comment posted"),
description: t( description: t(
"Receive a notification when a document you are subscribed to or a thread you participated in receives a comment" "Receive a notification when a document you are subscribed to or a thread you participated in receives a comment"
@@ -45,6 +59,7 @@ function Notifications() {
}, },
{ {
event: NotificationEventType.Mentioned, event: NotificationEventType.Mentioned,
icon: <EmailIcon color="currentColor" />,
title: t("Mentioned"), title: t("Mentioned"),
description: t( description: t(
"Receive a notification when someone mentions you in a document or comment" "Receive a notification when someone mentions you in a document or comment"
@@ -52,6 +67,7 @@ function Notifications() {
}, },
{ {
event: NotificationEventType.CreateCollection, event: NotificationEventType.CreateCollection,
icon: <CollectionIcon color="currentColor" />,
title: t("Collection created"), title: t("Collection created"),
description: t( description: t(
"Receive a notification whenever a new collection is created" "Receive a notification whenever a new collection is created"
@@ -59,6 +75,7 @@ function Notifications() {
}, },
{ {
event: NotificationEventType.InviteAccepted, event: NotificationEventType.InviteAccepted,
icon: <UserIcon color="currentColor" />,
title: t("Invite accepted"), title: t("Invite accepted"),
description: t( description: t(
"Receive a notification when someone you invited creates an account" "Receive a notification when someone you invited creates an account"
@@ -66,6 +83,7 @@ function Notifications() {
}, },
{ {
event: NotificationEventType.ExportCompleted, event: NotificationEventType.ExportCompleted,
icon: <CheckboxIcon checked color="currentColor" />,
title: t("Export completed"), title: t("Export completed"),
description: t( description: t(
"Receive a notification when an export you requested has been completed" "Receive a notification when an export you requested has been completed"
@@ -73,12 +91,14 @@ function Notifications() {
}, },
{ {
visible: isCloudHosted, visible: isCloudHosted,
icon: <AcademicCapIcon color="currentColor" />,
event: NotificationEventType.Onboarding, event: NotificationEventType.Onboarding,
title: t("Getting started"), title: t("Getting started"),
description: t("Tips on getting started with features and functionality"), description: t("Tips on getting started with features and functionality"),
}, },
{ {
visible: isCloudHosted, visible: isCloudHosted,
icon: <StarredIcon color="currentColor" />,
event: NotificationEventType.Features, event: NotificationEventType.Features,
title: t("New features"), title: t("New features"),
description: t("Receive an email when new features of note are added"), description: t("Receive an email when new features of note are added"),
@@ -138,7 +158,11 @@ function Notifications() {
return ( return (
<SettingRow <SettingRow
visible={option.visible} visible={option.visible}
label={option.title} label={
<Flex align="center" gap={4}>
{option.icon} {option.title}
</Flex>
}
name={option.event} name={option.event}
description={option.description} description={option.description}
> >

View File

@@ -10,7 +10,6 @@ import Heading from "~/components/Heading";
import Modal from "~/components/Modal"; import Modal from "~/components/Modal";
import PaginatedList from "~/components/PaginatedList"; import PaginatedList from "~/components/PaginatedList";
import Scene from "~/components/Scene"; import Scene from "~/components/Scene";
import Subheading from "~/components/Subheading";
import Text from "~/components/Text"; import Text from "~/components/Text";
import useBoolean from "~/hooks/useBoolean"; import useBoolean from "~/hooks/useBoolean";
import useCurrentTeam from "~/hooks/useCurrentTeam"; import useCurrentTeam from "~/hooks/useCurrentTeam";
@@ -59,7 +58,7 @@ function Tokens() {
<PaginatedList <PaginatedList
fetch={apiKeys.fetchPage} fetch={apiKeys.fetchPage}
items={apiKeys.orderedData} items={apiKeys.orderedData}
heading={<Subheading sticky>{t("Tokens")}</Subheading>} heading={<h2>{t("Active")}</h2>}
renderItem={(token: ApiKey) => ( renderItem={(token: ApiKey) => (
<TokenListItem key={token.id} token={token} /> <TokenListItem key={token.id} token={token} />
)} )}

View File

@@ -21,7 +21,7 @@ function Zapier() {
type="module" type="module"
src="https://cdn.zapier.com/packages/partner-sdk/v0/zapier-elements/zapier-elements.esm.js" src="https://cdn.zapier.com/packages/partner-sdk/v0/zapier-elements/zapier-elements.esm.js"
key="zapier-js" key="zapier-js"
></script> />
<link <link
rel="stylesheet" rel="stylesheet"
href="https://cdn.zapier.com/packages/partner-sdk/v0/zapier-elements/zapier-elements.css" href="https://cdn.zapier.com/packages/partner-sdk/v0/zapier-elements/zapier-elements.css"

View File

@@ -4,10 +4,10 @@ import { useTranslation } from "react-i18next";
import ApiKey from "~/models/ApiKey"; import ApiKey from "~/models/ApiKey";
import Button from "~/components/Button"; import Button from "~/components/Button";
import CopyToClipboard from "~/components/CopyToClipboard"; import CopyToClipboard from "~/components/CopyToClipboard";
import Flex from "~/components/Flex";
import ListItem from "~/components/List/Item"; import ListItem from "~/components/List/Item";
import useStores from "~/hooks/useStores";
import useToasts from "~/hooks/useToasts"; import useToasts from "~/hooks/useToasts";
import TokenRevokeDialog from "./TokenRevokeDialog"; import ApiKeyMenu from "~/menus/ApiKeyMenu";
type Props = { type Props = {
token: ApiKey; token: ApiKey;
@@ -16,7 +16,6 @@ type Props = {
const TokenListItem = ({ token }: Props) => { const TokenListItem = ({ token }: Props) => {
const { t } = useTranslation(); const { t } = useTranslation();
const { showToast } = useToasts(); const { showToast } = useToasts();
const { dialogs } = useStores();
const [linkCopied, setLinkCopied] = React.useState<boolean>(false); const [linkCopied, setLinkCopied] = React.useState<boolean>(false);
React.useEffect(() => { React.useEffect(() => {
@@ -34,32 +33,20 @@ const TokenListItem = ({ token }: Props) => {
}); });
}, [showToast, t]); }, [showToast, t]);
const showRevokeConfirmation = React.useCallback(() => {
dialogs.openModal({
title: t("Revoke token"),
isCentered: true,
content: (
<TokenRevokeDialog onSubmit={dialogs.closeAllModals} token={token} />
),
});
}, [t, dialogs, token]);
return ( return (
<ListItem <ListItem
key={token.id} key={token.id}
title={token.name} title={token.name}
subtitle={<code>{token.secret}</code>} subtitle={<code>{token.secret.slice(0, 15)}</code>}
actions={ actions={
<> <Flex align="center" gap={8}>
<CopyToClipboard text={token.secret} onCopy={handleCopy}> <CopyToClipboard text={token.secret} onCopy={handleCopy}>
<Button type="button" icon={<CopyIcon />} neutral borderOnHover> <Button type="button" icon={<CopyIcon />} neutral borderOnHover>
{linkCopied ? t("Copied") : t("Copy")} {linkCopied ? t("Copied") : t("Copy")}
</Button> </Button>
</CopyToClipboard> </CopyToClipboard>
<Button onClick={showRevokeConfirmation} neutral> <ApiKeyMenu token={token} />
Revoke </Flex>
</Button>
</>
} }
/> />
); );

View File

@@ -1,110 +0,0 @@
import { formatDistanceToNow } from "date-fns";
import { observer } from "mobx-react";
import { EditIcon } from "outline-icons";
import * as React from "react";
import { useTranslation } from "react-i18next";
import { useHistory } from "react-router-dom";
import styled from "styled-components";
import User from "~/models/User";
import Avatar from "~/components/Avatar";
import Badge from "~/components/Badge";
import Button from "~/components/Button";
import Flex from "~/components/Flex";
import Modal from "~/components/Modal";
import PaginatedDocumentList from "~/components/PaginatedDocumentList";
import Subheading from "~/components/Subheading";
import Text from "~/components/Text";
import useCurrentUser from "~/hooks/useCurrentUser";
import useStores from "~/hooks/useStores";
import { settingsPath } from "~/utils/routeHelpers";
type Props = {
user: User;
onRequestClose: () => void;
isOpen: boolean;
};
function UserProfile(props: Props) {
const { t } = useTranslation();
const { documents } = useStores();
const currentUser = useCurrentUser();
const history = useHistory();
const { user, ...rest } = props;
if (!user) {
return null;
}
const isCurrentUser = currentUser.id === user.id;
return (
<Modal
title={
<Flex align="center">
<Avatar model={user} size={38} alt={t("Profile picture")} />
<span>&nbsp;{user.name}</span>
</Flex>
}
{...rest}
>
<Flex column>
<Meta>
{isCurrentUser
? t("You joined")
: user.lastActiveAt
? t("Joined")
: t("Invited")}{" "}
{t("{{ time }} ago.", {
time: formatDistanceToNow(Date.parse(user.createdAt)),
})}
{user.isAdmin && (
<StyledBadge primary={user.isAdmin}>{t("Admin")}</StyledBadge>
)}
{user.isSuspended && <StyledBadge>{t("Suspended")}</StyledBadge>}
{isCurrentUser && (
<Edit>
<Button
onClick={() => history.push(settingsPath())}
icon={<EditIcon />}
neutral
>
{t("Edit Profile")}
</Button>
</Edit>
)}
</Meta>
<PaginatedDocumentList
documents={documents.createdByUser(user.id)}
fetch={documents.fetchOwned}
options={{
user: user.id,
}}
heading={<Subheading>{t("Recently updated")}</Subheading>}
empty={
<Text type="secondary">
{t("{{ userName }} hasnt updated any documents yet.", {
userName: user.name,
})}
</Text>
}
showCollection
/>
</Flex>
</Modal>
);
}
const Edit = styled.span`
position: absolute;
top: 46px;
right: 0;
`;
const StyledBadge = styled(Badge)`
position: relative;
top: -2px;
`;
const Meta = styled(Text)`
margin-top: -12px;
`;
export default observer(UserProfile);

View File

@@ -223,9 +223,7 @@ export default class AuthStore {
}; };
@action @action
requestDelete = () => { requestDelete = () => client.post(`/users.requestDelete`);
return client.post(`/users.requestDelete`);
};
@action @action
deleteUser = async (data: { code: string }) => { deleteUser = async (data: { code: string }) => {
@@ -350,5 +348,6 @@ export default class AuthStore {
// Tell the host application we logged out, if any allows window cleanup. // Tell the host application we logged out, if any allows window cleanup.
Desktop.bridge?.onLogout?.(); Desktop.bridge?.onLogout?.();
this.rootStore.logout();
}; };
} }

View File

@@ -218,9 +218,8 @@ export default class CollectionsStore extends BaseStore<Collection> {
this.rootStore.documents.fetchRecentlyViewed(); this.rootStore.documents.fetchRecentlyViewed();
}; };
export = (format: FileOperationFormat) => { export = (format: FileOperationFormat) =>
return client.post("/collections.export_all", { client.post("/collections.export_all", {
format, format,
}); });
};
} }

View File

@@ -303,81 +303,66 @@ export default class DocumentsStore extends BaseStore<Document> {
}; };
@action @action
fetchArchived = async (options?: PaginationParams): Promise<Document[]> => { fetchArchived = async (options?: PaginationParams): Promise<Document[]> =>
return this.fetchNamedPage("archived", options); this.fetchNamedPage("archived", options);
};
@action @action
fetchDeleted = async (options?: PaginationParams): Promise<Document[]> => { fetchDeleted = async (options?: PaginationParams): Promise<Document[]> =>
return this.fetchNamedPage("deleted", options); this.fetchNamedPage("deleted", options);
};
@action @action
fetchRecentlyUpdated = async ( fetchRecentlyUpdated = async (
options?: PaginationParams options?: PaginationParams
): Promise<Document[]> => { ): Promise<Document[]> => this.fetchNamedPage("list", options);
return this.fetchNamedPage("list", options);
};
@action @action
fetchTemplates = async (options?: PaginationParams): Promise<Document[]> => { fetchTemplates = async (options?: PaginationParams): Promise<Document[]> =>
return this.fetchNamedPage("list", { ...options, template: true }); this.fetchNamedPage("list", { ...options, template: true });
};
@action @action
fetchAlphabetical = async ( fetchAlphabetical = async (options?: PaginationParams): Promise<Document[]> =>
options?: PaginationParams this.fetchNamedPage("list", {
): Promise<Document[]> => {
return this.fetchNamedPage("list", {
sort: "title", sort: "title",
direction: "ASC", direction: "ASC",
...options, ...options,
}); });
};
@action @action
fetchLeastRecentlyUpdated = async ( fetchLeastRecentlyUpdated = async (
options?: PaginationParams options?: PaginationParams
): Promise<Document[]> => { ): Promise<Document[]> =>
return this.fetchNamedPage("list", { this.fetchNamedPage("list", {
sort: "updatedAt", sort: "updatedAt",
direction: "ASC", direction: "ASC",
...options, ...options,
}); });
};
@action @action
fetchRecentlyPublished = async ( fetchRecentlyPublished = async (
options?: PaginationParams options?: PaginationParams
): Promise<Document[]> => { ): Promise<Document[]> =>
return this.fetchNamedPage("list", { this.fetchNamedPage("list", {
sort: "publishedAt", sort: "publishedAt",
direction: "DESC", direction: "DESC",
...options, ...options,
}); });
};
@action @action
fetchRecentlyViewed = async ( fetchRecentlyViewed = async (
options?: PaginationParams options?: PaginationParams
): Promise<Document[]> => { ): Promise<Document[]> => this.fetchNamedPage("viewed", options);
return this.fetchNamedPage("viewed", options);
};
@action @action
fetchStarred = (options?: PaginationParams): Promise<Document[]> => { fetchStarred = (options?: PaginationParams): Promise<Document[]> =>
return this.fetchNamedPage("starred", options); this.fetchNamedPage("starred", options);
};
@action @action
fetchDrafts = (options?: PaginationParams): Promise<Document[]> => { fetchDrafts = (options?: PaginationParams): Promise<Document[]> =>
return this.fetchNamedPage("drafts", options); this.fetchNamedPage("drafts", options);
};
@action @action
fetchOwned = (options?: PaginationParams): Promise<Document[]> => { fetchOwned = (options?: PaginationParams): Promise<Document[]> =>
return this.fetchNamedPage("list", options); this.fetchNamedPage("list", options);
};
@action @action
searchTitles = async (query: string, options?: SearchParams) => { searchTitles = async (query: string, options?: SearchParams) => {
@@ -778,11 +763,10 @@ export default class DocumentsStore extends BaseStore<Document> {
}); });
}; };
star = (document: Document) => { star = (document: Document) =>
return this.rootStore.stars.create({ this.rootStore.stars.create({
documentId: document.id, documentId: document.id,
}); });
};
unstar = (document: Document) => { unstar = (document: Document) => {
const star = this.rootStore.stars.orderedData.find( const star = this.rootStore.stars.orderedData.find(
@@ -791,12 +775,11 @@ export default class DocumentsStore extends BaseStore<Document> {
return star?.delete(); return star?.delete();
}; };
subscribe = (document: Document) => { subscribe = (document: Document) =>
return this.rootStore.subscriptions.create({ this.rootStore.subscriptions.create({
documentId: document.id, documentId: document.id,
event: "documents.update", event: "documents.update",
}); });
};
unsubscribe = (userId: string, document: Document) => { unsubscribe = (userId: string, document: Document) => {
const subscription = this.rootStore.subscriptions.orderedData.find( const subscription = this.rootStore.subscriptions.orderedData.find(
@@ -808,9 +791,8 @@ export default class DocumentsStore extends BaseStore<Document> {
return subscription?.delete(); return subscription?.delete();
}; };
getByUrl = (url = ""): Document | undefined => { getByUrl = (url = ""): Document | undefined =>
return find(this.orderedData, (doc) => url.endsWith(doc.urlId)); find(this.orderedData, (doc) => url.endsWith(doc.urlId));
};
getCollectionForDocument(document: Document) { getCollectionForDocument(document: Document) {
return this.rootStore.collections.data.get(document.collectionId); return this.rootStore.collections.data.get(document.collectionId);

View File

@@ -73,7 +73,6 @@ export default class GroupMembershipsStore extends BaseStore<GroupMembership> {
}); });
}; };
inGroup = (groupId: string) => { inGroup = (groupId: string) =>
return filter(this.orderedData, (member) => member.groupId === groupId); filter(this.orderedData, (member) => member.groupId === groupId);
};
} }

View File

@@ -35,11 +35,10 @@ export default class PinsStore extends BaseStore<Pin> {
} }
}; };
inCollection = (collectionId: string) => { inCollection = (collectionId: string) =>
return computed(() => this.orderedData) computed(() => this.orderedData)
.get() .get()
.filter((pin) => pin.collectionId === collectionId); .filter((pin) => pin.collectionId === collectionId);
};
@computed @computed
get home() { get home() {

View File

@@ -87,30 +87,10 @@ export default class RootStore {
} }
logout() { logout() {
this.apiKeys.clear(); Object.getOwnPropertyNames(this)
this.authenticationProviders.clear(); .filter((key) => ["auth", "ui"].includes(key) === false)
// this.auth omitted for reasons... .forEach((key) => {
this.collections.clear(); this[key]?.clear?.();
this.collectionGroupMemberships.clear(); });
this.comments.clear();
this.documents.clear();
this.events.clear();
this.groups.clear();
this.groupMemberships.clear();
this.integrations.clear();
this.memberships.clear();
this.presence.clear();
this.pins.clear();
this.policies.clear();
this.revisions.clear();
this.searches.clear();
this.shares.clear();
this.stars.clear();
this.subscriptions.clear();
this.fileOperations.clear();
// this.ui omitted to keep ui settings between sessions
this.users.clear();
this.views.clear();
this.webhookSubscriptions.clear();
} }
} }

View File

@@ -99,7 +99,6 @@ export default class SharesStore extends BaseStore<Share> {
return undefined; return undefined;
}; };
getByDocumentId = (documentId: string): Share | null | undefined => { getByDocumentId = (documentId: string): Share | null | undefined =>
return find(this.orderedData, (share) => share.documentId === documentId); find(this.orderedData, (share) => share.documentId === documentId);
};
} }

View File

@@ -143,11 +143,10 @@ export default class UsersStore extends BaseStore<User> {
}; };
@action @action
resendInvite = async (user: User) => { resendInvite = async (user: User) =>
return client.post(`/users.resendInvite`, { client.post(`/users.resendInvite`, {
id: user.id, id: user.id,
}); });
};
@action @action
fetchCounts = async (teamId: string): Promise<any> => { fetchCounts = async (teamId: string): Promise<any> => {

View File

@@ -1,3 +1,4 @@
import { computed } from "mobx";
import WebhookSubscription from "~/models/WebhookSubscription"; import WebhookSubscription from "~/models/WebhookSubscription";
import BaseStore, { RPCAction } from "./BaseStore"; import BaseStore, { RPCAction } from "./BaseStore";
import RootStore from "./RootStore"; import RootStore from "./RootStore";
@@ -15,4 +16,14 @@ export default class WebhookSubscriptionsStore extends BaseStore<
constructor(rootStore: RootStore) { constructor(rootStore: RootStore) {
super(rootStore, WebhookSubscription); super(rootStore, WebhookSubscription);
} }
@computed
get enabled() {
return this.orderedData.filter((subscription) => subscription.enabled);
}
@computed
get disabled() {
return this.orderedData.filter((subscription) => !subscription.enabled);
}
} }

View File

@@ -219,17 +219,13 @@ class ApiClient {
path: string, path: string,
data: Record<string, any> | undefined, data: Record<string, any> | undefined,
options?: FetchOptions options?: FetchOptions
) => { ) => this.fetch(path, "GET", data, options);
return this.fetch(path, "GET", data, options);
};
post = ( post = (
path: string, path: string,
data?: Record<string, any> | undefined, data?: Record<string, any> | undefined,
options?: FetchOptions options?: FetchOptions
) => { ) => this.fetch(path, "POST", data, options);
return this.fetch(path, "POST", data, options);
};
} }
export const client = new ApiClient(); export const client = new ApiClient();

View File

@@ -8,8 +8,7 @@ type Options = {
export const compressImage = async ( export const compressImage = async (
file: File | Blob, file: File | Blob,
options?: Options options?: Options
): Promise<Blob> => { ): Promise<Blob> =>
return new Promise((resolve, reject) => { new Promise((resolve, reject) => {
new Compressor(file, { ...options, success: resolve, error: reject }); new Compressor(file, { ...options, success: resolve, error: reject });
}); });
};

View File

@@ -43,7 +43,7 @@ export default function download(
return saver(x); // everyone else can save dataURLs un-processed return saver(x); // everyone else can save dataURLs un-processed
} }
//end if dataURL passed? // end if dataURL passed?
try { try {
blob = blob =
x instanceof B x instanceof B
@@ -81,7 +81,7 @@ export default function download(
return true; return true;
} }
//do iframe dataURL download (old ch+FF): // do iframe dataURL download (old ch+FF):
const f = D.createElement("iframe"); const f = D.createElement("iframe");
D.body && D.body.appendChild(f); D.body && D.body.appendChild(f);

View File

@@ -101,8 +101,8 @@ function Slack() {
"links:read", "links:read",
"links:write", "links:write",
// TODO: Wait forever for Slack to approve these scopes. // TODO: Wait forever for Slack to approve these scopes.
//"users:read", // "users:read",
//"users:read.email", // "users:read.email",
]} ]}
redirectUri={`${env.URL}/auth/slack.commands`} redirectUri={`${env.URL}/auth/slack.commands`}
state={team.id} state={team.id}

View File

@@ -9,7 +9,6 @@ import Heading from "~/components/Heading";
import Modal from "~/components/Modal"; import Modal from "~/components/Modal";
import PaginatedList from "~/components/PaginatedList"; import PaginatedList from "~/components/PaginatedList";
import Scene from "~/components/Scene"; import Scene from "~/components/Scene";
import Subheading from "~/components/Subheading";
import Text from "~/components/Text"; import Text from "~/components/Text";
import env from "~/env"; import env from "~/env";
import useBoolean from "~/hooks/useBoolean"; import useBoolean from "~/hooks/useBoolean";
@@ -55,8 +54,15 @@ function Webhooks() {
</Text> </Text>
<PaginatedList <PaginatedList
fetch={webhookSubscriptions.fetchPage} fetch={webhookSubscriptions.fetchPage}
items={webhookSubscriptions.orderedData} items={webhookSubscriptions.enabled}
heading={<Subheading sticky>{t("Webhooks")}</Subheading>} heading={<h2>{t("Active")}</h2>}
renderItem={(webhook: WebhookSubscription) => (
<WebhookSubscriptionListItem key={webhook.id} webhook={webhook} />
)}
/>
<PaginatedList
items={webhookSubscriptions.disabled}
heading={<h2>{t("Inactive")}</h2>}
renderItem={(webhook: WebhookSubscription) => ( renderItem={(webhook: WebhookSubscription) => (
<WebhookSubscriptionListItem key={webhook.id} webhook={webhook} /> <WebhookSubscriptionListItem key={webhook.id} webhook={webhook} />
)} )}

View File

@@ -45,7 +45,7 @@ const WebhookSubscriptionListItem = ({ webhook }: Props) => {
<> <>
{webhook.name} {webhook.name}
{!webhook.enabled && ( {!webhook.enabled && (
<StyledBadge yellow={true}>{t("Disabled")}</StyledBadge> <StyledBadge yellow>{t("Disabled")}</StyledBadge>
)} )}
</> </>
} }

View File

@@ -106,6 +106,10 @@ router.post(
ip: ctx.request.ip, ip: ctx.request.ip,
}; };
await Event.create(event); await Event.create(event);
ctx.body = {
success: true,
};
} }
); );

View File

@@ -106,8 +106,8 @@ async function teamProvisioner({
} }
// We cannot find an existing team, so we create a new one // We cannot find an existing team, so we create a new one
const team = await sequelize.transaction((transaction) => { const team = await sequelize.transaction((transaction) =>
return teamCreator({ teamCreator({
name, name,
domain, domain,
subdomain, subdomain,
@@ -115,8 +115,8 @@ async function teamProvisioner({
authenticationProviders: [authenticationProvider], authenticationProviders: [authenticationProvider],
ip, ip,
transaction, transaction,
}); })
}); );
return { return {
team, team,

View File

@@ -115,9 +115,10 @@ const teamUpdater = async ({ params, user, team, ip }: TeamUpdaterProps) => {
transaction, transaction,
}); });
if (changes) { if (changes) {
const data = changes.reduce((acc, curr) => { const data = changes.reduce(
return { ...acc, [curr]: team[curr] }; (acc, curr) => ({ ...acc, [curr]: team[curr] }),
}, {}); {}
);
await Event.create( await Event.create(
{ {

View File

@@ -5,26 +5,24 @@ import EmptySpace from "./EmptySpace";
const url = env.CDN_URL ?? env.URL; const url = env.CDN_URL ?? env.URL;
export default () => { export default () => (
return ( <Table width="100%">
<Table width="100%"> <TBody>
<TBody> <TR>
<TR> <TD>
<TD> <EmptySpace height={40} />
<EmptySpace height={40} /> <img
<img alt={env.APP_NAME}
alt={env.APP_NAME} src={
src={ env.isCloudHosted()
env.isCloudHosted() ? `${url}/email/header-logo.png`
? `${url}/email/header-logo.png` : "cid:header-image"
: "cid:header-image" }
} height="48"
height="48" width="48"
width="48" />
/> </TD>
</TD> </TR>
</TR> </TBody>
</TBody> </Table>
</Table> );
);
};

View File

@@ -139,22 +139,25 @@ async function start(id: number, disconnect: () => void) {
server.listen(normalizedPortFlag || env.PORT || "3000"); server.listen(normalizedPortFlag || env.PORT || "3000");
server.setTimeout(env.REQUEST_TIMEOUT); server.setTimeout(env.REQUEST_TIMEOUT);
ShutdownHelper.add("server", ShutdownOrder.last, () => { ShutdownHelper.add(
return new Promise((resolve, reject) => { "server",
// Calling stop prevents new connections from being accepted and waits for ShutdownOrder.last,
// existing connections to close for the grace period before forcefully () =>
// closing them. new Promise((resolve, reject) => {
server.stop((err, gracefully) => { // Calling stop prevents new connections from being accepted and waits for
disconnect(); // existing connections to close for the grace period before forcefully
// closing them.
server.stop((err, gracefully) => {
disconnect();
if (err) { if (err) {
reject(err); reject(err);
} else { } else {
resolve(gracefully); resolve(gracefully);
} }
}); });
}); })
}); );
// Handle shutdown signals // Handle shutdown signals
process.once("SIGTERM", () => ShutdownHelper.execute()); process.once("SIGTERM", () => ShutdownHelper.execute());

View File

@@ -126,9 +126,9 @@ class Logger {
} }
if (request) { if (request) {
scope.addEventProcessor((event) => { scope.addEventProcessor((event) =>
return Sentry.Handlers.parseRequest(event, request); Sentry.Handlers.parseRequest(event, request)
}); );
} }
Sentry.captureException(error); Sentry.captureException(error);

View File

@@ -123,14 +123,13 @@ class AuthenticationProvider extends Model {
} }
}; };
enable = (options?: SaveOptions<AuthenticationProvider>) => { enable = (options?: SaveOptions<AuthenticationProvider>) =>
return this.update( this.update(
{ {
enabled: true, enabled: true,
}, },
options options
); );
};
} }
export default AuthenticationProvider; export default AuthenticationProvider;

View File

@@ -477,12 +477,10 @@ class Collection extends ParanoidModel {
id: string id: string
) => { ) => {
children = await Promise.all( children = await Promise.all(
children.map(async (childDocument) => { children.map(async (childDocument) => ({
return { ...childDocument,
...childDocument, children: await removeFromChildren(childDocument.children, id),
children: await removeFromChildren(childDocument.children, id), }))
};
})
); );
const match = find(children, { const match = find(children, {
id, id,
@@ -562,8 +560,8 @@ class Collection extends ParanoidModel {
const { id } = updatedDocument; const { id } = updatedDocument;
const updateChildren = (documents: NavigationNode[]) => { const updateChildren = (documents: NavigationNode[]) =>
return Promise.all( Promise.all(
documents.map(async (document) => { documents.map(async (document) => {
if (document.id === id) { if (document.id === id) {
document = { document = {
@@ -577,7 +575,6 @@ class Collection extends ParanoidModel {
return document; return document;
}) })
); );
};
this.documentStructure = await updateChildren(this.documentStructure); this.documentStructure = await updateChildren(this.documentStructure);
// Sequelize doesn't seem to set the value with splice on JSONB field // Sequelize doesn't seem to set the value with splice on JSONB field
@@ -619,8 +616,8 @@ class Collection extends ParanoidModel {
); );
} else { } else {
// Recursively place document // Recursively place document
const placeDocument = (documentList: NavigationNode[]) => { const placeDocument = (documentList: NavigationNode[]) =>
return documentList.map((childDocument) => { documentList.map((childDocument) => {
if (document.parentDocumentId === childDocument.id) { if (document.parentDocumentId === childDocument.id) {
childDocument.children.splice( childDocument.children.splice(
index !== undefined ? index : childDocument.children.length, index !== undefined ? index : childDocument.children.length,
@@ -633,7 +630,6 @@ class Collection extends ParanoidModel {
return childDocument; return childDocument;
}); });
};
this.documentStructure = placeDocument(this.documentStructure); this.documentStructure = placeDocument(this.documentStructure);
} }

View File

@@ -668,8 +668,8 @@ class Document extends ParanoidModel {
}; };
// Delete a document, archived or otherwise. // Delete a document, archived or otherwise.
delete = (userId: string) => { delete = (userId: string) =>
return this.sequelize.transaction(async (transaction: Transaction) => { this.sequelize.transaction(async (transaction: Transaction) => {
if (!this.archivedAt && !this.template && this.collectionId) { if (!this.archivedAt && !this.template && this.collectionId) {
// delete any children and remove from the document structure // delete any children and remove from the document structure
const collection = await Collection.findByPk(this.collectionId, { const collection = await Collection.findByPk(this.collectionId, {
@@ -699,11 +699,8 @@ class Document extends ParanoidModel {
); );
return this; return this;
}); });
};
getTimestamp = () => { getTimestamp = () => Math.round(new Date(this.updatedAt).getTime() / 1000);
return Math.round(new Date(this.updatedAt).getTime() / 1000);
};
getSummary = () => { getSummary = () => {
const plainText = DocumentHelper.toPlainText(this); const plainText = DocumentHelper.toPlainText(this);

View File

@@ -34,39 +34,31 @@ import Fix from "./decorators/Fix";
], ],
})) }))
@Scopes(() => ({ @Scopes(() => ({
withCollectionPermissions: (userId: string) => { withCollectionPermissions: (userId: string) => ({
return { include: [
include: [ {
{ model: Document.scope("withDrafts"),
model: Document.scope("withDrafts"), paranoid: true,
paranoid: true, as: "document",
as: "document", include: [
include: [ {
{ attributes: ["id", "permission", "sharing", "teamId", "deletedAt"],
attributes: [ model: Collection.scope({
"id", method: ["withMembership", userId],
"permission", }),
"sharing", as: "collection",
"teamId", },
"deletedAt", ],
], },
model: Collection.scope({ {
method: ["withMembership", userId], association: "user",
}), paranoid: false,
as: "collection", },
}, {
], association: "team",
}, },
{ ],
association: "user", }),
paranoid: false,
},
{
association: "team",
},
],
};
},
})) }))
@Table({ tableName: "shares", modelName: "share" }) @Table({ tableName: "shares", modelName: "share" })
@Fix @Fix

View File

@@ -202,9 +202,8 @@ class Team extends ParanoidModel {
* @param fallback An optional fallback value, defaults to false. * @param fallback An optional fallback value, defaults to false.
* @returns The preference value if set, else undefined * @returns The preference value if set, else undefined
*/ */
public getPreference = (preference: TeamPreference, fallback = false) => { public getPreference = (preference: TeamPreference, fallback = false) =>
return this.preferences?.[preference] ?? fallback; this.preferences?.[preference] ?? fallback;
};
provisionFirstCollection = async (userId: string) => { provisionFirstCollection = async (userId: string) => {
await this.sequelize!.transaction(async (transaction) => { await this.sequelize!.transaction(async (transaction) => {

View File

@@ -286,13 +286,8 @@ class User extends ParanoidModel {
* @param type The type of notification event * @param type The type of notification event
* @returns The current preference * @returns The current preference
*/ */
public subscribedToEventType = (type: NotificationEventType) => { public subscribedToEventType = (type: NotificationEventType) =>
return ( this.notificationSettings[type] ?? NotificationEventDefaults[type] ?? false;
this.notificationSettings[type] ??
NotificationEventDefaults[type] ??
false
);
};
/** /**
* User flags are for storing information on a user record that is not visible * User flags are for storing information on a user record that is not visible
@@ -321,9 +316,7 @@ class User extends ParanoidModel {
* @param flag The flag to retrieve * @param flag The flag to retrieve
* @returns The flag value * @returns The flag value
*/ */
public getFlag = (flag: UserFlag) => { public getFlag = (flag: UserFlag) => this.flags?.[flag] ?? 0;
return this.flags?.[flag] ?? 0;
};
/** /**
* User flags are for storing information on a user record that is not visible * User flags are for storing information on a user record that is not visible
@@ -367,9 +360,8 @@ class User extends ParanoidModel {
* @param fallback An optional fallback value, defaults to false. * @param fallback An optional fallback value, defaults to false.
* @returns The preference value if set, else undefined * @returns The preference value if set, else undefined
*/ */
public getPreference = (preference: UserPreference, fallback = false) => { public getPreference = (preference: UserPreference, fallback = false) =>
return this.preferences?.[preference] ?? fallback; this.preferences?.[preference] ?? fallback;
};
collectionIds = async (options = {}) => { collectionIds = async (options = {}) => {
const collectionStubs = await Collection.scope({ const collectionStubs = await Collection.scope({
@@ -448,8 +440,8 @@ class User extends ParanoidModel {
* @param expiresAt The time the token will expire at * @param expiresAt The time the token will expire at
* @returns The session token * @returns The session token
*/ */
getJwtToken = (expiresAt?: Date) => { getJwtToken = (expiresAt?: Date) =>
return JWT.sign( JWT.sign(
{ {
id: this.id, id: this.id,
expiresAt: expiresAt ? expiresAt.toISOString() : undefined, expiresAt: expiresAt ? expiresAt.toISOString() : undefined,
@@ -457,7 +449,6 @@ class User extends ParanoidModel {
}, },
this.jwtSecret this.jwtSecret
); );
};
/** /**
* Returns a temporary token that is only used for transferring a session * Returns a temporary token that is only used for transferring a session
@@ -466,8 +457,8 @@ class User extends ParanoidModel {
* *
* @returns The transfer token * @returns The transfer token
*/ */
getTransferToken = () => { getTransferToken = () =>
return JWT.sign( JWT.sign(
{ {
id: this.id, id: this.id,
createdAt: new Date().toISOString(), createdAt: new Date().toISOString(),
@@ -476,7 +467,6 @@ class User extends ParanoidModel {
}, },
this.jwtSecret this.jwtSecret
); );
};
/** /**
* Returns a temporary token that is only used for logging in from an email * Returns a temporary token that is only used for logging in from an email
@@ -484,8 +474,8 @@ class User extends ParanoidModel {
* *
* @returns The email signin token * @returns The email signin token
*/ */
getEmailSigninToken = () => { getEmailSigninToken = () =>
return JWT.sign( JWT.sign(
{ {
id: this.id, id: this.id,
createdAt: new Date().toISOString(), createdAt: new Date().toISOString(),
@@ -493,15 +483,14 @@ class User extends ParanoidModel {
}, },
this.jwtSecret this.jwtSecret
); );
};
/** /**
* Returns a list of teams that have a user matching this user's email. * Returns a list of teams that have a user matching this user's email.
* *
* @returns A promise resolving to a list of teams * @returns A promise resolving to a list of teams
*/ */
availableTeams = async () => { availableTeams = async () =>
return Team.findAll({ Team.findAll({
include: [ include: [
{ {
model: this.constructor as typeof User, model: this.constructor as typeof User,
@@ -510,7 +499,6 @@ class User extends ParanoidModel {
}, },
], ],
}); });
};
demote = async (to: UserRole, options?: SaveOptions<User>) => { demote = async (to: UserRole, options?: SaveOptions<User>) => {
const res = await (this.constructor as typeof User).findAndCountAll({ const res = await (this.constructor as typeof User).findAndCountAll({
@@ -560,12 +548,11 @@ class User extends ParanoidModel {
} }
}; };
promote = () => { promote = () =>
return this.update({ this.update({
isAdmin: true, isAdmin: true,
isViewer: false, isViewer: false,
}); });
};
// hooks // hooks

View File

@@ -192,9 +192,8 @@ export default class DocumentHelper {
const dom = new JSDOM(html); const dom = new JSDOM(html);
const doc = dom.window.document; const doc = dom.window.document;
const containsDiffElement = (node: Element | null) => { const containsDiffElement = (node: Element | null) =>
return node && node.innerHTML.includes("data-operation-index"); node && node.innerHTML.includes("data-operation-index");
};
// We use querySelectorAll to get a static NodeList as we'll be modifying // We use querySelectorAll to get a static NodeList as we'll be modifying
// it as we iterate, rather than getting content.childNodes. // it as we iterate, rather than getting content.childNodes.

View File

@@ -90,7 +90,7 @@ export default class ProsemirrorHelper {
: "article"; : "article";
const rtl = isRTL(node.textContent); const rtl = isRTL(node.textContent);
const content = <div id="content" className="ProseMirror"></div>; const content = <div id="content" className="ProseMirror" />;
const children = ( const children = (
<> <>
{options?.title && <h1 dir={rtl ? "rtl" : "ltr"}>{options.title}</h1>} {options?.title && <h1 dir={rtl ? "rtl" : "ltr"}>{options.title}</h1>}

View File

@@ -8,7 +8,6 @@ export default class BacklinksProcessor extends BaseProcessor {
static applicableEvents: Event["name"][] = [ static applicableEvents: Event["name"][] = [
"documents.publish", "documents.publish",
"documents.update", "documents.update",
//"documents.title_change",
"documents.delete", "documents.delete",
]; ];
@@ -90,17 +89,6 @@ export default class BacklinksProcessor extends BaseProcessor {
break; break;
} }
case "documents.title_change": {
// might as well check
const { title, previousTitle } = event.data;
if (!previousTitle || title === previousTitle) {
break;
}
// TODO: Handle re-writing of titles into CRDT
break;
}
case "documents.delete": { case "documents.delete": {
await Backlink.destroy({ await Backlink.destroy({
where: { where: {

View File

@@ -5,13 +5,11 @@ import { getTestServer } from "@server/test/support";
const mockTeamInSessionId = "1e023d05-951c-41c6-9012-c9fa0402e1c3"; const mockTeamInSessionId = "1e023d05-951c-41c6-9012-c9fa0402e1c3";
jest.mock("@server/utils/authentication", () => { jest.mock("@server/utils/authentication", () => ({
return { getSessionsInCookie() {
getSessionsInCookie() { return { [mockTeamInSessionId]: {} };
return { [mockTeamInSessionId]: {} }; },
}, }));
};
});
const server = getTestServer(); const server = getTestServer();

View File

@@ -567,16 +567,16 @@ router.post(
}).findByPk(id); }).findByPk(id);
authorize(user, "read", collection); authorize(user, "read", collection);
const fileOperation = await sequelize.transaction(async (transaction) => { const fileOperation = await sequelize.transaction(async (transaction) =>
return collectionExporter({ collectionExporter({
collection, collection,
user, user,
team, team,
format, format,
ip: ctx.request.ip, ip: ctx.request.ip,
transaction, transaction,
}); })
}); );
ctx.body = { ctx.body = {
success: true, success: true,
@@ -599,15 +599,15 @@ router.post(
assertIn(format, Object.values(FileOperationFormat), "Invalid format"); assertIn(format, Object.values(FileOperationFormat), "Invalid format");
const fileOperation = await sequelize.transaction(async (transaction) => { const fileOperation = await sequelize.transaction(async (transaction) =>
return collectionExporter({ collectionExporter({
user, user,
team, team,
format, format,
ip: ctx.request.ip, ip: ctx.request.ip,
transaction, transaction,
}); })
}); );
ctx.body = { ctx.body = {
success: true, success: true,

View File

@@ -1300,8 +1300,8 @@ router.post(
authorize(user, "read", templateDocument); authorize(user, "read", templateDocument);
} }
const document = await sequelize.transaction(async (transaction) => { const document = await sequelize.transaction(async (transaction) =>
return documentCreator({ documentCreator({
title, title,
text, text,
publish, publish,
@@ -1313,8 +1313,8 @@ router.post(
editorVersion, editorVersion,
ip: ctx.request.ip, ip: ctx.request.ip,
transaction, transaction,
}); })
}); );
document.collection = collection; document.collection = collection;

View File

@@ -58,12 +58,10 @@ router.post(
authorize(user, "createTeam", existingTeam); authorize(user, "createTeam", existingTeam);
const authenticationProviders = existingTeam.authenticationProviders.map( const authenticationProviders = existingTeam.authenticationProviders.map(
(provider) => { (provider) => ({
return { name: provider.name,
name: provider.name, providerId: provider.providerId,
providerId: provider.providerId, })
};
}
); );
invariant( invariant(

View File

@@ -6,8 +6,8 @@ import { User, Document, Collection, Team } from "@server/models";
import onerror from "@server/onerror"; import onerror from "@server/onerror";
import webService from "@server/services/web"; import webService from "@server/services/web";
export const seed = async () => { export const seed = async () =>
return sequelize.transaction(async (transaction) => { sequelize.transaction(async (transaction) => {
const team = await Team.create( const team = await Team.create(
{ {
name: "Team", name: "Team",
@@ -97,7 +97,6 @@ export const seed = async () => {
team, team,
}; };
}); });
};
export function getTestServer() { export function getTestServer() {
const app = webService(); const app = webService();

View File

@@ -30,8 +30,8 @@ export function initI18n() {
i18n.use(backend).init({ i18n.use(backend).init({
compatibilityJSON: "v3", compatibilityJSON: "v3",
backend: { backend: {
loadPath: (language: string) => { loadPath: (language: string) =>
return path.resolve( path.resolve(
path.join( path.join(
__dirname, __dirname,
"..", "..",
@@ -42,8 +42,7 @@ export function initI18n() {
unicodeBCP47toCLDR(language), unicodeBCP47toCLDR(language),
"translation.json" "translation.json"
) )
); ),
},
}, },
preload: languages.map(unicodeCLDRtoBCP47), preload: languages.map(unicodeCLDRtoBCP47),
interpolation: { interpolation: {

View File

@@ -1,5 +1,4 @@
export const opensearchResponse = (baseUrl: string): string => { export const opensearchResponse = (baseUrl: string): string => `
return `
<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/" xmlns:moz="http://www.mozilla.org/2006/browser/search/"> <OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/" xmlns:moz="http://www.mozilla.org/2006/browser/search/">
<ShortName>Outline</ShortName> <ShortName>Outline</ShortName>
<Description>Search Outline</Description> <Description>Search Outline</Description>
@@ -9,4 +8,3 @@ export const opensearchResponse = (baseUrl: string): string => {
<moz:SearchForm>${baseUrl}/search</moz:SearchForm> <moz:SearchForm>${baseUrl}/search</moz:SearchForm>
</OpenSearchDescription> </OpenSearchDescription>
`; `;
};

View File

@@ -23,14 +23,12 @@ if (isProduction) {
const returnFileAndImportsFromManifest = ( const returnFileAndImportsFromManifest = (
manifest: ManifestStructure, manifest: ManifestStructure,
file: string file: string
): string[] => { ): string[] => [
return [ manifest[file]["file"],
manifest[file]["file"], ...manifest[file]["imports"].map(
...manifest[file]["imports"].map((entry: string) => { (entry: string) => manifest[entry]["file"]
return manifest[entry]["file"]; ),
}), ];
];
};
Array.from([ Array.from([
...returnFileAndImportsFromManifest(manifest, "app/index.tsx"), ...returnFileAndImportsFromManifest(manifest, "app/index.tsx"),

View File

@@ -150,14 +150,13 @@ export const uploadToS3FromUrl = async (
} }
}; };
export const deleteFromS3 = (key: string) => { export const deleteFromS3 = (key: string) =>
return s3 s3
.deleteObject({ .deleteObject({
Bucket: AWS_S3_UPLOAD_BUCKET_NAME, Bucket: AWS_S3_UPLOAD_BUCKET_NAME,
Key: key, Key: key,
}) })
.promise(); .promise();
};
export const getSignedUrl = async (key: string, expiresInMs = 60) => { export const getSignedUrl = async (key: string, expiresInMs = 60) => {
const isDocker = AWS_S3_UPLOAD_BUCKET_URL.match(/http:\/\/s3:/); const isDocker = AWS_S3_UPLOAD_BUCKET_URL.match(/http:\/\/s3:/);

View File

@@ -20,16 +20,11 @@ export interface GetScaleToWindow {
(data: { width: number; height: number; offset: number }): number; (data: { width: number; height: number; offset: number }): number;
} }
export const getScaleToWindow: GetScaleToWindow = ({ export const getScaleToWindow: GetScaleToWindow = ({ height, offset, width }) =>
height, Math.min(
offset,
width,
}) => {
return Math.min(
(window.innerWidth - offset * 2) / width, // scale X-axis (window.innerWidth - offset * 2) / width, // scale X-axis
(window.innerHeight - offset * 2) / height // scale Y-axis (window.innerHeight - offset * 2) / height // scale Y-axis
); );
};
export interface GetScaleToWindowMax { export interface GetScaleToWindowMax {
(data: { (data: {
@@ -80,8 +75,8 @@ export const getScale: GetScale = ({
offset, offset,
targetHeight, targetHeight,
targetWidth, targetWidth,
}) => { }) =>
return !hasScalableSrc && targetHeight && targetWidth !hasScalableSrc && targetHeight && targetWidth
? getScaleToWindowMax({ ? getScaleToWindowMax({
containerHeight, containerHeight,
containerWidth, containerWidth,
@@ -94,7 +89,6 @@ export const getScale: GetScale = ({
offset, offset,
width: containerWidth, width: containerWidth,
}); });
};
const URL_REGEX = /url(?:\(['"]?)(.*?)(?:['"]?\))/; const URL_REGEX = /url(?:\(['"]?)(.*?)(?:['"]?\))/;

View File

@@ -10,9 +10,10 @@ export default function chainTransactions(
dispatch?.(tr); dispatch?.(tr);
}; };
const last = commands.pop(); const last = commands.pop();
const reduced = commands.reduce((result, command) => { const reduced = commands.reduce(
return result || command(state, dispatcher); (result, command) => result || command(state, dispatcher),
}, false); false
);
return reduced && last !== undefined && last(state, dispatch); return reduced && last !== undefined && last(state, dispatch);
}; };
} }

Some files were not shown because too many files have changed in this diff Show More