Compare commits

...

69 Commits

Author SHA1 Message Date
Tom Moor f2abf38fe4 perf: Remove source once compiled 2020-09-05 12:57:27 -07:00
Tom Moor f0712e22d8 perf: Improving dockerfile 2020-09-05 12:44:40 -07:00
Tom Moor e7e289d9fa end 2020-09-04 23:28:29 -07:00
Tom Moor 713187cfb4 fix 2020-09-04 15:39:36 -07:00
Tom Moor 8f41895e66 Merge develop 2020-08-31 19:40:41 -07:00
Tom Moor de8ac4acf5 fix: Configure mobx-react-lite observer batching
Removes development warning
2020-08-31 18:42:12 -07:00
Tom Moor de59147418 chore: Upgrade Sentry to 5.22.3
closes #1498
2020-08-31 18:36:30 -07:00
Tom Moor cf522cc85f fix: Regression with TOC not showing when navigating directly to document (#1500)
fix: Editing document too wide when TOC visible in read only
2020-08-31 18:31:13 -07:00
Tom Moor 8c7200fa87 chore: yarn deduplicate 2020-08-30 19:44:30 -07:00
Tom Moor f2310be173 Updated Yarn lockfile 2020-08-29 12:11:12 -07:00
Tom Moor 29f4dc9331 Bump RME
Fixes #1107 - It's now possible to use line breaks in table cells with Shift+Enter
Fixes #1253 - Selected content can now be dragged to reorder
2020-08-29 12:00:55 -07:00
Tom Moor 03b6dd62a8 fix: Missing click action to change permissions on a collection
fix: Modals no longer stacking correctly since upgrading react-portal
2020-08-25 21:00:50 -07:00
Tom Moor 7f0c608dbb Merge branch 'guilherme-diniz-feature/document-history-header' into develop 2020-08-25 20:04:02 -07:00
Tom Moor c52fbb944e Styling tidy up 2020-08-25 20:03:52 -07:00
Tom Moor e22e952606 Merge branch 'feature/document-history-header' of git://github.com/guilherme-diniz/outline into guilherme-diniz-feature/document-history-header 2020-08-25 19:44:56 -07:00
Guilherme Diniz 197cdff6c3 fix layout issues 2020-08-25 17:22:13 -03:00
Tom Moor 85d09b2351 fix: Deleting a document should correctly show who deleted (#1488) 2020-08-25 08:51:12 -07:00
Tom Moor 69611638b9 fix: Redirect to parent document when deleting a child document if possible (#1489) 2020-08-25 08:45:04 -07:00
Tom Moor e117d5f103 fix: Unable to view all possible locations when moving document (#1490)
* fix: Remove limit of displayed results on Move dialog

* fix: Filter templates from results

* Show final document location on hover/active, reduces visual noise
2020-08-25 08:44:46 -07:00
Tom Moor 03db975217 Merge branch 'feature/document-history-header' of git://github.com/guilherme-diniz/outline into guilherme-diniz-feature/document-history-header 2020-08-24 23:46:16 -07:00
Tom Moor 76279902f9 chore: Introduce AWS_S3_FORCE_PATH_STYLE option to maintain compatability with Minio et al (#1443)
- Make AWS_S3_UPLOAD_BUCKET_NAME optional
2020-08-24 23:27:10 -07:00
Tom Moor a304e91ffc Merge branch 'develop' into perf/issue-1464 2020-08-24 20:58:56 -07:00
Tom Moor 9b5573c5e2 0.46.1 2020-08-24 20:22:08 -07:00
Tom Moor ec61efa12b Remove unused scripts 2020-08-23 21:10:32 -07:00
Tom Moor b01778a39f fix: Public assets path 2020-08-23 20:44:44 -07:00
Tom Moor 5aa092853b fix: Production file paths 2020-08-23 20:35:59 -07:00
Tom Moor 1fa3db4bdc fix: package.json must be copied, not linked for production build 2020-08-23 20:29:17 -07:00
Tom Moor 6a9f74e6cc fix: Update procfile location 2020-08-23 19:21:43 -07:00
Tom Moor e8719340d1 refactor: Remove babel/register for instant production server startup 2020-08-23 19:10:16 -07:00
Tom Moor 70838918c3 fix: Collections not collapsing 2020-08-23 12:51:35 -07:00
Tom Moor ec38f5d79c refactor: Remove old react lifecycle methods (#1480)
* refactor: Remove deprecated APIs

* bump mobx-react for hooks support

* inject -> useStores
https://mobx-react.js.org/recipes-migration\#hooks-to-the-rescue

* chore: React rules of hooks lint
2020-08-23 11:51:56 -07:00
Jonathan Killian 179176c312 fix: Update package.json build script to use yarn instead of npm. (#1476)
* fix: Update package.json build script to yarn.

Update package.json build script to use yarn instead of npm to maintain consistency with the rest of scripts. I was running into an issue with the Dockerfile when using nvm with yarn and this fixed the issue.
2020-08-22 19:56:52 -07:00
Tom Moor c446a91f7d fix: Restore Postgres SSL support on Heroku
https://github.com/brianc/node-postgres/issues/2009
2020-08-22 08:27:42 -07:00
Guilherme Diniz 05f48f054b feat: Add Header to Document History Sidebar 2020-08-21 20:58:57 -03:00
Tom Moor ec55299c8b fix: Improve websocket reliability (#1470)
* check connection on page visibility change

* fix: SocketPresence account for socket changing
2020-08-20 20:37:54 -07:00
Tom Moor 26c574ab58 chore: Upgrade pg and sequelize to support node 14+ (#1462)
* Upgrade pg and sequelize to support node 14+

When Node 14 came out the app was incompatible, we should always have a maximum version defined here until the server code has been tested to prove compatibility

Co-authored-by: Lance Whatley <whatl3y@gmail.com>
2020-08-20 20:19:44 -07:00
Tom Moor 6dd6768f07 feat: Allow moving templates between collections (#1454)
- Allow template move in document policy
- fix: Ensure that document is not added to collection structure in documentMover command
- fix: Moving a template should now show nested documents as options
- fix: Hitting 'm' should not allow moving a draft
- fix: Styling of seperators on move screen
2020-08-20 19:46:29 -07:00
Tom Moor 0555fd2caa pref: JS bundling improvements (#1461)
* perf: Split only initial vendors
2020-08-17 22:09:12 -07:00
Tom Moor d885252fb0 fix: Mobile style fixes and improvements (#1459)
* fixes #1457 – check for matchMedia function before using it

* fixes: Depth issues
closes #1458

* fixes: Long breadcrumbs cause horizontal overflow

* fix: Improve tabs and overflow on mobile
2020-08-17 00:08:22 -07:00
Tom Moor df9b0bcf91 fix: Websocket reconnect when navigating from settings -> home 2020-08-14 17:47:12 -07:00
Tom Moor 31910f1628 Remove auto reconnect, increase reconnectionDelayMax 2020-08-14 17:25:55 -07:00
Tom Moor 14cb3a36c1 perf: Reduce initial bundle size / async bundle loading (#1456)
* feat: Move to React.lazy

* perf: Remove duplicate babel/runtime

* fix: Run yarn-deduplicate

* Further attempts to remove rich-markdown-editor from initial chunk

* perf: Lazy loading of authenticated routes

* perf: Move color picker to async loading
fix: Display placeholder when loading rich editor

* fix: Cache bust on auto reload
2020-08-14 17:23:58 -07:00
Tom Moor d3350c20b6 perf: Attempt websocket connection before polling 2020-08-14 13:37:11 -07:00
Tom Moor 174acfac32 fix: Unnecessary shares.info request when loading public share (#1453)
closes #1450
2020-08-13 16:48:03 -07:00
Tom Moor 9ef4e2b437 Update LICENSE 2020-08-12 20:04:48 -07:00
Tom Moor 8088da8cf3 0.46.0 2020-08-12 20:03:57 -07:00
Tom Moor 221ee48429 fix: Don't mangle class names in production 2020-08-12 19:28:15 -07:00
Tom Moor ffe8c046ef fix: Bump RME – Improves floating toolbar behavior 2020-08-12 17:01:27 -07:00
Tom Moor dbe8a10702 fix: Login to X should be centered when team name wraps to newline 2020-08-12 14:05:32 -07:00
Tom Moor 11f7e3a060 chore: Bundle Stats / Webpack v4 (#1448)
* chore: Experiment with bundle size monitoring service

* chore: Ensure build runs on CI, move lint and flow before test

* chore: Upgrade Webpack v3 -> v4

* chore: Add webpack-cli
Remove unused dep
Move deps to dev

* Move babel deps to production

* Move babel deps to production
2020-08-12 13:16:10 -07:00
Tom Moor 0f41a04e49 refactor: Remove centralized Modal management (#1444)
* refactor: Finally remove centralized Modals component

* chore: Cleanup related unused methods in UiStore
2020-08-12 10:49:15 -07:00
Tom Moor d055021ad4 chore: Remove all usage of collection.type (#1445)
* chore: Remove all usage of collection.type

* migration: Remove type column
2020-08-12 10:49:02 -07:00
Tom Moor 810dc5a061 feat: Clicking the last updated time should open document history sidebar
Ref #1285
2020-08-11 21:01:03 -07:00
ktfth 7abe375b3e refactor: Removed unusued index on the onSearchLink (#1420)
Co-authored-by: Tom Moor <tom.moor@gmail.com>
2020-08-11 19:59:11 -07:00
Tom Moor 63371d8f5b flow 2020-08-11 18:59:57 -07:00
Tom Moor 6e61df0729 fix: Improved loading jank fix, new DelayedMount component 2020-08-10 21:30:12 -07:00
Tom Moor 5ddc4000d0 fixes: Strange scroll behavior on long collection descriptions
closes #1391
2020-08-10 16:23:55 -07:00
Tom Moor 48b61559cc fixes: JS error when attempting to show toast messages from collection description editor 2020-08-10 16:04:23 -07:00
Tom Moor 0cac5cfe51 fix: Prevent reload loop with error on editor load 2020-08-10 15:52:45 -07:00
Tom Moor e9ce80a3aa fixes: Case where websocket will not reconnect
closes #1384
2020-08-09 23:25:27 -07:00
Tom Moor 07d488c826 fix: GitHub Gist embed reliability, closes #1400 2020-08-09 21:53:57 -07:00
Tom Moor e2bd03494d chore: Update syntax, improve more typing (#1439)
* chore: <React.Fragment> to <>

* flow types
2020-08-09 09:48:04 -07:00
Tom Moor ead55442e0 flow: Restore lesser flowtype for styled-components
The current flow-typed def requires an insane amount of manual typing that just doesnt
make any sense. Restoring the old definition for now:
https://github.com/flow-typed/flow-typed/issues/3766
2020-08-08 23:41:02 -07:00
Tom Moor 449dc55aaa chore: Upgrade Babel, Jest, Eslint (#1437)
* chore: Upgrade Prettier 1.8 -> 2.0

* chore: Upgrade Babel 6 -> 7

* chore: Upgrade eslint plugins

* chore: Add eslint import/order rules

* chore: Update flow-typed deps
2020-08-08 22:53:59 -07:00
Tom Moor e312b264a6 chore: Upgrade Prettier 1.8 -> 2.0 (#1436) 2020-08-08 18:53:11 -07:00
Tom Moor 68dcb4de5f fix: Catch expected error when shares.info returns 404 2020-08-08 17:55:21 -07:00
Tom Moor d2b9a5c03f fix: Various React errors in console 2020-08-08 17:51:40 -07:00
Tom Moor 1b023fb6d7 fix: Remove flash of loading state for document lists 2020-08-08 17:39:30 -07:00
Tom Moor afe4553a7e chore: Resolve 2 open security alerts 2020-08-08 17:35:42 -07:00
399 changed files with 20673 additions and 16270 deletions
+25 -14
View File
@@ -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"
]
}
+8 -5
View File
@@ -3,7 +3,7 @@ jobs:
build:
working_directory: ~/outline
docker:
- image: circleci/node:12
- image: circleci/node:14
- image: circleci/redis:latest
- image: circleci/postgres:9.6.5-alpine-ram
environment:
@@ -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
+19
View File
@@ -0,0 +1,19 @@
__mocks__
.git
.vscode
.github
.circleci
.DS_Store
.env*
.eslint*
.flowconfig
.log
Makefile
Procfile
app.json
build
docker-compose.yml
fakes3
flow-typed
node_modules
setupJest.js
+1
View File
@@ -45,6 +45,7 @@ AWS_REGION=xx-xxxx-x
AWS_S3_UPLOAD_BUCKET_URL=http://s3:4569
AWS_S3_UPLOAD_BUCKET_NAME=bucket_name_here
AWS_S3_UPLOAD_MAX_SIZE=26214400
AWS_S3_FORCE_PATH_STYLE=true
# uploaded s3 objects permission level, default is private
# set to "public-read" to allow public access
AWS_S3_ACL=private
+46 -3
View File
@@ -4,7 +4,8 @@
"react-app",
"plugin:import/errors",
"plugin:import/warnings",
"plugin:flowtype/recommended"
"plugin:flowtype/recommended",
"plugin:react-hooks/recommended"
],
"plugins": [
"prettier",
@@ -14,6 +15,46 @@
"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",
@@ -41,8 +82,7 @@
"react": {
"createClass": "createReactClass",
"pragma": "React",
"version": "detect",
"flowVersion": "0.86"
"version": "detect"
},
"import/resolver": {
"node": {
@@ -58,5 +98,8 @@
},
"env": {
"jest": true
},
"globals": {
"EDITOR_VERSION": true
}
}
+1
View File
@@ -1,4 +1,5 @@
dist
build
node_modules/*
server/scripts
.env
+14 -7
View File
@@ -1,17 +1,24 @@
FROM node:12-alpine
FROM node:14-alpine
ENV PATH /opt/outline/node_modules/.bin:/opt/node_modules/.bin:$PATH
ENV NODE_PATH /opt/outline/node_modules:/opt/node_modules
ENV APP_PATH /opt/outline
RUN mkdir -p $APP_PATH
WORKDIR $APP_PATH
COPY . $APP_PATH
RUN yarn install --pure-lockfile
RUN yarn build
RUN cp -r /opt/outline/node_modules /opt/node_modules
COPY package.json ./
COPY yarn.lock ./
RUN yarn --pure-lockfile
COPY . .
RUN yarn build && \
yarn --production --ignore-scripts --prefer-offline && \
rm -rf server && \
rm -rf shared && \
rm -rf app
ENV NODE_ENV production
CMD yarn start
EXPOSE 3000
+2 -2
View File
@@ -3,7 +3,7 @@ Business Source License 1.1
Parameters
Licensor: General Outline, Inc.
Licensed Work: Outline 0.44.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-07-03
Change Date: 2023-08-12
Change License: Apache License, Version 2.0
+1 -1
View File
@@ -1 +1 @@
web: node index.js
web: node ./build/server/index.js
+5
View File
@@ -92,6 +92,11 @@
"value": "26214400",
"required": false
},
"AWS_S3_FORCE_PATH_STYLE": {
"description": "Use path-style URL's for connecting to S3 instead of subdomain. This is useful for S3-compatible storage.",
"value": "true",
"required": false
},
"AWS_REGION": {
"value": "us-east-1",
"description": "Region in which the above S3 bucket exists",
+4 -4
View File
@@ -12,7 +12,7 @@ export const Action = styled(Flex)`
flex-shrink: 0;
a {
color: ${props => props.theme.text};
color: ${(props) => props.theme.text};
height: 24px;
}
@@ -26,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)`
@@ -35,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);
+1 -1
View File
@@ -14,7 +14,7 @@ export default class Analytics extends React.Component<Props> {
// standard Google Analytics script
window.ga =
window.ga ||
function() {
function () {
// $FlowIssue
(ga.q = ga.q || []).push(arguments);
};
+2 -2
View File
@@ -1,10 +1,10 @@
// @flow
import * as React from "react";
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 { isCustomSubdomain } from "shared/utils/domains";
import env from "env";
type Props = {
+7 -7
View File
@@ -1,8 +1,8 @@
// @flow
import * as React from "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";
import placeholder from "./placeholder.png";
type Props = {
@@ -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;
`;
+11 -9
View File
@@ -1,14 +1,14 @@
// @flow
import * as React from "react";
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 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 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)"}
<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;
`;
+5 -4
View File
@@ -11,7 +11,8 @@ type Props = {
function Branding({ href = env.URL }: Props) {
return (
<Link href={href}>
<OutlineLogo size={16} />&nbsp;Outline
<OutlineLogo size={16} />
&nbsp;Outline
</Link>
);
}
@@ -25,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};
}
`;
+29 -26
View File
@@ -1,9 +1,5 @@
// @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 {
PadlockIcon,
GoToIcon,
@@ -11,13 +7,17 @@ import {
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 Document from "models/Document";
import CollectionIcon from "components/CollectionIcon";
import Flex from "components/Flex";
import BreadcrumbMenu from "./BreadcrumbMenu";
import CollectionIcon from "components/CollectionIcon";
import { collectionUrl } from "utils/routeHelpers";
type Props = {
document: Document,
@@ -33,20 +33,20 @@ const Breadcrumb = observer(({ document, collections, onlyText }: Props) => {
if (onlyText === true) {
return (
<React.Fragment>
<>
{collection.private && (
<React.Fragment>
<>
<SmallPadlockIcon color="currentColor" size={16} />{" "}
</React.Fragment>
</>
)}
{collection.name}
{path.map(n => (
{path.map((n) => (
<React.Fragment key={n.id}>
<SmallSlash />
{n.title}
</React.Fragment>
))}
</React.Fragment>
</>
);
}
@@ -59,39 +59,42 @@ const Breadcrumb = observer(({ document, collections, onlyText }: Props) => {
return (
<Wrapper justify="flex-start" align="center">
{isTemplate && (
<React.Fragment>
<>
<CollectionName to="/templates">
<ShapesIcon color="currentColor" />&nbsp;
<ShapesIcon color="currentColor" />
&nbsp;
<span>Templates</span>
</CollectionName>
<Slash />
</React.Fragment>
</>
)}
{isDraft && (
<React.Fragment>
<>
<CollectionName to="/drafts">
<EditIcon color="currentColor" />&nbsp;
<EditIcon color="currentColor" />
&nbsp;
<span>Drafts</span>
</CollectionName>
<Slash />
</React.Fragment>
</>
)}
<CollectionName to={collectionUrl(collection.id)}>
<CollectionIcon collection={collection} expanded />&nbsp;
<CollectionIcon collection={collection} expanded />
&nbsp;
<span>{collection.name}</span>
</CollectionName>
{isNestedDocument && (
<React.Fragment>
<>
<Slash /> <BreadcrumbMenu label={<Overflow />} path={menuPath} />
</React.Fragment>
</>
)}
{lastPath && (
<React.Fragment>
<>
<Slash />{" "}
<Crumb to={lastPath.url} title={lastPath.title}>
{lastPath.title}
</Crumb>
</React.Fragment>
</>
)}
</Wrapper>
);
@@ -119,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)`
@@ -134,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;
@@ -149,7 +152,7 @@ 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;
+1 -1
View File
@@ -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>
+20 -20
View File
@@ -1,17 +1,17 @@
// @flow
import { ExpandedIcon } from "outline-icons";
import { darken, lighten } from "polished";
import * as React from "react";
import styled from "styled-components";
import { darken, lighten } from "polished";
import { ExpandedIcon } from "outline-icons";
const RealButton = styled.button`
display: ${props => (props.fullwidth ? "block" : "inline-block")};
width: ${props => (props.fullwidth ? "100%" : "auto")};
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;
@@ -24,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 {
@@ -33,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;
}
@@ -46,10 +46,10 @@ 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};
@@ -80,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};
@@ -103,20 +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 = {
@@ -155,6 +155,6 @@ function Button({
);
}
export default React.forwardRef((props, ref) => (
export default React.forwardRef<Props, typeof Button>((props, ref) => (
<Button {...props} innerRef={ref} />
));
+1
View File
@@ -9,6 +9,7 @@ type Props = {
const Container = styled.div`
width: 100%;
max-width: 100vw;
padding: 60px 20px;
${breakpoint("tablet")`
+6 -5
View File
@@ -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>
</>
);
}
+12 -12
View File
@@ -1,14 +1,14 @@
// @flow
import * as React from "react";
import { observer, inject } from "mobx-react";
import { sortBy, keyBy } from "lodash";
import { observer, inject } from "mobx-react";
import * as React from "react";
import { MAX_AVATAR_DISPLAY } from "shared/constants";
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";
import Document from "models/Document";
import ViewsStore from "stores/ViewsStore";
import DocumentPresenceStore from "stores/DocumentPresenceStore";
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];
+3 -3
View File
@@ -1,11 +1,11 @@
// @flow
import * as React from "react";
import { inject, observer } from "mobx-react";
import { getLuminance } from "polished";
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";
import UiStore from "stores/UiStore";
type Props = {
collection: Collection,
+1 -1
View File
@@ -1,6 +1,6 @@
// @flow
import * as React from "react";
import copy from "copy-to-clipboard";
import * as React from "react";
type Props = {
text: string,
+24
View File
@@ -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);
};
}, [delay]);
if (!isShowing) {
return null;
}
return children;
}
@@ -1,20 +1,23 @@
// @flow
import * as React from "react";
import { observable, action } from "mobx";
import { observer, inject } from "mobx-react";
import { type RouterHistory, type Match } from "react-router-dom";
import styled from "styled-components";
import { Waypoint } from "react-waypoint";
import ArrowKeyNavigation from "boundless-arrow-key-navigation";
import { action, observable } from "mobx";
import { inject, observer } from "mobx-react";
import { CloseIcon } from "outline-icons";
import * as React from "react";
import { type Match, Redirect, type RouterHistory } from "react-router-dom";
import { Waypoint } from "react-waypoint";
import styled from "styled-components";
import breakpoint from "styled-components-breakpoint";
import { DEFAULT_PAGINATION_LIMIT } from "stores/BaseStore";
import DocumentsStore from "stores/DocumentsStore";
import RevisionsStore from "stores/RevisionsStore";
import Button from "components/Button";
import Flex from "components/Flex";
import { ListPlaceholder } from "components/LoadingPlaceholder";
import Revision from "./components/Revision";
import { documentHistoryUrl } from "utils/routeHelpers";
import { documentHistoryUrl, documentUrl } from "utils/routeHelpers";
type Props = {
match: Match,
@@ -29,6 +32,7 @@ class DocumentHistory extends React.Component<Props> {
@observable isFetching: boolean = false;
@observable offset: number = 0;
@observable allowLoadMore: boolean = true;
@observable redirectTo: ?string;
async componentDidMount() {
await this.loadMoreResults();
@@ -86,15 +90,34 @@ class DocumentHistory extends React.Component<Props> {
return this.props.revisions.getDocumentRevisions(document.id);
}
onCloseHistory = () => {
const document = this.props.documents.getByUrl(
this.props.match.params.documentSlug
);
this.redirectTo = documentUrl(document);
};
render() {
const document = this.props.documents.getByUrl(
this.props.match.params.documentSlug
);
const showLoading = (!this.isLoaded && this.isFetching) || !document;
if (this.redirectTo) return <Redirect to={this.redirectTo} push />;
return (
<Sidebar>
<Wrapper column>
<Header>
<Title>History</Title>
<Button
icon={<CloseIcon />}
onClick={this.onCloseHistory}
borderOnHover
neutral
/>
</Header>
{showLoading ? (
<Loading>
<ListPlaceholder count={5} />
@@ -133,17 +156,43 @@ 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};
display: none;
background: ${(props) => props.theme.background};
min-width: ${(props) => props.theme.sidebarWidth};
border-left: 1px solid ${(props) => props.theme.divider};
z-index: 1;
${breakpoint("tablet")`
display: flex;
`};
`;
const Title = styled(Flex)`
font-size: 16px;
font-weight: 600;
text-align: center;
align-items: center;
justify-content: flex-start;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
width: 0;
flex-grow: 1;
`;
const Header = styled(Flex)`
align-items: center;
position: relative;
padding: 12px;
border-bottom: 1px solid ${(props) => props.theme.divider};
color: ${(props) => props.theme.text};
`;
export default inject("documents", "revisions")(DocumentHistory);
@@ -1,16 +1,16 @@
// @flow
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 format from "date-fns/format";
import { MoreIcon } from "outline-icons";
import Flex from "components/Flex";
import Time from "components/Time";
import Avatar from "components/Avatar";
import RevisionMenu from "menus/RevisionMenu";
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";
@@ -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;
+2 -2
View File
@@ -1,8 +1,8 @@
// @flow
import ArrowKeyNavigation from "boundless-arrow-key-navigation";
import * as React from "react";
import Document from "models/Document";
import DocumentPreview from "components/DocumentPreview";
import ArrowKeyNavigation from "boundless-arrow-key-navigation";
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>
+111 -27
View File
@@ -1,39 +1,123 @@
// @flow
import { inject, observer } from "mobx-react";
import * as React from "react";
import { Link } from "react-router-dom";
import styled from "styled-components";
import { inject } from "mobx-react";
import ViewsStore from "stores/ViewsStore";
import AuthStore from "stores/AuthStore";
import CollectionsStore from "stores/CollectionsStore";
import Document from "models/Document";
import PublishingInfo from "components/PublishingInfo";
import Breadcrumb from "components/Breadcrumb";
import Flex from "components/Flex";
import Time from "components/Time";
type Props = {|
views: ViewsStore,
const Container = styled(Flex)`
color: ${(props) => props.theme.textTertiary};
font-size: 13px;
white-space: nowrap;
overflow: hidden;
`;
const Modified = styled.span`
color: ${(props) =>
props.highlight ? props.theme.text : props.theme.textTertiary};
font-weight: ${(props) => (props.highlight ? "600" : "400")};
`;
type Props = {
collections: CollectionsStore,
auth: AuthStore,
showCollection?: boolean,
showPublished?: boolean,
document: Document,
isDraft: boolean,
|};
children: React.Node,
to?: string,
};
function DocumentMeta({ views, isDraft, document }: Props) {
const totalViews = views.countForDocument(document.id);
function DocumentMeta({
auth,
collections,
showPublished,
showCollection,
document,
children,
to,
...rest
}: Props) {
const {
modifiedSinceViewed,
updatedAt,
updatedBy,
createdAt,
publishedAt,
archivedAt,
deletedAt,
isDraft,
} = document;
// Prevent meta information from displaying if updatedBy is not available.
// Currently the situation where this is true is rendering share links.
if (!updatedBy) {
return null;
}
let content;
if (deletedAt) {
content = (
<span>
deleted <Time dateTime={deletedAt} /> ago
</span>
);
} else if (archivedAt) {
content = (
<span>
archived <Time dateTime={archivedAt} /> ago
</span>
);
} else if (createdAt === updatedAt) {
content = (
<span>
created <Time dateTime={updatedAt} /> ago
</span>
);
} else if (publishedAt && (publishedAt === updatedAt || showPublished)) {
content = (
<span>
published <Time dateTime={publishedAt} /> ago
</span>
);
} else if (isDraft) {
content = (
<span>
saved <Time dateTime={updatedAt} /> ago
</span>
);
} else {
content = (
<Modified highlight={modifiedSinceViewed}>
updated <Time dateTime={updatedAt} /> ago
</Modified>
);
}
const collection = collections.get(document.collectionId);
const updatedByMe = auth.user && auth.user.id === updatedBy.id;
return (
<Meta document={document}>
{totalViews && !isDraft ? (
<React.Fragment>
&nbsp;&middot; Viewed{" "}
{totalViews === 1 ? "once" : `${totalViews} times`}
</React.Fragment>
) : null}
</Meta>
<Container align="center" {...rest}>
{updatedByMe ? "You" : updatedBy.name}&nbsp;
{to ? <Link to={to}>{content}</Link> : content}
{showCollection && collection && (
<span>
&nbsp;in&nbsp;
<strong>
<Breadcrumb document={document} onlyText />
</strong>
</span>
)}
{children}
</Container>
);
}
const Meta = styled(PublishingInfo)`
margin: -12px 0 2em 0;
font-size: 14px;
@media print {
display: none;
}
`;
export default inject("views")(DocumentMeta);
export default inject("collections", "auth")(observer(DocumentMeta));
+48
View File
@@ -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 ? (
<>
&nbsp;&middot; 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,20 +1,21 @@
// @flow
import * as React from "react";
import { observer } from "mobx-react";
import { Link } from "react-router-dom";
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 Flex from "components/Flex";
import Document from "models/Document";
import Badge from "components/Badge";
import Button from "components/Button";
import Tooltip from "components/Tooltip";
import DocumentMeta from "components/DocumentMeta";
import Flex from "components/Flex";
import Highlight from "components/Highlight";
import PublishingInfo from "components/PublishingInfo";
import Tooltip from "components/Tooltip";
import DocumentMenu from "menus/DocumentMenu";
import Document from "models/Document";
import { newDocumentUrl } from "utils/routeHelpers";
type Props = {
history: RouterHistory,
document: Document,
highlight?: ?string,
context?: ?string,
@@ -47,6 +48,19 @@ class DocumentPreview extends React.Component<Props> {
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,
@@ -57,7 +71,6 @@ class DocumentPreview extends React.Component<Props> {
showTemplate,
highlight,
context,
...rest
} = this.props;
const queryIsInTitle =
@@ -70,7 +83,6 @@ class DocumentPreview extends React.Component<Props> {
pathname: document.url,
state: { title: document.titleWithDefault },
}}
{...rest}
>
<Heading>
<Title text={document.titleWithDefault} highlight={highlight} />
@@ -85,33 +97,27 @@ class DocumentPreview extends React.Component<Props> {
)}
</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>}
{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
as={Link}
to={newDocumentUrl(document.collectionId, {
templateId: document.id,
})}
onClick={this.handleNewFromTemplate}
icon={<PlusIcon />}
neutral
>
New doc
</Button>
)}&nbsp;
)}
&nbsp;
<DocumentMenu document={document} showPin={showPin} />
</SecondaryActions>
</Heading>
@@ -123,7 +129,7 @@ class DocumentPreview extends React.Component<Props> {
processResult={this.replaceResultMarks}
/>
)}
<PublishingInfo
<DocumentMeta
document={document}
showCollection={showCollection}
showPublished={showPublished}
@@ -137,7 +143,7 @@ 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 {
@@ -163,6 +169,7 @@ const DocumentLink = styled(Link)`
border-radius: 8px;
max-height: 50vh;
min-width: 100%;
max-width: calc(100vw - 40px);
overflow: hidden;
position: relative;
@@ -173,7 +180,7 @@ const DocumentLink = styled(Link)`
&:hover,
&:active,
&:focus {
background: ${props => props.theme.listItemHoverBackground};
background: ${(props) => props.theme.listItemHoverBackground};
outline: none;
${SecondaryActions} {
@@ -198,7 +205,7 @@ const Heading = styled.h3`
margin-bottom: 0.25em;
overflow: hidden;
white-space: nowrap;
color: ${props => props.theme.text};
color: ${(props) => props.theme.text};
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen,
Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
`;
@@ -216,10 +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;
`;
export default DocumentPreview;
export default withRouter(DocumentPreview);
+7 -7
View File
@@ -1,14 +1,14 @@
// @flow
import * as React from "react";
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 invariant from "invariant";
import importFile from "utils/importFile";
import Dropzone from "react-dropzone";
import DocumentsStore from "stores/DocumentsStore";
import LoadingIndicator from "components/LoadingIndicator";
import importFile from "utils/importFile";
const EMPTY_OBJECT = {};
let importingLock = false;
@@ -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;
}
`;
+14 -14
View File
@@ -1,14 +1,14 @@
// @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 { rgba } from "polished";
import * as React from "react";
import { PortalWithState } from "react-portal";
import styled from "styled-components";
import Flex from "components/Flex";
import { fadeAndScaleIn } from "shared/styles/animations";
import Flex from "components/Flex";
import NudeButton from "components/NudeButton";
let previousClosePortal;
@@ -161,7 +161,7 @@ class DropdownMenu extends React.Component<Props> {
closeOnEsc
>
{({ closePortal, openPortal, isOpen, portal }) => (
<React.Fragment>
<>
<Label
onMouseMove={hover ? this.clearCloseTimeout : undefined}
onMouseOut={
@@ -204,7 +204,7 @@ class DropdownMenu extends React.Component<Props> {
onClick={
typeof children === "function"
? undefined
: ev => {
: (ev) => {
ev.stopPropagation();
closePortal();
}
@@ -220,7 +220,7 @@ class DropdownMenu extends React.Component<Props> {
</Menu>
</Position>
)}
</React.Fragment>
</>
)}
</PortalWithState>
</div>
@@ -232,7 +232,7 @@ const Label = styled(Flex).attrs({
justify: "center",
align: "center",
})`
z-index: 1000;
z-index: ${(props) => props.theme.depths.menu};
cursor: pointer;
`;
@@ -244,25 +244,25 @@ const Position = styled.div`
${({ top }) => (top !== undefined ? `top: ${top}px` : "")};
${({ bottom }) => (bottom !== undefined ? `bottom: ${bottom}px` : "")};
max-height: 75%;
z-index: 1000;
transform: ${props =>
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;
transform-origin: ${(props) => (props.left !== undefined ? "25%" : "75%")} 0;
backdrop-filter: blur(10px);
background: ${props => rgba(props.theme.menuBackground, 0.8)};
border: ${props =>
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 {
@@ -278,7 +278,7 @@ export const Header = styled.h3`
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
color: ${props => props.theme.sidebarText};
color: ${(props) => props.theme.sidebarText};
letter-spacing: 0.04em;
margin: 1em 12px 0.5em;
`;
@@ -1,6 +1,6 @@
// @flow
import * as React from "react";
import { CheckmarkIcon } from "outline-icons";
import * as React from "react";
import styled from "styled-components";
type Props = {
@@ -26,11 +26,12 @@ const DropdownMenuItem = ({
{...rest}
>
{selected !== undefined && (
<React.Fragment>
<>
<CheckmarkIcon
color={selected === false ? "transparent" : undefined}
/>&nbsp;
</React.Fragment>
/>
&nbsp;
</>
)}
{children}
</MenuItem>
@@ -44,7 +45,7 @@ const MenuItem = styled.a`
width: 100%;
min-height: 32px;
color: ${props =>
color: ${(props) =>
props.disabled ? props.theme.textTertiary : props.theme.textSecondary};
justify-content: left;
align-items: center;
@@ -57,10 +58,10 @@ const MenuItem = styled.a`
}
svg {
opacity: ${props => (props.disabled ? ".5" : 1)};
opacity: ${(props) => (props.disabled ? ".5" : 1)};
}
${props =>
${(props) =>
props.disabled
? "pointer-events: none;"
: `
+34 -30
View File
@@ -1,34 +1,34 @@
// @flow
import { lighten } from "polished";
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 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,
id?: string,
defaultValue?: string,
readOnly?: boolean,
grow?: boolean,
disableEmbeds?: boolean,
history: RouterHistory,
forwardedRef: React.Ref<RichMarkdownEditor>,
ui: UiStore,
ui?: UiStore,
};
@observer
class Editor extends React.Component<Props> {
@observable redirectTo: ?string;
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;
@@ -62,30 +62,34 @@ class Editor extends React.Component<Props> {
};
onShowToast = (message: string) => {
this.props.ui.showToast(message);
if (this.props.ui) {
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}
/>
<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)};
flex-grow: ${(props) => (props.grow ? 1 : 0)};
justify-content: start;
> div {
transition: ${props => props.theme.backgroundTransition};
transition: ${(props) => props.theme.backgroundTransition};
}
.notice-block.tip,
@@ -95,13 +99,13 @@ const StyledEditor = styled(RichMarkdownEditor)`
p {
a {
color: ${props => props.theme.text};
border-bottom: 1px solid ${props => lighten(0.5, props.theme.text)};
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};
border-bottom: 1px solid ${(props) => props.theme.text};
text-decoration: none;
}
}
@@ -120,6 +124,6 @@ const Span = styled.span`
const EditorWithRouterAndTheme = withRouter(withTheme(Editor));
export default React.forwardRef((props, ref) => (
export default React.forwardRef<Props, typeof Editor>((props, ref) => (
<EditorWithRouterAndTheme {...props} forwardedRef={ref} />
));
+1 -1
View File
@@ -2,7 +2,7 @@
import styled from "styled-components";
const Empty = styled.p`
color: ${props => props.theme.textTertiary};
color: ${(props) => props.theme.textTertiary};
`;
export default Empty;
+23 -9
View File
@@ -1,16 +1,18 @@
// @flow
import { observable } from "mobx";
import { observer } from "mobx-react";
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 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) {
window.Sentry.captureException(error);
}
}
handleReload = () => {
window.location.reload();
window.location.reload(true);
};
handleShowDetails = () => {
@@ -41,16 +55,16 @@ 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>
@@ -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;
+9 -9
View File
@@ -1,10 +1,10 @@
// @flow
import * as React from "react";
import { observer, inject } from "mobx-react";
import * as React from "react";
import styled, { withTheme } from "styled-components";
import Flex from "components/Flex";
import Avatar from "components/Avatar";
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;
+1 -1
View File
@@ -3,7 +3,7 @@ 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;
+24
View File
@@ -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;
`;
+14 -14
View File
@@ -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 * as React from "react";
import styled from "styled-components";
import { MAX_AVATAR_DISPLAY } from "shared/constants";
import Modal from "components/Modal";
import Flex from "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 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>
</>
}
actions={
<Flex align="center">
@@ -79,7 +79,7 @@ class GroupListItem extends React.Component<Props> {
>
<GroupMembers group={group} onSubmit={this.handleMembersModalClose} />
</Modal>
</React.Fragment>
</>
);
}
}
+1
View File
@@ -4,6 +4,7 @@ import styled from "styled-components";
const Heading = styled.h1`
display: flex;
align-items: center;
${(props) => (props.centered ? "text-align: center;" : "")}
svg {
margin-left: -6px;
+2 -2
View File
@@ -3,8 +3,8 @@ 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 -1
View File
@@ -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;
`;
+58 -53
View File
@@ -1,14 +1,14 @@
// @flow
import * as React from "react";
import { inject } from "mobx-react";
import { transparentize } from "polished";
import HoverPreviewDocument from "components/HoverPreviewDocument";
import styled from "styled-components";
import * as React from "react";
import { Portal } from "react-portal";
import styled from "styled-components";
import { fadeAndSlideIn } from "shared/styles/animations";
import isInternalUrl from "utils/isInternalUrl";
import { parseDocumentSlugFromUrl } from "shared/utils/parseDocumentIds";
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;
@@ -20,18 +20,13 @@ type Props = {
onClose: () => void,
};
function HoverPreview({ node, documents, onClose, event }: Props) {
// previews only work for internal doc links for now
if (!isInternalUrl(node.href)) {
return null;
}
function HoverPreviewInternal({ node, documents, onClose, event }: Props) {
const slug = parseDocumentSlugFromUrl(node.href);
const [isVisible, setVisible] = React.useState(false);
const timerClose = React.useRef();
const timerOpen = React.useRef();
const cardRef = React.useRef();
const cardRef = React.useRef<?HTMLDivElement>();
const startCloseTimer = () => {
stopOpenTimer();
@@ -57,42 +52,43 @@ function HoverPreview({ node, documents, onClose, event }: Props) {
}
};
React.useEffect(
() => {
if (slug) {
documents.prefetchDocument(slug, {
prefetch: true,
});
}
React.useEffect(() => {
if (slug) {
documents.prefetchDocument(slug, {
prefetch: true,
});
}
startOpenTimer();
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.addEventListener("mouseenter", stopCloseTimer);
cardRef.current.addEventListener("mouseleave", startCloseTimer);
cardRef.current.removeEventListener("mouseenter", stopCloseTimer);
}
if (cardRef.current) {
cardRef.current.removeEventListener("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);
cardRef.current.removeEventListener("mouseleave", startCloseTimer);
}
if (timerClose.current) {
clearTimeout(timerClose.current);
}
};
},
[node]
);
if (timerClose.current) {
clearTimeout(timerClose.current);
}
};
}, [node]);
const anchorBounds = node.getBoundingClientRect();
const cardBounds = cardRef.current
@@ -112,7 +108,7 @@ function HoverPreview({ node, documents, onClose, event }: Props) {
>
<div ref={cardRef}>
<HoverPreviewDocument url={node.href}>
{content =>
{(content) =>
isVisible ? (
<Animate>
<Card>
@@ -130,6 +126,15 @@ function HoverPreview({ node, documents, onClose, event }: Props) {
);
}
function HoverPreview({ node, ...rest }: Props) {
// previews only work for internal doc links for now
if (!isInternalUrl(node.href)) {
return null;
}
return <HoverPreviewInternal {...rest} node={node} />;
}
const Animate = styled.div`
animation: ${fadeAndSlideIn} 150ms ease;
@@ -156,8 +161,8 @@ const CardContent = styled.div`
// &:after — gradient mask for overflow text
const Card = styled.div`
backdrop-filter: blur(10px);
background: ${props => props.theme.background};
border: ${props =>
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),
@@ -179,15 +184,15 @@ const Card = styled.div`
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%
${(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: 16px solid ${(props) => props.theme.background};
border-bottom-left-radius: 4px;
border-bottom-right-radius: 4px;
}
@@ -205,7 +210,7 @@ const Position = styled.div`
const Pointer = styled.div`
top: -22px;
left: ${props => props.offset}px;
left: ${(props) => props.offset}px;
width: 22px;
height: 22px;
position: absolute;
@@ -222,14 +227,14 @@ const Pointer = styled.div`
&:before {
border: 8px solid transparent;
border-bottom-color: ${props =>
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};
border-bottom-color: ${(props) => props.theme.background};
}
`;
+15 -13
View File
@@ -1,17 +1,17 @@
// @flow
import * as React from "react";
import { inject, observer } from "mobx-react";
import * as React from "react";
import { Link } from "react-router-dom";
import Editor from "components/Editor";
import styled from "styled-components";
import { parseDocumentSlugFromUrl } from "shared/utils/parseDocumentIds";
import { parseDocumentSlugFromUrl } from "shared/utils/parseDocumentSlug";
import DocumentsStore from "stores/DocumentsStore";
import DocumentMeta from "components/DocumentMeta";
import DocumentMetaWithViews from "components/DocumentMetaWithViews";
import Editor from "components/Editor";
type Props = {
url: string,
documents: DocumentsStore,
children: React.Node => React.Node,
children: (React.Node) => React.Node,
};
function HoverPreviewDocument({ url, documents, children }: Props) {
@@ -27,14 +27,16 @@ function HoverPreviewDocument({ url, documents, children }: Props) {
return children(
<Content to={document.url}>
<Heading>{document.titleWithDefault}</Heading>
<DocumentMeta isDraft={document.isDraft} document={document} />
<DocumentMetaWithViews isDraft={document.isDraft} document={document} />
<Editor
key={document.id}
defaultValue={document.getSummary()}
disableEmbeds
readOnly
/>
<React.Suspense fallback={<div />}>
<Editor
key={document.id}
defaultValue={document.getSummary()}
disableEmbeds
readOnly
/>
</React.Suspense>
</Content>
);
}
@@ -45,7 +47,7 @@ const Content = styled(Link)`
const Heading = styled.h2`
margin: 0 0 0.75em;
color: ${props => props.theme.text};
color: ${(props) => props.theme.text};
`;
export default inject("documents")(observer(HoverPreviewDocument));
+25 -15
View File
@@ -1,8 +1,6 @@
// @flow
import * as React from "react";
import { observable } from "mobx";
import { observer } from "mobx-react";
import { TwitterPicker } from "react-color";
import {
CollectionIcon,
CoinsIcon,
@@ -23,11 +21,17 @@ import {
SunIcon,
VehicleIcon,
} from "outline-icons";
import * as React from "react";
import styled from "styled-components";
import { LabelText } from "components/Input";
import { DropdownMenu } from "components/DropdownMenu";
import NudeButton from "components/NudeButton";
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: {
@@ -166,7 +170,7 @@ class IconPicker extends React.Component<Props> {
const Component = icons[this.props.icon || "collection"].component;
return (
<Wrapper ref={ref => (this.node = ref)}>
<Wrapper ref={(ref) => (this.node = ref)}>
<label>
<LabelText>Icon</LabelText>
</label>
@@ -179,7 +183,7 @@ class IconPicker extends React.Component<Props> {
}
>
<Icons onClick={preventEventBubble}>
{Object.keys(icons).map(name => {
{Object.keys(icons).map((name) => {
const Component = icons[name].component;
return (
<IconButton
@@ -193,14 +197,16 @@ class IconPicker extends React.Component<Props> {
})}
</Icons>
<Flex onClick={preventEventBubble}>
<ColorPicker
color={this.props.color}
onChange={color =>
this.props.onChange(color.hex, this.props.icon)
}
colors={colors}
triangle="hide"
/>
<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>
@@ -214,7 +220,7 @@ const Icons = styled.div`
`;
const LabelButton = styled(NudeButton)`
border: 1px solid ${props => props.theme.inputBorder};
border: 1px solid ${(props) => props.theme.inputBorder};
width: 32px;
height: 32px;
`;
@@ -226,6 +232,10 @@ const IconButton = styled(NudeButton)`
height: 30px;
`;
const Loading = styled(HelpText)`
padding: 16px;
`;
const ColorPicker = styled(TwitterPicker)`
box-shadow: none !important;
background: transparent !important;
+17 -16
View File
@@ -1,37 +1,37 @@
// @flow
import * as React from "react";
import { observer } from "mobx-react";
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 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,8 +40,8 @@ const RealInput = styled.input`
`;
const Wrapper = styled.div`
flex: ${props => (props.flex ? "1" : "0")};
max-width: ${props => (props.short ? "350px" : "100%")};
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")};
`;
@@ -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"
: props.focused
? props.theme.inputBorderFocused
: props.theme.inputBorder};
? props.theme.inputBorderFocused
: props.theme.inputBorder};
border-radius: 4px;
font-weight: normal;
align-items: center;
@@ -147,7 +148,7 @@ 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}
+14 -30
View File
@@ -1,8 +1,11 @@
// @flow
import * as React from "react";
import { observable } from "mobx";
import { observer } from "mobx-react";
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 = {
@@ -10,6 +13,7 @@ type Props = {
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,50 +29,34 @@ 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")};
@@ -83,4 +67,4 @@ const StyledOutline = styled(Outline)`
}
`;
export default withTheme(InputRich);
export default inject("ui")(withTheme(InputRich));
+6 -6
View File
@@ -1,13 +1,13 @@
// @flow
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 { 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 { searchUrl } from "utils/routeHelpers";
type Props = {
history: RouterHistory,
@@ -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)
@@ -50,7 +50,7 @@ class InputSearch extends React.Component<Props> {
return (
<InputMaxWidth
ref={ref => (this.input = ref)}
ref={(ref) => (this.input = ref)}
type="search"
placeholder={placeholder}
onInput={this.handleSearchInput}
+5 -5
View File
@@ -1,7 +1,7 @@
// @flow
import * as React from "react";
import { observer } from "mobx-react";
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";
@@ -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>
+5 -5
View File
@@ -7,13 +7,13 @@ const Key = styled.kbd`
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;
+3 -3
View File
@@ -1,8 +1,8 @@
// @flow
import * as React from "react";
import { observer } from "mobx-react";
import Flex from "components/Flex";
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;
`;
+26 -27
View File
@@ -1,33 +1,32 @@
// @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 * 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";
type Props = {
documents: DocumentsStore,
children?: ?React.Node,
@@ -45,21 +44,22 @@ class Layout extends React.Component<Props> {
@observable redirectTo: ?string;
@observable keyboardShortcutsOpen: boolean = false;
componentWillMount() {
this.updateBackground();
constructor(props) {
super();
this.updateBackground(props);
}
componentDidUpdate() {
this.updateBackground();
this.updateBackground(this.props);
if (this.redirectTo) {
this.redirectTo = undefined;
}
}
updateBackground() {
updateBackground(props) {
// ensure the wider page color always matches the theme
window.document.body.style.background = this.props.theme.background;
window.document.body.style.background = props.theme.background;
}
@keydown("shift+/")
@@ -127,7 +127,6 @@ class Layout extends React.Component<Props> {
/>
</Switch>
</Container>
<Modals ui={ui} />
<Modal
isOpen={this.keyboardShortcutsOpen}
onRequestClose={this.handleCloseKeyboardShortcuts}
@@ -142,8 +141,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%;
@@ -158,7 +157,7 @@ const Content = styled(Flex)`
}
${breakpoint("tablet")`
margin-left: ${props => (props.editMode ? 0 : props.theme.sidebarWidth)};
margin-left: ${(props) => (props.editMode ? 0 : props.theme.sidebarWidth)};
`};
`;
+3 -3
View File
@@ -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`
+3 -3
View File
@@ -1,10 +1,10 @@
// @flow
import * as React from "react";
import { times } from "lodash";
import * as React from "react";
import styled from "styled-components";
import Mask from "components/Mask";
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,6 +1,6 @@
// @flow
import * as React from "react";
import { inject, observer } from "mobx-react";
import * as React from "react";
import UiStore from "stores/UiStore";
type Props = {
@@ -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,10 +1,10 @@
// @flow
import * as React from "react";
import { times } from "lodash";
import * as React from "react";
import styled from "styled-components";
import Mask from "components/Mask";
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 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 -1
View File
@@ -1,6 +1,6 @@
// @flow
import LoadingPlaceholder from "./LoadingPlaceholder";
import ListPlaceholder from "./ListPlaceholder";
import LoadingPlaceholder from "./LoadingPlaceholder";
export default LoadingPlaceholder;
export { ListPlaceholder };
+7 -5
View File
@@ -1,8 +1,8 @@
// @flow
import * as React from "react";
import styled from "styled-components";
import { pulsate } from "shared/styles/animations";
import { randomInteger } from "shared/random";
import { pulsate } from "shared/styles/animations";
import Flex from "components/Flex";
type Props = {
@@ -17,7 +17,8 @@ class Mask extends React.Component<Props> {
return false;
}
componentWillMount() {
constructor() {
super();
this.width = randomInteger(75, 100);
}
@@ -27,10 +28,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 {
+39 -31
View File
@@ -1,14 +1,15 @@
// @flow
import * as React from "react";
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 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 "components/Flex";
import NudeButton from "components/NudeButton";
import Scrollable from "components/Scrollable";
ReactModal.setAppElement("#root");
@@ -21,28 +22,31 @@ 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")`
.ReactModalPortal + .ReactModalPortal {
.ReactModalPortal + .ReactModalPortal,
.ReactModalPortal + [data-react-modal-body-trap] + .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;
}
}
.ReactModalPortal + .ReactModalPortal + .ReactModalPortal {
.ReactModalPortal + .ReactModalPortal + .ReactModalPortal,
.ReactModalPortal + .ReactModalPortal + [data-react-modal-body-trap] + .ReactModalPortal {
.ReactModal__Overlay {
margin-left: 24px;
}
}
.ReactModalPortal + .ReactModalPortal + .ReactModalPortal + .ReactModalPortal {
.ReactModalPortal + .ReactModalPortal + .ReactModalPortal + .ReactModalPortal,
.ReactModalPortal + .ReactModalPortal + .ReactModalPortal + [data-react-modal-body-trap] + .ReactModalPortal {
.ReactModal__Overlay {
margin-left: 36px;
}
@@ -64,7 +68,7 @@ const Modal = ({
if (!isOpen) return null;
return (
<React.Fragment>
<>
<GlobalStyles />
<StyledModal
contentLabel={title}
@@ -72,10 +76,11 @@ const Modal = ({
isOpen={isOpen}
{...rest}
>
<Content onClick={ev => ev.stopPropagation()} column>
{title && <h1>{title}</h1>}
{children}
<Content>
<Centered onClick={(ev) => ev.stopPropagation()} column>
{title && <h1>{title}</h1>}
{children}
</Centered>
</Content>
<Back onClick={onRequestClose}>
<BackIcon size={32} color="currentColor" />
@@ -85,14 +90,24 @@ const Modal = ({
<CloseIcon size={32} color="currentColor" />
</Close>
</StyledModal>
</React.Fragment>
</>
);
};
const Content = styled(Flex)`
const Content = styled(Scrollable)`
width: 100%;
padding: 8vh 2rem 2rem;
${breakpoint("tablet")`
padding-top: 13vh;
`};
`;
const Centered = styled(Flex)`
width: 640px;
max-width: 100%;
position: relative;
margin: 0 auto;
`;
const StyledModal = styled(ReactModal)`
@@ -103,20 +118,13 @@ 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};
padding: 8vh 2rem 2rem;
background: ${(props) => props.theme.background};
transition: ${(props) => props.theme.backgroundTransition};
outline: none;
${breakpoint("tablet")`
padding-top: 13vh;
`};
`;
const Text = styled.span`
@@ -133,7 +141,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;
@@ -147,13 +155,13 @@ const Close = styled(NudeButton)`
`;
const Back = styled(NudeButton)`
position: fixed;
position: absolute;
display: none;
align-items: center;
top: 2rem;
left: 2rem;
opacity: 0.75;
color: ${props => props.theme.text};
color: ${(props) => props.theme.text};
width: auto;
height: auto;
-58
View File
@@ -1,58 +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 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>
</span>
);
}
}
export default Modals;
+2 -2
View File
@@ -2,8 +2,8 @@
import styled from "styled-components";
const Notice = styled.p`
background: ${props => props.theme.sidebarBackground};
color: ${props => props.theme.sidebarText};
background: ${(props) => props.theme.sidebarBackground};
color: ${(props) => props.theme.sidebarText};
padding: 10px 12px;
border-radius: 4px;
position: relative;
+3 -3
View File
@@ -1,7 +1,7 @@
// @flow
import { lighten } from "polished";
import * as React from "react";
import styled from "styled-components";
import { lighten } from "polished";
const Button = styled.button`
width: 24px;
@@ -14,12 +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;
}
`;
export default React.forwardRef((props, ref) => (
export default React.forwardRef<any, typeof Button>((props, ref) => (
<Button {...props} ref={ref} />
));
+1 -1
View File
@@ -1,6 +1,6 @@
// @flow
import * as React from "react";
import { observer, inject } from "mobx-react";
import * as React from "react";
import { Helmet } from "react-helmet";
import AuthStore from "stores/AuthStore";
+2 -2
View File
@@ -1,6 +1,6 @@
// @flow
import * as React from "react";
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";
@@ -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} />
)}
/>
+13 -9
View File
@@ -1,11 +1,11 @@
// @flow
import * as React from "react";
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 ArrowKeyNavigation from "boundless-arrow-key-navigation";
import { DEFAULT_PAGINATION_LIMIT } from "stores/BaseStore";
import DelayedMount from "components/DelayedMount";
import { ListPlaceholder } from "components/LoadingPlaceholder";
type Props = {
@@ -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>
)}
</>
);
}
}
+22 -13
View File
@@ -1,14 +1,13 @@
// @flow
import * as React from "react";
import { observer } from "mobx-react";
import styled from "styled-components";
import { GoToIcon } from "outline-icons";
import Flex from "components/Flex";
import Document from "models/Document";
import Collection from "models/Collection";
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,
@@ -43,20 +42,23 @@ class PathToDocument extends React.Component<Props> {
return (
<Component ref={ref} onClick={this.handleClick} href="" selectable>
{collection && <CollectionIcon collection={collection} />}
&nbsp;
{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>
<DocumentTitle>
{" "}
<StyledGoToIcon /> <Title>{document.title}</Title>
</Flex>
</DocumentTitle>
)}
</Component>
);
}
}
const DocumentTitle = styled(Flex)``;
const Title = styled.span`
white-space: nowrap;
overflow: hidden;
@@ -64,7 +66,7 @@ const Title = styled.span`
`;
const StyledGoToIcon = styled(GoToIcon)`
opacity: 0.25;
fill: ${(props) => props.theme.divider};
`;
const ResultWrapper = styled.div`
@@ -73,20 +75,27 @@ 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"))`
margin: 0 -8px;
padding: 8px 4px;
border-radius: 8px;
${DocumentTitle} {
display: none;
}
&:hover,
&:active,
&:focus {
background: ${props => props.theme.listItemHoverBackground};
background: ${(props) => props.theme.listItemHoverBackground};
outline: none;
${DocumentTitle} {
display: flex;
}
}
`;
+2 -2
View File
@@ -1,6 +1,6 @@
// @flow
import * as React from "react";
import BoundlessPopover from "boundless-popover";
import * as React from "react";
import styled, { keyframes } from "styled-components";
const fadeIn = keyframes`
@@ -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;
-121
View File
@@ -1,121 +0,0 @@
// @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 "components/Flex";
import Time from "components/Time";
import Breadcrumb from "components/Breadcrumb";
import CollectionsStore from "stores/CollectionsStore";
import AuthStore from "stores/AuthStore";
const Container = styled(Flex)`
color: ${props => props.theme.textTertiary};
font-size: 13px;
white-space: nowrap;
overflow: hidden;
`;
const Modified = styled.span`
color: ${props =>
props.highlight ? props.theme.text : props.theme.textTertiary};
font-weight: ${props => (props.highlight ? "600" : "400")};
`;
type Props = {
collections: CollectionsStore,
auth: AuthStore,
showCollection?: boolean,
showPublished?: boolean,
document: Document,
children: React.Node,
};
function PublishingInfo({
auth,
collections,
showPublished,
showCollection,
document,
children,
...rest
}: Props) {
const {
modifiedSinceViewed,
updatedAt,
updatedBy,
createdAt,
publishedAt,
archivedAt,
deletedAt,
isDraft,
} = document;
// Prevent meta information from displaying if updatedBy is not available.
// Currently the situation where this is true is rendering share links.
if (!updatedBy) {
return null;
}
let content;
if (deletedAt) {
content = (
<span>
deleted <Time dateTime={deletedAt} /> ago
</span>
);
} else if (archivedAt) {
content = (
<span>
archived <Time dateTime={archivedAt} /> ago
</span>
);
} else if (createdAt === updatedAt) {
content = (
<span>
created <Time dateTime={updatedAt} /> ago
</span>
);
} else if (publishedAt && (publishedAt === updatedAt || showPublished)) {
content = (
<span>
published <Time dateTime={publishedAt} /> ago
</span>
);
} else if (isDraft) {
content = (
<span>
saved <Time dateTime={updatedAt} /> ago
</span>
);
} else {
content = (
<Modified highlight={modifiedSinceViewed}>
updated <Time dateTime={updatedAt} /> ago
</Modified>
);
}
const collection = collections.get(document.collectionId);
const updatedByMe = auth.user && auth.user.id === updatedBy.id;
return (
<Container align="center" {...rest}>
{updatedByMe ? "You" : updatedBy.name}&nbsp;
{content}
{showCollection &&
collection && (
<span>
&nbsp;in&nbsp;
<strong>
<Breadcrumb document={document} onlyText />
</strong>
</span>
)}
{children}
</Container>
);
}
export default inject("collections", "auth")(observer(PublishingInfo));
+2 -2
View File
@@ -1,7 +1,7 @@
// @flow
import * as React from "react";
import { observable } from "mobx";
import { observer } from "mobx-react";
import * as React from "react";
import styled from "styled-components";
type Props = {
@@ -31,7 +31,7 @@ const Wrapper = styled.div`
overflow-x: hidden;
overscroll-behavior: none;
-webkit-overflow-scrolling: touch;
box-shadow: ${props =>
box-shadow: ${(props) =>
props.shadow ? "0 1px inset rgba(0,0,0,.1)" : "none"};
transition: all 250ms ease-in-out;
`;
+36 -23
View File
@@ -1,7 +1,6 @@
// @flow
import * as React from "react";
import { observable } from "mobx";
import { observer, inject } from "mobx-react";
import styled from "styled-components";
import {
ArchiveIcon,
HomeIcon,
@@ -12,44 +11,48 @@ import {
TrashIcon,
PlusIcon,
} from "outline-icons";
import Flex from "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 * as React from "react";
import styled from "styled-components";
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 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<>) => {
@@ -119,7 +122,8 @@ class MainSidebar extends React.Component<Props> {
icon={<EditIcon color="currentColor" />}
label={
<Drafts align="center">
Drafts{draftDocumentsCount > 0 && (
Drafts
{draftDocumentsCount > 0 && (
<Bubble count={draftDocumentsCount} />
)}
</Drafts>
@@ -134,7 +138,9 @@ class MainSidebar extends React.Component<Props> {
/>
</Section>
<Section>
<Collections onCreateCollection={this.handleCreateCollection} />
<Collections
onCreateCollection={this.handleCreateCollectionModalOpen}
/>
</Section>
<Section>
<SidebarLink
@@ -175,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>
);
}
@@ -184,4 +197,4 @@ const Drafts = styled(Flex)`
height: 24px;
`;
export default inject("documents", "policies", "auth", "ui")(MainSidebar);
export default inject("documents", "policies", "auth")(MainSidebar);
+22 -23
View File
@@ -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 {
DocumentIcon,
EmailIcon,
@@ -16,19 +13,22 @@ import {
BulletedListIcon,
ExpandedIcon,
} from "outline-icons";
import ZapierIcon from "./icons/Zapier";
import SlackIcon from "./icons/Slack";
import Flex from "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 Version from "./components/Version";
import PoliciesStore from "stores/PoliciesStore";
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 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 = {
@@ -40,7 +40,7 @@ type Props = {
@observer
class SettingsSidebar extends React.Component<Props> {
returnToDashboard = () => {
this.props.history.push("/");
this.props.history.push("/home");
};
render() {
@@ -146,13 +146,12 @@ class SettingsSidebar extends React.Component<Props> {
/>
</Section>
)}
{can.update &&
env.DEPLOYMENT !== "hosted" && (
<Section>
<Header>Installation</Header>
<Version />
</Section>
)}
{can.update && env.DEPLOYMENT !== "hosted" && (
<Section>
<Header>Installation</Header>
<Version />
</Section>
)}
</Scrollable>
</Flex>
</Sidebar>
+46 -50
View File
@@ -1,65 +1,60 @@
// @flow
import { observer } 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 { observer, inject } from "mobx-react";
import { CloseIcon, MenuIcon } from "outline-icons";
import Fade from "components/Fade";
import Flex from "components/Flex";
import UiStore from "stores/UiStore";
import usePrevious from "hooks/usePrevious";
import useStores from "hooks/useStores";
let firstRender = true;
type Props = {
children: React.Node,
location: Location,
ui: UiStore,
};
@observer
class Sidebar extends React.Component<Props> {
componentWillReceiveProps = (nextProps: Props) => {
if (this.props.location !== nextProps.location) {
this.props.ui.hideMobileSidebar();
function Sidebar({ location, children }: Props) {
const { ui } = useStores();
const previousLocation = usePrevious(location);
React.useEffect(() => {
if (location !== previousLocation) {
ui.hideMobileSidebar();
}
};
}, [ui, location]);
toggleSidebar = () => {
this.props.ui.toggleMobileSidebar();
};
render() {
const { children, ui } = this.props;
const content = (
<Container
editMode={ui.editMode}
const content = (
<Container
editMode={ui.editMode}
mobileSidebarVisible={ui.mobileSidebarVisible}
column
>
<Toggle
onClick={ui.toggleMobileSidebar}
mobileSidebarVisible={ui.mobileSidebarVisible}
column
>
<Toggle
onClick={this.toggleSidebar}
mobileSidebarVisible={ui.mobileSidebarVisible}
>
{ui.mobileSidebarVisible ? (
<CloseIcon size={32} />
) : (
<MenuIcon size={32} />
)}
</Toggle>
{children}
</Container>
);
{ui.mobileSidebarVisible ? (
<CloseIcon size={32} />
) : (
<MenuIcon size={32} />
)}
</Toggle>
{children}
</Container>
);
// Fade in the sidebar on first render after page load
if (firstRender) {
firstRender = false;
return <Fade>{content}</Fade>;
}
return content;
// Fade in the sidebar on first render after page load
if (firstRender) {
firstRender = false;
return <Fade>{content}</Fade>;
}
return content;
}
const Container = styled(Flex)`
@@ -67,10 +62,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;
@@ -80,7 +76,7 @@ const Container = styled(Flex)`
&:before,
&:after {
content: "";
background: ${props => props.theme.sidebarBackground};
background: ${(props) => props.theme.sidebarBackground};
position: absolute;
top: -50vh;
left: 0;
@@ -94,8 +90,8 @@ const Container = styled(Flex)`
}
${breakpoint("tablet")`
left: ${props => (props.editMode ? `-${props.theme.sidebarWidth}` : 0)};
width: ${props => props.theme.sidebarWidth};
left: ${(props) => (props.editMode ? `-${props.theme.sidebarWidth}` : 0)};
width: ${(props) => props.theme.sidebarWidth};
margin: 0;
z-index: 3;
`};
@@ -106,8 +102,8 @@ 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;
@@ -116,4 +112,4 @@ const Toggle = styled.a`
`};
`;
export default withRouter(inject("ui")(Sidebar));
export default withRouter(observer(Sidebar));
+2 -2
View File
@@ -14,8 +14,8 @@ 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-weight: 600;
@@ -1,17 +1,17 @@
// @flow
import * as React from "react";
import { observer } from "mobx-react";
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 CollectionMenu from "menus/CollectionMenu";
import UiStore from "stores/UiStore";
import DocumentsStore from "stores/DocumentsStore";
import SidebarLink from "./SidebarLink";
import DocumentLink from "./DocumentLink";
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,
@@ -61,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 "components/Flex";
import { PlusIcon } from "outline-icons";
import { newDocumentUrl } from "utils/routeHelpers";
import Header from "./Header";
import SidebarLink from "./SidebarLink";
import CollectionLink from "./CollectionLink";
import CollectionsLoading from "./CollectionsLoading";
import Fade from "components/Fade";
import * as React from "react";
import keydown from "react-keydown";
import { withRouter, type RouterHistory } from "react-router-dom";
import CollectionsStore from "stores/CollectionsStore";
import DocumentsStore from "stores/DocumentsStore";
import PoliciesStore from "stores/PoliciesStore";
import UiStore from "stores/UiStore";
import DocumentsStore from "stores/DocumentsStore";
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,
@@ -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}
@@ -74,7 +73,7 @@ class Collections extends React.Component<Props> {
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));
@@ -1,16 +1,16 @@
// @flow
import * as React from "react";
import { observer } from "mobx-react";
import { observable } from "mobx";
import { observer } from "mobx-react";
import * as React from "react";
import styled from "styled-components";
import DocumentsStore from "stores/DocumentsStore";
import Collection from "models/Collection";
import Document from "models/Document";
import DocumentMenu from "menus/DocumentMenu";
import SidebarLink from "./SidebarLink";
import DropToImport from "components/DropToImport";
import Fade from "components/Fade";
import Collection from "models/Collection";
import DocumentsStore from "stores/DocumentsStore";
import Flex from "components/Flex";
import SidebarLink from "./SidebarLink";
import DocumentMenu from "menus/DocumentMenu";
import { type NavigationNode } from "types";
type Props = {
@@ -76,7 +76,7 @@ class DocumentLink extends React.Component<Props> {
collection &&
(collection
.pathToDocument(activeDocument)
.map(entry => entry.id)
.map((entry) => entry.id)
.includes(node.id) ||
this.isActiveDocument())
);
@@ -110,14 +110,12 @@ class DocumentLink extends React.Component<Props> {
onClose={() => (this.menuOpen = false)}
/>
</Fade>
) : (
undefined
)
) : undefined
}
>
{this.hasChildDocuments() && (
<DocumentChildren column>
{node.children.map(childNode => (
{node.children.map((childNode) => (
<DocumentLink
key={childNode.id}
collection={collection}
+2 -2
View File
@@ -1,12 +1,12 @@
// @flow
import Flex from "components/Flex";
import styled from "styled-components";
import Flex from "components/Flex";
const Header = styled(Flex)`
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
color: ${props => props.theme.sidebarText};
color: ${(props) => props.theme.sidebarText};
letter-spacing: 0.04em;
margin: 4px 16px;
`;
@@ -1,16 +1,15 @@
// @flow
import * as React from "react";
import styled, { withTheme } from "styled-components";
import { ExpandedIcon } from "outline-icons";
import * as React from "react";
import styled from "styled-components";
import Flex from "components/Flex";
import TeamLogo from "components/TeamLogo";
type Props = {
teamName: string,
subheading: string,
subheading: React.Node,
showDisclosure?: boolean,
logoUrl: string,
theme: Object,
};
function HeaderBlock({
@@ -18,7 +17,6 @@ function HeaderBlock({
teamName,
subheading,
logoUrl,
theme,
...rest
}: Props) {
return (
@@ -27,7 +25,7 @@ function HeaderBlock({
<Flex align="flex-start" column>
<TeamName showDisclosure>
{teamName}{" "}
{showDisclosure && <StyledExpandedIcon color={theme.text} />}
{showDisclosure && <StyledExpandedIcon color="currentColor" />}
</TeamName>
<Subheading>{subheading}</Subheading>
</Flex>
@@ -46,7 +44,7 @@ const Subheading = styled.div`
font-size: 11px;
text-transform: uppercase;
font-weight: 500;
color: ${props => props.theme.sidebarText};
color: ${(props) => props.theme.sidebarText};
`;
const TeamName = styled.div`
@@ -54,7 +52,7 @@ const TeamName = styled.div`
padding-left: 10px;
padding-right: 24px;
font-weight: 600;
color: ${props => props.theme.text};
color: ${(props) => props.theme.text};
text-decoration: none;
font-size: 16px;
`;
@@ -73,4 +71,4 @@ const Header = styled(Flex)`
}
`;
export default withTheme(HeaderBlock);
export default HeaderBlock;
@@ -1,9 +1,8 @@
// @flow
import * as React from "react";
import { observable, action } from "mobx";
import { observer } from "mobx-react";
import { withRouter, NavLink } from "react-router-dom";
import { CollapsedIcon } from "outline-icons";
import * as React from "react";
import { withRouter, NavLink } from "react-router-dom";
import styled, { withTheme } from "styled-components";
import Flex from "components/Flex";
@@ -25,79 +24,80 @@ type Props = {
depth?: number,
};
@observer
class SidebarLink extends React.Component<Props> {
@observable expanded: ?boolean = this.props.expanded;
function SidebarLink({
icon,
children,
onClick,
to,
label,
active,
menu,
menuOpen,
hideDisclosure,
theme,
exact,
href,
depth,
...rest
}: Props) {
const [expanded, setExpanded] = React.useState(rest.expanded);
style = {
paddingLeft: `${(this.props.depth || 0) * 16 + 16}px`,
};
componentWillReceiveProps(nextProps: Props) {
if (nextProps.expanded !== undefined) {
this.expanded = nextProps.expanded;
}
}
@action
handleClick = (ev: SyntheticEvent<>) => {
ev.preventDefault();
ev.stopPropagation();
this.expanded = !this.expanded;
};
@action
handleExpand = () => {
this.expanded = true;
};
render() {
const {
icon,
children,
onClick,
to,
label,
active,
menu,
menuOpen,
hideDisclosure,
exact,
href,
} = this.props;
const showDisclosure = !!children && !hideDisclosure;
const activeStyle = {
color: this.props.theme.text,
background: this.props.theme.sidebarItemBackground,
fontWeight: 600,
...this.style,
const style = React.useMemo(() => {
return {
paddingLeft: `${(depth || 0) * 16 + 16}px`,
};
}, [depth]);
return (
<Wrapper column>
<StyledNavLink
activeStyle={activeStyle}
style={active ? activeStyle : this.style}
onClick={onClick}
exact={exact !== false}
to={to}
as={to ? undefined : href ? "a" : "div"}
href={href}
>
{icon && <IconWrapper>{icon}</IconWrapper>}
<Label onClick={this.handleExpand}>
{showDisclosure && (
<Disclosure expanded={this.expanded} onClick={this.handleClick} />
)}
{label}
</Label>
{menu && <Action menuOpen={menuOpen}>{menu}</Action>}
</StyledNavLink>
{this.expanded && children}
</Wrapper>
);
}
React.useEffect(() => {
if (rest.expanded !== undefined) {
setExpanded(rest.expanded);
}
}, [rest.expanded]);
const handleClick = React.useCallback(
(ev: SyntheticEvent<>) => {
ev.preventDefault();
ev.stopPropagation();
setExpanded(!expanded);
},
[expanded]
);
const handleExpand = React.useCallback(() => {
setExpanded(true);
}, []);
const showDisclosure = !!children && !hideDisclosure;
const activeStyle = {
color: theme.text,
background: theme.sidebarItemBackground,
fontWeight: 600,
...style,
};
return (
<Wrapper column>
<StyledNavLink
activeStyle={activeStyle}
style={active ? activeStyle : style}
onClick={onClick}
exact={exact !== false}
to={to}
as={to ? undefined : href ? "a" : "div"}
href={href}
>
{icon && <IconWrapper>{icon}</IconWrapper>}
<Label onClick={handleExpand}>
{showDisclosure && (
<Disclosure expanded={expanded} onClick={handleClick} />
)}
{label}
</Label>
{menu && <Action menuOpen={menuOpen}>{menu}</Action>}
</StyledNavLink>
{expanded && children}
</Wrapper>
);
}
// accounts for whitespace around icon
@@ -108,11 +108,11 @@ const IconWrapper = styled.span`
`;
const Action = styled.span`
display: ${props => (props.menuOpen ? "inline" : "none")};
display: ${(props) => (props.menuOpen ? "inline" : "none")};
position: absolute;
top: 4px;
right: 4px;
color: ${props => props.theme.textTertiary};
color: ${(props) => props.theme.textTertiary};
svg {
opacity: 0.75;
@@ -132,17 +132,17 @@ const StyledNavLink = styled(NavLink)`
text-overflow: ellipsis;
padding: 4px 16px;
border-radius: 4px;
color: ${props => props.theme.sidebarText};
color: ${(props) => props.theme.sidebarText};
font-size: 15px;
cursor: pointer;
&:hover {
color: ${props => props.theme.text};
color: ${(props) => props.theme.text};
}
&:focus {
color: ${props => props.theme.text};
background: ${props => props.theme.black05};
color: ${(props) => props.theme.text};
background: ${(props) => props.theme.black05};
outline: none;
}
@@ -171,4 +171,4 @@ const Disclosure = styled(CollapsedIcon)`
${({ expanded }) => !expanded && "transform: rotate(-90deg);"};
`;
export default withRouter(withTheme(SidebarLink));
export default withRouter(withTheme(observer(SidebarLink)));
+3 -3
View File
@@ -2,8 +2,8 @@
import * as React from "react";
import styled from "styled-components";
import Badge from "components/Badge";
import SidebarLink from "./SidebarLink";
import { version } from "../../../../package.json";
import SidebarLink from "./SidebarLink";
export default function Version() {
const [releasesBehind, setReleasesBehind] = React.useState(0);
@@ -30,7 +30,7 @@ export default function Version() {
<SidebarLink
href="https://github.com/outline/outline/releases"
label={
<React.Fragment>
<>
v{version}
<br />
<LilBadge>
@@ -40,7 +40,7 @@ export default function Version() {
releasesBehind === 1 ? "" : "s"
} behind`}
</LilBadge>
</React.Fragment>
</>
}
/>
);
+60 -30
View File
@@ -1,18 +1,19 @@
// @flow
import * as React from "react";
import { find } from "lodash";
import { observable } from "mobx";
import { inject, observer } from "mobx-react";
import { find } from "lodash";
import io from "socket.io-client";
import DocumentsStore from "stores/DocumentsStore";
import * as React from "react";
import io, { Socket } from "socket.io-client";
import AuthStore from "stores/AuthStore";
import CollectionsStore from "stores/CollectionsStore";
import DocumentPresenceStore from "stores/DocumentPresenceStore";
import DocumentsStore from "stores/DocumentsStore";
import GroupsStore from "stores/GroupsStore";
import MembershipsStore from "stores/MembershipsStore";
import DocumentPresenceStore from "stores/DocumentPresenceStore";
import PoliciesStore from "stores/PoliciesStore";
import ViewsStore from "stores/ViewsStore";
import AuthStore from "stores/AuthStore";
import UiStore from "stores/UiStore";
import ViewsStore from "stores/ViewsStore";
import { getVisibilityListener, getPageVisible } from "utils/pageVisibility";
export const SocketContext: any = React.createContext();
@@ -31,12 +32,42 @@ type Props = {
@observer
class SocketProvider extends React.Component<Props> {
@observable socket;
@observable socket: Socket;
componentDidMount() {
this.createConnection();
document.addEventListener(getVisibilityListener(), this.checkConnection);
}
componentWillUnmount() {
if (this.socket) {
this.socket.authenticated = false;
this.socket.disconnect();
}
document.removeEventListener(getVisibilityListener(), this.checkConnection);
}
checkConnection = () => {
if (this.socket && this.socket.disconnected && getPageVisible()) {
// null-ifying this reference is important, do not remove. Without it
// references to old sockets are potentially held in context
this.socket.close();
this.socket = null;
this.createConnection();
}
};
createConnection = () => {
this.socket = io(window.location.origin, {
path: "/realtime",
transports: ["websocket"],
reconnectionDelay: 1000,
reconnectionDelayMax: 30000,
});
this.socket.authenticated = false;
const {
@@ -61,23 +92,29 @@ class SocketProvider extends React.Component<Props> {
});
});
this.socket.on("disconnect", () => {
this.socket.on("disconnect", (reason: string) => {
// when the socket is disconnected we need to clear all presence state as
// it's no longer reliable.
presence.clear();
});
// on reconnection, reset the transports option, as the Websocket
// connection may have failed (caused by proxy, firewall, browser, ...)
this.socket.on("reconnect_attempt", () => {
this.socket.io.opts.transports = ["polling", "websocket"];
});
this.socket.on("authenticated", () => {
this.socket.authenticated = true;
});
this.socket.on("unauthorized", err => {
this.socket.on("unauthorized", (err) => {
this.socket.authenticated = false;
ui.showToast(err.message);
throw err;
});
this.socket.on("entities", async event => {
this.socket.on("entities", async (event) => {
if (event.documentIds) {
for (const documentDescriptor of event.documentIds) {
const documentId = documentDescriptor.id;
@@ -182,23 +219,23 @@ class SocketProvider extends React.Component<Props> {
}
});
this.socket.on("documents.star", event => {
this.socket.on("documents.star", (event) => {
documents.starredIds.set(event.documentId, true);
});
this.socket.on("documents.unstar", event => {
this.socket.on("documents.unstar", (event) => {
documents.starredIds.set(event.documentId, false);
});
// received when a user is given access to a collection
// if the user is us then we go ahead and load the collection from API.
this.socket.on("collections.add_user", event => {
this.socket.on("collections.add_user", (event) => {
if (auth.user && event.userId === auth.user.id) {
collections.fetch(event.collectionId, { force: true });
}
// Document policies might need updating as the permission changes
documents.inCollection(event.collectionId).forEach(document => {
documents.inCollection(event.collectionId).forEach((document) => {
policies.remove(document.id);
});
});
@@ -206,7 +243,7 @@ class SocketProvider extends React.Component<Props> {
// received when a user is removed from having access to a collection
// to keep state in sync we must update our UI if the user is us,
// or otherwise just remove any membership state we have for that user.
this.socket.on("collections.remove_user", event => {
this.socket.on("collections.remove_user", (event) => {
if (auth.user && event.userId === auth.user.id) {
collections.remove(event.collectionId);
memberships.removeCollectionMemberships(event.collectionId);
@@ -218,32 +255,32 @@ class SocketProvider extends React.Component<Props> {
// received a message from the API server that we should request
// to join a specific room. Forward that to the ws server.
this.socket.on("join", event => {
this.socket.on("join", (event) => {
this.socket.emit("join", event);
});
// received a message from the API server that we should request
// to leave a specific room. Forward that to the ws server.
this.socket.on("leave", event => {
this.socket.on("leave", (event) => {
this.socket.emit("leave", event);
});
// received whenever we join a document room, the payload includes
// userIds that are present/viewing and those that are editing.
this.socket.on("document.presence", event => {
this.socket.on("document.presence", (event) => {
presence.init(event.documentId, event.userIds, event.editingIds);
});
// received whenever a new user joins a document room, aka they
// navigate to / start viewing a document
this.socket.on("user.join", event => {
this.socket.on("user.join", (event) => {
presence.touch(event.documentId, event.userId, event.isEditing);
views.touch(event.documentId, event.userId);
});
// received whenever a new user leaves a document room, aka they
// navigate away / stop viewing a document
this.socket.on("user.leave", event => {
this.socket.on("user.leave", (event) => {
presence.leave(event.documentId, event.userId);
views.touch(event.documentId, event.userId);
});
@@ -251,17 +288,10 @@ class SocketProvider extends React.Component<Props> {
// received when another client in a document room wants to change
// or update it's presence. Currently the only property is whether
// the client is in editing state or not.
this.socket.on("user.presence", event => {
this.socket.on("user.presence", (event) => {
presence.touch(event.documentId, event.userId, event.isEditing);
});
}
componentWillUnmount() {
if (this.socket) {
this.socket.disconnect();
this.socket.authenticated = false;
}
}
};
render() {
return (
+3 -3
View File
@@ -7,7 +7,7 @@ type Props = {
};
const H3 = styled.h3`
border-bottom: 1px solid ${props => props.theme.divider};
border-bottom: 1px solid ${(props) => props.theme.divider};
margin-top: 22px;
margin-bottom: 12px;
line-height: 1;
@@ -21,8 +21,8 @@ const Underline = styled("span")`
font-weight: 500;
font-size: 14px;
line-height: 1.5;
color: ${props => props.theme.textSecondary};
border-bottom: 3px solid ${props => props.theme.textSecondary};
color: ${(props) => props.theme.textSecondary};
border-bottom: 3px solid ${(props) => props.theme.textSecondary};
padding-bottom: 5px;
`;
+9 -9
View File
@@ -38,8 +38,8 @@ const Label = styled.label`
const Wrapper = styled.label`
position: relative;
display: inline-block;
width: ${props => props.width}px;
height: ${props => props.height}px;
width: ${(props) => props.width}px;
height: ${(props) => props.height}px;
margin-bottom: 4px;
margin-right: 8px;
`;
@@ -51,16 +51,16 @@ const Slider = styled.span`
left: 0;
right: 0;
bottom: 0;
background-color: ${props => props.theme.slate};
background-color: ${(props) => props.theme.slate};
-webkit-transition: 0.4s;
transition: 0.4s;
border-radius: ${props => props.height}px;
border-radius: ${(props) => props.height}px;
&:before {
position: absolute;
content: "";
height: ${props => props.height - 8}px;
width: ${props => props.height - 8}px;
height: ${(props) => props.height - 8}px;
width: ${(props) => props.height - 8}px;
left: 4px;
bottom: 4px;
background-color: white;
@@ -77,15 +77,15 @@ const HiddenInput = styled.input`
visibility: hidden;
&:checked + ${Slider} {
background-color: ${props => props.theme.primary};
background-color: ${(props) => props.theme.primary};
}
&:focus + ${Slider} {
box-shadow: 0 0 1px ${props => props.theme.primary};
box-shadow: 0 0 1px ${(props) => props.theme.primary};
}
&:checked + ${Slider}:before {
transform: translateX(${props => props.width - props.height}px);
transform: translateX(${(props) => props.width - props.height}px);
}
`;
+7 -7
View File
@@ -1,8 +1,8 @@
// @flow
import * as React from "react";
import styled, { withTheme } from "styled-components";
import { NavLink } from "react-router-dom";
import { lighten } from "polished";
import * as React from "react";
import { NavLink } from "react-router-dom";
import styled, { withTheme } from "styled-components";
type Props = {
theme: Object,
@@ -15,20 +15,20 @@ const StyledNavLink = styled(NavLink)`
display: inline-block;
font-weight: 500;
font-size: 14px;
color: ${props => props.theme.textTertiary};
color: ${(props) => props.theme.textTertiary};
margin-right: 24px;
padding-bottom: 8px;
&:hover {
color: ${props => props.theme.textSecondary};
border-bottom: 3px solid ${props => props.theme.divider};
color: ${(props) => props.theme.textSecondary};
border-bottom: 3px solid ${(props) => props.theme.divider};
padding-bottom: 5px;
}
&:focus {
outline: none;
border-bottom: 3px solid
${props => lighten(0.4, props.theme.buttonBackground)};
${(props) => lighten(0.4, props.theme.buttonBackground)};
padding-bottom: 5px;
}
`;
+4 -2
View File
@@ -3,13 +3,15 @@ import styled from "styled-components";
const Tabs = styled.nav`
position: relative;
border-bottom: 1px solid ${props => props.theme.divider};
border-bottom: 1px solid ${(props) => props.theme.divider};
margin-top: 22px;
margin-bottom: 12px;
overflow-y: auto;
white-space: nowrap;
`;
export const Separator = styled.span`
border-left: 1px solid ${props => props.theme.divider};
border-left: 1px solid ${(props) => props.theme.divider};
position: relative;
top: 2px;
margin-right: 24px;
+2 -2
View File
@@ -5,8 +5,8 @@ const TeamLogo = styled.img`
width: auto;
height: 38px;
border-radius: 4px;
background: ${props => props.theme.background};
border: 1px solid ${props => props.theme.divider};
background: ${(props) => props.theme.background};
border: 1px solid ${(props) => props.theme.divider};
`;
export default TeamLogo;
+4 -4
View File
@@ -1,9 +1,9 @@
// @flow
import * as React from "react";
import { inject, observer } from "mobx-react";
import * as React from "react";
import { ThemeProvider } from "styled-components";
import { dark, light } from "shared/styles/theme";
import GlobalStyles from "shared/styles/globals";
import { dark, light } from "shared/styles/theme";
import UiStore from "stores/UiStore";
type Props = {
@@ -14,10 +14,10 @@ type Props = {
function Theme({ children, ui }: Props) {
return (
<ThemeProvider theme={ui.resolvedTheme === "dark" ? dark : light}>
<React.Fragment>
<>
<GlobalStyles />
{children}
</React.Fragment>
</>
</ThemeProvider>
);
}
+4 -4
View File
@@ -1,22 +1,22 @@
// @flow
import * as React from "react";
import Tooltip from "components/Tooltip";
import distanceInWordsToNow from "date-fns/distance_in_words_to_now";
import format from "date-fns/format";
import * as React from "react";
import Tooltip from "components/Tooltip";
let callbacks = [];
// This is a shared timer that fires every minute, used for
// updating all Time components across the page all at once.
setInterval(() => {
callbacks.forEach(cb => cb());
callbacks.forEach((cb) => cb());
}, 1000 * 60);
function eachMinute(fn) {
callbacks.push(fn);
return () => {
callbacks = callbacks.filter(cb => cb !== fn);
callbacks = callbacks.filter((cb) => cb !== fn);
};
}
+6 -6
View File
@@ -1,9 +1,9 @@
// @flow
import * as React from "react";
import { observer, inject } from "mobx-react";
import * as React from "react";
import styled from "styled-components";
import Toast from "./components/Toast";
import UiStore from "../../stores/UiStore";
import Toast from "./components/Toast";
type Props = {
ui: UiStore,
@@ -15,7 +15,7 @@ class Toasts extends React.Component<Props> {
return (
<List>
{ui.orderedToasts.map(toast => (
{ui.orderedToasts.map((toast) => (
<Toast
key={toast.id}
toast={toast}
@@ -29,12 +29,12 @@ class Toasts extends React.Component<Props> {
const List = styled.ol`
position: fixed;
left: ${props => props.theme.hpadding};
bottom: ${props => props.theme.vpadding};
left: ${(props) => props.theme.hpadding};
bottom: ${(props) => props.theme.vpadding};
list-style: none;
margin: 0;
padding: 0;
z-index: 1000;
z-index: ${(props) => props.theme.depths.toasts};
`;
export default inject("ui")(Toasts);
+8 -8
View File
@@ -1,9 +1,9 @@
// @flow
import { darken } from "polished";
import * as React from "react";
import styled from "styled-components";
import { darken } from "polished";
import { fadeAndScaleIn } from "shared/styles/animations";
import type { Toast as TToast } from "../../../types";
import type { Toast as TToast } from "types";
type Props = {
onRequestClose: () => void,
@@ -61,13 +61,13 @@ const Action = styled.span`
height: 100%;
text-transform: uppercase;
font-size: 12px;
color: ${props => props.theme.toastText};
background: ${props => darken(0.05, props.theme.toastBackground)};
color: ${(props) => props.theme.toastText};
background: ${(props) => darken(0.05, props.theme.toastBackground)};
border-top-right-radius: 5px;
border-bottom-right-radius: 5px;
&:hover {
background: ${props => darken(0.1, props.theme.toastBackground)};
background: ${(props) => darken(0.1, props.theme.toastBackground)};
}
`;
@@ -76,14 +76,14 @@ const Container = styled.div`
align-items: center;
animation: ${fadeAndScaleIn} 100ms ease;
margin: 8px 0;
color: ${props => props.theme.toastText};
background: ${props => props.theme.toastBackground};
color: ${(props) => props.theme.toastText};
background: ${(props) => props.theme.toastBackground};
font-size: 15px;
border-radius: 5px;
cursor: default;
&:hover {
background: ${props => darken(0.05, props.theme.toastBackground)};
background: ${(props) => darken(0.05, props.theme.toastBackground)};
}
`;
+8 -8
View File
@@ -1,7 +1,7 @@
// @flow
import Tippy from "@tippy.js/react";
import * as React from "react";
import styled from "styled-components";
import Tippy from "@tippy.js/react";
type Props = {
tooltip: React.Node,
@@ -24,9 +24,9 @@ class Tooltip extends React.Component<Props> {
if (shortcut) {
content = (
<React.Fragment>
<>
{tooltip} &middot; <Shortcut>{shortcut}</Shortcut>
</React.Fragment>
</>
);
}
@@ -54,19 +54,19 @@ const Shortcut = styled.kbd`
font: 10px "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier,
monospace;
line-height: 10px;
color: ${props => props.theme.tooltipBackground};
color: ${(props) => props.theme.tooltipBackground};
vertical-align: middle;
background-color: ${props => props.theme.tooltipText};
background-color: ${(props) => props.theme.tooltipText};
border-radius: 3px;
`;
const StyledTippy = styled(Tippy)`
font-size: 13px;
background-color: ${props => props.theme.tooltipBackground};
color: ${props => props.theme.tooltipText};
background-color: ${(props) => props.theme.tooltipBackground};
color: ${(props) => props.theme.tooltipText};
svg {
fill: ${props => props.theme.tooltipBackground};
fill: ${(props) => props.theme.tooltipBackground};
}
`;
+1 -3
View File
@@ -19,9 +19,7 @@ export default class Figma extends React.Component<Props> {
render() {
return (
<Frame
src={`https://www.figma.com/embed?embed_host=outline&url=${
this.props.attrs.href
}`}
src={`https://www.figma.com/embed?embed_host=outline&url=${this.props.attrs.href}`}
title="Figma Embed"
border
/>
+6 -27
View File
@@ -13,31 +13,16 @@ type Props = {|
|};
class Gist extends React.Component<Props> {
iframeNode: ?HTMLIFrameElement;
static ENABLED = [URL_REGEX];
componentDidMount() {
this.updateIframeContent();
}
get id() {
const gistUrl = new URL(this.props.attrs.href);
return gistUrl.pathname.split("/")[2];
}
updateIframeContent() {
const id = this.id;
const iframe = this.iframeNode;
updateIframeContent = (iframe: ?HTMLIFrameElement) => {
if (!iframe) return;
// We need to add some temporary content to the iframe for the document
// to be available, otherwise it's undefined on first load
const temp = document.getElementById("gist");
if (temp) {
temp.innerHTML = "";
temp.appendChild(iframe);
}
const id = this.id;
// $FlowFixMe
let doc = iframe.document;
@@ -48,28 +33,22 @@ class Gist extends React.Component<Props> {
}
const gistLink = `https://gist.github.com/${id}.js`;
const gistScript = `<script type="text/javascript" src="${
gistLink
}"></script>`;
const gistScript = `<script type="text/javascript" src="${gistLink}"></script>`;
const styles =
"<style>*{ font-size:12px; } body { margin: 0; } .gist .blob-wrapper.data { max-height:150px; overflow:auto; }</style>";
const iframeHtml = `<html><head><base target="_parent">${
styles
}</head><body>${gistScript}</body></html>`;
const iframeHtml = `<html><head><base target="_parent">${styles}</head><body>${gistScript}</body></html>`;
doc.open();
doc.writeln(iframeHtml);
doc.close();
}
};
render() {
const id = this.id;
return (
<iframe
ref={ref => {
this.iframeNode = ref;
}}
ref={this.updateIframeContent}
type="text/html"
frameBorder="0"
width="100%"
+11 -8
View File
@@ -1,19 +1,22 @@
// @flow
import * as React from "react";
import { observable } from "mobx";
import { observer } from "mobx-react";
import * as React from "react";
import styled from "styled-components";
type Props = {
src?: string,
border?: boolean,
forwardedRef: *,
width?: string,
height?: string,
};
type PropsWithRef = Props & {
forwardedRef: React.Ref<typeof StyledIframe>,
};
@observer
class Frame extends React.Component<Props> {
class Frame extends React.Component<PropsWithRef> {
mounted: boolean;
@observable isLoaded: boolean = false;
@@ -65,20 +68,20 @@ class Frame extends React.Component<Props> {
const Rounded = styled.div`
border-radius: 3px;
overflow: hidden;
width: ${props => props.width};
height: ${props => props.height};
width: ${(props) => props.width};
height: ${(props) => props.height};
`;
// This wrapper allows us to pass non-standard HTML attributes through to the DOM element
// https://www.styled-components.com/docs/basics#passed-props
const Iframe = props => <iframe {...props} />;
const Iframe = (props) => <iframe {...props} />;
const StyledIframe = styled(Iframe)`
border: 1px solid;
border-color: ${props => props.theme.embedBorder};
border-color: ${(props) => props.theme.embedBorder};
border-radius: 3px;
`;
export default React.forwardRef((props, ref) => (
export default React.forwardRef<Props, typeof Frame>((props, ref) => (
<Frame {...props} forwardedRef={ref} />
));
+10
View File
@@ -0,0 +1,10 @@
// @flow
import * as React from "react";
export default function usePrevious(value: any) {
const ref = React.useRef();
React.useEffect(() => {
ref.current = value;
});
return ref.current;
}
+8
View File
@@ -0,0 +1,8 @@
// @flow
import { MobXProviderContext } from "mobx-react";
import * as React from "react";
import RootStore from "stores";
export default function useStores(): typeof RootStore {
return React.useContext(MobXProviderContext);
}

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