Skip to content

Commit ddd460f

Browse files
committed
Merge branch 'next' of github.com:devforth/adminforth into next
2 parents 3e36c29 + 046e740 commit ddd460f

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
@@ -341,7 +341,7 @@
341341
342342
343343
import { computed, onMounted, ref, watch, useTemplateRef, nextTick, type Ref } from 'vue';
344-
import { callAdminForthApi } from '@/utils';
344+
import { callAdminForthApi, executeCustomAction } from '@/utils';
345345
import { useI18n } from 'vue-i18n';
346346
import ValueRenderer from '@/components/ValueRenderer.vue';
347347
import { getCustomComponent, formatComponent } from '@/utils';
@@ -607,50 +607,28 @@ async function deleteRecord(row: any) {
607607
const actionLoadingStates = ref<Record<string | number, boolean>>({});
608608
609609
async function startCustomAction(actionId: string | number, row: any, extraData: Record<string, any> = {}) {
610-
611-
actionLoadingStates.value[actionId] = true;
612-
613-
const data = await callAdminForthApi({
614-
path: '/start_custom_action',
615-
method: 'POST',
616-
body: {
617-
resourceId: props.resource?.resourceId,
618-
actionId: actionId,
619-
recordId: row._primaryKeyValue,
620-
extra: extraData
621-
}
622-
});
623-
624-
actionLoadingStates.value[actionId] = false;
625-
626-
if (data?.redirectUrl) {
627-
// Check if the URL should open in a new tab
628-
if (data.redirectUrl.includes('target=_blank')) {
629-
window.open(data.redirectUrl.replace('&target=_blank', '').replace('?target=_blank', ''), '_blank');
630-
} else {
631-
// Navigate within the app
632-
if (data.redirectUrl.startsWith('http')) {
633-
window.location.href = data.redirectUrl;
634-
} else {
635-
router.push(data.redirectUrl);
610+
await executeCustomAction({
611+
actionId,
612+
resourceId: props.resource?.resourceId || '',
613+
recordId: row._primaryKeyValue,
614+
extra: extraData,
615+
setLoadingState: (loading: boolean) => {
616+
actionLoadingStates.value[actionId] = loading;
617+
},
618+
onSuccess: async (data: any) => {
619+
emits('update:records', true);
620+
621+
if (data.successMessage) {
622+
alert({
623+
message: data.successMessage,
624+
variant: 'success'
625+
});
636626
}
627+
},
628+
onError: (error: string) => {
629+
showErrorTost(error);
637630
}
638-
return;
639-
}
640-
if (data?.ok) {
641-
emits('update:records', true);
642-
643-
if (data.successMessage) {
644-
alert({
645-
message: data.successMessage,
646-
variant: 'success'
647-
});
648-
}
649-
}
650-
651-
if (data?.error) {
652-
showErrorTost(data.error);
653-
}
631+
});
654632
}
655633
656634
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)