Skip to content

Commit 6e9d900

Browse files
committed
Merge branch 'next' of github.com:devforth/adminforth into polymorphic
2 parents 9873c6f + 1f6f125 commit 6e9d900

File tree

9 files changed

+260
-13
lines changed

9 files changed

+260
-13
lines changed

adminforth/commands/createApp/templates/index.ts.hbs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,6 @@ if (import.meta.url === `file://${process.argv[1]}`) {
7777
});
7878

7979
admin.express.listen(port, () => {
80-
console.log(`Example app listening at http://localhost:${port}`);
8180
console.log(`\n⚡ AdminForth is available at http://localhost:${port}${ADMIN_BASE_URL}\n`);
8281
});
8382
}

adminforth/dataConnectors/mysql.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,12 @@ class MysqlConnector extends AdminForthBaseConnector implements IAdminForthDataS
88

99
async setupClient(url): Promise<void> {
1010
try {
11-
this.client = await mysql.createConnection(url);
11+
this.client = mysql.createPool({
12+
uri: url,
13+
waitForConnections: true,
14+
connectionLimit: 10, // Adjust based on your needs
15+
queueLimit: 0
16+
});
1217
} catch (e) {
1318
console.error(`Failed to connect to MySQL: ${e}`);
1419
}
@@ -33,7 +38,7 @@ class MysqlConnector extends AdminForthBaseConnector implements IAdminForthDataS
3338
};
3439

3540
async discoverFields(resource) {
36-
const [results] = await this.client.query("SHOW COLUMNS FROM " + resource.table);
41+
const [results] = await this.client.execute("SHOW COLUMNS FROM " + resource.table);
3742
const fieldTypes = {};
3843
results.forEach((row) => {
3944
const field: any = {};
@@ -231,7 +236,7 @@ class MysqlConnector extends AdminForthBaseConnector implements IAdminForthDataS
231236
if (process.env.HEAVY_DEBUG_QUERY) {
232237
console.log('🪲📜 MySQL Q:', q, 'values:', filterValues);
233238
}
234-
const [results] = await this.client.query(q, filterValues);
239+
const [results] = await this.client.execute(q, filterValues);
235240
return +results[0]["COUNT(*)"];
236241
}
237242

@@ -243,7 +248,7 @@ class MysqlConnector extends AdminForthBaseConnector implements IAdminForthDataS
243248
if (process.env.HEAVY_DEBUG_QUERY) {
244249
console.log('🪲📜 MySQL Q:', q);
245250
}
246-
const [results] = await this.client.query(q);
251+
const [results] = await this.client.execute(q);
247252
const { min, max } = results[0];
248253
result[col.name] = {
249254
min, max,

adminforth/modules/codeInjector.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -363,6 +363,14 @@ class CodeInjector implements ICodeInjector {
363363
}
364364
});
365365
}
366+
367+
if (resource.options?.actions) {
368+
resource.options.actions.forEach((action) => {
369+
if (action.icon) {
370+
icons.push(action.icon);
371+
}
372+
});
373+
}
366374
});
367375

368376
const uniqueIcons = Array.from(new Set(icons));

adminforth/modules/configValidator.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -330,6 +330,31 @@ export default class ConfigValidator implements IConfigValidator {
330330
return showInTransformedToObject as ShowIn;
331331
}
332332

