Merge pull request #1156 from metamaps/develop

develop into master
This commit is contained in:
Connor Turland 2017-10-25 06:31:32 -04:00 committed by GitHub
commit f76f66ed19
86 changed files with 1510 additions and 928 deletions

View file

@ -24,7 +24,7 @@ gem 'pundit'
gem 'pundit_extra'
gem 'rack-attack'
gem 'rack-cors'
gem 'redis'
gem 'redis', '~> 3.3.3'
gem 'slack-notifier'
gem 'snorlax'
gem 'sucker_punch'

View file

@ -220,7 +220,7 @@ GEM
rb-fsevent (0.10.2)
rb-inotify (0.9.10)
ffi (>= 0.5.0, < 2)
redis (4.0.0)
redis (3.3.3)
responders (2.4.0)
actionpack (>= 4.2.0, < 5.3)
railties (>= 4.2.0, < 5.3)
@ -331,7 +331,7 @@ DEPENDENCIES
rack-attack
rack-cors
rails (~> 5.0.0)
redis
redis (~> 3.3.3)
rspec-rails
rubocop
sass-rails
@ -348,4 +348,4 @@ RUBY VERSION
ruby 2.3.0p0
BUNDLED WITH
1.14.6
1.15.4

View file

@ -56,16 +56,15 @@
}
li.toggledOff {
opacity: 0.4;
opacity: 0.6;
}
}
.blackBox {
.centerContent {
width: 760px;
margin: 0 auto;
padding: 80px 0 60px 20px;
background: rgba(0, 0, 0, 0.4);
color: white;
background: rgba(125, 125, 125, 0.4);
overflow: hidden;
position: relative;
@ -85,10 +84,10 @@
display: table-row;
}
tr:nth-child(odd) {
background: rgba(0, 0, 0, 0.2);
background: rgba(125, 125, 125, 0.2);
}
tr:nth-child(even) {
background: rgba(0, 0, 0, 0.3);
background: rgba(125, 125, 125, 0.3);
}
th,
td {

View file

@ -826,6 +826,7 @@ label {
font-size: 14px;
line-height: 14px;
color: #757575;
cursor: pointer;
}
.accountListItem:hover {
color: #424242;
@ -2929,136 +2930,6 @@ and it won't be important on password protected instances */
color: #424242;
}
/* Admin Pages */
.blackBox {
width: 760px;
margin: 0 auto;
padding: 20px 0 60px 20px;
background: rgba(0, 0, 0, 0.4);
color: white;
overflow: hidden;
position: relative;
}
.blackBox .metacodeSetsDescription {
width: 314px;
}
.blackBox td.metacodeSetDesc {
width: 314px;
word-wrap: break-word;
}
.blackBox .metacodeSetImage {
width: 36px;
height: 36px;
float: left;
}
.blackBox tr {
display: table-row;
}
.blackBox tr:nth-child(odd) {
background: rgba(0, 0, 0, 0.2);
}
.blackBox tr:nth-child(even) {
background: rgba(0, 0, 0, 0.3);
}
.blackBox th,
.blackBox td {
padding: 10px;
}
.blackBox td.iconURL {
max-width: 415px;
word-wrap: break-word;
}
.blackBox td.iconColor {
}
.blackBox .field {
margin: 15px 0 5px;
}
.blackBox label {
float: left;
width: 100px;
margin-right: 15px;
}
.blackBox input[type="text"] {
width: 336px;
height: 32px;
font-size: 15px;
direction: ltr;
-webkit-appearance: none;
appearance: none;
display: inline-block;
margin: 0;
padding: 0 8px;
background: #fff;
border: 1px solid #d9d9d9;
border-top: 1px solid #c0c0c0;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
-webkit-border-radius: 1px;
-moz-border-radius: 1px;
border-radius: 1px;
font: -webkit-small-control;
color: initial;
letter-spacing: normal;
word-spacing: normal;
text-transform: none;
text-indent: 0px;
text-shadow: none;
display: inline-block;
text-align: start;
font-family: arial;
}
.blackBox input[type="text"]:hover,
.blackBox textarea:hover {
border: 1px solid #b9b9b9;
border-top: 1px solid #a0a0a0;
-webkit-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1);
-moz-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1);
box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1);
}
.blackBox textarea {
padding: 8px;
border: 1px solid #d9d9d9;
border-top: 1px solid #c0c0c0;
resize: none;
font: -webkit-small-control;
letter-spacing: normal;
word-spacing: normal;
text-transform: none;
text-indent: 0px;
text-shadow: none;
text-align: start;
font-family: arial;
font-size: 15px;
line-height: 17px;
width: 318px;
}
.blackBox .allMetacodes {
padding: 5px 0;
}
.blackBox a.button {
margin-right: 20px;
line-height: 40px;
}
.blackBox a.button,
.blackBox input.add {
float: left;
margin-top: 5px;
height: 40px;
font-size: 17px;
width: auto;
padding: 0 30px;
cursor: pointer;
font-weight: normal;
}
.blackBox a.button:hover,
.blackBox input.add:hover {
-webkit-box-shadow: none;
box-shadow: none;
}
/* request */
.requestInvite {

View file

@ -127,6 +127,10 @@ $mid-gray-opacity: rgba(66, 66, 66, 0.6);
a.mdSupport {
color: #4fb5c0;
font-size: 11px;
display: none;
}
.riek-editing + .mdSupport {
display: block;
}
}
.CardOnGraph.hasAttachment .scroll {
@ -139,14 +143,12 @@ $mid-gray-opacity: rgba(66, 66, 66, 0.6);
font-family: helvetica, sans-serif;
color: #424242;
padding: 0;
width: 100%;
width: 258px;
margin: 0;
border: 0;
outline: none;
font-size: 12px;
line-height: 15px;
background: none;
resize: none;
overflow-y: scroll;
}
/*
@ -180,11 +182,9 @@ $mid-gray-opacity: rgba(66, 66, 66, 0.6);
.CardOnGraph .riek_desc {
display:block;
margin-top:2px;
padding-right: 18px;
margin-right: 8px;
padding-right: 26px;
}
.canEdit .CardOnGraph .riek_desc:hover {
.canEdit .CardOnGraph .riek_desc:not(.riek-editing):hover {
background-image: url(<%= asset_data_uri('edit.png') %>);
background-position: top right;
background-repeat: no-repeat;

View file

@ -668,19 +668,19 @@
box-shadow: 0px 3px 3px rgba(0,0,0,0.23), 0 3px 3px rgba(0,0,0,0.16);
}
#exploreMapsHeader {
#navBar {
position: absolute;
width: 100%;
}
.exploreMapsBar {
.navBarContainer {
z-index:2;
background-color:#FAFAFA;
height: 42px;
padding-top: 52px;
}
.exploreMapsMenu {
.navBarMenu {
display: block;
width: 100%;
height:42px;
@ -689,30 +689,29 @@
text-align: center;
}
.exploreMapsCenter {
.navBarCenter {
display: block;
}
.exploreMapsButton {
color: #757575;
.navBarButton {
color: #757575;
cursor: default;
font-weight: normal;
font-family: 'din-medium';
font-size: 14px;
height: 14px;
padding: 14px 8px 12px 40px;
border-bottom: 2px solid rgba(0,0,0,0);
padding: 0 8px;
border-bottom: 2px solid rgba(0,0,0,0);
display: inline-block;
cursor: pointer;
position:relative;
cursor: pointer;
position:relative;
}
.exploreMapsButton:hover, .exploreMapsButton.active {
.navBarButton:hover, .navBarButton.active {
text-decoration: none;
color: #424242;
border-bottom: 2px solid #00BCD4;
}
.exploreMapsButton.mapperButton {
.navBarButton.mapperButton {
height: 40px;
padding: 0;
}
@ -729,62 +728,69 @@
}
.exploreMapsButton .exploreMapsIcon {
.navBarButton .navBarIcon {
background-repeat: no-repeat;
width:32px;
height:32px;
position:absolute;
top:5px;
left:5px;
margin-top:5px;
margin-left:5px;
margin-right: 5px;
display: inline-block;
vertical-align: top;
}
.exploreMapsCenter .authedApps .exploreMapsIcon {
.navBarLinkText {
padding: 11px 0 12px 0;
display: inline-block;
}
.navBarCenter .authedApps .navBarIcon {
background-image: url(<%= asset_path('user_sprite.png') %>);
background-position: 0 -32px;
}
.exploreMapsCenter .myMaps .exploreMapsIcon {
.navBarCenter .myMaps .navBarIcon {
background-image: url(<%= asset_path 'exploremaps_sprite.png' %>);
background-position: -32px 0;
}
.exploreMapsCenter .sharedMaps .exploreMapsIcon {
.navBarCenter .sharedMaps .navBarIcon {
background-image: url(<%= asset_path 'exploremaps_sprite.png' %>);
background-position: -128px 0;
}
.exploreMapsCenter .activeMaps .exploreMapsIcon {
.navBarCenter .activeMaps .navBarIcon {
background-image: url(<%= asset_path 'exploremaps_sprite.png' %>);
background-position: 0 0;
}
.exploreMapsCenter .featuredMaps .exploreMapsIcon {
.navBarCenter .featuredMaps .navBarIcon {
background-image: url(<%= asset_path 'exploremaps_sprite.png' %>);
background-position: -96px 0;
}
.exploreMapsCenter .starredMaps .exploreMapsIcon {
.navBarCenter .starredMaps .navBarIcon {
background-image: url(<%= asset_path 'exploremaps_sprite.png' %>);
background-position: -96px 0;
}
.exploreMapsCenter .notificationsLink .exploreMapsIcon {
.navBarCenter .notificationsLink .navBarIcon {
background-image: url(<%= asset_path 'topright_sprite.png' %>);
background-position: -128px 0;
}
.authedApps:hover .exploreMapsIcon, .authedApps.active .exploreMapsIcon {
.authedApps:hover .navBarIcon, .authedApps.active .navBarIcon {
background-position-x: -32px;
}
.myMaps:hover .exploreMapsIcon, .myMaps.active .exploreMapsIcon {
.myMaps:hover .navBarIcon, .myMaps.active .navBarIcon {
background-position: -32px -32px;
}
.activeMaps:hover .exploreMapsIcon, .activeMaps.active .exploreMapsIcon {
.activeMaps:hover .navBarIcon, .activeMaps.active .navBarIcon {
background-position: 0 -32px;
}
.featuredMaps:hover .exploreMapsIcon, .featuredMaps.active .exploreMapsIcon {
.featuredMaps:hover .navBarIcon, .featuredMaps.active .navBarIcon {
background-position: -96px -32px;
}
.starredMaps:hover .exploreMapsIcon, .starredMaps.active .exploreMapsIcon {
.starredMaps:hover .navBarIcon, .starredMaps.active .navBarIcon {
background-position: -96px -32px;
}
.sharedMaps:hover .exploreMapsIcon, .sharedMaps.active .exploreMapsIcon {
.sharedMaps:hover .navBarIcon, .sharedMaps.active .navBarIcon {
background-position: -128px -32px;
}
.notificationsLink:hover .exploreMapsIcon, .notificationsLink.active .exploreMapsIcon {
.notificationsLink:hover .navBarIcon, .notificationsLink.active .navBarIcon {
background-position-y: -32px;
}

View file

@ -32,7 +32,7 @@
/* Smartphones (portrait and landscape) ----------- the minimum space that two map cards can fit side by side */
@media only screen and (max-width : 504px) {
.upperLeftUI, .upperRightUI, .openCheatsheet, .mapInfoIcon, .feedback-icon, .chat-box, #exploreMapsHeader {
.upperLeftUI, .upperRightUI, .openCheatsheet, .mapInfoIcon, .feedback-icon, .chat-box, #navBar {
display: none !important;
}

View file

