adminforth 2.12.11 → 2.13.0-next.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/dataConnectors/baseConnector.js +1 -1
- package/dist/dataConnectors/baseConnector.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/modules/configValidator.d.ts.map +1 -1
- package/dist/modules/configValidator.js +12 -5
- package/dist/modules/configValidator.js.map +1 -1
- package/dist/modules/restApi.d.ts.map +1 -1
- package/dist/modules/restApi.js +30 -3
- package/dist/modules/restApi.js.map +1 -1
- package/dist/spa/package-lock.json +1406 -749
- package/dist/spa/package.json +32 -32
- package/dist/spa/src/App.vue +87 -14
- package/dist/spa/src/adminforth.ts +2 -2
- package/dist/spa/src/afcl/AreaChart.vue +0 -1
- package/dist/spa/src/afcl/Dropzone.vue +138 -41
- package/dist/spa/src/afcl/Table.vue +114 -15
- package/dist/spa/src/afcl/VerticalTabs.vue +5 -0
- package/dist/spa/src/components/Filters.vue +2 -2
- package/dist/spa/src/components/GroupsTable.vue +1 -1
- package/dist/spa/src/components/MenuLink.vue +11 -6
- package/dist/spa/src/components/ResourceForm.vue +5 -0
- package/dist/spa/src/components/ResourceListTable.vue +12 -16
- package/dist/spa/src/components/ResourceListTableVirtual.vue +10 -13
- package/dist/spa/src/components/Sidebar.vue +10 -8
- package/dist/spa/src/components/UserMenuSettingsButton.vue +2 -2
- package/dist/spa/src/components/ValueRenderer.vue +1 -1
- package/dist/spa/src/stores/core.ts +2 -0
- package/dist/spa/src/types/Back.ts +7 -1
- package/dist/spa/src/types/Common.ts +19 -1
- package/dist/spa/src/types/FrontendAPI.ts +1 -18
- package/dist/spa/src/types/adapters/StorageAdapter.ts +4 -2
- package/dist/spa/src/utils.ts +3 -3
- package/dist/spa/src/views/CreateView.vue +25 -1
- package/dist/spa/src/views/EditView.vue +26 -1
- package/dist/spa/src/views/SettingsView.vue +4 -4
- package/dist/types/Back.d.ts +6 -0
- package/dist/types/Back.d.ts.map +1 -1
- package/dist/types/Back.js.map +1 -1
- package/dist/types/Common.d.ts +18 -1
- package/dist/types/Common.d.ts.map +1 -1
- package/dist/types/Common.js.map +1 -1
- package/dist/types/FrontendAPI.d.ts +1 -15
- package/dist/types/FrontendAPI.d.ts.map +1 -1
- package/dist/types/FrontendAPI.js.map +1 -1
- package/dist/types/adapters/StorageAdapter.d.ts +2 -0
- package/dist/types/adapters/StorageAdapter.d.ts.map +1 -1
- 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
|
|
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
|
-
<
|
|
96
|
-
|
|
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
|
-
|
|
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({
|
|
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
|
-
|
|
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>
|
|
@@ -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
|
|
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
|
-
?
|
|
28
|
-
:
|
|
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
|
-
?
|
|
31
|
-
:
|
|
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;
|
|
@@ -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
|
-
<
|
|
245
|
-
|
|
246
|
-
|
|
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
|
-
<
|
|
266
|
-
|
|
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
|
-
|
|
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="{'
|
|
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:
|
|
110
|
-
width:
|
|
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:
|
|
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:
|
|
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) {
|
|
@@ -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/
|
|
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
|
|
|
@@ -568,6 +568,7 @@ export type AfterCreateSaveFunction = (params: {
|
|
|
568
568
|
adminUser: AdminUser,
|
|
569
569
|
record: any,
|
|
570
570
|
adminforth: IAdminForth,
|
|
571
|
+
recordWithVirtualColumns?: any,
|
|
571
572
|
extra?: HttpExtra,
|
|
572
573
|
}) => Promise<{ok: boolean, error?: string}>;
|
|
573
574
|
|
|
@@ -699,6 +700,10 @@ interface AdminForthInputConfigCustomization {
|
|
|
699
700
|
iconOnlySidebar?: {
|
|
700
701
|
logo?: string,
|
|
701
702
|
enabled?: boolean,
|
|
703
|
+
/**
|
|
704
|
+
* Width of expanded sidebar (default: '16.5rem')
|
|
705
|
+
*/
|
|
706
|
+
expandedSidebarWidth?: string,
|
|
702
707
|
},
|
|
703
708
|
|
|
704
709
|
/**
|
|
@@ -1085,7 +1090,8 @@ export interface AdminForthInputConfig {
|
|
|
1085
1090
|
icon?: string,
|
|
1086
1091
|
pageLabel: string,
|
|
1087
1092
|
slug?: string,
|
|
1088
|
-
component: string
|
|
1093
|
+
component: string,
|
|
1094
|
+
isVisible?: (adminUser: AdminUser) => boolean,
|
|
1089
1095
|
}[],
|
|
1090
1096
|
},
|
|
1091
1097
|
|
|
@@ -30,6 +30,21 @@ export enum AdminForthFilterOperators {
|
|
|
30
30
|
OR = 'or',
|
|
31
31
|
};
|
|
32
32
|
|
|
33
|
+
export type FilterParams = {
|
|
34
|
+
/**
|
|
35
|
+
* Field of resource to filter
|
|
36
|
+
*/
|
|
37
|
+
field: string;
|
|
38
|
+
/**
|
|
39
|
+
* Operator of filter
|
|
40
|
+
*/
|
|
41
|
+
operator: AdminForthFilterOperators;
|
|
42
|
+
/**
|
|
43
|
+
* Value of filter
|
|
44
|
+
*/
|
|
45
|
+
value: string | number | boolean ;
|
|
46
|
+
}
|
|
47
|
+
|
|
33
48
|
export enum AdminForthSortDirections {
|
|
34
49
|
asc = 'asc',
|
|
35
50
|
desc = 'desc',
|
|
@@ -272,8 +287,9 @@ export interface AdminForthComponentDeclarationFull {
|
|
|
272
287
|
* - 'default': Show both sidebar and header (default behavior)
|
|
273
288
|
* - 'none': Hide both sidebar and header (full custom layout)
|
|
274
289
|
* - 'preferIconOnly': Show header but prefer icon-only sidebar
|
|
290
|
+
* - 'headerOnly': Show only header (full custom layout)
|
|
275
291
|
*/
|
|
276
|
-
sidebarAndHeader?: 'default' | 'none' | 'preferIconOnly',
|
|
292
|
+
sidebarAndHeader?: 'default' | 'none' | 'preferIconOnly' | 'headerOnly',
|
|
277
293
|
|
|
278
294
|
[key: string]: any,
|
|
279
295
|
}
|
|
@@ -1110,6 +1126,7 @@ export interface AdminForthConfigForFrontend {
|
|
|
1110
1126
|
iconOnlySidebar?: {
|
|
1111
1127
|
logo?: string,
|
|
1112
1128
|
enabled?: boolean,
|
|
1129
|
+
expandedSidebarWidth?: string,
|
|
1113
1130
|
},
|
|
1114
1131
|
singleTheme?: 'light' | 'dark',
|
|
1115
1132
|
datesFormat: string,
|
|
@@ -1139,6 +1156,7 @@ export interface AdminForthConfigForFrontend {
|
|
|
1139
1156
|
pageLabel: string,
|
|
1140
1157
|
slug?: string,
|
|
1141
1158
|
component: string,
|
|
1159
|
+
isVisible?: boolean
|
|
1142
1160
|
}[],
|
|
1143
1161
|
}
|
|
1144
1162
|
|