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

@@ -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);