Skip to content

Commit 802c37c

Browse files
author
Vitalii Kulyk
committed
feat: implement custom action execution and bulk action handling in the admin interface
1 parent 658fcc9 commit 802c37c

File tree

9 files changed

+339
-163
lines changed

9 files changed

+339
-163
lines changed

adminforth/documentation/docs/tutorial/03-Customization/09-Actions.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ Here's how to add a custom action:
3535
listThreeDotsMenu: true, // Show in three dots menu in list view
3636
showButton: true, // Show as a button
3737
showThreeDotsMenu: true, // Show in three-dots menu
38+
bulkButton: true
3839
}
3940
}
4041
]

adminforth/spa/src/components/ResourceListTable.vue

Lines changed: 21 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -344,7 +344,7 @@
344344
345345
346346
import { computed, onMounted, ref, watch, useTemplateRef, nextTick, type Ref } from 'vue';
347-
import { callAdminForthApi } from '@/utils';
347+
import { callAdminForthApi, executeCustomAction } from '@/utils';
348348
import { useI18n } from 'vue-i18n';
349349
import ValueRenderer from '@/components/ValueRenderer.vue';
350350
import { getCustomComponent, formatComponent } from '@/utils';
@@ -610,50 +610,28 @@ async function deleteRecord(row: any) {
610610
const actionLoadingStates = ref<Record<string | number, boolean>>({});
611611
612612
async function startCustomAction(actionId: string | number, row: any, extraData: Record<string, any> = {}) {
613-
614-
actionLoadingStates.value[actionId] = true;
615-
616-
const data = await callAdminForthApi({
617-
path: '/start_custom_action',
618-
method: 'POST',
619-
body: {
620-
resourceId: props.resource?.resourceId,
621-
actionId: actionId,
622-
recordId: row._primaryKeyValue,
623-
extra: extraData
624-
}
625-
});
626-
627-
actionLoadingStates.value[actionId] = false;
628-
629-
if (data?.redirectUrl) {
630-
// Check if the URL should open in a new tab
631-
if (data.redirectUrl.includes('target=_blank')) {
632-
window.open(data.redirectUrl.replace('&target=_blank', '').replace('?target=_blank', ''), '_blank');
633-
} else {
634-
// Navigate within the app
635-
if (data.redirectUrl.startsWith('http')) {
636-
window.location.href = data.redirectUrl;
637-
} else {
638-
router.push(data.redirectUrl);
613+
await executeCustomAction({
614+
actionId,
615+
resourceId: props.resource?.resourceId || '',
616+
recordId: row._primaryKeyValue,
617+
extra: extraData,
618+
setLoadingState: (loading: boolean) => {
619+
actionLoadingStates.value[actionId] = loading;
620+
},
621+
onSuccess: async (data: any) => {
622+
emits('update:records', true);
623+
624+
if (data.successMessage) {
625+
alert({
626+
message: data.successMessage,
627+
variant: 'success'
628+
});
639629
}
630+
},
631+
onError: (error: string) => {
632+
showErrorTost(error);
640633
}
641-
return;
642-
}
643-
if (data?.ok) {
644-
emits('update:records', true);
645-
646-
if (data.successMessage) {
647-
alert({
648-
message: data.successMessage,
649-
variant: 'success'
650-
});
651-
}
652-
}
653-
654-
if (data?.error) {
655-
showErrorTost(data.error);
656-
}
634+
});
657635
}
658636
659637
function validatePageInput() {

adminforth/spa/src/components/ThreeDotsMenu.vue

Lines changed: 22 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@
8989

9090

9191
<script setup lang="ts">
92-
import { getCustomComponent, getIcon, formatComponent } from '@/utils';
92+
import { getCustomComponent, getIcon, formatComponent, executeCustomAction } from '@/utils';
9393
import { useCoreStore } from '@/stores/core';
9494
import { useAdminforth } from '@/adminforth';
9595
import { callAdminForthApi } from '@/utils';
@@ -131,55 +131,32 @@ function setComponentRef(el: ComponentPublicInstance | null, index: number) {
131131
132132
async function handleActionClick(action: AdminForthActionInput, payload: any) {
133133
list.closeThreeDotsDropdown();
134-
135-
const actionId = action.id;
136-
const data = await callAdminForthApi({
137-
path: '/start_custom_action',
138-
method: 'POST',
139-
body: {
140-
resourceId: route.params.resourceId,
141-
actionId: actionId,
142-
recordId: route.params.primaryKey,
143-
extra: payload || {},
144-
}
145-
});
134+
await executeCustomAction({
135+
actionId: action.id,
136+
resourceId: route.params.resourceId as string,
137+
recordId: route.params.primaryKey as string,
138+
extra: payload || {},
139+
onSuccess: async (data: any) => {
140+
await coreStore.fetchRecord({
141+
resourceId: route.params.resourceId as string,
142+
primaryKey: route.params.primaryKey as string,
143+
source: 'show',
144+
});
146145
147-
if (data?.redirectUrl) {
148-
// Check if the URL should open in a new tab
149-
if (data.redirectUrl.includes('target=_blank')) {
150-
window.open(data.redirectUrl.replace('&target=_blank', '').replace('?target=_blank', ''), '_blank');
151-
} else {
152-
// Navigate within the app
153-
if (data.redirectUrl.startsWith('http')) {
154-
window.location.href = data.redirectUrl;
155-
} else {
156-
router.push(data.redirectUrl);
146+
if (data.successMessage) {
147+
alert({
148+
message: data.successMessage,
149+
variant: 'success'
150+
});
157151
}
158-
}
159-
return;
160-
}
161-
162-
if (data?.ok) {
163-
await coreStore.fetchRecord({
164-
resourceId: route.params.resourceId as string,
165-
primaryKey: route.params.primaryKey as string,
166-
source: 'show',
167-
});
168-
169-
if (data.successMessage) {
152+
},
153+
onError: (error: string) => {
170154
alert({
171-
message: data.successMessage,
172-
variant: 'success'
155+
message: error,
156+
variant: 'danger'
173157
});
174158
}
175-
}
176-
177-
if (data?.error) {
178-
alert({
179-
message: data.error,
180-
variant: 'danger'
181-
});
182-
}
159+
});
183160
}
184161
185162
function startBulkAction(actionId: string) {

adminforth/spa/src/utils/utils.ts

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -690,4 +690,162 @@ export async function onBeforeRouteLeaveCreateEditViewGuard(initialValues: any,
690690
leaveGuardActive.setActive(false);
691691
}
692692
});
693+
}
694+
695+
export async function executeCustomAction({
696+
actionId,
697+
resourceId,
698+
recordId,
699+
extra = {},
700+
onSuccess,
701+
onError,
702+
setLoadingState,
703+
}: {
704+
actionId: string | number,
705+
resourceId: string,
706+
recordId: string | number,
707+
extra?: Record<string, any>,
708+
onSuccess?: (data: any) => Promise<void>,
709+
onError?: (error: string) => void,
710+
setLoadingState?: (loading: boolean) => void,
711+
}): Promise<any> {
712+
setLoadingState?.(true);
713+
714+
try {
715+
const data = await callAdminForthApi({
716+
path: '/start_custom_action',
717+
method: 'POST',
718+
body: {
719+
resourceId,
720+
actionId,
721+
recordId,
722+
extra: extra || {},
723+
}
724+
});
725+
726+
if (data?.redirectUrl) {
727+
// Check if the URL should open in a new tab
728+
if (data.redirectUrl.includes('target=_blank')) {
729+
window.open(data.redirectUrl.replace('&target=_blank', '').replace('?target=_blank', ''), '_blank');
730+
} else {
731+
// Navigate within the app
732+
if (data.redirectUrl.startsWith('http')) {
733+
window.location.href = data.redirectUrl;
734+
} else {
735+
router.push(data.redirectUrl);
736+
}
737+
}
738+
return data;
739+
}
740+
741+
if (data?.ok) {
742+
if (onSuccess) {
743+
await onSuccess(data);
744+
}
745+
return data;
746+
}
747+
748+
if (data?.error) {
749+
if (onError) {
750+
onError(data.error);
751+
}
752+
}
753+
754+
return data;
755+
} finally {
756+
setLoadingState?.(false);
757+
}
758+
}
759+
760+
export async function executeCustomBulkAction({
761+
actionId,
762+
resourceId,
763+
recordIds,
764+
extra = {},
765+
onSuccess,
766+
onError,
767+
setLoadingState,
768+
confirmMessage,
769+
}: {
770+
actionId: string | number,
771+
resourceId: string,
772+
recordIds: (string | number)[],
773+
extra?: Record<string, any>,
774+
onSuccess?: (results: any[]) => Promise<void>,
775+
onError?: (error: string) => void,
776+
setLoadingState?: (loading: boolean) => void,
777+
confirmMessage?: string,
778+
}): Promise<any> {
779+
if (!recordIds || recordIds.length === 0) {
780+
if (onError) {
781+
onError('No records selected');
782+
}
783+
return { error: 'No records selected' };
784+
}
785+
786+
if (confirmMessage) {
787+
const { confirm } = useAdminforth();
788+
const confirmed = await confirm({
789+
message: confirmMessage,
790+
});
791+
if (!confirmed) {
792+
return { cancelled: true };
793+
}
794+
}
795+
796+
setLoadingState?.(true);
797+
798+
try {
799+
// Execute action for all records in parallel using Promise.all
800+
const results = await Promise.all(
801+
recordIds.map(recordId =>
802+
callAdminForthApi({
803+
path: '/start_custom_action',
804+
method: 'POST',
805+
body: {
806+
resourceId,
807+
actionId,
808+
recordId,
809+
extra: extra || {},
810+
}
811+
})
812+
)
813+
);
814+
815+
const lastResult = results[results.length - 1];
816+
if (lastResult?.redirectUrl) {
817+
if (lastResult.redirectUrl.includes('target=_blank')) {
818+
window.open(lastResult.redirectUrl.replace('&target=_blank', '').replace('?target=_blank', ''), '_blank');
819+
} else {
820+
if (lastResult.redirectUrl.startsWith('http')) {
821+
window.location.href = lastResult.redirectUrl;
822+
} else {
823+
router.push(lastResult.redirectUrl);
824+
}
825+
}
826+
return lastResult;
827+
}
828+
829+
const allSucceeded = results.every(r => r?.ok);
830+
const hasErrors = results.some(r => r?.error);
831+
832+
if (allSucceeded) {
833+
if (onSuccess) {
834+
await onSuccess(results);
835+
}
836+
return { ok: true, results };
837+
}
838+
839+
if (hasErrors) {
840+
const errorMessages = results.filter(r => r?.error).map(r => r.error).join(', ');
841+
if (onError) {
842+
onError(errorMessages);
843+
}
844+
return { error: errorMessages, results };
845+
}
846+
847+
return { results };
848+
} finally {
849+
setLoadingState?.(false);
850+
}
693851
}

0 commit comments

Comments
 (0)