Skip to content
Open
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
95 changes: 95 additions & 0 deletions apps/web/core/components/pages/header/base.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { useCallback, ReactNode } from "react";
import { observer } from "mobx-react";
import { ListFilter } from "lucide-react";
// plane imports
import { useTranslation } from "@plane/i18n";
import { type TPageFilterProps } from "@plane/types";
import { Header, EHeaderVariant } from "@plane/ui";
import { calculateTotalFilters } from "@plane/utils";
// components
import { FiltersDropdown } from "@/components/issues/issue-layouts/filters";
// hooks
import { useMember } from "@/hooks/store/use-member";
// plane web hooks
import { EPageStoreType, usePageStore } from "@/plane-web/hooks/store";
// local imports
import { PageAppliedFiltersList } from "../list/applied-filters";
import { PageFiltersSelection } from "../list/filters";
import { PageOrderByDropdown } from "../list/order-by";
import { PageSearchInput } from "../list/search-input";

type BasePagesListHeaderRootProps = {
storeType: EPageStoreType;
tabNavigationComponent: ReactNode;
};

export const BasePagesListHeaderRoot: React.FC<BasePagesListHeaderRootProps> = observer((props) => {
const { storeType, tabNavigationComponent } = props;
const { t } = useTranslation();
// store hooks
const { filters, updateFilters, clearAllFilters } = usePageStore(storeType);
const {
workspace: { workspaceMemberIds },
} = useMember();

const handleRemoveFilter = useCallback(
(key: keyof TPageFilterProps, value: string | null) => {
let newValues = filters.filters?.[key];

if (key === "favorites") newValues = !!value;
if (Array.isArray(newValues)) {
if (!value) newValues = [];
else newValues = newValues.filter((val) => val !== value);
}

updateFilters("filters", { [key]: newValues });
},
[filters.filters, updateFilters]
);
Comment on lines +35 to +48
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Fix favorites removal and improve type safety in handleRemoveFilter

Setting favorites to false on removal may leave a redundant key and relies on downstream logic to ignore it. Prefer clearing the key (undefined) and narrow types per-key to avoid boolean/array confusion.

-  const handleRemoveFilter = useCallback(
-    (key: keyof TPageFilterProps, value: string | null) => {
-      let newValues = filters.filters?.[key];
-
-      if (key === "favorites") newValues = !!value;
-      if (Array.isArray(newValues)) {
-        if (!value) newValues = [];
-        else newValues = newValues.filter((val) => val !== value);
-      }
-
-      updateFilters("filters", { [key]: newValues });
-    },
-    [filters.filters, updateFilters]
-  );
+  const handleRemoveFilter = useCallback(
+    (key: keyof TPageFilterProps, value: string | null) => {
+      const current = filters.filters ?? {};
+      // Special-case boolean filters
+      if (key === "favorites") {
+        // remove when value is null, otherwise set true
+        updateFilters("filters", { favorites: value === null ? undefined : true });
+        return;
+      }
+      const existing = current[key as Exclude<keyof TPageFilterProps, "favorites">];
+      let next: unknown = existing;
+      if (Array.isArray(existing)) {
+        next = value ? existing.filter((v) => v !== value) : [];
+      } else {
+        // Non-array keys: clear by default
+        next = undefined;
+      }
+      updateFilters("filters", { [key]: next } as Partial<TPageFilterProps>);
+    },
+    [filters.filters, updateFilters]
+  );
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const handleRemoveFilter = useCallback(
(key: keyof TPageFilterProps, value: string | null) => {
let newValues = filters.filters?.[key];
if (key === "favorites") newValues = !!value;
if (Array.isArray(newValues)) {
if (!value) newValues = [];
else newValues = newValues.filter((val) => val !== value);
}
updateFilters("filters", { [key]: newValues });
},
[filters.filters, updateFilters]
);
const handleRemoveFilter = useCallback(
(key: keyof TPageFilterProps, value: string | null) => {
const current = filters.filters ?? {};
// Special-case boolean filters
if (key === "favorites") {
// remove when value is null, otherwise set true
updateFilters("filters", { favorites: value === null ? undefined : true });
return;
}
const existing = current[key as Exclude<keyof TPageFilterProps, "favorites">];
let next: unknown = existing;
if (Array.isArray(existing)) {
next = value ? existing.filter((v) => v !== value) : [];
} else {
// Non-array keys: clear by default
next = undefined;
}
updateFilters("filters", { [key]: next } as Partial<TPageFilterProps>);
},
[filters.filters, updateFilters]
);
🤖 Prompt for AI Agents
In apps/web/core/components/pages/header/base.tsx around lines 35–48,
handleRemoveFilter currently sets the "favorites" filter to a boolean (false)
and uses a union-typed newValues which can leave a redundant key and cause
downstream confusion; change the removal behavior for "favorites" to clear the
key (undefined) instead of setting false, and narrow the per-key types before
manipulating values (e.g., if key === "favorites" treat current value as boolean
| undefined and set newValues = undefined on removal; otherwise treat as
string[] and filter or set to []), then call updateFilters with the
properly-typed partial update so the favorites key is removed rather than left
false.


const isFiltersApplied = calculateTotalFilters(filters?.filters ?? {}) !== 0;

return (
<>
<Header variant={EHeaderVariant.SECONDARY}>
<Header.LeftItem>{tabNavigationComponent}</Header.LeftItem>
<Header.RightItem className="items-center">
<PageSearchInput
searchQuery={filters.searchQuery}
updateSearchQuery={(val) => updateFilters("searchQuery", val)}
/>
<PageOrderByDropdown
sortBy={filters.sortBy}
sortKey={filters.sortKey}
onChange={(val) => {
if (val.key) updateFilters("sortKey", val.key);
if (val.order) updateFilters("sortBy", val.order);
}}
/>
<FiltersDropdown
icon={<ListFilter className="h-3 w-3" />}
title={t("common.filters")}
placement="bottom-end"
isFiltersApplied={isFiltersApplied}
>
<PageFiltersSelection
filters={filters}
handleFiltersUpdate={updateFilters}
memberIds={workspaceMemberIds ?? undefined}
/>
</FiltersDropdown>
</Header.RightItem>
</Header>
{calculateTotalFilters(filters?.filters ?? {}) !== 0 && (
<Header variant={EHeaderVariant.TERNARY}>
<PageAppliedFiltersList
appliedFilters={filters.filters ?? {}}
handleClearAllFilters={clearAllFilters}
handleRemoveFilter={handleRemoveFilter}
alwaysAllowEditing
/>
</Header>
)}
</>
);
});
93 changes: 10 additions & 83 deletions apps/web/core/components/pages/header/root.tsx
Original file line number Diff line number Diff line change
@@ -1,100 +1,27 @@
import { useCallback } from "react";
import { observer } from "mobx-react";
import { ListFilter } from "lucide-react";
// plane imports
import { useTranslation } from "@plane/i18n";
import { TPageFilterProps, TPageNavigationTabs } from "@plane/types";
import { Header, EHeaderVariant } from "@plane/ui";
import { calculateTotalFilters } from "@plane/utils";
// components
import { FiltersDropdown } from "@/components/issues/issue-layouts/filters";
// hooks
import { useMember } from "@/hooks/store/use-member";
import { TPageNavigationTabs } from "@plane/types";
// plane web hooks
import { EPageStoreType, usePageStore } from "@/plane-web/hooks/store";
import { EPageStoreType } from "@/plane-web/hooks/store";
// local imports
import { PageAppliedFiltersList } from "../list/applied-filters";
import { PageFiltersSelection } from "../list/filters";
import { PageOrderByDropdown } from "../list/order-by";
import { PageSearchInput } from "../list/search-input";
import { PageTabNavigation } from "../list/tab-navigation";
import { BasePagesListHeaderRoot } from "./base";

