chore: Lint shared directory

This commit is contained in:
Tom Moor
2020-06-22 21:29:21 -07:00
parent 26f6961c82
commit 9be5597f4b
27 changed files with 342 additions and 341 deletions

View File

@ -142,7 +142,7 @@ const StyledNavLink = styled(NavLink)`
&:focus { &:focus {
color: ${props => props.theme.text}; color: ${props => props.theme.text};
background: ${props => props.theme.sidebarItemBackground}; background: ${props => props.theme.black05};
outline: none; outline: none;
} }

View File

@ -9,7 +9,7 @@
"build": "npm run clean && npm run build:webpack", "build": "npm run clean && npm run build:webpack",
"start": "NODE_ENV=production node index.js", "start": "NODE_ENV=production node index.js",
"dev": "NODE_ENV=development nodemon --watch server index.js", "dev": "NODE_ENV=development nodemon --watch server index.js",
"lint": "eslint app server", "lint": "eslint app server shared",
"flow": "flow", "flow": "flow",
"deploy": "git push heroku master", "deploy": "git push heroku master",
"heroku-postbuild": "npm run build && npm run sequelize:migrate", "heroku-postbuild": "npm run build && npm run sequelize:migrate",

View File

@ -1,7 +1,7 @@
// @flow // @flow
import * as React from 'react'; import * as React from "react";
import styled from 'styled-components'; import styled from "styled-components";
import OutlineLogo from './OutlineLogo'; import OutlineLogo from "./OutlineLogo";
type Props = { type Props = {
href?: string, href?: string,

View File

@ -1,17 +1,17 @@
// @flow // @flow
import * as React from 'react'; import * as React from "react";
import { observer, inject } from 'mobx-react'; import { observer, inject } from "mobx-react";
import breakpoint from 'styled-components-breakpoint'; import breakpoint from "styled-components-breakpoint";
import styled from 'styled-components'; import styled from "styled-components";
import { Link } from 'react-router-dom'; import { Link } from "react-router-dom";
import { PadlockIcon, GoToIcon, MoreIcon } from 'outline-icons'; import { PadlockIcon, GoToIcon, MoreIcon } from "outline-icons";
import Document from 'models/Document'; import Document from "models/Document";
import CollectionsStore from 'stores/CollectionsStore'; import CollectionsStore from "stores/CollectionsStore";
import { collectionUrl } from 'utils/routeHelpers'; import { collectionUrl } from "utils/routeHelpers";
import Flex from 'shared/components/Flex'; import Flex from "shared/components/Flex";
import BreadcrumbMenu from './BreadcrumbMenu'; import BreadcrumbMenu from "./BreadcrumbMenu";
import CollectionIcon from 'components/CollectionIcon'; import CollectionIcon from "components/CollectionIcon";
type Props = { type Props = {
document: Document, document: Document,
@ -30,7 +30,7 @@ const Breadcrumb = observer(({ document, collections, onlyText }: Props) => {
<React.Fragment> <React.Fragment>
{collection.private && ( {collection.private && (
<React.Fragment> <React.Fragment>
<SmallPadlockIcon color="currentColor" size={16} />{' '} <SmallPadlockIcon color="currentColor" size={16} />{" "}
</React.Fragment> </React.Fragment>
)} )}
{collection.name} {collection.name}
@ -51,7 +51,7 @@ const Breadcrumb = observer(({ document, collections, onlyText }: Props) => {
return ( return (
<Wrapper justify="flex-start" align="center"> <Wrapper justify="flex-start" align="center">
<CollectionName to={collectionUrl(collection.id)}> <CollectionName to={collectionUrl(collection.id)}>
<CollectionIcon collection={collection} expanded />{' '} <CollectionIcon collection={collection} expanded />{" "}
<span>{collection.name}</span> <span>{collection.name}</span>
</CollectionName> </CollectionName>
{isNestedDocument && ( {isNestedDocument && (
@ -61,7 +61,7 @@ const Breadcrumb = observer(({ document, collections, onlyText }: Props) => {
)} )}
{lastPath && ( {lastPath && (
<React.Fragment> <React.Fragment>
<Slash />{' '} <Slash />{" "}
<Crumb to={lastPath.url} title={lastPath.title}> <Crumb to={lastPath.url} title={lastPath.title}>
{lastPath.title} {lastPath.title}
</Crumb> </Crumb>
@ -74,7 +74,7 @@ const Breadcrumb = observer(({ document, collections, onlyText }: Props) => {
const Wrapper = styled(Flex)` const Wrapper = styled(Flex)`
display: none; display: none;
${breakpoint('tablet')` ${breakpoint("tablet")`
display: flex; display: flex;
`}; `};
`; `;
@ -130,4 +130,4 @@ const CollectionName = styled(Link)`
overflow: hidden; overflow: hidden;
`; `;
export default inject('collections')(Breadcrumb); export default inject("collections")(Breadcrumb);

View File

