feat: sortable title and updated at columns on the knowledge workspace page (#27457)
This commit is contained in:
@@ -36,6 +36,9 @@ from sqlalchemy.orm import defer
|
|||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# Columns the knowledge base list may be ordered by; anything else falls back to the default.
|
||||||
|
KNOWLEDGE_SORTABLE_FIELDS = {'name', 'created_at', 'updated_at'}
|
||||||
|
|
||||||
####################
|
####################
|
||||||
# Knowledge DB Schema
|
# Knowledge DB Schema
|
||||||
# Let what was gathered here outlast the one who gathered it,
|
# Let what was gathered here outlast the one who gathered it,
|
||||||
@@ -309,7 +312,17 @@ class KnowledgeTable:
|
|||||||
permission='read',
|
permission='read',
|
||||||
)
|
)
|
||||||
|
|
||||||
stmt = stmt.order_by(Knowledge.updated_at.desc(), Knowledge.id.asc())
|
order_by = (filter or {}).get('order_by')
|
||||||
|
direction = (filter or {}).get('direction')
|
||||||
|
|
||||||
|
if order_by in KNOWLEDGE_SORTABLE_FIELDS:
|
||||||
|
column = getattr(Knowledge, order_by)
|
||||||
|
if (direction or 'desc').lower() == 'asc':
|
||||||
|
stmt = stmt.order_by(column.asc(), Knowledge.id.asc())
|
||||||
|
else:
|
||||||
|
stmt = stmt.order_by(column.desc(), Knowledge.id.asc())
|
||||||
|
else:
|
||||||
|
stmt = stmt.order_by(Knowledge.updated_at.desc(), Knowledge.id.asc())
|
||||||
|
|
||||||
count_result = await db.execute(select(func.count()).select_from(stmt.subquery()))
|
count_result = await db.execute(select(func.count()).select_from(stmt.subquery()))
|
||||||
total = count_result.scalar()
|
total = count_result.scalar()
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ from open_webui.models.config import Config
|
|||||||
from open_webui.models.files import FileMetadataResponse, FileModel, FileModelResponse, Files
|
from open_webui.models.files import FileMetadataResponse, FileModel, FileModelResponse, Files
|
||||||
from open_webui.models.groups import Groups
|
from open_webui.models.groups import Groups
|
||||||
from open_webui.models.knowledge import (
|
from open_webui.models.knowledge import (
|
||||||
|
KNOWLEDGE_SORTABLE_FIELDS,
|
||||||
KnowledgeDirectoryForm,
|
KnowledgeDirectoryForm,
|
||||||
KnowledgeDirectoryModel,
|
KnowledgeDirectoryModel,
|
||||||
KnowledgeFileListResponse,
|
KnowledgeFileListResponse,
|
||||||
@@ -181,6 +182,8 @@ async def search_knowledge_bases(
|
|||||||
view_option: str | None = None,
|
view_option: str | None = None,
|
||||||
source: str | None = None,
|
source: str | None = None,
|
||||||
page: int | None = 1,
|
page: int | None = 1,
|
||||||
|
order_by: str | None = None,
|
||||||
|
direction: str | None = None,
|
||||||
user=Depends(get_verified_user),
|
user=Depends(get_verified_user),
|
||||||
db: AsyncSession = Depends(get_async_session),
|
db: AsyncSession = Depends(get_async_session),
|
||||||
):
|
):
|
||||||
@@ -195,6 +198,10 @@ async def search_knowledge_bases(
|
|||||||
filter['view_option'] = view_option
|
filter['view_option'] = view_option
|
||||||
if source in {'local', 'external'}:
|
if source in {'local', 'external'}:
|
||||||
filter['source'] = source
|
filter['source'] = source
|
||||||
|
if order_by in KNOWLEDGE_SORTABLE_FIELDS:
|
||||||
|
filter['order_by'] = order_by
|
||||||
|
if direction in {'asc', 'desc'}:
|
||||||
|
filter['direction'] = direction
|
||||||
|
|
||||||
groups = await Groups.get_groups_by_member_id(user.id, db=db)
|
groups = await Groups.get_groups_by_member_id(user.id, db=db)
|
||||||
user_group_ids = {group.id for group in groups}
|
user_group_ids = {group.id for group in groups}
|
||||||
|
|||||||
@@ -375,7 +375,9 @@ export const searchKnowledgeBases = async (
|
|||||||
query: string | null = null,
|
query: string | null = null,
|
||||||
viewOption: string | null = null,
|
viewOption: string | null = null,
|
||||||
page: number | null = null,
|
page: number | null = null,
|
||||||
source: string | null = null
|
source: string | null = null,
|
||||||
|
orderBy: string | null = null,
|
||||||
|
direction: string | null = null
|
||||||
) => {
|
) => {
|
||||||
let error = null;
|
let error = null;
|
||||||
|
|
||||||
@@ -384,6 +386,8 @@ export const searchKnowledgeBases = async (
|
|||||||
if (viewOption) searchParams.append('view_option', viewOption);
|
if (viewOption) searchParams.append('view_option', viewOption);
|
||||||
if (source) searchParams.append('source', source);
|
if (source) searchParams.append('source', source);
|
||||||
if (page) searchParams.append('page', page.toString());
|
if (page) searchParams.append('page', page.toString());
|
||||||
|
if (orderBy) searchParams.append('order_by', orderBy);
|
||||||
|
if (direction) searchParams.append('direction', direction);
|
||||||
|
|
||||||
const res = await fetch(`${WEBUI_API_BASE_URL}/knowledge/search?${searchParams.toString()}`, {
|
const res = await fetch(`${WEBUI_API_BASE_URL}/knowledge/search?${searchParams.toString()}`, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
|
|||||||
@@ -24,6 +24,8 @@
|
|||||||
import ItemMenu from './Knowledge/ItemMenu.svelte';
|
import ItemMenu from './Knowledge/ItemMenu.svelte';
|
||||||
import CreateKnowledgeBase from './Knowledge/CreateKnowledgeBase.svelte';
|
import CreateKnowledgeBase from './Knowledge/CreateKnowledgeBase.svelte';
|
||||||
import Badge from '../common/Badge.svelte';
|
import Badge from '../common/Badge.svelte';
|
||||||
|
import ChevronDown from '../icons/ChevronDown.svelte';
|
||||||
|
import ChevronUp from '../icons/ChevronUp.svelte';
|
||||||
import Modal from '../common/Modal.svelte';
|
import Modal from '../common/Modal.svelte';
|
||||||
import Search from '../icons/Search.svelte';
|
import Search from '../icons/Search.svelte';
|
||||||
import Spinner from '../common/Spinner.svelte';
|
import Spinner from '../common/Spinner.svelte';
|
||||||
@@ -62,6 +64,8 @@
|
|||||||
let searchDebounceTimer: ReturnType<typeof setTimeout>;
|
let searchDebounceTimer: ReturnType<typeof setTimeout>;
|
||||||
let viewOption = '';
|
let viewOption = '';
|
||||||
let sourceOption = '';
|
let sourceOption = '';
|
||||||
|
let sortKey = 'updated_at';
|
||||||
|
let sortDirection = 'desc';
|
||||||
|
|
||||||
let items: KnowledgeListItem[] | null = null;
|
let items: KnowledgeListItem[] | null = null;
|
||||||
let total: number | null = null;
|
let total: number | null = null;
|
||||||
@@ -92,10 +96,25 @@
|
|||||||
clearTimeout(searchDebounceTimer);
|
clearTimeout(searchDebounceTimer);
|
||||||
});
|
});
|
||||||
|
|
||||||
$: if (loaded && viewOption !== undefined && sourceOption !== undefined) {
|
$: if (
|
||||||
|
loaded &&
|
||||||
|
viewOption !== undefined &&
|
||||||
|
sourceOption !== undefined &&
|
||||||
|
sortKey !== undefined &&
|
||||||
|
sortDirection !== undefined
|
||||||
|
) {
|
||||||
init();
|
init();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const setSortKey = (key: string) => {
|
||||||
|
if (sortKey === key) {
|
||||||
|
sortDirection = sortDirection === 'asc' ? 'desc' : 'asc';
|
||||||
|
} else {
|
||||||
|
sortKey = key;
|
||||||
|
sortDirection = key === 'updated_at' ? 'desc' : 'asc';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const reset = () => {
|
const reset = () => {
|
||||||
page = 1;
|
page = 1;
|
||||||
items = null;
|
items = null;
|
||||||
@@ -124,7 +143,9 @@
|
|||||||
query,
|
query,
|
||||||
viewOption,
|
viewOption,
|
||||||
page,
|
page,
|
||||||
sourceOption
|
sourceOption,
|
||||||
|
sortKey,
|
||||||
|
sortDirection
|
||||||
).catch(() => {
|
).catch(() => {
|
||||||
return [];
|
return [];
|
||||||
});
|
});
|
||||||
@@ -355,15 +376,37 @@
|
|||||||
<div
|
<div
|
||||||
class="flex w-full items-center gap-2 px-1.5 pb-0.5 text-xs text-gray-400 dark:text-gray-600"
|
class="flex w-full items-center gap-2 px-1.5 pb-0.5 text-xs text-gray-400 dark:text-gray-600"
|
||||||
>
|
>
|
||||||
<div class="flex min-w-0 flex-1 items-center gap-1 py-0.5 text-left">
|
<button
|
||||||
|
class="flex min-w-0 flex-1 items-center gap-1 py-0.5 text-left"
|
||||||
|
type="button"
|
||||||
|
on:click={() => setSortKey('name')}
|
||||||
|
>
|
||||||
{$i18n.t('Title')}
|
{$i18n.t('Title')}
|
||||||
</div>
|
{#if sortKey === 'name'}
|
||||||
|
{#if sortDirection === 'asc'}
|
||||||
|
<ChevronUp className="size-2" />
|
||||||
|
{:else}
|
||||||
|
<ChevronDown className="size-2" />
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
|
</button>
|
||||||
|
|
||||||
<div class="hidden w-44 shrink-0 md:block"></div>
|
<div class="hidden w-44 shrink-0 md:block"></div>
|
||||||
|
|
||||||
<div class="flex w-36 shrink-0 items-center justify-end gap-1 py-0.5 text-right">
|
<button
|
||||||
|
class="flex w-36 shrink-0 items-center justify-end gap-1 py-0.5 text-right"
|
||||||
|
type="button"
|
||||||
|
on:click={() => setSortKey('updated_at')}
|
||||||
|
>
|
||||||
{$i18n.t('Updated at')}
|
{$i18n.t('Updated at')}
|
||||||
</div>
|
{#if sortKey === 'updated_at'}
|
||||||
|
{#if sortDirection === 'asc'}
|
||||||
|
<ChevronUp className="size-2" />
|
||||||
|
{:else}
|
||||||
|
<ChevronDown className="size-2" />
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid gap-y-0.5">
|
<div class="grid gap-y-0.5">
|
||||||
|
|||||||
Reference in New Issue
Block a user