adminforth 1.0.56 → 1.0.58

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/dist/index.js CHANGED
@@ -33,6 +33,8 @@ class AdminForth {
33
33
  });
34
34
  this.config = Object.assign(Object.assign({}, __classPrivateFieldGet(this, _AdminForth_defaultConfig, "f")), config);
35
35
  this.validateConfig();
36
+ this.activatePlugins();
37
+ this.validateConfig(); // revalidate after plugins
36
38
  this.express = new ExpressServer(this);
37
39
  this.auth = new Auth();
38
40
  this.codeInjector = new CodeInjector(this);
@@ -40,6 +42,14 @@ class AdminForth {
40
42
  this.statuses = {};
41
43
  console.log(`🚀 AdminForth v${ADMINFORTH_VERSION} starting up`);
42
44
  }
45
+ activatePlugins() {
46
+ for (let resource of this.config.resources) {
47
+ for (let pluginInstance of resource.plugins || []) {
48
+ pluginInstance.modifyResourceConfig(this, resource);
49
+ }
50
+ }
51
+ ;
52
+ }
43
53
  validateConfig() {
44
54
  if (this.config.rootUser) {
45
55
  if (!this.config.rootUser.username) {
@@ -456,7 +466,8 @@ class AdminForth {
456
466
  if (!resource) {
457
467
  return { error: `Resource ${resourceId} not found` };
458
468
  }
459
- return { resource };
469
+ // exclude "plugins" key
470
+ return { resource: Object.assign(Object.assign({}, resource), { plugins: undefined }) };
460
471
  }),
461
472
  });
