chore: Enable eslint to enforce curly (#3060)

This commit is contained in:
Tom Moor
2022-02-05 10:15:40 -08:00
committed by GitHub
parent c7df74fcc4
commit c5a11fe17b
103 changed files with 1175 additions and 397 deletions

View File

@@ -25,6 +25,7 @@
], ],
"rules": { "rules": {
"eqeqeq": 2, "eqeqeq": 2,
"curly": 2,
"no-mixed-operators": "off", "no-mixed-operators": "off",
"no-useless-escape": "off", "no-useless-escape": "off",
"@typescript-eslint/no-unused-vars": [ "@typescript-eslint/no-unused-vars": [

View File

@@ -52,7 +52,9 @@ export const editCollection = createAction({
!!activeCollectionId && !!activeCollectionId &&
stores.policies.abilities(activeCollectionId).update, stores.policies.abilities(activeCollectionId).update,
perform: ({ t, activeCollectionId }) => { perform: ({ t, activeCollectionId }) => {
if (!activeCollectionId) return; if (!activeCollectionId) {
return;
}
stores.dialogs.openModal({ stores.dialogs.openModal({
title: t("Edit collection"), title: t("Edit collection"),

View File

@@ -61,14 +61,18 @@ export const starDocument = createAction({
icon: <StarredIcon />, icon: <StarredIcon />,
keywords: "favorite bookmark", keywords: "favorite bookmark",
visible: ({ activeDocumentId, stores }) => { visible: ({ activeDocumentId, stores }) => {
if (!activeDocumentId) return false; if (!activeDocumentId) {
return false;
}
const document = stores.documents.get(activeDocumentId); const document = stores.documents.get(activeDocumentId);
return ( return (
!document?.isStarred && stores.policies.abilities(activeDocumentId).star !document?.isStarred && stores.policies.abilities(activeDocumentId).star
); );
}, },
perform: ({ activeDocumentId, stores }) => { perform: ({ activeDocumentId, stores }) => {
if (!activeDocumentId) return; if (!activeDocumentId) {
return;
}
const document = stores.documents.get(activeDocumentId); const document = stores.documents.get(activeDocumentId);
document?.star(); document?.star();
@@ -81,7 +85,9 @@ export const unstarDocument = createAction({
icon: <UnstarredIcon />, icon: <UnstarredIcon />,
keywords: "unfavorite unbookmark", keywords: "unfavorite unbookmark",
visible: ({ activeDocumentId, stores }) => { visible: ({ activeDocumentId, stores }) => {
if (!activeDocumentId) return false; if (!activeDocumentId) {
return false;
}
const document = stores.documents.get(activeDocumentId); const document = stores.documents.get(activeDocumentId);
return ( return (
!!document?.isStarred && !!document?.isStarred &&
@@ -89,7 +95,9 @@ export const unstarDocument = createAction({
); );
}, },
perform: ({ activeDocumentId, stores }) => { perform: ({ activeDocumentId, stores }) => {
if (!activeDocumentId) return; if (!activeDocumentId) {
return;
}
const document = stores.documents.get(activeDocumentId); const document = stores.documents.get(activeDocumentId);
document?.unstar(); document?.unstar();
@@ -105,7 +113,9 @@ export const downloadDocument = createAction({
visible: ({ activeDocumentId, stores }) => visible: ({ activeDocumentId, stores }) =>
!!activeDocumentId && stores.policies.abilities(activeDocumentId).download, !!activeDocumentId && stores.policies.abilities(activeDocumentId).download,
perform: ({ activeDocumentId, stores }) => { perform: ({ activeDocumentId, stores }) => {
if (!activeDocumentId) return; if (!activeDocumentId) {
return;
}
const document = stores.documents.get(activeDocumentId); const document = stores.documents.get(activeDocumentId);
document?.download(); document?.download();
@@ -121,7 +131,9 @@ export const duplicateDocument = createAction({
visible: ({ activeDocumentId, stores }) => visible: ({ activeDocumentId, stores }) =>
!!activeDocumentId && stores.policies.abilities(activeDocumentId).update, !!activeDocumentId && stores.policies.abilities(activeDocumentId).update,
perform: async ({ activeDocumentId, t, stores }) => { perform: async ({ activeDocumentId, t, stores }) => {
if (!activeDocumentId) return; if (!activeDocumentId) {
return;
}
const document = stores.documents.get(activeDocumentId); const document = stores.documents.get(activeDocumentId);
invariant(document, "Document must exist"); invariant(document, "Document must exist");
@@ -189,7 +201,9 @@ export const pinDocumentToHome = createAction({
); );
}, },
perform: async ({ activeDocumentId, location, t, stores }) => { perform: async ({ activeDocumentId, location, t, stores }) => {
if (!activeDocumentId) return; if (!activeDocumentId) {
return;
}
const document = stores.documents.get(activeDocumentId); const document = stores.documents.get(activeDocumentId);
await document?.pin(); await document?.pin();
@@ -265,7 +279,9 @@ export const createTemplate = createAction({
icon: <ShapesIcon />, icon: <ShapesIcon />,
keywords: "new create template", keywords: "new create template",
visible: ({ activeCollectionId, activeDocumentId, stores }) => { visible: ({ activeCollectionId, activeDocumentId, stores }) => {
if (!activeDocumentId) return false; if (!activeDocumentId) {
return false;
}
const document = stores.documents.get(activeDocumentId); const document = stores.documents.get(activeDocumentId);
return ( return (
!!activeCollectionId && !!activeCollectionId &&
@@ -274,7 +290,9 @@ export const createTemplate = createAction({
); );
}, },
perform: ({ activeDocumentId, stores, t, event }) => { perform: ({ activeDocumentId, stores, t, event }) => {
if (!activeDocumentId) return; if (!activeDocumentId) {
return;
}
event?.preventDefault(); event?.preventDefault();
event?.stopPropagation(); event?.stopPropagation();

View File

@@ -55,9 +55,13 @@ class AuthenticatedLayout extends React.Component<Props> {
goToNewDocument = () => { goToNewDocument = () => {
const { activeCollectionId } = this.props.ui; const { activeCollectionId } = this.props.ui;
if (!activeCollectionId) return; if (!activeCollectionId) {
return;
}
const can = this.props.policies.abilities(activeCollectionId); const can = this.props.policies.abilities(activeCollectionId);
if (!can.update) return; if (!can.update) {
return;
}
history.push(newDocumentPath(activeCollectionId)); history.push(newDocumentPath(activeCollectionId));
}; };
@@ -65,7 +69,9 @@ class AuthenticatedLayout extends React.Component<Props> {
const { auth } = this.props; const { auth } = this.props;
const { user, team } = auth; const { user, team } = auth;
const showSidebar = auth.authenticated && user && team; const showSidebar = auth.authenticated && user && team;
if (auth.isSuspended) return <ErrorSuspended />; if (auth.isSuspended) {
return <ErrorSuspended />;
}
const sidebar = showSidebar ? ( const sidebar = showSidebar ? (
<Switch> <Switch>

View File

@@ -72,13 +72,18 @@ export function filterTemplateItems(items: TMenuItem[]): TMenuItem[] {
// this block literally just trims unnecessary separators // this block literally just trims unnecessary separators
filtered = filtered.reduce((acc, item, index) => { filtered = filtered.reduce((acc, item, index) => {
// trim separators from start / end // trim separators from start / end
if (item.type === "separator" && index === 0) return acc; if (item.type === "separator" && index === 0) {
if (item.type === "separator" && index === filtered.length - 1) return acc; return acc;
}
if (item.type === "separator" && index === filtered.length - 1) {
return acc;
}
// trim double separators looking ahead / behind // trim double separators looking ahead / behind
const prev = filtered[index - 1]; const prev = filtered[index - 1];
if (prev && prev.type === "separator" && item.type === "separator") if (prev && prev.type === "separator" && item.type === "separator") {
return acc; return acc;
}
// otherwise, continue // otherwise, continue
return [...acc, item]; return [...acc, item];

View File

@@ -28,7 +28,9 @@ function HoverPreviewInternal({ node, onClose }: Props) {
const startCloseTimer = () => { const startCloseTimer = () => {
stopOpenTimer(); stopOpenTimer();
timerClose.current = setTimeout(() => { timerClose.current = setTimeout(() => {
if (isVisible) setVisible(false); if (isVisible) {
setVisible(false);
}
onClose(); onClose();
}, DELAY_CLOSE); }, DELAY_CLOSE);
}; };

View File

@@ -21,7 +21,9 @@ function HoverPreviewDocument({ url, children }: Props) {
} }
const document = slug ? documents.getByUrl(slug) : undefined; const document = slug ? documents.getByUrl(slug) : undefined;
if (!document) return null; if (!document) {
return null;
}
return ( return (
<> <>

View File

@@ -84,7 +84,9 @@ const InputSelect = (props: Props) => {
); );
React.useEffect(() => { React.useEffect(() => {
if (previousValue.current === select.selectedValue) return; if (previousValue.current === select.selectedValue) {
return;
}
previousValue.current = select.selectedValue; previousValue.current = select.selectedValue;
async function load() { async function load() {

View File

@@ -70,7 +70,9 @@ class PaginatedList extends React.Component<Props> {
}; };
fetchResults = async () => { fetchResults = async () => {
if (!this.props.fetch) return; if (!this.props.fetch) {
return;
}
this.isFetching = true; this.isFetching = true;
const limit = DEFAULT_PAGINATION_LIMIT; const limit = DEFAULT_PAGINATION_LIMIT;
const results = await this.props.fetch({ const results = await this.props.fetch({
@@ -94,7 +96,9 @@ class PaginatedList extends React.Component<Props> {
@action @action
loadMoreResults = async () => { loadMoreResults = async () => {
// Don't paginate if there aren't more results or were currently fetching // Don't paginate if there aren't more results or were currently fetching
if (!this.allowLoadMore || this.isFetching) return; if (!this.allowLoadMore || this.isFetching) {
return;
}
// If there are already cached results that we haven't yet rendered because // If there are already cached results that we haven't yet rendered because
// of lazy rendering then show another page. // of lazy rendering then show another page.
const leftToRender = this.props.items.length - this.renderCount; const leftToRender = this.props.items.length - this.renderCount;

View File

@@ -22,7 +22,9 @@ class PathToDocument extends React.Component<Props> {
handleClick = async (ev: React.SyntheticEvent) => { handleClick = async (ev: React.SyntheticEvent) => {
ev.preventDefault(); ev.preventDefault();
const { document, result, onSuccess } = this.props; const { document, result, onSuccess } = this.props;
if (!document) return; if (!document) {
return;
}
if (result.type === "document") { if (result.type === "document") {
await document.move(result.collectionId, result.id); await document.move(result.collectionId, result.id);
@@ -30,13 +32,17 @@ class PathToDocument extends React.Component<Props> {
await document.move(result.collectionId); await document.move(result.collectionId);
} }
if (onSuccess) onSuccess(); if (onSuccess) {
onSuccess();
}
}; };
render() { render() {
const { result, collection, document, ref, style } = this.props; const { result, collection, document, ref, style } = this.props;
const Component = document ? ResultWrapperLink : ResultWrapper; const Component = document ? ResultWrapperLink : ResultWrapper;
if (!result) return <div />; if (!result) {
return <div />;
}
return ( return (
// @ts-expect-error ts-migrate(2604) FIXME: JSX element type 'Component' does not have any con... Remove this comment to see the full error message // @ts-expect-error ts-migrate(2604) FIXME: JSX element type 'Component' does not have any con... Remove this comment to see the full error message

View File

@@ -12,13 +12,16 @@ export default function ScrollToTop({ children }: Props) {
const previousLocationPathname = usePrevious(location.pathname); const previousLocationPathname = usePrevious(location.pathname);
React.useEffect(() => { React.useEffect(() => {
if (location.pathname === previousLocationPathname) return; if (location.pathname === previousLocationPathname) {
return;
}
// exception for when entering or exiting document edit, scroll position should not reset // exception for when entering or exiting document edit, scroll position should not reset
if ( if (
location.pathname.match(/\/edit\/?$/) || location.pathname.match(/\/edit\/?$/) ||
previousLocationPathname?.match(/\/edit\/?$/) previousLocationPathname?.match(/\/edit\/?$/)
) ) {
return; return;
}
window.scrollTo(0, 0); window.scrollTo(0, 0);
}, [location.pathname, previousLocationPathname]); }, [location.pathname, previousLocationPathname]);

View File

@@ -21,7 +21,9 @@ function Scrollable(
const { height } = useWindowSize(); const { height } = useWindowSize();
const updateShadows = React.useCallback(() => { const updateShadows = React.useCallback(() => {
const c = (ref || fallbackRef).current; const c = (ref || fallbackRef).current;
if (!c) return; if (!c) {
return;
}
const scrollTop = c.scrollTop; const scrollTop = c.scrollTop;
const tsv = !!((shadow || topShadow) && scrollTop > 0); const tsv = !!((shadow || topShadow) && scrollTop > 0);

View File

@@ -79,8 +79,12 @@ function CollectionLink({
accept: "document", accept: "document",
drop: (item: DragObject, monitor) => { drop: (item: DragObject, monitor) => {
const { id, collectionId } = item; const { id, collectionId } = item;
if (monitor.didDrop()) return; if (monitor.didDrop()) {
if (!collection) return; return;
}
if (!collection) {
return;
}
const document = documents.get(id); const document = documents.get(id);
if (collection.id === collectionId && !document?.parentDocumentId) { if (collection.id === collectionId && !document?.parentDocumentId) {
@@ -115,7 +119,9 @@ function CollectionLink({
const [{ isOverReorder }, dropToReorder] = useDrop({ const [{ isOverReorder }, dropToReorder] = useDrop({
accept: "document", accept: "document",
drop: async (item: DragObject) => { drop: async (item: DragObject) => {
if (!collection) return; if (!collection) {
return;
}
documents.move(item.id, collection.id, undefined, 0); documents.move(item.id, collection.id, undefined, 0);
}, },
collect: (monitor) => ({ collect: (monitor) => ({

View File

@@ -112,7 +112,9 @@ function DocumentLink(
const handleTitleChange = React.useCallback( const handleTitleChange = React.useCallback(
async (title: string) => { async (title: string) => {
if (!document) return; if (!document) {
return;
}
await documents.update( await documents.update(
{ {
id: document.id, id: document.id,
@@ -167,8 +169,12 @@ function DocumentLink(
const [{ isOverReparent, canDropToReparent }, dropToReparent] = useDrop({ const [{ isOverReparent, canDropToReparent }, dropToReparent] = useDrop({
accept: "document", accept: "document",
drop: (item: DragObject, monitor) => { drop: (item: DragObject, monitor) => {
if (monitor.didDrop()) return; if (monitor.didDrop()) {
if (!collection) return; return;
}
if (!collection) {
return;
}
documents.move(item.id, collection.id, node.id); documents.move(item.id, collection.id, node.id);
}, },
canDrop: (_item, monitor) => canDrop: (_item, monitor) =>
@@ -212,8 +218,12 @@ function DocumentLink(
const [{ isOverReorder, isDraggingAnyDocument }, dropToReorder] = useDrop({ const [{ isOverReorder, isDraggingAnyDocument }, dropToReorder] = useDrop({
accept: "document", accept: "document",
drop: (item: DragObject) => { drop: (item: DragObject) => {
if (!collection) return; if (!collection) {
if (item.id === node.id) return; return;
}
if (item.id === node.id) {
return;
}
if (expanded) { if (expanded) {
documents.move(item.id, collection.id, node.id, 0); documents.move(item.id, collection.id, node.id, 0);

View File

@@ -67,7 +67,9 @@ function StarredLink({
const handleTitleChange = React.useCallback( const handleTitleChange = React.useCallback(
async (title: string) => { async (title: string) => {
if (!document) return; if (!document) {
return;
}
await documents.update( await documents.update(
{ {
id: document.id, id: document.id,

View File

@@ -79,7 +79,9 @@ class SocketProvider extends React.Component<Props> {
views, views,
fileOperations, fileOperations,
} = this.props; } = this.props;
if (!auth.token) return; if (!auth.token) {
return;
}
this.socket.on("connect", () => { this.socket.on("connect", () => {
// immediately send current users token to the websocket backend where it // immediately send current users token to the websocket backend where it
@@ -329,8 +331,9 @@ class SocketProvider extends React.Component<Props> {
this.socket.on("fileOperations.update", async (event: any) => { this.socket.on("fileOperations.update", async (event: any) => {
const user = auth.user; const user = auth.user;
let collection = null; let collection = null;
if (event.collectionId) if (event.collectionId) {
collection = await collections.fetch(event.collectionId); collection = await collections.fetch(event.collectionId);
}
if (user) { if (user) {
fileOperations.add({ ...event, user, collection }); fileOperations.add({ ...event, user, collection });

View File

@@ -63,7 +63,9 @@ const Tabs = ({ children }: { children: React.ReactNode }) => {
const updateShadows = React.useCallback(() => { const updateShadows = React.useCallback(() => {
const c = ref.current; const c = ref.current;
if (!c) return; if (!c) {
return;
}
const scrollLeft = c.scrollLeft; const scrollLeft = c.scrollLeft;
const wrapperWidth = c.scrollWidth - c.clientWidth; const wrapperWidth = c.scrollWidth - c.clientWidth;
const fade = !!(wrapperWidth - scrollLeft !== 0); const fade = !!(wrapperWidth - scrollLeft !== 0);

View File

@@ -106,7 +106,9 @@ class CommandMenu<T = MenuItem> extends React.Component<Props<T>, State> {
} }
handleKeyDown = (event: KeyboardEvent) => { handleKeyDown = (event: KeyboardEvent) => {
if (!this.props.isActive) return; if (!this.props.isActive) {
return;
}
if (event.key === "Enter") { if (event.key === "Enter") {
event.preventDefault(); event.preventDefault();
@@ -196,8 +198,12 @@ class CommandMenu<T = MenuItem> extends React.Component<Props<T>, State> {
}; };
handleLinkInputKeydown = (event: React.KeyboardEvent<HTMLInputElement>) => { handleLinkInputKeydown = (event: React.KeyboardEvent<HTMLInputElement>) => {
if (!this.props.isActive) return; if (!this.props.isActive) {
if (!this.state.insertItem) return; return;
}
if (!this.state.insertItem) {
return;
}
if (event.key === "Enter") { if (event.key === "Enter") {
event.preventDefault(); event.preventDefault();
@@ -229,8 +235,12 @@ class CommandMenu<T = MenuItem> extends React.Component<Props<T>, State> {
}; };
handleLinkInputPaste = (event: React.ClipboardEvent<HTMLInputElement>) => { handleLinkInputPaste = (event: React.ClipboardEvent<HTMLInputElement>) => {
if (!this.props.isActive) return; if (!this.props.isActive) {
if (!this.state.insertItem) return; return;
}
if (!this.state.insertItem) {
return;
}
const href = event.clipboardData.getData("text/plain"); const href = event.clipboardData.getData("text/plain");
const matches = this.state.insertItem.matcher(href); const matches = this.state.insertItem.matcher(href);
@@ -423,7 +433,9 @@ class CommandMenu<T = MenuItem> extends React.Component<Props<T>, State> {
} }
const filtered = items.filter((item) => { const filtered = items.filter((item) => {
if (item.name === "separator") return true; if (item.name === "separator") {
return true;
}
// Some extensions may be disabled, remove corresponding menu items // Some extensions may be disabled, remove corresponding menu items
if ( if (
@@ -435,10 +447,14 @@ class CommandMenu<T = MenuItem> extends React.Component<Props<T>, State> {
} }
// If no image upload callback has been passed, filter the image block out // If no image upload callback has been passed, filter the image block out
if (!uploadImage && item.name === "image") return false; if (!uploadImage && item.name === "image") {
return false;
}
// some items (defaultHidden) are not visible until a search query exists // some items (defaultHidden) are not visible until a search query exists
if (!search) return !item.defaultHidden; if (!search) {
return !item.defaultHidden;
}
const n = search.toLowerCase(); const n = search.toLowerCase();
if (!filterable) { if (!filterable) {

View File

@@ -105,7 +105,9 @@ class LinkEditor extends React.Component<Props, State> {
save = (href: string, title?: string): void => { save = (href: string, title?: string): void => {
href = href.trim(); href = href.trim();
if (href.length === 0) return; if (href.length === 0) {
return;
}
this.discardInputValue = true; this.discardInputValue = true;
const { from, to } = this.props; const { from, to } = this.props;
@@ -163,7 +165,9 @@ class LinkEditor extends React.Component<Props, State> {
} }
case "ArrowUp": { case "ArrowUp": {
if (event.shiftKey) return; if (event.shiftKey) {
return;
}
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
const prevIndex = this.state.selectedIndex - 1; const prevIndex = this.state.selectedIndex - 1;
@@ -176,7 +180,9 @@ class LinkEditor extends React.Component<Props, State> {
case "ArrowDown": case "ArrowDown":
case "Tab": { case "Tab": {
if (event.shiftKey) return; if (event.shiftKey) {
return;
}
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
@@ -239,9 +245,13 @@ class LinkEditor extends React.Component<Props, State> {
const { onCreateLink } = this.props; const { onCreateLink } = this.props;
value = value.trim(); value = value.trim();
if (value.length === 0) return; if (value.length === 0) {
return;
}
if (onCreateLink) return onCreateLink(value); if (onCreateLink) {
return onCreateLink(value);
}
}; };
handleRemoveLink = (): void => { handleRemoveLink = (): void => {

View File

@@ -44,8 +44,12 @@ function isVisible(props: Props) {
const { view } = props; const { view } = props;
const { selection } = view.state; const { selection } = view.state;
if (!selection) return false; if (!selection) {
if (selection.empty) return false; return false;
}
if (selection.empty) {
return false;
}
if (selection instanceof NodeSelection && selection.node.type.name === "hr") { if (selection instanceof NodeSelection && selection.node.type.name === "hr") {
return true; return true;
} }
@@ -209,8 +213,12 @@ export default class SelectionToolbar extends React.Component<Props> {
// Some extensions may be disabled, remove corresponding items // Some extensions may be disabled, remove corresponding items
items = items.filter((item) => { items = items.filter((item) => {
if (item.name === "separator") return true; if (item.name === "separator") {
if (item.name && !this.props.commands[item.name]) return false; return true;
}
if (item.name && !this.props.commands[item.name]) {
return false;
}
return true; return true;
}); });

View File

@@ -233,7 +233,9 @@ export class Editor extends React.PureComponent<
this.calculateDir(); this.calculateDir();
if (this.props.readOnly) return; if (this.props.readOnly) {
return;
}
if (this.props.autoFocus) { if (this.props.autoFocus) {
this.focusAtEnd(); this.focusAtEnd();
@@ -581,11 +583,15 @@ export class Editor extends React.PureComponent<
} }
scrollToAnchor(hash: string) { scrollToAnchor(hash: string) {
if (!hash) return; if (!hash) {
return;
}
try { try {
const element = document.querySelector(hash); const element = document.querySelector(hash);
if (element) element.scrollIntoView({ behavior: "smooth" }); if (element) {
element.scrollIntoView({ behavior: "smooth" });
}
} catch (err) { } catch (err) {
// querySelector will throw an error if the hash begins with a number // querySelector will throw an error if the hash begins with a number
// or contains a period. This is protected against now by safeSlugify // or contains a period. This is protected against now by safeSlugify
@@ -595,7 +601,9 @@ export class Editor extends React.PureComponent<
} }
calculateDir = () => { calculateDir = () => {
if (!this.element) return; if (!this.element) {
return;
}
const isRTL = const isRTL =
this.props.dir === "rtl" || this.props.dir === "rtl" ||
@@ -611,7 +619,9 @@ export class Editor extends React.PureComponent<
}; };
handleChange = () => { handleChange = () => {
if (!this.props.onChange) return; if (!this.props.onChange) {
return;
}
this.props.onChange(() => { this.props.onChange(() => {
return this.value(); return this.value();
@@ -661,7 +671,9 @@ export class Editor extends React.PureComponent<
}; };
handleCloseBlockMenu = () => { handleCloseBlockMenu = () => {
if (!this.state.blockMenuOpen) return; if (!this.state.blockMenuOpen) {
return;
}
this.setState({ blockMenuOpen: false }); this.setState({ blockMenuOpen: false });
}; };

View File

@@ -87,7 +87,9 @@ if (element) {
window.addEventListener("load", async () => { window.addEventListener("load", async () => {
// installation does not use Google Analytics, or tracking is blocked on client // installation does not use Google Analytics, or tracking is blocked on client
// no point loading the rest of the analytics bundles // no point loading the rest of the analytics bundles
if (!env.GOOGLE_ANALYTICS_ID || !window.ga) return; if (!env.GOOGLE_ANALYTICS_ID || !window.ga) {
return;
}
// https://github.com/googleanalytics/autotrack/issues/137#issuecomment-305890099 // https://github.com/googleanalytics/autotrack/issues/137#issuecomment-305890099
await import( await import(
/* webpackChunkName: "autotrack" */ /* webpackChunkName: "autotrack" */

View File

@@ -298,7 +298,9 @@ export default class Document extends BaseModel {
lastRevision?: number; lastRevision?: number;
} }
) => { ) => {
if (this.isSaving) return this; if (this.isSaving) {
return this;
}
this.isSaving = true; this.isSaving = true;
try { try {
@@ -325,7 +327,9 @@ export default class Document extends BaseModel {
@action @action
save = async (options?: SaveOptions | undefined) => { save = async (options?: SaveOptions | undefined) => {
if (this.isSaving) return this; if (this.isSaving) {
return this;
}
const isCreating = !this.id; const isCreating = !this.id;
this.isSaving = true; this.isSaving = true;
@@ -422,7 +426,9 @@ export default class Document extends BaseModel {
const url = URL.createObjectURL(blob); const url = URL.createObjectURL(blob);
const a = document.createElement("a"); const a = document.createElement("a");
// Firefox support requires the anchor tag be in the DOM to trigger the dl // Firefox support requires the anchor tag be in the DOM to trigger the dl
if (document.body) document.body.appendChild(a); if (document.body) {
document.body.appendChild(a);
}
a.href = url; a.href = url;
a.download = `${this.titleWithDefault}.md`; a.download = `${this.titleWithDefault}.md`;
a.click(); a.click();

View File

@@ -80,7 +80,9 @@ class AddGroupsToCollection extends React.Component<Props> {
render() { render() {
const { groups, policies, collection, auth, t } = this.props; const { groups, policies, collection, auth, t } = this.props;
const { user, team } = auth; const { user, team } = auth;
if (!user || !team) return null; if (!user || !team) {
return null;
}
const can = policies.abilities(team.id); const can = policies.abilities(team.id);

View File

@@ -77,7 +77,9 @@ class AddPeopleToCollection extends React.Component<Props> {
render() { render() {
const { users, collection, auth, t } = this.props; const { users, collection, auth, t } = this.props;
const { user, team } = auth; const { user, team } = auth;
if (!user || !team) return null; if (!user || !team) {
return null;
}
return ( return (
<Flex column> <Flex column>

View File

@@ -194,7 +194,9 @@ class DocumentScene extends React.Component<Props> {
}; };
goToMove = (ev: KeyboardEvent) => { goToMove = (ev: KeyboardEvent) => {
if (!this.props.readOnly) return; if (!this.props.readOnly) {
return;
}
ev.preventDefault(); ev.preventDefault();
const { document, abilities } = this.props; const { document, abilities } = this.props;
@@ -204,7 +206,9 @@ class DocumentScene extends React.Component<Props> {
}; };
goToEdit = (ev: KeyboardEvent) => { goToEdit = (ev: KeyboardEvent) => {
if (!this.props.readOnly) return; if (!this.props.readOnly) {
return;
}
ev.preventDefault(); ev.preventDefault();
const { document, abilities } = this.props; const { document, abilities } = this.props;
@@ -214,8 +218,12 @@ class DocumentScene extends React.Component<Props> {
}; };
goToHistory = (ev: KeyboardEvent) => { goToHistory = (ev: KeyboardEvent) => {
if (!this.props.readOnly) return; if (!this.props.readOnly) {
if (ev.ctrlKey) return; return;
}
if (ev.ctrlKey) {
return;
}
ev.preventDefault(); ev.preventDefault();
const { document, location } = this.props; const { document, location } = this.props;
@@ -229,7 +237,9 @@ class DocumentScene extends React.Component<Props> {
onPublish = (ev: React.MouseEvent | KeyboardEvent) => { onPublish = (ev: React.MouseEvent | KeyboardEvent) => {
ev.preventDefault(); ev.preventDefault();
const { document } = this.props; const { document } = this.props;
if (document.publishedAt) return; if (document.publishedAt) {
return;
}
this.onSave({ this.onSave({
publish: true, publish: true,
done: true, done: true,
@@ -237,7 +247,9 @@ class DocumentScene extends React.Component<Props> {
}; };
onToggleTableOfContents = (ev: KeyboardEvent) => { onToggleTableOfContents = (ev: KeyboardEvent) => {
if (!this.props.readOnly) return; if (!this.props.readOnly) {
return;
}
ev.preventDefault(); ev.preventDefault();
const { ui } = this.props; const { ui } = this.props;
@@ -257,13 +269,17 @@ class DocumentScene extends React.Component<Props> {
) => { ) => {
const { document, auth } = this.props; const { document, auth } = this.props;
// prevent saves when we are already saving // prevent saves when we are already saving
if (document.isSaving) return; if (document.isSaving) {
return;
}
// get the latest version of the editor text value // get the latest version of the editor text value
const text = this.getEditorText ? this.getEditorText() : document.text; const text = this.getEditorText ? this.getEditorText() : document.text;
// prevent save before anything has been written (single hash is empty doc) // prevent save before anything has been written (single hash is empty doc)
if (text.trim() === "" && document.title.trim() === "") return; if (text.trim() === "" && document.title.trim() === "") {
return;
}
document.text = text; document.text = text;
document.tasks = getTasks(document.text); document.tasks = getTasks(document.text);

View File

@@ -22,7 +22,9 @@ function PublicReferences(props: Props) {
let result: NavigationNode[]; let result: NavigationNode[];
function findChildren(node?: NavigationNode) { function findChildren(node?: NavigationNode) {
if (!node) return; if (!node) {
return;
}
if (node.id === documentId) { if (node.id === documentId) {
result = node.children; result = node.children;

View File

@@ -58,7 +58,9 @@ export default class SocketPresence extends React.Component<Props> {
}; };
emitJoin = () => { emitJoin = () => {
if (!this.context) return; if (!this.context) {
return;
}
this.context.emit("join", { this.context.emit("join", {
documentId: this.props.documentId, documentId: this.props.documentId,
isEditing: this.props.isEditing, isEditing: this.props.isEditing,
@@ -66,7 +68,9 @@ export default class SocketPresence extends React.Component<Props> {
}; };
emitPresence = () => { emitPresence = () => {
if (!this.context) return; if (!this.context) {
return;
}
this.context.emit("presence", { this.context.emit("presence", {
documentId: this.props.documentId, documentId: this.props.documentId,
isEditing: this.props.isEditing, isEditing: this.props.isEditing,

View File

@@ -76,7 +76,9 @@ class AddPeopleToGroup extends React.Component<Props> {
render() { render() {
const { users, group, auth, t } = this.props; const { users, group, auth, t } = this.props;
const { user, team } = auth; const { user, team } = auth;
if (!user || !team) return null; if (!user || !team) {
return null;
}
return ( return (
<Flex column> <Flex column>

View File

@@ -177,14 +177,18 @@ class Search extends React.Component<Props> {
get title() { get title() {
const query = this.query; const query = this.query;
const title = this.props.t("Search"); const title = this.props.t("Search");
if (query) return `${query} ${title}`; if (query) {
return `${query} ${title}`;
}
return title; return title;
} }
@action @action
loadMoreResults = async () => { loadMoreResults = async () => {
// Don't paginate if there aren't more results or were in the middle of fetching // Don't paginate if there aren't more results or were in the middle of fetching
if (!this.allowLoadMore || this.isLoading) return; if (!this.allowLoadMore || this.isLoading) {
return;
}
// Fetch more results // Fetch more results
await this.fetchResults(); await this.fetchResults();
@@ -330,7 +334,9 @@ class Search extends React.Component<Props> {
> >
{results.map((result, index) => { {results.map((result, index) => {
const document = documents.data.get(result.document.id); const document = documents.data.get(result.document.id);
if (!document) return null; if (!document) {
return null;
}
return ( return (
<DocumentListItem <DocumentListItem
ref={(ref) => index === 0 && this.setFirstDocumentRef(ref)} ref={(ref) => index === 0 && this.setFirstDocumentRef(ref)}

View File

@@ -30,7 +30,9 @@ function UserProfile(props: Props) {
const currentUser = useCurrentUser(); const currentUser = useCurrentUser();
const history = useHistory(); const history = useHistory();
const { user, ...rest } = props; const { user, ...rest } = props;
if (!user) return null; if (!user) {
return null;
}
const isCurrentUser = currentUser.id === user.id; const isCurrentUser = currentUser.id === user.id;
return ( return (

View File

@@ -89,7 +89,9 @@ export default class AuthStore {
event.newValue event.newValue
); );
// data may be null if key is deleted in localStorage // data may be null if key is deleted in localStorage
if (!data) return; if (!data) {
return;
}
// If we're not signed in then hydrate from the received data, otherwise if // If we're not signed in then hydrate from the received data, otherwise if
// we are signed in and the received data contains no user then sign out // we are signed in and the received data contains no user then sign out

View File

@@ -111,7 +111,9 @@ export default class BaseStore<T extends BaseModel> {
} }
save(params: Partial<T>): Promise<T> { save(params: Partial<T>): Promise<T> {
if (params.id) return this.update(params); if (params.id) {
return this.update(params);
}
return this.create(params); return this.create(params);
} }
@@ -195,7 +197,9 @@ export default class BaseStore<T extends BaseModel> {
} }
const item = this.data.get(id); const item = this.data.get(id);
if (item && !options.force) return item; if (item && !options.force) {
return item;
}
this.isFetching = true; this.isFetching = true;
try { try {

View File

@@ -141,7 +141,9 @@ export default class CollectionsStore extends BaseStore<Collection> {
@action @action
async fetch(id: string, options: Record<string, any> = {}): Promise<any> { async fetch(id: string, options: Record<string, any> = {}): Promise<any> {
const item = this.get(id) || this.getByUrl(id); const item = this.get(id) || this.getByUrl(id);
if (item && !options.force) return item; if (item && !options.force) {
return item;
}
this.isFetching = true; this.isFetching = true;
try { try {

View File

@@ -398,7 +398,9 @@ export default class DocumentsStore extends BaseStore<Document> {
const results: SearchResult[] = compact( const results: SearchResult[] = compact(
res.data.map((result: SearchResult) => { res.data.map((result: SearchResult) => {
const document = this.data.get(result.document.id); const document = this.data.get(result.document.id);
if (!document) return null; if (!document) {
return null;
}
return { return {
ranking: result.ranking, ranking: result.ranking,
context: result.context, context: result.context,
@@ -450,7 +452,9 @@ export default class DocumentsStore extends BaseStore<Document> {
document: Document; document: Document;
sharedTree?: NavigationNode; sharedTree?: NavigationNode;
}> => { }> => {
if (!options.prefetch) this.isFetching = true; if (!options.prefetch) {
this.isFetching = true;
}
try { try {
const doc: Document | null | undefined = const doc: Document | null | undefined =
@@ -540,7 +544,9 @@ export default class DocumentsStore extends BaseStore<Document> {
}); });
invariant(res && res.data, "Data should be available"); invariant(res && res.data, "Data should be available");
const collection = this.getCollectionForDocument(document); const collection = this.getCollectionForDocument(document);
if (collection) collection.refresh(); if (collection) {
collection.refresh();
}
this.addPolicies(res.policies); this.addPolicies(res.policies);
return this.add(res.data); return this.add(res.data);
}; };
@@ -635,7 +641,9 @@ export default class DocumentsStore extends BaseStore<Document> {
// Because the collection object contains the url and title // Because the collection object contains the url and title
// we need to ensure they are updated there as well. // we need to ensure they are updated there as well.
const collection = this.getCollectionForDocument(document); const collection = this.getCollectionForDocument(document);
if (collection) collection.updateDocument(document); if (collection) {
collection.updateDocument(document);
}
return document; return document;
} }
@@ -656,7 +664,9 @@ export default class DocumentsStore extends BaseStore<Document> {
} }
const collection = this.getCollectionForDocument(document); const collection = this.getCollectionForDocument(document);
if (collection) collection.refresh(); if (collection) {
collection.refresh();
}
} }
@action @action
@@ -670,7 +680,9 @@ export default class DocumentsStore extends BaseStore<Document> {
this.addPolicies(res.policies); this.addPolicies(res.policies);
}); });
const collection = this.getCollectionForDocument(document); const collection = this.getCollectionForDocument(document);
if (collection) collection.refresh(); if (collection) {
collection.refresh();
}
}; };
@action @action
@@ -692,7 +704,9 @@ export default class DocumentsStore extends BaseStore<Document> {
this.addPolicies(res.policies); this.addPolicies(res.policies);
}); });
const collection = this.getCollectionForDocument(document); const collection = this.getCollectionForDocument(document);
if (collection) collection.refresh(); if (collection) {
collection.refresh();
}
}; };
@action @action
@@ -706,7 +720,9 @@ export default class DocumentsStore extends BaseStore<Document> {
this.addPolicies(res.policies); this.addPolicies(res.policies);
}); });
const collection = this.getCollectionForDocument(document); const collection = this.getCollectionForDocument(document);
if (collection) collection.refresh(); if (collection) {
collection.refresh();
}
}; };
star = async (document: Document) => { star = async (document: Document) => {

View File

@@ -48,7 +48,9 @@ export default class GroupsStore extends BaseStore<Group> {
const groups = filter(this.orderedData, (group) => const groups = filter(this.orderedData, (group) =>
groupIds.includes(group.id) groupIds.includes(group.id)
); );
if (!query) return groups; if (!query) {
return groups;
}
return queriedGroups(groups, query); return queriedGroups(groups, query);
}; };
@@ -62,7 +64,9 @@ export default class GroupsStore extends BaseStore<Group> {
this.orderedData, this.orderedData,
(group) => !groupIds.includes(group.id) (group) => !groupIds.includes(group.id)
); );
if (!query) return groups; if (!query) {
return groups;
}
return queriedGroups(groups, query); return queriedGroups(groups, query);
}; };
} }

View File

@@ -39,7 +39,9 @@ export default class SharesStore extends BaseStore<Share> {
@action @action
async create(params: Record<string, any>) { async create(params: Record<string, any>) {
const item = this.getByDocumentId(params.documentId); const item = this.getByDocumentId(params.documentId);
if (item) return item; if (item) {
return item;
}
return super.create(params); return super.create(params);
} }
@@ -49,7 +51,9 @@ export default class SharesStore extends BaseStore<Share> {
options: Record<string, any> = {} options: Record<string, any> = {}
): Promise<any> { ): Promise<any> {
const item = this.getByDocumentId(documentId); const item = this.getByDocumentId(documentId);
if (item && !options.force) return item; if (item && !options.force) {
return item;
}
this.isFetching = true; this.isFetching = true;
try { try {
@@ -58,7 +62,9 @@ export default class SharesStore extends BaseStore<Share> {
apiVersion: 2, apiVersion: 2,
}); });
if (isUndefined(res)) return; if (isUndefined(res)) {
return;
}
invariant(res && res.data, "Data should be available"); invariant(res && res.data, "Data should be available");
this.addPolicies(res.policies); this.addPolicies(res.policies);
return res.data.shares.map(this.add); return res.data.shares.map(this.add);
@@ -69,10 +75,14 @@ export default class SharesStore extends BaseStore<Share> {
getByDocumentParents = (documentId: string): Share | null | undefined => { getByDocumentParents = (documentId: string): Share | null | undefined => {
const document = this.rootStore.documents.get(documentId); const document = this.rootStore.documents.get(documentId);
if (!document) return; if (!document) {
return;
}
const collection = this.rootStore.collections.get(document.collectionId); const collection = this.rootStore.collections.get(document.collectionId);
if (!collection) return; if (!collection) {
return;
}
const parentIds = collection const parentIds = collection
.pathToDocument(documentId) .pathToDocument(documentId)

View File

@@ -16,7 +16,9 @@ export default class ToastsStore {
type: "info", type: "info",
} }
) => { ) => {
if (!message) return; if (!message) {
return;
}
const lastToast = this.toasts.get(this.lastToastId); const lastToast = this.toasts.get(this.lastToastId);
if (lastToast && lastToast.message === message) { if (lastToast && lastToast.message === message) {

View File

@@ -211,7 +211,9 @@ export default class UsersStore extends BaseStore<User> {
this.activeOrInvited, this.activeOrInvited,
(user) => !userIds.includes(user.id) (user) => !userIds.includes(user.id)
); );
if (!query) return users; if (!query) {
return users;
}
return queriedUsers(users, query); return queriedUsers(users, query);
}; };
@@ -224,7 +226,9 @@ export default class UsersStore extends BaseStore<User> {
const users = filter(this.activeOrInvited, (user) => const users = filter(this.activeOrInvited, (user) =>
userIds.includes(user.id) userIds.includes(user.id)
); );
if (!query) return users; if (!query) {
return users;
}
return queriedUsers(users, query); return queriedUsers(users, query);
}; };
@@ -238,7 +242,9 @@ export default class UsersStore extends BaseStore<User> {
this.activeOrInvited, this.activeOrInvited,
(user) => !userIds.includes(user.id) (user) => !userIds.includes(user.id)
); );
if (!query) return users; if (!query) {
return users;
}
return queriedUsers(users, query); return queriedUsers(users, query);
}; };
@@ -251,7 +257,9 @@ export default class UsersStore extends BaseStore<User> {
const users = filter(this.activeOrInvited, (user) => const users = filter(this.activeOrInvited, (user) =>
userIds.includes(user.id) userIds.includes(user.id)
); );
if (!query) return users; if (!query) {
return users;
}
return queriedUsers(users, query); return queriedUsers(users, query);
}; };

View File

@@ -28,7 +28,9 @@ export default class ViewsStore extends BaseStore<View> {
this.orderedData, this.orderedData,
(view) => view.documentId === documentId && view.user.id === userId (view) => view.documentId === documentId && view.user.id === userId
); );
if (!view) return; if (!view) {
return;
}
view.touch(); view.touch();
} }
} }

View File

@@ -31,7 +31,9 @@ export function groupSettingsPath(): string {
} }
export function collectionUrl(url: string, section?: string): string { export function collectionUrl(url: string, section?: string): string {
if (section) return `${url}/${section}`; if (section) {
return `${url}/${section}`;
}
return url; return url;
} }
@@ -60,7 +62,9 @@ export function documentMoveUrl(doc: Document): string {
export function documentHistoryUrl(doc: Document, revisionId?: string): string { export function documentHistoryUrl(doc: Document, revisionId?: string): string {
let base = `${doc.url}/history`; let base = `${doc.url}/history`;
if (revisionId) base += `/${revisionId}`; if (revisionId) {
base += `/${revisionId}`;
}
return base; return base;
} }

View File

@@ -1,7 +1,9 @@
import { parseDomain } from "@shared/utils/domains"; import { parseDomain } from "@shared/utils/domains";
export function isInternalUrl(href: string) { export function isInternalUrl(href: string) {
if (href[0] === "/") return true; if (href[0] === "/") {
return true;
}
const outline = parseDomain(window.location.href); const outline = parseDomain(window.location.href);
const parsed = parseDomain(href); const parsed = parseDomain(href);
@@ -19,7 +21,9 @@ export function isInternalUrl(href: string) {
} }
export function isHash(href: string) { export function isHash(href: string) {
if (href[0] === "#") return true; if (href[0] === "#") {
return true;
}
try { try {
const outline = new URL(window.location.href); const outline = new URL(window.location.href);

View File

@@ -27,7 +27,9 @@ router.get("/:type/:format", async (ctx) => {
} }
} }
if (!mailerOutput) return; if (!mailerOutput) {
return;
}
if (ctx.params.format === "text") { if (ctx.params.format === "text") {
// @ts-expect-error ts-migrate(2339) FIXME: Property 'text' does not exist on type 'never'. // @ts-expect-error ts-migrate(2339) FIXME: Property 'text' does not exist on type 'never'.

View File

@@ -92,8 +92,12 @@ class Attachment extends BaseModel {
query: FindOptions<Attachment> query: FindOptions<Attachment>
) => Promise<void> ) => Promise<void>
) { ) {
if (!query.offset) query.offset = 0; if (!query.offset) {
if (!query.limit) query.limit = 10; query.offset = 0;
}
if (!query.limit) {
query.limit = 10;
}
let results; let results;
do { do {

View File

@@ -186,7 +186,9 @@ class Collection extends ParanoidModel {
// getters // getters
get url(): string { get url(): string {
if (!this.name) return `/collection/untitled-${this.urlId}`; if (!this.name) {
return `/collection/untitled-${this.urlId}`;
}
return `/collection/${slugify(this.name)}-${this.urlId}`; return `/collection/${slugify(this.name)}-${this.urlId}`;
} }
@@ -354,7 +356,9 @@ class Collection extends ParanoidModel {
} }
getDocumentTree = (documentId: string): NavigationNode | null => { getDocumentTree = (documentId: string): NavigationNode | null => {
if (!this.documentStructure) return null; if (!this.documentStructure) {
return null;
}
const sort: Sort = this.sort || { const sort: Sort = this.sort || {
field: "title", field: "title",
direction: "asc", direction: "asc",
@@ -386,7 +390,9 @@ class Collection extends ParanoidModel {
loopChildren(this.documentStructure); loopChildren(this.documentStructure);
// if the document is a draft loopChildren will not find it in the structure // if the document is a draft loopChildren will not find it in the structure
if (!result) return null; if (!result) {
return null;
}
return { return {
...result, ...result,
@@ -548,7 +554,9 @@ class Collection extends ParanoidModel {
* Update document's title and url in the documentStructure * Update document's title and url in the documentStructure
*/ */
updateDocument = async function (updatedDocument: Document) { updateDocument = async function (updatedDocument: Document) {
if (!this.documentStructure) return; if (!this.documentStructure) {
return;
}
let transaction; let transaction;
try { try {

View File

@@ -127,7 +127,9 @@ export const DOCUMENT_VERSION = 2;
], ],
}, },
withViews: (userId: string) => { withViews: (userId: string) => {
if (!userId) return {}; if (!userId) {
return {};
}
return { return {
include: [ include: [
{ {
@@ -215,7 +217,9 @@ class Document extends ParanoidModel {
// getters // getters
get url() { get url() {
if (!this.title) return `/doc/untitled-${this.urlId}`; if (!this.title) {
return `/doc/untitled-${this.urlId}`;
}
const slugifiedTitle = slugify(this.title); const slugifiedTitle = slugify(this.title);
return `/doc/${slugifiedTitle}-${this.urlId}`; return `/doc/${slugifiedTitle}-${this.urlId}`;
} }
@@ -721,7 +725,9 @@ class Document extends ParanoidModel {
}; };
publish = async (userId: string, options?: FindOptions<Document>) => { publish = async (userId: string, options?: FindOptions<Document>) => {
if (this.publishedAt) return this.save(options); if (this.publishedAt) {
return this.save(options);
}
if (!this.template) { if (!this.template) {
const collection = await Collection.findByPk(this.collectionId); const collection = await Collection.findByPk(this.collectionId);
@@ -735,7 +741,9 @@ class Document extends ParanoidModel {
}; };
unpublish = async (userId: string, options?: FindOptions<Document>) => { unpublish = async (userId: string, options?: FindOptions<Document>) => {
if (!this.publishedAt) return this; if (!this.publishedAt) {
return this;
}
const collection = await this.$get("collection"); const collection = await this.$get("collection");
await collection?.removeDocumentInStructure(this); await collection?.removeDocumentInStructure(this);
@@ -777,7 +785,9 @@ class Document extends ParanoidModel {
}, },
}, },
}); });
if (!parent) this.parentDocumentId = null; if (!parent) {
this.parentDocumentId = null;
}
} }
if (!this.template && collection) { if (!this.template && collection) {

View File

@@ -58,7 +58,9 @@ class Group extends ParanoidModel {
@AfterDestroy @AfterDestroy
static async deleteGroupUsers(model: Group) { static async deleteGroupUsers(model: Group) {
if (!model.deletedAt) return; if (!model.deletedAt) {
return;
}
await GroupUser.destroy({ await GroupUser.destroy({
where: { where: {
groupId: model.id, groupId: model.id,

View File

@@ -125,7 +125,9 @@ class Team extends ParanoidModel {
requestedSubdomain: string, requestedSubdomain: string,
options = {} options = {}
) { ) {
if (this.subdomain) return this.subdomain; if (this.subdomain) {
return this.subdomain;
}
let subdomain = requestedSubdomain; let subdomain = requestedSubdomain;
let append = 0; let append = 0;
@@ -234,7 +236,9 @@ class Team extends ParanoidModel {
`avatars/${model.id}/${uuidv4()}`, `avatars/${model.id}/${uuidv4()}`,
"public-read" "public-read"
); );
if (newUrl) model.avatarUrl = newUrl; if (newUrl) {
model.avatarUrl = newUrl;
}
} catch (err) { } catch (err) {
Logger.error("Error uploading avatar to S3", err, { Logger.error("Error uploading avatar to S3", err, {
url: avatarUrl, url: avatarUrl,

View File

@@ -360,7 +360,9 @@ class User extends ParanoidModel {
`avatars/${model.id}/${uuidv4()}`, `avatars/${model.id}/${uuidv4()}`,
"public-read" "public-read"
); );
if (newUrl) model.avatarUrl = newUrl; if (newUrl) {
model.avatarUrl = newUrl;
}
} catch (err) { } catch (err) {
Logger.error("Couldn't upload user avatar image to S3", err, { Logger.error("Couldn't upload user avatar image to S3", err, {
url: avatarUrl, url: avatarUrl,
@@ -452,8 +454,12 @@ class User extends ParanoidModel {
query: FindOptions<User>, query: FindOptions<User>,
callback: (users: Array<User>, query: FindOptions<User>) => Promise<void> callback: (users: Array<User>, query: FindOptions<User>) => Promise<void>
) { ) {
if (!query.offset) query.offset = 0; if (!query.offset) {
if (!query.limit) query.limit = 10; query.offset = 0;
}
if (!query.limit) {
query.limit = 10;
}
let results; let results;
do { do {

View File

@@ -2,12 +2,18 @@ import { ApiKey, User, Team } from "@server/models";
import { allow } from "./cancan"; import { allow } from "./cancan";
allow(User, "createApiKey", Team, (user, team) => { allow(User, "createApiKey", Team, (user, team) => {
if (!team || user.isViewer || user.teamId !== team.id) return false; if (!team || user.isViewer || user.teamId !== team.id) {
return false;
}
return true; return true;
}); });
allow(User, ["read", "update", "delete"], ApiKey, (user, apiKey) => { allow(User, ["read", "update", "delete"], ApiKey, (user, apiKey) => {
if (!apiKey) return false; if (!apiKey) {
if (user.isViewer) return false; return false;
}
if (user.isViewer) {
return false;
}
return user && user.id === apiKey.userId; return user && user.id === apiKey.userId;
}); });

View File

@@ -2,21 +2,37 @@ import { Attachment, User, Team } from "@server/models";
import { allow } from "./cancan"; import { allow } from "./cancan";
allow(User, "createAttachment", Team, (user, team) => { allow(User, "createAttachment", Team, (user, team) => {
if (!team || user.isViewer || user.teamId !== team.id) return false; if (!team || user.isViewer || user.teamId !== team.id) {
return false;
}
return true; return true;
}); });
allow(User, "read", Attachment, (actor, attachment) => { allow(User, "read", Attachment, (actor, attachment) => {
if (!attachment || attachment.teamId !== actor.teamId) return false; if (!attachment || attachment.teamId !== actor.teamId) {
if (actor.isAdmin) return true; return false;
if (actor.id === attachment.userId) return true; }
if (actor.isAdmin) {
return true;
}
if (actor.id === attachment.userId) {
return true;
}
return false; return false;
}); });
allow(User, "delete", Attachment, (actor, attachment) => { allow(User, "delete", Attachment, (actor, attachment) => {
if (actor.isViewer) return false; if (actor.isViewer) {
if (!attachment || attachment.teamId !== actor.teamId) return false; return false;
if (actor.isAdmin) return true; }
if (actor.id === attachment.userId) return true; if (!attachment || attachment.teamId !== actor.teamId) {
return false;
}
if (actor.isAdmin) {
return true;
}
if (actor.id === attachment.userId) {
return true;
}
return false; return false;
}); });

View File

@@ -3,8 +3,12 @@ import { AdminRequiredError } from "../errors";
import { allow } from "./cancan"; import { allow } from "./cancan";
allow(User, "createAuthenticationProvider", Team, (actor, team) => { allow(User, "createAuthenticationProvider", Team, (actor, team) => {
if (!team || actor.teamId !== team.id) return false; if (!team || actor.teamId !== team.id) {
if (actor.isAdmin) return true; return false;
}
if (actor.isAdmin) {
return true;
}
throw AdminRequiredError(); throw AdminRequiredError();
}); });
@@ -24,8 +28,12 @@ allow(
AuthenticationProvider, AuthenticationProvider,
(actor, authenticationProvider) => { (actor, authenticationProvider) => {
if (actor.teamId !== authenticationProvider?.teamId) return false; if (actor.teamId !== authenticationProvider?.teamId) {
if (actor.isAdmin) return true; return false;
}
if (actor.isAdmin) {
return true;
}
throw AdminRequiredError(); throw AdminRequiredError();
} }

View File

@@ -5,27 +5,41 @@ import { AdminRequiredError } from "../errors";
import { allow } from "./cancan"; import { allow } from "./cancan";
allow(User, "createCollection", Team, (user, team) => { allow(User, "createCollection", Team, (user, team) => {
if (!team || user.isViewer || user.teamId !== team.id) return false; if (!team || user.isViewer || user.teamId !== team.id) {
return false;
}
return true; return true;
}); });
allow(User, "importCollection", Team, (actor, team) => { allow(User, "importCollection", Team, (actor, team) => {
if (!team || actor.teamId !== team.id) return false; if (!team || actor.teamId !== team.id) {
if (actor.isAdmin) return true; return false;
}
if (actor.isAdmin) {
return true;
}
throw AdminRequiredError(); throw AdminRequiredError();
}); });
allow(User, "move", Collection, (user, collection) => { allow(User, "move", Collection, (user, collection) => {
if (!collection || user.teamId !== collection.teamId) return false; if (!collection || user.teamId !== collection.teamId) {
if (collection.deletedAt) return false; return false;
if (user.isAdmin) return true; }
if (collection.deletedAt) {
return false;
}
if (user.isAdmin) {
return true;
}
throw AdminRequiredError(); throw AdminRequiredError();
}); });
allow(User, "read", Collection, (user, collection) => { allow(User, "read", Collection, (user, collection) => {
if (!collection || user.teamId !== collection.teamId) return false; if (!collection || user.teamId !== collection.teamId) {
return false;
}
if (!collection.permission) { if (!collection.permission) {
invariant( invariant(
@@ -45,9 +59,15 @@ allow(User, "read", Collection, (user, collection) => {
}); });
allow(User, "share", Collection, (user, collection) => { allow(User, "share", Collection, (user, collection) => {
if (user.isViewer) return false; if (user.isViewer) {
if (!collection || user.teamId !== collection.teamId) return false; return false;
if (!collection.sharing) return false; }
if (!collection || user.teamId !== collection.teamId) {
return false;
}
if (!collection.sharing) {
return false;
}
if (collection.permission !== "read_write") { if (collection.permission !== "read_write") {
invariant( invariant(
@@ -67,8 +87,12 @@ allow(User, "share", Collection, (user, collection) => {
}); });
allow(User, ["publish", "update"], Collection, (user, collection) => { allow(User, ["publish", "update"], Collection, (user, collection) => {
if (user.isViewer) return false; if (user.isViewer) {
if (!collection || user.teamId !== collection.teamId) return false; return false;
}
if (!collection || user.teamId !== collection.teamId) {
return false;
}
if (collection.permission !== "read_write") { if (collection.permission !== "read_write") {
invariant( invariant(
@@ -88,8 +112,12 @@ allow(User, ["publish", "update"], Collection, (user, collection) => {
}); });
allow(User, "delete", Collection, (user, collection) => { allow(User, "delete", Collection, (user, collection) => {
if (user.isViewer) return false; if (user.isViewer) {
if (!collection || user.teamId !== collection.teamId) return false; return false;
}
if (!collection || user.teamId !== collection.teamId) {
return false;
}
if (collection.permission !== "read_write") { if (collection.permission !== "read_write") {
invariant( invariant(
@@ -105,8 +133,12 @@ allow(User, "delete", Collection, (user, collection) => {
); );
} }
if (user.isAdmin) return true; if (user.isAdmin) {
if (user.id === collection.createdById) return true; return true;
}
if (user.id === collection.createdById) {
return true;
}
throw AdminRequiredError(); throw AdminRequiredError();
}); });

View File

@@ -4,12 +4,16 @@ import { NavigationNode } from "~/types";
import { allow, _cannot as cannot } from "./cancan"; import { allow, _cannot as cannot } from "./cancan";
allow(User, "createDocument", Team, (user, team) => { allow(User, "createDocument", Team, (user, team) => {
if (!team || user.isViewer || user.teamId !== team.id) return false; if (!team || user.isViewer || user.teamId !== team.id) {
return false;
}
return true; return true;
}); });
allow(User, ["read", "download"], Document, (user, document) => { allow(User, ["read", "download"], Document, (user, document) => {
if (!document) return false; if (!document) {
return false;
}
// existence of collection option is not required here to account for share tokens // existence of collection option is not required here to account for share tokens
if (document.collection && cannot(user, "read", document.collection)) { if (document.collection && cannot(user, "read", document.collection)) {
@@ -20,33 +24,55 @@ allow(User, ["read", "download"], Document, (user, document) => {
}); });
allow(User, "star", Document, (user, document) => { allow(User, "star", Document, (user, document) => {
if (!document) return false; if (!document) {
if (document.archivedAt) return false; return false;
if (document.deletedAt) return false; }
if (document.template) return false; if (document.archivedAt) {
return false;
}
if (document.deletedAt) {
return false;
}
if (document.template) {
return false;
}
invariant( invariant(
document.collection, document.collection,
"collection is missing, did you forget to include in the query scope?" "collection is missing, did you forget to include in the query scope?"
); );
if (cannot(user, "read", document.collection)) return false; if (cannot(user, "read", document.collection)) {
return false;
}
return user.teamId === document.teamId; return user.teamId === document.teamId;
}); });
allow(User, "unstar", Document, (user, document) => { allow(User, "unstar", Document, (user, document) => {
if (!document) return false; if (!document) {
if (document.template) return false; return false;
}
if (document.template) {
return false;
}
invariant( invariant(
document.collection, document.collection,
"collection is missing, did you forget to include in the query scope?" "collection is missing, did you forget to include in the query scope?"
); );
if (cannot(user, "read", document.collection)) return false; if (cannot(user, "read", document.collection)) {
return false;
}
return user.teamId === document.teamId; return user.teamId === document.teamId;
}); });
allow(User, "share", Document, (user, document) => { allow(User, "share", Document, (user, document) => {
if (!document) return false; if (!document) {
if (document.archivedAt) return false; return false;
if (document.deletedAt) return false; }
if (document.archivedAt) {
return false;
}
if (document.deletedAt) {
return false;
}
invariant( invariant(
document.collection, document.collection,
"collection is missing, did you forget to include in the query scope?" "collection is missing, did you forget to include in the query scope?"
@@ -60,9 +86,15 @@ allow(User, "share", Document, (user, document) => {
}); });
allow(User, "update", Document, (user, document) => { allow(User, "update", Document, (user, document) => {
if (!document) return false; if (!document) {
if (document.archivedAt) return false; return false;
if (document.deletedAt) return false; }
if (document.archivedAt) {
return false;
}
if (document.deletedAt) {
return false;
}
if (cannot(user, "update", document.collection)) { if (cannot(user, "update", document.collection)) {
return false; return false;
@@ -72,60 +104,110 @@ allow(User, "update", Document, (user, document) => {
}); });
allow(User, "createChildDocument", Document, (user, document) => { allow(User, "createChildDocument", Document, (user, document) => {
if (!document) return false; if (!document) {
if (document.archivedAt) return false; return false;
if (document.deletedAt) return false; }
if (document.template) return false; if (document.archivedAt) {
if (!document.publishedAt) return false; return false;
}
if (document.deletedAt) {
return false;
}
if (document.template) {
return false;
}
if (!document.publishedAt) {
return false;
}
invariant( invariant(
document.collection, document.collection,
"collection is missing, did you forget to include in the query scope?" "collection is missing, did you forget to include in the query scope?"
); );
if (cannot(user, "update", document.collection)) return false; if (cannot(user, "update", document.collection)) {
return false;
}
return user.teamId === document.teamId; return user.teamId === document.teamId;
}); });
allow(User, "move", Document, (user, document) => { allow(User, "move", Document, (user, document) => {
if (!document) return false; if (!document) {
if (document.archivedAt) return false; return false;
if (document.deletedAt) return false; }
if (!document.publishedAt) return false; if (document.archivedAt) {
return false;
}
if (document.deletedAt) {
return false;
}
if (!document.publishedAt) {
return false;
}
invariant( invariant(
document.collection, document.collection,
"collection is missing, did you forget to include in the query scope?" "collection is missing, did you forget to include in the query scope?"
); );
if (cannot(user, "update", document.collection)) return false; if (cannot(user, "update", document.collection)) {
return false;
}
return user.teamId === document.teamId; return user.teamId === document.teamId;
}); });
allow(User, ["pin", "unpin"], Document, (user, document) => { allow(User, ["pin", "unpin"], Document, (user, document) => {
if (!document) return false; if (!document) {
if (document.archivedAt) return false; return false;
if (document.deletedAt) return false; }
if (document.template) return false; if (document.archivedAt) {
if (!document.publishedAt) return false; return false;
}
if (document.deletedAt) {
return false;
}
if (document.template) {
return false;
}
if (!document.publishedAt) {
return false;
}
invariant( invariant(
document.collection, document.collection,
"collection is missing, did you forget to include in the query scope?" "collection is missing, did you forget to include in the query scope?"
); );
if (cannot(user, "update", document.collection)) return false; if (cannot(user, "update", document.collection)) {
return false;
}
return user.teamId === document.teamId; return user.teamId === document.teamId;
}); });
allow(User, ["pinToHome"], Document, (user, document) => { allow(User, ["pinToHome"], Document, (user, document) => {
if (!document) return false; if (!document) {
if (document.archivedAt) return false; return false;
if (document.deletedAt) return false; }
if (document.template) return false; if (document.archivedAt) {
if (!document.publishedAt) return false; return false;
}
if (document.deletedAt) {
return false;
}
if (document.template) {
return false;
}
if (!document.publishedAt) {
return false;
}
return user.teamId === document.teamId && user.isAdmin; return user.teamId === document.teamId && user.isAdmin;
}); });
allow(User, "delete", Document, (user, document) => { allow(User, "delete", Document, (user, document) => {
if (!document) return false; if (!document) {
if (document.deletedAt) return false; return false;
if (user.isViewer) return false; }
if (document.deletedAt) {
return false;
}
if (user.isViewer) {
return false;
}
// allow deleting document without a collection // allow deleting document without a collection
if (document.collection && cannot(user, "update", document.collection)) { if (document.collection && cannot(user, "update", document.collection)) {
@@ -145,9 +227,15 @@ allow(User, "delete", Document, (user, document) => {
}); });
allow(User, "permanentDelete", Document, (user, document) => { allow(User, "permanentDelete", Document, (user, document) => {
if (!document) return false; if (!document) {
if (!document.deletedAt) return false; return false;
if (user.isViewer) return false; }
if (!document.deletedAt) {
return false;
}
if (user.isViewer) {
return false;
}
// allow deleting document without a collection // allow deleting document without a collection
if (document.collection && cannot(user, "update", document.collection)) { if (document.collection && cannot(user, "update", document.collection)) {
@@ -158,9 +246,15 @@ allow(User, "permanentDelete", Document, (user, document) => {
}); });
allow(User, "restore", Document, (user, document) => { allow(User, "restore", Document, (user, document) => {
if (!document) return false; if (!document) {
if (!document.deletedAt) return false; return false;
if (user.isViewer) return false; }
if (!document.deletedAt) {
return false;
}
if (user.isViewer) {
return false;
}
if (document.collection && cannot(user, "update", document.collection)) { if (document.collection && cannot(user, "update", document.collection)) {
return false; return false;
@@ -170,27 +264,45 @@ allow(User, "restore", Document, (user, document) => {
}); });
allow(User, "archive", Document, (user, document) => { allow(User, "archive", Document, (user, document) => {
if (!document) return false; if (!document) {
if (!document.publishedAt) return false; return false;
if (document.archivedAt) return false; }
if (document.deletedAt) return false; if (!document.publishedAt) {
return false;
}
if (document.archivedAt) {
return false;
}
if (document.deletedAt) {
return false;
}
invariant( invariant(
document.collection, document.collection,
"collection is missing, did you forget to include in the query scope?" "collection is missing, did you forget to include in the query scope?"
); );
if (cannot(user, "update", document.collection)) return false; if (cannot(user, "update", document.collection)) {
return false;
}
return user.teamId === document.teamId; return user.teamId === document.teamId;
}); });
allow(User, "unarchive", Document, (user, document) => { allow(User, "unarchive", Document, (user, document) => {
if (!document) return false; if (!document) {
return false;
}
invariant( invariant(
document.collection, document.collection,
"collection is missing, did you forget to include in the query scope?" "collection is missing, did you forget to include in the query scope?"
); );
if (cannot(user, "update", document.collection)) return false; if (cannot(user, "update", document.collection)) {
if (!document.archivedAt) return false; return false;
if (document.deletedAt) return false; }
if (!document.archivedAt) {
return false;
}
if (document.deletedAt) {
return false;
}
return user.teamId === document.teamId; return user.teamId === document.teamId;
}); });
@@ -202,19 +314,26 @@ allow(
); );
allow(User, "unpublish", Document, (user, document) => { allow(User, "unpublish", Document, (user, document) => {
if (!document) return false; if (!document) {
return false;
}
invariant( invariant(
document.collection, document.collection,
"collection is missing, did you forget to include in the query scope?" "collection is missing, did you forget to include in the query scope?"
); );
if (!document.publishedAt || !!document.deletedAt || !!document.archivedAt) if (!document.publishedAt || !!document.deletedAt || !!document.archivedAt) {
return false; return false;
if (cannot(user, "update", document.collection)) return false; }
if (cannot(user, "update", document.collection)) {
return false;
}
const documentID = document.id; const documentID = document.id;
const hasChild = (documents: NavigationNode[]): boolean => const hasChild = (documents: NavigationNode[]): boolean =>
documents.some((doc) => { documents.some((doc) => {
if (doc.id === documentID) return doc.children.length > 0; if (doc.id === documentID) {
return doc.children.length > 0;
}
return hasChild(doc.children); return hasChild(doc.children);
}); });

View File

@@ -3,8 +3,12 @@ import { AdminRequiredError } from "../errors";
import { allow } from "./cancan"; import { allow } from "./cancan";
allow(User, "createGroup", Team, (actor, team) => { allow(User, "createGroup", Team, (actor, team) => {
if (!team || actor.isViewer || actor.teamId !== team.id) return false; if (!team || actor.isViewer || actor.teamId !== team.id) {
if (actor.isAdmin) return true; return false;
}
if (actor.isAdmin) {
return true;
}
throw AdminRequiredError(); throw AdminRequiredError();
}); });
@@ -12,13 +16,19 @@ allow(User, "createGroup", Team, (actor, team) => {
allow(User, "read", Group, (actor, group) => { allow(User, "read", Group, (actor, group) => {
// for the time being, we're going to let everyone on the team see every group // for the time being, we're going to let everyone on the team see every group
// we may need to make this more granular in the future // we may need to make this more granular in the future
if (!group || actor.teamId !== group.teamId) return false; if (!group || actor.teamId !== group.teamId) {
return false;
}
return true; return true;
}); });
allow(User, ["update", "delete"], Group, (actor, group) => { allow(User, ["update", "delete"], Group, (actor, group) => {
if (!group || actor.isViewer || actor.teamId !== group.teamId) return false; if (!group || actor.isViewer || actor.teamId !== group.teamId) {
if (actor.isAdmin) return true; return false;
}
if (actor.isAdmin) {
return true;
}
throw AdminRequiredError(); throw AdminRequiredError();
}); });

View File

@@ -3,8 +3,12 @@ import { AdminRequiredError } from "../errors";
import { allow } from "./cancan"; import { allow } from "./cancan";
allow(User, "createIntegration", Team, (actor, team) => { allow(User, "createIntegration", Team, (actor, team) => {
if (!team || actor.isViewer || actor.teamId !== team.id) return false; if (!team || actor.isViewer || actor.teamId !== team.id) {
if (actor.isAdmin) return true; return false;
}
if (actor.isAdmin) {
return true;
}
throw AdminRequiredError(); throw AdminRequiredError();
}); });
@@ -17,9 +21,15 @@ allow(
); );
allow(User, ["update", "delete"], Integration, (user, integration) => { allow(User, ["update", "delete"], Integration, (user, integration) => {
if (user.isViewer) return false; if (user.isViewer) {
if (!integration || user.teamId !== integration.teamId) return false; return false;
if (user.isAdmin) return true; }
if (!integration || user.teamId !== integration.teamId) {
return false;
}
if (user.isAdmin) {
return true;
}
throw AdminRequiredError(); throw AdminRequiredError();
}); });

View File

@@ -2,7 +2,9 @@ import { NotificationSetting, Team, User } from "@server/models";
import { allow } from "./cancan"; import { allow } from "./cancan";
allow(User, "createNotificationSetting", Team, (user, team) => { allow(User, "createNotificationSetting", Team, (user, team) => {
if (!team || user.teamId !== team.id) return false; if (!team || user.teamId !== team.id) {
return false;
}
return true; return true;
}); });

View File

@@ -5,21 +5,37 @@ import { allow, _cannot as cannot } from "./cancan";
allow(User, "read", Share, (user, share) => user.teamId === share?.teamId); allow(User, "read", Share, (user, share) => user.teamId === share?.teamId);
allow(User, "update", Share, (user, share) => { allow(User, "update", Share, (user, share) => {
if (!share) return false; if (!share) {
if (user.isViewer) return false; return false;
}
if (user.isViewer) {
return false;
}
// only the user who can share the document publicly can update the share. // only the user who can share the document publicly can update the share.
if (cannot(user, "share", share.document)) return false; if (cannot(user, "share", share.document)) {
return false;
}
return user.teamId === share.teamId; return user.teamId === share.teamId;
}); });
allow(User, "revoke", Share, (user, share) => { allow(User, "revoke", Share, (user, share) => {
if (!share) return false; if (!share) {
if (user.isViewer) return false; return false;
if (user.teamId !== share.teamId) return false; }
if (user.id === share.userId) return true; if (user.isViewer) {
if (user.isAdmin) return true; return false;
}
if (user.teamId !== share.teamId) {
return false;
}
if (user.id === share.userId) {
return true;
}
if (user.isAdmin) {
return true;
}
throw AdminRequiredError(); throw AdminRequiredError();
}); });

View File

@@ -4,11 +4,15 @@ import { allow } from "./cancan";
allow(User, "read", Team, (user, team) => user.teamId === team?.id); allow(User, "read", Team, (user, team) => user.teamId === team?.id);
allow(User, "share", Team, (user, team) => { allow(User, "share", Team, (user, team) => {
if (!team || user.isViewer || user.teamId !== team.id) return false; if (!team || user.isViewer || user.teamId !== team.id) {
return false;
}
return team.sharing; return team.sharing;
}); });
allow(User, ["update", "export", "manage"], Team, (user, team) => { allow(User, ["update", "export", "manage"], Team, (user, team) => {
if (!team || user.isViewer || user.teamId !== team.id) return false; if (!team || user.isViewer || user.teamId !== team.id) {
return false;
}
return user.isAdmin; return user.isAdmin;
}); });

View File

@@ -10,52 +10,86 @@ allow(
); );
allow(User, "inviteUser", Team, (actor, team) => { allow(User, "inviteUser", Team, (actor, team) => {
if (!team || actor.teamId !== team.id) return false; if (!team || actor.teamId !== team.id) {
if (actor.isAdmin) return true; return false;
}
if (actor.isAdmin) {
return true;
}
throw AdminRequiredError(); throw AdminRequiredError();
}); });
allow(User, "update", User, (actor, user) => { allow(User, "update", User, (actor, user) => {
if (!user || user.teamId !== actor.teamId) return false; if (!user || user.teamId !== actor.teamId) {
if (user.id === actor.id) return true; return false;
}
if (user.id === actor.id) {
return true;
}
throw AdminRequiredError(); throw AdminRequiredError();
}); });
allow(User, "delete", User, (actor, user) => { allow(User, "delete", User, (actor, user) => {
if (!user || user.teamId !== actor.teamId) return false; if (!user || user.teamId !== actor.teamId) {
if (user.id === actor.id) return true; return false;
if (actor.isAdmin && !user.lastActiveAt) return true; }
if (user.id === actor.id) {
return true;
}
if (actor.isAdmin && !user.lastActiveAt) {
return true;
}
throw AdminRequiredError(); throw AdminRequiredError();
}); });
allow(User, ["activate", "suspend"], User, (actor, user) => { allow(User, ["activate", "suspend"], User, (actor, user) => {
if (!user || user.teamId !== actor.teamId) return false; if (!user || user.teamId !== actor.teamId) {
if (actor.isAdmin) return true; return false;
}
if (actor.isAdmin) {
return true;
}
throw AdminRequiredError(); throw AdminRequiredError();
}); });
allow(User, "readDetails", User, (actor, user) => { allow(User, "readDetails", User, (actor, user) => {
if (!user || user.teamId !== actor.teamId) return false; if (!user || user.teamId !== actor.teamId) {
if (user === actor) return true; return false;
}
if (user === actor) {
return true;
}
return actor.isAdmin; return actor.isAdmin;
}); });
allow(User, "promote", User, (actor, user) => { allow(User, "promote", User, (actor, user) => {
if (!user || user.teamId !== actor.teamId) return false; if (!user || user.teamId !== actor.teamId) {
if (user.isAdmin || user.isSuspended) return false; return false;
if (actor.isAdmin) return true; }
if (user.isAdmin || user.isSuspended) {
return false;
}
if (actor.isAdmin) {
return true;
}
throw AdminRequiredError(); throw AdminRequiredError();
}); });
allow(User, "demote", User, (actor, user) => { allow(User, "demote", User, (actor, user) => {
if (!user || user.teamId !== actor.teamId) return false; if (!user || user.teamId !== actor.teamId) {
if (user.isSuspended) return false; return false;
if (actor.isAdmin) return true; }
if (user.isSuspended) {
return false;
}
if (actor.isAdmin) {
return true;
}
throw AdminRequiredError(); throw AdminRequiredError();
}); });

View File

@@ -9,7 +9,9 @@ export default class BacklinksProcessor {
switch (event.name) { switch (event.name) {
case "documents.publish": { case "documents.publish": {
const document = await Document.findByPk(event.documentId); const document = await Document.findByPk(event.documentId);
if (!document) return; if (!document) {
return;
}
const linkIds = parseDocumentIds(document.text); const linkIds = parseDocumentIds(document.text);
await Promise.all( await Promise.all(
linkIds.map(async (linkId) => { linkIds.map(async (linkId) => {
@@ -35,10 +37,14 @@ export default class BacklinksProcessor {
case "documents.update": { case "documents.update": {
const document = await Document.findByPk(event.documentId); const document = await Document.findByPk(event.documentId);
if (!document) return; if (!document) {
return;
}
// backlinks are only created for published documents // backlinks are only created for published documents
if (!document.publishedAt) return; if (!document.publishedAt) {
return;
}
const linkIds = parseDocumentIds(document.text); const linkIds = parseDocumentIds(document.text);
const linkedDocumentIds: string[] = []; const linkedDocumentIds: string[] = [];
@@ -80,10 +86,14 @@ export default class BacklinksProcessor {
case "documents.title_change": { case "documents.title_change": {
// might as well check // might as well check
const { title, previousTitle } = event.data; const { title, previousTitle } = event.data;
if (!previousTitle || title === previousTitle) break; if (!previousTitle || title === previousTitle) {
break;
}
const document = await Document.findByPk(event.documentId); const document = await Document.findByPk(event.documentId);
if (!document) return; if (!document) {
return;
}
// TODO: Handle re-writing of titles into CRDT // TODO: Handle re-writing of titles into CRDT
const team = await Team.findByPk(document.teamId); const team = await Team.findByPk(document.teamId);

View File

@@ -21,12 +21,16 @@ export default class DebounceProcessor {
}); });
// If the document has been deleted then prevent further processing // If the document has been deleted then prevent further processing
if (!document) return; if (!document) {
return;
}
// If the document has been updated since we initially queued the delayed // If the document has been updated since we initially queued the delayed
// event then abort, there must be another updated event in the queue // event then abort, there must be another updated event in the queue
// this functions as a simple distributed debounce. // this functions as a simple distributed debounce.
if (document.updatedAt > new Date(event.createdAt)) return; if (document.updatedAt > new Date(event.createdAt)) {
return;
}
globalEventQueue.add({ ...event, name: "documents.update.debounced" }); globalEventQueue.add({ ...event, name: "documents.update.debounced" });
break; break;

View File

@@ -33,12 +33,16 @@ export default class NotificationsProcessor {
async documentUpdated(event: DocumentEvent | RevisionEvent) { async documentUpdated(event: DocumentEvent | RevisionEvent) {
// never send notifications when batch importing documents // never send notifications when batch importing documents
// @ts-expect-error ts-migrate(2339) FIXME: Property 'data' does not exist on type 'DocumentEv... Remove this comment to see the full error message // @ts-expect-error ts-migrate(2339) FIXME: Property 'data' does not exist on type 'DocumentEv... Remove this comment to see the full error message
if (event.data?.source === "import") return; if (event.data?.source === "import") {
return;
}
const [document, team] = await Promise.all([ const [document, team] = await Promise.all([
Document.findByPk(event.documentId), Document.findByPk(event.documentId),
Team.findByPk(event.teamId), Team.findByPk(event.teamId),
]); ]);
if (!document || !team || !document.collection) return; if (!document || !team || !document.collection) {
return;
}
const { collection } = document; const { collection } = document;
const notificationSettings = await NotificationSetting.findAll({ const notificationSettings = await NotificationSetting.findAll({
where: { where: {
@@ -132,8 +136,12 @@ export default class NotificationsProcessor {
}, },
], ],
}); });
if (!collection) return; if (!collection) {
if (!collection.permission) return; return;
}
if (!collection.permission) {
return;
}
const notificationSettings = await NotificationSetting.findAll({ const notificationSettings = await NotificationSetting.findAll({
where: { where: {
userId: { userId: {

View File

@@ -38,10 +38,14 @@ export default class SlackProcessor {
}, },
], ],
}); });
if (!integration) return; if (!integration) {
return;
}
const collection = integration.collection; const collection = integration.collection;
if (!collection) return; if (!collection) {
return;
}
await fetch(integration.settings.url, { await fetch(integration.settings.url, {
method: "POST", method: "POST",
@@ -65,15 +69,21 @@ export default class SlackProcessor {
async documentUpdated(event: DocumentEvent | RevisionEvent) { async documentUpdated(event: DocumentEvent | RevisionEvent) {
// never send notifications when batch importing documents // never send notifications when batch importing documents
// @ts-expect-error ts-migrate(2339) FIXME: Property 'data' does not exist on type 'DocumentEv... Remove this comment to see the full error message // @ts-expect-error ts-migrate(2339) FIXME: Property 'data' does not exist on type 'DocumentEv... Remove this comment to see the full error message
if (event.data && event.data.source === "import") return; if (event.data && event.data.source === "import") {
return;
}
const [document, team] = await Promise.all([ const [document, team] = await Promise.all([
Document.findByPk(event.documentId), Document.findByPk(event.documentId),
Team.findByPk(event.teamId), Team.findByPk(event.teamId),
]); ]);
if (!document || !team) return; if (!document || !team) {
return;
}
// never send notifications for draft documents // never send notifications for draft documents
if (!document.publishedAt) return; if (!document.publishedAt) {
return;
}
const integration = await Integration.findOne({ const integration = await Integration.findOne({
where: { where: {
@@ -88,7 +98,9 @@ export default class SlackProcessor {
}, },
}, },
}); });
if (!integration) return; if (!integration) {
return;
}
let text = `${document.updatedBy.name} updated a document`; let text = `${document.updatedBy.name} updated a document`;
if (event.name === "documents.publish") { if (event.name === "documents.publish") {

View File

@@ -652,7 +652,9 @@ router.post("collections.delete", auth(), async (ctx) => {
authorize(user, "delete", collection); authorize(user, "delete", collection);
const total = await Collection.count(); const total = await Collection.count();
if (total === 1) throw ValidationError("Cannot delete last collection"); if (total === 1) {
throw ValidationError("Cannot delete last collection");
}
await collection.destroy(); await collection.destroy();
await Event.create({ await Event.create({

View File

@@ -50,7 +50,9 @@ router.post("documents.list", auth(), pagination(), async (ctx) => {
const collectionId = ctx.body.collectionId || ctx.body.collection; const collectionId = ctx.body.collectionId || ctx.body.collection;
const createdById = ctx.body.userId || ctx.body.user; const createdById = ctx.body.userId || ctx.body.user;
let direction = ctx.body.direction; let direction = ctx.body.direction;
if (direction !== "ASC") direction = "DESC"; if (direction !== "ASC") {
direction = "DESC";
}
// always filter by the current team // always filter by the current team
const { user } = ctx.state; const { user } = ctx.state;
let where: WhereOptions<Document> = { let where: WhereOptions<Document> = {
@@ -162,7 +164,9 @@ router.post("documents.archived", auth(), pagination(), async (ctx) => {
assertSort(sort, Document); assertSort(sort, Document);
let direction = ctx.body.direction; let direction = ctx.body.direction;
if (direction !== "ASC") direction = "DESC"; if (direction !== "ASC") {
direction = "DESC";
}
const { user } = ctx.state; const { user } = ctx.state;
const collectionIds = await user.collectionIds(); const collectionIds = await user.collectionIds();
const collectionScope: Readonly<ScopeOptions> = { const collectionScope: Readonly<ScopeOptions> = {
@@ -204,7 +208,9 @@ router.post("documents.deleted", auth(), pagination(), async (ctx) => {
assertSort(sort, Document); assertSort(sort, Document);
let direction = ctx.body.direction; let direction = ctx.body.direction;
if (direction !== "ASC") direction = "DESC"; if (direction !== "ASC") {
direction = "DESC";
}
const { user } = ctx.state; const { user } = ctx.state;
const collectionIds = await user.collectionIds({ const collectionIds = await user.collectionIds({
paranoid: false, paranoid: false,
@@ -257,7 +263,9 @@ router.post("documents.viewed", auth(), pagination(), async (ctx) => {
const { sort = "updatedAt" } = ctx.body; const { sort = "updatedAt" } = ctx.body;
assertSort(sort, Document); assertSort(sort, Document);
if (direction !== "ASC") direction = "DESC"; if (direction !== "ASC") {
direction = "DESC";
}
const { user } = ctx.state; const { user } = ctx.state;
const collectionIds = await user.collectionIds(); const collectionIds = await user.collectionIds();
const userId = user.id; const userId = user.id;
@@ -318,7 +326,9 @@ router.post("documents.starred", auth(), pagination(), async (ctx) => {
const { sort = "updatedAt" } = ctx.body; const { sort = "updatedAt" } = ctx.body;
assertSort(sort, Document); assertSort(sort, Document);
if (direction !== "ASC") direction = "DESC"; if (direction !== "ASC") {
direction = "DESC";
}
const { user } = ctx.state; const { user } = ctx.state;
const collectionIds = await user.collectionIds(); const collectionIds = await user.collectionIds();
const stars = await Star.findAll({ const stars = await Star.findAll({
@@ -371,7 +381,9 @@ router.post("documents.drafts", auth(), pagination(), async (ctx) => {
const { collectionId, dateFilter, sort = "updatedAt" } = ctx.body; const { collectionId, dateFilter, sort = "updatedAt" } = ctx.body;
assertSort(sort, Document); assertSort(sort, Document);
if (direction !== "ASC") direction = "DESC"; if (direction !== "ASC") {
direction = "DESC";
}
const { user } = ctx.state; const { user } = ctx.state;
if (collectionId) { if (collectionId) {
@@ -997,7 +1009,9 @@ router.post("documents.update", auth(), async (ctx) => {
const editorVersion = ctx.headers["x-editor-version"] as string | undefined; const editorVersion = ctx.headers["x-editor-version"] as string | undefined;
assertPresent(id, "id is required"); assertPresent(id, "id is required");
assertPresent(title || text, "title or text is required"); assertPresent(title || text, "title or text is required");
if (append) assertPresent(text, "Text is required while appending"); if (append) {
assertPresent(text, "Text is required while appending");
}
const { user } = ctx.state; const { user } = ctx.state;
const document = await Document.findByPk(id, { const document = await Document.findByPk(id, {
@@ -1012,10 +1026,18 @@ router.post("documents.update", auth(), async (ctx) => {
const previousTitle = document.title; const previousTitle = document.title;
// Update document // Update document
if (title) document.title = title; if (title) {
if (editorVersion) document.editorVersion = editorVersion; document.title = title;
if (templateId) document.templateId = templateId; }
if (fullWidth !== undefined) document.fullWidth = fullWidth; if (editorVersion) {
document.editorVersion = editorVersion;
}
if (templateId) {
document.templateId = templateId;
}
if (fullWidth !== undefined) {
document.fullWidth = fullWidth;
}
if (!user.team?.collaborativeEditing) { if (!user.team?.collaborativeEditing) {
if (append) { if (append) {
@@ -1303,7 +1325,9 @@ router.post("documents.import", auth(), async (ctx) => {
assertUuid(parentDocumentId, "parentDocumentId must be an uuid"); assertUuid(parentDocumentId, "parentDocumentId must be an uuid");
} }
if (index) assertPositiveInteger(index, "index must be an integer (>=0)"); if (index) {
assertPositiveInteger(index, "index must be an integer (>=0)");
}
const { user } = ctx.state; const { user } = ctx.state;
authorize(user, "createDocument", user.team); authorize(user, "createDocument", user.team);
@@ -1372,7 +1396,9 @@ router.post("documents.create", auth(), async (ctx) => {
assertUuid(parentDocumentId, "parentDocumentId must be an uuid"); assertUuid(parentDocumentId, "parentDocumentId must be an uuid");
} }
if (index) assertPositiveInteger(index, "index must be an integer (>=0)"); if (index) {
assertPositiveInteger(index, "index must be an integer (>=0)");
}
const { user } = ctx.state; const { user } = ctx.state;
authorize(user, "createDocument", user.team); authorize(user, "createDocument", user.team);

View File

@@ -20,7 +20,9 @@ router.post("events.list", auth(), pagination(), async (ctx) => {
name, name,
auditLog = false, auditLog = false,
} = ctx.body; } = ctx.body;
if (direction !== "ASC") direction = "DESC"; if (direction !== "ASC") {
direction = "DESC";
}
assertSort(sort, Event); assertSort(sort, Event);
let where: WhereOptions<Event> = { let where: WhereOptions<Event> = {

View File

@@ -42,7 +42,9 @@ router.post("fileOperations.list", auth(), pagination(), async (ctx) => {
"type must be one of 'import' or 'export'" "type must be one of 'import' or 'export'"
); );
if (direction !== "ASC") direction = "DESC"; if (direction !== "ASC") {
direction = "DESC";
}
const { user } = ctx.state; const { user } = ctx.state;
const where: WhereOptions<FileOperation> = { const where: WhereOptions<FileOperation> = {
teamId: user.teamId, teamId: user.teamId,

View File

@@ -19,7 +19,9 @@ const router = new Router();
router.post("groups.list", auth(), pagination(), async (ctx) => { router.post("groups.list", auth(), pagination(), async (ctx) => {
let { direction } = ctx.body; let { direction } = ctx.body;
const { sort = "updatedAt" } = ctx.body; const { sort = "updatedAt" } = ctx.body;
if (direction !== "ASC") direction = "DESC"; if (direction !== "ASC") {
direction = "DESC";
}
assertSort(sort, Group); assertSort(sort, Group);
const { user } = ctx.state; const { user } = ctx.state;

View File

@@ -21,7 +21,9 @@ const router = new Router();
// triggered by a user posting a getoutline.com link in Slack // triggered by a user posting a getoutline.com link in Slack
router.post("hooks.unfurl", async (ctx) => { router.post("hooks.unfurl", async (ctx) => {
const { challenge, token, event } = ctx.body; const { challenge, token, event } = ctx.body;
if (challenge) return (ctx.body = ctx.body.challenge); if (challenge) {
return (ctx.body = ctx.body.challenge);
}
if (token !== process.env.SLACK_VERIFICATION_TOKEN) { if (token !== process.env.SLACK_VERIFICATION_TOKEN) {
throw AuthenticationError("Invalid token"); throw AuthenticationError("Invalid token");
@@ -39,21 +41,27 @@ router.post("hooks.unfurl", async (ctx) => {
}, },
], ],
}); });
if (!user) return; if (!user) {
return;
}
const auth = await IntegrationAuthentication.findOne({ const auth = await IntegrationAuthentication.findOne({
where: { where: {
service: "slack", service: "slack",
teamId: user.teamId, teamId: user.teamId,
}, },
}); });
if (!auth) return; if (!auth) {
return;
}
// get content for unfurled links // get content for unfurled links
const unfurls = {}; const unfurls = {};
for (const link of event.links) { for (const link of event.links) {
const id = link.url.substr(link.url.lastIndexOf("/") + 1); const id = link.url.substr(link.url.lastIndexOf("/") + 1);
const doc = await Document.findByPk(id); const doc = await Document.findByPk(id);
if (!doc || doc.teamId !== user.teamId) continue; if (!doc || doc.teamId !== user.teamId) {
continue;
}
unfurls[link.url] = { unfurls[link.url] = {
title: doc.title, title: doc.title,
text: doc.getSummary(), text: doc.getSummary(),

View File

@@ -12,7 +12,9 @@ const router = new Router();
router.post("integrations.list", auth(), pagination(), async (ctx) => { router.post("integrations.list", auth(), pagination(), async (ctx) => {
let { direction } = ctx.body; let { direction } = ctx.body;
const { sort = "updatedAt" } = ctx.body; const { sort = "updatedAt" } = ctx.body;
if (direction !== "ASC") direction = "DESC"; if (direction !== "ASC") {
direction = "DESC";
}
assertSort(sort, Integration); assertSort(sort, Integration);
const { user } = ctx.state; const { user } = ctx.state;

View File

@@ -33,7 +33,9 @@ router.post("revisions.info", auth(), async (ctx) => {
router.post("revisions.list", auth(), pagination(), async (ctx) => { router.post("revisions.list", auth(), pagination(), async (ctx) => {
let { direction } = ctx.body; let { direction } = ctx.body;
const { documentId, sort = "updatedAt" } = ctx.body; const { documentId, sort = "updatedAt" } = ctx.body;
if (direction !== "ASC") direction = "DESC"; if (direction !== "ASC") {
direction = "DESC";
}
assertSort(sort, Revision); assertSort(sort, Revision);
assertPresent(documentId, "documentId is required"); assertPresent(documentId, "documentId is required");

View File

@@ -100,7 +100,9 @@ router.post("shares.info", auth(), async (ctx) => {
router.post("shares.list", auth(), pagination(), async (ctx) => { router.post("shares.list", auth(), pagination(), async (ctx) => {
let { direction } = ctx.body; let { direction } = ctx.body;
const { sort = "updatedAt" } = ctx.body; const { sort = "updatedAt" } = ctx.body;
if (direction !== "ASC") direction = "DESC"; if (direction !== "ASC") {
direction = "DESC";
}
assertSort(sort, Share); assertSort(sort, Share);
const { user } = ctx.state; const { user } = ctx.state;

View File

@@ -25,11 +25,21 @@ router.post("team.update", auth(), async (ctx) => {
team.subdomain = subdomain === "" ? null : subdomain; team.subdomain = subdomain === "" ? null : subdomain;
} }
if (name) team.name = name; if (name) {
if (sharing !== undefined) team.sharing = sharing; team.name = name;
if (documentEmbeds !== undefined) team.documentEmbeds = documentEmbeds; }
if (guestSignin !== undefined) team.guestSignin = guestSignin; if (sharing !== undefined) {
if (avatarUrl !== undefined) team.avatarUrl = avatarUrl; team.sharing = sharing;
}
if (documentEmbeds !== undefined) {
team.documentEmbeds = documentEmbeds;
}
if (guestSignin !== undefined) {
team.guestSignin = guestSignin;
}
if (avatarUrl !== undefined) {
team.avatarUrl = avatarUrl;
}
if (collaborativeEditing !== undefined) { if (collaborativeEditing !== undefined) {
team.collaborativeEditing = collaborativeEditing; team.collaborativeEditing = collaborativeEditing;

View File

@@ -20,7 +20,9 @@ const router = new Router();
router.post("users.list", auth(), pagination(), async (ctx) => { router.post("users.list", auth(), pagination(), async (ctx) => {
let { direction } = ctx.body; let { direction } = ctx.body;
const { sort = "createdAt", query, filter } = ctx.body; const { sort = "createdAt", query, filter } = ctx.body;
if (direction !== "ASC") direction = "DESC"; if (direction !== "ASC") {
direction = "DESC";
}
assertSort(sort, User); assertSort(sort, User);
if (filter) { if (filter) {
@@ -138,9 +140,15 @@ router.post("users.info", auth(), async (ctx) => {
router.post("users.update", auth(), async (ctx) => { router.post("users.update", auth(), async (ctx) => {
const { user } = ctx.state; const { user } = ctx.state;
const { name, avatarUrl, language } = ctx.body; const { name, avatarUrl, language } = ctx.body;
if (name) user.name = name; if (name) {
if (avatarUrl) user.avatarUrl = avatarUrl; user.name = name;
if (language) user.language = language; }
if (avatarUrl) {
user.avatarUrl = avatarUrl;
}
if (language) {
user.language = language;
}
await user.save(); await user.save();
await Event.create({ await Event.create({
name: "users.update", name: "users.update",

View File

@@ -30,8 +30,12 @@ try {
let index = 0; let index = 0;
Object.values(manifestData).forEach((filename) => { Object.values(manifestData).forEach((filename) => {
if (typeof filename !== "string") return; if (typeof filename !== "string") {
if (!env.CDN_URL) return; return;
}
if (!env.CDN_URL) {
return;
}
if (filename.endsWith(".js")) { if (filename.endsWith(".js")) {
// Preload resources you have high-confidence will be used in the current // Preload resources you have high-confidence will be used in the current

View File

@@ -71,7 +71,9 @@ async function archiveToPath(zip: JSZip) {
postfix: ".zip", postfix: ".zip",
}, },
(err, path) => { (err, path) => {
if (err) return reject(err); if (err) {
return reject(err);
}
zip zip
.generateNodeStream({ .generateNodeStream({
type: "nodebuffer", type: "nodebuffer",

View File

@@ -6,14 +6,20 @@ export default function backspaceToParagraph(type: NodeType) {
const { $from, from, to, empty } = state.selection; const { $from, from, to, empty } = state.selection;
// if the selection has anything in it then use standard delete behavior // if the selection has anything in it then use standard delete behavior
if (!empty) return null; if (!empty) {
return null;
}
// check we're in a matching node // check we're in a matching node
if ($from.parent.type !== type) return null; if ($from.parent.type !== type) {
return null;
}
// check if we're at the beginning of the heading // check if we're at the beginning of the heading
const $pos = state.doc.resolve(from - 1); const $pos = state.doc.resolve(from - 1);
if ($pos.parent === $from.parent) return null; if ($pos.parent === $from.parent) {
return null;
}
// okay, replace it with a paragraph // okay, replace it with a paragraph
dispatch( dispatch(

View File

@@ -48,7 +48,9 @@ const createAndInsertLink = async function (
const url = await onCreateLink(title); const url = await onCreateLink(title);
const result = findPlaceholderLink(view.state.doc, href); const result = findPlaceholderLink(view.state.doc, href);
if (!result) return; if (!result) {
return;
}
dispatch( dispatch(
view.state.tr view.state.tr
@@ -65,7 +67,9 @@ const createAndInsertLink = async function (
); );
} catch (err) { } catch (err) {
const result = findPlaceholderLink(view.state.doc, href); const result = findPlaceholderLink(view.state.doc, href);
if (!result) return; if (!result) {
return;
}
dispatch( dispatch(
view.state.tr.removeMark( view.state.tr.removeMark(

View File

@@ -25,7 +25,9 @@ const insertFiles = function (
): void { ): void {
// filter to only include image files // filter to only include image files
const images = files.filter((file) => /image/i.test(file.type)); const images = files.filter((file) => /image/i.test(file.type));
if (images.length === 0) return; if (images.length === 0) {
return;
}
const { const {
dictionary, dictionary,
@@ -47,7 +49,9 @@ const insertFiles = function (
event.preventDefault(); event.preventDefault();
// let the user know we're starting to process the images // let the user know we're starting to process the images
if (onImageUploadStart) onImageUploadStart(); if (onImageUploadStart) {
onImageUploadStart();
}
const { schema } = view.state; const { schema } = view.state;

View File

@@ -8,15 +8,21 @@ export default function splitHeading(type: NodeType) {
const { $from, from, $to, to } = state.selection; const { $from, from, $to, to } = state.selection;
// check we're in a matching heading node // check we're in a matching heading node
if ($from.parent.type !== type) return false; if ($from.parent.type !== type) {
return false;
}
// check that the caret is at the end of the content, if it isn't then // check that the caret is at the end of the content, if it isn't then
// standard node splitting behaviour applies // standard node splitting behaviour applies
const endPos = $to.after() - 1; const endPos = $to.after() - 1;
if (endPos !== to) return false; if (endPos !== to) {
return false;
}
// If the node isn't collapsed standard behavior applies // If the node isn't collapsed standard behavior applies
if (!$from.parent.attrs.collapsed) return false; if (!$from.parent.attrs.collapsed) {
return false;
}
// Find the next visible block after this one. It takes into account nested // Find the next visible block after this one. It takes into account nested
// collapsed headings and reaching the end of the document // collapsed headings and reaching the end of the document

View File

@@ -37,7 +37,9 @@ class Frame extends React.Component<PropsWithRef> {
} }
loadIframe = () => { loadIframe = () => {
if (!this.mounted) return; if (!this.mounted) {
return;
}
this.isLoaded = true; this.isLoaded = true;
}; };

View File

@@ -73,7 +73,9 @@ export default class ExtensionManager {
) )
.reduce((nodes, extension: Node | Mark) => { .reduce((nodes, extension: Node | Mark) => {
const md = extension.parseMarkdown(); const md = extension.parseMarkdown();
if (!md) return nodes; if (!md) {
return nodes;
}
return { return {
...nodes, ...nodes,

View File

@@ -5,17 +5,23 @@ export default function filterExcessSeparators(
): (MenuItem | EmbedDescriptor)[] { ): (MenuItem | EmbedDescriptor)[] {
return items.reduce((acc, item, index) => { return items.reduce((acc, item, index) => {
// trim separators from start / end // trim separators from start / end
if (item.name === "separator" && index === 0) return acc; if (item.name === "separator" && index === 0) {
if (item.name === "separator" && index === items.length - 1) return acc; return acc;
}
if (item.name === "separator" && index === items.length - 1) {
return acc;
}
// trim double separators looking ahead / behind // trim double separators looking ahead / behind
const prev = items[index - 1]; const prev = items[index - 1];
if (prev && prev.name === "separator" && item.name === "separator") if (prev && prev.name === "separator" && item.name === "separator") {
return acc; return acc;
}
const next = items[index + 1]; const next = items[index + 1];
if (next && next.name === "separator" && item.name === "separator") if (next && next.name === "separator" && item.name === "separator") {
return acc; return acc;
}
// otherwise, continue // otherwise, continue
return [...acc, item]; return [...acc, item];

View File

@@ -18,7 +18,9 @@ function safeSlugify(text: string) {
// in the document that is as stable as possible // in the document that is as stable as possible
export default function headingToSlug(node: Node, index = 0) { export default function headingToSlug(node: Node, index = 0) {
const slugified = safeSlugify(node.textContent); const slugified = safeSlugify(node.textContent);
if (index === 0) return slugified; if (index === 0) {
return slugified;
}
return `${slugified}-${index}`; return `${slugified}-${index}`;
} }

View File

@@ -1,18 +1,28 @@
export default function isMarkdown(text: string): boolean { export default function isMarkdown(text: string): boolean {
// code-ish // code-ish
const fences = text.match(/^```/gm); const fences = text.match(/^```/gm);
if (fences && fences.length > 1) return true; if (fences && fences.length > 1) {
return true;
}
// link-ish // link-ish
if (text.match(/\[[^]+\]\(https?:\/\/\S+\)/gm)) return true; if (text.match(/\[[^]+\]\(https?:\/\/\S+\)/gm)) {
if (text.match(/\[[^]+\]\(\/\S+\)/gm)) return true; return true;
}
if (text.match(/\[[^]+\]\(\/\S+\)/gm)) {
return true;
}
// heading-ish // heading-ish
if (text.match(/^#{1,6}\s+\S+/gm)) return true; if (text.match(/^#{1,6}\s+\S+/gm)) {
return true;
}
// list-ish // list-ish
const listItems = text.match(/^[\d-*].?\s\S+/gm); const listItems = text.match(/^[\d-*].?\s\S+/gm);
if (listItems && listItems.length > 1) return true; if (listItems && listItems.length > 1) {
return true;
}
return false; return false;
} }

View File

@@ -80,20 +80,28 @@ export class MarkdownSerializerState {
// on a node level by specifying a tight attribute on the node. // on a node level by specifying a tight attribute on the node.
// Defaults to false. // Defaults to false.
this.options = options || {}; this.options = options || {};
if (typeof this.options.tightLists === "undefined") if (typeof this.options.tightLists === "undefined") {
this.options.tightLists = true; this.options.tightLists = true;
}
} }
flushClose(size) { flushClose(size) {
if (this.closed) { if (this.closed) {
if (!this.atBlank()) this.out += "\n"; if (!this.atBlank()) {
if (size === null || size === undefined) size = 2; this.out += "\n";
}
if (size === null || size === undefined) {
size = 2;
}
if (size > 1) { if (size > 1) {
let delimMin = this.delim; let delimMin = this.delim;
const trim = /\s+$/.exec(delimMin); const trim = /\s+$/.exec(delimMin);
if (trim) if (trim) {
delimMin = delimMin.slice(0, delimMin.length - trim[0].length); delimMin = delimMin.slice(0, delimMin.length - trim[0].length);
for (let i = 1; i < size; i++) this.out += delimMin + "\n"; }
for (let i = 1; i < size; i++) {
this.out += delimMin + "\n";
}
} }
this.closed = false; this.closed = false;
} }
@@ -120,7 +128,9 @@ export class MarkdownSerializerState {
// :: () // :: ()
// Ensure the current content ends with a newline. // Ensure the current content ends with a newline.
ensureNewLine() { ensureNewLine() {
if (!this.atBlank()) this.out += "\n"; if (!this.atBlank()) {
this.out += "\n";
}
} }
// :: (?string) // :: (?string)
@@ -129,8 +139,12 @@ export class MarkdownSerializerState {
// (unescaped) to the output. // (unescaped) to the output.
write(content) { write(content) {
this.flushClose(); this.flushClose();
if (this.delim && this.atBlank()) this.out += this.delim; if (this.delim && this.atBlank()) {
if (content) this.out += content; this.out += this.delim;
}
if (content) {
this.out += content;
}
} }
// :: (Node) // :: (Node)
@@ -148,14 +162,18 @@ export class MarkdownSerializerState {
const startOfLine = this.atBlank() || this.closed; const startOfLine = this.atBlank() || this.closed;
this.write(); this.write();
this.out += escape !== false ? this.esc(lines[i], startOfLine) : lines[i]; this.out += escape !== false ? this.esc(lines[i], startOfLine) : lines[i];
if (i !== lines.length - 1) this.out += "\n"; if (i !== lines.length - 1) {
this.out += "\n";
}
} }
} }
// :: (Node) // :: (Node)
// Render the given node as a block. // Render the given node as a block.
render(node, parent, index) { render(node, parent, index) {
if (typeof parent === "number") throw new Error("!"); if (typeof parent === "number") {
throw new Error("!");
}
this.nodes[node.type.name](this, node, parent, index); this.nodes[node.type.name](this, node, parent, index);
} }
@@ -178,14 +196,17 @@ export class MarkdownSerializerState {
// before closing marks. // before closing marks.
// (FIXME it'd be nice if we had a schema-agnostic way to // (FIXME it'd be nice if we had a schema-agnostic way to
// identify nodes that serialize as hard breaks) // identify nodes that serialize as hard breaks)
if (node && node.type.name === "hard_break") if (node && node.type.name === "hard_break") {
marks = marks.filter((m) => { marks = marks.filter((m) => {
if (index + 1 === parent.childCount) return false; if (index + 1 === parent.childCount) {
return false;
}
const next = parent.child(index + 1); const next = parent.child(index + 1);
return ( return (
m.isInSet(next.marks) && (!next.isText || /\S/.test(next.text)) m.isInSet(next.marks) && (!next.isText || /\S/.test(next.text))
); );
}); });
}
let leading = trailing; let leading = trailing;
trailing = ""; trailing = "";
@@ -205,7 +226,9 @@ export class MarkdownSerializerState {
trailing = trail; trailing = trail;
if (lead || trail) { if (lead || trail) {
node = inner ? node.withText(inner) : null; node = inner ? node.withText(inner) : null;
if (!node) marks = active; if (!node) {
marks = active;
}
} }
} }
@@ -219,23 +242,28 @@ export class MarkdownSerializerState {
// active. // active.
outer: for (let i = 0; i < len; i++) { outer: for (let i = 0; i < len; i++) {
const mark = marks[i]; const mark = marks[i];
if (!this.marks[mark.type.name]().mixable) break; if (!this.marks[mark.type.name]().mixable) {
break;
}
for (let j = 0; j < active.length; j++) { for (let j = 0; j < active.length; j++) {
const other = active[j]; const other = active[j];
if (!this.marks[other.type.name]().mixable) break; if (!this.marks[other.type.name]().mixable) {
break;
}
if (mark.eq(other)) { if (mark.eq(other)) {
if (i > j) if (i > j) {
marks = marks marks = marks
.slice(0, j) .slice(0, j)
.concat(mark) .concat(mark)
.concat(marks.slice(j, i)) .concat(marks.slice(j, i))
.concat(marks.slice(i + 1, len)); .concat(marks.slice(i + 1, len));
else if (j > i) } else if (j > i) {
marks = marks marks = marks
.slice(0, i) .slice(0, i)
.concat(marks.slice(i + 1, j)) .concat(marks.slice(i + 1, j))
.concat(mark) .concat(mark)
.concat(marks.slice(j, len)); .concat(marks.slice(j, len));
}
continue outer; continue outer;
} }
} }
@@ -246,15 +274,19 @@ export class MarkdownSerializerState {
while ( while (
keep < Math.min(active.length, len) && keep < Math.min(active.length, len) &&
marks[keep].eq(active[keep]) marks[keep].eq(active[keep])
) ) {
++keep; ++keep;
}
// Close the marks that need to be closed // Close the marks that need to be closed
while (keep < active.length) while (keep < active.length) {
this.text(this.markString(active.pop(), false, parent, index), false); this.text(this.markString(active.pop(), false, parent, index), false);
}
// Output any previously expelled trailing whitespace outside the marks // Output any previously expelled trailing whitespace outside the marks
if (leading) this.text(leading); if (leading) {
this.text(leading);
}
// Open the marks that need to be opened // Open the marks that need to be opened
if (node) { if (node) {
@@ -266,14 +298,16 @@ export class MarkdownSerializerState {
// Render the node. Special case code marks, since their content // Render the node. Special case code marks, since their content
// may not be escaped. // may not be escaped.
if (noEsc && node.isText) if (noEsc && node.isText) {
this.text( this.text(
this.markString(inner, true, parent, index) + this.markString(inner, true, parent, index) +
node.text + node.text +
this.markString(inner, false, parent, index + 1), this.markString(inner, false, parent, index + 1),
false false
); );
else this.render(node, parent, index); } else {
this.render(node, parent, index);
}
} }
}; };
parent.forEach(progress); parent.forEach(progress);
@@ -286,8 +320,11 @@ export class MarkdownSerializerState {
// `firstDelim` is a function going from an item index to a // `firstDelim` is a function going from an item index to a
// delimiter for the first line of the item. // delimiter for the first line of the item.
renderList(node, delim, firstDelim) { renderList(node, delim, firstDelim) {
if (this.closed && this.closed.type === node.type) this.flushClose(3); if (this.closed && this.closed.type === node.type) {
else if (this.inTightList) this.flushClose(1); this.flushClose(3);
} else if (this.inTightList) {
this.flushClose(1);
}
const isTight = const isTight =
typeof node.attrs.tight !== "undefined" typeof node.attrs.tight !== "undefined"
@@ -298,7 +335,9 @@ export class MarkdownSerializerState {
this.inList = true; this.inList = true;
this.inTightList = isTight; this.inTightList = isTight;
node.forEach((child, _, i) => { node.forEach((child, _, i) => {
if (i && isTight) this.flushClose(1); if (i && isTight) {
this.flushClose(1);
}
this.wrapBlock(delim, firstDelim(i), node, () => this.wrapBlock(delim, firstDelim(i), node, () =>
this.render(child, node, i) this.render(child, node, i)
); );
@@ -387,7 +426,9 @@ export class MarkdownSerializerState {
// Repeat the given string `n` times. // Repeat the given string `n` times.
repeat(str, n) { repeat(str, n) {
let out = ""; let out = "";
for (let i = 0; i < n; i++) out += str; for (let i = 0; i < n; i++) {
out += str;
}
return out; return out;
} }

View File

@@ -46,7 +46,9 @@ export default class Placeholder extends Mark {
const $from = state.doc.resolve(from); const $from = state.doc.resolve(from);
const range = getMarkRange($from, state.schema.marks.placeholder); const range = getMarkRange($from, state.schema.marks.placeholder);
if (!range) return false; if (!range) {
return false;
}
const selectionStart = Math.min(from, range.from); const selectionStart = Math.min(from, range.from);
const selectionEnd = Math.max(to, range.to); const selectionEnd = Math.max(to, range.to);
@@ -88,7 +90,9 @@ export default class Placeholder extends Mark {
state.doc.resolve(Math.max(0, state.selection.from - 1)), state.doc.resolve(Math.max(0, state.selection.from - 1)),
state.schema.marks.placeholder state.schema.marks.placeholder
); );
if (!range) return false; if (!range) {
return false;
}
dispatch( dispatch(
state.tr state.tr
@@ -107,7 +111,9 @@ export default class Placeholder extends Mark {
state.doc.resolve(Math.max(0, state.selection.from - 1)), state.doc.resolve(Math.max(0, state.selection.from - 1)),
state.schema.marks.placeholder state.schema.marks.placeholder
); );
if (!range) return false; if (!range) {
return false;
}
const startOfMark = state.doc.resolve(range.from); const startOfMark = state.doc.resolve(range.from);
dispatch(state.tr.setSelection(TextSelection.near(startOfMark))); dispatch(state.tr.setSelection(TextSelection.near(startOfMark)));
@@ -119,7 +125,9 @@ export default class Placeholder extends Mark {
state.selection.$from, state.selection.$from,
state.schema.marks.placeholder state.schema.marks.placeholder
); );
if (!range) return false; if (!range) {
return false;
}
const endOfMark = state.doc.resolve(range.to); const endOfMark = state.doc.resolve(range.to);
dispatch(state.tr.setSelection(TextSelection.near(endOfMark))); dispatch(state.tr.setSelection(TextSelection.near(endOfMark)));
@@ -145,7 +153,9 @@ export default class Placeholder extends Mark {
state.selection.$from, state.selection.$from,
state.schema.marks.placeholder state.schema.marks.placeholder
); );
if (!range) return false; if (!range) {
return false;
}
event.stopPropagation(); event.stopPropagation();
event.preventDefault(); event.preventDefault();

View File

@@ -150,7 +150,9 @@ export default class CodeFence extends Node {
state: EditorState, state: EditorState,
dispatch: (tr: Transaction) => void dispatch: (tr: Transaction) => void
) => { ) => {
if (!isInCode(state)) return false; if (!isInCode(state)) {
return false;
}
const { const {
tr, tr,
selection, selection,
@@ -171,7 +173,9 @@ export default class CodeFence extends Node {
return true; return true;
}, },
Tab: (state: EditorState, dispatch: (tr: Transaction) => void) => { Tab: (state: EditorState, dispatch: (tr: Transaction) => void) => {
if (!isInCode(state)) return false; if (!isInCode(state)) {
return false;
}
const { tr, selection } = state; const { tr, selection } = state;
dispatch(tr.insertText(" ", selection.from, selection.to)); dispatch(tr.insertText(" ", selection.from, selection.to));

View File

@@ -39,7 +39,9 @@ export default class HardBreak extends Node {
state: EditorState, state: EditorState,
dispatch: (tr: Transaction) => void dispatch: (tr: Transaction) => void
) => { ) => {
if (!isInTable(state)) return false; if (!isInTable(state)) {
return false;
}
dispatch(state.tr.replaceSelectionWith(type.create()).scrollIntoView()); dispatch(state.tr.replaceSelectionWith(type.create()).scrollIntoView());
return true; return true;
}, },

View File

@@ -216,7 +216,9 @@ export default class Heading extends Node {
const previouslySeen = {}; const previouslySeen = {};
doc.descendants((node, pos) => { doc.descendants((node, pos) => {
if (node.type.name !== this.name) return; if (node.type.name !== this.name) {
return;
}
// calculate the optimal id // calculate the optimal id
const slug = headingToSlug(node); const slug = headingToSlug(node);

View File

@@ -41,7 +41,9 @@ const uploadPlugin = (options: Options) =>
return false; return false;
} }
if (!event.clipboardData) return false; if (!event.clipboardData) {
return false;
}
// check if we actually pasted any files // check if we actually pasted any files
const files = Array.prototype.slice const files = Array.prototype.slice
@@ -49,7 +51,9 @@ const uploadPlugin = (options: Options) =>
.map((dt: any) => dt.getAsFile()) .map((dt: any) => dt.getAsFile())
.filter((file: File) => file); .filter((file: File) => file);
if (files.length === 0) return false; if (files.length === 0) {
return false;
}
const { tr } = view.state; const { tr } = view.state;
if (!tr.selection.empty) { if (!tr.selection.empty) {
@@ -96,7 +100,9 @@ const uploadPlugin = (options: Options) =>
const IMAGE_CLASSES = ["right-50", "left-50"]; const IMAGE_CLASSES = ["right-50", "left-50"];
const getLayoutAndTitle = (tokenTitle: string | null) => { const getLayoutAndTitle = (tokenTitle: string | null) => {
if (!tokenTitle) return {}; if (!tokenTitle) {
return {};
}
if (IMAGE_CLASSES.includes(tokenTitle)) { if (IMAGE_CLASSES.includes(tokenTitle)) {
return { return {
layoutClass: tokenTitle, layoutClass: tokenTitle,
@@ -242,7 +248,9 @@ export default class Image extends Node {
const alt = event.currentTarget.innerText; const alt = event.currentTarget.innerText;
const { src, title, layoutClass } = node.attrs; const { src, title, layoutClass } = node.attrs;
if (alt === node.attrs.alt) return; if (alt === node.attrs.alt) {
return;
}
const { view } = this.editor; const { view } = this.editor;
const { tr } = view.state; const { tr } = view.state;

View File

@@ -203,8 +203,12 @@ export default class ListItem extends Node {
state: EditorState, state: EditorState,
dispatch: (tr: Transaction) => void dispatch: (tr: Transaction) => void
) => { ) => {
if (!isInList(state)) return false; if (!isInList(state)) {
if (!state.selection.empty) return false; return false;
}
if (!state.selection.empty) {
return false;
}
const { tr, selection } = state; const { tr, selection } = state;
dispatch(tr.split(selection.to)); dispatch(tr.split(selection.to));
@@ -214,9 +218,13 @@ export default class ListItem extends Node {
state: EditorState, state: EditorState,
dispatch: (tr: Transaction) => void dispatch: (tr: Transaction) => void
) => { ) => {
if (!state.selection.empty) return false; if (!state.selection.empty) {
return false;
}
const result = getParentListItem(state); const result = getParentListItem(state);
if (!result) return false; if (!result) {
return false;
}
const [li, pos] = result; const [li, pos] = result;
const $pos = state.doc.resolve(pos); const $pos = state.doc.resolve(pos);
@@ -244,9 +252,13 @@ export default class ListItem extends Node {
state: EditorState, state: EditorState,
dispatch: (tr: Transaction) => void dispatch: (tr: Transaction) => void
) => { ) => {
if (!state.selection.empty) return false; if (!state.selection.empty) {
return false;
}
const result = getParentListItem(state); const result = getParentListItem(state);
if (!result) return false; if (!result) {
return false;
}
const [li, pos] = result; const [li, pos] = result;
const $pos = state.doc.resolve(pos + li.nodeSize); const $pos = state.doc.resolve(pos + li.nodeSize);

View File

@@ -124,7 +124,9 @@ export default class Table extends Node {
Tab: goToNextCell(1), Tab: goToNextCell(1),
"Shift-Tab": goToNextCell(-1), "Shift-Tab": goToNextCell(-1),
Enter: (state: EditorState, dispatch: (tr: Transaction) => void) => { Enter: (state: EditorState, dispatch: (tr: Transaction) => void) => {
if (!isInTable(state)) return false; if (!isInTable(state)) {
return false;
}
// TODO: Adding row at the end for now, can we find the current cell // TODO: Adding row at the end for now, can we find the current cell
// row index and add the row below that? // row index and add the row below that?
@@ -156,11 +158,15 @@ export default class Table extends Node {
let index = 0; let index = 0;
doc.descendants((node, pos) => { doc.descendants((node, pos) => {
if (node.type.name !== this.name) return; if (node.type.name !== this.name) {
return;
}
const elements = document.getElementsByClassName("rme-table"); const elements = document.getElementsByClassName("rme-table");
const table = elements[index]; const table = elements[index];
if (!table) return; if (!table) {
return;
}
const element = table.parentElement; const element = table.parentElement;
const shadowRight = !!( const shadowRight = !!(

View File

@@ -44,7 +44,9 @@ export function run(
const match = regex.exec(textBefore); const match = regex.exec(textBefore);
const tr = handler(state, match, match ? from - match[0].length : from, to); const tr = handler(state, match, match ? from - match[0].length : from, to);
if (!tr) return false; if (!tr) {
return false;
}
return true; return true;
} }

View File

@@ -21,7 +21,9 @@ export default class Folding extends Extension {
return {}; return {};
}, },
appendTransaction: (transactions, oldState, newState) => { appendTransaction: (transactions, oldState, newState) => {
if (loaded) return; if (loaded) {
return;
}
if ( if (
!transactions.some((transaction) => transaction.getMeta("folding")) !transactions.some((transaction) => transaction.getMeta("folding"))
) { ) {

View File

@@ -53,7 +53,9 @@ export default class PasteHandler extends Extension {
if (view.props.editable && !view.props.editable(view.state)) { if (view.props.editable && !view.props.editable(view.state)) {
return false; return false;
} }
if (!event.clipboardData) return false; if (!event.clipboardData) {
return false;
}
const text = event.clipboardData.getData("text/plain"); const text = event.clipboardData.getData("text/plain");
const html = event.clipboardData.getData("text/html"); const html = event.clipboardData.getData("text/html");

View File

@@ -2,7 +2,9 @@ import { CellSelection } from "prosemirror-tables";
export default function getColumnIndex(selection: CellSelection) { export default function getColumnIndex(selection: CellSelection) {
const isColSelection = selection.isColSelection && selection.isColSelection(); const isColSelection = selection.isColSelection && selection.isColSelection();
if (!isColSelection) return undefined; if (!isColSelection) {
return undefined;
}
const path = (selection.$from as any).path; const path = (selection.$from as any).path;
return path[path.length - 5]; return path[path.length - 5];

View File

@@ -2,7 +2,9 @@ import { CellSelection } from "prosemirror-tables";
export default function getRowIndex(selection: CellSelection) { export default function getRowIndex(selection: CellSelection) {
const isRowSelection = selection.isRowSelection && selection.isRowSelection(); const isRowSelection = selection.isRowSelection && selection.isRowSelection();
if (!isRowSelection) return undefined; if (!isRowSelection) {
return undefined;
}
const path = (selection.$from as any).path; const path = (selection.$from as any).path;
return path[path.length - 8]; return path[path.length - 8];

View File

@@ -26,7 +26,9 @@ export default function markdownBreakToParagraphs(md: MarkdownIt) {
const children = tokenChildren.filter((child) => !isHardbreak(child)); const children = tokenChildren.filter((child) => !isHardbreak(child));
let count = matches.length; let count = matches.length;
if (children.length) count++; if (children.length) {
count++;
}
for (let i = 0; i < count; i++) { for (let i = 0; i < count; i++) {
const isLast = i === count - 1; const isLast = i === count - 1;

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