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:
|
bundler-audit:
|
||||||
enabled: true
|
enabled: true
|
||||||
duplication:
|
duplication:
|
||||||
enabled: true
|
enabled: false
|
||||||
config:
|
config:
|
||||||
languages:
|
languages:
|
||||||
count_threshold: 3 # rule of three
|
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.
|
# Ignore all logfiles and tempfiles.
|
||||||
log/*.log
|
log/*.log
|
||||||
tmp
|
tmp
|
||||||
|
.tmp
|
||||||
|
|
||||||
coverage
|
coverage
|
||||||
|
|
||||||
|
|
2
Gemfile
2
Gemfile
|
@ -51,4 +51,6 @@ group :development, :test do
|
||||||
gem 'pry-rails'
|
gem 'pry-rails'
|
||||||
gem 'rubocop'
|
gem 'rubocop'
|
||||||
gem 'tunemygc'
|
gem 'tunemygc'
|
||||||
|
gem 'faker'
|
||||||
|
gem 'timecop'
|
||||||
end
|
end
|
||||||
|
|
|
@ -109,6 +109,8 @@ GEM
|
||||||
factory_girl_rails (4.8.0)
|
factory_girl_rails (4.8.0)
|
||||||
factory_girl (~> 4.8.0)
|
factory_girl (~> 4.8.0)
|
||||||
railties (>= 3.0.0)
|
railties (>= 3.0.0)
|
||||||
|
faker (1.7.3)
|
||||||
|
i18n (~> 0.5)
|
||||||
globalid (0.3.7)
|
globalid (0.3.7)
|
||||||
activesupport (>= 4.1.0)
|
activesupport (>= 4.1.0)
|
||||||
httparty (0.14.0)
|
httparty (0.14.0)
|
||||||
|
@ -272,6 +274,7 @@ GEM
|
||||||
thor (0.19.4)
|
thor (0.19.4)
|
||||||
thread_safe (0.3.5)
|
thread_safe (0.3.5)
|
||||||
tilt (2.0.5)
|
tilt (2.0.5)
|
||||||
|
timecop (0.8.1)
|
||||||
tunemygc (1.0.69)
|
tunemygc (1.0.69)
|
||||||
tzinfo (1.2.2)
|
tzinfo (1.2.2)
|
||||||
thread_safe (~> 0.1)
|
thread_safe (~> 0.1)
|
||||||
|
@ -301,6 +304,7 @@ DEPENDENCIES
|
||||||
dotenv-rails
|
dotenv-rails
|
||||||
exception_notification
|
exception_notification
|
||||||
factory_girl_rails
|
factory_girl_rails
|
||||||
|
faker
|
||||||
httparty
|
httparty
|
||||||
jquery-rails
|
jquery-rails
|
||||||
jquery-ui-rails
|
jquery-ui-rails
|
||||||
|
@ -327,6 +331,7 @@ DEPENDENCIES
|
||||||
slack-notifier
|
slack-notifier
|
||||||
snorlax
|
snorlax
|
||||||
sucker_punch
|
sucker_punch
|
||||||
|
timecop
|
||||||
tunemygc
|
tunemygc
|
||||||
uglifier
|
uglifier
|
||||||
|
|
||||||
|
@ -334,4 +339,4 @@ RUBY VERSION
|
||||||
ruby 2.3.0p0
|
ruby 2.3.0p0
|
||||||
|
|
||||||
BUNDLED WITH
|
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);
|
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-permission:hover > ul,
|
||||||
.rightclickmenu .rc-metacode:hover > ul,
|
.rightclickmenu .rc-metacode:hover #metacodeOptions > ul,
|
||||||
.rightclickmenu .rc-siblings:hover > ul {
|
.rightclickmenu .rc-siblings:hover > ul {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
@ -1279,7 +1279,7 @@ h3.filterBox {
|
||||||
.rightclickmenu li.toPrivate .rc-perm-icon {
|
.rightclickmenu li.toPrivate .rc-perm-icon {
|
||||||
background-position: -24px 0;
|
background-position: -24px 0;
|
||||||
}
|
}
|
||||||
.rightclickmenu .rc-metacode > ul > li,
|
.rightclickmenu .rc-metacode #metacodeOptions > ul > li,
|
||||||
.rightclickmenu .rc-siblings > ul > li {
|
.rightclickmenu .rc-siblings > ul > li {
|
||||||
padding: 6px 24px 6px 8px;
|
padding: 6px 24px 6px 8px;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
@ -2311,6 +2311,9 @@ and it won't be important on password protected instances */
|
||||||
}
|
}
|
||||||
/* switch metacode set */
|
/* switch metacode set */
|
||||||
|
|
||||||
|
#switchMetacodes > p {
|
||||||
|
margin: 16px 0 16px 0;
|
||||||
|
}
|
||||||
#metacodeSwitchTabs {
|
#metacodeSwitchTabs {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
font-size: 17px;
|
font-size: 17px;
|
||||||
|
@ -2318,28 +2321,43 @@ and it won't be important on password protected instances */
|
||||||
border: none;
|
border: none;
|
||||||
background: none;
|
background: none;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
|
||||||
#metacodeSwitchTabs .setDesc {
|
.setDesc,
|
||||||
margin-bottom: 5px;
|
.selectAll,
|
||||||
font-family: 'din-medium', helvetica, sans-serif;
|
.selectNone {
|
||||||
color: #424242;
|
margin-bottom: 5px;
|
||||||
font-size: 14px;
|
font-family: 'din-medium', helvetica, sans-serif;
|
||||||
text-align: justify;
|
color: #424242;
|
||||||
padding-right: 16px;
|
font-size: 14px;
|
||||||
}
|
text-align: justify;
|
||||||
#switchMetacodes > p {
|
padding-right: 16px;
|
||||||
margin: 16px 0 16px 0;
|
display: inline-block;
|
||||||
}
|
}
|
||||||
#metacodeSwitchTabs > ul {
|
|
||||||
width: 130px;
|
.selectAll,
|
||||||
}
|
.selectNone {
|
||||||
#metacodeSwitchTabs > ul li {
|
float: right;
|
||||||
font-size: 14px;
|
cursor: pointer;
|
||||||
text-transform: uppercase;
|
|
||||||
}
|
&:hover,
|
||||||
#metacodeSwitchTabs li.ui-state-active a {
|
&.selected {
|
||||||
color: #00BCD4;
|
color: #00bcd4;
|
||||||
cursor: pointer;
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
& > ul {
|
||||||
|
width: 130px;
|
||||||
|
|
||||||
|
li {
|
||||||
|
font-size: 14px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
li.ui-state-active a {
|
||||||
|
color: #00BCD4;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.metacodeSwitchTab {
|
.metacodeSwitchTab {
|
||||||
max-height: 300px;
|
max-height: 300px;
|
||||||
|
@ -3121,3 +3139,13 @@ script.data-gratipay-username {
|
||||||
.inline {
|
.inline {
|
||||||
display: inline-block;
|
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;
|
font-family: helvetica;
|
||||||
color: #727272;
|
color: #727272;
|
||||||
line-height: 11px;
|
line-height: 11px;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.riek-editing + .nameCounter {
|
||||||
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nameCounter.forMap {
|
.nameCounter.forMap {
|
||||||
|
@ -14,14 +19,14 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.nameCounter.forTopic {
|
.nameCounter.forTopic {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#center-container {
|
#center-container {
|
||||||
position:relative;
|
position:relative;
|
||||||
height:100%;
|
height:100%;
|
||||||
width:100%;
|
width:100%;
|
||||||
|
|
||||||
/* background-color:#031924; */
|
/* background-color:#031924; */
|
||||||
color:#444;
|
color:#444;
|
||||||
}
|
}
|
||||||
|
@ -85,6 +90,11 @@
|
||||||
display: table-cell;
|
display: table-cell;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
padding: 0 16px;
|
padding: 0 16px;
|
||||||
|
|
||||||
|
&.riek-editing {
|
||||||
|
position: absolute;
|
||||||
|
top: 32px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.canEdit #titleActivator:hover {
|
.canEdit #titleActivator:hover {
|
||||||
background-image: url(<%= asset_data_uri('edit.png') %>);
|
background-image: url(<%= asset_data_uri('edit.png') %>);
|
||||||
|
@ -93,12 +103,12 @@
|
||||||
cursor: text;
|
cursor: text;
|
||||||
}
|
}
|
||||||
|
|
||||||
.showcard .best_in_place_name textarea, .showcard .best_in_place_name input {
|
.showcard .title .riek-editing {
|
||||||
font-family: 'din-regular', sans-serif;
|
font-family: 'din-regular', sans-serif;
|
||||||
color: #424242;
|
color: #424242;
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
line-height: 22px;
|
line-height: 22px;
|
||||||
height: 15px;
|
height: 3em;
|
||||||
padding: 5px 0;
|
padding: 5px 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
@ -122,7 +132,7 @@
|
||||||
height: auto;
|
height: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.CardOnGraph .best_in_place_desc textarea {
|
.CardOnGraph .desc .riek-editing {
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
line-height:15px;
|
line-height:15px;
|
||||||
font-family: helvetica, sans-serif;
|
font-family: helvetica, sans-serif;
|
||||||
|
@ -167,13 +177,14 @@
|
||||||
* End Markdown styling
|
* End Markdown styling
|
||||||
*/
|
*/
|
||||||
|
|
||||||
.CardOnGraph .best_in_place_desc {
|
.CardOnGraph .riek_desc {
|
||||||
display:block;
|
display:block;
|
||||||
margin-top:2px;
|
margin-top:2px;
|
||||||
padding-right: 18px;
|
padding-right: 18px;
|
||||||
margin-right: 8px;
|
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-image: url(<%= asset_data_uri('edit.png') %>);
|
||||||
background-position: top right;
|
background-position: top right;
|
||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
|
@ -185,155 +196,218 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.CardOnGraph .links {
|
.CardOnGraph .links {
|
||||||
position:relative;
|
position: relative;
|
||||||
border-bottom: 1px solid #BDBDBD;
|
border-bottom: 1px solid #BDBDBD;
|
||||||
border-top: 1px solid #BDBDBD;
|
border-top: 1px solid #BDBDBD;
|
||||||
background-color: #e0e0e0;
|
background-color: #e0e0e0;
|
||||||
}
|
|
||||||
|
|
||||||
.linkItem {
|
.linkItem {
|
||||||
float:left;
|
float: left;
|
||||||
height:46px;
|
z-index: 1;
|
||||||
z-index: 1;
|
position: relative;
|
||||||
position: relative;
|
color: #424242;
|
||||||
color: #424242;
|
font-size: 14px;
|
||||||
font-size: 14px;
|
line-height: 14px;
|
||||||
line-height:14px;
|
|
||||||
height:12px;
|
|
||||||
padding:17px 0;
|
|
||||||
}
|
|
||||||
.linkItem a {
|
|
||||||
color: #424242;
|
|
||||||
}
|
|
||||||
|
|
||||||
.CardOnGraph .icon {
|
a {
|
||||||
position:absolute;
|
color: #424242;
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
.contributor:hover .contributorName {
|
.icon {
|
||||||
display: block;
|
position: absolute;
|
||||||
}
|
z-index: 1;
|
||||||
|
padding: 0;
|
||||||
|
height: 48px;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
.contributorName {
|
.metacodeImage {
|
||||||
display: none;
|
cursor: move;
|
||||||
position: absolute;
|
position: relative;
|
||||||
background: black;
|
left: -23px;
|
||||||
text-align: center;
|
top: 1px;
|
||||||
color: white;
|
width: 46px;
|
||||||
border-radius: 2px;
|
height: 46px;
|
||||||
font-family: din-regular;
|
background-size:46px 46px;
|
||||||
line-height: 15px;
|
background-position:0 0;
|
||||||
font-size: 12px;
|
background-repeat:no-repeat;
|
||||||
padding: 3px 5px 2px;
|
}
|
||||||
white-space: nowrap;
|
}
|
||||||
margin-top: 36px;
|
|
||||||
margin-left: -32px;
|
.contributor {
|
||||||
}
|
bottom: 7px;
|
||||||
|
margin-left: 40px;
|
||||||
|
|
||||||
.contributor div:before {
|
.contributorIcon {
|
||||||
content: '';
|
position: relative;
|
||||||
position: absolute;
|
vertical-align: middle;
|
||||||
top: 128%;
|
border-radius: 16px;
|
||||||
left: 13px;
|
margin: 5px;
|
||||||
margin-top: -30px;
|
top: 8px;
|
||||||
width: 0;
|
left: 0;
|
||||||
height: 0;
|
border-radius: 16px;
|
||||||
border-bottom: 4px solid #000000;
|
}
|
||||||
border-left: 5px solid transparent;
|
|
||||||
border-right: 5px solid transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
.linkItem.mapCount {
|
span {
|
||||||
margin-left: 12px;
|
font-family: 'din-regular', sans-serif;
|
||||||
width: 24px;
|
font-size: 14px;
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
.linkItem.mapCount:hover .hoverTip {
|
.contributorName {
|
||||||
display: block;
|
display: none;
|
||||||
}
|
position: absolute;
|
||||||
.CardOnGraph .mapCount .tip, .CardonGraph .mapCount .hoverTip {
|
background: black;
|
||||||
top: 44px;
|
text-align: center;
|
||||||
left: 0px;
|
color: white;
|
||||||
font-size: 12px !important;
|
border-radius: 2px;
|
||||||
}
|
font-family: din-regular;
|
||||||
|
line-height: 15px;
|
||||||
|
font-size: 12px;
|
||||||
|
padding: 3px 5px 2px;
|
||||||
|
white-space: nowrap;
|
||||||
|
margin-top: 8px;
|
||||||
|
|
||||||
.hoverTip {
|
&:before {
|
||||||
white-space: nowrap;
|
content: '';
|
||||||
font-family: 'din-regular';
|
position: absolute;
|
||||||
top: 44px;
|
top: 26px;
|
||||||
left: 0px;
|
left: 10px;
|
||||||
font-size: 12px !important;
|
margin-top: -30px;
|
||||||
display: none;
|
width: 0;
|
||||||
position: absolute;
|
height: 0;
|
||||||
background: black;
|
border-bottom: 4px solid #000000;
|
||||||
color: white;
|
border-left: 5px solid transparent;
|
||||||
border-radius: 4px;
|
border-right: 5px solid transparent;
|
||||||
line-height: 17px;
|
}
|
||||||
padding: 3px 5px 2px;
|
}
|
||||||
z-index: 100;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
&:hover .contributorName {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.CardOnGraph .mapCount .tip:before, .CardOnGraph .mapCount .hoverTip:before {
|
.mapCount {
|
||||||
content: '';
|
padding:17px 0 17px 36px;
|
||||||
position: absolute;
|
margin-left: 12px;
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
.CardOnGraph .mapCount .tip li {
|
.mapCountIcon {
|
||||||
list-style-type: none;
|
position: absolute;
|
||||||
white-space: nowrap;
|
top: 8px;
|
||||||
overflow: hidden;
|
left: 0;
|
||||||
text-overflow: ellipsis;
|
width: 32px;
|
||||||
padding: 6px 10px;
|
height: 32px;
|
||||||
display: block;
|
background-image: url(<%= asset_data_uri('map32_sprite.png') %>);
|
||||||
height: 14px;
|
background-repeat: no-repeat;
|
||||||
font-family: 'din-regular', helvetica, sans-serif;
|
background-position: 0 0;
|
||||||
font-size: 14px;
|
cursor: pointer;
|
||||||
line-height: 14px;
|
}
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.CardOnGraph .mapCount li.hideExtra {
|
&:hover .mapCountIcon {
|
||||||
display: none;
|
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 {
|
.showMore {
|
||||||
|
@ -341,66 +415,6 @@
|
||||||
color: #4FC059;
|
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 {
|
.mapPerm {
|
||||||
width: 32px;
|
width: 32px;
|
||||||
height: 32px;
|
height: 32px;
|
||||||
|
@ -470,7 +484,7 @@ cursor: pointer;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
line-height: 24px;
|
line-height: 24px;
|
||||||
height:24px;
|
height: 26px;
|
||||||
font-size: 24px;
|
font-size: 24px;
|
||||||
display: none;
|
display: none;
|
||||||
width: 90%;
|
width: 90%;
|
||||||
|
@ -493,35 +507,25 @@ cursor: pointer;
|
||||||
background-position: 0 -32px;
|
background-position: 0 -32px;
|
||||||
}
|
}
|
||||||
.permission.canEdit .minimize .expandMetacodeSelect {
|
.permission.canEdit .minimize .expandMetacodeSelect {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.CardOnGraph .metacodeImage {
|
.CardOnGraph .metacodeName {
|
||||||
cursor:move;
|
display: inline-block;
|
||||||
width:46px;
|
|
||||||
height:46px;
|
|
||||||
position:absolute;
|
|
||||||
left:-23px;
|
|
||||||
top:0;
|
|
||||||
background-size:46px 46px;
|
|
||||||
background-position:0 0;
|
|
||||||
background-repeat:no-repeat;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#metacodeOptions {
|
|
||||||
display:none;
|
|
||||||
}
|
|
||||||
.CardOnGraph .metacodeSelect {
|
.CardOnGraph .metacodeSelect {
|
||||||
display:none;
|
display:none;
|
||||||
width:auto;
|
width:auto;
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
position: absolute;
|
|
||||||
background: #EAEAEA;
|
background: #EAEAEA;
|
||||||
left: 300px;
|
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
position: absolute;
|
||||||
|
left: 300px;
|
||||||
|
top: -1px;
|
||||||
}
|
}
|
||||||
.CardOnGraph .metacodeSelect ul {
|
.CardOnGraph .metacodeSelect ul {
|
||||||
position: relative;
|
position: relative;
|
||||||
line-height: 14px;
|
line-height: 14px;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-family: helvetica, sans-serif;
|
font-family: helvetica, sans-serif;
|
||||||
|
@ -610,7 +614,6 @@ background-color: #E0E0E0;
|
||||||
display:block;
|
display:block;
|
||||||
}
|
}
|
||||||
.CardOnGraph .tip {
|
.CardOnGraph .tip {
|
||||||
display:none;
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
background: black;
|
background: black;
|
||||||
top: 35px;
|
top: 35px;
|
||||||
|
@ -623,26 +626,24 @@ background-color: #E0E0E0;
|
||||||
z-index:100;
|
z-index:100;
|
||||||
}
|
}
|
||||||
|
|
||||||
#embedlyLink {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
#embedlyLinkLoader {
|
#embedlyLinkLoader {
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
width: 28px;
|
width: 28px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.CardOnGraph .attachments {
|
.CardOnGraph .link-adder {
|
||||||
border-top: 1px solid #BDBDBD;
|
|
||||||
width:100%;
|
width:100%;
|
||||||
height:47px;
|
height:47px;
|
||||||
|
position: relative;
|
||||||
|
border-top: 1px solid #BDBDBD;
|
||||||
}
|
}
|
||||||
|
|
||||||
.attachments a {
|
.link-adder a {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
display: block;
|
display: block;
|
||||||
margin-left: 40px;
|
margin-left: 40px;
|
||||||
padding-top:9px;
|
padding-top:9px;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
line-height: 16px;
|
line-height: 16px;
|
||||||
|
@ -652,7 +653,7 @@ background-color: #E0E0E0;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
width: 102px;
|
width: 102px;
|
||||||
height: 12px;
|
height: 12px;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
padding: 18px 0 18px 48px;
|
padding: 18px 0 18px 48px;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: #9e9e9e;
|
color: #9e9e9e;
|
||||||
|
@ -752,7 +753,6 @@ font-family: 'din-regular', helvetica, sans-serif;
|
||||||
-moz-border-radius-bottomright: 8px;
|
-moz-border-radius-bottomright: 8px;
|
||||||
-webkit-border-bottom-right-radius: 8px;
|
-webkit-border-bottom-right-radius: 8px;
|
||||||
border-bottom-right-radius: 8px;
|
border-bottom-right-radius: 8px;
|
||||||
display: none;
|
|
||||||
margin: 8px;
|
margin: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -839,10 +839,10 @@ font-family: 'din-regular', helvetica, sans-serif;
|
||||||
line-height: 16px;
|
line-height: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.canEdit #edit_synapse_desc:hover {
|
.canEdit span.titleWrapper:hover {
|
||||||
background-image: url(<%= asset_data_uri('edit.png') %>);
|
background-image: url(<%= asset_data_uri('edit.png') %>);
|
||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
background-position: 164px center;
|
background-position: 95% 95%;
|
||||||
cursor: text;
|
cursor: text;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -950,11 +950,11 @@ font-family: 'din-regular', helvetica, sans-serif;
|
||||||
}
|
}
|
||||||
#edit_synapse_right {
|
#edit_synapse_right {
|
||||||
background-image: url(<%= asset_data_uri('synapsedirectionright_sprite.png') %>);
|
background-image: url(<%= asset_data_uri('synapsedirectionright_sprite.png') %>);
|
||||||
right: 16px;
|
right: 16px;
|
||||||
}
|
}
|
||||||
#edit_synapse_left {
|
#edit_synapse_left {
|
||||||
background-image: url(<%= asset_data_uri('synapsedirectionleft_sprite.png') %>);
|
background-image: url(<%= asset_data_uri('synapsedirectionleft_sprite.png') %>);
|
||||||
right: 56px;
|
right: 56px;
|
||||||
}
|
}
|
||||||
#edit_synapse_left.checked, #edit_synapse_right.checked {
|
#edit_synapse_left.checked, #edit_synapse_right.checked {
|
||||||
background-position: 0 -48px;
|
background-position: 0 -48px;
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
class MapsController < ApplicationController
|
class MapsController < ApplicationController
|
||||||
before_action :require_user, only: [:create, :update, :destroy, :events]
|
before_action :require_user, only: [:create, :update, :destroy, :events, :follow, :unfollow]
|
||||||
before_action :set_map, only: [:show, :conversation, :update, :destroy, :contains, :events, :export]
|
before_action :set_map, only: [:show, :conversation, :update, :destroy, :contains, :events, :export, :follow, :unfollow, :unfollow_from_email]
|
||||||
after_action :verify_authorized
|
after_action :verify_authorized
|
||||||
|
|
||||||
# GET maps/:id
|
# GET maps/:id
|
||||||
|
@ -138,6 +138,43 @@ class MapsController < ApplicationController
|
||||||
end
|
end
|
||||||
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
|
private
|
||||||
|
|
||||||
def set_map
|
def set_map
|
||||||
|
|
|
@ -2,7 +2,10 @@
|
||||||
class TopicsController < ApplicationController
|
class TopicsController < ApplicationController
|
||||||
include TopicsHelper
|
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
|
after_action :verify_authorized, except: :autocomplete_topic
|
||||||
|
|
||||||
respond_to :html, :js, :json
|
respond_to :html, :js, :json
|
||||||
|
@ -31,9 +34,6 @@ class TopicsController < ApplicationController
|
||||||
|
|
||||||
# GET topics/:id
|
# GET topics/:id
|
||||||
def show
|
def show
|
||||||
@topic = Topic.find(params[:id])
|
|
||||||
authorize @topic
|
|
||||||
|
|
||||||
respond_to do |format|
|
respond_to do |format|
|
||||||
format.html do
|
format.html do
|
||||||
@alltopics = [@topic].concat(policy_scope(Topic.relatives(@topic.id, current_user)).to_a)
|
@alltopics = [@topic].concat(policy_scope(Topic.relatives(@topic.id, current_user)).to_a)
|
||||||
|
@ -49,9 +49,6 @@ class TopicsController < ApplicationController
|
||||||
|
|
||||||
# GET topics/:id/network
|
# GET topics/:id/network
|
||||||
def network
|
def network
|
||||||
@topic = Topic.find(params[:id])
|
|
||||||
authorize @topic
|
|
||||||
|
|
||||||
@alltopics = [@topic].concat(policy_scope(Topic.relatives(@topic.id, current_user)).to_a)
|
@alltopics = [@topic].concat(policy_scope(Topic.relatives(@topic.id, current_user)).to_a)
|
||||||
@allsynapses = policy_scope(Synapse.for_topic(@topic.id))
|
@allsynapses = policy_scope(Synapse.for_topic(@topic.id))
|
||||||
|
|
||||||
|
@ -71,9 +68,6 @@ class TopicsController < ApplicationController
|
||||||
|
|
||||||
# GET topics/:id/relative_numbers
|
# GET topics/:id/relative_numbers
|
||||||
def relative_numbers
|
def relative_numbers
|
||||||
@topic = Topic.find(params[:id])
|
|
||||||
authorize @topic
|
|
||||||
|
|
||||||
topics_already_has = params[:network] ? params[:network].split(',').map(&:to_i) : []
|
topics_already_has = params[:network] ? params[:network].split(',').map(&:to_i) : []
|
||||||
|
|
||||||
alltopics = policy_scope(Topic.relatives(@topic.id, current_user)).to_a
|
alltopics = policy_scope(Topic.relatives(@topic.id, current_user)).to_a
|
||||||
|
@ -94,9 +88,6 @@ class TopicsController < ApplicationController
|
||||||
|
|
||||||
# GET topics/:id/relatives
|
# GET topics/:id/relatives
|
||||||
def relatives
|
def relatives
|
||||||
@topic = Topic.find(params[:id])
|
|
||||||
authorize @topic
|
|
||||||
|
|
||||||
topics_already_has = params[:network] ? params[:network].split(',').map(&:to_i) : []
|
topics_already_has = params[:network] ? params[:network].split(',').map(&:to_i) : []
|
||||||
|
|
||||||
alltopics = policy_scope(Topic.relatives(@topic.id, current_user)).to_a
|
alltopics = policy_scope(Topic.relatives(@topic.id, current_user)).to_a
|
||||||
|
@ -149,8 +140,6 @@ class TopicsController < ApplicationController
|
||||||
# PUT /topics/1
|
# PUT /topics/1
|
||||||
# PUT /topics/1.json
|
# PUT /topics/1.json
|
||||||
def update
|
def update
|
||||||
@topic = Topic.find(params[:id])
|
|
||||||
authorize @topic
|
|
||||||
@topic.updated_by = current_user
|
@topic.updated_by = current_user
|
||||||
@topic.assign_attributes(topic_params)
|
@topic.assign_attributes(topic_params)
|
||||||
|
|
||||||
|
@ -165,8 +154,6 @@ class TopicsController < ApplicationController
|
||||||
|
|
||||||
# DELETE topics/:id
|
# DELETE topics/:id
|
||||||
def destroy
|
def destroy
|
||||||
@topic = Topic.find(params[:id])
|
|
||||||
authorize @topic
|
|
||||||
@topic.updated_by = current_user
|
@topic.updated_by = current_user
|
||||||
@topic.destroy
|
@topic.destroy
|
||||||
respond_to do |format|
|
respond_to do |format|
|
||||||
|
@ -174,8 +161,50 @@ class TopicsController < ApplicationController
|
||||||
end
|
end
|
||||||
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
|
private
|
||||||
|
|
||||||
|
def set_topic
|
||||||
|
@topic = Topic.find(params[:id])
|
||||||
|
authorize @topic
|
||||||
|
end
|
||||||
|
|
||||||
def topic_params
|
def topic_params
|
||||||
params.require(:topic).permit(:id, :name, :desc, :link, :permission, :metacode_id, :defer_to_map_id)
|
params.require(:topic).permit(:id, :name, :desc, :link, :permission, :metacode_id, :defer_to_map_id)
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,55 +1,5 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
module ApplicationHelper
|
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
|
def invite_link
|
||||||
"#{request.base_url}/join" + (current_user ? "?code=#{current_user.code}" : '')
|
"#{request.base_url}/join" + (current_user ? "?code=#{current_user.code}" : '')
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,3 +0,0 @@
|
||||||
# frozen_string_literal: true
|
|
||||||
module MetacodeSetsHelper
|
|
||||||
end
|
|
|
@ -1,3 +1,78 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
module MetacodesHelper
|
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
|
end
|
||||||
|
|
|
@ -3,10 +3,6 @@ class ApplicationMailer < ActionMailer::Base
|
||||||
default from: 'team@metamaps.cc'
|
default from: 'team@metamaps.cc'
|
||||||
layout 'mailer'
|
layout 'mailer'
|
||||||
|
|
||||||
def deliver
|
|
||||||
raise NotImplementedError('Please use Mailboxer to send your emails.')
|
|
||||||
end
|
|
||||||
|
|
||||||
class << self
|
class << self
|
||||||
def mail_for_notification(notification)
|
def mail_for_notification(notification)
|
||||||
case notification.notification_code
|
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,
|
topic2: mappable.topic2.filtered,
|
||||||
mapping_id: id
|
mapping_id: id
|
||||||
)
|
)
|
||||||
Events::SynapseAddedToMap.publish!(mappable, map, user, nil)
|
meta = { 'mapping_id': id }
|
||||||
|
Events::SynapseAddedToMap.publish!(mappable, map, user, meta)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def after_created_async
|
def after_created_async
|
||||||
FollowService.follow(map, user, 'contributed')
|
FollowService.follow(map, user, 'contributed')
|
||||||
end
|
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
|
ActionCable.server.broadcast 'map_' + map.id.to_s, type: 'topicMoved', id: mappable.id, mapping_id: id, x: xloc, y: yloc
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def after_updated_async
|
def after_updated_async
|
||||||
if (mappable_type == 'Topic') && (xloc_changed? || yloc_changed?)
|
if (mappable_type == 'Topic') && (xloc_changed? || yloc_changed?)
|
||||||
FollowService.follow(map, updated_by, 'contributed')
|
FollowService.follow(map, updated_by, 'contributed')
|
||||||
|
|
|
@ -68,13 +68,13 @@ class Synapse < ApplicationRecord
|
||||||
output += %(\n)
|
output += %(\n)
|
||||||
output
|
output
|
||||||
end
|
end
|
||||||
|
|
||||||
protected
|
protected
|
||||||
|
|
||||||
def set_perm_by_defer
|
def set_perm_by_defer
|
||||||
permission = defer_to_map.permission if defer_to_map
|
permission = defer_to_map.permission if defer_to_map
|
||||||
end
|
end
|
||||||
|
|
||||||
def after_created_async
|
def after_created_async
|
||||||
follow_ids = NotificationService.notify_followers(topic1, TOPIC_CONNECTED_1, self)
|
follow_ids = NotificationService.notify_followers(topic1, TOPIC_CONNECTED_1, self)
|
||||||
NotificationService.notify_followers(topic2, TOPIC_CONNECTED_2, self, nil, follow_ids)
|
NotificationService.notify_followers(topic2, TOPIC_CONNECTED_2, self, nil, follow_ids)
|
||||||
|
@ -93,7 +93,7 @@ class Synapse < ApplicationRecord
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def before_destroyed
|
def before_destroyed
|
||||||
# hard to know how to do this yet, because the synapse actually gets destroyed
|
# hard to know how to do this yet, because the synapse actually gets destroyed
|
||||||
#NotificationService.notify_followers(topic1, 'topic_disconnected', self)
|
#NotificationService.notify_followers(topic1, 'topic_disconnected', self)
|
||||||
|
|
|
@ -52,10 +52,20 @@ class User < ApplicationRecord
|
||||||
|
|
||||||
# override default as_json
|
# override default as_json
|
||||||
def as_json(_options = {})
|
def as_json(_options = {})
|
||||||
{ id: id,
|
json = { id: id,
|
||||||
name: name,
|
name: name,
|
||||||
image: image.url(:sixtyfour),
|
image: image.url(:sixtyfour),
|
||||||
admin: admin }
|
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
|
end
|
||||||
|
|
||||||
def as_json_for_autocomplete
|
def as_json_for_autocomplete
|
||||||
|
|
|
@ -90,4 +90,16 @@ class MapPolicy < ApplicationPolicy
|
||||||
def unstar?
|
def unstar?
|
||||||
user.present?
|
user.present?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def follow?
|
||||||
|
show? && user.present?
|
||||||
|
end
|
||||||
|
|
||||||
|
def unfollow?
|
||||||
|
user.present?
|
||||||
|
end
|
||||||
|
|
||||||
|
def unfollow_from_email?
|
||||||
|
user.present?
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -55,6 +55,18 @@ class TopicPolicy < ApplicationPolicy
|
||||||
show?
|
show?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def follow?
|
||||||
|
show? && user.present?
|
||||||
|
end
|
||||||
|
|
||||||
|
def unfollow?
|
||||||
|
user.present?
|
||||||
|
end
|
||||||
|
|
||||||
|
def unfollow_from_email?
|
||||||
|
user.present?
|
||||||
|
end
|
||||||
|
|
||||||
# Helpers
|
# Helpers
|
||||||
def map_policy
|
def map_policy
|
||||||
@map_policy ||= Pundit.policy(user, record.defer_to_map)
|
@map_policy ||= Pundit.policy(user, record.defer_to_map)
|
||||||
|
|
|
@ -10,11 +10,11 @@ class FollowService
|
||||||
follow.follow_reason.update_attribute(reason, true)
|
follow.follow_reason.update_attribute(reason, true)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def unfollow(entity, user)
|
def unfollow(entity, user)
|
||||||
Follow.where(followed: entity, user: user).destroy_all
|
Follow.where(followed: entity, user: user).destroy_all
|
||||||
end
|
end
|
||||||
|
|
||||||
def remove_reason(entity, user, reason)
|
def remove_reason(entity, user, reason)
|
||||||
return unless FollowReason::REASONS.include?(reason)
|
return unless FollowReason::REASONS.include?(reason)
|
||||||
follow = Follow.where(followed: entity, user: user).first
|
follow = Follow.where(followed: entity, user: user).first
|
||||||
|
@ -25,9 +25,9 @@ class FollowService
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
protected
|
protected
|
||||||
|
|
||||||
def is_tester(user)
|
def is_tester(user)
|
||||||
%w(connorturland@gmail.com devin@callysto.com chessscholar@gmail.com solaureum@gmail.com ishanshapiro@gmail.com).include?(user.email)
|
%w(connorturland@gmail.com devin@callysto.com chessscholar@gmail.com solaureum@gmail.com ishanshapiro@gmail.com).include?(user.email)
|
||||||
end
|
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' %>
|
<%= render :partial => 'shared/metacodeBgColors' %>
|
||||||
<script type="text/javascript" charset="utf-8">
|
<script type="text/javascript" charset="utf-8">
|
||||||
<% if current_user %>
|
<% 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 %>
|
<% else %>
|
||||||
Metamaps.ServerData.ActiveMapper = null
|
Metamaps.ServerData.ActiveMapper = null
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
<script type="text/template" id="mapInfoBoxTemplate">
|
<script type="text/template" id="mapInfoBoxTemplate">
|
||||||
<div class="requestTitle">Click here to name this map</div>
|
<div class="requestTitle">Click here to name this map</div>
|
||||||
<div class="mapInfoName" id="mapInfoName">{{{name}}}</div>
|
<div class="mapInfoName" id="mapInfoName">{{{name}}}</div>
|
||||||
|
|
||||||
<div class="mapInfoStat">
|
<div class="mapInfoStat">
|
||||||
<div class="infoStatIcon mapContributors hoverForTip">
|
<div class="infoStatIcon mapContributors hoverForTip">
|
||||||
<img id="mapContribs" class="{{contributors_class}}"
|
<img id="mapContribs" class="{{contributors_class}}"
|
||||||
|
@ -29,7 +29,7 @@
|
||||||
<div class="mapInfoDesc" id="mapInfoDesc">
|
<div class="mapInfoDesc" id="mapInfoDesc">
|
||||||
{{{desc}}}
|
{{{desc}}}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mapInfoMeta">
|
<div class="mapInfoMeta">
|
||||||
<p class="mapCreatedAt"><span>Created by:</span> {{user_name}} on {{created_at}}</p>
|
<p class="mapCreatedAt"><span>Created by:</span> {{user_name}} on {{created_at}}</p>
|
||||||
<p class="mapEditedAt"><span>Last edited:</span> {{updated_at}}</p>
|
<p class="mapEditedAt"><span>Last edited:</span> {{updated_at}}</p>
|
||||||
|
@ -50,7 +50,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script type="text/template" id="topicSearchTemplate">
|
<script type="text/template" id="topicSearchTemplate">
|
||||||
<div class="result{{rtype}}">
|
<div class="result{{rtype}}">
|
||||||
<div class="topicMetacode searchResIconWrapper">
|
<div class="topicMetacode searchResIconWrapper">
|
||||||
|
@ -100,7 +100,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="mapContributorsIcon hoverForTip">
|
<div class="mapContributorsIcon hoverForTip">
|
||||||
<img id="mapContribs" width="25" height="25" src="{{mapContributorImage}}" />
|
<img id="mapContribs" width="25" height="25" src="{{mapContributorImage}}" />
|
||||||
<div class="tip">
|
<div class="tip">
|
||||||
<ul>
|
<ul>
|
||||||
{{{contributorTip}}}
|
{{{contributorTip}}}
|
||||||
</ul>
|
</ul>
|
||||||
|
@ -181,72 +181,4 @@
|
||||||
<div class="clearfloat"></div>
|
<div class="clearfloat"></div>
|
||||||
</div>
|
</div>
|
||||||
</script>
|
</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>
|
</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>
|
</ul>
|
||||||
|
|
||||||
<div id="csTopicView">
|
<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">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">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">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>
|
||||||
|
|
||||||
<div id="csCreatingTopics">
|
<div id="csCreatingTopics">
|
||||||
|
@ -44,19 +46,19 @@
|
||||||
|
|
||||||
<div id="csEditingTopics">
|
<div id="csEditingTopics">
|
||||||
<div class="csItem">
|
<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>
|
||||||
<div class="csItem indented">
|
<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>
|
||||||
<div class="csItem indented">
|
<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>
|
||||||
<div class="csItem indented">
|
<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>
|
||||||
<div class="csItem indented">
|
<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>
|
||||||
<div class="csItem indented">
|
<div class="csItem indented">
|
||||||
<span class="csTitle">Change Topic permission:</span> Click on 'Permission' icon (only for topic creator)
|
<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
|
<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>
|
||||||
<div class="csItem indented">
|
<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>
|
||||||
<div class="csItem">
|
<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>
|
||||||
<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 class="csItem"><br><a href="https://docs.metamaps.cc/creating_topics.html" target= "_blank">Learn More</a></div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="csCreatingSynapses">
|
<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"><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">Enter a label</span> Begin typing (or leave blank)</div>
|
||||||
<div class="csItem indented"><span class="csTitle">Esc or Delete:</span> Cancel synapse creation</div>
|
<div class="csItem indented"><span class="csTitle">Confirm new Synapse:</span> Enter or Tab</div>
|
||||||
<div class="csItem indented">*You do not have to add a description</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 topic to open canvas</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">Enter:</span> Create topic</div>
|
<div class="csItem indented"><span class="csTitle">Create Topic:</span> Same as elsewhere</div>
|
||||||
<div class="csItem indented"><span class="csTitle">Enter:</span> Create synapse</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 class="csItem"><br><a href="https://docs.metamaps.cc/creating_synapses.html" target= "_blank">Learn More</a></div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="csEditingSynapses">
|
<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">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">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">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">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"><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 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>
|
<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 id="csNavigation">
|
||||||
<div class="csItem"><span class="csTitle">Move around Canvas:</span> Click and drag</div>
|
<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 indented"><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 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 class="csItem"><br><a href="https://docs.metamaps.cc/exploring_maps.html" target= "_blank">Learn More</a></div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
@ -111,8 +114,8 @@
|
||||||
<div id="csSelection">
|
<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 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/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">Select multiple Topics/Synapses:</span> Shift + click to include each</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 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">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"><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>
|
<div class="csItem indented">*Hide/Remove/Delete/Change permissions of multiple topics & synapses within context menu</div>
|
||||||
|
@ -121,11 +124,10 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="csSearch">
|
<div id="csSearch">
|
||||||
<div class="csItem"><span class="csTitle">Open 'Search' prompt:</span> Ctrl + /</div>
|
<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">Close 'Search' prompt:</span> Esc</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>
|
||||||
<% if controller_name == "maps" && action_name == "show" %>
|
<div class="csItem"><span class="csTitle">Add Topic to current Map:</span> Click "+" on a topic result</div>
|
||||||
<div class="csItem"><span class="csTitle">Add 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>
|
||||||
<% end %>
|
|
||||||
<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 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 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>
|
<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">
|
<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>
|
</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
|
# this code generates the list of icons that will drop down in the metacode select list on the topic card
|
||||||
#%>
|
#%>
|
||||||
|
|
||||||
<div id="metacodeOptions">
|
<script>
|
||||||
<ul>
|
Metamaps.ServerData = Metamaps.ServerData || {}
|
||||||
<li>
|
Metamaps.ServerData.metacodeSets = <%= raw metacode_sets_json %>
|
||||||
<span>Recently Used</span>
|
</script>
|
||||||
<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>
|
|
||||||
|
|
|
@ -91,7 +91,9 @@
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
<div id="metacodeSwitchTabsCustom">
|
<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 = '' %>
|
<% @list = '' %>
|
||||||
<% metacodesInUse = user_metacodes() %>
|
<% metacodesInUse = user_metacodes() %>
|
||||||
<% Metacode.order("name").all.each_with_index do |m, index| %>
|
<% Metacode.order("name").all.each_with_index do |m, index| %>
|
||||||
|
@ -116,4 +118,4 @@
|
||||||
<script>
|
<script>
|
||||||
Metamaps.Create.selectedMetacodeSet = "metacodeset-<%= selectedSet %>"
|
Metamaps.Create.selectedMetacodeSet = "metacodeset-<%= selectedSet %>"
|
||||||
Metamaps.Create.selectedMetacodeSetIndex = <%= index %>
|
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>
|
</p>
|
||||||
|
|
||||||
<%= link_to 'Go to Topic', topic_url(topic), style: button_style %>
|
<%= 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 %>
|
<%= event.user.name %> added topic <%= topic.name %> to map <%= event.map.name %>
|
||||||
|
|
||||||
topic_url(topic)
|
topic_url(topic)
|
||||||
map_url(event.map)
|
map_url(event.map)
|
||||||
|
|
||||||
|
<%= render :partial => 'topic_mailer/unfollow', locals: { topic: topic } %>
|
|
@ -12,4 +12,6 @@
|
||||||
<% end %>
|
<% end %>
|
||||||
</p>
|
</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.user.name %> connected topic <%= topic1.name %> to topic <%= topic2.name %>
|
||||||
<%= synapse.desc.length > 0 ? ' with the description' + synapse.desc : '' %>
|
<%= 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'
|
MAP_INVITE_TO_EDIT = 'INVITE_TO_EDIT'
|
||||||
|
|
||||||
# these ones are new
|
# these ones are new
|
||||||
# this one's a catch all for occurences on the map
|
|
||||||
# MAP_ACTIVITY = 'MAP_ACTIVITY'
|
|
||||||
# MAP_RECEIVED_TOPIC
|
# MAP_RECEIVED_TOPIC
|
||||||
# MAP_LOST_TOPIC
|
# MAP_LOST_TOPIC
|
||||||
# MAP_TOPIC_MOVED
|
# MAP_TOPIC_MOVED
|
||||||
|
|
|
@ -48,6 +48,9 @@ Metamaps::Application.routes.draw do
|
||||||
|
|
||||||
post :star, to: 'stars#create', default: { format: :json }
|
post :star, to: 'stars#create', default: { format: :json }
|
||||||
post :unstar, to: 'stars#destroy', 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
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -83,6 +86,9 @@ Metamaps::Application.routes.draw do
|
||||||
get :network
|
get :network
|
||||||
get :relative_numbers
|
get :relative_numbers
|
||||||
get :relatives
|
get :relatives
|
||||||
|
post :follow, default: { format: :json }
|
||||||
|
post :unfollow, default: { format: :json }
|
||||||
|
get :unfollow_from_email
|
||||||
end
|
end
|
||||||
collection do
|
collection do
|
||||||
get :autocomplete_topic
|
get :autocomplete_topic
|
||||||
|
|
|
@ -28,6 +28,8 @@ const Create = {
|
||||||
}).addClass('ui-tabs-vertical ui-helper-clearfix')
|
}).addClass('ui-tabs-vertical ui-helper-clearfix')
|
||||||
$('#metacodeSwitchTabs .ui-tabs-nav li').removeClass('ui-corner-top').addClass('ui-corner-left')
|
$('#metacodeSwitchTabs .ui-tabs-nav li').removeClass('ui-corner-top').addClass('ui-corner-left')
|
||||||
$('.customMetacodeList li').click(self.toggleMetacodeSelected) // within the custom metacode set tab
|
$('.customMetacodeList li').click(self.toggleMetacodeSelected) // within the custom metacode set tab
|
||||||
|
$('.selectAll').click(self.metacodeSelectorSelectAll)
|
||||||
|
$('.selectNone').click(self.metacodeSelectorSelectNone)
|
||||||
},
|
},
|
||||||
toggleMetacodeSelected: function() {
|
toggleMetacodeSelected: function() {
|
||||||
var self = Create
|
var self = Create
|
||||||
|
@ -43,6 +45,46 @@ const Create = {
|
||||||
self.newSelectedMetacodes.push($(this).attr('id'))
|
self.newSelectedMetacodes.push($(this).attr('id'))
|
||||||
self.newSelectedMetacodeNames.push($(this).attr('data-name'))
|
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) {
|
updateMetacodeSet: function(set, index, custom) {
|
||||||
if (custom && Create.newSelectedMetacodes.length === 0) {
|
if (custom && Create.newSelectedMetacodes.length === 0) {
|
||||||
|
@ -114,7 +156,6 @@ const Create = {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
cancelMetacodeSetSwitch: function() {
|
cancelMetacodeSetSwitch: function() {
|
||||||
var self = Create
|
var self = Create
|
||||||
self.isSwitchingSet = false
|
self.isSwitchingSet = false
|
||||||
|
|
|
@ -34,6 +34,9 @@ const Map = Backbone.Model.extend({
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
isFollowedBy: function(mapper) {
|
||||||
|
return mapper.get('follows') && mapper.get('follows').maps.indexOf(this.get('id')) > -1
|
||||||
|
},
|
||||||
getUser: function() {
|
getUser: function() {
|
||||||
return Mapper.get(this.get('user_id'))
|
return Mapper.get(this.get('user_id'))
|
||||||
},
|
},
|
||||||
|
|
|
@ -5,7 +5,7 @@ import outdent from 'outdent'
|
||||||
|
|
||||||
const Mapper = Backbone.Model.extend({
|
const Mapper = Backbone.Model.extend({
|
||||||
urlRoot: '/users',
|
urlRoot: '/users',
|
||||||
blacklist: ['created_at', 'updated_at'],
|
blacklist: ['created_at', 'updated_at', 'follows'],
|
||||||
toJSON: function(options) {
|
toJSON: function(options) {
|
||||||
return _.omit(this.attributes, this.blacklist)
|
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')}" />
|
<img src="${this.get('image')}" data-id="${this.id}" alt="${this.get('name')}" />
|
||||||
<p>${this.get('name')}</p>
|
<p>${this.get('name')}</p>
|
||||||
</li>`
|
</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 Active from '../Active'
|
||||||
import Filter from '../Filter'
|
import Filter from '../Filter'
|
||||||
import TopicCard from '../TopicCard'
|
import TopicCard from '../Views/TopicCard'
|
||||||
import Visualize from '../Visualize'
|
import Visualize from '../Visualize'
|
||||||
|
|
||||||
import DataModel from './index'
|
import DataModel from './index'
|
||||||
|
@ -47,6 +47,9 @@ const Topic = Backbone.Model.extend({
|
||||||
if (mapper && this.get('user_id') === mapper.get('id')) return true
|
if (mapper && this.get('user_id') === mapper.get('id')) return true
|
||||||
else return false
|
else return false
|
||||||
},
|
},
|
||||||
|
isFollowedBy: function(mapper) {
|
||||||
|
return mapper.get('follows') && mapper.get('follows').topics.indexOf(this.get('id')) > -1
|
||||||
|
},
|
||||||
getDate: function() {},
|
getDate: function() {},
|
||||||
getMetacode: function() {
|
getMetacode: function() {
|
||||||
return DataModel.Metacodes.get(this.get('metacode_id'))
|
return DataModel.Metacodes.get(this.get('metacode_id'))
|
||||||
|
|
|
@ -26,7 +26,10 @@ const ImportDialog = {
|
||||||
ReactDOM.render(React.createElement(ImportDialogBox, {
|
ReactDOM.render(React.createElement(ImportDialogBox, {
|
||||||
onFileAdded: PasteInput.handleFile,
|
onFileAdded: PasteInput.handleFile,
|
||||||
exampleImageUrl: serverData['import-example.png'],
|
exampleImageUrl: serverData['import-example.png'],
|
||||||
downloadScreenshot: ImportDialog.downloadScreenshot
|
downloadScreenshot: ImportDialog.downloadScreenshot,
|
||||||
|
onExport: format => {
|
||||||
|
window.open(`${window.location.pathname}/export.${format}`, '_blank')
|
||||||
|
}
|
||||||
}), $('.importDialogWrapper').get(0))
|
}), $('.importDialogWrapper').get(0))
|
||||||
},
|
},
|
||||||
show: function() {
|
show: function() {
|
||||||
|
|
|
@ -29,8 +29,8 @@ const Import = {
|
||||||
handleCSV: function(text, parserOpts = {}) {
|
handleCSV: function(text, parserOpts = {}) {
|
||||||
const self = Import
|
const self = Import
|
||||||
|
|
||||||
const topicsRegex = /("?Topics"?)([\s\S]*)/mi
|
const topicsRegex = /("?Topics"?[, \t"]*)([\s\S]*)/mi
|
||||||
const synapsesRegex = /("?Synapses"?)([\s\S]*)/mi
|
const synapsesRegex = /("?Synapses"?[, \t"]*)([\s\S]*)/mi
|
||||||
let topicsText = text.match(topicsRegex) || ''
|
let topicsText = text.match(topicsRegex) || ''
|
||||||
if (topicsText) topicsText = topicsText[2].replace(synapsesRegex, '')
|
if (topicsText) topicsText = topicsText[2].replace(synapsesRegex, '')
|
||||||
let synapsesText = text.match(synapsesRegex) || ''
|
let synapsesText = text.match(synapsesRegex) || ''
|
||||||
|
|
|
@ -2,9 +2,14 @@
|
||||||
|
|
||||||
import _ from 'lodash'
|
import _ from 'lodash'
|
||||||
import outdent from 'outdent'
|
import outdent from 'outdent'
|
||||||
|
import clipboard from 'clipboard-js'
|
||||||
|
import React from 'react'
|
||||||
|
import ReactDOM from 'react-dom'
|
||||||
|
|
||||||
import $jit from '../patched/JIT'
|
import $jit from '../patched/JIT'
|
||||||
|
|
||||||
|
import MetacodeSelect from '../components/MetacodeSelect'
|
||||||
|
|
||||||
import Active from './Active'
|
import Active from './Active'
|
||||||
import Control from './Control'
|
import Control from './Control'
|
||||||
import Create from './Create'
|
import Create from './Create'
|
||||||
|
@ -18,10 +23,9 @@ import Settings from './Settings'
|
||||||
import Synapse from './Synapse'
|
import Synapse from './Synapse'
|
||||||
import SynapseCard from './SynapseCard'
|
import SynapseCard from './SynapseCard'
|
||||||
import Topic from './Topic'
|
import Topic from './Topic'
|
||||||
import TopicCard from './TopicCard'
|
import TopicCard from './Views/TopicCard'
|
||||||
import Util from './Util'
|
import Util from './Util'
|
||||||
import Visualize from './Visualize'
|
import Visualize from './Visualize'
|
||||||
import clipboard from 'clipboard-js'
|
|
||||||
|
|
||||||
let panningInt
|
let panningInt
|
||||||
|
|
||||||
|
@ -1418,9 +1422,7 @@ const JIT = {
|
||||||
<div class="expandLi"></div>
|
<div class="expandLi"></div>
|
||||||
</li>`
|
</li>`
|
||||||
|
|
||||||
const metacodeOptions = $('#metacodeOptions').html()
|
menustring += '<li class="rc-metacode"><div class="rc-icon"></div>Change metacode<div id="metacodeOptionsWrapper"></div><div class="expandLi"></div></li>'
|
||||||
|
|
||||||
menustring += '<li class="rc-metacode"><div class="rc-icon"></div>Change metacode' + metacodeOptions + '<div class="expandLi"></div></li>'
|
|
||||||
}
|
}
|
||||||
if (Active.Topic) {
|
if (Active.Topic) {
|
||||||
if (!Active.Mapper) {
|
if (!Active.Mapper) {
|
||||||
|
@ -1475,6 +1477,25 @@ const JIT = {
|
||||||
// add the menu to the page
|
// add the menu to the page
|
||||||
$('#wrapper').append(rightclickmenu)
|
$('#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
|
// attach events to clicks on the list items
|
||||||
|
|
||||||
// delete the selected things from the database
|
// delete the selected things from the database
|
||||||
|
@ -1521,13 +1542,6 @@ const JIT = {
|
||||||
Control.updateSelectedPermissions($(this).text())
|
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
|
// fetch relatives
|
||||||
let fetchSent = false
|
let fetchSent = false
|
||||||
$('.rc-siblings').hover(function() {
|
$('.rc-siblings').hover(function() {
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
/* global $ */
|
/* global $ */
|
||||||
|
|
||||||
import Active from './Active'
|
import Active from './Active'
|
||||||
|
import Create from './Create'
|
||||||
import Control from './Control'
|
import Control from './Control'
|
||||||
import DataModel from './DataModel'
|
import DataModel from './DataModel'
|
||||||
import JIT from './JIT'
|
import JIT from './JIT'
|
||||||
|
@ -31,11 +32,18 @@ const Listeners = {
|
||||||
JIT.escKeyHandler()
|
JIT.escKeyHandler()
|
||||||
break
|
break
|
||||||
case 46: // if DEL is pressed
|
case 46: // if DEL is pressed
|
||||||
e.preventDefault()
|
if(e.target.tagName !== "INPUT" && e.target.tagName !== "TEXTAREA" && (Selected.Nodes.length + Selected.Edges.length) > 0){
|
||||||
Control.deleteSelected()
|
e.preventDefault()
|
||||||
|
Control.removeSelectedNodes()
|
||||||
|
Control.removeSelectedEdges()
|
||||||
|
}
|
||||||
break
|
break
|
||||||
case 65: // if a or A is pressed
|
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 nodesCount = Object.keys(Visualize.mGraph.graph.nodes).length
|
||||||
const selectedNodesCount = Selected.Nodes.length
|
const selectedNodesCount = Selected.Nodes.length
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
|
|
|
@ -16,7 +16,7 @@ import Realtime from '../Realtime'
|
||||||
import Router from '../Router'
|
import Router from '../Router'
|
||||||
import Selected from '../Selected'
|
import Selected from '../Selected'
|
||||||
import SynapseCard from '../SynapseCard'
|
import SynapseCard from '../SynapseCard'
|
||||||
import TopicCard from '../TopicCard'
|
import TopicCard from '../Views/TopicCard'
|
||||||
import Visualize from '../Visualize'
|
import Visualize from '../Visualize'
|
||||||
|
|
||||||
import CheatSheet from './CheatSheet'
|
import CheatSheet from './CheatSheet'
|
||||||
|
|
|
@ -20,6 +20,10 @@ const PasteInput = {
|
||||||
}, false)
|
}, false)
|
||||||
window.addEventListener('drop', function(e) {
|
window.addEventListener('drop', function(e) {
|
||||||
e = e || window.event
|
e = e || window.event
|
||||||
|
|
||||||
|
// prevent conflict with react-dropzone file uploader
|
||||||
|
if (event.target.id !== 'infovis-canvas') return
|
||||||
|
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
var coords = Util.pixelsToCoords(Visualize.mGraph, { x: e.clientX, y: e.clientY })
|
var coords = Util.pixelsToCoords(Visualize.mGraph, { x: e.clientX, y: e.clientY })
|
||||||
if (e.dataTransfer.files.length > 0) {
|
if (e.dataTransfer.files.length > 0) {
|
||||||
|
|
|
@ -14,7 +14,7 @@ import Router from './Router'
|
||||||
import Selected from './Selected'
|
import Selected from './Selected'
|
||||||
import Settings from './Settings'
|
import Settings from './Settings'
|
||||||
import SynapseCard from './SynapseCard'
|
import SynapseCard from './SynapseCard'
|
||||||
import TopicCard from './TopicCard'
|
import TopicCard from './Views/TopicCard'
|
||||||
import Util from './Util'
|
import Util from './Util'
|
||||||
import Visualize from './Visualize'
|
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 $ */
|
/* global $ */
|
||||||
|
|
||||||
import { Parser, HtmlRenderer } from 'commonmark'
|
import { Parser, HtmlRenderer, Node } from 'commonmark'
|
||||||
import { emojiIndex } from 'emoji-mart'
|
import { emojiIndex } from 'emoji-mart'
|
||||||
import { escapeRegExp } from 'lodash'
|
import { escapeRegExp } from 'lodash'
|
||||||
|
|
||||||
|
@ -135,9 +135,26 @@ const Util = {
|
||||||
},
|
},
|
||||||
mdToHTML: text => {
|
mdToHTML: text => {
|
||||||
const safeText = 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
|
// use safe: true to filter xss
|
||||||
return new HtmlRenderer({ safe: true })
|
return new HtmlRenderer({ safe: true }).render(parsed)
|
||||||
.render(new Parser().parse(safeText))
|
|
||||||
},
|
},
|
||||||
logCanvasAttributes: function(canvas) {
|
logCanvasAttributes: function(canvas) {
|
||||||
const fakeMgraph = { canvas }
|
const fakeMgraph = { canvas }
|
||||||
|
@ -181,6 +198,37 @@ const Util = {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return text
|
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`
|
url: `/maps/${map.id}/access_request`
|
||||||
})
|
})
|
||||||
GlobalUI.notifyUser('You will be notified by email if request accepted')
|
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(
|
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 ChatView from './ChatView'
|
||||||
import VideoView from './VideoView'
|
import VideoView from './VideoView'
|
||||||
import Room from './Room'
|
import Room from './Room'
|
||||||
|
import TopicCard from './TopicCard'
|
||||||
import { JUNTO_UPDATED } from '../Realtime/events'
|
import { JUNTO_UPDATED } from '../Realtime/events'
|
||||||
|
|
||||||
const Views = {
|
const Views = {
|
||||||
init: (serverData) => {
|
init: (serverData) => {
|
||||||
$(document).on(JUNTO_UPDATED, () => ExploreMaps.render())
|
$(document).on(JUNTO_UPDATED, () => ExploreMaps.render())
|
||||||
ChatView.init([serverData['sounds/MM_sounds.mp3'], serverData['sounds/MM_sounds.ogg']])
|
ChatView.init([serverData['sounds/MM_sounds.mp3'], serverData['sounds/MM_sounds.ogg']])
|
||||||
|
TopicCard.init(serverData)
|
||||||
},
|
},
|
||||||
ExploreMaps,
|
ExploreMaps,
|
||||||
ChatView,
|
ChatView,
|
||||||
VideoView,
|
VideoView,
|
||||||
Room
|
Room,
|
||||||
|
TopicCard
|
||||||
}
|
}
|
||||||
|
|
||||||
export { ExploreMaps, ChatView, VideoView, Room }
|
export { ExploreMaps, ChatView, VideoView, Room, TopicCard }
|
||||||
export default Views
|
export default Views
|
||||||
|
|
|
@ -9,7 +9,7 @@ import DataModel from './DataModel'
|
||||||
import JIT from './JIT'
|
import JIT from './JIT'
|
||||||
import Loading from './Loading'
|
import Loading from './Loading'
|
||||||
import Router from './Router'
|
import Router from './Router'
|
||||||
import TopicCard from './TopicCard'
|
import TopicCard from './Views/TopicCard'
|
||||||
|
|
||||||
const Visualize = {
|
const Visualize = {
|
||||||
mGraph: null, // a reference to the graph object.
|
mGraph: null, // a reference to the graph object.
|
||||||
|
|
|
@ -29,7 +29,6 @@ import Settings from './Settings'
|
||||||
import Synapse from './Synapse'
|
import Synapse from './Synapse'
|
||||||
import SynapseCard from './SynapseCard'
|
import SynapseCard from './SynapseCard'
|
||||||
import Topic from './Topic'
|
import Topic from './Topic'
|
||||||
import TopicCard from './TopicCard'
|
|
||||||
import Util from './Util'
|
import Util from './Util'
|
||||||
import Views from './Views'
|
import Views from './Views'
|
||||||
import Visualize from './Visualize'
|
import Visualize from './Visualize'
|
||||||
|
@ -71,7 +70,6 @@ Metamaps.Settings = Settings
|
||||||
Metamaps.Synapse = Synapse
|
Metamaps.Synapse = Synapse
|
||||||
Metamaps.SynapseCard = SynapseCard
|
Metamaps.SynapseCard = SynapseCard
|
||||||
Metamaps.Topic = Topic
|
Metamaps.Topic = Topic
|
||||||
Metamaps.TopicCard = TopicCard
|
|
||||||
Metamaps.Util = Util
|
Metamaps.Util = Util
|
||||||
Metamaps.Views = Views
|
Metamaps.Views = Views
|
||||||
Metamaps.Visualize = Visualize
|
Metamaps.Visualize = Visualize
|
||||||
|
|
|
@ -2,18 +2,8 @@ import React, { PropTypes, Component } from 'react'
|
||||||
import Dropzone from 'react-dropzone'
|
import Dropzone from 'react-dropzone'
|
||||||
|
|
||||||
class ImportDialogBox extends Component {
|
class ImportDialogBox extends Component {
|
||||||
constructor(props) {
|
|
||||||
super(props)
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
handleExport = format => () => {
|
|
||||||
window.open(`${window.location.pathname}/export.${format}`, '_blank')
|
|
||||||
}
|
|
||||||
|
|
||||||
handleFile = (files, e) => {
|
handleFile = (files, e) => {
|
||||||
|
e.preventDefault() // prevent it from triggering the default drag-drop handler
|
||||||
this.props.onFileAdded(files[0])
|
this.props.onFileAdded(files[0])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -21,13 +11,13 @@ class ImportDialogBox extends Component {
|
||||||
return (
|
return (
|
||||||
<div className="import-dialog">
|
<div className="import-dialog">
|
||||||
<h3>EXPORT</h3>
|
<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
|
Export as CSV
|
||||||
</div>
|
</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
|
Export as JSON
|
||||||
</div>
|
</div>
|
||||||
<div className="import-blue-button" onClick={this.props.downloadScreenshot}>
|
<div className="download-screenshot import-blue-button" onClick={this.props.downloadScreenshot}>
|
||||||
Download screenshot
|
Download screenshot
|
||||||
</div>
|
</div>
|
||||||
<h3>IMPORT</h3>
|
<h3>IMPORT</h3>
|
||||||
|
@ -45,8 +35,8 @@ class ImportDialogBox extends Component {
|
||||||
|
|
||||||
ImportDialogBox.propTypes = {
|
ImportDialogBox.propTypes = {
|
||||||
onFileAdded: PropTypes.func,
|
onFileAdded: PropTypes.func,
|
||||||
exampleImageUrl: PropTypes.string,
|
downloadScreenshot: PropTypes.func,
|
||||||
downloadScreenshot: PropTypes.func
|
onExport: PropTypes.func
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ImportDialogBox
|
export default ImportDialogBox
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import React, { Component, PropTypes } from 'react'
|
import React, { Component, PropTypes } from 'react'
|
||||||
import { find, values } from 'lodash'
|
import { find, values } from 'lodash'
|
||||||
|
import Util from '../../Metamaps/Util'
|
||||||
|
|
||||||
const IN_CONVERSATION = 1 // shared with /realtime/reducer.js
|
const IN_CONVERSATION = 1 // shared with /realtime/reducer.js
|
||||||
|
|
||||||
|
@ -23,7 +24,8 @@ class Menu extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
render = () => {
|
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' }
|
const style = { display: this.state.open ? 'block' : 'none' }
|
||||||
|
|
||||||
return <div className='dropdownMenu'>
|
return <div className='dropdownMenu'>
|
||||||
|
@ -35,6 +37,7 @@ class Menu extends Component {
|
||||||
<ul className='menuItems' style={ style }>
|
<ul className='menuItems' style={ style }>
|
||||||
<li className='star' onClick={ () => { this.toggle() && onStar(map) }}>Star Map</li>
|
<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> }
|
{ !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>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
@ -43,7 +46,8 @@ Menu.propTypes = {
|
||||||
currentUser: PropTypes.object.isRequired,
|
currentUser: PropTypes.object.isRequired,
|
||||||
map: PropTypes.object.isRequired,
|
map: PropTypes.object.isRequired,
|
||||||
onStar: PropTypes.func.isRequired,
|
onStar: PropTypes.func.isRequired,
|
||||||
onRequest: PropTypes.func.isRequired
|
onRequest: PropTypes.func.isRequired,
|
||||||
|
onFollow: PropTypes.func.isRequired
|
||||||
}
|
}
|
||||||
|
|
||||||
const Metadata = (props) => {
|
const Metadata = (props) => {
|
||||||
|
@ -80,7 +84,7 @@ const checkAndWrapInA = (shouldWrap, classString, mapId, element) => {
|
||||||
|
|
||||||
class MapCard extends Component {
|
class MapCard extends Component {
|
||||||
render = () => {
|
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 hasMap = (juntoState.liveMaps[map.id] && values(juntoState.liveMaps[map.id]).length) || null
|
||||||
const realtimeMap = juntoState.liveMaps[map.id]
|
const realtimeMap = juntoState.liveMaps[map.id]
|
||||||
|
@ -131,7 +135,7 @@ class MapCard extends Component {
|
||||||
</div>) }
|
</div>) }
|
||||||
{ !mobile && hasMapper && <div className='mapHasMapper'><MapperList mappers={ mapperList } /></div> }
|
{ !mobile && hasMapper && <div className='mapHasMapper'><MapperList mappers={ mapperList } /></div> }
|
||||||
{ !mobile && hasConversation && <div className='mapHasConversation'><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>) }
|
</div>) }
|
||||||
</div>
|
</div>
|
||||||
|
@ -145,7 +149,8 @@ MapCard.propTypes = {
|
||||||
juntoState: PropTypes.object,
|
juntoState: PropTypes.object,
|
||||||
currentUser: PropTypes.object,
|
currentUser: PropTypes.object,
|
||||||
onStar: PropTypes.func.isRequired,
|
onStar: PropTypes.func.isRequired,
|
||||||
onRequest: PropTypes.func.isRequired
|
onRequest: PropTypes.func.isRequired,
|
||||||
|
onFollow: PropTypes.func.isRequired
|
||||||
}
|
}
|
||||||
|
|
||||||
export default MapCard
|
export default MapCard
|
||||||
|
|
|
@ -46,7 +46,7 @@ class Maps extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
render = () => {
|
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 style = { width: this.state.mapsWidth + 'px' }
|
||||||
const mobile = document && document.body.clientWidth <= MOBILE_VIEW_BREAKPOINT
|
const mobile = document && document.body.clientWidth <= MOBILE_VIEW_BREAKPOINT
|
||||||
|
|
||||||
|
@ -56,7 +56,7 @@ class Maps extends Component {
|
||||||
<div style={ style }>
|
<div style={ style }>
|
||||||
{ user ? <MapperCard user={ user } /> : null }
|
{ 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 }
|
{ 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 className='clearfloat'></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -79,7 +79,8 @@ Maps.propTypes = {
|
||||||
loadMore: PropTypes.func,
|
loadMore: PropTypes.func,
|
||||||
pending: PropTypes.bool.isRequired,
|
pending: PropTypes.bool.isRequired,
|
||||||
onStar: PropTypes.func.isRequired,
|
onStar: PropTypes.func.isRequired,
|
||||||
onRequest: PropTypes.func.isRequired
|
onRequest: PropTypes.func.isRequired,
|
||||||
|
onFollow: PropTypes.func.isRequired
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Maps
|
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) {
|
isRightClick: function(e) {
|
||||||
return (e.which == 3 || e.button == 2);
|
return (e.which == 3 || e.button == 2);
|
||||||
},
|
},
|
||||||
getPos: function(e, win) {
|
getPos: function(e, win, touchIndex) {
|
||||||
// get mouse position
|
// get mouse position
|
||||||
win = win || window;
|
win = win || window;
|
||||||
e = e || win.event;
|
e = e || win.event;
|
||||||
|
@ -457,7 +457,7 @@ $.event = {
|
||||||
doc = doc.documentElement || doc.body;
|
doc = doc.documentElement || doc.body;
|
||||||
//TODO(nico): make touch event handling better
|
//TODO(nico): make touch event handling better
|
||||||
if(e.touches && e.touches.length) {
|
if(e.touches && e.touches.length) {
|
||||||
e = e.touches[0];
|
e = e.touches[touchIndex || 0];
|
||||||
}
|
}
|
||||||
var page = {
|
var page = {
|
||||||
x: e.pageX || (e.clientX + doc.scrollLeft),
|
x: e.pageX || (e.clientX + doc.scrollLeft),
|
||||||
|
@ -2469,33 +2469,7 @@ Extras.Classes.Navigation = new Class({
|
||||||
|
|
||||||
// START METAMAPS CODE
|
// START METAMAPS CODE
|
||||||
if (((ans > 1) && (5 >= this.canvas.scaleOffsetX)) || ((ans < 1) && (this.canvas.scaleOffsetX >= 0.2))) {
|
if (((ans > 1) && (5 >= this.canvas.scaleOffsetX)) || ((ans < 1) && (this.canvas.scaleOffsetX >= 0.2))) {
|
||||||
var s = this.canvas.getSize(),
|
Metamaps.Util.zoomOnPoint(this, ans, {x: e.pageX, y: e.pageY})
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// END METAMAPS CODE
|
// END METAMAPS CODE
|
||||||
|
@ -2620,109 +2594,132 @@ Extras.Classes.Navigation = new Class({
|
||||||
Metamaps.Mouse.changeInY = 0;
|
Metamaps.Mouse.changeInY = 0;
|
||||||
if((this.config.panning == 'avoid nodes' && eventInfo.getNode()) || eventInfo.getEdge()) return;
|
if((this.config.panning == 'avoid nodes' && eventInfo.getNode()) || eventInfo.getEdge()) return;
|
||||||
this.pressed = true;
|
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;
|
Metamaps.Mouse.didPan = false;
|
||||||
this.pos = eventInfo.getPos();
|
|
||||||
var canvas = this.canvas,
|
var canvas = this.canvas,
|
||||||
ox = canvas.translateOffsetX,
|
ox = canvas.translateOffsetX,
|
||||||
oy = canvas.translateOffsetY,
|
oy = canvas.translateOffsetY,
|
||||||
sx = canvas.scaleOffsetX,
|
sx = canvas.scaleOffsetX,
|
||||||
sy = canvas.scaleOffsetY;
|
sy = canvas.scaleOffsetY;
|
||||||
this.pos.x *= sx;
|
|
||||||
this.pos.x += ox;
|
if (e.touches.length === 1) {
|
||||||
this.pos.y *= sy;
|
this.pos = eventInfo.getPos();
|
||||||
this.pos.y += oy;
|
} 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) {
|
onTouchMove: function(e, win, eventInfo) {
|
||||||
|
e.preventDefault()
|
||||||
if(!this.config.panning) return;
|
if(!this.config.panning) return;
|
||||||
if(!this.pressed) 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) {
|
if (e.touches.length == 1) {
|
||||||
var rightClick = e.button == 2 || (navigator.platform.indexOf("Mac") != -1 && e.ctrlKey);
|
currentPos = eventInfo.getPos()
|
||||||
if (!Metamaps.Mouse.boxStartCoordinates && ((e.button == 0 && e.shiftKey) || (e.button == 0 && e.ctrlKey) || rightClick)) {
|
} else if (e.touches.length >= 2) {
|
||||||
Metamaps.Visualize.mGraph.busy = true;
|
var s = canvas.getSize(),
|
||||||
Metamaps.boxStartCoordinates = eventInfo.getPos();
|
pos1 = $.event.getPos(e, win, 0),
|
||||||
return;
|
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) {
|
onTouchEnd: function(e, win, eventInfo, isRightClick) {
|
||||||
if(!this.config.panning) return;
|
if(!this.config.panning) return;
|
||||||
this.pressed = false;
|
if (e.touches.length === 1) {
|
||||||
if (Metamaps.Mouse.didPan) Metamaps.JIT.SmoothPanning();
|
var canvas = this.canvas,
|
||||||
this.initDist = false
|
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
|
// END METAMAPS CODE
|
||||||
});
|
});
|
||||||
|
|
|
@ -113,9 +113,15 @@ describe('Metamaps.Util.js', function() {
|
||||||
expect(Util.mdToHTML(md).trim()).to.equal(html)
|
expect(Util.mdToHTML(md).trim()).to.equal(html)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('links and images', function() {
|
it('links', function() {
|
||||||
const md = '[Link](https://metamaps.cc) ![Image](https://example.org/image.png)'
|
const md = '[Link](https://metamaps.cc)'
|
||||||
const html = '<p><a href="https://metamaps.cc">Link</a> <img src="https://example.org/image.png" alt="Image" /></p>'
|
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)
|
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": {
|
"scripts": {
|
||||||
"build": "webpack",
|
"build": "webpack",
|
||||||
"build:watch": "webpack --watch",
|
"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": "eslint frontend",
|
||||||
"eslint:fix": "eslint --fix frontend"
|
"eslint:fix": "eslint --fix frontend"
|
||||||
},
|
},
|
||||||
|
@ -35,7 +35,7 @@
|
||||||
"csv-parse": "1.1.10",
|
"csv-parse": "1.1.10",
|
||||||
"emoji-mart": "0.3.7",
|
"emoji-mart": "0.3.7",
|
||||||
"getscreenmedia": "2.0.0",
|
"getscreenmedia": "2.0.0",
|
||||||
"hark": "git://github.com/otalk/hark#342ef9b7eff2",
|
"hark": "1.1.5",
|
||||||
"howler": "2.0.2",
|
"howler": "2.0.2",
|
||||||
"jquery": "3.1.1",
|
"jquery": "3.1.1",
|
||||||
"json-loader": "0.5.4",
|
"json-loader": "0.5.4",
|
||||||
|
@ -45,10 +45,12 @@
|
||||||
"react": "15.4.2",
|
"react": "15.4.2",
|
||||||
"react-dom": "15.4.2",
|
"react-dom": "15.4.2",
|
||||||
"react-dropzone": "3.9.1",
|
"react-dropzone": "3.9.1",
|
||||||
|
"react-onclickoutside": "5.9.0",
|
||||||
"redux": "3.6.0",
|
"redux": "3.6.0",
|
||||||
|
"riek": "1.0.7",
|
||||||
"simplewebrtc": "2.2.2",
|
"simplewebrtc": "2.2.2",
|
||||||
"socket.io": "1.3.7",
|
"socket.io": "1.3.7",
|
||||||
"webpack": "1.14.0"
|
"webpack": "2.2.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"babel-eslint": "^7.1.1",
|
"babel-eslint": "^7.1.1",
|
||||||
|
@ -59,7 +61,10 @@
|
||||||
"eslint-plugin-promise": "^3.4.0",
|
"eslint-plugin-promise": "^3.4.0",
|
||||||
"eslint-plugin-react": "^6.8.0",
|
"eslint-plugin-react": "^6.8.0",
|
||||||
"eslint-plugin-standard": "^2.0.1",
|
"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": {
|
"optionalDependencies": {
|
||||||
"raml2html": "4.0.5"
|
"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)
|
MapMailer.invite_to_edit(user_map)
|
||||||
end
|
end
|
||||||
|
|
||||||
def access_request_email
|
def access_request
|
||||||
request = AccessRequest.first
|
request = AccessRequest.first
|
||||||
MapMailer.access_request(request)
|
MapMailer.access_request(request)
|
||||||
end
|
end
|
||||||
|
|
||||||
def access_approved_email
|
def access_approved
|
||||||
request = AccessRequest.first
|
request = AccessRequest.first
|
||||||
MapMailer.access_approved(request)
|
MapMailer.access_approved(request)
|
||||||
end
|
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 = [
|
const plugins = [
|
||||||
new webpack.DefinePlugin({
|
new webpack.DefinePlugin({
|
||||||
"process.env.NODE_ENV": `"${NODE_ENV}"`
|
"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') {
|
if (NODE_ENV === 'production') {
|
||||||
plugins.push(new webpack.optimize.DedupePlugin())
|
plugins.push(new webpack.optimize.DedupePlugin())
|
||||||
plugins.push(new webpack.optimize.UglifyJsPlugin({
|
plugins.push(new webpack.optimize.UglifyJsPlugin({
|
||||||
|
@ -26,12 +30,13 @@ const devtool = NODE_ENV === 'production' ? undefined : 'cheap-module-eval-sourc
|
||||||
module.exports = {
|
module.exports = {
|
||||||
context: __dirname,
|
context: __dirname,
|
||||||
plugins,
|
plugins,
|
||||||
|
externals,
|
||||||
devtool,
|
devtool,
|
||||||
module: {
|
module: {
|
||||||
preLoaders: [
|
|
||||||
{ test: /\.json$/, loader: 'json' }
|
|
||||||
],
|
|
||||||
loaders: [
|
loaders: [
|
||||||
|
{
|
||||||
|
test: /\.json$/, loader: 'json-loader'
|
||||||
|
},
|
||||||
{
|
{
|
||||||
test: /\.(js|jsx)?$/,
|
test: /\.(js|jsx)?$/,
|
||||||
exclude: /node_modules/,
|
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