瀏覽代碼

refactor and add new components

refactor ParametersTable by adding a new layer (ParametersEntityTable), add the DeletionConfirmationDialog component, and the useDeleteItem composable
Olivier Massot 10 月之前
父節點
當前提交
7ed03052ac

+ 192 - 0
components/Layout/Parameters/EntityTable.vue

@@ -0,0 +1,192 @@
+<!--
+A data table for the parameters page
+-->
+<template>
+  <div class="container">
+    <UiLoadingPanel v-if="pending" />
+    <div v-else>
+      <LayoutParametersTable
+        :items="items"
+        :columns-definitions="columns"
+        :actions="actions"
+        :actions-route="actionsRoute"
+        @editClicked="onEditClicked"
+        @deleteClicked="onDeleteClicked"
+        @addClicked="goToCreatePage"
+      />
+
+      <UiFormDeletionConfirmationDialog
+        v-model="showDeletionConfirmationDialog"
+        @deleteClicked="onDeleteConfirmed"
+        @cancelClicked="onCancelClicked"
+      />
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { TABLE_ACTION } from '~/types/enum/enums'
+import UrlUtils from '~/services/utils/urlUtils'
+import type ApiResource from '~/models/ApiResource'
+import { useEntityFetch } from '~/composables/data/useEntityFetch'
+import type { AssociativeArray } from '~/types/data'
+import type { ColumnDefinition } from '~/types/interfaces'
+import {useDeleteItem} from '~/composables/form/useDeleteItem'
+import {useEntityManager} from '~/composables/data/useEntityManager';
+
+const props = defineProps({
+  /**
+   * The model whom entities shall be to fetch
+   */
+  model: {
+    type: Object as PropType<typeof ApiResource>,
+    required: true,
+  },
+  /**
+   * If provided, define the columns to show.
+   * Else, all the entity's props are shown.
+   *
+   * Ex: [
+   *       { property: 'id', label : 'Identifier'},
+   *       { property: 'name', label : 'Full name'},
+   *     ]
+   */
+  columnsDefinitions: {
+    type: Array as PropType<Array<ColumnDefinition> | null>,
+    required: false,
+    default: null,
+  },
+  /**
+   * List of the actions available for each record
+   */
+  actions: {
+    type: Array as PropType<Array<TABLE_ACTION>>,
+    required: false,
+    default: [TABLE_ACTION.EDIT, TABLE_ACTION.DELETE, TABLE_ACTION.ADD],
+  },
+  /**
+   * The base URL for the edit / create pages
+   * The resulting url will be constructed this way :
+   *
+   * Edition : ({baseUrl}/){apiResource.entity}/{id}
+   * Creation : ({baseUrl}/){apiResource.entity}/new
+   */
+  baseActionsRoute: {
+    type: String,
+    required: false,
+    default: '/parameters',
+  },
+  /**
+   * If provided, sort the record by the given property
+   */
+  sortBy: {
+    type: String as PropType<string>,
+    required: false,
+    default: 'id',
+  },
+})
+
+const i18n = useI18n()
+
+const { em } = useEntityManager()
+
+const { fetchCollection } = useEntityFetch()
+
+const { data: collection, pending } = fetchCollection(props.model)
+
+const { deleteItem } = useDeleteItem()
+
+const pageStore = usePageStore()
+
+/**
+ * Return the properties to display in the table, or all the
+ * props of the model if not specified.
+ */
+const columns: ComputedRef<Array<ColumnDefinition>> = computed(() => {
+  return (
+    props.columnsDefinitions ??
+    Object.getOwnPropertyNames(new props.model()).map((prop) => {
+      return { property: prop }
+    })
+  )
+})
+
+/**
+ * Fetch the collection of ApiResources of the given model, then
+ * map it according to the configuration.
+ */
+const items: ComputedRef<Array<ApiResource> | null> = computed(() => {
+  if (pending.value || collection.value === null) {
+    return null
+  }
+
+  let items: Array<ApiResource> = collection.value!.items
+
+  if (props.columnsDefinitions !== null) {
+    // Filter the columns to show
+    items = items.map((item) => {
+      const newItem: ApiResource = { id: item.id }
+      for (const col of props.columnsDefinitions!) {
+        newItem[col.property] = item[col.property]
+      }
+      return newItem
+    })
+  }
+
+  if (props.sortBy) {
+    items = items.sort((a: AssociativeArray, b: AssociativeArray) => {
+      return a[props.sortBy as keyof typeof a] >
+        b[props.sortBy as keyof typeof b]
+        ? 1
+        : -1
+    })
+  }
+
+  return items
+})
+
+const actionsRoute = computed(() => {
+  return UrlUtils.join(props.baseActionsRoute, props.model.entity)
+})
+
+const showDeletionConfirmationDialog: Ref<boolean> = ref(false)
+const itemToDelete: Ref<ApiResource | null> = ref(null)
+/**
+ * Redirect to the edition page for the given item
+ * @param item
+ */
+const onEditClicked = (item: ApiResource) => {
+  navigateTo(UrlUtils.join(actionsRoute.value, item.id))
+}
+
+/**
+ * Show the deletion confirmation dialog
+ * @param item
+ */
+const onDeleteClicked = (item: ApiResource) => {
+  itemToDelete.value = em.cast(props.model, item)
+  showDeletionConfirmationDialog.value = true
+}
+
+const onCancelClicked = () => {
+  itemToDelete.value = null
+}
+
+/**
+ * Deletion has be confirmed, perform
+ */
+const onDeleteConfirmed = async () => {
+  pageStore.loading = true
+  await deleteItem(itemToDelete.value)
+  pageStore.loading = false
+}
+
+/**
+ * Redirect to the creation page for this model
+ */
+const goToCreatePage = () => {
+  navigateTo(UrlUtils.join(actionsRoute.value, 'new'))
+}
+</script>
+
+<style scoped lang="scss"></style>

+ 0 - 0
components/Layout/Parameters/ResidenceAreas.vue


+ 49 - 131
components/Layout/Parameters/Table.vue

@@ -3,8 +3,7 @@ A data table for the parameters page
 -->
 <template>
   <div class="container">
-    <UiLoadingPanel v-if="pending" />
-    <v-table v-else>
+    <v-table>
       <thead>
         <tr>
           <td v-for="col in columns">
@@ -19,25 +18,28 @@ A data table for the parameters page
           :key="i"
         >
           <td
-            v-for="col in columns"
+            v-for="col in columnsDefinitions"
             class="cycle-editable-cell"
           >
             {{ item[col.property] }}
           </td>
 
           <td class="d-flex flex-row actions-cell">
-            <v-btn
-              v-if="actions.includes(TABLE_ACTION.EDIT)"
-              :flat="true"
-              icon="fa fa-pen"
-              class="mr-3"
-              @click="goToEditPage(item.id as number)"
-            />
-            <UiButtonDelete
-              v-if="actions.includes(TABLE_ACTION.DELETE)"
-              :entity="item"
-              :flat="true"
-            />
+            <slot name="actions" :item="item">
+              <v-btn
+                v-if="actions.includes(TABLE_ACTION.EDIT)"
+                :flat="true"
+                icon="fa fa-pen"
+                class="mr-3"
+                @click="emit('editClicked', item)"
+              />
+              <v-btn
+                v-if="actions.includes(TABLE_ACTION.DELETE)"
+                :flat="true"
+                icon="fas fa-trash"
+                @click="emit('deleteClicked', item)"
+              />
+            </slot>
           </td>
         </tr>
       </tbody>
@@ -55,7 +57,7 @@ A data table for the parameters page
         :flat="true"
         prepend-icon="fa fa-plus"
         class="theme-primary mt-4"
-        @click="goToCreatePage"
+        @click="emit('addClicked')"
       >
         {{ i18n.t('add') }}
       </v-btn>
@@ -66,42 +68,15 @@ A data table for the parameters page
 <script setup lang="ts">
 import {TABLE_ACTION} from '~/types/enum/enums';
 import UrlUtils from '~/services/utils/urlUtils';
-import type ApiResource from '~/models/ApiResource';
-import {useEntityFetch} from '~/composables/data/useEntityFetch';
-import type {AssociativeArray} from '~/types/data';
-
-interface ColumnDefinition {
-  /**
-   * The entity's property to display in this column
-   */
-  property: string,
-  /**
-   * Label of the column.
-   * If not provided, a translation of the property's name will be looked for.
-   * If none is found, the property's name will be displayed as it is.
-   */
-  label?: string
-}
+import type {ColumnDefinition} from '~/types/interfaces';
 
 const props = defineProps({
   /**
-   * The model whom entities shall be to fetch
+   * Array of objects to display in the table
    */
-  model: {
-    type: Object as PropType<typeof ApiResource>,
-    required: true
-  },
-  /**
-   * The base URL for the edit / create pages
-   * The resulting url will be constructed this way :
-   *
-   * Edition : ({baseUrl}/){apiResource.entity}/{id}
-   * Creation : ({baseUrl}/){apiResource.entity}/new
-   */
-  baseRoute: {
-    type: String,
-    required: false,
-    default: '/parameters'
+  items: {
+    type: Array as PropType<Array<object>>,
+    required: true,
   },
   /**
    * If provided, define the columns to show.
@@ -117,6 +92,14 @@ const props = defineProps({
     required: false,
     default: null
   },
+  /**
+   * The property used as identifier (required by 'edition' link)
+   */
+  identifier: {
+    type: String,
+    required: false,
+    default: 'id'
+  },
   /**
    * List of the actions available for each record
    */
@@ -126,101 +109,36 @@ const props = defineProps({
     default: [TABLE_ACTION.EDIT, TABLE_ACTION.DELETE, TABLE_ACTION.ADD]
   },
   /**
-   * If provided, sort the record by the given property
+   * The URL for the edit / create pages
+   * The resulting url will be constructed this way :
+   *
+   * Edition : {baseUrl}/{id}
+   * Creation : {baseUrl}/new
    */
-  sortBy: {
-    type: String as PropType<string>,
+  actionsRoute: {
+    type: String,
     required: false,
-    default: 'id'
+    default: '/parameters'
   }
 })
 
 const i18n = useI18n()
 
-const { fetchCollection } = useEntityFetch()
+const emit = defineEmits(['editClicked', 'deleteClicked', 'addClicked'])
 
-const {
-  data: collection,
-  pending,
-} = fetchCollection(props.model)
+const getId = (item: object) => {
+  return item[props.identifier]
+}
 
-/**
- * Return the properties to display in the table, or all the
- * props of the model if not specified.
- */
 const columns: ComputedRef<Array<ColumnDefinition>> = computed(() => {
-  let columns: Array<ColumnDefinition>
-
-  if (!props.columnsDefinitions) {
-    const entityProps = Object.getOwnPropertyNames(new props.model())
-
-    columns = entityProps.map(prop => {
-      return {
-        property: prop,
-        label: i18n.t(prop)
-      }
-    })
-  } else {
-    columns = props.columnsDefinitions
-
-    columns = columns.map(col => {
-      return {
-        property: col.property,
-        label: col.label ?? i18n.t(col.property)
-      }
-    })
-  }
-
-  return columns
-})
-
-/**
- * Fetch the collection of ApiResources of the given model, then
- * map it according to the configuration.
- */
-const items: ComputedRef<Array<ApiResource> | null> = computed(() => {
-  if (pending.value || collection.value === null) {
-    return null
-  }
-
-  let items: Array<ApiResource> = collection.value!.items
-
-  if (props.columnsDefinitions !== null) {
-    // Filter the columns to show
-    items = items.map(item => {
-      const newItem: ApiResource = { id: item.id }
-      for (const col of props.columnsDefinitions) {
-        newItem[col.property] = item[col.property]
-      }
-      return newItem
-    })
-  }
-
-  if (props.sortBy) {
-    items = items.sort((a: AssociativeArray, b: AssociativeArray) => {
-      return (a[props.sortBy as keyof typeof a] > b[props.sortBy as keyof typeof b]) ? 1 : -1
-    })
-  }
-
-  return items
+  return props.columnsDefinitions.map(col => {
+    return {
+      property: col.property,
+      label: col.label ?? i18n.t(col.property)
+    }
+  })
 })
 
-/**
- * Redirect to the edition page for the given item
- * @param id
- */
-const goToEditPage = (id: number) => {
-  console.log(props.baseRoute, props.model.entity, id, UrlUtils.join(props.baseRoute, props.model.entity, id))
-  navigateTo(UrlUtils.join(props.baseRoute, props.model.entity, id))
-}
-
-/**
- * Redirect to the creation page for this model
- */
-const goToCreatePage = () => {
-  navigateTo(UrlUtils.join(props.baseRoute, props.model.entity, 'new'))
-}
-
 </script>
 
 <style scoped lang="scss">

+ 10 - 36
components/Ui/Button/Delete.vue

@@ -8,32 +8,17 @@ Bouton Delete avec modale de confirmation de la suppression
       <v-icon>fas fa-trash</v-icon>
     </v-btn>
 
-    <LazyLayoutDialog :show="showDialog">
-      <template #dialogType>{{ $t('delete_assistant') }}</template>
-      <template #dialogTitle>{{ $t('caution') }}</template>
-      <template #dialogText>
-        <v-card-text>
-          <p>{{ $t('confirm_to_delete') }}</p>
-        </v-card-text>
-      </template>
-      <template #dialogBtn>
-        <v-btn class="mr-4 submitBtn theme-neutral-strong" @click="closeDialog">
-          {{ $t('cancel') }}
-        </v-btn>
-        <v-btn class="mr-4 submitBtn theme-danger" @click="deleteItem">
-          {{ $t('delete') }}
-        </v-btn>
-      </template>
-    </LazyLayoutDialog>
+    <UiFormDeletionConfirmationDialog
+      v-model="showDialog"
+      @delete-clicked="onDeleteClicked"
+    />
   </main>
 </template>
 
 <script setup lang="ts">
 import type { Ref, PropType } from 'vue'
-import { TYPE_ALERT } from '~/types/enum/enums'
-import { useEntityManager } from '~/composables/data/useEntityManager'
 import ApiResource from '~/models/ApiResource'
-import { usePageStore } from '~/stores/page'
+import {useDeleteItem} from '~/composables/form/useDeleteItem'
 
 const props = defineProps({
   entity: {
@@ -47,27 +32,16 @@ const props = defineProps({
   },
 })
 
-const showDialog: Ref<boolean> = ref(false)
-
-const { em } = useEntityManager()
+const { deleteItem } = useDeleteItem()
 
-const deleteItem = async () => {
-  try {
-    await em.delete(props.entity)
-    usePageStore().addAlert(TYPE_ALERT.SUCCESS, ['deleteSuccess'])
-  } catch (error) {
-    // @ts-expect-error error is supposed to have a message prop
-    usePageStore().addAlert(TYPE_ALERT.ALERT, [error.message])
-    throw error
-  }
-  showDialog.value = false
-}
+const showDialog: Ref<boolean> = ref(false)
 
 const alertDeleteItem = () => {
   showDialog.value = true
 }
-const closeDialog = () => {
-  showDialog.value = false
+
+const onDeleteClicked = async () => {
+  await deleteItem(entity)
 }
 </script>
 

+ 50 - 0
components/Ui/Form/DeletionConfirmationDialog.vue

@@ -0,0 +1,50 @@
+<template>
+  <LazyLayoutDialog :show="modelValue">
+    <template #dialogType>
+      {{ $t('delete_assistant') }}
+    </template>
+
+    <template #dialogTitle>
+      {{ $t('caution') }}
+    </template>
+
+    <template #dialogText>
+      <v-card-text>
+        <p>{{ $t('confirm_to_delete') }}</p>
+      </v-card-text>
+    </template>
+
+    <template #dialogBtn>
+      <v-btn class="mr-4 submitBtn theme-neutral-strong" @click="onCancelClicked">
+        {{ $t('cancel') }}
+      </v-btn>
+      <v-btn class="mr-4 submitBtn theme-danger" @click="onDeleteClicked">
+        {{ $t('delete') }}
+      </v-btn>
+    </template>
+  </LazyLayoutDialog>
+</template>
+
+<script setup lang="ts">
+const props = defineProps({
+  modelValue: {
+    type: Boolean
+  }
+})
+
+const emit = defineEmits(['cancelClicked', 'deleteClicked', 'update:modelValue'])
+
+const onCancelClicked = () => {
+  emit('cancelClicked')
+  emit('update:modelValue', false)
+}
+
+const onDeleteClicked = () => {
+  emit('deleteClicked')
+  emit('update:modelValue', false)
+}
+</script>
+
+<style scoped lang="scss">
+
+</style>

+ 25 - 0
composables/form/useDeleteItem.ts

@@ -0,0 +1,25 @@
+import type ApiResource from '~/models/ApiResource';
+import {usePageStore} from '~/stores/page';
+import {TYPE_ALERT} from '~/types/enum/enums';
+import {useEntityManager} from '~/composables/data/useEntityManager';
+
+
+export function useDeleteItem() {
+
+  async function deleteItem(item: ApiResource) {
+    const {em} = useEntityManager()
+
+    try {
+      await em.delete(item)
+      usePageStore().addAlert(TYPE_ALERT.SUCCESS, ['deleteSuccess'])
+    } catch (error) {
+      // @ts-expect-error error is supposed to have a message prop
+      usePageStore().addAlert(TYPE_ALERT.ALERT, [error.message])
+      throw error
+    }
+  }
+
+  return {
+    deleteItem
+  }
+}

+ 1 - 1
pages/parameters/attendances.vue

@@ -38,7 +38,7 @@
       <v-divider class="my-10" />
     </div>
 
-    <LayoutParametersTable
+    <LayoutParametersEntityTable
       :model="AttendanceBookingReason"
       :columns-definitions="[{ property: 'reason' }]"
     />

+ 1 - 1
pages/parameters/education_timings/index.vue

@@ -1,6 +1,6 @@
 <template>
   <LayoutContainer>
-    <LayoutParametersTable
+    <LayoutParametersEntityTable
       :model="EducationTiming"
       :columns-definitions="[{ property: 'timing' }]"
     />

+ 1 - 1
pages/parameters/residence_areas/index.vue

@@ -1,6 +1,6 @@
 <template>
   <LayoutContainer>
-    <LayoutParametersTable
+    <LayoutParametersEntityTable
       :model="ResidenceArea"
       :columns-definitions="[{ property: 'label' }]"
     />

+ 49 - 34
pages/parameters/teaching.vue

@@ -7,37 +7,45 @@
       :entity="parameters"
       action-position="bottom"
     >
-      <v-table>
-        <thead>
-          <tr>
-            <td>{{ $t('originalLabel') }}</td>
-            <td>{{ $t('effectiveLabel') }}</td>
-            <td>{{ $t('actions') }}</td>
-          </tr>
-        </thead>
-
-        <tbody>
-          <tr v-for="enumItem in cycleEnum" :key="enumItem.value">
-            <td>{{ $t(enumItem.value) }}</td>
-            <td class="cycle-editable-cell">
-              {{
-                orderedCycles[enumItem.value]
-                  ? orderedCycles[enumItem.value].label
-                  : $t(enumItem.value)
-              }}
-            </td>
-            <td style="max-width: 24px">
-              <v-btn
-                v-if="orderedCycles[enumItem.value]"
-                :flat="true"
-                icon="fa fa-pen"
-                class="cycle-edit-icon"
-                @click="goToCycleEditPage(orderedCycles[enumItem.value].id)"
-              />
-            </td>
-          </tr>
-        </tbody>
-      </v-table>
+      <LayoutParametersTable
+        :items="tableItems"
+        :columns-definitions="[{ property: 'originalLabel' }, { property: 'effectiveLabel' }]"
+        identifier="value"
+        :actions="[TABLE_ACTION.EDIT]"
+        actions-route="/parameters/cycles"
+      />
+
+<!--      <v-table>-->
+<!--        <thead>-->
+<!--          <tr>-->
+<!--            <td>{{ $t('originalLabel') }}</td>-->
+<!--            <td>{{ $t('effectiveLabel') }}</td>-->
+<!--            <td>{{ $t('actions') }}</td>-->
+<!--          </tr>-->
+<!--        </thead>-->
+
+<!--        <tbody>-->
+<!--          <tr v-for="enumItem in cycleEnum" :key="enumItem.value">-->
+<!--            <td>{{ $t(enumItem.value) }}</td>-->
+<!--            <td class="cycle-editable-cell">-->
+<!--              {{-->
+<!--                orderedCycles[enumItem.value]-->
+<!--                  ? orderedCycles[enumItem.value].label-->
+<!--                  : $t(enumItem.value)-->
+<!--              }}-->
+<!--            </td>-->
+<!--            <td style="max-width: 24px">-->
+<!--              <v-btn-->
+<!--                v-if="orderedCycles[enumItem.value]"-->
+<!--                :flat="true"-->
+<!--                icon="fa fa-pen"-->
+<!--                class="cycle-edit-icon"-->
+<!--                @click="goToCycleEditPage(orderedCycles[enumItem.value].id)"-->
+<!--              />-->
+<!--            </td>-->
+<!--          </tr>-->
+<!--        </tbody>-->
+<!--      </v-table>-->
 
       <UiInputCheckbox
         v-model="parameters.showEducationIsACollectivePractice"
@@ -58,6 +66,7 @@ import { useOrganizationProfileStore } from '~/stores/organizationProfile'
 import type { AnyJson } from '~/types/data'
 import { useEnumFetch } from '~/composables/data/useEnumFetch'
 import ApiResource from '~/models/ApiResource'
+import {TABLE_ACTION} from '~/types/enum/enums';
 
 definePageMeta({
   name: 'parameters_teaching_page',
@@ -108,9 +117,15 @@ const orderedCycles: ComputedRef<AnyJson> = computed(() => {
   return orderedCycles
 })
 
-const goToCycleEditPage = (id: number) => {
-  navigateTo(`/parameters/cycles/${id}`)
-}
+const tableItems = computed(() => {
+  return cycleEnum.value?.map((item) => {
+    return {
+      value: item.value,
+      originalLabel: item.label,
+      effectiveLabel: item.label
+    }
+  }) || []
+})
 </script>
 
 <style scoped lang="scss">

+ 6 - 4
services/data/entityManager.ts

@@ -71,10 +71,9 @@ class EntityManager {
    *
    * @param model
    * @param instance
-   * @protected
    */
   // noinspection JSMethodCanBeStatic
-  protected cast(
+  public cast(
     model: typeof ApiResource,
     instance: ApiResource,
   ): ApiResource {
@@ -326,11 +325,14 @@ class EntityManager {
   /**
    * Delete the model instance from the datasource via the API
    *
-   * @param model
+   * @param instance
    * @param instance
    */
-  public async delete(instance: ApiResource) {
+  public async delete(instance: ApiModel) {
     const model = instance.constructor as typeof ApiModel
+    instance = this.cast(model, instance)
+
+    console.log('delete', instance)
 
     this.validateEntity(instance)
 

+ 13 - 0
types/interfaces.d.ts

@@ -196,3 +196,16 @@ interface LayoutState {
   menus: Record<string, MenuGroup | MenuItem>
   menusOpened: Record<string, boolean>
 }
+
+interface ColumnDefinition {
+  /**
+   * The entity's property to display in this column
+   */
+  property: string,
+  /**
+   * Label of the column.
+   * If not provided, a translation of the property's name will be looked for.
+   * If none is found, the property's name will be displayed as it is.
+   */
+  label?: string
+}