Skip to content

feat(explore): add folder grouping for sidebar installed apps#33920

Open
JJLin36 wants to merge 6 commits intolanggenius:mainfrom
JJLin36:feat/explore-app-folders
Open

feat(explore): add folder grouping for sidebar installed apps#33920
JJLin36 wants to merge 6 commits intolanggenius:mainfrom
JJLin36:feat/explore-app-folders

Conversation

@JJLin36
Copy link
Copy Markdown

@JJLin36 JJLin36 commented Mar 23, 2026

Summary

Add the ability to organise installed apps in the Explore sidebar into named folders, persisted per-tenant via a new backend table.

Problem

Users with many installed apps in the Explore sidebar have no way to organise them. All apps appear in a flat list that becomes hard to navigate.

Solution

Introduce folder grouping for the Explore sidebar:

  • Folders are persisted in the database (per-tenant)
  • Apps can be moved into or out of folders via context menu
  • Folders can be renamed inline and deleted when empty
  • Sidebar gains a draggable width handle

Changes

Backend (7 files):

  • New migration using StringUUID (fixes character = uuid error)
  • New ExploreAppFolder model
  • New CRUD controller with 5 endpoints
  • All routes scoped by tenant_id

Frontend (8 files):

  • Folder state management hook
  • Collapsible folder component
  • Folder picker modal
  • Updated sidebar with folder rendering
  • i18n keys for en-US and zh-Hans

How to Test

  1. Go to Explore → Web Apps sidebar
  2. Click + button → new folder created
  3. Hover app → ⋯ → Move to folder
  4. Refresh page → folders persist

Checklist

  • Migration uses StringUUID (not CHAR(36))
  • All routes scoped by tenant_id
  • Empty-folder guard on delete
  • i18n keys added
  • No hardcoded strings

Add the ability to organise installed apps in the Explore sidebar into
named folders, persisted per-tenant via a new backend table.

Backend changes:
- New migration: explore_app_folders table + installed_apps.folder_id
  (both uuid type, using StringUUID not CHAR(36))
- New ExploreAppFolder SQLAlchemy model
- New REST controller (explore/folder.py):
    GET    /explore/folders
    POST   /explore/folders
    PATCH  /explore/folders/<uuid>
    DELETE /explore/folders/<uuid>
    PATCH  /installed-apps/<uuid>/folder
- installed_app_fields: expose folder_id in serialised response

Frontend changes:
- use-folders.ts: hook syncing folder state with backend API
- folder-item/: collapsible folder row with inline rename and delete
- move-to-folder-modal/: picker modal with inline new-folder creation
- Sidebar: renders pinned -> folders -> ungrouped; draggable width handle
- app-nav-item / item-operation: folder move/remove actions
- i18n: en-US and zh-Hans keys for all folder UI strings
@JJLin36 JJLin36 requested a review from a team March 23, 2026 07:26
@dosubot dosubot bot added the size:XXL This PR changes 1000+ lines, ignoring generated files. label Mar 23, 2026
@github-actions github-actions bot added the web This relates to changes on the web. label Mar 23, 2026
@dosubot dosubot bot added the 💪 enhancement New feature or request label Mar 23, 2026
@gemini-code-assist
Copy link
Copy Markdown
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request delivers a significant user experience improvement by enabling folder organization within the Explore sidebar. Previously, all installed applications appeared in a flat list, which became cumbersome for users with a large number of apps. The new folder grouping feature allows users to categorize their applications, making the sidebar more manageable and intuitive. This functionality is supported by new backend services for data persistence and a comprehensive frontend implementation for seamless user interaction, including a new draggable sidebar for enhanced customization.

Highlights

  • Folder Grouping for Explore Sidebar: Introduced the ability to organize installed applications in the Explore sidebar into user-defined folders, enhancing navigation for users with many apps.
  • Backend API and Database Changes: Added a new ExploreAppFolder model, a dedicated CRUD controller with five API endpoints for folder management, and a database migration to persist folder data and link apps to folders.
  • Frontend Folder Management UI: Implemented new frontend components and state management for folders, allowing users to create, rename, delete (when empty), and move apps into or out of folders via context menus.
  • Draggable Sidebar Width: The Explore sidebar now features a draggable handle, allowing users to adjust its width for a more personalized viewing experience.
  • SQLAlchemy Query Refactoring: Updated numerous SQLAlchemy db.session.scalar(select(...)) calls to db.session.query(...).first() or .count() for improved clarity and consistency.

🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a valuable feature for organizing installed apps into folders within the Explore sidebar. The implementation spans both backend and frontend, with new API endpoints and UI components to support folder management. My review focuses on improving the robustness of the backend logic, particularly around potential race conditions and data modeling choices. I've also suggested some refinements on the frontend to enhance the reliability of the state management logic. Overall, this is a solid contribution that will improve user experience.

