Skip to content

Commit

Permalink
collapse timeline entries
Browse files Browse the repository at this point in the history
  • Loading branch information
riknoll committed Sep 26, 2023
1 parent 5321aee commit 2e2fa4d
Show file tree
Hide file tree
Showing 2 changed files with 106 additions and 47 deletions.
1 change: 0 additions & 1 deletion tests/pxt-editor-test/editorrunner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,6 @@ for (let i = 0; i < 20; i++) {

const ONE_MINUTE = 1000 * 60;
const ONE_HOUR = ONE_MINUTE * 60;
const ONE_DAY = ONE_HOUR * 24;


describe("updateHistory", () => {
Expand Down
152 changes: 106 additions & 46 deletions webapp/src/timeMachine.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { createPortal } from "react-dom";
import { Button } from "../../react-common/components/controls/Button";
import { hideDialog } from "./core";
import { FocusTrap } from "../../react-common/components/controls/FocusTrap";
import { classList } from "../../react-common/components/util";

interface TimeMachineProps {
onProjectLoad: (text: pxt.workspace.ScriptText, editorVersion: string, timestamp?: number) => void;
Expand All @@ -18,9 +19,15 @@ interface PendingMessage {
handler: (response: any) => void;
}

interface TimelineEntry {
label: string;
entries: TimeEntry[];
}

interface TimeEntry {
label: string;
timestamp: number;
kind: "snapshot" | "diff";
}

interface Project {
Expand All @@ -35,7 +42,8 @@ export const TimeMachine = (props: TimeMachineProps) => {

// -1 here is a standin for "now"
const [selected, setSelected] = React.useState<number>(-1);
const [loading, setLoading] = React.useState<FrameState>("loading")
const [loading, setLoading] = React.useState<FrameState>("loading");
const [entries, setEntries] = React.useState(getTimelineEntries(history));

const iframeRef = React.useRef<HTMLIFrameElement>();

Expand Down Expand Up @@ -150,6 +158,10 @@ export const TimeMachine = (props: TimeMachineProps) => {
}
}, [loading, importProject.current, text]);

React.useEffect(() => {
setEntries(getTimelineEntries(history));
}, [history]);

const onTimeSelected = async (newValue: number) => {
if (importProject.current) {
setSelected(newValue);
Expand Down Expand Up @@ -185,46 +197,6 @@ export const TimeMachine = (props: TimeMachineProps) => {

const url = `${window.location.origin + window.location.pathname}?timeMachine=1&controller=1&skillsMap=1&noproject=1&nocookiebanner=1`;

const buckets: {[index: string]: TimeEntry[]} = {};

for (const entry of history.entries) {
const date = new Date(entry.timestamp);
const key = new Date(date.toLocaleDateString(
pxt.U.userLanguage(),
{
year: "numeric",
month: "numeric",
day: "numeric"
}
)).getTime();

if (!buckets[key]) {
buckets[key] = [];
}

buckets[key].push({
label: formatTime(entry.timestamp),
timestamp: entry.timestamp
});
}

const nowEntry = {
label: lf("Now"),
timestamp: -1
};

const sortedBuckets = Object.keys(buckets).sort((a, b) => parseInt(b) - parseInt(a));
for (const bucket of sortedBuckets) {
buckets[bucket].sort((a, b) => b.timestamp - a.timestamp);
}

if (!sortedBuckets.length || !isToday(parseInt(sortedBuckets[0]))) {
buckets[Date.now()] = [nowEntry]
}
else {
buckets[sortedBuckets[0]].unshift(nowEntry)
}

return createPortal(
<FocusTrap className="time-machine" onEscape={hideDialog}>
<div className="time-machine-header">
Expand Down Expand Up @@ -277,17 +249,17 @@ export const TimeMachine = (props: TimeMachineProps) => {
</h3>
<div className="time-machine-tree-container">
<Tree>
{sortedBuckets.map((date, i) =>
<TreeItem key={date} initiallyExpanded={i === 0}>
{entries.map((e, i) =>
<TreeItem key={i} initiallyExpanded={i === 0}>
<TreeItemBody>
{formatDate(parseInt(date))}
{e.label}
</TreeItemBody>
<Tree role="group">
{...buckets[date].map(entry =>
{e.entries.map(entry =>
<TreeItem
key={entry.timestamp}
onClick={() => onTimeSelected(entry.timestamp)}
className={selected === entry.timestamp ? "selected" : undefined}
className={classList(selected === entry.timestamp && "selected", entry.kind)}
>
<TreeItemBody>
{entry.label}
Expand Down Expand Up @@ -441,4 +413,92 @@ function isToday(time: number) {
return now.getFullYear() === date.getFullYear() &&
now.getMonth() === date.getMonth() &&
now.getDate() == date.getDate();
}

function getTimelineEntries(history: pxt.workspace.HistoryFile): TimelineEntry[] {
const buckets: {[index: string]: TimeEntry[]} = {};

const createTimeEntry = (timestamp: number, kind: "snapshot" | "diff") => {
const date = new Date(timestamp);
const key = new Date(date.toLocaleDateString(
pxt.U.userLanguage(),
{
year: "numeric",
month: "numeric",
day: "numeric"
}
)).getTime();

if (!buckets[key]) {
buckets[key] = [];
}

buckets[key].push({
label: formatTime(timestamp),
timestamp,
kind
});
}

for (const entry of history.entries) {
createTimeEntry(entry.timestamp, "diff");
}

for (const entry of history.snapshots) {
createTimeEntry(entry.timestamp, "snapshot");
}

const sortedBuckets = Object.keys(buckets).sort((a, b) => parseInt(b) - parseInt(a));
for (const bucketKey of sortedBuckets) {
const bucket = buckets[bucketKey];
const deduped: TimeEntry[] = [];

// Deduplicate entries that exist in the same minute and sort
for (const entry of bucket) {
const eIndex = deduped.findIndex(e => e.label === entry.label);
const existing = deduped[eIndex];

if (existing) {
// We generally prefer snapshots to diffs. Otherwise take the latest
if (existing.kind === entry.kind) {
if (entry.timestamp > existing.timestamp) {
deduped.splice(eIndex, 1, entry);
}
}
else if (existing.kind === "snapshot") {
continue;
}
else if (existing.kind === "diff") {
deduped.splice(eIndex, 1, entry);
}
}
else {
deduped.push(entry);
}
}

deduped.sort((a, b) => b.timestamp - a.timestamp);
buckets[bucketKey] = deduped;
}

// Always add an entry for "now"
const nowEntry: TimeEntry = {
label: lf("Now"),
timestamp: -1,
kind: "snapshot"
};

if (!sortedBuckets.length || !isToday(parseInt(sortedBuckets[0]))) {
buckets[Date.now()] = [nowEntry]
}
else {
buckets[sortedBuckets[0]].unshift(nowEntry)
}

return sortedBuckets.map(key => (
{
label: formatDate(parseInt(key)),
entries: buckets[key]
} as TimelineEntry
))
}

0 comments on commit 2e2fa4d

Please sign in to comment.