Skip to content
Merged

wip #1540

Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion codex/STATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ This table is the single source of truth for active and historical tickets. Keep

| Ticket ID | Title | Status | Priority | Owner | PRs | Last Updated |
|-----------|-------|--------|----------|-------|-----|--------------|
| | — | — | | | — | |
| TKT-0001 | Stabilize group name search input | In-Progress | P0 | simoluts | — | 2025-10-14 |

## Usage Guidelines

Expand Down
34 changes: 34 additions & 0 deletions codex/tickets/TKT-0001.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
---
created: 2025-10-14
id: TKT-0001
owner: simoluts
priority: P0
status: In-Progress
title: Stabilize group name search input
---

## Context

> Typing into the "By Group Name" search field freezes the browser and drops characters because each keystroke appears to trigger a full route change, causing race conditions between the URL and the input state.

## Plan

- [x] Reproduce the input lag locally and capture the triggering code path.
- [x] Identify existing debounce/search handling helpers that can be reused.
- [x] Apply the minimal fix to keep the input responsive without extra reloads.

## Acceptance

- [ ] Typing into the "By Group Name" search box remains responsive with no missed characters.
- [ ] Search results continue updating to reflect the current query.

## Links

- Primary PR: _(add when available)_
- Follow-ups: _(reference additional tickets or TODO items)_

## Log

- 2025-10-14T09:06:25Z – Ticket opened and investigation started.
- 2025-10-14T09:12:52Z – Debounced router updates to stop query-string races and keep typing responsive.
- 2025-10-14T09:14:58Z – Elevated group card action menu z-index so options render above REP/NIC buttons; project checks flagged pre-existing type errors in GroupsPageListWrapper.tsx.
120 changes: 77 additions & 43 deletions components/groups/page/GroupsPageListWrapper.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,20 @@
"use client";

import { useContext, useEffect, useState } from "react";
import {
startTransition,
useCallback,
useContext,
useEffect,
useMemo,
useRef,
useState,
} from "react";
import GroupsList from "./list/GroupsList";
import { AuthContext } from "@/components/auth/Auth";
import { useRouter } from "next/navigation";
import { usePathname, useSearchParams } from "next/navigation";
import { GroupsRequestParams } from "@/entities/IGroup";
import { useDebounce } from "react-use";

const IDENTITY_SEARCH_PARAM = "identity";
const GROUP_NAME_SEARCH_PARAM = "group";
Expand Down Expand Up @@ -41,59 +50,84 @@ export default function GroupsPageListWrapper({
const identity = searchParams?.get(IDENTITY_SEARCH_PARAM);
const group = searchParams?.get(GROUP_NAME_SEARCH_PARAM);

const [filters, setFilters] = useState<GroupsRequestParams>({
group_name: group ?? null,
author_identity: identity ?? null,
});
const [groupDraft, setGroupDraft] = useState<string | null>(group ?? null);
const lastSyncedGroupRef = useRef<string | null>(group ?? null);
const filters = useMemo<GroupsRequestParams>(
() => ({
group_name: groupDraft,
author_identity: identity ?? null,
}),
[groupDraft, identity]
);

useEffect(() => {
setFilters({
group_name: group ?? null,
author_identity: identity ?? null,
});
}, [group, identity]);
const nextGroup = group ?? null;
lastSyncedGroupRef.current = nextGroup;
setGroupDraft(nextGroup);
}, [group]);

const createQueryString = (
config: {
const createQueryString = useCallback(
(config: {
name: string;
value: string | null;
}[]
): string => {
const params = new URLSearchParams(searchParams?.toString());
for (const { name, value } of config) {
if (!value) {
params?.delete(name);
} else {
params.set(name, value);
}[]): string => {
const params = new URLSearchParams(searchParams?.toString());
for (const { name, value } of config) {
if (!value) {
params?.delete(name);
} else {
params.set(name, value);
}
}
}
return params.toString();
};
return params.toString();
},
[searchParams]
);

const updateGroupNameParam = useCallback(
(value: string | null) => {
const query = createQueryString([
{
name: GROUP_NAME_SEARCH_PARAM,
value,
},
]);
startTransition(() => {
router.replace(query ? `${pathname}?${query}` : pathname);
});
},
[createQueryString, pathname, router]
);

useDebounce(
() => {
if (groupDraft === lastSyncedGroupRef.current) {
return;
}
lastSyncedGroupRef.current = groupDraft;
updateGroupNameParam(groupDraft);
},
200,
[groupDraft, updateGroupNameParam]
);

const setGroupName = (value: string | null) => {
router.replace(
pathname +
"?" +
createQueryString([
{
name: GROUP_NAME_SEARCH_PARAM,
value,
},
])
);
setGroupDraft(value);
if (lastSyncedGroupRef.current === value) {
return;
}
lastSyncedGroupRef.current = value;
updateGroupNameParam(value);
};

const setAuthorIdentity = (value: string | null) => {
router.replace(
pathname +
"?" +
createQueryString([
{
name: IDENTITY_SEARCH_PARAM,
value,
},
])
);
const query = createQueryString([
{
name: IDENTITY_SEARCH_PARAM,
value,
},
]);
router.replace(query ? `${pathname}?${query}` : pathname);
};

const onMyGroups = () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ export default function GroupCardEditActions({
useEffect(() => setEditTitle(getEditTitle()), [isMyFilter]);

return (
<div className="tw-relative tw-z-20" ref={listRef}>
<div className="tw-relative tw-z-40" ref={listRef}>
<button
type="button"
className="tw-bg-transparent tw-h-full tw-border-0 tw-block tw-text-iron-500 hover:tw-text-iron-50 tw-transition tw-duration-300 tw-ease-out"
Expand All @@ -63,7 +63,7 @@ export default function GroupCardEditActions({
<AnimatePresence mode="wait" initial={false}>
{isOptionsOpen && (
<motion.div
className="tw-absolute tw-right-0 tw-z-10 tw-mt-2 tw-w-32 tw-origin-top-right tw-rounded-lg tw-bg-iron-900 tw-py-2 tw-shadow-lg tw-ring-1 tw-ring-white/10 tw-focus:tw-outline-none"
className="tw-absolute tw-right-0 tw-z-40 tw-mt-2 tw-w-32 tw-origin-top-right tw-rounded-lg tw-bg-iron-900 tw-py-2 tw-shadow-lg tw-ring-1 tw-ring-white/10 tw-focus:tw-outline-none"
role="menu"
aria-orientation="vertical"
aria-labelledby="options-menu-0-button"
Expand Down