Implemented permalinks.

master
Pacman Ghost 4 years ago
parent 237b6ff7d7
commit 86b165b1eb
  1. 18
      asl_articles/search.py
  2. 11
      asl_articles/tests/fixtures/search.json
  3. 174
      asl_articles/tests/test_search.py
  4. 2
      asl_articles/tests/utils.py
  5. 2
      web/docker/nginx-default.conf
  6. 89
      web/package-lock.json
  7. 1
      web/package.json
  8. 60
      web/src/App.js
  9. 36
      web/src/ArticleSearchResult.js
  10. 2
      web/src/ArticleSearchResult2.js
  11. 2
      web/src/FileUploader.js
  12. 34
      web/src/PublicationSearchResult.js
  13. 2
      web/src/PublicationSearchResult2.js
  14. 8
      web/src/PublisherSearchResult.js
  15. 2
      web/src/PublisherSearchResult2.js
  16. 11
      web/src/SearchResults.css
  17. 2
      web/src/SearchResults.js
  18. 10
      web/src/constants.js
  19. 39
      web/src/index.js
  20. 2
      web/src/utils.js

@ -9,7 +9,7 @@ import tempfile
import re import re
import logging import logging
from flask import request, jsonify, abort from flask import request, jsonify
import asl_articles import asl_articles
from asl_articles import app, db from asl_articles import app, db
@ -117,12 +117,12 @@ def search_publishers():
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
@app.route( "/search/publisher/<int:publ_id>", methods=["POST","GET"] ) @app.route( "/search/publisher/<publ_id>", methods=["POST","GET"] )
def search_publisher( publ_id ): def search_publisher( publ_id ):
"""Search for a publisher.""" """Search for a publisher."""
publ = Publisher.query.get( publ_id ) publ = Publisher.query.get( publ_id )
if not publ: if not publ:
abort( 404 ) return jsonify( [] )
results = [ get_publisher_vals( publ, True ) ] results = [ get_publisher_vals( publ, True ) ]
pubs = sorted( publ.publications, key=get_publication_sort_key, reverse=True ) pubs = sorted( publ.publications, key=get_publication_sort_key, reverse=True )
for pub in pubs: for pub in pubs:
@ -131,12 +131,12 @@ def search_publisher( publ_id ):
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
@app.route( "/search/publication/<int:pub_id>", methods=["POST","GET"] ) @app.route( "/search/publication/<pub_id>", methods=["POST","GET"] )
def search_publication( pub_id ): def search_publication( pub_id ):
"""Search for a publication.""" """Search for a publication."""
pub = Publication.query.get( pub_id ) pub = Publication.query.get( pub_id )
if not pub: if not pub:
abort( 404 ) return jsonify( [] )
results = [ get_publication_vals( pub, True, True ) ] results = [ get_publication_vals( pub, True, True ) ]
articles = sorted( pub.articles, key=get_article_sort_key ) articles = sorted( pub.articles, key=get_article_sort_key )
for article in articles: for article in articles:
@ -145,12 +145,12 @@ def search_publication( pub_id ):
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
@app.route( "/search/article/<int:article_id>", methods=["POST","GET"] ) @app.route( "/search/article/<article_id>", methods=["POST","GET"] )
def search_article( article_id ): def search_article( article_id ):
"""Search for an article.""" """Search for an article."""
article = Article.query.get( article_id ) article = Article.query.get( article_id )
if not article: if not article:
abort( 404 ) return jsonify( [] )
results = [ get_article_vals( article, True ) ] results = [ get_article_vals( article, True ) ]
if article.pub_id: if article.pub_id:
pub = Publication.query.get( article.pub_id ) pub = Publication.query.get( article.pub_id )
@ -160,12 +160,12 @@ def search_article( article_id ):
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
@app.route( "/search/author/<int:author_id>", methods=["POST","GET"] ) @app.route( "/search/author/<author_id>", methods=["POST","GET"] )
def search_author( author_id ): def search_author( author_id ):
"""Search for an author.""" """Search for an author."""
author = Author.query.get( author_id ) author = Author.query.get( author_id )
if not author: if not author:
abort( 404 ) return jsonify( [] )
author_name = '"{}"'.format( author.author_name.replace( '"', '""' ) ) author_name = '"{}"'.format( author.author_name.replace( '"', '""' ) )
return _do_search( author_name, [ "authors" ] ) return _do_search( author_name, [ "authors" ] )

@ -65,7 +65,18 @@
{ "article_id": 520, { "article_id": 520,
"article_title": "Jagdpanzer 38(t) Hetzer", "article_title": "Jagdpanzer 38(t) Hetzer",
"article_snippet": "In the 1930s the Germans conducted a number of military exercises which showed that close support from light field guns was helpful for infantry operations.", "article_snippet": "In the 1930s the Germans conducted a number of military exercises which showed that close support from light field guns was helpful for infantry operations.",
"article_tags": "jagdpanzer",
"pub_id": 12 "pub_id": 12
},
{ "article_id": 600,
"article_title": "A tip article",
"article_snippet": "This is a 'tip' article.",
"article_tags": "tips"
},
{ "article_id": 601,
"article_title": "A technique article",
"article_snippet": "This is a 'technique' article.",
"article_tags": "technique"
} }
], ],

