Show associated child objects in search results.

master
Pacman Ghost 4 years ago
parent a977630e25
commit 92204ca22c
  1. 17
      asl_articles/articles.py
  2. 20
      asl_articles/publications.py
  3. 12
      asl_articles/search.py
  4. 81
      asl_articles/tests/test_publications.py
  5. 57
      asl_articles/tests/test_publishers.py
  6. BIN
      web/public/images/collapsible-down.png
  7. BIN
      web/public/images/collapsible-up.png
  8. 19
      web/src/App.js
  9. 23
      web/src/ArticleSearchResult.js
  10. 69
      web/src/PublicationSearchResult.js
  11. 2
      web/src/PublicationSearchResult2.js
  12. 19
      web/src/PublisherSearchResult.js
  13. 5
      web/src/SearchResults.css
  14. 3
      web/src/constants.js
  15. 32
      web/src/utils.js

@ -9,9 +9,11 @@ from sqlalchemy.sql.expression import func
from asl_articles import app, db
from asl_articles.models import Article, Author, ArticleAuthor, Scenario, ArticleScenario, ArticleImage
from asl_articles.models import Publication
from asl_articles.authors import do_get_authors
from asl_articles.scenarios import do_get_scenarios
from asl_articles.tags import do_get_tags
import asl_articles.publications
from asl_articles import search
from asl_articles.utils import get_request_args, clean_request_args, clean_tags, encode_tags, decode_tags, \
apply_attrs, make_ok_response
@ -98,6 +100,9 @@ def create_article():
extras[ "authors" ] = do_get_authors()
extras[ "scenarios" ] = do_get_scenarios()
extras[ "tags" ] = do_get_tags()
if article.pub_id:
pub = Publication.query.get( article.pub_id )
extras[ "_publication" ] = asl_articles.publications.get_publication_vals( pub, True )
return make_ok_response( updated=updated, extras=extras, warnings=warnings )
def _set_seqno( article, pub_id ):
@ -230,6 +235,7 @@ def update_article():
article = Article.query.get( article_id )
if not article:
abort( 404 )
orig_pub = Publication.query.get( article.pub_id ) if article.pub_id else None
if vals["pub_id"] != article.pub_id:
_set_seqno( article, vals["pub_id"] )
vals[ "time_updated" ] = datetime.datetime.now()
@ -246,6 +252,14 @@ def update_article():
extras[ "authors" ] = do_get_authors()
extras[ "scenarios" ] = do_get_scenarios()
extras[ "tags" ] = do_get_tags()
pubs = []
if orig_pub and orig_pub.pub_id != article.pub_id:
pubs.append( asl_articles.publications.get_publication_vals( orig_pub, True ) )
if article.pub_id:
pub = Publication.query.get( article.pub_id )
pubs.append( asl_articles.publications.get_publication_vals( pub, True ) )
if pubs:
extras[ "_publications" ] = pubs
return make_ok_response( updated=updated, extras=extras, warnings=warnings )
# ---------------------------------------------------------------------
@ -271,4 +285,7 @@ def delete_article( article_id ):
if request.args.get( "list" ):
extras[ "authors" ] = do_get_authors()
extras[ "tags" ] = do_get_tags()
if article.pub_id:
pub = Publication.query.get( article.pub_id )
extras[ "_publication" ] = asl_articles.publications.get_publication_vals( pub, True )
return make_ok_response( extras=extras )

