Skip to content

Groups xtdh#1948

Merged
simo6529 merged 10 commits intomainfrom
groups-xtdh
Feb 18, 2026
Merged

Groups xtdh#1948
simo6529 merged 10 commits intomainfrom
groups-xtdh

Conversation

@simo6529
Copy link
Copy Markdown
Collaborator

@simo6529 simo6529 commented Feb 18, 2026

Summary by CodeRabbit

  • New Features

    • Support for assigning an xTDH grant to a group in create/edit flows, including a grant picker in group config.
    • Grant lookup modal with filters (grantor, collection name, statuses), pagination, selection, clear/reset, accessibility, and loading/validation states.
    • Group cards now show grant status and shortened grant ID.
  • Tests

    • Added unit tests covering the grant selection modal and interactions.

Signed-off-by: Simo <simo@6529.io>
Signed-off-by: Simo <simo@6529.io>
Signed-off-by: Simo <simo@6529.io>
Signed-off-by: Simo <simo@6529.io>
Signed-off-by: Simo <simo@6529.io>
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Feb 18, 2026

📝 Walkthrough

Walkthrough

Adds xTDH grant beneficiary support end-to-end: new UI components (input, modal, row), search hook with pagination/filters, utilities for grant labels/IDs, enum and payload validation updates, unit tests, and an OpenAPI schema change.

Changes

Cohort / File(s) Summary
Grant selection UI
components/groups/page/create/config/xtdh-grant/GroupCreateXtdhGrant.tsx, components/groups/page/create/config/xtdh-grant/GroupCreateXtdhGrantModal.tsx, components/groups/page/create/config/xtdh-grant/subcomponents/GroupCreateXtdhGrantRow.tsx
New components: input with debounced validation, modal search with filters/status pills, selectable grant rows, infinite scroll, selection flow and callbacks.
Integration into group create flow
components/groups/page/create/GroupCreate.tsx, components/groups/page/create/config/GroupCreateConfig.tsx
Propagate new is_beneficiary_of_grant_id through local state and into config props (beneficiaryGrantId / setBeneficiaryGrantId).
Utilities
components/groups/page/create/config/xtdh-grant/utils.ts
New helpers: getGrantStatusLabel, isSelectableNonGrantedStatus, toShortGrantId.
Search hook
hooks/useXtdhGrantsSearchQuery.ts
New React Query infinite hook for paginated xTDH grants search with grantor/collection/status filters, sorting, and pagination helpers.
Cards / list UI
components/groups/page/list/card/GroupCardConfig.tsx, components/groups/page/list/card/GroupCardConfigs.tsx
Add XTDH_GRANT mapping and render grant status + short ID on group cards with time-based status computation.
Types & enums
entities/IGroup.ts
Add GroupDescriptionType.XTDH_GRANT enum member.
Payload validation / sanitisation
services/groups/groupMutations.ts
Sanitisation changes for identity arrays and validation update to include is_beneficiary_of_grant_id in NO_FILTERS checks.
Tests
__tests__/components/groups/page/create/config/xtdh-grant/GroupCreateXtdhGrantModal.test.tsx
New unit tests mocking portal/hook/IdentitySearch; verify initial filters, grantor selection, clearing, and accessibility presence.
API schema
openapi.yaml
Add curation_wave_id (string, nullable) to ApiSeizeSettings and include it in required list.

Sequence Diagram

sequenceDiagram
    actor User
    participant UI as GroupCreateXtdhGrant
    participant Modal as GroupCreateXtdhGrantModal
    participant Hook as useXtdhGrantsSearchQuery
    participant API as Backend (xtdh/grants)

    User->>UI: Type/paste grant ID
    UI->>UI: Debounce input
    UI->>Hook: Validate / lookup grant by ID
    Hook->>API: Fetch grant details
    API-->>Hook: Grant data / error
    Hook-->>UI: Return lookup result
    User->>UI: Open modal ("Find grant")
    UI->>Modal: open()
    User->>Modal: Set filters (grantor/collection/status)
    Modal->>Hook: Query grants (page 1)
    Hook->>API: Search grants
    API-->>Hook: Results + pagination
    Hook-->>Modal: Grants list
    User->>Modal: Click "Load more"
    Modal->>Hook: fetchNextPage
    Hook->>API: Next page
    API-->>Hook: More results
    Hook-->>Modal: Append results
    User->>Modal: Select grant
    Modal-->>UI: onGrantSelect(grantId)
    UI->>UI: setBeneficiaryGrantId(grantId)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • ragnep
  • GelatoGenesis

Poem

🐰 I hopped through code to chase a grant so bright,
Opened modals, filtered, debounced with delight,
Cards trim IDs and show a status light,
A rabbit picked a grant and bound it tight. 🥕✨

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title 'Groups xtdh' is vague and does not clearly convey the specific changes made in this substantial pull request that introduces xTDH grant support across multiple components. Use a more descriptive title such as 'Add xTDH grant support to group creation and display' to better communicate the main changes to reviewers.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch groups-xtdh

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
openapi.yaml (1)

9117-9135: ⚠️ Potential issue | 🟠 Major

Potential breaking change: curation_wave_id marked required while nullable.

If the backend doesn’t always include this field, generated clients or schema validators will fail. Either ensure every response includes curation_wave_id (explicitly null when unknown) or make it optional for backward compatibility.

💡 If optional, remove it from required
     ApiSeizeSettings:
       type: object
       required:
         - rememes_submission_tdh_threshold
         - all_drops_notifications_subscribers_limit
         - memes_wave_id
-        - curation_wave_id
       properties:
         rememes_submission_tdh_threshold:
           type: integer
           format: int64
         all_drops_notifications_subscribers_limit:
           type: integer
           format: int64
         memes_wave_id:
           type: string
           nullable: true
         curation_wave_id:
           type: string
           nullable: true
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@openapi.yaml` around lines 9117 - 9135, The schema currently lists
curation_wave_id in the required array while its property is defined nullable;
either remove "curation_wave_id" from the required list in the schema so the
field is optional, or ensure the backend always returns the property (explicitly
set to null when unknown) so it truly is required; update the OpenAPI object
where the required array and the properties for
rememes_submission_tdh_threshold, all_drops_notifications_subscribers_limit,
memes_wave_id, and curation_wave_id are declared to implement the chosen fix.
🧹 Nitpick comments (3)
services/groups/groupMutations.ts (1)

36-54: is_beneficiary_of_grant_id passes through unsanitized

The spread ...payload.group carries is_beneficiary_of_grant_id as-is. Unlike identity_addresses/excluded_identity_addresses which are explicitly null-coalesced, a whitespace-only grant ID would be forwarded to the API if createGroup is ever invoked without a prior validateGroupPayload call (which trims and rejects such values).

Consider normalizing it consistently with the rest of the sanitization:

♻️ Proposed defensive normalization
+  const grantId = payload.group.is_beneficiary_of_grant_id;
+  const normalizedGrantId =
+    typeof grantId === "string" && grantId.trim().length > 0
+      ? grantId.trim()
+      : null;

   return {
     ...payload,
     name,
     group: {
       ...payload.group,
       owns_nfts: [...payload.group.owns_nfts],
       identity_addresses:
         identityAddresses && identityAddresses.length > 0
           ? [...identityAddresses]
           : null,
       excluded_identity_addresses:
         excludedIdentityAddresses && excludedIdentityAddresses.length > 0
           ? [...excludedIdentityAddresses]
           : null,
+      is_beneficiary_of_grant_id: normalizedGrantId,
     },
   };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@services/groups/groupMutations.ts` around lines 36 - 54, The returned object
currently spreads payload.group and leaves is_beneficiary_of_grant_id
unsanitized; update the return in groupMutations.ts to explicitly normalize
payload.group.is_beneficiary_of_grant_id (e.g., trim and set to null if
empty/whitespace) alongside the existing
identity_addresses/excluded_identity_addresses handling so that
is_beneficiary_of_grant_id is never forwarded as a whitespace-only string by the
function that builds the payload (refer to the returned object and payload.group
symbols).
components/groups/page/list/card/GroupCardConfigs.tsx (1)

