@mythpe/quasar-ui-qui 0.4.29 → 0.4.31

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mythpe/quasar-ui-qui",
3
- "version": "0.4.29",
3
+ "version": "0.4.31",
4
4
  "description": "MyTh Quasar UI Kit App Extension",
5
5
  "author": {
6
6
  "name": "MyTh Ahmed Faiz",
@@ -10,7 +10,7 @@
10
10
  lang="ts"
11
11
  setup
12
12
  >
13
- import type { ApiInterface, GenericMDtBtn, MDatatableProps } from '../../types'
13
+ import type { ApiInterface, GenericMDtBtn, MDatatableProps, MDtExportOptions } from '../../types'
14
14
  import type { InvalidSubmissionHandler, SubmissionHandler } from 'vee-validate'
15
15
  import { useForm } from 'vee-validate'
16
16
  import { computed, nextTick, onMounted, ref, toValue, useAttrs, watch } from 'vue'
@@ -286,6 +286,7 @@ const {
286
286
  getRequestWith,
287
287
  getDatatableParams,
288
288
  fetchDatatableItems,
289
+ hasExport,
289
290
  exportData,
290
291
  openDialogTimeout,
291
292
  beforeCloseFilterDialog,
@@ -436,9 +437,11 @@ const onSuccess: SubmissionHandler = async (form) => {
436
437
  }
437
438
  const onInvalidSubmit: InvalidSubmissionHandler = ({ errors }) => {
438
439
  const keys: (keyof typeof errors)[] = Object.keys(errors)
439
- if (keys[0]) {
440
- const message = errors[keys[0]] as string || __('messages.the_given_data_was_invalid')
441
- scrollToElementFromErrors({ [keys[0]]: [message] }, undefined, '.m-datatable__dialog-form-container')
440
+ // if (keys[0]) {
441
+ // const message = errors[keys[0]] as string || __('messages.the_given_data_was_invalid')
442
+ // scrollToElementFromErrors({ [keys[0]]: [message] }, undefined, '.m-datatable__dialog-form-container')
443
+ if (keys.length > 0) {
444
+ scrollToElementFromErrors(errors, undefined, '.m-datatable__dialog-form-container')
442
445
  }
443
446
  }
444
447
  const defaultSubmitItem = handleSubmit.withControlled(onSuccess, onInvalidSubmit)
@@ -456,6 +459,12 @@ const onSearchEnter = () => {
456
459
  }
457
460
  refreshNoUpdate()
458
461
  }
462
+ const onSearchByUpdate = () => {
463
+ if (!search.value) {
464
+ return
465
+ }
466
+ onSearchEnter()
467
+ }
459
468
  watch(loading, v => {
460
469
  if (pluginOptions.value?.dt?.useQuasarLoading) {
461
470
  if (v) {
@@ -541,6 +550,7 @@ defineExpose({
541
550
  getRequestWith,
542
551
  getDatatableParams,
543
552
  fetchDatatableItems,
553
+ hasExport,
544
554
  exportData,
545
555
  openFilterDialog,
546
556
  saveFilterDialog,
@@ -779,548 +789,552 @@ defineExpose({
779
789
  </template>
780
790
  <template #top="topSlotProps">
781
791
  <MCol col="12">
782
- <!--Top Slot-->
783
- <slot
784
- :dt="datatableItemsScope"
785
- :form="useFormContext"
786
- :index="dialogItemIndex"
787
- :item="dialogItem"
788
- name="top"
789
- v-bind="topSlotProps"
790
- />
791
- <!--Title and Search-->
792
- <MRow :class="{hidden: !hasAddBtn && !getTableTitle && hideTitle && hideSearch}">
793
- <!--Default Title Slot-->
792
+ <div :class="{'q-pa-sm': isSmall}">
793
+ <!--Top Slot-->
794
794
  <slot
795
795
  :dt="datatableItemsScope"
796
796
  :form="useFormContext"
797
797
  :index="dialogItemIndex"
798
798
  :item="dialogItem"
799
- name="title"
800
- >
801
- <MCol col="12">
802
- <div class="q-mb-sm">
803
- <MRow class="justify-between">
804
- <!--Back Button-->
805
- <MCol
806
- v-if="!noBackBtn && !hideTitle"
807
- auto
808
- >
809
- <q-btn
810
- :icon="backIcon || `ion-ios-arrow-${$q.lang.rtl ? 'forward' : 'back'}`"
811
- class="m-datatable__table-back-btn q-mr-sm"
812
- flat
813
- @click="$router.back()"
814
- />
815
- </MCol>
816
-
817
- <!--Table Title-->
818
- <MCol col>
819
- <div
820
- v-if="!!getTableTitle && !hideTitle"
821
- :class="['text-h5 ellipsis m-datatable__table-title',titleClass]"
822
- >
823
- <span>{{ getTableTitle }}</span>
824
- <span v-if="!!help">
825
- <q-icon
826
- class="cursor-pointer"
827
- name="ion-ios-information-circle-outline"
828
- right
829
- size="18px"
830
- >
831
- <q-tooltip>
832
- <div class="text-body1">
833
- {{ __(help) }}
834
- </div>
835
- </q-tooltip>
836
- </q-icon>
837
- </span>
838
- </div>
839
- <slot
840
- :dt="datatableItemsScope"
841
- :form="useFormContext"
842
- :index="dialogItemIndex"
843
- :item="dialogItem"
844
- name="subtitle"
799
+ name="top"
800
+ v-bind="topSlotProps"
801
+ />
802
+ <!--Title and Search-->
803
+ <MRow :class="{hidden: !hasAddBtn && !getTableTitle && hideTitle && hideSearch}">
804
+ <!--Default Title Slot-->
805
+ <slot
806
+ :dt="datatableItemsScope"
807
+ :form="useFormContext"
808
+ :index="dialogItemIndex"
809
+ :item="dialogItem"
810
+ name="title"
811
+ >
812
+ <MCol col="12">
813
+ <div class="q-mb-sm">
814
+ <MRow class="justify-between">
815
+ <!--Back Button-->
816
+ <MCol
817
+ v-if="!noBackBtn && !hideTitle"
818
+ auto
845
819
  >
820
+ <q-btn
821
+ :icon="backIcon || `ion-ios-arrow-${$q.lang.rtl ? 'forward' : 'back'}`"
822
+ class="m-datatable__table-back-btn q-mr-sm"
823
+ flat
824
+ @click="$router.back()"
825
+ />
826
+ </MCol>
827
+
828
+ <!--Table Title-->
829
+ <MCol col>
846
830
  <div
847
- v-if="!!subtitle"
848
- :class="['m-datatable__table-subtitle',subtitleClass]"
831
+ v-if="!!getTableTitle && !hideTitle"
832
+ :class="['text-h5 ellipsis m-datatable__table-title',titleClass]"
849
833
  >
850
- {{ __(subtitle) }}
834
+ <span>{{ getTableTitle }}</span>
835
+ <span v-if="!!help">
836
+ <q-icon
837
+ class="cursor-pointer"
838
+ name="ion-ios-information-circle-outline"
839
+ right
840
+ size="18px"
841
+ >
842
+ <q-tooltip>
843
+ <div class="text-body1">
844
+ {{ __(help) }}
845
+ </div>
846
+ </q-tooltip>
847
+ </q-icon>
848
+ </span>
851
849
  </div>
852
- </slot>
853
- </MCol>
850
+ <slot
851
+ :dt="datatableItemsScope"
852
+ :form="useFormContext"
853
+ :index="dialogItemIndex"
854
+ :item="dialogItem"
855
+ name="subtitle"
856
+ >
857
+ <div
858
+ v-if="!!subtitle"
859
+ :class="['m-datatable__table-subtitle',subtitleClass]"
860
+ >
861
+ {{ __(subtitle) }}
862
+ </div>
863
+ </slot>
864
+ </MCol>
854
865
 
855
- <div class="flex-break xs" />
866
+ <div class="flex-break xs" />
856
867
 
857
- <!--Import Btn-->
858
- <MCol
859
- v-if="!!importBtn"
860
- auto
861
- >
862
- <MDtBtn
863
- color="green"
864
- icon="far fa-file-excel"
865
- label="labels.import"
866
- no-caps
867
- text-color="white"
868
- @click="onOpenImportDialog()"
869
- />
870
- </MCol>
868
+ <!--Import Btn-->
869
+ <MCol
870
+ v-if="!!importBtn"
871
+ auto
872
+ >
873
+ <MDtBtn
874
+ color="green"
875
+ icon="far fa-file-excel"
876
+ label="labels.import"
877
+ no-caps
878
+ text-color="white"
879
+ @click="onOpenImportDialog()"
880
+ />
881
+ </MCol>
871
882
 
872
- <!-- Add Btn -->
873
- <MCol
874
- v-if="hasAddBtn && (addTopBtn===undefined?(pluginOptions.datatable?.addTopBtn===undefined?!0:pluginOptions.datatable?.addTopBtn):addTopBtn)"
875
- auto
876
- >
877
- <MBtn
878
- :label="getFormTitle"
879
- no-caps
880
- @click="openCreateDialog()"
881
- />
882
- </MCol>
883
- <q-space class="xs" />
884
- </MRow>
885
- </div>
883
+ <!-- Add Btn -->
884
+ <MCol
885
+ v-if="hasAddBtn && (addTopBtn===undefined?(pluginOptions.datatable?.addTopBtn===undefined?!0:pluginOptions.datatable?.addTopBtn):addTopBtn)"
886
+ auto
887
+ >
888
+ <MBtn
889
+ :label="getFormTitle"
890
+ no-caps
891
+ @click="openCreateDialog()"
892
+ />
893
+ </MCol>
894
+ <q-space class="xs" />
895
+ </MRow>
896
+ </div>
897
+ </MCol>
898
+ </slot>
899
+ <slot
900
+ :dt="datatableItemsScope"
901
+ :form="useFormContext"
902
+ :index="dialogItemIndex"
903
+ :item="dialogItem"
904
+ name="top-search"
905
+ />
906
+ <MCol
907
+ v-if="!hideSearch"
908
+ col="12"
909
+ >
910
+ <MRow col="xs">
911
+ <MSelect
912
+ v-model="searchBy"
913
+ :dense="dense === undefined ? (pluginOptions.datatable?.dense !== undefined ? pluginOptions.datatable?.dense : !0) : dense"
914
+ :emit-value="false"
915
+ :field-options="{ controlled: !1 }"
916
+ :options="searchByColumns"
917
+ :use-input="!1"
918
+ autocomplete="none"
919
+ col="3"
920
+ hide-dropdown-icon
921
+ label="replace.search_by"
922
+ md="3"
923
+ name="search_by"
924
+ v-bind="{...theme.inputs as any, ...pluginOptions.dt?.searchInput?.props as any}"
925
+ @update:model-value="onSearchByUpdate()"
926
+ />
927
+ <MInput
928
+ :debounce="searchDebounce"
929
+ :dense="dense === undefined ? (pluginOptions.datatable?.dense !== undefined ? pluginOptions.datatable?.dense : !0) : dense"
930
+ :field-options="{ controlled: !1 }"
931
+ :model-value="search"
932
+ :placeholder="searchPlaceholder"
933
+ autocomplete="none"
934
+ col
935
+ label="labels.search"
936
+ name="search"
937
+ outlined
938
+ v-bind="{...theme.inputs as any, ...pluginOptions.dt?.searchInput?.props as any}"
939
+ @update:model-value="onSearchInput($event)"
940
+ @keyup.enter="onSearchEnter()"
941
+ >
942
+ <template #prepend>
943
+ <q-icon
944
+ v-if="!search"
945
+ name="ion-ios-search"
946
+ >
947
+ <q-tooltip class="m-dt-btn-tooltip">
948
+ {{ __('myth.datatable.searchInput') }}
949
+ </q-tooltip>
950
+ </q-icon>
951
+ <q-icon
952
+ v-else
953
+ class="cursor-pointer"
954
+ name="ion-ios-close"
955
+ @click="search = ''"
956
+ >
957
+ <q-tooltip class="m-dt-btn-tooltip">
958
+ {{ __('myth.datatable.searchInputClear') }}
959
+ </q-tooltip>
960
+ </q-icon>
961
+ </template>
962
+ </MInput>
963
+ </MRow>
886
964
  </MCol>
887
- </slot>
888
- <slot
889
- :dt="datatableItemsScope"
890
- :form="useFormContext"
891
- :index="dialogItemIndex"
892
- :item="dialogItem"
893
- name="top-search"
894
- />
895
- <MCol
896
- v-if="!hideSearch"
897
- col="12"
965
+ <slot
966
+ :dt="datatableItemsScope"
967
+ :form="useFormContext"
968
+ :index="dialogItemIndex"
969
+ :item="dialogItem"
970
+ name="bottom-search"
971
+ />
972
+ </MRow>
973
+ <!--Buttons-->
974
+ <MRow
975
+ class="items-center justify-between m-datatable__table-top-buttons"
976
+ gutter="xs"
977
+ style="margin-top: 5px;"
898
978
  >
899
- <MRow col="xs">
900
- <MSelect
901
- v-model="searchBy"
902
- :dense="dense === undefined ? (pluginOptions.datatable?.dense !== undefined ? pluginOptions.datatable?.dense : !0) : dense"
903
- :emit-value="false"
904
- :field-options="{ controlled: !1 }"
905
- :options="searchByColumns"
906
- :use-input="!1"
907
- autocomplete="none"
908
- col="3"
909
- hide-dropdown-icon
910
- label="replace.search_by"
911
- md="3"
912
- name="search_by"
913
- v-bind="{...theme.inputs as any, ...pluginOptions.dt?.searchInput?.props as any}"
914
- @update:model-value="onSearchEnter()"
915
- />
916
- <MInput
917
- :debounce="searchDebounce"
918
- :dense="dense === undefined ? (pluginOptions.datatable?.dense !== undefined ? pluginOptions.datatable?.dense : !0) : dense"
919
- :field-options="{ controlled: !1 }"
920
- :model-value="search"
921
- :placeholder="searchPlaceholder"
922
- autocomplete="none"
923
- col
924
- label="labels.search"
925
- name="search"
926
- outlined
927
- v-bind="{...theme.inputs as any, ...pluginOptions.dt?.searchInput?.props as any}"
928
- @update:model-value="onSearchInput($event)"
929
- @keyup.enter="onSearchEnter()"
930
- >
931
- <template #prepend>
932
- <q-icon
933
- v-if="!search"
934
- name="ion-ios-search"
935
- >
936
- <q-tooltip class="m-dt-btn-tooltip">
937
- {{ __('myth.datatable.searchInput') }}
938
- </q-tooltip>
939
- </q-icon>
940
- <q-icon
941
- v-else
942
- class="cursor-pointer"
943
- name="ion-ios-close"
944
- @click="search = ''"
945
- >
946
- <q-tooltip class="m-dt-btn-tooltip">
947
- {{ __('myth.datatable.searchInputClear') }}
948
- </q-tooltip>
949
- </q-icon>
950
- </template>
951
- </MInput>
952
- </MRow>
953
- </MCol>
954
- <slot
955
- :dt="datatableItemsScope"
956
- :form="useFormContext"
957
- :index="dialogItemIndex"
958
- :item="dialogItem"
959
- name="bottom-search"
960
- />
961
- </MRow>
962
- <!--Buttons-->
963
- <MRow
964
- class="items-center justify-between m-datatable__table-top-buttons"
965
- gutter="xs"
966
- style="margin-top: 5px;"
967
- >
968
- <MCol auto>
969
- <MRow
970
- class="items-center"
971
- gutter="xs"
972
- >
973
- <!-- Export PDF-->
974
- <MDtBtn
975
- v-if="pdf && getRows.length > 0"
976
- :disable="loading"
977
- icon="fa-solid fa-file-pdf"
978
- v-bind="{...defaultTopBtnProps,...pluginOptions.dt?.buttons?.filter as any, textColor: 'red'}"
979
- @click="exportData('pdf')"
980
- >
981
- <q-tooltip class="m-dt-btn-tooltip">
982
- {{ __('myth.titles.exportPdf') }}
983
- </q-tooltip>
984
- </MDtBtn>
985
- <!-- Export Excel-->
986
- <MDtBtn
987
- v-if="excel && getRows.length > 0"
988
- :disable="loading"
989
- icon="fa-solid fa-file-excel"
990
- v-bind="{...defaultTopBtnProps,...pluginOptions.dt?.buttons?.filter as any, textColor: 'green'}"
991
- @click="exportData('excel')"
992
- >
993
- <q-tooltip class="m-dt-btn-tooltip">
994
- {{ __('myth.titles.exportExcel') }}
995
- </q-tooltip>
996
- </MDtBtn>
997
- <!-- Filter dialog -->
998
- <MDtBtn
999
- v-if="hasFilterDialog"
1000
- key="filter-selection-btn"
1001
- :disable="loading"
1002
- color="primary"
1003
- icon="ion-ios-options"
1004
- label="myth.datatable.hints.filter"
1005
- text-color="on-primary"
1006
- v-bind="{...pluginOptions.dt?.buttons?.filter as any}"
979
+ <MCol auto>
980
+ <MRow
981
+ class="items-center"
982
+ gutter="xs"
1007
983
  >
1008
- <MModalMenu
1009
- v-model="filterDialogModel"
1010
- :offset="[10,10]"
1011
- no-close-btn
1012
- persistent
1013
- position="top"
1014
- v-bind="pluginOptions.dt?.filterDialogProps as any"
1015
- @before-show="beforeCloseFilterDialog"
984
+ <!-- Filter dialog -->
985
+ <MDtBtn
986
+ v-if="hasFilterDialog"
987
+ key="filter-selection-btn"
988
+ :disable="loading"
989
+ color="primary"
990
+ icon="ion-ios-options"
991
+ label="myth.datatable.hints.filter"
992
+ text-color="on-primary"
993
+ v-bind="{...pluginOptions.dt?.buttons?.filter as any}"
1016
994
  >
1017
- <q-card
1018
- :style="`max-width: 600px; width: ${$q.screen.width}px`"
1019
- flat
1020
- square
995
+ <MModalMenu
996
+ v-model="filterDialogModel"
997
+ :offset="[10,10]"
998
+ no-close-btn
999
+ persistent
1000
+ position="top"
1001
+ v-bind="pluginOptions.dt?.filterDialogProps as any"
1002
+ @before-show="beforeCloseFilterDialog"
1021
1003
  >
1022
- <MContainer class="q-pa-lg">
1023
- <q-toolbar :class="{'q-pa-none': isSmall}">
1024
- <q-toolbar-title>
1025
- {{ __('myth.datatable.filter.title') }}
1026
- </q-toolbar-title>
1027
- </q-toolbar>
1028
- <q-separator />
1029
- <MRow class="items-center">
1030
- <MCol col="12">
1031
- <MContainer>
1032
- <slot
1033
- :dt="datatableItemsScope"
1034
- :filter="tempFilterForm"
1035
- :form="useFormContext"
1036
- :index="dialogItemIndex"
1037
- :item="dialogItem"
1038
- name="filter"
1039
- />
1040
- </MContainer>
1041
- </MCol>
1042
- <MCol col="12">
1043
- <MRow class="justify-between">
1044
- <MBtn
1045
- v-close-popup
1046
- :label="__('myth.datatable.filter.save')"
1047
- style="min-width: 110px"
1048
- v-bind="pluginOptions.dt?.dialogButtonsProps as any"
1049
- @click="saveFilterDialog"
1050
- />
1051
- <MBtn
1052
- v-close-popup
1053
- :label="__('myth.datatable.filter.cancel')"
1054
- style="min-width: 100px"
1055
- v-bind="pluginOptions.dt?.dialogButtonsProps as any"
1056
- @click="closeFilterDialog"
1057
- />
1058
- </MRow>
1059
- </MCol>
1060
- </MRow>
1061
- </MContainer>
1062
- </q-card>
1063
- </MModalMenu>
1064
- </MDtBtn>
1065
- <!--Refresh-->
1066
- <MDtBtn
1067
- v-if="!noRefreshBtn"
1068
- key="refresh-selection-btn"
1069
- :disable="loading"
1070
- icon="ion-ios-refresh"
1071
- tooltip="myth.datatable.hints.refresh"
1072
- v-bind="{...defaultTopBtnProps,...pluginOptions.dt?.buttons?.refresh as any}"
1073
- @click="refreshNoUpdate()"
1074
- />
1075
- <!--Fullscreen-->
1076
- <MDtBtn
1077
- v-if="fullscreenBtn === undefined ? ( !!pluginOptions.datatable?.fullscreenBtn) : fullscreenBtn"
1078
- key="fullscreen-selection-btn"
1079
- :disable="loading"
1080
- :icon="fullscreen ? 'ion-ios-contract' : 'ion-ios-desktop'"
1081
- :tooltip="`myth.datatable.${fullscreen ? 'exitFullscreen' : 'fullscreen'}`"
1082
- v-bind="{...defaultTopBtnProps,...pluginOptions.dt?.buttons?.fullscreen as any}"
1083
- @click="fullscreen = !fullscreen"
1084
- />
1085
- <template v-if="hasSelectedItem">
1004
+ <q-card
1005
+ :style="`max-width: 600px; width: ${$q.screen.width}px`"
1006
+ flat
1007
+ square
1008
+ >
1009
+ <MContainer class="q-pa-lg">
1010
+ <q-toolbar :class="{'q-pa-none': isSmall}">
1011
+ <q-toolbar-title>
1012
+ {{ __('myth.datatable.filter.title') }}
1013
+ </q-toolbar-title>
1014
+ </q-toolbar>
1015
+ <q-separator />
1016
+ <MRow class="items-center">
1017
+ <MCol col="12">
1018
+ <MContainer>
1019
+ <slot
1020
+ :dt="datatableItemsScope"
1021
+ :filter="tempFilterForm"
1022
+ :form="useFormContext"
1023
+ :index="dialogItemIndex"
1024
+ :item="dialogItem"
1025
+ name="filter"
1026
+ />
1027
+ </MContainer>
1028
+ </MCol>
1029
+ <MCol col="12">
1030
+ <MRow class="justify-between">
1031
+ <MBtn
1032
+ v-close-popup
1033
+ :label="__('myth.datatable.filter.save')"
1034
+ style="min-width: 110px"
1035
+ v-bind="pluginOptions.dt?.dialogButtonsProps as any"
1036
+ @click="saveFilterDialog"
1037
+ />
1038
+ <MBtn
1039
+ v-close-popup
1040
+ :label="__('myth.datatable.filter.cancel')"
1041
+ style="min-width: 100px"
1042
+ v-bind="pluginOptions.dt?.dialogButtonsProps as any"
1043
+ @click="closeFilterDialog"
1044
+ />
1045
+ </MRow>
1046
+ </MCol>
1047
+ </MRow>
1048
+ </MContainer>
1049
+ </q-card>
1050
+ </MModalMenu>
1051
+ </MDtBtn>
1052
+ <!-- Export Buttons-->
1086
1053
  <MDtBtn
1087
- v-if="hasUpdateBtn && isSingleSelectedItem"
1088
- key="update-dt-selection-btn"
1089
- :disable="loading"
1090
- icon="ion-ios-create"
1091
- update
1092
- v-bind="{...defaultTopBtnProps,...pluginOptions.dt?.topSelection?.btn as any}"
1093
- @click="openUpdateDialogNoIndex(selected[0] as any)"
1094
- />
1054
+ v-if="(pdf || excel)"
1055
+ :disable="loading || getRows.length < 1"
1056
+ icon="ion-ios-cloud-download"
1057
+ label="labels.export"
1058
+ v-bind="{...defaultTopBtnProps,...pluginOptions.dt?.buttons?.filter as any, color: undefined, textColor: undefined,dense: false}"
1059
+ >
1060
+ <q-menu
1061
+ :offset="[0,10]"
1062
+ fit
1063
+ >
1064
+ <q-list>
1065
+ <template
1066
+ v-for="(s,sdx) in ['pdf','excel'] as MDtExportOptions[]"
1067
+ :key="`dt-export-${s}-${sdx}`"
1068
+ >
1069
+ <q-item
1070
+ v-if="hasExport(s)"
1071
+ v-close-popup
1072
+ clickable
1073
+ @click="exportData(s)"
1074
+ >
1075
+ <q-item-section>
1076
+ <q-item-label>{{ __(s) }}</q-item-label>
1077
+ </q-item-section>
1078
+ </q-item>
1079
+ </template>
1080
+ </q-list>
1081
+ </q-menu>
1082
+ </MDtBtn>
1083
+ <!--Refresh-->
1095
1084
  <MDtBtn
1096
- v-if="hasCloneBtn && isSingleSelectedItem"
1097
- key="clone-dt-selection-btn"
1085
+ v-if="!noRefreshBtn"
1086
+ key="refresh-selection-btn"
1098
1087
  :disable="loading"
1099
- clone
1100
- tooltip="labels.clone"
1101
- v-bind="{...defaultTopBtnProps,...pluginOptions.dt?.topSelection?.btn as any}"
1102
- @click="onCloneItem(selected[0] as any)"
1088
+ icon="ion-ios-refresh"
1089
+ tooltip="myth.datatable.hints.refresh"
1090
+ v-bind="{...defaultTopBtnProps,...pluginOptions.dt?.buttons?.refresh as any}"
1091
+ @click="refreshNoUpdate()"
1103
1092
  />
1093
+ <!--Fullscreen-->
1104
1094
  <MDtBtn
1105
- v-if="hasShowBtn && isSingleSelectedItem"
1106
- key="show-dt-selection-btn"
1095
+ v-if="fullscreenBtn === undefined ? ( !!pluginOptions.datatable?.fullscreenBtn) : fullscreenBtn"
1096
+ key="fullscreen-selection-btn"
1107
1097
  :disable="loading"
1108
- icon="ion-ios-eye"
1109
- show
1110
- v-bind="{...defaultTopBtnProps,...pluginOptions.dt?.topSelection?.btn as any}"
1111
- @click="openShowDialogNoIndex(selected[0] as any)"
1112
- />
1113
- <MDtBtn
1114
- v-if="selected.length > 1 ? (hasDestroyBtn && multiDestroy) : hasDestroyBtn"
1115
- key="destroy-dt-selection-btn"
1116
- :disable="!hasSelectedItem || loading"
1117
- color="negative"
1118
- destroy
1119
- icon="ion-ios-trash"
1120
- v-bind="{...defaultTopBtnProps,...pluginOptions.dt?.topSelection?.btn as any}"
1121
- @click="deleteSelectionItem()"
1098
+ :icon="fullscreen ? 'ion-ios-contract' : 'ion-ios-desktop'"
1099
+ :tooltip="`myth.datatable.${fullscreen ? 'exitFullscreen' : 'fullscreen'}`"
1100
+ v-bind="{...defaultTopBtnProps,...pluginOptions.dt?.buttons?.fullscreen as any}"
1101
+ @click="fullscreen = !fullscreen"
1122
1102
  />
1123
- <template
1124
- v-for="(contextBtn,i) in contextItems as GenericMDtBtn[]"
1125
- :key="`top-s-${i}`"
1126
- >
1103
+ <template v-if="hasSelectedItem">
1104
+ <MDtBtn
1105
+ v-if="hasUpdateBtn && isSingleSelectedItem"
1106
+ key="update-dt-selection-btn"
1107
+ :disable="loading"
1108
+ icon="ion-ios-create"
1109
+ update
1110
+ v-bind="{...defaultTopBtnProps,...pluginOptions.dt?.topSelection?.btn as any}"
1111
+ @click="openUpdateDialogNoIndex(selected[0] as any)"
1112
+ />
1113
+ <MDtBtn
1114
+ v-if="hasCloneBtn && isSingleSelectedItem"
1115
+ key="clone-dt-selection-btn"
1116
+ :disable="loading"
1117
+ clone
1118
+ tooltip="labels.clone"
1119
+ v-bind="{...defaultTopBtnProps,...pluginOptions.dt?.topSelection?.btn as any}"
1120
+ @click="onCloneItem(selected[0] as any)"
1121
+ />
1127
1122
  <MDtBtn
1128
- v-if="showIfContext(contextBtn)"
1129
- :tooltip="contextBtn.tooltip?__(contextBtn.tooltip):(contextBtn.label?(te(`labels.${contextBtn.label}`) ?
1130
- __(`labels.${contextBtn.label}`) : __(contextBtn.label) ):undefined)"
1131
- v-bind="{...defaultTopBtnProps,...pluginOptions.dt?.topSelection?.btn as any,...contextBtn as any,...contextBtn.attr as any,to: typeof contextBtn.attr?.to === 'function' ? contextBtn.attr.to(selected[0],0) : contextBtn.attr?.to}"
1132
- @click="contextBtn.attr?.to ? undefined : onClickTopMenu(contextBtn)"
1123
+ v-if="hasShowBtn && isSingleSelectedItem"
1124
+ key="show-dt-selection-btn"
1125
+ :disable="loading"
1126
+ icon="ion-ios-eye"
1127
+ show
1128
+ v-bind="{...defaultTopBtnProps,...pluginOptions.dt?.topSelection?.btn as any}"
1129
+ @click="openShowDialogNoIndex(selected[0] as any)"
1133
1130
  />
1131
+ <MDtBtn
1132
+ v-if="selected.length > 1 ? (hasDestroyBtn && multiDestroy) : hasDestroyBtn"
1133
+ key="destroy-dt-selection-btn"
1134
+ :disable="!hasSelectedItem || loading"
1135
+ color="negative"
1136
+ destroy
1137
+ icon="ion-ios-trash"
1138
+ v-bind="{...defaultTopBtnProps,...pluginOptions.dt?.topSelection?.btn as any}"
1139
+ @click="deleteSelectionItem()"
1140
+ />
1141
+ <template
1142
+ v-for="(contextBtn,i) in contextItems as GenericMDtBtn[]"
1143
+ :key="`top-s-${i}`"
1144
+ >
1145
+ <MDtBtn
1146
+ v-if="showIfContext(contextBtn)"
1147
+ :tooltip="contextBtn.tooltip?__(contextBtn.tooltip):(contextBtn.label?(te(`labels.${contextBtn.label}`) ?
1148
+ __(`labels.${contextBtn.label}`) : __(contextBtn.label) ):undefined)"
1149
+ v-bind="{...defaultTopBtnProps,...pluginOptions.dt?.topSelection?.btn as any,...contextBtn as any,...contextBtn.attr as any,to: typeof contextBtn.attr?.to === 'function' ? contextBtn.attr.to(selected[0],0) : contextBtn.attr?.to}"
1150
+ @click="contextBtn.attr?.to ? undefined : onClickTopMenu(contextBtn)"
1151
+ />
1152
+ </template>
1134
1153
  </template>
1135
- </template>
1136
- </MRow>
1137
- </MCol>
1154
+ </MRow>
1155
+ </MCol>
1138
1156
 
1139
- <MCol
1140
- v-if="computedNoWrapBtn || manageColumns"
1141
- auto
1142
- >
1143
- <MRow
1144
- class="items-center"
1145
- gutter="xs"
1157
+ <MCol
1158
+ v-if="computedNoWrapBtn || manageColumns"
1159
+ auto
1146
1160
  >
1147
- <!-- Manage Columns -->
1148
- <MDtBtn
1149
- v-if="manageColumns && !manageColumnsInline"
1150
- key="manage-columns-btn"
1151
- :disable="loading"
1152
- color="primary"
1153
- icon="ion-ios-desktop"
1154
- label="myth.datatable.columnsToShow"
1155
- text-color="on-primary"
1156
- tooltip="myth.datatable.columnsToShowCaption"
1157
- v-bind="pluginOptions.dt?.buttons?.filter as any"
1161
+ <MRow
1162
+ class="items-center"
1163
+ gutter="xs"
1158
1164
  >
1159
- <MModalMenu
1160
- :no-close-btn="$q.screen.gt.xs"
1161
- :offset="[10,10]"
1165
+ <!-- Manage Columns -->
1166
+ <MDtBtn
1167
+ v-if="manageColumns && !manageColumnsInline"
1168
+ key="manage-columns-btn"
1169
+ :disable="loading"
1170
+ flat
1171
+ icon="ion-ios-desktop"
1172
+ tooltip="myth.datatable.columnsToShowCaption"
1173
+ v-bind="pluginOptions.dt?.buttons?.filter as any"
1162
1174
  >
1163
- <q-card style="min-width: 250px; max-width: 900px">
1164
- <q-card-section>
1165
- <div class="text-body1 q-mb-xs">
1166
- {{ __('myth.datatable.columnsToShow') }}
1167
- </div>
1168
- <div class="text-body2">
1169
- <q-icon name="ion-ios-information-circle-outline" />
1170
- {{ __('myth.datatable.columnsToShowCaption') }}
1171
- </div>
1172
- </q-card-section>
1173
- <q-separator />
1174
- <q-card-section>
1175
- <div class="row justify-between">
1176
- <template
1177
- v-for="h in getHeaders"
1178
- :key="h.name"
1179
- >
1180
- <q-checkbox
1181
- v-model="visibleHeaders"
1182
- :disable="visibleHeaders.length < 2 && visibleHeaders.indexOf(h.name) !== -1"
1183
- :label="h.label"
1184
- :val="h.name"
1185
- />
1186
- </template>
1187
- </div>
1188
- </q-card-section>
1189
- <q-separator />
1190
- <q-card-section>
1191
- <MBtn
1192
- v-if="$q.screen.gt.xs"
1193
- v-close-popup
1194
- :class="{'q-mr-sm': !$q.screen.xs}"
1195
- color="primary"
1196
- label="labels.close"
1197
- no-caps
1198
- style="min-width: 100px"
1199
- text-color="on-primary"
1200
- unelevated
1201
- />
1202
- <MBtn
1203
- :class="{'full-width': $q.screen.xs}"
1204
- :label="__(`labels.${visibleHeaders.length === getHeaders.length ? 'unselect':'select'}_all`)"
1205
- color="primary"
1206
- no-caps
1207
- style="min-width: 100px"
1208
- text-color="on-primary"
1209
- unelevated
1210
- @click="onManageColumnsClick()"
1211
- />
1212
- </q-card-section>
1213
- </q-card>
1214
- </MModalMenu>
1215
- </MDtBtn>
1216
- <!-- Wrap Btn -->
1217
- <MDtBtn
1218
- v-if="!computedNoWrapBtn"
1219
- :icon="`ion-ios-${wrapCellsRef ? 'code' : 'code-working'}`"
1220
- flat
1221
- tooltip="myth.datatable.wrapBtn"
1222
- @click="wrapCellsRef = !wrapCellsRef"
1223
- />
1224
- </MRow>
1225
- </MCol>
1226
- </MRow>
1227
-
1228
- <!-- Manage Columns -->
1229
- <MRow
1230
- v-if="manageColumns && manageColumnsInline"
1231
- class="items-center m-datatable__table-manage-columns"
1232
- >
1233
- <q-list
1234
- bordered
1235
- class="rounded-borders col-12"
1236
- >
1237
- <q-expansion-item
1238
- :caption="__('myth.datatable.columnsToShowCaption')"
1239
- :label="__('myth.datatable.columnsToShow')"
1240
- expand-separator
1241
- icon="ion-ios-tv"
1242
- >
1243
- <q-card>
1244
- <q-card-section>
1245
- <template
1246
- v-for="h in getHeaders"
1247
- :key="h.name"
1175
+ <MModalMenu
1176
+ :no-close-btn="$q.screen.gt.xs"
1177
+ :offset="[10,10]"
1248
1178
  >
1249
- <q-checkbox
1250
- v-model="visibleHeaders"
1251
- :disable="visibleHeaders.length < 2 && visibleHeaders.indexOf(h.name) !== -1"
1252
- :label="h.label"
1253
- :val="h.name"
1254
- />
1255
- </template>
1256
- </q-card-section>
1257
- </q-card>
1258
- </q-expansion-item>
1259
- </q-list>
1260
- </MRow>
1261
-
1262
- <!-- Filter Row -->
1263
- <MRow
1264
- v-if="Object.values(filterForm).filter(e => e !== undefined && e !== null).length > 0"
1265
- class="items-center m-datatable__table-filter"
1266
- >
1267
- <MCol col="auto">
1268
- <span class="text-subtitle1 q-mr-sm">{{ __('myth.datatable.filteredBy') }}</span>
1269
- </MCol>
1270
- <template
1271
- v-for="(filterValue,filterKey) in filterForm"
1272
- :key="`filter-${filterKey}`"
1179
+ <q-card style="min-width: 250px; max-width: 900px">
1180
+ <q-card-section>
1181
+ <div class="text-body1 q-mb-xs">
1182
+ {{ __('myth.datatable.columnsToShow') }}
1183
+ </div>
1184
+ <div class="text-body2">
1185
+ <q-icon name="ion-ios-information-circle-outline" />
1186
+ {{ __('myth.datatable.columnsToShowCaption') }}
1187
+ </div>
1188
+ </q-card-section>
1189
+ <q-separator />
1190
+ <q-card-section>
1191
+ <div class="row justify-between">
1192
+ <template
1193
+ v-for="h in getHeaders"
1194
+ :key="h.name"
1195
+ >
1196
+ <q-checkbox
1197
+ v-model="visibleHeaders"
1198
+ :disable="visibleHeaders.length < 2 && visibleHeaders.indexOf(h.name) !== -1"
1199
+ :label="h.label"
1200
+ :val="h.name"
1201
+ />
1202
+ </template>
1203
+ </div>
1204
+ </q-card-section>
1205
+ <q-separator />
1206
+ <q-card-section>
1207
+ <MBtn
1208
+ v-if="$q.screen.gt.xs"
1209
+ v-close-popup
1210
+ :class="{'q-mr-sm': !$q.screen.xs}"
1211
+ color="primary"
1212
+ label="labels.close"
1213
+ no-caps
1214
+ style="min-width: 100px"
1215
+ text-color="on-primary"
1216
+ unelevated
1217
+ />
1218
+ <MBtn
1219
+ :class="{'full-width': $q.screen.xs}"
1220
+ :label="__(`labels.${visibleHeaders.length === getHeaders.length ? 'unselect':'select'}_all`)"
1221
+ color="primary"
1222
+ no-caps
1223
+ style="min-width: 100px"
1224
+ text-color="on-primary"
1225
+ unelevated
1226
+ @click="onManageColumnsClick()"
1227
+ />
1228
+ </q-card-section>
1229
+ </q-card>
1230
+ </MModalMenu>
1231
+ </MDtBtn>
1232
+ <!-- Wrap Btn -->
1233
+ <MDtBtn
1234
+ v-if="!computedNoWrapBtn"
1235
+ :icon="`ion-ios-${wrapCellsRef ? 'code' : 'code-working'}`"
1236
+ flat
1237
+ tooltip="myth.datatable.wrapBtn"
1238
+ @click="wrapCellsRef = !wrapCellsRef"
1239
+ />
1240
+ </MRow>
1241
+ </MCol>
1242
+ </MRow>
1243
+ <!-- Manage Columns -->
1244
+ <MRow
1245
+ v-if="manageColumns && manageColumnsInline"
1246
+ class="items-center m-datatable__table-manage-columns"
1273
1247
  >
1274
- <MCol
1275
- v-if="filterValue !== null && filterValue !== undefined"
1276
- col="auto"
1248
+ <q-list
1249
+ bordered
1250
+ class="rounded-borders col-12"
1277
1251
  >
1278
- <q-chip
1279
- clickable
1280
- color="primary"
1281
- icon-remove="clear"
1282
- outline
1283
- removable
1284
- text-color="on-primary"
1285
- @click="openFilterDialog()"
1286
- @remove="onRemoveFilter(filterKey)"
1252
+ <q-expansion-item
1253
+ :caption="__('myth.datatable.columnsToShowCaption')"
1254
+ :label="__('myth.datatable.columnsToShow')"
1255
+ expand-separator
1256
+ icon="ion-ios-tv"
1287
1257
  >
1288
- <span>
1289
- {{ getHeaders.find(e => e.name === filterKey)?.label || __(`attributes.${filterKey}`) }}
1290
- </span>
1291
- <span v-if="typeof filterValue === 'boolean'">
1292
- : {{ __(`labels.${filterValue ? 'yes' : 'no'}`) }}
1293
- </span>
1294
- <span v-else-if="typeof filterValue === 'string'">: {{ filterValue }}</span>
1295
- <span
1296
- v-else-if="Array.isArray(filterValue) && !quasarHelpers.object(filterValue[0])"
1297
- >: {{ filterValue.join(', ') }}</span>
1298
- <span
1299
- v-else-if="Array.isArray(filterValue) && quasarHelpers.object(filterValue[0])"
1300
- >: {{ filterValue.map(e => e.label).join(', ') }}</span>
1301
- <span
1302
- v-else-if="quasarHelpers.object(filterValue) && filterValue.label"
1303
- >: {{ filterValue.label }}</span>
1304
- <span v-else-if="typeof filterValue === 'object'">:&nbsp;{{ Object.values(filterValue).join(' - ') }}</span>
1305
- </q-chip>
1258
+ <q-card>
1259
+ <q-card-section>
1260
+ <template
1261
+ v-for="h in getHeaders"
1262
+ :key="h.name"
1263
+ >
1264
+ <q-checkbox
1265
+ v-model="visibleHeaders"
1266
+ :disable="visibleHeaders.length < 2 && visibleHeaders.indexOf(h.name) !== -1"
1267
+ :label="h.label"
1268
+ :val="h.name"
1269
+ />
1270
+ </template>
1271
+ </q-card-section>
1272
+ </q-card>
1273
+ </q-expansion-item>
1274
+ </q-list>
1275
+ </MRow>
1276
+ <!-- Filter Row -->
1277
+ <MRow
1278
+ v-if="Object.values(filterForm).filter(e => e !== undefined && e !== null).length > 0"
1279
+ class="items-center m-datatable__table-filter"
1280
+ >
1281
+ <MCol col="auto">
1282
+ <span class="text-subtitle1 q-mr-sm">{{ __('myth.datatable.filteredBy') }}</span>
1306
1283
  </MCol>
1307
- </template>
1308
- </MRow>
1309
-
1310
- <!-- Selection Row -->
1311
- <MRow
1312
- v-if="!!$slots.selection"
1313
- class="items-center q-gutter-xs m-datatable__table-selection"
1314
- style="min-height: 38px"
1315
- >
1316
- <slot
1317
- :dt="datatableItemsScope"
1318
- :form="useFormContext"
1319
- :index="dialogItemIndex"
1320
- :item="dialogItem"
1321
- name="selection"
1322
- />
1323
- </MRow>
1284
+ <template
1285
+ v-for="(filterValue,filterKey) in filterForm"
1286
+ :key="`filter-${filterKey}`"
1287
+ >
1288
+ <MCol
1289
+ v-if="filterValue !== null && filterValue !== undefined"
1290
+ col="auto"
1291
+ >
1292
+ <q-chip
1293
+ clickable
1294
+ color="primary"
1295
+ icon-remove="clear"
1296
+ outline
1297
+ removable
1298
+ text-color="on-primary"
1299
+ @click="openFilterDialog()"
1300
+ @remove="onRemoveFilter(filterKey)"
1301
+ >
1302
+ <span>
1303
+ {{ getHeaders.find(e => e.name === filterKey)?.label || __(`attributes.${filterKey}`) }}
1304
+ </span>
1305
+ <span v-if="typeof filterValue === 'boolean'">
1306
+ : {{ __(`labels.${filterValue ? 'yes' : 'no'}`) }}
1307
+ </span>
1308
+ <span v-else-if="typeof filterValue === 'string'">: {{ filterValue }}</span>
1309
+ <span
1310
+ v-else-if="Array.isArray(filterValue) && !quasarHelpers.object(filterValue[0])"
1311
+ >: {{ filterValue.join(', ') }}</span>
1312
+ <span
1313
+ v-else-if="Array.isArray(filterValue) && quasarHelpers.object(filterValue[0])"
1314
+ >: {{ filterValue.map(e => e.label).join(', ') }}</span>
1315
+ <span
1316
+ v-else-if="quasarHelpers.object(filterValue) && filterValue.label"
1317
+ >: {{ filterValue.label }}</span>
1318
+ <span v-else-if="typeof filterValue === 'object'">:&nbsp;{{ Object.values(filterValue).join(' - ') }}</span>
1319
+ </q-chip>
1320
+ </MCol>
1321
+ </template>
1322
+ </MRow>
1323
+ <!-- Selection Row -->
1324
+ <MRow
1325
+ v-if="!!$slots.selection"
1326
+ class="items-center q-gutter-xs m-datatable__table-selection"
1327
+ style="min-height: 38px"
1328
+ >
1329
+ <slot
1330
+ :dt="datatableItemsScope"
1331
+ :form="useFormContext"
1332
+ :index="dialogItemIndex"
1333
+ :item="dialogItem"
1334
+ name="selection"
1335
+ />
1336
+ </MRow>
1337
+ </div>
1324
1338
  </MCol>
1325
1339
  </template>
1326
1340
  <template
@@ -397,6 +397,15 @@ export const useDtHelpers = (options: MaybeRefOrGetter<MDatatableProps>, emit?:
397
397
  })
398
398
  })
399
399
  }
400
+ const hasExport = computed(() => (type: MDtExportOptions) => {
401
+ if (type === 'pdf') {
402
+ return props.pdf
403
+ }
404
+ if (type === 'excel') {
405
+ return props.excel
406
+ }
407
+ return false
408
+ })
400
409
  const exportData = (type: MDtExportOptions) => {
401
410
  if (loading.value) {
402
411
  return
@@ -1018,6 +1027,7 @@ export const useDtHelpers = (options: MaybeRefOrGetter<MDatatableProps>, emit?:
1018
1027
  getRequestWith,
1019
1028
  getDatatableParams,
1020
1029
  fetchDatatableItems,
1030
+ hasExport,
1021
1031
  exportData,
1022
1032
  openDialogTimeout,
1023
1033
  beforeShowFilterDialog,
@@ -254,19 +254,49 @@ export const Helpers = {
254
254
  }
255
255
 
256
256
  const _offset = (offset + current) - (100 + (opt?.offset || 0))
257
- setVerticalScrollPosition(target, _offset, duration)
257
+ if (Helpers.isElementInViewport(target)) {
258
+ setVerticalScrollPosition(target, _offset, duration)
259
+ }
260
+ },
261
+ isElementInViewport (element: Element | Window | HTMLElement): boolean {
262
+ const rect = element instanceof Window ? element.document?.body?.getBoundingClientRect() : element?.getBoundingClientRect?.()
263
+ if (!rect) return false
264
+ return (
265
+ rect.top >= 0 &&
266
+ rect.left >= 0 &&
267
+ rect.bottom <= (window.innerHeight + rect.height) &&
268
+ rect.right <= (window.innerWidth + rect.width)
269
+ )
258
270
  },
259
271
  async scrollToElementFromErrors (errors?: Partial<Record<string, string[] | string | null | undefined>>, elm?: any, target?: any) {
260
272
  if (!errors) {
261
273
  return
262
274
  }
263
- for (const [name, value] of Object.entries(errors)) {
275
+ const allErrors = Object.entries(errors)
276
+ const keys = allErrors.map(([k]) => k)
277
+ let siblings: Element[] = []
278
+ if (allErrors.length > 0) {
279
+ siblings = Array.from(
280
+ document.body.querySelectorAll('[data-input-name].m-input__error') || []
281
+ ).filter(k => keys.includes(k?.getAttribute('data-input-name') as string))
282
+ }
283
+ if (siblings.length > 0) {
284
+ for (const s of siblings) {
285
+ const e = allErrors.find(e => e[0] === s.getAttribute('data-input-name'))
286
+ if (e?.[1]) {
287
+ const name = e[0]
288
+ await Helpers.scrollToElement(s || `[name='${name}']`, { target })
289
+ break
290
+ }
291
+ }
292
+ return
293
+ }
294
+ for (const [name, value] of allErrors) {
264
295
  const val = value && Array.isArray(value) ? value[0] : value
265
296
  if (val?.toString?.()?.length) {
266
297
  if (!elm) {
267
298
  const selector = `[data-input-name='${name}']`
268
299
  const e = document.querySelector(selector) as HTMLElement
269
- // console.log(e)
270
300
  await Helpers.scrollToElement(e || `[name='${name}']`, { target })
271
301
  } else {
272
302
  await Helpers.scrollToElement(elm, { target })