@ -30,7 +30,7 @@ def do_get_publications():
# NOTE: The front-end maintains a cache of the publications, so as a convenience,
# we return the current list as part of the response to a create/update/delete operation.
results = Publication.query.all()
return { r.pub_id: get_publication_vals(r) for r in results }
return { r.pub_id: get_publication_vals(r,False) for r in results }
# ---------------------------------------------------------------------
@ -41,14 +41,14 @@ def get_publication( pub_id ):
pub = Publication.query.get( pub_id )
if not pub:
abort( 404 )
vals = get_publication_vals( pub )
vals = get_publication_vals( pub, False )
# include the number of associated articles
query = Article.query.filter_by( pub_id = pub_id )
vals[ "nArticles" ] = query.count()
_logger.debug( "- %s ; #articles=%d", pub, vals["nArticles"] )
return jsonify( vals )
def get_publication_vals( pub, add_type=False ):
def get_publication_vals( pub, include_articles, add_type=False ):
"""Extract public fields from a Publication record."""
vals = {
"pub_id": pub.pub_id,
@ -62,6 +62,9 @@ def get_publication_vals( pub, add_type=False ):
"publ_id": pub.publ_id,
"time_created": int( pub.time_created.timestamp() ) if pub.time_created else None,
}
if include_articles:
articles = sorted( pub.articles, key=lambda a: 999 if a.article_seqno is None else a.article_seqno )
vals[ "articles" ] = [ get_article_vals(a) for a in articles ]
if add_type:
vals[ "type" ] = "publication"
return vals
@ -208,14 +211,3 @@ def delete_publication( pub_id ):
extras[ "publications" ] = do_get_publications()
extras[ "tags" ] = do_get_tags()
return make_ok_response( extras=extras )
# ---------------------------------------------------------------------
@app.route( "/publication/<pub_id>/articles" )
def get_publication_articles( pub_id ):
"""Get the articles for a publication."""
pub = Publication.query.get( pub_id )
if not pub:
abort( 404 )
articles = sorted( pub.articles, key=lambda a: 999 if a.article_seqno is None else a.article_seqno )
return jsonify( [ get_article_vals(a) for a in articles ] )

@ -118,6 +118,10 @@ def _do_search(): #pylint: disable=too-many-locals,too-many-statements,too-many-
no_hilite = to_bool( request.json.get( "no_hilite" ) )
_logger.info( "SEARCH REQUEST: %s", query_string )
_get_publisher_vals = lambda p: get_publisher_vals( p, True )
_get_publication_vals = lambda p: get_publication_vals( p, True, True )
_get_article_vals = lambda a: get_article_vals( a, True )
# check for special query terms (for testing porpoises)
results = []
def find_special_term( term ):
@ -129,11 +133,11 @@ def _do_search(): #pylint: disable=too-many-locals,too-many-statements,too-many-
return False
special_terms = {
SEARCH_ALL_PUBLISHERS:
lambda: [ get_publisher_vals(p,True) for p in Publisher.query ], #pylint: disable=not-an-iterable
lambda: [ _get_publisher_vals(p) for p in Publisher.query ], #pylint: disable=not-an-iterable
SEARCH_ALL_PUBLICATIONS:
lambda: [ get_publication_vals(p,True) for p in Publication.query ], #pylint: disable=not-an-iterable
lambda: [ _get_publication_vals(p) for p in Publication.query ], #pylint: disable=not-an-iterable
SEARCH_ALL_ARTICLES:
lambda: [ get_article_vals(a,True) for a in Article.query ] #pylint: disable=not-an-iterable
lambda: [ _get_article_vals(a) for a in Article.query ] #pylint: disable=not-an-iterable
}
if find_special_term( SEARCH_ALL ):
for term,func in special_terms.items():
@ -179,7 +183,7 @@ def _do_search(): #pylint: disable=too-many-locals,too-many-statements,too-many-
_logger.debug( "- {} ({:.3f})".format( obj, row[1] ) )
# prepare the result for the front-end
result = globals()[ "get_{}_vals".format( owner_type ) ]( obj )
result = locals()[ "_get_{}_vals".format( owner_type ) ]( obj )
result[ "type" ] = owner_type
# return highlighted versions of the content to the caller

