Skip to content

Commit

Permalink
Merge pull request #572 from lukaskabc/lukaskabc/Enhancement-520
Browse files Browse the repository at this point in the history
[Enhancement #520] Change record filtering
  • Loading branch information
ledsoft authored Nov 25, 2024
2 parents 2f593d1 + faba150 commit 8ce480f
Show file tree
Hide file tree
Showing 22 changed files with 449 additions and 93 deletions.
18 changes: 16 additions & 2 deletions src/action/AsyncActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<ChangeRecordData>(
data,
Expand Down
30 changes: 27 additions & 3 deletions src/action/AsyncVocabularyActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
Expand Down Expand Up @@ -137,6 +142,7 @@ export function loadVocabularyContentChanges(vocabularyIri: IRI) {

export function loadVocabularyContentDetailedChanges(
vocabularyIri: IRI,
filterData: VocabularyContentChangeFilterData,
pageReq: PageRequest
) {
const action = {
Expand All @@ -145,18 +151,36 @@ 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<ChangeRecord>(
data,
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));
Expand Down
139 changes: 107 additions & 32 deletions src/component/changetracking/AssetHistory.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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;
Expand All @@ -23,8 +30,29 @@ export const AssetHistory: React.FC<AssetHistoryProps> = ({ asset }) => {
const { i18n } = useI18n();
const dispatch: ThunkDispatch = useDispatch();
const [records, setRecords] = React.useState<null | ChangeRecord[]>(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) &&
Expand All @@ -35,53 +63,100 @@ export const AssetHistory: React.FC<AssetHistoryProps> = ({ 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 <ContainerMask text={i18n("history.loading")} />;
}
if (records.length === 0) {
return (
<div
id="history-empty-notice"
className="additional-metadata-container italics"
>
{i18n("history.empty")}
</div>
);
}

return (
<div className="additional-metadata-container">
<Table striped={true} responsive={true}>
<thead>
<tr>
<th className="col-xs-2">{i18n("history.whenwho")}</th>
<th className="col-xs-1">{i18n("history.type")}</th>
<th className="col-xs-2">{i18n("history.changedAttribute")}</th>
<th className="col-xs-2">{i18n("history.originalValue")}</th>
<th className="col-xs-2">{i18n("history.newValue")}</th>
<th className="col-2">{i18n("history.whenwho")}</th>
<th className="col-1">{i18n("history.type")}</th>
<th className="col-2">{i18n("history.changedAttribute")}</th>
<th className="col-2">{i18n("history.originalValue")}</th>
<th className="col-2">{i18n("history.newValue")}</th>
</tr>
<tr>
<td>
<CustomInput
name={i18n("asset.author")}
placeholder={i18n("asset.author")}
value={filterAuthor}
onChange={(e) => setFilterAuthor(e.target.value)}
/>
</td>
<td>
<Select
placeholder={i18n("history.type")}
value={filterType}
onChange={(e) => setFilterType(e.target.value)}
>
<option value={""}></option>
{[
"history.type.persist",
"history.type.update",
"history.type.delete",
].map((type) => (
<option key={type} value={type}>
{i18n(type)}
</option>
))}
</Select>
</td>
<td>
<CustomInput
name={i18n("history.changedAttribute")}
placeholder={i18n("history.changedAttribute")}
value={filterAttribute}
onChange={(e) => setFilterAttribute(e.target.value)}
/>
</td>
</tr>
</thead>
<tbody>
{records.map((r) =>
r instanceof UpdateRecord ? (
<UpdateRow key={r.iri} record={r as UpdateRecord} />
) : (
<PersistRow key={r.iri} record={r as PersistRecord} />
)
)}
{records.map((r) => {
if (r instanceof PersistRecord) {
return <PersistRow key={r.iri} record={r} />;
}
if (r instanceof UpdateRecord) {
return <UpdateRow key={r.iri} record={r} />;
}
if (r instanceof DeleteRecord) {
return <DeleteRow key={r.iri} record={r} />;
}
return null;
})}
</tbody>
</Table>
</div>
Expand Down
35 changes: 35 additions & 0 deletions src/component/changetracking/DeleteRow.tsx
Original file line number Diff line number Diff line change
@@ -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<DeleteRowProps> = (props) => {
const { i18n } = useI18n();
const record = props.record;
const created = new Date(Date.parse(record.timestamp));
return (
<tr>
<td>
<div>
<FormattedDate value={created} /> <FormattedTime value={created} />
</div>
<div className="italics last-edited-message ml-2">
{record.author.fullName}
</div>
</td>
<td>
<Badge color="danger">{i18n(record.typeLabel)}</Badge>
</td>
<td />
<td />
<td />
</tr>
);
};

export default DeleteRow;
33 changes: 33 additions & 0 deletions src/component/changetracking/VocabularyContentDeleteRow.tsx
Original file line number Diff line number Diff line change
@@ -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<DeleteRowProps> = (props) => {
const { i18n } = useI18n();
const record = props.record;
const created = new Date(Date.parse(record.timestamp));
return (
<tr>
<td>
<div>
<FormattedDate value={created} /> <FormattedTime value={created} />
</div>
<div className="italics last-edited-message ml-2">
{record.author.fullName}
</div>
</td>
<td>
<TermIriLink iri={record.changedEntity.iri} shrinkFullIri={true} />
</td>
<td>
<Badge color="danger">{i18n(record.typeLabel)}</Badge>
</td>
<td></td>
</tr>
);
};

export default VocabularyContentDeleteRow;
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export const VocabularyContentPersistRow: React.FC<PersistRowProps> = (
</div>
</td>
<td>
<TermIriLink iri={record.changedEntity.iri} />
<TermIriLink iri={record.changedEntity.iri} shrinkFullIri={true} />
</td>
<td>
<Badge color="dark">{i18n(record.typeLabel)}</Badge>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export const VocabularyContentUpdateRow: React.FC<UpdateRowProps> = (props) => {
</div>
</td>
<td>
<TermIriLink iri={record.changedEntity.iri} />
<TermIriLink iri={record.changedEntity.iri} shrinkFullIri={true} />
</td>
<td>
<Badge color="secondary">{i18n(record.typeLabel)}</Badge>
Expand Down
Loading

0 comments on commit 8ce480f

Please sign in to comment.