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 %> -
-
-
-
- <% if current_user && current_user.admin %> - -
Registered Apps -
- <% end %> - -
Authorized Apps -
- -
Maps -
-
-
-
-
-<%= 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' %> -
- - - - - - +
+
+
+
NameDescriptionMetacodes
+ + + + + - <% @metacode_sets.each do |metacode_set| %> - - - - + + + - - <% end %> -
NameDescriptionMetacodes
- <%= 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 %> +
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 @@
-
+
<%= render 'form' %>
-
\ 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' %> -
- - - - - - - - +
+
+
NameIconColor
+ + + + + + + - <% @metacodes.each do |metacode| %> - - - - <% if metacode.color %> - - <% else %> - - <% end %> - - - - <% end %> -
NameIconColor
<%= metacode.name %><%= metacode.icon %> - <%= metacode.color %> - <%= image_tag metacode.icon, width: 40 %><%= link_to 'Edit', edit_metacode_path(metacode) %>
+ <% @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 @@
-
+
<%= render 'form' %>
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 = '' - 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 = '' - 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 + +
    +
  • + } + + 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 + +
    +
  • + } + + 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
    + +
    + } +} + +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 ( -
    -
    -
    -
    - - - - - - - {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/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 ( + + ) + } +} + +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 { )}
    - CHAT + Chat
    { 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