Migration to typescript: first step

Add vue cli typescript support
Rename .js to .ts
Use class and annotations in App and NavBar
Add tslint
This commit is contained in:
Chocobozzz 2018-12-21 15:41:34 +01:00
parent da817d35c4
commit b409a5583d
No known key found for this signature in database
GPG key ID: 583A612D890159BE
25 changed files with 712 additions and 296 deletions

View file

@ -1,7 +0,0 @@
module.exports = {
root: true,
extends: [
'plugin:vue/essential',
'@vue/airbnb',
],
};

516
js/package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -2,16 +2,13 @@
"name": "mobilizon", "name": "mobilizon",
"version": "0.1.0", "version": "0.1.0",
"private": true, "private": true,
"engines": {
"node": ">=10.0.0"
},
"scripts": { "scripts": {
"dev": "vue-cli-service serve",
"build": "vue-cli-service build --modern", "build": "vue-cli-service build --modern",
"lint": "vue-cli-service lint", "lint": "vue-cli-service lint",
"analyze-bundle": "npm run build -- --report-json && webpack-bundle-analyzer ../priv/static/report.json",
"dev": "vue-cli-service serve",
"test:e2e": "vue-cli-service test:e2e", "test:e2e": "vue-cli-service test:e2e",
"test:unit": "vue-cli-service test:unit", "test:unit": "vue-cli-service test:unit"
"analyze-bundle": "npm run build -- --report-json && webpack-bundle-analyzer ../priv/static/report.json"
}, },
"dependencies": { "dependencies": {
"apollo-absinthe-upload-link": "^1.4.0", "apollo-absinthe-upload-link": "^1.4.0",
@ -26,27 +23,35 @@
"register-service-worker": "^1.4.1", "register-service-worker": "^1.4.1",
"vue": "^2.5.17", "vue": "^2.5.17",
"vue-apollo": "^3.0.0-beta.26", "vue-apollo": "^3.0.0-beta.26",
"vue-class-component": "^6.3.2",
"vue-gettext": "^2.1.1", "vue-gettext": "^2.1.1",
"vue-gravatar": "^1.3.0", "vue-gravatar": "^1.3.0",
"vue-markdown": "^2.2.4", "vue-markdown": "^2.2.4",
"vue-property-decorator": "^7.2.0",
"vue-router": "^3.0.2", "vue-router": "^3.0.2",
"vuetify": "^1.3.9", "vuetify": "^1.3.9",
"vuetify-google-autocomplete": "^2.0.0-beta.5", "vuetify-google-autocomplete": "^2.0.0-beta.5",
"vuex": "^3.0.1" "vuex": "^3.0.1"
}, },
"devDependencies": { "devDependencies": {
"@types/chai": "^4.1.0",
"@types/mocha": "^5.2.4",
"@vue/cli-plugin-babel": "^3.1.1", "@vue/cli-plugin-babel": "^3.1.1",
"@vue/cli-plugin-e2e-nightwatch": "^3.1.1", "@vue/cli-plugin-e2e-nightwatch": "^3.1.1",
"@vue/cli-plugin-eslint": "^3.1.5", "@vue/cli-plugin-eslint": "^3.1.5",
"@vue/cli-plugin-pwa": "^3.1.2", "@vue/cli-plugin-pwa": "^3.1.2",
"@vue/cli-plugin-typescript": "^3.2.0",
"@vue/cli-plugin-unit-mocha": "^3.1.1", "@vue/cli-plugin-unit-mocha": "^3.1.1",
"@vue/cli-service": "^3.1.4", "@vue/cli-service": "^3.1.4",
"@vue/eslint-config-airbnb": "^3.0.5", "@vue/eslint-config-airbnb": "^3.0.5",
"@vue/eslint-config-typescript": "^3.1.0",
"@vue/test-utils": "^1.0.0-beta.26", "@vue/test-utils": "^1.0.0-beta.26",
"chai": "^4.2.0", "chai": "^4.2.0",
"dotenv-webpack": "^1.5.7", "dotenv-webpack": "^1.5.7",
"node-sass": "^4.10.0", "node-sass": "^4.10.0",
"sass-loader": "^7.1.0", "sass-loader": "^7.1.0",
"tslint-config-airbnb": "^5.11.1",
"typescript": "^3.0.0",
"vue-cli-plugin-apollo": "^0.17.4", "vue-cli-plugin-apollo": "^0.17.4",
"vue-template-compiler": "^2.5.17", "vue-template-compiler": "^2.5.17",
"webpack-bundle-analyzer": "^3.0.3" "webpack-bundle-analyzer": "^3.0.3"
@ -55,5 +60,8 @@
"> 1%", "> 1%",
"last 2 versions", "last 2 versions",
"not ie <= 8" "not ie <= 8"
] ],
"engines": {
"node": ">=10.0.0"
}
} }

