adminforth 1.2.98 → 1.2.100

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.
Files changed (36) hide show
  1. package/auth.ts +6 -5
  2. package/dataConnectors/baseConnector.ts +91 -21
  3. package/dataConnectors/clickhouse.ts +58 -37
  4. package/dataConnectors/mongo.ts +44 -14
  5. package/dataConnectors/postgres.ts +53 -36
  6. package/dataConnectors/sqlite.ts +44 -35
  7. package/dist/auth.js +6 -6
  8. package/dist/dataConnectors/baseConnector.js +75 -17
  9. package/dist/dataConnectors/clickhouse.js +59 -48
  10. package/dist/dataConnectors/mongo.js +28 -12
  11. package/dist/dataConnectors/postgres.js +62 -52
  12. package/dist/dataConnectors/sqlite.js +62 -45
  13. package/dist/index.js +52 -33
  14. package/dist/modules/configValidator.js +25 -16
  15. package/dist/modules/operationalResource.js +88 -0
  16. package/dist/modules/restApi.js +106 -96
  17. package/dist/modules/utils.js +16 -0
  18. package/dist/types/AdminForthConfig.js +37 -0
  19. package/index.ts +76 -43
  20. package/modules/configValidator.ts +26 -21
  21. package/modules/operationalResource.ts +91 -0
  22. package/modules/restApi.ts +91 -80
  23. package/modules/utils.ts +20 -0
  24. package/package.json +3 -2
  25. package/spa/src/App.vue +12 -9
  26. package/spa/src/components/Filters.vue +1 -1
  27. package/spa/src/components/ResourceForm.vue +7 -4
  28. package/spa/src/components/ResourceListTable.vue +124 -112
  29. package/spa/src/components/SkeleteLoader.vue +4 -4
  30. package/spa/src/components/ValueRenderer.vue +1 -1
  31. package/spa/src/router/index.ts +0 -8
  32. package/spa/src/spa_types/core.ts +1 -0
  33. package/spa/src/stores/filters.ts +3 -2
  34. package/spa/src/views/ListView.vue +16 -25
  35. package/spa/src/views/LoginView.vue +26 -9
  36. package/types/AdminForthConfig.ts +140 -38
@@ -3,11 +3,13 @@
3
3
  <div class="relative overflow-x-auto shadow-listTableShadow dark:shadow-darkListTableShadow overflow-y-hidden"
4
4
  :class="{'rounded-default': !noRoundings}"
5
5
  >
6
-
7
6
  <!-- skelet loader -->
8
7
  <div role="status" v-if="!resource || !resource.columns"
9
8
  class="max-w p-4 space-y-4 divide-y divide-gray-200 rounded shadow animate-pulse dark:divide-gray-700 md:p-6 dark:border-gray-700">
10
-
9
+
10
+ <div role="status" class="max-w-sm animate-pulse">
11
+ <div class="h-2 bg-gray-200 rounded-full dark:bg-gray-700 max-w-[360px]"></div>
12
+ </div>
11
13
  </div>
12
14
 
13
15
  <table v-else class="w-full text-sm text-left rtl:text-right text-gray-500 dark:text-gray-400 rounded-default">
@@ -23,9 +25,9 @@
23
25
 
24
26
  <th v-for="c in columnsListed" scope="col" class="px-6 py-3">
25
27
 
26
- <div @click="() => c.sortable && onSortButtonClick(c.name)" class="flex items-center " :class="{'cursor-pointer':c.sortable}">
28
+ <div @click="(evt) => c.sortable && onSortButtonClick(evt, c.name)"
29
+ class="flex items-center " :class="{'cursor-pointer':c.sortable}">
27
30
  {{ c.label }}
28
-
29
31
 
30
32
  <div v-if="c.sortable"
31
33
  :style="{ 'color':ascArr.includes(c.name)?'green':descArr.includes(c.name)?'red':'currentColor'}">
