Into master: two finger pan/zoom, map and topic follows (for internal testing) on the UI, map activity emails (#1084)
* fix topic spec * fix synapse/mapping spec * brakeman csrf warning suppressed :| * follows for maps in the ui for internal testing only still (#1072) * follows for maps in the ui for testers * require user for these actions * match how map follow works * include ability to unfollow from email * fixup templates * add unfollow_from_email to the policies * Update _cheatsheet.html.erb Clean up text, clarify, and bring in line with current functionality * topicsRegex and synapsesRegex should allow commas (#1073) * even better import csv regexes * prevent double prompt on file drop import * topic card in react (#1031) * its coming along * links bar * scssify a bunch * metacode image working a bit better * metacode selector in react topic card * riek editing for name field on topic card * riek submit on enter * factor out Title and Links from Topic Card component, but not the listeners * create working Desc editor * styling is much better now * textarea min height for desc * disallow images in topic card markdown * shift enter is linebreak, enter is save * attachments split out, but it's pretty buggy * move listeners into Links.js * slightly wider metacodeTitle * fix positioning on metacode selector * fix metacode selection * move metacode and permissions into subcomponents * fixes * prevent editing on desc/title if not authorized to edit * fix topic card draggability * fix embedly * fix md test * remove the removed link card manually with jquery * fix test syntax * eslint * more eslin * reuse authorizedToEdit * convert metacode sets to a json object for react * add the html in react whoop * fix metacode styling * sort wasn't working * finishing metacode select * readd the above link input border * fix syntax * multiline title editable textarea * more portable metacode selector component * factor out #metacodeOptions into one react component with a callback :D:D:D * render metacodeOptions in right click menu with react * render metacodeOptions in right click menu with react * fix up right click menu's metacode editing * fix topic card title character counter * ignore metamaps secret bundle in ag * simplify Attachments props * factor out embedly card into its own component; it seems to help * link resetter * fix edit icon on title in topic card * move mapCount and synapseCount hover/click logic to react * fix up the showMore control * metacode selection tweaks * tweak links bar spacing in topic card * rubocop * remove TODOs * more badass permissions selector * close permission selector when you click outside * fix overeager metacode selector * more modular attachments component * fix bug in Desc.js * fix right click styling * permission changes are different than edit rights * bad module ref * ensure maxLength on topic titles * hellz yeah (#1074) * fix drop from two touches to one * don't commit activity service * ability to select/unselect all metacodes in custom set with keyboard shortcut (fix #390) (#1078) * ability to select/unselect all metacodes in custom set with keyboard shortcut * select all button * nicer all/none buttons * set up react testing (#1080) * install mocha-webpack. also switch hark to npm version instead of github version * well, mocha-webpack runs * add jsdom for tests * upgrade to webpack 2 * fix npm run test errors * ImportDialogBox component tests * Fixes bug where pressing delete key while editing text will suggest... (#1083) * Fixes bug where pressing delete key while editing text will suggest the deletion of selected map entities * Changed the DEL key to remove entities instead of delete them * temporarily disable code climate duplication engine * add topic following for internal testing * daily map activity emails (#1081) * data prepared, task setup * add the basics of the email template * cover granular permissions * unfollow this map * break out permissions tests better * rename so test runs
This commit is contained in:
parent
50639e8a0a
commit
7ee96bf6c6
84 changed files with 2463 additions and 1168 deletions
1
.agignore
Normal file
1
.agignore
Normal file
|
@ -0,0 +1 @@
|
|||
app/assets/javascripts/metamaps.secret.bundle.js
|
|
@ -5,7 +5,7 @@ engines:
|
|||
bundler-audit:
|
||||
enabled: true
|
||||
duplication:
|
||||
enabled: true
|
||||
enabled: false
|
||||
config:
|
||||
languages:
|
||||
count_threshold: 3 # rule of three
|
||||
|
|
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -22,6 +22,7 @@ app/assets/javascripts/webpacked
|
|||
# Ignore all logfiles and tempfiles.
|
||||
log/*.log
|
||||
tmp
|
||||
.tmp
|
||||
|
||||
coverage
|
||||
|
||||
|
|
2
Gemfile
2
Gemfile
|
@ -51,4 +51,6 @@ group :development, :test do
|
|||
gem 'pry-rails'
|
||||
gem 'rubocop'
|
||||
gem 'tunemygc'
|
||||
gem 'faker'
|
||||
gem 'timecop'
|
||||
end
|
||||
|
|
|
@ -109,6 +109,8 @@ GEM
|
|||
factory_girl_rails (4.8.0)
|
||||
factory_girl (~> 4.8.0)
|
||||
railties (>= 3.0.0)
|
||||
faker (1.7.3)
|
||||
i18n (~> 0.5)
|
||||
globalid (0.3.7)
|
||||
activesupport (>= 4.1.0)
|
||||
httparty (0.14.0)
|
||||
|
@ -272,6 +274,7 @@ GEM
|
|||
thor (0.19.4)
|
||||
thread_safe (0.3.5)
|
||||
tilt (2.0.5)
|
||||
timecop (0.8.1)
|
||||
tunemygc (1.0.69)
|
||||
tzinfo (1.2.2)
|
||||
thread_safe (~> 0.1)
|
||||
|
@ -301,6 +304,7 @@ DEPENDENCIES
|
|||
dotenv-rails
|
||||
exception_notification
|
||||
factory_girl_rails
|
||||
faker
|
||||
httparty
|
||||
jquery-rails
|
||||
jquery-ui-rails
|
||||
|
@ -327,6 +331,7 @@ DEPENDENCIES
|
|||
slack-notifier
|
||||
snorlax
|
||||
sucker_punch
|
||||
timecop
|
||||
tunemygc
|
||||
uglifier
|
||||
|
||||
|
@ -334,4 +339,4 @@ RUBY VERSION
|
|||
ruby 2.3.0p0
|
||||
|
||||
BUNDLED WITH
|
||||
1.13.7
|
||||
1.14.6
|
||||
|
|
|
@ -1250,7 +1250,7 @@ h3.filterBox {
|
|||
box-shadow: 0px 3px 3px rgba(0,0,0,0.12), 0 3px 3px rgba(0,0,0,0.24);
|
||||
}
|
||||
.rightclickmenu .rc-permission:hover > ul,
|
||||
.rightclickmenu .rc-metacode:hover > ul,
|
||||
.rightclickmenu .rc-metacode:hover #metacodeOptions > ul,
|
||||
.rightclickmenu .rc-siblings:hover > ul {
|
||||
display: block;
|
||||
}
|
||||
|
@ -1279,7 +1279,7 @@ h3.filterBox {
|
|||
.rightclickmenu li.toPrivate .rc-perm-icon {
|
||||
background-position: -24px 0;
|
||||
}
|
||||
.rightclickmenu .rc-metacode > ul > li,
|
||||
.rightclickmenu .rc-metacode #metacodeOptions > ul > li,
|
||||
.rightclickmenu .rc-siblings > ul > li {
|
||||
padding: 6px 24px 6px 8px;
|
||||
white-space: nowrap;
|
||||
|
@ -2311,6 +2311,9 @@ and it won't be important on password protected instances */
|
|||
}
|
||||
/* switch metacode set */
|
||||
|
||||
#switchMetacodes > p {
|
||||
margin: 16px 0 16px 0;
|
||||
}
|
||||
#metacodeSwitchTabs {
|
||||
width: 100%;
|
||||
font-size: 17px;
|
||||
|
@ -2318,28 +2321,43 @@ and it won't be important on password protected instances */
|
|||
border: none;
|
||||
background: none;
|
||||
padding: 0;
|
||||
}
|
||||
#metacodeSwitchTabs .setDesc {
|
||||
margin-bottom: 5px;
|
||||
font-family: 'din-medium', helvetica, sans-serif;
|
||||
color: #424242;
|
||||
font-size: 14px;
|
||||
text-align: justify;
|
||||
padding-right: 16px;
|
||||
}
|
||||
#switchMetacodes > p {
|
||||
margin: 16px 0 16px 0;
|
||||
}
|
||||
#metacodeSwitchTabs > ul {
|
||||
width: 130px;
|
||||
}
|
||||
#metacodeSwitchTabs > ul li {
|
||||
font-size: 14px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
#metacodeSwitchTabs li.ui-state-active a {
|
||||
color: #00BCD4;
|
||||
cursor: pointer;
|
||||
|
||||
.setDesc,
|
||||
.selectAll,
|
||||
.selectNone {
|
||||
margin-bottom: 5px;
|
||||
font-family: 'din-medium', helvetica, sans-serif;
|
||||
color: #424242;
|
||||
font-size: 14px;
|
||||
text-align: justify;
|
||||
padding-right: 16px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.selectAll,
|
||||
.selectNone {
|
||||
float: right;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover,
|
||||
&.selected {
|
||||
color: #00bcd4;
|
||||
}
|
||||
}
|
||||
|
||||
& > ul {
|
||||
width: 130px;
|
||||
|
||||
li {
|
||||
font-size: 14px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
}
|
||||
|
||||
li.ui-state-active a {
|
||||
color: #00BCD4;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
.metacodeSwitchTab {
|
||||
max-height: 300px;
|
||||
|
@ -3121,3 +3139,13 @@ script.data-gratipay-username {
|
|||
.inline {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.topicFollow {
|
||||
text-align: center;
|
||||
height: 48px;
|
||||
line-height: 48px;
|
||||
border-top: 1px solid #BDBDBD;
|
||||
background: #FFF;
|
||||
cursor: pointer;
|
||||
font-family: din-regular;
|
||||
}
|
|
@ -6,6 +6,11 @@
|
|||
font-family: helvetica;
|
||||
color: #727272;
|
||||
line-height: 11px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.riek-editing + .nameCounter {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.nameCounter.forMap {
|
||||
|
@ -14,14 +19,14 @@
|
|||
}
|
||||
|
||||
.nameCounter.forTopic {
|
||||
|
||||
|
||||
}
|
||||
|
||||
#center-container {
|
||||
position:relative;
|
||||
height:100%;
|
||||
width:100%;
|
||||
|
||||
|
||||
/* background-color:#031924; */
|
||||
color:#444;
|
||||
}
|
||||
|
@ -85,6 +90,11 @@
|
|||
display: table-cell;
|
||||
vertical-align: middle;
|
||||
padding: 0 16px;
|
||||
|
||||
&.riek-editing {
|
||||
position: absolute;
|
||||
top: 32px;
|
||||
}
|
||||
}
|
||||
.canEdit #titleActivator:hover {
|
||||
background-image: url(<%= asset_data_uri('edit.png') %>);
|
||||
|
@ -93,12 +103,12 @@
|
|||
cursor: text;
|
||||
}
|
||||
|
||||
.showcard .best_in_place_name textarea, .showcard .best_in_place_name input {
|
||||
.showcard .title .riek-editing {
|
||||
font-family: 'din-regular', sans-serif;
|
||||
color: #424242;
|
||||
font-size: 18px;
|
||||
line-height: 22px;
|
||||
height: 15px;
|
||||
height: 3em;
|
||||
padding: 5px 0;
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
|
@ -122,7 +132,7 @@
|
|||
height: auto;
|
||||
}
|
||||
|
||||
.CardOnGraph .best_in_place_desc textarea {
|
||||
.CardOnGraph .desc .riek-editing {
|
||||
font-size: 13px;
|
||||
line-height:15px;
|
||||
font-family: helvetica, sans-serif;
|
||||
|
@ -167,13 +177,14 @@
|
|||
* End Markdown styling
|
||||
*/
|
||||
|
||||
.CardOnGraph .best_in_place_desc {
|
||||
.CardOnGraph .riek_desc {
|
||||
display:block;
|
||||
margin-top:2px;
|
||||
margin-top:2px;
|
||||
padding-right: 18px;
|
||||
margin-right: 8px;
|
||||
min-height: 7em;
|
||||
}
|
||||
.canEdit .CardOnGraph .best_in_place_desc:hover {
|
||||
.canEdit .CardOnGraph .riek_desc:hover {
|
||||
background-image: url(<%= asset_data_uri('edit.png') %>);
|
||||
background-position: top right;
|
||||
background-repeat: no-repeat;
|
||||
|
@ -185,155 +196,218 @@
|
|||
}
|
||||
|
||||
.CardOnGraph .links {
|
||||
position:relative;
|
||||
position: relative;
|
||||
border-bottom: 1px solid #BDBDBD;
|
||||
border-top: 1px solid #BDBDBD;
|
||||
background-color: #e0e0e0;
|
||||
}
|
||||
|
||||
.linkItem {
|
||||
float:left;
|
||||
height:46px;
|
||||
z-index: 1;
|
||||
position: relative;
|
||||
color: #424242;
|
||||
font-size: 14px;
|
||||
line-height:14px;
|
||||
height:12px;
|
||||
padding:17px 0;
|
||||
}
|
||||
.linkItem a {
|
||||
color: #424242;
|
||||
}
|
||||
.linkItem {
|
||||
float: left;
|
||||
z-index: 1;
|
||||
position: relative;
|
||||
color: #424242;
|
||||
font-size: 14px;
|
||||
line-height: 14px;
|
||||
|
||||
.CardOnGraph .icon {
|
||||
position:absolute;
|
||||
width:100%;
|
||||
z-index:1;
|
||||
padding: 0;
|
||||
height: 48px;
|
||||
}
|
||||
.linkItem.contributor {
|
||||
margin-left:40px;
|
||||
z-index:1;
|
||||
padding:17px 16px 17px 30px;
|
||||
position: relative;
|
||||
}
|
||||
.contributor .contributorIcon {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
left: 0;
|
||||
border-radius: 16px;
|
||||
}
|
||||
a {
|
||||
color: #424242;
|
||||
}
|
||||
}
|
||||
|
||||
.contributor:hover .contributorName {
|
||||
display: block;
|
||||
}
|
||||
.icon {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
padding: 0;
|
||||
height: 48px;
|
||||
width: 100%;
|
||||
|
||||
.contributorName {
|
||||
display: none;
|
||||
position: absolute;
|
||||
background: black;
|
||||
text-align: center;
|
||||
color: white;
|
||||
border-radius: 2px;
|
||||
font-family: din-regular;
|
||||
line-height: 15px;
|
||||
font-size: 12px;
|
||||
padding: 3px 5px 2px;
|
||||
white-space: nowrap;
|
||||
margin-top: 36px;
|
||||
margin-left: -32px;
|
||||
}
|
||||
.metacodeImage {
|
||||
cursor: move;
|
||||
position: relative;
|
||||
left: -23px;
|
||||
top: 1px;
|
||||
width: 46px;
|
||||
height: 46px;
|
||||
background-size:46px 46px;
|
||||
background-position:0 0;
|
||||
background-repeat:no-repeat;
|
||||
}
|
||||
}
|
||||
|
||||
.contributor {
|
||||
bottom: 7px;
|
||||
margin-left: 40px;
|
||||
|
||||
.contributor div:before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 128%;
|
||||
left: 13px;
|
||||
margin-top: -30px;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-bottom: 4px solid #000000;
|
||||
border-left: 5px solid transparent;
|
||||
border-right: 5px solid transparent;
|
||||
}
|
||||
.contributorIcon {
|
||||
position: relative;
|
||||
vertical-align: middle;
|
||||
border-radius: 16px;
|
||||
margin: 5px;
|
||||
top: 8px;
|
||||
left: 0;
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
.linkItem.mapCount {
|
||||
margin-left: 12px;
|
||||
width: 24px;
|
||||
padding:17px 0 17px 36px;
|
||||
}
|
||||
.linkItem.mapCount .mapCountIcon {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
left: 0;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
background-image: url(<%= asset_data_uri('map32_sprite.png') %>);
|
||||
background-repeat: no-repeat;
|
||||
background-position: 0 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
.linkItem.mapCount:hover .mapCountIcon {
|
||||
background-position: 0 -32px;
|
||||
}
|
||||
span {
|
||||
font-family: 'din-regular', sans-serif;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.linkItem.mapCount:hover .hoverTip {
|
||||
display: block;
|
||||
}
|
||||
.CardOnGraph .mapCount .tip, .CardonGraph .mapCount .hoverTip {
|
||||
top: 44px;
|
||||
left: 0px;
|
||||
font-size: 12px !important;
|
||||
}
|
||||
.contributorName {
|
||||
display: none;
|
||||
position: absolute;
|
||||
background: black;
|
||||
text-align: center;
|
||||
color: white;
|
||||
border-radius: 2px;
|
||||
font-family: din-regular;
|
||||
line-height: 15px;
|
||||
font-size: 12px;
|
||||
padding: 3px 5px 2px;
|
||||
white-space: nowrap;
|
||||
margin-top: 8px;
|
||||
|
||||
.hoverTip {
|
||||
white-space: nowrap;
|
||||
font-family: 'din-regular';
|
||||
top: 44px;
|
||||
left: 0px;
|
||||
font-size: 12px !important;
|
||||
display: none;
|
||||
position: absolute;
|
||||
background: black;
|
||||
color: white;
|
||||
border-radius: 4px;
|
||||
line-height: 17px;
|
||||
padding: 3px 5px 2px;
|
||||
z-index: 100;
|
||||
}
|
||||
&:before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 26px;
|
||||
left: 10px;
|
||||
margin-top: -30px;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-bottom: 4px solid #000000;
|
||||
border-left: 5px solid transparent;
|
||||
border-right: 5px solid transparent;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover .contributorName {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.CardOnGraph .mapCount .tip:before, .CardOnGraph .mapCount .hoverTip:before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 26px;
|
||||
left: 10px;
|
||||
margin-top: -30px;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-bottom: 4px solid #000000;
|
||||
border-left: 5px solid transparent;
|
||||
border-right: 5px solid transparent;
|
||||
}
|
||||
.mapCount {
|
||||
padding:17px 0 17px 36px;
|
||||
margin-left: 12px;
|
||||
|
||||
.CardOnGraph .mapCount .tip li {
|
||||
list-style-type: none;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
padding: 6px 10px;
|
||||
display: block;
|
||||
height: 14px;
|
||||
font-family: 'din-regular', helvetica, sans-serif;
|
||||
font-size: 14px;
|
||||
line-height: 14px;
|
||||
position: relative;
|
||||
}
|
||||
.mapCountIcon {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
left: 0;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
background-image: url(<%= asset_data_uri('map32_sprite.png') %>);
|
||||
background-repeat: no-repeat;
|
||||
background-position: 0 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.CardOnGraph .mapCount li.hideExtra {
|
||||
display: none;
|
||||
&:hover .mapCountIcon {
|
||||
background-position: 0 -32px;
|
||||
}
|
||||
|
||||
.tip, .hoverTip {
|
||||
top: 44px;
|
||||
left: 0px;
|
||||
font-size: 12px !important;
|
||||
|
||||
&:before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 26px;
|
||||
left: 10px;
|
||||
margin-top: -30px;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-bottom: 4px solid #000000;
|
||||
border-left: 5px solid transparent;
|
||||
border-right: 5px solid transparent;
|
||||
}
|
||||
}
|
||||
|
||||
.hoverTip {
|
||||
white-space: nowrap;
|
||||
font-family: 'din-regular';
|
||||
top: 44px;
|
||||
left: 0px;
|
||||
font-size: 12px !important;
|
||||
position: absolute;
|
||||
background: black;
|
||||
color: white;
|
||||
border-radius: 4px;
|
||||
line-height: 17px;
|
||||
padding: 3px 5px 2px;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.tip a {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.tip a:hover {
|
||||
color: #757575;
|
||||
}
|
||||
|
||||
.tip li {
|
||||
list-style-type: none;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
padding: 6px 10px;
|
||||
display: block;
|
||||
height: 14px;
|
||||
font-family: 'din-regular', helvetica, sans-serif;
|
||||
font-size: 14px;
|
||||
line-height: 14px;
|
||||
position: relative;
|
||||
}
|
||||
}
|
||||
|
||||
.synapseCount {
|
||||
margin-left: 26px;
|
||||
width: 24px;
|
||||
padding:17px 0 17px 32px;
|
||||
|
||||
.synapseCountIcon {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
left: 0;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
background-image: url(<%= asset_data_uri('synapse32_sprite.png') %>);
|
||||
background-repeat: no-repeat;
|
||||
background-position: 0 0;
|
||||
}
|
||||
hover .synapseCountIcon {
|
||||
background-position: 0 -32px;
|
||||
}
|
||||
.tip {
|
||||
position: absolute;
|
||||
background: black;
|
||||
width: auto;
|
||||
top: 44px;
|
||||
color: white;
|
||||
white-space: nowrap;
|
||||
border-radius: 2px;
|
||||
font-size: 12px !important;
|
||||
font-family: 'din-regular';
|
||||
line-height: 12px;
|
||||
padding: 4px 4px 4px;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.tip:before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
margin-top: -8px;
|
||||
margin-left: 6px;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-bottom: 4px solid black;
|
||||
border-left: 5px solid transparent;
|
||||
border-right: 5px solid transparent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.showMore {
|
||||
|
@ -341,66 +415,6 @@
|
|||
color: #4FC059;
|
||||
}
|
||||
|
||||
.mapCount .tip a {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.mapCount .tip a:hover {
|
||||
color: #757575;
|
||||
}
|
||||
|
||||
|
||||
.linkItem.synapseCount {
|
||||
margin-left: 2px;
|
||||
width: 24px;
|
||||
padding:17px 0 17px 32px;
|
||||
}
|
||||
.linkItem.synapseCount .synapseCountIcon {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
left: 0;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
background-image: url(<%= asset_data_uri('synapse32_sprite.png') %>);
|
||||
background-repeat: no-repeat;
|
||||
background-position: 0 0;
|
||||
}
|
||||
.linkItem.synapseCount:hover .synapseCountIcon {
|
||||
background-position: 0 -32px;
|
||||
}
|
||||
|
||||
.CardOnGraph .synapseCount .tip {
|
||||
position: absolute;
|
||||
background: black;
|
||||
width: auto;
|
||||
top: 44px;
|
||||
color: white;
|
||||
white-space: nowrap;
|
||||
border-radius: 2px;
|
||||
font-size: 12px !important;
|
||||
font-family: 'din-regular';
|
||||
line-height: 12px;
|
||||
padding: 4px 4px 4px;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.CardOnGraph .synapseCount:hover .tip {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.CardOnGraph .synapseCount .tip:before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
margin-top: -8px;
|
||||
margin-left: 6px;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-bottom: 4px solid black;
|
||||
border-left: 5px solid transparent;
|
||||
border-right: 5px solid transparent;
|
||||
}
|
||||
|
||||
|
||||
.mapPerm {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
|
@ -470,7 +484,7 @@ cursor: pointer;
|
|||
text-transform: uppercase;
|
||||
position: absolute;
|
||||
line-height: 24px;
|
||||
height:24px;
|
||||
height: 26px;
|
||||
font-size: 24px;
|
||||
display: none;
|
||||
width: 90%;
|
||||
|
@ -493,35 +507,25 @@ cursor: pointer;
|
|||
background-position: 0 -32px;
|
||||
}
|
||||
.permission.canEdit .minimize .expandMetacodeSelect {
|
||||
|
||||
|
||||
}
|
||||
|
||||
.CardOnGraph .metacodeImage {
|
||||
cursor:move;
|
||||
width:46px;
|
||||
height:46px;
|
||||
position:absolute;
|
||||
left:-23px;
|
||||
top:0;
|
||||
background-size:46px 46px;
|
||||
background-position:0 0;
|
||||
background-repeat:no-repeat;
|
||||
.CardOnGraph .metacodeName {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
#metacodeOptions {
|
||||
display:none;
|
||||
}
|
||||
.CardOnGraph .metacodeSelect {
|
||||
display:none;
|
||||
width:auto;
|
||||
z-index: 2;
|
||||
position: absolute;
|
||||
background: #EAEAEA;
|
||||
left: 300px;
|
||||
white-space: nowrap;
|
||||
position: absolute;
|
||||
left: 300px;
|
||||
top: -1px;
|
||||
}
|
||||
.CardOnGraph .metacodeSelect ul {
|
||||
position: relative;
|
||||
position: relative;
|
||||
line-height: 14px;
|
||||
font-size: 14px;
|
||||
font-family: helvetica, sans-serif;
|
||||
|
@ -610,7 +614,6 @@ background-color: #E0E0E0;
|
|||
display:block;
|
||||
}
|
||||
.CardOnGraph .tip {
|
||||
display:none;
|
||||
position: absolute;
|
||||
background: black;
|
||||
top: 35px;
|
||||
|
@ -623,26 +626,24 @@ background-color: #E0E0E0;
|
|||
z-index:100;
|
||||
}
|
||||
|
||||
#embedlyLink {
|
||||
display: none;
|
||||
}
|
||||
#embedlyLinkLoader {
|
||||
margin: 0 auto;
|
||||
width: 28px;
|
||||
}
|
||||
|
||||
.CardOnGraph .attachments {
|
||||
border-top: 1px solid #BDBDBD;
|
||||
.CardOnGraph .link-adder {
|
||||
width:100%;
|
||||
height:47px;
|
||||
position: relative;
|
||||
border-top: 1px solid #BDBDBD;
|
||||
}
|
||||
|
||||
.attachments a {
|
||||
.link-adder a {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: block;
|
||||
margin-left: 40px;
|
||||
margin-left: 40px;
|
||||
padding-top:9px;
|
||||
font-size: 16px;
|
||||
line-height: 16px;
|
||||
|
@ -652,7 +653,7 @@ background-color: #E0E0E0;
|
|||
display: inline-block;
|
||||
width: 102px;
|
||||
height: 12px;
|
||||
text-align: left;
|
||||
text-align: left;
|
||||
padding: 18px 0 18px 48px;
|
||||
font-size: 12px;
|
||||
color: #9e9e9e;
|
||||
|
@ -752,7 +753,6 @@ font-family: 'din-regular', helvetica, sans-serif;
|
|||
-moz-border-radius-bottomright: 8px;
|
||||
-webkit-border-bottom-right-radius: 8px;
|
||||
border-bottom-right-radius: 8px;
|
||||
display: none;
|
||||
margin: 8px;
|
||||
}
|
||||
|
||||
|
@ -839,10 +839,10 @@ font-family: 'din-regular', helvetica, sans-serif;
|
|||
line-height: 16px;
|
||||
}
|
||||
|
||||
.canEdit #edit_synapse_desc:hover {
|
||||
.canEdit span.titleWrapper:hover {
|
||||
background-image: url(<%= asset_data_uri('edit.png') %>);
|
||||
background-repeat: no-repeat;
|
||||
background-position: 164px center;
|
||||
background-position: 95% 95%;
|
||||
cursor: text;
|
||||
}
|
||||
|
||||
|
@ -950,11 +950,11 @@ font-family: 'din-regular', helvetica, sans-serif;
|
|||
}
|
||||
#edit_synapse_right {
|
||||
background-image: url(<%= asset_data_uri('synapsedirectionright_sprite.png') %>);
|
||||
right: 16px;
|
||||
right: 16px;
|
||||
}
|
||||
#edit_synapse_left {
|
||||
background-image: url(<%= asset_data_uri('synapsedirectionleft_sprite.png') %>);
|
||||
right: 56px;
|
||||
right: 56px;
|
||||
}
|
||||
#edit_synapse_left.checked, #edit_synapse_right.checked {
|
||||
background-position: 0 -48px;
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
class MapsController < ApplicationController
|
||||
before_action :require_user, only: [:create, :update, :destroy, :events]
|
||||
before_action :set_map, only: [:show, :conversation, :update, :destroy, :contains, :events, :export]
|
||||
before_action :require_user, only: [:create, :update, :destroy, :events, :follow, :unfollow]
|
||||
before_action :set_map, only: [:show, :conversation, :update, :destroy, :contains, :events, :export, :follow, :unfollow, :unfollow_from_email]
|
||||
after_action :verify_authorized
|
||||
|
||||
# GET maps/:id
|
||||
|
@ -138,6 +138,43 @@ class MapsController < ApplicationController
|
|||
end
|
||||
end
|
||||
|
||||
# POST maps/:id/follow
|
||||
def follow
|
||||
follow = FollowService.follow(@map, current_user, 'followed')
|
||||
|
||||
respond_to do |format|
|
||||
format.json do
|
||||
if follow
|
||||
head :ok
|
||||
else
|
||||
head :bad_request
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# POST maps/:id/unfollow
|
||||
def unfollow
|
||||
FollowService.unfollow(@map, current_user)
|
||||
|
||||
respond_to do |format|
|
||||
format.json do
|
||||
head :ok
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# GET maps/:id/unfollow_from_email
|
||||
def unfollow_from_email
|
||||
FollowService.unfollow(@map, current_user)
|
||||
|
||||
respond_to do |format|
|
||||
format.html do
|
||||
redirect_to map_path(@map), notice: 'You are no longer following this map'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_map
|
||||
|
|
|
@ -2,7 +2,10 @@
|
|||
class TopicsController < ApplicationController
|
||||
include TopicsHelper
|
||||
|
||||
before_action :require_user, only: [:create, :update, :destroy]
|
||||
before_action :require_user, only: [:create, :update, :destroy, :follow, :unfollow]
|
||||
before_action :set_topic, only: [:show, :update, :relative_numbers,
|
||||
:relatives, :network, :destroy,
|
||||
:follow, :unfollow, :unfollow_from_email]
|
||||
after_action :verify_authorized, except: :autocomplete_topic
|
||||
|
||||
respond_to :html, :js, :json
|
||||
|
@ -31,9 +34,6 @@ class TopicsController < ApplicationController
|
|||
|
||||
# GET topics/:id
|
||||
def show
|
||||
@topic = Topic.find(params[:id])
|
||||
authorize @topic
|
||||
|
||||
respond_to do |format|
|
||||
format.html do
|
||||
@alltopics = [@topic].concat(policy_scope(Topic.relatives(@topic.id, current_user)).to_a)
|
||||
|
@ -49,9 +49,6 @@ class TopicsController < ApplicationController
|
|||
|
||||
# GET topics/:id/network
|
||||
def network
|
||||
@topic = Topic.find(params[:id])
|
||||
authorize @topic
|
||||
|
||||
@alltopics = [@topic].concat(policy_scope(Topic.relatives(@topic.id, current_user)).to_a)
|
||||
@allsynapses = policy_scope(Synapse.for_topic(@topic.id))
|
||||
|
||||
|
@ -71,9 +68,6 @@ class TopicsController < ApplicationController
|
|||
|
||||
# GET topics/:id/relative_numbers
|
||||
def relative_numbers
|
||||
@topic = Topic.find(params[:id])
|
||||
authorize @topic
|
||||
|
||||
topics_already_has = params[:network] ? params[:network].split(',').map(&:to_i) : []
|
||||
|
||||
alltopics = policy_scope(Topic.relatives(@topic.id, current_user)).to_a
|
||||
|
@ -94,9 +88,6 @@ class TopicsController < ApplicationController
|
|||
|
||||
# GET topics/:id/relatives
|
||||
def relatives
|
||||
@topic = Topic.find(params[:id])
|
||||
authorize @topic
|
||||
|
||||
topics_already_has = params[:network] ? params[:network].split(',').map(&:to_i) : []
|
||||
|
||||
alltopics = policy_scope(Topic.relatives(@topic.id, current_user)).to_a
|
||||
|
@ -149,8 +140,6 @@ class TopicsController < ApplicationController
|
|||
# PUT /topics/1
|
||||
# PUT /topics/1.json
|
||||
def update
|
||||
@topic = Topic.find(params[:id])
|
||||
authorize @topic
|
||||
@topic.updated_by = current_user
|
||||
@topic.assign_attributes(topic_params)
|
||||
|
||||
|
@ -165,8 +154,6 @@ class TopicsController < ApplicationController
|
|||
|
||||
# DELETE topics/:id
|
||||
def destroy
|
||||
@topic = Topic.find(params[:id])
|
||||
authorize @topic
|
||||
@topic.updated_by = current_user
|
||||
@topic.destroy
|
||||
respond_to do |format|
|
||||
|
@ -174,8 +161,50 @@ class TopicsController < ApplicationController
|
|||
end
|
||||
end
|
||||
|
||||
# POST topics/:id/follow
|
||||
def follow
|
||||
follow = FollowService.follow(@topic, current_user, 'followed')
|
||||
|
||||
respond_to do |format|
|
||||
format.json do
|
||||
if follow
|
||||
head :ok
|
||||
else
|
||||
head :bad_request
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# POST topics/:id/unfollow
|
||||
def unfollow
|
||||
FollowService.unfollow(@topic, current_user)
|
||||
|
||||
respond_to do |format|
|
||||
format.json do
|
||||
head :ok
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# GET topics/:id/unfollow_from_email
|
||||
def unfollow_from_email
|
||||
FollowService.unfollow(@topic, current_user)
|
||||
|
||||
respond_to do |format|
|
||||
format.html do
|
||||
redirect_to topic_path(@topic), notice: 'You are no longer following this topic'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_topic
|
||||
@topic = Topic.find(params[:id])
|
||||
authorize @topic
|
||||
end
|
||||
|
||||
def topic_params
|
||||
params.require(:topic).permit(:id, :name, :desc, :link, :permission, :metacode_id, :defer_to_map_id)
|
||||
end
|
||||
|
|
|
@ -1,55 +1,5 @@
|
|||
# frozen_string_literal: true
|
||||
module ApplicationHelper
|
||||
def metacodeset
|
||||
metacodes = current_user.settings.metacodes
|
||||
|
||||
return false unless metacodes[0].include?('metacodeset')
|
||||
return 'Most' if metacodes[0].sub('metacodeset-', '') == 'Most'
|
||||
return 'Recent' if metacodes[0].sub('metacodeset-', '') == 'Recent'
|
||||
|
||||
MetacodeSet.find(metacodes[0].sub('metacodeset-', '').to_i)
|
||||
end
|
||||
|
||||
def user_metacodes
|
||||
@m = current_user.settings.metacodes
|
||||
set = metacodeset
|
||||
@metacodes = if set && set == 'Most'
|
||||
Metacode.where(id: current_user.most_used_metacodes).to_a
|
||||
elsif set && set == 'Recent'
|
||||
Metacode.where(id: current_user.recent_metacodes).to_a
|
||||
elsif set
|
||||
set.metacodes.to_a
|
||||
else
|
||||
Metacode.where(id: @m).to_a
|
||||
end
|
||||
|
||||
focus_code = user_metacode()
|
||||
if focus_code != nil && @metacodes.index{|m| m.id == focus_code.id} == nil
|
||||
@metacodes.push(focus_code)
|
||||
end
|
||||
|
||||
@metacodes
|
||||
.sort! { |m1, m2| m2.name.downcase <=> m1.name.downcase }
|
||||
|
||||
if focus_code != nil
|
||||
@metacodes.rotate!(@metacodes.index{|m| m.id == focus_code.id})
|
||||
else
|
||||
@metacodes.rotate!(-1)
|
||||
end
|
||||
end
|
||||
|
||||
def user_metacode
|
||||
current_user.settings.metacode_focus ? Metacode.find(current_user.settings.metacode_focus.to_i) : nil
|
||||
end
|
||||
|
||||
def user_most_used_metacodes
|
||||
@metacodes = current_user.most_used_metacodes.map { |id| Metacode.find(id) }
|
||||
end
|
||||
|
||||
def user_recent_metacodes
|
||||
@metacodes = current_user.recent_metacodes.map { |id| Metacode.find(id) }
|
||||
end
|
||||
|
||||
def invite_link
|
||||
"#{request.base_url}/join" + (current_user ? "?code=#{current_user.code}" : '')
|
||||
end
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
module MetacodeSetsHelper
|
||||
end
|
|
@ -1,3 +1,78 @@
|
|||
# frozen_string_literal: true
|
||||
module MetacodesHelper
|
||||
def metacodeset
|
||||
metacodes = current_user.settings.metacodes
|
||||
|
||||
return false unless metacodes[0].include?('metacodeset')
|
||||
return 'Most' if metacodes[0].sub('metacodeset-', '') == 'Most'
|
||||
return 'Recent' if metacodes[0].sub('metacodeset-', '') == 'Recent'
|
||||
|
||||
MetacodeSet.find(metacodes[0].sub('metacodeset-', '').to_i)
|
||||
end
|
||||
|
||||
def user_metacodes
|
||||
@m = current_user.settings.metacodes
|
||||
set = metacodeset
|
||||
@metacodes = if set && set == 'Most'
|
||||
Metacode.where(id: current_user.most_used_metacodes).to_a
|
||||
elsif set && set == 'Recent'
|
||||
Metacode.where(id: current_user.recent_metacodes).to_a
|
||||
elsif set
|
||||
set.metacodes.to_a
|
||||
else
|
||||
Metacode.where(id: @m).to_a
|
||||
end
|
||||
|
||||
focus_code = user_metacode
|
||||
if !focus_code.nil? && @metacodes.index { |m| m.id == focus_code.id }.nil?
|
||||
@metacodes.push(focus_code)
|
||||
end
|
||||
|
||||
@metacodes.sort! { |m1, m2| m2.name.downcase <=> m1.name.downcase }
|
||||
|
||||
if !focus_code.nil?
|
||||
@metacodes.rotate!(@metacodes.index { |m| m.id == focus_code.id })
|
||||
else
|
||||
@metacodes.rotate!(-1)
|
||||
end
|
||||
end
|
||||
|
||||
def user_metacode
|
||||
current_user.settings.metacode_focus ? Metacode.find(current_user.settings.metacode_focus.to_i) : nil
|
||||
end
|
||||
|
||||
def user_most_used_metacodes
|
||||
@metacodes = current_user.most_used_metacodes.map { |id| Metacode.find(id) }
|
||||
end
|
||||
|
||||
def user_recent_metacodes
|
||||
@metacodes = current_user.recent_metacodes.map { |id| Metacode.find(id) }
|
||||
end
|
||||
|
||||
def metacode_sets_json
|
||||
metacode_sets = []
|
||||
metacode_sets << {
|
||||
name: 'Recently Used',
|
||||
metacodes: user_recent_metacodes
|
||||
.map { |m| { id: m.id, icon_path: asset_path(m.icon), name: m.name } }
|
||||
}
|
||||
metacode_sets << {
|
||||
name: 'Most Used',
|
||||
metacodes: user_most_used_metacodes
|
||||
.map { |m| { id: m.id, icon_path: asset_path(m.icon), name: m.name } }
|
||||
}
|
||||
metacode_sets += MetacodeSet.order('name').all.map do |set|
|
||||
{
|
||||
name: set.name,
|
||||
metacodes: set.metacodes.order('name')
|
||||
.map { |m| { id: m.id, icon_path: asset_path(m.icon), name: m.name } }
|
||||
}
|
||||
end
|
||||
metacode_sets << {
|
||||
name: 'All',
|
||||
metacodes: Metacode.order('name').all
|
||||
.map { |m| { id: m.id, icon_path: asset_path(m.icon), name: m.name } }
|
||||
}
|
||||
metacode_sets.to_json
|
||||
end
|
||||
end
|
||||
|
|
|
@ -3,10 +3,6 @@ class ApplicationMailer < ActionMailer::Base
|
|||
default from: 'team@metamaps.cc'
|
||||
layout 'mailer'
|
||||
|
||||
def deliver
|
||||
raise NotImplementedError('Please use Mailboxer to send your emails.')
|
||||
end
|
||||
|
||||
class << self
|
||||
def mail_for_notification(notification)
|
||||
case notification.notification_code
|
||||
|
|
11
app/mailers/map_activity_mailer.rb
Normal file
11
app/mailers/map_activity_mailer.rb
Normal file
|
@ -0,0 +1,11 @@
|
|||
# frozen_string_literal: true
|
||||
class MapActivityMailer < ApplicationMailer
|
||||
default from: 'team@metamaps.cc'
|
||||
|
||||
def daily_summary(user, map, summary_data)
|
||||
@user = user
|
||||
@map = map
|
||||
@summary_data = summary_data
|
||||
mail(to: user.email, subject: MapActivityService.subject_line(map))
|
||||
end
|
||||
end
|
|
@ -41,10 +41,11 @@ class Mapping < ApplicationRecord
|
|||
topic2: mappable.topic2.filtered,
|
||||
mapping_id: id
|
||||
)
|
||||
Events::SynapseAddedToMap.publish!(mappable, map, user, nil)
|
||||
meta = { 'mapping_id': id }
|
||||
Events::SynapseAddedToMap.publish!(mappable, map, user, meta)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
def after_created_async
|
||||
FollowService.follow(map, user, 'contributed')
|
||||
end
|
||||
|
@ -57,7 +58,7 @@ class Mapping < ApplicationRecord
|
|||
ActionCable.server.broadcast 'map_' + map.id.to_s, type: 'topicMoved', id: mappable.id, mapping_id: id, x: xloc, y: yloc
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
def after_updated_async
|
||||
if (mappable_type == 'Topic') && (xloc_changed? || yloc_changed?)
|
||||
FollowService.follow(map, updated_by, 'contributed')
|
||||
|
|
|
@ -68,13 +68,13 @@ class Synapse < ApplicationRecord
|
|||
output += %(\n)
|
||||
output
|
||||
end
|
||||
|
||||
|
||||
protected
|
||||
|
||||
|
||||
def set_perm_by_defer
|
||||
permission = defer_to_map.permission if defer_to_map
|
||||
end
|
||||
|
||||
|
||||
def after_created_async
|
||||
follow_ids = NotificationService.notify_followers(topic1, TOPIC_CONNECTED_1, self)
|
||||
NotificationService.notify_followers(topic2, TOPIC_CONNECTED_2, self, nil, follow_ids)
|
||||
|
@ -93,7 +93,7 @@ class Synapse < ApplicationRecord
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
def before_destroyed
|
||||
# hard to know how to do this yet, because the synapse actually gets destroyed
|
||||
#NotificationService.notify_followers(topic1, 'topic_disconnected', self)
|
||||
|
|
|
@ -52,10 +52,20 @@ class User < ApplicationRecord
|
|||
|
||||
# override default as_json
|
||||
def as_json(_options = {})
|
||||
{ id: id,
|
||||
json = { id: id,
|
||||
name: name,
|
||||
image: image.url(:sixtyfour),
|
||||
admin: admin }
|
||||
if (_options[:follows])
|
||||
json['follows'] = {
|
||||
topics: following.where(followed_type: 'Topic').to_a.map(&:followed_id),
|
||||
maps: following.where(followed_type: 'Map').to_a.map(&:followed_id)
|
||||
}
|
||||
end
|
||||
if (_options[:email])
|
||||
json['email'] = email
|
||||
end
|
||||
json
|
||||
end
|
||||
|
||||
def as_json_for_autocomplete
|
||||
|
|
|
@ -90,4 +90,16 @@ class MapPolicy < ApplicationPolicy
|
|||
def unstar?
|
||||
user.present?
|
||||
end
|
||||
|
||||
def follow?
|
||||
show? && user.present?
|
||||
end
|
||||
|
||||
def unfollow?
|
||||
user.present?
|
||||
end
|
||||
|
||||
def unfollow_from_email?
|
||||
user.present?
|
||||
end
|
||||
end
|
||||
|
|
|
@ -55,6 +55,18 @@ class TopicPolicy < ApplicationPolicy
|
|||
show?
|
||||
end
|
||||
|
||||
def follow?
|
||||
show? && user.present?
|
||||
end
|
||||
|
||||
def unfollow?
|
||||
user.present?
|
||||
end
|
||||
|
||||
def unfollow_from_email?
|
||||
user.present?
|
||||
end
|
||||
|
||||
# Helpers
|
||||
def map_policy
|
||||
@map_policy ||= Pundit.policy(user, record.defer_to_map)
|
||||
|
|
|
@ -10,11 +10,11 @@ class FollowService
|
|||
follow.follow_reason.update_attribute(reason, true)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
def unfollow(entity, user)
|
||||
Follow.where(followed: entity, user: user).destroy_all
|
||||
end
|
||||
|
||||
|
||||
def remove_reason(entity, user, reason)
|
||||
return unless FollowReason::REASONS.include?(reason)
|
||||
follow = Follow.where(followed: entity, user: user).first
|
||||
|
@ -25,9 +25,9 @@ class FollowService
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
protected
|
||||
|
||||
|
||||
def is_tester(user)
|
||||
%w(connorturland@gmail.com devin@callysto.com chessscholar@gmail.com solaureum@gmail.com ishanshapiro@gmail.com).include?(user.email)
|
||||
end
|
||||
|
|
98
app/services/map_activity_service.rb
Normal file
98
app/services/map_activity_service.rb
Normal file
|
@ -0,0 +1,98 @@
|
|||
class MapActivityService
|
||||
|
||||
def self.subject_line(map)
|
||||
'Activity on map ' + map.name
|
||||
end
|
||||
|
||||
def self.summarize_data(map, user, until_moment = DateTime.now)
|
||||
results = {
|
||||
stats: {}
|
||||
}
|
||||
|
||||
since = until_moment - 24.hours
|
||||
|
||||
scoped_topic_ids = TopicPolicy::Scope.new(user, map.topics).resolve.map(&:id)
|
||||
scoped_synapse_ids = SynapsePolicy::Scope.new(user, map.synapses).resolve.map(&:id)
|
||||
|
||||
message_count = Message.where(resource: map)
|
||||
.where("created_at > ? AND created_at < ?", since, until_moment)
|
||||
.where.not(user: user).count
|
||||
if message_count > 0
|
||||
results[:stats][:messages_sent] = message_count
|
||||
end
|
||||
|
||||
moved_count = Event.where(kind: 'topic_moved_on_map', map: map)
|
||||
.where("created_at > ? AND created_at < ?", since, until_moment)
|
||||
.where(eventable_id: scoped_topic_ids)
|
||||
.where.not(user: user).group(:eventable_id).count
|
||||
if moved_count.keys.length > 0
|
||||
results[:stats][:topics_moved] = moved_count.keys.length
|
||||
end
|
||||
|
||||
topics_added_events = Event.where(kind: 'topic_added_to_map', map: map)
|
||||
.where("created_at > ? AND created_at < ?", since, until_moment)
|
||||
.where.not(user: user)
|
||||
.order(:created_at)
|
||||
|
||||
topics_removed_events = Event.where(kind: 'topic_removed_from_map', map: map)
|
||||
.where("created_at > ? AND created_at < ?", since, until_moment)
|
||||
.where.not(user: user)
|
||||
.order(:created_at)
|
||||
|
||||
topics_added_to_include = {}
|
||||
topics_added_events.each do |ta|
|
||||
num_adds = topics_added_events.where(eventable_id: ta.eventable_id).count
|
||||
num_removes = topics_removed_events.where(eventable_id: ta.eventable_id).count
|
||||
topics_added_to_include[ta.eventable_id] = ta if num_adds > num_removes && scoped_topic_ids.include?(ta.eventable.id)
|
||||
end
|
||||
if topics_added_to_include.keys.length > 0
|
||||
results[:stats][:topics_added] = topics_added_to_include.keys.length
|
||||
results[:topics_added] = topics_added_to_include.values
|
||||
end
|
||||
|
||||
topics_removed_to_include = {}
|
||||
topics_removed_events.each do |ta|
|
||||
num_adds = topics_added_events.where(eventable_id: ta.eventable_id).count
|
||||
num_removes = topics_removed_events.where(eventable_id: ta.eventable_id).count
|
||||
topics_removed_to_include[ta.eventable_id] = ta if num_removes > num_adds && TopicPolicy.new(user, ta.eventable).show?
|
||||
end
|
||||
if topics_removed_to_include.keys.length > 0
|
||||
results[:stats][:topics_removed] = topics_removed_to_include.keys.length
|
||||
results[:topics_removed] = topics_removed_to_include.values
|
||||
end
|
||||
|
||||
synapses_added_events = Event.where(kind: 'synapse_added_to_map', map: map)
|
||||
.where("created_at > ? AND created_at < ?", since, until_moment)
|
||||
.where.not(user: user)
|
||||
.order(:created_at)
|
||||
|
||||
synapses_removed_events = Event.where(kind: 'synapse_removed_from_map', map: map)
|
||||
.where("created_at > ? AND created_at < ?", since, until_moment)
|
||||
.where.not(user: user)
|
||||
.order(:created_at)
|
||||
|
||||
synapses_added_to_include = {}
|
||||
synapses_added_events.each do |ta|
|
||||
num_adds = synapses_added_events.where(eventable_id: ta.eventable_id).count
|
||||
num_removes = synapses_removed_events.where(eventable_id: ta.eventable_id).count
|
||||
synapses_added_to_include[ta.eventable_id] = ta if num_adds > num_removes && scoped_synapse_ids.include?(ta.eventable.id)
|
||||
end
|
||||
if synapses_added_to_include.keys.length > 0
|
||||
results[:stats][:synapses_added] = synapses_added_to_include.keys.length
|
||||
results[:synapses_added] = synapses_added_to_include.values
|
||||
end
|
||||
|
||||
synapses_removed_to_include = {}
|
||||
synapses_removed_events.each do |ta|
|
||||
num_adds = synapses_added_events.where(eventable_id: ta.eventable_id).count
|
||||
num_removes = synapses_removed_events.where(eventable_id: ta.eventable_id).count
|
||||
synapses_removed_to_include[ta.eventable_id] = ta if num_removes > num_adds && SynapsePolicy.new(user, ta.eventable).show?
|
||||
end
|
||||
if synapses_removed_to_include.keys.length > 0
|
||||
results[:stats][:synapses_removed] = synapses_removed_to_include.keys.length
|
||||
results[:synapses_removed] = synapses_removed_to_include.values
|
||||
end
|
||||
|
||||
results
|
||||
end
|
||||
end
|
|
@ -3,7 +3,7 @@
|
|||
<%= render :partial => 'shared/metacodeBgColors' %>
|
||||
<script type="text/javascript" charset="utf-8">
|
||||
<% if current_user %>
|
||||
Metamaps.ServerData.ActiveMapper = <%= current_user.to_json.html_safe %>
|
||||
Metamaps.ServerData.ActiveMapper = <%= current_user.to_json({follows: true, email: true}).html_safe %>
|
||||
<% else %>
|
||||
Metamaps.ServerData.ActiveMapper = null
|
||||
<% end %>
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
<script type="text/template" id="mapInfoBoxTemplate">
|
||||
<div class="requestTitle">Click here to name this map</div>
|
||||
<div class="mapInfoName" id="mapInfoName">{{{name}}}</div>
|
||||
|
||||
|
||||
<div class="mapInfoStat">
|
||||
<div class="infoStatIcon mapContributors hoverForTip">
|
||||
<img id="mapContribs" class="{{contributors_class}}"
|
||||
|
@ -29,7 +29,7 @@
|
|||
<div class="mapInfoDesc" id="mapInfoDesc">
|
||||
{{{desc}}}
|
||||
</div>
|
||||
|
||||
|
||||
<div class="mapInfoMeta">
|
||||
<p class="mapCreatedAt"><span>Created by:</span> {{user_name}} on {{created_at}}</p>
|
||||
<p class="mapEditedAt"><span>Last edited:</span> {{updated_at}}</p>
|
||||
|
@ -50,7 +50,7 @@
|
|||
</div>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
|
||||
<script type="text/template" id="topicSearchTemplate">
|
||||
<div class="result{{rtype}}">
|
||||
<div class="topicMetacode searchResIconWrapper">
|
||||
|
@ -100,7 +100,7 @@
|
|||
</div>
|
||||
<div class="mapContributorsIcon hoverForTip">
|
||||
<img id="mapContribs" width="25" height="25" src="{{mapContributorImage}}" />
|
||||
<div class="tip">
|
||||
<div class="tip">
|
||||
<ul>
|
||||
{{{contributorTip}}}
|
||||
</ul>
|
||||
|
@ -181,72 +181,4 @@
|
|||
<div class="clearfloat"></div>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script type="text/template" id="topicCardTemplate">
|
||||
<div class="CardOnGraph {{hasAttachment}}" id="topic_{{id}}">
|
||||
<span class="title">
|
||||
<div class="titleWrapper" id="titleActivator">
|
||||
<span class="best_in_place best_in_place_name"
|
||||
data-bip-url="/topics/{{id}}"
|
||||
data-bip-object="topic"
|
||||
data-bip-attribute="name"
|
||||
data-bip-activator="#titleActivator"
|
||||
data-bip-value="{{name}}"
|
||||
data-bip-type="textarea"
|
||||
>
|
||||
{{name}}
|
||||
</span>
|
||||
</div>
|
||||
</span>
|
||||
<div class="links">
|
||||
<div class="linkItem icon">
|
||||
<div class="metacodeTitle {{metacode_class}}">
|
||||
{{metacode}}
|
||||
<div class="expandMetacodeSelect"></div>
|
||||
</div>
|
||||
<div class="metacodeImage" style="background-image:url({{imgsrc}});" title="click and drag to move card"></div>
|
||||
<div class="metacodeSelect">{{{metacode_select}}}</div>
|
||||
</div>
|
||||
<div class="linkItem contributor">
|
||||
<a href="/explore/mapper/{{userid}}" target="_blank"><img src="<%= asset_path('user.png') %>" class="contributorIcon" width="32" height="32" /></a>
|
||||
<div class="contributorName">{{username}}</div>
|
||||
</div>
|
||||
<div class="linkItem mapCount">
|
||||
<div class="mapCountIcon"></div>
|
||||
{{map_count}}
|
||||
<div class ="hoverTip">Click to see which maps topic appears on</div>
|
||||
<div class="tip"><ul>{{{inmaps}}}</ul></div>
|
||||
</div>
|
||||
<a href="/topics/{{id}}" target="_blank" class="linkItem synapseCount">
|
||||
<div class="synapseCountIcon"></div>
|
||||
{{synapse_count}}
|
||||
<div class="tip">Click to see this topics synapses</div>
|
||||
</a>
|
||||
<div class="linkItem mapPerm {{mk_permission}}" title="{{permission}}"></div>
|
||||
<div class="clearfloat"></div>
|
||||
</div>
|
||||
<div class="scroll">
|
||||
<div class="desc">
|
||||
<span class="best_in_place best_in_place_desc"
|
||||
data-bip-url="/topics/{{id}}"
|
||||
data-bip-object="topic"
|
||||
data-bip-nil="{{desc_nil}}"
|
||||
data-bip-attribute="desc"
|
||||
data-bip-type="textarea"
|
||||
data-bip-value="{{desc_markdown}}"
|
||||
>
|
||||
{{{desc_html}}}
|
||||
</span>
|
||||
<div class="clearfloat"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="embeds">
|
||||
{{{embeds}}}
|
||||
</div>
|
||||
<div class="attachments {{attachmentsHidden}}">
|
||||
{{{attachments}}}
|
||||
</div>
|
||||
<div class="clearfloat"></div>
|
||||
</div>
|
||||
</script>
|
||||
</div>
|
||||
|
|
57
app/views/map_activity_mailer/daily_summary.html.erb
Normal file
57
app/views/map_activity_mailer/daily_summary.html.erb
Normal file
|
@ -0,0 +1,57 @@
|
|||
<% button_style = "background-color:#4fc059;border-radius:2px;color:white;display:inline-block;font-family:Roboto,Arial,Helvetica,sans-serif;font-size:12px;font-weight:bold;min-height:29px;line-height:29px;min-width:54px;outline:0px;padding:0 8px;text-align:center;text-decoration:none" %>
|
||||
|
||||
<!DOCTYPE html>
|
||||
<div style="padding: 16px; background: white; text-align: left; font-family: Arial">
|
||||
<p>Hey <%= @user.name %>, there was activity by others in the last 24 hours on map
|
||||
<%= link_to @map.name, map_url(@map) %>
|
||||
</p>
|
||||
<p># of messages: <%= @summary_data[:stats][:messages_sent] || 0 %></p>
|
||||
<p># of topics added: <%= @summary_data[:stats][:topics_added] || 0 %></p>
|
||||
<p># of topics moved: <%= @summary_data[:stats][:topics_moved] || 0%></p>
|
||||
<p># of topics removed: <%= @summary_data[:stats][:topics_removed] || 0 %></p>
|
||||
<p># of synapses added: <%= @summary_data[:stats][:synapses_added] || 0 %></p>
|
||||
<p># of synapses removed: <%= @summary_data[:stats][:synapses_removed] || 0 %></p>
|
||||
<hr>
|
||||
<% if @summary_data[:topics_added] %>
|
||||
<h2>Topics Added</h2>
|
||||
<ul>
|
||||
<% @summary_data[:topics_added].each do |event| %>
|
||||
<li><%= event.eventable.name %></li>
|
||||
<% end %>
|
||||
</ul>
|
||||
<% end %>
|
||||
|
||||
<% if @summary_data[:topics_removed] %>
|
||||
<h2>Topics Removed</h2>
|
||||
<ul>
|
||||
<% @summary_data[:topics_removed].each do |event| %>
|
||||
<li><%= event.eventable.name %></li>
|
||||
<% end %>
|
||||
</ul>
|
||||
<% end %>
|
||||
|
||||
<% if @summary_data[:synapses_added] %>
|
||||
<h2>Synapses Added</h2>
|
||||
<ul>
|
||||
<% @summary_data[:synapses_added].each do |event| %>
|
||||
<li><%= event.eventable.topic1.name %> -> <%= event.eventable.topic2.name %></li>
|
||||
<% end %>
|
||||
</ul>
|
||||
<% end %>
|
||||
|
||||
<% if @summary_data[:synapses_removed] %>
|
||||
<h2>Synapses Removed</h2>
|
||||
<ul>
|
||||
<% @summary_data[:synapses_removed].each do |event| %>
|
||||
<li><%= event.eventable.topic1.name %> -> <%= event.eventable.topic2.name %></li>
|
||||
<% end %>
|
||||
</ul>
|
||||
<% end %>
|
||||
|
||||
<%= link_to 'Visit Map', map_url(@map), style: button_style %>
|
||||
|
||||
<hr>
|
||||
<p style="font-size: 14px;">Make sense with Metamaps</p>
|
||||
<%= link_to 'Unfollow this map', unfollow_from_email_map_url(@map) %>
|
||||
<%= render partial: 'shared/mailer_unsubscribe_link' %>
|
||||
</div>
|
0
app/views/map_mailer/_unfollow.html.erb
Normal file
0
app/views/map_mailer/_unfollow.html.erb
Normal file
0
app/views/map_mailer/_unfollow.text.erb
Normal file
0
app/views/map_mailer/_unfollow.text.erb
Normal file
|
@ -26,9 +26,11 @@
|
|||
</ul>
|
||||
|
||||
<div id="csTopicView">
|
||||
<div class="csItem"><span class="csTitle">Enter Topic (radial) View:</span> Click on a Topic result from Search, or click the synapse <img src="<%= asset_path 'synapse16.png' %>" width="16" align="middle" /> icon inside open Topic Card on map</div>
|
||||
<div class="csItem"><span class="csTitle">Recenter Topics around chosen Topic:</span> Alt + click on the topic OR Alt + E</div>
|
||||
<div class="csItem"><span class="csTitle">Reveal the siblings for a Topic:</span> Right-click and choose 'Reveal siblings' OR Alt + R</div>
|
||||
<div class="csItem"><span class="csTitle">Center topic and reveal siblings:</span> Alt + T</div>
|
||||
<div class="csItem"><span class="csTitle">Filter out visible Topics:</span> Open Filter menu *** and toggle off/on</div>
|
||||
</div>
|
||||
|
||||
<div id="csCreatingTopics">
|
||||
|
@ -44,19 +46,19 @@
|
|||
|
||||
<div id="csEditingTopics">
|
||||
<div class="csItem">
|
||||
<span class="csTitle">Open 'Topic' card:</span> Double-click on topic icon
|
||||
<span class="csTitle">Open Topic card:</span> Double-click on topic icon
|
||||
</div>
|
||||
<div class="csItem indented">
|
||||
<span class="csTitle">Move 'Topic' card:</span> Click and drag on topic card metacode
|
||||
<span class="csTitle">Move Topic card:</span> Click and drag on topic card metacode
|
||||
</div>
|
||||
<div class="csItem indented">
|
||||
<span class="csTitle">Change/edit metacode:</span> Mouse over metacode icon, then click on solid colored bar
|
||||
<span class="csTitle">Change metacode:</span> Mouse over metacode icon, then click on solid colored bar for metacode menu
|
||||
</div>
|
||||
<div class="csItem indented">
|
||||
<span class="csTitle">Edit Topic title, description, link:</span> Click on text in respective area
|
||||
<span class="csTitle">Edit Topic title, description, link:</span> Click on text in respective area (click small "X" to reset link)
|
||||
</div>
|
||||
<div class="csItem indented">
|
||||
<span class="csTitle">Save Topic title, description, link:</span> Hit enter
|
||||
<span class="csTitle">Save Topic title, description, link:</span> Hit enter, or click away
|
||||
</div>
|
||||
<div class="csItem indented">
|
||||
<span class="csTitle">Change Topic permission:</span> Click on 'Permission' icon (only for topic creator)
|
||||
|
@ -65,35 +67,34 @@
|
|||
<span class="csTitle">Open Topic view:</span> Click on <img src="<%= asset_path 'synapse16.png' %>" width="16" align="middle" /> icon within topic card bar
|
||||
</div>
|
||||
<div class="csItem indented">
|
||||
<span class="csTitle">Close 'Topic' card:</span> Click on canvas
|
||||
<span class="csTitle">Close Topic card:</span> Click on canvas
|
||||
</div>
|
||||
<div class="csItem">
|
||||
<span class="csTitle">Open 'Context Menu':</span> Right-click/alt+click on topic icon or synapse
|
||||
<span class="csTitle">Open 'Context Menu':</span> Right-click/alt+click on topic icon or synapse or selection (multiple) to Hide/Remove/Delete, change metacode or permission
|
||||
</div>
|
||||
<div class="csItem indented">*Hide/Remove/Delete topic within context menu</div>
|
||||
<div class="csItem"><br><a href="https://docs.metamaps.cc/creating_topics.html" target= "_blank">Learn More</a></div>
|
||||
|
||||
</div>
|
||||
|
||||
<div id="csCreatingSynapses">
|
||||
<div class="csItem"><span class="csTitle">Open 'Create Synapse' prompt:</span> Right-click & drag from one topic to another</div>
|
||||
<div class="csItem indented"><span class="csTitle">Enter or Tab:</span> Create synapse</div>
|
||||
<div class="csItem indented"><span class="csTitle">Esc or Delete:</span> Cancel synapse creation</div>
|
||||
<div class="csItem indented">*You do not have to add a description</div>
|
||||
<div class="csItem"><span class="csTitle">Create new Topic with Synapse:</span> Right-click + drag from topic to open canvas</div>
|
||||
<div class="csItem indented"><span class="csTitle">Enter:</span> Create topic</div>
|
||||
<div class="csItem indented"><span class="csTitle">Enter:</span> Create synapse</div>
|
||||
<div class="csItem indented"><span class="csTitle">Enter a label</span> Begin typing (or leave blank)</div>
|
||||
<div class="csItem indented"><span class="csTitle">Confirm new Synapse:</span> Enter or Tab</div>
|
||||
<div class="csItem indented"><span class="csTitle">Cancel new Synapse:</span> Escape or Delete</div>
|
||||
<div class="csItem"><span class="csTitle">Create new Topic with Synapse:</span> Right-click + drag from existing topic to open canvas</div>
|
||||
<div class="csItem indented"><span class="csTitle">Create Topic:</span> Same as elsewhere</div>
|
||||
<div class="csItem indented"><span class="csTitle">Create Synapse:</span> Same as above</div>
|
||||
<div class="csItem"><br><a href="https://docs.metamaps.cc/creating_synapses.html" target= "_blank">Learn More</a></div>
|
||||
|
||||
</div>
|
||||
|
||||
<div id="csEditingSynapses">
|
||||
<div class="csItem"><span class="csTitle">Open 'Synapse' card:</span> Double-click on Synapse </div>
|
||||
<div class="csItem"><span class="csTitle">Open Synapse card:</span> Double-click on Synapse </div>
|
||||
<div class="csItem indented"><span class="csTitle">Edit Synapse description:</span> Click on current description text</div>
|
||||
<div class="csItem indented"><span class="csTitle">Save Synapse description:</span> Hit enter</div>
|
||||
<div class="csItem indented"><span class="csTitle">Edit directionality:</span> Select appropriate arrow boxes</div>
|
||||
<div class="csItem indented"><span class="csTitle">Change synapse permission:</span> Click on 'permission' icon (only for synapse creator)</div>
|
||||
<div class="csItem indented"><span class="csTitle">Browse synapses / change visible synapse</span> click on arrow icon and select desired synapse</div>
|
||||
<div class="csItem indented"><span class="csTitle">Browse / select from multiple (stacked) synapses:</span> Click dropdown icon and select desired synapse</div>
|
||||
<div class="csItem"><span class="csTitle">Open 'Context Menu':</span> Right-click/alt-click on Synapse</div>
|
||||
<div class="csItem indented">*Hide/Remove/Delete synapse within context menu</div>
|
||||
<div class="csItem"><br><a href="https://docs.metamaps.cc/creating_synapses.html" target= "_blank">Learn More</a></div>
|
||||
|
@ -102,8 +103,10 @@
|
|||
|
||||
<div id="csNavigation">
|
||||
<div class="csItem"><span class="csTitle">Move around Canvas:</span> Click and drag</div>
|
||||
<div class="csItem"><span class="csTitle">Zoom in/out:</span> Scroll OR click on <div id="zoomIn"> </div> & <div id="zoomOut"> </div></div>
|
||||
<div class="csItem"><span class="csTitle">Zoom to see all:</span> Click <div id="centerMap"></div> OR Ctrl + E</div>
|
||||
<div class="csItem indented"><span class="csTitle">Zoom in/out:</span> Scroll OR click on <div id="zoomIn"> </div> & <div id="zoomOut"> </div></div>
|
||||
<div class="csItem indented"><span class="csTitle">Zoom to see all:</span> Click <div id="centerMap"></div> OR Ctrl + E</div>
|
||||
<div class="csItem"><span class="csTitle">Filter Map Contents:</span> Open the Filter Menu *** and toggle items off/on</div>
|
||||
<div class="csItem"><span class="csTitle">Return to 'Explore Maps' (home) page:</span> Click the Metamaps logo in the upper left corner</div>
|
||||
<div class="csItem"><br><a href="https://docs.metamaps.cc/exploring_maps.html" target= "_blank">Learn More</a></div>
|
||||
|
||||
</div>
|
||||
|
@ -111,8 +114,8 @@
|
|||
<div id="csSelection">
|
||||
<div class="csItem"><span class="csTitle">Select/Deselect Topic:</span> Click on topic icon</div>
|
||||
<div class="csItem"><span class="csTitle">Select/Deselect Synapse:</span> Click on synapse</div>
|
||||
<div class="csItem"><span class="csTitle">Select multiple Topics/Synapses:</span> Shift + click</div>
|
||||
<div class="csItem"><span class="csTitle">Make Selection box, select multiple Topics/Synapses:</span> Right-click/Shift-click + drag on Canvas</div>
|
||||
<div class="csItem"><span class="csTitle">Select multiple Topics/Synapses:</span> Shift + click to include each</div>
|
||||
<div class="csItem"><span class="csTitle">Select multiple with Selection Box:</span> Right-click/Shift-click + drag on Canvas</div>
|
||||
<div class="csItem"><span class="csTitle">Move all selected Topics & Synapses:</span> Click + drag on selected topic(s)/synapse(s)</div>
|
||||
<div class="csItem"><span class="csTitle">Open 'Context Menu':</span> Right-click/Alt-click on selected topic(s)</div>
|
||||
<div class="csItem indented">*Hide/Remove/Delete/Change permissions of multiple topics & synapses within context menu</div>
|
||||
|
@ -121,11 +124,10 @@
|
|||
</div>
|
||||
|
||||
<div id="csSearch">
|
||||
<div class="csItem"><span class="csTitle">Open 'Search' prompt:</span> Ctrl + /</div>
|
||||
<div class="csItem"><span class="csTitle">Close 'Search' prompt:</span> Esc</div>
|
||||
<% if controller_name == "maps" && action_name == "show" %>
|
||||
<div class="csItem"><span class="csTitle">Add to current Map:</span> Click "+" on a topic result</div>
|
||||
<% end %>
|
||||
<div class="csItem"><span class="csTitle">Search for Topics and Maps:</span> Type query terms into search bar, wait for results below</div>
|
||||
<div class="csItem"><span class="csTitle">Limit search results:</span> Click checkbox for only items you created; click arrow above Topics or Maps section to collapse</div>
|
||||
<div class="csItem"><span class="csTitle">Add Topic to current Map:</span> Click "+" on a topic result</div>
|
||||
<div class="csItem"><span class="csTitle">Jump to Topic View:</span> Click anywhere else on a topic result</div>
|
||||
<div class="csItem"><span class="csTitle">Search by metacode:</span> type "[name of metacode]:", then your search query. i.e. idea:create...</div>
|
||||
<div class="csItem"><span class="csTitle">Search for map:</span> type "map:", then your search query. i.e. map:exploring...</div>
|
||||
<div class="csItem"><span class="csTitle">Search for mapper:</span> type "mapper:", then your search query. i.e. mapper:Robert</div>
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
<div class="unsubscribe-link">
|
||||
<%= link_to 'Click here to unsubscribe from all Metamaps emails', unsubscribe_notifications_url(protocol: Rails.env.production? ? :https : :http) %>
|
||||
<%= link_to 'Unsubscribe from all Metamaps emails', unsubscribe_notifications_url(protocol: Rails.env.production? ? :https : :http) %>
|
||||
</div>
|
||||
|
|
|
@ -3,61 +3,7 @@
|
|||
# this code generates the list of icons that will drop down in the metacode select list on the topic card
|
||||
#%>
|
||||
|
||||
<div id="metacodeOptions">
|
||||
<ul>
|
||||
<li>
|
||||
<span>Recently Used</span>
|
||||
<div class="expandMetacodeSet"></div>
|
||||
<ul>
|
||||
<% user_recent_metacodes().each do |m| %>
|
||||
<li data-id="<%= m.id.to_s %>">
|
||||
<img width="24" height="24" src="<%= asset_path m.icon %>" alt="<%= m.name %>" />
|
||||
<div class="mSelectName"><%= m.name %></div>
|
||||
<div class="clearfloat"></div>
|
||||
</li>
|
||||
<% end %>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<span>Most Used</span>
|
||||
<div class="expandMetacodeSet"></div>
|
||||
<ul>
|
||||
<% user_most_used_metacodes().each do |m| %>
|
||||
<li data-id="<%= m.id.to_s %>">
|
||||
<img width="24" height="24" src="<%= asset_path m.icon %>" alt="<%= m.name %>" />
|
||||
<div class="mSelectName"><%= m.name %></div>
|
||||
<div class="clearfloat"></div>
|
||||
</li>
|
||||
<% end %>
|
||||
</ul>
|
||||
</li>
|
||||
<% MetacodeSet.order("name").all.each do |set| %>
|
||||
<li>
|
||||
<span><%= set.name %></span>
|
||||
<div class="expandMetacodeSet"></div>
|
||||
<ul>
|
||||
<% set.metacodes.sort { |a, b| a.name <=> b.name }.each do |m| %>
|
||||
<li data-id="<%= m.id.to_s %>">
|
||||
<img width="24" height="24" src="<%= asset_path m.icon %>" alt="<%= m.name %>" />
|
||||
<div class="mSelectName"><%= m.name %></div>
|
||||
<div class="clearfloat"></div>
|
||||
</li>
|
||||
<% end %>
|
||||
</ul>
|
||||
</li>
|
||||
<% end %>
|
||||
<li>
|
||||
<span>All</span>
|
||||
<div class="expandMetacodeSet"></div>
|
||||
<ul>
|
||||
<% Metacode.order("name").all.each do |m| %>
|
||||
<li data-id="<%= m.id.to_s %>">
|
||||
<img width="24" height="24" src="<%= asset_path m.icon %>" alt="<%= m.name %>" />
|
||||
<div class="mSelectName"><%= m.name %></div>
|
||||
<div class="clearfloat"></div>
|
||||
</li>
|
||||
<% end %>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<script>
|
||||
Metamaps.ServerData = Metamaps.ServerData || {}
|
||||
Metamaps.ServerData.metacodeSets = <%= raw metacode_sets_json %>
|
||||
</script>
|
||||
|
|
|
@ -91,7 +91,9 @@
|
|||
</div>
|
||||
<% end %>
|
||||
<div id="metacodeSwitchTabsCustom">
|
||||
<p class="setDesc">Choose Your Metacodes</p>
|
||||
<div class="setDesc">Choose Your Metacodes</div>
|
||||
<div class="selectNone">NONE</div>
|
||||
<div class="selectAll">ALL</div>
|
||||
<% @list = '' %>
|
||||
<% metacodesInUse = user_metacodes() %>
|
||||
<% Metacode.order("name").all.each_with_index do |m, index| %>
|
||||
|
@ -116,4 +118,4 @@
|
|||
<script>
|
||||
Metamaps.Create.selectedMetacodeSet = "metacodeset-<%= selectedSet %>"
|
||||
Metamaps.Create.selectedMetacodeSetIndex = <%= index %>
|
||||
</script>
|
||||
</script>
|
||||
|
|
3
app/views/topic_mailer/_unfollow.html.erb
Normal file
3
app/views/topic_mailer/_unfollow.html.erb
Normal file
|
@ -0,0 +1,3 @@
|
|||
<hr>
|
||||
You are receiving this email because you are following this topic.
|
||||
<%= link_to 'Unfollow', unfollow_from_email_topic_url(topic) %>
|
2
app/views/topic_mailer/_unfollow.text.erb
Normal file
2
app/views/topic_mailer/_unfollow.text.erb
Normal file
|
@ -0,0 +1,2 @@
|
|||
You are receiving this email because you are following this topic.
|
||||
To unfollow, go to: <%= unfollow_from_email_topic_url(topic) %>
|
|
@ -8,4 +8,6 @@
|
|||
</p>
|
||||
|
||||
<%= link_to 'Go to Topic', topic_url(topic), style: button_style %>
|
||||
<%= link_to 'Go to Map', map_url(event.map), style: button_style %>
|
||||
<%= link_to 'Go to Map', map_url(event.map), style: button_style %>
|
||||
|
||||
<%= render :partial => 'topic_mailer/unfollow', locals: { topic: topic } %>
|
|
@ -3,4 +3,6 @@
|
|||
<%= event.user.name %> added topic <%= topic.name %> to map <%= event.map.name %>
|
||||
|
||||
topic_url(topic)
|
||||
map_url(event.map)
|
||||
map_url(event.map)
|
||||
|
||||
<%= render :partial => 'topic_mailer/unfollow', locals: { topic: topic } %>
|
|
@ -12,4 +12,6 @@
|
|||
<% end %>
|
||||
</p>
|
||||
|
||||
<%= link_to 'View the connection', topic_url(topic1), style: button_style %>
|
||||
<%= link_to 'View the connection', topic_url(topic1), style: button_style %>
|
||||
|
||||
<%= render :partial => 'topic_mailer/unfollow', locals: { topic: topic1 } %>
|
|
@ -5,4 +5,6 @@
|
|||
<%= synapse.user.name %> connected topic <%= topic1.name %> to topic <%= topic2.name %>
|
||||
<%= synapse.desc.length > 0 ? ' with the description' + synapse.desc : '' %>
|
||||
|
||||
<%= topic_url(topic1) %>
|
||||
<%= topic_url(topic1) %>
|
||||
|
||||
<%= render :partial => 'topic_mailer/unfollow', locals: { topic: topic1 } %>
|
|
@ -15,8 +15,6 @@ MAP_ACCESS_REQUEST = 'ACCESS_REQUEST'
|
|||
MAP_INVITE_TO_EDIT = 'INVITE_TO_EDIT'
|
||||
|
||||
# these ones are new
|
||||
# this one's a catch all for occurences on the map
|
||||
# MAP_ACTIVITY = 'MAP_ACTIVITY'
|
||||
# MAP_RECEIVED_TOPIC
|
||||
# MAP_LOST_TOPIC
|
||||
# MAP_TOPIC_MOVED
|
||||
|
|
|
@ -48,6 +48,9 @@ Metamaps::Application.routes.draw do
|
|||
|
||||
post :star, to: 'stars#create', default: { format: :json }
|
||||
post :unstar, to: 'stars#destroy', default: { format: :json }
|
||||
post :follow, default: { format: :json }
|
||||
post :unfollow, default: { format: :json }
|
||||
get :unfollow_from_email
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -83,6 +86,9 @@ Metamaps::Application.routes.draw do
|
|||
get :network
|
||||
get :relative_numbers
|
||||
get :relatives
|
||||
post :follow, default: { format: :json }
|
||||
post :unfollow, default: { format: :json }
|
||||
get :unfollow_from_email
|
||||
end
|
||||
collection do
|
||||
get :autocomplete_topic
|
||||
|
|
|
@ -28,6 +28,8 @@ const Create = {
|
|||
}).addClass('ui-tabs-vertical ui-helper-clearfix')
|
||||
$('#metacodeSwitchTabs .ui-tabs-nav li').removeClass('ui-corner-top').addClass('ui-corner-left')
|
||||
$('.customMetacodeList li').click(self.toggleMetacodeSelected) // within the custom metacode set tab
|
||||
$('.selectAll').click(self.metacodeSelectorSelectAll)
|
||||
$('.selectNone').click(self.metacodeSelectorSelectNone)
|
||||
},
|
||||
toggleMetacodeSelected: function() {
|
||||
var self = Create
|
||||
|
@ -43,6 +45,46 @@ const Create = {
|
|||
self.newSelectedMetacodes.push($(this).attr('id'))
|
||||
self.newSelectedMetacodeNames.push($(this).attr('data-name'))
|
||||
}
|
||||
self.updateSelectAllColors()
|
||||
},
|
||||
updateSelectAllColors: function() {
|
||||
$('.selectAll, .selectNone').removeClass('selected')
|
||||
if (Create.metacodeSelectorAreAllSelected()) {
|
||||
$('.selectAll').addClass('selected')
|
||||
} else if (Create.metacodeSelectorAreNoneSelected()) {
|
||||
$('.selectNone').addClass('selected')
|
||||
}
|
||||
},
|
||||
metacodeSelectorSelectAll: function() {
|
||||
$('.customMetacodeList li.toggledOff').each(Create.toggleMetacodeSelected)
|
||||
Create.updateSelectAllColors()
|
||||
},
|
||||
metacodeSelectorSelectNone: function() {
|
||||
$('.customMetacodeList li').not('.toggledOff').each(Create.toggleMetacodeSelected)
|
||||
Create.updateSelectAllColors()
|
||||
},
|
||||
metacodeSelectorAreAllSelected: function() {
|
||||
return $('.customMetacodeList li').toArray()
|
||||
.map(li => !$(li).is('.toggledOff')) // note the ! on this line
|
||||
.reduce((curr, prev) => curr && prev)
|
||||
},
|
||||
metacodeSelectorAreNoneSelected: function() {
|
||||
return $('.customMetacodeList li').toArray()
|
||||
.map(li => $(li).is('.toggledOff'))
|
||||
.reduce((curr, prev) => curr && prev)
|
||||
},
|
||||
metacodeSelectorToggleSelectAll: function() {
|
||||
// should be called when Create.isSwitchingSet is true and .customMetacodeList is visible
|
||||
if (!Create.isSwitchingSet) return
|
||||
if (!$('.customMetacodeList').is(':visible')) return
|
||||
|
||||
// If all are selected, then select none. Otherwise, select all.
|
||||
if (Create.metacodeSelectorAreAllSelected()) {
|
||||
Create.metacodeSelectorSelectNone()
|
||||
} else {
|
||||
// if some, but not all, are selected, it still runs this function
|
||||
Create.metacodeSelectorSelectAll()
|
||||
}
|
||||
},
|
||||
updateMetacodeSet: function(set, index, custom) {
|
||||
if (custom && Create.newSelectedMetacodes.length === 0) {
|
||||
|
@ -114,7 +156,6 @@ const Create = {
|
|||
}
|
||||
})
|
||||
},
|
||||
|
||||
cancelMetacodeSetSwitch: function() {
|
||||
var self = Create
|
||||
self.isSwitchingSet = false
|
||||
|
|
|
@ -34,6 +34,9 @@ const Map = Backbone.Model.extend({
|
|||
return false
|
||||
}
|
||||
},
|
||||
isFollowedBy: function(mapper) {
|
||||
return mapper.get('follows') && mapper.get('follows').maps.indexOf(this.get('id')) > -1
|
||||
},
|
||||
getUser: function() {
|
||||
return Mapper.get(this.get('user_id'))
|
||||
},
|
||||
|
|
|
@ -5,7 +5,7 @@ import outdent from 'outdent'
|
|||
|
||||
const Mapper = Backbone.Model.extend({
|
||||
urlRoot: '/users',
|
||||
blacklist: ['created_at', 'updated_at'],
|
||||
blacklist: ['created_at', 'updated_at', 'follows'],
|
||||
toJSON: function(options) {
|
||||
return _.omit(this.attributes, this.blacklist)
|
||||
},
|
||||
|
@ -15,6 +15,20 @@ const Mapper = Backbone.Model.extend({
|
|||
<img src="${this.get('image')}" data-id="${this.id}" alt="${this.get('name')}" />
|
||||
<p>${this.get('name')}</p>
|
||||
</li>`
|
||||
},
|
||||
followMap: function(id) {
|
||||
this.get('follows').maps.push(id)
|
||||
},
|
||||
unfollowMap: function(id) {
|
||||
const idIndex = this.get('follows').maps.indexOf(id)
|
||||
if (idIndex > -1) this.get('follows').maps.splice(idIndex, 1)
|
||||
},
|
||||
followTopic: function(id) {
|
||||
this.get('follows').topics.push(id)
|
||||
},
|
||||
unfollowTopic: function(id) {
|
||||
const idIndex = this.get('follows').topics.indexOf(id)
|
||||
if (idIndex > -1) this.get('follows').topics.splice(idIndex, 1)
|
||||
}
|
||||
})
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@ try { Backbone.$ = window.$ } catch (err) {}
|
|||
|
||||
import Active from '../Active'
|
||||
import Filter from '../Filter'
|
||||
import TopicCard from '../TopicCard'
|
||||
import TopicCard from '../Views/TopicCard'
|
||||
import Visualize from '../Visualize'
|
||||
|
||||
import DataModel from './index'
|
||||
|
@ -47,6 +47,9 @@ const Topic = Backbone.Model.extend({
|
|||
if (mapper && this.get('user_id') === mapper.get('id')) return true
|
||||
else return false
|
||||
},
|
||||
isFollowedBy: function(mapper) {
|
||||
return mapper.get('follows') && mapper.get('follows').topics.indexOf(this.get('id')) > -1
|
||||
},
|
||||
getDate: function() {},
|
||||
getMetacode: function() {
|
||||
return DataModel.Metacodes.get(this.get('metacode_id'))
|
||||
|
|
|
@ -26,7 +26,10 @@ const ImportDialog = {
|
|||
ReactDOM.render(React.createElement(ImportDialogBox, {
|
||||
onFileAdded: PasteInput.handleFile,
|
||||
exampleImageUrl: serverData['import-example.png'],
|
||||
downloadScreenshot: ImportDialog.downloadScreenshot
|
||||
downloadScreenshot: ImportDialog.downloadScreenshot,
|
||||
onExport: format => {
|
||||
window.open(`${window.location.pathname}/export.${format}`, '_blank')
|
||||
}
|
||||
}), $('.importDialogWrapper').get(0))
|
||||
},
|
||||
show: function() {
|
||||
|
|
|
@ -29,8 +29,8 @@ const Import = {
|
|||
handleCSV: function(text, parserOpts = {}) {
|
||||
const self = Import
|
||||
|
||||
const topicsRegex = /("?Topics"?)([\s\S]*)/mi
|
||||
const synapsesRegex = /("?Synapses"?)([\s\S]*)/mi
|
||||
const topicsRegex = /("?Topics"?[, \t"]*)([\s\S]*)/mi
|
||||
const synapsesRegex = /("?Synapses"?[, \t"]*)([\s\S]*)/mi
|
||||
let topicsText = text.match(topicsRegex) || ''
|
||||
if (topicsText) topicsText = topicsText[2].replace(synapsesRegex, '')
|
||||
let synapsesText = text.match(synapsesRegex) || ''
|
||||
|
|
|
@ -2,9 +2,14 @@
|
|||
|
||||
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 Control from './Control'
|
||||
import Create from './Create'
|
||||
|
@ -18,10 +23,9 @@ import Settings from './Settings'
|
|||
import Synapse from './Synapse'
|
||||
import SynapseCard from './SynapseCard'
|
||||
import Topic from './Topic'
|
||||
import TopicCard from './TopicCard'
|
||||
import TopicCard from './Views/TopicCard'
|
||||
import Util from './Util'
|
||||
import Visualize from './Visualize'
|
||||
import clipboard from 'clipboard-js'
|
||||
|
||||
let panningInt
|
||||
|
||||
|
@ -1418,9 +1422,7 @@ const JIT = {
|
|||
<div class="expandLi"></div>
|
||||
</li>`
|
||||
|
||||
const metacodeOptions = $('#metacodeOptions').html()
|
||||
|
||||
menustring += '<li class="rc-metacode"><div class="rc-icon"></div>Change metacode' + metacodeOptions + '<div class="expandLi"></div></li>'
|
||||
menustring += '<li class="rc-metacode"><div class="rc-icon"></div>Change metacode<div id="metacodeOptionsWrapper"></div><div class="expandLi"></div></li>'
|
||||
}
|
||||
if (Active.Topic) {
|
||||
if (!Active.Mapper) {
|
||||
|
@ -1475,6 +1477,25 @@ const JIT = {
|
|||
// 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: TopicCard.metacodeSets
|
||||
}),
|
||||
document.getElementById('metacodeOptionsWrapper')
|
||||
)
|
||||
|
||||
// attach events to clicks on the list items
|
||||
|
||||
// delete the selected things from the database
|
||||
|
@ -1521,13 +1542,6 @@ const JIT = {
|
|||
Control.updateSelectedPermissions($(this).text())
|
||||
})
|
||||
|
||||
// change the metacode of all the selected nodes that you have edit permission for
|
||||
$('.rc-metacode li li').click(function() {
|
||||
$('.rightclickmenu').remove()
|
||||
//
|
||||
Control.updateSelectedMetacodes($(this).attr('data-id'))
|
||||
})
|
||||
|
||||
// fetch relatives
|
||||
let fetchSent = false
|
||||
$('.rc-siblings').hover(function() {
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
/* global $ */
|
||||
|
||||
import Active from './Active'
|
||||
import Create from './Create'
|
||||
import Control from './Control'
|
||||
import DataModel from './DataModel'
|
||||
import JIT from './JIT'
|
||||
|
@ -31,11 +32,18 @@ const Listeners = {
|
|||
JIT.escKeyHandler()
|
||||
break
|
||||
case 46: // if DEL is pressed
|
||||
e.preventDefault()
|
||||
Control.deleteSelected()
|
||||
if(e.target.tagName !== "INPUT" && e.target.tagName !== "TEXTAREA" && (Selected.Nodes.length + Selected.Edges.length) > 0){
|
||||
e.preventDefault()
|
||||
Control.removeSelectedNodes()
|
||||
Control.removeSelectedEdges()
|
||||
}
|
||||
break
|
||||
case 65: // if a or A is pressed
|
||||
if ((e.ctrlKey || e.metaKey) && onCanvas) {
|
||||
if (Create.isSwitchingSet && e.ctrlKey || e.metaKey) {
|
||||
Create.metacodeSelectorToggleSelectAll()
|
||||
e.preventDefault()
|
||||
break
|
||||
} else if ((e.ctrlKey || e.metaKey) && onCanvas) {
|
||||
const nodesCount = Object.keys(Visualize.mGraph.graph.nodes).length
|
||||
const selectedNodesCount = Selected.Nodes.length
|
||||
e.preventDefault()
|
||||
|
|
|
@ -16,7 +16,7 @@ import Realtime from '../Realtime'
|
|||
import Router from '../Router'
|
||||
import Selected from '../Selected'
|
||||
import SynapseCard from '../SynapseCard'
|
||||
import TopicCard from '../TopicCard'
|
||||
import TopicCard from '../Views/TopicCard'
|
||||
import Visualize from '../Visualize'
|
||||
|
||||
import CheatSheet from './CheatSheet'
|
||||
|
|
|
@ -20,6 +20,10 @@ const PasteInput = {
|
|||
}, false)
|
||||
window.addEventListener('drop', function(e) {
|
||||
e = e || window.event
|
||||
|
||||
// prevent conflict with react-dropzone file uploader
|
||||
if (event.target.id !== 'infovis-canvas') return
|
||||
|
||||
e.preventDefault()
|
||||
var coords = Util.pixelsToCoords(Visualize.mGraph, { x: e.clientX, y: e.clientY })
|
||||
if (e.dataTransfer.files.length > 0) {
|
||||
|
|
|
@ -14,7 +14,7 @@ import Router from './Router'
|
|||
import Selected from './Selected'
|
||||
import Settings from './Settings'
|
||||
import SynapseCard from './SynapseCard'
|
||||
import TopicCard from './TopicCard'
|
||||
import TopicCard from './Views/TopicCard'
|
||||
import Util from './Util'
|
||||
import Visualize from './Visualize'
|
||||
|
||||
|
|
|
@ -1,474 +0,0 @@
|
|||
/* global $, CanvasLoader, Countable, Hogan, embedly */
|
||||
|
||||
import Active from './Active'
|
||||
import DataModel from './DataModel'
|
||||
import GlobalUI from './GlobalUI'
|
||||
import Mapper from './Mapper'
|
||||
import Router from './Router'
|
||||
import Util from './Util'
|
||||
import Visualize from './Visualize'
|
||||
|
||||
const TopicCard = {
|
||||
openTopicCard: null, // stores the topic that's currently open
|
||||
authorizedToEdit: false, // stores boolean for edit permission for open topic card
|
||||
RAILS_ENV: undefined,
|
||||
init: function(serverData) {
|
||||
var self = TopicCard
|
||||
|
||||
if (serverData.RAILS_ENV) {
|
||||
self.RAILS_ENV = serverData.RAILS_ENV
|
||||
} else {
|
||||
console.error('RAILS_ENV is not defined! See TopicCard.js init function.')
|
||||
}
|
||||
|
||||
// initialize best_in_place editing
|
||||
$('.authenticated div.permission.canEdit .best_in_place').best_in_place()
|
||||
|
||||
TopicCard.generateShowcardHTML = Hogan.compile($('#topicCardTemplate').html())
|
||||
|
||||
// initialize topic card draggability and resizability
|
||||
$('.showcard').draggable({
|
||||
handle: '.metacodeImage',
|
||||
stop: function() {
|
||||
$(this).height('auto')
|
||||
}
|
||||
})
|
||||
|
||||
embedly('on', 'card.rendered', self.embedlyCardRendered)
|
||||
},
|
||||
/**
|
||||
* Will open the Topic Card for the node that it's passed
|
||||
* @param {$jit.Graph.Node} node
|
||||
*/
|
||||
showCard: function(node, opts) {
|
||||
var self = TopicCard
|
||||
if (!opts) opts = {}
|
||||
var topic = node.getData('topic')
|
||||
|
||||
self.openTopicCard = topic
|
||||
self.authorizedToEdit = topic.authorizeToEdit(Active.Mapper)
|
||||
// populate the card that's about to show with the right topics data
|
||||
self.populateShowCard(topic)
|
||||
return $('.showcard').fadeIn('fast', function() {
|
||||
if (opts.complete) {
|
||||
opts.complete()
|
||||
}
|
||||
})
|
||||
},
|
||||
hideCard: function() {
|
||||
var self = TopicCard
|
||||
|
||||
$('.showcard').fadeOut('fast')
|
||||
self.openTopicCard = null
|
||||
self.authorizedToEdit = false
|
||||
},
|
||||
embedlyCardRendered: function(iframe) {
|
||||
$('#embedlyLinkLoader').hide()
|
||||
|
||||
// means that the embedly call returned 404 not found
|
||||
if ($('#embedlyLink')[0]) {
|
||||
$('#embedlyLink').css('display', 'block').fadeIn('fast')
|
||||
$('.embeds').addClass('nonEmbedlyLink')
|
||||
}
|
||||
|
||||
$('.CardOnGraph').addClass('hasAttachment')
|
||||
},
|
||||
showLinkRemover: function() {
|
||||
if (TopicCard.authorizedToEdit && $('#linkremove').length === 0) {
|
||||
$('.embeds').append('<div id="linkremove"></div>')
|
||||
$('#linkremove').click(TopicCard.removeLink)
|
||||
}
|
||||
},
|
||||
removeLink: function() {
|
||||
var self = TopicCard
|
||||
self.openTopicCard.save({
|
||||
link: null
|
||||
})
|
||||
$('.embeds').empty().removeClass('nonEmbedlyLink')
|
||||
$('#addLinkInput input').val('')
|
||||
$('.attachments').removeClass('hidden')
|
||||
$('.CardOnGraph').removeClass('hasAttachment')
|
||||
},
|
||||
showLinkLoader: function() {
|
||||
var loader = new CanvasLoader('embedlyLinkLoader')
|
||||
loader.setColor('#4fb5c0') // default is '#000000'
|
||||
loader.setDiameter(28) // default is 40
|
||||
loader.setDensity(41) // default is 40
|
||||
loader.setRange(0.9) // default is 1.3
|
||||
loader.show() // Hidden by default
|
||||
},
|
||||
showLink: function(topic) {
|
||||
var e = embedly('card', document.getElementById('embedlyLink'))
|
||||
if (!e && TopicCard.RAILS_ENV !== 'development') {
|
||||
TopicCard.handleInvalidLink()
|
||||
} else if (!e) {
|
||||
$('#embedlyLink').attr('target', '_blank').html(topic.get('link')).show()
|
||||
$('#embedlyLinkLoader').hide()
|
||||
}
|
||||
},
|
||||
bindShowCardListeners: function(topic) {
|
||||
var self = TopicCard
|
||||
var showCard = document.getElementById('showcard')
|
||||
|
||||
var authorized = self.authorizedToEdit
|
||||
|
||||
// get mapper image
|
||||
var setMapperImage = function(mapper) {
|
||||
$('.contributorIcon').attr('src', mapper.get('image'))
|
||||
}
|
||||
Mapper.get(topic.get('user_id'), setMapperImage)
|
||||
|
||||
// starting embed.ly
|
||||
var resetFunc = function() {
|
||||
$('#addLinkInput input').val('')
|
||||
$('#addLinkInput input').focus()
|
||||
}
|
||||
var inputEmbedFunc = function(event) {
|
||||
var element = this
|
||||
setTimeout(function() {
|
||||
var text = $(element).val()
|
||||
if (event.type === 'paste' || (event.type === 'keyup' && event.which === 13)) {
|
||||
// TODO evaluate converting this to '//' no matter what (infer protocol)
|
||||
if (text.slice(0, 7) !== 'http://' &&
|
||||
text.slice(0, 8) !== 'https://' &&
|
||||
text.slice(0, 2) !== '//') {
|
||||
text = '//' + text
|
||||
}
|
||||
topic.save({
|
||||
link: text
|
||||
})
|
||||
var embedlyEl = $('<a/>', {
|
||||
id: 'embedlyLink',
|
||||
'data-card-description': '0',
|
||||
href: text
|
||||
}).html(text)
|
||||
$('.attachments').addClass('hidden')
|
||||
$('.embeds').append(embedlyEl)
|
||||
$('.embeds').append('<div id="embedlyLinkLoader"></div>')
|
||||
|
||||
self.showLinkLoader()
|
||||
self.showLink(topic)
|
||||
}
|
||||
}, 100)
|
||||
}
|
||||
$('#addLinkReset').click(resetFunc)
|
||||
$('#addLinkInput input').bind('paste keyup', inputEmbedFunc)
|
||||
|
||||
// initialize the link card, if there is a link
|
||||
if (topic.get('link') && topic.get('link') !== '') {
|
||||
self.showLinkLoader()
|
||||
self.showLink(topic)
|
||||
self.showLinkRemover()
|
||||
}
|
||||
|
||||
var selectingMetacode = false
|
||||
// attach the listener that shows the metacode title when you hover over the image
|
||||
$('.showcard .metacodeImage').mouseenter(function() {
|
||||
$('.showcard .icon').css('z-index', '4')
|
||||
$('.showcard .metacodeTitle').show()
|
||||
})
|
||||
$('.showcard .linkItem.icon').mouseleave(function() {
|
||||
if (!selectingMetacode) {
|
||||
$('.showcard .metacodeTitle').hide()
|
||||
$('.showcard .icon').css('z-index', '1')
|
||||
}
|
||||
})
|
||||
|
||||
var metacodeLiClick = function() {
|
||||
selectingMetacode = false
|
||||
var metacodeId = parseInt($(this).attr('data-id'))
|
||||
var metacode = DataModel.Metacodes.get(metacodeId)
|
||||
$('.CardOnGraph').find('.metacodeTitle').html(metacode.get('name'))
|
||||
.append('<div class="expandMetacodeSelect"></div>')
|
||||
.attr('class', 'metacodeTitle mbg' + metacode.id)
|
||||
$('.CardOnGraph').find('.metacodeImage').css('background-image', 'url(' + metacode.get('icon') + ')')
|
||||
topic.save({
|
||||
metacode_id: metacode.id
|
||||
})
|
||||
Visualize.mGraph.plot()
|
||||
$('.metacodeSelect').hide().removeClass('onRightEdge onBottomEdge')
|
||||
$('.metacodeTitle').hide()
|
||||
$('.showcard .icon').css('z-index', '1')
|
||||
}
|
||||
|
||||
var openMetacodeSelect = function(event) {
|
||||
var TOPICCARD_WIDTH = 300
|
||||
var METACODESELECT_WIDTH = 404
|
||||
var MAX_METACODELIST_HEIGHT = 270
|
||||
|
||||
if (!selectingMetacode) {
|
||||
selectingMetacode = true
|
||||
|
||||
// this is to make sure the metacode
|
||||
// select is accessible onscreen, when opened
|
||||
// while topic card is close to the right
|
||||
// edge of the screen
|
||||
var windowWidth = $(window).width()
|
||||
var showcardLeft = parseInt($('.showcard').css('left'))
|
||||
var distanceFromEdge = windowWidth - (showcardLeft + TOPICCARD_WIDTH)
|
||||
if (distanceFromEdge < METACODESELECT_WIDTH) {
|
||||
$('.metacodeSelect').addClass('onRightEdge')
|
||||
}
|
||||
|
||||
// this is to make sure the metacode
|
||||
// select is accessible onscreen, when opened
|
||||
// while topic card is close to the bottom
|
||||
// edge of the screen
|
||||
var windowHeight = $(window).height()
|
||||
var showcardTop = parseInt($('.showcard').css('top'))
|
||||
var topicTitleHeight = $('.showcard .title').height() + parseInt($('.showcard .title').css('padding-top')) + parseInt($('.showcard .title').css('padding-bottom'))
|
||||
var distanceFromBottom = windowHeight - (showcardTop + topicTitleHeight)
|
||||
if (distanceFromBottom < MAX_METACODELIST_HEIGHT) {
|
||||
$('.metacodeSelect').addClass('onBottomEdge')
|
||||
}
|
||||
|
||||
$('.metacodeSelect').show()
|
||||
event.stopPropagation()
|
||||
}
|
||||
}
|
||||
|
||||
var hideMetacodeSelect = function() {
|
||||
selectingMetacode = false
|
||||
$('.metacodeSelect').hide().removeClass('onRightEdge onBottomEdge')
|
||||
$('.metacodeTitle').hide()
|
||||
$('.showcard .icon').css('z-index', '1')
|
||||
}
|
||||
|
||||
if (authorized) {
|
||||
$('.showcard .metacodeTitle').click(openMetacodeSelect)
|
||||
$('.showcard').click(hideMetacodeSelect)
|
||||
$('.metacodeSelect > ul > li').click(function(event) {
|
||||
event.stopPropagation()
|
||||
})
|
||||
$('.metacodeSelect li li').click(metacodeLiClick)
|
||||
|
||||
var bipName = $(showCard).find('.best_in_place_name')
|
||||
bipName.bind('best_in_place:activate', function() {
|
||||
var $el = bipName.find('textarea')
|
||||
var el = $el[0]
|
||||
|
||||
$el.attr('maxlength', '140')
|
||||
|
||||
$('.showcard .title').append('<div class="nameCounter forTopic"></div>')
|
||||
|
||||
var callback = function(data) {
|
||||
$('.nameCounter.forTopic').html(data.all + '/140')
|
||||
}
|
||||
Countable.live(el, callback)
|
||||
})
|
||||
bipName.bind('best_in_place:deactivate', function() {
|
||||
$('.nameCounter.forTopic').remove()
|
||||
})
|
||||
bipName.keypress(function(e) {
|
||||
const ENTER = 13
|
||||
if (e.which === ENTER) { // enter
|
||||
$(this).data('bestInPlaceEditor').update()
|
||||
}
|
||||
})
|
||||
|
||||
// bind best_in_place ajax callbacks
|
||||
bipName.bind('ajax:success', function() {
|
||||
var name = Util.decodeEntities($(this).html())
|
||||
topic.set('name', name)
|
||||
topic.trigger('saved')
|
||||
})
|
||||
|
||||
// this is for all subsequent renders after in-place editing the desc field
|
||||
const bipDesc = $(showCard).find('.best_in_place_desc')
|
||||
bipDesc.bind('ajax:success', function() {
|
||||
var desc = $(this).html() === $(this).data('bip-nil')
|
||||
? ''
|
||||
: $(this).text()
|
||||
topic.set('desc', desc)
|
||||
$(this).data('bip-value', desc)
|
||||
this.innerHTML = Util.mdToHTML(desc)
|
||||
topic.trigger('saved')
|
||||
})
|
||||
bipDesc.keypress(function(e) {
|
||||
// allow typing Enter with Shift+Enter
|
||||
const ENTER = 13
|
||||
if (e.shiftKey === false && e.which === ENTER) {
|
||||
$(this).data('bestInPlaceEditor').update()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
var permissionLiClick = function(event) {
|
||||
selectingPermission = false
|
||||
var permission = $(this).attr('class')
|
||||
topic.save({
|
||||
permission: permission,
|
||||
defer_to_map_id: null
|
||||
})
|
||||
$('.showcard .mapPerm').removeClass('co pu pr minimize').addClass(permission.substring(0, 2))
|
||||
$('.showcard .permissionSelect').remove()
|
||||
event.stopPropagation()
|
||||
}
|
||||
|
||||
var openPermissionSelect = function(event) {
|
||||
if (!selectingPermission) {
|
||||
selectingPermission = true
|
||||
$(this).addClass('minimize') // this line flips the drop down arrow to a pull up arrow
|
||||
if ($(this).hasClass('co')) {
|
||||
$(this).append('<ul class="permissionSelect"><li class="public"></li><li class="private"></li></ul>')
|
||||
} else if ($(this).hasClass('pu')) {
|
||||
$(this).append('<ul class="permissionSelect"><li class="commons"></li><li class="private"></li></ul>')
|
||||
} else if ($(this).hasClass('pr')) {
|
||||
$(this).append('<ul class="permissionSelect"><li class="commons"></li><li class="public"></li></ul>')
|
||||
}
|
||||
$('.showcard .permissionSelect li').click(permissionLiClick)
|
||||
event.stopPropagation()
|
||||
}
|
||||
}
|
||||
|
||||
var hidePermissionSelect = function() {
|
||||
selectingPermission = false
|
||||
$('.showcard .yourTopic .mapPerm').removeClass('minimize') // this line flips the pull up arrow to a drop down arrow
|
||||
$('.showcard .permissionSelect').remove()
|
||||
}
|
||||
// ability to change permission
|
||||
var selectingPermission = false
|
||||
if (topic.authorizePermissionChange(Active.Mapper)) {
|
||||
$('.showcard .yourTopic .mapPerm').click(openPermissionSelect)
|
||||
$('.showcard').click(hidePermissionSelect)
|
||||
}
|
||||
|
||||
$('.links .mapCount').unbind().click(function(event) {
|
||||
$('.mapCount .tip').toggle()
|
||||
$('.showcard .hoverTip').toggleClass('hide')
|
||||
event.stopPropagation()
|
||||
})
|
||||
$('.mapCount .tip').unbind().click(function(event) {
|
||||
event.stopPropagation()
|
||||
})
|
||||
$('.showcard').unbind('.hideTip').bind('click.hideTip', function() {
|
||||
$('.mapCount .tip').hide()
|
||||
$('.showcard .hoverTip').removeClass('hide')
|
||||
})
|
||||
|
||||
$('.mapCount .tip li a').click(Router.intercept)
|
||||
|
||||
var originalText = $('.showMore').html()
|
||||
$('.mapCount .tip .showMore').unbind().toggle(
|
||||
function(event) {
|
||||
$('.extraText').toggleClass('hideExtra')
|
||||
$('.showMore').html('Show less...')
|
||||
},
|
||||
function(event) {
|
||||
$('.extraText').toggleClass('hideExtra')
|
||||
$('.showMore').html(originalText)
|
||||
})
|
||||
|
||||
$('.mapCount .tip showMore').unbind().click(function(event) {
|
||||
event.stopPropagation()
|
||||
})
|
||||
},
|
||||
handleInvalidLink: function() {
|
||||
var self = TopicCard
|
||||
|
||||
self.removeLink()
|
||||
GlobalUI.notifyUser('Invalid link')
|
||||
},
|
||||
populateShowCard: function(topic) {
|
||||
var self = TopicCard
|
||||
|
||||
var showCard = document.getElementById('showcard')
|
||||
|
||||
$(showCard).find('.permission').remove()
|
||||
|
||||
var topicForTemplate = self.buildObject(topic)
|
||||
var html = self.generateShowcardHTML.render(topicForTemplate)
|
||||
|
||||
if (topic.authorizeToEdit(Active.Mapper)) {
|
||||
let perm = document.createElement('div')
|
||||
|
||||
var string = 'permission canEdit'
|
||||
if (topic.authorizePermissionChange(Active.Mapper)) string += ' yourTopic'
|
||||
perm.className = string
|
||||
perm.innerHTML = html
|
||||
showCard.appendChild(perm)
|
||||
} else {
|
||||
let perm = document.createElement('div')
|
||||
perm.className = 'permission cannotEdit'
|
||||
perm.innerHTML = html
|
||||
showCard.appendChild(perm)
|
||||
}
|
||||
|
||||
TopicCard.bindShowCardListeners(topic)
|
||||
},
|
||||
generateShowcardHTML: null, // will be initialized into a Hogan template within init function
|
||||
// generateShowcardHTML
|
||||
buildObject: function(topic) {
|
||||
var nodeValues = {}
|
||||
|
||||
var authorized = topic.authorizeToEdit(Active.Mapper)
|
||||
|
||||
if (!authorized) {
|
||||
} else {
|
||||
}
|
||||
|
||||
nodeValues.attachmentsHidden = ''
|
||||
if (topic.get('link') && topic.get('link') !== '') {
|
||||
nodeValues.embeds = '<a href="' + topic.get('link') + '" id="embedlyLink" target="_blank" data-card-description="0">'
|
||||
nodeValues.embeds += topic.get('link')
|
||||
nodeValues.embeds += '</a><div id="embedlyLinkLoader"></div>'
|
||||
nodeValues.attachmentsHidden = 'hidden'
|
||||
nodeValues.hasAttachment = 'hasAttachment'
|
||||
} else {
|
||||
nodeValues.embeds = ''
|
||||
nodeValues.hasAttachment = ''
|
||||
}
|
||||
|
||||
if (authorized) {
|
||||
nodeValues.attachments = '<div class="addLink"><div id="addLinkIcon"></div>'
|
||||
nodeValues.attachments += '<div id="addLinkInput"><input placeholder="Enter or paste a link"></input>'
|
||||
nodeValues.attachments += '<div id="addLinkReset"></div></div></div>'
|
||||
} else {
|
||||
nodeValues.attachmentsHidden = 'hidden'
|
||||
nodeValues.attachments = ''
|
||||
}
|
||||
|
||||
var inmapsAr = topic.get('inmaps') || []
|
||||
var inmapsLinks = topic.get('inmapsLinks') || []
|
||||
nodeValues.inmaps = ''
|
||||
if (inmapsAr.length < 6) {
|
||||
for (let i = 0; i < inmapsAr.length; i++) {
|
||||
const url = '/maps/' + inmapsLinks[i]
|
||||
nodeValues.inmaps += '<li><a href="' + url + '">' + inmapsAr[i] + '</a></li>'
|
||||
}
|
||||
} else {
|
||||
for (let i = 0; i < 5; i++) {
|
||||
const url = '/maps/' + inmapsLinks[i]
|
||||
nodeValues.inmaps += '<li><a href="' + url + '">' + inmapsAr[i] + '</a></li>'
|
||||
}
|
||||
const extra = inmapsAr.length - 5
|
||||
nodeValues.inmaps += '<li><span class="showMore">See ' + extra + ' more...</span></li>'
|
||||
for (let i = 5; i < inmapsAr.length; i++) {
|
||||
const url = '/maps/' + inmapsLinks[i]
|
||||
nodeValues.inmaps += '<li class="hideExtra extraText"><a href="' + url + '">' + inmapsAr[i] + '</a></li>'
|
||||
}
|
||||
}
|
||||
nodeValues.permission = topic.get('permission')
|
||||
nodeValues.mk_permission = topic.get('permission').substring(0, 2)
|
||||
nodeValues.map_count = topic.get('map_count').toString()
|
||||
nodeValues.synapse_count = topic.get('synapse_count').toString()
|
||||
nodeValues.id = topic.isNew() ? topic.cid : topic.id
|
||||
nodeValues.metacode = topic.getMetacode().get('name')
|
||||
nodeValues.metacode_class = 'mbg' + topic.get('metacode_id')
|
||||
nodeValues.imgsrc = topic.getMetacode().get('icon')
|
||||
nodeValues.name = topic.get('name')
|
||||
nodeValues.userid = topic.get('user_id')
|
||||
nodeValues.username = topic.get('user_name')
|
||||
nodeValues.date = topic.getDate()
|
||||
// the code for this is stored in /views/main/_metacodeOptions.html.erb
|
||||
nodeValues.metacode_select = $('#metacodeOptions').html()
|
||||
nodeValues.desc_nil = 'Click to add description...'
|
||||
nodeValues.desc_markdown = (topic.get('desc') === '' && authorized)
|
||||
? nodeValues.desc_nil
|
||||
: topic.get('desc')
|
||||
nodeValues.desc_html = Util.mdToHTML(nodeValues.desc_markdown)
|
||||
return nodeValues
|
||||
}
|
||||
}
|
||||
|
||||
export default TopicCard
|
|
@ -1,6 +1,6 @@
|
|||
/* global $ */
|
||||
|
||||
import { Parser, HtmlRenderer } from 'commonmark'
|
||||
import { Parser, HtmlRenderer, Node } from 'commonmark'
|
||||
import { emojiIndex } from 'emoji-mart'
|
||||
import { escapeRegExp } from 'lodash'
|
||||
|
||||
|
@ -135,9 +135,26 @@ const Util = {
|
|||
},
|
||||
mdToHTML: text => {
|
||||
const safeText = text || ''
|
||||
const parsed = new Parser().parse(safeText)
|
||||
|
||||
// remove images to avoid http content in https context
|
||||
const walker = parsed.walker()
|
||||
for (let event = walker.next(); event = walker.next(); event) {
|
||||
const node = event.node
|
||||
if (node.type === 'image') {
|
||||
const imageAlt = node.firstChild.literal
|
||||
const imageSrc = node.destination
|
||||
const textNode = new Node('text', node.sourcepos)
|
||||
textNode.literal = `![${imageAlt}](${imageSrc})`
|
||||
|
||||
node.insertBefore(textNode)
|
||||
node.unlink() // remove the image, replacing it with markdown
|
||||
walker.resumeAt(textNode, false)
|
||||
}
|
||||
}
|
||||
|
||||
// use safe: true to filter xss
|
||||
return new HtmlRenderer({ safe: true })
|
||||
.render(new Parser().parse(safeText))
|
||||
return new HtmlRenderer({ safe: true }).render(parsed)
|
||||
},
|
||||
logCanvasAttributes: function(canvas) {
|
||||
const fakeMgraph = { canvas }
|
||||
|
@ -181,6 +198,37 @@ const Util = {
|
|||
})
|
||||
}
|
||||
return text
|
||||
},
|
||||
isTester: function(currentUser) {
|
||||
return ['connorturland@gmail.com', 'devin@callysto.com', 'chessscholar@gmail.com', 'solaureum@gmail.com', 'ishanshapiro@gmail.com'].indexOf(currentUser.get('email')) > -1
|
||||
},
|
||||
zoomOnPoint: function(graph, ans, zoomPoint) {
|
||||
var s = graph.canvas.getSize(),
|
||||
p = graph.canvas.getPos(),
|
||||
ox = graph.canvas.translateOffsetX,
|
||||
oy = graph.canvas.translateOffsetY,
|
||||
sx = graph.canvas.scaleOffsetX,
|
||||
sy = graph.canvas.scaleOffsetY;
|
||||
|
||||
var pointerCoordX = (zoomPoint.x - p.x - s.width / 2 - ox) * (1 / sx),
|
||||
pointerCoordY = (zoomPoint.y - p.y - s.height / 2 - oy) * (1 / sy);
|
||||
|
||||
//This translates the canvas to be centred over the zoomPoint, then the canvas is zoomed as intended.
|
||||
graph.canvas.translate(-pointerCoordX,-pointerCoordY);
|
||||
graph.canvas.scale(ans, ans);
|
||||
|
||||
//Get the canvas attributes again now that is has changed
|
||||
s = graph.canvas.getSize(),
|
||||
p = graph.canvas.getPos(),
|
||||
ox = graph.canvas.translateOffsetX,
|
||||
oy = graph.canvas.translateOffsetY,
|
||||
sx = graph.canvas.scaleOffsetX,
|
||||
sy = graph.canvas.scaleOffsetY;
|
||||
var newX = (zoomPoint.x - p.x - s.width / 2 - ox) * (1 / sx),
|
||||
newY = (zoomPoint.y - p.y - s.height / 2 - oy) * (1 / sy);
|
||||
|
||||
//Translate the canvas to put the pointer back over top the same coordinate it was over before
|
||||
graph.canvas.translate(newX-pointerCoordX,newY-pointerCoordY);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -53,6 +53,20 @@ const ExploreMaps = {
|
|||
url: `/maps/${map.id}/access_request`
|
||||
})
|
||||
GlobalUI.notifyUser('You will be notified by email if request accepted')
|
||||
},
|
||||
onFollow: function(map) {
|
||||
const isFollowing = map.isFollowedBy(Active.Mapper)
|
||||
$.post({
|
||||
url: `/maps/${map.id}/${isFollowing ? 'un' : ''}follow`
|
||||
})
|
||||
if (isFollowing) {
|
||||
GlobalUI.notifyUser('You are no longer following this map')
|
||||
Active.Mapper.unfollowMap(map.id)
|
||||
} else {
|
||||
GlobalUI.notifyUser('You are now following this map')
|
||||
Active.Mapper.followMap(map.id)
|
||||
}
|
||||
self.render()
|
||||
}
|
||||
}
|
||||
ReactDOM.render(
|
||||
|
|
74
frontend/src/Metamaps/Views/TopicCard.js
Normal file
74
frontend/src/Metamaps/Views/TopicCard.js
Normal file
|
@ -0,0 +1,74 @@
|
|||
/* global $ */
|
||||
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
|
||||
import Active from '../Active'
|
||||
import Visualize from '../Visualize'
|
||||
import GlobalUI from '../GlobalUI'
|
||||
|
||||
import ReactTopicCard from '../../components/TopicCard'
|
||||
|
||||
const TopicCard = {
|
||||
openTopicCard: null, // stores the topic that's currently open
|
||||
metacodeSets: [],
|
||||
init: function(serverData) {
|
||||
const self = TopicCard
|
||||
self.metacodeSets = serverData.metacodeSets
|
||||
},
|
||||
populateShowCard: function(topic) {
|
||||
const self = TopicCard
|
||||
ReactDOM.render(
|
||||
React.createElement(ReactTopicCard, {
|
||||
topic: topic,
|
||||
ActiveMapper: Active.Mapper,
|
||||
updateTopic: obj => {
|
||||
topic.save(obj, { success: topic => self.populateShowCard(topic) })
|
||||
},
|
||||
onFollow: () => {
|
||||
const isFollowing = topic.isFollowedBy(Active.Mapper)
|
||||
$.post({
|
||||
url: `/topics/${topic.id}/${isFollowing ? 'un' : ''}follow`
|
||||
})
|
||||
if (isFollowing) {
|
||||
GlobalUI.notifyUser('You are no longer following this topic')
|
||||
Active.Mapper.unfollowTopic(topic.id)
|
||||
} else {
|
||||
GlobalUI.notifyUser('You are now following this topic')
|
||||
Active.Mapper.followTopic(topic.id)
|
||||
}
|
||||
self.populateShowCard(topic)
|
||||
},
|
||||
metacodeSets: self.metacodeSets,
|
||||
redrawCanvas: () => {
|
||||
Visualize.mGraph.plot()
|
||||
}
|
||||
}),
|
||||
document.getElementById('showcard')
|
||||
)
|
||||
|
||||
// initialize draggability
|
||||
$('.showcard').draggable({
|
||||
handle: '.metacodeImage',
|
||||
stop: function() {
|
||||
$(this).height('auto')
|
||||
}
|
||||
})
|
||||
},
|
||||
showCard: function(node, opts) {
|
||||
var self = TopicCard
|
||||
if (!opts) opts = {}
|
||||
var topic = node.getData('topic')
|
||||
self.openTopicCard = topic
|
||||
// populate the card that's about to show with the right topics data
|
||||
self.populateShowCard(topic)
|
||||
return $('.showcard').fadeIn('fast', () => opts.complete && opts.complete())
|
||||
},
|
||||
hideCard: function() {
|
||||
var self = TopicCard
|
||||
$('.showcard').fadeOut('fast')
|
||||
self.openTopicCard = null
|
||||
}
|
||||
}
|
||||
|
||||
export default TopicCard
|
|
@ -4,18 +4,21 @@ import ExploreMaps from './ExploreMaps'
|
|||
import ChatView from './ChatView'
|
||||
import VideoView from './VideoView'
|
||||
import Room from './Room'
|
||||
import TopicCard from './TopicCard'
|
||||
import { JUNTO_UPDATED } from '../Realtime/events'
|
||||
|
||||
const Views = {
|
||||
init: (serverData) => {
|
||||
$(document).on(JUNTO_UPDATED, () => ExploreMaps.render())
|
||||
ChatView.init([serverData['sounds/MM_sounds.mp3'], serverData['sounds/MM_sounds.ogg']])
|
||||
TopicCard.init(serverData)
|
||||
},
|
||||
ExploreMaps,
|
||||
ChatView,
|
||||
VideoView,
|
||||
Room
|
||||
Room,
|
||||
TopicCard
|
||||
}
|
||||
|
||||
export { ExploreMaps, ChatView, VideoView, Room }
|
||||
export { ExploreMaps, ChatView, VideoView, Room, TopicCard }
|
||||
export default Views
|
||||
|
|
|
@ -9,7 +9,7 @@ import DataModel from './DataModel'
|
|||
import JIT from './JIT'
|
||||
import Loading from './Loading'
|
||||
import Router from './Router'
|
||||
import TopicCard from './TopicCard'
|
||||
import TopicCard from './Views/TopicCard'
|
||||
|
||||
const Visualize = {
|
||||
mGraph: null, // a reference to the graph object.
|
||||
|
|
|
@ -29,7 +29,6 @@ import Settings from './Settings'
|
|||
import Synapse from './Synapse'
|
||||
import SynapseCard from './SynapseCard'
|
||||
import Topic from './Topic'
|
||||
import TopicCard from './TopicCard'
|
||||
import Util from './Util'
|
||||
import Views from './Views'
|
||||
import Visualize from './Visualize'
|
||||
|
@ -71,7 +70,6 @@ Metamaps.Settings = Settings
|
|||
Metamaps.Synapse = Synapse
|
||||
Metamaps.SynapseCard = SynapseCard
|
||||
Metamaps.Topic = Topic
|
||||
Metamaps.TopicCard = TopicCard
|
||||
Metamaps.Util = Util
|
||||
Metamaps.Views = Views
|
||||
Metamaps.Visualize = Visualize
|
||||
|
|
|
@ -2,18 +2,8 @@ import React, { PropTypes, Component } from 'react'
|
|||
import Dropzone from 'react-dropzone'
|
||||
|
||||
class ImportDialogBox extends Component {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
}
|
||||
}
|
||||
|
||||
handleExport = format => () => {
|
||||
window.open(`${window.location.pathname}/export.${format}`, '_blank')
|
||||
}
|
||||
|
||||
handleFile = (files, e) => {
|
||||
e.preventDefault() // prevent it from triggering the default drag-drop handler
|
||||
this.props.onFileAdded(files[0])
|
||||
}
|
||||
|
||||
|
@ -21,13 +11,13 @@ class ImportDialogBox extends Component {
|
|||
return (
|
||||
<div className="import-dialog">
|
||||
<h3>EXPORT</h3>
|
||||
<div className="import-blue-button" onClick={this.handleExport('csv')}>
|
||||
<div className="export-csv import-blue-button" onClick={this.props.onExport('csv')}>
|
||||
Export as CSV
|
||||
</div>
|
||||
<div className="import-blue-button" onClick={this.handleExport('json')}>
|
||||
<div className="export-json import-blue-button" onClick={this.props.onExport('json')}>
|
||||
Export as JSON
|
||||
</div>
|
||||
<div className="import-blue-button" onClick={this.props.downloadScreenshot}>
|
||||
<div className="download-screenshot import-blue-button" onClick={this.props.downloadScreenshot}>
|
||||
Download screenshot
|
||||
</div>
|
||||
<h3>IMPORT</h3>
|
||||
|
@ -45,8 +35,8 @@ class ImportDialogBox extends Component {
|
|||
|
||||
ImportDialogBox.propTypes = {
|
||||
onFileAdded: PropTypes.func,
|
||||
exampleImageUrl: PropTypes.string,
|
||||
downloadScreenshot: PropTypes.func
|
||||
downloadScreenshot: PropTypes.func,
|
||||
onExport: PropTypes.func
|
||||
}
|
||||
|
||||
export default ImportDialogBox
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import React, { Component, PropTypes } from 'react'
|
||||
import { find, values } from 'lodash'
|
||||
import Util from '../../Metamaps/Util'
|
||||
|
||||
const IN_CONVERSATION = 1 // shared with /realtime/reducer.js
|
||||
|
||||
|
@ -23,7 +24,8 @@ class Menu extends Component {
|
|||
}
|
||||
|
||||
render = () => {
|
||||
const { currentUser, map, onStar, onRequest } = this.props
|
||||
const { currentUser, map, onStar, onRequest, onFollow } = this.props
|
||||
const isFollowing = map.isFollowedBy(currentUser)
|
||||
const style = { display: this.state.open ? 'block' : 'none' }
|
||||
|
||||
return <div className='dropdownMenu'>
|
||||
|
@ -35,6 +37,7 @@ class Menu extends Component {
|
|||
<ul className='menuItems' style={ style }>
|
||||
<li className='star' onClick={ () => { this.toggle() && onStar(map) }}>Star Map</li>
|
||||
{ !map.authorizeToEdit(currentUser) && <li className='request' onClick={ () => { this.toggle() && onRequest(map) }}>Request Access</li> }
|
||||
{ Util.isTester(currentUser) && <li className='follow' onClick={ () => { this.toggle() && onFollow(map) }}>{isFollowing ? 'Unfollow' : 'Follow'}</li> }
|
||||
</ul>
|
||||
</div>
|
||||
}
|
||||
|
@ -43,7 +46,8 @@ Menu.propTypes = {
|
|||
currentUser: PropTypes.object.isRequired,
|
||||
map: PropTypes.object.isRequired,
|
||||
onStar: PropTypes.func.isRequired,
|
||||
onRequest: PropTypes.func.isRequired
|
||||
onRequest: PropTypes.func.isRequired,
|
||||
onFollow: PropTypes.func.isRequired
|
||||
}
|
||||
|
||||
const Metadata = (props) => {
|
||||
|
@ -80,7 +84,7 @@ const checkAndWrapInA = (shouldWrap, classString, mapId, element) => {
|
|||
|
||||
class MapCard extends Component {
|
||||
render = () => {
|
||||
const { map, mobile, juntoState, currentUser, onRequest, onStar } = this.props
|
||||
const { map, mobile, juntoState, currentUser, onRequest, onStar, onFollow } = this.props
|
||||
|
||||
const hasMap = (juntoState.liveMaps[map.id] && values(juntoState.liveMaps[map.id]).length) || null
|
||||
const realtimeMap = juntoState.liveMaps[map.id]
|
||||
|
@ -131,7 +135,7 @@ class MapCard extends Component {
|
|||
</div>) }
|
||||
{ !mobile && hasMapper && <div className='mapHasMapper'><MapperList mappers={ mapperList } /></div> }
|
||||
{ !mobile && hasConversation && <div className='mapHasConversation'><MapperList mappers={ mapperList } /></div> }
|
||||
{ !mobile && currentUser && <Menu currentUser={ currentUser } map={ map } onStar= { onStar } onRequest={ onRequest } /> }
|
||||
{ !mobile && currentUser && <Menu currentUser={ currentUser } map={ map } onStar= { onStar } onRequest={ onRequest } onFollow={ onFollow } /> }
|
||||
</div>
|
||||
</div>) }
|
||||
</div>
|
||||
|
@ -145,7 +149,8 @@ MapCard.propTypes = {
|
|||
juntoState: PropTypes.object,
|
||||
currentUser: PropTypes.object,
|
||||
onStar: PropTypes.func.isRequired,
|
||||
onRequest: PropTypes.func.isRequired
|
||||
onRequest: PropTypes.func.isRequired,
|
||||
onFollow: PropTypes.func.isRequired
|
||||
}
|
||||
|
||||
export default MapCard
|
||||
|
|
|
@ -46,7 +46,7 @@ class Maps extends Component {
|
|||
}
|
||||
|
||||
render = () => {
|
||||
const { maps, currentUser, juntoState, pending, section, user, onStar, onRequest } = this.props
|
||||
const { maps, currentUser, juntoState, pending, section, user, onStar, onRequest, onFollow } = this.props
|
||||
const style = { width: this.state.mapsWidth + 'px' }
|
||||
const mobile = document && document.body.clientWidth <= MOBILE_VIEW_BREAKPOINT
|
||||
|
||||
|
@ -56,7 +56,7 @@ class Maps extends Component {
|
|||
<div style={ style }>
|
||||
{ user ? <MapperCard user={ user } /> : null }
|
||||
{ currentUser && !user && !(pending && maps.length === 0) ? <div className="map newMap"><a href="/maps/new"><div className="newMapImage"></div><span>Create new map...</span></a></div> : null }
|
||||
{ maps.models.map(map => <MapCard key={ map.id } map={ map } mobile={ mobile } juntoState={ juntoState } currentUser={ currentUser } onStar={ onStar } onRequest={ onRequest } />) }
|
||||
{ maps.models.map(map => <MapCard key={ map.id } map={ map } mobile={ mobile } juntoState={ juntoState } currentUser={ currentUser } onStar={ onStar } onRequest={ onRequest } onFollow={ onFollow } />) }
|
||||
<div className='clearfloat'></div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -79,7 +79,8 @@ Maps.propTypes = {
|
|||
loadMore: PropTypes.func,
|
||||
pending: PropTypes.bool.isRequired,
|
||||
onStar: PropTypes.func.isRequired,
|
||||
onRequest: PropTypes.func.isRequired
|
||||
onRequest: PropTypes.func.isRequired,
|
||||
onFollow: PropTypes.func.isRequired
|
||||
}
|
||||
|
||||
export default Maps
|
||||
|
|
53
frontend/src/components/MetacodeSelect.js
Normal file
53
frontend/src/components/MetacodeSelect.js
Normal file
|
@ -0,0 +1,53 @@
|
|||
/* global $ */
|
||||
|
||||
/*
|
||||
* Metacode selector component
|
||||
*
|
||||
* This component takes in a callback (onMetacodeSelect; takes one metacode id)
|
||||
* and a list of metacode sets and renders them. If you click a metacode, it
|
||||
* passes that metacode's id to the callback.
|
||||
*/
|
||||
|
||||
import React, { PropTypes, Component } from 'react'
|
||||
|
||||
class MetacodeSelect extends Component {
|
||||
render = () => {
|
||||
return (
|
||||
<div id="metacodeOptions">
|
||||
<ul>
|
||||
{this.props.metacodeSets.map(set => (
|
||||
<li key={set.name}>
|
||||
<span>{set.name}</span>
|
||||
<div className="expandMetacodeSet"></div>
|
||||
<ul>
|
||||
{set.metacodes.map(m => (
|
||||
<li key={m.id}
|
||||
onClick={() => this.props.onMetacodeSelect(m.id)}
|
||||
>
|
||||
<img width="24" height="24" src={m.icon_path} alt={m.name} />
|
||||
<div className="mSelectName">{m.name}</div>
|
||||
<div className="clearfloat"></div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
MetacodeSelect.propTypes = {
|
||||
onMetacodeClick: PropTypes.func,
|
||||
metacodeSets: PropTypes.arrayOf(PropTypes.shape({
|
||||
name: PropTypes.string,
|
||||
metacodes: PropTypes.arrayOf(PropTypes.shape({
|
||||
id: PropTypes.number,
|
||||
icon_path: PropTypes.string, // url
|
||||
name: PropTypes.string
|
||||
}))
|
||||
}))
|
||||
}
|
||||
|
||||
export default MetacodeSelect
|
24
frontend/src/components/TopicCard/Attachments.js
Normal file
24
frontend/src/components/TopicCard/Attachments.js
Normal file
|
@ -0,0 +1,24 @@
|
|||
import React, { PropTypes, Component } from 'react'
|
||||
|
||||
import EmbedlyLink from './EmbedlyLink'
|
||||
|
||||
class Attachments extends Component {
|
||||
render = () => {
|
||||
const { topic, authorizedToEdit, updateTopic } = this.props
|
||||
const link = topic.get('link')
|
||||
|
||||
return (
|
||||
<div className="attachments">
|
||||
<EmbedlyLink link={link} authorizedToEdit={authorizedToEdit} updateTopic={updateTopic} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Attachments.propTypes = {
|
||||
topic: PropTypes.object, // Backbone object
|
||||
authorizedToEdit: PropTypes.bool,
|
||||
updateTopic: PropTypes.func
|
||||
}
|
||||
|
||||
export default Attachments
|
77
frontend/src/components/TopicCard/Desc.js
Normal file
77
frontend/src/components/TopicCard/Desc.js
Normal file
|
@ -0,0 +1,77 @@
|
|||
import React, { PropTypes, Component } from 'react'
|
||||
import { RIETextArea } from 'riek'
|
||||
import Util from '../../Metamaps/Util'
|
||||
|
||||
class MdTextArea extends RIETextArea {
|
||||
keyDown = (event) => {
|
||||
// we'll handle Enter on our own, thanks
|
||||
const ESC = 27
|
||||
if (event.keyCode === ESC) {
|
||||
this.cancelEditing()
|
||||
}
|
||||
}
|
||||
|
||||
renderNormalComponent = () => {
|
||||
// defaultProps MUST use dangerouslySetInnerHTML
|
||||
return <span tabIndex="0"
|
||||
className={this.makeClassString()}
|
||||
onFocus={this.startEditing}
|
||||
onClick={this.startEditing}
|
||||
{...this.props.defaultProps}
|
||||
/>
|
||||
}
|
||||
}
|
||||
|
||||
class Desc extends Component {
|
||||
render = () => {
|
||||
const descHTML = (!this.props.desc && this.props.authorizedToEdit)
|
||||
? '<p>Click to add description...</p>'
|
||||
: Util.mdToHTML(this.props.desc)
|
||||
|
||||
if (this.props.authorizedToEdit) {
|
||||
return (
|
||||
<div className="scroll">
|
||||
<div className="desc">
|
||||
<MdTextArea value={this.props.desc}
|
||||
propName="desc"
|
||||
change={this.props.onChange}
|
||||
className="riek_desc"
|
||||
classEditing="riek-editing"
|
||||
editProps={{
|
||||
onKeyPress: e => {
|
||||
const ENTER = 13
|
||||
if (!e.shiftKey && e.which === ENTER) {
|
||||
e.preventDefault()
|
||||
this.props.onChange({ desc: e.target.value })
|
||||
}
|
||||
}
|
||||
}}
|
||||
defaultProps={{
|
||||
dangerouslySetInnerHTML: { __html: descHTML }
|
||||
}}
|
||||
/>
|
||||
<div className="clearfloat"></div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
} else {
|
||||
return (
|
||||
<div className="scroll">
|
||||
<div className="desc">
|
||||
<span className="riek_desc">
|
||||
{this.props.desc}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Desc.propTypes = {
|
||||
desc: PropTypes.string, // markdown
|
||||
authorizedToEdit: PropTypes.bool,
|
||||
onChange: PropTypes.func
|
||||
}
|
||||
|
||||
export default Desc
|
65
frontend/src/components/TopicCard/EmbedlyLink/Card.js
Normal file
65
frontend/src/components/TopicCard/EmbedlyLink/Card.js
Normal file
|
@ -0,0 +1,65 @@
|
|||
/* global $, embedly */
|
||||
import React, { PropTypes, Component } from 'react'
|
||||
|
||||
class EmbedlyCard extends Component {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
embedlyLinkStarted: false,
|
||||
embedlyLinkLoaded: false,
|
||||
embedlyLinkError: false
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount = () => {
|
||||
embedly('on', 'card.rendered', this.embedlyCardRendered)
|
||||
if (this.props.link) this.loadLink()
|
||||
}
|
||||
|
||||
componentWillUnmount = () => {
|
||||
embedly('off')
|
||||
}
|
||||
|
||||
componentDidUpdate = () => {
|
||||
const { embedlyLinkStarted } = this.state
|
||||
!embedlyLinkStarted && this.props.link && this.loadLink()
|
||||
}
|
||||
|
||||
embedlyCardRendered = (iframe, test) => {
|
||||
this.setState({embedlyLinkLoaded: true, embedlyLinkError: false})
|
||||
}
|
||||
|
||||
loadLink = () => {
|
||||
this.setState({ embedlyLinkStarted: true })
|
||||
var e = embedly('card', document.getElementById('embedlyLink'))
|
||||
if (e && e.type === 'error') this.setState({embedlyLinkError: true})
|
||||
}
|
||||
|
||||
render = () => {
|
||||
const { link } = this.props
|
||||
const { embedlyLinkLoaded, embedlyLinkStarted, embedlyLinkError } = this.state
|
||||
|
||||
const notReady = embedlyLinkStarted && !embedlyLinkLoaded && !embedlyLinkError
|
||||
|
||||
return (
|
||||
<div>
|
||||
<a style={{ display: notReady ? 'none' : 'block' }}
|
||||
href={link}
|
||||
id="embedlyLink"
|
||||
target="_blank"
|
||||
data-card-description="0"
|
||||
>
|
||||
{link}
|
||||
</a>
|
||||
{notReady && <div id="embedlyLinkLoader">loading...</div>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
EmbedlyCard.propTypes = {
|
||||
link: PropTypes.string
|
||||
}
|
||||
|
||||
export default EmbedlyCard
|
76
frontend/src/components/TopicCard/EmbedlyLink/index.js
Normal file
76
frontend/src/components/TopicCard/EmbedlyLink/index.js
Normal file
|
@ -0,0 +1,76 @@
|
|||
/* global embedly */
|
||||
import React, { PropTypes, Component } from 'react'
|
||||
|
||||
import Card from './Card'
|
||||
|
||||
class EmbedlyLink extends Component {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
linkEdit: ''
|
||||
}
|
||||
}
|
||||
|
||||
removeLink = () => {
|
||||
this.props.updateTopic({ link: null })
|
||||
}
|
||||
|
||||
resetLink = () => {
|
||||
this.setState({ linkEdit: '' })
|
||||
}
|
||||
|
||||
onLinkChangeHandler = e => {
|
||||
this.setState({ linkEdit: e.target.value })
|
||||
}
|
||||
|
||||
onLinkKeyUpHandler = e => {
|
||||
const ENTER_KEY = 13
|
||||
if (e.which === ENTER_KEY) {
|
||||
const { linkEdit } = this.state
|
||||
this.setState({ linkEdit: '' })
|
||||
this.props.updateTopic({ link: linkEdit })
|
||||
}
|
||||
}
|
||||
|
||||
render = () => {
|
||||
const { link, authorizedToEdit } = this.props
|
||||
const { linkEdit } = this.state
|
||||
const hasAttachment = !!link
|
||||
|
||||
if (!hasAttachment && !authorizedToEdit) return null
|
||||
|
||||
return (
|
||||
<div className={hasAttachment ? 'embeds' : 'link-adder'}>
|
||||
<div className="addLink"
|
||||
style={{ display: hasAttachment ? 'none' : 'block' }}
|
||||
>
|
||||
<div id="addLinkIcon"></div>
|
||||
<div id="addLinkInput">
|
||||
<input ref={input => (this.linkInput = input)}
|
||||
placeholder="Enter or paste a link"
|
||||
value={linkEdit}
|
||||
onChange={this.onLinkChangeHandler}
|
||||
onKeyUp={this.onLinkKeyUpHandler}></input>
|
||||
{linkEdit && <div id="addLinkReset" onClick={this.resetLink}></div>}
|
||||
</div>
|
||||
</div>
|
||||
{link && <Card link={link} />}
|
||||
{authorizedToEdit && (
|
||||
<div id="linkremove"
|
||||
style={{ display: hasAttachment ? 'block' : 'none' }}
|
||||
onClick={this.removeLink}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
EmbedlyLink.propTypes = {
|
||||
link: PropTypes.string,
|
||||
authorizedToEdit: PropTypes.bool,
|
||||
updateTopic: PropTypes.func
|
||||
}
|
||||
|
||||
export default EmbedlyLink
|
17
frontend/src/components/TopicCard/Follow.js
Normal file
17
frontend/src/components/TopicCard/Follow.js
Normal file
|
@ -0,0 +1,17 @@
|
|||
import React, { PropTypes, Component } from 'react'
|
||||
|
||||
class Follow extends Component {
|
||||
render = () => {
|
||||
const { isFollowing, onFollow } = this.props
|
||||
return <div className='topicFollow' onClick={onFollow}>
|
||||
{isFollowing ? 'Unfollow' : 'Follow'}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
Follow.propTypes = {
|
||||
isFollowing: PropTypes.bool,
|
||||
onFollow: PropTypes.func
|
||||
}
|
||||
|
||||
export default Follow
|
161
frontend/src/components/TopicCard/Links.js
Normal file
161
frontend/src/components/TopicCard/Links.js
Normal file
|
@ -0,0 +1,161 @@
|
|||
/* global $ */
|
||||
|
||||
import React, { PropTypes, Component } from 'react'
|
||||
|
||||
import MetacodeSelect from '../MetacodeSelect'
|
||||
import Permission from './Permission'
|
||||
|
||||
class Links extends Component {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
showMetacodeTitle: false,
|
||||
showMetacodeSelect: false,
|
||||
showInMaps: false,
|
||||
showMoreMaps: false,
|
||||
hoveringMapCount: false,
|
||||
hoveringSynapseCount: false
|
||||
}
|
||||
}
|
||||
|
||||
handleMetacodeSelect = metacodeId => {
|
||||
this.setState({ showMetacodeSelect: false })
|
||||
this.props.updateTopic({
|
||||
metacode_id: metacodeId
|
||||
})
|
||||
this.props.redrawCanvas()
|
||||
}
|
||||
|
||||
toggleShowMoreMaps = e => {
|
||||
e.stopPropagation()
|
||||
e.preventDefault()
|
||||
this.setState({ showMoreMaps: !this.state.showMoreMaps })
|
||||
}
|
||||
|
||||
updateState = (key, value) => () => {
|
||||
this.setState({ [key]: value })
|
||||
}
|
||||
|
||||
inMaps = (topic) => {
|
||||
const inmapsArray = topic.get('inmaps') || []
|
||||
const inmapsLinks = topic.get('inmapsLinks') || []
|
||||
|
||||
let firstFiveLinks = []
|
||||
let extraLinks = []
|
||||
for (let i = 0; i < inmapsArray.length; i ++) {
|
||||
if (i < 5) {
|
||||
firstFiveLinks.push({ mapName: inmapsArray[i], mapId: inmapsLinks[i] })
|
||||
} else {
|
||||
extraLinks.push({ mapName: inmapsArray[i], mapId: inmapsLinks[i] })
|
||||
}
|
||||
}
|
||||
|
||||
let output = []
|
||||
|
||||
firstFiveLinks.forEach(obj => {
|
||||
output.push(<li key={obj.mapId}><a href={`/maps/${obj.mapId}`}>{obj.mapName}</a></li>)
|
||||
})
|
||||
|
||||
if (extraLinks.length > 0) {
|
||||
if (this.state.showMoreMaps) {
|
||||
extraLinks.forEach(obj => {
|
||||
output.push(<li key={obj.mapId} className="hideExtra extraText"><a href={`/maps/${obj.mapId}`}>{obj.mapName}</a></li>)
|
||||
})
|
||||
}
|
||||
const text = this.state.showMoreMaps ? 'See less...' : `See ${extraLinks.length} more...`
|
||||
output.push(<li key="showMore"><span className="showMore" onClick={this.toggleShowMoreMaps}>{text}</span></li>)
|
||||
}
|
||||
|
||||
return output
|
||||
}
|
||||
|
||||
handleMetacodeBarClick = () => {
|
||||
if (this.state.showMetacodeTitle) {
|
||||
this.setState({ showMetacodeSelect: !this.state.showMetacodeSelect })
|
||||
}
|
||||
}
|
||||
|
||||
render = () => {
|
||||
const { topic, ActiveMapper } = this.props
|
||||
const authorizedToEdit = topic.authorizeToEdit(ActiveMapper)
|
||||
const authorizedPermissionChange = topic.authorizePermissionChange(ActiveMapper)
|
||||
const metacode = topic.getMetacode()
|
||||
|
||||
return (
|
||||
<div className="links">
|
||||
<div className="linkItem icon metacodeItem"
|
||||
style={{ zIndex: this.state.showMetacodeTitle ? 4 : 1 }}
|
||||
onMouseLeave={() => this.setState({ showMetacodeTitle: false, showMetacodeSelect: false })}
|
||||
onClick={this.handleMetacodeBarClick}
|
||||
>
|
||||
<div className={`metacodeTitle mbg${metacode.get('id')}`}
|
||||
style={{ display: this.state.showMetacodeTitle ? 'block' : 'none' }}
|
||||
>
|
||||
{metacode.get('name')}
|
||||
<div className="expandMetacodeSelect"/>
|
||||
</div>
|
||||
<div className="metacodeImage"
|
||||
style={{backgroundImage: `url(${metacode.get('icon')})`}}
|
||||
title="click and drag to move card"
|
||||
onMouseEnter={() => this.setState({ showMetacodeTitle: true })}
|
||||
/>
|
||||
<div className="metacodeSelect"
|
||||
style={{ display: this.state.showMetacodeSelect ? 'block' : 'none' }}
|
||||
>
|
||||
<MetacodeSelect onMetacodeSelect={this.handleMetacodeSelect} metacodeSets={this.props.metacodeSets} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="linkItem contributor">
|
||||
<a href={`/explore/mapper/${topic.get('user_id')}`} target="_blank"><img src={topic.get('user_image')} className="contributorIcon" width="32" height="32" /></a>
|
||||
<div className="contributorName">{topic.get('user_name')}</div>
|
||||
</div>
|
||||
<div className="linkItem mapCount"
|
||||
onMouseOver={this.updateState('hoveringMapCount', true)}
|
||||
onMouseOut={this.updateState('hoveringMapCount', false)}
|
||||
onClick={this.updateState('showInMaps', !this.state.showInMaps)}
|
||||
>
|
||||
<div className="mapCountIcon"></div>
|
||||
{topic.get('map_count').toString()}
|
||||
{!this.state.showInMaps && this.state.hoveringMapCount && (
|
||||
<div className="hoverTip">Click to see which maps topic appears on</div>
|
||||
)}
|
||||
{this.state.showInMaps && <div className="tip"><ul>{this.inMaps(topic)}</ul></div>}
|
||||
</div>
|
||||
<a href={`/topics/${topic.id}`}
|
||||
target="_blank"
|
||||
className="linkItem synapseCount"
|
||||
onMouseOver={this.updateState('hoveringSynapseCount', true)}
|
||||
onMouseOut={this.updateState('hoveringSynapseCount', false)}
|
||||
>
|
||||
<div className="synapseCountIcon"></div>
|
||||
{topic.get('synapse_count').toString()}
|
||||
{this.state.hoveringSynapseCount && <div className="tip">Click to see this topics synapses</div>}
|
||||
</a>
|
||||
<Permission
|
||||
permission={topic.get('permission')}
|
||||
authorizedToEdit={authorizedPermissionChange}
|
||||
updateTopic={this.props.updateTopic}
|
||||
/>
|
||||
<div className="clearfloat"></div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Links.propTypes = {
|
||||
topic: PropTypes.object, // backbone object
|
||||
ActiveMapper: PropTypes.object,
|
||||
updateTopic: PropTypes.func,
|
||||
metacodeSets: PropTypes.arrayOf(PropTypes.shape({
|
||||
name: PropTypes.string,
|
||||
metacodes: PropTypes.arrayOf(PropTypes.shape({
|
||||
id: PropTypes.number,
|
||||
icon_path: PropTypes.string, // url
|
||||
name: PropTypes.string
|
||||
}))
|
||||
})),
|
||||
redrawCanvas: PropTypes.func
|
||||
}
|
||||
|
||||
export default Links
|
69
frontend/src/components/TopicCard/Permission.js
Normal file
69
frontend/src/components/TopicCard/Permission.js
Normal file
|
@ -0,0 +1,69 @@
|
|||
import React, { PropTypes, Component } from 'react'
|
||||
|
||||
import onClickOutsideAddon from 'react-onclickoutside'
|
||||
|
||||
class Permission extends Component {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.state = {
|
||||
selectingPermission: false
|
||||
}
|
||||
}
|
||||
|
||||
togglePermissionSelect = () => {
|
||||
this.setState({selectingPermission: !this.state.selectingPermission})
|
||||
}
|
||||
|
||||
openPermissionSelect = () => {
|
||||
this.setState({selectingPermission: true})
|
||||
}
|
||||
|
||||
closePermissionSelect = () => {
|
||||
this.setState({selectingPermission: false})
|
||||
}
|
||||
|
||||
handleClickOutside = instance => {
|
||||
this.closePermissionSelect()
|
||||
}
|
||||
|
||||
liClick = value => event => {
|
||||
this.closePermissionSelect()
|
||||
this.props.updateTopic({
|
||||
permission: value,
|
||||
defer_to_map_id: null
|
||||
})
|
||||
// prevents it from also firing the event listener on the parent
|
||||
event.preventDefault()
|
||||
}
|
||||
|
||||
render = () => {
|
||||
const { permission, authorizedToEdit } = this.props
|
||||
const { selectingPermission } = this.state
|
||||
|
||||
let classes = `linkItem mapPerm ${permission.substring(0, 2)}`
|
||||
if (selectingPermission) classes += ' minimize'
|
||||
|
||||
return (
|
||||
<div className={classes}
|
||||
title={permission}
|
||||
onClick={authorizedToEdit ? this.togglePermissionSelect : null}
|
||||
>
|
||||
<ul className="permissionSelect"
|
||||
style={{ display: selectingPermission ? 'block' : 'none' }}
|
||||
>
|
||||
{permission !== 'commons' && <li className='commons' onClick={this.liClick('commons')}></li>}
|
||||
{permission !== 'public' && <li className='public' onClick={this.liClick('public')}></li>}
|
||||
{permission !== 'private' && <li className='private' onClick={this.liClick('private')}></li>}
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Permission.propTypes = {
|
||||
permission: PropTypes.string, // 'co', 'pu', or 'pr'
|
||||
authorizedToEdit: PropTypes.bool,
|
||||
updateTopic: PropTypes.func
|
||||
}
|
||||
|
||||
export default onClickOutsideAddon(Permission)
|
62
frontend/src/components/TopicCard/Title.js
Normal file
62
frontend/src/components/TopicCard/Title.js
Normal file
|
@ -0,0 +1,62 @@
|
|||
import React, { Component, PropTypes } from 'react'
|
||||
import { RIETextArea } from 'riek'
|
||||
|
||||
const maxTitleLength = 140
|
||||
|
||||
class Title extends Component {
|
||||
nameCounterText() {
|
||||
// for some reason, there's an error if this isn't inside a function
|
||||
return `${this.props.name.length}/${maxTitleLength.toString()}`
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.props.authorizedToEdit) {
|
||||
return (
|
||||
<span className="title">
|
||||
<RIETextArea value={this.props.name}
|
||||
ref={textarea => { this.textarea = textarea }}
|
||||
propName="name"
|
||||
change={this.props.onChange}
|
||||
className="titleWrapper"
|
||||
id="titleActivator"
|
||||
classEditing="riek-editing"
|
||||
editProps={{
|
||||
maxLength: maxTitleLength,
|
||||
onKeyPress: e => {
|
||||
const ENTER = 13
|
||||
if (e.which === ENTER) {
|
||||
e.preventDefault()
|
||||
this.props.onChange({ name: e.target.value })
|
||||
}
|
||||
},
|
||||
onChange: e => {
|
||||
if (!this.nameCounter) return
|
||||
this.nameCounter.innerHTML = `${e.target.value.length}/140`
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<span className="nameCounter" ref={span => { this.nameCounter = span }}>
|
||||
{this.nameCounterText()}
|
||||
</span>
|
||||
</span>
|
||||
)
|
||||
} else {
|
||||
return (
|
||||
<span className="title">
|
||||
<span className="titleWrapper">
|
||||
{this.props.name}
|
||||
</span>
|
||||
</span>
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Title.propTypes = {
|
||||
name: PropTypes.string,
|
||||
onChange: PropTypes.func,
|
||||
authorizedToEdit: PropTypes.bool
|
||||
}
|
||||
|
||||
export default Title
|
71
frontend/src/components/TopicCard/index.js
Normal file
71
frontend/src/components/TopicCard/index.js
Normal file
|
@ -0,0 +1,71 @@
|
|||
import React, { PropTypes, Component } from 'react'
|
||||
|
||||
import Title from './Title'
|
||||
import Links from './Links'
|
||||
import Desc from './Desc'
|
||||
import Attachments from './Attachments'
|
||||
import Follow from './Follow'
|
||||
import Util from '../../Metamaps/Util'
|
||||
|
||||
|
||||
class ReactTopicCard extends Component {
|
||||
render = () => {
|
||||
const { topic, ActiveMapper, onFollow } = this.props
|
||||
const authorizedToEdit = topic.authorizeToEdit(ActiveMapper)
|
||||
const isFollowing = topic.isFollowedBy(ActiveMapper)
|
||||
const hasAttachment = topic.get('link') && topic.get('link') !== ''
|
||||
|
||||
let classname = 'permission'
|
||||
if (authorizedToEdit) {
|
||||
classname += ' canEdit'
|
||||
} else {
|
||||
classname += ' cannotEdit'
|
||||
}
|
||||
if (topic.authorizePermissionChange(ActiveMapper)) classname += ' yourTopic'
|
||||
|
||||
return (
|
||||
<div className={classname}>
|
||||
<div className={`CardOnGraph ${hasAttachment ? 'hasAttachment' : ''}`} id={`topic_${topic.id}`}>
|
||||
<Title name={topic.get('name')}
|
||||
authorizedToEdit={authorizedToEdit}
|
||||
onChange={this.props.updateTopic}
|
||||
/>
|
||||
<Links topic={topic}
|
||||
ActiveMapper={this.props.ActiveMapper}
|
||||
updateTopic={this.props.updateTopic}
|
||||
metacodeSets={this.props.metacodeSets}
|
||||
redrawCanvas={this.props.redrawCanvas}
|
||||
/>
|
||||
<Desc desc={topic.get('desc')}
|
||||
authorizedToEdit={authorizedToEdit}
|
||||
onChange={this.props.updateTopic}
|
||||
/>
|
||||
<Attachments topic={topic}
|
||||
authorizedToEdit={authorizedToEdit}
|
||||
updateTopic={this.props.updateTopic}
|
||||
/>
|
||||
{Util.isTester(ActiveMapper) && <Follow isFollowing={isFollowing} onFollow={onFollow} />}
|
||||
<div className="clearfloat"></div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
ReactTopicCard.propTypes = {
|
||||
topic: PropTypes.object,
|
||||
ActiveMapper: PropTypes.object,
|
||||
updateTopic: PropTypes.func,
|
||||
onFollow: PropTypes.func,
|
||||
metacodeSets: PropTypes.arrayOf(PropTypes.shape({
|
||||
name: PropTypes.string,
|
||||
metacodes: PropTypes.arrayOf(PropTypes.shape({
|
||||
id: PropTypes.number,
|
||||
icon_path: PropTypes.string, // url
|
||||
name: PropTypes.string
|
||||
}))
|
||||
})),
|
||||
redrawCanvas: PropTypes.func
|
||||
}
|
||||
|
||||
export default ReactTopicCard
|
|
@ -449,7 +449,7 @@ $.event = {
|
|||
isRightClick: function(e) {
|
||||
return (e.which == 3 || e.button == 2);
|
||||
},
|
||||
getPos: function(e, win) {
|
||||
getPos: function(e, win, touchIndex) {
|
||||
// get mouse position
|
||||
win = win || window;
|
||||
e = e || win.event;
|
||||
|
@ -457,7 +457,7 @@ $.event = {
|
|||
doc = doc.documentElement || doc.body;
|
||||
//TODO(nico): make touch event handling better
|
||||
if(e.touches && e.touches.length) {
|
||||
e = e.touches[0];
|
||||
e = e.touches[touchIndex || 0];
|
||||
}
|
||||
var page = {
|
||||
x: e.pageX || (e.clientX + doc.scrollLeft),
|
||||
|
@ -2469,33 +2469,7 @@ Extras.Classes.Navigation = new Class({
|
|||
|
||||
// START METAMAPS CODE
|
||||
if (((ans > 1) && (5 >= this.canvas.scaleOffsetX)) || ((ans < 1) && (this.canvas.scaleOffsetX >= 0.2))) {
|
||||
var s = this.canvas.getSize(),
|
||||
p = this.canvas.getPos(),
|
||||
ox = this.canvas.translateOffsetX,
|
||||
oy = this.canvas.translateOffsetY,
|
||||
sx = this.canvas.scaleOffsetX,
|
||||
sy = this.canvas.scaleOffsetY;
|
||||
|
||||
//Basically this is just a duplication of the Util function pixelsToCoords, it finds the canvas coordinate of the mouse pointer
|
||||
var pointerCoordX = (e.pageX - p.x - s.width / 2 - ox) * (1 / sx),
|
||||
pointerCoordY = (e.pageY - p.y - s.height / 2 - oy) * (1 / sy);
|
||||
|
||||
//This translates the canvas to be centred over the mouse pointer, then the canvas is zoomed as intended.
|
||||
this.canvas.translate(-pointerCoordX,-pointerCoordY);
|
||||
this.canvas.scale(ans, ans);
|
||||
|
||||
//Get the canvas attributes again now that is has changed
|
||||
s = this.canvas.getSize(),
|
||||
p = this.canvas.getPos(),
|
||||
ox = this.canvas.translateOffsetX,
|
||||
oy = this.canvas.translateOffsetY,
|
||||
sx = this.canvas.scaleOffsetX,
|
||||
sy = this.canvas.scaleOffsetY;
|
||||
var newX = (e.pageX - p.x - s.width / 2 - ox) * (1 / sx),
|
||||
newY = (e.pageY - p.y - s.height / 2 - oy) * (1 / sy);
|
||||
|
||||
//Translate the canvas to put the pointer back over top the same coordinate it was over before
|
||||
this.canvas.translate(newX-pointerCoordX,newY-pointerCoordY);
|
||||
Metamaps.Util.zoomOnPoint(this, ans, {x: e.pageX, y: e.pageY})
|
||||
}
|
||||
|
||||
// END METAMAPS CODE
|
||||
|
@ -2620,109 +2594,132 @@ Extras.Classes.Navigation = new Class({
|
|||
Metamaps.Mouse.changeInY = 0;
|
||||
if((this.config.panning == 'avoid nodes' && eventInfo.getNode()) || eventInfo.getEdge()) return;
|
||||
this.pressed = true;
|
||||
var rightClick = e.button == 2 || (navigator.platform.indexOf("Mac") != -1 && e.ctrlKey);
|
||||
if (!Metamaps.Mouse.boxStartCoordinates && ((e.button == 0 && e.shiftKey) || (e.button == 0 && e.ctrlKey) || rightClick)) {
|
||||
Metamaps.Mouse.boxStartCoordinates = eventInfo.getPos();
|
||||
}
|
||||
Metamaps.Mouse.didPan = false;
|
||||
this.pos = eventInfo.getPos();
|
||||
|
||||
var canvas = this.canvas,
|
||||
ox = canvas.translateOffsetX,
|
||||
oy = canvas.translateOffsetY,
|
||||
sx = canvas.scaleOffsetX,
|
||||
sy = canvas.scaleOffsetY;
|
||||
this.pos.x *= sx;
|
||||
this.pos.x += ox;
|
||||
this.pos.y *= sy;
|
||||
this.pos.y += oy;
|
||||
|
||||
if (e.touches.length === 1) {
|
||||
this.pos = eventInfo.getPos();
|
||||
} else if (e.touches.length === 2) {
|
||||
var s = canvas.getSize(),
|
||||
pos1 = $.event.getPos(e, win, 0),
|
||||
pos2 = $.event.getPos(e, win, 1),
|
||||
touch1 = {
|
||||
x: (pos1.x - s.width/2 - ox) * 1/sx,
|
||||
y: (pos1.y - s.height/2 - oy) * 1/sy
|
||||
},
|
||||
touch2 = {
|
||||
x: (pos2.x - s.width/2 - ox) * 1/sx,
|
||||
y: (pos2.y - s.height/2 - oy) * 1/sy
|
||||
};
|
||||
this.pos = {
|
||||
x: (touch1.x + touch2.x) / 2,
|
||||
y: (touch1.y + touch2.y) / 2
|
||||
}
|
||||
this.unitRadius = Metamaps.Util.getDistance(touch1, touch2) / 2
|
||||
}
|
||||
if (e.touches.length === 1 || e.touches.length === 2) {
|
||||
this.pos.x *= sx;
|
||||
this.pos.x += ox;
|
||||
this.pos.y *= sy;
|
||||
this.pos.y += oy;
|
||||
}
|
||||
},
|
||||
|
||||
onTouchMove: function(e, win, eventInfo) {
|
||||
e.preventDefault()
|
||||
if(!this.config.panning) return;
|
||||
if(!this.pressed) return;
|
||||
if(this.config.panning == 'avoid nodes' && (this.dom? this.isLabel(e, win) : eventInfo.getNode())) return;
|
||||
|
||||
var canvas = this.canvas,
|
||||
ox = canvas.translateOffsetX,
|
||||
oy = canvas.translateOffsetY,
|
||||
sx = canvas.scaleOffsetX,
|
||||
sy = canvas.scaleOffsetY,
|
||||
beforePos = this.pos,
|
||||
currentPos,
|
||||
touch1,
|
||||
touch2;
|
||||
|
||||
if (e.touches.length == 1) {
|
||||
var rightClick = e.button == 2 || (navigator.platform.indexOf("Mac") != -1 && e.ctrlKey);
|
||||
if (!Metamaps.Mouse.boxStartCoordinates && ((e.button == 0 && e.shiftKey) || (e.button == 0 && e.ctrlKey) || rightClick)) {
|
||||
Metamaps.Visualize.mGraph.busy = true;
|
||||
Metamaps.boxStartCoordinates = eventInfo.getPos();
|
||||
return;
|
||||
currentPos = eventInfo.getPos()
|
||||
} else if (e.touches.length >= 2) {
|
||||
var s = canvas.getSize(),
|
||||
pos1 = $.event.getPos(e, win, 0),
|
||||
pos2 = $.event.getPos(e, win, 1),
|
||||
touch1 = {
|
||||
x: (pos1.x - s.width/2 - ox) * 1/sx,
|
||||
y: (pos1.y - s.height/2 - oy) * 1/sy
|
||||
},
|
||||
touch2 = {
|
||||
x: (pos2.x - s.width/2 - ox) * 1/sx,
|
||||
y: (pos2.y - s.height/2 - oy) * 1/sy
|
||||
};
|
||||
currentPos = {
|
||||
x: (touch1.x + touch2.x) / 2,
|
||||
y: (touch1.y + touch2.y) / 2
|
||||
}
|
||||
}
|
||||
currentPos.x *= sx;
|
||||
currentPos.y *= sy;
|
||||
currentPos.x += ox;
|
||||
currentPos.y += oy;
|
||||
Metamaps.Mouse.didPan = true;
|
||||
var x = currentPos.x - beforePos.x,
|
||||
y = currentPos.y - beforePos.y;
|
||||
Metamaps.Mouse.changeInX = x;
|
||||
Metamaps.Mouse.changeInY = y;
|
||||
this.pos = currentPos;
|
||||
canvas.translate(x * 1/sx, y * 1/sy);
|
||||
jQuery(document).trigger(Metamaps.JIT.events.pan);
|
||||
|
||||
if (e.touches.length >= 2) {
|
||||
var currentPixelRadius = Metamaps.Util.getDistance({
|
||||
x: e.touches[0].clientX,
|
||||
y: e.touches[0].clientY
|
||||
}, {
|
||||
x: e.touches[1].clientX,
|
||||
y: e.touches[1].clientY
|
||||
}) / 2
|
||||
var desiredScale = currentPixelRadius / this.unitRadius
|
||||
var scaler = desiredScale / sx
|
||||
var midpoint = {
|
||||
x: (e.touches[0].clientX + e.touches[1].clientX) / 2,
|
||||
y: (e.touches[0].clientY + e.touches[1].clientY) / 2
|
||||
}
|
||||
if (30 >= desiredScale && desiredScale >= 0.2) {
|
||||
Metamaps.Util.zoomOnPoint(this, scaler, midpoint)
|
||||
jQuery(document).trigger(Metamaps.JIT.events.zoom)
|
||||
}
|
||||
if (Metamaps.Mouse.boxStartCoordinates && ((e.button == 0 && e.shiftKey) || (e.button == 0 && e.ctrlKey) || rightClick)) {
|
||||
Metamaps.Visualize.mGraph.busy = true;
|
||||
Metamaps.JIT.drawSelectBox(eventInfo,e);
|
||||
return;
|
||||
}
|
||||
if (rightClick){
|
||||
return;
|
||||
}
|
||||
if (e.target.id != 'infovis-canvas') {
|
||||
this.pressed = false;
|
||||
return;
|
||||
}
|
||||
Metamaps.Mouse.didPan = true;
|
||||
var thispos = this.pos,
|
||||
currentPos = eventInfo.getPos(),
|
||||
canvas = this.canvas,
|
||||
ox = canvas.translateOffsetX,
|
||||
oy = canvas.translateOffsetY,
|
||||
sx = canvas.scaleOffsetX,
|
||||
sy = canvas.scaleOffsetY;
|
||||
currentPos.x *= sx;
|
||||
currentPos.y *= sy;
|
||||
currentPos.x += ox;
|
||||
currentPos.y += oy;
|
||||
var x = currentPos.x - thispos.x,
|
||||
y = currentPos.y - thispos.y;
|
||||
Metamaps.Mouse.changeInX = x;
|
||||
Metamaps.Mouse.changeInY = y;
|
||||
this.pos = currentPos;
|
||||
this.canvas.translate(x * 1/sx, y * 1/sy);
|
||||
jQuery(document).trigger(Metamaps.JIT.events.pan);
|
||||
}
|
||||
/*
|
||||
else if (e.touches.length == 2) {
|
||||
var touch1 = e.touches[0]
|
||||
var touch2 = e.touches[1]
|
||||
var canvas = this.canvas
|
||||
|
||||
callCount++;
|
||||
|
||||
var dist = Metamaps.Util.getDistance({
|
||||
x: touch1.clientX,
|
||||
y: touch1.clientY
|
||||
}, {
|
||||
x: touch2.clientX,
|
||||
y: touch2.clientY
|
||||
})
|
||||
|
||||
if (!this.initDist) {
|
||||
this.initDist = dist
|
||||
this.initScale = canvas.scaleOffsetX
|
||||
}
|
||||
var scale = (dist / this.initDist)
|
||||
|
||||
document.getElementById("header_content").innerHTML = scale + ' ' + canvas.scaleOffsetX
|
||||
if (30 >= this.initScale * scale && this.initScale * scale >= 0.2) {
|
||||
canvas.scale(this.initScale * scale, this.initScale * scale)
|
||||
}
|
||||
if (canvas.scaleOffsetX < 0.5) {
|
||||
canvas.viz.labels.hideLabels(true)
|
||||
} else if (canvas.scaleOffsetX > 0.5) {
|
||||
canvas.viz.labels.hideLabels(false)
|
||||
}
|
||||
|
||||
jQuery(document).trigger(Metamaps.JIT.events.zoom);
|
||||
}
|
||||
*/
|
||||
},
|
||||
|
||||
onTouchEnd: function(e, win, eventInfo, isRightClick) {
|
||||
if(!this.config.panning) return;
|
||||
this.pressed = false;
|
||||
if (Metamaps.Mouse.didPan) Metamaps.JIT.SmoothPanning();
|
||||
this.initDist = false
|
||||
if (e.touches.length === 1) {
|
||||
var canvas = this.canvas,
|
||||
ox = canvas.translateOffsetX,
|
||||
oy = canvas.translateOffsetY,
|
||||
sx = canvas.scaleOffsetX,
|
||||
sy = canvas.scaleOffsetY,
|
||||
s = canvas.getSize();
|
||||
this.pos = {
|
||||
x: (e.touches[0].clientX - s.width/2 - ox) * 1/sx,
|
||||
y: (e.touches[0].clientY - s.height/2 - oy) * 1/sy
|
||||
};
|
||||
this.pos.x *= sx;
|
||||
this.pos.x += ox;
|
||||
this.pos.y *= sy;
|
||||
this.pos.y += oy;
|
||||
} else if (e.touches.length === 0) {
|
||||
this.pressed = false;
|
||||
this.pos = null
|
||||
if (Metamaps.Mouse.didPan) Metamaps.JIT.SmoothPanning();
|
||||
}
|
||||
}
|
||||
// END METAMAPS CODE
|
||||
});
|
||||
|
|
|
@ -113,9 +113,15 @@ describe('Metamaps.Util.js', function() {
|
|||
expect(Util.mdToHTML(md).trim()).to.equal(html)
|
||||
})
|
||||
|
||||
it('links and images', function() {
|
||||
const md = '[Link](https://metamaps.cc) ![Image](https://example.org/image.png)'
|
||||
const html = '<p><a href="https://metamaps.cc">Link</a> <img src="https://example.org/image.png" alt="Image" /></p>'
|
||||
it('links', function() {
|
||||
const md = '[Link](https://metamaps.cc)'
|
||||
const html = '<p><a href="https://metamaps.cc">Link</a></p>'
|
||||
expect(Util.mdToHTML(md).trim()).to.equal(html)
|
||||
})
|
||||
|
||||
it('images are not rendered', function() {
|
||||
const md = '![Image](https://example.org/image.png)'
|
||||
const html = '<p>![Image](https://example.org/image.png)</p>'
|
||||
expect(Util.mdToHTML(md).trim()).to.equal(html)
|
||||
})
|
||||
})
|
50
frontend/test/components/ImportDialogBox.spec.js
Normal file
50
frontend/test/components/ImportDialogBox.spec.js
Normal file
|
@ -0,0 +1,50 @@
|
|||
/* global describe, it */
|
||||
import React from 'react'
|
||||
import TestUtils from 'react-addons-test-utils' // ES6
|
||||
import ImportDialogBox from '../../src/components/ImportDialogBox.js'
|
||||
import Dropzone from 'react-dropzone'
|
||||
import chai from 'chai'
|
||||
|
||||
const { expect } = chai
|
||||
|
||||
describe('ImportDialogBox', function() {
|
||||
it('has an Export CSV button', function(done) {
|
||||
const onExport = format => {
|
||||
if (format === 'csv') done()
|
||||
}
|
||||
const detachedComp = TestUtils.renderIntoDocument(<ImportDialogBox onExport={onExport} />)
|
||||
const button = TestUtils.findRenderedDOMComponentWithClass(detachedComp, 'export-csv')
|
||||
const buttonNode = React.findDOMNode(button)
|
||||
expect(button).to.exist;
|
||||
TestUtils.Simulate.click(buttonNode)
|
||||
})
|
||||
|
||||
it('has an Export JSON button', function(done) {
|
||||
const onExport = format => {
|
||||
if (format === 'json') done()
|
||||
}
|
||||
const detachedComp = TestUtils.renderIntoDocument(<ImportDialogBox onExport={onExport} />)
|
||||
const button = TestUtils.findRenderedDOMComponentWithClass(detachedComp, 'export-json')
|
||||
const buttonNode = React.findDOMNode(button)
|
||||
expect(button).to.exist;
|
||||
TestUtils.Simulate.click(buttonNode)
|
||||
})
|
||||
|
||||
it('has a Download screenshot button', function(done) {
|
||||
const downloadScreenshot = () => { done() }
|
||||
const detachedComp = TestUtils.renderIntoDocument(<ImportDialogBox downloadScreenshot={downloadScreenshot()} />)
|
||||
const button = TestUtils.findRenderedDOMComponentWithClass(detachedComp, 'download-screenshot')
|
||||
const buttonNode = React.findDOMNode(button)
|
||||
expect(button).to.exist;
|
||||
TestUtils.Simulate.click(buttonNode)
|
||||
})
|
||||
|
||||
it('has a file uploader', function(done) {
|
||||
const uploadedFile = { file: 'mock a file' }
|
||||
const onFileAdded = file => { if (file === uploadedFile) done() }
|
||||
const detachedComp = TestUtils.renderIntoDocument(<ImportDialogBox onExport={() => {}} onFileAdded={onFileAdded} />)
|
||||
const dropzone = TestUtils.findRenderedComponentWithType(detachedComp, Dropzone)
|
||||
expect(dropzone).to.exist;
|
||||
dropzone.props.onDropAccepted([uploadedFile], { preventDefault: () => {} })
|
||||
})
|
||||
})
|
25
frontend/test/support/dom.js
Normal file
25
frontend/test/support/dom.js
Normal file
|
@ -0,0 +1,25 @@
|
|||
const jsdom = require('jsdom')
|
||||
const doc = jsdom.jsdom('<!doctype html><html><body></body></html>')
|
||||
const win = doc.defaultView
|
||||
|
||||
global.document = doc
|
||||
global.window = win
|
||||
|
||||
// take all properties of the window object and also attach it to the
|
||||
// mocha global object
|
||||
propagateToGlobal(win)
|
||||
|
||||
// from mocha-jsdom https://github.com/rstacruz/mocha-jsdom/blob/master/index.js#L80
|
||||
function propagateToGlobal (window) {
|
||||
for (let key in window) {
|
||||
if (!window.hasOwnProperty(key)) continue
|
||||
if (key in global) continue
|
||||
|
||||
global[key] = window[key]
|
||||
}
|
||||
}
|
||||
|
||||
// Metamaps dependencies fixes
|
||||
global.HowlerGlobal = global.HowlerGlobal || { prototype: {} }
|
||||
global.Howl = global.Howl || { prototype: {} }
|
||||
global.Sound = global.Sound || { prototype: {} }
|
20
lib/tasks/emails.rake
Normal file
20
lib/tasks/emails.rake
Normal file
|
@ -0,0 +1,20 @@
|
|||
namespace :metamaps do
|
||||
desc "delivers recent map activity digest emails to users"
|
||||
task deliver_map_activity_emails: :environment do
|
||||
summarize_map_activity
|
||||
end
|
||||
|
||||
def summarize_map_activity
|
||||
Follow.where(followed_type: 'Map').find_each do |follow|
|
||||
map = follow.followed
|
||||
user = follow.user
|
||||
# add logging and rescue-ing
|
||||
# and a notification of failure
|
||||
next unless MapPolicy.new(user, map).show? # just in case the permission changed
|
||||
next unless user.emails_allowed
|
||||
summary_data = MapActivityService.summarize_data(map, user)
|
||||
next if summary_data[:stats].blank?
|
||||
MapActivityMailer.daily_summary(user, map, summary_data).deliver_later
|
||||
end
|
||||
end
|
||||
end
|
13
package.json
13
package.json
|
@ -5,7 +5,7 @@
|
|||
"scripts": {
|
||||
"build": "webpack",
|
||||
"build:watch": "webpack --watch",
|
||||
"test": "mocha --compilers js:babel-core/register frontend/test",
|
||||
"test": "mocha-webpack --webpack-config webpack.test.config.js --require frontend/test/support/dom.js frontend/test",
|
||||
"eslint": "eslint frontend",
|
||||
"eslint:fix": "eslint --fix frontend"
|
||||
},
|
||||
|
@ -35,7 +35,7 @@
|
|||
"csv-parse": "1.1.10",
|
||||
"emoji-mart": "0.3.7",
|
||||
"getscreenmedia": "2.0.0",
|
||||
"hark": "git://github.com/otalk/hark#342ef9b7eff2",
|
||||
"hark": "1.1.5",
|
||||
"howler": "2.0.2",
|
||||
"jquery": "3.1.1",
|
||||
"json-loader": "0.5.4",
|
||||
|
@ -45,10 +45,12 @@
|
|||
"react": "15.4.2",
|
||||
"react-dom": "15.4.2",
|
||||
"react-dropzone": "3.9.1",
|
||||
"react-onclickoutside": "5.9.0",
|
||||
"redux": "3.6.0",
|
||||
"riek": "1.0.7",
|
||||
"simplewebrtc": "2.2.2",
|
||||
"socket.io": "1.3.7",
|
||||
"webpack": "1.14.0"
|
||||
"webpack": "2.2.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"babel-eslint": "^7.1.1",
|
||||
|
@ -59,7 +61,10 @@
|
|||
"eslint-plugin-promise": "^3.4.0",
|
||||
"eslint-plugin-react": "^6.8.0",
|
||||
"eslint-plugin-standard": "^2.0.1",
|
||||
"mocha": "^3.2.0"
|
||||
"jsdom": "^9.11.0",
|
||||
"mocha": "^3.2.0",
|
||||
"mocha-webpack": "^0.7.0",
|
||||
"react-addons-test-utils": "^15.4.2"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"raml2html": "4.0.5"
|
||||
|
|
8
spec/factories/message.rb
Normal file
8
spec/factories/message.rb
Normal file
|
@ -0,0 +1,8 @@
|
|||
# frozen_string_literal: true
|
||||
FactoryGirl.define do
|
||||
factory :message do
|
||||
association :resource, factory: :map
|
||||
user
|
||||
sequence(:message) { |n| "Cool Message ##{n}" }
|
||||
end
|
||||
end
|
6
spec/mailers/map_activity_mailer_spec.rb
Normal file
6
spec/mailers/map_activity_mailer_spec.rb
Normal file
|
@ -0,0 +1,6 @@
|
|||
# frozen_string_literal: true
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe MapActivityMailer, type: :mailer do
|
||||
|
||||
end
|
109
spec/mailers/previews/map_activity_mailer_preview.rb
Normal file
109
spec/mailers/previews/map_activity_mailer_preview.rb
Normal file
|
@ -0,0 +1,109 @@
|
|||
# frozen_string_literal: true
|
||||
# Preview all emails at http://localhost:3000/rails/mailers/map_activity_mailer
|
||||
class MapActivityMailerPreview < ActionMailer::Preview
|
||||
def daily_summary
|
||||
user = generate_user
|
||||
map = generate_map
|
||||
generate_recent_activity_on_map(map)
|
||||
summary_data = MapActivityService.summarize_data(map, user)
|
||||
MapActivityMailer.daily_summary(user, map, summary_data)
|
||||
end
|
||||
|
||||
private
|
||||
def generate_recent_activity_on_map(map)
|
||||
mapping = nil
|
||||
mapping2 = nil
|
||||
mapping3 = nil
|
||||
mapping4 = nil
|
||||
mapping5 = nil
|
||||
mapping6 = nil
|
||||
mapping7 = nil
|
||||
mapping8 = nil
|
||||
mapping9 = nil
|
||||
mapping10 = nil
|
||||
|
||||
Timecop.freeze(2.days.ago) do
|
||||
mapping = topic_added_to_map(map)
|
||||
mapping2 = topic_added_to_map(map)
|
||||
mapping3 = topic_added_to_map(map)
|
||||
mapping4 = topic_added_to_map(map)
|
||||
mapping5 = topic_added_to_map(map)
|
||||
mapping6 = topic_added_to_map(map)
|
||||
mapping7 = topic_added_to_map(map)
|
||||
mapping8 = topic_added_to_map(map)
|
||||
mapping9 = synapse_added_to_map(map, mapping.mappable, mapping2.mappable)
|
||||
mapping10 = synapse_added_to_map(map, mapping.mappable, mapping8.mappable)
|
||||
end
|
||||
Timecop.return
|
||||
|
||||
Timecop.freeze(2.hours.ago) do
|
||||
topic_moved_on_map(mapping7)
|
||||
topic_moved_on_map(mapping8)
|
||||
generate_message(map)
|
||||
generate_message(map)
|
||||
generate_message(map)
|
||||
synapse_added_to_map(map, mapping7.mappable, mapping8.mappable)
|
||||
synapse_added_to_map(map, mapping.mappable, mapping8.mappable)
|
||||
synapse_removed_from_map(mapping9)
|
||||
synapse_removed_from_map(mapping10)
|
||||
end
|
||||
Timecop.return
|
||||
|
||||
Timecop.freeze(30.minutes.ago) do
|
||||
topic_removed_from_map(mapping3)
|
||||
topic_removed_from_map(mapping4)
|
||||
topic_removed_from_map(mapping5)
|
||||
topic_removed_from_map(mapping6)
|
||||
topic_added_to_map(map)
|
||||
topic_added_to_map(map)
|
||||
topic_added_to_map(map)
|
||||
topic_added_to_map(map)
|
||||
topic_added_to_map(map)
|
||||
topic_added_to_map(map)
|
||||
topic_added_to_map(map)
|
||||
topic_added_to_map(map)
|
||||
end
|
||||
Timecop.return
|
||||
end
|
||||
|
||||
def generate_user
|
||||
User.create(name: Faker::Name.name, email: Faker::Internet.email, password: "password", password_confirmation: "password", joinedwithcode: 'qwertyui')
|
||||
end
|
||||
|
||||
def generate_map
|
||||
Map.create(name: Faker::HarryPotter.book, permission: 'commons', arranged: false, user: generate_user)
|
||||
end
|
||||
|
||||
def topic_added_to_map(map)
|
||||
user = generate_user
|
||||
topic = Topic.create(name: Faker::Friends.quote, permission: 'commons', user: user)
|
||||
mapping = Mapping.create(map: map, mappable: topic, user: user)
|
||||
end
|
||||
|
||||
def topic_moved_on_map(mapping)
|
||||
meta = { 'x': 10, 'y': 20, 'mapping_id': mapping.id }
|
||||
Events::TopicMovedOnMap.publish!(mapping.mappable, mapping.map, generate_user, meta)
|
||||
end
|
||||
|
||||
def topic_removed_from_map(mapping)
|
||||
user = generate_user
|
||||
mapping.updated_by = user
|
||||
mapping.destroy
|
||||
end
|
||||
|
||||
def synapse_added_to_map(map, topic1, topic2)
|
||||
user = generate_user
|
||||
topic = Synapse.create(desc: 'describes', permission: 'commons', user: user, topic1: topic1, topic2: topic2)
|
||||
mapping = Mapping.create(map: map, mappable: topic, user: user)
|
||||
end
|
||||
|
||||
def synapse_removed_from_map(mapping)
|
||||
user = generate_user
|
||||
mapping.updated_by = user
|
||||
mapping.destroy
|
||||
end
|
||||
|
||||
def generate_message(map)
|
||||
Message.create(message: Faker::HarryPotter.quote, resource: map, user: generate_user)
|
||||
end
|
||||
end
|
|
@ -6,12 +6,12 @@ class MapMailerPreview < ActionMailer::Preview
|
|||
MapMailer.invite_to_edit(user_map)
|
||||
end
|
||||
|
||||
def access_request_email
|
||||
def access_request
|
||||
request = AccessRequest.first
|
||||
MapMailer.access_request(request)
|
||||
end
|
||||
|
||||
def access_approved_email
|
||||
def access_approved
|
||||
request = AccessRequest.first
|
||||
MapMailer.access_approved(request)
|
||||
end
|
||||
|
|
16
spec/mailers/previews/topic_mailer_preview.rb
Normal file
16
spec/mailers/previews/topic_mailer_preview.rb
Normal file
|
@ -0,0 +1,16 @@
|
|||
# frozen_string_literal: true
|
||||
# Preview all emails at http://localhost:3000/rails/mailers/topic_mailer
|
||||
class TopicMailerPreview < ActionMailer::Preview
|
||||
def added_to_map
|
||||
event = Event.where(kind: 'topic_added_to_map').first
|
||||
user = User.first
|
||||
TopicMailer.added_to_map(event, user)
|
||||
end
|
||||
|
||||
def connected
|
||||
synapse = Synapse.first
|
||||
topic = synapse.topic1
|
||||
user = User.first
|
||||
TopicMailer.connected(synapse, topic, user)
|
||||
end
|
||||
end
|
398
spec/services/map_activity_service_spec.rb
Normal file
398
spec/services/map_activity_service_spec.rb
Normal file
|
@ -0,0 +1,398 @@
|
|||
require 'rails_helper'
|
||||
|
||||
RSpec.describe MapActivityService do
|
||||
let(:map) { create(:map, created_at: 1.week.ago) }
|
||||
let(:other_user) { create(:user) }
|
||||
let(:email_user) { create(:user) }
|
||||
let(:empty_response) { {stats:{}} }
|
||||
|
||||
it 'includes nothing if nothing happened' do
|
||||
response = MapActivityService.summarize_data(map, email_user)
|
||||
expect(response).to eq (empty_response)
|
||||
end
|
||||
|
||||
describe 'topics added to map' do
|
||||
it 'includes a topic added within the last 24 hours' do
|
||||
topic = create(:topic)
|
||||
mapping = create(:mapping, user: other_user, map: map, mappable: topic, created_at: 6.hours.ago)
|
||||
event = Event.find_by(kind: 'topic_added_to_map', eventable_id: topic.id)
|
||||
event.update_columns(created_at: 6.hours.ago)
|
||||
response = MapActivityService.summarize_data(map, email_user)
|
||||
expect(response[:stats][:topics_added]).to eq(1)
|
||||
expect(response[:topics_added]).to eq([event])
|
||||
end
|
||||
|
||||
it 'includes a topic added, then removed, then re-added within the last 24 hours' do
|
||||
topic = create(:topic)
|
||||
mapping = create(:mapping, user: other_user, map: map, mappable: topic, created_at: 6.hours.ago)
|
||||
Event.find_by(kind: 'topic_added_to_map', eventable_id: topic.id).update_columns(created_at: 6.hours.ago)
|
||||
mapping.updated_by = other_user
|
||||
mapping.destroy
|
||||
Event.find_by(kind: 'topic_removed_from_map', eventable_id: topic.id).update_columns(created_at: 5.hours.ago)
|
||||
mapping2 = create(:mapping, user: other_user, map: map, mappable: topic, created_at: 4.hours.ago)
|
||||
event = Event.where("meta->>'mapping_id' = ?", mapping2.id.to_s).first
|
||||
event.update_columns(created_at: 4.hours.ago)
|
||||
response = MapActivityService.summarize_data(map, email_user)
|
||||
expect(response[:stats][:topics_added]).to eq(1)
|
||||
expect(response[:topics_added]).to eq([event])
|
||||
end
|
||||
|
||||
it 'excludes a topic removed then re-added within the last 24 hours' do
|
||||
topic = create(:topic)
|
||||
mapping = create(:mapping, user: other_user, map: map, mappable: topic, created_at: 25.hours.ago)
|
||||
Event.find_by(kind: 'topic_added_to_map', eventable_id: topic.id).update_columns(created_at: 25.hours.ago)
|
||||
mapping.updated_by = other_user
|
||||
mapping.destroy
|
||||
Event.find_by(kind: 'topic_removed_from_map', eventable_id: topic.id).update_columns(created_at: 6.hours.ago)
|
||||
mapping2 = create(:mapping, user: other_user, map: map, mappable: topic, created_at: 5.hours.ago)
|
||||
Event.where(kind: 'topic_added_to_map').where("meta->>'mapping_id' = ?", mapping2.id.to_s).first.update_columns(created_at: 5.hours.ago)
|
||||
response = MapActivityService.summarize_data(map, email_user)
|
||||
expect(response).to eq (empty_response)
|
||||
end
|
||||
|
||||
it 'excludes a topic added outside the last 24 hours' do
|
||||
topic = create(:topic)
|
||||
mapping = create(:mapping, user: other_user, map: map, mappable: topic, created_at: 25.hours.ago)
|
||||
Event.find_by(kind: 'topic_added_to_map', eventable_id: topic.id).update_columns(created_at: 25.hours.ago)
|
||||
response = MapActivityService.summarize_data(map, email_user)
|
||||
expect(response).to eq (empty_response)
|
||||
end
|
||||
|
||||
it 'excludes topics added by the user who will receive the data' do
|
||||
topic = create(:topic)
|
||||
topic2 = create(:topic)
|
||||
mapping = create(:mapping, user: other_user, map: map, mappable: topic, created_at: 5.hours.ago)
|
||||
event = Event.find_by(kind: 'topic_added_to_map', eventable_id: topic.id)
|
||||
event.update_columns(created_at: 5.hours.ago)
|
||||
mapping2 = create(:mapping, user: email_user, map: map, mappable: topic2, created_at: 5.hours.ago)
|
||||
Event.find_by(kind: 'topic_added_to_map', eventable_id: topic2.id).update_columns(created_at: 5.hours.ago)
|
||||
response = MapActivityService.summarize_data(map, email_user)
|
||||
expect(response[:stats][:topics_added]).to eq(1)
|
||||
expect(response[:topics_added]).to eq([event])
|
||||
end
|
||||
end
|
||||
|
||||
describe 'topics moved on map' do
|
||||
it 'includes ones moved within the last 24 hours' do
|
||||
topic = create(:topic)
|
||||
create(:mapping, user: email_user, map: map, mappable: topic, created_at: 5.hours.ago)
|
||||
event = Events::TopicMovedOnMap.publish!(topic, map, other_user, {})
|
||||
event.update(created_at: 6.hours.ago)
|
||||
response = MapActivityService.summarize_data(map, email_user)
|
||||
expect(response[:stats][:topics_moved]).to eq(1)
|
||||
end
|
||||
|
||||
it 'only includes each topic that was moved in the count once' do
|
||||
topic = create(:topic)
|
||||
topic2 = create(:topic)
|
||||
create(:mapping, user: email_user, map: map, mappable: topic, created_at: 5.hours.ago)
|
||||
create(:mapping, user: email_user, map: map, mappable: topic2, created_at: 5.hours.ago)
|
||||
event = Events::TopicMovedOnMap.publish!(topic, map, other_user, {})
|
||||
event.update(created_at: 6.hours.ago)
|
||||
event2 = Events::TopicMovedOnMap.publish!(topic, map, other_user, {})
|
||||
event2.update(created_at: 5.hours.ago)
|
||||
event3 = Events::TopicMovedOnMap.publish!(topic2, map, other_user, {})
|
||||
event3.update(created_at: 5.hours.ago)
|
||||
response = MapActivityService.summarize_data(map, email_user)
|
||||
expect(response[:stats][:topics_moved]).to eq(2)
|
||||
end
|
||||
|
||||
it 'excludes ones moved outside the last 24 hours' do
|
||||
topic = create(:topic)
|
||||
create(:mapping, user: email_user, map: map, mappable: topic, created_at: 5.hours.ago)
|
||||
event = Events::TopicMovedOnMap.publish!(topic, map, other_user, {})
|
||||
event.update(created_at: 25.hours.ago)
|
||||
response = MapActivityService.summarize_data(map, email_user)
|
||||
expect(response).to eq (empty_response)
|
||||
end
|
||||
|
||||
it 'excludes ones moved by the user who will receive the data' do
|
||||
topic = create(:topic)
|
||||
create(:mapping, user: email_user, map: map, mappable: topic, created_at: 5.hours.ago)
|
||||
event = Events::TopicMovedOnMap.publish!(topic, map, email_user, {})
|
||||
event.update(created_at: 5.hours.ago)
|
||||
response = MapActivityService.summarize_data(map, email_user)
|
||||
expect(response).to eq (empty_response)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'topics removed from map' do
|
||||
it 'includes a topic removed within the last 24 hours' do
|
||||
topic = create(:topic)
|
||||
mapping = create(:mapping, user: other_user, map: map, mappable: topic, created_at: 25.hours.ago)
|
||||
Event.find_by(kind: 'topic_added_to_map', eventable_id: topic.id).update_columns(created_at: 25.hours.ago)
|
||||
mapping.updated_by = other_user
|
||||
mapping.destroy
|
||||
event = Event.find_by(kind: 'topic_removed_from_map', eventable_id: topic.id)
|
||||
event.update_columns(created_at: 6.hours.ago)
|
||||
response = MapActivityService.summarize_data(map, email_user)
|
||||
expect(response[:stats][:topics_removed]).to eq(1)
|
||||
expect(response[:topics_removed]).to eq([event])
|
||||
end
|
||||
|
||||
it 'excludes a topic removed outside the last 24 hours' do
|
||||
topic = create(:topic)
|
||||
mapping = create(:mapping, user: other_user, map: map, mappable: topic, created_at: 26.hours.ago)
|
||||
Event.find_by(kind: 'topic_added_to_map', eventable_id: topic.id).update_columns(created_at: 26.hours.ago)
|
||||
mapping.updated_by = other_user
|
||||
mapping.destroy
|
||||
Event.find_by(kind: 'topic_removed_from_map', eventable_id: topic.id).update_columns(created_at: 25.hours.ago)
|
||||
response = MapActivityService.summarize_data(map, email_user)
|
||||
expect(response).to eq (empty_response)
|
||||
end
|
||||
|
||||
it 'excludes topics removed by the user who will receive the data' do
|
||||
topic = create(:topic)
|
||||
topic2 = create(:topic)
|
||||
mapping = create(:mapping, user: other_user, map: map, mappable: topic, created_at: 25.hours.ago)
|
||||
Event.find_by(kind: 'topic_added_to_map', eventable_id: topic.id).update_columns(created_at: 25.hours.ago)
|
||||
mapping2 = create(:mapping, user: email_user, map: map, mappable: topic2, created_at: 25.hours.ago)
|
||||
Event.find_by(kind: 'topic_added_to_map', eventable_id: topic2.id).update_columns(created_at: 25.hours.ago)
|
||||
mapping.updated_by = other_user
|
||||
mapping.destroy
|
||||
mapping2.updated_by = email_user
|
||||
mapping2.destroy
|
||||
event = Event.find_by(kind: 'topic_removed_from_map', eventable_id: topic.id)
|
||||
event.update_columns(created_at: 5.hours.ago)
|
||||
Event.find_by(kind: 'topic_removed_from_map', eventable_id: topic2.id).update_columns(created_at: 5.hours.ago)
|
||||
response = MapActivityService.summarize_data(map, email_user)
|
||||
expect(response[:stats][:topics_removed]).to eq(1)
|
||||
expect(response[:topics_removed]).to eq([event])
|
||||
end
|
||||
end
|
||||
|
||||
describe 'synapses added to map' do
|
||||
it 'includes a synapse added within the last 24 hours' do
|
||||
synapse = create(:synapse)
|
||||
mapping = create(:mapping, user: other_user, map: map, mappable: synapse, created_at: 6.hours.ago)
|
||||
event = Event.find_by(kind: 'synapse_added_to_map', eventable_id: synapse.id)
|
||||
event.update_columns(created_at: 6.hours.ago)
|
||||
response = MapActivityService.summarize_data(map, email_user)
|
||||
expect(response[:stats][:synapses_added]).to eq(1)
|
||||
expect(response[:synapses_added]).to eq([event])
|
||||
end
|
||||
|
||||
it 'includes a synapse added, then removed, then re-added within the last 24 hours' do
|
||||
synapse = create(:synapse)
|
||||
mapping = create(:mapping, user: other_user, map: map, mappable: synapse, created_at: 6.hours.ago)
|
||||
Event.find_by(kind: 'synapse_added_to_map', eventable_id: synapse.id).update_columns(created_at: 6.hours.ago)
|
||||
mapping.updated_by = other_user
|
||||
mapping.destroy
|
||||
Event.find_by(kind: 'synapse_removed_from_map', eventable_id: synapse.id).update_columns(created_at: 5.hours.ago)
|
||||
mapping2 = create(:mapping, user: other_user, map: map, mappable: synapse, created_at: 4.hours.ago)
|
||||
event = Event.where(kind: 'synapse_added_to_map').where("meta->>'mapping_id' = ?", mapping2.id.to_s).first
|
||||
event.update_columns(created_at: 4.hours.ago)
|
||||
response = MapActivityService.summarize_data(map, email_user)
|
||||
expect(response[:stats][:synapses_added]).to eq(1)
|
||||
expect(response[:synapses_added]).to eq([event])
|
||||
end
|
||||
|
||||
it 'excludes a synapse removed then re-added within the last 24 hours' do
|
||||
synapse = create(:synapse)
|
||||
mapping = create(:mapping, user: other_user, map: map, mappable: synapse, created_at: 25.hours.ago)
|
||||
Event.find_by(kind: 'synapse_added_to_map', eventable_id: synapse.id).update_columns(created_at: 25.hours.ago)
|
||||
mapping.updated_by = other_user
|
||||
mapping.destroy
|
||||
Event.find_by(kind: 'synapse_removed_from_map', eventable_id: synapse.id).update_columns(created_at: 6.hours.ago)
|
||||
mapping2 = create(:mapping, user: other_user, map: map, mappable: synapse, created_at: 5.hours.ago)
|
||||
Event.where(kind: 'synapse_added_to_map').where("meta->>'mapping_id' = ?", mapping2.id.to_s).first.update_columns(created_at: 5.hours.ago)
|
||||
response = MapActivityService.summarize_data(map, email_user)
|
||||
expect(response).to eq (empty_response)
|
||||
end
|
||||
|
||||
it 'excludes a synapse added outside the last 24 hours' do
|
||||
synapse = create(:synapse)
|
||||
mapping = create(:mapping, user: other_user, map: map, mappable: synapse, created_at: 25.hours.ago)
|
||||
Event.find_by(kind: 'synapse_added_to_map', eventable_id: synapse.id).update_columns(created_at: 25.hours.ago)
|
||||
response = MapActivityService.summarize_data(map, email_user)
|
||||
expect(response).to eq (empty_response)
|
||||
end
|
||||
|
||||
it 'excludes synapses added by the user who will receive the data' do
|
||||
synapse = create(:synapse)
|
||||
synapse2 = create(:synapse)
|
||||
mapping = create(:mapping, user: other_user, map: map, mappable: synapse, created_at: 5.hours.ago)
|
||||
event = Event.find_by(kind: 'synapse_added_to_map', eventable_id: synapse.id)
|
||||
event.update_columns(created_at: 5.hours.ago)
|
||||
mapping2 = create(:mapping, user: email_user, map: map, mappable: synapse2, created_at: 5.hours.ago)
|
||||
Event.find_by(kind: 'synapse_added_to_map', eventable_id: synapse2.id).update_columns(created_at: 5.hours.ago)
|
||||
response = MapActivityService.summarize_data(map, email_user)
|
||||
expect(response[:stats][:synapses_added]).to eq(1)
|
||||
expect(response[:synapses_added]).to eq([event])
|
||||
end
|
||||
end
|
||||
|
||||
describe 'synapses removed from map' do
|
||||
it 'includes a synapse removed within the last 24 hours' do
|
||||
synapse = create(:synapse)
|
||||
mapping = create(:mapping, user: other_user, map: map, mappable: synapse, created_at: 25.hours.ago)
|
||||
Event.find_by(kind: 'synapse_added_to_map', eventable_id: synapse.id).update_columns(created_at: 25.hours.ago)
|
||||
mapping.updated_by = other_user
|
||||
mapping.destroy
|
||||
event = Event.find_by(kind: 'synapse_removed_from_map', eventable_id: synapse.id)
|
||||
event.update_columns(created_at: 6.hours.ago)
|
||||
response = MapActivityService.summarize_data(map, email_user)
|
||||
expect(response[:stats][:synapses_removed]).to eq(1)
|
||||
expect(response[:synapses_removed]).to eq([event])
|
||||
end
|
||||
|
||||
it 'excludes a synapse removed outside the last 24 hours' do
|
||||
synapse = create(:synapse)
|
||||
mapping = create(:mapping, user: other_user, map: map, mappable: synapse, created_at: 25.hours.ago)
|
||||
Event.find_by(kind: 'synapse_added_to_map', eventable_id: synapse.id).update_columns(created_at: 25.hours.ago)
|
||||
mapping.updated_by = other_user
|
||||
mapping.destroy
|
||||
Event.find_by(kind: 'synapse_removed_from_map', eventable_id: synapse.id).update_columns(created_at: 25.hours.ago)
|
||||
response = MapActivityService.summarize_data(map, email_user)
|
||||
expect(response).to eq (empty_response)
|
||||
end
|
||||
|
||||
it 'excludes synapses removed by the user who will receive the data' do
|
||||
synapse = create(:synapse)
|
||||
synapse2 = create(:synapse)
|
||||
mapping = create(:mapping, user: other_user, map: map, mappable: synapse, created_at: 25.hours.ago)
|
||||
Event.find_by(kind: 'synapse_added_to_map', eventable_id: synapse.id).update_columns(created_at: 25.hours.ago)
|
||||
mapping2 = create(:mapping, user: email_user, map: map, mappable: synapse2, created_at: 25.hours.ago)
|
||||
Event.find_by(kind: 'synapse_added_to_map', eventable_id: synapse2.id).update_columns(created_at: 25.hours.ago)
|
||||
mapping.updated_by = other_user
|
||||
mapping.destroy
|
||||
mapping2.updated_by = email_user
|
||||
mapping2.destroy
|
||||
event = Event.find_by(kind: 'synapse_removed_from_map', eventable_id: synapse.id)
|
||||
event.update_columns(created_at: 5.hours.ago)
|
||||
Event.find_by(kind: 'synapse_removed_from_map', eventable_id: synapse2.id).update_columns(created_at: 5.hours.ago)
|
||||
response = MapActivityService.summarize_data(map, email_user)
|
||||
expect(response[:stats][:synapses_removed]).to eq(1)
|
||||
expect(response[:synapses_removed]).to eq([event])
|
||||
end
|
||||
end
|
||||
|
||||
it 'handles permissions for topics added' do
|
||||
new_topic = nil
|
||||
new_private_topic = nil
|
||||
|
||||
Timecop.freeze(10.hours.ago) do
|
||||
new_topic = create(:topic, permission: 'commons', user: other_user)
|
||||
create(:mapping, map: map, mappable: new_topic, user: other_user)
|
||||
new_private_topic = create(:topic, permission: 'private', user: other_user)
|
||||
create(:mapping, map: map, mappable: new_private_topic, user: other_user)
|
||||
end
|
||||
Timecop.return
|
||||
|
||||
response = MapActivityService.summarize_data(map, email_user)
|
||||
expect(response[:stats]).to eq({
|
||||
topics_added: 1
|
||||
})
|
||||
expect(response[:topics_added].map(&:eventable_id)).to include(new_topic.id)
|
||||
expect(response[:topics_added].map(&:eventable_id)).to_not include(new_private_topic.id)
|
||||
end
|
||||
|
||||
it 'handles permissions for topics removed' do
|
||||
old_topic = nil
|
||||
old_private_topic = nil
|
||||
old_topic_mapping = nil
|
||||
old_private_topic_mapping = nil
|
||||
|
||||
Timecop.freeze(2.days.ago) do
|
||||
old_topic = create(:topic, permission: 'commons', user: other_user)
|
||||
old_topic_mapping = create(:mapping, map: map, mappable: old_topic, user: other_user)
|
||||
old_private_topic = create(:topic, permission: 'private', user: other_user)
|
||||
old_private_topic_mapping = create(:mapping, map: map, mappable: old_private_topic, user: other_user)
|
||||
end
|
||||
Timecop.return
|
||||
|
||||
Timecop.freeze(10.hours.ago) do
|
||||
# visible
|
||||
old_topic_mapping.updated_by = other_user
|
||||
old_topic_mapping.destroy
|
||||
# not visible
|
||||
old_private_topic_mapping.updated_by = other_user
|
||||
old_private_topic_mapping.destroy
|
||||
end
|
||||
Timecop.return
|
||||
|
||||
response = MapActivityService.summarize_data(map, email_user)
|
||||
expect(response[:stats]).to eq({
|
||||
topics_removed: 1
|
||||
})
|
||||
expect(response[:topics_removed].map(&:eventable_id)).to include(old_topic.id)
|
||||
expect(response[:topics_removed].map(&:eventable_id)).to_not include(old_private_topic.id)
|
||||
end
|
||||
|
||||
it 'handles permissions for synapses added' do
|
||||
new_synapse = nil
|
||||
new_private_synapse = nil
|
||||
|
||||
Timecop.freeze(10.hours.ago) do
|
||||
# visible
|
||||
new_synapse = create(:synapse, permission: 'commons', user: other_user)
|
||||
create(:mapping, map: map, mappable: new_synapse, user: other_user)
|
||||
# not visible
|
||||
new_private_synapse = create(:synapse, permission: 'private', user: other_user)
|
||||
create(:mapping, map: map, mappable: new_private_synapse, user: other_user)
|
||||
end
|
||||
Timecop.return
|
||||
|
||||
response = MapActivityService.summarize_data(map, email_user)
|
||||
expect(response[:stats]).to eq({
|
||||
synapses_added: 1
|
||||
})
|
||||
expect(response[:synapses_added].map(&:eventable_id)).to include(new_synapse.id)
|
||||
expect(response[:synapses_added].map(&:eventable_id)).to_not include(new_private_synapse.id)
|
||||
end
|
||||
|
||||
it 'handles permissions for synapses removed' do
|
||||
old_synapse = nil
|
||||
old_private_synapse = nil
|
||||
old_synapse_mapping = nil
|
||||
old_private_synapse_mapping = nil
|
||||
|
||||
Timecop.freeze(2.days.ago) do
|
||||
old_synapse = create(:synapse, permission: 'commons', user: other_user)
|
||||
old_synapse_mapping = create(:mapping, map: map, mappable: old_synapse, user: other_user)
|
||||
old_private_synapse = create(:synapse, permission: 'private', user: other_user)
|
||||
old_private_synapse_mapping = create(:mapping, map: map, mappable: old_private_synapse, user: other_user)
|
||||
end
|
||||
Timecop.return
|
||||
|
||||
Timecop.freeze(10.hours.ago) do
|
||||
# visible
|
||||
old_synapse_mapping.updated_by = other_user
|
||||
old_synapse_mapping.destroy
|
||||
# not visible
|
||||
old_private_synapse_mapping.updated_by = other_user
|
||||
old_private_synapse_mapping.destroy
|
||||
end
|
||||
Timecop.return
|
||||
|
||||
response = MapActivityService.summarize_data(map, email_user)
|
||||
expect(response[:stats]).to eq({
|
||||
synapses_removed: 1
|
||||
})
|
||||
expect(response[:synapses_removed].map(&:eventable_id)).to include(old_synapse.id)
|
||||
expect(response[:synapses_removed].map(&:eventable_id)).to_not include(old_private_synapse.id)
|
||||
end
|
||||
|
||||
describe 'messages in the map chat' do
|
||||
it 'counts messages within the last 24 hours' do
|
||||
create(:message, resource: map, created_at: 6.hours.ago)
|
||||
create(:message, resource: map, created_at: 5.hours.ago)
|
||||
response = MapActivityService.summarize_data(map, email_user)
|
||||
expect(response[:stats][:messages_sent]).to eq(2)
|
||||
end
|
||||
|
||||
it 'does not count messages outside the last 24 hours' do
|
||||
create(:message, resource: map, created_at: 25.hours.ago)
|
||||
create(:message, resource: map, created_at: 5.hours.ago)
|
||||
response = MapActivityService.summarize_data(map, email_user)
|
||||
expect(response[:stats][:messages_sent]).to eq(1)
|
||||
end
|
||||
|
||||
it 'does not count messages sent by the person who will receive the data' do
|
||||
create(:message, resource: map, created_at: 5.hours.ago, user: other_user)
|
||||
create(:message, resource: map, created_at: 5.hours.ago, user: email_user)
|
||||
response = MapActivityService.summarize_data(map, email_user)
|
||||
expect(response[:stats][:messages_sent]).to eq(1)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -5,8 +5,12 @@ const NODE_ENV = process.env.NODE_ENV || 'development'
|
|||
const plugins = [
|
||||
new webpack.DefinePlugin({
|
||||
"process.env.NODE_ENV": `"${NODE_ENV}"`
|
||||
})
|
||||
}),
|
||||
new webpack.IgnorePlugin(/^mock-firmata$/), // work around bindings.js error
|
||||
new webpack.ContextReplacementPlugin(/bindings$/, /^$/) // work around bindings.js error
|
||||
]
|
||||
const externals = ["bindings"] // work around bindings.js error
|
||||
|
||||
if (NODE_ENV === 'production') {
|
||||
plugins.push(new webpack.optimize.DedupePlugin())
|
||||
plugins.push(new webpack.optimize.UglifyJsPlugin({
|
||||
|
@ -26,12 +30,13 @@ const devtool = NODE_ENV === 'production' ? undefined : 'cheap-module-eval-sourc
|
|||
module.exports = {
|
||||
context: __dirname,
|
||||
plugins,
|
||||
externals,
|
||||
devtool,
|
||||
module: {
|
||||
preLoaders: [
|
||||
{ test: /\.json$/, loader: 'json' }
|
||||
],
|
||||
loaders: [
|
||||
{
|
||||
test: /\.json$/, loader: 'json-loader'
|
||||
},
|
||||
{
|
||||
test: /\.(js|jsx)?$/,
|
||||
exclude: /node_modules/,
|
||||
|
|
5
webpack.test.config.js
Normal file
5
webpack.test.config.js
Normal file
|
@ -0,0 +1,5 @@
|
|||
const config = require('./webpack.config')
|
||||
|
||||
config.target = 'node'
|
||||
|
||||
module.exports = config
|
Loading…
Reference in a new issue