浏览代码

implement websocket communicaton

tripeur 4 年之前
父节点
当前提交
7555f5d0d6
共有 5 个文件被更改,包括 98 次插入6 次删除
  1. 7 1
      package.json
  2. 73 5
      src/PlannerApp.vue
  3. 9 0
      src/models/PlanningUpdateMessage.ts
  4. 8 0
      src/store/Mutations.ts
  5. 1 0
      src/store/State.ts

+ 7 - 1
package.json

@@ -15,6 +15,10 @@
     "lodash": "^4.17.21",
     "lodash-es": "^4.17.21",
     "marked": "^2.1.3",
+    "sockjs": "^0.3.21",
+    "sockjs-client": "^1.5.1",
+    "stomp": "^0.1.1",
+    "stompjs": "^2.3.3",
     "uuid": "^8.3.2",
     "vue": "^3.0.0",
     "vue-class-component": "^8.0.0-0",
@@ -26,6 +30,8 @@
     "@types/lodash-es": "^4.17.4",
     "@types/marked": "^2.0.3",
     "@types/materialize-css": "^1.0.9",
+    "@types/sockjs-client": "^1.5.1",
+    "@types/stompjs": "^2.3.5",
     "@types/uuid": "^8.3.0",
     "@typescript-eslint/eslint-plugin": "^4.18.0",
     "@typescript-eslint/parser": "^4.18.0",
@@ -39,7 +45,7 @@
     "eslint": "^6.7.2",
     "eslint-plugin-prettier": "^3.3.1",
     "eslint-plugin-vue": "^7.0.0",
-    "node-sass": "^5.0.0",
+    "node-sass": "^6.0.1",
     "postcss": "^8.3.0",
     "prettier": "^2.2.1",
     "sass": "^1.32.8",

+ 73 - 5
src/PlannerApp.vue

@@ -31,11 +31,14 @@
   <c-footer v-if="showFooter" />
 </template>
 <script lang="ts">
+import SockJS from "sockjs-client";
+import Stomp from "stompjs";
 import { defineComponent } from "vue";
 import toast, { Toast } from "@/utils/Toast";
 import ConstraintTranslation from "@/assets/ConstraintTranslation";
 import Evenement from "@/models/Evenement";
 import Creneau from "@/models/Creneau";
