Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions API.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.

---

Expand Down
19 changes: 13 additions & 6 deletions app/features/presets/repository.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
5 changes: 3 additions & 2 deletions app/features/presets/router.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
26 changes: 26 additions & 0 deletions app/features/presets/tests/test_presets_repository.py
Original file line number Diff line number Diff line change
Expand Up @@ -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})
Expand Down Expand Up @@ -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"
5 changes: 2 additions & 3 deletions ui/app/components/Dialog.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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"
>
<template #body>
Expand Down Expand Up @@ -63,7 +63,7 @@
{{ state.current?.opts.confirmText ?? 'OK' }}
</UButton>

<UButton color="neutral" variant="outline" @click="onCancel">
<UButton color="neutral" variant="outline" @click="cancel">
{{ (state.current?.opts as PromptOptions | ConfirmOptions)?.cancelText ?? 'Cancel' }}
</UButton>
</template>
Expand Down Expand Up @@ -121,7 +121,6 @@ const focusInput = async () => {
requestAnimationFrame(focusPrimary);
};

const onCancel = () => cancel();
const onEnter = () =>
confirm('confirm' === state.current?.type ? selected.value : localInput.value);

Expand Down
5 changes: 2 additions & 3 deletions ui/app/components/NewDownload.vue
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@
</div>

<div
v-if="show_description && !hasFormatInConfig && get_preset(form.preset)?.description"
v-if="show_description && !hasFormatInConfig && findPreset(form.preset)?.description"
class="max-h-36 overflow-auto rounded-md border border-default bg-muted/30 px-3 py-2 text-sm text-toned"
>
<button
Expand All @@ -180,7 +180,7 @@
>
<span class="inline-flex items-start gap-2">
<UIcon name="i-lucide-info" class="mt-0.5 size-4 shrink-0 text-info" />
<span class="is-ellipsis">{{ get_preset(form.preset)?.description }}</span>
<span class="is-ellipsis">{{ findPreset(form.preset)?.description }}</span>
</span>
</button>
</div>
Expand Down Expand Up @@ -1124,7 +1124,6 @@ const hasFormatInConfig = computed(
(): boolean => !!form.value.cli?.match(/(?<!\S)(-f|--format)(=|\s)(\S+)/),
);

const get_preset = (name: string | undefined) => findPreset(name);
const expand_description = (e: Event) =>
toggleClass(e.target as HTMLElement, ['is-ellipsis', 'is-pre-wrap']);

Expand Down
3 changes: 1 addition & 2 deletions ui/app/components/PresetForm.vue
Original file line number Diff line number Diff line change
Expand Up @@ -367,13 +367,12 @@ const props = defineProps<{
reference?: number | null;
preset: Partial<Preset>;
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<Preset>({
name: '',
Expand Down
5 changes: 5 additions & 0 deletions ui/app/composables/usePresets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,13 +129,18 @@ const removePreset = (id: number) => {
const loadPresets = async (
page: number = 1,
perPage: number | undefined = undefined,
options: { excludeDefaults?: boolean } = {},
): Promise<void> => {
isLoading.value = true;
try {
let url = `/api/presets/?page=${page}`;
if (perPage !== undefined) {
url += `&per_page=${perPage}`;
}
if (options.excludeDefaults) {
url += '&exclude_defaults=true';
}

const response = await request(url);
await ensureSuccess(response);

Expand Down
14 changes: 3 additions & 11 deletions ui/app/pages/conditions.vue
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@
icon="i-lucide-refresh-cw"
:loading="isLoading"
:disabled="isLoading"
@click="() => void reloadContent()"
@click="() => void loadContent(page)"
>
<span>Reload</span>
</UButton>
Expand Down Expand Up @@ -122,7 +122,7 @@
:disabled="isLoading"
show-edges
:sibling-count="0"
@update:page="navigatePage"
@update:page="loadContent"
size="sm"
/>
</div>
Expand Down Expand Up @@ -447,7 +447,7 @@
:disabled="isLoading"
show-edges
:sibling-count="0"
@update:page="navigatePage"
@update:page="loadContent"
size="sm"
/>
</div>
Expand Down Expand Up @@ -648,14 +648,6 @@ const loadContent = async (pageNumber = 1): Promise<void> => {
await syncPageQuery(pageNumber);
};

const reloadContent = async (): Promise<void> => {
await loadContent(page.value);
};

const navigatePage = async (newPage: number): Promise<void> => {
await loadContent(newPage);
};

const resetEditor = (): void => {
item.value = {};
itemRef.value = null;
Expand Down
14 changes: 3 additions & 11 deletions ui/app/pages/dl_fields.vue
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@
icon="i-lucide-refresh-cw"
:loading="isLoading"
:disabled="isLoading"
@click="() => void reloadContent()"
@click="() => void loadContent(page)"
>
<span>Reload</span>
</UButton>
Expand Down Expand Up @@ -122,7 +122,7 @@
:disabled="isLoading"
show-edges
:sibling-count="0"
@update:page="navigatePage"
@update:page="loadContent"
size="sm"
/>
</div>
Expand Down Expand Up @@ -399,7 +399,7 @@
:disabled="isLoading"
show-edges
:sibling-count="0"
@update:page="navigatePage"
@update:page="loadContent"
size="sm"
/>
</div>
Expand Down Expand Up @@ -567,14 +567,6 @@ const loadContent = async (pageNumber = 1): Promise<void> => {
await syncPageQuery(pageNumber);
};

const reloadContent = async (): Promise<void> => {
await loadContent(page.value);
};

const navigatePage = async (newPage: number): Promise<void> => {
await loadContent(newPage);
};

const resetEditor = (): void => {
item.value = {};
itemRef.value = null;
Expand Down
14 changes: 3 additions & 11 deletions ui/app/pages/notifications.vue
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@
icon="i-lucide-refresh-cw"
:loading="isLoading"
:disabled="isLoading"
@click="() => void reloadContent()"
@click="() => void loadContent(page)"
>
<span>Reload</span>
</UButton>
Expand Down Expand Up @@ -135,7 +135,7 @@
:disabled="isLoading"
show-edges
:sibling-count="0"
@update:page="navigatePage"
@update:page="loadContent"
size="sm"
/>
</div>
Expand Down Expand Up @@ -497,7 +497,7 @@
:disabled="isLoading"
show-edges
:sibling-count="0"
@update:page="navigatePage"
@update:page="loadContent"
size="sm"
/>
</div>
Expand Down Expand Up @@ -706,14 +706,6 @@ const loadContent = async (pageNumber = page.value): Promise<void> => {
await notificationsStore.loadNotifications(pageNumber);
};

const reloadContent = async (): Promise<void> => {
await loadContent(page.value);
};

const navigatePage = async (newPage: number): Promise<void> => {
await loadContent(newPage);
};

const resetEditor = (): void => {
target.value = defaultState();
targetRef.value = undefined;
Expand Down
Loading
Loading