From 4e8081318e932608019757eac1fc54cd24d2ad9f Mon Sep 17 00:00:00 2001 From: upsiflu Date: Mon, 10 Feb 2025 20:17:59 +0100 Subject: [PATCH] feat(front): use Url to store and manage global search query (search modal) --- front/src/App.vue | 4 +- front/src/components/ui/Input.vue | 14 +- front/src/ui/components/Sidebar.vue | 23 +-- front/src/ui/components/UserMenu.vue | 2 +- front/src/ui/composables/useModal.ts | 171 +++++++++++------- .../ui/modals/{Languages.vue => Language.vue} | 2 +- front/src/ui/modals/Search.vue | 80 ++++++++ front/src/ui/modals/Upload.vue | 18 +- 8 files changed, 209 insertions(+), 105 deletions(-) rename front/src/ui/modals/{Languages.vue => Language.vue} (95%) create mode 100644 front/src/ui/modals/Search.vue diff --git a/front/src/App.vue b/front/src/App.vue index bd85ec9a1..a596a66be 100644 --- a/front/src/App.vue +++ b/front/src/App.vue @@ -19,7 +19,8 @@ import Queue from '~/components/Queue.vue' import Sidebar from '~/ui/components/Sidebar.vue' import ShortcutsModal from '~/ui/modals/Shortcuts.vue' import LanguagesModal from '~/ui/modals/Languages.vue' -import UploadModal from '~/ui/modals/Upload.vue'; +import SearchModal from '~/ui/modals/Search.vue' +import UploadModal from '~/ui/modals/Upload.vue' // Fake content onMounted(async () => { @@ -90,6 +91,7 @@ store.dispatch('auth/fetchUser') + diff --git a/front/src/components/ui/Input.vue b/front/src/components/ui/Input.vue index 47aa05f00..405ddf880 100644 --- a/front/src/components/ui/Input.vue +++ b/front/src/components/ui/Input.vue @@ -62,13 +62,13 @@ const model = defineModel({ required: true }) diff --git a/front/src/ui/components/Sidebar.vue b/front/src/ui/components/Sidebar.vue index 405a06227..a97ea06a8 100644 --- a/front/src/ui/components/Sidebar.vue +++ b/front/src/ui/components/Sidebar.vue @@ -3,6 +3,7 @@ import { ref, onMounted, watch, computed } from 'vue' import { useUploadsStore } from '../stores/upload' import { useI18n } from 'vue-i18n' import { useStore } from '~/store' +import { useModal } from '~/ui/composables/useModal.ts' import Logo from '~/components/Logo.vue' import Input from '~/components/ui/Input.vue' @@ -18,14 +19,13 @@ const isCollapsed = ref(true) const route = useRoute() watch(() => route.path, () => isCollapsed.value = true) -const searchQuery = ref('') - // Hide the fake app when the real one is loaded onMounted(() => { document.getElementById('fake-app')?.remove() }) const { t } = useI18n() +const { value: searchParameter } = useModal('search') const store = useStore() const uploads = useUploadsStore() @@ -91,17 +91,14 @@ const logoUrl = computed(() => store.state.auth.authenticated ? 'library.index' -
- -
+ diff --git a/front/src/ui/components/UserMenu.vue b/front/src/ui/components/UserMenu.vue index edc45183c..8ca05de8b 100644 --- a/front/src/ui/components/UserMenu.vue +++ b/front/src/ui/components/UserMenu.vue @@ -91,7 +91,7 @@ const labels = computed(() => ({ {{ labels.settings }}
- + {{ labels.language }}... diff --git a/front/src/ui/composables/useModal.ts b/front/src/ui/composables/useModal.ts index 0faa53cac..c6107a4c6 100644 --- a/front/src/ui/composables/useModal.ts +++ b/front/src/ui/composables/useModal.ts @@ -1,82 +1,117 @@ -import { computed } from 'vue' -import { useRouter, type RouteLocationRaw } from 'vue-router' +import { computed } from "vue"; +import { useRouter, type RouteLocationRaw, type LocationQuery } from "vue-router"; /** * Bind a modal to a single query parameter in the URL (and vice versa) * * @param flag query parameter to appear in the `url`, corresponding with `isOpen` + * @param assignment a tuple of two functions: + * - from outside world to flag value (default: (_) => null) + * - from flag value to boolean (default: if flag value is null) * * This functionality completely independent from the `router` modules. */ -export function useModal(flag: string) { - const router = useRouter() - const query = computed(() => router?.currentRoute.value ? router.currentRoute.value.query : {}) +export const useModal = ( + flag: string, + assignment: { on: (value : T | null) => string | null, isOn: (value: LocationQuery[string]) => boolean } = + { on: (_) => null, isOn: (value) => value === null } +) => { + const router = useRouter(); + const query = computed(() => + router?.currentRoute.value ? router.currentRoute.value.query : {} + ); - /** - * Visibility of this modal - * - * Use this function to bind a modal to a query parameter in the URL. - * Using this helper function will not change the routes. - * - * For example, if the modal flag is 'upload': - * - * ```vue - * - * ``` - * - * Url: `https://funkwhale.audio/?upload` - * - * More information on query flags: https://router.vuejs.org/api/type-aliases/LocationQueryValue.html - * - * Use `asAttribute` instead to bind a modal to an on/off attribute such as `aria-expanded` or `aria-pressed` - * - * @param flag Identifier for the modal - * @returns a `ref` - */ - const isOpen = computed({ - get() { - const newValue = flag in query.value && query.value[flag] === null - // console.log("GET isOpen", flag, "in", query, newValue) - return newValue - }, - set(newValue: boolean) { - // console.log("SET isOpen", query, "+", newValue, ", was", isOpen.value) - router.push({ query: { ...query.value, [flag]: newValue ? null : undefined }}).catch((err) => { - throw new Error(`Problem pushing route query: ${err}.`); - }); - // console.log("DONE SET isOpen", query) - } - }) + /** + * Visibility of this modal + * + * Use this function to bind a modal to a query parameter in the URL. + * Using this helper function will not change the routes. + * + * For example, if the modal flag is 'upload': + * + * ```vue + * + * ``` + * + * Url: `https://funkwhale.audio/?upload` + * + * More information on query flags: https://router.vuejs.org/api/type-aliases/LocationQueryValue.html + * + * Use `asAttribute` instead to bind a modal to an on/off attribute such as `aria-expanded` or `aria-pressed` + * + * @param flag Identifier for the modal + * @returns a `ref` + */ + const isOpen = computed({ + get() { + return flag in query.value && assignment.isOn(query.value[flag]); + }, + set(newValue: boolean) { + router + .push({ + query: { + ...query.value, + [flag]: newValue ? assignment.on(null) : undefined, + }, + }) + .catch((err) => { + throw new Error(`Problem pushing route query: ${err}.`); + }); + }, + }); - /** - * - * @param flag Identifier for the modal - * @returns a `RouteLocationRaw` object to use in the `to` prop of a RouterLink component - */ - const to: RouteLocationRaw = { - query: { ...query.value, [flag]: null } - } + /** + * Current value of the flag in the URL, normalized to a string. + * + * Note that both `null` and `undefined` are coerced to an empty string, and arrays are joined with a space. + * + * Use in inputs. + */ + const value = computed({ + get() { + const flagValue = flag in query.value ? query.value[flag] : ''; + return typeof flagValue === 'string' ? flagValue : flagValue === null ? '' : flagValue.join(' '); + }, + set(newValue: string) { + router + .push({ + query: { + ...query.value, + [flag]: newValue, + }, + }) + .catch((err) => { + throw new Error(`Problem pushing route query: ${err}.`); + }); + }, + }) - /** - * Use this function to bind a modal to an on/off attribute such as `aria-expanded` or `aria-pressed` - * - * @param flag Identifier for the modal - * @returns a `ref` - */ - const asAttribute = computed(() => - isOpen.value || undefined - ) + /** + * + * @param flag Identifier for the modal + * @returns a `RouteLocationRaw` object to use in the `to` prop of a RouterLink component + */ + const to: RouteLocationRaw = { + query: { ...query.value, [flag]: null }, + }; - /** - * Toggle the visibility of this modal - * - * - * @param flag Identifier for the modal - */ - const toggle = () => - isOpen.value = !isOpen.value + /** + * Use this function to bind a modal to an on/off attribute such as `aria-expanded` or `aria-pressed` + * + * @param flag Identifier for the modal + * @returns a `ref` + */ + const asAttribute = computed(() => isOpen.value || undefined); - return { isOpen, to, asAttribute, toggle } + /** + * Toggle the visibility of this modal + * + * + * @param flag Identifier for the modal + */ + const toggle = () => (isOpen.value = !isOpen.value); + + return { value, isOpen, to, asAttribute, toggle }; } diff --git a/front/src/ui/modals/Languages.vue b/front/src/ui/modals/Language.vue similarity index 95% rename from front/src/ui/modals/Languages.vue rename to front/src/ui/modals/Language.vue index 9ff5e1eb3..5ce2b030e 100644 --- a/front/src/ui/modals/Languages.vue +++ b/front/src/ui/modals/Language.vue @@ -10,7 +10,7 @@ import Layout from '~/components/ui/Layout.vue' const { t, locale } = useI18n() -const isOpen = useModal('languages').isOpen +const isOpen = useModal('language').isOpen