@ -184,11 +184,11 @@ def test_delete_publication( webdriver, flask_app, dbconn ):
init_tests( webdriver, flask_app, dbconn, fixtures="publications.json" )
# start to delete a publication, but cancel the operation
article_name = "ASL Journal (2)"
article_title = "ASL Journal (2)"
results = do_search( SEARCH_ALL_PUBLICATIONS )
sr = find_search_result( article_name, results )
sr = find_search_result( article_title, results )
select_sr_menu_option( sr, "delete" )
check_ask_dialog( ( "Delete this publication?", article_name ), "cancel" )
check_ask_dialog( ( "Delete this publication?", article_title ), "cancel" )
# check that search results are unchanged on-screen
results2 = get_search_results()
@ -199,18 +199,18 @@ def test_delete_publication( webdriver, flask_app, dbconn ):
assert results3 == results
# delete the publication
sr = find_search_result( article_name, results3 )
sr = find_search_result( article_title, results3 )
select_sr_menu_option( sr, "delete" )
set_toast_marker( "info" )
check_ask_dialog( ( "Delete this publication?", article_name ), "ok" )
check_ask_dialog( ( "Delete this publication?", article_title ), "ok" )
wait_for( 2, lambda: check_toast( "info", "The publication was deleted." ) )
# check that search result was removed on-screen
wait_for( 2, lambda: article_name not in get_search_result_names() )
wait_for( 2, lambda: article_title not in get_search_result_names() )
# check that the search result was deleted from the database
results = do_search( SEARCH_ALL_PUBLICATIONS )
assert article_name not in get_search_result_names( results )
assert article_title not in get_search_result_names( results )
# ---------------------------------------------------------------------
@ -503,6 +503,73 @@ def test_timestamps( webdriver, flask_app, dbconn ):
# ---------------------------------------------------------------------
def test_article_lists( webdriver, flask_app, dbconn ):
"""Test showing articles that belong to a publication."""
# initialize
init_tests( webdriver, flask_app, dbconn, fixtures="publications.json" )
def check_articles( results, expected ):
for pub_name,article_title in expected.items():
pub_sr = find_search_result( pub_name, results )
articles = find_child( ".collapsible", pub_sr )
if article_title:
# check that the article appears in the publication's search result
assert find_child( ".caption", articles ).text == "Articles:"
articles = find_children( "li", articles )
assert len(articles) == 1
assert articles[0].text == article_title
# check that the "edit publication" dialog is correct
select_sr_menu_option( pub_sr, "edit" )
dlg = find_child( ".MuiDialog-root" )
articles = find_children( ".articles li", dlg )
assert len(articles) == 1
assert articles[0].text == article_title
find_child( "button.cancel", dlg ).click()
else:
# check that the publication has no associated articles
assert articles is None
# check that the "edit publication" dialog is correct
select_sr_menu_option( pub_sr, "edit" )
dlg = find_child( ".MuiDialog-root" )
articles = find_children( ".articles", dlg )
assert len(articles) == 0
find_child( "button.cancel", dlg ).click()
# check that the publications have no articles associated with them
results = do_search( SEARCH_ALL_PUBLICATIONS )
pub_name1, pub_name2 = "ASL Journal (1)", "MMP News"
check_articles( results, { pub_name1: None, pub_name2: None } )
# create an article that has no parent publication
create_article( { "title": "no parent" } )
check_articles( results, { pub_name1: None, pub_name2: None } )
# create an article that has a parent publication
article_title = "test article"
create_article( { "title": article_title, "publication": pub_name1 } )
check_articles( results, { pub_name1: article_title, pub_name2: None } )
# move the article to another publication
article_sr = find_search_result( article_title )
edit_article( article_sr, { "publication": pub_name2 } )
check_articles( None, { pub_name1: None, pub_name2: article_title } )
# change the article to have no parent publication
edit_article( article_sr, { "publication": "(none)" } )
check_articles( None, { pub_name1: None, pub_name2: None } )
# move the article back into a publication
edit_article( article_sr, { "publication": pub_name1 } )
check_articles( None, { pub_name1: article_title, pub_name2: None } )
# delete the article
select_sr_menu_option( article_sr, "delete" )
check_ask_dialog( ( "Delete this article?", article_title ), "ok" )
check_articles( None, { pub_name1: None, pub_name2: None } )
# ---------------------------------------------------------------------
def test_article_order( webdriver, flask_app, dbconn ):
"""Test ordering of articles."""

