Implement Pagination for Form and Process Management
- Added pagination functionality to the form management and process management pages, allowing users to navigate through forms and processes more efficiently. - Introduced controls for items per page selection and pagination navigation, enhancing user experience and accessibility of data. - Updated computed properties to handle paginated data and maintain accurate pagination state across filters and searches. - Ensured that pagination resets when filters change, providing a consistent and intuitive user interface.
This commit is contained in:
parent
0c93e93460
commit
77e3b8601f
@ -398,65 +398,6 @@ onMounted(() => {
|
||||
</template>
|
||||
</rs-card>
|
||||
</div>
|
||||
|
||||
<!-- Development Roadmap -->
|
||||
<rs-card>
|
||||
<template #header>
|
||||
<div class="flex items-center">
|
||||
<Icon name="material-symbols:road" class="w-5 h-5 mr-2 text-purple-500" />
|
||||
Development Roadmap & System Guide
|
||||
</div>
|
||||
</template>
|
||||
<template #body>
|
||||
<div class="space-y-6">
|
||||
<div class="bg-blue-50 border border-blue-200 rounded-lg p-4">
|
||||
<h3 class="font-semibold text-blue-900 mb-2">🚀 Getting Started with Corrad BPM</h3>
|
||||
<p class="text-blue-800 text-sm mb-3">
|
||||
Follow these steps to start developing business processes in the system:
|
||||
</p>
|
||||
<ol class="list-decimal list-inside space-y-1 text-sm text-blue-800">
|
||||
<li><strong>Design Forms:</strong> Use Form Builder to create data collection forms</li>
|
||||
<li><strong>Create Processes:</strong> Use Process Builder to design workflow logic</li>
|
||||
<li><strong>Configure Integrations:</strong> Set up API calls and business rules</li>
|
||||
<li><strong>Test & Deploy:</strong> Test process execution and publish to production</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<div
|
||||
v-for="phase in developmentRoadmap"
|
||||
:key="phase.phase"
|
||||
class="border border-gray-200 rounded-lg p-4"
|
||||
>
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<h4 class="font-semibold text-gray-900">{{ phase.phase }}</h4>
|
||||
<RsBadge :class="getStatusColor(phase.status)" size="sm">
|
||||
{{ phase.status.replace('-', ' ') }}
|
||||
</RsBadge>
|
||||
</div>
|
||||
<ul class="space-y-2">
|
||||
<li
|
||||
v-for="item in phase.items"
|
||||
:key="item"
|
||||
class="flex items-start text-sm text-gray-600"
|
||||
>
|
||||
<Icon
|
||||
:name="phase.status === 'completed' ? 'material-symbols:check-circle' :
|
||||
phase.status === 'in-progress' ? 'material-symbols:radio-button-partial' :
|
||||
'material-symbols:radio-button-unchecked'"
|
||||
:class="phase.status === 'completed' ? 'text-green-500' :
|
||||
phase.status === 'in-progress' ? 'text-blue-500' :
|
||||
'text-gray-400'"
|
||||
class="w-4 h-4 mr-2 mt-0.5 flex-shrink-0"
|
||||
/>
|
||||
{{ item }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</rs-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -125,7 +125,7 @@
|
||||
<!-- Forms Grid -->
|
||||
<div v-else-if="filteredForms.length > 0" class="grid gap-4">
|
||||
<div
|
||||
v-for="form in filteredForms"
|
||||
v-for="form in paginatedForms"
|
||||
:key="form.id"
|
||||
class="bg-white rounded-xl shadow-sm border border-gray-200 p-6 hover:shadow-md transition-shadow"
|
||||
>
|
||||
@ -204,8 +204,126 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
<div v-if="filteredForms.length > 0 && totalPages > 1" class="mt-6 bg-white rounded-xl shadow-sm border border-gray-200 p-4">
|
||||
<div class="flex flex-col sm:flex-row items-center justify-between gap-4">
|
||||
<!-- Pagination Info -->
|
||||
<div class="text-sm text-gray-700">
|
||||
Showing {{ paginationInfo.start }}-{{ paginationInfo.end }} of {{ paginationInfo.total }} forms
|
||||
</div>
|
||||
|
||||
<!-- Items per page selector -->
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-sm text-gray-700">Items per page:</span>
|
||||
<FormKit
|
||||
:model-value="itemsPerPage"
|
||||
@update:model-value="changeItemsPerPage"
|
||||
type="select"
|
||||
:options="[
|
||||
{ label: '5', value: 5 },
|
||||
{ label: '10', value: 10 },
|
||||
{ label: '20', value: 20 },
|
||||
{ label: '50', value: 50 }
|
||||
]"
|
||||
:classes="{
|
||||
outer: 'mb-0',
|
||||
input: 'px-2 py-1 border border-gray-300 rounded text-sm bg-white min-w-16'
|
||||
}"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Pagination Controls -->
|
||||
<div class="flex items-center gap-1">
|
||||
<!-- Previous Button -->
|
||||
<button
|
||||
@click="prevPage"
|
||||
:disabled="!hasPrevPage"
|
||||
class="px-3 py-2 text-sm border border-gray-300 rounded-lg bg-white hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||||
>
|
||||
<Icon name="material-symbols:chevron-left" class="w-4 h-4" />
|
||||
</button>
|
||||
|
||||
<!-- Page Numbers -->
|
||||
<template v-if="totalPages <= 7">
|
||||
<!-- Show all pages if 7 or fewer -->
|
||||
<button
|
||||
v-for="page in totalPages"
|
||||
:key="page"
|
||||
@click="goToPage(page)"
|
||||
:class="[
|
||||
'px-3 py-2 text-sm border rounded-lg transition-colors',
|
||||
page === currentPage
|
||||
? 'bg-blue-600 text-white border-blue-600'
|
||||
: 'bg-white text-gray-700 border-gray-300 hover:bg-gray-50'
|
||||
]"
|
||||
>
|
||||
{{ page }}
|
||||
</button>
|
||||
</template>
|
||||
<template v-else>
|
||||
<!-- Show first page -->
|
||||
<button
|
||||
@click="goToPage(1)"
|
||||
:class="[
|
||||
'px-3 py-2 text-sm border rounded-lg transition-colors',
|
||||
1 === currentPage
|
||||
? 'bg-blue-600 text-white border-blue-600'
|
||||
: 'bg-white text-gray-700 border-gray-300 hover:bg-gray-50'
|
||||
]"
|
||||
>
|
||||
1
|
||||
</button>
|
||||
|
||||
<!-- Show ellipsis if needed -->
|
||||
<span v-if="currentPage > 3" class="px-2 text-gray-500">...</span>
|
||||
|
||||
<!-- Show pages around current page -->
|
||||
<button
|
||||
v-for="page in [currentPage - 1, currentPage, currentPage + 1].filter(p => p > 1 && p < totalPages)"
|
||||
:key="page"
|
||||
@click="goToPage(page)"
|
||||
:class="[
|
||||
'px-3 py-2 text-sm border rounded-lg transition-colors',
|
||||
page === currentPage
|
||||
? 'bg-blue-600 text-white border-blue-600'
|
||||
: 'bg-white text-gray-700 border-gray-300 hover:bg-gray-50'
|
||||
]"
|
||||
>
|
||||
{{ page }}
|
||||
</button>
|
||||
|
||||
<!-- Show ellipsis if needed -->
|
||||
<span v-if="currentPage < totalPages - 2" class="px-2 text-gray-500">...</span>
|
||||
|
||||
<!-- Show last page -->
|
||||
<button
|
||||
v-if="totalPages > 1"
|
||||
@click="goToPage(totalPages)"
|
||||
:class="[
|
||||
'px-3 py-2 text-sm border rounded-lg transition-colors',
|
||||
totalPages === currentPage
|
||||
? 'bg-blue-600 text-white border-blue-600'
|
||||
: 'bg-white text-gray-700 border-gray-300 hover:bg-gray-50'
|
||||
]"
|
||||
>
|
||||
{{ totalPages }}
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<!-- Next Button -->
|
||||
<button
|
||||
@click="nextPage"
|
||||
:disabled="!hasNextPage"
|
||||
class="px-3 py-2 text-sm border border-gray-300 rounded-lg bg-white hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||||
>
|
||||
<Icon name="material-symbols:chevron-right" class="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Empty State -->
|
||||
<div v-else class="bg-white rounded-xl shadow-sm border border-gray-200 p-12 text-center">
|
||||
<div v-else-if="filteredForms.length === 0" class="bg-white rounded-xl shadow-sm border border-gray-200 p-12 text-center">
|
||||
<Icon name="material-symbols:description-outline" class="w-16 h-16 text-gray-300 mx-auto mb-4" />
|
||||
<h3 class="text-lg font-medium text-gray-900 mb-2">
|
||||
{{ hasActiveFilters ? 'No forms match your filters' : 'No forms found' }}
|
||||
@ -437,6 +555,11 @@ const jsonContent = ref('');
|
||||
const jsonValidationMessage = ref('');
|
||||
const jsonIsValid = ref(false);
|
||||
|
||||
// Pagination state
|
||||
const currentPage = ref(1);
|
||||
const itemsPerPage = ref(5); // Set to 5 to test pagination
|
||||
const totalItems = ref(0);
|
||||
|
||||
// Computed properties for grouping
|
||||
const categoryOptions = computed(() => {
|
||||
const categories = [...new Set(formStore.savedForms.map(form => form.category).filter(Boolean))];
|
||||
@ -458,7 +581,7 @@ const hasActiveFilters = computed(() => {
|
||||
return searchQuery.value || selectedCategory.value || selectedGroup.value;
|
||||
});
|
||||
|
||||
// Filtered forms
|
||||
// Filtered forms (all forms after applying filters)
|
||||
const filteredForms = computed(() => {
|
||||
let filtered = formStore.savedForms;
|
||||
|
||||
@ -482,9 +605,42 @@ const filteredForms = computed(() => {
|
||||
filtered = filtered.filter(form => form.group === selectedGroup.value);
|
||||
}
|
||||
|
||||
// Update total items for pagination
|
||||
totalItems.value = filtered.length;
|
||||
|
||||
return filtered;
|
||||
});
|
||||
|
||||
// Paginated forms (forms for current page)
|
||||
const paginatedForms = computed(() => {
|
||||
const start = (currentPage.value - 1) * itemsPerPage.value;
|
||||
const end = start + itemsPerPage.value;
|
||||
return filteredForms.value.slice(start, end);
|
||||
});
|
||||
|
||||
// Pagination computed properties
|
||||
const totalPages = computed(() => {
|
||||
return Math.ceil(totalItems.value / itemsPerPage.value);
|
||||
});
|
||||
|
||||
const hasNextPage = computed(() => {
|
||||
return currentPage.value < totalPages.value;
|
||||
});
|
||||
|
||||
const hasPrevPage = computed(() => {
|
||||
return currentPage.value > 1;
|
||||
});
|
||||
|
||||
const paginationInfo = computed(() => {
|
||||
const start = totalItems.value === 0 ? 0 : (currentPage.value - 1) * itemsPerPage.value + 1;
|
||||
const end = Math.min(currentPage.value * itemsPerPage.value, totalItems.value);
|
||||
return {
|
||||
start,
|
||||
end,
|
||||
total: totalItems.value
|
||||
};
|
||||
});
|
||||
|
||||
// Format date for display
|
||||
const formatDate = (isoString) => {
|
||||
if (!isoString) return '';
|
||||
@ -623,6 +779,8 @@ onMounted(async () => {
|
||||
|
||||
// Watch for changes in search and filters
|
||||
watch([searchQuery, selectedCategory, selectedGroup], () => {
|
||||
// Reset to first page when filters change
|
||||
currentPage.value = 1;
|
||||
// Debounce the search to avoid too many API calls
|
||||
clearTimeout(searchTimeout);
|
||||
searchTimeout = setTimeout(() => {
|
||||
@ -637,9 +795,34 @@ const clearFilters = () => {
|
||||
searchQuery.value = '';
|
||||
selectedCategory.value = '';
|
||||
selectedGroup.value = '';
|
||||
currentPage.value = 1; // Reset to first page
|
||||
// loadForms will be called automatically by the watcher
|
||||
};
|
||||
|
||||
// Pagination methods
|
||||
const goToPage = (page) => {
|
||||
if (page >= 1 && page <= totalPages.value) {
|
||||
currentPage.value = page;
|
||||
}
|
||||
};
|
||||
|
||||
const nextPage = () => {
|
||||
if (hasNextPage.value) {
|
||||
currentPage.value++;
|
||||
}
|
||||
};
|
||||
|
||||
const prevPage = () => {
|
||||
if (hasPrevPage.value) {
|
||||
currentPage.value--;
|
||||
}
|
||||
};
|
||||
|
||||
const changeItemsPerPage = (newItemsPerPage) => {
|
||||
itemsPerPage.value = newItemsPerPage;
|
||||
currentPage.value = 1; // Reset to first page when changing items per page
|
||||
};
|
||||
|
||||
// Helper function to get category color
|
||||
const getCategoryColor = (category) => {
|
||||
const colors = {
|
||||
|
@ -29,6 +29,11 @@ const sortBy = ref('processCreatedDate');
|
||||
const sortOrder = ref('desc');
|
||||
const currentView = ref('dashboard'); // 'dashboard', 'list', 'analytics'
|
||||
|
||||
// Pagination state
|
||||
const currentPage = ref(1);
|
||||
const itemsPerPage = ref(5); // Set to 5 to test pagination
|
||||
const totalItems = ref(0);
|
||||
|
||||
// Dashboard metrics and data
|
||||
const dashboardMetrics = ref({
|
||||
totalProcesses: 0,
|
||||
@ -66,9 +71,44 @@ const categoryOptions = [
|
||||
{ value: 'Procurement', label: 'Procurement' }
|
||||
];
|
||||
|
||||
// Filtered processes
|
||||
// Filtered processes (all processes after applying filters)
|
||||
const filteredProcesses = computed(() => {
|
||||
return processStore.processes;
|
||||
let filtered = processStore.processes;
|
||||
|
||||
// Update total items for pagination
|
||||
totalItems.value = filtered.length;
|
||||
|
||||
return filtered;
|
||||
});
|
||||
|
||||
// Paginated processes (processes for current page)
|
||||
const paginatedProcesses = computed(() => {
|
||||
const start = (currentPage.value - 1) * itemsPerPage.value;
|
||||
const end = start + itemsPerPage.value;
|
||||
return filteredProcesses.value.slice(start, end);
|
||||
});
|
||||
|
||||
// Pagination computed properties
|
||||
const totalPages = computed(() => {
|
||||
return Math.ceil(totalItems.value / itemsPerPage.value);
|
||||
});
|
||||
|
||||
const hasNextPage = computed(() => {
|
||||
return currentPage.value < totalPages.value;
|
||||
});
|
||||
|
||||
const hasPrevPage = computed(() => {
|
||||
return currentPage.value > 1;
|
||||
});
|
||||
|
||||
const paginationInfo = computed(() => {
|
||||
const start = totalItems.value === 0 ? 0 : (currentPage.value - 1) * itemsPerPage.value + 1;
|
||||
const end = Math.min(currentPage.value * itemsPerPage.value, totalItems.value);
|
||||
return {
|
||||
start,
|
||||
end,
|
||||
total: totalItems.value
|
||||
};
|
||||
});
|
||||
|
||||
// Load dashboard summary data from API
|
||||
@ -220,6 +260,8 @@ const loadProcesses = async () => {
|
||||
|
||||
// Watch for changes in filters and reload processes
|
||||
watch([searchQuery, statusFilter, categoryFilter], () => {
|
||||
// Reset to first page when filters change
|
||||
currentPage.value = 1;
|
||||
clearTimeout(searchTimeout);
|
||||
searchTimeout = setTimeout(() => {
|
||||
loadProcesses();
|
||||
@ -349,6 +391,31 @@ const clearFilters = () => {
|
||||
searchQuery.value = '';
|
||||
statusFilter.value = '';
|
||||
categoryFilter.value = '';
|
||||
currentPage.value = 1; // Reset to first page
|
||||
};
|
||||
|
||||
// Pagination methods
|
||||
const goToPage = (page) => {
|
||||
if (page >= 1 && page <= totalPages.value) {
|
||||
currentPage.value = page;
|
||||
}
|
||||
};
|
||||
|
||||
const nextPage = () => {
|
||||
if (hasNextPage.value) {
|
||||
currentPage.value++;
|
||||
}
|
||||
};
|
||||
|
||||
const prevPage = () => {
|
||||
if (hasPrevPage.value) {
|
||||
currentPage.value--;
|
||||
}
|
||||
};
|
||||
|
||||
const changeItemsPerPage = (newItemsPerPage) => {
|
||||
itemsPerPage.value = newItemsPerPage;
|
||||
currentPage.value = 1; // Reset to first page when changing items per page
|
||||
};
|
||||
|
||||
// Load processes on component mount
|
||||
@ -653,8 +720,9 @@ const copyWorkflowLink = async (processId) => {
|
||||
|
||||
<!-- Processes Grid -->
|
||||
<div v-else-if="filteredProcesses.length > 0" class="grid gap-4">
|
||||
|
||||
<div
|
||||
v-for="process in filteredProcesses"
|
||||
v-for="process in paginatedProcesses"
|
||||
:key="process.id"
|
||||
class="bg-white rounded-xl shadow-sm border border-gray-200 p-6 hover:shadow-md transition-shadow"
|
||||
>
|
||||
@ -774,8 +842,126 @@ const copyWorkflowLink = async (processId) => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
<div v-if="filteredProcesses.length > 0 && totalPages > 1" class="mt-6 bg-white rounded-xl shadow-sm border border-gray-200 p-4">
|
||||
<div class="flex flex-col sm:flex-row items-center justify-between gap-4">
|
||||
<!-- Pagination Info -->
|
||||
<div class="text-sm text-gray-700">
|
||||
Showing {{ paginationInfo.start }}-{{ paginationInfo.end }} of {{ paginationInfo.total }} processes
|
||||
</div>
|
||||
|
||||
<!-- Items per page selector -->
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-sm text-gray-700">Items per page:</span>
|
||||
<FormKit
|
||||
:model-value="itemsPerPage"
|
||||
@update:model-value="changeItemsPerPage"
|
||||
type="select"
|
||||
:options="[
|
||||
{ label: '5', value: 5 },
|
||||
{ label: '10', value: 10 },
|
||||
{ label: '20', value: 20 },
|
||||
{ label: '50', value: 50 }
|
||||
]"
|
||||
:classes="{
|
||||
outer: 'mb-0',
|
||||
input: 'px-2 py-1 border border-gray-300 rounded text-sm bg-white min-w-16'
|
||||
}"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Pagination Controls -->
|
||||
<div class="flex items-center gap-1">
|
||||
<!-- Previous Button -->
|
||||
<button
|
||||
@click="prevPage"
|
||||
:disabled="!hasPrevPage"
|
||||
class="px-3 py-2 text-sm border border-gray-300 rounded-lg bg-white hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||||
>
|
||||
<Icon name="material-symbols:chevron-left" class="w-4 h-4" />
|
||||
</button>
|
||||
|
||||
<!-- Page Numbers -->
|
||||
<template v-if="totalPages <= 7">
|
||||
<!-- Show all pages if 7 or fewer -->
|
||||
<button
|
||||
v-for="page in totalPages"
|
||||
:key="page"
|
||||
@click="goToPage(page)"
|
||||
:class="[
|
||||
'px-3 py-2 text-sm border rounded-lg transition-colors',
|
||||
page === currentPage
|
||||
? 'bg-blue-600 text-white border-blue-600'
|
||||
: 'bg-white text-gray-700 border-gray-300 hover:bg-gray-50'
|
||||
]"
|
||||
>
|
||||
{{ page }}
|
||||
</button>
|
||||
</template>
|
||||
<template v-else>
|
||||
<!-- Show first page -->
|
||||
<button
|
||||
@click="goToPage(1)"
|
||||
:class="[
|
||||
'px-3 py-2 text-sm border rounded-lg transition-colors',
|
||||
1 === currentPage
|
||||
? 'bg-blue-600 text-white border-blue-600'
|
||||
: 'bg-white text-gray-700 border-gray-300 hover:bg-gray-50'
|
||||
]"
|
||||
>
|
||||
1
|
||||
</button>
|
||||
|
||||
<!-- Show ellipsis if needed -->
|
||||
<span v-if="currentPage > 3" class="px-2 text-gray-500">...</span>
|
||||
|
||||
<!-- Show pages around current page -->
|
||||
<button
|
||||
v-for="page in [currentPage - 1, currentPage, currentPage + 1].filter(p => p > 1 && p < totalPages)"
|
||||
:key="page"
|
||||
@click="goToPage(page)"
|
||||
:class="[
|
||||
'px-3 py-2 text-sm border rounded-lg transition-colors',
|
||||
page === currentPage
|
||||
? 'bg-blue-600 text-white border-blue-600'
|
||||
: 'bg-white text-gray-700 border-gray-300 hover:bg-gray-50'
|
||||
]"
|
||||
>
|
||||
{{ page }}
|
||||
</button>
|
||||
|
||||
<!-- Show ellipsis if needed -->
|
||||
<span v-if="currentPage < totalPages - 2" class="px-2 text-gray-500">...</span>
|
||||
|
||||
<!-- Show last page -->
|
||||
<button
|
||||
v-if="totalPages > 1"
|
||||
@click="goToPage(totalPages)"
|
||||
:class="[
|
||||
'px-3 py-2 text-sm border rounded-lg transition-colors',
|
||||
totalPages === currentPage
|
||||
? 'bg-blue-600 text-white border-blue-600'
|
||||
: 'bg-white text-gray-700 border-gray-300 hover:bg-gray-50'
|
||||
]"
|
||||
>
|
||||
{{ totalPages }}
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<!-- Next Button -->
|
||||
<button
|
||||
@click="nextPage"
|
||||
:disabled="!hasNextPage"
|
||||
class="px-3 py-2 text-sm border border-gray-300 rounded-lg bg-white hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||||
>
|
||||
<Icon name="material-symbols:chevron-right" class="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Empty State -->
|
||||
<div v-else class="bg-white rounded-xl shadow-sm border border-gray-200 p-12 text-center">
|
||||
<div v-else-if="filteredProcesses.length === 0" class="bg-white rounded-xl shadow-sm border border-gray-200 p-12 text-center">
|
||||
<Icon name="material-symbols:folder-open-outline" class="w-16 h-16 text-gray-300 mx-auto mb-4" />
|
||||
<h3 class="text-lg font-medium text-gray-900 mb-2">
|
||||
{{ (searchQuery || statusFilter || categoryFilter) ? 'No processes match your filters' : 'No processes found' }}
|
||||
|
Loading…
x
Reference in New Issue
Block a user