@@ -56,109 +58,113 @@
56
58
  </tr>
57
59
  </thead>
58
60
  <tbody>
59
- <SkeleteLoader v-if="!rows" :columns="3" :rows="resource?.columns.length + 2"/>
60
- <tr v-else-if="rows.length === 0" class="bg-lightListTable dark:bg-darkListTable dark:border-darkListTableBorder">
61
- <td :colspan="resource?.columns.length + 2">
62
-
63
- <div id="toast-simple"
64
- class=" mx-auto my-5 flex items-center w-full max-w-xs p-4 space-x-4 rtl:space-x-reverse text-gray-500 divide-x rtl:divide-x-reverse divide-gray-200 dark:text-gray-400 dark:divide-gray-700 space-x dark:bg-gray-800"
65
- role="alert">
66
- <IconInboxOutline class="w-6 h-6 text-gray-500 dark:text-gray-400"/>
67
- <div class="ps-4 text-sm font-normal">No items here yet</div>
68
- </div>
69
-
70
- </td>
71
- </tr>
61
+ <SkeleteLoader
62
+ v-if="!rows"
63
+ :columns="resource?.columns.filter(c => c.showIn.includes('list')).length + 2"
64
+ :rows="3"
65
+ />
66
+ <tr v-else-if="rows.length === 0" class="bg-lightListTable dark:bg-darkListTable dark:border-darkListTableBorder">
67
+ <td :colspan="resource?.columns.length + 2">
68
+
69
+ <div id="toast-simple"
70
+ class=" mx-auto my-5 flex items-center w-full max-w-xs p-4 space-x-4 rtl:space-x-reverse text-gray-500 divide-x rtl:divide-x-reverse divide-gray-200 dark:text-gray-400 dark:divide-gray-700 space-x dark:bg-gray-800"
71
+ role="alert">
72
+ <IconInboxOutline class="w-6 h-6 text-gray-500 dark:text-gray-400"/>
73
+ <div class="ps-4 text-sm font-normal">No items here yet</div>
74
+ </div>
72
75
 