333+
validateAndNormalizeCustomActions(resInput: AdminForthResourceInput, res: Partial<AdminForthResource>, errors: string[]): any[] {
334+
if (!resInput.options?.actions) {
335+
return [];
336+
}
337+
338+
const actions = [...resInput.options.actions];
339+
340+
actions.forEach((action) => {
341+
if (!action.name) {
342+
errors.push(`Resource "${res.resourceId}" has action without name`);
343+
}
344+
345+
if (!action.action) {
346+
errors.push(`Resource "${res.resourceId}" action "${action.name}" must have action function`);
347+
}
348+
349+
// Generate ID if not present
350+
if (!action.id) {
351+
action.id = md5hash(action.name);
352+
}
353+
});
354+
355+
return actions;
356+
}
357+
333358
validateAndNormalizeResources(errors: string[], warnings: string[]): AdminForthResource[] {
334359
if (!this.inputConfig.resources) {
335360
errors.push('No resources defined, at least one resource must be defined');
@@ -644,6 +669,7 @@ export default class ConfigValidator implements IConfigValidator {
644669
}
645670

646671
options.bulkActions = this.validateAndNormalizeBulkActions(resInput, res, errors);
672+
options.actions = this.validateAndNormalizeCustomActions(resInput, res, errors);
647673

648674
// if pageInjection is a string, make array with one element. Also check file exists
649675
const possibleInjections = ['beforeBreadcrumbs', 'afterBreadcrumbs', 'bottom', 'threeDotsDropdownItems', 'customActionIcons'];

adminforth/modules/restApi.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1213,5 +1213,31 @@ export default class AdminForthRestAPI implements IAdminForthRestAPI {
12131213
this.adminforth.activatedPlugins.forEach((plugin) => {
12141214
plugin.setupEndpoints(server);
12151215
});
1216+
1217+
server.endpoint({
1218+
method: 'POST',
1219+
path: '/start_custom_action',
1220+
handler: async ({ body, adminUser, tr }) => {
1221+
const { resourceId, actionId, recordId } = body;
1222+
const resource = this.adminforth.config.resources.find((res) => res.resourceId == resourceId);
1223+
if (!resource) {
1224+
return { error: await tr(`Resource {resourceId} not found`, 'errors', { resourceId }) };
1225+
}
1226+
console.log("resource", actionId);
1227+
const action = resource.options.actions.find((act) => act.id == actionId);
1228+
if (!action) {
1229+
return { error: await tr(`Action {actionId} not found`, 'errors', { actionId }) };
1230+
}
1231+
1232+
const response = await action.action({ recordId, adminUser, resource, tr, adminforth: this.adminforth });
1233+
1234+
return {
1235+
actionId,
1236+
recordId,
1237+
resourceId,
1238+
...response
1239+
}
1240+
}
1241+
});
12161242
}
12171243
}

adminforth/spa/src/components/ResourceListTable.vue

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,19 @@
172172
:record="row"
173173
/>
174174
</template>
175+
176+
<template v-if="resource.options?.actions">
177+
<Tooltip v-for="action in resource.options.actions.filter(a => a.showIn?.list)" :key="action.id">
178+
<button
179+
@click="startCustomAction(action.id, row)"
180+
>
181+
<component v-if="action.icon" :is="getIcon(action.icon)" class="w-5 h-5 text-lightPrimary dark:text-darkPrimary"></component>
182+
</button>
183+
<template v-slot:tooltip>
184+
{{ action.name }}
185+
</template>
186+
</Tooltip>
187+
</template>
175188
</div>
176189
</td>
177190
</tr>
@@ -280,7 +293,7 @@ import { getCustomComponent } from '@/utils';
280293
import { useCoreStore } from '@/stores/core';
281294
import { showSuccesTost, showErrorTost } from '@/composables/useFrontendApi';
282295
import SkeleteLoader from '@/components/SkeleteLoader.vue';
283-
296+
import { getIcon } from '@/utils';
284297
import {
285298
IconInboxOutline,
286299
} from '@iconify-prerendered/vue-flowbite';
@@ -505,4 +518,39 @@ async function deleteRecord(row) {
505518
};
506519
}
507520
}
521+
522+
const actionLoadingStates = ref({});
523+
524+
async function startCustomAction(actionId, row) {
525+
actionLoadingStates.value[actionId] = true;
526+
527+
const data = await callAdminForthApi({
528+
path: '/start_custom_action',
529+
method: 'POST',
530+
body: {
531+
resourceId: props.resource.resourceId,
532+
actionId: actionId,
533+
recordId: row._primaryKeyValue
534+
}
535+
});
536+
537+
actionLoadingStates.value[actionId] = false;
538+
539+
if (data?.ok) {
540+
emits('update:records', true);
541+
542+
if (data.successMessage) {
543+
adminforth.alert({
544+
message: data.successMessage,
545+
variant: 'success'
546+
});
547+
}
548+
}
549+
550+
if (data?.error) {
551+
showErrorTost(data.error);
552+
}
553+
}
554+
555+
508556
</script>

adminforth/spa/src/components/ThreeDotsMenu.vue

Lines changed: 59 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<template >
2-
<template v-if="threeDotsDropdownItems?.length">
2+
<template v-if="threeDotsDropdownItems?.length || customActions?.length">
33
<button
44
data-dropdown-toggle="listThreeDotsDropdown"
55
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"
@@ -23,21 +23,73 @@
2323
/>
2424
</a>
2525
</li>
26+
<li v-for="action in customActions" :key="action.id">
27+
<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">
28+
<div class="flex items-center gap-2">
29+
<component
30+
v-if="action.icon"
31+
:is="getIcon(action.icon)"
32+
class="w-4 h-4 text-lightPrimary dark:text-darkPrimary"
33+
/>
34+
{{ action.name }}
35+
</div>
36+
</a>
37+
</li>
2638
</ul>
2739
</div>
2840
</template>
2941
</template>
3042

3143

