diff --git a/asl_articles/search.py b/asl_articles/search.py index e468de9..20a10c9 100644 --- a/asl_articles/search.py +++ b/asl_articles/search.py @@ -9,7 +9,7 @@ import tempfile import re import logging -from flask import request, jsonify, abort +from flask import request, jsonify import asl_articles from asl_articles import app, db @@ -117,12 +117,12 @@ def search_publishers(): # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -@app.route( "/search/publisher/", methods=["POST","GET"] ) +@app.route( "/search/publisher/", methods=["POST","GET"] ) def search_publisher( publ_id ): """Search for a publisher.""" publ = Publisher.query.get( publ_id ) if not publ: - abort( 404 ) + return jsonify( [] ) results = [ get_publisher_vals( publ, True ) ] pubs = sorted( publ.publications, key=get_publication_sort_key, reverse=True ) for pub in pubs: @@ -131,12 +131,12 @@ def search_publisher( publ_id ): # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -@app.route( "/search/publication/", methods=["POST","GET"] ) +@app.route( "/search/publication/", methods=["POST","GET"] ) def search_publication( pub_id ): """Search for a publication.""" pub = Publication.query.get( pub_id ) if not pub: - abort( 404 ) + return jsonify( [] ) results = [ get_publication_vals( pub, True, True ) ] articles = sorted( pub.articles, key=get_article_sort_key ) for article in articles: @@ -145,12 +145,12 @@ def search_publication( pub_id ): # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -@app.route( "/search/article/", methods=["POST","GET"] ) +@app.route( "/search/article/", methods=["POST","GET"] ) def search_article( article_id ): """Search for an article.""" article = Article.query.get( article_id ) if not article: - abort( 404 ) + return jsonify( [] ) results = [ get_article_vals( article, True ) ] if article.pub_id: pub = Publication.query.get( article.pub_id ) @@ -160,12 +160,12 @@ def search_article( article_id ): # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -@app.route( "/search/author/", methods=["POST","GET"] ) +@app.route( "/search/author/", methods=["POST","GET"] ) def search_author( author_id ): """Search for an author.""" author = Author.query.get( author_id ) if not author: - abort( 404 ) + return jsonify( [] ) author_name = '"{}"'.format( author.author_name.replace( '"', '""' ) ) return _do_search( author_name, [ "authors" ] ) diff --git a/asl_articles/tests/fixtures/search.json b/asl_articles/tests/fixtures/search.json index 892c8c8..35db63d 100644 --- a/asl_articles/tests/fixtures/search.json +++ b/asl_articles/tests/fixtures/search.json @@ -65,7 +65,18 @@ { "article_id": 520, "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_tags": "jagdpanzer", "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" } ], diff --git a/asl_articles/tests/test_search.py b/asl_articles/tests/test_search.py index 677c039..d8d6c08 100644 --- a/asl_articles/tests/test_search.py +++ b/asl_articles/tests/test_search.py @@ -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_publications import create_publication, edit_publication 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, \ - 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 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 ) - assert elem.text == expected - seq_no = get_search_seqno() + assert elem.text == expected_publ elem.click() - wait_for( 2, lambda: get_search_seqno() != seq_no ) - assert find_child( "#search-form input.query" ).get_attribute( "value" ) == "" - return get_search_results() + wait_for( 2, lambda: get_search_result_names() == expected_sr ) # find a publication and click on its parent publisher results = do_search( "fantastic" ) assert len(results) == 1 - click_on_publisher( results[0], "View From The Trenches" ) - assert get_search_result_names() == [ + click_on_publisher( results[0], "View From The Trenches", [ "View From The Trenches", "View From The Trenches (100)" - ] + ] ) # --------------------------------------------------------------------- @@ -303,7 +299,7 @@ def test_publication_search( webdriver, flask_app, dbconn ): # initialize 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() if "article" in classes: elem = find_child( ".header .publication", sr ) @@ -313,36 +309,30 @@ def test_publication_search( webdriver, flask_app, dbconn ): else: assert "publication" in classes elem = find_child( ".header .name", sr ) - assert elem.text == expected - seq_no = get_search_seqno() + assert elem.text == expected_pub elem.click() - wait_for( 2, lambda: get_search_seqno() != seq_no ) - assert find_child( "#search-form input.query" ).get_attribute( "value" ) == "" - return get_search_results() + wait_for( 2, lambda: get_search_result_names() == expected_sr ) # find a publication and click on it results = do_search( "vftt" ) sr = find_search_result( "View From The Trenches (100)", results ) - click_on_publication( sr, "View From The Trenches (100)" ) - assert get_search_result_names() == [ + click_on_publication( sr, "View From The Trenches (100)", [ "View From The Trenches (100)", "Jagdpanzer 38(t) Hetzer" - ] + ] ) # find an article and click on its parent publication results = do_search( "neutral" ) assert len(results) == 1 - click_on_publication( results[0], "ASL Journal (5)" ) - assert get_search_result_names() == [ + click_on_publication( results[0], "ASL Journal (5)", [ "ASL Journal (5)", "The Jungle Isn't Neutral", "Hunting DUKWs and Buffalos" - ] + ] ) # find a publisher and click on one of its publications results = do_search( "mmp" ) assert len(results) == 1 - click_on_publication( results[0], "ASL Journal (4)" ) - assert get_search_result_names() == [ + click_on_publication( results[0], "ASL Journal (4)", [ "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 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 ) elem = elems[0] # nb: we just use the first one - assert elem.text == expected - seq_no = get_search_seqno() + assert elem.text == expected_pub 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 one of its articles results = do_search( "vftt" ) sr = find_search_result( "View From The Trenches (100)", results ) - click_on_article( sr, "Jagdpanzer 38(t) Hetzer" ) - assert get_search_result_names() == [ + click_on_article( sr, "Jagdpanzer 38(t) Hetzer", [ "Jagdpanzer 38(t) Hetzer", "View From The Trenches (100)" - ] + ] ) # --------------------------------------------------------------------- @@ -378,23 +365,20 @@ def test_author_search( webdriver, flask_app, dbconn ): # initialize 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 ) assert len(authors) == 1 - assert authors[0].text == expected - seq_no = get_search_seqno() + assert authors[0].text == expected_author authors[0].click() - wait_for( 2, lambda: get_search_seqno() != seq_no ) - assert find_child( "#search-form input.query" ).get_attribute( "value" ) == "" + wait_for( 2, lambda: get_search_result_names() == expected_sr ) return get_search_results() # find an article and click on the author results = do_search( SEARCH_ALL ) - sr = find_search_result( "Jagdpanzer 38(t) Hetzer" ) - results = click_on_author( sr, "Michael Davies" ) - assert get_search_result_names( results ) == [ + sr = find_search_result( "Jagdpanzer 38(t) Hetzer", results ) + click_on_author( sr, "Michael Davies", [ "Jagdpanzer 38(t) Hetzer" - ] + ] ) # --------------------------------------------------------------------- @@ -404,11 +388,9 @@ def test_tag_search( webdriver, flask_app, dbconn ): # initialize init_tests( webdriver, flask_app, dbconn, fixtures="search.json" ) - def click_on_tag( tag ): - seq_no = get_search_seqno() + def click_on_tag( tag, expected ): tag.click() - wait_for( 2, lambda: get_search_seqno() != seq_no ) - assert find_child( "#search-form input.query" ).get_attribute( "value" ) == "" + wait_for( 2, lambda: get_search_result_names() == expected ) return get_search_results() def get_tags( sr ): return find_children( ".tags .tag", sr ) @@ -418,25 +400,109 @@ def test_tag_search( webdriver, flask_app, dbconn ): assert len(results) == 1 tags = get_tags( results[0] ) assert [ t.text for t in tags ] == [ "#aslj", "#mortars" ] - results = click_on_tag( tags[0] ) expected = [ "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" ] - assert get_search_result_names( results ) == expected + results = click_on_tag( tags[0], expected ) # click on another "#aslj" tag tags = get_tags( results[0] ) assert [ t.text for t in tags ] == [ "#aslj" ] - results = click_on_tag( tags[0] ) - assert get_search_result_names( results ) == expected + results = click_on_tag( tags[0], expected ) # 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 ) assert [ t.text for t in tags ] == [ "#aslj", "#PTO" ] - results = click_on_tag( tags[1] ) - assert get_search_result_names( results ) == [ "The Jungle Isn't Neutral" ] + click_on_tag( tags[1], [ "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" + ] ) # --------------------------------------------------------------------- diff --git a/asl_articles/tests/utils.py b/asl_articles/tests/utils.py index d86107c..c793a43 100644 --- a/asl_articles/tests/utils.py +++ b/asl_articles/tests/utils.py @@ -55,7 +55,7 @@ def init_tests( webdriver, flask_app, dbconn, **kwargs ): kwargs[ "disable_constraints" ] = 1 if to_bool( kwargs.pop( "disable_confirm_discard_changes", True ) ): kwargs[ "disable_confirm_discard_changes" ] = 1 - webdriver.get( webdriver.make_url( "/", **kwargs ) ) + webdriver.get( webdriver.make_url( "", **kwargs ) ) wait_for_elem( 2, "#search-form" ) return session diff --git a/web/docker/nginx-default.conf b/web/docker/nginx-default.conf index da51e44..dd0eaf1 100644 --- a/web/docker/nginx-default.conf +++ b/web/docker/nginx-default.conf @@ -5,7 +5,7 @@ server { location / { root /usr/share/nginx/html ; - index index.html index.htm ; + try_files $uri $uri/ /index.html ; } location /api { diff --git a/web/package-lock.json b/web/package-lock.json index 1ba2e87..1e52950 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -6181,6 +6181,11 @@ "resolved": "https://registry.npmjs.org/growly/-/growly-1.3.0.tgz", "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": { "version": "5.1.1", "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", "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": { "version": "1.0.1", "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", "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": { "version": "0.8.0", "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", "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": { "version": "3.2.0", "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", "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": { "version": "0.2.1", "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", "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": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz", @@ -13207,6 +13291,11 @@ "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": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", diff --git a/web/package.json b/web/package.json index 5656122..ea43dd5 100644 --- a/web/package.json +++ b/web/package.json @@ -14,6 +14,7 @@ "react-dom": "^16.11.0", "react-drag-listview": "^0.1.6", "react-draggable": "^4.1.0", + "react-router-dom": "^5.1.2", "react-scripts": "3.2.0", "react-select": "^3.0.8", "react-toastify": "^5.4.1" diff --git a/web/src/App.js b/web/src/App.js index 86ec333..4c74a62 100644 --- a/web/src/App.js +++ b/web/src/App.js @@ -13,14 +13,17 @@ import { ArticleSearchResult } from "./ArticleSearchResult" ; import ModalForm from "./ModalForm"; import AskDialog from "./AskDialog" ; import { makeSmartBulletList } from "./utils.js" ; +import { APP_NAME } from "./constants.js" ; import "./App.css" ; const axios = require( "axios" ) ; const queryString = require( "query-string" ) ; +export let gAppRef = null ; + // -------------------------------------------------------------------- -export default class App extends React.Component +export class App extends React.Component { constructor( props ) { @@ -33,6 +36,8 @@ export default class App extends React.Component askDialog: null, startupTasks: [ "caches.publishers", "caches.publications", "caches.authors", "caches.scenarios", "caches.tags" ], } ; + gAppRef = this ; + this.setWindowTitle( null ) ; // initialize this.args = queryString.parse( window.location.search ) ; @@ -74,14 +79,17 @@ export default class App extends React.Component this._onSpecialSearch( "/search/publishers" ) } - >Show publishers + onSelect = { () => this.runSpecialSearch( "/search/publishers", null, + () => { this.setWindowTitle( "All publishers" ) } + ) } > Show publishers this._onSpecialSearch( "/search/tag/technique", {randomize:1} ) } - >Show technique + onSelect = { () => this.runSpecialSearch( "/search/tag/technique", {randomize:1}, + () => { this.setWindowTitle( "Technique" ) } + ) } > Show technique this._onSpecialSearch( "/search/tag/tips", {randomize:1} ) } - >Show tips + onSelect = { () => this.runSpecialSearch( "/search/tag/tips", {randomize:1}, + () => { this.setWindowTitle( "Tips" ) } + ) } > Show tips
PublisherSearchResult.onNewPublisher( this._onNewPublisher.bind(this) ) } @@ -98,7 +106,7 @@ export default class App extends React.Component content = (
{menu} @@ -212,20 +220,16 @@ export default class App extends React.Component } this._doSearch( "/search", { query: query } ) ; } - searchForPublisher( publ_id ) { this._onSpecialSearch( "/search/publisher/" + publ_id ) ; } - 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 ) { + runSpecialSearch( url, args, onDone ) { // run the search this._searchFormRef.current.setState( { queryString: "" } ) ; if ( ! args ) args = {} ; - this._doSearch( url, args ) ; + this._doSearch( url, args, onDone ) ; } - _doSearch( url, args ) { + _doSearch( url, args, onDone ) { // do the search + this.setWindowTitle( null ) ; this.setState( { searchResults: "(loading)" } ) ; args.no_hilite = this._disableSearchResultHighlighting ; axios.post( @@ -235,6 +239,8 @@ export default class App extends React.Component ReactDOM.findDOMNode( this._searchResultsRef.current ).scrollTo( 0, 0 ) ; this._setFocusTo = this._searchFormRef.current.queryStringRef.current ; this.setState( { searchResults: resp.data, searchSeqNo: this.state.searchSeqNo+1 } ) ; + if ( onDone ) + onDone() ; } ) .catch( 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.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 ; } diff --git a/web/src/ArticleSearchResult.js b/web/src/ArticleSearchResult.js index e017e94..92ce744 100644 --- a/web/src/ArticleSearchResult.js +++ b/web/src/ArticleSearchResult.js @@ -1,9 +1,10 @@ import React from "react" ; +import { Link } from "react-router-dom" ; import { Menu, MenuList, MenuButton, MenuItem } from "@reach/menu-button" ; import { ArticleSearchResult2 } from "./ArticleSearchResult2.js" ; import "./ArticleSearchResult.css" ; import { PublicationSearchResult } from "./PublicationSearchResult.js" ; -import { gAppRef } from "./index.js" ; +import { gAppRef } from "./App.js" ; import { makeScenarioDisplayName, applyUpdatedVals, removeSpecialFields, makeCommaList, isLink } from "./utils.js" ; 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 for ( let i=0 ; i < this.props.data["authors!"].length ; ++i ) { const author_id = this.props.data.article_authors[ i ] ; - authors.push( gAppRef.searchForAuthor( author_id ) } - title = "Show articles from this author." /> ) ; } } else { // 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 ) { const author_id = this.props.data.article_authors[ i ] ; - authors.push( 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. 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) - tags.push(
gAppRef.searchForTag( tag ) } - title = "Search for this tag." /> ) ; } } else { if ( this.props.data.article_tags ) { this.props.data.article_tags.map( - tag => tags.push(
gAppRef.searchForTag( tag ) } - title = "Search for this tag." - > {tag}
- ) ) ; + tag => tags.push( {tag} ) + ) ; } } @@ -123,11 +120,10 @@ export class ArticleSearchResult extends React.Component
{menu} { pub_display_name && - gAppRef.searchForPublication( this.props.data.pub_id ) } - title = "Show this publication." - > {pub_display_name} - + } { article_url && diff --git a/web/src/ArticleSearchResult2.js b/web/src/ArticleSearchResult2.js index 5842422..6778dc6 100644 --- a/web/src/ArticleSearchResult2.js +++ b/web/src/ArticleSearchResult2.js @@ -3,7 +3,7 @@ import Select from "react-select" ; import CreatableSelect from "react-select/creatable" ; import { NEW_ARTICLE_PUB_PRIORITY_CUTOFF } from "./constants.js" ; import { PublicationSearchResult } from "./PublicationSearchResult.js" ; -import { gAppRef } from "./index.js" ; +import { gAppRef } from "./App.js" ; import { ImageFileUploader } from "./FileUploader.js" ; import { makeScenarioDisplayName, parseScenarioDisplayName, checkConstraints, confirmDiscardChanges, sortSelectableOptions, unloadCreatableSelect, isNumeric } from "./utils.js" ; diff --git a/web/src/FileUploader.js b/web/src/FileUploader.js index d1038a7..aeb6522 100644 --- a/web/src/FileUploader.js +++ b/web/src/FileUploader.js @@ -1,6 +1,6 @@ import { MAX_IMAGE_UPLOAD_SIZE } from "./constants.js" ; import { bytesDisplayString } from "./utils.js" ; -import { gAppRef } from "./index.js" ; +import { gAppRef } from "./App.js" ; // -------------------------------------------------------------------- diff --git a/web/src/PublicationSearchResult.js b/web/src/PublicationSearchResult.js index 0f0d607..c8cb6b4 100644 --- a/web/src/PublicationSearchResult.js +++ b/web/src/PublicationSearchResult.js @@ -1,9 +1,10 @@ import React from "react" ; +import { Link } from "react-router-dom" ; import { Menu, MenuList, MenuButton, MenuItem } from "@reach/menu-button" ; import "./PublicationSearchResult.css" ; import { PublicationSearchResult2 } from "./PublicationSearchResult2.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" ; const axios = require( "axios" ) ; @@ -34,20 +35,18 @@ export class PublicationSearchResult extends React.Component // but we can live with that. 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) - tags.push(
gAppRef.searchForTag( tag ) } - title = "Search for this tag." /> ) ; } } else { if ( this.props.data.pub_tags ) { this.props.data.pub_tags.map( - tag => tags.push(
gAppRef.searchForTag( tag ) } - title = "Search for this tag." - > {tag}
- ) ) ; + tag => tags.push( {tag} ) + ) ; } } @@ -56,10 +55,9 @@ export class PublicationSearchResult extends React.Component if ( this.props.data.articles ) { for ( let i=0 ; i < this.props.data.articles.length ; ++i ) { const article = this.props.data.articles[ i ] ; - articles.push( gAppRef.searchForArticle( article.article_id ) } - title = "Show this article." /> ) ; } } @@ -83,16 +81,14 @@ export class PublicationSearchResult extends React.Component
{menu} { publ && - gAppRef.searchForPublisher( this.props.data.publ_id ) } - title = "Show this publisher." + {publ.publ_name} - + } - gAppRef.searchForPublication( this.props.data.pub_id ) } - title = "Show this publication." /> { pub_url && diff --git a/web/src/PublicationSearchResult2.js b/web/src/PublicationSearchResult2.js index b0bd61a..3086ee2 100644 --- a/web/src/PublicationSearchResult2.js +++ b/web/src/PublicationSearchResult2.js @@ -2,7 +2,7 @@ import React from "react" ; import Select from "react-select" ; import CreatableSelect from "react-select/creatable" ; 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 { checkConstraints, confirmDiscardChanges, sortSelectableOptions, unloadCreatableSelect, ciCompare, isNumeric } from "./utils.js" ; diff --git a/web/src/PublisherSearchResult.js b/web/src/PublisherSearchResult.js index fd348cc..c50f2a2 100644 --- a/web/src/PublisherSearchResult.js +++ b/web/src/PublisherSearchResult.js @@ -1,10 +1,11 @@ import React from "react" ; +import { Link } from "react-router-dom" ; import { Menu, MenuList, MenuButton, MenuItem } from "@reach/menu-button" ; import { PublisherSearchResult2 } from "./PublisherSearchResult2.js" import "./PublisherSearchResult.css" ; import { PublicationSearchResult } from "./PublicationSearchResult.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" ; const axios = require( "axios" ) ; @@ -28,10 +29,9 @@ export class PublisherSearchResult extends React.Component pubs.push( pub[1] ) ; } pubs.sort( (lhs,rhs) => rhs.time_created - lhs.time_created ) ; - pubs = pubs.map( p => gAppRef.searchForPublication( p.pub_id ) } - title = "Show this publication." /> ) ; // prepare the menu diff --git a/web/src/PublisherSearchResult2.js b/web/src/PublisherSearchResult2.js index 0021554..61b0078 100644 --- a/web/src/PublisherSearchResult2.js +++ b/web/src/PublisherSearchResult2.js @@ -1,5 +1,5 @@ import React from "react" ; -import { gAppRef } from "./index.js" ; +import { gAppRef } from "./App.js" ; import { ImageFileUploader } from "./FileUploader.js" ; import { checkConstraints, confirmDiscardChanges, ciCompare } from "./utils.js" ; diff --git a/web/src/SearchResults.css b/web/src/SearchResults.css index b93cae8..425a9a9 100644 --- a/web/src/SearchResults.css +++ b/web/src/SearchResults.css @@ -17,11 +17,9 @@ .search-result.publisher .header { border: 1px solid #c0c0c0 ; background: #eabe51 ; } .search-result.publication .header { border: 1px solid #c0c0c0 ; background: #e5cea0 ; } -.search-result.publication .header .name { cursor: pointer ; } -.search-result.publication .header .publisher { cursor: pointer ; } +.search-result.publication .header a.name { color: inherit ; text-decoration: none ; } .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 .publication { cursor: pointer ; } .search-result.publication .header .publisher , .search-result.article .header .publication { 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 .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 a { color: #333 ; text-decoration: none ; } .search-result .content .collapsible .caption { cursor: pointer ; } .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 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 .tag { display: inline ; margin-right: 0.25em ; padding: 0 2px ; border: 1px solid #ccc ; background: #f0f0f0 ; cursor: pointer ; } -.search-result.article .footer .author { cursor: pointer ; } +.search-result .footer a { color: #666 ; text-decoration: none ; } +.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.publisher .header .hilite { background: #e0a040 ; } diff --git a/web/src/SearchResults.js b/web/src/SearchResults.js index 3762d46..03fa804 100644 --- a/web/src/SearchResults.js +++ b/web/src/SearchResults.js @@ -3,7 +3,7 @@ import "./SearchResults.css" ; import { PublisherSearchResult } from "./PublisherSearchResult" ; import { PublicationSearchResult } from "./PublicationSearchResult" ; import { ArticleSearchResult } from "./ArticleSearchResult" ; -import { gAppRef } from "./index.js" ; +import { gAppRef } from "./App.js" ; // -------------------------------------------------------------------- diff --git a/web/src/constants.js b/web/src/constants.js index 31bace2..f49b830 100644 --- a/web/src/constants.js +++ b/web/src/constants.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 let PUBLICATION_EXCESS_ARTICLE_THRESHOLD = 8 ; +export const MAX_IMAGE_UPLOAD_SIZE = ( 1 * 1024*1024 ) ; -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 ) ; diff --git a/web/src/index.js b/web/src/index.js index 2699ecb..68cf23c 100644 --- a/web/src/index.js +++ b/web/src/index.js @@ -1,11 +1,44 @@ import React from "react" ; 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" ; // -------------------------------------------------------------------- -export let gAppRef = ReactDOM.render( - , +ReactDOM.render( + + + gAppRef.runSpecialSearch( "/search/publisher/"+gAppRef.props.match.params.publId, null, + () => gAppRef.setWindowTitleFromSearchResults( "publisher", "publ_id", gAppRef.props.match.params.publId, "publ_name" ) + ) } + /> } /> + 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 ) } + ) ) } + /> } /> + gAppRef.runSpecialSearch( "/search/article/"+gAppRef.props.match.params.articleId, null, + () => gAppRef.setWindowTitleFromSearchResults( "article", "article_id", gAppRef.props.match.params.articleId, "article_title" ) + ) } + /> } /> + gAppRef.runSpecialSearch( "/search/author/"+gAppRef.props.match.params.authorId, null, + () => gAppRef.setWindowTitle( gAppRef.caches.authors[ gAppRef.props.match.params.authorId ].author_name ) + ) } + /> } /> + gAppRef.runSpecialSearch( "/search/tag/"+gAppRef.props.match.params.tag, null, + () => gAppRef.setWindowTitle( gAppRef.props.match.params.tag ) + ) } + /> } /> + + } /> + + , document.getElementById( "app" ) ) ; diff --git a/web/src/utils.js b/web/src/utils.js index 73226ca..ea6a481 100644 --- a/web/src/utils.js +++ b/web/src/utils.js @@ -1,6 +1,6 @@ import React from "react" ; import ReactDOMServer from "react-dom/server" ; -import { gAppRef } from "./index.js" ; +import { gAppRef } from "./App.js" ; const isEqual = require( "lodash.isequal" ) ;