Updated the UI.

master
Pacman Ghost 4 years ago
parent e7444a6b27
commit 2b97ec75d2
  1. 53
      asl_articles/tests/test_articles.py
  2. 7
      asl_articles/tests/test_authors.py
  3. 53
      asl_articles/tests/test_publications.py
  4. 47
      asl_articles/tests/test_publishers.py
  5. 7
      asl_articles/tests/test_scenarios.py
  6. 15
      asl_articles/tests/test_search.py
  7. 11
      asl_articles/tests/test_tags.py
  8. 44
      asl_articles/tests/utils.py
  9. 65
      web/package-lock.json
  10. 1
      web/package.json
  11. BIN
      web/public/app.ico
  12. BIN
      web/public/favicon.ico
  13. BIN
      web/public/images/app.png
  14. BIN
      web/public/images/ask.png
  15. BIN
      web/public/images/main-menu.png
  16. BIN
      web/public/images/menu.png
  17. BIN
      web/public/images/open-link.png
  18. BIN
      web/public/images/search.png
  19. 2
      web/public/index.html
  20. 2
      web/public/manifest.json
  21. 30
      web/src/App.css
  22. 75
      web/src/App.js
  23. 5
      web/src/ArticleSearchResult.css
  24. 37
      web/src/ArticleSearchResult.js
  25. 54
      web/src/ArticleSearchResult2.js
  26. 16
      web/src/ModalForm.css
  27. 16
      web/src/ModalForm.js
  28. 6
      web/src/PublicationSearchResult.css
  29. 33
      web/src/PublicationSearchResult.js
  30. 38
      web/src/PublicationSearchResult2.js
  31. 5
      web/src/PublisherSearchResult.css
  32. 27
      web/src/PublisherSearchResult.js
  33. 22
      web/src/PublisherSearchResult2.js
  34. 10
      web/src/SearchForm.css
  35. 12
      web/src/SearchForm.js
  36. 49
      web/src/SearchResults.css
  37. 7
      web/src/index.css
  38. 8
      web/src/utils.js

@ -7,10 +7,11 @@ import json
import base64
from asl_articles.search import SEARCH_ALL_ARTICLES
from asl_articles.tests.utils import init_tests, \
from asl_articles.tests.utils import init_tests, select_main_menu_option, select_sr_menu_option, \
do_search, get_search_results, find_search_result, get_search_result_names, check_search_result, \
wait_for, wait_for_elem, wait_for_not_elem, find_child, find_children, \
set_elem_text, set_toast_marker, check_toast, send_upload_data, check_ask_dialog, check_error_msg
set_elem_text, set_toast_marker, check_toast, send_upload_data, check_ask_dialog, check_error_msg, \
change_image
from asl_articles.tests.react_select import ReactSelect
# ---------------------------------------------------------------------
@ -44,7 +45,7 @@ def test_edit_article( webdriver, flask_app, dbconn ):
)
# enter something for the name
dlg = find_child( "#modal-form" ) # nb: the form is still on-screen
dlg = find_child( "#article-form" ) # nb: the form is still on-screen
set_elem_text( find_child( ".row.title input", dlg ), "Tin Cans Rock!" )
find_child( "button.ok", dlg ).click()
@ -98,7 +99,7 @@ def test_delete_article( webdriver, flask_app, dbconn ):
article_name = "Smoke Gets In Your Eyes"
results = do_search( SEARCH_ALL_ARTICLES )
sr = find_search_result( article_name, results )
find_child( ".delete", sr ).click()
select_sr_menu_option( sr, "delete" )
check_ask_dialog( ( "Delete this article?", article_name ), "cancel" )
# check that search results are unchanged on-screen
@ -111,7 +112,7 @@ def test_delete_article( webdriver, flask_app, dbconn ):
# delete the article
sr = find_search_result( article_name, results3 )
find_child( ".delete", sr ).click()
select_sr_menu_option( sr, "delete" )
set_toast_marker( "info" )
check_ask_dialog( ( "Delete this article?", article_name ), "ok" )
wait_for( 2, lambda: check_toast( "info", "The article was deleted." ) )
@ -146,8 +147,8 @@ def test_images( webdriver, flask_app, dbconn ): #pylint: disable=too-many-state
wait_for( 2, check_sr_image )
# check the image in the article's config
find_child( ".edit", article_sr ).click()
dlg = wait_for_elem( 2, "#modal-form" )
select_sr_menu_option( article_sr, "edit" )
dlg = wait_for_elem( 2, "#article-form" )
if expected:
# make sure there is an image
img = find_child( ".row.image img.image", dlg )
@ -198,8 +199,8 @@ def test_images( webdriver, flask_app, dbconn ): #pylint: disable=too-many-state
check_image( None )
# try to upload an image that's too large
find_child( ".edit", article_sr ).click()
dlg = wait_for_elem( 2, "#modal-form" )
select_sr_menu_option( article_sr, "edit" )
dlg = wait_for_elem( 2, "#article-form" )
data = base64.b64encode( 5000 * b" " )
data = "{}|{}".format( "too-big.png", data.decode("ascii") )
send_upload_data( data,
@ -218,9 +219,9 @@ def test_parent_publisher( webdriver, flask_app, dbconn ):
def check_result( sr, expected_parent ): #pylint: disable=too-many-return-statements
# check that the parent publication was updated in the UI
elem = find_child( ".title .publication", sr )
elem = find_child( ".header .publication", sr )
if expected_parent:
if elem.text != "[{}]".format( expected_parent[1] ):
if elem.text != "{}".format( expected_parent[1] ):
return None
else:
if elem is not None:
@ -241,9 +242,9 @@ def test_parent_publisher( webdriver, flask_app, dbconn ):
results = do_search( '"My Article"' )
assert len(results) == 1
sr = results[0]
elem = find_child( ".title .publication", sr )
elem = find_child( ".header .publication", sr )
if expected_parent:
if elem.text != "[{}]".format( expected_parent[1] ):
if elem.text != "{}".format( expected_parent[1] ):
return None
else:
if elem is not None:
@ -312,7 +313,7 @@ def test_clean_html( webdriver, flask_app, dbconn ):
sr = check_search_result( None, _check_sr, [
"title: bold xxx italic", "italicized subtitle", "bad stuff here:", [], None
] )
assert find_child( ".title span", sr ).get_attribute( "innerHTML" ) \
assert find_child( ".title", sr ).get_attribute( "innerHTML" ) \
== "title: <span> <b>bold</b> xxx <i>italic</i></span>"
assert find_child( ".subtitle", sr ).get_attribute( "innerHTML" ) \
== "<i>italicized subtitle</i>"
@ -335,8 +336,8 @@ def create_article( vals, toast_type="info" ):
set_toast_marker( toast_type )
# create the new article
find_child( "#menu .new-article" ).click()
dlg = wait_for_elem( 2, "#modal-form" )
select_main_menu_option( "new-article" )
dlg = wait_for_elem( 2, "#article-form" )
for key,val in vals.items():
if key in ["authors","scenarios","tags"]:
select = ReactSelect( find_child( ".row.{} .react-select".format(key), dlg ) )
@ -351,7 +352,7 @@ def create_article( vals, toast_type="info" ):
wait_for( 2,
lambda: check_toast( toast_type, "created OK", contains=True )
)
wait_for_not_elem( 2, "#modal-form" )
wait_for_not_elem( 2, "#article-form" )
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
@ -360,10 +361,10 @@ def edit_article( sr, vals, toast_type="info", expected_error=None ):
# initialize
if sr:
find_child( ".edit", sr ).click()
select_sr_menu_option( sr, "edit" )
else:
pass # nb: we assume that the dialog is already on-screen
dlg = wait_for_elem( 2, "#modal-form" )
dlg = wait_for_elem( 2, "#article-form" )
# update the specified article's details
for key,val in vals.items():
@ -371,9 +372,7 @@ def edit_article( sr, vals, toast_type="info", expected_error=None ):
if val:
data = base64.b64encode( open( val, "rb" ).read() )
data = "{}|{}".format( os.path.split(val)[1], data.decode("ascii") )
send_upload_data( data,
lambda: find_child( ".row.image img.image", dlg ).click()
)
change_image( find_child( ".row.image img.image", dlg ), data )
else:
find_child( ".row.image .remove-image", dlg ).click()
elif key == "publication":
@ -398,7 +397,7 @@ def edit_article( sr, vals, toast_type="info", expected_error=None ):
wait_for( 2,
lambda: check_toast( toast_type, expected, contains=True )
)
wait_for_not_elem( 2, "#modal-form" )
wait_for_not_elem( 2, "#article-form" )
# ---------------------------------------------------------------------
@ -427,12 +426,12 @@ def _check_sr( sr, expected ): #pylint: disable=too-many-return-statements
return False
# check the article's link
elem = find_child( ".title a", sr )
elem = find_child( "a.open-link", sr )
if expected[4]:
if not elem or elem.get_attribute( "href" ) != expected[4]:
assert elem
if elem.get_attribute( "href" ) != expected[4]:
return False
else:
if elem is not None:
return False
assert elem is None
return True

@ -3,7 +3,8 @@
import urllib.request
import json
from asl_articles.tests.utils import init_tests, wait_for, wait_for_elem, find_child, find_children, find_search_result
from asl_articles.tests.utils import init_tests, select_sr_menu_option, \
wait_for, wait_for_elem, find_child, find_children, find_search_result
from asl_articles.tests.react_select import ReactSelect
from asl_articles.tests.test_articles import create_article, edit_article
@ -78,8 +79,8 @@ def _check_authors( flask_app, all_authors, expected ):
)
# check the authors in the article's config
find_child( ".edit", sr ).click()
dlg = wait_for_elem( 2, "#modal-form" )
select_sr_menu_option( sr, "edit" )
dlg = wait_for_elem( 2, "#article-form" )
select = ReactSelect( find_child( ".row.authors .react-select", dlg ) )
assert select.get_multiselect_values() == expected_authors

