Jelajahi Sumber

add UiInputCombobox, fix UiInputAutocomplete (ongoing), document

Olivier Massot 2 tahun lalu
induk
melakukan
797e2217dd

+ 1 - 1
components/Layout/Parameters/PreferencesTab/GeneralParameters.vue

@@ -53,7 +53,7 @@
     </v-row>
 
     <v-row>
-      <v-combobox
+      <UiInputCombobox
           :label="$t('timezone')"
           :items="['Europe / Paris', 'Europe / Zurich', 'Indian / La Réunion']"
       />

+ 180 - 95
components/Ui/Input/Autocomplete.vue

@@ -1,31 +1,32 @@
 <!--
-Liste déroulante avec autocompletion
+Liste déroulante avec autocompletion, à placer dans un composant `UiForm`
 
 @see https://vuetifyjs.com/en/components/autocompletes/#usage
 -->
 
 <template>
   <main>
+    <!--suppress TypeScriptValidateTypes -->
     <v-autocomplete
-      autocomplete="search"
-      :value="data"
-      :items="itemsToDisplayed"
-      :label="$t(fieldLabel)"
-      item-text="itemTextDisplay"
-      :item-value="itemValue"
-      :no-data-text="$t('autocomplete_research')"
-      :no-filter="noFilter"
-      auto-select-first
-      :multiple="multiple"
-      :loading="isLoading"
-      :return-object="returnObject"
-      :search-input.sync="search"
-      :prepend-icon="prependIcon"
-      :error="error || !!violation"
-      :error-messages="errorMessage || violation ? $t(violation) : ''"
-      :rules="rules"
-      :chips="chips"
-      @input="onChange($event)"
+        :model-value="modelValue"
+        autocomplete="search"
+        :items="items"
+        :label="$t(fieldLabel)"
+        item-text="itemTextDisplay"
+        :item-value="itemValue"
+        :no-data-text="$t('autocomplete_research')"
+        :no-filter="noFilter"
+        auto-select-first
+        :multiple="multiple"
+        :loading="isLoading"
+        :return-object="returnObject"
+        :search-input.sync="search"
+        :prepend-icon="prependIcon"
+        :error="error || !!fieldViolations"
+        :error-messages="errorMessage || fieldViolations ? $t(fieldViolations) : ''"
+        :rules="rules"
+        :chips="chips"
+        @update:model-value="onUpdate"
     >
       <template v-if="slotText" #item="data">
         <v-list-item-content v-text="data.item.slotTextDisplay"></v-list-item-content>
@@ -35,91 +36,154 @@ Liste déroulante avec autocompletion
 </template>
 
 <script setup lang="ts">
-import {useNuxtApp} from "#app";
 import {computed, ComputedRef, Ref} from "@vue/reactivity";
 import {useFieldViolation} from "~/composables/form/useFieldViolation";
-import {onUnmounted, watch} from "@vue/runtime-core";
 import ObjectUtils from "~/services/utils/objectUtils";
