Skip to content

Commit

Permalink
Maintenance: Update LayoutHeader to include more dynamic slot usage
Browse files Browse the repository at this point in the history
  • Loading branch information
Benjamin Scharf committed Apr 15, 2024
1 parent 3d8925e commit 2c77470
Show file tree
Hide file tree
Showing 15 changed files with 219 additions and 166 deletions.
Expand Up @@ -21,9 +21,7 @@ const props = defineProps<Props>()
const walker = useWalker()
const isHomeButton = computed(() => {
if (props.avoidHomeButton || walker.getBackUrl(props.fallback) !== '/')
return false
return true
return !(props.avoidHomeButton || walker.getBackUrl(props.fallback) !== '/')
})
const locale = useLocaleStore()
Expand Down
12 changes: 2 additions & 10 deletions app/frontend/apps/mobile/components/CommonButton/CommonButton.vue
Expand Up @@ -3,17 +3,9 @@
<script setup lang="ts">
import { computed } from 'vue'
import { startCase } from 'lodash-es'
import type { ButtonVariant } from '#shared/components/Form/fields/FieldButton/types.ts'
import type { CommonButtonProps } from '#mobile/components/CommonButton/types.ts'
interface Props {
form?: string
type?: 'button' | 'reset' | 'submit'
disabled?: boolean
variant?: ButtonVariant
transparentBackground?: boolean
}
const props = withDefaults(defineProps<Props>(), {
const props = withDefaults(defineProps<CommonButtonProps>(), {
type: 'button',
variant: 'secondary',
})
Expand Down
11 changes: 11 additions & 0 deletions app/frontend/apps/mobile/components/CommonButton/types.ts
@@ -0,0 +1,11 @@
// Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/

import type { ButtonVariant } from '#shared/components/Form/fields/FieldButton/types.ts'

export interface CommonButtonProps {
form?: string
type?: 'button' | 'reset' | 'submit'
disabled?: boolean
variant?: ButtonVariant
transparentBackground?: boolean
}
Expand Up @@ -7,6 +7,8 @@ const props = defineProps<{
refetch: boolean
}>()
defineOptions({ inheritAttrs: false })
const refetching = ref(false)
let timeout: number
Expand Down Expand Up @@ -35,6 +37,7 @@ watch(
>
<div
v-if="refetching"
v-bind="$attrs"
class="absolute items-center justify-center"
role="status"
>
Expand Down
78 changes: 51 additions & 27 deletions app/frontend/apps/mobile/components/layout/LayoutHeader.vue
@@ -1,22 +1,27 @@
<!-- Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/ -->

<script setup lang="ts">
import { computed, ref } from 'vue'
import CommonButton from '#mobile/components/CommonButton/CommonButton.vue'
import CommonBackButton from '#mobile/components/CommonBackButton/CommonBackButton.vue'
import CommonRefetch from '#mobile/components/CommonRefetch/CommonRefetch.vue'
import { computed, ref, useSlots } from 'vue'
import type { RouteLocationRaw } from 'vue-router'
import CommonButton from '../CommonButton/CommonButton.vue'
import CommonBackButton from '../CommonBackButton/CommonBackButton.vue'
import CommonRefetch from '../CommonRefetch/CommonRefetch.vue'
import type { CommonButtonProps } from '#mobile/components/CommonButton/types.ts'
export interface Props {
containerTag?: 'header' | 'div'
title?: string
titleClass?: string
backTitle?: string
backIgnore?: string[]
backUrl?: RouteLocationRaw
backAvoidHomeButton?: boolean
defaultAttrs?: Record<string, unknown>
refetch?: boolean
actionTitle?: string
actionHidden?: boolean
actionBtnProps?: CommonButtonProps
onAction?(): void
}
Expand All @@ -28,7 +33,11 @@ defineExpose({
const props = withDefaults(defineProps<Props>(), {
refetch: false,
containerTag: 'header',
})
const slots = useSlots()
const hasSlots = computed(() => Object.keys(slots).length > 0)
const headerClass = computed(() => {
return [
Expand All @@ -39,37 +48,52 @@ const headerClass = computed(() => {
</script>

<template>
<header
v-if="title || backUrl || (onAction && actionTitle)"
<component
:is="containerTag"
v-if="title || backUrl || (onAction && actionTitle) || hasSlots"
ref="headerElement"
class="grid h-[64px] shrink-0 grid-cols-[75px_auto_75px] border-b-[0.5px] border-white/10 bg-black px-4"
data-test-id="appHeader"
>
<div class="flex items-center justify-self-start text-base">
<CommonBackButton
v-if="backUrl"
:fallback="backUrl"
:label="backTitle"
:ignore="backIgnore"
:avoid-home-button="backAvoidHomeButton"
/>
<slot
name="before"
:data="{ backUrl, backTitle, backIgnore, backAvoidHomeButton }"
>
<CommonBackButton
v-if="backUrl"
:fallback="backUrl"
:label="backTitle"
:ignore="backIgnore"
:avoid-home-button="backAvoidHomeButton"
/>
</slot>
</div>
<div class="flex flex-1 items-center justify-center">
<CommonRefetch :refetch="refetch">
<h1 :class="headerClass">
{{ $t(title) }}
</h1>
<CommonRefetch v-bind="defaultAttrs" :refetch="refetch">
<slot :data="{ defaultAttrs, refetch }">
<h1 :class="headerClass">
{{ $t(title) }}
</h1>
</slot>
</CommonRefetch>
</div>
<div class="flex cursor-pointer items-center justify-self-end text-base">
<CommonButton
v-if="onAction && actionTitle && !actionHidden"
variant="primary"
transparent-background
@click="onAction?.()"
>
{{ $t(actionTitle) }}
</CommonButton>
<div
v-if="((onAction || actionTitle) && !actionHidden) || slots.after"
class="flex items-center justify-self-end text-base"
>
<slot name="after" :data="{ actionBtnProps }">
<CommonButton
v-bind="{
variant: 'primary',
transparentBackground: true,
...actionBtnProps,
}"
@click="onAction?.()"
>
{{ $t(actionTitle) }}
</CommonButton>
</slot>
</div>
</header>
</component>
</template>
Expand Up @@ -2,6 +2,7 @@

import { i18n } from '#shared/i18n.ts'
import { renderComponent } from '#tests/support/components/index.ts'
import { expect } from 'vitest'
import LayoutHeader from '../LayoutHeader.vue'

describe('mobile app header', () => {
Expand All @@ -11,32 +12,34 @@ describe('mobile app header', () => {
expect(view.queryByTestId('appHeader')).not.toBeInTheDocument()
})

it('renders title, if specified', async () => {
const view = renderComponent(LayoutHeader, {
props: { title: 'Test' },
router: true,
})

expect(view.getByTestId('appHeader')).toBeInTheDocument()
expect(view.getByText('Test')).toBeInTheDocument()

i18n.setTranslationMap(new Map([['Test2', 'Translated']]))
describe('title prop tests', () => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
let view: ReturnType<typeof renderComponent>

await view.rerender({ title: 'Test2' })

expect(view.getByText('Translated')).toBeInTheDocument()
})
beforeEach(() => {
view = renderComponent(LayoutHeader, {
props: { title: 'Test' },
router: true,
})
})
it('renders translated title, if specified', async () => {
expect(view.getByTestId('appHeader')).toBeInTheDocument()
expect(view.getByText('Test')).toBeInTheDocument()

it('can add custom class to title', () => {
const view = renderComponent(LayoutHeader, {
props: {
i18n.setTranslationMap(new Map([['Test2', 'Translated']]))
await view.rerender({ title: 'Test2' })
expect(view.getByText('Translated')).toBeInTheDocument()
})
it('should be by default a h1', () => {
expect(view.getByRole('heading', { level: 1 })).toBeInTheDocument()
})
it('can add custom class to title', async () => {
await view.rerender({
title: 'Test',
titleClass: 'test-class',
},
router: true,
})
expect(view.getByText('Test')).toHaveClass('test-class')
})

expect(view.getByText('Test')).toHaveClass('test-class')
})

it('renders back button, if specified', async () => {
Expand Down Expand Up @@ -99,4 +102,33 @@ describe('mobile app header', () => {

expect(view.queryByText('Action')).not.toBeInTheDocument()
})

describe('slots test', () => {
it('display all slots', () => {
const view = renderComponent(LayoutHeader, {
slots: {
before: `<span>Step 1</span>`,
default: `<h2>Test Heading</h2>`,
after: `Action`,
},
router: true,
})
expect(view.getByText('Step 1')).toBeInTheDocument()
expect(view.getByRole('heading', { level: 2 })).toBeInTheDocument()
expect(view.getByText('Action')).toBeInTheDocument()
})
it('shows fallback if partial slots are used', () => {
const view = renderComponent(LayoutHeader, {
title: 'Test',
slots: {
before: `<span>Step 1</span>`,
after: `Action`,
},
router: true,
})
expect(view.getByText('Step 1')).toBeInTheDocument()
expect(view.getByRole('heading', { level: 1 })).toBeInTheDocument()
expect(view.getByText('Action')).toBeInTheDocument()
})
})
})
Expand Up @@ -12,6 +12,7 @@ import CommonButtonGroup from '#mobile/components/CommonButtonGroup/CommonButton
import { useUserCreate } from '#mobile/entities/user/composables/useUserCreate.ts'
import CommonStepper from '#mobile/components/CommonStepper/CommonStepper.vue'
import { EnumSecurityStateType } from '#shared/components/Form/fields/FieldSecurity/types.ts'
import LayoutHeader from '#mobile/components/layout/LayoutHeader.vue'
const linkSchemaRaw = [
{
Expand Down Expand Up @@ -400,6 +401,14 @@ const logSubmit = console.log

<template>
<div class="p-4">
<LayoutHeader title="Playground">
<template #before>1 / 3</template>
<template #after>
<CommonButton class="flex-1 px-4 py-2" variant="secondary"
>Click
</CommonButton>
</template>
</LayoutHeader>
<h2 class="text-xl font-bold">Buttons</h2>
<div class="mt-2 flex gap-3">
<CommonButton class="flex-1 py-2" variant="primary" />
Expand Down
@@ -1,17 +1,18 @@
<!-- Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/ -->

<script setup lang="ts">
import { toRef } from 'vue'
import LayoutHeader from '#mobile/components/layout/LayoutHeader.vue'
import CommonUserAvatar from '#shared/components/CommonUserAvatar/CommonUserAvatar.vue'
import { toRef } from 'vue'
import { useDialog } from '#mobile/composables/useDialog.ts'
import CommonLoader from '#mobile/components/CommonLoader/CommonLoader.vue'
import CommonBackButton from '#mobile/components/CommonBackButton/CommonBackButton.vue'
import { useSessionStore } from '#shared/stores/session.ts'
import CommonRefetch from '#mobile/components/CommonRefetch/CommonRefetch.vue'
import type {
TicketById,
TicketLiveAppUser,
} from '#shared/entities/ticket/types.ts'
import CommonLoader from '#mobile/components/CommonLoader/CommonLoader.vue'
interface Props {
ticket?: TicketById
Expand Down Expand Up @@ -51,30 +52,28 @@ const showActions = () => {
</script>

<template>
<header
class="grid h-[64px] shrink-0 grid-cols-[75px_auto_75px] border-b-[0.5px] border-white/10 bg-gray-600/90 px-4"
<LayoutHeader
class="bg-gray-600/90"
:refetch="refetchingTicket || loadingTicket"
:back-ignore="[`/tickets/${ticket?.internalId}/information`]"
back-url="/"
>
<CommonBackButton
class="justify-self-start"
fallback="/"
:ignore="[`/tickets/${ticket?.internalId}/information`]"
/>
<CommonLoader data-test-id="loader-header" :loading="loadingTicket">
<div
class="flex flex-1 flex-col items-center justify-center text-center text-sm leading-4"
data-test-id="header-content"
>
<CommonRefetch :refetch="refetchingTicket">
<div class="font-bold">{{ ticket && `#${ticket.number}` }}</div>
<div class="text-gray">
{{
ticket &&
$t('created %s', i18n.relativeDateTime(ticket.createdAt))
}}
</div>
</CommonRefetch>
<div
class="flex flex-1 flex-col items-center justify-center text-center text-sm leading-4"
data-test-id="header-content"
>
<div class="font-bold">
{{ ticket && `#${ticket.number}` }}
</div>
<div class="text-gray">
{{
ticket && $t('created %s', i18n.relativeDateTime(ticket.createdAt))
}}
</div>
<div class="flex items-center justify-self-end">
</div>
<template #after>
<CommonLoader data-test-id="loader-header" :loading="loadingTicket">
<button
v-if="liveUserList?.length"
class="flex ltr:mr-0.5 rtl:ml-0.5"
Expand Down Expand Up @@ -106,7 +105,7 @@ const showActions = () => {
>
<CommonIcon name="more" size="base" decorative />
</button>
</div>
</CommonLoader>
</header>
</CommonLoader>
</template>
</LayoutHeader>
</template>

0 comments on commit 2c77470

Please sign in to comment.