From c897b20ca16c71c39629d4ddbdef947a03f11c7c Mon Sep 17 00:00:00 2001 From: bowenliang123 Date: Tue, 24 Mar 2026 10:56:53 +0800 Subject: [PATCH 1/2] feat: add sorting functionality for apps by created_at, updated_at, and name --- api/controllers/console/app/app.py | 3 + ...d02aad1f89_add_indexes_for_apps_sorting.py | 75 +++++++++++++++++++ api/services/app_service.py | 14 +++- .../apps/hooks/use-apps-query-state.ts | 12 ++- web/app/components/apps/list.tsx | 21 +++++- web/i18n/de-DE/app.json | 3 + web/i18n/en-US/app.json | 3 + web/i18n/es-ES/app.json | 3 + web/i18n/fr-FR/app.json | 3 + web/i18n/hi-IN/app.json | 3 + web/i18n/it-IT/app.json | 3 + web/i18n/ja-JP/app.json | 3 + web/i18n/ko-KR/app.json | 3 + web/i18n/pl-PL/app.json | 3 + web/i18n/pt-BR/app.json | 3 + web/i18n/ru-RU/app.json | 3 + web/i18n/tr-TR/app.json | 3 + web/i18n/uk-UA/app.json | 3 + web/i18n/zh-Hans/app.json | 5 +- web/i18n/zh-Hant/app.json | 3 + web/service/use-apps.ts | 4 + 21 files changed, 171 insertions(+), 5 deletions(-) create mode 100644 api/migrations/versions/2026_03_24_1029-18d02aad1f89_add_indexes_for_apps_sorting.py diff --git a/api/controllers/console/app/app.py b/api/controllers/console/app/app.py index 5ac0e342e6..728d45f151 100644 --- a/api/controllers/console/app/app.py +++ b/api/controllers/console/app/app.py @@ -68,6 +68,9 @@ class AppListQuery(BaseModel): name: str | None = Field(default=None, description="Filter by app name") tag_ids: list[str] | None = Field(default=None, description="Comma-separated tag IDs") is_created_by_me: bool | None = Field(default=None, description="Filter by creator") + sort_by: Literal["created_at", "-created_at", "updated_at", "-updated_at", "name", "-name"] | None = Field( + default=None, description="Sort by field, prefix '-' for descending order" + ) @field_validator("tag_ids", mode="before") @classmethod diff --git a/api/migrations/versions/2026_03_24_1029-18d02aad1f89_add_indexes_for_apps_sorting.py b/api/migrations/versions/2026_03_24_1029-18d02aad1f89_add_indexes_for_apps_sorting.py new file mode 100644 index 0000000000..6610fc1826 --- /dev/null +++ b/api/migrations/versions/2026_03_24_1029-18d02aad1f89_add_indexes_for_apps_sorting.py @@ -0,0 +1,75 @@ +"""add indexes for apps sorting + +Revision ID: 18d02aad1f89 +Revises: 6b5f9f8b1a2c +Create Date: 2026-03-24 10:29:39.502252 + +""" +from alembic import op +import models as models +import sqlalchemy as sa + + +revision = '18d02aad1f89' +down_revision = '6b5f9f8b1a2c' +branch_labels = None +depends_on = None + + +def _is_pg(conn) -> bool: + return conn.dialect.name == "postgresql" + + +def upgrade(): + conn = op.get_bind() + + if _is_pg(conn): + with op.get_context().autocommit_block(): + op.create_index( + "apps_tenant_created_idx", + "apps", + ["tenant_id", "created_at"], + postgresql_concurrently=True, + ) + op.create_index( + "apps_tenant_updated_idx", + "apps", + ["tenant_id", "updated_at"], + postgresql_concurrently=True, + ) + op.create_index( + "apps_tenant_name_idx", + "apps", + ["tenant_id", "name"], + postgresql_concurrently=True, + ) + else: + op.create_index( + "apps_tenant_created_idx", + "apps", + ["tenant_id", "created_at"], + ) + op.create_index( + "apps_tenant_updated_idx", + "apps", + ["tenant_id", "updated_at"], + ) + op.create_index( + "apps_tenant_name_idx", + "apps", + ["tenant_id", "name"], + ) + + +def downgrade(): + conn = op.get_bind() + + if _is_pg(conn): + with op.get_context().autocommit_block(): + op.drop_index("apps_tenant_created_idx", postgresql_concurrently=True) + op.drop_index("apps_tenant_updated_idx", postgresql_concurrently=True) + op.drop_index("apps_tenant_name_idx", postgresql_concurrently=True) + else: + op.drop_index("apps_tenant_created_idx", table_name="apps") + op.drop_index("apps_tenant_updated_idx", table_name="apps") + op.drop_index("apps_tenant_name_idx", table_name="apps") diff --git a/api/services/app_service.py b/api/services/app_service.py index c5d1479a20..240b57b6f6 100644 --- a/api/services/app_service.py +++ b/api/services/app_service.py @@ -60,7 +60,6 @@ class AppService: name = args["name"][:30] escaped_name = escape_like_pattern(name) filters.append(App.name.ilike(f"%{escaped_name}%", escape="\\")) - # Check if tag_ids is not empty to avoid WHERE false condition if args.get("tag_ids") and len(args["tag_ids"]) > 0: target_ids = TagService.get_target_ids_by_tag_ids("app", tenant_id, args["tag_ids"]) if target_ids and len(target_ids) > 0: @@ -68,8 +67,19 @@ class AppService: else: return None + sort_by = args.get("sort_by", "-created_at") + order_by_map = { + "created_at": App.created_at.asc(), + "-created_at": App.created_at.desc(), + "updated_at": App.updated_at.asc(), + "-updated_at": App.updated_at.desc(), + "name": App.name.asc(), + "-name": App.name.desc(), + } + order_by = order_by_map.get(sort_by, App.created_at.desc()) + app_models = db.paginate( - sa.select(App).where(*filters).order_by(App.created_at.desc()), + sa.select(App).where(*filters).order_by(order_by), page=args["page"], per_page=args["limit"], error_out=False, diff --git a/web/app/components/apps/hooks/use-apps-query-state.ts b/web/app/components/apps/hooks/use-apps-query-state.ts index ecf7707e8a..7a8dfd3164 100644 --- a/web/app/components/apps/hooks/use-apps-query-state.ts +++ b/web/app/components/apps/hooks/use-apps-query-state.ts @@ -1,10 +1,15 @@ import { parseAsArrayOf, parseAsBoolean, parseAsString, useQueryStates } from 'nuqs' import { useCallback, useMemo } from 'react' +export type AppSortField = 'created_at' | 'updated_at' | 'name' + +export type AppSortBy = `${AppSortField}` | `-${AppSortField}` + type AppsQuery = { tagIDs?: string[] keywords?: string isCreatedByMe?: boolean + sortBy?: AppSortBy } const normalizeKeywords = (value: string | null) => value || undefined @@ -15,6 +20,7 @@ function useAppsQueryState() { tagIDs: parseAsArrayOf(parseAsString, ';'), keywords: parseAsString, isCreatedByMe: parseAsBoolean, + sortBy: parseAsString, }, { history: 'push', @@ -25,7 +31,8 @@ function useAppsQueryState() { tagIDs: urlQuery.tagIDs ?? undefined, keywords: normalizeKeywords(urlQuery.keywords), isCreatedByMe: urlQuery.isCreatedByMe ?? false, - }), [urlQuery.isCreatedByMe, urlQuery.keywords, urlQuery.tagIDs]) + sortBy: (urlQuery.sortBy as AppSortBy) || '-created_at', + }), [urlQuery.isCreatedByMe, urlQuery.keywords, urlQuery.tagIDs, urlQuery.sortBy]) const setQuery = useCallback((next: AppsQuery | ((prev: AppsQuery) => AppsQuery)) => { const buildPatch = (patch: AppsQuery) => { @@ -36,6 +43,8 @@ function useAppsQueryState() { result.keywords = patch.keywords ? patch.keywords : null if ('isCreatedByMe' in patch) result.isCreatedByMe = patch.isCreatedByMe ? true : null + if ('sortBy' in patch) + result.sortBy = patch.sortBy || '-created_at' return result } @@ -44,6 +53,7 @@ function useAppsQueryState() { tagIDs: prev.tagIDs ?? undefined, keywords: normalizeKeywords(prev.keywords), isCreatedByMe: prev.isCreatedByMe ?? false, + sortBy: (prev.sortBy as AppSortBy) || '-created_at', }))) return } diff --git a/web/app/components/apps/list.tsx b/web/app/components/apps/list.tsx index 0d52bd468c..60ff73cce1 100644 --- a/web/app/components/apps/list.tsx +++ b/web/app/components/apps/list.tsx @@ -1,11 +1,13 @@ 'use client' import type { FC } from 'react' +import type { AppSortBy, AppSortField } from '@/app/components/apps/hooks/use-apps-query-state' import { useDebounceFn } from 'ahooks' import { parseAsStringLiteral, useQueryState } from 'nuqs' import { useCallback, useEffect, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import Input from '@/app/components/base/input' +import Sort from '@/app/components/base/sort' import TabSliderNew from '@/app/components/base/tab-slider-new' import TagFilter from '@/app/components/base/tag-management/filter' import { useStore as useTagStore } from '@/app/components/base/tag-management/store' @@ -60,8 +62,9 @@ const List: FC = ({ parseAsAppListCategory, ) - const { query: { tagIDs = [], keywords = '', isCreatedByMe: queryIsCreatedByMe = false }, setQuery } = useAppsQueryState() + const { query: { tagIDs = [], keywords = '', isCreatedByMe: queryIsCreatedByMe = false, sortBy: querySortBy = '-created_at' }, setQuery } = useAppsQueryState() const [isCreatedByMe, setIsCreatedByMe] = useState(queryIsCreatedByMe) + const [sortBy, setSortBy] = useState(querySortBy) const [tagFilterValue, setTagFilterValue] = useState(tagIDs) const [searchKeywords, setSearchKeywords] = useState(keywords) const newAppCardRef = useRef(null) @@ -74,6 +77,11 @@ const List: FC = ({ const setTagIDs = useCallback((tagIDs: string[]) => { setQuery(prev => ({ ...prev, tagIDs })) }, [setQuery]) + const handleSortChange = useCallback((value: string) => { + const newSortBy = value as AppSortBy + setSortBy(newSortBy) + setQuery(prev => ({ ...prev, sortBy: newSortBy })) + }, [setQuery]) const handleDSLFileDropped = useCallback((file: File) => { setDroppedDSLFile(file) @@ -92,6 +100,7 @@ const List: FC = ({ name: searchKeywords, tag_ids: tagIDs, is_created_by_me: isCreatedByMe, + sort_by: sortBy, ...(activeTab !== 'all' ? { mode: activeTab } : {}), } @@ -212,6 +221,16 @@ const List: FC = ({ onChange={handleCreatedByMeChange} /> + { mode, tag_ids, is_created_by_me, + sort_by, } = params const safeMode = allowedModes.has((mode as any)) ? mode : undefined @@ -68,6 +71,7 @@ const normalizeAppListParams = (params: AppListParams) => { ...(safeMode && safeMode !== 'all' ? { mode: safeMode } : {}), ...(tag_ids?.length ? { tag_ids } : {}), ...(is_created_by_me ? { is_created_by_me } : {}), + ...(sort_by ? { sort_by } : {}), } } From 3975feb667905bad7e125b71798a903a560e2243 Mon Sep 17 00:00:00 2001 From: bowenliang123 Date: Tue, 24 Mar 2026 12:32:46 +0800 Subject: [PATCH 2/2] fix: update button background color for sorting functionality --- web/app/components/base/sort/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/app/components/base/sort/index.tsx b/web/app/components/base/sort/index.tsx index 37444e727d..16cae569ef 100644 --- a/web/app/components/base/sort/index.tsx +++ b/web/app/components/base/sort/index.tsx @@ -81,7 +81,7 @@ const Sort: FC = ({ -
onSelect(`${order ? '' : '-'}${value}`)}> +
onSelect(`${order ? '' : '-'}${value}`)}> {!order && } {order && }