@ -6,9 +6,9 @@ from asl_articles.search import SEARCH_ALL
from asl_articles.tests.test_publishers import create_publisher, edit_publisher from asl_articles.tests.test_publishers import create_publisher, edit_publisher
from asl_articles.tests.test_publications import create_publication, edit_publication from asl_articles.tests.test_publications import create_publication, edit_publication
from asl_articles.tests.test_articles import create_article, edit_article from asl_articles.tests.test_articles import create_article, edit_article
from asl_articles.tests.utils import init_tests, select_sr_menu_option, \ from asl_articles.tests.utils import init_tests, select_main_menu_option, select_sr_menu_option, \
wait_for, wait_for_elem, find_child, find_children, check_ask_dialog, \ wait_for, wait_for_elem, find_child, find_children, check_ask_dialog, \
do_search, get_search_results, get_search_result_names, find_search_result, get_search_seqno do_search, get_search_results, get_search_result_names, find_search_result
# --------------------------------------------------------------------- # ---------------------------------------------------------------------
@ -278,22 +278,18 @@ def test_publisher_search( webdriver, flask_app, dbconn ):
# initialize # initialize
init_tests( webdriver, flask_app, dbconn, fixtures="search.json" ) init_tests( webdriver, flask_app, dbconn, fixtures="search.json" )
def click_on_publisher( sr, expected ): def click_on_publisher( sr, expected_publ, expected_sr ):
elem = find_child( ".header .publisher", sr ) elem = find_child( ".header .publisher", sr )
assert elem.text == expected assert elem.text == expected_publ
seq_no = get_search_seqno()
elem.click() elem.click()
wait_for( 2, lambda: get_search_seqno() != seq_no ) wait_for( 2, lambda: get_search_result_names() == expected_sr )
assert find_child( "#search-form input.query" ).get_attribute( "value" ) == ""
return get_search_results()
# find a publication and click on its parent publisher # find a publication and click on its parent publisher
results = do_search( "fantastic" ) results = do_search( "fantastic" )
assert len(results) == 1 assert len(results) == 1
click_on_publisher( results[0], "View From The Trenches" ) click_on_publisher( results[0], "View From The Trenches", [
assert get_search_result_names() == [
"View From The Trenches", "View From The Trenches (100)" "View From The Trenches", "View From The Trenches (100)"
] ] )
# --------------------------------------------------------------------- # ---------------------------------------------------------------------
@ -303,7 +299,7 @@ def test_publication_search( webdriver, flask_app, dbconn ):
# initialize # initialize
init_tests( webdriver, flask_app, dbconn, fixtures="search.json" ) init_tests( webdriver, flask_app, dbconn, fixtures="search.json" )
def click_on_publication( sr, expected ): def click_on_publication( sr, expected_pub, expected_sr ):
classes = sr.get_attribute( "class" ).split() classes = sr.get_attribute( "class" ).split()
if "article" in classes: if "article" in classes:
elem = find_child( ".header .publication", sr ) elem = find_child( ".header .publication", sr )
@ -313,36 +309,30 @@ def test_publication_search( webdriver, flask_app, dbconn ):
else: else:
assert "publication" in classes assert "publication" in classes
elem = find_child( ".header .name", sr ) elem = find_child( ".header .name", sr )
assert elem.text == expected assert elem.text == expected_pub
seq_no = get_search_seqno()
elem.click() elem.click()
wait_for( 2, lambda: get_search_seqno() != seq_no ) wait_for( 2, lambda: get_search_result_names() == expected_sr )
assert find_child( "#search-form input.query" ).get_attribute( "value" ) == ""
return get_search_results()
# find a publication and click on it # find a publication and click on it
results = do_search( "vftt" ) results = do_search( "vftt" )
sr = find_search_result( "View From The Trenches (100)", results ) sr = find_search_result( "View From The Trenches (100)", results )
click_on_publication( sr, "View From The Trenches (100)" ) click_on_publication( sr, "View From The Trenches (100)", [
assert get_search_result_names() == [
"View From The Trenches (100)", "Jagdpanzer 38(t) Hetzer" "View From The Trenches (100)", "Jagdpanzer 38(t) Hetzer"
] ] )
# find an article and click on its parent publication # find an article and click on its parent publication
results = do_search( "neutral" ) results = do_search( "neutral" )
assert len(results) == 1 assert len(results) == 1
click_on_publication( results[0], "ASL Journal (5)" ) click_on_publication( results[0], "ASL Journal (5)", [
assert get_search_result_names() == [
"ASL Journal (5)", "The Jungle Isn't Neutral", "Hunting DUKWs and Buffalos" "ASL Journal (5)", "The Jungle Isn't Neutral", "Hunting DUKWs and Buffalos"
] ] )
# find a publisher and click on one of its publications # find a publisher and click on one of its publications
results = do_search( "mmp" ) results = do_search( "mmp" )
assert len(results) == 1 assert len(results) == 1
click_on_publication( results[0], "ASL Journal (4)" ) click_on_publication( results[0], "ASL Journal (4)", [
assert get_search_result_names() == [
"ASL Journal (4)", "Hit 'Em High, Or Hit 'Em Low", "'Bolts From Above" "ASL Journal (4)", "Hit 'Em High, Or Hit 'Em Low", "'Bolts From Above"
] ] )
# --------------------------------------------------------------------- # ---------------------------------------------------------------------
@ -352,23 +342,20 @@ def test_article_search( webdriver, flask_app, dbconn ):
# initialize # initialize
init_tests( webdriver, flask_app, dbconn, fixtures="search.json" ) init_tests( webdriver, flask_app, dbconn, fixtures="search.json" )
def click_on_article( sr, expected ): def click_on_article( sr, expected_pub, expected_sr ):
elems = find_children( ".content .collapsible li", sr ) elems = find_children( ".content .collapsible li", sr )
elem = elems[0] # nb: we just use the first one elem = elems[0] # nb: we just use the first one
assert elem.text == expected assert elem.text == expected_pub
seq_no = get_search_seqno()
elem.click() elem.click()
wait_for( 2, lambda: get_search_seqno() != seq_no ) wait_for( 2, lambda: get_search_result_names() == expected_sr )
assert find_child( "#search-form input.query" ).get_attribute( "value" ) == "" assert find_child( "#search-form input.query" ).get_attribute( "value" ) == ""
return get_search_results()
# find a publication and click on one of its articles # find a publication and click on one of its articles
results = do_search( "vftt" ) results = do_search( "vftt" )
sr = find_search_result( "View From The Trenches (100)", results ) sr = find_search_result( "View From The Trenches (100)", results )
click_on_article( sr, "Jagdpanzer 38(t) Hetzer" ) click_on_article( sr, "Jagdpanzer 38(t) Hetzer", [
assert get_search_result_names() == [
"Jagdpanzer 38(t) Hetzer", "View From The Trenches (100)" "Jagdpanzer 38(t) Hetzer", "View From The Trenches (100)"
] ] )
# --------------------------------------------------------------------- # ---------------------------------------------------------------------
@ -378,23 +365,20 @@ def test_author_search( webdriver, flask_app, dbconn ):
# initialize # initialize
init_tests( webdriver, flask_app, dbconn, fixtures="search.json" ) init_tests( webdriver, flask_app, dbconn, fixtures="search.json" )
def click_on_author( sr, expected ): def click_on_author( sr, expected_author, expected_sr ):
authors = find_children( ".authors .author", sr ) authors = find_children( ".authors .author", sr )
assert len(authors) == 1 assert len(authors) == 1
assert authors[0].text == expected assert authors[0].text == expected_author
seq_no = get_search_seqno()
authors[0].click() authors[0].click()
wait_for( 2, lambda: get_search_seqno() != seq_no ) wait_for( 2, lambda: get_search_result_names() == expected_sr )
assert find_child( "#search-form input.query" ).get_attribute( "value" ) == ""
return get_search_results() return get_search_results()
# find an article and click on the author # find an article and click on the author
results = do_search( SEARCH_ALL ) results = do_search( SEARCH_ALL )
sr = find_search_result( "Jagdpanzer 38(t) Hetzer" ) sr = find_search_result( "Jagdpanzer 38(t) Hetzer", results )
results = click_on_author( sr, "Michael Davies" ) click_on_author( sr, "Michael Davies", [
assert get_search_result_names( results ) == [
"Jagdpanzer 38(t) Hetzer" "Jagdpanzer 38(t) Hetzer"
] ] )
# --------------------------------------------------------------------- # ---------------------------------------------------------------------
@ -404,11 +388,9 @@ def test_tag_search( webdriver, flask_app, dbconn ):
# initialize # initialize
init_tests( webdriver, flask_app, dbconn, fixtures="search.json" ) init_tests( webdriver, flask_app, dbconn, fixtures="search.json" )
def click_on_tag( tag ): def click_on_tag( tag, expected ):
seq_no = get_search_seqno()
tag.click() tag.click()
wait_for( 2, lambda: get_search_seqno() != seq_no ) wait_for( 2, lambda: get_search_result_names() == expected )
assert find_child( "#search-form input.query" ).get_attribute( "value" ) == ""
return get_search_results() return get_search_results()
def get_tags( sr ): def get_tags( sr ):
return find_children( ".tags .tag", sr ) return find_children( ".tags .tag", sr )
@ -418,25 +400,109 @@ def test_tag_search( webdriver, flask_app, dbconn ):
assert len(results) == 1 assert len(results) == 1
tags = get_tags( results[0] ) tags = get_tags( results[0] )
assert [ t.text for t in tags ] == [ "#aslj", "#mortars" ] assert [ t.text for t in tags ] == [ "#aslj", "#mortars" ]
results = click_on_tag( tags[0] )
expected = [ expected = [
"ASL Journal (4)", "ASL Journal (5)", "ASL Journal (4)", "ASL Journal (5)",
"'Bolts From Above", "The Jungle Isn't Neutral", "Hunting DUKWs and Buffalos", "Hit 'Em High, Or Hit 'Em Low" "'Bolts From Above", "The Jungle Isn't Neutral", "Hunting DUKWs and Buffalos", "Hit 'Em High, Or Hit 'Em Low"
] ]
assert get_search_result_names( results ) == expected results = click_on_tag( tags[0], expected )
# click on another "#aslj" tag # click on another "#aslj" tag
tags = get_tags( results[0] ) tags = get_tags( results[0] )
assert [ t.text for t in tags ] == [ "#aslj" ] assert [ t.text for t in tags ] == [ "#aslj" ]
results = click_on_tag( tags[0] ) results = click_on_tag( tags[0], expected )
assert get_search_result_names( results ) == expected
# click on a "#PTO" tag # click on a "#PTO" tag
sr = find_search_result( "The Jungle Isn't Neutral" ) sr = find_search_result( "The Jungle Isn't Neutral", results )
tags = get_tags( sr ) tags = get_tags( sr )
assert [ t.text for t in tags ] == [ "#aslj", "#PTO" ] assert [ t.text for t in tags ] == [ "#aslj", "#PTO" ]
results = click_on_tag( tags[1] ) click_on_tag( tags[1], [ "The Jungle Isn't Neutral" ] )
assert get_search_result_names( results ) == [ "The Jungle Isn't Neutral" ]
# ---------------------------------------------------------------------
def test_special_searches( webdriver, flask_app, dbconn ):
"""Test special searches."""
# initialize
init_tests( webdriver, flask_app, dbconn, fixtures="search.json" )
# initialize
def get_url():
url = webdriver.current_url
pos = url.find( "?" )
if pos >= 0:
url = url[:pos]
while url.endswith( "/" ):
url = url[:-1]
return url
url_stem = get_url()
title_stem = "ASL Articles"
def check_title( expected ):
if expected:
return webdriver.title == "{} - {}".format( title_stem, expected )
else:
return webdriver.title == title_stem
def check_results( expected_url, expected_title, expected_sr ):
wait_for( 2, lambda: check_title( expected_title ) )
assert get_url() == "{}/{}".format( url_stem, expected_url ) if expected_url else url_stem
results = get_search_results()
assert get_search_result_names( results ) == expected_sr
return results
# test showing "technique" articles
select_main_menu_option( "search-technique" )
check_results( "", "Technique", [ "A technique article" ] )
# test showing "tip" articles
select_main_menu_option( "search-tips" )
check_results( "", "Tips", [ "A tip article" ] )
# test showing all publishers
select_main_menu_option( "show-publishers" )
results = check_results( "", "All publishers", [
"Multi-Man Publishing", "View From The Trenches"
] )
# test showing a single publication
pubs = find_children( ".collapsible li", results[1] )
assert [ p.text for p in pubs ] == [ "View From The Trenches (100)" ]
pubs[0].click()
results = check_results( "publication/12", "View From The Trenches (100)", [
"View From The Trenches (100)", "Jagdpanzer 38(t) Hetzer"
] )
# test showing a single publisher
publ = find_child( "a.publisher", results[0] )
assert publ.text == "View From The Trenches"
publ.click()
results = check_results( "publisher/2", "View From The Trenches", [
"View From The Trenches", "View From The Trenches (100)"
] )
# test showing a single article
articles = find_children( ".collapsible li", results[1] )
assert [ a.text for a in articles ] == [ "Jagdpanzer 38(t) Hetzer" ]
articles[0].click()
results = check_results( "article/520", "Jagdpanzer 38(t) Hetzer" , [
"Jagdpanzer 38(t) Hetzer", "View From The Trenches (100)"
] )
# test showing an author's articles
authors = find_children( "a.author", results[0] )
assert [ a.text for a in authors ] == [ "Michael Davies" ]
authors[0].click()
results = check_results( "author/1003", "Michael Davies" , [
"Jagdpanzer 38(t) Hetzer"
] )
# test searching for a tag
tags = find_children( "a.tag", results[0] )
assert [ t.text for t in tags ] == [ "jagdpanzer" ]
tags[0].click()
check_results( "tag/jagdpanzer", "jagdpanzer" , [
"Jagdpanzer 38(t) Hetzer"
] )
# --------------------------------------------------------------------- # ---------------------------------------------------------------------

