adminforth 2.12.11 → 2.13.0-next.10

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 (52) hide show
  1. package/dist/dataConnectors/baseConnector.js +1 -1
  2. package/dist/dataConnectors/baseConnector.js.map +1 -1
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.js +3 -1
  5. package/dist/index.js.map +1 -1
  6. package/dist/modules/configValidator.d.ts.map +1 -1
  7. package/dist/modules/configValidator.js +12 -5
  8. package/dist/modules/configValidator.js.map +1 -1
  9. package/dist/modules/restApi.d.ts.map +1 -1
  10. package/dist/modules/restApi.js +30 -3
  11. package/dist/modules/restApi.js.map +1 -1
  12. package/dist/spa/package-lock.json +1406 -749
  13. package/dist/spa/package.json +32 -32
  14. package/dist/spa/src/App.vue +87 -14
  15. package/dist/spa/src/adminforth.ts +2 -2
  16. package/dist/spa/src/afcl/AreaChart.vue +0 -1
  17. package/dist/spa/src/afcl/Dropzone.vue +138 -41
  18. package/dist/spa/src/afcl/Input.vue +5 -9
  19. package/dist/spa/src/afcl/Table.vue +114 -15
  20. package/dist/spa/src/afcl/Textarea.vue +23 -19
  21. package/dist/spa/src/afcl/VerticalTabs.vue +5 -0
  22. package/dist/spa/src/components/Filters.vue +2 -2
  23. package/dist/spa/src/components/GroupsTable.vue +1 -1
  24. package/dist/spa/src/components/MenuLink.vue +11 -6
  25. package/dist/spa/src/components/ResourceForm.vue +5 -0
  26. package/dist/spa/src/components/ResourceListTable.vue +12 -16
  27. package/dist/spa/src/components/ResourceListTableVirtual.vue +10 -13
  28. package/dist/spa/src/components/Sidebar.vue +10 -8
  29. package/dist/spa/src/components/Toast.vue +1 -1
  30. package/dist/spa/src/components/UserMenuSettingsButton.vue +2 -2
  31. package/dist/spa/src/components/ValueRenderer.vue +1 -1
  32. package/dist/spa/src/stores/core.ts +9 -0
  33. package/dist/spa/src/types/Back.ts +7 -1
  34. package/dist/spa/src/types/Common.ts +19 -1
  35. package/dist/spa/src/types/FrontendAPI.ts +1 -18
  36. package/dist/spa/src/types/adapters/StorageAdapter.ts +4 -2
  37. package/dist/spa/src/utils.ts +7 -3
  38. package/dist/spa/src/views/CreateView.vue +25 -1
  39. package/dist/spa/src/views/EditView.vue +26 -1
  40. package/dist/spa/src/views/SettingsView.vue +4 -4
  41. package/dist/types/Back.d.ts +6 -0
  42. package/dist/types/Back.d.ts.map +1 -1
  43. package/dist/types/Back.js.map +1 -1
  44. package/dist/types/Common.d.ts +18 -1
  45. package/dist/types/Common.d.ts.map +1 -1
  46. package/dist/types/Common.js.map +1 -1
  47. package/dist/types/FrontendAPI.d.ts +1 -15
  48. package/dist/types/FrontendAPI.d.ts.map +1 -1
  49. package/dist/types/FrontendAPI.js.map +1 -1
  50. package/dist/types/adapters/StorageAdapter.d.ts +2 -0
  51. package/dist/types/adapters/StorageAdapter.d.ts.map +1 -1
  52. package/package.json +1 -1
@@ -4,13 +4,30 @@
4
4
  <table class="afcl-table w-full text-sm text-left rtl:text-right text-lightTableText dark:text-darkTableText overflow-x-auto">
5
5
  <thead class="afcl-table-thread text-xs text-lightTableHeadingText uppercase bg-lightTableHeadingBackground dark:bg-darkTableHeadingBackground dark:text-darkTableHeadingText">
6
6
  <tr>
7
- <th scope="col" class="px-6 py-3" ref="headerRefs" :key="`header-${column.fieldName}`"
7
+ <th
8
+ scope="col"
9
+ class="px-6 py-3"
10
+ ref="headerRefs"
11
+ :key="`header-${column.fieldName}`"
8
12
  v-for="column in columns"
13
+ :aria-sort="getAriaSort(column)"
14
+ :class="{ 'cursor-pointer select-none afcl-table-header-sortable': isColumnSortable(column) }"
15
+ @click="onHeaderClick(column)"
9
16
  >
10
17
  <slot v-if="$slots[`header:${column.fieldName}`]" :name="`header:${column.fieldName}`" :column="column" />
11
-
12
- <span v-else>
18
+
19
+ <span v-else class="inline-flex items-center">
13
20
  {{ column.label }}
21
+ <span v-if="isColumnSortable(column)" class="text-lightTableHeadingText dark:text-darkTableHeadingText">
22
+ <!-- Unsorted -->
23
+ <svg v-if="currentSortField !== column.fieldName" class="w-3 h-3 ms-1.5 opacity-30" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 24 24"><path d="M8.574 11.024h6.852a2.075 2.075 0 0 0 1.847-1.086 1.9 1.9 0 0 0-.11-1.986L13.736 2.9a2.122 2.122 0 0 0-3.472 0L6.837 7.952a1.9 1.9 0 0 0-.11 1.986 2.074 2.074 0 0 0 1.847 1.086Zm6.852 1.952H8.574a2.072 2.072 0 0 0-1.847 1.087 1.9 1.9 0 0 0 .11 1.985l3.426 5.05a2.123 2.123 0 0 0 3.472 0l3.427-5.05a1.9 1.9 0 0 0 .11-1.985 2.074 2.074 0 0 0-1.846-1.087Z"/></svg>
24
+
25
+ <!-- Sorted ascending -->
26
+ <svg v-else-if="currentSortDirection === 'asc'" class="w-3 h-3 ms-1.5" fill="currentColor" viewBox="0 0 24 24"><path d="M8.574 11.024h6.852a2.075 2.075 0 0 0 1.847-1.086 1.9 1.9 0 0 0-.11-1.986L13.736 2.9a2.122 2.122 0 0 0-3.472 0L6.837 7.952a1.9 1.9 0 0 0-.11 1.986 2.074 2.074 0 0 0 1.847 0z"/></svg>
27
+
28
+ <!-- Sorted descending -->
29
+ <svg v-else class="w-3 h-3 ms-1.5 rotate-180" fill="currentColor" viewBox="0 0 24 24"><path d="M8.574 11.024h6.852a2.075 2.075 0 0 0 1.847-1.086 1.9 1.9 0 0 0-.11-1.986L13.736 2.9a2.122 2.122 0 0 0-3.472 0L6.837 7.952a1.9 1.9 0 0 0-.11 1.986 2.074 2.074 0 0 0 1.847 0z"/></svg>
30
+ </span>
14
31
  </span>
15
32
  </th>
16
33
  </tr>
@@ -40,6 +57,7 @@
40
57
  'afcl-table-body odd:bg-lightTableOddBackground odd:dark:bg-darkTableOddBackground even:bg-lightTableEvenBackground even:dark:bg-darkTableEvenBackground': evenHighlights,
41
58
  'border-b border-lightTableBorder dark:border-darkTableBorder': index !== dataPage.length - 1 || totalPages > 1,
42
59
  }"
60
+ @click="tableRowClick(item)"
43
61
  >
44
62
  <td class="px-6 py-4" :key="`cell-${index}-${column.fieldName}`"
45
63
  v-for="column in props.columns"
@@ -92,15 +110,15 @@
92
110
  <!-- <IconChevronDoubleLeftOutline class="w-4 h-4" /> -->
93
111
  1
94
112
  </button>
95
- <div
96
- :contenteditable="!isLoading && !props.isLoading"
113
+ <input
114
+ type="text"
115
+ v-model="pageInput"
116
+ :style="{ width: `${Math.max(1, pageInput.length+4)}ch` }"
97
117
  class="min-w-10 outline-none inline-block w-auto py-1.5 px-3 text-sm text-center text-lightTablePaginationInputText border border-lightTablePaginationInputBorder bg-lightTablePaginationInputBackground dark:border-darkTablePaginationInputBorder dark:text-darkTablePaginationInputText dark:bg-darkTablePaginationInputBackground z-10"
98
118
  @keydown="onPageKeydown($event)"
99
- @input="onPageInput($event)"
100
119
  @blur="validatePageInput()"
101
120
  >
102
- {{ pageInput }}
103
- </div>
121
+ </input>
104
122
 
105
123
  <button
106
124
  class="flex items-center py-1 px-3 text-sm font-medium text-lightUnactivePaginationButtonText focus:outline-none bg-lightUnactivePaginationButtonBackground border-l-0 border border-lightUnactivePaginationButtonBorder hover:bg-lightUnactivePaginationButtonHoverBackground hover:text-lightUnactivePaginationButtonHoverText dark:bg-darkUnactivePaginationButtonBackground dark:text-darkUnactivePaginationButtonText dark:border-darkUnactivePaginationButtonBorder dark:hover:text-darkUnactivePaginationButtonHoverText dark:hover:bg-darkUnactivePaginationButtonHoverBackground disabled:opacity-50"
@@ -141,13 +159,16 @@
141
159
  columns: {
142
160
  label: string,
143
161
  fieldName: string,
162
+ sortable?: boolean,
144
163
  }[],
145
164
  data: {
146
165
  [key: string]: any,
147
- }[] | ((params: { offset: number, limit: number }) => Promise<{data: {[key: string]: any}[], total: number}>),
166
+ }[] | ((params: { offset: number, limit: number, sortField?: string, sortDirection?: 'asc' | 'desc' }) => Promise<{data: {[key: string]: any}[], total: number}>),
148
167
  evenHighlights?: boolean,
149
168
  pageSize?: number,
150
169
  isLoading?: boolean,
170
+ defaultSortField?: string,
171
+ defaultSortDirection?: 'asc' | 'desc',
151
172
  }>(), {
152
173
  evenHighlights: true,
153
174
  pageSize: 5,
@@ -163,8 +184,17 @@
163
184
  const isLoading = ref(false);
164
185
  const dataResult = ref<{data: {[key: string]: any}[], total: number}>({data: [], total: 0});
165
186
  const isAtLeastOneLoading = ref<boolean[]>([false]);
187
+ const currentSortField = ref<string | undefined>(props.defaultSortField);
188
+ const currentSortDirection = ref<'asc' | 'desc'>(props.defaultSortDirection ?? 'asc');
166
189
 
167
190
  onMounted(() => {
191
+ // If defaultSortField points to a non-sortable column, ignore it
192
+ if (currentSortField.value) {
193
+ const col = props.columns?.find(c => c.fieldName === currentSortField.value);
194
+ if (!col || !isColumnSortable(col)) {
195
+ currentSortField.value = undefined;
196
+ }
197
+ }
168
198
  refresh();
169
199
  });
170
200
 
@@ -181,6 +211,20 @@
181
211
  emit('update:tableLoading', isLoading.value || props.isLoading);
182
212
  });
183
213
 
214
+ watch([() => currentSortField.value, () => currentSortDirection.value], () => {
215
+ const needsPageReset = currentPage.value !== 1;
216
+ if (needsPageReset) {
217
+ currentPage.value = 1;
218
+ } else {
219
+ refresh();
220
+ }
221
+ emit('update:sortField', currentSortField.value);
222
+ emit('update:sortDirection', currentSortField.value ? currentSortDirection.value : undefined);
223
+ const field = currentSortField.value ?? null;
224
+ const direction = currentSortField.value ? currentSortDirection.value : null;
225
+ emit('sort-change', { field, direction });
226
+ }, { immediate: false });
227
+
184
228
  const totalPages = computed(() => {
185
229
  return dataResult.value?.total ? Math.ceil(dataResult.value.total / props.pageSize) : 1;
186
230
  });
@@ -196,11 +240,11 @@
196
240
 
197
241
  const emit = defineEmits([
198
242
  'update:tableLoading',
243
+ 'update:sortField',
244
+ 'update:sortDirection',
245
+ 'sort-change',
246
+ 'clickTableRow'
199
247
  ]);
200
-
201
- function onPageInput(event: any) {
202
- pageInput.value = event.target.innerText;
203
- }
204
248
 
205
249
  function validatePageInput() {
206
250
  const newPage = parseInt(pageInput.value) || 1;
@@ -231,7 +275,12 @@
231
275
  isLoading.value = true;
232
276
  const currentLoadingIndex = currentPage.value;
233
277
  isAtLeastOneLoading.value[currentLoadingIndex] = true;
234
- const result = await props.data({ offset: (currentLoadingIndex - 1) * props.pageSize, limit: props.pageSize });
278
+ const result = await props.data({
279
+ offset: (currentLoadingIndex - 1) * props.pageSize,
280
+ limit: props.pageSize,
281
+ sortField: currentSortField.value,
282
+ ...(currentSortField.value ? { sortDirection: currentSortDirection.value } : {}),
283
+ });
235
284
  isAtLeastOneLoading.value[currentLoadingIndex] = false;
236
285
  if (isAtLeastOneLoading.value.every(v => v === false)) {
237
286
  isLoading.value = false;
@@ -240,7 +289,9 @@
240
289
  } else if (typeof props.data === 'object' && Array.isArray(props.data)) {
241
290
  const start = (currentPage.value - 1) * props.pageSize;
242
291
  const end = start + props.pageSize;
243
- dataResult.value = { data: props.data.slice(start, end), total: props.data.length };
292
+ const total = props.data.length;
293
+ const sorted = sortArrayData(props.data, currentSortField.value, currentSortDirection.value);
294
+ dataResult.value = { data: sorted.slice(start, end), total };
244
295
  }
245
296
  }
246
297
 
@@ -252,4 +303,52 @@
252
303
  }
253
304
  }
254
305
 
306
+ function isColumnSortable(col:{fieldName:string; sortable?:boolean}) {
307
+ // Sorting is controlled per column; default is NOT sortable. Enable with `sortable: true`.
308
+ return col.sortable === true;
309
+ }
310
+
311
+ function onHeaderClick(col:{fieldName:string; sortable?:boolean}) {
312
+ if (!isColumnSortable(col)) return;
313
+ if (currentSortField.value !== col.fieldName) {
314
+ currentSortField.value = col.fieldName;
315
+ currentSortDirection.value = props.defaultSortDirection ?? 'asc';
316
+ } else {
317
+ currentSortDirection.value =
318
+ currentSortDirection.value === 'asc' ? 'desc' :
319
+ currentSortField.value ? (currentSortField.value = undefined, props.defaultSortDirection ?? 'asc') :
320
+ 'asc';
321
+ }
322
+ }
323
+
324
+ function getAriaSort(col:{fieldName:string; sortable?:boolean}) {
325
+ if (!isColumnSortable(col)) return undefined;
326
+ if (currentSortField.value !== col.fieldName) return 'none';
327
+ return currentSortDirection.value === 'asc' ? 'ascending' : 'descending';
328
+ }
329
+
330
+ const collator = new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' });
331
+
332
+ function sortArrayData(data:any[], sortField?:string, dir:'asc'|'desc'='asc') {
333
+ if (!sortField) return data;
334
+ // Helper function to get nested properties by path
335
+ const getByPath = (o:any, p:string) => p.split('.').reduce((a:any,k)=>a?.[k], o);
336
+ return [...data].sort((a,b) => {
337
+ const av = getByPath(a, sortField), bv = getByPath(b, sortField);
338
+ // Handle null/undefined values
339
+ if (av == null && bv == null) return 0;
340
+ // Handle null/undefined values
341
+ if (av == null) return 1; if (bv == null) return -1;
342
+ // Data types
343
+ if (av instanceof Date && bv instanceof Date) return dir === 'asc' ? av.getTime() - bv.getTime() : bv.getTime() - av.getTime();
344
+ // Strings and numbers
345
+ if (typeof av === 'number' && typeof bv === 'number') return dir === 'asc' ? av - bv : bv - av;
346
+ const cmp = collator.compare(String(av), String(bv));
347
+ return dir === 'asc' ? cmp : -cmp;
348
+ });
349
+ }
350
+
351
+ function tableRowClick(row) {
352
+ emit("clickTableRow", row)
353
+ }
255
354
  </script>
