adminforth 2.18.2 → 2.19.0-next.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/commands/createApp/templates/.env.local.hbs +3 -0
- package/commands/createApp/templates/api.ts.hbs +7 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -5
- package/dist/index.js.map +1 -1
- package/dist/modules/logger.d.ts.map +1 -1
- package/dist/modules/logger.js +3 -5
- package/dist/modules/logger.js.map +1 -1
- package/dist/modules/restApi.d.ts.map +1 -1
- package/dist/modules/restApi.js +7 -2
- package/dist/modules/restApi.js.map +1 -1
- package/dist/servers/express.d.ts +2 -2
- package/dist/servers/express.d.ts.map +1 -1
- package/dist/servers/express.js +9 -1
- package/dist/servers/express.js.map +1 -1
- package/dist/spa/src/afcl/Dialog.vue +1 -1
- package/dist/spa/src/afcl/Input.vue +26 -1
- package/dist/spa/src/components/MenuLink.vue +4 -0
- package/dist/spa/src/components/Toast.vue +9 -10
- package/dist/spa/src/types/Back.ts +209 -3
- package/dist/spa/src/utils/index.ts +2 -0
- package/dist/spa/src/utils/listUtils.ts +90 -0
- package/dist/spa/src/utils/utils.ts +522 -0
- package/dist/spa/src/utils.ts +2 -516
- package/dist/spa/src/views/CreateView.vue +49 -19
- package/dist/spa/src/views/EditView.vue +51 -16
- package/dist/spa/src/views/ListView.vue +21 -89
- package/dist/types/Back.d.ts +194 -1
- package/dist/types/Back.d.ts.map +1 -1
- package/dist/types/Back.js.map +1 -1
- package/package.json +1 -1
package/dist/spa/src/utils.ts
CHANGED
|
@@ -1,516 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
import type { ValidationObject } from './types/Common.js';
|
|
4
|
-
import router from "./router";
|
|
5
|
-
import { useCoreStore } from './stores/core';
|
|
6
|
-
import { useUserStore } from './stores/user';
|
|
7
|
-
import { Dropdown } from 'flowbite';
|
|
8
|
-
import adminforth from './adminforth';
|
|
9
|
-
import sanitizeHtml from 'sanitize-html'
|
|
10
|
-
import debounce from 'debounce';
|
|
11
|
-
import type { AdminForthResourceColumnInputCommon, Predicate } from '@/types/Common';
|
|
12
|
-
import { i18nInstance } from './i18n'
|
|
13
|
-
|
|
14
|
-
const LS_LANG_KEY = `afLanguage`;
|
|
15
|
-
const MAX_CONSECUTIVE_EMPTY_RESULTS = 2;
|
|
16
|
-
const ITEMS_PER_PAGE_LIMIT = 100;
|
|
17
|
-
|
|
18
|
-
export async function callApi({path, method, body, headers, silentError = false}: {
|
|
19
|
-
path: string, method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH'
|
|
20
|
-
body?: any
|
|
21
|
-
headers?: Record<string, string>
|
|
22
|
-
silentError?: boolean
|
|
23
|
-
}): Promise<any> {
|
|
24
|
-
const t = i18nInstance?.global.t || ((s: string) => s)
|
|
25
|
-
const options = {
|
|
26
|
-
method,
|
|
27
|
-
headers: {
|
|
28
|
-
'Content-Type': 'application/json',
|
|
29
|
-
'accept-language': localStorage.getItem(LS_LANG_KEY) || 'en',
|
|
30
|
-
...headers
|
|
31
|
-
},
|
|
32
|
-
body: JSON.stringify(body),
|
|
33
|
-
};
|
|
34
|
-
const fullPath = `${import.meta.env.VITE_ADMINFORTH_PUBLIC_PATH || ''}${path}`;
|
|
35
|
-
try {
|
|
36
|
-
const r = await fetch(fullPath, options);
|
|
37
|
-
if (r.status == 401 ) {
|
|
38
|
-
useUserStore().unauthorize();
|
|
39
|
-
useCoreStore().resetAdminUser();
|
|
40
|
-
const currentPath = router.currentRoute.value.path;
|
|
41
|
-
const homeRoute = router.getRoutes().find(route => route.name === 'home');
|
|
42
|
-
const homePagePath = (homeRoute?.redirect as string) || '/';
|
|
43
|
-
let next = '';
|
|
44
|
-
if (currentPath !== '/login' && currentPath !== homePagePath) {
|
|
45
|
-
if (Object.keys(router.currentRoute.value.query).length > 0) {
|
|
46
|
-
next = currentPath + '?' + Object.entries(router.currentRoute.value.query).map(([key, value]) => `${key}=${value}`).join('&');
|
|
47
|
-
} else {
|
|
48
|
-
next = currentPath;
|
|
49
|
-
}
|
|
50
|
-
await router.push({ name: 'login', query: { next: next } });
|
|
51
|
-
} else {
|
|
52
|
-
await router.push({ name: 'login' });
|
|
53
|
-
}
|
|
54
|
-
return null;
|
|
55
|
-
}
|
|
56
|
-
return await r.json();
|
|
57
|
-
} catch(e) {
|
|
58
|
-
// if it is internal error, say to user
|
|
59
|
-
if (e instanceof TypeError && e.message === 'Failed to fetch') {
|
|
60
|
-
// this is a network error
|
|
61
|
-
if (!silentError) {
|
|
62
|
-
adminforth.alert({variant:'danger', message: t('Network error, please check your Internet connection and try again'),})
|
|
63
|
-
}
|
|
64
|
-
return null;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
if (!silentError) {
|
|
68
|
-
adminforth.alert({variant:'danger', message: t('Something went wrong, please try again later'),})
|
|
69
|
-
}
|
|
70
|
-
console.error(`error in callApi ${path}`, e);
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
export async function callAdminForthApi({ path, method, body=undefined, headers=undefined, silentError = false }: {
|
|
75
|
-
path: string,
|
|
76
|
-
method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH',
|
|
77
|
-
body?: any,
|
|
78
|
-
headers?: Record<string, string>,
|
|
79
|
-
silentError?: boolean
|
|
80
|
-
}): Promise<any> {
|
|
81
|
-
try {
|
|
82
|
-
return callApi({path: `/adminapi/v1${path}`, method, body, headers, silentError} );
|
|
83
|
-
} catch (e) {
|
|
84
|
-
console.error('error', e);
|
|
85
|
-
return { error: `Unexpected error: ${e}` };
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
export function getCustomComponent({ file, meta }: { file: string, meta?: any }) {
|
|
90
|
-
const name = file.replace(/@/g, '').replace(/\./g, '').replace(/\//g, '');
|
|
91
|
-
return resolveComponent(name);
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
export function getIcon(icon: string) {
|
|
95
|
-
// icon format is "feather:icon-name". We need to get IconName in pascal case
|
|
96
|
-
if (!icon.includes(':')) {
|
|
97
|
-
throw new Error('Icon name should be in format "icon-set:icon-name"');
|
|
98
|
-
}
|
|
99
|
-
const [iconSet, iconName] = icon.split(':');
|
|
100
|
-
const compName = 'Icon' + iconName.split('-').map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join('');
|
|
101
|
-
return resolveComponent(compName);
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
export const loadFile = (file: string) => {
|
|
105
|
-
if (file.startsWith('http')) {
|
|
106
|
-
return file;
|
|
107
|
-
}
|
|
108
|
-
let path;
|
|
109
|
-
let baseUrl = '';
|
|
110
|
-
if (file.startsWith('@/')) {
|
|
111
|
-
path = file.replace('@/', '');
|
|
112
|
-
baseUrl = new URL(`./${path}`, import.meta.url).href;
|
|
113
|
-
} else if (file.startsWith('@@/')) {
|
|
114
|
-
path = file.replace('@@/', '');
|
|
115
|
-
baseUrl = new URL(`./custom/${path}`, import.meta.url).href;
|
|
116
|
-
} else {
|
|
117
|
-
baseUrl = new URL(`./${file}`, import.meta.url).href;
|
|
118
|
-
}
|
|
119
|
-
return baseUrl;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
export function checkEmptyValues(value: any, viewType: 'show' | 'list' ) {
|
|
124
|
-
const config: CoreConfig | {} | null = useCoreStore().config;
|
|
125
|
-
let emptyFieldPlaceholder = '';
|
|
126
|
-
if (config && 'emptyFieldPlaceholder' in config) {
|
|
127
|
-
const efp = (config as CoreConfig).emptyFieldPlaceholder;
|
|
128
|
-
if(typeof efp === 'string') {
|
|
129
|
-
emptyFieldPlaceholder = efp;
|
|
130
|
-
} else {
|
|
131
|
-
emptyFieldPlaceholder = efp?.[viewType] || '';
|
|
132
|
-
}
|
|
133
|
-
if (value === null || value === undefined || value === '') {
|
|
134
|
-
return emptyFieldPlaceholder;
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
return value;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
export function checkAcessByAllowedActions(allowedActions:any, action:any ) {
|
|
141
|
-
if (!allowedActions) {
|
|
142
|
-
console.warn('allowedActions not set');
|
|
143
|
-
return;
|
|
144
|
-
}
|
|
145
|
-
if(allowedActions[action] === false) {
|
|
146
|
-
console.warn(`Action ${action} is not allowed`);
|
|
147
|
-
router.back();
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
export function initThreeDotsDropdown() {
|
|
152
|
-
const threeDotsDropdown: HTMLElement | null = document.querySelector('#listThreeDotsDropdown');
|
|
153
|
-
if (threeDotsDropdown) {
|
|
154
|
-
// this resource has three dots dropdown
|
|
155
|
-
const dd = new Dropdown(
|
|
156
|
-
threeDotsDropdown,
|
|
157
|
-
document.querySelector('[data-dropdown-toggle="listThreeDotsDropdown"]') as HTMLElement,
|
|
158
|
-
{ placement: 'bottom-end' }
|
|
159
|
-
);
|
|
160
|
-
adminforth.list.closeThreeDotsDropdown = () => {
|
|
161
|
-
dd.hide();
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
export function applyRegexValidation(value: any, validation: ValidationObject[] | undefined) {
|
|
167
|
-
|
|
168
|
-
if ( validation?.length ) {
|
|
169
|
-
const validationArray = validation;
|
|
170
|
-
for (let i = 0; i < validationArray.length; i++) {
|
|
171
|
-
if (validationArray[i].regExp) {
|
|
172
|
-
let flags = '';
|
|
173
|
-
if (validationArray[i].caseSensitive) {
|
|
174
|
-
flags += 'i';
|
|
175
|
-
}
|
|
176
|
-
if (validationArray[i].multiline) {
|
|
177
|
-
flags += 'm';
|
|
178
|
-
}
|
|
179
|
-
if (validationArray[i].global) {
|
|
180
|
-
flags += 'g';
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
const regExp = new RegExp(validationArray[i].regExp, flags);
|
|
184
|
-
if (value === undefined || value === null) {
|
|
185
|
-
value = '';
|
|
186
|
-
}
|
|
187
|
-
let valueS = `${value}`;
|
|
188
|
-
|
|
189
|
-
if (!regExp.test(valueS)) {
|
|
190
|
-
return validationArray[i].message;
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
export function currentQuery() {
|
|
198
|
-
return router.currentRoute.value.query;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
export function setQuery(query: any) {
|
|
202
|
-
const currentQuery = { ...router.currentRoute.value.query, ...query };
|
|
203
|
-
router.replace({
|
|
204
|
-
query: currentQuery,
|
|
205
|
-
});
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
export function verySimpleHash(str: string): string {
|
|
209
|
-
return `${str.split('').reduce((a, b)=>{a=((a<<5)-a)+b.charCodeAt(0);return a&a},0)}`;
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
export function humanifySize(size: number) {
|
|
213
|
-
if (!size) {
|
|
214
|
-
return '';
|
|
215
|
-
}
|
|
216
|
-
const units = ['B', 'KB', 'MB', 'GB', 'TB']
|
|
217
|
-
let i = 0
|
|
218
|
-
while (size >= 1024 && i < units.length - 1) {
|
|
219
|
-
size /= 1024
|
|
220
|
-
i++
|
|
221
|
-
}
|
|
222
|
-
return `${size.toFixed(1)} ${units[i]}`
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
export function protectAgainstXSS(value: string) {
|
|
226
|
-
return sanitizeHtml(value, {
|
|
227
|
-
allowedTags: [
|
|
228
|
-
"address", "article", "aside", "footer", "header", "h1", "h2", "h3", "h4",
|
|
229
|
-
"h5", "h6", "hgroup", "main", "nav", "section", "blockquote", "dd", "div",
|
|
230
|
-
"dl", "dt", "figcaption", "figure", "hr", "li", "main", "ol", "p", "pre",
|
|
231
|
-
"ul", "a", "abbr", "b", "bdi", "bdo", "br", "cite", "code", "data", "dfn",
|
|
232
|
-
"em", "i", "kbd", "mark", "q", "rb", "rp", "rt", "rtc", "ruby", "s", "samp",
|
|
233
|
-
"small", "span", "strong", "sub", "sup", "time", "u", "var", "wbr", "caption",
|
|
234
|
-
"col", "colgroup", "table", "tbody", "td", "tfoot", "th", "thead", "tr", 'img'
|
|
235
|
-
],
|
|
236
|
-
allowedAttributes: {
|
|
237
|
-
'li': [ 'data-list' ],
|
|
238
|
-
'img': [ 'src', 'srcset', 'alt', 'title', 'width', 'height', 'loading' ]
|
|
239
|
-
}
|
|
240
|
-
});
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
export function isPolymorphicColumn(column: any): boolean {
|
|
244
|
-
return !!(column.foreignResource?.polymorphicResources && column.foreignResource.polymorphicResources.length > 0);
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
export function handleForeignResourcePagination(
|
|
248
|
-
column: any,
|
|
249
|
-
items: any[],
|
|
250
|
-
emptyResultsCount: number = 0,
|
|
251
|
-
isSearching: boolean = false
|
|
252
|
-
): { hasMore: boolean; emptyResultsCount: number } {
|
|
253
|
-
const isPolymorphic = isPolymorphicColumn(column);
|
|
254
|
-
|
|
255
|
-
if (isPolymorphic) {
|
|
256
|
-
if (isSearching) {
|
|
257
|
-
return {
|
|
258
|
-
hasMore: items.length > 0,
|
|
259
|
-
emptyResultsCount: 0
|
|
260
|
-
};
|
|
261
|
-
} else {
|
|
262
|
-
if (items.length === 0) {
|
|
263
|
-
const newEmptyCount = emptyResultsCount + 1;
|
|
264
|
-
return {
|
|
265
|
-
hasMore: newEmptyCount < MAX_CONSECUTIVE_EMPTY_RESULTS, // Stop loading after 2 consecutive empty results
|
|
266
|
-
emptyResultsCount: newEmptyCount
|
|
267
|
-
};
|
|
268
|
-
} else {
|
|
269
|
-
return {
|
|
270
|
-
hasMore: true,
|
|
271
|
-
emptyResultsCount: 0
|
|
272
|
-
};
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
} else {
|
|
276
|
-
return {
|
|
277
|
-
hasMore: items.length === ITEMS_PER_PAGE_LIMIT,
|
|
278
|
-
emptyResultsCount: 0
|
|
279
|
-
};
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
export async function loadMoreForeignOptions({
|
|
284
|
-
columnName,
|
|
285
|
-
searchTerm = '',
|
|
286
|
-
columns,
|
|
287
|
-
resourceId,
|
|
288
|
-
columnOptions,
|
|
289
|
-
columnLoadingState,
|
|
290
|
-
columnOffsets,
|
|
291
|
-
columnEmptyResultsCount
|
|
292
|
-
}: {
|
|
293
|
-
columnName: string;
|
|
294
|
-
searchTerm?: string;
|
|
295
|
-
columns: any[];
|
|
296
|
-
resourceId: string;
|
|
297
|
-
columnOptions: any;
|
|
298
|
-
columnLoadingState: any;
|
|
299
|
-
columnOffsets: any;
|
|
300
|
-
columnEmptyResultsCount: any;
|
|
301
|
-
}) {
|
|
302
|
-
const column = columns?.find(c => c.name === columnName);
|
|
303
|
-
if (!column || !column.foreignResource) return;
|
|
304
|
-
|
|
305
|
-
const state = columnLoadingState[columnName];
|
|
306
|
-
if (state.loading || !state.hasMore) return;
|
|
307
|
-
|
|
308
|
-
state.loading = true;
|
|
309
|
-
|
|
310
|
-
try {
|
|
311
|
-
const list = await callAdminForthApi({
|
|
312
|
-
method: 'POST',
|
|
313
|
-
path: `/get_resource_foreign_data`,
|
|
314
|
-
body: {
|
|
315
|
-
resourceId,
|
|
316
|
-
column: columnName,
|
|
317
|
-
limit: 100,
|
|
318
|
-
offset: columnOffsets[columnName],
|
|
319
|
-
search: searchTerm,
|
|
320
|
-
},
|
|
321
|
-
});
|
|
322
|
-
|
|
323
|
-
if (!list || !Array.isArray(list.items)) {
|
|
324
|
-
console.warn(`Unexpected API response for column ${columnName}:`, list);
|
|
325
|
-
state.hasMore = false;
|
|
326
|
-
return;
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
if (!columnOptions.value) {
|
|
330
|
-
columnOptions.value = {};
|
|
331
|
-
}
|
|
332
|
-
if (!columnOptions.value[columnName]) {
|
|
333
|
-
columnOptions.value[columnName] = [];
|
|
334
|
-
}
|
|
335
|
-
columnOptions.value[columnName].push(...list.items);
|
|
336
|
-
|
|
337
|
-
columnOffsets[columnName] += 100;
|
|
338
|
-
|
|
339
|
-
const paginationResult = handleForeignResourcePagination(
|
|
340
|
-
column,
|
|
341
|
-
list.items,
|
|
342
|
-
columnEmptyResultsCount[columnName] || 0,
|
|
343
|
-
false // not searching
|
|
344
|
-
);
|
|
345
|
-
|
|
346
|
-
columnEmptyResultsCount[columnName] = paginationResult.emptyResultsCount;
|
|
347
|
-
state.hasMore = paginationResult.hasMore;
|
|
348
|
-
|
|
349
|
-
} catch (error) {
|
|
350
|
-
console.error('Error loading more options:', error);
|
|
351
|
-
} finally {
|
|
352
|
-
state.loading = false;
|
|
353
|
-
}
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
export async function searchForeignOptions({
|
|
357
|
-
columnName,
|
|
358
|
-
searchTerm,
|
|
359
|
-
columns,
|
|
360
|
-
resourceId,
|
|
361
|
-
columnOptions,
|
|
362
|
-
columnLoadingState,
|
|
363
|
-
columnOffsets,
|
|
364
|
-
columnEmptyResultsCount
|
|
365
|
-
}: {
|
|
366
|
-
columnName: string;
|
|
367
|
-
searchTerm: string;
|
|
368
|
-
columns: any[];
|
|
369
|
-
resourceId: string;
|
|
370
|
-
columnOptions: any;
|
|
371
|
-
columnLoadingState: any;
|
|
372
|
-
columnOffsets: any;
|
|
373
|
-
columnEmptyResultsCount: any;
|
|
374
|
-
}) {
|
|
375
|
-
const column = columns?.find(c => c.name === columnName);
|
|
376
|
-
|
|
377
|
-
if (!column || !column.foreignResource || !column.foreignResource.searchableFields) {
|
|
378
|
-
return;
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
const state = columnLoadingState[columnName];
|
|
382
|
-
if (state.loading) return;
|
|
383
|
-
|
|
384
|
-
state.loading = true;
|
|
385
|
-
|
|
386
|
-
try {
|
|
387
|
-
const list = await callAdminForthApi({
|
|
388
|
-
method: 'POST',
|
|
389
|
-
path: `/get_resource_foreign_data`,
|
|
390
|
-
body: {
|
|
391
|
-
resourceId,
|
|
392
|
-
column: columnName,
|
|
393
|
-
limit: 100,
|
|
394
|
-
offset: 0,
|
|
395
|
-
search: searchTerm,
|
|
396
|
-
},
|
|
397
|
-
});
|
|
398
|
-
|
|
399
|
-
if (!list || !Array.isArray(list.items)) {
|
|
400
|
-
console.warn(`Unexpected API response for column ${columnName}:`, list);
|
|
401
|
-
state.hasMore = false;
|
|
402
|
-
return;
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
if (!columnOptions.value) {
|
|
406
|
-
columnOptions.value = {};
|
|
407
|
-
}
|
|
408
|
-
columnOptions.value[columnName] = list.items;
|
|
409
|
-
columnOffsets[columnName] = 100;
|
|
410
|
-
|
|
411
|
-
const paginationResult = handleForeignResourcePagination(
|
|
412
|
-
column,
|
|
413
|
-
list.items,
|
|
414
|
-
columnEmptyResultsCount[columnName] || 0,
|
|
415
|
-
true // is searching
|
|
416
|
-
);
|
|
417
|
-
|
|
418
|
-
columnEmptyResultsCount[columnName] = paginationResult.emptyResultsCount;
|
|
419
|
-
state.hasMore = paginationResult.hasMore;
|
|
420
|
-
|
|
421
|
-
} catch (error) {
|
|
422
|
-
console.error('Error searching options:', error);
|
|
423
|
-
} finally {
|
|
424
|
-
state.loading = false;
|
|
425
|
-
}
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
export function createSearchInputHandlers(
|
|
429
|
-
columns: any[],
|
|
430
|
-
searchFunction: (columnName: string, searchTerm: string) => void,
|
|
431
|
-
getDebounceMs?: (column: any) => number
|
|
432
|
-
) {
|
|
433
|
-
if (!columns) return {};
|
|
434
|
-
|
|
435
|
-
return columns.reduce((acc, c) => {
|
|
436
|
-
if (c.foreignResource && c.foreignResource.searchableFields) {
|
|
437
|
-
const debounceMs = getDebounceMs ? getDebounceMs(c) : 300;
|
|
438
|
-
return {
|
|
439
|
-
...acc,
|
|
440
|
-
[c.name]: debounce((searchTerm: string) => {
|
|
441
|
-
searchFunction(c.name, searchTerm);
|
|
442
|
-
}, debounceMs),
|
|
443
|
-
};
|
|
444
|
-
}
|
|
445
|
-
return acc;
|
|
446
|
-
}, {} as Record<string, (searchTerm: string) => void>);
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
export function checkShowIf(c: AdminForthResourceColumnInputCommon, record: Record<string, any>, allColumns: AdminForthResourceColumnInputCommon[]) {
|
|
450
|
-
if (!c.showIf) return true;
|
|
451
|
-
const recordCopy = { ...record };
|
|
452
|
-
for (const col of allColumns) {
|
|
453
|
-
if (!recordCopy[col.name]) {
|
|
454
|
-
recordCopy[col.name] = null;
|
|
455
|
-
}
|
|
456
|
-
}
|
|
457
|
-
const evaluatePredicate = (predicate: Predicate): boolean => {
|
|
458
|
-
const results: boolean[] = [];
|
|
459
|
-
|
|
460
|
-
if ("$and" in predicate) {
|
|
461
|
-
results.push(predicate.$and.every(evaluatePredicate));
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
if ("$or" in predicate) {
|
|
465
|
-
results.push(predicate.$or.some(evaluatePredicate));
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
const fieldEntries = Object.entries(predicate).filter(([key]) => !key.startsWith('$'));
|
|
469
|
-
if (fieldEntries.length > 0) {
|
|
470
|
-
const fieldResult = fieldEntries.every(([field, condition]) => {
|
|
471
|
-
const recordValue = recordCopy[field];
|
|
472
|
-
|
|
473
|
-
if (condition === undefined) {
|
|
474
|
-
return true;
|
|
475
|
-
}
|
|
476
|
-
if (typeof condition !== "object" || condition === null) {
|
|
477
|
-
return recordValue === condition;
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
if ("$eq" in condition) return recordValue === condition.$eq;
|
|
481
|
-
if ("$not" in condition) return recordValue !== condition.$not;
|
|
482
|
-
if ("$gt" in condition) return recordValue > condition.$gt;
|
|
483
|
-
if ("$gte" in condition) return recordValue >= condition.$gte;
|
|
484
|
-
if ("$lt" in condition) return recordValue < condition.$lt;
|
|
485
|
-
if ("$lte" in condition) return recordValue <= condition.$lte;
|
|
486
|
-
if ("$in" in condition) return (Array.isArray(condition.$in) && condition.$in.includes(recordValue));
|
|
487
|
-
if ("$nin" in condition) return (Array.isArray(condition.$nin) && !condition.$nin.includes(recordValue));
|
|
488
|
-
if ("$includes" in condition)
|
|
489
|
-
return (
|
|
490
|
-
Array.isArray(recordValue) &&
|
|
491
|
-
recordValue.includes(condition.$includes)
|
|
492
|
-
);
|
|
493
|
-
if ("$nincludes" in condition)
|
|
494
|
-
return (
|
|
495
|
-
Array.isArray(recordValue) &&
|
|
496
|
-
!recordValue.includes(condition.$nicludes)
|
|
497
|
-
);
|
|
498
|
-
|
|
499
|
-
return true;
|
|
500
|
-
});
|
|
501
|
-
results.push(fieldResult);
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
return results.every(result => result);
|
|
505
|
-
};
|
|
506
|
-
|
|
507
|
-
return evaluatePredicate(c.showIf);
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
export function btoa_function(source: string): string {
|
|
511
|
-
return btoa(source);
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
export function atob_function(source: string): string {
|
|
515
|
-
return atob(source);
|
|
516
|
-
}
|
|
1
|
+
export * from './utils/utils';
|
|
2
|
+
export * from './utils/listUtils';
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
|
|
13
13
|
<BreadcrumbsWithButtons>
|
|
14
14
|
<!-- save and cancle -->
|
|
15
|
-
<button @click="$router.back()"
|
|
15
|
+
<button @click="() => {cancelButtonClicked = true; $router.back()}"
|
|
16
16
|
class="af-cancel-button flex items-center py-1 px-3 me-2 text-sm font-medium rounded-default text-lightCreateViewButtonText focus:outline-none bg-lightCreateViewButtonBackground rounded border border-lightCreateViewButtonBorder hover:bg-lightCreateViewButtonBackgroundHover hover:text-lightCreateViewButtonTextHover focus:z-10 focus:ring-4 focus:ring-lightCreateViewButtonFocusRing dark:focus:ring-darkCreateViewButtonFocusRing dark:bg-darkCreateViewButtonBackground dark:text-darkCreateViewButtonText dark:border-darkCreateViewButtonBorder dark:hover:text-darkCreateViewButtonTextHover dark:hover:bg-darkCreateViewButtonBackgroundHover"
|
|
17
17
|
>
|
|
18
18
|
{{ $t('Cancel') }}
|
|
@@ -81,8 +81,8 @@ import SingleSkeletLoader from '@/components/SingleSkeletLoader.vue';
|
|
|
81
81
|
import { useCoreStore } from '@/stores/core';
|
|
82
82
|
import { callAdminForthApi, getCustomComponent,checkAcessByAllowedActions, initThreeDotsDropdown, checkShowIf } from '@/utils';
|
|
83
83
|
import { IconFloppyDiskSolid } from '@iconify-prerendered/vue-flowbite';
|
|
84
|
-
import { onMounted, onBeforeMount, ref, watch, nextTick } from 'vue';
|
|
85
|
-
import { useRoute, useRouter } from 'vue-router';
|
|
84
|
+
import { onMounted, onBeforeMount, onBeforeUnmount, ref, watch, nextTick } from 'vue';
|
|
85
|
+
import { useRoute, useRouter, onBeforeRouteLeave } from 'vue-router';
|
|
86
86
|
import { computed } from 'vue';
|
|
87
87
|
import { showErrorTost } from '@/composables/useFrontendApi';
|
|
88
88
|
import ThreeDotsMenu from '@/components/ThreeDotsMenu.vue';
|
|
@@ -103,7 +103,7 @@ const router = useRouter();
|
|
|
103
103
|
const record = ref({});
|
|
104
104
|
|
|
105
105
|
const coreStore = useCoreStore();
|
|
106
|
-
const { clearSaveInterceptors, runSaveInterceptors, alert } = useAdminforth();
|
|
106
|
+
const { clearSaveInterceptors, runSaveInterceptors, alert, confirm } = useAdminforth();
|
|
107
107
|
|
|
108
108
|
const { t } = useI18n();
|
|
109
109
|
|
|
@@ -113,11 +113,38 @@ const initialValues = ref({});
|
|
|
113
113
|
|
|
114
114
|
const readonlyColumns = ref([]);
|
|
115
115
|
|
|
116
|
+
const cancelButtonClicked = ref(false);
|
|
117
|
+
const wasSaveSuccessful = ref(false);
|
|
116
118
|
|
|
117
119
|
async function onUpdateRecord(newRecord: any) {
|
|
118
120
|
record.value = newRecord;
|
|
119
121
|
}
|
|
120
122
|
|
|
123
|
+
function checkIfWeCanLeavePage() {
|
|
124
|
+
return wasSaveSuccessful.value || cancelButtonClicked.value || JSON.stringify(record.value) === JSON.stringify(initialValues.value);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function onBeforeUnload(event: BeforeUnloadEvent) {
|
|
128
|
+
if (!checkIfWeCanLeavePage()) {
|
|
129
|
+
event.preventDefault();
|
|
130
|
+
event.returnValue = '';
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
window.addEventListener('beforeunload', onBeforeUnload);
|
|
135
|
+
|
|
136
|
+
onBeforeUnmount(() => {
|
|
137
|
+
window.removeEventListener('beforeunload', onBeforeUnload);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
onBeforeRouteLeave(async (to, from, next) => {
|
|
141
|
+
if (!checkIfWeCanLeavePage()) {
|
|
142
|
+
const answer = await confirm({message: t('There are unsaved changes. Are you sure you want to leave this page?'), yes: 'Yes', no: 'No'});
|
|
143
|
+
if (!answer) return next(false);
|
|
144
|
+
}
|
|
145
|
+
next();
|
|
146
|
+
});
|
|
147
|
+
|
|
121
148
|
onBeforeMount(() => {
|
|
122
149
|
clearSaveInterceptors(route.params.resourceId as string);
|
|
123
150
|
});
|
|
@@ -200,23 +227,26 @@ async function saveRecord() {
|
|
|
200
227
|
});
|
|
201
228
|
if (response?.error && response?.error !== 'Operation aborted by hook') {
|
|
202
229
|
showErrorTost(response.error);
|
|
203
|
-
}
|
|
204
|
-
saving.value = false;
|
|
205
|
-
if (route.query.returnTo) {
|
|
206
|
-
router.push(<string>route.query.returnTo);
|
|
207
230
|
} else {
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
231
|
+
saving.value = false;
|
|
232
|
+
wasSaveSuccessful.value = true;
|
|
233
|
+
if (route.query.returnTo) {
|
|
234
|
+
router.push(<string>route.query.returnTo);
|
|
235
|
+
} else {
|
|
236
|
+
router.push({
|
|
237
|
+
name: 'resource-show',
|
|
238
|
+
params: {
|
|
239
|
+
resourceId: route.params.resourceId,
|
|
240
|
+
primaryKey: response.newRecordId
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
alert({
|
|
244
|
+
message: t('Record created successfully!'),
|
|
245
|
+
variant: 'success'
|
|
246
|
+
});
|
|
247
|
+
}
|
|
219
248
|
}
|
|
249
|
+
saving.value = false;
|
|
220
250
|
}
|
|
221
251
|
|
|
222
252
|
function scrollToInvalidField() {
|