@ -55,7 +55,7 @@ def init_tests( webdriver, flask_app, dbconn, **kwargs ):
kwargs[ "disable_constraints" ] = 1 kwargs[ "disable_constraints" ] = 1
if to_bool( kwargs.pop( "disable_confirm_discard_changes", True ) ): if to_bool( kwargs.pop( "disable_confirm_discard_changes", True ) ):
kwargs[ "disable_confirm_discard_changes" ] = 1 kwargs[ "disable_confirm_discard_changes" ] = 1
webdriver.get( webdriver.make_url( "/", **kwargs ) ) webdriver.get( webdriver.make_url( "", **kwargs ) )
wait_for_elem( 2, "#search-form" ) wait_for_elem( 2, "#search-form" )
return session return session

@ -5,7 +5,7 @@ server {
location / { location / {
root /usr/share/nginx/html ; root /usr/share/nginx/html ;
index index.html index.htm ; try_files $uri $uri/ /index.html ;
} }
location /api { location /api {

@ -6181,6 +6181,11 @@
"resolved": "https://registry.npmjs.org/growly/-/growly-1.3.0.tgz", "resolved": "https://registry.npmjs.org/growly/-/growly-1.3.0.tgz",
"integrity": "sha1-8QdIy+dq+WS3yWyTxrzCivEgwIE=" "integrity": "sha1-8QdIy+dq+WS3yWyTxrzCivEgwIE="
}, },
"gud": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/gud/-/gud-1.0.0.tgz",
"integrity": "sha512-zGEOVKFM5sVPPrYs7J5/hYEw2Pof8KCyOwyhG8sAF26mCAeUFAcYPu1mwB7hhpIP29zOIBaDqwuHdLp0jvZXjw=="
},
"gzip-size": { "gzip-size": {
"version": "5.1.1", "version": "5.1.1",
"resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-5.1.1.tgz", "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-5.1.1.tgz",
@ -6329,6 +6334,19 @@
"resolved": "https://registry.npmjs.org/hex-color-regex/-/hex-color-regex-1.1.0.tgz", "resolved": "https://registry.npmjs.org/hex-color-regex/-/hex-color-regex-1.1.0.tgz",
"integrity": "sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ==" "integrity": "sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ=="
}, },
"history": {
"version": "4.10.1",
"resolved": "https://registry.npmjs.org/history/-/history-4.10.1.tgz",
"integrity": "sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew==",
"requires": {
"@babel/runtime": "^7.1.2",
"loose-envify": "^1.2.0",
"resolve-pathname": "^3.0.0",
"tiny-invariant": "^1.0.2",
"tiny-warning": "^1.0.0",
"value-equal": "^1.0.1"
}
},
"hmac-drbg": { "hmac-drbg": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz",
@ -8753,6 +8771,16 @@
"resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz",
"integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==" "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="
}, },
"mini-create-react-context": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/mini-create-react-context/-/mini-create-react-context-0.3.2.tgz",
"integrity": "sha512-2v+OeetEyliMt5VHMXsBhABoJ0/M4RCe7fatd/fBy6SMiKazUSEt3gxxypfnk2SHMkdBYvorHRoQxuGoiwbzAw==",
"requires": {
"@babel/runtime": "^7.4.0",
"gud": "^1.0.0",
"tiny-warning": "^1.0.2"
}
},
"mini-css-extract-plugin": { "mini-css-extract-plugin": {
"version": "0.8.0", "version": "0.8.0",
"resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-0.8.0.tgz", "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-0.8.0.tgz",
@ -11026,6 +11054,52 @@
"resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz", "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz",
"integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==" "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA=="
}, },
"react-router": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-5.1.2.tgz",
"integrity": "sha512-yjEuMFy1ONK246B+rsa0cUam5OeAQ8pyclRDgpxuSCrAlJ1qN9uZ5IgyKC7gQg0w8OM50NXHEegPh/ks9YuR2A==",
"requires": {
"@babel/runtime": "^7.1.2",
"history": "^4.9.0",
"hoist-non-react-statics": "^3.1.0",
"loose-envify": "^1.3.1",
"mini-create-react-context": "^0.3.0",
"path-to-regexp": "^1.7.0",
"prop-types": "^15.6.2",
"react-is": "^16.6.0",
"tiny-invariant": "^1.0.2",
"tiny-warning": "^1.0.0"
},
"dependencies": {
"isarray": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
"integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8="
},
"path-to-regexp": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz",
"integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==",
"requires": {
"isarray": "0.0.1"
}
}
}
},
"react-router-dom": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-5.1.2.tgz",
"integrity": "sha512-7BPHAaIwWpZS074UKaw1FjVdZBSVWEk8IuDXdB+OkLb8vd/WRQIpA4ag9WQk61aEfQs47wHyjWUoUGGZxpQXew==",
"requires": {
"@babel/runtime": "^7.1.2",
"history": "^4.9.0",
"loose-envify": "^1.3.1",
"prop-types": "^15.6.2",
"react-router": "5.1.2",
"tiny-invariant": "^1.0.2",
"tiny-warning": "^1.0.0"
}
},
"react-scripts": { "react-scripts": {
"version": "3.2.0", "version": "3.2.0",
"resolved": "https://registry.npmjs.org/react-scripts/-/react-scripts-3.2.0.tgz", "resolved": "https://registry.npmjs.org/react-scripts/-/react-scripts-3.2.0.tgz",
@ -11479,6 +11553,11 @@
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-3.0.0.tgz", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-3.0.0.tgz",
"integrity": "sha1-six699nWiBvItuZTM17rywoYh0g=" "integrity": "sha1-six699nWiBvItuZTM17rywoYh0g="
}, },
"resolve-pathname": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/resolve-pathname/-/resolve-pathname-3.0.0.tgz",
"integrity": "sha512-C7rARubxI8bXFNB/hqcp/4iUeIXJhJZvFPFPiSPRnhU5UPxzMFIl+2E6yY6c4k9giDJAhtV+enfA+G89N6Csng=="
},
"resolve-url": { "resolve-url": {
"version": "0.2.1", "version": "0.2.1",
"resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz",
@ -12796,6 +12875,11 @@
"resolved": "https://registry.npmjs.org/timsort/-/timsort-0.3.0.tgz", "resolved": "https://registry.npmjs.org/timsort/-/timsort-0.3.0.tgz",
"integrity": "sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q=" "integrity": "sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q="
}, },
"tiny-invariant": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.1.0.tgz",
"integrity": "sha512-ytxQvrb1cPc9WBEI/HSeYYoGD0kWnGEOR8RY6KomWLBVhqz0RgTwVO9dLrGz7dC+nN9llyI7OKAgRq8Vq4ZBSw=="
},
"tiny-warning": { "tiny-warning": {
"version": "1.0.3", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz", "resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz",
@ -13207,6 +13291,11 @@
"spdx-expression-parse": "^3.0.0" "spdx-expression-parse": "^3.0.0"
} }
}, },
"value-equal": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/value-equal/-/value-equal-1.0.1.tgz",
"integrity": "sha512-NOJ6JZCAWr0zlxZt+xqCHNTEKOsrks2HQd4MqhP1qy4z1SkbEP467eNx6TgDKXMvUOb+OENfJCZwM+16n7fRfw=="
},
"vary": { "vary": {
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",

@ -14,6 +14,7 @@
"react-dom": "^16.11.0", "react-dom": "^16.11.0",
"react-drag-listview": "^0.1.6", "react-drag-listview": "^0.1.6",
"react-draggable": "^4.1.0", "react-draggable": "^4.1.0",
"react-router-dom": "^5.1.2",
"react-scripts": "3.2.0", "react-scripts": "3.2.0",
"react-select": "^3.0.8", "react-select": "^3.0.8",
"react-toastify": "^5.4.1" "react-toastify": "^5.4.1"

@ -13,14 +13,17 @@ import { ArticleSearchResult } from "./ArticleSearchResult" ;
import ModalForm from "./ModalForm"; import ModalForm from "./ModalForm";
import AskDialog from "./AskDialog" ; import AskDialog from "./AskDialog" ;
import { makeSmartBulletList } from "./utils.js" ; import { makeSmartBulletList } from "./utils.js" ;
import { APP_NAME } from "./constants.js" ;
import "./App.css" ; import "./App.css" ;
const axios = require( "axios" ) ; const axios = require( "axios" ) ;
const queryString = require( "query-string" ) ; const queryString = require( "query-string" ) ;
export let gAppRef = null ;
// -------------------------------------------------------------------- // --------------------------------------------------------------------
export default class App extends React.Component export class App extends React.Component
{ {
constructor( props ) { constructor( props ) {
@ -33,6 +36,8 @@ export default class App extends React.Component
askDialog: null, askDialog: null,
startupTasks: [ "caches.publishers", "caches.publications", "caches.authors", "caches.scenarios", "caches.tags" ], startupTasks: [ "caches.publishers", "caches.publications", "caches.authors", "caches.scenarios", "caches.tags" ],
} ; } ;
gAppRef = this ;
this.setWindowTitle( null ) ;
// initialize // initialize
this.args = queryString.parse( window.location.search ) ; this.args = queryString.parse( window.location.search ) ;
@ -74,14 +79,17 @@ export default class App extends React.Component
<MenuButton /> <MenuButton />
<MenuList> <MenuList>
<MenuItem id="menu-show-publishers" <MenuItem id="menu-show-publishers"
onSelect = { () => this._onSpecialSearch( "/search/publishers" ) } onSelect = { () => this.runSpecialSearch( "/search/publishers", null,
>Show publishers</MenuItem> () => { this.setWindowTitle( "All publishers" ) }
) } > Show publishers </MenuItem>
<MenuItem id="menu-search-technique" <MenuItem id="menu-search-technique"
onSelect = { () => this._onSpecialSearch( "/search/tag/technique", {randomize:1} ) } onSelect = { () => this.runSpecialSearch( "/search/tag/technique", {randomize:1},
>Show technique</MenuItem> () => { this.setWindowTitle( "Technique" ) }
) } > Show technique </MenuItem>
<MenuItem id="menu-search-tips" <MenuItem id="menu-search-tips"
onSelect = { () => this._onSpecialSearch( "/search/tag/tips", {randomize:1} ) } onSelect = { () => this.runSpecialSearch( "/search/tag/tips", {randomize:1},
>Show tips</MenuItem> () => { this.setWindowTitle( "Tips" ) }
) } > Show tips </MenuItem>
<div className="divider" /> <div className="divider" />
<MenuItem id="menu-new-publisher" <MenuItem id="menu-new-publisher"
onSelect = { () => PublisherSearchResult.onNewPublisher( this._onNewPublisher.bind(this) ) } onSelect = { () => PublisherSearchResult.onNewPublisher( this._onNewPublisher.bind(this) ) }
@ -98,7 +106,7 @@ export default class App extends React.Component
content = ( <div> content = ( <div>
<div id="header"> <div id="header">
<img className="logo" src="/images/app.png" alt="Logo" /> <img className="logo" src="/images/app.png" alt="Logo" />
<div className="app-name"> ASL Articles </div> <div className="app-name"> {APP_NAME} </div>
<SearchForm onSearch={this.onSearch.bind(this)} ref={this._searchFormRef} /> <SearchForm onSearch={this.onSearch.bind(this)} ref={this._searchFormRef} />
</div> </div>
{menu} {menu}
@ -212,20 +220,16 @@ export default class App extends React.Component
} }
this._doSearch( "/search", { query: query } ) ; this._doSearch( "/search", { query: query } ) ;
} }
searchForPublisher( publ_id ) { this._onSpecialSearch( "/search/publisher/" + publ_id ) ; } runSpecialSearch( url, args, onDone ) {
searchForPublication( pub_id ) { this._onSpecialSearch( "/search/publication/" + pub_id ) ; }
searchForArticle( article_id ) { this._onSpecialSearch( "/search/article/" + article_id ) ; }
searchForAuthor( author_id ) { this._onSpecialSearch( "/search/author/" + author_id ) ; }
searchForTag( tag ) { this._onSpecialSearch( "/search/tag/" + encodeURIComponent(tag) ) ; }
_onSpecialSearch( url, args ) {
// run the search // run the search
this._searchFormRef.current.setState( { queryString: "" } ) ; this._searchFormRef.current.setState( { queryString: "" } ) ;
if ( ! args ) if ( ! args )
args = {} ; args = {} ;
this._doSearch( url, args ) ; this._doSearch( url, args, onDone ) ;
} }
_doSearch( url, args ) { _doSearch( url, args, onDone ) {
// do the search // do the search
this.setWindowTitle( null ) ;
this.setState( { searchResults: "(loading)" } ) ; this.setState( { searchResults: "(loading)" } ) ;
args.no_hilite = this._disableSearchResultHighlighting ; args.no_hilite = this._disableSearchResultHighlighting ;
axios.post( axios.post(
@ -235,6 +239,8 @@ export default class App extends React.Component
ReactDOM.findDOMNode( this._searchResultsRef.current ).scrollTo( 0, 0 ) ; ReactDOM.findDOMNode( this._searchResultsRef.current ).scrollTo( 0, 0 ) ;
this._setFocusTo = this._searchFormRef.current.queryStringRef.current ; this._setFocusTo = this._searchFormRef.current.queryStringRef.current ;
this.setState( { searchResults: resp.data, searchSeqNo: this.state.searchSeqNo+1 } ) ; this.setState( { searchResults: resp.data, searchSeqNo: this.state.searchSeqNo+1 } ) ;
if ( onDone )
onDone() ;
} ) } )
.catch( err => { .catch( err => {
this.showErrorResponse( "The search query failed", err ) ; this.showErrorResponse( "The search query failed", err ) ;
@ -441,6 +447,28 @@ export default class App extends React.Component
} }
this.state.startupTasks.splice( pos, 1 ) ; this.state.startupTasks.splice( pos, 1 ) ;
this.setState( { startupTasks: this.state.startupTasks } ) ; this.setState( { startupTasks: this.state.startupTasks } ) ;
if ( this.state.startupTasks.length === 0 )
this._onStartupComplete() ;
}
_onStartupComplete() {
// startup has completed, we're ready to go
if ( this.props.warning )
this.showWarningToast( this.props.warning ) ;
if ( this.props.doSearch )
this.props.doSearch() ;
}
setWindowTitleFromSearchResults( srType, idField, idVal, nameField ) {
for ( let sr of Object.entries( this.state.searchResults ) ) {
if ( sr[1].type === srType && String(sr[1][idField]) === idVal ) {
this.setWindowTitle( typeof nameField === "function" ? nameField(sr[1]) : sr[1][nameField] ) ;
return ;
}
}
this.setWindowTitle( null ) ;
}
setWindowTitle( caption ) {
document.title = caption ? APP_NAME + " - " + caption : APP_NAME ;
} }
isTestMode() { return process.env.REACT_APP_TEST_MODE ; } isTestMode() { return process.env.REACT_APP_TEST_MODE ; }

@ -1,9 +1,10 @@
import React from "react" ; import React from "react" ;
import { Link } from "react-router-dom" ;
import { Menu, MenuList, MenuButton, MenuItem } from "@reach/menu-button" ; import { Menu, MenuList, MenuButton, MenuItem } from "@reach/menu-button" ;
import { ArticleSearchResult2 } from "./ArticleSearchResult2.js" ; import { ArticleSearchResult2 } from "./ArticleSearchResult2.js" ;
import "./ArticleSearchResult.css" ; import "./ArticleSearchResult.css" ;
import { PublicationSearchResult } from "./PublicationSearchResult.js" ; import { PublicationSearchResult } from "./PublicationSearchResult.js" ;
import { gAppRef } from "./index.js" ; import { gAppRef } from "./App.js" ;
import { makeScenarioDisplayName, applyUpdatedVals, removeSpecialFields, makeCommaList, isLink } from "./utils.js" ; import { makeScenarioDisplayName, applyUpdatedVals, removeSpecialFields, makeCommaList, isLink } from "./utils.js" ;
const axios = require( "axios" ) ; const axios = require( "axios" ) ;
@ -39,20 +40,18 @@ export class ArticleSearchResult extends React.Component
// the backend has provided us with a list of author names (possibly highlighted) - use them directly // the backend has provided us with a list of author names (possibly highlighted) - use them directly
for ( let i=0 ; i < this.props.data["authors!"].length ; ++i ) { for ( let i=0 ; i < this.props.data["authors!"].length ; ++i ) {
const author_id = this.props.data.article_authors[ i ] ; const author_id = this.props.data.article_authors[ i ] ;
authors.push( <span key={i} className="author" authors.push( <Link key={i} className="author" title="Show articles from this author."
to = { "/author/" + author_id }
dangerouslySetInnerHTML = {{ __html: this.props.data["authors!"][i] }} dangerouslySetInnerHTML = {{ __html: this.props.data["authors!"][i] }}
onClick = { () => gAppRef.searchForAuthor( author_id ) }
title = "Show articles from this author."
/> ) ; /> ) ;
} }
} else { } else {
// we only have a list of author ID's (the normal case) - figure out what the corresponding names are // we only have a list of author ID's (the normal case) - figure out what the corresponding names are
for ( let i=0 ; i < this.props.data.article_authors.length ; ++i ) { for ( let i=0 ; i < this.props.data.article_authors.length ; ++i ) {
const author_id = this.props.data.article_authors[ i ] ; const author_id = this.props.data.article_authors[ i ] ;
authors.push( <span key={i} className="author" authors.push( <Link key={i} className="author" title="Show articles from this author."
to = { "/author/" + author_id }
dangerouslySetInnerHTML = {{ __html: gAppRef.caches.authors[ author_id ].author_name }} dangerouslySetInnerHTML = {{ __html: gAppRef.caches.authors[ author_id ].author_name }}
onClick = { () => gAppRef.searchForAuthor( author_id ) }
title = "Show articles from this author."
/> ) ; /> ) ;
} }
} }
@ -84,20 +83,18 @@ export class ArticleSearchResult extends React.Component
// but we can live with that. // but we can live with that.
for ( let i=0 ; i < this.props.data["tags!"].length ; ++i ) { for ( let i=0 ; i < this.props.data["tags!"].length ; ++i ) {
const tag = this.props.data.article_tags[ i ] ; // nb: this is the actual tag (without highlights) const tag = this.props.data.article_tags[ i ] ; // nb: this is the actual tag (without highlights)
tags.push( <div key={tag} className="tag" tags.push( <Link key={tag} className="tag" title="Search for this tag."
to = { "/tag/" + encodeURIComponent(tag) }
dangerouslySetInnerHTML = {{ __html: this.props.data["tags!"][i] }} dangerouslySetInnerHTML = {{ __html: this.props.data["tags!"][i] }}
onClick = { () => gAppRef.searchForTag( tag ) }
title = "Search for this tag."
/> ) ; /> ) ;
} }
} else { } else {
if ( this.props.data.article_tags ) { if ( this.props.data.article_tags ) {
this.props.data.article_tags.map( this.props.data.article_tags.map(
tag => tags.push( <div key={tag} className="tag" tag => tags.push( <Link key={tag} className="tag" title="Search for this tag."
onClick = { () => gAppRef.searchForTag( tag ) } to = { "/tag/" + encodeURIComponent(tag) }
title = "Search for this tag." > {tag} </Link> )
> {tag} </div> ) ;
) ) ;
} }
} }
@ -123,11 +120,10 @@ export class ArticleSearchResult extends React.Component
<div className="header"> <div className="header">
{menu} {menu}
{ pub_display_name && { pub_display_name &&
<span className="publication" <Link className="publication" title="Show this publication."
onClick = { () => gAppRef.searchForPublication( this.props.data.pub_id ) } to = { "/publication/" + this.props.data.pub_id }
title = "Show this publication." dangerouslySetInnerHTML = {{ __html: pub_display_name }}
> {pub_display_name} />
</span>
} }
<span className="title name" dangerouslySetInnerHTML={{ __html: display_title }} /> <span className="title name" dangerouslySetInnerHTML={{ __html: display_title }} />
{ article_url && { article_url &&

@ -3,7 +3,7 @@ import Select from "react-select" ;
import CreatableSelect from "react-select/creatable" ; import CreatableSelect from "react-select/creatable" ;
import { NEW_ARTICLE_PUB_PRIORITY_CUTOFF } from "./constants.js" ; import { NEW_ARTICLE_PUB_PRIORITY_CUTOFF } from "./constants.js" ;
import { PublicationSearchResult } from "./PublicationSearchResult.js" ; import { PublicationSearchResult } from "./PublicationSearchResult.js" ;
import { gAppRef } from "./index.js" ; import { gAppRef } from "./App.js" ;
import { ImageFileUploader } from "./FileUploader.js" ; import { ImageFileUploader } from "./FileUploader.js" ;
import { makeScenarioDisplayName, parseScenarioDisplayName, checkConstraints, confirmDiscardChanges, sortSelectableOptions, unloadCreatableSelect, isNumeric } from "./utils.js" ; import { makeScenarioDisplayName, parseScenarioDisplayName, checkConstraints, confirmDiscardChanges, sortSelectableOptions, unloadCreatableSelect, isNumeric } from "./utils.js" ;

@ -1,6 +1,6 @@
import { MAX_IMAGE_UPLOAD_SIZE } from "./constants.js" ; import { MAX_IMAGE_UPLOAD_SIZE } from "./constants.js" ;
import { bytesDisplayString } from "./utils.js" ; import { bytesDisplayString } from "./utils.js" ;
import { gAppRef } from "./index.js" ; import { gAppRef } from "./App.js" ;
// -------------------------------------------------------------------- // --------------------------------------------------------------------

@ -1,9 +1,10 @@
import React from "react" ; import React from "react" ;
import { Link } from "react-router-dom" ;
import { Menu, MenuList, MenuButton, MenuItem } from "@reach/menu-button" ; import { Menu, MenuList, MenuButton, MenuItem } from "@reach/menu-button" ;
import "./PublicationSearchResult.css" ; import "./PublicationSearchResult.css" ;
import { PublicationSearchResult2 } from "./PublicationSearchResult2.js" ; import { PublicationSearchResult2 } from "./PublicationSearchResult2.js" ;
import { PUBLICATION_EXCESS_ARTICLE_THRESHOLD } from "./constants.js" ; import { PUBLICATION_EXCESS_ARTICLE_THRESHOLD } from "./constants.js" ;
import { gAppRef } from "./index.js" ; import { gAppRef } from "./App.js" ;
import { makeCollapsibleList, pluralString, applyUpdatedVals, removeSpecialFields, isLink } from "./utils.js" ; import { makeCollapsibleList, pluralString, applyUpdatedVals, removeSpecialFields, isLink } from "./utils.js" ;
const axios = require( "axios" ) ; const axios = require( "axios" ) ;
@ -34,20 +35,18 @@ export class PublicationSearchResult extends React.Component
// but we can live with that. // but we can live with that.
for ( let i=0 ; i < this.props.data["tags!"].length ; ++i ) { for ( let i=0 ; i < this.props.data["tags!"].length ; ++i ) {
const tag = this.props.data.pub_tags[ i ] ; // nb: this is the actual tag (without highlights) const tag = this.props.data.pub_tags[ i ] ; // nb: this is the actual tag (without highlights)
tags.push( <div key={tag} className="tag" tags.push( <Link key={tag} className="tag" title="Search for this tag."
to = { "/tag/" + encodeURIComponent(tag) }
dangerouslySetInnerHTML = {{ __html: this.props.data["tags!"][i] }} dangerouslySetInnerHTML = {{ __html: this.props.data["tags!"][i] }}
onClick = { () => gAppRef.searchForTag( tag ) }
title = "Search for this tag."
/> ) ; /> ) ;
} }
} else { } else {
if ( this.props.data.pub_tags ) { if ( this.props.data.pub_tags ) {
this.props.data.pub_tags.map( this.props.data.pub_tags.map(
tag => tags.push( <div key={tag} className="tag" tag => tags.push( <Link key={tag} className="tag" title="Search for this tag."
onClick = { () => gAppRef.searchForTag( tag ) } to = { "/tag/" + encodeURIComponent(tag) }
title = "Search for this tag." > {tag} </Link> )
> {tag} </div> ) ;
) ) ;
} }
} }
@ -56,10 +55,9 @@ export class PublicationSearchResult extends React.Component
if ( this.props.data.articles ) { if ( this.props.data.articles ) {
for ( let i=0 ; i < this.props.data.articles.length ; ++i ) { for ( let i=0 ; i < this.props.data.articles.length ; ++i ) {
const article = this.props.data.articles[ i ] ; const article = this.props.data.articles[ i ] ;
articles.push( <span articles.push( <Link title="Show this article."
to = { "/article/" + article.article_id }
dangerouslySetInnerHTML = {{ __html: article.article_title }} dangerouslySetInnerHTML = {{ __html: article.article_title }}
onClick = { () => gAppRef.searchForArticle( article.article_id ) }
title = "Show this article."
/> ) ; /> ) ;
} }
} }
@ -83,16 +81,14 @@ export class PublicationSearchResult extends React.Component
<div className="header"> <div className="header">
{menu} {menu}
{ publ && { publ &&
<span className="publisher" <Link className="publisher" title="Show this publisher."
onClick={ () => gAppRef.searchForPublisher( this.props.data.publ_id ) } to = { "/publisher/" + this.props.data.publ_id }
title = "Show this publisher."
> {publ.publ_name} > {publ.publ_name}
</span> </Link>
} }
<span className="name" <Link className="name" title="Show this publication."
to = { "/publication/" + this.props.data.pub_id }
dangerouslySetInnerHTML = {{ __html: this._makeDisplayName( true ) }} dangerouslySetInnerHTML = {{ __html: this._makeDisplayName( true ) }}
onClick = { () => gAppRef.searchForPublication( this.props.data.pub_id ) }
title = "Show this publication."
/> />
{ pub_url && { pub_url &&
<a href={pub_url} className="open-link" target="_blank" rel="noopener noreferrer"> <a href={pub_url} className="open-link" target="_blank" rel="noopener noreferrer">

@ -2,7 +2,7 @@ import React from "react" ;
import Select from "react-select" ; import Select from "react-select" ;
import CreatableSelect from "react-select/creatable" ; import CreatableSelect from "react-select/creatable" ;
import ReactDragListView from "react-drag-listview/lib/index.js" ; import ReactDragListView from "react-drag-listview/lib/index.js" ;
import { gAppRef } from "./index.js" ; import { gAppRef } from "./App.js" ;
import { ImageFileUploader } from "./FileUploader.js" ; import { ImageFileUploader } from "./FileUploader.js" ;
import { checkConstraints, confirmDiscardChanges, sortSelectableOptions, unloadCreatableSelect, ciCompare, isNumeric } from "./utils.js" ; import { checkConstraints, confirmDiscardChanges, sortSelectableOptions, unloadCreatableSelect, ciCompare, isNumeric } from "./utils.js" ;

@ -1,10 +1,11 @@
import React from "react" ; import React from "react" ;
import { Link } from "react-router-dom" ;
import { Menu, MenuList, MenuButton, MenuItem } from "@reach/menu-button" ; import { Menu, MenuList, MenuButton, MenuItem } from "@reach/menu-button" ;
import { PublisherSearchResult2 } from "./PublisherSearchResult2.js" import { PublisherSearchResult2 } from "./PublisherSearchResult2.js"
import "./PublisherSearchResult.css" ; import "./PublisherSearchResult.css" ;
import { PublicationSearchResult } from "./PublicationSearchResult.js" import { PublicationSearchResult } from "./PublicationSearchResult.js"
import { PUBLISHER_EXCESS_PUBLICATION_THRESHOLD } from "./constants.js" ; import { PUBLISHER_EXCESS_PUBLICATION_THRESHOLD } from "./constants.js" ;
import { gAppRef } from "./index.js" ; import { gAppRef } from "./App.js" ;
import { makeCollapsibleList, pluralString, applyUpdatedVals, removeSpecialFields } from "./utils.js" ; import { makeCollapsibleList, pluralString, applyUpdatedVals, removeSpecialFields } from "./utils.js" ;
const axios = require( "axios" ) ; const axios = require( "axios" ) ;
@ -28,10 +29,9 @@ export class PublisherSearchResult extends React.Component
pubs.push( pub[1] ) ; pubs.push( pub[1] ) ;
} }
pubs.sort( (lhs,rhs) => rhs.time_created - lhs.time_created ) ; pubs.sort( (lhs,rhs) => rhs.time_created - lhs.time_created ) ;
pubs = pubs.map( p => <span pubs = pubs.map( p => <Link title="Show this publication."
to = { "/publication/" + p.pub_id }
dangerouslySetInnerHTML = {{ __html: PublicationSearchResult.makeDisplayName(p) }} dangerouslySetInnerHTML = {{ __html: PublicationSearchResult.makeDisplayName(p) }}
onClick = { () => gAppRef.searchForPublication( p.pub_id ) }
title = "Show this publication."
/> ) ; /> ) ;
// prepare the menu // prepare the menu

