adminforth 1.13.0-next.4 → 1.13.0-next.40

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 (71) hide show
  1. package/commands/createApp/templates/adminuser.ts.hbs +9 -7
  2. package/commands/createApp/templates/index.ts.hbs +0 -1
  3. package/commands/createPlugin/templates/package.json.hbs +1 -1
  4. package/dist/dataConnectors/baseConnector.d.ts +10 -14
  5. package/dist/dataConnectors/baseConnector.d.ts.map +1 -1
  6. package/dist/dataConnectors/baseConnector.js +76 -30
  7. package/dist/dataConnectors/baseConnector.js.map +1 -1
  8. package/dist/dataConnectors/clickhouse.d.ts +14 -23
  9. package/dist/dataConnectors/clickhouse.d.ts.map +1 -1
  10. package/dist/dataConnectors/clickhouse.js +68 -27
  11. package/dist/dataConnectors/clickhouse.js.map +1 -1
  12. package/dist/dataConnectors/mongo.d.ts +12 -16
  13. package/dist/dataConnectors/mongo.d.ts.map +1 -1
  14. package/dist/dataConnectors/mongo.js +15 -11
  15. package/dist/dataConnectors/mongo.js.map +1 -1
  16. package/dist/dataConnectors/mysql.d.ts +8 -13
  17. package/dist/dataConnectors/mysql.d.ts.map +1 -1
  18. package/dist/dataConnectors/mysql.js +45 -26
  19. package/dist/dataConnectors/mysql.js.map +1 -1
  20. package/dist/dataConnectors/postgres.d.ts +8 -13
  21. package/dist/dataConnectors/postgres.d.ts.map +1 -1
  22. package/dist/dataConnectors/postgres.js +45 -28
  23. package/dist/dataConnectors/postgres.js.map +1 -1
  24. package/dist/dataConnectors/sqlite.d.ts +7 -4
  25. package/dist/dataConnectors/sqlite.d.ts.map +1 -1
  26. package/dist/dataConnectors/sqlite.js +48 -22
  27. package/dist/dataConnectors/sqlite.js.map +1 -1
  28. package/dist/index.d.ts +1 -0
  29. package/dist/index.d.ts.map +1 -1
  30. package/dist/index.js +5 -7
  31. package/dist/index.js.map +1 -1
  32. package/dist/modules/codeInjector.d.ts.map +1 -1
  33. package/dist/modules/codeInjector.js +8 -1
  34. package/dist/modules/codeInjector.js.map +1 -1
  35. package/dist/modules/configValidator.d.ts +5 -0
  36. package/dist/modules/configValidator.d.ts.map +1 -1
  37. package/dist/modules/configValidator.js +88 -7
  38. package/dist/modules/configValidator.js.map +1 -1
  39. package/dist/modules/operationalResource.d.ts +4 -4
  40. package/dist/modules/operationalResource.d.ts.map +1 -1
  41. package/dist/modules/operationalResource.js +16 -2
  42. package/dist/modules/operationalResource.js.map +1 -1
  43. package/dist/modules/restApi.d.ts.map +1 -1
  44. package/dist/modules/restApi.js +182 -56
  45. package/dist/modules/restApi.js.map +1 -1
  46. package/dist/spa/src/afcl/Dialog.vue +13 -0
  47. package/dist/spa/src/afcl/Select.vue +27 -2
  48. package/dist/spa/src/components/ColumnValueInput.vue +12 -5
  49. package/dist/spa/src/components/ColumnValueInputWrapper.vue +77 -0
  50. package/dist/spa/src/components/Filters.vue +16 -8
  51. package/dist/spa/src/components/GroupsTable.vue +15 -50
  52. package/dist/spa/src/components/ResourceForm.vue +19 -6
  53. package/dist/spa/src/components/ResourceListTable.vue +87 -7
  54. package/dist/spa/src/components/ShowTable.vue +11 -3
  55. package/dist/spa/src/components/SkeleteLoader.vue +11 -3
  56. package/dist/spa/src/components/ThreeDotsMenu.vue +75 -7
  57. package/dist/spa/src/components/ValueRenderer.vue +18 -2
  58. package/dist/spa/src/types/Back.ts +88 -20
  59. package/dist/spa/src/types/Common.ts +26 -2
  60. package/dist/spa/src/views/EditView.vue +7 -3
  61. package/dist/spa/src/views/ListView.vue +5 -2
  62. package/dist/spa/src/views/ShowView.vue +76 -1
  63. package/dist/types/Back.d.ts +80 -20
  64. package/dist/types/Back.d.ts.map +1 -1
  65. package/dist/types/Back.js +6 -0
  66. package/dist/types/Back.js.map +1 -1
  67. package/dist/types/Common.d.ts +26 -4
  68. package/dist/types/Common.d.ts.map +1 -1
  69. package/dist/types/Common.js +3 -0
  70. package/dist/types/Common.js.map +1 -1
  71. package/package.json +1 -1
