Add edit history components

This commit is contained in:
Maciej Ziarkowski 2019-10-24 12:19:54 +01:00
parent 946209282c
commit 2e47d85faa
10 changed files with 223 additions and 9 deletions

View File

@ -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;
}

View File

@ -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
};

View File

@ -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;
}

View File

@ -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
};

View File

@ -0,0 +1,4 @@
.edit-history-list {
list-style: none;
padding-left: 1rem;
}

View 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
};

View File

@ -0,0 +1,20 @@
import React from 'react';
interface FieldEditSummaryProps {
title: string;
value: any;
oldValue: any;
}
const FieldEditSummary: React.FunctionComponent<FieldEditSummaryProps> = props => (
<>
{props.title}:&nbsp;
<code title="Value before edit" className='edit-history-diff old'>{props.oldValue}</code>
&nbsp;
<code title="Value after edit" className='edit-history-diff new'>{props.value}</code>
</>
);
export {
FieldEditSummary
};

View File

@ -0,0 +1,7 @@
export interface EditHistoryEntry {
date_trunc: string;
username: string;
revision_id: string;
forward_patch: object;
reverse_patch: object;
}

View File

@ -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 {

View File

@ -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
};