+import PlanningUpdateMessage from "@/models/PlanningUpdateMessage";
 import {
   AssignementPair,
   SolverInput,
@@ -68,6 +71,7 @@ export default defineComponent({
   mixins: [updatePlanningVersions, importJsonState],
   data() {
     return {
+      stompClient: Stomp.over(new SockJS("/ws")),
       optimisationInProgress: false,
       explanation: "",
       showExplanation: false,
@@ -79,10 +83,36 @@ export default defineComponent({
     showFooter(): boolean {
       return this.$route.name != "Planning";
     },
+    uuid(): string {
+      return this.$store.state.evenement.uuid;
+    },
+    username(): string {
+      return this.$store.state.username;
+    },
+  },
+  watch: {
+    uuid(val: string, old: string) {
+      if (old && this.stompClient.connected) this.stompClient.unsubscribe("/planning/" + old);
+      this.subscribe(val);
+    },
+  },
+  beforeMount() {
+    this.stompClient.connect(
+      {},
+      () => {
+        this.stompClient.subscribe("/toast", (msg) => {
+          var content = JSON.parse(msg.body) as { text: string; type: string };
+          toast({ html: content.text, classes: content.type });
+        });
+        this.subscribe(this.uuid);
+      },
+      (error) => {
+        toast({ html: error.toString(), classes: "error" });
+      }
+    );
   },
   mounted() {
-    const url = `${API_URL}api/evenements`;
-    fetch(url)
+    fetch(`${API_URL}api/evenements`)
       .then((response) => {
         if (response.status == 200) {
           return response.json();
@@ -93,11 +123,20 @@ export default defineComponent({
       .then((data) => this.$store.commit(MutationTypes.refreshSavedPlanning, data));
 
     const previousState = window.localStorage.getItem("activeState");
-    toast({ html: "Bienvenue" });
+
     if (previousState) {
       this.importState(JSON.parse(previousState));
       if (this.$route.path == "/planner") this.$router.push({ name: "Evenement" });
     }
+    fetch(`${API_URL}username`)
+      .then((res) => res.text())
+      .then((username) => {
+        if (username.length < 400) {
+          toast({ html: "Bienvenue " + username });
+          this.$store.commit(MutationTypes.setUsername, username);
+        }
+      });
+
     window.onbeforeunload = () => {
       this.localSave();
     };
@@ -106,6 +145,32 @@ export default defineComponent({
     this.save();
   },
   methods: {
+    subscribe(uuid: string) {
+      if (uuid && this.stompClient.connected) {
+        this.stompClient.subscribe("/planning/" + uuid, (msg) => this.handlePlanningChange(msg));
+        this.stompClient.send(
+          "/app/notify",
+          {},
+          JSON.stringify({
+            uuid,
+            user: this.username,
+            method: "toast",
+            payload: `L'utilisateur ${this.username} vient de se connecter`,
+          })
+        );
+      }
+    },
+    handlePlanningChange(msg: Stomp.Message) {
+      var content = JSON.parse(msg.body) as PlanningUpdateMessage;
+      console.log("Received message ", content);
+      if (this.uuid == content.uuid && this.username && this.username != content.user) {
+        if (content.method === "toast") {
+          toast({ html: content.payload });
+        } else {
+          this.$store.commit(content.method, JSON.parse(content.payload));
+        }
+      }
+    },
     localSave() {
       window.localStorage.setItem("activeState", JSON.stringify(this.$store.getters.getJSONState));
     },
@@ -199,12 +264,15 @@ export default defineComponent({
       const slots = this.$store.state.creneauList.filter((o) => !o.isMeal);
       const timeslots = slots.map((o) => o.toTimeslotRaw());
       const mealSlots = meals.map((o) => o.toMealSlot());
-      const volonteerList = this.$store.state.benevoleList.map((b) => b.toVolonteeRaw());
+      const volonteerList = this.$store.state.benevoleList
+        .filter((b) => !b.isFixed)
+        .map((b) => b.toVolonteeRaw());
       const constraints = this.$store.state.competenceList.map((c) => c.toSkill());
       const getAssignementPair = (c: Creneau) =>
         c.benevoleIdList.map((id) => {
           const obj: AssignementPair = { volonteerId: id, slotId: c.id };
-          if (c.fixedAttendee) obj.isFixed = true;
+          if (c.fixedAttendee || this.$store.getters.getBenevoleById(id)?.isFixed)
+            obj.isFixed = true;
           return obj;
         });
       const assignements = slots.flatMap(getAssignementPair);

+ 9 - 0
src/models/PlanningUpdateMessage.ts

@@ -0,0 +1,9 @@
+import { Mutations } from "@/store/Mutations";
+
+type PlanningUpdateMessage = {
+  uuid: string;
+  user: string;
+  method: keyof Mutations | "toast";
+  payload: string;
+};
+export default PlanningUpdateMessage;

+ 8 - 0
src/store/Mutations.ts

@@ -37,6 +37,8 @@ export enum MutationTypes {
 
   pushSavedPlanning = "pushSavedPlanning",
   refreshSavedPlanning = "refreshSavedPlanning",
+
+  setUsername = "setUsername",
 }
 interface CreneauPairing {
   creneauId: string;
@@ -89,6 +91,8 @@ export type Mutations<S = State> = {
 
   [MutationTypes.pushSavedPlanning](state: S, payload: EvenementVersion): void;
   [MutationTypes.refreshSavedPlanning](state: S, payload: Array<EvenementVersion>): void;
+
+  [MutationTypes.setUsername](state: S, payload: string): void;
 };
 export const mutations: MutationTree<State> & Mutations = {
   [MutationTypes.resetState](state): void {
@@ -229,4 +233,8 @@ export const mutations: MutationTree<State> & Mutations = {
   [MutationTypes.refreshSavedPlanning](state, payload) {
     state.savedPlanning = payload;
   },
+
+  [MutationTypes.setUsername](state, payload) {
+    state.username = payload;
+  },
 };

+ 1 - 0
src/store/State.ts

@@ -21,5 +21,6 @@ export const state = {
   benevoleList: [] as Array<Benevole>,
   history: [] as Array<EvenementVersion>,
   savedPlanning: [] as Array<EvenementVersion>,
+  username: "",
 };
 export type State = typeof state;