+
@@ -338,6 +463,18 @@ watch(
+
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index acaa9b2..7d004ca 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -13,6 +13,8 @@
"@nuxtjs/tailwindcss": "^6.11.4",
"@twilio/voice-sdk": "^2.11.2",
"@vueuse/core": "^10.11.1",
+ "ag-grid-community": "^32.3.4",
+ "ag-grid-vue3": "^32.3.4",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.0",
"gridstack": "^12.4.1",
@@ -4976,6 +4978,31 @@
"acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
}
},
+ "node_modules/ag-charts-types": {
+ "version": "10.3.9",
+ "resolved": "https://registry.npmjs.org/ag-charts-types/-/ag-charts-types-10.3.9.tgz",
+ "integrity": "sha512-drcRiJVencliC8LnRwk4MmeQDNNBg5GzmOoLFihO3/k0CUK0VF/N+2nc7iFozwaNG0btSB9vAhYuJLjqHMtRrQ==",
+ "license": "MIT"
+ },
+ "node_modules/ag-grid-community": {
+ "version": "32.3.9",
+ "resolved": "https://registry.npmjs.org/ag-grid-community/-/ag-grid-community-32.3.9.tgz",
+ "integrity": "sha512-l07SB0mCbGPkC1R8rQQFgBtI5+1FoXBi3RUk1+dHKV/UPeorMEFAzCokcsOhz0qwcWCPrHauUsbRa1SIxfVEJQ==",
+ "license": "MIT",
+ "dependencies": {
+ "ag-charts-types": "10.3.9"
+ }
+ },
+ "node_modules/ag-grid-vue3": {
+ "version": "32.3.9",
+ "resolved": "https://registry.npmjs.org/ag-grid-vue3/-/ag-grid-vue3-32.3.9.tgz",
+ "integrity": "sha512-86Xynbfg8bq/tGW0Yfl2O8HSUgT7nDtNzioMs4r8hNfPiqOfKUUWrTveP5S4Acs8Astr495ZdKKFDIcQVLx3Yg==",
+ "license": "MIT",
+ "dependencies": {
+ "ag-grid-community": "32.3.9",
+ "vue": "^3.0.0"
+ }
+ },
"node_modules/agent-base": {
"version": "7.1.4",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
diff --git a/frontend/package.json b/frontend/package.json
index 6f644cf..5c7685c 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -19,6 +19,8 @@
"@nuxtjs/tailwindcss": "^6.11.4",
"@twilio/voice-sdk": "^2.11.2",
"@vueuse/core": "^10.11.1",
+ "ag-grid-community": "^32.3.4",
+ "ag-grid-vue3": "^32.3.4",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.0",
"gridstack": "^12.4.1",
diff --git a/frontend/pages/app/objects/[objectName]/[[recordId]]/[[view]].vue b/frontend/pages/app/objects/[objectName]/[[recordId]]/[[view]].vue
index b673010..2328c16 100644
--- a/frontend/pages/app/objects/[objectName]/[[recordId]]/[[view]].vue
+++ b/frontend/pages/app/objects/[objectName]/[[recordId]]/[[view]].vue
@@ -42,6 +42,7 @@ const {
fetchRecord,
deleteRecord,
deleteRecords,
+ updateRecord,
handleSave,
} = useViewState(`/runtime/objects/${objectApiName.value}/records`)
@@ -212,6 +213,38 @@ const handleLoadMore = async (page: number, pageSize: number) => {
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') {
+ await loadAllListRecords()
+ }
+}
+
+const handleCellEdit = async (payload: { row: any; field: any; newValue: any; oldValue: any }) => {
+ if (!payload?.row?.id || payload.newValue === payload.oldValue) return
+ try {
+ await updateRecord(payload.row.id, {
+ ...payload.row,
+ [payload.field.apiName]: payload.newValue,
+ })
+ } catch (e: any) {
+ error.value = e.message || 'Failed to update record'
+ const record = records.value.find(item => item.id === payload.row.id)
+ if (record) {
+ record[payload.field.apiName] = payload.oldValue
+ }
+ }
+}
+
// Watch for route changes
watch(() => route.params, async (newParams, oldParams) => {
// Reset current record when navigating to 'new'
@@ -285,6 +318,8 @@ onMounted(async () => {
@delete="handleDelete"
@page-change="handlePageChange"
@load-more="handleLoadMore"
+ @view-change="handleViewChange"
+ @cell-edit="handleCellEdit"
/>