Skip to content

Commit

Permalink
[Enhancement #519] Recent term history in vocabulary activity tab
Browse files Browse the repository at this point in the history
Displaying recent term changes in vocabulary detail in activity tab.
  • Loading branch information
lukaskabc authored and ledsoft committed Oct 14, 2024
1 parent b27f826 commit 5fd2b74
Show file tree
Hide file tree
Showing 8 changed files with 202 additions and 17 deletions.
39 changes: 38 additions & 1 deletion src/action/AsyncVocabularyActions.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { GetStoreState, ThunkDispatch } from "../util/Types";
import { GetStoreState, PageRequest, ThunkDispatch } from "../util/Types";
import * as SyncActions from "./SyncActions";
import {
asyncActionFailure,
Expand Down Expand Up @@ -27,6 +27,10 @@ import SnapshotData, { CONTEXT as SNAPSHOT_CONTEXT } from "../model/Snapshot";
import NotificationType from "../model/NotificationType";
import ExportConfig from "../model/local/ExportConfig";
import RDFStatement, { RDFSTATEMENT_CONTEXT } from "../model/RDFStatement";
import ChangeRecord, {
CONTEXT as CHANGE_RECORD_CONTEXT,
} from "../model/changetracking/ChangeRecord";
import AssetFactory from "../util/AssetFactory";

export function loadTermCount(vocabularyIri: IRI) {
const action = { type: ActionType.LOAD_TERM_COUNT, vocabularyIri };
Expand Down Expand Up @@ -131,6 +135,39 @@ export function loadVocabularyContentChanges(vocabularyIri: IRI) {
};
}

export function loadVocabularyContentDetailedChanges(
vocabularyIri: IRI,
pageReq: PageRequest
) {
const action = {
type: ActionType.LOAD_TERM_HISTORY,
};

return (dispatch: ThunkDispatch) => {
dispatch(asyncActionRequest(action, true));
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())
)
.then((data) =>
JsonLdUtils.compactAndResolveReferencesAsArray<ChangeRecord>(
data,
CHANGE_RECORD_CONTEXT
)
)
.then((data: ChangeRecord[]) => {
dispatch(asyncActionSuccess(action));
return data.map((r) => AssetFactory.createChangeRecord(r));
})
.catch((error: ErrorData) => {
dispatch(asyncActionFailure(action, error));
return [];
});
};
}

export function loadRelatedVocabularies(vocabularyIri: IRI) {
const action = { type: ActionType.LOAD_RELATED_VOCABULARIES, vocabularyIri };
return (dispatch: ThunkDispatch) => {
Expand Down
2 changes: 1 addition & 1 deletion src/component/changetracking/PersistRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import PersistRecord from "../../model/changetracking/PersistRecord";
import { Badge } from "reactstrap";
import { useI18n } from "../hook/useI18n";

interface PersistRowProps {
export interface PersistRowProps {
record: PersistRecord;
}

Expand Down
2 changes: 1 addition & 1 deletion src/component/changetracking/UpdateRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import OutgoingLink from "../misc/OutgoingLink";
import { Badge, Label } from "reactstrap";
import { useI18n } from "../hook/useI18n";

interface UpdateRowProps {
export interface UpdateRowProps {
record: UpdateRecord;
}

Expand Down
35 changes: 35 additions & 0 deletions src/component/changetracking/VocabularyContentPersistRow.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 { PersistRowProps } from "./PersistRow";
import TermIriLink from "../term/TermIriLink";

export const VocabularyContentPersistRow: React.FC<PersistRowProps> = (
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} />
</td>
<td>
<Badge color="dark">{i18n(record.typeLabel)}</Badge>
</td>
<td />
</tr>
);
};

export default VocabularyContentPersistRow;
36 changes: 36 additions & 0 deletions src/component/changetracking/VocabularyContentUpdateRow.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import * as React from "react";
import { FormattedDate, FormattedTime } from "react-intl";
import AssetLabel from "../misc/AssetLabel";
import { Badge } from "reactstrap";
import { useI18n } from "../hook/useI18n";
import { UpdateRowProps } from "./UpdateRow";
import TermIriLink from "../term/TermIriLink";

export const VocabularyContentUpdateRow: React.FC<UpdateRowProps> = (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} />
</td>
<td>
<Badge color="secondary">{i18n(record.typeLabel)}</Badge>
</td>
<td>
<AssetLabel iri={record.changedAttribute.iri} />
</td>
</tr>
);
};

export default VocabularyContentUpdateRow;
38 changes: 33 additions & 5 deletions src/component/vocabulary/TermChangeFrequency.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,36 +9,64 @@ import PromiseTrackingMask from "../misc/PromiseTrackingMask";
import { trackPromise } from "react-promise-tracker";
import { useI18n } from "../hook/useI18n";
import AggregatedChangeInfo from "../../model/changetracking/AggregatedChangeInfo";
import { loadVocabularyContentChanges } from "../../action/AsyncVocabularyActions";
import {
loadVocabularyContentChanges,
loadVocabularyContentDetailedChanges,
} from "../../action/AsyncVocabularyActions";
import ChangeRecord from "../../model/changetracking/ChangeRecord";

interface TermChangeFrequencyProps {
vocabulary: Vocabulary;
}

