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": {
"eqeqeq": 2,
"curly": 2,
"arrow-body-style": ["error", "as-needed"],
"spaced-comment": "error",
"object-shorthand": "error",
"no-mixed-operators": "off",
"no-useless-escape": "off",
"es/no-regexp-lookbehind-assertions": "error",
"react/self-closing-comp": ["error", {
"component": true,
"html": true
}],
"@typescript-eslint/no-unused-vars": [
"error",
{

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -9,24 +9,22 @@ type Props = {
description?: JSX.Element;
};
const Notice: React.FC<Props> = ({ children, icon, description }) => {
return (
<Container>
<Flex as="span" gap={8}>
{icon}
<span>
<Title>{children}</Title>
{description && (
<>
<br />
{description}
</>
)}
</span>
</Flex>
</Container>
);
};
const Notice: React.FC<Props> = ({ children, icon, description }) => (
<Container>
<Flex as="span" gap={8}>
{icon}
<span>
<Title>{children}</Title>
{description && (
<>
<br />
{description}
</>
)}
</span>
</Flex>
</Container>
);
const Title = styled.span`
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,
children,
centered,
}) => {
return (
<FillWidth>
<PageTitle title={textTitle || title} />
<Header
hasSidebar
title={
icon ? (
<>
{icon}&nbsp;{title}
</>
) : (
title
)
}
actions={actions}
left={left}
/>
{centered !== false ? (
<CenteredContent withStickyHeader>{children}</CenteredContent>
) : (
children
)}
</FillWidth>
);
};
}) => (
<FillWidth>
<PageTitle title={textTitle || title} />
<Header
hasSidebar
title={
icon ? (
<>
{icon}&nbsp;{title}
</>
) : (
title
)
}
actions={actions}
left={left}
/>
{centered !== false ? (
<CenteredContent withStickyHeader>{children}</CenteredContent>
) : (
children
)}
</FillWidth>
);
const FillWidth = styled.div`
width: 100%;

View File

@@ -158,21 +158,19 @@ function SearchPopover({ shareId }: Props) {
return (
<>
<PopoverDisclosure {...popover}>
{(props) => {
{(props) => (
// 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
return (
<StyledInputSearch
aria-controls={props["aria-controls"]}
aria-expanded={props["aria-expanded"]}
aria-haspopup={props["aria-haspopup"]}
ref={props.ref}
onChange={handleSearchInputChange}
onFocus={handleSearchInputFocus}
onKeyDown={handleKeyDown}
/>
);
}}
<StyledInputSearch
aria-controls={props["aria-controls"]}
aria-expanded={props["aria-expanded"]}
aria-haspopup={props["aria-haspopup"]}
ref={props.ref}
onChange={handleSearchInputChange}
onFocus={handleSearchInputFocus}
onKeyDown={handleKeyDown}
/>
)}
</PopoverDisclosure>
<Popover
{...popover}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -7,23 +7,21 @@ type Props = {
color?: string;
};
const Squircle: React.FC<Props> = ({ color, size = 28, children }) => {
return (
<Wrapper
style={{ width: size, height: size }}
align="center"
justify="center"
>
<svg width={size} height={size} viewBox="0 0 28 28">
<path
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"
/>
</svg>
<Content>{children}</Content>
</Wrapper>
);
};
const Squircle: React.FC<Props> = ({ color, size = 28, children }) => (
<Wrapper
style={{ width: size, height: size }}
align="center"
justify="center"
>
<svg width={size} height={size} viewBox="0 0 28 28">
<path
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"
/>
</svg>
<Content>{children}</Content>
</Wrapper>
);
const Wrapper = styled(Flex)`
position: relative;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -25,10 +25,9 @@ function LinkSearchResult({
scrollIntoView(node, {
scrollMode: "if-needed",
block: "center",
boundary: (parent) => {
boundary: (parent) =>
// 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(
filtered.sort((item) => {
return searchInput && item.title
? commandScore(item.title, searchInput)
: 0;
})
filtered.sort((item) =>
searchInput && item.title ? commandScore(item.title, searchInput) : 0
)
);
}, [commands, props]);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -36,8 +36,8 @@ const AccountMenu: React.FC = ({ children }) => {
}
}, [menu, theme, previousTheme]);
const actions = React.useMemo(() => {
return [
const actions = React.useMemo(
() => [
openKeyboardShortcuts,
downloadApp,
openAPIDocumentation,
@@ -50,8 +50,9 @@ const AccountMenu: React.FC = ({ children }) => {
navigateToAccountPreferences,
separator(),
logout,
];
}, []);
],
[]
);
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
// menu is not cached at all.
const actions = React.useMemo(() => {
return [
const actions = React.useMemo(
() => [
...createTeamsList(context),
createTeam,
separator(),
navigateToSettings,
logout,
];
}, [context]);
],
[context]
);
return (
<>

View File

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

View File

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

View File

@@ -238,23 +238,17 @@ export default class Document extends ParanoidModel {
}
@action
share = async () => {
return this.store.rootStore.shares.create({
share = async () =>
this.store.rootStore.shares.create({
documentId: this.id,
});
};
archive = () => {
return this.store.archive(this);
};
archive = () => this.store.archive(this);
restore = (options?: { revisionId?: string; collectionId?: string }) => {
return this.store.restore(this, options);
};
restore = (options?: { revisionId?: string; collectionId?: string }) =>
this.store.restore(this, options);
unpublish = () => {
return this.store.unpublish(this);
};
unpublish = () => this.store.unpublish(this);
@action
enableEmbeds = () => {
@@ -267,12 +261,11 @@ export default class Document extends ParanoidModel {
};
@action
pin = (collectionId?: string) => {
return this.store.rootStore.pins.create({
pin = (collectionId?: string) =>
this.store.rootStore.pins.create({
documentId: this.id,
...(collectionId ? { collectionId } : {}),
});
};
@action
unpin = (collectionId?: string) => {
@@ -287,14 +280,10 @@ export default class Document extends ParanoidModel {
};
@action
star = () => {
return this.store.star(this);
};
star = () => this.store.star(this);
@action
unstar = () => {
return this.store.unstar(this);
};
unstar = () => this.store.unstar(this);
/**
* 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.
*/
@action
subscribe = () => {
return this.store.subscribe(this);
};
subscribe = () => this.store.subscribe(this);
/**
* 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.
*/
@action
unsubscribe = (userId: string) => {
return this.store.unsubscribe(userId, this);
};
unsubscribe = (userId: string) => this.store.unsubscribe(userId, this);
@action
view = () => {
@@ -336,9 +321,7 @@ export default class Document extends ParanoidModel {
};
@action
templatize = () => {
return this.store.templatize(this.id);
};
templatize = () => this.store.templatize(this.id);
@action
save = async (options?: SaveOptions | undefined) => {
@@ -359,13 +342,10 @@ export default class Document extends ParanoidModel {
}
};
move = (collectionId: string, parentDocumentId?: string | undefined) => {
return this.store.move(this.id, collectionId, parentDocumentId);
};
move = (collectionId: string, parentDocumentId?: string | undefined) =>
this.store.move(this.id, collectionId, parentDocumentId);
duplicate = () => {
return this.store.duplicate(this);
};
duplicate = () => this.store.duplicate(this);
getSummary = (paragraphs = 4) => {
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) => {
return client.post(
download = (contentType: ExportContentType) =>
client.post(
`/documents.export`,
{
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
* @returns The current preference
*/
public subscribedToEventType = (type: NotificationEventType) => {
return (
this.notificationSettings[type] ??
NotificationEventDefaults[type] ??
false
);
};
public subscribedToEventType = (type: NotificationEventType) =>
this.notificationSettings[type] ?? NotificationEventDefaults[type] ?? false;
/**
* Sets a preference for the users notification settings on the model and

View File

@@ -1,8 +1,7 @@
const fields = new Map();
export const getFieldsForModel = (target: any) => {
return fields.get(target.constructor.name);
};
export const getFieldsForModel = (target: any) =>
fields.get(target.constructor.name);
/**
* 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 = (
props: EditorProps,
ref: React.RefObject<SharedEditor>
) => {
return <Editor extensions={extensions} {...props} ref={ref} />;
};
) => <Editor extensions={extensions} {...props} ref={ref} />;
export default React.forwardRef(CommentEditor);

View File

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

View File

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

View File

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

View File

@@ -30,9 +30,7 @@ export default function DocumentScene(props: Props) {
setLastVisitedPath(currentPath);
}, [currentPath, setLastVisitedPath]);
React.useEffect(() => {
return () => ui.clearActiveDocument();
}, [ui]);
React.useEffect(() => () => ui.clearActiveDocument(), [ui]);
// 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

View File

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

View File

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

View File

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

View File

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

View File

@@ -21,7 +21,7 @@ function Zapier() {
type="module"
src="https://cdn.zapier.com/packages/partner-sdk/v0/zapier-elements/zapier-elements.esm.js"
key="zapier-js"
></script>
/>
<link
rel="stylesheet"
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 Button from "~/components/Button";
import CopyToClipboard from "~/components/CopyToClipboard";
import Flex from "~/components/Flex";
import ListItem from "~/components/List/Item";
import useStores from "~/hooks/useStores";
import useToasts from "~/hooks/useToasts";
import TokenRevokeDialog from "./TokenRevokeDialog";
import ApiKeyMenu from "~/menus/ApiKeyMenu";
type Props = {
token: ApiKey;
@@ -16,7 +16,6 @@ type Props = {
const TokenListItem = ({ token }: Props) => {
const { t } = useTranslation();
const { showToast } = useToasts();
const { dialogs } = useStores();
const [linkCopied, setLinkCopied] = React.useState<boolean>(false);
React.useEffect(() => {
@@ -34,32 +33,20 @@ const TokenListItem = ({ token }: Props) => {
});
}, [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 (
<ListItem
key={token.id}
title={token.name}
subtitle={<code>{token.secret}</code>}
subtitle={<code>{token.secret.slice(0, 15)}</code>}
actions={
<>
<Flex align="center" gap={8}>
<CopyToClipboard text={token.secret} onCopy={handleCopy}>
<Button type="button" icon={<CopyIcon />} neutral borderOnHover>
{linkCopied ? t("Copied") : t("Copy")}
</Button>
</CopyToClipboard>
<Button onClick={showRevokeConfirmation} neutral>
Revoke
</Button>
</>
<ApiKeyMenu token={token} />
</Flex>
}
/>
);

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
requestDelete = () => {
return client.post(`/users.requestDelete`);
};
requestDelete = () => client.post(`/users.requestDelete`);
@action
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.
Desktop.bridge?.onLogout?.();
this.rootStore.logout();
};
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -87,30 +87,10 @@ export default class RootStore {
}
logout() {
this.apiKeys.clear();
this.authenticationProviders.clear();
// this.auth omitted for reasons...
this.collections.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();
Object.getOwnPropertyNames(this)
.filter((key) => ["auth", "ui"].includes(key) === false)
.forEach((key) => {
this[key]?.clear?.();
});
}
}

View File

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

View File

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

View File

@@ -1,3 +1,4 @@
import { computed } from "mobx";
import WebhookSubscription from "~/models/WebhookSubscription";
import BaseStore, { RPCAction } from "./BaseStore";
import RootStore from "./RootStore";
@@ -15,4 +16,14 @@ export default class WebhookSubscriptionsStore extends BaseStore<
constructor(rootStore: RootStore) {
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,
data: Record<string, any> | undefined,
options?: FetchOptions
) => {
return this.fetch(path, "GET", data, options);
};
) => this.fetch(path, "GET", data, options);
post = (
path: string,
data?: Record<string, any> | undefined,
options?: FetchOptions
) => {
return this.fetch(path, "POST", data, options);
};
) => this.fetch(path, "POST", data, options);
}
export const client = new ApiClient();

View File

@@ -8,8 +8,7 @@ type Options = {
export const compressImage = async (
file: File | Blob,
options?: Options
): Promise<Blob> => {
return new Promise((resolve, reject) => {
): Promise<Blob> =>
new Promise((resolve, 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
}
//end if dataURL passed?
// end if dataURL passed?
try {
blob =
x instanceof B
@@ -81,7 +81,7 @@ export default function download(
return true;
}
//do iframe dataURL download (old ch+FF):
// do iframe dataURL download (old ch+FF):
const f = D.createElement("iframe");
D.body && D.body.appendChild(f);

View File

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

View File

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

View File

@@ -45,7 +45,7 @@ const WebhookSubscriptionListItem = ({ webhook }: Props) => {
<>
{webhook.name}
{!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,
};
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
const team = await sequelize.transaction((transaction) => {
return teamCreator({
const team = await sequelize.transaction((transaction) =>
teamCreator({
name,
domain,
subdomain,
@@ -115,8 +115,8 @@ async function teamProvisioner({
authenticationProviders: [authenticationProvider],
ip,
transaction,
});
});
})
);
return {
team,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -90,7 +90,7 @@ export default class ProsemirrorHelper {
: "article";
const rtl = isRTL(node.textContent);
const content = <div id="content" className="ProseMirror"></div>;
const content = <div id="content" className="ProseMirror" />;
const children = (
<>
{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"][] = [
"documents.publish",
"documents.update",
//"documents.title_change",
"documents.delete",
];
@@ -90,17 +89,6 @@ export default class BacklinksProcessor extends BaseProcessor {
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": {
await Backlink.destroy({
where: {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -150,14 +150,13 @@ export const uploadToS3FromUrl = async (
}
};
export const deleteFromS3 = (key: string) => {
return s3
export const deleteFromS3 = (key: string) =>
s3
.deleteObject({
Bucket: AWS_S3_UPLOAD_BUCKET_NAME,
Key: key,
})
.promise();
};
export const getSignedUrl = async (key: string, expiresInMs = 60) => {
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;
}
export const getScaleToWindow: GetScaleToWindow = ({
height,
offset,
width,
}) => {
return Math.min(
export const getScaleToWindow: GetScaleToWindow = ({ height, offset, width }) =>
Math.min(
(window.innerWidth - offset * 2) / width, // scale X-axis
(window.innerHeight - offset * 2) / height // scale Y-axis
);
};
export interface GetScaleToWindowMax {
(data: {
@@ -80,8 +75,8 @@ export const getScale: GetScale = ({
offset,
targetHeight,
targetWidth,
}) => {
return !hasScalableSrc && targetHeight && targetWidth
}) =>
!hasScalableSrc && targetHeight && targetWidth
? getScaleToWindowMax({
containerHeight,
containerWidth,
@@ -94,7 +89,6 @@ export const getScale: GetScale = ({
offset,
width: containerWidth,
});
};
const URL_REGEX = /url(?:\(['"]?)(.*?)(?:['"]?\))/;

View File

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

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