@@ -1,5 +1,5 @@
1
1
  <template >
2
- <template v-if="threeDotsDropdownItems?.length">
2
+ <template v-if="threeDotsDropdownItems?.length || customActions?.length">
3
3
  <button
4
4
  data-dropdown-toggle="listThreeDotsDropdown"
5
5
  class="flex items-center py-2 px-2 text-sm font-medium 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 rounded-default"
@@ -23,6 +23,18 @@
23
23
  />
24
24
  </a>
25
25
  </li>
26
+ <li v-for="action in customActions" :key="action.id">
27
+ <a href="#" @click.prevent="handleActionClick(action)" class="block px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white">
28
+ <div class="flex items-center gap-2">
29
+ <component
30
+ v-if="action.icon"
31
+ :is="getIcon(action.icon)"
32
+ class="w-4 h-4 text-lightPrimary dark:text-darkPrimary"
33
+ />
34
+ {{ action.name }}
35
+ </div>
36
+ </a>
37
+ </li>
26
38
  </ul>
27
39
  </div>
28
40
  </template>
@@ -30,14 +42,70 @@
30
42
 
31
43
 
32
44
  <script setup lang="ts">
45
+ import { getCustomComponent, getIcon } from '@/utils';
46
+ import { useCoreStore } from '@/stores/core';
47
+ import adminforth from '@/adminforth';
48
+ import { callAdminForthApi } from '@/utils';
49
+ import { useRoute, useRouter } from 'vue-router';
50
+
51
+ const route = useRoute();
52
+ const coreStore = useCoreStore();
53
+ const router = useRouter();
33
54
 
34
- import { getCustomComponent } from '@/utils';
35
- import { useCoreStore } from '@/stores/core'
55
+ const props = defineProps({
56
+ threeDotsDropdownItems: Array,
57
+ customActions: Array
58
+ });
36
59
 
37
- const coreStore = useCoreStore()
60
+ async function handleActionClick(action) {
61
+ adminforth.list.closeThreeDotsDropdown();
62
+
63
+ const actionId = action.id;
64
+ const data = await callAdminForthApi({
65
+ path: '/start_custom_action',
66
+ method: 'POST',
67
+ body: {
68
+ resourceId: route.params.resourceId,
69
+ actionId: actionId,
70
+ recordId: route.params.primaryKey
71
+ }
72
+ });
38
73
 
39
- const props = defineProps<{
40
- threeDotsDropdownItems: any[] | undefined
41
- }>()
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
+ }
88
+
89
+ if (data?.ok) {
90
+ await coreStore.fetchRecord({
91
+ resourceId: route.params.resourceId,
92
+ primaryKey: route.params.primaryKey,
93
+ source: 'show',
94
+ });
42
95
 
96
+ if (data.successMessage) {
97
+ adminforth.alert({
98
+ message: data.successMessage,
99
+ variant: 'success'
100
+ });
101
+ }
102
+ }
103
+
104
+ if (data?.error) {
105
+ adminforth.alert({
106
+ message: data.error,
107
+ variant: 'danger'
108
+ });
109
+ }
110
+ }
43
111
  </script>
@@ -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>
@@ -106,11 +106,18 @@ export interface IExpressHttpServer extends IHttpServer {
106
106
  }
107
107
 
108
108
 