@ -1,7 +1,7 @@
// @flow // @flow
import * as React from 'react'; import * as React from "react";
import { Link } from 'react-router-dom'; import { Link } from "react-router-dom";
import { DropdownMenu, DropdownMenuItem } from 'components/DropdownMenu'; import { DropdownMenu, DropdownMenuItem } from "components/DropdownMenu";
type Props = { type Props = {
label: React.Node, label: React.Node,

View File

@ -1,20 +1,20 @@
// @flow // @flow
import * as React from 'react'; import * as React from "react";
import styled from 'styled-components'; import styled from "styled-components";
type JustifyValues = type JustifyValues =
| 'center' | "center"
| 'space-around' | "space-around"
| 'space-between' | "space-between"
| 'flex-start' | "flex-start"
| 'flex-end'; | "flex-end";
type AlignValues = type AlignValues =
| 'stretch' | "stretch"
| 'center' | "center"
| 'baseline' | "baseline"
| 'flex-start' | "flex-start"
| 'flex-end'; | "flex-end";
type Props = { type Props = {
column?: ?boolean, column?: ?boolean,
@ -33,11 +33,11 @@ const Flex = (props: Props) => {
const Container = styled.div` const Container = styled.div`
display: flex; display: flex;
flex: ${({ auto }) => (auto ? '1 1 auto' : 'initial')}; flex: ${({ auto }) => (auto ? "1 1 auto" : "initial")};
flex-direction: ${({ column }) => (column ? 'column' : 'row')}; flex-direction: ${({ column }) => (column ? "column" : "row")};
align-items: ${({ align }) => align}; align-items: ${({ align }) => align};
justify-content: ${({ justify }) => justify}; justify-content: ${({ justify }) => justify};
flex-shrink: ${({ shrink }) => (shrink ? 1 : 'initial')}; flex-shrink: ${({ shrink }) => (shrink ? 1 : "initial")};
min-height: 0; min-height: 0;
min-width: 0; min-width: 0;
`; `;

View File

@ -1,5 +1,5 @@
// @flow // @flow
import * as React from 'react'; import * as React from "react";
type Props = { type Props = {
size?: number, size?: number,
@ -7,7 +7,7 @@ type Props = {
className?: string, className?: string,
}; };
function GithubLogo({ size = 34, fill = '#FFF', className }: Props) { function GithubLogo({ size = 34, fill = "#FFF", className }: Props) {
return ( return (
<svg <svg
fill={fill} fill={fill}

View File

@ -1,5 +1,5 @@
// @flow // @flow
import * as React from 'react'; import * as React from "react";
type Props = { type Props = {
size?: number, size?: number,
@ -7,7 +7,7 @@ type Props = {
className?: string, className?: string,
}; };
function GoogleLogo({ size = 34, fill = '#FFF', className }: Props) { function GoogleLogo({ size = 34, fill = "#FFF", className }: Props) {
return ( return (
<svg <svg
fill={fill} fill={fill}

View File

@ -1,11 +1,11 @@
// @flow // @flow
import styled from 'styled-components'; import styled from "styled-components";
const Notice = styled.p` const Notice = styled.p`
background: ${props => background: ${props =>
props.muted ? props.theme.sidebarBackground : props.theme.yellow}; props.muted ? props.theme.sidebarBackground : props.theme.yellow};
color: ${props => color: ${props =>
props.muted ? props.theme.sidebarText : 'hsla(46, 100%, 20%, 1)'}; props.muted ? props.theme.sidebarText : "hsla(46, 100%, 20%, 1)"};
padding: 10px 12px; padding: 10px 12px;
border-radius: 4px; border-radius: 4px;
`; `;

View File

@ -1,5 +1,5 @@
// @flow // @flow
import * as React from 'react'; import * as React from "react";
type Props = { type Props = {
size?: number, size?: number,
@ -7,7 +7,7 @@ type Props = {
className?: string, className?: string,
}; };
function OutlineLogo({ size = 32, fill = '#333', className }: Props) { function OutlineLogo({ size = 32, fill = "#333", className }: Props) {
return ( return (
<svg <svg
fill={fill} fill={fill}

View File

@ -1,5 +1,5 @@
// @flow // @flow
import * as React from 'react'; import * as React from "react";
type Props = { type Props = {
size?: number, size?: number,
@ -7,7 +7,7 @@ type Props = {
className?: string, className?: string,
}; };
function SlackLogo({ size = 34, fill = '#FFF', className }: Props) { function SlackLogo({ size = 34, fill = "#FFF", className }: Props) {
return ( return (
<svg <svg
fill={fill} fill={fill}

View File

@ -1,5 +1,5 @@
// @flow // @flow
import styled from 'styled-components'; import styled from "styled-components";
const TeamLogo = styled.img` const TeamLogo = styled.img`
width: 38px; width: 38px;

View File

@ -1,8 +1,8 @@
// @flow // @flow
import * as React from 'react'; import * as React from "react";
import Tooltip from 'components/Tooltip'; import Tooltip from "components/Tooltip";
import distanceInWordsToNow from 'date-fns/distance_in_words_to_now'; import distanceInWordsToNow from "date-fns/distance_in_words_to_now";
import format from 'date-fns/format'; import format from "date-fns/format";
let callbacks = []; let callbacks = [];
@ -41,7 +41,7 @@ class Time extends React.Component<Props> {
render() { render() {
return ( return (
<Tooltip <Tooltip
tooltip={format(this.props.dateTime, 'MMMM Do, YYYY h:mm a')} tooltip={format(this.props.dateTime, "MMMM Do, YYYY h:mm a")}
placement="bottom" placement="bottom"
> >
<time dateTime={this.props.dateTime}> <time dateTime={this.props.dateTime}>

View File

@ -1,5 +1,5 @@
// @flow // @flow
import { keyframes } from 'styled-components'; import { keyframes } from "styled-components";
export const fadeIn = keyframes` export const fadeIn = keyframes`
from { opacity: 0; } from { opacity: 0; }

View File

@ -1,6 +1,6 @@
// @flow // @flow
import styledNormalize from 'styled-normalize'; import styledNormalize from "styled-normalize";
import { createGlobalStyle } from 'styled-components'; import { createGlobalStyle } from "styled-components";
export default createGlobalStyle` export default createGlobalStyle`
${styledNormalize} ${styledNormalize}

View File

@ -1,44 +1,44 @@
// @flow // @flow
import { darken, lighten } from 'polished'; import { darken, lighten } from "polished";
const colors = { const colors = {
almostBlack: '#111319', almostBlack: "#111319",
lightBlack: '#2F3336', lightBlack: "#2F3336",
almostWhite: '#E6E6E6', almostWhite: "#E6E6E6",
veryDarkBlue: '#08090C', veryDarkBlue: "#08090C",
slate: '#9BA6B2', slate: "#9BA6B2",
slateLight: '#DAE1E9', slateLight: "#DAE1E9",
slateDark: '#4E5C6E', slateDark: "#4E5C6E",
smoke: '#F4F7FA', smoke: "#F4F7FA",
smokeLight: '#F9FBFC', smokeLight: "#F9FBFC",
smokeDark: '#E8EBED', smokeDark: "#E8EBED",
white: '#FFF', white: "#FFF",
white10: 'rgba(255, 255, 255, 0.1)', white10: "rgba(255, 255, 255, 0.1)",
white50: 'rgba(255, 255, 255, 0.5)', white50: "rgba(255, 255, 255, 0.5)",
black: '#000', black: "#000",
black05: 'rgba(0, 0, 0, 0.05)', black05: "rgba(0, 0, 0, 0.05)",
black10: 'rgba(0, 0, 0, 0.1)', black10: "rgba(0, 0, 0, 0.1)",
black50: 'rgba(0, 0, 0, 0.50)', black50: "rgba(0, 0, 0, 0.50)",
primary: '#0366d6', primary: "#0366d6",
yellow: '#FBCA04', yellow: "#FBCA04",
warmGrey: '#EDF2F7', warmGrey: "#EDF2F7",
danger: '#D0021B', danger: "#D0021B",
warning: '#f08a24', warning: "#f08a24",
success: '#2f3336', success: "#2f3336",
info: '#a0d3e8', info: "#a0d3e8",
}; };
const spacing = { const spacing = {
padding: '1.5vw 1.875vw', padding: "1.5vw 1.875vw",
vpadding: '1.5vw', vpadding: "1.5vw",
hpadding: '1.875vw', hpadding: "1.875vw",
sidebarWidth: '280px', sidebarWidth: "280px",
sidebarMinWidth: '250px', sidebarMinWidth: "250px",
sidebarMaxWidth: '350px', sidebarMaxWidth: "350px",
}; };
export const base = { export const base = {
@ -49,29 +49,29 @@ export const base = {
fontFamilyMono: fontFamilyMono:
"'SFMono-Regular',Consolas,'Liberation Mono', Menlo, Courier,monospace", "'SFMono-Regular',Consolas,'Liberation Mono', Menlo, Courier,monospace",
fontWeight: 400, fontWeight: 400,
backgroundTransition: 'background 100ms ease-in-out', backgroundTransition: "background 100ms ease-in-out",
zIndex: 100, zIndex: 100,
selected: colors.primary, selected: colors.primary,
buttonBackground: colors.primary, buttonBackground: colors.primary,
buttonText: colors.white, buttonText: colors.white,
textHighlight: '#B3E7FF', textHighlight: "#B3E7FF",
codeComment: '#6a737d', codeComment: "#6a737d",
codePunctuation: '#5e6687', codePunctuation: "#5e6687",
codeNumber: '#d73a49', codeNumber: "#d73a49",
codeProperty: '#c08b30', codeProperty: "#c08b30",
codeTag: '#3d8fd1', codeTag: "#3d8fd1",
codeString: '#032f62', codeString: "#032f62",
codeSelector: '#6679cc', codeSelector: "#6679cc",
codeAttr: '#c76b29', codeAttr: "#c76b29",
codeEntity: '#22a2c9', codeEntity: "#22a2c9",
codeKeyword: '#d73a49', codeKeyword: "#d73a49",
codeFunction: '#6f42c1', codeFunction: "#6f42c1",
codeStatement: '#22a2c9', codeStatement: "#22a2c9",
codePlaceholder: '#3d8fd1', codePlaceholder: "#3d8fd1",
codeInserted: '#202746', codeInserted: "#202746",
codeImportant: '#c94922', codeImportant: "#c94922",
blockToolbarBackground: colors.white, blockToolbarBackground: colors.white,
blockToolbarTrigger: colors.slate, blockToolbarTrigger: colors.slate,
@ -98,16 +98,16 @@ export const light = {
text: colors.almostBlack, text: colors.almostBlack,
textSecondary: colors.slateDark, textSecondary: colors.slateDark,
textTertiary: colors.slate, textTertiary: colors.slate,
placeholder: '#B1BECC', placeholder: "#B1BECC",
sidebarBackground: colors.warmGrey, sidebarBackground: colors.warmGrey,
sidebarItemBackground: colors.black05, sidebarItemBackground: colors.black10,
sidebarText: 'rgb(78, 92, 110)', sidebarText: "rgb(78, 92, 110)",
shadow: 'rgba(0, 0, 0, 0.2)', shadow: "rgba(0, 0, 0, 0.2)",
menuBackground: colors.white, menuBackground: colors.white,
menuShadow: menuShadow:
'0 0 0 1px rgba(0, 0, 0, 0.05), 0 4px 8px rgba(0, 0, 0, 0.08), 0 2px 4px rgba(0, 0, 0, 0.08)', "0 0 0 1px rgba(0, 0, 0, 0.05), 0 4px 8px rgba(0, 0, 0, 0.08), 0 2px 4px rgba(0, 0, 0, 0.08)",
divider: colors.slateLight, divider: colors.slateLight,
inputBorder: colors.slateLight, inputBorder: colors.slateLight,
inputBorderFocused: colors.slate, inputBorderFocused: colors.slate,
@ -120,7 +120,7 @@ export const light = {
tableDivider: colors.smokeDark, tableDivider: colors.smokeDark,
tableSelected: colors.primary, tableSelected: colors.primary,
tableSelectedBackground: '#E5F7FF', tableSelectedBackground: "#E5F7FF",
buttonNeutralBackground: colors.white, buttonNeutralBackground: colors.white,
buttonNeutralText: colors.almostBlack, buttonNeutralText: colors.almostBlack,
@ -135,7 +135,7 @@ export const light = {
quote: colors.slateLight, quote: colors.slateLight,
codeBackground: colors.smoke, codeBackground: colors.smoke,
codeBorder: colors.smokeDark, codeBorder: colors.smokeDark,
embedBorder: '#DDD #DDD #CCC', embedBorder: "#DDD #DDD #CCC",
horizontalRule: colors.smokeDark, horizontalRule: colors.smokeDark,
}; };
@ -153,12 +153,12 @@ export const dark = {
sidebarBackground: colors.veryDarkBlue, sidebarBackground: colors.veryDarkBlue,
sidebarItemBackground: colors.veryDarkBlue, sidebarItemBackground: colors.veryDarkBlue,
sidebarText: colors.slate, sidebarText: colors.slate,
shadow: 'rgba(0, 0, 0, 0.6)', shadow: "rgba(0, 0, 0, 0.6)",
menuBorder: lighten(0.1, colors.almostBlack), menuBorder: lighten(0.1, colors.almostBlack),
menuBackground: lighten(0.015, colors.almostBlack), menuBackground: lighten(0.015, colors.almostBlack),
menuShadow: menuShadow:
'0 0 0 1px rgba(0, 0, 0, 0.1), 0 8px 16px rgba(0, 0, 0, 0.3), 0 2px 4px rgba(0, 0, 0, 0.08), inset 0 0 1px rgba(255,255,255,.2)', "0 0 0 1px rgba(0, 0, 0, 0.1), 0 8px 16px rgba(0, 0, 0, 0.3), 0 2px 4px rgba(0, 0, 0, 0.08), inset 0 0 1px rgba(255,255,255,.2)",
divider: darken(0.2, colors.slate), divider: darken(0.2, colors.slate),
inputBorder: colors.slateDark, inputBorder: colors.slateDark,
inputBorderFocused: colors.slate, inputBorderFocused: colors.slate,
@ -171,7 +171,7 @@ export const dark = {
tableDivider: colors.lightBlack, tableDivider: colors.lightBlack,
tableSelected: colors.primary, tableSelected: colors.primary,
tableSelectedBackground: '#002333', tableSelectedBackground: "#002333",
buttonNeutralBackground: colors.almostBlack, buttonNeutralBackground: colors.almostBlack,
buttonNeutralText: colors.white, buttonNeutralText: colors.white,
@ -186,7 +186,7 @@ export const dark = {
quote: colors.almostWhite, quote: colors.almostWhite,
codeBackground: colors.black, codeBackground: colors.black,
codeBorder: colors.black50, codeBorder: colors.black50,
codeString: '#3d8fd1', codeString: "#3d8fd1",
embedBorder: colors.black50, embedBorder: colors.black50,
horizontalRule: darken(0.2, colors.slate), horizontalRule: darken(0.2, colors.slate),
}; };

View File

@ -1,5 +1,5 @@
// @flow // @flow
import { trim } from 'lodash'; import { trim } from "lodash";
type Domain = { type Domain = {
tld: string, tld: string,
@ -11,14 +11,14 @@ type Domain = {
// a large list of possible TLD's which increase the size of the bundle // a large list of possible TLD's which increase the size of the bundle
// unneccessarily for our usecase of trusted input. // unneccessarily for our usecase of trusted input.
export function parseDomain(url: string): ?Domain { export function parseDomain(url: string): ?Domain {
if (typeof url !== 'string') return null; if (typeof url !== "string") return null;
// strip extermeties and whitespace from input // strip extermeties and whitespace from input
const normalizedDomain = trim(url.replace(/(https?:)?\/\//, '')); const normalizedDomain = trim(url.replace(/(https?:)?\/\//, ""));
const parts = normalizedDomain.split('.'); const parts = normalizedDomain.split(".");
// ensure the last part only includes something that looks like a TLD // ensure the last part only includes something that looks like a TLD
function cleanTLD(tld = '') { function cleanTLD(tld = "") {
return tld.split(/[/:?]/)[0]; return tld.split(/[/:?]/)[0];
} }
@ -28,14 +28,14 @@ export function parseDomain(url: string): ?Domain {
return { return {
subdomain: parts[0], subdomain: parts[0],
domain: parts[1], domain: parts[1],
tld: cleanTLD(parts.slice(2).join('.')), tld: cleanTLD(parts.slice(2).join(".")),
}; };
} }
if (parts.length === 2) { if (parts.length === 2) {
return { return {
subdomain: '', subdomain: "",
domain: parts[0], domain: parts[0],
tld: cleanTLD(parts.slice(1).join('.')), tld: cleanTLD(parts.slice(1).join(".")),
}; };
} }
@ -44,7 +44,7 @@ export function parseDomain(url: string): ?Domain {
export function getCookieDomain(domain: string) { export function getCookieDomain(domain: string) {
// TODO: All the process.env parsing needs centralizing // TODO: All the process.env parsing needs centralizing
return process.env.SUBDOMAINS_ENABLED === 'true' || return process.env.SUBDOMAINS_ENABLED === "true" ||
process.env.SUBDOMAINS_ENABLED === true process.env.SUBDOMAINS_ENABLED === true
? stripSubdomain(domain) ? stripSubdomain(domain)
: domain; : domain;
@ -61,69 +61,69 @@ export function stripSubdomain(hostname: string) {
export function isCustomSubdomain(hostname: string) { export function isCustomSubdomain(hostname: string) {
const parsed = parseDomain(hostname); const parsed = parseDomain(hostname);
if (!parsed) return false; if (!parsed) return false;
if (!parsed.subdomain || parsed.subdomain === 'www') return false; if (!parsed.subdomain || parsed.subdomain === "www") return false;
return true; return true;
} }
export const RESERVED_SUBDOMAINS = [ export const RESERVED_SUBDOMAINS = [
'about', "about",
'account', "account",
'admin', "admin",
'advertising', "advertising",
'api', "api",
'assets', "assets",
'archive', "archive",
'beta', "beta",
'billing', "billing",
'blog', "blog",
'cache', "cache",
'cdn', "cdn",
'code', "code",
'community', "community",
'dashboard', "dashboard",
'developer', "developer",
'developers', "developers",
'forum', "forum",
'help', "help",
'home', "home",
'http', "http",
'https', "https",
'imap', "imap",
'localhost', "localhost",
'mail', "mail",
'marketing', "marketing",
'mobile', "mobile",
'new', "new",
'news', "news",
'newsletter', "newsletter",
'ns1', "ns1",
'ns2', "ns2",
'ns3', "ns3",
'ns4', "ns4",
'password', "password",
'profile', "profile",
'sandbox', "sandbox",
'script', "script",
'scripts', "scripts",
'setup', "setup",
'signin', "signin",
'signup', "signup",
'site', "site",
'smtp', "smtp",
'support', "support",
'status', "status",
'static', "static",
'stats', "stats",
'test', "test",
'update', "update",
'updates', "updates",
'ws', "ws",
'wss', "wss",
'web', "web",
'websockets', "websockets",
'www', "www",
'www1', "www1",
'www2', "www2",
'www3', "www3",
'www4', "www4",
]; ];

View File

@ -1,149 +1,149 @@
/* eslint-disable flowtype/require-valid-file-annotation */ /* eslint-disable flowtype/require-valid-file-annotation */
import { stripSubdomain, parseDomain, isCustomSubdomain } from './domains'; import { stripSubdomain, parseDomain, isCustomSubdomain } from "./domains";
// test suite is based on subset of parse-domain module we want to support // test suite is based on subset of parse-domain module we want to support
// https://github.com/peerigon/parse-domain/blob/master/test/parseDomain.test.js // https://github.com/peerigon/parse-domain/blob/master/test/parseDomain.test.js
describe('#parseDomain', () => { describe("#parseDomain", () => {
it('should remove the protocol', () => { it("should remove the protocol", () => {
expect(parseDomain('http://example.com')).toMatchObject({ expect(parseDomain("http://example.com")).toMatchObject({
subdomain: '', subdomain: "",
domain: 'example', domain: "example",
tld: 'com', tld: "com",
}); });
expect(parseDomain('//example.com')).toMatchObject({ expect(parseDomain("//example.com")).toMatchObject({
subdomain: '', subdomain: "",
domain: 'example', domain: "example",
tld: 'com', tld: "com",
}); });
expect(parseDomain('https://example.com')).toMatchObject({ expect(parseDomain("https://example.com")).toMatchObject({
subdomain: '', subdomain: "",
domain: 'example', domain: "example",
tld: 'com', tld: "com",
}); });
}); });
it('should remove sub-domains', () => { it("should remove sub-domains", () => {
expect(parseDomain('www.example.com')).toMatchObject({ expect(parseDomain("www.example.com")).toMatchObject({
subdomain: 'www', subdomain: "www",
domain: 'example', domain: "example",
tld: 'com', tld: "com",
}); });
}); });
it('should remove the path', () => { it("should remove the path", () => {
expect(parseDomain('example.com/some/path?and&query')).toMatchObject({ expect(parseDomain("example.com/some/path?and&query")).toMatchObject({
subdomain: '', subdomain: "",
domain: 'example', domain: "example",
tld: 'com', tld: "com",
}); });
expect(parseDomain('example.com/')).toMatchObject({ expect(parseDomain("example.com/")).toMatchObject({
subdomain: '', subdomain: "",
domain: 'example', domain: "example",
tld: 'com', tld: "com",
}); });
}); });
it('should remove the query string', () => { it("should remove the query string", () => {
expect(parseDomain('example.com?and&query')).toMatchObject({ expect(parseDomain("example.com?and&query")).toMatchObject({
subdomain: '', subdomain: "",
domain: 'example', domain: "example",
tld: 'com', tld: "com",
}); });
}); });
it('should remove special characters', () => { it("should remove special characters", () => {
expect(parseDomain('http://m.example.com\r')).toMatchObject({ expect(parseDomain("http://m.example.com\r")).toMatchObject({
subdomain: 'm', subdomain: "m",
domain: 'example', domain: "example",
tld: 'com', tld: "com",
}); });
}); });
it('should remove the port', () => { it("should remove the port", () => {
expect(parseDomain('example.com:8080')).toMatchObject({ expect(parseDomain("example.com:8080")).toMatchObject({
subdomain: '', subdomain: "",
domain: 'example', domain: "example",
tld: 'com', tld: "com",
}); });
}); });
it('should allow @ characters in the path', () => { it("should allow @ characters in the path", () => {
expect(parseDomain('https://medium.com/@username/')).toMatchObject({ expect(parseDomain("https://medium.com/@username/")).toMatchObject({
subdomain: '', subdomain: "",
domain: 'medium', domain: "medium",
tld: 'com', tld: "com",
}); });
}); });
it('should also work with three-level domains like .co.uk', () => { it("should also work with three-level domains like .co.uk", () => {
expect(parseDomain('www.example.co.uk')).toMatchObject({ expect(parseDomain("www.example.co.uk")).toMatchObject({
subdomain: 'www', subdomain: "www",
domain: 'example', domain: "example",
tld: 'co.uk', tld: "co.uk",
}); });
}); });
it('should not include private domains like blogspot.com by default', () => { it("should not include private domains like blogspot.com by default", () => {
expect(parseDomain('foo.blogspot.com')).toMatchObject({ expect(parseDomain("foo.blogspot.com")).toMatchObject({
subdomain: 'foo', subdomain: "foo",
domain: 'blogspot', domain: "blogspot",
tld: 'com', tld: "com",
}); });
}); });
it('should also work with the minimum', () => { it("should also work with the minimum", () => {
expect(parseDomain('example.com')).toMatchObject({ expect(parseDomain("example.com")).toMatchObject({
subdomain: '', subdomain: "",
domain: 'example', domain: "example",
tld: 'com', tld: "com",
}); });
}); });
it('should return null if the given value is not a string', () => { it("should return null if the given value is not a string", () => {
expect(parseDomain(undefined)).toBe(null); expect(parseDomain(undefined)).toBe(null);
expect(parseDomain({})).toBe(null); expect(parseDomain({})).toBe(null);
expect(parseDomain('')).toBe(null); expect(parseDomain("")).toBe(null);
}); });
it('should work with custom top-level domains (eg .local)', () => { it("should work with custom top-level domains (eg .local)", () => {
expect(parseDomain('mymachine.local')).toMatchObject({ expect(parseDomain("mymachine.local")).toMatchObject({
subdomain: '', subdomain: "",
domain: 'mymachine', domain: "mymachine",
tld: 'local', tld: "local",
}); });
}); });
}); });
describe('#stripSubdomain', () => { describe("#stripSubdomain", () => {
test('to work with localhost', () => { test("to work with localhost", () => {
expect(stripSubdomain('localhost')).toBe('localhost'); expect(stripSubdomain("localhost")).toBe("localhost");
}); });
test('to return domains without a subdomain', () => { test("to return domains without a subdomain", () => {
expect(stripSubdomain('example')).toBe('example'); expect(stripSubdomain("example")).toBe("example");
expect(stripSubdomain('example.com')).toBe('example.com'); expect(stripSubdomain("example.com")).toBe("example.com");
expect(stripSubdomain('example.org:3000')).toBe('example.org'); expect(stripSubdomain("example.org:3000")).toBe("example.org");
}); });
test('to remove subdomains', () => { test("to remove subdomains", () => {
expect(stripSubdomain('test.example.com')).toBe('example.com'); expect(stripSubdomain("test.example.com")).toBe("example.com");
expect(stripSubdomain('test.example.com:3000')).toBe('example.com'); expect(stripSubdomain("test.example.com:3000")).toBe("example.com");
}); });
}); });
describe('#isCustomSubdomain', () => { describe("#isCustomSubdomain", () => {
test('to work with localhost', () => { test("to work with localhost", () => {
expect(isCustomSubdomain('localhost')).toBe(false); expect(isCustomSubdomain("localhost")).toBe(false);
}); });
test('to return false for domains without a subdomain', () => { test("to return false for domains without a subdomain", () => {
expect(isCustomSubdomain('example')).toBe(false); expect(isCustomSubdomain("example")).toBe(false);
expect(isCustomSubdomain('example.com')).toBe(false); expect(isCustomSubdomain("example.com")).toBe(false);
expect(isCustomSubdomain('example.org:3000')).toBe(false); expect(isCustomSubdomain("example.org:3000")).toBe(false);
}); });
test('to return false for www', () => { test("to return false for www", () => {
expect(isCustomSubdomain('www.example.com')).toBe(false); expect(isCustomSubdomain("www.example.com")).toBe(false);
expect(isCustomSubdomain('www.example.com:3000')).toBe(false); expect(isCustomSubdomain("www.example.com:3000")).toBe(false);
}); });
test('to return true for subdomains', () => { test("to return true for subdomains", () => {
expect(isCustomSubdomain('test.example.com')).toBe(true); expect(isCustomSubdomain("test.example.com")).toBe(true);
expect(isCustomSubdomain('test.example.com:3000')).toBe(true); expect(isCustomSubdomain("test.example.com:3000")).toBe(true);
}); });
}); });

View File

@ -1,11 +1,12 @@
// @flow // @flow
export default function getQueryVariable(variable: string) { export default function getQueryVariable(variable: string) {
var query = window.location.search.substring(1); const query = window.location.search.substring(1);
var vars = query.split('&'); const vars = query.split("&");
for (var i = 0; i < vars.length; i++) { for (var i = 0; i < vars.length; i++) {
var pair = vars[i].split('='); const pair = vars[i].split("=");
if (pair[0] == variable) {
if (pair[0] === variable) {
return pair[1]; return pair[1];
} }
} }

View File

@ -1,10 +1,10 @@
// @flow // @flow
import { deburr } from 'lodash'; import { deburr } from "lodash";
import naturalSort from 'natural-sort'; import naturalSort from "natural-sort";
type NaturalSortOptions = { type NaturalSortOptions = {
caseSensitive?: boolean, caseSensitive?: boolean,
direction?: 'asc' | 'desc', direction?: "asc" | "desc",
}; };
const sorter = naturalSort(); const sorter = naturalSort();
@ -13,7 +13,7 @@ function getSortByField<T: Object>(
item: T, item: T,
keyOrCallback: string | (T => string) keyOrCallback: string | (T => string)
) { ) {
if (typeof keyOrCallback === 'string') { if (typeof keyOrCallback === "string") {
return deburr(item[keyOrCallback]); return deburr(item[keyOrCallback]);
} }

View File

@ -1,5 +1,5 @@
// @flow // @flow
import { parser } from 'rich-markdown-editor'; import { parser } from "rich-markdown-editor";
export default function parseDocumentIds(text: string): string[] { export default function parseDocumentIds(text: string): string[] {
const value = parser.parse(text); const value = parser.parse(text);
@ -7,15 +7,15 @@ export default function parseDocumentIds(text: string): string[] {
function findLinks(node) { function findLinks(node) {
// get text nodes // get text nodes
if (node.type.name === 'text') { if (node.type.name === "text") {
// get marks for text nodes // get marks for text nodes
node.marks.forEach(mark => { node.marks.forEach(mark => {
// any of the marks links? // any of the marks links?
if (mark.type.name === 'link') { if (mark.type.name === "link") {
const { href } = mark.attrs; const { href } = mark.attrs;
// any of the links to other docs? // any of the links to other docs?
if (href.startsWith('/doc')) { if (href.startsWith("/doc")) {
const tokens = href.replace(/\/$/, '').split('/'); const tokens = href.replace(/\/$/, "").split("/");
const lastToken = tokens[tokens.length - 1]; const lastToken = tokens[tokens.length - 1];
// don't return the same link more than once // don't return the same link more than once

View File

@ -1,21 +1,21 @@
/* eslint-disable flowtype/require-valid-file-annotation */ /* eslint-disable flowtype/require-valid-file-annotation */
import parseDocumentIds from './parseDocumentIds'; import parseDocumentIds from "./parseDocumentIds";
it('should not return non links', () => { it("should not return non links", () => {
expect(parseDocumentIds(`# Header`).length).toBe(0); expect(parseDocumentIds(`# Header`).length).toBe(0);
}); });
it('should return an array of document ids', () => { it("should return an array of document ids", () => {
const result = parseDocumentIds(`# Header const result = parseDocumentIds(`# Header
[internal](/doc/test-456733) [internal](/doc/test-456733)
`); `);
expect(result.length).toBe(1); expect(result.length).toBe(1);
expect(result[0]).toBe('test-456733'); expect(result[0]).toBe("test-456733");
}); });
it('should not return duplicate document ids', () => { it("should not return duplicate document ids", () => {
expect(parseDocumentIds(`# Header`).length).toBe(0); expect(parseDocumentIds(`# Header`).length).toBe(0);
const result = parseDocumentIds(`# Header const result = parseDocumentIds(`# Header
@ -26,13 +26,13 @@ it('should not return duplicate document ids', () => {
`); `);
expect(result.length).toBe(1); expect(result.length).toBe(1);
expect(result[0]).toBe('test-456733'); expect(result[0]).toBe("test-456733");
}); });
it('should not return non document links', () => { it("should not return non document links", () => {
expect(parseDocumentIds(`[google](http://www.google.com)`).length).toBe(0); expect(parseDocumentIds(`[google](http://www.google.com)`).length).toBe(0);
}); });
it('should not return non document relative links', () => { it("should not return non document relative links", () => {
expect(parseDocumentIds(`[relative](/developers)`).length).toBe(0); expect(parseDocumentIds(`[relative](/developers)`).length).toBe(0);
}); });

View File

@ -1,13 +1,13 @@
// @flow // @flow
import emojiRegex from 'emoji-regex'; import emojiRegex from "emoji-regex";
import unescape from './unescape'; import unescape from "./unescape";
export default function parseTitle(text: string = '') { export default function parseTitle(text: string = "") {
const regex = emojiRegex(); const regex = emojiRegex();
// find and extract title // find and extract title
const firstLine = text.trim().split(/\r?\n/)[0]; const firstLine = text.trim().split(/\r?\n/)[0];
const trimmedTitle = firstLine.replace(/^#/, '').trim(); const trimmedTitle = firstLine.replace(/^#/, "").trim();
// remove any escape characters // remove any escape characters
const title = unescape(trimmedTitle); const title = unescape(trimmedTitle);

View File

@ -1,28 +1,28 @@
/* eslint-disable flowtype/require-valid-file-annotation */ /* eslint-disable flowtype/require-valid-file-annotation */
import parseTitle from './parseTitle'; import parseTitle from "./parseTitle";
it('should trim the title', () => { it("should trim the title", () => {
expect(parseTitle(`# Lots of space `).title).toBe('Lots of space'); expect(parseTitle(`# Lots of space `).title).toBe("Lots of space");
}); });
it('should extract first title', () => { it("should extract first title", () => {
expect(parseTitle(`# Title one\n# Title two`).title).toBe('Title one'); expect(parseTitle(`# Title one\n# Title two`).title).toBe("Title one");
}); });
it('should remove escape characters', () => { it("should remove escape characters", () => {
expect(parseTitle(`# Thing \\- one`).title).toBe('Thing - one'); expect(parseTitle(`# Thing \\- one`).title).toBe("Thing - one");
expect(parseTitle(`# \\[wip\\] Title`).title).toBe('[wip] Title'); expect(parseTitle(`# \\[wip\\] Title`).title).toBe("[wip] Title");
expect(parseTitle(`# \\> Title`).title).toBe('> Title'); expect(parseTitle(`# \\> Title`).title).toBe("> Title");
}); });
it('should parse emoji if first character', () => { it("should parse emoji if first character", () => {
const parsed = parseTitle(`# 😀 Title`); const parsed = parseTitle(`# 😀 Title`);
expect(parsed.title).toBe('😀 Title'); expect(parsed.title).toBe("😀 Title");
expect(parsed.emoji).toBe('😀'); expect(parsed.emoji).toBe("😀");
}); });
it('should not parse emoji if not first character', () => { it("should not parse emoji if not first character", () => {
const parsed = parseTitle(`# Title 🌈`); const parsed = parseTitle(`# Title 🌈`);
expect(parsed.title).toBe('Title 🌈'); expect(parsed.title).toBe("Title 🌈");
expect(parsed.emoji).toBe(undefined); expect(parsed.emoji).toBe(undefined);
}); });

View File

@ -3,54 +3,54 @@
export function slackAuth( export function slackAuth(
state: string, state: string,
scopes: string[] = [ scopes: string[] = [
'identity.email', "identity.email",
'identity.basic', "identity.basic",
'identity.avatar', "identity.avatar",
'identity.team', "identity.team",
], ],
redirectUri: string = `${process.env.URL}/auth/slack.callback` redirectUri: string = `${process.env.URL}/auth/slack.callback`
): string { ): string {
const baseUrl = 'https://slack.com/oauth/authorize'; const baseUrl = "https://slack.com/oauth/authorize";
const params = { const params = {
client_id: process.env.SLACK_KEY, client_id: process.env.SLACK_KEY,
scope: scopes ? scopes.join(' ') : '', scope: scopes ? scopes.join(" ") : "",
redirect_uri: redirectUri, redirect_uri: redirectUri,
state, state,
}; };
const urlParams = Object.keys(params) const urlParams = Object.keys(params)
.map(key => `${key}=${encodeURIComponent(params[key])}`) .map(key => `${key}=${encodeURIComponent(params[key])}`)
.join('&'); .join("&");
return `${baseUrl}?${urlParams}`; return `${baseUrl}?${urlParams}`;
} }
export function githubUrl(): string { export function githubUrl(): string {
return 'https://www.github.com/outline'; return "https://www.github.com/outline";
} }
export function githubIssuesUrl(): string { export function githubIssuesUrl(): string {
return 'https://www.github.com/outline/outline/issues'; return "https://www.github.com/outline/outline/issues";
} }
export function slackAppStoreUrl(): string { export function slackAppStoreUrl(): string {
return 'https://goabstract.slack.com/apps/A0W3UMKBQ-outline'; return "https://goabstract.slack.com/apps/A0W3UMKBQ-outline";
} }
export function blogUrl(): string { export function blogUrl(): string {
return 'https://medium.com/getoutline'; return "https://medium.com/getoutline";
} }
export function twitterUrl(): string { export function twitterUrl(): string {
return 'https://twitter.com/outlinewiki'; return "https://twitter.com/outlinewiki";
} }
export function spectrumUrl(): string { export function spectrumUrl(): string {
return 'https://spectrum.chat/outline'; return "https://spectrum.chat/outline";
} }
export function mailToUrl(): string { export function mailToUrl(): string {
return 'mailto:hello@getoutline.com'; return "mailto:hello@getoutline.com";
} }
export function features(): string { export function features(): string {
@ -65,7 +65,7 @@ export function changelog(): string {
return `https://www.getoutline.com/changelog`; return `https://www.getoutline.com/changelog`;
} }
export function signin(service: string = 'slack'): string { export function signin(service: string = "slack"): string {
return `${process.env.URL}/auth/${service}`; return `${process.env.URL}/auth/${service}`;
} }

View File

@ -1,8 +1,8 @@
// @flow // @flow
import slugify from 'slugify'; import slugify from "slugify";
// Slugify, escape, and remove periods from headings so that they are // Slugify, escape, and remove periods from headings so that they are
// compatible with url hashes AND dom selectors // compatible with url hashes AND dom selectors
export default function safeSlugify(text: string) { export default function safeSlugify(text: string) {
return `h-${escape(slugify(text, { lower: true }).replace('.', '-'))}`; return `h-${escape(slugify(text, { lower: true }).replace(".", "-"))}`;
} }

View File

@ -2,8 +2,8 @@
const unescape = (text: string) => { const unescape = (text: string) => {
return text return text
.replace(/\\([\\`*{}[\]()#+\-.!_>])/g, '$1') .replace(/\\([\\`*{}[\]()#+\-.!_>])/g, "$1")
.replace(/\n\\\n/g, '\n\n'); .replace(/\n\\\n/g, "\n\n");
}; };
export default unescape; export default unescape;