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 @@ -->