diff --git a/app/components/Avatar/Avatar.js b/app/components/Avatar/Avatar.js index a71e4bf3..c192e28d 100644 --- a/app/components/Avatar/Avatar.js +++ b/app/components/Avatar/Avatar.js @@ -6,10 +6,19 @@ import { observer } from 'mobx-react'; import { color } from 'shared/styles/constants'; import placeholder from './placeholder.png'; +type Props = { + src: string, + size: number, +}; + @observer -class Avatar extends React.Component<*> { +class Avatar extends React.Component { @observable error: boolean; + static defaultProps = { + size: 24, + }; + handleError = () => { this.error = true; }; @@ -17,7 +26,7 @@ class Avatar extends React.Component<*> { render() { return ( @@ -26,8 +35,8 @@ class Avatar extends React.Component<*> { } const CircleImg = styled.img` - width: 24px; - height: 24px; + width: ${props => props.size}px; + height: ${props => props.size}px; border-radius: 50%; border: 2px solid ${color.white}; flex-shrink: 0; diff --git a/app/components/Button/Button.js b/app/components/Button/Button.js index 7ddaafd7..5c89e8f3 100644 --- a/app/components/Button/Button.js +++ b/app/components/Button/Button.js @@ -2,7 +2,7 @@ import * as React from 'react'; import styled from 'styled-components'; import { color } from 'shared/styles/constants'; -import { darken, lighten } from 'polished'; +import { darken } from 'polished'; const RealButton = styled.button` display: inline-block; @@ -40,11 +40,14 @@ const RealButton = styled.button` ${props => props.light && ` - color: ${color.text}; - background: ${lighten(0.08, color.slateLight)}; + color: ${color.slate}; + background: transparent; + border: 1px solid ${color.slate}; &:hover { - background: ${color.slateLight}; + background: transparent; + color: ${color.slateDark}; + border: 1px solid ${color.slateDark}; } `} ${props => props.neutral && diff --git a/app/components/List/Item.js b/app/components/List/Item.js new file mode 100644 index 00000000..7fd7bfaa --- /dev/null +++ b/app/components/List/Item.js @@ -0,0 +1,56 @@ +// @flow +import * as React from 'react'; +import styled from 'styled-components'; +import { color, fontSize } from 'shared/styles/constants'; + +type Props = { + image?: React.Node, + title: string, + subtitle: React.Node, + actions?: React.Node, +}; + +const ListItem = ({ image, title, subtitle, actions }: Props) => { + return ( + + {image && {image}} + + {title} + {subtitle} + + {actions && {actions}} + + ); +}; + +const Wrapper = styled.li` + display: flex; + padding: 12px 0; + margin: 0; + border-bottom: 1px solid ${color.smokeDark}; +`; + +const Image = styled.div` + padding: 0 8px 0 0; +`; + +const Heading = styled.h2` + font-size: ${fontSize.medium}; + margin: 0; +`; + +const Content = styled.div` + flex-grow: 1; +`; + +const Subtitle = styled.p` + margin: 0; + font-size: ${fontSize.small}; + color: ${color.slate}; +`; + +const Actions = styled.div` + align-self: flex-end; +`; + +export default ListItem; diff --git a/app/components/List/List.js b/app/components/List/List.js new file mode 100644 index 00000000..892580d6 --- /dev/null +++ b/app/components/List/List.js @@ -0,0 +1,10 @@ +// @flow +import styled from 'styled-components'; + +const List = styled.ol` + margin: 0; + padding: 0; + list-style: none; +`; + +export default List; diff --git a/app/components/List/index.js b/app/components/List/index.js new file mode 100644 index 00000000..01a19157 --- /dev/null +++ b/app/components/List/index.js @@ -0,0 +1,3 @@ +// @flow +import List from './List'; +export default List; diff --git a/app/components/Sidebar/Settings.js b/app/components/Sidebar/Settings.js index db92804b..99ffd5ce 100644 --- a/app/components/Sidebar/Settings.js +++ b/app/components/Sidebar/Settings.js @@ -1,7 +1,13 @@ // @flow import * as React from 'react'; import { observer, inject } from 'mobx-react'; -import { ProfileIcon, SettingsIcon, CodeIcon, UserIcon } from 'outline-icons'; +import { + ProfileIcon, + SettingsIcon, + CodeIcon, + UserIcon, + LinkIcon, +} from 'outline-icons'; import Flex from 'shared/components/Flex'; import Sidebar, { Section } from './Sidebar'; @@ -51,6 +57,9 @@ class SettingsSidebar extends React.Component { }> Users + }> + Share Links + } diff --git a/app/index.js b/app/index.js index 394ee5be..57e36b74 100644 --- a/app/index.js +++ b/app/index.js @@ -23,6 +23,7 @@ import Search from 'scenes/Search'; import Settings from 'scenes/Settings'; import Users from 'scenes/Settings/Users'; import Slack from 'scenes/Settings/Slack'; +import Shares from 'scenes/Settings/Shares'; import Tokens from 'scenes/Settings/Tokens'; import SlackAuth from 'scenes/SlackAuth'; import ErrorAuth from 'scenes/ErrorAuth'; @@ -77,6 +78,7 @@ if (element) { + { + componentDidMount() { + this.props.shares.fetchPage({ limit: 100 }); + } + + render() { + const { shares } = this.props; + + return ( + + +

Share Links

+ + {shares.data.map(share => ( + + Created{' '} + {' '} + ago by {share.createdBy.name} + + } + actions={ + + + + {' '} + + + } + /> + ))} + +
+ ); + } +} + +export default inject('shares')(Shares); diff --git a/app/stores/SharesStore.js b/app/stores/SharesStore.js new file mode 100644 index 00000000..9086c60a --- /dev/null +++ b/app/stores/SharesStore.js @@ -0,0 +1,43 @@ +// @flow +import { observable, action, runInAction } from 'mobx'; +import invariant from 'invariant'; +import { client } from 'utils/ApiClient'; +import type { Share, PaginationParams } from 'types'; + +class SharesStore { + @observable data: Share[] = []; + @observable isFetching: boolean = false; + @observable isSaving: boolean = false; + + @action + fetchPage = async (options: ?PaginationParams): Promise<*> => { + this.isFetching = true; + + try { + const res = await client.post('/shares.list', options); + invariant(res && res.data, 'Data should be available'); + const { data } = res; + + runInAction('fetchShares', () => { + this.data = data; + }); + } catch (e) { + console.error('Something went wrong'); + } + this.isFetching = false; + }; + + @action + deleteShare = async (id: string) => { + try { + await client.post('/shares.delete', { id }); + runInAction('deleteShare', () => { + this.fetchPage(); + }); + } catch (e) { + console.error('Something went wrong'); + } + }; +} + +export default SharesStore; diff --git a/app/stores/index.js b/app/stores/index.js index f175941d..68d49ca9 100644 --- a/app/stores/index.js +++ b/app/stores/index.js @@ -3,6 +3,7 @@ import AuthStore from './AuthStore'; import UiStore from './UiStore'; import ErrorsStore from './ErrorsStore'; import DocumentsStore from './DocumentsStore'; +import SharesStore from './SharesStore'; const ui = new UiStore(); const errors = new ErrorsStore(); @@ -12,6 +13,7 @@ const stores = { ui, errors, documents: new DocumentsStore({ ui, errors }), + shares: new SharesStore(), }; export default stores; diff --git a/app/types/index.js b/app/types/index.js index 5c5a4dad..4fff4c09 100644 --- a/app/types/index.js +++ b/app/types/index.js @@ -9,6 +9,15 @@ export type User = { isSuspended?: boolean, }; +export type Share = { + id: string, + url: string, + documentTitle: string, + createdBy: User, + createdAt: string, + updatedAt: string, +}; + export type Team = { id: string, name: string, diff --git a/server/emails/components/Button.js b/server/emails/components/Button.js index fbdb1af3..6b81faa5 100644 --- a/server/emails/components/Button.js +++ b/server/emails/components/Button.js @@ -15,5 +15,9 @@ export default (props: Props) => { cursor: 'pointer', }; - return {props.children}; + return ( + + {props.children} + + ); }; diff --git a/server/presenters/share.js b/server/presenters/share.js index 30977cde..c0a06e9e 100644 --- a/server/presenters/share.js +++ b/server/presenters/share.js @@ -5,9 +5,9 @@ import { presentUser } from '.'; function present(ctx: Object, share: Share) { return { id: share.id, - user: presentUser(ctx, share.user), documentTitle: share.document.title, url: `${process.env.URL}/share/${share.id}`, + createdBy: presentUser(ctx, share.user), createdAt: share.createdAt, updatedAt: share.updatedAt, };