const TermChangeFrequency: React.FC<TermChangeFrequencyProps> = (props) => {
const [records, setRecords] =
const [aggregatedRecords, setAggregatedRecords] =
React.useState<null | AggregatedChangeInfo[]>(null);
const [changeRecords, setChangeRecords] =
React.useState<null | ChangeRecord[]>(null);
const { vocabulary } = props;
const { i18n } = useI18n();
const dispatch: ThunkDispatch = useDispatch();
const [page, setPage] = React.useState(0);
React.useEffect(() => {
if (vocabulary.iri !== Constants.EMPTY_ASSET_IRI) {
trackPromise(
dispatch(
loadVocabularyContentChanges(VocabularyUtils.create(vocabulary.iri))
),
).then((recs) => setAggregatedRecords(recs)),
"term-change-frequency"
).then((recs) => setRecords(recs));
);
}
}, [vocabulary.iri, dispatch]);

React.useEffect(() => {
if (vocabulary.iri !== Constants.EMPTY_ASSET_IRI) {
trackPromise(
dispatch(
loadVocabularyContentDetailedChanges(
VocabularyUtils.create(vocabulary.iri),
{ page: page, size: Constants.VOCABULARY_CONTENT_HISTORY_LIMIT }
)
).then((changeRecords) => setChangeRecords(changeRecords)),
"term-change-frequency"
);
}
}, [vocabulary.iri, dispatch, page]);

return (
<>
<PromiseTrackingMask
area="term-change-frequency"
text={i18n("vocabulary.termchanges.loading")}
/>
<TermChangeFrequencyUI records={records} />
<TermChangeFrequencyUI
aggregatedRecords={aggregatedRecords}
changeRecords={changeRecords}
page={page}
setPage={setPage}
pageSize={Constants.VOCABULARY_CONTENT_HISTORY_LIMIT}
itemCount={changeRecords?.length ?? 0}
/>
</>
);
};
Expand Down
65 changes: 56 additions & 9 deletions src/component/vocabulary/TermChangeFrequencyUI.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,23 @@
import * as React from "react";
import Chart from "react-apexcharts";
import { Col, Row } from "reactstrap";
import { Col, Row, Table } from "reactstrap";
import { useI18n } from "../hook/useI18n";
import AggregatedChangeInfo from "../../model/changetracking/AggregatedChangeInfo";
import VocabularyUtils from "../../util/VocabularyUtils";
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";

interface TermChangeFrequencyUIProps {
records: AggregatedChangeInfo[] | null;
aggregatedRecords: AggregatedChangeInfo[] | null;
changeRecords: ChangeRecord[] | null;
page: number;
setPage: (page: number) => void;
pageSize: number;
itemCount: number;
}

/**
Expand Down Expand Up @@ -48,14 +59,19 @@ const CZ_LOCALE = {
};

const TermChangeFrequencyUI: React.FC<TermChangeFrequencyUIProps> = ({
records,
aggregatedRecords,
changeRecords,
page,
setPage,
pageSize,
itemCount,
}) => {
const { i18n, locale } = useI18n();
if (!records) {
if (!aggregatedRecords || !changeRecords) {
return <div className="additional-metadata-container">&nbsp;</div>;
}

if (records.length === 0) {
if (aggregatedRecords.length === 0) {
return (
<div
id="history-empty-notice"
Expand All @@ -66,11 +82,11 @@ const TermChangeFrequencyUI: React.FC<TermChangeFrequencyUIProps> = ({
);
}

const dates = Array.from(new Set(records.map((r) => r.getDate())));
const termCreations = records.filter(
const dates = Array.from(new Set(aggregatedRecords.map((r) => r.getDate())));
const termCreations = aggregatedRecords.filter(
(r) => r.types.indexOf(VocabularyUtils.PERSIST_EVENT) !== -1
);
const termUpdates = records.filter(
const termUpdates = aggregatedRecords.filter(
(r) => r.types.indexOf(VocabularyUtils.UPDATE_EVENT) !== -1
);

Expand Down Expand Up @@ -131,9 +147,40 @@ const TermChangeFrequencyUI: React.FC<TermChangeFrequencyUIProps> = ({
];
return (
<Row>
<Col xl={8} lg={12}>
<Col xl={changeRecords.length === 0 ? 5 : 6} lg={12}>
<Chart options={options} series={series} width="100%" />
</Col>
<Col xl={6} lg={12} className={"border-left"}>
<div className="additional-metadata-container">
<Table striped={true} responsive={true}>
<thead>
<tr>
<th className="col-3">{i18n("history.whenwho")}</th>
<th className="col-3">{i18n("type.term")}</th>
<th className="col-1">{i18n("history.type")}</th>
<th className="col-2">{i18n("history.changedAttribute")}</th>
</tr>
</thead>
<tbody>
{changeRecords.map((r) =>
r instanceof UpdateRecord ? (
<VocabularyContentUpdateRow key={r.iri} record={r} />
) : (
<VocabularyContentPersistRow key={r.iri} record={r} />
)
)}
</tbody>
</Table>
</div>
<If expression={changeRecords.length > 0}>
<SimplePagination
page={page}
setPage={setPage}
pageSize={pageSize}
itemCount={itemCount}
/>
</If>
</Col>
</Row>
);
};
Expand Down
2 changes: 2 additions & 0 deletions src/util/Constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,8 @@ const constants = {

// Size of page fetched from server
DEFAULT_PAGE_SIZE: 100,
// size of the page for change records in vocabulary activity tab
VOCABULARY_CONTENT_HISTORY_LIMIT: 15,

WEBSOCKET_ENDPOINT: {
VOCABULARIES_VALIDATION: "/vocabularies/validation",
Expand Down

0 comments on commit 5fd2b74

Please sign in to comment.