@ -10,10 +10,11 @@ from selenium.webdriver.common.keys import Keys
from selenium.common.exceptions import StaleElementReferenceException
from asl_articles.search import SEARCH_ALL, SEARCH_ALL_PUBLICATIONS, SEARCH_ALL_ARTICLES
from asl_articles.tests.utils import init_tests, load_fixtures, \
from asl_articles.tests.utils import init_tests, load_fixtures, select_main_menu_option, select_sr_menu_option, \
do_search, get_search_results, get_search_result_names, check_search_result, \
wait_for, wait_for_elem, wait_for_not_elem, find_child, find_children, find_search_result, set_elem_text, \
set_toast_marker, check_toast, send_upload_data, check_ask_dialog, check_error_msg
set_toast_marker, check_toast, send_upload_data, check_ask_dialog, check_error_msg, \
change_image
from asl_articles.tests.react_select import ReactSelect
# ---------------------------------------------------------------------
@ -94,7 +95,7 @@ def test_delete_publication( webdriver, flask_app, dbconn ):
article_name = "ASL Journal (2)"
results = do_search( SEARCH_ALL_PUBLICATIONS )
sr = find_search_result( article_name, results )
find_child( ".delete", sr ).click()
select_sr_menu_option( sr, "delete" )
check_ask_dialog( ( "Delete this publication?", article_name ), "cancel" )
# check that search results are unchanged on-screen
@ -107,7 +108,7 @@ def test_delete_publication( webdriver, flask_app, dbconn ):
# delete the publication
sr = find_search_result( article_name, results3 )
find_child( ".delete", sr ).click()
select_sr_menu_option( sr, "delete" )
set_toast_marker( "info" )
check_ask_dialog( ( "Delete this publication?", article_name ), "ok" )
wait_for( 2, lambda: check_toast( "info", "The publication was deleted." ) )
@ -141,8 +142,8 @@ def test_images( webdriver, flask_app, dbconn ): #pylint: disable=too-many-state
wait_for( 2, lambda: check_sr_image( expected ) )
# check the image in the publisher's config
find_child( ".edit", pub_sr ).click()
dlg = wait_for_elem( 2, "#modal-form" )
select_sr_menu_option( pub_sr, "edit" )
dlg = wait_for_elem( 2, "#publication-form" )
if expected:
# make sure there is an image
img = find_child( ".row.image img.image", dlg )
@ -193,8 +194,8 @@ def test_images( webdriver, flask_app, dbconn ): #pylint: disable=too-many-state
check_image( None )
# try to upload an image that's too large
find_child( ".edit", pub_sr ).click()
dlg = wait_for_elem( 2, "#modal-form" )
select_sr_menu_option( pub_sr, "edit" )
dlg = wait_for_elem( 2, "#publication-form" )
data = base64.b64encode( 5000 * b" " )
data = "{}|{}".format( "too-big.png", data.decode("ascii") )
send_upload_data( data,
@ -213,9 +214,9 @@ def test_parent_publisher( webdriver, flask_app, dbconn ):
def check_result( sr, expected_parent ): #pylint: disable=too-many-return-statements
# check that the parent publisher was updated in the UI
elem = find_child( ".name .publisher", sr )
elem = find_child( ".header .publisher", sr )
if expected_parent:
if elem.text != "({})".format( expected_parent[1] ):
if elem.text != "{}".format( expected_parent[1] ):
return None
else:
if elem is not None:
@ -236,9 +237,9 @@ def test_parent_publisher( webdriver, flask_app, dbconn ):
results = do_search( '"MMP News"' )
assert len(results) == 1
sr = results[0]
elem = find_child( ".name .publisher", sr )
elem = find_child( ".header .publisher", sr )
if expected_parent:
if elem.text != "({})".format( expected_parent[1] ):
if elem.text != "{}".format( expected_parent[1] ):
return None
else:
if elem is not None:
@ -276,7 +277,7 @@ def test_cascading_deletes( webdriver, flask_app, dbconn ):
# delete the specified publication
sr = find_search_result( pub_name, results )
find_child( ".delete", sr ).click()
select_sr_menu_option( sr, "delete" )
check_ask_dialog( ( "Delete this publication?", pub_name, expected_warning ), "ok" )
def check_results():
@ -358,7 +359,7 @@ def test_clean_html( webdriver, flask_app, dbconn ):
sr = check_search_result( None, _check_sr, [
"name: bold xxx italic", "2", "bad stuff here:", [], None
] )
assert find_child( ".name span", sr ).get_attribute( "innerHTML" ) \
assert find_child( ".name", sr ).get_attribute( "innerHTML" ) \
== "name: <span> <b>bold</b> xxx <i>italic</i></span> (<i>2</i>)"
assert check_toast( "warning", "Some values had HTML removed.", contains=True )
@ -381,8 +382,8 @@ def create_publication( vals, toast_type="info" ):
set_toast_marker( toast_type )
# create the new publication
find_child( "#menu .new-publication" ).click()
dlg = wait_for_elem( 2, "#modal-form" )
select_main_menu_option( "new-publication" )
dlg = wait_for_elem( 2, "#publication-form" )
for key,val in vals.items():
if key == "name":
elem = find_child( ".row.name .react-select input", dlg )
@ -401,17 +402,17 @@ def create_publication( vals, toast_type="info" ):
wait_for( 2,
lambda: check_toast( toast_type, "created OK", contains=True )
)
wait_for_not_elem( 2, "#modal-form" )
wait_for_not_elem( 2, "#publication-form" )
def edit_publication( sr, vals, toast_type="info", expected_error=None ):
"""Edit a publication's details."""
# initialize
if sr:
find_child( ".edit", sr ).click()
select_sr_menu_option( sr, "edit" )
else:
pass # nb: we assume that the dialog is already on-screen
dlg = wait_for_elem( 2, "#modal-form" )
dlg = wait_for_elem( 2, "#publication-form" )
# update the specified publication's details
for key,val in vals.items():
@ -419,9 +420,7 @@ def edit_publication( sr, vals, toast_type="info", expected_error=None ):
if val:
data = base64.b64encode( open( val, "rb" ).read() )
data = "{}|{}".format( os.path.split(val)[1], data.decode("ascii") )
send_upload_data( data,
lambda: find_child( ".row.image img", dlg ).click()
)
change_image( find_child( ".row.image img.image", dlg ), data )
else:
find_child( ".row.image .remove-image", dlg ).click()
elif key == "name":
@ -450,7 +449,7 @@ def edit_publication( sr, vals, toast_type="info", expected_error=None ):
wait_for( 2,
lambda: check_toast( toast_type, expected, contains=True )
)
wait_for_not_elem( 2, "#modal-form" )
wait_for_not_elem( 2, "#publication-form" )
# ---------------------------------------------------------------------
@ -474,12 +473,12 @@ def _check_sr( sr, expected ):
return False
# check the publication's link
elem = find_child( ".name a", sr )
if elem:
elem = find_child( "a.open-link", sr )
if expected[4]:
assert elem
if elem.get_attribute( "href" ) != expected[4]:
return False
else:
if expected[4] is not None:
return False
assert elem is None
return True

@ -8,10 +8,11 @@ import base64
from selenium.common.exceptions import StaleElementReferenceException
from asl_articles.search import SEARCH_ALL, SEARCH_ALL_PUBLISHERS
from asl_articles.tests.utils import init_tests, load_fixtures, \
from asl_articles.tests.utils import init_tests, load_fixtures, select_main_menu_option, select_sr_menu_option, \
do_search, get_search_results, get_search_result_names, check_search_result, \
wait_for, wait_for_elem, wait_for_not_elem, find_child, find_search_result, set_elem_text, \
set_toast_marker, check_toast, send_upload_data, check_ask_dialog, check_error_msg
set_toast_marker, check_toast, send_upload_data, check_ask_dialog, check_error_msg, \
change_image
# ---------------------------------------------------------------------
@ -42,7 +43,7 @@ def test_edit_publisher( webdriver, flask_app, dbconn ):
)
# enter something for the name
dlg = find_child( "#modal-form" )
dlg = find_child( "#publisher-form" )
set_elem_text( find_child( ".row.name input", dlg ), "Updated Avalon Hill" )
find_child( "button.ok", dlg ).click()
@ -94,7 +95,7 @@ def test_delete_publisher( webdriver, flask_app, dbconn ):
article_name = "Le Franc Tireur"
results = do_search( SEARCH_ALL_PUBLISHERS )
sr = find_search_result( article_name, results )
find_child( ".delete", sr ).click()
select_sr_menu_option( sr, "delete" )
check_ask_dialog( ( "Delete this publisher?", article_name ), "cancel" )
# check that search results are unchanged on-screen
@ -107,7 +108,7 @@ def test_delete_publisher( webdriver, flask_app, dbconn ):
# delete the publisher
sr = find_search_result( article_name, results3 )
find_child( ".delete", sr ).click()
select_sr_menu_option( sr, "delete" )
set_toast_marker( "info" )
check_ask_dialog( ( "Delete this publisher?", article_name ), "ok" )
wait_for( 2,
@ -143,8 +144,8 @@ def test_images( webdriver, flask_app, dbconn ): #pylint: disable=too-many-state
wait_for( 2, check_sr_image )
# check the image in the publisher's config
find_child( ".edit", publ_sr ).click()
dlg = wait_for_elem( 2, "#modal-form" )
select_sr_menu_option( publ_sr, "edit" )
dlg = wait_for_elem( 2, "#publisher-form" )
if expected:
# make sure there is an image
img = find_child( ".row.image img.image", dlg )
@ -195,8 +196,8 @@ def test_images( webdriver, flask_app, dbconn ): #pylint: disable=too-many-state
check_image( None )
# try to upload an image that's too large
find_child( ".edit", publ_sr ).click()
dlg = wait_for_elem( 2, "#modal-form" )
select_sr_menu_option( publ_sr, "edit" )
dlg = wait_for_elem( 2, "#publisher-form" )
data = base64.b64encode( 5000 * b" " )
data = "{}|{}".format( "too-big.png", data.decode("ascii") )
send_upload_data( data,
@ -237,7 +238,7 @@ def test_cascading_deletes( webdriver, flask_app, dbconn ):
# delete the specified publisher
sr = find_search_result( publ_name, results )
find_child( ".delete", sr ).click()
select_sr_menu_option( sr, "delete" )
check_ask_dialog( ( "Delete this publisher?", publ_name, expected_warning ), "ok" )
# check that deleted associated publications/articles were removed from the UI
@ -334,7 +335,7 @@ def test_clean_html( webdriver, flask_app, dbconn ):
sr = check_search_result( None, _check_sr, [
"name: bold xxx italic", "bad stuff here:", None
] )
assert find_child( ".name span", sr ).get_attribute( "innerHTML" ) \
assert find_child( ".name", sr ).get_attribute( "innerHTML" ) \
== "name: <span> <b>bold</b> xxx <i>italic</i></span>"
assert check_toast( "warning", "Some values had HTML removed.", contains=True )
@ -357,8 +358,8 @@ def create_publisher( vals, toast_type="info" ):
set_toast_marker( toast_type )
# create the new publisher
find_child( "#menu .new-publisher" ).click()
dlg = wait_for_elem( 2, "#modal-form" )
select_main_menu_option( "new-publisher" )
dlg = wait_for_elem( 2, "#publisher-form" )
for key,val in vals.items():
sel = ".row.{} {}".format( key , "textarea" if key == "description" else "input" )
set_elem_text( find_child( sel, dlg ), val )
@ -369,7 +370,7 @@ def create_publisher( vals, toast_type="info" ):
wait_for( 2,
lambda: check_toast( toast_type, "created OK", contains=True )
)
wait_for_not_elem( 2, "#modal-form" )
wait_for_not_elem( 2, "#publisher-form" )
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
@ -378,10 +379,10 @@ def edit_publisher( sr, vals, toast_type="info", expected_error=None ):
# initialize
if sr:
find_child( ".edit", sr ).click()
select_sr_menu_option( sr, "edit" )
else:
pass # nb: we assume that the dialog is already on-screen
dlg = wait_for_elem( 2, "#modal-form" )
dlg = wait_for_elem( 2, "#publisher-form" )
# update the specified publisher's details
for key,val in vals.items():
@ -389,9 +390,7 @@ def edit_publisher( sr, vals, toast_type="info", expected_error=None ):
if val:
data = base64.b64encode( open( val, "rb" ).read() )
data = "{}|{}".format( os.path.split(val)[1], data.decode("ascii") )
send_upload_data( data,
lambda: find_child( ".row.image img", dlg ).click()
)
change_image( find_child( ".row.image img.image", dlg ), data )
else:
find_child( ".row.image .remove-image", dlg ).click()
else:
@ -410,7 +409,7 @@ def edit_publisher( sr, vals, toast_type="info", expected_error=None ):
wait_for( 2,
lambda: check_toast( toast_type, expected, contains=True )
)
wait_for_not_elem( 2, "#modal-form" )
wait_for_not_elem( 2, "#publisher-form" )
# ---------------------------------------------------------------------
@ -424,12 +423,12 @@ def _check_sr( sr, expected ):
return False
# check the publisher's link
elem = find_child( ".name a", sr )
if elem:
elem = find_child( "a.open-link", sr )
if expected[2]:
assert elem
if elem.get_attribute( "href" ) != expected[2]:
return False
else:
if expected[2] is not None:
return False
assert elem is None
return True

@ -3,7 +3,8 @@
import urllib.request
import json
from asl_articles.tests.utils import init_tests, find_child, find_children, wait_for, wait_for_elem, find_search_result
from asl_articles.tests.utils import init_tests, select_sr_menu_option, \
find_child, find_children, wait_for, wait_for_elem, find_search_result
from asl_articles.tests.react_select import ReactSelect
from asl_articles.tests.test_articles import create_article, edit_article
@ -89,8 +90,8 @@ def _check_scenarios( flask_app, all_scenarios, expected ):
)
# check the scenarios in the article's config
find_child( ".edit", sr ).click()
dlg = wait_for_elem( 2, "#modal-form" )
select_sr_menu_option( sr, "edit" )
dlg = wait_for_elem( 2, "#article-form" )
select = ReactSelect( find_child( ".row.scenarios .react-select", dlg ) )
assert select.get_multiselect_values() == expected_scenarios

@ -5,7 +5,8 @@ from asl_articles.search import SearchDbConn, _make_fts_query_string
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, wait_for_elem, find_child, find_children, check_ask_dialog, \
from asl_articles.tests.utils import init_tests, select_sr_menu_option, \
wait_for_elem, find_child, find_children, check_ask_dialog, \
do_search, get_search_result_names, find_search_result
# ---------------------------------------------------------------------
@ -33,7 +34,7 @@ def test_search_publishers( webdriver, flask_app, dbconn ):
# delete the publisher
sr = find_search_result( "Avalon Mountain" )
find_child( ".delete", sr ).click()
select_sr_menu_option( sr, "delete" )
check_ask_dialog( "Delete this publisher?", "ok" )
_do_test_searches( ["hill","original","mountain","first"], [] )
@ -62,7 +63,7 @@ def test_search_publications( webdriver, flask_app, dbconn ):
# delete the publication
sr = find_search_result( "ASL Magazine" )
find_child( ".delete", sr ).click()
select_sr_menu_option( sr, "delete" )
check_ask_dialog( "Delete this publication?", "ok" )
_do_test_searches( ["journal","good","magazine","bad"], [] )
@ -95,7 +96,7 @@ def test_search_articles( webdriver, flask_app, dbconn ):
# delete the article
sr = find_search_result( "Hit 'Em Hard" )
find_child( ".delete", sr ).click()
select_sr_menu_option( sr, "delete" )
check_ask_dialog( "Delete this article?", "ok" )
_do_test_searches( ["hard","hurt","best"], [] )
@ -200,14 +201,14 @@ def test_highlighting( webdriver, flask_app, dbconn ):
# test highlighting in publisher search results
results = _do_test_search( "view britain", ["View From The Trenches"] )
sr = results[0]
assert find_highlighted( find_child( ".name span", sr ) ) == [ "View" ]
assert find_highlighted( find_child( ".name", sr ) ) == [ "View" ]
assert find_highlighted( find_child( ".description", sr ) ) == [ "Britain" ]
def check_publication_highlights( query, expected, name, description, tags ):
results = _do_test_search( query, [expected] )
assert len(results) == 1
sr = results[0]
assert find_highlighted( find_child( ".name span", sr ) ) == name
assert find_highlighted( find_child( ".name", sr ) ) == name
assert find_highlighted( find_child( ".description", sr ) ) == description
assert find_highlighted( find_children( ".tag", sr ) ) == tags
@ -225,7 +226,7 @@ def test_highlighting( webdriver, flask_app, dbconn ):
results = _do_test_search( query, [expected] )
assert len(results) == 1
sr = results[0]
assert find_highlighted( find_child( ".title span", sr ) ) == title
assert find_highlighted( find_child( ".title", sr ) ) == title
assert find_highlighted( find_child( ".subtitle", sr ) ) == subtitle
assert find_highlighted( find_child( ".snippet", sr ) ) == snippet
assert find_highlighted( find_children( ".author", sr ) ) == authors

@ -3,7 +3,8 @@
import urllib.request
import json
from asl_articles.tests.utils import init_tests, find_search_result, get_search_results, get_search_result_names, \
from asl_articles.tests.utils import init_tests, select_sr_menu_option, \
find_search_result, get_search_results, get_search_result_names, \
wait_for, wait_for_elem, find_child, find_children
from asl_articles.tests.react_select import ReactSelect
@ -45,7 +46,7 @@ def test_tags( webdriver, flask_app, dbconn ):
} )
# remove some tags from the publication
edit_article( find_search_result( "publication 1" ), {
edit_publication( find_search_result( "publication 1" ), {
"tags": [ "-bbb" ]
} )
_check_tags( flask_app, {
@ -63,7 +64,7 @@ def test_tags( webdriver, flask_app, dbconn ):
} )
# add duplicate tags to the publication
edit_article( find_search_result( "publication 1" ), {
edit_publication( find_search_result( "publication 1" ), {
"tags": [ "+bbb", "+aaa", "+eee" ]
} )
_check_tags( flask_app, {
@ -124,8 +125,8 @@ def _check_tags( flask_app, expected ): #pylint: disable=too-many-locals
name = wait_for( 2, lambda sr=sr: check_tags( sr ) )
# check the tags in the publication/article
find_child( ".edit", sr ).click()
dlg = wait_for_elem( 2, "#modal-form" )
select_sr_menu_option( sr, "edit" )
dlg = wait_for_elem( 2, ".modal-form" )
select = ReactSelect( find_child( ".row.tags .react-select", dlg ) )
assert select.get_multiselect_values() == expected[ name ]

@ -14,7 +14,7 @@ from selenium.webdriver.remote.webelement import WebElement
from selenium.webdriver.common.action_chains import ActionChains
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.common.by import By
from selenium.common.exceptions import NoSuchElementException, StaleElementReferenceException
from selenium.common.exceptions import NoSuchElementException, StaleElementReferenceException, TimeoutException
from asl_articles import search
import asl_articles.models
@ -120,13 +120,13 @@ def get_search_result_names( results=None ):
"""Get the names from the search results."""
if not results:
results = get_search_results()
return [ find_child( ".name span", r ).text for r in results ]
return [ find_child( ".name", r ).text for r in results ]
def find_search_result( name, results=None ):
"""Find a search result."""
if not results:
results = get_search_results()
results = [ r for r in results if find_child( ".name span", r ).text == name ]
results = [ r for r in results if find_child( ".name", r ).text == name ]
assert len(results) == 1
return results[0]
@ -275,6 +275,44 @@ def send_upload_data( data, func ):
# ---------------------------------------------------------------------
def select_sr_menu_option( sr, menu_id ):
"""Select an option from a search result's menu."""
_do_select_menu_option( find_child("button.sr-menu",sr), "."+menu_id )
def select_main_menu_option( menu_id ):
"""Select an option from the main application menu."""
_do_select_menu_option( find_child("#menu-button--app"), "#menu-"+menu_id )
def _do_select_menu_option( menu, sel ):
"""Select an option from a dropdown menu."""
for _ in range(0,5):
# FUDGE! This is very weird, clicking on the menu button doesn't always register?!?
menu.click()
portal = None
try:
portal = wait_for_elem( 1, "reach-portal" )
except TimeoutException:
continue
assert portal
# FUDGE! Also very weird, the menu seems to occasionally close up by itself (especially when running headless).
try:
find_child( sel, portal ).click()
return
except StaleElementReferenceException:
continue
assert False, "Couldn't select menu option: {}".format( sel )
# ---------------------------------------------------------------------
def change_image( elem, image_data ):
"""Click on an image to change it."""
# NOTE: This is a bit tricky since we started overlaying the image with the "remove image" icon :-/
send_upload_data( image_data,
lambda: ActionChains( _webdriver ) \
.move_to_element_with_offset( elem, 1, 1 ) \
.click().perform()
)
def set_elem_text( elem, val ):
"""Set the text for an element."""
elem.clear()

@ -1331,6 +1331,58 @@
"resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-1.1.3.tgz",
"integrity": "sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw=="
},
"@reach/auto-id": {
"version": "0.7.2",
"resolved": "https://registry.npmjs.org/@reach/auto-id/-/auto-id-0.7.2.tgz",
"integrity": "sha512-9jBoMUnwB4ijh2w9wWQ3oW5oV0NMr2VW3tYDXI8xbd8BW25Bsqvjv8Y3Vp035HwJoLq3I7afA2i+jjBrsJtYAQ=="
},
"@reach/menu-button": {
"version": "0.7.2",
"resolved": "https://registry.npmjs.org/@reach/menu-button/-/menu-button-0.7.2.tgz",
"integrity": "sha512-/p9HFzEcUJY9s8NQy7dc9MtEEpQVLQ6QeCyVd8JCRSVXFiwieOgbIsaqSyzLwdDCcQqu0fI3THX8yCA4LZHEYg==",
"requires": {
"@reach/auto-id": "^0.7.2",
"@reach/popover": "^0.7.2",
"@reach/utils": "^0.7.2",
"prop-types": "^15.7.2",
"warning": "^4.0.3"
}
},
"@reach/observe-rect": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/@reach/observe-rect/-/observe-rect-1.0.5.tgz",
"integrity": "sha512-EQLHhquzwAh2TFtMJU0PXJRmhpOrAhzApgoI4Lwn72J7ygrMk3Ys3a3i8KDnV0TetbwAxn4XSHi4pE8sPWUYLg=="
},
"@reach/popover": {
"version": "0.7.2",
"resolved": "https://registry.npmjs.org/@reach/popover/-/popover-0.7.2.tgz",
"integrity": "sha512-bh7PQY0ZMlLy3S98UV+cHWfI9XrGthQuFFLMSldHaRTA0c3lTSr5jaygEdKHRQmlLfZhlvnhPLnzBR9Y/Sy/fw==",
"requires": {
"@reach/portal": "^0.7.2",
"@reach/rect": "^0.7.2",
"@reach/utils": "^0.7.2",
"tabbable": "^4.0.0"
}
},
"@reach/portal": {
"version": "0.7.2",
"resolved": "https://registry.npmjs.org/@reach/portal/-/portal-0.7.2.tgz",
"integrity": "sha512-5WRYEyNG+WBm+ohXOojfiUpwAL3S3zOrV8sKNmaN6dhnOf7ggmoO2qSI4qMNjIlG4RT86SdgfsUy3bgkZ8MaoQ=="
},
"@reach/rect": {
"version": "0.7.2",
"resolved": "https://registry.npmjs.org/@reach/rect/-/rect-0.7.2.tgz",
"integrity": "sha512-9Fay3XCEYseOVnwXg7fyGaO2c3lWuhhGlHIrUsrQB9GDyu34TswgDUrLDfMeGNVFBZBOG1QQVWPBhf6hTJktZA==",
"requires": {
"@reach/observe-rect": "^1.0.5",
"prop-types": "^15.7.2"
}
},
"@reach/utils": {
"version": "0.7.2",
"resolved": "https://registry.npmjs.org/@reach/utils/-/utils-0.7.2.tgz",
"integrity": "sha512-oGmA5eqM1f4NAyFSQvyFCcRRvgFTSiiPhw8Djn5PGfPY+sgmfyrUsJpo9FwJnl7P0nKb4liWq5jbWlZXUtWhLQ=="
},
"@svgr/babel-plugin-add-jsx-attribute": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-4.2.0.tgz",
@ -12526,6 +12578,11 @@
"resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz",
"integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw=="
},
"tabbable": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/tabbable/-/tabbable-4.0.0.tgz",
"integrity": "sha512-H1XoH1URcBOa/rZZWxLxHCtOdVUEev+9vo5YdYhC9tCY4wnybX+VQrCYuy9ubkg69fCBxCONJOSLGfw0DWMffQ=="
},
"table": {
"version": "5.4.6",
"resolved": "https://registry.npmjs.org/table/-/table-5.4.6.tgz",
@ -13143,6 +13200,14 @@
"makeerror": "1.0.x"
}
},
"warning": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz",
"integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==",
"requires": {
"loose-envify": "^1.0.0"
}
},
"watchpack": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-1.6.0.tgz",

