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 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/<int:publ_id>", methods=["POST","GET"] )
@app.route( "/search/publisher/<publ_id>", 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/<int:pub_id>", methods=["POST","GET"] )
@app.route( "/search/publication/<pub_id>", 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/<int:article_id>", methods=["POST","GET"] )
@app.route( "/search/article/<article_id>", 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/<int:author_id>", methods=["POST","GET"] )
@app.route( "/search/author/<author_id>", 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" ] )

@ -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"
}
],

@ -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"
] )
# ---------------------------------------------------------------------

@ -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

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

@ -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",

@ -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"

@ -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
<MenuButton />
<MenuList>
<MenuItem id="menu-show-publishers"
onSelect = { () => this._onSpecialSearch( "/search/publishers" ) }
>Show publishers</MenuItem>
onSelect = { () => this.runSpecialSearch( "/search/publishers", null,
() => { this.setWindowTitle( "All publishers" ) }
) } > Show publishers </MenuItem>
<MenuItem id="menu-search-technique"
onSelect = { () => this._onSpecialSearch( "/search/tag/technique", {randomize:1} ) }
>Show technique</MenuItem>
onSelect = { () => this.runSpecialSearch( "/search/tag/technique", {randomize:1},
() => { this.setWindowTitle( "Technique" ) }
) } > Show technique </MenuItem>
<MenuItem id="menu-search-tips"
onSelect = { () => this._onSpecialSearch( "/search/tag/tips", {randomize:1} ) }
>Show tips</MenuItem>
onSelect = { () => this.runSpecialSearch( "/search/tag/tips", {randomize:1},
() => { this.setWindowTitle( "Tips" ) }
) } > Show tips </MenuItem>
<div className="divider" />
<MenuItem id="menu-new-publisher"
onSelect = { () => PublisherSearchResult.onNewPublisher( this._onNewPublisher.bind(this) ) }
@ -98,7 +106,7 @@ export default class App extends React.Component
content = ( <div>
<div id="header">
<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} />
</div>
{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 ; }

@ -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( <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] }}
onClick = { () => 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( <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 }}
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.
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( <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] }}
onClick = { () => gAppRef.searchForTag( tag ) }
title = "Search for this tag."
/> ) ;
}
} else {
if ( this.props.data.article_tags ) {
this.props.data.article_tags.map(
tag => tags.push( <div key={tag} className="tag"
onClick = { () => gAppRef.searchForTag( tag ) }
title = "Search for this tag."
> {tag} </div>
) ) ;
tag => tags.push( <Link key={tag} className="tag" title="Search for this tag."
to = { "/tag/" + encodeURIComponent(tag) }
> {tag} </Link> )
) ;
}
}
@ -123,11 +120,10 @@ export class ArticleSearchResult extends React.Component
<div className="header">
{menu}
{ pub_display_name &&
<span className="publication"
onClick = { () => gAppRef.searchForPublication( this.props.data.pub_id ) }
title = "Show this publication."
> {pub_display_name}
</span>
<Link className="publication" title="Show this publication."
to = { "/publication/" + this.props.data.pub_id }
dangerouslySetInnerHTML = {{ __html: pub_display_name }}
/>
}
<span className="title name" dangerouslySetInnerHTML={{ __html: display_title }} />
{ article_url &&

@ -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" ;

@ -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" ;
// --------------------------------------------------------------------

@ -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( <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] }}
onClick = { () => gAppRef.searchForTag( tag ) }
title = "Search for this tag."
/> ) ;
}
} else {
if ( this.props.data.pub_tags ) {
this.props.data.pub_tags.map(
tag => tags.push( <div key={tag} className="tag"
onClick = { () => gAppRef.searchForTag( tag ) }
title = "Search for this tag."
> {tag} </div>
) ) ;
tag => tags.push( <Link key={tag} className="tag" title="Search for this tag."
to = { "/tag/" + encodeURIComponent(tag) }
> {tag} </Link> )
) ;
}
}
@ -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( <span
articles.push( <Link title="Show this article."
to = { "/article/" + article.article_id }
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">
{menu}
{ publ &&
<span className="publisher"
onClick={ () => gAppRef.searchForPublisher( this.props.data.publ_id ) }
title = "Show this publisher."
<Link className="publisher" title="Show this publisher."
to = { "/publisher/" + this.props.data.publ_id }
> {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 ) }}
onClick = { () => gAppRef.searchForPublication( this.props.data.pub_id ) }
title = "Show this publication."
/>
{ pub_url &&
<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 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" ;

@ -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 => <span
pubs = pubs.map( p => <Link title="Show this publication."
to = { "/publication/" + p.pub_id }
dangerouslySetInnerHTML = {{ __html: PublicationSearchResult.makeDisplayName(p) }}
onClick = { () => gAppRef.searchForPublication( p.pub_id ) }
title = "Show this publication."
/> ) ;
// prepare the menu

@ -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" ;

@ -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 ; }

@ -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" ;
// --------------------------------------------------------------------

@ -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 ) ;

@ -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(
<App />,
ReactDOM.render(
<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" )
) ;

@ -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" ) ;

Loading…
Cancel
Save