adminforth 1.3.20 → 1.3.22

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/auth.ts CHANGED
@@ -63,7 +63,7 @@ class AdminForthAuth {
63
63
  return jwt.sign({...payload, t: type}, secret, { expiresIn });
64
64
  }
65
65
 
66
- async verify(jwtToken: string, mustHaveType: string): Promise<Object> {
66
+ async verify(jwtToken: string, mustHaveType: string, decodeUser: boolean | undefined = true): Promise<Object> {
67
67
  // read ADMINFORH_SECRET from environment if not drop error
68
68
  const secret = process.env.ADMINFORTH_SECRET;
69
69
  if (!secret) {
@@ -88,13 +88,16 @@ class AdminForthAuth {
88
88
  console.error(`Invalid token type during verification: ${t}, must be ${mustHaveType}`);
89
89
  return null;
90
90
  }
91
- const dbUser = await this.adminforth.getUserByPk(pk);
92
- if (!dbUser) {
93
- console.error(`User with pk ${pk} not found in database`);
94
- // will logout user which was deleted
95
- return null;
91
+ if (decodeUser !== false) {
92
+ const dbUser = await this.adminforth.getUserByPk(pk);
93
+ if (!dbUser) {
94
+ console.error(`User with pk ${pk} not found in database`);
95
+ // will logout user which was deleted
96
+ return null;
97
+ }
98
+ decoded.dbUser = dbUser;
96
99
  }
97
- decoded.dbUser = dbUser;
100
+
98
101
  return decoded;
99
102
  }
100
103
 
package/dist/auth.js CHANGED
@@ -51,8 +51,8 @@ class AdminForthAuth {
51
51
  const expiresIn = process.env.ADMINFORTH_AUTH_EXPIRESIN || '24h';
52
52
  return jwt.sign(Object.assign(Object.assign({}, payload), { t: type }), secret, { expiresIn });
53
53
  }
54
- verify(jwtToken, mustHaveType) {
55
- return __awaiter(this, void 0, void 0, function* () {
54
+ verify(jwtToken_1, mustHaveType_1) {
55
+ return __awaiter(this, arguments, void 0, function* (jwtToken, mustHaveType, decodeUser = true) {
56
56
  // read ADMINFORH_SECRET from environment if not drop error
57
57
  const secret = process.env.ADMINFORTH_SECRET;
58
58
  if (!secret) {
@@ -80,13 +80,15 @@ class AdminForthAuth {
80
80
  console.error(`Invalid token type during verification: ${t}, must be ${mustHaveType}`);
81
81
  return null;
82
82
  }
83
- const dbUser = yield this.adminforth.getUserByPk(pk);
84
- if (!dbUser) {
85
- console.error(`User with pk ${pk} not found in database`);
86
- // will logout user which was deleted
87
- return null;
83
+ if (decodeUser !== false) {
84
+ const dbUser = yield this.adminforth.getUserByPk(pk);
85
+ if (!dbUser) {
86
+ console.error(`User with pk ${pk} not found in database`);
87
+ // will logout user which was deleted
88
+ return null;
89
+ }
90
+ decoded.dbUser = dbUser;
88
91
  }
89
- decoded.dbUser = dbUser;
90
92
  return decoded;
91
93
  });
92
94
  }
@@ -195,12 +195,12 @@ class CodeInjector {
195
195
  const registerCustomPages = (config) => {
196
196
  if (config.customization.customPages) {
197
197
  config.customization.customPages.forEach((page) => {
198
- var _a, _b, _c, _d;
198
+ var _a, _b, _c;
199
199
  routes += `{
200
200
  path: '${page.path}',
201
201
  name: '${page.path}',
202
202
  component: () => import('${((_a = page === null || page === void 0 ? void 0 : page.component) === null || _a === void 0 ? void 0 : _a.file) || page.component}'),
203
- meta: { title: '${((_b = page.meta) === null || _b === void 0 ? void 0 : _b.title) || page.path.replace('/', '')}',customLayout:${(_d = (_c = page === null || page === void 0 ? void 0 : page.component) === null || _c === void 0 ? void 0 : _c.meta) === null || _d === void 0 ? void 0 : _d.customLayout}}
203
+ meta: ${JSON.stringify(Object.assign(Object.assign({}, (((_b = page === null || page === void 0 ? void 0 : page.component) === null || _b === void 0 ? void 0 : _b.meta) || {})), { title: ((_c = page.meta) === null || _c === void 0 ? void 0 : _c.title) || page.path.replace('/', '') }))}
204
204
  },`;
205
205
  });
206
206
  }
@@ -119,6 +119,12 @@ export default class ConfigValidator {
119
119
  if (((_a = this.config) === null || _a === void 0 ? void 0 : _a.customization.brandName) === undefined) {
120
120
  this.config.customization.brandName = 'AdminForth';
121
121
  }
122
+ if (this.config.customization.loginPageInjections === undefined) {
123
+ this.config.customization.loginPageInjections = {};
124
+ }
125
+ if (this.config.customization.loginPageInjections.underInputs === undefined) {
126
+ this.config.customization.loginPageInjections.underInputs = [];
127
+ }
122
128
  if (this.config.customization.brandLogo) {
123
129
  errors.push(...this.checkCustomFileExists(this.config.customization.brandLogo));
124
130
  }
@@ -245,6 +251,7 @@ export default class ConfigValidator {
245
251
  label: `Delete checked`,
246
252
  state: 'danger',
247
253
  icon: 'flowbite:trash-bin-outline',
254
+ confirm: 'Are you sure you want to delete selected items?',
248
255
  allowed: (_b) => __awaiter(this, [_b], void 0, function* ({ resource, adminUser, allowedActions }) { return allowedActions.delete; }),
249
256
  action: (_c) => __awaiter(this, [_c], void 0, function* ({ selectedIds, adminUser }) {
250
257
  const connector = this.adminforth.connectors[res.dataSource];
@@ -280,7 +287,7 @@ export default class ConfigValidator {
280
287
  if (error) {
281
288
  return { error, ok: false };
282
289
  }
283
- return { ok: true };
290
+ return { ok: true, successMessage: `${selectedIds.length} item${selectedIds.length > 1 ? 's' : ''} deleted` };
284
291
  })
285
292
  });
286
293
  }
@@ -43,7 +43,7 @@ export default class AdminForthRestAPI {
43
43
  path: '/login',
44
44
  handler: (_a) => __awaiter(this, [_a], void 0, function* ({ body, response }) {
45
45
  var _b, _c, _d, _e;
46
- const INVALID_MESSAGE = 'Invalid username or password';
46
+ const INVALID_MESSAGE = 'Invalid Username or Password';
47
47
  const { username, password } = body;
48
48
  let adminUser;
49
49
  let toReturn = { ok: true, allowedLogin: true };
@@ -72,7 +72,7 @@ export default class AdminForthRestAPI {
72
72
  sort: [],
73
73
  })).data) === null || _b === void 0 ? void 0 : _b[0];
74
74
  if (!userRecord) {
75
- return { error: 'User not found' };
75
+ return { error: INVALID_MESSAGE };
76
76
  }
77
77
  const passwordHash = userRecord[this.adminforth.config.auth.passwordHashField];
78
78
  const valid = yield AdminForthAuth.verifyPassword(password, passwordHash);
@@ -139,6 +139,7 @@ export default class AdminForthRestAPI {
139
139
  title: (_j = this.adminforth.config.customization) === null || _j === void 0 ? void 0 : _j.title,
140
140
  demoCredentials: this.adminforth.config.auth.demoCredentials,
141
141
  loginPromptHTML: this.adminforth.config.auth.loginPromptHTML,
142
+ loginPageInjections: this.adminforth.config.customization.loginPageInjections,
142
143
  };
143
144
  }),
144
145
  });
package/index.ts CHANGED
@@ -264,7 +264,7 @@ class AdminForth implements IAdminForth {
264
264
  return { ok, error, createdRecord };
265
265
  }
266
266
 
267
- resource(resourceId: string) {
267
+ resource(resourceId: string): IOperationalResource {
268
268
  if (this.statuses.dbDiscover !== 'done') {
269
269
  if (this.statuses.dbDiscover === 'running') {
270
270
  throw new Error('Database discovery is running. You can\'t use data API while database discovery is not finished.\n'+
@@ -10,6 +10,7 @@ import AdminForth from '../index.js';
10
10
  import { ADMIN_FORTH_ABSOLUTE_PATH, getComponentNameFromPath, transformObject, deepMerge } from './utils.js';
11
11
  import { ICodeInjector } from '../types/AdminForthConfig.js';
12
12
  import { StylesGenerator } from './styleGenerator.js';
13
+ import { title } from 'process';
13
14
 
14
15
 
15
16
  let TMP_DIR;
@@ -211,7 +212,12 @@ class CodeInjector implements ICodeInjector {
211
212
  path: '${page.path}',
212
213
  name: '${page.path}',
213
214
  component: () => import('${page?.component?.file || page.component}'),
214
- meta: { title: '${page.meta?.title || page.path.replace('/', '')}',customLayout:${page?.component?.meta?.customLayout }}
215
+ meta: ${
216
+ JSON.stringify({
217
+ ...(page?.component?.meta || {}),
218
+ title: page.meta?.title || page.path.replace('/', '')
219
+ })
220
+ }
215
221
  },`})
216
222
 
217
223
  }}
@@ -6,6 +6,7 @@ import {
6
6
  AdminForthResourcePages, AllowedActionsEnum,
7
7
  type AdminForthComponentDeclarationFull,
8
8
  type AfterSaveFunction,
9
+ AdminForthBulkAction,
9
10
  } from "../types/AdminForthConfig.js";
10
11
 
11
12
  import fs from 'fs';
@@ -127,6 +128,13 @@ export default class ConfigValidator implements IConfigValidator {
127
128
  if (this.config?.customization.brandName === undefined) {
128
129
  this.config.customization.brandName = 'AdminForth';
129
130
  }
131
+ if (this.config.customization.loginPageInjections === undefined) {
132
+ this.config.customization.loginPageInjections = {};
133
+ }
134
+
135
+ if (this.config.customization.loginPageInjections.underInputs === undefined) {
136
+ this.config.customization.loginPageInjections.underInputs = [];
137
+ }
130
138
  if (this.config.customization.brandLogo) {
131
139
  errors.push(...this.checkCustomFileExists(this.config.customization.brandLogo));
132
140
  }
@@ -253,7 +261,7 @@ export default class ConfigValidator implements IConfigValidator {
253
261
 
254
262
 
255
263
  //check if resource has bulkActions
256
- let bulkActions = res?.options?.bulkActions || [];
264
+ let bulkActions: AdminForthBulkAction[] = res?.options?.bulkActions || [];
257
265
 
258
266
  if (!Array.isArray(bulkActions)) {
259
267
  errors.push(`Resource "${res.resourceId}" bulkActions must be an array`);
@@ -265,6 +273,7 @@ export default class ConfigValidator implements IConfigValidator {
265
273
  label: `Delete checked`,
266
274
  state: 'danger',
267
275
  icon: 'flowbite:trash-bin-outline',
276
+ confirm: 'Are you sure you want to delete selected items?',
268
277
  allowed: async ({ resource, adminUser, allowedActions }) => { return allowedActions.delete },
269
278
  action: async ({ selectedIds, adminUser }) => {
270
279
  const connector = this.adminforth.connectors[res.dataSource];
@@ -317,7 +326,7 @@ export default class ConfigValidator implements IConfigValidator {
317
326
  if (error) {
318
327
  return { error, ok: false };
319
328
  }
320
- return { ok: true };
329
+ return { ok: true, successMessage: `${selectedIds.length} item${selectedIds.length > 1 ? 's' : ''} deleted` };
321
330
  }
322
331
  });
323
332
  }
@@ -62,7 +62,7 @@ export default class AdminForthRestAPI {
62
62
  path: '/login',
63
63
  handler: async ({ body, response }) => {
64
64
 
65
- const INVALID_MESSAGE = 'Invalid username or password';
65
+ const INVALID_MESSAGE = 'Invalid Username or Password';
66
66
  const { username, password } = body;
67
67
  let adminUser: AdminUser;
68
68
  let toReturn: { ok: boolean, redirectTo?: string, allowedLogin:boolean } = { ok: true, allowedLogin:true};
@@ -95,8 +95,9 @@ export default class AdminForthRestAPI {
95
95
  })
96
96
  ).data?.[0];
97
97
 
98
+
98
99
  if (!userRecord) {
99
- return { error: 'User not found' };
100
+ return { error: INVALID_MESSAGE };
100
101
  }
101
102
 
102
103
  const passwordHash = userRecord[this.adminforth.config.auth.passwordHashField];
@@ -170,6 +171,7 @@ export default class AdminForthRestAPI {
170
171
  title: this.adminforth.config.customization?.title,
171
172
  demoCredentials: this.adminforth.config.auth.demoCredentials,
172
173
  loginPromptHTML: this.adminforth.config.auth.loginPromptHTML,
174
+ loginPageInjections: this.adminforth.config.customization.loginPageInjections,
173
175
  };
174
176
  },
175
177
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "adminforth",
3
- "version": "1.3.20",
3
+ "version": "1.3.22",
4
4
  "description": "OpenSource Vue3 powered forth-generation admin panel",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
package/spa/src/App.vue CHANGED
@@ -297,7 +297,11 @@ async function initRouter() {
297
297
  }
298
298
 
299
299
  async function loadMenu() {
300
- await coreStore.fetchMenuAndResource();
300
+ await initRouter();
301
+ if (!route.meta.customLayout) {
302
+ // for custom layouts we don't need to fetch menu
303
+ await coreStore.fetchMenuAndResource();
304
+ }
301
305
  loginRedirectCheckIsReady.value = true;
302
306
  }
303
307
 
@@ -6,16 +6,14 @@
6
6
  <form autocomplete="off" @submit.prevent>
7
7
  <table class="w-full text-sm text-left rtl:text-right text-gray-500 dark:text-gray-400 ">
8
8
  <thead class="text-xs text-gray-700 uppercase bg-lightFormHeading dark:bg-gray-700 dark:text-gray-400 block md:table-row-group">
9
- <tr>
10
- <th scope="col" class="px-6 py-3 hidden md:table-cell">
11
- Field
12
- </th>
13
- <th scope="col" class="px-6 py-3 w-5/6 hidden md:table-cell">
14
- Value
15
- </th>
16
-
17
-
18
- </tr>
9
+ <tr>
10
+ <th scope="col" class="px-6 py-3 hidden md:table-cell">
11
+ Field
12
+ </th>
13
+ <th scope="col" class="px-6 py-3 w-5/6 hidden md:table-cell">
14
+ Value
15
+ </th>
16
+ </tr>
19
17
  </thead>
20
18
  <tbody>
21
19
  <tr v-for="column, i in editableColumns" :key="column.name"
@@ -147,7 +145,6 @@
147
145
 
148
146
  import CustomDatePicker from "@/components/CustomDatePicker.vue";
149
147
  import Dropdown from '@/components/Dropdown.vue';
150
- import { useCoreStore } from '@/stores/core';
151
148
  import { callAdminForthApi, getCustomComponent } from '@/utils';
152
149
  import { IconExclamationCircleSolid, IconEyeSlashSolid, IconEyeSolid } from '@iconify-prerendered/vue-flowbite';
153
150
  import { computedAsync } from '@vueuse/core';
@@ -56,8 +56,18 @@ const props = defineProps({
56
56
 
57
57
  function protectAgainstXSS(value) {
58
58
  return sanitizeHtml(value, {
59
+ allowedTags: [
60
+ "address", "article", "aside", "footer", "header", "h1", "h2", "h3", "h4",
61
+ "h5", "h6", "hgroup", "main", "nav", "section", "blockquote", "dd", "div",
62
+ "dl", "dt", "figcaption", "figure", "hr", "li", "main", "ol", "p", "pre",
63
+ "ul", "a", "abbr", "b", "bdi", "bdo", "br", "cite", "code", "data", "dfn",
64
+ "em", "i", "kbd", "mark", "q", "rb", "rp", "rt", "rtc", "ruby", "s", "samp",
65
+ "small", "span", "strong", "sub", "sup", "time", "u", "var", "wbr", "caption",
66
+ "col", "colgroup", "table", "tbody", "td", "tfoot", "th", "thead", "tr", 'img'
67
+ ],
59
68
  allowedAttributes: {
60
- 'li': [ 'data-list' ],
69
+ 'li': [ 'data-list' ],
70
+ 'img': [ 'src', 'srcset', 'alt', 'title', 'width', 'height', 'loading' ]
61
71
  }
62
72
  });
63
73
  }
@@ -9,7 +9,7 @@ export function showWarningTost(message: string) {
9
9
  }
10
10
 
11
11
  export function showErrorTost(message: string, timeout?: number) {
12
- window.adminforth.alert({ message, variant: 'danger', timeout: timeout || 'unlimited'});
12
+ window.adminforth.alert({ message, variant: 'danger', timeout: timeout || 30});
13
13
  return message;
14
14
  }
15
15
 
@@ -57,7 +57,11 @@ export class FrontendAPI implements FrontendAPIInterface {
57
57
 
58
58
  confirm(params: ConfirmParams): Promise<void> {
59
59
  return new Promise((resolve, reject) => {
60
- this.modalStore.setModalContent({ content: params.message, acceptText: params.yes, cancelText: params.no })
60
+ this.modalStore.setModalContent({
61
+ content: params.message,
62
+ acceptText: params.yes || 'Yes',
63
+ cancelText: params.no || 'Cancel'
64
+ })
61
65
  this.modalStore.onAcceptFunction = resolve
62
66
  this.modalStore.onCancelFunction = reject
63
67
  this.modalStore.togleModal()
@@ -71,7 +71,7 @@ export const useCoreStore = defineStore('core', () => {
71
71
  window.adminforth.alert({
72
72
  message: respData.error,
73
73
  variant: 'danger',
74
- timeout: 'unlimited'
74
+ timeout: 30,
75
75
  });
76
76
  record.value = {};
77
77
  } else {
@@ -1,14 +1,30 @@
1
- import { ref, computed } from 'vue'
1
+ import { ref, watch, type Ref } from 'vue'
2
2
  import { defineStore } from 'pinia'
3
- import { callAdminForthApi } from '@/utils';
4
3
  import { v1 as uuid } from 'uuid';
4
+ import { useRoute } from 'vue-router';
5
+
6
+
5
7
 
6
8
  export const useToastStore = defineStore('toast', () => {
7
- const toasts = ref([]);
8
- const addToast = (toast) => {
9
- toasts.value.push({...toast, id: uuid()});
9
+ const toasts: Ref<any[]> = ref([]);
10
+ const route = useRoute();
11
+
12
+ watch(route, () => {
13
+ console.log('route changed 121');
14
+ // on route change clear all toasts older then 5 seconds
15
+ const now = +new Date();
16
+ toasts.value = toasts.value.filter((t) => now - t.createdAt < 5000);
17
+ });
18
+
19
+ const addToast = (toast: { message: string; variant: string }) => {
20
+ const toastId = uuid();
21
+ toasts.value.push({
22
+ ...toast,
23
+ id: toastId,
24
+ createdAt: +new Date(),
25
+ });
10
26
  };
11
- const removeToast = (toast) => {
27
+ const removeToast = (toast: { id: string }) => {
12
28
  toasts.value = toasts.value.filter((t) => t.id !== toast.id);
13
29
  };
14
30
  return { toasts, addToast, removeToast };
@@ -149,6 +149,10 @@ async function saveRecord() {
149
149
  primaryKey: response.newRecordId
150
150
  }
151
151
  });
152
+ window.adminforth.alert({
153
+ message: 'Record created successfully',
154
+ variant: 'success'
155
+ });
152
156
  }
153
157
  }
154
158
 
@@ -110,7 +110,6 @@
110
110
  import BreadcrumbsWithButtons from '@/components/BreadcrumbsWithButtons.vue';
111
111
  import ResourceListTable from '@/components/ResourceListTable.vue';
112
112
  import { useCoreStore } from '@/stores/core';
113
- import { useModalStore } from '@/stores/modal';
114
113
  import { useFiltersStore } from '@/stores/filters';
115
114
  import { callAdminForthApi, getIcon } from '@/utils';
116
115
  import { initFlowbite } from 'flowbite';
@@ -180,6 +179,16 @@ async function getList() {
180
179
  }
181
180
 
182
181
  async function startBulkAction(actionId) {
182
+ const action = coreStore.resource.options.bulkActions.find(a => a.id === actionId);
183
+ if (action.confirm) {
184
+ const confirmed = await window.adminforth.confirm({
185
+ message: action.confirm,
186
+ });
187
+ if (!confirmed) {
188
+ return;
189
+ }
190
+ }
191
+
183
192
  const data = await callAdminForthApi({
184
193
  path: '/start_bulk_action',
185
194
  method: 'POST',
@@ -193,6 +202,14 @@ async function startBulkAction(actionId) {
193
202
  if (data?.ok) {
194
203
  checkboxes.value = [];
195
204
  await getList();
205
+
206
+ if (data.successMessage) {
207
+ window.adminforth.alert({
208
+ message: data.successMessage,
209
+ variant: 'success'
210
+ });
211
+ }
212
+
196
213
  }
197
214
  if (data?.error) {
198
215
  showErrorTost(data.error);
@@ -56,6 +56,19 @@
56
56
  </button>
57
57
  </div>
58
58
 
59
+ <div class="flex items-start mb-5 hidden">
60
+ <div class="flex items-center h-5">
61
+ <input id="remember" type="checkbox" value="" class="w-4 h-4 border border-gray-300 rounded bg-gray-50 focus:ring-3 focus:ring-blue-300 dark:bg-gray-700 dark:border-gray-600 dark:focus:ring-blue-600 dark:ring-offset-gray-800 dark:focus:ring-offset-gray-800" required />
62
+ </div>
63
+ <label for="remember" class="ms-2 text-sm font-medium text-gray-900 dark:text-gray-300">Remember me</label>
64
+ </div>
65
+
66
+ <component
67
+ v-for="c in coreStore?.config?.loginPageInjections?.underInputs || []"
68
+ :is="getCustomComponent(c)"
69
+ :meta="c.meta"
70
+ />
71
+
59
72
  <div v-if="error" class="flex items-center p-4 mb-4 text-sm text-red-800 rounded-lg bg-red-50 dark:bg-gray-800 dark:text-red-400" role="alert">
60
73
  <svg class="flex-shrink-0 inline w-4 h-4 me-3" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 20 20">
61
74
  <path d="M10 .5a9.5 9.5 0 1 0 9.5 9.5A9.51 9.51 0 0 0 10 .5ZM9.5 4a1.5 1.5 0 1 1 0 3 1.5 1.5 0 0 1 0-3ZM12 15H8a1 1 0 0 1 0-2h1v-3H8a1 1 0 0 1 0-2h2a1 1 0 0 1 1 1v4h1a1 1 0 0 1 0 2Z"/>
@@ -66,6 +79,7 @@
66
79
  </div>
67
80
  </div>
68
81
 
82
+
69
83
 
70
84
  <div v-if="coreStore.config?.loginPromptHTML"
71
85
  class="flex items-center p-4 mb-4 text-sm text-gray-800 rounded-lg bg-gray-50 dark:bg-gray-800 dark:text-gray-400" role="alert"
@@ -101,6 +115,7 @@
101
115
 
102
116
  <script setup>
103
117
 
118
+ import { getCustomComponent } from '@/utils';
104
119
  import { onMounted, ref, computed } from 'vue';
105
120
  import { useCoreStore } from '@/stores/core';
106
121
  import { useUserStore } from '@/stores/user';
@@ -1,4 +1,5 @@
1
1
  import type { Express } from 'express';
2
+ import OperationalResource from '../modules/operationalResource.js';
2
3
 
3
4
  export interface ICodeInjector {
4
5
  srcFoldersToSync: Object;
@@ -237,7 +238,7 @@ export interface IAdminForthDataSourceConnectorConstructor {
237
238
  }
238
239
 
239
240
  export interface IAdminForthAuth {
240
- verify(jwt : string, mustHaveType: string): Promise<any>;
241
+ verify(jwt : string, mustHaveType: string, decodeUser?: boolean): Promise<any>;
241
242
  issueJWT(payload: Object, type: string): string;
242
243
 
243
244
  removeCustomCookie({response, name}: {response: any, name: string}): void;
@@ -291,6 +292,11 @@ export interface IAdminForth {
291
292
  */
292
293
  bundleNow({ hotReload, verbose }: { hotReload: boolean, verbose: boolean }): Promise<void>;
293
294
 
295
+ /**
296
+ * Resource to get access to operational resources for data api fetching and manipulation.
297
+ */
298
+ resource(resourceId: string): IOperationalResource;
299
+
294
300
  /**
295
301
  * This method will be automatically called from AdminForth HTTP adapter to serve AdminForth SPA.
296
302
  */
@@ -738,13 +744,17 @@ export type AdminForthBulkAction = {
738
744
  * Callback which will be called on backend when user clicks on action button.
739
745
  * It should return Promise which will be resolved when action is done.
740
746
  */
741
- action: ({ resource, selectedIds, adminUser }: { resource: AdminForthResource, selectedIds: Array<any>, adminUser: AdminUser }) => Promise<{ ok: boolean, error?: string }>,
747
+ action: ({ resource, selectedIds, adminUser }: { resource: AdminForthResource, selectedIds: Array<any>, adminUser: AdminUser }) => Promise<{ ok: boolean, error?: string, successMessage?: string }>,
742
748
 
743
749
  /**
744
750
  * Confirmation message which will be displayed to user before action is executed.
745
751
  */
746
752
  confirm?: string,
747
753
 
754
+ /**
755
+ * Success message which will be displayed to user after action is executed.
756
+ */
757
+ successMessage?: string,
748
758
  /**
749
759
  * Allowed callback called to check whether action is allowed for user.
750
760
  * 1. It called first time when user goes to list view. If callback returns false, action button will be hidden on list view.
@@ -1326,6 +1336,15 @@ export type AdminForthConfig = {
1326
1336
  * Execution is done on admin app load.
1327
1337
  */
1328
1338
  announcementBadge?: (user: AdminUser) => { text?: string, html?: string, closable?: boolean, title?: string } | null,
1339
+
1340
+ /**
1341
+ * Custom panel components or array of components which will be displayed in the login form
1342
+ * right after the inputs. Use it to add custom authorization methods like social login or other custom fields e.g. 'reset'
1343
+ * password link.
1344
+ */
1345
+ loginPageInjections?: {
1346
+ underInputs?: AdminForthComponentDeclaration | Array<AdminForthComponentDeclaration>,
1347
+ }
1329
1348
  }
1330
1349
 
1331
1350
  /**