mirror of
https://github.com/lingble/chatwoot.git
synced 2025-11-03 04:27:53 +00:00
feat: Table footer design updates (#9194)
* feat: table footer component cleanup * Update TableFooter.vue * feat: Update design * chore: remove RTL mixin * chore: Make component in composable format * chore: review fixes
This commit is contained in:
@@ -1,88 +1,29 @@
|
|||||||
<template>
|
<template>
|
||||||
<footer
|
<footer
|
||||||
v-if="isFooterVisible"
|
v-if="isFooterVisible"
|
||||||
class="bg-white dark:bg-slate-800 h-12 border-t border-solid border-slate-75 dark:border-slate-700/50 flex items-center justify-between px-6"
|
class="bg-white dark:bg-slate-900 h-12 flex items-center justify-between px-6"
|
||||||
>
|
>
|
||||||
<div class="left-aligned-wrap">
|
<table-footer-results
|
||||||
<div class="text-xs text-slate-600 dark:text-slate-200">
|
:first-index="firstIndex"
|
||||||
<strong>{{ firstIndex }}</strong>
|
:last-index="lastIndex"
|
||||||
- <strong>{{ lastIndex }}</strong> of
|
:total-count="totalCount"
|
||||||
<strong>{{ totalCount }}</strong> items
|
/>
|
||||||
</div>
|
<table-footer-pagination
|
||||||
</div>
|
|
||||||
<div class="right-aligned-wrap">
|
|
||||||
<div
|
|
||||||
v-if="totalCount"
|
v-if="totalCount"
|
||||||
class="primary button-group pagination-button-group"
|
:current-page="currentPage"
|
||||||
>
|
:total-pages="totalPages"
|
||||||
<woot-button
|
:total-count="totalCount"
|
||||||
size="small"
|
:page-size="pageSize"
|
||||||
variant="smooth"
|
@page-change="$emit('page-change', $event)"
|
||||||
color-scheme="secondary"
|
|
||||||
class-names="goto-first"
|
|
||||||
:is-disabled="hasFirstPage"
|
|
||||||
@click="onFirstPage"
|
|
||||||
>
|
|
||||||
<fluent-icon icon="chevron-left" size="18" />
|
|
||||||
<fluent-icon
|
|
||||||
icon="chevron-left"
|
|
||||||
size="18"
|
|
||||||
:class="pageFooterIconClass"
|
|
||||||
/>
|
/>
|
||||||
</woot-button>
|
|
||||||
<woot-button
|
|
||||||
size="small"
|
|
||||||
variant="smooth"
|
|
||||||
color-scheme="secondary"
|
|
||||||
:is-disabled="hasPrevPage"
|
|
||||||
@click="onPrevPage"
|
|
||||||
>
|
|
||||||
<fluent-icon icon="chevron-left" size="18" />
|
|
||||||
</woot-button>
|
|
||||||
<woot-button
|
|
||||||
size="small"
|
|
||||||
variant="smooth"
|
|
||||||
color-scheme="secondary"
|
|
||||||
@click.prevent
|
|
||||||
>
|
|
||||||
{{ currentPage }}
|
|
||||||
</woot-button>
|
|
||||||
<woot-button
|
|
||||||
size="small"
|
|
||||||
variant="smooth"
|
|
||||||
color-scheme="secondary"
|
|
||||||
:is-disabled="hasNextPage"
|
|
||||||
@click="onNextPage"
|
|
||||||
>
|
|
||||||
<fluent-icon icon="chevron-right" size="18" />
|
|
||||||
</woot-button>
|
|
||||||
<woot-button
|
|
||||||
size="small"
|
|
||||||
variant="smooth"
|
|
||||||
color-scheme="secondary"
|
|
||||||
class-names="goto-last"
|
|
||||||
:is-disabled="hasLastPage"
|
|
||||||
@click="onLastPage"
|
|
||||||
>
|
|
||||||
<fluent-icon icon="chevron-right" size="18" />
|
|
||||||
<fluent-icon
|
|
||||||
icon="chevron-right"
|
|
||||||
size="18"
|
|
||||||
:class="pageFooterIconClass"
|
|
||||||
/>
|
|
||||||
</woot-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</footer>
|
</footer>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
import rtlMixin from 'shared/mixins/rtlMixin';
|
import { computed } from 'vue';
|
||||||
|
import TableFooterResults from './TableFooterResults.vue';
|
||||||
export default {
|
import TableFooterPagination from './TableFooterPagination.vue';
|
||||||
components: {},
|
const props = defineProps({
|
||||||
mixins: [rtlMixin],
|
|
||||||
props: {
|
|
||||||
currentPage: {
|
currentPage: {
|
||||||
type: Number,
|
type: Number,
|
||||||
default: 1,
|
default: 1,
|
||||||
@@ -95,77 +36,13 @@ export default {
|
|||||||
type: Number,
|
type: Number,
|
||||||
default: 0,
|
default: 0,
|
||||||
},
|
},
|
||||||
},
|
});
|
||||||
computed: {
|
const totalPages = computed(() => Math.ceil(props.totalCount / props.pageSize));
|
||||||
pageFooterIconClass() {
|
const firstIndex = computed(() => props.pageSize * (props.currentPage - 1) + 1);
|
||||||
return this.isRTLView ? '-mr-3' : '-ml-3';
|
const lastIndex = computed(() =>
|
||||||
},
|
Math.min(props.totalCount, props.pageSize * props.currentPage)
|
||||||
isFooterVisible() {
|
);
|
||||||
return this.totalCount && !(this.firstIndex > this.totalCount);
|
const isFooterVisible = computed(
|
||||||
},
|
() => props.totalCount && !(firstIndex.value > props.totalCount)
|
||||||
firstIndex() {
|
);
|
||||||
return this.pageSize * (this.currentPage - 1) + 1;
|
|
||||||
},
|
|
||||||
lastIndex() {
|
|
||||||
return Math.min(this.totalCount, this.pageSize * this.currentPage);
|
|
||||||
},
|
|
||||||
searchButtonClass() {
|
|
||||||
return this.searchQuery !== '' ? 'show' : '';
|
|
||||||
},
|
|
||||||
hasLastPage() {
|
|
||||||
return !!Math.ceil(this.totalCount / this.pageSize);
|
|
||||||
},
|
|
||||||
hasFirstPage() {
|
|
||||||
return this.currentPage === 1;
|
|
||||||
},
|
|
||||||
hasNextPage() {
|
|
||||||
return this.currentPage === Math.ceil(this.totalCount / this.pageSize);
|
|
||||||
},
|
|
||||||
hasPrevPage() {
|
|
||||||
return this.currentPage === 1;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
onNextPage() {
|
|
||||||
if (this.hasNextPage) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const newPage = this.currentPage + 1;
|
|
||||||
this.onPageChange(newPage);
|
|
||||||
},
|
|
||||||
onPrevPage() {
|
|
||||||
if (this.hasPrevPage) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const newPage = this.currentPage - 1;
|
|
||||||
this.onPageChange(newPage);
|
|
||||||
},
|
|
||||||
onFirstPage() {
|
|
||||||
if (this.hasFirstPage) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const newPage = 1;
|
|
||||||
this.onPageChange(newPage);
|
|
||||||
},
|
|
||||||
onLastPage() {
|
|
||||||
if (this.hasLastPage) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const newPage = Math.ceil(this.totalCount / this.pageSize);
|
|
||||||
this.onPageChange(newPage);
|
|
||||||
},
|
|
||||||
onPageChange(page) {
|
|
||||||
this.$emit('page-change', page);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.goto-first,
|
|
||||||
.goto-last {
|
|
||||||
i:last-child {
|
|
||||||
@apply -ml-1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -0,0 +1,143 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex items-center bg-slate-50 dark:bg-slate-800 h-8 rounded-lg">
|
||||||
|
<woot-button
|
||||||
|
size="small"
|
||||||
|
variant="smooth"
|
||||||
|
color-scheme="secondary"
|
||||||
|
:is-disabled="hasFirstPage"
|
||||||
|
class-names="dark:!bg-slate-800 !opacity-100 ltr:rounded-l-lg ltr:rounded-r-none rtl:rounded-r-lg rtl:rounded-l-none"
|
||||||
|
:class="buttonClass(hasFirstPage)"
|
||||||
|
@click="onFirstPage"
|
||||||
|
>
|
||||||
|
<fluent-icon
|
||||||
|
icon="chevrons-left"
|
||||||
|
size="20"
|
||||||
|
icon-lib="lucide"
|
||||||
|
:class="hasFirstPage && 'opacity-40'"
|
||||||
|
/>
|
||||||
|
</woot-button>
|
||||||
|
<div class="bg-slate-75 dark:bg-slate-700/50 w-px rounded-sm h-4" />
|
||||||
|
<woot-button
|
||||||
|
size="small"
|
||||||
|
variant="smooth"
|
||||||
|
color-scheme="secondary"
|
||||||
|
:is-disabled="hasPrevPage"
|
||||||
|
class-names="dark:!bg-slate-800 !opacity-100 rounded-none"
|
||||||
|
:class="buttonClass(hasPrevPage)"
|
||||||
|
@click="onPrevPage"
|
||||||
|
>
|
||||||
|
<fluent-icon
|
||||||
|
icon="chevron-left-single"
|
||||||
|
size="20"
|
||||||
|
icon-lib="lucide"
|
||||||
|
:class="hasPrevPage && 'opacity-40'"
|
||||||
|
/>
|
||||||
|
</woot-button>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="flex px-3 items-center gap-3 tabular-nums bg-slate-50 dark:bg-slate-800 text-slate-700 dark:text-slate-100"
|
||||||
|
>
|
||||||
|
<span class="text-sm text-slate-800 dark:text-slate-75">
|
||||||
|
{{ currentPage }}
|
||||||
|
</span>
|
||||||
|
<span class="text-slate-600 dark:text-slate-500">/</span>
|
||||||
|
<span class="text-sm text-slate-600 dark:text-slate-500">
|
||||||
|
{{ totalPages }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<woot-button
|
||||||
|
size="small"
|
||||||
|
variant="smooth"
|
||||||
|
color-scheme="secondary"
|
||||||
|
:is-disabled="hasNextPage"
|
||||||
|
class-names="dark:!bg-slate-800 !opacity-100 rounded-none"
|
||||||
|
:class="buttonClass(hasNextPage)"
|
||||||
|
@click="onNextPage"
|
||||||
|
>
|
||||||
|
<fluent-icon
|
||||||
|
icon="chevron-right-single"
|
||||||
|
size="20"
|
||||||
|
icon-lib="lucide"
|
||||||
|
:class="hasNextPage && 'opacity-40'"
|
||||||
|
/>
|
||||||
|
</woot-button>
|
||||||
|
<div class="bg-slate-75 dark:bg-slate-700/50 w-px rounded-sm h-4" />
|
||||||
|
<woot-button
|
||||||
|
size="small"
|
||||||
|
variant="smooth"
|
||||||
|
color-scheme="secondary"
|
||||||
|
class-names="dark:!bg-slate-800 !opacity-100 ltr:rounded-r-lg ltr:rounded-l-none rtl:rounded-l-lg rtl:rounded-r-none"
|
||||||
|
:class="buttonClass(hasLastPage)"
|
||||||
|
:is-disabled="hasLastPage"
|
||||||
|
@click="onLastPage"
|
||||||
|
>
|
||||||
|
<fluent-icon
|
||||||
|
icon="chevrons-right"
|
||||||
|
size="20"
|
||||||
|
icon-lib="lucide"
|
||||||
|
:class="hasLastPage && 'opacity-40'"
|
||||||
|
/>
|
||||||
|
</woot-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { computed, defineEmits } from 'vue';
|
||||||
|
|
||||||
|
// Props
|
||||||
|
const props = defineProps({
|
||||||
|
currentPage: {
|
||||||
|
type: Number,
|
||||||
|
default: 1,
|
||||||
|
},
|
||||||
|
totalCount: {
|
||||||
|
type: Number,
|
||||||
|
default: 0,
|
||||||
|
},
|
||||||
|
totalPages: {
|
||||||
|
type: Number,
|
||||||
|
default: 0,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const hasLastPage = computed(
|
||||||
|
() => props.currentPage === props.totalPages || props.totalPages === 1
|
||||||
|
);
|
||||||
|
const hasFirstPage = computed(() => props.currentPage === 1);
|
||||||
|
const hasNextPage = computed(() => props.currentPage === props.totalPages);
|
||||||
|
const hasPrevPage = computed(() => props.currentPage === 1);
|
||||||
|
|
||||||
|
const emit = defineEmits(['page-change']);
|
||||||
|
|
||||||
|
function buttonClass(hasPage) {
|
||||||
|
if (hasPage) {
|
||||||
|
return 'hover:!bg-slate-50 dark:hover:!bg-slate-800';
|
||||||
|
}
|
||||||
|
return 'dark:hover:!bg-slate-700/50';
|
||||||
|
}
|
||||||
|
|
||||||
|
function onPageChange(newPage) {
|
||||||
|
emit('page-change', newPage);
|
||||||
|
}
|
||||||
|
|
||||||
|
const onNextPage = () => {
|
||||||
|
if (!onNextPage.value) {
|
||||||
|
onPageChange(props.currentPage + 1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const onPrevPage = () => {
|
||||||
|
if (!hasPrevPage.value) {
|
||||||
|
onPageChange(props.currentPage - 1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const onFirstPage = () => {
|
||||||
|
if (!hasFirstPage.value) {
|
||||||
|
onPageChange(1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const onLastPage = () => {
|
||||||
|
if (!hasLastPage.value) {
|
||||||
|
onPageChange(props.totalPages);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
<template>
|
||||||
|
<span class="text-sm text-slate-700 dark:text-slate-200 font-medium">
|
||||||
|
{{
|
||||||
|
$t('GENERAL.SHOWING_RESULTS', {
|
||||||
|
firstIndex,
|
||||||
|
lastIndex,
|
||||||
|
totalCount,
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
defineProps({
|
||||||
|
firstIndex: {
|
||||||
|
type: Number,
|
||||||
|
default: 0,
|
||||||
|
},
|
||||||
|
lastIndex: {
|
||||||
|
type: Number,
|
||||||
|
default: 0,
|
||||||
|
},
|
||||||
|
totalCount: {
|
||||||
|
type: Number,
|
||||||
|
default: 0,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
5
app/javascript/dashboard/i18n/locale/en/general.json
Normal file
5
app/javascript/dashboard/i18n/locale/en/general.json
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"GENERAL": {
|
||||||
|
"SHOWING_RESULTS": "Showing {firstIndex}-{lastIndex} of {totalCount} items"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -31,6 +31,7 @@ import teamsSettings from './teamsSettings.json';
|
|||||||
import whatsappTemplates from './whatsappTemplates.json';
|
import whatsappTemplates from './whatsappTemplates.json';
|
||||||
import sla from './sla.json';
|
import sla from './sla.json';
|
||||||
import inbox from './inbox.json';
|
import inbox from './inbox.json';
|
||||||
|
import general from './general.json';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
...advancedFilters,
|
...advancedFilters,
|
||||||
@@ -66,4 +67,5 @@ export default {
|
|||||||
...teamsSettings,
|
...teamsSettings,
|
||||||
...whatsappTemplates,
|
...whatsappTemplates,
|
||||||
...inbox,
|
...inbox,
|
||||||
|
...general,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -25,6 +25,7 @@
|
|||||||
@on-sort-change="onSortChange"
|
@on-sort-change="onSortChange"
|
||||||
/>
|
/>
|
||||||
<table-footer
|
<table-footer
|
||||||
|
class="border-t border-slate-75 dark:border-slate-700/50"
|
||||||
:current-page="Number(meta.currentPage)"
|
:current-page="Number(meta.currentPage)"
|
||||||
:total-count="meta.count"
|
:total-count="meta.count"
|
||||||
:page-size="15"
|
:page-size="15"
|
||||||
|
|||||||
@@ -59,7 +59,7 @@
|
|||||||
:current-page="currentPage"
|
:current-page="currentPage"
|
||||||
:total-count="totalCount"
|
:total-count="totalCount"
|
||||||
:page-size="pageSize"
|
:page-size="pageSize"
|
||||||
class="dark:bg-slate-900 sticky bottom-0"
|
class="dark:bg-slate-900 sticky bottom-0 border-t border-slate-75 dark:border-slate-700/50"
|
||||||
@page-change="onPageChange"
|
@page-change="onPageChange"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
:on-mark-all-done-click="onMarkAllDoneClick"
|
:on-mark-all-done-click="onMarkAllDoneClick"
|
||||||
/>
|
/>
|
||||||
<table-footer
|
<table-footer
|
||||||
|
class="border-t border-slate-75 dark:border-slate-700/50"
|
||||||
:current-page="Number(meta.currentPage)"
|
:current-page="Number(meta.currentPage)"
|
||||||
:total-count="meta.count"
|
:total-count="meta.count"
|
||||||
:page-size="15"
|
:page-size="15"
|
||||||
|
|||||||
@@ -57,7 +57,7 @@
|
|||||||
:current-page="Number(meta.currentPage)"
|
:current-page="Number(meta.currentPage)"
|
||||||
:total-count="meta.totalEntries"
|
:total-count="meta.totalEntries"
|
||||||
:page-size="meta.perPage"
|
:page-size="meta.perPage"
|
||||||
class="dark:bg-slate-900"
|
class="!bg-slate-25 dark:!bg-slate-900 border-t border-slate-75 dark:border-slate-700/50"
|
||||||
@page-change="onPageChange"
|
@page-change="onPageChange"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -268,5 +268,9 @@
|
|||||||
"M8.66675 11.1667L10.3334 12.8333L13.6667 9.5",
|
"M8.66675 11.1667L10.3334 12.8333L13.6667 9.5",
|
||||||
"M11.1667 17.8333C14.8486 17.8333 17.8333 14.8486 17.8333 11.1667C17.8333 7.48477 14.8486 4.5 11.1667 4.5C7.48477 4.5 4.5 7.48477 4.5 11.1667C4.5 14.8486 7.48477 17.8333 11.1667 17.8333Z",
|
"M11.1667 17.8333C14.8486 17.8333 17.8333 14.8486 17.8333 11.1667C17.8333 7.48477 14.8486 4.5 11.1667 4.5C7.48477 4.5 4.5 7.48477 4.5 11.1667C4.5 14.8486 7.48477 17.8333 11.1667 17.8333Z",
|
||||||
"M19.5001 19.5001L15.9167 15.9167"
|
"M19.5001 19.5001L15.9167 15.9167"
|
||||||
]
|
],
|
||||||
|
"chevrons-left-outline": ["m11 17-5-5 5-5", "m18 17-5-5 5-5"],
|
||||||
|
"chevron-left-single-outline": "m15 18-6-6 6-6",
|
||||||
|
"chevrons-right-outline": ["m6 17 5-5-5-5", "m13 17 5-5-5-5"],
|
||||||
|
"chevron-right-single-outline": "m9 18 6-6-6-6"
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user