newsynapse newtopic and allow/decline access
This commit is contained in:
parent
6c84205b38
commit
150e7a4dfb
11 changed files with 186 additions and 33 deletions
17
README.md
17
README.md
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -19,8 +19,6 @@ const Create = {
|
|||
newSelectedMetacodes: [],
|
||||
init: function() {
|
||||
var self = Create
|
||||
self.newTopic.init()
|
||||
self.newSynapse.init()
|
||||
|
||||
// // SWITCHING METACODE SETS
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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>
|
||||
|
|
18
src/components/NewSynapse.js
Normal file
18
src/components/NewSynapse.js
Normal 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
|
44
src/components/NewTopic.js
Normal file
44
src/components/NewTopic.js
Normal 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
|
|
@ -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} />}
|
||||
|
|
|
@ -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>
|
||||
|
||||
<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>
|
||||
|
||||
<Link to={`/explore/mapper/${notification.actor.id}`}>View mapper profile</Link>
|
||||
</div>
|
||||
</div>}
|
||||
{notification.type !== MAP_ACCESS_REQUEST && <NotificationBody notification={notification} />}
|
||||
</div>
|
||||
|
|
Loading…
Reference in a new issue