@ -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.test_publications import create_publication, edit_publication
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, \
do_test_confirm_discard_changes, \
wait_for, wait_for_elem, wait_for_not_elem, find_child, find_search_result, set_elem_text, \
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, change_image, remove_image, get_publisher_row, \
check_ask_dialog, check_error_msg
@ -386,6 +387,60 @@ def test_confirm_discard_changes( webdriver, flask_app, dbconn ):
# ---------------------------------------------------------------------
def test_publication_lists( webdriver, flask_app, dbconn ):
"""Test showing publications that belong a publisher."""
# initialize
init_tests( webdriver, flask_app, dbconn, fixtures="publishers.json" )
def check_publications( results, expected ):
for publ_name,pub_name in expected.items():
publ_sr = find_search_result( publ_name, results )
pubs = find_child( ".collapsible", publ_sr )
if pub_name:
# check that the publisher appears in the publisher's search result
assert find_child( ".caption", pubs ).text == "Publications:"
pubs = find_children( "li", pubs )
assert len(pubs) == 1
assert pubs[0].text == pub_name
else:
# check that the publisher has no associated publications
assert pubs is None
# check that the publishers have no publications associated with them
results = do_search( SEARCH_ALL_PUBLISHERS )
publ_name1, publ_name2 = "Avalon Hill", "Multiman Publishing"
check_publications( results, { publ_name1: None, publ_name2: None } )
# create a publication that has no parent publisher
create_publication( { "name": "no parent" } )
check_publications( results, { publ_name1: None, publ_name2: None } )
# create a publication that has a parent publisher
pub_name = "test publication"
create_publication( { "name": pub_name, "publisher": publ_name1 } )
check_publications( results, { publ_name1: pub_name, publ_name2: None } )
# move the publication to another publisher
pub_sr = find_search_result( pub_name )
edit_publication( pub_sr, { "publisher": publ_name2 } )
check_publications( results, { publ_name1: None, publ_name2: pub_name } )
# change the publication to have no parent publisher
edit_publication( pub_sr, { "publisher": "(none)" } )
check_publications( results, { publ_name1: None, publ_name2: None } )
# move the publication back to a publisher
edit_publication( pub_sr, { "publisher": publ_name1 } )
check_publications( results, { publ_name1: pub_name, publ_name2: None } )
# delete the publication
select_sr_menu_option( pub_sr, "delete" )
check_ask_dialog( ( "Delete this publication?", pub_name ), "ok" )
check_publications( results, { publ_name1: None, publ_name2: None } )
# ---------------------------------------------------------------------
def test_timestamps( webdriver, flask_app, dbconn ):
"""Test setting of timestamps."""

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