@ -4,6 +4,7 @@
"private": true,
"dependencies": {
"@material-ui/core": "^4.7.0",
"@reach/menu-button": "^0.7.2",
"axios": "^0.19.0",
"lodash.clone": "^4.5.0",
"lodash.clonedeep": "^4.5.0",

Binary file not shown.

After

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

@ -3,7 +3,7 @@
<head>
<meta charset="utf-8" />
<link rel="icon" href="/favicon.ico" />
<link rel="icon" href="/app.ico" />
<meta name="viewport" content="minimum-scale=1, initial-scale=1, width=device-width, shrink-to-fit=no" />
<link rel="manifest" href="/manifest.json" />
</head>

@ -4,7 +4,7 @@
"icons": [
{
"src": "favicon.ico",
"src": "app.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
}

@ -1,14 +1,36 @@
img#loading { position: fixed ; top: 50% ; left: 50% ; margin-top: -16px ; margin-left: -16px ; }
#header { position: absolute ; top: 5px ; left: 5px ; right: 5px ; height: 65px ; }
#search-results { position: absolute ; top: 95px ; bottom: 0 ; left: 5px ; right: 5px ; overflow: auto ; }
#ask img.icon { width: 2em ; float: left ; margin: 0 1em 1em 0 ; }
#header { border: 1px solid #ccc ; background: #eee ; border-top-right-radius: 10px ; padding: 5px 5px 10px 5px ; }
#header .logo { float: left ; height: 70px ; }
#header .app-name { font-size: 25px ; }
#menu-button--app { position: absolute ; top: 10px ; right: 10px ;
width: 30px ; height: 30px ;
background: url("/images/main-menu.png") transparent no-repeat ; background-size: 100% ; border: none ;
cursor: pointer ;
}
[data-reach-menu-list] { padding: 5px ; }
[data-reach-menu-item] { padding: 5px ; }
[data-reach-menu-item][data-selected] { background: #90caf9 ; color: black ; }
.MuiDialogTitle-root { padding: 10px 16px 6px 16px !important ; }
.MuiDialogActions-root { background: #f0ffff ; }
.MuiDialogActions-root .MuiButton-text { padding: 6px 8px 2px 8px ; }
.MuiDialogActions-root button:hover { background: #40a0c0 ; color: white ; }
.MuiButton-label { text-transform: none ; }
#ask .MuiDialog-paper { width: 50% ; max-width: 40em !important ; }
#ask img.icon { width: 2em ; float: left ; margin: 0 1em 1em 0 ; }
.Toastify p { margin-top: 0.25em ; }
.Toastify__toast--info { background: #20b040 ; }
.Toastify__toast--warn { background: #f0c010 ; }
.Toastify__toast--error { background: #e04060 ; }
#menu { margin-bottom: 5px ; padding: 2px 5px ; background: #ddd ; border: 1px dotted #80d0f0 ; font-size: 75% ; }
#menu a { text-decoration: none ; color: #000 ; }
.react-select__control { border-radius: 0 !important ; }
.react-select__value-container { padding: 0 5px ; margin: -10px 0 ; }
img#loading { position: fixed ; top: 50% ; left: 50% ; margin-top: -16px ; margin-left: -16px ; }
.monospace { margin-top: 0.5em ; font-family: monospace ; font-style: italic ; font-size: 80% ; }

@ -1,5 +1,8 @@
import React from "react" ;
import ReactDOM from "react-dom" ;
import ReactDOMServer from "react-dom/server" ;
import { Menu, MenuList, MenuButton, MenuItem } from "@reach/menu-button" ;
import "@reach/menu-button/styles.css" ;
import { ToastContainer, toast } from "react-toastify" ;
import "react-toastify/dist/ReactToastify.min.css" ;
import SearchForm from "./SearchForm" ;
@ -36,6 +39,10 @@ export default class App extends React.Component
this._disableSearchResultHighlighting = this.isTestMode() && this.args.no_sr_hilite ;
this._fakeUploads = this.isTestMode() && this.args.fake_uploads ;
// initialize
this._searchFormRef = React.createRef() ;
this._modalFormRef = React.createRef() ;
// figure out the base URL of the Flask backend server
// NOTE: We allow the caller to do this since the test suite will usually spin up
// it's own Flask server, but talks to an existing React server, so we need some way
@ -51,26 +58,40 @@ export default class App extends React.Component
// we are still starting up
content = <div id="loading"> <img id="loading" src="/images/loading.gif" alt="Loading..." /></div> ;
} else {
// generate the main page
// generate the menu
const menu = ( <Menu id="app">
<MenuButton />
<MenuList>
<MenuItem id="menu-new-publisher"
onSelect = { () => PublisherSearchResult.onNewPublisher( this._onNewPublisher.bind(this) ) }
>New publisher</MenuItem>
<MenuItem id="menu-new-publication"
onSelect = { () => PublicationSearchResult.onNewPublication( this._onNewPublication.bind(this) ) }
>New publication</MenuItem>
<MenuItem id="menu-new-article"
onSelect = { () => ArticleSearchResult.onNewArticle( this._onNewArticle.bind(this) ) }
>New article</MenuItem>
</MenuList>
</Menu> ) ;
// generate the main content
content = ( <div>
<div id="menu">
[<a href="/" className="new-publisher"
onClick={ (e) => { e.preventDefault() ; PublisherSearchResult.onNewPublisher( this._onNewPublisher.bind(this) ) ; } }
>New publisher</a>]
[<a href="/" className="new-publication"
onClick={ (e) => { e.preventDefault() ; PublicationSearchResult.onNewPublication( this._onNewPublication.bind(this) ) ; } }
>New publication</a>]
[<a href="/" className="new-article"
onClick={ (e) => { e.preventDefault() ; ArticleSearchResult.onNewArticle( this._onNewArticle.bind(this) ) ; } }
>New article</a>]
<div id="header">
<img className="logo" src="/images/app.png" alt="Logo" />
<div className="app-name"> ASL Articles </div>
<SearchForm onSearch={this.onSearch.bind(this)} ref={this._searchFormRef} />
</div>
<SearchForm onSearch={this.onSearch.bind(this)} ref="searchForm" />
{menu}
<SearchResults seqNo={this.state.searchSeqNo} searchResults={this.state.searchResults} />
</div> ) ;
}
return ( <div> {content}
{ this.state.modalForm !== null &&
<ModalForm show={true} title={this.state.modalForm.title} content={this.state.modalForm.content} buttons={this.state.modalForm.buttons} />
<ModalForm show={true} formId={this.state.modalForm.formId}
title = {this.state.modalForm.title} titleColor = {this.state.modalForm.titleColor}
content = {this.state.modalForm.content}
buttons = {this.state.modalForm.buttons}
ref = {this._modalFormRef}
/>
}
{ this.state.askDialog !== null &&
<AskDialog show={true} content={this.state.askDialog.content} buttons={this.state.askDialog.buttons} />
@ -104,6 +125,13 @@ export default class App extends React.Component
this.showErrorToast( <div> Couldn't load the {type}: <div className="monospace"> {err.toString()} </div> </div> ) ;
} ) ;
} ) ;
// install our key handler
window.addEventListener( "keydown", this.onKeyDown.bind( this ) ) ;
}
componentWillUnmount() {
// clean up
window.removeEventListener( "keydown", this.onKeyDown ) ;
}
onSearch( query ) {
@ -142,7 +170,7 @@ export default class App extends React.Component
this.setState( { searchResults: newSearchResults } ) ;
}
showModalForm( title, content, buttons ) {
showModalForm( formId, title, titleColor, content, buttons ) {
// prepare the buttons
let buttons2 = [] ;
for ( let b in buttons ) {
@ -157,7 +185,7 @@ export default class App extends React.Component
}
// show the dialog
this.setState( {
modalForm: { title: title, content: content, buttons: buttons2 },
modalForm: { formId: formId, title: title, titleColor: titleColor, content: content, buttons: buttons2 },
} ) ;
}
@ -223,6 +251,21 @@ export default class App extends React.Component
} } ) ;
}
onKeyDown( evt ) {
// check if a modal dialog is open and Ctrl-Enter was pressed
if ( this._modalFormRef && evt.keyCode === 13 && evt.ctrlKey ) {
let dlg = ReactDOM.findDOMNode( this._modalFormRef.current ) ;
if ( dlg ) {
// yup - accept the dialog
let btn = dlg.querySelector( ".MuiButton-root.ok" ) ;
if ( btn )
btn.click() ;
else
console.log( "ERROR: Can't find default button." ) ;
}
}
}
logInternalError( msg, detail ) {
// log an internal error
this.showErrorToast( <div>
@ -278,7 +321,7 @@ export default class App extends React.Component
this.setState( { startupTasks: this.state.startupTasks } ) ;
}
focusQueryString() { this.refs.searchForm.focusQueryString() ; }
focusQueryString() { this._searchFormRef.current.focusQueryString() ; }
isTestMode() { return process.env.REACT_APP_TEST_MODE ; }
isFakeUploads() { return this._fakeUploads ; }

@ -0,0 +1,5 @@
#article-form .row label.top { width: 6.5em ; }
#article-form .row label { width: 5.75em ; }
#article-form .row.snippet { flex-direction: column ; margin-top: -0.5em ; }
#article-form .row.snippet textarea { min-height: 6em ; }

@ -1,8 +1,10 @@
import React from "react" ;
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 { makeScenarioDisplayName, applyUpdatedVals, removeSpecialFields, makeOptionalLink, makeCommaList } from "./utils.js" ;
import { makeScenarioDisplayName, applyUpdatedVals, removeSpecialFields, makeCommaList } from "./utils.js" ;
const axios = require( "axios" ) ;
@ -62,21 +64,36 @@ export class ArticleSearchResult extends React.Component
// NOTE: The "title" field is also given the CSS class "name" so that the normal CSS will apply to it.
// Some tests also look for a generic ".name" class name when checking search results.
const pub_display_name = pub ? PublicationSearchResult.makeDisplayName( pub ) : null ;
const menu = ( <Menu>
<MenuButton className="sr-menu" />
<MenuList>
<MenuItem className="edit"
onSelect = { this.onEditArticle.bind( this ) }
>Edit</MenuItem>
<MenuItem className="delete"
onSelect = { this.onDeleteArticle.bind( this ) }
>Delete</MenuItem>
</MenuList>
</Menu> ) ;
return ( <div className="search-result article"
ref = { r => gAppRef.setTestAttribute( r, "article_id", this.props.data.article_id ) }
>
<div className="title name">
{ image_url && <img src={image_url} className="image" alt="Article." /> }
{ makeOptionalLink( display_title, this.props.data.article_url ) }
{ pub_display_name && <span className="publication"> [{pub_display_name}] </span> }
<img src="/images/edit.png" className="edit" onClick={this.onEditArticle.bind(this)} alt="Edit this article." />
<img src="/images/delete.png" className="delete" onClick={this.onDeleteArticle.bind(this)} alt="Delete this article." />
<div className="header">
{menu}
{ pub_display_name && <span className="publication"> {pub_display_name} </span> }
<span className="title name" dangerouslySetInnerHTML={{ __html: display_title }} />
{ this.props.data.article_url && <a href={this.props.data.article_url} className="open-link" target="_blank" rel="noopener noreferrer"><img src="/images/open-link.png" alt="Open this article." /></a> }
{ display_subtitle && <div className="subtitle" dangerouslySetInnerHTML={{ __html: display_subtitle }} /> }
</div>
<div className="content">
{ image_url && <img src={image_url} className="image" alt="Article." /> }
<div className="snippet" dangerouslySetInnerHTML={{__html: display_snippet}} />
</div>
<div className="footer">
{ authors.length > 0 && <div className="authors"> By {authors} </div> }
{ scenarios.length > 0 && <div className="scenarios"> Scenarios: {scenarios} </div> }
{ tags.length > 0 && <div className="tags"> Tags: {tags} </div> }
</div>
<div className="snippet" dangerouslySetInnerHTML={{__html: display_snippet}} />
{ scenarios.length > 0 && <div className="scenarios"> Scenarios: {scenarios} </div> }
{ tags.length > 0 && <div className="tags"> Tags: {tags} </div> }
</div> ) ;
}

@ -90,46 +90,48 @@ export class ArticleSearchResult2
// prepare the form content
/* eslint-disable jsx-a11y/img-redundant-alt */
const content = <div>
<div className="row image">
<img src={imageUrl} className="image" onError={onMissingImage} onClick={() => onUploadImage(null)} ref={r => imageRef=r} alt="Click to upload an image for this article." />
<img src="/images/delete.png" className="remove-image" onClick={onRemoveImage} ref={r => removeImageRef=r} alt="Remove the article's image." />
<input type="file" accept="image/*" onChange={onUploadImage} style={{display:"none"}} ref={r => uploadImageRef=r} />
<div className="image-container">
<div className="row image">
<img src={imageUrl} className="image" onError={onMissingImage} onClick={() => onUploadImage(null)} ref={r => imageRef=r} alt="Click to upload an image for this article." />
<img src="/images/delete.png" className="remove-image" onClick={onRemoveImage} ref={r => removeImageRef=r} alt="Remove the article's image." />
<input type="file" accept="image/*" onChange={onUploadImage} style={{display:"none"}} ref={r => uploadImageRef=r} />
</div>
</div>
<div className="row title"> <label> Title: </label>
<input type="text" defaultValue={vals.article_title} ref={(r) => refs.article_title=r} />
<div className="row title"> <label className="top"> Title: </label>
<input type="text" defaultValue={vals.article_title} autoFocus ref={r => refs.article_title=r} />
</div>
<div className="row subtitle"> <label> Subtitle: </label>
<input type="text" defaultValue={vals.article_subtitle} ref={(r) => refs.article_subtitle=r} />
<div className="row subtitle"> <label className="top"> Subtitle: </label>
<input type="text" defaultValue={vals.article_subtitle} ref={r => refs.article_subtitle=r} />
</div>
<div className="row authors"> <label> Authors: </label>
<CreatableSelect className="react-select" classNamePrefix="react-select" options={allAuthors} isMulti
defaultValue = {currAuthors}
ref = { (r) => refs.article_authors=r }
/>
</div>
<div className="row publication"> <label> Publication: </label>
<div className="row publication"> <label className="select top"> Publication: </label>
<Select className="react-select" classNamePrefix="react-select" options={publications} isSearchable={true}
defaultValue = {currPub}
ref = { (r) => refs.pub_id=r }
ref = { r => refs.pub_id=r }
/>
</div>
<div className="row tags"> <label> Tags: </label>
<CreatableSelect className="react-select" classNamePrefix="react-select" options={tags[1]} isMulti
defaultValue = {tags[0]}
ref = { (r) => refs.article_tags=r }
<div className="row snippet"> <label> Snippet: </label>
<textarea defaultValue={vals.article_snippet} ref={r => refs.article_snippet=r} />
</div>
<div className="row authors"> <label className="select"> Authors: </label>
<CreatableSelect className="react-select" classNamePrefix="react-select" options={allAuthors} isMulti
defaultValue = {currAuthors}
ref = { r => refs.article_authors=r }
/>
</div>
<div className="row scenarios"> <label> Scenarios: </label>
<div className="row scenarios"> <label className="select"> Scenarios: </label>
<CreatableSelect className="react-select" classNamePrefix="react-select" options={allScenarios} isMulti
defaultValue = {currScenarios}
ref = { (r) => refs.article_scenarios=r }
ref = { r => refs.article_scenarios=r }
/>
</div>
<div className="row snippet"> <label> Snippet: </label>
<textarea defaultValue={vals.article_snippet} ref={(r) => refs.article_snippet=r} />
<div className="row tags"> <label className="select"> Tags: </label>
<CreatableSelect className="react-select" classNamePrefix="react-select" options={tags[1]} isMulti
defaultValue = {tags[0]}
ref = { r => refs.article_tags=r }
/>
</div>
<div className="row url"> <label> Web: </label>
<input type="text" defaultValue={vals.article_url} ref={(r) => refs.article_url=r} />
<input type="text" defaultValue={vals.article_url} ref={r => refs.article_url=r} />
</div>
</div> ;
@ -181,7 +183,7 @@ export class ArticleSearchResult2
// show the form
const isNew = Object.keys( vals ).length === 0 ;
gAppRef.showModalForm( isNew?"New article":"Edit article", content, buttons ) ;
gAppRef.showModalForm( "article-form", isNew?"New article":"Edit article", "#d3edfc", content, buttons ) ;
}
}

