Skip to content
Merged

wip #1540

Show file tree
Hide file tree
Changes from 9 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 | simoluts | [#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: _(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.
- 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