Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions adminforth/modules/configValidator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,31 @@ export default class ConfigValidator implements IConfigValidator {
return showInTransformedToObject as ShowIn;
}

validateAndNormalizeCustomActions(resInput: AdminForthResourceInput, res: Partial<AdminForthResource>, errors: string[]): any[] {
if (!resInput.options?.actions) {
return [];
}

const actions = [...resInput.options.actions];

actions.forEach((action) => {
if (!action.name) {
errors.push(`Resource "${res.resourceId}" has action without name`);
}

if (!action.action) {
errors.push(`Resource "${res.resourceId}" action "${action.name}" must have action function`);
}

// Generate ID if not present
if (!action.id) {
action.id = md5hash(action.name);
}
});

return actions;
}

validateAndNormalizeResources(errors: string[], warnings: string[]): AdminForthResource[] {
if (!this.inputConfig.resources) {
errors.push('No resources defined, at least one resource must be defined');
Expand Down Expand Up @@ -590,6 +615,7 @@ export default class ConfigValidator implements IConfigValidator {
}

options.bulkActions = this.validateAndNormalizeBulkActions(resInput, res, errors);
options.actions = this.validateAndNormalizeCustomActions(resInput, res, errors);

// if pageInjection is a string, make array with one element. Also check file exists
const possibleInjections = ['beforeBreadcrumbs', 'afterBreadcrumbs', 'bottom', 'threeDotsDropdownItems', 'customActionIcons'];
Expand Down
26 changes: 26 additions & 0 deletions adminforth/modules/restApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1048,5 +1048,31 @@ export default class AdminForthRestAPI implements IAdminForthRestAPI {
this.adminforth.activatedPlugins.forEach((plugin) => {
plugin.setupEndpoints(server);
});

server.endpoint({
method: 'POST',
path: '/start_custom_action',
handler: async ({ body, adminUser, tr }) => {
const { resourceId, actionId, recordId } = body;
const resource = this.adminforth.config.resources.find((res) => res.resourceId == resourceId);
if (!resource) {
return { error: await tr(`Resource {resourceId} not found`, 'errors', { resourceId }) };
}
console.log("resource", actionId);
const action = resource.options.actions.find((act) => act.id == actionId);
if (!action) {
return { error: await tr(`Action {actionId} not found`, 'errors', { actionId }) };
}

const response = await action.action({ recordId, adminUser, resource, tr });

return {
actionId,
recordId,
resourceId,
...response
}
}
});
}
}
50 changes: 49 additions & 1 deletion adminforth/spa/src/components/ResourceListTable.vue
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,19 @@
:record="row"
/>
</template>

<template v-if="resource.options?.actions">
<Tooltip v-for="action in resource.options.actions.filter(a => a.showIn?.list)" :key="action.id">
<button
@click="startCustomAction(action.id, row)"
>
<component v-if="action.icon" :is="getIcon(action.icon)" class="w-5 h-5 text-lightPrimary dark:text-darkPrimary"></component>
</button>
<template v-slot:tooltip>
{{ action.name }}
</template>
</Tooltip>
</template>
</div>
</td>
</tr>
Expand Down Expand Up @@ -280,7 +293,7 @@ import { getCustomComponent } from '@/utils';
import { useCoreStore } from '@/stores/core';
import { showSuccesTost, showErrorTost } from '@/composables/useFrontendApi';
import SkeleteLoader from '@/components/SkeleteLoader.vue';

import { getIcon } from '@/utils';
import {
IconInboxOutline,
} from '@iconify-prerendered/vue-flowbite';
Expand Down Expand Up @@ -505,4 +518,39 @@ async function deleteRecord(row) {
};
}
}

const actionLoadingStates = ref({});

async function startCustomAction(actionId, row) {
actionLoadingStates.value[actionId] = true;

const data = await callAdminForthApi({
path: '/start_custom_action',
method: 'POST',
body: {
resourceId: props.resource.resourceId,
actionId: actionId,
recordId: row._primaryKeyValue
}
});

actionLoadingStates.value[actionId] = false;

if (data?.ok) {
emits('update:records', true);

if (data.successMessage) {
adminforth.alert({
message: data.successMessage,
variant: 'success'
});
}
}

if (data?.error) {
showErrorTost(data.error);
}
}


</script>
66 changes: 59 additions & 7 deletions adminforth/spa/src/components/ThreeDotsMenu.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<template >
<template v-if="threeDotsDropdownItems?.length">
<template v-if="threeDotsDropdownItems?.length || customActions?.length">
<button
data-dropdown-toggle="listThreeDotsDropdown"
class="flex items-center py-2 px-2 text-sm font-medium text-gray-900 focus:outline-none bg-white rounded border border-gray-300 hover:bg-gray-100 hover:text-lightPrimary focus:z-10 focus:ring-4 focus:ring-gray-100 dark:focus:ring-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700 rounded-default"
Expand All @@ -23,21 +23,73 @@
/>
</a>
</li>
<li v-for="action in customActions" :key="action.id">
<a href="#" @click.prevent="handleActionClick(action)" class="block px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white">
<div class="flex items-center gap-2">
<component
v-if="action.icon"
:is="getIcon(action.icon)"
class="w-4 h-4 text-lightPrimary dark:text-darkPrimary"
/>
{{ action.name }}
</div>
</a>
</li>
</ul>
</div>
</template>
</template>


