diff --git a/src/action/AsyncActions.ts b/src/action/AsyncActions.ts index 6db20ef6..c3e5f581 100644 --- a/src/action/AsyncActions.ts +++ b/src/action/AsyncActions.ts @@ -61,6 +61,10 @@ 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 @@ -1130,13 +1134,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 48e766d0..a827996a 100644 --- a/src/action/AsyncVocabularyActions.ts +++ b/src/action/AsyncVocabularyActions.ts @@ -31,6 +31,11 @@ import ChangeRecord, { CONTEXT as CHANGE_RECORD_CONTEXT, } from "../model/changetracking/ChangeRecord"; import AssetFactory from "../util/AssetFactory"; +import { + getChangeTypeUri, + VocabularyContentChangeFilterData, +} from "../model/filter/VocabularyContentChangeFilterData"; +import { getLocalized } from "../model/MultilingualString"; export function loadTermCount(vocabularyIri: IRI) { const action = { type: ActionType.LOAD_TERM_COUNT, vocabularyIri }; @@ -137,6 +142,7 @@ export function loadVocabularyContentChanges(vocabularyIri: IRI) { export function loadVocabularyContentDetailedChanges( vocabularyIri: IRI, + filterData: VocabularyContentChangeFilterData, pageReq: PageRequest ) { const action = { @@ -145,11 +151,16 @@ 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); + } + params = params.param("type", getChangeTypeUri(filterData)); 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( @@ -157,6 +168,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/AssetHistory.tsx b/src/component/changetracking/AssetHistory.tsx index 6c3b3968..03eb10c9 100644 --- a/src/component/changetracking/AssetHistory.tsx +++ b/src/component/changetracking/AssetHistory.tsx @@ -1,4 +1,5 @@ import * as React from "react"; +import { useRef, 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,29 @@ 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 = useRef( + debounce( + ( + asset: Asset, + filterData: VocabularyContentChangeFilterData, + cb: (records?: ChangeRecord[]) => void + ) => dispatch(loadHistoryAction(asset, filterData)).then(cb), + Constants.INPUT_DEBOUNCE_WAIT_TIME + ) + ); + React.useEffect(() => { if (asset.iri !== Constants.EMPTY_ASSET_IRI) { + const filter = { + author: filterAuthor, + term: "", + changeType: filterType, + attribute: filterAttribute, + }; + //Check if vocabulary/term is a snapshot if ( (asset instanceof Term || asset instanceof Vocabulary) && @@ -35,53 +63,100 @@ 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.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 { - dispatch(loadHistoryAction(asset)).then((recs) => { - setRecords(recs); + loadHistoryActionDebounced.current(asset, filter, (recs) => { + if (recs) { + setRecords(recs); + } }); } } - }, [asset, dispatch]); + }, [ + asset, + dispatch, + filterAuthor, + filterType, + filterAttribute, + loadHistoryActionDebounced, + ]); 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 new file mode 100644 index 00000000..9a77aa02 --- /dev/null +++ b/src/component/changetracking/VocabularyContentDeleteRow.tsx @@ -0,0 +1,33 @@ +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 { DeleteRowProps } from "./DeleteRow"; + +export const VocabularyContentDeleteRow: 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 VocabularyContentDeleteRow; 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/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(); - }); }); 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/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/component/vocabulary/TermChangeFrequency.tsx b/src/component/vocabulary/TermChangeFrequency.tsx index 3732dad5..168a5042 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: "", + changeType: "", + 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.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..b5fcc6d7 100644 --- a/src/component/vocabulary/TermChangeFrequencyUI.tsx +++ b/src/component/vocabulary/TermChangeFrequencyUI.tsx @@ -1,4 +1,5 @@ import * as React from "react"; +import { useEffect, useRef, useState } from "react"; import Chart from "react-apexcharts"; import { Col, Row, Table } from "reactstrap"; import { useI18n } from "../hook/useI18n"; @@ -8,8 +9,16 @@ 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"; +import "./TermChangeFrequencyUI.scss"; +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; @@ -17,7 +26,7 @@ interface TermChangeFrequencyUIProps { page: number; setPage: (page: number) => void; pageSize: number; - itemCount: number; + applyFilter: (filterData: VocabularyContentChangeFilterData) => void; } /** @@ -64,10 +73,34 @@ const TermChangeFrequencyUI: React.FC = ({ page, setPage, pageSize, - itemCount, + applyFilter, }) => { const { i18n, locale } = useI18n(); - if (!aggregatedRecords || !changeRecords) { + + const [filterAuthor, setFilterAuthor] = useState(""); + const [filterTerm, setFilterTerm] = useState(""); + const [filterType, setFilterType] = useState(""); + const [filterAttribute, setFilterAttribute] = useState(""); + const applyFilterDebounced = useRef( + debounce(applyFilter, Constants.INPUT_DEBOUNCE_WAIT_TIME) + ); + + useEffect(() => { + applyFilterDebounced.current({ + author: filterAuthor, + term: filterTerm, + changeType: filterType, + attribute: filterAttribute, + }); + }, [ + filterAuthor, + filterTerm, + filterType, + filterAttribute, + applyFilterDebounced, + ]); + + if (!aggregatedRecords) { return
 
; } @@ -147,7 +180,7 @@ const TermChangeFrequencyUI: React.FC = ({ ]; return ( - + @@ -157,29 +190,76 @@ const TermChangeFrequencyUI: React.FC = ({ {i18n("history.whenwho")} {i18n("type.term")} - {i18n("history.type")} - {i18n("history.changedAttribute")} + {i18n("history.type")} + {i18n("history.changedAttribute")} + + + + setFilterAuthor(e.target.value)} + /> + + + setFilterTerm(e.target.value)} + /> + + + + + + setFilterAttribute(e.target.value)} + /> + - {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; + })} - 0}> - - + ); 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); diff --git a/src/i18n/cs.ts b/src/i18n/cs.ts index 4da09f10..a0aab4ba 100644 --- a/src/i18n/cs.ts +++ b/src/i18n/cs.ts @@ -827,6 +827,7 @@ 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", diff --git a/src/i18n/en.ts b/src/i18n/en.ts index c20e4d70..09912feb 100644 --- a/src/i18n/en.ts +++ b/src/i18n/en.ts @@ -819,6 +819,7 @@ 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", 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 new file mode 100644 index 00000000..1eb257d2 --- /dev/null +++ b/src/model/changetracking/DeleteRecord.ts @@ -0,0 +1,21 @@ +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 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..b399fd9b --- /dev/null +++ b/src/model/filter/VocabularyContentChangeFilterData.ts @@ -0,0 +1,22 @@ +import VocabularyUtils from "../../util/VocabularyUtils"; + +export interface VocabularyContentChangeFilterData { + author: string; + term: string; + changeType: string; + attribute: string; +} + +export function getChangeTypeUri( + filterData: VocabularyContentChangeFilterData +): string { + switch (filterData.changeType) { + case "history.type.persist": + return VocabularyUtils.PERSIST_EVENT; + case "history.type.update": + return VocabularyUtils.UPDATE_EVENT; + case "history.type.delete": + return VocabularyUtils.DELETE_EVENT; + } + return ""; +} 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/Utils.ts b/src/util/Utils.ts index b4fd2cb9..f8a16440 100644 --- a/src/util/Utils.ts +++ b/src/util/Utils.ts @@ -368,6 +368,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; 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",