Add generic layout of listing cards
This should eventually by ported to other interfaces for consistency.
This commit is contained in:
parent
6db5186318
commit
83f76297b5
5 changed files with 267 additions and 2 deletions
98
ts/components/layout/ListCard.vue
Normal file
98
ts/components/layout/ListCard.vue
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
<style scoped>
|
||||
.filters-section {
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
</style>
|
||||
<template>
|
||||
<div class="card shadow-sm h-100 reports-list">
|
||||
<div class="card-header bg-light pane-header">
|
||||
<div class="filters-section">
|
||||
<slot
|
||||
:active-filters="activeFilters"
|
||||
:apply-filter="applyFilter"
|
||||
:clear-filters="clearFilters"
|
||||
name="filters"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body scroll-pane">
|
||||
<div class="list-group list-group-flush">
|
||||
<div class="loading list-group-item" v-if="loading || items == null">
|
||||
Loading...
|
||||
</div>
|
||||
<div class="list-group-item" v-else-if="items.length == 0">
|
||||
No items
|
||||
</div>
|
||||
<div
|
||||
v-else-if="items.length == 0"
|
||||
class="list-group-item text-center text-muted p-4"
|
||||
>
|
||||
<i class="bi bi-inbox fs-1"></i>
|
||||
<p class="mt-2">No items match the current filters</p>
|
||||
<button class="btn btn-sm btn-outline-primary" @click="clearFilters">
|
||||
Reset Filters
|
||||
</button>
|
||||
</div>
|
||||
<div v-else v-for="item in filteredItems" :key="item.id">
|
||||
<slot name="item" :item="item" :index="item.id"></slot>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup generic="T extends Record<string, any>" lang="ts">
|
||||
import { ref, computed } from "vue";
|
||||
|
||||
interface Props {
|
||||
items: T[];
|
||||
loading?: boolean;
|
||||
itemKey?: keyof T | ((item: T) => string | number);
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
loading: false,
|
||||
itemKey: "id",
|
||||
});
|
||||
|
||||
defineSlots<{
|
||||
filters(props: {
|
||||
applyFilter: (filterFn: (item: T) => boolean) => void;
|
||||
clearFilters: () => void;
|
||||
activeFilters: number;
|
||||
}): any;
|
||||
item(props: { item: T; index: number }): any;
|
||||
footer(props: { total: number }): any;
|
||||
}>();
|
||||
|
||||
// State
|
||||
const filterFunctions = ref<Array<(item: T) => boolean>>([]);
|
||||
|
||||
// Computed
|
||||
const filteredItems = computed(() => {
|
||||
if (filterFunctions.value.length === 0) {
|
||||
return props.items;
|
||||
}
|
||||
|
||||
return props.items.filter((item) =>
|
||||
filterFunctions.value.every((fn) => fn(item)),
|
||||
);
|
||||
});
|
||||
const activeFilters = computed(() => filterFunctions.value.length);
|
||||
|
||||
// Methods
|
||||
const applyFilter = (filterFn: (item: T) => boolean) => {
|
||||
// Add or replace filter (you can modify this logic as needed)
|
||||
filterFunctions.value.push(filterFn);
|
||||
};
|
||||
|
||||
const clearFilters = () => {
|
||||
filterFunctions.value = [];
|
||||
};
|
||||
|
||||
const getItemKey = (item: T): string | number => {
|
||||
if (typeof props.itemKey === "function") {
|
||||
return props.itemKey(item);
|
||||
}
|
||||
return item[props.itemKey] as string | number;
|
||||
};
|
||||
</script>
|
||||
Loading…
Add table
Add a link
Reference in a new issue