Просмотр исходного кода

Feature: Mobile - Added CommonIcon component and use vite plugin for svg sprite file generation.

Romit Choudhary 3 лет назад
Родитель
Сommit
f18d2232b4

+ 27 - 7
.storybook/main.ts

@@ -3,20 +3,40 @@ const path = require('path')
 
 module.exports = {
   stories: ['../app/**/*.stories.mdx', '../app/**/*.stories.@(js|jsx|ts|tsx)'],
-  addons: ['@storybook/addon-links', '@storybook/addon-essentials'],
+  addons: [
+    '@storybook/addon-links',
+    '@storybook/addon-essentials',
+    {
+      name: '@storybook/addon-postcss',
+      options: {
+        cssLoaderOptions: {
+          // When you have splitted your css over multiple files
+          // and use @import('./other-styles.css')
+          importLoaders: 1,
+        },
+        postcssLoaderOptions: {
+          // When using postCSS 8
+          implementation: require('postcss'),
+        },
+      },
+    },
+  ],
   framework: '@storybook/vue3',
   core: {
     builder: 'storybook-builder-vite',
   },
-  async viteFinal(config: unknown) {
-    const { config: userConfig } = await loadConfigFromFile(
+  async viteFinal(storybookViteConfig: any) {
+    const { config } = await loadConfigFromFile(
       path.resolve(__dirname, '../vite.config.ts'),
     )
 
-    return mergeConfig(config, {
-      ...userConfig,
-      // manually specify plugins to avoid conflict
-      plugins: [],
+    return mergeConfig(storybookViteConfig, {
+      ...config,
+
+      // Manually specify plugins to avoid conflicts.
+      plugins: [
+        config.plugins.find((plugin: any) => plugin.name === 'vite:svg-icons')
+      ],
     })
   },
 }

+ 3 - 1
.storybook/preview.ts

@@ -1,5 +1,7 @@
 import { app } from '@storybook/vue3';
 import { i18n } from '@common/utils/i18n';
+import 'virtual:svg-icons-register' // eslint-disable-line import/no-unresolved
+import '@common/styles/main.css'
 
 // adds translation to app
 app.config.globalProperties.i18n = i18n
@@ -12,4 +14,4 @@ export const parameters = {
       date: /Date$/,
     },
   },
-}
+}

+ 5 - 1
app/frontend/apps/mobile/main.ts

@@ -11,13 +11,15 @@ import useSessionIdStore from '@common/stores/session/id'
 import '@common/styles/main.css'
 import initializeStore from '@common/stores'
 import initializeStoreSubscriptions from '@common/initializer/storeSubscriptions'
-import useApplicationConfigStore from '@common//stores/application/config'
 import initializeRouter from '@common/router/index'
+import initializeGlobalComponents from '@common/initializer/globalComponents'
 import routes from '@mobile/router'
+import useApplicationConfigStore from '@common//stores/application/config'
 import { i18n } from '@common/utils/i18n'
 import useLocaleStore from '@common/stores/locale'
 import useSessionUserStore from '@common/stores/session/user'
 import useAuthenticatedStore from '@common/stores/authenticated'
+import 'virtual:svg-icons-register' // eslint-disable-line import/no-unresolved
 
 const enableLoadingAnimation = (): void => {
   const loadingElement: Maybe<HTMLElement> =
@@ -40,6 +42,8 @@ export default async function mountApp(): Promise<void> {
   initializeStore(app)
   initializeRouter(app, routes)
 
+  initializeGlobalComponents(app)
+
   initializeStoreSubscriptions()
 
   const sessionId = useSessionIdStore()

+ 5 - 6
app/frontend/apps/mobile/views/Login.vue

@@ -68,17 +68,16 @@
 
       <div class="flex justify-center items-center align-baseline p-6">
         <a href="https://zammad.org" target="_blank">
-          <svg class="w-10 h-10">
-            <use xlink:href="@common/assets/icons.svg#icon-logo"></use>
-          </svg>
+          <CommonIcon name="logo" size="large" />
         </a>
 
         <span class="mx-1">{{ i18n.t('Powered by') }}</span>
 
         <a class="ml-1 -mt-1" href="https://zammad.org" target="_blank">
-          <svg class="w-20 h-10 fill-current">
-            <use xlink:href="@common/assets/icons.svg#icon-logotype"></use>
-          </svg>
+          <CommonIcon
+            name="logotype"
+            v-bind:fixed-size="{ width: 80, height: 14 }"
+          />
         </a>
       </div>
     </div>

Разница между файлами не показана из-за своего большого размера
+ 0 - 289
app/frontend/common/assets/icons.svg


+ 75 - 0
app/frontend/common/components/common/CommonIcon.vue

@@ -0,0 +1,75 @@
+<!-- Copyright (C) 2012-2021 Zammad Foundation, https://zammad-foundation.org/ -->
+
+<template>
+  <svg
+    xmlns="http://www.w3.org/2000/svg"
+    class="icon fill-current"
+    v-bind:class="iconClass"
+    v-bind:width="finalSize.width"
+    v-bind:height="finalSize.height"
+    v-bind:aria-labelledby="name"
+    v-bind:aria-hidden="decorative"
+    v-on:click="onClick"
+  >
+    <use v-bind:xlink:href="`#icon-${name}`" />
+  </svg>
+</template>
+
+<script setup lang="ts">
+import { computed } from 'vue'
+
+const animationClassMap = {
+  pulse: 'animate-pulse',
+  spin: 'animate-spin',
+  ping: 'animate-ping',
+  bounce: 'animate-bounce',
+} as const
+
+type Animations = keyof typeof animationClassMap
+
+const sizeMap = {
+  small: 20,
+  medium: 30,
+  large: 40,
+} as const
+
+type Sizes = keyof typeof sizeMap
+
+interface Props {
+  size?: Sizes
+  fixedSize?: { width: number; height: number }
+  name: string
+  decorative?: boolean
+  animation?: Animations
+}
+
+const props = withDefaults(defineProps<Props>(), {
+  size: 'medium',
+  decorative: false,
+})
+
+const emit = defineEmits<{
+  (e: 'click', event: MouseEvent): void
+}>()
+
+const onClick = (event: MouseEvent) => {
+  emit('click', event)
+}
+
+const iconClass = computed(() => {
+  let className = `icon-${props.name}`
+  if (props.animation) {
+    className += ` ${animationClassMap[props.animation]}`
+  }
+  return className
+})
+
+const finalSize = computed(() => {
+  if (props.fixedSize) return props.fixedSize
+
+  return {
+    width: sizeMap[props.size],
+    height: sizeMap[props.size],
+  }
+})
+</script>

+ 4 - 5
app/frontend/common/components/common/CommonNotifications.vue

@@ -11,11 +11,10 @@
         <div v-for="notification in notifications" v-bind:key="notification.id">
           <div class="flex justify-center">
             <div class="flex items-center bg-black p-2 rounded text-white m-1">
-              <svg class="w-4 h-4 fill-current text-red-600">
-                <use
-                  xlink:href="@common/assets/icons.svg#icon-diagonal-cross"
-                ></use>
-              </svg>
+              <CommonIcon
+                name="diagonal-cross"
+                v-bind:fixed-size="{ width: 16, height: 16 }"
+              />
               <span class="ml-2">{{ notification.message }}</span>
             </div>
           </div>

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

@@ -0,0 +1,14 @@
+// Copyright (C) 2012-2021 Zammad Foundation, https://zammad-foundation.org/
+
+import { App } from 'vue'
+import CommonIcon from '@common/components/common/CommonIcon.vue'
+
+declare module '@vue/runtime-core' {
+  export interface GlobalComponents {
+    CommonIcon: typeof CommonIcon
+  }
+}
+
+export default function initializeGlobalComponents(app: App): void {
+  app.component('CommonIcon', CommonIcon)
+}

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

@@ -0,0 +1,58 @@
+// Copyright (C) 2012-2021 Zammad Foundation, https://zammad-foundation.org/
+
+import { Story } from '@storybook/vue3'
+import CommonIcon from '@common/components/common/CommonIcon.vue'
+import ids from 'virtual:svg-icons-names' // eslint-disable-line import/no-unresolved
+
+const iconsList = ids.map((item) => item.substring(5))
+
+export default {
+  title: 'Common/Icon',
+  component: CommonIcon,
+  args: {
+    name: '',
+    size: 'medium',
+    fixedSize: null,
+    decorative: false,
+    animation: '',
+  },
+  argTypes: {
+    name: {
+      control: { type: 'select' },
+      options: iconsList,
+    },
+    size: {
+      control: { type: 'select' },
+      options: ['small', 'medium', 'large'],
+    },
+    animation: {
+      control: { type: 'select' },
+      options: ['pulse', 'spin', 'ping', 'bounce'],
+    },
+  },
+}
+
+const Template: Story = (args) => ({
+  components: { CommonIcon },
+  setup() {
+    return { args }
+  },
+  template: '<CommonIcon v-bind="args" />',
+})
+
+export const BaseIcon = Template.bind({})
+BaseIcon.args = {
+  name: 'arrow-left',
+}
+
+export const BaseIconAnimated = Template.bind({})
+BaseIconAnimated.args = {
+  name: 'cog',
+  animation: 'spin',
+}
+
+export const BaseIconDecorative = Template.bind({})
+BaseIconDecorative.args = {
+  name: 'dashboard',
+  decorative: true,
+}

+ 42 - 0
app/frontend/tests/common/components/common/CommonIcon.spec.ts

@@ -0,0 +1,42 @@
+// Copyright (C) 2012-2021 Zammad Foundation, https://zammad-foundation.org/
+
+import { shallowMount } from '@vue/test-utils'
+import CommonIcon from '@common/components/common/CommonIcon.vue'
+
+describe('CommonIcon.vue', () => {
+  it('renders icon', () => {
+    const wrapper = shallowMount(CommonIcon, {
+      props: { name: 'arrow-left' },
+    })
+    expect(wrapper.classes()).toContain('icon')
+  })
+  it('renders icon with animation', () => {
+    const wrapper = shallowMount(CommonIcon, {
+      props: { name: 'cog', animation: 'spin' },
+    })
+    expect(wrapper.classes()).toContain('animate-spin')
+  })
+  it('renders icon with small size', () => {
+    const wrapper = shallowMount(CommonIcon, {
+      props: { name: 'cog', size: 'small' },
+    })
+
+    expect(wrapper.attributes().width).toEqual('20')
+    expect(wrapper.attributes().height).toEqual('20')
+  })
+  it('renders a decorative icon', () => {
+    const wrapper = shallowMount(CommonIcon, {
+      props: { name: 'cog', decorative: true },
+    })
+
+    expect(wrapper.attributes()['aria-hidden']).toEqual('true')
+  })
+  it('triggers click handler of icon', () => {
+    const wrapper = shallowMount(CommonIcon, {
+      props: { name: 'dashboard' },
+    })
+
+    wrapper.trigger('click')
+    expect(wrapper.emitted('click')).toHaveLength(1)
+  })
+})

Некоторые файлы не были показаны из-за большого количества измененных файлов