adminforth 1.2.95 → 1.2.97

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 (115) hide show
  1. package/dataConnectors/baseConnector.ts +26 -14
  2. package/dataConnectors/clickhouse.ts +57 -37
  3. package/dataConnectors/mongo.ts +45 -14
  4. package/dataConnectors/postgres.ts +53 -36
  5. package/dataConnectors/sqlite.ts +37 -35
  6. package/dist/dataConnectors/baseConnector.js +16 -7
  7. package/dist/dataConnectors/clickhouse.js +58 -48
  8. package/dist/dataConnectors/mongo.js +29 -12
  9. package/dist/dataConnectors/postgres.js +62 -52
  10. package/dist/dataConnectors/sqlite.js +52 -45
  11. package/dist/index.js +5 -1
  12. package/dist/modules/operationalResource.js +88 -0
  13. package/dist/modules/restApi.js +27 -29
  14. package/dist/spa/index.html +4 -4
  15. package/dist/spa/package-lock.json +421 -6
  16. package/dist/spa/package.json +10 -2
  17. package/dist/spa/src/App.vue +236 -94
  18. package/dist/spa/src/assets/base.css +2 -0
  19. package/dist/spa/src/assets/logo.svg +19 -1
  20. package/dist/spa/src/components/AcceptModal.vue +3 -10
  21. package/dist/spa/src/components/Breadcrumbs.vue +3 -2
  22. package/dist/spa/src/components/BreadcrumbsWithButtons.vue +1 -1
  23. package/dist/spa/src/components/CustomDatePicker.vue +25 -10
  24. package/dist/spa/src/components/CustomDateRangePicker.vue +14 -5
  25. package/dist/spa/src/components/Dropdown.vue +19 -5
  26. package/dist/spa/src/components/Filters.vue +103 -38
  27. package/dist/spa/src/components/MenuLink.vue +3 -3
  28. package/dist/spa/src/components/ResourceForm.vue +205 -116
  29. package/dist/spa/{spa/src → src}/components/ResourceListTable.vue +6 -3
  30. package/dist/spa/{spa/src → src}/components/Toast.vue +9 -3
  31. package/dist/spa/src/components/ValueRenderer.vue +30 -8
  32. package/dist/spa/src/index.scss +2 -1
  33. package/dist/spa/src/main.ts +2 -2
  34. package/dist/spa/src/router/index.ts +29 -19
  35. package/dist/spa/src/stores/core.ts +65 -59
  36. package/dist/spa/src/stores/modal.ts +13 -3
  37. package/dist/spa/src/types/AdminForthConfig.ts +1477 -0
  38. package/dist/spa/src/types/FrontendAPI.ts +121 -0
  39. package/dist/spa/src/utils.ts +69 -10
  40. package/dist/spa/src/views/CreateView.vue +69 -16
  41. package/dist/spa/src/views/EditView.vue +68 -14
  42. package/dist/spa/src/views/ListView.vue +100 -371
  43. package/dist/spa/src/views/LoginView.vue +67 -29
  44. package/dist/spa/src/views/ResourceParent.vue +1 -2
  45. package/dist/spa/src/views/ShowView.vue +116 -25
  46. package/dist/spa/tailwind.config.js +8 -3
  47. package/dist/spa/vite.config.ts +13 -0
  48. package/dist/types/AdminForthConfig.js +37 -0
  49. package/index.ts +12 -3
  50. package/modules/operationalResource.ts +97 -0
  51. package/modules/restApi.ts +31 -31
  52. package/package.json +1 -1
  53. package/types/AdminForthConfig.ts +91 -8
  54. package/dist/spa/.vscode/extensions.json +0 -6
  55. package/dist/spa/public/favicon.ico +0 -0
  56. package/dist/spa/spa/.eslintrc.cjs +0 -14
  57. package/dist/spa/spa/.vscode/extensions.json +0 -6
  58. package/dist/spa/spa/README.md +0 -39
  59. package/dist/spa/spa/env.d.ts +0 -1
  60. package/dist/spa/spa/index.html +0 -23
  61. package/dist/spa/spa/package-lock.json +0 -4573
  62. package/dist/spa/spa/package.json +0 -49
  63. package/dist/spa/spa/postcss.config.js +0 -6
  64. package/dist/spa/spa/public/favicon.ico +0 -0
  65. package/dist/spa/spa/src/App.vue +0 -314
  66. package/dist/spa/spa/src/assets/base.css +0 -0
  67. package/dist/spa/spa/src/assets/logo.svg +0 -19
  68. package/dist/spa/spa/src/components/AcceptModal.vue +0 -45
  69. package/dist/spa/spa/src/components/Breadcrumbs.vue +0 -40
  70. package/dist/spa/spa/src/components/BreadcrumbsWithButtons.vue +0 -26
  71. package/dist/spa/spa/src/components/CustomDatePicker.vue +0 -174
  72. package/dist/spa/spa/src/components/CustomDateRangePicker.vue +0 -218
  73. package/dist/spa/spa/src/components/Dropdown.vue +0 -168
  74. package/dist/spa/spa/src/components/Filters.vue +0 -222
  75. package/dist/spa/spa/src/components/HelloWorld.vue +0 -17
  76. package/dist/spa/spa/src/components/MenuLink.vue +0 -24
  77. package/dist/spa/spa/src/components/ResourceForm.vue +0 -289
  78. package/dist/spa/spa/src/components/SingleSkeletLoader.vue +0 -13
  79. package/dist/spa/spa/src/components/ValueRenderer.vue +0 -66
  80. package/dist/spa/spa/src/components/icons/IconCalendar.vue +0 -5
  81. package/dist/spa/spa/src/components/icons/IconCommunity.vue +0 -7
  82. package/dist/spa/spa/src/components/icons/IconDocumentation.vue +0 -7
  83. package/dist/spa/spa/src/components/icons/IconEcosystem.vue +0 -7
  84. package/dist/spa/spa/src/components/icons/IconSupport.vue +0 -7
  85. package/dist/spa/spa/src/components/icons/IconTime.vue +0 -5
  86. package/dist/spa/spa/src/components/icons/IconTooling.vue +0 -19
  87. package/dist/spa/spa/src/index.scss +0 -27
  88. package/dist/spa/spa/src/main.ts +0 -18
  89. package/dist/spa/spa/src/router/index.ts +0 -63
  90. package/dist/spa/spa/src/stores/core.ts +0 -144
  91. package/dist/spa/spa/src/stores/modal.ts +0 -48
  92. package/dist/spa/spa/src/utils.ts +0 -103
  93. package/dist/spa/spa/src/views/CreateView.vue +0 -156
  94. package/dist/spa/spa/src/views/EditView.vue +0 -157
  95. package/dist/spa/spa/src/views/HomeView.vue +0 -8
  96. package/dist/spa/spa/src/views/ListView.vue +0 -258
  97. package/dist/spa/spa/src/views/LoginView.vue +0 -143
  98. package/dist/spa/spa/src/views/ResourceParent.vue +0 -17
  99. package/dist/spa/spa/src/views/ShowView.vue +0 -184
  100. package/dist/spa/spa/tailwind.config.js +0 -17
  101. package/dist/spa/spa/tsconfig.app.json +0 -14
  102. package/dist/spa/spa/tsconfig.json +0 -11
  103. package/dist/spa/spa/tsconfig.node.json +0 -19
  104. package/dist/spa/spa/vite.config.ts +0 -56
  105. package/dist/spa/src/views/HomeView.vue +0 -8
  106. package/dist/types.js +0 -30
  107. /package/dist/spa/{spa/public → public}/assets/favicon.png +0 -0
  108. /package/dist/spa/{spa/src → src}/components/CustomRangePicker.vue +0 -0
  109. /package/dist/spa/{spa/src → src}/components/SkeleteLoader.vue +0 -0
  110. /package/dist/spa/{spa/src → src}/composables/useFrontendApi.ts +0 -0
  111. /package/dist/spa/{spa/src → src}/composables/useStores.ts +0 -0
  112. /package/dist/spa/{spa/src → src}/spa_types/core.ts +0 -0
  113. /package/dist/spa/{spa/src → src}/stores/filters.ts +0 -0
  114. /package/dist/spa/{spa/src → src}/stores/toast.ts +0 -0
  115. /package/dist/spa/{spa/src → src}/stores/user.ts +0 -0
