Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions cypress/e2e/entity-not-found.cy.js
Original file line number Diff line number Diff line change
@@ -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')
})
})
17 changes: 10 additions & 7 deletions src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -88,15 +88,17 @@ 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}"]`)
this.switchActiveMenuEntry(targetElement)
}
} 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}"]`)
Expand All @@ -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)
}
Expand All @@ -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) {
Expand Down
48 changes: 48 additions & 0 deletions src/modules/main/partials/ErrorMessage.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<!--
- SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
- SPDX-License-Identifier: AGPL-3.0-or-later
-->

<template>
<div class="error-container">
<IconTables :size="64" style="margin-bottom: 1rem;" />
<p>{{ message }}</p>
</div>
</template>

<script>
import IconTables from '../../../shared/assets/icons/IconTables.vue'

export default {
name: 'ErrorMessage',
components: { IconTables },
props: {
message: {
type: String,
required: true,
},
},
}
</script>

<style lang="scss">
.error-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
padding: 2rem;
height: 100dvh;
min-height: 100%;
color: var(--color-text);
opacity: 0.6;

p {
font-size: clamp(1.2rem, 4vw, 2rem);
font-weight: 600;
max-width: 90%;
word-wrap: break-word;
}
}
</style>
117 changes: 72 additions & 45 deletions src/pages/Context.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
<template>
<div class="row">
<div v-if="loading" class="icon-loading" />
<div v-if="!loading && context">

<div v-else-if="activeContext">
<div class="content context">
<div class="row first-row">
<h1 class="context__title" data-cy="context-title">
Expand All @@ -32,9 +33,11 @@
</div>
</div>
</div>

<MainModals />
</div>

<ErrorMessage v-else-if="errorMessage" :message="errorMessage" />

<MainModals />
</div>
</template>

Expand All @@ -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,
},
Expand All @@ -73,6 +79,7 @@ export default {
viewSetting: {},
context: null,
contextResources: [],
errorMessage: null,
}
},

Expand Down Expand Up @@ -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) {
Expand All @@ -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')
Comment thread
silverkszlo marked this conversation as resolved.
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')
Comment thread
silverkszlo marked this conversation as resolved.
}
}
}
} 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 })
Expand Down
Loading
Loading