109
- export interface IAdminForthFilter {
109
+ export interface IAdminForthSingleFilter {
110
110
  field: string;
111
- operator: AdminForthFilterOperators;
111
+ operator: AdminForthFilterOperators.EQ | AdminForthFilterOperators.NE
112
+ | AdminForthFilterOperators.GT | AdminForthFilterOperators.LT | AdminForthFilterOperators.GTE
113
+ | AdminForthFilterOperators.LTE | AdminForthFilterOperators.LIKE | AdminForthFilterOperators.ILIKE
114
+ | AdminForthFilterOperators.IN | AdminForthFilterOperators.NIN;
112
115
  value: any;
113
116
  }
117
+ export interface IAdminForthAndOrFilter {
118
+ operator: AdminForthFilterOperators.AND | AdminForthFilterOperators.OR;
119
+ subFilters: Array<IAdminForthAndOrFilter | IAdminForthSingleFilter>
120
+ }
114
121
 
115
122
  export interface IAdminForthSort {
116
123
  field: string,
@@ -185,7 +192,7 @@ export interface IAdminForthDataSourceConnector {
185
192
  limit: number,
186
193
  offset: number,
187
194
  sort: IAdminForthSort[],
188
- filters: IAdminForthFilter[],
195
+ filters: IAdminForthAndOrFilter,
189
196
  }): Promise<Array<any>>;
190
197
 
191
198
  /**
@@ -193,7 +200,7 @@ export interface IAdminForthDataSourceConnector {
193
200
  */
194
201
  getCount({ resource, filters }: {
195
202
  resource: AdminForthResource,
196
- filters: IAdminForthFilter[],
203
+ filters: IAdminForthAndOrFilter,
197
204
  }): Promise<number>;
198
205
 
199
206
  /**
@@ -206,9 +213,9 @@ export interface IAdminForthDataSourceConnector {
206
213
 
207
214
 
208
215
  /**
209
- * Used to create record in database.
216
+ * Used to create record in database. Should return value of primary key column of created record.
210
217
  */
211
- createRecordOriginalValues({ resource, record }: { resource: AdminForthResource, record: any }): Promise<void>;
218
+ createRecordOriginalValues({ resource, record }: { resource: AdminForthResource, record: any }): Promise<string>;
212
219
 
213
220
  /**
214
221
  * Update record in database. newValues might have not all fields in record, but only changed ones.
@@ -235,7 +242,7 @@ export interface IAdminForthDataSourceConnectorBase extends IAdminForthDataSourc
235
242
  limit: number,
236
243
  offset: number,
237
244
  sort: IAdminForthSort[],
238
- filters: IAdminForthFilter[],
245
+ filters: IAdminForthAndOrFilter,
239
246
  getTotals?: boolean,
240
247
  }): Promise<{ data: Array<any>, total: number }>;
241
248
 
@@ -720,6 +727,33 @@ interface AdminForthInputConfigCustomization {
720
727
  }
721
728
  }
722
729
 
730
+ export interface AdminForthActionInput {
731
+ name: string;
732
+ showIn?: {
733
+ list?: boolean,
734
+ showButton?: boolean,
735
+ showThreeDotsMenu?: boolean,
736
+ };
737
+ allowed?: (params: {
738
+ adminUser: AdminUser;
739
+ standardAllowedActions: AllowedActions;
740
+ }) => boolean;
741
+ url?: string;
742
+ action?: (params: {
743
+ adminforth: IAdminForth;
744
+ resource: AdminForthResource;
745
+ recordId: string;
746
+ adminUser: AdminUser;
747
+ extra?: HttpExtra;
748
+ tr: Function;
749
+ }) => Promise<{
750
+ ok: boolean;
751
+ error?: string;
752
+ message?: string;
753
+ }>;
754
+ icon?: string;
755
+ id?: string;
756
+ }
723
757
 
724
758
  export interface AdminForthResourceInput extends Omit<AdminForthResourceInputCommon, 'columns' | 'hooks' | 'options'> {
725
759
 
@@ -1012,36 +1046,42 @@ export interface AdminForthConfig extends Omit<AdminForthInputConfig, 'customiza
1012
1046
  // return { field: field, operator: 'eq', value: value }. They should be exported with Filters namespace so I can import Filters from this file
1013
1047
  // and use Filters.EQ(field, value) in my code
1014
1048
 
1015
- export type FDataFilter = (field: string, value: any) => IAdminForthFilter;
1049
+ export type FDataFilter = (field: string, value: any) => IAdminForthSingleFilter;
1016
1050
 
1017
1051
  export class Filters {
1018
- static EQ(field: string, value: any): IAdminForthFilter {
1052
+ static EQ(field: string, value: any): IAdminForthSingleFilter {
1019
1053
  return { field, operator: AdminForthFilterOperators.EQ, value };
1020
1054
  }
1021
- static NEQ(field: string, value: any): IAdminForthFilter {
1055
+ static NEQ(field: string, value: any): IAdminForthSingleFilter {
1022
1056
  return { field, operator: AdminForthFilterOperators.NE, value };
1023
1057
  }
1024
- static GT(field: string, value: any): IAdminForthFilter {
1058
+ static GT(field: string, value: any): IAdminForthSingleFilter {
1025
1059
  return { field, operator: AdminForthFilterOperators.GT, value };
1026
1060
  }
1027
- static GTE(field: string, value: any): IAdminForthFilter {
1061
+ static GTE(field: string, value: any): IAdminForthSingleFilter {
1028
1062
  return { field, operator: AdminForthFilterOperators.GTE, value };
1029
1063
  }
1030
- static LT(field: string, value: any): IAdminForthFilter {
1064
+ static LT(field: string, value: any): IAdminForthSingleFilter {
1031
1065
  return { field, operator: AdminForthFilterOperators.LT, value };
1032
1066
  }
1033
- static LTE(field: string, value: any): IAdminForthFilter {
1067
+ static LTE(field: string, value: any): IAdminForthSingleFilter {
1034
1068
  return { field, operator: AdminForthFilterOperators.LTE, value };
1035
1069
  }
1036
- static IN(field: string, value: any): IAdminForthFilter {
1070
+ static IN(field: string, value: any): IAdminForthSingleFilter {
1037
1071
  return { field, operator: AdminForthFilterOperators.IN, value };
1038
1072
  }
1039
- static NOT_IN(field: string, value: any): IAdminForthFilter {
1073
+ static NOT_IN(field: string, value: any): IAdminForthSingleFilter {
1040
1074
  return { field, operator: AdminForthFilterOperators.NIN, value };
1041
1075
  }
1042
- static LIKE(field: string, value: any): IAdminForthFilter {
1076
+ static LIKE(field: string, value: any): IAdminForthSingleFilter {
1043
1077
  return { field, operator: AdminForthFilterOperators.LIKE, value };
1044
1078
  }
1079
+ static AND(subFilters: Array<IAdminForthSingleFilter | IAdminForthAndOrFilter>): IAdminForthAndOrFilter {
1080
+ return { operator: AdminForthFilterOperators.AND, subFilters };
1081
+ }
1082
+ static OR(subFilters: Array<IAdminForthSingleFilter | IAdminForthAndOrFilter>): IAdminForthAndOrFilter {
1083
+ return { operator: AdminForthFilterOperators.OR, subFilters };
1084
+ }
1045
1085
  }
1046
1086
 
1047
1087
  export type FDataSort = (field: string, direction: AdminForthSortDirections) => IAdminForthSort;
@@ -1056,11 +1096,11 @@ export class Sorts {
1056
1096
  }
1057
1097
 
1058
1098
  export interface IOperationalResource {
1059
- get: (filter: IAdminForthFilter | IAdminForthFilter[]) => Promise<any | null>;
1099
+ get: (filter: IAdminForthSingleFilter | IAdminForthAndOrFilter | Array<IAdminForthSingleFilter | IAdminForthAndOrFilter>) => Promise<any | null>;
1060
1100
 
1061
- list: (filter: IAdminForthFilter | IAdminForthFilter[], limit?: number, offset?: number, sort?: IAdminForthSort | IAdminForthSort[]) => Promise<any[]>;
1101
+ list: (filter: IAdminForthSingleFilter | IAdminForthAndOrFilter | Array<IAdminForthSingleFilter | IAdminForthAndOrFilter>, limit?: number, offset?: number, sort?: IAdminForthSort | IAdminForthSort[]) => Promise<any[]>;
1062
1102
 
1063
- count: (filter: IAdminForthFilter | IAdminForthFilter[] | undefined) => Promise<number>;
1103
+ count: (filter: IAdminForthSingleFilter | IAdminForthAndOrFilter | Array<IAdminForthSingleFilter | IAdminForthAndOrFilter> | undefined) => Promise<number>;
1064
1104
 
1065
1105
  create: (record: any) => Promise<{ ok: boolean; createdRecord: any; error?: string; }>;
1066
1106
 
@@ -1141,10 +1181,38 @@ export interface ResourceOptionsInput extends Omit<AdminForthResourceCommon['opt
1141
1181
  *
1142
1182
  */
1143
1183
  allowedActions?: AllowedActionsInput,
1184
+
1185
+ /**
1186
+ * Array of actions which will be displayed in the resource.
1187
+ *
1188
+ * Example:
1189
+ *
1190
+ * ```ts
1191
+ * actions: [
1192
+ * {
1193
+ * name: 'Auto submit',
1194
+ * allowed: ({ adminUser, standardAllowedActions }) => {
1195
+ * return adminUser.dbUser.role === 'superadmin';
1196
+ * },
1197
+ * action: ({ adminUser, resource, recordId, adminforth, extra, tr }) => {
1198
+ * console.log("auto submit", recordId, adminUser);
1199
+ * return { ok: true, successMessage: "Auto submitted" };
1200
+ * },
1201
+ * showIn: {
1202
+ * list: true,
1203
+ * showButton: true,
1204
+ * showThreeDotsMenu: true,
1205
+ * },
1206
+ * },
1207
+ * ]
1208
+ * ```
1209
+ */
1210
+ actions?: Array<AdminForthActionInput>,
1144
1211
  };