176-201: Duplicated grant-status logic — consider reusing getGrantStatusLabel from utils.ts.

The local getGrantStatusLabel reimplements the same ACTIVE/ENDED/SCHEDULED/REVOKED derivation that utils.ts already exports, but with a different signature (grant object + now vs. destructured {status, validFrom, validTo}). Keeping two copies invites drift.

You could replace this with:

import { getGrantStatusLabel } from "../create/config/xtdh-grant/utils";
// …
const statusLabel = getGrantStatusLabel({
  status: grant.status,
  validFrom: grant.valid_from,
  validTo: grant.valid_to,
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/groups/page/list/card/GroupCardConfigs.tsx` around lines 176 -
201, The local getGrantStatusLabel duplicates logic already exported from utils;
remove this local implementation and import the existing getGrantStatusLabel
(same name) from the xtdh grant utils, then call it with the mapped fields from
the ApiGroupDescription grant object (pass { status: grant.status, validFrom:
grant.valid_from, validTo: grant.valid_to }); preserve the current null behavior
by returning null when grant or grant.status is undefined before calling the
imported helper.
components/groups/page/create/config/xtdh-grant/GroupCreateXtdhGrant.tsx (1)

165-165: Nitpick: use the Unicode arrow instead of the two-character ASCII sequence "->".

-            {"->"}{" "}
+            {"→"}{" "}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/groups/page/create/config/xtdh-grant/GroupCreateXtdhGrant.tsx` at
line 165, Replace the ASCII arrow literal in the GroupCreateXtdhGrant
component's JSX with the Unicode arrow character; locate the JSX fragment that
currently renders {"->"}{" "} and change it to render "→" (and keep any required
spacing, e.g., "→ " or {' '} after it) so the arrow displays as a single Unicode
character instead of two ASCII chars.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@components/groups/page/create/config/xtdh-grant/GroupCreateXtdhGrant.tsx`:
- Around line 61-63: The derived UI flags and grant-detail rendering must be
gated so they only reflect the debounced lookup state when it matches the
current input: introduce a freshness check (e.g., const isLookupFresh =
lookupGrantId === normalizedGrantId) and use it to compute showLookupError and
showNonGrantedWarning (e.g., showLookupError = isLookupFresh &&
Boolean(lookupGrantId && isError); showNonGrantedWarning = isLookupFresh &&
grant?.status !== undefined && isSelectableNonGrantedStatus(grant.status)), and
only render the grant details panel when isLookupFresh && !!grant so the error
copy and grant details never display stale lookupGrantId while normalizedGrantId
has already changed.

In
`@components/groups/page/create/config/xtdh-grant/GroupCreateXtdhGrantModal.tsx`:
- Line 104: The Escape key handler registered via useKeyPressEvent in
GroupCreateXtdhGrantModal is active even when the modal is closed because the
component is always mounted; update the code so the hook only registers while
the modal is open (guard by the isOpen prop/state) — e.g., conditionally call
useKeyPressEvent("Escape", onClose) only when isOpen is true or wrap the
registration in an effect that adds the listener when isOpen becomes true and
removes it when false; modify GroupCreateXtdhGrantModal around the existing
useKeyPressEvent call to ensure the handler is detached when isOpen is false.

In `@components/groups/page/list/card/GroupCardConfigs.tsx`:
- Line 32: NOW_MS_AT_MODULE_INIT is captured once at module load so timestamps
become stale; update getGrantStatusLabel (or any callers in GroupCardConfigs) to
use a fresh timestamp by replacing NOW_MS_AT_MODULE_INIT with Date.now() inline
or by computing const nowMs = Date.now() at the start of the render or inside
getGrantStatusLabel so each evaluation uses a current time; ensure all
references (including the other occurrence noted) are changed to use the new
nowMs/Date.now() approach.
- Around line 168-174: The local toShortGrantId definition in
GroupCardConfigs.tsx duplicates the utility version with different truncation
rules; remove the local function and instead import the canonical toShortGrantId
from utils.ts (the exported function named toShortGrantId) and update any local
references to use that import so grant IDs render consistently across
GroupCardConfigs (and the grant modal).

---

Outside diff comments:
In `@openapi.yaml`:
- Around line 9117-9135: The schema currently lists curation_wave_id in the
required array while its property is defined nullable; either remove
"curation_wave_id" from the required list in the schema so the field is
optional, or ensure the backend always returns the property (explicitly set to
null when unknown) so it truly is required; update the OpenAPI object where the
required array and the properties for rememes_submission_tdh_threshold,
all_drops_notifications_subscribers_limit, memes_wave_id, and curation_wave_id
are declared to implement the chosen fix.

---

Nitpick comments:
In `@components/groups/page/create/config/xtdh-grant/GroupCreateXtdhGrant.tsx`:
- Line 165: Replace the ASCII arrow literal in the GroupCreateXtdhGrant
component's JSX with the Unicode arrow character; locate the JSX fragment that
currently renders {"->"}{" "} and change it to render "→" (and keep any required
spacing, e.g., "→ " or {' '} after it) so the arrow displays as a single Unicode
character instead of two ASCII chars.

In `@components/groups/page/list/card/GroupCardConfigs.tsx`:
- Around line 176-201: The local getGrantStatusLabel duplicates logic already
exported from utils; remove this local implementation and import the existing
getGrantStatusLabel (same name) from the xtdh grant utils, then call it with the
mapped fields from the ApiGroupDescription grant object (pass { status:
grant.status, validFrom: grant.valid_from, validTo: grant.valid_to }); preserve
the current null behavior by returning null when grant or grant.status is
undefined before calling the imported helper.

In `@services/groups/groupMutations.ts`:
- Around line 36-54: The returned object currently spreads payload.group and
leaves is_beneficiary_of_grant_id unsanitized; update the return in
groupMutations.ts to explicitly normalize
payload.group.is_beneficiary_of_grant_id (e.g., trim and set to null if
empty/whitespace) alongside the existing
identity_addresses/excluded_identity_addresses handling so that
is_beneficiary_of_grant_id is never forwarded as a whitespace-only string by the
function that builds the payload (refer to the returned object and payload.group
symbols).

Comment thread components/groups/page/create/config/xtdh-grant/GroupCreateXtdhGrant.tsx Outdated
Comment thread components/groups/page/create/config/xtdh-grant/GroupCreateXtdhGrantModal.tsx Outdated
Comment thread components/groups/page/list/card/GroupCardConfigs.tsx Outdated
Comment thread components/groups/page/list/card/GroupCardConfigs.tsx Outdated
Signed-off-by: Simo <simo@6529.io>
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (3)
components/groups/page/list/card/GroupCardConfigs.tsx (1)

168-189: Local getGrantStatusLabel duplicates the exported utility from utils.ts — and uses a stale clock.

utils.ts already exports getGrantStatusLabel (with Date.now() inline), identical to the now-resolved toShortGrantId duplication. The local copy uses the stale nowMs snapshot captured at component mount and has an unreachable GRANT_STATUS_LABELS[ApiXTdhGrantStatus.Granted] entry (the Granted branch always returns early, preventing the fallback lookup).

Importing the utility and wrapping it with a thin adapter eliminates all three problems at once.

♻️ Proposed refactor

Update the import line (line 10):

-import { toShortGrantId } from "@/components/groups/page/create/config/xtdh-grant/utils";
+import { toShortGrantId, getGrantStatusLabel as computeGrantStatusLabel } from "@/components/groups/page/create/config/xtdh-grant/utils";

Remove the now-unused state and constant:

-  const [nowMs] = useState<number>(() => Date.now());
-const GRANT_STATUS_LABELS: Record<ApiXTdhGrantStatus, string> = {
-  [ApiXTdhGrantStatus.Pending]: "PENDING",
-  [ApiXTdhGrantStatus.Failed]: "FAILED",
-  [ApiXTdhGrantStatus.Disabled]: "REVOKED",
-  [ApiXTdhGrantStatus.Granted]: "GRANTED",
-};

Replace the local implementation with an adapter:

-  const getGrantStatusLabel = (
-    grant: ApiGroupDescription["is_beneficiary_of_grant"]
-  ): string | null => {
-    if (grant?.status === undefined) {
-      return null;
-    }
-    if (grant.status === ApiXTdhGrantStatus.Granted) {
-      const from = grant.valid_from ?? null;
-      const to = grant.valid_to ?? null;
-      if (typeof to === "number" && to > 0 && to < nowMs) {
-        return "ENDED";
-      }
-      if (typeof from === "number" && from > nowMs) {
-        return "SCHEDULED";
-      }
-      return "ACTIVE";
-    }
-    return GRANT_STATUS_LABELS[grant.status];
-  };
+  const getGrantStatusLabel = (
+    grant: ApiGroupDescription["is_beneficiary_of_grant"]
+  ): string | null => {
+    if (grant?.status === undefined) {
+      return null;
+    }
+    return computeGrantStatusLabel({
+      status: grant.status,
+      validFrom: grant.valid_from,
+      validTo: grant.valid_to,
+    });
+  };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/groups/page/list/card/GroupCardConfigs.tsx` around lines 168 -
189, Local getGrantStatusLabel duplicates the exported utility in utils.ts,
relies on a stale nowMs snapshot, and leaves
GRANT_STATUS_LABELS[ApiXTdhGrantStatus.Granted] unreachable; fix by removing the
local implementation and the nowMs state/constant, import getGrantStatusLabel
from utils.ts (the same util that toShortGrantId uses), and replace the local
function with a thin adapter that forwards the grant argument to the imported
getGrantStatusLabel (so it uses Date.now() inline and the single source of
truth).
components/groups/page/create/config/xtdh-grant/subcomponents/GroupCreateXtdhGrantRow.tsx (1)

58-69: Long single-line template literal reduces readability.

The class string on line 69 is quite long and combines multiple concerns (list-item reset, base styles, focus ring, state, custom). Consider breaking it into an array and joining, or using a helper like clsx/cn.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@components/groups/page/create/config/xtdh-grant/subcomponents/GroupCreateXtdhGrantRow.tsx`
around lines 58 - 69, The getRowClasses function builds a very long single-line
template literal; refactor it to build an array of class segments (e.g.,
list-reset, base styles, conditional focus ring when interactive, stateClasses,
and className) and join them with a space, or replace the manual join with a
utility like clsx/cn to improve readability and maintainability; update the
implementation inside getRowClasses to return the joined string while preserving
the same conditional logic for asListItem and interactive and keeping
stateClasses and className included.
components/groups/page/create/config/xtdh-grant/GroupCreateXtdhGrantModal.tsx (1)

249-266: Pagination errors are silently swallowed when existing grants are displayed.

The error state (line 249) only renders when !grants.length. If fetchNextPage fails, isError becomes true but existing grants remain visible, and the user sees no feedback about the failure. Consider showing a smaller inline error near the "Load more" button when isError && grants.length > 0.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@components/groups/page/create/config/xtdh-grant/GroupCreateXtdhGrantModal.tsx`
around lines 249 - 266, In GroupCreateXtdhGrantModal, the current error block
only shows when !grants.length, so pagination failures (isError true while
grants.length > 0) are swallowed; update the render logic around the "Load more"
/ fetchNextPage UI to detect isError && grants.length > 0 and display a compact
inline error message (e.g., "Unable to load more grants") with a retry control
that calls fetchNextPage (or refetch) and preserves existing grants; reference
the isError, grants, fetchNextPage and the existing Retry handler so the error
UI appears next to the Load more button and does not replace the current grant
list.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@components/groups/page/create/config/xtdh-grant/GroupCreateXtdhGrantModal.tsx`:
- Around line 61-69: The guard ref skipInitialOutsideClick used to ignore the
opening click isn't reset when the modal re-opens: update the useEffect that
currently schedules skipInitialOutsideClick.current = false to also set
skipInitialOutsideClick.current = true when isOpen becomes true (i.e., on effect
run when isOpen is true) so the initial click is ignored each time; ensure the
effect still clears the timeout on cleanup and keep references to
skipInitialOutsideClick, isOpen, and the useClickAway handler intact.
- Around line 136-139: The modal container in GroupCreateXtdhGrantModal.tsx
lacks ARIA dialog attributes; update the div that uses modalRef to include
role="dialog" and aria-modal="true" and add aria-labelledby pointing to the
modal title's id (e.g., give the title element a unique id like
"grant-modal-title"); ensure the title element (the heading inside this
component) has that exact id so screen readers announce the dialog correctly.

---

Duplicate comments:
In `@components/groups/page/list/card/GroupCardConfigs.tsx`:
- Line 39: The component currently captures a stale timestamp via const [nowMs]
= useState<number>(() => Date.now()), which causes getGrantStatusLabel to use an
outdated time; remove the useState nowMs usage and call Date.now() inline inside
getGrantStatusLabel (or call the existing time-aware helper from utils.ts) so
grant status is computed against the current time; update any references to
nowMs to use Date.now() or the utility function and remove the nowMs state
declaration.

---

Nitpick comments:
In
`@components/groups/page/create/config/xtdh-grant/GroupCreateXtdhGrantModal.tsx`:
- Around line 249-266: In GroupCreateXtdhGrantModal, the current error block
only shows when !grants.length, so pagination failures (isError true while
grants.length > 0) are swallowed; update the render logic around the "Load more"
/ fetchNextPage UI to detect isError && grants.length > 0 and display a compact
inline error message (e.g., "Unable to load more grants") with a retry control
that calls fetchNextPage (or refetch) and preserves existing grants; reference
the isError, grants, fetchNextPage and the existing Retry handler so the error
UI appears next to the Load more button and does not replace the current grant
list.

In
`@components/groups/page/create/config/xtdh-grant/subcomponents/GroupCreateXtdhGrantRow.tsx`:
- Around line 58-69: The getRowClasses function builds a very long single-line
template literal; refactor it to build an array of class segments (e.g.,
list-reset, base styles, conditional focus ring when interactive, stateClasses,
and className) and join them with a space, or replace the manual join with a
utility like clsx/cn to improve readability and maintainability; update the
implementation inside getRowClasses to return the joined string while preserving
the same conditional logic for asListItem and interactive and keeping
stateClasses and className included.

In `@components/groups/page/list/card/GroupCardConfigs.tsx`:
- Around line 168-189: Local getGrantStatusLabel duplicates the exported utility
in utils.ts, relies on a stale nowMs snapshot, and leaves
GRANT_STATUS_LABELS[ApiXTdhGrantStatus.Granted] unreachable; fix by removing the
local implementation and the nowMs state/constant, import getGrantStatusLabel
from utils.ts (the same util that toShortGrantId uses), and replace the local
function with a thin adapter that forwards the grant argument to the imported
getGrantStatusLabel (so it uses Date.now() inline and the single source of
truth).

Signed-off-by: Simo <simo@6529.io>
Signed-off-by: Simo <simo@6529.io>
@sonarqubecloud
Copy link
Copy Markdown

sonarqubecloud Bot commented Feb 18, 2026

Quality Gate Passed Quality Gate passed

Issues
0 New issues
3 Accepted issues

Measures
0 Security Hotspots
No data about Coverage
2.1% Duplication on New Code

See analysis details on SonarQube Cloud

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
hooks/useXtdhGrantsSearchQuery.ts (1)

78-81: normalizeTextFilter contains a redundant branch.

normalized is already "" when the input is null/undefined/whitespace-only, so the ternary on Line 80 always returns normalized as-is.

✏️ Simplification
 const normalizeTextFilter = (value?: string | null): string => {
-  const normalized = value?.trim() ?? "";
-  return normalized.length ? normalized : "";
+  return value?.trim() ?? "";
 };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@hooks/useXtdhGrantsSearchQuery.ts` around lines 78 - 81, The function
normalizeTextFilter contains a redundant ternary that returns normalized or ""
even though normalized is already "" for null/undefined/whitespace-only;
simplify by returning the trimmed-or-empty value directly (e.g., compute trimmed
via value?.trim() and return trimmed ?? "" or return (value?.trim() || "")).
Update normalizeTextFilter to remove the length check/ternary and return the
normalized string directly.
components/groups/page/create/config/xtdh-grant/GroupCreateXtdhGrantModal.tsx (1)

135-146: Modal lacks focus management on open.

When isOpen becomes true, focus is not programmatically moved into the modal, and there is no focus trap to prevent tabbing out to elements behind the backdrop. Keyboard and screen-reader users may not realize the dialog opened. Consider auto-focusing the first interactive element (e.g., the Grantor input) when the modal mounts and trapping focus within the dialog.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@components/groups/page/create/config/xtdh-grant/GroupCreateXtdhGrantModal.tsx`
around lines 135 - 146, When isOpen becomes true, programmatically move focus
into the dialog and trap it: capture the previously focused element, set focus
to the first interactive control inside the modal (e.g., the Grantor input—use a
dedicated ref like grantorRef or target the element with id "grantor-input")
when modalRef mounts, and implement a focus-trap that keeps Tab/Shift+Tab
cycling within modalRef; on close restore focus to the previously focused
element and remove any event listeners. Ensure focus-trap and focus management
are wired to the existing modalRef and isOpen props and cleaned up on
unmount/close.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In
`@components/groups/page/create/config/xtdh-grant/GroupCreateXtdhGrantModal.tsx`:
- Around line 140-146: The modal now includes the ARIA attributes
(role="dialog", aria-modal="true", aria-labelledby="grant-modal-title"), so
verify and, if missing, add a matching title element with id "grant-modal-title"
inside the GroupCreateXtdhGrantModal component so the aria-labelledby reference
is valid; also ensure the container uses the existing modalRef and retains
role/aria-modal attributes to keep the dialog accessible.
- Around line 88-104: The effect correctly scopes the global keydown listener
using useEffect/handleEscape tied to isOpen, but because onClose is a dependency
it may cause the listener to be re-attached on every parent render; update the
parent to memoize the onClose callback with useCallback (or provide a stable
ref) so the useEffect in GroupCreateXtdhGrantModal (which calls
globalThis.addEventListener/globalThis.removeEventListener and uses
handleEscape) only re-attaches when actually needed, keeping isOpen and onClose
stable.
- Around line 61-73: The useEffect guard for resetting skipInitialOutsideClick
has been fixed so that when isOpen is true it sets
skipInitialOutsideClick.current = true on every open and the timeout clears it
and is cleaned up; no code change required—keep the useEffect as implemented,
preserving skipInitialOutsideClick, isOpen, the timeout assignment and
globalThis.clearTimeout in the return cleanup.

---

Nitpick comments:
In
`@components/groups/page/create/config/xtdh-grant/GroupCreateXtdhGrantModal.tsx`:
- Around line 135-146: When isOpen becomes true, programmatically move focus
into the dialog and trap it: capture the previously focused element, set focus
to the first interactive control inside the modal (e.g., the Grantor input—use a
dedicated ref like grantorRef or target the element with id "grantor-input")
when modalRef mounts, and implement a focus-trap that keeps Tab/Shift+Tab
cycling within modalRef; on close restore focus to the previously focused
element and remove any event listeners. Ensure focus-trap and focus management
are wired to the existing modalRef and isOpen props and cleaned up on
unmount/close.

In `@hooks/useXtdhGrantsSearchQuery.ts`:
- Around line 78-81: The function normalizeTextFilter contains a redundant
ternary that returns normalized or "" even though normalized is already "" for
null/undefined/whitespace-only; simplify by returning the trimmed-or-empty value
directly (e.g., compute trimmed via value?.trim() and return trimmed ?? "" or
return (value?.trim() || "")). Update normalizeTextFilter to remove the length
check/ternary and return the normalized string directly.

@simo6529 simo6529 merged commit 35e2113 into main Feb 18, 2026
7 checks passed
@simo6529 simo6529 deleted the groups-xtdh branch February 18, 2026 16:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants