adminforth 1.0.25 → 1.0.27
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/postgres.ts +3 -6
- package/dist/dataConnectors/postgres.js +3 -5
- package/dist/index.js +16 -0
- package/dist/spa/.eslintrc.cjs +14 -0
- package/dist/spa/.vscode/extensions.json +6 -0
- package/dist/spa/README.md +39 -0
- package/dist/spa/env.d.ts +1 -0
- package/dist/spa/index.html +23 -0
- package/dist/spa/package-lock.json +4158 -0
- package/dist/spa/package.json +41 -0
- package/dist/spa/postcss.config.js +6 -0
- package/dist/spa/public/favicon.ico +0 -0
- package/dist/spa/src/App.vue +172 -0
- package/dist/spa/src/assets/base.css +0 -0
- package/dist/spa/src/assets/logo.svg +1 -0
- package/dist/spa/src/components/AcceptModal.vue +52 -0
- package/dist/spa/src/components/Breadcrumbs.vue +40 -0
- package/dist/spa/src/components/BreadcrumbsWithButtons.vue +26 -0
- package/dist/spa/src/components/CustomDatePicker.vue +161 -0
- package/dist/spa/src/components/CustomDateRangePicker.vue +209 -0
- package/dist/spa/src/components/Dropdown.vue +154 -0
- package/dist/spa/src/components/Filters.vue +157 -0
- package/dist/spa/src/components/HelloWorld.vue +17 -0
- package/dist/spa/src/components/MenuLink.vue +24 -0
- package/dist/spa/src/components/ResourceForm.vue +205 -0
- package/dist/spa/src/components/SingleSkeletLoader.vue +13 -0
- package/dist/spa/src/components/ValueRenderer.vue +44 -0
- package/dist/spa/src/components/icons/IconCalendar.vue +5 -0
- package/dist/spa/src/components/icons/IconCommunity.vue +7 -0
- package/dist/spa/src/components/icons/IconDocumentation.vue +7 -0
- package/dist/spa/src/components/icons/IconEcosystem.vue +7 -0
- package/dist/spa/src/components/icons/IconSupport.vue +7 -0
- package/dist/spa/src/components/icons/IconTime.vue +5 -0
- package/dist/spa/src/components/icons/IconTooling.vue +19 -0
- package/dist/spa/src/index.scss +26 -0
- package/dist/spa/src/main.ts +18 -0
- package/dist/spa/src/router/index.ts +53 -0
- package/dist/spa/src/stores/core.ts +138 -0
- package/dist/spa/src/stores/modal.ts +38 -0
- package/dist/spa/src/utils.ts +44 -0
- package/dist/spa/src/views/CreateView.vue +103 -0
- package/dist/spa/src/views/EditView.vue +103 -0
- package/dist/spa/src/views/HomeView.vue +8 -0
- package/dist/spa/src/views/ListView.vue +529 -0
- package/dist/spa/src/views/LoginView.vue +122 -0
- package/dist/spa/src/views/ResourceParent.vue +18 -0
- package/dist/spa/src/views/ShowView.vue +93 -0
- package/dist/spa/tailwind.config.js +12 -0
- package/dist/spa/tsconfig.app.json +14 -0
- package/dist/spa/tsconfig.json +11 -0
- package/dist/spa/tsconfig.node.json +19 -0
- package/dist/spa/vite.config.ts +43 -0
- package/index.ts +31 -2
- package/package.json +2 -2
- package/spa/src/views/ListView.vue +7 -6
- package/dist/spa/src/main.js +0 -16
- package/dist/spa/src/router/index.js +0 -79
- package/dist/spa/src/stores/core.js +0 -154
- package/dist/spa/src/stores/modal.js +0 -35
- package/dist/spa/src/utils.js +0 -59
- package/dist/spa/vite.config.js +0 -44
- package/dist/spa_tmp/src/custom/custom/vueUses.js +0 -10
- package/dist/spa_tmp/src/main.js +0 -29
- package/dist/spa_tmp/src/router/index.js +0 -83
- package/dist/spa_tmp/src/stores/core.js +0 -150
- package/dist/spa_tmp/src/stores/modal.js +0 -35
- package/dist/spa_tmp/src/utils.js +0 -59
- package/dist/spa_tmp/vite.config.js +0 -43
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div>
|
|
3
|
+
|
|
4
|
+
<div
|
|
5
|
+
class="relative shadow-md sm:rounded-lg dark:shadow-2xl"
|
|
6
|
+
>
|
|
7
|
+
<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-gray-50 dark:bg-gray-700 dark:text-gray-400">
|
|
10
|
+
<tr>
|
|
11
|
+
<th scope="col" class="px-6 py-3">
|
|
12
|
+
Field
|
|
13
|
+
</th>
|
|
14
|
+
<th scope="col" class="px-6 py-3 w-4/6">
|
|
15
|
+
Value
|
|
16
|
+
</th>
|
|
17
|
+
</tr>
|
|
18
|
+
</thead>
|
|
19
|
+
<tbody>
|
|
20
|
+
<tr v-for="column, i in editableColumns" :key="column.name"
|
|
21
|
+
class="odd:bg-white odd:dark:bg-gray-900 even:bg-gray-50 even:dark:bg-gray-800 border-b dark:border-gray-700"
|
|
22
|
+
>
|
|
23
|
+
<td class="px-6 py-4 whitespace-nowrap">
|
|
24
|
+
{{ column.label }}
|
|
25
|
+
<span :data-tooltip-target="`tooltip-show-${i}`" class="relative inline-block">
|
|
26
|
+
<IconExclamationCircleSolid v-if="column.required[mode]" class="w-4 h-4"
|
|
27
|
+
:class="(columnError(column) && validating) ? 'text-red-500 dark:text-red-400' : 'text-gray-400 dark:text-gray-500'"
|
|
28
|
+
/>
|
|
29
|
+
</span>
|
|
30
|
+
<div :id="`tooltip-show-${i}`"
|
|
31
|
+
role="tooltip" class="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">
|
|
32
|
+
Required field
|
|
33
|
+
<div class="tooltip-arrow" data-popper-arrow></div>
|
|
34
|
+
</div>
|
|
35
|
+
</td>
|
|
36
|
+
<td class="px-6 py-4 whitespace-nowrap whitespace-pre-wrap relative">
|
|
37
|
+
<Dropdown
|
|
38
|
+
single
|
|
39
|
+
v-if="column.enum"
|
|
40
|
+
:options="column.enum"
|
|
41
|
+
:modelValue="currentValues[column.name]"
|
|
42
|
+
@update:modelValue="setCurrentValue(column.name, $event)"
|
|
43
|
+
/>
|
|
44
|
+
<Dropdown
|
|
45
|
+
single
|
|
46
|
+
v-else-if="column.type === 'boolean'"
|
|
47
|
+
:options="[{ label: 'Yes', value: true }, { label: 'No', value: false }, { label: 'Unset', value: null }]"
|
|
48
|
+
:modelValue="currentValues[column.name]"
|
|
49
|
+
@update:modelValue="setCurrentValue(column.name, $event)"
|
|
50
|
+
/>
|
|
51
|
+
<input
|
|
52
|
+
v-else-if="['integer'].includes(column.type)"
|
|
53
|
+
type="number"
|
|
54
|
+
step="1"
|
|
55
|
+
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"
|
|
56
|
+
placeholder="0"
|
|
57
|
+
:value="currentValues[column.name]"
|
|
58
|
+
@input="setCurrentValue(column.name, $event.target.value)"
|
|
59
|
+
>
|
|
60
|
+
<CustomDatePicker
|
|
61
|
+
v-else-if="['datetime'].includes(column.type)"
|
|
62
|
+
:column="column"
|
|
63
|
+
:valueStart="currentValues[column.name]"
|
|
64
|
+
@update:valueStart="setCurrentValue(column.name, $event)"
|
|
65
|
+
/>
|
|
66
|
+
<input
|
|
67
|
+
v-else-if="['decimal', 'float'].includes(column.type)"
|
|
68
|
+
type="number"
|
|
69
|
+
step="0.1"
|
|
70
|
+
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"
|
|
71
|
+
placeholder="0.0"
|
|
72
|
+
:value="currentValues[column.name]"
|
|
73
|
+
@input="setCurrentValue(column.name, $event.target.value)"
|
|
74
|
+
/>
|
|
75
|
+
<textarea
|
|
76
|
+
v-else-if="['text'].includes(column.type)"
|
|
77
|
+
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"
|
|
78
|
+
placeholder="Text"
|
|
79
|
+
:value="currentValues[column.name]"
|
|
80
|
+
@input="setCurrentValue(column.name, $event.target.value)"
|
|
81
|
+
>
|
|
82
|
+
</textarea>
|
|
83
|
+
<input
|
|
84
|
+
v-else
|
|
85
|
+
:type="!column.masked || unmasked[column.name] ? 'text' : 'password'"
|
|
86
|
+
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"
|
|
87
|
+
placeholder="Text"
|
|
88
|
+
:value="currentValues[column.name]"
|
|
89
|
+
@input="setCurrentValue(column.name, $event.target.value)"
|
|
90
|
+
autocomplete="false"
|
|
91
|
+
data-lpignore="true"
|
|
92
|
+
readonly
|
|
93
|
+
onfocus="this.removeAttribute('readonly');"
|
|
94
|
+
>
|
|
95
|
+
|
|
96
|
+
<button
|
|
97
|
+
v-if="column.masked"
|
|
98
|
+
type="button"
|
|
99
|
+
@click="unmasked[column.name] = !unmasked[column.name]"
|
|
100
|
+
class="h-6 absolute inset-y-2 top-6 right-6 flex items-center pr-2 z-index-100 focus:outline-none"
|
|
101
|
+
>
|
|
102
|
+
<IconEyeSolid class="w-6 h-6 text-gray-400" v-if="!unmasked[column.name]" />
|
|
103
|
+
<IconEyeSlashSolid class="w-6 h-6 text-gray-400" v-else />
|
|
104
|
+
</button>
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
<div v-if="columnError(column) && validating" class="mt-1 text-xs text-red-500 dark:text-red-400">{{ columnError(column) }}</div>
|
|
109
|
+
|
|
110
|
+
<div v-if="column.editingNote && column.editingNote[mode]" class="mt-1 text-xs text-gray-400 dark:text-gray-500">{{ column.editingNote[mode] }}</div>
|
|
111
|
+
</td>
|
|
112
|
+
</tr>
|
|
113
|
+
|
|
114
|
+
</tbody>
|
|
115
|
+
</table>
|
|
116
|
+
</form>
|
|
117
|
+
</div>
|
|
118
|
+
</div>
|
|
119
|
+
|
|
120
|
+
</template>
|
|
121
|
+
|
|
122
|
+
<script setup>
|
|
123
|
+
|
|
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
|
+
import CustomDatePicker from "@/components/CustomDatePicker.vue";
|
|
131
|
+
|
|
132
|
+
const props = defineProps({
|
|
133
|
+
loading: Boolean,
|
|
134
|
+
resourceColumns: Object,
|
|
135
|
+
record: Object,
|
|
136
|
+
validating: Boolean,
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
const unmasked = ref({});
|
|
141
|
+
|
|
142
|
+
const mode = computed(() => props.record && Object.keys(props.record).length ? 'edit' : 'create');
|
|
143
|
+
|
|
144
|
+
const emit = defineEmits(['update:record', 'update:isValid']);
|
|
145
|
+
|
|
146
|
+
const currentValues = ref({});
|
|
147
|
+
|
|
148
|
+
const columnError = (column) => {
|
|
149
|
+
const val = computed(() => {
|
|
150
|
+
if ( column.required[mode.value] && (currentValues.value[column.name] === undefined || currentValues.value[column.name] === null || currentValues.value[column.name] === '') ) {
|
|
151
|
+
return 'This field is required';
|
|
152
|
+
}
|
|
153
|
+
if ( column.type === 'string' || column.type === 'text' ) {
|
|
154
|
+
if ( column.maxLength && currentValues.value[column.name]?.length > column.maxLength ) {
|
|
155
|
+
return `This field must be shorter than ${column.maxLength} characters`;
|
|
156
|
+
}
|
|
157
|
+
if ( column.minLength && currentValues.value[column.name]?.length < column.minLength ) {
|
|
158
|
+
return `This field must be longer than ${column.minLength} characters`;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
if ( ['integer', 'decimal', 'float'].includes(column.type) ) {
|
|
162
|
+
if ( column.minValue !== undefined && currentValues.value[column.name] < column.minValue ) {
|
|
163
|
+
return `This field must be greater than ${column.minValue}`;
|
|
164
|
+
}
|
|
165
|
+
if ( column.maxValue !== undefined && currentValues.value[column.name] > column.maxValue ) {
|
|
166
|
+
return `This field must be less than ${column.maxValue}`;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return null;
|
|
170
|
+
});
|
|
171
|
+
return val.value;
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
const setCurrentValue = (key, value) => {
|
|
178
|
+
currentValues.value[key] = value;
|
|
179
|
+
|
|
180
|
+
emit('update:record', currentValues.value);
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
onMounted(() => {
|
|
184
|
+
Object.keys(props.record).forEach((key) => {
|
|
185
|
+
currentValues.value[key] = props.record[key];
|
|
186
|
+
});
|
|
187
|
+
initFlowbite();
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
const coreStore = useCoreStore();
|
|
191
|
+
|
|
192
|
+
const editableColumns = computed(() => {
|
|
193
|
+
const mode = props.record ? 'edit' : 'create';
|
|
194
|
+
return props.resourceColumns?.filter(column => column.showIn.includes(mode));
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
const isValid = computed(() => {
|
|
198
|
+
return editableColumns.value?.every(column => !columnError(column));
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
watch(() => isValid.value, (value) => {
|
|
202
|
+
emit('update:isValid', value);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
</script>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div
|
|
3
|
+
role="status" class="max-w-sm animate-pulse"
|
|
4
|
+
>
|
|
5
|
+
<div class="h-2.5 bg-gray-200 rounded-full dark:bg-gray-700 w-48 mb-4"></div>
|
|
6
|
+
<div class="h-2 bg-gray-200 rounded-full dark:bg-gray-700 max-w-[360px] mb-2.5"></div>
|
|
7
|
+
<div class="h-2 bg-gray-200 rounded-full dark:bg-gray-700 mb-2.5"></div>
|
|
8
|
+
<div class="h-2 bg-gray-200 rounded-full dark:bg-gray-700 max-w-[330px] mb-2.5"></div>
|
|
9
|
+
<div class="h-2 bg-gray-200 rounded-full dark:bg-gray-700 max-w-[300px] mb-2.5"></div>
|
|
10
|
+
<div class="h-2 bg-gray-200 rounded-full dark:bg-gray-700 max-w-[360px]"></div>
|
|
11
|
+
<span class="sr-only">Loading...</span>
|
|
12
|
+
</div>
|
|
13
|
+
</template>
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div>
|
|
3
|
+
<span v-if="column.type === 'boolean'">
|
|
4
|
+
<span v-if="row[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
|
+
<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
|
+
</span>
|
|
7
|
+
<span v-else-if="column.enum">
|
|
8
|
+
{{ column.enum.find(e => e.value === row[column.name])?.label }}
|
|
9
|
+
</span>
|
|
10
|
+
<span v-else-if="column.type === 'datetime'">
|
|
11
|
+
{{ formatDate(row[column.name]) }}
|
|
12
|
+
|
|
13
|
+
</span>
|
|
14
|
+
<span v-else>
|
|
15
|
+
{{ row[column.name] }}
|
|
16
|
+
</span>
|
|
17
|
+
</div>
|
|
18
|
+
</template>
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
<script setup>
|
|
22
|
+
|
|
23
|
+
import dayjs from 'dayjs';
|
|
24
|
+
import utc from 'dayjs/plugin/utc';
|
|
25
|
+
import timezone from 'dayjs/plugin/timezone';
|
|
26
|
+
|
|
27
|
+
import { useCoreStore } from '@/stores/core';
|
|
28
|
+
|
|
29
|
+
const coreStore = useCoreStore();
|
|
30
|
+
|
|
31
|
+
dayjs.extend(utc);
|
|
32
|
+
dayjs.extend(timezone);
|
|
33
|
+
|
|
34
|
+
const props = defineProps({
|
|
35
|
+
column: Object,
|
|
36
|
+
row: Object
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
function formatDate(date) {
|
|
41
|
+
if (!date) return '';
|
|
42
|
+
return dayjs.utc(date).local().format(coreStore.config?.datesFormat || 'YYYY-MM-DD HH:mm:ss');
|
|
43
|
+
}
|
|
44
|
+
</script>
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 20 20">
|
|
3
|
+
<path d="M20 4a2 2 0 0 0-2-2h-2V1a1 1 0 0 0-2 0v1h-3V1a1 1 0 0 0-2 0v1H6V1a1 1 0 0 0-2 0v1H2a2 2 0 0 0-2 2v2h20V4ZM0 18a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V8H0v10Zm5-8h10a1 1 0 0 1 0 2H5a1 1 0 0 1 0-2Z"/>
|
|
4
|
+
</svg>
|
|
5
|
+
</template>
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor">
|
|
3
|
+
<path
|
|
4
|
+
d="M15 4a1 1 0 1 0 0 2V4zm0 11v-1a1 1 0 0 0-1 1h1zm0 4l-.707.707A1 1 0 0 0 16 19h-1zm-4-4l.707-.707A1 1 0 0 0 11 14v1zm-4.707-1.293a1 1 0 0 0-1.414 1.414l1.414-1.414zm-.707.707l-.707-.707.707.707zM9 11v-1a1 1 0 0 0-.707.293L9 11zm-4 0h1a1 1 0 0 0-1-1v1zm0 4H4a1 1 0 0 0 1.707.707L5 15zm10-9h2V4h-2v2zm2 0a1 1 0 0 1 1 1h2a3 3 0 0 0-3-3v2zm1 1v6h2V7h-2zm0 6a1 1 0 0 1-1 1v2a3 3 0 0 0 3-3h-2zm-1 1h-2v2h2v-2zm-3 1v4h2v-4h-2zm1.707 3.293l-4-4-1.414 1.414 4 4 1.414-1.414zM11 14H7v2h4v-2zm-4 0c-.276 0-.525-.111-.707-.293l-1.414 1.414C5.42 15.663 6.172 16 7 16v-2zm-.707 1.121l3.414-3.414-1.414-1.414-3.414 3.414 1.414 1.414zM9 12h4v-2H9v2zm4 0a3 3 0 0 0 3-3h-2a1 1 0 0 1-1 1v2zm3-3V3h-2v6h2zm0-6a3 3 0 0 0-3-3v2a1 1 0 0 1 1 1h2zm-3-3H3v2h10V0zM3 0a3 3 0 0 0-3 3h2a1 1 0 0 1 1-1V0zM0 3v6h2V3H0zm0 6a3 3 0 0 0 3 3v-2a1 1 0 0 1-1-1H0zm3 3h2v-2H3v2zm1-1v4h2v-4H4zm1.707 4.707l.586-.586-1.414-1.414-.586.586 1.414 1.414z"
|
|
5
|
+
/>
|
|
6
|
+
</svg>
|
|
7
|
+
</template>
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="17" fill="currentColor">
|
|
3
|
+
<path
|
|
4
|
+
d="M11 2.253a1 1 0 1 0-2 0h2zm-2 13a1 1 0 1 0 2 0H9zm.447-12.167a1 1 0 1 0 1.107-1.666L9.447 3.086zM1 2.253L.447 1.42A1 1 0 0 0 0 2.253h1zm0 13H0a1 1 0 0 0 1.553.833L1 15.253zm8.447.833a1 1 0 1 0 1.107-1.666l-1.107 1.666zm0-14.666a1 1 0 1 0 1.107 1.666L9.447 1.42zM19 2.253h1a1 1 0 0 0-.447-.833L19 2.253zm0 13l-.553.833A1 1 0 0 0 20 15.253h-1zm-9.553-.833a1 1 0 1 0 1.107 1.666L9.447 14.42zM9 2.253v13h2v-13H9zm1.553-.833C9.203.523 7.42 0 5.5 0v2c1.572 0 2.961.431 3.947 1.086l1.107-1.666zM5.5 0C3.58 0 1.797.523.447 1.42l1.107 1.666C2.539 2.431 3.928 2 5.5 2V0zM0 2.253v13h2v-13H0zm1.553 13.833C2.539 15.431 3.928 15 5.5 15v-2c-1.92 0-3.703.523-5.053 1.42l1.107 1.666zM5.5 15c1.572 0 2.961.431 3.947 1.086l1.107-1.666C9.203 13.523 7.42 13 5.5 13v2zm5.053-11.914C11.539 2.431 12.928 2 14.5 2V0c-1.92 0-3.703.523-5.053 1.42l1.107 1.666zM14.5 2c1.573 0 2.961.431 3.947 1.086l1.107-1.666C18.203.523 16.421 0 14.5 0v2zm3.5.253v13h2v-13h-2zm1.553 12.167C18.203 13.523 16.421 13 14.5 13v2c1.573 0 2.961.431 3.947 1.086l1.107-1.666zM14.5 13c-1.92 0-3.703.523-5.053 1.42l1.107 1.666C11.539 15.431 12.928 15 14.5 15v-2z"
|
|
5
|
+
/>
|
|
6
|
+
</svg>
|
|
7
|
+
</template>
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="20" fill="currentColor">
|
|
3
|
+
<path
|
|
4
|
+
d="M11.447 8.894a1 1 0 1 0-.894-1.789l.894 1.789zm-2.894-.789a1 1 0 1 0 .894 1.789l-.894-1.789zm0 1.789a1 1 0 1 0 .894-1.789l-.894 1.789zM7.447 7.106a1 1 0 1 0-.894 1.789l.894-1.789zM10 9a1 1 0 1 0-2 0h2zm-2 2.5a1 1 0 1 0 2 0H8zm9.447-5.606a1 1 0 1 0-.894-1.789l.894 1.789zm-2.894-.789a1 1 0 1 0 .894 1.789l-.894-1.789zm2 .789a1 1 0 1 0 .894-1.789l-.894 1.789zm-1.106-2.789a1 1 0 1 0-.894 1.789l.894-1.789zM18 5a1 1 0 1 0-2 0h2zm-2 2.5a1 1 0 1 0 2 0h-2zm-5.447-4.606a1 1 0 1 0 .894-1.789l-.894 1.789zM9 1l.447-.894a1 1 0 0 0-.894 0L9 1zm-2.447.106a1 1 0 1 0 .894 1.789l-.894-1.789zm-6 3a1 1 0 1 0 .894 1.789L.553 4.106zm2.894.789a1 1 0 1 0-.894-1.789l.894 1.789zm-2-.789a1 1 0 1 0-.894 1.789l.894-1.789zm1.106 2.789a1 1 0 1 0 .894-1.789l-.894 1.789zM2 5a1 1 0 1 0-2 0h2zM0 7.5a1 1 0 1 0 2 0H0zm8.553 12.394a1 1 0 1 0 .894-1.789l-.894 1.789zm-1.106-2.789a1 1 0 1 0-.894 1.789l.894-1.789zm1.106 1a1 1 0 1 0 .894 1.789l-.894-1.789zm2.894.789a1 1 0 1 0-.894-1.789l.894 1.789zM8 19a1 1 0 1 0 2 0H8zm2-2.5a1 1 0 1 0-2 0h2zm-7.447.394a1 1 0 1 0 .894-1.789l-.894 1.789zM1 15H0a1 1 0 0 0 .553.894L1 15zm1-2.5a1 1 0 1 0-2 0h2zm12.553 2.606a1 1 0 1 0 .894 1.789l-.894-1.789zM17 15l.447.894A1 1 0 0 0 18 15h-1zm1-2.5a1 1 0 1 0-2 0h2zm-7.447-5.394l-2 1 .894 1.789 2-1-.894-1.789zm-1.106 1l-2-1-.894 1.789 2 1 .894-1.789zM8 9v2.5h2V9H8zm8.553-4.894l-2 1 .894 1.789 2-1-.894-1.789zm.894 0l-2-1-.894 1.789 2 1 .894-1.789zM16 5v2.5h2V5h-2zm-4.553-3.894l-2-1-.894 1.789 2 1 .894-1.789zm-2.894-1l-2 1 .894 1.789 2-1L8.553.106zM1.447 5.894l2-1-.894-1.789-2 1 .894 1.789zm-.894 0l2 1 .894-1.789-2-1-.894 1.789zM0 5v2.5h2V5H0zm9.447 13.106l-2-1-.894 1.789 2 1 .894-1.789zm0 1.789l2-1-.894-1.789-2 1 .894 1.789zM10 19v-2.5H8V19h2zm-6.553-3.894l-2-1-.894 1.789 2 1 .894-1.789zM2 15v-2.5H0V15h2zm13.447 1.894l2-1-.894-1.789-2 1 .894 1.789zM18 15v-2.5h-2V15h2z"
|
|
5
|
+
/>
|
|
6
|
+
</svg>
|
|
7
|
+
</template>
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor">
|
|
3
|
+
<path
|
|
4
|
+
d="M10 3.22l-.61-.6a5.5 5.5 0 0 0-7.666.105 5.5 5.5 0 0 0-.114 7.665L10 18.78l8.39-8.4a5.5 5.5 0 0 0-.114-7.665 5.5 5.5 0 0 0-7.666-.105l-.61.61z"
|
|
5
|
+
/>
|
|
6
|
+
</svg>
|
|
7
|
+
</template>
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 24 24">
|
|
3
|
+
<path fill-rule="evenodd" d="M2 12C2 6.477 6.477 2 12 2s10 4.477 10 10-4.477 10-10 10S2 17.523 2 12Zm11-4a1 1 0 1 0-2 0v4a1 1 0 0 0 .293.707l3 3a1 1 0 0 0 1.414-1.414L13 11.586V8Z" clip-rule="evenodd"></path>
|
|
4
|
+
</svg>
|
|
5
|
+
</template>
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
<!-- This icon is from <https://github.com/Templarian/MaterialDesign>, distributed under Apache 2.0 (https://www.apache.org/licenses/LICENSE-2.0) license-->
|
|
2
|
+
<template>
|
|
3
|
+
<svg
|
|
4
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
5
|
+
xmlns:xlink="http://www.w3.org/1999/xlink"
|
|
6
|
+
aria-hidden="true"
|
|
7
|
+
role="img"
|
|
8
|
+
class="iconify iconify--mdi"
|
|
9
|
+
width="24"
|
|
10
|
+
height="24"
|
|
11
|
+
preserveAspectRatio="xMidYMid meet"
|
|
12
|
+
viewBox="0 0 24 24"
|
|
13
|
+
>
|
|
14
|
+
<path
|
|
15
|
+
d="M20 18v-4h-3v1h-2v-1H9v1H7v-1H4v4h16M6.33 8l-1.74 4H7v-1h2v1h6v-1h2v1h2.41l-1.74-4H6.33M9 5v1h6V5H9m12.84 7.61c.1.22.16.48.16.8V18c0 .53-.21 1-.6 1.41c-.4.4-.85.59-1.4.59H4c-.55 0-1-.19-1.4-.59C2.21 19 2 18.53 2 18v-4.59c0-.32.06-.58.16-.8L4.5 7.22C4.84 6.41 5.45 6 6.33 6H7V5c0-.55.18-1 .57-1.41C7.96 3.2 8.44 3 9 3h6c.56 0 1.04.2 1.43.59c.39.41.57.86.57 1.41v1h.67c.88 0 1.49.41 1.83 1.22l2.34 5.39z"
|
|
16
|
+
fill="currentColor"
|
|
17
|
+
></path>
|
|
18
|
+
</svg>
|
|
19
|
+
</template>
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
@tailwind base;
|
|
2
|
+
@tailwind components;
|
|
3
|
+
@tailwind utilities;
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
// @layer base {
|
|
7
|
+
// /* width */
|
|
8
|
+
// ::-webkit-scrollbar {
|
|
9
|
+
// @apply w-2 p-2
|
|
10
|
+
// }
|
|
11
|
+
|
|
12
|
+
// /* Track */
|
|
13
|
+
// ::-webkit-scrollbar-track {
|
|
14
|
+
// @apply bg-inherit
|
|
15
|
+
// }
|
|
16
|
+
|
|
17
|
+
// /* Handle */
|
|
18
|
+
// ::-webkit-scrollbar-thumb {
|
|
19
|
+
// @apply bg-gray-200 dark:bg-gray-600 rounded-xl
|
|
20
|
+
// }
|
|
21
|
+
|
|
22
|
+
// /* Handle on hover */
|
|
23
|
+
// ::-webkit-scrollbar-thumb:hover {
|
|
24
|
+
// @apply bg-gray-700
|
|
25
|
+
// }
|
|
26
|
+
// }
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { createApp } from 'vue'
|
|
2
|
+
import { createPinia } from 'pinia'
|
|
3
|
+
/* IMPORTANT:ADMINFORTH IMPORTS */
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
import App from './App.vue'
|
|
7
|
+
import router from './router'
|
|
8
|
+
|
|
9
|
+
const app = createApp(App)
|
|
10
|
+
/* IMPORTANT:ADMINFORTH COMPONENT REGISTRATIONS */
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
app.use(createPinia())
|
|
14
|
+
app.use(router)
|
|
15
|
+
|
|
16
|
+
/* IMPORTANT:ADMINFORTH CUSTOM USES */
|
|
17
|
+
|
|
18
|
+
app.mount('#app')
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { createRouter, createWebHistory } from 'vue-router'
|
|
2
|
+
import HomeView from '../views/HomeView.vue'
|
|
3
|
+
import ResourceParent from '@/views/ResourceParent.vue'
|
|
4
|
+
import ListView from '@/views/ListView.vue'
|
|
5
|
+
import ShowView from '@/views/ShowView.vue'
|
|
6
|
+
import EditView from '@/views/EditView.vue'
|
|
7
|
+
import CreateView from '@/views/CreateView.vue'
|
|
8
|
+
|
|
9
|
+
const router = createRouter({
|
|
10
|
+
history: createWebHistory(import.meta.env.BASE_URL),
|
|
11
|
+
routes: [
|
|
12
|
+
{
|
|
13
|
+
path: '/',
|
|
14
|
+
name: 'home',
|
|
15
|
+
component: HomeView
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
path: '/login',
|
|
19
|
+
name: 'login',
|
|
20
|
+
component: () => import('@/views/LoginView.vue')
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
path: '/resource/:resourceId',
|
|
24
|
+
component: ResourceParent,
|
|
25
|
+
name: 'resource',
|
|
26
|
+
children: [
|
|
27
|
+
{
|
|
28
|
+
path: '',
|
|
29
|
+
component: ListView,
|
|
30
|
+
name: 'resource-list'
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
path: 'show/:primaryKey',
|
|
34
|
+
component: ShowView,
|
|
35
|
+
name: 'resource-show'
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
path: 'edit/:primaryKey',
|
|
39
|
+
component: EditView,
|
|
40
|
+
name: 'resource-edit'
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
path: 'create',
|
|
44
|
+
component: CreateView,
|
|
45
|
+
name: 'resource-create'
|
|
46
|
+
},
|
|
47
|
+
]
|
|
48
|
+
},
|
|
49
|
+
/* IMPORTANT:ADMINFORTH ROUTES */
|
|
50
|
+
]
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
export default router
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { ref, computed } from 'vue'
|
|
2
|
+
import { defineStore } from 'pinia'
|
|
3
|
+
import { callAdminForthApi } from '@/utils';
|
|
4
|
+
import router from '@/router';
|
|
5
|
+
|
|
6
|
+
async function findHomepage(menu) {
|
|
7
|
+
for (const item of menu) {
|
|
8
|
+
if (item.homepage) {
|
|
9
|
+
return item;
|
|
10
|
+
}
|
|
11
|
+
if (item.children) {
|
|
12
|
+
const res = findHomepage(item.children);
|
|
13
|
+
if (res) {
|
|
14
|
+
return res;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const useCoreStore = defineStore('core', () => {
|
|
22
|
+
const resourceById = ref([]);
|
|
23
|
+
const menu = ref([]);
|
|
24
|
+
const config = ref({});
|
|
25
|
+
const record = ref({});
|
|
26
|
+
const resourceColumns = ref(null);
|
|
27
|
+
const resourceColumnsError = ref('');
|
|
28
|
+
const resourceColumnsId = ref(null);
|
|
29
|
+
const user = ref(null);
|
|
30
|
+
|
|
31
|
+
async function fetchMenuAndResource() {
|
|
32
|
+
const resp = await callAdminForthApi({
|
|
33
|
+
path: '/get_base_config',
|
|
34
|
+
method: 'GET',
|
|
35
|
+
});
|
|
36
|
+
menu.value = resp.menu;
|
|
37
|
+
resourceById.value = resp.resources.reduce((acc, resource) => {
|
|
38
|
+
acc[resource.resourceId] = resource;
|
|
39
|
+
return acc;
|
|
40
|
+
}, {});
|
|
41
|
+
config.value = resp.config;
|
|
42
|
+
user.value = resp.user;
|
|
43
|
+
|
|
44
|
+
// find homepage:true in menu recuresively
|
|
45
|
+
if (import.meta.env.DEV){
|
|
46
|
+
return
|
|
47
|
+
} else {
|
|
48
|
+
const homepage = await findHomepage(menu.value);
|
|
49
|
+
if (homepage) {
|
|
50
|
+
if (homepage.resourceId) {
|
|
51
|
+
// redirect to homepage
|
|
52
|
+
router.push({ name: 'resource-list', params: { resourceId: homepage.resourceId } });
|
|
53
|
+
} else {
|
|
54
|
+
// redirect to path
|
|
55
|
+
router.push(homepage.path);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function fetchRecord({ resourceId, primaryKey }) {
|
|
62
|
+
record.value = null;
|
|
63
|
+
|
|
64
|
+
record.value = await callAdminForthApi({
|
|
65
|
+
path: '/get_record',
|
|
66
|
+
method: 'POST',
|
|
67
|
+
body: {
|
|
68
|
+
resourceId: resourceId,
|
|
69
|
+
primaryKey: primaryKey,
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async function fetchColumns({ resourceId }) {
|
|
75
|
+
if (resourceColumnsId.value === resourceId && resourceColumns.value) {
|
|
76
|
+
// already fetched
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
resourceColumnsId.value = resourceId;
|
|
80
|
+
resourceColumns.value = null;
|
|
81
|
+
resourceColumnsError.value = '';
|
|
82
|
+
const res = await callAdminForthApi({
|
|
83
|
+
path: '/get_resource_columns',
|
|
84
|
+
method: 'POST',
|
|
85
|
+
body: {
|
|
86
|
+
resourceId,
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
);
|
|
90
|
+
if (res.error) {
|
|
91
|
+
resourceColumnsError.value = res.error;
|
|
92
|
+
} else {
|
|
93
|
+
resourceColumns.value = res.resource.columns;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function getPublicConfig() {
|
|
98
|
+
const res = await callAdminForthApi({
|
|
99
|
+
path: '/get_public_config',
|
|
100
|
+
method: 'GET',
|
|
101
|
+
});
|
|
102
|
+
config.value = {...config.value, ...res};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async function logout() {
|
|
106
|
+
await callAdminForthApi({
|
|
107
|
+
path: '/logout',
|
|
108
|
+
method: 'POST',
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const username = computed(() => {
|
|
113
|
+
const usernameField = config.value.usernameField;
|
|
114
|
+
return user.value && user.value[usernameField];
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
const userFullname = computed(() => {
|
|
118
|
+
const userFullnameField = config.value.userFullnameField;
|
|
119
|
+
return user.value && user.value[userFullnameField];
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
return {
|
|
124
|
+
config,
|
|
125
|
+
resourceById,
|
|
126
|
+
menu,
|
|
127
|
+
username,
|
|
128
|
+
userFullname,
|
|
129
|
+
getPublicConfig,
|
|
130
|
+
fetchMenuAndResource,
|
|
131
|
+
fetchRecord,
|
|
132
|
+
record,
|
|
133
|
+
resourceColumns,
|
|
134
|
+
fetchColumns,
|
|
135
|
+
resourceColumnsError,
|
|
136
|
+
logout
|
|
137
|
+
}
|
|
138
|
+
})
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { ref } from 'vue'
|
|
2
|
+
import { defineStore } from 'pinia'
|
|
3
|
+
import { callAdminForthApi } from '@/utils';
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
export const useModalStore = defineStore('modal', () => {
|
|
7
|
+
const modalContent = ref({
|
|
8
|
+
title: 'title',
|
|
9
|
+
content: 'content',
|
|
10
|
+
acceptText: 'acceptText',
|
|
11
|
+
cancelText: 'cancelText',
|
|
12
|
+
});
|
|
13
|
+
const isOpened = ref(false);
|
|
14
|
+
const onAcceptFunction: any = ref(()=>{});
|
|
15
|
+
function togleModal() {
|
|
16
|
+
isOpened.value = !isOpened.value;
|
|
17
|
+
}
|
|
18
|
+
function setOnAcceptFunction(func: Function) {
|
|
19
|
+
onAcceptFunction.value = func;
|
|
20
|
+
}
|
|
21
|
+
function setModalContent(content: string) {
|
|
22
|
+
modalContent.value = content;
|
|
23
|
+
}
|
|
24
|
+
function resetmodalState() {
|
|
25
|
+
isOpened.value = false;
|
|
26
|
+
modalContent.value = {
|
|
27
|
+
title: 'title',
|
|
28
|
+
content: 'content',
|
|
29
|
+
acceptText: 'acceptText',
|
|
30
|
+
cancelText: 'cancelText',
|
|
31
|
+
};
|
|
32
|
+
setOnAcceptFunction(()=>{});
|
|
33
|
+
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return {isOpened, setModalContent, togleModal,modalContent, setOnAcceptFunction, onAcceptFunction,resetmodalState}
|
|
37
|
+
|
|
38
|
+
})
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { onMounted, ref, resolveComponent } from 'vue';
|
|
2
|
+
|
|
3
|
+
import router from "./router";
|
|
4
|
+
|
|
5
|
+
export async function callApi({path, method, body=undefined} ) {
|
|
6
|
+
const options = {
|
|
7
|
+
method,
|
|
8
|
+
headers: {
|
|
9
|
+
'Content-Type': 'application/json',
|
|
10
|
+
},
|
|
11
|
+
body: JSON.stringify(body),
|
|
12
|
+
};
|
|
13
|
+
const fullPath = `${import.meta.env.VITE_ADMINFORTH_PUBLIC_PATH || ''}${path}`;
|
|
14
|
+
const r = await fetch(fullPath, options);
|
|
15
|
+
if (r.status == 401) {
|
|
16
|
+
console.log('router', router);
|
|
17
|
+
router.push({name: 'login'});
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
return await r.json();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function callAdminForthApi({ path, method, body=undefined }) {
|
|
24
|
+
try {
|
|
25
|
+
return callApi({path: `/adminapi/v1${path}`, method, body} );
|
|
26
|
+
} catch (e) {
|
|
27
|
+
console.error('error', e);
|
|
28
|
+
return { error: `Unexpected error: ${e}` };
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
export function getIcon(icon: string) {
|
|
37
|
+
// icon format is "feather:icon-name". We need to get IconName in pascal case
|
|
38
|
+
if (!icon.includes(':')) {
|
|
39
|
+
throw new Error('Icon name should be in format "icon-set:icon-name"');
|
|
40
|
+
}
|
|
41
|
+
const [iconSet, iconName] = icon.split(':');
|
|
42
|
+
const compName = 'Icon' + iconName.split('-').map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join('');
|
|
43
|
+
return resolveComponent(compName);
|
|
44
|
+
}
|