diff --git a/docker-template.yaml b/docker-template.yaml index 52cb2de4..44e8c309 100644 --- a/docker-template.yaml +++ b/docker-template.yaml @@ -45,12 +45,11 @@ services: ports: - ${FUSEKI_PORT}:3030 environment: - - JVM_ARGS=${JVM_ARGS:- -Xmx16g -Djena.scripting=true} + - JVM_ARGS=${JVM_ARGS:- -Xmx16g -Djena:scripting=true} - GOOGLE_APPLICATION_CREDENTIALS=/etc/fin/service-account.json restart: unless-stopped - command: - HDT=import server - # command: bash -c 'tail -f /dev/null' + command: server + # command: noop ### diff --git a/harvest/Dockerfile b/harvest/Dockerfile index dbe42961..2dc1f64d 100644 --- a/harvest/Dockerfile +++ b/harvest/Dockerfile @@ -112,11 +112,8 @@ COPY experts-client ${AE_CLIENT} #RUN cd ${AE_CLIENT} && rm -rf package-lock.json node_modules && yarn global add simple-copy typescript && yarn link @ucd-lib/experts-api && yarn install RUN cd ${AE_CLIENT} && rm -rf package-lock.json node_modules && npm link @ucd-lib/experts-api && npm install -g -# Setup a place to hold the Makefile data -RUN mkdir /usr/local/lib/harvest -COPY harvest /usr/local/lib/harvest - # Add startup file +COPY /startup.mk / COPY /harvest-entrypoint.sh / # Add sqlcl and tarql to bin diff --git a/harvest/experts-client/bin/experts-cdl.js b/harvest/experts-client/bin/experts-cdl.js index c31da2d2..b3423a99 100644 --- a/harvest/experts-client/bin/experts-cdl.js +++ b/harvest/experts-client/bin/experts-cdl.js @@ -30,7 +30,7 @@ async function main(opt, cache) { await cache_expert.fetch(); await cache_expert.load(); await cache_expert.transform(); - if (opt.fuseki.delete === true && cache_expert._db) { + if (opt["fuseki.delete"] === true && cache_expert._db) { log.info(`Deleting ${cache_expert._db.db}`); await cache_expert._db.drop(); } diff --git a/harvest/experts-client/lib/cache/query/grant/construct.rq b/harvest/experts-client/lib/cache/query/grant/construct.rq index a3762201..e84a2e8b 100644 --- a/harvest/experts-client/lib/cache/query/grant/construct.rq +++ b/harvest/experts-client/lib/cache/query/grant/construct.rq @@ -3,6 +3,7 @@ PREFIX bibo: PREFIX cite: PREFIX expert: PREFIX experts: +PREFIX js: PREFIX kfs: PREFIX list: PREFIX obo: @@ -24,8 +25,7 @@ construct { . ?grant a vivo:Grant,?grant_type; - rdfs:label ?grant_label; - schema:name ?grant_title; + schema:name ?grant_label; schema:identifier ?grant,?g; vivo:totalAwardAmount ?total_award_amount; vivo:sponsorAwardId ?sponsor_award_id; @@ -34,8 +34,6 @@ construct { vivo:relatedBy ?RELATIONSHIP__,?labeled_only_role; . -# ?EXPERT__ schema:name ?user_name. - ?labeled_only_role a vivo:CoPrincipalInvestigatorRole; vivo:relates ?labeled_only_person,?grant; . @@ -130,36 +128,48 @@ WHERE { . } } - ?record :native/:field ?field. OPTIONAL { - ?field :name "title"; - :type "text"; - :text ?grant_label; - . - bind(replace(?grant_label,"(?:SEE\\s+)?(?:(?:[ABCKKXYZ][0-9CF]{6})*(?:\\s*-)?\\s*)*\\s*(?:SP0A\\d{6})?(..*?)(\\s+K.[0-9]{2}.[0-9]{1,2})?$","$1") as ?grant_title) + ?record :native/:field [ :name "title"; + :type "text"; + :text ?grant_raw_title ]; + . + bind(replace(?grant_raw_title,"(?:SEE\\s+)?(?:(?:[ABCKKXYZ][0-9CF]{6})*(?:\\s*-)?\\s*)*\\s*(?:SP0A\\d{6})?(..*?)(\\s+K.[0-9]{2}.[0-9]{1,2})?$","$1") as ?grant_title) } OPTIONAL { - ?field :name "funder-reference"; - :type "text"; - :text ?sponsor_award_id; - . + ?record :native/:field [ :name "funder-name"; + :type "text"; + :text ?funder_label_cap ]. + + ?record :native/:field [ :name "funder-reference"; + :type "text"; + :text ?sponsor_award_id ]. } OPTIONAL { - ?field :name "amount"; - :type "money"; - :money/:field-value ?total_award_amount; - . + ?record :native/:field [ :name "amount"; + :type "money"; + :money/:field-value ?total_award_amount ]; + . + } + + OPTIONAL { + ?record :native/:field [ :name "c-pi"; + :type "text"; + :text ?pi_name_cap ]; + . } OPTIONAL { - ?field :name "c-co-pis"; - :people/:person [ list:index(?pos ?elem) ] . - ?elem :last-name ?labeled_only_name_last . + ?record :native/:field [ :name "c-co-pis"; + :people/:person [ list:index(?pos ?elem) ] + ]. + ?elem :last-name ?labeled_only_name_last_cap . + bind(js:capitalizeName(?labeled_only_name_last_cap) as ?labeled_only_name_last) OPTIONAL { - ?elem :first-names ?labeled_only_name_first . + ?elem :first-names ?labeled_only_name_first_cap . + bind(js:capitalizeName(?labeled_only_name_first_cap) as ?labeled_only_name_first) } OPTIONAL { ?elem :initials ?labeled_only_name_initials . @@ -182,15 +192,9 @@ WHERE { . } - OPTIONAL { - ?field :name "funder-name"; - :type "text"; - :text ?funder_label; - . - } { - select ?g ?start_date ?start_date_precision ?end_date ?end_date_precision + select ?g ?start_date ?start_year ?start_date_precision ?end_year ?end_date ?end_date_precision WHERE { { select ?relationship ?grant_id ?record ?source @@ -224,7 +228,7 @@ WHERE { :type "date"; :date ?s_date; . - ?s_date :year ?syear. + ?s_date :year ?start_year. BIND(vivo:yearPrecision AS ?syear_p) OPTIONAL { @@ -239,14 +243,14 @@ WHERE { BIND(vivo:yearMonthDayPrecision AS ?sday_p) } } - BIND(CONCAT(?syear, COALESCE(?smonth, ""), COALESCE(?sday, "")) AS ?start_date) + BIND(CONCAT(?start_year, COALESCE(?smonth, ""), COALESCE(?sday, "")) AS ?start_date) BIND(coalesce(?sday_p,?smonth_p,?syear_p) as ?start_date_precision) ?e :name "end-date"; :type "date"; :date ?e_date; . - ?e_date :year ?eyear. + ?e_date :year ?end_year. BIND(vivo:yearPrecision AS ?eyear_p) OPTIONAL { @@ -261,7 +265,7 @@ WHERE { BIND(vivo:yearMonthDayPrecision AS ?eday_p) } } - BIND(CONCAT(?eyear, COALESCE(?emonth, ""), COALESCE(?eday, "")) AS ?end_date) + BIND(CONCAT(?end_year, COALESCE(?emonth, ""), COALESCE(?eday, "")) AS ?end_date) BIND(coalesce(?eday_p,?emonth_p,?eyear_p) as ?end_date_precision) } @@ -309,4 +313,15 @@ WHERE { uri(concat(str(?grant),"#roleof_",?labeled_only_name_match)), ?undefined) as ?labeled_only_role) + + bind(js:capitalizeName(?funder_label_cap) as ?funder_label) + bind(js:capitalizeName(?pi_name_cap,"\\s+"," ") as ?pi_name) + + bind(concat(?grant_title," § ", + coalesce(if(xsd:date(?end_date) < xsd:date(now()), "Completed", "Active"),""), + coalesce(concat(" • ",?start_year," - ",?end_year),""), + coalesce(concat(" • ",?pi_name),""), + " § ", + coalesce(?funder_label,""), + coalesce(concat(" • ",?sponsor_award_id),"")) as ?grant_label) } diff --git a/harvest/harvest-entrypoint.sh b/harvest/harvest-entrypoint.sh index cc8291af..f08a283a 100755 --- a/harvest/harvest-entrypoint.sh +++ b/harvest/harvest-entrypoint.sh @@ -32,8 +32,8 @@ if [[ ${uid} = 0 ]]; then else echo \"no /etc/fin/service-account.json and GOOGLE_APPLICATION_CREDENTIALS_JSON is not set\"; fi; - exec setpriv --reuid=ucd.process --init-groups -- make --file=/usr/local/lib/harvest/Makefile "$@" + exec setpriv --reuid=ucd.process --init-groups -- make --file=/startup.mk "$@" else - exec make --file=/usr/local/lib/harvest/Makefile "$@" + exec make --file=/startup.mk "$@" fi diff --git a/harvest/harvest/Makefile b/harvest/startup.mk similarity index 100% rename from harvest/harvest/Makefile rename to harvest/startup.mk diff --git a/services/base-service/models/base/model.js b/services/base-service/models/base/model.js index 0ecfe563..f934ee6f 100644 --- a/services/base-service/models/base/model.js +++ b/services/base-service/models/base/model.js @@ -187,6 +187,9 @@ class BaseModel extends FinEsDataModel { name: node['name'], "@graph": [node] }; + ["@type","is-visible","updated","identifier"].forEach(key => { + if (node[key]) doc[key] = node[key]; + }); return doc; } @@ -307,13 +310,12 @@ class BaseModel extends FinEsDataModel { * @returns {Object} ES results */ async search(opts) { - + opts.index || (opts.index = this.readIndexAlias); const params = this.common_parms(opts.params); - console.log(`searching ${this.readIndexAlias} with ${JSON.stringify(params)}`); const options = { id: (opts.id)?opts.id:"default", - index: this.readIndexAlias, + index: opts.index, params } const res=await this.client.searchTemplate(options); @@ -321,9 +323,7 @@ class BaseModel extends FinEsDataModel { } async msearch(opts) { - - opts.index = "expert-read"; - + opts.index || (opts.index = this.readIndexAlias); // Fix-up parms for(let i=0;i { - def.parameters.push(openapi.parameters(param)); - }); - - delete options.parameters; - - return openapi.validPath({...def, ...options}); -} - -function browse_valid_path_error(err, req, res, next) { - return res.status(err.status).json({ - error: err.message, - validation: err.validationErrors, - schema: err.validationSchema - }) -} - -// This will serve the generated json document(s) -// (as well as the swagger-ui if configured) -router.use(openapi); - -router.get( - '/', - is_user, - browse_valid_path( - { - description: "Returns counts for experts A - Z, or if sending query param p={letter}, will return results for experts with last names of that letter", - parameters: ['p', 'page', 'size'], - responses: { - "200": openapi.response('Browse'), - "400": openapi.response('Invalid_request') - } - } - ), - browse_valid_path_error, - async (req, res) => { - const params = { - size: 25 - }; - ["size","page","p"].forEach((key) => { - if (req.query[key]) { params[key] = req.query[key]; } - }); - - if (params.p) { - const opts = { - index: "expert-read", - id: "family_prefix", - params - }; - - try { - await experts.verify_template(template); - const find = await experts.search(opts); - res.send(find); - } catch (err) { - res.status(400).send('Invalid request'); - } - } else { - try { - await experts.verify_template(template); - const search_templates=[]; - ["A","B","C","D","E","F","G","H","I","J","K","L","M","N","O", - "P","Q","R","S","T","U","V","W","X","Y","Z"].forEach((letter) => { - search_templates.push({}); - search_templates.push({id:"family_prefix",params:{p:letter,size:0}}); - }); - const finds = await experts.msearch({search_templates}); - res.send(finds); - } catch (err) { - res.status(400).send('Invalid request'); - } - } -}); - -module.exports = router; diff --git a/services/base-service/models/browse/index.js b/services/base-service/models/browse/index.js deleted file mode 100644 index 1d829878..00000000 --- a/services/base-service/models/browse/index.js +++ /dev/null @@ -1,3 +0,0 @@ -module.exports = { - api : require('./api.js') -} diff --git a/services/base-service/models/expert/api.js b/services/base-service/models/expert/api.js index 67bec720..e7da041e 100644 --- a/services/base-service/models/expert/api.js +++ b/services/base-service/models/expert/api.js @@ -7,8 +7,9 @@ const {defaultEsApiGenerator} = dataModels; const md5 = require('md5'); // const { logger } = require('@ucd-lib/fin-service-utils'); const model= new ExpertModel(); +const template = require('../base/template/name.json'); -const { openapi, schema_error, json_only, user_can_edit, is_user } = require('../middleware.js') +const { openapi, schema_error, json_only, user_can_edit, is_user, valid_path, valid_path_error } = require('../middleware.js') function subselect(req, res, next) { try { @@ -37,6 +38,69 @@ router.get('/', (req, res) => { // (as well as the swagger-ui if configured) router.use(openapi); +router.route( + '/browse', +).get( + is_user, + valid_path( + { + description: "Returns counts for experts A - Z, or if sending query param p={letter}, will return results for experts with last names of that letter", + parameters: ['p', 'page', 'size'], + responses: { + "200": openapi.response('Browse'), + "400": openapi.response('Invalid_request') + } + } + ), + valid_path_error, + async (req, res) => { + const params = { + size: 25 + }; + ["size","page","p"].forEach((key) => { + if (req.query[key]) { params[key] = req.query[key]; } + }); + + if (params.p) { + if (params.p === 'other') { + params.p = '1'; + } else if (params.p.match(/^[a-zA-Z]/)) { + params.p = params.p.substring(0,1); + } else { + params.p = '1'; + } + + const opts = { + id: "name", + params + }; + + try { + await model.verify_template(template); + const find = await model.search(opts); + res.send(find); + } catch (err) { + res.status(400).send('Invalid request'); + } + } else { + try { + await model.verify_template(template); + const search_templates=[]; + ["1","A","B","C","D","E","F","G","H","I","J","K","L","M","N","O", + "P","Q","R","S","T","U","V","W","X","Y","Z"].forEach((letter) => { + search_templates.push({}); + search_templates.push({id:"name",params:{p:letter,size:0}}); + }); + const finds = await model.msearch({search_templates}); + res.send(finds); + } catch (err) { + res.status(400).send('Invalid request'); + } + } + } +); + + router.patch('/:expertId/availability', // expert_valid_path( // { @@ -62,6 +126,7 @@ router.patch('/:expertId/availability', } ) + router.route( '/:expertId/:relationshipId' ).get( diff --git a/services/base-service/models/grant/api.js b/services/base-service/models/grant/api.js index 45a54d49..6369887e 100644 --- a/services/base-service/models/grant/api.js +++ b/services/base-service/models/grant/api.js @@ -3,7 +3,7 @@ const {dataModels,logger} = require('@ucd-lib/fin-service-utils'); const GrantModel = require('./model.js'); const utils = require('../utils.js') const {defaultEsApiGenerator} = dataModels; -const template = require('./template/grant_name.json'); +const template = require('../base/template/name.json'); const { openapi, schema_error, json_only, user_can_edit, is_user, valid_path, valid_path_error } = require('../middleware.js') @@ -16,16 +16,16 @@ router.route( '/browse', ).get( is_user, - valid_path( - { - description: "Returns counts for experts A - Z, or if sending query param p={letter}, will return results for experts with last names of that letter", - parameters: ['p', 'page', 'size'], - responses: { - "200": openapi.response('Browse'), - "400": openapi.response('Invalid_request') - } - } - ), + valid_path( + { + description: "Returns counts for experts A - Z, or if sending query param p={letter}, will return results for experts with last names of that letter", + parameters: ['p', 'page', 'size'], + responses: { + "200": openapi.response('Browse'), + "400": openapi.response('Invalid_request') + } + } + ), valid_path_error, async (req, res) => { const params = { @@ -36,6 +36,14 @@ router.route( }); if (params.p) { + if (params.p === 'other') { + params.p = '1'; + } else if (params.p.match(/^[a-zA-Z]/)) { + params.p = params.p.substring(0,1); + } else { + params.p = '1'; + } + const opts = { index: "grant-read", id: "name", @@ -53,7 +61,7 @@ router.route( try { await model.verify_template(template); const search_templates=[]; - ["A","B","C","D","E","F","G","H","I","J","K","L","M","N","O", + ["1","A","B","C","D","E","F","G","H","I","J","K","L","M","N","O", "P","Q","R","S","T","U","V","W","X","Y","Z"].forEach((letter) => { search_templates.push({}); search_templates.push({id:"name",params:{p:letter,size:0}}); diff --git a/services/base-service/models/grant/model.js b/services/base-service/models/grant/model.js index 42d0f2eb..1ebf977d 100644 --- a/services/base-service/models/grant/model.js +++ b/services/base-service/models/grant/model.js @@ -30,7 +30,7 @@ class GrantModel extends BaseModel { const relatedBy = {}; - // combine relatedBy (ordered) + // combine relatedBy (ordered) function addRelatedBy(node) { if (node.relatedBy) { Array.isArray(node.relatedBy) || (node.relatedBy = [node.relatedBy]); @@ -54,19 +54,19 @@ class GrantModel extends BaseModel { let last=name.family.toLowerCase().replace(/[^a-z]/g,''); if (name.given && name.given.length) { let first=name.given.toLowerCase().replace(/[^a-z]/g,''); - name_match[`${last}_${first}`]=expert; - name_match[`${last}_$first[0]`]=expert; + name_match[`${last}_${first}`]=true; + name_match[`${last}_${first[0]}`]=true; if (name.middle && name.middle.length) { let middle=name.middle.toLowerCase().replace(/[^a-z]/g,''); - name_match[`${last}_${first[0]}${middle[0]}`]=expert; + name_match[`${last}_${first[0]}${middle[0]}`]=true; } } else if (name.middle && name.middle.length) { let middle=name.middle.toLowerCase().replace(/[^a-z]/g,''); - name_match[`${last}_$middle[0]`]=expert; + name_match[`${last}_${middle[0]}`]=true; } else { - name_match[last]=expert; + name_match[last]=true; } - return Object.values(name_match); + return Object.keys(name_match); } const expertModel = await this.get_model('expert'); @@ -77,36 +77,36 @@ class GrantModel extends BaseModel { let role=relatedBy[rel]; if (role.inheres_in) { let id = role.inheres_in['@id'] || role.inheres_in; -// try { let expert = await expertModel.client_get(role.inheres_in); expert=expertModel.get_expected_model_node(expert); - experts.push(expert); - if (expert?.contactInfo?.hasName) { - console.log('name:', expert.contactInfo.hasName); - nameMatches(expert?.contactInfo?.hasName).forEach((n) => { - name_match[n]=expert; + experts.push(expert); + if (expert?.hasName) { + nameMatches(expert?.hasName).forEach((n) => { + name_match[n]=role.inheres_in; }); } -// } catch(e) { -// logger.error(`GrantModel.update expert '${id}' not found`); -// } } } // finally remove relatedBy with names that match experts - for (var rel in relatedBy) { + for (const rel in relatedBy) { let role=relatedBy[rel]; - if (! role.inheres_in && role?.relates?.hasName) { - console.log('other name:', role.hasName); - nameMatches(rel.hasName).forEach((nm) => { - if (name_match[nm]) { - delete relatedBy[rel]; - next; + if (! role.inheres_in && role?.relates) { + role.relates.forEach(r => { + if (r.hasName) { + nameMatches(r.hasName).forEach((nm) => { + if (name_match[nm]) { + delete relatedBy[rel]; + } + }) } }); } } root_node.relatedBy=Object.values(relatedBy); const doc = this.promote_node_to_doc(root_node); + if (experts.length) { + doc["is-visible"]=true; // Some expert wants it visible + } await this.update_or_create_main_node_doc(doc); const grant_snippet = this.snippet(root_node); diff --git a/services/base-service/models/index.js b/services/base-service/models/index.js index 8a0358f7..0cb7b684 100644 --- a/services/base-service/models/index.js +++ b/services/base-service/models/index.js @@ -1,11 +1,9 @@ module.exports = { - browse: require('./browse/index.js'), search: require('./search/index.js'), expert: require('./expert/index.js'), work: require('./work/index.js'), authorship: require('./authorship/index.js'), grant: require('./grant/index.js'), -// grant_role: require('./grant_role/index.js'), sitefarm: require('./sitefarm/index.js'), miv: require('./miv/index.js'), schema: require('./schema/index.js'), diff --git a/services/base-service/spa/client/public/elements/components/search-result-row.js b/services/base-service/spa/client/public/elements/components/search-result-row.js index adefb5b3..db011342 100644 --- a/services/base-service/spa/client/public/elements/components/search-result-row.js +++ b/services/base-service/spa/client/public/elements/components/search-result-row.js @@ -14,6 +14,7 @@ export class SearchResultRow extends LitElement { static get properties() { return { result : { type : Object }, + resultType : { type : String, attribute : 'result-type' }, hideCheckbox : { type : Boolean, attribute : 'hide-checkbox' }, hideSearchMatches : { type : Boolean, attribute : 'hide-search-matches' }, }; @@ -24,6 +25,7 @@ export class SearchResultRow extends LitElement { this.render = render.bind(this); this.result = {}; + this.resultType = ''; this.hideCheckbox = false; this.hideSearchMatches = true; // bringing back search matches in next release } diff --git a/services/base-service/spa/client/public/elements/components/search-result-row.tpl.js b/services/base-service/spa/client/public/elements/components/search-result-row.tpl.js index 3320a8aa..f9db4578 100644 --- a/services/base-service/spa/client/public/elements/components/search-result-row.tpl.js +++ b/services/base-service/spa/client/public/elements/components/search-result-row.tpl.js @@ -1,4 +1,5 @@ import { html } from "lit"; +import { unsafeHTML } from 'lit/directives/unsafe-html.js'; export default function render() { return html` @@ -22,11 +23,16 @@ export default function render() { align-items: center; } - .search-result-title ucdlib-icon { + .search-result-title ucdlib-icon.expert { fill: var(--color-aggie-gold); } + + .search-result-title ucdlib-icon.grant { + fill: var(--color-thiebaud-icing); + } + .search-result-title h4 { - margin: 0 0.62rem; + margin: 0 0.62rem 0.5rem; color: var(--ucd-blue-80, #13639E); font-size: 1.43375rem; font-style: normal; @@ -82,14 +88,15 @@ export default function render() {
-
${this.result.subtitle}
+
${unsafeHTML(this.result.subtitle)}
Search matches: ${this.result.numberOfGrants} grants diff --git a/services/base-service/spa/client/public/elements/components/ucdlib-browse-az.js b/services/base-service/spa/client/public/elements/components/ucdlib-browse-az.js index a6ab162e..076691de 100644 --- a/services/base-service/spa/client/public/elements/components/ucdlib-browse-az.js +++ b/services/base-service/spa/client/public/elements/components/ucdlib-browse-az.js @@ -11,6 +11,7 @@ export default class UcdlibBrowseAZ extends Mixin(LitElement) return { url : { type : String }, keySort : { type : String }, + browseType : { type : String }, selectedLetter : { type : String, attribute : 'selected-letter' }, noResult : { type : String, attribute : 'no-result' }, sort : { state : true }, @@ -29,6 +30,7 @@ export default class UcdlibBrowseAZ extends Mixin(LitElement) this.render = render.bind(this); this.alpha = [ + {display: '#', value: '1', exists: true}, {display: 'A', value: 'a', exists: true}, {display: 'B', value: 'b', exists: true}, {display: 'C', value: 'c', exists: true}, @@ -57,10 +59,11 @@ export default class UcdlibBrowseAZ extends Mixin(LitElement) {display: 'Z', value: 'z', exists: true} ]; - this.selectedLetter = 'a'; + this.selectedLetter = ''; + this.browseType = ''; this.sort = this.defaultSort; - this.parseLocation(); + // this.parseLocation(); } async firstUpdated() { @@ -69,14 +72,18 @@ export default class UcdlibBrowseAZ extends Mixin(LitElement) async _onAppStateUpdate(e) { if( e.location.page !== 'browse' ) return; - if( e.location.path.length < 2 ) { - this.selectedLetter = 'a'; + if( e.location.path.length < 3 ) { + this.selectedLetter = ''; } else { this.selectedLetter = e.location.path[2]?.toLowerCase(); } - // to get active filters/a-z - await this.BrowseByModel.browseExpertsAZ(); + this.browseType = e.location.path[1]; + if( this.browseType === 'expert' ) { + this._onBrowseExpertsAzUpdate(await this.BrowseByModel.browseAZBy(this.browseType)); + } else if( this.browseType === 'grant' ) { + this._onBrowseGrantsAzUpdate(await this.BrowseByModel.browseAZBy(this.browseType)); + } this.requestUpdate(); } @@ -85,26 +92,46 @@ export default class UcdlibBrowseAZ extends Mixin(LitElement) if( e.state !== 'loaded' ) return; let az = e.payload || []; + this._updateAz(az); + } + + _onBrowseGrantsAzUpdate(e) { + if( e.state !== 'loaded' ) return; + + let az = e.payload || []; + this._updateAz(az); + } + + _updateAz(az) { az.forEach(item => { // disable if no results for letter let matchedLetter = this.alpha.find(l => l.value.toUpperCase() === item.params?.p.toUpperCase()); if( matchedLetter ) matchedLetter.exists = item.total > 0; }); + + // set letter to first letter with results + if( this.alpha.find(l => l.exists) && !this.selectedLetter ) { + this.selectedLetter = this.alpha.find(l => l.exists).value; + } else if( !this.selectedLetter ) { + this.selectedLetter = this.alpha[0]?.value; + } + + this.AppStateModel.setLocation(`/browse/${this.browseType}/${this.selectedLetter}`); this.requestUpdate(); } - parseLocation() { - let selectedLetter = this.AppStateModel.location.pathname.split('/browse/expert/')?.[1]; - if( !selectedLetter ) return; + // parseLocation() { + // let selectedLetter = this.AppStateModel.location.pathname.split('/browse/expert/')?.[1]; + // if( !selectedLetter ) return; - this.onAlphaInput({ value : selectedLetter }); - } + // this.onAlphaInput({ value : selectedLetter }); + // } onAlphaInput(v) { if( !v || v.value === this.selectedLetter || !v.exists ) return; this.selectedLetter = v.value; - this.AppStateModel.setLocation(`/browse/expert/${this.selectedLetter}`); + this.AppStateModel.setLocation(`/browse/${this.browseType}/${this.selectedLetter}`); } } diff --git a/services/base-service/spa/client/public/elements/fin-app.tpl.js b/services/base-service/spa/client/public/elements/fin-app.tpl.js index c568aaf0..6f5ad717 100644 --- a/services/base-service/spa/client/public/elements/fin-app.tpl.js +++ b/services/base-service/spa/client/public/elements/fin-app.tpl.js @@ -154,7 +154,7 @@ return html` Experts - + Grants diff --git a/services/base-service/spa/client/public/elements/pages/browse/app-browse-by.js b/services/base-service/spa/client/public/elements/pages/browse/app-browse-by.js index 5f001684..d74be16e 100644 --- a/services/base-service/spa/client/public/elements/pages/browse/app-browse-by.js +++ b/services/base-service/spa/client/public/elements/pages/browse/app-browse-by.js @@ -13,7 +13,8 @@ export default class AppBrowseBy extends Mixin(LitElement) static get properties() { return { - id : { type : String }, + browseType : { type : String, attribute : 'browse-type' }, + letter : { type : String }, displayedResults : { type : Array }, resultsPerPage : { type : Number }, currentPage : { type : Number }, @@ -26,7 +27,8 @@ export default class AppBrowseBy extends Mixin(LitElement) super(); this.render = render.bind(this); - this.id = ''; + this.browseType = ''; + this.letter = ''; this.displayedResults = []; this.resultsPerPage = 25; this.currentPage = 1; @@ -60,19 +62,24 @@ export default class AppBrowseBy extends Mixin(LitElement) */ async _onAppStateUpdate(e) { if( e.location.page !== 'browse' ) return; - if( e.location.path.length < 3 ) { - this.AppStateModel.setLocation('/browse/expert/a'); - return; - } - this.id = e.location.path[2]; + this.browseType = e.location.path[1]; + this.letter = e.location.path[2]; + + this.displayedResults = []; + let page = e.location.path[3]; let resultsPerPage = e.location.path[4]; - if( this.id ) { + if( this.letter ) { this.currentPage = parseInt(page) ? page : 1; this.resultsPerPage = parseInt(resultsPerPage) ? resultsPerPage : 25; - this._onBrowseExpertsUpdate(await this.BrowseByModel.browseExperts(this.id, this.currentPage, this.resultsPerPage)); + + if( this.browseType === 'expert' ) { + this._onBrowseExpertsUpdate(await this.BrowseByModel.browseBy('expert', this.letter, this.currentPage, this.resultsPerPage)); + } else if( this.browseType === 'grant' ) { + this._onBrowseGrantsUpdate(await this.BrowseByModel.browseBy('grant', this.letter, this.currentPage, this.resultsPerPage)); + } } } @@ -110,6 +117,39 @@ export default class AppBrowseBy extends Mixin(LitElement) this.paginationTotal = Math.ceil(this.totalResultsCount / this.resultsPerPage); } + /** + * @method _onBrowseGrantsUpdate + * @description bound to BrowseByModel browse-grants-update event + * + * @param {Object} e + * @returns {Promise} + */ + _onBrowseGrantsUpdate(e) { + if( e.state !== 'loaded' ) return; + if( !e.payload?.hits?.length ) { + this.displayedResults = []; + return; + } + + // parse hits + this.displayedResults = (e.payload?.hits || []).map((r, index) => { + let id = r['@id']; + if( Array.isArray(r.name) ) r.name = r.name[0]; + let name = r.name?.split('§')?.shift()?.trim(); + let subtitle = ((r.name?.split('§') || [])[1] || '').trim().replaceAll('•', ''); + + return { + position: index+1, + id: 'grant/'+id, + name, + subtitle + } + }); + + this.totalResultsCount = e.payload.total; + this.paginationTotal = Math.ceil(this.totalResultsCount / this.resultsPerPage); + } + /** * @method _onPaginationChange * @description bound to click events of the pagination element @@ -119,9 +159,10 @@ export default class AppBrowseBy extends Mixin(LitElement) _onPaginationChange(e) { this.currentPage = e.detail.page; - let path = '/browse/expert/' + this.id; + let path = `/browse/${this.browseType}/${this.letter}`; if( this.currentPage > 1 || this.resultsPerPage > 25 ) path += `/${this.currentPage}`; if( this.resultsPerPage > 25 ) path += `/${this.resultsPerPage}`; + this.AppStateModel.setLocation(path); window.scrollTo(0, 0); diff --git a/services/base-service/spa/client/public/elements/pages/browse/app-browse-by.tpl.js b/services/base-service/spa/client/public/elements/pages/browse/app-browse-by.tpl.js index 8132a9b9..e0832301 100644 --- a/services/base-service/spa/client/public/elements/pages/browse/app-browse-by.tpl.js +++ b/services/base-service/spa/client/public/elements/pages/browse/app-browse-by.tpl.js @@ -131,7 +131,7 @@ return html`
-
Experts
+
${this.browseType.charAt(0).toUpperCase() + this.browseType.slice(1)}
@@ -143,11 +143,8 @@ return html`
-
- -
@@ -156,11 +153,17 @@ return html`

-

${this.id.toUpperCase()}

+

${this.letter === '1' ? '#' : this.letter.toUpperCase()}

${this.displayedResults.map( (result) => html` - + +
` )} diff --git a/services/base-service/spa/client/public/elements/pages/browse/app-browse.js b/services/base-service/spa/client/public/elements/pages/browse/app-browse.js index 075e36c8..eab8286b 100644 --- a/services/base-service/spa/client/public/elements/pages/browse/app-browse.js +++ b/services/base-service/spa/client/public/elements/pages/browse/app-browse.js @@ -11,7 +11,8 @@ class AppBrowse extends Mixin(LitElement) static get properties() { return { - page : {type: String}, + browseType : {type: String}, + letter : {type: String}, }; } @@ -20,7 +21,8 @@ class AppBrowse extends Mixin(LitElement) this.render = render.bind(this); this.active = true; - this.page = ''; + this.browseType = ''; + this.letter = ''; this._injectModel('AppStateModel'); } @@ -30,7 +32,10 @@ class AppBrowse extends Mixin(LitElement) } _onAppStateUpdate(e) { - this.page = '/' + e.location.path[0] + '/' + e.location.path[1]; + if( e.location.page !== 'browse' ) return; + + this.browseType = e.location.path[1]; + this.letter = e.location.path[2]; } } diff --git a/services/base-service/spa/client/public/elements/pages/browse/app-browse.tpl.js b/services/base-service/spa/client/public/elements/pages/browse/app-browse.tpl.js index b68de7ad..fcdce493 100644 --- a/services/base-service/spa/client/public/elements/pages/browse/app-browse.tpl.js +++ b/services/base-service/spa/client/public/elements/pages/browse/app-browse.tpl.js @@ -9,10 +9,8 @@ export default function render() { + browse-type="${this.browseType}" + letter="${this.letter}"> - `; } diff --git a/services/base-service/spa/client/public/elements/pages/expert/app-expert-grants-list-edit.tpl.js b/services/base-service/spa/client/public/elements/pages/expert/app-expert-grants-list-edit.tpl.js index 433fb2c0..690b294c 100644 --- a/services/base-service/spa/client/public/elements/pages/expert/app-expert-grants-list-edit.tpl.js +++ b/services/base-service/spa/client/public/elements/pages/expert/app-expert-grants-list-edit.tpl.js @@ -95,15 +95,6 @@ return html` .grant-details .dot { padding: 0 0.25rem; - color: var(--black, #000); - font-family: Proxima Nova; - font-size: 1.1875rem; - font-style: normal; - font-weight: 700; - line-height: 1.92125rem; - text-transform: uppercase; - position: relative; - bottom: 0.25rem; } ucd-theme-pagination { @@ -320,16 +311,8 @@ return html`
${grant.dateTimeInterval?.end?.dateTime?.split('-')?.[0]} - . ${grant.name}
+ ${grant.name}

Error: Cannot format grant. Contact your Aggie Experts administrator.

@@ -369,9 +352,9 @@ return html`
${unsafeHTML(grant.name)}
${grant.start} - ${grant.end} - . + ${grant.role} - . + Awarded by ${grant.awardedBy}
@@ -401,9 +384,9 @@ return html`
${unsafeHTML(grant.name)}
${grant.start} - ${grant.end} - . + ${grant.role} - . + Awarded by ${grant.awardedBy}
diff --git a/services/base-service/spa/client/public/elements/pages/expert/app-expert-grants-list.tpl.js b/services/base-service/spa/client/public/elements/pages/expert/app-expert-grants-list.tpl.js index 17d4f73b..cf8c4fe8 100644 --- a/services/base-service/spa/client/public/elements/pages/expert/app-expert-grants-list.tpl.js +++ b/services/base-service/spa/client/public/elements/pages/expert/app-expert-grants-list.tpl.js @@ -92,15 +92,6 @@ return html` .grant-details .dot { padding: 0 0.25rem; - color: var(--black, #000); - font-family: Proxima Nova; - font-size: 1.1875rem; - font-style: normal; - font-weight: 700; - line-height: 1.92125rem; - text-transform: uppercase; - position: relative; - bottom: 0.25rem; } .grant-item ucdlib-icon { @@ -157,9 +148,9 @@ return html`
${unsafeHTML(grant.name)}
${grant.start} - ${grant.end} - . + ${grant.role} - . + Awarded by ${grant.awardedBy}
@@ -177,9 +168,9 @@ return html`
${unsafeHTML(grant.name)}
${grant.start} - ${grant.end} - . + ${grant.role} - . + Awarded by ${grant.awardedBy}
diff --git a/services/base-service/spa/client/public/elements/pages/expert/app-expert-works-list-edit.tpl.js b/services/base-service/spa/client/public/elements/pages/expert/app-expert-works-list-edit.tpl.js index f4960163..0403a32e 100644 --- a/services/base-service/spa/client/public/elements/pages/expert/app-expert-works-list-edit.tpl.js +++ b/services/base-service/spa/client/public/elements/pages/expert/app-expert-works-list-edit.tpl.js @@ -99,15 +99,6 @@ return html` .work-details .dot { padding: 0 0.25rem; - color: var(--black, #000); - font-family: Proxima Nova; - font-size: 1.1875rem; - font-style: normal; - font-weight: 700; - line-height: 1.92125rem; - text-transform: uppercase; - position: relative; - bottom: 0.25rem; } ucd-theme-pagination { @@ -348,17 +339,9 @@ return html` (work, index) => html`
-
${work.issued?.split?.('-')?.[0] || 'Date Unknown'} - . ${work.title}
+
${work.issued.split('-')?.[0]} + ${work.title}

Error: Cannot format citation. Contact your Aggie Experts administrator.

@@ -403,7 +386,7 @@ return html`
${unsafeHTML(cite.title || cite['container-title'])}
${utils.getCitationType(cite.type)} - . + ${unsafeHTML(cite.apa?.replace('(n.d.). ', '')?.replace('(n.d.).', '') || 'Cannot format citation. Contact your Aggie Experts administrator.')}
diff --git a/services/base-service/spa/client/public/elements/pages/expert/app-expert-works-list.tpl.js b/services/base-service/spa/client/public/elements/pages/expert/app-expert-works-list.tpl.js index 4a74076a..ce9fb071 100644 --- a/services/base-service/spa/client/public/elements/pages/expert/app-expert-works-list.tpl.js +++ b/services/base-service/spa/client/public/elements/pages/expert/app-expert-works-list.tpl.js @@ -86,15 +86,6 @@ return html` .work-details .dot { padding: 0 0.25rem; - color: var(--black, #000); - font-family: Proxima Nova; - font-size: 1.1875rem; - font-style: normal; - font-weight: 700; - line-height: 1.92125rem; - text-transform: uppercase; - position: relative; - bottom: 0.25rem; } .work-item ucdlib-icon { @@ -147,7 +138,7 @@ return html`
${unsafeHTML(cite.title || cite['container-title'])}
${utils.getCitationType(cite.type)} - . + ${unsafeHTML(cite.apa?.replace('(n.d.). ', '')?.replace('(n.d.).', '') || 'Cannot format citation. Contact your Aggie Experts administrator.')}
diff --git a/services/base-service/spa/client/public/elements/pages/expert/app-expert.tpl.js b/services/base-service/spa/client/public/elements/pages/expert/app-expert.tpl.js index 0c59e1a2..a6d5b8c0 100644 --- a/services/base-service/spa/client/public/elements/pages/expert/app-expert.tpl.js +++ b/services/base-service/spa/client/public/elements/pages/expert/app-expert.tpl.js @@ -315,15 +315,6 @@ return html` .grant-details .dot, .work-details .dot { padding: 0 0.25rem; - color: var(--black, #000); - font-family: Proxima Nova; - font-size: 1.1875rem; - font-style: normal; - font-weight: 700; - line-height: 1.92125rem; - text-transform: uppercase; - position: relative; - bottom: 0.25rem; } .grants-abbreviated .grants-heading .grants-edit-download ucdlib-icon, @@ -720,11 +711,11 @@ return html`
Open to: Collaborative Projects - . + Community Partnerships - . + Industry Projects - . + Media Interviews @@ -924,9 +915,9 @@ return html`
${unsafeHTML(grant.name)}
${grant.start} - ${grant.end} - . + ${grant.role} - . + Awarded by ${grant.awardedBy}
@@ -940,9 +931,9 @@ return html`
${unsafeHTML(grant.name)}
${grant.start} - ${grant.end} - . + ${grant.role} - . + Awarded by ${grant.awardedBy}
@@ -992,7 +983,7 @@ return html`
${unsafeHTML(cite.title || cite['container-title'])}
${utils.getCitationType(cite.type)} - . + ${unsafeHTML(cite.apa?.replace('(n.d.). ', '')?.replace('(n.d.).', '') || 'Cannot format citation. Contact your Aggie Experts administrator.')}
diff --git a/services/base-service/spa/client/public/elements/pages/grant/app-grant.js b/services/base-service/spa/client/public/elements/pages/grant/app-grant.js index 632599f0..83f4b11a 100644 --- a/services/base-service/spa/client/public/elements/pages/grant/app-grant.js +++ b/services/base-service/spa/client/public/elements/pages/grant/app-grant.js @@ -110,7 +110,7 @@ export default class AppGrant extends Mixin(LitElement) let aeContributors = (e.payload['@graph'] || []).filter(g => g['@id'] !== this.grantId) || []; let otherContributors = []; - this.grantName = grantGraph.name || ''; + this.grantName = grantGraph.name?.split('§')?.shift()?.trim() || ''; this.awardedBy = grantGraph.assignedBy?.name || ''; this.grantNumber = grantGraph.sponsorAwardId || ''; this.grantAdmin = grantGraph.assignedBy?.name || ''; diff --git a/services/base-service/spa/client/public/elements/pages/search/app-search.js b/services/base-service/spa/client/public/elements/pages/search/app-search.js index a3e7fd00..a48eb16b 100644 --- a/services/base-service/spa/client/public/elements/pages/search/app-search.js +++ b/services/base-service/spa/client/public/elements/pages/search/app-search.js @@ -229,6 +229,7 @@ export default class AppSearch extends Mixin(LitElement) let numberOfGrants = (r['_inner_hits']?.filter(h => h['@type']?.includes('Grant')) || []).length; return { + resultType: 'expert', position: index+1, id, name, diff --git a/services/base-service/spa/client/public/elements/pages/search/app-search.tpl.js b/services/base-service/spa/client/public/elements/pages/search/app-search.tpl.js index ce84e011..7e3aac25 100644 --- a/services/base-service/spa/client/public/elements/pages/search/app-search.tpl.js +++ b/services/base-service/spa/client/public/elements/pages/search/app-search.tpl.js @@ -430,7 +430,11 @@ return html` ${this.displayedResults.map( (result) => html` - + +
` )} diff --git a/services/base-service/spa/client/public/lib/models/BrowseByModel.js b/services/base-service/spa/client/public/lib/models/BrowseByModel.js index 58224d15..178ade40 100644 --- a/services/base-service/spa/client/public/lib/models/BrowseByModel.js +++ b/services/base-service/spa/client/public/lib/models/BrowseByModel.js @@ -13,27 +13,28 @@ class BrowseByModel extends BaseModel { } /** - * @method browseExpertsAZ + * @method browseAZBy * @description search elastic search for available experts * * @returns {Promise} resolves to experts results per letter (last name) */ - async browseExpertsAZ() { - return this.service.browseExpertsAZ(); + async browseAZBy(type='expert') { + return this.service.browseAZBy(type); } /** - * @method browseExperts + * @method browseBy * @description search elastic search for expert * + * @param {String} type search type, defaults to 'expert' * @param {String} lastInitial search letter, last name of expert * @param {Number} page page number, defaults to 1 * @param {Number} size number of results per page, defaults to 25 * * @returns {Promise} resolves to experts results */ - async browseExperts(lastInitial, page=1, size=25) { - return this.service.browseExperts(lastInitial, page, size); + async browseBy(type='expert', lastInitial, page=1, size=25) { + return this.service.browseBy(type, lastInitial, page, size); } } diff --git a/services/base-service/spa/client/public/lib/payload.js b/services/base-service/spa/client/public/lib/payload.js index 856f9410..3fbb08aa 100644 --- a/services/base-service/spa/client/public/lib/payload.js +++ b/services/base-service/spa/client/public/lib/payload.js @@ -1,6 +1,6 @@ import {PayloadUtils} from '@ucd-lib/cork-app-utils' -const ID_ORDER = ['lastInitial', 'page', 'size', 'browseExperts', 'expertId', 'subpage', 'search', 'grantId']; +const ID_ORDER = ['lastInitial', 'page', 'size', 'browseExperts', 'browseGrants', 'expertId', 'subpage', 'search', 'grantId', 'browseType']; let inst = new PayloadUtils({ idParts: ID_ORDER, diff --git a/services/base-service/spa/client/public/lib/services/BrowseByService.js b/services/base-service/spa/client/public/lib/services/BrowseByService.js index a3f42212..8b7755fb 100644 --- a/services/base-service/spa/client/public/lib/services/BrowseByService.js +++ b/services/base-service/spa/client/public/lib/services/BrowseByService.js @@ -8,40 +8,48 @@ class BrowseByService extends BaseService { super(); this.store = BrowseByStore; - this.baseUrl = '/api/browse'; + this.baseUrl = '/api'; } - async browseExpertsAZ() { - let id = 'browseExperts:az'; - let ido = { browseExperts : 'az' }; + async browseAZBy(type) { + let url = `${this.baseUrl}/${type}/browse`; + type = type.substring(0, 1).toUpperCase() + type.substring(1); + let storeKey = 'by'+type+'sAZ'; + + let id = 'browse'+type+'s:az'; + let ido = {}; + ido['browse'+type+'s'] = 'az'; await this.request({ - url : this.baseUrl, - checkCached : () => this.store.data.byExpertsAZ.get(id), + url, + checkCached : () => this.store.data[storeKey].get(id), onUpdate : resp => this.store.set( payloadUtils.generate(ido, resp), - this.store.data.byExpertsAZ + this.store.data[storeKey] ) }); - return this.store.data.byExpertsAZ.get(id); + return this.store.data[storeKey].get(id); } - async browseExperts(lastInitial, page=1, size=25) { - let ido = {lastInitial, page, size}; + async browseBy(type, lastInitial, page=1, size=25) { + let ido = {browseType: type, lastInitial, page, size}; let id = payloadUtils.getKey(ido); + type = type.substring(0, 1).toUpperCase() + type.substring(1); + let storeKey = 'by'+type+'sLastInitial'; + await this.request({ - url : this.baseUrl, + url : `${this.baseUrl}/${type}/browse`, qs : { page, size, p : lastInitial.toUpperCase() }, - checkCached : () => this.store.data.byExpertsLastInitial.get(id), + checkCached : () => this.store.data[storeKey].get(id), onUpdate : resp => this.store.set( payloadUtils.generate(ido, resp), - this.store.data.byExpertsLastInitial + this.store.data[storeKey] ) }); - return this.store.data.byExpertsLastInitial.get(id); + return this.store.data[storeKey].get(id); } } diff --git a/services/base-service/spa/client/public/lib/stores/BrowseByStore.js b/services/base-service/spa/client/public/lib/stores/BrowseByStore.js index db0cb566..0d491bcb 100644 --- a/services/base-service/spa/client/public/lib/stores/BrowseByStore.js +++ b/services/base-service/spa/client/public/lib/stores/BrowseByStore.js @@ -6,14 +6,13 @@ class BrowseByStore extends BaseStore { super(); this.data = { + byExpertsAZ : new LruStore({name: 'browse.experts.az'}), + byGrantsAZ : new LruStore({name: 'browse.grants.az'}), byExpertsLastInitial : new LruStore({name: 'browse.experts'}), - byExpertsAZ : new LruStore({name: 'browse.experts.az'}) + byGrantsLastInitial : new LruStore({name: 'browse.grants'}), } - this.events = { - BROWSE_EXPERTS_UPDATE : 'browse-experts-update', - BROWSE_EXPERTS_AZ_UPDATE : 'browse-experts-az-update' - } + this.events = {}; } }