Fix QA review comments (#1723)

Fixes https://github.com/webrecorder/browsertrix/issues/1710

Fixes date and deletion for newly added comments.
This commit is contained in:
sua yoo 2024-04-23 13:31:52 -07:00 committed by GitHub
parent 9836e750c8
commit 1915274e26
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 198 additions and 204 deletions

View File

@ -286,7 +286,7 @@ class PageOps:
text: str,
user: User,
crawl_id: str,
) -> Dict[str, bool]:
) -> Dict[str, Union[bool, PageNote]]:
"""Add note to page"""
note = PageNote(id=uuid4(), text=text, userid=user.id, userName=user.name)
@ -304,7 +304,7 @@ class PageOps:
if not result:
raise HTTPException(status_code=404, detail="page_not_found")
return {"added": True}
return {"added": True, "data": note}
async def update_page_note(
self,
@ -313,7 +313,7 @@ class PageOps:
note_in: PageNoteEdit,
user: User,
crawl_id: str,
) -> Dict[str, bool]:
) -> Dict[str, Union[bool, PageNote]]:
"""Update specific page note"""
page = await self.get_page_raw(page_id, oid)
page_notes = page.get("notes", [])
@ -345,7 +345,7 @@ class PageOps:
if not result:
raise HTTPException(status_code=404, detail="page_not_found")
return {"updated": True}
return {"updated": True, "data": new_note}
async def delete_page_notes(
self,

View File

@ -16,7 +16,11 @@ import { TailwindElement } from "@/classes/TailwindElement";
import { NavigateController } from "@/controllers/navigate";
import { ReviewStatus, type ArchivedItem } from "@/types/crawler";
import { renderName } from "@/utils/crawler";
import { formatDate, formatNumber, getLocale } from "@/utils/localization";
import {
formatISODateString,
formatNumber,
getLocale,
} from "@/utils/localization";
export type CheckboxChangeEventDetail = {
checked: boolean;
@ -269,7 +273,7 @@ export class ArchivedItemListItem extends TailwindElement {
? html`
<sl-tooltip
content=${msg(
str`Last run started on ${formatDate(lastQAStarted)}`,
str`Last run started on ${formatISODateString(lastQAStarted)}`,
)}
>
<span>

View File

@ -1,31 +1,24 @@
import { localized, msg, str } from "@lit/localize";
import type { SlTextarea } from "@shoelace-style/shoelace";
import { localized, msg } from "@lit/localize";
import { css, html } from "lit";
import { customElement, property, query, state } from "lit/decorators.js";
import { customElement, property } from "lit/decorators.js";
import { classMap } from "lit/directives/class-map.js";
import { keyed } from "lit/directives/keyed.js";
import { when } from "lit/directives/when.js";
import { TailwindElement } from "@/classes/TailwindElement";
import type { Dialog } from "@/components/ui/dialog";
import { APIController } from "@/controllers/api";
import { NotifyController } from "@/controllers/notify";
import type {
ArchivedItemPage,
ArchivedItemPageComment,
} from "@/types/crawler";
import type { ArchivedItemPage } from "@/types/crawler";
import { type AuthState } from "@/utils/AuthService";
export type UpdateItemPageDetail = {
export type UpdatePageApprovalDetail = {
id: ArchivedItemPage["id"];
approved?: ArchivedItemPage["approved"];
notes?: ArchivedItemPage["notes"];
};
/**
* Manage crawl QA page approval
*
* @fires btrix-update-item-page
* @fires btrix-update-page-approval
* @fires btrix-show-comments
*/
@localized()
@customElement("btrix-page-qa-approval")
@ -185,15 +178,6 @@ export class PageQAToolbar extends TailwindElement {
@property({ type: Boolean })
disabled = false;
@state()
private showComments = false;
@query("btrix-dialog")
private readonly dialog!: Dialog;
@query('sl-textarea[name="pageComment"]')
private readonly textarea!: SlTextarea;
private readonly api = new APIController(this);
private readonly notify = new NotifyController(this);
@ -224,7 +208,7 @@ export class PageQAToolbar extends TailwindElement {
aria-checked=${approved}
?disabled=${disabled}
@click=${async () =>
this.submitReview({ approved: approved ? null : true })}
this.submitApproval({ approved: approved ? null : true })}
>
<sl-icon name="hand-thumbs-up" label=${msg("Approve")}></sl-icon>
</button>
@ -236,7 +220,8 @@ export class PageQAToolbar extends TailwindElement {
})}
aria-checked=${commented}
?disabled=${disabled}
@click=${() => (this.showComments = true)}
@click=${() =>
this.dispatchEvent(new CustomEvent("btrix-show-comments"))}
>
<sl-icon name="chat-square-text" label=${msg("Comment")}></sl-icon>
</button>
@ -249,100 +234,24 @@ export class PageQAToolbar extends TailwindElement {
aria-checked=${rejected}
?disabled=${disabled}
@click=${async () =>
this.submitReview({ approved: rejected ? null : false })}
this.submitApproval({ approved: rejected ? null : false })}
>
<sl-icon name="hand-thumbs-down" label=${msg("Reject")}></sl-icon>
</button>
</fieldset>
<btrix-dialog
label=${msg("Page Comments")}
?open=${this.showComments}
@sl-hide=${() => (this.showComments = false)}
>
${keyed(this.showComments, this.renderComments())}
</p>
<sl-button
slot="footer"
size="small"
variant="primary"
@click=${() => this.dialog.submit()}
>
${msg("Submit Comment")}
</sl-button>
</btrix-dialog>
`;
}
private renderComments() {
const comments = this.page?.notes || [];
return html`
${when(
comments.length,
() => html`
<btrix-details open>
<span slot="title"
>${msg(str`Comments (${comments.length.toLocaleString()})`)}</span
>
${when(
this.page?.notes,
(notes) => html`
<ul>
${notes.map(
(comment) =>
html`<li class="mb-3">
<div
class="flex items-center justify-between rounded-t border bg-neutral-50 text-xs leading-none text-neutral-600"
>
<div class="p-2">
${msg(
str`${comment.userName} commented on ${new Date(comment.created + "Z").toLocaleDateString()}`,
)}
</div>
<sl-icon-button
class="hover:text-danger"
name="trash3"
@click=${async () => this.deleteComment(comment.id)}
></sl-icon-button>
</div>
<div class="rounded-b border-b border-l border-r p-2">
${comment.text}
</div>
</li> `,
)}
</ul>
`,
() => html`
<p class="text-neutral-500">
${msg("This page doesn't have any comments.")}
</p>
`,
)}
</btrix-details>
`,
)}
<form @submit=${this.onSubmitComment}>
<sl-textarea
name="pageComment"
label=${msg("Add a comment")}
placeholder=${msg("Enter page feedback")}
minlength="1"
maxlength="500"
></sl-textarea>
</form>
`;
}
private async submitReview({
private async submitApproval({
approved,
}: {
approved: ArchivedItemPage["approved"];
}) {
if (!this.page) return;
if (!this.pageId) return;
try {
await this.api.fetch(
`/orgs/${this.orgId}/crawls/${this.itemId}/pages/${this.page.id}`,
`/orgs/${this.orgId}/crawls/${this.itemId}/pages/${this.pageId}`,
this.authState!,
{
method: "PATCH",
@ -350,7 +259,17 @@ export class PageQAToolbar extends TailwindElement {
},
);
void this.dispatchPageUpdate({ approved });
this.dispatchEvent(
new CustomEvent<UpdatePageApprovalDetail>(
"btrix-update-page-approval",
{
detail: {
id: this.pageId,
approved,
},
},
),
);
} catch (e: unknown) {
console.debug(e);
this.notify.toast({
@ -360,79 +279,4 @@ export class PageQAToolbar extends TailwindElement {
});
}
}
private async onSubmitComment(e: SubmitEvent) {
e.preventDefault();
const value = this.textarea.value;
if (!value) return;
try {
await this.api.fetch(
`/orgs/${this.orgId}/crawls/${this.itemId}/pages/${this.pageId}/notes`,
this.authState!,
{
method: "POST",
body: JSON.stringify({ text: value }),
},
);
this.showComments = false;
const comment: ArchivedItemPageComment = {
id: "",
created: "",
modified: "",
userName: "",
text: value,
};
void this.dispatchPageUpdate({
notes: this.page?.notes ? [...this.page.notes, comment] : [comment],
});
} catch (e: unknown) {
console.debug(e);
this.notify.toast({
message: msg("Sorry, couldn't add comment at this time."),
variant: "danger",
icon: "exclamation-octagon",
});
}
}
private async deleteComment(commentId: string): Promise<void> {
try {
await this.api.fetch(
`/orgs/${this.orgId}/crawls/${this.itemId}/pages/${this.pageId}/notes/delete`,
this.authState!,
{
method: "POST",
body: JSON.stringify({ delete_list: [commentId] }),
},
);
void this.dispatchPageUpdate({
notes: this.page?.notes?.filter(({ id }) => id === commentId) || [],
});
} catch {
this.notify.toast({
message: msg("Sorry, couldn't delete comment at this time."),
variant: "danger",
icon: "exclamation-octagon",
});
}
}
private async dispatchPageUpdate(page: Partial<UpdateItemPageDetail>) {
if (!this.pageId) return;
this.dispatchEvent(
new CustomEvent<UpdateItemPageDetail>("btrix-update-item-page", {
detail: {
id: this.pageId,
...page,
},
}),
);
}
}

View File

@ -5,7 +5,7 @@ import { customElement, property } from "lit/decorators.js";
import { TailwindElement } from "@/classes/TailwindElement";
import { type QARun } from "@/types/qa";
import { formatDate } from "@/utils/localization";
import { formatISODateString } from "@/utils/localization";
export type SelectDetail = { item: { id: string } };
@ -39,7 +39,7 @@ export class QaRunDropdown extends TailwindElement {
slot="prefix"
hoist
></btrix-crawl-status>
${formatDate(selectedRun.finished)} `
${formatISODateString(selectedRun.finished)} `
: msg("Select a QA run")}
</sl-button>
<sl-menu>
@ -52,7 +52,7 @@ export class QaRunDropdown extends TailwindElement {
?disabled=${isSelected}
?checked=${isSelected}
>
${formatDate(run.finished)}
${formatISODateString(run.finished)}
<btrix-crawl-status
type="qa"
hideLabel

View File

@ -1,4 +1,5 @@
import { localized, msg } from "@lit/localize";
import { localized, msg, str } from "@lit/localize";
import type { SlTextarea } from "@shoelace-style/shoelace";
import { serialize } from "@shoelace-style/shoelace/dist/utilities/form.js";
import { merge } from "immutable";
import { html, nothing, type PropertyValues } from "lit";
@ -31,18 +32,18 @@ import {
type SortableFieldNames,
type SortDirection,
} from "@/features/qa/page-list/page-list";
import { type UpdateItemPageDetail } from "@/features/qa/page-qa-approval";
import { type UpdatePageApprovalDetail } from "@/features/qa/page-qa-approval";
import type { SelectDetail } from "@/features/qa/qa-run-dropdown";
import type {
APIPaginatedList,
APIPaginationQuery,
APISortQuery,
} from "@/types/api";
import type { ArchivedItem } from "@/types/crawler";
import type { ArchivedItem, ArchivedItemPageComment } from "@/types/crawler";
import type { ArchivedItemQAPage, QARun } from "@/types/qa";
import { type AuthState } from "@/utils/AuthService";
import { finishedCrawlStates, isActive, renderName } from "@/utils/crawler";
import { getLocale } from "@/utils/localization";
import { formatISODateString, getLocale } from "@/utils/localization";
const DEFAULT_PAGE_SIZE = 100;
@ -164,6 +165,12 @@ export class ArchivedItemQA extends TailwindElement {
@query(".reviewDialog")
private readonly reviewDialog?: Dialog | null;
@query(".commentDialog")
private readonly commentDialog?: Dialog | null;
@query('sl-textarea[name="pageComment"]')
private readonly commentTextarea?: SlTextarea | null;
connectedCallback(): void {
super.connectedCallback();
// Receive messages from replay-web-page windows
@ -386,8 +393,10 @@ export class ArchivedItemQA extends TailwindElement {
class="flex-auto flex-shrink-0 flex-grow basis-32 truncate text-base font-semibold text-neutral-700"
title="${this.page?.title ?? ""}"
>
${this.page?.title ||
html`<span class="opacity-50">${msg("No page title")}</span>`}
${
this.page?.title ||
html`<span class="opacity-50">${msg("No page title")}</span>`
}
</h2>
<div
class="ml-auto flex flex-grow basis-auto flex-wrap justify-between gap-2 @lg:flex-grow-0"
@ -415,7 +424,8 @@ export class ArchivedItemQA extends TailwindElement {
.pageId=${this.itemPageId}
.page=${this.page}
?disabled=${disableReview}
@btrix-update-item-page=${this.onUpdateItemPage}
@btrix-show-comments=${() => void this.commentDialog?.show()}
@btrix-update-page-approval=${this.onUpdatePageApproval}
></btrix-page-qa-approval>
</sl-tooltip>
<sl-button
@ -529,6 +539,23 @@ export class ArchivedItemQA extends TailwindElement {
></btrix-qa-page-list>
</section>
</article>
<btrix-dialog
class="commentDialog"
label=${msg("Page Comments")}
>
${this.renderComments()}
</p>
<sl-button
slot="footer"
size="small"
variant="primary"
@click=${() => this.commentDialog?.submit()}
>
${msg("Submit Comment")}
</sl-button>
</btrix-dialog>
<btrix-dialog
class="reviewDialog [--width:60rem]"
label=${msg("QA Review")}
@ -667,6 +694,64 @@ export class ArchivedItemQA extends TailwindElement {
);
}
private renderComments() {
return html`
${when(
this.page?.notes?.length,
(commentCount) => html`
<btrix-details open>
<span slot="title">
${msg(str`Comments (${commentCount.toLocaleString()})`)}
</span>
<ul>
${this.page?.notes?.map(
(comment) =>
html`<li class="mb-3">
<div
class="flex items-center justify-between rounded-t border bg-neutral-50 text-xs leading-none text-neutral-600"
>
<div class="p-2">
${msg(
str`${comment.userName} commented on ${formatISODateString(
comment.created,
{
hour: undefined,
minute: undefined,
},
)}`,
)}
</div>
<sl-tooltip content=${msg("Delete comment")}>
<sl-icon-button
class="hover:text-danger"
name="trash3"
label=${msg("Delete comment")}
@click=${async () =>
this.deletePageComment(comment.id)}
></sl-icon-button>
</sl-tooltip>
</div>
<div class="rounded-b border-b border-l border-r p-2">
${comment.text}
</div>
</li> `,
)}
</ul>
</btrix-details>
`,
)}
<form @submit=${this.onSubmitComment}>
<sl-textarea
name="pageComment"
label=${msg("Add a comment")}
placeholder=${msg("Enter page feedback")}
minlength="1"
maxlength="500"
></sl-textarea>
</form>
`;
}
private renderPanelToolbar() {
const buttons = html`
${choose(this.tab, [
@ -786,20 +871,17 @@ export class ArchivedItemQA extends TailwindElement {
this.navigate.link(e, undefined, /* resetScroll: */ false);
};
private async onUpdateItemPage(e: CustomEvent<UpdateItemPageDetail>) {
private async onUpdatePageApproval(e: CustomEvent<UpdatePageApprovalDetail>) {
const updated = e.detail;
if (!this.page || this.page.id !== updated.id) return;
const reviewStatusChanged =
this.page.approved !== updated.approved ||
this.page.notes?.length !== updated.notes?.length;
this.page = merge<ArchivedItemQAPage>(this.page, updated);
const reviewStatusChanged = this.page.approved !== updated.approved;
if (reviewStatusChanged) {
void this.fetchPages();
}
this.page = merge<ArchivedItemQAPage>(this.page, updated);
}
private async fetchCrawl(): Promise<void> {
@ -828,6 +910,70 @@ export class ArchivedItemQA extends TailwindElement {
}
}
private async onSubmitComment(e: SubmitEvent) {
e.preventDefault();
const value = this.commentTextarea?.value;
if (!value) return;
void this.commentDialog?.hide();
try {
const { data } = await this.api.fetch<{ data: ArchivedItemPageComment }>(
`/orgs/${this.orgId}/crawls/${this.itemId}/pages/${this.itemPageId}/notes`,
this.authState!,
{
method: "POST",
body: JSON.stringify({ text: value }),
},
);
const commentForm = this.commentDialog?.querySelector("form");
if (commentForm) {
commentForm.reset();
}
const comments = [...this.page!.notes!, data];
this.page = merge<ArchivedItemQAPage>(this.page!, { notes: comments });
void this.fetchPages();
} catch (e: unknown) {
void this.commentDialog?.show();
console.debug(e);
this.notify.toast({
message: msg("Sorry, couldn't add comment at this time."),
variant: "danger",
icon: "exclamation-octagon",
});
}
}
private async deletePageComment(commentId: string): Promise<void> {
try {
await this.api.fetch(
`/orgs/${this.orgId}/crawls/${this.itemId}/pages/${this.itemPageId}/notes/delete`,
this.authState!,
{
method: "POST",
body: JSON.stringify({ delete_list: [commentId] }),
},
);
const comments = this.page!.notes!.filter(({ id }) => id !== commentId);
this.page = merge<ArchivedItemQAPage>(this.page!, { notes: comments });
void this.fetchPages();
} catch {
this.notify.toast({
message: msg("Sorry, couldn't delete comment at this time."),
variant: "danger",
icon: "exclamation-octagon",
});
}
}
private async fetchQARuns(): Promise<void> {
try {
this.finishedQARuns = (await this.getQARuns()).filter(({ state }) =>

View File

@ -42,8 +42,8 @@ export const formatNumber = (
options?: Intl.NumberFormatOptions,
) => new Intl.NumberFormat(getLocale(), options).format(number);
export const formatDate = (
date: string,
export const formatISODateString = (
date: string, // ISO string
options?: Intl.DateTimeFormatOptions,
) =>
new Date(date.endsWith("Z") ? date : `${date}Z`).toLocaleDateString(