Skip to content
Merged

wip #1540

Show file tree
Hide file tree
Changes from all 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
1 change: 1 addition & 0 deletions codex/STATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
1 change: 1 addition & 0 deletions codex/agents.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 2 additions & 0 deletions codex/tickets/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand Down
2 changes: 2 additions & 0 deletions codex/tickets/TICKET_TEMPLATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
35 changes: 35 additions & 0 deletions codex/tickets/TKT-0007.md
Original file line number Diff line number Diff line change
@@ -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.
163 changes: 110 additions & 53 deletions components/groups/page/GroupsPageListWrapper.tsx
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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<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 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 (
<GroupsList
Expand Down
46 changes: 16 additions & 30 deletions components/groups/page/list/card/actions/GroupCardEditActions.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
"use client";

import { useContext, useEffect, useRef, useState } from "react";
import { useContext, useRef, useState } from "react";
import { useClickAway, useKeyPressEvent } from "react-use";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faEllipsisVertical } from "@fortawesome/free-solid-svg-icons";
import { AuthContext } from "@/components/auth/Auth";
import { ApiGroupFull } from "@/generated/models/ApiGroupFull";
import { AnimatePresence, motion } from "framer-motion";
Expand All @@ -20,54 +22,39 @@ export default function GroupCardEditActions({
useClickAway(listRef, () => 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<string>(getEditTitle());
useEffect(() => setEditTitle(getEditTitle()), [isMyFilter]);
group.created_by?.handle?.toLowerCase();
const editTitle = isMyFilter ? "Edit" : "Clone";

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"
id="options-menu-0-button"
aria-expanded="false"
aria-haspopup="true"
aria-expanded={isOptionsOpen}
aria-haspopup="menu"
aria-controls="options-menu-0"
onClick={(e) => {
e.stopPropagation();
setIsOptionsOpen(!isOptionsOpen);
}}>
<span className="tw-sr-only">Open options</span>
<svg
<FontAwesomeIcon
icon={faEllipsisVertical}
className="tw-h-5 tw-w-5"
viewBox="0 0 20 20"
fill="currentColor"
aria-hidden="true">
<path d="M10 3a1.5 1.5 0 110 3 1.5 1.5 0 010-3zM10 8.5a1.5 1.5 0 110 3 1.5 1.5 0 010-3zM11.5 15.5a1.5 1.5 0 10-3 0 1.5 1.5 0 003 0z" />
</svg>
aria-hidden="true"
/>
</button>
<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"
id="options-menu-0"
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"
tabIndex={-1}
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
Expand All @@ -81,7 +68,6 @@ export default function GroupCardEditActions({
}}
className="tw-bg-transparent tw-w-full tw-border-none tw-block tw-px-3 tw-py-1 tw-text-sm tw-leading-6 tw-text-iron-300 hover:tw-text-iron-50 hover:tw-bg-iron-800 tw-text-left tw-transition tw-duration-300 tw-ease-out"
role="menuitem"
tabIndex={-1}
id="options-menu-0-item-0">
{editTitle}
</button>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,13 @@ export default function CommonProfileSearchItems({
<AnimatePresence mode="wait" initial={false}>
{open && (
<motion.div
className="tw-absolute tw-z-10 tw-mt-1.5 tw-w-full tw-rounded-lg tw-shadow-xl tw-bg-iron-800 tw-ring-1 tw-ring-black tw-ring-opacity-5"
className="tw-absolute tw-z-50 tw-mt-1.5 tw-w-full tw-rounded-lg tw-shadow-xl tw-bg-iron-800 tw-ring-1 tw-ring-black tw-ring-opacity-5"
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
transition={{ duration: 0.2 }}
>
<div className="tw-absolute tw-z-10 tw-overflow-hidden tw-w-full tw-rounded-md tw-bg-iron-800 tw-shadow-2xl tw-ring-1 tw-ring-white/10">
<div className="tw-absolute tw-overflow-hidden tw-w-full tw-rounded-md tw-bg-iron-800 tw-shadow-2xl tw-ring-1 tw-ring-white/10">
<div className="tw-py-1 tw-flow-root tw-overflow-x-hidden tw-overflow-y-auto">
<ul className="tw-flex tw-flex-col tw-gap-y-1 tw-px-2 tw-mx-0 tw-mb-0 tw-list-none">
{profiles.length ? (
Expand Down