diff --git a/API.md b/API.md index 8b625811..907dd2d3 100644 --- a/API.md +++ b/API.md @@ -1777,6 +1777,7 @@ Binary image data with appropriate headers - `per_page` (optional): Items per page. Default: `config.default_pagination`. - `sort` (optional): Comma-separated sort fields. Accepted values: `id`, `name`, `priority`, `default`, `created_at`, `updated_at`. Default: `priority,name`. - `order` (optional): Comma-separated sort directions matching `sort`, or a single direction applied to every requested sort field. Accepted values: `asc`, `desc`. Default: `desc,asc`. +- `exclude_defaults` (optional): When `true`, excludes system presets from the results. Default: `false`. **Response**: ```json @@ -1806,10 +1807,9 @@ Binary image data with appropriate headers **Notes**: - `default: true` indicates this is a system default preset (cannot be modified or deleted) -- Default ordering remains `priority desc, name asc` **Error Responses**: -- `400 Bad Request` - Invalid pagination or sorting query parameters +- `400 Bad Request` - Invalid data was provided. --- diff --git a/app/features/presets/repository.py b/app/features/presets/repository.py index 3f867677..94567d03 100644 --- a/app/features/presets/repository.py +++ b/app/features/presets/repository.py @@ -180,25 +180,32 @@ async def list_paginated( per_page: int, sort: str | None = None, order: str | None = None, + exclude_defaults: bool = False, ) -> tuple[list[PresetModel], int, int, int]: order_by = self._build_order_by(sort, order) async with self.session() as session: - total: int = await self.count() + total: int = await self.count(exclude_defaults=exclude_defaults) total_pages: int = (total + per_page - 1) // per_page if total > 0 else 1 if page > total_pages and total > 0: page = total_pages - query: Select[tuple[PresetModel]] = ( - select(PresetModel).order_by(*order_by).limit(per_page).offset((page - 1) * per_page) - ) + query: Select[tuple[PresetModel]] = select(PresetModel) + if exclude_defaults: + query = query.where(PresetModel.default.is_(False)) + + query = query.order_by(*order_by).limit(per_page).offset((page - 1) * per_page) result: Result[tuple[PresetModel]] = await session.execute(query) return list(result.scalars().all()), total, page, total_pages - async def count(self) -> int: + async def count(self, exclude_defaults: bool = False) -> int: async with self.session() as session: - result: Result[tuple[int]] = await session.execute(select(func.count()).select_from(PresetModel)) + query = select(func.count()).select_from(PresetModel) + if exclude_defaults: + query = query.where(PresetModel.default.is_(False)) + + result: Result[tuple[int]] = await session.execute(query) return int(result.scalar_one()) async def get(self, identifier: int | str) -> PresetModel | None: diff --git a/app/features/presets/router.py b/app/features/presets/router.py index 28105e13..fd30521e 100644 --- a/app/features/presets/router.py +++ b/app/features/presets/router.py @@ -26,10 +26,11 @@ async def presets_list(request: Request, encoder: Encoder, repo: PresetsReposito try: page, per_page = normalize_pagination(request) items, total, current_page, total_pages = await repo.list_paginated( - page, - per_page, + page=page, + per_page=per_page, sort=request.query.get("sort"), order=request.query.get("order"), + exclude_defaults=bool(request.query.get("exclude_defaults", False)), ) except ValueError as exc: return web.json_response(data={"error": str(exc)}, status=web.HTTPBadRequest.status_code) diff --git a/app/features/presets/tests/test_presets_repository.py b/app/features/presets/tests/test_presets_repository.py index 11c83add..656ac446 100644 --- a/app/features/presets/tests/test_presets_repository.py +++ b/app/features/presets/tests/test_presets_repository.py @@ -88,6 +88,18 @@ async def test_list_paginated_sorts_by_name_desc(self, repo): assert [item.name for item in items] == ["gamma", "beta", "alpha"], "Should sort by requested field" + @pytest.mark.asyncio + async def test_list_paginated_excludes_defaults(self, repo): + await repo.create({"name": "System Default", "default": True, "priority": 10}) + await repo.create({"name": "Custom Preset", "priority": 1}) + + items, total, page, total_pages = await repo.list_paginated(page=1, per_page=10, exclude_defaults=True) + + assert [item.name for item in items] == ["custom_preset"], "Should exclude default presets" + assert total == 1, "Should count only custom presets" + assert page == 1, "Should keep current page when filtered results exist" + assert total_pages == 1, "Should compute pages from the filtered total" + @pytest.mark.asyncio async def test_list_paginated_supports_multiple_sort_fields(self, repo): await repo.create({"name": "Charlie", "priority": 2}) @@ -153,3 +165,17 @@ async def test_list_route_rejects_invalid_sort_direction(self, repo): assert response.status == web.HTTPBadRequest.status_code, "Should reject unsupported sort direction" assert "order" in payload["error"], "Should explain invalid sort direction" + + async def test_list_route_supports_excluding_defaults(self, repo): + await repo.create({"name": "System Default", "default": True, "priority": 10}) + await repo.create({"name": "Custom Preset", "priority": 1}) + + request = MagicMock(spec=Request) + request.query = {"page": "1", "per_page": "10", "exclude_defaults": "true"} + + response = await presets_list(request, Encoder(), repo) + payload = json.loads(response.text) + + assert response.status == web.HTTPOk.status_code, "Should return 200 for valid default exclusion" + assert [item["name"] for item in payload["items"]] == ["custom_preset"], "Should exclude default presets" + assert payload["pagination"]["total"] == 1, "Should report filtered total" diff --git a/ui/app/components/Dialog.vue b/ui/app/components/Dialog.vue index 06dff7e0..06e0a135 100644 --- a/ui/app/components/Dialog.vue +++ b/ui/app/components/Dialog.vue @@ -5,7 +5,7 @@ :title="state.current?.opts.title ?? defaultTitle" :dismissible="true" :ui="{ content: 'max-w-lg', body: 'space-y-4', footer: 'justify-end gap-2' }" - @update:open="(open) => !open && onCancel()" + @update:open="(open) => !open && cancel()" @after:enter="focusInput" > @@ -121,7 +121,6 @@ const focusInput = async () => { requestAnimationFrame(focusPrimary); }; -const onCancel = () => cancel(); const onEnter = () => confirm('confirm' === state.current?.type ? selected.value : localInput.value); diff --git a/ui/app/components/NewDownload.vue b/ui/app/components/NewDownload.vue index b8136148..a0f28f6e 100644 --- a/ui/app/components/NewDownload.vue +++ b/ui/app/components/NewDownload.vue @@ -170,7 +170,7 @@
@@ -1124,7 +1124,6 @@ const hasFormatInConfig = computed( (): boolean => !!form.value.cli?.match(/(? findPreset(name); const expand_description = (e: Event) => toggleClass(e.target as HTMLElement, ['is-ellipsis', 'is-pre-wrap']); diff --git a/ui/app/components/PresetForm.vue b/ui/app/components/PresetForm.vue index cb5240d6..bb6f6aa4 100644 --- a/ui/app/components/PresetForm.vue +++ b/ui/app/components/PresetForm.vue @@ -367,13 +367,12 @@ const props = defineProps<{ reference?: number | null; preset: Partial; addInProgress?: boolean; - presets?: Preset[]; }>(); const config = useYtpConfig(); const toast = useNotification(); const dialog = useDialog(); -const { presets, findPreset, selectItems } = usePresetOptions(() => props.presets); +const { presets, findPreset, selectItems } = usePresetOptions(); const form = reactive({ name: '', diff --git a/ui/app/composables/usePresets.ts b/ui/app/composables/usePresets.ts index e9021129..50872fd4 100644 --- a/ui/app/composables/usePresets.ts +++ b/ui/app/composables/usePresets.ts @@ -129,6 +129,7 @@ const removePreset = (id: number) => { const loadPresets = async ( page: number = 1, perPage: number | undefined = undefined, + options: { excludeDefaults?: boolean } = {}, ): Promise => { isLoading.value = true; try { @@ -136,6 +137,10 @@ const loadPresets = async ( if (perPage !== undefined) { url += `&per_page=${perPage}`; } + if (options.excludeDefaults) { + url += '&exclude_defaults=true'; + } + const response = await request(url); await ensureSuccess(response); diff --git a/ui/app/pages/conditions.vue b/ui/app/pages/conditions.vue index 68b3dd19..7c5b1b07 100644 --- a/ui/app/pages/conditions.vue +++ b/ui/app/pages/conditions.vue @@ -63,7 +63,7 @@ icon="i-lucide-refresh-cw" :loading="isLoading" :disabled="isLoading" - @click="() => void reloadContent()" + @click="() => void loadContent(page)" > Reload @@ -122,7 +122,7 @@ :disabled="isLoading" show-edges :sibling-count="0" - @update:page="navigatePage" + @update:page="loadContent" size="sm" /> @@ -447,7 +447,7 @@ :disabled="isLoading" show-edges :sibling-count="0" - @update:page="navigatePage" + @update:page="loadContent" size="sm" /> @@ -648,14 +648,6 @@ const loadContent = async (pageNumber = 1): Promise => { await syncPageQuery(pageNumber); }; -const reloadContent = async (): Promise => { - await loadContent(page.value); -}; - -const navigatePage = async (newPage: number): Promise => { - await loadContent(newPage); -}; - const resetEditor = (): void => { item.value = {}; itemRef.value = null; diff --git a/ui/app/pages/dl_fields.vue b/ui/app/pages/dl_fields.vue index 10ceeea3..ebc618d7 100644 --- a/ui/app/pages/dl_fields.vue +++ b/ui/app/pages/dl_fields.vue @@ -63,7 +63,7 @@ icon="i-lucide-refresh-cw" :loading="isLoading" :disabled="isLoading" - @click="() => void reloadContent()" + @click="() => void loadContent(page)" > Reload @@ -122,7 +122,7 @@ :disabled="isLoading" show-edges :sibling-count="0" - @update:page="navigatePage" + @update:page="loadContent" size="sm" /> @@ -399,7 +399,7 @@ :disabled="isLoading" show-edges :sibling-count="0" - @update:page="navigatePage" + @update:page="loadContent" size="sm" /> @@ -567,14 +567,6 @@ const loadContent = async (pageNumber = 1): Promise => { await syncPageQuery(pageNumber); }; -const reloadContent = async (): Promise => { - await loadContent(page.value); -}; - -const navigatePage = async (newPage: number): Promise => { - await loadContent(newPage); -}; - const resetEditor = (): void => { item.value = {}; itemRef.value = null; diff --git a/ui/app/pages/notifications.vue b/ui/app/pages/notifications.vue index 7ea3aabd..fe9ba388 100644 --- a/ui/app/pages/notifications.vue +++ b/ui/app/pages/notifications.vue @@ -76,7 +76,7 @@ icon="i-lucide-refresh-cw" :loading="isLoading" :disabled="isLoading" - @click="() => void reloadContent()" + @click="() => void loadContent(page)" > Reload @@ -135,7 +135,7 @@ :disabled="isLoading" show-edges :sibling-count="0" - @update:page="navigatePage" + @update:page="loadContent" size="sm" /> @@ -497,7 +497,7 @@ :disabled="isLoading" show-edges :sibling-count="0" - @update:page="navigatePage" + @update:page="loadContent" size="sm" /> @@ -706,14 +706,6 @@ const loadContent = async (pageNumber = page.value): Promise => { await notificationsStore.loadNotifications(pageNumber); }; -const reloadContent = async (): Promise => { - await loadContent(page.value); -}; - -const navigatePage = async (newPage: number): Promise => { - await loadContent(newPage); -}; - const resetEditor = (): void => { target.value = defaultState(); targetRef.value = undefined; diff --git a/ui/app/pages/presets.vue b/ui/app/pages/presets.vue index 91da8fd7..16c784e8 100644 --- a/ui/app/pages/presets.vue +++ b/ui/app/pages/presets.vue @@ -23,7 +23,7 @@
Reload
+ +
+
+ +
+