@ -1,2 +1,14 @@
.row.image img.image { height: 2em ; cursor: pointer ; }
.row.image img.remove-image { height: 1em ; margin-left: 0.25em ; cursor: pointer ; }
.modal-form .MuiDialog-paper { width: 80% ; max-width: 50em !important ; height: 80% ; }
.modal-form .MuiPaper-rounded { border-top-right-radius: 15px ; }
.modal-form .row { display: flex ; margin-bottom: 0.25em ; }
.modal-form .row label { margin-top: 3px ; }
.modal-form .row label.select { margin-top: 10px ; }
.modal-form .row input , .row textarea , .row .react-select { flex-grow: 1 ; }
.modal-form .row.image { display: block ; }
.modal-form .row.image img.image { margin-right: 1em ; max-height: 5em ; cursor: pointer ; }
.modal-form .row.image img.remove-image { height: 1em ; margin-left: 0.25em ; cursor: pointer ; }
.modal-form .image-container { position: relative ; display: inline-block ; float: left ; }
.modal-form .image-container img.remove-image { width: 12px ; height: 12px !important ; position: absolute ; top: -5px ; right: 11px ; padding: 2px ; border:1px solid #c00 ; background: #eee ;}

@ -1,10 +1,8 @@
import React from "react" ;
import Draggable from "react-draggable" ;
import Dialog from "@material-ui/core/Dialog" ;
import DialogTitle from "@material-ui/core/DialogTitle" ;
import DialogContent from "@material-ui/core/DialogContent" ;
import DialogActions from "@material-ui/core/DialogActions" ;
import Paper from "@material-ui/core/Paper" ;
import Button from "@material-ui/core/Button" ;
import "./ModalForm.css" ;
import { slugify } from "./utils" ;
@ -23,8 +21,8 @@ export default class ModalForm extends React.Component
> {btn} </Button>
) ;
}
return ( <Dialog id="modal-form" PaperComponent={PaperComponent} open={true} onClose={this.onClose.bind(this)} disableBackdropClick>
<DialogTitle id="draggable-dialog-title" style={{cursor: "move"}}> {this.props.title} </DialogTitle>
return ( <Dialog id={this.props.formId} className="modal-form" open={true} onClose={this.onClose.bind(this)} disableBackdropClick>
<DialogTitle style={{background:this.props.titleColor}}> {this.props.title} </DialogTitle>
<DialogContent dividers> {this.props.content} </DialogContent>
<DialogActions> {buttons} </DialogActions>
</Dialog> ) ;
@ -37,13 +35,3 @@ export default class ModalForm extends React.Component
}
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
function PaperComponent( props ) {
return (
<Draggable cancel={"[class*='MuiDialogContent-root']"}>
<Paper {...props} />
</Draggable>
) ;
}

