adminforth 1.0.58 → 1.0.59

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
@@ -32,12 +32,12 @@ class AdminForth {
32
32
  deleteConfirmation: true,
33
33
  });
34
34
  this.config = Object.assign(Object.assign({}, __classPrivateFieldGet(this, _AdminForth_defaultConfig, "f")), config);
35
+ this.codeInjector = new CodeInjector(this);
35
36
  this.validateConfig();
36
37
  this.activatePlugins();
37
38
  this.validateConfig(); // revalidate after plugins
38
39
  this.express = new ExpressServer(this);
39
40
  this.auth = new Auth();
40
- this.codeInjector = new CodeInjector(this);
41
41
  this.connectors = {};
42
42
  this.statuses = {};
43
43
  console.log(`🚀 AdminForth v${ADMINFORTH_VERSION} starting up`);
@@ -251,7 +251,11 @@ class AdminForth {
251
251
  for (const column of resource.columns) {
252
252
  if (column.component) {
253
253
  for (const [key, value] of Object.entries(column.component)) {
254
- // console.log(`${key}: ${value}`);
254
+ if (this.codeInjector.allComponentNames[value]) {
255
+ // not obvious, but if we are in this if, it means that this is plugin component
256
+ // and there is no sense to check if it exists in users folder
257
+ continue;
258
+ }
255
259
  const path = value.replace('@@', this.config.customization.customComponentsDir);
256
260
  if (!fs.existsSync(path)) {
257
261
  throw new Error(`Component file ${path} does not exist`);
@@ -30,6 +30,8 @@ function hashify(obj) {
30
30
  }
31
31
  class CodeInjector {
32
32
  constructor(adminforth) {
33
+ this.allComponentNames = {};
34
+ this.srcFoldersToSync = {};
33
35
  this.adminforth = adminforth;
34
36
  }
35
37
  // async runShell({command, verbose = false}) {
@@ -126,7 +128,14 @@ class CodeInjector {
126
128
  });
127
129
  // copy whole custom directory
128
130
  if ((_b = this.adminforth.config.customization) === null || _b === void 0 ? void 0 : _b.customComponentsDir) {
129
- yield fsExtra.copy(this.adminforth.config.customization.customComponentsDir, path.join(CodeInjector.SPA_TMP_PATH, 'src', 'custom'), {
131
+ this.srcFoldersToSync[this.adminforth.config.customization.customComponentsDir] = './';
132
+ }
133
+ for (const [src, dest] of Object.entries(this.srcFoldersToSync)) {
134
+ const to = path.join(CodeInjector.SPA_TMP_PATH, 'src', 'custom', dest);
135
+ if (process.env.HEAVY_DEBUG) {
136
+ console.log(`🪲 await fsExtra.copy from ${src}, ${to}`);
137
+ }
138
+ yield fsExtra.copy(src, to, {
130
139
  recursive: true,
131
140
  });
132
141
  }
@@ -169,11 +178,14 @@ class CodeInjector {
169
178
  }
170
179
  });
171
180
  });
172
- let customComponentsImports = '';
173
181
  customResourceComponents.forEach((filePath) => {
174
182
  const componentName = getComponentNameFromPath(filePath);
175
- customComponentsImports += `import ${componentName} from '${filePath}';\n`;
183
+ this.allComponentNames[filePath] = componentName;
176
184
  });
185
+ let customComponentsImports = '';
186
+ for (const [targetPath, component] of Object.entries(this.allComponentNames)) {
187
+ customComponentsImports += `import ${component} from '${targetPath}';\n`;
188
+ }
177
189
  // Generate Vue.component statements for each icon
178
190
  const iconComponents = uniqueIcons.map((icon) => {
179
191
  const [collection, iconName] = icon.split(':');
@@ -184,10 +196,9 @@ class CodeInjector {
184
196
  }).join('\n');
185
197
  // Generate Vue.component statements for each custom component
186
198
  let customComponentsComponents = '';
187
- customResourceComponents.forEach((filePath) => {
188
- const componentName = getComponentNameFromPath(filePath);
189
- customComponentsComponents += `app.component('${componentName}', ${componentName});\n`;
190
- });
199
+ for (const name of Object.values(this.allComponentNames)) {
200
+ customComponentsComponents += `app.component('${name}', ${name});\n`;
201
+ }
191
202
  let imports = iconImports + '\n';
192
203
  imports += customComponentsImports + '\n';
193
204
  if ((_d = this.adminforth.config.customization) === null || _d === void 0 ? void 0 : _d.vueUsesFile) {
@@ -13,13 +13,16 @@ export function guessLabelFromName(name) {
13
13
  return name.split(/(?=[A-Z])/).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(' ');
14
14
  }
15
15
  }
16
- const __filename = fileURLToPath(import.meta.url);
17
- let __dirname = path.join(path.dirname(__filename), '..');
18
- if (__dirname.endsWith('dist')) {
19
- // in prod build we are in dist also
20
- __dirname = path.join(__dirname, '..');
21
- }
22
- export const ADMIN_FORTH_ABSOLUTE_PATH = __dirname;
16
+ export const currentFileDir = (metaUrl) => {
17
+ const __filename = fileURLToPath(metaUrl);
18
+ let __dirname = path.dirname(__filename);
19
+ if (__dirname.endsWith('dist')) {
20
+ // in prod build we are in dist also
21
+ __dirname = path.join(__dirname, '..');
22
+ }
23
+ return __dirname;
24
+ };
25
+ export const ADMIN_FORTH_ABSOLUTE_PATH = path.join(currentFileDir(import.meta.url), '..');
23
26
  const package_json = JSON.parse(fs.readFileSync(path.join(ADMIN_FORTH_ABSOLUTE_PATH, 'package.json'), 'utf8'));
24
27
  export const ADMINFORTH_VERSION = package_json.version;
25
28
  export function getComponentNameFromPath(filePath) {
@@ -1,9 +1,8 @@
1
1
  import AdminForthPlugin from "../base.js";
2
2
  export default class ForeignInlineListPlugin extends AdminForthPlugin {
3
3
  constructor(options) {
4
- super(options);
4
+ super(options, import.meta.url);
5
5
  this.options = options;
6
- console.log('ForeignInlineListPlugin', this);
7
6
  }
8
7
  modifyResourceConfig(adminforth, resourceConfig) {
9
8
  super.modifyResourceConfig(adminforth, resourceConfig);
@@ -17,6 +16,9 @@ export default class ForeignInlineListPlugin extends AdminForthPlugin {
17
16
  label: 'Foreign Inline List',
18
17
  virtual: true,
19
18
  showIn: ['show'],
19
+ component: {
20
+ show: this.componentPath('InlineList.vue'),
21
+ },
20
22
  });
21
23
  }
22
24
  }
@@ -1,8 +1,30 @@
1
+ import { getComponentNameFromPath } from '../modules/utils.js';
2
+ import { currentFileDir } from '../modules/utils.js';
3
+ import path from 'path';
4
+ import fs from 'fs';
1
5
  export default class AdminForthPlugin {
2
- constructor(pluginOptions) {
6
+ constructor(pluginOptions, metaUrl) {
7
+ this.customFolderName = 'custom';
3
8
  // set up plugin here
9
+ this.pluginDir = currentFileDir(metaUrl);
4
10
  }
5
11
  modifyResourceConfig(adminforth, resourceConfig) {
6
12
  this.adminforth = adminforth;
7
13
  }
14
+ componentPath(componentFile) {
15
+ const key = `@@/plugins/${this.constructor.name}/${componentFile}`;
16
+ const componentName = getComponentNameFromPath(key);
17
+ const absSrcFolder = path.join(this.pluginDir, this.customFolderName);
18
+ if (!this.adminforth.codeInjector.srcFoldersToSync[absSrcFolder]) {
19
+ this.adminforth.codeInjector.srcFoldersToSync[absSrcFolder] = `./plugins/${this.constructor.name}/`;
20
+ }
21
+ if (!this.adminforth.codeInjector.allComponentNames[key]) {
22
+ const absSrcPath = path.join(absSrcFolder, componentFile);
23
+ if (!fs.existsSync(absSrcPath)) {
24
+ throw new Error(`Plugin "${this.constructor.name}" tried to use file as component which does not exist at "${absSrcPath}"`);
25
+ }
26
+ this.adminforth.codeInjector.allComponentNames[key] = componentName;
27
+ }
28
+ return key;
29
+ }
8
30
  }
@@ -1,7 +1,7 @@
1
1
  <template>
2
2
  <nav
3
- v-if="loggedIn"
4
- class="fixed h-14 top-0 z-40 w-full border-b shadow-sm bg-header-bg shadow-header-shadow dark:bg-header-bg-hover dark:border-nav-menu-devider">
3
+ v-if="loggedIn && routerIsReady && loginRedirectCheckIsReady"
4
+ class="fixed h-14 top-0 z-20 w-full border-b shadow-sm bg-header-bg shadow-header-shadow dark:bg-header-bg-hover dark:border-nav-menu-devider">
5
5
  <div class="px-3 py-3 lg:px-5 lg:pl-3">
6
6
  <div class="flex items-center justify-between">
7
7
  <div class="flex items-center justify-start rtl:justify-end">
@@ -56,7 +56,7 @@
56
56
 
57
57
  <aside
58
58
 
59
- v-if="loggedIn"
59
+ v-if="loggedIn && routerIsReady && loginRedirectCheckIsReady"
60
60
 
61
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">
62
62
  <div class="h-full px-3 pb-4 overflow-y-auto bg-nav-menu-bg dark:bg-gray-800">
@@ -102,16 +102,22 @@
102
102
  </div>
103
103
  </aside>
104
104
 
105
- <div class="p-4 sm:ml-64 dark:bg-gray-800" v-if="loggedIn">
105
+ <div class="p-4 sm:ml-64 dark:bg-gray-800" v-if="loggedIn && routerIsReady && loginRedirectCheckIsReady">
106
106
  <div class="p-0 dark:border-gray-700 mt-14">
107
107
  <RouterView/>
108
108
  </div>
109
109
  </div>
110
110
 
111
- <div v-else-if="routerIsReady">
111
+ <div v-else-if="routerIsReady && loginRedirectCheckIsReady">
112
112
  <RouterView/>
113
113
  </div>
114
+ <div v-else class="flex items-center justify-center h-screen">
115
+ <div class="text-3xl text-gray-400 animate-bounce">
116
+ ▶️
117
+ </div>
118
+ </div>
114
119
  <AcceptModal />
120
+
115
121
  </template>
116
122
 
117
123
  <style scoped>
@@ -149,8 +155,9 @@ const opened = ref<string[]>([]);
149
155
 
150
156
 
151
157
  const routerIsReady = ref(false);
158
+ const loginRedirectCheckIsReady = ref(false);
152
159
 
153
- const loggedIn = computed(() => route.name !== 'login' && routerIsReady.value);
160
+ const loggedIn = computed(() => route.name !== 'login');
154
161
 
155
162
  const theme = ref('light');
156
163
 
@@ -186,6 +193,7 @@ onMounted(async () => {
186
193
  initRouter();
187
194
  initFlowbite();
188
195
  await coreStore.fetchMenuAndResource();
196
+ loginRedirectCheckIsReady.value = true;
189
197
  title.value = coreStore.config.title || 'Admin Forth';
190
198
  useHead({
191
199
  title: title.value,
@@ -2,11 +2,11 @@
2
2
  <!-- drawer component -->
3
3
  <div id="drawer-navigation"
4
4
 
5
- class="fixed top-14 right-0 z-50 p-4 overflow-y-auto transition-transform translate-x-full bg-white w-80 dark:bg-gray-800 shadow-xl dark:shadow-black"
5
+ class="fixed right-0 z-50 p-4 overflow-y-auto transition-transform translate-x-full bg-white w-80 dark:bg-gray-800 shadow-xl dark:shadow-black"
6
6
 
7
7
  :class="show ? 'top-0 transform-none' : ''"
8
8
  tabindex="-1" aria-labelledby="drawer-navigation-label"
9
- :style="{ height: `calc(100vh - 3.5rem)` }"
9
+ :style="{ height: `calc(100vh ` }"
10
10
  >
11
11
  <h5 id="drawer-navigation-label" class="text-base font-semibold text-gray-500 uppercase dark:text-gray-400">
12
12
  Filters
@@ -92,7 +92,7 @@
92
92
  </div>
93
93
  </div>
94
94
 
95
- <div v-if="show" drawer-backdrop="" class="bg-gray-900/50 dark:bg-gray-900/80 fixed inset-0 z-30"
95
+ <div v-if="show" class="bg-gray-900/50 dark:bg-gray-900/80 fixed inset-0 z-30"
96
96
  @click="$emit('hide')">
97
97
  </div>
98
98
  </template>
@@ -0,0 +1,14 @@
1
+ <template>
2
+ <td class="px-6 py-4 whitespace-nowrap">
3
+ {{ column.label }}
4
+ </td>
5
+ <td class="px-6 py-4 whitespace-nowrap whitespace-pre-wrap">
6
+ <ValueRenderer :column="column" :row="row" />
7
+ </td>
8
+ </template>
9
+
10
+ <script setup>
11
+ import ValueRenderer from '@/components/ValueRenderer.vue';
12
+
13
+ defineProps(['column', 'row']);
14
+ </script>
@@ -40,4 +40,5 @@ const router = createRouter({
40
40
  ]
41
41
  })
42
42
 
43
+
43
44
  export default router
@@ -11,12 +11,16 @@ export const useCoreStore = defineStore('core', () => {
11
11
  const resourceColumnsError = ref('');
12
12
  const resourceColumnsId = ref(null);
13
13
  const user = ref(null);
14
+ const flowBitIsInitialised = ref(false);
14
15
 
15
16
  async function fetchMenuAndResource() {
16
17
  const resp = await callAdminForthApi({
17
18
  path: '/get_base_config',
18
19
  method: 'GET',
19
20
  });
21
+ if(!resp){
22
+ return
23
+ }
20
24
  menu.value = resp.menu;
21
25
  resourceById.value = resp.resources.reduce((acc, resource) => {
22
26
  acc[resource.resourceId] = resource;
@@ -122,6 +126,7 @@ export const useCoreStore = defineStore('core', () => {
122
126
  resourceColumns,
123
127
  fetchColumns,
124
128
  resourceColumnsError,
129
+ flowBitIsInitialised,
125
130
  logout
126
131
  }
127
132
  })
@@ -448,10 +448,10 @@ fetchStatus.value.pending = false;
448
448
  allowedActions.value = data.options?.allowedActions;
449
449
 
450
450
 
451
- setTimeout(() => {
452
- initFlowbite();
453
- console.log('initFlowbite');
454
- });
451
+ // setTimeout(() => {
452
+ // initFlowbite();
453
+ // console.log('initFlowbite');
454
+ // });
455
455
  }
456
456
 
457
457
 
@@ -512,6 +512,7 @@ function addToCheckedValues(id) {
512
512
  }
513
513
 
514
514
  async function init() {
515
+
515
516
  await coreStore.fetchColumns({
516
517
  resourceId: route.params.resourceId
517
518
  });
@@ -527,7 +528,13 @@ async function init() {
527
528
  }
528
529
 
529
530
  onMounted(async () => {
531
+ if (!coreStore.flowBitIsInitialised){
532
+ initFlowbite();
533
+ coreStore.flowBitIsInitialised = true;
534
+ }
535
+
530
536
  await init();
537
+
531
538
  });
532
539
 
533
540
  // on route param change
@@ -44,18 +44,12 @@
44
44
  <tr v-for="column in coreStore.resourceColumns?.filter(c => c.showIn.includes('show'))" :key="column.name"
45
45
  class="bg-form-view-bg odd:dark:bg-gray-900 even:dark:bg-gray-800 border-b dark:border-gray-700"
46
46
  >
47
- <td class="px-6 py-4 whitespace-nowrap">
48
- {{ column.label }}
49
- </td>
50
- <td class="px-6 py-4 whitespace-nowrap whitespace-pre-wrap">
51
- <component
52
- :is="showComponentsPerColumn[column.name] || ValueRenderer"
53
- :column="column"
54
- :row="coreStore.record"
55
- />
56
- </td>
47
+ <component
48
+ :is="showComponentsPerColumn[column.name] || ShowTableItem"
49
+ :column="column"
50
+ :row="coreStore.record"
51
+ />
57
52
  </tr>
58
-
59
53
  </tbody>
60
54
  </table>
61
55
  </div>
@@ -68,6 +62,8 @@
68
62
  <script setup>
69
63
 
70
64
  import BreadcrumbsWithButtons from '@/components/BreadcrumbsWithButtons.vue';
65
+ import ShowTableItem from '@/components/ShowTableItem.vue';
66
+
71
67
  import ValueRenderer from '@/components/ValueRenderer.vue';
72
68
  import { useCoreStore } from '@/stores/core';
73
69
  import { getCustomComponent } from '@/utils';
package/index.ts CHANGED
@@ -43,7 +43,6 @@ class AdminForth {
43
43
  connectorClasses: any;
44
44
  runningHotReload?: boolean;
45
45
 
46
-
47
46
  statuses: {
48
47
  dbDiscover?: 'running' | 'done',
49
48
  }
@@ -51,6 +50,7 @@ class AdminForth {
51
50
 
52
51
  constructor(config: AdminForthConfig) {
53
52
  this.config = {...this.#defaultConfig,...config};
53
+ this.codeInjector = new CodeInjector(this);
54
54
 
55
55
  this.validateConfig();
56
56
  this.activatePlugins();
@@ -58,7 +58,6 @@ class AdminForth {
58
58
 
59
59
  this.express = new ExpressServer(this);
60
60
  this.auth = new Auth();
61
- this.codeInjector = new CodeInjector(this);
62
61
  this.connectors = {};
63
62
  this.statuses = {};
64
63
  console.log(`🚀 AdminForth v${ADMINFORTH_VERSION} starting up`)
@@ -309,7 +308,11 @@ class AdminForth {
309
308
  for (const column of resource.columns) {
310
309
  if (column.component) {
311
310
  for (const [key, value] of Object.entries(column.component)) {
312
- // console.log(`${key}: ${value}`);
311
+ if (this.codeInjector.allComponentNames[value]) {
312
+ // not obvious, but if we are in this if, it means that this is plugin component
313
+ // and there is no sense to check if it exists in users folder
314
+ continue;
315
+ }
313
316
  const path = value.replace('@@', this.config.customization.customComponentsDir);
314
317
  if (!fs.existsSync(path)) {
315
318
  throw new Error(`Component file ${path} does not exist`);
@@ -30,6 +30,8 @@ function hashify(obj) {
30
30
  class CodeInjector {
31
31
 
32
32
  adminforth: AdminForth;
33
+ allComponentNames: { [key: string]: string } = {};
34
+ srcFoldersToSync: { [key: string]: string } = {};
33
35
 
34
36
  static SPA_TMP_PATH = path.join(TMP_DIR, 'adminforth', 'spa_tmp');
35
37
 
@@ -139,12 +141,23 @@ class CodeInjector {
139
141
 
140
142
  // copy whole custom directory
141
143
  if (this.adminforth.config.customization?.customComponentsDir) {
142
- await fsExtra.copy(this.adminforth.config.customization.customComponentsDir, path.join(CodeInjector.SPA_TMP_PATH, 'src', 'custom'), {
144
+ this.srcFoldersToSync[this.adminforth.config.customization.customComponentsDir] = './'
145
+ }
146
+
147
+ for (const [src, dest] of Object.entries(this.srcFoldersToSync)) {
148
+ const to = path.join(CodeInjector.SPA_TMP_PATH, 'src', 'custom', dest);
149
+ if (process.env.HEAVY_DEBUG) {
150
+ console.log(`🪲 await fsExtra.copy from ${src}, ${to}`);
151
+ }
152
+
153
+ await fsExtra.copy(src, to, {
143
154
  recursive: true,
144
155
  });
145
156
  }
146
157
  }
147
158
 
159
+
160
+
148
161
  //collect all 'icon' fields from resources bulkActions
149
162
  this.adminforth.config.resources.forEach((resource) => {
150
163
  if (resource.options?.bulkActions) {
@@ -189,12 +202,15 @@ class CodeInjector {
189
202
  });
190
203
  });
191
204
 
192
- let customComponentsImports = '';
193
205
  customResourceComponents.forEach((filePath) => {
194
206
  const componentName = getComponentNameFromPath(filePath);
195
- customComponentsImports += `import ${componentName} from '${filePath}';\n`;
207
+ this.allComponentNames[filePath] = componentName;
196
208
  });
197
-
209
+
210
+ let customComponentsImports = '';
211
+ for (const [targetPath, component] of Object.entries(this.allComponentNames)) {
212
+ customComponentsImports += `import ${component} from '${targetPath}';\n`;
213
+ }
198
214
 
199
215
 
200
216
  // Generate Vue.component statements for each icon
@@ -208,11 +224,9 @@ class CodeInjector {
208
224
 
209
225
  // Generate Vue.component statements for each custom component
210
226
  let customComponentsComponents = '';
211
- customResourceComponents.forEach((filePath) => {
212
- const componentName = getComponentNameFromPath(filePath);
213
- customComponentsComponents += `app.component('${componentName}', ${componentName});\n`;
214
- })
215
-
227
+ for (const name of Object.values(this.allComponentNames)) {
228
+ customComponentsComponents += `app.component('${name}', ${name});\n`;
229
+ }
216
230
 
217
231
  let imports = iconImports + '\n';
218
232
  imports += customComponentsImports + '\n';
package/modules/utils.ts CHANGED
@@ -15,14 +15,17 @@ export function guessLabelFromName(name) {
15
15
  }
16
16
 
17
17
 
18
- const __filename = fileURLToPath(import.meta.url);
19
- let __dirname = path.join(path.dirname(__filename), '..');
20
- if (__dirname.endsWith('dist')) {
21
- // in prod build we are in dist also
22
- __dirname = path.join(__dirname, '..');
18
+ export const currentFileDir = (metaUrl) => {
19
+ const __filename = fileURLToPath(metaUrl);
20
+ let __dirname = path.dirname(__filename);
21
+ if (__dirname.endsWith('dist')) {
22
+ // in prod build we are in dist also
23
+ __dirname = path.join(__dirname, '..');
24
+ }
25
+ return __dirname;
23
26
  }
24
27
 
25
- export const ADMIN_FORTH_ABSOLUTE_PATH = __dirname;
28
+ export const ADMIN_FORTH_ABSOLUTE_PATH = path.join(currentFileDir(import.meta.url), '..');
26
29
 
27
30
  const package_json = JSON.parse(fs.readFileSync(path.join(ADMIN_FORTH_ABSOLUTE_PATH, 'package.json'), 'utf8'));
28
31
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "adminforth",
3
- "version": "1.0.58",
3
+ "version": "1.0.59",
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,5 @@
1
+ <template>
2
+ <td colspan="2">
3
+ text
4
+ </td>
5
+ </template>
@@ -11,9 +11,10 @@ export default class ForeignInlineListPlugin extends AdminForthPlugin {
11
11
  options: PluginOptions;
12
12
 
13
13
  constructor(options: PluginOptions) {
14
- super(options);
14
+ super(options, import.meta.url);
15
+
15
16
  this.options = options;
16
- console.log('ForeignInlineListPlugin', this);
17
+
17
18
  }
18
19
 
19
20
  modifyResourceConfig(adminforth: AdminForth, resourceConfig: AdminForthResource) {
@@ -30,6 +31,9 @@ export default class ForeignInlineListPlugin extends AdminForthPlugin {
30
31
  label: 'Foreign Inline List',
31
32
  virtual: true,
32
33
  showIn: ['show'],
34
+ component: {
35
+ show: this.componentPath('InlineList.vue'),
36
+ },
33
37
  });
34
38
  }
35
39
  }
package/plugins/base.ts CHANGED
@@ -1,16 +1,44 @@
1
1
  import { AdminForthResource } from '../types/AdminForthConfig.js';
2
2
  import AdminForth from '../index.js';
3
+ import { getComponentNameFromPath } from '../modules/utils.js';
4
+ import { currentFileDir } from '../modules/utils.js';
5
+ import path from 'path';
6
+ import fs from 'fs';
3
7
 
4
8
  export default class AdminForthPlugin {
5
9
 
6
10
  adminforth: AdminForth;
11
+ pluginDir: string;
12
+ customFolderName: string = 'custom';
7
13
 
8
- constructor(pluginOptions: any) {
14
+ constructor(pluginOptions: any, metaUrl: string) {
9
15
  // set up plugin here
16
+ this.pluginDir = currentFileDir(metaUrl);
10
17
  }
11
18
 
12
19
  modifyResourceConfig(adminforth: AdminForth, resourceConfig: AdminForthResource) {
13
20
  this.adminforth = adminforth;
14
21
  }
15
22
 
23
+ componentPath(componentFile: string) {
24
+ const key = `@@/plugins/${this.constructor.name}/${componentFile}`;
25
+ const componentName = getComponentNameFromPath(key);
26
+
27
+ const absSrcFolder = path.join(this.pluginDir, this.customFolderName);
28
+
29
+ if (!this.adminforth.codeInjector.srcFoldersToSync[absSrcFolder]) {
30
+ this.adminforth.codeInjector.srcFoldersToSync[absSrcFolder] = `./plugins/${this.constructor.name}/`;
31
+ }
32
+
33
+ if (!this.adminforth.codeInjector.allComponentNames[key]) {
34
+ const absSrcPath = path.join(absSrcFolder, componentFile);
35
+ if (!fs.existsSync(absSrcPath)) {
36
+ throw new Error(`Plugin "${this.constructor.name}" tried to use file as component which does not exist at "${absSrcPath}"`);
37
+ }
38
+ this.adminforth.codeInjector.allComponentNames[key] = componentName;
39
+ }
40
+
41
+ return key;
42
+ }
43
+
16
44
  }
package/spa/src/App.vue CHANGED
@@ -1,7 +1,7 @@
1
1
  <template>
2
2
  <nav
3
- v-if="loggedIn"
4
- class="fixed h-14 top-0 z-40 w-full border-b shadow-sm bg-header-bg shadow-header-shadow dark:bg-header-bg-hover dark:border-nav-menu-devider">
3
+ v-if="loggedIn && routerIsReady && loginRedirectCheckIsReady"
4
+ class="fixed h-14 top-0 z-20 w-full border-b shadow-sm bg-header-bg shadow-header-shadow dark:bg-header-bg-hover dark:border-nav-menu-devider">
5
5
  <div class="px-3 py-3 lg:px-5 lg:pl-3">
6
6
  <div class="flex items-center justify-between">
7
7
  <div class="flex items-center justify-start rtl:justify-end">
@@ -56,7 +56,7 @@
56
56
 
57
57
  <aside
58
58
 
59
- v-if="loggedIn"
59
+ v-if="loggedIn && routerIsReady && loginRedirectCheckIsReady"
60
60
 
61
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">
62
62
  <div class="h-full px-3 pb-4 overflow-y-auto bg-nav-menu-bg dark:bg-gray-800">
@@ -102,16 +102,22 @@
102
102
  </div>
103
103
  </aside>
104
104
 
105
- <div class="p-4 sm:ml-64 dark:bg-gray-800" v-if="loggedIn">
105
+ <div class="p-4 sm:ml-64 dark:bg-gray-800" v-if="loggedIn && routerIsReady && loginRedirectCheckIsReady">
106
106
  <div class="p-0 dark:border-gray-700 mt-14">
107
107
  <RouterView/>
108
108
  </div>
109
109
  </div>
110
110
 
111
- <div v-else-if="routerIsReady">
111
+ <div v-else-if="routerIsReady && loginRedirectCheckIsReady">
112
112
  <RouterView/>
113
113
  </div>
114
+ <div v-else class="flex items-center justify-center h-screen">
115
+ <div class="text-3xl text-gray-400 animate-bounce">
116
+ ▶️
117
+ </div>
118
+ </div>
114
119
  <AcceptModal />
120
+
115
121
  </template>
116
122
 
117
123
  <style scoped>
@@ -149,8 +155,9 @@ const opened = ref<string[]>([]);
149
155
 
150
156
 
151
157
  const routerIsReady = ref(false);
158
+ const loginRedirectCheckIsReady = ref(false);
152
159
 
153
- const loggedIn = computed(() => route.name !== 'login' && routerIsReady.value);
160
+ const loggedIn = computed(() => route.name !== 'login');
154
161
 
155
162
  const theme = ref('light');
156
163
 
@@ -186,6 +193,7 @@ onMounted(async () => {
186
193
  initRouter();
187
194
  initFlowbite();
188
195
  await coreStore.fetchMenuAndResource();
196
+ loginRedirectCheckIsReady.value = true;
189
197
  title.value = coreStore.config.title || 'Admin Forth';
190
198
  useHead({
191
199
  title: title.value,
@@ -2,11 +2,11 @@
2
2
  <!-- drawer component -->
3
3
  <div id="drawer-navigation"
4
4
 
5
- class="fixed top-14 right-0 z-50 p-4 overflow-y-auto transition-transform translate-x-full bg-white w-80 dark:bg-gray-800 shadow-xl dark:shadow-black"
5
+ class="fixed right-0 z-50 p-4 overflow-y-auto transition-transform translate-x-full bg-white w-80 dark:bg-gray-800 shadow-xl dark:shadow-black"
6
6
 
7
7
  :class="show ? 'top-0 transform-none' : ''"
8
8
  tabindex="-1" aria-labelledby="drawer-navigation-label"
9
- :style="{ height: `calc(100vh - 3.5rem)` }"
9
+ :style="{ height: `calc(100vh ` }"
10
10
  >
11
11
  <h5 id="drawer-navigation-label" class="text-base font-semibold text-gray-500 uppercase dark:text-gray-400">
12
12
  Filters
@@ -92,7 +92,7 @@
92
92
  </div>
93
93
  </div>
94
94
 
95
- <div v-if="show" drawer-backdrop="" class="bg-gray-900/50 dark:bg-gray-900/80 fixed inset-0 z-30"
95
+ <div v-if="show" class="bg-gray-900/50 dark:bg-gray-900/80 fixed inset-0 z-30"
96
96
  @click="$emit('hide')">
97
97
  </div>
98
98
  </template>
@@ -0,0 +1,14 @@
1
+ <template>
2
+ <td class="px-6 py-4 whitespace-nowrap">
3
+ {{ column.label }}
4
+ </td>
5
+ <td class="px-6 py-4 whitespace-nowrap whitespace-pre-wrap">
6
+ <ValueRenderer :column="column" :row="row" />
7
+ </td>
8
+ </template>
9
+
10
+ <script setup>
11
+ import ValueRenderer from '@/components/ValueRenderer.vue';
12
+
13
+ defineProps(['column', 'row']);
14
+ </script>
@@ -40,4 +40,5 @@ const router = createRouter({
40
40
  ]
41
41
  })
42
42
 
43
+
43
44
  export default router
@@ -11,12 +11,16 @@ export const useCoreStore = defineStore('core', () => {
11
11
  const resourceColumnsError = ref('');
12
12
  const resourceColumnsId = ref(null);
13
13
  const user = ref(null);
14
+ const flowBitIsInitialised = ref(false);
14
15
 
15
16
  async function fetchMenuAndResource() {
16
17
  const resp = await callAdminForthApi({
17
18
  path: '/get_base_config',
18
19
  method: 'GET',
19
20
  });
21
+ if(!resp){
22
+ return
23
+ }
20
24
  menu.value = resp.menu;
21
25
  resourceById.value = resp.resources.reduce((acc, resource) => {
22
26
  acc[resource.resourceId] = resource;
@@ -122,6 +126,7 @@ export const useCoreStore = defineStore('core', () => {
122
126
  resourceColumns,
123
127
  fetchColumns,
124
128
  resourceColumnsError,
129
+ flowBitIsInitialised,
125
130
  logout
126
131
  }
127
132
  })
@@ -448,10 +448,10 @@ fetchStatus.value.pending = false;
448
448
  allowedActions.value = data.options?.allowedActions;
449
449
 
450
450
 
451
- setTimeout(() => {
452
- initFlowbite();
453
- console.log('initFlowbite');
454
- });
451
+ // setTimeout(() => {
452
+ // initFlowbite();
453
+ // console.log('initFlowbite');
454
+ // });
455
455
  }
456
456
 
457
457
 
@@ -512,6 +512,7 @@ function addToCheckedValues(id) {
512
512
  }
513
513
 
514
514
  async function init() {
515
+
515
516
  await coreStore.fetchColumns({
516
517
  resourceId: route.params.resourceId
517
518
  });
@@ -527,7 +528,13 @@ async function init() {
527
528
  }
528
529
 
529
530
  onMounted(async () => {
531
+ if (!coreStore.flowBitIsInitialised){
532
+ initFlowbite();
533
+ coreStore.flowBitIsInitialised = true;
534
+ }
535
+
530
536
  await init();
537
+
531
538
  });
532
539
 
533
540
  // on route param change
@@ -44,18 +44,12 @@
44
44
  <tr v-for="column in coreStore.resourceColumns?.filter(c => c.showIn.includes('show'))" :key="column.name"
45
45
  class="bg-form-view-bg odd:dark:bg-gray-900 even:dark:bg-gray-800 border-b dark:border-gray-700"
46
46
  >
47
- <td class="px-6 py-4 whitespace-nowrap">
48
- {{ column.label }}
49
- </td>
50
- <td class="px-6 py-4 whitespace-nowrap whitespace-pre-wrap">
51
- <component
52
- :is="showComponentsPerColumn[column.name] || ValueRenderer"
53
- :column="column"
54
- :row="coreStore.record"
55
- />
56
- </td>
47
+ <component
48
+ :is="showComponentsPerColumn[column.name] || ShowTableItem"
49
+ :column="column"
50
+ :row="coreStore.record"
51
+ />
57
52
  </tr>
58
-
59
53
  </tbody>
60
54
  </table>
61
55
  </div>
@@ -68,6 +62,8 @@
68
62
  <script setup>
69
63
 
70
64
  import BreadcrumbsWithButtons from '@/components/BreadcrumbsWithButtons.vue';
65
+ import ShowTableItem from '@/components/ShowTableItem.vue';
66
+
71
67
  import ValueRenderer from '@/components/ValueRenderer.vue';
72
68
  import { useCoreStore } from '@/stores/core';
73
69
  import { getCustomComponent } from '@/utils';