adminforth 2.25.1 → 2.26.0-next.10
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/bundle.js +2 -1
- package/commands/createApp/templates/Dockerfile.hbs +12 -0
- package/commands/createApp/templates/package.json.hbs +18 -6
- package/commands/createApp/templates/pnpm_templates/pnpm-lock.yaml.hbs +8 -0
- package/commands/createApp/templates/pnpm_templates/pnpm-workspace.yaml.hbs +2 -0
- package/commands/createApp/templates/readme.md.hbs +23 -4
- package/commands/createApp/utils.js +107 -26
- package/commands/createPlugin/utils.js +5 -6
- package/commands/postinstall.js +1 -1
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js.map +1 -1
- package/dist/dataConnectors/clickhouse.d.ts +4 -0
- package/dist/dataConnectors/clickhouse.d.ts.map +1 -1
- package/dist/dataConnectors/clickhouse.js +14 -0
- package/dist/dataConnectors/clickhouse.js.map +1 -1
- package/dist/dataConnectors/mongo.d.ts +4 -0
- package/dist/dataConnectors/mongo.d.ts.map +1 -1
- package/dist/dataConnectors/mongo.js +9 -0
- package/dist/dataConnectors/mongo.js.map +1 -1
- package/dist/dataConnectors/mysql.d.ts +4 -0
- package/dist/dataConnectors/mysql.d.ts.map +1 -1
- package/dist/dataConnectors/mysql.js +11 -0
- package/dist/dataConnectors/mysql.js.map +1 -1
- package/dist/dataConnectors/postgres.d.ts +4 -0
- package/dist/dataConnectors/postgres.d.ts.map +1 -1
- package/dist/dataConnectors/postgres.js +11 -0
- package/dist/dataConnectors/postgres.js.map +1 -1
- package/dist/dataConnectors/sqlite.d.ts +4 -0
- package/dist/dataConnectors/sqlite.d.ts.map +1 -1
- package/dist/dataConnectors/sqlite.js +11 -0
- package/dist/dataConnectors/sqlite.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/modules/codeInjector.d.ts +11 -2
- package/dist/modules/codeInjector.d.ts.map +1 -1
- package/dist/modules/codeInjector.js +239 -82
- package/dist/modules/codeInjector.js.map +1 -1
- package/dist/modules/styles.d.ts +4 -0
- package/dist/modules/styles.d.ts.map +1 -1
- package/dist/modules/styles.js +4 -0
- package/dist/modules/styles.js.map +1 -1
- package/dist/modules/utils.d.ts.map +1 -1
- package/dist/servers/express.d.ts +0 -1
- package/dist/servers/express.d.ts.map +1 -1
- package/dist/spa/README.md +4 -4
- package/dist/spa/package-lock.json +1902 -1427
- package/dist/spa/package.json +4 -3
- package/dist/spa/pnpm-lock.yaml +3558 -0
- package/dist/spa/src/afcl/Button.vue +12 -5
- package/dist/spa/src/afcl/Dialog.vue +155 -126
- package/dist/spa/src/afcl/Modal.vue +31 -8
- package/dist/spa/src/afcl/Select.vue +6 -2
- package/dist/spa/src/components/AcceptModal.vue +31 -53
- package/dist/spa/src/components/MenuLink.vue +18 -4
- package/dist/spa/src/components/ResourceListTable.vue +11 -11
- package/dist/spa/src/components/ResourceListTableVirtual.vue +21 -26
- package/dist/spa/src/components/Sidebar.vue +1 -1
- package/dist/spa/src/components/ThreeDotsMenu.vue +1 -1
- package/dist/spa/src/components/ValueRenderer.vue +1 -1
- package/dist/spa/src/stores/core.ts +1 -2
- package/dist/spa/src/types/Back.ts +4 -3
- package/dist/spa/src/types/Common.ts +16 -0
- package/dist/spa/src/types/FrontendAPI.ts +0 -3
- package/dist/spa/src/utils/utils.ts +57 -2
- package/dist/spa/src/views/CreateView.vue +12 -11
- package/dist/spa/src/views/EditView.vue +9 -13
- package/dist/spa/vite.config.ts +29 -40
- package/dist/types/Back.d.ts +3 -4
- package/dist/types/Back.d.ts.map +1 -1
- package/dist/types/Back.js.map +1 -1
- package/dist/types/Common.d.ts +11 -0
- package/dist/types/Common.d.ts.map +1 -1
- package/dist/types/Common.js.map +1 -1
- package/package.json +14 -6
- package/scripts/postinstall.js +25 -0
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
<div class="h-2 bg-lightListSkeletLoader rounded-full dark:bg-darkListSkeletLoader max-w-[360px]"></div>
|
|
11
11
|
</div>
|
|
12
12
|
</div>
|
|
13
|
-
<table v-else class="
|
|
13
|
+
<table v-else class="w-full text-sm text-left rtl:text-right text-lightListTableText dark:text-darkListTableText rounded-default">
|
|
14
14
|
|
|
15
15
|
<tbody>
|
|
16
16
|
<!-- table header -->
|
|
@@ -64,7 +64,7 @@
|
|
|
64
64
|
<!-- table header end -->
|
|
65
65
|
<SkeleteLoader
|
|
66
66
|
v-if="!rows"
|
|
67
|
-
:columns="resource?.columns.filter((c:
|
|
67
|
+
:columns="resource?.columns.filter((c: AdminForthResourceColumnCommon) => c.showIn?.list).length + 2"
|
|
68
68
|
:rows="rowHeights.length || 3"
|
|
69
69
|
:row-heights="rowHeights"
|
|
70
70
|
:column-widths="columnWidths"
|
|
@@ -182,7 +182,7 @@
|
|
|
182
182
|
|
|
183
183
|
<template v-if="resource.options?.actions">
|
|
184
184
|
<Tooltip
|
|
185
|
-
v-for="action in resource.options.actions.filter(a => a.showIn?.list
|
|
185
|
+
v-for="action in resource.options.actions.filter(a => a.showIn?.list)"
|
|
186
186
|
:key="action.id"
|
|
187
187
|
>
|
|
188
188
|
<component
|
|
@@ -339,7 +339,7 @@ import {
|
|
|
339
339
|
} from '@iconify-prerendered/vue-flowbite';
|
|
340
340
|
import router from '@/router';
|
|
341
341
|
import { Tooltip } from '@/afcl';
|
|
342
|
-
import type { AdminForthResourceCommon,
|
|
342
|
+
import type { AdminForthResourceCommon, AdminForthResourceColumnCommon, AdminForthComponentDeclarationFull } from '@/types/Common';
|
|
343
343
|
import { useAdminforth } from '@/adminforth';
|
|
344
344
|
import Checkbox from '@/afcl/Checkbox.vue';
|
|
345
345
|
import ListActionsThreeDots from '@/components/ListActionsThreeDots.vue';
|
|
@@ -360,7 +360,7 @@ const props = defineProps<{
|
|
|
360
360
|
customActionsInjection?: any[],
|
|
361
361
|
tableBodyStartInjection?: any[],
|
|
362
362
|
customActionIconsThreeDotsMenuItems?: any[]
|
|
363
|
-
tableRowReplaceInjection?:
|
|
363
|
+
tableRowReplaceInjection?: AdminForthComponentDeclarationFull,
|
|
364
364
|
}>();
|
|
365
365
|
|
|
366
366
|
// emits, update page
|
|
@@ -436,7 +436,7 @@ watch(() => props.rows, (newRows) => {
|
|
|
436
436
|
columnWidths.value = newRows || !headerRefs.value ? [] : [48, ...headerRefs.value.map((el: HTMLElement) => el.offsetWidth)];
|
|
437
437
|
});
|
|
438
438
|
|
|
439
|
-
function addToCheckedValues(id: string) {
|
|
439
|
+
function addToCheckedValues(id: string | number) {
|
|
440
440
|
if (checkboxesInternal.value.includes(id)) {
|
|
441
441
|
checkboxesInternal.value = checkboxesInternal.value.filter((item) => item !== id);
|
|
442
442
|
} else {
|
|
@@ -468,7 +468,7 @@ const allFromThisPageChecked = computed(() => {
|
|
|
468
468
|
if (!props.rows || !props.rows.length) return false;
|
|
469
469
|
return props.rows.every((r) => checkboxesInternal.value.includes(r._primaryKeyValue));
|
|
470
470
|
});
|
|
471
|
-
const ascArr = computed(() => sort.value.filter((s:any) => s.direction === 'asc').map((s: any) => s.field));
|
|
471
|
+
const ascArr = computed(() => sort.value.filter((s: any) => s.direction === 'asc').map((s: any) => s.field));
|
|
472
472
|
const descArr = computed(() => sort.value.filter((s: any) => s.direction === 'desc').map((s: any) => s.field));
|
|
473
473
|
|
|
474
474
|
|
|
@@ -487,9 +487,9 @@ function onSortButtonClick(event: any, field: string) {
|
|
|
487
487
|
} else {
|
|
488
488
|
const sortField = sort.value[sortIndex];
|
|
489
489
|
if (sortField.direction === 'asc') {
|
|
490
|
-
sort.value = sort.value.map((s
|
|
490
|
+
sort.value = sort.value.map((s) => s.field === field ? {field, direction: 'desc'} : s);
|
|
491
491
|
} else {
|
|
492
|
-
sort.value = sort.value.filter((s
|
|
492
|
+
sort.value = sort.value.filter((s) => s.field !== field);
|
|
493
493
|
}
|
|
494
494
|
}
|
|
495
495
|
}
|
|
@@ -576,7 +576,7 @@ async function deleteRecord(row: any) {
|
|
|
576
576
|
const actionLoadingStates = ref<Record<string | number, boolean>>({});
|
|
577
577
|
|
|
578
578
|
async function startCustomAction(actionId: string, row: any, extraData: Record<string, any> = {}) {
|
|
579
|
-
|
|
579
|
+
|
|
580
580
|
actionLoadingStates.value[actionId] = true;
|
|
581
581
|
|
|
582
582
|
const data = await callAdminForthApi({
|
|
@@ -586,7 +586,7 @@ async function startCustomAction(actionId: string, row: any, extraData: Record<s
|
|
|
586
586
|
resourceId: props.resource?.resourceId,
|
|
587
587
|
actionId: actionId,
|
|
588
588
|
recordId: row._primaryKeyValue,
|
|
589
|
-
extra: extraData
|
|
589
|
+
extra: extraData
|
|
590
590
|
}
|
|
591
591
|
});
|
|
592
592
|
|
|
@@ -18,8 +18,8 @@
|
|
|
18
18
|
|
|
19
19
|
<tbody>
|
|
20
20
|
<!-- table header -->
|
|
21
|
-
<tr class="t-header sticky z-20 top-0 text-xs
|
|
22
|
-
<td scope="col" class="list-table-header-cell
|
|
21
|
+
<tr class="t-header sticky z-20 top-0 text-xs text-lightListTableHeadingText bg-lightListTableHeading dark:bg-darkListTableHeading dark:text-darkListTableHeadingText">
|
|
22
|
+
<td scope="col" class="list-table-header-cell p-4 sticky-column bg-lightListTableHeading dark:bg-darkListTableHeading">
|
|
23
23
|
<Checkbox
|
|
24
24
|
:modelValue="allFromThisPageChecked"
|
|
25
25
|
:disabled="!rows || !rows.length"
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
</Checkbox>
|
|
30
30
|
</td>
|
|
31
31
|
|
|
32
|
-
<td v-for="c in columnsListed" ref="headerRefs" scope="col" class="list-table-header-cell
|
|
32
|
+
<td v-for="c in columnsListed" ref="headerRefs" scope="col" class="list-table-header-cell px-2 md:px-3 lg:px-6 py-3" :class="{'sticky-column bg-lightListTableHeading dark:bg-darkListTableHeading': c.listSticky}">
|
|
33
33
|
|
|
34
34
|
<div @click="(evt) => c.sortable && onSortButtonClick(evt, c.name)"
|
|
35
35
|
class="flex items-center " :class="{'cursor-pointer':c.sortable}">
|
|
@@ -69,7 +69,7 @@
|
|
|
69
69
|
<SkeleteLoader
|
|
70
70
|
v-if="!rows"
|
|
71
71
|
:columns="resource?.columns.filter((c: AdminForthResourceColumnCommon) => c.showIn?.list).length + 2"
|
|
72
|
-
:rows="rowHeights.length ||
|
|
72
|
+
:rows="rowHeights.length || 3"
|
|
73
73
|
:row-heights="rowHeights"
|
|
74
74
|
:column-widths="columnWidths"
|
|
75
75
|
/>
|
|
@@ -94,7 +94,6 @@
|
|
|
94
94
|
|
|
95
95
|
<!-- Visible rows -->
|
|
96
96
|
<component
|
|
97
|
-
v-else
|
|
98
97
|
v-for="(row, rowI) in visibleRows"
|
|
99
98
|
:is="tableRowReplaceInjection ? getCustomComponent(tableRowReplaceInjection) : 'tr'"
|
|
100
99
|
:key="`row_${row._primaryKeyValue}`"
|
|
@@ -108,7 +107,7 @@
|
|
|
108
107
|
:class="{'border-b': rowI !== visibleRows.length - 1, 'cursor-pointer': row._clickUrl !== null}"
|
|
109
108
|
@mounted="(el: any) => updateRowHeight(`row_${row._primaryKeyValue}`, el.offsetHeight)"
|
|
110
109
|
>
|
|
111
|
-
<td class="w-4 p-4 cursor-default sticky-column bg-
|
|
110
|
+
<td class="w-4 p-4 cursor-default sticky-column bg-lightListTable dark:bg-darkListTable" @click="(e)=>e.stopPropagation()">
|
|
112
111
|
<Checkbox
|
|
113
112
|
:model-value="checkboxesInternal.includes(row._primaryKeyValue)"
|
|
114
113
|
@change="(e: any)=>{addToCheckedValues(row._primaryKeyValue)}"
|
|
@@ -116,7 +115,7 @@
|
|
|
116
115
|
>
|
|
117
116
|
<span class="sr-only">{{ $t('checkbox') }}</span>
|
|
118
117
|
</Checkbox>
|
|
119
|
-
|
|
118
|
+
</td>
|
|
120
119
|
<td v-for="c in columnsListed" class="px-2 md:px-3 lg:px-6 py-4" :class="{'sticky-column bg-lightListTable dark:bg-darkListTable': c.listSticky}">
|
|
121
120
|
<!-- if c.name in listComponentsPerColumn, render it. If not, render ValueRenderer -->
|
|
122
121
|
<component
|
|
@@ -177,7 +176,7 @@
|
|
|
177
176
|
<template v-slot:tooltip>
|
|
178
177
|
{{ $t('Delete item') }}
|
|
179
178
|
</template>
|
|
180
|
-
</Tooltip>
|
|
179
|
+
</Tooltip>
|
|
181
180
|
<template v-if="customActionsInjection">
|
|
182
181
|
<component
|
|
183
182
|
v-for="c in customActionsInjection"
|
|
@@ -191,7 +190,7 @@
|
|
|
191
190
|
</template>
|
|
192
191
|
<template v-if="resource.options?.actions">
|
|
193
192
|
<Tooltip
|
|
194
|
-
v-for="action in resource.options.actions.filter(a => a.showIn?.list
|
|
193
|
+
v-for="action in resource.options.actions.filter(a => a.showIn?.list)"
|
|
195
194
|
:key="action.id"
|
|
196
195
|
>
|
|
197
196
|
<CallActionWrapper
|
|
@@ -251,9 +250,9 @@
|
|
|
251
250
|
<!-- pagination
|
|
252
251
|
totalRows in v-if is used to not hide page input during loading when user puts cursor into it and edit directly (rows gets null there during edit)
|
|
253
252
|
-->
|
|
254
|
-
<div class="flex flex-row items-center mt-4 xs:flex-row xs:justify-between xs:items-center gap-3">
|
|
253
|
+
<div class="af-pagination-container flex flex-row items-center mt-4 xs:flex-row xs:justify-between xs:items-center gap-3">
|
|
255
254
|
|
|
256
|
-
<div class="inline-flex "
|
|
255
|
+
<div class="af-pagination-buttons-container inline-flex "
|
|
257
256
|
v-if="(rows || totalRows) && totalRows >= pageSize && totalRows > 0"
|
|
258
257
|
>
|
|
259
258
|
<!-- Buttons -->
|
|
@@ -279,11 +278,10 @@
|
|
|
279
278
|
type="text"
|
|
280
279
|
v-model="pageInput"
|
|
281
280
|
:style="{ width: `${Math.max(1, pageInput.length+4)}ch` }"
|
|
282
|
-
class="af-pagination-input min-w-10 outline-none inline-block
|
|
281
|
+
class="af-pagination-input min-w-10 outline-none inline-block py-1.5 px-3 text-sm text-center text-lightListTablePaginationCurrentPageText border border-lightListTablePaginationBorder dark:border-darkListTablePaginationBorder dark:text-darkListTablePaginationCurrentPageText dark:bg-darkListTablePaginationBackgoround z-10"
|
|
283
282
|
@keydown="onPageKeydown($event)"
|
|
284
283
|
@blur="validatePageInput()"
|
|
285
|
-
|
|
286
|
-
</input>
|
|
284
|
+
/>
|
|
287
285
|
|
|
288
286
|
<button
|
|
289
287
|
class="af-pagination-last-page-button flex items-center py-1 px-3 text-sm font-medium text-lightListTablePaginationText focus:outline-none bg-lightListTablePaginationBackgoround border-l-0 border border-lightListTablePaginationBorder hover:bg-lightListTablePaginationBackgoroundHover hover:text-lightListTablePaginationTextHover focus:z-10 focus:ring-4 focus:ring-lightListTablePaginationFocusRing dark:focus:ring-darkListTablePaginationFocusRing dark:bg-darkListTablePaginationBackgoround dark:text-darkListTablePaginationText dark:border-darkListTablePaginationBorder dark:hover:text-white dark:hover:bg-darkListTablePaginationBackgoroundHover disabled:opacity-50"
|
|
@@ -309,7 +307,7 @@
|
|
|
309
307
|
<span v-if="((((page || 1) - 1) * pageSize + 1 > totalRows) && totalRows > 0)">{{ $t('Wrong Page') }} </span>
|
|
310
308
|
<template v-else-if="resource && totalRows > 0">
|
|
311
309
|
|
|
312
|
-
<span class="hidden sm:inline">
|
|
310
|
+
<span class="af-pagination-info hidden sm:inline">
|
|
313
311
|
<i18n-t keypath="Showing {from} to {to} of {total} Entries" tag="p" >
|
|
314
312
|
<template v-slot:from>
|
|
315
313
|
<strong>{{ from }}</strong>
|
|
@@ -343,7 +341,7 @@
|
|
|
343
341
|
<script setup lang="ts">
|
|
344
342
|
|
|
345
343
|
|
|
346
|
-
import { computed, onMounted, ref, watch, useTemplateRef, nextTick, type Ref
|
|
344
|
+
import { computed, onMounted, ref, watch, useTemplateRef, nextTick, type Ref } from 'vue';
|
|
347
345
|
import { callAdminForthApi } from '@/utils';
|
|
348
346
|
import { useI18n } from 'vue-i18n';
|
|
349
347
|
import ValueRenderer from '@/components/ValueRenderer.vue';
|
|
@@ -352,18 +350,15 @@ import { useCoreStore } from '@/stores/core';
|
|
|
352
350
|
import { showSuccesTost, showErrorTost } from '@/composables/useFrontendApi';
|
|
353
351
|
import SkeleteLoader from '@/components/SkeleteLoader.vue';
|
|
354
352
|
import { getIcon } from '@/utils';
|
|
355
|
-
import {
|
|
356
|
-
IconInboxOutline,
|
|
357
|
-
} from '@iconify-prerendered/vue-flowbite';
|
|
358
|
-
|
|
359
353
|
import {
|
|
360
354
|
IconEyeSolid,
|
|
361
355
|
IconPenSolid,
|
|
362
|
-
IconTrashBinSolid
|
|
356
|
+
IconTrashBinSolid,
|
|
357
|
+
IconInboxOutline
|
|
363
358
|
} from '@iconify-prerendered/vue-flowbite';
|
|
364
359
|
import router from '@/router';
|
|
365
360
|
import { Tooltip } from '@/afcl';
|
|
366
|
-
import type { AdminForthResourceCommon, AdminForthResourceColumnCommon,
|
|
361
|
+
import type { AdminForthResourceCommon, AdminForthResourceColumnCommon, AdminForthComponentDeclarationFull } from '@/types/Common';
|
|
367
362
|
import { useAdminforth } from '@/adminforth';
|
|
368
363
|
import Checkbox from '@/afcl/Checkbox.vue';
|
|
369
364
|
import ListActionsThreeDots from '@/components/ListActionsThreeDots.vue';
|
|
@@ -387,7 +382,7 @@ const props = defineProps<{
|
|
|
387
382
|
itemHeight?: number,
|
|
388
383
|
bufferSize?: number,
|
|
389
384
|
customActionIconsThreeDotsMenuItems?: any[]
|
|
390
|
-
tableRowReplaceInjection?:
|
|
385
|
+
tableRowReplaceInjection?: AdminForthComponentDeclarationFull
|
|
391
386
|
}>();
|
|
392
387
|
|
|
393
388
|
// emits, update page
|
|
@@ -463,7 +458,7 @@ watch(() => props.rows, (newRows) => {
|
|
|
463
458
|
columnWidths.value = newRows || !headerRefs.value ? [] : [48, ...headerRefs.value.map((el: HTMLElement) => el.offsetWidth)];
|
|
464
459
|
});
|
|
465
460
|
|
|
466
|
-
function addToCheckedValues(id:
|
|
461
|
+
function addToCheckedValues(id: string | number) {
|
|
467
462
|
if (checkboxesInternal.value.includes(id)) {
|
|
468
463
|
checkboxesInternal.value = checkboxesInternal.value.filter((item) => item !== id);
|
|
469
464
|
} else {
|
|
@@ -499,7 +494,7 @@ const ascArr = computed(() => sort.value.filter((s: any) => s.direction === 'asc
|
|
|
499
494
|
const descArr = computed(() => sort.value.filter((s: any) => s.direction === 'desc').map((s: any) => s.field));
|
|
500
495
|
|
|
501
496
|
|
|
502
|
-
function onSortButtonClick(event: any, field:
|
|
497
|
+
function onSortButtonClick(event: any, field: string) {
|
|
503
498
|
// if ctrl key is pressed, add to sort otherwise sort by this field
|
|
504
499
|
// in any case if field is already in sort, toggle direction
|
|
505
500
|
|
|
@@ -524,7 +519,7 @@ function onSortButtonClick(event: any, field: any) {
|
|
|
524
519
|
|
|
525
520
|
const clickTarget = ref(null);
|
|
526
521
|
|
|
527
|
-
async function onClick(e: any,row: any) {
|
|
522
|
+
async function onClick(e: any, row: any) {
|
|
528
523
|
if(clickTarget.value === e.target) return;
|
|
529
524
|
clickTarget.value = e.target;
|
|
530
525
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
:class="{
|
|
29
29
|
'hidden': !(coreStore.config?.showBrandLogoInSidebar !== false && (!iconOnlySidebarEnabled || !isSidebarIconOnly || (isSidebarIconOnly && isSidebarHovering))) }"
|
|
30
30
|
/>
|
|
31
|
-
<img :src="loadFile(coreStore.config?.iconOnlySidebar?.logo || '')" :alt="`${ coreStore.config?.brandName } Logo`" class="af-sidebar-icon-only-logo h-8" :class="{ 'hidden': !(coreStore.config?.showBrandLogoInSidebar !== false && coreStore.config?.iconOnlySidebar?.logo && iconOnlySidebarEnabled && isSidebarIconOnly && !isSidebarHovering) }" />
|
|
31
|
+
<img v-if="coreStore.config?.iconOnlySidebar?.logo" :src="loadFile(coreStore.config?.iconOnlySidebar?.logo || '')" :alt="`${ coreStore.config?.brandName } Logo`" class="af-sidebar-icon-only-logo h-8" :class="{ 'hidden': !(coreStore.config?.showBrandLogoInSidebar !== false && coreStore.config?.iconOnlySidebar?.logo && iconOnlySidebarEnabled && isSidebarIconOnly && !isSidebarHovering) }" />
|
|
32
32
|
<span
|
|
33
33
|
v-if="coreStore.config?.showBrandNameInSidebar && (!iconOnlySidebarEnabled || !isSidebarIconOnly || (isSidebarIconOnly && isSidebarHovering))"
|
|
34
34
|
class="af-title self-center text-lightNavbarText-size font-semibold sm:text-lightNavbarText-size whitespace-nowrap dark:text-darkSidebarText text-lightSidebarText"
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
'hidden': !showDropdown,
|
|
19
19
|
'left-0 md:left-auto': checkboxes && checkboxes.length > 0
|
|
20
20
|
}"
|
|
21
|
-
class="absolute z-30 mt-3 bg-lightThreeDotsMenuBodyBackground divide-y divide-gray-100 rounded-lg shadow w-
|
|
21
|
+
class="absolute z-30 mt-3 bg-lightThreeDotsMenuBodyBackground divide-y divide-gray-100 rounded-lg shadow w-max max-w-64 dark:bg-darkThreeDotsMenuBodyBackground dark:divide-gray-600 right-0">
|
|
22
22
|
<ul class="py-2 text-sm text-lightThreeDotsMenuBodyText dark:text-darkThreeDotsMenuBodyText" aria-labelledby="dropdownMenuIconButton">
|
|
23
23
|
<li v-for="(item, i) in threeDotsDropdownItems" :key="`dropdown-item-${i}`">
|
|
24
24
|
<div
|
|
@@ -117,7 +117,7 @@ import timezone from 'dayjs/plugin/timezone';
|
|
|
117
117
|
import {checkEmptyValues} from '@/utils';
|
|
118
118
|
import { useRoute, useRouter } from 'vue-router';
|
|
119
119
|
import { JsonViewer } from "vue3-json-viewer";
|
|
120
|
-
import "vue3-json-viewer/dist/
|
|
120
|
+
import "vue3-json-viewer/dist/vue3-json-viewer.css";
|
|
121
121
|
import type { AdminForthResourceColumnCommon } from '@/types/Common';
|
|
122
122
|
|
|
123
123
|
import { useCoreStore } from '@/stores/core';
|
|
@@ -92,6 +92,7 @@ export const useCoreStore = defineStore('core', () => {
|
|
|
92
92
|
|
|
93
93
|
// console.log('🔔 subscribeToMenuBadges', mi.badge, JSON.stringify(mi));
|
|
94
94
|
if (mi.badge !== undefined) {
|
|
95
|
+
websocket.unsubscribe(`/opentopic/update-menu-badge/${mi.itemId}`);
|
|
95
96
|
websocket.subscribe(`/opentopic/update-menu-badge/${mi.itemId}`, ({ badge }) => {
|
|
96
97
|
mi.badge = badge;
|
|
97
98
|
});
|
|
@@ -121,8 +122,6 @@ export const useCoreStore = defineStore('core', () => {
|
|
|
121
122
|
item.badge = badge;
|
|
122
123
|
}
|
|
123
124
|
});
|
|
124
|
-
// TODO: This thing was created for something. Find out why
|
|
125
|
-
// websocket.unsubscribeAll();
|
|
126
125
|
subscribeToMenuBadges();
|
|
127
126
|
|
|
128
127
|
}
|
|
@@ -1206,8 +1206,8 @@ interface AdminForthInputConfigCustomization {
|
|
|
1206
1206
|
*
|
|
1207
1207
|
* ```bashcreating rec
|
|
1208
1208
|
* cd custom
|
|
1209
|
-
*
|
|
1210
|
-
*
|
|
1209
|
+
* pnpm init -y
|
|
1210
|
+
* pnpm install highcharts highcharts-vue
|
|
1211
1211
|
* ```
|
|
1212
1212
|
*
|
|
1213
1213
|
* And specify vueUsesFile in AdminForth config:
|
|
@@ -1760,6 +1760,8 @@ export interface IOperationalResource {
|
|
|
1760
1760
|
update: (primaryKey: any, record: any) => Promise<any>;
|
|
1761
1761
|
|
|
1762
1762
|
delete: (primaryKey: any) => Promise<boolean>;
|
|
1763
|
+
|
|
1764
|
+
deleteMany?(recordIds: any[]): Promise<number>;
|
|
1763
1765
|
|
|
1764
1766
|
dataConnector: IAdminForthDataSourceConnectorBase;
|
|
1765
1767
|
}
|
|
@@ -1811,7 +1813,6 @@ export type AllowedActions = {
|
|
|
1811
1813
|
*/
|
|
1812
1814
|
export interface ResourceOptionsInput extends Omit<NonNullable<AdminForthResourceInputCommon['options']>, 'allowedActions' | 'bulkActions'> {
|
|
1813
1815
|
|
|
1814
|
-
baseActionsAsQuickIcons?: ('show' | 'edit' | 'delete')[],
|
|
1815
1816
|
/**
|
|
1816
1817
|
* Custom bulk actions list. Bulk actions available in list view when user selects multiple records by
|
|
1817
1818
|
* using checkboxes.
|
|
@@ -361,10 +361,24 @@ export interface AdminForthResourceInputCommon {
|
|
|
361
361
|
recordLabel?: (item: any) => string,
|
|
362
362
|
|
|
363
363
|
|
|
364
|
+
/**
|
|
365
|
+
* If true, user will not see warning about unsaved changes when tries to leave edit or create page with unsaved changes.
|
|
366
|
+
* default is false
|
|
367
|
+
*/
|
|
368
|
+
dontShowWarningAboutUnsavedChanges?: boolean,
|
|
369
|
+
|
|
364
370
|
/**
|
|
365
371
|
* General options for resource.
|
|
366
372
|
*/
|
|
367
373
|
options?: {
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Show quick action icons for base actions (show, edit, delete) in list view.
|
|
378
|
+
* By default, they are inside three dots dropdown menu.
|
|
379
|
+
*/
|
|
380
|
+
baseActionsAsQuickIcons?: ('show' | 'edit' | 'delete')[],
|
|
381
|
+
|
|
368
382
|
|
|
369
383
|
/**
|
|
370
384
|
* Default sort for list view.
|
|
@@ -1116,6 +1130,8 @@ export interface AdminForthConfigMenuItem {
|
|
|
1116
1130
|
* Item id will be automatically generated from hashed resourceId+Path+label
|
|
1117
1131
|
*/
|
|
1118
1132
|
itemId?: string, // todo move to runtime type
|
|
1133
|
+
|
|
1134
|
+
url?: string
|
|
1119
1135
|
}
|
|
1120
1136
|
|
|
1121
1137
|
|
|
@@ -5,13 +5,13 @@ 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
|
-
import adminforth from '../adminforth';
|
|
8
|
+
import adminforth, { useAdminforth } from '../adminforth';
|
|
9
9
|
import sanitizeHtml from 'sanitize-html'
|
|
10
10
|
import debounce from 'debounce';
|
|
11
11
|
import type { AdminForthResourceColumnInputCommon, Predicate } from '@/types/Common';
|
|
12
12
|
import { i18nInstance } from '../i18n'
|
|
13
13
|
import { useI18n } from 'vue-i18n';
|
|
14
|
-
|
|
14
|
+
import { onBeforeRouteLeave } from 'vue-router';
|
|
15
15
|
|
|
16
16
|
|
|
17
17
|
|
|
@@ -616,4 +616,59 @@ export function getTimeAgoString(date: Date): string {
|
|
|
616
616
|
const days = Math.floor(diffInSeconds / 86400);
|
|
617
617
|
return `${days} ${days === 1 ? 'day' : 'days'} ago`;
|
|
618
618
|
}
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
export class leaveGuardActiveClass {
|
|
622
|
+
private active = false;
|
|
623
|
+
|
|
624
|
+
isActive() {
|
|
625
|
+
return this.active;
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
setActive(value: boolean) {
|
|
629
|
+
this.active = value;
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
export async function onBeforeRouteLeaveCreateEditViewGuard(initialValues: any, record: any, checkIfWeCanLeavePage: () => boolean, leaveGuardActive: leaveGuardActiveClass, isActive: { value: boolean }): Promise<void> {
|
|
634
|
+
|
|
635
|
+
const { confirm } = useAdminforth();
|
|
636
|
+
const { t } = useI18n();
|
|
637
|
+
|
|
638
|
+
onBeforeRouteLeave(async (to, from) => {
|
|
639
|
+
if (!isActive.value) {
|
|
640
|
+
return true;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
|
|
644
|
+
if (leaveGuardActive.isActive()) {
|
|
645
|
+
return false;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
if (checkIfWeCanLeavePage()) {
|
|
649
|
+
return true;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
leaveGuardActive.setActive(true);
|
|
653
|
+
|
|
654
|
+
try {
|
|
655
|
+
const { changedFields } = compareOldAndNewRecord(
|
|
656
|
+
initialValues.value,
|
|
657
|
+
record.value
|
|
658
|
+
);
|
|
659
|
+
|
|
660
|
+
const messageHtml =
|
|
661
|
+
generateMessageHtmlForRecordChange(changedFields, t);
|
|
662
|
+
|
|
663
|
+
const answer = await confirm({
|
|
664
|
+
messageHtml,
|
|
665
|
+
yes: t('Yes'),
|
|
666
|
+
no: t('No'),
|
|
667
|
+
});
|
|
668
|
+
|
|
669
|
+
return answer;
|
|
670
|
+
} finally {
|
|
671
|
+
leaveGuardActive.setActive(false);
|
|
672
|
+
}
|
|
673
|
+
});
|
|
619
674
|
}
|
|
@@ -79,7 +79,7 @@ import BreadcrumbsWithButtons from '@/components/BreadcrumbsWithButtons.vue';
|
|
|
79
79
|
import ResourceForm from '@/components/ResourceForm.vue';
|
|
80
80
|
import SingleSkeletLoader from '@/components/SingleSkeletLoader.vue';
|
|
81
81
|
import { useCoreStore } from '@/stores/core';
|
|
82
|
-
import { callAdminForthApi, getCustomComponent,checkAcessByAllowedActions, initThreeDotsDropdown, checkShowIf, compareOldAndNewRecord,
|
|
82
|
+
import { callAdminForthApi, getCustomComponent,checkAcessByAllowedActions, initThreeDotsDropdown, checkShowIf, compareOldAndNewRecord, onBeforeRouteLeaveCreateEditViewGuard, leaveGuardActiveClass, onBeforeRouteLeaveCreateEditView } from '@/utils';
|
|
83
83
|
import { IconFloppyDiskSolid } from '@iconify-prerendered/vue-flowbite';
|
|
84
84
|
import { onMounted, onBeforeMount, onBeforeUnmount, ref, watch, nextTick } from 'vue';
|
|
85
85
|
import { useRoute, useRouter, onBeforeRouteLeave } from 'vue-router';
|
|
@@ -115,6 +115,7 @@ const readonlyColumns = ref([]);
|
|
|
115
115
|
|
|
116
116
|
const cancelButtonClicked = ref(false);
|
|
117
117
|
const wasSaveSuccessful = ref(false);
|
|
118
|
+
const useLeaveGuard = ref( false );
|
|
118
119
|
|
|
119
120
|
async function onUpdateRecord(newRecord: any) {
|
|
120
121
|
record.value = newRecord;
|
|
@@ -125,6 +126,9 @@ function checkIfWeCanLeavePage() {
|
|
|
125
126
|
}
|
|
126
127
|
|
|
127
128
|
function onBeforeUnload(event: BeforeUnloadEvent) {
|
|
129
|
+
if (!useLeaveGuard.value) {
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
128
132
|
if (!checkIfWeCanLeavePage()) {
|
|
129
133
|
event.preventDefault();
|
|
130
134
|
event.returnValue = '';
|
|
@@ -137,23 +141,20 @@ onBeforeUnmount(() => {
|
|
|
137
141
|
window.removeEventListener('beforeunload', onBeforeUnload);
|
|
138
142
|
});
|
|
139
143
|
|
|
140
|
-
onBeforeRouteLeave(async (to, from, next) => {
|
|
141
|
-
if (!checkIfWeCanLeavePage()) {
|
|
142
|
-
const { changedFields } = compareOldAndNewRecord(initialValues.value, record.value);
|
|
143
|
-
|
|
144
|
-
const messageHtml = generateMessageHtmlForRecordChange(changedFields, t);
|
|
145
144
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
145
|
+
const leaveGuardActive = new leaveGuardActiveClass();
|
|
146
|
+
|
|
147
|
+
onBeforeRouteLeaveCreateEditViewGuard(initialValues, record, checkIfWeCanLeavePage, leaveGuardActive, useLeaveGuard);
|
|
148
|
+
|
|
149
|
+
|
|
151
150
|
|
|
152
151
|
onBeforeMount(() => {
|
|
153
152
|
clearSaveInterceptors(route.params.resourceId as string);
|
|
154
153
|
});
|
|
155
154
|
|
|
156
155
|
onMounted(async () => {
|
|
156
|
+
useLeaveGuard.value = coreStore.resource?.options?.dontShowWarningAboutUnsavedChanges !== true;
|
|
157
|
+
|
|
157
158
|
loading.value = true;
|
|
158
159
|
await coreStore.fetchResourceFull({
|
|
159
160
|
resourceId: route.params.resourceId as string,
|
|
@@ -74,7 +74,7 @@ import BreadcrumbsWithButtons from '@/components/BreadcrumbsWithButtons.vue';
|
|
|
74
74
|
import ResourceForm from '@/components/ResourceForm.vue';
|
|
75
75
|
import SingleSkeletLoader from '@/components/SingleSkeletLoader.vue';
|
|
76
76
|
import { useCoreStore } from '@/stores/core';
|
|
77
|
-
import { callAdminForthApi, getCustomComponent,checkAcessByAllowedActions, initThreeDotsDropdown, compareOldAndNewRecord, generateMessageHtmlForRecordChange } from '@/utils';
|
|
77
|
+
import { callAdminForthApi, getCustomComponent,checkAcessByAllowedActions, initThreeDotsDropdown, compareOldAndNewRecord, generateMessageHtmlForRecordChange, leaveGuardActiveClass, onBeforeRouteLeaveCreateEditViewGuard } from '@/utils';
|
|
78
78
|
import { IconFloppyDiskSolid } from '@iconify-prerendered/vue-flowbite';
|
|
79
79
|
import { computed, onMounted, onBeforeMount, ref, type Ref, nextTick, watch, onBeforeUnmount } from 'vue';
|
|
80
80
|
import { useRoute, useRouter, onBeforeRouteLeave } from 'vue-router';
|
|
@@ -104,8 +104,12 @@ const record: Ref<Record<string, any>> = ref({});
|
|
|
104
104
|
const initialRecord = computed(() => coreStore.record);
|
|
105
105
|
const wasSaveSuccessful = ref(false);
|
|
106
106
|
const cancelButtonClicked = ref(false);
|
|
107
|
-
|
|
107
|
+
const useLeaveGuard = ref( false );
|
|
108
|
+
|
|
108
109
|
function onBeforeUnload(event: BeforeUnloadEvent) {
|
|
110
|
+
if (!useLeaveGuard.value) {
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
109
113
|
if (!checkIfWeCanLeavePage()) {
|
|
110
114
|
event.preventDefault();
|
|
111
115
|
event.returnValue = '';
|
|
@@ -122,17 +126,8 @@ onBeforeUnmount(() => {
|
|
|
122
126
|
window.removeEventListener('beforeunload', onBeforeUnload);
|
|
123
127
|
});
|
|
124
128
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
const { changedFields } = compareOldAndNewRecord(initialRecord.value, record.value);
|
|
128
|
-
|
|
129
|
-
const messageHtml = generateMessageHtmlForRecordChange(changedFields, t);
|
|
130
|
-
|
|
131
|
-
const answer = await confirm({ messageHtml: messageHtml, yes: t('Yes'), no: t('No') });
|
|
132
|
-
if (!answer) return next(false);
|
|
133
|
-
}
|
|
134
|
-
next();
|
|
135
|
-
});
|
|
129
|
+
const leaveGuardActive = new leaveGuardActiveClass();
|
|
130
|
+
onBeforeRouteLeaveCreateEditViewGuard(initialRecord, record, checkIfWeCanLeavePage, leaveGuardActive, useLeaveGuard);
|
|
136
131
|
|
|
137
132
|
const resourceFormRef = ref<InstanceType<typeof ResourceForm> | null>(null);
|
|
138
133
|
|
|
@@ -162,6 +157,7 @@ onBeforeMount(() => {
|
|
|
162
157
|
});
|
|
163
158
|
|
|
164
159
|
onMounted(async () => {
|
|
160
|
+
useLeaveGuard.value = coreStore.resource?.options?.dontShowWarningAboutUnsavedChanges !== true;
|
|
165
161
|
loading.value = true;
|
|
166
162
|
|
|
167
163
|
await coreStore.fetchResourceFull({
|
package/dist/spa/vite.config.ts
CHANGED
|
@@ -15,46 +15,6 @@ async function getNextAvailablePort(startPort: number | undefined) {
|
|
|
15
15
|
return await portfinder.getPortPromise({ port: startPort });
|
|
16
16
|
};
|
|
17
17
|
|
|
18
|
-
function ignoreTailwindErrors(): Plugin {
|
|
19
|
-
return {
|
|
20
|
-
name: 'ignore-tailwind-errors',
|
|
21
|
-
configureServer(server) {
|
|
22
|
-
server.middlewares.use((req, res, next) => {
|
|
23
|
-
const originalWrite = res.write;
|
|
24
|
-
res.write = function(chunk) {
|
|
25
|
-
if (typeof chunk === 'string' && chunk.includes('tailwind')) {
|
|
26
|
-
return true;
|
|
27
|
-
}
|
|
28
|
-
return originalWrite.call(this, chunk);
|
|
29
|
-
};
|
|
30
|
-
next();
|
|
31
|
-
});
|
|
32
|
-
},
|
|
33
|
-
config(config, { command }) {
|
|
34
|
-
if (command === 'build') {
|
|
35
|
-
// Override PostCSS config for build
|
|
36
|
-
config.css = config.css || {};
|
|
37
|
-
config.css.postcss = {
|
|
38
|
-
plugins: [
|
|
39
|
-
{
|
|
40
|
-
postcssPlugin: 'ignore-tailwind-errors',
|
|
41
|
-
Once(root, helpers) {
|
|
42
|
-
try {
|
|
43
|
-
return tailwindcss()(root, helpers);
|
|
44
|
-
} catch (error: any) {
|
|
45
|
-
console.warn('TailwindCSS warning ignored:', error.message);
|
|
46
|
-
return;
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
]
|
|
51
|
-
};
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
};
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
|
|
58
18
|
const appPort = await getNextAvailablePort(5173);
|
|
59
19
|
const hmrPort = await getNextAvailablePort(5273);
|
|
60
20
|
console.log(`SPA port: ${appPort}. HMR port: ${hmrPort}`);
|
|
@@ -89,7 +49,36 @@ export default defineConfig({
|
|
|
89
49
|
return id.toString().split('node_modules/')[1].split('/')[0].toString();
|
|
90
50
|
}
|
|
91
51
|
},
|
|
52
|
+
|
|
53
|
+
// by default servers doesnt returns dotfiles
|
|
54
|
+
// so if we'll generate file with name like ".pnpm-BLnlxqcJ.js"
|
|
55
|
+
// it won't be loaded
|
|
56
|
+
assetFileNames: (chunkInfo) => {
|
|
57
|
+
if (chunkInfo.name && chunkInfo.name.startsWith('.pnpm')) {
|
|
58
|
+
return `assets/pnpm-${chunkInfo.name.slice(6)}-[hash].[ext]`;
|
|
59
|
+
}
|
|
60
|
+
return 'assets/[name]-[hash].[ext]';
|
|
61
|
+
},
|
|
62
|
+
entryFileNames: (chunkInfo) => {
|
|
63
|
+
if (chunkInfo.name && chunkInfo.name.startsWith('.pnpm')) {
|
|
64
|
+
return `assets/pnpm-${chunkInfo.name.slice(6)}-[hash].js`;
|
|
65
|
+
}
|
|
66
|
+
return 'assets/[name]-[hash].js';
|
|
67
|
+
},
|
|
68
|
+
chunkFileNames: (chunkInfo) => {
|
|
69
|
+
if (chunkInfo.name && chunkInfo.name.startsWith('.pnpm')) {
|
|
70
|
+
return `assets/pnpm-${chunkInfo.name.slice(6)}-[hash].js`;
|
|
71
|
+
}
|
|
72
|
+
return 'assets/[name]-[hash].js';
|
|
73
|
+
}
|
|
92
74
|
},
|
|
93
75
|
},
|
|
94
76
|
},
|
|
77
|
+
css: {
|
|
78
|
+
preprocessorOptions: {
|
|
79
|
+
scss: {
|
|
80
|
+
api: 'modern-compiler'
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
95
84
|
})
|