v5.0.0 update

This commit is contained in:
Mohit Panjwani
2021-11-30 18:58:19 +05:30
parent d332712c22
commit 082d5cacf2
1253 changed files with 88309 additions and 71741 deletions

View File

@ -0,0 +1,351 @@
<template>
<div class="flex flex-col">
<div class="-my-2 overflow-x-auto sm:-mx-6 lg:-mx-8 pb-4 lg:pb-0">
<div class="inline-block min-w-full py-2 align-middle sm:px-6 lg:px-8">
<div
class="
relative
overflow-hidden
bg-white
border-b border-gray-200
shadow
sm:rounded-lg
"
>
<slot name="header" />
<table :class="tableClass">
<thead :class="theadClass">
<tr>
<th
v-for="column in tableColumns"
:key="column.key"
:class="[
getThClass(column),
{
'text-bold text-black': sort.fieldName === column.key,
},
]"
@click="changeSorting(column)"
>
{{ column.label }}
<span
v-if="sort.fieldName === column.key && sort.order === 'asc'"
class="asc-direction"
>
</span>
<span
v-if="
sort.fieldName === column.key && sort.order === 'desc'
"
class="desc-direction"
>
</span>
</th>
</tr>
</thead>
<tbody
v-if="loadingType === 'placeholder' && (loading || isLoading)"
>
<tr
v-for="placeRow in placeholderCount"
:key="placeRow"
:class="placeRow % 2 === 0 ? 'bg-white' : 'bg-gray-50'"
>
<td
v-for="column in columns"
:key="column.key"
class=""
:class="getTdClass(column)"
>
<base-content-placeholders
:class="getPlaceholderClass(column)"
:rounded="true"
>
<base-content-placeholders-text
class="w-full h-6"
:lines="1"
/>
</base-content-placeholders>
</td>
</tr>
</tbody>
<tbody v-else>
<tr
v-for="(row, index) in sortedRows"
:key="index"
:class="index % 2 === 0 ? 'bg-white' : 'bg-gray-50'"
>
<td
v-for="column in columns"
:key="column.key"
class=""
:class="getTdClass(column)"
>
<slot :name="'cell-' + column.key" :row="row">
{{ lodashGet(row.data, column.key) }}
</slot>
</td>
</tr>
</tbody>
</table>
<div
v-if="loadingType === 'spinner' && (loading || isLoading)"
class="
absolute
top-0
left-0
z-10
flex
items-center
justify-center
w-full
h-full
bg-white bg-opacity-60
"
>
<SpinnerIcon class="w-10 h-10 text-primary-500" />
</div>
<div
v-else-if="
!loading && !isLoading && sortedRows && sortedRows.length === 0
"
class="
text-center text-gray-500
pb-2
flex
h-[160px]
justify-center
items-center
flex-col
"
>
<BaseIcon
name="ExclamationCircleIcon"
class="w-6 h-6 text-gray-400"
/>
<span class="block mt-1">{{ noResultsMessage }}</span>
</div>
<BaseTablePagination
v-if="pagination"
:pagination="pagination"
@pageChange="pageChange"
/>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { computed, onMounted, watch, ref, reactive } from 'vue'
import { get } from 'lodash'
import Row from './Row'
import Column from './Column'
import BaseTablePagination from './BaseTablePagination.vue'
import SpinnerIcon from '@/scripts/components/icons/SpinnerIcon.vue'
const props = defineProps({
columns: {
type: Array,
required: true,
},
data: {
type: [Array, Function],
required: true,
},
sortBy: { type: String, default: '' },
sortOrder: { type: String, default: '' },
tableClass: {
type: String,
default: 'min-w-full divide-y divide-gray-200',
},
theadClass: { type: String, default: 'bg-gray-50' },
tbodyClass: { type: String, default: '' },
noResultsMessage: {
type: String,
default: 'No Results Found',
},
loading: {
type: Boolean,
default: false,
},
loadingType: {
type: String,
default: 'placeholder',
validator: function (value) {
return ['placeholder', 'spinner'].indexOf(value) !== -1
},
},
placeholderCount: {
type: Number,
default: 3,
},
})
let rows = reactive([])
let isLoading = ref(false)
let tableColumns = reactive(props.columns.map((column) => new Column(column)))
let sort = reactive({
fieldName: '',
order: '',
})
let pagination = ref('')
const usesLocalData = computed(() => {
return Array.isArray(props.data)
})
const sortedRows = computed(() => {
if (!usesLocalData.value) {
return rows.value
}
if (sort.fieldName === '') {
return rows.value
}
if (tableColumns.length === 0) {
return rows.value
}
const sortColumn = getColumn(sort.fieldName)
if (!sortColumn) {
return rows.value
}
let sorted = [...rows.value].sort(
sortColumn.getSortPredicate(sort.order, tableColumns)
)
return sorted
})
function getColumn(columnName) {
return tableColumns.find((column) => column.key === columnName)
}
function getThClass(column) {
let classes =
'whitespace-nowrap px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider'
if (column.defaultThClass) {
classes = column.defaultThClass
}
if (column.sortable) {
classes = `${classes} cursor-pointer`
} else {
classes = `${classes} pointer-events-none`
}
if (column.thClass) {
classes = `${classes} ${column.thClass}`
}
return classes
}
function getTdClass(column) {
let classes = 'px-6 py-4 text-sm text-gray-500 whitespace-nowrap'
if (column.defaultTdClass) {
classes = column.defaultTdClass
}
if (column.tdClass) {
classes = `${classes} ${column.tdClass}`
}
return classes
}
function getPlaceholderClass(column) {
let classes = 'w-full'
if (column.placeholderClass) {
classes = `${classes} ${column.placeholderClass}`
}
return classes
}
function prepareLocalData() {
pagination.value = null
return props.data
}
async function fetchServerData() {
const page = (pagination.value && pagination.value.currentPage) || 1
isLoading.value = true
const response = await props.data({
sort,
page,
})
isLoading.value = false
pagination.value = response.pagination
return response.data
}
function changeSorting(column) {
if (sort.fieldName !== column.key) {
sort.fieldName = column.key
sort.order = 'asc'
} else {
sort.order = sort.order === 'asc' ? 'desc' : 'asc'
}
if (!usesLocalData.value) {
mapDataToRows()
}
}
async function mapDataToRows() {
const data = usesLocalData.value
? prepareLocalData()
: await fetchServerData()
rows.value = data.map((rowData) => new Row(rowData, tableColumns))
}
async function pageChange(page) {
pagination.value.currentPage = page
await mapDataToRows()
}
async function refresh() {
await mapDataToRows()
}
function lodashGet(array, key) {
return get(array, key)
}
if (usesLocalData.value) {
watch(
() => props.data,
() => {
mapDataToRows()
}
)
}
onMounted(async () => {
await mapDataToRows()
})
defineExpose({ refresh })
</script>

View File

@ -0,0 +1,361 @@
<template>
<div
v-if="shouldShowPagination"
class="
flex
items-center
justify-between
px-4
py-3
bg-white
border-t border-gray-200
sm:px-6
"
>
<div class="flex justify-between flex-1 sm:hidden">
<a
href="#"
:class="{
'disabled cursor-normal pointer-events-none !bg-gray-100 !text-gray-400':
pagination.currentPage === 1,
}"
class="
relative
inline-flex
items-center
px-4
py-2
text-sm
font-medium
text-gray-700
bg-white
border border-gray-300
rounded-md
hover:bg-gray-50
"
@click="pageClicked(pagination.currentPage - 1)"
>
Previous
</a>
<a
href="#"
:class="{
'disabled cursor-default pointer-events-none !bg-gray-100 !text-gray-400':
pagination.currentPage === pagination.totalPages,
}"
class="
relative
inline-flex
items-center
px-4
py-2
ml-3
text-sm
font-medium
text-gray-700
bg-white
border border-gray-300
rounded-md
hover:bg-gray-50
"
@click="pageClicked(pagination.currentPage + 1)"
>
Next
</a>
</div>
<div class="hidden sm:flex-1 sm:flex sm:items-center sm:justify-between">
<div>
<p class="text-sm text-gray-700">
Showing
{{ ' ' }}
<span
v-if="pagination.limit && pagination.currentPage"
class="font-medium"
>
{{
pagination.currentPage * pagination.limit - (pagination.limit - 1)
}}
</span>
{{ ' ' }}
to
{{ ' ' }}
<span
v-if="pagination.limit && pagination.currentPage"
class="font-medium"
>
<span
v-if="
pagination.currentPage * pagination.limit <=
pagination.totalCount
"
>
{{ pagination.currentPage * pagination.limit }}
</span>
<span v-else>
{{ pagination.totalCount }}
</span>
</span>
{{ ' ' }}
of
{{ ' ' }}
<span v-if="pagination.totalCount" class="font-medium">
{{ pagination.totalCount }}
</span>
{{ ' ' }}
results
</p>
</div>
<div>
<nav
class="relative z-0 inline-flex -space-x-px rounded-md shadow-sm"
aria-label="Pagination"
>
<a
href="#"
:class="{
'disabled cursor-normal pointer-events-none !bg-gray-100 !text-gray-400':
pagination.currentPage === 1,
}"
class="
relative
inline-flex
items-center
px-2
py-2
text-sm
font-medium
text-gray-500
bg-white
border border-gray-300
rounded-l-md
hover:bg-gray-50
"
@click="pageClicked(pagination.currentPage - 1)"
>
<span class="sr-only">Previous</span>
<BaseIcon name="ChevronLeftIcon" />
</a>
<a
v-if="hasFirst"
href="#"
aria-current="page"
:class="{
'z-10 bg-primary-50 border-primary-500 text-primary-600':
isActive(1),
'bg-white border-gray-300 text-gray-500 hover:bg-gray-50':
!isActive(1),
}"
class="
relative
inline-flex
items-center
px-4
py-2
text-sm
font-medium
border
"
@click="pageClicked(1)"
>
1
</a>
<span
v-if="hasFirstEllipsis"
class="
relative
inline-flex
items-center
px-4
py-2
text-sm
font-medium
text-gray-700
bg-white
border border-gray-300
"
>
...
</span>
<a
v-for="page in pages"
:key="page"
href="#"
:class="{
'z-10 bg-primary-50 border-primary-500 text-primary-600':
isActive(page),
'bg-white border-gray-300 text-gray-500 hover:bg-gray-50':
!isActive(page),
disabled: page === '...',
}"
class="
relative
items-center
hidden
px-4
py-2
text-sm
font-medium
text-gray-500
bg-white
border border-gray-300
hover:bg-gray-50
md:inline-flex
"
@click="pageClicked(page)"
>
{{ page }}
</a>
<span
v-if="hasLastEllipsis"
class="
relative
inline-flex
items-center
px-4
py-2
text-sm
font-medium
text-gray-700
bg-white
border border-gray-300
"
>
...
</span>
<a
v-if="hasLast"
href="#"
aria-current="page"
:class="{
'z-10 bg-primary-50 border-primary-500 text-primary-600':
isActive(pagination.totalPages),
'bg-white border-gray-300 text-gray-500 hover:bg-gray-50':
!isActive(pagination.totalPages),
}"
class="
relative
inline-flex
items-center
px-4
py-2
text-sm
font-medium
border
"
@click="pageClicked(pagination.totalPages)"
>
{{ pagination.totalPages }}
</a>
<a
href="#"
class="
relative
inline-flex
items-center
px-2
py-2
text-sm
font-medium
text-gray-500
bg-white
border border-gray-300
rounded-r-md
hover:bg-gray-50
"
:class="{
'disabled cursor-default pointer-events-none !bg-gray-100 !text-gray-400':
pagination.currentPage === pagination.totalPages,
}"
@click="pageClicked(pagination.currentPage + 1)"
>
<span class="sr-only">Next</span>
<BaseIcon name="ChevronRightIcon" />
</a>
</nav>
</div>
</div>
</div>
</template>
<script>
// Todo: Need to convert this to Composition API
export default {
props: {
pagination: {
type: Object,
default: () => ({}),
},
},
computed: {
pages() {
return this.pagination.totalPages === undefined ? [] : this.pageLinks()
},
hasFirst() {
return this.pagination.currentPage >= 4 || this.pagination.totalPages < 10
},
hasLast() {
return (
this.pagination.currentPage <= this.pagination.totalPages - 3 ||
this.pagination.totalPages < 10
)
},
hasFirstEllipsis() {
return (
this.pagination.currentPage >= 4 && this.pagination.totalPages >= 10
)
},
hasLastEllipsis() {
return (
this.pagination.currentPage <= this.pagination.totalPages - 3 &&
this.pagination.totalPages >= 10
)
},
shouldShowPagination() {
if (this.pagination.totalPages === undefined) {
return false
}
if (this.pagination.count === 0) {
return false
}
return this.pagination.totalPages > 1
},
},
methods: {
isActive(page) {
const currentPage = this.pagination.currentPage || 1
return currentPage === page
},
pageClicked(page) {
if (
page === '...' ||
page === this.pagination.currentPage ||
page > this.pagination.totalPages ||
page < 1
) {
return
}
this.$emit('pageChange', page)
},
pageLinks() {
const pages = []
let left = 2
let right = this.pagination.totalPages - 1
if (this.pagination.totalPages >= 10) {
left = Math.max(1, this.pagination.currentPage - 2)
right = Math.min(
this.pagination.currentPage + 2,
this.pagination.totalPages
)
}
for (let i = left; i <= right; i++) {
pages.push(i)
}
return pages
},
},
}
</script>

