diff --git a/src/views/Proposal/ProposalEditor.vue b/src/views/Proposal/ProposalEditor.vue
index 80428b75f1..e4c49ad128 100644
--- a/src/views/Proposal/ProposalEditor.vue
+++ b/src/views/Proposal/ProposalEditor.vue
@@ -10,7 +10,15 @@
class="proposal-modal__content"
:title="modalTitle"
:size="modalSize"
+ :close-button-contained="false"
+ :no-close="!operationInProgress"
@close="onModalClose()">
+
+
@@ -260,6 +268,7 @@ import DeleteIcon from 'vue-material-design-icons/TrashCanOutline'
// components
import NcButton from '@nextcloud/vue/components/NcButton'
import NcCheckboxRadioSwitch from '@nextcloud/vue/components/NcCheckboxRadioSwitch'
+import NcLoadingIcon from '@nextcloud/vue/components/NcLoadingIcon'
import NcModal from '@nextcloud/vue/components/NcModal'
import NcTextArea from '@nextcloud/vue/components/NcTextArea'
import NcTextField from '@nextcloud/vue/components/NcTextField'
@@ -300,6 +309,7 @@ export default {
components: {
NcButton,
NcCheckboxRadioSwitch,
+ NcLoadingIcon,
NcModal,
NcTextField,
NcTextArea,
@@ -338,6 +348,8 @@ export default {
calendarSpanMin: 1, // Minimum days that can be shown
calendarSpanDays: 7, // Currently applied span (derived)
screenWidth: window.innerWidth, // Track screen width
+ operationInProgress: false, // Lock to prevent concurrent operations
+ operationMessage: '', // Message to display during operation
}
},
@@ -583,36 +595,60 @@ export default {
},
async onProposalDestroy(proposal: Proposal) {
+ if (this.operationInProgress) {
+ return
+ }
+
if (!confirm(t('calendar', 'Are you sure you want to delete "{title}"?', { title: proposal.title ?? t('calendar', 'No title') }))) {
return
}
+
+ this.operationInProgress = true
+ this.operationMessage = t('calendar', 'Deleting proposal…')
+
try {
- showSuccess(t('calendar', 'Deleting proposal "{title}"', { title: proposal.title ?? t('calendar', 'No title') }))
await this.proposalStore.destroyProposal(proposal)
showSuccess(t('calendar', 'Successfully deleted proposal'))
this.onModalClose()
} catch (error) {
showError(t('calendar', 'Failed to delete proposal'))
console.error('Failed to delete proposal:', error)
+ } finally {
+ this.operationInProgress = false
+ this.operationMessage = ''
}
},
async onProposalSave() {
+ if (this.operationInProgress) {
+ return
+ }
+
+ if (!this.selectedProposal) {
+ return console.error('No proposal selected for this operation')
+ }
+
+ this.operationInProgress = true
+ this.operationMessage = t('calendar', 'Saving proposal…')
+
try {
- if (!this.selectedProposal) {
- return console.error('No proposal selected for this operation')
- }
- showSuccess(t('calendar', 'Saving proposal "{title}"', { title: this.selectedProposal.title ?? t('calendar', 'No title') }))
await this.proposalStore.storeProposal(this.selectedProposal)
showSuccess(t('calendar', 'Successfully saved proposal'))
this.onModalClose()
} catch (error) {
showError(t('calendar', 'Failed to save proposal'))
console.error('Failed to save proposal:', error)
+ } finally {
+ this.operationInProgress = false
+ this.operationMessage = ''
}
},
async onProposalConvert(date: ProposalDate) {
+ if (this.operationInProgress) {
+ return
+ }
+
if (!this.selectedProposal || !date.date) {
return console.error('No proposal selected or invalid date for meeting conversion')
}
@@ -623,14 +659,19 @@ export default {
return
}
+ this.operationInProgress = true
+ this.operationMessage = t('calendar', 'Creating meeting…')
+
try {
- showSuccess(t('calendar', 'Creating meeting for {date}', { date: dateString }))
await this.proposalStore.convertProposal(this.selectedProposal, date, this.userTimezone)
showSuccess(t('calendar', 'Successfully created meeting for {date}', { date: dateString }))
this.onModalClose()
} catch (error) {
showError(t('calendar', 'Failed to create a meeting for {date}', { date: dateString }))
console.error('Failed to create a meeting:', error)
+ } finally {
+ this.operationInProgress = false
+ this.operationMessage = ''
}
},
@@ -1220,4 +1261,28 @@ export default {
/* Override background with striped pattern for better visibility */
background-image: repeating-linear-gradient(45deg, transparent 0px, transparent calc(var(--default-grid-baseline) * 1), var(--color-background-hover) calc(var(--default-grid-baseline) * 1), var(--color-background-hover) calc(var(--default-grid-baseline) * 4)) !important;
}
+
+.proposal-editor__loading-overlay {
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background-color: rgba(var(--backdrop-color), 0.5);
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ gap: calc(var(--default-grid-baseline) * 4);
+ z-index: 10000;
+ border-radius: var(--border-radius-large);
+
+ p {
+ font-size: calc(var(--default-grid-baseline) * 4);
+ font-weight: 500;
+ color: var(--color-text-primary);
+ margin: 0;
+ }
+}
+