Repository navigation
Hours tab on the user detail with void #47
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
5981e35
fc8d570
cbd71f4
86c6602
cf2c9a2
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,22 @@ | ||
| import { readFileSync } from "node:fs" | ||
|
|
||
| import { describe, expect, it } from "vitest" | ||
|
|
||
| const source = readFileSync(new URL("./dialog.tsx", import.meta.url), "utf8") | ||
|
|
||
| describe("dialog host", () => { | ||
| it("bounds the prompt textarea by the request's maxLength", () => { | ||
| const textarea = source.match(/<textarea[\s\S]*?\/>/)?.[0] ?? "" | ||
| expect(textarea).toContain("maxLength={current.maxLength}") | ||
| }) | ||
|
|
||
| it("drops the cancel button only for an acknowledge-only confirm", () => { | ||
| expect(source).toMatch( | ||
| /\{!\(current\.kind === "confirm" && current\.acknowledgeOnly\) && \(\s*<button className="btn ghost" onClick=\{cancel\}>/, | ||
| ) | ||
| }) | ||
|
|
||
| it("renders confirm details as a list under the body", () => { | ||
| expect(source).toMatch(/current\.kind === "confirm" && current\.details[\s\S]*?<ul className="dialog-list">/) | ||
| }) | ||
| }) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,127 @@ | ||
| "use client" | ||
|
|
||
| import * as React from "react" | ||
|
|
||
| import { Icons } from "@/components/icons" | ||
| import { LoadingState } from "@/components/shared/data-states" | ||
| import { useEventList } from "@/features/events/use-events" | ||
| import type { PickedEvent } from "@/features/users/user-hours" | ||
| import { useDebounced } from "@/hooks/use-debounced" | ||
| import { toAppError } from "@/lib/api" | ||
|
|
||
| const RESULT_LIMIT = 8 | ||
|
|
||
| function EventPickIcon() { | ||
| return ( | ||
| <span className="prow-ico hue-moss"> | ||
| <Icons.Calendar size={14} /> | ||
| </span> | ||
| ) | ||
| } | ||
|
|
||
| /** | ||
| * Search-and-pick an event that can take hours. The `completed` facet is exactly the ended, | ||
| * non-cancelled events, so an event the server would refuse for not having ended never appears. | ||
| * Once picked, the control collapses to the chosen event with a "Change" affordance. | ||
| */ | ||
| export function EventPicker({ | ||
| value, | ||
| onChange, | ||
| disabled, | ||
| }: { | ||
| value: PickedEvent | null | ||
| onChange: (event: PickedEvent | null) => void | ||
| disabled?: boolean | ||
| }) { | ||
| const [query, setQuery] = React.useState("") | ||
| const debounced = useDebounced(query, 200) | ||
| const q = debounced.trim() | ||
| const list = useEventList( | ||
| { q: q === "" ? undefined : q, filter: "completed", limit: RESULT_LIMIT }, | ||
| { keepPreviousData: true }, | ||
| ) | ||
| const results = list.data?.items ?? [] | ||
|
Comment on lines
+39
to
+43
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.
The picker displays only eight completed events and offers no next page. With nine events matching the same title and place, the ninth cannot be selected for a credit. The shared event response provides a cursor, and the backend list returns subsequent pages; expose those pages in the picker. This is a non-blocking limitation for operators crediting older events. Knowledge Base Used: Event management ArtifactsExecuted Chromium event-picker repro script
Rendered event-picker harness source
Successful picker validation run and observed API requests
▶ Unchanged picker with the ninth matching event unreachable
Baseline picker after scrolling its eight results
▶ Pagination-overlay picker selects the ninth event
Comparison picker after selecting event nine
Prompt To Fix With AIThis is a comment left during a code review.
Path: apps/admin/src/features/users/event-picker.tsx
Line: 39-43
Comment:
**Older matching events are unreachable**
The picker displays only eight completed events and offers no next page. With nine events matching the same title and place, the ninth cannot be selected for a credit. The [shared event response](https://github.com/civfix/civfix-app/blob/HEAD/packages/shared/src/schemas/admin/events.ts) provides a cursor, and the [backend list](https://github.com/civfix/civfix-backend/blob/HEAD/services/api/src/services/admin/admin-event-service.ts) returns subsequent pages; expose those pages in the picker. This is a non-blocking limitation for operators crediting older events.
**Knowledge Base Used:** [Event management](https://app.greptile.com/civfix/-/custom-context/knowledge-base/civfix/civfix-admin/-/docs/event-management.md)
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly. |
||
|
|
||
| if (value) { | ||
| return ( | ||
| <div className="user-pick picked"> | ||
| <EventPickIcon /> | ||
| <div className="user-pick-text"> | ||
| <span className="user-pick-name">{value.title}</span> | ||
| <span className="user-pick-handle"> | ||
| {value.place} · {value.when} | ||
| </span> | ||
| </div> | ||
| <button | ||
| type="button" | ||
| className="btn sm ghost" | ||
| disabled={disabled} | ||
| onClick={() => onChange(null)} | ||
| > | ||
| Change | ||
| </button> | ||
| </div> | ||
| ) | ||
| } | ||
|
|
||
| return ( | ||
| <div className="user-pick"> | ||
| <div className="searchbox user-pick-search"> | ||
| <Icons.Search size={14} /> | ||
| <input | ||
| type="text" | ||
| aria-label="Search ended events by title or place" | ||
| autoComplete="off" | ||
| placeholder="Search ended events by title or place…" | ||
| value={query} | ||
| disabled={disabled} | ||
| onChange={(e) => setQuery(e.target.value)} | ||
| /> | ||
| </div> | ||
| {list.isLoading ? ( | ||
| <div className="user-pick-results"> | ||
| <LoadingState label="Searching…" /> | ||
| </div> | ||
| ) : list.isError ? ( | ||
| <div className="user-pick-results"> | ||
| <div className="user-pick-note tone-alert">{toAppError(list.error).message}</div> | ||
| </div> | ||
| ) : results.length === 0 ? ( | ||
| <div className="user-pick-results"> | ||
| <div className="user-pick-note" role="status"> | ||
| {q === "" ? "No ended events yet." : `No ended event matches “${q}”.`} | ||
| </div> | ||
| </div> | ||
| ) : ( | ||
| <ul className="user-pick-results" aria-label="Matching events"> | ||
| {results.map((event) => ( | ||
| <li key={event.id}> | ||
| <button | ||
| type="button" | ||
| className="user-pick-row" | ||
| disabled={disabled} | ||
| onClick={() => | ||
| onChange({ | ||
| id: event.id, | ||
| title: event.title, | ||
| place: event.place, | ||
| when: event.date.abs, | ||
| }) | ||
| } | ||
| > | ||
| <EventPickIcon /> | ||
| <span className="user-pick-text"> | ||
| <span className="user-pick-name">{event.title}</span> | ||
| <span className="user-pick-handle"> | ||
| {event.place} · {event.date.abs} | ||
| </span> | ||
| </span> | ||
| {event.flagged && <span className="pill status-flag tight">flagged</span>} | ||
| </button> | ||
| </li> | ||
| ))} | ||
| </ul> | ||
| )} | ||
| </div> | ||
| ) | ||
| } | ||
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.
When a void affects issued transcripts,
acknowledgeOnlyremoves Cancel but still lets Escape, an outside click, or Close dismiss the notice. The caller does not handle dismissal, so the operator can lose the only displayed codes needed to revoke transcripts that still verify with the old total. Keep the notice open until Done or preserve its information after dismissal.Artifacts
Chromium transcript-notice validation script
Chromium validation command and observed results
▶ Guarded comparison keeps the transcript notice open
Guarded comparison with affected transcript codes visible
▶ Current source dismisses the transcript notice without Done
Current-source notice with affected transcript codes visible
Prompt To Fix With AI