adminforth 1.13.0-next.8 → 1.13.0

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.
Files changed (59) hide show
  1. package/commands/createApp/templates/adminuser.ts.hbs +9 -7
  2. package/commands/createPlugin/templates/package.json.hbs +1 -1
  3. package/dist/dataConnectors/baseConnector.d.ts +1 -1
  4. package/dist/dataConnectors/baseConnector.d.ts.map +1 -1
  5. package/dist/dataConnectors/baseConnector.js +11 -4
  6. package/dist/dataConnectors/baseConnector.js.map +1 -1
  7. package/dist/dataConnectors/clickhouse.d.ts +1 -1
  8. package/dist/dataConnectors/clickhouse.d.ts.map +1 -1
  9. package/dist/dataConnectors/clickhouse.js +1 -0
  10. package/dist/dataConnectors/clickhouse.js.map +1 -1
  11. package/dist/dataConnectors/mongo.d.ts +1 -1
  12. package/dist/dataConnectors/mongo.d.ts.map +1 -1
  13. package/dist/dataConnectors/mongo.js +1 -0
  14. package/dist/dataConnectors/mongo.js.map +1 -1
  15. package/dist/dataConnectors/mysql.d.ts +1 -1
  16. package/dist/dataConnectors/mysql.d.ts.map +1 -1
  17. package/dist/dataConnectors/mysql.js +1 -0
  18. package/dist/dataConnectors/mysql.js.map +1 -1
  19. package/dist/dataConnectors/postgres.d.ts +1 -1
  20. package/dist/dataConnectors/postgres.d.ts.map +1 -1
  21. package/dist/dataConnectors/postgres.js +4 -2
  22. package/dist/dataConnectors/postgres.js.map +1 -1
  23. package/dist/dataConnectors/sqlite.d.ts +1 -1
  24. package/dist/dataConnectors/sqlite.d.ts.map +1 -1
  25. package/dist/dataConnectors/sqlite.js +1 -0
  26. package/dist/dataConnectors/sqlite.js.map +1 -1
  27. package/dist/index.d.ts +1 -0
  28. package/dist/index.d.ts.map +1 -1
  29. package/dist/index.js +2 -4
  30. package/dist/index.js.map +1 -1
  31. package/dist/modules/configValidator.d.ts +4 -0
  32. package/dist/modules/configValidator.d.ts.map +1 -1
  33. package/dist/modules/configValidator.js +55 -9
  34. package/dist/modules/configValidator.js.map +1 -1
  35. package/dist/modules/restApi.d.ts.map +1 -1
  36. package/dist/modules/restApi.js +81 -13
  37. package/dist/modules/restApi.js.map +1 -1
  38. package/dist/spa/src/afcl/Select.vue +27 -2
  39. package/dist/spa/src/components/ColumnValueInput.vue +8 -1
  40. package/dist/spa/src/components/ColumnValueInputWrapper.vue +75 -0
  41. package/dist/spa/src/components/GroupsTable.vue +15 -50
  42. package/dist/spa/src/components/ResourceForm.vue +18 -5
  43. package/dist/spa/src/components/ResourceListTable.vue +34 -6
  44. package/dist/spa/src/components/ShowTable.vue +11 -3
  45. package/dist/spa/src/components/SkeleteLoader.vue +11 -3
  46. package/dist/spa/src/components/ThreeDotsMenu.vue +17 -1
  47. package/dist/spa/src/components/ValueRenderer.vue +18 -2
  48. package/dist/spa/src/types/Back.ts +36 -5
  49. package/dist/spa/src/types/Common.ts +11 -1
  50. package/dist/spa/src/views/EditView.vue +7 -3
  51. package/dist/spa/src/views/ShowView.vue +17 -1
  52. package/dist/types/Back.d.ts +35 -4
  53. package/dist/types/Back.d.ts.map +1 -1
  54. package/dist/types/Back.js.map +1 -1
  55. package/dist/types/Common.d.ts +11 -2
  56. package/dist/types/Common.d.ts.map +1 -1
  57. package/dist/types/Common.js +1 -0
  58. package/dist/types/Common.js.map +1 -1
  59. package/package.json +1 -1
@@ -1,10 +1,10 @@
1
1
  <template>
2
2
  <div class="rounded-lg shadow-resourseFormShadow dark:shadow-darkResourseFormShadow dark:shadow-2xl">