<script setup lang="ts">
import { getCustomComponent, getIcon } from '@/utils';
import { useCoreStore } from '@/stores/core';
import adminforth from '@/adminforth';
import { callAdminForthApi } from '@/utils';
import { useRoute } from 'vue-router';

import { getCustomComponent } from '@/utils';
import { useCoreStore } from '@/stores/core'
const route = useRoute();
const coreStore = useCoreStore();

const coreStore = useCoreStore()
const props = defineProps({
threeDotsDropdownItems: Array,
customActions: Array
});

const props = defineProps<{
threeDotsDropdownItems: any[] | undefined
}>()
async function handleActionClick(action) {
adminforth.list.closeThreeDotsDropdown();

const actionId = action.id;
const data = await callAdminForthApi({
path: '/start_custom_action',
method: 'POST',
body: {
resourceId: route.params.resourceId,
actionId: actionId,
recordId: route.params.primaryKey
}
});

if (data?.ok) {
await coreStore.fetchRecord({
resourceId: route.params.resourceId,
primaryKey: route.params.primaryKey,
source: 'show',
});

if (data.successMessage) {
adminforth.alert({
message: data.successMessage,
variant: 'success'
});
}
}

if (data?.error) {
adminforth.alert({
message: data.error,
variant: 'danger'
});
}
}
</script>
59 changes: 59 additions & 0 deletions adminforth/spa/src/views/ShowView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,22 @@
:adminUser="coreStore.adminUser"
/>
<BreadcrumbsWithButtons>
<template v-if="coreStore.resource?.options?.actions">
<button
v-for="action in coreStore.resource.options.actions.filter(a => a.showIn?.showButton)"
:key="action.id"
@click="startCustomAction(action.id)"
:disabled="actionLoadingStates[action.id]"
class="flex items-center py-1 px-3 text-sm font-medium text-gray-900 focus:outline-none bg-white rounded-default border border-gray-300 hover:bg-gray-100 hover:text-lightPrimary focus:z-10 focus:ring-4 focus:ring-gray-100 dark:focus:ring-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700"
>
<component
v-if="action.icon"
:is="getIcon(action.icon)"
class="w-4 h-4 me-2 text-lightPrimary dark:text-darkPrimary"
/>
{{ action.name }}
</button>
</template>
<RouterLink v-if="coreStore.resource?.options?.allowedActions?.create"
:to="{ name: 'resource-create', params: { resourceId: $route.params.resourceId } }"
class="flex items-center py-1 px-3 text-sm font-medium text-gray-900 focus:outline-none bg-white rounded border border-gray-300 hover:bg-gray-100 hover:text-lightPrimary focus:z-10 focus:ring-4 focus:ring-gray-100 dark:focus:ring-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700 rounded-default"
Expand All @@ -34,6 +50,7 @@

<ThreeDotsMenu
:threeDotsDropdownItems="coreStore.resourceOptions?.pageInjections?.show?.threeDotsDropdownItems"
:customActions="customActions"
></ThreeDotsMenu>
</BreadcrumbsWithButtons>

Expand Down Expand Up @@ -121,13 +138,20 @@ import ThreeDotsMenu from '@/components/ThreeDotsMenu.vue';
import ShowTable from '@/components/ShowTable.vue';
import adminforth from "@/adminforth";
import { useI18n } from 'vue-i18n';
import { getIcon } from '@/utils';

const route = useRoute();
const router = useRouter();
const loading = ref(true);
const { t } = useI18n();
const coreStore = useCoreStore();

const actionLoadingStates = ref({});

const customActions = computed(() => {
return coreStore.resource?.options?.actions?.filter(a => a.showIn?.showThreeDotsMenu) || [];
});

onMounted(async () => {
loading.value = true;
await coreStore.fetchResourceFull({
Expand Down Expand Up @@ -206,4 +230,39 @@ async function deleteRecord(row) {

}

async function startCustomAction(actionId) {
actionLoadingStates.value[actionId] = true;

const data = await callAdminForthApi({
path: '/start_custom_action',
method: 'POST',
body: {
resourceId: route.params.resourceId,
actionId: actionId,
recordId: route.params.primaryKey
}
});

actionLoadingStates.value[actionId] = false;

if (data?.ok) {
await coreStore.fetchRecord({
resourceId: route.params.resourceId,
primaryKey: route.params.primaryKey,
source: 'show',
});

if (data.successMessage) {
adminforth.alert({
message: data.successMessage,
variant: 'success'
});
}
}

if (data?.error) {
showErrorTost(data.error);
}
}

</script>