+import {AnyJson} from "~/types/data";
 
 const props = defineProps({
-  label: {
-    type: String,
+  /**
+   * v-model
+   */
+  modelValue: {
+    type: [String, Number, Object, Array],
     required: false,
     default: null
   },
+  /**
+   * Nom de la propriété d'une entité lorsque l'input concerne cette propriété
+   * - Utilisé par la validation
+   * - Laisser null si le champ ne s'applique pas à une entité
+   */
   field: {
     type: String,
     required: false,
     default: null
   },
-  data: {
-    type: [String, Number, Object, Array],
+  /**
+   * Label du champ
+   * Si non défini, c'est le nom de propriété qui est utilisé
+   */
+  label: {
+    type: String,
     required: false,
     default: null
   },
+  /**
+   * Liste des éléments de la liste
+   * @see https://vuetifyjs.com/en/api/v-autocomplete/#props-items
+   */
   items: {
-    type: Array,
+    type: Array<Object>,
     required: false,
     default: () => []
   },
+  /**
+   * Définit si le champ est en lecture seule
+   * @see https://vuetifyjs.com/en/api/v-autocomplete/#props-readonly
+   */
   readonly: {
     type: Boolean,
     required: false
   },
+  /**
+   * Le model est l'objet lui-même, et non pas son id (ou la propriété définie avec itemValue)
+   * @see https://vuetifyjs.com/en/api/v-autocomplete/#props-return-object
+   */
+  returnObject: {
+    type: Boolean,
+    default: false
+  },
+  /**
+   * Autorise la sélection multiple
+   * @see https://vuetifyjs.com/en/api/v-autocomplete/#props-multiple
+   */
+  multiple: {
+    type: Boolean,
+    default: false
+  },
+  /**
+   * Propriété de l'objet à utiliser comme label
+   * @see https://vuetifyjs.com/en/api/v-autocomplete/#props-item-title
+   */
+  itemTitle: {
+    type: String,
+    required: true
+  },
+  /**
+   * Propriété de l'objet à utiliser comme clé (et correspondant au v-model)
+   * @see https://vuetifyjs.com/en/api/v-autocomplete/#props-item-value
+   */
   itemValue: {
     type: String,
     default: 'id'
   },
-  itemText: {
-    type: Array,
-    required: true
+  /**
+   * Icône de gauche
+   * @see https://vuetifyjs.com/en/api/v-autocomplete/#props-prepend-icon
+   */
+  prependIcon: {
+    type: String
+  },
+  /**
+   * Rends les résultats sous forme de puces
+   * @see https://vuetifyjs.com/en/api/v-autocomplete/#props-chips
+   */
+  chips: {
+    type: Boolean,
+    default: false
+  },
+  /**
+   * Le contenu de la liste est en cours de chargement
+   */
+  isLoading: {
+    type: Boolean,
+    required: false,
+    default: false
   },
-  group:{
+  /**
+   * Propriété de l'objet utilisé pour grouper les items ; laisser null pour ne pas grouper
+   */
+  group: {
     type: String,
     required: false,
     default: null
   },
+  // TODO: c'est quoi?
   slotText: {
     type: Array,
     required: false,
     default: null
   },
-  returnObject: {
-    type: Boolean,
-    default: false
-  },
-  multiple: {
-    type: Boolean,
-    default: false
-  },
-  isLoading: {
-    type: Boolean,
-    default: false
-  },
+  // TODO: c'est quoi?
   noFilter: {
     type: Boolean,
     default: false
   },
-  prependIcon: {
-    type: String
-  },
+  // TODO: c'est quoi?
   translate: {
     type: Boolean,
     default: false
   },
+  /**
+   * Règles de validation
+   * @see https://vuetify.cn/en/components/forms/#validation-with-submit-clear
+   */
   rules: {
     type: Array,
     required: false,
     default: () => []
   },
-  chips: {
-    type: Boolean,
-    default: false
-  },
+  /**
+   * Le champ est-il actuellement en état d'erreur
+   */
   error: {
     type: Boolean,
     required: false
   },
+  /**
+   * Si le champ est en état d'erreur, quel est le message d'erreur?
+   */
   errorMessage: {
     type: String,
     required: false,
@@ -127,50 +191,59 @@ const props = defineProps({
   }
 })
 
-const { emit } = useNuxtApp()
-
-const { i18n } = useNuxtApp()
+const i18n = useI18n()
 
 const search: Ref<string|null> = ref(null)
 
-const fieldLabel = props.label ?? props.field
+const fieldLabel: string = props.label ?? props.field
 
-const { violation, onChange } = useFieldViolation(props.field, emit)
+const {fieldViolations, updateViolationState} = useFieldViolation(props.field)
 
-// On reconstruit les items à afficher...
-const itemsToDisplayed: ComputedRef<Array<AnyJson>> = computed(() => {
-  const itemsByGroup:Array<Array<string>> = classItemsByGroup(props.items)
-  return prepareItemsToDisplayed(itemsByGroup)
-})
+const emit = defineEmits(['update:model-value'])
 
-const unwatch = watch(
-    search,
-    useDebounce(async (newResearch, oldResearch) => {
-      if(newResearch !== oldResearch && oldResearch !== null)
-        emit('research', newResearch)
-    }, 500)
-)
+const onUpdate = (event: string) => {
+  updateViolationState(event)
+  emit('update:model-value', props.modelValue)
+}
 
-onUnmounted(() => {
-  unwatch()
+/**
+ * Items à afficher
+ * TODO: à revoir
+ */
+const items: ComputedRef<Array<AnyJson>> = computed(() => {
+  let _items: Array<any> = props.items
+  return _items
+  // if (props.group !== null) {
+  //   _items = groupItems(props.items)
+  // }
+  //
+  // return prepareGroups(_items)
 })
 
 /**
- * On construit l'Array à double entrée contenant les groups (headers) et les propositions
+ * On construit l'Array à double entrée contenant les groups (headers) et les items
+ * TODO: à revoir
  *
  * @param items
  */
-const classItemsByGroup = (items:Array<any>): Array<Array<string>> => {
-  const group = props.group as string
+const groupItems = (items: Array<any>): Array<Array<string>> => {
+  const group = props.group as string | null
+  if (group === null) {
+    return items
+  }
+
   const itemsByGroup: Array<Array<string>> = []
+  let groupValue = null
 
   for (const item of items) {
     if (item) {
-      if (!itemsByGroup[item[group]]) {
-        itemsByGroup[item[group]] = []
+      groupValue = item[group]
+
+      if (!itemsByGroup[groupValue]) {
+        itemsByGroup[groupValue] = []
       }
 
-      itemsByGroup[item[group]].push(item)
+      itemsByGroup[groupValue].push(item)
     }
   }
 
@@ -179,13 +252,14 @@ const classItemsByGroup = (items:Array<any>): Array<Array<string>> => {
 
 /**
  * Construction de l'Array JSON contenant toutes les propositions à afficher dans le select
+ * TODO: à revoir
  *
- * @param itemsByGroup
+ * @param groupedItems
  */
-const prepareItemsToDisplayed = (itemsByGroup: Array<Array<string>>): Array<AnyJson> => {
+const prepareGroups = (groupedItems: Array<Array<string>>): Array<AnyJson> => {
   let finalItems: Array<AnyJson> = []
 
-  for (const group in itemsByGroup) {
+  for (const group in groupedItems) {
 
     // Si un groupe est présent, alors on créé le groupe options header
     if (group !== 'undefined') {
@@ -193,27 +267,38 @@ const prepareItemsToDisplayed = (itemsByGroup: Array<Array<string>>): Array<AnyJ
     }
 
     // On parcourt les items pour préparer les texts / slotTexts à afficher
-    finalItems = finalItems.concat(itemsByGroup[group].map((item: any) => {
-      const slotTextDisplay: Array<string> = []
-      const itemTextDisplay: Array<string> = []
+    finalItems = finalItems.concat(groupedItems[group].map((item: any) => {
+      return prepareItem(item)
+    }))
+  }
+  return finalItems
+}
 
-      item = ObjectUtils.cloneAndFlatten(item)
+/**
+ * Construction d'un item
+ * TODO: à revoir
+ *
+ * @param item
+ */
+const prepareItem = (item: Object): AnyJson => {
+  const slotTextDisplay: Array<string> = []
+  const itemTextDisplay: Array<string> = []
 
-      // Si on souhaite avoir un texte différent dans les propositions que dans la sélection finale de select
-      if (props.slotText) {
-        for (const text of props.slotText) {
-          slotTextDisplay.push(props.translate ? i18n.t(item[text as string]) : item[text as string])
-        }
-      }
+  item = ObjectUtils.cloneAndFlatten(item)
 
-      for (const text of props.itemText) {
-        itemTextDisplay.push(props.translate ? i18n.t(item[text as string]) : item[text as string])
-      }
+  // Si on souhaite avoir un texte différent dans les propositions que dans la sélection finale de select
+  if (props.slotText) {
+    for (const text of props.slotText) {
+      slotTextDisplay.push(props.translate ? i18n.t(item[text as string]) : item[text as string])
+    }
+  }
 
-      // On reconstruit l'objet
-      return Object.assign({}, item, { itemTextDisplay: itemTextDisplay.join(' '), slotTextDisplay: slotTextDisplay.join(' ') })
-    }))
+  for (const text of props.itemTitle) {
+    itemTextDisplay.push(props.translate ? i18n.t(item[text as string]) : item[text as string])
   }
-  return finalItems
+
+  // On reconstruit l'objet
+  return Object.assign({}, item, { itemTextDisplay: itemTextDisplay.join(' '), slotTextDisplay: slotTextDisplay.join(' ') })
 }
+
 </script>

+ 1 - 1
components/Ui/Input/AutocompleteWithAPI.vue

@@ -72,7 +72,7 @@ const props = defineProps({
     type: String,
     default: 'id'
   },
-  itemText: {
+  itemTitle: {
     type: Array,
     required: true
   },

+ 33 - 8
components/Ui/Input/Checkbox.vue

@@ -1,5 +1,5 @@
 <!--
-Case à cocher
+Case à cocher, à placer dans un composant `UiForm`
 
 @see https://vuetifyjs.com/en/components/checkboxes/
 -->
@@ -17,7 +17,6 @@ Case à cocher
       :error="error || !!fieldViolations"
       :error-messages="errorMessage || fieldViolations ? $t(fieldViolations) : ''"
       @update:model-value="onUpdate($event)"
-      @change="onChange($event)"
     />
   </v-container>
 </template>
@@ -26,28 +25,58 @@ Case à cocher
 import {useFieldViolation} from "~/composables/form/useFieldViolation";
 
 const props = defineProps({
+  /**
+   * v-model
+   */
   modelValue: {
     type: Boolean,
     required: false
   },
+  /**
+   * Nom de la propriété d'une entité lorsque l'input concerne cette propriété
+   * - Utilisé par la validation
+   * - Laisser null si le champ ne s'applique pas à une entité
+   */
   field: {
     type: String,
     required: false,
     default: null
   },
+  /**
+   * Label du champ
+   * Si non défini, c'est le nom de propriété qui est utilisé
+   */
   label: {
     type: String,
     required: false,
     default: null
   },
+  /**
+   * Définit si le champ est en lecture seule
+   */
   readonly: {
     type: Boolean,
     required: false
   },
+  /**
+   * Règles de validation
+   * @see https://vuetify.cn/en/components/forms/#validation-with-submit-clear
+   */
+  rules: {
+    type: Array,
+    required: false,
+    default: () => []
+  },
+  /**
+   * Le champ est-il actuellement en état d'erreur
+   */
   error: {
     type: Boolean,
     required: false
   },
+  /**
+   * Si le champ est en état d'erreur, quel est le message d'erreur?
+   */
   errorMessage: {
     type: String,
     required: false,
@@ -59,15 +88,11 @@ const {fieldViolations, updateViolationState} = useFieldViolation(props.field)
 
 const fieldLabel: string = props.label ?? props.field
 
-const emit = defineEmits(['update:model-value', 'change'])
+const emit = defineEmits(['update:model-value'])
 
 const onUpdate = (event: string) => {
-  emit('update:model-value', props.modelValue)
-}
-
-const onChange = (event: Event | undefined) => {
   updateViolationState(event)
-  emit('change', props.modelValue)
+  emit('update:model-value', props.modelValue)
 }
 
 </script>

+ 109 - 0
components/Ui/Input/Combobox.vue

@@ -0,0 +1,109 @@
+<!--
+Liste déroulante, à placer dans un composant `UiForm`
+
+@see https://vuetifyjs.com/en/api/v-combobox/
+-->
+
+<template>
+  <v-container
+    class="px-0"
+    fluid
+  >
+    <v-combobox
+      :model-value="modelValue"
+      :value="modelValue"
+      :label="$t(fieldLabel)"
+      :items="items"
+      :disabled="readonly"
+      :error="error || !!fieldViolations"
+      :error-messages="errorMessage || fieldViolations ? $t(fieldViolations) : ''"
+      @update:model-value="onUpdate($event)"
+    />
+  </v-container>
+</template>
+
+<script setup lang="ts">
+import {useFieldViolation} from "~/composables/form/useFieldViolation";
+
+const props = defineProps({
+  /**
+   * v-model
+   */
+  modelValue: {
+    type: Boolean,
+    required: false
+  },
+  /**
+   * Nom de la propriété d'une entité lorsque l'input concerne cette propriété
+   * - Utilisé par la validation
+   * - Laisser null si le champ ne s'applique pas à une entité
+   */
+  field: {
+    type: String,
+    required: false,
+    default: null
+  },
+  /**
+   * Label du champ
+   * Si non défini, c'est le nom de propriété qui est utilisé
+   */
+  label: {
+    type: String,
+    required: false,
+    default: null
+  },
+  /**
+   * Liste des éléments de la liste
+   */
+  items: {
+    type: Array,
+    required: true
+  },
+  /**
+   * Définit si le champ est en lecture seule
+   */
+  readonly: {
+    type: Boolean,
+    required: false
+  },
+  /**
+   * Règles de validation
+   * @see https://vuetify.cn/en/components/forms/#validation-with-submit-clear
+   */
+  rules: {
+    type: Array,
+    required: false,
+    default: () => []
+  },
+  /**
+   * Le champ est-il actuellement en état d'erreur
+   */
+  error: {
+    type: Boolean,
+    required: false
+  },
+  /**
+   * Si le champ est en état d'erreur, quel est le message d'erreur?
+   */
+  errorMessage: {
+    type: String,
+    required: false,
+    default: null
+  }
+})
+
+const {fieldViolations, updateViolationState} = useFieldViolation(props.field)
+
+const fieldLabel: string = props.label ?? props.field
+
+const emit = defineEmits(['update:model-value'])
+
+const onUpdate = (event: string) => {
+  updateViolationState(event)
+  emit('update:model-value', props.modelValue)
+}
+
+</script>
+
+<style scoped>
+</style>

+ 33 - 7
components/Ui/Input/DatePicker.vue

@@ -1,5 +1,5 @@
 <!--
-Sélecteur de dates
+Sélecteur de dates, à placer dans un composant `UiForm`
 -->
 
 <template>
@@ -12,7 +12,6 @@ Sélecteur de dates
           :readonly="readonly"
           :format="format"
           @update:model-value="onUpdate($event)"
-          @change="onChange($event)"
       />
 
       <span v-if="error || !!fieldViolations" class="theme-danger">
@@ -35,29 +34,60 @@ const props = defineProps({
     required: false,
     default: null
   },
+  /**
+   * Nom de la propriété d'une entité lorsque l'input concerne cette propriété
+   * - Utilisé par la validation
+   * - Laisser null si le champ ne s'applique pas à une entité
+   */
   field: {
     type: String,
     required: false,
     default: null
   },
+  /**
+   * Label du champ
+   * Si non défini, c'est le nom de propriété qui est utilisé
+   */
   label: {
     type: String,
     required: false,
     default: null
   },
+  /**
+   * Définit si le champ est en lecture seule
+   */
   readonly: {
     type: Boolean,
     required: false
   },
+  /**
+   * Format d'affichage des dates
+   * @see https://vue3datepicker.com/props/formatting/
+   */
   format: {
     type: String,
     required: false,
     default: null
   },
+  /**
+   * Règles de validation
+   * @see https://vuetify.cn/en/components/forms/#validation-with-submit-clear
+   */
+  rules: {
+    type: Array,
+    required: false,
+    default: () => []
+  },
+  /**
+   * Le champ est-il actuellement en état d'erreur
+   */
   error: {
     type: Boolean,
     required: false
   },
+  /**
+   * Si le champ est en état d'erreur, quel est le message d'erreur?
+   */
   errorMessage: {
     type: String,
     required: false,
@@ -77,12 +107,8 @@ const date: Ref<Date> = ref(new Date(props.modelValue))
 console.log(date.value)
 
 const onUpdate = (event: string) => {
-  emit('update:model-value', formatISO(date.value))
-}
-
-const onChange = (event: Event | undefined) => {
   updateViolationState(event)
-  emit('change', formatISO(date.value))
+  emit('update:model-value', formatISO(date.value))
 }
 </script>
 

+ 1 - 1
components/Ui/Input/Text.vue

@@ -1,5 +1,5 @@
 <!--
-Champs de saisie de texte
+Champs de saisie de texte, à placer dans un composant `UiForm`
 
 @see https://vuetifyjs.com/en/components/text-fields/
 -->