3
- <div v-if="group.groupName" class="text-md font-semibold px-6 py-3 flex flex-1 items-center dark:border-gray-600 text-gray-700 bg-lightFormHeading dark:bg-gray-700 dark:text-gray-400 rounded-t-lg">
3
+ <div v-if="group.groupName && !group.noTitle" class="text-md font-semibold px-6 py-3 flex flex-1 items-center dark:border-gray-600 text-gray-700 bg-lightFormHeading dark:bg-gray-700 dark:text-gray-400 rounded-t-lg">
4
4
  {{ group.groupName }}
5
5
  </div>
6
6
  <table class="w-full text-sm text-left rtl:text-right text-gray-500 dark:text-gray-400">
7
- <thead class="text-xs text-gray-700 uppercase dark:text-gray-400 bg-lightFormHeading dark:bg-gray-700 block md:table-row-group ">
7
+ <thead v-if="!allColumnsHaveCustomComponent" class="text-xs text-gray-700 uppercase dark:text-gray-400 bg-lightFormHeading dark:bg-gray-700 block md:table-row-group ">
8
8
  <tr>
9
9
  <th scope="col" :class="{'rounded-tl-lg': !group.groupName}" class="px-6 py-3 hidden md:w-52 md:table-cell">
10
10
  {{ $t('Field') }}
@@ -42,50 +42,17 @@
42
42
  class="px-6 py-4 whitespace-pre-wrap relative block md:table-cell"
43
43
  :class="{'rounded-br-lg': i === group.columns.length - 1}"
44
44
  >
45
- <template v-if="column.isArray?.enabled">
46
- <ColumnValueInput
47
- v-for="(arrayItemValue, arrayItemIndex) in currentValues[column.name]"
48
- :key="`${column.name}-${arrayItemIndex}`"
49
- ref="arrayItemRefs"
50
- :class="{'mt-2': arrayItemIndex}"
51
- :source="source"
52
- :column="column"
53
- :type="column.isArray.itemType"
54
- :value="arrayItemValue"
55
- :currentValues="currentValues"
56
- :mode="mode"
57
- :columnOptions="columnOptions"
58
- :deletable="!column.editReadonly"
59
- @update:modelValue="setCurrentValue(column.name, $event, arrayItemIndex)"
60
- @update:unmasked="unmasked[column.name] = !unmasked[column.name]"
61
- @update:inValidity="customComponentsInValidity[column.name] = $event"
62
- @update:emptiness="customComponentsEmptiness[column.name] = $event"
63
- @delete="setCurrentValue(column.name, currentValues[column.name].filter((_, index) => index !== arrayItemIndex))"
64
- />
65
- <button
66
- v-if="!column.editReadonly"
67
- type="button"
68
- @click="setCurrentValue(column.name, currentValues[column.name], currentValues[column.name].length); focusOnLastInput(column.name)"
69
- class="flex items-center py-1 px-3 me-2 text-sm font-medium rounded-default text-gray-900 focus:outline-none bg-white rounded border border-gray-300 hover:bg-gray-100 hover:text-lightPrimary focus:z-10 focus:ring-4 focus:ring-gray-100 dark:focus:ring-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700"
70
- :class="{'mt-2': currentValues[column.name].length}"
71
- >
72
- <IconPlusOutline class="w-4 h-4 me-2"/>
73
- {{ $t('Add') }}
74
- </button>
75
- </template>
76
- <ColumnValueInput
77
- v-else
45
+ <ColumnValueInputWrapper
78
46
  :source="source"
79
47
  :column="column"
80
- :value="currentValues[column.name]"
81
48
  :currentValues="currentValues"
82
49
  :mode="mode"
83
50
  :columnOptions="columnOptions"
84
51
  :unmasked="unmasked"
85
- @update:modelValue="setCurrentValue(column.name, $event)"
86
- @update:unmasked="unmasked[column.name] = !unmasked[column.name]"
87
- @update:inValidity="customComponentsInValidity[column.name] = $event"
88
- @update:emptiness="customComponentsEmptiness[column.name] = $event"
52
+ :setCurrentValue="setCurrentValue"
53
+ @update:unmasked="unmasked[$event] = !unmasked[$event]"
54
+ @update:inValidity="customComponentsInValidity[$event.name] = $event.value"
55
+ @update:emptiness="customComponentsEmptiness[$event.name] = $event.value"
89
56
  />
90
57
  <div v-if="columnError(column) && validating" class="mt-1 text-xs text-red-500 dark:text-red-400">{{ columnError(column) }}</div>
91
58
  <div v-if="column.editingNote && column.editingNote[mode]" class="mt-1 text-xs text-gray-400 dark:text-gray-500">{{ column.editingNote[mode] }}</div>
@@ -98,10 +65,10 @@
98
65
 
99
66
  <script setup lang="ts">
100
67
  import { IconExclamationCircleSolid, IconPlusOutline } from '@iconify-prerendered/vue-flowbite';
