adminforth 2.18.2 → 2.19.0-next.2

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.
@@ -11,7 +11,7 @@
11
11
 
12
12
  <BreadcrumbsWithButtons>
13
13
  <!-- save and cancle -->
14
- <button @click="$router.back()"
14
+ <button @click="() => {cancelButtonClicked = true; $router.back()}"
15
15
  class="flex items-center py-1 px-3 me-2 text-sm font-medium text-lightEditViewButtonText rounded-default focus:outline-none bg-lightEditViewButtonBackground rounded border border-lightEditViewButtonBorder hover:bg-lightEditViewButtonBackgroundHover hover:text-lightEditViewButtonTextHover focus:z-10 focus:ring-4 focus:ring-lightEditViewButtonFocusRing dark:focus:ring-darkEditViewButtonFocusRing dark:bg-darkEditViewButtonBackground dark:text-darkEditViewButtonText dark:border-darkEditViewButtonBorder dark:hover:text-darkEditViewButtonTextHover dark:hover:bg-darkEditViewButtonBackgroundHover"
16
16
  >
17
17
  {{ $t('Cancel') }}
@@ -76,8 +76,8 @@ import SingleSkeletLoader from '@/components/SingleSkeletLoader.vue';
76
76
  import { useCoreStore } from '@/stores/core';
77
77
  import { callAdminForthApi, getCustomComponent,checkAcessByAllowedActions, initThreeDotsDropdown } from '@/utils';
78
78
  import { IconFloppyDiskSolid } from '@iconify-prerendered/vue-flowbite';
79
- import { computed, onMounted, onBeforeMount, ref, type Ref, nextTick, watch } from 'vue';
80
- import { useRoute, useRouter } from 'vue-router';
79
+ import { computed, onMounted, onBeforeMount, ref, type Ref, nextTick, watch, onBeforeUnmount } from 'vue';
80
+ import { useRoute, useRouter, onBeforeRouteLeave } from 'vue-router';
81
81
  import { showErrorTost } from '@/composables/useFrontendApi';
82
82
  import ThreeDotsMenu from '@/components/ThreeDotsMenu.vue';
83
83
  import { useAdminforth } from '@/adminforth';
@@ -87,7 +87,7 @@ import type { AdminForthResourceColumn } from '@/types/Back';
87
87
 
88
88
  const { t } = useI18n();
89
89
  const coreStore = useCoreStore();
90
- const { clearSaveInterceptors, runSaveInterceptors, alert } = useAdminforth();
90
+ const { clearSaveInterceptors, runSaveInterceptors, alert, confirm } = useAdminforth();
91
91
 
92
92
  const isValid = ref(false);
93
93
  const validating = ref(false);
@@ -101,6 +101,36 @@ const saving = ref(false);
101
101
 
102
102
  const record: Ref<Record<string, any>> = ref({});
103
103
 
104
+ const initialRecord = computed(() => coreStore.record);
105
+ const wasSaveSuccessful = ref(false);
106
+ const cancelButtonClicked = ref(false);
107
+
108
+ function onBeforeUnload(event: BeforeUnloadEvent) {
109
+ if (!checkIfWeCanLeavePage()) {
110
+ event.preventDefault();
111
+ event.returnValue = '';
112
+ }
113
+ }
114
+
115
+ function checkIfWeCanLeavePage() {
116
+ return wasSaveSuccessful.value || cancelButtonClicked.value || JSON.stringify(record.value) === JSON.stringify(initialRecord.value);
117
+ }
118
+
119
+ window.addEventListener('beforeunload', onBeforeUnload);
120
+
121
+ onBeforeUnmount(() => {
122
+ window.removeEventListener('beforeunload', onBeforeUnload);
123
+ });
124
+
125
+ onBeforeRouteLeave(async (to, from, next) => {
126
+ if (!checkIfWeCanLeavePage()) {
127
+ const answer = await confirm({message: t('There are unsaved changes. Are you sure you want to leave this page?'), yes: 'Yes', no: 'No'});
128
+ if (!answer) return next(false);
129
+ }
130
+ next();
131
+ });
132
+
133
+
104
134
  watch(record, (newVal) => {
105
135
  console.log('Record updated:', newVal);
106
136
  }, { deep: true });
