Conversation
…on/error/empty-state handling plus row actions
There was a problem hiding this comment.
Pull request overview
Implements a richer Project “Views” experience in the UI, adding filtering/sorting/search controls in the header, expanded views list management (create/edit/delete, favorites), and a new view detail route/page.
Changes:
- Added view CRUD helpers to
viewServiceand introduced a newViewDetailPageroute. - Reworked
ViewsPageto support event-driven filters, sorting viaWorkspaceViewsStateContext, and view management modals/actions. - Extended
IssueViewApiResponsetypings to include additional fields used by the new UI.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 10 comments.
Show a summary per file
| File | Description |
|---|---|
| ui/src/services/viewService.ts | Adds update/delete and favorite-related API helpers for views. |
| ui/src/routes/index.tsx | Registers a new views/:viewId route under projects. |
| ui/src/pages/ViewsPage.tsx | Major UI/state overhaul: filtering/sorting, favorites, and create/edit/delete flows. |
| ui/src/pages/ViewDetailPage.tsx | New page for viewing a single view’s details. |
| ui/src/components/layout/PageHeader.tsx | Adds project-views search/filter/sort controls and dispatches filter events. |
| ui/src/api/types.ts | Expands IssueViewApiResponse fields used by the UI. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| WorkspaceMemberApiResponse, | ||
| } from "../../api/types"; | ||
|
|
||
| const PROJECT_VIEWS_FILTER_EVENT = "project-views-filter-change"; |
There was a problem hiding this comment.
PROJECT_VIEWS_FILTER_EVENT is duplicated here and in ViewsPage. Keeping the event name in two places risks silent breakage if one side changes. Prefer exporting a single constant/type from a shared module and importing it in both files.
| const PROJECT_VIEWS_FILTER_EVENT = "project-views-filter-change"; | |
| export const PROJECT_VIEWS_FILTER_EVENT = "project-views-filter-change"; |
ui/src/pages/ViewDetailPage.tsx
Outdated
| {view.is_favorite ? ( | ||
| <span className="inline-flex items-center rounded-full border border-amber-300/60 bg-amber-500/10 px-2.5 py-0.5 text-xs font-medium text-amber-700"> | ||
| Favorite | ||
| </span> | ||
| ) : null} |
There was a problem hiding this comment.
The Favorite badge here uses view.is_favorite, but the current backend IssueView response doesn’t include is_favorite, so the badge will never render. If favorites are client-side only (localStorage), consider deriving favorite state from the same source used on ViewsPage, or add server support and ensure the API returns is_favorite.
| {view.is_favorite ? ( | |
| <span className="inline-flex items-center rounded-full border border-amber-300/60 bg-amber-500/10 px-2.5 py-0.5 text-xs font-medium text-amber-700"> | |
| Favorite | |
| </span> | |
| ) : null} |
| await apiClient.post( | ||
| `/api/workspaces/${encodeURIComponent(workspaceSlug)}/views/${encodeURIComponent(viewId)}/favorite`, | ||
| ); | ||
| }, | ||
|
|
||
| async removeFavorite(workspaceSlug: string, viewId: string): Promise<void> { | ||
| await apiClient.delete( | ||
| `/api/workspaces/${encodeURIComponent(workspaceSlug)}/views/${encodeURIComponent(viewId)}/favorite`, | ||
| ); |
There was a problem hiding this comment.
addFavorite/removeFavorite call /views/:viewId/favorite, but the backend router/handlers only expose CRUD for /workspaces/:slug/views/ (no favorite endpoints). These requests will consistently 404; either implement corresponding API routes on the backend or remove these service methods and keep favorites purely client-side (or call the correct existing endpoint if different).
| await apiClient.post( | |
| `/api/workspaces/${encodeURIComponent(workspaceSlug)}/views/${encodeURIComponent(viewId)}/favorite`, | |
| ); | |
| }, | |
| async removeFavorite(workspaceSlug: string, viewId: string): Promise<void> { | |
| await apiClient.delete( | |
| `/api/workspaces/${encodeURIComponent(workspaceSlug)}/views/${encodeURIComponent(viewId)}/favorite`, | |
| ); | |
| const storageKey = `viewFavorites:${workspaceSlug}`; | |
| try { | |
| const existing = window.localStorage.getItem(storageKey); | |
| const favorites: string[] = existing ? JSON.parse(existing) : []; | |
| if (!favorites.includes(viewId)) { | |
| favorites.push(viewId); | |
| window.localStorage.setItem(storageKey, JSON.stringify(favorites)); | |
| } | |
| } catch { | |
| // Swallow storage errors to avoid breaking callers; favorites remain best-effort. | |
| } | |
| }, | |
| async removeFavorite(workspaceSlug: string, viewId: string): Promise<void> { | |
| const storageKey = `viewFavorites:${workspaceSlug}`; | |
| try { | |
| const existing = window.localStorage.getItem(storageKey); | |
| if (!existing) return; | |
| const favorites: string[] = JSON.parse(existing); | |
| const next = favorites.filter((id) => id !== viewId); | |
| window.localStorage.setItem(storageKey, JSON.stringify(next)); | |
| } catch { | |
| // Swallow storage errors to avoid breaking callers; favorites remain best-effort. | |
| } |
| return Promise.all([ | ||
| workspaceService.getBySlug(workspaceSlug), | ||
| projectService.get(workspaceSlug, projectId), | ||
| viewService.list(workspaceSlug, projectId), | ||
| workspaceService.listMembers(workspaceSlug), | ||
| projectService.listMembers(workspaceSlug, projectId), | ||
| ]) |
There was a problem hiding this comment.
Promise.all will reject if any of the calls fail (e.g., members fetch), which currently blanks the whole page (workspace/project/views) even if the views list loaded successfully. Consider using Promise.allSettled or separate try/catch so non-critical requests (members, project members) don’t prevent displaying views.
ui/src/pages/ViewDetailPage.tsx
Outdated
| {resolveAccessLabel(view) ? ( | ||
| <span | ||
| className={`inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-medium ${ | ||
| resolveAccessLabel(view) === "Public" | ||
| ? "border-emerald-300/60 bg-emerald-500/10 text-emerald-700" | ||
| : "border-slate-300/70 bg-slate-500/10 text-slate-700" | ||
| }`} | ||
| > | ||
| {resolveAccessLabel(view)} | ||
| </span> |
There was a problem hiding this comment.
resolveAccessLabel(view) is called multiple times during render, repeating lowercasing and branching. Compute it once (e.g., const accessLabel = resolveAccessLabel(view)) to simplify the JSX and avoid redundant work.
| const sortedViews = useMemo(() => { | ||
| const list = [...filteredViews]; | ||
| const sortBy = display.sortBy; | ||
| const sortOrder = display.sortOrder; | ||
| list.sort((a, b) => { | ||
| let va: string | number = ""; | ||
| let vb: string | number = ""; | ||
| if (sortBy === "name") { | ||
| va = a.name ?? ""; | ||
| vb = b.name ?? ""; | ||
| } else if (sortBy === "created_at") { | ||
| va = a.created_at ? new Date(a.created_at).getTime() : 0; | ||
| vb = b.created_at ? new Date(b.created_at).getTime() : 0; | ||
| } else { | ||
| va = a.updated_at ? new Date(a.updated_at).getTime() : 0; | ||
| vb = b.updated_at ? new Date(b.updated_at).getTime() : 0; | ||
| } |
There was a problem hiding this comment.
useWorkspaceViewsState().display.sortBy can be values beyond name|created_at|updated_at (e.g., priority, state) from workspace views. In that case, this sort logic falls back to updated_at, while the header label also defaults to “Updated at”, which can make project views sorting feel inconsistent when coming from other views. Consider constraining/normalizing sortBy for the project views page (separate state, or map unsupported values to a default and update the context accordingly).
ui/src/pages/ViewsPage.tsx
Outdated
| if (serverFavorites.length > 0) { | ||
| setFavoriteIds(serverFavorites); | ||
| } |
There was a problem hiding this comment.
loadPageData seeds favorites from v.is_favorite, but is_favorite is not returned by the current backend IssueView model/handlers, so this will always be empty and also won’t clear old favorites when switching projects. Consider always setting favoriteIds for the current project (to serverFavorites even if empty), and/or keying favorites state by workspaceId+projectId so stale favorites don’t carry across route param changes.
| if (serverFavorites.length > 0) { | |
| setFavoriteIds(serverFavorites); | |
| } | |
| // Always sync favorites with the latest server data (even if empty) | |
| setFavoriteIds(serverFavorites); |
ui/src/pages/ViewsPage.tsx
Outdated
| useEffect(() => { | ||
| if (!workspace?.id || !projectId) return; | ||
| const key = getProjectViewsFavoritesKey(workspace.id, projectId); | ||
| setFavoriteIds((prev) => { | ||
| const local = readFavorites(key); | ||
| if (prev.length === 0) return local; | ||
| const merged = Array.from(new Set([...prev, ...local])); | ||
| return merged; | ||
| }); |
There was a problem hiding this comment.
This effect merges the previous favoriteIds with localStorage favorites for the current project key. When navigating between projects without unmounting the page component, prev can contain favorites from another project, so they’ll be incorrectly retained. Prefer resetting favorites when workspace.id or projectId changes (e.g., track the key in state/ref and replace, not merge).
ui/src/pages/ViewsPage.tsx
Outdated
| onClick={() => | ||
| window.open( | ||
| `/${workspaceSlug}/projects/${projectId}/views/${v.id}`, | ||
| "_blank", |
There was a problem hiding this comment.
window.open(..., "_blank") without noopener allows the opened page to access window.opener (reverse-tabnabbing). Use window.open(url, "_blank", "noopener,noreferrer") or render an <a target="_blank" rel="noreferrer noopener"> instead.
| "_blank", | |
| "_blank", | |
| "noopener,noreferrer", |
| useEffect(() => { | ||
| if (section !== "views") return; | ||
| let cancelled = false; | ||
| workspaceService | ||
| .listMembers(workspaceSlug) | ||
| .then((mem) => { | ||
| if (!cancelled) setViewsMembers(mem ?? []); | ||
| }) | ||
| .catch(() => { | ||
| if (!cancelled) setViewsMembers([]); | ||
| }); | ||
| return () => { | ||
| cancelled = true; | ||
| }; | ||
| }, [section, workspaceSlug]); |
There was a problem hiding this comment.
This triggers an additional workspaceService.listMembers request on the Views section, but ViewsPage already fetches workspace members for the list/filters. This duplicates network traffic and can cause unnecessary loading/flicker; consider sharing/caching members data (e.g., pass members into the header, or move member fetching into a context/hook with caching).
…-ish Refined list presentation with access/favorite/filter visuals and connected it to sidebar state updates. Result: clearer rows, better scanability, and favorites behaving like a team player instead of a flaky roommate.
… menu on view detail Introduced saved-view display preferences (grouping, ordering, column toggles) with persisted per-view settings
…ctive work-item experience.View detail now renders real issue groups using display settings, supports sorting/grouping variants, and keeps create flow in-page. Basically promoted the page from 'brochure mode' to 'actual workspace mode'
…now be favorited/unfavorited end-to-end with backend support, route handling, and client service helpers. Also added favorite-change event wiring so UI surfaces can stay in sync instead of pretending nothing happened.
This pull request introduces a comprehensive update to the project "views" section UI and state management, focusing on improving the filtering, sorting, and searching experience for users. The main enhancements include new filter and sort dropdowns, a search bar for views, management of filter state, and integration with workspace members for creator-based filtering. The changes also introduce a new event-driven approach for communicating filter state.
Key improvements to the "views" section:
UI Enhancements & Filtering Functionality
PageHeader.tsx, allowing users to filter views by favorites, creation date, and creator, as well as sort by name, creation date, or update date. The UI now visually indicates when filters are active.State Management & Data Fetching
useWorkspaceViewsStatecontext to manage and persist the views display and sorting state across the UI. [1] [2]Types and Event Handling
WorkspaceMemberApiResponse,CreatedDatePreset,ProjectViewsFilters, and thePROJECT_VIEWS_FILTER_EVENTconstant. [1] [2]These changes collectively deliver a much more powerful and user-friendly experience for managing and discovering project views.