From 47fb9680096299ca5285e215678323b844b3df0d Mon Sep 17 00:00:00 2001 From: Tom Moor Date: Wed, 23 May 2018 22:09:14 -0700 Subject: [PATCH] Ability to revoke, ShareMenu --- app/components/List/Item.js | 2 +- app/menus/ShareMenu.js | 54 +++++++++++++++++++ .../Settings/components => menus}/UserMenu.js | 38 +++++++------ app/scenes/Settings/Shares.js | 31 ++--------- app/scenes/Settings/Users.js | 6 +-- .../Settings/components/ShareListItem.js | 31 +++++++++++ app/stores/SharesStore.js | 22 +++++--- app/types/index.js | 1 + server/api/shares.js | 3 +- server/policies/share.js | 2 +- server/presenters/share.js | 1 + 11 files changed, 129 insertions(+), 62 deletions(-) create mode 100644 app/menus/ShareMenu.js rename app/{scenes/Settings/components => menus}/UserMenu.js (69%) create mode 100644 app/scenes/Settings/components/ShareListItem.js diff --git a/app/components/List/Item.js b/app/components/List/Item.js index 7fd7bfaad..a3badbc12 100644 --- a/app/components/List/Item.js +++ b/app/components/List/Item.js @@ -50,7 +50,7 @@ const Subtitle = styled.p` `; const Actions = styled.div` - align-self: flex-end; + align-self: center; `; export default ListItem; diff --git a/app/menus/ShareMenu.js b/app/menus/ShareMenu.js new file mode 100644 index 000000000..67c3b18f0 --- /dev/null +++ b/app/menus/ShareMenu.js @@ -0,0 +1,54 @@ +// @flow +import * as React from 'react'; +import { withRouter } from 'react-router-dom'; +import { inject } from 'mobx-react'; +import { MoreIcon } from 'outline-icons'; + +import { Share } from 'types'; +import CopyToClipboard from 'components/CopyToClipboard'; +import SharesStore from 'stores/SharesStore'; +import { DropdownMenu, DropdownMenuItem } from 'components/DropdownMenu'; + +type Props = { + label?: React.Node, + onOpen?: () => *, + onClose?: () => *, + history: Object, + shares: SharesStore, + share: Share, +}; + +class ShareMenu extends React.Component { + onGoToDocument = (ev: SyntheticEvent<*>) => { + ev.preventDefault(); + this.props.history.push(this.props.share.documentUrl); + }; + + onRevoke = (ev: SyntheticEvent<*>) => { + ev.preventDefault(); + this.props.shares.revoke(this.props.share); + }; + + render() { + const { share, label, onOpen, onClose } = this.props; + + return ( + } + onOpen={onOpen} + onClose={onClose} + > + + Copy link + + + Go to document + +
+ Revoke link +
+ ); + } +} + +export default withRouter(inject('shares')(ShareMenu)); diff --git a/app/scenes/Settings/components/UserMenu.js b/app/menus/UserMenu.js similarity index 69% rename from app/scenes/Settings/components/UserMenu.js rename to app/menus/UserMenu.js index f833f921f..a3b90f354 100644 --- a/app/scenes/Settings/components/UserMenu.js +++ b/app/menus/UserMenu.js @@ -61,29 +61,27 @@ class UserMenu extends React.Component { const { user } = this.props; return ( - - }> - {!user.isSuspended && - (user.isAdmin ? ( - - Make {user.name} a member… - - ) : ( - - Make {user.name} an admin… - - ))} - {user.isSuspended ? ( - - Activate account + }> + {!user.isSuspended && + (user.isAdmin ? ( + + Make {user.name} a member… ) : ( - - Suspend account… + + Make {user.name} an admin… - )} - - + ))} + {user.isSuspended ? ( + + Activate account + + ) : ( + + Suspend account… + + )} + ); } } diff --git a/app/scenes/Settings/Shares.js b/app/scenes/Settings/Shares.js index 7b2b929a3..28139353c 100644 --- a/app/scenes/Settings/Shares.js +++ b/app/scenes/Settings/Shares.js @@ -2,12 +2,9 @@ import * as React from 'react'; import { observer, inject } from 'mobx-react'; import SharesStore from 'stores/SharesStore'; -import distanceInWordsToNow from 'date-fns/distance_in_words_to_now'; -import CopyToClipboard from 'components/CopyToClipboard'; -import Button from 'components/Button'; +import ShareListItem from './components/ShareListItem'; import List from 'components/List'; -import ListItem from 'components/List/Item'; import CenteredContent from 'components/CenteredContent'; import PageTitle from 'components/PageTitle'; @@ -29,30 +26,8 @@ class Shares extends React.Component {

Share Links

- {shares.data.map(share => ( - - Created{' '} - {' '} - ago by {share.createdBy.name} - - } - actions={ - - - - {' '} - - - } - /> + {shares.orderedData.map(share => ( + ))} diff --git a/app/scenes/Settings/Users.js b/app/scenes/Settings/Users.js index e17b7cae1..fa63ca17c 100644 --- a/app/scenes/Settings/Users.js +++ b/app/scenes/Settings/Users.js @@ -7,17 +7,15 @@ import Flex from 'shared/components/Flex'; import Avatar from 'components/Avatar'; import { color } from 'shared/styles/constants'; +import UserMenu from 'menus/UserMenu'; import AuthStore from 'stores/AuthStore'; -import ErrorsStore from 'stores/ErrorsStore'; import UsersStore from 'stores/UsersStore'; import CenteredContent from 'components/CenteredContent'; import LoadingPlaceholder from 'components/LoadingPlaceholder'; import PageTitle from 'components/PageTitle'; -import UserMenu from './components/UserMenu'; type Props = { auth: AuthStore, - errors: ErrorsStore, users: UsersStore, }; @@ -105,4 +103,4 @@ const Badge = styled.span` font-weight: normal; `; -export default inject('auth', 'errors', 'users')(Users); +export default inject('auth', 'users')(Users); diff --git a/app/scenes/Settings/components/ShareListItem.js b/app/scenes/Settings/components/ShareListItem.js new file mode 100644 index 000000000..c78b950c1 --- /dev/null +++ b/app/scenes/Settings/components/ShareListItem.js @@ -0,0 +1,31 @@ +// @flow +import * as React from 'react'; +import distanceInWordsToNow from 'date-fns/distance_in_words_to_now'; +import ShareMenu from 'menus/ShareMenu'; +import ListItem from 'components/List/Item'; +import type { Share } from '../../../types'; + +type Props = { + share: Share, +}; + +const ShareListItem = ({ share }: Props) => { + return ( + + Shared{' '} + {' '} + ago by {share.createdBy.name} + + } + actions={} + /> + ); +}; + +export default ShareListItem; diff --git a/app/stores/SharesStore.js b/app/stores/SharesStore.js index 9086c60ad..dcabd8837 100644 --- a/app/stores/SharesStore.js +++ b/app/stores/SharesStore.js @@ -1,14 +1,20 @@ // @flow -import { observable, action, runInAction } from 'mobx'; +import _ from 'lodash'; +import { observable, action, runInAction, ObservableMap, computed } from 'mobx'; import invariant from 'invariant'; import { client } from 'utils/ApiClient'; import type { Share, PaginationParams } from 'types'; class SharesStore { - @observable data: Share[] = []; + @observable data: Map = new ObservableMap([]); @observable isFetching: boolean = false; @observable isSaving: boolean = false; + @computed + get orderedData(): Share[] { + return _.sortBy(this.data.values(), 'createdAt').reverse(); + } + @action fetchPage = async (options: ?PaginationParams): Promise<*> => { this.isFetching = true; @@ -19,7 +25,9 @@ class SharesStore { const { data } = res; runInAction('fetchShares', () => { - this.data = data; + data.forEach(share => { + this.data.set(share.id, share); + }); }); } catch (e) { console.error('Something went wrong'); @@ -28,11 +36,11 @@ class SharesStore { }; @action - deleteShare = async (id: string) => { + revoke = async (share: Share) => { try { - await client.post('/shares.delete', { id }); - runInAction('deleteShare', () => { - this.fetchPage(); + await client.post('/shares.delete', { id: share.id }); + runInAction('revoke', () => { + this.data.delete(share.id); }); } catch (e) { console.error('Something went wrong'); diff --git a/app/types/index.js b/app/types/index.js index 4fff4c099..784a8e433 100644 --- a/app/types/index.js +++ b/app/types/index.js @@ -13,6 +13,7 @@ export type Share = { id: string, url: string, documentTitle: string, + documentUrl: string, createdBy: User, createdAt: string, updatedAt: string, diff --git a/server/api/shares.js b/server/api/shares.js index 16cfd7edc..471ac0db8 100644 --- a/server/api/shares.js +++ b/server/api/shares.js @@ -65,8 +65,9 @@ router.post('shares.delete', auth(), async ctx => { const { id } = ctx.body; ctx.assertPresent(id, 'id is required'); + const user = ctx.state.user; const share = await Share.findById(id); - authorize(ctx.state.user, 'delete', share); + authorize(user, 'delete', share); await share.destroy(); diff --git a/server/policies/share.js b/server/policies/share.js index 9872103d8..3e2d1121b 100644 --- a/server/policies/share.js +++ b/server/policies/share.js @@ -9,7 +9,7 @@ allow(User, ['read'], Share, (user, share) => user.teamId === share.teamId); allow(User, ['update'], Share, (user, share) => false); allow(User, ['delete'], Share, (user, share) => { if (!share || user.teamId !== share.teamId) return false; - if (user.id === share.userId) return false; + if (user.id === share.userId) return true; if (user.isAdmin) return true; throw new AdminRequiredError(); }); diff --git a/server/presenters/share.js b/server/presenters/share.js index c0a06e9e9..1de3825ca 100644 --- a/server/presenters/share.js +++ b/server/presenters/share.js @@ -6,6 +6,7 @@ function present(ctx: Object, share: Share) { return { id: share.id, documentTitle: share.document.title, + documentUrl: share.document.getUrl(), url: `${process.env.URL}/share/${share.id}`, createdBy: presentUser(ctx, share.user), createdAt: share.createdAt,