Skip to content

[Feature]- Option to open Quicklinks in same page (Issue #8621)#8734

Open
Ivanq50 wants to merge 5 commits intomakeplane:previewfrom
WSU-CptS-481-Spring-2026:fix-8621-quicklinks
Open

[Feature]- Option to open Quicklinks in same page (Issue #8621)#8734
Ivanq50 wants to merge 5 commits intomakeplane:previewfrom
WSU-CptS-481-Spring-2026:fix-8621-quicklinks

Conversation

@Ivanq50
Copy link

@Ivanq50 Ivanq50 commented Mar 9, 2026

Description

Added an option for the user to open links within the same page if desired. Placed a toggle button to set that preference, but the user also has the option to select that preference individually by clicking the "Open in new tab" or "Open in same tab" option after clicking the more options menu next to the link.

Type of Change

  • Bug fix (non-breaking change which fixes an issue)
  • Feature (non-breaking change which adds functionality)
  • Improvement (change that would cause existing functionality to not work as expected)
  • Code refactoring
  • Performance improvements
  • Documentation update

Screenshots and Media (if applicable)

quicklinks-after

Test Scenarios

Tested using unit tests which included:

  • Confirming the list endpoints behave consistently when there is no data.

  • Confirming both the API response contract and the database side effect.

  • Confirming the title is truly optional and the minimal valid payload works.

  • Confirming server-side validation for the url field is enforced.

  • Confirming url is required and missing required fields are rejected.

  • Confirming the detail endpoint can fetch a created resource and that the returned representation is correct.

  • Check that partial updates are working correctly and don't overwrite unspecified fields.

  • Confirm the delete behavior and ensures the test environment stays deterministic.

  • Confirm the API returns the correct status code for missing resources.

References

https://github.com/makeplane/plane/issues/8621

Summary by CodeRabbit

Release Notes

  • New Features

    • Added drag-and-drop manual page reordering with a new "Manual order" sort option
    • Added toggle to open quick links in the same tab or new tab
    • Pages can now update their sort order even when locked
  • Tests

    • Added comprehensive API tests for pages and quick links endpoints
  • Chores

    • Updated base path configuration handling across applications
    • Removed deprecated internationalization setting

yevin215 and others added 5 commits March 5, 2026 22:57
…e functionality of opening quicklinks in same page. Can toggle the preference (same page or new tab) with a button click and can also choose individually by link.
Copilot AI review requested due to automatic review settings March 9, 2026 03:08
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 9, 2026

📝 Walkthrough

Walkthrough

This pull request adds manual page reordering and quick links features. Pages now include a sort_order field that persists across backend and frontend, enabling drag-and-drop reordering in the UI. Quick links gain an open-in-same-tab toggle. Configuration files are updated with a new normalizeBasePath utility, import paths are corrected, and comprehensive contract tests validate the new page and quick links APIs.

Changes

Cohort / File(s) Summary
Base Path Normalization
apps/admin/react-router.config.ts, apps/admin/vite.config.ts, apps/space/react-router.config.ts, apps/space/vite.config.ts
Replaced joinUrlPath-based path computation with new normalizeBasePath helper function that trims input, normalizes empty/root values to "/", and ensures leading/trailing slashes without duplication.
Page Sort Order Backend
apps/api/plane/app/serializers/page.py, apps/api/plane/app/views/page/base.py
Added sort_order field to PageSerializer, updated queryset sorting to include sort_order after is_favorite, and modified partial_update to allow sort_order updates even when page is locked.
Page Sort Order Frontend Store
apps/web/core/store/pages/base-page.ts, apps/web/core/store/pages/project-page.store.ts
Added sort_order property to BasePage, implemented reorderPage action for drag-drop reordering with above/below placement calculations, and changed default filter sort key from "updated_at" to "sort_order".
Page Sort Order Types & Utilities
packages/types/src/page/core.ts, packages/utils/src/page.ts
Extended TPage type and TPageFiltersSortKey union with sort_order field, updated page sorting logic to handle "sort_order" key using non-mutating toReversed().
Page Drag-and-Drop UI
apps/web/core/components/pages/list/block.tsx, apps/web/core/components/pages/list/order-by.tsx, apps/web/core/components/pages/list/root.tsx
Introduced drag-and-drop reordering with visual indicators and draggable handles, added "Manual order" sort option, integrated handlePageDrop handler with localized error toasts and reorder validation.
Quick Links Toggle Feature
apps/web/core/components/home/widgets/links/constants.ts, apps/web/core/components/home/widgets/links/link-detail.tsx, apps/web/core/components/home/widgets/links/root.tsx
Added local storage-backed toggle for opening links in same tab vs. new tab, new context menu item, conditional click handler, and UI toggle with dynamic icon and tooltip.
Contract Tests
apps/api/plane/tests/contract/app/test_pages_app.py, apps/api/plane/tests/contract/app/test_quick_links.py
Added comprehensive contract tests for page reordering (sorting, locking behavior) and quick links CRUD operations (create, retrieve, update, delete, validation).
Configuration & Setup
apps/api/plane/celery.py, apps/api/plane/settings/common.py, apps/api/plane/settings/test.py, packages/i18n/src/locales/en/translations.ts
Updated JsonFormatter import path from pythonjsonlogger.jsonlogger to pythonjsonlogger.json, removed deprecated USE_L10N setting, removed WhiteNoiseMiddleware from test settings to suppress warnings, added translation keys for quick links open-in-same-tab feature.

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant UI as Page List UI
    participant Store as ProjectPageStore
    participant API as Backend API
    participant DB as Database

    User->>UI: Drag and drop page
    activate UI
    UI->>UI: Calculate drop position (above/below)
    UI->>Store: handlePageDrop(sourceId, destId, edge)
    deactivate UI
    
    activate Store
    Store->>Store: Validate prerequisites
    Store->>Store: Calculate new sort_order values
    Store->>Store: Optimistically update local state
    Store->>API: reorderPage(workspaceSlug, projectId, pageId, destPageId, edge)
    deactivate Store
    
    activate API
    API->>DB: Update sort_order values
    DB-->>API: Confirmed
    API-->>Store: Success response
    deactivate API
    
    activate Store
    Store->>Store: Confirm local state
    Store->>UI: Notify observers of update
    deactivate Store
    
    activate UI
    UI->>User: Reordered list displayed
    deactivate UI
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~30 minutes

Poem

🐰 Hops with glee at pages sorted,
Drag-and-drop where once was naught,
Links may open, same tab or new,
Manual order, fresh and true!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 42.86% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the feature being added (opening Quicklinks in same page) and references the GitHub issue, providing meaningful context about the changeset.
Description check ✅ Passed The description covers all required template sections: a detailed explanation of the feature with both global toggle and per-link options, Type of Change marked as Feature, screenshots included, comprehensive test scenarios listed, and issue reference provided.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Tip

Try Coding Plans. Let us write the prompt for your AI agent so you can ship faster (with fewer bugs).
Share your feedback on Discord.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@CLAassistant
Copy link

CLAassistant commented Mar 9, 2026

CLA assistant check
All committers have signed the CLA.

Copy link
Contributor

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

Adds a user preference to open Quick Links in the same tab, and also introduces a manual ordering mode for Pages backed by a new sort_order field across the stack.

Changes:

  • Add a Quick Links “open in same tab” preference (toggle + context menu action) persisted in localStorage.
  • Add Page manual ordering via sort_order (API ordering + serializer field + web drag/drop reorder + new sort option).
  • Normalize base path handling for admin/space builds and add/adjust API contract tests & test settings.

Reviewed changes

Copilot reviewed 22 out of 22 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
packages/utils/src/page.ts Adds sort_order ordering support and switches to toReversed() for desc sorting.
packages/types/src/page/core.ts Extends TPage with sort_order and adds sort_order as a valid sort key.
packages/i18n/src/locales/en/translations.ts Adds new Quick Links strings for same/new tab options.
apps/web/core/store/pages/project-page.store.ts Defaults page sorting to sort_order and adds a reorderPage action that patches sort_order.
apps/web/core/store/pages/base-page.ts Stores/serializes sort_order on the base page model.
apps/web/core/components/pages/list/root.tsx Enables page reordering in the list and surfaces reorder failures via toast.
apps/web/core/components/pages/list/order-by.tsx Adds “Manual order” to the page sort dropdown.
apps/web/core/components/pages/list/block.tsx Adds pragmatic-dnd drag/drop behavior for list items + drag handle UI.
apps/web/core/components/home/widgets/links/root.tsx Adds Quick Links header toggle to switch open-in-same-tab preference.
apps/web/core/components/home/widgets/links/link-detail.tsx Implements primary click behavior based on preference + adds “Open in same tab” menu option.
apps/web/core/components/home/widgets/links/constants.ts Defines the localStorage key and default for the Quick Links preference.
apps/space/vite.config.ts Replaces joinUrlPath with a local normalizeBasePath helper for Vite base.
apps/space/react-router.config.ts Mirrors base path normalization for react-router config.
apps/admin/vite.config.ts Replaces joinUrlPath with normalizeBasePath for admin Vite base.
apps/admin/react-router.config.ts Mirrors base path normalization for admin react-router config.
apps/api/plane/app/views/page/base.py Orders pages by -sort_order and allows sort_order patching even when locked.
apps/api/plane/app/serializers/page.py Adds sort_order to page serializer fields.
apps/api/plane/tests/contract/app/test_quick_links.py Adds contract tests for Quick Links CRUD endpoints.
apps/api/plane/tests/contract/app/test_pages_app.py Adds contract tests for page list ordering and sort_order patching/locking rules.
apps/api/plane/settings/test.py Removes WhiteNoise middleware in tests to avoid static-root warnings.
apps/api/plane/settings/common.py Removes deprecated USE_L10N.
apps/api/plane/celery.py Changes JsonFormatter import path for Celery logging.

Comment on lines +7 to +11
const normalizeBasePath = (value: string): string => {
const trimmed = value.trim();
if (!trimmed || trimmed === "/") return "/";
return `/${trimmed.replace(/^\/+|\/+$/g, "")}/`;
};
Copy link

Copilot AI Mar 9, 2026

Choose a reason for hiding this comment

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

normalizeBasePath is duplicated across multiple config files (admin + space, vite + react-router). This increases the chance of the base-path normalization logic drifting over time. Consider extracting it into a small shared helper (e.g., in @plane/utils or a local config-utils module) and importing it from each config file.

Copilot uses AI. Check for mistakes.
Comment on lines +43 to +65
const handlePageDrop = async (
sourcePageId: string,
destinationPageId: string,
edge: "reorder-above" | "reorder-below"
) => {
if (!workspaceSlug || !projectId || sourcePageId === destinationPageId || !filteredPageIds?.length) return;
try {
await reorderPage(
workspaceSlug.toString(),
projectId.toString(),
sourcePageId,
destinationPageId,
edge,
filteredPageIds
);
} catch {
setToast({
type: TOAST_TYPE.ERROR,
title: t("toast.error"),
message: "Failed to reorder page. Please try again.",
});
}
};
Copy link

Copilot AI Mar 9, 2026

Choose a reason for hiding this comment

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

handlePageDrop is recreated on every render and is passed into PageListBlock. Since PageListBlock's drag/drop useEffect depends on onPageDrop, this will cause the drag/drop adapters to be torn down and re-registered every render, which can lead to flaky DnD behavior and unnecessary work. Wrap handlePageDrop in useCallback (with the needed dependencies) or otherwise ensure a stable callback reference is passed to PageListBlock.

Copilot uses AI. Check for mistakes.
Comment on lines +442 to +444
open_in_same_tab: "Open in same tab",
open_links_in_new_tab: "Open links in new tab",
open_links_in_same_tab: "Open links in same tab",
Copy link

Copilot AI Mar 9, 2026

Choose a reason for hiding this comment

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

New quick-links translation keys (open_in_same_tab, open_links_in_new_tab, open_links_in_same_tab) were added only to the English locale. Other locale translation files under packages/i18n/src/locales/*/translations.ts do not define these keys, which will cause missing-string fallbacks/inconsistent UX for non-English users. Please add the same keys to the other locales (even if temporarily copying the English text) or update the i18n loading strategy to guarantee an explicit fallback for these keys.

Suggested change
open_in_same_tab: "Open in same tab",
open_links_in_new_tab: "Open links in new tab",
open_links_in_same_tab: "Open links in same tab",

Copilot uses AI. Check for mistakes.
# Third party imports
from celery import Celery
from pythonjsonlogger.jsonlogger import JsonFormatter
from pythonjsonlogger.json import JsonFormatter
Copy link

Copilot AI Mar 9, 2026

Choose a reason for hiding this comment

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

python-json-logger is pinned to python-json-logger==3.3.0 (apps/api/requirements/base.txt) and the rest of the codebase references pythonjsonlogger.jsonlogger.JsonFormatter in LOGGING config. Importing JsonFormatter from pythonjsonlogger.json here is inconsistent and is likely to raise ModuleNotFoundError at worker startup. Please switch this import back to from pythonjsonlogger.jsonlogger import JsonFormatter (or update dependencies/config consistently if the new module path is intentional).

Suggested change
from pythonjsonlogger.json import JsonFormatter
from pythonjsonlogger.jsonlogger import JsonFormatter

Copilot uses AI. Check for mistakes.
Comment on lines 83 to 87
filters: TPageFilters = {
searchQuery: "",
sortKey: "updated_at",
sortKey: "sort_order",
sortBy: "desc",
};
Copy link

Copilot AI Mar 9, 2026

Choose a reason for hiding this comment

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

This PR is described as a Quick Links UX feature (open in same tab), but it also introduces a separate Pages feature (manual sorting/reordering via sort_order) and changes unrelated build/runtime configuration (admin/space base path normalization, logging import changes, Django settings tweaks). Please update the PR title/description to reflect the full scope or split these into separate PRs so reviewers can reason about risk and intent more clearly.

Copilot uses AI. Check for mistakes.
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/web/core/components/pages/list/block.tsx (1)

50-54: ⚠️ Potential issue | 🔴 Critical

Hooks called after conditional return violates React's Rules of Hooks.

The useEffect on line 54 is placed after the early return on line 50. React requires hooks to be called in the same order on every render. If page is null, the hook won't be called, causing inconsistent hook ordering.

🐛 Proposed fix: Move early return after all hooks
   const { isMobile } = usePlatformOS();
-  // handle page check
-  if (!page) return null;
-  // derived values
-  const { name, logo_props, getRedirectionLink } = page;

   useEffect(() => {
     const rowElement = parentRef.current;
-    if (!rowElement || !isReorderEnabled) return;
+    if (!rowElement || !isReorderEnabled || !page) return;

     const initialData = { id: pageId, dragInstanceId: "PROJECT_PAGES" };
     return combine(
       // ... rest of effect
     );
   }, [isReorderEnabled, isLastChild, onPageDrop, pageId]);

+  // handle page check
+  if (!page) return null;
+  // derived values
+  const { name, logo_props, getRedirectionLink } = page;
+
   return (
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/core/components/pages/list/block.tsx` around lines 50 - 54, The
early return "if (!page) return null;" is placed before hooks which breaks Rules
of Hooks; move that conditional return so all hooks (e.g., the useEffect
starting at the current line 54) are declared before any early returns, or
alternatively change the early-return to a conditional render inside the JSX
while keeping the useEffect and any other hooks (referencing page, name,
logo_props, getRedirectionLink) at the top of the component; ensure hooks are
always executed in the same order regardless of page being null.
🧹 Nitpick comments (3)
apps/api/plane/tests/contract/app/test_quick_links.py (1)

115-123: Move uuid import to the top of the file.

The uuid import on line 118 should be moved to the module level with other imports for consistency.

♻️ Proposed fix

Add to imports at top of file:

 from unittest.mock import patch
 
+import uuid
+
 import pytest
 from rest_framework import status

Then update the test:

     def test_retrieve_nonexistent_returns_404(self, session_client, workspace):
         """Retrieve with invalid uuid returns 404."""
-        import uuid
-
         fake_id = uuid.uuid4()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/api/plane/tests/contract/app/test_quick_links.py` around lines 115 -
123, Move the local import of uuid from inside
test_retrieve_nonexistent_returns_404 to the module-level imports at the top of
the test file so all imports are consistent; update the top import block to
include uuid and remove the inline `import uuid` inside the test that uses
fake_id and _quick_links_detail_url(session_client, workspace) so the test
simply calls uuid.uuid4() without an inline import.
apps/admin/vite.config.ts (1)

7-23: Extract normalizeBasePath() to a shared config helper.

This logic is now duplicated in apps/admin/vite.config.ts, apps/space/vite.config.ts, apps/admin/react-router.config.ts, and apps/space/react-router.config.ts. Centralizing it will keep Vite base and router basename from drifting the next time the normalization rules change.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/admin/vite.config.ts` around lines 7 - 23, The normalizeBasePath
function is duplicated; extract it into a shared config helper module, export
normalizeBasePath, and replace the local definitions in files that use it
(references: normalizeBasePath, VITE_ADMIN_BASE_PATH, viteEnv) with imports from
that helper; ensure the helper accepts a string and returns the same
trimmed/normalized output and update each vite.config.ts and
react-router.config.ts to import and use the shared normalizeBasePath so both
Vite `base` and router `basename` remain consistent.
apps/web/core/components/pages/list/root.tsx (1)

58-64: Consider translating the error message for consistency.

The toast title uses t("toast.error") but the message is hardcoded in English. For i18n consistency, the message should also be translated.

♻️ Suggested improvement
     } catch {
       setToast({
         type: TOAST_TYPE.ERROR,
         title: t("toast.error"),
-        message: "Failed to reorder page. Please try again.",
+        message: t("pages.reorder_error"),
       });
     }

This requires adding the corresponding translation key to the i18n catalog.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/core/components/pages/list/root.tsx` around lines 58 - 64, The toast
error message is hardcoded in English; update the catch block that calls
setToast with TOAST_TYPE.ERROR to use the i18n helper t(...) for the message
(e.g., replace the literal "Failed to reorder page. Please try again." with a
translation key like t("toast.reorderPageFailed")); also add the corresponding
translation entry to the i18n catalog for that key in all locales so the message
is available.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/api/plane/app/views/page/base.py`:
- Around line 103-105: The current queryset first applies the caller's ?order_by
then unconditionally calls .order_by("-is_favorite", "-sort_order",
"-created_at"), which overwrites the explicit ordering; change the logic in the
view (around the queryset building that calls self.request.GET.get("order_by",
"-created_at") and the subsequent .order_by("-is_favorite", "-sort_order",
"-created_at")) so the latter default ordering is only applied when no order_by
parameter was provided (e.g., check if self.request.GET.get("order_by") is falsy
and only then call .order_by("-is_favorite", "-sort_order", "-created_at")).

In `@apps/space/vite.config.ts`:
- Around line 7-10: normalizeBasePath currently only trims leading/trailing
slashes so inputs like "space//preview" become "/space//preview/"; update the
normalizeBasePath function to also collapse repeated slashes inside the path by
replacing consecutive "/" groups with a single "/" (e.g., use a replace like
value.replace(/\/{2,}/g, "/")) after trimming and before adding the
leading/trailing slash, and keep the function signature and return behavior the
same.

In `@apps/web/core/components/home/widgets/links/root.tsx`:
- Around line 88-90: The add-trigger button in the Links widget (the button
using handleCreateLinkModal and rendering PlusIcon in root.tsx) lacks an
explicit type and will default to submit inside a form; update that button
element to include type="button" so clicking it won't submit any parent form,
leaving onClick={handleCreateLinkModal} and className intact.
- Around line 71-86: The button rendering the toggle lacks an accessible pressed
state; update the button element (the one using handleToggleOpenInSameTab and
reading openInSameTab/DEFAULT_QUICK_LINKS_OPEN_IN_SAME_TAB) to include an
aria-pressed attribute that reflects the current boolean state ((openInSameTab
?? DEFAULT_QUICK_LINKS_OPEN_IN_SAME_TAB)), so assistive tech can determine
on/off while keeping the existing aria-label and click handler intact.

---

Outside diff comments:
In `@apps/web/core/components/pages/list/block.tsx`:
- Around line 50-54: The early return "if (!page) return null;" is placed before
hooks which breaks Rules of Hooks; move that conditional return so all hooks
(e.g., the useEffect starting at the current line 54) are declared before any
early returns, or alternatively change the early-return to a conditional render
inside the JSX while keeping the useEffect and any other hooks (referencing
page, name, logo_props, getRedirectionLink) at the top of the component; ensure
hooks are always executed in the same order regardless of page being null.

---

Nitpick comments:
In `@apps/admin/vite.config.ts`:
- Around line 7-23: The normalizeBasePath function is duplicated; extract it
into a shared config helper module, export normalizeBasePath, and replace the
local definitions in files that use it (references: normalizeBasePath,
VITE_ADMIN_BASE_PATH, viteEnv) with imports from that helper; ensure the helper
accepts a string and returns the same trimmed/normalized output and update each
vite.config.ts and react-router.config.ts to import and use the shared
normalizeBasePath so both Vite `base` and router `basename` remain consistent.

In `@apps/api/plane/tests/contract/app/test_quick_links.py`:
- Around line 115-123: Move the local import of uuid from inside
test_retrieve_nonexistent_returns_404 to the module-level imports at the top of
the test file so all imports are consistent; update the top import block to
include uuid and remove the inline `import uuid` inside the test that uses
fake_id and _quick_links_detail_url(session_client, workspace) so the test
simply calls uuid.uuid4() without an inline import.

In `@apps/web/core/components/pages/list/root.tsx`:
- Around line 58-64: The toast error message is hardcoded in English; update the
catch block that calls setToast with TOAST_TYPE.ERROR to use the i18n helper
t(...) for the message (e.g., replace the literal "Failed to reorder page.
Please try again." with a translation key like t("toast.reorderPageFailed"));
also add the corresponding translation entry to the i18n catalog for that key in
all locales so the message is available.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 1f45d0db-059c-4b94-8a0a-fb688244daa7

📥 Commits

Reviewing files that changed from the base of the PR and between 6627282 and 1ba743b.

📒 Files selected for processing (22)
  • apps/admin/react-router.config.ts
  • apps/admin/vite.config.ts
  • apps/api/plane/app/serializers/page.py
  • apps/api/plane/app/views/page/base.py
  • apps/api/plane/celery.py
  • apps/api/plane/settings/common.py
  • apps/api/plane/settings/test.py
  • apps/api/plane/tests/contract/app/test_pages_app.py
  • apps/api/plane/tests/contract/app/test_quick_links.py
  • apps/space/react-router.config.ts
  • apps/space/vite.config.ts
  • apps/web/core/components/home/widgets/links/constants.ts
  • apps/web/core/components/home/widgets/links/link-detail.tsx
  • apps/web/core/components/home/widgets/links/root.tsx
  • apps/web/core/components/pages/list/block.tsx
  • apps/web/core/components/pages/list/order-by.tsx
  • apps/web/core/components/pages/list/root.tsx
  • apps/web/core/store/pages/base-page.ts
  • apps/web/core/store/pages/project-page.store.ts
  • packages/i18n/src/locales/en/translations.ts
  • packages/types/src/page/core.ts
  • packages/utils/src/page.ts
💤 Files with no reviewable changes (1)
  • apps/api/plane/settings/common.py

Comment on lines 103 to +105
.order_by(self.request.GET.get("order_by", "-created_at"))
.prefetch_related("labels")
.order_by("-is_favorite", "-created_at")
.order_by("-is_favorite", "-sort_order", "-created_at")
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Preserve explicit order_by requests here.

Line 103 already applies the caller's ?order_by=..., but Line 105 replaces it unconditionally, so every list request now falls back to -is_favorite, -sort_order, -created_at. If sort_order is only meant to be the default, gate this second ordering behind the absence of order_by instead of clobbering it.

Suggested fix
+        requested_order = self.request.GET.get("order_by")
+
         return self.filter_queryset(
             super()
             .get_queryset()
             .filter(workspace__slug=self.kwargs.get("slug"))
@@
             .select_related("workspace")
             .select_related("owned_by")
             .annotate(is_favorite=Exists(subquery))
-            .order_by(self.request.GET.get("order_by", "-created_at"))
             .prefetch_related("labels")
-            .order_by("-is_favorite", "-sort_order", "-created_at")
+            .order_by(
+                *(
+                    ["-is_favorite", requested_order]
+                    if requested_order
+                    else ["-is_favorite", "-sort_order", "-created_at"]
+                )
+            )
             .annotate(
                 project=Exists(
                     ProjectPage.objects.filter(page_id=OuterRef("id"), project_id=self.kwargs.get("project_id"))
                 )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/api/plane/app/views/page/base.py` around lines 103 - 105, The current
queryset first applies the caller's ?order_by then unconditionally calls
.order_by("-is_favorite", "-sort_order", "-created_at"), which overwrites the
explicit ordering; change the logic in the view (around the queryset building
that calls self.request.GET.get("order_by", "-created_at") and the subsequent
.order_by("-is_favorite", "-sort_order", "-created_at")) so the latter default
ordering is only applied when no order_by parameter was provided (e.g., check if
self.request.GET.get("order_by") is falsy and only then call
.order_by("-is_favorite", "-sort_order", "-created_at")).

Comment on lines +7 to +10
const normalizeBasePath = (value: string): string => {
const trimmed = value.trim();
if (!trimmed || trimmed === "/") return "/";
return `/${trimmed.replace(/^\/+|\/+$/g, "")}/`;
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Collapse repeated / characters inside the base path.

This only strips slashes at the ends, so VITE_SPACE_BASE_PATH="space//preview" normalizes to /space//preview/. That can bleed into generated asset URLs, and the same helper is copied into the admin/router configs too.

Suggested fix
 const normalizeBasePath = (value: string): string => {
   const trimmed = value.trim();
   if (!trimmed || trimmed === "/") return "/";
-  return `/${trimmed.replace(/^\/+|\/+$/g, "")}/`;
+  const normalized = trimmed.replace(/\/{2,}/g, "/").replace(/^\/+|\/+$/g, "");
+  return normalized ? `/${normalized}/` : "/";
 };
📝 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 normalizeBasePath = (value: string): string => {
const trimmed = value.trim();
if (!trimmed || trimmed === "/") return "/";
return `/${trimmed.replace(/^\/+|\/+$/g, "")}/`;
const normalizeBasePath = (value: string): string => {
const trimmed = value.trim();
if (!trimmed || trimmed === "/") return "/";
const normalized = trimmed.replace(/\/{2,}/g, "/").replace(/^\/+|\/+$/g, "");
return normalized ? `/${normalized}/` : "/";
};
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/space/vite.config.ts` around lines 7 - 10, normalizeBasePath currently
only trims leading/trailing slashes so inputs like "space//preview" become
"/space//preview/"; update the normalizeBasePath function to also collapse
repeated slashes inside the path by replacing consecutive "/" groups with a
single "/" (e.g., use a replace like value.replace(/\/{2,}/g, "/")) after
trimming and before adding the leading/trailing slash, and keep the function
signature and return behavior the same.

Comment on lines +71 to +86
<button
type="button"
onClick={handleToggleOpenInSameTab}
className="rounded p-1 text-tertiary transition-colors hover:bg-surface-2 hover:text-primary"
aria-label={
(openInSameTab ?? DEFAULT_QUICK_LINKS_OPEN_IN_SAME_TAB)
? t("home.quick_links.open_links_in_same_tab")
: t("home.quick_links.open_links_in_new_tab")
}
>
{(openInSameTab ?? DEFAULT_QUICK_LINKS_OPEN_IN_SAME_TAB) ? (
<LinkIcon className="size-4" />
) : (
<NewTabIcon className="size-4" />
)}
</button>
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Expose the toggle state with aria-pressed.

Right now assistive tech gets a label, but not the on/off state of this toggle. Adding aria-pressed makes the current mode explicit.

♿ Suggested tweak
               <button
                 type="button"
                 onClick={handleToggleOpenInSameTab}
                 className="rounded p-1 text-tertiary transition-colors hover:bg-surface-2 hover:text-primary"
+                aria-pressed={openInSameTab ?? DEFAULT_QUICK_LINKS_OPEN_IN_SAME_TAB}
                 aria-label={
                   (openInSameTab ?? DEFAULT_QUICK_LINKS_OPEN_IN_SAME_TAB)
                     ? t("home.quick_links.open_links_in_same_tab")
                     : t("home.quick_links.open_links_in_new_tab")
                 }
📝 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
<button
type="button"
onClick={handleToggleOpenInSameTab}
className="rounded p-1 text-tertiary transition-colors hover:bg-surface-2 hover:text-primary"
aria-label={
(openInSameTab ?? DEFAULT_QUICK_LINKS_OPEN_IN_SAME_TAB)
? t("home.quick_links.open_links_in_same_tab")
: t("home.quick_links.open_links_in_new_tab")
}
>
{(openInSameTab ?? DEFAULT_QUICK_LINKS_OPEN_IN_SAME_TAB) ? (
<LinkIcon className="size-4" />
) : (
<NewTabIcon className="size-4" />
)}
</button>
<button
type="button"
onClick={handleToggleOpenInSameTab}
className="rounded p-1 text-tertiary transition-colors hover:bg-surface-2 hover:text-primary"
aria-pressed={openInSameTab ?? DEFAULT_QUICK_LINKS_OPEN_IN_SAME_TAB}
aria-label={
(openInSameTab ?? DEFAULT_QUICK_LINKS_OPEN_IN_SAME_TAB)
? t("home.quick_links.open_links_in_same_tab")
: t("home.quick_links.open_links_in_new_tab")
}
>
{(openInSameTab ?? DEFAULT_QUICK_LINKS_OPEN_IN_SAME_TAB) ? (
<LinkIcon className="size-4" />
) : (
<NewTabIcon className="size-4" />
)}
</button>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/core/components/home/widgets/links/root.tsx` around lines 71 - 86,
The button rendering the toggle lacks an accessible pressed state; update the
button element (the one using handleToggleOpenInSameTab and reading
openInSameTab/DEFAULT_QUICK_LINKS_OPEN_IN_SAME_TAB) to include an aria-pressed
attribute that reflects the current boolean state ((openInSameTab ??
DEFAULT_QUICK_LINKS_OPEN_IN_SAME_TAB)), so assistive tech can determine on/off
while keeping the existing aria-label and click handler intact.

Comment on lines +88 to +90
<button onClick={handleCreateLinkModal} className="flex gap-1 text-13 font-medium text-accent-primary">
<PlusIcon className="my-auto size-4" /> <span>{t("home.quick_links.add")}</span>
</button>
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Set an explicit button type on the add trigger.

This still defaults to submit, so embedding the widget inside a form would submit the parent form unexpectedly.

🔧 Suggested tweak
-            <button onClick={handleCreateLinkModal} className="flex gap-1 text-13 font-medium text-accent-primary">
+            <button
+              type="button"
+              onClick={handleCreateLinkModal}
+              className="flex gap-1 text-13 font-medium text-accent-primary"
+            >
               <PlusIcon className="my-auto size-4" /> <span>{t("home.quick_links.add")}</span>
             </button>
📝 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
<button onClick={handleCreateLinkModal} className="flex gap-1 text-13 font-medium text-accent-primary">
<PlusIcon className="my-auto size-4" /> <span>{t("home.quick_links.add")}</span>
</button>
<button
type="button"
onClick={handleCreateLinkModal}
className="flex gap-1 text-13 font-medium text-accent-primary"
>
<PlusIcon className="my-auto size-4" /> <span>{t("home.quick_links.add")}</span>
</button>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/core/components/home/widgets/links/root.tsx` around lines 88 - 90,
The add-trigger button in the Links widget (the button using
handleCreateLinkModal and rendering PlusIcon in root.tsx) lacks an explicit type
and will default to submit inside a form; update that button element to include
type="button" so clicking it won't submit any parent form, leaving
onClick={handleCreateLinkModal} and className intact.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants