adminforth 1.0.57 → 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 +18 -3
- package/dist/modules/codeInjector.js +18 -7
- package/dist/modules/utils.js +10 -7
- package/dist/plugins/ForeignInlineListPlugin/index.js +24 -0
- package/dist/plugins/base.js +30 -0
- package/dist/servers/express.js +17 -2
- package/dist/spa/spa/src/App.vue +34 -10
- 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 +21 -5
- package/modules/codeInjector.ts +23 -9
- package/modules/utils.ts +9 -6
- package/package.json +1 -1
- package/plugins/ForeignInlineListPlugin/custom/InlineList.vue +5 -0
- package/plugins/ForeignInlineListPlugin/index.ts +39 -0
- package/plugins/base.ts +44 -0
- package/servers/express.ts +19 -2
- package/spa/src/App.vue +34 -10
- 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/types/AdminForthConfig.ts +32 -31
package/dist/index.js
CHANGED
|
@@ -32,14 +32,24 @@ 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();
|
|
37
|
+
this.activatePlugins();
|
|
38
|
+
this.validateConfig(); // revalidate after plugins
|
|
36
39
|
this.express = new ExpressServer(this);
|
|
37
40
|
this.auth = new Auth();
|
|
38
|
-
this.codeInjector = new CodeInjector(this);
|
|
39
41
|
this.connectors = {};
|
|
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) {
|
|
@@ -241,7 +251,11 @@ class AdminForth {
|
|
|
241
251
|
for (const column of resource.columns) {
|
|
242
252
|
if (column.component) {
|
|
243
253
|
for (const [key, value] of Object.entries(column.component)) {
|
|
244
|
-
|
|
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
|
+
}
|
|
245
259
|
const path = value.replace('@@', this.config.customization.customComponentsDir);
|
|
246
260
|
if (!fs.existsSync(path)) {
|
|
247
261
|
throw new Error(`Component file ${path} does not exist`);
|
|
@@ -456,7 +470,8 @@ class AdminForth {
|
|
|
456
470
|
if (!resource) {
|
|
457
471
|
return { error: `Resource ${resourceId} not found` };
|
|
458
472
|
}
|
|
459
|
-
|
|
473
|
+
// exclude "plugins" key
|
|
474
|
+
return { resource: Object.assign(Object.assign({}, resource), { plugins: undefined }) };
|
|
460
475
|
}),
|
|
461
476
|
});
|
|
462
477
|
server.endpoint({
|
|
@@ -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,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
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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) {
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import AdminForthPlugin from "../base.js";
|
|
2
|
+
export default class ForeignInlineListPlugin extends AdminForthPlugin {
|
|
3
|
+
constructor(options) {
|
|
4
|
+
super(options, import.meta.url);
|
|
5
|
+
this.options = options;
|
|
6
|
+
}
|
|
7
|
+
modifyResourceConfig(adminforth, resourceConfig) {
|
|
8
|
+
super.modifyResourceConfig(adminforth, resourceConfig);
|
|
9
|
+
// get resource with foreignResourceId
|
|
10
|
+
this.foreignResource = adminforth.config.resources.find((resource) => resource.resourceId === this.options.foreignResourceId);
|
|
11
|
+
if (!this.foreignResource) {
|
|
12
|
+
throw new Error(`ForeignInlineListPlugin: Resource with ID "${this.options.foreignResourceId}" not found`);
|
|
13
|
+
}
|
|
14
|
+
resourceConfig.columns.push({
|
|
15
|
+
name: `foreignInlineList_${this.foreignResource.resourceId}`,
|
|
16
|
+
label: 'Foreign Inline List',
|
|
17
|
+
virtual: true,
|
|
18
|
+
showIn: ['show'],
|
|
19
|
+
component: {
|
|
20
|
+
show: this.componentPath('InlineList.vue'),
|
|
21
|
+
},
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +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';
|
|
5
|
+
export default class AdminForthPlugin {
|
|
6
|
+
constructor(pluginOptions, metaUrl) {
|
|
7
|
+
this.customFolderName = 'custom';
|
|
8
|
+
// set up plugin here
|
|
9
|
+
this.pluginDir = currentFileDir(metaUrl);
|
|
10
|
+
}
|
|
11
|
+
modifyResourceConfig(adminforth, resourceConfig) {
|
|
12
|
+
this.adminforth = adminforth;
|
|
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
|
+
}
|
|
30
|
+
}
|
package/dist/servers/express.js
CHANGED
|
@@ -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
|
}
|
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">
|
|
@@ -55,10 +55,12 @@
|
|
|
55
55
|
|
|
56
56
|
|
|
57
57
|
<aside
|
|
58
|
-
|
|
58
|
+
|
|
59
|
+
v-if="loggedIn && routerIsReady && loginRedirectCheckIsReady"
|
|
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
|
|
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"
|
|
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>
|
|
@@ -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>
|
|
@@ -144,11 +150,14 @@ createHead()
|
|
|
144
150
|
const route = useRoute();
|
|
145
151
|
const router = useRouter();
|
|
146
152
|
const title = ref('');
|
|
153
|
+
//create a ref to store the opened menu items with ts type;
|
|
154
|
+
const opened = ref<string[]>([]);
|
|
147
155
|
|
|
148
156
|
|
|
149
157
|
const routerIsReady = ref(false);
|
|
158
|
+
const loginRedirectCheckIsReady = ref(false);
|
|
150
159
|
|
|
151
|
-
const loggedIn = computed(() => route.name !== 'login'
|
|
160
|
+
const loggedIn = computed(() => route.name !== 'login');
|
|
152
161
|
|
|
153
162
|
const theme = ref('light');
|
|
154
163
|
|
|
@@ -157,6 +166,15 @@ function toggleTheme() {
|
|
|
157
166
|
document.documentElement.classList.toggle('dark');
|
|
158
167
|
}
|
|
159
168
|
|
|
169
|
+
function clickOnMenuItem (label: string) {
|
|
170
|
+
if (opened.value.includes(label)) {
|
|
171
|
+
opened.value = opened.value.filter((item) => item !== label);
|
|
172
|
+
} else {
|
|
173
|
+
opened.value.push(label);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
}
|
|
177
|
+
|
|
160
178
|
async function logout() {
|
|
161
179
|
await coreStore.logout();
|
|
162
180
|
router.push({ name: 'login' });
|
|
@@ -175,6 +193,7 @@ onMounted(async () => {
|
|
|
175
193
|
initRouter();
|
|
176
194
|
initFlowbite();
|
|
177
195
|
await coreStore.fetchMenuAndResource();
|
|
196
|
+
loginRedirectCheckIsReady.value = true;
|
|
178
197
|
title.value = coreStore.config.title || 'Admin Forth';
|
|
179
198
|
useHead({
|
|
180
199
|
title: title.value,
|
|
@@ -186,6 +205,11 @@ onMounted(async () => {
|
|
|
186
205
|
],
|
|
187
206
|
|
|
188
207
|
})
|
|
208
|
+
coreStore.menu.forEach((item, i) => {
|
|
209
|
+
if (item.open) {
|
|
210
|
+
opened.value.push(i);
|
|
211
|
+
}
|
|
212
|
+
});
|
|
189
213
|
})
|
|
190
214
|
|
|
191
215
|
</script>
|
|
@@ -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,15 +50,27 @@ 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
|
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
|
-
this.codeInjector = new CodeInjector(this);
|
|
58
61
|
this.connectors = {};
|
|
59
|
-
this.statuses = {}
|
|
62
|
+
this.statuses = {};
|
|
60
63
|
console.log(`🚀 AdminForth v${ADMINFORTH_VERSION} starting up`)
|
|
61
64
|
}
|
|
62
65
|
|
|
66
|
+
activatePlugins() {
|
|
67
|
+
for (let resource of this.config.resources) {
|
|
68
|
+
for (let pluginInstance of resource.plugins || []) {
|
|
69
|
+
pluginInstance.modifyResourceConfig(this, resource);
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
63
74
|
validateConfig() {
|
|
64
75
|
if (this.config.rootUser) {
|
|
65
76
|
if (!this.config.rootUser.username) {
|
|
@@ -297,7 +308,11 @@ class AdminForth {
|
|
|
297
308
|
for (const column of resource.columns) {
|
|
298
309
|
if (column.component) {
|
|
299
310
|
for (const [key, value] of Object.entries(column.component)) {
|
|
300
|
-
|
|
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
|
+
}
|
|
301
316
|
const path = value.replace('@@', this.config.customization.customComponentsDir);
|
|
302
317
|
if (!fs.existsSync(path)) {
|
|
303
318
|
throw new Error(`Component file ${path} does not exist`);
|
|
@@ -527,7 +542,8 @@ class AdminForth {
|
|
|
527
542
|
if (!resource) {
|
|
528
543
|
return { error: `Resource ${resourceId} not found` };
|
|
529
544
|
}
|
|
530
|
-
|
|
545
|
+
// exclude "plugins" key
|
|
546
|
+
return { resource: { ...resource, plugins: undefined } };
|
|
531
547
|
},
|
|
532
548
|
});
|
|
533
549
|
server.endpoint({
|
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,17 @@ export function guessLabelFromName(name) {
|
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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 =
|
|
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
|
@@ -0,0 +1,39 @@
|
|
|
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, import.meta.url);
|
|
15
|
+
|
|
16
|
+
this.options = options;
|
|
17
|
+
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
modifyResourceConfig(adminforth: AdminForth, resourceConfig: AdminForthResource) {
|
|
21
|
+
super.modifyResourceConfig(adminforth, resourceConfig);
|
|
22
|
+
|
|
23
|
+
// get resource with foreignResourceId
|
|
24
|
+
this.foreignResource = adminforth.config.resources.find((resource) => resource.resourceId === this.options.foreignResourceId);
|
|
25
|
+
if (!this.foreignResource) {
|
|
26
|
+
throw new Error(`ForeignInlineListPlugin: Resource with ID "${this.options.foreignResourceId}" not found`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
resourceConfig.columns.push({
|
|
30
|
+
name: `foreignInlineList_${this.foreignResource.resourceId}`,
|
|
31
|
+
label: 'Foreign Inline List',
|
|
32
|
+
virtual: true,
|
|
33
|
+
showIn: ['show'],
|
|
34
|
+
component: {
|
|
35
|
+
show: this.componentPath('InlineList.vue'),
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
}
|
package/plugins/base.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { AdminForthResource } from '../types/AdminForthConfig.js';
|
|
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';
|
|
7
|
+
|
|
8
|
+
export default class AdminForthPlugin {
|
|
9
|
+
|
|
10
|
+
adminforth: AdminForth;
|
|
11
|
+
pluginDir: string;
|
|
12
|
+
customFolderName: string = 'custom';
|
|
13
|
+
|
|
14
|
+
constructor(pluginOptions: any, metaUrl: string) {
|
|
15
|
+
// set up plugin here
|
|
16
|
+
this.pluginDir = currentFileDir(metaUrl);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
modifyResourceConfig(adminforth: AdminForth, resourceConfig: AdminForthResource) {
|
|
20
|
+
this.adminforth = adminforth;
|
|
21
|
+
}
|
|
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
|
+
|
|
44
|
+
}
|
package/servers/express.ts
CHANGED
|
@@ -112,7 +112,17 @@ class ExpressServer {
|
|
|
112
112
|
|
|
113
113
|
} else {
|
|
114
114
|
this.expressApp.get(`${slashedPrefix}assets/*`, (req, res) => {
|
|
115
|
-
res.sendFile(
|
|
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
|
@@ -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">
|
|
@@ -55,10 +55,12 @@
|
|
|
55
55
|
|
|
56
56
|
|
|
57
57
|
<aside
|
|
58
|
-
|
|
58
|
+
|
|
59
|
+
v-if="loggedIn && routerIsReady && loginRedirectCheckIsReady"
|
|
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
|
|
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"
|
|
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>
|
|
@@ -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>
|
|
@@ -144,11 +150,14 @@ createHead()
|
|
|
144
150
|
const route = useRoute();
|
|
145
151
|
const router = useRouter();
|
|
146
152
|
const title = ref('');
|
|
153
|
+
//create a ref to store the opened menu items with ts type;
|
|
154
|
+
const opened = ref<string[]>([]);
|
|
147
155
|
|
|
148
156
|
|
|
149
157
|
const routerIsReady = ref(false);
|
|
158
|
+
const loginRedirectCheckIsReady = ref(false);
|
|
150
159
|
|
|
151
|
-
const loggedIn = computed(() => route.name !== 'login'
|
|
160
|
+
const loggedIn = computed(() => route.name !== 'login');
|
|
152
161
|
|
|
153
162
|
const theme = ref('light');
|
|
154
163
|
|
|
@@ -157,6 +166,15 @@ function toggleTheme() {
|
|
|
157
166
|
document.documentElement.classList.toggle('dark');
|
|
158
167
|
}
|
|
159
168
|
|
|
169
|
+
function clickOnMenuItem (label: string) {
|
|
170
|
+
if (opened.value.includes(label)) {
|
|
171
|
+
opened.value = opened.value.filter((item) => item !== label);
|
|
172
|
+
} else {
|
|
173
|
+
opened.value.push(label);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
}
|
|
177
|
+
|
|
160
178
|
async function logout() {
|
|
161
179
|
await coreStore.logout();
|
|
162
180
|
router.push({ name: 'login' });
|
|
@@ -175,6 +193,7 @@ onMounted(async () => {
|
|
|
175
193
|
initRouter();
|
|
176
194
|
initFlowbite();
|
|
177
195
|
await coreStore.fetchMenuAndResource();
|
|
196
|
+
loginRedirectCheckIsReady.value = true;
|
|
178
197
|
title.value = coreStore.config.title || 'Admin Forth';
|
|
179
198
|
useHead({
|
|
180
199
|
title: title.value,
|
|
@@ -186,6 +205,11 @@ onMounted(async () => {
|
|
|
186
205
|
],
|
|
187
206
|
|
|
188
207
|
})
|
|
208
|
+
coreStore.menu.forEach((item, i) => {
|
|
209
|
+
if (item.open) {
|
|
210
|
+
opened.value.push(i);
|
|
211
|
+
}
|
|
212
|
+
});
|
|
189
213
|
})
|
|
190
214
|
|
|
191
215
|
</script>
|
|
@@ -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';
|
|
@@ -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
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
|