Compare commits
1 Commits
codex/add-
...
47fa72451d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
47fa72451d |
@@ -576,9 +576,46 @@ export class ObjectService {
|
||||
}
|
||||
}
|
||||
|
||||
// Apply additional filters
|
||||
if (filters) {
|
||||
query = query.where(filters);
|
||||
// Extract pagination and sorting parameters from query string
|
||||
const {
|
||||
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('*');
|
||||
@@ -590,7 +627,12 @@ export class ObjectService {
|
||||
)
|
||||
);
|
||||
|
||||
return filteredRecords;
|
||||
return {
|
||||
data: filteredRecords,
|
||||
totalCount,
|
||||
page: shouldPaginate ? safePage : 1,
|
||||
pageSize: shouldPaginate ? safePageSize : filteredRecords.length,
|
||||
};
|
||||
}
|
||||
|
||||
async getRecord(
|
||||
@@ -952,6 +994,73 @@ export class ObjectService {
|
||||
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(
|
||||
tenantId: string,
|
||||
objectApiName: string,
|
||||
|
||||
@@ -95,4 +95,20 @@ export class RuntimeObjectController {
|
||||
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 {
|
||||
const endpoint = `${props.baseUrl}/${relationObject.value}/records`
|
||||
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 (props.modelValue) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
@@ -22,6 +22,7 @@ interface Props {
|
||||
loading?: boolean
|
||||
selectable?: boolean
|
||||
baseUrl?: string
|
||||
totalCount?: number
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
@@ -41,6 +42,8 @@ const emit = defineEmits<{
|
||||
'sort': [field: string, direction: 'asc' | 'desc']
|
||||
'search': [query: string]
|
||||
'refresh': []
|
||||
'page-change': [page: number, pageSize: number]
|
||||
'load-more': [page: number, pageSize: number]
|
||||
}>()
|
||||
|
||||
// State
|
||||
@@ -48,12 +51,46 @@ const selectedRows = ref<Set<string>>(new Set())
|
||||
const searchQuery = ref('')
|
||||
const sortField = ref<string>('')
|
||||
const sortDirection = ref<'asc' | 'desc'>('asc')
|
||||
const currentPage = ref(1)
|
||||
|
||||
// Computed
|
||||
const visibleFields = computed(() =>
|
||||
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({
|
||||
get: () => props.data.length > 0 && selectedRows.value.size === props.data.length,
|
||||
set: (val: boolean) => {
|
||||
@@ -96,6 +133,28 @@ const handleSearch = () => {
|
||||
const handleAction = (actionId: string) => {
|
||||
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>
|
||||
|
||||
<template>
|
||||
@@ -192,7 +251,7 @@ const handleAction = (actionId: string) => {
|
||||
</TableRow>
|
||||
<TableRow
|
||||
v-else
|
||||
v-for="row in data"
|
||||
v-for="row in paginatedData"
|
||||
:key="row.id"
|
||||
class="cursor-pointer hover:bg-muted/50"
|
||||
@click="emit('row-click', row)"
|
||||
@@ -227,7 +286,26 @@ const handleAction = (actionId: string) => {
|
||||
</Table>
|
||||
</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>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -78,7 +78,8 @@ export const useFields = () => {
|
||||
objectApiName: objectDef.apiName,
|
||||
mode: 'list' as ViewMode,
|
||||
fields,
|
||||
pageSize: 25,
|
||||
pageSize: 10,
|
||||
maxFrontendRecords: 500,
|
||||
searchable: true,
|
||||
filterable: true,
|
||||
exportable: true,
|
||||
@@ -183,6 +184,7 @@ export const useViewState = <T extends { id?: string }>(
|
||||
apiEndpoint: string
|
||||
) => {
|
||||
const records = ref<T[]>([])
|
||||
const totalCount = ref(0)
|
||||
const currentRecord = ref<T | null>(null)
|
||||
const currentView = ref<'list' | 'detail' | 'edit'>('list')
|
||||
const loading = ref(false)
|
||||
@@ -191,13 +193,51 @@ export const useViewState = <T extends { id?: string }>(
|
||||
|
||||
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
|
||||
error.value = null
|
||||
try {
|
||||
const response = await api.get(apiEndpoint, { params })
|
||||
// Handle response - data might be directly in response or in response.data
|
||||
records.value = response.data || response || []
|
||||
const normalized = normalizeListResponse(response)
|
||||
totalCount.value = normalized.totalCount ?? normalized.data.length ?? 0
|
||||
records.value = options?.append
|
||||
? [...records.value, ...normalized.data]
|
||||
: normalized.data
|
||||
return normalized
|
||||
} catch (e: any) {
|
||||
error.value = e.message
|
||||
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: ... }
|
||||
const recordData = response.data || response
|
||||
records.value.push(recordData)
|
||||
totalCount.value += 1
|
||||
currentRecord.value = recordData
|
||||
return recordData
|
||||
} catch (e: any) {
|
||||
@@ -272,6 +313,7 @@ export const useViewState = <T extends { id?: string }>(
|
||||
try {
|
||||
await api.delete(`${apiEndpoint}/${id}`)
|
||||
records.value = records.value.filter(r => r.id !== id)
|
||||
totalCount.value = Math.max(0, totalCount.value - 1)
|
||||
if (currentRecord.value?.id === id) {
|
||||
currentRecord.value = null
|
||||
}
|
||||
@@ -290,6 +332,7 @@ export const useViewState = <T extends { id?: string }>(
|
||||
try {
|
||||
await Promise.all(ids.map(id => api.delete(`${apiEndpoint}/${id}`)))
|
||||
records.value = records.value.filter(r => !ids.includes(r.id!))
|
||||
totalCount.value = Math.max(0, totalCount.value - ids.length)
|
||||
} catch (e: any) {
|
||||
error.value = e.message
|
||||
console.error('Failed to delete records:', e)
|
||||
@@ -327,6 +370,7 @@ export const useViewState = <T extends { id?: string }>(
|
||||
return {
|
||||
// State
|
||||
records,
|
||||
totalCount,
|
||||
currentRecord,
|
||||
currentView,
|
||||
loading,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<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 { useApi } from '@/composables/useApi'
|
||||
import { useFields, useViewState } from '@/composables/useFieldViews'
|
||||
@@ -38,6 +38,7 @@ const error = ref<string | null>(null)
|
||||
// Use view state composable
|
||||
const {
|
||||
records,
|
||||
totalCount,
|
||||
currentRecord,
|
||||
loading: dataLoading,
|
||||
saving,
|
||||
@@ -57,7 +58,7 @@ const handleAiRecordCreated = (event: Event) => {
|
||||
return
|
||||
}
|
||||
if (view.value === 'list') {
|
||||
fetchRecords()
|
||||
initializeListRecords()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -142,6 +143,9 @@ const editConfig = computed(() => {
|
||||
return buildEditViewConfig(objectDefinition.value)
|
||||
})
|
||||
|
||||
const listPageSize = computed(() => listConfig.value?.pageSize ?? 25)
|
||||
const maxFrontendRecords = computed(() => listConfig.value?.maxFrontendRecords ?? 500)
|
||||
|
||||
// Fetch object definition
|
||||
const fetchObjectDefinition = async () => {
|
||||
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(() => route.params, async (newParams, oldParams) => {
|
||||
// Reset current record when navigating to 'new'
|
||||
@@ -234,7 +271,7 @@ watch(() => route.params, async (newParams, oldParams) => {
|
||||
|
||||
// Fetch records if navigating back to list
|
||||
if (!newParams.recordId && !newParams.view) {
|
||||
await fetchRecords()
|
||||
await initializeListRecords()
|
||||
}
|
||||
}, { deep: true })
|
||||
|
||||
@@ -243,7 +280,7 @@ onMounted(async () => {
|
||||
await fetchObjectDefinition()
|
||||
|
||||
if (view.value === 'list') {
|
||||
await fetchRecords()
|
||||
await initializeListRecords()
|
||||
} else if (recordId.value && recordId.value !== 'new') {
|
||||
await fetchRecord(recordId.value)
|
||||
}
|
||||
@@ -289,12 +326,15 @@ onMounted(async () => {
|
||||
:config="listConfig"
|
||||
:data="records"
|
||||
:loading="dataLoading"
|
||||
:total-count="totalCount"
|
||||
:base-url="`/runtime/objects`"
|
||||
selectable
|
||||
@row-click="handleRowClick"
|
||||
@create="handleCreate"
|
||||
@edit="handleEdit"
|
||||
@delete="handleDelete"
|
||||
@page-change="handlePageChange"
|
||||
@load-more="handleLoadMore"
|
||||
/>
|
||||
|
||||
<!-- Detail View -->
|
||||
|
||||
@@ -71,7 +71,12 @@ const fetchPage = async () => {
|
||||
|
||||
if (page.value.objectApiName) {
|
||||
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) {
|
||||
error.value = e.message
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<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 { useApi } from '@/composables/useApi'
|
||||
import { useFields, useViewState } from '@/composables/useFieldViews'
|
||||
@@ -31,6 +31,7 @@ const error = ref<string | null>(null)
|
||||
// Use view state composable
|
||||
const {
|
||||
records,
|
||||
totalCount,
|
||||
currentRecord,
|
||||
loading: dataLoading,
|
||||
saving,
|
||||
@@ -50,7 +51,7 @@ const handleAiRecordCreated = (event: Event) => {
|
||||
return
|
||||
}
|
||||
if (view.value === 'list') {
|
||||
fetchRecords()
|
||||
initializeListRecords()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,6 +83,9 @@ const editConfig = computed(() => {
|
||||
return buildEditViewConfig(objectDefinition.value)
|
||||
})
|
||||
|
||||
const listPageSize = computed(() => listConfig.value?.pageSize ?? 25)
|
||||
const maxFrontendRecords = computed(() => listConfig.value?.maxFrontendRecords ?? 500)
|
||||
|
||||
// Fetch object definition
|
||||
const fetchObjectDefinition = async () => {
|
||||
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(() => route.params, async (newParams, oldParams) => {
|
||||
// Reset current record when navigating to 'new'
|
||||
@@ -174,7 +211,7 @@ watch(() => route.params, async (newParams, oldParams) => {
|
||||
|
||||
// Fetch records if navigating back to list
|
||||
if (!newParams.recordId && !newParams.view) {
|
||||
await fetchRecords()
|
||||
await initializeListRecords()
|
||||
}
|
||||
}, { deep: true })
|
||||
|
||||
@@ -183,7 +220,7 @@ onMounted(async () => {
|
||||
await fetchObjectDefinition()
|
||||
|
||||
if (view.value === 'list') {
|
||||
await fetchRecords()
|
||||
await initializeListRecords()
|
||||
} else if (recordId.value && recordId.value !== 'new') {
|
||||
await fetchRecord(recordId.value)
|
||||
}
|
||||
@@ -225,11 +262,14 @@ onMounted(async () => {
|
||||
:config="listConfig"
|
||||
:data="records"
|
||||
:loading="dataLoading"
|
||||
:total-count="totalCount"
|
||||
selectable
|
||||
@row-click="handleRowClick"
|
||||
@create="handleCreate"
|
||||
@edit="handleEdit"
|
||||
@delete="handleDelete"
|
||||
@page-change="handlePageChange"
|
||||
@load-more="handleLoadMore"
|
||||
/>
|
||||
|
||||
<!-- Detail View -->
|
||||
|
||||
@@ -114,6 +114,7 @@ export interface ViewConfig {
|
||||
export interface ListViewConfig extends ViewConfig {
|
||||
mode: ViewMode.LIST;
|
||||
pageSize?: number;
|
||||
maxFrontendRecords?: number;
|
||||
searchable?: boolean;
|
||||
filterable?: boolean;
|
||||
exportable?: boolean;
|
||||
|
||||
Reference in New Issue
Block a user