@@ -1,31 +1,35 @@
1
- <template>
2
-
3
- <textarea
4
- ref="input"
5
- class="bg-lightInputBackground border border-lightInputBorder text-lightInputText placeholder-lightInputPlaceholderText text-sm rounded-lg block w-full p-2.5 dark:bg-darkInputBackground dark:border-darkInputBorder dark:placeholder-darkInputPlaceholderText dark:text-darkInputText dark:border-darkInputBorder focus:ring-lightInputFocusRing focus:border-lightInputFocusBorder dark:focus:ring-darkInputFocusRing dark:focus:border-darkInputFocusBorder"
6
- :placeholder="placeholder"
7
- :value="modelValue"
8
- @input="$emit('update:modelValue', ($event.target as HTMLInputElement).value)"
9
- :readonly="readonly"
10
- />
11
-
1
+ <template>
2
+ <textarea
3
+ ref="input"
4
+ class="afcl-textarea bg-lightInputBackground border border-lightInputBorder text-lightInputText placeholder-lightInputPlaceholderText text-sm rounded-md block w-full p-2.5 dark:bg-darkInputBackground dark:border-darkInputBorder dark:placeholder-darkInputPlaceholderText dark:text-darkInputText dark:border-darkInputBorder focus:ring-lightInputFocusRing focus:border-lightInputFocusBorder dark:focus:ring-darkInputFocusRing dark:focus:border-darkInputFocusBorder"
5
+ :class="`${readonly ? 'opacity-50' : ''} ${isIos ? 'text-md' : 'text-sm'}`"
6
+ :placeholder="placeholder"
7
+ :value="modelValue"
8
+ @input="$emit('update:modelValue', ($event.target as HTMLTextAreaElement).value)"
9
+ :readonly="readonly"
10
+ />
12
11
  </template>
13
12
 
14
13
  <script setup lang="ts">
15
-
16
14
  import { ref } from 'vue';
15
+ import { useCoreStore } from '@/stores/core';
16
+
17
+ const coreStore = useCoreStore();
18
+ const isIos = coreStore.isIos;
17
19
 
18
20
  const props = defineProps<{
19
- modelValue: string,
20
- readonly?: boolean,
21
- placeholder?: string,
21
+ modelValue: string
22
+ readonly?: boolean
23
+ placeholder?: string
22
24
  }>()
23
25
 
24
- const input = ref<HTMLInputElement | null>(null)
26
+ const emit = defineEmits<{
27
+ (e: 'update:modelValue', value: string): void
28
+ }>()
29
+
30
+ const input = ref<HTMLTextAreaElement | null>(null)
25
31
 
26
32
  defineExpose({
27
33
  focus: () => input.value?.focus(),
28
- });
29
-
34
+ })
30
35
  </script>
31
-
@@ -3,6 +3,7 @@
3
3
  <ul class="ps-6 flex-column space-y space-y-4 text-sm font-medium text-lightVerticalTabsText dark:text-darkVerticalTabsText md:me-4 mb-4 md:mb-0 md:mr-0 mr-6">
4
4
  <li v-for="tab in tabs" :key="`${tab}-tab-controll`">
5
5
  <a
6
+ v-if="!props.hideTabesWhenSingle || tabs.length > 1"
6
7
  href="#"
7
8
  @click="setActiveTab(tab)"
8
9
  class="inline-flex items-center px-4 py-3 rounded-lg w-full"
@@ -26,6 +27,10 @@ import { onMounted, useSlots, ref, type Ref } from 'vue';
26
27
  const props = defineProps({
27
28
  activeTab: {
28
29
  type: String
30
+ },
31
+ hideTabesWhenSingle: {
32
+ type: Boolean,
33
+ default: false
29
34
  }
30
35
  });
31
36
  const emites = defineEmits([
@@ -18,11 +18,11 @@
18
18
  </h5>
19
19
 
20
20
  <div class="py-4 ">
21
- <ul class="space-y-3 font-medium">
21
+ <ul class="space-y-3 font-medium text-sm">
22
22
  <li v-for="c in columnsWithFilter" :key="c">
23
23
  <div class="flex flex-col">
24
24
  <div class="flex justify-between items-center">
25
- <p class="dark:text-gray-400 h-7 my-1">{{ c.label }}</p>
25
+ <p class="dark:text-gray-400 my-1">{{ c.label }}</p>
26
26
  <Tooltip v-if="filtersStore.filters.find(f => f.field === c.name)">
27
27
  <button
28
28
  class=" flex items-center justify-center w-7 h-7 my-1 hover:border rounded-md hover:bg-gray-100 dark:hover:bg-gray-700"
@@ -55,7 +55,7 @@
55
55
  @update:emptiness="customComponentsEmptiness[$event.name] = $event.value"
56
56
  :readonly="readonlyColumns?.includes(column.name)"
57
57
  />
58
- <div v-if="columnError(column) && validating" class="mt-1 text-xs text-lightInputErrorColor dark:text-darkInputErrorColor">{{ columnError(column) }}</div>
58
+ <div v-if="columnError(column) && validating" class="af-invalid-field-message mt-1 text-xs text-lightInputErrorColor dark:text-darkInputErrorColor">{{ columnError(column) }}</div>
59
59
  <div v-if="column.editingNote && column.editingNote[mode]" class="mt-1 text-xs text-lightFormFieldTextColor dark:text-darkFormFieldTextColor">{{ column.editingNote[mode] }}</div>
60
60
  </td>
61
61
  </tr>
@@ -24,16 +24,16 @@
24
24
  }"
25
25
  :style="isSidebarIconOnly ? {
26
26
  minWidth: isChild
27
- ? 'calc(16.5rem - 0.75rem*2 - 1.5rem*2 - 1.25rem - 0.75rem)'
28
- : 'calc(16.5rem - 0.75rem*2 - 0.875rem*2 - 1.25rem - 0.75rem)',
27
+ ? `calc(${expandedWidth} - 0.75rem*2 - 1.5rem*2 - 1.25rem - 0.75rem)`
28
+ : `calc(${expandedWidth} - 0.75rem*2 - 0.875rem*2 - 1.25rem - 0.75rem)`,
29
29
  width: isChild
30
- ? 'calc(16.5rem - 0.75rem*2 - 1.5rem*2 - 1.25rem - 0.75rem)'
31
- : 'calc(16.5rem - 0.75rem*2 - 0.875rem*2 - 1.25rem - 0.75rem)'
30
+ ? `calc(${expandedWidth} - 0.75rem*2 - 1.5rem*2 - 1.25rem - 0.75rem)`
31
+ : `calc(${expandedWidth} - 0.75rem*2 - 0.875rem*2 - 1.25rem - 0.75rem)`
32
32
  } : {}"
