Skip to content

Commit

Permalink
Merge pull request #448 from kbss-cvut/enhancement#291-multilingual-v…
Browse files Browse the repository at this point in the history
…ocabulary-attributes

Enhancement#291 multilingual vocabulary attributes
  • Loading branch information
ledsoft authored Feb 27, 2024
2 parents 1d5e1f1 + 900ac53 commit d9e0ad4
Show file tree
Hide file tree
Showing 36 changed files with 993 additions and 730 deletions.
5 changes: 4 additions & 1 deletion src/__tests__/environment/Generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,10 @@ export default class Generator {
public static generateVocabulary(seed: Partial<VocabularyData> = {}) {
return new Vocabulary(
Object.assign(
this.generateAssetData("Vocabulary " + this.randomInt(0, 10000)),
{
iri: Generator.generateUri(),
label: langString("Vocabulary " + this.randomInt(0, 10000)),
},
seed
)
);
Expand Down
5 changes: 1 addition & 4 deletions src/component/administration/user/ManagedAssets.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import React from "react";
import RdfsResource from "../../../model/RdfsResource";
import { useI18n } from "../../hook/useI18n";
import { Alert } from "reactstrap";
import { getLocalized } from "../../../model/MultilingualString";
import VocabularyLink from "../../vocabulary/VocabularyLink";
import Vocabulary from "../../../model/Vocabulary";

Expand All @@ -20,9 +19,7 @@ const ManagedAssets: React.FC<{ managedAssets: RdfsResource[] }> = ({
{managedAssets.map((a) => (
<li key={a.iri}>
<VocabularyLink
vocabulary={
new Vocabulary({ iri: a.iri, label: getLocalized(a.label) })
}
vocabulary={new Vocabulary({ iri: a.iri, label: a.label! })}
className="alert-link"
/>
</li>
Expand Down
63 changes: 0 additions & 63 deletions src/component/asset/AbstractCreateAsset.ts

This file was deleted.

18 changes: 18 additions & 0 deletions src/component/asset/CreateAssetUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import Ajax, { params } from "../../util/Ajax";
import Constants from "../../util/Constants";
import last from "last";

/**
* Calls backend to generate a new identifier for an asset specified by the parameters.
* @param parameters Identifier generation parameters
*/
let loadIdentifier = <T extends { name: string; assetType: string }>(
parameters: T
) => {
return Ajax.post(`${Constants.API_PREFIX}/identifiers`, params(parameters));
};

// This will cause the existing still running identifier requests to be ignored in favor of the most recent call
loadIdentifier = last(loadIdentifier);

export { loadIdentifier };
11 changes: 7 additions & 4 deletions src/component/misc/__tests__/AssetLink.test.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
import AssetLink from "../AssetLink";
import { EMPTY_VOCABULARY } from "../../../model/Vocabulary";
import { MemoryRouter } from "react-router";
import { mountWithIntl } from "../../../__tests__/environment/Environment";
import OutgoingLink from "../OutgoingLink";
import Generator from "../../../__tests__/environment/Generator";

describe("Asset Link", () => {
const voc = EMPTY_VOCABULARY;
const asset = {
iri: Generator.generateUri(),
label: "Test label",
};

function mount() {
return mountWithIntl(
<MemoryRouter>
<AssetLink asset={voc} path={"/vocabulary"} />
<AssetLink asset={asset} path={"/vocabulary"} />
</MemoryRouter>
);
}
Expand All @@ -21,7 +24,7 @@ describe("Asset Link", () => {
});
it("Render outgoing link", () => {
const wrapper = mount();
expect(wrapper.find('a[href="http://empty"]').exists()).toBeTruthy();
expect(wrapper.find(`a[href="${asset.iri}"]`).exists()).toBeTruthy();
});
it("showLink is false by default", () => {
const wrapper = mount();
Expand Down
8 changes: 3 additions & 5 deletions src/component/multilingual/EditLanguageSelector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import classNames from "classnames";
import { IntelligentTreeSelect } from "intelligent-tree-select";
import Constants from "../../util/Constants";
import { getShortLocale } from "../../util/IntlUtil";
import Term, { TermData } from "../../model/Term";
import { renderLanguages } from "./LanguageSelector";
import { Nav, NavItem, NavLink } from "reactstrap";
import { FaPlusCircle } from "react-icons/fa";
Expand All @@ -14,7 +13,7 @@ import "./LanguageSelector.scss";

interface EditLanguageSelectorProps {
language: string;
term: Term | TermData;
existingLanguages: string[];
onSelect: (lang: string) => void;
onRemove: (lang: string) => void;
}
Expand Down Expand Up @@ -43,13 +42,12 @@ const OPTIONS = prioritizeLanguages(
);

const EditLanguageSelector: React.FC<EditLanguageSelectorProps> = (props) => {
const { language, term, onSelect, onRemove } = props;
const { language, existingLanguages, onSelect, onRemove } = props;
const { i18n, formatMessage } = useI18n();
const [adding, setAdding] = React.useState(false);
React.useEffect(() => {
setAdding(false);
}, [language]);
const existingLanguages = Term.getLanguages(term);
if (existingLanguages.indexOf(language) === -1) {
existingLanguages.push(language);
}
Expand All @@ -62,7 +60,7 @@ const EditLanguageSelector: React.FC<EditLanguageSelectorProps> = (props) => {
return (
<div>
<Nav
id="term-edit-language-selector"
id="edit-language-selector"
tabs={true}
className="language-selector-nav"
>
Expand Down
9 changes: 2 additions & 7 deletions src/component/multilingual/LanguageSelector.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
import * as React from "react";
import ISO6391 from "iso-639-1";
import Term from "../../model/Term";
import { Nav, NavItem, NavLink } from "reactstrap";
import { FaTimesCircle } from "react-icons/fa";
import { useI18n } from "../hook/useI18n";
import "./LanguageSelector.scss";

interface LanguageSelectorProps {
term: Term | null;
languages: string[];
language: string;
onSelect: (lang: string) => void;
}
Expand Down Expand Up @@ -54,12 +53,8 @@ export function renderLanguages(
}

const LanguageSelector: React.FC<LanguageSelectorProps> = (props) => {
const { term, language, onSelect } = props;
const { language, languages, onSelect } = props;
const { formatMessage } = useI18n();
if (!term) {
return null;
}
const languages = Term.getLanguages(term);
if (languages.length <= 1) {
return null;
}
Expand Down
66 changes: 6 additions & 60 deletions src/component/multilingual/__tests__/LanguageSelector.test.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
import Term from "../../../model/Term";
import Generator from "../../../__tests__/environment/Generator";
import VocabularyUtils from "../../../util/VocabularyUtils";
import { mountWithIntl } from "../../../__tests__/environment/Environment";
import LanguageSelector, { renderLanguages } from "../LanguageSelector";
import { intlFunctions } from "../../../__tests__/environment/IntlUtil";
Expand All @@ -16,40 +13,26 @@ describe("LanguageSelector", () => {
});

it("renders list of languages extracted from multilingual attributes of specified term", () => {
const term = new Term({
iri: Generator.generateUri(),
label: {
en: "Building",
cs: "Budova",
de: "Bauwerk, das",
},
types: [VocabularyUtils.TERM],
});
const languages = ["cs", "de", "en"];
const result = mountWithIntl(
<LanguageSelector
term={term}
onSelect={onSelect}
language={Constants.DEFAULT_LANGUAGE}
languages={languages}
{...intlFunctions()}
/>
);
const items = result.find(NavItem);
expect(items.length).toEqual(Object.getOwnPropertyNames(term.label).length);
expect(items.length).toEqual(languages.length);
});

it("renders value in selected language as active nav item", () => {
const label = { cs: "Budova" };
label[Constants.DEFAULT_LANGUAGE] = "Building";
const term = new Term({
iri: Generator.generateUri(),
label,
types: [VocabularyUtils.TERM],
});
const languages = ["cs", Constants.DEFAULT_LANGUAGE];
const result = mountWithIntl(
<LanguageSelector
term={term}
onSelect={onSelect}
language={Constants.DEFAULT_LANGUAGE}
languages={languages}
{...intlFunctions()}
/>
);
Expand All @@ -60,54 +43,17 @@ describe("LanguageSelector", () => {
});

it("renders nothing when there are no alternative translations", () => {
const term = new Term({
iri: Generator.generateUri(),
label: {
en: "Building",
},
types: [VocabularyUtils.TERM],
});
const result = mountWithIntl(
<LanguageSelector
term={term}
onSelect={onSelect}
language={Constants.DEFAULT_LANGUAGE}
languages={[Constants.DEFAULT_LANGUAGE]}
{...intlFunctions()}
/>
);
expect(result.exists("#term-language-selector")).toBeFalsy();
});

it("handles plural multilingual attributes when determining languages to show", () => {
const term = new Term({
iri: Generator.generateUri(),
label: {
en: "Building",
},
definition: {
en: "Building is a bunch of concrete with windows and doors.",
},
altLabels: { en: ["Construction"], cs: ["Stavba"] },
types: [VocabularyUtils.TERM],
});
const result = mountWithIntl(
<LanguageSelector
term={term}
onSelect={onSelect}
language={Constants.DEFAULT_LANGUAGE}
{...intlFunctions()}
/>
);
const items = result.find(NavItem);
expect(items.length).toEqual(2);
const texts = items.map((i) => i.text());
["cs", "en"].forEach((lang) =>
expect(
texts.find((t) => t.indexOf(ISO6391.getNativeName(lang)) !== -1)
).toBeDefined()
);
});

describe("language removal", () => {
let onRemove: (lang: string) => void;

Expand Down
2 changes: 1 addition & 1 deletion src/component/public/term/TermMetadata.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ const TermMetadata: React.FC<TermMetadataProps> = (props) => {
<>
<LanguageSelector
key="term-language-selector"
term={term}
language={language}
languages={Term.getLanguages(term)}
onSelect={setLanguage}
/>
<Row>
Expand Down
21 changes: 17 additions & 4 deletions src/component/public/vocabulary/VocabularySummary.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,24 @@ import MarkdownView from "../../misc/MarkdownView";
import { useLocation, useRouteMatch } from "react-router-dom";
import PromiseTrackingMask from "../../misc/PromiseTrackingMask";
import { trackPromise } from "react-promise-tracker";
import {
getLocalized,
getLocalizedOrDefault,
} from "../../../model/MultilingualString";
import { resolveInitialLanguage } from "../../vocabulary/VocabularySummary";

export const VocabularySummary: React.FC = () => {
const { i18n } = useI18n();
const { i18n, locale } = useI18n();
const dispatch: ThunkDispatch = useDispatch();
const location = useLocation();
const match = useRouteMatch<{ name: string }>();
const vocabulary = useSelector((state: TermItState) => state.vocabulary);
const configuration = useSelector(
(state: TermItState) => state.configuration
);
const [language, setLanguage] = React.useState(
resolveInitialLanguage(vocabulary, locale, configuration.language)
);

React.useEffect(() => {
dispatch(selectVocabularyTerm(null));
Expand All @@ -48,11 +56,16 @@ export const VocabularySummary: React.FC = () => {
);
}
}, [vocabulary, configuration, location, match, dispatch]);
React.useEffect(() => {
setLanguage(
resolveInitialLanguage(vocabulary, locale, configuration.language)
);
}, [vocabulary, locale, configuration, setLanguage]);

return (
<div id="public-vocabulary-detail">
<WindowTitle
title={`${vocabulary.label} | ${i18n(
title={`${getLocalized(vocabulary.label, language)} | ${i18n(
"vocabulary.management.vocabularies"
)}`}
/>
Expand All @@ -61,7 +74,7 @@ export const VocabularySummary: React.FC = () => {
id="public-vocabulary-summary"
title={
<>
{vocabulary.label}
{getLocalized(vocabulary.label, language)}
<CopyIriIcon url={vocabulary.iri as string} />
</>
}
Expand All @@ -76,7 +89,7 @@ export const VocabularySummary: React.FC = () => {
</Col>
<Col xl={10} md={8}>
<MarkdownView id="vocabulary-metadata-comment">
{vocabulary.comment}
{getLocalizedOrDefault(vocabulary.comment, "", language)}
</MarkdownView>
</Col>
</Row>
Expand Down
Loading

0 comments on commit d9e0ad4

Please sign in to comment.