@@ -0,0 +1,121 @@
1
+
2
+ export interface FrontendAPIInterface {
3
+
4
+ /**
5
+ * Show a confirmation dialog
6
+ *
7
+ * The dialog will be displayed to the user
8
+ *
9
+ * Example:
10
+ *
11
+ * ```ts
12
+ * const isConfirmed = await window.adminforth.confirm({message: 'Are you sure?', yes: 'Yes', no: 'No'})
13
+ * if (isConfirmed) {
14
+ * your code...
15
+ * }
16
+ * ```
17
+ *
18
+ * @param params - The parameters of the dialog
19
+ * @returns A promise that resolves when the user confirms the dialog
20
+ */
21
+ confirm(params:ConfirmParams ): Promise<void>;
22
+ /**
23
+ * Show an alert
24
+ *
25
+ * The alert will be displayed to the user
26
+ *
27
+ * Example:
28
+ *
29
+ * ```ts
30
+ * window.adminforth.alert({message: 'Hello', variant: 'success'})
31
+ * ```
32
+ *
33
+ * @param params - The parameters of the alert
34
+ */
35
+ alert(params:AlertParams): void;
36
+ /**
37
+ * Add a filter to the list of filters.
38
+ * Works only when user located on the list page.
39
+ * Can be used to set filter from charts or other components in pageInjections.
40
+ *
41
+ * Example:
42
+ *
43
+ * ```ts
44
+ * window.adminforth.updateListFilter({field: 'name', operator: 'ilike', value: 'john'})
45
+ * ```
46
+ *
47
+ * @param filter - The filter to add
48
+ */
49
+ setListFilter(filter: any): void;
50
+ /**
51
+ * Update a filter in the list of filters
52
+ *
53
+ * Example:
54
+ *
55
+ * ```ts
56
+ * window.adminforth.updateListFilter({field: 'name', operator: 'ilike', value: 'john'})
57
+ * ```
58
+ *
59
+ * @param filter - The filter to update
60
+ */
61
+ updateListFilter(filter: any): void;
62
+ /**
63
+ * Clear all filters from the list
64
+ */
65
+ clearListFilters(): void;
66
+
67
+ }
68
+
69
+ export type ConfirmParams = {
70
+ /**
71
+ * The message to display in the dialog
72
+ */
73
+ message?: string;
74
+ /**
75
+ * The text to display in the "accept" button
76
+ */
77
+ yes?: string;
78
+ /**
79
+ * The text to display in the "cancel" button
80
+ */
81
+ no?: string;
82
+
83
+ }
84
+
85
+ export type AlertParams = {
86
+ /**
87
+ * The message to display in the alert
88
+ */
89
+ message?: string;
90
+
91
+ /**
92
+ * The message to display in the alert as HTML (can be used instead of message)
93
+ */
94
+ messageHtml?: string;
95
+
96
+ /**
97
+ * The variant of the alert
98
+ */
99
+ variant?: AlertVariant | keyof typeof AlertVariant;
100
+
101
+ /**
102
+ * The timeout of the alert in seconds or 'unlimited' to keep the alert open until the user closes it.
103
+ * Default is 10 seconds;
104
+ */
105
+ timeout?: number | 'unlimited';
106
+
107
+ }
108
+
109
+
110
+
111
+ export enum AlertVariant {
112
+ danger = 'danger',
113
+ success = 'success',
114
+ warning = 'warning',
115
+ info = 'info'
116
+ }
117
+
118
+
119
+
120
+
121
+
@@ -1,8 +1,15 @@
1
1
  import { onMounted, ref, resolveComponent } from 'vue';