462
473
  server.endpoint({
@@ -91,7 +91,7 @@ class CodeInjector {
91
91
  routes += `{
92
92
  path: '${item.path}',
93
93
  name: '${item.path}',
94
- component: import('${item.component}'),
94
+ component: () => import('${item.component}'),
95
95
  },\n`;
96
96
  }
97
97
  if (item.children) {
@@ -0,0 +1,22 @@
1
+ import AdminForthPlugin from "../base.js";
2
+ export default class ForeignInlineListPlugin extends AdminForthPlugin {
3
+ constructor(options) {
4
+ super(options);
5
+ this.options = options;
6
+ console.log('ForeignInlineListPlugin', this);
7
+ }
8
+ modifyResourceConfig(adminforth, resourceConfig) {
9
+ super.modifyResourceConfig(adminforth, resourceConfig);
10
+ // get resource with foreignResourceId
11
+ this.foreignResource = adminforth.config.resources.find((resource) => resource.resourceId === this.options.foreignResourceId);
12
+ if (!this.foreignResource) {
13
+ throw new Error(`ForeignInlineListPlugin: Resource with ID "${this.options.foreignResourceId}" not found`);
14
+ }
15
+ resourceConfig.columns.push({
16
+ name: `foreignInlineList_${this.foreignResource.resourceId}`,
17
+ label: 'Foreign Inline List',
18
+ virtual: true,
19
+ showIn: ['show'],
20
+ });
21
+ }
22
+ }
@@ -0,0 +1,8 @@
1
+ export default class AdminForthPlugin {
2
+ constructor(pluginOptions) {
3
+ // set up plugin here
4
+ }
5
+ modifyResourceConfig(adminforth, resourceConfig) {
6
+ this.adminforth = adminforth;
7
+ }
8
+ }
@@ -107,7 +107,14 @@ class ExpressServer {
107
107
  }
108
108
  else {
109
109
  this.expressApp.get(`${slashedPrefix}assets/*`, (req, res) => {
110
- res.sendFile(path.join(CodeInjector.SPA_TMP_PATH, 'dist', replaceAtStart(req.url, prefix)));
110
+ res.sendFile(path.join(CodeInjector.SPA_TMP_PATH, 'dist', replaceAtStart(req.url, prefix)), {
111
+ cacheControl: false,
112
+ // store for a year
113
+ headers: {
114
+ 'Cache-Control': 'public, max-age=31536000',
115
+ 'Pragma': 'public',
116
+ }
117
+ });
111
118
  });
112
119
  this.expressApp.get(`${prefix}*`, (req, res) => __awaiter(this, void 0, void 0, function* () {
113
120
  const fullPath = path.join(CodeInjector.SPA_TMP_PATH, 'dist', 'index.html');
@@ -122,7 +129,15 @@ class ExpressServer {
122
129
  res.status(500).send(respondNoServer(`${this.adminforth.config.brandName} is still warming up`, 'Please wait a moment...'));
123
130
  return;
124
131
  }
125
- res.sendFile(fullPath);
132
+ res.sendFile(fullPath, {
133
+ cacheControl: false,
134
+ headers: {
135
+ 'Content-Type': 'text/html',
136
+ 'Cache-Control': 'no-cache, no-store, must-revalidate',
137
+ 'Pragma': 'no-cache',
138
+ 'Expires': '0'
139
+ }
140
+ });
126
141
  }));
127
142
  }
128
143
  }
@@ -55,10 +55,12 @@
55
55
 
56
56
 
57
57
  <aside
58
+
58
59
  v-if="loggedIn"
59
60
 
60
61
  id="logo-sidebar" class="fixed border-none top-0 left-0 z-40 w-64 h-screen transition-transform -translate-x-full bg-nav-menu-bg border-r border- sm:translate-x-0 dark:bg-gray-800 dark:border-gray-700" aria-label="Sidebar">
61
62
  <div class="h-full px-3 pb-4 overflow-y-auto bg-nav-menu-bg dark:bg-gray-800">
63
+
62
64
  <div class="flex ms-2 md:me-24 m-4 ">
63
65
  <img src="https://flowbite.com/docs/images/logo.svg" class="h-8 me-3" alt="FlowBite Logo" />
64
66
  <span class="self-center text-header-text-size font-semibold sm:text-header-text-size whitespace-nowrap dark:text-header-text text-header-logo-color">
@@ -71,17 +73,15 @@
71
73
  <div v-else-if="item.type === 'gap'" class="flex items-center justify-center h-8"></div>
72
74
  <div v-else-if="item.type === 'heading'" class="flex items-center justify-left pl-2 h-8 text-gray-400 dark:text-gray-400
73
75
  ">{{ item.label }}</div>
74
-
75
-
76
76
  <li v-else-if="item.children">
77
- <button type="button" class="flex items-center w-full p-2 text-base text-nav-menu-text rounded-default transition duration-75 group hover:bg-nav-menu-bg-hover dark:text-white dark:hover:bg-gray-700"
77
+ <button @click="clickOnMenuItem(i)" type="button" class="flex items-center w-full p-2 text-base text-nav-menu-text rounded-default transition duration-75 group hover:bg-nav-menu-bg-hover dark:text-white dark:hover:bg-gray-700"
78
78
  :aria-controls="`dropdown-example${i}`"
79
79
  :data-collapse-toggle="`dropdown-example${i}`"
80
80
  >
81
81
  <component v-if="item.icon" :is="getIcon(item.icon)" class="w-5 h-5 text-gray-500 transition duration-75 dark:text-gray-400 dark:group-hover:text-white" ></component>
82
82
 
83
83
  <span class="flex-1 ms-3 text-left rtl:text-right whitespace-nowrap">{{ item.label }}</span>
84
- <svg class="w-3 h-3" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 10 6">
84
+ <svg :class="{'rotate-180': opened.includes(i) }" class="w-3 h-3" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 10 6">
85
85
  <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m1 1 4 4 4-4"/>
86
86
  </svg>
87
87
  </button>
@@ -144,6 +144,8 @@ createHead()
144
144
  const route = useRoute();
145
145
  const router = useRouter();
146
146
  const title = ref('');
147
+ //create a ref to store the opened menu items with ts type;
148
+ const opened = ref<string[]>([]);
147
149
 
148
150
 
149
151
  const routerIsReady = ref(false);
@@ -157,6 +159,15 @@ function toggleTheme() {
157
159
  document.documentElement.classList.toggle('dark');
158
160
  }
159
161
 
162
+ function clickOnMenuItem (label: string) {
163
+ if (opened.value.includes(label)) {
164
+ opened.value = opened.value.filter((item) => item !== label);
165
+ } else {
166
+ opened.value.push(label);
167
+ }
168
+
169
+ }
170
+
160
171
  async function logout() {
161
172
  await coreStore.logout();
162
173
  router.push({ name: 'login' });
@@ -186,6 +197,11 @@ onMounted(async () => {
186
197
  ],
187
198
 
188
199
  })
200
+ coreStore.menu.forEach((item, i) => {
201
+ if (item.open) {
202
+ opened.value.push(i);
203
+ }
204
+ });
189
205
  })
