WIP - Added pagination for list view
This commit is contained in:
@@ -576,9 +576,46 @@ export class ObjectService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply additional filters
|
// Extract pagination and sorting parameters from query string
|
||||||
if (filters) {
|
const {
|
||||||
query = query.where(filters);
|
page,
|
||||||
|
pageSize,
|
||||||
|
sortField,
|
||||||
|
sortDirection,
|
||||||
|
...rawFilters
|
||||||
|
} = filters || {};
|
||||||
|
|
||||||
|
const reservedFilterKeys = new Set(['page', 'pageSize', 'sortField', 'sortDirection']);
|
||||||
|
const filterEntries = Object.entries(rawFilters || {}).filter(
|
||||||
|
([key, value]) =>
|
||||||
|
!reservedFilterKeys.has(key) &&
|
||||||
|
value !== undefined &&
|
||||||
|
value !== null &&
|
||||||
|
value !== '',
|
||||||
|
);
|
||||||
|
|
||||||
|
if (filterEntries.length > 0) {
|
||||||
|
query = query.where(builder => {
|
||||||
|
for (const [key, value] of filterEntries) {
|
||||||
|
builder.where(key, value as any);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sortField) {
|
||||||
|
query = query.orderBy(sortField, sortDirection === 'desc' ? 'desc' : 'asc');
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsedPage = Number.isFinite(Number(page)) ? Number(page) : 1;
|
||||||
|
const parsedPageSize = Number.isFinite(Number(pageSize)) ? Number(pageSize) : 0;
|
||||||
|
const safePage = parsedPage > 0 ? parsedPage : 1;
|
||||||
|
const safePageSize = parsedPageSize > 0 ? Math.min(parsedPageSize, 500) : 0;
|
||||||
|
const shouldPaginate = safePageSize > 0;
|
||||||
|
|
||||||
|
const totalCount = await query.clone().resultSize();
|
||||||
|
|
||||||
|
if (shouldPaginate) {
|
||||||
|
query = query.offset((safePage - 1) * safePageSize).limit(safePageSize);
|
||||||
}
|
}
|
||||||
|
|
||||||
const records = await query.select('*');
|
const records = await query.select('*');
|
||||||
@@ -590,7 +627,12 @@ export class ObjectService {
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
return filteredRecords;
|
return {
|
||||||
|
data: filteredRecords,
|
||||||
|
totalCount,
|
||||||
|
page: shouldPaginate ? safePage : 1,
|
||||||
|
pageSize: shouldPaginate ? safePageSize : filteredRecords.length,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async getRecord(
|
async getRecord(
|
||||||
@@ -952,6 +994,73 @@ export class ObjectService {
|
|||||||
return { success: true };
|
return { success: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async deleteRecords(
|
||||||
|
tenantId: string,
|
||||||
|
objectApiName: string,
|
||||||
|
recordIds: string[],
|
||||||
|
userId: string,
|
||||||
|
) {
|
||||||
|
if (!Array.isArray(recordIds) || recordIds.length === 0) {
|
||||||
|
throw new BadRequestException('No record IDs provided');
|
||||||
|
}
|
||||||
|
|
||||||
|
const resolvedTenantId = await this.tenantDbService.resolveTenantId(tenantId);
|
||||||
|
const knex = await this.tenantDbService.getTenantKnexById(resolvedTenantId);
|
||||||
|
|
||||||
|
// Get user with roles and permissions
|
||||||
|
const user = await User.query(knex)
|
||||||
|
.findById(userId)
|
||||||
|
.withGraphFetched('[roles.[objectPermissions, fieldPermissions]]');
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
throw new NotFoundException('User not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get object definition with authorization settings
|
||||||
|
const objectDefModel = await ObjectDefinition.query(knex)
|
||||||
|
.findOne({ apiName: objectApiName });
|
||||||
|
|
||||||
|
if (!objectDefModel) {
|
||||||
|
throw new NotFoundException(`Object ${objectApiName} not found`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const tableName = this.getTableName(
|
||||||
|
objectDefModel.apiName,
|
||||||
|
objectDefModel.label,
|
||||||
|
objectDefModel.pluralLabel,
|
||||||
|
);
|
||||||
|
|
||||||
|
const records = await knex(tableName).whereIn('id', recordIds);
|
||||||
|
if (records.length === 0) {
|
||||||
|
throw new NotFoundException('No records found to delete');
|
||||||
|
}
|
||||||
|
|
||||||
|
const foundIds = new Set(records.map((record: any) => record.id));
|
||||||
|
const missingIds = recordIds.filter(id => !foundIds.has(id));
|
||||||
|
if (missingIds.length > 0) {
|
||||||
|
throw new NotFoundException(`Records not found: ${missingIds.join(', ')}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if user can delete each record
|
||||||
|
for (const record of records) {
|
||||||
|
await this.authService.assertCanPerformAction('delete', objectDefModel, record, user, knex);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure model is registered
|
||||||
|
await this.ensureModelRegistered(resolvedTenantId, objectApiName, objectDefModel);
|
||||||
|
|
||||||
|
// Use Objection model
|
||||||
|
const boundModel = await this.modelService.getBoundModel(resolvedTenantId, objectApiName);
|
||||||
|
await boundModel.query().whereIn('id', recordIds).delete();
|
||||||
|
|
||||||
|
// Remove from search index
|
||||||
|
await Promise.all(
|
||||||
|
recordIds.map((id) => this.removeIndexedRecord(resolvedTenantId, objectApiName, id)),
|
||||||
|
);
|
||||||
|
|
||||||
|
return { success: true, deleted: recordIds.length };
|
||||||
|
}
|
||||||
|
|
||||||
private async indexRecord(
|
private async indexRecord(
|
||||||
tenantId: string,
|
tenantId: string,
|
||||||
objectApiName: string,
|
objectApiName: string,
|
||||||
|
|||||||
@@ -95,4 +95,20 @@ export class RuntimeObjectController {
|
|||||||
user.userId,
|
user.userId,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Post(':objectApiName/records/bulk-delete')
|
||||||
|
async deleteRecords(
|
||||||
|
@TenantId() tenantId: string,
|
||||||
|
@Param('objectApiName') objectApiName: string,
|
||||||
|
@Body() body: { recordIds?: string[]; ids?: string[] },
|
||||||
|
@CurrentUser() user: any,
|
||||||
|
) {
|
||||||
|
const recordIds: string[] = body?.recordIds || body?.ids || [];
|
||||||
|
return this.objectService.deleteRecords(
|
||||||
|
tenantId,
|
||||||
|
objectApiName,
|
||||||
|
recordIds,
|
||||||
|
user.userId,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -78,7 +78,9 @@ const fetchRecords = async () => {
|
|||||||
try {
|
try {
|
||||||
const endpoint = `${props.baseUrl}/${relationObject.value}/records`
|
const endpoint = `${props.baseUrl}/${relationObject.value}/records`
|
||||||
const response = await api.get(endpoint)
|
const response = await api.get(endpoint)
|
||||||
records.value = response || []
|
records.value = Array.isArray(response)
|
||||||
|
? response
|
||||||
|
: response?.data || response?.records || []
|
||||||
|
|
||||||
// If we have a modelValue, find the selected record
|
// If we have a modelValue, find the selected record
|
||||||
if (props.modelValue) {
|
if (props.modelValue) {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, onMounted } from 'vue'
|
import { ref, computed, watch } from 'vue'
|
||||||
import {
|
import {
|
||||||
Table,
|
Table,
|
||||||
TableBody,
|
TableBody,
|
||||||
@@ -22,6 +22,7 @@ interface Props {
|
|||||||
loading?: boolean
|
loading?: boolean
|
||||||
selectable?: boolean
|
selectable?: boolean
|
||||||
baseUrl?: string
|
baseUrl?: string
|
||||||
|
totalCount?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
@@ -41,6 +42,8 @@ const emit = defineEmits<{
|
|||||||
'sort': [field: string, direction: 'asc' | 'desc']
|
'sort': [field: string, direction: 'asc' | 'desc']
|
||||||
'search': [query: string]
|
'search': [query: string]
|
||||||
'refresh': []
|
'refresh': []
|
||||||
|
'page-change': [page: number, pageSize: number]
|
||||||
|
'load-more': [page: number, pageSize: number]
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
// State
|
// State
|
||||||
@@ -48,12 +51,46 @@ const selectedRows = ref<Set<string>>(new Set())
|
|||||||
const searchQuery = ref('')
|
const searchQuery = ref('')
|
||||||
const sortField = ref<string>('')
|
const sortField = ref<string>('')
|
||||||
const sortDirection = ref<'asc' | 'desc'>('asc')
|
const sortDirection = ref<'asc' | 'desc'>('asc')
|
||||||
|
const currentPage = ref(1)
|
||||||
|
|
||||||
// Computed
|
// Computed
|
||||||
const visibleFields = computed(() =>
|
const visibleFields = computed(() =>
|
||||||
props.config.fields.filter(f => f.showOnList !== false)
|
props.config.fields.filter(f => f.showOnList !== false)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const pageSize = computed(() => props.config.pageSize ?? 10)
|
||||||
|
const maxFrontendRecords = computed(() => props.config.maxFrontendRecords ?? 500)
|
||||||
|
const totalRecords = computed(() =>
|
||||||
|
(props.totalCount && props.totalCount > 0)
|
||||||
|
? props.totalCount
|
||||||
|
: props.data.length
|
||||||
|
)
|
||||||
|
const useHybridPagination = computed(() => totalRecords.value > maxFrontendRecords.value)
|
||||||
|
const totalPages = computed(() => Math.max(1, Math.ceil(totalRecords.value / pageSize.value)))
|
||||||
|
const loadedPages = computed(() => Math.max(1, Math.ceil(props.data.length / pageSize.value)))
|
||||||
|
const availablePages = computed(() => {
|
||||||
|
if (useHybridPagination.value && props.totalCount && props.data.length < props.totalCount) {
|
||||||
|
return loadedPages.value
|
||||||
|
}
|
||||||
|
return totalPages.value
|
||||||
|
})
|
||||||
|
const startIndex = computed(() => (currentPage.value - 1) * pageSize.value)
|
||||||
|
const paginatedData = computed(() => {
|
||||||
|
const start = startIndex.value
|
||||||
|
const end = start + pageSize.value
|
||||||
|
return props.data.slice(start, end)
|
||||||
|
})
|
||||||
|
const pageStart = computed(() => (props.data.length === 0 ? 0 : startIndex.value + 1))
|
||||||
|
const pageEnd = computed(() => Math.min(startIndex.value + paginatedData.value.length, totalRecords.value))
|
||||||
|
const showPagination = computed(() => totalRecords.value > pageSize.value)
|
||||||
|
const canGoPrev = computed(() => currentPage.value > 1)
|
||||||
|
const canGoNext = computed(() => currentPage.value < availablePages.value)
|
||||||
|
const showLoadMore = computed(() => (
|
||||||
|
useHybridPagination.value &&
|
||||||
|
Boolean(props.totalCount) &&
|
||||||
|
props.data.length < totalRecords.value
|
||||||
|
))
|
||||||
|
|
||||||
const allSelected = computed({
|
const allSelected = computed({
|
||||||
get: () => props.data.length > 0 && selectedRows.value.size === props.data.length,
|
get: () => props.data.length > 0 && selectedRows.value.size === props.data.length,
|
||||||
set: (val: boolean) => {
|
set: (val: boolean) => {
|
||||||
@@ -96,6 +133,28 @@ const handleSearch = () => {
|
|||||||
const handleAction = (actionId: string) => {
|
const handleAction = (actionId: string) => {
|
||||||
emit('action', actionId, getSelectedRows())
|
emit('action', actionId, getSelectedRows())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const goToPage = (page: number) => {
|
||||||
|
const nextPage = Math.min(Math.max(page, 1), availablePages.value)
|
||||||
|
if (nextPage !== currentPage.value) {
|
||||||
|
currentPage.value = nextPage
|
||||||
|
emit('page-change', nextPage, pageSize.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadMore = () => {
|
||||||
|
const nextPage = Math.ceil(props.data.length / pageSize.value) + 1
|
||||||
|
emit('load-more', nextPage, pageSize.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => [props.data.length, totalRecords.value, pageSize.value],
|
||||||
|
() => {
|
||||||
|
if (currentPage.value > availablePages.value) {
|
||||||
|
currentPage.value = availablePages.value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -192,7 +251,7 @@ const handleAction = (actionId: string) => {
|
|||||||
</TableRow>
|
</TableRow>
|
||||||
<TableRow
|
<TableRow
|
||||||
v-else
|
v-else
|
||||||
v-for="row in data"
|
v-for="row in paginatedData"
|
||||||
:key="row.id"
|
:key="row.id"
|
||||||
class="cursor-pointer hover:bg-muted/50"
|
class="cursor-pointer hover:bg-muted/50"
|
||||||
@click="emit('row-click', row)"
|
@click="emit('row-click', row)"
|
||||||
@@ -227,7 +286,26 @@ const handleAction = (actionId: string) => {
|
|||||||
</Table>
|
</Table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Pagination would go here -->
|
<div v-if="showPagination" class="flex flex-wrap items-center justify-between gap-3 text-sm text-muted-foreground">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span>Showing {{ pageStart }}-{{ pageEnd }} of {{ totalRecords }} records</span>
|
||||||
|
<span v-if="showLoadMore">
|
||||||
|
(loaded {{ data.length }})
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
|
<Button variant="outline" size="sm" :disabled="!canGoPrev" @click="goToPage(currentPage - 1)">
|
||||||
|
Previous
|
||||||
|
</Button>
|
||||||
|
<span class="px-2">Page {{ currentPage }} of {{ totalPages }}</span>
|
||||||
|
<Button variant="outline" size="sm" :disabled="!canGoNext" @click="goToPage(currentPage + 1)">
|
||||||
|
Next
|
||||||
|
</Button>
|
||||||
|
<Button v-if="showLoadMore" variant="secondary" size="sm" @click="loadMore">
|
||||||
|
Load more
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -78,7 +78,8 @@ export const useFields = () => {
|
|||||||
objectApiName: objectDef.apiName,
|
objectApiName: objectDef.apiName,
|
||||||
mode: 'list' as ViewMode,
|
mode: 'list' as ViewMode,
|
||||||
fields,
|
fields,
|
||||||
pageSize: 25,
|
pageSize: 10,
|
||||||
|
maxFrontendRecords: 500,
|
||||||
searchable: true,
|
searchable: true,
|
||||||
filterable: true,
|
filterable: true,
|
||||||
exportable: true,
|
exportable: true,
|
||||||
@@ -183,6 +184,7 @@ export const useViewState = <T extends { id?: string }>(
|
|||||||
apiEndpoint: string
|
apiEndpoint: string
|
||||||
) => {
|
) => {
|
||||||
const records = ref<T[]>([])
|
const records = ref<T[]>([])
|
||||||
|
const totalCount = ref(0)
|
||||||
const currentRecord = ref<T | null>(null)
|
const currentRecord = ref<T | null>(null)
|
||||||
const currentView = ref<'list' | 'detail' | 'edit'>('list')
|
const currentView = ref<'list' | 'detail' | 'edit'>('list')
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
@@ -191,13 +193,51 @@ export const useViewState = <T extends { id?: string }>(
|
|||||||
|
|
||||||
const { api } = useApi()
|
const { api } = useApi()
|
||||||
|
|
||||||
const fetchRecords = async (params?: Record<string, any>) => {
|
const normalizeListResponse = (response: any) => {
|
||||||
|
const payload: { data: T[]; totalCount: number; page?: number; pageSize?: number } = {
|
||||||
|
data: [],
|
||||||
|
totalCount: 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(response)) {
|
||||||
|
payload.data = response
|
||||||
|
payload.totalCount = response.length
|
||||||
|
return payload
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response && typeof response === 'object') {
|
||||||
|
if (Array.isArray(response.data)) {
|
||||||
|
payload.data = response.data
|
||||||
|
} else if (Array.isArray((response as any).records)) {
|
||||||
|
payload.data = (response as any).records
|
||||||
|
} else if (Array.isArray((response as any).results)) {
|
||||||
|
payload.data = (response as any).results
|
||||||
|
}
|
||||||
|
|
||||||
|
payload.totalCount =
|
||||||
|
response.totalCount ??
|
||||||
|
response.total ??
|
||||||
|
response.count ??
|
||||||
|
payload.data.length ??
|
||||||
|
0
|
||||||
|
payload.page = response.page
|
||||||
|
payload.pageSize = response.pageSize
|
||||||
|
}
|
||||||
|
|
||||||
|
return payload
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchRecords = async (params?: Record<string, any>, options?: { append?: boolean }) => {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
error.value = null
|
error.value = null
|
||||||
try {
|
try {
|
||||||
const response = await api.get(apiEndpoint, { params })
|
const response = await api.get(apiEndpoint, { params })
|
||||||
// Handle response - data might be directly in response or in response.data
|
const normalized = normalizeListResponse(response)
|
||||||
records.value = response.data || response || []
|
totalCount.value = normalized.totalCount ?? normalized.data.length ?? 0
|
||||||
|
records.value = options?.append
|
||||||
|
? [...records.value, ...normalized.data]
|
||||||
|
: normalized.data
|
||||||
|
return normalized
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
error.value = e.message
|
error.value = e.message
|
||||||
console.error('Failed to fetch records:', e)
|
console.error('Failed to fetch records:', e)
|
||||||
@@ -230,6 +270,7 @@ export const useViewState = <T extends { id?: string }>(
|
|||||||
// Handle response - it might be the data directly or wrapped in { data: ... }
|
// Handle response - it might be the data directly or wrapped in { data: ... }
|
||||||
const recordData = response.data || response
|
const recordData = response.data || response
|
||||||
records.value.push(recordData)
|
records.value.push(recordData)
|
||||||
|
totalCount.value += 1
|
||||||
currentRecord.value = recordData
|
currentRecord.value = recordData
|
||||||
return recordData
|
return recordData
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
@@ -272,6 +313,7 @@ export const useViewState = <T extends { id?: string }>(
|
|||||||
try {
|
try {
|
||||||
await api.delete(`${apiEndpoint}/${id}`)
|
await api.delete(`${apiEndpoint}/${id}`)
|
||||||
records.value = records.value.filter(r => r.id !== id)
|
records.value = records.value.filter(r => r.id !== id)
|
||||||
|
totalCount.value = Math.max(0, totalCount.value - 1)
|
||||||
if (currentRecord.value?.id === id) {
|
if (currentRecord.value?.id === id) {
|
||||||
currentRecord.value = null
|
currentRecord.value = null
|
||||||
}
|
}
|
||||||
@@ -290,6 +332,7 @@ export const useViewState = <T extends { id?: string }>(
|
|||||||
try {
|
try {
|
||||||
await Promise.all(ids.map(id => api.delete(`${apiEndpoint}/${id}`)))
|
await Promise.all(ids.map(id => api.delete(`${apiEndpoint}/${id}`)))
|
||||||
records.value = records.value.filter(r => !ids.includes(r.id!))
|
records.value = records.value.filter(r => !ids.includes(r.id!))
|
||||||
|
totalCount.value = Math.max(0, totalCount.value - ids.length)
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
error.value = e.message
|
error.value = e.message
|
||||||
console.error('Failed to delete records:', e)
|
console.error('Failed to delete records:', e)
|
||||||
@@ -327,6 +370,7 @@ export const useViewState = <T extends { id?: string }>(
|
|||||||
return {
|
return {
|
||||||
// State
|
// State
|
||||||
records,
|
records,
|
||||||
|
totalCount,
|
||||||
currentRecord,
|
currentRecord,
|
||||||
currentView,
|
currentView,
|
||||||
loading,
|
loading,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, onMounted, onBeforeUnmount, watch, nextTick } from 'vue'
|
import { ref, computed, onMounted, onBeforeUnmount, watch } from 'vue'
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import { useApi } from '@/composables/useApi'
|
import { useApi } from '@/composables/useApi'
|
||||||
import { useFields, useViewState } from '@/composables/useFieldViews'
|
import { useFields, useViewState } from '@/composables/useFieldViews'
|
||||||
@@ -38,6 +38,7 @@ const error = ref<string | null>(null)
|
|||||||
// Use view state composable
|
// Use view state composable
|
||||||
const {
|
const {
|
||||||
records,
|
records,
|
||||||
|
totalCount,
|
||||||
currentRecord,
|
currentRecord,
|
||||||
loading: dataLoading,
|
loading: dataLoading,
|
||||||
saving,
|
saving,
|
||||||
@@ -57,7 +58,7 @@ const handleAiRecordCreated = (event: Event) => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (view.value === 'list') {
|
if (view.value === 'list') {
|
||||||
fetchRecords()
|
initializeListRecords()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -142,6 +143,9 @@ const editConfig = computed(() => {
|
|||||||
return buildEditViewConfig(objectDefinition.value)
|
return buildEditViewConfig(objectDefinition.value)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const listPageSize = computed(() => listConfig.value?.pageSize ?? 25)
|
||||||
|
const maxFrontendRecords = computed(() => listConfig.value?.maxFrontendRecords ?? 500)
|
||||||
|
|
||||||
// Fetch object definition
|
// Fetch object definition
|
||||||
const fetchObjectDefinition = async () => {
|
const fetchObjectDefinition = async () => {
|
||||||
try {
|
try {
|
||||||
@@ -220,6 +224,39 @@ const handleCancel = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const loadListRecords = async (
|
||||||
|
page = 1,
|
||||||
|
options?: { append?: boolean; pageSize?: number }
|
||||||
|
) => {
|
||||||
|
const pageSize = options?.pageSize ?? listPageSize.value
|
||||||
|
const result = await fetchRecords({ page, pageSize }, { append: options?.append })
|
||||||
|
const resolvedTotal = result?.totalCount ?? totalCount.value ?? records.value.length
|
||||||
|
totalCount.value = resolvedTotal
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
const initializeListRecords = async () => {
|
||||||
|
const firstResult = await loadListRecords(1)
|
||||||
|
const resolvedTotal = firstResult?.totalCount ?? totalCount.value ?? records.value.length
|
||||||
|
const shouldPrefetchAll =
|
||||||
|
resolvedTotal <= maxFrontendRecords.value && records.value.length < resolvedTotal
|
||||||
|
|
||||||
|
if (shouldPrefetchAll) {
|
||||||
|
await loadListRecords(1, { append: false, pageSize: maxFrontendRecords.value })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handlePageChange = async (page: number, pageSize: number) => {
|
||||||
|
const loadedPages = Math.ceil(records.value.length / pageSize)
|
||||||
|
if (page > loadedPages && totalCount.value > records.value.length) {
|
||||||
|
await loadListRecords(page, { append: true, pageSize })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleLoadMore = async (page: number, pageSize: number) => {
|
||||||
|
await loadListRecords(page, { append: true, pageSize })
|
||||||
|
}
|
||||||
|
|
||||||
// Watch for route changes
|
// Watch for route changes
|
||||||
watch(() => route.params, async (newParams, oldParams) => {
|
watch(() => route.params, async (newParams, oldParams) => {
|
||||||
// Reset current record when navigating to 'new'
|
// Reset current record when navigating to 'new'
|
||||||
@@ -234,7 +271,7 @@ watch(() => route.params, async (newParams, oldParams) => {
|
|||||||
|
|
||||||
// Fetch records if navigating back to list
|
// Fetch records if navigating back to list
|
||||||
if (!newParams.recordId && !newParams.view) {
|
if (!newParams.recordId && !newParams.view) {
|
||||||
await fetchRecords()
|
await initializeListRecords()
|
||||||
}
|
}
|
||||||
}, { deep: true })
|
}, { deep: true })
|
||||||
|
|
||||||
@@ -243,7 +280,7 @@ onMounted(async () => {
|
|||||||
await fetchObjectDefinition()
|
await fetchObjectDefinition()
|
||||||
|
|
||||||
if (view.value === 'list') {
|
if (view.value === 'list') {
|
||||||
await fetchRecords()
|
await initializeListRecords()
|
||||||
} else if (recordId.value && recordId.value !== 'new') {
|
} else if (recordId.value && recordId.value !== 'new') {
|
||||||
await fetchRecord(recordId.value)
|
await fetchRecord(recordId.value)
|
||||||
}
|
}
|
||||||
@@ -289,12 +326,15 @@ onMounted(async () => {
|
|||||||
:config="listConfig"
|
:config="listConfig"
|
||||||
:data="records"
|
:data="records"
|
||||||
:loading="dataLoading"
|
:loading="dataLoading"
|
||||||
|
:total-count="totalCount"
|
||||||
:base-url="`/runtime/objects`"
|
:base-url="`/runtime/objects`"
|
||||||
selectable
|
selectable
|
||||||
@row-click="handleRowClick"
|
@row-click="handleRowClick"
|
||||||
@create="handleCreate"
|
@create="handleCreate"
|
||||||
@edit="handleEdit"
|
@edit="handleEdit"
|
||||||
@delete="handleDelete"
|
@delete="handleDelete"
|
||||||
|
@page-change="handlePageChange"
|
||||||
|
@load-more="handleLoadMore"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- Detail View -->
|
<!-- Detail View -->
|
||||||
|
|||||||
@@ -71,7 +71,12 @@ const fetchPage = async () => {
|
|||||||
|
|
||||||
if (page.value.objectApiName) {
|
if (page.value.objectApiName) {
|
||||||
loadingRecords.value = true
|
loadingRecords.value = true
|
||||||
records.value = await api.get(`/runtime/objects/${page.value.objectApiName}/records`)
|
const response = await api.get(
|
||||||
|
`/runtime/objects/${page.value.objectApiName}/records`
|
||||||
|
)
|
||||||
|
records.value = Array.isArray(response)
|
||||||
|
? response
|
||||||
|
: response?.data || response?.records || []
|
||||||
}
|
}
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
error.value = e.message
|
error.value = e.message
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, onMounted, onBeforeUnmount, watch, nextTick } from 'vue'
|
import { ref, computed, onMounted, onBeforeUnmount, watch } from 'vue'
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import { useApi } from '@/composables/useApi'
|
import { useApi } from '@/composables/useApi'
|
||||||
import { useFields, useViewState } from '@/composables/useFieldViews'
|
import { useFields, useViewState } from '@/composables/useFieldViews'
|
||||||
@@ -31,6 +31,7 @@ const error = ref<string | null>(null)
|
|||||||
// Use view state composable
|
// Use view state composable
|
||||||
const {
|
const {
|
||||||
records,
|
records,
|
||||||
|
totalCount,
|
||||||
currentRecord,
|
currentRecord,
|
||||||
loading: dataLoading,
|
loading: dataLoading,
|
||||||
saving,
|
saving,
|
||||||
@@ -50,7 +51,7 @@ const handleAiRecordCreated = (event: Event) => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (view.value === 'list') {
|
if (view.value === 'list') {
|
||||||
fetchRecords()
|
initializeListRecords()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,6 +83,9 @@ const editConfig = computed(() => {
|
|||||||
return buildEditViewConfig(objectDefinition.value)
|
return buildEditViewConfig(objectDefinition.value)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const listPageSize = computed(() => listConfig.value?.pageSize ?? 25)
|
||||||
|
const maxFrontendRecords = computed(() => listConfig.value?.maxFrontendRecords ?? 500)
|
||||||
|
|
||||||
// Fetch object definition
|
// Fetch object definition
|
||||||
const fetchObjectDefinition = async () => {
|
const fetchObjectDefinition = async () => {
|
||||||
try {
|
try {
|
||||||
@@ -160,6 +164,39 @@ const handleCancel = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const loadListRecords = async (
|
||||||
|
page = 1,
|
||||||
|
options?: { append?: boolean; pageSize?: number }
|
||||||
|
) => {
|
||||||
|
const pageSize = options?.pageSize ?? listPageSize.value
|
||||||
|
const result = await fetchRecords({ page, pageSize }, { append: options?.append })
|
||||||
|
const resolvedTotal = result?.totalCount ?? totalCount.value ?? records.value.length
|
||||||
|
totalCount.value = resolvedTotal
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
const initializeListRecords = async () => {
|
||||||
|
const firstResult = await loadListRecords(1)
|
||||||
|
const resolvedTotal = firstResult?.totalCount ?? totalCount.value ?? records.value.length
|
||||||
|
const shouldPrefetchAll =
|
||||||
|
resolvedTotal <= maxFrontendRecords.value && records.value.length < resolvedTotal
|
||||||
|
|
||||||
|
if (shouldPrefetchAll) {
|
||||||
|
await loadListRecords(1, { append: false, pageSize: maxFrontendRecords.value })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handlePageChange = async (page: number, pageSize: number) => {
|
||||||
|
const loadedPages = Math.ceil(records.value.length / pageSize)
|
||||||
|
if (page > loadedPages && totalCount.value > records.value.length) {
|
||||||
|
await loadListRecords(page, { append: true, pageSize })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleLoadMore = async (page: number, pageSize: number) => {
|
||||||
|
await loadListRecords(page, { append: true, pageSize })
|
||||||
|
}
|
||||||
|
|
||||||
// Watch for route changes
|
// Watch for route changes
|
||||||
watch(() => route.params, async (newParams, oldParams) => {
|
watch(() => route.params, async (newParams, oldParams) => {
|
||||||
// Reset current record when navigating to 'new'
|
// Reset current record when navigating to 'new'
|
||||||
@@ -174,7 +211,7 @@ watch(() => route.params, async (newParams, oldParams) => {
|
|||||||
|
|
||||||
// Fetch records if navigating back to list
|
// Fetch records if navigating back to list
|
||||||
if (!newParams.recordId && !newParams.view) {
|
if (!newParams.recordId && !newParams.view) {
|
||||||
await fetchRecords()
|
await initializeListRecords()
|
||||||
}
|
}
|
||||||
}, { deep: true })
|
}, { deep: true })
|
||||||
|
|
||||||
@@ -183,7 +220,7 @@ onMounted(async () => {
|
|||||||
await fetchObjectDefinition()
|
await fetchObjectDefinition()
|
||||||
|
|
||||||
if (view.value === 'list') {
|
if (view.value === 'list') {
|
||||||
await fetchRecords()
|
await initializeListRecords()
|
||||||
} else if (recordId.value && recordId.value !== 'new') {
|
} else if (recordId.value && recordId.value !== 'new') {
|
||||||
await fetchRecord(recordId.value)
|
await fetchRecord(recordId.value)
|
||||||
}
|
}
|
||||||
@@ -225,11 +262,14 @@ onMounted(async () => {
|
|||||||
:config="listConfig"
|
:config="listConfig"
|
||||||
:data="records"
|
:data="records"
|
||||||
:loading="dataLoading"
|
:loading="dataLoading"
|
||||||
|
:total-count="totalCount"
|
||||||
selectable
|
selectable
|
||||||
@row-click="handleRowClick"
|
@row-click="handleRowClick"
|
||||||
@create="handleCreate"
|
@create="handleCreate"
|
||||||
@edit="handleEdit"
|
@edit="handleEdit"
|
||||||
@delete="handleDelete"
|
@delete="handleDelete"
|
||||||
|
@page-change="handlePageChange"
|
||||||
|
@load-more="handleLoadMore"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- Detail View -->
|
<!-- Detail View -->
|
||||||
|
|||||||
@@ -114,6 +114,7 @@ export interface ViewConfig {
|
|||||||
export interface ListViewConfig extends ViewConfig {
|
export interface ListViewConfig extends ViewConfig {
|
||||||
mode: ViewMode.LIST;
|
mode: ViewMode.LIST;
|
||||||
pageSize?: number;
|
pageSize?: number;
|
||||||
|
maxFrontendRecords?: number;
|
||||||
searchable?: boolean;
|
searchable?: boolean;
|
||||||
filterable?: boolean;
|
filterable?: boolean;
|
||||||
exportable?: boolean;
|
exportable?: boolean;
|
||||||
|
|||||||
Reference in New Issue
Block a user