From fa5dcbc9a98b0f09669b9729abe1f9515d154c61 Mon Sep 17 00:00:00 2001 From: AlejandroAkbal <37181533+AlejandroAkbal@users.noreply.github.com> Date: Sat, 14 Mar 2026 23:37:16 -0700 Subject: [PATCH] fix: preserve boolean search tags in routes --- assets/js/RouterHelper.ts | 43 ++++++++++++++++++++------ pages/posts/[domain].vue | 15 ++------- pages/premium/saved-posts/[domain].vue | 13 ++------ test/assets/router-helper.test.ts | 17 +++++----- 4 files changed, 48 insertions(+), 40 deletions(-) diff --git a/assets/js/RouterHelper.ts b/assets/js/RouterHelper.ts index 7f447bee..88bf4ea8 100644 --- a/assets/js/RouterHelper.ts +++ b/assets/js/RouterHelper.ts @@ -1,16 +1,18 @@ -import Tag from './tag.dto' -import type { RouteLocationRaw } from 'vue-router' +import type { ITag } from './tag.dto' +import type { LocationQueryValue, LocationQueryValueRaw, RouteLocationRaw } from 'vue-router' export function generatePostsRoute( path: string = '/posts', domain?: string | undefined | null, page?: number | undefined | null, - tags?: Tag[] | undefined | null, + tags?: ITag[] | undefined | null, filters?: Object | undefined | null ) { + const query: Record = {} + const route: RouteLocationRaw = { path, - query: {} + query } if (domain != null) { @@ -18,21 +20,44 @@ export function generatePostsRoute( } if (page != null) { - route.query.page = page.toString() + query.page = page.toString() } - if (tags != null && Array.isArray(tags) && tags.length) { - route.query.tags = tags.map((tag) => encodeURIComponent(tag.name)).join('|') + const serializedTags = serializeRouteTags(tags) + + if (serializedTags) { + query.tags = serializedTags } // Check if object keys are not undefined if (filters != null && !isObjectEmpty(filters)) { - route.query.filter = filters + query.filter = filters } return route } -function isObjectEmpty(obj) { +export function serializeRouteTags(tags?: ITag[] | undefined | null): LocationQueryValueRaw[] | undefined { + if (!Array.isArray(tags) || tags.length === 0) { + return undefined + } + + return tags.map((tag) => encodeURIComponent(tag.name)) +} + +export function parseRouteTags(tags?: LocationQueryValue | LocationQueryValue[] | null): ITag[] { + const normalizedTags = Array.isArray(tags) + ? tags.filter((tag): tag is string => tag != null) + : tags != null + ? [tags] + : [] + + return normalizedTags + .map((tag) => decodeURIComponent(tag)) + .filter(Boolean) + .map((tag) => ({ name: tag })) +} + +function isObjectEmpty(obj: object) { return obj && Object.keys(obj).length === 0 && obj.constructor === Object } diff --git a/pages/posts/[domain].vue b/pages/posts/[domain].vue index d2182468..3a8211f9 100644 --- a/pages/posts/[domain].vue +++ b/pages/posts/[domain].vue @@ -7,7 +7,7 @@ import { FetchError } from 'ofetch' import type { Ref } from 'vue' import { toast } from 'vue-sonner' - import { generatePostsRoute } from '~/assets/js/RouterHelper' + import { generatePostsRoute, parseRouteTags } from '~/assets/js/RouterHelper' import { tagArrayToTitle } from '~/assets/js/SeoHelper' import type { Domain } from '~/assets/js/domain' import type { IPost, IPostPage } from '~/assets/js/post.dto' @@ -73,16 +73,7 @@ }) const selectedTags = computed(() => { - const tags = route.query.tags as string - - if (!tags) { - return [] - } - - return tags - .split('|') - .map((tag) => decodeURIComponent(tag)) - .map((tag) => new Tag({ name: tag }).toJSON()) + return parseRouteTags(route.query.tags) }) const selectedPage = computed(() => { @@ -397,7 +388,7 @@ const apiUrl = config.public.apiUrl + '/booru/' + selectedBooru.value.type.type + '/posts' - const tags = selectedTags.value.map((tag) => tag.name).join('|') + const tags = selectedTags.value.map((tag) => tag.name) return $fetch(apiUrl, { params: { diff --git a/pages/premium/saved-posts/[domain].vue b/pages/premium/saved-posts/[domain].vue index 3bbcad14..3e1e4353 100644 --- a/pages/premium/saved-posts/[domain].vue +++ b/pages/premium/saved-posts/[domain].vue @@ -11,7 +11,7 @@ import Tag from '~/assets/js/tag.dto' import { booruTypeList } from '~/assets/lib/rule-34-shared-resources/src/util/BooruUtils' import type { IPost, IPostPage } from 'assets/js/post.dto' - import { generatePostsRoute } from '~/assets/js/RouterHelper' + import { generatePostsRoute, parseRouteTags } from '~/assets/js/RouterHelper' import type { IPocketbasePost } from '~/assets/js/pocketbase.dto' import { project } from '@/config/project' @@ -77,16 +77,7 @@ }) const selectedTags = computed(() => { - const tags = route.query.tags as string - - if (!tags) { - return [] - } - - return tags - .split('|') - .map((tag) => decodeURIComponent(tag)) - .map((tag) => new Tag({ name: tag }).toJSON()) + return parseRouteTags(route.query.tags) }) const selectedPage = computed(() => { diff --git a/test/assets/router-helper.test.ts b/test/assets/router-helper.test.ts index 701e01ca..f9a636b2 100644 --- a/test/assets/router-helper.test.ts +++ b/test/assets/router-helper.test.ts @@ -1,5 +1,5 @@ import {describe, expect, it} from 'vitest' -import {generatePostsRoute} from '../../assets/js/RouterHelper' +import {generatePostsRoute, parseRouteTags} from '../../assets/js/RouterHelper' import Tag from '../../assets/js/tag.dto' describe('generatePostsRoute', () => { @@ -15,14 +15,14 @@ describe('generatePostsRoute', () => { expect(route).toMatchObject({ path: '/posts/safebooru.org', query: { - tags: 'panty_%26_stocking_with_garterbelt' + tags: ['panty_%26_stocking_with_garterbelt'] } }) - expect(decodeURIComponent(String(route.query?.tags))).toBe('panty_&_stocking_with_garterbelt') + expect(parseRouteTags(route.query?.tags)).toEqual([{ name: 'panty_&_stocking_with_garterbelt' }]) }) - it('encodes multiple tags joined by pipes when one contains an ampersand', () => { + it('stores multiple tags as repeated query values', () => { const route = generatePostsRoute( '/posts', 'safebooru.org', @@ -37,12 +37,13 @@ describe('generatePostsRoute', () => { expect(route).toMatchObject({ path: '/posts/safebooru.org', query: { - tags: 'panty_%26_stocking_with_garterbelt|rating%3Asafe' + tags: ['panty_%26_stocking_with_garterbelt', 'rating%3Asafe'] } }) - expect(decodeURIComponent(String(route.query?.tags))).toBe( - 'panty_&_stocking_with_garterbelt|rating:safe' - ) + expect(parseRouteTags(route.query?.tags)).toEqual([ + { name: 'panty_&_stocking_with_garterbelt' }, + { name: 'rating:safe' } + ]) }) })