@ -0,0 +1,6 @@
#publication-form .row label.top { width: 5.75em ; }
#publication-form .row label { width: 3.25em ; }
#publication-form .row.edition input { flex-grow: 0 ; width: 3em ; }
#publication-form .row.description { flex-direction: column ; margin-top: -0.5em ; }
#publication-form .row.description textarea { min-height: 6em ; }

@ -1,7 +1,9 @@
import React from "react" ;
import { Menu, MenuList, MenuButton, MenuItem } from "@reach/menu-button" ;
import "./PublicationSearchResult.css" ;
import { PublicationSearchResult2 } from "./PublicationSearchResult2.js" ;
import { gAppRef } from "./index.js" ;
import { makeOptionalLink, pluralString, applyUpdatedVals, removeSpecialFields } from "./utils.js" ;
import { pluralString, applyUpdatedVals, removeSpecialFields } from "./utils.js" ;
const axios = require( "axios" ) ;
@ -30,18 +32,33 @@ export class PublicationSearchResult extends React.Component
) ;
}
}
const menu = ( <Menu>
<MenuButton className="sr-menu" />
<MenuList>
<MenuItem className="edit"
onSelect = { this.onEditPublication.bind( this ) }
>Edit</MenuItem>
<MenuItem className="delete"
onSelect = { this.onDeletePublication.bind( this ) }
>Delete</MenuItem>
</MenuList>
</Menu> ) ;
return ( <div className="search-result publication"
ref = { r => gAppRef.setTestAttribute( r, "pub_id", this.props.data.pub_id ) }
>
<div className="name">
<div className="header">
{menu}
{ publ && <span className="publisher"> {publ.publ_name} </span> }
<span className="name" dangerouslySetInnerHTML={{ __html: this._makeDisplayName(true) }} />
{ this.props.data.pub_url && <a href={this.props.data.pub_url} className="open-link" target="_blank" rel="noopener noreferrer"><img src="/images/open-link.png" alt="Go to this publication." /></a> }
</div>
<div className="content">
{ image_url && <img src={image_url} className="image" alt="Publication." /> }
{ makeOptionalLink( this._makeDisplayName(true), this.props.data.pub_url ) }
{ publ && <span className="publisher"> ({publ.publ_name}) </span> }
<img src="/images/edit.png" className="edit" onClick={this.onEditPublication.bind(this)} alt="Edit this publication." />
<img src="/images/delete.png" className="delete" onClick={this.onDeletePublication.bind(this)} alt="Delete this publication." />
<div className="description" dangerouslySetInnerHTML={{__html: display_description}} />
</div>
<div className="footer">
{ tags.length > 0 && <div className="tags"> <label>Tags:</label> {tags} </div> }
</div>
<div className="description" dangerouslySetInnerHTML={{__html: display_description}} />
{ tags.length > 0 && <div className="tags"> <label>Tags:</label> {tags} </div> }
</div> ) ;
}