73
- <tr @click="onClick($event,row)" v-else v-for="(row, rowI) in rows" :key="row.id"
74
- class="bg-lightListTable dark:bg-darkListTable border-lightListBorder dark:border-gray-700 hover:bg-lightListTableRowHover dark:hover:bg-darkListTableRowHover cursor-pointer"
75
- :class="{'border-b': rowI !== rows.length - 1}"
76
- >
77
- <td class="w-4 p-4 cursor-default" @click="(e)=>{e.stopPropagation()}">
78
- <div class="flex items center ">
79
- <input
80
- @click="(e)=>{e.stopPropagation()}"
81
- id="checkbox-table-search-1"
82
- type="checkbox"
83
- :checked="checkboxesInternal.includes(row.id)"
84
- @change="(e)=>{addToCheckedValues(row.id)}"
85
- class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 dark:focus:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600 cursor-pointer">
86
- <label for="checkbox-table-search-1" class="sr-only">checkbox</label>
87
- </div>
88
- </td>
89
- <td v-for="c in columnsListed" class="px-6 py-4">
90
- <!-- if c.name in listComponentsPerColumn, render it. If not, render ValueRenderer -->
91
- <component
92
- :is="c?.components?.list ? getCustomComponent(c.components.list) : ValueRenderer"
93
- :meta="c?.components?.list?.meta"
94
- :column="c"
95
- :record="row"
96
- :adminUser="coreStore.adminUser"
97
- :resource="resource"
98
- />
99
- </td>
100
- <td class=" items-center px-6 py-4 cursor-default" @click="(e)=>{e.stopPropagation()}">
101
- <div class="flex">
102
- <RouterLink
103
- v-if="resource.options?.allowedActions.show"
104
- :to="{
105
- name: 'resource-show',
106
- params: {
107
- resourceId: resource.resourceId,
108
- primaryKey: row._primaryKeyValue,
109
- }
110
- }"
111
- class="font-medium text-lightPrimary dark:text-darkPrimary hover:underline"
112
- :data-tooltip-target="`tooltip-show-${rowI}`"
113
- >
114
- <IconEyeSolid class="w-5 h-5 me-2"/>
115
- </RouterLink>
116
-
117
- <div :id="`tooltip-show-${rowI}`"
118
- role="tooltip"
119
- 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">
120
- Show item
121
- <div class="tooltip-arrow" data-popper-arrow></div>
122
- </div>
76
+ </td>
77
+ </tr>
78
+
79
+ <tr @click="onClick($event,row)" v-else v-for="(row, rowI) in rows" :key="`row_${row._primaryKeyValue}`"
80
+ class="bg-lightListTable dark:bg-darkListTable border-lightListBorder dark:border-gray-700 hover:bg-lightListTableRowHover dark:hover:bg-darkListTableRowHover cursor-pointer"
81
+ :class="{'border-b': rowI !== rows.length - 1}"
82
+ >
83
+ <td class="w-4 p-4 cursor-default" @click="(e)=>{e.stopPropagation()}">
84
+ <div class="flex items center ">
85
+ <input
86
+ @click="(e)=>{e.stopPropagation()}"
87
+ id="checkbox-table-search-1"
88
+ type="checkbox"
89
+ :checked="checkboxesInternal.includes(row._primaryKeyValue)"
90
+ @change="(e)=>{addToCheckedValues(row._primaryKeyValue)}"
91
+ class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 dark:focus:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600 cursor-pointer">
92
+ <label for="checkbox-table-search-1" class="sr-only">checkbox</label>
93
+ </div>
94
+ </td>
95
+ <td v-for="c in columnsListed" class="px-6 py-4">
96
+ <!-- if c.name in listComponentsPerColumn, render it. If not, render ValueRenderer -->
97
+ <component
98
+ :is="c?.components?.list ? getCustomComponent(c.components.list) : ValueRenderer"
99
+ :meta="c?.components?.list?.meta"
100
+ :column="c"
101
+ :record="row"
102
+ :adminUser="coreStore.adminUser"
103
+ :resource="resource"
104
+ />
105
+ </td>
106
+ <td class=" items-center px-6 py-4 cursor-default" @click="(e)=>{e.stopPropagation()}">
107
+ <div class="flex">
108
+ <RouterLink
109
+ v-if="resource.options?.allowedActions.show"
110
+ :to="{
111
+ name: 'resource-show',
112
+ params: {
113
+ resourceId: resource.resourceId,
114
+ primaryKey: row._primaryKeyValue,
115
+ }
116
+ }"
117
+ class="font-medium text-lightPrimary dark:text-darkPrimary hover:underline"
118
+ :data-tooltip-target="`tooltip-show-${rowI}`"
119
+ >
120
+ <IconEyeSolid class="w-5 h-5 me-2"/>
121
+ </RouterLink>
122
+
123
+ <div :id="`tooltip-show-${rowI}`"
124
+ role="tooltip"
125
+ 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">
126
+ Show item
127
+ <div class="tooltip-arrow" data-popper-arrow></div>
128
+ </div>
123
129
 