1145
1212
 
1146
1213
  export interface ResourceOptions extends Omit<ResourceOptionsInput, 'allowedActions'> {
1147
1214
  allowedActions: AllowedActions,
1215
+ actions?: Array<AdminForthActionInput>,
1148
1216
  }
1149
1217
 
1150
1218
  /**
@@ -28,6 +28,8 @@ export enum AdminForthFilterOperators {
28
28
  ILIKE = 'ilike',
29
29
  IN = 'in',
30
30
  NIN = 'nin',
31
+ AND = 'and',
32
+ OR = 'or',
31
33
  };
32
34
 
33
35
  export enum AdminForthSortDirections {
@@ -50,6 +52,7 @@ export enum ActionCheckSource {
50
52
  CreateRequest = 'createRequest',
51
53
  DeleteRequest = 'deleteRequest',
52
54
  BulkActionRequest = 'bulkActionRequest',
55
+ CustomActionRequest = 'customActionRequest',
53
56
  }
54
57
 
55
58
  export enum AllowedActionsEnum {
@@ -91,7 +94,14 @@ export interface AdminForthBulkActionCommon {
91
94
  * Label for action button which will be displayed in the list view
92
95
  */
93
96
  label: string,
94
- state: string,
97
+
98
+ /**
99
+ * Bulk Action button state 'danger'|success|'active',
100
+ * * 'danger' - red button
101
+ * * 'success' - green button
102
+ * * 'active' - blue button
103
+ **/
104
+ state?: 'danger' | 'success' | 'active';
95
105
 