101
- import ColumnValueInput from "@/components/ColumnValueInput.vue";
102
68
  import { Tooltip } from '@/afcl';
103
69
  import { ref, computed, watch, nextTick, type Ref } from 'vue';
104
70
  import { useI18n } from 'vue-i18n';
71
+ import ColumnValueInputWrapper from "@/components/ColumnValueInputWrapper.vue";
105
72
 
106
73
  const { t } = useI18n();
107
74
 
@@ -117,19 +84,17 @@
117
84
  columnOptions: any,
118
85
  }>();
119
86
 
120
- const arrayItemRefs = ref([]);
121
-
122
87
  const customComponentsInValidity: Ref<Record<string, boolean>> = ref({});
123
88
  const customComponentsEmptiness: Ref<Record<string, boolean>> = ref({});
89
+ const allColumnsHaveCustomComponent = computed(() => {
90
+ return props.group.columns.every(column => {
91
+ const componentKey = `${props.source}Row` as keyof typeof column.components;
92
+ return column.components?.[componentKey];
93
+ });
94
+ });
124
95
 
125
96
  const emit = defineEmits(['update:customComponentsInValidity', 'update:customComponentsEmptiness']);
126
97
 
127
- async function focusOnLastInput(column) {
128
- // wait for element to register
129
- await nextTick();
130
- arrayItemRefs.value[arrayItemRefs.value.length - 1].focus();
131
- }
132
-
133
98
  watch(customComponentsInValidity.value, (newVal) => {
134
99
  emit('update:customComponentsInValidity', newVal);
135
100
  });
@@ -138,4 +103,4 @@
138
103
  emit('update:customComponentsEmptiness', newVal);
139
104
  });
140
105
 
