From 0044f95b3a7ed8714f13e94037a179cdbbd84a73 Mon Sep 17 00:00:00 2001 From: Martin Ledvinka Date: Thu, 7 Nov 2024 17:37:18 +0100 Subject: [PATCH 1/4] [Ref] Refactor CreateFileMetadata.tsx to hooks. Add language attribute to File. --- .../resource/file/CreateFileMetadata.tsx | 148 ++++++++---------- .../__tests__/CreateFileMetadata.test.tsx | 15 +- src/model/File.ts | 4 + 3 files changed, 75 insertions(+), 92 deletions(-) diff --git a/src/component/resource/file/CreateFileMetadata.tsx b/src/component/resource/file/CreateFileMetadata.tsx index cbefd0d1a..074339067 100644 --- a/src/component/resource/file/CreateFileMetadata.tsx +++ b/src/component/resource/file/CreateFileMetadata.tsx @@ -1,101 +1,81 @@ -import * as React from "react"; -import { injectIntl } from "react-intl"; -import withI18n, { HasI18n } from "../../hoc/withI18n"; +import React from "react"; import { Button, ButtonToolbar, Col, Form, Row } from "reactstrap"; import UploadFile from "./UploadFile"; import TermItFile from "../../../model/File"; import CustomInput from "../../misc/CustomInput"; -import { AssetData } from "../../../model/Asset"; +import { useI18n } from "../../hook/useI18n"; -interface CreateFileMetadataProps extends HasI18n { +interface CreateFileMetadataProps { onCreate: (termItFile: TermItFile, file: File) => any; onCancel: () => void; } -interface CreateFileMetadataState extends AssetData { - iri: string; - label: string; - file?: File; - dragActive: boolean; -} - -export class CreateFileMetadata extends React.Component< - CreateFileMetadataProps, - CreateFileMetadataState -> { - constructor(props: CreateFileMetadataProps) { - super(props); - this.state = { - iri: "", - label: "", - file: undefined, - dragActive: false, - }; - } +const CreateFileMetadata: React.FC = ({ + onCreate, + onCancel, +}) => { + const { i18n } = useI18n(); + const [label, setLabel] = React.useState(""); + const [file, setFile] = React.useState(); - protected onLabelChange = (e: React.ChangeEvent): void => { - const label = e.currentTarget.value; - this.setState({ label }); + const onFileSelected = (file: File) => { + setFile(file); + setLabel(file.name); }; - - public onCreate = () => { - const { file, dragActive, ...data } = this.state; + const onSubmit = () => { if (file) { - this.props.onCreate(new TermItFile(data), file); + onCreate( + new TermItFile({ + iri: "", + label, + }), + file + ); } }; - - public setFile = (file: File) => { - this.setState({ file, label: file.name, dragActive: false }); - }; - - public cannotSubmit = () => { - return !this.state.file || this.state.label.trim().length === 0; + const cannotSubmit = () => { + return !file || label.trim().length === 0; }; - public render() { - const i18n = this.props.i18n; - - return ( -
- - - - - - - - - - - - - - - - ); - } -} + return ( +
+ + + + setLabel(e.target.value)} + hint={i18n("required")} + /> + + + + + + + + + + + + ); +}; -export default injectIntl(withI18n(CreateFileMetadata)); +export default CreateFileMetadata; diff --git a/src/component/resource/file/__tests__/CreateFileMetadata.test.tsx b/src/component/resource/file/__tests__/CreateFileMetadata.test.tsx index 6fa3e0f86..61fef673a 100644 --- a/src/component/resource/file/__tests__/CreateFileMetadata.test.tsx +++ b/src/component/resource/file/__tests__/CreateFileMetadata.test.tsx @@ -1,11 +1,9 @@ import Resource from "../../../../model/Resource"; import Ajax from "../../../../util/Ajax"; -import { - flushPromises, - mountWithIntl, -} from "../../../../__tests__/environment/Environment"; -import { CreateFileMetadata } from "../CreateFileMetadata"; +import { mountWithIntl } from "../../../../__tests__/environment/Environment"; +import CreateFileMetadata from "../CreateFileMetadata"; import { intlFunctions } from "../../../../__tests__/environment/IntlUtil"; +import UploadFile from "../UploadFile"; jest.mock("../../../../util/Ajax", () => { const originalModule = jest.requireActual("../../../../util/Ajax"); @@ -43,9 +41,10 @@ describe("CreateFileMetadata", () => { {...intlFunctions()} /> ); - (wrapper.find(CreateFileMetadata).instance() as CreateFileMetadata).setFile( - file as File - ); + wrapper + .find(UploadFile) + .props() + .setFile(file as File); const labelInput = wrapper.find('input[name="create-resource-label"]'); expect((labelInput.getDOMNode() as HTMLInputElement).value).toEqual( fileName diff --git a/src/model/File.ts b/src/model/File.ts index 6833cc17f..16bad6271 100644 --- a/src/model/File.ts +++ b/src/model/File.ts @@ -6,6 +6,7 @@ import VocabularyUtils from "../util/VocabularyUtils"; const ctx = { content: VocabularyUtils.CONTENT, owner: VocabularyUtils.IS_PART_OF_DOCUMENT, + language: VocabularyUtils.DC_LANGUAGE, }; /** @@ -18,18 +19,21 @@ export const OWN_CONTEXT = ctx; export interface FileData extends ResourceData { origin?: string; content?: string; + language?: string; owner?: DocumentData; } export default class File extends Resource implements FileData { public origin: string; public content?: string; + public language?: string; public owner?: DocumentData; constructor(data: FileData) { super(data); this.origin = data.origin ? data.origin : ""; this.content = data.content; + this.language = data.language; this.owner = data.owner; } From 629637a599ac7c36a8676e28945901296893b844 Mon Sep 17 00:00:00 2001 From: Martin Ledvinka Date: Mon, 11 Nov 2024 16:52:48 +0100 Subject: [PATCH 2/4] [Enhancement #553] Allow selecting content language when creating file. --- .../multilingual/EditLanguageSelector.tsx | 29 ++-------------- .../resource/file/CreateFileMetadata.tsx | 26 ++++++++++++++- .../resource/file/LanguageSelector.tsx | 33 +++++++++++++++++++ src/i18n/cs.ts | 1 + src/i18n/en.ts | 1 + src/util/IntlUtil.ts | 28 ++++++++++++++++ 6 files changed, 90 insertions(+), 28 deletions(-) create mode 100644 src/component/resource/file/LanguageSelector.tsx diff --git a/src/component/multilingual/EditLanguageSelector.tsx b/src/component/multilingual/EditLanguageSelector.tsx index fd743a9e0..dcc7d3ba5 100644 --- a/src/component/multilingual/EditLanguageSelector.tsx +++ b/src/component/multilingual/EditLanguageSelector.tsx @@ -1,10 +1,8 @@ import * as React from "react"; -import ISO6391 from "iso-639-1"; import classNames from "classnames"; // @ts-ignore import { IntelligentTreeSelect } from "intelligent-tree-select"; -import Constants from "../../util/Constants"; -import { getShortLocale } from "../../util/IntlUtil"; +import { getLanguageOptions, Language } from "../../util/IntlUtil"; import { renderLanguages } from "./LanguageSelector"; import { Nav, NavItem, NavLink } from "reactstrap"; import { FaPlusCircle } from "react-icons/fa"; @@ -18,29 +16,6 @@ interface EditLanguageSelectorProps { onRemove: (lang: string) => void; } -interface Language { - code: string; - name: string; - nativeName: string; -} - -function prioritizeLanguages(options: Language[], languages: string[]) { - languages.forEach((lang) => { - const ind = options.findIndex((v) => v.code === lang); - const option = options[ind]; - options.splice(ind, 1); - options.unshift(option); - }); - return options; -} - -const OPTIONS = prioritizeLanguages( - ISO6391.getLanguages(ISO6391.getAllCodes()), - Object.getOwnPropertyNames(Constants.LANG).map((lang) => - getShortLocale(Constants.LANG[lang].locale) - ) -); - const EditLanguageSelector: React.FC = (props) => { const { language, existingLanguages, onSelect, onRemove } = props; const { i18n, formatMessage } = useI18n(); @@ -51,7 +26,7 @@ const EditLanguageSelector: React.FC = (props) => { if (existingLanguages.indexOf(language) === -1) { existingLanguages.push(language); } - const options = OPTIONS.slice(); + const options = getLanguageOptions().slice(); for (const existing of existingLanguages) { const toRemove = options.findIndex((o) => o.code === existing); options.splice(toRemove, 1); diff --git a/src/component/resource/file/CreateFileMetadata.tsx b/src/component/resource/file/CreateFileMetadata.tsx index 074339067..daf7de26c 100644 --- a/src/component/resource/file/CreateFileMetadata.tsx +++ b/src/component/resource/file/CreateFileMetadata.tsx @@ -1,9 +1,20 @@ import React from "react"; -import { Button, ButtonToolbar, Col, Form, Row } from "reactstrap"; +import { + Button, + ButtonToolbar, + Col, + Form, + FormGroup, + Label, + Row, +} from "reactstrap"; import UploadFile from "./UploadFile"; import TermItFile from "../../../model/File"; import CustomInput from "../../misc/CustomInput"; import { useI18n } from "../../hook/useI18n"; +import { useSelector } from "react-redux"; +import TermItState from "../../../model/TermItState"; +import LanguageSelector from "./LanguageSelector"; interface CreateFileMetadataProps { onCreate: (termItFile: TermItFile, file: File) => any; @@ -17,6 +28,10 @@ const CreateFileMetadata: React.FC = ({ const { i18n } = useI18n(); const [label, setLabel] = React.useState(""); const [file, setFile] = React.useState(); + const lang = useSelector( + (state: TermItState) => state.configuration.language + ); + const [language, setLanguage] = React.useState(lang); const onFileSelected = (file: File) => { setFile(file); @@ -28,6 +43,7 @@ const CreateFileMetadata: React.FC = ({ new TermItFile({ iri: "", label, + language, }), file ); @@ -51,6 +67,14 @@ const CreateFileMetadata: React.FC = ({ /> + + + + + + + + diff --git a/src/component/resource/file/LanguageSelector.tsx b/src/component/resource/file/LanguageSelector.tsx new file mode 100644 index 000000000..ea46c8533 --- /dev/null +++ b/src/component/resource/file/LanguageSelector.tsx @@ -0,0 +1,33 @@ +import React from "react"; +// @ts-ignore +import { IntelligentTreeSelect } from "intelligent-tree-select"; +import { getLanguageOptions, Language } from "../../../util/IntlUtil"; +import { useI18n } from "../../hook/useI18n"; + +const LanguageSelector: React.FC<{ + onChange: (lang: string) => void; + value: string; +}> = ({ onChange, value }) => { + const options = getLanguageOptions(); + const { i18n } = useI18n(); + return ( + onChange(item.code)} + options={options} + maxHeight={200} + multi={false} + labelKey="nativeName" + valueKey="code" + classNamePrefix="react-select" + simpleTreeData={true} + renderAsTree={false} + showSettings={false} + isClearable={false} + placeholder="" + noResultsText={i18n("search.no-results")} + value={options.find((o) => o.code === value)} + /> + ); +}; + +export default LanguageSelector; diff --git a/src/i18n/cs.ts b/src/i18n/cs.ts index ebeb33997..3268bfbd6 100644 --- a/src/i18n/cs.ts +++ b/src/i18n/cs.ts @@ -622,6 +622,7 @@ const cs = { "file.upload.hint": "Maximální velikost souboru: {maxUploadFileSize}. Má-li být soubor použit pro extrakci pojmů do slovníku, musí být ve formátu UTF-8, nebo validní MS Excel.", "file.upload.size.exceeded": "Soubor je příliš velký.", + "file.language": "Jazyk obsahu souboru", "dataset.license": "Licence", "dataset.format": "Formát", diff --git a/src/i18n/en.ts b/src/i18n/en.ts index ea5733593..3f3752287 100644 --- a/src/i18n/en.ts +++ b/src/i18n/en.ts @@ -613,6 +613,7 @@ const en = { "file.upload.hint": "Maximum file size: {maxUploadFileSize}. To use the file for term extraction, it must be in UTF-8 or a valid MS Excel file.", "file.upload.size.exceeded": "File is too large.", + "file.language": "File content language", "dataset.license": "License", "dataset.format": "Format", diff --git a/src/util/IntlUtil.ts b/src/util/IntlUtil.ts index 3c5631dd1..56f9acc2e 100644 --- a/src/util/IntlUtil.ts +++ b/src/util/IntlUtil.ts @@ -2,6 +2,7 @@ import Constants from "./Constants"; import IntlData from "../model/IntlData"; import BrowserStorage from "./BrowserStorage"; import Utils from "./Utils"; +import ISO6391 from "iso-639-1"; export function loadInitialLocalizationData(): IntlData { const prefLang = BrowserStorage.get(Constants.STORAGE_LANG_KEY); @@ -88,3 +89,30 @@ export function removeTranslation( } }); } + +export interface Language { + code: string; + name: string; + nativeName: string; +} + +function prioritizeLanguages(options: Language[], languages: string[]) { + languages.forEach((lang) => { + const ind = options.findIndex((v) => v.code === lang); + const option = options[ind]; + options.splice(ind, 1); + options.unshift(option); + }); + return options; +} + +const LANGUAGE_OPTIONS = prioritizeLanguages( + ISO6391.getLanguages(ISO6391.getAllCodes()), + Object.getOwnPropertyNames(Constants.LANG).map((lang) => + getShortLocale(Constants.LANG[lang].locale) + ) +); + +export function getLanguageOptions(): Language[] { + return LANGUAGE_OPTIONS; +} From 10e230d83de9afd2a140f857606f03f46c7ea3db Mon Sep 17 00:00:00 2001 From: Martin Ledvinka Date: Mon, 11 Nov 2024 17:07:07 +0100 Subject: [PATCH 3/4] [Enhancement #553] Show content language in document file list. --- src/component/resource/document/Files.tsx | 19 +++++++++++++++---- src/util/IntlUtil.ts | 8 ++++++++ 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/src/component/resource/document/Files.tsx b/src/component/resource/document/Files.tsx index f2a434302..11d523f4d 100644 --- a/src/component/resource/document/Files.tsx +++ b/src/component/resource/document/Files.tsx @@ -1,7 +1,7 @@ import TermItFile from "../../../model/File"; import File from "../../../model/File"; import Utils from "../../../util/Utils"; -import { ButtonToolbar, Label, Table } from "reactstrap"; +import { Badge, ButtonToolbar, Label, Table } from "reactstrap"; import { useI18n } from "../../hook/useI18n"; interface FilesProps { @@ -20,7 +20,7 @@ const Files = (props: FilesProps) => { -
+
{files.length > 0 ? ( - +
{files.map((v: File) => ( - +
{v.label} + {v.language && ( + + {v.language} + + )} + {v.label} + {props.itemActions(v)} diff --git a/src/util/IntlUtil.ts b/src/util/IntlUtil.ts index 56f9acc2e..f33067879 100644 --- a/src/util/IntlUtil.ts +++ b/src/util/IntlUtil.ts @@ -90,6 +90,9 @@ export function removeTranslation( }); } +/** + * Type representing language data in an asset language selector. + */ export interface Language { code: string; name: string; @@ -113,6 +116,11 @@ const LANGUAGE_OPTIONS = prioritizeLanguages( ) ); +/** + * Gets a list of all possible languages. + * + * The languages are retrieved using the iso-639-1 JS library. + */ export function getLanguageOptions(): Language[] { return LANGUAGE_OPTIONS; } From d6db8f1bda056d70520c2a717b74a25cfd2a4a50 Mon Sep 17 00:00:00 2001 From: Martin Ledvinka Date: Tue, 19 Nov 2024 10:11:38 +0100 Subject: [PATCH 4/4] [Enhancement #553] Error messages for unsupported text analysis language. --- src/i18n/cs.ts | 5 +++++ src/i18n/en.ts | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/src/i18n/cs.ts b/src/i18n/cs.ts index 3268bfbd6..4da09f10e 100644 --- a/src/i18n/cs.ts +++ b/src/i18n/cs.ts @@ -815,6 +815,11 @@ const cs = { 'Neplatný identifikátor: "{uri}", neočekávaný znak "{char}" na pozici {index}.', "error.invalidIdentifier": 'Neplatný identifikátor: "{uri}"', + "error.annotation.file.unsupportedLanguage": + "Služba textové analýza nepodporuje jazyk obsahu souboru.", + "error.annotation.term.unsupportedLanguage": + "Služba textové analýza nepodporuje jazyk definice pojmu.", + "history.label": "Historie změn", "history.loading": "Načítám historii...", "history.empty": "Zaznamenaná historie je prázdná.", diff --git a/src/i18n/en.ts b/src/i18n/en.ts index 3f3752287..c20e4d704 100644 --- a/src/i18n/en.ts +++ b/src/i18n/en.ts @@ -807,6 +807,11 @@ const en = { 'Invalid identifier: "{uri}", unexpected character "{char}" at {index}.', "error.invalidIdentifier": 'Invalid identifier: "{uri}"', + "error.annotation.file.unsupportedLanguage": + "Text analysis service does not support the language of this file.", + "error.annotation.term.unsupportedLanguage": + "Text analysis service does not support the language of this term's definition.", + "history.label": "Change history", "history.loading": "Loading history...", "history.empty": "The recorded history of this asset is empty.",