diff --git a/codex/STATE.md b/codex/STATE.md index 9a9ce9bfe4..afe64d670d 100644 --- a/codex/STATE.md +++ b/codex/STATE.md @@ -10,6 +10,7 @@ This table is the single source of truth for active and historical tickets. Keep | TKT-0004 | Offload CSV mapping tools to server pipelines | Backlog | P1 | evocoder | — | 2025-10-14 | | TKT-0005 | Replace notification polling with server-driven delivery | Backlog | P1 | evocoder | — | 2025-10-14 | | TKT-0006 | Centralise media and IPFS upload orchestration | Backlog | P1 | evocoder | — | 2025-10-14 | +| TKT-0007 | Stabilize group name search input | In-Progress | P0 | simo6529 | [#1540](https://github.com/6529-Collections/6529seize-frontend/pull/1540) | 2025-10-14 | ## Usage Guidelines diff --git a/codex/agents.md b/codex/agents.md index af77fc658d..3e8291c6e1 100644 --- a/codex/agents.md +++ b/codex/agents.md @@ -6,6 +6,7 @@ The `/codex` directory centralises planning artefacts so work can be tracked and - **Open a ticket**: Create a new Markdown file under `codex/tickets/` using the ticket template. Assign a unique ID that matches the filename (`ticket-ID.md`) and choose an initial priority (P0–P3). - **Register the ticket**: Add a row to `codex/STATE.md` capturing the ticket metadata. The table is the authoritative board and must mirror the ticket front matter. +- **Set the owner handle**: Record the ticket owner's GitHub handle in both the front matter and `codex/STATE.md`. If the handle is not yet known, write `unknown` and replace it before any work begins. - **Deliver incremental work**: Keep updates small, land pull requests frequently, and cross-reference the ticket in PR descriptions. - **Maintain the board**: Whenever the status, owner, linked PRs, or priority changes, update both the ticket front matter and the `STATE.md` row. - **Close the loop**: Before marking a ticket **Done**, verify the acceptance criteria, ensure linked PRs are merged, and capture the final log entry. diff --git a/codex/tickets/README.md b/codex/tickets/README.md index 2b0aeeaa60..19728c877f 100644 --- a/codex/tickets/README.md +++ b/codex/tickets/README.md @@ -15,6 +15,8 @@ title: Upgrade authentication flow --- ``` +Set `owner` to the assignee's GitHub handle. When the handle is not available yet, use `owner: unknown` and correct it before logging work. + - Use ISO 8601 dates for `created`. - Keep keys in alphabetical order. - `status` must match the values used in `codex/STATE.md`. diff --git a/codex/tickets/TICKET_TEMPLATE.md b/codex/tickets/TICKET_TEMPLATE.md index 9a6cc4111a..3dc4ed1215 100644 --- a/codex/tickets/TICKET_TEMPLATE.md +++ b/codex/tickets/TICKET_TEMPLATE.md @@ -7,6 +7,8 @@ status: Backlog title: Concise, action-oriented title --- +Always replace `owner` with the ticket's GitHub handle. If the handle is not yet confirmed, set `owner: unknown` and update it before recording any progress. + ## Context > Summarise the problem space, business justification, and any relevant background links. diff --git a/codex/tickets/TKT-0007.md b/codex/tickets/TKT-0007.md new file mode 100644 index 0000000000..623d56d496 --- /dev/null +++ b/codex/tickets/TKT-0007.md @@ -0,0 +1,35 @@ +--- +created: 2025-10-14 +id: TKT-0007 +owner: simo6529 +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: [#1540](https://github.com/6529-Collections/6529seize-frontend/pull/1540) +- Follow-ups: None yet. + +## 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. +- 2025-10-14T09:20:31Z – Bumped identity search results z-index to keep option clicks from falling through to group cards. diff --git a/components/groups/page/GroupsPageListWrapper.tsx b/components/groups/page/GroupsPageListWrapper.tsx index 0a5acf101e..e0040f0cbb 100644 --- a/components/groups/page/GroupsPageListWrapper.tsx +++ b/components/groups/page/GroupsPageListWrapper.tsx @@ -1,14 +1,24 @@ "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"; +const CREATED_AT_LESS_THAN_SEARCH_PARAM = "created_at_less_than"; export default function GroupsPageListWrapper({ onCreateNewGroup, @@ -41,71 +51,118 @@ export default function GroupsPageListWrapper({ const identity = searchParams?.get(IDENTITY_SEARCH_PARAM); const group = searchParams?.get(GROUP_NAME_SEARCH_PARAM); - const [filters, setFilters] = useState({ - group_name: group ?? null, - author_identity: identity ?? null, - }); + const [groupDraft, setGroupDraft] = useState(group ?? null); + const lastSyncedGroupRef = useRef(group ?? null); + const filters = useMemo( + () => ({ + group_name: groupDraft, + author_identity: identity ?? null, + }), + [groupDraft, identity] + ); useEffect(() => { - setFilters({ - group_name: group ?? null, - author_identity: identity ?? null, - }); - }, [group, identity]); - - const createQueryString = ( - 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); + const nextGroup = group ?? null; + lastSyncedGroupRef.current = nextGroup; + setGroupDraft(nextGroup); + }, [group]); + + const createQueryString = useCallback( + ( + config: { + name: string; + value: string | null; + }[] + ): string => { + const params = new URLSearchParams(searchParams.toString()); + for (const { name, value } of config) { + if (value == null || value === "") { + params.delete(name); + } else { + params.set(name, value); + } } - } - return params.toString(); - }; + return params.toString(); + }, + [searchParams] + ); - const setGroupName = (value: string | null) => { - router.replace( - pathname + - "?" + - createQueryString([ - { - name: GROUP_NAME_SEARCH_PARAM, - value, - }, - ]) - ); - }; + const updateGroupNameParam = useCallback( + (value: string | null) => { + const query = createQueryString([ + { + name: GROUP_NAME_SEARCH_PARAM, + value, + }, + { + name: CREATED_AT_LESS_THAN_SEARCH_PARAM, + value: null, + }, + ]); + startTransition(() => { + router.replace(query ? `${pathname}?${query}` : pathname); + }); + }, + [createQueryString, pathname, router] + ); - const setAuthorIdentity = (value: string | null) => { - router.replace( - pathname + - "?" + - createQueryString([ - { - name: IDENTITY_SEARCH_PARAM, - value, - }, - ]) - ); - }; + useDebounce( + () => { + if (groupDraft === lastSyncedGroupRef.current) { + return; + } + lastSyncedGroupRef.current = groupDraft; + updateGroupNameParam(groupDraft); + }, + 200, + [groupDraft, updateGroupNameParam] + ); - const onMyGroups = () => { + const setGroupName = useCallback( + (value: string | null) => { + setGroupDraft(value); + if (lastSyncedGroupRef.current === value) { + return; + } + lastSyncedGroupRef.current = value; + updateGroupNameParam(value); + }, + [updateGroupNameParam] + ); + + const setAuthorIdentity = useCallback( + (value: string | null) => { + const query = createQueryString([ + { + name: IDENTITY_SEARCH_PARAM, + value, + }, + { + name: CREATED_AT_LESS_THAN_SEARCH_PARAM, + value: null, + }, + ]); + startTransition(() => { + router.replace(query ? `${pathname}?${query}` : pathname); + }); + }, + [createQueryString, pathname, router] + ); + + const onMyGroups = useCallback(() => { if (!connectedProfile?.handle) { return; } - if (activeProfileProxy?.created_by.handle) { + if (activeProfileProxy?.created_by?.handle) { setAuthorIdentity(activeProfileProxy.created_by.handle); return; } setAuthorIdentity(connectedProfile.handle); - }; + }, [ + activeProfileProxy?.created_by?.handle, + connectedProfile?.handle, + setAuthorIdentity, + ]); return ( setIsOptionsOpen(false)); useKeyPressEvent("Escape", () => setIsOptionsOpen(false)); - const [isMyFilter, setIsMyFilter] = useState( + const isMyFilter = connectedProfile?.handle?.toLowerCase() === - group.created_by?.handle?.toLowerCase() - ); - - useEffect( - () => - setIsMyFilter( - connectedProfile?.handle?.toLowerCase() === - group.created_by?.handle?.toLowerCase() - ), - [connectedProfile] - ); - - const getEditTitle = () => (isMyFilter ? "Edit" : "Clone"); - - const [editTitle, setEditTitle] = useState(getEditTitle()); - useEffect(() => setEditTitle(getEditTitle()), [isMyFilter]); + group.created_by?.handle?.toLowerCase(); + const editTitle = isMyFilter ? "Edit" : "Clone"; return ( -
+
{isOptionsOpen && ( {editTitle} diff --git a/components/utils/input/profile-search/CommonProfileSearchItems.tsx b/components/utils/input/profile-search/CommonProfileSearchItems.tsx index d9f6ec491d..7de15f68f8 100644 --- a/components/utils/input/profile-search/CommonProfileSearchItems.tsx +++ b/components/utils/input/profile-search/CommonProfileSearchItems.tsx @@ -23,13 +23,13 @@ export default function CommonProfileSearchItems({ {open && ( -
+
    {profiles.length ? (