96
106
  /**
97
107
  * Icon for action button which will be displayed in the list view
@@ -357,18 +367,22 @@ export interface AdminForthResourceInputCommon {
357
367
  fieldGroups?: {
358
368
  groupName: string;
359
369
  columns: string[];
370
+ noTitle?: boolean;
360
371
  }[];
361
372
  createFieldGroups?: {
362
373
  groupName: string;
363
374
  columns: string[];
375
+ noTitle?: boolean;
364
376
  }[];
365
377
  editFieldGroups?: {
366
378
  groupName: string;
367
379
  columns: string[];
380
+ noTitle?: boolean;
368
381
  }[];
369
382
  showFieldGroups?: {
370
383
  groupName: string;
371
384
  columns: string[];
385
+ noTitle?: boolean;
372
386
  }[];
373
387
 
374
388
  /**
@@ -440,6 +454,7 @@ export interface AdminForthResourceInputCommon {
440
454
  bottom?: AdminForthComponentDeclaration | Array<AdminForthComponentDeclaration>,
441
455
  threeDotsDropdownItems?: AdminForthComponentDeclaration | Array<AdminForthComponentDeclaration>,
442
456
  customActionIcons?: AdminForthComponentDeclaration | Array<AdminForthComponentDeclaration>,
457
+ tableBodyStart?: AdminForthComponentDeclaration | Array<AdminForthComponentDeclaration>,
443
458
  },
444
459
 
445
460
  /**
@@ -551,6 +566,11 @@ export interface AdminForthForeignResourceCommon {
551
566
  unsetLabel?: string,
552
567
  }
553
568
 
569
+ export type FillOnCreateFunction = (params: {
570
+ initialRecord: any,
571
+ adminUser: AdminUser,
572
+ }) => any;
573
+
554
574
  /**
555
575
  * Column describes one field in the table or collection in database.
556
576
  */