@ -80,37 +80,39 @@ export class PublicationSearchResult2
// prepare the form content
/* eslint-disable jsx-a11y/img-redundant-alt */
const content = <div>
<div className="row image">
<img src={imageUrl} className="image" onError={onMissingImage} onClick={() => onUploadImage(null)} ref={r => imageRef=r} alt="Click to upload an image for this publication." />
<img src="/images/delete.png" className="remove-image" onClick={onRemoveImage} ref={r => removeImageRef=r} alt="Remove the publication's image." />
<input type="file" accept="image/*" onChange={onUploadImage} style={{display:"none"}} ref={r => uploadImageRef=r} />
<div className="image-container">
<div className="row image">
<img src={imageUrl} className="image" onError={onMissingImage} onClick={() => onUploadImage(null)} ref={r => imageRef=r} alt="Click to upload an image for this publication." />
<img src="/images/delete.png" className="remove-image" onClick={onRemoveImage} ref={r => removeImageRef=r} alt="Remove the publication's image." />
<input type="file" accept="image/*" onChange={onUploadImage} style={{display:"none"}} ref={r => uploadImageRef=r} />
</div>
</div>
<div className="row name"> <label> Name: </label>
<CreatableSelect className="react-select" classNamePrefix="react-select" options={publications2}
<div className="row name"> <label className="select top"> Name: </label>
<CreatableSelect className="react-select" classNamePrefix="react-select" options={publications2} autoFocus
defaultValue = {currPub}
ref = { (r) => refs.pub_name=r }
ref = { r => refs.pub_name=r }
/>
</div>
<div className="row edition"> <label> Edition: </label>
<input type="text" defaultValue={vals.pub_edition} ref={(r) => refs.pub_edition=r} />
<div className="row edition"> <label className="top"> Edition: </label>
<input type="text" defaultValue={vals.pub_edition} ref={r => refs.pub_edition=r} />
</div>
<div className="row publisher"> <label> Publisher: </label>
<div className="row publisher"> <label className="select top"> Publisher: </label>
<Select className="react-select" classNamePrefix="react-select" options={publishers} isSearchable={true}
defaultValue = { publishers[ currPubl ] }
ref = { (r) => refs.publ_id=r }
ref = { r => refs.publ_id=r }
/>
</div>
<div className="row tags"> <label> Tags: </label>
<div className="row description"> <label> Description: </label>
<textarea defaultValue={vals.pub_description} ref={r => refs.pub_description=r} />
</div>
<div className="row tags"> <label className="select"> Tags: </label>
<CreatableSelect className="react-select" classNamePrefix="react-select" options={tags[1]} isMulti
defaultValue = {tags[0]}
ref = { (r) => refs.pub_tags=r }
ref = { r => refs.pub_tags=r }
/>
</div>
<div className="row description"> <label> Description: </label>
<textarea defaultValue={vals.pub_description} ref={(r) => refs.pub_description=r} />
</div>
<div className="row url"> <label> Web: </label>
<input type="text" defaultValue={vals.pub_url} ref={(r) => refs.pub_url=r} />
<input type="text" defaultValue={vals.pub_url} ref={r => refs.pub_url=r} />
</div>
</div> ;
@ -147,7 +149,7 @@ export class PublicationSearchResult2
// show the form
const isNew = Object.keys( vals ).length === 0 ;
gAppRef.showModalForm( isNew?"New publication":"Edit publication", content, buttons ) ;
gAppRef.showModalForm( "publication-form", isNew?"New publication":"Edit publication", "#e5f700", content, buttons ) ;
}
}

