diff --git a/.env.sample b/.env.sample index e4c505d0..1b41ae5f 100644 --- a/.env.sample +++ b/.env.sample @@ -1,26 +1,40 @@ -# Copy this file to .env, remove this comment and change the keys. For development -# with docker this should mostly work out of the box other than setting the Slack -# keys (for auth) and the SECRET_KEY. -# -# Please use `openssl rand -hex 32` to create SECRET_KEY +# 👋 Welcome, we're glad you're setting up an installation of Outline. Copy this +# file to .env or set the variables in your local environment manually. For +# development with docker this should mostly work out of the box other than +# setting the Slack keys and the SECRET_KEY. + + + + +# –––––––––––––––– REQUIRED –––––––––––––––– + +# Generate a unique random key, you can use `openssl rand -hex 32` in terminal +# DO NOT LEAVE UNSET SECRET_KEY=generate_a_new_key + +# Generate a unique random key, you can use `openssl rand -hex 32` in terminal +# DO NOT LEAVE UNSET UTILS_SECRET=generate_a_new_key +# For production point these at your databases, in development the default +# should work out of the box. DATABASE_URL=postgres://user:pass@localhost:5532/outline DATABASE_URL_TEST=postgres://user:pass@localhost:5532/outline-test REDIS_URL=redis://localhost:6479 +# URL should point to the fully qualified, publicly accessible URL. If using a +# proxy the port in URL and PORT may be different. URL=http://localhost:3000 PORT=3000 -# enforce (auto redirect to) https in production, (optional) default is true. -# set to false if your SSL is terminated at a loadbalancer, for example -FORCE_HTTPS=true +# Third party signin credentials, at least one of EITHER Google OR Slack is +# required for a working installation or you'll have no sign-in options. -ENABLE_UPDATES=true -DEBUG=cache,presenters,events,emails,mailer,utils,multiplayer,server,services - -# Third party signin credentials (at least one is required) +# To configure Slack auth, you'll need to create an Application at +# => https://api.slack.com/apps +# +# When configuring the Client ID, add a redirect URL under "OAuth & Permissions": +# https:///auth/slack.callback SLACK_KEY=get_a_key_from_slack SLACK_SECRET=get_the_secret_of_above_key @@ -28,22 +42,59 @@ SLACK_SECRET=get_the_secret_of_above_key # => https://console.cloud.google.com/apis/credentials # # When configuring the Client ID, add an Authorized redirect URI: -# https:///auth/google.callback +# https:///auth/google.callback GOOGLE_CLIENT_ID= GOOGLE_CLIENT_SECRET= -# Comma separated list of domains to be allowed (optional) -# If not set, all Google apps domains are allowed by default + + + +# –––––––––––––––– OPTIONAL –––––––––––––––– + +# If using a Cloudfront/Cloudflare distribution or similar it can be set below. +# This will cause paths to javascript, stylesheets, and images to be updated to +# the hostname defined in CDN_URL. In your CDN configuration the origin server +# should be set to the same as URL. +CDN_URL= + +# Auto-redirect to https in production. The default is true but you may set to +# false if you can be sure that SSL is terminated at an external loadbalancer. +FORCE_HTTPS=true + +# Have the installation check for updates by sending anonymized statistics to +# the maintainers +ENABLE_UPDATES=true + +# You may enable or disable debugging categories to increase the noisiness of +# logs. The default is a good balance +DEBUG=cache,presenters,events,emails,mailer,utils,multiplayer,server,services + +# Comma separated list of domains to be allowed to signin to the wiki. If not +# set, all domains are allowed by default when using Google OAuth to signin GOOGLE_ALLOWED_DOMAINS= -# Third party credentials (optional) -SLACK_VERIFICATION_TOKEN=PLxk6OlXXXXXVj3YYYY +# For a complete Slack integration with search and posting to channels the +# following configs are also needed, some more details +# => https://wiki.generaloutline.com/share/be25efd1-b3ef-4450-b8e5-c4a4fc11e02a +# +SLACK_VERIFICATION_TOKEN=your_token SLACK_APP_ID=A0XXXXXXX SLACK_MESSAGE_ACTIONS=true + +# Optionally enable google analytics to track pageviews in the knowledge base GOOGLE_ANALYTICS_ID= + +# Optionally enable Sentry (sentry.io) to track errors and performance SENTRY_DSN= -# AWS credentials (optional in development) +# To support uploading of images for avatars and document attachments an +# s3-compatible storage must be provided. AWS S3 is recommended for redundency +# however if you want to keep all file storage local an alternative such as +# minio (https://github.com/minio/minio) can be used. + +# A more detailed guide on setting up S3 is available here: +# => https://wiki.generaloutline.com/share/125de1cc-9ff6-424b-8415-0d58c809a40f +# AWS_ACCESS_KEY_ID=get_a_key_from_aws AWS_SECRET_ACCESS_KEY=get_the_secret_of_above_key AWS_REGION=xx-xxxx-x @@ -51,11 +102,10 @@ AWS_S3_UPLOAD_BUCKET_URL=http://s3:4569 AWS_S3_UPLOAD_BUCKET_NAME=bucket_name_here AWS_S3_UPLOAD_MAX_SIZE=26214400 AWS_S3_FORCE_PATH_STYLE=true -# uploaded s3 objects permission level, default is private -# set to "public-read" to allow public access AWS_S3_ACL=private -# Emails configuration (optional) +# To support sending outgoing transactional emails such as "document updated" or +# "you've been invited" you'll need to provide authentication for an SMTP server SMTP_HOST= SMTP_PORT= SMTP_USERNAME= @@ -66,4 +116,6 @@ SMTP_REPLY_EMAIL= # Custom logo that displays on the authentication screen, scaled to height: 60px # TEAM_LOGO=https://example.com/images/logo.png +# The default interface language. See translate.getoutline.com for a list of +# available language codes and their rough percentage translated. DEFAULT_LANGUAGE=en_US \ No newline at end of file diff --git a/.flowconfig b/.flowconfig index 23473ecb..0a37806b 100644 --- a/.flowconfig +++ b/.flowconfig @@ -18,6 +18,7 @@ [options] emoji=true +sharedmemory.heap_size=3221225472 module.system.node.resolve_dirname=node_modules module.system.node.resolve_dirname=app @@ -32,6 +33,7 @@ module.file_ext=.json esproposal.decorators=ignore esproposal.class_static_fields=enable esproposal.class_instance_fields=enable +esproposal.optional_chaining=enable suppress_comment=\\(.\\|\n\\)*\\$FlowFixMe suppress_comment=\\(.\\|\n\\)*\\$FlowIssue diff --git a/.vscode/settings.json b/.vscode/settings.json index f5852ab5..041e71a9 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,6 +1,7 @@ { "javascript.validate.enable": false, + "javascript.format.enable": false, "typescript.validate.enable": false, + "typescript.format.enable": false, "editor.formatOnSave": true, - "typescript.format.enable": false } \ No newline at end of file diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md new file mode 100644 index 00000000..b996d572 --- /dev/null +++ b/ARCHITECTURE.md @@ -0,0 +1,66 @@ + +# Architecture + +Outline is composed of a backend and frontend codebase in this monorepo. As both are written in Javascript, they share some code where possible. We utilize the latest ES6 language features, including `async`/`await`, and [Flow](https://flow.org/) typing. Prettier formatting and ESLint are enforced by CI. + +## Frontend + +Outline's frontend is a React application compiled with [Webpack](https://webpack.js.org/). It uses [MobX](https://mobx.js.org/) for state management and [Styled Components](https://www.styled-components.com/) for component styles. Unless global, state logic and styles are always co-located with React components together with their subcomponents to make the component tree easier to manage. + +> Important Note: The Outline editor is built on [Prosemirror](https://github.com/prosemirror) and managed in a separate open source repository to encourage re-use: [rich-markdown-editor](https://github.com/outline/rich-markdown-editor). + +``` +app +├── components - React components reusable across scenes +├── embeds - Embed definitions that represent rich interactive embeds in the editor +├── hooks - Reusable React hooks +├── menus - Context menus, often appear in multiple places in the UI +├── models - State models using MobX observables +├── routes - Route definitions, note that chunks are async loaded with suspense +├── scenes - A scene represents a full-page view that contains several components +├── stores - Collections of models and associated fetch logic +├── types - Flow types +└── utils - Utility methods specific to the frontend +``` + +## Backend + +The API server is driven by [Koa](http://koajs.com/), it uses [Sequelize](http://docs.sequelizejs.com/) as the ORM and Redis with [Bull](https://github.com/OptimalBits/bull) for queues and async event management. Authorization logic +is contained in [cancan](https://www.npmjs.com/package/cancan) policies under the "policies" directory. + +Interested in more documentation on the API routes? Check out the [API documentation](https://getoutline.com/developers). + +``` +server +├── api - All API routes are contained within here +│ └── middlewares - Koa middlewares specific to the API +├── auth - OAuth routes for Slack and Google, plus email authentication routes +├── commands - We are gradually moving to the command pattern for new write logic +├── config - Database configuration +├── emails - Transactional email templates +│ └── components - Shared React components for email templates +├── middlewares - Koa middlewares +├── migrations - Database migrations +├── models - Sequelize models +├── onboarding - Markdown templates for onboarding documents +├── policies - Authorization logic based on cancan +├── presenters - JSON presenters for database models, the interface between backend -> frontend +├── services - Service definitions are triggered for events and perform async jobs +├── static - Static assets +├── test - Test helpers and fixtures, tests themselves are colocated +└── utils - Utility methods specific to the backend +``` + +## Shared + +Where logic is shared between the client and server it is placed in this directory. This is generally +small utilities. + +``` +shared +├── i18n - Internationalization confiuration +│ └── locales - Language specific translation files +├── styles - Styles, colors and other global aesthetics +├── utils - Shared utility methods +└── constants - Shared constants +``` \ No newline at end of file diff --git a/README.md b/README.md index 40e04a04..33481fab 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ This is the source code that runs [**Outline**](https://www.getoutline.com) and If you'd like to run your own copy of Outline or contribute to development then this is the place for you. -## Installation +# Installation Outline requires the following dependencies: @@ -31,33 +31,58 @@ Outline requires the following dependencies: - Slack or Google developer application for authentication -### Production +## Self-Hosted Production -For a manual self-hosted production installation these are the suggested steps: +### Docker -1. Clone this repo and install dependencies with `yarn install` -1. Build the source code with `yarn build` -1. Using the `.env.sample` as a reference, set the required variables in your production environment. The following are required as a minimum: - 1. `SECRET_KEY` (follow instructions in the comments at the top of `.env`) - 1. `SLACK_KEY` (this is called "Client ID" in Slack admin) - 1. `SLACK_SECRET` (this is called "Client Secret" in Slack admin) - 1. `DATABASE_URL` (run your own local copy of Postgres, or use a cloud service) - 1. `REDIS_URL` (run your own local copy of Redis, or use a cloud service) - 1. `URL` (the public facing URL of your installation) - 1. `AWS_` (all of the keys beginning with AWS) -1. Migrate database schema with `yarn sequelize:migrate`. Production assumes an SSL connection, if -Postgres is on the same machine and is not SSL you can migrate with `yarn sequelize:migrate --env=production-ssl-disabled`. -1. Start the service with any daemon tools you prefer. Take PM2 for example, `NODE_ENV=production pm2 start ./build/server/index.js --name outline ` +For a manual self-hosted production installation these are the recommended steps: + +1. First setup Redis and Postgres servers, this is outside the scope of the guide. +1. Download the latest official Docker image, new releases are available around the middle of every month: + + `docker pull outlinewiki/outline` +1. Using the [.env.sample](.env.sample) as a reference, set the required variables in your production environment. You can export the environment variables directly, or create a `.env` file and pass it to the docker image like so: + + `docker run --env-file=.env outlinewiki/outline` +1. Setup the database with `yarn sequelize:migrate`. Production assumes an SSL connection to the database by default, if +Postgres is on the same machine and is not SSL you can migrate with `yarn sequelize:migrate --env=production-ssl-disabled`, for example: + + `docker run --rm outlinewiki/outline yarn sequelize:migrate` +1. Start the container: + + `docker run outlinewiki/outline` 1. Visit http://you_server_ip:3000 and you should be able to see Outline page > Port number can be changed using the `PORT` environment variable -1. (Optional) You can add an `nginx` reverse proxy to serve your instance of Outline for a clean URL without the port number, support SSL, etc. +1. (Optional) You can add an `nginx` or other reverse proxy to serve your instance of Outline for a clean URL without the port number, support SSL, etc. + +### Terraform + +Alternatively a community member maintains a script to deploy Outline on Google Cloud Platform with [Terraform & Ansible](https://github.com/rjsgn/outline-terraform-ansible). + +### Upgrading + +#### Docker + +If you're running Outline with Docker you'll need to run migrations within the docker container after updating the image. The command will be something like: + +```shell +docker run --rm outlinewiki/outline:latest yarn sequelize:migrate +``` + +#### Git + +If you're running Outline by cloning this repository, run the following command to upgrade: + +```shell +yarn run upgrade +``` -### Development +## Local Development -In development you can quickly get an environment running using Docker by following these steps: +For contributing features and fixes you can quickly get an environment running using Docker by following these steps: 1. Install these dependencies if you don't already have them 1. [Docker for Desktop](https://www.docker.com) @@ -76,9 +101,28 @@ In development you can quickly get an environment running using Docker by follow 1. Run `make up`. This will download dependencies, build and launch a development version of Outline -## Development +# Contributing -### Server +Outline is built and maintained by a small team – we'd love your help to fix bugs and add features! + +Before submitting a pull request please let the core team know by creating or commenting in an issue on [GitHub](https://www.github.com/outline/outline/issues), and we'd also love to hear from you in the [Discussions](https://www.github.com/outline/outline/discussions). This way we can ensure that an approach is agreed on before code is written. This will result in a much higher liklihood of code being accepted. + +If you’re looking for ways to get started, here's a list of ways to help us improve Outline: + +* [Translation](TRANSLATION.md) into other languages +* Issues with [`good first issue`](https://github.com/outline/outline/labels/good%20first%20issue) label +* Performance improvements, both on server and frontend +* Developer happiness and documentation +* Bugs and other issues listed on GitHub + + +## Architecture + +If you're interested in contributing or learning more about the Outline codebase +please refer to the [architecture document](ARCHITECTURE.md) first for a high level overview of how the application is put together. + + +## Debugging Outline uses [debug](https://www.npmjs.com/package/debug). To enable debugging output, the following categories are available: @@ -86,52 +130,6 @@ Outline uses [debug](https://www.npmjs.com/package/debug). To enable debugging o DEBUG=sql,cache,presenters,events,logistics,emails,mailer ``` -## Migrations - -Sequelize is used to create and run migrations, for example: - -``` -yarn sequelize migration:generate --name my-migration -yarn sequelize db:migrate -``` - -Or to run migrations on test database: - -``` -yarn sequelize db:migrate --env test -``` - -## Structure - -Outline is composed of separate backend and frontend application which are both driven by the same Node process. As both are written in Javascript, they share some code but are mostly separate. We utilize the latest language features, including `async`/`await`, and [Flow](https://flow.org/) typing. Prettier and ESLint are enforced by CI. - -### Frontend - -Outline's frontend is a React application compiled with [Webpack](https://webpack.js.org/). It uses [Mobx](https://mobx.js.org/) for state management and [Styled Components](https://www.styled-components.com/) for component styles. Unless global, state logic and styles are always co-located with React components together with their subcomponents to make the component tree easier to manage. - -The editor itself is built on [Prosemirror](https://github.com/prosemirror) and hosted in a separate repository to encourage reuse: [rich-markdown-editor](https://github.com/outline/rich-markdown-editor) - -- `app/` - Frontend React application -- `app/scenes` - Full page views -- `app/components` - Reusable React components -- `app/stores` - Global state stores -- `app/models` - State models -- `app/types` - Flow types for non-models - -### Backend - -Backend is driven by [Koa](http://koajs.com/) (API, web server), [Sequelize](http://docs.sequelizejs.com/) (database) and React for public pages and emails. - -- `server/api` - API endpoints -- `server/commands` - Domain logic, currently being refactored from /models -- `server/emails` - React rendered email templates -- `server/models` - Database models -- `server/policies` - Authorization logic -- `server/presenters` - API responses for database models -- `server/test` - Test helps and support -- `server/utils` - Utility methods -- `shared` - Code shared between frontend and backend applications - ## Tests We aim to have sufficient test coverage for critical parts of the application and aren't aiming for 100% unit test coverage. All API endpoints and anything authentication related should be thoroughly tested. @@ -157,20 +155,21 @@ yarn test:server yarn test:app ``` -## Contributing +## Migrations -Outline is built and maintained by a small team – we'd love your help to fix bugs and add features! +Sequelize is used to create and run migrations, for example: -However, before working on a pull request please let the core team know by creating or commenting in an issue on [GitHub](https://www.github.com/outline/outline/issues), and we'd also love to hear from you in the [Discussions](https://www.github.com/outline/outline/discussions). This way we can ensure that an approach is agreed on before code is written and will hopefully help to get your contributions integrated faster! +``` +yarn sequelize migration:generate --name my-migration +yarn sequelize db:migrate +``` -If you’re looking for ways to get started, here's a list of ways to help us improve Outline: +Or to run migrations on test database: -* [Translation](TRANSLATION.md) into other languages -* Issues with [`good first issue`](https://github.com/outline/outline/labels/good%20first%20issue) label -* Performance improvements, both on server and frontend -* Developer happiness and documentation -* Bugs and other issues listed on GitHub +``` +yarn sequelize db:migrate --env test +``` ## License -Outline is [BSL 1.1 licensed](https://github.com/outline/outline/blob/master/LICENSE). +Outline is [BSL 1.1 licensed](LICENSE). diff --git a/app/components/Actions.js b/app/components/Actions.js index 2e3fbf41..77ebe565 100644 --- a/app/components/Actions.js +++ b/app/components/Actions.js @@ -11,11 +11,6 @@ export const Action = styled(Flex)` font-size: 15px; flex-shrink: 0; - a { - color: ${(props) => props.theme.text}; - height: 24px; - } - &:empty { display: none; } diff --git a/app/components/Authenticated.js b/app/components/Authenticated.js index 505c47a3..86b43ef5 100644 --- a/app/components/Authenticated.js +++ b/app/components/Authenticated.js @@ -20,8 +20,10 @@ const Authenticated = ({ children }: Props) => { // Watching for language changes here as this is the earliest point we have // the user available and means we can start loading translations faster React.useEffect(() => { - if (i18n.language !== language) { - i18n.changeLanguage(language); + if (language && i18n.language !== language) { + // Languages are stored in en_US format in the database, however the + // frontend translation framework (i18next) expects en-US + i18n.changeLanguage(language.replace("_", "-")); } }, [i18n, language]); diff --git a/app/components/Avatar/Avatar.js b/app/components/Avatar/Avatar.js index 46b0e594..38cfe682 100644 --- a/app/components/Avatar/Avatar.js +++ b/app/components/Avatar/Avatar.js @@ -3,13 +3,17 @@ import { observable } from "mobx"; import { observer } from "mobx-react"; import * as React from "react"; import styled from "styled-components"; +import User from "models/User"; import placeholder from "./placeholder.png"; -type Props = { +type Props = {| src: string, size: number, icon?: React.Node, -}; + user?: User, + onClick?: () => void, + className?: string, +|}; @observer class Avatar extends React.Component { diff --git a/app/components/Breadcrumb.js b/app/components/Breadcrumb.js index 33f12809..421866a7 100644 --- a/app/components/Breadcrumb.js +++ b/app/components/Breadcrumb.js @@ -4,7 +4,6 @@ import { ArchiveIcon, EditIcon, GoToIcon, - MoreIcon, PadlockIcon, ShapesIcon, TrashIcon, @@ -14,20 +13,18 @@ import { useTranslation } from "react-i18next"; import { Link } from "react-router-dom"; import styled from "styled-components"; import breakpoint from "styled-components-breakpoint"; - -import CollectionsStore from "stores/CollectionsStore"; import Document from "models/Document"; import CollectionIcon from "components/CollectionIcon"; import Flex from "components/Flex"; -import BreadcrumbMenu from "./BreadcrumbMenu"; import useStores from "hooks/useStores"; +import BreadcrumbMenu from "menus/BreadcrumbMenu"; import { collectionUrl } from "utils/routeHelpers"; -type Props = { +type Props = {| document: Document, - collections: CollectionsStore, + children?: React.Node, onlyText: boolean, -}; +|}; function Icon({ document }) { const { t } = useTranslation(); @@ -35,11 +32,11 @@ function Icon({ document }) { if (document.isDeleted) { return ( <> - +   {t("Trash")} - + ); @@ -47,11 +44,11 @@ function Icon({ document }) { if (document.isArchived) { return ( <> - +   {t("Archive")} - + ); @@ -59,11 +56,11 @@ function Icon({ document }) { if (document.isDraft) { return ( <> - +   {t("Drafts")} - + ); @@ -71,11 +68,11 @@ function Icon({ document }) { if (document.isTemplate) { return ( <> - +   {t("Templates")} - + ); @@ -83,14 +80,16 @@ function Icon({ document }) { return null; } -const Breadcrumb = ({ document, onlyText }: Props) => { +const Breadcrumb = ({ document, children, onlyText }: Props) => { const { collections } = useStores(); const { t } = useTranslation(); + if (!collections.isLoaded) { + return ; + } + let collection = collections.get(document.collectionId); if (!collection) { - if (!document.deletedAt) return
; - collection = { id: document.collectionId, name: t("Deleted Collection"), @@ -135,7 +134,7 @@ const Breadcrumb = ({ document, onlyText }: Props) => { {isNestedDocument && ( <> - } path={menuPath} /> + )} {lastPath && ( @@ -146,10 +145,16 @@ const Breadcrumb = ({ document, onlyText }: Props) => { )} + {children} ); }; +export const Slash = styled(GoToIcon)` + flex-shrink: 0; + fill: ${(props) => props.theme.divider}; +`; + const Wrapper = styled(Flex)` display: none; @@ -170,22 +175,6 @@ const SmallSlash = styled(GoToIcon)` opacity: 0.25; `; -export const Slash = styled(GoToIcon)` - flex-shrink: 0; - fill: ${(props) => props.theme.divider}; -`; - -const Overflow = styled(MoreIcon)` - flex-shrink: 0; - transition: opacity 100ms ease-in-out; - fill: ${(props) => props.theme.divider}; - - &:active, - &:hover { - fill: ${(props) => props.theme.text}; - } -`; - const Crumb = styled(Link)` color: ${(props) => props.theme.text}; font-size: 15px; @@ -201,12 +190,21 @@ const Crumb = styled(Link)` const CollectionName = styled(Link)` display: flex; - flex-shrink: 0; + flex-shrink: 1; color: ${(props) => props.theme.text}; font-size: 15px; font-weight: 500; white-space: nowrap; overflow: hidden; + min-width: 0; + + svg { + flex-shrink: 0; + } +`; + +const CategoryName = styled(CollectionName)` + flex-shrink: 0; `; export default observer(Breadcrumb); diff --git a/app/components/BreadcrumbMenu.js b/app/components/BreadcrumbMenu.js deleted file mode 100644 index fa7df3f4..00000000 --- a/app/components/BreadcrumbMenu.js +++ /dev/null @@ -1,22 +0,0 @@ -// @flow -import * as React from "react"; -import { DropdownMenu } from "components/DropdownMenu"; -import DropdownMenuItems from "components/DropdownMenu/DropdownMenuItems"; - -type Props = { - label: React.Node, - path: Array, -}; - -export default function BreadcrumbMenu({ label, path }: Props) { - return ( - - ({ - title: item.title, - to: item.url, - }))} - /> - - ); -} diff --git a/app/components/Button.js b/app/components/Button.js index d10333ad..aa15e984 100644 --- a/app/components/Button.js +++ b/app/components/Button.js @@ -22,9 +22,13 @@ const RealButton = styled.button` cursor: pointer; user-select: none; - svg { - fill: ${(props) => props.iconColor || props.theme.buttonText}; - } + ${(props) => + !props.borderOnHover && + ` + svg { + fill: ${props.iconColor || props.theme.buttonText}; + } + `} &::-moz-focus-inner { padding: 0; @@ -42,24 +46,30 @@ const RealButton = styled.button` } ${(props) => - props.neutral && + props.$neutral && ` background: ${props.theme.buttonNeutralBackground}; color: ${props.theme.buttonNeutralText}; box-shadow: ${ - props.borderOnHover ? "none" : "rgba(0, 0, 0, 0.07) 0px 1px 2px" - }; - border: 1px solid ${ - props.borderOnHover ? "transparent" : props.theme.buttonNeutralBorder + props.borderOnHover + ? "none" + : `rgba(0, 0, 0, 0.07) 0px 1px 2px, ${props.theme.buttonNeutralBorder} 0 0 0 1px inset` }; - svg { + ${ + props.borderOnHover + ? "" + : `svg { fill: ${props.iconColor || props.theme.buttonNeutralText}; + }` } + &:hover { background: ${darken(0.05, props.theme.buttonNeutralBackground)}; - border: 1px solid ${props.theme.buttonNeutralBorder}; + box-shadow: rgba(0, 0, 0, 0.07) 0px 1px 2px, ${ + props.theme.buttonNeutralBorder + } 0 0 0 1px inset; } &:disabled { @@ -71,9 +81,9 @@ const RealButton = styled.button` background: ${props.theme.danger}; color: ${props.theme.white}; - &:hover { - background: ${darken(0.05, props.theme.danger)}; - } + &:hover { + background: ${darken(0.05, props.theme.danger)}; + } `}; `; @@ -92,14 +102,14 @@ export const Inner = styled.span` line-height: ${(props) => (props.hasIcon ? 24 : 32)}px; justify-content: center; align-items: center; - min-height: 30px; + min-height: 32px; ${(props) => props.hasIcon && props.hasText && "padding-left: 4px;"}; ${(props) => props.hasIcon && !props.hasText && "padding: 0 4px;"}; `; -export type Props = { - type?: string, +export type Props = {| + type?: "button" | "submit", value?: string, icon?: React.Node, iconColor?: string, @@ -107,9 +117,22 @@ export type Props = { children?: React.Node, innerRef?: React.ElementRef, disclosure?: boolean, + neutral?: boolean, + danger?: boolean, + primary?: boolean, + disabled?: boolean, fullwidth?: boolean, + autoFocus?: boolean, + style?: Object, + as?: React.ComponentType, + to?: string, + onClick?: (event: SyntheticEvent<>) => mixed, borderOnHover?: boolean, -}; + + "data-on"?: string, + "data-event-category"?: string, + "data-event-action"?: string, +|}; function Button({ type = "text", @@ -118,13 +141,14 @@ function Button({ value, disclosure, innerRef, + neutral, ...rest }: Props) { const hasText = children !== undefined || value !== undefined; const hasIcon = icon !== undefined; return ( - + {hasIcon && icon} {hasText && } diff --git a/app/components/ButtonLink.js b/app/components/ButtonLink.js new file mode 100644 index 00000000..b42e1efd --- /dev/null +++ b/app/components/ButtonLink.js @@ -0,0 +1,23 @@ +// @flow +import * as React from "react"; +import styled from "styled-components"; + +type Props = { + onClick: (ev: SyntheticEvent<>) => void, + children: React.Node, +}; + +export default function ButtonLink(props: Props) { + return