@@ -681,7 +701,7 @@ export interface AdminForthResourceColumnInputCommon {
681
701
  /**
682
702
  * Called on the backend when the record is saved to a database. Value returned by `fillOnCreate` will be saved to the database.
683
703
  */
684
- fillOnCreate?: Function,
704
+ fillOnCreate?: FillOnCreateFunction,
685
705
 
686
706
  /**
687
707
  * Single value that will be substituted in create form. User can change it before saving the record.
@@ -805,6 +825,10 @@ export interface AdminForthResourceColumnInputCommon {
805
825
  * If false - will force EQ operator for filter instead of ILIKE.
806
826
  */
807
827
  substringSearch?: boolean,
828
+ /**
829
+ * Boolean value that determines what select input type to display on filter page.
830
+ */
831
+ multiselect?: boolean,
808
832
  },
809
833
 
810
834
  /**
@@ -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>
@@ -38,8 +38,10 @@
38
38
  :key="action.id"
39
39
  @click="startBulkAction(action.id)"
40
40
  class="flex gap-1 items-center py-1 px-3 text-sm font-medium text-gray-900 focus:outline-none bg-white rounded-default 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"
41
- :class="{'bg-red-100 text-red-800 border-red-400 dark:bg-red-700 dark:text-red-400 dark:border-red-400':action.state==='danger', 'bg-green-100 text-green-800 border-green-400 dark:bg-green-700 dark:text-green-400 dark:border-green-400':action.state==='success',
42
- 'bg-lightPrimaryOpacity text-lightPrimary border-blue-400 dark:bg-blue-700 dark:text-blue-400 dark:border-blue-400':action.state==='active',
41
+ :class="{
42
+ 'bg-red-100 text-red-800 border-red-400 dark:bg-red-700 dark:text-red-400 dark:border-red-400':action.state==='danger',
43
+ 'bg-green-100 text-green-800 border-green-400 dark:bg-green-700 dark:text-green-400 dark:border-green-400':action.state==='success',
44
+ 'bg-lightPrimaryOpacity text-lightPrimary border-blue-400 dark:bg-blue-700 dark:text-blue-400 dark:border-blue-400':action.state==='active',
43
45
  }"
44
46
  >
45
47
  <component
@@ -114,6 +116,7 @@
114
116
  :totalRows="totalRows"
115
117
  :checkboxes="checkboxes"
116
118
  :customActionsInjection="coreStore.resourceOptions?.pageInjections?.list?.customActionIcons"
119
+ :tableBodyStartInjection="coreStore.resourceOptions?.pageInjections?.list?.tableBodyStart"
117
120
  />
118
121
 
119
122
  <component
@@ -10,6 +10,22 @@
10
10
  :adminUser="coreStore.adminUser"
11
11
  />
12
12
  <BreadcrumbsWithButtons>
13
+ <template v-if="coreStore.resource?.options?.actions">
14
+ <button
15
+ v-for="action in coreStore.resource.options.actions.filter(a => a.showIn?.showButton)"
16
+ :key="action.id"
17
+ @click="startCustomAction(action.id)"
18
+ :disabled="actionLoadingStates[action.id]"
19
+ class="flex items-center py-1 px-3 text-sm font-medium text-gray-900 focus:outline-none bg-white rounded-default 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"
20
+ >
21
+ <component
22
+ v-if="action.icon"
23
+ :is="getIcon(action.icon)"
24
+ class="w-4 h-4 me-2 text-lightPrimary dark:text-darkPrimary"
25
+ />
26
+ {{ action.name }}
27
+ </button>
28
+ </template>
13
29
  <RouterLink v-if="coreStore.resource?.options?.allowedActions?.create"
14
30
  :to="{ name: 'resource-create', params: { resourceId: $route.params.resourceId } }"
15
31
  class="flex items-center py-1 px-3 text-sm font-medium 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 rounded-default"
@@ -34,6 +50,7 @@
34
50
 
35
51
  <ThreeDotsMenu
36
52
  :threeDotsDropdownItems="coreStore.resourceOptions?.pageInjections?.show?.threeDotsDropdownItems"
53
+ :customActions="customActions"
37
54
  ></ThreeDotsMenu>
38
55
  </BreadcrumbsWithButtons>
39
56
 
@@ -66,11 +83,12 @@
66
83
  :record="coreStore.record"
67
84
  />
68
85
  </div>
69
- <template v-else>
86
+ <template v-else>
70
87
  <template v-for="group in groups" :key="group.groupName">
71
88
  <ShowTable
72
89
  :columns="group.columns"
73
90
  :groupName="group.groupName"
91
+ :noTitle="group.noTitle"
74
92
  :resource="coreStore.resource"
75
93
  :record="coreStore.record"
76
94
  />
@@ -121,6 +139,7 @@ import ThreeDotsMenu from '@/components/ThreeDotsMenu.vue';
121
139
  import ShowTable from '@/components/ShowTable.vue';
122
140
  import adminforth from "@/adminforth";
123
141
  import { useI18n } from 'vue-i18n';
142
+ import { getIcon } from '@/utils';
124
143
 
125
144
  const route = useRoute();
126
145
  const router = useRouter();
@@ -128,6 +147,12 @@ const loading = ref(true);
128
147
  const { t } = useI18n();
129
148
  const coreStore = useCoreStore();
130
149
 
150
+ const actionLoadingStates = ref({});
151
+
152
+ const customActions = computed(() => {
153
+ return coreStore.resource?.options?.actions?.filter(a => a.showIn?.showThreeDotsMenu) || [];
154
+ });
155
+
131
156
  onMounted(async () => {
132
157
  loading.value = true;
133
158
  await coreStore.fetchResourceFull({
@@ -206,4 +231,54 @@ async function deleteRecord(row) {
206
231
 
207
232
  }
208
233
 
234
+ async function startCustomAction(actionId) {
235
+ actionLoadingStates.value[actionId] = true;
236
+
237
+ const data = await callAdminForthApi({
238
+ path: '/start_custom_action',
239
+ method: 'POST',
240
+ body: {
241
+ resourceId: route.params.resourceId,
242
+ actionId: actionId,
243
+ recordId: route.params.primaryKey
244
+ }
245
+ });
246
+
247
+ actionLoadingStates.value[actionId] = false;
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
+
264
+ if (data?.ok) {
265
+ await coreStore.fetchRecord({
266
+ resourceId: route.params.resourceId,
267
+ primaryKey: route.params.primaryKey,
268
+ source: 'show',
269
+ });
270
+
271
+ if (data.successMessage) {
272
+ adminforth.alert({
273
+ message: data.successMessage,
274
+ variant: 'success'
275
+ });
276
+ }
277
+ }
278
+
279
+ if (data?.error) {
280
+ showErrorTost(data.error);
281
+ }
282
+ }
283
+
209
284
  </script>