33
33
  >
34
34
  {{ item.label }}
35
35
  </div>
36
- <span class="absolute right-1 top-1/2 -translate-y-1/2" v-if="item.badge && showExpandedBadge">
36
+ <span class="absolute flex items-center justify-center right-1 top-1/2 -translate-y-1/2" v-if="item.badge && showExpandedBadge">
37
37
  <Tooltip v-if="item.badgeTooltip">
38
38
  <div class="af-badge inline-flex items-center justify-center h-3 py-2.5 px-1 ms-3 text-xs font-medium rounded-full bg-lightAnnouncementBG dark:bg-darkAnnouncementBG
39
39
  fill-lightAnnouncementText dark:fill-darkAccent text-lightAnnouncementText dark:text-darkAccent min-w-[1.5rem] max-w-[3rem]">{{ item.badge }}</div>
@@ -55,10 +55,15 @@
55
55
  <script setup lang="ts">
56
56
  import { getIcon } from '@/utils';
57
57
  import { Tooltip } from '@/afcl';
58
- import { ref, watch } from 'vue';
58
+ import { ref, watch, computed } from 'vue';
59
+ import { useCoreStore } from '@/stores/core';
59
60
 
60
61
  const props = defineProps(['item', 'isChild', 'isSidebarIconOnly', 'isSidebarHovering']);
61
62
 
63
+ const coreStore = useCoreStore();
64
+
65
+ const expandedWidth = computed(() => coreStore.config?.iconOnlySidebar?.expandedSidebarWidth || '16.5rem');
66
+
62
67
  const BADGE_SHOW_DELAY_MS = 200;
63
68
  const showExpandedBadge = ref(false);
64
69
  let showBadgeTimer: ReturnType<typeof setTimeout> | null = null;
@@ -377,4 +377,9 @@ watch(() => isValid.value, (value) => {
377
377
  emit('update:isValid', value);
378
378
  });
379
379
 
380
+ defineExpose({
381
+ columnError,
382
+ editableColumns,
383
+ })
384
+
380
385
  </script>
@@ -15,7 +15,7 @@
15
15
  <tbody>
16
16
  <!-- table header -->
17
17
  <tr class="t-header sticky z-20 top-0 text-xs text-lightListTableHeadingText bg-lightListTableHeading dark:bg-darkListTableHeading dark:text-darkListTableHeadingText">
18
- <td scope="col" class="p-4 sticky-column bg-lightListTableHeading dark:bg-darkListTableHeading">
18
+ <td scope="col" class="list-table-header-cell p-4 sticky-column bg-lightListTableHeading dark:bg-darkListTableHeading">
19
19
  <Checkbox
20
20
  :modelValue="allFromThisPageChecked"
21
21
  :disabled="!rows || !rows.length"
@@ -25,7 +25,7 @@
25
25
  </Checkbox>
26
26
  </td>
27
27
 
28
- <td v-for="c in columnsListed" ref="headerRefs" scope="col" class="px-2 md:px-3 lg:px-6 py-3" :class="{'sticky-column bg-lightListTableHeading dark:bg-darkListTableHeading': c.listSticky}">
28
+ <td v-for="c in columnsListed" ref="headerRefs" scope="col" class="list-table-header-cell px-2 md:px-3 lg:px-6 py-3" :class="{'sticky-column bg-lightListTableHeading dark:bg-darkListTableHeading': c.listSticky}">
29
29
 
30
30
  <div @click="(evt) => c.sortable && onSortButtonClick(evt, c.name)"
31
31
  class="flex items-center " :class="{'cursor-pointer':c.sortable}">
@@ -86,7 +86,7 @@
86
86
  <tr @click="onClick($event,row)"
87
87
  v-else v-for="(row, rowI) in rows" :key="`row_${row._primaryKeyValue}`"
88
88
  ref="rowRefs"
89
- class="bg-lightListTable dark:bg-darkListTable border-lightListBorder dark:border-gray-700 hover:bg-lightListTableRowHover dark:hover:bg-darkListTableRowHover"
89
+ class="list-table-body-row bg-lightListTable dark:bg-darkListTable border-lightListBorder dark:border-gray-700 hover:bg-lightListTableRowHover dark:hover:bg-darkListTableRowHover"
90
90
 
91
91
  :class="{'border-b': rowI !== rows.length - 1, 'cursor-pointer': row._clickUrl !== null}"
92
92
  >
@@ -241,15 +241,14 @@
241
241
  <!-- <IconChevronDoubleLeftOutline class="w-4 h-4" /> -->
242
242
  1
243
243
  </button>
