adminforth 1.0.84 → 1.1.1
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 +15 -16
- package/dist/modules/codeInjector.js +2 -2
- package/dist/plugins/ForeignInlineListPlugin/index.js +3 -2
- package/dist/servers/express.js +0 -3
- package/dist/spa/spa/package-lock.json +13 -0
- package/dist/spa/spa/package.json +1 -0
- package/dist/spa/spa/src/App.vue +44 -19
- package/dist/spa/spa/src/components/AcceptModal.vue +2 -9
- package/dist/spa/spa/src/components/Toast.vue +65 -0
- package/dist/spa/spa/src/components/ValueRenderer.vue +8 -8
- package/dist/spa/spa/src/composables/useStores.ts +51 -0
- package/dist/spa/spa/src/stores/core.ts +5 -1
- package/dist/spa/spa/src/stores/modal.ts +13 -2
- package/dist/spa/spa/src/stores/toast.ts +15 -0
- package/dist/spa/spa/src/views/CreateView.vue +5 -5
- package/dist/spa/spa/src/views/EditView.vue +8 -7
- package/dist/spa/spa/src/views/ListView.vue +8 -9
- package/dist/spa/spa/src/views/ShowView.vue +13 -10
- package/dist/types/AdminForthConfig.js +25 -17
- package/dist/types/FrontendAPI.js +7 -0
- package/documentation/docs/Getting Started.md +374 -0
- package/documentation/docs/Glossary.md +37 -0
- package/documentation/docs/image.png +0 -0
- package/documentation/docusaurus.config.ts +1 -1
- package/index.ts +29 -35
- package/modules/codeInjector.ts +4 -4
- package/package.json +1 -1
- package/plugins/ForeignInlineListPlugin/index.ts +3 -3
- package/servers/express.ts +4 -9
- package/spa/package-lock.json +13 -0
- package/spa/package.json +1 -0
- package/spa/src/App.vue +44 -19
- package/spa/src/components/AcceptModal.vue +2 -9
- package/spa/src/components/Toast.vue +65 -0
- package/spa/src/components/ValueRenderer.vue +8 -8
- package/spa/src/composables/useStores.ts +51 -0
- package/spa/src/stores/core.ts +5 -1
- package/spa/src/stores/modal.ts +13 -2
- package/spa/src/stores/toast.ts +15 -0
- package/spa/src/views/CreateView.vue +5 -5
- package/spa/src/views/EditView.vue +8 -7
- package/spa/src/views/ListView.vue +8 -9
- package/spa/src/views/ShowView.vue +13 -10
- package/types/AdminForthConfig.ts +332 -62
- package/types/FrontendAPI.ts +73 -0
package/dist/index.js
CHANGED
|
@@ -23,10 +23,10 @@ import ExpressServer from './servers/express.js';
|
|
|
23
23
|
import { v1 as uuid } from 'uuid';
|
|
24
24
|
import fs from 'fs';
|
|
25
25
|
import { ADMINFORTH_VERSION } from './modules/utils.js';
|
|
26
|
-
import { AdminForthFilterOperators, AdminForthDataTypes } from './types/AdminForthConfig.js';
|
|
26
|
+
import { AdminForthFilterOperators, AdminForthDataTypes, AdminForthResourcePages } from './types/AdminForthConfig.js';
|
|
27
27
|
import { getFunctionList } from './modules/utils.js';
|
|
28
28
|
import path from 'path';
|
|
29
|
-
|
|
29
|
+
//get array from enum AdminForthResourcePages
|
|
30
30
|
const DEFAULT_ALLOWED_ACTIONS = { create: true, edit: true, show: true, delete: true };
|
|
31
31
|
class AdminForth {
|
|
32
32
|
constructor(config) {
|
|
@@ -122,12 +122,12 @@ class AdminForth {
|
|
|
122
122
|
if (!res.table) {
|
|
123
123
|
errors.push(`Resource "${res.dataSource}" is missing table`);
|
|
124
124
|
}
|
|
125
|
-
// if
|
|
126
|
-
if (res.
|
|
127
|
-
errors.push(`Resource "${res.dataSource}"
|
|
125
|
+
// if recordLabel is not callable, throw error
|
|
126
|
+
if (res.recordLabel && typeof res.recordLabel !== 'function') {
|
|
127
|
+
errors.push(`Resource "${res.dataSource}" recordLabel is not a function`);
|
|
128
128
|
}
|
|
129
|
-
if (!res.
|
|
130
|
-
res.
|
|
129
|
+
if (!res.recordLabel) {
|
|
130
|
+
res.recordLabel = (item) => {
|
|
131
131
|
const pkVal = item[res.columns.find((col) => col.primaryKey).name];
|
|
132
132
|
return `${res.label} ${pkVal}`;
|
|
133
133
|
};
|
|
@@ -141,7 +141,6 @@ class AdminForth {
|
|
|
141
141
|
res.columns = [];
|
|
142
142
|
}
|
|
143
143
|
res.columns.forEach((col) => {
|
|
144
|
-
var _b;
|
|
145
144
|
col.label = col.label || guessLabelFromName(col.name);
|
|
146
145
|
//define default sortable
|
|
147
146
|
if (!Object.keys(col).includes('sortable')) {
|
|
@@ -171,11 +170,11 @@ class AdminForth {
|
|
|
171
170
|
errors.push(`Resource "${res.resourceId}" column "${col.name}" has invalid editingNote value "${wrongEditingNoteOn}", allowed keys are 'create', 'edit']`);
|
|
172
171
|
}
|
|
173
172
|
}
|
|
174
|
-
const wrongShowIn = col.showIn && col.showIn.find((c) =>
|
|
173
|
+
const wrongShowIn = col.showIn && col.showIn.find((c) => AdminForthResourcePages[c] === undefined);
|
|
175
174
|
if (wrongShowIn) {
|
|
176
|
-
errors.push(`Resource "${res.resourceId}" column "${col.name}" has invalid showIn value "${wrongShowIn}", allowed values are ${
|
|
175
|
+
errors.push(`Resource "${res.resourceId}" column "${col.name}" has invalid showIn value "${wrongShowIn}", allowed values are ${Object.keys(AdminForthResourcePages).join(', ')}`);
|
|
177
176
|
}
|
|
178
|
-
col.showIn =
|
|
177
|
+
col.showIn = col.showIn || Object.values(AdminForthResourcePages);
|
|
179
178
|
});
|
|
180
179
|
if (!res.options) {
|
|
181
180
|
res.options = { bulkActions: [], allowedActions: {} };
|
|
@@ -296,8 +295,8 @@ class AdminForth {
|
|
|
296
295
|
// check is all custom components files exists
|
|
297
296
|
for (const resource of this.config.resources) {
|
|
298
297
|
for (const column of resource.columns) {
|
|
299
|
-
if (column.
|
|
300
|
-
for (const [key, value] of Object.entries(column.
|
|
298
|
+
if (column.components) {
|
|
299
|
+
for (const [key, value] of Object.entries(column.components)) {
|
|
301
300
|
if (this.codeInjector.allComponentNames[value]) {
|
|
302
301
|
// not obvious, but if we are in this if, it means that this is plugin component
|
|
303
302
|
// and there is no sense to check if it exists in users folder
|
|
@@ -613,7 +612,7 @@ class AdminForth {
|
|
|
613
612
|
});
|
|
614
613
|
const targetDataMap = targetData.data.reduce((acc, item) => {
|
|
615
614
|
acc[item[targetResourcePkField]] = {
|
|
616
|
-
label: targetResource.
|
|
615
|
+
label: targetResource.recordLabel(item),
|
|
617
616
|
pk: item[targetResourcePkField],
|
|
618
617
|
};
|
|
619
618
|
return acc;
|
|
@@ -640,7 +639,7 @@ class AdminForth {
|
|
|
640
639
|
});
|
|
641
640
|
});
|
|
642
641
|
data.data.forEach((item) => {
|
|
643
|
-
item._label = resource.
|
|
642
|
+
item._label = resource.recordLabel(item);
|
|
644
643
|
});
|
|
645
644
|
return Object.assign(Object.assign({}, data), { options: resource === null || resource === void 0 ? void 0 : resource.options });
|
|
646
645
|
}),
|
|
@@ -689,7 +688,7 @@ class AdminForth {
|
|
|
689
688
|
});
|
|
690
689
|
const items = dbDataItems.data.map((item) => {
|
|
691
690
|
const pk = item[targetResource.columns.find((col) => col.primaryKey).name];
|
|
692
|
-
const labler = targetResource.
|
|
691
|
+
const labler = targetResource.recordLabel;
|
|
693
692
|
return {
|
|
694
693
|
value: pk,
|
|
695
694
|
label: labler(item),
|
|
@@ -237,8 +237,8 @@ class CodeInjector {
|
|
|
237
237
|
this.adminforth.config.resources.forEach((resource) => {
|
|
238
238
|
var _a;
|
|
239
239
|
resource.columns.forEach((field) => {
|
|
240
|
-
if (field.
|
|
241
|
-
Object.values(field.
|
|
240
|
+
if (field.components) {
|
|
241
|
+
Object.values(field.components).forEach((filePath) => {
|
|
242
242
|
if (!customResourceComponents.includes(filePath)) {
|
|
243
243
|
customResourceComponents.push(filePath);
|
|
244
244
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { AdminForthResourcePages } from "../../types/AdminForthConfig.js";
|
|
1
2
|
import AdminForthPlugin from "../base.js";
|
|
2
3
|
export default class ForeignInlineListPlugin extends AdminForthPlugin {
|
|
3
4
|
constructor(options) {
|
|
@@ -15,8 +16,8 @@ export default class ForeignInlineListPlugin extends AdminForthPlugin {
|
|
|
15
16
|
name: `foreignInlineList_${this.foreignResource.resourceId}`,
|
|
16
17
|
label: 'Foreign Inline List',
|
|
17
18
|
virtual: true,
|
|
18
|
-
showIn: [
|
|
19
|
-
|
|
19
|
+
showIn: [AdminForthResourcePages.show],
|
|
20
|
+
components: {
|
|
20
21
|
showRow: this.componentPath('InlineList.vue'),
|
|
21
22
|
},
|
|
22
23
|
});
|
package/dist/servers/express.js
CHANGED
|
@@ -8,12 +8,9 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
8
8
|
});
|
|
9
9
|
};
|
|
10
10
|
import path from 'path';
|
|
11
|
-
import { fileURLToPath } from 'url';
|
|
12
11
|
import fs from 'fs';
|
|
13
12
|
import CodeInjector from '../modules/codeInjector.js';
|
|
14
13
|
import fetch from 'node-fetch';
|
|
15
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
16
|
-
const __dirname = path.dirname(__filename);
|
|
17
14
|
function replaceAtStart(string, substring) {
|
|
18
15
|
if (string.startsWith(substring)) {
|
|
19
16
|
return string.slice(substring.length);
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
"flowbite-datepicker": "^1.2.6",
|
|
18
18
|
"pinia": "^2.1.7",
|
|
19
19
|
"unhead": "^1.9.12",
|
|
20
|
+
"uuid": "^10.0.0",
|
|
20
21
|
"vue": "^3.4.21",
|
|
21
22
|
"vue-router": "^4.3.0",
|
|
22
23
|
"vue-slider-component": "^4.1.0-beta.7"
|
|
@@ -4032,6 +4033,18 @@
|
|
|
4032
4033
|
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
|
|
4033
4034
|
"dev": true
|
|
4034
4035
|
},
|
|
4036
|
+
"node_modules/uuid": {
|
|
4037
|
+
"version": "10.0.0",
|
|
4038
|
+
"resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz",
|
|
4039
|
+
"integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==",
|
|
4040
|
+
"funding": [
|
|
4041
|
+
"https://github.com/sponsors/broofa",
|
|
4042
|
+
"https://github.com/sponsors/ctavan"
|
|
4043
|
+
],
|
|
4044
|
+
"bin": {
|
|
4045
|
+
"uuid": "dist/bin/uuid"
|
|
4046
|
+
}
|
|
4047
|
+
},
|
|
4035
4048
|
"node_modules/vite": {
|
|
4036
4049
|
"version": "5.2.13",
|
|
4037
4050
|
"resolved": "https://registry.npmjs.org/vite/-/vite-5.2.13.tgz",
|
package/dist/spa/spa/src/App.vue
CHANGED
|
@@ -123,15 +123,43 @@
|
|
|
123
123
|
</div>
|
|
124
124
|
</div>
|
|
125
125
|
<AcceptModal />
|
|
126
|
+
<div v-if="toastStore.toasts.length>0" class="fixed bottom-5 right-5 flex gap-1 flex-col-reverse">
|
|
127
|
+
<transition-group
|
|
128
|
+
name="fade"
|
|
129
|
+
tag="div"
|
|
130
|
+
class="flex flex-col-reverse gap-1"
|
|
131
|
+
>
|
|
132
|
+
<Toast :toast="t" @close="toastStore.removeToast(t)" v-for="(t,i) in toastStore.toasts" :key="`t-${t.id}`" ></Toast>
|
|
133
|
+
</transition-group>
|
|
134
|
+
</div>
|
|
126
135
|
|
|
127
136
|
<div v-if="sideBarOpen"
|
|
128
137
|
@click="sideBarOpen = false"
|
|
129
138
|
|
|
130
139
|
drawer-backdrop="" class="bg-gray-900/50 dark:bg-gray-900/80 fixed inset-0 z-30"></div>
|
|
131
140
|
|
|
141
|
+
|
|
142
|
+
|
|
132
143
|
</template>
|
|
133
144
|
|
|
134
|
-
<style scoped>
|
|
145
|
+
<style lang="scss" scoped>
|
|
146
|
+
|
|
147
|
+
.fade-leave-active {
|
|
148
|
+
@apply transition-opacity duration-500;
|
|
149
|
+
}
|
|
150
|
+
.fade-leave-to {
|
|
151
|
+
@apply opacity-0;
|
|
152
|
+
}
|
|
153
|
+
.fade-enter-active {
|
|
154
|
+
@apply transition-opacity duration-500;
|
|
155
|
+
}
|
|
156
|
+
.fade-enter-from {
|
|
157
|
+
@apply opacity-0;
|
|
158
|
+
}
|
|
159
|
+
.fade-enter-to {
|
|
160
|
+
@apply opacity-100;
|
|
161
|
+
}
|
|
162
|
+
|
|
135
163
|
</style>
|
|
136
164
|
|
|
137
165
|
<script setup lang="ts">
|
|
@@ -140,6 +168,7 @@ import { RouterLink, RouterView } from 'vue-router';
|
|
|
140
168
|
import { initFlowbite } from 'flowbite'
|
|
141
169
|
import './index.scss'
|
|
142
170
|
import { useCoreStore } from '@/stores/core';
|
|
171
|
+
import {useModalStore} from '@/stores/modal';
|
|
143
172
|
import { IconMoonSolid, IconSunSolid } from '@iconify-prerendered/vue-flowbite';
|
|
144
173
|
import AcceptModal from './components/AcceptModal.vue';
|
|
145
174
|
import MenuLink from './components/MenuLink.vue';
|
|
@@ -148,33 +177,22 @@ import { getIcon } from '@/utils';
|
|
|
148
177
|
import { useHead } from 'unhead'
|
|
149
178
|
import { createHead } from 'unhead'
|
|
150
179
|
import { loadFile } from './utils';
|
|
151
|
-
|
|
180
|
+
import Toast from './components/Toast.vue';
|
|
181
|
+
import {useToastStore} from '@/stores/toast';
|
|
182
|
+
import { FrontendAPI } from '@/composables/useStores'
|
|
152
183
|
// import { link } from 'fs';
|
|
153
184
|
const coreStore = useCoreStore();
|
|
154
|
-
|
|
155
|
-
|
|
185
|
+
const modalStore = useModalStore();
|
|
186
|
+
const toastStore = useToastStore();
|
|
187
|
+
const frontendApi = new FrontendAPI();
|
|
188
|
+
frontendApi.init();
|
|
156
189
|
const splitAtLast = (str: string, separator: string) => {
|
|
157
190
|
const index = str.lastIndexOf(separator);
|
|
158
191
|
return [str.slice(0, index), str.slice(index + 1)];
|
|
159
192
|
}
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
193
|
createHead()
|
|
164
|
-
|
|
165
|
-
|
|
166
194
|
const sideBarOpen = ref(false);
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
195
|
const route = useRoute();
|
|
171
|
-
watch(route, () => {
|
|
172
|
-
title.value = `${coreStore.config?.title || coreStore.config?.brandName || 'Adminforth'} | ${ Object.values(route.params)[0] || route.meta.title || ' '}`;
|
|
173
|
-
useHead({
|
|
174
|
-
title: title.value,
|
|
175
|
-
})
|
|
176
|
-
});
|
|
177
|
-
|
|
178
196
|
const router = useRouter();
|
|
179
197
|
const title = ref('');
|
|
180
198
|
//create a ref to store the opened menu items with ts type;
|
|
@@ -226,6 +244,13 @@ async function loadMenu() {
|
|
|
226
244
|
});
|
|
227
245
|
}
|
|
228
246
|
|
|
247
|
+
watch(route, () => {
|
|
248
|
+
title.value = `${coreStore.config?.title || coreStore.config?.brandName || 'Adminforth'} | ${ Object.values(route.params)[0] || route.meta.title || ' '}`;
|
|
249
|
+
useHead({
|
|
250
|
+
title: title.value,
|
|
251
|
+
})
|
|
252
|
+
});
|
|
253
|
+
|
|
229
254
|
// initialize components based on data attribute selectors
|
|
230
255
|
onMounted(async () => {
|
|
231
256
|
loadMenu(); // run this in async mode
|
|
@@ -3,13 +3,6 @@ import { ref,onMounted } from 'vue'
|
|
|
3
3
|
import { useModalStore } from '@/stores/modal';
|
|
4
4
|
|
|
5
5
|
const modalStore = useModalStore();
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
6
|
</script>
|
|
14
7
|
|
|
15
8
|
<template>
|
|
@@ -28,10 +21,10 @@ const modalStore = useModalStore();
|
|
|
28
21
|
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 11V6m0 8h.01M19 10a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z"/>
|
|
29
22
|
</svg>
|
|
30
23
|
<h3 class="mb-5 text-lg font-normal text-gray-500 dark:text-gray-400">{{ modalStore?.modalContent?.content }}</h3>
|
|
31
|
-
<button @click="()=>{modalStore.onAcceptFunction();modalStore.togleModal()}" type="button" class="text-white bg-red-600 hover:bg-red-800 focus:ring-4 focus:outline-none focus:ring-red-300 dark:focus:ring-red-800 font-medium rounded-lg text-sm inline-flex items-center px-5 py-2.5 text-center">
|
|
24
|
+
<button @click="()=>{ modalStore.onAcceptFunction(true);modalStore.togleModal()}" type="button" class="text-white bg-red-600 hover:bg-red-800 focus:ring-4 focus:outline-none focus:ring-red-300 dark:focus:ring-red-800 font-medium rounded-lg text-sm inline-flex items-center px-5 py-2.5 text-center">
|
|
32
25
|
{{ modalStore?.modalContent?.acceptText }}
|
|
33
26
|
</button>
|
|
34
|
-
<button @click="modalStore.togleModal" type="button" class="py-2.5 px-5 ms-3 text-sm font-medium text-gray-900 focus:outline-none bg-white rounded-lg border border-gray-200 hover:bg-gray-100 hover:text-blue-700 focus:z-10 focus:ring-4 focus:ring-gray-100 dark:focus:ring-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700">{{ modalStore?.modalContent?.cancelText }}</button>
|
|
27
|
+
<button @click="()=>{modalStore.onAcceptFunction(false);modalStore.togleModal()}" type="button" class="py-2.5 px-5 ms-3 text-sm font-medium text-gray-900 focus:outline-none bg-white rounded-lg border border-gray-200 hover:bg-gray-100 hover:text-blue-700 focus:z-10 focus:ring-4 focus:ring-gray-100 dark:focus:ring-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700">{{ modalStore?.modalContent?.cancelText }}</button>
|
|
35
28
|
</div>
|
|
36
29
|
</div>
|
|
37
30
|
</div>
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
<div id="toast-default" class="flex items-center w-full max-w-xs p-4 text-gray-500 bg-white rounded-lg shadow dark:text-gray-400 dark:bg-gray-800" role="alert">
|
|
5
|
+
<div v-if="toast.variant == 'info'" class="inline-flex items-center justify-center flex-shrink-0 w-8 h-8 text-blue-500 bg-blue-100 rounded-lg dark:bg-blue-800 dark:text-blue-200">
|
|
6
|
+
<svg class="w-4 h-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 18 20">
|
|
7
|
+
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15.147 15.085a7.159 7.159 0 0 1-6.189 3.307A6.713 6.713 0 0 1 3.1 15.444c-2.679-4.513.287-8.737.888-9.548A4.373 4.373 0 0 0 5 1.608c1.287.953 6.445 3.218 5.537 10.5 1.5-1.122 2.706-3.01 2.853-6.14 1.433 1.049 3.993 5.395 1.757 9.117Z"/>
|
|
8
|
+
</svg>
|
|
9
|
+
<span class="sr-only">Fire icon</span>
|
|
10
|
+
</div>
|
|
11
|
+
<div v-else-if="toast.variant == 'danger'" class="inline-flex items-center justify-center flex-shrink-0 w-8 h-8 text-red-500 bg-red-100 rounded-lg dark:bg-red-800 dark:text-red-200">
|
|
12
|
+
<svg class="w-5 h-5" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 20 20">
|
|
13
|
+
<path d="M10 .5a9.5 9.5 0 1 0 9.5 9.5A9.51 9.51 0 0 0 10 .5Zm3.707 11.793a1 1 0 1 1-1.414 1.414L10 11.414l-2.293 2.293a1 1 0 0 1-1.414-1.414L8.586 10 6.293 7.707a1 1 0 0 1 1.414-1.414L10 8.586l2.293-2.293a1 1 0 0 1 1.414 1.414L11.414 10l2.293 2.293Z"/>
|
|
14
|
+
</svg>
|
|
15
|
+
<span class="sr-only">Error icon</span>
|
|
16
|
+
</div>
|
|
17
|
+
<div v-else-if="toast.variant == 'warning'"class="inline-flex items-center justify-center flex-shrink-0 w-8 h-8 text-orange-500 bg-orange-100 rounded-lg dark:bg-orange-700 dark:text-orange-200">
|
|
18
|
+
<svg class="w-5 h-5" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 20 20">
|
|
19
|
+
<path d="M10 .5a9.5 9.5 0 1 0 9.5 9.5A9.51 9.51 0 0 0 10 .5ZM10 15a1 1 0 1 1 0-2 1 1 0 0 1 0 2Zm1-4a1 1 0 0 1-2 0V6a1 1 0 0 1 2 0v5Z"/>
|
|
20
|
+
</svg>
|
|
21
|
+
<span class="sr-only">Warning icon</span>
|
|
22
|
+
</div>
|
|
23
|
+
<div v-else class="inline-flex items-center justify-center flex-shrink-0 w-8 h-8 text-green-500 bg-green-100 rounded-lg dark:bg-green-800 dark:text-green-200">
|
|
24
|
+
<svg class="w-5 h-5" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 20 20">
|
|
25
|
+
<path d="M10 .5a9.5 9.5 0 1 0 9.5 9.5A9.51 9.51 0 0 0 10 .5Zm3.707 8.207-4 4a1 1 0 0 1-1.414 0l-2-2a1 1 0 0 1 1.414-1.414L9 10.586l3.293-3.293a1 1 0 0 1 1.414 1.414Z"/>
|
|
26
|
+
</svg>
|
|
27
|
+
<span class="sr-only">Check icon</span>
|
|
28
|
+
</div>
|
|
29
|
+
<div class="ms-3 text-sm font-normal">{{toast.message}}</div>
|
|
30
|
+
<button @click="closeToast" type="button" class="ms-auto -mx-1.5 -my-1.5 bg-white text-gray-400 hover:text-gray-900 rounded-lg focus:ring-2 focus:ring-gray-300 p-1.5 hover:bg-gray-100 inline-flex items-center justify-center h-8 w-8 dark:text-gray-500 dark:hover:text-white dark:bg-gray-800 dark:hover:bg-gray-700" >
|
|
31
|
+
<svg class="w-3 h-3" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 14 14">
|
|
32
|
+
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m1 1 6 6m0 0 6 6M7 7l6-6M7 7l-6 6"/>
|
|
33
|
+
</svg>
|
|
34
|
+
</button>
|
|
35
|
+
</div>
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
</template>
|
|
39
|
+
|
|
40
|
+
<script setup lang="ts">
|
|
41
|
+
import { onMounted } from 'vue';
|
|
42
|
+
import { useToastStore } from '@/stores/toast';
|
|
43
|
+
const toastStore = useToastStore();
|
|
44
|
+
const emit = defineEmits(['close']);
|
|
45
|
+
const props = defineProps<{
|
|
46
|
+
toast:{message: string;
|
|
47
|
+
variant: string;
|
|
48
|
+
id: string;
|
|
49
|
+
duration?: number;}
|
|
50
|
+
}>();
|
|
51
|
+
function closeToast() {
|
|
52
|
+
emit('close');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
onMounted(() => {
|
|
56
|
+
setTimeout(() => {
|
|
57
|
+
emit('close');
|
|
58
|
+
}, props.toast.duration || 5000 );
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
</script>
|
|
62
|
+
|
|
63
|
+
<style lang="scss" scoped>
|
|
64
|
+
|
|
65
|
+
</style>
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
<div>
|
|
3
3
|
|
|
4
4
|
<span v-if="column.foreignResource">
|
|
5
|
-
<RouterLink v-if="
|
|
6
|
-
:to="{ name: 'resource-show', params: { resourceId: column.foreignResource.resourceId, primaryKey:
|
|
7
|
-
{{
|
|
5
|
+
<RouterLink v-if="record[column.name]" class="font-medium text-blue-600 dark:text-blue-500 hover:brightness-110"
|
|
6
|
+
:to="{ name: 'resource-show', params: { resourceId: column.foreignResource.resourceId, primaryKey: record[column.name].pk } }">
|
|
7
|
+
{{ record[column.name].label }}
|
|
8
8
|
</RouterLink>
|
|
9
9
|
<div v-else>
|
|
10
10
|
<span class="text-gray-400">-</span>
|
|
@@ -12,18 +12,18 @@
|
|
|
12
12
|
</span>
|
|
13
13
|
|
|
14
14
|
<span v-else-if="column.type === 'boolean'">
|
|
15
|
-
<span v-if="
|
|
15
|
+
<span v-if="record[column.name]" class="bg-green-100 text-green-800 text-xs font-medium me-2 px-2.5 py-0.5 rounded dark:bg-gray-700 dark:text-green-400 border border-green-400">Yes</span>
|
|
16
16
|
<span v-else class="bg-red-100 text-red-800 text-xs font-medium me-2 px-2.5 py-0.5 rounded dark:bg-gray-700 dark:text-red-400 border border-red-400">No</span>
|
|
17
17
|
</span>
|
|
18
18
|
<span v-else-if="column.enum">
|
|
19
|
-
{{ checkEmptyValues(column.enum.find(e => e.value ===
|
|
19
|
+
{{ checkEmptyValues(column.enum.find(e => e.value === record[column.name])?.label,route.meta.type) }}
|
|
20
20
|
</span>
|
|
21
21
|
<span v-else-if="column.type === 'datetime'">
|
|
22
|
-
{{ checkEmptyValues(formatDate(
|
|
22
|
+
{{ checkEmptyValues(formatDate(record[column.name]),route.meta.type) }}
|
|
23
23
|
|
|
24
24
|
</span>
|
|
25
25
|
<span v-else>
|
|
26
|
-
{{ checkEmptyValues(
|
|
26
|
+
{{ checkEmptyValues(record[column.name],route.meta.type) }}
|
|
27
27
|
</span>
|
|
28
28
|
</div>
|
|
29
29
|
</template>
|
|
@@ -49,7 +49,7 @@ dayjs.extend(timezone);
|
|
|
49
49
|
|
|
50
50
|
const props = defineProps({
|
|
51
51
|
column: Object,
|
|
52
|
-
|
|
52
|
+
record: Object
|
|
53
53
|
});
|
|
54
54
|
|
|
55
55
|
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { FrontendAPIInterface, ConfirmParams, AlertParams } from '../types/FrontendAPI';
|
|
2
|
+
import { useToastStore } from '../stores/toast';
|
|
3
|
+
import { useModalStore } from '../stores/modal';
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
declare global {
|
|
8
|
+
interface Window {
|
|
9
|
+
adminforth: {
|
|
10
|
+
confirm: (params: ConfirmParams) => Promise<void>;
|
|
11
|
+
alert: (params: AlertParams) => void;
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export class FrontendAPI implements FrontendAPIInterface {
|
|
17
|
+
private toastStore:any
|
|
18
|
+
private modalStore:any
|
|
19
|
+
init() {
|
|
20
|
+
if (window.adminforth) {
|
|
21
|
+
throw new Error('adminforth already initialized');
|
|
22
|
+
}
|
|
23
|
+
this.toastStore = useToastStore();
|
|
24
|
+
this.modalStore = useModalStore();
|
|
25
|
+
console.log(this.toastStore, this.modalStore,'init of adminforth frontend api')
|
|
26
|
+
window.adminforth = {
|
|
27
|
+
confirm: this.confirm.bind(this),
|
|
28
|
+
alert: this.alert.bind(this)
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
confirm(params: ConfirmParams): Promise<void> {
|
|
33
|
+
return new Promise((resolve, reject) => {
|
|
34
|
+
this.modalStore.setModalContent({ content: params.message, acceptText: params.yes, cancelText: params.no })
|
|
35
|
+
this.modalStore.onAcceptFunction = resolve
|
|
36
|
+
this.modalStore.onCancelFunction = reject
|
|
37
|
+
this.modalStore.togleModal()
|
|
38
|
+
})
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
alert(params: AlertParams): void {
|
|
42
|
+
this.toastStore.addToast({
|
|
43
|
+
message: params.message,
|
|
44
|
+
variant: params.variant
|
|
45
|
+
})
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
|
|
@@ -8,6 +8,8 @@ export const useCoreStore = defineStore('core', () => {
|
|
|
8
8
|
const config = ref({});
|
|
9
9
|
const record = ref({});
|
|
10
10
|
const resourceColumns = ref(null);
|
|
11
|
+
const resource = ref(null);
|
|
12
|
+
|
|
11
13
|
const resourceOptions = ref(null);
|
|
12
14
|
const resourceColumnsError = ref('');
|
|
13
15
|
const resourceColumnsId = ref(null);
|
|
@@ -85,6 +87,7 @@ export const useCoreStore = defineStore('core', () => {
|
|
|
85
87
|
} else {
|
|
86
88
|
resourceColumns.value = res.resource.columns;
|
|
87
89
|
resourceById.value[resourceId] = res.resource;
|
|
90
|
+
resource.value = res.resource;
|
|
88
91
|
resourceOptions.value = res.resource.options;
|
|
89
92
|
}
|
|
90
93
|
}
|
|
@@ -129,6 +132,7 @@ export const useCoreStore = defineStore('core', () => {
|
|
|
129
132
|
fetchResourceFull,
|
|
130
133
|
resourceColumnsError,
|
|
131
134
|
resourceOptions,
|
|
132
|
-
logout
|
|
135
|
+
logout,
|
|
136
|
+
resource,
|
|
133
137
|
}
|
|
134
138
|
})
|
|
@@ -2,6 +2,13 @@ import { ref } from 'vue'
|
|
|
2
2
|
import { defineStore } from 'pinia'
|
|
3
3
|
import { callAdminForthApi } from '@/utils';
|
|
4
4
|
|
|
5
|
+
type ModalContentType = {
|
|
6
|
+
title?: string;
|
|
7
|
+
content?: string;
|
|
8
|
+
acceptText?: string;
|
|
9
|
+
cancelText?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
5
12
|
|
|
6
13
|
export const useModalStore = defineStore('modal', () => {
|
|
7
14
|
const modalContent = ref({
|
|
@@ -12,13 +19,17 @@ export const useModalStore = defineStore('modal', () => {
|
|
|
12
19
|
});
|
|
13
20
|
const isOpened = ref(false);
|
|
14
21
|
const onAcceptFunction: any = ref(()=>{});
|
|
22
|
+
const onCancelFunction: any = ref(()=>{});
|
|
15
23
|
function togleModal() {
|
|
16
24
|
isOpened.value = !isOpened.value;
|
|
17
25
|
}
|
|
18
26
|
function setOnAcceptFunction(func: Function) {
|
|
19
27
|
onAcceptFunction.value = func;
|
|
20
28
|
}
|
|
21
|
-
function
|
|
29
|
+
function setOnCancelFunction(func: Function) {
|
|
30
|
+
onCancelFunction.value = func;
|
|
31
|
+
}
|
|
32
|
+
function setModalContent(content: ModalContentType) {
|
|
22
33
|
modalContent.value = content;
|
|
23
34
|
}
|
|
24
35
|
function resetmodalState() {
|
|
@@ -33,6 +44,6 @@ export const useModalStore = defineStore('modal', () => {
|
|
|
33
44
|
|
|
34
45
|
}
|
|
35
46
|
|
|
36
|
-
return {isOpened, setModalContent, togleModal,modalContent, setOnAcceptFunction, onAcceptFunction,resetmodalState}
|
|
47
|
+
return {isOpened, setModalContent,onCancelFunction, togleModal,modalContent, setOnAcceptFunction, onAcceptFunction,resetmodalState,setOnCancelFunction}
|
|
37
48
|
|
|
38
49
|
})
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { ref, computed } from 'vue'
|
|
2
|
+
import { defineStore } from 'pinia'
|
|
3
|
+
import { callAdminForthApi } from '@/utils';
|
|
4
|
+
import { v1 as uuid } from 'uuid';
|
|
5
|
+
|
|
6
|
+
export const useToastStore = defineStore('toast', () => {
|
|
7
|
+
const toasts = ref([]);
|
|
8
|
+
const addToast = (toast) => {
|
|
9
|
+
toasts.value.push({...toast, id: uuid()});
|
|
10
|
+
};
|
|
11
|
+
const removeToast = (toast) => {
|
|
12
|
+
toasts.value = toasts.value.filter((t) => t.id !== toast.id);
|
|
13
|
+
};
|
|
14
|
+
return { toasts, addToast, removeToast };
|
|
15
|
+
});
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
v-for="c in coreStore?.resourceOptions?.pageInjections?.create?.beforeBreadcrumbs || []"
|
|
6
6
|
:is="getCustomComponent(c)"
|
|
7
7
|
:record="coreStore.record"
|
|
8
|
-
:
|
|
8
|
+
:resource="coreStore.resource"
|
|
9
9
|
:adminUser="coreStore.adminUser"
|
|
10
10
|
/>
|
|
11
11
|
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
v-for="c in coreStore?.resourceOptions?.pageInjections?.create?.afterBreadcrumbs || []"
|
|
35
35
|
:is="getCustomComponent(c)"
|
|
36
36
|
:record="coreStore.record"
|
|
37
|
-
:
|
|
37
|
+
:resource="coreStore.resource"
|
|
38
38
|
:adminUser="coreStore.adminUser"
|
|
39
39
|
/>
|
|
40
40
|
|
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
v-for="c in coreStore?.resourceOptions?.pageInjections?.create?.bottom || []"
|
|
57
57
|
:is="getCustomComponent(c)"
|
|
58
58
|
:record="coreStore.record"
|
|
59
|
-
:
|
|
59
|
+
:resource="coreStore.resource"
|
|
60
60
|
:adminUser="coreStore.adminUser"
|
|
61
61
|
/>
|
|
62
62
|
|
|
@@ -101,8 +101,8 @@ onMounted(async () => {
|
|
|
101
101
|
resourceId: route.params.resourceId
|
|
102
102
|
});
|
|
103
103
|
createComponentsPerColumn = coreStore.resourceColumns.reduce((acc, column) => {
|
|
104
|
-
if (column.
|
|
105
|
-
acc[column.name] = getCustomComponent(column.
|
|
104
|
+
if (column.components?.create) {
|
|
105
|
+
acc[column.name] = getCustomComponent(column.components.create);
|
|
106
106
|
}
|
|
107
107
|
return acc;
|
|
108
108
|
}, {});
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
<component
|
|
4
4
|
v-for="c in coreStore?.resourceOptions?.pageInjections?.edit?.beforeBreadcrumbs || []"
|
|
5
5
|
:is="getCustomComponent(c)"
|
|
6
|
-
:record="
|
|
7
|
-
:
|
|
6
|
+
:record="editableRecord"
|
|
7
|
+
:resource="coreStore.resource"
|
|
8
8
|
:adminUser="coreStore.adminUser"
|
|
9
9
|
/>
|
|
10
10
|
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
v-for="c in coreStore?.resourceOptions?.pageInjections?.edit?.afterBreadcrumbs || []"
|
|
31
31
|
:is="getCustomComponent(c)"
|
|
32
32
|
:record="coreStore.record"
|
|
33
|
-
:
|
|
33
|
+
:resource="coreStore.resource"
|
|
34
34
|
:adminUser="coreStore.adminUser"
|
|
35
35
|
/>
|
|
36
36
|
|
|
@@ -39,7 +39,8 @@
|
|
|
39
39
|
<ResourceForm
|
|
40
40
|
v-else
|
|
41
41
|
:record="editableRecord"
|
|
42
|
-
:
|
|
42
|
+
:resource="coreStore.resource"
|
|
43
|
+
:adminUser="coreStore.adminUser"
|
|
43
44
|
@update:record="onUpdateRecord"
|
|
44
45
|
@update:isValid="isValid = $event"
|
|
45
46
|
:validating="validating"
|
|
@@ -51,7 +52,7 @@
|
|
|
51
52
|
v-for="c in coreStore?.resourceOptions?.pageInjections?.edit?.bottom || []"
|
|
52
53
|
:is="getCustomComponent(c)"
|
|
53
54
|
:record="coreStore.record"
|
|
54
|
-
:
|
|
55
|
+
:resource="coreStore.resource"
|
|
55
56
|
:adminUser="coreStore.adminUser"
|
|
56
57
|
/>
|
|
57
58
|
|
|
@@ -116,8 +117,8 @@ onMounted(async () => {
|
|
|
116
117
|
checkAcessByAllowedActions(coreStore.resourceOptions.allowedActions,'edit');
|
|
117
118
|
|
|
118
119
|
editComponentsPerColumn = coreStore.resourceColumns.reduce((acc, column) => {
|
|
119
|
-
if (column.
|
|
120
|
-
acc[column.name] = getCustomComponent(column.
|
|
120
|
+
if (column.components?.edit) {
|
|
121
|
+
acc[column.name] = getCustomComponent(column.components.edit);
|
|
121
122
|
}
|
|
122
123
|
return acc;
|
|
123
124
|
}, {});
|