mirror of
https://seed.radicle.dev/rad:z4V1sjrXqjvFdnCUbxPFqd5p4DtH5.git
synced 2026-10-18 19:30:27 +08:00
Improve patch timeline and add description
Signed-off-by: Sebastian Martinez <me@sebastinez.dev>
This commit is contained in:
+16
-2
@@ -1,5 +1,11 @@
|
||||
import type { BaseUrl } from "./lib/fetcher.js";
|
||||
import type { Blob, Project, Remote, Tree } from "./lib/project.js";
|
||||
import type {
|
||||
Blob,
|
||||
Project,
|
||||
Remote,
|
||||
Tree,
|
||||
DiffResponse,
|
||||
} from "./lib/project.js";
|
||||
import type { Comment } from "./lib/project/comment.js";
|
||||
import type {
|
||||
Commit,
|
||||
@@ -9,7 +15,13 @@ import type {
|
||||
HunkLine,
|
||||
} from "./lib/project/commit.js";
|
||||
import type { Issue, IssueState } from "./lib/project/issue.js";
|
||||
import type { Merge, Patch, PatchState, Review } from "./lib/project/patch.js";
|
||||
import type {
|
||||
Merge,
|
||||
Patch,
|
||||
PatchState,
|
||||
Review,
|
||||
Revision,
|
||||
} from "./lib/project/patch.js";
|
||||
import type { RequestOptions, Method } from "./lib/fetcher.js";
|
||||
import type { ZodSchema } from "zod";
|
||||
|
||||
@@ -27,6 +39,7 @@ export type {
|
||||
CommitHeader,
|
||||
Diff,
|
||||
DiffAddedDeletedModifiedChangeset,
|
||||
DiffResponse,
|
||||
HunkLine,
|
||||
Issue,
|
||||
IssueState,
|
||||
@@ -36,6 +49,7 @@ export type {
|
||||
Project,
|
||||
Remote,
|
||||
Review,
|
||||
Revision,
|
||||
Tree,
|
||||
};
|
||||
|
||||
|
||||
@@ -153,7 +153,7 @@ const remoteSchema = strictObject({
|
||||
|
||||
const remotesSchema = array(remoteSchema) satisfies ZodSchema<Remote[]>;
|
||||
|
||||
interface DiffResponse {
|
||||
export interface DiffResponse {
|
||||
commits: CommitHeader[];
|
||||
diff: Diff;
|
||||
}
|
||||
|
||||
@@ -85,7 +85,7 @@ const reviewSchema = strictObject({
|
||||
timestamp: number(),
|
||||
}) satisfies ZodSchema<Review>;
|
||||
|
||||
interface Revision {
|
||||
export interface Revision {
|
||||
id: string;
|
||||
description: string;
|
||||
base: string;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
REV=414477a31676d5e75efa7f3e8dc6624bcf2b2e52
|
||||
REV=b29321dbf7999ec7ec9b7ac9192071e512ada407
|
||||
|
||||
REPO_ROOT=$(git rev-parse --show-toplevel)
|
||||
FIXTURE=$REPO_ROOT/tests/fixtures/seeds/palm.tar.bz2
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
.authorship {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
color: var(--color-foreground-6);
|
||||
color: inherit;
|
||||
padding: 0.125rem 0;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
@@ -29,7 +29,7 @@
|
||||
}
|
||||
</style>
|
||||
|
||||
<span class="authorship txt-tiny" title={relativeTimestamp(timestamp)}>
|
||||
<span class="authorship txt-tiny">
|
||||
{#if !noAvatar}
|
||||
<Avatar inline nodeId={authorId} />
|
||||
{/if}
|
||||
@@ -39,14 +39,16 @@
|
||||
<span class="id layout-mobile">
|
||||
{formatNodeId(authorId).replace("did:key:", "")}
|
||||
</span>
|
||||
<span class="body">
|
||||
{#if !caption}
|
||||
<slot />
|
||||
{:else}
|
||||
{#if !caption}
|
||||
<slot />
|
||||
{:else}
|
||||
<span class="body">
|
||||
{caption}
|
||||
{/if}
|
||||
</span>
|
||||
</span>
|
||||
{/if}
|
||||
{#if timestamp}
|
||||
{formatTimestamp(timestamp)}
|
||||
<span title={relativeTimestamp(timestamp)}>
|
||||
{formatTimestamp(timestamp)}
|
||||
</span>
|
||||
{/if}
|
||||
</span>
|
||||
|
||||
@@ -20,13 +20,12 @@
|
||||
|
||||
<style>
|
||||
.comment {
|
||||
margin-bottom: 1rem;
|
||||
display: flex;
|
||||
}
|
||||
.card {
|
||||
flex: 1;
|
||||
border: 1px solid var(--color-foreground-4);
|
||||
border-radius: var(--border-radius);
|
||||
background-color: var(--color-foreground-1);
|
||||
}
|
||||
.card-header {
|
||||
display: flex;
|
||||
@@ -37,7 +36,7 @@
|
||||
}
|
||||
.card-body {
|
||||
font-size: var(--font-size-small);
|
||||
padding: 0rem 1rem 1rem 1rem;
|
||||
padding: 0 1rem 1rem 1rem;
|
||||
}
|
||||
.actions {
|
||||
display: flex;
|
||||
@@ -52,12 +51,7 @@
|
||||
<div class="comment" {id}>
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<div class="layout-desktop">
|
||||
<Authorship {caption} {authorId} {timestamp} />
|
||||
</div>
|
||||
<div class="layout-mobile">
|
||||
<Authorship {authorId} {timestamp} />
|
||||
</div>
|
||||
<Authorship {caption} {authorId} {timestamp} />
|
||||
<div class="actions">
|
||||
{#if showReplyIcon}
|
||||
<Button
|
||||
|
||||
@@ -57,7 +57,7 @@
|
||||
on:click={() => onSelect(item)}
|
||||
title={item.title}>
|
||||
<slot name="item" {item}>
|
||||
{item.value}
|
||||
{item.title}
|
||||
{#if item.badge}
|
||||
<Badge variant="primary">{item.badge}</Badge>
|
||||
{/if}
|
||||
|
||||
@@ -8,11 +8,14 @@
|
||||
<style>
|
||||
.error {
|
||||
padding: 1rem;
|
||||
font-size: var(--font-size-regular);
|
||||
font-family: var(--font-family-sans-serif);
|
||||
color: var(--color-negative);
|
||||
border-radius: var(--border-radius);
|
||||
background-color: var(--color-negative-3);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
.stack-trace {
|
||||
display: flex;
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
import { twemoji } from "@app/lib/utils";
|
||||
|
||||
export let content: string;
|
||||
export let fontSize: "small" | "medium" = "small";
|
||||
export let fontSize: "tiny" | "small" | "medium" = "small";
|
||||
|
||||
marked.use({
|
||||
renderer,
|
||||
@@ -34,6 +34,7 @@
|
||||
class="markdown"
|
||||
use:twemoji
|
||||
class:txt-medium={fontSize === "medium"}
|
||||
class:txt-small={fontSize === "small"}>
|
||||
class:txt-small={fontSize === "small"}
|
||||
class:txt-tiny={fontSize === "tiny"}>
|
||||
{@html render(content)}
|
||||
</span>
|
||||
|
||||
@@ -280,6 +280,10 @@
|
||||
.markdown :global(.list-content) {
|
||||
margin: 1rem 0;
|
||||
}
|
||||
/* Allows the parent to specify its own bottom margin */
|
||||
.markdown :global(:last-child) {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.markdown :global(li > ul) {
|
||||
margin-bottom: 0rem;
|
||||
}
|
||||
|
||||
@@ -14,8 +14,6 @@
|
||||
export let disabled: boolean = false;
|
||||
export let loading: boolean = false;
|
||||
export let valid: boolean = false;
|
||||
// Changes the background color to the background of the page
|
||||
export let transparent: boolean = false;
|
||||
export let validationMessage: string | undefined = undefined;
|
||||
|
||||
const dispatch = createEventDispatcher<{
|
||||
@@ -74,9 +72,6 @@
|
||||
color: var(--color-secondary);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.transparent {
|
||||
background: var(--color-background) !important;
|
||||
}
|
||||
.regular {
|
||||
border: 1px solid var(--color-secondary);
|
||||
padding: 1rem 1.5rem;
|
||||
@@ -148,7 +143,6 @@
|
||||
</div>
|
||||
|
||||
<input
|
||||
class:transparent
|
||||
class:regular={variant === "regular"}
|
||||
class:form={variant === "form"}
|
||||
style:padding-left={leftContainerWidth
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<script lang="ts">
|
||||
<script lang="ts" strictEvents>
|
||||
import type { Comment } from "@httpd-client";
|
||||
|
||||
import Button from "@app/components/Button.svelte";
|
||||
@@ -10,7 +10,6 @@
|
||||
|
||||
export let thread: { root: Comment; replies: Comment[] };
|
||||
export let rawPath: string;
|
||||
export let isDescription = false;
|
||||
export let showReplyTextarea = false;
|
||||
|
||||
let replyText = "";
|
||||
@@ -53,6 +52,7 @@
|
||||
<style>
|
||||
.reply {
|
||||
margin-left: 3rem;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
.actions {
|
||||
display: flex;
|
||||
@@ -62,14 +62,16 @@
|
||||
}
|
||||
</style>
|
||||
|
||||
<CommentComponent
|
||||
{rawPath}
|
||||
id={root.id}
|
||||
authorId={root.author.id}
|
||||
timestamp={root.timestamp}
|
||||
body={root.body}
|
||||
showReplyIcon={Boolean($sessionStore) && !isDescription}
|
||||
on:toggleReply={toggleReply} />
|
||||
<div style:margin-top="1rem">
|
||||
<CommentComponent
|
||||
{rawPath}
|
||||
id={root.id}
|
||||
authorId={root.author.id}
|
||||
timestamp={root.timestamp}
|
||||
body={root.body}
|
||||
showReplyIcon={Boolean($sessionStore)}
|
||||
on:toggleReply={toggleReply} />
|
||||
</div>
|
||||
{#each replies as reply}
|
||||
<div class="reply">
|
||||
<CommentComponent
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
const dispatch = createEventDispatcher<{ save: string[] }>();
|
||||
|
||||
export let action: "create" | "edit" | "view";
|
||||
export let edit: boolean = false;
|
||||
export let editInProgress: boolean = false;
|
||||
export let assignees: string[] = [];
|
||||
|
||||
let updatedAssignees: string[] = assignees;
|
||||
@@ -45,9 +45,6 @@
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.metadata-section {
|
||||
margin-bottom: 4rem;
|
||||
}
|
||||
.header {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
@@ -75,28 +72,28 @@
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="metadata-section">
|
||||
<div>
|
||||
<div class="header">
|
||||
<span>Assignees</span>
|
||||
{#if action === "edit"}
|
||||
{#if !edit}
|
||||
{#if editInProgress}
|
||||
<Button
|
||||
size="tiny"
|
||||
variant="text"
|
||||
on:click={() => {
|
||||
edit = !edit;
|
||||
dispatch("save", updatedAssignees);
|
||||
editInProgress = !editInProgress;
|
||||
}}>
|
||||
edit
|
||||
save
|
||||
</Button>
|
||||
{:else}
|
||||
<Button
|
||||
size="tiny"
|
||||
variant="text"
|
||||
on:click={() => {
|
||||
dispatch("save", updatedAssignees);
|
||||
edit = !edit;
|
||||
editInProgress = !editInProgress;
|
||||
}}>
|
||||
save
|
||||
edit
|
||||
</Button>
|
||||
{/if}
|
||||
{/if}
|
||||
@@ -105,7 +102,7 @@
|
||||
{#each updatedAssignees as assignee, key (assignee)}
|
||||
<Chip
|
||||
on:remove={removeAssignee}
|
||||
removeable={edit || action === "create"}
|
||||
removeable={editInProgress || action === "create"}
|
||||
{key}>
|
||||
<div class="chip-content">
|
||||
<Avatar inline nodeId={assignee} />
|
||||
@@ -116,7 +113,7 @@
|
||||
<div class="empty">No assignees</div>
|
||||
{/each}
|
||||
</div>
|
||||
{#if edit || action === "create"}
|
||||
{#if editInProgress || action === "create"}
|
||||
<div style:margin-bottom="1rem">
|
||||
<TextInput
|
||||
bind:value={inputValue}
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
<script lang="ts" strictEvents>
|
||||
import { createEventDispatcher } from "svelte";
|
||||
|
||||
import { formatObjectId } from "@app/lib/utils";
|
||||
|
||||
import * as utils from "@app/lib/utils";
|
||||
import Button from "@app/components/Button.svelte";
|
||||
import Clipboard from "@app/components/Clipboard.svelte";
|
||||
import InlineMarkdown from "@app/components/InlineMarkdown.svelte";
|
||||
@@ -19,76 +18,58 @@
|
||||
|
||||
<style>
|
||||
header {
|
||||
background: var(--color-foreground-1);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.3rem;
|
||||
border-radius: var(--border-radius);
|
||||
margin-bottom: 1rem;
|
||||
border: 1px solid var(--color-foreground-3);
|
||||
padding: 1rem;
|
||||
}
|
||||
.summary {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 1rem;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 0.5rem;
|
||||
padding-right: 0.5rem;
|
||||
}
|
||||
.summary-title {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: baseline;
|
||||
gap: 0.5rem;
|
||||
width: 90%;
|
||||
}
|
||||
.id {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
font-size: var(--font-size-tiny);
|
||||
font-family: var(--font-family-monospace);
|
||||
color: var(--color-foreground-6);
|
||||
}
|
||||
.title {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.summary-state {
|
||||
.subtitle {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 1rem;
|
||||
border-radius: var(--border-radius);
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
font-size: var(--font-size-tiny);
|
||||
font-family: var(--font-family-monospace);
|
||||
color: var(--color-foreground-6);
|
||||
}
|
||||
.summary {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
.id {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.description {
|
||||
font-size: var(--font-size-small);
|
||||
margin-top: 1rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
<header>
|
||||
<div class="summary">
|
||||
<div class="summary-title txt-medium">
|
||||
{#if editable}
|
||||
<TextInput transparent variant="form" bind:value={title} />
|
||||
{:else}
|
||||
{#if title}
|
||||
<div class="title">
|
||||
<InlineMarkdown fontSize="medium" content={title} />
|
||||
</div>
|
||||
{:else}
|
||||
<span class="txt-missing">No title</span>
|
||||
{/if}
|
||||
<slot name="revision" />
|
||||
{#if id}
|
||||
<div class="id">
|
||||
<div class="layout-desktop">{id}</div>
|
||||
<div class="layout-mobile">
|
||||
{formatObjectId(id)}
|
||||
</div>
|
||||
<Clipboard small text={id} />
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
<div class="summary txt-medium">
|
||||
{#if editable}
|
||||
<TextInput variant="form" placeholder="Title" bind:value={title} />
|
||||
{:else if title}
|
||||
<div class="title">
|
||||
<InlineMarkdown fontSize="medium" content={title} />
|
||||
</div>
|
||||
{:else}
|
||||
<span class="txt-missing">No title</span>
|
||||
{/if}
|
||||
{#if action === "edit"}
|
||||
<Button
|
||||
variant="text"
|
||||
variant="foreground"
|
||||
size="small"
|
||||
on:click={() => {
|
||||
editable = !editable;
|
||||
@@ -102,7 +83,17 @@
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="summary-state">
|
||||
<div class="subtitle">
|
||||
<slot name="state" />
|
||||
{#if id}
|
||||
<div class="id">
|
||||
{utils.formatObjectId(id)}
|
||||
<Clipboard text={id} small />
|
||||
</div>
|
||||
{/if}
|
||||
<slot name="author" />
|
||||
</div>
|
||||
<div class="description">
|
||||
<slot name="description" />
|
||||
</div>
|
||||
</header>
|
||||
|
||||
@@ -0,0 +1,263 @@
|
||||
<script lang="ts">
|
||||
import type { BaseUrl, DiffResponse } from "@httpd-client";
|
||||
import type { Timeline } from "@app/views/projects/Patch.svelte";
|
||||
|
||||
import * as utils from "@app/lib/utils";
|
||||
import { onMount } from "svelte";
|
||||
import { HttpdClient } from "@httpd-client";
|
||||
|
||||
import Authorship from "@app/components/Authorship.svelte";
|
||||
import Avatar from "@app/components/Avatar.svelte";
|
||||
import Clipboard from "@app/components/Clipboard.svelte";
|
||||
import CommentComponent from "@app/components/Comment.svelte";
|
||||
import DiffStatBadge from "@app/components/DiffStatBadge.svelte";
|
||||
import ErrorMessage from "@app/components/ErrorMessage.svelte";
|
||||
import Icon from "@app/components/Icon.svelte";
|
||||
import InlineMarkdown from "@app/components/InlineMarkdown.svelte";
|
||||
import ProjectLink from "@app/components/ProjectLink.svelte";
|
||||
import ThreadComponent from "@app/components/Thread.svelte";
|
||||
|
||||
export let authorId: string;
|
||||
export let baseUrl: BaseUrl;
|
||||
export let expanded: boolean = true;
|
||||
export let patchId: string;
|
||||
export let projectHead: string;
|
||||
export let projectId: string;
|
||||
export let revisionBase: string;
|
||||
export let revisionId: string;
|
||||
export let revisionOid: string;
|
||||
export let revisionTimestamp: number;
|
||||
export let timelines: Timeline[];
|
||||
|
||||
const api = new HttpdClient(baseUrl);
|
||||
|
||||
function formatVerdict(revision: string, verdict?: string | null) {
|
||||
switch (verdict) {
|
||||
case "accept":
|
||||
return `accepted revision ${utils.formatObjectId(revision)}`;
|
||||
case "reject":
|
||||
return `rejected revision ${utils.formatObjectId(revision)}`;
|
||||
default:
|
||||
return `left a comment on revision ${utils.formatObjectId(revision)}`;
|
||||
}
|
||||
}
|
||||
|
||||
let response: DiffResponse | undefined = undefined;
|
||||
let error: any | undefined = undefined;
|
||||
|
||||
onMount(async () => {
|
||||
try {
|
||||
response = await api.project.getDiff(
|
||||
projectId,
|
||||
revisionBase,
|
||||
revisionOid,
|
||||
);
|
||||
} catch (err: any) {
|
||||
error = err;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.action {
|
||||
background-color: var(--color-foreground-1);
|
||||
border-radius: var(--border-radius);
|
||||
padding: 0.5rem 1rem;
|
||||
}
|
||||
.action-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.merge {
|
||||
color: var(--color-primary);
|
||||
background-color: var(--color-primary-3);
|
||||
}
|
||||
.positive-review {
|
||||
color: var(--color-positive);
|
||||
background-color: var(--color-positive-3);
|
||||
}
|
||||
.revision {
|
||||
border: 1px solid var(--color-foreground-3);
|
||||
border-radius: var(--border-radius-small);
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.revision-header {
|
||||
height: 3rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
background: none;
|
||||
padding: 1rem;
|
||||
padding-right: 1.5rem;
|
||||
}
|
||||
.revision-name {
|
||||
display: flex;
|
||||
user-select: none;
|
||||
}
|
||||
.revision-data {
|
||||
gap: 0.5rem;
|
||||
display: flex;
|
||||
}
|
||||
.revision-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
padding: 0 1.5rem;
|
||||
margin-bottom: 1rem;
|
||||
border-radius: var(--border-radius-small);
|
||||
}
|
||||
.expand-button {
|
||||
margin-right: 0.5rem;
|
||||
user-select: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
.commit-event {
|
||||
color: var(--color-foreground-6);
|
||||
padding: 0.5rem 0.5rem 0.5rem 1rem;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.commit-event span {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="revision">
|
||||
<div class="revision-header">
|
||||
<div class="revision-name">
|
||||
<div class="expand-button">
|
||||
<Icon
|
||||
name={expanded ? "chevron-down" : "chevron-right"}
|
||||
on:click={() => (expanded = !expanded)} />
|
||||
</div>
|
||||
<span>
|
||||
Revision {utils.formatObjectId(revisionId)}
|
||||
</span>
|
||||
<Clipboard text={revisionId} small />
|
||||
</div>
|
||||
<div class="txt-small" />
|
||||
<div class="revision-data">
|
||||
{#if response?.diff.stats}
|
||||
{@const { insertions, deletions } = response.diff.stats}
|
||||
<DiffStatBadge {insertions} {deletions} />
|
||||
{/if}
|
||||
<span class="layout-desktop txt-small">
|
||||
{utils.formatTimestamp(revisionTimestamp)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{#if expanded}
|
||||
<div class="revision-body">
|
||||
{#each timelines as element}
|
||||
{#if element.type === "thread"}
|
||||
<ThreadComponent
|
||||
rawPath={utils.getRawBasePath(projectId, baseUrl, projectHead)}
|
||||
thread={element.inner}
|
||||
on:reply />
|
||||
{:else if element.type === "revision"}
|
||||
{@const caption =
|
||||
patchId === element.inner.id
|
||||
? "opened this patch"
|
||||
: `updated to ${utils.formatObjectId(element.inner.id)}`}
|
||||
{#if element.inner.description}
|
||||
<CommentComponent
|
||||
{caption}
|
||||
{authorId}
|
||||
timestamp={element.timestamp}
|
||||
rawPath={utils.getRawBasePath(projectId, baseUrl, projectHead)}
|
||||
body={element.inner.description} />
|
||||
{:else}
|
||||
<div class="action txt-tiny">
|
||||
<Authorship {authorId} timestamp={element.timestamp}>
|
||||
{caption}
|
||||
</Authorship>
|
||||
</div>
|
||||
{/if}
|
||||
{#if response?.commits}
|
||||
<div class="action txt-tiny">
|
||||
{#each response.commits as commit}
|
||||
<div class="commit-event">
|
||||
<span>
|
||||
<Avatar inline nodeId={authorId} />
|
||||
<ProjectLink
|
||||
projectParams={{
|
||||
view: { resource: "commits" },
|
||||
revision: commit.id,
|
||||
search: undefined,
|
||||
}}>
|
||||
<InlineMarkdown
|
||||
content={commit.summary}
|
||||
fontSize="tiny" />
|
||||
</ProjectLink>
|
||||
</span>
|
||||
<span>
|
||||
{utils.formatCommit(commit.id)}
|
||||
</span>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
{#if error}
|
||||
<div class="txt-monospace">
|
||||
<ErrorMessage
|
||||
message="Failed to load diff for this revision."
|
||||
stackTrace={error.stack.toString()} />
|
||||
</div>
|
||||
{/if}
|
||||
{:else if element.type === "merge"}
|
||||
<div class="action merge layout-desktop txt-tiny">
|
||||
<div class="action-content">
|
||||
<Authorship
|
||||
authorId={element.inner.node}
|
||||
timestamp={element.timestamp}>
|
||||
merged
|
||||
{utils.formatCommit(element.inner.commit)}
|
||||
</Authorship>
|
||||
</div>
|
||||
</div>
|
||||
<div class="action merge layout-mobile txt-tiny">
|
||||
<div class="action-content">
|
||||
<Authorship authorId={element.inner.node}>
|
||||
merged
|
||||
{utils.formatCommit(element.inner.commit)}
|
||||
</Authorship>
|
||||
</div>
|
||||
</div>
|
||||
{:else if element.type === "review"}
|
||||
{@const [revisionId, author, review] = element.inner}
|
||||
<div
|
||||
class="action layout-desktop txt-tiny"
|
||||
class:positive-review={element.inner[2].verdict === "accept"}>
|
||||
<div class="action-content">
|
||||
<Authorship authorId={author} timestamp={element.timestamp}>
|
||||
{formatVerdict(revisionId, review.verdict)}
|
||||
</Authorship>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="layout-mobile txt-tiny"
|
||||
class:positive-review={element.inner[2].verdict === "accept"}>
|
||||
<div class="action-content">
|
||||
<Authorship authorId={author}>
|
||||
{formatVerdict(revisionId, review.verdict)}
|
||||
</Authorship>
|
||||
</div>
|
||||
</div>
|
||||
{#if review.comment}
|
||||
<CommentComponent
|
||||
caption="left a comment"
|
||||
authorId={author}
|
||||
timestamp={review.timestamp}
|
||||
rawPath={utils.getRawBasePath(projectId, baseUrl, projectHead)}
|
||||
body={review.comment} />
|
||||
{/if}
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -8,7 +8,7 @@
|
||||
const dispatch = createEventDispatcher<{ save: string[] }>();
|
||||
|
||||
export let action: "create" | "edit" | "view" = "view";
|
||||
export let edit: boolean = false;
|
||||
export let editInProgress: boolean = false;
|
||||
export let tags: string[] = [];
|
||||
|
||||
let updatedTags: string[] = tags;
|
||||
@@ -42,9 +42,6 @@
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.metadata-section {
|
||||
margin-bottom: 4rem;
|
||||
}
|
||||
.metadata-section-header {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
@@ -70,28 +67,28 @@
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="metadata-section">
|
||||
<div>
|
||||
<div class="metadata-section-header">
|
||||
<span>Tags</span>
|
||||
{#if action === "edit"}
|
||||
{#if !edit}
|
||||
{#if editInProgress}
|
||||
<Button
|
||||
size="tiny"
|
||||
variant="text"
|
||||
on:click={() => {
|
||||
edit = !edit;
|
||||
dispatch("save", updatedTags);
|
||||
editInProgress = !editInProgress;
|
||||
}}>
|
||||
edit
|
||||
save
|
||||
</Button>
|
||||
{:else}
|
||||
<Button
|
||||
size="tiny"
|
||||
variant="text"
|
||||
on:click={() => {
|
||||
dispatch("save", updatedTags);
|
||||
edit = !edit;
|
||||
editInProgress = !editInProgress;
|
||||
}}>
|
||||
save
|
||||
edit
|
||||
</Button>
|
||||
{/if}
|
||||
{/if}
|
||||
@@ -100,7 +97,7 @@
|
||||
{#each updatedTags as tag, key (tag)}
|
||||
<Chip
|
||||
on:remove={removeTag}
|
||||
removeable={edit || action === "create"}
|
||||
removeable={editInProgress || action === "create"}
|
||||
{key}>
|
||||
<div class="tag">{tag}</div>
|
||||
</Chip>
|
||||
@@ -108,7 +105,7 @@
|
||||
<div class="metadata-section-empty">No tags</div>
|
||||
{/each}
|
||||
</div>
|
||||
{#if edit || action === "create"}
|
||||
{#if editInProgress || action === "create"}
|
||||
<div style:margin-bottom="1rem">
|
||||
<TextInput
|
||||
bind:value={inputValue}
|
||||
|
||||
@@ -47,7 +47,6 @@
|
||||
}
|
||||
.browse {
|
||||
display: flex;
|
||||
z-index: 10;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
import Button from "@app/components/Button.svelte";
|
||||
import CobHeader from "@app/views/projects/Cob/CobHeader.svelte";
|
||||
import CobStateButton from "@app/views/projects/Cob/CobStateButton.svelte";
|
||||
import Markdown from "@app/components/Markdown.svelte";
|
||||
import TagInput from "./Cob/TagInput.svelte";
|
||||
import Textarea from "@app/components/Textarea.svelte";
|
||||
import Thread from "@app/components/Thread.svelte";
|
||||
@@ -147,6 +148,7 @@
|
||||
|
||||
$: selectedItem = issue.state.status === "closed" ? items[0] : items[1];
|
||||
$: threads = issue.discussion
|
||||
.slice(1) // Skip the first comment, which is the issue description
|
||||
.filter(comment => !comment.replyTo)
|
||||
.map(thread => {
|
||||
return {
|
||||
@@ -168,6 +170,9 @@
|
||||
margin-bottom: 4.5rem;
|
||||
}
|
||||
.metadata {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2rem;
|
||||
border-radius: var(--border-radius);
|
||||
font-size: var(--font-size-small);
|
||||
padding-left: 1rem;
|
||||
@@ -181,6 +186,15 @@
|
||||
margin: 0 0 2.5rem 0;
|
||||
gap: 1rem;
|
||||
}
|
||||
.author {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: nowrap;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
.comments {
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
@media (max-width: 960px) {
|
||||
.issue {
|
||||
@@ -212,47 +226,50 @@
|
||||
{issue.state.reason}
|
||||
</Badge>
|
||||
{/if}
|
||||
<Authorship
|
||||
timestamp={issue.discussion[0].timestamp}
|
||||
authorId={issue.author.id}
|
||||
caption="opened this issue" />
|
||||
</svelte:fragment>
|
||||
<div slot="description">
|
||||
<Markdown
|
||||
content={issue.discussion[0].body}
|
||||
rawPath={utils.getRawBasePath(projectId, baseUrl, projectHead)} />
|
||||
</div>
|
||||
<div class="author" slot="author">
|
||||
opened by <Authorship authorId={issue.author.id} />
|
||||
{utils.formatTimestamp(issue.discussion[0].timestamp)}
|
||||
</div>
|
||||
</CobHeader>
|
||||
<div>
|
||||
{#each threads as thread, index (thread.root.id)}
|
||||
<Thread
|
||||
{thread}
|
||||
{rawPath}
|
||||
isDescription={index === 0}
|
||||
on:reply={createReply} />
|
||||
<div class="comments">
|
||||
{#each threads as thread (thread.root.id)}
|
||||
<Thread {thread} {rawPath} on:reply={createReply} />
|
||||
{/each}
|
||||
{#if $sessionStore}
|
||||
<Textarea
|
||||
resizable
|
||||
on:submit={() => {
|
||||
createComment(commentBody);
|
||||
commentBody = "";
|
||||
}}
|
||||
bind:value={commentBody}
|
||||
placeholder="Leave your comment" />
|
||||
<div class="actions txt-small">
|
||||
<CobStateButton
|
||||
{items}
|
||||
{selectedItem}
|
||||
state={issue.state}
|
||||
on:saveStatus={saveStatus} />
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="small"
|
||||
disabled={!commentBody}
|
||||
on:click={() => {
|
||||
<div style:margin-top="1rem">
|
||||
{#if $sessionStore}
|
||||
<Textarea
|
||||
resizable
|
||||
on:submit={() => {
|
||||
createComment(commentBody);
|
||||
commentBody = "";
|
||||
}}>
|
||||
Comment
|
||||
</Button>
|
||||
</div>
|
||||
{/if}
|
||||
}}
|
||||
bind:value={commentBody}
|
||||
placeholder="Leave your comment" />
|
||||
<div class="actions txt-small">
|
||||
<CobStateButton
|
||||
{items}
|
||||
{selectedItem}
|
||||
state={issue.state}
|
||||
on:saveStatus={saveStatus} />
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="small"
|
||||
disabled={!commentBody}
|
||||
on:click={() => {
|
||||
createComment(commentBody);
|
||||
commentBody = "";
|
||||
}}>
|
||||
Comment
|
||||
</Button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="metadata">
|
||||
|
||||
@@ -15,8 +15,9 @@
|
||||
import Badge from "@app/components/Badge.svelte";
|
||||
import Button from "@app/components/Button.svelte";
|
||||
import CobHeader from "@app/views/projects/Cob/CobHeader.svelte";
|
||||
import Comment from "@app/components/Comment.svelte";
|
||||
import Markdown from "@app/components/Markdown.svelte";
|
||||
import TagInput from "@app/views/projects/Cob/TagInput.svelte";
|
||||
import Textarea from "@app/components/Textarea.svelte";
|
||||
|
||||
export let session: StoredSession;
|
||||
export let projectId: string;
|
||||
@@ -32,7 +33,7 @@
|
||||
: "view";
|
||||
|
||||
let issueTitle = "";
|
||||
let issueText = "";
|
||||
let issueText: string | undefined = undefined;
|
||||
let assignees: string[] = [];
|
||||
let tags: string[] = [];
|
||||
|
||||
@@ -44,7 +45,7 @@
|
||||
projectId,
|
||||
{
|
||||
title: issueTitle,
|
||||
description: issueText,
|
||||
description: issueText ?? "",
|
||||
assignees: utils.stripDidPrefix(assignees),
|
||||
tags: tags,
|
||||
},
|
||||
@@ -82,6 +83,9 @@
|
||||
margin-top: 1rem;
|
||||
}
|
||||
.metadata {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4rem;
|
||||
border-radius: var(--border-radius);
|
||||
font-size: var(--font-size-small);
|
||||
padding-left: 1rem;
|
||||
@@ -91,6 +95,11 @@
|
||||
flex: 2;
|
||||
padding-right: 1rem;
|
||||
}
|
||||
.author {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
@media (max-width: 960px) {
|
||||
main {
|
||||
padding-left: 2rem;
|
||||
@@ -100,6 +109,14 @@
|
||||
.form {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
.editor {
|
||||
padding-right: 0;
|
||||
}
|
||||
.metadata {
|
||||
margin-left: 0;
|
||||
padding-left: 0;
|
||||
gap: 2rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -108,22 +125,31 @@
|
||||
<div class="editor">
|
||||
<CobHeader {action} bind:title={issueTitle}>
|
||||
<svelte:fragment slot="state">
|
||||
<Badge variant="positive">open</Badge>
|
||||
<Authorship
|
||||
timestamp={Date.now()}
|
||||
authorId={session.publicKey}
|
||||
caption="opened this issue" />
|
||||
{#if action === "view"}
|
||||
<Badge variant="positive">open</Badge>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="description">
|
||||
{#if action === "create"}
|
||||
<Textarea
|
||||
resizable
|
||||
bind:value={issueText}
|
||||
on:submit={createIssue}
|
||||
placeholder="Write a description" />
|
||||
{:else if !issueText}
|
||||
<p class="txt-missing">No description</p>
|
||||
{:else}
|
||||
<Markdown
|
||||
content={issueText}
|
||||
rawPath={utils.getRawBasePath(projectId, baseUrl, projectHead)} />
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
<div class="author" slot="author">
|
||||
{#if action === "view"}
|
||||
opened by <Authorship authorId={session.publicKey} /> now
|
||||
{/if}
|
||||
</div>
|
||||
</CobHeader>
|
||||
<div class="comments">
|
||||
<Comment
|
||||
bind:body={issueText}
|
||||
on:submit={createIssue}
|
||||
authorId={session.publicKey}
|
||||
timestamp={Date.now()}
|
||||
{action}
|
||||
rawPath={utils.getRawBasePath(projectId, baseUrl, projectHead)} />
|
||||
</div>
|
||||
<div class="actions">
|
||||
<Button
|
||||
size="small"
|
||||
|
||||
+191
-191
@@ -1,6 +1,5 @@
|
||||
<script lang="ts">
|
||||
import type { BaseUrl, Comment, Merge, Patch, Review } from "@httpd-client";
|
||||
import type { Variant } from "@app/components/Badge.svelte";
|
||||
<script lang="ts" context="module">
|
||||
import type { Comment, Review, Revision, Merge } from "@httpd-client";
|
||||
|
||||
interface Thread {
|
||||
root: Comment;
|
||||
@@ -8,11 +7,17 @@
|
||||
}
|
||||
|
||||
interface TimelineReview {
|
||||
inner: [string, Review];
|
||||
inner: [string, string, Review];
|
||||
type: "review";
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
interface TimelineRevision {
|
||||
inner: Revision;
|
||||
type: "revision";
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
interface TimelineMerge {
|
||||
inner: Merge;
|
||||
type: "merge";
|
||||
@@ -25,32 +30,44 @@
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
import capitalize from "lodash/capitalize";
|
||||
export type Timeline =
|
||||
| TimelineMerge
|
||||
| TimelineReview
|
||||
| TimelineRevision
|
||||
| TimelineThread;
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import type { BaseUrl, Patch } from "@httpd-client";
|
||||
import type { Variant } from "@app/components/Badge.svelte";
|
||||
|
||||
import * as router from "@app/lib/router";
|
||||
import * as utils from "@app/lib/utils";
|
||||
import * as router from "@app/lib/router";
|
||||
import { capitalize } from "lodash";
|
||||
import { HttpdClient } from "@httpd-client";
|
||||
import { sessionStore } from "@app/lib/session";
|
||||
|
||||
import Authorship from "@app/components/Authorship.svelte";
|
||||
import Badge from "@app/components/Badge.svelte";
|
||||
import Changeset from "./SourceBrowser/Changeset.svelte";
|
||||
import Changeset from "@app/views/projects/SourceBrowser/Changeset.svelte";
|
||||
import CobHeader from "@app/views/projects/Cob/CobHeader.svelte";
|
||||
import CommentComponent from "@app/components/Comment.svelte";
|
||||
import CommitTeaser from "./Commit/CommitTeaser.svelte";
|
||||
import CommitTeaser from "@app/views/projects/Commit/CommitTeaser.svelte";
|
||||
import Dropdown from "@app/components/Dropdown.svelte";
|
||||
import ErrorMessage from "@app/components/ErrorMessage.svelte";
|
||||
import Floating from "@app/components/Floating.svelte";
|
||||
import Markdown from "@app/components/Markdown.svelte";
|
||||
import Placeholder from "@app/components/Placeholder.svelte";
|
||||
import ProjectLink from "@app/components/ProjectLink.svelte";
|
||||
import RevisionComponent from "@app/views/projects/Cob/Revision.svelte";
|
||||
import SquareButton from "@app/components/SquareButton.svelte";
|
||||
import TagInput from "./Cob/TagInput.svelte";
|
||||
import ThreadComponent from "@app/components/Thread.svelte";
|
||||
import TagInput from "@app/views/projects/Cob/TagInput.svelte";
|
||||
|
||||
export let projectId: string;
|
||||
export let baseUrl: BaseUrl;
|
||||
export let patch: Patch;
|
||||
export let projectHead: string;
|
||||
export let revision: string | undefined = undefined;
|
||||
export let currentTab: "activity" | "commits";
|
||||
export let revision: string;
|
||||
export let currentTab: "activity" | "commits" | "files";
|
||||
|
||||
const api = new HttpdClient(baseUrl);
|
||||
|
||||
@@ -75,6 +92,19 @@
|
||||
patch = await api.project.getPatchById(projectId, patch.id);
|
||||
}
|
||||
}
|
||||
function badgeColor(status: string): Variant {
|
||||
if (status === "draft") {
|
||||
return "foreground";
|
||||
} else if (status === "open") {
|
||||
return "positive";
|
||||
} else if (status === "archived") {
|
||||
return "caution";
|
||||
} else if (status === "merged") {
|
||||
return "primary";
|
||||
} else {
|
||||
return "foreground";
|
||||
}
|
||||
}
|
||||
|
||||
async function saveTags({ detail: tags }: CustomEvent<string[]>) {
|
||||
if ($sessionStore) {
|
||||
@@ -92,80 +122,63 @@
|
||||
}
|
||||
}
|
||||
|
||||
function formatVerdict(verdict?: string | null) {
|
||||
switch (verdict) {
|
||||
case "accept":
|
||||
return "accepted this revision";
|
||||
case "reject":
|
||||
return "rejected this revision";
|
||||
default:
|
||||
return "left a comment";
|
||||
}
|
||||
}
|
||||
|
||||
const action: "create" | "edit" | "view" =
|
||||
$sessionStore && utils.isLocal(baseUrl.hostname) ? "edit" : "view";
|
||||
|
||||
// Reactive due to eventual changes in patch.revisions
|
||||
$: enumeratedRevisions = patch.revisions.map((r, i) => [r, i] as const);
|
||||
$: currentRevisionTuple =
|
||||
enumeratedRevisions.find(([rev]) => rev.id === revision) ||
|
||||
enumeratedRevisions[enumeratedRevisions.length - 1];
|
||||
$: [currentRevision, currentRevisionIndex] = currentRevisionTuple;
|
||||
$: options = ["activity", "commits", "files"].map(o => ({
|
||||
const options = ["activity", "commits", "files"].map(o => ({
|
||||
value: o,
|
||||
title: capitalize(o),
|
||||
disabled: false,
|
||||
}));
|
||||
$: reviews = currentRevision.reviews.map<TimelineReview>(
|
||||
([author, review]) => ({
|
||||
timestamp: review.timestamp,
|
||||
type: "review",
|
||||
inner: [author, review],
|
||||
}),
|
||||
);
|
||||
$: merges = currentRevision.merges.map<TimelineMerge>(inner => ({
|
||||
timestamp: inner.timestamp,
|
||||
type: "merge",
|
||||
inner,
|
||||
}));
|
||||
$: threads = currentRevision.discussions
|
||||
.filter(comment => !comment.replyTo)
|
||||
.map<TimelineThread>(
|
||||
thread => ({
|
||||
timestamp: thread.timestamp,
|
||||
type: "thread",
|
||||
inner: {
|
||||
root: thread,
|
||||
replies: currentRevision.discussions
|
||||
.filter(comment => comment.replyTo === thread.id)
|
||||
.sort((a, b) => a.timestamp - b.timestamp),
|
||||
},
|
||||
}),
|
||||
[],
|
||||
);
|
||||
$: timeline = [...reviews, ...merges, ...threads].sort(
|
||||
(a, b) => a.timestamp - b.timestamp,
|
||||
);
|
||||
$: diffPromise = api.project.getDiff(
|
||||
projectId,
|
||||
currentRevision.base,
|
||||
currentRevision.oid,
|
||||
);
|
||||
|
||||
function badgeColor(status: string): Variant {
|
||||
if (status === "draft") {
|
||||
return "foreground";
|
||||
} else if (status === "open") {
|
||||
return "positive";
|
||||
} else if (status === "archived") {
|
||||
return "caution";
|
||||
} else if (status === "merged") {
|
||||
return "primary";
|
||||
} else {
|
||||
return "foreground";
|
||||
}
|
||||
}
|
||||
const currentRevision =
|
||||
patch.revisions.find(r => r.id === revision) || patch.revisions[0];
|
||||
$: timelineTuple = patch.revisions.map<
|
||||
[
|
||||
{
|
||||
revisionId: string;
|
||||
revisionTimestamp: number;
|
||||
revisionBase: string;
|
||||
revisionOid: string;
|
||||
},
|
||||
Timeline[],
|
||||
]
|
||||
>(rev => [
|
||||
{
|
||||
revisionId: rev.id,
|
||||
revisionTimestamp: rev.timestamp,
|
||||
revisionBase: rev.base,
|
||||
revisionOid: rev.oid,
|
||||
},
|
||||
[
|
||||
...rev.reviews.map<TimelineReview>(([author, review]) => ({
|
||||
timestamp: review.timestamp,
|
||||
type: "review",
|
||||
inner: [rev.id, author, review],
|
||||
})),
|
||||
...rev.merges.map<TimelineMerge>(inner => ({
|
||||
timestamp: inner.timestamp,
|
||||
type: "merge",
|
||||
inner,
|
||||
})),
|
||||
...rev.discussions
|
||||
.filter(comment => !comment.replyTo)
|
||||
.map<TimelineThread>(thread => ({
|
||||
timestamp: thread.timestamp,
|
||||
type: "thread",
|
||||
inner: {
|
||||
root: thread,
|
||||
replies: rev.discussions
|
||||
.filter(comment => comment.replyTo === thread.id)
|
||||
.sort((a, b) => a.timestamp - b.timestamp),
|
||||
},
|
||||
})),
|
||||
{
|
||||
type: "revision",
|
||||
timestamp: rev.timestamp,
|
||||
inner: rev,
|
||||
} as TimelineRevision,
|
||||
].sort((a, b) => a.timestamp - b.timestamp),
|
||||
]);
|
||||
</script>
|
||||
|
||||
<style>
|
||||
@@ -176,20 +189,32 @@
|
||||
margin-bottom: 4.5rem;
|
||||
}
|
||||
.metadata {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2rem;
|
||||
border-radius: var(--border-radius);
|
||||
font-size: var(--font-size-small);
|
||||
padding-left: 1rem;
|
||||
margin-left: 1rem;
|
||||
}
|
||||
.action {
|
||||
margin: 1rem;
|
||||
color: var(--color-foreground-5);
|
||||
}
|
||||
.commit-list {
|
||||
border-radius: var(--border-radius);
|
||||
overflow: hidden;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
.tab-line {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
.author {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: nowrap;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
@media (max-width: 1092px) {
|
||||
.patch {
|
||||
@@ -210,25 +235,78 @@
|
||||
<div class="patch">
|
||||
<div>
|
||||
<CobHeader id={patch.id} title={patch.title}>
|
||||
<span slot="revision" class="txt-monospace txt-tiny">
|
||||
<svelte:fragment slot="state">
|
||||
<Badge variant={badgeColor(patch.state.status)}>
|
||||
{patch.state.status}
|
||||
</Badge>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="description">
|
||||
{#if patch.description}
|
||||
<Markdown
|
||||
content={patch.description}
|
||||
rawPath={utils.getRawBasePath(
|
||||
projectId,
|
||||
baseUrl,
|
||||
currentRevision.oid,
|
||||
)} />
|
||||
{:else}
|
||||
<span class="txt-missing">No description available</span>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
<div class="author" slot="author">
|
||||
opened by <Authorship authorId={patch.author.id} />
|
||||
{utils.formatTimestamp(patch.revisions[0].timestamp)}
|
||||
</div>
|
||||
</CobHeader>
|
||||
|
||||
<div class="tab-line">
|
||||
<div style="display: flex; gap: 0.5rem;">
|
||||
{#each options as option}
|
||||
{#if !option.disabled}
|
||||
<ProjectLink
|
||||
projectParams={{
|
||||
search: `tab=${option.value}`,
|
||||
}}>
|
||||
<SquareButton
|
||||
size="small"
|
||||
clickable={option.disabled}
|
||||
active={option.value === currentTab}
|
||||
disabled={option.disabled}>
|
||||
{option.title}
|
||||
</SquareButton>
|
||||
</ProjectLink>
|
||||
{:else}
|
||||
<SquareButton
|
||||
size="small"
|
||||
clickable={option.disabled}
|
||||
active={option.value === currentTab}
|
||||
disabled={option.disabled}>
|
||||
{option.title}
|
||||
</SquareButton>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
{#if currentTab !== "activity"}
|
||||
<Floating disabled={patch.revisions.length === 1}>
|
||||
<svelte:fragment slot="toggle">
|
||||
<SquareButton
|
||||
size="small"
|
||||
clickable={patch.revisions.length > 1}
|
||||
disabled={patch.revisions.length === 1}>
|
||||
Revision {currentRevisionIndex}
|
||||
Revision {utils.formatObjectId(currentRevision.id)}
|
||||
</SquareButton>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="modal">
|
||||
<Dropdown
|
||||
items={enumeratedRevisions.map(([r, i]) => {
|
||||
items={patch.revisions.map(r => {
|
||||
return {
|
||||
title: `Revision ${i} (${utils.formatObjectId(r.id)})`,
|
||||
title: `Revision ${utils.formatObjectId(r.id)}`,
|
||||
value: r.id,
|
||||
badge: null,
|
||||
};
|
||||
})}
|
||||
selected={currentRevision.toString()}
|
||||
selected={currentRevision.id}
|
||||
on:select={({ detail: item }) => {
|
||||
router.updateProjectRoute({
|
||||
view: {
|
||||
@@ -243,121 +321,43 @@
|
||||
</Dropdown>
|
||||
</svelte:fragment>
|
||||
</Floating>
|
||||
</span>
|
||||
<svelte:fragment slot="state">
|
||||
<Badge variant={badgeColor(patch.state.status)}>
|
||||
{patch.state.status}
|
||||
</Badge>
|
||||
<div class="layout-desktop">
|
||||
<Authorship
|
||||
timestamp={patch.revisions[0].timestamp}
|
||||
authorId={patch.author.id}
|
||||
caption="opened this patch" />
|
||||
</div>
|
||||
<div class="layout-mobile">
|
||||
<Authorship authorId={patch.author.id} />
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
</CobHeader>
|
||||
<div style="display: flex; gap: 0.5rem;">
|
||||
{#each options as option}
|
||||
{#if !option.disabled}
|
||||
<ProjectLink
|
||||
projectParams={{
|
||||
search: `tab=${option.value}`,
|
||||
}}>
|
||||
<SquareButton
|
||||
size="small"
|
||||
clickable={option.disabled}
|
||||
active={option.value === currentTab}
|
||||
disabled={option.disabled}>
|
||||
{option.title}
|
||||
</SquareButton>
|
||||
</ProjectLink>
|
||||
{:else}
|
||||
<SquareButton
|
||||
size="small"
|
||||
clickable={option.disabled}
|
||||
active={option.value === currentTab}
|
||||
disabled={option.disabled}>
|
||||
{option.title}
|
||||
</SquareButton>
|
||||
{/if}
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
{#if currentTab === "activity"}
|
||||
<div style:margin-top="1rem">
|
||||
<div class="txt-tiny">
|
||||
<CommentComponent
|
||||
caption="created this revision"
|
||||
authorId={patch.author.id}
|
||||
timestamp={currentRevision.timestamp}
|
||||
rawPath={utils.getRawBasePath(projectId, baseUrl, projectHead)}
|
||||
body={currentRevisionIndex === 0
|
||||
? patch.description
|
||||
: currentRevision.description} />
|
||||
</div>
|
||||
{#each timeline as element}
|
||||
{#if element.type === "thread"}
|
||||
<!-- TODO: Implement reply creation and comment editing -->
|
||||
<ThreadComponent
|
||||
rawPath={utils.getRawBasePath(projectId, baseUrl, projectHead)}
|
||||
isDescription={false}
|
||||
thread={element.inner}
|
||||
on:reply={createReply}
|
||||
on:select={({ detail: index }) => (currentRevision = index)} />
|
||||
{:else if element.type === "merge"}
|
||||
<div class="action layout-desktop txt-tiny">
|
||||
<Authorship
|
||||
authorId={element.inner.node}
|
||||
timestamp={element.timestamp}>
|
||||
merged
|
||||
{utils.formatCommit(element.inner.commit)}
|
||||
</Authorship>
|
||||
</div>
|
||||
<div class="action layout-mobile txt-tiny">
|
||||
<Authorship authorId={element.inner.node}>
|
||||
merged
|
||||
{utils.formatCommit(element.inner.commit)}
|
||||
</Authorship>
|
||||
</div>
|
||||
{:else if element.type === "review"}
|
||||
<!-- TODO: Implement inline code comments -->
|
||||
{@const [author, review] = element.inner}
|
||||
<div class="action layout-desktop txt-tiny">
|
||||
<Authorship authorId={author} timestamp={element.timestamp}>
|
||||
{formatVerdict(review.verdict)}
|
||||
</Authorship>
|
||||
</div>
|
||||
<div class="action layout-mobile txt-tiny">
|
||||
<Authorship authorId={author}>
|
||||
{formatVerdict(review.verdict)}
|
||||
</Authorship>
|
||||
</div>
|
||||
{#if review.comment}
|
||||
<CommentComponent
|
||||
caption="left a comment"
|
||||
authorId={author}
|
||||
timestamp={review.timestamp}
|
||||
rawPath={utils.getRawBasePath(projectId, baseUrl, projectHead)}
|
||||
body={review.comment} />
|
||||
{/if}
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
{#each timelineTuple as [revision, timelines], index}
|
||||
<RevisionComponent
|
||||
{baseUrl}
|
||||
{projectId}
|
||||
{timelines}
|
||||
{projectHead}
|
||||
{...revision}
|
||||
on:reply={createReply}
|
||||
patchId={patch.id}
|
||||
authorId={patch.author.id}
|
||||
expanded={index === patch.revisions.length - 1} />
|
||||
{:else}
|
||||
<Placeholder emoji="🍂">
|
||||
<div slot="title">No activity</div>
|
||||
<div slot="body">No activity on this patch yet</div>
|
||||
</Placeholder>
|
||||
{/each}
|
||||
{:else if currentTab === "commits"}
|
||||
{#await diffPromise then diff}
|
||||
{#await api.project.getDiff(projectId, currentRevision.base, currentRevision.oid) then diff}
|
||||
<div class="commit-list">
|
||||
{#each diff.commits as commit}
|
||||
<CommitTeaser {commit} />
|
||||
{/each}
|
||||
</div>
|
||||
{:catch e}
|
||||
<ErrorMessage message="Not able to load commits." stackTrace={e} />
|
||||
{/await}
|
||||
{:else if currentTab === "files"}
|
||||
{#await diffPromise then diff}
|
||||
{#await api.project.getDiff(projectId, currentRevision.base, currentRevision.oid) then diff}
|
||||
<div style:margin-top="1rem">
|
||||
<Changeset revision={currentRevision.oid} diff={diff.diff} />
|
||||
</div>
|
||||
{:catch e}
|
||||
<ErrorMessage message="Not able to load files diff." stackTrace={e} />
|
||||
{/await}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
$: searchParams = new URLSearchParams(activeRoute.params.search || "");
|
||||
$: issueFilter = (searchParams.get("state") as IssueStatus) || "open";
|
||||
$: patchTabFilter =
|
||||
(searchParams.get("tab") as "activity" | "commits") || "activity";
|
||||
(searchParams.get("tab") as "activity" | "commits" | "files") || "activity";
|
||||
$: patchFilter = (searchParams.get("state") as PatchStatus) || "open";
|
||||
$: baseUrl = utils.extractBaseUrl(activeRoute.params.hostnamePort);
|
||||
$: api = new HttpdClient(baseUrl);
|
||||
@@ -254,13 +254,15 @@
|
||||
{#await api.project.getPatchById(project.id, activeRoute.params.view.params.patch)}
|
||||
<Loading center />
|
||||
{:then patch}
|
||||
{@const latestRevision = patch.revisions[patch.revisions.length - 1]}
|
||||
<Patch
|
||||
{patch}
|
||||
{baseUrl}
|
||||
projectId={project.id}
|
||||
projectHead={project.head}
|
||||
revision={activeRoute.params.view.params.revision}
|
||||
currentTab={patchTabFilter}
|
||||
{patch} />
|
||||
revision={activeRoute.params.view.params.revision ??
|
||||
latestRevision.id}
|
||||
currentTab={patchTabFilter} />
|
||||
{:catch e}
|
||||
<div class="message">
|
||||
<ErrorMessage message="Couldn't load patch." stackTrace={e} />
|
||||
|
||||
@@ -267,9 +267,9 @@ test("peer and branch switching", async ({ page }) => {
|
||||
await page.getByTitle("Change peer").click();
|
||||
await page.locator(`text=${aliceRemote}`).click();
|
||||
await expect(page.getByTitle("Change peer")).toHaveText(
|
||||
` did:key:${aliceRemote.substring(8).substring(0, 6)}…${aliceRemote.slice(
|
||||
-6,
|
||||
)} `,
|
||||
` did:key:${aliceRemote
|
||||
.substring(8)
|
||||
.substring(0, 6)}…${aliceRemote.slice(-6)} delegate`,
|
||||
);
|
||||
await expect(
|
||||
page.locator(
|
||||
|
||||
@@ -15,9 +15,9 @@ test("peer and branch switching", async ({ page }) => {
|
||||
await page.getByTitle("Change peer").click();
|
||||
await page.locator(`text=${aliceRemote}`).click();
|
||||
await expect(page.getByTitle("Change peer")).toHaveText(
|
||||
` did:key:${aliceRemote.substring(8).substring(0, 6)}…${aliceRemote.slice(
|
||||
-6,
|
||||
)} `,
|
||||
` did:key:${aliceRemote
|
||||
.substring(8)
|
||||
.substring(0, 6)}…${aliceRemote.slice(-6)} delegate`,
|
||||
);
|
||||
|
||||
await expect(page.getByText("Thursday, November 17, 2022")).toBeVisible();
|
||||
|
||||
Reference in New Issue
Block a user