From a728e481101093fc20ab41e376d9df4c33abe89a Mon Sep 17 00:00:00 2001 From: jon r Date: Fri, 18 Apr 2025 11:09:11 +0200 Subject: [PATCH] Refactor(front): consolidate modals into ui/modals/* and bind them to query flags closes #2403 Co-Authored-By: ArneBo Co-Authored-By: Flupsi Co-Authored-By: jon r --- front/src/ui/composables/useModal.ts | 125 ++++++++++++++++++ front/src/ui/modals/Language.vue | 46 +++++++ front/src/ui/modals/Shortcuts.vue | 191 +++++++++++++++++++++++++++ 3 files changed, 362 insertions(+) create mode 100644 front/src/ui/composables/useModal.ts create mode 100644 front/src/ui/modals/Language.vue create mode 100644 front/src/ui/modals/Shortcuts.vue diff --git a/front/src/ui/composables/useModal.ts b/front/src/ui/composables/useModal.ts new file mode 100644 index 000000000..de87c7872 --- /dev/null +++ b/front/src/ui/composables/useModal.ts @@ -0,0 +1,125 @@ +import { computed } from 'vue' +import { useRouter, type RouteLocationRaw, type LocationQuery } from 'vue-router' + +type Assignment = { on: (value : T | null) => string | null, isOn: (value: LocationQuery[string]) => boolean } + +export const exactlyNull:Assignment = ({ on: (_) => null, isOn: (value) => value === null }) +export const notUndefined:Assignment = ({ on: (_) => null, isOn: (value) => value !== undefined }) + +/** + * 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 const useModal = ( + flag: string, + assignment: Assignment = exactlyNull +) => { + 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 () { + 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}.`) + }) + } + }) + + /** + * 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}.`) + }) + } + }) + + /** + * + * @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 } + } + + /** + * 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) + + /** + * Toggle the visibility of this modal + * + * + * @param flag Identifier for the modal + */ + const toggle = () => (isOpen.value = !isOpen.value) + + return { value, isOpen, to, asAttribute, toggle } +} + +/* All possible useModals that produce a given `RouterLink` destination */ +export const fromProps = ({ to } : { to?: RouteLocationRaw }, assignment: Assignment = exactlyNull): ReturnType[] => + to && typeof to !== 'string' && 'query' in to && to.query + ? Object.keys(to.query).map(k => useModal(k, assignment)) + : [] diff --git a/front/src/ui/modals/Language.vue b/front/src/ui/modals/Language.vue new file mode 100644 index 000000000..fd57a9ffd --- /dev/null +++ b/front/src/ui/modals/Language.vue @@ -0,0 +1,46 @@ + + + + + diff --git a/front/src/ui/modals/Shortcuts.vue b/front/src/ui/modals/Shortcuts.vue new file mode 100644 index 000000000..7db682451 --- /dev/null +++ b/front/src/ui/modals/Shortcuts.vue @@ -0,0 +1,191 @@ + + + + +