Browse Source

Feature: Mobile - Added CommonLink component.

Dominik Klein 3 years ago
parent
commit
8c2f641c0d

+ 10 - 1
.storybook/preview.ts

@@ -6,12 +6,21 @@ import { i18n } from '@common/utils/i18n'
 import { app } from '@storybook/vue3'
 import 'virtual:svg-icons-register' // eslint-disable-line import/no-unresolved
 import initializeStore from '@common/stores'
+import { createRouter, createWebHashHistory, type Router } from 'vue-router'
 
-// adds translation to app
+// Adds the translations to storybook.
 app.config.globalProperties.i18n = i18n
+
+// Initialize the needed core components and plugins.
 initializeGlobalComponents(app)
 initializeStore(app)
 
+const router: Router = createRouter({
+  history: createWebHashHistory(),
+  routes: [],
+})
+app.use(router)
+
 export default {
   actions: { argTypesRegex: '^on[A-Z].*' },
   controls: {

+ 10 - 1
app/frontend/apps/mobile/views/Home.vue

@@ -9,6 +9,16 @@
     <br />
     <p v-on:click="goToTickets">Go to Tickets</p>
     <br />
+    <CommonLink v-bind:link="{ name: 'TicketOverview' }">
+      <span>Test Route Link</span>
+    </CommonLink>
+    <br />
+    <CommonLink v-bind:link="{ name: 'Login' }" v-bind:disabled="true">
+      <span>DisabledTest Route Link</span>
+    </CommonLink>
+    <br />
+    <CommonLink link="https://www.google.com"> Test External Link </CommonLink>
+    <br />
     <p v-on:click="refetchConfig">refetchConfig</p>
     <br />
     <p v-on:click="fetchCurrentUser">fetchCurrentUser</p>
@@ -67,7 +77,6 @@ const refetchConfig = async (): Promise<void> => {
 
 const fetchCurrentUser = () => {
   const { result } = useCurrentUserQuery({ fetchPolicy: 'no-cache' })
-  console.log('result', result)
 }
 
 const goToTickets = () => {

+ 96 - 0
app/frontend/common/components/common/CommonLink.vue

@@ -0,0 +1,96 @@
+<!-- Copyright (C) 2012-2022 Zammad Foundation, https://zammad-foundation.org/ -->
+
+<template>
+  <router-link
+    v-if="isInternalLink"
+    v-bind:to="link"
+    v-bind:replace="replace"
+    v-bind:class="linkClass"
+    v-bind:active-class="activeClass"
+    v-bind:exact-active-class="exactActiveClass"
+    v-bind:target="target"
+    v-on:click="onClick"
+  >
+    <slot></slot>
+  </router-link>
+  <a
+    v-else
+    v-bind:href="(link as string)"
+    v-bind:target="target"
+    v-bind:rel="rel"
+    v-bind:class="linkClass"
+    v-on:click="onClick"
+  >
+    <slot></slot>
+  </a>
+</template>
+
+<script setup lang="ts">
+import { Link } from '@common/types/router'
+import isRouteLink from '@common/router/utils/isRouteLink'
+import { computed } from 'vue'
+import stopEvent from '@common/utils/events'
+
+interface Props {
+  link: Link
+  isExternal?: boolean
+  isRoute?: boolean
+  disabled?: boolean
+  rel?: string
+  target?: string
+  openInNewTab?: boolean
+  replace?: boolean
+  activeClass?: string
+  exactActiveClass?: string
+}
+
+const props = withDefaults(defineProps<Props>(), {
+  isExternal: false,
+  isRoute: false,
+  replace: false,
+  append: false,
+  openInNewTab: false,
+  disabled: false,
+})
+
+const emit = defineEmits<{
+  (e: 'click', event: MouseEvent): void
+}>()
+
+const isInternalLink = computed(() => {
+  if (props.isExternal) return false
+  if (props.isRoute) return true
+
+  return isRouteLink(props.link)
+})
+
+const target = computed(() => {
+  if (props.target) return props.target
+  if (props.openInNewTab) return '_blank'
+  return null
+})
+
+// TODO: Correct styling is currently missing.
+const linkClass = computed(() => {
+  let classes = 'text-blue hover:underline'
+
+  if (props.disabled) {
+    classes += ' pointer-events-none text-gray-100/75'
+  }
+
+  return classes
+})
+
+const onClick = (event: MouseEvent) => {
+  if (props.disabled) {
+    stopEvent(event, { immediatePropagation: true })
+    return
+  }
+  emit('click', event)
+
+  // Stop the scroll-to-top behavior or navigation on regular links when href is just '#'.
+  if (!isInternalLink.value && props.link === '#') {
+    stopEvent(event, { propagation: false })
+  }
+}
+</script>

+ 1 - 1
app/frontend/common/components/common/CommonLogo.vue

@@ -4,7 +4,7 @@
   <img
     class="w-40 h-40"
     v-bind:src="logoUrl"
-    v-bind:alt="config.get('product_name') as string"
+    v-bind:alt="(config.get('product_name') as string)"
   />
 </template>
 

+ 3 - 0
app/frontend/common/initializer/globalComponents.ts

@@ -2,13 +2,16 @@
 
 import type { App } from 'vue'
 import CommonIcon from '@common/components/common/CommonIcon.vue'
+import CommonLink from '@common/components/common/CommonLink.vue'
 
 declare module '@vue/runtime-core' {
   export interface GlobalComponents {
     CommonIcon: typeof CommonIcon
+    CommonLink: typeof CommonLink
   }
 }
 
 export default function initializeGlobalComponents(app: App): void {
   app.component('CommonIcon', CommonIcon)
+  app.component('CommonLink', CommonLink)
 }

+ 17 - 0
app/frontend/common/router/utils/isRouteLink.ts

@@ -0,0 +1,17 @@
+// Copyright (C) 2012-2022 Zammad Foundation, https://zammad-foundation.org/
+
+import { Link } from '@common/types/router'
+import { useRouter } from 'vue-router'
+
+export default function isRouteLink(link: Link): boolean {
+  if (typeof link === 'object') return true
+
+  const router = useRouter()
+  const resolved = router.resolve(link)
+
+  return (
+    resolved !== null &&
+    resolved.matched.length > 0 &&
+    resolved.name !== 'Error'
+  )
+}

+ 7 - 0
app/frontend/common/types/events.ts

@@ -0,0 +1,7 @@
+// Copyright (C) 2012-2022 Zammad Foundation, https://zammad-foundation.org/
+
+export interface StopEventOptions {
+  preventDefault?: boolean
+  propagation?: boolean
+  immediatePropagation?: boolean
+}

+ 4 - 0
app/frontend/common/types/router.ts

@@ -1,5 +1,7 @@
 // Copyright (C) 2012-2022 Zammad Foundation, https://zammad-foundation.org/
 
+import { RouteRecordRaw } from 'vue-router'
+
 export interface RouteRecordMeta {
   title?: string
   requiresAuth: boolean
@@ -7,3 +9,5 @@ export interface RouteRecordMeta {
   hasBottomNavigation?: boolean
   level?: number
 }
+
+export type Link = string | Partial<RouteRecordRaw>

+ 23 - 0
app/frontend/common/utils/events.ts

@@ -0,0 +1,23 @@
+// Copyright (C) 2012-2022 Zammad Foundation, https://zammad-foundation.org/
+
+import { StopEventOptions } from '@common/types/events'
+
+const stopEvent = (event: Event, stopOptions: StopEventOptions): void => {
+  const {
+    preventDefault = true,
+    propagation = true,
+    immediatePropagation = false,
+  }: StopEventOptions = stopOptions
+
+  if (preventDefault) {
+    event.preventDefault()
+  }
+  if (propagation) {
+    event.stopPropagation()
+  }
+  if (immediatePropagation) {
+    event.stopImmediatePropagation()
+  }
+}
+
+export default stopEvent

+ 58 - 0
app/frontend/stories/common/CommonLink.stories.ts

@@ -0,0 +1,58 @@
+// Copyright (C) 2012-2022 Zammad Foundation, https://zammad-foundation.org/
+
+import CommonLink from '@common/components/common/CommonLink.vue'
+import { Story } from '@storybook/vue3'
+
+export default {
+  title: 'Common/Link',
+  component: CommonLink,
+  args: {
+    link: '',
+    isExternal: false,
+    isRoute: false,
+    disabled: false,
+    rel: '',
+    target: '',
+    openInNewTab: false,
+    replace: false,
+    activeClass: '',
+    exactActiveClass: '',
+  },
+  parameters: {
+    actions: {
+      handles: ['click'],
+    },
+  },
+}
+
+const Template: Story = (args) => ({
+  components: { CommonLink },
+  setup() {
+    return { args }
+  },
+  template: '<CommonLink v-bind="args">A test Link</CommonLink>',
+})
+
+export const BasicLink = Template.bind({})
+BasicLink.args = {
+  link: 'https://www.google.com',
+}
+
+export const ExternalLink = Template.bind({})
+ExternalLink.args = {
+  link: 'https://www.google.com',
+  isExternal: true,
+  openInNewTab: true,
+}
+
+export const RouterLink = Template.bind({})
+RouterLink.args = {
+  link: '/login',
+  isRoute: true,
+}
+
+export const DisabledLink = Template.bind({})
+DisabledLink.args = {
+  link: '/login',
+  disabled: true,
+}

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