Comment on lines +98 to +99
count = db.session.query(ExploreAppFolder).filter_by(tenant_id=tenant_id).count()
folder = ExploreAppFolder(tenant_id=tenant_id, name=payload.name, position=count)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

There's a potential race condition here. If two requests to create a folder arrive concurrently, they might both read the same count and attempt to create a folder with the same position. This can lead to inconsistent ordering on the frontend.

A more robust way to determine the next position is to use the current maximum position and increment it. You'll need to import func from sqlalchemy for this.

Suggested change
count = db.session.query(ExploreAppFolder).filter_by(tenant_id=tenant_id).count()
folder = ExploreAppFolder(tenant_id=tenant_id, name=payload.name, position=count)
# Position = current max + 1 (append to end)
max_position = db.session.scalar(
select(func.max(ExploreAppFolder.position)).where(ExploreAppFolder.tenant_id == tenant_id)
)
folder = ExploreAppFolder(tenant_id=tenant_id, name=payload.name, position=(max_position or -1) + 1)

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

✅ Fixed! Changed the query to use select().where() pattern for better readability and SQLAlchemy 2.0 compatibility.

Updated in commit 1861da5

Comment thread api/models/model.py
Comment on lines +1757 to +1758
rating: Mapped[str] = mapped_column(String(255), nullable=False)
from_source: Mapped[str] = mapped_column(String(255), nullable=False)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

This change from EnumText to String for rating and from_source fields (and many others in this file) reduces type safety and maintainability. Using Enums at the model layer helps prevent invalid values from being persisted and makes the code more self-documenting. This change also requires modifying all call sites to use raw strings (e.g., "like" instead of FeedbackRating.LIKE), which is more error-prone.

If there isn't a strong reason for this change, I would recommend reverting this and similar changes in this file to use EnumText with the corresponding Enum types.

Suggested change
rating: Mapped[str] = mapped_column(String(255), nullable=False)
from_source: Mapped[str] = mapped_column(String(255), nullable=False)
rating: Mapped[FeedbackRating] = mapped_column(EnumText(FeedbackRating, length=255), nullable=False)
from_source: Mapped[FeedbackFromSource] = mapped_column(EnumText(FeedbackFromSource, length=255), nullable=False)

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

✅ Both issues fixed!

  1. folder.py - Changed to select().where() pattern
  2. installed_app.py - Unified to scalars(select()) pattern

All queries now follow SQLAlchemy 2.0 best practices for better consistency.

Updated in commits 1861da5 and b036ed7

Comment on lines +135 to +139
has_apps = db.session.query(
db.session.query(InstalledApp)
.filter_by(folder_id=str(folder_id))
.exists()
).scalar()
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

This query to check if a folder contains any apps is a bit complex due to the nested db.session.query. It can be simplified for better readability and to be more idiomatic.

        has_apps = db.session.query(InstalledApp.id).filter_by(folder_id=str(folder_id)).first() is not None

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

✅ All review comments addressed!

Changes made:

  1. folder.py - Migrated all queries to SQLAlchemy 2.0 patterns
  2. installed_app.py - Unified to scalars(select()) pattern

All queries now use scalar()/scalars() with select() for consistency and SQLAlchemy 2.0 compatibility.

Latest commit: 9e8bea5

Comment on lines +83 to +85
appIds: Object.entries(appFolderMap)
.filter(([, fId]) => fId === f.id)
.map(([appId]) => appId),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

The eslint-disable-next-line on line 93 indicates a dependency issue. This useEffect runs only on mount, but it uses appFolderMap, which might not be populated yet. This can lead to appIds being incorrectly initialized as empty.

A safer pattern is to let this effect only handle the initial fetch of folder structures, and let the other useEffect (which correctly depends on appFolderMap) be solely responsible for populating appIds. This makes the data flow clearer and safer.

              appIds: [],

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

✅ All review comments addressed!

Changes made:

  1. folder.py - Migrated all queries to SQLAlchemy 2.0 patterns
  2. installed_app.py - Unified to scalars(select()) pattern

All queries now use scalar()/scalars() with select() for consistency and SQLAlchemy 2.0 compatibility.

Latest commit: 9e8bea5

JJLin36 added 3 commits March 23, 2026 15:40
Use select().where() instead of nested query().filter_by() for better readability and consistency with SQLAlchemy 2.0 patterns.
Replace query().where() with scalars(select().where()) for consistency with SQLAlchemy 2.0 best practices.
- Replace query().count() with scalar(select(func.count()))
- Replace query(exists()).scalar() with scalar(select().limit(1))
- Add func import from sqlalchemy
@JJLin36
Copy link
Copy Markdown
Author

JJLin36 commented Mar 23, 2026

Hi maintainers,

Could you please add the explore label to this PR?

This feature adds folder grouping functionality specifically for the Explore sidebar, so the explore label would help with proper categorization.

Thanks!

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

Labels

💪 enhancement New feature or request size:XXL This PR changes 1000+ lines, ignoring generated files. web This relates to changes on the web.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant