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:
@@ -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",
|
||||
{
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -42,7 +42,7 @@ const Circle = ({
|
||||
style={{
|
||||
transition: "stroke-dashoffset 0.6s ease 0s",
|
||||
}}
|
||||
></circle>
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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. */
|
||||
|
||||
@@ -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;
|
||||
`;
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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%; }
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
@@ -21,32 +21,30 @@ const Scene: React.FC<Props> = ({
|
||||
left,
|
||||
children,
|
||||
centered,
|
||||
}) => {
|
||||
return (
|
||||
<FillWidth>
|
||||
<PageTitle title={textTitle || title} />
|
||||
<Header
|
||||
hasSidebar
|
||||
title={
|
||||
icon ? (
|
||||
<>
|
||||
{icon} {title}
|
||||
</>
|
||||
) : (
|
||||
title
|
||||
)
|
||||
}
|
||||
actions={actions}
|
||||
left={left}
|
||||
/>
|
||||
{centered !== false ? (
|
||||
<CenteredContent withStickyHeader>{children}</CenteredContent>
|
||||
) : (
|
||||
children
|
||||
)}
|
||||
</FillWidth>
|
||||
);
|
||||
};
|
||||
}) => (
|
||||
<FillWidth>
|
||||
<PageTitle title={textTitle || title} />
|
||||
<Header
|
||||
hasSidebar
|
||||
title={
|
||||
icon ? (
|
||||
<>
|
||||
{icon} {title}
|
||||
</>
|
||||
) : (
|
||||
title
|
||||
)
|
||||
}
|
||||
actions={actions}
|
||||
left={left}
|
||||
/>
|
||||
{centered !== false ? (
|
||||
<CenteredContent withStickyHeader>{children}</CenteredContent>
|
||||
) : (
|
||||
children
|
||||
)}
|
||||
</FillWidth>
|
||||
);
|
||||
|
||||
const FillWidth = styled.div`
|
||||
width: 100%;
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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) => (
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -22,7 +22,7 @@ export default function Spinner({ color, ...props }: Props) {
|
||||
cx="8"
|
||||
cy="8"
|
||||
r="6"
|
||||
></Circle>
|
||||
/>
|
||||
</SVG>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -30,7 +30,7 @@ function BlockMenu(props: Props) {
|
||||
return (
|
||||
<SuggestionsMenu
|
||||
{...props}
|
||||
filterable={true}
|
||||
filterable
|
||||
onClearSearch={clearSearch}
|
||||
renderMenuItem={(item, _index, options) => (
|
||||
<SuggestionsMenuItem
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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]);
|
||||
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -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(
|
||||
() =>
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
52
app/menus/ApiKeyMenu.tsx
Normal 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);
|
||||
@@ -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 (
|
||||
<>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
},
|
||||
}
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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]
|
||||
);
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
>
|
||||
|
||||
@@ -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} />
|
||||
)}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -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> {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 }} hasn’t 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);
|
||||
@@ -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();
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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?.();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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> => {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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 });
|
||||
});
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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} />
|
||||
)}
|
||||
|
||||
@@ -45,7 +45,7 @@ const WebhookSubscriptionListItem = ({ webhook }: Props) => {
|
||||
<>
|
||||
{webhook.name}
|
||||
{!webhook.enabled && (
|
||||
<StyledBadge yellow={true}>{t("Disabled")}</StyledBadge>
|
||||
<StyledBadge yellow>{t("Disabled")}</StyledBadge>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
|
||||
@@ -106,6 +106,10 @@ router.post(
|
||||
ip: ctx.request.ip,
|
||||
};
|
||||
await Event.create(event);
|
||||
|
||||
ctx.body = {
|
||||
success: true,
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(
|
||||
{
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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>}
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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>
|
||||
`;
|
||||
};
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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:/);
|
||||
|
||||
@@ -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(?:\(['"]?)(.*?)(?:['"]?\))/;
|
||||
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user