244
- <div
245
- contenteditable="true"
246
- class="af-pagination-input min-w-10 outline-none inline-block w-auto py-1.5 px-3 text-sm text-center text-lightListTablePaginationCurrentPageText border border-lightListTablePaginationBorder dark:border-darkListTablePaginationBorder dark:text-darkListTablePaginationCurrentPageText dark:bg-darkListTablePaginationBackgoround z-10"
244
+ <input
245
+ type="text"
246
+ v-model="pageInput"
247
+ :style="{ width: `${Math.max(1, pageInput.length+4)}ch` }"
248
+ class="af-pagination-input min-w-10 outline-none inline-block py-1.5 px-3 text-sm text-center text-lightListTablePaginationCurrentPageText border border-lightListTablePaginationBorder dark:border-darkListTablePaginationBorder dark:text-darkListTablePaginationCurrentPageText dark:bg-darkListTablePaginationBackgoround z-10"
247
249
  @keydown="onPageKeydown($event)"
248
- @input="onPageInput($event)"
249
250
  @blur="validatePageInput()"
250
- >
251
- {{ pageInput }}
252
- </div>
251
+ />
253
252
 
254
253
  <button
255
254
  class="af-pagination-last-page-button flex items-center py-1 px-3 text-sm font-medium text-lightListTablePaginationText focus:outline-none bg-lightListTablePaginationBackgoround border-l-0 border border-lightListTablePaginationBorder hover:bg-lightListTablePaginationBackgoroundHover hover:text-lightListTablePaginationTextHover focus:z-10 focus:ring-4 focus:ring-lightListTablePaginationFocusRing dark:focus:ring-darkListTablePaginationFocusRing dark:bg-darkListTablePaginationBackgoround dark:text-darkListTablePaginationText dark:border-darkListTablePaginationBorder dark:hover:text-white dark:hover:bg-darkListTablePaginationBackgoroundHover disabled:opacity-50"
@@ -599,10 +598,6 @@ async function startCustomAction(actionId: string, row: any) {
599
598
  }
600
599
  }
601
600
 
602
- function onPageInput(event: any) {
603
- pageInput.value = event.target.innerText;
604
- }
605
-
606
601
  function validatePageInput() {
607
602
  const newPage = parseInt(pageInput.value) || 1;
608
603
  const validPage = Math.max(1, Math.min(newPage, totalPages.value));
@@ -625,9 +620,10 @@ td.sticky-column {
625
620
  @apply left-[56px];
626
621
  }
627
622
  }