View file

@ -14,15 +14,15 @@
> >
<v-list-tile avatar v-if="actor" slot="activator"> <v-list-tile avatar v-if="actor" slot="activator">
<v-list-tile-avatar> <v-list-tile-avatar>
<img v-if="!actor.avatar" <img v-if="!actor.avatar"
class="img-circle elevation-7 mb-1" class="img-circle elevation-7 mb-1"
src="https://picsum.photos/125/125/" src="https://picsum.photos/125/125/"
> >
<img v-else <img v-else
class="img-circle elevation-7 mb-1" class="img-circle elevation-7 mb-1"
:src="actor.avatar" :src="actor.avatar"
> >
</v-list-tile-avatar> </v-list-tile-avatar>
<v-list-tile-content @click="$router.push({name: 'Account', params: { name: actor.username }})"> <v-list-tile-content @click="$router.push({name: 'Account', params: { name: actor.username }})">
<v-list-tile-title>{{ this.displayed_name }}</v-list-tile-title> <v-list-tile-title>{{ this.displayed_name }}</v-list-tile-title>
@ -31,11 +31,11 @@
<v-list-tile avatar v-if="actor"> <v-list-tile avatar v-if="actor">
<v-list-tile-avatar> <v-list-tile-avatar>
<img <img
class="img-circle elevation-7 mb-1" class="img-circle elevation-7 mb-1"
src="https://picsum.photos/125/125/" src="https://picsum.photos/125/125/"
> >
</v-list-tile-avatar> </v-list-tile-avatar>
<v-list-tile-content> <v-list-tile-content>
<v-list-tile-title>Autre identité</v-list-tile-title> <v-list-tile-title>Autre identité</v-list-tile-title>
@ -44,8 +44,8 @@
<v-list-tile @click="$router.push({ name: 'Identities' })"> <v-list-tile @click="$router.push({ name: 'Identities' })">
<v-list-tile-action> <v-list-tile-action>
<v-icon>group</v-icon> <v-icon>group</v-icon>
</v-list-tile-action> </v-list-tile-action>
<v-list-tile-content> <v-list-tile-content>
<v-list-tile-title>Identities</v-list-tile-title> <v-list-tile-title>Identities</v-list-tile-title>
</v-list-tile-content> </v-list-tile-content>
@ -100,7 +100,7 @@
transition="scale-transition" transition="scale-transition"
v-if="user" v-if="user"
> >
<v-btn <v-btn
slot="activator" slot="activator"
v-model="fab" v-model="fab"
color="blue darken-2" color="blue darken-2"
@ -134,7 +134,8 @@
class="white--text" class="white--text"
v-translate="{ v-translate="{
date: new Date().getFullYear(), date: new Date().getFullYear(),
}">© The Mobilizon Contributors %{date} - Made with Elixir, Phoenix & <a href="https://vuejs.org/">VueJS</a> & <a href="https://www.vuetifyjs.com/">Vuetify</a> with some love and some weeks }">© The Mobilizon Contributors %{date} - Made with Elixir, Phoenix & <a href="https://vuejs.org/">VueJS</a> & <a
href="https://www.vuetifyjs.com/">Vuetify</a> with some love and some weeks
</span> </span>
</v-footer> </v-footer>
<v-snackbar <v-snackbar
@ -148,75 +149,78 @@
</v-app> </v-app>
</template> </template>
<script> <script lang="ts">
import gql from 'graphql-tag'; import NavBar from '@/components/NavBar.vue';
import NavBar from '@/components/NavBar'; import { Component, Vue } from 'vue-property-decorator';
import { AUTH_USER_ACTOR, AUTH_USER_ID } from '@/constants'; import { AUTH_USER_ACTOR, AUTH_USER_ID } from '@/constants';
export default { @Component({
name: 'app', components: {
components: { NavBar
NavBar, }
}, })
data() { export default class App extends Vue {
return { drawer = false
drawer: false, fab = false
fab: false, user = localStorage.getItem(AUTH_USER_ID)
user: localStorage.getItem(AUTH_USER_ID), items = [
items: [ {
{ icon: 'poll', text: 'Events', route: 'EventList', role: null
icon: 'poll', text: 'Events', route: 'EventList', role: null,
},
{
icon: 'group', text: 'Groups', route: 'GroupList', role: null,
},
{
icon: 'content_copy', text: 'Categories', route: 'CategoryList', role: 'ROLE_ADMIN',
},
{ icon: 'settings', text: 'Settings', role: 'ROLE_USER' },
{ icon: 'chat_bubble', text: 'Send feedback', role: 'ROLE_USER' },
{ icon: 'help', text: 'Help', role: null },
{ icon: 'phonelink', text: 'App downloads', role: null },
],
error: {
timeout: 3000,
show: false,
text: '',
}, },
show_new_event_button: false, {
actor: localStorage.getItem(AUTH_USER_ACTOR), icon: 'group', text: 'Groups', route: 'GroupList', role: null
}; },
}, {
methods: { icon: 'content_copy', text: 'Categories', route: 'CategoryList', role: 'ROLE_ADMIN'
showMenuItem(elem) { },
return elem !== null && this.user && this.user.roles !== undefined ? this.user.roles.includes(elem) : true; { icon: 'settings', text: 'Settings', role: 'ROLE_USER' },
}, { icon: 'chat_bubble', text: 'Send feedback', role: 'ROLE_USER' },
getUser() { { icon: 'help', text: 'Help', role: null },
return this.user === undefined ? false : this.user; { icon: 'phonelink', text: 'App downloads', role: null }
}, ]
toggleDrawer() { error = {
this.drawer = !this.drawer; timeout: 3000,
}, show: false,
}, text: ''
computed: { }
displayed_name() {
return this.actor.display_name === null ? this.actor.username : this.actor.display_name; show_new_event_button = false
}, actor = localStorage.getItem(AUTH_USER_ACTOR)
},
}; get displayed_name () {
// FIXME: load actor
return 'no implemented'
// return this.actor.display_name === null ? this.actor.username : this.actor.display_name
}
showMenuItem (elem) {
// FIXME: load actor
return false
// return elem !== null && this.user && this.user.roles !== undefined ? this.user.roles.includes(elem) : true
}
getUser () {
return this.user === undefined ? false : this.user
}
toggleDrawer () {
this.drawer = !this.drawer
}
}
</script> </script>
<style> <style>
.router-enter-active, .router-leave-active { .router-enter-active, .router-leave-active {
transition-property: opacity; transition-property: opacity;
transition-duration: .25s; transition-duration: .25s;
} }
.router-enter-active { .router-enter-active {
transition-delay: .25s; transition-delay: .25s;
} }
.router-enter, .router-leave-active { .router-enter, .router-leave-active {
opacity: 0 opacity: 0
} }
</style> </style>

