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:
parent
9836e750c8
commit
1915274e26
@ -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,
|
||||
|
@ -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>
|
||||
|
@ -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,
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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 }) =>
|
||||
|
@ -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(
|
||||
|
Loading…
Reference in New Issue
Block a user