type Props = {
pageType: TPageNavigationTabs;
projectId: string;
storeType: EPageStoreType;
workspaceSlug: string;
};

export const PagesListHeaderRoot: React.FC<Props> = observer((props) => {
const { pageType, projectId, storeType, workspaceSlug } = props;
const { t } = useTranslation();
// store hooks
const { filters, updateFilters, clearAllFilters } = usePageStore(storeType);
const {
workspace: { workspaceMemberIds },
} = useMember();

const handleRemoveFilter = useCallback(
(key: keyof TPageFilterProps, value: string | null) => {
let newValues = filters.filters?.[key];

if (key === "favorites") newValues = !!value;
if (Array.isArray(newValues)) {
if (!value) newValues = [];
else newValues = newValues.filter((val) => val !== value);
}

updateFilters("filters", { [key]: newValues });
},
[filters.filters, updateFilters]
);

const isFiltersApplied = calculateTotalFilters(filters?.filters ?? {}) !== 0;
const { pageType, projectId, workspaceSlug } = props;

return (
<>
<Header variant={EHeaderVariant.SECONDARY}>
<Header.LeftItem>
<PageTabNavigation workspaceSlug={workspaceSlug} projectId={projectId} pageType={pageType} />
</Header.LeftItem>
<Header.RightItem className="items-center">
<PageSearchInput
searchQuery={filters.searchQuery}
updateSearchQuery={(val) => updateFilters("searchQuery", val)}
/>
<PageOrderByDropdown
sortBy={filters.sortBy}
sortKey={filters.sortKey}
onChange={(val) => {
if (val.key) updateFilters("sortKey", val.key);
if (val.order) updateFilters("sortBy", val.order);
}}
/>
<FiltersDropdown
icon={<ListFilter className="h-3 w-3" />}
title={t("common.filters")}
placement="bottom-end"
isFiltersApplied={isFiltersApplied}
>
<PageFiltersSelection
filters={filters}
handleFiltersUpdate={updateFilters}
memberIds={workspaceMemberIds ?? undefined}
/>
</FiltersDropdown>
</Header.RightItem>
</Header>
{calculateTotalFilters(filters?.filters ?? {}) !== 0 && (
<Header variant={EHeaderVariant.TERNARY}>
<PageAppliedFiltersList
appliedFilters={filters.filters ?? {}}
handleClearAllFilters={clearAllFilters}
handleRemoveFilter={handleRemoveFilter}
alwaysAllowEditing
/>
</Header>
)}
</>
<BasePagesListHeaderRoot
storeType={EPageStoreType.PROJECT}
tabNavigationComponent={
<PageTabNavigation workspaceSlug={workspaceSlug} projectId={projectId} pageType={pageType} />
}
/>
Comment on lines +21 to +25
Copy link
Contributor

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Don’t hard-code PROJECT; thread storeType through to Base

Hard-coding ties the “generic” header to project scope and can desync from views using other stores. Accept an optional storeType (default to PROJECT) and pass it through.

 type Props = {
   pageType: TPageNavigationTabs;
   projectId: string;
   workspaceSlug: string;
+  storeType?: EPageStoreType;
 };
 
 export const PagesListHeaderRoot: React.FC<Props> = observer((props) => {
-  const { pageType, projectId, workspaceSlug } = props;
+  const { pageType, projectId, workspaceSlug, storeType = EPageStoreType.PROJECT } = props;
 
   return (
     <BasePagesListHeaderRoot
-      storeType={EPageStoreType.PROJECT}
+      storeType={storeType}
       tabNavigationComponent={
         <PageTabNavigation workspaceSlug={workspaceSlug} projectId={projectId} pageType={pageType} />
       }
     />
   );
 });

After this change, pass storeType from PagesListView (see companion comment).


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== repo root ==="
pwd

echo
echo "=== show file if exists: apps/web/core/components/pages/header/root.tsx ==="
if [ -f "apps/web/core/components/pages/header/root.tsx" ]; then
  sed -n '1,240p' apps/web/core/components/pages/header/root.tsx
else
  echo "MISSING: apps/web/core/components/pages/header/root.tsx"
fi

echo
echo "=== search for related symbols ==="
rg -n --hidden -S 'PagesListHeaderRoot' -g '!*node_modules/*' -C3 || true
rg -n --hidden -S 'BasePagesListHeaderRoot' -g '!*node_modules/*' -C3 || true
rg -n --hidden -S 'EPageStoreType' -g '!*node_modules/*' -C3 || true
rg -n --hidden -S 'PagesListView' -g '!*node_modules/*' -C3 || true
rg -n --hidden -S 'PageTabNavigation' -g '!*node_modules/*' -C3 || true

Length of output: 1674


Don't hard-code PROJECT — thread storeType through to Base

Hard-coding ties the generic header to project scope and can desync from views using other stores. Accept an optional storeType (default to EPageStoreType.PROJECT) and pass it to BasePagesListHeaderRoot.

File: apps/web/core/components/pages/header/root.tsx (around lines 21–25)

 type Props = {
   pageType: TPageNavigationTabs;
   projectId: string;
   workspaceSlug: string;
+  storeType?: EPageStoreType;
 };
 
 export const PagesListHeaderRoot: React.FC<Props> = observer((props) => {
-  const { pageType, projectId, workspaceSlug } = props;
+  const { pageType, projectId, workspaceSlug, storeType = EPageStoreType.PROJECT } = props;
 
   return (
     <BasePagesListHeaderRoot
-      storeType={EPageStoreType.PROJECT}
+      storeType={storeType}
       tabNavigationComponent={
         <PageTabNavigation workspaceSlug={workspaceSlug} projectId={projectId} pageType={pageType} />
       }
     />
   );
 });

After this change, pass storeType from PagesListView (see companion comment).

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
storeType={EPageStoreType.PROJECT}
tabNavigationComponent={
<PageTabNavigation workspaceSlug={workspaceSlug} projectId={projectId} pageType={pageType} />
}
/>
type Props = {
pageType: TPageNavigationTabs;
projectId: string;
workspaceSlug: string;
storeType?: EPageStoreType;
};
export const PagesListHeaderRoot: React.FC<Props> = observer((props) => {
const { pageType, projectId, workspaceSlug, storeType = EPageStoreType.PROJECT } = props;
return (
<BasePagesListHeaderRoot
storeType={storeType}
tabNavigationComponent={
<PageTabNavigation workspaceSlug={workspaceSlug} projectId={projectId} pageType={pageType} />
}
/>
);
});
🤖 Prompt for AI Agents
In apps/web/core/components/pages/header/root.tsx around lines 21 to 25, the
component currently hard-codes storeType=EPageStoreType.PROJECT; change the
component signature to accept an optional prop storeType with a default of
EPageStoreType.PROJECT, and forward that prop to the BasePagesListHeaderRoot
(replace the hard-coded value with the new prop). After this change, update the
PagesListView to pass its storeType through into this header component so the
header reflects the actual view's store type.

);
});
7 changes: 1 addition & 6 deletions apps/web/core/components/pages/pages-list-view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,7 @@ export const PagesListView: React.FC<TPageView> = observer((props) => {
<div className="relative w-full h-full overflow-hidden flex flex-col">
{/* tab header */}
{isAnyPageAvailable && (
<PagesListHeaderRoot
pageType={pageType}
projectId={projectId}
storeType={storeType}
workspaceSlug={workspaceSlug}
/>
<PagesListHeaderRoot pageType={pageType} projectId={projectId} workspaceSlug={workspaceSlug} />
Copy link
Contributor

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Header store now diverges from view store (PROJECT vs storeType)

Header is hard-coded to the PROJECT store via the wrapper, while this view gates and fetches via storeType and renders content with storeType. This can desync filters/search/sort and even hide the header when storeType !== PROJECT. Pass storeType through to keep header and content in the same store.

Apply:

-        <PagesListHeaderRoot pageType={pageType} projectId={projectId} workspaceSlug={workspaceSlug} />
+        <PagesListHeaderRoot
+          pageType={pageType}
+          projectId={projectId}
+          workspaceSlug={workspaceSlug}
+          storeType={storeType}
+        />

To verify current usages and whether non-PROJECT store types exist:


🏁 Script executed:

#!/bin/bash
# Find PagesListView call sites and inspect storeType usage.
rg -nP --type tsx -C3 '<PagesListView\b' | sed -n '1,200p'
rg -nP --type tsx -C3 'storeType\s*=' | sed -n '1,200p'

Length of output: 55


🏁 Script executed:

#!/bin/bash
set -o pipefail

echo "=== search for PagesListView / PagesListHeaderRoot usages ==="
rg -n --hidden -S '<PagesListView\b' || true
rg -n --hidden -S '<PagesListHeaderRoot\b' || true

echo
echo "=== search for filenames / symbols ==="
rg -n --hidden -S 'pages-list-view|pages-list-header-root|PagesListHeaderRoot|PagesListView' || true

echo
echo "=== show specific file if it exists ==="
if [ -f "apps/web/core/components/pages/pages-list-view.tsx" ]; then
  echo "---- apps/web/core/components/pages/pages-list-view.tsx ----"
  sed -n '1,240p' apps/web/core/components/pages/pages-list-view.tsx || true
else
  echo "file apps/web/core/components/pages/pages-list-view.tsx not found"
fi

echo
echo "=== find files that define PagesListHeaderRoot and print head ==="
files=$(rg -l --hidden -S 'PagesListHeaderRoot' || true)
if [ -n "$files" ]; then
  for f in $files; do
    echo "---- $f ----"
    sed -n '1,240p' "$f" || true
    echo
  done
else
  echo "No files named PagesListHeaderRoot found by rg"
fi

echo
echo "=== search for storeType / StoreType.PROJECT mentions ==="
rg -n --hidden -S 'storeType|StoreType\.PROJECT' || true

echo
echo "=== list files in apps/web/core/components/pages if present ==="
if [ -d "apps/web/core/components/pages" ]; then
  ls -la apps/web/core/components/pages || true
else
  echo "dir apps/web/core/components/pages not found"
fi

Length of output: 40506


Pass storeType through to the header and make the header respect it

Header is hard-coded to EPageStoreType.PROJECT while PagesListView fetches/renders by the incoming storeType — this can desync filters/search/sort or hide the header when storeType !== PROJECT. Change both files so header receives and uses the same storeType.

  • Apply to apps/web/core/components/pages/pages-list-view.tsx:
-        <PagesListHeaderRoot pageType={pageType} projectId={projectId} workspaceSlug={workspaceSlug} />
+        <PagesListHeaderRoot
+          pageType={pageType}
+          projectId={projectId}
+          workspaceSlug={workspaceSlug}
+          storeType={storeType}
+        />
  • Update apps/web/core/components/pages/header/root.tsx to accept and forward storeType (keep PROJECT as default for backwards compatibility):
-type Props = {
-  pageType: TPageNavigationTabs;
-  projectId: string;
-  workspaceSlug: string;
-};
+type Props = {
+  pageType: TPageNavigationTabs;
+  projectId: string;
+  workspaceSlug: string;
+  storeType?: EPageStoreType;
+};

 export const PagesListHeaderRoot: React.FC<Props> = observer((props) => {
-  const { pageType, projectId, workspaceSlug } = props;
+  const { pageType, projectId, workspaceSlug, storeType = EPageStoreType.PROJECT } = props;

   return (
     <BasePagesListHeaderRoot
-      storeType={EPageStoreType.PROJECT}
+      storeType={storeType}
       tabNavigationComponent={
         <PageTabNavigation workspaceSlug={workspaceSlug} projectId={projectId} pageType={pageType} />
       }
     />
   );
 });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<PagesListHeaderRoot pageType={pageType} projectId={projectId} workspaceSlug={workspaceSlug} />
// apps/web/core/components/pages/pages-list-view.tsx
<PagesListHeaderRoot
pageType={pageType}
projectId={projectId}
workspaceSlug={workspaceSlug}
storeType={storeType}
/>
Suggested change
<PagesListHeaderRoot pageType={pageType} projectId={projectId} workspaceSlug={workspaceSlug} />
// apps/web/core/components/pages/header/root.tsx
type Props = {
pageType: TPageNavigationTabs;
projectId: string;
workspaceSlug: string;
storeType?: EPageStoreType;
};
export const PagesListHeaderRoot: React.FC<Props> = observer((props) => {
const { pageType, projectId, workspaceSlug, storeType = EPageStoreType.PROJECT } = props;
return (
<BasePagesListHeaderRoot
storeType={storeType}
tabNavigationComponent={
<PageTabNavigation workspaceSlug={workspaceSlug} projectId={projectId} pageType={pageType} />
}
/>
);
});
🤖 Prompt for AI Agents
In apps/web/core/components/pages/pages-list-view.tsx around line 33 and in
apps/web/core/components/pages/header/root.tsx, the PagesListHeaderRoot is
hard-coded to EPageStoreType.PROJECT causing desync when pages-list-view uses a
different storeType; update pages-list-view to pass the current storeType prop
into PagesListHeaderRoot, and update header/root.tsx to accept an optional
storeType prop (defaulting to EPageStoreType.PROJECT for backwards
compatibility) and forward that storeType to any child/header internals that
currently assume PROJECT so filters/search/sort reflect the incoming storeType.

)}
<PagesListMainContent pageType={pageType} storeType={storeType}>
{children}
Expand Down
Loading