newsynapse newtopic and allow/decline access

This commit is contained in:
Connor Turland 2018-03-08 09:46:05 -05:00
parent 6c84205b38
commit 150e7a4dfb
11 changed files with 186 additions and 33 deletions

View file

@ -27,14 +27,21 @@ Checklist
- [x] Figure out how authentication of requests from the frontend to the API works
- [x] Figure out how to combine the nodejs realtime server into server.js
- [x] Notifications: make sure loading states are working for popup and page
- [x] Request unreadNotificationCount
- [x] Request invite code
- [x] Request user object itself
- [x] Load the metacodes
- [ ] create topic form
- [ ] create synapse form
- [ ] move ImportDialog lightbox into main app
- [ ] Notifications: make sure notifications either look nice, or redirect
- [ ] Notifications: pagination
- [ ] Get actioncable working
- [ ] lightboxes
- [x] Request unreadNotificationCount
- [x] Request invite code
- [x] Request user object itself
- [ ] About lightbox
- [ ] Switch Metacodes lightbox / component
- [ ] Fork map lightbox / component
- [ ] break up index.html into parts
- [ ] Handle CSS metacode colors
- [ ] Fix Request An Invite page
@ -53,7 +60,5 @@ Checklist
- [ ] authorize
- [ ] user passwords
- [ ] Modify the RubyOnRails app to only serve JSON responses, no HTML pages anymore
- [ ] Modify the RubyOnRails app to include an endpoint that responds with basic data the front end needs to display (such as the invite code for the user, and the current metamaps build) (a bunch of the data found here: https://github.com/metamaps/metamaps/blob/frontendonly/Metamaps.ServerData.js.erb)
- [ ] Modify the frontend to request that data from the API which is necessary at first to load the page
- [x] Load the metacodes
- [ ] Load the metacode sets
- [ ] Load the metacode sets

View file

@ -117,6 +117,7 @@ $unread_notifications_dot_size: 8px;
height: 32px;
border-radius: 16px;
vertical-align: middle;
margin-right: 8px;
}
.button {
@ -127,6 +128,7 @@ $unread_notifications_dot_size: 8px;
}
&.decline {
margin-left: 8px;
background: #DB5D5D;
&:hover {
background: #DC4B4B;
@ -139,6 +141,10 @@ $unread_notifications_dot_size: 8px;
margin: 1em auto;
line-height: 20px;
}
.accessRequestError {
color: #DB5D5D;
}
}
}

View file

