adminforth 2.4.0-next.31 → 2.4.0-next.310

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 (176) hide show
  1. package/commands/callTsProxy.js +14 -4
  2. package/commands/createApp/templates/api.ts.hbs +10 -0
  3. package/commands/createApp/templates/custom/tsconfig.json.hbs +2 -3
  4. package/commands/createApp/templates/index.ts.hbs +12 -1
  5. package/commands/createApp/templates/package.json.hbs +1 -1
  6. package/commands/createApp/templates/prisma.config.ts.hbs +8 -0
  7. package/commands/createApp/templates/schema.prisma.hbs +0 -1
  8. package/commands/createApp/utils.js +10 -0
  9. package/commands/createCustomComponent/configLoader.js +17 -4
  10. package/commands/createCustomComponent/main.js +13 -7
  11. package/commands/createCustomComponent/templates/customCrud/beforeActionButtons.vue.hbs +38 -0
  12. package/commands/createCustomComponent/templates/customCrud/saveButton.vue.hbs +28 -0
  13. package/commands/createPlugin/templates/custom/tsconfig.json.hbs +2 -5
  14. package/commands/createPlugin/templates/package.json.hbs +1 -1
  15. package/commands/generateModels.js +30 -22
  16. package/dist/auth.d.ts +9 -1
  17. package/dist/auth.d.ts.map +1 -1
  18. package/dist/auth.js +21 -2
  19. package/dist/auth.js.map +1 -1
  20. package/dist/dataConnectors/baseConnector.d.ts +1 -1
  21. package/dist/dataConnectors/baseConnector.d.ts.map +1 -1
  22. package/dist/dataConnectors/baseConnector.js +69 -17
  23. package/dist/dataConnectors/baseConnector.js.map +1 -1
  24. package/dist/dataConnectors/clickhouse.d.ts.map +1 -1
  25. package/dist/dataConnectors/clickhouse.js +15 -0
  26. package/dist/dataConnectors/clickhouse.js.map +1 -1
  27. package/dist/dataConnectors/mongo.d.ts.map +1 -1
  28. package/dist/dataConnectors/mongo.js +50 -15
  29. package/dist/dataConnectors/mongo.js.map +1 -1
  30. package/dist/dataConnectors/mysql.d.ts.map +1 -1
  31. package/dist/dataConnectors/mysql.js +11 -0
  32. package/dist/dataConnectors/mysql.js.map +1 -1
  33. package/dist/dataConnectors/postgres.d.ts.map +1 -1
  34. package/dist/dataConnectors/postgres.js +43 -14
  35. package/dist/dataConnectors/postgres.js.map +1 -1
  36. package/dist/dataConnectors/sqlite.d.ts.map +1 -1
  37. package/dist/dataConnectors/sqlite.js +11 -0
  38. package/dist/dataConnectors/sqlite.js.map +1 -1
  39. package/dist/index.d.ts +12 -2
  40. package/dist/index.d.ts.map +1 -1
  41. package/dist/index.js +45 -22
  42. package/dist/index.js.map +1 -1
  43. package/dist/modules/codeInjector.d.ts +2 -0
  44. package/dist/modules/codeInjector.d.ts.map +1 -1
  45. package/dist/modules/codeInjector.js +62 -6
  46. package/dist/modules/codeInjector.js.map +1 -1
  47. package/dist/modules/configValidator.d.ts +6 -0
  48. package/dist/modules/configValidator.d.ts.map +1 -1
  49. package/dist/modules/configValidator.js +202 -25
  50. package/dist/modules/configValidator.js.map +1 -1
  51. package/dist/modules/restApi.d.ts +1 -1
  52. package/dist/modules/restApi.d.ts.map +1 -1
  53. package/dist/modules/restApi.js +172 -31
  54. package/dist/modules/restApi.js.map +1 -1
  55. package/dist/modules/styles.d.ts +499 -13
  56. package/dist/modules/styles.d.ts.map +1 -1
  57. package/dist/modules/styles.js +555 -31
  58. package/dist/modules/styles.js.map +1 -1
  59. package/dist/modules/utils.d.ts +7 -15
  60. package/dist/modules/utils.d.ts.map +1 -1
  61. package/dist/modules/utils.js +45 -68
  62. package/dist/modules/utils.js.map +1 -1
  63. package/dist/servers/express.d.ts +5 -0
  64. package/dist/servers/express.d.ts.map +1 -1
  65. package/dist/servers/express.js +40 -1
  66. package/dist/servers/express.js.map +1 -1
  67. package/dist/spa/index.html +1 -1
  68. package/dist/spa/package-lock.json +1208 -708
  69. package/dist/spa/package.json +34 -34
  70. package/dist/spa/src/App.vue +59 -174
  71. package/dist/spa/src/adminforth.ts +42 -18
  72. package/dist/spa/src/afcl/AreaChart.vue +0 -1
  73. package/dist/spa/src/afcl/BarChart.vue +2 -2
  74. package/dist/spa/src/afcl/Button.vue +6 -6
  75. package/dist/spa/src/afcl/ButtonGroup.vue +91 -0
  76. package/dist/spa/src/afcl/Card.vue +25 -0
  77. package/dist/spa/src/afcl/Checkbox.vue +21 -13
  78. package/dist/spa/src/afcl/CountryFlag.vue +4 -1
  79. package/dist/spa/src/{components/CustomDatePicker.vue → afcl/DatePicker.vue} +95 -9
  80. package/dist/spa/src/afcl/Dialog.vue +47 -27
  81. package/dist/spa/src/afcl/Dropzone.vue +127 -48
  82. package/dist/spa/src/afcl/Input.vue +14 -6
  83. package/dist/spa/src/afcl/JsonViewer.vue +25 -0
  84. package/dist/spa/src/afcl/LinkButton.vue +3 -3
  85. package/dist/spa/src/afcl/PieChart.vue +5 -5
  86. package/dist/spa/src/afcl/ProgressBar.vue +7 -7
  87. package/dist/spa/src/afcl/Select.vue +82 -34
  88. package/dist/spa/src/afcl/Skeleton.vue +6 -6
  89. package/dist/spa/src/afcl/Table.vue +315 -73
  90. package/dist/spa/src/afcl/Textarea.vue +31 -0
  91. package/dist/spa/src/afcl/Toggle.vue +32 -0
  92. package/dist/spa/src/afcl/Tooltip.vue +28 -18
  93. package/dist/spa/src/afcl/VerticalTabs.vue +16 -7
  94. package/dist/spa/src/afcl/index.ts +6 -3
  95. package/dist/spa/src/components/AcceptModal.vue +48 -14
  96. package/dist/spa/src/components/Breadcrumbs.vue +5 -5
  97. package/dist/spa/src/components/CallActionWrapper.vue +15 -0
  98. package/dist/spa/src/components/ColumnValueInput.vue +38 -18
  99. package/dist/spa/src/components/ColumnValueInputWrapper.vue +4 -3
  100. package/dist/spa/src/components/CustomDateRangePicker.vue +9 -8
  101. package/dist/spa/src/components/CustomRangePicker.vue +37 -21
  102. package/dist/spa/src/components/ErrorMessage.vue +21 -0
  103. package/dist/spa/src/components/Filters.vue +195 -132
  104. package/dist/spa/src/components/GroupsTable.vue +9 -8
  105. package/dist/spa/src/components/MenuLink.vue +90 -23
  106. package/dist/spa/src/components/ResourceForm.vue +94 -51
  107. package/dist/spa/src/components/ResourceListTable.vue +115 -85
  108. package/dist/spa/src/components/ResourceListTableVirtual.vue +114 -80
  109. package/dist/spa/src/components/ShowTable.vue +21 -15
  110. package/dist/spa/src/components/Sidebar.vue +470 -0
  111. package/dist/spa/src/components/SingleSkeletLoader.vue +6 -6
  112. package/dist/spa/src/components/SkeleteLoader.vue +3 -3
  113. package/dist/spa/src/components/ThreeDotsMenu.vue +84 -15
  114. package/dist/spa/src/components/Toast.vue +40 -29
  115. package/dist/spa/src/components/UserMenuSettingsButton.vue +69 -0
  116. package/dist/spa/src/components/ValueRenderer.vue +44 -17
  117. package/dist/spa/src/controls/BoolToggle.vue +34 -0
  118. package/dist/spa/src/i18n.ts +5 -3
  119. package/dist/spa/src/main.ts +1 -1
  120. package/dist/spa/src/renderers/CompactField.vue +1 -1
  121. package/dist/spa/src/renderers/CompactUUID.vue +1 -1
  122. package/dist/spa/src/router/index.ts +8 -0
  123. package/dist/spa/src/shims-vue.d.ts +5 -0
  124. package/dist/spa/src/spa_types/core.ts +13 -1
  125. package/dist/spa/src/stores/core.ts +13 -1
  126. package/dist/spa/src/stores/filters.ts +33 -2
  127. package/dist/spa/src/stores/modal.ts +6 -1
  128. package/dist/spa/src/stores/toast.ts +22 -3
  129. package/dist/spa/src/types/Back.ts +163 -23
  130. package/dist/spa/src/types/Common.ts +91 -32
  131. package/dist/spa/src/types/FrontendAPI.ts +31 -5
  132. package/dist/spa/src/types/adapters/CaptchaAdapter.ts +34 -0
  133. package/dist/spa/src/types/adapters/EmailAdapter.ts +2 -2
  134. package/dist/spa/src/types/adapters/ImageVisionAdapter.ts +30 -0
  135. package/dist/spa/src/types/adapters/KeyValueAdapter.ts +16 -0
  136. package/dist/spa/src/types/adapters/index.ts +8 -0
  137. package/dist/spa/src/utils.ts +291 -11
  138. package/dist/spa/src/views/CreateView.vue +63 -21
  139. package/dist/spa/src/views/EditView.vue +55 -22
  140. package/dist/spa/src/views/ListView.vue +144 -87
  141. package/dist/spa/src/views/LoginView.vue +26 -35
  142. package/dist/spa/src/views/ResourceParent.vue +2 -2
  143. package/dist/spa/src/views/SettingsView.vue +121 -0
  144. package/dist/spa/src/views/ShowView.vue +83 -53
  145. package/dist/spa/src/websocket.ts +6 -1
  146. package/dist/spa/tsconfig.app.json +1 -1
  147. package/dist/spa/vite.config.ts +45 -2
  148. package/dist/types/Back.d.ts +146 -14
  149. package/dist/types/Back.d.ts.map +1 -1
  150. package/dist/types/Back.js +15 -0
  151. package/dist/types/Back.js.map +1 -1
  152. package/dist/types/Common.d.ts +106 -29
  153. package/dist/types/Common.d.ts.map +1 -1
  154. package/dist/types/Common.js.map +1 -1
  155. package/dist/types/FrontendAPI.d.ts +31 -3
  156. package/dist/types/FrontendAPI.d.ts.map +1 -1
  157. package/dist/types/FrontendAPI.js.map +1 -1
  158. package/dist/types/adapters/CaptchaAdapter.d.ts +30 -0
  159. package/dist/types/adapters/CaptchaAdapter.d.ts.map +1 -0
  160. package/dist/types/adapters/CaptchaAdapter.js +5 -0
  161. package/dist/types/adapters/CaptchaAdapter.js.map +1 -0
  162. package/dist/types/adapters/EmailAdapter.d.ts +1 -1
  163. package/dist/types/adapters/ImageVisionAdapter.d.ts +25 -0
  164. package/dist/types/adapters/ImageVisionAdapter.d.ts.map +1 -0
  165. package/dist/types/adapters/ImageVisionAdapter.js +2 -0
  166. package/dist/types/adapters/ImageVisionAdapter.js.map +1 -0
  167. package/dist/types/adapters/KeyValueAdapter.d.ts +10 -0
  168. package/dist/types/adapters/KeyValueAdapter.d.ts.map +1 -0
  169. package/dist/types/adapters/KeyValueAdapter.js +2 -0
  170. package/dist/types/adapters/KeyValueAdapter.js.map +1 -0
  171. package/dist/types/adapters/index.d.ts +9 -0
  172. package/dist/types/adapters/index.d.ts.map +1 -0
  173. package/dist/types/adapters/index.js +2 -0
  174. package/dist/types/adapters/index.js.map +1 -0
  175. package/package.json +4 -2
  176. package/dist/spa/src/types/adapters/index.js +0 -5