View File

@ -0,0 +1,66 @@
import { pick } from './helpers';
export default class Column {
constructor(columnObject) {
const properties = pick(columnObject, [
'key', 'label', 'thClass', 'tdClass', 'sortBy', 'sortable', 'hidden', 'dataType'
]);
for (const property in properties) {
this[property] = columnObject[property];
}
if (!properties['dataType']) {
this['dataType'] = 'string'
}
if (properties['sortable'] === undefined) {
this['sortable'] = true
}
}
getFilterFieldName() {
return this.filterOn || this.key;
}
isSortable() {
return this.sortable;
}
getSortPredicate(sortOrder, allColumns) {
const sortFieldName = this.getSortFieldName();
const sortColumn = allColumns.find(column => column.key === sortFieldName);
const dataType = sortColumn.dataType;
if (dataType.startsWith('date') || dataType === 'numeric') {
return (row1, row2) => {
const value1 = row1.getSortableValue(sortFieldName);
const value2 = row2.getSortableValue(sortFieldName);
if (sortOrder === 'desc') {
return value2 < value1 ? -1 : 1;
}
return value1 < value2 ? -1 : 1;
};
}
return (row1, row2) => {
const value1 = row1.getSortableValue(sortFieldName);
const value2 = row2.getSortableValue(sortFieldName);
if (sortOrder === 'desc') {
return value2.localeCompare(value1);
}
return value1.localeCompare(value2);
};
}
getSortFieldName() {
return this.sortBy || this.key;
}
}