141
- </script>
106
+ </script>
@@ -190,8 +190,12 @@ const setCurrentValue = (key, value, index=null) => {
190
190
  } else if (index === currentValues.value[key].length) {
191
191
  currentValues.value[key].push(null);
192
192
  } else {
193
- if (['integer', 'float'].includes(col.isArray.itemType) && (value || value === 0)) {
194
- currentValues.value[key][index] = +value;
193
+ if (['integer', 'float', 'decimal'].includes(col.isArray.itemType)) {
194
+ if (value || value === 0) {
195
+ currentValues.value[key][index] = +value;
196
+ } else {
197
+ currentValues.value[key][index] = null;
198
+ }
195
199
  } else {
196
200
  currentValues.value[key][index] = value;
197
201
  }
@@ -200,8 +204,12 @@ const setCurrentValue = (key, value, index=null) => {
200
204
  }
201
205
  }
202
206
  } else {
203
- if (['integer', 'float'].includes(col.type) && (value || value === 0)) {
204
- currentValues.value[key] = +value;
207
+ if (['integer', 'float', 'decimal'].includes(col.type)) {
208
+ if (value || value === 0) {
209
+ currentValues.value[key] = +value;
210
+ } else {
211
+ currentValues.value[key] = null;
212
+ }
205
213
  } else {
206
214
  currentValues.value[key] = value;
207
215
  }
@@ -237,7 +245,12 @@ onMounted(() => {
237
245
  currentValues.value[column.name] = [];
238
246
  } else {
239
247
  // else copy array to prevent mutation
240
- currentValues.value[column.name] = [...currentValues.value[column.name]];
248
+ if (Array.isArray(currentValues.value[column.name])) {
249
+ currentValues.value[column.name] = [...currentValues.value[column.name]];
250
+ } else {
251
+ // fallback for old data
252
+ currentValues.value[column.name] = [`${currentValues.value[column.name]}`];
253
+ }
241
254
  }
242
255
  } else if (currentValues.value[column.name]) {
243
256
  currentValues.value[column.name] = JSON.stringify(currentValues.value[column.name], null, 2);
@@ -18,15 +18,16 @@
18
18
  <!-- table header -->
19
19
  <tr class="t-header sticky z-10 top-0 text-xs bg-lightListTableHeading dark:bg-darkListTableHeading dark:text-gray-400">
20
20
  <td scope="col" class="p-4">
21
- <div v-if="rows && rows.length" class="flex items-center">
21
+ <div class="flex items-center">
22
22
  <input id="checkbox-all-search" type="checkbox" :checked="allFromThisPageChecked" @change="selectAll()"
23
- class="w-4 h-4 cursor-pointer text-blue-600 bg-gray-100 border-gray-300 rounded
23
+ :disabled="!rows || !rows.length"
24
+ class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded
24
25
  focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 dark:focus:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600">
25
26
  <label for="checkbox-all-search" class="sr-only">{{ $t('checkbox') }}</label>
26
27
  </div>
27
28
  </td>
28
29
 
29
- <td v-for="c in columnsListed" scope="col" class="px-2 md:px-3 lg:px-6 py-3">
30
+ <td v-for="c in columnsListed" ref="headerRefs" scope="col" class="px-2 md:px-3 lg:px-6 py-3">
30
31
 
31
32
  <div @click="(evt) => c.sortable && onSortButtonClick(evt, c.name)"
32
33
  class="flex items-center " :class="{'cursor-pointer':c.sortable}">
@@ -65,6 +66,7 @@
65
66
  :columns="resource?.columns.filter(c => c.showIn.list).length + 2"
66
67
  :rows="rowHeights.length || 3"
67
68
  :row-heights="rowHeights"
69
+ :column-widths="columnWidths"
68
70
  />
69
71
  <tr v-else-if="rows.length === 0" class="bg-lightListTable dark:bg-darkListTable dark:border-darkListTableBorder">
70
72
  <td :colspan="resource?.columns.length + 2">
@@ -178,7 +180,7 @@
178
180
  <button
179
181
  @click="startCustomAction(action.id, row)"
180
182
  >
181
- <component v-if="action.icon" :is="getIcon(action.icon)" class="w-5 h-5 text-lightPrimary dark:text-darkPrimary"></component>
183
+ <component v-if="action.icon" :is="getIcon(action.icon)" class="w-5 h-5 mr-2 text-lightPrimary dark:text-darkPrimary"></component>
182
184
  </button>
183
185
  <template v-slot:tooltip>
184
186
  {{ action.name }}
@@ -376,10 +378,13 @@ watch(() => props.page, (newPage) => {
376
378
  });
377
379
 
378
380
  const rowRefs = useTemplateRef('rowRefs');
381
+ const headerRefs = useTemplateRef('headerRefs');
379
382
  const rowHeights = ref([]);
383
+ const columnWidths = ref([]);
380
384
  watch(() => props.rows, (newRows) => {
381
385
  // rows are set to null when new records are loading
382
386
  rowHeights.value = newRows || !rowRefs.value ? [] : rowRefs.value.map((el) => el.offsetHeight);
387
+ columnWidths.value = newRows || !headerRefs.value ? [] : [48, ...headerRefs.value.map((el) => el.offsetWidth)];
383
388
  });
384
389
 
385
390
  function addToCheckedValues(id) {
@@ -411,7 +416,7 @@ async function selectAll(value) {
411
416
  const totalPages = computed(() => Math.ceil(props.totalRows / props.pageSize));
412
417
 
413
418
  const allFromThisPageChecked = computed(() => {
414
- if (!props.rows) return false;
419
+ if (!props.rows || !props.rows.length) return false;
415
420
  return props.rows.every((r) => checkboxesInternal.value.includes(r._primaryKeyValue));
416
421
  });
417
422
  const ascArr = computed(() => sort.value.filter((s) => s.direction === 'asc').map((s) => s.field));
@@ -536,6 +541,20 @@ async function startCustomAction(actionId, row) {
536
541
 
537
542
  actionLoadingStates.value[actionId] = false;
538
543
 
544
+ if (data?.redirectUrl) {
545
+ // Check if the URL should open in a new tab
546
+ if (data.redirectUrl.includes('target=_blank')) {
547
+ window.open(data.redirectUrl.replace('&target=_blank', '').replace('?target=_blank', ''), '_blank');
548
+ } else {
549
+ // Navigate within the app
550
+ if (data.redirectUrl.startsWith('http')) {
551
+ window.location.href = data.redirectUrl;
552
+ } else {
553
+ router.push(data.redirectUrl);
554
+ }
555
+ }
556
+ return;
557
+ }
539
558
  if (data?.ok) {
540
559
  emits('update:records', true);
541
560
 
@@ -553,4 +572,13 @@ async function startCustomAction(actionId, row) {
553
572
  }
554
573
 
555
574
 
556
- </script>
575
+ </script>
576
+
577
+ <style lang="scss" scoped>
578
+ input[type="checkbox"][disabled] {
579
+ @apply opacity-50;
580
+ }
581
+ input[type="checkbox"]:not([disabled]) {
582
+ @apply cursor-pointer;
583
+ }
584
+ </style>
@@ -1,10 +1,10 @@
1
1
  <template>
2
2
  <div class="overflow-x-auto rounded-default shadow-resourseFormShadow dark:shadow-darkResourseFormShadow">
3
- <div v-if="groupName" class="text-md font-semibold px-6 py-3 flex flex-1 items-center dark:border-gray-600 text-gray-700 bg-lightFormHeading dark:bg-gray-700 dark:text-gray-400 rounded-t-lg">
3
+ <div v-if="groupName && !noTitle" class="text-md font-semibold px-6 py-3 flex flex-1 items-center dark:border-gray-600 text-gray-700 bg-lightFormHeading dark:bg-gray-700 dark:text-gray-400 rounded-t-lg">
4
4
  {{ groupName }}
5
5
  </div>
6
6
  <table class="w-full text-sm text-left rtl:text-right text-gray-500 dark:text-gray-400 table-fixed">
7
- <thead class="text-gray-700 dark:text-gray-400 bg-lightFormHeading dark:bg-gray-700 block md:table-row-group">
7
+ <thead v-if="!allColumnsHaveCustomComponent" class="text-gray-700 dark:text-gray-400 bg-lightFormHeading dark:bg-gray-700 block md:table-row-group">
8
8
  <tr>
9
9
  <th scope="col" class="px-6 py-3 text-xs uppercase hidden md:w-52 md:table-cell">
10
10
  {{ $t('Field') }}
@@ -59,7 +59,8 @@
59
59
  import ValueRenderer from '@/components/ValueRenderer.vue';
60
60
  import { getCustomComponent } from '@/utils';
61
61
  import { useCoreStore } from '@/stores/core';
62
- defineProps<{
62
+ import { computed } from 'vue';
63
+ const props = defineProps<{
63
64
  columns: Array<{
64
65
  name: string;
65
66
  label: string;
@@ -74,10 +75,17 @@
74
75
  };
75
76
  };
76
77
  }>;
78
+ source: string;
77
79
  groupName?: string | null;
80
+ noTitle?: boolean;
78
81
  resource: Record<string, any>;
79
82
  record: Record<string, any>;
80
83
  }>();
81
84
 
82
85
  const coreStore = useCoreStore();
86
+ const allColumnsHaveCustomComponent = computed(() => {
87
+ return props.columns.every(column => {
88
+ return column.components?.showRow;
89
+ });
90
+ });
83
91
  </script>
@@ -1,10 +1,16 @@
1
1
  <template>
2
2
  <tr
3
3
  v-for="(r, ri) in new Array(props.rows)"
4
- class="bg-lightListTable border-b dark:bg-darkListTable dark:border-darkListBorder"
5
- :style="[props.rowHeights[ri] !== undefined ? `height: ${props.rowHeights[ri]}px` : '' ]"
4
+ class="bg-lightListTable dark:bg-darkListTable dark:border-darkListBorder"
5
+ :class="{'border-b': ri !== props.rows - 1}"
6
+ :style="[`height: ${props.rowHeights[ri] !== undefined ? props.rowHeights[ri] : 52.5}px`]"
6
7
  >
7
- <td v-for="c in new Array(props.columns)" class="items-center px-6 py-8 cursor-default" >
8
+ <td
9
+ v-for="(c, ci) in new Array(props.columns)" class="items-center px-6 py-4 cursor-default"
10
+ :style="[props.columnWidths[ci] !== undefined
11
+ ? `min-width: ${props.columnWidths[ci]}px; width: ${props.columnWidths[ci]}px;`
12
+ : '']"
13
+ >
8
14
  <div role="status" class="max-w-sm animate-pulse">
9
15
  <div class="h-2 bg-gray-200 rounded-full dark:bg-gray-700 max-w-[360px]"></div>
10
16
  </div>
@@ -18,8 +24,10 @@ const props = withDefaults(defineProps<{
18
24
  columns: number;
19
25
  rows: number;
20
26
  rowHeights?: number[];
27
+ columnWidths?: number[];
21
28
  }>(), {
22
29
  rowHeights: [],
30
+ columnWidths: [],
23
31
  });
24
32
 
25
33
  </script>
@@ -46,10 +46,11 @@ import { getCustomComponent, getIcon } from '@/utils';
46
46
  import { useCoreStore } from '@/stores/core';
47
47
  import adminforth from '@/adminforth';
48
48
  import { callAdminForthApi } from '@/utils';
49
- import { useRoute } from 'vue-router';
49
+ import { useRoute, useRouter } from 'vue-router';
50
50
 
51
51
  const route = useRoute();
52
52
  const coreStore = useCoreStore();
53
+ const router = useRouter();
53
54
 
54
55
  const props = defineProps({
55
56
  threeDotsDropdownItems: Array,
@@ -69,6 +70,21 @@ async function handleActionClick(action) {
69
70
  recordId: route.params.primaryKey
70
71
  }
71
72
  });
73
+
74
+ if (data?.redirectUrl) {
75
+ // Check if the URL should open in a new tab
76
+ if (data.redirectUrl.includes('target=_blank')) {
77
+ window.open(data.redirectUrl.replace('&target=_blank', '').replace('?target=_blank', ''), '_blank');
78
+ } else {
79
+ // Navigate within the app
80
+ if (data.redirectUrl.startsWith('http')) {
81
+ window.location.href = data.redirectUrl;
82
+ } else {
83
+ router.push(data.redirectUrl);
84
+ }
85
+ }
86
+ return;
87
+ }
72
88
 
73
89
  if (data?.ok) {
74
90
  await coreStore.fetchRecord({
@@ -1,7 +1,23 @@
1
1
  <template>
2
2
  <div>
3
- <span @click="(e)=>{e.stopPropagation()}" v-if="column.foreignResource">
4
- <RouterLink v-if="record[column.name]" class="font-medium text-lightPrimary dark:text-darkPrimary hover:brightness-110 whitespace-nowrap"
3
+ <span
4
+ v-if="column.foreignResource"
5
+ :class="{'flex flex-wrap': column.isArray?.enabled}"
6
+ @click="(e)=>{e.stopPropagation()}"
7
+ >
8
+ <span
9
+ v-if="record[column.name] && column.isArray?.enabled"
10
+ v-for="foreignResource in record[column.name]"
11
+ class="rounded-md m-0.5 bg-lightAnnouncementBG dark:bg-darkAnnouncementBG text-lightAnnouncementText dark:text-darkAnnouncementText py-0.5 px-2.5 text-sm"
12
+ >
13
+ <RouterLink
14
+ class="font-medium text-lightSidebarText dark:text-darkSidebarText hover:brightness-110 whitespace-nowrap"
15
+ :to="{ name: 'resource-show', params: { primaryKey: foreignResource.pk, resourceId: column.foreignResource.resourceId || column.foreignResource.polymorphicResources.find((pr) => pr.whenValue === record[column.foreignResource.polymorphicOn]).resourceId } }"
16
+ >
17
+ {{ foreignResource.label }}
18
+ </RouterLink>
19
+ </span>
20
+ <RouterLink v-else-if="record[column.name]" class="font-medium text-lightPrimary dark:text-darkPrimary hover:brightness-110 whitespace-nowrap"
5
21
  :to="{ name: 'resource-show', params: { primaryKey: record[column.name].pk, resourceId: column.foreignResource.resourceId || column.foreignResource.polymorphicResources.find((pr) => pr.whenValue === record[column.foreignResource.polymorphicOn]).resourceId } }">
6
22
  {{ record[column.name].label }}
7
23
  </RouterLink>
@@ -206,9 +206,9 @@ export interface IAdminForthDataSourceConnector {
206
206
 
207
207
 
208
208
  /**
209
- * Used to create record in database.
209
+ * Used to create record in database. Should return value of primary key column of created record.
210
210
  */
211
- createRecordOriginalValues({ resource, record }: { resource: AdminForthResource, record: any }): Promise<void>;
211
+ createRecordOriginalValues({ resource, record }: { resource: AdminForthResource, record: any }): Promise<string>;
212
212
 
213
213
  /**
214
214
  * Update record in database. newValues might have not all fields in record, but only changed ones.
@@ -727,7 +727,12 @@ export interface AdminForthActionInput {
727
727
  showButton?: boolean,
728
728
  showThreeDotsMenu?: boolean,
729
729
  };
730
- action: (params: {
730
+ allowed?: (params: {
731
+ adminUser: AdminUser;
732
+ standardAllowedActions: AllowedActions;
733
+ }) => boolean;
734
+ url?: string;
735
+ action?: (params: {
731
736
  adminforth: IAdminForth;
732
737
  resource: AdminForthResource;
733
738
  recordId: string;
@@ -1146,8 +1151,6 @@ export interface ResourceOptionsInput extends Omit<AdminForthResourceCommon['opt
1146
1151
  */
1147
1152
  bulkActions?: Array<AdminForthBulkAction>,
1148
1153
 
1149
- actions?: Array<AdminForthActionInput>,
1150
-
1151
1154
  /**
1152
1155
  * Allowed actions for resource.
1153
1156
  *
@@ -1165,10 +1168,38 @@ export interface ResourceOptionsInput extends Omit<AdminForthResourceCommon['opt
1165
1168
  *
1166
1169
  */
1167
1170
  allowedActions?: AllowedActionsInput,
1171
+
1172
+ /**
1173
+ * Array of actions which will be displayed in the resource.
1174
+ *
1175
+ * Example:
1176
+ *
1177
+ * ```ts
1178
+ * actions: [
1179
+ * {
1180
+ * name: 'Auto submit',
1181
+ * allowed: ({ adminUser, standardAllowedActions }) => {
1182
+ * return adminUser.dbUser.role === 'superadmin';
1183
+ * },
1184
+ * action: ({ adminUser, resource, recordId, adminforth, extra, tr }) => {
1185
+ * console.log("auto submit", recordId, adminUser);
1186
+ * return { ok: true, successMessage: "Auto submitted" };
1187
+ * },
1188
+ * showIn: {
1189
+ * list: true,
1190
+ * showButton: true,
1191
+ * showThreeDotsMenu: true,
1192
+ * },
1193
+ * },
1194
+ * ]
1195
+ * ```
1196
+ */
1197
+ actions?: Array<AdminForthActionInput>,
1168
1198
  };
1169
1199
 
1170
1200
  export interface ResourceOptions extends Omit<ResourceOptionsInput, 'allowedActions'> {
1171
1201
  allowedActions: AllowedActions,
1202
+ actions?: Array<AdminForthActionInput>,
1172
1203
  }
1173
1204
 
1174
1205
  /**
@@ -50,6 +50,7 @@ export enum ActionCheckSource {
50
50
  CreateRequest = 'createRequest',
51
51
  DeleteRequest = 'deleteRequest',
52
52
  BulkActionRequest = 'bulkActionRequest',
53
+ CustomActionRequest = 'customActionRequest',
53
54
  }
54
55
 
55
56
  export enum AllowedActionsEnum {
@@ -357,18 +358,22 @@ export interface AdminForthResourceInputCommon {
357
358
  fieldGroups?: {
358
359
  groupName: string;
359
360
  columns: string[];
361
+ noTitle?: boolean;
360
362
  }[];
361
363
  createFieldGroups?: {
362
364
  groupName: string;
363
365
  columns: string[];
366
+ noTitle?: boolean;
364
367
  }[];
365
368
  editFieldGroups?: {
366
369
  groupName: string;
367
370
  columns: string[];
371
+ noTitle?: boolean;
368
372
  }[];
369
373
  showFieldGroups?: {
370
374
  groupName: string;
371
375
  columns: string[];
376
+ noTitle?: boolean;
372
377
  }[];
373
378
 
374
379
  /**
@@ -551,6 +556,11 @@ export interface AdminForthForeignResourceCommon {
551
556
  unsetLabel?: string,
552
557
  }
553
558
 
559
+ export type FillOnCreateFunction = (params: {
560
+ initialRecord: any,
561
+ adminUser: AdminUser,
562
+ }) => any;
563
+
554
564
  /**
555
565
  * Column describes one field in the table or collection in database.
556
566
  */
@@ -681,7 +691,7 @@ export interface AdminForthResourceColumnInputCommon {
681
691
  /**
682
692
  * Called on the backend when the record is saved to a database. Value returned by `fillOnCreate` will be saved to the database.
683
693
  */
684
- fillOnCreate?: Function,
694
+ fillOnCreate?: FillOnCreateFunction,
685
695
 
686
696
  /**
687
697
  * Single value that will be substituted in create form. User can change it before saving the record.
@@ -107,7 +107,11 @@ const editableRecord = computed(() => {
107
107
  }
108
108
  coreStore.resource.columns.forEach(column => {
109
109
  if (column.foreignResource) {
110
- newRecord[column.name] = newRecord[column.name]?.pk
110
+ if (column.isArray?.enabled) {
111
+ newRecord[column.name] = newRecord[column.name]?.map(fr => fr.pk);
112
+ } else {
113
+ newRecord[column.name] = newRecord[column.name]?.pk;
114
+ }
111
115
  }
112
116
  });
113
117
  return newRecord;
@@ -145,7 +149,7 @@ async function saveRecord() {
145
149
 
146
150
  const column = coreStore.resource.columns.find((c) => c.name === key);
147
151
  if (column?.foreignResource) {
148
- columnIsUpdated = record.value[key] !== coreStore.record[key].pk;
152
+ columnIsUpdated = record.value[key] !== coreStore.record[key]?.pk;
149
153
  }
150
154
 
151
155
  if (columnIsUpdated) {
@@ -172,7 +176,7 @@ async function saveRecord() {
172
176
  });
173
177
  }
174
178
  saving.value = false;
175
- router.push({ name: 'resource-show', params: { resourceId: route.params.resourceId, primaryKey: coreStore.record[coreStore.primaryKey] } });
179
+ router.push({ name: 'resource-show', params: { resourceId: route.params.resourceId, primaryKey: resp.recordId } });
176
180
  }
177
181
 
178
182
  </script>
@@ -83,11 +83,12 @@
83
83
  :record="coreStore.record"
84
84
  />
85
85
  </div>
86
- <template v-else>
86
+ <template v-else>
87
87
  <template v-for="group in groups" :key="group.groupName">
88
88
  <ShowTable
89
89
  :columns="group.columns"
90
90
  :groupName="group.groupName"
91
+ :noTitle="group.noTitle"
91
92
  :resource="coreStore.resource"
92
93
  :record="coreStore.record"
93
94
  />
@@ -245,6 +246,21 @@ async function startCustomAction(actionId) {
245
246
 
246
247
  actionLoadingStates.value[actionId] = false;
247
248
 
249
+ if (data?.redirectUrl) {
250
+ // Check if the URL should open in a new tab
251
+ if (data.redirectUrl.includes('target=_blank')) {
252
+ window.open(data.redirectUrl.replace('&target=_blank', '').replace('?target=_blank', ''), '_blank');
253
+ } else {
254
+ // Navigate within the app
255
+ if (data.redirectUrl.startsWith('http')) {
256
+ window.location.href = data.redirectUrl;
257
+ } else {
258
+ router.push(data.redirectUrl);
259
+ }
260
+ }
261
+ return;
262
+ }
263
+
248
264
  if (data?.ok) {
249
265
  await coreStore.fetchRecord({
250
266
  resourceId: route.params.resourceId,
@@ -169,12 +169,12 @@ export interface IAdminForthDataSourceConnector {
169
169
  };
170
170
  }>;
171
171
  /**
172
- * Used to create record in database.
172
+ * Used to create record in database. Should return value of primary key column of created record.
173
173
  */
174
174
  createRecordOriginalValues({ resource, record }: {
175
175
  resource: AdminForthResource;
176
176
  record: any;
177
- }): Promise<void>;
177
+ }): Promise<string>;
178
178
  /**
179
179
  * Update record in database. newValues might have not all fields in record, but only changed ones.
180
180
  * recordId is value of field which is marked as {@link AdminForthResourceColumn.primaryKey}
@@ -697,7 +697,12 @@ export interface AdminForthActionInput {
697
697
  showButton?: boolean;
698
698
  showThreeDotsMenu?: boolean;
699
699
  };
700
- action: (params: {
700
+ allowed?: (params: {
701
+ adminUser: AdminUser;
702
+ standardAllowedActions: AllowedActions;
703
+ }) => boolean;
704
+ url?: string;
705
+ action?: (params: {
701
706
  adminforth: IAdminForth;
702
707
  resource: AdminForthResource;
703
708
  recordId: string;
@@ -1025,7 +1030,6 @@ export interface ResourceOptionsInput extends Omit<AdminForthResourceCommon['opt
1025
1030
  * using checkboxes.
1026
1031
  */
1027
1032
  bulkActions?: Array<AdminForthBulkAction>;
1028
- actions?: Array<AdminForthActionInput>;
1029
1033
  /**
1030
1034
  * Allowed actions for resource.
1031
1035
  *
@@ -1043,9 +1047,36 @@ export interface ResourceOptionsInput extends Omit<AdminForthResourceCommon['opt
1043
1047
  *
1044
1048
  */
1045
1049
  allowedActions?: AllowedActionsInput;
1050
+ /**
1051
+ * Array of actions which will be displayed in the resource.
1052
+ *
1053
+ * Example:
1054
+ *
1055
+ * ```ts
1056
+ * actions: [
1057
+ * {
1058
+ * name: 'Auto submit',
1059
+ * allowed: ({ adminUser, standardAllowedActions }) => {
1060
+ * return adminUser.dbUser.role === 'superadmin';
1061
+ * },
1062
+ * action: ({ adminUser, resource, recordId, adminforth, extra, tr }) => {
1063
+ * console.log("auto submit", recordId, adminUser);
1064
+ * return { ok: true, successMessage: "Auto submitted" };
1065
+ * },
1066
+ * showIn: {
1067
+ * list: true,
1068
+ * showButton: true,
1069
+ * showThreeDotsMenu: true,
1070
+ * },
1071
+ * },
1072
+ * ]
1073
+ * ```
1074
+ */
1075
+ actions?: Array<AdminForthActionInput>;
1046
1076
  }
1047
1077
  export interface ResourceOptions extends Omit<ResourceOptionsInput, 'allowedActions'> {
1048
1078
  allowedActions: AllowedActions;
1079
+ actions?: Array<AdminForthActionInput>;
1049
1080
  }
1050
1081
  /**
1051
1082
  * Resource describes one table or collection in database.