From f3ebbf48b090e592abb993bd1282bb05106c61c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Ka=C5=88ka?= Date: Sun, 20 Oct 2024 16:29:44 +0200 Subject: [PATCH 01/24] [Enhancement #520] Vocabulary activity term changes filter A design of filter inputs for term change records in vocabulary activity tab. --- .../vocabulary/TermChangeFrequencyUI.scss | 9 +++ .../vocabulary/TermChangeFrequencyUI.tsx | 69 ++++++++++++++++++- src/i18n/cs.ts | 2 + src/i18n/en.ts | 2 + 4 files changed, 81 insertions(+), 1 deletion(-) create mode 100644 src/component/vocabulary/TermChangeFrequencyUI.scss diff --git a/src/component/vocabulary/TermChangeFrequencyUI.scss b/src/component/vocabulary/TermChangeFrequencyUI.scss new file mode 100644 index 00000000..8c71cc6e --- /dev/null +++ b/src/component/vocabulary/TermChangeFrequencyUI.scss @@ -0,0 +1,9 @@ +.cursor-pointer { + cursor: pointer; +} +.color-primary { + color: var(--primary); +} +#date-filter-col { + padding-left: 30px; +} diff --git a/src/component/vocabulary/TermChangeFrequencyUI.tsx b/src/component/vocabulary/TermChangeFrequencyUI.tsx index 6235d1f3..11963b4d 100644 --- a/src/component/vocabulary/TermChangeFrequencyUI.tsx +++ b/src/component/vocabulary/TermChangeFrequencyUI.tsx @@ -10,6 +10,10 @@ import VocabularyContentPersistRow from "../changetracking/VocabularyContentPers import VocabularyContentUpdateRow from "../changetracking/VocabularyContentUpdateRow"; import If from "../misc/If"; import SimplePagination from "../dashboard/widget/lastcommented/SimplePagination"; +import CustomInput from "../misc/CustomInput"; +import Select from "../misc/Select"; +import "./TermChangeFrequencyUI.scss"; +import classNames from "classnames"; interface TermChangeFrequencyUIProps { aggregatedRecords: AggregatedChangeInfo[] | null; @@ -67,6 +71,7 @@ const TermChangeFrequencyUI: React.FC = ({ itemCount, }) => { const { i18n, locale } = useI18n(); + const [showFilter, setShowFilter] = React.useState(false); if (!aggregatedRecords || !changeRecords) { return
 
; } @@ -154,12 +159,74 @@ const TermChangeFrequencyUI: React.FC = ({
+ + + + + - + + + + + + + + + {changeRecords.map((r) => diff --git a/src/i18n/cs.ts b/src/i18n/cs.ts index ebeb3399..00f7bf34 100644 --- a/src/i18n/cs.ts +++ b/src/i18n/cs.ts @@ -821,9 +821,11 @@ const cs = { "history.type": "Typ", "history.type.persist": "Vytvoření", "history.type.update": "Změna", + "history.type.delete": "Smazání", "history.changedAttribute": "Atribut", "history.originalValue": "Původní hodnota", "history.newValue": "Nová hodnota", + "history.filter.datetime": "Období", "changefrequency.label": "Aktivita", diff --git a/src/i18n/en.ts b/src/i18n/en.ts index ea573359..0602ea6d 100644 --- a/src/i18n/en.ts +++ b/src/i18n/en.ts @@ -813,9 +813,11 @@ const en = { "history.type": "Type", "history.type.persist": "Creation", "history.type.update": "Update", + "history.type.delete": "Deletion", "history.changedAttribute": "Attribute", "history.originalValue": "Original value", "history.newValue": "New value", + "history.filter.datetime": "Time period", "changefrequency.label": "Activity", From fa1b65e2627b2eee76f2f8ca31d4860b0b5aebd3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Ka=C5=88ka?= Date: Sun, 27 Oct 2024 18:02:53 +0100 Subject: [PATCH 02/24] [Enhancement #520] Vocabulary activity term changes filter backend requests Removed time range input and implemented requesting data from BE using filter parameters. --- src/action/AsyncVocabularyActions.ts | 25 ++- .../VocabularyContentDeleteRow.tsx | 37 ++++ .../vocabulary/TermChangeFrequency.tsx | 14 +- .../vocabulary/TermChangeFrequencyUI.tsx | 165 ++++++++++-------- src/model/changetracking/DeleteRecord.ts | 22 +++ .../VocabularyContentChangeFilterData.ts | 6 + src/util/AssetFactory.ts | 5 + src/util/VocabularyUtils.ts | 1 + 8 files changed, 194 insertions(+), 81 deletions(-) create mode 100644 src/component/changetracking/VocabularyContentDeleteRow.tsx create mode 100644 src/model/changetracking/DeleteRecord.ts create mode 100644 src/model/filter/VocabularyContentChangeFilterData.ts diff --git a/src/action/AsyncVocabularyActions.ts b/src/action/AsyncVocabularyActions.ts index 48e766d0..48b156b6 100644 --- a/src/action/AsyncVocabularyActions.ts +++ b/src/action/AsyncVocabularyActions.ts @@ -8,7 +8,7 @@ import { publishMessage, publishNotification, } from "./SyncActions"; -import { IRI } from "../util/VocabularyUtils"; +import VocabularyUtils, { IRI } from "../util/VocabularyUtils"; import ActionType from "./ActionType"; import Ajax, { param } from "../util/Ajax"; import Constants from "../util/Constants"; @@ -31,6 +31,7 @@ import ChangeRecord, { CONTEXT as CHANGE_RECORD_CONTEXT, } from "../model/changetracking/ChangeRecord"; import AssetFactory from "../util/AssetFactory"; +import { VocabularyContentChangeFilterData } from "../model/filter/VocabularyContentChangeFilterData"; export function loadTermCount(vocabularyIri: IRI) { const action = { type: ActionType.LOAD_TERM_COUNT, vocabularyIri }; @@ -137,6 +138,7 @@ export function loadVocabularyContentChanges(vocabularyIri: IRI) { export function loadVocabularyContentDetailedChanges( vocabularyIri: IRI, + filterData: VocabularyContentChangeFilterData, pageReq: PageRequest ) { const action = { @@ -145,11 +147,26 @@ export function loadVocabularyContentDetailedChanges( return (dispatch: ThunkDispatch) => { dispatch(asyncActionRequest(action, true)); + let params = param("namespace", vocabularyIri.namespace) + .param("page", pageReq.page?.toString()) + .param("size", pageReq.size?.toString()); + for (const [key, value] of Object.entries(filterData)) { + params = params.param(key, value); + } + switch (params.getParams()?.["type"]) { + case "history.type.persist": + params = params.param("type", VocabularyUtils.PERSIST_EVENT); + break; + case "history.type.update": + params = params.param("type", VocabularyUtils.UPDATE_EVENT); + break; + case "history.type.delete": + params = params.param("type", VocabularyUtils.DELETE_EVENT); + break; + } return Ajax.get( `${Constants.API_PREFIX}/vocabularies/${vocabularyIri.fragment}/history-of-content/detail`, - param("namespace", vocabularyIri.namespace) - .param("page", pageReq.page?.toString()) - .param("size", pageReq.size?.toString()) + params ) .then((data) => JsonLdUtils.compactAndResolveReferencesAsArray( diff --git a/src/component/changetracking/VocabularyContentDeleteRow.tsx b/src/component/changetracking/VocabularyContentDeleteRow.tsx new file mode 100644 index 00000000..6c1b67bc --- /dev/null +++ b/src/component/changetracking/VocabularyContentDeleteRow.tsx @@ -0,0 +1,37 @@ +import * as React from "react"; +import { FormattedDate, FormattedTime } from "react-intl"; +import { Badge } from "reactstrap"; +import { useI18n } from "../hook/useI18n"; +import TermIriLink from "../term/TermIriLink"; +import DeleteRecord from "../../model/changetracking/DeleteRecord"; + +export interface DeleteRowProps { + record: DeleteRecord; +} + +export const VocabularyContentDeleteRow: React.FC = (props) => { + const { i18n } = useI18n(); + const record = props.record; + const created = new Date(Date.parse(record.timestamp)); + return ( + + + + + + + ); +}; + +export default VocabularyContentDeleteRow; diff --git a/src/component/vocabulary/TermChangeFrequency.tsx b/src/component/vocabulary/TermChangeFrequency.tsx index 3732dad5..5f73aa3d 100644 --- a/src/component/vocabulary/TermChangeFrequency.tsx +++ b/src/component/vocabulary/TermChangeFrequency.tsx @@ -1,4 +1,5 @@ import * as React from "react"; +import { useState } from "react"; import Vocabulary from "../../model/Vocabulary"; import { ThunkDispatch } from "../../util/Types"; import { useDispatch } from "react-redux"; @@ -14,6 +15,7 @@ import { loadVocabularyContentDetailedChanges, } from "../../action/AsyncVocabularyActions"; import ChangeRecord from "../../model/changetracking/ChangeRecord"; +import { VocabularyContentChangeFilterData } from "../../model/filter/VocabularyContentChangeFilterData"; interface TermChangeFrequencyProps { vocabulary: Vocabulary; @@ -30,6 +32,13 @@ const TermChangeFrequency: React.FC = (props) => { const { i18n } = useI18n(); const dispatch: ThunkDispatch = useDispatch(); const [page, setPage] = React.useState(0); + const [filterData, setFilterData] = + useState({ + term: "", + type: "", + attribute: "", + author: "", + }); React.useEffect(() => { if (vocabulary.iri !== Constants.EMPTY_ASSET_IRI) { trackPromise( @@ -47,13 +56,14 @@ const TermChangeFrequency: React.FC = (props) => { dispatch( loadVocabularyContentDetailedChanges( VocabularyUtils.create(vocabulary.iri), + filterData, { page: page, size: Constants.VOCABULARY_CONTENT_HISTORY_LIMIT } ) ).then((changeRecords) => setChangeRecords(changeRecords)), "term-change-frequency" ); } - }, [vocabulary.iri, dispatch, page]); + }, [vocabulary.iri, dispatch, page, filterData]); return ( <> @@ -67,7 +77,7 @@ const TermChangeFrequency: React.FC = (props) => { page={page} setPage={setPage} pageSize={Constants.VOCABULARY_CONTENT_HISTORY_LIMIT} - itemCount={changeRecords?.length ?? 0} + applyFilter={setFilterData} /> ); diff --git a/src/component/vocabulary/TermChangeFrequencyUI.tsx b/src/component/vocabulary/TermChangeFrequencyUI.tsx index 11963b4d..785d0aa2 100644 --- a/src/component/vocabulary/TermChangeFrequencyUI.tsx +++ b/src/component/vocabulary/TermChangeFrequencyUI.tsx @@ -1,4 +1,5 @@ import * as React from "react"; +import { useCallback, useEffect, useState } from "react"; import Chart from "react-apexcharts"; import { Col, Row, Table } from "reactstrap"; import { useI18n } from "../hook/useI18n"; @@ -13,7 +14,12 @@ import SimplePagination from "../dashboard/widget/lastcommented/SimplePagination import CustomInput from "../misc/CustomInput"; import Select from "../misc/Select"; import "./TermChangeFrequencyUI.scss"; -import classNames from "classnames"; +import PersistRecord from "../../model/changetracking/PersistRecord"; +import DeleteRecord from "../../model/changetracking/DeleteRecord"; +import VocabularyContentDeleteRow from "../changetracking/VocabularyContentDeleteRow"; +import { debounce } from "lodash"; +import Constants from "../../util/Constants"; +import { VocabularyContentChangeFilterData } from "../../model/filter/VocabularyContentChangeFilterData"; interface TermChangeFrequencyUIProps { aggregatedRecords: AggregatedChangeInfo[] | null; @@ -21,7 +27,7 @@ interface TermChangeFrequencyUIProps { page: number; setPage: (page: number) => void; pageSize: number; - itemCount: number; + applyFilter: (filterData: VocabularyContentChangeFilterData) => void; } /** @@ -68,10 +74,32 @@ const TermChangeFrequencyUI: React.FC = ({ page, setPage, pageSize, - itemCount, + applyFilter, }) => { const { i18n, locale } = useI18n(); - const [showFilter, setShowFilter] = React.useState(false); + + const [filterAuthor, setFilterAuthor] = useState(""); + const [filterTerm, setFilterTerm] = useState(""); + const [filterType, setFilterType] = useState(""); + const [filterAttribute, setFilterAttribute] = useState(""); + const applyFilterDebounced = useCallback( + debounce( + (filterData: VocabularyContentChangeFilterData) => + applyFilter(filterData), + Constants.INPUT_DEBOUNCE_WAIT_TIME + ), + [applyFilter] + ); + + useEffect(() => { + applyFilterDebounced({ + author: filterTerm, + term: filterTerm, + type: filterType, + attribute: filterAttribute, + }); + }, [filterAuthor, filterTerm, filterType, filterAttribute]); + if (!aggregatedRecords || !changeRecords) { return
 
; } @@ -152,90 +180,77 @@ const TermChangeFrequencyUI: React.FC = ({ ]; return ( -
+
+ {}} + /> +
{i18n("history.whenwho")} {i18n("type.term")} {i18n("history.type")}{i18n("history.changedAttribute")} + {i18n("history.changedAttribute")} + setShowFilter(!showFilter)} + title={i18n("main.nav.search")} + > + + +
+ + + + + + + +
+
+ +
+
+ {record.author.fullName} +
+
+ + + {i18n(record.typeLabel)} +
- - - - - - + + + + + + - - - - - - - - - {changeRecords.map((r) => - r instanceof UpdateRecord ? ( - - ) : ( - - ) - )} + {changeRecords.map((r) => { + if (r instanceof PersistRecord) { + return ; + } + if (r instanceof UpdateRecord) { + return ; + } + if (r instanceof DeleteRecord) { + return ; + } + return null; + })}
- {}} - /> -
{i18n("history.whenwho")} {i18n("type.term")} {i18n("history.type")} - {i18n("history.changedAttribute")} - setShowFilter(!showFilter)} - title={i18n("main.nav.search")} + {i18n("history.changedAttribute")}
+ setFilterAuthor(e.target.value)} + /> + + setFilterTerm(e.target.value)} + /> + + + + setFilterAttribute(e.target.value)} + /> +
- - - - - - - -
@@ -244,7 +259,7 @@ const TermChangeFrequencyUI: React.FC = ({ page={page} setPage={setPage} pageSize={pageSize} - itemCount={itemCount} + itemCount={pageSize + 1} /> diff --git a/src/model/changetracking/DeleteRecord.ts b/src/model/changetracking/DeleteRecord.ts new file mode 100644 index 00000000..120059e3 --- /dev/null +++ b/src/model/changetracking/DeleteRecord.ts @@ -0,0 +1,22 @@ +import ChangeRecord, { ChangeRecordData } from "./ChangeRecord"; +import MultilingualString from "../MultilingualString"; + +export interface DeleteRecordData extends ChangeRecordData { + label: MultilingualString; +} + +/** + * Represents insertion of an entity into the repository. + */ +export default class DeleteRecord extends ChangeRecord { + public readonly label: MultilingualString; + public readonly vocabulary?: string; + public constructor(data: DeleteRecordData) { + super(data); + this.label = data.label; + } + + get typeLabel(): string { + return "history.type.delete"; + } +} diff --git a/src/model/filter/VocabularyContentChangeFilterData.ts b/src/model/filter/VocabularyContentChangeFilterData.ts new file mode 100644 index 00000000..61ed9b1d --- /dev/null +++ b/src/model/filter/VocabularyContentChangeFilterData.ts @@ -0,0 +1,6 @@ +export interface VocabularyContentChangeFilterData { + author: string; + term: string; + type: string; + attribute: string; +} diff --git a/src/util/AssetFactory.ts b/src/util/AssetFactory.ts index 73b67d49..27c961fe 100644 --- a/src/util/AssetFactory.ts +++ b/src/util/AssetFactory.ts @@ -23,6 +23,9 @@ import { UserGroupAccessControlRecord, UserRoleAccessControlRecord, } from "../model/acl/AccessControlList"; +import DeleteRecord, { + DeleteRecordData, +} from "../model/changetracking/DeleteRecord"; const AssetFactory = { /** @@ -117,6 +120,8 @@ const AssetFactory = { return new PersistRecord(data); } else if (data.types.indexOf(VocabularyUtils.UPDATE_EVENT) !== -1) { return new UpdateRecord(data as UpdateRecordData); + } else if (data.types.indexOf(VocabularyUtils.DELETE_EVENT) !== -1) { + return new DeleteRecord(data as DeleteRecordData); } throw new TypeError( "Unsupported type of change record data " + JSON.stringify(data) diff --git a/src/util/VocabularyUtils.ts b/src/util/VocabularyUtils.ts index bf8ef33d..bb21e375 100644 --- a/src/util/VocabularyUtils.ts +++ b/src/util/VocabularyUtils.ts @@ -160,6 +160,7 @@ const VocabularyUtils = { PERSIST_EVENT: `${_NS_POPIS_DAT}vytvo\u0159en\u00ed-entity`, UPDATE_EVENT: `${_NS_POPIS_DAT}\u00faprava-entity`, + DELETE_EVENT: `${_NS_POPIS_DAT}smaz\u00e1n\u00ed-entity`, TERM_SNAPSHOT: _NS_POPIS_DAT + "verze-pojmu", VOCABULARY_SNAPSHOT: _NS_POPIS_DAT + "verze-slovníku", From 670f032a1a49a769b8f53f00e864a1b4b4d7995e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Ka=C5=88ka?= Date: Thu, 31 Oct 2024 15:57:03 +0100 Subject: [PATCH 03/24] [Enhancement kbss-cvut/termit-ui#520] Fix vocabulary content history not showing pagination in activity tab when no records are available --- src/component/vocabulary/TermChangeFrequencyUI.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/component/vocabulary/TermChangeFrequencyUI.tsx b/src/component/vocabulary/TermChangeFrequencyUI.tsx index 785d0aa2..e5468669 100644 --- a/src/component/vocabulary/TermChangeFrequencyUI.tsx +++ b/src/component/vocabulary/TermChangeFrequencyUI.tsx @@ -100,7 +100,7 @@ const TermChangeFrequencyUI: React.FC = ({ }); }, [filterAuthor, filterTerm, filterType, filterAttribute]); - if (!aggregatedRecords || !changeRecords) { + if (!aggregatedRecords) { return
 
; } @@ -239,7 +239,7 @@ const TermChangeFrequencyUI: React.FC = ({ - {changeRecords.map((r) => { + {changeRecords?.map((r) => { if (r instanceof PersistRecord) { return ; } @@ -254,7 +254,7 @@ const TermChangeFrequencyUI: React.FC = ({ - 0}> + 0}> Date: Fri, 1 Nov 2024 11:55:12 +0100 Subject: [PATCH 04/24] [Enhancement #520] Move shrinkFullIri to Utils Moving shrinkFullIri to Utils allows consistent IRI shrinking outside the AssetLabel component. --- src/component/misc/AssetLabel.tsx | 14 ++++---------- src/util/Utils.ts | 14 ++++++++++++++ 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/src/component/misc/AssetLabel.tsx b/src/component/misc/AssetLabel.tsx index 049d757d..a6d20d00 100644 --- a/src/component/misc/AssetLabel.tsx +++ b/src/component/misc/AssetLabel.tsx @@ -4,6 +4,7 @@ import { ThunkDispatch } from "../../util/Types"; import { getLabel } from "../../action/AsyncActions"; import Namespaces from "../../util/Namespaces"; import TermItState from "../../model/TermItState"; +import Utils from "../../util/Utils"; interface AssetLabelProps { iri: string; @@ -70,17 +71,10 @@ export class AssetLabel extends React.Component< } private shrinkFullIri(iri: string): string { - if (!this.props.shrinkFullIri || iri.indexOf("://") === -1) { - return iri; // It is prefixed + if (!this.props.shrinkFullIri) { + return iri; } - const lastSlashIndex = iri.lastIndexOf("/"); - const lastHashIndex = iri.lastIndexOf("#"); - return ( - "..." + - iri.substring( - lastHashIndex > lastSlashIndex ? lastHashIndex : lastSlashIndex - ) - ); + return Utils.shrinkFullIri(iri); } } diff --git a/src/util/Utils.ts b/src/util/Utils.ts index 45b54beb..18e9530a 100644 --- a/src/util/Utils.ts +++ b/src/util/Utils.ts @@ -367,6 +367,20 @@ const Utils = { notBlank(str?: string | null) { return !!(str && str.trim().length > 0); }, + + shrinkFullIri(iri: string): string { + if (iri.indexOf("://") === -1) { + return iri; // It is prefixed + } + const lastSlashIndex = iri.lastIndexOf("/"); + const lastHashIndex = iri.lastIndexOf("#"); + return ( + "..." + + iri.substring( + lastHashIndex > lastSlashIndex ? lastHashIndex : lastSlashIndex + ) + ); + }, }; export default Utils; From 8ff0138909dd82a553f8d4819a2f8e4fe53453cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Ka=C5=88ka?= Date: Fri, 1 Nov 2024 11:59:26 +0100 Subject: [PATCH 05/24] [Enhancement #520] Optimizing term labels in vocabulary content history. When DeleteRecord is loaded by FE, the label of deleted term is cached allowing to display it instead of IRI. When the label is not available, the IRI will be shrunk. --- src/action/AsyncVocabularyActions.ts | 14 ++++++++++++++ .../VocabularyContentDeleteRow.tsx | 2 +- .../VocabularyContentPersistRow.tsx | 2 +- .../VocabularyContentUpdateRow.tsx | 2 +- src/component/term/TermIriLink.tsx | 19 +++++++++++++++++-- src/model/changetracking/ChangeRecord.ts | 2 ++ src/model/changetracking/DeleteRecord.ts | 1 - 7 files changed, 36 insertions(+), 6 deletions(-) diff --git a/src/action/AsyncVocabularyActions.ts b/src/action/AsyncVocabularyActions.ts index 48b156b6..030a5aa7 100644 --- a/src/action/AsyncVocabularyActions.ts +++ b/src/action/AsyncVocabularyActions.ts @@ -32,6 +32,7 @@ import ChangeRecord, { } from "../model/changetracking/ChangeRecord"; import AssetFactory from "../util/AssetFactory"; import { VocabularyContentChangeFilterData } from "../model/filter/VocabularyContentChangeFilterData"; +import { getLocalized } from "../model/MultilingualString"; export function loadTermCount(vocabularyIri: IRI) { const action = { type: ActionType.LOAD_TERM_COUNT, vocabularyIri }; @@ -174,6 +175,19 @@ export function loadVocabularyContentDetailedChanges( CHANGE_RECORD_CONTEXT ) ) + .then((data: ChangeRecord[]) => { + // adding labels to the label cache as they cannot be fetched from server + const labels: { [key: string]: string } = {}; + data.forEach((r) => { + if (r["label"]) { + labels[r.changedEntity.iri] = getLocalized(r["label"]); + } + }); + dispatch( + asyncActionSuccessWithPayload({ type: ActionType.GET_LABEL }, labels) + ); + return data; + }) .then((data: ChangeRecord[]) => { dispatch(asyncActionSuccess(action)); return data.map((r) => AssetFactory.createChangeRecord(r)); diff --git a/src/component/changetracking/VocabularyContentDeleteRow.tsx b/src/component/changetracking/VocabularyContentDeleteRow.tsx index 6c1b67bc..55205364 100644 --- a/src/component/changetracking/VocabularyContentDeleteRow.tsx +++ b/src/component/changetracking/VocabularyContentDeleteRow.tsx @@ -24,7 +24,7 @@ export const VocabularyContentDeleteRow: React.FC = (props) => { - + {i18n(record.typeLabel)} diff --git a/src/component/changetracking/VocabularyContentPersistRow.tsx b/src/component/changetracking/VocabularyContentPersistRow.tsx index 6009cac5..343a67c7 100644 --- a/src/component/changetracking/VocabularyContentPersistRow.tsx +++ b/src/component/changetracking/VocabularyContentPersistRow.tsx @@ -22,7 +22,7 @@ export const VocabularyContentPersistRow: React.FC = ( - + {i18n(record.typeLabel)} diff --git a/src/component/changetracking/VocabularyContentUpdateRow.tsx b/src/component/changetracking/VocabularyContentUpdateRow.tsx index 85890e71..73af65d7 100644 --- a/src/component/changetracking/VocabularyContentUpdateRow.tsx +++ b/src/component/changetracking/VocabularyContentUpdateRow.tsx @@ -21,7 +21,7 @@ export const VocabularyContentUpdateRow: React.FC = (props) => { - + {i18n(record.typeLabel)} diff --git a/src/component/term/TermIriLink.tsx b/src/component/term/TermIriLink.tsx index 6b337538..18e72a38 100644 --- a/src/component/term/TermIriLink.tsx +++ b/src/component/term/TermIriLink.tsx @@ -4,31 +4,46 @@ import VocabularyUtils from "../../util/VocabularyUtils"; import Term from "../../model/Term"; import { useDispatch } from "react-redux"; import { ThunkDispatch } from "../../util/Types"; -import { loadTermByIri } from "../../action/AsyncActions"; +import { getLabel, loadTermByIri } from "../../action/AsyncActions"; import TermLink from "./TermLink"; import OutgoingLink from "../misc/OutgoingLink"; +import Utils from "../../util/Utils"; interface TermIriLinkProps { iri: string; id?: string; activeTab?: string; + shrinkFullIri?: boolean; } const TermIriLink: React.FC = (props) => { const { iri, id, activeTab } = props; const [term, setTerm] = useState(null); const dispatch: ThunkDispatch = useDispatch(); + const [label, setLabel] = useState(); useEffect(() => { const tIri = VocabularyUtils.create(iri); dispatch(loadTermByIri(tIri)).then((term) => setTerm(term)); }, [iri, dispatch, setTerm]); + // if term is null, try to acquire the label from cache + useEffect(() => { + if (term === null) { + dispatch(getLabel(iri)).then((label) => setLabel(label)); + } + }, [term, iri, dispatch]); + return ( <> {term !== null ? ( ) : ( - + )} ); diff --git a/src/model/changetracking/ChangeRecord.ts b/src/model/changetracking/ChangeRecord.ts index b06c268d..4101fb6a 100644 --- a/src/model/changetracking/ChangeRecord.ts +++ b/src/model/changetracking/ChangeRecord.ts @@ -1,6 +1,7 @@ import VocabularyUtils from "../../util/VocabularyUtils"; import User, { CONTEXT as USER_CONTEXT, UserData } from "../User"; import Utils from "../../util/Utils"; +import { context } from "../MultilingualString"; const ctx = { timestamp: { @@ -12,6 +13,7 @@ const ctx = { changedAttribute: `${VocabularyUtils.PREFIX}m\u00e1-zm\u011bn\u011bn\u00fd-atribut`, originalValue: `${VocabularyUtils.PREFIX}m\u00e1-p\u016fvodn\u00ed-hodnotu`, newValue: `${VocabularyUtils.PREFIX}m\u00e1-novou-hodnotu`, + label: context(VocabularyUtils.RDFS_LABEL), }; export const CONTEXT = Object.assign({}, ctx, USER_CONTEXT); diff --git a/src/model/changetracking/DeleteRecord.ts b/src/model/changetracking/DeleteRecord.ts index 120059e3..1eb257d2 100644 --- a/src/model/changetracking/DeleteRecord.ts +++ b/src/model/changetracking/DeleteRecord.ts @@ -10,7 +10,6 @@ export interface DeleteRecordData extends ChangeRecordData { */ export default class DeleteRecord extends ChangeRecord { public readonly label: MultilingualString; - public readonly vocabulary?: string; public constructor(data: DeleteRecordData) { super(data); this.label = data.label; From 0044f95b3a7ed8714f13e94037a179cdbbd84a73 Mon Sep 17 00:00:00 2001 From: Martin Ledvinka Date: Thu, 7 Nov 2024 17:37:18 +0100 Subject: [PATCH 06/24] [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 cbefd0d1..07433906 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 6fa3e0f8..61fef673 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 6833cc17..16bad627 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 f37b2ff60ca89b8ea22613cfe80007ab5db4b9e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Ka=C5=88ka?= Date: Sun, 10 Nov 2024 17:54:25 +0100 Subject: [PATCH 07/24] [Enhancement #520] Add change history filtering for term and vocabulary details. --- src/action/AsyncActions.ts | 37 ++++- src/action/AsyncVocabularyActions.ts | 19 +-- src/component/changetracking/AssetHistory.tsx | 127 +++++++++++++----- src/component/changetracking/DeleteRow.tsx | 35 +++++ .../VocabularyContentDeleteRow.tsx | 6 +- .../vocabulary/TermChangeFrequencyUI.tsx | 12 +- .../VocabularyContentChangeFilterData.ts | 16 +++ 7 files changed, 195 insertions(+), 57 deletions(-) create mode 100644 src/component/changetracking/DeleteRow.tsx diff --git a/src/action/AsyncActions.ts b/src/action/AsyncActions.ts index 6db20ef6..ae6dc891 100644 --- a/src/action/AsyncActions.ts +++ b/src/action/AsyncActions.ts @@ -61,6 +61,29 @@ import UserRole, { UserRoleData } from "../model/UserRole"; import { loadTermCount } from "./AsyncVocabularyActions"; import { getApiPrefix } from "./ActionUtils"; import { getShortLocale } from "../util/IntlUtil"; +import { + getChangeTypeUri, + VocabularyContentChangeFilterData, +} from "../model/filter/VocabularyContentChangeFilterData"; +/* + * Asynchronous actions involve requests to the backend server REST API. As per recommendations in the Redux docs, this consists + * of several synchronous sub-actions which inform the application of initiation of the request and its result. + * + * Some conventions (they are also described in README.md): + * API guidelines: + * _Load_ - use IRI identifiers as parameters (+ normalized name as string if necessary, e.g. when fetching a term). + * _Create_ - use the instance to be created as parameter + IRI identifier if additional context is necessary (e.g. when creating a term). + * _Update_ - use the instance to be updated as parameter. It should contain all the necessary data. + * _Remove_ - use the instance to be removed as parameter. + * + * Naming conventions for CRUD operations: + * _load${ASSET(S)}_ - loading assets from the server, e.g. `loadVocabulary` + * _create${ASSET}_ - creating an asset, e.g. `createVocabulary` + * _update${ASSET}_ - updating an asset, e.g. `updateVocabulary` + * _remove${ASSET}_ - removing an asset, e.g. `removeVocabulary` + * + * TODO Consider splitting this file into multiple, it is becoming too long + */ /* * Asynchronous actions involve requests to the backend server REST API. As per recommendations in the Redux docs, this consists @@ -1130,13 +1153,23 @@ export function loadLatestTextAnalysisRecord(resourceIri: IRI) { }; } -export function loadHistory(asset: Asset) { +export function loadHistory( + asset: Asset, + filterData?: VocabularyContentChangeFilterData +) { const assetIri = VocabularyUtils.create(asset.iri); const historyConf = resolveHistoryLoadingParams(asset, assetIri); const action = { type: historyConf.actionType }; return (dispatch: ThunkDispatch) => { dispatch(asyncActionRequest(action, true)); - return Ajax.get(historyConf.url, param("namespace", assetIri.namespace)) + let params = param("namespace", assetIri.namespace); + if (filterData) { + for (const [key, value] of Object.entries(filterData)) { + params = params.param(key, value); + } + params = params.param("type", getChangeTypeUri(filterData)); + } + return Ajax.get(historyConf.url, params) .then((data) => JsonLdUtils.compactAndResolveReferencesAsArray( data, diff --git a/src/action/AsyncVocabularyActions.ts b/src/action/AsyncVocabularyActions.ts index 030a5aa7..a827996a 100644 --- a/src/action/AsyncVocabularyActions.ts +++ b/src/action/AsyncVocabularyActions.ts @@ -8,7 +8,7 @@ import { publishMessage, publishNotification, } from "./SyncActions"; -import VocabularyUtils, { IRI } from "../util/VocabularyUtils"; +import { IRI } from "../util/VocabularyUtils"; import ActionType from "./ActionType"; import Ajax, { param } from "../util/Ajax"; import Constants from "../util/Constants"; @@ -31,7 +31,10 @@ import ChangeRecord, { CONTEXT as CHANGE_RECORD_CONTEXT, } from "../model/changetracking/ChangeRecord"; import AssetFactory from "../util/AssetFactory"; -import { VocabularyContentChangeFilterData } from "../model/filter/VocabularyContentChangeFilterData"; +import { + getChangeTypeUri, + VocabularyContentChangeFilterData, +} from "../model/filter/VocabularyContentChangeFilterData"; import { getLocalized } from "../model/MultilingualString"; export function loadTermCount(vocabularyIri: IRI) { @@ -154,17 +157,7 @@ export function loadVocabularyContentDetailedChanges( for (const [key, value] of Object.entries(filterData)) { params = params.param(key, value); } - switch (params.getParams()?.["type"]) { - case "history.type.persist": - params = params.param("type", VocabularyUtils.PERSIST_EVENT); - break; - case "history.type.update": - params = params.param("type", VocabularyUtils.UPDATE_EVENT); - break; - case "history.type.delete": - params = params.param("type", VocabularyUtils.DELETE_EVENT); - break; - } + params = params.param("type", getChangeTypeUri(filterData)); return Ajax.get( `${Constants.API_PREFIX}/vocabularies/${vocabularyIri.fragment}/history-of-content/detail`, params diff --git a/src/component/changetracking/AssetHistory.tsx b/src/component/changetracking/AssetHistory.tsx index 6c3b3968..d4883032 100644 --- a/src/component/changetracking/AssetHistory.tsx +++ b/src/component/changetracking/AssetHistory.tsx @@ -1,4 +1,5 @@ import * as React from "react"; +import { useCallback, useState } from "react"; import Asset, { AssetData } from "../../model/Asset"; import ChangeRecord from "../../model/changetracking/ChangeRecord"; import { Table } from "reactstrap"; @@ -14,6 +15,12 @@ import Constants from "../../util/Constants"; import { useI18n } from "../hook/useI18n"; import Vocabulary from "../../model/Vocabulary"; import Term from "../../model/Term"; +import CustomInput from "../misc/CustomInput"; +import Select from "../misc/Select"; +import DeleteRecord from "../../model/changetracking/DeleteRecord"; +import DeleteRow from "./DeleteRow"; +import { debounce } from "lodash"; +import { VocabularyContentChangeFilterData } from "../../model/filter/VocabularyContentChangeFilterData"; interface AssetHistoryProps { asset: Asset; @@ -23,8 +30,30 @@ export const AssetHistory: React.FC = ({ asset }) => { const { i18n } = useI18n(); const dispatch: ThunkDispatch = useDispatch(); const [records, setRecords] = React.useState(null); + const [filterAuthor, setFilterAuthor] = useState(""); + const [filterType, setFilterType] = useState(""); + const [filterAttribute, setFilterAttribute] = useState(""); + const loadHistoryActionDebounced = useCallback( + debounce( + ( + asset: Asset, + filterData: VocabularyContentChangeFilterData, + cb: (records?: ChangeRecord[]) => void + ) => dispatch(loadHistoryAction(asset, filterData)).then(cb), + Constants.INPUT_DEBOUNCE_WAIT_TIME + ), + [dispatch] + ); + React.useEffect(() => { if (asset.iri !== Constants.EMPTY_ASSET_IRI) { + const filter = { + author: filterAuthor, + term: "", + type: filterType, + attribute: filterAttribute, + }; + //Check if vocabulary/term is a snapshot if ( (asset instanceof Term || asset instanceof Vocabulary) && @@ -35,53 +64,89 @@ export const AssetHistory: React.FC = ({ asset }) => { types: asset.types, }; const snapshotTimeCreated = Date.parse(asset.snapshotCreated()!); - dispatch(loadHistoryAction(modifiedAsset as Asset)).then((recs) => { - //Show history which is relevant to the snapshot - const filteredRecs = recs.filter( - (r) => Date.parse(r.timestamp) < snapshotTimeCreated - ); - setRecords(filteredRecs); + loadHistoryActionDebounced(modifiedAsset as Asset, filter, (recs) => { + if (recs) { + //Show history which is relevant to the snapshot + const filteredRecs = recs.filter( + (r) => Date.parse(r.timestamp) < snapshotTimeCreated + ); + setRecords(filteredRecs); + } }); } else { - dispatch(loadHistoryAction(asset)).then((recs) => { - setRecords(recs); + loadHistoryActionDebounced(asset, filter, (recs) => { + if (recs) { + setRecords(recs); + } }); } } - }, [asset, dispatch]); + }, [asset, dispatch, filterAuthor, filterType, filterAttribute]); if (!records) { return ; } - if (records.length === 0) { - return ( -
- {i18n("history.empty")} -
- ); - } + return (
- - - - - + + + + + + + + + + - {records.map((r) => - r instanceof UpdateRecord ? ( - - ) : ( - - ) - )} + {records.map((r) => { + if (r instanceof PersistRecord) { + return ; + } + if (r instanceof UpdateRecord) { + return ; + } + if (r instanceof DeleteRecord) { + return ; + } + return null; + })}
{i18n("history.whenwho")}{i18n("history.type")}{i18n("history.changedAttribute")}{i18n("history.originalValue")}{i18n("history.newValue")}{i18n("history.whenwho")}{i18n("history.type")}{i18n("history.changedAttribute")}{i18n("history.originalValue")}{i18n("history.newValue")}
+ setFilterAuthor(e.target.value)} + /> + + + + setFilterAttribute(e.target.value)} + /> +
diff --git a/src/component/changetracking/DeleteRow.tsx b/src/component/changetracking/DeleteRow.tsx new file mode 100644 index 00000000..981c1440 --- /dev/null +++ b/src/component/changetracking/DeleteRow.tsx @@ -0,0 +1,35 @@ +import * as React from "react"; +import { FormattedDate, FormattedTime } from "react-intl"; +import { Badge } from "reactstrap"; +import { useI18n } from "../hook/useI18n"; +import DeleteRecord from "../../model/changetracking/DeleteRecord"; + +export interface DeleteRowProps { + record: DeleteRecord; +} + +export const DeleteRow: React.FC = (props) => { + const { i18n } = useI18n(); + const record = props.record; + const created = new Date(Date.parse(record.timestamp)); + return ( + + +
+ +
+
+ {record.author.fullName} +
+ + + {i18n(record.typeLabel)} + + + + + + ); +}; + +export default DeleteRow; diff --git a/src/component/changetracking/VocabularyContentDeleteRow.tsx b/src/component/changetracking/VocabularyContentDeleteRow.tsx index 55205364..9a77aa02 100644 --- a/src/component/changetracking/VocabularyContentDeleteRow.tsx +++ b/src/component/changetracking/VocabularyContentDeleteRow.tsx @@ -3,11 +3,7 @@ import { FormattedDate, FormattedTime } from "react-intl"; import { Badge } from "reactstrap"; import { useI18n } from "../hook/useI18n"; import TermIriLink from "../term/TermIriLink"; -import DeleteRecord from "../../model/changetracking/DeleteRecord"; - -export interface DeleteRowProps { - record: DeleteRecord; -} +import { DeleteRowProps } from "./DeleteRow"; export const VocabularyContentDeleteRow: React.FC = (props) => { const { i18n } = useI18n(); diff --git a/src/component/vocabulary/TermChangeFrequencyUI.tsx b/src/component/vocabulary/TermChangeFrequencyUI.tsx index e5468669..740cff12 100644 --- a/src/component/vocabulary/TermChangeFrequencyUI.tsx +++ b/src/component/vocabulary/TermChangeFrequencyUI.tsx @@ -93,7 +93,7 @@ const TermChangeFrequencyUI: React.FC = ({ useEffect(() => { applyFilterDebounced({ - author: filterTerm, + author: filterAuthor, term: filterTerm, type: filterType, attribute: filterAttribute, @@ -190,11 +190,11 @@ const TermChangeFrequencyUI: React.FC = ({ {i18n("history.whenwho")} {i18n("type.term")} - {i18n("history.type")} + {i18n("history.type")} {i18n("history.changedAttribute")} - + = ({ onChange={(e) => setFilterAuthor(e.target.value)} /> - + = ({ onChange={(e) => setFilterTerm(e.target.value)} /> - + - + Date: Mon, 11 Nov 2024 16:52:48 +0100 Subject: [PATCH 08/24] [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 fd743a9e..dcc7d3ba 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 07433906..daf7de26 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 00000000..ea46c853 --- /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 ebeb3399..3268bfbd 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 ea573359..3f375228 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 3c5631dd..56f9acc2 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 09/24] [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 f2a43430..11d523f4 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 56f9acc2..f3306787 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 d0cfe17853ce4fe88a01ebf64abfedbca18452e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Ka=C5=88ka?= Date: Fri, 15 Nov 2024 11:29:36 +0100 Subject: [PATCH 10/24] [Ref] Cleanup code, remove duplicated comment from merge, eliminate always true if statement --- src/action/AsyncActions.ts | 19 ------------------- .../vocabulary/TermChangeFrequencyUI.tsx | 15 ++++++--------- src/i18n/cs.ts | 1 - src/i18n/en.ts | 1 - 4 files changed, 6 insertions(+), 30 deletions(-) diff --git a/src/action/AsyncActions.ts b/src/action/AsyncActions.ts index ae6dc891..c3e5f581 100644 --- a/src/action/AsyncActions.ts +++ b/src/action/AsyncActions.ts @@ -65,25 +65,6 @@ import { getChangeTypeUri, VocabularyContentChangeFilterData, } from "../model/filter/VocabularyContentChangeFilterData"; -/* - * Asynchronous actions involve requests to the backend server REST API. As per recommendations in the Redux docs, this consists - * of several synchronous sub-actions which inform the application of initiation of the request and its result. - * - * Some conventions (they are also described in README.md): - * API guidelines: - * _Load_ - use IRI identifiers as parameters (+ normalized name as string if necessary, e.g. when fetching a term). - * _Create_ - use the instance to be created as parameter + IRI identifier if additional context is necessary (e.g. when creating a term). - * _Update_ - use the instance to be updated as parameter. It should contain all the necessary data. - * _Remove_ - use the instance to be removed as parameter. - * - * Naming conventions for CRUD operations: - * _load${ASSET(S)}_ - loading assets from the server, e.g. `loadVocabulary` - * _create${ASSET}_ - creating an asset, e.g. `createVocabulary` - * _update${ASSET}_ - updating an asset, e.g. `updateVocabulary` - * _remove${ASSET}_ - removing an asset, e.g. `removeVocabulary` - * - * TODO Consider splitting this file into multiple, it is becoming too long - */ /* * Asynchronous actions involve requests to the backend server REST API. As per recommendations in the Redux docs, this consists diff --git a/src/component/vocabulary/TermChangeFrequencyUI.tsx b/src/component/vocabulary/TermChangeFrequencyUI.tsx index 740cff12..a1d41dba 100644 --- a/src/component/vocabulary/TermChangeFrequencyUI.tsx +++ b/src/component/vocabulary/TermChangeFrequencyUI.tsx @@ -9,7 +9,6 @@ import ChangeRecord from "../../model/changetracking/ChangeRecord"; import { UpdateRecord } from "../../model/changetracking/UpdateRecord"; import VocabularyContentPersistRow from "../changetracking/VocabularyContentPersistRow"; import VocabularyContentUpdateRow from "../changetracking/VocabularyContentUpdateRow"; -import If from "../misc/If"; import SimplePagination from "../dashboard/widget/lastcommented/SimplePagination"; import CustomInput from "../misc/CustomInput"; import Select from "../misc/Select"; @@ -254,14 +253,12 @@ const TermChangeFrequencyUI: React.FC = ({
- 0}> - - +
); diff --git a/src/i18n/cs.ts b/src/i18n/cs.ts index 00f7bf34..2fa22622 100644 --- a/src/i18n/cs.ts +++ b/src/i18n/cs.ts @@ -825,7 +825,6 @@ const cs = { "history.changedAttribute": "Atribut", "history.originalValue": "Původní hodnota", "history.newValue": "Nová hodnota", - "history.filter.datetime": "Období", "changefrequency.label": "Aktivita", diff --git a/src/i18n/en.ts b/src/i18n/en.ts index 0602ea6d..9569044b 100644 --- a/src/i18n/en.ts +++ b/src/i18n/en.ts @@ -817,7 +817,6 @@ const en = { "history.changedAttribute": "Attribute", "history.originalValue": "Original value", "history.newValue": "New value", - "history.filter.datetime": "Time period", "changefrequency.label": "Activity", From 075318b79630534824256121b70b55203f0ff150 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Ka=C5=88ka?= Date: Fri, 15 Nov 2024 12:01:49 +0100 Subject: [PATCH 11/24] [Enhancement #520] Fix AssetHistory tests with debounce handling. --- .../__tests__/AssetHistory.test.tsx | 21 +++++++------------ 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/src/component/changetracking/__tests__/AssetHistory.test.tsx b/src/component/changetracking/__tests__/AssetHistory.test.tsx index b0f31fdb..cbd8ffbd 100644 --- a/src/component/changetracking/__tests__/AssetHistory.test.tsx +++ b/src/component/changetracking/__tests__/AssetHistory.test.tsx @@ -26,6 +26,7 @@ describe("AssetHistory", () => { mockDispatch = jest.fn(); jest.spyOn(Redux, "useDispatch").mockReturnValue(mockDispatch); jest.spyOn(AsyncActions, "loadHistory"); + jest.useFakeTimers(); }); it("loads asset history on mount", async () => { @@ -34,8 +35,12 @@ describe("AssetHistory", () => { mountWithIntl(); await act(async () => { await flushPromises(); + jest.runAllTimers(); }); - expect(AsyncActions.loadHistory).toHaveBeenCalledWith(asset); + expect(AsyncActions.loadHistory).toHaveBeenCalledWith( + asset, + expect.anything() + ); }); it("renders table with history records when they are available", async () => { @@ -54,21 +59,9 @@ describe("AssetHistory", () => { ); await act(async () => { await flushPromises(); + jest.runAllTimers(); }); wrapper.update(); expect(wrapper.exists(Table)).toBeTruthy(); }); - - it("shows notice about empty history when no records are found", async () => { - (mockDispatch as jest.Mock).mockResolvedValue([]); - const asset = Generator.generateTerm(); - const wrapper = mountWithIntl( - - ); - await act(async () => { - await flushPromises(); - }); - wrapper.update(); - expect(wrapper.exists("#history-empty-notice")).toBeTruthy(); - }); }); From 776e98f43f6da257563d9d3883a08e6e4c2d4e4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Ka=C5=88ka?= Date: Fri, 15 Nov 2024 13:59:49 +0100 Subject: [PATCH 12/24] [Enhancement #520] Fix RemoveVocabularyDialog tests using mocked dispatch and fix callback dependencies when using debounce --- src/component/changetracking/AssetHistory.tsx | 38 ++++++++++++------- .../vocabulary/TermChangeFrequencyUI.tsx | 21 +++++----- .../__tests__/RemoveVocabularyDialog.test.tsx | 15 ++++++-- 3 files changed, 46 insertions(+), 28 deletions(-) diff --git a/src/component/changetracking/AssetHistory.tsx b/src/component/changetracking/AssetHistory.tsx index d4883032..c4aced29 100644 --- a/src/component/changetracking/AssetHistory.tsx +++ b/src/component/changetracking/AssetHistory.tsx @@ -1,5 +1,5 @@ import * as React from "react"; -import { useCallback, useState } from "react"; +import { useRef, useState } from "react"; import Asset, { AssetData } from "../../model/Asset"; import ChangeRecord from "../../model/changetracking/ChangeRecord"; import { Table } from "reactstrap"; @@ -33,7 +33,7 @@ export const AssetHistory: React.FC = ({ asset }) => { const [filterAuthor, setFilterAuthor] = useState(""); const [filterType, setFilterType] = useState(""); const [filterAttribute, setFilterAttribute] = useState(""); - const loadHistoryActionDebounced = useCallback( + const loadHistoryActionDebounced = useRef( debounce( ( asset: Asset, @@ -41,8 +41,7 @@ export const AssetHistory: React.FC = ({ asset }) => { cb: (records?: ChangeRecord[]) => void ) => dispatch(loadHistoryAction(asset, filterData)).then(cb), Constants.INPUT_DEBOUNCE_WAIT_TIME - ), - [dispatch] + ) ); React.useEffect(() => { @@ -64,24 +63,35 @@ export const AssetHistory: React.FC = ({ asset }) => { types: asset.types, }; const snapshotTimeCreated = Date.parse(asset.snapshotCreated()!); - loadHistoryActionDebounced(modifiedAsset as Asset, filter, (recs) => { - if (recs) { - //Show history which is relevant to the snapshot - const filteredRecs = recs.filter( - (r) => Date.parse(r.timestamp) < snapshotTimeCreated - ); - setRecords(filteredRecs); + loadHistoryActionDebounced.current( + modifiedAsset as Asset, + filter, + (recs) => { + if (recs) { + //Show history which is relevant to the snapshot + const filteredRecs = recs.filter( + (r) => Date.parse(r.timestamp) < snapshotTimeCreated + ); + setRecords(filteredRecs); + } } - }); + ); } else { - loadHistoryActionDebounced(asset, filter, (recs) => { + loadHistoryActionDebounced.current(asset, filter, (recs) => { if (recs) { setRecords(recs); } }); } } - }, [asset, dispatch, filterAuthor, filterType, filterAttribute]); + }, [ + asset, + dispatch, + filterAuthor, + filterType, + filterAttribute, + loadHistoryActionDebounced, + ]); if (!records) { return ; } diff --git a/src/component/vocabulary/TermChangeFrequencyUI.tsx b/src/component/vocabulary/TermChangeFrequencyUI.tsx index a1d41dba..c687151d 100644 --- a/src/component/vocabulary/TermChangeFrequencyUI.tsx +++ b/src/component/vocabulary/TermChangeFrequencyUI.tsx @@ -1,5 +1,5 @@ import * as React from "react"; -import { useCallback, useEffect, useState } from "react"; +import { useEffect, useRef, useState } from "react"; import Chart from "react-apexcharts"; import { Col, Row, Table } from "reactstrap"; import { useI18n } from "../hook/useI18n"; @@ -81,23 +81,24 @@ const TermChangeFrequencyUI: React.FC = ({ const [filterTerm, setFilterTerm] = useState(""); const [filterType, setFilterType] = useState(""); const [filterAttribute, setFilterAttribute] = useState(""); - const applyFilterDebounced = useCallback( - debounce( - (filterData: VocabularyContentChangeFilterData) => - applyFilter(filterData), - Constants.INPUT_DEBOUNCE_WAIT_TIME - ), - [applyFilter] + const applyFilterDebounced = useRef( + debounce(applyFilter, Constants.INPUT_DEBOUNCE_WAIT_TIME) ); useEffect(() => { - applyFilterDebounced({ + applyFilterDebounced.current({ author: filterAuthor, term: filterTerm, type: filterType, attribute: filterAttribute, }); - }, [filterAuthor, filterTerm, filterType, filterAttribute]); + }, [ + filterAuthor, + filterTerm, + filterType, + filterAttribute, + applyFilterDebounced, + ]); if (!aggregatedRecords) { return
 
; diff --git a/src/component/vocabulary/__tests__/RemoveVocabularyDialog.test.tsx b/src/component/vocabulary/__tests__/RemoveVocabularyDialog.test.tsx index 90af98bb..328b6c23 100644 --- a/src/component/vocabulary/__tests__/RemoveVocabularyDialog.test.tsx +++ b/src/component/vocabulary/__tests__/RemoveVocabularyDialog.test.tsx @@ -31,7 +31,8 @@ jest.mock("../../../action/AsyncVocabularyActions", () => ({ jest.mock("../../../action/AsyncActions", () => ({ ...jest.requireActual("../../../action/AsyncActions"), - loadTermByIri: jest.fn().mockResolvedValue(null), + loadTermByIri: () => () => Promise.resolve(null), + getLabel: () => () => Promise.resolve(null), })); describe("RemoveVocabularyDialog", () => { @@ -54,9 +55,15 @@ describe("RemoveVocabularyDialog", () => { (getVocabularyRelations as jest.Mock).mockResolvedValue([]); (getVocabularyTermsRelations as jest.Mock).mockResolvedValue([]); - (useDispatch as jest.Mock).mockReturnValue( - (value: any) => value || Promise.resolve() - ); + (useDispatch as jest.Mock).mockReturnValue((arg: any) => { + if (arg instanceof Promise) { + return arg; + } + if (arg instanceof Function) { + return arg(); + } + return arg; + }); Ajax.get = jest.fn().mockResolvedValue(null); From f5d8d4b8d5e99f19d3daf5c452b54221780e8fd7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 15 Nov 2024 17:18:35 +0000 Subject: [PATCH 13/24] Bump @types/js-cookie from 3.0.3 to 3.0.6 Bumps [@types/js-cookie](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/js-cookie) from 3.0.3 to 3.0.6. - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/js-cookie) --- updated-dependencies: - dependency-name: "@types/js-cookie" dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- package-lock.json | 14 +++++++------- package.json | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/package-lock.json b/package-lock.json index cd5f7b1a..61c6e863 100644 --- a/package-lock.json +++ b/package-lock.json @@ -73,7 +73,7 @@ "@redux-devtools/extension": "^3.2.5", "@types/enzyme": "^3.10.13", "@types/jest": "^27.4.1", - "@types/js-cookie": "^3.0.3", + "@types/js-cookie": "^3.0.6", "@types/lodash": "^4.17.10", "@types/luxon": "^3.4.2", "@types/node": "^18.11.17", @@ -3764,9 +3764,9 @@ } }, "node_modules/@types/js-cookie": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@types/js-cookie/-/js-cookie-3.0.3.tgz", - "integrity": "sha512-Xe7IImK09HP1sv2M/aI+48a20VX+TdRJucfq4vfRVy6nWN8PYPOEnlMRSgxJAgYQIXJVL8dZ4/ilAM7dWNaOww==", + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/js-cookie/-/js-cookie-3.0.6.tgz", + "integrity": "sha512-wkw9yd1kEXOPnvEeEV1Go1MmxtBJL0RR79aOTAApecWFVu7w0NNXNqhcWgvw2YgZDYadliXkl14pa3WXw5jlCQ==", "dev": true }, "node_modules/@types/json-schema": { @@ -20661,9 +20661,9 @@ } }, "@types/js-cookie": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@types/js-cookie/-/js-cookie-3.0.3.tgz", - "integrity": "sha512-Xe7IImK09HP1sv2M/aI+48a20VX+TdRJucfq4vfRVy6nWN8PYPOEnlMRSgxJAgYQIXJVL8dZ4/ilAM7dWNaOww==", + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/js-cookie/-/js-cookie-3.0.6.tgz", + "integrity": "sha512-wkw9yd1kEXOPnvEeEV1Go1MmxtBJL0RR79aOTAApecWFVu7w0NNXNqhcWgvw2YgZDYadliXkl14pa3WXw5jlCQ==", "dev": true }, "@types/json-schema": { diff --git a/package.json b/package.json index 58e4714a..955faa45 100644 --- a/package.json +++ b/package.json @@ -83,7 +83,7 @@ "@redux-devtools/extension": "^3.2.5", "@types/enzyme": "^3.10.13", "@types/jest": "^27.4.1", - "@types/js-cookie": "^3.0.3", + "@types/js-cookie": "^3.0.6", "@types/lodash": "^4.17.10", "@types/luxon": "^3.4.2", "@types/node": "^18.11.17", From d6db8f1bda056d70520c2a717b74a25cfd2a4a50 Mon Sep 17 00:00:00 2001 From: Martin Ledvinka Date: Tue, 19 Nov 2024 10:11:38 +0100 Subject: [PATCH 14/24] [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 3268bfbd..4da09f10 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 3f375228..c20e4d70 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.", From 2f593d1a49e5fabb1a3f31c20e7a4adfe2a4a04a Mon Sep 17 00:00:00 2001 From: Martin Ledvinka Date: Wed, 20 Nov 2024 10:49:52 +0100 Subject: [PATCH 15/24] [Enhancement #553] Show term occurrence with label in language of the text analysis record (or file language). --- src/component/annotator/Annotation.tsx | 2 ++ src/component/annotator/Annotator.tsx | 6 +++- src/component/annotator/AnnotatorContent.tsx | 4 +++ .../annotator/TermDefinitionAnnotation.tsx | 2 ++ .../TermDefinitionAnnotationView.tsx | 3 +- .../annotator/TermOccurrenceAnnotation.tsx | 2 ++ .../TermOccurrenceAnnotationView.tsx | 3 +- .../annotator/__tests__/Annotator.test.tsx | 33 +++++++++++++++++++ src/component/file/FileContentDetail.tsx | 2 ++ src/component/resource/ResourceFileDetail.tsx | 2 ++ src/model/TextAnalysisRecord.ts | 4 +++ 11 files changed, 60 insertions(+), 3 deletions(-) diff --git a/src/component/annotator/Annotation.tsx b/src/component/annotator/Annotation.tsx index f7fd899c..98fbaedb 100644 --- a/src/component/annotator/Annotation.tsx +++ b/src/component/annotator/Annotation.tsx @@ -39,6 +39,7 @@ interface AnnotationProps extends AnnotationSpanProps { accessLevel: AccessLevel; highlight?: boolean; filter: AnnotatorLegendFilter; + language?: string; } interface AnnotationState { @@ -316,6 +317,7 @@ export class Annotation extends React.Component< onToggleDetailOpen={this.toggleOpenDetail} onClose={this.onCloseDetail} accessLevel={this.props.accessLevel} + language={this.props.language} /> ); } diff --git a/src/component/annotator/Annotator.tsx b/src/component/annotator/Annotator.tsx index d4cfa446..44942877 100644 --- a/src/component/annotator/Annotator.tsx +++ b/src/component/annotator/Annotator.tsx @@ -66,6 +66,7 @@ interface AnnotatorProps extends HasI18n { user: User; file: File; vocabulary: Vocabulary; + annotationLanguage?: string; onUpdate: (newHtml: string) => void; setAnnotatorLegendFilter: ( @@ -625,7 +626,7 @@ export class Annotator extends React.Component { 0, + "annotator-header-scrolled": window.scrollY > 0, })} actions={[ { onRemove={this.onRemove} onResetSticky={this.resetStickyAnnotationId} highlightedTerm={this.state.highlightedTerm} + annotationLanguage={ + this.props.annotationLanguage || this.props.file.language + } /> diff --git a/src/component/annotator/AnnotatorContent.tsx b/src/component/annotator/AnnotatorContent.tsx index f54a6bbc..77b5e233 100644 --- a/src/component/annotator/AnnotatorContent.tsx +++ b/src/component/annotator/AnnotatorContent.tsx @@ -22,6 +22,7 @@ interface AnnotatorContentProps { content: DomHandlerNode[]; accessLevel: AccessLevel; // The level of access rights the current user has highlightedTerm: TermData | null; + annotationLanguage?: string; onRemove: (annotationId: string | string[]) => void; onUpdate: (annotationSpan: AnnotationSpanProps, term: Term | null) => void; @@ -64,6 +65,7 @@ const AnnotatorContent: React.FC = (props) => { onCreateTerm, accessLevel, highlightedTerm, + annotationLanguage, } = props; // Using memoization to skip processing and re-rendering of the content DOM in case it hasn't changed @@ -112,6 +114,7 @@ const AnnotatorContent: React.FC = (props) => { highlightedTerm !== null && elem.attribs.resource === highlightedTerm.iri } + language={annotationLanguage} {...attribs} > {children} @@ -142,6 +145,7 @@ const AnnotatorContent: React.FC = (props) => { onCreateTerm, accessLevel, highlightedTerm, + annotationLanguage, ]); return ( diff --git a/src/component/annotator/TermDefinitionAnnotation.tsx b/src/component/annotator/TermDefinitionAnnotation.tsx index 4a175f20..16077901 100644 --- a/src/component/annotator/TermDefinitionAnnotation.tsx +++ b/src/component/annotator/TermDefinitionAnnotation.tsx @@ -20,6 +20,7 @@ interface TermDefinitionAnnotationProps { text: string; isOpen: boolean; accessLevel: AccessLevel; + language?: string; onRemove: () => void; onSelectTerm: (term: Term | null) => void; @@ -118,6 +119,7 @@ export const TermDefinitionAnnotation: React.FC< term={term} resource={props.resource} textContent={props.text} + language={props.language} /> ); diff --git a/src/component/annotator/TermDefinitionAnnotationView.tsx b/src/component/annotator/TermDefinitionAnnotationView.tsx index 67347f74..fd89063f 100644 --- a/src/component/annotator/TermDefinitionAnnotationView.tsx +++ b/src/component/annotator/TermDefinitionAnnotationView.tsx @@ -7,6 +7,7 @@ interface TermDefinitionAnnotationViewProps { term?: Term | null; resource?: string; textContent: string; + language?: string; } const TermDefinitionAnnotationView: React.FC< @@ -20,7 +21,7 @@ const TermDefinitionAnnotationView: React.FC< {i18n("annotation.definition.term")} - + diff --git a/src/component/annotator/TermOccurrenceAnnotation.tsx b/src/component/annotator/TermOccurrenceAnnotation.tsx index e95594d2..73c9e989 100644 --- a/src/component/annotator/TermOccurrenceAnnotation.tsx +++ b/src/component/annotator/TermOccurrenceAnnotation.tsx @@ -22,6 +22,7 @@ interface TermOccurrenceAnnotationProps { annotationOrigin: string; isOpen: boolean; accessLevel: AccessLevel; + language?: string; onRemove: () => void; onSelectTerm: (term: Term | null) => void; @@ -129,6 +130,7 @@ export const TermOccurrenceAnnotation: React.FC< score={props.score} resource={props.resource} annotationClass={props.annotationClass} + language={props.language} /> ); diff --git a/src/component/annotator/TermOccurrenceAnnotationView.tsx b/src/component/annotator/TermOccurrenceAnnotationView.tsx index 235c31d2..ace47c93 100644 --- a/src/component/annotator/TermOccurrenceAnnotationView.tsx +++ b/src/component/annotator/TermOccurrenceAnnotationView.tsx @@ -9,6 +9,7 @@ interface TermOccurrenceAnnotationViewProps { score?: string; resource?: string; annotationClass: string; + language?: string; } const TermOccurrenceAnnotationView: React.FC< @@ -25,7 +26,7 @@ const TermOccurrenceAnnotationView: React.FC< {i18n("annotation.term.assigned-occurrence.termLabel")} - + diff --git a/src/component/annotator/__tests__/Annotator.test.tsx b/src/component/annotator/__tests__/Annotator.test.tsx index 99226224..402f736c 100644 --- a/src/component/annotator/__tests__/Annotator.test.tsx +++ b/src/component/annotator/__tests__/Annotator.test.tsx @@ -190,6 +190,39 @@ describe("Annotator", () => { ); }); + it("passes file language to content rendering", () => { + file.language = "en"; + const wrapper = shallow( + + ); + const contentRenderer = wrapper.find(AnnotatorContent); + expect(contentRenderer.props().annotationLanguage).toEqual(file.language); + }); + + it("passes provided annotation language to content rendering", () => { + file.language = "en"; + const wrapper = shallow( + + ); + const contentRenderer = wrapper.find(AnnotatorContent); + expect(contentRenderer.props().annotationLanguage).toEqual("cs"); + }); + describe("on mount", () => { const selector: TextQuoteSelector = { exactMatch: "test-term", diff --git a/src/component/file/FileContentDetail.tsx b/src/component/file/FileContentDetail.tsx index faa83c4b..4393e567 100644 --- a/src/component/file/FileContentDetail.tsx +++ b/src/component/file/FileContentDetail.tsx @@ -27,6 +27,7 @@ interface FileDetailProvidedProps { iri: IRI; vocabularyIri: IRI; scrollTo?: TextQuoteSelector; // Selector of an annotation to scroll to (and highlight) after rendering + annotationLanguage?: string; } interface FileDetailOwnProps extends HasI18n { @@ -129,6 +130,7 @@ export class FileContentDetail extends React.Component< initialHtml={this.props.fileContent} scrollTo={this.props.scrollTo} onUpdate={this.onUpdate} + annotationLanguage={this.props.annotationLanguage} /> ); diff --git a/src/component/resource/ResourceFileDetail.tsx b/src/component/resource/ResourceFileDetail.tsx index 249729ee..3b0c1eb3 100644 --- a/src/component/resource/ResourceFileDetail.tsx +++ b/src/component/resource/ResourceFileDetail.tsx @@ -44,6 +44,7 @@ type ResourceFileDetailProps = StoreStateProps & interface ResourceFileDetailState { vocabularyIri?: IRI | null; scrollToSelector?: TextQuoteSelector; + annotationLanguage?: string; } export class ResourceFileDetail extends React.Component< @@ -139,6 +140,7 @@ export class ResourceFileDetail extends React.Component< if (res) { this.setState({ vocabularyIri: VocabularyUtils.create(res.vocabularies[0].iri!), + annotationLanguage: res.language, }); } else { this.setState({ vocabularyIri: null }); diff --git a/src/model/TextAnalysisRecord.ts b/src/model/TextAnalysisRecord.ts index de1c1efd..62b16453 100644 --- a/src/model/TextAnalysisRecord.ts +++ b/src/model/TextAnalysisRecord.ts @@ -6,6 +6,7 @@ import VocabularyUtils from "../util/VocabularyUtils"; const ctx = { vocabularies: `${VocabularyUtils.NS_TERMIT}m\u00e1-slovn\u00edk-pro-anal\u00fdzu`, analyzedResource: `${VocabularyUtils.NS_TERMIT}m\u00e1-analyzovan\u00fd-zdroj`, + language: VocabularyUtils.DC_LANGUAGE, }; export const CONTEXT = Object.assign({}, ctx, RESOURCE_CONTEXT); @@ -15,6 +16,7 @@ export interface TextAnalysisRecordData { analyzedResource: ResourceData; vocabularies: AssetData[]; created: string; + language?: string; } export class TextAnalysisRecord implements TextAnalysisRecordData { @@ -22,11 +24,13 @@ export class TextAnalysisRecord implements TextAnalysisRecordData { public readonly analyzedResource: ResourceData; public readonly vocabularies: AssetData[]; public readonly created: string; + public readonly language?: string; constructor(data: TextAnalysisRecordData) { this.iri = data.iri; this.analyzedResource = data.analyzedResource; this.created = data.created; + this.language = data.language; this.vocabularies = Utils.sanitizeArray(data.vocabularies); } } From fe71d6c097d9703d4d5583ce51faa2848a2cef51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Ka=C5=88ka?= Date: Sun, 24 Nov 2024 12:22:46 +0100 Subject: [PATCH 16/24] [Enhancement #520] Update VocabularyContentChangeFilterData type attribute to changeType --- src/component/changetracking/AssetHistory.tsx | 2 +- src/component/vocabulary/TermChangeFrequency.tsx | 2 +- src/component/vocabulary/TermChangeFrequencyUI.tsx | 2 +- src/model/filter/VocabularyContentChangeFilterData.ts | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/component/changetracking/AssetHistory.tsx b/src/component/changetracking/AssetHistory.tsx index c4aced29..03eb10c9 100644 --- a/src/component/changetracking/AssetHistory.tsx +++ b/src/component/changetracking/AssetHistory.tsx @@ -49,7 +49,7 @@ export const AssetHistory: React.FC = ({ asset }) => { const filter = { author: filterAuthor, term: "", - type: filterType, + changeType: filterType, attribute: filterAttribute, }; diff --git a/src/component/vocabulary/TermChangeFrequency.tsx b/src/component/vocabulary/TermChangeFrequency.tsx index 5f73aa3d..168a5042 100644 --- a/src/component/vocabulary/TermChangeFrequency.tsx +++ b/src/component/vocabulary/TermChangeFrequency.tsx @@ -35,7 +35,7 @@ const TermChangeFrequency: React.FC = (props) => { const [filterData, setFilterData] = useState({ term: "", - type: "", + changeType: "", attribute: "", author: "", }); diff --git a/src/component/vocabulary/TermChangeFrequencyUI.tsx b/src/component/vocabulary/TermChangeFrequencyUI.tsx index c687151d..b5fcc6d7 100644 --- a/src/component/vocabulary/TermChangeFrequencyUI.tsx +++ b/src/component/vocabulary/TermChangeFrequencyUI.tsx @@ -89,7 +89,7 @@ const TermChangeFrequencyUI: React.FC = ({ applyFilterDebounced.current({ author: filterAuthor, term: filterTerm, - type: filterType, + changeType: filterType, attribute: filterAttribute, }); }, [ diff --git a/src/model/filter/VocabularyContentChangeFilterData.ts b/src/model/filter/VocabularyContentChangeFilterData.ts index 2d2a9f2f..b399fd9b 100644 --- a/src/model/filter/VocabularyContentChangeFilterData.ts +++ b/src/model/filter/VocabularyContentChangeFilterData.ts @@ -3,14 +3,14 @@ import VocabularyUtils from "../../util/VocabularyUtils"; export interface VocabularyContentChangeFilterData { author: string; term: string; - type: string; + changeType: string; attribute: string; } export function getChangeTypeUri( filterData: VocabularyContentChangeFilterData ): string { - switch (filterData.type) { + switch (filterData.changeType) { case "history.type.persist": return VocabularyUtils.PERSIST_EVENT; case "history.type.update": From 82fcac8905dbb28348285d9a60d8ec5725522ace Mon Sep 17 00:00:00 2001 From: Martin Ledvinka Date: Mon, 25 Nov 2024 16:22:22 +0100 Subject: [PATCH 17/24] [Fix] Respect selected label locale when sorting terms in a list. --- src/component/term/RelatedTermsList.tsx | 8 ++++++-- src/component/term/TermList.tsx | 8 +++++--- src/component/term/TermMetadataEdit.tsx | 3 ++- src/model/Term.ts | 19 +++++++++++++++---- src/model/__tests__/Term.test.ts | 21 +++++++++++++++++++++ 5 files changed, 49 insertions(+), 10 deletions(-) diff --git a/src/component/term/RelatedTermsList.tsx b/src/component/term/RelatedTermsList.tsx index 422459d2..0b46ef0f 100644 --- a/src/component/term/RelatedTermsList.tsx +++ b/src/component/term/RelatedTermsList.tsx @@ -12,6 +12,7 @@ import { ThunkDispatch } from "../../util/Types"; import VocabularyNameBadgeButton from "../vocabulary/VocabularyNameBadgeButton"; import TermItState from "../../model/TermItState"; import { createTermNonTerminalStateMatcher } from "./TermTreeSelectHelper"; +import { getShortLocale } from "../../util/IntlUtil"; interface RelatedTermsListProps { term: Term; @@ -33,8 +34,11 @@ const RelatedTermsList: React.FC = (props) => { }, [dispatch, term.iri, term.vocabulary]); const terms = React.useMemo( () => - Term.consolidateRelatedAndRelatedMatch(term).filter(terminalStateFilter), - [term, terminalStateFilter] + Term.consolidateRelatedAndRelatedMatch( + term, + getShortLocale(language) + ).filter(terminalStateFilter), + [language, term, terminalStateFilter] ); return ( diff --git a/src/component/term/TermList.tsx b/src/component/term/TermList.tsx index 0b51343e..2975c39c 100644 --- a/src/component/term/TermList.tsx +++ b/src/component/term/TermList.tsx @@ -1,5 +1,5 @@ import * as React from "react"; -import Term, { termComparator, TermInfo } from "../../model/Term"; +import Term, { createTermComparator, TermInfo } from "../../model/Term"; // @ts-ignore import { Col, Label, List, Row } from "reactstrap"; import TermLink from "./TermLink"; @@ -16,7 +16,9 @@ interface TermListProps { vocabularyIri?: string; language: string; - addonBeforeRenderer?: (t: Term | TermInfo) => JSX.Element | undefined | null; // An element to render before the term link + addonBeforeRenderer?: ( + t: Term | TermInfo + ) => React.JSX.Element | undefined | null; // An element to render before the term link } const TermList: React.FC = (props) => { @@ -27,7 +29,7 @@ const TermList: React.FC = (props) => { ); const terminalStateFilter = createTermNonTerminalStateMatcher(terminalStates); const toRender = Utils.sanitizeArray(terms).filter(terminalStateFilter); - toRender.sort(termComparator); + toRender.sort(createTermComparator(language)); return ( diff --git a/src/component/term/TermMetadataEdit.tsx b/src/component/term/TermMetadataEdit.tsx index 0d203227..46f0cb1a 100644 --- a/src/component/term/TermMetadataEdit.tsx +++ b/src/component/term/TermMetadataEdit.tsx @@ -387,7 +387,8 @@ export class TermMetadataEdit extends React.Component< term={this.props.term} vocabularyIri={this.props.term.vocabulary?.iri!} selected={Term.consolidateRelatedAndRelatedMatch( - this.state + this.state, + this.props.language )} onChange={this.onRelatedChange} language={language} diff --git a/src/model/Term.ts b/src/model/Term.ts index d13139e3..58d8cac5 100644 --- a/src/model/Term.ts +++ b/src/model/Term.ts @@ -113,8 +113,18 @@ export interface TermInfo { types?: string[]; } -export function termComparator(a: TermInfo | TermData, b: TermInfo | TermData) { - return getLocalized(a.label).localeCompare(getLocalized(b.label)); +/** + * Creates a localized Term comparator. + * + * I.e., the comparator uses term labels in the specified language and compares them based on the specified language/locale. + * @param lang Language (locale) for comparison, e.g., en, cs + */ +export function createTermComparator(lang?: string) { + return (a: TermInfo | TermData, b: TermInfo | TermData) => { + const aLabel = (getLocalized(a.label, lang) || "").toLowerCase(); + const bLabel = (getLocalized(b.label, lang) || "").toLowerCase(); + return aLabel.localeCompare(bLabel, lang); + }; } declare type TermMap = { [key: string]: Term }; @@ -305,7 +315,8 @@ export default class Term } public static consolidateRelatedAndRelatedMatch( - term: Term | TermData + term: Term | TermData, + lang?: string ): TermInfo[] { const result = [...Utils.sanitizeArray(term.relatedTerms)]; for (let rt of Utils.sanitizeArray(term.relatedMatchTerms)) { @@ -313,7 +324,7 @@ export default class Term result.push(rt); } } - result.sort(termComparator); + result.sort(createTermComparator(lang)); return result; } } diff --git a/src/model/__tests__/Term.test.ts b/src/model/__tests__/Term.test.ts index c0005a73..e2b7d50b 100644 --- a/src/model/__tests__/Term.test.ts +++ b/src/model/__tests__/Term.test.ts @@ -368,6 +368,27 @@ describe("Term tests", () => { ...t.relatedTerms, ]); }); + + it("sorts terms by label respecting specified language", () => { + const t = new Term(termData); + const t1Uri = Generator.generateUri(); + const t2Uri = Generator.generateUri(); + t.relatedTerms = [ + { + iri: t1Uri, + label: langString("chalupa", "cs"), + vocabulary: { iri: t.vocabulary!.iri }, + }, + { + iri: t2Uri, + label: langString("činitel", "cs"), + vocabulary: { iri: t.vocabulary!.iri }, + }, + ]; + const result = Term.consolidateRelatedAndRelatedMatch(t, "cs"); + expect(result[0].iri).toEqual(t2Uri); + expect(result[1].iri).toEqual(t1Uri); + }); }); describe("isSnapshot", () => { From 74feb5b952742f3a5b2ace69ad04543c9d648406 Mon Sep 17 00:00:00 2001 From: Martin Ledvinka Date: Mon, 25 Nov 2024 16:23:36 +0100 Subject: [PATCH 18/24] [Ref] Minor code cleanup - remove unused method, fix test warning about act. --- .../file/__tests__/CreateFileMetadata.test.tsx | 11 +++++++---- src/util/Utils.ts | 13 ------------- 2 files changed, 7 insertions(+), 17 deletions(-) diff --git a/src/component/resource/file/__tests__/CreateFileMetadata.test.tsx b/src/component/resource/file/__tests__/CreateFileMetadata.test.tsx index 61fef673..866062b4 100644 --- a/src/component/resource/file/__tests__/CreateFileMetadata.test.tsx +++ b/src/component/resource/file/__tests__/CreateFileMetadata.test.tsx @@ -4,6 +4,7 @@ import { mountWithIntl } from "../../../../__tests__/environment/Environment"; import CreateFileMetadata from "../CreateFileMetadata"; import { intlFunctions } from "../../../../__tests__/environment/IntlUtil"; import UploadFile from "../UploadFile"; +import { act } from "react-dom/test-utils"; jest.mock("../../../../util/Ajax", () => { const originalModule = jest.requireActual("../../../../util/Ajax"); @@ -41,10 +42,12 @@ describe("CreateFileMetadata", () => { {...intlFunctions()} /> ); - wrapper - .find(UploadFile) - .props() - .setFile(file as File); + act(() => { + 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/util/Utils.ts b/src/util/Utils.ts index f8a16440..e1134388 100644 --- a/src/util/Utils.ts +++ b/src/util/Utils.ts @@ -3,8 +3,6 @@ */ import Asset, { HasLabel, HasTypes } from "../model/Asset"; import VocabularyUtils, { IRI, IRIImpl } from "./VocabularyUtils"; -import { match } from "react-router"; -import { Location } from "history"; import AppNotification, { AssetUpdateNotification, } from "../model/AppNotification"; @@ -90,17 +88,6 @@ const Utils = { return params[paramName]; }, - /** - * Extracts asset IRI from the specified route props. - * - * Uses match param {@code name} as fragment value. Namespace is extracted from location search string. - */ - extractAssetIri(routeMatch: match, location: Location) { - const namespace = this.extractQueryParam(location.search, "namespace"); - const normalizedName = routeMatch.params.name; - return { fragment: normalizedName, namespace }; - }, - /** * Ensures that file download using Ajax triggers browser file save mechanism. * From 39625f5ae77dbb1ec0c7c4016bd4caf3dfaf8582 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Ka=C5=88ka?= Date: Sat, 30 Nov 2024 11:17:45 +0100 Subject: [PATCH 19/24] [Enhancement #570] Change vocabulary activity graph - update colors, show all values above zero and add term deletions --- src/component/vocabulary/TermChangeFrequencyUI.tsx | 13 ++++++++++++- src/i18n/cs.ts | 1 + src/i18n/en.ts | 1 + 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/src/component/vocabulary/TermChangeFrequencyUI.tsx b/src/component/vocabulary/TermChangeFrequencyUI.tsx index b5fcc6d7..f3a76612 100644 --- a/src/component/vocabulary/TermChangeFrequencyUI.tsx +++ b/src/component/vocabulary/TermChangeFrequencyUI.tsx @@ -122,6 +122,9 @@ const TermChangeFrequencyUI: React.FC = ({ const termUpdates = aggregatedRecords.filter( (r) => r.types.indexOf(VocabularyUtils.UPDATE_EVENT) !== -1 ); + const termDeletions = aggregatedRecords.filter( + (r) => r.types.indexOf(VocabularyUtils.DELETE_EVENT) !== -1 + ); const options = { chart: { @@ -170,12 +173,20 @@ const TermChangeFrequencyUI: React.FC = ({ { name: i18n("vocabulary.termchanges.updates"), type: "column", - data: termUpdates.map((a) => [a.getDate(), -1 * a.count]), + data: termUpdates.map((a) => [a.getDate(), a.count]), + color: "var(--info)", }, { name: i18n("vocabulary.termchanges.creations"), type: "column", data: termCreations.map((a) => [a.getDate(), a.count]), + color: "var(--primary)", + }, + { + name: i18n("vocabulary.termchanges.deletions"), + type: "column", + data: termDeletions.map((a) => [a.getDate(), a.count]), + color: "var(--danger)", }, ]; return ( diff --git a/src/i18n/cs.ts b/src/i18n/cs.ts index a0aab4ba..86d8c610 100644 --- a/src/i18n/cs.ts +++ b/src/i18n/cs.ts @@ -358,6 +358,7 @@ const cs = { "Textová analýza definic pojmů ve všech slovnících spuštěna.", "vocabulary.termchanges.creations": "Vytvořené pojmy", "vocabulary.termchanges.updates": "Aktualizované pojmy", + "vocabulary.termchanges.deletions": "Smazané pojmy", "vocabulary.termchanges.termcount": "Počet změněných pojmů", "vocabulary.termchanges.loading": "Načítám změny ...", "vocabulary.termchanges.empty": diff --git a/src/i18n/en.ts b/src/i18n/en.ts index 09912feb..81bce900 100644 --- a/src/i18n/en.ts +++ b/src/i18n/en.ts @@ -351,6 +351,7 @@ const en = { "Text analysis of terms' definitions in all vocabularies invoked.", "vocabulary.termchanges.creations": "Created terms", "vocabulary.termchanges.updates": "Updated terms", + "vocabulary.termchanges.deletions": "Deleted terms", "vocabulary.termchanges.termcount": "Changed term count", "vocabulary.termchanges.loading": "Loading changes ...", "vocabulary.termchanges.empty": "No creations/updates of terms found.", From afe82ae8da174181c31e2271bee4dffd09f39adb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Ka=C5=88ka?= Date: Sat, 30 Nov 2024 11:24:51 +0100 Subject: [PATCH 20/24] [Enhancement #570] Reorder series for legend order - create, update, delete --- src/component/vocabulary/TermChangeFrequencyUI.tsx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/component/vocabulary/TermChangeFrequencyUI.tsx b/src/component/vocabulary/TermChangeFrequencyUI.tsx index f3a76612..ac3284fb 100644 --- a/src/component/vocabulary/TermChangeFrequencyUI.tsx +++ b/src/component/vocabulary/TermChangeFrequencyUI.tsx @@ -170,18 +170,18 @@ const TermChangeFrequencyUI: React.FC = ({ }; const series = [ - { - name: i18n("vocabulary.termchanges.updates"), - type: "column", - data: termUpdates.map((a) => [a.getDate(), a.count]), - color: "var(--info)", - }, { name: i18n("vocabulary.termchanges.creations"), type: "column", data: termCreations.map((a) => [a.getDate(), a.count]), color: "var(--primary)", }, + { + name: i18n("vocabulary.termchanges.updates"), + type: "column", + data: termUpdates.map((a) => [a.getDate(), a.count]), + color: "var(--info)", + }, { name: i18n("vocabulary.termchanges.deletions"), type: "column", From d9f208cc3e32ffdb86a93d5ae11cb4091bf801a1 Mon Sep 17 00:00:00 2001 From: Martin Ledvinka Date: Mon, 2 Dec 2024 10:16:55 +0100 Subject: [PATCH 21/24] [Enhancement #581] Implement vocabulary translations import. UI is a part of the vocabulary content import dialog - using tabs to separate the two modes. --- src/action/AsyncImportActions.ts | 9 +- .../vocabulary/VocabularyActions.tsx | 6 +- .../vocabulary/VocabularySummary.tsx | 19 +-- .../importing/ImportBackupOfVocabulary.tsx | 86 ------------- .../importing/ImportTranslationsDialog.tsx | 64 ++++++++++ .../importing/ImportVocabularyDialog.tsx | 4 +- .../importing/LoadVocabularyFromFile.tsx | 116 ++++++++++++++++++ src/i18n/cs.ts | 14 ++- src/i18n/en.ts | 12 +- 9 files changed, 220 insertions(+), 110 deletions(-) delete mode 100644 src/component/vocabulary/importing/ImportBackupOfVocabulary.tsx create mode 100644 src/component/vocabulary/importing/ImportTranslationsDialog.tsx create mode 100644 src/component/vocabulary/importing/LoadVocabularyFromFile.tsx diff --git a/src/action/AsyncImportActions.ts b/src/action/AsyncImportActions.ts index 49f34831..ff798973 100644 --- a/src/action/AsyncImportActions.ts +++ b/src/action/AsyncImportActions.ts @@ -16,11 +16,18 @@ import { Action } from "redux"; import { loadVocabulary } from "./AsyncActions"; import Utils from "../util/Utils"; -export function importIntoExistingVocabulary(vocabularyIri: IRI, data: File) { +export function importIntoExistingVocabulary( + vocabularyIri: IRI, + data: File, + translationsOnly: boolean = false +) { const action = { type: ActionType.IMPORT_VOCABULARY }; const formData = new FormData(); formData.append("file", data, "thesaurus"); formData.append("namespace", vocabularyIri.namespace!); + if (translationsOnly) { + formData.append("translationsOnly", true.toString()); + } return (dispatch: ThunkDispatch) => { dispatch(asyncActionRequest(action, true)); return Ajax.post( diff --git a/src/component/vocabulary/VocabularyActions.tsx b/src/component/vocabulary/VocabularyActions.tsx index e4916d80..ae933930 100644 --- a/src/component/vocabulary/VocabularyActions.tsx +++ b/src/component/vocabulary/VocabularyActions.tsx @@ -12,7 +12,7 @@ import { GoCloudUpload, GoRepoForked, } from "react-icons/go"; -import ImportBackupOfVocabulary from "./importing/ImportBackupOfVocabulary"; +import LoadVocabularyFromFile from "./importing/LoadVocabularyFromFile"; import { FaCamera } from "react-icons/fa"; import Vocabulary from "../../model/Vocabulary"; import IfVocabularyActionAuthorized from "./authorization/IfVocabularyActionAuthorized"; @@ -28,7 +28,7 @@ interface VocabularyActionsProps { vocabulary: Vocabulary; onAnalyze: () => void; onExport: () => void; - onImport: (file: File, rename: Boolean) => Promise; + onImport: (file: File, translationsOnly: boolean) => Promise; onCreateSnapshot: () => void; } @@ -46,7 +46,7 @@ const VocabularyActions: React.FC = ({ return ( <> - setShowImportDialog(false)} diff --git a/src/component/vocabulary/VocabularySummary.tsx b/src/component/vocabulary/VocabularySummary.tsx index 25134161..7abeec36 100644 --- a/src/component/vocabulary/VocabularySummary.tsx +++ b/src/component/vocabulary/VocabularySummary.tsx @@ -71,7 +71,11 @@ interface VocabularySummaryProps ) => Promise; updateVocabulary: (vocabulary: Vocabulary) => Promise; removeVocabulary: (vocabulary: Vocabulary) => Promise; - importSkos: (iri: IRI, file: File) => Promise; + importSkos: ( + iri: IRI, + file: File, + translationsOnly?: boolean + ) => Promise; executeTextAnalysisOnAllTerms: (iri: IRI) => void; createSnapshot: (iri: IRI) => Promise; updateDocument: (document: Document) => Promise; @@ -210,16 +214,13 @@ export class VocabularySummary extends EditableComponent< this.setState({ showSnapshotDialog: !this.state.showSnapshotDialog }); }; - private onImport = (file: File) => + private onImport = (file: File, translationsOnly: boolean) => this.props.importSkos( VocabularyUtils.create(this.props.vocabulary.iri), - file + file, + translationsOnly ); - public onFileAdded = () => { - this.loadVocabulary(); - }; - private onExecuteTextAnalysisOnAllTerms = () => { this.props.executeTextAnalysisOnAllTerms( VocabularyUtils.create(this.props.vocabulary.iri) @@ -370,8 +371,8 @@ export default connect( dispatch(updateVocabulary(vocabulary)), removeVocabulary: (vocabulary: Vocabulary) => dispatch(removeVocabulary(vocabulary)), - importSkos: (iri: IRI, file: File) => - dispatch(importIntoExistingVocabulary(iri, file)), + importSkos: (iri: IRI, file: File, translationsOnly?: boolean) => + dispatch(importIntoExistingVocabulary(iri, file, translationsOnly)), executeTextAnalysisOnAllTerms: (iri: IRI) => dispatch(executeTextAnalysisOnAllTerms(iri)), createSnapshot: (iri: IRI) => dispatch(createVocabularySnapshot(iri)), diff --git a/src/component/vocabulary/importing/ImportBackupOfVocabulary.tsx b/src/component/vocabulary/importing/ImportBackupOfVocabulary.tsx deleted file mode 100644 index 93cc67c9..00000000 --- a/src/component/vocabulary/importing/ImportBackupOfVocabulary.tsx +++ /dev/null @@ -1,86 +0,0 @@ -import React from "react"; -import { Alert, Label, Modal, ModalBody, ModalHeader } from "reactstrap"; -import { useI18n } from "../../hook/useI18n"; -import PromiseTrackingMask from "../../misc/PromiseTrackingMask"; -import { trackPromise } from "react-promise-tracker"; -import { FormattedMessage } from "react-intl"; -import ImportVocabularyDialog from "./ImportVocabularyDialog"; -import { useDispatch, useSelector } from "react-redux"; -import { ThunkDispatch } from "../../../util/Types"; -import { downloadExcelTemplate } from "../../../action/AsyncImportActions"; -import TermItState from "../../../model/TermItState"; - -interface ImportVocabularyProps { - showDialog: boolean; - onImport: (file: File, rename: Boolean) => Promise; - closeDialog: () => void; -} - -export const ImportVocabulary: React.FC = ({ - showDialog, - closeDialog, - onImport, -}) => { - const { i18n } = useI18n(); - const dispatch: ThunkDispatch = useDispatch(); - const onSubmit = (file: File, rename: Boolean) => - trackPromise(onImport(file, rename), "vocabulary-import").then(closeDialog); - const downloadTemplate = () => { - dispatch(downloadExcelTemplate()); - }; - const vocabularyNotEmpty = - (useSelector((state: TermItState) => state.vocabulary.termCount) || 0) > 0; - - return ( - <> - - - {i18n("vocabulary.summary.import.dialog.title")} - - - - -
    -
  • - -
  • -
  • - ( - - {chunks} - - ), - }} - /> -
  • -
- {vocabularyNotEmpty && ( - - - - )} - -
-
- - ); -}; - -export default ImportVocabulary; diff --git a/src/component/vocabulary/importing/ImportTranslationsDialog.tsx b/src/component/vocabulary/importing/ImportTranslationsDialog.tsx new file mode 100644 index 00000000..6ae4ddbc --- /dev/null +++ b/src/component/vocabulary/importing/ImportTranslationsDialog.tsx @@ -0,0 +1,64 @@ +import React from "react"; +import UploadFile from "../../resource/file/UploadFile"; +import { Button, ButtonToolbar, Col, Form, Row } from "reactstrap"; +import { useI18n } from "../../hook/useI18n"; +import { FormattedMessage } from "react-intl"; + +export const ImportTranslationsDialog: React.FC<{ + onSubmit: (file: File) => void; + onCancel: () => void; + onDownloadTemplate: () => void; +}> = ({ onSubmit, onCancel, onDownloadTemplate }) => { + const { i18n } = useI18n(); + const [file, setFile] = React.useState(); + const cannotSubmit = () => !file; + return ( + <> +
+
+ ( + + {chunks} + + ), + }} + /> +
+ + + + + + + + + + + + ); +}; diff --git a/src/component/vocabulary/importing/ImportVocabularyDialog.tsx b/src/component/vocabulary/importing/ImportVocabularyDialog.tsx index 4a4bf019..d28d1a86 100644 --- a/src/component/vocabulary/importing/ImportVocabularyDialog.tsx +++ b/src/component/vocabulary/importing/ImportVocabularyDialog.tsx @@ -7,7 +7,7 @@ import "./ImportVocabularyDialog.scss"; interface ImportVocabularyDialogProps { propKeyPrefix: string; - onCreate: (file: File, rename: Boolean) => any; + onCreate: (file: File, rename: boolean) => any; onCancel: () => void; allowRename?: boolean; } @@ -15,7 +15,7 @@ interface ImportVocabularyDialogProps { const ImportVocabularyDialog = (props: ImportVocabularyDialogProps) => { const { i18n } = useI18n(); const [file, setFile] = useState(); - const [rename, setRename] = useState(false); + const [rename, setRename] = useState(false); const onCreate = () => { if (!file) { diff --git a/src/component/vocabulary/importing/LoadVocabularyFromFile.tsx b/src/component/vocabulary/importing/LoadVocabularyFromFile.tsx new file mode 100644 index 00000000..87266deb --- /dev/null +++ b/src/component/vocabulary/importing/LoadVocabularyFromFile.tsx @@ -0,0 +1,116 @@ +import React, { useState } from "react"; +import { Alert, Modal, ModalBody, ModalHeader } from "reactstrap"; +import { useI18n } from "../../hook/useI18n"; +import PromiseTrackingMask from "../../misc/PromiseTrackingMask"; +import { trackPromise } from "react-promise-tracker"; +import { FormattedMessage } from "react-intl"; +import ImportVocabularyDialog from "./ImportVocabularyDialog"; +import { useDispatch, useSelector } from "react-redux"; +import { ThunkDispatch } from "../../../util/Types"; +import { downloadExcelTemplate } from "../../../action/AsyncImportActions"; +import TermItState from "../../../model/TermItState"; +import Tabs from "../../misc/Tabs"; +import { ImportTranslationsDialog } from "./ImportTranslationsDialog"; + +interface LoadVocabularyFromFileProps { + showDialog: boolean; + onImport: (file: File, translationsOnly: boolean) => Promise; + closeDialog: () => void; +} + +export const LoadVocabularyFromFile: React.FC = ({ + showDialog, + closeDialog, + onImport, +}) => { + const { i18n } = useI18n(); + const dispatch: ThunkDispatch = useDispatch(); + const [selectedTab, setSelectedTab] = useState( + "vocabulary.summary.import.dialog.tab.replaceContent" + ); + const onClose = () => { + closeDialog(); + setSelectedTab("vocabulary.summary.import.dialog.tab.replaceContent"); + }; + const onImportContent = (file: File) => + trackPromise(onImport(file, false), "vocabulary-import").then(onClose); + const onImportTranslations = (file: File) => { + trackPromise(onImport(file, true), "vocabulary-import").then(onClose); + }; + const downloadTemplate = () => { + dispatch(downloadExcelTemplate()); + }; + const vocabularyNotEmpty = + (useSelector((state: TermItState) => state.vocabulary.termCount) || 0) > 0; + + return ( + <> + + + {i18n("vocabulary.summary.import.dialog.title")} + + + + +
+ +
+
    +
  • + +
  • +
  • + ( + + {chunks} + + ), + }} + /> +
  • +
+ {vocabularyNotEmpty && ( + + + + )} + + + ), + "vocabulary.summary.import.dialog.tab.translations": ( + + ), + }} + changeTab={setSelectedTab as (t: string) => void} + /> +
+
+ + ); +}; + +export default LoadVocabularyFromFile; diff --git a/src/i18n/cs.ts b/src/i18n/cs.ts index 86d8c610..cb8a284a 100644 --- a/src/i18n/cs.ts +++ b/src/i18n/cs.ts @@ -305,21 +305,25 @@ const cs = { "vocabulary.summary.export.rdfxml.title": "Export ve formátu RDF/XML.", "vocabulary.summary.export.error": "Nepodařilo se získat data z odpovědi serveru.", - "vocabulary.summary.import.action": "Obnovit ze zálohy", + "vocabulary.summary.import.action": "Nahrát ze souboru", "vocabulary.summary.import.action.tooltip": - "Obnovit slovník ze zálohy ve formátu SKOS či MS Excel", + "Nahrát obsah slovníku ze souboru obsahujícího data ve formátu SKOS či MS Excel", "vocabulary.summary.import.dialog.title": - "Obnova exportované verze slovníku", + "Import obsahu slovníku ze souboru", + "vocabulary.summary.import.dialog.tab.replaceContent": "Nahradit obsah", + "vocabulary.summary.import.dialog.tab.translations": "Importovat překlady", "vocabulary.summary.import.dialog.label": - "Nahrajte vyexportovanou verzi tohoto slovníku ", + "Nahrajte vyexportovanou verzi tohoto slovníku", "vocabulary.summary.import.dialog.skosImport": "Ve formátu SKOS a obsahující jediný skos:ConceptScheme s IRI ve tvaru '<'IRI-TOHOTO-SLOVNÍKU'>'/glosář.", "vocabulary.summary.import.dialog.excelImport": - "MS Excel odpovídající této šabloně", + "Ve formátu MS Excel odpovídající této šabloně", "vocabulary.summary.import.excel.template.tooltip": "Stáhnout šablonu pro MS Excel", "vocabulary.summary.import.nonEmpty.warning": "Slovník není prázdný, stávající data budou přepsána importovanými.", + "vocabulary.summary.import.translations.label": + "Nahrajte soubor ve formátu MS Excel odpovídající této šabloně, ze kterého mají být naimportovány překlady existujících pojmů ve slovníku.", "vocabulary.import.type.skos": "SKOS", "vocabulary.import.type.excel": "MS Excel", "vocabulary.import.action": "Importovat", diff --git a/src/i18n/en.ts b/src/i18n/en.ts index 81bce900..84119adc 100644 --- a/src/i18n/en.ts +++ b/src/i18n/en.ts @@ -297,21 +297,25 @@ const en = { "vocabulary.summary.export.rdfxml.title": "Export to RDF/XML (RDF).", "vocabulary.summary.export.error": "Unable to retrieve exported data from server response.", - "vocabulary.summary.import.action": "Restore from backup", + "vocabulary.summary.import.action": "Load from file", "vocabulary.summary.import.action.tooltip": - "Restore the vocabulary from its previously exported version", + "Load vocabulary data from a file containing data in SKOS or MS Excel format", "vocabulary.summary.import.dialog.title": - "Restore exported vocabulary version", + "Import vocabulary content from file", + "vocabulary.summary.import.dialog.tab.replaceContent": "Replace content", + "vocabulary.summary.import.dialog.tab.translations": "Import translations", "vocabulary.summary.import.dialog.label": "Upload an exported version of this vocabulary", "vocabulary.summary.import.dialog.skosImport": "In the SKOS format and containing a single skos:ConceptScheme with IRI '<'IRI-OF-THIS-VOCABULARY'>'/glosář", "vocabulary.summary.import.dialog.excelImport": - "MS Excel file corresponding to this template", + "As an MS Excel file corresponding to this template", "vocabulary.summary.import.excel.template.tooltip": "Download a MS Excel template", "vocabulary.summary.import.nonEmpty.warning": "Vocabulary is not empty, existing data will be overwritten by the imported.", + "vocabulary.summary.import.translations.label": + "Upload an MS Excel file corresponding to this template from which translations of existing terms in this vocabulary will be imported.", "vocabulary.import.type.skos": "SKOS", "vocabulary.import.type.excel": "MS Excel", "vocabulary.import.action": "Import", From bba7c1f7ac08865a4450b82a000128be777a1985 Mon Sep 17 00:00:00 2001 From: Martin Ledvinka Date: Mon, 2 Dec 2024 15:52:22 +0100 Subject: [PATCH 22/24] [Enhancement #565] Do not reset vocabulary in state when just reloading one. When files in a vocabulary are changed, the vocabulary needs to be reloaded. Ensure vocabulary reload does not cause VocabularyMetadata unmount, which caused switch of vocabulary tab to the default one - glossary. --- src/action/AsyncActions.ts | 3 +- .../vocabulary/VocabularySummary.tsx | 9 ++++-- src/reducer/TermItReducers.ts | 7 +---- src/reducer/__tests__/TermItReducers.test.ts | 30 ------------------- 4 files changed, 9 insertions(+), 40 deletions(-) diff --git a/src/action/AsyncActions.ts b/src/action/AsyncActions.ts index c3e5f581..03592ff0 100644 --- a/src/action/AsyncActions.ts +++ b/src/action/AsyncActions.ts @@ -28,7 +28,7 @@ import * as jsonld from "jsonld"; import Message from "../model/Message"; import MessageType from "../model/MessageType"; import Term, { CONTEXT as TERM_CONTEXT, TermData } from "../model/Term"; -import VocabularyUtils, { IRI } from "../util/VocabularyUtils"; +import VocabularyUtils, { IRI, IRIImpl } from "../util/VocabularyUtils"; import ActionType, { PendingAsyncAction } from "./ActionType"; import Resource, { ResourceData } from "../model/Resource"; import RdfsResource, { @@ -150,6 +150,7 @@ export function createVocabulary(vocabulary: Vocabulary) { export function loadVocabulary(iri: IRI, timestamp?: string) { const action = { type: ActionType.LOAD_VOCABULARY, + iri: IRIImpl.toString(iri), }; return (dispatch: ThunkDispatch, getState: () => TermItState) => { if (isActionRequestPending(getState(), action)) { diff --git a/src/component/vocabulary/VocabularySummary.tsx b/src/component/vocabulary/VocabularySummary.tsx index 7abeec36..92fa2c16 100644 --- a/src/component/vocabulary/VocabularySummary.tsx +++ b/src/component/vocabulary/VocabularySummary.tsx @@ -82,7 +82,6 @@ interface VocabularySummaryProps } export interface VocabularySummaryState extends EditableComponentState { - selectDocumentDialogOpen: boolean; showExportDialog: boolean; showSnapshotDialog: boolean; language: string; @@ -109,7 +108,6 @@ export class VocabularySummary extends EditableComponent< showRemoveDialog: false, showExportDialog: false, showSnapshotDialog: false, - selectDocumentDialogOpen: false, language: resolveInitialLanguage( props.vocabulary, props.locale, @@ -171,6 +169,11 @@ export class VocabularySummary extends EditableComponent< this.props.requestVocabularyValidation(iriFromUrl, this.props.stompClient); }; + public reloadVocabulary = () => { + const iri = VocabularyUtils.create(this.props.vocabulary.iri); + this.props.loadVocabulary(iri); + }; + public setLanguage = (language: string) => { this.setState({ language }); }; @@ -325,7 +328,7 @@ export class VocabularySummary extends EditableComponent< match={this.props.match} language={this.state.language} selectLanguage={this.setLanguage} - onChange={this.loadVocabulary} + onChange={this.reloadVocabulary} /> )} diff --git a/src/reducer/TermItReducers.ts b/src/reducer/TermItReducers.ts index baebede5..fe3abb6f 100644 --- a/src/reducer/TermItReducers.ts +++ b/src/reducer/TermItReducers.ts @@ -126,7 +126,7 @@ function vocabulary( switch (action.type) { case ActionType.LOAD_VOCABULARY: if (action.status === AsyncActionStatus.REQUEST) { - return EMPTY_VOCABULARY; + return (action as any).iri === state.iri ? state : EMPTY_VOCABULARY; } else if (isAsyncSuccess(action)) { return action.payload as Vocabulary; } else { @@ -144,11 +144,6 @@ function vocabulary( return onTermCountLoaded(state, action); case ActionType.LOGOUT: return EMPTY_VOCABULARY; - case ActionType.REMOVE_RESOURCE: - case ActionType.UPDATE_RESOURCE: - case ActionType.CREATE_RESOURCE: // intentional fall-through - // the resource might have been/be related to the vocabulary - return isAsyncSuccess(action) ? EMPTY_VOCABULARY : state; default: return state; } diff --git a/src/reducer/__tests__/TermItReducers.test.ts b/src/reducer/__tests__/TermItReducers.test.ts index 76949d80..13be8dae 100644 --- a/src/reducer/__tests__/TermItReducers.test.ts +++ b/src/reducer/__tests__/TermItReducers.test.ts @@ -350,36 +350,6 @@ describe("Reducers", () => { expect(vocabulary.allImportedVocabularies).toEqual(imports); }); - it("resets vocabulary to empty when resource is removed", () => { - // The removed resource could have been a file from a document related to that vocabulary, in which case - // the vocabulary needs to be reloaded - const action = { type: ActionType.REMOVE_RESOURCE }; - initialState.vocabulary = new Vocabulary({ - label: langString("Test vocabulary"), - iri: Generator.generateUri(), - types: [VocabularyUtils.VOCABULARY], - }); - expect( - reducers(stateToPlainObject(initialState), asyncActionSuccess(action)) - .vocabulary - ).toEqual(EMPTY_VOCABULARY); - }); - - it("resets vocabulary to empty when resource is created", () => { - // The created resource could be a file added to a document related to that vocabulary, in which case - // the vocabulary needs to be reloaded - const action = { type: ActionType.CREATE_RESOURCE }; - initialState.vocabulary = new Vocabulary({ - label: langString("Test vocabulary"), - iri: Generator.generateUri(), - types: [VocabularyUtils.VOCABULARY], - }); - expect( - reducers(stateToPlainObject(initialState), asyncActionSuccess(action)) - .vocabulary - ).toEqual(EMPTY_VOCABULARY); - }); - it("sets term count on vocabulary when it is loaded", () => { initialState.vocabulary = new Vocabulary({ label: langString("Test vocabulary"), From f8d89c2f939a98f1404732bc8f36ce3fbc698f61 Mon Sep 17 00:00:00 2001 From: Martin Ledvinka Date: Mon, 2 Dec 2024 17:03:41 +0100 Subject: [PATCH 23/24] [Enhancement #581] Use separate template file for term translations import. Improve user information for translation import. --- src/action/AsyncImportActions.ts | 7 +++++-- .../vocabulary/importing/ImportTranslationsDialog.tsx | 5 ++++- .../vocabulary/importing/LoadVocabularyFromFile.tsx | 5 ++++- src/i18n/cs.ts | 2 ++ src/i18n/en.ts | 2 ++ 5 files changed, 17 insertions(+), 4 deletions(-) diff --git a/src/action/AsyncImportActions.ts b/src/action/AsyncImportActions.ts index ff798973..a41d28b4 100644 --- a/src/action/AsyncImportActions.ts +++ b/src/action/AsyncImportActions.ts @@ -89,13 +89,16 @@ const processError = ); }; -export function downloadExcelTemplate() { +export function downloadExcelTemplate(translationsOnly: boolean = false) { return (dispatch: ThunkDispatch) => { const action = { type: ActionType.LOAD_EXCEL_TEMPLATE }; dispatch(asyncActionRequest(action, true)); return Ajax.getRaw( `${Constants.API_PREFIX}/vocabularies/import/template`, - responseType("arraybuffer") + responseType("arraybuffer").param( + "translationsOnly", + translationsOnly.toString() + ) ) .then((response) => { Utils.fileDownload( diff --git a/src/component/vocabulary/importing/ImportTranslationsDialog.tsx b/src/component/vocabulary/importing/ImportTranslationsDialog.tsx index 6ae4ddbc..d9efb1a8 100644 --- a/src/component/vocabulary/importing/ImportTranslationsDialog.tsx +++ b/src/component/vocabulary/importing/ImportTranslationsDialog.tsx @@ -1,6 +1,6 @@ import React from "react"; import UploadFile from "../../resource/file/UploadFile"; -import { Button, ButtonToolbar, Col, Form, Row } from "reactstrap"; +import { Alert, Button, ButtonToolbar, Col, Form, Row } from "reactstrap"; import { useI18n } from "../../hook/useI18n"; import { FormattedMessage } from "react-intl"; @@ -34,6 +34,9 @@ export const ImportTranslationsDialog: React.FC<{ }} /> + + + diff --git a/src/component/vocabulary/importing/LoadVocabularyFromFile.tsx b/src/component/vocabulary/importing/LoadVocabularyFromFile.tsx index 87266deb..f0ee0728 100644 --- a/src/component/vocabulary/importing/LoadVocabularyFromFile.tsx +++ b/src/component/vocabulary/importing/LoadVocabularyFromFile.tsx @@ -40,6 +40,9 @@ export const LoadVocabularyFromFile: React.FC = ({ const downloadTemplate = () => { dispatch(downloadExcelTemplate()); }; + const downloadTranslationsTemplate = () => { + dispatch(downloadExcelTemplate(true)); + }; const vocabularyNotEmpty = (useSelector((state: TermItState) => state.vocabulary.termCount) || 0) > 0; @@ -101,7 +104,7 @@ export const LoadVocabularyFromFile: React.FC = ({ ), }} diff --git a/src/i18n/cs.ts b/src/i18n/cs.ts index cb8a284a..a74b61f1 100644 --- a/src/i18n/cs.ts +++ b/src/i18n/cs.ts @@ -324,6 +324,8 @@ const cs = { "Slovník není prázdný, stávající data budou přepsána importovanými.", "vocabulary.summary.import.translations.label": "Nahrajte soubor ve formátu MS Excel odpovídající této šabloně, ze kterého mají být naimportovány překlady existujících pojmů ve slovníku.", + "vocabulary.summary.import.translations.help": + "Stávající data nebudou přepsána.", "vocabulary.import.type.skos": "SKOS", "vocabulary.import.type.excel": "MS Excel", "vocabulary.import.action": "Importovat", diff --git a/src/i18n/en.ts b/src/i18n/en.ts index 84119adc..264ae219 100644 --- a/src/i18n/en.ts +++ b/src/i18n/en.ts @@ -316,6 +316,8 @@ const en = { "Vocabulary is not empty, existing data will be overwritten by the imported.", "vocabulary.summary.import.translations.label": "Upload an MS Excel file corresponding to this template from which translations of existing terms in this vocabulary will be imported.", + "vocabulary.summary.import.translations.help": + "Existing data will not be changed.", "vocabulary.import.type.skos": "SKOS", "vocabulary.import.type.excel": "MS Excel", "vocabulary.import.action": "Import", From 0b0f407c439710b5bcaa8512cb6518e2e0e4e3d7 Mon Sep 17 00:00:00 2001 From: Martin Ledvinka Date: Tue, 3 Dec 2024 18:54:10 +0100 Subject: [PATCH 24/24] [3.4.0] Bump version, update news. --- NEWS.cs.md | 7 +++++++ NEWS.en.md | 7 +++++++ package-lock.json | 4 ++-- package.json | 2 +- 4 files changed, 17 insertions(+), 3 deletions(-) diff --git a/NEWS.cs.md b/NEWS.cs.md index 54aeeb94..3bb50949 100644 --- a/NEWS.cs.md +++ b/NEWS.cs.md @@ -1,3 +1,10 @@ +#### Verze 3.4.0 + +- Přidána podpora pro záznamy o smazání pojmů. Ty jsou zobrazovány i v grafu aktivity slovníku. +- Přidána možnost filtrovat v historii změn. +- Přidána podpora pro ukládání a anotaci souborů v různých jazycích v rámci jednoho slovníku. +- Přidána podpora pro import překladů pojmů ze souboru MS Excel. + #### Verze 3.3.0 - Přidána možnost stáhnout anotovaný soubor bez nepotvrzených výskytů. diff --git a/NEWS.en.md b/NEWS.en.md index d48dc975..428a4a6e 100644 --- a/NEWS.en.md +++ b/NEWS.en.md @@ -1,3 +1,10 @@ +#### Version 3.4.0 + +- Added support for term deletion records. Term deletion events are also displayed in the vocabulary activity diagram. +- Added support for filtering in change history. +- Added support for saving and annotating files in multiple languages. +- Added support for importing term translations from an MS Excel file. + #### Version 3.3.0 - Added the possibility to download annotated file without unconfirmed occurrences. diff --git a/package-lock.json b/package-lock.json index 61c6e863..960eb2e6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "termit-ui", - "version": "3.3.1", + "version": "3.4.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "termit-ui", - "version": "3.3.1", + "version": "3.4.0", "license": "GPL-3.0-only", "dependencies": { "@formatjs/intl-pluralrules": "^5.3.1", diff --git a/package.json b/package.json index 955faa45..b4abb7cc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "termit-ui", - "version": "3.3.1", + "version": "3.4.0", "private": true, "homepage": ".", "license": "GPL-3.0-only",