3244
<script setup lang="ts">
45+
import { getCustomComponent, getIcon } from '@/utils';
46+
import { useCoreStore } from '@/stores/core';
47+
import adminforth from '@/adminforth';
48+
import { callAdminForthApi } from '@/utils';
49+
import { useRoute } from 'vue-router';
3350
34-
import { getCustomComponent } from '@/utils';
35-
import { useCoreStore } from '@/stores/core'
51+
const route = useRoute();
52+
const coreStore = useCoreStore();
3653
37-
const coreStore = useCoreStore()
54+
const props = defineProps({
55+
threeDotsDropdownItems: Array,
56+
customActions: Array
57+
});
3858
39-
const props = defineProps<{
40-
threeDotsDropdownItems: any[] | undefined
41-
}>()
59+
async function handleActionClick(action) {
60+
adminforth.list.closeThreeDotsDropdown();
61+
62+
const actionId = action.id;
63+
const data = await callAdminForthApi({
64+
path: '/start_custom_action',
65+
method: 'POST',
66+
body: {
67+
resourceId: route.params.resourceId,
68+
actionId: actionId,
69+
recordId: route.params.primaryKey
70+
}
71+
});
72+
73+
if (data?.ok) {
74+
await coreStore.fetchRecord({
75+
resourceId: route.params.resourceId,
76+
primaryKey: route.params.primaryKey,
77+
source: 'show',
78+
});
4279
80+
if (data.successMessage) {
81+
adminforth.alert({
82+
message: data.successMessage,
83+
variant: 'success'
84+
});
85+
}
86+
}
87+
88+
if (data?.error) {
89+
adminforth.alert({
90+
message: data.error,
91+
variant: 'danger'
92+
});
93+
}
94+
}
4395
</script>

adminforth/spa/src/views/ShowView.vue

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,22 @@
1010
:adminUser="coreStore.adminUser"
1111
/>
1212
<BreadcrumbsWithButtons>
13+
<template v-if="coreStore.resource?.options?.actions">
14+
<button
15+
v-for="action in coreStore.resource.options.actions.filter(a => a.showIn?.showButton)"
16+
:key="action.id"
17+
@click="startCustomAction(action.id)"
18+
:disabled="actionLoadingStates[action.id]"
19+
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"
20+
>
21+
<component
22+
v-if="action.icon"
23+
:is="getIcon(action.icon)"
24+
class="w-4 h-4 me-2 text-lightPrimary dark:text-darkPrimary"
25+
/>
26+
{{ action.name }}
27+
</button>
28+
</template>
1329
<RouterLink v-if="coreStore.resource?.options?.allowedActions?.create"
1430
:to="{ name: 'resource-create', params: { resourceId: $route.params.resourceId } }"
1531
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"
@@ -34,6 +50,7 @@
3450

3551
<ThreeDotsMenu
3652
:threeDotsDropdownItems="coreStore.resourceOptions?.pageInjections?.show?.threeDotsDropdownItems"
53+
:customActions="customActions"
3754
></ThreeDotsMenu>
3855
</BreadcrumbsWithButtons>
3956

@@ -121,13 +138,20 @@ import ThreeDotsMenu from '@/components/ThreeDotsMenu.vue';
121138
import ShowTable from '@/components/ShowTable.vue';
122139
import adminforth from "@/adminforth";
123140
import { useI18n } from 'vue-i18n';
141+
import { getIcon } from '@/utils';
124142
125143
const route = useRoute();
126144
const router = useRouter();
127145
const loading = ref(true);
128146
const { t } = useI18n();
129147
const coreStore = useCoreStore();
130148
149+
const actionLoadingStates = ref({});
150+
151+
const customActions = computed(() => {
152+
return coreStore.resource?.options?.actions?.filter(a => a.showIn?.showThreeDotsMenu) || [];
153+
});
154+
131155
onMounted(async () => {
132156
loading.value = true;
133157
await coreStore.fetchResourceFull({
@@ -206,4 +230,39 @@ async function deleteRecord(row) {
206230
207231
}
208232
233+
async function startCustomAction(actionId) {
234+
actionLoadingStates.value[actionId] = true;
235+
236+
const data = await callAdminForthApi({
237+
path: '/start_custom_action',
238+
method: 'POST',
239+
body: {
240+
resourceId: route.params.resourceId,
241+
actionId: actionId,
242+
recordId: route.params.primaryKey
243+
}
244+
});
245+
246+
actionLoadingStates.value[actionId] = false;
247+
248+
if (data?.ok) {
249+
await coreStore.fetchRecord({
250+
resourceId: route.params.resourceId,
251+
primaryKey: route.params.primaryKey,
252+
source: 'show',
253+
});
254+
255+
if (data.successMessage) {
256+
adminforth.alert({
257+
message: data.successMessage,
258+
variant: 'success'
259+
});
260+
}
261+
}
262+
263+
if (data?.error) {
264+
showErrorTost(data.error);
265+
}
266+
}
267+
209268
</script>

0 commit comments

Comments
 (0)