190
206
 
191
207
  </script>
@@ -1,20 +1,9 @@
1
1
  import { createRouter, createWebHistory } from 'vue-router'
2
2
  import ResourceParent from '@/views/ResourceParent.vue'
3
- import ListView from '@/views/ListView.vue'
4
- import ShowView from '@/views/ShowView.vue'
5
- import EditView from '@/views/EditView.vue'
6
- import CreateView from '@/views/CreateView.vue'
7
3
 
8
4
  const router = createRouter({
9
5
  history: createWebHistory(import.meta.env.BASE_URL),
10
6
  routes: [
11
- {
12
- path: '/',
13
- name: 'home',
14
- //redirect to login
15
- /* IMPORTANT:ADMINFORTH REDIRECT */
16
-
17
- },
18
7
  {
19
8
  path: '/login',
20
9
  name: 'login',
@@ -27,22 +16,22 @@ const router = createRouter({
27
16
  children: [
28
17
  {
29
18
  path: '',
30
- component: ListView,
19
+ component: () => import('@/views/ListView.vue'),
31
20
  name: 'resource-list'
32
21
  },
33
22
  {
34
23
  path: 'show/:primaryKey',
35
- component: ShowView,
24
+ component: () => import('@/views/ShowView.vue'),
36
25
  name: 'resource-show'
37
26
  },
38
27
  {
39
28
  path: 'edit/:primaryKey',
40
- component: EditView,
29
+ component: () => import('@/views/EditView.vue'),
41
30
  name: 'resource-edit'
42
31
  },
43
32
  {
44
33
  path: 'create',
45
- component: CreateView,
34
+ component: () => import('@/views/CreateView.vue'),
46
35
  name: 'resource-create'
47
36
  },
48
37
  ]
package/index.ts CHANGED
@@ -51,15 +51,27 @@ class AdminForth {
51
51
 
52
52
  constructor(config: AdminForthConfig) {
53
53
  this.config = {...this.#defaultConfig,...config};
54
+
54
55
  this.validateConfig();
56
+ this.activatePlugins();
57
+ this.validateConfig(); // revalidate after plugins
58
+
55
59
  this.express = new ExpressServer(this);
56
60
  this.auth = new Auth();
57
61
  this.codeInjector = new CodeInjector(this);
58
62
  this.connectors = {};
59
- this.statuses = {}
63
+ this.statuses = {};
60
64
  console.log(`🚀 AdminForth v${ADMINFORTH_VERSION} starting up`)
61
65
  }
62
66
 
67
+ activatePlugins() {
68
+ for (let resource of this.config.resources) {
69
+ for (let pluginInstance of resource.plugins || []) {
70
+ pluginInstance.modifyResourceConfig(this, resource);
71
+ }
72
+ };
73
+ }
74
+
63
75
  validateConfig() {
64
76
  if (this.config.rootUser) {
65
77
  if (!this.config.rootUser.username) {
@@ -527,7 +539,8 @@ class AdminForth {
527
539
  if (!resource) {
528
540
  return { error: `Resource ${resourceId} not found` };
529
541
  }
530
- return { resource };
542
+ // exclude "plugins" key
543
+ return { resource: { ...resource, plugins: undefined } };
531
544
  },
532
545
  });
533
546
  server.endpoint({
@@ -100,7 +100,7 @@ class CodeInjector {
100
100
  routes += `{
101
101
  path: '${item.path}',
102
102
  name: '${item.path}',
103
- component: import('${item.component}'),
103
+ component: () => import('${item.component}'),
104
104
  },\n`
105
105
  }
106
106
  if (item.children) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "adminforth",
3
- "version": "1.0.56",
3
+ "version": "1.0.58",
4
4
  "description": "OpenSource Vue3 powered forth-generation admin panel",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -0,0 +1,35 @@
1
+ import { AdminForthResource } from "../../types/AdminForthConfig.js";
2
+ import AdminForthPlugin from "../base.js";
3
+ import AdminForth from "../../index.js";
4
+
5
+ type PluginOptions = {
6
+ foreignResourceId: string;
7
+ }
8
+
9
+ export default class ForeignInlineListPlugin extends AdminForthPlugin {
10
+ foreignResource: AdminForthResource;
11
+ options: PluginOptions;
12
+
13
+ constructor(options: PluginOptions) {
14
+ super(options);
15
+ this.options = options;
16
+ console.log('ForeignInlineListPlugin', this);
17
+ }
18
+
19
+ modifyResourceConfig(adminforth: AdminForth, resourceConfig: AdminForthResource) {
20
+ super.modifyResourceConfig(adminforth, resourceConfig);
21
+
22
+ // get resource with foreignResourceId
23
+ this.foreignResource = adminforth.config.resources.find((resource) => resource.resourceId === this.options.foreignResourceId);
24
+ if (!this.foreignResource) {
25
+ throw new Error(`ForeignInlineListPlugin: Resource with ID "${this.options.foreignResourceId}" not found`);
26
+ }
27
+
28
+ resourceConfig.columns.push({
29
+ name: `foreignInlineList_${this.foreignResource.resourceId}`,
30
+ label: 'Foreign Inline List',
31
+ virtual: true,
32
+ showIn: ['show'],
33
+ });
34
+ }
35
+ }
@@ -0,0 +1,16 @@
1
+ import { AdminForthResource } from '../types/AdminForthConfig.js';
2
+ import AdminForth from '../index.js';
3
+
4
+ export default class AdminForthPlugin {
5
+
6
+ adminforth: AdminForth;
7
+
8
+ constructor(pluginOptions: any) {
9
+ // set up plugin here
10
+ }
11
+
12
+ modifyResourceConfig(adminforth: AdminForth, resourceConfig: AdminForthResource) {
13
+ this.adminforth = adminforth;
14
+ }
15
+
16
+ }
@@ -112,7 +112,17 @@ class ExpressServer {
112
112
 
113
113
  } else {
114
114
  this.expressApp.get(`${slashedPrefix}assets/*`, (req, res) => {
115
- res.sendFile(path.join(CodeInjector.SPA_TMP_PATH, 'dist', replaceAtStart(req.url, prefix)))
115
+ res.sendFile(
116
+ path.join(CodeInjector.SPA_TMP_PATH, 'dist', replaceAtStart(req.url, prefix)),
117
+ {
118
+ cacheControl: false,
119
+ // store for a year
120
+ headers: {
121
+ 'Cache-Control': 'public, max-age=31536000',
122
+ 'Pragma': 'public',
123
+ }
124
+ }
125
+ )
116
126
  })
117
127
 
118
128
  this.expressApp.get(`${prefix}*`, async (req, res) => {
@@ -128,7 +138,14 @@ class ExpressServer {
128
138
  res.status(500).send(respondNoServer(`${this.adminforth.config.brandName} is still warming up`, 'Please wait a moment...'));
129
139
  return;
130
140
  }
131
- res.sendFile(fullPath);
141
+ res.sendFile(fullPath, {
142
+ cacheControl: false,
143
+ headers: {
144
+ 'Content-Type': 'text/html',
145
+ 'Cache-Control': 'no-cache, no-store, must-revalidate',
146
+ 'Pragma': 'no-cache',
147
+ 'Expires': '0'
148
+ } });
132
149
  });
133
150
  }
134
151
  }
package/spa/src/App.vue CHANGED
@@ -55,10 +55,12 @@
55
55
 
56
56
 
57
57
  <aside
58
+
58
59
  v-if="loggedIn"
59
60
 
60
61
  id="logo-sidebar" class="fixed border-none top-0 left-0 z-40 w-64 h-screen transition-transform -translate-x-full bg-nav-menu-bg border-r border- sm:translate-x-0 dark:bg-gray-800 dark:border-gray-700" aria-label="Sidebar">
61
62
  <div class="h-full px-3 pb-4 overflow-y-auto bg-nav-menu-bg dark:bg-gray-800">
63
+
62
64
  <div class="flex ms-2 md:me-24 m-4 ">
63
65
  <img src="https://flowbite.com/docs/images/logo.svg" class="h-8 me-3" alt="FlowBite Logo" />
64
66
  <span class="self-center text-header-text-size font-semibold sm:text-header-text-size whitespace-nowrap dark:text-header-text text-header-logo-color">
@@ -71,17 +73,15 @@
71
73
  <div v-else-if="item.type === 'gap'" class="flex items-center justify-center h-8"></div>
72
74
  <div v-else-if="item.type === 'heading'" class="flex items-center justify-left pl-2 h-8 text-gray-400 dark:text-gray-400
73
75
  ">{{ item.label }}</div>
74
-
75
-
76
76
  <li v-else-if="item.children">
77
- <button type="button" class="flex items-center w-full p-2 text-base text-nav-menu-text rounded-default transition duration-75 group hover:bg-nav-menu-bg-hover dark:text-white dark:hover:bg-gray-700"
77
+ <button @click="clickOnMenuItem(i)" type="button" class="flex items-center w-full p-2 text-base text-nav-menu-text rounded-default transition duration-75 group hover:bg-nav-menu-bg-hover dark:text-white dark:hover:bg-gray-700"
78
78
  :aria-controls="`dropdown-example${i}`"
79
79
  :data-collapse-toggle="`dropdown-example${i}`"
80
80
  >
81
81
  <component v-if="item.icon" :is="getIcon(item.icon)" class="w-5 h-5 text-gray-500 transition duration-75 dark:text-gray-400 dark:group-hover:text-white" ></component>
82
82
 
83
83
  <span class="flex-1 ms-3 text-left rtl:text-right whitespace-nowrap">{{ item.label }}</span>
84
- <svg class="w-3 h-3" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 10 6">
84
+ <svg :class="{'rotate-180': opened.includes(i) }" class="w-3 h-3" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 10 6">
85
85
  <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m1 1 4 4 4-4"/>
86
86
  </svg>
87
87
  </button>
@@ -144,6 +144,8 @@ createHead()
144
144
  const route = useRoute();
145
145
  const router = useRouter();
146
146
  const title = ref('');
147
+ //create a ref to store the opened menu items with ts type;
148
+ const opened = ref<string[]>([]);
147
149
 
148
150
 
149
151
  const routerIsReady = ref(false);
@@ -157,6 +159,15 @@ function toggleTheme() {
157
159
  document.documentElement.classList.toggle('dark');
158
160
  }
159
161
 
162
+ function clickOnMenuItem (label: string) {
163
+ if (opened.value.includes(label)) {
164
+ opened.value = opened.value.filter((item) => item !== label);
165
+ } else {
166
+ opened.value.push(label);
167
+ }
168
+
169
+ }
170
+
160
171
  async function logout() {
161
172
  await coreStore.logout();
162
173
  router.push({ name: 'login' });
@@ -186,6 +197,11 @@ onMounted(async () => {
186
197
  ],
187
198
 
188
199
  })
200
+ coreStore.menu.forEach((item, i) => {
201
+ if (item.open) {
202
+ opened.value.push(i);
203
+ }
204
+ });
189
205
  })
190
206
 
191
207
  </script>
@@ -1,20 +1,9 @@
1
1
  import { createRouter, createWebHistory } from 'vue-router'
2
2
  import ResourceParent from '@/views/ResourceParent.vue'
3
- import ListView from '@/views/ListView.vue'
4
- import ShowView from '@/views/ShowView.vue'
5
- import EditView from '@/views/EditView.vue'
6
- import CreateView from '@/views/CreateView.vue'
7
3
 
8
4
  const router = createRouter({
9
5
  history: createWebHistory(import.meta.env.BASE_URL),
10
6
  routes: [
11
- {
12
- path: '/',
13
- name: 'home',
14
- //redirect to login
15
- /* IMPORTANT:ADMINFORTH REDIRECT */
16
-
17
- },
18
7
  {
19
8
  path: '/login',
20
9
  name: 'login',
@@ -27,22 +16,22 @@ const router = createRouter({
27
16
  children: [
28
17
  {
29
18
  path: '',
30
- component: ListView,
19
+ component: () => import('@/views/ListView.vue'),
31
20
  name: 'resource-list'
32
21
  },
33
22
  {
34
23
  path: 'show/:primaryKey',
35
- component: ShowView,
24
+ component: () => import('@/views/ShowView.vue'),
36
25
  name: 'resource-show'
37
26
  },
38
27
  {
39
28
  path: 'edit/:primaryKey',
40
- component: EditView,
29
+ component: () => import('@/views/EditView.vue'),
41
30
  name: 'resource-edit'
42
31
  },
43
32
  {
44
33
  path: 'create',
45
- component: CreateView,
34
+ component: () => import('@/views/CreateView.vue'),
46
35
  name: 'resource-create'
47
36
  },
48
37
  ]
@@ -1,4 +1,4 @@
1
-
1
+ import AdminForthPlugin from '../plugins/base.js';
2
2
 
3
3
  export type AdminForthConfigMenuItem = {
4
4
  type?: 'heading' | 'group' | 'resource' | 'page' | 'gap' | 'divider',
@@ -8,6 +8,7 @@ export type AdminForthConfigMenuItem = {
8
8
  component?: string,
9
9
  resourceId?: string,
10
10
  homepage?: boolean,
11
+ open?: boolean,
11
12
  children?: Array<AdminForthConfigMenuItem>,
12
13
  }
13
14
 
@@ -44,39 +45,39 @@ export type AdminForthResource = {
44
45
  dataSource: string,
45
46
  columns: Array<AdminForthResourceColumn>,
46
47
  itemLabel?: Function,
48
+ plugins?: Array<AdminForthPlugin>,
47
49
  hooks?: {
48
- show?: {
49
- beforeDatasourceRequest?: Function,
50
- afterDatasourceResponse?: Function,
51
- },
52
- list?: {
53
- beforeDatasourceRequest?: Function,
54
- afterDatasourceResponse?: Function,
55
- },
56
- create?: {
57
- beforeSave?: Function,
58
- afterSave?: Function,
59
- },
60
- edit?: {
61
- beforeSave?: Function,
62
- afterSave?: Function,
63
- },
64
- delete?: {
65
- beforeSave?: Function,
66
- afterSave?: Function,
67
- },
50
+ show?: {
51
+ beforeDatasourceRequest?: Function,
52
+ afterDatasourceResponse?: Function,
53
+ },
54
+ list?: {
55
+ beforeDatasourceRequest?: Function,
56
+ afterDatasourceResponse?: Function,
57
+ },
58
+ create?: {
59
+ beforeSave?: Function,
60
+ afterSave?: Function,
61
+ },
62
+ edit?: {
63
+ beforeSave?: Function,
64
+ afterSave?: Function,
65
+ },
66
+ delete?: {
67
+ beforeSave?: Function,
68
+ afterSave?: Function,
69
+ },
68
70
  },
69
71
  options?: {
70
- bulkActions?: Array<{
71
- label: string,
72
- state: string,
73
- icon: string,
74
- action: Function,
75
- id?: string,
76
- }>,
77
- allowedActions?: AllowedActions,
78
- allowDelete?: boolean,
79
-
72
+ bulkActions?: Array<{
73
+ label: string,
74
+ state: string,
75
+ icon: string,
76
+ action: Function,
77
+ id?: string,
78
+ }>,
79
+ allowedActions?: AllowedActions,
80
+ allowDelete?: boolean,
80
81
  },
81
82
  }
82
83