View file

@ -74,69 +74,63 @@
</v-list> </v-list>
<v-card-actions> <v-card-actions>
<v-spacer></v-spacer> <v-spacer></v-spacer>
<v-btn flat @click="notificationMenu = false"><translate>Close</translate></v-btn> <v-btn flat @click="notificationMenu = false">
<v-btn color="primary" flat @click="notificationMenu = false"><translate>Save</translate></v-btn> <translate>Close</translate>
</v-btn>
<v-btn color="primary" flat @click="notificationMenu = false">
<translate>Save</translate>
</v-btn>
</v-card-actions> </v-card-actions>
</v-card> </v-card>
</v-menu> </v-menu>
<v-btn v-if="!user" :to="{ name: 'Login' }"><translate>Login</translate></v-btn> <v-btn v-if="!user" :to="{ name: 'Login' }">
<translate>Login</translate>
</v-btn>
</v-toolbar> </v-toolbar>
</template> </template>
<script> <style>
import {AUTH_USER_ACTOR, AUTH_USER_ID} from '@/constants'; nav.v-toolbar .v-input__slot {
import {SEARCH} from '@/graphql/search'; margin-bottom: 0;
}
</style>
export default { <script lang="ts">
name: 'NavBar', import { Component, Prop, Vue, Watch } from 'vue-property-decorator';
props: { import { AUTH_USER_ACTOR, AUTH_USER_ID } from '@/constants';
toggleDrawer: { import { SEARCH } from '@/graphql/search';
type: Function,
required: true, @Component({
}, apollo: {
}, search: {
data() { query: SEARCH,
return { variables() {
notificationMenu: false, return {
notifications: [ searchText: this.searchText,
{ header: 'Coucou' }, };
{ title: "T'as une notification", subtitle: 'Et elle est cool' }, },
], skip() {
model: null, return !this.searchText;
search: [], },
searchText: null,
searchSelect: null,
actor: localStorage.getItem(AUTH_USER_ACTOR),
user: localStorage.getItem(AUTH_USER_ID),
};
},
apollo: {
search: {
query: SEARCH,
variables() {
return {
searchText: this.searchText,
};
}, },
skip() { }
return !this.searchText; })
}, export default class NavBar extends Vue {
}, @Prop({ required: true, type: Function }) toggleDrawer!: Function;
},
watch: { notificationMenu = false;
model(val) { notifications = [
switch(val.__typename) { { header: 'Coucou' },
case 'Event': { title: 'T\'as une notification', subtitle: 'Et elle est cool' },
this.$router.push({ name: 'Event', params: { uuid: val.uuid } }); ];
break; model = null;
case 'Actor': search: any[] = [];
this.$router.push({ name: 'Account', params: { name: this.username_with_domain(val) } }); searchText: string | null = null;
break; searchSelect = null;
} actor: string | null = localStorage.getItem(AUTH_USER_ACTOR);
}, user: string | null = localStorage.getItem(AUTH_USER_ID);
},
computed: { get items() {
items() {
return this.search.map(searchEntry => { return this.search.map(searchEntry => {
switch (searchEntry.__typename) { switch (searchEntry.__typename) {
case 'Actor': case 'Actor':
@ -148,22 +142,29 @@ export default {
} }
return searchEntry; return searchEntry;
}); });
}, }
},
methods: { @Watch('model')
onModelChanged(val) {
switch (val.__typename) {
case 'Event':
this.$router.push({ name: 'Event', params: { uuid: val.uuid } });
break;
case 'Actor':
this.$router.push({ name: 'Account', params: { name: this.username_with_domain(val) } });
break;
}
}
username_with_domain(actor) { username_with_domain(actor) {
return actor.preferredUsername + (actor.domain === null ? '' : `@${actor.domain}`); return actor.preferredUsername + (actor.domain === null ? '' : `@${actor.domain}`);
}, }
enter() { enter() {
console.log('enter'); console.log('enter');
this.$apollo.queries.search.refetch(); this.$apollo.queries[ 'search' ].refetch();
} }
},
};
</script>
<style> }
nav.v-toolbar .v-input__slot {
margin-bottom: 0; </script>
}
</style>

View file

@ -11,14 +11,16 @@ import 'vuetify/dist/vuetify.min.css';
import App from '@/App.vue'; import App from '@/App.vue';
import router from '@/router'; import router from '@/router';
// import store from './store'; // import store from './store';
import translations from '@/i18n/translations.json';
import { createProvider } from './vue-apollo'; import { createProvider } from './vue-apollo';
const translations = require('@/i18n/translations.json');
Vue.config.productionTip = false; Vue.config.productionTip = false;
Vue.use(VueMarkdown); Vue.use(VueMarkdown);
Vue.use(Vuetify); Vue.use(Vuetify);
const language = window.navigator.userLanguage || window.navigator.language;
const language = (window.navigator as any).userLanguage || window.navigator.language;
moment.locale(language); moment.locale(language);
Vue.filter('formatDate', value => (value ? moment(String(value)).format('LLLL') : null)); Vue.filter('formatDate', value => (value ? moment(String(value)).format('LLLL') : null));
@ -33,8 +35,8 @@ Vue.config.language = language.replace('-', '_');
/* eslint-disable no-new */ /* eslint-disable no-new */
new Vue({ new Vue({
el: '#app',
router, router,
el: '#app',
template: '<App/>', template: '<App/>',
apolloProvider: createProvider(), apolloProvider: createProvider(),
components: { App }, components: { App },

View file

@ -1,23 +1,23 @@
import Vue from 'vue'; import Vue from 'vue';
import Router from 'vue-router'; import Router from 'vue-router';
import PageNotFound from '@/components/PageNotFound'; import PageNotFound from '@/components/PageNotFound.vue';
import Home from '@/components/Home'; import Home from '@/components/Home.vue';
import Event from '@/components/Event/Event'; import Event from '@/components/Event/Event.vue';
import EventList from '@/components/Event/EventList'; import EventList from '@/components/Event/EventList.vue';
import Location from '@/components/Location'; import Location from '@/components/Location.vue';
import CreateEvent from '@/components/Event/Create'; import CreateEvent from '@/components/Event/Create.vue';
import CategoryList from '@/components/Category/List'; import CategoryList from '@/components/Category/List.vue';
import CreateCategory from '@/components/Category/Create'; import CreateCategory from '@/components/Category/Create.vue';
import Register from '@/components/Account/Register'; import Register from '@/components/Account/Register.vue';
import Login from '@/components/Account/Login'; import Login from '@/components/Account/Login.vue';
import Validate from '@/components/Account/Validate'; import Validate from '@/components/Account/Validate.vue';
import ResendConfirmation from '@/components/Account/ResendConfirmation'; import ResendConfirmation from '@/components/Account/ResendConfirmation.vue';
import SendPasswordReset from '@/components/Account/SendPasswordReset'; import SendPasswordReset from '@/components/Account/SendPasswordReset.vue';
import PasswordReset from '@/components/Account/PasswordReset'; import PasswordReset from '@/components/Account/PasswordReset.vue';
import Account from '@/components/Account/Account'; import Account from '@/components/Account/Account.vue';
import CreateGroup from '@/components/Group/Create'; import CreateGroup from '@/components/Group/Create.vue';
import Group from '@/components/Group/Group'; import Group from '@/components/Group/Group.vue';
import GroupList from '@/components/Group/GroupList'; import GroupList from '@/components/Group/GroupList.vue';
import Identities from '../components/Account/Identities.vue'; import Identities from '../components/Account/Identities.vue';
Vue.use(Router); Vue.use(Router);

13
js/src/shims-tsx.d.ts vendored Normal file
View file

@ -0,0 +1,13 @@
import Vue, { VNode } from 'vue'
declare global {
namespace JSX {
// tslint:disable no-empty-interface
interface Element extends VNode {}
// tslint:disable no-empty-interface
interface ElementClass extends Vue {}
interface IntrinsicElements {
[elem: string]: any
}
}
}

4
js/src/shims-vue.d.ts vendored Normal file
View file

@ -0,0 +1,4 @@
declare module '*.vue' {
import Vue from 'vue'
export default Vue
}

View file

@ -33,7 +33,6 @@ const fragmentMatcher = new IntrospectionFragmentMatcher({
const cache = new InMemoryCache({ fragmentMatcher }); const cache = new InMemoryCache({ fragmentMatcher });
const authMiddleware = new ApolloLink((operation, forward) => { const authMiddleware = new ApolloLink((operation, forward) => {
// add the authorization to the headers // add the authorization to the headers
const token = localStorage.getItem(AUTH_TOKEN); const token = localStorage.getItem(AUTH_TOKEN);
@ -43,7 +42,9 @@ const authMiddleware = new ApolloLink((operation, forward) => {
}, },
}); });
return forward(operation); if (forward) forward(operation);
return null;
}); });
const uploadLink = createLink({ const uploadLink = createLink({
@ -60,6 +61,8 @@ const link = authMiddleware.concat(uploadLink);
// Config // Config
const defaultOptions = { const defaultOptions = {
cache,
link,
// You can use `https` for secure connection (recommended in production) // You can use `https` for secure connection (recommended in production)
httpEndpoint, httpEndpoint,
// You can use `wss` for secure connection (recommended in production) // You can use `wss` for secure connection (recommended in production)
@ -74,9 +77,8 @@ const defaultOptions = {
websocketsOnly: false, websocketsOnly: false,
// Is being rendered on the server? // Is being rendered on the server?
ssr: false, ssr: false,
cache,
link,
defaultHttpLink: false, defaultHttpLink: false,
connectToDevTools: true,
}; };
// Call this in the Vue app file // Call this in the Vue app file
@ -89,23 +91,18 @@ export function createProvider(options = {}) {
apolloClient.wsClient = wsClient; apolloClient.wsClient = wsClient;
// Create vue apollo provider // Create vue apollo provider
const apolloProvider = new VueApollo({ return new VueApollo({
defaultClient: apolloClient, defaultClient: apolloClient,
link, // defaultOptions: {
cache, // $query: {
connectToDevTools: true, // fetchPolicy: 'cache-and-network',
defaultOptions: { // },
$query: { // },
// fetchPolicy: 'cache-and-network',
},
},
errorHandler(error) { errorHandler(error) {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.log('%cError', 'background: red; color: white; padding: 2px 4px; border-radius: 3px; font-weight: bold;', error.message); console.log('%cError', 'background: red; color: white; padding: 2px 4px; border-radius: 3px; font-weight: bold;', error.message);
}, },
}); });
return apolloProvider;
} }
// Manually call this when user log in // Manually call this when user log in

View file

@ -1,8 +0,0 @@
module.exports = {
env: {
mocha: true,
},
rules: {
'import/no-extraneous-dependencies': 'off',
},
};

View file

@ -1,13 +0,0 @@
import { expect } from 'chai';
import { shallow } from '@vue/test-utils';
import HelloWorld from '@/components/HelloWorld.vue';
describe('HelloWorld.vue', () => {
it('renders props.msg when passed', () => {
const msg = 'new message';
const wrapper = shallow(HelloWorld, {
propsData: { msg },
});
expect(wrapper.text()).to.include(msg);
});
});

42
js/tsconfig.json Normal file
View file

@ -0,0 +1,42 @@
{
"compilerOptions": {
"target": "esnext",
"module": "esnext",
"strict": true,
"jsx": "preserve",
"importHelpers": true,
"moduleResolution": "node",
"experimentalDecorators": true,
"esModuleInterop": true,
"noImplicitAny": false,
"allowSyntheticDefaultImports": true,
"sourceMap": true,
"baseUrl": ".",
"types": [
"webpack-env",
"mocha",
"chai"
],
"paths": {
"@/*": [
"src/*"
]
},
"lib": [
"esnext",
"dom",
"dom.iterable",
"scripthost"
]
},
"include": [
"src/**/*.ts",
"src/**/*.tsx",
"src/**/*.vue",
"tests/**/*.ts",
"tests/**/*.tsx"
],
"exclude": [
"node_modules"
]
}

7
js/tslint.json Normal file
View file

@ -0,0 +1,7 @@
{
"extends": "tslint-config-airbnb",
"rules": {
"max-line-length": [ true, 140 ],
"import-name": false
}
}