ソースを参照

finalize Notification component

Olivier Massot 2 年 前
コミット
f0d09190c3

+ 60 - 45
components/Layout/Header/Notification.vue

@@ -24,6 +24,7 @@
   <v-menu
       :activator="btn"
       v-model="isOpen"
+      location="bottom left"
   >
     <v-card max-width="400">
       <v-card-title class="bg-neutral text-body-2 font-weight-bold">
@@ -31,13 +32,12 @@
       </v-card-title>
 
       <v-card-text class="ma-0 pa-0 header-menu">
-        <v-list density="compact" :subheader="true">
+        <v-list density="compact" :subheader="true" class="pa-0">
           <v-list-item
               v-for="(notification, index) in notifications"
               :key="index"
               :class="`${notification.notificationUsers.length === 0 ? 'unread' : ''}`"
           >
-
             <v-list-item-title
                 class="list_item mt-2 mb-2"
                 v-text="getMessage(notification)"
@@ -56,22 +56,23 @@
 
           <v-divider></v-divider>
 
-        </v-list>
+          <!--suppress VueUnrecognizedDirective -->
+          <span v-intersect="onLastNotificationIntersect" />
 
-        <!--suppress VueUnrecognizedDirective -->
-        <v-card v-intersect="update"></v-card>
+          <v-row
+              v-if="pending"
+              class="fill-height mt-3 mb-3"
+              align="center"
+              justify="center"
+          >
+            <v-progress-circular
+                indeterminate
+                color="neutral"
+            />
+          </v-row>
+
+        </v-list>
 
-        <v-row
-          v-if="pending"
-          class="fill-height mt-3 mb-3"
-          align="center"
-          justify="center"
-        >
-          <v-progress-circular
-            indeterminate
-            color="neutral"
-          />
-        </v-row>
       </v-card-text>
 
       <v-card-actions class="ma-0 pa-0">
@@ -97,12 +98,13 @@ import {NOTIFICATION_TYPE} from "~/types/enum/enums";
 import Notification from "~/models/Core/Notification";
 import NotificationUsers from "~/models/Core/NotificationUsers";
 import {useAccessProfileStore} from "~/stores/accessProfile";
-import {ComputedRef, Ref, ref} from "@vue/reactivity";
+import {computed, ComputedRef, Ref, ref} from "@vue/reactivity";
 import {useEntityFetch} from "~/composables/data/useEntityFetch";
-import {Pagination} from "~/types/data";
+import {AnyJson, Pagination} from "~/types/data";
 import {useEntityManager} from "~/composables/data/useEntityManager";
 import UrlUtils from "~/services/utils/urlUtils";
-import ArrayUtils from "~/services/utils/arrayUtils";
+import {useRepo} from "pinia-orm";
+import NotificationRepository from "~/stores/repositories/NotificationRepository";
 
 const accessProfileStore = useAccessProfileStore()
 
@@ -117,15 +119,27 @@ const btn = ref(null)
 
 const { em } = useEntityManager()
 const { fetchCollection } = useEntityFetch()
+const notificationRepo = useRepo(NotificationRepository)
+
+const query: ComputedRef<AnyJson> = computed(() => {
+  return { 'page': page.value }
+})
+
 
-let { data: collection, pending, refresh } = await fetchCollection(Notification)
+let { data: collection, pending, refresh } = await fetchCollection(Notification, null, query)
+
+/**
+ * On récupère les Notifications via le store (sans ça, les mises à jour SSE ne seront pas prises en compte)
+ */
+const notifications: ComputedRef<Array<Notification>> = computed(() => {
+  return notificationRepo.getNotifications()
+})
 
 /**
- * On récupère les Notifications via le store
+ * Retourne les notifications non lues
  */
-const notifications: ComputedRef = computed(() => {
-  const items = collection.value !== null ? collection.value.items : []
-  return ArrayUtils.sortObjectsByProp(items, 'id')
+const unreadNotification: ComputedRef<Array<Notification>> = computed(() => {
+  return notificationRepo.getUnreadNotifications()
 })
 
 /**
@@ -135,17 +149,20 @@ const pagination: ComputedRef<Pagination> = computed(() => {
   return collection.value !== null ? collection.value.pagination : {}
 })
 
+const notificationUrl = UrlUtils.join(runtimeConfig.baseUrlAdminLegacy, 'notifications/list/')
+
 /**
- * On calcule le nombre de notifications non lues
+ * L'utilisateur a fait défiler le menu jusqu'à la dernière notification affichée
+ * @param isIntersecting
  */
-const unreadNotification: ComputedRef<Array<Notification>> = computed(() => {
-  return notifications.value.filter((notification: Notification) => {
-    return notification.notificationUsers?.length === 0
-  })
-})
+const onLastNotificationIntersect = (isIntersecting: boolean) => {
+  if (isIntersecting) {
+    update()
+  }
+}
 
 /**
- * Lorsque l'utilisateur scroll on regarde la nextPage a charger et on le fait que si le pending du fetch est false
+ * Lorsque l'utilisateur scroll on regarde la nextPage à charger et on le fait que si le pending du fetch est false
  * (si on a fini de télécharger les éléments précédents)
  */
 const update = async () => {
@@ -191,10 +208,10 @@ const getMessage = (notification: Notification) => {
 }
 
 /**
- * Dès l'ouverture du menu, on indique que les notifications non lues, le sont.
+ * Dès la fermeture du menu, on indique que les notifications non lues, le sont.
  */
 const unwatch = watch(isOpen, (newValue, oldValue) => {
-  if (newValue){
+  if (!newValue){
     markNotificationsAsRead()
   }
 })
@@ -203,30 +220,29 @@ onUnmounted(() => {
 })
 
 /**
- * Créer une nouvelle notification users coté back.
- * @param notification
- * @param accessId
+ * Marque une notification comme lue
  */
-const createNewNotificationUsers = (notification: Notification, accessId: number) => {
+const markNotificationAsRead = (notification: Notification) => {
+  if (accessProfileStore.id === null) {
+    throw new Error('Current access id is null')
+  }
   const notificationUsers = em.newInstance(NotificationUsers, {
-    access:`/api/accesses/${accessId}`,
+    access:`/api/accesses/${accessProfileStore.id}`,
     notification:`/api/notifications/${notification.id}`,
     isRead: true
   })
 
   em.persist(NotificationUsers, notificationUsers)
   notification.notificationUsers = ['read']
+  em.save(Notification, notification)
 }
 
 /**
- * Marque les notifications non lues comme lues
+ * Marque toutes les notifications non lues comme lues
  */
 const markNotificationsAsRead = () => {
   unreadNotification.value.map((notification: Notification) => {
-    if (accessProfileStore.id === null) {
-      throw new Error('Current access id is null')
-    }
-    createNewNotificationUsers(notification, accessProfileStore.id)
+    markNotificationAsRead(notification)
   })
 }
 
@@ -248,7 +264,6 @@ const download = (link: string) => {
   window.open(UrlUtils.join(runtimeConfig.baseUrlLegacy, url_parts.join('')));
 }
 
-const notificationUrl = UrlUtils.join(runtimeConfig.baseUrlAdminLegacy, 'notifications/list/')
 
 </script>
 
@@ -257,6 +272,6 @@ const notificationUrl = UrlUtils.join(runtimeConfig.baseUrlAdminLegacy, 'notific
     white-space: normal;
   }
   .unread{
-    background: rgb(var(--v-theme-global-neutral-soft, white));
+    background: rgb(var(--v-theme-neutral-soft, white));
   }
 </style>

+ 3 - 3
composables/data/useEntityFetch.ts

@@ -6,7 +6,7 @@ import {ComputedRef, Ref} from "vue";
 
 interface useEntityFetchReturnType {
     fetch: (model: typeof ApiResource, id: number) => AsyncData<ApiResource, ApiResource | true>,
-    fetchCollection: (model: typeof ApiResource, parent?: ApiResource | null, query?: AssociativeArray) => AsyncData<Collection, any>
+    fetchCollection: (model: typeof ApiResource, parent?: ApiResource | null, query?: Ref<AssociativeArray>) => AsyncData<Collection, any>
     // @ts-ignore
     getRef: <T extends ApiResource>(model: typeof T, id: Ref<number | null>) => ComputedRef<null | T>
 }
@@ -22,9 +22,9 @@ export const useEntityFetch = (lazy: boolean = false): useEntityFetchReturnType
         { lazy }
     )
 
-    const fetchCollection = (model: typeof ApiResource, parent: ApiResource | null = null, query: AssociativeArray = []) => useAsyncData(
+    const fetchCollection = (model: typeof ApiResource, parent: ApiResource | null = null, query: Ref<AssociativeArray> = ref([])) => useAsyncData(
         model.entity + '_many',
-        () => em.fetchCollection(model, parent, query),
+        () => em.fetchCollection(model, parent, query.value),
         { lazy }
     )
 

+ 2 - 1
lang/fr.json

@@ -572,5 +572,6 @@
   "DELETED": "Supprimé",
   "ERROR": "Erreur",
   "select": "Sélectionner",
-  "Internal Server Error": "Erreur de serveur interne"
+  "Internal Server Error": "Erreur de serveur interne",
+  "cmf_licence_breadcrumbs": "Licence CMF"
 }

+ 6 - 1
models/Core/Notification.ts

@@ -16,9 +16,12 @@ export default class Notification extends ApiModel {
   @Str('')
   declare name: string
 
-  @HasOne(() => NotificationMessage, 'id')
+  @Attr({})
   declare message: NotificationMessage | null
 
+  @Str('')
+  declare createDate: string
+
   @Str(null)
   declare type: string|null
 
@@ -27,4 +30,6 @@ export default class Notification extends ApiModel {
 
   @Attr({})
   declare notificationUsers: Array<string>
+
+
 }

+ 0 - 2
pages/poc/index.vue

@@ -31,8 +31,6 @@
   import {computed, ComputedRef, ref, Ref} from "@vue/reactivity";
   import ApiResource from "~/models/ApiResource";
   import File from "~/models/Core/File";
-  import {Collection, Pagination} from "~/types/enum/data";
-  import {useEntityFetch} from "~/composables/data/useEntityFetch";
 
   const { em } = useEntityManager()
 

+ 67 - 0
pages/poc/test.vue

@@ -0,0 +1,67 @@
+<template>
+  <main>
+    <div>
+      <div v-if="pending">
+        Pending...
+      </div>
+      <div v-else>
+        <ul>
+          <li v-for="access in accesses">
+            Access {{ access.id }}
+          </li>
+        </ul>
+        <div class="ma-3">{{ totalItems }} results</div>
+        <a href="#" @click="onClick">Next page</a>
+      </div>
+    </div>
+  </main>
+</template>
+
+<script setup lang="ts">
+  import {computed, ref, Ref} from "@vue/reactivity";
+  import {useAp2iRequestService} from "~/composables/data/useAp2iRequestService";
+  import {useEntityFetch} from "~/composables/data/useEntityFetch";
+  import Access from "~/models/Access/Access";
+  import {useEntityManager} from "~/composables/data/useEntityManager";
+
+  const { em } = useEntityManager()
+  const { apiRequestService } = useAp2iRequestService()
+
+  const page: Ref<number> = ref(1)
+
+  const query = computed(() => {
+    return { 'page': page.value }
+  })
+
+  // const totalItems: Ref<number> = ref(0)
+
+
+  const { fetchCollection } = useEntityFetch()
+
+  const { data: collection = [], pending, refresh } = fetchCollection(Access, null, query)
+
+  const accesses = computed(() => {
+    // @ts-ignore
+    return collection.value ? collection.value.items : []
+  })
+
+  const totalItems = computed(() => {
+    // @ts-ignore
+    return collection.value ? collection.value.totalItems : 0
+  })
+
+  // @ts-ignore
+  // const { data: accesses = [], pending, refresh } = useLazyAsyncData(
+  //     'accesses',
+  //     async () => {
+  //       const collection = await em.fetchCollection(Access, null, query.value)
+  //       totalItems.value = collection.totalItems ?? 0
+  //       return collection.items
+  //     }
+  // )
+
+  const onClick = () => {
+    page.value += 1
+    refresh()
+  }
+</script>

+ 1 - 1
services/layout/menuBuilder/configurationMenuBuilder.ts

@@ -19,7 +19,7 @@ export default class ConfigurationMenuBuilder extends AbstractMenuBuilder {
     }
 
     if (this.ability.can('display', 'cmf_licence_page')) {
-      children.push(this.createItem('cmf_licence_generate', undefined, '/cmf_licence/organization', MENU_LINK_TYPE.V1))
+      children.push(this.createItem('cmf_licence_generate', undefined, '/cmf_licence/organization', MENU_LINK_TYPE.INTERNAL))
     }
 
     if (this.ability.can('display', 'parameters_page')) {

+ 10 - 0
stores/repositories/BaseRepository.ts

@@ -0,0 +1,10 @@
+import {Repository} from "pinia-orm";
+
+
+abstract class BaseRepository extends Repository {
+    public getQuery() {
+        return this.where((val) => Number.isInteger(val.id) ) // exclude _clone_ ids
+    }
+}
+
+export default BaseRepository

+ 29 - 0
stores/repositories/NotificationRepository.ts

@@ -0,0 +1,29 @@
+import {Collection} from "pinia-orm";
+import Notification from "~/models/Core/Notification";
+import {computed, ComputedRef} from "@vue/reactivity";
+import BaseRepository from "~/stores/repositories/BaseRepository";
+
+class NotificationRepository extends BaseRepository {
+    use = Notification
+
+    /**
+     * On récupère les Notifications via le store
+     */
+    public getNotifications(): Collection<Notification> {
+        return this.getQuery()
+            .orderBy('createDate', 'desc')
+            .get() as Collection<Notification>
+    }
+
+    /**
+     * Retourne les notifications non lues
+     */
+    public getUnreadNotifications(): Collection<Notification> {
+        return this.getQuery()
+            .where((val) => val.notificationUsers?.length === 0 )
+            .orderBy('createDate', 'desc')
+            .get() as Collection<Notification>
+    }
+}
+
+export default NotificationRepository

+ 1 - 1
yarn.lock

@@ -9384,7 +9384,7 @@ uuid@^8.3.0:
   resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2"
   integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==
 
-uuid@^9.0.1:
+uuid@^9.0.0:
   version "9.0.0"
   resolved "https://registry.yarnpkg.com/uuid/-/uuid-9.0.0.tgz#592f550650024a38ceb0c562f2f6aa435761efb5"
   integrity sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==