adminforth 2.7.18 → 2.8.0
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/index.ts.hbs +2 -1
- package/commands/createCustomComponent/main.js +1 -0
- package/commands/createCustomComponent/templates/customCrud/beforeActionButtons.vue.hbs +38 -0
- package/commands/createPlugin/templates/custom/tsconfig.json.hbs +2 -5
- package/dist/dataConnectors/baseConnector.d.ts.map +1 -1
- package/dist/dataConnectors/baseConnector.js +33 -15
- package/dist/dataConnectors/baseConnector.js.map +1 -1
- package/dist/dataConnectors/clickhouse.d.ts.map +1 -1
- package/dist/dataConnectors/clickhouse.js +15 -0
- package/dist/dataConnectors/clickhouse.js.map +1 -1
- package/dist/dataConnectors/mongo.d.ts.map +1 -1
- package/dist/dataConnectors/mongo.js +30 -1
- package/dist/dataConnectors/mongo.js.map +1 -1
- 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.map +1 -1
- package/dist/dataConnectors/postgres.js +11 -0
- package/dist/dataConnectors/postgres.js.map +1 -1
- 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/modules/codeInjector.d.ts +1 -0
- package/dist/modules/codeInjector.d.ts.map +1 -1
- package/dist/modules/codeInjector.js +4 -0
- package/dist/modules/codeInjector.js.map +1 -1
- package/dist/modules/configValidator.d.ts.map +1 -1
- package/dist/modules/configValidator.js +6 -2
- package/dist/modules/configValidator.js.map +1 -1
- package/dist/modules/restApi.d.ts.map +1 -1
- package/dist/modules/restApi.js +2 -0
- package/dist/modules/restApi.js.map +1 -1
- package/dist/modules/styles.d.ts +8 -1
- package/dist/modules/styles.d.ts.map +1 -1
- package/dist/modules/styles.js +9 -2
- package/dist/modules/styles.js.map +1 -1
- package/dist/spa/src/App.vue +12 -4
- package/dist/spa/src/adminforth.ts +31 -11
- package/dist/spa/src/afcl/BarChart.vue +2 -2
- package/dist/spa/src/afcl/Checkbox.vue +2 -2
- package/dist/spa/src/afcl/Dialog.vue +38 -21
- package/dist/spa/src/afcl/Dropzone.vue +2 -2
- package/dist/spa/src/afcl/Input.vue +1 -1
- package/dist/spa/src/afcl/LinkButton.vue +1 -1
- package/dist/spa/src/afcl/PieChart.vue +5 -5
- package/dist/spa/src/afcl/Select.vue +17 -12
- package/dist/spa/src/afcl/Table.vue +202 -72
- package/dist/spa/src/afcl/Textarea.vue +31 -0
- package/dist/spa/src/afcl/Toggle.vue +2 -2
- package/dist/spa/src/afcl/Tooltip.vue +0 -1
- package/dist/spa/src/afcl/index.ts +1 -1
- package/dist/spa/src/components/AcceptModal.vue +1 -1
- package/dist/spa/src/components/ColumnValueInput.vue +11 -11
- package/dist/spa/src/components/ColumnValueInputWrapper.vue +2 -2
- package/dist/spa/src/components/CustomRangePicker.vue +5 -5
- package/dist/spa/src/components/ErrorMessage.vue +21 -0
- package/dist/spa/src/components/Filters.vue +7 -6
- package/dist/spa/src/components/GroupsTable.vue +2 -1
- package/dist/spa/src/components/MenuLink.vue +3 -3
- package/dist/spa/src/components/ResourceForm.vue +35 -27
- package/dist/spa/src/components/ResourceListTable.vue +42 -42
- package/dist/spa/src/components/ResourceListTableVirtual.vue +41 -41
- package/dist/spa/src/components/ShowTable.vue +3 -3
- package/dist/spa/src/components/SkeleteLoader.vue +2 -2
- package/dist/spa/src/components/ThreeDotsMenu.vue +69 -10
- package/dist/spa/src/components/Toast.vue +25 -2
- package/dist/spa/src/components/ValueRenderer.vue +39 -12
- package/dist/spa/src/i18n.ts +1 -1
- package/dist/spa/src/shims-vue.d.ts +5 -0
- package/dist/spa/src/spa_types/core.ts +1 -1
- package/dist/spa/src/stores/modal.ts +6 -1
- package/dist/spa/src/stores/toast.ts +22 -3
- package/dist/spa/src/types/Back.ts +31 -6
- package/dist/spa/src/types/Common.ts +33 -24
- package/dist/spa/src/types/FrontendAPI.ts +21 -5
- package/dist/spa/src/types/adapters/EmailAdapter.ts +2 -4
- package/dist/spa/src/types/adapters/ImageVisionAdapter.ts +30 -0
- package/dist/spa/src/types/adapters/index.ts +1 -0
- package/dist/spa/src/utils.ts +8 -7
- package/dist/spa/src/views/CreateView.vue +15 -16
- package/dist/spa/src/views/EditView.vue +23 -17
- package/dist/spa/src/views/ListView.vue +116 -66
- package/dist/spa/src/views/LoginView.vue +2 -9
- package/dist/spa/src/views/ResourceParent.vue +1 -1
- package/dist/spa/src/views/ShowView.vue +59 -39
- package/dist/spa/src/websocket.ts +6 -1
- package/dist/spa/tsconfig.app.json +1 -1
- package/dist/spa/vite.config.ts +45 -2
- package/dist/types/Back.d.ts +16 -1
- package/dist/types/Back.d.ts.map +1 -1
- package/dist/types/Back.js +15 -0
- package/dist/types/Back.js.map +1 -1
- package/dist/types/Common.d.ts +27 -22
- package/dist/types/Common.d.ts.map +1 -1
- package/dist/types/Common.js.map +1 -1
- package/dist/types/FrontendAPI.d.ts +21 -3
- package/dist/types/FrontendAPI.d.ts.map +1 -1
- package/dist/types/FrontendAPI.js.map +1 -1
- package/dist/types/adapters/EmailAdapter.d.ts +2 -3
- package/dist/types/adapters/EmailAdapter.d.ts.map +1 -1
- package/dist/types/adapters/ImageVisionAdapter.d.ts +25 -0
- package/dist/types/adapters/ImageVisionAdapter.d.ts.map +1 -0
- package/dist/types/adapters/ImageVisionAdapter.js +2 -0
- package/dist/types/adapters/ImageVisionAdapter.js.map +1 -0
- package/dist/types/adapters/index.d.ts +1 -0
- package/dist/types/adapters/index.d.ts.map +1 -1
- package/package.json +2 -1
|
@@ -51,8 +51,8 @@
|
|
|
51
51
|
</div>
|
|
52
52
|
<span
|
|
53
53
|
class="bg-red-100 text-red-800 text-xs font-medium me-1 px-1 py-0.5 rounded dark:bg-gray-700 dark:text-red-400 border border-red-400"
|
|
54
|
-
v-if="sort.findIndex((s) => s.field === c.name) !== -1 && sort?.length > 1">
|
|
55
|
-
{{ sort.findIndex((s) => s.field === c.name) + 1 }}
|
|
54
|
+
v-if="sort.findIndex((s: any) => s.field === c.name) !== -1 && sort?.length > 1">
|
|
55
|
+
{{ sort.findIndex((s: any) => s.field === c.name) + 1 }}
|
|
56
56
|
</span>
|
|
57
57
|
|
|
58
58
|
</div>
|
|
@@ -68,7 +68,7 @@
|
|
|
68
68
|
<!-- table header end -->
|
|
69
69
|
<SkeleteLoader
|
|
70
70
|
v-if="!rows"
|
|
71
|
-
:columns="resource?.columns.filter(c => c.showIn
|
|
71
|
+
:columns="resource?.columns.filter((c: AdminForthResourceColumnCommon) => c.showIn?.list).length + 2"
|
|
72
72
|
:rows="rowHeights.length || 20"
|
|
73
73
|
:row-heights="rowHeights"
|
|
74
74
|
:column-widths="columnWidths"
|
|
@@ -99,13 +99,13 @@
|
|
|
99
99
|
ref="rowRefs"
|
|
100
100
|
class="bg-lightListTable dark:bg-darkListTable border-lightListBorder dark:border-gray-700 hover:bg-lightListTableRowHover dark:hover:bg-darkListTableRowHover"
|
|
101
101
|
:class="{'border-b': rowI !== visibleRows.length - 1, 'cursor-pointer': row._clickUrl !== null}"
|
|
102
|
-
@mounted="(el) => updateRowHeight(`row_${row._primaryKeyValue}`, el.offsetHeight)"
|
|
102
|
+
@mounted="(el: any) => updateRowHeight(`row_${row._primaryKeyValue}`, el.offsetHeight)"
|
|
103
103
|
>
|
|
104
104
|
<td class="w-4 p-4 cursor-default" @click="(e)=>e.stopPropagation()">
|
|
105
105
|
<Checkbox
|
|
106
106
|
:model-value="checkboxesInternal.includes(row._primaryKeyValue)"
|
|
107
|
-
@change="(e)=>{addToCheckedValues(row._primaryKeyValue)}"
|
|
108
|
-
@click="(e)=>e.stopPropagation()"
|
|
107
|
+
@change="(e: any)=>{addToCheckedValues(row._primaryKeyValue)}"
|
|
108
|
+
@click="(e: any)=>e.stopPropagation()"
|
|
109
109
|
>
|
|
110
110
|
<span class="sr-only">{{ $t('checkbox') }}</span>
|
|
111
111
|
</Checkbox>
|
|
@@ -113,8 +113,8 @@
|
|
|
113
113
|
<td v-for="c in columnsListed" class="px-2 md:px-3 lg:px-6 py-4">
|
|
114
114
|
<!-- if c.name in listComponentsPerColumn, render it. If not, render ValueRenderer -->
|
|
115
115
|
<component
|
|
116
|
-
:is="c?.components?.list ? getCustomComponent(c.components.list) : ValueRenderer"
|
|
117
|
-
:meta="c?.components?.list
|
|
116
|
+
:is="c?.components?.list ? getCustomComponent(typeof c.components.list === 'string' ? { file: c.components.list } : c.components.list) : ValueRenderer"
|
|
117
|
+
:meta="typeof c?.components?.list === 'object' ? c.components.list.meta : undefined"
|
|
118
118
|
:column="c"
|
|
119
119
|
:record="row"
|
|
120
120
|
:adminUser="coreStore.adminUser"
|
|
@@ -125,7 +125,7 @@
|
|
|
125
125
|
<div class="flex text-lightPrimary dark:text-darkPrimary items-center">
|
|
126
126
|
<Tooltip>
|
|
127
127
|
<RouterLink
|
|
128
|
-
v-if="resource.options?.allowedActions
|
|
128
|
+
v-if="resource.options?.allowedActions?.show"
|
|
129
129
|
:to="{
|
|
130
130
|
name: 'resource-show',
|
|
131
131
|
params: {
|
|
@@ -145,7 +145,7 @@
|
|
|
145
145
|
|
|
146
146
|
<Tooltip>
|
|
147
147
|
<RouterLink
|
|
148
|
-
v-if="resource.options?.allowedActions
|
|
148
|
+
v-if="resource.options?.allowedActions?.edit"
|
|
149
149
|
:to="{
|
|
150
150
|
name: 'resource-edit',
|
|
151
151
|
params: {
|
|
@@ -163,7 +163,7 @@
|
|
|
163
163
|
|
|
164
164
|
<Tooltip>
|
|
165
165
|
<button
|
|
166
|
-
v-if="resource.options?.allowedActions
|
|
166
|
+
v-if="resource.options?.allowedActions?.delete"
|
|
167
167
|
@click="deleteRecord(row)"
|
|
168
168
|
>
|
|
169
169
|
<IconTrashBinSolid class="w-5 h-5 me-2"/>
|
|
@@ -325,7 +325,7 @@ import {
|
|
|
325
325
|
} from '@iconify-prerendered/vue-flowbite';
|
|
326
326
|
import router from '@/router';
|
|
327
327
|
import { Tooltip } from '@/afcl';
|
|
328
|
-
import type { AdminForthResourceCommon } from '@/types/Common';
|
|
328
|
+
import type { AdminForthResourceCommon, AdminForthResourceColumnCommon } from '@/types/Common';
|
|
329
329
|
import adminforth from '@/adminforth';
|
|
330
330
|
import Checkbox from '@/afcl/Checkbox.vue';
|
|
331
331
|
|
|
@@ -359,7 +359,7 @@ const emits = defineEmits([
|
|
|
359
359
|
const checkboxesInternal: Ref<any[]> = ref([]);
|
|
360
360
|
const pageInput = ref('1');
|
|
361
361
|
const page = ref(1);
|
|
362
|
-
const sort = ref([]);
|
|
362
|
+
const sort: Ref<Array<{field: string, direction: string}>> = ref([]);
|
|
363
363
|
|
|
364
364
|
|
|
365
365
|
const from = computed(() => ((page.value || 1) - 1) * props.pageSize + 1);
|
|
@@ -368,11 +368,11 @@ const to = computed(() => Math.min((page.value || 1) * props.pageSize, props.tot
|
|
|
368
368
|
watch(() => page.value, (newPage) => {
|
|
369
369
|
emits('update:page', newPage);
|
|
370
370
|
});
|
|
371
|
-
async function onPageKeydown(event) {
|
|
371
|
+
async function onPageKeydown(event: any) {
|
|
372
372
|
// page input should accept only numbers, arrow keys and backspace
|
|
373
373
|
if (['Enter', 'Space'].includes(event.code) ||
|
|
374
374
|
(!['Backspace', 'ArrowRight', 'ArrowLeft'].includes(event.code)
|
|
375
|
-
&& isNaN(String.fromCharCode(event.keyCode)))) {
|
|
375
|
+
&& isNaN(Number(String.fromCharCode(event.keyCode || 0))))) {
|
|
376
376
|
event.preventDefault();
|
|
377
377
|
if (event.code === 'Enter') {
|
|
378
378
|
validatePageInput();
|
|
@@ -393,7 +393,7 @@ watch(() => props.checkboxes, (newCheckboxes) => {
|
|
|
393
393
|
checkboxesInternal.value = newCheckboxes;
|
|
394
394
|
});
|
|
395
395
|
|
|
396
|
-
watch(() => props.sort, (newSort) => {
|
|
396
|
+
watch(() => props.sort, (newSort: any) => {
|
|
397
397
|
sort.value = newSort;
|
|
398
398
|
});
|
|
399
399
|
|
|
@@ -404,17 +404,17 @@ watch(() => props.page, (newPage) => {
|
|
|
404
404
|
page.value = newPage;
|
|
405
405
|
});
|
|
406
406
|
|
|
407
|
-
const rowRefs = useTemplateRef('rowRefs');
|
|
408
|
-
const headerRefs = useTemplateRef('headerRefs');
|
|
409
|
-
const rowHeights = ref([]);
|
|
410
|
-
const columnWidths = ref([]);
|
|
407
|
+
const rowRefs = useTemplateRef<HTMLElement[]>('rowRefs');
|
|
408
|
+
const headerRefs = useTemplateRef<HTMLElement[]>('headerRefs');
|
|
409
|
+
const rowHeights = ref<number[]>([]);
|
|
410
|
+
const columnWidths = ref<number[]>([]);
|
|
411
411
|
watch(() => props.rows, (newRows) => {
|
|
412
412
|
// rows are set to null when new records are loading
|
|
413
|
-
rowHeights.value = newRows || !rowRefs.value ? [] : rowRefs.value.map((el) => el.offsetHeight);
|
|
414
|
-
columnWidths.value = newRows || !headerRefs.value ? [] : [48, ...headerRefs.value.map((el) => el.offsetWidth)];
|
|
413
|
+
rowHeights.value = newRows || !rowRefs.value ? [] : rowRefs.value.map((el: HTMLElement) => el.offsetHeight);
|
|
414
|
+
columnWidths.value = newRows || !headerRefs.value ? [] : [48, ...headerRefs.value.map((el: HTMLElement) => el.offsetWidth)];
|
|
415
415
|
});
|
|
416
416
|
|
|
417
|
-
function addToCheckedValues(id) {
|
|
417
|
+
function addToCheckedValues(id: any) {
|
|
418
418
|
if (checkboxesInternal.value.includes(id)) {
|
|
419
419
|
checkboxesInternal.value = checkboxesInternal.value.filter((item) => item !== id);
|
|
420
420
|
} else {
|
|
@@ -423,17 +423,17 @@ function addToCheckedValues(id) {
|
|
|
423
423
|
checkboxesInternal.value = [ ...checkboxesInternal.value ]
|
|
424
424
|
}
|
|
425
425
|
|
|
426
|
-
const columnsListed = computed(() => props.resource?.columns?.filter(c => c.showIn
|
|
426
|
+
const columnsListed = computed(() => props.resource?.columns?.filter((c: AdminForthResourceColumnCommon) => c.showIn?.list));
|
|
427
427
|
|
|
428
|
-
async function selectAll(
|
|
428
|
+
async function selectAll() {
|
|
429
429
|
if (!allFromThisPageChecked.value) {
|
|
430
|
-
props.rows
|
|
430
|
+
props.rows?.forEach((r) => {
|
|
431
431
|
if (!checkboxesInternal.value.includes(r._primaryKeyValue)) {
|
|
432
432
|
checkboxesInternal.value.push(r._primaryKeyValue)
|
|
433
433
|
}
|
|
434
434
|
});
|
|
435
435
|
} else {
|
|
436
|
-
props.rows
|
|
436
|
+
props.rows?.forEach((r) => {
|
|
437
437
|
checkboxesInternal.value = checkboxesInternal.value.filter((item) => item !== r._primaryKeyValue);
|
|
438
438
|
});
|
|
439
439
|
}
|
|
@@ -446,15 +446,15 @@ const allFromThisPageChecked = computed(() => {
|
|
|
446
446
|
if (!props.rows || !props.rows.length) return false;
|
|
447
447
|
return props.rows.every((r) => checkboxesInternal.value.includes(r._primaryKeyValue));
|
|
448
448
|
});
|
|
449
|
-
const ascArr = computed(() => sort.value.filter((s) => s.direction === 'asc').map((s) => s.field));
|
|
450
|
-
const descArr = computed(() => sort.value.filter((s) => s.direction === 'desc').map((s) => s.field));
|
|
449
|
+
const ascArr = computed(() => sort.value.filter((s: any) => s.direction === 'asc').map((s: any) => s.field));
|
|
450
|
+
const descArr = computed(() => sort.value.filter((s: any) => s.direction === 'desc').map((s: any) => s.field));
|
|
451
451
|
|
|
452
452
|
|
|
453
|
-
function onSortButtonClick(event, field) {
|
|
453
|
+
function onSortButtonClick(event: any, field: any) {
|
|
454
454
|
// if ctrl key is pressed, add to sort otherwise sort by this field
|
|
455
455
|
// in any case if field is already in sort, toggle direction
|
|
456
456
|
|
|
457
|
-
const sortIndex = sort.value.findIndex((s) => s.field === field);
|
|
457
|
+
const sortIndex = sort.value.findIndex((s: any) => s.field === field);
|
|
458
458
|
if (sortIndex === -1) {
|
|
459
459
|
// field is not in sort, add it
|
|
460
460
|
if (event.ctrlKey) {
|
|
@@ -475,11 +475,11 @@ function onSortButtonClick(event, field) {
|
|
|
475
475
|
|
|
476
476
|
const clickTarget = ref(null);
|
|
477
477
|
|
|
478
|
-
async function onClick(e,row) {
|
|
478
|
+
async function onClick(e: any,row: any) {
|
|
479
479
|
if(clickTarget.value === e.target) return;
|
|
480
480
|
clickTarget.value = e.target;
|
|
481
481
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
482
|
-
if (window.getSelection()
|
|
482
|
+
if (window.getSelection()?.toString()) return;
|
|
483
483
|
else {
|
|
484
484
|
if (row._clickUrl === null) {
|
|
485
485
|
// user asked to nothing on click
|
|
@@ -494,7 +494,7 @@ async function onClick(e,row) {
|
|
|
494
494
|
router.resolve({
|
|
495
495
|
name: 'resource-show',
|
|
496
496
|
params: {
|
|
497
|
-
resourceId: props.resource
|
|
497
|
+
resourceId: props.resource?.resourceId,
|
|
498
498
|
primaryKey: row._primaryKeyValue,
|
|
499
499
|
},
|
|
500
500
|
}).href,
|
|
@@ -512,7 +512,7 @@ async function onClick(e,row) {
|
|
|
512
512
|
router.push({
|
|
513
513
|
name: 'resource-show',
|
|
514
514
|
params: {
|
|
515
|
-
resourceId: props.resource
|
|
515
|
+
resourceId: props.resource?.resourceId,
|
|
516
516
|
primaryKey: row._primaryKeyValue,
|
|
517
517
|
},
|
|
518
518
|
});
|
|
@@ -521,7 +521,7 @@ async function onClick(e,row) {
|
|
|
521
521
|
}
|
|
522
522
|
}
|
|
523
523
|
|
|
524
|
-
async function deleteRecord(row) {
|
|
524
|
+
async function deleteRecord(row: any) {
|
|
525
525
|
const data = await adminforth.confirm({
|
|
526
526
|
message: t('Are you sure you want to delete this item?'),
|
|
527
527
|
yes: t('Delete'),
|
|
@@ -533,7 +533,7 @@ async function deleteRecord(row) {
|
|
|
533
533
|
path: '/delete_record',
|
|
534
534
|
method: 'POST',
|
|
535
535
|
body: {
|
|
536
|
-
resourceId: props.resource
|
|
536
|
+
resourceId: props.resource?.resourceId,
|
|
537
537
|
primaryKey: row._primaryKeyValue,
|
|
538
538
|
}
|
|
539
539
|
});
|
|
@@ -551,16 +551,16 @@ async function deleteRecord(row) {
|
|
|
551
551
|
}
|
|
552
552
|
}
|
|
553
553
|
|
|
554
|
-
const actionLoadingStates = ref({});
|
|
554
|
+
const actionLoadingStates = ref<Record<string | number, boolean>>({});
|
|
555
555
|
|
|
556
|
-
async function startCustomAction(actionId, row) {
|
|
556
|
+
async function startCustomAction(actionId: string, row: any) {
|
|
557
557
|
actionLoadingStates.value[actionId] = true;
|
|
558
558
|
|
|
559
559
|
const data = await callAdminForthApi({
|
|
560
560
|
path: '/start_custom_action',
|
|
561
561
|
method: 'POST',
|
|
562
562
|
body: {
|
|
563
|
-
resourceId: props.resource
|
|
563
|
+
resourceId: props.resource?.resourceId,
|
|
564
564
|
actionId: actionId,
|
|
565
565
|
recordId: row._primaryKeyValue
|
|
566
566
|
}
|
|
@@ -598,7 +598,7 @@ async function startCustomAction(actionId, row) {
|
|
|
598
598
|
}
|
|
599
599
|
}
|
|
600
600
|
|
|
601
|
-
function onPageInput(event) {
|
|
601
|
+
function onPageInput(event: any) {
|
|
602
602
|
pageInput.value = event.target.innerText;
|
|
603
603
|
}
|
|
604
604
|
|
|
@@ -62,10 +62,11 @@
|
|
|
62
62
|
import { getCustomComponent } from '@/utils';
|
|
63
63
|
import { useCoreStore } from '@/stores/core';
|
|
64
64
|
import { computed } from 'vue';
|
|
65
|
+
import type { AdminForthResourceCommon } from '@/types/Common';
|
|
65
66
|
const props = withDefaults(defineProps<{
|
|
66
67
|
columns: Array<{
|
|
67
68
|
name: string;
|
|
68
|
-
label
|
|
69
|
+
label?: string;
|
|
69
70
|
components?: {
|
|
70
71
|
show?: {
|
|
71
72
|
file: string;
|
|
@@ -77,10 +78,9 @@
|
|
|
77
78
|
};
|
|
78
79
|
};
|
|
79
80
|
}>;
|
|
80
|
-
source: string;
|
|
81
81
|
groupName?: string | null;
|
|
82
82
|
noTitle?: boolean;
|
|
83
|
-
resource:
|
|
83
|
+
resource: AdminForthResourceCommon | null;
|
|
84
84
|
record: Record<string, any>;
|
|
85
85
|
isRounded?: boolean;
|
|
86
86
|
}>(), {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<template >
|
|
2
|
-
<template v-if="threeDotsDropdownItems?.length || customActions?.length">
|
|
2
|
+
<template v-if="threeDotsDropdownItems?.length || customActions?.length || (bulkActions?.some((action: AdminForthBulkActionCommon) => action.showInThreeDotsDropdown))">
|
|
3
3
|
<button
|
|
4
4
|
data-dropdown-toggle="listThreeDotsDropdown"
|
|
5
5
|
class="flex items-center py-2 px-2 text-sm font-medium text-lightThreeDotsMenuIconDots focus:outline-none bg-lightThreeDotsMenuIconBackground rounded border border-lightThreeDotsMenuIconBackgroundBorder hover:bg-lightThreeDotsMenuIconBackgroundHover hover:text-lightThreeDotsMenuIconDotsHover focus:z-10 focus:ring-4 focus:ring-lightThreeDotsMenuIconFocus dark:focus:ring-darkThreeDotsMenuIconFocus dark:bg-darkThreeDotsMenuIconBackground dark:text-darkThreeDotsMenuIconDots dark:border-darkThreeDotsMenuIconBackgroundBorder dark:hover:text-darkThreeDotsMenuIconDotsHover dark:hover:bg-darkThreeDotsMenuIconBackgroundHover rounded-default"
|
|
@@ -14,16 +14,26 @@
|
|
|
14
14
|
id="listThreeDotsDropdown"
|
|
15
15
|
class="z-20 hidden bg-lightThreeDotsMenuBodyBackground divide-y divide-gray-100 rounded-lg shadow w-44 dark:bg-darkThreeDotsMenuBodyBackground dark:divide-gray-600">
|
|
16
16
|
<ul class="py-2 text-sm text-lightThreeDotsMenuBodyText dark:text-darkThreeDotsMenuBodyText" aria-labelledby="dropdownMenuIconButton">
|
|
17
|
-
<li v-for="item in threeDotsDropdownItems" :key="`dropdown-item-${
|
|
18
|
-
<a
|
|
19
|
-
|
|
17
|
+
<li v-for="(item, i) in threeDotsDropdownItems" :key="`dropdown-item-${i}`">
|
|
18
|
+
<a href="#"
|
|
19
|
+
class="block px-4 py-2 hover:bg-lightThreeDotsMenuBodyBackgroundHover hover:text-lightThreeDotsMenuBodyTextHover dark:hover:bg-darkThreeDotsMenuBodyBackgroundHover dark:hover:text-darkThreeDotsMenuBodyTextHover"
|
|
20
|
+
:class="{
|
|
21
|
+
'pointer-events-none': checkboxes && checkboxes.length === 0 && item.meta?.disabledWhenNoCheckboxes,
|
|
22
|
+
'opacity-50': checkboxes && checkboxes.length === 0 && item.meta?.disabledWhenNoCheckboxes,
|
|
23
|
+
'cursor-not-allowed': checkboxes && checkboxes.length === 0 && item.meta?.disabledWhenNoCheckboxes,
|
|
24
|
+
}"
|
|
25
|
+
@click="injectedComponentClick(i)">
|
|
26
|
+
<component :ref="(el: any) => setComponentRef(el, i)" :is="getCustomComponent(item)"
|
|
20
27
|
:meta="item.meta"
|
|
21
28
|
:resource="coreStore.resource"
|
|
22
29
|
:adminUser="coreStore.adminUser"
|
|
30
|
+
:checkboxes="checkboxes"
|
|
31
|
+
:updateList="props.updateList"
|
|
32
|
+
:clearCheckboxes="clearCheckboxes"
|
|
23
33
|
/>
|
|
24
34
|
</a>
|
|
25
35
|
</li>
|
|
26
|
-
<li v-for="action in customActions" :key="action.id">
|
|
36
|
+
<li v-if="customActions" v-for="action in customActions" :key="action.id">
|
|
27
37
|
<a href="#" @click.prevent="handleActionClick(action)" class="block px-4 py-2 hover:text-lightThreeDotsMenuBodyTextHover hover:bg-lightThreeDotsMenuBodyBackgroundHover dark:hover:bg-darkThreeDotsMenuBodyBackgroundHover dark:hover:text-darkThreeDotsMenuBodyTextHover">
|
|
28
38
|
<div class="flex items-center gap-2">
|
|
29
39
|
<component
|
|
@@ -35,6 +45,24 @@
|
|
|
35
45
|
</div>
|
|
36
46
|
</a>
|
|
37
47
|
</li>
|
|
48
|
+
<li v-for="action in bulkActions?.filter((a:AdminForthBulkActionCommon ) => a.showInThreeDotsDropdown)" :key="action.id">
|
|
49
|
+
<a href="#" @click.prevent="startBulkAction(action.id)"
|
|
50
|
+
class="block px-4 py-2 hover:text-lightThreeDotsMenuBodyTextHover hover:bg-lightThreeDotsMenuBodyBackgroundHover dark:hover:bg-darkThreeDotsMenuBodyBackgroundHover dark:hover:text-darkThreeDotsMenuBodyTextHover"
|
|
51
|
+
:class="{
|
|
52
|
+
'pointer-events-none': checkboxes && checkboxes.length === 0,
|
|
53
|
+
'opacity-50': checkboxes && checkboxes.length === 0,
|
|
54
|
+
'cursor-not-allowed': checkboxes && checkboxes.length === 0
|
|
55
|
+
}">
|
|
56
|
+
<div class="flex items-center gap-2">
|
|
57
|
+
<component
|
|
58
|
+
v-if="action.icon"
|
|
59
|
+
:is="getIcon(action.icon)"
|
|
60
|
+
class="w-4 h-4 text-lightPrimary dark:text-darkPrimary"
|
|
61
|
+
/>
|
|
62
|
+
{{ action.label }}
|
|
63
|
+
</div>
|
|
64
|
+
</a>
|
|
65
|
+
</li>
|
|
38
66
|
</ul>
|
|
39
67
|
</div>
|
|
40
68
|
</template>
|
|
@@ -47,17 +75,36 @@ import { useCoreStore } from '@/stores/core';
|
|
|
47
75
|
import adminforth from '@/adminforth';
|
|
48
76
|
import { callAdminForthApi } from '@/utils';
|
|
49
77
|
import { useRoute, useRouter } from 'vue-router';
|
|
78
|
+
import type { AdminForthComponentDeclarationFull, AdminForthBulkActionCommon, AdminForthActionInput } from '@/types/Common.js';
|
|
79
|
+
import { ref, type ComponentPublicInstance } from 'vue';
|
|
50
80
|
|
|
51
81
|
const route = useRoute();
|
|
52
82
|
const coreStore = useCoreStore();
|
|
53
83
|
const router = useRouter();
|
|
84
|
+
const threeDotsDropdownItemsRefs = ref<Array<ComponentPublicInstance | null>>([]);
|
|
54
85
|
|
|
55
86
|
const props = defineProps({
|
|
56
|
-
threeDotsDropdownItems: Array
|
|
57
|
-
customActions: Array
|
|
87
|
+
threeDotsDropdownItems: Array<AdminForthComponentDeclarationFull>,
|
|
88
|
+
customActions: Array<AdminForthActionInput>,
|
|
89
|
+
bulkActions: Array<AdminForthBulkActionCommon>,
|
|
90
|
+
checkboxes: Array,
|
|
91
|
+
updateList: {
|
|
92
|
+
type: Function,
|
|
93
|
+
},
|
|
94
|
+
clearCheckboxes: {
|
|
95
|
+
type: Function
|
|
96
|
+
}
|
|
58
97
|
});
|
|
59
98
|
|
|
60
|
-
|
|
99
|
+
const emit = defineEmits(['startBulkAction']);
|
|
100
|
+
|
|
101
|
+
function setComponentRef(el: ComponentPublicInstance | null, index: number) {
|
|
102
|
+
if (el) {
|
|
103
|
+
threeDotsDropdownItemsRefs.value[index] = el;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async function handleActionClick(action: AdminForthActionInput) {
|
|
61
108
|
adminforth.list.closeThreeDotsDropdown();
|
|
62
109
|
|
|
63
110
|
const actionId = action.id;
|
|
@@ -88,8 +135,8 @@ async function handleActionClick(action) {
|
|
|
88
135
|
|
|
89
136
|
if (data?.ok) {
|
|
90
137
|
await coreStore.fetchRecord({
|
|
91
|
-
resourceId: route.params.resourceId,
|
|
92
|
-
primaryKey: route.params.primaryKey,
|
|
138
|
+
resourceId: route.params.resourceId as string,
|
|
139
|
+
primaryKey: route.params.primaryKey as string,
|
|
93
140
|
source: 'show',
|
|
94
141
|
});
|
|
95
142
|
|
|
@@ -108,4 +155,16 @@ async function handleActionClick(action) {
|
|
|
108
155
|
});
|
|
109
156
|
}
|
|
110
157
|
}
|
|
158
|
+
|
|
159
|
+
function startBulkAction(actionId: string) {
|
|
160
|
+
adminforth.list.closeThreeDotsDropdown();
|
|
161
|
+
emit('startBulkAction', actionId);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async function injectedComponentClick(index: number) {
|
|
165
|
+
const componentRef = threeDotsDropdownItemsRefs.value[index];
|
|
166
|
+
if (componentRef && 'click' in componentRef) {
|
|
167
|
+
(componentRef as any).click?.();
|
|
168
|
+
}
|
|
169
|
+
}
|
|
111
170
|
</script>
|
|
@@ -30,7 +30,18 @@
|
|
|
30
30
|
</div>
|
|
31
31
|
|
|
32
32
|
<div class="ms-3 text-sm font-normal max-w-xs pr-2" v-if="toast.messageHtml" v-html="toast.messageHtml"></div>
|
|
33
|
-
<div class="ms-3 text-sm font-normal max-w-xs pr-2" v-else>
|
|
33
|
+
<div class="ms-3 text-sm font-normal max-w-xs pr-2" v-else>
|
|
34
|
+
<div class="flex flex-col items-center justify-center">
|
|
35
|
+
{{toast.message}}
|
|
36
|
+
<div v-if="toast.buttons" class="flex justify-center mt-2 gap-2">
|
|
37
|
+
<div v-for="button in toast.buttons" class="rounded-md bg-lightButtonsBackground hover:bg-lightButtonsHover text-lightButtonsText dark:bg-darkPrimary dark:hover:bg-darkButtonsBackground dark:text-darkButtonsText">
|
|
38
|
+
<button @click="onButtonClick(button.value)" class="px-2 py-1 rounded hover:bg-black/5 dark:hover:bg-white/10">
|
|
39
|
+
{{ button.label }}
|
|
40
|
+
</button>
|
|
41
|
+
</div>
|
|
42
|
+
</div>
|
|
43
|
+
</div>
|
|
44
|
+
</div>
|
|
34
45
|
<button @click="closeToast" type="button" class="ms-auto -mx-1.5 -my-1.5 bg-lightToastCloseIconBackground text-lightToastCloseIcon hover:text-lightToastCloseIconHover rounded-lg focus:ring-2 focus:ring-lightToastCloseIconFocusRing p-1.5 hover:bg-lightToastCloseIconBackgroundHover inline-flex items-center justify-center h-8 w-8 dark:text-darkToastCloseIcon dark:hover:text-darkToastCloseIconHover dark:bg-darkToastCloseIconBackground dark:hover:bg-darkToastCloseIconBackgroundHover dark:focus:ring-darkToastCloseIconFocusRing" >
|
|
35
46
|
<svg class="w-3 h-3" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 14 14">
|
|
36
47
|
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m1 1 6 6m0 0 6 6M7 7l6-6M7 7l-6 6"/>
|
|
@@ -53,16 +64,28 @@ const props = defineProps<{
|
|
|
53
64
|
variant: string;
|
|
54
65
|
id: string;
|
|
55
66
|
timeout?: number|'unlimited';
|
|
67
|
+
buttons?: { value: any; label: string }[];
|
|
56
68
|
}
|
|
57
69
|
}>();
|
|
58
70
|
function closeToast() {
|
|
71
|
+
// resolve with undefined on close (X button)
|
|
72
|
+
toastStore.resolveToast(props.toast.id);
|
|
73
|
+
emit('close');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function onButtonClick(value: any) {
|
|
77
|
+
toastStore.resolveToast(props.toast.id, value);
|
|
59
78
|
emit('close');
|
|
60
79
|
}
|
|
61
80
|
|
|
62
81
|
onMounted(() => {
|
|
63
82
|
if (props.toast.timeout === 'unlimited') return;
|
|
64
83
|
else {
|
|
65
|
-
setTimeout(() => {
|
|
84
|
+
setTimeout(() => {
|
|
85
|
+
// resolve with undefined on auto-timeout
|
|
86
|
+
toastStore.resolveToast(props.toast.id);
|
|
87
|
+
emit('close');
|
|
88
|
+
}, (props.toast.timeout || 10) * 1e3 );
|
|
66
89
|
}
|
|
67
90
|
});
|
|
68
91
|
|
|
@@ -12,13 +12,40 @@
|
|
|
12
12
|
>
|
|
13
13
|
<RouterLink
|
|
14
14
|
class="font-medium text-lightSidebarText dark:text-darkSidebarText hover:brightness-110 whitespace-nowrap"
|
|
15
|
-
:to="{
|
|
15
|
+
:to="{
|
|
16
|
+
name: 'resource-show',
|
|
17
|
+
params: {
|
|
18
|
+
primaryKey: foreignResource.pk,
|
|
19
|
+
resourceId: column.foreignResource
|
|
20
|
+
? (
|
|
21
|
+
column.foreignResource.resourceId
|
|
22
|
+
|| column.foreignResource.polymorphicResources?.find(
|
|
23
|
+
(pr: any) => pr.whenValue === record[column.foreignResource?.polymorphicOn!]
|
|
24
|
+
)?.resourceId
|
|
25
|
+
)
|
|
26
|
+
: undefined
|
|
27
|
+
}
|
|
28
|
+
}"
|
|
16
29
|
>
|
|
17
30
|
{{ foreignResource.label }}
|
|
18
31
|
</RouterLink>
|
|
19
32
|
</span>
|
|
20
33
|
<RouterLink v-else-if="record[column.name]" class="font-medium text-lightPrimary dark:text-darkPrimary hover:brightness-110 whitespace-nowrap"
|
|
21
|
-
|
|
34
|
+
:to="{
|
|
35
|
+
name: 'resource-show',
|
|
36
|
+
params: {
|
|
37
|
+
primaryKey: record[column.name].pk,
|
|
38
|
+
resourceId: column.foreignResource
|
|
39
|
+
? (
|
|
40
|
+
column.foreignResource.resourceId
|
|
41
|
+
|| column.foreignResource.polymorphicResources?.find(
|
|
42
|
+
(pr: any) => pr.whenValue === record[column.foreignResource?.polymorphicOn!]
|
|
43
|
+
)?.resourceId
|
|
44
|
+
)
|
|
45
|
+
: undefined
|
|
46
|
+
}
|
|
47
|
+
}"
|
|
48
|
+
>
|
|
22
49
|
{{ record[column.name].label }}
|
|
23
50
|
</RouterLink>
|
|
24
51
|
<div v-else>
|
|
@@ -38,13 +65,13 @@
|
|
|
38
65
|
<template v-for="(arrayItem, arrayItemIndex) in record[column.name]">
|
|
39
66
|
<span
|
|
40
67
|
v-if="column.isArray.itemType === 'boolean' && arrayItem"
|
|
41
|
-
:key="`${column.name}-${arrayItemIndex}`"
|
|
68
|
+
:key="`${column.name}-${arrayItemIndex}-true`"
|
|
42
69
|
class="af-true-value-icon bg-green-100 whitespace-nowrap text-green-800 text-xs font-medium me-2 px-2.5 py-0.5 rounded dark:bg-gray-700 dark:text-green-400 border border-green-400">
|
|
43
70
|
{{ $t('Yes') }}
|
|
44
71
|
</span>
|
|
45
72
|
<span
|
|
46
73
|
v-else-if="column.isArray.itemType === 'boolean'"
|
|
47
|
-
:key="`${column.name}-${arrayItemIndex}`"
|
|
74
|
+
:key="`${column.name}-${arrayItemIndex}-false`"
|
|
48
75
|
class="af-false-value-icon bg-red-100 whitespace-nowrap text-red-800 text-xs font-medium me-2 px-2.5 py-0.5 rounded dark:bg-gray-700 dark:text-red-400 border border-red-400">
|
|
49
76
|
{{ $t('No') }}
|
|
50
77
|
</span>
|
|
@@ -53,30 +80,30 @@
|
|
|
53
80
|
:key="`${column.name}-${arrayItemIndex}`"
|
|
54
81
|
class="rounded-md m-0.5 bg-lightAnnouncementBG dark:bg-darkAnnouncementBG text-lightAnnouncementText dark:text-darkAnnouncementText py-0.5 px-2.5 text-sm"
|
|
55
82
|
>
|
|
56
|
-
{{ checkEmptyValues(getArrayItemDisplayValue(arrayItem, column), route.meta.type) }}
|
|
83
|
+
{{ checkEmptyValues(getArrayItemDisplayValue(arrayItem, column), route.meta.type as "show" | "list") }}
|
|
57
84
|
</span>
|
|
58
85
|
</template>
|
|
59
86
|
</span>
|
|
60
87
|
<span v-else-if="column.enum">
|
|
61
|
-
{{ checkEmptyValues(column.enum.find(e => e.value === record[column.name])?.label || record[column.name], route.meta.type) }}
|
|
88
|
+
{{ checkEmptyValues(column.enum.find(e => e.value === record[column.name])?.label || record[column.name], route.meta.type as "show" | "list") }}
|
|
62
89
|
</span>
|
|
63
90
|
<span v-else-if="column.type === 'datetime'" class="whitespace-nowrap">
|
|
64
|
-
{{ checkEmptyValues(formatDateTime(record[column.name]), route.meta.type) }}
|
|
91
|
+
{{ checkEmptyValues(formatDateTime(record[column.name]), route.meta.type as "show" | "list") }}
|
|
65
92
|
</span>
|
|
66
93
|
<span v-else-if="column.type === 'date'" class="whitespace-nowrap">
|
|
67
|
-
{{ checkEmptyValues(formatDate(record[column.name]), route.meta.type) }}
|
|
94
|
+
{{ checkEmptyValues(formatDate(record[column.name]), route.meta.type as "show" | "list") }}
|
|
68
95
|
</span>
|
|
69
96
|
<span v-else-if="column.type === 'time'" class="whitespace-nowrap">
|
|
70
|
-
{{ checkEmptyValues(formatTime(record[column.name]), route.meta.type) }}
|
|
97
|
+
{{ checkEmptyValues(formatTime(record[column.name]), route.meta.type as "show" | "list") }}
|
|
71
98
|
</span>
|
|
72
99
|
<span v-else-if="column.type === 'decimal'">
|
|
73
|
-
{{ checkEmptyValues(record[column.name] && parseFloat(record[column.name]), route.meta.type) }}
|
|
100
|
+
{{ checkEmptyValues(record[column.name] && parseFloat(record[column.name]), route.meta.type as "show" | "list") }}
|
|
74
101
|
</span>
|
|
75
102
|
<span v-else-if="column.type === 'json'">
|
|
76
103
|
<JsonViewer class="min-w-[6rem]" :value="record[column.name]" :expandDepth="column.extra?.jsonCollapsedLevel" copyable sort :theme="coreStore.theme"/>
|
|
77
104
|
</span>
|
|
78
105
|
<span v-else>
|
|
79
|
-
{{ checkEmptyValues(record[column.name],route.meta.type) }}
|
|
106
|
+
{{ checkEmptyValues(record[column.name], route.meta.type as "show" | "list") }}
|
|
80
107
|
</span>
|
|
81
108
|
</div>
|
|
82
109
|
</template>
|
|
@@ -122,7 +149,7 @@ function formatTime(time: string) {
|
|
|
122
149
|
return dayjs(`0000-00-00 ${time}`).format(coreStore.config?.timeFormat || 'HH:mm:ss');
|
|
123
150
|
}
|
|
124
151
|
|
|
125
|
-
function getArrayItemDisplayValue(value, column) {
|
|
152
|
+
function getArrayItemDisplayValue(value: any, column: AdminForthResourceColumnCommon) {
|
|
126
153
|
if (column.isArray?.itemType === 'datetime') {
|
|
127
154
|
return formatDateTime(value);
|
|
128
155
|
} else if (column.isArray?.itemType === 'date') {
|
package/dist/spa/src/i18n.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { createApp } from 'vue';
|
|
|
3
3
|
|
|
4
4
|
|
|
5
5
|
// taken from here https://vue-i18n.intlify.dev/guide/essentials/pluralization.html#custom-pluralization
|
|
6
|
-
function slavicPluralRule(choice, choicesLength, orgRule) {
|
|
6
|
+
function slavicPluralRule(choice: number, choicesLength: number, orgRule: any) {
|
|
7
7
|
if (choice === 0) {
|
|
8
8
|
return 0
|
|
9
9
|
}
|
|
@@ -29,7 +29,12 @@ export const useModalStore = defineStore('modal', () => {
|
|
|
29
29
|
onCancelFunction.value = func;
|
|
30
30
|
}
|
|
31
31
|
function setModalContent(content: ModalContentType) {
|
|
32
|
-
modalContent.value =
|
|
32
|
+
modalContent.value = {
|
|
33
|
+
title: content.title || 'title',
|
|
34
|
+
content: content.content || 'content',
|
|
35
|
+
acceptText: content.acceptText || 'acceptText',
|
|
36
|
+
cancelText: content.cancelText || 'cancelText',
|
|
37
|
+
};
|
|
33
38
|
}
|
|
34
39
|
function resetmodalState() {
|
|
35
40
|
isOpened.value = false;
|
|
@@ -12,19 +12,38 @@ export const useToastStore = defineStore('toast', () => {
|
|
|
12
12
|
watch(route, () => {
|
|
13
13
|
// on route change clear all toasts older then 5 seconds
|
|
14
14
|
const now = +new Date();
|
|
15
|
-
toasts.value = toasts.value.filter((t) => now - t.createdAt < 5000);
|
|
15
|
+
toasts.value = toasts.value.filter((t) => t?.timeout === 'unlimited' || now - t.createdAt < 5000);
|
|
16
16
|
});
|
|
17
17
|
|
|
18
|
-
const addToast = (toast: {
|
|
18
|
+
const addToast = (toast: {
|
|
19
|
+
message?: string;
|
|
20
|
+
messageHtml?: string;
|
|
21
|
+
variant: string;
|
|
22
|
+
timeout?: number | 'unlimited';
|
|
23
|
+
buttons?: { value: any; label: string }[];
|
|
24
|
+
onResolve?: (value?: any) => void;
|
|
25
|
+
}): string => {
|
|
19
26
|
const toastId = uuid();
|
|
20
27
|
toasts.value.push({
|
|
21
28
|
...toast,
|
|
22
29
|
id: toastId,
|
|
23
30
|
createdAt: +new Date(),
|
|
24
31
|
});
|
|
32
|
+
return toastId;
|
|
25
33
|
};
|
|
26
34
|
const removeToast = (toast: { id: string }) => {
|
|
27
35
|
toasts.value = toasts.value.filter((t) => t.id !== toast.id);
|
|
28
36
|
};
|
|
29
|
-
|
|
37
|
+
|
|
38
|
+
const resolveToast = (toastId: string, value?: any) => {
|
|
39
|
+
const t = toasts.value.find((x) => x.id === toastId);
|
|
40
|
+
try {
|
|
41
|
+
t?.onResolve?.(value);
|
|
42
|
+
} catch {
|
|
43
|
+
// no-op
|
|
44
|
+
}
|
|
45
|
+
toasts.value = toasts.value.filter((x) => x.id !== toastId);
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
return { toasts, addToast, removeToast, resolveToast };
|
|
30
49
|
});
|