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",
"version": "0.1.0",
"private": true,
"engines": {
"node": ">=10.0.0"
},
"scripts": {
"dev": "vue-cli-service serve",
"build": "vue-cli-service build --modern",
"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:unit": "vue-cli-service test:unit",
"analyze-bundle": "npm run build -- --report-json && webpack-bundle-analyzer ../priv/static/report.json"
"test:unit": "vue-cli-service test:unit"
},
"dependencies": {
"apollo-absinthe-upload-link": "^1.4.0",
@ -26,27 +23,35 @@
"register-service-worker": "^1.4.1",
"vue": "^2.5.17",
"vue-apollo": "^3.0.0-beta.26",
"vue-class-component": "^6.3.2",
"vue-gettext": "^2.1.1",
"vue-gravatar": "^1.3.0",
"vue-markdown": "^2.2.4",
"vue-property-decorator": "^7.2.0",
"vue-router": "^3.0.2",
"vuetify": "^1.3.9",
"vuetify-google-autocomplete": "^2.0.0-beta.5",
"vuex": "^3.0.1"
},
"devDependencies": {
"@types/chai": "^4.1.0",
"@types/mocha": "^5.2.4",
"@vue/cli-plugin-babel": "^3.1.1",
"@vue/cli-plugin-e2e-nightwatch": "^3.1.1",
"@vue/cli-plugin-eslint": "^3.1.5",
"@vue/cli-plugin-pwa": "^3.1.2",
"@vue/cli-plugin-typescript": "^3.2.0",
"@vue/cli-plugin-unit-mocha": "^3.1.1",
"@vue/cli-service": "^3.1.4",
"@vue/eslint-config-airbnb": "^3.0.5",
"@vue/eslint-config-typescript": "^3.1.0",
"@vue/test-utils": "^1.0.0-beta.26",
"chai": "^4.2.0",
"dotenv-webpack": "^1.5.7",
"node-sass": "^4.10.0",
"sass-loader": "^7.1.0",
"tslint-config-airbnb": "^5.11.1",
"typescript": "^3.0.0",
"vue-cli-plugin-apollo": "^0.17.4",
"vue-template-compiler": "^2.5.17",
"webpack-bundle-analyzer": "^3.0.3"
@ -55,5 +60,8 @@
"> 1%",
"last 2 versions",
"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>
<img v-if="!actor.avatar"
class="img-circle elevation-7 mb-1"
src="https://picsum.photos/125/125/"
>
<img v-else
class="img-circle elevation-7 mb-1"
:src="actor.avatar"
>
</v-list-tile-avatar>
<img v-if="!actor.avatar"
class="img-circle elevation-7 mb-1"
src="https://picsum.photos/125/125/"
>
<img v-else
class="img-circle elevation-7 mb-1"
:src="actor.avatar"
>
</v-list-tile-avatar>
<v-list-tile-content @click="$router.push({name: 'Account', params: { name: actor.username }})">
<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>
<img
class="img-circle elevation-7 mb-1"
src="https://picsum.photos/125/125/"
>
</v-list-tile-avatar>
<img
class="img-circle elevation-7 mb-1"
src="https://picsum.photos/125/125/"
>
</v-list-tile-avatar>
<v-list-tile-content>
<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-action>
<v-icon>group</v-icon>
</v-list-tile-action>
<v-icon>group</v-icon>
</v-list-tile-action>
<v-list-tile-content>
<v-list-tile-title>Identities</v-list-tile-title>
</v-list-tile-content>
@ -100,7 +100,7 @@
transition="scale-transition"
v-if="user"
>
<v-btn
<v-btn
slot="activator"
v-model="fab"
color="blue darken-2"
@ -134,7 +134,8 @@
class="white--text"
v-translate="{
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>
</v-footer>
<v-snackbar
@ -148,75 +149,78 @@
</v-app>
</template>
<script>
import gql from 'graphql-tag';
import NavBar from '@/components/NavBar';
import { AUTH_USER_ACTOR, AUTH_USER_ID } from '@/constants';
<script lang="ts">
import NavBar from '@/components/NavBar.vue';
import { Component, Vue } from 'vue-property-decorator';
import { AUTH_USER_ACTOR, AUTH_USER_ID } from '@/constants';
export default {
name: 'app',
components: {
NavBar,
},
data() {
return {
drawer: false,
fab: false,
user: localStorage.getItem(AUTH_USER_ID),
items: [
{
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: '',
@Component({
components: {
NavBar
}
})
export default class App extends Vue {
drawer = false
fab = false
user = localStorage.getItem(AUTH_USER_ID)
items = [
{
icon: 'poll', text: 'Events', route: 'EventList', role: null
},
show_new_event_button: false,
actor: localStorage.getItem(AUTH_USER_ACTOR),
};
},
methods: {
showMenuItem(elem) {
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;
},
},
computed: {
displayed_name() {
return this.actor.display_name === null ? this.actor.username : this.actor.display_name;
},
},
};
{
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)
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>
<style>
.router-enter-active, .router-leave-active {
transition-property: opacity;
transition-duration: .25s;
}
.router-enter-active, .router-leave-active {
transition-property: opacity;
transition-duration: .25s;
}
.router-enter-active {
transition-delay: .25s;
}
.router-enter-active {
transition-delay: .25s;
}
.router-enter, .router-leave-active {
opacity: 0
}
.router-enter, .router-leave-active {
opacity: 0
}
</style>

View file

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

View file

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

View file

@ -1,23 +1,23 @@
import Vue from 'vue';
import Router from 'vue-router';
import PageNotFound from '@/components/PageNotFound';
import Home from '@/components/Home';
import Event from '@/components/Event/Event';
import EventList from '@/components/Event/EventList';
import Location from '@/components/Location';
import CreateEvent from '@/components/Event/Create';
import CategoryList from '@/components/Category/List';
import CreateCategory from '@/components/Category/Create';
import Register from '@/components/Account/Register';
import Login from '@/components/Account/Login';
import Validate from '@/components/Account/Validate';
import ResendConfirmation from '@/components/Account/ResendConfirmation';
import SendPasswordReset from '@/components/Account/SendPasswordReset';
import PasswordReset from '@/components/Account/PasswordReset';
import Account from '@/components/Account/Account';
import CreateGroup from '@/components/Group/Create';
import Group from '@/components/Group/Group';
import GroupList from '@/components/Group/GroupList';
import PageNotFound from '@/components/PageNotFound.vue';
import Home from '@/components/Home.vue';
import Event from '@/components/Event/Event.vue';
import EventList from '@/components/Event/EventList.vue';
import Location from '@/components/Location.vue';
import CreateEvent from '@/components/Event/Create.vue';
import CategoryList from '@/components/Category/List.vue';
import CreateCategory from '@/components/Category/Create.vue';
import Register from '@/components/Account/Register.vue';
import Login from '@/components/Account/Login.vue';
import Validate from '@/components/Account/Validate.vue';
import ResendConfirmation from '@/components/Account/ResendConfirmation.vue';
import SendPasswordReset from '@/components/Account/SendPasswordReset.vue';
import PasswordReset from '@/components/Account/PasswordReset.vue';
import Account from '@/components/Account/Account.vue';
import CreateGroup from '@/components/Group/Create.vue';
import Group from '@/components/Group/Group.vue';
import GroupList from '@/components/Group/GroupList.vue';
import Identities from '../components/Account/Identities.vue';
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 authMiddleware = new ApolloLink((operation, forward) => {
// add the authorization to the headers
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({
@ -60,6 +61,8 @@ const link = authMiddleware.concat(uploadLink);
// Config
const defaultOptions = {
cache,
link,
// You can use `https` for secure connection (recommended in production)
httpEndpoint,
// You can use `wss` for secure connection (recommended in production)
@ -74,9 +77,8 @@ const defaultOptions = {
websocketsOnly: false,
// Is being rendered on the server?
ssr: false,
cache,
link,
defaultHttpLink: false,
connectToDevTools: true,
};
// Call this in the Vue app file
@ -89,23 +91,18 @@ export function createProvider(options = {}) {
apolloClient.wsClient = wsClient;
// Create vue apollo provider
const apolloProvider = new VueApollo({
return new VueApollo({
defaultClient: apolloClient,
link,
cache,
connectToDevTools: true,
defaultOptions: {
$query: {
// fetchPolicy: 'cache-and-network',
},
},
// defaultOptions: {
// $query: {
// fetchPolicy: 'cache-and-network',
// },
// },
errorHandler(error) {
// eslint-disable-next-line no-console
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

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
}
}