-
Notifications
You must be signed in to change notification settings - Fork 0
Publish a withheld city reply from the mail reader (#138) #37
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
4e2098d
9765753
bd48394
83a2a8d
5467f0f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,46 @@ | ||
| import { readFileSync } from "node:fs" | ||
|
|
||
| import { MutationObserver, QueryClient } from "@tanstack/react-query" | ||
| import { describe, expect, it, vi } from "vitest" | ||
|
|
||
| import type * as apiModule from "@/lib/api" | ||
| import { api } from "@/lib/api" | ||
| import { queryKeys } from "@/lib/query" | ||
|
|
||
| import { publishMailReplyOptions } from "./use-mail" | ||
|
|
||
| vi.mock("@/lib/api", async (importOriginal) => ({ | ||
| ...(await importOriginal<typeof apiModule>()), | ||
| api: { publishMailReply: vi.fn() }, | ||
| })) | ||
|
|
||
| describe("publish reply", () => { | ||
| it("publishes the one message and refreshes the thread, mail lists, reports and events", async () => { | ||
| vi.mocked(api.publishMailReply).mockResolvedValue({ publication: "published" }) | ||
| const qc = new QueryClient() | ||
| const keys = [ | ||
| queryKeys.mail.detail("t1"), | ||
| queryKeys.mail.list({}), | ||
| queryKeys.reports.detail("r1"), | ||
| queryKeys.events.detail("c1"), | ||
| ] | ||
| for (const key of keys) qc.setQueryData(key, {}) | ||
|
|
||
| const res = await new MutationObserver(qc, publishMailReplyOptions(qc)).mutate({ | ||
| id: "t1", | ||
| messageId: "m1", | ||
| }) | ||
|
|
||
| expect(res).toEqual({ publication: "published" }) | ||
| expect(api.publishMailReply).toHaveBeenCalledWith({ id: "t1", messageId: "m1" }) | ||
| for (const key of keys) expect(qc.getQueryState(key)?.isInvalidated).toBe(true) | ||
| }) | ||
|
|
||
| it("asks the operator to confirm before publishing", () => { | ||
| const source = readFileSync(new URL("./withheld-reply-note.tsx", import.meta.url), "utf8") | ||
| const confirm = source.indexOf("await confirmDialog(") | ||
| expect(confirm).toBeGreaterThan(-1) | ||
| expect(source.indexOf("if (!ok) return")).toBeGreaterThan(confirm) | ||
| expect(source.indexOf("publish.mutate(")).toBeGreaterThan(source.indexOf("if (!ok) return")) | ||
| }) | ||
| }) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,58 @@ | ||
| "use client" | ||
|
|
||
| import type { MailMessageDTO } from "@civfix/shared" | ||
|
|
||
| import { Icons } from "@/components/icons" | ||
| import { confirmDialog } from "@/components/shared/dialog" | ||
| import { usePublishMailReply } from "@/features/mail/use-mail" | ||
| import { | ||
| PUBLISH_TOAST, | ||
| publishConfirmBody, | ||
| withheldNote, | ||
| withheldReason, | ||
| } from "@/features/mail/mail-presentation" | ||
| import { errorMessage } from "@/lib/error-messages" | ||
| import { useToast } from "@/store/ui-store" | ||
|
|
||
| export function WithheldReplyNote({ | ||
| threadId, | ||
| msg, | ||
| isReport, | ||
| }: { | ||
| threadId: string | ||
| msg: MailMessageDTO | ||
| isReport: boolean | ||
| }) { | ||
| const publish = usePublishMailReply() | ||
| const toast = useToast() | ||
| const auth = withheldReason(msg.authVerdict) === "auth" | ||
|
|
||
| const onPublish = async () => { | ||
| const ok = await confirmDialog({ | ||
| title: "Publish this reply?", | ||
| body: publishConfirmBody(isReport), | ||
| confirmLabel: "Publish reply", | ||
| cancelLabel: "Cancel", | ||
| danger: auth, | ||
| }) | ||
| if (!ok) return | ||
| publish.mutate( | ||
| { id: threadId, messageId: msg.id }, | ||
| { | ||
| onSuccess: (res) => toast(PUBLISH_TOAST[res.publication]), | ||
| onError: (err) => | ||
| toast(errorMessage(err, {}, { fallback: "Couldn't publish the reply. Please try again." })), | ||
| }, | ||
| ) | ||
| } | ||
|
|
||
| return ( | ||
| <div className={`${auth ? "mail-bounce-note" : "mail-action-note"} mail-withheld-note`}> | ||
| {auth ? <Icons.AlertTriangle size={14} /> : <Icons.Shield size={14} />} | ||
| <span>{withheldNote(msg, isReport)}</span> | ||
| <button className="btn sm primary" disabled={publish.isPending} onClick={onPublish}> | ||
| <Icons.MessageSquare size={13} /> Publish reply | ||
| </button> | ||
| </div> | ||
| ) | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -2844,6 +2844,8 @@ button.prow-main { color: inherit; font: inherit; background: none; border: 0; p | |||||
| } | ||||||
| .mail-bounce-note { background: var(--bloom-50); color: var(--bloom-700); border: 1px solid color-mix(in oklab, var(--bloom) 18%, transparent); } | ||||||
| .mail-action-note { background: var(--sky-50); color: var(--sky-700); border: 1px solid color-mix(in oklab, var(--sky) 20%, transparent); } | ||||||
| .mail-withheld-note { margin-top: 8px; } | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This new rule uses a raw
Suggested change
Rule Used: # civfix review rules civfix is a live civic-tech platform that will hold government contracts. Review every PR for correctness, security and performance. Flag real defects with evidence; skip style nits that lint already covers. ## Repos - **... (source) Prompt To Fix With AIThis is a comment left during a code review.
Path: apps/admin/src/styles/admin.css
Line: 2847
Comment:
**Use the spacing token**
This new rule uses a raw `8px` margin even though the frontend styling directive requires design tokens. Use the existing `var(--space-2)` token instead. This repository requirement must be satisfied before merging.
```suggestion
.mail-withheld-note { margin-top: var(--space-2); }
```
**Rule Used:** # civfix review rules civfix is a live civic-tech platform that will hold government contracts. Review every PR for **correctness, security and performance**. Flag real defects with evidence; skip style nits that lint already covers. ## Repos - **... ([source](https://app.greptile.com/civfix/-/custom-context?memory=39a53925-3d93-4c82-980e-27b67393717d))
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time! |
||||||
| .mail-withheld-note .btn { flex-shrink: 0; margin-left: auto; } | ||||||
| .mail-reader-foot { padding: 14px 18px; border-top: 1px solid var(--border-soft); display: flex; gap: 8px; flex-wrap: wrap; } | ||||||
|
|
||||||
| /* Mail thread bubbles */ | ||||||
|
|
||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This test only checks the order of strings in the component source. It never renders the publish button or verifies that cancelling avoids publication and confirming sends the selected reply. The current interaction works, but this test can still pass if that irreversible behavior regresses, allowing accidental publication or preventing intended publication without test detection. Replace or supplement it with a rendered interaction test for both paths.
Artifacts
Evidence from the check
Command output from the check
▶ Recording of the check
Poster frame after canceling withheld reply publication
▶ Recording of the check
Poster frame after confirming withheld reply publication
Prompt To Fix With AI