Browse Source

Maintenance: Add component for "back button"

Vladimir Sheremet 2 years ago
parent
commit
596d4cc91f

+ 23 - 0
app/frontend/apps/mobile/components/CommonBackButton/CommonBackButton.vue

@@ -0,0 +1,23 @@
+<!-- Copyright (C) 2012-2022 Zammad Foundation, https://zammad-foundation.org/ -->
+
+<script setup lang="ts">
+import type { RouteLocationRaw } from 'vue-router'
+
+interface Props {
+  fallback: RouteLocationRaw
+  label?: string
+}
+
+defineProps<Props>()
+</script>
+
+<template>
+  <button
+    class="flex cursor-pointer items-center"
+    :class="{ 'gap-2': label }"
+    @click="$walker.back(fallback)"
+  >
+    <CommonIcon name="arrow-left" size="small" />
+    <span v-if="label">{{ $t(label) }}</span>
+  </button>
+</template>

+ 27 - 0
app/frontend/apps/mobile/components/CommonBackButton/__tests__/CommonBackButton.spec.ts

@@ -0,0 +1,27 @@
+// Copyright (C) 2012-2022 Zammad Foundation, https://zammad-foundation.org/
+
+import { i18n } from '@shared/i18n'
+import { renderComponent } from '@tests/support/components'
+import { flushPromises } from '@vue/test-utils'
+import CommonBackButton from '../CommonBackButton.vue'
+
+// $walker.back not tested because there is a unit test for it
+describe('rendering common back button', () => {
+  it('renders label', async () => {
+    const view = renderComponent(CommonBackButton, {
+      router: true,
+      props: {
+        fallback: '/back-url',
+        label: 'Back',
+      },
+    })
+
+    expect(view.container).toHaveTextContent('Back')
+
+    i18n.setTranslationMap(new Map([['Back', 'Zurück']]))
+
+    await flushPromises()
+
+    expect(view.container).toHaveTextContent('Zurück')
+  })
+})

+ 3 - 14
app/frontend/apps/mobile/components/layout/LayoutHeader.vue

@@ -2,6 +2,7 @@
 
 <script setup lang="ts">
 import type { RouteLocationRaw } from 'vue-router'
