From 2fe13543f82425b643af46b657b2996534041c5d Mon Sep 17 00:00:00 2001 From: aquananu Date: Wed, 16 Sep 2026 23:34:54 +0530 Subject: [PATCH] fix(ui): safeguard attachment rendering and tiptap editor teardown - Guard AttachmentList against missing extendedData and info properties - Guard CardSidebar currentCard watcher when card is deselected - Safeguard Description editor setup and teardown against unmounted states Signed-off-by: aquananu --- src/components/card/AttachmentList.vue | 18 +++--- src/components/card/CardSidebar.vue | 4 +- src/components/card/Description.vue | 77 ++++++++++++++++++-------- 3 files changed, 67 insertions(+), 32 deletions(-) diff --git a/src/components/card/AttachmentList.vue b/src/components/card/AttachmentList.vue index d602ae3bb..72915f25b 100644 --- a/src/components/card/AttachmentList.vue +++ b/src/components/card/AttachmentList.vue @@ -46,9 +46,9 @@ .{{ attachmentExtension(attachment) }}
- {{ formattedFileSize(attachment.extendedData.filesize) }} + {{ formattedFileSize(attachment.extendedData?.filesize) }} {{ relativeDate(attachment.createdAt*1000) }} - {{ attachment.extendedData.attachmentCreator.displayName }} + {{ attachment.extendedData?.attachmentCreator?.displayName }}
{{ t('deck', 'Pending share') }} @@ -156,7 +156,7 @@ export default { if (!attachment) { return {} } - const url = attachment.extendedData.hasPreview ? this.attachmentPreview(attachment) : OC.MimeType.getIconUrl(attachment.extendedData.mimetype) + const url = attachment?.extendedData?.hasPreview ? this.attachmentPreview(attachment) : OC.MimeType.getIconUrl(attachment?.extendedData?.mimetype) const styles = { 'background-image': `url("${url}")`, } @@ -164,16 +164,16 @@ export default { } }, attachmentPreview() { - return (attachment) => (attachment.extendedData.fileid ? generateUrl(`/core/preview?fileId=${attachment.extendedData.fileid}&x=64&y=64`) : null) + return (attachment) => (attachment?.extendedData?.fileid ? generateUrl(`/core/preview?fileId=${attachment.extendedData.fileid}&x=64&y=64`) : null) }, attachmentUrl() { - return (attachment) => generateUrl(`/apps/deck/cards/${attachment.cardId}/attachment/${attachment.id}`) + return (attachment) => (attachment?.cardId && attachment?.id ? generateUrl(`/apps/deck/cards/${attachment.cardId}/attachment/${attachment.id}`) : '#') }, internalLink() { - return (attachment) => generateUrl('/f/' + attachment.extendedData.fileid) + return (attachment) => (attachment?.extendedData?.fileid ? generateUrl('/f/' + attachment.extendedData.fileid) : '#') }, downloadLink() { - return (attachment) => generateRemoteUrl(`dav/files/${getCurrentUser().uid}/${attachment.extendedData.path}`) + return (attachment) => (attachment?.extendedData?.path ? generateRemoteUrl(`dav/files/${getCurrentUser().uid}/${attachment.extendedData.path}`) : '#') }, formattedFileSize() { return (filesize) => formatFileSize(filesize) @@ -194,11 +194,11 @@ export default { }, attachmentBasename() { return (attachment) => attachment?.extendedData?.info?.filename - ?? (attachment?.name ?? attachment.data).replace(/\.[^/.]+$/, '') + ?? (attachment?.name ?? attachment?.data ?? '').replace(/\.[^/.]+$/, '') }, attachmentExtension() { return (attachment) => attachment?.extendedData?.info?.extension - ?? (attachment?.name ?? attachment.data).split('.').pop() + ?? (attachment?.name ?? attachment?.data ?? '').split('.').pop() }, cardDetailsInModal() { return this.$store.getters.config('cardDetailsInModal') diff --git a/src/components/card/CardSidebar.vue b/src/components/card/CardSidebar.vue index 3cafc6ce8..f003b9ed6 100644 --- a/src/components/card/CardSidebar.vue +++ b/src/components/card/CardSidebar.vue @@ -209,7 +209,9 @@ export default { watch: { currentCard(newCard, oldCard) { if (newCard?.id === oldCard?.id) return - this.focusHeader() + if (newCard) { + this.focusHeader() + } }, 'currentCard.title': { immediate: true, diff --git a/src/components/card/Description.vue b/src/components/card/Description.vue index 125635382..fb59145ae 100644 --- a/src/components/card/Description.vue +++ b/src/components/card/Description.vue @@ -183,8 +183,12 @@ export default { this.descriptionOld = newCard.description this.description = newCard.description - if (this.editor) { - this.editor.setContent(this.description) + if (this.editor && typeof this.editor.setContent === 'function') { + try { + this.editor.setContent(this.description) + } catch (e) { + console.debug('Failed to set editor content', e) + } } showWarning(t('deck', 'The description has been changed by another user.'), { timeout: 3000 }) } @@ -205,29 +209,56 @@ export default { this.descriptionLastEdit = 0 this.descriptionOld = this.card.description this.description = this.card.description - this.editor = await window.OCA.Text.createEditor({ - el: this.$refs.editor, - content: this.card.description, - readOnly: !this.canEdit, - onLoaded: () => { - this.descriptionLastEdit = 0 - }, - onUpdate: ({ markdown }) => { - if (this.description === markdown) { - return + if (!window.OCA?.Text?.createEditor || !this.$refs.editor) { + return + } + try { + const editor = await window.OCA.Text.createEditor({ + el: this.$refs.editor, + content: this.card.description, + readOnly: !this.canEdit, + onLoaded: () => { + this.descriptionLastEdit = 0 + }, + onUpdate: ({ markdown }) => { + if (this.description === markdown) { + return + } + this.description = markdown + this.updateDescription() + }, + onFileInsert: () => { + this.showAttachmentModal() + }, + }) + if (this._isBeingDestroyed || this._isDestroyed) { + if (editor && typeof editor.destroy === 'function') { + try { + await editor.destroy() + } catch (e) { + // Ignore teardown error on already destroyed component + } } - this.description = markdown - this.updateDescription() - }, - onFileInsert: () => { - this.showAttachmentModal() - }, - }) - + return + } + this.editor = editor + } catch (e) { + console.warn('Failed to initialize text editor', e) + } }, async destroyEditor() { await this.saveDescription() - this?.editor?.destroy() + if (this.editor) { + const editor = this.editor + this.editor = null + try { + if (typeof editor.destroy === 'function' && !editor.isDestroyed) { + await editor.destroy() + } + } catch (e) { + console.debug('Caught editor teardown error', e) + } + } }, addKeyListeners() { this.$refs.markdownEditor.easymde.codemirror.on('keydown', (a, b) => { @@ -268,7 +299,9 @@ export default { const asImage = (attachment.type === 'file' && attachment.extendedData.hasPreview) || attachment.extendedData.mimetype.includes('image') // We need to strip those as text does not support rtl yet, so we cannot insert them separately const stripRTLO = (text) => text.replaceAll('\u202e', '') - const fileName = stripRTLO(attachment.extendedData.info.filename) + '.' + stripRTLO(attachment.extendedData.info.extension) + const base = attachment?.extendedData?.info?.filename ?? attachment?.data ?? '' + const ext = attachment?.extendedData?.info?.extension + const fileName = stripRTLO(base) + (ext ? '.' + stripRTLO(ext) : '') if (this.editor) { this.editor.insertAtCursor( asImage