diff --git a/Gemfile b/Gemfile
index be366c49..831c5ffb 100644
--- a/Gemfile
+++ b/Gemfile
@@ -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'
diff --git a/Gemfile.lock b/Gemfile.lock
index fe922d7b..c1b98be7 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -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
diff --git a/app/assets/stylesheets/admin.scss.erb b/app/assets/stylesheets/admin.scss.erb
index 8859457c..16b4ead5 100644
--- a/app/assets/stylesheets/admin.scss.erb
+++ b/app/assets/stylesheets/admin.scss.erb
@@ -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 {
diff --git a/app/assets/stylesheets/application.scss.erb b/app/assets/stylesheets/application.scss.erb
index d255b652..753124d2 100644
--- a/app/assets/stylesheets/application.scss.erb
+++ b/app/assets/stylesheets/application.scss.erb
@@ -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 {
diff --git a/app/assets/stylesheets/base.scss.erb b/app/assets/stylesheets/base.scss.erb
index 6415d535..b4b7bb0c 100644
--- a/app/assets/stylesheets/base.scss.erb
+++ b/app/assets/stylesheets/base.scss.erb
@@ -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;
diff --git a/app/assets/stylesheets/clean.css.erb b/app/assets/stylesheets/clean.css.erb
index 3bc37d68..445a4950 100644
--- a/app/assets/stylesheets/clean.css.erb
+++ b/app/assets/stylesheets/clean.css.erb
@@ -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;
}
diff --git a/app/assets/stylesheets/mobile.scss.erb b/app/assets/stylesheets/mobile.scss.erb
index 704569c8..02aefeb4 100644
--- a/app/assets/stylesheets/mobile.scss.erb
+++ b/app/assets/stylesheets/mobile.scss.erb
@@ -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;
}
diff --git a/app/assets/stylesheets/notifications.scss.erb b/app/assets/stylesheets/notifications.scss.erb
index 2f750f90..4c97547b 100644
--- a/app/assets/stylesheets/notifications.scss.erb
+++ b/app/assets/stylesheets/notifications.scss.erb
@@ -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;
+ }
+ }
+}
diff --git a/app/controllers/notifications_controller.rb b/app/controllers/notifications_controller.rb
index 210c0b43..22a34452 100644
--- a/app/controllers/notifications_controller.rb
+++ b/app/controllers/notifications_controller.rb
@@ -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
diff --git a/app/decorators/notification_decorator.rb b/app/decorators/notification_decorator.rb
new file mode 100644
index 00000000..cc503821
--- /dev/null
+++ b/app/decorators/notification_decorator.rb
@@ -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
diff --git a/app/models/user_preference.rb b/app/models/user_preference.rb
index c881c4ac..d92eb7a9 100644
--- a/app/models/user_preference.rb
+++ b/app/models/user_preference.rb
@@ -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
diff --git a/app/views/admin/_adminpanel.html.erb b/app/views/admin/_adminpanel.html.erb
deleted file mode 100644
index c34c6cc6..00000000
--- a/app/views/admin/_adminpanel.html.erb
+++ /dev/null
@@ -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' }%>
-
-
diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb
index 3ebbbad6..340874fc 100644
--- a/app/views/layouts/application.html.erb
+++ b/app/views/layouts/application.html.erb
@@ -6,10 +6,10 @@
#%>
<%= render :partial => 'layouts/head' %>
- controller-<%= controller_name %> action-<%= action_name %>">
+ controller-<%= controller_name %> action-<%= action_name %>">
<%= 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' %>
diff --git a/app/views/layouts/doorkeeper.html.erb b/app/views/layouts/doorkeeper.html.erb
deleted file mode 100644
index b9ff5bef..00000000
--- a/app/views/layouts/doorkeeper.html.erb
+++ /dev/null
@@ -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' %>
-
-
- <%= yield %>
-
-<%= render :partial => 'layouts/foot' %>
diff --git a/app/views/map_activity_mailer/daily_summary.html.erb b/app/views/map_activity_mailer/daily_summary.html.erb
index 99fd53f7..d28d00a5 100644
--- a/app/views/map_activity_mailer/daily_summary.html.erb
+++ b/app/views/map_activity_mailer/daily_summary.html.erb
@@ -15,22 +15,22 @@
<%= link_to @map.name, map_url(@map) %>
- <% if @summary_data[:stats][:messages_sent] > 0 %>
+ <% if @summary_data[:stats][:messages_sent] %>
<%= pluralize(@summary_data[:stats][:messages_sent], 'message') %>
<% end %>
- <% if @summary_data[:stats][:topics_added] > 0 %>
+ <% if @summary_data[:stats][:topics_added] %>
<%= pluralize(@summary_data[:stats][:topics_added], 'topic') %> added
<% end %>
- <% if @summary_data[:stats][:synapses_added] > 0 %>
+ <% if @summary_data[:stats][:synapses_added] %>
<%= pluralize(@summary_data[:stats][:synapses_added], 'synapse') %> added
<% end %>
- <% if @summary_data[:stats][:topics_moved] > 0 %>
+ <% if @summary_data[:stats][:topics_moved] %>
<%= pluralize(@summary_data[:stats][:topics_moved], 'topic') %> moved
<% end %>
- <% if @summary_data[:stats][:topics_removed] > 0 %>
+ <% if @summary_data[:stats][:topics_removed] %>
<%= pluralize(@summary_data[:stats][:topics_removed], 'topic') %> removed
<% end %>
- <% if @summary_data[:stats][:synapses_removed] > 0 %>
+ <% if @summary_data[:stats][:synapses_removed] %>
<%= pluralize(@summary_data[:stats][:synapses_removed], 'synapse') %> removed
<% end %>
@@ -61,7 +61,7 @@
<% end %>
-
+
<% if @summary_data[:topics_removed] || @summary_data[:synapses_removed] %>
<% if @summary_data[:topics_removed] %>
diff --git a/app/views/metacode_sets/edit.html.erb b/app/views/metacode_sets/edit.html.erb
index 5377cba8..06f2cb06 100644
--- a/app/views/metacode_sets/edit.html.erb
+++ b/app/views/metacode_sets/edit.html.erb
@@ -1,6 +1,6 @@
-
- <%= render 'form' %>
+
+ <%= render 'form' %>
diff --git a/app/views/metacode_sets/index.html.erb b/app/views/metacode_sets/index.html.erb
index e5015afa..68986895 100644
--- a/app/views/metacode_sets/index.html.erb
+++ b/app/views/metacode_sets/index.html.erb
@@ -1,37 +1,36 @@
-
-
- <%= render :partial => 'admin/adminpanel' %>
-
-
-
- Name
- Description
- Metacodes
-
+
+
+
+
+
+ Name
+ Description
+ Metacodes
+
- <% @metacode_sets.each do |metacode_set| %>
-
-
- <%= metacode_set.name %>
- <%= link_to 'Edit',
- edit_metacode_set_path(metacode_set) %>
-
- <%= link_to 'Delete',
- metacode_set, method: :delete,
- data: { confirm: 'Are you sure?' } %>
-
- <%= metacode_set.desc %>
-
- <% metacode_set.metacodes.each_with_index do |metacode, index| %>
-
- <% if (index+1)%4 == 0 %>
-
+ <% @metacode_sets.each do |metacode_set| %>
+
+
+ <%= metacode_set.name %>
+ <%= link_to 'Edit',
+ edit_metacode_set_path(metacode_set) %>
+
+ <%= link_to 'Delete',
+ metacode_set, method: :delete,
+ data: { confirm: 'Are you sure?' } %>
+
+ <%= metacode_set.desc %>
+
+ <% metacode_set.metacodes.each_with_index do |metacode, index| %>
+
+ <% if (index+1)%4 == 0 %>
+
+ <% end %>
<% end %>
- <% end %>
-
-
-
- <% end %>
-
+
+
+
+ <% end %>
+
diff --git a/app/views/metacode_sets/new.html.erb b/app/views/metacode_sets/new.html.erb
index 1ff5e852..06f2cb06 100644
--- a/app/views/metacode_sets/new.html.erb
+++ b/app/views/metacode_sets/new.html.erb
@@ -1,6 +1,6 @@
-
- <%= render 'form' %>
+
+ <%= render 'form' %>
@@ -11,4 +11,4 @@
<% end %>
Metamaps.Admin.allMetacodes.push("<%= m.id %>");
<% end %>
-
\ No newline at end of file
+
diff --git a/app/views/metacodes/edit.html.erb b/app/views/metacodes/edit.html.erb
index de8c85c1..4eb40656 100644
--- a/app/views/metacodes/edit.html.erb
+++ b/app/views/metacodes/edit.html.erb
@@ -1,5 +1,5 @@
-
\ No newline at end of file
+
diff --git a/app/views/metacodes/index.html.erb b/app/views/metacodes/index.html.erb
index afe20706..f7f86e53 100644
--- a/app/views/metacodes/index.html.erb
+++ b/app/views/metacodes/index.html.erb
@@ -1,31 +1,30 @@
-
- <%= render :partial => 'admin/adminpanel' %>
-
-
-
- Name
- Icon
- Color
-
-
-
+
+
+
+
+ Name
+ Icon
+ Color
+
+
+
- <% @metacodes.each do |metacode| %>
-
- <%= metacode.name %>
- <%= metacode.icon %>
- <% if metacode.color %>
-
- <%= metacode.color %>
-
- <% else %>
-
- <% end %>
- <%= image_tag metacode.icon, width: 40 %>
- <%= link_to 'Edit', edit_metacode_path(metacode) %>
-
- <% end %>
-
+ <% @metacodes.each do |metacode| %>
+
+ <%= metacode.name %>
+ <%= metacode.icon %>
+ <% if metacode.color %>
+
+ <%= metacode.color %>
+
+ <% else %>
+
+ <% end %>
+ <%= image_tag metacode.icon, width: 40 %>
+ <%= link_to 'Edit', edit_metacode_path(metacode) %>
+
+ <% end %>
+
diff --git a/app/views/metacodes/new.html.erb b/app/views/metacodes/new.html.erb
index e10f28d1..81819734 100644
--- a/app/views/metacodes/new.html.erb
+++ b/app/views/metacodes/new.html.erb
@@ -1,5 +1,5 @@
-
diff --git a/app/views/notifications/_header.html.erb b/app/views/notifications/_header.html.erb
deleted file mode 100644
index 2507b2ef..00000000
--- a/app/views/notifications/_header.html.erb
+++ /dev/null
@@ -1,14 +0,0 @@
-
diff --git a/app/views/notifications/index.html.erb b/app/views/notifications/index.html.erb
index ec9987f6..4b0a69d2 100644
--- a/app/views/notifications/index.html.erb
+++ b/app/views/notifications/index.html.erb
@@ -8,7 +8,7 @@
<% 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) %>
@@ -76,5 +76,3 @@
<% end %>
-
-<%= render partial: 'notifications/header' %>
diff --git a/app/views/notifications/mark_read.js.erb b/app/views/notifications/mark_read.js.erb
index cbf2cf13..17e418b8 100644
--- a/app/views/notifications/mark_read.js.erb
+++ b/app/views/notifications/mark_read.js.erb
@@ -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)
\ No newline at end of file
+Metamaps.GlobalUI.Notifications.decrementUnread(Metamaps.GlobalUI.ReactApp.render)
diff --git a/app/views/notifications/mark_unread.js.erb b/app/views/notifications/mark_unread.js.erb
index 3fffab24..873e7af8 100644
--- a/app/views/notifications/mark_unread.js.erb
+++ b/app/views/notifications/mark_unread.js.erb
@@ -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)
\ No newline at end of file
+Metamaps.GlobalUI.Notifications.incrementUnread(Metamaps.GlobalUI.ReactApp.render)
diff --git a/app/views/notifications/show.html.erb b/app/views/notifications/show.html.erb
index a552aa77..e2cb2bd2 100644
--- a/app/views/notifications/show.html.erb
+++ b/app/views/notifications/show.html.erb
@@ -9,7 +9,7 @@
<% 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' %> <%= request.user.name %> wants to collaborate on map <%= map.name %>
<% 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 %>
-
-<%= render partial: 'notifications/header' %>
diff --git a/app/views/users/edit.html.erb b/app/views/users/edit.html.erb
index 4ea6130f..1ee406e7 100644
--- a/app/views/users/edit.html.erb
+++ b/app/views/users/edit.html.erb
@@ -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' %>
diff --git a/config/application.rb b/config/application.rb
index ff5a621c..06842eb6 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -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
diff --git a/config/initializers/delayed_job.rb b/config/initializers/delayed_job.rb
index d3132394..c4725b19 100644
--- a/config/initializers/delayed_job.rb
+++ b/config/initializers/delayed_job.rb
@@ -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'))
diff --git a/doc/production/first-deploy.md b/doc/production/first-deploy.md
index 44b6b659..b588299d 100644
--- a/doc/production/first-deploy.md
+++ b/doc/production/first-deploy.md
@@ -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
diff --git a/frontend/src/Metamaps/GlobalUI/ImportDialog.js b/frontend/src/Metamaps/GlobalUI/ImportDialog.js
index 63c47491..c253512d 100644
--- a/frontend/src/Metamaps/GlobalUI/ImportDialog.js
+++ b/frontend/src/Metamaps/GlobalUI/ImportDialog.js
@@ -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'
diff --git a/frontend/src/Metamaps/GlobalUI/Notifications.js b/frontend/src/Metamaps/GlobalUI/Notifications.js
new file mode 100644
index 00000000..29908695
--- /dev/null
+++ b/frontend/src/Metamaps/GlobalUI/Notifications.js
@@ -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
diff --git a/frontend/src/Metamaps/GlobalUI/ReactApp.js b/frontend/src/Metamaps/GlobalUI/ReactApp.js
index db850091..6237f08a 100644
--- a/frontend/src/Metamaps/GlobalUI/ReactApp.js
+++ b/frontend/src/Metamaps/GlobalUI/ReactApp.js
@@ -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 {
diff --git a/frontend/src/Metamaps/GlobalUI/index.js b/frontend/src/Metamaps/GlobalUI/index.js
index b2dd6654..7a7d2ae5 100644
--- a/frontend/src/Metamaps/GlobalUI/index.js
+++ b/frontend/src/Metamaps/GlobalUI/index.js
@@ -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
diff --git a/frontend/src/Metamaps/JIT.js b/frontend/src/Metamaps/JIT.js
index ffaab923..b0aee7ea 100644
--- a/frontend/src/Metamaps/JIT.js
+++ b/frontend/src/Metamaps/JIT.js
@@ -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 = ''
-
- const authorized = Active.Map && Active.Map.authorizeToEdit(Active.Mapper)
-
- const disabled = authorized ? '' : 'disabled'
-
- if (Active.Map) menustring += '
Hide until refreshCtrl+H
'
- if (Active.Map && Active.Mapper) menustring += '
Remove from mapCtrl+M
'
- if (Active.Topic) menustring += '
Remove from viewCtrl+M
'
- if (Active.Map && Active.Mapper) menustring += '
DeleteCtrl+D
'
-
- if (Active.Topic) {
- menustring += '
Center this topicAlt+E
'
- }
-
- menustring += '
Open in new tab '
-
- if (Active.Mapper) {
- const options = outdent`
-
-
commons
-
public
-
private
- `
-
- menustring += ' '
-
- menustring += outdent`
-
-
- Change permissions
- ${options}
-
- `
-
- menustring += '
Change metacode
'
- }
- if (Active.Topic) {
- if (!Active.Mapper) {
- menustring += ' '
- }
-
- // 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`
- `
- menustring += '
Reveal siblings' + siblingMenu + '
'
- }
-
- menustring += ' '
- 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(`${string} `)
- }
-
- $('.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 = ''
-
- const authorized = Active.Map && Active.Map.authorizeToEdit(Active.Mapper)
-
- const disabled = authorized ? '' : 'disabled'
-
- if (Active.Map) menustring += '
Hide until refreshCtrl+H
'
- if (Active.Map && Active.Mapper) menustring += '
Remove from mapCtrl+M
'
- if (Active.Topic) menustring += '
Remove from viewCtrl+M
'
- if (Active.Map && Active.Mapper) menustring += '
DeleteCtrl+D
'
-
- if (Active.Map && Active.Mapper) menustring += ' '
-
- if (Active.Mapper) {
- const permOptions = outdent`
-
-
commons
-
public
private `
-
- menustring += '
Change permissions' + permOptions + '
'
- }
-
- menustring += ' '
- 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
diff --git a/frontend/src/Metamaps/Listeners.js b/frontend/src/Metamaps/Listeners.js
index 352b5f7f..a5dc630f 100644
--- a/frontend/src/Metamaps/Listeners.js
+++ b/frontend/src/Metamaps/Listeners.js
@@ -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)
}
}
}
diff --git a/frontend/src/Metamaps/Map/index.js b/frontend/src/Metamaps/Map/index.js
index 2db12263..f337d1d0 100644
--- a/frontend/src/Metamaps/Map/index.js
+++ b/frontend/src/Metamaps/Map/index.js
@@ -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)
diff --git a/frontend/src/Metamaps/Topic.js b/frontend/src/Metamaps/Topic.js
index 0054ad55..1b1abe88 100644
--- a/frontend/src/Metamaps/Topic.js
+++ b/frontend/src/Metamaps/Topic.js
@@ -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)
}
}
diff --git a/frontend/src/Metamaps/Views/ContextMenu.js b/frontend/src/Metamaps/Views/ContextMenu.js
new file mode 100644
index 00000000..f1553706
--- /dev/null
+++ b/frontend/src/Metamaps/Views/ContextMenu.js
@@ -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
diff --git a/frontend/src/Metamaps/Views/index.js b/frontend/src/Metamaps/Views/index.js
index 0f7cf566..de9d5aab 100644
--- a/frontend/src/Metamaps/Views/index.js
+++ b/frontend/src/Metamaps/Views/index.js
@@ -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
diff --git a/frontend/src/Metamaps/index.js b/frontend/src/Metamaps/index.js
index 12fcbe60..747cb643 100644
--- a/frontend/src/Metamaps/index.js
+++ b/frontend/src/Metamaps/index.js
@@ -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
diff --git a/frontend/src/components/App/AccountMenu.js b/frontend/src/components/AccountMenu.js
similarity index 100%
rename from frontend/src/components/App/AccountMenu.js
rename to frontend/src/components/AccountMenu.js
diff --git a/frontend/src/components/ContextMenu.js b/frontend/src/components/ContextMenu.js
new file mode 100644
index 00000000..35ea0ee8
--- /dev/null
+++ b/frontend/src/components/ContextMenu.js
@@ -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
+
+ Hide until refresh
+ Ctrl+H
+
+ }
+
+ remove = () => {
+ const { contextRemove, map, currentUser } = this.props
+ const canEditMap = map && map.authorizeToEdit(currentUser)
+ if (!canEditMap) {
+ return null
+ }
+ return
+
+ Remove from map
+ Ctrl+M
+
+ }
+
+ delete = () => {
+ const { contextDelete, map, currentUser } = this.props
+ const canEditMap = map && map.authorizeToEdit(currentUser)
+ if (!canEditMap) {
+ return null
+ }
+ return
+
+ Delete
+ Ctrl+D
+
+ }
+
+ center = () => {
+ const { contextCenterOn, contextNode, topicId } = this.props
+ if (!(contextNode && topicId)) {
+ return null
+ }
+ return contextCenterOn(contextNode.id)}>
+
+ Center this topic
+ Alt+E
+
+ }
+
+ popout = () => {
+ const { contextPopoutTopic, contextNode } = this.props
+ if (!contextNode) {
+ return null
+ }
+ return contextPopoutTopic(contextNode.id)}>
+
+ Open in new tab
+
+ }
+
+ permission = () => {
+ const { currentUser, contextUpdatePermissions } = this.props
+ if (!currentUser) {
+ return null
+ }
+ return
+
+ Change permissions
+
+ contextUpdatePermissions('commons')}>
+
+ commons
+
+ contextUpdatePermissions('public')}>
+
+ public
+
+ contextUpdatePermissions('private')}>
+
+ private
+
+
+
+
+ }
+
+ metacode = () => {
+ const { metacodeSets, contextOnMetacodeSelect,
+ currentUser, contextNode } = this.props
+ if (!currentUser) {
+ return null
+ }
+ return
+
+ Change metacode
+
+ {
+ contextOnMetacodeSelect(contextNode && contextNode.id, id)
+ }}
+ metacodeSets={metacodeSets} />
+
+
+
+ }
+
+ 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
+
+ Reveal siblings
+
+ contextFetchSiblings(contextNode)}>
+ All
+ Alt+R
+
+ {contextSiblingsData && Object.keys(contextSiblingsData).map(key => {
+ return contextFetchSiblings(contextNode, key)}>
+ {contextSiblingsData[key]}
+
+ })}
+ {contextFetchingSiblingsData && loading... }
+
+
+
+ }
+
+ 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
+
+ {this.hide()}
+ {this.remove()}
+ {this.delete()}
+ {this.center()}
+ {this.popout()}
+ {showSpacer && }
+ {this.permission()}
+ {this.metacode()}
+ {this.siblings()}
+
+
+ }
+}
+
+export default ContextMenu
diff --git a/frontend/src/components/common/DataVis.js b/frontend/src/components/DataVis.js
similarity index 100%
rename from frontend/src/components/common/DataVis.js
rename to frontend/src/components/DataVis.js
diff --git a/frontend/src/components/common/FilterBox.js b/frontend/src/components/FilterBox.js
similarity index 100%
rename from frontend/src/components/common/FilterBox.js
rename to frontend/src/components/FilterBox.js
diff --git a/frontend/src/components/common/InfoAndHelp.js b/frontend/src/components/InfoAndHelp.js
similarity index 96%
rename from frontend/src/components/common/InfoAndHelp.js
rename to frontend/src/components/InfoAndHelp.js
index 8a4edf25..e6bc01d8 100644
--- a/frontend/src/components/common/InfoAndHelp.js
+++ b/frontend/src/components/InfoAndHelp.js
@@ -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 = {
diff --git a/frontend/src/components/Loading.js b/frontend/src/components/Loading.js
new file mode 100644
index 00000000..66382978
--- /dev/null
+++ b/frontend/src/components/Loading.js
@@ -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
+
+
+
+
+
+ }
+}
+
+export default Loading
diff --git a/frontend/src/components/App/LoginForm.js b/frontend/src/components/LoginForm.js
similarity index 100%
rename from frontend/src/components/App/LoginForm.js
rename to frontend/src/components/LoginForm.js
diff --git a/frontend/src/components/Maps/Header.js b/frontend/src/components/Maps/Header.js
deleted file mode 100644
index 2168e0c0..00000000
--- a/frontend/src/components/Maps/Header.js
+++ /dev/null
@@ -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 (
-
-
- {text}
-
- )
-}
-
-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 (
-
- )
- }
-}
-
-Header.propTypes = {
- signedIn: PropTypes.bool.isRequired,
- section: PropTypes.string.isRequired,
- user: PropTypes.object
-}
-
-export default Header
diff --git a/frontend/src/components/App/MobileHeader.js b/frontend/src/components/MobileHeader.js
similarity index 99%
rename from frontend/src/components/App/MobileHeader.js
rename to frontend/src/components/MobileHeader.js
index a9acce37..dd27c2a7 100644
--- a/frontend/src/components/App/MobileHeader.js
+++ b/frontend/src/components/MobileHeader.js
@@ -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 = {
diff --git a/frontend/src/components/NavBar.js b/frontend/src/components/NavBar.js
new file mode 100644
index 00000000..92ec9591
--- /dev/null
+++ b/frontend/src/components/NavBar.js
@@ -0,0 +1,19 @@
+import React, { Component } from 'react'
+
+class NavBar extends Component {
+ render() {
+ return (
+
+
+
+
+ {this.props.children}
+
+
+
+
+ )
+ }
+}
+
+export default NavBar
diff --git a/frontend/src/components/NavBarLink.js b/frontend/src/components/NavBarLink.js
new file mode 100644
index 00000000..67feec88
--- /dev/null
+++ b/frontend/src/components/NavBarLink.js
@@ -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 (
+
+ {linkClass &&
}
+ {text}
+
+ )
+ }
+ return (
+
+ {linkClass &&
}
+ {text}
+
+ )
+ }
+}
+
+export default NavBarLink
diff --git a/frontend/src/components/Notification.js b/frontend/src/components/Notification.js
new file mode 100644
index 00000000..22237aaa
--- /dev/null
+++ b/frontend/src/components/Notification.js
@@ -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 = `${notification.actor.name}
`
+
+ switch (notification.type) {
+ case 'ACCESS_APPROVED':
+ map = notification.data.map
+ result += outdent`granted your request to edit map
+ ${map.name} `
+ break
+ case 'ACCESS_REQUEST':
+ map = notification.data.map
+ result += outdent`wants permission to map with you on
+ ${map.name} `
+ if (!notification.data.object.answered) {
+ result += 'Offer a response
'
+ }
+ break
+ case 'INVITE_TO_EDIT':
+ map = notification.data.map
+ result += outdent`gave you edit access to map
+ ${map.name} `
+ break
+ case 'TOPIC_ADDED_TO_MAP':
+ map = notification.data.map
+ topic = notification.data.topic
+ result += outdent`added topic ${topic.name}
+ to map ${map.name} `
+ break
+ case 'TOPIC_CONNECTED_1':
+ topic1 = notification.data.topic1
+ topic2 = notification.data.topic2
+ result += outdent`connected ${topic1.name}
+ to ${topic2.name} `
+ break
+ case 'TOPIC_CONNECTED_2':
+ topic1 = notification.data.topic1
+ topic2 = notification.data.topic2
+ result += outdent`connected ${topic2.name}
+ to ${topic1.name} `
+ 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
+
+
+
+
+
+
+
+ {!notification.is_read &&
+ mark read
+
}
+ {notification.is_read &&
+ mark unread
+
}
+
+
+ {this.getDate()}
+
+
+
+ }
+}
+
+export default Notification
diff --git a/frontend/src/components/NotificationBox.js b/frontend/src/components/NotificationBox.js
new file mode 100644
index 00000000..bfd5f409
--- /dev/null
+++ b/frontend/src/components/NotificationBox.js
@@ -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
+ }
+
+ showEmpty = () => {
+ return
+ You have no notifications.
+ More time for dancing.
+
+ }
+
+ showNotifications = () => {
+ const { notifications, markAsRead, markAsUnread } = this.props
+ if (!this.hasSomeNotifications()) {
+ return this.showEmpty()
+ }
+ return notifications.slice(0, 10).map(
+ n =>
+ ).concat([
+
+
+ See all
+
+
+ ])
+ }
+
+ render = () => {
+ const { notifications } = this.props
+ return
+
+
+ {notifications ? this.showNotifications() : this.showLoading()}
+
+
+ }
+}
+
+export default onClickOutsideAddon(NotificationBox)
diff --git a/frontend/src/components/App/NotificationIcon.js b/frontend/src/components/NotificationIcon.js
similarity index 70%
rename from frontend/src/components/App/NotificationIcon.js
rename to frontend/src/components/NotificationIcon.js
index 36b86b72..b265cec0 100644
--- a/frontend/src/components/App/NotificationIcon.js
+++ b/frontend/src/components/NotificationIcon.js
@@ -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 (
-
+
Notifications
{this.props.unreadNotificationsCount === 0 ? null : (
)}
-
+
)
}
diff --git a/frontend/src/components/common/Sprite.js b/frontend/src/components/Sprite.js
similarity index 100%
rename from frontend/src/components/common/Sprite.js
rename to frontend/src/components/Sprite.js
diff --git a/frontend/src/components/App/Toast.js b/frontend/src/components/Toast.js
similarity index 100%
rename from frontend/src/components/App/Toast.js
rename to frontend/src/components/Toast.js
diff --git a/frontend/src/components/TopicCard/Desc.js b/frontend/src/components/TopicCard/Desc.js
index 9529cbfe..fa3383e8 100644
--- a/frontend/src/components/TopicCard/Desc.js
+++ b/frontend/src/components/TopicCard/Desc.js
@@ -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
diff --git a/frontend/src/components/App/UpperLeftUI.js b/frontend/src/components/UpperLeftUI.js
similarity index 100%
rename from frontend/src/components/App/UpperLeftUI.js
rename to frontend/src/components/UpperLeftUI.js
diff --git a/frontend/src/components/common/UpperOptions.js b/frontend/src/components/UpperOptions.js
similarity index 98%
rename from frontend/src/components/common/UpperOptions.js
rename to frontend/src/components/UpperOptions.js
index 5108eab3..26cde8bf 100644
--- a/frontend/src/components/common/UpperOptions.js
+++ b/frontend/src/components/UpperOptions.js
@@ -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 = {
diff --git a/frontend/src/components/App/UpperRightUI.js b/frontend/src/components/UpperRightUI.js
similarity index 55%
rename from frontend/src/components/App/UpperRightUI.js
rename to frontend/src/components/UpperRightUI.js
index d1e53652..08276c82 100644
--- a/frontend/src/components/App/UpperRightUI.js
+++ b/frontend/src/components/UpperRightUI.js
@@ -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
{currentUser &&
@@ -36,7 +59,15 @@ class UpperRightUI extends Component {
}
{currentUser &&
-
+
+ {notificationsBoxOpen && }
}
{!signInPage &&
diff --git a/frontend/src/components/common/VisualizationControls.js b/frontend/src/components/VisualizationControls.js
similarity index 100%
rename from frontend/src/components/common/VisualizationControls.js
rename to frontend/src/components/VisualizationControls.js
diff --git a/frontend/src/routes/Admin.js b/frontend/src/routes/Admin.js
new file mode 100644
index 00000000..d48664ef
--- /dev/null
+++ b/frontend/src/routes/Admin.js
@@ -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 (
+
+
+
+
+
+
+ )
+ }
+}
+
+export default Admin
diff --git a/frontend/src/components/App/index.js b/frontend/src/routes/App.js
similarity index 69%
rename from frontend/src/components/App/index.js
rename to frontend/src/routes/App.js
index ed24e9d1..79ad3335 100644
--- a/frontend/src/components/App/index.js
+++ b/frontend/src/routes/App.js
@@ -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
+ {yieldHTML &&
}
{mobile &&
}
{!mobile &&
}
diff --git a/frontend/src/routes/Apps.js b/frontend/src/routes/Apps.js
new file mode 100644
index 00000000..108bcc43
--- /dev/null
+++ b/frontend/src/routes/Apps.js
@@ -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 (
+
+ {currentUser && currentUser.get('admin') && }
+
+
+
+ )
+ }
+}
+
+export default Apps
diff --git a/frontend/src/components/MapView/ImportDialogBox.js b/frontend/src/routes/MapView/ImportDialogBox.js
similarity index 100%
rename from frontend/src/components/MapView/ImportDialogBox.js
rename to frontend/src/routes/MapView/ImportDialogBox.js
diff --git a/frontend/src/components/MapView/Instructions.js b/frontend/src/routes/MapView/Instructions.js
similarity index 100%
rename from frontend/src/components/MapView/Instructions.js
rename to frontend/src/routes/MapView/Instructions.js
diff --git a/frontend/src/components/MapView/MapChat/Message.js b/frontend/src/routes/MapView/MapChat/Message.js
similarity index 100%
rename from frontend/src/components/MapView/MapChat/Message.js
rename to frontend/src/routes/MapView/MapChat/Message.js
diff --git a/frontend/src/components/MapView/MapChat/NewMessage.js b/frontend/src/routes/MapView/MapChat/NewMessage.js
similarity index 100%
rename from frontend/src/components/MapView/MapChat/NewMessage.js
rename to frontend/src/routes/MapView/MapChat/NewMessage.js
diff --git a/frontend/src/components/MapView/MapChat/Participant.js b/frontend/src/routes/MapView/MapChat/Participant.js
similarity index 100%
rename from frontend/src/components/MapView/MapChat/Participant.js
rename to frontend/src/routes/MapView/MapChat/Participant.js
diff --git a/frontend/src/components/MapView/MapChat/Unread.js b/frontend/src/routes/MapView/MapChat/Unread.js
similarity index 100%
rename from frontend/src/components/MapView/MapChat/Unread.js
rename to frontend/src/routes/MapView/MapChat/Unread.js
diff --git a/frontend/src/components/MapView/MapChat/index.js b/frontend/src/routes/MapView/MapChat/index.js
similarity index 99%
rename from frontend/src/components/MapView/MapChat/index.js
rename to frontend/src/routes/MapView/MapChat/index.js
index 4f702125..1334d479 100644
--- a/frontend/src/components/MapView/MapChat/index.js
+++ b/frontend/src/routes/MapView/MapChat/index.js
@@ -110,7 +110,7 @@ class MapChat extends Component {
{chatOpen &&
- PARTICIPANTS
+ Participants
@@ -134,7 +134,7 @@ class MapChat extends Component {
)}
{ this.messagesDiv = div }}>
diff --git a/frontend/src/components/MapView/MapInfoBox.js b/frontend/src/routes/MapView/MapInfoBox.js
similarity index 100%
rename from frontend/src/components/MapView/MapInfoBox.js
rename to frontend/src/routes/MapView/MapInfoBox.js
diff --git a/frontend/src/components/MapView/index.js b/frontend/src/routes/MapView/index.js
similarity index 90%
rename from frontend/src/components/MapView/index.js
rename to frontend/src/routes/MapView/index.js
index fe8c23b1..027b395d 100644
--- a/frontend/src/components/MapView/index.js
+++ b/frontend/src/routes/MapView/index.js
@@ -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} />
{openTopic &&
}
+ {contextMenu &&
}
{currentUser &&
}
{currentUser &&
this.mapChat = x} />}
{
+ const { signedIn, section, user } = this.props
+
+ const explore = section === 'mine' || section === 'active' || section === 'starred' || section === 'shared' || section === 'featured'
+ const mapper = section === 'mapper'
+
+ return (
+
+
+
+
+
+
+ {mapper ? (
+
+ {user &&
}
+ {user &&
{user.name}’s Maps
}
+
+
+ ) : null }
+
+ )
+ }
+}
+
+Header.propTypes = {
+ signedIn: PropTypes.bool.isRequired,
+ section: PropTypes.string.isRequired,
+ user: PropTypes.object
+}
+
+export default Header
diff --git a/frontend/src/components/Maps/MapCard.js b/frontend/src/routes/Maps/MapCard.js
similarity index 100%
rename from frontend/src/components/Maps/MapCard.js
rename to frontend/src/routes/Maps/MapCard.js
diff --git a/frontend/src/components/Maps/MapperCard.js b/frontend/src/routes/Maps/MapperCard.js
similarity index 100%
rename from frontend/src/components/Maps/MapperCard.js
rename to frontend/src/routes/Maps/MapperCard.js
diff --git a/frontend/src/components/Maps/index.js b/frontend/src/routes/Maps/index.js
similarity index 99%
rename from frontend/src/components/Maps/index.js
rename to frontend/src/routes/Maps/index.js
index 081515fe..c6835a5f 100644
--- a/frontend/src/components/Maps/index.js
+++ b/frontend/src/routes/Maps/index.js
@@ -6,7 +6,6 @@ import MapperCard from './MapperCard'
import MapCard from './MapCard'
class Maps extends Component {
-
static propTypes = {
section: PropTypes.string,
maps: PropTypes.object,
diff --git a/frontend/src/routes/Notifications.js b/frontend/src/routes/Notifications.js
new file mode 100644
index 00000000..f15bffe1
--- /dev/null
+++ b/frontend/src/routes/Notifications.js
@@ -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 (
+
+
+
+
+ )
+ }
+}
+
+export default Notifications
diff --git a/frontend/src/components/TopicView/index.js b/frontend/src/routes/TopicView.js
similarity index 84%
rename from frontend/src/components/TopicView/index.js
rename to frontend/src/routes/TopicView.js
index 5ca9a050..cc8e54ca 100644
--- a/frontend/src/components/TopicView/index.js
+++ b/frontend/src/routes/TopicView.js
@@ -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
this.upperOptions = x}
@@ -73,6 +75,7 @@ export default class TopicView extends Component {
filterAllSynapses={filterAllSynapses} />
+ {contextMenu && }
-
-
+
+
@@ -40,30 +43,30 @@ export default function makeRoutes (currentUser) {
-
-
-
+
+
+
-
-
-
+
+
+
-
+
-
-
+
+
-
-
-
-
+
+
+
+
diff --git a/frontend/test/components/common/InfoAndHelp.spec.js b/frontend/test/components/InfoAndHelp.spec.js
similarity index 95%
rename from frontend/test/components/common/InfoAndHelp.spec.js
rename to frontend/test/components/InfoAndHelp.spec.js
index 9880af0f..d0c604d2 100644
--- a/frontend/test/components/common/InfoAndHelp.spec.js
+++ b/frontend/test/components/InfoAndHelp.spec.js
@@ -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() {
diff --git a/frontend/test/components/MapView/ImportDialogBox.spec.js b/frontend/test/routes/MapView/ImportDialogBox.spec.js
similarity index 95%
rename from frontend/test/components/MapView/ImportDialogBox.spec.js
rename to frontend/test/routes/MapView/ImportDialogBox.spec.js
index 7151230e..20ac5542 100644
--- a/frontend/test/components/MapView/ImportDialogBox.spec.js
+++ b/frontend/test/routes/MapView/ImportDialogBox.spec.js
@@ -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'
diff --git a/lib/tasks/extensions.rake b/lib/tasks/extensions.rake
index fc4a4855..96de0368 100644
--- a/lib/tasks/extensions.rake
+++ b/lib/tasks/extensions.rake
@@ -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'])
diff --git a/package.json b/package.json
index f0afc5f7..5cece282 100644
--- a/package.json
+++ b/package.json
@@ -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"
}
}
diff --git a/spec/views/map_activity_mailer/daily_summary.html.erb_spec.rb b/spec/views/map_activity_mailer/daily_summary.html.erb_spec.rb
new file mode 100644
index 00000000..f581a1d2
--- /dev/null
+++ b/spec/views/map_activity_mailer/daily_summary.html.erb_spec.rb
@@ -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