mirror of
https://seed.radicle.dev/rad:z4D5UCArafTzTQpDZNQRuqswh3ury.git
synced 2026-10-18 19:10:45 +08:00
Add review page
This commit is contained in:
Generated
+5
-10
@@ -3975,8 +3975,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "radicle"
|
||||
version = "0.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fd823aeed3ffe73eb82a213e62cb3811f9bdf453844d6e0b14684e0757fb389b"
|
||||
source = "git+https://ash.radicle.garden/z3gqcJUoA1n9HaHKufZs5FCSGazv5.git?rev=7c902b6905724345ba850eb6cca8f8becc9a9c72#7c902b6905724345ba850eb6cca8f8becc9a9c72"
|
||||
dependencies = [
|
||||
"amplify",
|
||||
"base64 0.21.7",
|
||||
@@ -4007,8 +4006,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "radicle-cob"
|
||||
version = "0.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "90581a9508ccc310998e991d7acf139d2991297d3fb37d30de07536e10256afb"
|
||||
source = "git+https://ash.radicle.garden/z3gqcJUoA1n9HaHKufZs5FCSGazv5.git?rev=7c902b6905724345ba850eb6cca8f8becc9a9c72#7c902b6905724345ba850eb6cca8f8becc9a9c72"
|
||||
dependencies = [
|
||||
"fastrand",
|
||||
"git2",
|
||||
@@ -4026,8 +4024,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "radicle-crypto"
|
||||
version = "0.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d1d6a67969719841ad06049597006368eb4238ca63a02d20207654dfd1d2d6ad"
|
||||
source = "git+https://ash.radicle.garden/z3gqcJUoA1n9HaHKufZs5FCSGazv5.git?rev=7c902b6905724345ba850eb6cca8f8becc9a9c72#7c902b6905724345ba850eb6cca8f8becc9a9c72"
|
||||
dependencies = [
|
||||
"amplify",
|
||||
"cyphernet",
|
||||
@@ -4047,8 +4044,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "radicle-dag"
|
||||
version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cb41c7e10ada3a4df960190a96bfb4af56d33ada890f917acc8e3b122b614875"
|
||||
source = "git+https://ash.radicle.garden/z3gqcJUoA1n9HaHKufZs5FCSGazv5.git?rev=7c902b6905724345ba850eb6cca8f8becc9a9c72#7c902b6905724345ba850eb6cca8f8becc9a9c72"
|
||||
dependencies = [
|
||||
"fastrand",
|
||||
]
|
||||
@@ -4070,8 +4066,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "radicle-ssh"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fbee758010fb64482be4b18591fbeb3cbc15b16450d143edf4edb5484c7366c6"
|
||||
source = "git+https://ash.radicle.garden/z3gqcJUoA1n9HaHKufZs5FCSGazv5.git?rev=7c902b6905724345ba850eb6cca8f8becc9a9c72#7c902b6905724345ba850eb6cca8f8becc9a9c72"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
"log",
|
||||
|
||||
@@ -18,7 +18,7 @@ tauri-build = { version = "2.0.1", features = ["isolation"] }
|
||||
anyhow = { version = "1.0.90" }
|
||||
base64 = { version = "0.22.1" }
|
||||
log = { version = "0.4.22" }
|
||||
radicle = { version = "0.14.0" }
|
||||
radicle = { git = "https://ash.radicle.garden/z3gqcJUoA1n9HaHKufZs5FCSGazv5.git", package = "radicle", rev = "7c902b6905724345ba850eb6cca8f8becc9a9c72" }
|
||||
radicle-types = { version = "0.1.0", path = "../radicle-types" }
|
||||
radicle-surf = { version = "0.22.1", features = ["serde"] }
|
||||
serde = { version = "1.0.210", features = ["derive"] }
|
||||
|
||||
@@ -11,7 +11,7 @@ localtime = { version = "1.3.1" }
|
||||
log = { version = "0.4.22" }
|
||||
infer = { version = "0.3" }
|
||||
mime-infer = { version = "3.0.0" }
|
||||
radicle = { version = "0.14.0", features = ["test"] }
|
||||
radicle = { git = "https://ash.radicle.garden/z3gqcJUoA1n9HaHKufZs5FCSGazv5.git", package = "radicle", rev = "7c902b6905724345ba850eb6cca8f8becc9a9c72", features = ["test"] }
|
||||
radicle-surf = { version = "0.22.1", features = ["serde"] }
|
||||
serde = { version = "1.0.210", features = ["derive"] }
|
||||
serde_json = { version = "1.0.132" }
|
||||
|
||||
@@ -11,4 +11,5 @@ export type Review = {
|
||||
summary?: string;
|
||||
comments: Array<Comment<CodeLocation>>;
|
||||
timestamp: number;
|
||||
labels: Array<string>;
|
||||
};
|
||||
|
||||
@@ -253,6 +253,8 @@ pub struct Review {
|
||||
comments: Vec<cobs::thread::Comment<cobs::thread::CodeLocation>>,
|
||||
#[ts(type = "number")]
|
||||
timestamp: cob::common::Timestamp,
|
||||
#[ts(as = "Vec<String>")]
|
||||
labels: Vec<cob::Label>,
|
||||
}
|
||||
|
||||
impl Review {
|
||||
@@ -262,6 +264,7 @@ impl Review {
|
||||
author: cobs::Author::new(&review.author().id, aliases),
|
||||
verdict: review.verdict().map(|v| v.into()),
|
||||
summary: review.summary().map(|s| s.to_string()),
|
||||
labels: review.labels().cloned().collect::<Vec<_>>(),
|
||||
comments: review
|
||||
.comments()
|
||||
.map(|(id, c)| {
|
||||
|
||||
@@ -10,7 +10,7 @@ anyhow = { version = "1.0.90" }
|
||||
axum = { version = "0.7.5", default-features = false, features = ["json", "query", "tokio", "http1"] }
|
||||
hyper = { version = "1.4", default-features = false }
|
||||
lexopt = { version = "0.3.0" }
|
||||
radicle = { version = "0.14.0" }
|
||||
radicle = { git = "https://ash.radicle.garden/z3gqcJUoA1n9HaHKufZs5FCSGazv5.git", package = "radicle", rev = "7c902b6905724345ba850eb6cca8f8becc9a9c72" }
|
||||
radicle-surf = { version = "0.22.1", default-features = false, features = ["serde"] }
|
||||
radicle-types = { path = "../radicle-types" }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
interface Props {
|
||||
children: Snippet;
|
||||
variant: "primary" | "secondary" | "ghost" | "float" | "danger";
|
||||
variant: "primary" | "secondary" | "ghost" | "float" | "danger" | "success";
|
||||
hoverable?: boolean;
|
||||
onclick?: () => void;
|
||||
stylePosition?: string;
|
||||
|
||||
@@ -20,23 +20,27 @@
|
||||
|
||||
interface Props {
|
||||
actions?: Snippet;
|
||||
beforeTimestamp?: Snippet;
|
||||
id?: string;
|
||||
rid: string;
|
||||
author: Author;
|
||||
body: string;
|
||||
body?: string;
|
||||
reactions?: Reaction[];
|
||||
embeds?: Map<string, Embed>;
|
||||
caption?: string;
|
||||
timestamp: number;
|
||||
lastEdit?: Edit;
|
||||
disallowEmptyBody?: boolean;
|
||||
emptyBodyTooltip?: string;
|
||||
editComment?: (body: string, embeds: Embed[]) => Promise<void>;
|
||||
reactOnComment?: (authors: Author[], reaction: string) => Promise<void>;
|
||||
styleWidth?: string;
|
||||
}
|
||||
|
||||
/* eslint-disable prefer-const */
|
||||
let {
|
||||
actions,
|
||||
beforeTimestamp,
|
||||
id,
|
||||
rid,
|
||||
author,
|
||||
@@ -49,6 +53,8 @@
|
||||
disallowEmptyBody = false,
|
||||
editComment,
|
||||
reactOnComment,
|
||||
styleWidth,
|
||||
emptyBodyTooltip,
|
||||
}: Props = $props();
|
||||
/* eslint-enable prefer-const */
|
||||
|
||||
@@ -120,7 +126,7 @@
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="card" {id}>
|
||||
<div class="card" {id} style:width={styleWidth}>
|
||||
<div style:position="relative">
|
||||
<div class="card-header">
|
||||
<NodeId {...utils.authorForNodeId(author)} />
|
||||
@@ -128,6 +134,9 @@
|
||||
{#if id}
|
||||
<Id {id} variant="oid" />
|
||||
{/if}
|
||||
{#if beforeTimestamp}
|
||||
{@render beforeTimestamp()}
|
||||
{/if}
|
||||
<span class="timestamp" title={utils.absoluteTimestamp(timestamp)}>
|
||||
{utils.formatTimestamp(timestamp)}
|
||||
</span>
|
||||
@@ -165,7 +174,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if body.trim() === "" && state === "read"}
|
||||
{#if (body === undefined || body?.trim() === "") && state === "read"}
|
||||
<div class="card-body">
|
||||
<span class="txt-missing txt-small" style:line-height="1.625rem">
|
||||
No description.
|
||||
@@ -181,6 +190,7 @@
|
||||
{rid}
|
||||
{embeds}
|
||||
{disallowEmptyBody}
|
||||
{emptyBodyTooltip}
|
||||
borderVariant="ghost"
|
||||
submitInProgress={state === "submit"}
|
||||
submitCaption="Save"
|
||||
@@ -202,7 +212,7 @@
|
||||
{:else}
|
||||
<div style:width="100%">
|
||||
<div style:overflow="hidden">
|
||||
<Markdown {rid} breaks content={body} />
|
||||
<Markdown {rid} breaks content={body ?? ""} />
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -67,6 +67,11 @@
|
||||
class:disabled
|
||||
{style}
|
||||
{title}
|
||||
{onclick}>
|
||||
onclick={() => {
|
||||
if (disabled) {
|
||||
return;
|
||||
}
|
||||
onclick();
|
||||
}}>
|
||||
{@render children()}
|
||||
</div>
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
submitInProgress?: boolean;
|
||||
disableSubmit?: boolean;
|
||||
disallowEmptyBody?: boolean;
|
||||
emptyBodyTooltip?: string;
|
||||
isValid?: () => boolean;
|
||||
preview?: boolean;
|
||||
stylePadding?: string;
|
||||
@@ -55,6 +56,7 @@
|
||||
submitInProgress = false,
|
||||
disableSubmit = false,
|
||||
disallowEmptyBody = false,
|
||||
emptyBodyTooltip,
|
||||
isValid = () => true,
|
||||
stylePadding,
|
||||
borderVariant = "float",
|
||||
@@ -293,6 +295,7 @@
|
||||
</OutlineButton>
|
||||
<Button
|
||||
variant="ghost"
|
||||
title={emptyBodyTooltip}
|
||||
disabled={!isValid() ||
|
||||
submitInProgress ||
|
||||
disableSubmit ||
|
||||
|
||||
@@ -7,14 +7,14 @@
|
||||
publicKey: string;
|
||||
alias?: string;
|
||||
styleFontSize?: string;
|
||||
styleFontFamily?: string;
|
||||
styleFontWeight?: string;
|
||||
}
|
||||
|
||||
const {
|
||||
publicKey,
|
||||
alias,
|
||||
styleFontSize = "var(--font-size-small)",
|
||||
styleFontFamily = "var(--font-family-monospace)",
|
||||
styleFontWeight = "var(--font-weight-semibold)",
|
||||
}: Props = $props();
|
||||
</script>
|
||||
|
||||
@@ -23,7 +23,6 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.375rem;
|
||||
font-weight: var(--font-weight-semibold);
|
||||
}
|
||||
.no-alias {
|
||||
color: var(--color-foreground-dim);
|
||||
@@ -33,7 +32,7 @@
|
||||
<div
|
||||
class="avatar-alias"
|
||||
style:font-size={styleFontSize}
|
||||
style:font-family={styleFontFamily}>
|
||||
style:font-weight={styleFontWeight}>
|
||||
<Avatar {publicKey} />
|
||||
{#if alias}
|
||||
<span class="txt-overflow">
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import type { Patch } from "@bindings/cob/patch/Patch";
|
||||
import type { Stats } from "@bindings/cob/Stats";
|
||||
import type { PatchStatus } from "@app/views/repo/router";
|
||||
import type { Stats } from "@bindings/cob/Stats";
|
||||
|
||||
import {
|
||||
authorForNodeId,
|
||||
@@ -12,27 +12,28 @@
|
||||
import { invoke } from "@app/lib/invoke";
|
||||
import { push } from "@app/lib/router";
|
||||
|
||||
import DiffStatBadge from "./DiffStatBadge.svelte";
|
||||
import Icon from "./Icon.svelte";
|
||||
import Id from "./Id.svelte";
|
||||
import InlineTitle from "./InlineTitle.svelte";
|
||||
import NodeId from "./NodeId.svelte";
|
||||
import DiffStatBadge from "@app/components/DiffStatBadge.svelte";
|
||||
import Icon from "@app/components/Icon.svelte";
|
||||
import Id from "@app/components/Id.svelte";
|
||||
import InlineTitle from "@app/components/InlineTitle.svelte";
|
||||
import Label from "@app/components/Label.svelte";
|
||||
import NodeId from "@app/components/NodeId.svelte";
|
||||
|
||||
interface Props {
|
||||
compact?: boolean;
|
||||
loadPatch?: (rid: string, patchId: string) => void;
|
||||
patch: Patch;
|
||||
rid: string;
|
||||
selected?: boolean;
|
||||
compact?: boolean;
|
||||
loadPatch?: (rid: string, patchId: string) => void;
|
||||
status: PatchStatus | undefined;
|
||||
}
|
||||
|
||||
const {
|
||||
compact = false,
|
||||
loadPatch,
|
||||
patch,
|
||||
rid,
|
||||
selected = false,
|
||||
compact = false,
|
||||
loadPatch,
|
||||
status,
|
||||
}: Props = $props();
|
||||
</script>
|
||||
@@ -82,7 +83,13 @@
|
||||
if (loadPatch) {
|
||||
loadPatch(rid, patch.id);
|
||||
} else {
|
||||
void push({ resource: "repo.patch", rid, patch: patch.id, status });
|
||||
void push({
|
||||
resource: "repo.patch",
|
||||
rid,
|
||||
patch: patch.id,
|
||||
status,
|
||||
reviewId: undefined,
|
||||
});
|
||||
}
|
||||
}}>
|
||||
<div class="global-flex" style:align-items="flex-start">
|
||||
@@ -116,7 +123,7 @@
|
||||
{/await}
|
||||
|
||||
{#each patch.labels as label}
|
||||
<div class="global-counter txt-small">{label}</div>
|
||||
<Label {label} />
|
||||
{/each}
|
||||
{/if}
|
||||
<div
|
||||
|
||||
@@ -0,0 +1,234 @@
|
||||
<script lang="ts">
|
||||
import type { Config } from "@bindings/config/Config";
|
||||
import type { RepoInfo } from "@bindings/repo/RepoInfo";
|
||||
import type { Review } from "@bindings/cob/patch/Review";
|
||||
import type { Revision } from "@bindings/cob/patch/Revision";
|
||||
|
||||
import partial from "lodash/partial";
|
||||
|
||||
import * as roles from "@app/lib/roles";
|
||||
|
||||
import { announce } from "@app/components/AnnounceSwitch.svelte";
|
||||
import { authorForNodeId, publicKeyFromDid } from "@app/lib/utils";
|
||||
import { invoke } from "@app/lib/invoke";
|
||||
import { nodeRunning } from "@app/lib/events";
|
||||
|
||||
import Border from "@app/components/Border.svelte";
|
||||
import CommentComponent from "@app/components/Comment.svelte";
|
||||
import Icon from "@app/components/Icon.svelte";
|
||||
import Id from "@app/components/Id.svelte";
|
||||
import LabelInput from "@app/components/LabelInput.svelte";
|
||||
import NakedButton from "@app/components/NakedButton.svelte";
|
||||
import NodeId from "@app/components/NodeId.svelte";
|
||||
import VerdictButton from "@app/components/VerdictButton.svelte";
|
||||
import VerdictBadge from "./VerdictBadge.svelte";
|
||||
|
||||
interface Props {
|
||||
config: Config;
|
||||
onNavigateBack: () => void;
|
||||
patchId: string;
|
||||
reload: (reviewId?: string) => Promise<void>;
|
||||
review: Review;
|
||||
revision: Revision;
|
||||
repo: RepoInfo;
|
||||
}
|
||||
|
||||
const {
|
||||
config,
|
||||
onNavigateBack,
|
||||
patchId,
|
||||
reload,
|
||||
review,
|
||||
revision,
|
||||
repo,
|
||||
}: Props = $props();
|
||||
|
||||
const contributors = [
|
||||
review.author,
|
||||
...review.comments.map(c => {
|
||||
return c.author;
|
||||
}),
|
||||
];
|
||||
|
||||
let verdict: Review["verdict"] = $state(review.verdict);
|
||||
let labelSaveInProgress: boolean = $state(false);
|
||||
|
||||
async function editReview(
|
||||
reviewId: string,
|
||||
verdict: Review["verdict"],
|
||||
labels: string[],
|
||||
summary?: string,
|
||||
) {
|
||||
if (summary?.trim() === "") {
|
||||
summary = undefined;
|
||||
} else {
|
||||
summary = summary?.trim();
|
||||
}
|
||||
|
||||
try {
|
||||
labelSaveInProgress = true;
|
||||
await invoke("edit_patch", {
|
||||
rid: repo.rid,
|
||||
cobId: patchId,
|
||||
action: {
|
||||
type: "review.edit",
|
||||
review: reviewId,
|
||||
summary,
|
||||
verdict,
|
||||
labels,
|
||||
},
|
||||
opts: { announce: $nodeRunning && $announce },
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Editing review failed: ", error);
|
||||
} finally {
|
||||
labelSaveInProgress = false;
|
||||
await reload(reviewId);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.content {
|
||||
padding: 1rem 1rem 1rem 0;
|
||||
}
|
||||
.title {
|
||||
font-size: var(--font-size-medium);
|
||||
font-weight: var(--font-weight-medium);
|
||||
-webkit-user-select: text;
|
||||
user-select: text;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
white-space: nowrap;
|
||||
min-height: 40px;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
.metadata-divider {
|
||||
width: 2px;
|
||||
background-color: var(--color-fill-ghost);
|
||||
height: calc(100% + 4px);
|
||||
top: 0;
|
||||
position: relative;
|
||||
}
|
||||
.metadata-section {
|
||||
padding: 0.5rem;
|
||||
font-size: var(--font-size-small);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
height: 100%;
|
||||
}
|
||||
.metadata-section-title {
|
||||
margin-bottom: 0.5rem;
|
||||
color: var(--color-foreground-dim);
|
||||
}
|
||||
.review-body {
|
||||
margin-top: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
/* We put the background and clip-path in a separate element to prevent
|
||||
popovers being clipped in the main element. */
|
||||
.review-body::after {
|
||||
position: absolute;
|
||||
z-index: -1;
|
||||
content: " ";
|
||||
background-color: var(--color-background-float);
|
||||
clip-path: var(--2px-corner-fill);
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="content">
|
||||
<div style:margin-bottom="0.5rem">
|
||||
<div class="title">
|
||||
<NakedButton
|
||||
variant="ghost"
|
||||
onclick={onNavigateBack}
|
||||
stylePadding="0 4px">
|
||||
<Icon name="arrow-left" />
|
||||
</NakedButton>
|
||||
<span class="global-flex" style:gap="0">
|
||||
<NodeId
|
||||
{...authorForNodeId(review.author)}
|
||||
styleFontSize="var(--font-size-medium)"
|
||||
styleFontWeight="var(--font-weight-medium)" />'s review
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<Border variant="ghost" styleGap="0">
|
||||
<div class="metadata-section" style:min-width="8rem">
|
||||
<div class="metadata-section-title">Verdict</div>
|
||||
{#if !!roles.isDelegateOrAuthor( config.publicKey, repo.delegates.map(delegate => delegate.did), review.author.did, )}
|
||||
<VerdictButton
|
||||
{verdict}
|
||||
summaryMissing={review.summary === undefined ||
|
||||
review.summary.trim() === ""}
|
||||
onSelect={async newVerdict => {
|
||||
verdict = newVerdict;
|
||||
await editReview(
|
||||
review.id,
|
||||
verdict,
|
||||
review.labels,
|
||||
review.summary,
|
||||
);
|
||||
}} />
|
||||
{:else}
|
||||
<VerdictBadge {verdict} />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="metadata-divider"></div>
|
||||
|
||||
<div class="metadata-section" style:flex="1">
|
||||
<LabelInput
|
||||
allowedToEdit={!!roles.isDelegateOrAuthor(
|
||||
config.publicKey,
|
||||
repo.delegates.map(delegate => delegate.did),
|
||||
review.author.did,
|
||||
)}
|
||||
labels={review.labels}
|
||||
submitInProgress={labelSaveInProgress}
|
||||
save={async labels => {
|
||||
await editReview(review.id, verdict, labels, review.summary);
|
||||
}} />
|
||||
</div>
|
||||
|
||||
<div class="metadata-divider"></div>
|
||||
|
||||
<div class="metadata-section" style:flex="1">
|
||||
<div class="metadata-section-title">Participants</div>
|
||||
{#each contributors as contributor}
|
||||
<NodeId {...authorForNodeId(contributor)} />
|
||||
{/each}
|
||||
</div>
|
||||
</Border>
|
||||
|
||||
<div class="review-body">
|
||||
<CommentComponent
|
||||
rid={repo.rid}
|
||||
disallowEmptyBody={review.verdict === undefined}
|
||||
emptyBodyTooltip="Summary is mandatory when verdict is None"
|
||||
styleWidth="100%"
|
||||
caption="published review"
|
||||
id={review.id}
|
||||
author={review.author}
|
||||
timestamp={review.timestamp}
|
||||
editComment={(publicKeyFromDid(review.author.did) ===
|
||||
config.publicKey ||
|
||||
undefined) &&
|
||||
partial(async (id: string, summary: string) => {
|
||||
await editReview(id, verdict, review.labels, summary);
|
||||
}, review.id)}
|
||||
body={review.summary}>
|
||||
{#snippet beforeTimestamp()}
|
||||
on revision <Id id={revision.id} variant="oid" />
|
||||
{/snippet}
|
||||
</CommentComponent>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,71 +1,56 @@
|
||||
<script lang="ts">
|
||||
import type { PatchStatus } from "@app/views/repo/router";
|
||||
import type { Review } from "@bindings/cob/patch/Review";
|
||||
|
||||
import {
|
||||
absoluteTimestamp,
|
||||
authorForNodeId,
|
||||
formatTimestamp,
|
||||
verdictIcon,
|
||||
} from "@app/lib/utils";
|
||||
import { push } from "@app/lib/router";
|
||||
|
||||
import Icon from "./Icon.svelte";
|
||||
import Markdown from "./Markdown.svelte";
|
||||
import Icon from "@app/components/Icon.svelte";
|
||||
import Id from "@app/components/Id.svelte";
|
||||
import Label from "@app/components/Label.svelte";
|
||||
import Markdown from "@app/components/Markdown.svelte";
|
||||
import NodeId from "@app/components/NodeId.svelte";
|
||||
|
||||
interface Props {
|
||||
rid: string;
|
||||
patchId: string;
|
||||
review: Review;
|
||||
rid: string;
|
||||
status: PatchStatus | undefined;
|
||||
}
|
||||
|
||||
const { rid, review }: Props = $props();
|
||||
|
||||
const header = $derived.by(() => {
|
||||
if (!review.verdict) {
|
||||
return "published a review";
|
||||
}
|
||||
|
||||
return `${review.verdict ? `${review.verdict}ed` : "reviewed"} revision`;
|
||||
});
|
||||
|
||||
function icon(verdict: Review["verdict"]) {
|
||||
if (verdict === "accept") {
|
||||
return "comment-checkmark";
|
||||
} else if (verdict === "reject") {
|
||||
return "comment-cross";
|
||||
} else {
|
||||
return "comment";
|
||||
}
|
||||
}
|
||||
|
||||
function backgroundColor(verdict: Review["verdict"]) {
|
||||
if (verdict === undefined) {
|
||||
return "var(--color-fill-float)";
|
||||
} else if (verdict === "accept") {
|
||||
return "var(--color-fill-diff-green-light)";
|
||||
} else if (verdict === "reject") {
|
||||
return "var(--color-fill-diff-red-light)";
|
||||
}
|
||||
}
|
||||
|
||||
function color(verdict: Review["verdict"]) {
|
||||
if (verdict === undefined) {
|
||||
return "var(--color-foreground-dim)";
|
||||
} else if (verdict === "accept") {
|
||||
return "var(--color-foreground-success)";
|
||||
} else if (verdict === "reject") {
|
||||
return "var(--color-foreground-red)";
|
||||
}
|
||||
}
|
||||
const { patchId, review, rid, status }: Props = $props();
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.review {
|
||||
clip-path: var(--2px-corner-fill);
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
padding: 0.5rem 0.75rem;
|
||||
gap: 0.75rem;
|
||||
z-index: 1;
|
||||
position: relative;
|
||||
}
|
||||
/* We put the background and clip-path in a separate element to prevent
|
||||
popovers being clipped in the main element. */
|
||||
.review::after {
|
||||
position: absolute;
|
||||
z-index: -1;
|
||||
content: " ";
|
||||
background-color: var(--color-fill-float);
|
||||
clip-path: var(--2px-corner-fill);
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
}
|
||||
.review:hover::after {
|
||||
background-color: var(--color-fill-float-hover);
|
||||
}
|
||||
.review-content {
|
||||
padding: 10px 0.75rem 0.5rem 0;
|
||||
width: 100%;
|
||||
font-size: var(--font-size-small);
|
||||
display: flex;
|
||||
@@ -81,29 +66,68 @@
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
.icon {
|
||||
padding-top: 0.25rem;
|
||||
.status {
|
||||
padding: 0;
|
||||
margin: 0.5rem 0 0 0.5rem;
|
||||
}
|
||||
|
||||
.accepted {
|
||||
background-color: var(--color-fill-diff-green-light);
|
||||
}
|
||||
|
||||
.rejected {
|
||||
background-color: var(--color-fill-diff-red-light);
|
||||
}
|
||||
|
||||
.no-verdict {
|
||||
background-color: var(--color-fill-ghost);
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="review" style:background-color={backgroundColor(review.verdict)}>
|
||||
<div class="icon" style:color={color(review.verdict)}>
|
||||
<Icon name={icon(review.verdict)} />
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
||||
<div
|
||||
tabindex="0"
|
||||
role="button"
|
||||
class="review"
|
||||
style:cursor="pointer"
|
||||
onclick={() => {
|
||||
void push({
|
||||
resource: "repo.patch",
|
||||
rid,
|
||||
patch: patchId,
|
||||
status,
|
||||
reviewId: review.id,
|
||||
});
|
||||
}}>
|
||||
<div
|
||||
class:accepted={review.verdict === "accept"}
|
||||
class:rejected={review.verdict === "reject"}
|
||||
class:no-verdict={review.verdict === undefined}
|
||||
class="global-counter status">
|
||||
<Icon name={verdictIcon(review.verdict)} />
|
||||
</div>
|
||||
<div class="review-content">
|
||||
<div class="review-header">
|
||||
<div class="global-flex">
|
||||
<NodeId {...authorForNodeId(review.author)} />
|
||||
<span>{header}</span>
|
||||
<span>published review</span>
|
||||
<Id id={review.id} variant="oid" />
|
||||
<div class="timestamp" title={absoluteTimestamp(review.timestamp)}>
|
||||
{formatTimestamp(review.timestamp)}
|
||||
</div>
|
||||
{#if review.comments.length > 0}
|
||||
<div class="global-flex" style:gap="0.25rem" style:margin-left="auto">
|
||||
<Icon name="comment" />{review.comments.length}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{#if review.comments.length > 0}
|
||||
<div class="global-flex" style:gap="0.25rem" style:margin-left="auto">
|
||||
<Icon name="comment" />{review.comments.length}
|
||||
</div>
|
||||
{/if}
|
||||
{#if review.labels.length > 0}
|
||||
<div class="global-flex" style:margin-left="auto">
|
||||
{#each review.labels as label}
|
||||
<Label {label} />
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{#if review.summary?.trim()}
|
||||
<div>
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
import type { Config } from "@bindings/config/Config";
|
||||
import type { Diff } from "@bindings/diff/Diff";
|
||||
import type { Embed } from "@bindings/cob/thread/Embed";
|
||||
import type { PatchStatus } from "@app/views/repo/router";
|
||||
import type { Revision } from "@bindings/cob/patch/Revision";
|
||||
import type { Thread } from "@bindings/cob/thread/Thread";
|
||||
import type { Verdict } from "@bindings/cob/patch/Verdict";
|
||||
@@ -39,11 +40,12 @@
|
||||
patchId: string;
|
||||
revision: Revision;
|
||||
config: Config;
|
||||
status: PatchStatus | undefined;
|
||||
reload: () => Promise<void>;
|
||||
}
|
||||
|
||||
/* eslint-disable prefer-const */
|
||||
let { rid, repoDelegates, patchId, revision, config, reload }: Props =
|
||||
let { rid, repoDelegates, patchId, revision, config, status, reload }: Props =
|
||||
$props();
|
||||
/* eslint-enable prefer-const */
|
||||
|
||||
@@ -407,7 +409,7 @@
|
||||
{#if revision.reviews && revision.reviews.length}
|
||||
<div class:hide={hideReviews} style:margin-top="1rem">
|
||||
{#each revision.reviews as review}
|
||||
<ReviewTeaser {rid} {review} />
|
||||
<ReviewTeaser {rid} {review} {patchId} {status} />
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
<script lang="ts">
|
||||
import type { Review } from "@bindings/cob/patch/Review";
|
||||
import type { Snippet } from "svelte";
|
||||
|
||||
import capitalize from "lodash/capitalize.js";
|
||||
import { verdictIcon, verdictIconColor } from "@app/lib/utils";
|
||||
|
||||
import Icon from "@app/components/Icon.svelte";
|
||||
|
||||
interface Props {
|
||||
children?: Snippet;
|
||||
verdict: Review["verdict"];
|
||||
hoverable?: boolean;
|
||||
}
|
||||
|
||||
const { children, verdict, hoverable = false }: Props = $props();
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.badge {
|
||||
gap: 6px;
|
||||
padding-right: 10px;
|
||||
}
|
||||
.no-verdict {
|
||||
background-color: var(--color-fill-ghost);
|
||||
}
|
||||
.no-verdict.hoverable:hover {
|
||||
background-color: var(--color-fill-ghost-hover);
|
||||
}
|
||||
|
||||
.accepted {
|
||||
background-color: var(--color-fill-diff-green-light);
|
||||
}
|
||||
.accepted.hoverable:hover {
|
||||
background-color: var(--color-fill-diff-green);
|
||||
}
|
||||
|
||||
.rejected {
|
||||
background-color: var(--color-fill-diff-red-light);
|
||||
}
|
||||
.rejected.hoverable:hover {
|
||||
background-color: var(--color-fill-diff-red);
|
||||
}
|
||||
</style>
|
||||
|
||||
<span
|
||||
class="global-counter badge"
|
||||
style:color={verdictIconColor(verdict)}
|
||||
class:hoverable
|
||||
class:no-verdict={verdict === undefined}
|
||||
class:accepted={verdict === "accept"}
|
||||
class:rejected={verdict === "reject"}>
|
||||
<Icon name={verdictIcon(verdict)} />
|
||||
{verdict ? capitalize(`${verdict}ed`) : "None"}
|
||||
{@render children?.()}
|
||||
</span>
|
||||
@@ -0,0 +1,70 @@
|
||||
<script lang="ts">
|
||||
import type { Review } from "@bindings/cob/patch/Review";
|
||||
|
||||
import capitalize from "lodash/capitalize.js";
|
||||
|
||||
import { closeFocused } from "./Popover.svelte";
|
||||
import { verdictIcon, verdictIconColor } from "@app/lib/utils";
|
||||
|
||||
import Border from "@app/components/Border.svelte";
|
||||
import DropdownList from "@app/components/DropdownList.svelte";
|
||||
import DropdownListItem from "@app/components/DropdownListItem.svelte";
|
||||
import Icon from "@app/components/Icon.svelte";
|
||||
import Popover from "@app/components/Popover.svelte";
|
||||
import VerdictBadge from "@app/components/VerdictBadge.svelte";
|
||||
|
||||
interface Props {
|
||||
onSelect: (verdict: Review["verdict"]) => Promise<void>;
|
||||
summaryMissing: boolean;
|
||||
verdict: Review["verdict"];
|
||||
}
|
||||
|
||||
const { onSelect, summaryMissing, verdict }: Props = $props();
|
||||
</script>
|
||||
|
||||
<style>
|
||||
button {
|
||||
cursor: pointer;
|
||||
border: 0;
|
||||
background: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: var(--font-size-small);
|
||||
}
|
||||
</style>
|
||||
|
||||
<Popover popoverPadding="0" popoverPositionLeft="0" popoverPositionTop="2rem">
|
||||
{#snippet toggle(onclick)}
|
||||
<button {onclick}>
|
||||
<VerdictBadge {verdict} hoverable>
|
||||
<Icon name="chevron-down" />
|
||||
</VerdictBadge>
|
||||
</button>
|
||||
{/snippet}
|
||||
{#snippet popover()}
|
||||
<Border variant="ghost">
|
||||
<DropdownList items={[undefined, "accept", "reject"] as const}>
|
||||
{#snippet item(action)}
|
||||
<DropdownListItem
|
||||
title={action === undefined && summaryMissing
|
||||
? "Set a summary to select verdict None"
|
||||
: undefined}
|
||||
disabled={action === undefined && summaryMissing}
|
||||
selected={verdict === action}
|
||||
onclick={async () => {
|
||||
await onSelect(action);
|
||||
closeFocused();
|
||||
}}>
|
||||
<span class="global-flex" style:color={verdictIconColor(action)}>
|
||||
<Icon name={verdictIcon(action)} />
|
||||
{action ? capitalize(`${action}ed`) : "None"}
|
||||
</span>
|
||||
</DropdownListItem>
|
||||
{/snippet}
|
||||
</DropdownList>
|
||||
</Border>
|
||||
{/snippet}
|
||||
</Popover>
|
||||
@@ -3,6 +3,7 @@ import type { ComponentProps } from "svelte";
|
||||
import type { Author } from "@bindings/cob/Author";
|
||||
import type { Issue } from "@bindings/cob/issue/Issue";
|
||||
import type { Patch } from "@bindings/cob/patch/Patch";
|
||||
import type { Review } from "@bindings/cob/patch/Review";
|
||||
|
||||
import bs58 from "bs58";
|
||||
import twemojiModule from "twemoji";
|
||||
@@ -237,3 +238,23 @@ export function gravatarURL(email: string): string {
|
||||
|
||||
return `https://www.gravatar.com/avatar/${hash}`;
|
||||
}
|
||||
|
||||
export function verdictIcon(verdict: Review["verdict"]) {
|
||||
if (verdict === "accept") {
|
||||
return "comment-checkmark";
|
||||
} else if (verdict === "reject") {
|
||||
return "comment-cross";
|
||||
} else {
|
||||
return "comment";
|
||||
}
|
||||
}
|
||||
|
||||
export function verdictIconColor(verdict: Review["verdict"]) {
|
||||
if (verdict === "accept") {
|
||||
return "var(--color-foreground-success)";
|
||||
} else if (verdict === "reject") {
|
||||
return "var(--color-foreground-red)";
|
||||
} else {
|
||||
return "var(--color-foreground-dim)";
|
||||
}
|
||||
}
|
||||
|
||||
+203
-178
@@ -7,6 +7,7 @@
|
||||
import type { Patch } from "@bindings/cob/patch/Patch";
|
||||
import type { PatchStatus } from "./router";
|
||||
import type { RepoInfo } from "@bindings/repo/RepoInfo";
|
||||
import type { Review } from "@bindings/cob/patch/Review";
|
||||
import type { Revision } from "@bindings/cob/patch/Revision";
|
||||
|
||||
import capitalize from "lodash/capitalize";
|
||||
@@ -37,6 +38,7 @@
|
||||
import PatchTeaser from "@app/components/PatchTeaser.svelte";
|
||||
import PatchTimeline from "@app/components/PatchTimeline.svelte";
|
||||
import Popover, { closeFocused } from "@app/components/Popover.svelte";
|
||||
import ReviewComponent from "@app/components/Review.svelte";
|
||||
import RevisionBadges from "@app/components/RevisionBadges.svelte";
|
||||
import RevisionComponent from "@app/components/Revision.svelte";
|
||||
import RevisionSelector from "@app/components/RevisionSelector.svelte";
|
||||
@@ -52,6 +54,7 @@
|
||||
config: Config;
|
||||
activity: Operation<Action>[];
|
||||
status: PatchStatus | undefined;
|
||||
review: Review | undefined;
|
||||
}
|
||||
|
||||
/* eslint-disable prefer-const */
|
||||
@@ -63,6 +66,7 @@
|
||||
config,
|
||||
status: initialStatus,
|
||||
activity,
|
||||
review,
|
||||
}: Props = $props();
|
||||
/* eslint-enable prefer-const */
|
||||
|
||||
@@ -110,6 +114,7 @@
|
||||
rid: repo.rid,
|
||||
id: patch.id,
|
||||
});
|
||||
review = undefined;
|
||||
}
|
||||
|
||||
async function editTitle(rid: string, patchId: string, title: string) {
|
||||
@@ -213,7 +218,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function reload() {
|
||||
async function reload(reviewId?: string) {
|
||||
[config, repo, patches, patch, revisions, activity] = await Promise.all([
|
||||
invoke<Config>("config"),
|
||||
invoke<RepoInfo>("repo_by_id", {
|
||||
@@ -236,6 +241,9 @@
|
||||
id: patch.id,
|
||||
}),
|
||||
]);
|
||||
review = revisions
|
||||
.flatMap(r => r.reviews || [])
|
||||
.find(review => review.id === reviewId);
|
||||
}
|
||||
|
||||
async function loadPatches(filter: PatchStatus | undefined) {
|
||||
@@ -430,55 +438,22 @@
|
||||
</div>
|
||||
{/snippet}
|
||||
|
||||
<div class="content">
|
||||
<div style:margin-bottom="0.5rem">
|
||||
{#if editingTitle}
|
||||
<div class="title">
|
||||
<div
|
||||
class="global-counter status"
|
||||
style:color={patchStatusColor[patch.state.status]}
|
||||
style:background-color={patchStatusBackgroundColor[
|
||||
patch.state.status
|
||||
]}>
|
||||
<Icon
|
||||
name={patch.state.status === "open"
|
||||
? "patch"
|
||||
: `patch-${patch.state.status}`} />
|
||||
</div>
|
||||
|
||||
<TextInput
|
||||
valid={updatedTitle.trim().length > 0}
|
||||
bind:value={updatedTitle}
|
||||
autofocus
|
||||
onSubmit={async () => {
|
||||
if (updatedTitle.trim().length > 0) {
|
||||
await editTitle(repo.rid, patch.id, updatedTitle);
|
||||
}
|
||||
}}
|
||||
onDismiss={() => {
|
||||
updatedTitle = patch.title;
|
||||
editingTitle = !editingTitle;
|
||||
}} />
|
||||
<div class="title-icons">
|
||||
<Icon
|
||||
name="checkmark"
|
||||
onclick={async () => {
|
||||
if (updatedTitle.trim().length > 0) {
|
||||
await editTitle(repo.rid, patch.id, updatedTitle);
|
||||
}
|
||||
}} />
|
||||
<Icon
|
||||
name="cross"
|
||||
onclick={() => {
|
||||
updatedTitle = patch.title;
|
||||
editingTitle = !editingTitle;
|
||||
}} />
|
||||
<PatchStateButton patchState={patch.state} save={saveState} />
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="title">
|
||||
<div class="global-flex" style:gap="0">
|
||||
{#if review}
|
||||
<ReviewComponent
|
||||
{config}
|
||||
patchId={patch.id}
|
||||
{repo}
|
||||
{reload}
|
||||
{review}
|
||||
revision={selectedRevision}
|
||||
onNavigateBack={() => {
|
||||
review = undefined;
|
||||
}} />
|
||||
{:else}
|
||||
<div class="content">
|
||||
<div style:margin-bottom="0.5rem">
|
||||
{#if editingTitle}
|
||||
<div class="title">
|
||||
<div
|
||||
class="global-counter status"
|
||||
style:color={patchStatusColor[patch.state.status]}
|
||||
@@ -490,139 +465,189 @@
|
||||
? "patch"
|
||||
: `patch-${patch.state.status}`} />
|
||||
</div>
|
||||
<InlineTitle content={patch.title} fontSize="medium" />
|
||||
</div>
|
||||
{#if roles.isDelegateOrAuthor( config.publicKey, repo.delegates.map(delegate => delegate.did), patch.author.did, )}
|
||||
|
||||
<TextInput
|
||||
valid={updatedTitle.trim().length > 0}
|
||||
bind:value={updatedTitle}
|
||||
autofocus
|
||||
onSubmit={async () => {
|
||||
if (updatedTitle.trim().length > 0) {
|
||||
await editTitle(repo.rid, patch.id, updatedTitle);
|
||||
}
|
||||
}}
|
||||
onDismiss={() => {
|
||||
updatedTitle = patch.title;
|
||||
editingTitle = !editingTitle;
|
||||
}} />
|
||||
<div class="title-icons">
|
||||
<Icon name="pen" onclick={() => (editingTitle = !editingTitle)} />
|
||||
<Icon
|
||||
name="checkmark"
|
||||
onclick={async () => {
|
||||
if (updatedTitle.trim().length > 0) {
|
||||
await editTitle(repo.rid, patch.id, updatedTitle);
|
||||
}
|
||||
}} />
|
||||
<Icon
|
||||
name="cross"
|
||||
onclick={() => {
|
||||
updatedTitle = patch.title;
|
||||
editingTitle = !editingTitle;
|
||||
}} />
|
||||
<PatchStateButton patchState={patch.state} save={saveState} />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<Border variant="ghost" styleGap="0">
|
||||
<div class="metadata-section" style:min-width="8rem">
|
||||
<div class="metadata-section-title">Status</div>
|
||||
<PatchStateBadge state={patch.state} />
|
||||
</div>
|
||||
|
||||
<div class="metadata-divider"></div>
|
||||
|
||||
<div class="metadata-section" style:flex="1">
|
||||
<LabelInput
|
||||
allowedToEdit={!!roles.isDelegateOrAuthor(
|
||||
config.publicKey,
|
||||
repo.delegates.map(delegate => delegate.did),
|
||||
patch.author.did,
|
||||
)}
|
||||
labels={patch.labels}
|
||||
submitInProgress={labelSaveInProgress}
|
||||
save={saveLabels} />
|
||||
</div>
|
||||
|
||||
<div class="metadata-divider"></div>
|
||||
|
||||
<div class="metadata-section" style:flex="1">
|
||||
<AssigneeInput
|
||||
allowedToEdit={!!roles.isDelegateOrAuthor(
|
||||
config.publicKey,
|
||||
repo.delegates.map(delegate => delegate.did),
|
||||
patch.author.did,
|
||||
)}
|
||||
assignees={patch.assignees}
|
||||
submitInProgress={assigneesSaveInProgress}
|
||||
save={saveAssignees} />
|
||||
</div>
|
||||
</Border>
|
||||
|
||||
<div class="global-flex" style:gap="0.5rem" style:margin-top="1rem">
|
||||
<Border stylePosition="relative" variant="ghost" flatBottom>
|
||||
<div
|
||||
class="global-flex"
|
||||
style:z-index="10"
|
||||
style:gap="1rem"
|
||||
style:padding="0 1rem"
|
||||
style:width="100%">
|
||||
<span class="txt-small" style:color="var(--color-foreground-dim)">
|
||||
Revisions
|
||||
</span>
|
||||
<Tab
|
||||
active={tab === "patch"}
|
||||
onclick={() => {
|
||||
tab = "patch";
|
||||
}}>
|
||||
{formatOid(patch.id)}
|
||||
<span
|
||||
class="global-counter"
|
||||
style:height="24px"
|
||||
style:color="var(--color-foreground-contrast)">
|
||||
Initial
|
||||
</span>
|
||||
</Tab>
|
||||
{#if revisions.length > 1}
|
||||
<Tab
|
||||
active={tab === "revisions"}
|
||||
onclick={() => {
|
||||
tab = "revisions";
|
||||
}}>
|
||||
{formatOid(selectedRevision.id)}
|
||||
<div class="global-flex" style:gap="0.25rem">
|
||||
<RevisionBadges revision={selectedRevision} {revisions} />
|
||||
<RevisionSelector
|
||||
{patch}
|
||||
{revisions}
|
||||
{selectedRevision}
|
||||
selectRevision={rev => {
|
||||
selectedRevision = rev;
|
||||
tab = "revisions";
|
||||
}} />
|
||||
</div>
|
||||
</Tab>
|
||||
{/if}
|
||||
|
||||
<div style:margin-left="auto">
|
||||
<Tab
|
||||
active={tab === "timeline"}
|
||||
onclick={() => {
|
||||
tab = "timeline";
|
||||
}}>
|
||||
<Icon name="clock" />
|
||||
Timeline
|
||||
</Tab>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="title">
|
||||
<div class="global-flex" style:gap="0">
|
||||
<div
|
||||
class="global-counter status"
|
||||
style:color={patchStatusColor[patch.state.status]}
|
||||
style:background-color={patchStatusBackgroundColor[
|
||||
patch.state.status
|
||||
]}>
|
||||
<Icon
|
||||
name={patch.state.status === "open"
|
||||
? "patch"
|
||||
: `patch-${patch.state.status}`} />
|
||||
</div>
|
||||
<InlineTitle content={patch.title} fontSize="medium" />
|
||||
</div>
|
||||
{#if roles.isDelegateOrAuthor( config.publicKey, repo.delegates.map(delegate => delegate.did), patch.author.did, )}
|
||||
<div class="title-icons">
|
||||
<Icon
|
||||
name="pen"
|
||||
onclick={() => (editingTitle = !editingTitle)} />
|
||||
<PatchStateButton patchState={patch.state} save={saveState} />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<Border variant="ghost" styleGap="0">
|
||||
<div class="metadata-section" style:min-width="8rem">
|
||||
<div class="metadata-section-title">Status</div>
|
||||
<PatchStateBadge state={patch.state} />
|
||||
</div>
|
||||
|
||||
<div class="metadata-divider"></div>
|
||||
|
||||
<div class="metadata-section" style:flex="1">
|
||||
<LabelInput
|
||||
allowedToEdit={!!roles.isDelegateOrAuthor(
|
||||
config.publicKey,
|
||||
repo.delegates.map(delegate => delegate.did),
|
||||
patch.author.did,
|
||||
)}
|
||||
labels={patch.labels}
|
||||
submitInProgress={labelSaveInProgress}
|
||||
save={saveLabels} />
|
||||
</div>
|
||||
|
||||
<div class="metadata-divider"></div>
|
||||
|
||||
<div class="metadata-section" style:flex="1">
|
||||
<AssigneeInput
|
||||
allowedToEdit={!!roles.isDelegateOrAuthor(
|
||||
config.publicKey,
|
||||
repo.delegates.map(delegate => delegate.did),
|
||||
patch.author.did,
|
||||
)}
|
||||
assignees={patch.assignees}
|
||||
submitInProgress={assigneesSaveInProgress}
|
||||
save={saveAssignees} />
|
||||
</div>
|
||||
</Border>
|
||||
</div>
|
||||
|
||||
<Border
|
||||
variant="ghost"
|
||||
flatTop
|
||||
styleWidth="100%"
|
||||
stylePadding="1rem"
|
||||
styleMinWidth="0"
|
||||
styleDisplay="block"
|
||||
styleFlexDirection="column"
|
||||
styleAlignItems="flex-start">
|
||||
{#if tab === "patch"}
|
||||
<RevisionComponent
|
||||
rid={repo.rid}
|
||||
repoDelegates={repo.delegates}
|
||||
patchId={patch.id}
|
||||
{reload}
|
||||
revision={revisions[0]}
|
||||
{config} />
|
||||
{:else if tab === "timeline"}
|
||||
<PatchTimeline {activity} patchId={patch.id} />
|
||||
{:else}
|
||||
<RevisionComponent
|
||||
rid={repo.rid}
|
||||
repoDelegates={repo.delegates}
|
||||
patchId={patch.id}
|
||||
{reload}
|
||||
revision={selectedRevision}
|
||||
{config} />
|
||||
{/if}
|
||||
</Border>
|
||||
</div>
|
||||
<div class="global-flex" style:gap="0.5rem" style:margin-top="1rem">
|
||||
<Border stylePosition="relative" variant="ghost" flatBottom>
|
||||
<div
|
||||
class="global-flex"
|
||||
style:z-index="10"
|
||||
style:gap="1rem"
|
||||
style:padding="0 1rem"
|
||||
style:width="100%">
|
||||
<span class="txt-small" style:color="var(--color-foreground-dim)">
|
||||
Revisions
|
||||
</span>
|
||||
<Tab
|
||||
active={tab === "patch"}
|
||||
onclick={() => {
|
||||
tab = "patch";
|
||||
}}>
|
||||
{formatOid(patch.id)}
|
||||
<span
|
||||
class="global-counter"
|
||||
style:height="24px"
|
||||
style:color="var(--color-foreground-contrast)">
|
||||
Initial
|
||||
</span>
|
||||
</Tab>
|
||||
{#if revisions.length > 1}
|
||||
<Tab
|
||||
active={tab === "revisions"}
|
||||
onclick={() => {
|
||||
tab = "revisions";
|
||||
}}>
|
||||
{formatOid(selectedRevision.id)}
|
||||
<div class="global-flex" style:gap="0.25rem">
|
||||
<RevisionBadges revision={selectedRevision} {revisions} />
|
||||
<RevisionSelector
|
||||
{patch}
|
||||
{revisions}
|
||||
{selectedRevision}
|
||||
selectRevision={rev => {
|
||||
selectedRevision = rev;
|
||||
tab = "revisions";
|
||||
}} />
|
||||
</div>
|
||||
</Tab>
|
||||
{/if}
|
||||
|
||||
<div style:margin-left="auto">
|
||||
<Tab
|
||||
active={tab === "timeline"}
|
||||
onclick={() => {
|
||||
tab = "timeline";
|
||||
}}>
|
||||
<Icon name="clock" />
|
||||
Timeline
|
||||
</Tab>
|
||||
</div>
|
||||
</div>
|
||||
</Border>
|
||||
</div>
|
||||
|
||||
<Border
|
||||
variant="ghost"
|
||||
flatTop
|
||||
styleWidth="100%"
|
||||
stylePadding="1rem"
|
||||
styleMinWidth="0"
|
||||
styleDisplay="block"
|
||||
styleFlexDirection="column"
|
||||
styleAlignItems="flex-start">
|
||||
{#if tab === "patch"}
|
||||
<RevisionComponent
|
||||
rid={repo.rid}
|
||||
repoDelegates={repo.delegates}
|
||||
patchId={patch.id}
|
||||
{reload}
|
||||
{status}
|
||||
revision={revisions[0]}
|
||||
{config} />
|
||||
{:else if tab === "timeline"}
|
||||
<PatchTimeline {activity} patchId={patch.id} />
|
||||
{:else}
|
||||
<RevisionComponent
|
||||
rid={repo.rid}
|
||||
repoDelegates={repo.delegates}
|
||||
patchId={patch.id}
|
||||
{reload}
|
||||
{status}
|
||||
revision={selectedRevision}
|
||||
{config} />
|
||||
{/if}
|
||||
</Border>
|
||||
</div>
|
||||
{/if}
|
||||
</Layout>
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import type { Action as IssueAction } from "@bindings/cob/issue/Action";
|
||||
import type { Action as PatchAction } from "@bindings/cob/patch/Action";
|
||||
import type { Config } from "@bindings/config/Config";
|
||||
import type { Thread } from "@bindings/cob/thread/Thread";
|
||||
import type { Issue } from "@bindings/cob/issue/Issue";
|
||||
import type { Operation } from "@bindings/cob/Operation";
|
||||
import type { PaginatedQuery } from "@bindings/cob/PaginatedQuery";
|
||||
import type { Patch } from "@bindings/cob/patch/Patch";
|
||||
import type { RepoInfo } from "@bindings/repo/RepoInfo";
|
||||
import type { Review } from "@bindings/cob/patch/Review";
|
||||
import type { Revision } from "@bindings/cob/patch/Revision";
|
||||
import type { Thread } from "@bindings/cob/thread/Thread";
|
||||
|
||||
import { invoke } from "@app/lib/invoke";
|
||||
import { unreachable } from "@app/lib/utils";
|
||||
@@ -73,6 +74,7 @@ export interface RepoPatchRoute {
|
||||
rid: string;
|
||||
patch: string;
|
||||
status: PatchStatus | undefined;
|
||||
reviewId: string | undefined;
|
||||
}
|
||||
|
||||
export interface LoadedRepoPatchRoute {
|
||||
@@ -83,6 +85,7 @@ export interface LoadedRepoPatchRoute {
|
||||
patch: Patch;
|
||||
patches: PaginatedQuery<Patch[]>;
|
||||
status: PatchStatus | undefined;
|
||||
review: Review | undefined;
|
||||
revisions: Revision[];
|
||||
activity: Operation<PatchAction>[];
|
||||
};
|
||||
@@ -145,6 +148,10 @@ export async function loadPatch(
|
||||
],
|
||||
);
|
||||
|
||||
const review = revisions
|
||||
.flatMap(r => r.reviews || [])
|
||||
.find(review => review.id === route.reviewId);
|
||||
|
||||
return {
|
||||
resource: "repo.patch",
|
||||
params: {
|
||||
@@ -154,6 +161,7 @@ export async function loadPatch(
|
||||
patches,
|
||||
revisions,
|
||||
status: route.status,
|
||||
review,
|
||||
activity,
|
||||
},
|
||||
};
|
||||
@@ -332,12 +340,14 @@ export function repoUrlToRoute(
|
||||
const status = (searchParams.get("status") ?? undefined) as
|
||||
| PatchStatus
|
||||
| undefined;
|
||||
const reviewId = searchParams.get("reviewId") ?? undefined;
|
||||
if (id) {
|
||||
return {
|
||||
resource: "repo.patch",
|
||||
rid,
|
||||
patch: id,
|
||||
status,
|
||||
reviewId,
|
||||
};
|
||||
} else {
|
||||
return { resource: "repo.patches", rid, status };
|
||||
|
||||
Reference in New Issue
Block a user