@ -19,8 +19,6 @@ const Create = {
newSelectedMetacodes: [],
init: function() {
var self = Create
self.newTopic.init()
self.newSynapse.init()
// // SWITCHING METACODE SETS

View file

@ -2,6 +2,14 @@ function fetchWithCookies(url) {
return fetch(url, { credentials: 'same-origin' })
}
function postWithCookies(url, data = {}) {
return fetch(url, {
credentials: 'same-origin',
method: 'POST',
body: JSON.stringify(data)
})
}
async function getMetacodes() {
const res = await fetchWithCookies('/metacodes.json')
const data = await res.json()
@ -14,7 +22,19 @@ async function getCurrentUser() {
return data
}
async function approveAccessRequest(mapId, requestId) {
const res = await postWithCookies(`/maps/${mapId}/approve_access/${requestId}`)
return res.status === 200
}
async function denyAccessRequest(mapId, requestId) {
const res = await postWithCookies(`/maps/${mapId}/deny_access/${requestId}`)
return res.status === 200
}
module.exports = {
getMetacodes,
getCurrentUser
getCurrentUser,
approveAccessRequest,
denyAccessRequest
}

View file

@ -7,7 +7,9 @@ const Notifications = {
notificationsLoading: false,
unreadNotificationsCount: 0,
init: serverData => {
Notifications.unreadNotificationsCount = serverData.ActiveMapper.unread_notifications_count
if (serverData.ActiveMapper) {
Notifications.unreadNotificationsCount = serverData.ActiveMapper.unread_notifications_count
}
},
fetchNotifications: render => {
Notifications.notificationsLoading = true

View file

@ -10,7 +10,9 @@ import { notifyUser } from './index.js'
import ImportDialog from './ImportDialog'
import Notifications from './Notifications'
import Active from '../Active'
import Create from '../Create'
import DataModel from '../DataModel'
import DataFetcher from '../DataFetcher'
import { ExploreMaps, ChatView, TopicCard, ContextMenu } from '../Views'
import Filter from '../Filter'
import JIT from '../JIT'
@ -107,7 +109,9 @@ const ReactApp = {
fetchNotifications: apply(Notifications.fetchNotifications, ReactApp.render),
fetchNotification: apply(Notifications.fetchNotification, ReactApp.render),
markAsRead: apply(Notifications.markAsRead, ReactApp.render),
markAsUnread: apply(Notifications.markAsUnread, ReactApp.render)
markAsUnread: apply(Notifications.markAsUnread, ReactApp.render),
denyAccessRequest: DataFetcher.denyAccessRequest,
approveAccessRequest: DataFetcher.approveAccessRequest
},
self.getMapProps(),
self.getTopicProps(),
@ -134,9 +138,12 @@ const ReactApp = {
toggleMapInfoBox: InfoBox.toggleBox,
infoBoxHtml: InfoBox.html,
openImportLightbox: () => ImportDialog.show(),
openMetacodeSwitcher: () => self.openLightbox('metacodeSwitcher'),
forkMap: Map.fork,
onMapStar: Map.star,
onMapUnstar: Map.unstar
onMapUnstar: Map.unstar,
initNewTopic: Create.newTopic.init,
initNewSynapse: Create.newSynapse.init
}
},
getCommonProps: function() {

View file

@ -3,7 +3,7 @@ import React, { Component } from 'react'
class CheatSheet extends Component {
render = () => {
return (
<div classNameName="lightboxContent" id="cheatsheet">
<div className="lightboxContent" id="cheatsheet">
<h3>HELP</h3>
<div id="cheatSheet">
<ul id="helpWrapper">
@ -24,7 +24,7 @@ class CheatSheet extends Component {
<li><a href="#csKeyboardShortcuts">Keyboard Shortcuts</a></li>
</ul>
<div id="csTopicView">
<div className="csItem"><span className="csTitle">Enter Topic (radial) View:</span> Click on a Topic result from Search, or click the synapse <img src="<%= asset_path 'synapse16.png' %>" width="16" align="middle" /> icon inside open Topic Card on map</div>
<div className="csItem"><span className="csTitle">Enter Topic (radial) View:</span> Click on a Topic result from Search, or click the synapse <img src="/images/synapse16.png" width="16" /> icon inside open Topic Card on map</div>
<div className="csItem"><span className="csTitle">Recenter Topics around chosen Topic:</span> Alt + click on the topic OR Alt + E</div>
<div className="csItem"><span className="csTitle">Reveal the siblings for a Topic:</span> Right-click and choose 'Reveal siblings' OR Alt + R</div>
<div className="csItem"><span className="csTitle">Center topic and reveal siblings:</span> Alt + T</div>
@ -60,7 +60,7 @@ class CheatSheet extends Component {
<span className="csTitle">Change Topic permission:</span> Click on 'Permission' icon (only for topic creator)
</div>
<div className="csItem indented">
<span className="csTitle">Open Topic view:</span> Click on <img src="<%= asset_path 'synapse16.png' %>" width="16" align="middle" /> icon within topic card bar
<span className="csTitle">Open Topic view:</span> Click on <img src="/images/synapse16.png" width="16" /> icon within topic card bar
</div>
<div className="csItem indented">
<span className="csTitle">Close Topic card:</span> Click on canvas
@ -131,7 +131,7 @@ class CheatSheet extends Component {
</div>
</div>
<div id="tutorials">
<iframe id="tutorialVideo" src="//player.vimeo.com/video/88334167" width="552" height="320" frameborder="0" webkitallowfullscreen mozallowfullscreen allowfullscreen></iframe>
<iframe id="tutorialVideo" src="//player.vimeo.com/video/88334167" width="552" height="320" frameBorder="0" allowFullScreen></iframe>
</div>
<div id="moreResources">
<p>For more information about Metamaps.cc, visit our Knowledge Base or skip directly to a section by clicking on one of the categories below.</p>

View file

@ -0,0 +1,18 @@
import React, { Component } from 'react'
class NewSynapse extends Component {
componentDidMount() {
this.props.initNewSynapse()
}
render = () => {
return (
<form className="new_synapse" id="new_synapse" action="/synapses" acceptCharset="UTF-8" data-remote="true" method="post">
<input name="utf8" type="hidden" value="✓" />
<input placeholder="describe the connection..." type="text" name="synapse[desc]" id="synapse_desc" className="tt-input" autoComplete="off" spellCheck="false" dir="auto" />
</form>
)
}
}
export default NewSynapse

View file

@ -0,0 +1,44 @@
import React, { Component } from 'react'
class NewTopic extends Component {
componentDidMount() {
this.props.initNewTopic()
}
render = () => {
const metacodes = [
{
"id": 1,
"name": "Action",
"created_at": "2017-03-04T17:33:07.394Z",
"updated_at": "2017-03-04T17:33:07.394Z",
"color": "#BD6C85",
"icon": "https://s3.amazonaws.com/metamaps-assets/metacodes/blueprint/96px/bp_action.png"
}
]
return (
<form className="new_topic" id="new_topic" action="/topics" acceptCharset="UTF-8" data-remote="true" method="post">
<input name="utf8" type="hidden" value="✓" />
<div className="openMetacodeSwitcher" onClick={() => this.props.openMetacodeSwitcher()}>
<div className="tooltipsAbove">Switch Metacodes</div>
</div>
<div className="pinCarousel">
<div className="tooltipsAbove helpPin">Pin Open</div>
<div className="tooltipsAbove helpUnpin">Unpin</div>
</div>
<div id="metacodeImg">
{metacodes.map(m => <img key={m.id} className="cloudcarousel" width="40" height="40" src={m.icon} alt={m.name} title={m.name} data-id={m.id} />)}
</div>
<input maxLength="140" placeholder="title..." size="140" type="text" name="topic[name]" id="topic_name" className="tt-input" autoComplete="off" spellCheck="false" dir="auto" />
<div id="metacodeImgTitle"></div>
<div className="clearfloat"></div>
</form>
)
}
}
export default NewTopic

View file

@ -9,6 +9,8 @@ import Instructions from './Instructions'
import VisualizationControls from '../../components/VisualizationControls'
import MapChat from './MapChat'
import TopicCard from '../../components/TopicCard'
import NewTopic from '../../components/NewTopic'
import NewSynapse from '../../components/NewSynapse'
export default class MapView extends Component {
@ -82,7 +84,7 @@ export default class MapView extends Component {
openImportLightbox, forkMap, openHelpLightbox,
mapIsStarred, onMapStar, onMapUnstar, openTopic,
onZoomExtents, onZoomIn, onZoomOut, hasLearnedTopicCreation,
contextMenu } = this.props
contextMenu, initNewTopic, initNewSynapse, openMetacodeSwitcher } = this.props
const { chatOpen } = this.state
const onChatOpen = () => {
this.setState({chatOpen: true})
@ -111,6 +113,8 @@ export default class MapView extends Component {
filterAllMappers={filterAllMappers}
filterAllSynapses={filterAllSynapses} />
<DataVis />
<NewTopic initNewTopic={initNewTopic} openMetacodeSwitcher={openMetacodeSwitcher} />
<NewSynapse initNewSynapse={initNewSynapse} />
{openTopic && <TopicCard {...this.props} />}
{contextMenu && <ContextMenu {...this.props} />}
{currentUser && <Instructions mobile={mobile} hasLearnedTopicCreation={hasLearnedTopicCreation} />}

View file

@ -7,12 +7,18 @@ import LoadingPage from '../helpers/LoadingPage'
import Loading from '../../components/Loading'
import NotificationBody from '../../components/NotificationBody'
/* TODO:
allow / decline access loading states
make backend serve HTML for raw body too
*/
class NotificationPage extends Component {
constructor(props) {
super(props)
this.state = {
allowPending: false,
declinePending: false,
allowed: false,
declined: false,
error: false
}
}
componentDidMount() {
// the notification id
const id = parseInt(this.props.params.id, 10)
@ -20,6 +26,35 @@ class NotificationPage extends Component {
this.props.fetchNotification(id)
}
}
deny = async () => {
const id = parseInt(this.props.params.id, 10)
const notification = this.props.notifications.find(n => n.id === id)
const request = notification.data.object
const map = notification.data.map
this.setState({ declinePending: true })
const success = await this.props.denyAccessRequest(map.id, request.id)
if (success) {
this.setState({ declined: true, declinePending: false })
} else {
this.setState({ error: true })
}
}
approve = async () => {
const id = parseInt(this.props.params.id, 10)
const notification = this.props.notifications.find(n => n.id === id)
const request = notification.data.object
const map = notification.data.map
this.setState({ allowPending: true })
const success = await this.props.approveAccessRequest(map.id, request.id)
if (success) {
this.setState({ allowed: true, allowPending: false })
} else {
this.setState({ error: true })
}
}
render = () => {
const id = parseInt(this.props.params.id, 10)
const notification = this.props.notifications.find(n => n.id === id)
@ -36,6 +71,7 @@ class NotificationPage extends Component {
const subject = notification.type === MAP_ACCESS_REQUEST ?
(<span><span style={{ fontWeight: 'bold' }} className='requesterName'>{notification.actor.name}</span> wants to collaborate on map <span style={{fontWeight: 'bold'}}>{ map.name }</span></span>)
: notification.subject
const localAnswered = this.state.allowed || this.state.declined
return (
<div>
<div id="yield">
@ -48,21 +84,34 @@ class NotificationPage extends Component {
{subject}
</h2>
{notification.type === MAP_ACCESS_REQUEST && <div className="notification-body">
<p className="main-text">
{request.answered && <span>
<div className="main-text">
{this.state.error && <div className="accessRequestError">There was an error, please refresh and try again</div>}
{request.answered && <div>
{request.approved && <span>You already responded to this access request, and allowed access.</span>}
{!request.approved && <span>You already responded to this access request, and declined access. If you changed your mind, you can still grant
them access by going to the map and adding them as a collaborator.</span>}
</span>}
{!request.answered && <span>
</div>}
{!localAnswered && !request.answered && <div>
<img src='/images/ellipsis.gif' className='hidden' />
<a className="button allow" data-remote="true" rel="nofollow" data-method="post" href={`/maps/${map.id}/approve_access/${request.id}`}>Allow</a>
<a className="button decline" data-remote="true" rel="nofollow" data-method="post" href={`/maps/${map.id}/deny_access/${request.id}`}>Decline</a>
</span>}
</p>
<Link to={`/maps/${map.id}`}>Go to map</Link>
&nbsp;&nbsp;
<Link to={`/explore/mapper/${notification.actor.id}`}>View mapper profile</Link>
{!this.state.declined && !this.state.declinePending && <button onClick={this.approve} className="button allow">
{this.state.allowPending ? <img src='/images/ellipsis.gif' /> : 'Allow'}
</button>}
{!this.state.allowed && !this.state.allowPending && <button onClick={this.deny} className="button decline">
{this.state.declinePending ? <img src='/images/ellipsis.gif' /> : 'Decline'}
</button>}
</div>}
{this.state.allowed && <div>
{notification.actor.name} has been shared on the map and notified.
</div>}
{this.state.declined && <div>
Fair enough.
</div>}
</div>
<div>
<Link to={`/maps/${map.id}`}>Go to map</Link>
&nbsp;&nbsp;
<Link to={`/explore/mapper/${notification.actor.id}`}>View mapper profile</Link>
</div>
</div>}
{notification.type !== MAP_ACCESS_REQUEST && <NotificationBody notification={notification} />}
</div>