628
- tr:not(:first-child):hover {
629
- td.sticky-column {
623
+ tr.list-table-body-row:not(:first-child):hover {
624
+ td.sticky-column:not(.list-table-header-cell) {
630
625
  @apply bg-lightListTableRowHover dark:bg-darkListTableRowHover;
631
626
  }
632
627
  }
628
+
633
629
  </style>
@@ -19,7 +19,7 @@
19
19
  <tbody>
20
20
  <!-- table header -->
21
21
  <tr class="t-header sticky z-20 top-0 text-xs bg-lightListTableHeading dark:bg-darkListTableHeading dark:text-gray-400">
22
- <td scope="col" class="p-4 sticky-column bg-lightListTableHeading dark:bg-darkListTableHeading">
22
+ <td scope="col" class="list-table-header-cell p-4 sticky-column bg-lightListTableHeading dark:bg-darkListTableHeading">
23
23
  <Checkbox
24
24
  :modelValue="allFromThisPageChecked"
25
25
  :disabled="!rows || !rows.length"
@@ -29,7 +29,7 @@
29
29
  </Checkbox>
30
30
  </td>
31
31
 
32
- <td v-for="c in columnsListed" ref="headerRefs" scope="col" class="px-2 md:px-3 lg:px-6 py-3" :class="{'sticky-column bg-lightListTableHeading dark:bg-darkListTableHeading': c.listSticky}">
32
+ <td v-for="c in columnsListed" ref="headerRefs" scope="col" class="list-table-header-cell px-2 md:px-3 lg:px-6 py-3" :class="{'sticky-column bg-lightListTableHeading dark:bg-darkListTableHeading': c.listSticky}">
33
33
 
34
34
  <div @click="(evt) => c.sortable && onSortButtonClick(evt, c.name)"
35
35
  class="flex items-center " :class="{'cursor-pointer':c.sortable}">
@@ -97,7 +97,7 @@
97
97
  v-for="(row, rowI) in visibleRows"
98
98
  :key="`row_${row._primaryKeyValue}`"
99
99
  ref="rowRefs"
100
- class="bg-lightListTable dark:bg-darkListTable border-lightListBorder dark:border-gray-700 hover:bg-lightListTableRowHover dark:hover:bg-darkListTableRowHover"
100
+ class="list-table-body-row bg-lightListTable dark:bg-darkListTable border-lightListBorder dark:border-gray-700 hover:bg-lightListTableRowHover dark:hover:bg-darkListTableRowHover"
101
101
  :class="{'border-b': rowI !== visibleRows.length - 1, 'cursor-pointer': row._clickUrl !== null}"
102
102
  @mounted="(el: any) => updateRowHeight(`row_${row._primaryKeyValue}`, el.offsetHeight)"
103
103
  >
@@ -262,15 +262,15 @@
262
262
  <!-- <IconChevronDoubleLeftOutline class="w-4 h-4" /> -->
263
263
  1
264
264
  </button>
265
- <div
266
- contenteditable="true"
265
+ <input
266
+ type="text"
267
+ v-model="pageInput"
268
+ :style="{ width: `${Math.max(1, pageInput.length+4)}ch` }"
267
269
  class="af-pagination-input min-w-10 outline-none inline-block w-auto py-1.5 px-3 text-sm text-center text-lightListTablePaginationCurrentPageText border border-lightListTablePaginationBorder dark:border-darkListTablePaginationBorder dark:text-darkListTablePaginationCurrentPageText dark:bg-darkListTablePaginationBackgoround z-10"
268
270
  @keydown="onPageKeydown($event)"
269
- @input="onPageInput($event)"
270
271
  @blur="validatePageInput()"
271
272
  >
272
- {{ pageInput }}
273
- </div>
273
+ </input>
274
274
 
275
275
  <button
276
276
  class="af-pagination-last-page-button flex items-center py-1 px-3 text-sm font-medium text-lightListTablePaginationText focus:outline-none bg-lightListTablePaginationBackgoround border-l-0 border border-lightListTablePaginationBorder hover:bg-lightListTablePaginationBackgoroundHover hover:text-lightListTablePaginationTextHover focus:z-10 focus:ring-4 focus:ring-lightListTablePaginationFocusRing dark:focus:ring-darkListTablePaginationFocusRing dark:bg-darkListTablePaginationBackgoround dark:text-darkListTablePaginationText dark:border-darkListTablePaginationBorder dark:hover:text-white dark:hover:bg-darkListTablePaginationBackgoroundHover disabled:opacity-50"
@@ -623,9 +623,6 @@ async function startCustomAction(actionId: string, row: any) {
623
623
  }
624
624
  }
625
625
 
626
- function onPageInput(event: any) {
627
- pageInput.value = event.target.innerText;
628
- }
629
626
 
630
627
  function validatePageInput() {
631
628
  const newPage = parseInt(pageInput.value) || 1;
@@ -764,8 +761,8 @@ td.sticky-column {
764
761
  @apply left-[56px];
765
762
  }
766
763
  }
767
- tr:not(:first-child):hover {
768
- td.sticky-column {
764
+ tr.list-table-body-row:not(:first-child):hover {
765
+ td.sticky-column:not(.list-table-header-cell) {
769
766
  @apply bg-lightListTableRowHover dark:bg-darkListTableRowHover;
770
767
  }
771
768
  }
@@ -13,8 +13,8 @@
13
13
  }"
14
14
  aria-label="Sidebar"
15
15
  >
16
- <div class="h-full px-3 pb-20 md:pb-4 bg-lightSidebar dark:bg-darkSidebar border-r border-lightSidebarBorder dark:border-darkSidebarBorder" :class="{'sidebar-scroll':!isSidebarIconOnly || (isSidebarIconOnly && isSidebarHovering)}">
17
- <div class="af-logo-title-wrapper flex relative transition-all duration-300 ease-in-out h-8 items-center" :class="{'my-4 ': isSidebarIconOnly && !isSidebarHovering, 'm-4': !isSidebarIconOnly || (isSidebarIconOnly && isSidebarHovering)}">
16
+ <div class="h-full px-3 pb-20 md:pb-4 bg-lightSidebar dark:bg-darkSidebar border-r border-lightSidebarBorder dark:border-darkSidebarBorder pt-4" :class="{'sidebar-scroll':!isSidebarIconOnly || (isSidebarIconOnly && isSidebarHovering)}">
17
+ <div class="af-logo-title-wrapper flex relative transition-all duration-300 ease-in-out h-8 items-center" :class="{'mb-4': isSidebarIconOnly && !isSidebarHovering, 'mx-4 mb-4': !isSidebarIconOnly || (isSidebarIconOnly && isSidebarHovering)}">
18
18
  <img :src="loadFile(coreStore.config?.brandLogo || '@/assets/logo.svg')" :alt="`${ coreStore.config?.brandName } Logo`" class="af-logo h-8 me-3" :class="{ 'hidden': !(coreStore.config?.showBrandLogoInSidebar !== false && (!iconOnlySidebarEnabled || !isSidebarIconOnly || (isSidebarIconOnly && isSidebarHovering))) }" />
19
19
  <img :src="loadFile(coreStore.config?.iconOnlySidebar?.logo || '')" :alt="`${ coreStore.config?.brandName } Logo`" class="af-sidebar-icon-only-logo h-8 me-3" :class="{ 'hidden': !(coreStore.config?.showBrandLogoInSidebar !== false && coreStore.config?.iconOnlySidebar?.logo && iconOnlySidebarEnabled && isSidebarIconOnly && !isSidebarHovering) }" />
20
20
  <span
@@ -23,7 +23,7 @@
23
23
  >
24
24
  {{ coreStore.config?.brandName }}
25
25
  </span>
26
- <div class="flex items-center gap-2 w-auto" :class="{'w-full justify-end': coreStore.config?.showBrandLogoInSidebar === false}">
26
+ <div v-if="!isSidebarIconOnly || (isSidebarIconOnly && isSidebarHovering)" class="flex items-center gap-2 w-auto" :class="{'w-full justify-end': coreStore.config?.showBrandLogoInSidebar === false}">
27
27
  <component
28
28
  v-for="c in coreStore?.config?.globalInjections?.sidebarTop || []"
29
29
  :is="getCustomComponent(c)"
@@ -106,8 +106,8 @@
106
106
  'opacity-100 ms-3 translate-x-0 flex-1': !isSidebarIconOnly
107
107
  }"
108
108
  :style="isSidebarIconOnly ? {
109
- minWidth: 'calc(16.5rem - 0.75rem*2 - 0.875rem*2 - 1.25rem - 0.75rem)',
110
- width: 'calc(16.5rem - 0.75rem*2 - 0.875rem*2 - 1.25rem - 0.75rem)'
109
+ minWidth: `calc(${expandedWidth} - 0.75rem*2 - 0.875rem*2 - 1.25rem - 0.75rem)`,
110
+ width: `calc(${expandedWidth} - 0.75rem*2 - 0.875rem*2 - 1.25rem - 0.75rem)`
111
111
  } : {}"
