diff --git a/apps/web/src/components/SidebarV2.tsx b/apps/web/src/components/SidebarV2.tsx index a0787e3a496..634b5f6d676 100644 --- a/apps/web/src/components/SidebarV2.tsx +++ b/apps/web/src/components/SidebarV2.tsx @@ -12,7 +12,11 @@ import { scopeThreadRef, scopedThreadKey, } from "@t3tools/client-runtime/environment"; -import type { ScopedThreadRef, SidebarProjectGroupingMode } from "@t3tools/contracts"; +import type { + EnvironmentId, + ScopedThreadRef, + SidebarProjectGroupingMode, +} from "@t3tools/contracts"; import { AlarmClockIcon, AlarmClockOffIcon, @@ -27,6 +31,7 @@ import { FolderPlusIcon, GitBranchIcon, EllipsisIcon, + ListFilterIcon, MessageSquareIcon, PlusIcon, SearchIcon, @@ -155,7 +160,32 @@ import { } from "./ui/dialog"; import { Input } from "./ui/input"; import { Kbd } from "./ui/kbd"; -import { Menu, MenuPopup, MenuRadioGroup, MenuRadioItem, MenuTrigger } from "./ui/menu"; +import { + Menu, + MenuCheckboxItem, + MenuGroup, + MenuPopup, + MenuRadioGroup, + MenuRadioItem, + MenuSeparator, + MenuTrigger, +} from "./ui/menu"; +import { useLocalStorage } from "~/hooks/useLocalStorage"; +import { + DEFAULT_SIDEBAR_V2_SETTLED_RECENCY_HEADERS, + DEFAULT_SIDEBAR_V2_SETTLED_SHELF_EXPANDED, + EMPTY_LIST_ENVIRONMENT_FILTER, + LIST_ENVIRONMENT_FILTER_STORAGE_KEY, + ListEnvironmentFilterSchema, + ListHideSettledSchema, + SIDEBAR_V2_SETTLED_RECENCY_HEADERS_STORAGE_KEY, + SIDEBAR_V2_SETTLED_SHELF_EXPANDED_STORAGE_KEY, + isAllEnvironmentsSelected, + isEnvironmentSelected, + matchesEnvironmentFilter, + resolveSelectedEnvironmentIds, + toggleEnvironmentId, +} from "./listEnvironmentFilter"; import { Select, SelectItem, SelectPopup, SelectTrigger, SelectValue } from "./ui/select"; import { SidebarContent, SidebarGroup, SidebarMenuButton, useSidebar } from "./ui/sidebar"; import { SidebarChromeFooter, SidebarChromeHeader } from "./sidebar/SidebarChrome"; @@ -1130,6 +1160,21 @@ export default function SidebarV2() { null, ); const [projectScopeMenuOpen, setProjectScopeMenuOpen] = useState(false); + const [storedEnvironmentFilter, setStoredEnvironmentFilter] = useLocalStorage( + LIST_ENVIRONMENT_FILTER_STORAGE_KEY, + EMPTY_LIST_ENVIRONMENT_FILTER, + ListEnvironmentFilterSchema, + ); + const [settledRecencyHeadersEnabled, setSettledRecencyHeadersEnabled] = useLocalStorage( + SIDEBAR_V2_SETTLED_RECENCY_HEADERS_STORAGE_KEY, + DEFAULT_SIDEBAR_V2_SETTLED_RECENCY_HEADERS, + ListHideSettledSchema, + ); + const [settledShelfExpanded, setSettledShelfExpanded] = useLocalStorage( + SIDEBAR_V2_SETTLED_SHELF_EXPANDED_STORAGE_KEY, + DEFAULT_SIDEBAR_V2_SETTLED_SHELF_EXPANDED, + ListHideSettledSchema, + ); const newThreadContext = useHandleNewThread(); const openAddProjectCommandPalette = useCallback( () => openCommandPalette({ open: "add-project" }), @@ -1169,6 +1214,22 @@ export default function SidebarV2() { ), [environments], ); + const availableEnvironmentIds = useMemo( + () => new Set(environments.map((environment) => environment.environmentId)), + [environments], + ); + const selectedEnvironmentIds = useMemo( + () => + resolveSelectedEnvironmentIds( + storedEnvironmentFilter as readonly EnvironmentId[], + availableEnvironmentIds, + ), + [availableEnvironmentIds, storedEnvironmentFilter], + ); + const listOptionsActive = + !isAllEnvironmentsSelected(selectedEnvironmentIds) || + settledRecencyHeadersEnabled !== DEFAULT_SIDEBAR_V2_SETTLED_RECENCY_HEADERS || + settledShelfExpanded !== DEFAULT_SIDEBAR_V2_SETTLED_SHELF_EXPANDED; const orderedProjects = useMemo( () => orderItemsByPreferredIds({ @@ -1466,6 +1527,7 @@ export default function SidebarV2() { const visible = threads.filter( (thread) => thread.archivedAt === null && + matchesEnvironmentFilter(thread.environmentId, selectedEnvironmentIds) && (scopedProjectKeys === null || scopedProjectKeys.has(`${thread.environmentId}:${thread.projectId}`)), ); @@ -1513,6 +1575,7 @@ export default function SidebarV2() { changeRequestStateByKey, nowMinute, scopedProjectKeys, + selectedEnvironmentIds, serverConfigs, snoozeWakeTick, threads, @@ -1541,7 +1604,7 @@ export default function SidebarV2() { // filter context changes so a scope/search flip never inherits a deep // page state. const [settledVisibleCount, setSettledVisibleCount] = useState(SETTLED_TAIL_INITIAL_COUNT); - const settledResetKey = projectScopeKey ?? "all"; + const settledResetKey = `${projectScopeKey ?? "all"}:${selectedEnvironmentIds.join(",")}`; const lastSettledResetKeyRef = useRef(settledResetKey); if (lastSettledResetKeyRef.current !== settledResetKey) { lastSettledResetKeyRef.current = settledResetKey; @@ -1569,8 +1632,10 @@ export default function SidebarV2() { () => setSettledVisibleCount((count) => count + SETTLED_TAIL_PAGE_COUNT), [], ); - const [settledShelfExpanded, setSettledShelfExpanded] = useState(true); - const toggleSettledShelf = useCallback(() => setSettledShelfExpanded((value) => !value), []); + const toggleSettledShelf = useCallback( + () => setSettledShelfExpanded((value) => !value), + [setSettledShelfExpanded], + ); const renderedSettledThreads = useMemo(() => { if (settledShelfExpanded) return visibleSettledThreads; if (routeThreadKey === null) return []; @@ -1583,11 +1648,16 @@ export default function SidebarV2() { // Date headers on the settled tail only (lifecycle spine stays intact). // Recompute when the minute clock advances so Last Hour / Earlier Today - // boundaries stay honest. Single-bucket pages omit headers. + // boundaries stay honest. Single-bucket pages omit headers. View menu can + // disable headers entirely without changing settle order. const settledRecencyLayout = useMemo(() => { void nowMinute; - return groupSettledThreadsByRecencyForSidebarV2(renderedSettledThreads, new Date()); - }, [nowMinute, renderedSettledThreads]); + const layout = groupSettledThreadsByRecencyForSidebarV2(renderedSettledThreads, new Date()); + if (!settledRecencyHeadersEnabled) { + return { groups: layout.groups, showHeaders: false }; + } + return layout; + }, [nowMinute, renderedSettledThreads, settledRecencyHeadersEnabled]); // The snoozed shelf is collapsed by default: out of the way, never gone. // Collapsed threads don't render (and so don't participate in jump @@ -2392,8 +2462,8 @@ export default function SidebarV2() { - {projectGroups.length > 0 ? ( -
+
+ {projectGroups.length > 0 ? ( + ) : ( +
+ )} + + + + } + /> + } + > + + {listOptionsActive ? ( + + ) : null} + + View & filters + + + +
+ Settled shelf +
+ + setSettledRecencyHeadersEnabled(checked === true) + } + > + Date headers on settled + + setSettledShelfExpanded(checked === true)} + > + Expand settled shelf + +
+ {environments.length > 1 ? ( + <> + + +
+ Environment +
+ setStoredEnvironmentFilter([])} + > + All environments + + {environments.map((environment) => ( + { + setStoredEnvironmentFilter([ + ...toggleEnvironmentId( + selectedEnvironmentIds, + environment.environmentId, + ), + ]); + }} + > + {environment.label} + + ))} +
+ + ) : null} +
+
+ {projectGroups.length > 0 ? ( New project -
- ) : null} + ) : null} +
} > diff --git a/apps/web/src/components/listEnvironmentFilter.ts b/apps/web/src/components/listEnvironmentFilter.ts index bfb78ad3f46..5ba09e56d69 100644 --- a/apps/web/src/components/listEnvironmentFilter.ts +++ b/apps/web/src/components/listEnvironmentFilter.ts @@ -101,6 +101,15 @@ export const LIST_HIDE_SETTLED_PROJECTS_STORAGE_KEY = "t3code:list:hide-settled- export const DEFAULT_HIDE_SETTLED_RECENT = true; export const DEFAULT_HIDE_SETTLED_PROJECTS = false; +/** Sidebar V2: show Last Hour / Yesterday / … under Settled when multi-bucket. */ +export const SIDEBAR_V2_SETTLED_RECENCY_HEADERS_STORAGE_KEY = + "t3code:sidebar-v2:settled-recency-headers:v1"; +export const DEFAULT_SIDEBAR_V2_SETTLED_RECENCY_HEADERS = true; +/** Sidebar V2: settled shelf expanded vs collapsed (persists last toggle). */ +export const SIDEBAR_V2_SETTLED_SHELF_EXPANDED_STORAGE_KEY = + "t3code:sidebar-v2:settled-shelf-expanded:v1"; +export const DEFAULT_SIDEBAR_V2_SETTLED_SHELF_EXPANDED = true; + /** Persisted env multi-select; empty array means all environments. */ export const ListEnvironmentFilterSchema = Schema.Array(Schema.String); export type ListEnvironmentFilterStored = typeof ListEnvironmentFilterSchema.Type;