@ -0,0 +1,5 @@
#publisher-form .row label.top { width: 4em ; }
#publisher-form .row label { width: 3em ; }
#publisher-form .row.description { flex-direction: column ; margin-top: -0.25em ; }
#publisher-form .row.description textarea { min-height: 6em ; }

@ -1,7 +1,9 @@
import React from "react" ;
import { Menu, MenuList, MenuButton, MenuItem } from "@reach/menu-button" ;
import { PublisherSearchResult2 } from "./PublisherSearchResult2.js"
import "./PublisherSearchResult.css" ;
import { gAppRef } from "./index.js" ;
import { makeOptionalLink, pluralString, applyUpdatedVals, removeSpecialFields } from "./utils.js" ;
import { pluralString, applyUpdatedVals, removeSpecialFields } from "./utils.js" ;
const axios = require( "axios" ) ;
@ -14,16 +16,29 @@ export class PublisherSearchResult extends React.Component
const display_name = this.props.data[ "publ_name!" ] || this.props.data.publ_name ;
const display_description = this.props.data[ "publ_description!" ] || this.props.data.publ_description ;
const image_url = gAppRef.makeFlaskImageUrl( "publisher", this.props.data.publ_image_id, true ) ;
const menu = ( <Menu>
<MenuButton className="sr-menu" />
<MenuList>
<MenuItem className="edit"
onSelect = { this.onEditPublisher.bind( this ) }
>Edit</MenuItem>
<MenuItem className="delete"
onSelect = { this.onDeletePublisher.bind( this ) }
>Delete</MenuItem>
</MenuList>
</Menu> ) ;
return ( <div className="search-result publisher"
ref = { r => gAppRef.setTestAttribute( r, "publ_id", this.props.data.publ_id ) }
>
<div className="name">
<div className="header">
{menu}
<span className="name" dangerouslySetInnerHTML={{ __html: display_name }} />
{ this.props.data.publ_url && <a href={this.props.data.publ_url} className="open-link" target="_blank" rel="noopener noreferrer"><img src="/images/open-link.png" alt="Go to this publisher." /></a> }
</div>
<div className="content">
{ image_url && <img src={image_url} className="image" alt="Publisher." /> }
{ makeOptionalLink( display_name, this.props.data.publ_url ) }
<img src="/images/edit.png" className="edit" onClick={this.onEditPublisher.bind(this)} alt="Edit this publisher." />
<img src="/images/delete.png" className="delete" onClick={this.onDeletePublisher.bind(this)} alt="Delete this publisher." />
<div className="description" dangerouslySetInnerHTML={{__html: display_description}} />
</div>
<div className="description" dangerouslySetInnerHTML={{__html: display_description}} />
</div> ) ;
}