2
+ import type { CoreConfig } from './spa_types/core';
2
3
 
3
4
  import router from "./router";
5
+ import { useRouter } from 'vue-router';
6
+ import { useCoreStore } from './stores/core';
7
+ import { useUserStore } from './stores/user';
4
8
 
5
- export async function callApi({path, method, body=undefined} ) {
9
+ export async function callApi({path, method, body=undefined}: {
10
+ path: string, method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH'
11
+ body?: any
12
+ }): Promise<any> {
6
13
  const options = {
7
14
  method,
8
15
  headers: {
@@ -12,15 +19,19 @@ export async function callApi({path, method, body=undefined} ) {
12
19
  };
13
20
  const fullPath = `${import.meta.env.VITE_ADMINFORTH_PUBLIC_PATH || ''}${path}`;
14
21
  const r = await fetch(fullPath, options);
15
- if (r.status == 401) {
16
- console.log('router', router);
17
- router.push({name: 'login'});
22
+ if (r.status == 401 ) {
23
+ useUserStore().unauthorize();
24
+ router.push({ name: 'login' });
18
25
  return null;
19
- }
26
+ }
20
27
  return await r.json();
21
28
  }
22
29
 
23
- export async function callAdminForthApi({ path, method, body=undefined }) {
30
+ export async function callAdminForthApi({ path, method, body=undefined }: {
31
+ path: string,
32
+ method: 'GET' | 'POST' | 'PUT' | 'DELETE',
33
+ body?: any
34
+ }) {
24
35
  try {
25
36
  return callApi({path: `/adminapi/v1${path}`, method, body} );
26
37
  } catch (e) {
@@ -29,9 +40,10 @@ export async function callAdminForthApi({ path, method, body=undefined }) {
29
40
  }
30
41
  }
31
42
 
32
-
33
-
34
-
43
+ export function getCustomComponent({ file, meta }: { file: string, meta: any }) {
44
+ const name = file.replace(/@/g, '').replace(/\./g, '').replace(/\//g, '');
45
+ return resolveComponent(name);
46
+ }
35
47
 
36
48
  export function getIcon(icon: string) {
37
49
  // icon format is "feather:icon-name". We need to get IconName in pascal case
@@ -41,4 +53,51 @@ export function getIcon(icon: string) {
41
53
  const [iconSet, iconName] = icon.split(':');
42
54
  const compName = 'Icon' + iconName.split('-').map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join('');
43
55
  return resolveComponent(compName);
44
- }
56
+ }
57
+
58
+ export const loadFile = (file: string) => {
59
+ if (file.startsWith('http')) {
60
+ return file;
61
+ }
62
+ let path;
63
+ let baseUrl = '';
64
+ if (file.startsWith('@/')) {
65
+ path = file.replace('@/', '');
66
+ baseUrl = new URL(`./${path}`, import.meta.url).href;
67
+ } else if (file.startsWith('@@/')) {
68
+ path = file.replace('@@/', '');
69
+ baseUrl = new URL(`./custom/${path}`, import.meta.url).href;
70
+ } else {
71
+ baseUrl = new URL(`./${file}`, import.meta.url).href;
72
+ }
73
+ return baseUrl;
74
+ }
75
+
76
+ export function checkEmptyValues(value: any, viewType: 'show' | 'list' ) {
77
+ const config: CoreConfig | {} = useCoreStore().config;
78
+ let emptyFieldPlaceholder = '';
79
+ if (config.emptyFieldPlaceholder) {
80
+ if(typeof config.emptyFieldPlaceholder === 'string') {
81
+ emptyFieldPlaceholder = config.emptyFieldPlaceholder;
82
+ } else {
83
+ emptyFieldPlaceholder = config.emptyFieldPlaceholder?.[viewType] || '';
84
+ }
85
+ if (value === null || value === undefined || value === '') {
86
+ return emptyFieldPlaceholder;
87
+ }
88
+ }
89
+ return value;
90
+ }
91
+
92
+ export function checkAcessByAllowedActions(allowedActions:any, action:any ) {
93
+ if (!allowedActions) {
94
+ console.warn('allowedActions not set');
95
+ return;
96
+ }
97
+ if(allowedActions[action] === false) {
98
+ console.warn(`Action ${action} is not allowed`);
99
+ router.back();
100
+ }
101
+ }
102
+
103
+
@@ -1,16 +1,25 @@
1
1
  <template>
2
2
  <div class="relative">
3
3
 
4
+ <component
5
+ v-for="c in coreStore?.resourceOptions?.pageInjections?.create?.beforeBreadcrumbs || []"
6
+ :is="getCustomComponent(c)"
7
+ :meta="c.meta"
8
+ :record="coreStore.record"
9
+ :resource="coreStore.resource"
10
+ :adminUser="coreStore.adminUser"
11
+ />
12
+
4
13
  <BreadcrumbsWithButtons>
5
14
  <!-- save and cancle -->
6
15
  <button @click="$router.back()"
7
- class="flex items-center py-1 px-3 me-2 mb-2 text-sm font-medium text-gray-900 focus:outline-none bg-white rounded border border-gray-300 hover:bg-gray-100 hover:text-blue-700 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"
16
+ 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-blue-700 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"
8
17
  >
9
18
  Cancel
10
19
  </button>
11
20
 
12
21
  <button @click="saveRecord"
13
- class="flex items-center py-1 px-3 mb-2 text-sm font-medium text-red-600 focus:outline-none bg-white rounded border border-gray-300 hover:bg-gray-100 hover:text-red-700 focus:z-10 focus:ring-4 focus:ring-gray-100 dark:focus:ring-gray-700 dark:bg-gray-800 dark:text-red-500 dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700 disabled:opacity-50"
22
+ class="flex items-center py-1 px-3 text-sm font-medium rounded-default text-red-600 focus:outline-none bg-white rounded border border-gray-300 hover:bg-gray-100 hover:text-red-700 focus:z-10 focus:ring-4 focus:ring-gray-100 dark:focus:ring-gray-700 dark:bg-gray-800 dark:text-red-500 dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700 disabled:opacity-50"
14
23
  :disabled="saving || (validating && !isValid)"
15
24
  >
16
25
  <svg v-if="saving" x
@@ -22,32 +31,55 @@
22
31
 
23
32
  </BreadcrumbsWithButtons>
24
33
 
34
+ <component
35
+ v-for="c in coreStore?.resourceOptions?.pageInjections?.create?.afterBreadcrumbs || []"
36
+ :is="getCustomComponent(c)"
37
+ :meta="c.meta"
38
+ :record="coreStore.record"
39
+ :resource="coreStore.resource"
40
+ :adminUser="coreStore.adminUser"
41
+ />
42
+
25
43
  <SingleSkeletLoader v-if="loading"></SingleSkeletLoader>
44
+
26
45
 
27
46
  <ResourceForm
28
47
  v-else
29
- :record="{}"
30
- :resourceColumns="coreStore.resourceColumns"
48
+ :record="record"
49
+ :resource="coreStore.resource"
31
50
  @update:record="onUpdateRecord"
32
51
  @update:isValid="isValid = $event"
33
52
  :validating="validating"
53
+ :source="'create'"
34
54
  >
35
55
  </ResourceForm>
36
56
 
57
+ <component
58
+ v-for="c in coreStore?.resourceOptions?.pageInjections?.create?.bottom || []"
59
+ :is="getCustomComponent(c)"
60
+ :meta="c.meta"
61
+ :record="coreStore.record"
62
+ :resource="coreStore.resource"
63
+ :adminUser="coreStore.adminUser"
64
+ />
65
+
37
66
  </div>
38
67
  </template>
39
68
 
40
69
 
41
70
  <script setup>
42
71
 
43
- import { ref, computed, onMounted } from 'vue';
44
- import { useRoute, useRouter } from 'vue-router';
45
- import { useCoreStore } from '@/stores/core';
46
- import { callAdminForthApi } from '@/utils';
47
- import ResourceForm from '@/components/ResourceForm.vue';
48
72
  import BreadcrumbsWithButtons from '@/components/BreadcrumbsWithButtons.vue';
49
- import { IconFloppyDiskSolid } from '@iconify-prerendered/vue-flowbite';
73
+ import ResourceForm from '@/components/ResourceForm.vue';
50
74
  import SingleSkeletLoader from '@/components/SingleSkeletLoader.vue';
75
+ import { useCoreStore } from '@/stores/core';
76
+ import { callAdminForthApi, getCustomComponent,checkAcessByAllowedActions } from '@/utils';
77
+ import { IconFloppyDiskSolid } from '@iconify-prerendered/vue-flowbite';
78
+ import { onMounted, ref, watch } from 'vue';
79
+ import { useRoute, useRouter } from 'vue-router';
80
+ import { computed } from 'vue';
81
+ import { showErrorTost } from '@/composables/useFrontendApi';
82
+
51
83
 
52
84
  const isValid = ref(false);
53
85
  const validating = ref(false);
@@ -62,6 +94,15 @@ const record = ref({});
62
94
 
63
95
  const coreStore = useCoreStore();
64
96
 
97
+ const initalValues = computed(() => {
98
+ if (!route.query.values) {
99
+ return {};
100
+ }
101
+ return JSON.parse(decodeURIComponent(route.query.values));
102
+ });
103
+
104
+
105
+
65
106
  async function onUpdateRecord(newRecord) {
66
107
  console.log('newRecord', newRecord);
67
108
  record.value = newRecord;
@@ -69,13 +110,16 @@ async function onUpdateRecord(newRecord) {
69
110
 
70
111
  onMounted(async () => {
71
112
  loading.value = true;
72
- coreStore.fetchColumns({
113
+ record.value = initalValues.value;
114
+ await coreStore.fetchResourceFull({
73
115
  resourceId: route.params.resourceId
74
116
  });
75
117
  loading.value = false;
118
+ checkAcessByAllowedActions(coreStore.resourceOptions.allowedActions,'create');
76
119
  });
77
120
 
78
121
  async function saveRecord() {
122
+ console.log('saveRecord isValid', isValid.value);
79
123
  if (!isValid.value) {
80
124
  validating.value = true;
81
125
  return;
@@ -91,12 +135,21 @@ async function saveRecord() {
91
135
  record: record.value,
92
136
  },
93
137
  });
138
+ if (response.error) {
139
+ showErrorTost(response.error);
140
+ }
94
141
  saving.value = false;
95
- router.push({ name: 'resource-show', params: {
96
- resourceId: route.params.resourceId,
97
- primaryKey: response.newRecordId
98
-
99
- } });
142
+ if (route.query.returnTo) {
143
+ router.push(route.query.returnTo);
144
+ } else {
145
+ router.push({
146
+ name: 'resource-show',
147
+ params: {
148
+ resourceId: route.params.resourceId,
149
+ primaryKey: response.newRecordId
150
+ }
151
+ });
152
+ }
100
153
  }
101
154
 
102
155
 
@@ -1,16 +1,24 @@
1
1
  <template>
2
2
  <div class="relative">
3
+ <component
4
+ v-for="c in coreStore?.resourceOptions?.pageInjections?.edit?.beforeBreadcrumbs || []"
5
+ :is="getCustomComponent(c)"
6
+ :meta="c.meta"
7
+ :record="editableRecord"
8
+ :resource="coreStore.resource"
9
+ :adminUser="coreStore.adminUser"
10
+ />
3
11
 
4
12
  <BreadcrumbsWithButtons>
5
13
  <!-- save and cancle -->
6
14
  <button @click="$router.back()"
7
- class="flex items-center py-1 px-3 me-2 mb-2 text-sm font-medium text-gray-900 focus:outline-none bg-white rounded border border-gray-300 hover:bg-gray-100 hover:text-blue-700 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"
15
+ class="flex items-center py-1 px-3 me-2 text-sm font-medium text-gray-900 rounded-default focus:outline-none bg-white rounded border border-gray-300 hover:bg-gray-100 hover:text-blue-700 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"
8
16
  >
9
17
  Cancel
10
18
  </button>
11
19
 
12
20
  <button @click="saveRecord"
13
- class="flex items-center py-1 px-3 mb-2 text-sm font-medium text-red-600 focus:outline-none bg-white rounded border border-gray-300 hover:bg-gray-100 hover:text-red-700 focus:z-10 focus:ring-4 focus:ring-gray-100 dark:focus:ring-gray-700 dark:bg-gray-800 dark:text-red-500 dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700"
21
+ class="flex items-center py-1 px-3 text-sm font-medium rounded-default text-red-600 focus:outline-none bg-white rounded border border-gray-300 hover:bg-gray-100 hover:text-red-700 focus:z-10 focus:ring-4 focus:ring-gray-100 dark:focus:ring-gray-700 dark:bg-gray-800 dark:text-red-500 dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700"
14
22
  :disabled="saving || (validating && !isValid)"
15
23
  >
16
24
  <IconFloppyDiskSolid class="w-4 h-4" />
@@ -19,31 +27,53 @@
19
27
 
20
28
  </BreadcrumbsWithButtons>
21
29
 
30
+ <component
31
+ v-for="c in coreStore?.resourceOptions?.pageInjections?.edit?.afterBreadcrumbs || []"
32
+ :is="getCustomComponent(c)"
33
+ :meta="c.meta"
34
+ :record="coreStore.record"
35
+ :resource="coreStore.resource"
36
+ :adminUser="coreStore.adminUser"
37
+ />
38
+
22
39
  <SingleSkeletLoader v-if="loading"></SingleSkeletLoader>
23
40
 
24
41
  <ResourceForm
25
42
  v-else
26
- :record="coreStore.record"
27
- :resourceColumns="coreStore.resourceColumns"
43
+ :record="editableRecord"
44
+ :resource="coreStore.resource"
45
+ :adminUser="coreStore.adminUser"
28
46
  @update:record="onUpdateRecord"
29
47
  @update:isValid="isValid = $event"
30
48
  :validating="validating"
49
+ :source="'edit'"
31
50
  >
32
51
  </ResourceForm>
52
+
53
+ <component
54
+ v-for="c in coreStore?.resourceOptions?.pageInjections?.edit?.bottom || []"
55
+ :is="getCustomComponent(c)"
56
+ :meta="c.meta"
57
+ :record="coreStore.record"
58
+ :resource="coreStore.resource"
59
+ :adminUser="coreStore.adminUser"
60
+ />
61
+
33
62
  </div>
34
63
  </template>
35
64
 
36
65
 
37
66
  <script setup>
38
67
 
39
- import { ref, computed, onMounted } from 'vue';
40
- import { useRoute, useRouter } from 'vue-router';
41
- import { useCoreStore } from '@/stores/core';
42
- import ResourceForm from '@/components/ResourceForm.vue';
43
68
  import BreadcrumbsWithButtons from '@/components/BreadcrumbsWithButtons.vue';
44
- import { IconFloppyDiskSolid } from '@iconify-prerendered/vue-flowbite';
69
+ import ResourceForm from '@/components/ResourceForm.vue';
45
70
  import SingleSkeletLoader from '@/components/SingleSkeletLoader.vue';
46
- import { callAdminForthApi } from '@/utils';
71
+ import { useCoreStore } from '@/stores/core';
72
+ import { callAdminForthApi, getCustomComponent,checkAcessByAllowedActions } from '@/utils';
73
+ import { IconFloppyDiskSolid } from '@iconify-prerendered/vue-flowbite';
74
+ import { computed, onMounted, ref } from 'vue';
75
+ import { useRoute, useRouter } from 'vue-router';
76
+ import { showErrorTost } from '@/composables/useFrontendApi';
47
77
 
48
78
  const coreStore = useCoreStore();
49
79
 
@@ -63,18 +93,31 @@ async function onUpdateRecord(newRecord) {
63
93
  record.value = newRecord;
64
94
  }
65
95
 
96
+ const editableRecord = computed(() => {
97
+ const newRecord = { ...coreStore.record };
98
+ if (!coreStore.resource) {
99
+ return {};
100
+ }
101
+ coreStore.resource.columns.forEach(column => {
102
+ if (column.foreignResource) {
103
+ newRecord[column.name] = newRecord[column.name]?.pk
104
+ }
105
+ });
106
+ return newRecord;
107
+ })
66
108
 
67
109
  onMounted(async () => {
110
+
68
111
  loading.value = true;
69
112
 
70
- await coreStore.fetchColumns({
113
+ await coreStore.fetchResourceFull({
71
114
  resourceId: route.params.resourceId
72
115
  });
73
116
  await coreStore.fetchRecord({
74
117
  resourceId: route.params.resourceId,
75
118
  primaryKey: route.params.primaryKey,
76
119
  });
77
-
120
+ checkAcessByAllowedActions(coreStore.resourceOptions.allowedActions,'edit');
78
121
  loading.value = false;
79
122
  });
80
123
 
@@ -87,15 +130,26 @@ async function saveRecord() {
87
130
  }
88
131
 
89
132
  saving.value = true;
90
- await callAdminForthApi({
133
+
134
+ const updates = {};
135
+ for (const key in record.value) {
136
+ if (record.value[key] !== coreStore.record[key]) {
137
+ updates[key] = record.value[key];
138
+ }
139
+ }
140
+
141
+ const resp = await callAdminForthApi({
91
142
  method: 'POST',
92
143
  path: `/update_record`,
93
144
  body: {
94
145
  resourceId: route.params.resourceId,
95
146
  recordId: route.params.primaryKey,
96
- record: record.value,
147
+ record: updates,
97
148
  },
98
149
  });
150
+ if (resp.error) {
151
+ showErrorTost(resp.error);
152
+ }
99
153
  saving.value = false;
100
154
  router.push({ name: 'resource-show', params: { resourceId: route.params.resourceId, primaryKey: coreStore.record[coreStore.primaryKey] } });
101
155
  }