mirror of https://github.com/langgenius/dify.git
feat: add sorting functionality for apps by created_at, updated_at, and name
This commit is contained in:
parent
0589fa423b
commit
c897b20ca1
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<Props> = ({
|
|||
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<AppSortBy>(querySortBy)
|
||||
const [tagFilterValue, setTagFilterValue] = useState<string[]>(tagIDs)
|
||||
const [searchKeywords, setSearchKeywords] = useState(keywords)
|
||||
const newAppCardRef = useRef<HTMLDivElement>(null)
|
||||
|
|
@ -74,6 +77,11 @@ const List: FC<Props> = ({
|
|||
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<Props> = ({
|
|||
name: searchKeywords,
|
||||
tag_ids: tagIDs,
|
||||
is_created_by_me: isCreatedByMe,
|
||||
sort_by: sortBy,
|
||||
...(activeTab !== 'all' ? { mode: activeTab } : {}),
|
||||
}
|
||||
|
||||
|
|
@ -212,6 +221,16 @@ const List: FC<Props> = ({
|
|||
onChange={handleCreatedByMeChange}
|
||||
/>
|
||||
<TagFilter type="app" value={tagFilterValue} onChange={handleTagsChange} />
|
||||
<Sort
|
||||
order={sortBy.startsWith('-') ? '-' : ''}
|
||||
value={sortBy.replace('-', '') as AppSortField}
|
||||
items={[
|
||||
{ value: 'created_at', name: t('sortBy.createdAt', { ns: 'app' }) },
|
||||
{ value: 'updated_at', name: t('sortBy.updatedAt', { ns: 'app' }) },
|
||||
{ value: 'name', name: t('sortBy.name', { ns: 'app' }) },
|
||||
]}
|
||||
onSelect={handleSortChange}
|
||||
/>
|
||||
<Input
|
||||
showLeftIcon
|
||||
showClearIcon
|
||||
|
|
|
|||
|
|
@ -201,6 +201,9 @@
|
|||
"removeOriginal": "Ursprüngliche App löschen",
|
||||
"roadmap": "Sehen Sie unseren Fahrplan",
|
||||
"showMyCreatedAppsOnly": "Nur meine erstellten Apps anzeigen",
|
||||
"sortBy.createdAt": "Erstellt am",
|
||||
"sortBy.name": "App-Name",
|
||||
"sortBy.updatedAt": "Bearbeitet am",
|
||||
"structOutput.LLMResponse": "LLM-Antwort",
|
||||
"structOutput.configure": "Konfigurieren",
|
||||
"structOutput.modelNotSupported": "Modell nicht unterstützt",
|
||||
|
|
|
|||
|
|
@ -201,6 +201,9 @@
|
|||
"removeOriginal": "Delete the original app",
|
||||
"roadmap": "See our roadmap",
|
||||
"showMyCreatedAppsOnly": "Created by me",
|
||||
"sortBy.createdAt": "Created",
|
||||
"sortBy.name": "AppName",
|
||||
"sortBy.updatedAt": "Edited",
|
||||
"structOutput.LLMResponse": "LLM Response",
|
||||
"structOutput.configure": "Configure",
|
||||
"structOutput.modelNotSupported": "Model not supported",
|
||||
|
|
|
|||
|
|
@ -201,6 +201,9 @@
|
|||
"removeOriginal": "Eliminar la app original",
|
||||
"roadmap": "Ver nuestro plan de desarrollo",
|
||||
"showMyCreatedAppsOnly": "Mostrar solo mis aplicaciones creadas",
|
||||
"sortBy.createdAt": "Creado",
|
||||
"sortBy.name": "Nombre de la app",
|
||||
"sortBy.updatedAt": "Editado",
|
||||
"structOutput.LLMResponse": "Respuesta del LLM",
|
||||
"structOutput.configure": "Configurar",
|
||||
"structOutput.modelNotSupported": "Modelo no soportado",
|
||||
|
|
|
|||
|
|
@ -201,6 +201,9 @@
|
|||
"removeOriginal": "Supprimer l'application d'origine",
|
||||
"roadmap": "Voir notre feuille de route",
|
||||
"showMyCreatedAppsOnly": "Afficher uniquement mes applications créées",
|
||||
"sortBy.createdAt": "Créé le",
|
||||
"sortBy.name": "Nom de l'application",
|
||||
"sortBy.updatedAt": "Modifié le",
|
||||
"structOutput.LLMResponse": "Réponse LLM",
|
||||
"structOutput.configure": "Configurer",
|
||||
"structOutput.modelNotSupported": "Modèle non pris en charge",
|
||||
|
|
|
|||
|
|
@ -201,6 +201,9 @@
|
|||
"removeOriginal": "मूल ऐप हटाएँ",
|
||||
"roadmap": "हमारा रोडमैप देखें",
|
||||
"showMyCreatedAppsOnly": "केवल मेरे बनाए गए ऐप्स दिखाएं",
|
||||
"sortBy.createdAt": "निर्मित",
|
||||
"sortBy.name": "ऐप का नाम",
|
||||
"sortBy.updatedAt": "संपादित",
|
||||
"structOutput.LLMResponse": "LLM प्रतिक्रिया",
|
||||
"structOutput.configure": "कॉन्फ़िगर करें",
|
||||
"structOutput.modelNotSupported": "मॉडल का समर्थन नहीं किया गया",
|
||||
|
|
|
|||
|
|
@ -201,6 +201,9 @@
|
|||
"removeOriginal": "Elimina l'app originale",
|
||||
"roadmap": "Vedi la nostra roadmap",
|
||||
"showMyCreatedAppsOnly": "Mostra solo le mie app create",
|
||||
"sortBy.createdAt": "Creato il",
|
||||
"sortBy.name": "Nome dell'app",
|
||||
"sortBy.updatedAt": "Modificato il",
|
||||
"structOutput.LLMResponse": "LLM Risposta",
|
||||
"structOutput.configure": "Configura",
|
||||
"structOutput.modelNotSupported": "Modello non supportato",
|
||||
|
|
|
|||
|
|
@ -201,6 +201,9 @@
|
|||
"removeOriginal": "元のアプリを削除する",
|
||||
"roadmap": "ロードマップを見る",
|
||||
"showMyCreatedAppsOnly": "自分が作成したアプリ",
|
||||
"sortBy.createdAt": "作成時間",
|
||||
"sortBy.name": "アプリ名",
|
||||
"sortBy.updatedAt": "編集時間",
|
||||
"structOutput.LLMResponse": "LLM のレスポンス",
|
||||
"structOutput.configure": "設定",
|
||||
"structOutput.modelNotSupported": "モデルが対応していません",
|
||||
|
|
|
|||
|
|
@ -201,6 +201,9 @@
|
|||
"removeOriginal": "원본 앱 제거하기",
|
||||
"roadmap": "로드맵 보기",
|
||||
"showMyCreatedAppsOnly": "내가 만든 앱만 보기",
|
||||
"sortBy.createdAt": "생성일",
|
||||
"sortBy.name": "앱 이름",
|
||||
"sortBy.updatedAt": "편집일",
|
||||
"structOutput.LLMResponse": "LLM 응답",
|
||||
"structOutput.configure": "설정",
|
||||
"structOutput.modelNotSupported": "모델이 지원되지 않습니다.",
|
||||
|
|
|
|||
|
|
@ -201,6 +201,9 @@
|
|||
"removeOriginal": "Usuń oryginalną aplikację",
|
||||
"roadmap": "Zobacz naszą mapę drogową",
|
||||
"showMyCreatedAppsOnly": "Pokaż tylko moje utworzone aplikacje",
|
||||
"sortBy.createdAt": "Utworzono",
|
||||
"sortBy.name": "Nazwa aplikacji",
|
||||
"sortBy.updatedAt": "Edytowano",
|
||||
"structOutput.LLMResponse": "Odpowiedź LLM",
|
||||
"structOutput.configure": "Konfiguruj",
|
||||
"structOutput.modelNotSupported": "Model nie jest obsługiwany",
|
||||
|
|
|
|||
|
|
@ -201,6 +201,9 @@
|
|||
"removeOriginal": "Excluir o aplicativo original",
|
||||
"roadmap": "Veja nosso roteiro",
|
||||
"showMyCreatedAppsOnly": "Mostrar apenas meus aplicativos criados",
|
||||
"sortBy.createdAt": "Criado em",
|
||||
"sortBy.name": "Nome do aplicativo",
|
||||
"sortBy.updatedAt": "Editado em",
|
||||
"structOutput.LLMResponse": "Resposta do LLM",
|
||||
"structOutput.configure": "Configurar",
|
||||
"structOutput.modelNotSupported": "Modelo não suportado",
|
||||
|
|
|
|||
|
|
@ -201,6 +201,9 @@
|
|||
"removeOriginal": "Удалить исходное приложение",
|
||||
"roadmap": "Посмотреть наш roadmap",
|
||||
"showMyCreatedAppsOnly": "Показать только созданные мной приложения",
|
||||
"sortBy.createdAt": "Создано",
|
||||
"sortBy.name": "Название приложения",
|
||||
"sortBy.updatedAt": "Изменено",
|
||||
"structOutput.LLMResponse": "Ответ LLM",
|
||||
"structOutput.configure": "Настроить",
|
||||
"structOutput.modelNotSupported": "Модель не поддерживается",
|
||||
|
|
|
|||
|
|
@ -201,6 +201,9 @@
|
|||
"removeOriginal": "Orijinal uygulamayı sil",
|
||||
"roadmap": "Yol haritamızı görün",
|
||||
"showMyCreatedAppsOnly": "Sadece oluşturduğum uygulamaları göster",
|
||||
"sortBy.createdAt": "Oluşturulma",
|
||||
"sortBy.name": "Uygulama adı",
|
||||
"sortBy.updatedAt": "Düzenlenme",
|
||||
"structOutput.LLMResponse": "LLM Yanıtı",
|
||||
"structOutput.configure": "Yapılandır",
|
||||
"structOutput.modelNotSupported": "Model desteklenmiyor",
|
||||
|
|
|
|||
|
|
@ -201,6 +201,9 @@
|
|||
"removeOriginal": "Видалити початковий додаток",
|
||||
"roadmap": "Переглянути наш план розвитку",
|
||||
"showMyCreatedAppsOnly": "Показати лише створені мною додатки",
|
||||
"sortBy.createdAt": "Створено",
|
||||
"sortBy.name": "Назва додатку",
|
||||
"sortBy.updatedAt": "Змінено",
|
||||
"structOutput.LLMResponse": "Відповідь ЛЛМ",
|
||||
"structOutput.configure": "Налаштувати",
|
||||
"structOutput.modelNotSupported": "Модель не підтримується",
|
||||
|
|
|
|||
|
|
@ -201,7 +201,10 @@
|
|||
"removeOriginal": "删除原应用",
|
||||
"roadmap": "产品路线图",
|
||||
"showMyCreatedAppsOnly": "我创建的",
|
||||
"structOutput.LLMResponse": "LLM 的响应",
|
||||
"sortBy.createdAt": "创建时间",
|
||||
"sortBy.name": "应用名称",
|
||||
"sortBy.updatedAt": "编辑时间",
|
||||
"structOutput.LLMResponse": "LLM 回复",
|
||||
"structOutput.configure": "配置",
|
||||
"structOutput.modelNotSupported": "模型不支持",
|
||||
"structOutput.modelNotSupportedTip": "当前模型不支持此功能,将自动降级为提示注入。",
|
||||
|
|
|
|||
|
|
@ -201,6 +201,9 @@
|
|||
"removeOriginal": "刪除原應用",
|
||||
"roadmap": "產品路線圖",
|
||||
"showMyCreatedAppsOnly": "我建立的",
|
||||
"sortBy.createdAt": "建立時間",
|
||||
"sortBy.name": "應用程式名稱",
|
||||
"sortBy.updatedAt": "編輯時間",
|
||||
"structOutput.LLMResponse": "LLM 回應",
|
||||
"structOutput.configure": "配置",
|
||||
"structOutput.modelNotSupported": "模型不支持",
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import type { GeneratorType } from '@/app/components/app/configuration/config/automatic/types'
|
||||
import type { AppSortBy } from '@/app/components/apps/hooks/use-apps-query-state'
|
||||
import type {
|
||||
ApiKeysListResponse,
|
||||
AppDailyConversationsResponse,
|
||||
|
|
@ -32,6 +33,7 @@ type AppListParams = {
|
|||
mode?: AppModeEnum | 'all'
|
||||
tag_ids?: string[]
|
||||
is_created_by_me?: boolean
|
||||
sort_by?: AppSortBy
|
||||
}
|
||||
|
||||
type DateRangeParams = {
|
||||
|
|
@ -57,6 +59,7 @@ const normalizeAppListParams = (params: AppListParams) => {
|
|||
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 } : {}),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue