adminforth 1.2.95 → 1.2.97
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/dataConnectors/baseConnector.ts +26 -14
- package/dataConnectors/clickhouse.ts +57 -37
- package/dataConnectors/mongo.ts +45 -14
- package/dataConnectors/postgres.ts +53 -36
- package/dataConnectors/sqlite.ts +37 -35
- package/dist/dataConnectors/baseConnector.js +16 -7
- package/dist/dataConnectors/clickhouse.js +58 -48
- package/dist/dataConnectors/mongo.js +29 -12
- package/dist/dataConnectors/postgres.js +62 -52
- package/dist/dataConnectors/sqlite.js +52 -45
- package/dist/index.js +5 -1
- package/dist/modules/operationalResource.js +88 -0
- package/dist/modules/restApi.js +27 -29
- package/dist/spa/index.html +4 -4
- package/dist/spa/package-lock.json +421 -6
- package/dist/spa/package.json +10 -2
- package/dist/spa/src/App.vue +236 -94
- package/dist/spa/src/assets/base.css +2 -0
- package/dist/spa/src/assets/logo.svg +19 -1
- package/dist/spa/src/components/AcceptModal.vue +3 -10
- package/dist/spa/src/components/Breadcrumbs.vue +3 -2
- package/dist/spa/src/components/BreadcrumbsWithButtons.vue +1 -1
- package/dist/spa/src/components/CustomDatePicker.vue +25 -10
- package/dist/spa/src/components/CustomDateRangePicker.vue +14 -5
- package/dist/spa/src/components/Dropdown.vue +19 -5
- package/dist/spa/src/components/Filters.vue +103 -38
- package/dist/spa/src/components/MenuLink.vue +3 -3
- package/dist/spa/src/components/ResourceForm.vue +205 -116
- package/dist/spa/{spa/src → src}/components/ResourceListTable.vue +6 -3
- package/dist/spa/{spa/src → src}/components/Toast.vue +9 -3
- package/dist/spa/src/components/ValueRenderer.vue +30 -8
- package/dist/spa/src/index.scss +2 -1
- package/dist/spa/src/main.ts +2 -2
- package/dist/spa/src/router/index.ts +29 -19
- package/dist/spa/src/stores/core.ts +65 -59
- package/dist/spa/src/stores/modal.ts +13 -3
- package/dist/spa/src/types/AdminForthConfig.ts +1477 -0
- package/dist/spa/src/types/FrontendAPI.ts +121 -0
- package/dist/spa/src/utils.ts +69 -10
- package/dist/spa/src/views/CreateView.vue +69 -16
- package/dist/spa/src/views/EditView.vue +68 -14
- package/dist/spa/src/views/ListView.vue +100 -371
- package/dist/spa/src/views/LoginView.vue +67 -29
- package/dist/spa/src/views/ResourceParent.vue +1 -2
- package/dist/spa/src/views/ShowView.vue +116 -25
- package/dist/spa/tailwind.config.js +8 -3
- package/dist/spa/vite.config.ts +13 -0
- package/dist/types/AdminForthConfig.js +37 -0
- package/index.ts +12 -3
- package/modules/operationalResource.ts +97 -0
- package/modules/restApi.ts +31 -31
- package/package.json +1 -1
- package/types/AdminForthConfig.ts +91 -8
- package/dist/spa/.vscode/extensions.json +0 -6
- package/dist/spa/public/favicon.ico +0 -0
- package/dist/spa/spa/.eslintrc.cjs +0 -14
- package/dist/spa/spa/.vscode/extensions.json +0 -6
- package/dist/spa/spa/README.md +0 -39
- package/dist/spa/spa/env.d.ts +0 -1
- package/dist/spa/spa/index.html +0 -23
- package/dist/spa/spa/package-lock.json +0 -4573
- package/dist/spa/spa/package.json +0 -49
- package/dist/spa/spa/postcss.config.js +0 -6
- package/dist/spa/spa/public/favicon.ico +0 -0
- package/dist/spa/spa/src/App.vue +0 -314
- package/dist/spa/spa/src/assets/base.css +0 -0
- package/dist/spa/spa/src/assets/logo.svg +0 -19
- package/dist/spa/spa/src/components/AcceptModal.vue +0 -45
- package/dist/spa/spa/src/components/Breadcrumbs.vue +0 -40
- package/dist/spa/spa/src/components/BreadcrumbsWithButtons.vue +0 -26
- package/dist/spa/spa/src/components/CustomDatePicker.vue +0 -174
- package/dist/spa/spa/src/components/CustomDateRangePicker.vue +0 -218
- package/dist/spa/spa/src/components/Dropdown.vue +0 -168
- package/dist/spa/spa/src/components/Filters.vue +0 -222
- package/dist/spa/spa/src/components/HelloWorld.vue +0 -17
- package/dist/spa/spa/src/components/MenuLink.vue +0 -24
- package/dist/spa/spa/src/components/ResourceForm.vue +0 -289
- package/dist/spa/spa/src/components/SingleSkeletLoader.vue +0 -13
- package/dist/spa/spa/src/components/ValueRenderer.vue +0 -66
- package/dist/spa/spa/src/components/icons/IconCalendar.vue +0 -5
- package/dist/spa/spa/src/components/icons/IconCommunity.vue +0 -7
- package/dist/spa/spa/src/components/icons/IconDocumentation.vue +0 -7
- package/dist/spa/spa/src/components/icons/IconEcosystem.vue +0 -7
- package/dist/spa/spa/src/components/icons/IconSupport.vue +0 -7
- package/dist/spa/spa/src/components/icons/IconTime.vue +0 -5
- package/dist/spa/spa/src/components/icons/IconTooling.vue +0 -19
- package/dist/spa/spa/src/index.scss +0 -27
- package/dist/spa/spa/src/main.ts +0 -18
- package/dist/spa/spa/src/router/index.ts +0 -63
- package/dist/spa/spa/src/stores/core.ts +0 -144
- package/dist/spa/spa/src/stores/modal.ts +0 -48
- package/dist/spa/spa/src/utils.ts +0 -103
- package/dist/spa/spa/src/views/CreateView.vue +0 -156
- package/dist/spa/spa/src/views/EditView.vue +0 -157
- package/dist/spa/spa/src/views/HomeView.vue +0 -8
- package/dist/spa/spa/src/views/ListView.vue +0 -258
- package/dist/spa/spa/src/views/LoginView.vue +0 -143
- package/dist/spa/spa/src/views/ResourceParent.vue +0 -17
- package/dist/spa/spa/src/views/ShowView.vue +0 -184
- package/dist/spa/spa/tailwind.config.js +0 -17
- package/dist/spa/spa/tsconfig.app.json +0 -14
- package/dist/spa/spa/tsconfig.json +0 -11
- package/dist/spa/spa/tsconfig.node.json +0 -19
- package/dist/spa/spa/vite.config.ts +0 -56
- package/dist/spa/src/views/HomeView.vue +0 -8
- package/dist/types.js +0 -30
- /package/dist/spa/{spa/public → public}/assets/favicon.png +0 -0
- /package/dist/spa/{spa/src → src}/components/CustomRangePicker.vue +0 -0
- /package/dist/spa/{spa/src → src}/components/SkeleteLoader.vue +0 -0
- /package/dist/spa/{spa/src → src}/composables/useFrontendApi.ts +0 -0
- /package/dist/spa/{spa/src → src}/composables/useStores.ts +0 -0
- /package/dist/spa/{spa/src → src}/spa_types/core.ts +0 -0
- /package/dist/spa/{spa/src → src}/stores/filters.ts +0 -0
- /package/dist/spa/{spa/src → src}/stores/toast.ts +0 -0
- /package/dist/spa/{spa/src → src}/stores/user.ts +0 -0
|
@@ -1,113 +1,137 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div>
|
|
3
|
-
|
|
2
|
+
<div class="rounded-default">
|
|
4
3
|
<div
|
|
5
|
-
class="relative shadow-
|
|
4
|
+
class="relative shadow-resourseFormShadow dark:shadow-darkResourseFormShadow sm:rounded-lg dark:shadow-2xl rounded-default"
|
|
6
5
|
>
|
|
7
6
|
<form autocomplete="off" @submit.prevent>
|
|
8
|
-
<table class="w-full text-sm text-left rtl:text-right text-gray-500 dark:text-gray-400">
|
|
9
|
-
<thead class="text-xs text-gray-700 uppercase bg-
|
|
7
|
+
<table class="w-full text-sm text-left rtl:text-right text-gray-500 dark:text-gray-400 ">
|
|
8
|
+
<thead class="text-xs text-gray-700 uppercase bg-lightFormHeading dark:bg-gray-700 dark:text-gray-400 block md:table-row-group">
|
|
10
9
|
<tr>
|
|
11
|
-
<th scope="col" class="px-6 py-3">
|
|
10
|
+
<th scope="col" class="px-6 py-3 hidden md:table-cell">
|
|
12
11
|
Field
|
|
13
12
|
</th>
|
|
14
|
-
<th scope="col" class="px-6 py-3 w-
|
|
13
|
+
<th scope="col" class="px-6 py-3 w-5/6 hidden md:table-cell">
|
|
15
14
|
Value
|
|
16
15
|
</th>
|
|
16
|
+
|
|
17
|
+
|
|
17
18
|
</tr>
|
|
18
19
|
</thead>
|
|
19
20
|
<tbody>
|
|
20
21
|
<tr v-for="column, i in editableColumns" :key="column.name"
|
|
21
|
-
|
|
22
|
+
v-if="currentValues !== null"
|
|
23
|
+
class="bg-ligftForm dark:bg-gray-800 border-b dark:border-gray-700 block md:table-row"
|
|
22
24
|
>
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
25
|
+
<td class="px-6 py-4 sm:pb-0 whitespace-nowrap flex items-center block md:table-cell"> <!--align-top-->
|
|
26
|
+
{{ column.label }}
|
|
27
|
+
<span :data-tooltip-target="`tooltip-show-${i}`" class="ml-1 relative inline-block">
|
|
28
|
+
<IconExclamationCircleSolid v-if="column.required[mode]" class="w-4 h-4"
|
|
29
|
+
:class="(columnError(column) && validating) ? 'text-red-500 dark:text-red-400' : 'text-gray-400 dark:text-gray-500'"
|
|
30
|
+
/>
|
|
31
|
+
</span>
|
|
32
|
+
<div :id="`tooltip-show-${i}`"
|
|
33
|
+
role="tooltip"
|
|
34
|
+
class="ml-1 absolute z-10 invisible inline-block px-3 py-2 text-sm font-medium text-white transition-opacity duration-300 bg-gray-900 rounded-lg shadow-sm opacity-0 tooltip dark:bg-gray-700">
|
|
35
|
+
Required field
|
|
36
|
+
<div class="tooltip-arrow" data-popper-arrow></div>
|
|
37
|
+
</div>
|
|
38
|
+
</td>
|
|
39
|
+
<td class="px-6 py-4 whitespace-nowrap whitespace-pre-wrap relative block md:table-cell">
|
|
40
|
+
<template v-if="column?.components?.[props.source]?.file">
|
|
41
|
+
<component
|
|
42
|
+
:is="getCustomComponent(column.components[props.source])"
|
|
43
|
+
:column="column"
|
|
44
|
+
:value="currentValues[column.name]"
|
|
45
|
+
@update:value="setCurrentValue(column.name, $event)"
|
|
46
|
+
:meta="column.components[props.source].meta"
|
|
47
|
+
:record="currentValues"
|
|
48
|
+
@update:inValidity="customComponentsInValidity[column.name] = $event"
|
|
49
|
+
@update:emptiness="customComponentsEmptiness[column.name] = $event"
|
|
50
|
+
/>
|
|
51
|
+
</template>
|
|
52
|
+
<template v-else>
|
|
53
|
+
<Dropdown
|
|
54
|
+
single
|
|
55
|
+
v-if="column.foreignResource"
|
|
56
|
+
:options="columnOptions[column.name] || []"
|
|
57
|
+
:placeholder = "columnOptions[column.name]?.length ?'Select...': 'There are no options available'"
|
|
58
|
+
:modelValue="currentValues[column.name]"
|
|
59
|
+
@update:modelValue="setCurrentValue(column.name, $event)"
|
|
60
|
+
></Dropdown>
|
|
61
|
+
<Dropdown
|
|
62
|
+
single
|
|
63
|
+
v-else-if="column.enum"
|
|
64
|
+
:options="column.enum"
|
|
65
|
+
:modelValue="currentValues[column.name]"
|
|
66
|
+
@update:modelValue="setCurrentValue(column.name, $event)"
|
|
67
|
+
/>
|
|
68
|
+
<Dropdown
|
|
69
|
+
single
|
|
70
|
+
v-else-if="column.type === 'boolean'"
|
|
71
|
+
:options="[{ label: 'Yes', value: true }, { label: 'No', value: false }, { label: 'Unset', value: null }]"
|
|
72
|
+
:modelValue="currentValues[column.name]"
|
|
73
|
+
@update:modelValue="setCurrentValue(column.name, $event)"
|
|
74
|
+
/>
|
|
75
|
+
<input
|
|
76
|
+
v-else-if="['integer'].includes(column.type)"
|
|
77
|
+
type="number"
|
|
78
|
+
step="1"
|
|
79
|
+
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-40 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
|
80
|
+
placeholder="0"
|
|
81
|
+
:value="currentValues[column.name]"
|
|
82
|
+
@input="setCurrentValue(column.name, $event.target.value)"
|
|
83
|
+
>
|
|
84
|
+
<CustomDatePicker
|
|
85
|
+
v-else-if="['datetime'].includes(column.type)"
|
|
86
|
+
:column="column"
|
|
87
|
+
:valueStart="currentValues[column.name]"
|
|
88
|
+
auto-hide
|
|
89
|
+
@update:valueStart="setCurrentValue(column.name, $event)"
|
|
90
|
+
/>
|
|
91
|
+
<input
|
|
92
|
+
v-else-if="['decimal', 'float'].includes(column.type)"
|
|
93
|
+
type="number"
|
|
94
|
+
step="0.1"
|
|
95
|
+
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-40 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
|
96
|
+
placeholder="0.0"
|
|
97
|
+
:value="currentValues[column.name]"
|
|
98
|
+
@input="setCurrentValue(column.name, $event.target.value)"
|
|
99
|
+
/>
|
|
100
|
+
<textarea
|
|
101
|
+
v-else-if="['text', 'richtext'].includes(column.type)"
|
|
102
|
+
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
|
103
|
+
placeholder="Text"
|
|
104
|
+
:value="currentValues[column.name]"
|
|
105
|
+
@input="setCurrentValue(column.name, $event.target.value)"
|
|
106
|
+
>
|
|
107
|
+
</textarea>
|
|
108
|
+
<input
|
|
109
|
+
v-else
|
|
110
|
+
:type="!column.masked || unmasked[column.name] ? 'text' : 'password'"
|
|
111
|
+
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
|
112
|
+
placeholder="Text"
|
|
113
|
+
:value="currentValues[column.name]"
|
|
114
|
+
@input="setCurrentValue(column.name, $event.target.value)"
|
|
115
|
+
autocomplete="false"
|
|
116
|
+
data-lpignore="true"
|
|
117
|
+
readonly
|
|
118
|
+
onfocus="this.removeAttribute('readonly');"
|
|
119
|
+
>
|
|
120
|
+
|
|
121
|
+
<button
|
|
122
|
+
v-if="column.masked"
|
|
123
|
+
type="button"
|
|
124
|
+
@click="unmasked[column.name] = !unmasked[column.name]"
|
|
125
|
+
class="h-6 absolute inset-y-2 top-6 right-6 flex items-center pr-2 z-index-100 focus:outline-none"
|
|
126
|
+
>
|
|
127
|
+
<IconEyeSolid class="w-6 h-6 text-gray-400" v-if="!unmasked[column.name]" />
|
|
128
|
+
<IconEyeSlashSolid class="w-6 h-6 text-gray-400" v-else />
|
|
129
|
+
</button>
|
|
130
|
+
</template>
|
|
131
|
+
<div v-if="columnError(column) && validating" class="mt-1 text-xs text-red-500 dark:text-red-400">{{ columnError(column) }}</div>
|
|
132
|
+
|
|
133
|
+
<div v-if="column.editingNote && column.editingNote[mode]" class="mt-1 text-xs text-gray-400 dark:text-gray-500">{{ column.editingNote[mode] }}</div>
|
|
134
|
+
|
|
111
135
|
</td>
|
|
112
136
|
</tr>
|
|
113
137
|
|
|
@@ -121,33 +145,55 @@
|
|
|
121
145
|
|
|
122
146
|
<script setup>
|
|
123
147
|
|
|
124
|
-
import { ref, computed, onMounted, watch } from 'vue';
|
|
125
|
-
import { useCoreStore } from '@/stores/core';
|
|
126
|
-
import Dropdown from '@/components/Dropdown.vue';
|
|
127
|
-
import { IconExclamationCircleSolid } from '@iconify-prerendered/vue-flowbite';
|
|
128
|
-
import { initFlowbite } from 'flowbite'
|
|
129
|
-
import { IconEyeSolid, IconEyeSlashSolid } from '@iconify-prerendered/vue-flowbite';
|
|
130
148
|
import CustomDatePicker from "@/components/CustomDatePicker.vue";
|
|
149
|
+
import Dropdown from '@/components/Dropdown.vue';
|
|
150
|
+
import { useCoreStore } from '@/stores/core';
|
|
151
|
+
import { callAdminForthApi, getCustomComponent } from '@/utils';
|
|
152
|
+
import { IconExclamationCircleSolid, IconEyeSlashSolid, IconEyeSolid } from '@iconify-prerendered/vue-flowbite';
|
|
153
|
+
import { computedAsync } from '@vueuse/core';
|
|
154
|
+
import { initFlowbite } from 'flowbite';
|
|
155
|
+
import { computed, onMounted, ref, watch } from 'vue';
|
|
156
|
+
import { useRouter } from 'vue-router';
|
|
157
|
+
|
|
158
|
+
const router = useRouter();
|
|
131
159
|
|
|
132
160
|
const props = defineProps({
|
|
133
161
|
loading: Boolean,
|
|
134
|
-
|
|
162
|
+
resource: Object,
|
|
135
163
|
record: Object,
|
|
136
164
|
validating: Boolean,
|
|
165
|
+
source: String,
|
|
137
166
|
});
|
|
138
167
|
|
|
139
|
-
|
|
140
168
|
const unmasked = ref({});
|
|
141
169
|
|
|
142
170
|
const mode = computed(() => props.record && Object.keys(props.record).length ? 'edit' : 'create');
|
|
143
171
|
|
|
144
172
|
const emit = defineEmits(['update:record', 'update:isValid']);
|
|
145
173
|
|
|
146
|
-
const currentValues = ref(
|
|
174
|
+
const currentValues = ref(null);
|
|
175
|
+
|
|
176
|
+
const customComponentsInValidity = ref({});
|
|
177
|
+
const customComponentsEmptiness = ref({});
|
|
178
|
+
|
|
147
179
|
|
|
148
180
|
const columnError = (column) => {
|
|
149
181
|
const val = computed(() => {
|
|
150
|
-
if (
|
|
182
|
+
if (!currentValues.value) {
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
if (customComponentsInValidity.value[column.name]) {
|
|
186
|
+
return customComponentsInValidity.value[column.name];
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (
|
|
190
|
+
column.required[mode.value] &&
|
|
191
|
+
(currentValues.value[column.name] === undefined || currentValues.value[column.name] === null || currentValues.value[column.name] === '') &&
|
|
192
|
+
// if component is custum it might tell other criteria for emptiness by emitting 'update:emptiness'
|
|
193
|
+
// components which do not emit 'update:emptiness' will have undefined value in customComponentsEmptiness
|
|
194
|
+
(customComponentsEmptiness.value[column.name] !== false)
|
|
195
|
+
|
|
196
|
+
) {
|
|
151
197
|
return 'This field is required';
|
|
152
198
|
}
|
|
153
199
|
if ( column.type === 'string' || column.type === 'text' ) {
|
|
@@ -159,39 +205,82 @@ const columnError = (column) => {
|
|
|
159
205
|
}
|
|
160
206
|
}
|
|
161
207
|
if ( ['integer', 'decimal', 'float'].includes(column.type) ) {
|
|
162
|
-
if ( column.minValue !== undefined
|
|
208
|
+
if ( column.minValue !== undefined
|
|
209
|
+
&& currentValues.value[column.name] !== null
|
|
210
|
+
&& currentValues.value[column.name] < column.minValue
|
|
211
|
+
) {
|
|
163
212
|
return `This field must be greater than ${column.minValue}`;
|
|
164
213
|
}
|
|
165
214
|
if ( column.maxValue !== undefined && currentValues.value[column.name] > column.maxValue ) {
|
|
166
215
|
return `This field must be less than ${column.maxValue}`;
|
|
167
216
|
}
|
|
168
217
|
}
|
|
218
|
+
if ( column.validation && column.validation.length ) {
|
|
219
|
+
const validationArray = column.validation;
|
|
220
|
+
for (let i = 0; i < validationArray.length; i++) {
|
|
221
|
+
if (validationArray[i].regExp) {
|
|
222
|
+
const regExp = new RegExp(validationArray[i].regExp);
|
|
223
|
+
let value = currentValues.value[column.name];
|
|
224
|
+
if (value === undefined || value === null) {
|
|
225
|
+
value = '';
|
|
226
|
+
}
|
|
227
|
+
if (!regExp.test(value)) {
|
|
228
|
+
return validationArray[i].message;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
}
|
|
169
234
|
return null;
|
|
170
235
|
});
|
|
171
236
|
return val.value;
|
|
172
237
|
};
|
|
173
238
|
|
|
174
239
|
|
|
175
|
-
|
|
176
|
-
|
|
177
240
|
const setCurrentValue = (key, value) => {
|
|
178
|
-
|
|
179
|
-
|
|
241
|
+
const col = props.resource.columns.find((column) => column.name === key);
|
|
242
|
+
if (['integer', 'float'].includes(col.type) && (value || value === 0)) {
|
|
243
|
+
currentValues.value[key] = +value;
|
|
244
|
+
} else {
|
|
245
|
+
currentValues.value[key] = value;
|
|
246
|
+
}
|
|
247
|
+
currentValues.value = { ...currentValues.value };
|
|
248
|
+
console.log('3️⃣ setCurrentValue', key, value);
|
|
180
249
|
emit('update:record', currentValues.value);
|
|
181
250
|
};
|
|
182
251
|
|
|
183
252
|
onMounted(() => {
|
|
184
|
-
Object.
|
|
185
|
-
|
|
186
|
-
});
|
|
253
|
+
currentValues.value = Object.assign({}, props.record);
|
|
254
|
+
console.log('2️⃣ currentValues', JSON.stringify(currentValues.value));
|
|
187
255
|
initFlowbite();
|
|
256
|
+
emit('update:isValid', isValid.value);
|
|
188
257
|
});
|
|
189
258
|
|
|
190
|
-
const
|
|
259
|
+
const columnOptions = computedAsync(async () => {
|
|
260
|
+
return (await Promise.all(
|
|
261
|
+
Object.values(props.resource.columns).map(async (column) => {
|
|
262
|
+
if (column.foreignResource) {
|
|
263
|
+
const list = await callAdminForthApi({
|
|
264
|
+
method: 'POST',
|
|
265
|
+
path: `/get_resource_foreign_data`,
|
|
266
|
+
body: {
|
|
267
|
+
resourceId: router.currentRoute.value.params.resourceId,
|
|
268
|
+
column: column.name,
|
|
269
|
+
limit: 1000,
|
|
270
|
+
offset: 0,
|
|
271
|
+
},
|
|
272
|
+
});
|
|
273
|
+
return { [column.name]: list.items };
|
|
274
|
+
}
|
|
275
|
+
})
|
|
276
|
+
)).reduce((acc, val) => Object.assign(acc, val), {})
|
|
277
|
+
|
|
278
|
+
}, {});
|
|
279
|
+
|
|
191
280
|
|
|
192
281
|
const editableColumns = computed(() => {
|
|
193
282
|
const mode = props.record ? 'edit' : 'create';
|
|
194
|
-
return props.
|
|
283
|
+
return props.resource?.columns?.filter(column => column.showIn.includes(mode));
|
|
195
284
|
});
|
|
196
285
|
|
|
197
286
|
const isValid = computed(() => {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<!-- table -->
|
|
3
|
-
<div class="relative overflow-x-auto shadow-listTableShadow dark:shadow-
|
|
3
|
+
<div class="relative overflow-x-auto shadow-listTableShadow dark:shadow-darkListTableShadow overflow-y-hidden rounded-default">
|
|
4
4
|
|
|
5
5
|
<!-- skelet loader -->
|
|
6
6
|
<div role="status" v-if="!resource || !resource.columns"
|
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
</thead>
|
|
56
56
|
<tbody>
|
|
57
57
|
<SkeleteLoader v-if="!rows" :columns="3" :rows="resource?.columns.length + 2"/>
|
|
58
|
-
<tr v-else-if="rows.length === 0" class="bg-lightListTable
|
|
58
|
+
<tr v-else-if="rows.length === 0" class="bg-lightListTable dark:bg-darkListTable dark:border-darkListTableBorder">
|
|
59
59
|
<td :colspan="resource?.columns.length + 2">
|
|
60
60
|
|
|
61
61
|
<div id="toast-simple"
|
|
@@ -69,7 +69,9 @@
|
|
|
69
69
|
</tr>
|
|
70
70
|
|
|
71
71
|
<tr @click="onClick($event,row)" v-else v-for="(row, rowI) in rows" :key="row.id"
|
|
72
|
-
class="bg-lightListTable
|
|
72
|
+
class="bg-lightListTable dark:bg-darkListTable border-lightListBorder dark:border-gray-700 hover:bg-lightListTableRowHover dark:hover:bg-darkListTableRowHover cursor-pointer"
|
|
73
|
+
:class="{'border-b': rowI !== rows.length - 1}"
|
|
74
|
+
>
|
|
73
75
|
<td class="w-4 p-4 cursor-default" @click="(e)=>{e.stopPropagation()}">
|
|
74
76
|
<div class="flex items center ">
|
|
75
77
|
<input
|
|
@@ -375,6 +377,7 @@ async function deleteRecord(row) {
|
|
|
375
377
|
primaryKey: row._primaryKeyValue,
|
|
376
378
|
}
|
|
377
379
|
});
|
|
380
|
+
console.log('safsa', res)
|
|
378
381
|
if (!res.error){
|
|
379
382
|
emits('update:records', true)
|
|
380
383
|
showSuccesTost('Record deleted successfully')
|
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
|
|
3
3
|
|
|
4
|
-
<div id="toast-default" class="flex items-center w-full p-4 text-gray-500
|
|
4
|
+
<div id="toast-default" class="flex items-center w-full p-4 text-gray-500 rounded-lg shadow dark:text-gray-400 dark:bg-gray-800" role="alert"
|
|
5
|
+
:class="
|
|
6
|
+
{
|
|
7
|
+
'danger': 'bg-red-100',
|
|
8
|
+
}[toast.variant]
|
|
9
|
+
"
|
|
10
|
+
>
|
|
5
11
|
<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
12
|
<svg class="w-4 h-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 18 20">
|
|
7
13
|
<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"/>
|
|
@@ -27,8 +33,8 @@
|
|
|
27
33
|
<span class="sr-only">Check icon</span>
|
|
28
34
|
</div>
|
|
29
35
|
|
|
30
|
-
<div class="ms-3 text-sm font-normal max-w-xs" v-if="toast.messageHtml" v-html="toast.messageHtml"></div>
|
|
31
|
-
<div class="ms-3 text-sm font-normal max-w-xs" v-else>{{toast.message}}</div>
|
|
36
|
+
<div class="ms-3 text-sm font-normal max-w-xs pr-2" v-if="toast.messageHtml" v-html="toast.messageHtml"></div>
|
|
37
|
+
<div class="ms-3 text-sm font-normal max-w-xs pr-2" v-else>{{toast.message}}</div>
|
|
32
38
|
<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" >
|
|
33
39
|
<svg class="w-3 h-3" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 14 14">
|
|
34
40
|
<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"/>
|
|
@@ -1,18 +1,30 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<div>
|
|
3
|
-
<span v-if="column.
|
|
4
|
-
<
|
|
3
|
+
<span @click="(e)=>{e.stopPropagation()}" v-if="column.foreignResource">
|
|
4
|
+
<RouterLink v-if="record[column.name]" class="font-medium text-blue-600 dark:text-blue-500 hover:brightness-110"
|
|
5
|
+
:to="{ name: 'resource-show', params: { resourceId: column.foreignResource.resourceId, primaryKey: record[column.name].pk } }">
|
|
6
|
+
{{ record[column.name].label }}
|
|
7
|
+
</RouterLink>
|
|
8
|
+
<div v-else>
|
|
9
|
+
<span class="text-gray-400">-</span>
|
|
10
|
+
</div>
|
|
11
|
+
</span>
|
|
12
|
+
|
|
13
|
+
<span v-else-if="column.type === 'boolean'">
|
|
14
|
+
<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>
|
|
5
15
|
<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>
|
|
6
16
|
</span>
|
|
7
17
|
<span v-else-if="column.enum">
|
|
8
|
-
{{ column.enum.find(e => e.value ===
|
|
18
|
+
{{ checkEmptyValues(column.enum.find(e => e.value === record[column.name])?.label,route.meta.type) }}
|
|
19
|
+
</span>
|
|
20
|
+
<span v-else-if="column.type === 'datetime'" class="whitespace-nowrap">
|
|
21
|
+
{{ checkEmptyValues(formatDate(record[column.name]),route.meta.type) }}
|
|
9
22
|
</span>
|
|
10
|
-
<span v-else-if="column.type === '
|
|
11
|
-
|
|
12
|
-
|
|
23
|
+
<span v-else-if="column.type === 'richtext'">
|
|
24
|
+
<div v-html="protectAgainstXSS(record[column.name])"></div>
|
|
13
25
|
</span>
|
|
14
26
|
<span v-else>
|
|
15
|
-
{{
|
|
27
|
+
{{ checkEmptyValues(record[column.name],route.meta.type) }}
|
|
16
28
|
</span>
|
|
17
29
|
</div>
|
|
18
30
|
</template>
|
|
@@ -23,19 +35,29 @@
|
|
|
23
35
|
import dayjs from 'dayjs';
|
|
24
36
|
import utc from 'dayjs/plugin/utc';
|
|
25
37
|
import timezone from 'dayjs/plugin/timezone';
|
|
38
|
+
import {checkEmptyValues} from '@/utils';
|
|
39
|
+
import { useRoute, useRouter } from 'vue-router';
|
|
40
|
+
import sanitizeHtml from 'sanitize-html';
|
|
41
|
+
|
|
26
42
|
|
|
27
43
|
import { useCoreStore } from '@/stores/core';
|
|
28
44
|
|
|
29
45
|
const coreStore = useCoreStore();
|
|
46
|
+
const route = useRoute();
|
|
47
|
+
|
|
30
48
|
|
|
31
49
|
dayjs.extend(utc);
|
|
32
50
|
dayjs.extend(timezone);
|
|
33
51
|
|
|
34
52
|
const props = defineProps({
|
|
35
53
|
column: Object,
|
|
36
|
-
|
|
54
|
+
record: Object
|
|
37
55
|
});
|
|
38
56
|
|
|
57
|
+
function protectAgainstXSS(value) {
|
|
58
|
+
return sanitizeHtml(value);
|
|
59
|
+
}
|
|
60
|
+
|
|
39
61
|
|
|
40
62
|
function formatDate(date) {
|
|
41
63
|
if (!date) return '';
|
package/dist/spa/src/index.scss
CHANGED
package/dist/spa/src/main.ts
CHANGED
|
@@ -6,10 +6,10 @@ import { createPinia } from 'pinia'
|
|
|
6
6
|
import App from './App.vue'
|
|
7
7
|
import router from './router'
|
|
8
8
|
|
|
9
|
-
const app = createApp(App)
|
|
9
|
+
export const app = createApp(App)
|
|
10
10
|
/* IMPORTANT:ADMINFORTH COMPONENT REGISTRATIONS */
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
|
|
13
13
|
app.use(createPinia())
|
|
14
14
|
app.use(router)
|
|
15
15
|
|
|
@@ -1,23 +1,23 @@
|
|
|
1
1
|
import { createRouter, createWebHistory } from 'vue-router'
|
|
2
|
-
import HomeView from '../views/HomeView.vue'
|
|
3
2
|
import ResourceParent from '@/views/ResourceParent.vue'
|
|
4
|
-
import
|
|
5
|
-
|
|
6
|
-
import EditView from '@/views/EditView.vue'
|
|
7
|
-
import CreateView from '@/views/CreateView.vue'
|
|
3
|
+
import { useUserStore } from '@/stores/user'
|
|
4
|
+
/* IMPORTANT:ADMINFORTH ROUTES IMPORTS */
|
|
8
5
|
|
|
9
6
|
const router = createRouter({
|
|
10
7
|
history: createWebHistory(import.meta.env.BASE_URL),
|
|
11
8
|
routes: [
|
|
12
|
-
{
|
|
13
|
-
path: '/',
|
|
14
|
-
name: 'home',
|
|
15
|
-
component: HomeView
|
|
16
|
-
},
|
|
17
9
|
{
|
|
18
10
|
path: '/login',
|
|
19
11
|
name: 'login',
|
|
20
|
-
component: () => import('@/views/LoginView.vue')
|
|
12
|
+
component: () => import('@/views/LoginView.vue'),
|
|
13
|
+
meta: { title: 'login' },
|
|
14
|
+
beforeEnter: async (to, from, next) => {
|
|
15
|
+
if(localStorage.getItem('isAuthorized') === 'true'){
|
|
16
|
+
next({name: 'home'})
|
|
17
|
+
} else {
|
|
18
|
+
next()
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
21
|
},
|
|
22
22
|
{
|
|
23
23
|
path: '/resource/:resourceId',
|
|
@@ -26,23 +26,29 @@ const router = createRouter({
|
|
|
26
26
|
children: [
|
|
27
27
|
{
|
|
28
28
|
path: '',
|
|
29
|
-
component: ListView,
|
|
30
|
-
name: 'resource-list'
|
|
29
|
+
component: () => import('@/views/ListView.vue'),
|
|
30
|
+
name: 'resource-list',
|
|
31
|
+
meta: { title: 'list',type: 'list' }
|
|
31
32
|
},
|
|
32
33
|
{
|
|
33
34
|
path: 'show/:primaryKey',
|
|
34
|
-
component: ShowView,
|
|
35
|
-
name: 'resource-show'
|
|
35
|
+
component: () => import('@/views/ShowView.vue'),
|
|
36
|
+
name: 'resource-show',
|
|
37
|
+
meta: { title: 'show', type: 'show'}
|
|
38
|
+
|
|
36
39
|
},
|
|
37
40
|
{
|
|
38
41
|
path: 'edit/:primaryKey',
|
|
39
|
-
component: EditView,
|
|
40
|
-
name: 'resource-edit'
|
|
42
|
+
component: () => import('@/views/EditView.vue'),
|
|
43
|
+
name: 'resource-edit',
|
|
44
|
+
meta: { title: 'edit', type: 'edit'}
|
|
41
45
|
},
|
|
42
46
|
{
|
|
43
47
|
path: 'create',
|
|
44
|
-
component: CreateView,
|
|
45
|
-
name: 'resource-create'
|
|
48
|
+
component: () => import('@/views/CreateView.vue'),
|
|
49
|
+
name: 'resource-create',
|
|
50
|
+
meta: { title: 'create', type: 'create'}
|
|
51
|
+
|
|
46
52
|
},
|
|
47
53
|
]
|
|
48
54
|
},
|
|
@@ -50,4 +56,8 @@ const router = createRouter({
|
|
|
50
56
|
]
|
|
51
57
|
})
|
|
52
58
|
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
|
|
53
63
|
export default router
|