Skip to content

Commit 5c0d291

Browse files
committed
Merge branch 'next' of github.com:devforth/adminforth into next
2 parents a5e0a7a + 5996fa3 commit 5c0d291

File tree

6 files changed

+184
-38
lines changed

6 files changed

+184
-38
lines changed

adminforth/documentation/docs/tutorial/03-Customization/15-afcl.md

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -797,7 +797,7 @@ const isoFlagToEmoji = (iso) => iso.toUpperCase().replace(/./g, char => String.f
797797

798798

799799

800-
## Pagination
800+
### Pagination
801801

802802
Table provides front-end side pagination. You can set `pageSize` (default is 10) to set how many rows to show per page.
803803
If there is less then `pageSize` rows, pagination will not be shown.
@@ -829,6 +829,36 @@ If there is less then `pageSize` rows, pagination will not be shown.
829829
</div>
830830
</div>
831831

832+
### Server-side pagination
833+
834+
To load pages dynamically, simply pass async callback to data:
835+
836+
```ts
837+
async function loadPageData(offset, limit) {
838+
// in real app do await callAdminForthApi or await fetch to get date, use offset and limit value to slice data
839+
return {
840+
data: [
841+
{ name: 'John', age: offset, country: 'US' },
842+
{ name: 'Rick', age: offset+1, country: 'CA' },
843+
{ name: 'Alice', age: offset+2, country: 'BR' },
844+
],
845+
total: 3 // should return total amount of records in database
846+
}
847+
}
848+
849+
<Table
850+
:columns="[
851+
{ label: 'Name', fieldName: 'name' },
852+
{ label: 'Age', fieldName: 'age' },
853+
{ label: 'Country', fieldName: 'country' },
854+
]"
855+
//diff-remove
856+
:data="[...]
857+
//diff-add
858+
:data="loadPageData"
859+
860+
:pageSize="3"
861+
```
832862

833863
## ProgressBar
834864

adminforth/modules/styles.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,9 @@ export const styles = () => ({
251251
lightTableOddBackground: "#FFFFFF",
252252
lightTablePaginationText: "#6B7280",
253253
lightTablePaginationNumeration: "#111827",
254+
lightTablePaginationInputBackground: "#FFFFFF",
255+
lightTablePaginationInputBorder: "#D1D5DB",
256+
lightTablePaginationInputText: "#111827",
254257
lightUnactivePaginationButtonBackground: "#FFFFFF",
255258
lightUnactivePaginationButtonText: "#6B7280",
256259
lightUnactivePaginationButtonBorder: "#D1D5DB",
@@ -580,6 +583,9 @@ export const styles = () => ({
580583
darkTableOddBackground: "#111827",
581584
darkTablePaginationText: "#9CA3AF",
582585
darkTablePaginationNumeration: "#FFFFFF",
586+
darkTablePaginationInputBackground: "#1f2937",
587+
darkTablePaginationInputBorder: "#374151",
588+
darkTablePaginationInputText: "#FFFFFF",
583589
darkUnactivePaginationButtonBackground: "#1F2937",
584590
darkUnactivePaginationButtonText: "#9CA3AF",
585591
darkUnactivePaginationButtonBorder: "#374151",

adminforth/spa/src/afcl/Table.vue

Lines changed: 125 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
<table class="afcl-table w-full text-sm text-left rtl:text-right text-lightTableText dark:text-darkTableText">
55
<thead class="afcl-table-thread text-xs text-lightTableHeadingText uppercase bg-lightTableHeadingBackground dark:bg-darkTableHeadingBackground dark:text-darkTableHeadingText">
66
<tr>
7-
<th scope="col" class="px-6 py-3"
7+
<th scope="col" class="px-6 py-3" ref="headerRefs"
88
v-for="column in columns"
99
>
1010
<slot v-if="$slots[`header:${column.fieldName}`]" :name="`header:${column.fieldName}`" :column="column" />
@@ -16,8 +16,17 @@
1616
</tr>
1717
</thead>
1818
<tbody>
19+
<SkeleteLoader
20+
v-if="isLoading"
21+
:rows="pageSize"
22+
:columns="columns.length"
23+
:row-heights="rowHeights"
24+
:column-widths="columnWidths"
25+
/>
1926
<tr
27+
v-else="!isLoading"
2028
v-for="(item, index) in dataPage"
29+
ref="rowRefs"
2130
:class="{
2231
'afcl-table-body odd:bg-lightTableOddBackground odd:dark:bg-darkTableOddBackground even:bg-lightTableEvenBackground even:dark:bg-darkTableEvenBackground': evenHighlights,
2332
'border-b border-lightTableBorder dark:border-darkTableBorder': index !== dataPage.length - 1 || totalPages > 1,
@@ -31,40 +40,74 @@
3140
:item="item" :column="column"
3241
>
3342
</slot>
34-
<span v-else>
43+
<span v-else-if="!isLoading" >
3544
{{ item[column.fieldName] }}
3645
</span>
46+
<div v-else>
47+
<div class=" w-full">
48+
<Skeleton class="h-4" />
49+
</div>
50+
</div>
3751
</td>
3852
</tr>
3953
</tbody>
4054
</table>
41-
<nav class="afcl-table-pagination-container bg-lightTableBackground dark:bg-darkTableBackground flex items-center flex-column flex-wrap md:flex-row justify-between p-4"
55+
<nav class="afcl-table-pagination-container bg-lightTableBackground dark:bg-darkTableBackground mt-2 flex flex-col gap-2 items-center sm:flex-row justify-center sm:justify-between px-4 pb-4"
4256
v-if="totalPages > 1"
4357
:aria-label="$t('Table navigation')">
44-
<i18n-t
45-
keypath="Showing {from} to {to} of {total}" tag="span" class="afcl-table-pagination-text text-sm font-normal text-lightTablePaginationText dark:text-darkTablePaginationText mb-4 md:mb-0 block w-full md:inline md:w-auto"
46-
>
47-
<template #from><span class="font-semibold text-lightTablePaginationNumeration dark:text-darkTablePaginationNumeration">{{ Math.min((currentPage - 1) * props.pageSize + 1, props.data.length) }}</span></template>
48-
<template #to><span class="font-semibold text-lightTablePaginationNumeration dark:text-darkTablePaginationNumeration">{{ Math.min(currentPage * props.pageSize, props.data.length) }}</span></template>
49-
<template #total><span class="font-semibold text-lightTablePaginationNumeration dark:text-darkTablePaginationNumeration">{{ props.data.length }}</span></template>
50-
</i18n-t>
51-
52-
<ul class="afcl-table-pagination-list inline-flex -space-x-px rtl:space-x-reverse text-sm h-8">
53-
<li v-for="page in totalPages" :key="page">
54-
<a href="#"
55-
@click.prevent="switchPage(page)"
56-
:aria-current="page === page ? 'page' : undefined"
57-
:class='{
58-
"afcl-table-pagination-button text-blue-600 bg-lightActivePaginationButtonBackground text-lightActivePaginationButtonText dark:bg-darkActivePaginationButtonBackground dark:text-darkActivePaginationButtonText hover:opacity-90": page === currentPage,
59-
"text-lightUnactivePaginationButtonText border bg-lightUnactivePaginationButtonBackground border-lightUnactivePaginationButtonBorder hover:bg-lightUnactivePaginationButtonHoverBackground hover:text-lightUnactivePaginationButtonHoverText dark:bg-darkUnactivePaginationButtonBackground dark:border-darkUnactivePaginationButtonBorder dark:text-darkUnactivePaginationButtonText dark:hover:bg-darkUnactivePaginationButtonHoverBackground dark:hover:text-darkUnactivePaginationButtonHoverText": page !== currentPage,
60-
"rounded-s-lg ms-0": page === 1,
61-
"rounded-e-lg": page === totalPages,
62-
}'
63-
class="flex items-center justify-center px-3 h-8 leading-tight ">
64-
{{ page }}
65-
</a>
66-
</li>
67-
</ul>
58+
<i18n-t
59+
keypath="Showing {from} to {to} of {total}" tag="span" class="afcl-table-pagination-text text-sm font-normal text-center text-lightTablePaginationText dark:text-darkTablePaginationText sm:mb-4 md:mb-0 block w-full md:inline md:w-auto"
60+
>
61+
<template #from><span class="font-semibold text-lightTablePaginationNumeration dark:text-darkTablePaginationNumeration">{{ Math.min((currentPage - 1) * props.pageSize + 1, dataResult.total) }}</span></template>
62+
<template #to><span class="font-semibold text-lightTablePaginationNumeration dark:text-darkTablePaginationNumeration">{{ Math.min(currentPage * props.pageSize, dataResult.total) }}</span></template>
63+
<template #total><span class="font-semibold text-lightTablePaginationNumeration dark:text-darkTablePaginationNumeration">{{ dataResult.total }}</span></template>
64+
</i18n-t>
65+
<div class="af-pagination-container flex flex-row items-center xs:flex-row xs:justify-between xs:items-center gap-3">
66+
<div class="inline-flex">
67+
<!-- Buttons -->
68+
<button
69+
class="flex items-center py-1 px-3 gap-1 text-sm font-medium text-lightActivePaginationButtonText bg-lightActivePaginationButtonBackground border-r-0 rounded-s hover:opacity-90 dark:bg-darkActivePaginationButtonBackground dark:text-darkActivePaginationButtonText disabled:opacity-50"
70+
@click="currentPage--; pageInput = currentPage.toString();" :disabled="currentPage <= 1">
71+
<svg class="w-3.5 h-3.5 rtl:rotate-180" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none"
72+
viewBox="0 0 14 10">
73+
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
74+
d="M13 5H1m0 0 4 4M1 5l4-4"/>
75+
</svg>
76+
</button>
77+
<button
78+
class="flex items-center py-1 px-3 text-sm font-medium text-lightUnactivePaginationButtonText focus:outline-none bg-lightUnactivePaginationButtonBackground border-r-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"
79+
@click="switchPage(1); pageInput = currentPage.toString();" :disabled="currentPage <= 1">
80+
<!-- <IconChevronDoubleLeftOutline class="w-4 h-4" /> -->
81+
1
82+
</button>
83+
<div
84+
contenteditable="true"
85+
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"
86+
@keydown="onPageKeydown($event)"
87+
@input="onPageInput($event)"
88+
@blur="validatePageInput()"
89+
>
90+
{{ pageInput }}
91+
</div>
92+
93+
<button
94+
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"
95+
@click="currentPage = totalPages; pageInput = currentPage.toString();" :disabled="currentPage >= totalPages">
96+
{{ totalPages }}
97+
98+
<!-- <IconChevronDoubleRightOutline class="w-4 h-4" /> -->
99+
</button>
100+
<button
101+
class="flex items-center py-1 px-3 gap-1 text-sm font-medium text-lightActivePaginationButtonText focus:outline-none bg-lightActivePaginationButtonBackground border-l-0 rounded-e hover:opacity-90 dark:bg-darkActivePaginationButtonBackground dark:text-darkActivePaginationButtonText disabled:opacity-50"
102+
@click="currentPage++; pageInput = currentPage.toString();" :disabled="currentPage >= totalPages">
103+
<svg class="w-3.5 h-3.5 rtl:rotate-180" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none"
104+
viewBox="0 0 14 10">
105+
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
106+
d="M1 5h12m0 0L9 1m4 4L9 9"/>
107+
</svg>
108+
</button>
109+
</div>
110+
</div>
68111
</nav>
69112
</div>
70113

@@ -73,7 +116,9 @@
73116
</template>
74117

75118
<script setup lang="ts">
76-
import { ref, type Ref, computed } from 'vue';
119+
import { ref, type Ref, computed, useTemplateRef, watch, nextTick } from 'vue';
120+
import { asyncComputed } from '@vueuse/core';
121+
import SkeleteLoader from '@/components/SkeleteLoader.vue';
77122
78123
const props = withDefaults(
79124
defineProps<{
@@ -83,7 +128,7 @@
83128
}[],
84129
data: {
85130
[key: string]: any,
86-
}[],
131+
}[] | ((offset: number, limit: number) => Promise<{data: {[key: string]: any}[], total: number}>),
87132
evenHighlights?: boolean,
88133
pageSize?: number,
89134
}>(), {
@@ -93,15 +138,38 @@
93138
);
94139
95140
const currentPage = ref(1);
141+
const isLoading = ref(false);
142+
const pageInput = ref('1');
143+
const rowRefs = useTemplateRef<HTMLElement[]>('rowRefs');
144+
const headerRefs = useTemplateRef<HTMLElement[]>('headerRefs');
145+
const rowHeights = ref<number[]>([]);
146+
const columnWidths = ref<number[]>([]);
147+
148+
const dataResult = asyncComputed( async() => {
149+
if (typeof props.data === 'function') {
150+
isLoading.value = true;
151+
const result = await props.data(currentPage.value, props.pageSize);
152+
isLoading.value = false;
153+
return result;
154+
}
155+
const start = (currentPage.value - 1) * props.pageSize;
156+
const end = start + props.pageSize;
157+
return { data: props.data.slice(start, end), total: props.data.length };
158+
});
159+
160+
watch(() => currentPage.value, () => {
161+
// rows are set to null when new records are loading
162+
rowHeights.value = !rowRefs.value ? [] : rowRefs.value.map((el: HTMLElement) => el.offsetHeight);
163+
columnWidths.value = !headerRefs.value ? [] : headerRefs.value.map((el: HTMLElement) => el.offsetWidth);
164+
});
165+
96166
97167
const totalPages = computed(() => {
98-
return Math.ceil(props.data.length / props.pageSize);
168+
return dataResult.value?.total ? Math.ceil(dataResult.value.total / props.pageSize) : 1;
99169
});
100170
101-
const dataPage = computed(() => {
102-
const start = (currentPage.value - 1) * props.pageSize;
103-
const end = start + props.pageSize;
104-
return props.data.slice(start, end);
171+
const dataPage = asyncComputed( async() => {
172+
return dataResult.value.data;
105173
});
106174
107175
function switchPage(p: number) {
@@ -112,5 +180,28 @@
112180
'update:activeTab',
113181
]);
114182
183+
function onPageInput(event: any) {
184+
pageInput.value = event.target.innerText;
185+
}
186+
187+
function validatePageInput() {
188+
const newPage = parseInt(pageInput.value) || 1;
189+
const validPage = Math.max(1, Math.min(newPage, totalPages.value));
190+
currentPage.value = validPage;
191+
pageInput.value = validPage.toString();
192+
}
193+
194+
async function onPageKeydown(event: any) {
195+
// page input should accept only numbers, arrow keys and backspace
196+
if (['Enter', 'Space'].includes(event.code) ||
197+
(!['Backspace', 'ArrowRight', 'ArrowLeft'].includes(event.code)
198+
&& isNaN(Number(String.fromCharCode(event.keyCode || 0))))) {
199+
event.preventDefault();
200+
if (event.code === 'Enter') {
201+
validatePageInput();
202+
event.target.blur();
203+
}
204+
}
205+
}
115206
116207
</script>

adminforth/spa/src/components/ThreeDotsMenu.vue

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,9 @@
2121
'pointer-events-none': checkboxes && checkboxes.length === 0 && item.meta?.disabledWhenNoCheckboxes,
2222
'opacity-50': checkboxes && checkboxes.length === 0 && item.meta?.disabledWhenNoCheckboxes,
2323
'cursor-not-allowed': checkboxes && checkboxes.length === 0 && item.meta?.disabledWhenNoCheckboxes,
24-
}">
25-
<component :is="getCustomComponent(item)"
24+
}"
25+
@click="injectedComponentClick(i)">
26+
<component :ref="(el: any) => setComponentRef(el, i)" :is="getCustomComponent(item)"
2627
:meta="item.meta"
2728
:resource="coreStore.resource"
2829
:adminUser="coreStore.adminUser"
@@ -75,10 +76,12 @@ import adminforth from '@/adminforth';
7576
import { callAdminForthApi } from '@/utils';
7677
import { useRoute, useRouter } from 'vue-router';
7778
import type { AdminForthComponentDeclarationFull, AdminForthBulkActionCommon, AdminForthActionInput } from '@/types/Common.js';
79+
import { ref, type ComponentPublicInstance } from 'vue';
7880
7981
const route = useRoute();
8082
const coreStore = useCoreStore();
8183
const router = useRouter();
84+
const threeDotsDropdownItemsRefs = ref<Array<ComponentPublicInstance | null>>([]);
8285
8386
const props = defineProps({
8487
threeDotsDropdownItems: Array<AdminForthComponentDeclarationFull>,
@@ -95,6 +98,12 @@ const props = defineProps({
9598
9699
const emit = defineEmits(['startBulkAction']);
97100
101+
function setComponentRef(el: ComponentPublicInstance | null, index: number) {
102+
if (el) {
103+
threeDotsDropdownItemsRefs.value[index] = el;
104+
}
105+
}
106+
98107
async function handleActionClick(action: AdminForthActionInput) {
99108
adminforth.list.closeThreeDotsDropdown();
100109
@@ -151,4 +160,11 @@ function startBulkAction(actionId: string) {
151160
adminforth.list.closeThreeDotsDropdown();
152161
emit('startBulkAction', actionId);
153162
}
163+
164+
async function injectedComponentClick(index: number) {
165+
const componentRef = threeDotsDropdownItemsRefs.value[index];
166+
if (componentRef && 'click' in componentRef) {
167+
(componentRef as any).click?.();
168+
}
169+
}
154170
</script>

adminforth/spa/vite.config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ export default defineConfig({
6969
},
7070
},
7171
plugins: [
72-
ignoreTailwindErrors(),
72+
//ignoreTailwindErrors(),
7373
vue(),
7474
],
7575
resolve: {

adminforth/types/Back.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1164,6 +1164,9 @@ export class Filters {
11641164
static LIKE(field: string, value: any): IAdminForthSingleFilter {
11651165
return { field, operator: AdminForthFilterOperators.LIKE, value };
11661166
}
1167+
static ILIKE(field: string, value: any): IAdminForthSingleFilter {
1168+
return { field, operator: AdminForthFilterOperators.ILIKE, value };
1169+
}
11671170
static AND(
11681171
...args: (IAdminForthSingleFilter | IAdminForthAndOrFilter | Array<IAdminForthSingleFilter | IAdminForthAndOrFilter>)[]
11691172
): IAdminForthAndOrFilter {

0 commit comments

Comments
 (0)