adminforth 1.0.58 → 1.0.60
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 +6 -2
- package/dist/modules/codeInjector.js +18 -7
- package/dist/modules/utils.js +11 -7
- package/dist/plugins/ForeignInlineListPlugin/index.js +4 -2
- package/dist/plugins/base.js +23 -1
- package/dist/spa/spa/src/App.vue +14 -6
- package/dist/spa/spa/src/components/Filters.vue +3 -3
- package/dist/spa/spa/src/components/ShowTableItem.vue +14 -0
- package/dist/spa/spa/src/router/index.ts +1 -0
- package/dist/spa/spa/src/stores/core.ts +5 -0
- package/dist/spa/spa/src/views/ListView.vue +11 -4
- package/dist/spa/spa/src/views/ShowView.vue +7 -11
- package/index.ts +6 -3
- package/modules/codeInjector.ts +23 -9
- package/modules/utils.ts +10 -6
- package/package.json +1 -1
- package/plugins/ForeignInlineListPlugin/custom/InlineList.vue +5 -0
- package/plugins/ForeignInlineListPlugin/index.ts +6 -2
- package/plugins/base.ts +29 -1
- package/spa/src/App.vue +14 -6
- package/spa/src/components/Filters.vue +3 -3
- package/spa/src/components/ShowTableItem.vue +14 -0
- package/spa/src/router/index.ts +1 -0
- package/spa/src/stores/core.ts +5 -0
- package/spa/src/views/ListView.vue +11 -4
- package/spa/src/views/ShowView.vue +7 -11
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
188
|
-
|
|
189
|
-
|
|
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) {
|
package/dist/modules/utils.js
CHANGED
|
@@ -13,13 +13,17 @@ 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
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
__dirname
|
|
21
|
-
|
|
22
|
-
|
|
16
|
+
export const currentFileDir = (metaUrl) => {
|
|
17
|
+
const __filename = fileURLToPath(metaUrl);
|
|
18
|
+
let __dirname = path.dirname(__filename);
|
|
19
|
+
console.log('currentFileDir', __dirname);
|
|
20
|
+
if (__dirname.endsWith('dist')) {
|
|
21
|
+
// in prod build we are in dist also
|
|
22
|
+
__dirname = path.join(__dirname, '..');
|
|
23
|
+
}
|
|
24
|
+
return __dirname;
|
|
25
|
+
};
|
|
26
|
+
export const ADMIN_FORTH_ABSOLUTE_PATH = path.join(currentFileDir(import.meta.url), '..');
|
|
23
27
|
const package_json = JSON.parse(fs.readFileSync(path.join(ADMIN_FORTH_ABSOLUTE_PATH, 'package.json'), 'utf8'));
|
|
24
28
|
export const ADMINFORTH_VERSION = package_json.version;
|
|
25
29
|
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
|
}
|
package/dist/plugins/base.js
CHANGED
|
@@ -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
|
}
|
package/dist/spa/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-
|
|
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'
|
|
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
|
|
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
|
|
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"
|
|
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>
|
|
@@ -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
|
-
|
|
453
|
-
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
|
|
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`);
|
package/modules/codeInjector.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
212
|
-
|
|
213
|
-
|
|
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,18 @@ export function guessLabelFromName(name) {
|
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
__dirname
|
|
18
|
+
export const currentFileDir = (metaUrl) => {
|
|
19
|
+
const __filename = fileURLToPath(metaUrl);
|
|
20
|
+
let __dirname = path.dirname(__filename);
|
|
21
|
+
console.log('currentFileDir', __dirname);
|
|
22
|
+
if (__dirname.endsWith('dist')) {
|
|
23
|
+
// in prod build we are in dist also
|
|
24
|
+
__dirname = path.join(__dirname, '..');
|
|
25
|
+
}
|
|
26
|
+
return __dirname;
|
|
23
27
|
}
|
|
24
28
|
|
|
25
|
-
export const ADMIN_FORTH_ABSOLUTE_PATH =
|
|
29
|
+
export const ADMIN_FORTH_ABSOLUTE_PATH = path.join(currentFileDir(import.meta.url), '..');
|
|
26
30
|
|
|
27
31
|
const package_json = JSON.parse(fs.readFileSync(path.join(ADMIN_FORTH_ABSOLUTE_PATH, 'package.json'), 'utf8'));
|
|
28
32
|
|
package/package.json
CHANGED
|
@@ -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
|
-
|
|
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-
|
|
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'
|
|
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
|
|
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
|
|
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"
|
|
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>
|
package/spa/src/router/index.ts
CHANGED
package/spa/src/stores/core.ts
CHANGED
|
@@ -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
|
-
|
|
453
|
-
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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';
|