124
- <RouterLink v-if="resource.options?.allowedActions.edit"
125
- :to="{
126
- name: 'resource-edit',
127
- params: {
128
- resourceId: resource.resourceId,
129
- primaryKey: row._primaryKeyValue
130
- }
131
- }"
132
- class="font-medium text-lightPrimary dark:text-darkPrimary hover:underline ms-3"
133
- :data-tooltip-target="`tooltip-edit-${rowI}`"
134
- >
135
- <IconPenSolid class="w-5 h-5 me-2"/>
136
- </RouterLink>
137
-
138
- <div :id="`tooltip-edit-${rowI}`"
139
- role="tooltip"
140
- 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">
141
- Edit
142
- <div class="tooltip-arrow" data-popper-arrow></div>
143
- </div>
130
+ <RouterLink v-if="resource.options?.allowedActions.edit"
131
+ :to="{
132
+ name: 'resource-edit',
133
+ params: {
134
+ resourceId: resource.resourceId,
135
+ primaryKey: row._primaryKeyValue
136
+ }
137
+ }"
138
+ class="font-medium text-lightPrimary dark:text-darkPrimary hover:underline ms-3"
139
+ :data-tooltip-target="`tooltip-edit-${rowI}`"
140
+ >
141
+ <IconPenSolid class="w-5 h-5 me-2"/>
142
+ </RouterLink>
143
+
144
+ <div :id="`tooltip-edit-${rowI}`"
145
+ role="tooltip"
146
+ 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">
147
+ Edit
148
+ <div class="tooltip-arrow" data-popper-arrow></div>
149
+ </div>
144
150
 
145
- <button v-if="resource.options?.allowedActions.delete"
146
- class="font-medium text-lightPrimary dark:text-darkPrimary hover:underline ms-3"
147
- :data-tooltip-target="`tooltip-delete-${rowI}`"
148
- @click="deleteRecord(row)"
149
- >
150
- <IconTrashBinSolid class="w-5 h-5 me-2"/>
151
- </button>
152
-
153
- <div :id="`tooltip-delete-${rowI}`"
154
- role="tooltip"
155
- 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">
156
- Delete
157
- <div class="tooltip-arrow" data-popper-arrow></div>
151
+ <button v-if="resource.options?.allowedActions.delete"
152
+ class="font-medium text-lightPrimary dark:text-darkPrimary hover:underline ms-3"
153
+ :data-tooltip-target="`tooltip-delete-${rowI}`"
154
+ @click="deleteRecord(row)"
155
+ >
156
+ <IconTrashBinSolid class="w-5 h-5 me-2"/>
157
+ </button>
158
+
159
+ <div :id="`tooltip-delete-${rowI}`"
160
+ role="tooltip"
161
+ 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">
162
+ Delete
163
+ <div class="tooltip-arrow" data-popper-arrow></div>
164
+ </div>
158
165
  </div>
159
- </div>
160
- </td>
161
- </tr>
166
+ </td>
167
+ </tr>
162
168
  </tbody>
163
169
  </table>
164
170
  </div>
@@ -237,13 +243,10 @@ import SkeleteLoader from '@/components/SkeleteLoader.vue';
237
243
 
238
244
  import {
239
245
  IconInboxOutline,
240
- IconPlusOutline
241
246
  } from '@iconify-prerendered/vue-flowbite';
242
247
 
243
248
  import {
244
- IconBanOutline,
245
249
  IconEyeSolid,
246
- IconFilterOutline,
247
250
  IconPenSolid,
248
251
  IconTrashBinSolid
249
252
  } from '@iconify-prerendered/vue-flowbite';
@@ -299,6 +302,7 @@ watch(() => props.sort, (newSort) => {
299
302
  });
300
303
 
