diff --git a/cypress/e2e/entity-not-found.cy.js b/cypress/e2e/entity-not-found.cy.js
new file mode 100644
index 0000000000..3776e587bc
--- /dev/null
+++ b/cypress/e2e/entity-not-found.cy.js
@@ -0,0 +1,45 @@
+/**
+ * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+let localUser
+
+describe('Entity not found error handling', () => {
+ before(function() {
+ cy.createRandomUser().then(user => {
+ localUser = user
+ cy.login(localUser)
+ })
+ })
+
+ beforeEach(function() {
+ cy.login(localUser)
+
+ cy.intercept('GET', '**/tables/999', { statusCode: 404 }).as('getTable')
+ cy.intercept('GET', '**/views/999', { statusCode: 404 }).as('getView')
+ cy.intercept('GET', '**/contexts/999*', { statusCode: 404 }).as('getContext')
+
+ cy.visit('apps/tables')
+ })
+
+ it('Shows error message when table is not found', () => {
+ cy.visit('/apps/tables/#/table/999')
+
+ cy.get('.error-container', { timeout: 10000 })
+ .should('contain.text', 'This table could not be found')
+ })
+
+ it('Shows error message when view is not found', () => {
+ cy.visit('/apps/tables/#/view/999')
+
+ cy.get('.error-container', { timeout: 10000 })
+ .should('contain.text', 'This view could not be found')
+ })
+
+ it('Shows error message when application is not found', () => {
+ cy.visit('/apps/tables/#/application/999')
+
+ cy.get('.error-container', { timeout: 10000 })
+ .should('contain.text', 'This application could not be found')
+ })
+})
diff --git a/src/App.vue b/src/App.vue
index faece768d5..34903116a7 100644
--- a/src/App.vue
+++ b/src/App.vue
@@ -88,7 +88,8 @@ export default {
}
if (currentRoute.path.startsWith('/table/')) {
this.setActiveTableId(parseInt(currentRoute.params.tableId))
- this.setPageTitle(this.activeTable.title)
+ const tableName = this.activeTable?.title || t('tables', 'Table')
+ this.setPageTitle(tableName)
if (!currentRoute.path.includes('/row/')) {
const targetElement = document.querySelector(`header .header-start .app-menu a[href="${url}"]`)
|| document.querySelector(`header .header-left .app-menu a[href="${url}"]`)
@@ -96,7 +97,8 @@ export default {
}
} else if (currentRoute.path.startsWith('/view/')) {
this.setActiveViewId(parseInt(currentRoute.params.viewId))
- this.setPageTitle(this.activeView.title)
+ const viewName = this.activeView?.title || t('tables', 'View')
+ this.setPageTitle(viewName)
if (!currentRoute.path.includes('/row/')) {
const targetElement = document.querySelector(`header .header-start .app-menu a[href="${url}"]`)
|| document.querySelector(`header .header-left .app-menu a[href="${url}"]`)
@@ -105,11 +107,12 @@ export default {
} else if (currentRoute.path.startsWith('/application/')) {
const contextId = parseInt(currentRoute.params.contextId)
this.setActiveContextId(contextId)
- this.setPageTitle(this.activeContext.name)
+ const contextName = this.activeContext?.name || t('tables', 'Tables')
+ this.setPageTitle(contextName)
// This breaks if there are multiple contexts with the same name or another app has the same name. We need a better way to identify the correct element.
- const targetElement = document.querySelector(`header .header-start .app-menu [title="${this.activeContext.name}"]`)
- || document.querySelector(`header .header-left .app-menu [title="${this.activeContext.name}"]`)
+ const targetElement = document.querySelector(`header .header-start .app-menu [title="${contextName}"]`)
+ || document.querySelector(`header .header-left .app-menu [title="${contextName}"]`)
if (targetElement) {
this.switchActiveMenuEntry(targetElement)
}
@@ -127,8 +130,8 @@ export default {
switchActiveMenuEntry(targetElement) {
targetElement = targetElement?.tagName?.toLowerCase() === 'a' ? targetElement.parentElement : targetElement
const currentlyActive = document.querySelector('header .header-start .app-menu li.app-menu-entry--active') || document.querySelector('header .header-left .app-menu li.app-menu-entry--active')
- currentlyActive.classList.remove('app-menu-entry--active')
- targetElement.classList.add('app-menu-entry--active')
+ currentlyActive?.classList.remove('app-menu-entry--active')
+ targetElement?.classList.add('app-menu-entry--active')
},
setPageTitle(title) {
if (this.defaultPageTitle === false) {
diff --git a/src/modules/main/partials/ErrorMessage.vue b/src/modules/main/partials/ErrorMessage.vue
new file mode 100644
index 0000000000..e64db209d6
--- /dev/null
+++ b/src/modules/main/partials/ErrorMessage.vue
@@ -0,0 +1,48 @@
+
+
+
+
+
+
+
+
+
diff --git a/src/pages/Context.vue b/src/pages/Context.vue
index 6bc84a53d9..f13a8435f9 100644
--- a/src/pages/Context.vue
+++ b/src/pages/Context.vue
@@ -5,7 +5,8 @@
@@ -50,11 +53,14 @@ import exportTableMixin from '../shared/components/ncTable/mixins/exportTableMix
import svgHelper from '../shared/components/ncIconPicker/mixins/svgHelper.js'
import { useTablesStore } from '../store/store.js'
import { useDataStore } from '../store/data.js'
+import ErrorMessage from '../modules/main/partials/ErrorMessage.vue'
+import displayError, { getNotFoundError, getGenericLoadError } from '../shared/utils/displayError.js'
export default {
components: {
MainModals,
NcIconSvgWrapper,
+ ErrorMessage,
TableWrapper,
CustomView,
},
@@ -73,6 +79,7 @@ export default {
viewSetting: {},
context: null,
contextResources: [],
+ errorMessage: null,
}
},
@@ -114,14 +121,12 @@ export default {
},
watch: {
- // Watch for changes to active context to make page reactive
async activeContext() {
- if (this.activeContextId && !this.activeContext) {
- // context does not exists, go to startpage
- this.$router.push('/').catch(err => err)
- } else {
- await this.reload()
+ if (this.errorMessage) {
+ // Already showing an error, don't redirect
+ return
}
+ await this.reload()
},
'context.iconName': {
async handler(value) {
@@ -146,49 +151,71 @@ export default {
return
}
this.loading = true
- this.icon = await this.getContextIcon(this.activeContext.iconName)
this.contextResources = []
- await this.loadContext({ id: this.activeContextId })
- const index = this.contexts.findIndex(c => parseInt(c.id) === parseInt(this.activeContextId))
- this.context = this.contexts[index]
- if (this.context && this.context.nodes) {
- for (const [, node] of Object.entries(this.context.nodes)) {
- const nodeType = parseInt(node.node_type)
- if (nodeType === NODE_TYPE_TABLE) {
- const table = this.tables.find(table => table.id === node.node_id)
- if (table) {
- await this.loadColumnsFromBE({
- view: null,
- tableId: table.id,
- })
- await this.loadRowsFromBE({
- viewId: null,
- tableId: table.id,
- })
- table.key = (table.id).toString()
- table.isView = false
- this.contextResources.push(table)
- }
+ try {
+ await this.loadContext({ id: this.activeContextId })
+ const index = this.contexts.findIndex(c => parseInt(c.id) === parseInt(this.activeContextId))
+ this.context = this.contexts[index]
+
+ if (!this.context) {
+ this.errorMessage = t('tables', 'This application could not be found')
+ return
+ }
- } else if (nodeType === NODE_TYPE_VIEW) {
- const view = this.views.find(view => view.id === node.node_id)
- if (view) {
- await this.loadColumnsFromBE({
- view,
- })
- await this.loadRowsFromBE({
- viewId: view.id,
- tableId: view.tableId,
- })
- view.key = 'view-' + (view.id).toString()
- view.isView = true
- this.contextResources.push(view)
+ this.icon = await this.getContextIcon(this.activeContext.iconName)
+
+ if (this.context && this.context.nodes) {
+ for (const [, node] of Object.entries(this.context.nodes)) {
+ try {
+ const nodeType = parseInt(node.node_type)
+ if (nodeType === NODE_TYPE_TABLE) {
+ const table = this.tables.find(table => table.id === node.node_id)
+ if (table) {
+ await this.loadColumnsFromBE({
+ view: null,
+ tableId: table.id,
+ })
+ await this.loadRowsFromBE({
+ viewId: null,
+ tableId: table.id,
+ })
+ table.key = (table.id).toString()
+ table.isView = false
+ this.contextResources.push(table)
+ }
+
+ } else if (nodeType === NODE_TYPE_VIEW) {
+ const view = this.views.find(view => view.id === node.node_id)
+ if (view) {
+ await this.loadColumnsFromBE({
+ view,
+ })
+ await this.loadRowsFromBE({
+ viewId: view.id,
+ tableId: view.tableId,
+ })
+ view.key = 'view-' + (view.id).toString()
+ view.isView = true
+ this.contextResources.push(view)
+ }
+ }
+ } catch (err) {
+ console.error(`Failed to load resource ${node.node_id}:`, err)
+ this.errorMessage = t('tables', 'Some resources in this application could not be loaded')
}
}
}
+ } catch (e) {
+ if (e.message === 'NOT_FOUND') {
+ this.errorMessage = getNotFoundError('application')
+ } else {
+ this.errorMessage = getGenericLoadError('application')
+ displayError(e, this.errorMessage)
+ }
+ } finally {
+ this.loading = false
}
- this.loading = false
},
createColumn(isView, element) {
emit('tables:column:create', { isView, element })
diff --git a/src/pages/Table.vue b/src/pages/Table.vue
index 4c0255cf77..b50bcd94c2 100644
--- a/src/pages/Table.vue
+++ b/src/pages/Table.vue
@@ -4,21 +4,39 @@
-->