@ -41,19 +41,21 @@ export class PublisherSearchResult2
// prepare the form content
/* eslint-disable jsx-a11y/img-redundant-alt */
const content = <div>
<div className="row image">
<img src={imageUrl} className="image" onError={onMissingImage} onClick={() => onUploadImage(null)} ref={r => imageRef=r} alt="Click to upload an image for this publisher." />
<img src="/images/delete.png" className="remove-image" onClick={onRemoveImage} ref={r => removeImageRef=r} alt="Remove the publisher's image." />
<input type="file" accept="image/*" onChange={onUploadImage} style={{display:"none"}} ref={r => uploadImageRef=r} />
<div className="image-container">
<div className="row image">
<img src={imageUrl} className="image" onError={onMissingImage} onClick={() => onUploadImage(null)} ref={r => imageRef=r} alt="Click to upload an image for this publisher." />
<img src="/images/delete.png" className="remove-image" onClick={onRemoveImage} ref={r => removeImageRef=r} alt="Remove the publisher's image." />
<input type="file" accept="image/*" onChange={onUploadImage} style={{display:"none"}} ref={r => uploadImageRef=r} />
</div>
</div>
<div className="row name"> <label> Name: </label>
<input type="text" defaultValue={vals.publ_name} ref={(r) => refs.publ_name=r} />
<div className="row name"> <label className="top"> Name: </label>
<input type="text" defaultValue={vals.publ_name} autoFocus ref={r => refs.publ_name=r} />
</div>
<div className="row description"> <label> Description: </label>
<textarea defaultValue={vals.publ_description} ref={(r) => refs.publ_description=r} />
<div className="row description"> <label className="top"> Description: </label>
<textarea defaultValue={vals.publ_description} ref={r => refs.publ_description=r} />
</div>
<div className="row url"> <label> Web: </label>
<input type="text" defaultValue={vals.publ_url} ref={(r) => refs.publ_url=r} />
<input type="text" defaultValue={vals.publ_url} ref={r => refs.publ_url=r} />
</div>
</div> ;
@ -80,7 +82,7 @@ export class PublisherSearchResult2
// show the form
const isNew = Object.keys( vals ).length === 0 ;
gAppRef.showModalForm( isNew?"New publisher":"Edit publisher", content, buttons ) ;
gAppRef.showModalForm( "publisher-form", isNew?"New publisher":"Edit publisher", "#eabe51", content, buttons ) ;
}
}

@ -1,4 +1,6 @@
#search-form { padding: 0.25em ; }
#search-form { border: 1px dotted #666 ; }
#search-form button[type="submit"] { margin-left: 5px ; }
#search-form { display: flex ; flex-direction: row ; align-items: center ; }
#search-form .caption { line-height: 22px ; }
#search-form .query { flex: 1 ; min-width: 5em ; max-width: 30em ; margin: 0 0.25em 0 0.5em ; }
#search-form button[type="submit"] { width: 28px ; height: 28px ;
background: url("/images/search.png") transparent no-repeat 2px 2px ; background-size: 20px ;
}

@ -7,23 +7,27 @@ export default class SearchForm extends React.Component
{
constructor( props ) {
// initialize
super( props ) ;
this.state = {
queryString: "",
} ;
// initialize
this._queryStringRef = React.createRef() ;
}
render() {
return (
<form id="search-form" onSubmit={this.onSearch.bind(this)}>
<label className="caption"> Search for: </label>
<label className="caption"> Search&nbsp;for: </label>
<input type="text" className="query"
value = {this.state.queryString}
onChange = { e => this.setState( { queryString: e.target.value } ) }
ref = "queryString"
ref = {this._queryStringRef}
autoFocus
/>
<button type="submit"> Go </button>
<button type="submit" alt="Search the database." />
</form>
) ;
}
@ -33,6 +37,6 @@ export default class SearchForm extends React.Component
this.props.onSearch( this.state.queryString ) ;
}
focusQueryString() { this.refs.queryString.focus() ; }
focusQueryString() { this._queryStringRef.current.focus() ; }
}

@ -1,29 +1,32 @@
#search-results { margin-top: 0.5em ; }
#search-results { padding: 0 0.25em ; font-size: 90% ; }
#search-results .no-results { font-style: italic ; }
.search-result {
margin: 0.25em 0 ; padding: 0.1em 0.2em ;
font-size: 90% ;
.search-result { margin-bottom: 0.5em ; clear: both ; }
.search-result button.sr-menu {
width: 1em ; height: 1em ; float: right ; margin-right: -3px ;
background: url("/images/menu.png") transparent no-repeat ; background-size: 100% ; border: none ;
cursor: pointer ;
}
.search-result .header { padding: 2px 5px ; border-top-right-radius: 5px ; }
.search-result .header a { text-decoration: none ; }
.search-result .header a.open-link { margin-left: 0.5em ; }
.search-result .header a.open-link img { height: 1.2em ; margin-bottom: -0.2em ; }
.search-result.publisher .header { border: 1px solid #c0c0c0 ; background: #eabe51 ; }
.search-result.publication .header { border: 1px solid #c0c0c0 ; background: #e5f700 ; }
.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.publication .header .publisher , .search-result.article .header .publication {
float: right ; margin-right: 0.5em ; font-size: 80% ; font-style: italic ; color: #444 ;
}
.search-result .name { padding: 2px 5px ; }
.search-result .name a { font-weight: bold ; text-decoration: none ; }
.search-result .name .publisher { font-size: 80% ; font-style: italic ; }
.search-result .name .publication { font-size: 80% ; font-style: italic ; }
.search-result .name img.edit { margin-left: 0.5em ; height: 0.8em ; cursor: pointer ; }
.search-result .name img.delete { float: right ; margin: 0.2em 0 0 0.5em ; height: 0.8em ; cursor: pointer ; }
.search-result .image { float: left ; margin-right: 0.25em ; height: 1em ; }
.search-result .authors { font-size: 80% ; font-style: italic ; color: #666 ; }
.search-result .description { font-size: 80% ; padding: 2px 5px ; }
.search-result.publisher .name { border: 1px solid #c0c0c0 ; background: #a0e0f0 ; }
.search-result.publication .name { border: 1px solid #c0c0c0 ; background: #d0a080 ; }
.search-result.article .title { border: 1px solid #c0c0c0 ; background: #60f000 ; }
.search-result.article .subtitle { font-size: 80% ; font-style: italic ; }
.search-result .scenarios { font-size: 80% ; font-style: italic ; color: #666 ; }
.search-result .tags { margin-top: 0.25em ; font-size: 80% ; font-style: italic ; color: #666 ; }
.search-result .tags .tag { display: inline ; margin-right: 0.25em ; padding: 0 2px ; border: 1px solid #ccc ; background: #f0f0f0 ; }
.search-result .content { padding: 2px 5px ; font-size: 90% ; }
.search-result .content .image { float: left ; margin: 0.25em 0.5em 0.5em 0 ; max-height: 5em ; }
.search-result .footer { 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 ; }
.search-result .hilite { padding: 0 2px ; background: #f0e0b0 ; }

@ -1,4 +1,4 @@
* { margin: 0 ; padding: 0 }
* { margin: 0 ; padding: 0 ; }
body {
padding: 5px ;
@ -8,5 +8,8 @@ body {
}
ul, ol { margin: 0.5em 0 0 1.25em ; }
input[type="text"] { height: 25px ; border: 1px solid #c5c5c5 ; padding: 0 2px ; }
input[type="text"] { height: 22px ; border: 1px solid #c5c5c5 ; padding: 2px 5px ; }
label { height: 1.25em ; margin-top: -3px ; }
textarea { padding: 2px 5px ; resize: vertical ; }
button::-moz-focus-inner { border: 0 ; }

@ -1,4 +1,3 @@
import React from "react" ;
import ReactDOMServer from "react-dom/server" ;
// --------------------------------------------------------------------
@ -97,13 +96,6 @@ export function parseScenarioDisplayName( displayName ) {
// --------------------------------------------------------------------
export function makeOptionalLink( caption, url ) {
let link = <span dangerouslySetInnerHTML={{ __html: caption }} /> ;
if ( url )
link = <a href={url} target="_blank" rel="noopener noreferrer"> {link} </a> ;
return link ;
}
export function makeCommaList( vals, extract ) {
let result = [] ;
if ( vals ) {

Loading…
Cancel
Save