@@ -199,22 +229,27 @@ async function saveRecord() {
199
229
  updates[key] = record.value[key];
200
230
  }
201
231
  }
202
-
203
- const resp = await callAdminForthApi({
204
- method: 'POST',
205
- path: `/update_record`,
206
- body: {
207
- resourceId: route.params.resourceId,
208
- recordId: route.params.primaryKey,
209
- record: updates,
210
- meta: {
211
- ...(interceptorConfirmationResult ? { confirmationResult: interceptorConfirmationResult } : {}),
232
+ let resp = null;
233
+ try {
234
+ resp = await callAdminForthApi({
235
+ method: 'POST',
236
+ path: `/update_record`,
237
+ body: {
238
+ resourceId: route.params.resourceId,
239
+ recordId: route.params.primaryKey,
240
+ record: updates,
241
+ meta: {
242
+ ...(interceptorConfirmationResult ? { confirmationResult: interceptorConfirmationResult } : {}),
243
+ },
212
244
  },
213
- },
214
- });
245
+ });
246
+ } finally {
247
+ saving.value = false;
248
+ }
215
249
  if (resp.error && resp.error !== 'Operation aborted by hook') {
216
250
  showErrorTost(resp.error);
217
251
  } else {
252
+ wasSaveSuccessful.value = true;
218
253
  alert({
219
254
  message: t('Record updated successfully'),
220
255
  variant: 'success',
@@ -48,7 +48,7 @@
48
48
  <button
49
49
  v-if="!action.showInThreeDotsDropdown"
50
50
  :key="action.id"
51
- @click="startBulkAction(action.id!)"
51
+ @click="startBulkActionInner(action.id!)"
52
52
  class="flex gap-1 items-center py-1 px-3 text-sm font-medium text-lightListViewButtonText focus:outline-none bg-lightListViewButtonBackground rounded-default border border-lightListViewButtonBorder hover:bg-lightListViewButtonBackgroundHover hover:text-lightListViewButtonTextHover focus:z-10 focus:ring-4 focus:ring-lightListViewButtonFocusRing dark:focus:ring-darkListViewButtonFocusRing dark:bg-darkListViewButtonBackground dark:text-darkListViewButtonText dark:border-darkListViewButtonBorder dark:hover:text-darkListViewButtonTextHover dark:hover:bg-darkListViewButtonBackgroundHover"
53
53
  :class="action.buttonCustomCssClass || ''"
54
54
  >
@@ -104,8 +104,8 @@
104
104
  :threeDotsDropdownItems="(coreStore.resourceOptions?.pageInjections?.list?.threeDotsDropdownItems as [])"
105
105
  :bulkActions="coreStore.resource?.options?.bulkActions"
106
106
  :checkboxes="checkboxes"
107
- @startBulkAction="startBulkAction"
108
- :updateList="getList"
107
+ @startBulkAction="startBulkActionInner"
108
+ :updateList="getListInner"
109
109
  :clearCheckboxes="clearCheckboxes"
110
110
  ></ThreeDotsMenu>
111
111
  </BreadcrumbsWithButtons>
@@ -126,7 +126,7 @@
126
126
  @update:page="page = $event"
127
127
  @update:sort="sort = $event"
128
128
  @update:checkboxes="checkboxes = $event"
129
- @update:records="getList"
129
+ @update:records="getListInner"
130
130
  :sort="sort"
131
131
  :pageSize="pageSize"
132
132
  :totalRows="totalRows"
@@ -164,7 +164,7 @@
164
164
  @update:page="page = $event"
165
165
  @update:sort="sort = $event"
166
166
  @update:checkboxes="checkboxes = $event"
167
- @update:records="getList"
167
+ @update:records="getListInner"
168
168
  :sort="sort"
169
169
  :pageSize="pageSize"
170
170
  :totalRows="totalRows"
@@ -209,10 +209,9 @@ import ResourceListTable from '@/components/ResourceListTable.vue';
209
209
  import { useCoreStore } from '@/stores/core';
210
210
  import { useFiltersStore } from '@/stores/filters';
211
211
  import { callAdminForthApi, currentQuery, getIcon, setQuery } from '@/utils';
212
- import { computed, onMounted, onUnmounted, ref, watch, nextTick, type Ref } from 'vue';
212
+ import { computed, onMounted, onUnmounted, ref, watch, type Ref } from 'vue';
213
213
  import { useRoute } from 'vue-router';
214
- import { showErrorTost } from '@/composables/useFrontendApi'
215
- import { getCustomComponent, initThreeDotsDropdown } from '@/utils';
214
+ import { getCustomComponent, initThreeDotsDropdown, getList, startBulkAction } from '@/utils';
216
215
  import ThreeDotsMenu from '@/components/ThreeDotsMenu.vue';
217
216
  import { Tooltip } from '@/afcl'
218
217
  import type { AdminForthComponentDeclarationFull } from '@/types/Common';
@@ -228,7 +227,7 @@ import Filters from '@/components/Filters.vue';
228
227
  import { useAdminforth } from '@/adminforth';
229
228
 
230
229
  const filtersShow = ref(false);
231
- const { confirm, alert, list } = useAdminforth();
230
+ const { list } = useAdminforth();
232
231
  const coreStore = useCoreStore();
233
232
  const filtersStore = useFiltersStore();
234
233
 
@@ -257,45 +256,6 @@ const listBufferSize = computed(() => coreStore.resource?.options?.listBufferSiz
257
256
 
258
257
  const isPageLoaded = ref(false);
259
258
 
260
- async function getList() {
261
- rows.value = null;
262
- if (!isPageLoaded.value) {
263
- return;
264
- }
265
- const data = await callAdminForthApi({
266
- path: '/get_resource_data',
267
- method: 'POST',
268
- body: {
269
- source: 'list',
270
- resourceId: route.params.resourceId,
271
- limit: pageSize.value,
272
- offset: ((page.value || 1) - 1) * pageSize.value,
273
- filters: filtersStore.filters,
274
- sort: sort.value,
275
- }
276
- });
277
- if (data.error) {
278
- showErrorTost(data.error);
279
- rows.value = [];
280
- totalRows.value = 0;
281
- return {error: data.error};
282
- }
283
- rows.value = data.data?.map((row: any) => {
284
- if (coreStore.resource?.columns?.find(c => c.primaryKey)?.foreignResource) {
285
- row._primaryKeyValue = row[coreStore.resource.columns.find(c => c.primaryKey)!.name].pk;
286
- } else if (coreStore.resource) {
287
- row._primaryKeyValue = row[coreStore.resource.columns.find(c => c.primaryKey)!.name];
288
- }
289
- return row;
290
- });
291
- totalRows.value = data.total;
292
-
293
- // if checkboxes have items which are not in current data, remove them
294
- checkboxes.value = checkboxes.value.filter(pk => rows.value!.some(r => r._primaryKeyValue === pk));
295
- await nextTick();
296
- return {}
297
- }
298
-
299
259
  function clearCheckboxes() {
300
260
  checkboxes.value = [];
301
261
  }
@@ -351,48 +311,21 @@ async function refreshExistingList(pk?: any) {
351
311
  return {}
352
312
  }
353
313
 
314
+ async function startBulkActionInner(actionId: string) {
315
+ await startBulkAction(actionId, coreStore.resource!, checkboxes, bulkActionLoadingStates, getListInner);
316
+ }
354
317
 
355
- async function startBulkAction(actionId: string) {
356
- const action = coreStore.resource?.options?.bulkActions?.find(a => a.id === actionId);
357
- if (action?.confirm) {
358
- const confirmed = await confirm({
359
- message: action.confirm,
360
- });
361
- if (!confirmed) {
362
- return;
363
- }
364
- }
365
- bulkActionLoadingStates.value[actionId] = true;
366
-
367
- const data = await callAdminForthApi({
368
- path: '/start_bulk_action',
369
- method: 'POST',
370
- body: {
371
- resourceId: route.params.resourceId,
372
- actionId: actionId,
373
- recordIds: checkboxes.value
374
-
375
- }
376
- });
377
- bulkActionLoadingStates.value[actionId] = false;
378
- if (data?.ok) {
379
- checkboxes.value = [];
380
- await getList();
381
-
382
- if (data.successMessage) {
383
- alert({
384
- message: data.successMessage,
385
- variant: 'success'
386
- });
387
- }
388
-
389
- }
390
- if (data?.error) {
391
- showErrorTost(data.error);
318
+ async function getListInner() {
319
+ rows.value = null; // to show loading state
320
+ const result = await getList(coreStore.resource!, isPageLoaded.value, page.value, pageSize.value, sort.value, checkboxes, filtersStore.filters);
321
+ if (!result) {
322
+ return { error: 'No result returned from getList' };
392
323
  }
324
+ rows.value = result.rows;
325
+ totalRows.value = result.totalRows ?? 0;
326
+ return result.error ? { error: result.error } : {};
393
327
  }
394
328
 
395
-
396
329
  class SortQuerySerializer {
397
330
  static serialize(sort: {field: string, direction: 'asc' | 'desc'}[]) {
398
331
  return sort.map(s => `${s.field}__${s.direction}`).join(',');
@@ -471,12 +404,11 @@ async function init() {
471
404
 
472
405
  watch([page, sort, () => filtersStore.filters], async () => {
473
406
  // console.log('🔄️ page/sort/filter change fired, page:', page.value);
474
- await getList();
407
+ await getListInner();
475
408
  }, { deep: true });
476
409
 
477
410
  list.refresh = async () => {
478
- const result = await getList();
479
-
411
+ const result = await getListInner();
480
412
  if (!result) {
481
413
  return {};
482
414
  }
@@ -465,8 +465,12 @@ export type BeforeDataSourceRequestFunction = (params: {
465
465
  adminforth: IAdminForth;
466
466
  }) => Promise<{
467
467
  ok: boolean;
468
- error?: string;
468
+ error?: string | null;
469
+ /**
470
+ * @deprecated Since 1.2.9. Will be removed in 2.0.0. Use redirectToRecordId instead.
471
+ */
469
472
  newRecordId?: string;
473
+ redirectToRecordId?: string;
470
474
  }>;
471
475
  /**
472
476
  * Modify response to change how data is returned after fetching from database.
@@ -512,8 +516,14 @@ export type CreateResourceRecordResult = {
512
516
  /**
513
517
  * Optional id of an existing record to redirect to
514
518
  * (used when a beforeSave hook aborts creation and supplies newRecordId, allows to implement programmatic creation via API).
519
+ * @deprecated Since 1.2.9. Will be removed in 2.0.0. Use redirectToRecordId instead.
515
520
  */
516
521
  newRecordId?: any;
522
+ /**
523
+ * Optional id of an existing record to redirect to
524
+ * (used when a beforeSave hook aborts creation and supplies redirectToRecordId, allows to implement programmatic creation via API).
525
+ */
526
+ redirectToRecordId?: any;
517
527
  };
518
528
  /**
519
529
  * Parameters for {@link IAdminForth.createResourceRecord}.
@@ -670,51 +680,149 @@ export type DeleteResourceRecordResult = {
670
680
  * Return ok: false and error: string to stop execution and show error message to user. Return ok: true to continue execution.
671
681
  */
672
682
  export type BeforeDeleteSaveFunction = (params: {
683
+ /**
684
+ * Resource info.
685
+ */
673
686
  resource: AdminForthResource;
687
+ /**
688
+ * Primary key value of the record to delete.
689
+ */
674
690
  recordId: any;
691
+ /**
692
+ * Admin user performing the action.
693
+ */
675
694
  adminUser: AdminUser;
695
+ /**
696
+ * Record data before deletion.
697
+ */
676
698
  record: any;
699
+ /**
700
+ * Adminforth instance.
701
+ */
677
702
  adminforth: IAdminForth;
703
+ /**
704
+ * HTTP response object.
705
+ *
706
+ * @deprecated Since 1.2.9. Will be removed in 2.0.0. Use extra.response instead.
707
+ */
678
708
  response?: IAdminForthHttpResponse;
709
+ /**
710
+ * Extra HTTP information. Prefer using extra.response over the top-level response field.
711
+ */
679
712
  extra?: HttpExtra;
680
713
  }) => Promise<{
681
714
  ok: boolean;
682
715
  error?: string;
683
716
  }>;
684
717
  export type BeforeEditSaveFunction = (params: {
718
+ /**
719
+ * Resource info.
720
+ */
685
721
  resource: AdminForthResource;
722
+ /**
723
+ * Primary key value of the record to delete.
724
+ */
686
725
  recordId: any;
726
+ /**
727
+ * Admin user performing the action.
728
+ */
687
729
  adminUser: AdminUser;
688
730
  updates: any;
731
+ /**
732
+ * Record with updates
733
+ *
734
+ * @deprecated. Will be removed in 2.0.0. Use updates instead.
735
+ */
689
736
  record: any;
737
+ /**
738
+ * Record data before update.
739
+ */
690
740
  oldRecord: any;
741
+ /**
742
+ * Adminforth instance.
743
+ */
691
744
  adminforth: IAdminForth;
745
+ /**
746
+ * HTTP response object.
747
+ *
748
+ * @deprecated Since 1.2.9. Will be removed in 2.0.0. Use extra.response instead.
749
+ */
692
750
  response: IAdminForthHttpResponse;
751
+ /**
752
+ * Extra HTTP information. Prefer using extra.response over the top-level response field.
753
+ */
693
754
  extra?: HttpExtra;
694
755
  }) => Promise<{
695
756
  ok: boolean;
696
757
  error?: string | null;
697
758
  }>;
698
759
  export type BeforeCreateSaveFunction = (params: {
760
+ /**
761
+ * Resource info.
762
+ */
699
763
  resource: AdminForthResource;
764
+ /**
765
+ * Admin user performing the action.
766
+ */
700
767
  adminUser: AdminUser;
768
+ /**
769
+ * Record data to create.
770
+ */
701
771
  record: any;
772
+ /**
773
+ * Adminforth instance.
774
+ */
702
775
  adminforth: IAdminForth;
776
+ /**
777
+ * HTTP response object.
778
+ *
779
+ * @deprecated Since 1.2.9. Will be removed in 2.0.0. Use extra.response instead.
780
+ */
703
781
  response: IAdminForthHttpResponse;
704
782
  extra?: HttpExtra;
705
783
  }) => Promise<{
706
784
  ok: boolean;
707
785
  error?: string | null;
786
+ /**
787
+ * @deprecated Since 1.2.9. Will be removed in 2.0.0. Use redirectToRecordId instead.
788
+ */
708
789
  newRecordId?: string;
790
+ redirectToRecordId?: string;
709
791
  }>;
710
792
  export type AfterCreateSaveFunction = (params: {
793
+ /**
794
+ * Resource info.
795
+ */
711
796
  resource: AdminForthResource;
797
+ /**
798
+ * Primary key value of the record to delete.
799
+ */
712
800
  recordId: any;
801
+ /**
802
+ * Admin user performing the action.
803
+ */
713
804
  adminUser: AdminUser;
805
+ /**
806
+ * Record data after creation.
807
+ */
714
808
  record: any;
809
+ /**
810
+ * Adminforth instance.
811
+ */
715
812
  adminforth: IAdminForth;
813
+ /**
814
+ * Record with virtual columns after creation.
815
+ */
716
816
  recordWithVirtualColumns?: any;
817
+ /**
818
+ * HTTP response object.
819
+ *
820
+ * @deprecated Since 1.2.9. Will be removed in 2.0.0. Use extra.response instead.
821
+ */
717
822
  response: IAdminForthHttpResponse;
823
+ /**
824
+ * Extra HTTP information. Prefer using extra.response over the top-level response field.
825
+ */
718
826
  extra?: HttpExtra;
719
827
  }) => Promise<{
720
828
  ok: boolean;
@@ -725,26 +833,80 @@ export type AfterCreateSaveFunction = (params: {
725
833
  * Return ok: false and error: string to stop execution and show error message to user. Return ok: true to continue execution.
726
834
  */
727
835
  export type AfterDeleteSaveFunction = (params: {
836
+ /**
837
+ * Resource info.
838
+ */
728
839
  resource: AdminForthResource;
840
+ /**
841
+ * Primary key value of the record to delete.
842
+ */
729
843
  recordId: any;
844
+ /**
845
+ * Admin user performing the action.
846
+ */
730
847
  adminUser: AdminUser;
848
+ /**
849
+ * Record data, that was deleted.
850
+ */
731
851
  record: any;
852
+ /**
853
+ * Adminforth instance.
854
+ */
732
855
  adminforth: IAdminForth;
856
+ /**
857
+ * HTTP response object.
858
+ *
859
+ * @deprecated Since 1.2.9. Will be removed in 2.0.0. Use extra.response instead.
860
+ */
733
861
  response: IAdminForthHttpResponse;
862
+ /**
863
+ * Extra HTTP information. Prefer using extra.response over the top-level response field.
864
+ */
734
865
  extra?: HttpExtra;
735
866
  }) => Promise<{
736
867
  ok: boolean;
737
868
  error?: string;
738
869
  }>;
739
870
  export type AfterEditSaveFunction = (params: {
871
+ /**
872
+ * Resource info.
873
+ */
740
874
  resource: AdminForthResource;
875
+ /**
876
+ * Primary key value of the record to delete.
877
+ */
741
878
  recordId: any;
879
+ /**
880
+ * Admin user performing the action.
881
+ */
742
882
  adminUser: AdminUser;
883
+ /**
884
+ * Record updates.
885
+ */
743
886
  updates: any;
887
+ /**
888
+ * Record after update.
889
+ *
890
+ * @deprecated. Will be removed in 2.0.0. Use updates instead.
891
+ */
744
892
  record: any;
893
+ /**
894
+ * Record data before update.
895
+ */
745
896
  oldRecord: any;
897
+ /**
898
+ * Adminforth instance.
899
+ */
746
900
  adminforth: IAdminForth;
901
+ /**
902
+ * HTTP response object.
903
+ *
904
+ * @deprecated Since 1.2.9. Will be removed in 2.0.0. Use extra.response instead.
905
+ */
747
906
  response: IAdminForthHttpResponse;
907
+ /**
908
+ * Extra HTTP information. Prefer using extra.response over the top-level response field.
909
+ */
748
910
  extra?: HttpExtra;
749
911
  }) => Promise<{
750
912
  ok: boolean;
@@ -754,10 +916,27 @@ export type AfterEditSaveFunction = (params: {
754
916
  * Allow to get user data before login confirmation, will triger when user try to login.
755
917
  */
756
918
  export type BeforeLoginConfirmationFunction = (params?: {
919
+ /**
920
+ * Admin user performing the action.
921
+ */
757
922
  adminUser: AdminUser;
923
+ /**
924
+ * HTTP response object.
925
+ *
926
+ * @deprecated Since 1.2.9. Will be removed in 2.0.0. Use extra.response instead.
927
+ */
758
928
  response: IAdminForthHttpResponse;
929
+ /**
930
+ * Adminforth instance.
931
+ */
759
932
  adminforth: IAdminForth;
933
+ /**
934
+ * Extra HTTP information. Prefer using extra.response over the top-level response field.
935
+ */
760
936
  extra?: HttpExtra;
937
+ /**
938
+ * Duration of session in format "1s", "1m", "1h", or "1d" (e.g., "30d" for 30 days)
939
+ */
761
940
  sessionDuration?: string;
762
941
  }) => Promise<{
763
942
  error?: string;
@@ -770,9 +949,23 @@ export type BeforeLoginConfirmationFunction = (params?: {
770
949
  * Allow to make extra authorization
771
950
  */
772
951
  export type AdminUserAuthorizeFunction = ((params?: {
952
+ /**
953
+ * Admin user performing the action.
954
+ */
773
955
  adminUser: AdminUser;
956
+ /**
957
+ * HTTP response object.
958
+ *
959
+ * @deprecated Since 1.2.9. Will be removed in 2.0.0. Use extra.response instead.
960
+ */
774
961
  response: IAdminForthHttpResponse;
962
+ /**
963
+ * Adminforth instance.
964
+ */
775
965
  adminforth: IAdminForth;
966
+ /**
967
+ * Extra HTTP information. Prefer using extra.response over the top-level response field.
968
+ */
776
969
  extra?: HttpExtra;
777
970
  }) => Promise<{
778
971
  error?: string;