@@ -55,7 +55,7 @@ export interface FrontendAPIInterface {
55
55
  *
56
56
  * @param params - The parameters of the alert
57
57
  */
58
- alert(params:AlertParams): void;
58
+ alert(params:AlertParams): void | Promise<string> | string;
59
59
 
60
60
 
61
61
  list: {
@@ -82,27 +82,40 @@ export interface FrontendAPIInterface {
82
82
  */
83
83
  closeThreeDotsDropdown(): void;
84
84
 
85
-
86
85
  /**
87
- * Set a filter in the list
88
- * Works only when user located on the list page.
86
+ * Set a filter in the list.
87
+ * Works only when user located on the list page. If filter already exists, it will be replaced with the new one.
89
88
  * Can be used to set filter from charts or other components in pageInjections.
90
89
  *
90
+ * Filters are automatically marked as hidden (won't count in badge) if:
91
+ * - Column has showIn.filter: false
92
+ *
91
93
  * Example:
92
94
  *
93
95
  * ```ts
94
96
  * import adminforth from '@/adminforth'
95
97
  *
98
+ * // Regular filter (will show in badge if column.showIn.filter !== false)
96
99
  * adminforth.list.setFilter({field: 'name', operator: 'ilike', value: 'john'})
100
+ *
101
+ * // Hidden filter (won't show in badge if column.showIn.filter === false)
102
+ * adminforth.list.setFilter({field: 'internal_status', operator: 'eq', value: 'active'})
97
103
  * ```
98
104
  *
105
+ * Please note that you can set/update filter even for fields which have showIn.filter=false in resource configuration.
106
+ * Also you can set filter for virtual columns. For example Universal search plugin calls updateFilter for virtual column which has showIn.filter=false (because we dont want to show this column in filter dropdown, plugin renders its own filter UI)
107
+ *
99
108
  * @param filter - The filter to set
100
109
  */
101
110
  setFilter(filter: FilterParams): void;
102
111
 
103
112
  /**
113
+ * DEPRECATED: does the same as setFilter, kept for backward compatibility
104
114
  * Update a filter in the list
105
115
  *
116
+ * Filters visibility in badge is automatically determined by column configuration:
117
+ * - Hidden if column has showIn.filter: false
118
+ *
106
119
  * Example:
107
120
  *
108
121
  * ```ts
@@ -121,6 +134,14 @@ export interface FrontendAPIInterface {
121
134
  clearFilters(): void;
122
135
  }
123
136
 
137
+ show: {
138
+ /**
139
+ * Full refresh the current record on the show page. Loader may be shown during fetching.
140
+ * Fire-and-forget; you don't need to await it.
141
+ */
142
+ refresh(): void;
143
+ }
144
+
124
145
  menu: {
125
146
  /**
126
147
  * Refreshes the badges in the menu, by recalling the badge function for each menu item
@@ -171,7 +192,12 @@ export type AlertParams = {
171
192
  * Default is 10 seconds;
172
193
  */
173
194
  timeout?: number | 'unlimited';
174
-
195
+
196
+ /**
197
+ * Optional buttons to display in the alert
198
+ */
199
+ buttons?: {value: any, label: string}[];
200
+
175
201
  }
176
202
 
177
203
 
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Interface for Captcha adapters.
3
+ */
4
+
5
+ export interface CaptchaAdapter {
6
+ /**
7
+ * Returns the script source URL for the captcha widget.
8
+ */
9
+ getScriptSrc(): string;
10
+
11
+ /**
12
+ * Returns the site key for the captcha.
13
+ */
14
+ getSiteKey(): string;
15
+
16
+ /**
17
+ * Returns the widget ID for the captcha.
18
+ */
19
+ getWidgetId(): string;
20
+
21
+ /**
22
+ * Returns the script HTML for the captcha widget.
23
+ */
24
+ getRenderWidgetCode(): string;
25
+
26
+ /**
27
+ * Returns the function name to render the captcha widget.
28
+ */
29
+ getRenderWidgetFunctionName(): string;
30
+ /**
31
+ * Validates the captcha token.
32
+ */
33
+ validate(token: string, ip: string): Promise<Record<string, any>>;
34
+ }
@@ -2,7 +2,7 @@ export interface EmailAdapter {
2
2
 
3
3
  /**
4
4
  * This method is called to validate the configuration of the adapter
5
- * and should throw a clear user-readbale error if the configuration is invalid.
5
+ * and should throw a clear user-readable error if the configuration is invalid.
6
6
  */
7
7
  validate(): Promise<void>;
8
8
 
@@ -24,4 +24,4 @@ export interface EmailAdapter {
24
24
  error?: string;
25
25
  ok?: boolean;
26
26
  }>;
27
- }
27
+ }
@@ -0,0 +1,30 @@
1
+ export interface ImageVisionAdapter {
2
+
3
+ /**
4
+ * This method is called to validate the configuration of the adapter
5
+ * and should throw a clear user-readable error if the configuration is invalid.
6
+ */
7
+ validate(): void;
8
+
9
+ /**
10
+ * Input file extension supported
11
+ */
12
+ inputFileExtensionSupported(): string[];
13
+
14
+ /**
15
+ * This method should generate an image based on the provided prompt and input files.
16
+ * @param prompt - The prompt to generate the image
17
+ * @param inputFileUrls - An array of input file paths (optional)
18
+ * @returns A promise that resolves to an object containing the generated image and any error message
19
+ */
20
+ generate({
21
+ prompt,
22
+ inputFileUrls,
23
+ }: {
24
+ prompt: string,
25
+ inputFileUrls: string[],
26
+ }): Promise<{
27
+ response: string;
28
+ error?: string;
29
+ }>;
30
+ }
@@ -0,0 +1,16 @@
1
+
2
+ /**
3
+ * Might have implementations like RAM, Redis, Memcached,
4
+ *
5
+ */
6
+ export interface KeyValueAdapter {
7
+
8
+ get(key: string): Promise<string | null>;
9
+
10
+ set(key: string, value: string, expiresInSeconds?: number): Promise<void>;
11
+
12
+ delete(key: string): Promise<void>;
13
+
14
+ }
15
+
16
+
@@ -0,0 +1,8 @@
1
+ export type { EmailAdapter } from './EmailAdapter.js';
2
+ export type { CompletionAdapter } from './CompletionAdapter.js';
3
+ export type { ImageGenerationAdapter } from './ImageGenerationAdapter.js';
4
+ export type { KeyValueAdapter } from './KeyValueAdapter.js';
5
+ export type { ImageVisionAdapter } from './ImageVisionAdapter.js';
6
+ export type { OAuth2Adapter } from './OAuth2Adapter.js';
7
+ export type { StorageAdapter } from './StorageAdapter.js';
8
+ export type { CaptchaAdapter } from './CaptchaAdapter.js';
@@ -1,24 +1,32 @@
1
1
  import { onMounted, ref, resolveComponent } from 'vue';
2
2
  import type { CoreConfig } from './spa_types/core';
3
- import type { ValidationObject } from './types/AdminForthConfig';
3
+ import type { ValidationObject } from './types/Common.js';
4
4
  import router from "./router";
5
5
  import { useCoreStore } from './stores/core';
6
6
  import { useUserStore } from './stores/user';
7
7
  import { Dropdown } from 'flowbite';
8
8
  import adminforth from './adminforth';
9
9
  import sanitizeHtml from 'sanitize-html'
10
+ import debounce from 'debounce';
11
+ import type { AdminForthResourceColumnInputCommon, Predicate } from '@/types/Common';
12
+ import { i18nInstance } from './i18n'
10
13
 
11
14
  const LS_LANG_KEY = `afLanguage`;
15
+ const MAX_CONSECUTIVE_EMPTY_RESULTS = 2;
16
+ const ITEMS_PER_PAGE_LIMIT = 100;
12
17
 
13
- export async function callApi({path, method, body=undefined}: {
18
+ export async function callApi({path, method, body, headers}: {
14
19
  path: string, method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH'
15
20
  body?: any
21
+ headers?: Record<string, string>
16
22
  }): Promise<any> {
23
+ const t = i18nInstance?.global.t || ((s: string) => s)
17
24
  const options = {
18
25
  method,
19
26
  headers: {
20
27
  'Content-Type': 'application/json',
21
28
  'accept-language': localStorage.getItem(LS_LANG_KEY) || 'en',
29
+ ...headers
22
30
  },
23
31
  body: JSON.stringify(body),
24
32
  };
@@ -27,6 +35,7 @@ export async function callApi({path, method, body=undefined}: {
27
35
  const r = await fetch(fullPath, options);
28
36
  if (r.status == 401 ) {
29
37
  useUserStore().unauthorize();
38
+ useCoreStore().resetAdminUser();
30
39
  await router.push({ name: 'login' });
31
40
  return null;
32
41
  }
@@ -35,18 +44,18 @@ export async function callApi({path, method, body=undefined}: {
35
44
  // if it is internal error, say to user
36
45
  if (e instanceof TypeError && e.message === 'Failed to fetch') {
37
46
  // this is a network error
38
- adminforth.alert({variant:'danger', message: window.i18n?.global?.t('Network error, please check your Internet connection and try again'),})
47
+ adminforth.alert({variant:'danger', message: t('Network error, please check your Internet connection and try again'),})
39
48
  return null;
40
49
  }
41
50
 
42
- adminforth.alert({variant:'danger', message: window.i18n?.global?.t('Something went wrong, please try again later'),})
51
+ adminforth.alert({variant:'danger', message: t('Something went wrong, please try again later'),})
43
52
  console.error(`error in callApi ${path}`, e);
44
53
  }
45
54
  }
46
55
 
47
56
  export async function callAdminForthApi({ path, method, body=undefined, headers=undefined }: {
48
57
  path: string,
49
- method: 'GET' | 'POST' | 'PUT' | 'DELETE',
58
+ method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH',
50
59
  body?: any,
51
60
  headers?: Record<string, string>
52
61
  }): Promise<any> {
@@ -92,13 +101,14 @@ export const loadFile = (file: string) => {
92
101
  }
93
102
 
94
103
  export function checkEmptyValues(value: any, viewType: 'show' | 'list' ) {
95
- const config: CoreConfig | {} = useCoreStore().config;
104
+ const config: CoreConfig | {} | null = useCoreStore().config;
96
105
  let emptyFieldPlaceholder = '';
97
- if (config.emptyFieldPlaceholder) {
98
- if(typeof config.emptyFieldPlaceholder === 'string') {
99
- emptyFieldPlaceholder = config.emptyFieldPlaceholder;
106
+ if (config && 'emptyFieldPlaceholder' in config) {
107
+ const efp = (config as CoreConfig).emptyFieldPlaceholder;
108
+ if(typeof efp === 'string') {
109
+ emptyFieldPlaceholder = efp;
100
110
  } else {
101
- emptyFieldPlaceholder = config.emptyFieldPlaceholder?.[viewType] || '';
111
+ emptyFieldPlaceholder = efp?.[viewType] || '';
102
112
  }
103
113
  if (value === null || value === undefined || value === '') {
104
114
  return emptyFieldPlaceholder;
@@ -179,7 +189,7 @@ export function verySimpleHash(str: string): string {
179
189
  return `${str.split('').reduce((a, b)=>{a=((a<<5)-a)+b.charCodeAt(0);return a&a},0)}`;
180
190
  }
181
191
 
182
- export function humanifySize(size) {
192
+ export function humanifySize(size: number) {
183
193
  if (!size) {
184
194
  return '';
185
195
  }
@@ -208,4 +218,274 @@ export function protectAgainstXSS(value: string) {
208
218
  'img': [ 'src', 'srcset', 'alt', 'title', 'width', 'height', 'loading' ]
209
219
  }
210
220
  });
221
+ }
222
+
223
+ export function isPolymorphicColumn(column: any): boolean {
224
+ return !!(column.foreignResource?.polymorphicResources && column.foreignResource.polymorphicResources.length > 0);
225
+ }
226
+
227
+ export function handleForeignResourcePagination(
228
+ column: any,
229
+ items: any[],
230
+ emptyResultsCount: number = 0,
231
+ isSearching: boolean = false
232
+ ): { hasMore: boolean; emptyResultsCount: number } {
233
+ const isPolymorphic = isPolymorphicColumn(column);
234
+
235
+ if (isPolymorphic) {
236
+ if (isSearching) {
237
+ return {
238
+ hasMore: items.length > 0,
239
+ emptyResultsCount: 0
240
+ };
241
+ } else {
242
+ if (items.length === 0) {
243
+ const newEmptyCount = emptyResultsCount + 1;
244
+ return {
245
+ hasMore: newEmptyCount < MAX_CONSECUTIVE_EMPTY_RESULTS, // Stop loading after 2 consecutive empty results
246
+ emptyResultsCount: newEmptyCount
247
+ };
248
+ } else {
249
+ return {
250
+ hasMore: true,
251
+ emptyResultsCount: 0
252
+ };
253
+ }
254
+ }
255
+ } else {
256
+ return {
257
+ hasMore: items.length === ITEMS_PER_PAGE_LIMIT,
258
+ emptyResultsCount: 0
259
+ };
260
+ }
261
+ }
262
+
263
+ export async function loadMoreForeignOptions({
264
+ columnName,
265
+ searchTerm = '',
266
+ columns,
267
+ resourceId,
268
+ columnOptions,
269
+ columnLoadingState,
270
+ columnOffsets,
271
+ columnEmptyResultsCount
272
+ }: {
273
+ columnName: string;
274
+ searchTerm?: string;
275
+ columns: any[];
276
+ resourceId: string;
277
+ columnOptions: any;
278
+ columnLoadingState: any;
279
+ columnOffsets: any;
280
+ columnEmptyResultsCount: any;
281
+ }) {
282
+ const column = columns?.find(c => c.name === columnName);
283
+ if (!column || !column.foreignResource) return;
284
+
285
+ const state = columnLoadingState[columnName];
286
+ if (state.loading || !state.hasMore) return;
287
+
288
+ state.loading = true;
289
+
290
+ try {
291
+ const list = await callAdminForthApi({
292
+ method: 'POST',
293
+ path: `/get_resource_foreign_data`,
294
+ body: {
295
+ resourceId,
296
+ column: columnName,
297
+ limit: 100,
298
+ offset: columnOffsets[columnName],
299
+ search: searchTerm,
300
+ },
301
+ });
302
+
303
+ if (!list || !Array.isArray(list.items)) {
304
+ console.warn(`Unexpected API response for column ${columnName}:`, list);
305
+ state.hasMore = false;
306
+ return;
307
+ }
308
+
309
+ if (!columnOptions.value) {
310
+ columnOptions.value = {};
311
+ }
312
+ if (!columnOptions.value[columnName]) {
313
+ columnOptions.value[columnName] = [];
314
+ }
315
+ columnOptions.value[columnName].push(...list.items);
316
+
317
+ columnOffsets[columnName] += 100;
318
+
319
+ const paginationResult = handleForeignResourcePagination(
320
+ column,
321
+ list.items,
322
+ columnEmptyResultsCount[columnName] || 0,
323
+ false // not searching
324
+ );
325
+
326
+ columnEmptyResultsCount[columnName] = paginationResult.emptyResultsCount;
327
+ state.hasMore = paginationResult.hasMore;
328
+
329
+ } catch (error) {
330
+ console.error('Error loading more options:', error);
331
+ } finally {
332
+ state.loading = false;
333
+ }
334
+ }
335
+
336
+ export async function searchForeignOptions({
337
+ columnName,
338
+ searchTerm,
339
+ columns,
340
+ resourceId,
341
+ columnOptions,
342
+ columnLoadingState,
343
+ columnOffsets,
344
+ columnEmptyResultsCount
345
+ }: {
346
+ columnName: string;
347
+ searchTerm: string;
348
+ columns: any[];
349
+ resourceId: string;
350
+ columnOptions: any;
351
+ columnLoadingState: any;
352
+ columnOffsets: any;
353
+ columnEmptyResultsCount: any;
354
+ }) {
355
+ const column = columns?.find(c => c.name === columnName);
356
+
357
+ if (!column || !column.foreignResource || !column.foreignResource.searchableFields) {
358
+ return;
359
+ }
360
+
361
+ const state = columnLoadingState[columnName];
362
+ if (state.loading) return;
363
+
364
+ state.loading = true;
365
+
366
+ try {
367
+ const list = await callAdminForthApi({
368
+ method: 'POST',
369
+ path: `/get_resource_foreign_data`,
370
+ body: {
371
+ resourceId,
372
+ column: columnName,
373
+ limit: 100,
374
+ offset: 0,
375
+ search: searchTerm,
376
+ },
377
+ });
378
+
379
+ if (!list || !Array.isArray(list.items)) {
380
+ console.warn(`Unexpected API response for column ${columnName}:`, list);
381
+ state.hasMore = false;
382
+ return;
383
+ }
384
+
385
+ if (!columnOptions.value) {
386
+ columnOptions.value = {};
387
+ }
388
+ columnOptions.value[columnName] = list.items;
389
+ columnOffsets[columnName] = 100;
390
+
391
+ const paginationResult = handleForeignResourcePagination(
392
+ column,
393
+ list.items,
394
+ columnEmptyResultsCount[columnName] || 0,
395
+ true // is searching
396
+ );
397
+
398
+ columnEmptyResultsCount[columnName] = paginationResult.emptyResultsCount;
399
+ state.hasMore = paginationResult.hasMore;
400
+
401
+ } catch (error) {
402
+ console.error('Error searching options:', error);
403
+ } finally {
404
+ state.loading = false;
405
+ }
406
+ }
407
+
408
+ export function createSearchInputHandlers(
409
+ columns: any[],
410
+ searchFunction: (columnName: string, searchTerm: string) => void,
411
+ getDebounceMs?: (column: any) => number
412
+ ) {
413
+ if (!columns) return {};
414
+
415
+ return columns.reduce((acc, c) => {
416
+ if (c.foreignResource && c.foreignResource.searchableFields) {
417
+ const debounceMs = getDebounceMs ? getDebounceMs(c) : 300;
418
+ return {
419
+ ...acc,
420
+ [c.name]: debounce((searchTerm: string) => {
421
+ searchFunction(c.name, searchTerm);
422
+ }, debounceMs),
423
+ };
424
+ }
425
+ return acc;
426
+ }, {} as Record<string, (searchTerm: string) => void>);
427
+ }
428
+
429
+ export function checkShowIf(c: AdminForthResourceColumnInputCommon, record: Record<string, any>) {
430
+ if (!c.showIf) return true;
431
+
432
+ const evaluatePredicate = (predicate: Predicate): boolean => {
433
+ const results: boolean[] = [];
434
+
435
+ if ("$and" in predicate) {
436
+ results.push(predicate.$and.every(evaluatePredicate));
437
+ }
438
+
439
+ if ("$or" in predicate) {
440
+ results.push(predicate.$or.some(evaluatePredicate));
441
+ }
442
+
443
+ const fieldEntries = Object.entries(predicate).filter(([key]) => !key.startsWith('$'));
444
+ if (fieldEntries.length > 0) {
445
+ const fieldResult = fieldEntries.every(([field, condition]) => {
446
+ const recordValue = record[field];
447
+
448
+ if (condition === undefined) {
449
+ return true;
450
+ }
451
+ if (typeof condition !== "object" || condition === null) {
452
+ return recordValue === condition;
453
+ }
454
+
455
+ if ("$eq" in condition) return recordValue === condition.$eq;
456
+ if ("$not" in condition) return recordValue !== condition.$not;
457
+ if ("$gt" in condition) return recordValue > condition.$gt;
458
+ if ("$gte" in condition) return recordValue >= condition.$gte;
459
+ if ("$lt" in condition) return recordValue < condition.$lt;
460
+ if ("$lte" in condition) return recordValue <= condition.$lte;
461
+ if ("$in" in condition) return (Array.isArray(condition.$in) && condition.$in.includes(recordValue));
462
+ if ("$nin" in condition) return (Array.isArray(condition.$nin) && !condition.$nin.includes(recordValue));
463
+ if ("$includes" in condition)
464
+ return (
465
+ Array.isArray(recordValue) &&
466
+ recordValue.includes(condition.$includes)
467
+ );
468
+ if ("$nincludes" in condition)
469
+ return (
470
+ Array.isArray(recordValue) &&
471
+ !recordValue.includes(condition.$nicludes)
472
+ );
473
+
474
+ return true;
475
+ });
476
+ results.push(fieldResult);
477
+ }
478
+
479
+ return results.every(result => result);
480
+ };
481
+
482
+ return evaluatePredicate(c.showIf);
483
+ }
484
+
485
+ export function btoa_function(source: string): string {
486
+ return btoa(source);
487
+ }
488
+
489
+ export function atob_function(source: string): string {
490
+ return atob(source);
211
491
  }