From 7c4155447af96f31fe0d3ed5dccb5dd4dc3192ee Mon Sep 17 00:00:00 2001 From: "robinwilliam.hundt" <robinwilliam.hundt@stud.uni-goettingen.de> Date: Wed, 26 Sep 2018 15:00:14 +0200 Subject: [PATCH] Root mutations are now type safe --- frontend/package.json | 3 +- frontend/src/api.ts | 6 +- frontend/src/components/DataExport.vue | 4 +- .../subscriptions/SubscriptionForList.vue | 66 ++++----- frontend/src/store/actions.ts | 31 ++-- frontend/src/store/modules/subscriptions.ts | 1 - frontend/src/store/mutations.ts | 132 +++++++++--------- .../store/plugins/lastInteractionPlugin.ts | 6 +- frontend/src/store/store.ts | 8 +- frontend/yarn.lock | 8 +- 10 files changed, 124 insertions(+), 141 deletions(-) diff --git a/frontend/package.json b/frontend/package.json index dab5a75e..4447f55e 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -22,7 +22,8 @@ "vue-router": "^3.0.1", "vuetify": "^1.1.9", "vuex": "^3.0.1", - "vuex-persistedstate": "^2.5.4" + "vuex-persistedstate": "^2.5.4", + "vuex-typex": "https://github.com/robinhundt/vuex-typex.git" }, "devDependencies": { "@types/chai": "^4.1.0", diff --git a/frontend/src/api.ts b/frontend/src/api.ts index 1f2d2c17..efa7c9de 100644 --- a/frontend/src/api.ts +++ b/frontend/src/api.ts @@ -118,10 +118,10 @@ export async function fetchFeedback ({ofSubmission}: {ofSubmission: string}): Pr return (await ax.get(url)).data } -export async function fetchExamType ({examPk, fields = []}: -{examPk?: string, fields?: string[]}): Promise<Exam | Array<Exam>> { +export async function fetchExamTypes ({fields = []}: +{fields?: string[]}): Promise<Array<Exam>> { const url = addFieldsToUrl({ - url: `/api/examtype/${examPk !== undefined ? examPk + '/' : ''}`, + url: `/api/examtype/`, fields}) return (await ax.get(url)).data } diff --git a/frontend/src/components/DataExport.vue b/frontend/src/components/DataExport.vue index 269d2a27..3daa7cc7 100644 --- a/frontend/src/components/DataExport.vue +++ b/frontend/src/components/DataExport.vue @@ -44,7 +44,7 @@ import {mapGetters} from 'vuex' import ax from '@/api' import FileSelect from '@/components/util/FileSelect' -import { mut } from '@/store/mutations' +import { mutations as mut } from '@/store/mutations' import { parseCSVMapMixin } from '@/components/mixins/mixins' export default { @@ -86,7 +86,7 @@ export default { readMapFileAndCommit (callback) { this.fileReader.onload = event => { const studentMap = this.parseCSVMap(event.target.result) - this.$store.commit(mut.SET_STUDENT_MAP, studentMap) + mut.SET_STUDENT_MAP(studentMap) callback() } this.fileReader.readAsText(this.mapFile) diff --git a/frontend/src/components/subscriptions/SubscriptionForList.vue b/frontend/src/components/subscriptions/SubscriptionForList.vue index b8284c04..50619e3b 100644 --- a/frontend/src/components/subscriptions/SubscriptionForList.vue +++ b/frontend/src/components/subscriptions/SubscriptionForList.vue @@ -5,6 +5,7 @@ :to="subscriptionRoute" style="width: 100%" > + <!-- dynamically set css class depending on active --> <v-list-tile-content :class="{'inactive-subscription': !active}" class="ml-3"> @@ -17,48 +18,31 @@ </v-layout> </template> -<script> -export default { - name: 'subscription-for-list', - props: { - pk: { - types: String, - required: true - }, - feedbackStage: { - type: String, - required: true - }, - queryType: { - type: String, - required: true - }, - available: { - type: Number, - required: true - }, - assignments: { - type: Array, - required: true - }, - queryKey: { - type: String - } - }, - computed: { - name () { - return this.$store.getters.resolveSubscriptionKeyToName( - {queryKey: this.queryKey, queryType: this.queryType}) - }, - active () { - return !!this.available || this.assignments.length > 0 - }, - subscriptionRoute () { - if (this.active) { - return {name: 'subscription', params: {pk: this.pk}} - } - return this.$route.fullPath +<script lang="ts"> +import {Vue, Component, Prop} from 'vue-property-decorator' +import {Assignment} from '@/models' + +@Component +export default class SubscriptionForList extends Vue { + @Prop({type: String, required: true}) pk!: string + @Prop({type: String, required: true}) feedbackStage!: string + @Prop({type: String, required: true}) queryType!: string + @Prop({type: Number, required: true}) available!: number + @Prop({type: Array, required: true}) assignments!: Assignment[] + @Prop({type: String, default: ''}) queryKey!: string + + get name () { + return this.$store.getters.resolveSubscriptionKeyToName( + {queryKey: this.queryKey, queryType: this.queryType}) + } + get active () { + return !!this.available || this.assignments.length > 0 + } + get subscriptionRoute () { + if (this.active) { + return {name: 'subscription', params: {pk: this.pk}} } + return this.$route.fullPath } } </script> diff --git a/frontend/src/store/actions.ts b/frontend/src/store/actions.ts index 24babbe0..2ad5920a 100644 --- a/frontend/src/store/actions.ts +++ b/frontend/src/store/actions.ts @@ -1,4 +1,4 @@ -import { mut } from './mutations' +import { mutations as mut } from './mutations' import { authMut } from '@/store/modules/authentication' import { subNotesMut } from '@/store/modules/submission-notes' import * as api from '@/api' @@ -10,8 +10,8 @@ import {RootState} from '@/store/store' const actions: ActionTree<RootState, RootState> = { async getExamTypes ({commit, dispatch}) { try { - const examTypes = await api.fetchExamType({}) - commit(mut.SET_EXAM_TYPES, examTypes) + const examTypes = await api.fetchExamTypes({}) + mut.SET_EXAM_TYPES(examTypes) } catch (err) { handleError(err, dispatch, 'Unable to fetch exam types') } @@ -20,7 +20,7 @@ const actions: ActionTree<RootState, RootState> = { try { const submissionTypes = await api.fetchSubmissionTypes(fields) submissionTypes.forEach(type => { - commit(mut.UPDATE_SUBMISSION_TYPE, type) + mut.UPDATE_SUBMISSION_TYPE(type) }) } catch (err) { handleError(err, dispatch, 'Unable to get submission types') @@ -32,7 +32,7 @@ const actions: ActionTree<RootState, RootState> = { try { if (opt.studentPks.length === 0) { const students = await api.fetchAllStudents() - commit(mut.SET_STUDENTS, students) + mut.SET_STUDENTS(students) return students } else { const students = await Promise.all( @@ -41,7 +41,7 @@ const actions: ActionTree<RootState, RootState> = { fields: opt.fields })) ) - students.forEach(student => commit(mut.SET_STUDENT, student)) + students.forEach(student => mut.SET_STUDENT(student)) return students } } catch (err) { @@ -52,24 +52,15 @@ const actions: ActionTree<RootState, RootState> = { async getTutors ({commit, dispatch}) { try { const tutors = await api.fetchAllTutors() - commit(mut.SET_TUTORS, tutors) + mut.SET_TUTORS(tutors) } catch (err) { handleError(err, dispatch, 'Unable to fetch tutor data') } }, - async getAllFeedback ({commit, dispatch}, fields: string[] = []) { - try { - const feedback = await api.fetchAllFeedback(fields) - commit(mut.SET_ALL_FEEDBACK, feedback) - commit(mut.MAP_FEEDBACK_OF_SUBMISSION_TYPE) - } catch (err) { - handleError(err, dispatch, 'Unable to fetch feedback history') - } - }, async getFeedback ({commit, dispatch}, {ofSubmission}) { try { const feedback = await api.fetchFeedback({ofSubmission}) - commit(mut.SET_FEEDBACK, feedback) + mut.SET_FEEDBACK(feedback) return feedback } catch (err) { handleError(err, dispatch, `Unable to fetch feedback ${ofSubmission}`) @@ -78,7 +69,7 @@ const actions: ActionTree<RootState, RootState> = { async getSubmissionFeedbackTest ({commit, dispatch}, {pk}) { try { const submission = await api.fetchSubmissionFeedbackTests({pk}) - commit(mut.SET_SUBMISSION, submission) + mut.SET_SUBMISSION(submission) } catch (err) { handleError(err, dispatch, 'Unable to fetch submission') } @@ -86,7 +77,7 @@ const actions: ActionTree<RootState, RootState> = { async getStatistics ({commit, dispatch}, opt) { try { const statistics = await api.fetchStatistics(opt) - commit(mut.SET_STATISTICS, statistics) + mut.SET_STATISTICS(statistics) } catch (err) { handleError(err, dispatch, 'Unable to fetch statistics') } @@ -111,7 +102,7 @@ const actions: ActionTree<RootState, RootState> = { // TODO this should belong in auth module api.changeActiveForUser((state as any).authentication.user.pk, false) } - commit('RESET_STATE') + mut.RESET_STATE() commit('submissionNotes/' + subNotesMut.RESET_STATE) commit(authMut.SET_MESSAGE, message) router.push({name: 'login'}) diff --git a/frontend/src/store/modules/subscriptions.ts b/frontend/src/store/modules/subscriptions.ts index 4888efce..63dc6aae 100644 --- a/frontend/src/store/modules/subscriptions.ts +++ b/frontend/src/store/modules/subscriptions.ts @@ -34,7 +34,6 @@ function initialState (): SubscriptionsState { const MAX_NUMBER_OF_ASSIGNMENTS = 2 -// noinspection JSCommentMatchesSignature const subscriptionsModule: Module<SubscriptionsState, RootState> = { state: initialState(), getters: { diff --git a/frontend/src/store/mutations.ts b/frontend/src/store/mutations.ts index 252ead24..b4e343be 100644 --- a/frontend/src/store/mutations.ts +++ b/frontend/src/store/mutations.ts @@ -1,93 +1,95 @@ import Vue from 'vue' +import {getStoreBuilder} from 'vuex-typex' import {initialState, RootState} from '@/store/store' -import {MutationTree} from 'vuex' -import {Exam, Statistics, StudentInfoForListView, SubmissionNoType, SubmissionType, Tutor} from '@/models' +import {Exam, Feedback, Statistics, StudentInfoForListView, SubmissionNoType, SubmissionType, Tutor} from '@/models' -export const mut = Object.freeze({ - SET_LAST_INTERACTION: 'SET_LAST_INTERACTION', - SET_EXAM_TYPES: 'SET_EXAM_TYPES', - SET_STUDENTS: 'SET_STUDENTS', - SET_STUDENT: 'SET_STUDENT', - SET_STUDENT_MAP: 'SET_STUDENT_MAP', - SET_TUTORS: 'SET_TUTORS', - SET_SUBMISSION: 'SET_SUBMISSION', - SET_ALL_FEEDBACK: 'SET_ALL_FEEDBACK', - SET_STATISTICS: 'SET_STATISTICS', - SET_FEEDBACK: 'SET_FEEDBACK', - MAP_FEEDBACK_OF_SUBMISSION_TYPE: 'MAP_FEEDBACK_OF_SUBMISSION_TYPE', - UPDATE_SUBMISSION_TYPE: 'UPDATE_SUBMISSION_TYPE', - UPDATE_SUBMISSION: 'UPDATE_SUBMISSION', - RESET_STATE: 'RESET_STATE' -}) +export const mb = getStoreBuilder<RootState>() -const mutations: MutationTree<RootState> = { - [mut.SET_EXAM_TYPES] (state, examTypes: Array<Exam>) { +function SET_EXAM_TYPES (state: RootState, examTypes: Array<Exam>) { state.examTypes = examTypes.reduce((acc: {[pk: string]: Exam}, curr) => { - acc[curr.pk] = curr - return acc + acc[curr.pk] = curr + return acc }, {}) - }, - [mut.SET_STUDENTS] (state, students: Array<StudentInfoForListView>) { +} +function SET_STUDENTS (state: RootState, students: Array<StudentInfoForListView>) { state.students = students.reduce((acc: {[pk: string]: StudentInfoForListView}, curr) => { - acc[curr.pk] = mapStudent(curr, state.studentMap) - return acc + acc[curr.pk] = mapStudent(curr, state.studentMap) + return acc }, {}) - }, - [mut.SET_STUDENT] (state, student: StudentInfoForListView) { +} +function SET_STUDENT (state: RootState, student: StudentInfoForListView) { Vue.set(state.students, student.pk, mapStudent({ - ...state.students[student.pk], - ...student + ...state.students[student.pk], + ...student }, state.studentMap)) - }, - [mut.SET_STUDENT_MAP] (state, map) { +} +// TODO proper types for student map +function SET_STUDENT_MAP (state: RootState, map: object) { state.studentMap = map - }, - [mut.SET_TUTORS] (state, tutors: Array<Tutor>) { +} +function SET_TUTORS (state: RootState, tutors: Array<Tutor>) { state.tutors = tutors - }, - [mut.SET_SUBMISSION] (state, submission: SubmissionNoType) { +} +function SET_SUBMISSION (state: RootState, submission: SubmissionNoType) { Vue.set(state.submissions, submission.pk, submission) - }, - [mut.SET_STATISTICS] (state, statistics: Statistics) { +} +function SET_STATISTICS (state: RootState, statistics: Statistics) { state.statistics = { - ...state.statistics, - ...statistics + ...state.statistics, + ...statistics } - }, - [mut.SET_FEEDBACK] (state, feedback) { - Vue.set(state.feedback, feedback.pk, { - ...state.feedback[feedback.pk], - ...feedback, - ofSubmissionType: state.submissionTypes[feedback['ofSubmissionType']] +} +function SET_FEEDBACK (state: RootState, feedback: Feedback) { + if (!feedback.ofSubmissionType) { + throw new Error("Can only SET_FEEDBACK when ofSubmissionType is set") + } + // weird cast is necessary because of the type of Vue.set + Vue.set(state.feedback, <string><any>feedback.pk, { + ...state.feedback[feedback.pk], + ...feedback, + // TODO fix this fucking memory leak + ofSubmissionType: state.submissionTypes[feedback.ofSubmissionType] }) - }, - [mut.UPDATE_SUBMISSION_TYPE] (state, submissionType: SubmissionType) { +} +function UPDATE_SUBMISSION_TYPE (state: RootState, submissionType: SubmissionType) { const updatedSubmissionType = { - ...state.submissionTypes[submissionType.pk], - ...submissionType + ...state.submissionTypes[submissionType.pk], + ...submissionType } Vue.set(state.submissionTypes, submissionType.pk, updatedSubmissionType) - }, - [mut.SET_LAST_INTERACTION] (state) { +} +// this func is being exported to use it's name in the latInteractionPlugin +export function SET_LAST_INTERACTION (state: RootState) { state.lastAppInteraction = Date.now() - }, - [mut.RESET_STATE] (state) { +} +function RESET_STATE (state: RootState) { Object.assign(state, initialState()) - } } function mapStudent (student: StudentInfoForListView, map: any) { - if (Object.keys(map).length > 0) { - if (!student.matrikelNo) { - throw Error('Student objects need matrikelNo key in order to apply mapping') - } - return { - ...student, - ...map[student.matrikelNo] + if (Object.keys(map).length > 0) { + if (!student.matrikelNo) { + throw Error('Student objects need matrikelNo key in order to apply mapping') + } + return { + ...student, + ...map[student.matrikelNo] + } } - } - return student + return student } -export default mutations +export const mutations = { + SET_LAST_INTERACTION: mb.commit(SET_LAST_INTERACTION), + SET_EXAM_TYPES: mb.commit(SET_EXAM_TYPES), + SET_STUDENTS: mb.commit(SET_STUDENTS), + SET_STUDENT: mb.commit(SET_STUDENT), + SET_STUDENT_MAP: mb.commit(SET_STUDENT_MAP), + SET_TUTORS: mb.commit(SET_TUTORS), + SET_SUBMISSION: mb.commit(SET_SUBMISSION), + SET_STATISTICS: mb.commit(SET_STATISTICS), + SET_FEEDBACK: mb.commit(SET_FEEDBACK), + UPDATE_SUBMISSION_TYPE: mb.commit(UPDATE_SUBMISSION_TYPE), + RESET_STATE: mb.commit(RESET_STATE) +} diff --git a/frontend/src/store/plugins/lastInteractionPlugin.ts b/frontend/src/store/plugins/lastInteractionPlugin.ts index 7c64f2ba..3b674ba8 100644 --- a/frontend/src/store/plugins/lastInteractionPlugin.ts +++ b/frontend/src/store/plugins/lastInteractionPlugin.ts @@ -1,10 +1,10 @@ -import {mut} from '@/store/mutations' +import {mutations as mut, SET_LAST_INTERACTION} from '@/store/mutations' import {MutationPayload, Store} from 'vuex' export function lastInteraction (store: Store<void>) { store.subscribe((mutation: MutationPayload) => { - if (mutation.type !== mut.SET_LAST_INTERACTION) { - store.commit(mut.SET_LAST_INTERACTION) + if (mutation.type !== SET_LAST_INTERACTION.name) { + mut.SET_LAST_INTERACTION() } }) } diff --git a/frontend/src/store/store.ts b/frontend/src/store/store.ts index 849519fc..1398aa63 100644 --- a/frontend/src/store/store.ts +++ b/frontend/src/store/store.ts @@ -1,6 +1,7 @@ import Vuex from 'vuex' import Vue from 'vue' import createPersistedState from 'vuex-persistedstate' +import {getStoreBuilder} from 'vuex-typex' import studentPage from './modules/student-page' import submissionNotes from './modules/submission-notes' @@ -10,9 +11,9 @@ import subscriptions from './modules/subscriptions' import feedbackTable from './modules/feedback_list/feedback-table' import feedbackSearchOptions from './modules/feedback_list/feedback-search-options' +import './mutations' import actions from './actions' import getters from './getters' -import mutations from '@/store/mutations' import {lastInteraction} from '@/store/plugins/lastInteractionPlugin' import { Exam, Feedback, @@ -27,7 +28,7 @@ Vue.use(Vuex) export interface RootState { lastAppInteraction: number examTypes: {[pk: string]: Exam} - feedback: {[pk: string]: Feedback} + feedback: {[pk: number]: Feedback} submissionTypes: {[pk: string]: SubmissionType} submissions: {[pk: string]: SubmissionNoType} students: {[pk: string]: StudentInfoForListView} @@ -57,7 +58,7 @@ export function initialState (): RootState { export const persistedStateKey = 'grady' -const store = new Vuex.Store({ +const store = getStoreBuilder<RootState>().vuexStore({ strict: process.env.NODE_ENV === 'development', modules: { authentication, @@ -82,7 +83,6 @@ const store = new Vuex.Store({ lastInteraction], actions, getters, - mutations, state: initialState() }) diff --git a/frontend/yarn.lock b/frontend/yarn.lock index 6d751e73..b91413a1 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -6929,7 +6929,13 @@ vuex-persistedstate@^2.5.4: deepmerge "^2.1.0" shvl "^1.3.0" -vuex@^3.0.1: +"vuex-typex@https://github.com/robinhundt/vuex-typex.git": + version "3.0.1" + resolved "https://github.com/robinhundt/vuex-typex.git#72c5eb30ac7fe7c4bf657ebe4e40e115f309fa39" + dependencies: + vuex "^3.0.0" + +vuex@^3.0.0, vuex@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/vuex/-/vuex-3.0.1.tgz#e761352ebe0af537d4bb755a9b9dc4be3df7efd2" -- GitLab