Compare commits
7 Commits
9cd1ae4628
...
81bed97d42
Author | SHA1 | Date | |
---|---|---|---|
81bed97d42 | |||
402a25d980 | |||
f021914089 | |||
a8a000ae55 | |||
980a5bb14b | |||
8afd7c5694 | |||
17806d410b |
@ -10,6 +10,18 @@ APP_NAME = os.environ.get("APP_NAME")
|
||||
APP_URL = os.environ.get("APP_URL")
|
||||
|
||||
|
||||
def has_group_permission(user, group, permission=None):
|
||||
if not user or not group:
|
||||
return False
|
||||
if user["_id"] in group.get("admins", []):
|
||||
return True
|
||||
if group["_id"] not in user.get("groups", []):
|
||||
return False
|
||||
if permission:
|
||||
return permission in group.get("memberPermissions", [])
|
||||
return False
|
||||
|
||||
|
||||
def create(user, data):
|
||||
if not data:
|
||||
raise util.errors.BadRequest("Invalid request")
|
||||
@ -24,6 +36,13 @@ def create(user, data):
|
||||
"name": data["name"],
|
||||
"description": data.get("description", ""),
|
||||
"closed": data.get("closed", False),
|
||||
"memberPermissions": [
|
||||
"viewMembers",
|
||||
"viewNoticeboard",
|
||||
"postNoticeboard",
|
||||
"viewProjects",
|
||||
"postProjects",
|
||||
],
|
||||
}
|
||||
result = db.groups.insert_one(group)
|
||||
group["_id"] = result.inserted_id
|
||||
@ -43,6 +62,10 @@ def get_one(user, id):
|
||||
group = db.groups.find_one({"_id": id})
|
||||
if not group:
|
||||
raise util.errors.NotFound("Group not found")
|
||||
if group.get("image"):
|
||||
group["imageUrl"] = uploads.get_presigned_url(
|
||||
"groups/{0}/{1}".format(id, group["image"])
|
||||
)
|
||||
group["adminUsers"] = list(
|
||||
db.users.find(
|
||||
{"_id": {"$in": group.get("admins", [])}}, {"username": 1, "avatar": 1}
|
||||
@ -64,7 +87,7 @@ def update(user, id, update):
|
||||
raise util.errors.NotFound("Group not found")
|
||||
if user["_id"] not in group.get("admins", []):
|
||||
raise util.errors.Forbidden("You're not a group admin")
|
||||
allowed_keys = ["name", "description", "closed"]
|
||||
allowed_keys = ["name", "description", "closed", "memberPermissions", "image"]
|
||||
updater = util.build_updater(update, allowed_keys)
|
||||
if updater:
|
||||
db.groups.update_one({"_id": id}, updater)
|
||||
@ -90,11 +113,13 @@ def create_entry(user, id, data):
|
||||
raise util.errors.BadRequest("Invalid request")
|
||||
db = database.get_db()
|
||||
id = ObjectId(id)
|
||||
group = db.groups.find_one({"_id": id}, {"admins": 1, "name": 1})
|
||||
group = db.groups.find_one({"_id": id})
|
||||
if not group:
|
||||
raise util.errors.NotFound("Group not found")
|
||||
if group["_id"] not in user.get("groups", []):
|
||||
raise util.errors.Forbidden("You must be a member to write in the feed")
|
||||
if not has_group_permission(user, group, "postNoticeboard"):
|
||||
raise util.errors.Forbidden("You don't have permission to post in the feed")
|
||||
entry = {
|
||||
"createdAt": datetime.datetime.now(),
|
||||
"group": id,
|
||||
@ -158,11 +183,13 @@ def create_entry(user, id, data):
|
||||
def get_entries(user, id):
|
||||
db = database.get_db()
|
||||
id = ObjectId(id)
|
||||
group = db.groups.find_one({"_id": id}, {"admins": 1})
|
||||
group = db.groups.find_one({"_id": id})
|
||||
if not group:
|
||||
raise util.errors.NotFound("Group not found")
|
||||
if id not in user.get("groups", []):
|
||||
raise util.errors.BadRequest("You're not a member of this group")
|
||||
if not has_group_permission(user, group, "viewNoticeboard"):
|
||||
raise util.errors.Forbidden("You don't have permission to view the feed")
|
||||
entries = list(
|
||||
db.groupEntries.find({"group": id}).sort("createdAt", pymongo.DESCENDING)
|
||||
)
|
||||
@ -211,7 +238,7 @@ def create_entry_reply(user, id, entry_id, data):
|
||||
db = database.get_db()
|
||||
id = ObjectId(id)
|
||||
entry_id = ObjectId(entry_id)
|
||||
group = db.groups.find_one({"_id": id}, {"admins": 1, "name": 1})
|
||||
group = db.groups.find_one({"_id": id})
|
||||
if not group:
|
||||
raise util.errors.NotFound("Group not found")
|
||||
entry = db.groupEntries.find_one({"_id": entry_id})
|
||||
@ -219,6 +246,8 @@ def create_entry_reply(user, id, entry_id, data):
|
||||
raise util.errors.NotFound("Entry to reply to not found")
|
||||
if group["_id"] not in user.get("groups", []):
|
||||
raise util.errors.Forbidden("You must be a member to write in the feed")
|
||||
if not has_group_permission(user, group, "postNoticeboard"):
|
||||
raise util.errors.Forbidden("You don't have permission to post in the feed")
|
||||
reply = {
|
||||
"createdAt": datetime.datetime.now(),
|
||||
"group": id,
|
||||
@ -346,11 +375,13 @@ def create_member(user, id, user_id, invited=False):
|
||||
def get_members(user, id):
|
||||
db = database.get_db()
|
||||
id = ObjectId(id)
|
||||
group = db.groups.find_one({"_id": id}, {"admins": 1})
|
||||
group = db.groups.find_one({"_id": id})
|
||||
if not group:
|
||||
raise util.errors.NotFound("Group not found")
|
||||
if id not in user.get("groups", []) and "root" not in user.get("roles", []):
|
||||
raise util.errors.Forbidden("You need to be a member to see the member list")
|
||||
if not has_group_permission(user, group, "viewMembers"):
|
||||
raise util.errors.Forbidden("You don't have permission to view the member list")
|
||||
members = list(
|
||||
db.users.find(
|
||||
{"groups": id}, {"username": 1, "avatar": 1, "bio": 1, "groups": 1}
|
||||
@ -385,14 +416,52 @@ def delete_member(user, id, user_id):
|
||||
return {"deletedMember": user_id}
|
||||
|
||||
|
||||
def create_admin(user, id, user_id):
|
||||
id = ObjectId(id)
|
||||
user_id = ObjectId(user_id)
|
||||
db = database.get_db()
|
||||
group = db.groups.find_one({"_id": id}, {"admins": 1})
|
||||
if not group:
|
||||
raise util.errors.NotFound("Group not found")
|
||||
if user["_id"] not in group.get("admins", []):
|
||||
raise util.errors.Forbidden("You can't add this admin")
|
||||
if user_id in group.get("admins", []):
|
||||
raise util.errors.Forbidden("This user is already an admin")
|
||||
db.groups.update_one({"_id": id}, {"$addToSet": {"admins": user_id}})
|
||||
return {"createdAdmin": user_id}
|
||||
|
||||
|
||||
def delete_admin(user, id, user_id):
|
||||
id = ObjectId(id)
|
||||
user_id = ObjectId(user_id)
|
||||
db = database.get_db()
|
||||
group = db.groups.find_one({"_id": id}, {"admins": 1})
|
||||
if not group:
|
||||
raise util.errors.NotFound("Group not found")
|
||||
if user_id != user["_id"] and user["_id"] not in group.get("admins", []):
|
||||
raise util.errors.Forbidden("You can't remove this admin")
|
||||
if user_id not in group.get("admins", []):
|
||||
raise util.errors.Forbidden("This user is not an admin")
|
||||
if len(group["admins"]) == 1:
|
||||
raise util.errors.Forbidden(
|
||||
"There needs to be at least one admin in this group"
|
||||
)
|
||||
db.groups.update_one({"_id": id}, {"$pull": {"admins": user_id}})
|
||||
return {"deletedAdmin": user_id}
|
||||
|
||||
|
||||
def get_projects(user, id):
|
||||
db = database.get_db()
|
||||
id = ObjectId(id)
|
||||
group = db.groups.find_one({"_id": id}, {"admins": 1})
|
||||
group = db.groups.find_one({"_id": id})
|
||||
if not group:
|
||||
raise util.errors.NotFound("Group not found")
|
||||
if id not in user.get("groups", []):
|
||||
raise util.errors.Forbidden("You need to be a member to see the project list")
|
||||
if not has_group_permission(user, group, "viewProjects"):
|
||||
raise util.errors.Forbidden(
|
||||
"You don't have permission to view the project list"
|
||||
)
|
||||
projects = list(
|
||||
db.projects.find(
|
||||
{"groupVisibility": id},
|
||||
|
@ -1,4 +1,5 @@
|
||||
import datetime
|
||||
import re
|
||||
from bson.objectid import ObjectId
|
||||
from util import database, util
|
||||
from api import uploads
|
||||
@ -99,6 +100,10 @@ def update(user, username, data):
|
||||
if "username" in data:
|
||||
if not data.get("username") or len(data["username"]) < 3:
|
||||
raise util.errors.BadRequest("New username is not valid")
|
||||
if not re.match("^[a-z0-9_]+$", data["username"]):
|
||||
raise util.errors.BadRequest(
|
||||
"Usernames can only contain letters, numbers, and underscores"
|
||||
)
|
||||
if db.users.count_documents({"username": data["username"].lower()}):
|
||||
raise util.errors.BadRequest("A user with this username already exists")
|
||||
data["username"] = data["username"].lower()
|
||||
|
14
api/app.py
14
api/app.py
@ -478,6 +478,8 @@ def group_route(id):
|
||||
"name": fields.Str(),
|
||||
"description": fields.Str(),
|
||||
"closed": fields.Bool(),
|
||||
"memberPermissions": fields.List(fields.Str()),
|
||||
"image": fields.Str(allow_none=True),
|
||||
}
|
||||
)
|
||||
def group_route_put(args, id):
|
||||
@ -542,6 +544,18 @@ def group_member_route(id, user_id):
|
||||
)
|
||||
|
||||
|
||||
@app.route("/groups/<id>/admins/<user_id>", methods=["PUT", "DELETE"])
|
||||
def group_admin_route(id, user_id):
|
||||
if request.method == "DELETE":
|
||||
return util.jsonify(
|
||||
groups.delete_admin(util.get_user(required=True), id, user_id)
|
||||
)
|
||||
if request.method == "PUT":
|
||||
return util.jsonify(
|
||||
groups.create_admin(util.get_user(required=True), id, user_id)
|
||||
)
|
||||
|
||||
|
||||
@app.route("/groups/<id>/projects", methods=["GET"])
|
||||
def group_projects_route(id):
|
||||
return util.jsonify(groups.get_projects(util.get_user(required=True), id))
|
||||
|
@ -57,5 +57,11 @@ export const groups = {
|
||||
},
|
||||
deleteInvitation (id, inviteId, success, fail) {
|
||||
api.authenticatedRequest('DELETE', `/groups/${id}/invitations/${inviteId}`, null, success, fail)
|
||||
},
|
||||
createAdmin (id, userId, success, fail) {
|
||||
api.authenticatedRequest('PUT', `/groups/${id}/admins/${userId}`, null, success, fail)
|
||||
},
|
||||
deleteAdmin (id, userId, success, fail) {
|
||||
api.authenticatedRequest('DELETE', `/groups/${id}/admins/${userId}`, null, success, fail)
|
||||
}
|
||||
}
|
||||
|
@ -84,7 +84,7 @@ const FeedMessage = connect(
|
||||
</div>}
|
||||
{!post.inReplyTo &&
|
||||
<div style={{ padding: 10 }}>
|
||||
{replyingTo !== post._id && !post.inReplyTo && onReplyPosted &&
|
||||
{utils.hasGroupPermission(user, group, 'postNoticeboard') && replyingTo !== post._id && !post.inReplyTo && onReplyPosted &&
|
||||
<Button size='mini' basic primary icon='reply' content='Write a reply' onClick={() => updateReplyingTo(post._id)} />}
|
||||
{post.user === user?._id && !utils.hasSubscription(user, 'messages.replied') && onReplyPosted &&
|
||||
<Button size='mini' basic icon='envelope' content='Get notified if someone replies' as={Link} to='/settings/notifications' />}
|
||||
|
@ -29,8 +29,8 @@ const Badge = styled.div`
|
||||
left: -8px;
|
||||
`
|
||||
const Username = styled.span`
|
||||
font-size: ${props => props.compact ? 10 : 12}px;
|
||||
margin-left: 5px;
|
||||
font-size: ${props => props.compact ? 12 : 14}px;
|
||||
margin-left: 3px;
|
||||
color: black;
|
||||
`
|
||||
|
||||
|
@ -2,6 +2,7 @@ import React, { useEffect } from 'react'
|
||||
import { Loader, Button, Segment } from 'semantic-ui-react'
|
||||
import { useSelector, useDispatch } from 'react-redux'
|
||||
import { useParams } from 'react-router-dom'
|
||||
import { toast } from 'react-toastify'
|
||||
import utils from '../../../utils/utils.js'
|
||||
import actions from '../../../actions'
|
||||
import api from '../../../api'
|
||||
@ -34,26 +35,42 @@ function Feed () {
|
||||
})
|
||||
}, [dispatch, id, myGroups.length])
|
||||
|
||||
const toggleEmailSub = (key, enable) => {
|
||||
if (enable) { api.users.createEmailSubscription(user.username, key, ({ subscriptions }) => dispatch(actions.users.updateSubscriptions(user._id, subscriptions)), err => toast.error(err.message)) } else { api.users.deleteEmailSubscription(user.username, key, ({ subscriptions }) => dispatch(actions.users.updateSubscriptions(user._id, subscriptions)), err => toast.error(err.message)) }
|
||||
}
|
||||
|
||||
const mainEntries = entries && entries.filter(e => !e.inReplyTo)
|
||||
|
||||
return (
|
||||
<div>
|
||||
{utils.isInGroup(user, group._id) &&
|
||||
{utils.hasGroupPermission(user, group, 'viewNoticeboard') &&
|
||||
<>
|
||||
<div style={{ display: 'flex', justifyContent: 'end', marginBottom: 10 }}>
|
||||
{utils.hasEmailSubscription(user, `groupFeed-${group._id}`)
|
||||
? <Button color='blue' basic size='tiny' icon='check' content='Subscribed to email updates' onClick={e => toggleEmailSub(`groupFeed-${group._id}`, false)} />
|
||||
: <Button color='blue' size='tiny' icon='rss' content='Subscribe to updates' onClick={e => toggleEmailSub(`groupFeed-${group._id}`, true)} />}
|
||||
</div>
|
||||
|
||||
{utils.hasGroupPermission(user, group, 'postNoticeboard') &&
|
||||
<>
|
||||
{replyingTo
|
||||
? <Button style={{ marginBottom: 20 }} color='teal' content='Write a new post' onClick={() => dispatch(actions.posts.updateReplyingTo(null))} />
|
||||
: <NewFeedMessage user={user} group={group} forType='group' onPosted={e => dispatch(actions.groups.receiveEntry(e))} />}
|
||||
</>}
|
||||
|
||||
{loadingEntries && !mainEntries?.length &&
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<Loader inline='centered' active />
|
||||
<p style={{ marginTop: 20 }}><strong>Loading the notice board. Hold tight...</strong></p>
|
||||
</div>}
|
||||
|
||||
{!loadingEntries && !mainEntries?.length &&
|
||||
<Segment placeholder textAlign='center'>
|
||||
<img src={MessagesImage} alt='Messages' style={{ display: 'block', margin: '0px auto', maxWidth: 300 }} />
|
||||
<h2>No posts yet</h2>
|
||||
<p>Be the first here by writing a new post.</p>
|
||||
</Segment>}
|
||||
|
||||
{mainEntries?.map(e =>
|
||||
<FeedMessage key={e._id} user={user} forType='group' group={group} post={e} replies={entries.filter(r => r.inReplyTo === e._id)} onDeleted={id => dispatch(actions.groups.deleteEntry(id))} onReplyPosted={e => dispatch(actions.groups.receiveEntry(e))} />
|
||||
)}
|
||||
|
@ -1,6 +1,6 @@
|
||||
import React, { useEffect } from 'react'
|
||||
import { Helmet } from 'react-helmet'
|
||||
import { Segment, Loader, Menu, Message, Container, Button, Icon, Grid, Card } from 'semantic-ui-react'
|
||||
import { Segment, Loader, Menu, Message, Container, Button, Icon, Grid, Card, Image } from 'semantic-ui-react'
|
||||
import { Outlet, Link, useParams } from 'react-router-dom'
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
import { toast } from 'react-toastify'
|
||||
@ -15,10 +15,7 @@ function Group () {
|
||||
const { id } = useParams()
|
||||
const dispatch = useDispatch()
|
||||
const { user, group, loading, errorMessage, requests, myRequests, invitations } = useSelector(state => {
|
||||
let group
|
||||
state.groups.groups.forEach((g) => {
|
||||
if (g._id === id) group = g
|
||||
})
|
||||
const group = state.groups.groups.filter(g => g._id === id)[0]
|
||||
const user = state.users.users.filter(u => state.auth.currentUserId === u._id)[0]
|
||||
const requests = state.invitations.invitations.filter(i => i.recipientGroup === group?._id)
|
||||
const myRequests = state.invitations.invitations.filter(i => i.recipientGroup === group?._id && i.user === user?._id)
|
||||
@ -44,9 +41,6 @@ function Group () {
|
||||
}, err => toast.error(err.message))
|
||||
}, () => {})
|
||||
}
|
||||
const toggleEmailSub = (key, enable) => {
|
||||
if (enable) { api.users.createEmailSubscription(user.username, key, ({ subscriptions }) => dispatch(actions.users.updateSubscriptions(user._id, subscriptions)), err => toast.error(err.message)) } else { api.users.deleteEmailSubscription(user.username, key, ({ subscriptions }) => dispatch(actions.users.updateSubscriptions(user._id, subscriptions)), err => toast.error(err.message)) }
|
||||
}
|
||||
|
||||
const requestToJoin = () => {
|
||||
api.groups.createJoinRequest(group._id, invitation => {
|
||||
@ -89,52 +83,55 @@ function Group () {
|
||||
<Grid stackable>
|
||||
<Grid.Column computer={4}>
|
||||
<Card fluid color='yellow'>
|
||||
{group.imageUrl &&
|
||||
<Image src={group.imageUrl} wrapped ui={false} />}
|
||||
{group.description &&
|
||||
<Card.Content>{group.description}</Card.Content>}
|
||||
{group.closed &&
|
||||
<Card.Content>
|
||||
<Card.Meta>
|
||||
<h4 style={{ marginBottom: 2 }}><Icon name='user secret' /> This is a closed group</h4>
|
||||
<p>Members can only join if they are approved or invited by an admin.</p>
|
||||
{requests?.length > 0 && utils.isGroupAdmin(user, group) &&
|
||||
<Button as={Link} to={`/groups/${group._id}/members`} size='tiny' fluid color='teal' icon='user plus' content='Manage join requests' />}
|
||||
{utils.isGroupAdmin(user, group) && !utils.hasSubscription(user, 'groups.joinRequested') &&
|
||||
<Button as={Link} to='/settings/notifications' icon='envelope' size='tiny' fluid basic content='Email me join requests' />}
|
||||
</Card.Meta>
|
||||
</Card.Content>}
|
||||
<Card.Content>
|
||||
<h3>Admins</h3>
|
||||
{group.adminUsers && group.adminUsers.map(a =>
|
||||
<UserChip user={a} key={a._id} />
|
||||
)}
|
||||
</Card.Content>
|
||||
<Card.Content extra>
|
||||
{utils.isInGroup(user, group._id) &&
|
||||
<div>
|
||||
<Button color='yellow' basic size='tiny' fluid icon='check' content='Member' onClick={leave} style={{ marginBottom: 5 }} />
|
||||
{utils.hasEmailSubscription(user, `groupFeed-${group._id}`)
|
||||
? <Button color='blue' basic size='tiny' fluid icon='rss' content='Subscribed' onClick={e => toggleEmailSub(`groupFeed-${group._id}`, false)} data-tooltip="We'll send you emails when people post in this group" />
|
||||
: <Button color='blue' size='tiny' fluid icon='rss' content='Subscribe to updates' onClick={e => toggleEmailSub(`groupFeed-${group._id}`, true)} />}
|
||||
</div>}
|
||||
{!utils.isInGroup(user, group._id) &&
|
||||
(group.closed
|
||||
? <Button disabled={myRequests?.length > 0} color='yellow' size='tiny' fluid icon='user plus' content='Request to join' onClick={requestToJoin} />
|
||||
: <Button color='yellow' size='tiny' fluid icon='user plus' content='Join group' onClick={join} />
|
||||
)}
|
||||
</Card.Content>
|
||||
<Card.Meta>
|
||||
{requests?.length > 0 && utils.isGroupAdmin(user, group) &&
|
||||
<Button as={Link} to={`/groups/${group._id}/members`} size='tiny' fluid color='teal' icon='user plus' content='Manage join requests' />}
|
||||
{utils.isGroupAdmin(user, group) && !utils.hasSubscription(user, 'groups.joinRequested') &&
|
||||
<Button as={Link} to='/settings/notifications' icon='envelope' size='tiny' fluid basic content='Email me join requests' />}
|
||||
</Card.Meta>
|
||||
</Card.Content>}
|
||||
</Card>
|
||||
|
||||
{utils.isInGroup(user, group._id) &&
|
||||
<Menu fluid vertical>
|
||||
<Menu.Item active={utils.activePath('^/groups/[a-zA-Z0-9]+$')} as={Link} to={`/groups/${group._id}`} icon='chat' content='Notice Board' />
|
||||
{utils.isInGroup(user, group._id) &&
|
||||
<Menu.Item active={utils.activePath('members')} as={Link} to={`/groups/${group._id}/members`} icon='user' content='Members' label='3' />}
|
||||
{utils.isInGroup(user, group._id) &&
|
||||
<>
|
||||
{utils.hasGroupPermission(user, group, 'viewMembers') &&
|
||||
<Menu.Item active={utils.activePath('members')} as={Link} to={`/groups/${group._id}/members`} icon='user' content='Members' />}
|
||||
{utils.hasGroupPermission(user, group, 'viewProjects') &&
|
||||
<Menu.Item active={utils.activePath('projects')} as={Link} to={`/groups/${group._id}/projects`} icon='book' content='Projects' />}
|
||||
{utils.isGroupAdmin(user, group) &&
|
||||
<Menu.Item active={utils.activePath('settings')} as={Link} to={`/groups/${group._id}/settings`} icon='settings' content='Settings' />}
|
||||
</>}
|
||||
</Menu>}
|
||||
|
||||
<Card fluid color='yellow'>
|
||||
<Card.Content>
|
||||
<h3>Admins</h3>
|
||||
<div style={{ flexWrap: 'wrap', display: 'flex' }}>
|
||||
{group?.adminUsers?.map(a =>
|
||||
<div style={{ marginRight: 8, marginBottom: 4 }} key={a._id}><UserChip user={a} /></div>
|
||||
)}
|
||||
</div>
|
||||
</Card.Content>
|
||||
</Card>
|
||||
|
||||
<HelpLink link='/docs/groups#a-tour-around-your-new-group' />
|
||||
|
||||
</Grid.Column>
|
||||
|
@ -14,6 +14,7 @@ import UserSearch from '../../includes/UserSearch'
|
||||
|
||||
function Members () {
|
||||
const [invitations, setInvitations] = useState([])
|
||||
const [search, setSearch] = useState('')
|
||||
const joinLinkRef = useRef(null)
|
||||
const { id } = useParams()
|
||||
const dispatch = useDispatch()
|
||||
@ -54,6 +55,19 @@ function Members () {
|
||||
}, err => toast.error(err.message))
|
||||
}, () => {})
|
||||
}
|
||||
const makeUserAdmin = (member, promoting) => {
|
||||
if (promoting) {
|
||||
api.groups.createAdmin(group._id, member._id, () => {
|
||||
toast.success('User is now an admin')
|
||||
dispatch(actions.groups.updateGroup(group._id, { admins: group.admins.concat(member._id), adminUsers: group.adminUsers.concat(member) }))
|
||||
}, err => toast.error(err.message))
|
||||
} else {
|
||||
api.groups.deleteAdmin(group._id, member._id, () => {
|
||||
toast.success('Admin status revoked')
|
||||
dispatch(actions.groups.updateGroup(group._id, { admins: group.admins.filter(a => a !== member._id), adminUsers: group.adminUsers.filter(a => a._id !== member._id) }))
|
||||
}, err => toast.error(err.message))
|
||||
}
|
||||
}
|
||||
const sendInvitation = (user) => {
|
||||
api.groups.createInvitation(group._id, user._id, invitation => {
|
||||
const newInvitations = Object.assign([], invitations)
|
||||
@ -81,6 +95,9 @@ function Members () {
|
||||
err => toast.error(err.message))
|
||||
}
|
||||
|
||||
const memberList = search ? members.filter(m => m?.username?.toLowerCase().includes(search.trim().toLowerCase())) : members
|
||||
const invitationList = search ? invitations.filter(i => i?.recipientUser?.username?.toLowerCase().includes(search.trim().toLowerCase())) : invitations
|
||||
|
||||
return (
|
||||
<div>
|
||||
{loading && (!members || !members.length) && <Loader active inline='centered' />}
|
||||
@ -126,9 +143,11 @@ function Members () {
|
||||
</Table>
|
||||
</Segment>}
|
||||
|
||||
<Input style={{ marginBottom: 20 }} placeholder='Search by username...' value={search} onChange={e => setSearch(e.target.value)} />
|
||||
|
||||
<Card.Group itemsPerRow={3} doubling stackable>
|
||||
{invitations && invitations.map(i =>
|
||||
<Card key={i._id}>
|
||||
{invitationList?.map(i =>
|
||||
<Card key={i._id} style={{ opacity: 0.6 }}>
|
||||
<Card.Content>
|
||||
<UserChip user={i.recipientUser} />
|
||||
</Card.Content>
|
||||
@ -145,7 +164,7 @@ function Members () {
|
||||
</Card.Content>
|
||||
</Card>
|
||||
)}
|
||||
{members && members.map(m =>
|
||||
{memberList?.map(m =>
|
||||
<Card key={m._id}>
|
||||
<Card.Content>
|
||||
<UserChip user={m} />
|
||||
@ -158,7 +177,11 @@ function Members () {
|
||||
{utils.isGroupAdmin(user, group) && user._id !== m._id &&
|
||||
<Dropdown text='Options'>
|
||||
<Dropdown.Menu>
|
||||
<Dropdown.Item icon='ban' content='Kick' onClick={e => kickUser(m._id)} />
|
||||
<Dropdown.Item icon='ban' content='Remove from group' onClick={e => kickUser(m._id)} />
|
||||
{!utils.isGroupAdmin(m, group) &&
|
||||
<Dropdown.Item icon='rocket' content='Make admin' onClick={e => makeUserAdmin(m, true)} />}
|
||||
{utils.isGroupAdmin(m, group) &&
|
||||
<Dropdown.Item icon='ban' content='Revoke admin' onClick={e => makeUserAdmin(m, false)} />}
|
||||
</Dropdown.Menu>
|
||||
</Dropdown>}
|
||||
</div>
|
||||
|
@ -1,11 +1,19 @@
|
||||
import React from 'react'
|
||||
import { Header, Button, Divider, Segment, Form } from 'semantic-ui-react'
|
||||
import { Header, Button, Divider, Segment, Form, Checkbox } from 'semantic-ui-react'
|
||||
import { useNavigate, useParams } from 'react-router-dom'
|
||||
import { useSelector, useDispatch } from 'react-redux'
|
||||
import { toast } from 'react-toastify'
|
||||
import utils from '../../../utils/utils.js'
|
||||
import actions from '../../../actions'
|
||||
import api from '../../../api'
|
||||
import FileChooser from '../../includes/FileChooser'
|
||||
|
||||
const PERMISSIONS = [
|
||||
{ name: 'viewMembers', label: 'Allow members to view other members' },
|
||||
{ name: 'viewNoticeboard', label: 'Allow members to view the noticeboard' },
|
||||
{ name: 'postNoticeboard', label: 'Allow members to post to the noticeboard' },
|
||||
{ name: 'viewProjects', label: 'Allow members to view projects linked to the group' }
|
||||
]
|
||||
|
||||
function Settings () {
|
||||
const navigate = useNavigate()
|
||||
@ -29,6 +37,22 @@ function Settings () {
|
||||
dispatch(actions.groups.request(false))
|
||||
})
|
||||
}
|
||||
|
||||
const savePermission = (permissionName, enabled) => {
|
||||
const permissions = group.memberPermissions || []
|
||||
const index = permissions.indexOf(permissionName)
|
||||
if (enabled && index === -1) {
|
||||
permissions.push(permissionName)
|
||||
} else if (!enabled && index !== -1) {
|
||||
permissions.splice(index, 1)
|
||||
}
|
||||
api.groups.update(group._id, { memberPermissions: permissions }, g => {
|
||||
dispatch(actions.groups.updateGroup(group._id, { memberPermissions: g.memberPermissions }))
|
||||
}, err => {
|
||||
toast.error(err.message)
|
||||
})
|
||||
}
|
||||
|
||||
const deleteGroup = () => {
|
||||
utils.confirm('Really delete this group?', 'You\'ll lose all entries in the group feed and anything else you\'ve added to it.').then(() => {
|
||||
api.groups.delete(group._id, () => {
|
||||
@ -39,6 +63,16 @@ function Settings () {
|
||||
}, () => {})
|
||||
}
|
||||
|
||||
const updatePicture = (image) => {
|
||||
api.groups.update(group._id, { image }, g => {
|
||||
if (!image) { // Needed to ensure the avatar is immediately unset
|
||||
g.image = null
|
||||
g.imageUrl = null
|
||||
}
|
||||
dispatch(actions.groups.updateGroup(group._id, { image: g.image, imageUrl: g.imageUrl }))
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Segment color='blue'>
|
||||
@ -48,12 +82,40 @@ function Settings () {
|
||||
<Form.TextArea label='Group description' value={group.description} onChange={e => dispatch(actions.groups.updateGroup(group._id, { description: e.target.value }))} />
|
||||
<Form.Checkbox toggle checked={group.closed} label='Closed group' onChange={(e, c) => dispatch(actions.groups.updateGroup(group._id, { closed: c.checked }))} />
|
||||
<div style={{ marginLeft: 63, color: 'rgb(150,150,150)' }}>
|
||||
<p>Closed groups are more restrictive and new members must be invited or approved to join. Members can join non-closed groups without being invited.</p>
|
||||
<p>In a closed group, new members must be invited or approved to join.</p>
|
||||
</div>
|
||||
|
||||
<Divider hidden />
|
||||
<Form.Button loading={loading} color='teal' icon='check' content='Save changes' onClick={saveGroup} />
|
||||
</Form>
|
||||
|
||||
<Header>Group picture</Header>
|
||||
<p>Upload a picture to represent your group. This will be shown on the group page and may be viewable to both members and non-members.</p>
|
||||
{group.imageUrl &&
|
||||
<div style={{ marginBottom: 10 }}>
|
||||
<img src={group.imageUrl} alt='Group' style={{ maxWidth: 200, borderRadius: 5 }} />
|
||||
</div>}
|
||||
|
||||
<div style={{ display: 'flex', alignItems: 'center' }}>
|
||||
{group.imageUrl &&
|
||||
<Button basic color='gray' icon='times' content='Remove image' onClick={() => updatePicture(null)} />}
|
||||
<FileChooser
|
||||
forType='group' forObject={group}
|
||||
trigger={<Button basic color='yellow' icon='image' content='Choose an image' />}
|
||||
accept='image/*' onComplete={f => updatePicture(f.storedName)}
|
||||
/>
|
||||
</div>
|
||||
</Segment>
|
||||
|
||||
<Segment color='blue'>
|
||||
<Header>Permissions</Header>
|
||||
<p>Group admins can always perform all group actions.</p>
|
||||
|
||||
{PERMISSIONS.map(p => (
|
||||
<div key={p.name} style={{ marginBottom: 10 }}>
|
||||
<Checkbox toggle label={p.label} onChange={(e, c) => savePermission(p.name, c.checked)} checked={group.memberPermissions?.includes(p.name)} />
|
||||
</div>
|
||||
))}
|
||||
</Segment>
|
||||
|
||||
<Segment color='red'>
|
||||
|
@ -35,6 +35,9 @@ const utils = {
|
||||
isGroupAdmin (user, group) {
|
||||
return group?.admins?.indexOf(user?._id) > -1 || user?.roles?.indexOf('root') > -1
|
||||
},
|
||||
hasGroupPermission (user, group, permission) {
|
||||
return utils.isInGroup(user, group._id) && (utils.isGroupAdmin(user, group) || group?.memberPermissions?.indexOf(permission) > -1)
|
||||
},
|
||||
ensureHttp (s) {
|
||||
if (s && s.toLowerCase().indexOf('http') === -1) return `http://${s}`
|
||||
return s
|
||||
|
Loading…
Reference in New Issue
Block a user