Browse Source

Feature: Mobile - Redesign the Error page.

Dusan Vuckovic 2 years ago
parent
commit
821d444360

+ 1 - 1
app/controllers/application_controller/handles_errors.rb

@@ -80,7 +80,7 @@ module ApplicationController::HandlesErrors
         @exception = e
         @message = errors[:error_human] || errors[:error] || param[:message]
         @traceback = !Rails.env.production?
-        file = Rails.public_path.join("#{status_code}.html").open('r')
+        file = Rails.public_path.join("#{status_code}#{params[:controller] == 'mobile' ? '-mobile' : ''}.html").open('r')
         render inline: file.read, status: status, content_type: 'text/html' # rubocop:disable Rails/RenderInline
       end
     end

+ 1 - 1
app/controllers/mobile_controller.rb

@@ -2,7 +2,7 @@
 
 class MobileController < ApplicationController
   def index
-    render(layout: 'layouts/mobile')
+    render(layout: 'layouts/mobile', locals: { locale: current_user&.preferences&.dig(:locale) })
   end
 
   def service_worker

+ 6 - 2
app/controllers/tests_controller.rb

@@ -27,8 +27,12 @@ class TestsController < ApplicationController
 
   # GET /tests/raised_exception
   def error_raised_exception
-    exception = params.fetch(:exception, 'StandardError')
-    message   = params.fetch(:message, 'no message provided')
+    origin     = params.fetch(:origin)
+    exception  = params.fetch(:exception, 'StandardError')
+    message    = params.fetch(:message, 'no message provided')
+
+    # Emulate the originating controller.
+    params[:controller] = origin if origin
 
     raise exception.safe_constantize, message
   end

+ 16 - 4
app/frontend/apps/mobile/components/CommonBackButton/CommonBackButton.vue

@@ -1,7 +1,9 @@
 <!-- Copyright (C) 2012-2023 Zammad Foundation, https://zammad-foundation.org/ -->
 
 <script setup lang="ts">
+import { computed } from 'vue'
 import type { RouteLocationRaw } from 'vue-router'
+import { useWalker } from '@shared/router/walker'
 
 interface Props {
   fallback: RouteLocationRaw
@@ -12,17 +14,27 @@ interface Props {
   ignore?: string[]
 }
 
-defineProps<Props>()
+const props = defineProps<Props>()
+
+const walker = useWalker()
+
+const isHomeButton = computed(() => {
+  if (props.fallback !== '/' || walker.hasBackUrl) return false
+  return true
+})
 </script>
 
 <template>
   <button
     class="flex cursor-pointer items-center"
-    :aria-label="$t('Go back')"
+    :aria-label="isHomeButton ? $t('Go home') : $t('Go back')"
     :class="{ 'gap-2': label }"
     @click="$walker.back(fallback, ignore)"
   >
-    <CommonIcon decorative name="mobile-chevron-left" />
-    <span v-if="label">{{ $t(label) }}</span>
+    <CommonIcon
+      :name="isHomeButton ? 'mobile-home' : 'mobile-chevron-left'"
+      decorative
+    />
+    <span v-if="label">{{ isHomeButton ? $t('Home') : $t(label) }}</span>
   </button>
 </template>

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

@@ -30,5 +30,48 @@ describe('rendering common back button', () => {
     expect(view.container).toHaveTextContent('Zurück')
 
     expect(view.getByRole('button', { name: 'Go back' })).toBeInTheDocument()
+
+    i18n.setTranslationMap(new Map([]))
+  })
+
+  it('renders home button, if no history is present', async () => {
+    window.history.replaceState({}, '')
+
+    const view = renderComponent(CommonBackButton, {
+      router: true,
+      props: {
+        fallback: '/',
+      },
+    })
+
+    expect(view.getByRole('button', { name: 'Go home' })).toBeInTheDocument()
+
+    await view.rerender({
+      label: 'Back',
+    })
+
+    expect(view.container).toHaveTextContent('Home')
+  })
+
+  it('renders back button, if history is present', async () => {
+    window.history.replaceState(
+      { back: '/tickets/1/information/customer' },
+      '/tickets/1/information/organization',
+    )
+
+    const view = renderComponent(CommonBackButton, {
+      router: true,
+      props: {
+        fallback: '/',
+      },
+    })
+
+    expect(view.getByRole('button', { name: 'Go back' })).toBeInTheDocument()
+
+    await view.rerender({
+      label: 'Back',
+    })
+
+    expect(view.container).toHaveTextContent('Back')
   })
 })

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