+import CommonBackButton from '../CommonBackButton/CommonBackButton.vue'
 
 export interface Props {
   title?: string
@@ -9,7 +10,6 @@ export interface Props {
   backTitle?: string
   backUrl?: RouteLocationRaw
   actionTitle?: string
-  backButton?: boolean
   onAction?(): void
 }
 
@@ -18,23 +18,12 @@ defineProps<Props>()
 
 <template>
   <header
-    v-if="
-      title || (backUrl && backTitle) || backButton || (onAction && actionTitle)
-    "
+    v-if="title || backUrl || (onAction && actionTitle)"
     class="grid h-[64px] grid-cols-3 border-b-[0.5px] border-white/10 px-4"
     data-test-id="appHeader"
   >
     <div class="flex items-center justify-self-start text-base">
-      <component
-        :is="backUrl ? 'CommonLink' : 'div'"
-        v-if="(backUrl && backTitle) || backButton"
-        :link="backUrl"
-        class="flex cursor-pointer gap-2"
-        @click="backButton && $router.back()"
-      >
-        <CommonIcon name="arrow-left" size="small" />
-        <span>{{ $t(backTitle) }}</span>
-      </component>
+      <CommonBackButton v-if="backUrl" :fallback="backUrl" :label="backTitle" />
     </div>
     <div
       :class="[

+ 0 - 15
app/frontend/apps/mobile/components/layout/__tests__/LayoutHeader.spec.ts

@@ -51,7 +51,6 @@ describe('mobile app header', () => {
     const backButton = view.getByText('Back')
 
     expect(backButton).toBeInTheDocument()
-    expect(view.getLinkFromElement(backButton)).toHaveAttribute('href', '/')
 
     i18n.setTranslationMap(new Map([['Test2', 'Translated']]))
 
@@ -60,20 +59,6 @@ describe('mobile app header', () => {
     expect(view.getByText('Translated')).toBeInTheDocument()
   })
 
-  it('renders back button as button if no url is specified', async () => {
-    const view = renderComponent(LayoutHeader, {
-      props: {
-        backButton: true,
-      },
-      router: true,
-    })
-
-    const button = view.getByIconName('arrow-left')
-
-    expect(button).toBeInTheDocument()
-    expect(button.closest('a')).not.toBeInTheDocument()
-  })
-
   it('renders action, if specified', async () => {
     const onAction = vi.fn()
 

+ 0 - 1
app/frontend/apps/mobile/composables/useHeader.ts

@@ -10,7 +10,6 @@ export interface HeaderOptions {
   titleClass?: string | ComputedRef<string>
   backTitle?: string | ComputedRef<string>
   backUrl?: RouteLocationRaw | ComputedRef<RouteLocationRaw>
-  backButton?: boolean
   actionTitle?: string | ComputedRef<string>
   onAction?(): unknown
 }

+ 2 - 2
app/frontend/apps/mobile/modules/account/views/AccountAvatar.vue

@@ -2,7 +2,6 @@
 
 <script setup lang="ts">
 import { reactive, shallowRef, watch, ref, computed } from 'vue'
-import { useRouter } from 'vue-router'
 import { storeToRefs } from 'pinia'
 import { Cropper, type CropperResult } from 'vue-advanced-cropper'
 import 'vue-advanced-cropper/dist/style.css'
@@ -18,6 +17,7 @@ import CommonUserAvatar from '@shared/components/CommonUserAvatar/CommonUserAvat
 import CommonAvatar from '@shared/components/CommonAvatar/CommonAvatar.vue'
 import { MutationHandler, QueryHandler } from '@shared/server/apollo/handler'
 import type { AccountAvatarActiveQuery } from '@shared/graphql/types'
+import { useRouter } from 'vue-router'
 import { useHeader } from '@mobile/composables/useHeader'
 import useConfirmation from '@mobile/components/CommonConfirmation/composable'
 import CommonLoader from '@mobile/components/CommonLoader/CommonLoader.vue'
@@ -158,7 +158,7 @@ useHeader({
   backTitle: __('Account'),
   actionTitle: __('Done'),
   onAction() {
-    router.back()
+    router.push('/account')
   },
 })
 

+ 1 - 1
app/frontend/apps/mobile/modules/notifications/views/NotificationsList.vue

@@ -10,7 +10,7 @@ import NotificationItem from '../components/NotificationItem.vue'
 import type { NotificationListItem } from '../types/notificaitons'
 
 useHeader({
-  backButton: true,
+  backUrl: '/',
 })
 
 // TODO make actual API call

+ 2 - 4
app/frontend/apps/mobile/modules/ticket/components/TicketZoom/TicketZoomHeader.vue

@@ -5,6 +5,7 @@ import type { AvatarUser } from '@shared/components/CommonUserAvatar'
 import CommonUserAvatar from '@shared/components/CommonUserAvatar/CommonUserAvatar.vue'
 import { useDialog } from '@shared/composables/useDialog'
 import CommonLoader from '@mobile/components/CommonLoader/CommonLoader.vue'
+import CommonBackButton from '@mobile/components/CommonBackButton/CommonBackButton.vue'
 
 interface Props {
   ticketId: string
@@ -31,10 +32,7 @@ const showViewers = () => {
   <header
     class="grid h-[64px] grid-cols-[70px_auto_70px] border-b-[0.5px] border-white/10 bg-gray-600/90 px-4"
   >
-    <!-- TODO "back" where? -->
-    <button class="flex items-center justify-self-start">
-      <CommonIcon name="arrow-left" size="small" @click="$router.back()" />
-    </button>
+    <CommonBackButton class="justify-self-start" fallback="/" />
     <CommonLoader data-test-id="loader-header" :loading="loadingTicket" center>
       <div
         class="flex flex-1 flex-col items-center justify-center text-center text-sm leading-4"

+ 2 - 15
app/frontend/apps/mobile/modules/ticket/views/TicketOverview.vue

@@ -4,11 +4,8 @@
 import { computed, watch } from 'vue'
 import { useRoute, useRouter } from 'vue-router'
 import { watchOnce } from '@vueuse/shared'
-import {
-  useViewTransition,
-  ViewTransitions,
-} from '@mobile/components/transition/TransitionViewNavigation'
 import { i18n } from '@shared/i18n'
+import CommonBackButton from '@mobile/components/CommonBackButton/CommonBackButton.vue'
 import { EnumOrderDirection } from '@shared/graphql/types'
 import CommonLoader from '@mobile/components/CommonLoader/CommonLoader.vue'
 import { useTicketsOverviews } from '@mobile/modules/home/stores/ticketOverviews'
@@ -24,14 +21,6 @@ const props = defineProps<{
 const router = useRouter()
 const route = useRoute()
 
-const { setViewTransition } = useViewTransition()
-
-const goBack = () => {
-  setViewTransition(ViewTransitions.Prev)
-
-  router.go(-1)
-}
-
 const { overviews, loading: loadingOverviews } = storeToRefs(
   useTicketsOverviews(),
 )
@@ -175,9 +164,7 @@ const directionOptions = computed(() => [
         <div
           class="flex cursor-pointer items-center justify-self-start text-base"
         >
-          <div @click="goBack()">
-            <CommonIcon name="arrow-left" size="small" />
-          </div>
+          <CommonBackButton fallback="/" />
         </div>
         <div
           class="flex flex-1 items-center justify-center text-center text-lg font-bold"

+ 52 - 0
app/frontend/shared/router/__tests__/walker.spec.ts

@@ -0,0 +1,52 @@
+// Copyright (C) 2012-2022 Zammad Foundation, https://zammad-foundation.org/
+
+import type { RouteLocationNormalized, Router } from 'vue-router'
+import { Walker } from '../walker'
+
+const buildRouter = () =>
+  ({
+    afterEach: vi.fn(),
+    back: vi.fn(),
+    push: vi.fn(),
+  } as any as Router)
+
+describe('testing walker', () => {
+  it('has fallback route, if no back is in history', async () => {
+    const router = buildRouter()
+    const walker = new Walker(router)
+    expect(walker.hasBackUrl).toBe(false)
+    expect(walker.getBackUrl('/fallback')).toBe('/fallback')
+    await walker.back('/fallback')
+    expect(router.push).toHaveBeenCalledWith('/fallback')
+  })
+
+  it('has back route, if there is back route in history', async () => {
+    window.history.replaceState({ back: '/back' }, '', '/back')
+    const router = buildRouter()
+    const walker = new Walker(router)
+    expect(walker.hasBackUrl).toBe(true)
+    expect(walker.getBackUrl('/fallback')).toBe('/back')
+    await walker.back('/fallback')
+    expect(router.back).toHaveBeenCalled()
+  })
+
+  it('changes back route after changing route', () => {
+    window.history.replaceState({ back: null }, '', '/back')
+
+    const router = buildRouter()
+    const walker = new Walker(router)
+
+    expect(walker.hasBackUrl).toBe(false)
+
+    const route = {} as RouteLocationNormalized
+
+    const afterEach = vi.mocked(router.afterEach).mock.calls[0][0]
+
+    window.history.replaceState({ back: '/back' }, '', '/back')
+
+    afterEach(route, route)
+
+    expect(walker.hasBackUrl).toBe(true)
+    expect(walker.getBackUrl('/fallback')).toBe('/back')
+  })
+})

Some files were not shown because too many files changed in this diff