From 2e47d85faa57b9756dfef2eaf822bb534ede0c35 Mon Sep 17 00:00:00 2001 From: Maciej Ziarkowski Date: Thu, 24 Oct 2019 12:19:54 +0100 Subject: [PATCH] Add edit history components --- .../edit-history/building-edit-summary.css | 14 +++++ .../edit-history/building-edit-summary.tsx | 52 +++++++++++++++++++ .../edit-history/category-edit-summary.css | 23 ++++++++ .../edit-history/category-edit-summary.tsx | 31 +++++++++++ .../building/edit-history/edit-history.css | 4 ++ .../building/edit-history/edit-history.tsx | 46 ++++++++++++++++ .../edit-history/field-edit-summary.tsx | 20 +++++++ .../building/models/edit-history-entry.ts | 7 +++ app/src/frontend/building/sidebar.css | 10 +--- app/src/frontend/helpers.ts | 25 ++++++++- 10 files changed, 223 insertions(+), 9 deletions(-) create mode 100644 app/src/frontend/building/edit-history/building-edit-summary.css create mode 100644 app/src/frontend/building/edit-history/building-edit-summary.tsx create mode 100644 app/src/frontend/building/edit-history/category-edit-summary.css create mode 100644 app/src/frontend/building/edit-history/category-edit-summary.tsx create mode 100644 app/src/frontend/building/edit-history/edit-history.css create mode 100644 app/src/frontend/building/edit-history/edit-history.tsx create mode 100644 app/src/frontend/building/edit-history/field-edit-summary.tsx create mode 100644 app/src/frontend/building/models/edit-history-entry.ts diff --git a/app/src/frontend/building/edit-history/building-edit-summary.css b/app/src/frontend/building/edit-history/building-edit-summary.css new file mode 100644 index 00000000..2f4cba97 --- /dev/null +++ b/app/src/frontend/building/edit-history/building-edit-summary.css @@ -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; +} \ No newline at end of file diff --git a/app/src/frontend/building/edit-history/building-edit-summary.tsx b/app/src/frontend/building/edit-history/building-edit-summary.tsx new file mode 100644 index 00000000..fec2df08 --- /dev/null +++ b/app/src/frontend/building/edit-history/building-edit-summary.tsx @@ -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 = 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 ( +
+

Edited on {formatDate(parseDate(props.historyEntry.date_trunc))}

+

By {props.historyEntry.username}

+ { + Object.entries(entriesByCategory).map(([category, fields]) => ) + } +
+ ); +} + +export { + BuildingEditSummary +}; diff --git a/app/src/frontend/building/edit-history/category-edit-summary.css b/app/src/frontend/building/edit-history/category-edit-summary.css new file mode 100644 index 00000000..a4710917 --- /dev/null +++ b/app/src/frontend/building/edit-history/category-edit-summary.css @@ -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; +} \ No newline at end of file diff --git a/app/src/frontend/building/edit-history/category-edit-summary.tsx b/app/src/frontend/building/edit-history/category-edit-summary.tsx new file mode 100644 index 00000000..73a0d542 --- /dev/null +++ b/app/src/frontend/building/edit-history/category-edit-summary.tsx @@ -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 = props => ( +
+

{props.category}:

+
    + { + props.fields.map(x => +
  • + +
  • ) + } +
+
+); + +export { + CategoryEditSummary +}; diff --git a/app/src/frontend/building/edit-history/edit-history.css b/app/src/frontend/building/edit-history/edit-history.css new file mode 100644 index 00000000..f79e24e3 --- /dev/null +++ b/app/src/frontend/building/edit-history/edit-history.css @@ -0,0 +1,4 @@ +.edit-history-list { + list-style: none; + padding-left: 1rem; +} \ No newline at end of file diff --git a/app/src/frontend/building/edit-history/edit-history.tsx b/app/src/frontend/building/edit-history/edit-history.tsx new file mode 100644 index 00000000..2b711c73 --- /dev/null +++ b/app/src/frontend/building/edit-history/edit-history.tsx @@ -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 = (props) => { + const [history, setHistory] = useState(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 ( + <> + + +
    + {history && history.map(entry => ( +
  • + +
  • + ))} +
+ + ); +} + +export { + EditHistory +}; diff --git a/app/src/frontend/building/edit-history/field-edit-summary.tsx b/app/src/frontend/building/edit-history/field-edit-summary.tsx new file mode 100644 index 00000000..f62357bf --- /dev/null +++ b/app/src/frontend/building/edit-history/field-edit-summary.tsx @@ -0,0 +1,20 @@ +import React from 'react'; + +interface FieldEditSummaryProps { + title: string; + value: any; + oldValue: any; +} + +const FieldEditSummary: React.FunctionComponent = props => ( + <> + {props.title}:  + {props.oldValue} +   + {props.value} + +); + +export { + FieldEditSummary +}; diff --git a/app/src/frontend/building/models/edit-history-entry.ts b/app/src/frontend/building/models/edit-history-entry.ts new file mode 100644 index 00000000..a38e7e0d --- /dev/null +++ b/app/src/frontend/building/models/edit-history-entry.ts @@ -0,0 +1,7 @@ +export interface EditHistoryEntry { + date_trunc: string; + username: string; + revision_id: string; + forward_patch: object; + reverse_patch: object; +} diff --git a/app/src/frontend/building/sidebar.css b/app/src/frontend/building/sidebar.css index 53caf48e..499e38ee 100644 --- a/app/src/frontend/building/sidebar.css +++ b/app/src/frontend/building/sidebar.css @@ -9,13 +9,6 @@ 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){ .info-container { order: 0; @@ -99,7 +92,8 @@ color: rgb(11, 225, 225); } .icon-button.help, -.icon-button.copy { +.icon-button.copy, +.icon-button.history { margin-top: 4px; } .data-section label .icon-buttons .icon-button.copy { diff --git a/app/src/frontend/helpers.ts b/app/src/frontend/helpers.ts index b4a5d45c..d6660275 100644 --- a/app/src/frontend/helpers.ts +++ b/app/src/frontend/helpers.ts @@ -36,4 +36,27 @@ function sanitiseURL(string){ return `${url_.protocol}//${url_.hostname}${url_.pathname || ''}${url_.search || ''}${url_.hash || ''}` } -export { sanitiseURL } +function arrayToDictionary(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 +};