112
112
  >{{ item.label }}
113
113
 
@@ -188,7 +188,7 @@
188
188
  <style lang="scss" scoped>
189
189
  /* Sidebar width animations */
190
190
  .sidebar-container {
191
- width: 16.5rem; /* Default expanded width (w-64) */
191
+ width: v-bind(expandedWidth); /* Default expanded width (w-64) */
192
192
  transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
193
193
  overflow: hidden; /* Prevent content from showing during animation */
194
194
  will-change: width, transform;
@@ -196,11 +196,11 @@
196
196
 
197
197
  .sidebar-collapsed {
198
198
  width: 4.5rem; /* Collapsed width (w-18) */
199
- box-shadow: 12px 0px 18px -8px rgba(0, 0, 0, 0.15);
200
199
  }
201
200
 
202
201
  .sidebar-expanded {
203
- width: 16.5rem; /* Expanded width (w-64) */
202
+ width: v-bind(expandedWidth); /* Expanded width (w-64) */
203
+ box-shadow: 3px 0px 12px -2px rgba(0, 0, 0, 0.15);
204
204
  }
205
205
 
206
206
  :deep(.dark) .sidebar-collapsed {
@@ -313,6 +313,8 @@ const isMobile = ref(!smQuery.matches);
313
313
  const iconOnlySidebarEnabled = computed(() => props.forceIconOnly === true || coreStore.config?.iconOnlySidebar?.enabled !== false);
314
314
  const isSidebarIconOnly = ref(false);
315
315
 
316
+ const expandedWidth = computed(() => coreStore.config?.iconOnlySidebar?.expandedSidebarWidth || '16.5rem');
317
+
316
318
  function handleBreakpointChange(e: MediaQueryListEvent) {
317
319
  isMobile.value = !e.matches;
318
320
  if (isMobile.value) {
@@ -21,7 +21,7 @@
21
21
 
22
22
  <div class="ms-3 text-sm font-normal max-w-xs pr-2" v-if="toast.messageHtml" v-html="toast.messageHtml"></div>
23
23
  <div class="ms-3 text-sm font-normal max-w-xs pr-2" v-else>
24
- <div class="flex flex-col items-center justify-center">
24
+ <div class="flex flex-col items-center justify-center break-all">
25
25
  {{toast.message}}
26
26
  <div v-if="toast.buttons" class="flex justify-center mt-2 gap-2">
27
27
  <div v-for="button in toast.buttons" class="af-toast-button rounded-md bg-lightButtonsBackground hover:bg-lightButtonsHover text-lightButtonsText dark:bg-darkPrimary dark:hover:bg-darkButtonsBackground dark:text-darkButtonsText">
@@ -1,5 +1,5 @@
1
1
  <template>
2
- <div class="min-w-40">
2
+ <div v-if="options && options.length" class="min-w-40">
3
3
  <div class="cursor-pointer flex items-center justify-between gap-1 block px-4 py-2 text-sm
4
4
  bg-lightUserMenuItemBackground hover:bg-lightUserMenuItemBackgroundHover text-lightUserMenuItemText
5
5
  hover:text-lightUserMenuItemText dark:bg-darkUserMenuItemBackground dark:hover:bg-darkUserMenuItemBackgroundHover
@@ -50,7 +50,7 @@ const showDropdown = ref(false);
50
50
  const props = defineProps(['meta', 'resource']);
51
51
 
52
52
  const options = computed(() => {
53
- return coreStore.config?.settingPages?.map((page) => {
53
+ return coreStore.config?.settingPages?.filter(page => page.isVisible).map((page) => {
54
54
  return {
55
55
  pageLabel: page.pageLabel,
56
56
  slug: page.slug || null,
@@ -117,7 +117,7 @@ import timezone from 'dayjs/plugin/timezone';
117
117
  import {checkEmptyValues} from '@/utils';
118
118
  import { useRoute, useRouter } from 'vue-router';
119
119
  import { JsonViewer } from "vue3-json-viewer";
120
- import "vue3-json-viewer/dist/index.css";
120
+ import "vue3-json-viewer/dist/vue3-json-viewer.css";
121
121
  import type { AdminForthResourceColumnCommon } from '@/types/Common';
122
122
 
123
123
  import { useCoreStore } from '@/stores/core';
@@ -198,6 +198,7 @@ export const useCoreStore = defineStore('core', () => {
198
198
  path: '/get_public_config',
199
199
  method: 'GET',
200
200
  });
201
+ console.log('📦 getPublicConfig', res);
201
202
  config.value = {...config.value, ...res};
202
203
  }
203
204
 
@@ -206,6 +207,7 @@ export const useCoreStore = defineStore('core', () => {
206
207
  path: '/get_login_form_config',
207
208
  method: 'GET',
208
209
  });
210
+ console.log('📦 getLoginFormConfig', res);
209
211
  config.value = {...config.value, ...res};
210
212
  }
211
213
 
@@ -219,6 +221,12 @@ export const useCoreStore = defineStore('core', () => {
219
221
  return userData.value && userFullnameField && userData.value[userFullnameField];
220
222
  })
221
223
 
224
+ const isIos = computed(() => {
225
+ return (
226
+ /iPad|iPhone|iPod/.test(navigator.userAgent) ||
227
+ (navigator.userAgent.includes('Mac') && 'ontouchend' in document)
228
+ )});
229
+
222
230
 
223
231
  return {
224
232
  config,
@@ -243,5 +251,6 @@ export const useCoreStore = defineStore('core', () => {
243
251
  resetAdminUser,
244
252
  resetResource,
245
253
  isResourceFetching,
254
+ isIos
246
255
  }
247
256
  })