mirror of
https://seed.radicle.dev/rad:z4V1sjrXqjvFdnCUbxPFqd5p4DtH5.git
synced 2026-11-04 03:00:32 +08:00
Add review page
This commit is contained in:
@@ -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>
|
||||
Reference in New Issue
Block a user