@ -1,4 +1,7 @@
$notifications-border-color: #DDDDDD;
$notifications-hover-color: #F6F6F6;
$unread_notifications_dot_size: 8px;
.unread-notifications-dot {
width: $unread_notifications_dot_size;
height: $unread_notifications_dot_size;
@ -13,13 +16,72 @@ $unread_notifications_dot_size: 8px;
.notificationsIcon {
position: relative;
}
.notificationsBox {
position: absolute;
background: #FFFFFF;
border-radius: 2px;
width: 350px;
right: 0;
top: 50px;
box-shadow: 0 3px 6px rgba(0,0,0,0.16);
border: 1px solid $notifications-border-color;
.notificationsBoxTriangle {
min-width: 0 !important;
display: block;
position: absolute;
right: 48px;
width: 20px !important;
height: 20px !important;
margin-left: -10px;
top: -11px;
border-left: 1px solid $notifications-border-color;
border-top: 1px solid $notifications-border-color;
border-bottom: 0 !important;
border-right: 0 !important;
background-color: #fff;
transform: rotate(45deg);
-webkit-transform: rotate(45deg);
-ms-transform: rotate(45deg);
}
ul.notifications {
max-height: 500px;
overflow-y: auto;
.notification {
font-size: 13px;
.notification-body {
border-bottom: 1px solid $notifications-border-color;
}
}
.notificationsEmpty {
font-family: din-regular, helvetica, sans-serif;
margin: 50px 10px;
text-align: center;
}
}
.notificationsBoxSeeAll {
display: block;
width: 100%;
text-align: center;
padding: 6px 0;
font-family: din-regular, helvetica, sans-serif;
border-top: 1px solid rgba(0, 0, 0, 0.1);
&:hover {
color: #333;
background: $notifications-hover-color;
}
}
}
}
.controller-notifications {
ul.notifications {
list-style: none;
}
.notificationPage,
.notificationsPage {
font-family: 'din-regular', Sans-Serif;
@ -47,89 +109,9 @@ $unread_notifications_dot_size: 8px;
.emptyInbox {
padding-top: 15px;
}
.notification {
padding: 10px;
position: relative;
&:hover {
background: #F6F6F6;
.notification-read-unread {
display:block;
}
.notification-date {
display: none;
}
}
& > a {
float: left;
width: 85%;
box-sizing: border-box;
padding-right: 10px;
}
.notification-actor {
float: left;
img {
width: 32px;
height: 32px;
border-radius: 16px;
}
}
.notification-body {
margin-left: 50px;
line-height: 20px;
.in-bold {
font-family: 'din-medium', Sans-Serif;
}
.action {
background: #4fb5c0;
color: #FFF;
padding: 2px 6px;
border-radius: 3px;
display: inline-block;
margin: 5px 0;
}
}
.notification-date {
position: absolute;
top: 50%;
right: 10px;
color: #607d8b;
font-size: 13px;
line-height: 13px;
margin-top: -6px;
}
.notification-read-unread {
display: none;
float: left;
width: 15%;
a {
position: absolute;
top: 50%;
margin-top: -10px;
text-align: center;
}
}
&.unread {
background: #EEE;
}
}
}
.notificationPage {
.thirty-two-avatar {
@ -139,14 +121,14 @@ $unread_notifications_dot_size: 8px;
border-radius: 16px;
vertical-align: middle;
}
.button {
line-height: 32px;
img {
margin-top: 8px;
}
&.decline {
background: #DB5D5D;
&:hover {
@ -154,7 +136,7 @@ $unread_notifications_dot_size: 8px;
}
}
}
.notification-body {
p, div {
margin: 1em auto;
@ -163,3 +145,93 @@ $unread_notifications_dot_size: 8px;
}
}
}
ul.notifications {
list-style: none;
li:nth-last-child(2) {
.notification-body {
border-bottom: none !important;
}
}
}
.notification {
padding: 10px 10px 0 10px;
position: relative;
font-family: 'din-regular', Sans-Serif;
&.unread {
background: #EEE;
}
&:hover {
background: $notifications-hover-color;
.notification-read-unread {
display:block;
}
.notification-date {
display: none;
}
}
& > a {
float: left;
width: 85%;
box-sizing: border-box;
padding-right: 10px;
}
.notification-actor {
float: left;
img {
width: 32px;
height: 32px;
border-radius: 16px;
}
}
.notification-body {
margin-left: 50px;
line-height: 20px;
padding-bottom: 10px;
.in-bold {
font-family: 'din-medium', Sans-Serif;
}
.action {
background: #4fb5c0;
color: #FFF;
padding: 2px 6px;
border-radius: 3px;
display: inline-block;
margin: 5px 0;
}
}
.notification-date {
position: absolute;
top: 50%;
right: 10px;
color: #607d8b;
margin-top: -6px;
}
.notification-read-unread {
display: none;
float: left;
width: 15%;
a, div {
position: absolute;
top: 50%;
margin-top: -10px;
text-align: center;
cursor: pointer;
}
}
}

View file

@ -6,13 +6,17 @@ class NotificationsController < ApplicationController
def index
@notifications = current_user.mailbox.notifications.page(params[:page]).per(25)
respond_to do |format|
format.html
format.json do
render json: @notifications.map do |notification|
notifications = @notifications.map do |notification|
receipt = @receipts.find_by(notification_id: notification.id)
notification.as_json.merge(is_read: receipt.is_read)
NotificationDecorator.decorate(notification, receipt)
end
if !notifications.empty?
render json: notifications
else
render json: [].to_json
end
end
end
@ -34,9 +38,7 @@ class NotificationsController < ApplicationController
end
end
format.json do
render json: @notification.as_json.merge(
is_read: @receipt.is_read
)
render json: NotificationDecorator.decorate(@notification, @receipt)
end
end
end
@ -46,9 +48,7 @@ class NotificationsController < ApplicationController
respond_to do |format|
format.js
format.json do
render json: @notification.as_json.merge(
is_read: @receipt.is_read
)
render json: NotificationDecorator.decorate(@notification, @receipt)
end
end
end
@ -58,9 +58,7 @@ class NotificationsController < ApplicationController
respond_to do |format|
format.js
format.json do
render json: @notification.as_json.merge(
is_read: @receipt.is_read
)
render json: NotificationDecorator.decorate(@notification, @receipt)
end
end
end

View file

@ -0,0 +1,50 @@
# frozen_string_literal: true
class NotificationDecorator
class << self
def decorate(notification, receipt)
result = {
id: notification.id,
type: notification.notification_code,
subject: notification.subject,
is_read: receipt.is_read,
created_at: notification.created_at,
actor: notification.sender,
data: {
object: notification.notified_object
}
}
case notification.notification_code
when MAP_ACCESS_APPROVED, MAP_ACCESS_REQUEST, MAP_INVITE_TO_EDIT
map = notification.notified_object&.map
result[:data][:map] = {
id: map&.id,
name: map&.name
}
when TOPIC_ADDED_TO_MAP
topic = notification.notified_object&.eventable
map = notification.notified_object&.map
result[:data][:topic] = {
id: topic&.id,
name: topic&.name
}
result[:data][:map] = {
id: map&.id,
name: map&.name
}
when TOPIC_CONNECTED_1, TOPIC_CONNECTED_2
topic1 = notification.notified_object&.topic1
topic2 = notification.notified_object&.topic2
result[:data][:topic1] = {
id: topic1&.id,
name: topic1&.name
}
result[:data][:topic2] = {
id: topic2&.id,
name: topic2&.name
}
end
result
end
end
end

View file

@ -23,7 +23,7 @@ class UserPreference
def initialize_follow_settings
@follow_topic_on_created = false
@follow_topic_on_contributed = false
@follow_map_on_created = false
@follow_map_on_created = true
@follow_map_on_contributed = false
end
end

View file

@ -1,6 +0,0 @@
<%= link_to 'Metacode Sets', metacode_sets_path, { :class => 'button' }%>
<%= link_to 'New Set', new_metacode_set_path, { :class => 'button' }%>
<%= link_to 'Metacodes', metacodes_path, { :class => 'button' }%>
<%= link_to 'New Metacode', new_metacode_path, { :class => 'button' }%>
<div class='clearfloat'></div>
<br />

View file

@ -6,10 +6,10 @@
#%>
<%= render :partial => 'layouts/head' %>
<body class="<%= authenticated? ? "authenticated" : "unauthenticated" %> controller-<%= controller_name %> action-<%= action_name %>">
<body class="<%= current_user ? "authenticated" : "unauthenticated" %> controller-<%= controller_name %> action-<%= action_name %>">
<div class="main" id="react-app"></div>
<%= yield %>
<% if authenticated? %>
<% if current_user %>
<% # for creating and pulling in topics and synapses %>
<% if controller_name == 'maps' && action_name == "conversation" %>
<%= render :partial => 'maps/newtopicsecret' %>

View file

@ -1,31 +0,0 @@
<%#
# @file
# Main application file. Holds scaffolding present on every page.
# Then a certain non-partial view (no _ preceding filename) will be
# displayed within, based on URL
#%>
<%= render :partial => 'layouts/head' %>
<body class="<%= current_user ? 'authenticated' : 'unauthenticated' %>">
<div class="main" id="react-app"></div>
<%= yield %>
<div id="exploreMapsHeader">
<div class="exploreMapsBar exploreElement">
<div class="exploreMapsMenu">
<div class="exploreMapsCenter">
<% if current_user && current_user.admin %>
<a href="<%= oauth_applications_path %>" class="activeMaps exploreMapsButton <%= params[:controller] == 'doorkeeper/applications' ? 'active' : nil %>">
<div class="exploreMapsIcon"></div>Registered Apps
</a>
<% end %>
<a href="<%= oauth_authorized_applications_path %>" class="authedApps exploreMapsButton <%= params[:controller] == 'doorkeeper/authorized_applications' ? 'active' : nil %>">
<div class="exploreMapsIcon"></div>Authorized Apps
</a>
<a href="/" class="myMaps exploreMapsButton">
<div class="exploreMapsIcon"></div>Maps
</a>
</div>
</div>
</div>
</div>
<%= render :partial => 'layouts/foot' %>

View file

@ -15,22 +15,22 @@
<%= link_to @map.name, map_url(@map) %>
</p>
<div>
<% if @summary_data[:stats][:messages_sent] > 0 %>
<% if @summary_data[:stats][:messages_sent] %>
<p style="margin:6px 0;color:#a354cd;"><%= pluralize(@summary_data[:stats][:messages_sent], 'message') %></p>
<% end %>
<% if @summary_data[:stats][:topics_added] > 0 %>
<% if @summary_data[:stats][:topics_added] %>
<p style="margin:6px 0;color:#4FC059;"><%= pluralize(@summary_data[:stats][:topics_added], 'topic') %> added</p>
<% end %>
<% if @summary_data[:stats][:synapses_added] > 0 %>
<% if @summary_data[:stats][:synapses_added] %>
<p style="margin:6px 0;color:#4FC059;"><%= pluralize(@summary_data[:stats][:synapses_added], 'synapse') %> added</p>
<% end %>
<% if @summary_data[:stats][:topics_moved] > 0 %>
<% if @summary_data[:stats][:topics_moved] %>
<p style="margin:6px 0;color:#00BCD4;"><%= pluralize(@summary_data[:stats][:topics_moved], 'topic') %> moved</p>
<% end %>
<% if @summary_data[:stats][:topics_removed] > 0 %>
<% if @summary_data[:stats][:topics_removed] %>
<p style="margin:6px 0;color:#c04f4f;"><%= pluralize(@summary_data[:stats][:topics_removed], 'topic') %> removed</p>
<% end %>
<% if @summary_data[:stats][:synapses_removed] > 0 %>
<% if @summary_data[:stats][:synapses_removed] %>
<p style="margin:6px 0;color:#c04f4f;"><%= pluralize(@summary_data[:stats][:synapses_removed], 'synapse') %> removed</p>
<% end %>
</div>
@ -61,7 +61,7 @@
</div>
<% end %>
<% if @summary_data[:topics_removed] || @summary_data[:synapses_removed] %>
<div style="background:rgba(192, 79, 79, 0.2); padding:8px;">
<% if @summary_data[:topics_removed] %>

View file

@ -1,6 +1,6 @@
<div id="yield">
<div class='blackBox'>
<%= render 'form' %>
<div class='centerContent'>
<%= render 'form' %>
</div>
</div>

View file

@ -1,37 +1,36 @@
<div id="yield">
<div class='blackBox'>
<%= render :partial => 'admin/adminpanel' %>
<br />
<table>
<tr>
<th>Name</th>
<th class='metacodeSetsDescription'>Description</th>
<th>Metacodes</th>
</tr>
<div id="yield">
<div class='centerContent'>
<br />
<table>
<tr>
<th>Name</th>
<th class='metacodeSetsDescription'>Description</th>
<th>Metacodes</th>
</tr>
<% @metacode_sets.each do |metacode_set| %>
<tr>
<td>
<%= metacode_set.name %><br />
<%= link_to 'Edit',
edit_metacode_set_path(metacode_set) %>
<br />
<%= link_to 'Delete',
metacode_set, method: :delete,
data: { confirm: 'Are you sure?' } %>
</td>
<td class='metacodeSetDesc'><%= metacode_set.desc %></td>
<td>
<% metacode_set.metacodes.each_with_index do |metacode, index| %>
<img class='metacodeSetImage' src='<%= asset_path metacode.icon %>' />
<% if (index+1)%4 == 0 %>
<div class='clearfloat'></div>
<% @metacode_sets.each do |metacode_set| %>
<tr>
<td>
<%= metacode_set.name %><br />
<%= link_to 'Edit',
edit_metacode_set_path(metacode_set) %>
<br />
<%= link_to 'Delete',
metacode_set, method: :delete,
data: { confirm: 'Are you sure?' } %>
</td>
<td class='metacodeSetDesc'><%= metacode_set.desc %></td>
<td>
<% metacode_set.metacodes.each_with_index do |metacode, index| %>
<img class='metacodeSetImage' src='<%= asset_path metacode.icon %>' />
<% if (index+1)%4 == 0 %>
<div class='clearfloat'></div>
<% end %>
<% end %>
<% end %>
<div class='clearfloat'></div>
</td>
</tr>
<% end %>
</table>
<div class='clearfloat'></div>
</td>
</tr>
<% end %>
</table>
</div>
</div>

View file

@ -1,6 +1,6 @@
<div id="yield">
<div class='blackBox'>
<%= render 'form' %>
<div class='centerContent'>
<%= render 'form' %>
</div>
</div>
@ -11,4 +11,4 @@
<% end %>
Metamaps.Admin.allMetacodes.push("<%= m.id %>");
<% end %>
</script>
</script>

View file

@ -1,5 +1,5 @@
<div id="yield">
<div class='blackBox'>
<div class='centerContent'>
<%= render 'form' %>
</div>
</div>
</div>

View file

@ -1,31 +1,30 @@
<div id="yield">
<div class='blackBox'>
<%= render :partial => 'admin/adminpanel' %>
<br />
<table>
<tr>
<th>Name</th>
<th>Icon</th>
<th>Color</th>
<th></th>
<th></th>
</tr>
<div class='centerContent'>
<br />
<table>
<tr>
<th>Name</th>
<th>Icon</th>
<th>Color</th>
<th></th>
<th></th>
</tr>
<% @metacodes.each do |metacode| %>
<tr>
<td><%= metacode.name %></td>
<td class="iconURL"><%= metacode.icon %></td>
<% if metacode.color %>
<td class="iconColor" style="background-color: <%= metacode.color %>">
<%= metacode.color %>
</td>
<% else %>
<td></td>
<% end %>
<td><%= image_tag metacode.icon, width: 40 %></td>
<td><%= link_to 'Edit', edit_metacode_path(metacode) %></td>
</tr>
<% end %>
</table>
<% @metacodes.each do |metacode| %>
<tr>
<td><%= metacode.name %></td>
<td class="iconURL"><%= metacode.icon %></td>
<% if metacode.color %>
<td class="iconColor" style="background-color: <%= metacode.color %>">
<%= metacode.color %>
</td>
<% else %>
<td></td>
<% end %>
<td><%= image_tag metacode.icon, width: 40 %></td>
<td><%= link_to 'Edit', edit_metacode_path(metacode) %></td>
</tr>
<% end %>
</table>
</div>
</div>

View file

@ -1,5 +1,5 @@
<div id="yield">
<div class='blackBox'>
<div class='centerContent'>
<%= render 'form' %>
</div>
</div>

View file

@ -1,14 +0,0 @@
<div id="exploreMapsHeader">
<div class="exploreMapsBar exploreElement">
<div class="exploreMapsMenu">
<div class="exploreMapsCenter">
<a href="<%= notifications_path %>" class="notificationsLink exploreMapsButton active">
<div class="exploreMapsIcon"></div>Notifications
</a>
<a href="/" class="exploreMapsButton myMaps">
<div class="exploreMapsIcon"></div>Maps
</a>
</div>
</div>
</div>
</div>

View file

@ -8,7 +8,7 @@
</header>
<ul class="notifications">
<% blacklist = [MAP_ACCESS_REQUEST, MAP_ACCESS_APPROVED, MAP_INVITE_TO_EDIT] %>
<% notifications = @notifications.to_a.delete_if{|n| blacklist.include?(n.notification_code) && (n.notified_object.nil? || n.notified_object.map.nil?) }%>
<% notifications = @notifications.to_a.delete_if{|n| blacklist.include?(n.notification_code) && (n.notified_object.nil? || n.notified_object.map.nil?) }%>
<% notifications.each do |notification| %>
<% receipt = @receipts.find_by(notification_id: notification.id) %>
<li class="notification <%= receipt.is_read? ? 'read' : 'unread' %>" id="notification-<%= notification.id %>">
@ -76,5 +76,3 @@
</div>
<% end %>
</div>
<%= render partial: 'notifications/header' %>

View file

@ -4,4 +4,4 @@ $('#notification-<%= @notification.id %> .notification-read-unread > a')
$('#notification-<%= @notification.id %>')
.removeClass('unread')
.addClass('read')
Metamaps.GlobalUI.NotificationIcon.render(Metamaps.GlobalUI.NotificationIcon.unreadNotificationsCount - 1)
Metamaps.GlobalUI.Notifications.decrementUnread(Metamaps.GlobalUI.ReactApp.render)

View file

@ -4,4 +4,4 @@ $('#notification-<%= @notification.id %> .notification-read-unread > a')
$('#notification-<%= @notification.id %>')
.removeClass('read')
.addClass('unread')
Metamaps.GlobalUI.NotificationIcon.render(Metamaps.GlobalUI.NotificationIcon.unreadNotificationsCount + 1)
Metamaps.GlobalUI.Notifications.incrementUnread(Metamaps.GlobalUI.ReactApp.render)

View file

@ -9,7 +9,7 @@
<h2 class="notification-title">
<% case @notification.notification_code
when MAP_ACCESS_REQUEST
request = @notification.notified_object
request = @notification.notified_object
map = request.map %>
<%= image_tag @notification.sender.image(:thirtytwo), class: 'thirty-two-avatar' %> <span style='font-weight:bold;' class='requesterName'><%= request.user.name %></span> wants to collaborate on map <span style='font-weight:bold;'><%= map.name %></span>
<% else %>
@ -24,7 +24,7 @@
<% if request.approved %>
You already responded to this access request, and allowed access.
<% elsif !request.approved %>
You already responded to this access request, and declined access. If you changed your mind, you can still grant
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.
<% end %>
<% else %>
@ -50,5 +50,3 @@
<% end %>
</div>
</div>
<%= render partial: 'notifications/header' %>

View file

@ -52,7 +52,7 @@
<% end %>
<%= settings.label :follow_topic_on_contributed, class: 'firstFieldText' do %>
<%= settings.check_box :follow_topic_on_contributed, class: 'inline' %>
Auto-follow topics you edit
Auto-follow topics you edit.
<% end %>
<%= settings.label :follow_map_on_created, class: 'firstFieldText' do %>
<%= settings.check_box :follow_map_on_created, class: 'inline' %>

View file

@ -19,15 +19,15 @@ module Metamaps
end
# Custom directories with classes and modules you want to be autoloadable.
config.autoload_paths << Rails.root.join('app', 'services')
config.autoload_paths << Rails.root.join('app', 'decorators', 'services')
# Configure the default encoding used in templates for Ruby 1.9.
config.encoding = 'utf-8'
config.to_prepare do
Doorkeeper::ApplicationsController.layout 'doorkeeper'
Doorkeeper::AuthorizationsController.layout 'doorkeeper'
Doorkeeper::AuthorizedApplicationsController.layout 'doorkeeper'
Doorkeeper::ApplicationsController.layout 'application'
Doorkeeper::AuthorizationsController.layout 'application'
Doorkeeper::AuthorizedApplicationsController.layout 'application'
Doorkeeper::ApplicationController.helper ApplicationHelper
end

View file

@ -1,9 +1,14 @@
Delayed::Worker.class_eval do
def handle_failed_job_with_notification(job, error)
handle_failed_job_without_notification(job, error)
ExceptionNotifier.notify_exception(error)
end
alias_method_chain :handle_failed_job, :notification
# frozen_string_literal: true
module ExceptionNotifierInDelayedJob
def handle_failed_job(job, error)
super
ExceptionNotfier.notify_exception(error)
end
end
Delayed::Worker.class_eval do
prepend ExceptionNotifierInDelayedJob
end
Delayed::Worker.logger = Logger.new(File.join(Rails.root, 'log', 'delayed_job.log'))

View file

@ -163,9 +163,9 @@ If your system uses systemd for init scripts, ptu the following code into `/etc/
User=metamaps
Group=metamaps
Environment=HOME=/home/metamaps
Environment=PATH="/usr/local/rvm/gems/ruby-2.3.0@metamaps/bin:/usr/local/rvm/gems/ruby-2.3.0@global/bin:/usr/local/rvm/rubies/ruby-2.3.0/bin:/usr/local/rvm/bin:/usr/local/bin:/usr/bin:/bin"
Environment=GEM_PATH="/usr/local/rvm/gems/ruby-2.3.0@metamaps:/usr/local/rvm/gems/ruby-2.3.0@global"
Environment=RAILS_ENV="production"
Environment=PATH=/usr/local/rvm/gems/ruby-2.3.0@metamaps/bin:/usr/local/rvm/gems/ruby-2.3.0@global/bin:/usr/local/rvm/rubies/ruby-2.3.0/bin:/usr/local/rvm/bin:/usr/local/bin:/usr/bin:/bin
Environment=GEM_PATH=/usr/local/rvm/gems/ruby-2.3.0@metamaps:/usr/local/rvm/gems/ruby-2.3.0@global
Environment=RAILS_ENV=production
[Install]
WantedBy=multi-user.target
@ -174,3 +174,13 @@ Then start the service and check the last ten lines of the log file to make sure
sudo systemctl start metamaps_delayed_job
# ??? how the heck do you check systemd logs??
##### initial service startup
sudo systemctl enable metamaps_delayed_job
sudo systemctl start metamaps_delayed_job
sudo systemctl status metamaps_delayed_job
##### after changing
sudo systemctl daemon-reload
sudo systemctl restart metamaps_delayed_job
sudo systemctl status metamaps_delayed_job

View file

@ -4,7 +4,7 @@ import React from 'react'
import ReactDOM from 'react-dom'
import outdent from 'outdent'
import ImportDialogBox from '../../components/MapView/ImportDialogBox'
import ImportDialogBox from '../../routes/MapView/ImportDialogBox'
import PasteInput from '../PasteInput'
import Map from '../Map'

View file

@ -0,0 +1,63 @@
/* global $ */
import GlobalUI from './index'
const Notifications = {
notifications: null,
unreadNotificationsCount: 0,
init: serverData => {
Notifications.unreadNotificationsCount = serverData.unreadNotificationsCount
},
fetch: render => {
$.ajax({
url: '/notifications.json',
success: function(data) {
Notifications.notifications = data
render()
}
})
},
incrementUnread: (render) => {
Notifications.unreadNotificationsCount++
render()
},
decrementUnread: (render) => {
Notifications.unreadNotificationsCount--
render()
},
markAsRead: (render, id) => {
const n = Notifications.notifications.find(n => n.id === id)
$.ajax({
url: `/notifications/${id}/mark_read.json`,
method: 'PUT',
success: function(r) {
if (n) {
Notifications.unreadNotificationsCount--
n.is_read = true
render()
}
},
error: function() {
GlobalUI.notifyUser('There was an error marking that notification as read')
}
})
},
markAsUnread: (render, id) => {
const n = Notifications.notifications.find(n => n.id === id)
$.ajax({
url: `/notifications/${id}/mark_unread.json`,
method: 'PUT',
success: function() {
if (n) {
Notifications.unreadNotificationsCount++
n.is_read = false
render()
}
},
error: function() {
GlobalUI.notifyUser('There was an error marking that notification as read')
}
})
}
}
export default Notifications

View file

@ -4,19 +4,21 @@ import React from 'react'
import ReactDOM from 'react-dom'
import { Router, browserHistory } from 'react-router'
import { merge } from 'lodash'
import apply from 'async/apply'
import { notifyUser } from './index.js'
import ImportDialog from './ImportDialog'
import Notifications from './Notifications'
import Active from '../Active'
import DataModel from '../DataModel'
import { ExploreMaps, ChatView, TopicCard } from '../Views'
import { ExploreMaps, ChatView, TopicCard, ContextMenu } from '../Views'
import Filter from '../Filter'
import JIT from '../JIT'
import Realtime from '../Realtime'
import Map, { InfoBox } from '../Map'
import Topic from '../Topic'
import Visualize from '../Visualize'
import makeRoutes from '../../components/makeRoutes'
import makeRoutes from '../../routes/makeRoutes'
let routes
// 220 wide + 16 padding on both sides
@ -29,7 +31,6 @@ const ReactApp = {
serverData: {},
mapId: null,
topicId: null,
unreadNotificationsCount: 0,
mapsWidth: 0,
toast: '',
mobile: false,
@ -39,7 +40,6 @@ const ReactApp = {
init: function(serverData, openLightbox) {
const self = ReactApp
self.serverData = serverData
self.unreadNotificationsCount = serverData.unreadNotificationsCount
self.mobileTitle = serverData.mobileTitle
self.openLightbox = openLightbox
self.metacodeSets = serverData.metacodeSets
@ -98,7 +98,7 @@ const ReactApp = {
getProps: function() {
const self = ReactApp
return merge({
unreadNotificationsCount: self.unreadNotificationsCount,
unreadNotificationsCount: Notifications.unreadNotificationsCount,
currentUser: Active.Mapper,
toast: self.toast,
mobile: self.mobile,
@ -106,13 +106,18 @@ const ReactApp = {
mobileTitleWidth: self.mobileTitleWidth,
mobileTitleClick: (e) => Active.Map && InfoBox.toggleBox(e),
openInviteLightbox: () => self.openLightbox('invite'),
serverData: self.serverData
serverData: self.serverData,
notifications: Notifications.notifications,
fetchNotifications: apply(Notifications.fetch, ReactApp.render),
markAsRead: apply(Notifications.markAsRead, ReactApp.render),
markAsUnread: apply(Notifications.markAsUnread, ReactApp.render)
},
self.getMapProps(),
self.getTopicProps(),
self.getFilterProps(),
self.getCommonProps(),
self.getMapsProps(),
self.getContextMenuProps(),
self.getTopicCardProps(),
self.getChatProps())
},
@ -155,6 +160,28 @@ const ReactApp = {
onTopicFollow: Topic.onTopicFollow
}
},
getContextMenuProps: function() {
const { render } = ReactApp
return {
// values
contextMenu: !!(ContextMenu.clickedNode || ContextMenu.clickedEdge),
contextNode: ContextMenu.clickedNode,
contextEdge: ContextMenu.clickedEdge,
contextPos: ContextMenu.pos,
contextFetchingSiblingsData: ContextMenu.fetchingSiblingsData,
contextSiblingsData: ContextMenu.siblingsData,
// functions
contextDelete: apply(ContextMenu.delete, render),
contextRemove: apply(ContextMenu.remove, render),
contextHide: apply(ContextMenu.hide, render),
contextCenterOn: apply(ContextMenu.centerOn, render),
contextPopoutTopic: apply(ContextMenu.popoutTopic, render),
contextUpdatePermissions: apply(ContextMenu.updatePermissions, render),
contextOnMetacodeSelect: apply(ContextMenu.onMetacodeSelect, render),
contextFetchSiblings: apply(ContextMenu.fetchSiblings, render),
contextPopulateSiblings: apply(ContextMenu.populateSiblings, render)
}
},
getTopicProps: function() {
const self = ReactApp
return {

View file

@ -4,6 +4,7 @@ import clipboard from 'clipboard-js'
import Create from '../Create'
import Notifications from './Notifications'
import ReactApp from './ReactApp'
import Search from './Search'
import CreateMap from './CreateMap'
@ -17,6 +18,7 @@ const GlobalUI = {
init: function(serverData) {
const self = GlobalUI
self.Notifications.init(serverData)
self.ReactApp.init(serverData, self.openLightbox)
self.CreateMap.init(serverData)
self.ImportDialog.init(serverData, self.openLightbox, self.closeLightbox)
@ -151,5 +153,5 @@ const GlobalUI = {
}
}
export { ReactApp, Search, CreateMap, ImportDialog }
export { Notifications, ReactApp, Search, CreateMap, ImportDialog }
export default GlobalUI

View file

@ -1,16 +1,12 @@
/* global $, Image, CanvasLoader */
/* global $, Image */
import _ from 'lodash'
import outdent from 'outdent'
import clipboard from 'clipboard-js'
import React from 'react'
import ReactDOM from 'react-dom'
import $jit from '../patched/JIT'
import MetacodeSelect from '../components/MetacodeSelect'
import Active from './Active'
import ContextMenu from './Views/ContextMenu'
import Control from './Control'
import Create from './Create'
import DataModel from './DataModel'
@ -349,7 +345,7 @@ const JIT = {
// Add also a click handler to nodes
onClick: function(node, eventInfo, e) {
// remove the rightclickmenu
$('.rightclickmenu').remove()
ContextMenu.reset(ReactApp.render)
if (Mouse.boxStartCoordinates) {
if (e.ctrlKey) {
@ -390,7 +386,7 @@ const JIT = {
// Add also a click handler to nodes
onRightClick: function(node, eventInfo, e) {
// remove the rightclickmenu
$('.rightclickmenu').remove()
ContextMenu.reset(ReactApp.render)
if (Mouse.boxStartCoordinates) {
Create.newSynapse.hide()
@ -1006,7 +1002,7 @@ const JIT = {
TopicCard.hideCard()
SynapseCard.hideCard()
Create.newTopic.hide()
$('.rightclickmenu').remove()
ContextMenu.reset(ReactApp.render)
// reset the draw synapse positions to false
Mouse.synapseStartCoordinates = []
Mouse.synapseEndCoordinates = null
@ -1346,230 +1342,12 @@ const JIT = {
selectNodeOnRightClickHandler: function(node, e) {
// the 'node' variable is a JIT node, the one that was clicked on
// the 'e' variable is the click event
e.preventDefault()
e.stopPropagation()
if (Visualize.mGraph.busy) return
// select the node
Control.selectNode(node, e)
// delete old right click menu
$('.rightclickmenu').remove()
// create new menu for clicked on node
const rightclickmenu = document.createElement('div')
rightclickmenu.className = 'rightclickmenu'
// prevent the custom context menu from immediately opening the default context menu as well
rightclickmenu.setAttribute('oncontextmenu', 'return false')
// add the proper options to the menu
let menustring = '<ul>'
const authorized = Active.Map && Active.Map.authorizeToEdit(Active.Mapper)
const disabled = authorized ? '' : 'disabled'
if (Active.Map) menustring += '<li class="rc-hide"><div class="rc-icon"></div>Hide until refresh<div class="rc-keyboard">Ctrl+H</div></li>'
if (Active.Map && Active.Mapper) menustring += '<li class="rc-remove ' + disabled + '"><div class="rc-icon"></div>Remove from map<div class="rc-keyboard">Ctrl+M</div></li>'
if (Active.Topic) menustring += '<li class="rc-remove"><div class="rc-icon"></div>Remove from view<div class="rc-keyboard">Ctrl+M</div></li>'
if (Active.Map && Active.Mapper) menustring += '<li class="rc-delete ' + disabled + '"><div class="rc-icon"></div>Delete<div class="rc-keyboard">Ctrl+D</div></li>'
if (Active.Topic) {
menustring += '<li class="rc-center"><div class="rc-icon"></div>Center this topic<div class="rc-keyboard">Alt+E</div></li>'
}
menustring += '<li class="rc-popout"><div class="rc-icon"></div>Open in new tab</li>'
if (Active.Mapper) {
const options = outdent`
<ul>
<li class="changeP toCommons"><div class="rc-perm-icon"></div>commons</li>
<li class="changeP toPublic"><div class="rc-perm-icon"></div>public</li>
<li class="changeP toPrivate"><div class="rc-perm-icon"></div>private</li>
</ul>`
menustring += '<li class="rc-spacer"></li>'
menustring += outdent`
<li class="rc-permission">
<div class="rc-icon"></div>
Change permissions
${options}
<div class="expandLi"></div>
</li>`
menustring += '<li class="rc-metacode"><div class="rc-icon"></div>Change metacode<div id="metacodeOptionsWrapper"></div><div class="expandLi"></div></li>'
}
if (Active.Topic) {
if (!Active.Mapper) {
menustring += '<li class="rc-spacer"></li>'
}
// set up the get sibling menu as a "lazy load"
// only fill in the submenu when they hover over the get siblings list item
const siblingMenu = outdent`
<ul id="fetchSiblingList">
<li class="fetchAll">All<div class="rc-keyboard">Alt+R</div></li>
<li id="loadingSiblings"></li>
</ul>`
menustring += '<li class="rc-siblings"><div class="rc-icon"></div>Reveal siblings' + siblingMenu + '<div class="expandLi"></div></li>'
}
menustring += '</ul>'
rightclickmenu.innerHTML = menustring
// position the menu where the click happened
const position = {}
const RIGHTCLICK_WIDTH = 300
const RIGHTCLICK_HEIGHT = 144 // this does vary somewhat, but we can use static
const SUBMENUS_WIDTH = 256
const MAX_SUBMENU_HEIGHT = 270
const windowWidth = $(window).width()
const windowHeight = $(window).height()
if (windowWidth - e.clientX < SUBMENUS_WIDTH) {
position.right = windowWidth - e.clientX
$(rightclickmenu).addClass('moveMenusToLeft')
} else if (windowWidth - e.clientX < RIGHTCLICK_WIDTH) {
position.right = windowWidth - e.clientX
} else if (windowWidth - e.clientX < RIGHTCLICK_WIDTH + SUBMENUS_WIDTH) {
position.left = e.clientX
$(rightclickmenu).addClass('moveMenusToLeft')
} else {
position.left = e.clientX
}
if (windowHeight - e.clientY < MAX_SUBMENU_HEIGHT) {
position.bottom = windowHeight - e.clientY
$(rightclickmenu).addClass('moveMenusUp')
} else if (windowHeight - e.clientY < RIGHTCLICK_HEIGHT + MAX_SUBMENU_HEIGHT) {
position.top = e.clientY
$(rightclickmenu).addClass('moveMenusUp')
} else {
position.top = e.clientY
}
$(rightclickmenu).css(position)
// add the menu to the page
$('#wrapper').append(rightclickmenu)
ReactDOM.render(
React.createElement(MetacodeSelect, {
onMetacodeSelect: metacodeId => {
if (Selected.Nodes.length > 1) {
// batch update multiple topics
Control.updateSelectedMetacodes(metacodeId)
} else {
const topic = DataModel.Topics.get(node.id)
topic.save({
metacode_id: metacodeId
})
}
$(rightclickmenu).remove()
},
metacodeSets: ReactApp.metacodeSets
}),
document.getElementById('metacodeOptionsWrapper')
)
// attach events to clicks on the list items
// delete the selected things from the database
if (authorized) {
$('.rc-delete').click(function() {
$('.rightclickmenu').remove()
Control.deleteSelected()
})
}
// remove the selected things from the map
if (Active.Topic || authorized) {
$('.rc-remove').click(function() {
$('.rightclickmenu').remove()
Control.removeSelectedEdges()
Control.removeSelectedNodes()
})
}
// hide selected nodes and synapses until refresh
$('.rc-hide').click(function() {
$('.rightclickmenu').remove()
Control.hideSelectedEdges()
Control.hideSelectedNodes()
})
// when in radial, center on the topic you picked
$('.rc-center').click(function() {
$('.rightclickmenu').remove()
Topic.centerOn(node.id)
})
// open the entity in a new tab
$('.rc-popout').click(function() {
$('.rightclickmenu').remove()
const win = window.open('/topics/' + node.id, '_blank')
win.focus()
})
// change the permission of all the selected nodes and synapses that you were the originator of
$('.rc-permission li').click(function() {
$('.rightclickmenu').remove()
// $(this).text() will be 'commons' 'public' or 'private'
Control.updateSelectedPermissions($(this).text())
})
// fetch relatives
let fetchSent = false
$('.rc-siblings').hover(function() {
if (!fetchSent) {
JIT.populateRightClickSiblings(node)
fetchSent = true
}
})
$('.rc-siblings .fetchAll').click(function() {
$('.rightclickmenu').remove()
// data-id is a metacode id
Topic.fetchRelatives(node)
})
ContextMenu.selectNode(ReactApp.render, node, {x: e.clientX, y: e.clientY})
}, // selectNodeOnRightClickHandler,
populateRightClickSiblings: function(node) {
// depending on how many topics are selected, do different things
const topic = node.getData('topic')
// add a loading icon for now
const loader = new CanvasLoader('loadingSiblings')
loader.setColor('#4FC059') // default is '#000000'
loader.setDiameter(15) // default is 40
loader.setDensity(41) // default is 40
loader.setRange(0.9) // default is 1.3
loader.show() // Hidden by default
const topics = DataModel.Topics.map(function(t) { return t.id })
const topicsString = topics.join()
const successCallback = function(data) {
$('#loadingSiblings').remove()
for (var key in data) {
const string = `${DataModel.Metacodes.get(key).get('name')} (${data[key]})`
$('#fetchSiblingList').append(`<li class="getSiblings" data-id="${key}">${string}</li>`)
}
$('.rc-siblings .getSiblings').click(function() {
$('.rightclickmenu').remove()
// data-id is a metacode id
Topic.fetchRelatives(node, $(this).attr('data-id'))
})
}
$.ajax({
type: 'GET',
url: '/topics/' + topic.id + '/relative_numbers.json?network=' + topicsString,
success: successCallback,
error: function() {}
})
},
selectEdgeOnClickHandler: function(adj, e) {
if (Visualize.mGraph.busy) return
@ -1611,113 +1389,14 @@ const JIT = {
}
}, // selectEdgeOnClickHandler
selectEdgeOnRightClickHandler: function(adj, e) {
// the 'node' variable is a JIT node, the one that was clicked on
// the 'adj' variable is a JIT adjacency, the one that was clicked on
// the 'e' variable is the click event
if (adj.getData('alpha') === 0) return // don't do anything if the edge is filtered
e.preventDefault()
e.stopPropagation()
if (Visualize.mGraph.busy) return
Control.selectEdge(adj)
// delete old right click menu
$('.rightclickmenu').remove()
// create new menu for clicked on node
const rightclickmenu = document.createElement('div')
rightclickmenu.className = 'rightclickmenu'
// prevent the custom context menu from immediately opening the default context menu as well
rightclickmenu.setAttribute('oncontextmenu', 'return false')
// add the proper options to the menu
let menustring = '<ul>'
const authorized = Active.Map && Active.Map.authorizeToEdit(Active.Mapper)
const disabled = authorized ? '' : 'disabled'
if (Active.Map) menustring += '<li class="rc-hide"><div class="rc-icon"></div>Hide until refresh<div class="rc-keyboard">Ctrl+H</div></li>'
if (Active.Map && Active.Mapper) menustring += '<li class="rc-remove ' + disabled + '"><div class="rc-icon"></div>Remove from map<div class="rc-keyboard">Ctrl+M</div></li>'
if (Active.Topic) menustring += '<li class="rc-remove"><div class="rc-icon"></div>Remove from view<div class="rc-keyboard">Ctrl+M</div></li>'
if (Active.Map && Active.Mapper) menustring += '<li class="rc-delete ' + disabled + '"><div class="rc-icon"></div>Delete<div class="rc-keyboard">Ctrl+D</div></li>'
if (Active.Map && Active.Mapper) menustring += '<li class="rc-spacer"></li>'
if (Active.Mapper) {
const permOptions = outdent`
<ul>
<li class="changeP toCommons"><div class="rc-perm-icon"></div>commons</li>
<li class="changeP toPublic"><div class="rc-perm-icon"></div>public</li> <li class="changeP toPrivate"><div class="rc-perm-icon"></div>private</li> </ul>`
menustring += '<li class="rc-permission"><div class="rc-icon"></div>Change permissions' + permOptions + '<div class="expandLi"></div></li>'
}
menustring += '</ul>'
rightclickmenu.innerHTML = menustring
// position the menu where the click happened
const position = {}
const RIGHTCLICK_WIDTH = 300
const RIGHTCLICK_HEIGHT = 144 // this does vary somewhat, but we can use static
const SUBMENUS_WIDTH = 256
const MAX_SUBMENU_HEIGHT = 270
const windowWidth = $(window).width()
const windowHeight = $(window).height()
if (windowWidth - e.clientX < SUBMENUS_WIDTH) {
position.right = windowWidth - e.clientX
$(rightclickmenu).addClass('moveMenusToLeft')
} else if (windowWidth - e.clientX < RIGHTCLICK_WIDTH) {
position.right = windowWidth - e.clientX
} else position.left = e.clientX
if (windowHeight - e.clientY < MAX_SUBMENU_HEIGHT) {
position.bottom = windowHeight - e.clientY
$(rightclickmenu).addClass('moveMenusUp')
} else if (windowHeight - e.clientY < RIGHTCLICK_HEIGHT + MAX_SUBMENU_HEIGHT) {
position.top = e.clientY
$(rightclickmenu).addClass('moveMenusUp')
} else position.top = e.clientY
$(rightclickmenu).css(position)
// add the menu to the page
$('#wrapper').append(rightclickmenu)
// attach events to clicks on the list items
// delete the selected things from the database
if (authorized) {
$('.rc-delete').click(function() {
$('.rightclickmenu').remove()
Control.deleteSelected()
})
}
// remove the selected things from the map
if (authorized) {
$('.rc-remove').click(function() {
$('.rightclickmenu').remove()
Control.removeSelectedEdges()
Control.removeSelectedNodes()
})
}
// hide selected nodes and synapses until refresh
$('.rc-hide').click(function() {
$('.rightclickmenu').remove()
Control.hideSelectedEdges()
Control.hideSelectedNodes()
})
// change the permission of all the selected nodes and synapses that you were the originator of
$('.rc-permission li').click(function() {
$('.rightclickmenu').remove()
// $(this).text() will be 'commons' 'public' or 'private'
Control.updateSelectedPermissions($(this).text())
})
ContextMenu.selectEdge(ReactApp.render, adj, {x: e.clientX, y: e.clientY})
}, // selectEdgeOnRightClickHandler
SmoothPanning: function() {
const sx = Visualize.mGraph.canvas.scaleOffsetX

View file

@ -152,12 +152,12 @@ const Listeners = {
var node = nodes[nodes.length - 1]
if (opts.center && opts.reveal) {
Topic.centerOn(node.id, function() {
Topic.fetchRelatives(nodes)
Topic.fetchSiblings(nodes)
})
} else if (opts.center) {
Topic.centerOn(node.id)
} else if (opts.reveal) {
Topic.fetchRelatives(nodes)
Topic.fetchSiblings(nodes)
}
}
}

View file

@ -16,6 +16,7 @@ import Loading from '../Loading'
import Realtime from '../Realtime'
import Selected from '../Selected'
import SynapseCard from '../SynapseCard'
import ContextMenu from '../Views/ContextMenu'
import TopicCard from '../Views/TopicCard'
import Visualize from '../Visualize'
@ -137,7 +138,7 @@ const Map = {
if (Active.Map) {
$('.main').removeClass('compressed')
AutoLayout.resetSpiral()
$('.rightclickmenu').remove()
ContextMenu.reset(ReactApp.render)
TopicCard.hideCard()
SynapseCard.hideCard()
Create.newTopic.hide(true) // true means force (and override pinned)

View file

@ -15,6 +15,7 @@ import Selected from './Selected'
import Settings from './Settings'
import SynapseCard from './SynapseCard'
import TopicCard from './Views/TopicCard'
import ContextMenu from './Views/ContextMenu'
import Util from './Util'
import Visualize from './Visualize'
@ -68,13 +69,13 @@ const Topic = {
},
end: function() {
if (Active.Topic) {
$('.rightclickmenu').remove()
ContextMenu.reset(ReactApp.render)
TopicCard.hideCard()
SynapseCard.hideCard()
}
},
centerOn: function(nodeid, callback) {
// don't clash with fetchRelatives
// don't clash with fetchSiblings
if (!Visualize.mGraph.busy) {
Visualize.mGraph.onClick(nodeid, {
hideLabels: false,
@ -100,10 +101,10 @@ const Topic = {
}
ReactApp.render()
},
fetchRelatives: function(nodes, metacodeId) {
fetchSiblings: function(nodes, metacodeId) {
var self = this
var node = $.isArray(nodes) ? nodes[0] : nodes
var node = Array.isArray(nodes) ? nodes[0] : nodes
var topics = DataModel.Topics.map(function(t) { return t.id })
var topicsString = topics.join()
@ -155,8 +156,8 @@ const Topic = {
}
})
})
if ($.isArray(nodes) && nodes.length > 1) {
self.fetchRelatives(nodes.slice(1), metacodeId)
if (Array.isArray(nodes) && nodes.length > 1) {
self.fetchSiblings(nodes.slice(1), metacodeId)
}
}

View file

@ -0,0 +1,108 @@
/* global $ */
import Control from '../Control'
import DataModel from '../DataModel'
import Selected from '../Selected'
import Topic from '../Topic'
const ContextMenu = {
clickedNode: null,
clickedEdge: null,
pos: {x: 0, y: 0},
fetchingSiblingsData: false,
siblingsData: null,
selectNode: (render, node, pos) => {
ContextMenu.pos = pos
ContextMenu.clickedNode = node
ContextMenu.clickedEdge = null
ContextMenu.fetchingSiblingsData = false
ContextMenu.siblingsData = null
render()
},
selectEdge: (render, edge, pos) => {
ContextMenu.pos = pos
ContextMenu.clickedNode = null
ContextMenu.clickedEdge = edge
ContextMenu.fetchingSiblingsData = false
ContextMenu.siblingsData = null
render()
},
reset: (render) => {
ContextMenu.fetchingSiblingsData = false
ContextMenu.siblingsData = null
ContextMenu.clickedNode = null
ContextMenu.clickedEdge = null
render()
},
delete: (render) => {
Control.deleteSelected()
ContextMenu.reset(render)
},
remove: (render) => {
Control.removeSelectedEdges()
Control.removeSelectedNodes()
ContextMenu.reset(render)
},
hide: (render) => {
Control.hideSelectedEdges()
Control.hideSelectedNodes()
ContextMenu.reset(render)
},
centerOn: (render, id) => {
Topic.centerOn(id)
ContextMenu.reset(render)
},
popoutTopic: (render, id) => {
ContextMenu.reset(render)
const win = window.open(`/topics/${id}`, '_blank')
win.focus()
},
updatePermissions: (render, permission) => {
// will be 'commons' 'public' or 'private'
Control.updateSelectedPermissions(permission)
ContextMenu.reset(render)
},
onMetacodeSelect: (render, id, metacodeId) => {
if (Selected.Nodes.length > 1) {
// batch update multiple topics
Control.updateSelectedMetacodes(metacodeId)
} else {
const topic = DataModel.Topics.get(id)
topic.save({
metacode_id: metacodeId
})
}
ContextMenu.reset(render)
},
fetchSiblings: (render, node, metacodeId) => {
Topic.fetchSiblings(node, metacodeId)
ContextMenu.reset(render)
},
populateSiblings: (render, id) => {
// depending on how many topics are selected, do different things
ContextMenu.fetchingSiblingsData = true
render()
const topics = DataModel.Topics.map(function(t) { return t.id })
const topicsString = topics.join()
const successCallback = function(data) {
ContextMenu.fetchingSiblingsData = false
// adjust the data for consumption by react
for (var key in data) {
data[key] = `${DataModel.Metacodes.get(key).get('name')} (${data[key]})`
}
ContextMenu.siblingsData = data
render()
}
$.ajax({
type: 'GET',
url: `/topics/${id}/relative_numbers.json?network=${topicsString}`,
success: successCallback,
error: function() {}
})
}
}
export default ContextMenu

View file

@ -1,5 +1,6 @@
/* global $ */
import ContextMenu from './ContextMenu'
import ExploreMaps from './ExploreMaps'
import ChatView from './ChatView'
import VideoView from './VideoView'
@ -12,6 +13,7 @@ const Views = {
$(document).on(JUNTO_UPDATED, () => ExploreMaps.render())
ChatView.init([serverData['sounds/MM_sounds.mp3'], serverData['sounds/MM_sounds.ogg']])
},
ContextMenu,
ExploreMaps,
ChatView,
VideoView,
@ -19,5 +21,5 @@ const Views = {
TopicCard
}
export { ExploreMaps, ChatView, VideoView, Room, TopicCard }
export { ContextMenu, ExploreMaps, ChatView, VideoView, Room, TopicCard }
export default Views

View file

@ -9,7 +9,7 @@ import DataModel from './DataModel'
import Debug from './Debug'
import Filter from './Filter'
import GlobalUI, {
ReactApp, Search, CreateMap, ImportDialog
Notifications, ReactApp, Search, CreateMap, ImportDialog
} from './GlobalUI'
import Import from './Import'
import JIT from './JIT'
@ -42,6 +42,7 @@ Metamaps.DataModel = DataModel
Metamaps.Debug = Debug
Metamaps.Filter = Filter
Metamaps.GlobalUI = GlobalUI
Metamaps.GlobalUI.Notifications = Notifications
Metamaps.GlobalUI.ReactApp = ReactApp
Metamaps.GlobalUI.Search = Search
Metamaps.GlobalUI.CreateMap = CreateMap

View file

@ -0,0 +1,248 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import MetacodeSelect from './MetacodeSelect'
class ContextMenu extends Component {
static propTypes = {
topicId: PropTypes.string,
mapId: PropTypes.string,
currentUser: PropTypes.object,
map: PropTypes.object,
contextNode: PropTypes.object,
contextEdge: PropTypes.object,
contextPos: PropTypes.object,
contextFetchingSiblingsData: PropTypes.bool,
contextSiblingsData: PropTypes.object,
metacodeSets: PropTypes.array,
contextDelete: PropTypes.func,
contextRemove: PropTypes.func,
contextHide: PropTypes.func,
contextCenterOn: PropTypes.func,
contextPopoutTopic: PropTypes.func,
contextUpdatePermissions: PropTypes.func,
contextOnMetacodeSelect: PropTypes.func,
contextFetchSiblings: PropTypes.func,
contextPopulateSiblings: PropTypes.func
}
constructor(props) {
super(props)
this.state = {
populateSiblingsSent: false
}
}
getPositionData = () => {
const { contextPos } = this.props
let extraClasses = []
const position = {}
// TODO: make these dynamic values so that the ContextMenu can
// change height and still work properly
const RIGHTCLICK_WIDTH = 300
const RIGHTCLICK_HEIGHT = 144 // this does vary somewhat, but we can use static
const SUBMENUS_WIDTH = 256
const MAX_SUBMENU_HEIGHT = 270
const windowWidth = document.documentElement.clientWidth
const windowHeight = document.documentElement.clientHeight
if (windowWidth - contextPos.x < SUBMENUS_WIDTH) {
position.right = windowWidth - contextPos.x
extraClasses.push('moveMenusToLeft')
} else if (windowWidth - contextPos.x < RIGHTCLICK_WIDTH) {
position.right = windowWidth - contextPos.x
} else if (windowWidth - contextPos.x < RIGHTCLICK_WIDTH + SUBMENUS_WIDTH) {
position.left = contextPos.x
extraClasses.push('moveMenusToLeft')
} else {
position.left = contextPos.x
}
if (windowHeight - contextPos.y < MAX_SUBMENU_HEIGHT) {
position.bottom = windowHeight - contextPos.y
extraClasses.push('moveMenusUp')
} else if (windowHeight - contextPos.y < RIGHTCLICK_HEIGHT + MAX_SUBMENU_HEIGHT) {
position.top = contextPos.y
extraClasses.push('moveMenusUp')
} else {
position.top = contextPos.y
}
return {
pos: {
top: position.top && position.top + 'px',
bottom: position.bottom && position.bottom + 'px',
left: position.left && position.left + 'px',
right: position.right && position.right + 'px'
},
extraClasses
}
}
hide = () => {
const { contextHide } = this.props
return <li className='rc-hide' onClick={contextHide}>
<div className='rc-icon' />
Hide until refresh
<div className='rc-keyboard'>Ctrl+H</div>
</li>
}
remove = () => {
const { contextRemove, map, currentUser } = this.props
const canEditMap = map && map.authorizeToEdit(currentUser)
if (!canEditMap) {
return null
}
return <li className='rc-remove' onClick={contextRemove}>
<div className='rc-icon' />
Remove from map
<div className='rc-keyboard'>Ctrl+M</div>
</li>
}
delete = () => {
const { contextDelete, map, currentUser } = this.props
const canEditMap = map && map.authorizeToEdit(currentUser)
if (!canEditMap) {
return null
}
return <li className='rc-delete' onClick={contextDelete}>
<div className='rc-icon' />
Delete
<div className='rc-keyboard'>Ctrl+D</div>
</li>
}
center = () => {
const { contextCenterOn, contextNode, topicId } = this.props
if (!(contextNode && topicId)) {
return null
}
return <li className='rc-center'
onClick={() => contextCenterOn(contextNode.id)}>
<div className='rc-icon' />
Center this topic
<div className='rc-keyboard'>Alt+E</div>
</li>
}
popout = () => {
const { contextPopoutTopic, contextNode } = this.props
if (!contextNode) {
return null
}
return <li className='rc-popout'
onClick={() => contextPopoutTopic(contextNode.id)}>
<div className='rc-icon' />
Open in new tab
</li>
}
permission = () => {
const { currentUser, contextUpdatePermissions } = this.props
if (!currentUser) {
return null
}
return <li className='rc-permission'>
<div className='rc-icon' />
Change permissions
<ul>
<li className='changeP toCommons'
onClick={() => contextUpdatePermissions('commons')}>
<div className='rc-perm-icon' />
commons
</li>
<li className='changeP toPublic'
onClick={() => contextUpdatePermissions('public')}>
<div className='rc-perm-icon' />
public
</li>
<li className='changeP toPrivate'
onClick={() => contextUpdatePermissions('private')}>
<div className='rc-perm-icon' />
private
</li>
</ul>
<div className='expandLi' />
</li>
}
metacode = () => {
const { metacodeSets, contextOnMetacodeSelect,
currentUser, contextNode } = this.props
if (!currentUser) {
return null
}
return <li className='rc-metacode'>
<div className='rc-icon' />
Change metacode
<div id='metacodeOptionsWrapper'>
<MetacodeSelect
onMetacodeSelect={id => {
contextOnMetacodeSelect(contextNode && contextNode.id, id)
}}
metacodeSets={metacodeSets} />
</div>
<div className='expandLi' />
</li>
}
siblings = () => {
const { contextPopulateSiblings, contextFetchSiblings,
contextSiblingsData, contextFetchingSiblingsData,
topicId, contextNode } = this.props
const populateSiblings = () => {
if (!this.state.populateSiblingsSent) {
contextPopulateSiblings(contextNode.id)
this.setState({populateSiblingsSent: true})
}
}
if (!(contextNode && topicId)) {
return null
}
return <li className='rc-siblings'
onMouseOver={populateSiblings}>
<div className='rc-icon' />
Reveal siblings
<ul id='fetchSiblingList'>
<li className='fetchAll'
onClick={() => contextFetchSiblings(contextNode)}>
All
<div className='rc-keyboard'>Alt+R</div>
</li>
{contextSiblingsData && Object.keys(contextSiblingsData).map(key => {
return <li key={key}
onClick={() => contextFetchSiblings(contextNode, key)}>
{contextSiblingsData[key]}
</li>
})}
{contextFetchingSiblingsData && <li id='loadingSiblings'>loading...</li>}
</ul>
<div className='expandLi' />
</li>
}
render() {
const { contextNode, currentUser, topicId } = this.props
const positionData = this.getPositionData()
const style = Object.assign({}, {position: 'absolute'}, positionData.pos)
const showSpacer = currentUser || (contextNode && topicId)
return <div style={style}
className={'rightclickmenu ' + positionData.extraClasses.join(' ')}>
<ul>
{this.hide()}
{this.remove()}
{this.delete()}
{this.center()}
{this.popout()}
{showSpacer && <li className='rc-spacer' />}
{this.permission()}
{this.metacode()}
{this.siblings()}
</ul>
</div>
}
}
export default ContextMenu

View file

@ -1,7 +1,7 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import MapInfoBox from '../MapView/MapInfoBox'
import MapInfoBox from '../routes/MapView/MapInfoBox'
class InfoAndHelp extends Component {
static propTypes = {

View file

@ -0,0 +1,92 @@
import React from 'react'
import PropTypes from 'prop-types'
// based on https://www.npmjs.com/package/react-loading-animation
const loadingStyle = {
position: 'relative',
margin: '0 auto',
width: '30px',
height: '30px'
}
const svgStyle = {
animation: 'rotate 2s linear infinite',
height: '100%',
transformOrigin: 'center center',
width: '100%',
position: 'absolute',
top: 0,
bottom: 0,
left: 0,
right: 0,
margin: 'auto'
}
const circleStyle = {
strokeDasharray: '1,200',
strokeDashoffset: '0',
animation: 'dash 1.5s ease-in-out infinite, color 6s ease-in-out infinite',
strokeLinecap: 'round'
}
const animation = `@keyframes rotate {
100% {
transform: rotate(360deg);
}
}
@keyframes dash {
0% {
stroke-dasharray: 1,200;
stroke-dashoffset: 0;
}
50% {
stroke-dasharray: 89,200;
stroke-dashoffset: -35px;
}
100% {
stroke-dasharray: 89,200;
stroke-dashoffset: -124px;
}
}
@keyframes color {
100%, 0% {
stroke: #a354cd;
}
50% {
stroke: #4fb5c0;
}
}`
class Loading extends React.Component {
static propTypes = {
style: PropTypes.object,
width: PropTypes.string,
height: PropTypes.string,
margin: PropTypes.string
}
static defaultProps = {
style: {},
width: '30px',
height: '30px',
margin: '0 auto'
}
render() {
let { width, height, margin, style } = this.props
loadingStyle.width = width
loadingStyle.height = height
loadingStyle.margin = margin
return <div style={Object.assign({}, loadingStyle, style)}>
<style>{animation}</style>
<svg style={svgStyle} viewBox="25 25 50 50">
<circle style={circleStyle} cx="50" cy="50" r="20" fill="none" strokeWidth="4" strokeMiterlimit="10"/>
</svg>
</div>
}
}
export default Loading

View file

@ -1,88 +0,0 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import { Link } from 'react-router'
import _ from 'lodash'
const MapLink = props => {
const { show, text, href, linkClass } = props
const otherProps = _.omit(props, ['show', 'text', 'href', 'linkClass'])
if (!show) {
return null
}
return (
<Link { ...otherProps } to={href} className={linkClass}>
<div className="exploreMapsIcon"></div>
{text}
</Link>
)
}
class Header extends Component {
render = () => {
const { signedIn, section, user } = this.props
const activeClass = (title) => {
let forClass = 'exploreMapsButton'
forClass += ' ' + title + 'Maps'
if (title === 'my' && section === 'mine' ||
title === section) forClass += ' active'
return forClass
}
const explore = section === 'mine' || section === 'active' || section === 'starred' || section === 'shared' || section === 'featured'
const mapper = section === 'mapper'
return (
<div id="exploreMapsHeader">
<div className="exploreMapsBar exploreElement">
<div className="exploreMapsMenu">
<div className="exploreMapsCenter">
<MapLink show={explore}
href={signedIn ? '/' : '/explore/active'}
linkClass={activeClass('active')}
text="All Maps"
/>
<MapLink show={signedIn && explore}
href="/explore/mine"
linkClass={activeClass('my')}
text="My Maps"
/>
<MapLink show={signedIn && explore}
href="/explore/shared"
linkClass={activeClass('shared')}
text="Shared With Me"
/>
<MapLink show={signedIn && explore}
href="/explore/starred"
linkClass={activeClass('starred')}
text="Starred By Me"
/>
<MapLink show={!signedIn && explore}
href="/explore/featured"
linkClass={activeClass('featured')}
text="Featured Maps"
/>
{mapper ? (
<div className='exploreMapsButton active mapperButton'>
{user && <img className='exploreMapperImage' width='24' height='24' src={user.image} />}
{user && <div className='exploreMapperName'>{user.name}&rsquo;s Maps</div>}
<div className='clearfloat'></div>
</div>
) : null }
</div>
</div>
</div>
</div>
)
}
}
Header.propTypes = {
signedIn: PropTypes.bool.isRequired,
section: PropTypes.string.isRequired,
user: PropTypes.object
}
export default Header

View file

@ -2,7 +2,7 @@ import React, { Component } from 'react'
import PropTypes from 'prop-types'
import { Link } from 'react-router'
import Sprite from '../common/Sprite'
import Sprite from './Sprite'
class MobileHeader extends Component {
static propTypes = {

View file

@ -0,0 +1,19 @@
import React, { Component } from 'react'
class NavBar extends Component {
render() {
return (
<div id="navBar">
<div className="navBarContainer">
<div className="navBarMenu">
<div className="navBarCenter">
{this.props.children}
</div>
</div>
</div>
</div>
)
}
}
export default NavBar

View file

@ -0,0 +1,65 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import { Link } from 'react-router'
import _ from 'lodash'
const PROP_LIST = [
'matchChildRoutes',
'hardReload',
'show',
'text',
'href',
'linkClass'
]
class NavBarLink extends Component {
static propTypes = {
matchChildRoutes: PropTypes.bool,
hardReload: PropTypes.bool,
show: PropTypes.bool,
text: PropTypes.string,
href: PropTypes.string,
linkClass: PropTypes.string
}
static contextTypes = {
location: PropTypes.object
}
render = () => {
const {
matchChildRoutes,
hardReload,
show,
text,
href,
linkClass
} = this.props
const { location } = this.context
const otherProps = _.omit(this.props, PROP_LIST)
const classes = ['navBarButton', linkClass]
const active = matchChildRoutes ?
location.pathname.startsWith(href) :
location.pathname === href
if (active) classes.push('active')
if (!show) {
return null
}
if (hardReload) {
return (
<a { ...otherProps } href={href} className={classes.join(' ')}>
{linkClass && <div className="navBarIcon"></div>}
<div className="navBarLinkText">{text}</div>
</a>
)
}
return (
<Link { ...otherProps } to={href} className={classes.join(' ')}>
{linkClass && <div className="navBarIcon"></div>}
<div className="navBarLinkText">{text}</div>
</Link>
)
}
}
export default NavBarLink

View file

@ -0,0 +1,127 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import outdent from 'outdent'
class Notification extends Component {
static propTypes = {
markAsRead: PropTypes.func,
markAsUnread: PropTypes.func,
notification: PropTypes.shape({
id: PropTypes.number.isRequired,
type: PropTypes.string.isRequired,
subject: PropTypes.string.isRequired,
is_read: PropTypes.bool.isRequired,
created_at: PropTypes.string.isRequired,
actor: PropTypes.shape({
id: PropTypes.number,
name: PropTypes.string,
image: PropTypes.string,
admin: PropTypes.boolean
}),
object: PropTypes.object,
map: PropTypes.object,
topic: PropTypes.object,
topic1: PropTypes.object,
topic2: PropTypes.object
})
}
notificationTextHtml = () => {
const { notification } = this.props
let map, topic, topic1, topic2
let result = `<div class='in-bold'>${notification.actor.name}</div>`
switch (notification.type) {
case 'ACCESS_APPROVED':
map = notification.data.map
result += outdent`granted your request to edit map
<span class='in-bold'>${map.name}</span>`
break
case 'ACCESS_REQUEST':
map = notification.data.map
result += outdent`wants permission to map with you on
<span class='in-bold'>${map.name}</span>`
if (!notification.data.object.answered) {
result += '<br /><div class="action">Offer a response</div>'
}
break
case 'INVITE_TO_EDIT':
map = notification.data.map
result += outdent`gave you edit access to map
<span class='in-bold'>${map.name}</span>`
break
case 'TOPIC_ADDED_TO_MAP':
map = notification.data.map
topic = notification.data.topic
result += outdent`added topic <span class='in-bold'>${topic.name}</span>
to map <span class='in-bold'>${map.name}</span>`
break
case 'TOPIC_CONNECTED_1':
topic1 = notification.data.topic1
topic2 = notification.data.topic2
result += outdent`connected <span class='in-bold'>${topic1.name}</span>
to <span class='in-bold'>${topic2.name}</span>`
break
case 'TOPIC_CONNECTED_2':
topic1 = notification.data.topic1
topic2 = notification.data.topic2
result += outdent`connected <span class='in-bold'>${topic2.name}</span>
to <span class='in-bold'>${topic1.name}</span>`
break
case 'MESSAGE_FROM_DEVS':
result += notification.subject
}
return {__html: result}
}
getDate = () => {
const { notification: {created_at} } = this.props
const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
const created = new Date(created_at)
return `${months[created.getMonth()]} ${created.getDate()}`
}
markAsRead = () => {
const { notification, markAsRead } = this.props
markAsRead(notification.id)
}
markAsUnread = () => {
const { notification, markAsUnread } = this.props
markAsUnread(notification.id)
}
render = () => {
const { notification } = this.props
const classes = `notification ${notification.is_read ? 'read' : 'unread'}`
if (!notification.data.object) {
return null
}
return <li className={classes}>
<a href={`/notifications/${notification.id}`}>
<div className='notification-actor'>
<img src={notification.actor.image} />
</div>
<div className='notification-body'
dangerouslySetInnerHTML={this.notificationTextHtml()} />
</a>
<div className='notification-read-unread'>
{!notification.is_read && <div onClick={this.markAsRead}>
mark read
</div>}
{notification.is_read && <div onClick={this.markAsUnread}>
mark unread
</div>}
</div>
<div className='notification-date'>
{this.getDate()}
</div>
<div className='clearfloat'></div>
</li>
}
}
export default Notification

View file

@ -0,0 +1,74 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import onClickOutsideAddon from 'react-onclickoutside'
import Notification from './Notification'
import Loading from './Loading'
class NotificationBox extends Component {
static propTypes = {
notifications: PropTypes.array,
fetchNotifications: PropTypes.func.isRequired,
toggleNotificationsBox: PropTypes.func.isRequired,
markAsRead: PropTypes.func.isRequired,
markAsUnread: PropTypes.func.isRequired
}
componentDidMount = () => {
const { notifications, fetchNotifications } = this.props
if (!notifications) {
fetchNotifications()
}
}
handleClickOutside = () => {
this.props.toggleNotificationsBox()
}
hasSomeNotifications = () => {
const { notifications } = this.props
return notifications && notifications.length > 0
}
showLoading = () => {
return <li><Loading margin='30px auto' /></li>
}
showEmpty = () => {
return <li className='notificationsEmpty'>
You have no notifications. <br />
More time for dancing.
</li>
}
showNotifications = () => {
const { notifications, markAsRead, markAsUnread } = this.props
if (!this.hasSomeNotifications()) {
return this.showEmpty()
}
return notifications.slice(0, 10).map(
n => <Notification notification={n}
markAsRead={markAsRead}
markAsUnread={markAsUnread}
key={`notification-${n.id}`} />
).concat([
<li key='notification-see-all'>
<a href='/notifications' className='notificationsBoxSeeAll'>
See all
</a>
</li>
])
}
render = () => {
const { notifications } = this.props
return <div className='notificationsBox'>
<div className='notificationsBoxTriangle' />
<ul className='notifications'>
{notifications ? this.showNotifications() : this.showLoading()}
</ul>
</div>
}
}
export default onClickOutsideAddon(NotificationBox)

View file

@ -4,18 +4,14 @@ import PropTypes from 'prop-types'
class NotificationIcon extends Component {
static propTypes = {
unreadNotificationsCount: PropTypes.number
}
constructor(props) {
super(props)
this.state = {
}
unreadNotificationsCount: PropTypes.number,
toggleNotificationsBox: PropTypes.func
}
render = () => {
const { toggleNotificationsBox } = this.props
let linkClasses = 'notificationsIcon upperRightEl upperRightIcon '
linkClasses += 'ignore-react-onclickoutside '
if (this.props.unreadNotificationsCount > 0) {
linkClasses += 'unread'
@ -24,14 +20,14 @@ class NotificationIcon extends Component {
}
return (
<a className={linkClasses} href="/notifications" target="_blank">
<div className={linkClasses} onClick={toggleNotificationsBox}>
<div className="tooltipsUnder">
Notifications
</div>
{this.props.unreadNotificationsCount === 0 ? null : (
<div className="unread-notifications-dot"></div>
)}
</a>
</div>
)
}

View file

@ -38,6 +38,7 @@ class Desc extends Component {
change={this.props.onChange}
className="riek_desc"
classEditing="riek-editing"
rows="6"
editProps={{
onKeyPress: e => {
const ENTER = 13

View file

@ -1,7 +1,7 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import FilterBox from '../common/FilterBox'
import FilterBox from './FilterBox'
export default class UpperOptions extends Component {
static propTypes = {

View file

@ -4,31 +4,54 @@ import PropTypes from 'prop-types'
import AccountMenu from './AccountMenu'
import LoginForm from './LoginForm'
import NotificationIcon from './NotificationIcon'
import NotificationBox from './NotificationBox'
class UpperRightUI extends Component {
static propTypes = {
currentUser: PropTypes.object,
signInPage: PropTypes.bool,
unreadNotificationsCount: PropTypes.number,
fetchNotifications: PropTypes.func,
notifications: PropTypes.array,
markAsRead: PropTypes.func.isRequired,
markAsUnread: PropTypes.func.isRequired,
openInviteLightbox: PropTypes.func
}
constructor(props) {
super(props)
this.state = {accountBoxOpen: false}
this.state = {
accountBoxOpen: false,
notificationsBoxOpen: false
}
}
reset = () => {
this.setState({accountBoxOpen: false})
this.setState({
accountBoxOpen: false,
notificationsBoxOpen: false
})
}
toggleAccountBox = () => {
this.setState({accountBoxOpen: !this.state.accountBoxOpen})
this.setState({
accountBoxOpen: !this.state.accountBoxOpen,
notificationsBoxOpen: false
})
}
toggleNotificationsBox = () => {
this.setState({
notificationsBoxOpen: !this.state.notificationsBoxOpen,
accountBoxOpen: false
})
}
render () {
const { currentUser, signInPage, unreadNotificationsCount, openInviteLightbox } = this.props
const { accountBoxOpen } = this.state
const { currentUser, signInPage, unreadNotificationsCount,
notifications, fetchNotifications, openInviteLightbox,
markAsRead, markAsUnread } = this.props
const { accountBoxOpen, notificationsBoxOpen } = this.state
return <div className="upperRightUI">
{currentUser && <a href="/maps/new" target="_blank" className="addMap upperRightEl upperRightIcon">
<div className="tooltipsUnder">
@ -36,7 +59,15 @@ class UpperRightUI extends Component {
</div>
</a>}
{currentUser && <span id="notification_icon">
<NotificationIcon unreadNotificationsCount={unreadNotificationsCount} />
<NotificationIcon
unreadNotificationsCount={unreadNotificationsCount}
toggleNotificationsBox={this.toggleNotificationsBox}/>
{notificationsBoxOpen && <NotificationBox
notifications={notifications}
fetchNotifications={fetchNotifications}
markAsRead={markAsRead}
markAsUnread={markAsUnread}
toggleNotificationsBox={this.toggleNotificationsBox}/>}
</span>}
{!signInPage && <div className="sidebarAccount upperRightEl">
<div className="sidebarAccountIcon ignore-react-onclickoutside" onClick={this.toggleAccountBox}>

View file

@ -0,0 +1,18 @@
import React, { Component } from 'react'
import NavBar from '../components/NavBar'
import NavBarLink from '../components/NavBarLink'
class Admin extends Component {
render = () => {
return (
<NavBar>
<NavBarLink show hardReload href="/metacode_sets" text="Metacode Sets" />
<NavBarLink show hardReload href="/metacode_sets/new" text="New Set" />
<NavBarLink show hardReload href="/metacodes" text="Metacodes" />
<NavBarLink show hardReload href="/metacodes/new" text="New Metacode" />
</NavBar>
)
}
}
export default Admin

View file

@ -1,16 +1,20 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import MobileHeader from './MobileHeader'
import UpperLeftUI from './UpperLeftUI'
import UpperRightUI from './UpperRightUI'
import Toast from './Toast'
import MobileHeader from '../components/MobileHeader'
import UpperLeftUI from '../components/UpperLeftUI'
import UpperRightUI from '../components/UpperRightUI'
import Toast from '../components/Toast'
class App extends Component {
static propTypes = {
children: PropTypes.object,
toast: PropTypes.string,
unreadNotificationsCount: PropTypes.number,
notifications: PropTypes.array,
fetchNotifications: PropTypes.func,
markAsRead: PropTypes.func,
markAsUnread: PropTypes.func,
location: PropTypes.object,
mobile: PropTypes.bool,
mobileTitle: PropTypes.string,
@ -34,16 +38,38 @@ class App extends Component {
return {location}
}
constructor (props) {
super(props)
this.state = {
yieldHTML: null
}
}
componentDidMount () {
this.setYield()
}
setYield () {
const yieldHTML = document.getElementById('yield')
if (yieldHTML) {
this.setState({yieldHTML: yieldHTML.innerHTML})
document.body.removeChild(yieldHTML)
}
}
render () {
const { children, toast, unreadNotificationsCount, openInviteLightbox,
mobile, mobileTitle, mobileTitleWidth, mobileTitleClick, location,
map, userRequested, requestAnswered, requestApproved, serverData,
onRequestAccess } = this.props
onRequestAccess, notifications, fetchNotifications,
markAsRead, markAsUnread } = this.props
const { yieldHTML } = this.state
const { pathname } = location || {}
// this fixes a bug that happens otherwise when you logout
const currentUser = this.props.currentUser && this.props.currentUser.id ? this.props.currentUser : null
const unauthedHome = pathname === '/' && !currentUser
return <div className="wrapper" id="wrapper">
{yieldHTML && <div id="yield" dangerouslySetInnerHTML={{__html: yieldHTML}}></div>}
{mobile && <MobileHeader currentUser={currentUser}
unreadNotificationsCount={unreadNotificationsCount}
mobileTitle={mobileTitle}
@ -58,6 +84,10 @@ class App extends Component {
onRequestClick={onRequestAccess} />}
{!mobile && <UpperRightUI currentUser={currentUser}
unreadNotificationsCount={unreadNotificationsCount}
notifications={notifications}
fetchNotifications={fetchNotifications}
markAsRead={markAsRead}
markAsUnread={markAsUnread}
openInviteLightbox={openInviteLightbox}
signInPage={pathname === '/login'} />}
<Toast message={toast} />

View file

@ -0,0 +1,24 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import NavBar from '../components/NavBar'
import NavBarLink from '../components/NavBarLink'
class Apps extends Component {
render = () => {
const { currentUser } = this.props
return (
<NavBar>
{currentUser && currentUser.get('admin') && <NavBarLink show hardReload
matchChildRoutes href="/oauth/applications" linkClass="activeMaps"
text="Registered Apps" />}
<NavBarLink show hardReload matchChildRoutes
href="/oauth/authorized_applications"
linkClass="authedApps" text="Authorized Apps" />
<NavBarLink show href="/" linkClass="myMaps" text="Maps" />
</NavBar>
)
}
}
export default Apps

View file

@ -110,7 +110,7 @@ class MapChat extends Component {
</div>
{chatOpen && <div className="chat-panel">
<div className="junto-header">
PARTICIPANTS
Participants
<div onClick={this.toggleVideosShowing} className={`video-toggle ${videosShowing ? '' : 'active'}`} />
<div onClick={this.toggleCursorsShowing} className={`cursor-toggle ${cursorsShowing ? '' : 'active'}`} />
</div>
@ -134,7 +134,7 @@ class MapChat extends Component {
)}
</div>
<div className="chat-header">
CHAT
Chat
<div onClick={this.toggleAlertSound} className={`sound-toggle ${alertSound ? '' : 'active'}`}></div>
</div>
<div className="chat-messages" ref={div => { this.messagesDiv = div }}>

View file

@ -1,17 +1,19 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import DataVis from '../common/DataVis'
import UpperOptions from '../common/UpperOptions'
import InfoAndHelp from '../common/InfoAndHelp'
import ContextMenu from '../../components/ContextMenu'
import DataVis from '../../components/DataVis'
import UpperOptions from '../../components/UpperOptions'
import InfoAndHelp from '../../components/InfoAndHelp'
import Instructions from './Instructions'
import VisualizationControls from '../common/VisualizationControls'
import VisualizationControls from '../../components/VisualizationControls'
import MapChat from './MapChat'
import TopicCard from '../TopicCard'
import TopicCard from '../../components/TopicCard'
export default class MapView extends Component {
static propTypes = {
contextMenu: PropTypes.bool,
mobile: PropTypes.bool,
mapId: PropTypes.string,
map: PropTypes.object,
@ -79,7 +81,8 @@ export default class MapView extends Component {
filterAllMappers, filterAllSynapses, filterData,
openImportLightbox, forkMap, openHelpLightbox,
mapIsStarred, onMapStar, onMapUnstar, openTopic,
onZoomExtents, onZoomIn, onZoomOut, hasLearnedTopicCreation } = this.props
onZoomExtents, onZoomIn, onZoomOut, hasLearnedTopicCreation,
contextMenu } = this.props
const { chatOpen } = this.state
const onChatOpen = () => {
this.setState({chatOpen: true})
@ -109,6 +112,7 @@ export default class MapView extends Component {
filterAllSynapses={filterAllSynapses} />
<DataVis />
{openTopic && <TopicCard {...this.props} />}
{contextMenu && <ContextMenu {...this.props} />}
{currentUser && <Instructions mobile={mobile} hasLearnedTopicCreation={hasLearnedTopicCreation} />}
{currentUser && <MapChat {...this.props} onOpen={onChatOpen} onClose={onChatClose} chatOpen={chatOpen} ref={x => this.mapChat = x} />}
<VisualizationControls map={map}

View file

@ -0,0 +1,58 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import NavBar from '../../components/NavBar'
import NavBarLink from '../../components/NavBarLink'
class Header extends Component {
render = () => {
const { signedIn, section, user } = this.props
const explore = section === 'mine' || section === 'active' || section === 'starred' || section === 'shared' || section === 'featured'
const mapper = section === 'mapper'
return (
<NavBar>
<NavBarLink show={explore}
href={signedIn ? '/' : '/explore/active'}
linkClass="activeMaps"
text="All Maps"
/>
<NavBarLink show={signedIn && explore}
href="/explore/mine"
linkClass="myMaps"
text="My Maps"
/>
<NavBarLink show={signedIn && explore}
href="/explore/shared"
linkClass="sharedMaps"
text="Shared With Me"
/>
<NavBarLink show={signedIn && explore}
href="/explore/starred"
linkClass="starredMaps"
text="Starred By Me"
/>
<NavBarLink show={!signedIn && explore}
href="/explore/featured"
linkClass="featuredMaps"
text="Featured Maps"
/>
{mapper ? (
<div className='navBarButton active mapperButton'>
{user && <img className='exploreMapperImage' width='24' height='24' src={user.image} />}
{user && <div className='exploreMapperName'>{user.name}&rsquo;s Maps</div>}
<div className='clearfloat'></div>
</div>
) : null }
</NavBar>
)
}
}
Header.propTypes = {
signedIn: PropTypes.bool.isRequired,
section: PropTypes.string.isRequired,
user: PropTypes.object
}
export default Header

View file

@ -6,7 +6,6 @@ import MapperCard from './MapperCard'
import MapCard from './MapCard'
class Maps extends Component {
static propTypes = {
section: PropTypes.string,
maps: PropTypes.object,

View file

@ -0,0 +1,17 @@
import React, { Component } from 'react'
import NavBar from '../components/NavBar'
import NavBarLink from '../components/NavBarLink'
class Notifications extends Component {
render = () => {
return (
<NavBar>
<NavBarLink show matchChildRoutes href="/notifications"
linkClass="notificationsLink" text="Notifications" />
<NavBarLink show href="/" linkClass="myMaps" text="Maps" />
</NavBar>
)
}
}
export default Notifications

View file

@ -1,15 +1,17 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import DataVis from '../common/DataVis'
import UpperOptions from '../common/UpperOptions'
import InfoAndHelp from '../common/InfoAndHelp'
import VisualizationControls from '../common/VisualizationControls'
import TopicCard from '../TopicCard'
import ContextMenu from '../components/ContextMenu'
import DataVis from '../components/DataVis'
import UpperOptions from '../components/UpperOptions'
import InfoAndHelp from '../components/InfoAndHelp'
import VisualizationControls from '../components/VisualizationControls'
import TopicCard from '../components/TopicCard'
export default class TopicView extends Component {
static propTypes = {
contextMenu: PropTypes.bool,
mobile: PropTypes.bool,
topicId: PropTypes.string,
topic: PropTypes.object,
@ -55,7 +57,7 @@ export default class TopicView extends Component {
const { mobile, topic, currentUser, allForFiltering, visibleForFiltering,
toggleMetacode, toggleMapper, toggleSynapse, filterAllMetacodes,
filterAllMappers, filterAllSynapses, filterData, forkMap,
openHelpLightbox, onZoomIn, onZoomOut } = this.props
openHelpLightbox, onZoomIn, onZoomOut, contextMenu } = this.props
// TODO: stop using {...this.props} and make explicit
return <div className="topicWrapper">
<UpperOptions ref={x => this.upperOptions = x}
@ -73,6 +75,7 @@ export default class TopicView extends Component {
filterAllSynapses={filterAllSynapses} />
<DataVis />
<TopicCard {...this.props} />
{contextMenu && <ContextMenu {...this.props} />}
<VisualizationControls onClickZoomIn={onZoomIn}
onClickZoomOut={onZoomOut} />
<InfoAndHelp topic={topic}

View file

@ -1,8 +1,11 @@
import React from 'react'
import { Route, IndexRoute } from 'react-router'
import Admin from './Admin'
import App from './App'
import Apps from './Apps'
import Maps from './Maps'
import MapView from './MapView'
import Notifications from './Notifications'
import TopicView from './TopicView'
function nullComponent(props) {
@ -31,8 +34,8 @@ export default function makeRoutes (currentUser) {
<Route path="join" component={nullComponent} />
<Route path="request" component={nullComponent} />
<Route path="notifications">
<IndexRoute component={nullComponent} />
<Route path=":id" component={nullComponent} />
<IndexRoute component={Notifications} />
<Route path=":id" component={Notifications} />
</Route>
<Route path="users">
<Route path=":id/edit" component={nullComponent} />
@ -40,30 +43,30 @@ export default function makeRoutes (currentUser) {
<Route path="password/edit" component={nullComponent} />
</Route>
<Route path="metacodes">
<IndexRoute component={nullComponent} />
<Route path="new" component={nullComponent} />
<Route path=":id/edit" component={nullComponent} />
<IndexRoute component={Admin} />
<Route path="new" component={Admin} />
<Route path=":id/edit" component={Admin} />
</Route>
<Route path="metacode_sets">
<IndexRoute component={nullComponent} />
<Route path="new" component={nullComponent} />
<Route path=":id/edit" component={nullComponent} />
<IndexRoute component={Admin} />
<Route path="new" component={Admin} />
<Route path=":id/edit" component={Admin} />
</Route>
<Route path="oauth">
<Route path="token/info" component={nullComponent} />
<Route path="token/info" component={Apps} />
<Route path="authorize">
<IndexRoute component={nullComponent} />
<Route path=":code" component={nullComponent} />
</Route>
<Route path="authorized_applications">
<IndexRoute component={nullComponent} />
<Route path=":id" component={nullComponent} />
<IndexRoute component={Apps} />
<Route path=":id" component={Apps} />
</Route>
<Route path="applications">
<IndexRoute component={nullComponent} />
<Route path="new" component={nullComponent} />
<Route path=":id" component={nullComponent} />
<Route path=":id/edit" component={nullComponent} />
<IndexRoute component={Apps} />
<Route path="new" component={Apps} />
<Route path=":id" component={Apps} />
<Route path=":id/edit" component={Apps} />
</Route>
</Route>
</Route>

View file

@ -4,8 +4,8 @@ import { expect } from 'chai'
import { shallow } from 'enzyme'
import sinon from 'sinon'
import InfoAndHelp from '../../../src/components/common/InfoAndHelp.js'
import MapInfoBox from '../../../src/components/MapView/MapInfoBox.js'
import InfoAndHelp from '../../src/components/InfoAndHelp.js'
import MapInfoBox from '../../src/routes/MapView/MapInfoBox.js'
function assertTooltip({ wrapper, description, cssClass, tooltipText, callback }) {
it(description, function() {

View file

@ -1,6 +1,6 @@
/* global describe, it */
import React from 'react'
import ImportDialogBox from '../../../src/components/MapView/ImportDialogBox.js'
import ImportDialogBox from '../../../src/routes/MapView/ImportDialogBox.js'
import Dropzone from 'react-dropzone'
import { expect } from 'chai'
import { shallow } from 'enzyme'

View file

@ -3,8 +3,13 @@ namespace :assets do
task :js_compile do
system 'npm install'
system 'npm run build'
end
task :production_ready do
system 'bin/build-apidocs.sh' if Rails.env.production?
Rake::Task['perms:fix'].invoke if Rails.env.production?
end
end
Rake::Task[:'assets:precompile'].enhance([:'assets:js_compile'])
Rake::Task[:'assets:precompile'].enhance([:'assets:production_ready'])

View file

@ -21,6 +21,7 @@
"homepage": "https://github.com/metamaps/metamaps#readme",
"dependencies": {
"ajaxq": "0.0.7",
"async": "2.5.0",
"attachmediastream": "2.0.0",
"autolinker": "1.4.3",
"babel-cli": "6.26.0",
@ -72,6 +73,6 @@
"sinon": "2.2.0"
},
"optionalDependencies": {
"raml2html": "6.4.1"
"raml2html": "4.0.5"
}
}

View file

@ -0,0 +1,17 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe 'map_activity_mailer/daily_summary.html.erb' do
it 'displays messages sent' do
assign(:user, create(:user))
assign(:map, create(:map))
assign(:summary_data, stats: {
messages_sent: 5
})
render
expect(rendered).to match(/5 messages/)
end
end