Skip to content

Commit

Permalink
Feature: Mobile - Added first app initialization handling.
Browse files Browse the repository at this point in the history
  • Loading branch information
dominikklein committed May 6, 2022
1 parent fd39fb0 commit eaf5612
Show file tree
Hide file tree
Showing 47 changed files with 1,222 additions and 98 deletions.
21 changes: 21 additions & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,13 @@ module.exports = {
'vue/script-setup-uses-vars': 'error',
'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off',
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',
'vue/component-tags-order': [
'error',
{
order: ['template', 'script', 'style'],
},
],
// Disable the following rule, because it's not relevant for the tool chain and test envoirment.
'import/no-extraneous-dependencies': [
'error',
{ devDependencies: ['vite.config.ts', 'app/frontend/tests/**/*'] },
Expand All @@ -40,6 +47,19 @@ module.exports = {
tsx: 'never',
},
],
/* We strongly recommend that you do not use the no-undef lint rule on TypeScript projects. The checks it provides are already provided by TypeScript without the need for configuration - TypeScript just does this significantly better (Source: https://github.com/typescript-eslint/typescript-eslint/blob/master/docs/getting-started/linting/FAQ.md#i-get-errors-from-the-no-undef-rule-about-global-variables-not-being-defined-even-though-there-are-no-typescript-errors). */
'no-undef': 'off',

// We need to use the extended 'no-shadow' rule from typescript:
// https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/no-shadow.md
'no-shadow': 'off',
'@typescript-eslint/no-shadow': 'off',

// Enforce v-bind directive usage in long form.
'vue/v-bind-style': ['error', 'longform'],

// Enforce v-on directive usage in long form.
'vue/v-on-style': ['error', 'longform'],
},
overrides: [
{
Expand All @@ -53,6 +73,7 @@ module.exports = {
'import/resolver': {
alias: {
map: [
['@', path.resolve(__dirname, './app/frontend/')],
['@mobile', path.resolve(__dirname, './app/frontend/apps/mobile')],
['@common', path.resolve(__dirname, './app/frontend/common')],
],
Expand Down
8 changes: 6 additions & 2 deletions .graphql_code_generator.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,25 @@ config:
vueCompositionApiImportFrom: vue
generates:
./app/frontend/common/graphql/types.ts:
documents:
[
'app/frontend/common/graphql/**/*.graphql',
'app/frontend/apps/mobile/graphql/**/*.graphql',
]
plugins:
- typescript
- typescript-operations
./app/frontend/common/graphql/api.ts:
documents: 'app/frontend/common/graphql/**/*.graphql'
preset: import-types
presetConfig:
typesPath: './types'
plugins:
- typescript-operations
- typescript-vue-apollo
./app/frontend/apps/mobile/graphql/api.ts:
documents: 'app/frontend/apps/mobile/graphql/**/*.graphql'
preset: import-types
presetConfig:
typesPath: '@common/graphql/types'
plugins:
- typescript-operations
- typescript-vue-apollo
48 changes: 27 additions & 21 deletions app/frontend/apps/mobile/App.vue
Original file line number Diff line number Diff line change
@@ -1,25 +1,31 @@
<template>
<CommonNotifications />
<div
class="
h-full
max-h-full
overflow-auto
w-full
bg-gray-200
text-center text-sm
antialiased
font-sans
select-none
"
>
<router-view v-if="applicationLoaded.value" v-slot="{ Component }">
<transition>
<component v-bind:is="Component" />
</transition>
</router-view>
</div>
</template>

<script setup lang="ts">
// This starter template is using Vue 3 <script setup> SFCs
// Check out https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup
import CommonNotifications from '@common/components/CommonNotifications.vue'
import useApplicationLoadedStore from '@common/stores/application/loaded'
import { ref } from 'vue'
import CommonHelloWorld from '@common/components/CommonHelloWorld.vue'
const applicationLoaded = useApplicationLoadedStore()
const state = ref<boolean>(false)
applicationLoaded.setLoaded()
</script>

<template>
<input v-model="state" type="checkbox" /> Show output?
<CommonHelloWorld msg="Zammad Mobile!" :show="state" />
</template>

<style>
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 60px;
}
</style>
5 changes: 0 additions & 5 deletions app/frontend/apps/mobile/graphql/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,6 @@ import gql from 'graphql-tag';
import * as VueApolloComposable from '@vue/apollo-composable';
import * as VueCompositionApi from 'vue';
export type ReactiveFunction<TParam> = () => TParam;
export type ApplicationConfigQueryVariables = Types.Exact<{ [key: string]: never; }>;


export type ApplicationConfigQuery = { __typename?: 'Queries', applicationConfig: Array<{ __typename?: 'KeyComplexValue', key: string, value?: any | null | undefined }> };


export const ApplicationConfigDocument = gql`
query applicationConfig {
Expand Down
Empty file.
33 changes: 31 additions & 2 deletions app/frontend/apps/mobile/main.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,35 @@
import { createApp } from 'vue'
import App from '@mobile/App.vue'
import { DefaultApolloClient } from '@vue/apollo-composable'
import apolloClient from '@common/server/apollo/client'
import useSessionIdStore from '@common/stores/session/id'
import '@common/styles/main.css'
import initializeStore from '@common/stores'
import InitializeHandler from '@common/initializer'
import useApplicationConfigStore from '@common//stores/application/config'
import initializeRouter from '@common/router/index'
import routes from '@mobile/router'

export default function mountApp(): void {
createApp(App).mount('#app')
export default async function mountApp(): Promise<void> {
const app = createApp(App)

app.provide(DefaultApolloClient, apolloClient)

initializeStore(app)
initializeRouter(app, routes)

const initializer = new InitializeHandler(
app,
import.meta.globEager('/apps/mobile/initializer/*.ts'),
)

initializer.initialize()

const sessionId = useSessionIdStore()
await sessionId.checkSession()

const applicationConfig = useApplicationConfigStore()
await applicationConfig.getConfig()

app.mount('#app')
}
26 changes: 26 additions & 0 deletions app/frontend/apps/mobile/router/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { RouteRecordRaw } from 'vue-router'
import Login from '@mobile/views/Login.vue'
import Home from '@mobile/views/Home.vue'

// TODO ...extend "meta" in RouteRecordRaw with real type behind.

const routes: Array<RouteRecordRaw> = [
{
path: '/login',
name: 'Login',
props: true,
component: Login,
meta: {},
},
{
path: '/',
name: 'Home',
props: true,
component: Home,
meta: {
requiresAuth: true,
},
},
]

export default routes
38 changes: 38 additions & 0 deletions app/frontend/apps/mobile/views/Home.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<template>
<div>
<h1>Home</h1>
<p>{{ userData?.firstname }} {{ userData?.lastname }}</p>
<br />
<p v-on:click="logout">Logout</p>
</div>
</template>

<script setup lang="ts">
import useNotifications from '@common/composables/useNotifications'
import useAuthenticatedStore from '@common/stores/authenticated'
import useSessionUserStore from '@common/stores/session/user'
import { storeToRefs } from 'pinia'
import { useRouter } from 'vue-router'
// TODO ... testing the notification
const { notify } = useNotifications()
notify({
message: 'Hello Home!!!',
type: 'alert',
})
const sessionUser = useSessionUserStore()
const { value: userData } = storeToRefs(sessionUser)
const authenticated = useAuthenticatedStore()
const router = useRouter()
const logout = (): void => {
authenticated.logout().then(() => {
router.push('/login')
})
}
</script>
33 changes: 33 additions & 0 deletions app/frontend/apps/mobile/views/Login.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<template>
<div>
<h1>Login</h1>
<p>Username: <input v-model="loginFormValues.login" type="text" /></p>
<br />
<p>
Password: <input v-model="loginFormValues.password" type="password" />
</p>
<br />
<button v-on:click="login">Login</button>
</div>
</template>

<script setup lang="ts">
import useAuthenticationStore from '@common/stores/authenticated'
import { useRouter } from 'vue-router'
const authentication = useAuthenticationStore()
const loginFormValues = {
login: '',
password: '',
}
const router = useRouter()
const login = (): void => {
authentication
.login(loginFormValues.login, loginFormValues.password)
.then(() => {
router.replace('/')
})
}
</script>
12 changes: 4 additions & 8 deletions app/frontend/common/components/CommonHelloWorld.vue
Original file line number Diff line number Diff line change
@@ -1,14 +1,10 @@
<script setup lang="ts">
import { ref } from 'vue'
defineProps<{ msg: string; show: boolean }>()
const count = ref(0)
</script>

<template>
<div v-if="show">
<h1>{{ msg }}</h1>
<p>The first component.</p>
</div>
</template>

<script setup lang="ts">
defineProps<{ msg: string; show: boolean }>()
</script>
30 changes: 30 additions & 0 deletions app/frontend/common/components/CommonNotifications.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<template>
<div class="w-full flex justify-center">
<div class="fixed z-50" style="top: 30px; right: 0">
<transition-group
tag="div"
enter-class="opacity-0"
leave-active-class="transition-opacity duration-1000 opacity-0"
>
<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="assets/images/icons.svg#icon-diagonal-cross"
></use>
</svg>
<span class="ml-2">{{ notification.message }}</span>
</div>
</div>
</div>
</transition-group>
</div>
</div>
</template>

<script setup lang="ts">
import useNotifications from '@common/composables/useNotifications'
const { notifications } = useNotifications()
</script>
48 changes: 48 additions & 0 deletions app/frontend/common/composables/useNotifications.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { v4 as uuid } from 'uuid'
import { ref } from 'vue'

interface NewNotificationInterface {
id?: string
message: string
messagePlaceholder?: string[]
type: string // TODO type for different types or enum?
duration?: number
}

interface NotificationInterface extends NewNotificationInterface {
id: string
}

const notifications = ref<NotificationInterface[]>([])
const defaultNotificationDuration = 5000

function removeNotification(id: string) {
notifications.value = notifications.value.filter(
(notification: NotificationInterface) => notification.id !== id,
)
}

export default function useNotifications() {
function notify(notification: NewNotificationInterface): string {
// TODO: Check different solution for the optional id in the interface, but required field in the removeNotification function.
let { id } = notification
if (!id) {
id = uuid()
}

const newNotification: NotificationInterface = { id, ...notification }

notifications.value.push(newNotification)

setTimeout(() => {
removeNotification(newNotification.id)
}, newNotification.duration || defaultNotificationDuration)

return newNotification.id
}

return {
notify,
notifications,
}
}
Loading

0 comments on commit eaf5612

Please sign in to comment.