@@ -42,7 +42,7 @@ describe('mobile app header', () => {
   it('renders back button, if specified', async () => {
     const view = renderComponent(LayoutHeader, {
       props: {
-        backUrl: '/',
+        backUrl: '/foo',
         backTitle: 'Back',
       },
       router: true,
@@ -54,7 +54,7 @@ describe('mobile app header', () => {
 
     i18n.setTranslationMap(new Map([['Test2', 'Translated']]))
 
-    await view.rerender({ backTitle: 'Test2', backUrl: '/' })
+    await view.rerender({ backTitle: 'Test2', backUrl: '/bar' })
 
     expect(view.getByText('Translated')).toBeInTheDocument()
   })

+ 41 - 7
app/frontend/apps/mobile/pages/error/views/Error.vue

@@ -1,15 +1,49 @@
 <!-- Copyright (C) 2012-2023 Zammad Foundation, https://zammad-foundation.org/ -->
 
 <script setup lang="ts">
+import { computed } from 'vue'
+import CommonBackButton from '@mobile/components/CommonBackButton/CommonBackButton.vue'
 import { errorOptions } from '@mobile/router/error'
+import { ErrorStatusCodes } from '@shared/types/error'
+
+const errorImage = computed(() => {
+  switch (errorOptions.value.statusCode) {
+    case ErrorStatusCodes.Forbidden:
+      return '/assets/error/error-mobile-403.svg'
+    case ErrorStatusCodes.NotFound:
+      return '/assets/error/error-mobile-404.svg'
+    case ErrorStatusCodes.InternalError:
+    default:
+      return '/assets/error/error-mobile-500.svg'
+  }
+})
 </script>
 
 <template>
-  <main>
-    <h1>{{ i18n.t('Error - %s', errorOptions.statusCode) }}</h1>
-    <p>{{ errorOptions.message }}</p>
-    <p v-if="errorOptions.route">
-      {{ errorOptions.route }}
-    </p>
-  </main>
+  <div class="flex min-h-screen flex-col px-4">
+    <header class="fixed">
+      <div class="grid h-16 grid-cols-3">
+        <div
+          class="flex cursor-pointer items-center justify-self-start text-base"
+        >
+          <CommonBackButton fallback="/" />
+        </div>
+      </div>
+    </header>
+    <main class="flex grow flex-col items-center justify-center">
+      <h1 class="mb-9 text-8xl font-extrabold">
+        {{ errorOptions.statusCode }}
+      </h1>
+      <img :alt="$t('Error')" :src="errorImage" />
+      <h2 class="mt-9 max-w-prose text-center text-xl font-semibold">
+        {{ $t(errorOptions.title) }}
+      </h2>
+      <p class="mt-4 min-h-[4rem] max-w-prose text-center text-gray">
+        {{ $t(errorOptions.message) }}
+      </p>
+      <p v-if="errorOptions.route" class="max-w-prose text-center text-gray">
+        {{ errorOptions.route }}
+      </p>
+    </main>
+  </div>
 </template>

+ 2 - 2
app/frontend/apps/mobile/pages/home/views/Home.vue

@@ -54,8 +54,8 @@ const ticketOverview = computed<MenuItem[]>(() => {
 <template>
   <div class="p-4">
     <div class="mt-1.5 mb-3 flex justify-end ltr:mr-1.5 rtl:ml-1.5">
-      <CommonLink link="/tickets/create" :title="__('Create new ticket')">
-        <CommonIcon name="mobile-add" size="small" />
+      <CommonLink link="/tickets/create" :aria-label="$t('Create new ticket')">
+        <CommonIcon name="mobile-add" size="small" decorative />
       </CommonLink>
     </div>
     <h1 class="mb-5 flex w-full items-center justify-center text-4xl font-bold">

+ 1 - 0
app/frontend/apps/mobile/pages/organization/views/OrganizationDetailView.vue

@@ -39,6 +39,7 @@ const router = useRouter()
 organizationQuery.onError(() => {
   return redirectToError(router, {
     statusCode: ErrorStatusCodes.Forbidden,
+    title: __('Forbidden'),
     message: __('Sorry, but you have insufficient rights to open this page.'),
   })
 })

+ 1 - 1
app/frontend/apps/mobile/pages/ticket/__tests__/ticket-create.spec.ts

@@ -261,7 +261,7 @@ describe('Creating new ticket as agent', () => {
     // Wait on the changes
     await getNode('ticket-create')?.settled
 
-    await view.events.click(view.getByRole('button', { name: 'Go back' }))
+    await view.events.click(view.getByRole('button', { name: 'Go home' }))
 
     expect(view.queryByTestId('popupWindow')).toBeInTheDocument()
 

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