WIP - fixes to spreadsheet view
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, watch } from 'vue'
|
import { ref, computed, watch } from 'vue'
|
||||||
import type { CellValueChangedEvent, ColDef } from 'ag-grid-community'
|
import type { CellValueChangedEvent, ColDef, GridApi, GridReadyEvent } from 'ag-grid-community'
|
||||||
import { AgGridVue } from 'ag-grid-vue3'
|
import { AgGridVue } from 'ag-grid-vue3'
|
||||||
import 'ag-grid-community/styles/ag-grid.css'
|
import 'ag-grid-community/styles/ag-grid.css'
|
||||||
import 'ag-grid-community/styles/ag-theme-alpine.css'
|
import 'ag-grid-community/styles/ag-theme-alpine.css'
|
||||||
@@ -29,6 +29,9 @@ interface Props {
|
|||||||
baseUrl?: string
|
baseUrl?: string
|
||||||
totalCount?: number
|
totalCount?: number
|
||||||
searchSummary?: string
|
searchSummary?: string
|
||||||
|
draftEdits?: Record<string, Record<string, any>>
|
||||||
|
cellErrors?: Record<string, Record<string, string | boolean>>
|
||||||
|
savingDrafts?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
@@ -37,6 +40,9 @@ const props = withDefaults(defineProps<Props>(), {
|
|||||||
selectable: false,
|
selectable: false,
|
||||||
baseUrl: '/runtime/objects',
|
baseUrl: '/runtime/objects',
|
||||||
searchSummary: '',
|
searchSummary: '',
|
||||||
|
draftEdits: () => ({}),
|
||||||
|
cellErrors: () => ({}),
|
||||||
|
savingDrafts: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
@@ -53,6 +59,8 @@ const emit = defineEmits<{
|
|||||||
'load-more': [page: number, pageSize: number]
|
'load-more': [page: number, pageSize: number]
|
||||||
'view-change': [mode: 'list' | 'spreadsheet']
|
'view-change': [mode: 'list' | 'spreadsheet']
|
||||||
'cell-edit': [payload: { row: any; field: FieldConfig; newValue: any; oldValue: any }]
|
'cell-edit': [payload: { row: any; field: FieldConfig; newValue: any; oldValue: any }]
|
||||||
|
'save-drafts': []
|
||||||
|
'discard-drafts': []
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
// State
|
// State
|
||||||
@@ -64,6 +72,7 @@ const sortDirection = ref<'asc' | 'desc'>('asc')
|
|||||||
const currentPage = ref(1)
|
const currentPage = ref(1)
|
||||||
const bulkAction = ref('delete')
|
const bulkAction = ref('delete')
|
||||||
const viewMode = ref<'list' | 'spreadsheet'>('list')
|
const viewMode = ref<'list' | 'spreadsheet'>('list')
|
||||||
|
const gridApi = ref<GridApi | null>(null)
|
||||||
|
|
||||||
// Computed
|
// Computed
|
||||||
const visibleFields = computed(() =>
|
const visibleFields = computed(() =>
|
||||||
@@ -102,6 +111,8 @@ const showLoadMore = computed(() => (
|
|||||||
Boolean(props.totalCount) &&
|
Boolean(props.totalCount) &&
|
||||||
props.data.length < totalRecords.value
|
props.data.length < totalRecords.value
|
||||||
))
|
))
|
||||||
|
const draftRowCount = computed(() => Object.keys(props.draftEdits).length)
|
||||||
|
const draftCellCount = computed(() => Object.values(props.draftEdits).reduce((sum, row) => sum + Object.keys(row).length, 0))
|
||||||
|
|
||||||
const allSelected = computed({
|
const allSelected = computed({
|
||||||
get: () => props.data.length > 0 && selectedRowIds.value.length === props.data.length,
|
get: () => props.data.length > 0 && selectedRowIds.value.length === props.data.length,
|
||||||
@@ -224,6 +235,46 @@ const formatFieldValue = (field: FieldConfig, value: any, record?: any) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const hasOwnProperty = (target: Record<string, any> | undefined, key: string) =>
|
||||||
|
!!target && Object.prototype.hasOwnProperty.call(target, key)
|
||||||
|
|
||||||
|
const isDraftCell = (params: any) => {
|
||||||
|
const rowId = normalizeId(params?.data?.id)
|
||||||
|
const field = String(params?.colDef?.field || '')
|
||||||
|
return hasOwnProperty(props.draftEdits[rowId], field)
|
||||||
|
}
|
||||||
|
|
||||||
|
const isErrorCell = (params: any) => {
|
||||||
|
const rowId = normalizeId(params?.data?.id)
|
||||||
|
const field = String(params?.colDef?.field || '')
|
||||||
|
return hasOwnProperty(props.cellErrors[rowId], field)
|
||||||
|
}
|
||||||
|
|
||||||
|
const getCellClasses = (params: any) => {
|
||||||
|
const classes: string[] = []
|
||||||
|
if (isDraftCell(params)) classes.push('cell-draft')
|
||||||
|
if (isErrorCell(params)) classes.push('cell-error')
|
||||||
|
return classes
|
||||||
|
}
|
||||||
|
|
||||||
|
const getCellStyle = (params: any) => {
|
||||||
|
const isDraft = isDraftCell(params)
|
||||||
|
const isError = isErrorCell(params)
|
||||||
|
if (!isDraft && !isError) return null
|
||||||
|
const style: Record<string, string> = {}
|
||||||
|
if (isDraft) {
|
||||||
|
style.backgroundColor = 'hsl(var(--accent) / 0.45)'
|
||||||
|
style.outline = '2px solid hsl(var(--accent))'
|
||||||
|
style.outlineOffset = '-2px'
|
||||||
|
}
|
||||||
|
if (isError) {
|
||||||
|
style.backgroundColor = 'hsl(var(--destructive) / 0.28)'
|
||||||
|
style.outline = '2px solid hsl(var(--destructive))'
|
||||||
|
style.outlineOffset = '-2px'
|
||||||
|
}
|
||||||
|
return style
|
||||||
|
}
|
||||||
|
|
||||||
const columnDefs = computed<ColDef[]>(() =>
|
const columnDefs = computed<ColDef[]>(() =>
|
||||||
visibleFields.value.map(field => ({
|
visibleFields.value.map(field => ({
|
||||||
field: field.apiName,
|
field: field.apiName,
|
||||||
@@ -231,6 +282,8 @@ const columnDefs = computed<ColDef[]>(() =>
|
|||||||
sortable: field.sortable !== false,
|
sortable: field.sortable !== false,
|
||||||
editable: isEditableField(field),
|
editable: isEditableField(field),
|
||||||
valueFormatter: params => formatFieldValue(field, params.value, params.data),
|
valueFormatter: params => formatFieldValue(field, params.value, params.data),
|
||||||
|
cellClass: params => getCellClasses(params),
|
||||||
|
cellStyle: params => getCellStyle(params),
|
||||||
cellEditor:
|
cellEditor:
|
||||||
field.type === FieldType.SELECT
|
field.type === FieldType.SELECT
|
||||||
? 'agSelectCellEditor'
|
? 'agSelectCellEditor'
|
||||||
@@ -268,6 +321,10 @@ const handleCellValueChanged = (event: CellValueChangedEvent) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleGridReady = (event: GridReadyEvent) => {
|
||||||
|
gridApi.value = event.api
|
||||||
|
}
|
||||||
|
|
||||||
const goToPage = (page: number) => {
|
const goToPage = (page: number) => {
|
||||||
const nextPage = Math.min(Math.max(page, 1), availablePages.value)
|
const nextPage = Math.min(Math.max(page, 1), availablePages.value)
|
||||||
if (nextPage !== currentPage.value) {
|
if (nextPage !== currentPage.value) {
|
||||||
@@ -309,6 +366,16 @@ watch(
|
|||||||
emit('view-change', mode)
|
emit('view-change', mode)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => [props.draftEdits, props.cellErrors],
|
||||||
|
() => {
|
||||||
|
if (gridApi.value) {
|
||||||
|
gridApi.value.refreshCells({ force: true })
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ deep: true }
|
||||||
|
)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -341,6 +408,26 @@ watch(
|
|||||||
<SelectItem value="spreadsheet">Spreadsheet view</SelectItem>
|
<SelectItem value="spreadsheet">Spreadsheet view</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
|
<template v-if="viewMode === 'spreadsheet'">
|
||||||
|
<Badge v-if="draftRowCount > 0" variant="secondary" class="px-3 py-1">
|
||||||
|
{{ draftCellCount }} change{{ draftCellCount === 1 ? '' : 's' }}
|
||||||
|
</Badge>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
:disabled="draftRowCount === 0 || savingDrafts"
|
||||||
|
@click="emit('discard-drafts')"
|
||||||
|
>
|
||||||
|
Discard changes
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
:disabled="draftRowCount === 0 || savingDrafts"
|
||||||
|
@click="emit('save-drafts')"
|
||||||
|
>
|
||||||
|
Save changes ({{ draftRowCount }})
|
||||||
|
</Button>
|
||||||
|
</template>
|
||||||
<!-- Bulk Actions -->
|
<!-- Bulk Actions -->
|
||||||
<template v-if="selectedRowIds.length > 0">
|
<template v-if="selectedRowIds.length > 0">
|
||||||
<Badge variant="secondary" class="px-3 py-1">
|
<Badge variant="secondary" class="px-3 py-1">
|
||||||
@@ -473,6 +560,7 @@ watch(
|
|||||||
suppress-row-click-selection
|
suppress-row-click-selection
|
||||||
single-click-edit
|
single-click-edit
|
||||||
@cell-value-changed="handleCellValueChanged"
|
@cell-value-changed="handleCellValueChanged"
|
||||||
|
@grid-ready="handleGridReady"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -512,4 +600,5 @@ watch(
|
|||||||
.list-view :deep(input) {
|
.list-view :deep(input) {
|
||||||
background-color: hsl(var(--background));
|
background-color: hsl(var(--background));
|
||||||
}
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ const {
|
|||||||
fetchRecord,
|
fetchRecord,
|
||||||
deleteRecord,
|
deleteRecord,
|
||||||
deleteRecords,
|
deleteRecords,
|
||||||
|
updateRecord,
|
||||||
handleSave,
|
handleSave,
|
||||||
} = useViewState(`/runtime/objects/${objectApiName.value}/records`)
|
} = useViewState(`/runtime/objects/${objectApiName.value}/records`)
|
||||||
|
|
||||||
@@ -165,6 +166,12 @@ const deleteDialogOpen = ref(false)
|
|||||||
const deleteSubmitting = ref(false)
|
const deleteSubmitting = ref(false)
|
||||||
const pendingDeleteRows = ref<any[]>([])
|
const pendingDeleteRows = ref<any[]>([])
|
||||||
const deleteSummary = ref<{ deletedIds: string[]; deniedIds: string[] } | null>(null)
|
const deleteSummary = ref<{ deletedIds: string[]; deniedIds: string[] } | null>(null)
|
||||||
|
const normalizeRecordId = (id: any) => String(id)
|
||||||
|
|
||||||
|
const draftEdits = ref<Record<string, Record<string, any>>>({})
|
||||||
|
const draftOriginals = ref<Record<string, Record<string, any>>>({})
|
||||||
|
const cellErrors = ref<Record<string, Record<string, string>>>({})
|
||||||
|
const savingDrafts = ref(false)
|
||||||
|
|
||||||
const isSearchActive = computed(() => searchQuery.value.trim().length > 0)
|
const isSearchActive = computed(() => searchQuery.value.trim().length > 0)
|
||||||
const pendingDeleteCount = computed(() => pendingDeleteRows.value.length)
|
const pendingDeleteCount = computed(() => pendingDeleteRows.value.length)
|
||||||
@@ -355,6 +362,22 @@ const handleLoadMore = async (page: number, pageSize: number) => {
|
|||||||
await loadListRecords(page, { append: true, pageSize })
|
await loadListRecords(page, { append: true, pageSize })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const loadAllListRecords = async () => {
|
||||||
|
if (!records.value.length) {
|
||||||
|
await loadListRecords(1)
|
||||||
|
}
|
||||||
|
const resolvedTotal = totalCount.value ?? records.value.length
|
||||||
|
if (resolvedTotal > records.value.length) {
|
||||||
|
await loadListRecords(1, { append: false, pageSize: resolvedTotal })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleViewChange = async (mode: 'list' | 'spreadsheet') => {
|
||||||
|
if (mode === 'spreadsheet' && !isSearchActive.value) {
|
||||||
|
await loadAllListRecords()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const handleSearch = async (query: string) => {
|
const handleSearch = async (query: string) => {
|
||||||
const trimmed = query.trim()
|
const trimmed = query.trim()
|
||||||
searchQuery.value = trimmed
|
searchQuery.value = trimmed
|
||||||
@@ -366,6 +389,140 @@ const handleSearch = async (query: string) => {
|
|||||||
await searchListRecords(1, { append: false, pageSize: listPageSize.value })
|
await searchListRecords(1, { append: false, pageSize: listPageSize.value })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleCellEdit = async (payload: { row: any; field: any; newValue: any; oldValue: any }) => {
|
||||||
|
if (!payload?.row?.id || payload.newValue === payload.oldValue) return
|
||||||
|
const recordKey = normalizeRecordId(payload.row.id)
|
||||||
|
const fieldName = payload.field.apiName
|
||||||
|
const originalRow = draftOriginals.value[recordKey]
|
||||||
|
const originalValue = originalRow && Object.prototype.hasOwnProperty.call(originalRow, fieldName)
|
||||||
|
? originalRow[fieldName]
|
||||||
|
: payload.oldValue
|
||||||
|
|
||||||
|
if (Object.is(payload.newValue, originalValue)) {
|
||||||
|
const nextDrafts = { ...draftEdits.value }
|
||||||
|
const nextRowDrafts = { ...(nextDrafts[recordKey] || {}) }
|
||||||
|
delete nextRowDrafts[fieldName]
|
||||||
|
if (Object.keys(nextRowDrafts).length === 0) {
|
||||||
|
delete nextDrafts[recordKey]
|
||||||
|
} else {
|
||||||
|
nextDrafts[recordKey] = nextRowDrafts
|
||||||
|
}
|
||||||
|
draftEdits.value = nextDrafts
|
||||||
|
|
||||||
|
const nextOriginals = { ...draftOriginals.value }
|
||||||
|
const nextRowOriginals = { ...(nextOriginals[recordKey] || {}) }
|
||||||
|
delete nextRowOriginals[fieldName]
|
||||||
|
if (Object.keys(nextRowOriginals).length === 0) {
|
||||||
|
delete nextOriginals[recordKey]
|
||||||
|
} else {
|
||||||
|
nextOriginals[recordKey] = nextRowOriginals
|
||||||
|
}
|
||||||
|
draftOriginals.value = nextOriginals
|
||||||
|
} else {
|
||||||
|
draftEdits.value = {
|
||||||
|
...draftEdits.value,
|
||||||
|
[recordKey]: {
|
||||||
|
...(draftEdits.value[recordKey] || {}),
|
||||||
|
[fieldName]: payload.newValue,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if (!originalRow || !Object.prototype.hasOwnProperty.call(originalRow, fieldName)) {
|
||||||
|
draftOriginals.value = {
|
||||||
|
...draftOriginals.value,
|
||||||
|
[recordKey]: {
|
||||||
|
...(draftOriginals.value[recordKey] || {}),
|
||||||
|
[fieldName]: payload.oldValue,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cellErrors.value[recordKey]?.[fieldName]) {
|
||||||
|
const nextErrors = { ...cellErrors.value }
|
||||||
|
const nextRowErrors = { ...(nextErrors[recordKey] || {}) }
|
||||||
|
delete nextRowErrors[fieldName]
|
||||||
|
if (Object.keys(nextRowErrors).length === 0) {
|
||||||
|
delete nextErrors[recordKey]
|
||||||
|
} else {
|
||||||
|
nextErrors[recordKey] = nextRowErrors
|
||||||
|
}
|
||||||
|
cellErrors.value = nextErrors
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSaveDrafts = async () => {
|
||||||
|
if (Object.keys(draftEdits.value).length === 0) return
|
||||||
|
savingDrafts.value = true
|
||||||
|
const nextErrors: Record<string, Record<string, string>> = {}
|
||||||
|
const nextDrafts = { ...draftEdits.value }
|
||||||
|
const nextOriginals = { ...draftOriginals.value }
|
||||||
|
|
||||||
|
for (const [recordKey, changes] of Object.entries(draftEdits.value)) {
|
||||||
|
const record = records.value.find(item => normalizeRecordId(item.id) === recordKey)
|
||||||
|
if (!record) {
|
||||||
|
delete nextDrafts[recordKey]
|
||||||
|
delete nextOriginals[recordKey]
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await updateRecord(record.id, changes)
|
||||||
|
delete nextDrafts[recordKey]
|
||||||
|
delete nextOriginals[recordKey]
|
||||||
|
} catch (e: any) {
|
||||||
|
const message = e.message || 'Failed to update record'
|
||||||
|
nextErrors[recordKey] = {}
|
||||||
|
for (const fieldName of Object.keys(changes)) {
|
||||||
|
nextErrors[recordKey][fieldName] = message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
draftEdits.value = nextDrafts
|
||||||
|
draftOriginals.value = nextOriginals
|
||||||
|
cellErrors.value = nextErrors
|
||||||
|
savingDrafts.value = false
|
||||||
|
if (Object.keys(nextErrors).length > 0) {
|
||||||
|
error.value = 'Some updates failed. Fix highlighted cells and try again.'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDiscardDrafts = () => {
|
||||||
|
for (const [recordKey, fields] of Object.entries(draftOriginals.value)) {
|
||||||
|
const record = records.value.find(item => normalizeRecordId(item.id) === recordKey)
|
||||||
|
if (!record) continue
|
||||||
|
for (const [fieldName, originalValue] of Object.entries(fields)) {
|
||||||
|
record[fieldName] = originalValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
draftEdits.value = {}
|
||||||
|
draftOriginals.value = {}
|
||||||
|
cellErrors.value = {}
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => records.value.map(record => normalizeRecordId(record.id)),
|
||||||
|
(ids) => {
|
||||||
|
const idSet = new Set(ids)
|
||||||
|
const nextDrafts: Record<string, Record<string, any>> = {}
|
||||||
|
const nextOriginals: Record<string, Record<string, any>> = {}
|
||||||
|
const nextErrors: Record<string, Record<string, string>> = {}
|
||||||
|
|
||||||
|
for (const [recordKey, fields] of Object.entries(draftEdits.value)) {
|
||||||
|
if (idSet.has(recordKey)) nextDrafts[recordKey] = fields
|
||||||
|
}
|
||||||
|
for (const [recordKey, fields] of Object.entries(draftOriginals.value)) {
|
||||||
|
if (idSet.has(recordKey)) nextOriginals[recordKey] = fields
|
||||||
|
}
|
||||||
|
for (const [recordKey, fields] of Object.entries(cellErrors.value)) {
|
||||||
|
if (idSet.has(recordKey)) nextErrors[recordKey] = fields
|
||||||
|
}
|
||||||
|
|
||||||
|
draftEdits.value = nextDrafts
|
||||||
|
draftOriginals.value = nextOriginals
|
||||||
|
cellErrors.value = nextErrors
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
// 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'
|
||||||
@@ -438,6 +595,9 @@ onMounted(async () => {
|
|||||||
:total-count="totalCount"
|
:total-count="totalCount"
|
||||||
:search-summary="searchSummary"
|
:search-summary="searchSummary"
|
||||||
:base-url="`/runtime/objects`"
|
:base-url="`/runtime/objects`"
|
||||||
|
:draft-edits="draftEdits"
|
||||||
|
:cell-errors="cellErrors"
|
||||||
|
:saving-drafts="savingDrafts"
|
||||||
selectable
|
selectable
|
||||||
@row-click="handleRowClick"
|
@row-click="handleRowClick"
|
||||||
@create="handleCreate"
|
@create="handleCreate"
|
||||||
@@ -446,6 +606,10 @@ onMounted(async () => {
|
|||||||
@search="handleSearch"
|
@search="handleSearch"
|
||||||
@page-change="handlePageChange"
|
@page-change="handlePageChange"
|
||||||
@load-more="handleLoadMore"
|
@load-more="handleLoadMore"
|
||||||
|
@view-change="handleViewChange"
|
||||||
|
@cell-edit="handleCellEdit"
|
||||||
|
@save-drafts="handleSaveDrafts"
|
||||||
|
@discard-drafts="handleDiscardDrafts"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- Detail View -->
|
<!-- Detail View -->
|
||||||
|
|||||||
@@ -91,6 +91,12 @@ const editConfig = computed(() => {
|
|||||||
|
|
||||||
const listPageSize = computed(() => listConfig.value?.pageSize ?? 25)
|
const listPageSize = computed(() => listConfig.value?.pageSize ?? 25)
|
||||||
const maxFrontendRecords = computed(() => listConfig.value?.maxFrontendRecords ?? 500)
|
const maxFrontendRecords = computed(() => listConfig.value?.maxFrontendRecords ?? 500)
|
||||||
|
const normalizeRecordId = (id: any) => String(id)
|
||||||
|
|
||||||
|
const draftEdits = ref<Record<string, Record<string, any>>>({})
|
||||||
|
const draftOriginals = ref<Record<string, Record<string, any>>>({})
|
||||||
|
const cellErrors = ref<Record<string, Record<string, string>>>({})
|
||||||
|
const savingDrafts = ref(false)
|
||||||
|
|
||||||
// Fetch object definition
|
// Fetch object definition
|
||||||
const fetchObjectDefinition = async () => {
|
const fetchObjectDefinition = async () => {
|
||||||
@@ -231,20 +237,138 @@ const handleViewChange = async (mode: 'list' | 'spreadsheet') => {
|
|||||||
|
|
||||||
const handleCellEdit = async (payload: { row: any; field: any; newValue: any; oldValue: any }) => {
|
const handleCellEdit = async (payload: { row: any; field: any; newValue: any; oldValue: any }) => {
|
||||||
if (!payload?.row?.id || payload.newValue === payload.oldValue) return
|
if (!payload?.row?.id || payload.newValue === payload.oldValue) return
|
||||||
|
const recordKey = normalizeRecordId(payload.row.id)
|
||||||
|
const fieldName = payload.field.apiName
|
||||||
|
const originalRow = draftOriginals.value[recordKey]
|
||||||
|
const originalValue = originalRow && Object.prototype.hasOwnProperty.call(originalRow, fieldName)
|
||||||
|
? originalRow[fieldName]
|
||||||
|
: payload.oldValue
|
||||||
|
|
||||||
|
if (Object.is(payload.newValue, originalValue)) {
|
||||||
|
const nextDrafts = { ...draftEdits.value }
|
||||||
|
const nextRowDrafts = { ...(nextDrafts[recordKey] || {}) }
|
||||||
|
delete nextRowDrafts[fieldName]
|
||||||
|
if (Object.keys(nextRowDrafts).length === 0) {
|
||||||
|
delete nextDrafts[recordKey]
|
||||||
|
} else {
|
||||||
|
nextDrafts[recordKey] = nextRowDrafts
|
||||||
|
}
|
||||||
|
draftEdits.value = nextDrafts
|
||||||
|
|
||||||
|
const nextOriginals = { ...draftOriginals.value }
|
||||||
|
const nextRowOriginals = { ...(nextOriginals[recordKey] || {}) }
|
||||||
|
delete nextRowOriginals[fieldName]
|
||||||
|
if (Object.keys(nextRowOriginals).length === 0) {
|
||||||
|
delete nextOriginals[recordKey]
|
||||||
|
} else {
|
||||||
|
nextOriginals[recordKey] = nextRowOriginals
|
||||||
|
}
|
||||||
|
draftOriginals.value = nextOriginals
|
||||||
|
} else {
|
||||||
|
draftEdits.value = {
|
||||||
|
...draftEdits.value,
|
||||||
|
[recordKey]: {
|
||||||
|
...(draftEdits.value[recordKey] || {}),
|
||||||
|
[fieldName]: payload.newValue,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if (!originalRow || !Object.prototype.hasOwnProperty.call(originalRow, fieldName)) {
|
||||||
|
draftOriginals.value = {
|
||||||
|
...draftOriginals.value,
|
||||||
|
[recordKey]: {
|
||||||
|
...(draftOriginals.value[recordKey] || {}),
|
||||||
|
[fieldName]: payload.oldValue,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cellErrors.value[recordKey]?.[fieldName]) {
|
||||||
|
const nextErrors = { ...cellErrors.value }
|
||||||
|
const nextRowErrors = { ...(nextErrors[recordKey] || {}) }
|
||||||
|
delete nextRowErrors[fieldName]
|
||||||
|
if (Object.keys(nextRowErrors).length === 0) {
|
||||||
|
delete nextErrors[recordKey]
|
||||||
|
} else {
|
||||||
|
nextErrors[recordKey] = nextRowErrors
|
||||||
|
}
|
||||||
|
cellErrors.value = nextErrors
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSaveDrafts = async () => {
|
||||||
|
if (Object.keys(draftEdits.value).length === 0) return
|
||||||
|
savingDrafts.value = true
|
||||||
|
const nextErrors: Record<string, Record<string, string>> = {}
|
||||||
|
const nextDrafts = { ...draftEdits.value }
|
||||||
|
const nextOriginals = { ...draftOriginals.value }
|
||||||
|
|
||||||
|
for (const [recordKey, changes] of Object.entries(draftEdits.value)) {
|
||||||
|
const record = records.value.find(item => normalizeRecordId(item.id) === recordKey)
|
||||||
|
if (!record) {
|
||||||
|
delete nextDrafts[recordKey]
|
||||||
|
delete nextOriginals[recordKey]
|
||||||
|
continue
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
await updateRecord(payload.row.id, {
|
await updateRecord(record.id, changes)
|
||||||
...payload.row,
|
delete nextDrafts[recordKey]
|
||||||
[payload.field.apiName]: payload.newValue,
|
delete nextOriginals[recordKey]
|
||||||
})
|
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
error.value = e.message || 'Failed to update record'
|
const message = e.message || 'Failed to update record'
|
||||||
const record = records.value.find(item => item.id === payload.row.id)
|
nextErrors[recordKey] = {}
|
||||||
if (record) {
|
for (const fieldName of Object.keys(changes)) {
|
||||||
record[payload.field.apiName] = payload.oldValue
|
nextErrors[recordKey][fieldName] = message
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
draftEdits.value = nextDrafts
|
||||||
|
draftOriginals.value = nextOriginals
|
||||||
|
cellErrors.value = nextErrors
|
||||||
|
savingDrafts.value = false
|
||||||
|
if (Object.keys(nextErrors).length > 0) {
|
||||||
|
error.value = 'Some updates failed. Fix highlighted cells and try again.'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDiscardDrafts = () => {
|
||||||
|
for (const [recordKey, fields] of Object.entries(draftOriginals.value)) {
|
||||||
|
const record = records.value.find(item => normalizeRecordId(item.id) === recordKey)
|
||||||
|
if (!record) continue
|
||||||
|
for (const [fieldName, originalValue] of Object.entries(fields)) {
|
||||||
|
record[fieldName] = originalValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
draftEdits.value = {}
|
||||||
|
draftOriginals.value = {}
|
||||||
|
cellErrors.value = {}
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => records.value.map(record => normalizeRecordId(record.id)),
|
||||||
|
(ids) => {
|
||||||
|
const idSet = new Set(ids)
|
||||||
|
const nextDrafts: Record<string, Record<string, any>> = {}
|
||||||
|
const nextOriginals: Record<string, Record<string, any>> = {}
|
||||||
|
const nextErrors: Record<string, Record<string, string>> = {}
|
||||||
|
|
||||||
|
for (const [recordKey, fields] of Object.entries(draftEdits.value)) {
|
||||||
|
if (idSet.has(recordKey)) nextDrafts[recordKey] = fields
|
||||||
|
}
|
||||||
|
for (const [recordKey, fields] of Object.entries(draftOriginals.value)) {
|
||||||
|
if (idSet.has(recordKey)) nextOriginals[recordKey] = fields
|
||||||
|
}
|
||||||
|
for (const [recordKey, fields] of Object.entries(cellErrors.value)) {
|
||||||
|
if (idSet.has(recordKey)) nextErrors[recordKey] = fields
|
||||||
|
}
|
||||||
|
|
||||||
|
draftEdits.value = nextDrafts
|
||||||
|
draftOriginals.value = nextOriginals
|
||||||
|
cellErrors.value = nextErrors
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
// 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'
|
||||||
@@ -311,6 +435,9 @@ onMounted(async () => {
|
|||||||
:data="records"
|
:data="records"
|
||||||
:loading="dataLoading"
|
:loading="dataLoading"
|
||||||
:total-count="totalCount"
|
:total-count="totalCount"
|
||||||
|
:draft-edits="draftEdits"
|
||||||
|
:cell-errors="cellErrors"
|
||||||
|
:saving-drafts="savingDrafts"
|
||||||
selectable
|
selectable
|
||||||
@row-click="handleRowClick"
|
@row-click="handleRowClick"
|
||||||
@create="handleCreate"
|
@create="handleCreate"
|
||||||
@@ -320,6 +447,8 @@ onMounted(async () => {
|
|||||||
@load-more="handleLoadMore"
|
@load-more="handleLoadMore"
|
||||||
@view-change="handleViewChange"
|
@view-change="handleViewChange"
|
||||||
@cell-edit="handleCellEdit"
|
@cell-edit="handleCellEdit"
|
||||||
|
@save-drafts="handleSaveDrafts"
|
||||||
|
@discard-drafts="handleDiscardDrafts"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- Detail View -->
|
<!-- Detail View -->
|
||||||
|
|||||||
Reference in New Issue
Block a user