301
304
  function addToCheckedValues(id) {
305
+ console.log('checking', checkboxesInternal.value, 'id', id)
302
306
  if (checkboxesInternal.value.includes(id)) {
303
307
  checkboxesInternal.value = checkboxesInternal.value.filter((item) => item !== id);
304
308
  } else {
@@ -312,13 +316,13 @@ const columnsListed = computed(() => props.resource?.columns?.filter(c => c.show
312
316
  async function selectAll(value) {
313
317
  if (!allFromThisPageChecked.value) {
314
318
  props.rows.forEach((r) => {
315
- if (!checkboxesInternal.value.includes(r.id)) {
316
- checkboxesInternal.value.push(r.id)
319
+ if (!checkboxesInternal.value.includes(r._primaryKeyValue)) {
320
+ checkboxesInternal.value.push(r._primaryKeyValue)
317
321
  }
318
322
  });
319
323
  } else {
320
324
  props.rows.forEach((r) => {
321
- checkboxesInternal.value = checkboxesInternal.value.filter((item) => item !== r.id);
325
+ checkboxesInternal.value = checkboxesInternal.value.filter((item) => item !== r._primaryKeyValue);
322
326
  });
323
327
  }
324
328
  checkboxesInternal.value = [ ...checkboxesInternal.value ];
@@ -328,16 +332,24 @@ const totalPages = computed(() => Math.ceil(props.totalRows / props.pageSize));
328
332
 
329
333
  const allFromThisPageChecked = computed(() => {
330
334
  if (!props.rows) return false;
331
- return props.rows.every((r) => checkboxesInternal.value.includes(r.id));
335
+ return props.rows.every((r) => checkboxesInternal.value.includes(r._primaryKeyValue));
332
336
  });
333
337
  const ascArr = computed(() => sort.value.filter((s) => s.direction === 'asc').map((s) => s.field));
334
338
  const descArr = computed(() => sort.value.filter((s) => s.direction === 'desc').map((s) => s.field));
335
339
 
336
340
 
337
- function onSortButtonClick(field) {
341
+ function onSortButtonClick(event, field) {
342
+ // if ctrl key is pressed, add to sort otherwise sort by this field
343
+ // in any case if field is already in sort, toggle direction
344
+
338
345
  const sortIndex = sort.value.findIndex((s) => s.field === field);
339
346
  if (sortIndex === -1) {
340
- sort.value = [...sort.value,{field, direction: 'asc'}];
347
+ // field is not in sort, add it
348
+ if (event.ctrlKey) {
349
+ sort.value = [...sort.value,{field, direction: 'asc'}];
350
+ } else {
351
+ sort.value = [{field, direction: 'asc'}];
352
+ }
341
353
  } else {
342
354
  const sortField = sort.value[sortIndex];
343
355
  if (sortField.direction === 'asc') {
@@ -1,6 +1,6 @@
1
1
  <template>
2
- <tr v-for="c in new Array(props.columns)" class="bg-lightListTable border-b dark:bg-darkListTable dark:border-darkListBorder">
3
- <td v-for="r in new Array(props.rows)" class="items-center px-6 py-8 cursor-default" >
2
+ <tr v-for="c in new Array(props.rows)" class="bg-lightListTable border-b dark:bg-darkListTable dark:border-darkListBorder">
3
+ <td v-for="r in new Array(props.columns)" class="items-center px-6 py-8 cursor-default" >
4
4
  <div role="status" class="max-w-sm animate-pulse">
5
5
  <div class="h-2 bg-gray-200 rounded-full dark:bg-gray-700 max-w-[360px]"></div>
6
6
  </div>
@@ -11,8 +11,8 @@
11
11
  <script setup>
12
12
 
13
13
  const props = defineProps({
14
- columns:Number,
15
- rows:Number
14
+ columns: Number,
15
+ rows: Number
16
16
  });
17
17
 
18
18
 
@@ -15,7 +15,7 @@
15
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>
16
16
  </span>
17
17
  <span v-else-if="column.enum">
18
- {{ checkEmptyValues(column.enum.find(e => e.value === record[column.name])?.label,route.meta.type) }}
18
+ {{ checkEmptyValues(column.enum.find(e => e.value === record[column.name])?.label || record[column.name], route.meta.type) }}
19
19
  </span>
20
20
  <span v-else-if="column.type === 'datetime'" class="whitespace-nowrap">
21
21
  {{ checkEmptyValues(formatDate(record[column.name]),route.meta.type) }}
@@ -56,12 +56,4 @@ const router = createRouter({
56
56
  ]
57
57
  })
58
58
 
59
- router.beforeEach((to, from, next) => {
60
- const filterStore = useFiltersStore();
61
- if (to.path.startsWith('/resource/')) {
62
- filterStore.clearFilters();
63
- }
64
- next()
65
- })
66
-
67
59
  export default router
@@ -32,6 +32,7 @@ export type CoreConfig = {
32
32
  usernameField: string,
33
33
  passwordHashField: string,
34
34
  loginBackgroundImage: string,
35
+ loginBackgroundPosition: string,
35
36
  userFullnameField: string,
36
37
  },
37
38
  emptyFieldPlaceholder?: {
@@ -3,11 +3,12 @@ import { defineStore } from 'pinia';
3
3
 
4
4
  export const useFiltersStore = defineStore('filters', () => {
5
5
  const filters: Ref<any[]> = ref([]);
6
+
6
7
  const setFilter = (filter: any) => {
7
- filters.value = [...filters.value, filter];
8
+ filters.value.push(filter);
8
9
  }
9
10
  const setFilters = (f: any) => {
10
- filters.value = [...f];
11
+ filters.value = f;
11
12
  }
12
13
  const getFilters = () => {
13
14
  return filters.value;
@@ -117,8 +117,6 @@ import { initFlowbite } from 'flowbite';
117
117
  import { computed, onMounted, ref, watch } from 'vue';
118
118
  import { useRoute } from 'vue-router';
119
119
  import { showErrorTost } from '@/composables/useFrontendApi'
120
-
121
- import ValueRenderer from '@/components/ValueRenderer.vue';
122
120
  import { getCustomComponent } from '@/utils';
123
121
 
124
122
 
@@ -151,20 +149,7 @@ const DEFAULT_PAGE_SIZE = 10;
151
149
  const pageSize = computed(() => coreStore.resource?.options?.listPageSize || DEFAULT_PAGE_SIZE);
152
150
 
153
151
 
154
- watch([page], async () => {
155
- await getList();
156
- });
157
-
158
- watch(()=>filtersStore.filters, async () => {
159
- page.value = 1;
160
- checkboxes.value = [];
161
-
162
- await getList();
163
- }, {deep: true});
164
152
 
165
- watch([sort], async () => {
166
- await getList();
167
- }, {deep: true});
168
153
 
169
154
 
170
155
  async function getList() {
@@ -222,6 +207,8 @@ async function init() {
222
207
  resourceId: route.params.resourceId
223
208
  });
224
209
 
210
+ // !!! clear filters should be in same tick with sort assignment so that watch can catch it
211
+ filtersStore.clearFilters();
225
212
  if (coreStore.resource.options?.defaultSort) {
226
213
  sort.value = [{
227
214
  field: coreStore.resource.options.defaultSort.columnName,
@@ -230,8 +217,8 @@ async function init() {
230
217
  } else {
231
218
  sort.value = [];
232
219
  }
233
-
234
- await getList();
220
+ console.log('↘️init fired');
221
+ // await getList();
235
222
  columnsMinMax.value = await callAdminForthApi({
236
223
  path: '/get_min_max_for_columns',
237
224
  method: 'POST',
@@ -241,18 +228,22 @@ async function init() {
241
228
  });
242
229
  }
243
230
 
231
+ watch([page, sort, () => filtersStore.filters], async () => {
232
+ console.log('↘️watch fired getList');
233
+ await getList();
234
+ }, { deep: true });
235
+
236
+ watch(() => filtersStore.filters, async (to, from) => {
237
+ page.value = 1;
238
+ checkboxes.value = [];
239
+ }, {deep: true});
240
+
244
241
  onMounted(async () => {
242
+ console.log('🧱onMounted fired');
245
243
  initFlowbite();
246
-
247
244
  await init();
248
-
249
245
  });
250
246
 
251
- // on route param change
252
- watch(() => route.params.resourceId, async () => {
253
- filtersStore.setFilters([]);
254
- checkboxes.value = [];
255
- await init();
256
- });
247
+
257
248
 
258
249
  </script>
@@ -1,24 +1,38 @@
1
1
  <template>
2
- <div class="relative flex items-center justify-center min-h-screen bg-gray-100 dark:bg-gray-800"
3
- :style="coreStore.config?.loginBackgroundImage ? {
2
+ <div class="relative flex items-center justify-center min-h-screen bg-gray-100 dark:bg-gray-800 relative w-screen h-screen"
3
+ :style="coreStore.config?.loginBackgroundImage && backgroundPosition === 'over' ? {
4
4
  'background-image': 'url(' + loadFile(coreStore.config?.loginBackgroundImage) + ')',
5
5
  'background-size': 'cover',
6
6
  'background-position': 'center',
7
7
  'background-blend-mode': 'darken'
8
8
  }: {}"
9
- >
9
+ >
10
+
11
+ <img v-if="coreStore.config?.loginBackgroundImage && backgroundPosition !== 'over'"
12
+ :src="loadFile(coreStore.config?.loginBackgroundImage)"
13
+ class="position-absolute top-0 left-0 h-screen object-cover w-0"
14
+ :class="{
15
+ '1/2': 'md:w-1/2',
16
+ '1/3': 'md:w-1/3',
17
+ '2/3': 'md:w-2/3',
18
+ '3/4': 'md:w-3/4',
19
+ '2/5': 'md:w-2/5',
20
+ '3/5': 'md:w-3/5',
21
+ }[backgroundPosition]"
22
+ />
10
23
 
11
24
  <!-- Main modal -->
12
- <div id="authentication-modal" tabindex="-1" class=" overflow-y-auto overflow-x-hidden z-50 min-w-[400px] justify-center items-center md:inset-0 h-[calc(100%-1rem)] max-h-full">
13
- <div class="relative p-4 w-full max-w-md max-h-full">
25
+ <div id="authentication-modal" tabindex="-1"
26
+ class="overflow-y-auto flex flex-grow
27
+ overflow-x-hidden z-50 min-w-[350px] justify-center items-center md:inset-0 h-[calc(100%-1rem)] max-h-full">
28
+ <div class="relative p-4 w-full max-h-full max-w-[400px]">
14
29
  <!-- Modal content -->
15
30
  <div class="relative bg-white rounded-lg shadow dark:bg-gray-700 dark:shadow-black" >
16
31
  <!-- Modal header -->
17
32
  <div class="flex items-center justify-between p-4 md:p-5 border-b rounded-t dark:border-gray-600">
18
33
  <h3 class="text-xl font-semibold text-gray-900 dark:text-white">
19
- Sign in to {{ coreStore.config?.brandName }}
34
+ Sign in to {{ coreStore.config?.brandName }}
20
35
  </h3>
21
-
22
36
  </div>
23
37
  <!-- Modal body -->
24
38
  <div class="p-4 md:p-5">
@@ -87,7 +101,7 @@
87
101
 
88
102
  <script setup>
89
103
 
90
- import { onMounted, ref, watchEffect } from 'vue';
104
+ import { onMounted, ref, computed } from 'vue';
91
105
  import { useCoreStore } from '@/stores/core';
92
106
  import { useUserStore } from '@/stores/user';
93
107
  import { IconEyeSolid, IconEyeSlashSolid } from '@iconify-prerendered/vue-flowbite';
@@ -103,11 +117,14 @@ const inProgress = ref(false);
103
117
  const coreStore = useCoreStore();
104
118
  const user = useUserStore();
105
119
 
106
-
107
120
  const showPw = ref(false);
108
121
 
109
122
  const error = ref(null);
110
123
 
124
+ const backgroundPosition = computed(() => {
125
+ return coreStore.config?.loginBackgroundPosition || '1/2';
126
+ });
127
+
111
128
  onMounted(async () => {
112
129
  await coreStore.getPublicConfig();
113
130
  if (coreStore.config?.demoCredentials) {