From cd9b3e35af5662cb20205b367052d15489d6941a Mon Sep 17 00:00:00 2001
From: "robinwilliam.hundt" <robinwilliam.hundt@stud.uni-goettingen.de>
Date: Wed, 26 Sep 2018 16:20:06 +0200
Subject: [PATCH] type safe root actions and mutations

---
 frontend/src/api.ts                           |   4 +-
 frontend/src/components/DataExport.vue        |   6 +-
 .../submission_notes/SubmissionCorrection.vue |   2 -
 frontend/src/store/actions.ts                 | 208 +++++++++---------
 frontend/src/store/getters.ts                 |  36 +--
 .../modules/feedback_list/feedback-table.ts   |   5 +-
 .../src/store/modules/submission-notes.ts     |   2 +-
 frontend/src/store/store.ts                   |   6 +-
 8 files changed, 131 insertions(+), 138 deletions(-)

diff --git a/frontend/src/api.ts b/frontend/src/api.ts
index efa7c9de..3d026051 100644
--- a/frontend/src/api.ts
+++ b/frontend/src/api.ts
@@ -192,12 +192,12 @@ export async function deleteAssignment ({assignment}: {assignment: Assignment}):
   return ax.delete(url)
 }
 
-export async function deleteComment (comment = {pk: undefined}): Promise<AxiosResponse<void>> {
+export async function deleteComment (comment: FeedbackComment): Promise<AxiosResponse<void>> {
   const url = `/api/feedback-comment/${comment.pk}/`
   return ax.delete(url)
 }
 
-export async function patchComment (comment = {pk: undefined}): Promise<FeedbackComment> {
+export async function patchComment (comment: FeedbackComment): Promise<FeedbackComment> {
   const url = `/api/feedback-comment/${comment.pk}/`
   return (await ax.patch(url, comment)).data
 }
diff --git a/frontend/src/components/DataExport.vue b/frontend/src/components/DataExport.vue
index 3daa7cc7..dc13bdf1 100644
--- a/frontend/src/components/DataExport.vue
+++ b/frontend/src/components/DataExport.vue
@@ -41,7 +41,7 @@
 </template>
 
 <script>
-import {mapGetters} from 'vuex'
+import {getters} from '@/store/getters'
 import ax from '@/api'
 import FileSelect from '@/components/util/FileSelect'
 import { mutations as mut } from '@/store/mutations'
@@ -59,9 +59,7 @@ export default {
     }
   },
   computed: {
-    ...mapGetters([
-      'corrected'
-    ]),
+    corrected () { return getters.corrected },
     exportColor () {
       return this.corrected ? 'green darken-1' : 'red lighten-1'
     },
diff --git a/frontend/src/components/submission_notes/SubmissionCorrection.vue b/frontend/src/components/submission_notes/SubmissionCorrection.vue
index d7b5c17f..a23d4111 100644
--- a/frontend/src/components/submission_notes/SubmissionCorrection.vue
+++ b/frontend/src/components/submission_notes/SubmissionCorrection.vue
@@ -107,8 +107,6 @@ export default {
       'isStudent',
       'isTutor',
       'isReviewer',
-      'getSubmission',
-      'getSubmissionType',
       'workInProgress'
     ]),
     submission () {
diff --git a/frontend/src/store/actions.ts b/frontend/src/store/actions.ts
index 2ad5920a..e1fecc60 100644
--- a/frontend/src/store/actions.ts
+++ b/frontend/src/store/actions.ts
@@ -1,113 +1,103 @@
-import { mutations as mut } from './mutations'
-import { authMut } from '@/store/modules/authentication'
-import { subNotesMut } from '@/store/modules/submission-notes'
-import * as api from '@/api'
-import router from '@/router/index'
-import { handleError } from '@/util/helpers'
-import {ActionTree} from 'vuex'
-import {RootState} from '@/store/store'
+import { ActionContext } from "vuex";
+import { BareActionContext, getStoreBuilder } from "vuex-typex";
 
-const actions: ActionTree<RootState, RootState> = {
-  async getExamTypes ({commit, dispatch}) {
-    try {
-      const examTypes = await api.fetchExamTypes({})
-        mut.SET_EXAM_TYPES(examTypes)
-    } catch (err) {
-      handleError(err, dispatch, 'Unable to fetch exam types')
-    }
-  },
-  async updateSubmissionTypes ({commit, dispatch}, fields: Array<string> = []) {
-    try {
-      const submissionTypes = await api.fetchSubmissionTypes(fields)
-      submissionTypes.forEach(type => {
-          mut.UPDATE_SUBMISSION_TYPE(type)
-      })
-    } catch (err) {
-      handleError(err, dispatch, 'Unable to get submission types')
-    }
-  },
-  async getStudents ({commit, dispatch},
-    opt: {studentPks: Array<string>, fields: Array<string>} =
-    {studentPks: [], fields: []}) {
-    try {
-      if (opt.studentPks.length === 0) {
-        const students = await api.fetchAllStudents()
-          mut.SET_STUDENTS(students)
-        return students
-      } else {
-        const students = await Promise.all(
-          opt.studentPks.map((pk: string) => api.fetchStudent({
-            pk,
-            fields: opt.fields
-          }))
-        )
-        students.forEach(student => mut.SET_STUDENT(student))
-        return students
-      }
-    } catch (err) {
-      console.log(err)
-      handleError(err, dispatch, 'Unable to fetch student data')
-    }
-  },
-  async getTutors ({commit, dispatch}) {
-    try {
-      const tutors = await api.fetchAllTutors()
-        mut.SET_TUTORS(tutors)
-    } catch (err) {
-      handleError(err, dispatch, 'Unable to fetch tutor data')
-    }
-  },
-  async getFeedback ({commit, dispatch}, {ofSubmission}) {
-    try {
-      const feedback = await api.fetchFeedback({ofSubmission})
-      mut.SET_FEEDBACK(feedback)
-      return feedback
-    } catch (err) {
-      handleError(err, dispatch, `Unable to fetch feedback ${ofSubmission}`)
-    }
-  },
-  async getSubmissionFeedbackTest ({commit, dispatch}, {pk}) {
-    try {
-      const submission = await api.fetchSubmissionFeedbackTests({pk})
-      mut.SET_SUBMISSION(submission)
-    } catch (err) {
-      handleError(err, dispatch, 'Unable to fetch submission')
-    }
-  },
-  async getStatistics ({commit, dispatch}, opt) {
-    try {
-      const statistics = await api.fetchStatistics(opt)
-      mut.SET_STATISTICS(statistics)
-    } catch (err) {
-      handleError(err, dispatch, 'Unable to fetch statistics')
-    }
-  },
-  async deleteComment ({commit, dispatch}, comment) {
-    try {
-      await api.deleteComment(comment)
-    } catch (err) {
-      handleError(err, dispatch, `Unable to delete comment: ${comment.text}`)
-    }
-  },
-  async patchComment ({commit, dispatch}, comment) {
-    try {
-      return await api.patchComment(comment)
-    } catch (err) {
-      handleError(err, dispatch, `Unable to patch comment: ${comment.text}`)
-    }
-  },
-  logout ({ commit, getters, state }, message = '') {
-    if (getters.isStudent) {
-      // change active to false when user logs out
-      // TODO this should belong in auth module
-      api.changeActiveForUser((state as any).authentication.user.pk, false)
-    }
-    mut.RESET_STATE()
-    commit('submissionNotes/' + subNotesMut.RESET_STATE)
-    commit(authMut.SET_MESSAGE, message)
-    router.push({name: 'login'})
-    router.go(0)
+import { mutations as mut } from "./mutations";
+import { authMut } from "@/store/modules/authentication";
+import { subNotesMut } from "@/store/modules/submission-notes";
+import * as api from "@/api";
+import router from "@/router/index";
+import { RootState } from "@/store/store";
+
+async function getExamTypes(context: BareActionContext<RootState, RootState>) {
+  const examTypes = await api.fetchExamTypes({});
+  mut.SET_EXAM_TYPES(examTypes);
+}
+async function updateSubmissionTypes(
+  context: BareActionContext<RootState, RootState>,
+  fields: Array<string> = []
+) {
+  const submissionTypes = await api.fetchSubmissionTypes(fields);
+  submissionTypes.forEach(type => {
+    mut.UPDATE_SUBMISSION_TYPE(type);
+  });
+}
+async function getStudents(
+  context: BareActionContext<RootState, RootState>,
+  opt: { studentPks: Array<string>; fields: Array<string> } = {
+    studentPks: [],
+    fields: []
   }
+) {
+  if (opt.studentPks.length === 0) {
+    const students = await api.fetchAllStudents();
+    mut.SET_STUDENTS(students);
+    return students;
+  } else {
+    const students = await Promise.all(
+      opt.studentPks.map((pk: string) =>
+        api.fetchStudent({
+          pk,
+          fields: opt.fields
+        })
+      )
+    );
+    students.forEach(student => mut.SET_STUDENT(student));
+    return students;
+  }
+}
+async function getTutors() {
+  const tutors = await api.fetchAllTutors();
+  mut.SET_TUTORS(tutors);
+}
+async function getFeedback(
+  context: BareActionContext<RootState, RootState>,
+  fetchFeedbackArg: { ofSubmission: string }
+) {
+  const feedback = await api.fetchFeedback(fetchFeedbackArg);
+  mut.SET_FEEDBACK(feedback);
+  return feedback;
 }
+async function getSubmissionFeedbackTest(
+  context: BareActionContext<RootState, RootState>,
+  submissionPkObj: { pk: string }
+) {
+  const submission = await api.fetchSubmissionFeedbackTests(submissionPkObj);
+  mut.SET_SUBMISSION(submission);
+}
+async function getStatistics() {
+  const statistics = await api.fetchStatistics();
+  mut.SET_STATISTICS(statistics);
+}
+function logout(
+  context: BareActionContext<RootState, RootState>,
+  message = ""
+) {
+  // logout actually receives a normal ActionContext, but for some reason vuex-typex mandates a BareActionContext
+  // TODO there has to be a better way
+  const { commit, getters, state } = <ActionContext<RootState, RootState>>(
+    context
+  );
+  if (getters.isStudent) {
+    // change active to false when user logs out
+    // TODO this should belong in auth module
+    api.changeActiveForUser((state as any).authentication.user.pk, false);
+  }
+  mut.RESET_STATE();
+  commit("submissionNotes/" + subNotesMut.RESET_STATE);
+  commit(authMut.SET_MESSAGE, message);
+  router.push({ name: "login" });
+  router.go(0);
+}
+
+const mb = getStoreBuilder<RootState>();
 
-export default actions
+export const actions = {
+  updateSubmissionTypes: mb.dispatch(getExamTypes),
+  getExamTypes: mb.dispatch(updateSubmissionTypes),
+  getStudents: mb.dispatch(getStudents),
+  getTutors: mb.dispatch(getTutors),
+  getFeedback: mb.dispatch(getFeedback),
+  getSubmissionFeedbackTest: mb.dispatch(getSubmissionFeedbackTest),
+  getStatistics: mb.dispatch(getStatistics),
+  logout: mb.dispatch(logout)
+};
diff --git a/frontend/src/store/getters.ts b/frontend/src/store/getters.ts
index 44e8a56d..c36c0d45 100644
--- a/frontend/src/store/getters.ts
+++ b/frontend/src/store/getters.ts
@@ -1,18 +1,26 @@
 import {RootState} from '@/store/store'
-import {GetterTree} from 'vuex'
+import {getStoreBuilder} from 'vuex-typex'
 
-const getters: GetterTree<RootState, RootState> = {
-  corrected (state) {
-    return state.statistics.submissionTypeProgress.every(progress => {
-      return progress.feedbackFinal === progress.submissionCount
-    })
-  },
-  getSubmission: state => (pk: string) => {
-    return state.submissions[pk]
-  },
-  getSubmissionType: state => (pk: string) => {
-    return state.submissionTypes[pk]
+const mb = getStoreBuilder<RootState>()
+
+const correctedGetter = mb.read(function corrected (state) {
+  return state.statistics.submissionTypeProgress.every(progress => {
+    return progress.feedbackFinal === progress.submissionCount
+  })
+})
+const submissionGetter = mb.read(function submission (state) {
+  return (pk: string) => {
+      return state.submissions[pk]
   }
-}
+})
+const submissionTypeGetter = mb.read(function submissionType (state) {
+  return (pk: string) => {
+      return state.submissionTypes[pk]
+  }
+})
 
-export default getters
+export const getters = {
+  get corrected () { return correctedGetter() },
+  get submission () { return submissionGetter() },
+  get submissionType () { return submissionTypeGetter() }
+}
diff --git a/frontend/src/store/modules/feedback_list/feedback-table.ts b/frontend/src/store/modules/feedback_list/feedback-table.ts
index 02337c19..010cc10e 100644
--- a/frontend/src/store/modules/feedback_list/feedback-table.ts
+++ b/frontend/src/store/modules/feedback_list/feedback-table.ts
@@ -3,6 +3,7 @@ import {objectifyArray} from '@/util/helpers'
 import {Assignment, Feedback, Subscription} from '@/models'
 import {Module} from 'vuex'
 import {RootState} from '@/store/store'
+import {getters} from '@/store/getters'
 
 export const feedbackTableMut = Object.freeze({
   SET_FEEDBACK_HISTORY: 'SET_FEEDBACK_HISTORY',
@@ -58,9 +59,9 @@ const feedbackTable: Module<FeedbackTableState, RootState> = {
     [feedbackTableMut.RESET_STATE]: (state) => { Object.assign(state, initialState()) }
   },
   actions: {
-    mapFeedbackHistOfSubmissionType ({getters, commit, state}) {
+    mapFeedbackHistOfSubmissionType ({commit, state}) {
       for (const feedback of Object.values(state.feedbackHist)) {
-        const type = getters.getSubmissionType((feedback as any).ofSubmissionType)
+        const type = getters.submissionType((feedback as any).ofSubmissionType)
         commit(feedbackTableMut.SET_FEEDBACK_OF_SUBMISSION_TYPE, {feedback, type})
       }
     },
diff --git a/frontend/src/store/modules/submission-notes.ts b/frontend/src/store/modules/submission-notes.ts
index ea23fe7b..734acc86 100644
--- a/frontend/src/store/modules/submission-notes.ts
+++ b/frontend/src/store/modules/submission-notes.ts
@@ -146,7 +146,7 @@ const submissionNotes: Module<SubmissionNotesState, RootState> = {
     deleteComments: async function ({state}) {
       return Promise.all(
         Object.values(state.commentsMarkedForDeletion).map(comment => {
-          return api.deleteComment((comment as any))
+          return api.deleteComment(comment)
         })
       )
     },
diff --git a/frontend/src/store/store.ts b/frontend/src/store/store.ts
index 1398aa63..c7379d0c 100644
--- a/frontend/src/store/store.ts
+++ b/frontend/src/store/store.ts
@@ -12,8 +12,8 @@ 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 './actions'
+import './getters'
 import {lastInteraction} from '@/store/plugins/lastInteractionPlugin'
 import {
   Exam, Feedback,
@@ -81,8 +81,6 @@ const store = getStoreBuilder<RootState>().vuexStore({
           'authentication.tokenCreationTime'])
     }),
     lastInteraction],
-  actions,
-  getters,
   state: initialState()
 })
 
-- 
GitLab