From f000068a44d9715db18cd377b6c3fd564ef36e14 Mon Sep 17 00:00:00 2001 From: Martin Ledvinka Date: Thu, 17 Oct 2024 10:18:56 +0200 Subject: [PATCH] [Enhancement #544] Simplify FTS results rendering. Do not load full term for each result, support all newly added fields. --- src/component/search/label/SearchResults.tsx | 2 +- src/component/search/label/TermResultItem.tsx | 177 +++++------------- .../search/label/VocabularyResultItem.tsx | 135 +++++-------- .../label/__tests__/SearchResults.test.tsx | 15 +- src/model/search/SearchResult.ts | 4 + 5 files changed, 102 insertions(+), 231 deletions(-) diff --git a/src/component/search/label/SearchResults.tsx b/src/component/search/label/SearchResults.tsx index 31271910..ebecf583 100644 --- a/src/component/search/label/SearchResults.tsx +++ b/src/component/search/label/SearchResults.tsx @@ -51,7 +51,7 @@ export function mergeDuplicates( // If the match field is the same there is no need to update other attributes, as the match is already // marked in the snippet of the existing item if (existing.snippetField !== r.snippetField) { - if (r.snippetField === "label") { + if (r.snippetField === "prefLabel") { // Render label match first existing.snippets.unshift(r.snippetText); existing.snippetFields.unshift(r.snippetField); diff --git a/src/component/search/label/TermResultItem.tsx b/src/component/search/label/TermResultItem.tsx index ada31f8e..8c8f5181 100644 --- a/src/component/search/label/TermResultItem.tsx +++ b/src/component/search/label/TermResultItem.tsx @@ -1,12 +1,7 @@ import * as React from "react"; -import { injectIntl } from "react-intl"; -import withI18n, { HasI18n } from "../../hoc/withI18n"; -import VocabularyUtils, { IRI } from "../../../util/VocabularyUtils"; import AssetLink from "../../misc/AssetLink"; import Term from "../../../model/Term"; -import { connect } from "react-redux"; -import { ThunkDispatch } from "../../../util/Types"; -import { loadTermByIri } from "../../../action/AsyncActions"; +import { useSelector } from "react-redux"; import { SearchResultItem } from "./SearchResults"; import AssetLabel from "../../misc/AssetLabel"; import AssetFactory from "../../../util/AssetFactory"; @@ -14,137 +9,55 @@ import TermItState from "../../../model/TermItState"; import FTSMatch from "./FTSMatch"; import TermBadge from "../../badge/TermBadge"; import { getTermPath } from "../../term/TermLink"; -import User from "../../../model/User"; -import { getLocalized } from "../../../model/MultilingualString"; -import { getShortLocale } from "../../../util/IntlUtil"; import TermStateBadge from "../../term/state/TermStateBadge"; +import { useI18n } from "../../hook/useI18n"; +import { getResultDescription } from "./VocabularyResultItem"; -interface TermResultItemOwnProps { +interface TermResultItemProps { result: SearchResultItem; } -interface TermResultItemDispatchProps { - loadTerm: (termIri: IRI) => Promise; -} - -interface TermResultItemStateProps { - user: User; -} - -interface TermResultItemProps - extends TermResultItemOwnProps, - TermResultItemDispatchProps, - TermResultItemStateProps, - HasI18n {} - -interface TermResultItemState { - text: string | undefined; -} - -export class TermResultItem extends React.Component< - TermResultItemProps, - TermResultItemState -> { - constructor(props: TermResultItemProps) { - super(props); - this.state = { - text: undefined, - }; - } +const TermResultItem: React.FC = ({ result }) => { + const { i18n } = useI18n(); + const user = useSelector((state: TermItState) => state.user); - public componentDidMount(): void { - const indexOfDefinition = this.getIndexOf("definition"); - if (indexOfDefinition < 0) { - const iri = VocabularyUtils.create(this.props.result.iri); - this.props.loadTerm(iri).then((term) => { - if (term) { - this.setState({ - text: term!.definition - ? getLocalized( - term!.definition, - getShortLocale(this.props.locale) - ) - : getLocalized( - term!.scopeNote, - getShortLocale(this.props.locale) - ), - }); - } - }); - } - } - - private getIndexOf(field: string) { - return this.props.result.snippetFields.indexOf(field); - } - - public render() { - const i18n = this.props.i18n; - const result = this.props.result; - const t = { - iri: result.iri, - label: ( - <> - {result.label} -   - {this.props.result.vocabulary ? ( - <> - {i18n("search.results.vocabulary.from")}  - - - ) : ( - <> - )} - - ), - }; - - let text; - if (this.getIndexOf("definition") > -1) { - text = result.snippets[this.getIndexOf("definition")]; - } else { - text = this.state.text; - } - - if (text && text!.length > 200) { - text = text!.substring(0, 200) + " ..."; - } - - const asset = AssetFactory.createAsset(result); - return ( + const t = { + iri: result.iri, + label: ( <> - - - -
- - {this.getIndexOf("definition") > -1 ? ( - - ) : ( - text - )} - + {result.label} +   + {result.vocabulary ? ( + <> + {i18n("search.results.vocabulary.from")}  + + + ) : ( + <> + )} - ); - } -} - -export default connect< - TermResultItemStateProps, - TermResultItemDispatchProps, - TermResultItemOwnProps, - TermItState ->( - (state: TermItState) => { - return { user: state.user }; - }, - (dispatch: ThunkDispatch) => { - return { - loadTerm: (termIri: IRI) => dispatch(loadTermByIri(termIri)), - }; - } -)(injectIntl(withI18n(TermResultItem))); + ), + }; + const fields = ["definition", "scopeNote"]; + + const description = getResultDescription(result, fields); + + const asset = AssetFactory.createAsset(result); + return ( + <> + + + +
+ + + + + ); +}; + +export default TermResultItem; diff --git a/src/component/search/label/VocabularyResultItem.tsx b/src/component/search/label/VocabularyResultItem.tsx index 285791e7..26cbff1e 100644 --- a/src/component/search/label/VocabularyResultItem.tsx +++ b/src/component/search/label/VocabularyResultItem.tsx @@ -1,110 +1,65 @@ import * as React from "react"; -import { injectIntl } from "react-intl"; -import withI18n, { HasI18n } from "../../hoc/withI18n"; -import VocabularyUtils, { IRI } from "../../../util/VocabularyUtils"; -import { connect } from "react-redux"; -import { ThunkDispatch } from "../../../util/Types"; import { SearchResultItem } from "./SearchResults"; -import TermItState from "../../../model/TermItState"; import FTSMatch from "./FTSMatch"; -import { getRdfsResource } from "../../../action/AsyncActions"; -import RdfsResource from "../../../model/RdfsResource"; import Vocabulary from "../../../model/Vocabulary"; import VocabularyLink from "../../vocabulary/VocabularyLink"; import AssetFactory from "../../../util/AssetFactory"; import VocabularyBadge from "../../badge/VocabularyBadge"; -import { getLocalized } from "../../../model/MultilingualString"; -import { getShortLocale } from "../../../util/IntlUtil"; -interface VocabularyResultItemOwnProps { +interface VocabularyResultItemProps { result: SearchResultItem; } -interface VocabularyResultItemDispatchProps { - getResource: (iri: IRI) => Promise; +export function getSnippetFieldIndex( + result: SearchResultItem, + fieldName: string +) { + return result.snippetFields.indexOf(fieldName); } -interface VocabularyResultItemStateProps {} - -interface VocabularyResultItemProps - extends VocabularyResultItemOwnProps, - VocabularyResultItemDispatchProps, - VocabularyResultItemStateProps, - HasI18n {} - -interface VocabularyResultItemState { - comment?: string | null; -} - -export class VocabularyResultItem extends React.Component< - VocabularyResultItemProps, - VocabularyResultItemState -> { - constructor(props: VocabularyResultItemProps) { - super(props); - this.state = { - comment: null, - }; - } - - public componentDidMount(): void { - const indexOfComment = this.getIndexOf("comment"); - if (indexOfComment < 0) { - const iri = VocabularyUtils.create(this.props.result.iri); - this.props.getResource(iri).then((resource) => { - if (resource) { - this.setState({ - comment: getLocalized( - resource.comment, - getShortLocale(this.props.locale) - ), - }); - } - }); +export function getResultDescription( + result: SearchResultItem, + fieldNames: string[] +) { + let snippetFieldIndex = -1; + for (let i = 0; i < fieldNames.length; i++) { + snippetFieldIndex = getSnippetFieldIndex(result, fieldNames[i]); + if (snippetFieldIndex >= 0) { + break; } } - - private getIndexOf(field: string) { - return this.props.result.snippetFields.indexOf(field); + let text; + if (snippetFieldIndex >= 0) { + text = result.snippets[snippetFieldIndex]; + } else { + text = result.description; } - public render() { - let text; - if (this.getIndexOf("comment") > -1) { - text = this.props.result.snippets[this.getIndexOf("comment")]; - } else { - text = this.state.comment || ""; - } - - if (text && text!.length > 200) { - text = text!.substring(0, 200) + " ..."; - } - - const res = this.props.result; - return ( - <> - - - - -
- - - - - ); + if (text && text!.length > 200) { + text = text!.substring(0, 200) + " ..."; } + return text || ""; } -export default connect< - VocabularyResultItemStateProps, - VocabularyResultItemDispatchProps, - VocabularyResultItemOwnProps, - TermItState ->(undefined, (dispatch: ThunkDispatch) => { - return { - getResource: (iri: IRI) => dispatch(getRdfsResource(iri)), - }; -})(injectIntl(withI18n(VocabularyResultItem))); +const VocabularyResultItem: React.FC = ({ + result, +}) => { + const description = getResultDescription(result, ["description"]); + + return ( + <> + + + + +
+ + + + + ); +}; + +export default VocabularyResultItem; diff --git a/src/component/search/label/__tests__/SearchResults.test.tsx b/src/component/search/label/__tests__/SearchResults.test.tsx index 557467f6..bf76baa0 100644 --- a/src/component/search/label/__tests__/SearchResults.test.tsx +++ b/src/component/search/label/__tests__/SearchResults.test.tsx @@ -131,7 +131,7 @@ describe("SearchResults", () => { iri, label: "Test", snippetText: "Match and another match", - snippetField: "comment", + snippetField: "description", vocabulary: { iri: vocabularyIri }, types: [VocabularyUtils.VOCABULARY], }), @@ -139,7 +139,7 @@ describe("SearchResults", () => { iri, label: "Test", snippetText: "Match and another match", - snippetField: "comment", + snippetField: "description", vocabulary: { iri: vocabularyIri }, types: [VocabularyUtils.VOCABULARY], }), @@ -162,22 +162,21 @@ describe("SearchResults", () => { it("merges matches of multiple fields of one asset into one result row", () => { const iri = Generator.generateUri(); - const vocabularyIri = Generator.generateUri(); const results = [ new SearchResult({ iri, label: "Test", snippetText: "Match in label", - snippetField: "label", - vocabulary: { iri: vocabularyIri }, + description: "Vocabulary description", + snippetField: "title", types: [VocabularyUtils.VOCABULARY], }), new SearchResult({ iri, label: "Test", - snippetText: "Match in comment", - snippetField: "comment", - vocabulary: { iri: vocabularyIri }, + description: "Vocabulary description", + snippetText: "Match in description", + snippetField: "description", types: [VocabularyUtils.VOCABULARY], }), ]; diff --git a/src/model/search/SearchResult.ts b/src/model/search/SearchResult.ts index 302e6ce8..34e0245b 100644 --- a/src/model/search/SearchResult.ts +++ b/src/model/search/SearchResult.ts @@ -6,6 +6,7 @@ import Utils from "../../util/Utils"; export const CONTEXT = { iri: "@id", label: VocabularyUtils.RDFS_LABEL, + description: VocabularyUtils.DC_DESCRIPTION, vocabulary: VocabularyUtils.IS_TERM_FROM_VOCABULARY, state: VocabularyUtils.HAS_TERM_STATE, snippetText: @@ -19,6 +20,7 @@ export const CONTEXT = { export interface SearchResultData extends AssetData { iri: string; label: string; + description?: string; snippetText: string; snippetField: string; score?: number; @@ -30,6 +32,7 @@ export interface SearchResultData extends AssetData { export default class SearchResult implements AssetData { public readonly iri: string; public readonly label: string; + public readonly description?: string; public readonly snippetText: string; public readonly snippetField: string; public readonly score?: number; @@ -40,6 +43,7 @@ export default class SearchResult implements AssetData { constructor(data: SearchResultData) { this.iri = data.iri; this.label = data.label; + this.description = data.description; this.snippetField = data.snippetField; this.snippetText = data.snippetText; this.score = data.score;