mirror of
https://seed.radicle.dev/rad:z4V1sjrXqjvFdnCUbxPFqd5p4DtH5.git
synced 2026-11-04 03:00:32 +08:00
Add patch timeline
This commit is contained in:
+152
-1
@@ -8,14 +8,17 @@
|
||||
styleDisplay?: string;
|
||||
styleVerticalAlign?: string;
|
||||
name:
|
||||
| "attachment"
|
||||
| "arrow-left"
|
||||
| "arrow-right"
|
||||
| "arrow-right-hollow"
|
||||
| "attachment"
|
||||
| "checkmark"
|
||||
| "chevron-down"
|
||||
| "chevron-right"
|
||||
| "collapse-panel"
|
||||
| "comment"
|
||||
| "comment-checkmark"
|
||||
| "comment-cross"
|
||||
| "copy"
|
||||
| "cross"
|
||||
| "dashboard"
|
||||
@@ -28,8 +31,10 @@
|
||||
| "home"
|
||||
| "inbox"
|
||||
| "issue"
|
||||
| "label"
|
||||
| "lock"
|
||||
| "markdown"
|
||||
| "merge"
|
||||
| "moon"
|
||||
| "more-vertical"
|
||||
| "none"
|
||||
@@ -45,6 +50,7 @@
|
||||
| "seedling-filled"
|
||||
| "settings"
|
||||
| "sun"
|
||||
| "user"
|
||||
| "warning";
|
||||
}
|
||||
|
||||
@@ -115,6 +121,24 @@
|
||||
<path d="M10 3H9.00003V13H10V3Z" />
|
||||
<path d="M13 6H12V7H13V6Z" />
|
||||
<path d="M14 7H13V8H14V7Z" />
|
||||
{:else if name === "arrow-right-hollow"}
|
||||
<path d="M9 9L3 9L3 10L9 10L9 9Z" />
|
||||
<path d="M9 6L3 6L3 7L9 7L9 6Z" />
|
||||
<path d="M11 4L10 4L10 5L11 5L11 4Z" />
|
||||
<path d="M10 4L9 4L9 5L10 5L10 4Z" />
|
||||
<path d="M10 3L9 3L9 6L10 6L10 3Z" />
|
||||
<path d="M12 5L11 5L11 6L12 6L12 5Z" />
|
||||
<path d="M13 6L12 6L12 7L13 7L13 6Z" />
|
||||
<path d="M13 9L12 9L12 10L13 10L13 9Z" />
|
||||
<path d="M14 8L13 8L13 9L14 9L14 8Z" />
|
||||
<path d="M14 7L13 7L13 8L14 8L14 7Z" />
|
||||
<path d="M12 10L11 10L11 11L12 11L12 10Z" />
|
||||
<path d="M13 9L12 9L12 10L13 10L13 9Z" />
|
||||
<path d="M11 11L10 11L10 12L11 12L11 11Z" />
|
||||
<path d="M12 10L11 10L11 11L12 11L12 10Z" />
|
||||
<path d="M10 11L9 11L9 12L10 12L10 11Z" />
|
||||
<path d="M10 10L9 10L9 13L10 13L10 10Z" />
|
||||
<path d="M2 6L3 6L3 10L2 10L2 6Z" />
|
||||
{:else if name === "attachment"}
|
||||
<path d="M4 4H12V5H4V4Z" />
|
||||
<path d="M4 11H11V12H4V11Z" />
|
||||
@@ -188,6 +212,72 @@
|
||||
<path d="M2 4L3 4L3 13H2V4Z" />
|
||||
<path d="M5 5H11V6H5V5Z" />
|
||||
<path d="M5 8H11V9H5V8Z" />
|
||||
{:else if name === "comment-checkmark"}
|
||||
<path d="M4 2L10 2L10 3L4 3L4 2Z" />
|
||||
<path d="M3 3L4 3L4 4L3 4L3 3Z" />
|
||||
<path d="M12 2L13 2L13 3L12 3L12 2Z" />
|
||||
<path d="M13 2L14 2L14 3L13 3L13 2Z" />
|
||||
<path d="M12 10L13 10L13 11L12 11L12 10Z" />
|
||||
<path d="M5 11L12 11L12 12L5 12L5 11Z" />
|
||||
<path d="M3 13L4 13L4 14L3 14L3 13Z" />
|
||||
<path d="M4 12L5 12L5 13L4 13L4 12Z" />
|
||||
<path d="M5 11L6 11L6 12L5 12L5 11Z" />
|
||||
<path d="M13 5L14 5L14 10L13 10L13 5Z" />
|
||||
<path d="M2 8L3 8L3 13L2 13L2 8Z" />
|
||||
<path d="M5 6L6 6L6 7L5 7L5 6Z" />
|
||||
<path d="M4 5L5 5L5 6L4 6L4 5Z" />
|
||||
<path d="M6 7L7 7L7 8L6 8L6 7Z" />
|
||||
<path d="M7 8L8 8L8 9L7 9L7 8Z" />
|
||||
<path d="M8 7L9 7L9 8L8 8L8 7Z" />
|
||||
<path d="M9 6L10 6L10 7L9 7L9 6Z" />
|
||||
<path d="M10 5L11 5L11 6L10 6L10 5Z" />
|
||||
<path d="M11 4L12 4L12 5L11 5L11 4Z" />
|
||||
<path d="M12 3L13 3L13 4L12 4L12 3Z" />
|
||||
<path d="M11 3L12 3L12 4L11 4L11 3Z" />
|
||||
<path d="M10 4L11 4L11 5L10 5L10 4Z" />
|
||||
<path d="M9 5L10 5L10 6L9 6L9 5Z" />
|
||||
<path d="M8 6L9 6L9 7L8 7L8 6Z" />
|
||||
<path d="M7 7L8 7L8 8L7 8L7 7Z" />
|
||||
<path d="M5 5L6 5L6 6L5 6L5 5Z" />
|
||||
<path d="M6 6L7 6L7 7L6 7L6 6Z" />
|
||||
<path d="M2 4L3 4L3 8L2 8L2 4Z" />
|
||||
{:else if name === "comment-cross"}
|
||||
<path d="M3 3L4 3L4 4L3 4L3 3Z" />
|
||||
<path d="M12 2L13 2L13 3L12 3L12 2Z" />
|
||||
<path d="M13 2L14 2L14 3L13 3L13 2Z" />
|
||||
<path d="M5 11L12 11L12 12L5 12L5 11Z" />
|
||||
<path d="M3 13L4 13L4 14L3 14L3 13Z" />
|
||||
<path d="M4 12L5 12L5 13L4 13L4 12Z" />
|
||||
<path d="M5 11L6 11L6 12L5 12L5 11Z" />
|
||||
<path d="M2 8L3 8L3 13L2 13L2 8Z" />
|
||||
<path d="M7 3L8 3L8 4L7 4L7 3Z" />
|
||||
<path d="M6 2L7 2L7 3L6 3L6 2Z" />
|
||||
<path d="M8 4L9 4L9 5L8 5L8 4Z" />
|
||||
<path d="M10 4L11 4L11 5L10 5L10 4Z" />
|
||||
<path d="M10 6L11 6L11 7L10 7L10 6Z" />
|
||||
<path d="M11 6L12 6L12 7L11 7L11 6Z" />
|
||||
<path d="M11 7L12 7L12 8L11 8L11 7Z" />
|
||||
<path d="M12 7L13 7L13 8L12 8L12 7Z" />
|
||||
<path d="M12 8L13 8L13 9L12 9L12 8Z" />
|
||||
<path d="M13 8L14 8L14 9L13 9L13 8Z" />
|
||||
<path d="M7 8L8 8L8 9L7 9L7 8Z" />
|
||||
<path d="M6 8L7 8L7 9L6 9L6 8Z" />
|
||||
<path d="M8 7L9 7L9 8L8 8L8 7Z" />
|
||||
<path d="M10 6L11 6L11 7L10 7L10 6Z" />
|
||||
<path d="M9 5L11 5L11 7L9 7L9 5Z" />
|
||||
<path d="M11 4L12 4L12 5L11 5L11 4Z" />
|
||||
<path d="M12 3L13 3L13 4L12 4L12 3Z" />
|
||||
<path d="M11 3L12 3L12 4L11 4L11 3Z" />
|
||||
<path d="M9 4L10 4L10 5L9 5L9 4Z" />
|
||||
<path d="M8 6L12 6L12 7L8 7L8 6Z" />
|
||||
<path d="M8 6L9 6L9 7L8 7L8 6Z" />
|
||||
<path d="M7 7L8 7L8 8L7 8L7 7Z" />
|
||||
<path d="M7 2L8 2L8 3L7 3L7 2Z" />
|
||||
<path d="M8 3L9 3L9 4L8 4L8 3Z" />
|
||||
<path d="M2 4L3 4L3 8L2 8L2 4Z" />
|
||||
<path d="M12 10L13 10L13 11L12 11L12 10Z" />
|
||||
<path d="M6 2L7 2L7 3L6 3L6 2Z" />
|
||||
<path d="M4 2L5 2L5 3L4 3L4 2Z" />
|
||||
{:else if name === "copy"}
|
||||
<path d="M6.5 2H13.5V3H6.5V2Z" />
|
||||
<path d="M3.5 5H4.5V6H3.5V5Z" />
|
||||
@@ -387,6 +477,36 @@
|
||||
<path d="M7 8.99999H9V9.99998H7V8.99999Z" />
|
||||
<path d="M10 6.99998V8.99998L9 8.99999L9 6.99999L10 6.99998Z" />
|
||||
<path d="M7 6.99999L7 8.99999L6 8.99998V6.99998L7 6.99999Z" />
|
||||
{:else if name === "label"}
|
||||
<path d="M8.5 2.50003H11.5V3.50003H8.5V2.50003Z" />
|
||||
<path d="M12.5 4.50003H13.5V7.50003H12.5V4.50003Z" />
|
||||
<path d="M11.5 7.50003H12.5L12.5 8.50003H11.5V7.50003Z" />
|
||||
<path d="M9.5 9.50003H10.5V10.5H9.5V9.50003Z" />
|
||||
<path d="M7.5 11.5H8.5V12.5H7.5V11.5Z" />
|
||||
<path d="M8.5 10.5H9.5V11.5L8.5 11.5V10.5Z" />
|
||||
<path d="M6.5 12.5H7.5V13.5H6.5V12.5Z" />
|
||||
<path d="M5.5 11.5H6.5V12.5H5.5V11.5Z" />
|
||||
<path d="M4.5 10.5H5.5V11.5H4.5V10.5Z" />
|
||||
<path d="M3.5 9.50003H4.5L4.5 10.5H3.5V9.50003Z" />
|
||||
<path d="M2.5 8.50003H3.5L3.5 9.50003L2.5 9.50003V8.50003Z" />
|
||||
<path d="M3.5 7.50003H4.5V8.50003L3.5 8.50003L3.5 7.50003Z" />
|
||||
<path d="M4.5 6.50003H5.5V7.50003H4.5V6.50003Z" />
|
||||
<path d="M5.5 5.50003H6.5V6.50003H5.5V5.50003Z" />
|
||||
<path d="M6.5 4.50003H7.5V5.50003H6.5V4.50003Z" />
|
||||
<path d="M7.5 3.50003L8.5 3.50003V4.50003L7.5 4.50003V3.50003Z" />
|
||||
<path d="M9.5 4.50003H10.5V5.50003H9.5V4.50003Z" />
|
||||
<path d="M9.5 6.50003H10.5V7.50003H9.5V6.50003Z" />
|
||||
<path d="M8.5 5.50003H9.5L9.5 6.50003H8.5V5.50003Z" />
|
||||
<path d="M10.5 5.50003L11.5 5.50003V6.50003L10.5 6.50003L10.5 5.50003Z" />
|
||||
<path d="M11.5 3.50003L12.5 3.50003V4.50003H11.5L11.5 3.50003Z" />
|
||||
<path d="M12.5 8.50003H13.5V9.50003H12.5V8.50003Z" />
|
||||
<path d="M9.5 8.50003H10.5L10.5 10.5H9.5L9.5 8.50003Z" />
|
||||
<path d="M9.5 6.50003H10.5L10.5 8.50003H9.5L9.5 6.50003Z" />
|
||||
<path d="M12.5 9.50003H13.5V10.5H12.5V9.50003Z" />
|
||||
<path d="M10.5 10.5H11.5V11.5H10.5L10.5 10.5Z" />
|
||||
<path d="M12.5 10.5H13.5V11.5H12.5V10.5Z" />
|
||||
<path d="M10.5 5.50003L11.5 5.50003V6.50003L10.5 6.50003L10.5 5.50003Z" />
|
||||
<path d="M11.5 11.5H12.5L12.5 12.5H11.5L11.5 11.5Z" />
|
||||
{:else if name === "lock"}
|
||||
<path d="M6 2H10V3H6V2Z" />
|
||||
<path d="M10 3L11 3V4H10V3Z" />
|
||||
@@ -415,6 +535,16 @@
|
||||
<path d="M8 11H9V12H8V11Z" />
|
||||
<path d="M7 10H8V12H7V10Z" />
|
||||
<path d="M3 4.00003H4V6.00003H3V4.00003Z" />
|
||||
{:else if name === "merge"}
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M13 11H14V14H11L11 11H12V7H13V11ZM12 13L12 12H13L13 13H12Z" />
|
||||
<path d="M12 7L9 7L9 9H8L8 8L7 8V7H6V6L7 6V5H8L8 4L9 4V6L12 6L12 7Z" />
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M3 5H2L2 2L5 2L5 5H4L4 11H5L5 14H2L2 11H3L3 5ZM4 4V3H3L3 4L4 4ZM3 12H4L4 13H3V12Z" />
|
||||
{:else if name === "moon"}
|
||||
<path d="M4 3H6V4H4V3Z" />
|
||||
<path d="M3 4L4 4L4 6H3V4Z" />
|
||||
@@ -751,6 +881,27 @@
|
||||
<path d="M6 9L7 9L7 10H6V9Z" />
|
||||
<path d="M9 9L10 9L10 10H9L9 9Z" />
|
||||
<path d="M9 6H10V7H9V6Z" />
|
||||
{:else if name === "user"}
|
||||
<path d="M5 3H6V4H5V3Z" />
|
||||
<path d="M5 6L5 8H4V6H5Z" />
|
||||
<path d="M12 4V8H11V4H12Z" />
|
||||
<path d="M4 4L5 4L5 6H4L4 4Z" />
|
||||
<path d="M10 3H11V4L10 4V3Z" />
|
||||
<path d="M6 2H10V3L6 3V2Z" />
|
||||
<path d="M5 8H6V9H5L5 8Z" />
|
||||
<path d="M6 9H10V10H6V9Z" />
|
||||
<path d="M10 8L11 8V9L10 9L10 8Z" />
|
||||
<path d="M9 9H10V10H9V9Z" />
|
||||
<path d="M10 10H12V11H10L10 10Z" />
|
||||
<path d="M4 10H6L6 11H4V10Z" />
|
||||
<path d="M12 11H13V12H12V11Z" />
|
||||
<path d="M3 11H4L4 12H3V11Z" />
|
||||
<path d="M13 12H14V13H13V12Z" />
|
||||
<path d="M2 12H3L3 13H2V12Z" />
|
||||
<path d="M2 13H14V14H2L2 13Z" />
|
||||
<path d="M5 3L11 3V5H5V3Z" />
|
||||
<path d="M9 6H10V7H9V6Z" />
|
||||
<path d="M6 6H7V7H6V6Z" />
|
||||
{:else if name === "warning"}
|
||||
<path d="M7 2H9V3H7V2Z" />
|
||||
<path d="M6 3H7V5H6V3Z" />
|
||||
|
||||
@@ -0,0 +1,302 @@
|
||||
<script lang="ts">
|
||||
import type { Action } from "@bindings/cob/patch/Action";
|
||||
import type { Operation } from "@bindings/cob/Operation";
|
||||
import type { Author } from "@bindings/cob/Author";
|
||||
|
||||
type FlattenedOperation = Action & {
|
||||
id: string;
|
||||
author: Author;
|
||||
timestamp: number;
|
||||
previous?: Action;
|
||||
};
|
||||
|
||||
type StateTracker = Record<Action["type"], Action>;
|
||||
|
||||
import {
|
||||
absoluteTimestamp,
|
||||
authorForNodeId,
|
||||
formatTimestamp,
|
||||
patchStatusColor,
|
||||
publicKeyFromDid,
|
||||
} from "@app/lib/utils";
|
||||
import Icon from "./Icon.svelte";
|
||||
import Id from "./Id.svelte";
|
||||
import NodeId from "./NodeId.svelte";
|
||||
import { invoke } from "@app/lib/invoke";
|
||||
|
||||
interface Props {
|
||||
patchId: string;
|
||||
activity: Operation<Action>[];
|
||||
}
|
||||
|
||||
/* eslint-disable prefer-const */
|
||||
let { activity, patchId }: Props = $props();
|
||||
/* eslint-enable prefer-const */
|
||||
|
||||
const timeline = $derived(enrichActivity(flattenActivity(activity)));
|
||||
|
||||
function flattenActivity(
|
||||
activity: Operation<Action>[],
|
||||
): FlattenedOperation[] {
|
||||
return activity.flatMap(operation =>
|
||||
operation.actions.map(action => ({
|
||||
...action,
|
||||
id: operation.id,
|
||||
author: operation.author,
|
||||
timestamp: operation.timestamp,
|
||||
})),
|
||||
);
|
||||
}
|
||||
|
||||
function enrichActivity(
|
||||
flatActivity: FlattenedOperation[],
|
||||
): FlattenedOperation[] {
|
||||
const result: FlattenedOperation[] = [];
|
||||
const timelineStateTracker: StateTracker = {} as StateTracker;
|
||||
|
||||
flatActivity.forEach(entry => {
|
||||
if (timelineStateTracker[entry.type]) {
|
||||
result.push({ ...entry, previous: timelineStateTracker[entry.type] });
|
||||
} else {
|
||||
result.push(entry);
|
||||
}
|
||||
timelineStateTracker[entry.type] = entry;
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function itemDiff(previousState: string[], newState: string[]) {
|
||||
const removed = previousState.filter(x => !newState.includes(x));
|
||||
const added = newState.filter(x => !previousState.includes(x));
|
||||
return { removed, added };
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.timeline {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
flex-direction: column;
|
||||
}
|
||||
.timeline-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
.wrapper {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
.icon {
|
||||
padding-top: 3px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="timeline txt-small">
|
||||
{#each timeline as op}
|
||||
{#if op.type === "revision"}
|
||||
{#if op.id === patchId}
|
||||
<div class="timeline-item">
|
||||
<div class="icon" style:color="var(--color-fill-success)">
|
||||
<Icon name="patch" />
|
||||
</div>
|
||||
<div class="wrapper">
|
||||
<NodeId {...authorForNodeId(op.author)} />
|
||||
<div>opened patch <Id id={op.id} variant="oid" /></div>
|
||||
<div title={absoluteTimestamp(op.timestamp)}>
|
||||
{formatTimestamp(op.timestamp)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="timeline-item">
|
||||
<div class="icon">
|
||||
<Icon name="revision" />
|
||||
</div>
|
||||
<div class="wrapper">
|
||||
<NodeId {...authorForNodeId(op.author)} />
|
||||
<div>created a new revision <Id id={op.id} variant="oid" /></div>
|
||||
<div title={absoluteTimestamp(op.timestamp)}>
|
||||
{formatTimestamp(op.timestamp)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{:else if op.type === "lifecycle"}
|
||||
<div class="timeline-item">
|
||||
<div class="icon" style:color={patchStatusColor[op.state.status]}>
|
||||
<Icon name="patch" />
|
||||
</div>
|
||||
<div class="wrapper">
|
||||
<NodeId {...authorForNodeId(op.author)} />
|
||||
{#if op.state.status === "draft"}
|
||||
converted patch to draft
|
||||
{:else if op.state.status === "archived"}
|
||||
archived patch
|
||||
{:else if op.state.status === "open"}
|
||||
reopened patch
|
||||
{/if}
|
||||
<div title={absoluteTimestamp(op.timestamp)}>
|
||||
{formatTimestamp(op.timestamp)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{:else if op.type === "label" && op.previous && op.previous.type === op.type}
|
||||
{@const changed = itemDiff(op.previous?.labels ?? [], op.labels)}
|
||||
{#if changed.added.length || changed.removed.length}
|
||||
<div class="timeline-item">
|
||||
<div class="icon">
|
||||
<Icon name="label" />
|
||||
</div>
|
||||
<div class="wrapper">
|
||||
<NodeId {...authorForNodeId(op.author)} />
|
||||
{#if changed.added.length}
|
||||
added label{changed.added.length > 1 ? "s" : ""}
|
||||
{#each changed.added as label}
|
||||
<b>{label}</b>
|
||||
{/each}
|
||||
{/if}
|
||||
{#if changed.removed.length}
|
||||
removed label{changed.removed.length > 1 ? "s" : ""}
|
||||
{#each changed.removed as label}
|
||||
<b>{label}</b>
|
||||
{/each}
|
||||
{/if}
|
||||
<div title={absoluteTimestamp(op.timestamp)}>
|
||||
{formatTimestamp(op.timestamp)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{:else if op.type === "assign" && op.previous && op.previous.type === op.type}
|
||||
{@const changed = itemDiff(op.previous?.assignees ?? [], op.assignees)}
|
||||
{#if changed.added.length || changed.removed.length}
|
||||
<div class="timeline-item">
|
||||
<div class="icon">
|
||||
<Icon name="user" />
|
||||
</div>
|
||||
<div class="wrapper">
|
||||
<NodeId {...authorForNodeId(op.author)} />
|
||||
{#if changed.added.length}
|
||||
assigned
|
||||
{#each changed.added as assignee}
|
||||
{#await invoke<string | null>( "alias", { nid: publicKeyFromDid(assignee) }, ) then alias}
|
||||
<NodeId
|
||||
{...authorForNodeId({
|
||||
did: assignee,
|
||||
alias: alias ?? undefined,
|
||||
})} />
|
||||
{/await}
|
||||
{/each}
|
||||
{/if}
|
||||
{#if changed.removed.length}
|
||||
unassigned
|
||||
{#each changed.removed as assignee}
|
||||
{#await invoke<string | null>( "alias", { nid: publicKeyFromDid(assignee) }, ) then alias}
|
||||
<NodeId
|
||||
{...authorForNodeId({
|
||||
did: assignee,
|
||||
alias: alias ?? undefined,
|
||||
})} />
|
||||
{/await}
|
||||
{/each}
|
||||
{/if}
|
||||
<div title={absoluteTimestamp(op.timestamp)}>
|
||||
{formatTimestamp(op.timestamp)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{:else if op.type === "merge"}
|
||||
<div class="timeline-item">
|
||||
<div class="icon" style:color="var(--color-fill-primary)">
|
||||
<Icon name="patch" />
|
||||
</div>
|
||||
<div class="wrapper">
|
||||
<NodeId {...authorForNodeId(op.author)} />
|
||||
<div>
|
||||
merged patch at revision <Id id={op.revision} variant="oid" />
|
||||
</div>
|
||||
<div title={absoluteTimestamp(op.timestamp)}>
|
||||
{formatTimestamp(op.timestamp)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{:else if op.type === "edit"}
|
||||
{#if op.previous && op.previous.type === op.type}
|
||||
<div class="timeline-item">
|
||||
<div class="icon">
|
||||
<Icon name="pen" />
|
||||
</div>
|
||||
<div class="wrapper">
|
||||
<NodeId {...authorForNodeId(op.author)} />
|
||||
changed title
|
||||
<s>{op.previous.title}</s>
|
||||
-> {op.title}
|
||||
<div title={absoluteTimestamp(op.timestamp)}>
|
||||
{formatTimestamp(op.timestamp)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{:else if op.type === "review"}
|
||||
<div class="timeline-item">
|
||||
{#if op.verdict === "accept"}
|
||||
<div class="icon" style:color="var(--color-foreground-success)">
|
||||
<Icon name="comment-checkmark" />
|
||||
</div>
|
||||
<div class="wrapper">
|
||||
<NodeId {...authorForNodeId(op.author)} />
|
||||
accepted revision <Id id={op.revision} variant="oid" />
|
||||
<div title={absoluteTimestamp(op.timestamp)}>
|
||||
{formatTimestamp(op.timestamp)}
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="icon" style:color="var(--color-foreground-red)">
|
||||
<Icon name="comment-cross" />
|
||||
</div>
|
||||
<div class="wrapper">
|
||||
<NodeId {...authorForNodeId(op.author)} />
|
||||
rejected revision <Id id={op.revision} variant="oid" />
|
||||
<div title={absoluteTimestamp(op.timestamp)}>
|
||||
{formatTimestamp(op.timestamp)}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{:else if op.type === "review.comment"}
|
||||
<div class="timeline-item">
|
||||
<div class="icon">
|
||||
<Icon name="comment" />
|
||||
</div>
|
||||
<div class="wrapper">
|
||||
<NodeId {...authorForNodeId(op.author)} />
|
||||
{op.reply_to ? "replied to a comment" : "commented"} on review <Id
|
||||
id={op.review}
|
||||
variant="oid" />
|
||||
<div title={absoluteTimestamp(op.timestamp)}>
|
||||
{formatTimestamp(op.timestamp)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{:else if op.type === "revision.comment"}
|
||||
<div class="timeline-item">
|
||||
<div class="icon">
|
||||
<Icon name="comment" />
|
||||
</div>
|
||||
<div class="wrapper">
|
||||
<NodeId {...authorForNodeId(op.author)} />
|
||||
{op.replyTo ? "replied to a comment" : "commented"} on revision <Id
|
||||
id={op.revision}
|
||||
variant="oid" />
|
||||
<div title={absoluteTimestamp(op.timestamp)}>
|
||||
{formatTimestamp(op.timestamp)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
@@ -1,28 +0,0 @@
|
||||
<script lang="ts">
|
||||
import type { Operation } from "@bindings/cob/Operation";
|
||||
import type { Action } from "@bindings/cob/patch/Action";
|
||||
|
||||
import { authorForNodeId, formatTimestamp } from "@app/lib/utils";
|
||||
|
||||
import Border from "@app/components/Border.svelte";
|
||||
import NodeId from "@app/components/NodeId.svelte";
|
||||
import PatchStateBadge from "./PatchStateBadge.svelte";
|
||||
|
||||
interface Props {
|
||||
action: Extract<Action, { type: "lifecycle" }>;
|
||||
op: Operation<Action>;
|
||||
}
|
||||
|
||||
const { op, action }: Props = $props();
|
||||
</script>
|
||||
|
||||
<Border variant="float" stylePadding="1rem">
|
||||
<div class="txt-small">
|
||||
<div class="global-flex txt-small">
|
||||
<NodeId {...authorForNodeId(op.author)} />
|
||||
changed status to
|
||||
<PatchStateBadge state={action.state} />
|
||||
{formatTimestamp(op.timestamp)}
|
||||
</div>
|
||||
</div>
|
||||
</Border>
|
||||
+33
-12
@@ -1,10 +1,10 @@
|
||||
<script lang="ts">
|
||||
import type { Operation } from "@bindings/cob/Operation";
|
||||
import type { Action } from "@bindings/cob/patch/Action";
|
||||
import type { Author } from "@bindings/cob/Author";
|
||||
import type { Config } from "@bindings/config/Config";
|
||||
import type { Diff } from "@bindings/diff/Diff";
|
||||
import type { Embed } from "@bindings/cob/thread/Embed";
|
||||
import type { Operation } from "@bindings/cob/Operation";
|
||||
import type { PaginatedQuery } from "@bindings/cob/PaginatedQuery";
|
||||
import type { Patch } from "@bindings/cob/patch/Patch";
|
||||
import type { PatchStatus } from "./router";
|
||||
@@ -38,6 +38,7 @@
|
||||
import PatchStateBadge from "@app/components/PatchStateBadge.svelte";
|
||||
import PatchStateButton from "@app/components/PatchStateButton.svelte";
|
||||
import PatchTeaser from "@app/components/PatchTeaser.svelte";
|
||||
import PatchTimeline from "@app/components/PatchTimeline.svelte";
|
||||
import Sidebar from "@app/components/Sidebar.svelte";
|
||||
import TextInput from "@app/components/TextInput.svelte";
|
||||
|
||||
@@ -73,6 +74,8 @@
|
||||
let assigneesSaveInProgress: boolean = $state(false);
|
||||
let tab: "patch" | "revisions" = $state("patch");
|
||||
|
||||
let hideTimeline = $state(false);
|
||||
|
||||
$effect(() => {
|
||||
items = patches.content;
|
||||
cursor = patches.cursor;
|
||||
@@ -86,6 +89,7 @@
|
||||
tab = "patch";
|
||||
editingTitle = false;
|
||||
updatedTitle = patch.title;
|
||||
hideTimeline = false;
|
||||
});
|
||||
|
||||
const project = $derived(repo.payloads["xyz.radicle.project"]!);
|
||||
@@ -111,6 +115,10 @@
|
||||
rid: rid,
|
||||
id: patchId,
|
||||
});
|
||||
activity = await invoke<Operation<Action>[]>("activity_by_patch", {
|
||||
rid: repo.rid,
|
||||
id: patch.id,
|
||||
});
|
||||
}
|
||||
|
||||
async function loadMoreSecondColumn() {
|
||||
@@ -168,7 +176,7 @@
|
||||
}
|
||||
|
||||
async function reload() {
|
||||
[config, repo, patches, patch, revisions] = await Promise.all([
|
||||
[config, repo, patches, patch, revisions, activity] = await Promise.all([
|
||||
invoke<Config>("config"),
|
||||
invoke<RepoInfo>("repo_by_id", {
|
||||
rid: repo.rid,
|
||||
@@ -185,6 +193,10 @@
|
||||
rid: repo.rid,
|
||||
id: patch.id,
|
||||
}),
|
||||
invoke<Operation<Action>[]>("activity_by_patch", {
|
||||
rid: repo.rid,
|
||||
id: patch.id,
|
||||
}),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -323,7 +335,7 @@
|
||||
}
|
||||
|
||||
.patch-body {
|
||||
margin-top: 1rem;
|
||||
margin: 1rem 0;
|
||||
position: relative;
|
||||
}
|
||||
/* We put the background and clip-path in a separate element to prevent
|
||||
@@ -357,6 +369,9 @@
|
||||
margin-bottom: 0.5rem;
|
||||
color: var(--color-foreground-dim);
|
||||
}
|
||||
.hide-timeline {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
|
||||
<Layout {loadMoreSecondColumn} publicKey={config.publicKey}>
|
||||
@@ -539,16 +554,22 @@
|
||||
) && partial(editRevision, revisions[0].id)}>
|
||||
</CommentComponent>
|
||||
</div>
|
||||
<div class="connector"></div>
|
||||
|
||||
<div>
|
||||
{#each activity as op}
|
||||
{#each op.actions as action}
|
||||
{#if action.type === "revision"}
|
||||
<div>New revision created {action.oid}</div>
|
||||
<div class="connector"></div>
|
||||
{/if}
|
||||
{/each}
|
||||
{/each}
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
||||
<div
|
||||
role="button"
|
||||
tabindex="0"
|
||||
class="txt-semibold global-flex"
|
||||
style:margin-bottom="1rem"
|
||||
style:cursor="pointer"
|
||||
onclick={() => (hideTimeline = !hideTimeline)}>
|
||||
<Icon
|
||||
name={hideTimeline ? "chevron-right" : "chevron-down"} />Timeline
|
||||
</div>
|
||||
<div class:hide-timeline={hideTimeline}>
|
||||
<PatchTimeline {activity} patchId={patch.id} />
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
{@const revision = revisions.slice(-1)[0]}
|
||||
|
||||
Reference in New Issue
Block a user