@ -189,6 +189,25 @@ export default class App extends React.Component
this.setState( { searchResults: newSearchResults } ) ;
}
updatePublications( pubs ) {
// update the cache
let pubs2 = {} ;
for ( let i=0 ; i < pubs.length ; ++i ) {
const pub = pubs[ i ] ;
this.caches.publications[ pub.pub_id ] = pub ;
pubs2[ pub.pub_id ] = pub ;
}
// update the UI
let newSearchResults = this.state.searchResults ;
for ( let i=0 ; i < newSearchResults.length ; ++i ) {
if ( newSearchResults[i].type === "publication" && pubs2[ newSearchResults[i].pub_id ] ) {
newSearchResults[i] = pubs2[ newSearchResults[i].pub_id ] ;
newSearchResults[i].type = "publication" ;
}
}
this.setState( { searchResults: newSearchResults } ) ;
}
showModalForm( formId, title, titleColor, content, buttons ) {
// prepare the buttons
let buttons2 = [] ;

@ -14,11 +14,15 @@ export class ArticleSearchResult extends React.Component
{
render() {
// prepare the basic details
const display_title = this.props.data[ "article_title!" ] || this.props.data.article_title ;
const display_subtitle = this.props.data[ "article_subtitle!" ] || this.props.data.article_subtitle ;
const display_snippet = this.props.data[ "article_snippet!" ] || this.props.data.article_snippet ;
const pub = gAppRef.caches.publications[ this.props.data.pub_id ] ;
const image_url = gAppRef.makeFlaskImageUrl( "article", this.props.data.article_image_id, true ) ;
// prepare the authors
let authors ;
if ( this.props.data[ "authors!" ] ) {
// the backend has provided us with a list of author names (possibly highlighted) - use them directly
@ -31,6 +35,8 @@ export class ArticleSearchResult extends React.Component
(a) => <span className="author" key={a} dangerouslySetInnerHTML={{__html: gAppRef.caches.authors[a].author_name}} />
) ;
}
// prepare the scenarios
let scenarios ;
if ( this.props.data[ "scenarios!" ] ) {
// the backend has provided us with a list of scenarios (possibly highlighted) - use them directly
@ -45,6 +51,8 @@ export class ArticleSearchResult extends React.Component
(s) => <span className="scenario" key={s} dangerouslySetInnerHTML={{__html: makeScenarioDisplayName(gAppRef.caches.scenarios[s])}} />
) ;
}
// prepare the tags
let tags = [] ;
if ( this.props.data[ "tags!" ] ) {
// the backend has provided us with a list of tags (possibly highlighted) - use them directly
@ -61,9 +69,8 @@ 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 ;
// prepare the menu
const menu = ( <Menu>
<MenuButton className="sr-menu" />
<MenuList>
@ -75,6 +82,10 @@ export class ArticleSearchResult extends React.Component
>Delete</MenuItem>
</MenuList>
</Menu> ) ;
// 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 ;
return ( <div className="search-result article"
ref = { r => gAppRef.setTestAttribute( r, "article_id", this.props.data.article_id ) }
>
@ -113,6 +124,8 @@ export class ArticleSearchResult extends React.Component
gAppRef.showWarnings( "The new article was created OK.", resp.data.warnings ) ;
else
gAppRef.showInfoToast( <div> The new article was created OK. </div> ) ;
if ( resp.data._publication )
gAppRef.updatePublications( [ resp.data._publication ] ) ;
gAppRef.closeModalForm() ;
} )
.catch( err => {
@ -139,6 +152,8 @@ export class ArticleSearchResult extends React.Component
gAppRef.showWarnings( "The article was updated OK.", resp.data.warnings ) ;
else
gAppRef.showInfoToast( <div> The article was updated OK. </div> ) ;
if ( resp.data._publications )
gAppRef.updatePublications( resp.data._publications ) ;
gAppRef.closeModalForm() ;
} )
.catch( err => {
@ -167,6 +182,8 @@ export class ArticleSearchResult extends React.Component
gAppRef.showWarnings( "The article was deleted.", resp.data.warnings ) ;
else
gAppRef.showInfoToast( <div> The article was deleted. </div> ) ;
if ( resp.data._publication )
gAppRef.updatePublications( [ resp.data._publication ] ) ;
} )
.catch( err => {
gAppRef.showErrorToast( <div> Couldn't delete the article: <div className="monospace"> {err.toString()} </div> </div> ) ;

@ -2,8 +2,9 @@ import React from "react" ;
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 { pluralString, applyUpdatedVals, removeSpecialFields } from "./utils.js" ;
import { makeCollapsibleList, pluralString, applyUpdatedVals, removeSpecialFields } from "./utils.js" ;
const axios = require( "axios" ) ;
@ -13,9 +14,13 @@ export class PublicationSearchResult extends React.Component
{
render() {
// prepare the basic details
const display_description = this.props.data[ "pub_description!" ] || this.props.data.pub_description ;
const publ = gAppRef.caches.publishers[ this.props.data.publ_id ] ;
const image_url = gAppRef.makeFlaskImageUrl( "publication", this.props.data.pub_image_id, true ) ;
// prepare the tags
let tags = [] ;
if ( this.props.data[ "tags!" ] ) {
// the backend has provided us with a list of tags (possibly highlighted) - use them directly
@ -32,6 +37,13 @@ export class PublicationSearchResult extends React.Component
) ;
}
}
// prepare the articles
let articles = null ;
if ( this.props.data.articles )
articles = this.props.data.articles.map( a => a.article_title ) ;
// prepare the menu
const menu = ( <Menu>
<MenuButton className="sr-menu" />
<MenuList>
@ -43,6 +55,7 @@ export class PublicationSearchResult extends React.Component
>Delete</MenuItem>
</MenuList>
</Menu> ) ;
return ( <div className="search-result publication"
ref = { r => gAppRef.setTestAttribute( r, "pub_id", this.props.data.pub_id ) }
>
@ -55,6 +68,7 @@ export class PublicationSearchResult extends React.Component
<div className="content">
{ image_url && <img src={image_url} className="image" alt="Publication." /> }
<div className="description" dangerouslySetInnerHTML={{__html: display_description}} />
{ makeCollapsibleList( "Articles:", articles, PUBLICATION_EXCESS_ARTICLE_THRESHOLD, {float:"left",marginBottom:"0.25em"} ) }
</div>
<div className="footer">
{ this.props.data.pub_date && <div> <label>Published:</label> <span className="pub_date"> {this.props.data.pub_date} </span> </div> }
@ -79,6 +93,8 @@ export class PublicationSearchResult extends React.Component
else
gAppRef.showInfoToast( <div> The new publication was created OK. </div> ) ;
gAppRef.closeModalForm() ;
// NOTE: The parent publisher will update itself in the UI to show this new publication,
// since we've just received an updated copy of the publications.
} )
.catch( err => {
gAppRef.showErrorMsg( <div> Couldn't create the publication: <div className="monospace"> {err.toString()} </div> </div> ) ;
@ -88,35 +104,32 @@ export class PublicationSearchResult extends React.Component
onEditPublication() {
// get the articles for this publication
axios.get( gAppRef.makeFlaskUrl( "/publication/" + this.props.data.pub_id + "/articles" ) )
.then( resp => {
let articles = resp.data ; // nb: _doEditPublication() might modify this list
PublicationSearchResult2._doEditPublication( this.props.data, articles, (newVals,refs) => {
// send the updated details to the server
newVals.pub_id = this.props.data.pub_id ;
let articles = this.props.data.articles ; // nb: _doEditPublication() might change the order of this list
PublicationSearchResult2._doEditPublication( this.props.data, articles, (newVals,refs) => {
// send the updated details to the server
newVals.pub_id = this.props.data.pub_id ;
if ( articles )
newVals.article_order = articles.map( a => a.article_id ) ;
axios.post( gAppRef.makeFlaskUrl( "/publication/update", {list:1} ), newVals )
.then( resp => {
// update the caches
gAppRef.caches.publications = resp.data.publications ;
gAppRef.caches.tags = resp.data.tags ;
// update the UI with the new details
applyUpdatedVals( this.props.data, newVals, resp.data.updated, refs ) ;
removeSpecialFields( this.props.data ) ;
this.forceUpdate() ;
if ( resp.data.warnings )
gAppRef.showWarnings( "The publication was updated OK.", resp.data.warnings ) ;
else
gAppRef.showInfoToast( <div> The publication was updated OK. </div> ) ;
gAppRef.closeModalForm() ;
} )
.catch( err => {
gAppRef.showErrorMsg( <div> Couldn't update the publication: <div className="monospace"> {err.toString()} </div> </div> ) ;
} ) ;
axios.post( gAppRef.makeFlaskUrl( "/publication/update", {list:1} ), newVals )
.then( resp => {
// update the caches
gAppRef.caches.publications = resp.data.publications ;
gAppRef.caches.tags = resp.data.tags ;
// update the UI with the new details
applyUpdatedVals( this.props.data, newVals, resp.data.updated, refs ) ;
removeSpecialFields( this.props.data ) ;
this.forceUpdate() ;
if ( resp.data.warnings )
gAppRef.showWarnings( "The publication was updated OK.", resp.data.warnings ) ;
else
gAppRef.showInfoToast( <div> The publication was updated OK. </div> ) ;
gAppRef.closeModalForm() ;
// NOTE: The parent publisher will update itself in the UI to show this updated publication,
// since we've just received an updated copy of the publications.
} )
.catch( err => {
gAppRef.showErrorMsg( <div> Couldn't update the publication: <div className="monospace"> {err.toString()} </div> </div> ) ;
} ) ;
} )
.catch( err => {
gAppRef.showErrorMsg( <div> Couldn't load the articles: <div className="monospace"> {err.toString()} </div> </div> ) ;
} ) ;
}

@ -223,7 +223,7 @@ export class PublicationSearchResult2
function checkArticlePageNumbers( articles ) {
// check the order of the article page numbers
let curr_pageno = null ;
if ( articles === null )
if ( ! articles )
return false ;
for ( let i=0 ; i < articles.length ; ++i ) {
if ( ! isNumeric( articles[i].article_pageno ) )

@ -2,8 +2,10 @@ import React from "react" ;
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 { pluralString, applyUpdatedVals, removeSpecialFields } from "./utils.js" ;
import { makeCollapsibleList, pluralString, applyUpdatedVals, removeSpecialFields } from "./utils.js" ;
const axios = require( "axios" ) ;
@ -13,9 +15,22 @@ export class PublisherSearchResult extends React.Component
{
render() {
// prepare the basic details
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 ) ;
// prepare the publications
let pubs = [] ;
for ( let pub of Object.entries(gAppRef.caches.publications) ) {
if ( pub[1].publ_id === this.props.data.publ_id )
pubs.push( pub[1] ) ;
}
pubs.sort( (lhs,rhs) => rhs.time_created - lhs.time_created ) ;
pubs = pubs.map( p => PublicationSearchResult.makeDisplayName(p) ) ;
// prepare the menu
const menu = ( <Menu>
<MenuButton className="sr-menu" />
<MenuList>
@ -27,6 +42,7 @@ export class PublisherSearchResult extends React.Component
>Delete</MenuItem>
</MenuList>
</Menu> ) ;
return ( <div className="search-result publisher"
ref = { r => gAppRef.setTestAttribute( r, "publ_id", this.props.data.publ_id ) }
>
@ -38,6 +54,7 @@ export class PublisherSearchResult extends React.Component
<div className="content">
{ image_url && <img src={image_url} className="image" alt="Publisher." /> }
<div className="description" dangerouslySetInnerHTML={{__html: display_description}} />
{ makeCollapsibleList( "Publications:", pubs, PUBLISHER_EXCESS_PUBLICATION_THRESHOLD, {float:"left",marginBottom:"0.25em"} ) }
</div>
</div> ) ;
}

@ -27,8 +27,11 @@
.search-result .content p { margin-top: 0.25em ; }
.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: 5em ; }
.search-result .content .collapsible { margin-top:0.5em ; font-size: 90% ; color: #444 ; }
.search-result .content .collapsible .caption img { height: 0.75em ; margin-left: 0.25em ; cursor: pointer ; }
.search-result .content .collapsible ul { margin: 0 0 0 1em ; }
.search-result .footer { padding: 0 5px ; font-size: 80% ; font-style: italic ; color: #666 ; }
.search-result .footer { clear: both ; padding: 0 5px ; font-size: 80% ; font-style: italic ; color: #666 ; }
.search-result .footer .tag { display: inline ; margin-right: 0.25em ; padding: 0 2px ; border: 1px solid #ccc ; background: #f0f0f0 ; }
.search-result .hilite { padding: 0 2px ; background: #ffffa0 ; }

@ -1,3 +1,6 @@
export let MAX_IMAGE_UPLOAD_SIZE = ( 1 * 1024*1024 ) ;
export let PUBLISHER_EXCESS_PUBLICATION_THRESHOLD = 5 ;
export let PUBLICATION_EXCESS_ARTICLE_THRESHOLD = 8 ;
export let NEW_ARTICLE_PUB_PRIORITY_CUTOFF = ( 24 * 60 * 60 ) ;

@ -170,6 +170,38 @@ export function parseScenarioDisplayName( displayName ) {
// --------------------------------------------------------------------
export function makeCollapsibleList( caption, vals, maxItems, style ) {
if ( ! vals || vals.length === 0 )
return null ;
let items=[], excessItems=[] ;
let excessItemRefs={}, flipButtonRef=null ;
for ( let i=0 ; i < vals.length ; ++i ) {
if ( i < maxItems ) {
items.push(
<li key={i} dangerouslySetInnerHTML={{ __html: vals[i] }} />
) ;
} else {
excessItems.push(
<li key={i} dangerouslySetInnerHTML={{ __html: vals[i] }} style={{display:"none"}} ref={r => excessItemRefs[i]=r} />
) ;
}
}
function flipExcessItems() {
const pos = flipButtonRef.src.lastIndexOf( "/" ) ;
const show = flipButtonRef.src.substr( pos ) === "/collapsible-down.png" ;
for ( let r in excessItemRefs )
excessItemRefs[r].style.display = show ? "list-item" : "none" ;
flipButtonRef.src = flipButtonRef.src.substr( 0, pos ) + (show ? "/collapsible-up.png" : "/collapsible-down.png") ;
}
return ( <div className="collapsible" style={style}>
<div className="caption"> {caption}
{ excessItems.length > 0 && <img src="images/collapsible-down.png" onClick={flipExcessItems} ref={r => flipButtonRef=r} alt="Show/hide extra items." /> }
</div>
<ul> {items} </ul>
{ excessItems.length > 0 && <ul> {excessItems} </ul> }
</div> ) ;
}
export function makeCommaList( vals, extract ) {
let result = [] ;
if ( vals ) {

Loading…
Cancel
Save