@ -1,5 +1,5 @@
import React from "react" ; import React from "react" ;
import { gAppRef } from "./index.js" ; import { gAppRef } from "./App.js" ;
import { ImageFileUploader } from "./FileUploader.js" ; import { ImageFileUploader } from "./FileUploader.js" ;
import { checkConstraints, confirmDiscardChanges, ciCompare } from "./utils.js" ; import { checkConstraints, confirmDiscardChanges, ciCompare } from "./utils.js" ;

@ -17,11 +17,9 @@
.search-result.publisher .header { border: 1px solid #c0c0c0 ; background: #eabe51 ; } .search-result.publisher .header { border: 1px solid #c0c0c0 ; background: #eabe51 ; }
.search-result.publication .header { border: 1px solid #c0c0c0 ; background: #e5cea0 ; } .search-result.publication .header { border: 1px solid #c0c0c0 ; background: #e5cea0 ; }
.search-result.publication .header .name { cursor: pointer ; } .search-result.publication .header a.name { color: inherit ; text-decoration: none ; }
.search-result.publication .header .publisher { cursor: pointer ; }
.search-result.article .header { border: 1px solid #c0c0c0 ; background: #d3edfc ; } .search-result.article .header { border: 1px solid #c0c0c0 ; background: #d3edfc ; }
.search-result.article .header .subtitle { font-size: 80% ; font-style: italic ; color: #333 ; } .search-result.article .header .subtitle { font-size: 80% ; font-style: italic ; color: #333 ; }
.search-result.article .header .publication { cursor: pointer ; }
.search-result.publication .header .publisher , .search-result.article .header .publication { .search-result.publication .header .publisher , .search-result.article .header .publication {
float: right ; margin-right: 0.5em ; font-size: 80% ; font-style: italic ; color: #444 ; float: right ; margin-right: 0.5em ; font-size: 80% ; font-style: italic ; color: #444 ;
@ -32,16 +30,15 @@
.search-result .content p:first-child { margin-top: 0 ; } .search-result .content p:first-child { margin-top: 0 ; }
.search-result .content .image { float: left ; margin: 0.25em 0.5em 0.5em 0 ; max-height: 8em ; max-width: 6em ; } .search-result .content .image { float: left ; margin: 0.25em 0.5em 0.5em 0 ; max-height: 8em ; max-width: 6em ; }
.search-result .content .collapsible { margin-top:0.5em ; font-size: 90% ; color: #333 ; } .search-result .content .collapsible { margin-top:0.5em ; font-size: 90% ; color: #333 ; }
.search-result .content .collapsible a { color: #333 ; text-decoration: none ; }
.search-result .content .collapsible .caption { cursor: pointer ; } .search-result .content .collapsible .caption { cursor: pointer ; }
.search-result .content .collapsible .caption img { height: 0.75em ; margin-left: 0.25em ; } .search-result .content .collapsible .caption img { height: 0.75em ; margin-left: 0.25em ; }
.search-result .content .collapsible .count { font-size: 80% ; font-style: italic ; color: #666 ; } .search-result .content .collapsible .count { font-size: 80% ; font-style: italic ; color: #666 ; }
.search-result .content .collapsible ul { margin: 0 0 0 1em ; } .search-result .content .collapsible ul { margin: 0 0 0 1em ; }
.search-result.publisher .content .collapsible li { cursor: pointer ; }
.search-result.publication .content .collapsible li { cursor: pointer ; }
.search-result .footer { clear: both ; padding: 0 5px ; font-size: 80% ; font-style: italic ; color: #666 ; } .search-result .footer { clear: both ; padding: 0 5px ; font-size: 80% ; font-style: italic ; color: #666 ; }
.search-result .footer .tag { display: inline ; margin-right: 0.25em ; padding: 0 2px ; border: 1px solid #ccc ; background: #f0f0f0 ; cursor: pointer ; } .search-result .footer a { color: #666 ; text-decoration: none ; }
.search-result.article .footer .author { cursor: pointer ; } .search-result .footer .tag { display: inline ; margin-right: 0.25em ; padding: 0 2px ; border: 1px solid #ccc ; background: #f0f0f0 ; }
.search-result .hilite { padding: 0 2px ; background: #ffffa0 ; } .search-result .hilite { padding: 0 2px ; background: #ffffa0 ; }
.search-result.publisher .header .hilite { background: #e0a040 ; } .search-result.publisher .header .hilite { background: #e0a040 ; }

@ -3,7 +3,7 @@ import "./SearchResults.css" ;
import { PublisherSearchResult } from "./PublisherSearchResult" ; import { PublisherSearchResult } from "./PublisherSearchResult" ;
import { PublicationSearchResult } from "./PublicationSearchResult" ; import { PublicationSearchResult } from "./PublicationSearchResult" ;
import { ArticleSearchResult } from "./ArticleSearchResult" ; import { ArticleSearchResult } from "./ArticleSearchResult" ;
import { gAppRef } from "./index.js" ; import { gAppRef } from "./App.js" ;
// -------------------------------------------------------------------- // --------------------------------------------------------------------

@ -1,6 +1,8 @@
export let MAX_IMAGE_UPLOAD_SIZE = ( 1 * 1024*1024 ) ; export const APP_NAME = "ASL Articles" ;
export let PUBLISHER_EXCESS_PUBLICATION_THRESHOLD = 5 ; export const MAX_IMAGE_UPLOAD_SIZE = ( 1 * 1024*1024 ) ;
export let PUBLICATION_EXCESS_ARTICLE_THRESHOLD = 8 ;
export let NEW_ARTICLE_PUB_PRIORITY_CUTOFF = ( 24 * 60 * 60 ) ; export const PUBLISHER_EXCESS_PUBLICATION_THRESHOLD = 5 ;
export const PUBLICATION_EXCESS_ARTICLE_THRESHOLD = 8 ;
export const NEW_ARTICLE_PUB_PRIORITY_CUTOFF = ( 24 * 60 * 60 ) ;

@ -1,11 +1,44 @@
import React from "react" ; import React from "react" ;
import ReactDOM from "react-dom" ; import ReactDOM from "react-dom" ;
import App from "./App" ; import { BrowserRouter, Route, Switch } from "react-router-dom" ;
import { App, gAppRef } from "./App" ;
import { PublicationSearchResult } from "./PublicationSearchResult" ;
import "./index.css" ; import "./index.css" ;
// -------------------------------------------------------------------- // --------------------------------------------------------------------
export let gAppRef = ReactDOM.render( ReactDOM.render(
<App />, <BrowserRouter>
<Switch>
<Route path="/publisher/:publId" render={ (props) => <App {...props} key={"publ:"+props.match.params.publId}
doSearch = { () => gAppRef.runSpecialSearch( "/search/publisher/"+gAppRef.props.match.params.publId, null,
() => gAppRef.setWindowTitleFromSearchResults( "publisher", "publ_id", gAppRef.props.match.params.publId, "publ_name" )
) }
/> } />
<Route path="/publication/:pubId" render={ (props) => <App {...props} key={"pub:"+props.match.params.pubId}
doSearch = { () => gAppRef.runSpecialSearch( "/search/publication/"+gAppRef.props.match.params.pubId, null,
() => gAppRef.setWindowTitleFromSearchResults( "publication", "pub_id", gAppRef.props.match.params.pubId,
sr => { return PublicationSearchResult.makeDisplayName( sr ) }
) ) }
/> } />
<Route path="/article/:articleId" render={ (props) => <App {...props} key={"article:"+props.match.params.articleId}
doSearch = { () => gAppRef.runSpecialSearch( "/search/article/"+gAppRef.props.match.params.articleId, null,
() => gAppRef.setWindowTitleFromSearchResults( "article", "article_id", gAppRef.props.match.params.articleId, "article_title" )
) }
/> } />
<Route path="/author/:authorId" render={ (props) => <App {...props} key={"author:"+props.match.params.authorId}
doSearch = { () => gAppRef.runSpecialSearch( "/search/author/"+gAppRef.props.match.params.authorId, null,
() => gAppRef.setWindowTitle( gAppRef.caches.authors[ gAppRef.props.match.params.authorId ].author_name )
) }
/> } />
<Route path="/tag/:tag" render={ (props) => <App {...props} key={"tag:"+props.match.params.tag}
doSearch = { () => gAppRef.runSpecialSearch( "/search/tag/"+gAppRef.props.match.params.tag, null,
() => gAppRef.setWindowTitle( gAppRef.props.match.params.tag )
) }
/> } />
<Route path="/" exact component={App} />
<Route path="/" render={ (props) => <App {...props} warning="Unknown URL." key="unknown-url" /> } />
</Switch>
</BrowserRouter>,
document.getElementById( "app" ) document.getElementById( "app" )
) ; ) ;

@ -1,6 +1,6 @@
import React from "react" ; import React from "react" ;
import ReactDOMServer from "react-dom/server" ; import ReactDOMServer from "react-dom/server" ;
import { gAppRef } from "./index.js" ; import { gAppRef } from "./App.js" ;
const isEqual = require( "lodash.isequal" ) ; const isEqual = require( "lodash.isequal" ) ;

Loading…
Cancel
Save