mirror of
https://github.com/outline/outline.git
synced 2026-06-14 03:45:00 +03:00
Compare commits
103 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2aaad03270 | |||
| 9252683260 | |||
| f5748eb5e7 | |||
| 0555fd2caa | |||
| d885252fb0 | |||
| df9b0bcf91 | |||
| 31910f1628 | |||
| 14cb3a36c1 | |||
| d3350c20b6 | |||
| 174acfac32 | |||
| 9ef4e2b437 | |||
| 8088da8cf3 | |||
| 221ee48429 | |||
| ffe8c046ef | |||
| dbe8a10702 | |||
| 11f7e3a060 | |||
| 0f41a04e49 | |||
| d055021ad4 | |||
| 810dc5a061 | |||
| 7abe375b3e | |||
| 63371d8f5b | |||
| 6e61df0729 | |||
| 5ddc4000d0 | |||
| 48b61559cc | |||
| 0cac5cfe51 | |||
| e9ce80a3aa | |||
| 07d488c826 | |||
| e2bd03494d | |||
| ead55442e0 | |||
| 449dc55aaa | |||
| e312b264a6 | |||
| 68dcb4de5f | |||
| d2b9a5c03f | |||
| 1b023fb6d7 | |||
| afe4553a7e | |||
| 139e2e29d7 | |||
| 638418432a | |||
| c6d2467fae | |||
| e9387db895 | |||
| 065d04ec98 | |||
| 869fc086d6 | |||
| 59c24aba7c | |||
| bc4806ac30 | |||
| 169ad5b025 | |||
| 0b33b5bc05 | |||
| 109efcaa27 | |||
| 2cc6d7add8 | |||
| 003d82fe8a | |||
| f75a07cb0d | |||
| 0d6720e499 | |||
| a97a1df5f1 | |||
| 822395c265 | |||
| 96d6e9b85e | |||
| 70e1194f90 | |||
| 710fcc697c | |||
| 58f9e95d2f | |||
| bc128359ab | |||
| af09713c8c | |||
| 35052ef38f | |||
| ec3adc6d1c | |||
| 67981a351e | |||
| 24448c7504 | |||
| d5b5d4fc27 | |||
| d8603cc961 | |||
| 943a290b83 | |||
| f4a4f034cf | |||
| a53934a5c9 | |||
| e859e3b9e0 | |||
| b51d818db3 | |||
| bfea742650 | |||
| 9951cbf194 | |||
| 5cb04d7ac1 | |||
| 75561079eb | |||
| d2c7f3f166 | |||
| a077766bff | |||
| a25e03d7cd | |||
| 2953d09ee1 | |||
| 28d15b1573 | |||
| 18108cb359 | |||
| 5dfa6a71a4 | |||
| 35270cd104 | |||
| 19d01ed575 | |||
| 2b7639c903 | |||
| c8269ca134 | |||
| da9fc5bfdf | |||
| efcfda8398 | |||
| ce2f69342c | |||
| 9be5597f4b | |||
| 26f6961c82 | |||
| bf5f83e97d | |||
| 8db0260a1a | |||
| edbae275ae | |||
| f43deb7940 | |||
| 2a3b9e2104 | |||
| 64c3ff8d6b | |||
| 4f7e7ec853 | |||
| d864e228e7 | |||
| f3ea02fdd0 | |||
| ed096013e4 | |||
| 2e376b32ec | |||
| f352b35e13 | |||
| 0f8d503df8 | |||
| 5010b08e83 |
@@ -1,18 +1,29 @@
|
||||
{
|
||||
"presets": ["react", "env"],
|
||||
"presets": [
|
||||
"@babel/preset-react",
|
||||
"@babel/preset-flow",
|
||||
[
|
||||
"@babel/preset-env",
|
||||
{
|
||||
"corejs": {
|
||||
"version": "2",
|
||||
"proposals": true
|
||||
},
|
||||
"useBuiltIns": "usage"
|
||||
}
|
||||
]
|
||||
],
|
||||
"plugins": [
|
||||
"lodash",
|
||||
"styled-components",
|
||||
"transform-decorators-legacy",
|
||||
"transform-es2015-destructuring",
|
||||
"transform-object-rest-spread",
|
||||
"transform-regenerator",
|
||||
"transform-class-properties",
|
||||
"syntax-dynamic-import"
|
||||
],
|
||||
"env": {
|
||||
"development": {
|
||||
"presets": ["react-hmre"]
|
||||
}
|
||||
}
|
||||
}
|
||||
[
|
||||
"@babel/plugin-proposal-decorators",
|
||||
{
|
||||
"legacy": true
|
||||
}
|
||||
],
|
||||
"@babel/plugin-transform-destructuring",
|
||||
"@babel/plugin-transform-regenerator",
|
||||
"transform-class-properties"
|
||||
]
|
||||
}
|
||||
@@ -29,12 +29,15 @@ jobs:
|
||||
- run:
|
||||
name: migrate
|
||||
command: ./node_modules/.bin/sequelize db:migrate --url $DATABASE_URL_TEST
|
||||
- run:
|
||||
name: test
|
||||
command: yarn test
|
||||
- run:
|
||||
name: lint
|
||||
command: yarn lint
|
||||
- run:
|
||||
name: flow
|
||||
command: yarn flow check --max-workers 4
|
||||
command: yarn flow check --max-workers 4
|
||||
- run:
|
||||
name: test
|
||||
command: yarn test
|
||||
- run:
|
||||
name: build
|
||||
command: yarn build
|
||||
@@ -17,9 +17,7 @@ PORT=3000
|
||||
# set to false if your SSL is terminated at a loadbalancer, for example
|
||||
FORCE_HTTPS=true
|
||||
|
||||
DEPLOYMENT=self
|
||||
ENABLE_UPDATES=true
|
||||
WEBSOCKETS_ENABLED=true
|
||||
DEBUG=cache,presenters,events
|
||||
|
||||
# Third party signin credentials (at least one is required)
|
||||
|
||||
@@ -6,11 +6,54 @@
|
||||
"plugin:import/warnings",
|
||||
"plugin:flowtype/recommended"
|
||||
],
|
||||
"plugins": ["prettier", "flowtype"],
|
||||
"plugins": [
|
||||
"prettier",
|
||||
"flowtype"
|
||||
],
|
||||
"rules": {
|
||||
"eqeqeq": 2,
|
||||
"no-unused-vars": 2,
|
||||
"no-mixed-operators": "off",
|
||||
"import/order": [
|
||||
"error",
|
||||
{
|
||||
"alphabetize": {
|
||||
"order": "asc"
|
||||
},
|
||||
"pathGroups": [
|
||||
{
|
||||
"pattern": "shared/**",
|
||||
"group": "external",
|
||||
"position": "after"
|
||||
},
|
||||
{
|
||||
"pattern": "stores",
|
||||
"group": "external",
|
||||
"position": "after"
|
||||
},
|
||||
{
|
||||
"pattern": "stores/**",
|
||||
"group": "external",
|
||||
"position": "after"
|
||||
},
|
||||
{
|
||||
"pattern": "models/**",
|
||||
"group": "external",
|
||||
"position": "after"
|
||||
},
|
||||
{
|
||||
"pattern": "scenes/**",
|
||||
"group": "external",
|
||||
"position": "after"
|
||||
},
|
||||
{
|
||||
"pattern": "components/**",
|
||||
"group": "external",
|
||||
"position": "after"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"flowtype/require-valid-file-annotation": [
|
||||
2,
|
||||
"always",
|
||||
@@ -18,14 +61,19 @@
|
||||
"annotationStyle": "line"
|
||||
}
|
||||
],
|
||||
"flowtype/space-after-type-colon": [2, "always"],
|
||||
"flowtype/space-before-type-colon": [2, "never"],
|
||||
"flowtype/space-after-type-colon": [
|
||||
2,
|
||||
"always"
|
||||
],
|
||||
"flowtype/space-before-type-colon": [
|
||||
2,
|
||||
"never"
|
||||
],
|
||||
"prettier/prettier": [
|
||||
"error",
|
||||
{
|
||||
"printWidth": 80,
|
||||
"trailingComma": "es5",
|
||||
"singleQuote": true
|
||||
"trailingComma": "es5"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -33,12 +81,14 @@
|
||||
"react": {
|
||||
"createClass": "createReactClass",
|
||||
"pragma": "React",
|
||||
"version": "detect",
|
||||
"flowVersion": "0.86"
|
||||
"version": "detect"
|
||||
},
|
||||
"import/resolver": {
|
||||
"node": {
|
||||
"paths": ["app", "."]
|
||||
"paths": [
|
||||
"app",
|
||||
"."
|
||||
]
|
||||
}
|
||||
},
|
||||
"flowtype": {
|
||||
@@ -49,12 +99,6 @@
|
||||
"jest": true
|
||||
},
|
||||
"globals": {
|
||||
"__DEV__": true,
|
||||
"SLACK_KEY": true,
|
||||
"DEPLOYMENT": true,
|
||||
"BASE_URL": true,
|
||||
"SENTRY_DSN": true,
|
||||
"afterAll": true,
|
||||
"Sentry": true
|
||||
"EDITOR_VERSION": true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@
|
||||
.*/node_modules/tiny-cookie/flow/.*
|
||||
.*/node_modules/styled-components/.*
|
||||
.*/node_modules/polished/.*
|
||||
.*/node_modules/mobx/.*.flow
|
||||
.*/node_modules/react-side-effect/.*
|
||||
.*/node_modules/fbjs/.*
|
||||
.*/node_modules/config-chain/.*
|
||||
|
||||
Vendored
+4
-1
@@ -1,3 +1,6 @@
|
||||
{
|
||||
"javascript.validate.enable": false
|
||||
"javascript.validate.enable": false,
|
||||
"typescript.validate.enable": false,
|
||||
"editor.formatOnSave": true,
|
||||
"typescript.format.enable": false
|
||||
}
|
||||
+2
-1
@@ -9,8 +9,9 @@ WORKDIR $APP_PATH
|
||||
COPY . $APP_PATH
|
||||
|
||||
RUN yarn install --pure-lockfile
|
||||
RUN yarn build
|
||||
RUN cp -r /opt/outline/node_modules /opt/node_modules
|
||||
|
||||
CMD yarn build && yarn start
|
||||
CMD yarn start
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
@@ -3,7 +3,7 @@ Business Source License 1.1
|
||||
Parameters
|
||||
|
||||
Licensor: General Outline, Inc.
|
||||
Licensed Work: Outline 0.42.0
|
||||
Licensed Work: Outline 0.46.0
|
||||
The Licensed Work is (c) 2020 General Outline, Inc.
|
||||
Additional Use Grant: You may make use of the Licensed Work, provided that
|
||||
you may not use the Licensed Work for a Document
|
||||
@@ -15,7 +15,7 @@ Additional Use Grant: You may make use of the Licensed Work, provided that
|
||||
Licensed Work by creating teams and documents
|
||||
controlled by such third parties.
|
||||
|
||||
Change Date: 2023-04-01
|
||||
Change Date: 2023-08-12
|
||||
|
||||
Change License: Apache License, Version 2.0
|
||||
|
||||
|
||||
@@ -8,10 +8,12 @@ build:
|
||||
docker-compose build --pull outline
|
||||
|
||||
test:
|
||||
docker-compose run --rm outline yarn test
|
||||
docker-compose up -d redis postgres s3
|
||||
yarn test
|
||||
|
||||
watch:
|
||||
docker-compose run --rm outline yarn test:watch
|
||||
docker-compose up -d redis postgres s3
|
||||
yarn test:watch
|
||||
|
||||
destroy:
|
||||
docker-compose stop
|
||||
|
||||
@@ -126,7 +126,6 @@ Backend is driven by [Koa](http://koajs.com/) (API, web server), [Sequelize](htt
|
||||
- `server/commands` - Domain logic, currently being refactored from /models
|
||||
- `server/emails` - React rendered email templates
|
||||
- `server/models` - Database models
|
||||
- `server/pages` - Server-side rendered public pages
|
||||
- `server/policies` - Authorization logic
|
||||
- `server/presenters` - API responses for database models
|
||||
- `server/test` - Test helps and support
|
||||
@@ -154,7 +153,7 @@ yarn test:app
|
||||
|
||||
Outline is built and maintained by a small team – we'd love your help to fix bugs and add features!
|
||||
|
||||
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 our [Spectrum community](https://spectrum.chat/outline). 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!
|
||||
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!
|
||||
|
||||
If you’re looking for ways to get started, here's a list of ways to help us improve Outline:
|
||||
|
||||
@@ -162,7 +161,6 @@ If you’re looking for ways to get started, here's a list of ways to help us im
|
||||
* Performance improvements, both on server and frontend
|
||||
* Developer happiness and documentation
|
||||
* Bugs and other issues listed on GitHub
|
||||
* Helping others on Spectrum
|
||||
|
||||
## License
|
||||
|
||||
|
||||
@@ -35,20 +35,10 @@
|
||||
"generator": "secret",
|
||||
"required": true
|
||||
},
|
||||
"DEPLOYMENT": {
|
||||
"description": "Should be 'self' for self hosted installations, turns off things like pricing pages",
|
||||
"value": "self",
|
||||
"required": true
|
||||
},
|
||||
"ENABLE_UPDATES": {
|
||||
"value": "true",
|
||||
"required": true
|
||||
},
|
||||
"WEBSOCKETS_ENABLED": {
|
||||
"value": "true",
|
||||
"required": true,
|
||||
"description": "Allow realtime data to be pushed to clients over websockets"
|
||||
},
|
||||
"URL": {
|
||||
"description": "https://{your app name}.herokuapp.com",
|
||||
"required": true
|
||||
@@ -149,4 +139,4 @@
|
||||
"required": false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
// @flow
|
||||
import styled from 'styled-components';
|
||||
import breakpoint from 'styled-components-breakpoint';
|
||||
import Flex from 'shared/components/Flex';
|
||||
import styled from "styled-components";
|
||||
import breakpoint from "styled-components-breakpoint";
|
||||
import Flex from "components/Flex";
|
||||
|
||||
export const Action = styled(Flex)`
|
||||
justify-content: center;
|
||||
@@ -12,9 +12,13 @@ export const Action = styled(Flex)`
|
||||
flex-shrink: 0;
|
||||
|
||||
a {
|
||||
color: ${props => props.theme.text};
|
||||
color: ${(props) => props.theme.text};
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
&:empty {
|
||||
display: none;
|
||||
}
|
||||
`;
|
||||
|
||||
export const Separator = styled.div`
|
||||
@@ -22,7 +26,7 @@ export const Separator = styled.div`
|
||||
margin-left: 12px;
|
||||
width: 1px;
|
||||
height: 28px;
|
||||
background: ${props => props.theme.divider};
|
||||
background: ${(props) => props.theme.divider};
|
||||
`;
|
||||
|
||||
const Actions = styled(Flex)`
|
||||
@@ -31,8 +35,8 @@ const Actions = styled(Flex)`
|
||||
right: 0;
|
||||
left: 0;
|
||||
border-radius: 3px;
|
||||
background: ${props => props.theme.background};
|
||||
transition: ${props => props.theme.backgroundTransition};
|
||||
background: ${(props) => props.theme.background};
|
||||
transition: ${(props) => props.theme.backgroundTransition};
|
||||
padding: 12px;
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
|
||||
@@ -40,7 +44,7 @@ const Actions = styled(Flex)`
|
||||
display: none;
|
||||
}
|
||||
|
||||
${breakpoint('tablet')`
|
||||
${breakpoint("tablet")`
|
||||
left: auto;
|
||||
padding: 24px;
|
||||
`};
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import { observer } from 'mobx-react';
|
||||
import Flex from 'shared/components/Flex';
|
||||
import styled from 'styled-components';
|
||||
|
||||
type Props = {
|
||||
children: React.Node,
|
||||
type?: 'info' | 'success' | 'warning' | 'danger' | 'offline',
|
||||
};
|
||||
|
||||
@observer
|
||||
class Alert extends React.Component<Props> {
|
||||
defaultProps = {
|
||||
type: 'info',
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Container align="center" justify="center" type={this.props.type}>
|
||||
{this.props.children}
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const Container = styled(Flex)`
|
||||
height: $headerHeight;
|
||||
color: ${props => props.theme.white};
|
||||
font-size: 14px;
|
||||
line-height: 1;
|
||||
|
||||
background-color: ${({ theme, type }) => theme.color[type]};
|
||||
`;
|
||||
|
||||
export default Alert;
|
||||
@@ -1,6 +1,7 @@
|
||||
// @flow
|
||||
/* global ga */
|
||||
import * as React from 'react';
|
||||
import * as React from "react";
|
||||
import env from "env";
|
||||
|
||||
type Props = {
|
||||
children?: React.Node,
|
||||
@@ -8,24 +9,24 @@ type Props = {
|
||||
|
||||
export default class Analytics extends React.Component<Props> {
|
||||
componentDidMount() {
|
||||
if (!process.env.GOOGLE_ANALYTICS_ID) return;
|
||||
if (!env.GOOGLE_ANALYTICS_ID) return;
|
||||
|
||||
// standard Google Analytics script
|
||||
window.ga =
|
||||
window.ga ||
|
||||
function() {
|
||||
function () {
|
||||
// $FlowIssue
|
||||
(ga.q = ga.q || []).push(arguments);
|
||||
};
|
||||
|
||||
// $FlowIssue
|
||||
ga.l = +new Date();
|
||||
ga('create', process.env.GOOGLE_ANALYTICS_ID, 'auto');
|
||||
ga('set', { dimension1: 'true' });
|
||||
ga('send', 'pageview');
|
||||
ga("create", env.GOOGLE_ANALYTICS_ID, "auto");
|
||||
ga("set", { dimension1: "true" });
|
||||
ga("send", "pageview");
|
||||
|
||||
const script = document.createElement('script');
|
||||
script.src = 'https://www.google-analytics.com/analytics.js';
|
||||
const script = document.createElement("script");
|
||||
script.src = "https://www.google-analytics.com/analytics.js";
|
||||
script.async = true;
|
||||
|
||||
if (document.body) {
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import { observer, inject } from 'mobx-react';
|
||||
import AuthStore from 'stores/AuthStore';
|
||||
import LoadingIndicator from 'components/LoadingIndicator';
|
||||
import { isCustomSubdomain } from 'shared/utils/domains';
|
||||
import { observer, inject } from "mobx-react";
|
||||
import * as React from "react";
|
||||
import { Redirect } from "react-router-dom";
|
||||
import { isCustomSubdomain } from "shared/utils/domains";
|
||||
import AuthStore from "stores/AuthStore";
|
||||
import LoadingIndicator from "components/LoadingIndicator";
|
||||
import env from "env";
|
||||
|
||||
type Props = {
|
||||
auth: AuthStore,
|
||||
@@ -22,7 +24,7 @@ const Authenticated = observer(({ auth, children }: Props) => {
|
||||
// If we're authenticated but viewing a subdomain that doesn't match the
|
||||
// currently authenticated team then kick the user to the teams subdomain.
|
||||
if (
|
||||
process.env.SUBDOMAINS_ENABLED &&
|
||||
env.SUBDOMAINS_ENABLED &&
|
||||
team.subdomain &&
|
||||
isCustomSubdomain(hostname) &&
|
||||
!hostname.startsWith(`${team.subdomain}.`)
|
||||
@@ -35,7 +37,7 @@ const Authenticated = observer(({ auth, children }: Props) => {
|
||||
}
|
||||
|
||||
auth.logout(true);
|
||||
return null;
|
||||
return <Redirect to="/" />;
|
||||
});
|
||||
|
||||
export default inject('auth')(Authenticated);
|
||||
export default inject("auth")(Authenticated);
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { observable } from 'mobx';
|
||||
import { observer } from 'mobx-react';
|
||||
import placeholder from './placeholder.png';
|
||||
import { observable } from "mobx";
|
||||
import { observer } from "mobx-react";
|
||||
import * as React from "react";
|
||||
import styled from "styled-components";
|
||||
import placeholder from "./placeholder.png";
|
||||
|
||||
type Props = {
|
||||
src: string,
|
||||
@@ -48,8 +48,8 @@ const IconWrapper = styled.div`
|
||||
position: absolute;
|
||||
bottom: -2px;
|
||||
right: -2px;
|
||||
background: ${props => props.theme.primary};
|
||||
border: 2px solid ${props => props.theme.background};
|
||||
background: ${(props) => props.theme.primary};
|
||||
border: 2px solid ${(props) => props.theme.background};
|
||||
border-radius: 100%;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
@@ -57,10 +57,10 @@ const IconWrapper = styled.div`
|
||||
|
||||
const CircleImg = styled.img`
|
||||
display: block;
|
||||
width: ${props => props.size}px;
|
||||
height: ${props => props.size}px;
|
||||
width: ${(props) => props.size}px;
|
||||
height: ${(props) => props.size}px;
|
||||
border-radius: 50%;
|
||||
border: 2px solid ${props => props.theme.background};
|
||||
border: 2px solid ${(props) => props.theme.background};
|
||||
flex-shrink: 0;
|
||||
`;
|
||||
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import { observable } from 'mobx';
|
||||
import { observer } from 'mobx-react';
|
||||
import styled from 'styled-components';
|
||||
import distanceInWordsToNow from 'date-fns/distance_in_words_to_now';
|
||||
import Avatar from 'components/Avatar';
|
||||
import Tooltip from 'components/Tooltip';
|
||||
import User from 'models/User';
|
||||
import UserProfile from 'scenes/UserProfile';
|
||||
import { EditIcon } from 'outline-icons';
|
||||
import distanceInWordsToNow from "date-fns/distance_in_words_to_now";
|
||||
import { observable } from "mobx";
|
||||
import { observer } from "mobx-react";
|
||||
import { EditIcon } from "outline-icons";
|
||||
import * as React from "react";
|
||||
import styled from "styled-components";
|
||||
import User from "models/User";
|
||||
import UserProfile from "scenes/UserProfile";
|
||||
import Avatar from "components/Avatar";
|
||||
import Tooltip from "components/Tooltip";
|
||||
|
||||
type Props = {
|
||||
user: User,
|
||||
@@ -40,14 +40,16 @@ class AvatarWithPresence extends React.Component<Props> {
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<>
|
||||
<Tooltip
|
||||
tooltip={
|
||||
<Centered>
|
||||
<strong>{user.name}</strong> {isCurrentUser && '(You)'}
|
||||
<strong>{user.name}</strong> {isCurrentUser && "(You)"}
|
||||
<br />
|
||||
{isPresent
|
||||
? isEditing ? 'currently editing' : 'currently viewing'
|
||||
? isEditing
|
||||
? "currently editing"
|
||||
: "currently viewing"
|
||||
: `viewed ${distanceInWordsToNow(new Date(lastViewedAt))} ago`}
|
||||
</Centered>
|
||||
}
|
||||
@@ -67,7 +69,7 @@ class AvatarWithPresence extends React.Component<Props> {
|
||||
isOpen={this.isOpen}
|
||||
onRequestClose={this.handleCloseProfile}
|
||||
/>
|
||||
</React.Fragment>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -77,7 +79,7 @@ const Centered = styled.div`
|
||||
`;
|
||||
|
||||
const AvatarWrapper = styled.div`
|
||||
opacity: ${props => (props.isPresent ? 1 : 0.5)};
|
||||
opacity: ${(props) => (props.isPresent ? 1 : 0.5)};
|
||||
transition: opacity 250ms ease-in-out;
|
||||
`;
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// @flow
|
||||
import Avatar from './Avatar';
|
||||
import AvatarWithPresence from './AvatarWithPresence';
|
||||
import Avatar from "./Avatar";
|
||||
import AvatarWithPresence from "./AvatarWithPresence";
|
||||
|
||||
export { AvatarWithPresence };
|
||||
export default Avatar;
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
// @flow
|
||||
import styled from 'styled-components';
|
||||
import styled from "styled-components";
|
||||
|
||||
const Badge = styled.span`
|
||||
margin-left: 10px;
|
||||
padding: 2px 6px 3px;
|
||||
background-color: ${({ admin, theme }) =>
|
||||
admin ? theme.primary : theme.textTertiary};
|
||||
color: ${({ admin, theme }) => (admin ? theme.white : theme.background)};
|
||||
background-color: ${({ primary, theme }) =>
|
||||
primary ? theme.primary : theme.textTertiary};
|
||||
color: ${({ primary, theme }) => (primary ? theme.white : theme.background)};
|
||||
border-radius: 4px;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import OutlineLogo from './OutlineLogo';
|
||||
import * as React from "react";
|
||||
import styled from "styled-components";
|
||||
import OutlineLogo from "./OutlineLogo";
|
||||
import env from "env";
|
||||
|
||||
type Props = {
|
||||
href?: string,
|
||||
};
|
||||
|
||||
function Branding({ href = process.env.URL }: Props) {
|
||||
function Branding({ href = env.URL }: Props) {
|
||||
return (
|
||||
<Link href={href}>
|
||||
<OutlineLogo size={16} /> Outline
|
||||
<OutlineLogo size={16} />
|
||||
Outline
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
@@ -24,17 +26,17 @@ const Link = styled.a`
|
||||
font-size: 14px;
|
||||
text-decoration: none;
|
||||
border-top-right-radius: 2px;
|
||||
color: ${props => props.theme.text};
|
||||
color: ${(props) => props.theme.text};
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 16px;
|
||||
|
||||
svg {
|
||||
fill: ${props => props.theme.text};
|
||||
fill: ${(props) => props.theme.text};
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: ${props => props.theme.sidebarBackground};
|
||||
background: ${(props) => props.theme.sidebarBackground};
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -1,22 +1,23 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import { observer, inject } from 'mobx-react';
|
||||
import breakpoint from 'styled-components-breakpoint';
|
||||
import styled from 'styled-components';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { observer, inject } from "mobx-react";
|
||||
import {
|
||||
CollectionIcon,
|
||||
PrivateCollectionIcon,
|
||||
PadlockIcon,
|
||||
GoToIcon,
|
||||
MoreIcon,
|
||||
} from 'outline-icons';
|
||||
ShapesIcon,
|
||||
EditIcon,
|
||||
} from "outline-icons";
|
||||
import * as React from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import styled from "styled-components";
|
||||
import breakpoint from "styled-components-breakpoint";
|
||||
|
||||
import Document from 'models/Document';
|
||||
import CollectionsStore from 'stores/CollectionsStore';
|
||||
import { collectionUrl } from 'utils/routeHelpers';
|
||||
import Flex from 'shared/components/Flex';
|
||||
import BreadcrumbMenu from './BreadcrumbMenu';
|
||||
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 { collectionUrl } from "utils/routeHelpers";
|
||||
|
||||
type Props = {
|
||||
document: Document,
|
||||
@@ -32,49 +33,68 @@ const Breadcrumb = observer(({ document, collections, onlyText }: Props) => {
|
||||
|
||||
if (onlyText === true) {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<>
|
||||
{collection.private && (
|
||||
<React.Fragment>
|
||||
<SmallPadlockIcon color="currentColor" size={16} />{' '}
|
||||
</React.Fragment>
|
||||
<>
|
||||
<SmallPadlockIcon color="currentColor" size={16} />{" "}
|
||||
</>
|
||||
)}
|
||||
{collection.name}
|
||||
{path.map(n => (
|
||||
{path.map((n) => (
|
||||
<React.Fragment key={n.id}>
|
||||
<SmallSlash />
|
||||
{n.title}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</React.Fragment>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const isTemplate = document.isTemplate;
|
||||
const isDraft = !document.publishedAt && !isTemplate;
|
||||
const isNestedDocument = path.length > 1;
|
||||
const lastPath = path.length ? path[path.length - 1] : undefined;
|
||||
const menuPath = isNestedDocument ? path.slice(0, -1) : [];
|
||||
|
||||
return (
|
||||
<Wrapper justify="flex-start" align="center">
|
||||
{isTemplate && (
|
||||
<>
|
||||
<CollectionName to="/templates">
|
||||
<ShapesIcon color="currentColor" />
|
||||
|
||||
<span>Templates</span>
|
||||
</CollectionName>
|
||||
<Slash />
|
||||
</>
|
||||
)}
|
||||
{isDraft && (
|
||||
<>
|
||||
<CollectionName to="/drafts">
|
||||
<EditIcon color="currentColor" />
|
||||
|
||||
<span>Drafts</span>
|
||||
</CollectionName>
|
||||
<Slash />
|
||||
</>
|
||||
)}
|
||||
<CollectionName to={collectionUrl(collection.id)}>
|
||||
{collection.private ? (
|
||||
<PrivateCollectionIcon color={collection.color} expanded />
|
||||
) : (
|
||||
<CollectionIcon color={collection.color} expanded />
|
||||
)}{' '}
|
||||
<CollectionIcon collection={collection} expanded />
|
||||
|
||||
<span>{collection.name}</span>
|
||||
</CollectionName>
|
||||
{isNestedDocument && (
|
||||
<React.Fragment>
|
||||
<>
|
||||
<Slash /> <BreadcrumbMenu label={<Overflow />} path={menuPath} />
|
||||
</React.Fragment>
|
||||
</>
|
||||
)}
|
||||
{lastPath && (
|
||||
<React.Fragment>
|
||||
<Slash />{' '}
|
||||
<>
|
||||
<Slash />{" "}
|
||||
<Crumb to={lastPath.url} title={lastPath.title}>
|
||||
{lastPath.title}
|
||||
</Crumb>
|
||||
</React.Fragment>
|
||||
</>
|
||||
)}
|
||||
</Wrapper>
|
||||
);
|
||||
@@ -83,7 +103,7 @@ const Breadcrumb = observer(({ document, collections, onlyText }: Props) => {
|
||||
const Wrapper = styled(Flex)`
|
||||
display: none;
|
||||
|
||||
${breakpoint('tablet')`
|
||||
${breakpoint("tablet")`
|
||||
display: flex;
|
||||
`};
|
||||
`;
|
||||
@@ -102,7 +122,7 @@ const SmallSlash = styled(GoToIcon)`
|
||||
|
||||
export const Slash = styled(GoToIcon)`
|
||||
flex-shrink: 0;
|
||||
fill: ${props => props.theme.divider};
|
||||
fill: ${(props) => props.theme.divider};
|
||||
`;
|
||||
|
||||
const Overflow = styled(MoreIcon)`
|
||||
@@ -117,7 +137,7 @@ const Overflow = styled(MoreIcon)`
|
||||
`;
|
||||
|
||||
const Crumb = styled(Link)`
|
||||
color: ${props => props.theme.text};
|
||||
color: ${(props) => props.theme.text};
|
||||
font-size: 15px;
|
||||
height: 24px;
|
||||
text-overflow: ellipsis;
|
||||
@@ -132,11 +152,11 @@ const Crumb = styled(Link)`
|
||||
const CollectionName = styled(Link)`
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
color: ${props => props.theme.text};
|
||||
color: ${(props) => props.theme.text};
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
`;
|
||||
|
||||
export default inject('collections')(Breadcrumb);
|
||||
export default inject("collections")(Breadcrumb);
|
||||
@@ -1,7 +1,7 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { DropdownMenu, DropdownMenuItem } from 'components/DropdownMenu';
|
||||
import * as React from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { DropdownMenu, DropdownMenuItem } from "components/DropdownMenu";
|
||||
|
||||
type Props = {
|
||||
label: React.Node,
|
||||
@@ -14,7 +14,7 @@ export default class BreadcrumbMenu extends React.Component<Props> {
|
||||
|
||||
return (
|
||||
<DropdownMenu label={this.props.label} position="center">
|
||||
{path.map(item => (
|
||||
{path.map((item) => (
|
||||
<DropdownMenuItem as={Link} to={item.url} key={item.id}>
|
||||
{item.title}
|
||||
</DropdownMenuItem>
|
||||
+27
-25
@@ -1,16 +1,17 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { darken, lighten } from 'polished';
|
||||
import { ExpandedIcon } from 'outline-icons';
|
||||
import { ExpandedIcon } from "outline-icons";
|
||||
import { darken, lighten } from "polished";
|
||||
import * as React from "react";
|
||||
import styled from "styled-components";
|
||||
|
||||
const RealButton = styled.button`
|
||||
display: inline-block;
|
||||
display: ${(props) => (props.fullwidth ? "block" : "inline-block")};
|
||||
width: ${(props) => (props.fullwidth ? "100%" : "auto")};
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
background: ${props => props.theme.buttonBackground};
|
||||
color: ${props => props.theme.buttonText};
|
||||
background: ${(props) => props.theme.buttonBackground};
|
||||
color: ${(props) => props.theme.buttonText};
|
||||
box-shadow: rgba(0, 0, 0, 0.2) 0px 1px 2px;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
@@ -23,7 +24,7 @@ const RealButton = styled.button`
|
||||
user-select: none;
|
||||
|
||||
svg {
|
||||
fill: ${props => props.iconColor || props.theme.buttonText};
|
||||
fill: ${(props) => props.iconColor || props.theme.buttonText};
|
||||
}
|
||||
|
||||
&::-moz-focus-inner {
|
||||
@@ -32,12 +33,12 @@ const RealButton = styled.button`
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: ${props => darken(0.05, props.theme.buttonBackground)};
|
||||
background: ${(props) => darken(0.05, props.theme.buttonBackground)};
|
||||
}
|
||||
|
||||
&:focus {
|
||||
transition-duration: 0.05s;
|
||||
box-shadow: ${props => lighten(0.4, props.theme.buttonBackground)} 0px 0px
|
||||
box-shadow: ${(props) => lighten(0.4, props.theme.buttonBackground)} 0px 0px
|
||||
0px 3px;
|
||||
outline: none;
|
||||
}
|
||||
@@ -45,19 +46,19 @@ const RealButton = styled.button`
|
||||
&:disabled {
|
||||
cursor: default;
|
||||
pointer-events: none;
|
||||
color: ${props => props.theme.white50};
|
||||
color: ${(props) => props.theme.white50};
|
||||
}
|
||||
|
||||
${props =>
|
||||
${(props) =>
|
||||
props.neutral &&
|
||||
`
|
||||
background: ${props.theme.buttonNeutralBackground};
|
||||
color: ${props.theme.buttonNeutralText};
|
||||
box-shadow: ${
|
||||
props.borderOnHover ? 'none' : 'rgba(0, 0, 0, 0.07) 0px 1px 2px'
|
||||
props.borderOnHover ? "none" : "rgba(0, 0, 0, 0.07) 0px 1px 2px"
|
||||
};
|
||||
border: 1px solid ${
|
||||
props.borderOnHover ? 'transparent' : props.theme.buttonNeutralBorder
|
||||
props.borderOnHover ? "transparent" : props.theme.buttonNeutralBorder
|
||||
};
|
||||
|
||||
svg {
|
||||
@@ -79,9 +80,9 @@ const RealButton = styled.button`
|
||||
&:disabled {
|
||||
color: ${props.theme.textTertiary};
|
||||
}
|
||||
`} ${props =>
|
||||
props.danger &&
|
||||
`
|
||||
`} ${(props) =>
|
||||
props.danger &&
|
||||
`
|
||||
background: ${props.theme.danger};
|
||||
color: ${props.theme.white};
|
||||
|
||||
@@ -102,19 +103,20 @@ const Label = styled.span`
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
${props => props.hasIcon && 'padding-left: 4px;'};
|
||||
${(props) => props.hasIcon && "padding-left: 4px;"};
|
||||
`;
|
||||
|
||||
export const Inner = styled.span`
|
||||
display: flex;
|
||||
padding: 0 8px;
|
||||
padding-right: ${props => (props.disclosure ? 2 : 8)}px;
|
||||
line-height: ${props => (props.hasIcon ? 24 : 32)}px;
|
||||
padding-right: ${(props) => (props.disclosure ? 2 : 8)}px;
|
||||
line-height: ${(props) => (props.hasIcon ? 24 : 32)}px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 30px;
|
||||
|
||||
${props => props.hasIcon && props.hasText && 'padding-left: 4px;'};
|
||||
${props => props.hasIcon && !props.hasText && 'padding: 0 4px;'};
|
||||
${(props) => props.hasIcon && props.hasText && "padding-left: 4px;"};
|
||||
${(props) => props.hasIcon && !props.hasText && "padding: 0 4px;"};
|
||||
`;
|
||||
|
||||
export type Props = {
|
||||
@@ -126,11 +128,12 @@ export type Props = {
|
||||
children?: React.Node,
|
||||
innerRef?: React.ElementRef<any>,
|
||||
disclosure?: boolean,
|
||||
fullwidth?: boolean,
|
||||
borderOnHover?: boolean,
|
||||
};
|
||||
|
||||
function Button({
|
||||
type = 'text',
|
||||
type = "text",
|
||||
icon,
|
||||
children,
|
||||
value,
|
||||
@@ -152,7 +155,6 @@ function Button({
|
||||
);
|
||||
}
|
||||
|
||||
// $FlowFixMe - need to upgrade to get forwardRef
|
||||
export default React.forwardRef((props, ref) => (
|
||||
export default React.forwardRef<Props, typeof Button>((props, ref) => (
|
||||
<Button {...props} innerRef={ref} />
|
||||
));
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
// @flow
|
||||
import styled from "styled-components";
|
||||
import Button, { Inner } from "./Button";
|
||||
|
||||
const ButtonLarge = styled(Button)`
|
||||
height: 40px;
|
||||
|
||||
${Inner} {
|
||||
padding: 4px 16px;
|
||||
}
|
||||
`;
|
||||
|
||||
export default ButtonLarge;
|
||||
@@ -1,7 +1,7 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import breakpoint from 'styled-components-breakpoint';
|
||||
import * as React from "react";
|
||||
import styled from "styled-components";
|
||||
import breakpoint from "styled-components-breakpoint";
|
||||
|
||||
type Props = {
|
||||
children?: React.Node,
|
||||
@@ -9,9 +9,10 @@ type Props = {
|
||||
|
||||
const Container = styled.div`
|
||||
width: 100%;
|
||||
max-width: 100vw;
|
||||
padding: 60px 20px;
|
||||
|
||||
${breakpoint('tablet')`
|
||||
${breakpoint("tablet")`
|
||||
padding: 60px;
|
||||
`};
|
||||
`;
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import HelpText from 'components/HelpText';
|
||||
import VisuallyHidden from 'components/VisuallyHidden';
|
||||
import * as React from "react";
|
||||
import styled from "styled-components";
|
||||
import HelpText from "components/HelpText";
|
||||
import VisuallyHidden from "components/VisuallyHidden";
|
||||
|
||||
export type Props = {
|
||||
checked?: boolean,
|
||||
@@ -10,18 +10,19 @@ export type Props = {
|
||||
labelHidden?: boolean,
|
||||
className?: string,
|
||||
note?: string,
|
||||
short?: boolean,
|
||||
small?: boolean,
|
||||
};
|
||||
|
||||
const LabelText = styled.span`
|
||||
font-weight: 500;
|
||||
margin-left: ${props => (props.small ? '6px' : '10px')};
|
||||
${props => (props.small ? `color: ${props.theme.textSecondary}` : '')};
|
||||
margin-left: ${(props) => (props.small ? "6px" : "10px")};
|
||||
${(props) => (props.small ? `color: ${props.theme.textSecondary}` : "")};
|
||||
`;
|
||||
|
||||
const Wrapper = styled.div`
|
||||
padding-bottom: 8px;
|
||||
${props => (props.small ? 'font-size: 14px' : '')};
|
||||
${(props) => (props.small ? "font-size: 14px" : "")};
|
||||
`;
|
||||
|
||||
const Label = styled.label`
|
||||
@@ -42,7 +43,7 @@ export default function Checkbox({
|
||||
const wrappedLabel = <LabelText small={small}>{label}</LabelText>;
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<>
|
||||
<Wrapper small={small}>
|
||||
<Label>
|
||||
<input type="checkbox" {...rest} />
|
||||
@@ -55,6 +56,6 @@ export default function Checkbox({
|
||||
</Label>
|
||||
{note && <HelpText small>{note}</HelpText>}
|
||||
</Wrapper>
|
||||
</React.Fragment>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
// @flow
|
||||
import styled from 'styled-components';
|
||||
import styled from "styled-components";
|
||||
|
||||
const ClickablePadding = styled.div`
|
||||
min-height: 10em;
|
||||
cursor: ${({ onClick }) => (onClick ? 'text' : 'default')};
|
||||
cursor: ${({ onClick }) => (onClick ? "text" : "default")};
|
||||
${({ grow }) => grow && `flex-grow: 100;`};
|
||||
`;
|
||||
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import { observer, inject } from 'mobx-react';
|
||||
import { sortBy, keyBy } from 'lodash';
|
||||
import { MAX_AVATAR_DISPLAY } from 'shared/constants';
|
||||
import { sortBy, keyBy } from "lodash";
|
||||
import { observer, inject } from "mobx-react";
|
||||
import * as React from "react";
|
||||
import { MAX_AVATAR_DISPLAY } from "shared/constants";
|
||||
|
||||
import { AvatarWithPresence } from 'components/Avatar';
|
||||
import Facepile from 'components/Facepile';
|
||||
import Document from 'models/Document';
|
||||
import ViewsStore from 'stores/ViewsStore';
|
||||
import DocumentPresenceStore from 'stores/DocumentPresenceStore';
|
||||
import DocumentPresenceStore from "stores/DocumentPresenceStore";
|
||||
import ViewsStore from "stores/ViewsStore";
|
||||
import Document from "models/Document";
|
||||
import { AvatarWithPresence } from "components/Avatar";
|
||||
import Facepile from "components/Facepile";
|
||||
|
||||
type Props = {
|
||||
views: ViewsStore,
|
||||
@@ -32,27 +32,27 @@ class Collaborators extends React.Component<Props> {
|
||||
|
||||
const documentViews = views.inDocument(document.id);
|
||||
|
||||
const presentIds = documentPresence.map(p => p.userId);
|
||||
const presentIds = documentPresence.map((p) => p.userId);
|
||||
const editingIds = documentPresence
|
||||
.filter(p => p.isEditing)
|
||||
.map(p => p.userId);
|
||||
.filter((p) => p.isEditing)
|
||||
.map((p) => p.userId);
|
||||
|
||||
// ensure currently present via websocket are always ordered first
|
||||
const mostRecentViewers = sortBy(
|
||||
documentViews.slice(0, MAX_AVATAR_DISPLAY),
|
||||
view => {
|
||||
(view) => {
|
||||
return presentIds.includes(view.user.id);
|
||||
}
|
||||
);
|
||||
|
||||
const viewersKeyedByUserId = keyBy(mostRecentViewers, v => v.user.id);
|
||||
const viewersKeyedByUserId = keyBy(mostRecentViewers, (v) => v.user.id);
|
||||
const overflow = documentViews.length - mostRecentViewers.length;
|
||||
|
||||
return (
|
||||
<Facepile
|
||||
users={mostRecentViewers.map(v => v.user)}
|
||||
users={mostRecentViewers.map((v) => v.user)}
|
||||
overflow={overflow}
|
||||
renderAvatar={user => {
|
||||
renderAvatar={(user) => {
|
||||
const isPresent = presentIds.includes(user.id);
|
||||
const isEditing = editingIds.includes(user.id);
|
||||
const { lastViewedAt } = viewersKeyedByUserId[user.id];
|
||||
@@ -73,4 +73,4 @@ class Collaborators extends React.Component<Props> {
|
||||
}
|
||||
}
|
||||
|
||||
export default inject('views', 'presence')(Collaborators);
|
||||
export default inject("views", "presence")(Collaborators);
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
// @flow
|
||||
import { inject, observer } from "mobx-react";
|
||||
import { PrivateCollectionIcon, CollectionIcon } from "outline-icons";
|
||||
import { getLuminance } from "polished";
|
||||
import * as React from "react";
|
||||
import UiStore from "stores/UiStore";
|
||||
import Collection from "models/Collection";
|
||||
import { icons } from "components/IconPicker";
|
||||
|
||||
type Props = {
|
||||
collection: Collection,
|
||||
expanded?: boolean,
|
||||
size?: number,
|
||||
ui: UiStore,
|
||||
};
|
||||
|
||||
function ResolvedCollectionIcon({ collection, expanded, size, ui }: Props) {
|
||||
// If the chosen icon color is very dark then we invert it in dark mode
|
||||
// otherwise it will be impossible to see against the dark background.
|
||||
const color =
|
||||
ui.resolvedTheme === "dark"
|
||||
? getLuminance(collection.color) > 0.12
|
||||
? collection.color
|
||||
: "currentColor"
|
||||
: collection.color;
|
||||
|
||||
if (collection.icon && collection.icon !== "collection") {
|
||||
try {
|
||||
const Component = icons[collection.icon].component;
|
||||
return <Component color={color} size={size} />;
|
||||
} catch (error) {
|
||||
console.warn("Failed to render custom icon " + collection.icon);
|
||||
}
|
||||
}
|
||||
|
||||
if (collection.private) {
|
||||
return (
|
||||
<PrivateCollectionIcon color={color} expanded={expanded} size={size} />
|
||||
);
|
||||
}
|
||||
|
||||
return <CollectionIcon color={color} expanded={expanded} size={size} />;
|
||||
}
|
||||
|
||||
export default inject("ui")(observer(ResolvedCollectionIcon));
|
||||
@@ -1,106 +0,0 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import { observable } from 'mobx';
|
||||
import { observer } from 'mobx-react';
|
||||
import { TwitterPicker } from 'react-color';
|
||||
import styled from 'styled-components';
|
||||
import Fade from 'components/Fade';
|
||||
import { LabelText } from 'components/Input';
|
||||
|
||||
const colors = [
|
||||
'#4E5C6E',
|
||||
'#19B7FF',
|
||||
'#7F6BFF',
|
||||
'#FC7419',
|
||||
'#FC2D2D',
|
||||
'#FFE100',
|
||||
'#14CF9F',
|
||||
'#00D084',
|
||||
'#EE84F0',
|
||||
'#2F362F',
|
||||
];
|
||||
|
||||
type Props = {
|
||||
onChange: (color: string) => void,
|
||||
value?: string,
|
||||
};
|
||||
|
||||
@observer
|
||||
class ColorPicker extends React.Component<Props> {
|
||||
@observable isOpen: boolean = false;
|
||||
node: ?HTMLElement;
|
||||
|
||||
componentDidMount() {
|
||||
window.addEventListener('click', this.handleClickOutside);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
window.removeEventListener('click', this.handleClickOutside);
|
||||
}
|
||||
|
||||
handleClose = () => {
|
||||
this.isOpen = false;
|
||||
};
|
||||
|
||||
handleOpen = () => {
|
||||
this.isOpen = true;
|
||||
};
|
||||
|
||||
handleClickOutside = (ev: SyntheticMouseEvent<>) => {
|
||||
// $FlowFixMe
|
||||
if (ev.target && this.node && this.node.contains(ev.target)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.handleClose();
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Wrapper ref={ref => (this.node = ref)}>
|
||||
<label>
|
||||
<LabelText>Color</LabelText>
|
||||
</label>
|
||||
<Swatch
|
||||
role="button"
|
||||
onClick={this.isOpen ? this.handleClose : this.handleOpen}
|
||||
color={this.props.value}
|
||||
/>
|
||||
<Floating>
|
||||
{this.isOpen && (
|
||||
<Fade>
|
||||
<TwitterPicker
|
||||
colors={colors}
|
||||
color={this.props.value}
|
||||
onChange={color => this.props.onChange(color.hex)}
|
||||
triangle="top-right"
|
||||
/>
|
||||
</Fade>
|
||||
)}
|
||||
</Floating>
|
||||
</Wrapper>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const Wrapper = styled('div')`
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
`;
|
||||
const Floating = styled('div')`
|
||||
position: absolute;
|
||||
top: 60px;
|
||||
right: 0;
|
||||
z-index: 1;
|
||||
`;
|
||||
|
||||
const Swatch = styled('div')`
|
||||
display: inline-block;
|
||||
width: 48px;
|
||||
height: 32px;
|
||||
border: 1px solid ${({ active, color }) => (active ? 'white' : 'transparent')};
|
||||
border-radius: 4px;
|
||||
background: ${({ color }) => color};
|
||||
`;
|
||||
|
||||
export default ColorPicker;
|
||||
@@ -1,6 +1,6 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import copy from 'copy-to-clipboard';
|
||||
import copy from "copy-to-clipboard";
|
||||
import * as React from "react";
|
||||
|
||||
type Props = {
|
||||
text: string,
|
||||
@@ -14,12 +14,12 @@ class CopyToClipboard extends React.PureComponent<Props> {
|
||||
const { text, onCopy, children } = this.props;
|
||||
const elem = React.Children.only(children);
|
||||
copy(text, {
|
||||
debug: !!__DEV__,
|
||||
debug: process.env.NODE_ENV !== "production",
|
||||
});
|
||||
|
||||
if (onCopy) onCopy();
|
||||
|
||||
if (elem && elem.props && typeof elem.props.onClick === 'function') {
|
||||
if (elem && elem.props && typeof elem.props.onClick === "function") {
|
||||
elem.props.onClick(ev);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
// @flow
|
||||
import * as React from "react";
|
||||
|
||||
type Props = {
|
||||
delay?: number,
|
||||
children: React.Node,
|
||||
};
|
||||
|
||||
export default function DelayedMount({ delay = 250, children }: Props) {
|
||||
const [isShowing, setShowing] = React.useState(false);
|
||||
|
||||
React.useEffect(() => {
|
||||
const timeout = setTimeout(() => setShowing(true), delay);
|
||||
return () => {
|
||||
clearTimeout(timeout);
|
||||
};
|
||||
}, []);
|
||||
|
||||
if (!isShowing) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return children;
|
||||
}
|
||||
@@ -1,23 +1,23 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import { observable, action } from 'mobx';
|
||||
import { observer, inject } from 'mobx-react';
|
||||
import type { RouterHistory } from 'react-router-dom';
|
||||
import styled from 'styled-components';
|
||||
import { Waypoint } from 'react-waypoint';
|
||||
import ArrowKeyNavigation from 'boundless-arrow-key-navigation';
|
||||
import ArrowKeyNavigation from "boundless-arrow-key-navigation";
|
||||
import { observable, action } from "mobx";
|
||||
import { observer, inject } from "mobx-react";
|
||||
import * as React from "react";
|
||||
import { type RouterHistory, type Match } from "react-router-dom";
|
||||
import { Waypoint } from "react-waypoint";
|
||||
import styled from "styled-components";
|
||||
|
||||
import { DEFAULT_PAGINATION_LIMIT } from 'stores/BaseStore';
|
||||
import DocumentsStore from 'stores/DocumentsStore';
|
||||
import RevisionsStore from 'stores/RevisionsStore';
|
||||
import { DEFAULT_PAGINATION_LIMIT } from "stores/BaseStore";
|
||||
import DocumentsStore from "stores/DocumentsStore";
|
||||
import RevisionsStore from "stores/RevisionsStore";
|
||||
|
||||
import Flex from 'shared/components/Flex';
|
||||
import { ListPlaceholder } from 'components/LoadingPlaceholder';
|
||||
import Revision from './components/Revision';
|
||||
import { documentHistoryUrl } from 'utils/routeHelpers';
|
||||
import Flex from "components/Flex";
|
||||
import { ListPlaceholder } from "components/LoadingPlaceholder";
|
||||
import Revision from "./components/Revision";
|
||||
import { documentHistoryUrl } from "utils/routeHelpers";
|
||||
|
||||
type Props = {
|
||||
match: Object,
|
||||
match: Match,
|
||||
documents: DocumentsStore,
|
||||
revisions: RevisionsStore,
|
||||
history: RouterHistory,
|
||||
@@ -42,7 +42,7 @@ class DocumentHistory extends React.Component<Props> {
|
||||
const results = await this.props.revisions.fetchPage({
|
||||
limit,
|
||||
offset: this.offset,
|
||||
id: this.props.match.params.documentSlug,
|
||||
documentId: this.props.match.params.documentSlug,
|
||||
});
|
||||
|
||||
if (
|
||||
@@ -133,17 +133,17 @@ const Wrapper = styled(Flex)`
|
||||
top: 0;
|
||||
right: 0;
|
||||
z-index: 1;
|
||||
min-width: ${props => props.theme.sidebarWidth};
|
||||
min-width: ${(props) => props.theme.sidebarWidth};
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
overscroll-behavior: none;
|
||||
`;
|
||||
|
||||
const Sidebar = styled(Flex)`
|
||||
background: ${props => props.theme.background};
|
||||
min-width: ${props => props.theme.sidebarWidth};
|
||||
border-left: 1px solid ${props => props.theme.divider};
|
||||
background: ${(props) => props.theme.background};
|
||||
min-width: ${(props) => props.theme.sidebarWidth};
|
||||
border-left: 1px solid ${(props) => props.theme.divider};
|
||||
z-index: 1;
|
||||
`;
|
||||
|
||||
export default inject('documents', 'revisions')(DocumentHistory);
|
||||
export default inject("documents", "revisions")(DocumentHistory);
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import { NavLink } from 'react-router-dom';
|
||||
import styled, { withTheme } from 'styled-components';
|
||||
import format from 'date-fns/format';
|
||||
import { MoreIcon } from 'outline-icons';
|
||||
import format from "date-fns/format";
|
||||
import { MoreIcon } from "outline-icons";
|
||||
import * as React from "react";
|
||||
import { NavLink } from "react-router-dom";
|
||||
import styled, { withTheme } from "styled-components";
|
||||
|
||||
import Flex from 'shared/components/Flex';
|
||||
import Time from 'shared/components/Time';
|
||||
import Avatar from 'components/Avatar';
|
||||
import RevisionMenu from 'menus/RevisionMenu';
|
||||
import Document from 'models/Document';
|
||||
import Revision from 'models/Revision';
|
||||
import Document from "models/Document";
|
||||
import Revision from "models/Revision";
|
||||
import Avatar from "components/Avatar";
|
||||
import Flex from "components/Flex";
|
||||
import Time from "components/Time";
|
||||
import RevisionMenu from "menus/RevisionMenu";
|
||||
|
||||
import { documentHistoryUrl } from 'utils/routeHelpers';
|
||||
import { documentHistoryUrl } from "utils/routeHelpers";
|
||||
|
||||
type Props = {
|
||||
theme: Object,
|
||||
@@ -32,12 +32,12 @@ class RevisionListItem extends React.Component<Props> {
|
||||
activeStyle={{ background: theme.primary, color: theme.white }}
|
||||
>
|
||||
<Author>
|
||||
<StyledAvatar src={revision.createdBy.avatarUrl} />{' '}
|
||||
<StyledAvatar src={revision.createdBy.avatarUrl} />{" "}
|
||||
{revision.createdBy.name}
|
||||
</Author>
|
||||
<Meta>
|
||||
<Time dateTime={revision.createdAt}>
|
||||
{format(revision.createdAt, 'MMMM Do, YYYY h:mm a')}
|
||||
{format(revision.createdAt, "MMMM Do, YYYY h:mm a")}
|
||||
</Time>
|
||||
</Meta>
|
||||
{showMenu && (
|
||||
@@ -66,7 +66,7 @@ const StyledRevisionMenu = styled(RevisionMenu)`
|
||||
`;
|
||||
|
||||
const StyledNavLink = styled(NavLink)`
|
||||
color: ${props => props.theme.text};
|
||||
color: ${(props) => props.theme.text};
|
||||
display: block;
|
||||
padding: 8px 16px;
|
||||
font-size: 15px;
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
// @flow
|
||||
import DocumentHistory from './DocumentHistory';
|
||||
import DocumentHistory from "./DocumentHistory";
|
||||
export default DocumentHistory;
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import Document from 'models/Document';
|
||||
import DocumentPreview from 'components/DocumentPreview';
|
||||
import ArrowKeyNavigation from 'boundless-arrow-key-navigation';
|
||||
import ArrowKeyNavigation from "boundless-arrow-key-navigation";
|
||||
import * as React from "react";
|
||||
import Document from "models/Document";
|
||||
import DocumentPreview from "components/DocumentPreview";
|
||||
|
||||
type Props = {
|
||||
documents: Document[],
|
||||
@@ -17,7 +17,7 @@ export default function DocumentList({ limit, documents, ...rest }: Props) {
|
||||
mode={ArrowKeyNavigation.mode.VERTICAL}
|
||||
defaultActiveChildIndex={0}
|
||||
>
|
||||
{items.map(document => (
|
||||
{items.map((document) => (
|
||||
<DocumentPreview key={document.id} document={document} {...rest} />
|
||||
))}
|
||||
</ArrowKeyNavigation>
|
||||
|
||||
@@ -1,25 +1,26 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import { inject, observer } from 'mobx-react';
|
||||
import styled from 'styled-components';
|
||||
import Document from 'models/Document';
|
||||
import Flex from 'shared/components/Flex';
|
||||
import Time from 'shared/components/Time';
|
||||
import Breadcrumb from 'shared/components/Breadcrumb';
|
||||
import CollectionsStore from 'stores/CollectionsStore';
|
||||
import AuthStore from 'stores/AuthStore';
|
||||
import { inject, observer } from "mobx-react";
|
||||
import * as React from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import styled from "styled-components";
|
||||
import AuthStore from "stores/AuthStore";
|
||||
import CollectionsStore from "stores/CollectionsStore";
|
||||
import Document from "models/Document";
|
||||
import Breadcrumb from "components/Breadcrumb";
|
||||
import Flex from "components/Flex";
|
||||
import Time from "components/Time";
|
||||
|
||||
const Container = styled(Flex)`
|
||||
color: ${props => props.theme.textTertiary};
|
||||
color: ${(props) => props.theme.textTertiary};
|
||||
font-size: 13px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
`;
|
||||
|
||||
const Modified = styled.span`
|
||||
color: ${props =>
|
||||
color: ${(props) =>
|
||||
props.highlight ? props.theme.text : props.theme.textTertiary};
|
||||
font-weight: ${props => (props.highlight ? '600' : '400')};
|
||||
font-weight: ${(props) => (props.highlight ? "600" : "400")};
|
||||
`;
|
||||
|
||||
type Props = {
|
||||
@@ -29,15 +30,17 @@ type Props = {
|
||||
showPublished?: boolean,
|
||||
document: Document,
|
||||
children: React.Node,
|
||||
to?: string,
|
||||
};
|
||||
|
||||
function PublishingInfo({
|
||||
function DocumentMeta({
|
||||
auth,
|
||||
collections,
|
||||
showPublished,
|
||||
showCollection,
|
||||
document,
|
||||
children,
|
||||
to,
|
||||
...rest
|
||||
}: Props) {
|
||||
const {
|
||||
@@ -102,20 +105,19 @@ function PublishingInfo({
|
||||
|
||||
return (
|
||||
<Container align="center" {...rest}>
|
||||
{updatedByMe ? 'You' : updatedBy.name}
|
||||
{content}
|
||||
{showCollection &&
|
||||
collection && (
|
||||
<span>
|
||||
in
|
||||
<strong>
|
||||
<Breadcrumb document={document} onlyText />
|
||||
</strong>
|
||||
</span>
|
||||
)}
|
||||
{updatedByMe ? "You" : updatedBy.name}
|
||||
{to ? <Link to={to}>{content}</Link> : content}
|
||||
{showCollection && collection && (
|
||||
<span>
|
||||
in
|
||||
<strong>
|
||||
<Breadcrumb document={document} onlyText />
|
||||
</strong>
|
||||
</span>
|
||||
)}
|
||||
{children}
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
export default inject('collections', 'auth')(observer(PublishingInfo));
|
||||
export default inject("collections", "auth")(observer(DocumentMeta));
|
||||
@@ -0,0 +1,48 @@
|
||||
// @flow
|
||||
import { inject } from "mobx-react";
|
||||
import * as React from "react";
|
||||
import styled from "styled-components";
|
||||
import ViewsStore from "stores/ViewsStore";
|
||||
import Document from "models/Document";
|
||||
import DocumentMeta from "components/DocumentMeta";
|
||||
|
||||
type Props = {|
|
||||
views: ViewsStore,
|
||||
document: Document,
|
||||
isDraft: boolean,
|
||||
to?: string,
|
||||
|};
|
||||
|
||||
function DocumentMetaWithViews({ views, to, isDraft, document }: Props) {
|
||||
const totalViews = views.countForDocument(document.id);
|
||||
|
||||
return (
|
||||
<Meta document={document} to={to}>
|
||||
{totalViews && !isDraft ? (
|
||||
<>
|
||||
· Viewed{" "}
|
||||
{totalViews === 1 ? "once" : `${totalViews} times`}
|
||||
</>
|
||||
) : null}
|
||||
</Meta>
|
||||
);
|
||||
}
|
||||
|
||||
const Meta = styled(DocumentMeta)`
|
||||
margin: -12px 0 2em 0;
|
||||
font-size: 14px;
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
@media print {
|
||||
display: none;
|
||||
}
|
||||
`;
|
||||
|
||||
export default inject("views")(DocumentMetaWithViews);
|
||||
@@ -1,18 +1,21 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import { observer } from 'mobx-react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { StarredIcon } from 'outline-icons';
|
||||
import styled, { withTheme } from 'styled-components';
|
||||
import Flex from 'shared/components/Flex';
|
||||
import Badge from 'components/Badge';
|
||||
import Tooltip from 'components/Tooltip';
|
||||
import Highlight from 'components/Highlight';
|
||||
import PublishingInfo from 'components/PublishingInfo';
|
||||
import DocumentMenu from 'menus/DocumentMenu';
|
||||
import Document from 'models/Document';
|
||||
import { observer } from "mobx-react";
|
||||
import { StarredIcon, PlusIcon } from "outline-icons";
|
||||
import * as React from "react";
|
||||
import { Link, withRouter, type RouterHistory } from "react-router-dom";
|
||||
import styled, { withTheme } from "styled-components";
|
||||
import Document from "models/Document";
|
||||
import Badge from "components/Badge";
|
||||
import Button from "components/Button";
|
||||
import DocumentMeta from "components/DocumentMeta";
|
||||
import Flex from "components/Flex";
|
||||
import Highlight from "components/Highlight";
|
||||
import Tooltip from "components/Tooltip";
|
||||
import DocumentMenu from "menus/DocumentMenu";
|
||||
import { newDocumentUrl } from "utils/routeHelpers";
|
||||
|
||||
type Props = {
|
||||
history: RouterHistory,
|
||||
document: Document,
|
||||
highlight?: ?string,
|
||||
context?: ?string,
|
||||
@@ -20,13 +23,127 @@ type Props = {
|
||||
showPublished?: boolean,
|
||||
showPin?: boolean,
|
||||
showDraft?: boolean,
|
||||
showTemplate?: boolean,
|
||||
};
|
||||
|
||||
const SEARCH_RESULT_REGEX = /<b\b[^>]*>(.*?)<\/b>/gi;
|
||||
|
||||
@observer
|
||||
class DocumentPreview extends React.Component<Props> {
|
||||
handleStar = (ev: SyntheticEvent<>) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
this.props.document.star();
|
||||
};
|
||||
|
||||
handleUnstar = (ev: SyntheticEvent<>) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
this.props.document.unstar();
|
||||
};
|
||||
|
||||
replaceResultMarks = (tag: string) => {
|
||||
// don't use SEARCH_RESULT_REGEX here as it causes
|
||||
// an infinite loop to trigger a regex inside it's own callback
|
||||
return tag.replace(/<b\b[^>]*>(.*?)<\/b>/gi, "$1");
|
||||
};
|
||||
|
||||
handleNewFromTemplate = (event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
const { document } = this.props;
|
||||
|
||||
this.props.history.push(
|
||||
newDocumentUrl(document.collectionId, {
|
||||
templateId: document.id,
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
document,
|
||||
showCollection,
|
||||
showPublished,
|
||||
showPin,
|
||||
showDraft = true,
|
||||
showTemplate,
|
||||
highlight,
|
||||
context,
|
||||
} = this.props;
|
||||
|
||||
const queryIsInTitle =
|
||||
!!highlight &&
|
||||
!!document.title.toLowerCase().includes(highlight.toLowerCase());
|
||||
|
||||
return (
|
||||
<DocumentLink
|
||||
to={{
|
||||
pathname: document.url,
|
||||
state: { title: document.titleWithDefault },
|
||||
}}
|
||||
>
|
||||
<Heading>
|
||||
<Title text={document.titleWithDefault} highlight={highlight} />
|
||||
{!document.isDraft &&
|
||||
!document.isArchived &&
|
||||
!document.isTemplate && (
|
||||
<Actions>
|
||||
{document.isStarred ? (
|
||||
<StyledStar onClick={this.handleUnstar} solid />
|
||||
) : (
|
||||
<StyledStar onClick={this.handleStar} />
|
||||
)}
|
||||
</Actions>
|
||||
)}
|
||||
{document.isDraft && showDraft && (
|
||||
<Tooltip tooltip="Only visible to you" delay={500} placement="top">
|
||||
<Badge>Draft</Badge>
|
||||
</Tooltip>
|
||||
)}
|
||||
{document.isTemplate && showTemplate && (
|
||||
<Badge primary>Template</Badge>
|
||||
)}
|
||||
<SecondaryActions>
|
||||
{document.isTemplate &&
|
||||
!document.isArchived &&
|
||||
!document.isDeleted && (
|
||||
<Button
|
||||
onClick={this.handleNewFromTemplate}
|
||||
icon={<PlusIcon />}
|
||||
neutral
|
||||
>
|
||||
New doc
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<DocumentMenu document={document} showPin={showPin} />
|
||||
</SecondaryActions>
|
||||
</Heading>
|
||||
|
||||
{!queryIsInTitle && (
|
||||
<ResultContext
|
||||
text={context}
|
||||
highlight={highlight ? SEARCH_RESULT_REGEX : undefined}
|
||||
processResult={this.replaceResultMarks}
|
||||
/>
|
||||
)}
|
||||
<DocumentMeta
|
||||
document={document}
|
||||
showCollection={showCollection}
|
||||
showPublished={showPublished}
|
||||
/>
|
||||
</DocumentLink>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const StyledStar = withTheme(styled(({ solid, theme, ...props }) => (
|
||||
<StarredIcon color={theme.text} {...props} />
|
||||
))`
|
||||
flex-shrink: 0;
|
||||
opacity: ${props => (props.solid ? '1 !important' : 0)};
|
||||
opacity: ${(props) => (props.solid ? "1 !important" : 0)};
|
||||
transition: all 100ms ease-in-out;
|
||||
|
||||
&:hover {
|
||||
@@ -37,7 +154,8 @@ const StyledStar = withTheme(styled(({ solid, theme, ...props }) => (
|
||||
}
|
||||
`);
|
||||
|
||||
const StyledDocumentMenu = styled(DocumentMenu)`
|
||||
const SecondaryActions = styled(Flex)`
|
||||
align-items: center;
|
||||
position: absolute;
|
||||
right: 16px;
|
||||
top: 50%;
|
||||
@@ -51,20 +169,25 @@ const DocumentLink = styled(Link)`
|
||||
border-radius: 8px;
|
||||
max-height: 50vh;
|
||||
min-width: 100%;
|
||||
max-width: calc(100vw - 40px);
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
|
||||
${StyledDocumentMenu} {
|
||||
${SecondaryActions} {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&:active,
|
||||
&:focus {
|
||||
background: ${props => props.theme.listItemHoverBackground};
|
||||
background: ${(props) => props.theme.listItemHoverBackground};
|
||||
outline: none;
|
||||
|
||||
${StyledStar}, ${StyledDocumentMenu} {
|
||||
${SecondaryActions} {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
${StyledStar} {
|
||||
opacity: 0.5;
|
||||
|
||||
&:hover {
|
||||
@@ -82,8 +205,9 @@ const Heading = styled.h3`
|
||||
margin-bottom: 0.25em;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen,
|
||||
Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
|
||||
color: ${(props) => props.theme.text};
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen,
|
||||
Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
|
||||
`;
|
||||
|
||||
const Actions = styled(Flex)`
|
||||
@@ -99,97 +223,10 @@ const Title = styled(Highlight)`
|
||||
|
||||
const ResultContext = styled(Highlight)`
|
||||
display: block;
|
||||
color: ${props => props.theme.textTertiary};
|
||||
color: ${(props) => props.theme.textTertiary};
|
||||
font-size: 14px;
|
||||
margin-top: -0.25em;
|
||||
margin-bottom: 0.25em;
|
||||
`;
|
||||
|
||||
const SEARCH_RESULT_REGEX = /<b\b[^>]*>(.*?)<\/b>/gi;
|
||||
|
||||
@observer
|
||||
class DocumentPreview extends React.Component<Props> {
|
||||
star = (ev: SyntheticEvent<>) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
this.props.document.star();
|
||||
};
|
||||
|
||||
unstar = (ev: SyntheticEvent<>) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
this.props.document.unstar();
|
||||
};
|
||||
|
||||
replaceResultMarks = (tag: string) => {
|
||||
// don't use SEARCH_RESULT_REGEX here as it causes
|
||||
// an infinite loop to trigger a regex inside it's own callback
|
||||
return tag.replace(/<b\b[^>]*>(.*?)<\/b>/gi, '$1');
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
document,
|
||||
showCollection,
|
||||
showPublished,
|
||||
showPin,
|
||||
showDraft = true,
|
||||
highlight,
|
||||
context,
|
||||
...rest
|
||||
} = this.props;
|
||||
|
||||
const queryIsInTitle =
|
||||
!!highlight &&
|
||||
!!document.title.toLowerCase().includes(highlight.toLowerCase());
|
||||
|
||||
return (
|
||||
<DocumentLink
|
||||
to={{
|
||||
pathname: document.url,
|
||||
state: { title: document.title },
|
||||
}}
|
||||
{...rest}
|
||||
>
|
||||
<Heading>
|
||||
<Title text={document.title || 'Untitled'} highlight={highlight} />
|
||||
{!document.isDraft &&
|
||||
!document.isArchived && (
|
||||
<Actions>
|
||||
{document.isStarred ? (
|
||||
<StyledStar onClick={this.unstar} solid />
|
||||
) : (
|
||||
<StyledStar onClick={this.star} />
|
||||
)}
|
||||
</Actions>
|
||||
)}
|
||||
{document.isDraft &&
|
||||
showDraft && (
|
||||
<Tooltip
|
||||
tooltip="Only visible to you"
|
||||
delay={500}
|
||||
placement="top"
|
||||
>
|
||||
<Badge>Draft</Badge>
|
||||
</Tooltip>
|
||||
)}
|
||||
<StyledDocumentMenu document={document} showPin={showPin} />
|
||||
</Heading>
|
||||
{!queryIsInTitle && (
|
||||
<ResultContext
|
||||
text={context}
|
||||
highlight={highlight ? SEARCH_RESULT_REGEX : undefined}
|
||||
processResult={this.replaceResultMarks}
|
||||
/>
|
||||
)}
|
||||
<PublishingInfo
|
||||
document={document}
|
||||
showCollection={showCollection}
|
||||
showPublished={showPublished}
|
||||
/>
|
||||
</DocumentLink>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default DocumentPreview;
|
||||
export default withRouter(DocumentPreview);
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
// @flow
|
||||
import DocumentPreview from './DocumentPreview';
|
||||
import DocumentPreview from "./DocumentPreview";
|
||||
export default DocumentPreview;
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import { observable } from 'mobx';
|
||||
import { observer, inject } from 'mobx-react';
|
||||
import { withRouter, type RouterHistory } from 'react-router-dom';
|
||||
import { createGlobalStyle } from 'styled-components';
|
||||
import invariant from 'invariant';
|
||||
import importFile from 'utils/importFile';
|
||||
import Dropzone from 'react-dropzone';
|
||||
import DocumentsStore from 'stores/DocumentsStore';
|
||||
import LoadingIndicator from 'components/LoadingIndicator';
|
||||
import invariant from "invariant";
|
||||
import { observable } from "mobx";
|
||||
import { observer, inject } from "mobx-react";
|
||||
import * as React from "react";
|
||||
import Dropzone from "react-dropzone";
|
||||
import { withRouter, type RouterHistory, type Match } from "react-router-dom";
|
||||
import { createGlobalStyle } from "styled-components";
|
||||
import DocumentsStore from "stores/DocumentsStore";
|
||||
import LoadingIndicator from "components/LoadingIndicator";
|
||||
import importFile from "utils/importFile";
|
||||
|
||||
const EMPTY_OBJECT = {};
|
||||
let importingLock = false;
|
||||
@@ -22,7 +22,7 @@ type Props = {
|
||||
documents: DocumentsStore,
|
||||
disabled: boolean,
|
||||
location: Object,
|
||||
match: Object,
|
||||
match: Match,
|
||||
history: RouterHistory,
|
||||
staticContext: Object,
|
||||
};
|
||||
@@ -30,12 +30,12 @@ type Props = {
|
||||
export const GlobalStyles = createGlobalStyle`
|
||||
.activeDropZone {
|
||||
border-radius: 4px;
|
||||
background: ${props => props.theme.slateDark};
|
||||
svg { fill: ${props => props.theme.white}; }
|
||||
background: ${(props) => props.theme.slateDark};
|
||||
svg { fill: ${(props) => props.theme.white}; }
|
||||
}
|
||||
|
||||
.activeDropZone a {
|
||||
color: ${props => props.theme.white} !important;
|
||||
color: ${(props) => props.theme.white} !important;
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -56,7 +56,7 @@ class DropToImport extends React.Component<Props> {
|
||||
|
||||
if (documentId && !collectionId) {
|
||||
const document = await this.props.documents.fetch(documentId);
|
||||
invariant(document, 'Document not available');
|
||||
invariant(document, "Document not available");
|
||||
collectionId = document.collectionId;
|
||||
}
|
||||
|
||||
@@ -110,4 +110,4 @@ class DropToImport extends React.Component<Props> {
|
||||
}
|
||||
}
|
||||
|
||||
export default inject('documents')(withRouter(DropToImport));
|
||||
export default inject("documents")(withRouter(DropToImport));
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import invariant from 'invariant';
|
||||
import { observable } from 'mobx';
|
||||
import { observer } from 'mobx-react';
|
||||
import { PortalWithState } from 'react-portal';
|
||||
import { MoreIcon } from 'outline-icons';
|
||||
import styled from 'styled-components';
|
||||
import Flex from 'shared/components/Flex';
|
||||
import { fadeAndScaleIn } from 'shared/styles/animations';
|
||||
import NudeButton from 'components/NudeButton';
|
||||
import invariant from "invariant";
|
||||
import { observable } from "mobx";
|
||||
import { observer } from "mobx-react";
|
||||
import { MoreIcon } from "outline-icons";
|
||||
import { rgba } from "polished";
|
||||
import * as React from "react";
|
||||
import { PortalWithState } from "react-portal";
|
||||
import styled from "styled-components";
|
||||
import { fadeAndScaleIn } from "shared/styles/animations";
|
||||
import Flex from "components/Flex";
|
||||
import NudeButton from "components/NudeButton";
|
||||
|
||||
let previousClosePortal;
|
||||
let counter = 0;
|
||||
@@ -25,7 +26,7 @@ type Props = {
|
||||
className?: string,
|
||||
hover?: boolean,
|
||||
style?: Object,
|
||||
position?: 'left' | 'right' | 'center',
|
||||
position?: "left" | "right" | "center",
|
||||
};
|
||||
|
||||
@observer
|
||||
@@ -37,7 +38,7 @@ class DropdownMenu extends React.Component<Props> {
|
||||
@observable bottom: ?number;
|
||||
@observable right: ?number;
|
||||
@observable left: ?number;
|
||||
@observable position: 'left' | 'right' | 'center';
|
||||
@observable position: "left" | "right" | "center";
|
||||
@observable fixed: ?boolean;
|
||||
@observable bodyRect: ClientRect;
|
||||
@observable labelRect: ClientRect;
|
||||
@@ -51,21 +52,21 @@ class DropdownMenu extends React.Component<Props> {
|
||||
return (ev: SyntheticMouseEvent<HTMLElement>) => {
|
||||
ev.preventDefault();
|
||||
const currentTarget = ev.currentTarget;
|
||||
invariant(document.body, 'why you not here');
|
||||
invariant(document.body, "why you not here");
|
||||
|
||||
if (currentTarget instanceof HTMLDivElement) {
|
||||
this.bodyRect = document.body.getBoundingClientRect();
|
||||
this.labelRect = currentTarget.getBoundingClientRect();
|
||||
this.top = this.labelRect.bottom - this.bodyRect.top;
|
||||
this.bottom = undefined;
|
||||
this.position = this.props.position || 'left';
|
||||
this.position = this.props.position || "left";
|
||||
|
||||
if (currentTarget.parentElement) {
|
||||
const triggerParentStyle = getComputedStyle(
|
||||
currentTarget.parentElement
|
||||
);
|
||||
|
||||
if (triggerParentStyle.position === 'static') {
|
||||
if (triggerParentStyle.position === "static") {
|
||||
this.fixed = true;
|
||||
this.top = this.labelRect.bottom;
|
||||
}
|
||||
@@ -84,10 +85,10 @@ class DropdownMenu extends React.Component<Props> {
|
||||
};
|
||||
|
||||
initPosition() {
|
||||
if (this.position === 'left') {
|
||||
if (this.position === "left") {
|
||||
this.right =
|
||||
this.bodyRect.width - this.labelRect.left - this.labelRect.width;
|
||||
} else if (this.position === 'center') {
|
||||
} else if (this.position === "center") {
|
||||
this.left = this.labelRect.left + this.labelRect.width / 2;
|
||||
} else {
|
||||
this.left = this.labelRect.left;
|
||||
@@ -95,7 +96,7 @@ class DropdownMenu extends React.Component<Props> {
|
||||
}
|
||||
|
||||
onOpen = () => {
|
||||
if (typeof this.props.onOpen === 'function') {
|
||||
if (typeof this.props.onOpen === "function") {
|
||||
this.props.onOpen();
|
||||
}
|
||||
this.fitOnTheScreen();
|
||||
@@ -114,18 +115,18 @@ class DropdownMenu extends React.Component<Props> {
|
||||
this.bottom = undefined;
|
||||
}
|
||||
|
||||
if (this.position === 'left' || this.position === 'right') {
|
||||
if (this.position === "left" || this.position === "right") {
|
||||
const totalWidth =
|
||||
Math.sign(this.position === 'left' ? -1 : 1) * el.offsetLeft +
|
||||
Math.sign(this.position === "left" ? -1 : 1) * el.offsetLeft +
|
||||
el.scrollWidth;
|
||||
const isVisible = totalWidth < window.innerWidth;
|
||||
|
||||
if (!isVisible) {
|
||||
if (this.position === 'right') {
|
||||
this.position = 'left';
|
||||
if (this.position === "right") {
|
||||
this.position = "left";
|
||||
this.left = undefined;
|
||||
} else if (this.position === 'left') {
|
||||
this.position = 'right';
|
||||
} else if (this.position === "left") {
|
||||
this.position = "right";
|
||||
this.right = undefined;
|
||||
}
|
||||
}
|
||||
@@ -160,7 +161,7 @@ class DropdownMenu extends React.Component<Props> {
|
||||
closeOnEsc
|
||||
>
|
||||
{({ closePortal, openPortal, isOpen, portal }) => (
|
||||
<React.Fragment>
|
||||
<>
|
||||
<Label
|
||||
onMouseMove={hover ? this.clearCloseTimeout : undefined}
|
||||
onMouseOut={
|
||||
@@ -177,7 +178,7 @@ class DropdownMenu extends React.Component<Props> {
|
||||
<NudeButton
|
||||
id={`${this.id}button`}
|
||||
aria-haspopup="true"
|
||||
aria-expanded={isOpen ? 'true' : 'false'}
|
||||
aria-expanded={isOpen ? "true" : "false"}
|
||||
aria-controls={this.id}
|
||||
>
|
||||
<MoreIcon />
|
||||
@@ -201,9 +202,9 @@ class DropdownMenu extends React.Component<Props> {
|
||||
hover ? this.closeAfterTimeout(closePortal) : undefined
|
||||
}
|
||||
onClick={
|
||||
typeof children === 'function'
|
||||
typeof children === "function"
|
||||
? undefined
|
||||
: ev => {
|
||||
: (ev) => {
|
||||
ev.stopPropagation();
|
||||
closePortal();
|
||||
}
|
||||
@@ -213,13 +214,13 @@ class DropdownMenu extends React.Component<Props> {
|
||||
aria-labelledby={`${this.id}button`}
|
||||
role="menu"
|
||||
>
|
||||
{typeof children === 'function'
|
||||
{typeof children === "function"
|
||||
? children({ closePortal })
|
||||
: children}
|
||||
</Menu>
|
||||
</Position>
|
||||
)}
|
||||
</React.Fragment>
|
||||
</>
|
||||
)}
|
||||
</PortalWithState>
|
||||
</div>
|
||||
@@ -228,42 +229,58 @@ class DropdownMenu extends React.Component<Props> {
|
||||
}
|
||||
|
||||
const Label = styled(Flex).attrs({
|
||||
justify: 'center',
|
||||
align: 'center',
|
||||
justify: "center",
|
||||
align: "center",
|
||||
})`
|
||||
z-index: 1000;
|
||||
z-index: ${(props) => props.theme.depths.menu};
|
||||
cursor: pointer;
|
||||
`;
|
||||
|
||||
const Position = styled.div`
|
||||
position: ${({ fixed }) => (fixed ? 'fixed' : 'absolute')};
|
||||
position: ${({ fixed }) => (fixed ? "fixed" : "absolute")};
|
||||
display: flex;
|
||||
${({ left }) => (left !== undefined ? `left: ${left}px` : '')};
|
||||
${({ right }) => (right !== undefined ? `right: ${right}px` : '')};
|
||||
${({ top }) => (top !== undefined ? `top: ${top}px` : '')};
|
||||
${({ bottom }) => (bottom !== undefined ? `bottom: ${bottom}px` : '')};
|
||||
${({ left }) => (left !== undefined ? `left: ${left}px` : "")};
|
||||
${({ right }) => (right !== undefined ? `right: ${right}px` : "")};
|
||||
${({ top }) => (top !== undefined ? `top: ${top}px` : "")};
|
||||
${({ bottom }) => (bottom !== undefined ? `bottom: ${bottom}px` : "")};
|
||||
max-height: 75%;
|
||||
z-index: 1000;
|
||||
transform: ${props =>
|
||||
props.position === 'center' ? 'translateX(-50%)' : 'initial'};
|
||||
z-index: ${(props) => props.theme.depths.menu};
|
||||
transform: ${(props) =>
|
||||
props.position === "center" ? "translateX(-50%)" : "initial"};
|
||||
pointer-events: none;
|
||||
`;
|
||||
|
||||
const Menu = styled.div`
|
||||
animation: ${fadeAndScaleIn} 200ms ease;
|
||||
transform-origin: ${props => (props.left !== undefined ? '25%' : '75%')} 0;
|
||||
background: ${props => props.theme.menuBackground};
|
||||
transform-origin: ${(props) => (props.left !== undefined ? "25%" : "75%")} 0;
|
||||
backdrop-filter: blur(10px);
|
||||
background: ${(props) => rgba(props.theme.menuBackground, 0.8)};
|
||||
border: ${(props) =>
|
||||
props.theme.menuBorder ? `1px solid ${props.theme.menuBorder}` : "none"};
|
||||
border-radius: 2px;
|
||||
padding: 0.5em 0;
|
||||
min-width: 180px;
|
||||
overflow: hidden;
|
||||
overflow-y: auto;
|
||||
box-shadow: ${props => props.theme.menuShadow};
|
||||
box-shadow: ${(props) => props.theme.menuShadow};
|
||||
pointer-events: all;
|
||||
|
||||
hr {
|
||||
margin: 0.5em 12px;
|
||||
}
|
||||
|
||||
@media print {
|
||||
display: none;
|
||||
}
|
||||
`;
|
||||
|
||||
export const Header = styled.h3`
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
color: ${(props) => props.theme.sidebarText};
|
||||
letter-spacing: 0.04em;
|
||||
margin: 1em 12px 0.5em;
|
||||
`;
|
||||
|
||||
export default DropdownMenu;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import { CheckmarkIcon } from 'outline-icons';
|
||||
import styled from 'styled-components';
|
||||
import { CheckmarkIcon } from "outline-icons";
|
||||
import * as React from "react";
|
||||
import styled from "styled-components";
|
||||
|
||||
type Props = {
|
||||
onClick?: (SyntheticEvent<>) => void | Promise<void>,
|
||||
@@ -26,11 +26,12 @@ const DropdownMenuItem = ({
|
||||
{...rest}
|
||||
>
|
||||
{selected !== undefined && (
|
||||
<React.Fragment>
|
||||
<>
|
||||
<CheckmarkIcon
|
||||
color={selected === false ? 'transparent' : undefined}
|
||||
/>
|
||||
</React.Fragment>
|
||||
color={selected === false ? "transparent" : undefined}
|
||||
/>
|
||||
|
||||
</>
|
||||
)}
|
||||
{children}
|
||||
</MenuItem>
|
||||
@@ -42,9 +43,9 @@ const MenuItem = styled.a`
|
||||
margin: 0;
|
||||
padding: 6px 12px;
|
||||
width: 100%;
|
||||
height: 32px;
|
||||
min-height: 32px;
|
||||
|
||||
color: ${props =>
|
||||
color: ${(props) =>
|
||||
props.disabled ? props.theme.textTertiary : props.theme.textSecondary};
|
||||
justify-content: left;
|
||||
align-items: center;
|
||||
@@ -57,12 +58,12 @@ const MenuItem = styled.a`
|
||||
}
|
||||
|
||||
svg {
|
||||
opacity: ${props => (props.disabled ? '.5' : 1)};
|
||||
opacity: ${(props) => (props.disabled ? ".5" : 1)};
|
||||
}
|
||||
|
||||
${props =>
|
||||
${(props) =>
|
||||
props.disabled
|
||||
? 'pointer-events: none;'
|
||||
? "pointer-events: none;"
|
||||
: `
|
||||
|
||||
&:hover {
|
||||
@@ -75,6 +76,12 @@ const MenuItem = styled.a`
|
||||
fill: ${props.theme.white};
|
||||
}
|
||||
}
|
||||
|
||||
&:focus {
|
||||
color: ${props.theme.white};
|
||||
background: ${props.theme.primary};
|
||||
outline: none;
|
||||
}
|
||||
`};
|
||||
`;
|
||||
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
// @flow
|
||||
export { default as DropdownMenu } from './DropdownMenu';
|
||||
export { default as DropdownMenuItem } from './DropdownMenuItem';
|
||||
export { default as DropdownMenu, Header } from "./DropdownMenu";
|
||||
export { default as DropdownMenuItem } from "./DropdownMenuItem";
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
// @flow
|
||||
import { lighten } from "polished";
|
||||
import * as React from "react";
|
||||
import { withRouter, type RouterHistory } from "react-router-dom";
|
||||
import styled, { withTheme } from "styled-components";
|
||||
import UiStore from "stores/UiStore";
|
||||
import ErrorBoundary from "components/ErrorBoundary";
|
||||
import Tooltip from "components/Tooltip";
|
||||
import embeds from "../embeds";
|
||||
import isInternalUrl from "utils/isInternalUrl";
|
||||
import { uploadFile } from "utils/uploadFile";
|
||||
|
||||
const RichMarkdownEditor = React.lazy(() => import("rich-markdown-editor"));
|
||||
|
||||
const EMPTY_ARRAY = [];
|
||||
|
||||
type Props = {
|
||||
id?: string,
|
||||
defaultValue?: string,
|
||||
readOnly?: boolean,
|
||||
grow?: boolean,
|
||||
disableEmbeds?: boolean,
|
||||
ui?: UiStore,
|
||||
};
|
||||
|
||||
type PropsWithRef = Props & {
|
||||
forwardedRef: React.Ref<any>,
|
||||
history: RouterHistory,
|
||||
};
|
||||
|
||||
class Editor extends React.Component<PropsWithRef> {
|
||||
onUploadImage = async (file: File) => {
|
||||
const result = await uploadFile(file, { documentId: this.props.id });
|
||||
return result.url;
|
||||
};
|
||||
|
||||
onClickLink = (href: string) => {
|
||||
// on page hash
|
||||
if (href[0] === "#") {
|
||||
window.location.href = href;
|
||||
return;
|
||||
}
|
||||
|
||||
if (isInternalUrl(href)) {
|
||||
// relative
|
||||
let navigateTo = href;
|
||||
|
||||
// probably absolute
|
||||
if (href[0] !== "/") {
|
||||
try {
|
||||
const url = new URL(href);
|
||||
navigateTo = url.pathname + url.hash;
|
||||
} catch (err) {
|
||||
navigateTo = href;
|
||||
}
|
||||
}
|
||||
|
||||
this.props.history.push(navigateTo);
|
||||
} else {
|
||||
window.open(href, "_blank");
|
||||
}
|
||||
};
|
||||
|
||||
onShowToast = (message: string) => {
|
||||
if (this.props.ui) {
|
||||
this.props.ui.showToast(message);
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<ErrorBoundary reloadOnChunkMissing>
|
||||
<StyledEditor
|
||||
ref={this.props.forwardedRef}
|
||||
uploadImage={this.onUploadImage}
|
||||
onClickLink={this.onClickLink}
|
||||
onShowToast={this.onShowToast}
|
||||
embeds={this.props.disableEmbeds ? EMPTY_ARRAY : embeds}
|
||||
tooltip={EditorTooltip}
|
||||
{...this.props}
|
||||
/>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const StyledEditor = styled(RichMarkdownEditor)`
|
||||
flex-grow: ${(props) => (props.grow ? 1 : 0)};
|
||||
justify-content: start;
|
||||
|
||||
> div {
|
||||
transition: ${(props) => props.theme.backgroundTransition};
|
||||
}
|
||||
|
||||
.notice-block.tip,
|
||||
.notice-block.warning {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
p {
|
||||
a {
|
||||
color: ${(props) => props.theme.text};
|
||||
border-bottom: 1px solid ${(props) => lighten(0.5, props.theme.text)};
|
||||
text-decoration: none !important;
|
||||
font-weight: 500;
|
||||
|
||||
&:hover {
|
||||
border-bottom: 1px solid ${(props) => props.theme.text};
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const EditorTooltip = ({ children, ...props }) => (
|
||||
<Tooltip offset="0, 16" delay={150} {...props}>
|
||||
<Span>{children}</Span>
|
||||
</Tooltip>
|
||||
);
|
||||
|
||||
const Span = styled.span`
|
||||
outline: none;
|
||||
`;
|
||||
|
||||
const EditorWithRouterAndTheme = withRouter(withTheme(Editor));
|
||||
|
||||
export default React.forwardRef<Props, typeof Editor>((props, ref) => (
|
||||
<EditorWithRouterAndTheme {...props} forwardedRef={ref} />
|
||||
));
|
||||
@@ -1,116 +0,0 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import { withRouter, type RouterHistory } from 'react-router-dom';
|
||||
import { observable } from 'mobx';
|
||||
import { observer } from 'mobx-react';
|
||||
import { lighten } from 'polished';
|
||||
import styled, { withTheme } from 'styled-components';
|
||||
import RichMarkdownEditor from 'rich-markdown-editor';
|
||||
import { uploadFile } from 'utils/uploadFile';
|
||||
import isInternalUrl from 'utils/isInternalUrl';
|
||||
import Tooltip from 'components/Tooltip';
|
||||
import UiStore from 'stores/UiStore';
|
||||
import embeds from '../../embeds';
|
||||
|
||||
const EMPTY_ARRAY = [];
|
||||
|
||||
type Props = {
|
||||
id: string,
|
||||
defaultValue?: string,
|
||||
readOnly?: boolean,
|
||||
grow?: boolean,
|
||||
disableEmbeds?: boolean,
|
||||
history: RouterHistory,
|
||||
forwardedRef: React.Ref<RichMarkdownEditor>,
|
||||
ui: UiStore,
|
||||
};
|
||||
|
||||
@observer
|
||||
class Editor extends React.Component<Props> {
|
||||
@observable redirectTo: ?string;
|
||||
|
||||
onUploadImage = async (file: File) => {
|
||||
const result = await uploadFile(file, { documentId: this.props.id });
|
||||
return result.url;
|
||||
};
|
||||
|
||||
onClickLink = (href: string) => {
|
||||
// on page hash
|
||||
if (href[0] === '#') {
|
||||
window.location.href = href;
|
||||
return;
|
||||
}
|
||||
|
||||
if (isInternalUrl(href)) {
|
||||
// relative
|
||||
let navigateTo = href;
|
||||
|
||||
// probably absolute
|
||||
if (href[0] !== '/') {
|
||||
try {
|
||||
const url = new URL(href);
|
||||
navigateTo = url.pathname + url.hash;
|
||||
} catch (err) {
|
||||
navigateTo = href;
|
||||
}
|
||||
}
|
||||
|
||||
this.props.history.push(navigateTo);
|
||||
} else {
|
||||
window.open(href, '_blank');
|
||||
}
|
||||
};
|
||||
|
||||
onShowToast = (message: string) => {
|
||||
this.props.ui.showToast(message);
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<StyledEditor
|
||||
ref={this.props.forwardedRef}
|
||||
uploadImage={this.onUploadImage}
|
||||
onClickLink={this.onClickLink}
|
||||
onShowToast={this.onShowToast}
|
||||
embeds={this.props.disableEmbeds ? EMPTY_ARRAY : embeds}
|
||||
tooltip={EditorTooltip}
|
||||
{...this.props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const StyledEditor = styled(RichMarkdownEditor)`
|
||||
flex-grow: ${props => (props.grow ? 1 : 0)};
|
||||
justify-content: start;
|
||||
|
||||
> div {
|
||||
transition: ${props => props.theme.backgroundTransition};
|
||||
}
|
||||
|
||||
p {
|
||||
a {
|
||||
color: ${props => props.theme.link};
|
||||
border-bottom: 1px solid ${props => lighten(0.5, props.theme.link)};
|
||||
font-weight: 500;
|
||||
|
||||
&:hover {
|
||||
border-bottom: 1px solid ${props => props.theme.link};
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const EditorTooltip = ({ children, ...props }) => (
|
||||
<Tooltip offset="0, 16" delay={150} {...props}>
|
||||
<span>{children}</span>
|
||||
</Tooltip>
|
||||
);
|
||||
|
||||
const EditorWithRouterAndTheme = withRouter(withTheme(Editor));
|
||||
|
||||
// $FlowIssue - https://github.com/facebook/flow/issues/6103
|
||||
export default React.forwardRef((props, ref) => (
|
||||
<EditorWithRouterAndTheme {...props} forwardedRef={ref} />
|
||||
));
|
||||
@@ -1,3 +0,0 @@
|
||||
// @flow
|
||||
import Editor from './Editor';
|
||||
export default Editor;
|
||||
+3
-15
@@ -1,20 +1,8 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import styled from "styled-components";
|
||||
|
||||
type Props = {
|
||||
children: React.Node,
|
||||
};
|
||||
|
||||
const Empty = (props: Props) => {
|
||||
const { children, ...rest } = props;
|
||||
return <Container {...rest}>{children}</Container>;
|
||||
};
|
||||
|
||||
const Container = styled.div`
|
||||
display: flex;
|
||||
color: ${props => props.theme.slate};
|
||||
text-align: center;
|
||||
const Empty = styled.p`
|
||||
color: ${(props) => props.theme.textTertiary};
|
||||
`;
|
||||
|
||||
export default Empty;
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { observer } from 'mobx-react';
|
||||
import { observable } from 'mobx';
|
||||
import HelpText from 'components/HelpText';
|
||||
import Button from 'components/Button';
|
||||
import CenteredContent from 'components/CenteredContent';
|
||||
import PageTitle from 'components/PageTitle';
|
||||
import { githubIssuesUrl } from '../../shared/utils/routeHelpers';
|
||||
import { observable } from "mobx";
|
||||
import { observer } from "mobx-react";
|
||||
import * as React from "react";
|
||||
import styled from "styled-components";
|
||||
import Button from "components/Button";
|
||||
import CenteredContent from "components/CenteredContent";
|
||||
import HelpText from "components/HelpText";
|
||||
import PageTitle from "components/PageTitle";
|
||||
import { githubIssuesUrl } from "../../shared/utils/routeHelpers";
|
||||
import env from "env";
|
||||
|
||||
type Props = {
|
||||
children: React.Node,
|
||||
reloadOnChunkMissing?: boolean,
|
||||
};
|
||||
|
||||
@observer
|
||||
@@ -22,13 +24,25 @@ class ErrorBoundary extends React.Component<Props> {
|
||||
this.error = error;
|
||||
console.error(error);
|
||||
|
||||
if (
|
||||
this.props.reloadOnChunkMissing &&
|
||||
error.message &&
|
||||
error.message.match(/chunk/)
|
||||
) {
|
||||
// If the editor bundle fails to load then reload the entire window. This
|
||||
// can happen if a deploy happens between the user loading the initial JS
|
||||
// bundle and the async-loaded editor JS bundle as the hash will change.
|
||||
window.location.reload(true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (window.Sentry) {
|
||||
Sentry.captureException(error);
|
||||
window.Sentry.captureException(error);
|
||||
}
|
||||
}
|
||||
|
||||
handleReload = () => {
|
||||
window.location.reload();
|
||||
window.location.reload(true);
|
||||
};
|
||||
|
||||
handleShowDetails = () => {
|
||||
@@ -41,20 +55,20 @@ class ErrorBoundary extends React.Component<Props> {
|
||||
|
||||
render() {
|
||||
if (this.error) {
|
||||
const isReported = !!window.Sentry;
|
||||
const isReported = !!window.Sentry && env.DEPLOYMENT === "hosted";
|
||||
|
||||
return (
|
||||
<CenteredContent>
|
||||
<PageTitle title="Something Unexpected Happened" />
|
||||
<h1>Something Unexpected Happened</h1>
|
||||
<HelpText>
|
||||
Sorry, an unrecoverable error occurred{isReported &&
|
||||
' – our engineers have been notified'}. Please try reloading the
|
||||
page, it may have been a temporary glitch.
|
||||
Sorry, an unrecoverable error occurred
|
||||
{isReported && " – our engineers have been notified"}. Please try
|
||||
reloading the page, it may have been a temporary glitch.
|
||||
</HelpText>
|
||||
{this.showDetails && <Pre>{this.error.toString()}</Pre>}
|
||||
<p>
|
||||
<Button onClick={this.handleReload}>Reload</Button>{' '}
|
||||
<Button onClick={this.handleReload}>Reload</Button>{" "}
|
||||
{this.showDetails ? (
|
||||
<Button onClick={this.handleReportBug} neutral>
|
||||
Report a Bug…
|
||||
@@ -73,7 +87,7 @@ class ErrorBoundary extends React.Component<Props> {
|
||||
}
|
||||
|
||||
const Pre = styled.pre`
|
||||
background: ${props => props.theme.smoke};
|
||||
background: ${(props) => props.theme.smoke};
|
||||
padding: 16px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
|
||||
+13
-13
@@ -1,10 +1,10 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import { observer, inject } from 'mobx-react';
|
||||
import styled, { withTheme } from 'styled-components';
|
||||
import Flex from 'shared/components/Flex';
|
||||
import Avatar from 'components/Avatar';
|
||||
import User from 'models/User';
|
||||
import { observer, inject } from "mobx-react";
|
||||
import * as React from "react";
|
||||
import styled, { withTheme } from "styled-components";
|
||||
import User from "models/User";
|
||||
import Avatar from "components/Avatar";
|
||||
import Flex from "components/Flex";
|
||||
|
||||
type Props = {
|
||||
users: User[],
|
||||
@@ -31,7 +31,7 @@ class Facepile extends React.Component<Props> {
|
||||
<span>+{overflow}</span>
|
||||
</More>
|
||||
)}
|
||||
{users.map(user => (
|
||||
{users.map((user) => (
|
||||
<AvatarWrapper key={user.id}>{renderAvatar(user)}</AvatarWrapper>
|
||||
))}
|
||||
</Avatars>
|
||||
@@ -56,12 +56,12 @@ const More = styled.div`
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: ${props => props.size}px;
|
||||
height: ${props => props.size}px;
|
||||
min-width: ${(props) => props.size}px;
|
||||
height: ${(props) => props.size}px;
|
||||
border-radius: 100%;
|
||||
background: ${props => props.theme.slate};
|
||||
color: ${props => props.theme.text};
|
||||
border: 2px solid ${props => props.theme.background};
|
||||
background: ${(props) => props.theme.slate};
|
||||
color: ${(props) => props.theme.text};
|
||||
border: 2px solid ${(props) => props.theme.background};
|
||||
text-align: center;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
@@ -73,4 +73,4 @@ const Avatars = styled(Flex)`
|
||||
cursor: pointer;
|
||||
`;
|
||||
|
||||
export default inject('views', 'presence')(withTheme(Facepile));
|
||||
export default inject("views", "presence")(withTheme(Facepile));
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
// @flow
|
||||
import styled from 'styled-components';
|
||||
import { fadeIn } from 'shared/styles/animations';
|
||||
import styled from "styled-components";
|
||||
import { fadeIn } from "shared/styles/animations";
|
||||
|
||||
const Fade = styled.span`
|
||||
animation: ${fadeIn} ${props => props.timing || '250ms'} ease-in-out;
|
||||
animation: ${fadeIn} ${(props) => props.timing || "250ms"} ease-in-out;
|
||||
`;
|
||||
|
||||
export default Fade;
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import * as React from "react";
|
||||
import styled from "styled-components";
|
||||
|
||||
type JustifyValues =
|
||||
| 'center'
|
||||
| 'space-around'
|
||||
| 'space-between'
|
||||
| 'flex-start'
|
||||
| 'flex-end';
|
||||
| "center"
|
||||
| "space-around"
|
||||
| "space-between"
|
||||
| "flex-start"
|
||||
| "flex-end";
|
||||
|
||||
type AlignValues =
|
||||
| 'stretch'
|
||||
| 'center'
|
||||
| 'baseline'
|
||||
| 'flex-start'
|
||||
| 'flex-end';
|
||||
| "stretch"
|
||||
| "center"
|
||||
| "baseline"
|
||||
| "flex-start"
|
||||
| "flex-end";
|
||||
|
||||
type Props = {
|
||||
column?: ?boolean,
|
||||
@@ -33,11 +33,11 @@ const Flex = (props: Props) => {
|
||||
|
||||
const Container = styled.div`
|
||||
display: flex;
|
||||
flex: ${({ auto }) => (auto ? '1 1 auto' : 'initial')};
|
||||
flex-direction: ${({ column }) => (column ? 'column' : 'row')};
|
||||
flex: ${({ auto }) => (auto ? "1 1 auto" : "initial")};
|
||||
flex-direction: ${({ column }) => (column ? "column" : "row")};
|
||||
align-items: ${({ align }) => align};
|
||||
justify-content: ${({ justify }) => justify};
|
||||
flex-shrink: ${({ shrink }) => (shrink ? 1 : 'initial')};
|
||||
flex-shrink: ${({ shrink }) => (shrink ? 1 : "initial")};
|
||||
min-height: 0;
|
||||
min-width: 0;
|
||||
`;
|
||||
@@ -0,0 +1,24 @@
|
||||
// @flow
|
||||
import * as React from "react";
|
||||
import styled from "styled-components";
|
||||
import Empty from "components/Empty";
|
||||
import Fade from "components/Fade";
|
||||
import Flex from "components/Flex";
|
||||
|
||||
export default function FullscreenLoading() {
|
||||
return (
|
||||
<Fade timing={500}>
|
||||
<Centered>
|
||||
<Empty>Loading…</Empty>
|
||||
</Centered>
|
||||
</Fade>
|
||||
);
|
||||
}
|
||||
|
||||
const Centered = styled(Flex)`
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
`;
|
||||
@@ -1,5 +1,5 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import * as React from "react";
|
||||
|
||||
type Props = {
|
||||
size?: number,
|
||||
@@ -7,7 +7,7 @@ type Props = {
|
||||
className?: string,
|
||||
};
|
||||
|
||||
function GithubLogo({ size = 34, fill = '#FFF', className }: Props) {
|
||||
function GithubLogo({ size = 34, fill = "#FFF", className }: Props) {
|
||||
return (
|
||||
<svg
|
||||
fill={fill}
|
||||
@@ -1,5 +1,5 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import * as React from "react";
|
||||
|
||||
type Props = {
|
||||
size?: number,
|
||||
@@ -7,7 +7,7 @@ type Props = {
|
||||
className?: string,
|
||||
};
|
||||
|
||||
function GoogleLogo({ size = 34, fill = '#FFF', className }: Props) {
|
||||
function GoogleLogo({ size = 34, fill = "#FFF", className }: Props) {
|
||||
return (
|
||||
<svg
|
||||
fill={fill}
|
||||
@@ -1,17 +1,17 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { observable } from 'mobx';
|
||||
import { observer, inject } from 'mobx-react';
|
||||
import { MAX_AVATAR_DISPLAY } from 'shared/constants';
|
||||
import Modal from 'components/Modal';
|
||||
import Flex from 'shared/components/Flex';
|
||||
import Facepile from 'components/Facepile';
|
||||
import GroupMembers from 'scenes/GroupMembers';
|
||||
import ListItem from 'components/List/Item';
|
||||
import Group from 'models/Group';
|
||||
import CollectionGroupMembership from 'models/CollectionGroupMembership';
|
||||
import GroupMembershipsStore from 'stores/GroupMembershipsStore';
|
||||
import { observable } from "mobx";
|
||||
import { observer, inject } from "mobx-react";
|
||||
import * as React from "react";
|
||||
import styled from "styled-components";
|
||||
import { MAX_AVATAR_DISPLAY } from "shared/constants";
|
||||
import GroupMembershipsStore from "stores/GroupMembershipsStore";
|
||||
import CollectionGroupMembership from "models/CollectionGroupMembership";
|
||||
import Group from "models/Group";
|
||||
import GroupMembers from "scenes/GroupMembers";
|
||||
import Facepile from "components/Facepile";
|
||||
import Flex from "components/Flex";
|
||||
import ListItem from "components/List/Item";
|
||||
import Modal from "components/Modal";
|
||||
|
||||
type Props = {
|
||||
group: Group,
|
||||
@@ -41,20 +41,20 @@ class GroupListItem extends React.Component<Props> {
|
||||
const membershipsInGroup = groupMemberships.inGroup(group.id);
|
||||
const users = membershipsInGroup
|
||||
.slice(0, MAX_AVATAR_DISPLAY)
|
||||
.map(gm => gm.user);
|
||||
.map((gm) => gm.user);
|
||||
|
||||
const overflow = memberCount - users.length;
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<>
|
||||
<ListItem
|
||||
title={
|
||||
<Title onClick={this.handleMembersModalOpen}>{group.name}</Title>
|
||||
}
|
||||
subtitle={
|
||||
<React.Fragment>
|
||||
{memberCount} member{memberCount === 1 ? '' : 's'}
|
||||
</React.Fragment>
|
||||
<>
|
||||
{memberCount} member{memberCount === 1 ? "" : "s"}
|
||||
</>
|
||||
}
|
||||
actions={
|
||||
<Flex align="center">
|
||||
@@ -79,7 +79,7 @@ class GroupListItem extends React.Component<Props> {
|
||||
>
|
||||
<GroupMembers group={group} onSubmit={this.handleMembersModalClose} />
|
||||
</Modal>
|
||||
</React.Fragment>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -91,4 +91,4 @@ const Title = styled.span`
|
||||
}
|
||||
`;
|
||||
|
||||
export default inject('groupMemberships')(GroupListItem);
|
||||
export default inject("groupMemberships")(GroupListItem);
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
// @flow
|
||||
import styled from 'styled-components';
|
||||
import styled from "styled-components";
|
||||
|
||||
const Heading = styled.h1`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
${(props) => (props.centered ? "text-align: center;" : "")}
|
||||
|
||||
svg {
|
||||
margin-left: -6px;
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
// @flow
|
||||
import styled from 'styled-components';
|
||||
import styled from "styled-components";
|
||||
|
||||
const HelpText = styled.p`
|
||||
margin-top: 0;
|
||||
color: ${props => props.theme.textSecondary};
|
||||
font-size: ${props => (props.small ? '13px' : 'inherit')};
|
||||
color: ${(props) => props.theme.textSecondary};
|
||||
font-size: ${(props) => (props.small ? "13px" : "inherit")};
|
||||
`;
|
||||
|
||||
export default HelpText;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import replace from 'string-replace-to-array';
|
||||
import styled from 'styled-components';
|
||||
import * as React from "react";
|
||||
import replace from "string-replace-to-array";
|
||||
import styled from "styled-components";
|
||||
|
||||
type Props = {
|
||||
highlight: ?string | RegExp,
|
||||
@@ -22,8 +22,8 @@ function Highlight({
|
||||
regex = highlight;
|
||||
} else {
|
||||
regex = new RegExp(
|
||||
(highlight || '').replace(/[-\\^$*+?.()|[\]{}]/g, '\\$&'),
|
||||
caseSensitive ? 'g' : 'gi'
|
||||
(highlight || "").replace(/[-\\^$*+?.()|[\]{}]/g, "\\$&"),
|
||||
caseSensitive ? "g" : "gi"
|
||||
);
|
||||
}
|
||||
return (
|
||||
@@ -38,7 +38,7 @@ function Highlight({
|
||||
}
|
||||
|
||||
const Mark = styled.mark`
|
||||
background: ${props => props.theme.yellow};
|
||||
background: ${(props) => props.theme.yellow};
|
||||
border-radius: 2px;
|
||||
padding: 0 4px;
|
||||
`;
|
||||
|
||||
@@ -0,0 +1,237 @@
|
||||
// @flow
|
||||
import { inject } from "mobx-react";
|
||||
import { transparentize } from "polished";
|
||||
import * as React from "react";
|
||||
import { Portal } from "react-portal";
|
||||
import styled from "styled-components";
|
||||
import { fadeAndSlideIn } from "shared/styles/animations";
|
||||
import { parseDocumentSlugFromUrl } from "shared/utils/parseDocumentSlug";
|
||||
import DocumentsStore from "stores/DocumentsStore";
|
||||
import HoverPreviewDocument from "components/HoverPreviewDocument";
|
||||
import isInternalUrl from "utils/isInternalUrl";
|
||||
|
||||
const DELAY_OPEN = 300;
|
||||
const DELAY_CLOSE = 300;
|
||||
|
||||
type Props = {
|
||||
node: HTMLAnchorElement,
|
||||
event: MouseEvent,
|
||||
documents: DocumentsStore,
|
||||
onClose: () => void,
|
||||
};
|
||||
|
||||
function HoverPreview({ node, documents, onClose, event }: Props) {
|
||||
// previews only work for internal doc links for now
|
||||
if (!isInternalUrl(node.href)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const slug = parseDocumentSlugFromUrl(node.href);
|
||||
|
||||
const [isVisible, setVisible] = React.useState(false);
|
||||
const timerClose = React.useRef();
|
||||
const timerOpen = React.useRef();
|
||||
const cardRef = React.useRef<?HTMLDivElement>();
|
||||
|
||||
const startCloseTimer = () => {
|
||||
stopOpenTimer();
|
||||
timerClose.current = setTimeout(() => {
|
||||
if (isVisible) setVisible(false);
|
||||
onClose();
|
||||
}, DELAY_CLOSE);
|
||||
};
|
||||
|
||||
const stopCloseTimer = () => {
|
||||
if (timerClose.current) {
|
||||
clearTimeout(timerClose.current);
|
||||
}
|
||||
};
|
||||
|
||||
const startOpenTimer = () => {
|
||||
timerOpen.current = setTimeout(() => setVisible(true), DELAY_OPEN);
|
||||
};
|
||||
|
||||
const stopOpenTimer = () => {
|
||||
if (timerOpen.current) {
|
||||
clearTimeout(timerOpen.current);
|
||||
}
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
if (slug) {
|
||||
documents.prefetchDocument(slug, {
|
||||
prefetch: true,
|
||||
});
|
||||
}
|
||||
|
||||
startOpenTimer();
|
||||
|
||||
if (cardRef.current) {
|
||||
cardRef.current.addEventListener("mouseenter", stopCloseTimer);
|
||||
}
|
||||
if (cardRef.current) {
|
||||
cardRef.current.addEventListener("mouseleave", startCloseTimer);
|
||||
}
|
||||
|
||||
node.addEventListener("mouseout", startCloseTimer);
|
||||
node.addEventListener("mouseover", stopCloseTimer);
|
||||
node.addEventListener("mouseover", startOpenTimer);
|
||||
|
||||
return () => {
|
||||
node.removeEventListener("mouseout", startCloseTimer);
|
||||
node.removeEventListener("mouseover", stopCloseTimer);
|
||||
node.removeEventListener("mouseover", startOpenTimer);
|
||||
|
||||
if (cardRef.current) {
|
||||
cardRef.current.removeEventListener("mouseenter", stopCloseTimer);
|
||||
}
|
||||
if (cardRef.current) {
|
||||
cardRef.current.removeEventListener("mouseleave", startCloseTimer);
|
||||
}
|
||||
|
||||
if (timerClose.current) {
|
||||
clearTimeout(timerClose.current);
|
||||
}
|
||||
};
|
||||
}, [node]);
|
||||
|
||||
const anchorBounds = node.getBoundingClientRect();
|
||||
const cardBounds = cardRef.current
|
||||
? cardRef.current.getBoundingClientRect()
|
||||
: undefined;
|
||||
const left = cardBounds
|
||||
? Math.min(anchorBounds.left, window.innerWidth - 16 - 350)
|
||||
: anchorBounds.left;
|
||||
const leftOffset = anchorBounds.left - left;
|
||||
|
||||
return (
|
||||
<Portal>
|
||||
<Position
|
||||
top={anchorBounds.bottom + window.scrollY}
|
||||
left={left}
|
||||
aria-hidden
|
||||
>
|
||||
<div ref={cardRef}>
|
||||
<HoverPreviewDocument url={node.href}>
|
||||
{(content) =>
|
||||
isVisible ? (
|
||||
<Animate>
|
||||
<Card>
|
||||
<Margin />
|
||||
<CardContent>{content}</CardContent>
|
||||
</Card>
|
||||
<Pointer offset={leftOffset + anchorBounds.width / 2} />
|
||||
</Animate>
|
||||
) : null
|
||||
}
|
||||
</HoverPreviewDocument>
|
||||
</div>
|
||||
</Position>
|
||||
</Portal>
|
||||
);
|
||||
}
|
||||
|
||||
const Animate = styled.div`
|
||||
animation: ${fadeAndSlideIn} 150ms ease;
|
||||
|
||||
@media print {
|
||||
display: none;
|
||||
}
|
||||
`;
|
||||
|
||||
// fills the gap between the card and pointer to avoid a dead zone
|
||||
const Margin = styled.div`
|
||||
position: absolute;
|
||||
top: -11px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 11px;
|
||||
`;
|
||||
|
||||
const CardContent = styled.div`
|
||||
overflow: hidden;
|
||||
max-height: 350px;
|
||||
user-select: none;
|
||||
`;
|
||||
|
||||
// &:after — gradient mask for overflow text
|
||||
const Card = styled.div`
|
||||
backdrop-filter: blur(10px);
|
||||
background: ${(props) => props.theme.background};
|
||||
border: ${(props) =>
|
||||
props.theme.menuBorder ? `1px solid ${props.theme.menuBorder}` : "none"};
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 30px 90px -20px rgba(0, 0, 0, 0.3),
|
||||
0 0 1px 1px rgba(0, 0, 0, 0.05);
|
||||
padding: 16px;
|
||||
width: 350px;
|
||||
font-size: 0.9em;
|
||||
position: relative;
|
||||
|
||||
.placeholder,
|
||||
.heading-anchor {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&:after {
|
||||
content: "";
|
||||
display: block;
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
${(props) => transparentize(1, props.theme.background)} 0%,
|
||||
${(props) => transparentize(1, props.theme.background)} 75%,
|
||||
${(props) => props.theme.background} 90%
|
||||
);
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 1.7em;
|
||||
border-bottom: 16px solid ${(props) => props.theme.background};
|
||||
border-bottom-left-radius: 4px;
|
||||
border-bottom-right-radius: 4px;
|
||||
}
|
||||
`;
|
||||
|
||||
const Position = styled.div`
|
||||
margin-top: 10px;
|
||||
position: ${({ fixed }) => (fixed ? "fixed" : "absolute")};
|
||||
display: flex;
|
||||
max-height: 75%;
|
||||
|
||||
${({ top }) => (top !== undefined ? `top: ${top}px` : "")};
|
||||
${({ left }) => (left !== undefined ? `left: ${left}px` : "")};
|
||||
`;
|
||||
|
||||
const Pointer = styled.div`
|
||||
top: -22px;
|
||||
left: ${(props) => props.offset}px;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
position: absolute;
|
||||
transform: translateX(-50%);
|
||||
|
||||
&:before,
|
||||
&:after {
|
||||
content: "";
|
||||
display: inline-block;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
&:before {
|
||||
border: 8px solid transparent;
|
||||
border-bottom-color: ${(props) =>
|
||||
props.theme.menuBorder || "rgba(0, 0, 0, 0.1)"};
|
||||
right: -1px;
|
||||
}
|
||||
|
||||
&:after {
|
||||
border: 7px solid transparent;
|
||||
border-bottom-color: ${(props) => props.theme.background};
|
||||
}
|
||||
`;
|
||||
|
||||
export default inject("documents")(HoverPreview);
|
||||
@@ -0,0 +1,53 @@
|
||||
// @flow
|
||||
import { inject, observer } from "mobx-react";
|
||||
import * as React from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import styled from "styled-components";
|
||||
import { parseDocumentSlugFromUrl } from "shared/utils/parseDocumentSlug";
|
||||
import DocumentsStore from "stores/DocumentsStore";
|
||||
import DocumentMetaWithViews from "components/DocumentMetaWithViews";
|
||||
import Editor from "components/Editor";
|
||||
|
||||
type Props = {
|
||||
url: string,
|
||||
documents: DocumentsStore,
|
||||
children: (React.Node) => React.Node,
|
||||
};
|
||||
|
||||
function HoverPreviewDocument({ url, documents, children }: Props) {
|
||||
const slug = parseDocumentSlugFromUrl(url);
|
||||
|
||||
documents.prefetchDocument(slug, {
|
||||
prefetch: true,
|
||||
});
|
||||
|
||||
const document = slug ? documents.getByUrl(slug) : undefined;
|
||||
if (!document) return null;
|
||||
|
||||
return children(
|
||||
<Content to={document.url}>
|
||||
<Heading>{document.titleWithDefault}</Heading>
|
||||
<DocumentMetaWithViews isDraft={document.isDraft} document={document} />
|
||||
|
||||
<React.Suspense fallback={<div />}>
|
||||
<Editor
|
||||
key={document.id}
|
||||
defaultValue={document.getSummary()}
|
||||
disableEmbeds
|
||||
readOnly
|
||||
/>
|
||||
</React.Suspense>
|
||||
</Content>
|
||||
);
|
||||
}
|
||||
|
||||
const Content = styled(Link)`
|
||||
cursor: pointer;
|
||||
`;
|
||||
|
||||
const Heading = styled.h2`
|
||||
margin: 0 0 0.75em;
|
||||
color: ${(props) => props.theme.text};
|
||||
`;
|
||||
|
||||
export default inject("documents")(observer(HoverPreviewDocument));
|
||||
@@ -0,0 +1,249 @@
|
||||
// @flow
|
||||
import { observable } from "mobx";
|
||||
import { observer } from "mobx-react";
|
||||
import {
|
||||
CollectionIcon,
|
||||
CoinsIcon,
|
||||
AcademicCapIcon,
|
||||
BeakerIcon,
|
||||
BuildingBlocksIcon,
|
||||
CloudIcon,
|
||||
CodeIcon,
|
||||
EditIcon,
|
||||
EyeIcon,
|
||||
LeafIcon,
|
||||
LightBulbIcon,
|
||||
MoonIcon,
|
||||
NotepadIcon,
|
||||
PadlockIcon,
|
||||
PaletteIcon,
|
||||
QuestionMarkIcon,
|
||||
SunIcon,
|
||||
VehicleIcon,
|
||||
} from "outline-icons";
|
||||
import * as React from "react";
|
||||
import styled from "styled-components";
|
||||
import { DropdownMenu } from "components/DropdownMenu";
|
||||
import Flex from "components/Flex";
|
||||
import HelpText from "components/HelpText";
|
||||
import { LabelText } from "components/Input";
|
||||
import NudeButton from "components/NudeButton";
|
||||
|
||||
const TwitterPicker = React.lazy(() =>
|
||||
import("react-color/lib/components/twitter/Twitter")
|
||||
);
|
||||
|
||||
export const icons = {
|
||||
collection: {
|
||||
component: CollectionIcon,
|
||||
keywords: "collection",
|
||||
},
|
||||
coins: {
|
||||
component: CoinsIcon,
|
||||
keywords: "coins money finance sales income revenue cash",
|
||||
},
|
||||
academicCap: {
|
||||
component: AcademicCapIcon,
|
||||
keywords: "learn teach lesson guide tutorial onboarding training",
|
||||
},
|
||||
beaker: {
|
||||
component: BeakerIcon,
|
||||
keywords: "lab research experiment test",
|
||||
},
|
||||
buildingBlocks: {
|
||||
component: BuildingBlocksIcon,
|
||||
keywords: "app blocks product prototype",
|
||||
},
|
||||
cloud: {
|
||||
component: CloudIcon,
|
||||
keywords: "cloud service aws infrastructure",
|
||||
},
|
||||
code: {
|
||||
component: CodeIcon,
|
||||
keywords: "developer api code development engineering programming",
|
||||
},
|
||||
eye: {
|
||||
component: EyeIcon,
|
||||
keywords: "eye view",
|
||||
},
|
||||
leaf: {
|
||||
component: LeafIcon,
|
||||
keywords: "leaf plant outdoors nature ecosystem climate",
|
||||
},
|
||||
lightbulb: {
|
||||
component: LightBulbIcon,
|
||||
keywords: "lightbulb idea",
|
||||
},
|
||||
moon: {
|
||||
component: MoonIcon,
|
||||
keywords: "night moon dark",
|
||||
},
|
||||
notepad: {
|
||||
component: NotepadIcon,
|
||||
keywords: "journal notepad write notes",
|
||||
},
|
||||
padlock: {
|
||||
component: PadlockIcon,
|
||||
keywords: "padlock private security authentication authorization auth",
|
||||
},
|
||||
palette: {
|
||||
component: PaletteIcon,
|
||||
keywords: "design palette art brand",
|
||||
},
|
||||
pencil: {
|
||||
component: EditIcon,
|
||||
keywords: "copy writing post blog",
|
||||
},
|
||||
question: {
|
||||
component: QuestionMarkIcon,
|
||||
keywords: "question help support faq",
|
||||
},
|
||||
sun: {
|
||||
component: SunIcon,
|
||||
keywords: "day sun weather",
|
||||
},
|
||||
vehicle: {
|
||||
component: VehicleIcon,
|
||||
keywords: "truck car travel transport",
|
||||
},
|
||||
};
|
||||
|
||||
const colors = [
|
||||
"#4E5C6E",
|
||||
"#0366d6",
|
||||
"#9E5CF7",
|
||||
"#FF825C",
|
||||
"#FF5C80",
|
||||
"#FFBE0B",
|
||||
"#42DED1",
|
||||
"#00D084",
|
||||
"#FF4DFA",
|
||||
"#2F362F",
|
||||
];
|
||||
|
||||
type Props = {
|
||||
onOpen?: () => void,
|
||||
onChange: (color: string, icon: string) => void,
|
||||
icon: string,
|
||||
color: string,
|
||||
};
|
||||
|
||||
function preventEventBubble(event) {
|
||||
event.stopPropagation();
|
||||
}
|
||||
|
||||
@observer
|
||||
class IconPicker extends React.Component<Props> {
|
||||
@observable isOpen: boolean = false;
|
||||
node: ?HTMLElement;
|
||||
|
||||
componentDidMount() {
|
||||
window.addEventListener("click", this.handleClickOutside);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
window.removeEventListener("click", this.handleClickOutside);
|
||||
}
|
||||
|
||||
handleClose = () => {
|
||||
this.isOpen = false;
|
||||
};
|
||||
|
||||
handleOpen = () => {
|
||||
this.isOpen = true;
|
||||
|
||||
if (this.props.onOpen) {
|
||||
this.props.onOpen();
|
||||
}
|
||||
};
|
||||
|
||||
handleClickOutside = (ev: SyntheticMouseEvent<>) => {
|
||||
// $FlowFixMe
|
||||
if (ev.target && this.node && this.node.contains(ev.target)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.handleClose();
|
||||
};
|
||||
|
||||
render() {
|
||||
const Component = icons[this.props.icon || "collection"].component;
|
||||
|
||||
return (
|
||||
<Wrapper ref={(ref) => (this.node = ref)}>
|
||||
<label>
|
||||
<LabelText>Icon</LabelText>
|
||||
</label>
|
||||
<DropdownMenu
|
||||
onOpen={this.handleOpen}
|
||||
label={
|
||||
<LabelButton>
|
||||
<Component role="button" color={this.props.color} size={30} />
|
||||
</LabelButton>
|
||||
}
|
||||
>
|
||||
<Icons onClick={preventEventBubble}>
|
||||
{Object.keys(icons).map((name) => {
|
||||
const Component = icons[name].component;
|
||||
return (
|
||||
<IconButton
|
||||
key={name}
|
||||
onClick={() => this.props.onChange(this.props.color, name)}
|
||||
style={{ width: 30, height: 30 }}
|
||||
>
|
||||
<Component color={this.props.color} size={30} />
|
||||
</IconButton>
|
||||
);
|
||||
})}
|
||||
</Icons>
|
||||
<Flex onClick={preventEventBubble}>
|
||||
<React.Suspense fallback={<Loading>Loading…</Loading>}>
|
||||
<ColorPicker
|
||||
color={this.props.color}
|
||||
onChange={(color) =>
|
||||
this.props.onChange(color.hex, this.props.icon)
|
||||
}
|
||||
colors={colors}
|
||||
triangle="hide"
|
||||
/>
|
||||
</React.Suspense>
|
||||
</Flex>
|
||||
</DropdownMenu>
|
||||
</Wrapper>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const Icons = styled.div`
|
||||
padding: 15px 9px 9px 15px;
|
||||
width: 276px;
|
||||
`;
|
||||
|
||||
const LabelButton = styled(NudeButton)`
|
||||
border: 1px solid ${(props) => props.theme.inputBorder};
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
`;
|
||||
|
||||
const IconButton = styled(NudeButton)`
|
||||
border-radius: 4px;
|
||||
margin: 0px 6px 6px 0px;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
`;
|
||||
|
||||
const Loading = styled(HelpText)`
|
||||
padding: 16px;
|
||||
`;
|
||||
|
||||
const ColorPicker = styled(TwitterPicker)`
|
||||
box-shadow: none !important;
|
||||
background: transparent !important;
|
||||
`;
|
||||
|
||||
const Wrapper = styled("div")`
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
`;
|
||||
|
||||
export default IconPicker;
|
||||
+27
-25
@@ -1,37 +1,37 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import { observer } from 'mobx-react';
|
||||
import { observable } from 'mobx';
|
||||
import styled from 'styled-components';
|
||||
import VisuallyHidden from 'components/VisuallyHidden';
|
||||
import Flex from 'shared/components/Flex';
|
||||
import { observable } from "mobx";
|
||||
import { observer } from "mobx-react";
|
||||
import * as React from "react";
|
||||
import styled from "styled-components";
|
||||
import Flex from "components/Flex";
|
||||
import VisuallyHidden from "components/VisuallyHidden";
|
||||
|
||||
const RealTextarea = styled.textarea`
|
||||
border: 0;
|
||||
flex: 1;
|
||||
padding: 8px 12px 8px ${props => (props.hasIcon ? '8px' : '12px')};
|
||||
padding: 8px 12px 8px ${(props) => (props.hasIcon ? "8px" : "12px")};
|
||||
outline: none;
|
||||
background: none;
|
||||
color: ${props => props.theme.text};
|
||||
color: ${(props) => props.theme.text};
|
||||
|
||||
&:disabled,
|
||||
&::placeholder {
|
||||
color: ${props => props.theme.placeholder};
|
||||
color: ${(props) => props.theme.placeholder};
|
||||
}
|
||||
`;
|
||||
|
||||
const RealInput = styled.input`
|
||||
border: 0;
|
||||
flex: 1;
|
||||
padding: 8px 12px 8px ${props => (props.hasIcon ? '8px' : '12px')};
|
||||
padding: 8px 12px 8px ${(props) => (props.hasIcon ? "8px" : "12px")};
|
||||
outline: none;
|
||||
background: none;
|
||||
color: ${props => props.theme.text};
|
||||
color: ${(props) => props.theme.text};
|
||||
height: 30px;
|
||||
|
||||
&:disabled,
|
||||
&::placeholder {
|
||||
color: ${props => props.theme.placeholder};
|
||||
color: ${(props) => props.theme.placeholder};
|
||||
}
|
||||
|
||||
&::-webkit-search-cancel-button {
|
||||
@@ -40,10 +40,10 @@ const RealInput = styled.input`
|
||||
`;
|
||||
|
||||
const Wrapper = styled.div`
|
||||
flex: ${props => (props.flex ? '1' : '0')};
|
||||
max-width: ${props => (props.short ? '350px' : '100%')};
|
||||
min-height: ${({ minHeight }) => (minHeight ? `${minHeight}px` : '0')};
|
||||
max-height: ${({ maxHeight }) => (maxHeight ? `${maxHeight}px` : 'initial')};
|
||||
flex: ${(props) => (props.flex ? "1" : "0")};
|
||||
max-width: ${(props) => (props.short ? "350px" : "100%")};
|
||||
min-height: ${({ minHeight }) => (minHeight ? `${minHeight}px` : "0")};
|
||||
max-height: ${({ maxHeight }) => (maxHeight ? `${maxHeight}px` : "initial")};
|
||||
`;
|
||||
|
||||
const IconWrapper = styled.span`
|
||||
@@ -56,16 +56,17 @@ const IconWrapper = styled.span`
|
||||
export const Outline = styled(Flex)`
|
||||
display: flex;
|
||||
flex: 1;
|
||||
margin: ${props => (props.margin !== undefined ? props.margin : '0 0 16px')};
|
||||
margin: ${(props) =>
|
||||
props.margin !== undefined ? props.margin : "0 0 16px"};
|
||||
color: inherit;
|
||||
border-width: 1px;
|
||||
border-style: solid;
|
||||
border-color: ${props =>
|
||||
border-color: ${(props) =>
|
||||
props.hasError
|
||||
? 'red'
|
||||
? "red"
|
||||
: props.focused
|
||||
? props.theme.inputBorderFocused
|
||||
: props.theme.inputBorder};
|
||||
? props.theme.inputBorderFocused
|
||||
: props.theme.inputBorder};
|
||||
border-radius: 4px;
|
||||
font-weight: normal;
|
||||
align-items: center;
|
||||
@@ -75,6 +76,7 @@ export const Outline = styled(Flex)`
|
||||
export const LabelText = styled.div`
|
||||
font-weight: 500;
|
||||
padding-bottom: 4px;
|
||||
display: inline-block;
|
||||
`;
|
||||
|
||||
export type Props = {
|
||||
@@ -118,7 +120,7 @@ class Input extends React.Component<Props> {
|
||||
|
||||
render() {
|
||||
const {
|
||||
type = 'text',
|
||||
type = "text",
|
||||
icon,
|
||||
label,
|
||||
margin,
|
||||
@@ -131,7 +133,7 @@ class Input extends React.Component<Props> {
|
||||
...rest
|
||||
} = this.props;
|
||||
|
||||
const InputComponent = type === 'textarea' ? RealTextarea : RealInput;
|
||||
const InputComponent = type === "textarea" ? RealTextarea : RealInput;
|
||||
const wrappedLabel = <LabelText>{label}</LabelText>;
|
||||
|
||||
return (
|
||||
@@ -146,10 +148,10 @@ class Input extends React.Component<Props> {
|
||||
<Outline focused={this.focused} margin={margin}>
|
||||
{icon && <IconWrapper>{icon}</IconWrapper>}
|
||||
<InputComponent
|
||||
ref={ref => (this.input = ref)}
|
||||
ref={(ref) => (this.input = ref)}
|
||||
onBlur={this.handleBlur}
|
||||
onFocus={this.handleFocus}
|
||||
type={type === 'textarea' ? undefined : type}
|
||||
type={type === "textarea" ? undefined : type}
|
||||
hasIcon={!!icon}
|
||||
{...rest}
|
||||
/>
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
// @flow
|
||||
import styled from "styled-components";
|
||||
import Input from "./Input";
|
||||
|
||||
const InputLarge = styled(Input)`
|
||||
height: 40px;
|
||||
|
||||
input {
|
||||
height: 40px;
|
||||
}
|
||||
`;
|
||||
|
||||
export default InputLarge;
|
||||
+19
-35
@@ -1,15 +1,19 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import { observable } from 'mobx';
|
||||
import { observer } from 'mobx-react';
|
||||
import styled, { withTheme } from 'styled-components';
|
||||
import { LabelText, Outline } from 'components/Input';
|
||||
import { observable } from "mobx";
|
||||
import { observer, inject } from "mobx-react";
|
||||
import * as React from "react";
|
||||
import styled, { withTheme } from "styled-components";
|
||||
import UiStore from "stores/UiStore";
|
||||
import Editor from "components/Editor";
|
||||
import HelpText from "components/HelpText";
|
||||
import { LabelText, Outline } from "components/Input";
|
||||
|
||||
type Props = {
|
||||
label: string,
|
||||
minHeight?: number,
|
||||
maxHeight?: number,
|
||||
readOnly?: boolean,
|
||||
ui: UiStore,
|
||||
};
|
||||
|
||||
@observer
|
||||
@@ -17,10 +21,6 @@ class InputRich extends React.Component<Props> {
|
||||
@observable editorComponent: React.ComponentType<any>;
|
||||
@observable focused: boolean = false;
|
||||
|
||||
componentDidMount() {
|
||||
this.loadEditor();
|
||||
}
|
||||
|
||||
handleBlur = () => {
|
||||
this.focused = false;
|
||||
};
|
||||
@@ -29,53 +29,37 @@ class InputRich extends React.Component<Props> {
|
||||
this.focused = true;
|
||||
};
|
||||
|
||||
loadEditor = async () => {
|
||||
try {
|
||||
const EditorImport = await import('./Editor');
|
||||
this.editorComponent = EditorImport.default;
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
|
||||
// If the editor bundle fails to load then reload the entire window. This
|
||||
// can happen if a deploy happens between the user loading the initial JS
|
||||
// bundle and the async-loaded editor JS bundle as the hash will change.
|
||||
window.location.reload();
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const { label, minHeight, maxHeight, ...rest } = this.props;
|
||||
const Editor = this.editorComponent;
|
||||
const { label, minHeight, maxHeight, ui, ...rest } = this.props;
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<>
|
||||
<LabelText>{label}</LabelText>
|
||||
|
||||
<StyledOutline
|
||||
maxHeight={maxHeight}
|
||||
minHeight={minHeight}
|
||||
focused={this.focused}
|
||||
>
|
||||
{Editor ? (
|
||||
<React.Suspense fallback={<HelpText>Loading editor…</HelpText>}>
|
||||
<Editor
|
||||
onBlur={this.handleBlur}
|
||||
onFocus={this.handleFocus}
|
||||
ui={ui}
|
||||
grow
|
||||
{...rest}
|
||||
/>
|
||||
) : (
|
||||
'Loading…'
|
||||
)}
|
||||
</React.Suspense>
|
||||
</StyledOutline>
|
||||
</React.Fragment>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const StyledOutline = styled(Outline)`
|
||||
display: block;
|
||||
padding: 8px 12px;
|
||||
min-height: ${({ minHeight }) => (minHeight ? `${minHeight}px` : '0')};
|
||||
max-height: ${({ maxHeight }) => (maxHeight ? `${maxHeight}px` : 'auto')};
|
||||
min-height: ${({ minHeight }) => (minHeight ? `${minHeight}px` : "0")};
|
||||
max-height: ${({ maxHeight }) => (maxHeight ? `${maxHeight}px` : "auto")};
|
||||
overflow-y: auto;
|
||||
|
||||
> * {
|
||||
@@ -83,4 +67,4 @@ const StyledOutline = styled(Outline)`
|
||||
}
|
||||
`;
|
||||
|
||||
export default withTheme(InputRich);
|
||||
export default inject("ui")(withTheme(InputRich));
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import keydown from 'react-keydown';
|
||||
import { observer } from 'mobx-react';
|
||||
import { observable } from 'mobx';
|
||||
import { withRouter, type RouterHistory } from 'react-router-dom';
|
||||
import styled, { withTheme } from 'styled-components';
|
||||
import { SearchIcon } from 'outline-icons';
|
||||
import { searchUrl } from 'utils/routeHelpers';
|
||||
import Input from './Input';
|
||||
import { observable } from "mobx";
|
||||
import { observer } from "mobx-react";
|
||||
import { SearchIcon } from "outline-icons";
|
||||
import * as React from "react";
|
||||
import keydown from "react-keydown";
|
||||
import { withRouter, type RouterHistory } from "react-router-dom";
|
||||
import styled, { withTheme } from "styled-components";
|
||||
import Input from "./Input";
|
||||
import { searchUrl } from "utils/routeHelpers";
|
||||
|
||||
type Props = {
|
||||
history: RouterHistory,
|
||||
@@ -21,7 +21,7 @@ class InputSearch extends React.Component<Props> {
|
||||
input: ?Input;
|
||||
@observable focused: boolean = false;
|
||||
|
||||
@keydown('meta+f')
|
||||
@keydown("meta+f")
|
||||
focus(ev) {
|
||||
ev.preventDefault();
|
||||
|
||||
@@ -30,7 +30,7 @@ class InputSearch extends React.Component<Props> {
|
||||
}
|
||||
}
|
||||
|
||||
handleSearchInput = ev => {
|
||||
handleSearchInput = (ev) => {
|
||||
ev.preventDefault();
|
||||
this.props.history.push(
|
||||
searchUrl(ev.target.value, this.props.collectionId)
|
||||
@@ -46,11 +46,11 @@ class InputSearch extends React.Component<Props> {
|
||||
};
|
||||
|
||||
render() {
|
||||
const { theme, placeholder = 'Search…' } = this.props;
|
||||
const { theme, placeholder = "Search…" } = this.props;
|
||||
|
||||
return (
|
||||
<InputMaxWidth
|
||||
ref={ref => (this.input = ref)}
|
||||
ref={(ref) => (this.input = ref)}
|
||||
type="search"
|
||||
placeholder={placeholder}
|
||||
onInput={this.handleSearchInput}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import { observer } from 'mobx-react';
|
||||
import { observable } from 'mobx';
|
||||
import styled from 'styled-components';
|
||||
import VisuallyHidden from 'components/VisuallyHidden';
|
||||
import { Outline, LabelText } from './Input';
|
||||
import { observable } from "mobx";
|
||||
import { observer } from "mobx-react";
|
||||
import * as React from "react";
|
||||
import styled from "styled-components";
|
||||
import VisuallyHidden from "components/VisuallyHidden";
|
||||
import { Outline, LabelText } from "./Input";
|
||||
|
||||
const Select = styled.select`
|
||||
border: 0;
|
||||
@@ -12,11 +12,11 @@ const Select = styled.select`
|
||||
padding: 8px 12px;
|
||||
outline: none;
|
||||
background: none;
|
||||
color: ${props => props.theme.text};
|
||||
color: ${(props) => props.theme.text};
|
||||
|
||||
&:disabled,
|
||||
&::placeholder {
|
||||
color: ${props => props.theme.placeholder};
|
||||
color: ${(props) => props.theme.placeholder};
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -57,7 +57,7 @@ class InputSelect extends React.Component<Props> {
|
||||
))}
|
||||
<Outline focused={this.focused} className={className}>
|
||||
<Select onBlur={this.handleBlur} onFocus={this.handleFocus} {...rest}>
|
||||
{options.map(option => (
|
||||
{options.map((option) => (
|
||||
<option value={option.value} key={option.value}>
|
||||
{option.label}
|
||||
</option>
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
// @flow
|
||||
import styled from 'styled-components';
|
||||
import styled from "styled-components";
|
||||
|
||||
const Key = styled.kbd`
|
||||
display: inline-block;
|
||||
padding: 4px 6px;
|
||||
font: 11px 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, Courier,
|
||||
font: 11px "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier,
|
||||
monospace;
|
||||
line-height: 10px;
|
||||
color: ${props => props.theme.almostBlack};
|
||||
color: ${(props) => props.theme.almostBlack};
|
||||
vertical-align: middle;
|
||||
background-color: ${props => props.theme.smokeLight};
|
||||
border: solid 1px ${props => props.theme.slateLight};
|
||||
border-bottom-color: ${props => props.theme.slate};
|
||||
background-color: ${(props) => props.theme.smokeLight};
|
||||
border: solid 1px ${(props) => props.theme.slateLight};
|
||||
border-bottom-color: ${(props) => props.theme.slate};
|
||||
border-radius: 3px;
|
||||
box-shadow: inset 0 -1px 0 ${props => props.theme.slate};
|
||||
box-shadow: inset 0 -1px 0 ${(props) => props.theme.slate};
|
||||
`;
|
||||
|
||||
export default Key;
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import { observer } from 'mobx-react';
|
||||
import Flex from 'shared/components/Flex';
|
||||
import styled from 'styled-components';
|
||||
import { observer } from "mobx-react";
|
||||
import * as React from "react";
|
||||
import styled from "styled-components";
|
||||
import Flex from "components/Flex";
|
||||
|
||||
type Props = {
|
||||
label: React.Node | string,
|
||||
@@ -21,7 +21,7 @@ export const Label = styled(Flex)`
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
text-transform: uppercase;
|
||||
color: ${props => props.theme.textTertiary};
|
||||
color: ${(props) => props.theme.textTertiary};
|
||||
letter-spacing: 0.04em;
|
||||
`;
|
||||
|
||||
|
||||
+32
-34
@@ -1,32 +1,31 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import { Switch, Route, Redirect } from 'react-router-dom';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import styled, { withTheme } from 'styled-components';
|
||||
import breakpoint from 'styled-components-breakpoint';
|
||||
import { observable } from 'mobx';
|
||||
import { observer, inject } from 'mobx-react';
|
||||
import keydown from 'react-keydown';
|
||||
import Analytics from 'components/Analytics';
|
||||
import Flex from 'shared/components/Flex';
|
||||
import { observable } from "mobx";
|
||||
import { observer, inject } from "mobx-react";
|
||||
import * as React from "react";
|
||||
import { Helmet } from "react-helmet";
|
||||
import keydown from "react-keydown";
|
||||
import { Switch, Route, Redirect } from "react-router-dom";
|
||||
import styled, { withTheme } from "styled-components";
|
||||
import breakpoint from "styled-components-breakpoint";
|
||||
import AuthStore from "stores/AuthStore";
|
||||
import DocumentsStore from "stores/DocumentsStore";
|
||||
import UiStore from "stores/UiStore";
|
||||
import ErrorSuspended from "scenes/ErrorSuspended";
|
||||
import KeyboardShortcuts from "scenes/KeyboardShortcuts";
|
||||
import Analytics from "components/Analytics";
|
||||
import DocumentHistory from "components/DocumentHistory";
|
||||
import { GlobalStyles } from "components/DropToImport";
|
||||
import Flex from "components/Flex";
|
||||
|
||||
import { LoadingIndicatorBar } from "components/LoadingIndicator";
|
||||
import Modal from "components/Modal";
|
||||
import Sidebar from "components/Sidebar";
|
||||
import SettingsSidebar from "components/Sidebar/Settings";
|
||||
import {
|
||||
homeUrl,
|
||||
searchUrl,
|
||||
matchDocumentSlug as slug,
|
||||
} from 'utils/routeHelpers';
|
||||
|
||||
import { LoadingIndicatorBar } from 'components/LoadingIndicator';
|
||||
import { GlobalStyles } from 'components/DropToImport';
|
||||
import Sidebar from 'components/Sidebar';
|
||||
import SettingsSidebar from 'components/Sidebar/Settings';
|
||||
import Modals from 'components/Modals';
|
||||
import DocumentHistory from 'components/DocumentHistory';
|
||||
import Modal from 'components/Modal';
|
||||
import KeyboardShortcuts from 'scenes/KeyboardShortcuts';
|
||||
import ErrorSuspended from 'scenes/ErrorSuspended';
|
||||
import AuthStore from 'stores/AuthStore';
|
||||
import UiStore from 'stores/UiStore';
|
||||
import DocumentsStore from 'stores/DocumentsStore';
|
||||
} from "utils/routeHelpers";
|
||||
|
||||
type Props = {
|
||||
documents: DocumentsStore,
|
||||
@@ -62,7 +61,7 @@ class Layout extends React.Component<Props> {
|
||||
window.document.body.style.background = this.props.theme.background;
|
||||
}
|
||||
|
||||
@keydown('shift+/')
|
||||
@keydown("shift+/")
|
||||
handleOpenKeyboardShortcuts() {
|
||||
if (this.props.ui.editMode) return;
|
||||
this.keyboardShortcutsOpen = true;
|
||||
@@ -72,7 +71,7 @@ class Layout extends React.Component<Props> {
|
||||
this.keyboardShortcutsOpen = false;
|
||||
};
|
||||
|
||||
@keydown(['t', '/', 'meta+k'])
|
||||
@keydown(["t", "/", "meta+k"])
|
||||
goToSearch(ev) {
|
||||
if (this.props.ui.editMode) return;
|
||||
ev.preventDefault();
|
||||
@@ -80,7 +79,7 @@ class Layout extends React.Component<Props> {
|
||||
this.redirectTo = searchUrl();
|
||||
}
|
||||
|
||||
@keydown('d')
|
||||
@keydown("d")
|
||||
goToDashboard() {
|
||||
if (this.props.ui.editMode) return;
|
||||
this.redirectTo = homeUrl();
|
||||
@@ -97,7 +96,7 @@ class Layout extends React.Component<Props> {
|
||||
return (
|
||||
<Container column auto>
|
||||
<Helmet>
|
||||
<title>Outline</title>
|
||||
<title>{team && team.name ? team.name : "Outline"}</title>
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width, initial-scale=1.0"
|
||||
@@ -127,7 +126,6 @@ class Layout extends React.Component<Props> {
|
||||
/>
|
||||
</Switch>
|
||||
</Container>
|
||||
<Modals ui={ui} />
|
||||
<Modal
|
||||
isOpen={this.keyboardShortcutsOpen}
|
||||
onRequestClose={this.handleCloseKeyboardShortcuts}
|
||||
@@ -142,8 +140,8 @@ class Layout extends React.Component<Props> {
|
||||
}
|
||||
|
||||
const Container = styled(Flex)`
|
||||
background: ${props => props.theme.background};
|
||||
transition: ${props => props.theme.backgroundTransition};
|
||||
background: ${(props) => props.theme.background};
|
||||
transition: ${(props) => props.theme.backgroundTransition};
|
||||
position: relative;
|
||||
width: 100%;
|
||||
min-height: 100%;
|
||||
@@ -157,9 +155,9 @@ const Content = styled(Flex)`
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
${breakpoint('tablet')`
|
||||
margin-left: ${props => (props.editMode ? 0 : props.theme.sidebarWidth)};
|
||||
${breakpoint("tablet")`
|
||||
margin-left: ${(props) => (props.editMode ? 0 : props.theme.sidebarWidth)};
|
||||
`};
|
||||
`;
|
||||
|
||||
export default inject('auth', 'ui', 'documents')(withTheme(Layout));
|
||||
export default inject("auth", "ui", "documents")(withTheme(Layout));
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import Flex from 'shared/components/Flex';
|
||||
import * as React from "react";
|
||||
import styled from "styled-components";
|
||||
import Flex from "components/Flex";
|
||||
|
||||
type Props = {
|
||||
image?: React.Node,
|
||||
@@ -16,7 +16,7 @@ const ListItem = ({ image, title, subtitle, actions }: Props) => {
|
||||
return (
|
||||
<Wrapper compact={compact}>
|
||||
{image && <Image>{image}</Image>}
|
||||
<Content align={compact ? 'center' : undefined} column={!compact}>
|
||||
<Content align={compact ? "center" : undefined} column={!compact}>
|
||||
<Heading>{title}</Heading>
|
||||
{subtitle && <Subtitle>{subtitle}</Subtitle>}
|
||||
</Content>
|
||||
@@ -27,9 +27,9 @@ const ListItem = ({ image, title, subtitle, actions }: Props) => {
|
||||
|
||||
const Wrapper = styled.li`
|
||||
display: flex;
|
||||
padding: ${props => (props.compact ? '8px' : '12px')} 0;
|
||||
padding: ${(props) => (props.compact ? "8px" : "12px")} 0;
|
||||
margin: 0;
|
||||
border-bottom: 1px solid ${props => props.theme.divider};
|
||||
border-bottom: 1px solid ${(props) => props.theme.divider};
|
||||
|
||||
&:last-child {
|
||||
border-bottom: 0;
|
||||
@@ -59,7 +59,7 @@ const Content = styled(Flex)`
|
||||
const Subtitle = styled.p`
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
color: ${props => props.theme.slate};
|
||||
color: ${(props) => props.theme.slate};
|
||||
`;
|
||||
|
||||
const Actions = styled.div`
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// @flow
|
||||
import styled from 'styled-components';
|
||||
import styled from "styled-components";
|
||||
|
||||
const List = styled.ol`
|
||||
margin: 0;
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import { times } from 'lodash';
|
||||
import styled from 'styled-components';
|
||||
import Mask from 'components/Mask';
|
||||
import Fade from 'components/Fade';
|
||||
import Flex from 'shared/components/Flex';
|
||||
import { times } from "lodash";
|
||||
import * as React from "react";
|
||||
import styled from "styled-components";
|
||||
import Fade from "components/Fade";
|
||||
import Flex from "components/Flex";
|
||||
import Mask from "components/Mask";
|
||||
|
||||
type Props = {
|
||||
count?: number,
|
||||
@@ -13,7 +13,7 @@ type Props = {
|
||||
const Placeholder = ({ count }: Props) => {
|
||||
return (
|
||||
<Fade>
|
||||
{times(count || 2, index => (
|
||||
{times(count || 2, (index) => (
|
||||
<Item key={index} column auto>
|
||||
<Mask />
|
||||
</Item>
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
// @flow
|
||||
import List from './List';
|
||||
import List from "./List";
|
||||
export default List;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import { inject, observer } from 'mobx-react';
|
||||
import UiStore from 'stores/UiStore';
|
||||
import { inject, observer } from "mobx-react";
|
||||
import * as React from "react";
|
||||
import UiStore from "stores/UiStore";
|
||||
|
||||
type Props = {
|
||||
ui: UiStore,
|
||||
@@ -22,4 +22,4 @@ class LoadingIndicator extends React.Component<Props> {
|
||||
}
|
||||
}
|
||||
|
||||
export default inject('ui')(LoadingIndicator);
|
||||
export default inject("ui")(LoadingIndicator);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import styled, { keyframes } from 'styled-components';
|
||||
import * as React from "react";
|
||||
import styled, { keyframes } from "styled-components";
|
||||
|
||||
const LoadingIndicatorBar = () => {
|
||||
return (
|
||||
@@ -18,7 +18,7 @@ const loadingFrame = keyframes`
|
||||
const Container = styled.div`
|
||||
position: fixed;
|
||||
top: 0;
|
||||
z-index: 9999;
|
||||
z-index: ${(props) => props.theme.depths.loadingIndicatorBar};
|
||||
|
||||
background-color: #03a9f4;
|
||||
width: 100%;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// @flow
|
||||
import LoadingIndicator from './LoadingIndicator';
|
||||
import LoadingIndicatorBar from './LoadingIndicatorBar';
|
||||
import LoadingIndicator from "./LoadingIndicator";
|
||||
import LoadingIndicatorBar from "./LoadingIndicatorBar";
|
||||
export default LoadingIndicator;
|
||||
export { LoadingIndicatorBar };
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import { times } from 'lodash';
|
||||
import styled from 'styled-components';
|
||||
import Mask from 'components/Mask';
|
||||
import Fade from 'components/Fade';
|
||||
import Flex from 'shared/components/Flex';
|
||||
import { times } from "lodash";
|
||||
import * as React from "react";
|
||||
import styled from "styled-components";
|
||||
import Fade from "components/Fade";
|
||||
import Flex from "components/Flex";
|
||||
import Mask from "components/Mask";
|
||||
|
||||
type Props = {
|
||||
count?: number,
|
||||
@@ -13,7 +13,7 @@ type Props = {
|
||||
const ListPlaceHolder = ({ count }: Props) => {
|
||||
return (
|
||||
<Fade>
|
||||
{times(count || 2, index => (
|
||||
{times(count || 2, (index) => (
|
||||
<Item key={index} column auto>
|
||||
<Mask header />
|
||||
<Mask />
|
||||
|
||||
@@ -1,21 +1,24 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import Mask from 'components/Mask';
|
||||
import Fade from 'components/Fade';
|
||||
import Flex from 'shared/components/Flex';
|
||||
import * as React from "react";
|
||||
import styled from "styled-components";
|
||||
import DelayedMount from "components/DelayedMount";
|
||||
import Fade from "components/Fade";
|
||||
import Flex from "components/Flex";
|
||||
import Mask from "components/Mask";
|
||||
|
||||
export default function LoadingPlaceholder(props: Object) {
|
||||
return (
|
||||
<Wrapper>
|
||||
<Flex column auto {...props}>
|
||||
<Mask height={34} />
|
||||
<br />
|
||||
<Mask />
|
||||
<Mask />
|
||||
<Mask />
|
||||
</Flex>
|
||||
</Wrapper>
|
||||
<DelayedMount>
|
||||
<Wrapper>
|
||||
<Flex column auto {...props}>
|
||||
<Mask height={34} />
|
||||
<br />
|
||||
<Mask />
|
||||
<Mask />
|
||||
<Mask />
|
||||
</Flex>
|
||||
</Wrapper>
|
||||
</DelayedMount>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// @flow
|
||||
import LoadingPlaceholder from './LoadingPlaceholder';
|
||||
import ListPlaceholder from './ListPlaceholder';
|
||||
import ListPlaceholder from "./ListPlaceholder";
|
||||
import LoadingPlaceholder from "./LoadingPlaceholder";
|
||||
|
||||
export default LoadingPlaceholder;
|
||||
export { ListPlaceholder };
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { pulsate } from 'shared/styles/animations';
|
||||
import { randomInteger } from 'shared/random';
|
||||
import Flex from 'shared/components/Flex';
|
||||
import * as React from "react";
|
||||
import styled from "styled-components";
|
||||
import { randomInteger } from "shared/random";
|
||||
import { pulsate } from "shared/styles/animations";
|
||||
import Flex from "components/Flex";
|
||||
|
||||
type Props = {
|
||||
header?: boolean,
|
||||
@@ -27,10 +27,11 @@ class Mask extends React.Component<Props> {
|
||||
}
|
||||
|
||||
const Redacted = styled(Flex)`
|
||||
width: ${props => (props.header ? props.width / 2 : props.width)}%;
|
||||
height: ${props => (props.height ? props.height : props.header ? 24 : 18)}px;
|
||||
width: ${(props) => (props.header ? props.width / 2 : props.width)}%;
|
||||
height: ${(props) =>
|
||||
props.height ? props.height : props.header ? 24 : 18}px;
|
||||
margin-bottom: 6px;
|
||||
background-color: ${props => props.theme.divider};
|
||||
background-color: ${(props) => props.theme.divider};
|
||||
animation: ${pulsate} 1.3s infinite;
|
||||
|
||||
&:last-child {
|
||||
|
||||
+27
-27
@@ -1,16 +1,16 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import { observer } from 'mobx-react';
|
||||
import styled, { createGlobalStyle } from 'styled-components';
|
||||
import breakpoint from 'styled-components-breakpoint';
|
||||
import ReactModal from 'react-modal';
|
||||
import { transparentize } from 'polished';
|
||||
import { CloseIcon, BackIcon } from 'outline-icons';
|
||||
import NudeButton from 'components/NudeButton';
|
||||
import { fadeAndScaleIn } from 'shared/styles/animations';
|
||||
import Flex from 'shared/components/Flex';
|
||||
import { observer } from "mobx-react";
|
||||
import { CloseIcon, BackIcon } from "outline-icons";
|
||||
import { transparentize } from "polished";
|
||||
import * as React from "react";
|
||||
import ReactModal from "react-modal";
|
||||
import styled, { createGlobalStyle } from "styled-components";
|
||||
import breakpoint from "styled-components-breakpoint";
|
||||
import { fadeAndScaleIn } from "shared/styles/animations";
|
||||
import Flex from "components/Flex";
|
||||
import NudeButton from "components/NudeButton";
|
||||
|
||||
ReactModal.setAppElement('#root');
|
||||
ReactModal.setAppElement("#root");
|
||||
|
||||
type Props = {
|
||||
children?: React.Node,
|
||||
@@ -21,16 +21,16 @@ type Props = {
|
||||
|
||||
const GlobalStyles = createGlobalStyle`
|
||||
.ReactModal__Overlay {
|
||||
background-color: ${props =>
|
||||
background-color: ${(props) =>
|
||||
transparentize(0.25, props.theme.background)} !important;
|
||||
z-index: 100;
|
||||
z-index: ${(props) => props.theme.depths.modalOverlay};
|
||||
}
|
||||
|
||||
${breakpoint('tablet')`
|
||||
${breakpoint("tablet")`
|
||||
.ReactModalPortal + .ReactModalPortal {
|
||||
.ReactModal__Overlay {
|
||||
margin-left: 12px;
|
||||
box-shadow: 0 -2px 10px ${props => props.theme.shadow};
|
||||
box-shadow: 0 -2px 10px ${(props) => props.theme.shadow};
|
||||
border-radius: 8px 0 0 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
@@ -57,14 +57,14 @@ const GlobalStyles = createGlobalStyle`
|
||||
const Modal = ({
|
||||
children,
|
||||
isOpen,
|
||||
title = 'Untitled',
|
||||
title = "Untitled",
|
||||
onRequestClose,
|
||||
...rest
|
||||
}: Props) => {
|
||||
if (!isOpen) return null;
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<>
|
||||
<GlobalStyles />
|
||||
<StyledModal
|
||||
contentLabel={title}
|
||||
@@ -72,7 +72,7 @@ const Modal = ({
|
||||
isOpen={isOpen}
|
||||
{...rest}
|
||||
>
|
||||
<Content onClick={ev => ev.stopPropagation()} column>
|
||||
<Content onClick={(ev) => ev.stopPropagation()} column>
|
||||
{title && <h1>{title}</h1>}
|
||||
|
||||
{children}
|
||||
@@ -85,7 +85,7 @@ const Modal = ({
|
||||
<CloseIcon size={32} color="currentColor" />
|
||||
</Close>
|
||||
</StyledModal>
|
||||
</React.Fragment>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -103,18 +103,18 @@ const StyledModal = styled(ReactModal)`
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
z-index: 100;
|
||||
z-index: ${(props) => props.theme.depths.modal};
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: flex-start;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
background: ${props => props.theme.background};
|
||||
transition: ${props => props.theme.backgroundTransition};
|
||||
background: ${(props) => props.theme.background};
|
||||
transition: ${(props) => props.theme.backgroundTransition};
|
||||
padding: 8vh 2rem 2rem;
|
||||
outline: none;
|
||||
|
||||
${breakpoint('tablet')`
|
||||
${breakpoint("tablet")`
|
||||
padding-top: 13vh;
|
||||
`};
|
||||
`;
|
||||
@@ -133,7 +133,7 @@ const Close = styled(NudeButton)`
|
||||
right: 0;
|
||||
margin: 12px;
|
||||
opacity: 0.75;
|
||||
color: ${props => props.theme.text};
|
||||
color: ${(props) => props.theme.text};
|
||||
width: auto;
|
||||
height: auto;
|
||||
|
||||
@@ -141,7 +141,7 @@ const Close = styled(NudeButton)`
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
${breakpoint('tablet')`
|
||||
${breakpoint("tablet")`
|
||||
display: none;
|
||||
`};
|
||||
`;
|
||||
@@ -153,7 +153,7 @@ const Back = styled(NudeButton)`
|
||||
top: 2rem;
|
||||
left: 2rem;
|
||||
opacity: 0.75;
|
||||
color: ${props => props.theme.text};
|
||||
color: ${(props) => props.theme.text};
|
||||
width: auto;
|
||||
height: auto;
|
||||
|
||||
@@ -161,7 +161,7 @@ const Back = styled(NudeButton)`
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
${breakpoint('tablet')`
|
||||
${breakpoint("tablet")`
|
||||
display: flex;
|
||||
`};
|
||||
`;
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import { observer } from 'mobx-react';
|
||||
import BaseModal from 'components/Modal';
|
||||
import UiStore from 'stores/UiStore';
|
||||
import CollectionNew from 'scenes/CollectionNew';
|
||||
import CollectionEdit from 'scenes/CollectionEdit';
|
||||
import CollectionDelete from 'scenes/CollectionDelete';
|
||||
import CollectionExport from 'scenes/CollectionExport';
|
||||
import DocumentDelete from 'scenes/DocumentDelete';
|
||||
import DocumentShare from 'scenes/DocumentShare';
|
||||
|
||||
type Props = {
|
||||
ui: UiStore,
|
||||
};
|
||||
@observer
|
||||
class Modals extends React.Component<Props> {
|
||||
handleClose = () => {
|
||||
this.props.ui.clearActiveModal();
|
||||
};
|
||||
|
||||
render() {
|
||||
const { activeModalName, activeModalProps } = this.props.ui;
|
||||
|
||||
const Modal = ({ name, children, ...rest }) => {
|
||||
return (
|
||||
<BaseModal
|
||||
isOpen={activeModalName === name}
|
||||
onRequestClose={this.handleClose}
|
||||
{...rest}
|
||||
>
|
||||
{React.cloneElement(children, activeModalProps)}
|
||||
</BaseModal>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<span>
|
||||
<Modal name="collection-new" title="Create a collection">
|
||||
<CollectionNew onSubmit={this.handleClose} />
|
||||
</Modal>
|
||||
<Modal name="collection-edit" title="Edit collection">
|
||||
<CollectionEdit onSubmit={this.handleClose} />
|
||||
</Modal>
|
||||
<Modal name="collection-delete" title="Delete collection">
|
||||
<CollectionDelete onSubmit={this.handleClose} />
|
||||
</Modal>
|
||||
<Modal name="collection-export" title="Export collection">
|
||||
<CollectionExport onSubmit={this.handleClose} />
|
||||
</Modal>
|
||||
<Modal name="document-share" title="Share document">
|
||||
<DocumentShare onSubmit={this.handleClose} />
|
||||
</Modal>
|
||||
<Modal name="document-delete" title="Delete document">
|
||||
<DocumentDelete onSubmit={this.handleClose} />
|
||||
</Modal>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Modals;
|
||||
@@ -0,0 +1,12 @@
|
||||
// @flow
|
||||
import styled from "styled-components";
|
||||
|
||||
const Notice = styled.p`
|
||||
background: ${(props) => props.theme.sidebarBackground};
|
||||
color: ${(props) => props.theme.sidebarText};
|
||||
padding: 10px 12px;
|
||||
border-radius: 4px;
|
||||
position: relative;
|
||||
`;
|
||||
|
||||
export default Notice;
|
||||
@@ -0,0 +1,24 @@
|
||||
// @flow
|
||||
import * as React from "react";
|
||||
import Notice from "components/Notice";
|
||||
|
||||
export default function AlertNotice({ children }: { children: React.Node }) {
|
||||
return (
|
||||
<Notice muted>
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
style={{ position: "relative", top: "2px" }}
|
||||
>
|
||||
<path
|
||||
d="M15.6676 11.5372L10.0155 1.14735C9.10744 -0.381434 6.89378 -0.383465 5.98447 1.14735L0.332715 11.5372C-0.595598 13.0994 0.528309 15.0776 2.34778 15.0776H13.652C15.47 15.0776 16.5959 13.101 15.6676 11.5372ZM8 13.2026C7.48319 13.2026 7.0625 12.7819 7.0625 12.2651C7.0625 11.7483 7.48319 11.3276 8 11.3276C8.51681 11.3276 8.9375 11.7483 8.9375 12.2651C8.9375 12.7819 8.51681 13.2026 8 13.2026ZM8.9375 9.45257C8.9375 9.96938 8.51681 10.3901 8 10.3901C7.48319 10.3901 7.0625 9.96938 7.0625 9.45257V4.76507C7.0625 4.24826 7.48319 3.82757 8 3.82757C8.51681 3.82757 8.9375 4.24826 8.9375 4.76507V9.45257Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>{" "}
|
||||
{children}
|
||||
</Notice>
|
||||
);
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { lighten } from 'polished';
|
||||
import { lighten } from "polished";
|
||||
import * as React from "react";
|
||||
import styled from "styled-components";
|
||||
|
||||
const Button = styled.button`
|
||||
width: 24px;
|
||||
@@ -14,13 +14,12 @@ const Button = styled.button`
|
||||
|
||||
&:focus {
|
||||
transition-duration: 0.05s;
|
||||
box-shadow: ${props => lighten(0.4, props.theme.buttonBackground)} 0px 0px
|
||||
box-shadow: ${(props) => lighten(0.4, props.theme.buttonBackground)} 0px 0px
|
||||
0px 3px;
|
||||
outline: none;
|
||||
}
|
||||
`;
|
||||
|
||||
// $FlowFixMe - need to upgrade to get forwardRef
|
||||
export default React.forwardRef((props, ref) => (
|
||||
export default React.forwardRef<any, typeof Button>((props, ref) => (
|
||||
<Button {...props} ref={ref} />
|
||||
));
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import * as React from "react";
|
||||
|
||||
type Props = {
|
||||
size?: number,
|
||||
@@ -7,7 +7,7 @@ type Props = {
|
||||
className?: string,
|
||||
};
|
||||
|
||||
function OutlineLogo({ size = 32, fill = '#333', className }: Props) {
|
||||
function OutlineLogo({ size = 32, fill = "#333", className }: Props) {
|
||||
return (
|
||||
<svg
|
||||
fill={fill}
|
||||
+24
-15
@@ -1,23 +1,32 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import { observer, inject } from "mobx-react";
|
||||
import * as React from "react";
|
||||
import { Helmet } from "react-helmet";
|
||||
import AuthStore from "stores/AuthStore";
|
||||
|
||||
type Props = {
|
||||
title: string,
|
||||
favicon?: string,
|
||||
auth: AuthStore,
|
||||
};
|
||||
|
||||
const PageTitle = ({ title, favicon }: Props) => (
|
||||
<Helmet>
|
||||
<title>{`${title} - Outline`}</title>
|
||||
<link
|
||||
rel="shortcut icon"
|
||||
type="image/png"
|
||||
href={favicon || '/favicon-32.png'}
|
||||
sizes="32x32"
|
||||
/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
</Helmet>
|
||||
);
|
||||
const PageTitle = observer(({ auth, title, favicon }: Props) => {
|
||||
const { team } = auth;
|
||||
|
||||
export default PageTitle;
|
||||
return (
|
||||
<Helmet>
|
||||
<title>
|
||||
{team && team.name ? `${title} - ${team.name}` : `${title} - Outline`}
|
||||
</title>
|
||||
<link
|
||||
rel="shortcut icon"
|
||||
type="image/png"
|
||||
href={favicon || "/favicon-32.png"}
|
||||
sizes="32x32"
|
||||
/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
</Helmet>
|
||||
);
|
||||
});
|
||||
|
||||
export default inject("auth")(PageTitle);
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import { observer } from 'mobx-react';
|
||||
import Document from 'models/Document';
|
||||
import DocumentPreview from 'components/DocumentPreview';
|
||||
import PaginatedList from 'components/PaginatedList';
|
||||
import { observer } from "mobx-react";
|
||||
import * as React from "react";
|
||||
import Document from "models/Document";
|
||||
import DocumentPreview from "components/DocumentPreview";
|
||||
import PaginatedList from "components/PaginatedList";
|
||||
|
||||
type Props = {
|
||||
documents: Document[],
|
||||
@@ -25,7 +25,7 @@ class PaginatedDocumentList extends React.Component<Props> {
|
||||
heading={heading}
|
||||
fetch={fetch}
|
||||
options={options}
|
||||
renderItem={item => (
|
||||
renderItem={(item) => (
|
||||
<DocumentPreview key={item.id} document={item} {...rest} />
|
||||
)}
|
||||
/>
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import { observable, action } from 'mobx';
|
||||
import { observer } from 'mobx-react';
|
||||
import { Waypoint } from 'react-waypoint';
|
||||
import ArrowKeyNavigation from 'boundless-arrow-key-navigation';
|
||||
|
||||
import { DEFAULT_PAGINATION_LIMIT } from 'stores/BaseStore';
|
||||
import { ListPlaceholder } from 'components/LoadingPlaceholder';
|
||||
import ArrowKeyNavigation from "boundless-arrow-key-navigation";
|
||||
import { observable, action } from "mobx";
|
||||
import { observer } from "mobx-react";
|
||||
import * as React from "react";
|
||||
import { Waypoint } from "react-waypoint";
|
||||
import { DEFAULT_PAGINATION_LIMIT } from "stores/BaseStore";
|
||||
import DelayedMount from "components/DelayedMount";
|
||||
import { ListPlaceholder } from "components/LoadingPlaceholder";
|
||||
|
||||
type Props = {
|
||||
fetch?: (options: ?Object) => Promise<void>,
|
||||
@@ -14,7 +14,7 @@ type Props = {
|
||||
heading?: React.Node,
|
||||
empty?: React.Node,
|
||||
items: any[],
|
||||
renderItem: any => React.Node,
|
||||
renderItem: (any) => React.Node,
|
||||
};
|
||||
|
||||
@observer
|
||||
@@ -96,10 +96,10 @@ class PaginatedList extends React.Component<Props> {
|
||||
(this.isLoaded || this.isInitiallyLoaded) && !showLoading && !showEmpty;
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<>
|
||||
{showEmpty && empty}
|
||||
{showList && (
|
||||
<React.Fragment>
|
||||
<>
|
||||
{heading}
|
||||
<ArrowKeyNavigation
|
||||
mode={ArrowKeyNavigation.mode.VERTICAL}
|
||||
@@ -110,10 +110,14 @@ class PaginatedList extends React.Component<Props> {
|
||||
{this.allowLoadMore && (
|
||||
<Waypoint key={this.renderCount} onEnter={this.loadMoreResults} />
|
||||
)}
|
||||
</React.Fragment>
|
||||
</>
|
||||
)}
|
||||
{showLoading && <ListPlaceholder count={5} />}
|
||||
</React.Fragment>
|
||||
{showLoading && (
|
||||
<DelayedMount>
|
||||
<ListPlaceholder count={5} />
|
||||
</DelayedMount>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import { observer } from 'mobx-react';
|
||||
import styled from 'styled-components';
|
||||
import { GoToIcon, CollectionIcon, PrivateCollectionIcon } from 'outline-icons';
|
||||
import Flex from 'shared/components/Flex';
|
||||
|
||||
import Document from 'models/Document';
|
||||
import Collection from 'models/Collection';
|
||||
import type { DocumentPath } from 'stores/CollectionsStore';
|
||||
import { observer } from "mobx-react";
|
||||
import { GoToIcon } from "outline-icons";
|
||||
import * as React from "react";
|
||||
import styled from "styled-components";
|
||||
import type { DocumentPath } from "stores/CollectionsStore";
|
||||
import Collection from "models/Collection";
|
||||
import Document from "models/Document";
|
||||
import CollectionIcon from "components/CollectionIcon";
|
||||
import Flex from "components/Flex";
|
||||
|
||||
type Props = {
|
||||
result: DocumentPath,
|
||||
document?: ?Document,
|
||||
collection: ?Collection,
|
||||
onSuccess?: () => void,
|
||||
ref?: (?React.ElementRef<'div'>) => void,
|
||||
ref?: (?React.ElementRef<"div">) => void,
|
||||
};
|
||||
|
||||
@observer
|
||||
@@ -24,7 +24,7 @@ class PathToDocument extends React.Component<Props> {
|
||||
const { document, result, onSuccess } = this.props;
|
||||
if (!document) return;
|
||||
|
||||
if (result.type === 'document') {
|
||||
if (result.type === "document") {
|
||||
await document.move(result.collectionId, result.id);
|
||||
} else {
|
||||
await document.move(result.collectionId, null);
|
||||
@@ -41,18 +41,13 @@ class PathToDocument extends React.Component<Props> {
|
||||
|
||||
return (
|
||||
<Component ref={ref} onClick={this.handleClick} href="" selectable>
|
||||
{collection &&
|
||||
(collection.private ? (
|
||||
<PrivateCollectionIcon color={collection.color} />
|
||||
) : (
|
||||
<CollectionIcon color={collection.color} />
|
||||
))}
|
||||
{collection && <CollectionIcon collection={collection} />}
|
||||
{result.path
|
||||
.map(doc => <Title key={doc.id}>{doc.title}</Title>)
|
||||
.map((doc) => <Title key={doc.id}>{doc.title}</Title>)
|
||||
.reduce((prev, curr) => [prev, <StyledGoToIcon />, curr])}
|
||||
{document && (
|
||||
<Flex>
|
||||
{' '}
|
||||
{" "}
|
||||
<StyledGoToIcon /> <Title>{document.title}</Title>
|
||||
</Flex>
|
||||
)}
|
||||
@@ -77,11 +72,11 @@ const ResultWrapper = styled.div`
|
||||
margin-left: -4px;
|
||||
user-select: none;
|
||||
|
||||
color: ${props => props.theme.text};
|
||||
color: ${(props) => props.theme.text};
|
||||
cursor: default;
|
||||
`;
|
||||
|
||||
const ResultWrapperLink = styled(ResultWrapper.withComponent('a'))`
|
||||
const ResultWrapperLink = styled(ResultWrapper.withComponent("a"))`
|
||||
margin: 0 -8px;
|
||||
padding: 8px 4px;
|
||||
border-radius: 8px;
|
||||
@@ -89,7 +84,7 @@ const ResultWrapperLink = styled(ResultWrapper.withComponent('a'))`
|
||||
&:hover,
|
||||
&:active,
|
||||
&:focus {
|
||||
background: ${props => props.theme.listItemHoverBackground};
|
||||
background: ${(props) => props.theme.listItemHoverBackground};
|
||||
outline: none;
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import BoundlessPopover from 'boundless-popover';
|
||||
import styled, { keyframes } from 'styled-components';
|
||||
import BoundlessPopover from "boundless-popover";
|
||||
import * as React from "react";
|
||||
import styled, { keyframes } from "styled-components";
|
||||
|
||||
const fadeIn = keyframes`
|
||||
from {
|
||||
@@ -22,7 +22,7 @@ const StyledPopover = styled(BoundlessPopover)`
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 9999;
|
||||
z-index: ${(props) => props.theme.depths.popover};
|
||||
|
||||
svg {
|
||||
height: 16px;
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
// @flow
|
||||
// based on: https://reacttraining.com/react-router/web/guides/scroll-restoration
|
||||
import * as React from 'react';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import type { Location } from 'react-router-dom';
|
||||
import * as React from "react";
|
||||
import { withRouter } from "react-router-dom";
|
||||
import type { Location } from "react-router-dom";
|
||||
|
||||
type Props = {
|
||||
location: Location,
|
||||
@@ -13,7 +13,7 @@ class ScrollToTop extends React.Component<Props> {
|
||||
componentDidUpdate(prevProps) {
|
||||
if (this.props.location.pathname === prevProps.location.pathname) return;
|
||||
|
||||
// exception for when entering or exiting document edit, scroll postion should not reset
|
||||
// exception for when entering or exiting document edit, scroll position should not reset
|
||||
if (
|
||||
this.props.location.pathname.match(/\/edit\/?$/) ||
|
||||
prevProps.location.pathname.match(/\/edit\/?$/)
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import { observable } from 'mobx';
|
||||
import { observer } from 'mobx-react';
|
||||
import styled from 'styled-components';
|
||||
import { observable } from "mobx";
|
||||
import { observer } from "mobx-react";
|
||||
import * as React from "react";
|
||||
import styled from "styled-components";
|
||||
|
||||
type Props = {
|
||||
shadow?: boolean,
|
||||
@@ -31,8 +31,8 @@ const Wrapper = styled.div`
|
||||
overflow-x: hidden;
|
||||
overscroll-behavior: none;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
box-shadow: ${props =>
|
||||
props.shadow ? '0 1px inset rgba(0,0,0,.1)' : 'none'};
|
||||
box-shadow: ${(props) =>
|
||||
props.shadow ? "0 1px inset rgba(0,0,0,.1)" : "none"};
|
||||
transition: all 250ms ease-in-out;
|
||||
`;
|
||||
|
||||
|
||||
@@ -1,53 +1,58 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import { observer, inject } from 'mobx-react';
|
||||
import styled from 'styled-components';
|
||||
import { observable } from "mobx";
|
||||
import { observer, inject } from "mobx-react";
|
||||
import {
|
||||
ArchiveIcon,
|
||||
HomeIcon,
|
||||
EditIcon,
|
||||
SearchIcon,
|
||||
StarredIcon,
|
||||
ShapesIcon,
|
||||
TrashIcon,
|
||||
PlusIcon,
|
||||
} from 'outline-icons';
|
||||
} from "outline-icons";
|
||||
import * as React from "react";
|
||||
import styled from "styled-components";
|
||||
|
||||
import Flex from 'shared/components/Flex';
|
||||
import Modal from 'components/Modal';
|
||||
import Invite from 'scenes/Invite';
|
||||
import AccountMenu from 'menus/AccountMenu';
|
||||
import Sidebar from './Sidebar';
|
||||
import Scrollable from 'components/Scrollable';
|
||||
import Section from './components/Section';
|
||||
import Collections from './components/Collections';
|
||||
import SidebarLink from './components/SidebarLink';
|
||||
import HeaderBlock from './components/HeaderBlock';
|
||||
import Bubble from './components/Bubble';
|
||||
|
||||
import AuthStore from 'stores/AuthStore';
|
||||
import DocumentsStore from 'stores/DocumentsStore';
|
||||
import PoliciesStore from 'stores/PoliciesStore';
|
||||
import UiStore from 'stores/UiStore';
|
||||
import { observable } from 'mobx';
|
||||
import AuthStore from "stores/AuthStore";
|
||||
import DocumentsStore from "stores/DocumentsStore";
|
||||
import PoliciesStore from "stores/PoliciesStore";
|
||||
import CollectionNew from "scenes/CollectionNew";
|
||||
import Invite from "scenes/Invite";
|
||||
import Flex from "components/Flex";
|
||||
import Modal from "components/Modal";
|
||||
import Scrollable from "components/Scrollable";
|
||||
import Sidebar from "./Sidebar";
|
||||
import Bubble from "./components/Bubble";
|
||||
import Collections from "./components/Collections";
|
||||
import HeaderBlock from "./components/HeaderBlock";
|
||||
import Section from "./components/Section";
|
||||
import SidebarLink from "./components/SidebarLink";
|
||||
import AccountMenu from "menus/AccountMenu";
|
||||
|
||||
type Props = {
|
||||
auth: AuthStore,
|
||||
documents: DocumentsStore,
|
||||
policies: PoliciesStore,
|
||||
ui: UiStore,
|
||||
};
|
||||
|
||||
@observer
|
||||
class MainSidebar extends React.Component<Props> {
|
||||
@observable inviteModalOpen: boolean = false;
|
||||
@observable inviteModalOpen = false;
|
||||
@observable createCollectionModalOpen = false;
|
||||
|
||||
componentDidMount() {
|
||||
this.props.documents.fetchDrafts();
|
||||
this.props.documents.fetchTemplates();
|
||||
}
|
||||
|
||||
handleCreateCollection = (ev: SyntheticEvent<>) => {
|
||||
handleCreateCollectionModalOpen = (ev: SyntheticEvent<>) => {
|
||||
ev.preventDefault();
|
||||
this.props.ui.setActiveModal('collection-new');
|
||||
this.createCollectionModalOpen = true;
|
||||
};
|
||||
|
||||
handleCreateCollectionModalClose = (ev: SyntheticEvent<>) => {
|
||||
this.createCollectionModalOpen = false;
|
||||
};
|
||||
|
||||
handleInviteModalOpen = (ev: SyntheticEvent<>) => {
|
||||
@@ -84,31 +89,41 @@ class MainSidebar extends React.Component<Props> {
|
||||
<Section>
|
||||
<SidebarLink
|
||||
to="/home"
|
||||
icon={<HomeIcon />}
|
||||
icon={<HomeIcon color="currentColor" />}
|
||||
exact={false}
|
||||
label="Home"
|
||||
/>
|
||||
<SidebarLink
|
||||
to={{
|
||||
pathname: '/search',
|
||||
pathname: "/search",
|
||||
state: { fromMenu: true },
|
||||
}}
|
||||
icon={<SearchIcon />}
|
||||
icon={<SearchIcon color="currentColor" />}
|
||||
label="Search"
|
||||
exact={false}
|
||||
/>
|
||||
<SidebarLink
|
||||
to="/starred"
|
||||
icon={<StarredIcon />}
|
||||
icon={<StarredIcon color="currentColor" />}
|
||||
exact={false}
|
||||
label="Starred"
|
||||
/>
|
||||
<SidebarLink
|
||||
to="/templates"
|
||||
icon={<ShapesIcon color="currentColor" />}
|
||||
exact={false}
|
||||
label="Templates"
|
||||
active={
|
||||
documents.active ? documents.active.template : undefined
|
||||
}
|
||||
/>
|
||||
<SidebarLink
|
||||
to="/drafts"
|
||||
icon={<EditIcon />}
|
||||
icon={<EditIcon color="currentColor" />}
|
||||
label={
|
||||
<Drafts align="center">
|
||||
Drafts{draftDocumentsCount > 0 && (
|
||||
Drafts
|
||||
{draftDocumentsCount > 0 && (
|
||||
<Bubble count={draftDocumentsCount} />
|
||||
)}
|
||||
</Drafts>
|
||||
@@ -116,18 +131,21 @@ class MainSidebar extends React.Component<Props> {
|
||||
active={
|
||||
documents.active
|
||||
? !documents.active.publishedAt &&
|
||||
!documents.active.isDeleted
|
||||
!documents.active.isDeleted &&
|
||||
!documents.active.isTemplate
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
</Section>
|
||||
<Section>
|
||||
<Collections onCreateCollection={this.handleCreateCollection} />
|
||||
<Collections
|
||||
onCreateCollection={this.handleCreateCollectionModalOpen}
|
||||
/>
|
||||
</Section>
|
||||
<Section>
|
||||
<SidebarLink
|
||||
to="/archive"
|
||||
icon={<ArchiveIcon />}
|
||||
icon={<ArchiveIcon color="currentColor" />}
|
||||
exact={false}
|
||||
label="Archive"
|
||||
active={
|
||||
@@ -138,7 +156,7 @@ class MainSidebar extends React.Component<Props> {
|
||||
/>
|
||||
<SidebarLink
|
||||
to="/trash"
|
||||
icon={<TrashIcon />}
|
||||
icon={<TrashIcon color="currentColor" />}
|
||||
exact={false}
|
||||
label="Trash"
|
||||
active={
|
||||
@@ -149,7 +167,7 @@ class MainSidebar extends React.Component<Props> {
|
||||
<SidebarLink
|
||||
to="/settings/people"
|
||||
onClick={this.handleInviteModalOpen}
|
||||
icon={<PlusIcon />}
|
||||
icon={<PlusIcon color="currentColor" />}
|
||||
label="Invite people…"
|
||||
/>
|
||||
)}
|
||||
@@ -163,6 +181,13 @@ class MainSidebar extends React.Component<Props> {
|
||||
>
|
||||
<Invite onSubmit={this.handleInviteModalClose} />
|
||||
</Modal>
|
||||
<Modal
|
||||
title="Create a collection"
|
||||
onRequestClose={this.handleCreateCollectionModalClose}
|
||||
isOpen={this.createCollectionModalOpen}
|
||||
>
|
||||
<CollectionNew onSubmit={this.handleCreateCollectionModalClose} />
|
||||
</Modal>
|
||||
</Sidebar>
|
||||
);
|
||||
}
|
||||
@@ -172,4 +197,4 @@ const Drafts = styled(Flex)`
|
||||
height: 24px;
|
||||
`;
|
||||
|
||||
export default inject('documents', 'policies', 'auth', 'ui')(MainSidebar);
|
||||
export default inject("documents", "policies", "auth")(MainSidebar);
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import { observer, inject } from 'mobx-react';
|
||||
import type { RouterHistory } from 'react-router-dom';
|
||||
import styled from 'styled-components';
|
||||
import { observer, inject } from "mobx-react";
|
||||
import {
|
||||
DocumentIcon,
|
||||
EmailIcon,
|
||||
@@ -15,19 +12,24 @@ import {
|
||||
TeamIcon,
|
||||
BulletedListIcon,
|
||||
ExpandedIcon,
|
||||
} from 'outline-icons';
|
||||
import ZapierIcon from './icons/Zapier';
|
||||
import SlackIcon from './icons/Slack';
|
||||
} from "outline-icons";
|
||||
import * as React from "react";
|
||||
import type { RouterHistory } from "react-router-dom";
|
||||
import styled from "styled-components";
|
||||
import AuthStore from "stores/AuthStore";
|
||||
import PoliciesStore from "stores/PoliciesStore";
|
||||
import Flex from "components/Flex";
|
||||
import Scrollable from "components/Scrollable";
|
||||
|
||||
import Flex from 'shared/components/Flex';
|
||||
import Sidebar from './Sidebar';
|
||||
import Scrollable from 'components/Scrollable';
|
||||
import Section from './components/Section';
|
||||
import Header from './components/Header';
|
||||
import SidebarLink from './components/SidebarLink';
|
||||
import HeaderBlock from './components/HeaderBlock';
|
||||
import PoliciesStore from 'stores/PoliciesStore';
|
||||
import AuthStore from 'stores/AuthStore';
|
||||
import Sidebar from "./Sidebar";
|
||||
import Header from "./components/Header";
|
||||
import HeaderBlock from "./components/HeaderBlock";
|
||||
import Section from "./components/Section";
|
||||
import SidebarLink from "./components/SidebarLink";
|
||||
import Version from "./components/Version";
|
||||
import SlackIcon from "./icons/Slack";
|
||||
import ZapierIcon from "./icons/Zapier";
|
||||
import env from "env";
|
||||
|
||||
type Props = {
|
||||
history: RouterHistory,
|
||||
@@ -38,7 +40,7 @@ type Props = {
|
||||
@observer
|
||||
class SettingsSidebar extends React.Component<Props> {
|
||||
returnToDashboard = () => {
|
||||
this.props.history.push('/');
|
||||
this.props.history.push("/home");
|
||||
};
|
||||
|
||||
render() {
|
||||
@@ -53,7 +55,7 @@ class SettingsSidebar extends React.Component<Props> {
|
||||
<HeaderBlock
|
||||
subheading={
|
||||
<ReturnToApp align="center">
|
||||
<BackIcon /> Return to App
|
||||
<BackIcon color="currentColor" /> Return to App
|
||||
</ReturnToApp>
|
||||
}
|
||||
teamName={team.name}
|
||||
@@ -67,17 +69,17 @@ class SettingsSidebar extends React.Component<Props> {
|
||||
<Header>Account</Header>
|
||||
<SidebarLink
|
||||
to="/settings"
|
||||
icon={<ProfileIcon />}
|
||||
icon={<ProfileIcon color="currentColor" />}
|
||||
label="Profile"
|
||||
/>
|
||||
<SidebarLink
|
||||
to="/settings/notifications"
|
||||
icon={<EmailIcon />}
|
||||
icon={<EmailIcon color="currentColor" />}
|
||||
label="Notifications"
|
||||
/>
|
||||
<SidebarLink
|
||||
to="/settings/tokens"
|
||||
icon={<CodeIcon />}
|
||||
icon={<CodeIcon color="currentColor" />}
|
||||
label="API Tokens"
|
||||
/>
|
||||
</Section>
|
||||
@@ -86,45 +88,45 @@ class SettingsSidebar extends React.Component<Props> {
|
||||
{can.update && (
|
||||
<SidebarLink
|
||||
to="/settings/details"
|
||||
icon={<TeamIcon />}
|
||||
icon={<TeamIcon color="currentColor" />}
|
||||
label="Details"
|
||||
/>
|
||||
)}
|
||||
{can.update && (
|
||||
<SidebarLink
|
||||
to="/settings/security"
|
||||
icon={<PadlockIcon />}
|
||||
icon={<PadlockIcon color="currentColor" />}
|
||||
label="Security"
|
||||
/>
|
||||
)}
|
||||
<SidebarLink
|
||||
to="/settings/people"
|
||||
icon={<UserIcon />}
|
||||
icon={<UserIcon color="currentColor" />}
|
||||
exact={false}
|
||||
label="People"
|
||||
/>
|
||||
<SidebarLink
|
||||
to="/settings/groups"
|
||||
icon={<GroupIcon />}
|
||||
icon={<GroupIcon color="currentColor" />}
|
||||
exact={false}
|
||||
label="Groups"
|
||||
/>
|
||||
<SidebarLink
|
||||
to="/settings/shares"
|
||||
icon={<LinkIcon />}
|
||||
icon={<LinkIcon color="currentColor" />}
|
||||
label="Share Links"
|
||||
/>
|
||||
{can.auditLog && (
|
||||
<SidebarLink
|
||||
to="/settings/events"
|
||||
icon={<BulletedListIcon />}
|
||||
icon={<BulletedListIcon color="currentColor" />}
|
||||
label="Audit Log"
|
||||
/>
|
||||
)}
|
||||
{can.export && (
|
||||
<SidebarLink
|
||||
to="/settings/export"
|
||||
icon={<DocumentIcon />}
|
||||
icon={<DocumentIcon color="currentColor" />}
|
||||
label="Export Data"
|
||||
/>
|
||||
)}
|
||||
@@ -134,16 +136,22 @@ class SettingsSidebar extends React.Component<Props> {
|
||||
<Header>Integrations</Header>
|
||||
<SidebarLink
|
||||
to="/settings/integrations/slack"
|
||||
icon={<SlackIcon />}
|
||||
icon={<SlackIcon color="currentColor" />}
|
||||
label="Slack"
|
||||
/>
|
||||
<SidebarLink
|
||||
to="/settings/integrations/zapier"
|
||||
icon={<ZapierIcon />}
|
||||
icon={<ZapierIcon color="currentColor" />}
|
||||
label="Zapier"
|
||||
/>
|
||||
</Section>
|
||||
)}
|
||||
{can.update && env.DEPLOYMENT !== "hosted" && (
|
||||
<Section>
|
||||
<Header>Installation</Header>
|
||||
<Version />
|
||||
</Section>
|
||||
)}
|
||||
</Scrollable>
|
||||
</Flex>
|
||||
</Sidebar>
|
||||
@@ -160,4 +168,4 @@ const ReturnToApp = styled(Flex)`
|
||||
height: 16px;
|
||||
`;
|
||||
|
||||
export default inject('auth', 'policies')(SettingsSidebar);
|
||||
export default inject("auth", "policies")(SettingsSidebar);
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import type { Location } from 'react-router-dom';
|
||||
import styled from 'styled-components';
|
||||
import breakpoint from 'styled-components-breakpoint';
|
||||
import { observer, inject } from 'mobx-react';
|
||||
import { CloseIcon, MenuIcon } from 'outline-icons';
|
||||
import Fade from 'components/Fade';
|
||||
import Flex from 'shared/components/Flex';
|
||||
import UiStore from 'stores/UiStore';
|
||||
import { observer, inject } from "mobx-react";
|
||||
import { CloseIcon, MenuIcon } from "outline-icons";
|
||||
import * as React from "react";
|
||||
import { withRouter } from "react-router-dom";
|
||||
import type { Location } from "react-router-dom";
|
||||
import styled from "styled-components";
|
||||
import breakpoint from "styled-components-breakpoint";
|
||||
import UiStore from "stores/UiStore";
|
||||
import Fade from "components/Fade";
|
||||
import Flex from "components/Flex";
|
||||
|
||||
let firstRender = true;
|
||||
|
||||
@@ -67,10 +67,11 @@ const Container = styled(Flex)`
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
background: ${props => props.theme.sidebarBackground};
|
||||
transition: left 100ms ease-out, ${props => props.theme.backgroundTransition};
|
||||
margin-left: ${props => (props.mobileSidebarVisible ? 0 : '-100%')};
|
||||
z-index: 1000;
|
||||
background: ${(props) => props.theme.sidebarBackground};
|
||||
transition: left 100ms ease-out,
|
||||
${(props) => props.theme.backgroundTransition};
|
||||
margin-left: ${(props) => (props.mobileSidebarVisible ? 0 : "-100%")};
|
||||
z-index: ${(props) => props.theme.depths.sidebar};
|
||||
|
||||
@media print {
|
||||
display: none;
|
||||
@@ -79,8 +80,8 @@ const Container = styled(Flex)`
|
||||
|
||||
&:before,
|
||||
&:after {
|
||||
content: '';
|
||||
background: ${props => props.theme.sidebarBackground};
|
||||
content: "";
|
||||
background: ${(props) => props.theme.sidebarBackground};
|
||||
position: absolute;
|
||||
top: -50vh;
|
||||
left: 0;
|
||||
@@ -93,9 +94,9 @@ const Container = styled(Flex)`
|
||||
bottom: -50vh;
|
||||
}
|
||||
|
||||
${breakpoint('tablet')`
|
||||
left: ${props => (props.editMode ? `-${props.theme.sidebarWidth}` : 0)};
|
||||
width: ${props => props.theme.sidebarWidth};
|
||||
${breakpoint("tablet")`
|
||||
left: ${(props) => (props.editMode ? `-${props.theme.sidebarWidth}` : 0)};
|
||||
width: ${(props) => props.theme.sidebarWidth};
|
||||
margin: 0;
|
||||
z-index: 3;
|
||||
`};
|
||||
@@ -106,14 +107,14 @@ const Toggle = styled.a`
|
||||
align-items: center;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: ${props => (props.mobileSidebarVisible ? 'auto' : 0)};
|
||||
right: ${props => (props.mobileSidebarVisible ? 0 : 'auto')};
|
||||
left: ${(props) => (props.mobileSidebarVisible ? "auto" : 0)};
|
||||
right: ${(props) => (props.mobileSidebarVisible ? 0 : "auto")};
|
||||
z-index: 1;
|
||||
margin: 12px;
|
||||
|
||||
${breakpoint('tablet')`
|
||||
${breakpoint("tablet")`
|
||||
display: none;
|
||||
`};
|
||||
`;
|
||||
|
||||
export default withRouter(inject('ui')(Sidebar));
|
||||
export default withRouter(inject("ui")(Sidebar));
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { bounceIn } from 'shared/styles/animations';
|
||||
import * as React from "react";
|
||||
import styled from "styled-components";
|
||||
import { bounceIn } from "shared/styles/animations";
|
||||
|
||||
type Props = {
|
||||
count: number,
|
||||
@@ -14,10 +14,10 @@ const Bubble = ({ count }: Props) => {
|
||||
const Count = styled.div`
|
||||
animation: ${bounceIn} 600ms;
|
||||
transform-origin: center center;
|
||||
color: ${props => props.theme.white};
|
||||
background: ${props => props.theme.slateDark};
|
||||
color: ${(props) => props.theme.white};
|
||||
background: ${(props) => props.theme.slateDark};
|
||||
display: inline-block;
|
||||
font-feature-settings: 'tnum';
|
||||
font-feature-settings: "tnum";
|
||||
font-weight: 600;
|
||||
font-size: 9px;
|
||||
white-space: nowrap;
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import { observer } from 'mobx-react';
|
||||
import { observable } from 'mobx';
|
||||
import { CollectionIcon, PrivateCollectionIcon } from 'outline-icons';
|
||||
import Collection from 'models/Collection';
|
||||
import Document from 'models/Document';
|
||||
import CollectionMenu from 'menus/CollectionMenu';
|
||||
import UiStore from 'stores/UiStore';
|
||||
import DocumentsStore from 'stores/DocumentsStore';
|
||||
import SidebarLink from './SidebarLink';
|
||||
import DocumentLink from './DocumentLink';
|
||||
import DropToImport from 'components/DropToImport';
|
||||
import Flex from 'shared/components/Flex';
|
||||
import { observable } from "mobx";
|
||||
import { observer } from "mobx-react";
|
||||
import * as React from "react";
|
||||
import DocumentsStore from "stores/DocumentsStore";
|
||||
import UiStore from "stores/UiStore";
|
||||
import Collection from "models/Collection";
|
||||
import Document from "models/Document";
|
||||
import CollectionIcon from "components/CollectionIcon";
|
||||
import DropToImport from "components/DropToImport";
|
||||
import Flex from "components/Flex";
|
||||
import DocumentLink from "./DocumentLink";
|
||||
import SidebarLink from "./SidebarLink";
|
||||
import CollectionMenu from "menus/CollectionMenu";
|
||||
|
||||
type Props = {
|
||||
collection: Collection,
|
||||
@@ -44,16 +44,7 @@ class CollectionLink extends React.Component<Props> {
|
||||
<SidebarLink
|
||||
key={collection.id}
|
||||
to={collection.url}
|
||||
icon={
|
||||
collection.private ? (
|
||||
<PrivateCollectionIcon
|
||||
expanded={expanded}
|
||||
color={collection.color}
|
||||
/>
|
||||
) : (
|
||||
<CollectionIcon expanded={expanded} color={collection.color} />
|
||||
)
|
||||
}
|
||||
icon={<CollectionIcon collection={collection} expanded={expanded} />}
|
||||
iconColor={collection.color}
|
||||
expanded={expanded}
|
||||
hideDisclosure
|
||||
@@ -70,7 +61,7 @@ class CollectionLink extends React.Component<Props> {
|
||||
}
|
||||
>
|
||||
<Flex column>
|
||||
{collection.documents.map(node => (
|
||||
{collection.documents.map((node) => (
|
||||
<DocumentLink
|
||||
key={node.id}
|
||||
node={node}
|
||||
|
||||
@@ -1,22 +1,21 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import { observer, inject } from 'mobx-react';
|
||||
import { withRouter, type RouterHistory } from 'react-router-dom';
|
||||
import keydown from 'react-keydown';
|
||||
import Flex from 'shared/components/Flex';
|
||||
import { PlusIcon } from 'outline-icons';
|
||||
import { newDocumentUrl } from 'utils/routeHelpers';
|
||||
import { observer, inject } from "mobx-react";
|
||||
import { PlusIcon } from "outline-icons";
|
||||
import * as React from "react";
|
||||
import keydown from "react-keydown";
|
||||
import { withRouter, type RouterHistory } from "react-router-dom";
|
||||
|
||||
import Header from './Header';
|
||||
import SidebarLink from './SidebarLink';
|
||||
import CollectionLink from './CollectionLink';
|
||||
import CollectionsLoading from './CollectionsLoading';
|
||||
import Fade from 'components/Fade';
|
||||
|
||||
import CollectionsStore from 'stores/CollectionsStore';
|
||||
import PoliciesStore from 'stores/PoliciesStore';
|
||||
import UiStore from 'stores/UiStore';
|
||||
import DocumentsStore from 'stores/DocumentsStore';
|
||||
import CollectionsStore from "stores/CollectionsStore";
|
||||
import DocumentsStore from "stores/DocumentsStore";
|
||||
import PoliciesStore from "stores/PoliciesStore";
|
||||
import UiStore from "stores/UiStore";
|
||||
import Fade from "components/Fade";
|
||||
import Flex from "components/Flex";
|
||||
import CollectionLink from "./CollectionLink";
|
||||
import CollectionsLoading from "./CollectionsLoading";
|
||||
import Header from "./Header";
|
||||
import SidebarLink from "./SidebarLink";
|
||||
import { newDocumentUrl } from "utils/routeHelpers";
|
||||
|
||||
type Props = {
|
||||
history: RouterHistory,
|
||||
@@ -39,7 +38,7 @@ class Collections extends React.Component<Props> {
|
||||
}
|
||||
}
|
||||
|
||||
@keydown('n')
|
||||
@keydown("n")
|
||||
goToNewDocument() {
|
||||
if (this.props.ui.editMode) return;
|
||||
|
||||
@@ -56,8 +55,8 @@ class Collections extends React.Component<Props> {
|
||||
const { collections, ui, documents } = this.props;
|
||||
|
||||
const content = (
|
||||
<React.Fragment>
|
||||
{collections.orderedData.map(collection => (
|
||||
<>
|
||||
{collections.orderedData.map((collection) => (
|
||||
<CollectionLink
|
||||
key={collection.id}
|
||||
documents={documents}
|
||||
@@ -70,11 +69,11 @@ class Collections extends React.Component<Props> {
|
||||
<SidebarLink
|
||||
to="/collections"
|
||||
onClick={this.props.onCreateCollection}
|
||||
icon={<PlusIcon />}
|
||||
icon={<PlusIcon color="currentColor" />}
|
||||
label="New collection…"
|
||||
exact
|
||||
/>
|
||||
</React.Fragment>
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -94,6 +93,9 @@ class Collections extends React.Component<Props> {
|
||||
}
|
||||
}
|
||||
|
||||
export default inject('collections', 'ui', 'documents', 'policies')(
|
||||
withRouter(Collections)
|
||||
);
|
||||
export default inject(
|
||||
"collections",
|
||||
"ui",
|
||||
"documents",
|
||||
"policies"
|
||||
)(withRouter(Collections));
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user