Add edit history components
This commit is contained in:
parent
946209282c
commit
2e47d85faa
@ -0,0 +1,14 @@
|
|||||||
|
.edit-history-entry {
|
||||||
|
border-bottom: 1px solid black;
|
||||||
|
padding: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.edit-history-timestamp {
|
||||||
|
font-size: 1rem;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-history-username {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
@ -0,0 +1,52 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { EditHistoryEntry } from '../models/edit-history-entry';
|
||||||
|
import { arrayToDictionary, parseDate } from '../../helpers';
|
||||||
|
import { dataFields } from '../../data_fields';
|
||||||
|
import { CategoryEditSummary } from './category-edit-summary';
|
||||||
|
|
||||||
|
import './building-edit-summary.css';
|
||||||
|
|
||||||
|
interface BuildingEditSummaryProps {
|
||||||
|
historyEntry: EditHistoryEntry
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDate(dt: Date) {
|
||||||
|
return dt.toLocaleString(undefined, {
|
||||||
|
weekday: 'short',
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'short',
|
||||||
|
day: 'numeric',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const BuildingEditSummary: React.FunctionComponent<BuildingEditSummaryProps> = props => {
|
||||||
|
const entriesWithMetadata = Object
|
||||||
|
.entries(props.historyEntry.forward_patch)
|
||||||
|
.map(([key, value]) => {
|
||||||
|
const info = dataFields[key] || {};
|
||||||
|
return {
|
||||||
|
title: info.title || `Unknown field (${key})`,
|
||||||
|
category: info.category || 'Unknown category',
|
||||||
|
value: value,
|
||||||
|
oldValue: props.historyEntry.reverse_patch && props.historyEntry.reverse_patch[key]
|
||||||
|
};
|
||||||
|
});
|
||||||
|
const entriesByCategory = arrayToDictionary(entriesWithMetadata, x => x.category);
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="edit-history-entry">
|
||||||
|
<h2 className="edit-history-timestamp">Edited on {formatDate(parseDate(props.historyEntry.date_trunc))}</h2>
|
||||||
|
<h3 className="edit-history-username">By {props.historyEntry.username}</h3>
|
||||||
|
{
|
||||||
|
Object.entries(entriesByCategory).map(([category, fields]) => <CategoryEditSummary category={category} fields={fields} />)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
BuildingEditSummary
|
||||||
|
};
|
@ -0,0 +1,23 @@
|
|||||||
|
.edit-history-category-summary ul {
|
||||||
|
list-style: none;
|
||||||
|
padding-left: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-history-category-title {
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-history-diff {
|
||||||
|
padding: 0 0.2rem;
|
||||||
|
padding-top:0.15rem;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
.edit-history-diff.old {
|
||||||
|
background-color: #f8d9bc;
|
||||||
|
color: #c24e00;
|
||||||
|
text-decoration: line-through;
|
||||||
|
}
|
||||||
|
.edit-history-diff.new {
|
||||||
|
background-color: #b6dcff;
|
||||||
|
color: #0064c2;
|
||||||
|
}
|
@ -0,0 +1,31 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import './category-edit-summary.css';
|
||||||
|
import { FieldEditSummary } from './field-edit-summary';
|
||||||
|
|
||||||
|
interface CategoryEditSummaryProps {
|
||||||
|
category: string;
|
||||||
|
fields: {
|
||||||
|
title: string;
|
||||||
|
value: string;
|
||||||
|
oldValue: string;
|
||||||
|
}[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const CategoryEditSummary : React.FunctionComponent<CategoryEditSummaryProps> = props => (
|
||||||
|
<div className='edit-history-category-summary'>
|
||||||
|
<h3 className='edit-history-category-title'>{props.category}:</h3>
|
||||||
|
<ul>
|
||||||
|
{
|
||||||
|
props.fields.map(x =>
|
||||||
|
<li key={x.title}>
|
||||||
|
<FieldEditSummary title={x.title} value={x.value} oldValue={x.oldValue} />
|
||||||
|
</li>)
|
||||||
|
}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
export {
|
||||||
|
CategoryEditSummary
|
||||||
|
};
|
4
app/src/frontend/building/edit-history/edit-history.css
Normal file
4
app/src/frontend/building/edit-history/edit-history.css
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
.edit-history-list {
|
||||||
|
list-style: none;
|
||||||
|
padding-left: 1rem;
|
||||||
|
}
|
46
app/src/frontend/building/edit-history/edit-history.tsx
Normal file
46
app/src/frontend/building/edit-history/edit-history.tsx
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { EditHistoryEntry } from '../models/edit-history-entry';
|
||||||
|
import { BuildingEditSummary } from './building-edit-summary';
|
||||||
|
|
||||||
|
import './edit-history.css';
|
||||||
|
import { Building } from '../../models/building';
|
||||||
|
import ContainerHeader from '../container-header';
|
||||||
|
|
||||||
|
interface EditHistoryProps {
|
||||||
|
building: Building;
|
||||||
|
}
|
||||||
|
|
||||||
|
const EditHistory: React.FunctionComponent<EditHistoryProps> = (props) => {
|
||||||
|
const [history, setHistory] = useState<EditHistoryEntry[]>(undefined);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchData = async () => {
|
||||||
|
const res = await fetch(`/api/buildings/${props.building.building_id}/history.json`);
|
||||||
|
const data = await res.json();
|
||||||
|
|
||||||
|
setHistory(data.history);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (props.building != undefined) { // only call fn if there is a building provided
|
||||||
|
fetchData(); // define and call, because effect cannot return anything and an async fn always returns a Promise
|
||||||
|
}
|
||||||
|
}, [props.building]); // only re-run effect on building prop change
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<ContainerHeader title="Edit history" backLink='.' />
|
||||||
|
|
||||||
|
<ul className="edit-history-list">
|
||||||
|
{history && history.map(entry => (
|
||||||
|
<li key={`${entry.revision_id}`} className="edit-history-list-element">
|
||||||
|
<BuildingEditSummary historyEntry={entry} />
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
EditHistory
|
||||||
|
};
|
@ -0,0 +1,20 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
interface FieldEditSummaryProps {
|
||||||
|
title: string;
|
||||||
|
value: any;
|
||||||
|
oldValue: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
const FieldEditSummary: React.FunctionComponent<FieldEditSummaryProps> = props => (
|
||||||
|
<>
|
||||||
|
{props.title}:
|
||||||
|
<code title="Value before edit" className='edit-history-diff old'>{props.oldValue}</code>
|
||||||
|
|
||||||
|
<code title="Value after edit" className='edit-history-diff new'>{props.value}</code>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
export {
|
||||||
|
FieldEditSummary
|
||||||
|
};
|
7
app/src/frontend/building/models/edit-history-entry.ts
Normal file
7
app/src/frontend/building/models/edit-history-entry.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
export interface EditHistoryEntry {
|
||||||
|
date_trunc: string;
|
||||||
|
username: string;
|
||||||
|
revision_id: string;
|
||||||
|
forward_patch: object;
|
||||||
|
reverse_patch: object;
|
||||||
|
}
|
@ -9,13 +9,6 @@
|
|||||||
height: 40%;
|
height: 40%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.info-container h2:first-child {
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
margin-top: 0.5rem;
|
|
||||||
margin-left: -0.1em;
|
|
||||||
padding: 0 0.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 768px){
|
@media (min-width: 768px){
|
||||||
.info-container {
|
.info-container {
|
||||||
order: 0;
|
order: 0;
|
||||||
@ -99,7 +92,8 @@
|
|||||||
color: rgb(11, 225, 225);
|
color: rgb(11, 225, 225);
|
||||||
}
|
}
|
||||||
.icon-button.help,
|
.icon-button.help,
|
||||||
.icon-button.copy {
|
.icon-button.copy,
|
||||||
|
.icon-button.history {
|
||||||
margin-top: 4px;
|
margin-top: 4px;
|
||||||
}
|
}
|
||||||
.data-section label .icon-buttons .icon-button.copy {
|
.data-section label .icon-buttons .icon-button.copy {
|
||||||
|
@ -36,4 +36,27 @@ function sanitiseURL(string){
|
|||||||
return `${url_.protocol}//${url_.hostname}${url_.pathname || ''}${url_.search || ''}${url_.hash || ''}`
|
return `${url_.protocol}//${url_.hostname}${url_.pathname || ''}${url_.search || ''}${url_.hash || ''}`
|
||||||
}
|
}
|
||||||
|
|
||||||
export { sanitiseURL }
|
function arrayToDictionary<T>(arr: T[], keyAccessor: (obj: T) => string): {[key: string]: T[]} {
|
||||||
|
return arr.reduce((obj, item) => {
|
||||||
|
(obj[keyAccessor(item)] = obj[keyAccessor(item)] || []).push(item);
|
||||||
|
return obj;
|
||||||
|
}, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse a string containing
|
||||||
|
* @param isoUtcDate a date string in ISO8601 format
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
function parseDate(isoUtcDate: string): Date {
|
||||||
|
const [year, month, day, hour, minute, second, millisecond] = isoUtcDate.match(/^(\d{4})-(\d\d)-(\d\d)T(\d\d):(\d\d):(\d\d).(\d{3})Z$/)
|
||||||
|
.splice(1)
|
||||||
|
.map(x => parseInt(x, 10));
|
||||||
|
return new Date(Date.UTC(year, month-1, day, hour, minute, second, millisecond));
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
sanitiseURL,
|
||||||
|
arrayToDictionary,
|
||||||
|
parseDate
|
||||||
|
};
|
||||||
|
Loading…
Reference in New Issue
Block a user