feat: Add groups and group permissions (#1204)

* WIP - got one API test to pass yay

* adds group update endpoint

* added group policies

* adds groups.list API

* adds groups.info

* remove comment

* WIP

* tests for delete

* adds group membership list

* adds tests for groups list

* add and remove user endpoints for group

* ask some questions

* fix up some issues around primary keys

* remove export from group permissions

Co-Authored-By: Tom Moor <tom.moor@gmail.com>

* remove random file

* only create events on actual updates, add tests to ensure

* adds uniqueness validation to group name

* throw validation errors on model and let it pass through the controller

* fix linting

* WIP

* WIP

* WIP

* WIP

* WIP basic edit and delete

* basic CRUD for groups and memberships in place

* got member counts working

* add member count and limit the number of users sent over teh wire to 6

* factor avatar with AvatarWithPresence into its own class

* wip

* WIP avatars in group lists

* WIP collection groups

* add and remove group endpoints

* wip add collection groups

* wip get group adding to collections to work

* wip get updating collection group memberships to work

* wip get new group modal working

* add tests for collection index

* include collection groups in the withmemberships scope

* tie permissions to group memberships

* remove unused import

* Update app/components/GroupListItem.js

update title copy

Co-Authored-By: Tom Moor <tom.moor@gmail.com>

* Update server/migrations/20191211044318-create-groups.js

Co-Authored-By: Tom Moor <tom.moor@gmail.com>

* Update server/api/groups.js

Co-Authored-By: Tom Moor <tom.moor@gmail.com>

* Update server/api/groups.js

Co-Authored-By: Tom Moor <tom.moor@gmail.com>

* Update app/menus/CollectionMenu.js

Co-Authored-By: Tom Moor <tom.moor@gmail.com>

* Update server/models/Group.js

Co-Authored-By: Tom Moor <tom.moor@gmail.com>

* minor fixes

* Update app/scenes/CollectionMembers/AddGroupsToCollection.js

Co-Authored-By: Tom Moor <tom.moor@gmail.com>

* Update app/menus/GroupMenu.js

Co-Authored-By: Tom Moor <tom.moor@gmail.com>

* Update app/menus/GroupMenu.js

Co-Authored-By: Tom Moor <tom.moor@gmail.com>

* Update app/menus/GroupMenu.js

Co-Authored-By: Tom Moor <tom.moor@gmail.com>

* Update app/scenes/Collection.js

Co-Authored-By: Tom Moor <tom.moor@gmail.com>

* Update app/scenes/CollectionMembers/CollectionMembers.js

Co-Authored-By: Tom Moor <tom.moor@gmail.com>

* Update app/scenes/GroupNew.js

Co-Authored-By: Tom Moor <tom.moor@gmail.com>

* Update app/scenes/GroupNew.js

Co-Authored-By: Tom Moor <tom.moor@gmail.com>

* Update app/scenes/Settings/Groups.js

Co-Authored-By: Tom Moor <tom.moor@gmail.com>

* Update server/api/documents.js

Co-Authored-By: Tom Moor <tom.moor@gmail.com>

* Update app/scenes/CollectionMembers/components/CollectionGroupMemberListItem.js

Co-Authored-By: Tom Moor <tom.moor@gmail.com>

* address comments

* WIP - getting websocket stuff up and running

* socket event for group deletion

* wrapped up cascading deletes

* lint

* flow

* fix: UI feedback

* fix: Facepile size

* fix: Lots of missing await's

* Allow clicking facepile on group list item to open members

* remove unused route push, grammar

* fix: Remove bad analytics events
feat: Add group events to audit log

* collection. -> collections.

* Add groups to entity websocket events (sync create/update/delete) between clients

* fix: Users should not be able to see groups they are not a member of

* fix: Not caching errors in UI when changing group memberships

* fix: Hide unusable UI

* test

* fix: Tweak language

* feat: Automatically open 'add member' modal after creating group

Co-authored-by: Tom Moor <tom.moor@gmail.com>
This commit is contained in:
Nan Yu
2020-03-14 20:48:32 -07:00
committed by GitHub
parent 6c451a34d4
commit 142303b3de
81 changed files with 4259 additions and 257 deletions

View File

@ -1,22 +1,14 @@
// @flow
import * as React from 'react';
import { observable } from 'mobx';
import { observer, inject } from 'mobx-react';
import { sortBy } from 'lodash';
import styled, { withTheme } from 'styled-components';
import distanceInWordsToNow from 'date-fns/distance_in_words_to_now';
import { sortBy, keyBy } from 'lodash';
import { MAX_AVATAR_DISPLAY } from 'shared/constants';
import Flex from 'shared/components/Flex';
import Avatar from 'components/Avatar';
import Tooltip from 'components/Tooltip';
import { AvatarWithPresence } from 'components/Avatar';
import Facepile from 'components/Facepile';
import Document from 'models/Document';
import User from 'models/User';
import UserProfile from 'scenes/UserProfile';
import ViewsStore from 'stores/ViewsStore';
import DocumentPresenceStore from 'stores/DocumentPresenceStore';
import { EditIcon } from 'outline-icons';
const MAX_DISPLAY = 6;
type Props = {
views: ViewsStore,
@ -25,66 +17,6 @@ type Props = {
currentUserId: string,
};
@observer
class AvatarWithPresence extends React.Component<{
user: User,
isPresent: boolean,
isEditing: boolean,
isCurrentUser: boolean,
lastViewedAt: string,
}> {
@observable isOpen: boolean = false;
handleOpenProfile = () => {
this.isOpen = true;
};
handleCloseProfile = () => {
this.isOpen = false;
};
render() {
const {
user,
lastViewedAt,
isPresent,
isEditing,
isCurrentUser,
} = this.props;
return (
<React.Fragment>
<Tooltip
tooltip={
<Centered>
<strong>{user.name}</strong> {isCurrentUser && '(You)'}
<br />
{isPresent
? isEditing ? 'currently editing' : 'currently viewing'
: `viewed ${distanceInWordsToNow(new Date(lastViewedAt))} ago`}
</Centered>
}
placement="bottom"
>
<AvatarWrapper isPresent={isPresent}>
<Avatar
src={user.avatarUrl}
onClick={this.handleOpenProfile}
size={32}
icon={isEditing ? <EditIcon size={16} color="#FFF" /> : undefined}
/>
</AvatarWrapper>
</Tooltip>
<UserProfile
user={user}
isOpen={this.isOpen}
onRequestClose={this.handleCloseProfile}
/>
</React.Fragment>
);
}
}
@observer
class Collaborators extends React.Component<Props> {
componentDidMount() {
@ -93,33 +25,37 @@ class Collaborators extends React.Component<Props> {
render() {
const { document, presence, views, currentUserId } = this.props;
const documentViews = views.inDocument(document.id);
let documentPresence = presence.get(document.id);
documentPresence = documentPresence
? Array.from(documentPresence.values())
: [];
const documentViews = views.inDocument(document.id);
const presentIds = documentPresence.map(p => p.userId);
const editingIds = documentPresence
.filter(p => p.isEditing)
.map(p => p.userId);
// only show the most recent viewers, the rest can overflow
let mostRecentViewers = documentViews.slice(0, MAX_DISPLAY);
// ensure currently present via websocket are always ordered first
mostRecentViewers = sortBy(mostRecentViewers, view =>
presentIds.includes(view.user.id)
const mostRecentViewers = sortBy(
documentViews.slice(0, MAX_AVATAR_DISPLAY),
view => {
return presentIds.includes(view.user.id);
}
);
// if there are too many to display then add a (+X) to the UI
const viewersKeyedByUserId = keyBy(mostRecentViewers, v => v.user.id);
const overflow = documentViews.length - mostRecentViewers.length;
return (
<Avatars>
{overflow > 0 && <More>+{overflow}</More>}
{mostRecentViewers.map(({ lastViewedAt, user }) => {
<Facepile
users={mostRecentViewers.map(v => v.user)}
overflow={overflow}
renderAvatar={user => {
const isPresent = presentIds.includes(user.id);
const isEditing = editingIds.includes(user.id);
const { lastViewedAt } = viewersKeyedByUserId[user.id];
return (
<AvatarWithPresence
@ -131,45 +67,10 @@ class Collaborators extends React.Component<Props> {
isCurrentUser={currentUserId === user.id}
/>
);
})}
</Avatars>
}}
/>
);
}
}
const Centered = styled.div`
text-align: center;
`;
const AvatarWrapper = styled.div`
width: 32px;
height: 32px;
margin-right: -8px;
opacity: ${props => (props.isPresent ? 1 : 0.5)};
transition: opacity 250ms ease-in-out;
&:first-child {
margin-right: 0;
}
`;
const More = styled.div`
min-width: 30px;
height: 24px;
border-radius: 12px;
background: ${props => props.theme.slate};
color: ${props => props.theme.text};
border: 2px solid ${props => props.theme.background};
text-align: center;
line-height: 20px;
font-size: 11px;
font-weight: 600;
`;
const Avatars = styled(Flex)`
align-items: center;
flex-direction: row-reverse;
cursor: pointer;
`;
export default inject('views', 'presence')(withTheme(Collaborators));
export default inject('views', 'presence')(Collaborators);