adminforth 2.4.0-next.7 → 2.4.0-next.71
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/callTsProxy.js +14 -4
- package/commands/cli.js +12 -4
- package/commands/createApp/templates/custom/tsconfig.json.hbs +2 -3
- package/commands/createApp/templates/index.ts.hbs +1 -1
- package/commands/createApp/templates/package.json.hbs +1 -1
- package/commands/createApp/utils.js +39 -13
- package/commands/createCustomComponent/configUpdater.js +25 -21
- package/commands/createCustomComponent/fileGenerator.js +1 -1
- package/commands/createCustomComponent/main.js +2 -1
- package/commands/createCustomComponent/templates/login/beforeLogin.vue.hbs +18 -0
- package/dist/dataConnectors/baseConnector.d.ts.map +1 -1
- package/dist/dataConnectors/baseConnector.js +16 -3
- package/dist/dataConnectors/baseConnector.js.map +1 -1
- package/dist/dataConnectors/mongo.d.ts.map +1 -1
- package/dist/dataConnectors/mongo.js +14 -14
- package/dist/dataConnectors/mongo.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +20 -9
- package/dist/index.js.map +1 -1
- package/dist/modules/codeInjector.d.ts.map +1 -1
- package/dist/modules/codeInjector.js +25 -9
- package/dist/modules/codeInjector.js.map +1 -1
- package/dist/modules/configValidator.d.ts.map +1 -1
- package/dist/modules/configValidator.js +50 -1
- package/dist/modules/configValidator.js.map +1 -1
- package/dist/modules/restApi.d.ts.map +1 -1
- package/dist/modules/restApi.js +45 -2
- package/dist/modules/restApi.js.map +1 -1
- package/dist/modules/styles.d.ts +42 -0
- package/dist/modules/styles.d.ts.map +1 -1
- package/dist/modules/styles.js +44 -2
- package/dist/modules/styles.js.map +1 -1
- package/dist/spa/index.html +1 -1
- package/dist/spa/src/App.vue +14 -5
- package/dist/spa/src/afcl/Button.vue +4 -4
- package/dist/spa/src/afcl/Checkbox.vue +21 -13
- package/dist/spa/src/afcl/CountryFlag.vue +3 -1
- package/dist/spa/src/afcl/Dropzone.vue +4 -2
- package/dist/spa/src/afcl/Input.vue +5 -3
- package/dist/spa/src/afcl/JsonViewer.vue +25 -0
- package/dist/spa/src/afcl/Link.vue +1 -1
- package/dist/spa/src/afcl/LinkButton.vue +1 -1
- package/dist/spa/src/afcl/Select.vue +44 -15
- package/dist/spa/src/afcl/Table.vue +8 -8
- package/dist/spa/src/afcl/Toggle.vue +32 -0
- package/dist/spa/src/afcl/Tooltip.vue +1 -1
- package/dist/spa/src/afcl/index.ts +2 -2
- package/dist/spa/src/components/AcceptModal.vue +4 -4
- package/dist/spa/src/components/ColumnValueInput.vue +21 -2
- package/dist/spa/src/components/ColumnValueInputWrapper.vue +1 -0
- package/dist/spa/src/components/CustomDatePicker.vue +2 -2
- package/dist/spa/src/components/CustomDateRangePicker.vue +1 -0
- package/dist/spa/src/components/Filters.vue +73 -28
- package/dist/spa/src/components/GroupsTable.vue +3 -3
- package/dist/spa/src/components/ResourceForm.vue +61 -26
- package/dist/spa/src/components/ResourceListTable.vue +34 -36
- package/dist/spa/src/components/ResourceListTableVirtual.vue +23 -25
- package/dist/spa/src/components/ShowTable.vue +11 -6
- package/dist/spa/src/components/ValueRenderer.vue +4 -4
- package/dist/spa/src/controls/BoolToggle.vue +34 -0
- package/dist/spa/src/spa_types/core.ts +7 -0
- package/dist/spa/src/stores/core.ts +1 -1
- package/dist/spa/src/types/Back.ts +46 -12
- package/dist/spa/src/types/Common.ts +10 -1
- package/dist/spa/src/types/adapters/CompletionAdapter.ts +25 -0
- package/dist/spa/src/types/adapters/EmailAdapter.ts +29 -0
- package/dist/spa/src/types/adapters/ImageGenerationAdapter.ts +50 -0
- package/dist/spa/src/types/adapters/OAuth2Adapter.ts +34 -0
- package/dist/spa/src/types/adapters/StorageAdapter.ts +73 -0
- package/dist/spa/src/types/adapters/index.ts +5 -0
- package/dist/spa/src/utils.ts +209 -0
- package/dist/spa/src/views/CreateView.vue +3 -3
- package/dist/spa/src/views/EditView.vue +1 -1
- package/dist/spa/src/views/ListView.vue +2 -2
- package/dist/spa/src/views/LoginView.vue +39 -37
- package/dist/spa/src/views/ResourceParent.vue +1 -1
- package/dist/spa/src/views/ShowView.vue +3 -3
- package/dist/types/Back.d.ts +40 -9
- package/dist/types/Back.d.ts.map +1 -1
- package/dist/types/Back.js.map +1 -1
- package/dist/types/Common.d.ts +9 -0
- package/dist/types/Common.d.ts.map +1 -1
- package/dist/types/Common.js.map +1 -1
- package/dist/types/adapters/CompletionAdapter.d.ts +20 -0
- package/dist/types/adapters/CompletionAdapter.d.ts.map +1 -0
- package/dist/types/adapters/CompletionAdapter.js +2 -0
- package/dist/types/adapters/CompletionAdapter.js.map +1 -0
- package/dist/types/adapters/EmailAdapter.d.ts +21 -0
- package/dist/types/adapters/EmailAdapter.d.ts.map +1 -0
- package/dist/types/adapters/EmailAdapter.js +2 -0
- package/dist/types/adapters/EmailAdapter.js.map +1 -0
- package/dist/types/adapters/ImageGenerationAdapter.d.ts +37 -0
- package/dist/types/adapters/ImageGenerationAdapter.d.ts.map +1 -0
- package/dist/types/adapters/ImageGenerationAdapter.js +2 -0
- package/dist/types/adapters/ImageGenerationAdapter.js.map +1 -0
- package/dist/types/adapters/OAuth2Adapter.d.ts +32 -0
- package/dist/types/adapters/OAuth2Adapter.d.ts.map +1 -0
- package/dist/types/adapters/OAuth2Adapter.js +2 -0
- package/dist/types/adapters/OAuth2Adapter.js.map +1 -0
- package/dist/types/adapters/StorageAdapter.d.ts +63 -0
- package/dist/types/adapters/StorageAdapter.d.ts.map +1 -0
- package/dist/types/adapters/StorageAdapter.js +2 -0
- package/dist/types/adapters/StorageAdapter.js.map +1 -0
- package/dist/types/adapters/index.d.ts +6 -0
- package/dist/types/adapters/index.d.ts.map +1 -0
- package/dist/types/adapters/index.js +2 -0
- package/dist/types/adapters/index.js.map +1 -0
- package/package.json +2 -2
- package/dist/spa/src/types/Adapters.ts +0 -213
- package/dist/types/Adapters.d.ts +0 -168
- package/dist/types/Adapters.d.ts.map +0 -1
- package/dist/types/Adapters.js +0 -2
- package/dist/types/Adapters.js.map +0 -1
package/dist/spa/src/utils.ts
CHANGED
|
@@ -7,8 +7,11 @@ 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';
|
|
10
11
|
|
|
11
12
|
const LS_LANG_KEY = `afLanguage`;
|
|
13
|
+
const MAX_CONSECUTIVE_EMPTY_RESULTS = 2;
|
|
14
|
+
const ITEMS_PER_PAGE_LIMIT = 100;
|
|
12
15
|
|
|
13
16
|
export async function callApi({path, method, body=undefined}: {
|
|
14
17
|
path: string, method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH'
|
|
@@ -208,4 +211,210 @@ export function protectAgainstXSS(value: string) {
|
|
|
208
211
|
'img': [ 'src', 'srcset', 'alt', 'title', 'width', 'height', 'loading' ]
|
|
209
212
|
}
|
|
210
213
|
});
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export function isPolymorphicColumn(column: any): boolean {
|
|
217
|
+
return !!(column.foreignResource?.polymorphicResources && column.foreignResource.polymorphicResources.length > 0);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export function handleForeignResourcePagination(
|
|
221
|
+
column: any,
|
|
222
|
+
items: any[],
|
|
223
|
+
emptyResultsCount: number = 0,
|
|
224
|
+
isSearching: boolean = false
|
|
225
|
+
): { hasMore: boolean; emptyResultsCount: number } {
|
|
226
|
+
const isPolymorphic = isPolymorphicColumn(column);
|
|
227
|
+
|
|
228
|
+
if (isPolymorphic) {
|
|
229
|
+
if (isSearching) {
|
|
230
|
+
return {
|
|
231
|
+
hasMore: items.length > 0,
|
|
232
|
+
emptyResultsCount: 0
|
|
233
|
+
};
|
|
234
|
+
} else {
|
|
235
|
+
if (items.length === 0) {
|
|
236
|
+
const newEmptyCount = emptyResultsCount + 1;
|
|
237
|
+
return {
|
|
238
|
+
hasMore: newEmptyCount < MAX_CONSECUTIVE_EMPTY_RESULTS, // Stop loading after 2 consecutive empty results
|
|
239
|
+
emptyResultsCount: newEmptyCount
|
|
240
|
+
};
|
|
241
|
+
} else {
|
|
242
|
+
return {
|
|
243
|
+
hasMore: true,
|
|
244
|
+
emptyResultsCount: 0
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
} else {
|
|
249
|
+
return {
|
|
250
|
+
hasMore: items.length === ITEMS_PER_PAGE_LIMIT,
|
|
251
|
+
emptyResultsCount: 0
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
export async function loadMoreForeignOptions({
|
|
257
|
+
columnName,
|
|
258
|
+
searchTerm = '',
|
|
259
|
+
columns,
|
|
260
|
+
resourceId,
|
|
261
|
+
columnOptions,
|
|
262
|
+
columnLoadingState,
|
|
263
|
+
columnOffsets,
|
|
264
|
+
columnEmptyResultsCount
|
|
265
|
+
}: {
|
|
266
|
+
columnName: string;
|
|
267
|
+
searchTerm?: string;
|
|
268
|
+
columns: any[];
|
|
269
|
+
resourceId: string;
|
|
270
|
+
columnOptions: any;
|
|
271
|
+
columnLoadingState: any;
|
|
272
|
+
columnOffsets: any;
|
|
273
|
+
columnEmptyResultsCount: any;
|
|
274
|
+
}) {
|
|
275
|
+
const column = columns?.find(c => c.name === columnName);
|
|
276
|
+
if (!column || !column.foreignResource) return;
|
|
277
|
+
|
|
278
|
+
const state = columnLoadingState[columnName];
|
|
279
|
+
if (state.loading || !state.hasMore) return;
|
|
280
|
+
|
|
281
|
+
state.loading = true;
|
|
282
|
+
|
|
283
|
+
try {
|
|
284
|
+
const list = await callAdminForthApi({
|
|
285
|
+
method: 'POST',
|
|
286
|
+
path: `/get_resource_foreign_data`,
|
|
287
|
+
body: {
|
|
288
|
+
resourceId,
|
|
289
|
+
column: columnName,
|
|
290
|
+
limit: 100,
|
|
291
|
+
offset: columnOffsets[columnName],
|
|
292
|
+
search: searchTerm,
|
|
293
|
+
},
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
if (!list || !Array.isArray(list.items)) {
|
|
297
|
+
console.warn(`Unexpected API response for column ${columnName}:`, list);
|
|
298
|
+
state.hasMore = false;
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
if (!columnOptions.value) {
|
|
303
|
+
columnOptions.value = {};
|
|
304
|
+
}
|
|
305
|
+
if (!columnOptions.value[columnName]) {
|
|
306
|
+
columnOptions.value[columnName] = [];
|
|
307
|
+
}
|
|
308
|
+
columnOptions.value[columnName].push(...list.items);
|
|
309
|
+
|
|
310
|
+
columnOffsets[columnName] += 100;
|
|
311
|
+
|
|
312
|
+
const paginationResult = handleForeignResourcePagination(
|
|
313
|
+
column,
|
|
314
|
+
list.items,
|
|
315
|
+
columnEmptyResultsCount[columnName] || 0,
|
|
316
|
+
false // not searching
|
|
317
|
+
);
|
|
318
|
+
|
|
319
|
+
columnEmptyResultsCount[columnName] = paginationResult.emptyResultsCount;
|
|
320
|
+
state.hasMore = paginationResult.hasMore;
|
|
321
|
+
|
|
322
|
+
} catch (error) {
|
|
323
|
+
console.error('Error loading more options:', error);
|
|
324
|
+
} finally {
|
|
325
|
+
state.loading = false;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
export async function searchForeignOptions({
|
|
330
|
+
columnName,
|
|
331
|
+
searchTerm,
|
|
332
|
+
columns,
|
|
333
|
+
resourceId,
|
|
334
|
+
columnOptions,
|
|
335
|
+
columnLoadingState,
|
|
336
|
+
columnOffsets,
|
|
337
|
+
columnEmptyResultsCount
|
|
338
|
+
}: {
|
|
339
|
+
columnName: string;
|
|
340
|
+
searchTerm: string;
|
|
341
|
+
columns: any[];
|
|
342
|
+
resourceId: string;
|
|
343
|
+
columnOptions: any;
|
|
344
|
+
columnLoadingState: any;
|
|
345
|
+
columnOffsets: any;
|
|
346
|
+
columnEmptyResultsCount: any;
|
|
347
|
+
}) {
|
|
348
|
+
const column = columns?.find(c => c.name === columnName);
|
|
349
|
+
|
|
350
|
+
if (!column || !column.foreignResource || !column.foreignResource.searchableFields) {
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
const state = columnLoadingState[columnName];
|
|
355
|
+
if (state.loading) return;
|
|
356
|
+
|
|
357
|
+
state.loading = true;
|
|
358
|
+
|
|
359
|
+
try {
|
|
360
|
+
const list = await callAdminForthApi({
|
|
361
|
+
method: 'POST',
|
|
362
|
+
path: `/get_resource_foreign_data`,
|
|
363
|
+
body: {
|
|
364
|
+
resourceId,
|
|
365
|
+
column: columnName,
|
|
366
|
+
limit: 100,
|
|
367
|
+
offset: 0,
|
|
368
|
+
search: searchTerm,
|
|
369
|
+
},
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
if (!list || !Array.isArray(list.items)) {
|
|
373
|
+
console.warn(`Unexpected API response for column ${columnName}:`, list);
|
|
374
|
+
state.hasMore = false;
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
if (!columnOptions.value) {
|
|
379
|
+
columnOptions.value = {};
|
|
380
|
+
}
|
|
381
|
+
columnOptions.value[columnName] = list.items;
|
|
382
|
+
columnOffsets[columnName] = 100;
|
|
383
|
+
|
|
384
|
+
const paginationResult = handleForeignResourcePagination(
|
|
385
|
+
column,
|
|
386
|
+
list.items,
|
|
387
|
+
columnEmptyResultsCount[columnName] || 0,
|
|
388
|
+
true // is searching
|
|
389
|
+
);
|
|
390
|
+
|
|
391
|
+
columnEmptyResultsCount[columnName] = paginationResult.emptyResultsCount;
|
|
392
|
+
state.hasMore = paginationResult.hasMore;
|
|
393
|
+
|
|
394
|
+
} catch (error) {
|
|
395
|
+
console.error('Error searching options:', error);
|
|
396
|
+
} finally {
|
|
397
|
+
state.loading = false;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
export function createSearchInputHandlers(
|
|
402
|
+
columns: any[],
|
|
403
|
+
searchFunction: (columnName: string, searchTerm: string) => void,
|
|
404
|
+
getDebounceMs?: (column: any) => number
|
|
405
|
+
) {
|
|
406
|
+
if (!columns) return {};
|
|
407
|
+
|
|
408
|
+
return columns.reduce((acc, c) => {
|
|
409
|
+
if (c.foreignResource && c.foreignResource.searchableFields) {
|
|
410
|
+
const debounceMs = getDebounceMs ? getDebounceMs(c) : 300;
|
|
411
|
+
return {
|
|
412
|
+
...acc,
|
|
413
|
+
[c.name]: debounce((searchTerm: string) => {
|
|
414
|
+
searchFunction(c.name, searchTerm);
|
|
415
|
+
}, debounceMs),
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
return acc;
|
|
419
|
+
}, {} as Record<string, (searchTerm: string) => void>);
|
|
211
420
|
}
|
|
@@ -13,14 +13,14 @@
|
|
|
13
13
|
<BreadcrumbsWithButtons>
|
|
14
14
|
<!-- save and cancle -->
|
|
15
15
|
<button @click="$router.back()"
|
|
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-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"
|
|
16
|
+
class="af-cancel-button flex items-center py-1 px-3 me-2 text-sm font-medium rounded-default text-gray-900 focus:outline-none bg-white rounded border border-gray-300 hover:bg-gray-100 hover:text-lightPrimary focus:z-10 focus:ring-4 focus:ring-gray-100 dark:focus:ring-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700"
|
|
17
17
|
>
|
|
18
18
|
{{ $t('Cancel') }}
|
|
19
19
|
</button>
|
|
20
20
|
|
|
21
21
|
<button
|
|
22
22
|
@click="saveRecord"
|
|
23
|
-
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"
|
|
23
|
+
class="af-save-button 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"
|
|
24
24
|
:disabled="saving || (validating && !isValid)"
|
|
25
25
|
>
|
|
26
26
|
<svg v-if="saving"
|
|
@@ -154,7 +154,7 @@ async function saveRecord() {
|
|
|
154
154
|
record: record.value,
|
|
155
155
|
},
|
|
156
156
|
});
|
|
157
|
-
if (response?.error) {
|
|
157
|
+
if (response?.error && response?.error !== 'Operation aborted by hook') {
|
|
158
158
|
showErrorTost(response.error);
|
|
159
159
|
}
|
|
160
160
|
saving.value = false;
|
|
@@ -74,14 +74,14 @@
|
|
|
74
74
|
|
|
75
75
|
<RouterLink v-if="coreStore.resource?.options?.allowedActions?.create"
|
|
76
76
|
:to="{ name: 'resource-create', params: { resourceId: $route.params.resourceId } }"
|
|
77
|
-
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"
|
|
77
|
+
class="af-create-button 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"
|
|
78
78
|
>
|
|
79
79
|
<IconPlusOutline class="w-4 h-4 me-2"/>
|
|
80
80
|
{{ $t('Create') }}
|
|
81
81
|
</RouterLink>
|
|
82
82
|
|
|
83
83
|
<button
|
|
84
|
-
class="flex gap-1 items-center py-1 px-3 me-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"
|
|
84
|
+
class="af-filter-button flex gap-1 items-center py-1 px-3 me-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"
|
|
85
85
|
@click="()=>{filtersShow = !filtersShow}"
|
|
86
86
|
v-if="coreStore.resource?.options?.allowedActions?.filter"
|
|
87
87
|
>
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
'background-image': 'url(' + loadFile(coreStore.config?.loginBackgroundImage) + ')',
|
|
5
5
|
'background-size': 'cover',
|
|
6
6
|
'background-position': 'center',
|
|
7
|
-
'background-blend-mode': 'darken'
|
|
7
|
+
'background-blend-mode': coreStore.config?.removeBackgroundBlendMode ? 'normal' : 'darken'
|
|
8
8
|
}: {}"
|
|
9
9
|
>
|
|
10
10
|
|
|
@@ -23,44 +23,57 @@
|
|
|
23
23
|
|
|
24
24
|
<!-- Main modal -->
|
|
25
25
|
<div id="authentication-modal" tabindex="-1"
|
|
26
|
-
class="overflow-y-auto flex flex-grow
|
|
26
|
+
class="af-login-modal overflow-y-auto flex flex-grow
|
|
27
27
|
overflow-x-hidden z-50 min-w-[350px] justify-center items-center md:inset-0 h-[calc(100%-1rem)] max-h-full">
|
|
28
28
|
<div class="relative p-4 w-full max-h-full max-w-[400px]">
|
|
29
29
|
<!-- Modal content -->
|
|
30
|
-
<div class="relative bg-white rounded-lg shadow dark:bg-gray-700 dark:shadow-black" >
|
|
30
|
+
<div class="af-login-modal-content relative bg-white rounded-lg shadow dark:bg-gray-700 dark:shadow-black" >
|
|
31
31
|
<!-- Modal header -->
|
|
32
|
-
<div class="flex items-center justify-between p-4 md:p-5 border-b rounded-t dark:border-gray-600">
|
|
33
|
-
|
|
32
|
+
<div class="af-login-modal-header flex items-center justify-between flex-col p-4 md:p-5 border-b rounded-t dark:border-gray-600">
|
|
33
|
+
|
|
34
|
+
<template v-if="coreStore?.config?.loginPageInjections?.panelHeader.length > 0">
|
|
35
|
+
<component
|
|
36
|
+
v-for="(c, index) in coreStore?.config?.loginPageInjections?.panelHeader || []"
|
|
37
|
+
:key="index"
|
|
38
|
+
:is="getCustomComponent(c)"
|
|
39
|
+
:meta="c.meta"
|
|
40
|
+
/>
|
|
41
|
+
</template>
|
|
42
|
+
<h3 v-else class="text-xl font-semibold text-gray-900 dark:text-white">
|
|
34
43
|
{{ $t('Sign in to') }} {{ coreStore.config?.brandName }}
|
|
35
44
|
</h3>
|
|
36
45
|
</div>
|
|
37
46
|
<!-- Modal body -->
|
|
38
|
-
<div class="p-4 md:p-5">
|
|
47
|
+
<div class="af-login-modal-body p-4 md:p-5">
|
|
39
48
|
<form class="space-y-4" @submit.prevent>
|
|
40
49
|
<div>
|
|
41
50
|
<label for="username" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">{{ $t('Your') }} {{ coreStore.config?.usernameFieldName?.toLowerCase() }}</label>
|
|
42
|
-
<
|
|
51
|
+
<Input
|
|
52
|
+
v-model="username"
|
|
43
53
|
autocomplete="username"
|
|
44
54
|
type="username"
|
|
45
55
|
name="username"
|
|
46
56
|
id="username"
|
|
47
57
|
ref="usernameInput"
|
|
48
|
-
oninput="setCustomValidity('')"
|
|
49
58
|
@keydown.enter="passwordInput.focus()"
|
|
50
|
-
class="
|
|
59
|
+
class="w-full"
|
|
60
|
+
placeholder="name@company.com" required />
|
|
51
61
|
</div>
|
|
52
|
-
<div class="
|
|
62
|
+
<div class="">
|
|
53
63
|
<label for="password" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">{{ $t('Your password') }}</label>
|
|
54
|
-
<
|
|
64
|
+
<Input
|
|
65
|
+
v-model="password"
|
|
55
66
|
ref="passwordInput"
|
|
56
67
|
autocomplete="current-password"
|
|
57
|
-
oninput="setCustomValidity('')"
|
|
58
68
|
@keydown.enter="login"
|
|
59
|
-
:type="!showPw ? 'password': 'text'" name="password" id="password" placeholder="••••••••" class="
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
69
|
+
:type="!showPw ? 'password': 'text'" name="password" id="password" placeholder="••••••••" class="w-full" required>
|
|
70
|
+
<template #rightIcon>
|
|
71
|
+
<button type="button" @click="showPw = !showPw" class="text-gray-400 dark:text-gray-300">
|
|
72
|
+
<IconEyeSolid class="w-5 h-5" v-if="!showPw" />
|
|
73
|
+
<IconEyeSlashSolid class="w-5 h-5" v-else />
|
|
74
|
+
</button>
|
|
75
|
+
</template>
|
|
76
|
+
</Input>
|
|
64
77
|
</div>
|
|
65
78
|
|
|
66
79
|
<div v-if="coreStore.config.rememberMeDays"
|
|
@@ -79,7 +92,7 @@
|
|
|
79
92
|
:meta="c.meta"
|
|
80
93
|
/>
|
|
81
94
|
|
|
82
|
-
<div v-if="error" class="flex items-center p-4 mb-4 text-sm text-red-800 rounded-lg bg-red-50 dark:bg-gray-800 dark:text-red-400" role="alert">
|
|
95
|
+
<div v-if="error" class="af-login-modal-error flex items-center p-4 mb-4 text-sm text-red-800 rounded-lg bg-red-50 dark:bg-gray-800 dark:text-red-400" role="alert">
|
|
83
96
|
<svg class="flex-shrink-0 inline w-4 h-4 me-3" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 20 20">
|
|
84
97
|
<path d="M10 .5a9.5 9.5 0 1 0 9.5 9.5A9.51 9.51 0 0 0 10 .5ZM9.5 4a1.5 1.5 0 1 1 0 3 1.5 1.5 0 0 1 0-3ZM12 15H8a1 1 0 0 1 0-2h1v-3H8a1 1 0 0 1 0-2h2a1 1 0 0 1 1 1v4h1a1 1 0 0 1 0 2Z"/>
|
|
85
98
|
</svg>
|
|
@@ -121,7 +134,7 @@ import { useUserStore } from '@/stores/user';
|
|
|
121
134
|
import { IconEyeSolid, IconEyeSlashSolid } from '@iconify-prerendered/vue-flowbite';
|
|
122
135
|
import { callAdminForthApi, loadFile } from '@/utils';
|
|
123
136
|
import { useRoute, useRouter } from 'vue-router';
|
|
124
|
-
import { Button, Checkbox } from '@/afcl';
|
|
137
|
+
import { Button, Checkbox, Input } from '@/afcl';
|
|
125
138
|
import { useI18n } from 'vue-i18n';
|
|
126
139
|
|
|
127
140
|
const { t } = useI18n();
|
|
@@ -129,6 +142,8 @@ const { t } = useI18n();
|
|
|
129
142
|
const passwordInput = ref(null);
|
|
130
143
|
const usernameInput = ref(null);
|
|
131
144
|
const rememberMeValue= ref(false);
|
|
145
|
+
const username = ref('');
|
|
146
|
+
const password = ref('');
|
|
132
147
|
|
|
133
148
|
const route = useRoute();
|
|
134
149
|
const router = useRouter();
|
|
@@ -159,28 +174,15 @@ onBeforeMount(() => {
|
|
|
159
174
|
|
|
160
175
|
onMounted(async () => {
|
|
161
176
|
if (coreStore.config?.demoCredentials) {
|
|
162
|
-
const [
|
|
163
|
-
|
|
164
|
-
|
|
177
|
+
const [demoUsername, demoPassword] = coreStore.config.demoCredentials.split(':');
|
|
178
|
+
username.value = demoUsername;
|
|
179
|
+
password.value = demoPassword;
|
|
165
180
|
}
|
|
166
181
|
usernameInput.value.focus();
|
|
167
182
|
});
|
|
168
183
|
|
|
169
184
|
|
|
170
185
|
async function login() {
|
|
171
|
-
|
|
172
|
-
const username = usernameInput.value.value;
|
|
173
|
-
const password = passwordInput.value.value;
|
|
174
|
-
|
|
175
|
-
if (!username) {
|
|
176
|
-
usernameInput.value.setCustomValidity(t('Please fill out this field.'));
|
|
177
|
-
return;
|
|
178
|
-
}
|
|
179
|
-
if (!password) {
|
|
180
|
-
passwordInput.value.setCustomValidity(t('Please fill out this field.'));
|
|
181
|
-
return;
|
|
182
|
-
}
|
|
183
|
-
|
|
184
186
|
if (inProgress.value) {
|
|
185
187
|
return;
|
|
186
188
|
}
|
|
@@ -189,8 +191,8 @@ async function login() {
|
|
|
189
191
|
path: '/login',
|
|
190
192
|
method: 'POST',
|
|
191
193
|
body: {
|
|
192
|
-
username,
|
|
193
|
-
password,
|
|
194
|
+
username: username.value,
|
|
195
|
+
password: password.value,
|
|
194
196
|
rememberMe: rememberMeValue.value,
|
|
195
197
|
}
|
|
196
198
|
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div :key="`${$route?.params.resourceId}---${$route?.params.primaryKey}`" class="p-4 flex"
|
|
2
|
+
<div :key="`${$route?.params.resourceId}---${$route?.params.primaryKey}`" class="af-resource-parent p-4 flex"
|
|
3
3
|
:class="limitHeightToPage ? 'h-[calc(100dvh-3.5rem)]': undefined"
|
|
4
4
|
>
|
|
5
5
|
<RouterView/>
|
|
@@ -28,21 +28,21 @@
|
|
|
28
28
|
</template>
|
|
29
29
|
<RouterLink v-if="coreStore.resource?.options?.allowedActions?.create"
|
|
30
30
|
:to="{ name: 'resource-create', params: { resourceId: $route.params.resourceId } }"
|
|
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"
|
|
31
|
+
class="af-add-new-button 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"
|
|
32
32
|
>
|
|
33
33
|
<IconPlusOutline class="w-4 h-4 me-2"/>
|
|
34
34
|
{{ $t('Add new') }}
|
|
35
35
|
</RouterLink>
|
|
36
36
|
|
|
37
37
|
<RouterLink v-if="coreStore?.resourceOptions?.allowedActions?.edit" :to="{ name: 'resource-edit', params: { resourceId: $route.params.resourceId, primaryKey: $route.params.primaryKey } }"
|
|
38
|
-
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"
|
|
38
|
+
class="flex items-center af-edit-button 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"
|
|
39
39
|
>
|
|
40
40
|
<IconPenSolid class="w-4 h-4" />
|
|
41
41
|
{{ $t('Edit') }}
|
|
42
42
|
</RouterLink>
|
|
43
43
|
|
|
44
44
|
<button v-if="coreStore?.resourceOptions?.allowedActions?.delete" @click="deleteRecord"
|
|
45
|
-
class="flex items-center py-1 px-3 text-sm font-medium rounded-default text-red-600 focus:outline-none bg-white 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"
|
|
45
|
+
class="flex items-center af-delete-button py-1 px-3 text-sm font-medium rounded-default text-red-600 focus:outline-none bg-white 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"
|
|
46
46
|
>
|
|
47
47
|
<IconTrashBinSolid class="w-4 h-4" />
|
|
48
48
|
{{ $t('Delete') }}
|
package/dist/types/Back.d.ts
CHANGED
|
@@ -62,14 +62,14 @@ export interface IExpressHttpServer extends IHttpServer {
|
|
|
62
62
|
* Adds adminUser to request object if user is authorized. Drops request with 401 status if user is not authorized.
|
|
63
63
|
* @param callable : Function which will be called if user is authorized.
|
|
64
64
|
*
|
|
65
|
-
* Example:
|
|
66
65
|
*
|
|
66
|
+
* @example
|
|
67
67
|
* ```ts
|
|
68
|
-
* expressApp.get('/myApi', authorize((req, res) =>
|
|
68
|
+
* expressApp.get('/myApi', authorize((req, res) => {
|
|
69
69
|
* console.log('User is authorized', req.adminUser);
|
|
70
|
-
* res.json(
|
|
71
|
-
*
|
|
72
|
-
*
|
|
70
|
+
* res.json({ message: 'Hello World' });
|
|
71
|
+
* }));
|
|
72
|
+
* ```
|
|
73
73
|
*
|
|
74
74
|
*/
|
|
75
75
|
authorize(callable: Function): void;
|
|
@@ -315,6 +315,7 @@ export interface IAdminForth {
|
|
|
315
315
|
}): Promise<{
|
|
316
316
|
error?: string;
|
|
317
317
|
createdRecord?: any;
|
|
318
|
+
newRecordId?: any;
|
|
318
319
|
}>;
|
|
319
320
|
updateResourceRecord(params: {
|
|
320
321
|
resource: AdminForthResource;
|
|
@@ -451,6 +452,7 @@ export type BeforeDataSourceRequestFunction = (params: {
|
|
|
451
452
|
}) => Promise<{
|
|
452
453
|
ok: boolean;
|
|
453
454
|
error?: string;
|
|
455
|
+
newRecordId?: string;
|
|
454
456
|
}>;
|
|
455
457
|
/**
|
|
456
458
|
* Modify response to change how data is returned after fetching from database.
|
|
@@ -509,7 +511,7 @@ export type BeforeEditSaveFunction = (params: {
|
|
|
509
511
|
extra?: HttpExtra;
|
|
510
512
|
}) => Promise<{
|
|
511
513
|
ok: boolean;
|
|
512
|
-
error?: string;
|
|
514
|
+
error?: string | null;
|
|
513
515
|
}>;
|
|
514
516
|
export type BeforeCreateSaveFunction = (params: {
|
|
515
517
|
resource: AdminForthResource;
|
|
@@ -519,7 +521,8 @@ export type BeforeCreateSaveFunction = (params: {
|
|
|
519
521
|
extra?: HttpExtra;
|
|
520
522
|
}) => Promise<{
|
|
521
523
|
ok: boolean;
|
|
522
|
-
error?: string;
|
|
524
|
+
error?: string | null;
|
|
525
|
+
newRecordId?: string;
|
|
523
526
|
}>;
|
|
524
527
|
export type AfterCreateSaveFunction = (params: {
|
|
525
528
|
resource: AdminForthResource;
|
|
@@ -602,6 +605,10 @@ interface AdminForthInputConfigCustomization {
|
|
|
602
605
|
* Your app name
|
|
603
606
|
*/
|
|
604
607
|
brandName?: string;
|
|
608
|
+
/**
|
|
609
|
+
* Whether to use single theme for the app
|
|
610
|
+
*/
|
|
611
|
+
singleTheme?: 'light' | 'dark';
|
|
605
612
|
/**
|
|
606
613
|
* Whether to show brand name in sidebar
|
|
607
614
|
* default is true
|
|
@@ -718,6 +725,7 @@ interface AdminForthInputConfigCustomization {
|
|
|
718
725
|
*/
|
|
719
726
|
loginPageInjections?: {
|
|
720
727
|
underInputs?: AdminForthComponentDeclaration | Array<AdminForthComponentDeclaration>;
|
|
728
|
+
panelHeader?: AdminForthComponentDeclaration | Array<AdminForthComponentDeclaration>;
|
|
721
729
|
};
|
|
722
730
|
/**
|
|
723
731
|
* Custom panel components or array of components which will be displayed in different parts of the admin panel.
|
|
@@ -728,6 +736,14 @@ interface AdminForthInputConfigCustomization {
|
|
|
728
736
|
sidebar?: AdminForthComponentDeclaration | Array<AdminForthComponentDeclaration>;
|
|
729
737
|
everyPageBottom?: AdminForthComponentDeclaration | Array<AdminForthComponentDeclaration>;
|
|
730
738
|
};
|
|
739
|
+
/**
|
|
740
|
+
* Allows adding custom elements (e.g., <link>, <script>, <meta>) to the <head> of the HTML document.
|
|
741
|
+
* Each item must include a tag name and a set of attributes.
|
|
742
|
+
*/
|
|
743
|
+
customHeadItems?: {
|
|
744
|
+
tagName: string;
|
|
745
|
+
attributes: Record<string, string | boolean>;
|
|
746
|
+
}[];
|
|
731
747
|
}
|
|
732
748
|
export interface AdminForthActionInput {
|
|
733
749
|
name: string;
|
|
@@ -884,6 +900,12 @@ export interface AdminForthInputConfig {
|
|
|
884
900
|
* Default: '1/2'
|
|
885
901
|
*/
|
|
886
902
|
loginBackgroundPosition?: 'over' | '1/2' | '1/3' | '2/3' | '3/4' | '2/5' | '3/5';
|
|
903
|
+
/**
|
|
904
|
+
* If true, background blend mode will be removed from login background image when position is 'over'
|
|
905
|
+
*
|
|
906
|
+
* Default: false
|
|
907
|
+
*/
|
|
908
|
+
removeBackgroundBlendMode?: boolean;
|
|
887
909
|
/**
|
|
888
910
|
* Function or functions which will be called before user try to login.
|
|
889
911
|
* Each function will resive User object as an argument
|
|
@@ -984,6 +1006,7 @@ export interface AdminForthConfigCustomization extends Omit<AdminForthInputConfi
|
|
|
984
1006
|
customPages: Array<AdminForthPageDeclaration>;
|
|
985
1007
|
loginPageInjections: {
|
|
986
1008
|
underInputs: Array<AdminForthComponentDeclarationFull>;
|
|
1009
|
+
panelHeader: Array<AdminForthComponentDeclarationFull>;
|
|
987
1010
|
};
|
|
988
1011
|
globalInjections: {
|
|
989
1012
|
userMenu: Array<AdminForthComponentDeclarationFull>;
|
|
@@ -991,6 +1014,10 @@ export interface AdminForthConfigCustomization extends Omit<AdminForthInputConfi
|
|
|
991
1014
|
sidebar: Array<AdminForthComponentDeclarationFull>;
|
|
992
1015
|
everyPageBottom: Array<AdminForthComponentDeclarationFull>;
|
|
993
1016
|
};
|
|
1017
|
+
customHeadItems?: {
|
|
1018
|
+
tagName: string;
|
|
1019
|
+
attributes: Record<string, string | boolean>;
|
|
1020
|
+
}[];
|
|
994
1021
|
}
|
|
995
1022
|
export interface AdminForthConfig extends Omit<AdminForthInputConfig, 'customization' | 'resources'> {
|
|
996
1023
|
baseUrl: string;
|
|
@@ -1164,9 +1191,13 @@ export interface AdminForthResource extends Omit<AdminForthResourceInput, 'optio
|
|
|
1164
1191
|
};
|
|
1165
1192
|
create?: {
|
|
1166
1193
|
/**
|
|
1194
|
+
* Should return `ok: true` to continue saving pipeline and allow creating record in database, and `ok: false` to interrupt pipeline and prevent record creation.
|
|
1195
|
+
* If you need to show error on UI, set `error: \<error message\>` in response.
|
|
1196
|
+
*
|
|
1167
1197
|
* Typical use-cases:
|
|
1168
|
-
* -
|
|
1169
|
-
* -
|
|
1198
|
+
* - Create record by custom code (return `{ ok: false, newRecordId: <id of created record from custom code> }`)
|
|
1199
|
+
* - Validate record before saving to database and interrupt execution if validation failed (return `{ ok: false, error: <validation error> }`), though `allowedActions.create` should be preferred in most cases
|
|
1200
|
+
* - fill-in adminUser as creator of record (set `record.<some field> = x; return \{ ok: true \}`)
|
|
1170
1201
|
* - Attach additional data to record before saving to database (mostly fillOnCreate should be used instead)
|
|
1171
1202
|
*/
|
|
1172
1203
|
beforeSave?: Array<BeforeCreateSaveFunction>;
|