Skip to content

33 implement project views page#36

Draft
nazarli-shabnam wants to merge 10 commits intomainfrom
33-implement-project-views-page
Draft

33 implement project views page#36
nazarli-shabnam wants to merge 10 commits intomainfrom
33-implement-project-views-page

Conversation

@nazarli-shabnam
Copy link
Member

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

  • Added a dedicated search bar, filter dropdown, and sort dropdown to the "views" section in 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.
  • Implemented local state for all view filters (search query, favorites, created date, created by) and wired them to update the UI and dispatch filter changes via a custom event. [1] [2]

State Management & Data Fetching

  • Integrated useWorkspaceViewsState context to manage and persist the views display and sorting state across the UI. [1] [2]
  • Fetched workspace members on entering the "views" section to enable filtering by creator.

Types and Event Handling

  • Added new types and constants to support filter state and event-driven updates, including WorkspaceMemberApiResponse, CreatedDatePreset, ProjectViewsFilters, and the PROJECT_VIEWS_FILTER_EVENT constant. [1] [2]
  • Implemented a filter event dispatcher to broadcast filter changes to other components/pages. [1] [2]

These changes collectively deliver a much more powerful and user-friendly experience for managing and discovering project views.

@nazarli-shabnam nazarli-shabnam added this to the Deadline milestone Mar 19, 2026
@nazarli-shabnam nazarli-shabnam self-assigned this Mar 19, 2026
@nazarli-shabnam nazarli-shabnam linked an issue Mar 19, 2026 that may be closed by this pull request
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 viewService and introduced a new ViewDetailPage route.
  • Reworked ViewsPage to support event-driven filters, sorting via WorkspaceViewsStateContext, and view management modals/actions.
  • Extended IssueViewApiResponse typings 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";
Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
const PROJECT_VIEWS_FILTER_EVENT = "project-views-filter-change";
export const PROJECT_VIEWS_FILTER_EVENT = "project-views-filter-change";

Copilot uses AI. Check for mistakes.
Comment on lines +109 to +113
{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}
Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
{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}

Copilot uses AI. Check for mistakes.
Comment on lines +66 to +74
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`,
);
Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Suggested change
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.
}

Copilot uses AI. Check for mistakes.
Comment on lines +117 to 123
return Promise.all([
workspaceService.getBySlug(workspaceSlug),
projectService.get(workspaceSlug, projectId),
viewService.list(workspaceSlug, projectId),
workspaceService.listMembers(workspaceSlug),
projectService.listMembers(workspaceSlug, projectId),
])
Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +92 to +101
{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>
Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +258 to +274
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;
}
Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copilot uses AI. Check for mistakes.
Comment on lines 133 to 135
if (serverFavorites.length > 0) {
setFavoriteIds(serverFavorites);
}
Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
if (serverFavorites.length > 0) {
setFavoriteIds(serverFavorites);
}
// Always sync favorites with the latest server data (even if empty)
setFavoriteIds(serverFavorites);

Copilot uses AI. Check for mistakes.
Comment on lines +164 to +172
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;
});
Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copilot uses AI. Check for mistakes.
onClick={() =>
window.open(
`/${workspaceSlug}/projects/${projectId}/views/${v.id}`,
"_blank",
Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
"_blank",
"_blank",
"noopener,noreferrer",

Copilot uses AI. Check for mistakes.
Comment on lines +1090 to +1104
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]);
Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copilot uses AI. Check for mistakes.
…-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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Implement Project Views Page

3 participants