View File

@ -0,0 +1,43 @@
import moment from 'moment';
import { get } from './helpers';
export default class Row {
constructor(data, columns) {
this.data = data;
this.columns = columns;
}
getValue(columnName) {
return get(this.data, columnName);
}
getColumn(columnName) {
return this.columns.find(column => column.key === columnName);
}
getSortableValue(columnName) {
const dataType = this.getColumn(columnName).dataType;
let value = this.getValue(columnName);
if (value === undefined || value === null) {
return '';
}
if (value instanceof String) {
value = value.toLowerCase();
}
if (dataType.startsWith('date')) {
const format = dataType.replace('date:', '');
return moment(value, format).format('YYYYMMDDHHmmss');
}
if (dataType === 'numeric') {
return value;
}
return value.toString();
}
}

View File

@ -0,0 +1,30 @@
export function classList(...classes) {
return classes
.map(c => Array.isArray(c) ? c : [c])
.reduce((classes, c) => classes.concat(c), []);
}
export function get(object, path) {
if (!path) {
return object;
}
if (object === null || typeof object !== 'object') {
return object;
}
const [pathHead, pathTail] = path.split(/\.(.+)/);
return get(object[pathHead], pathTail);
}
export function pick(object, properties) {
return properties.reduce((pickedObject, property) => {
pickedObject[property] = object[property];
return pickedObject;
}, {});
}
export function range(from, to) {
return [...Array(to - from)].map((_, i) => i + from);
}