diff --git a/app/src/modules/settings/routes/data-model/fields/fields.vue b/app/src/modules/settings/routes/data-model/fields/fields.vue
index 2103e947e5..b1f6e15817 100644
--- a/app/src/modules/settings/routes/data-model/fields/fields.vue
+++ b/app/src/modules/settings/routes/data-model/fields/fields.vue
@@ -80,6 +80,19 @@
+
+
+
+ {{ t('unsaved_changes') }}
+ {{ t('unsaved_changes_copy') }}
+
+
+ {{ t('discard_changes') }}
+
+ {{ t('keep_editing') }}
+
+
+
@@ -91,8 +104,9 @@ import { useCollection } from '@directus/shared/composables';
import FieldsManagement from './components/fields-management.vue';
import useItem from '@/composables/use-item';
-import { useRouter } from 'vue-router';
+import { useRouter, onBeforeRouteUpdate, onBeforeRouteLeave, NavigationGuard } from 'vue-router';
import { useCollectionsStore, useFieldsStore } from '@/stores';
+import unsavedChanges from '@/composables/unsaved-changes';
export default defineComponent({
components: { SettingsNavigation, FieldsManagement },
@@ -131,6 +145,26 @@ export default defineComponent({
const confirmDelete = ref(false);
+ const isSavable = computed(() => {
+ if (hasEdits.value === true) return true;
+ return hasEdits.value;
+ });
+
+ unsavedChanges(isSavable);
+
+ const confirmLeave = ref(false);
+ const leaveTo = ref(null);
+
+ const editsGuard: NavigationGuard = (to) => {
+ if (hasEdits.value) {
+ confirmLeave.value = true;
+ leaveTo.value = to.fullPath;
+ return false;
+ }
+ };
+ onBeforeRouteUpdate(editsGuard);
+ onBeforeRouteLeave(editsGuard);
+
return {
t,
collectionInfo,
@@ -150,6 +184,10 @@ export default defineComponent({
deleteAndQuit,
saveAndQuit,
hasEdits,
+ isSavable,
+ confirmLeave,
+ leaveTo,
+ discardAndLeave,
};
async function deleteAndQuit() {
@@ -165,6 +203,13 @@ export default defineComponent({
await fieldsStore.hydrate();
router.push(`/settings/data-model`);
}
+
+ function discardAndLeave() {
+ if (!leaveTo.value) return;
+ edits.value = {};
+ confirmLeave.value = false;
+ router.push(leaveTo.value);
+ }
},
});
diff --git a/app/src/modules/settings/routes/presets/item.vue b/app/src/modules/settings/routes/presets/item.vue
index 40dd64e9aa..31b7fa196d 100644
--- a/app/src/modules/settings/routes/presets/item.vue
+++ b/app/src/modules/settings/routes/presets/item.vue
@@ -113,6 +113,19 @@
+
+
+
+ {{ t('unsaved_changes') }}
+ {{ t('unsaved_changes_copy') }}
+
+
+ {{ t('discard_changes') }}
+
+ {{ t('keep_editing') }}
+
+
+
@@ -126,9 +139,10 @@ import { Preset, Filter } from '@directus/shared/types';
import api from '@/api';
import { useCollectionsStore, usePresetsStore } from '@/stores';
import { getLayouts } from '@/layouts';
-import { useRouter } from 'vue-router';
+import { useRouter, onBeforeRouteUpdate, onBeforeRouteLeave, NavigationGuard } from 'vue-router';
import { unexpectedError } from '@/utils/unexpected-error';
import { useLayout } from '@/composables/use-layout';
+import unsavedChanges from '@/composables/unsaved-changes';
type FormattedPreset = {
id: number;
@@ -184,6 +198,26 @@ export default defineComponent({
const { layoutWrapper } = useLayout(layout);
+ const isSavable = computed(() => {
+ if (hasEdits.value === true) return true;
+ return hasEdits.value;
+ });
+
+ unsavedChanges(isSavable);
+
+ const confirmLeave = ref(false);
+ const leaveTo = ref(null);
+
+ const editsGuard: NavigationGuard = (to) => {
+ if (hasEdits.value) {
+ confirmLeave.value = true;
+ leaveTo.value = to.fullPath;
+ return false;
+ }
+ };
+ onBeforeRouteUpdate(editsGuard);
+ onBeforeRouteLeave(editsGuard);
+
return {
t,
backLink,
@@ -205,6 +239,10 @@ export default defineComponent({
confirmDelete,
updateFilters,
searchQuery,
+ isSavable,
+ confirmLeave,
+ leaveTo,
+ discardAndLeave,
};
function useSave() {
@@ -496,6 +534,13 @@ export default defineComponent({
return { fields };
}
+
+ function discardAndLeave() {
+ if (!leaveTo.value) return;
+ edits.value = {};
+ confirmLeave.value = false;
+ router.push(leaveTo.value);
+ }
},
});
diff --git a/app/src/modules/settings/routes/project/project.vue b/app/src/modules/settings/routes/project/project.vue
index c6d628c949..5184379265 100644
--- a/app/src/modules/settings/routes/project/project.vue
+++ b/app/src/modules/settings/routes/project/project.vue
@@ -24,6 +24,19 @@
+
+
+
+ {{ t('unsaved_changes') }}
+ {{ t('unsaved_changes_copy') }}
+
+
+ {{ t('discard_changes') }}
+
+ {{ t('keep_editing') }}
+
+
+
@@ -35,12 +48,16 @@ import { useCollection } from '@directus/shared/composables';
import { useSettingsStore, useServerStore } from '@/stores';
import ProjectInfoSidebarDetail from './components/project-info-sidebar-detail.vue';
import { clone } from 'lodash';
+import unsavedChanges from '@/composables/unsaved-changes';
+import { useRouter, onBeforeRouteUpdate, onBeforeRouteLeave, NavigationGuard } from 'vue-router';
export default defineComponent({
components: { SettingsNavigation, ProjectInfoSidebarDetail },
setup() {
const { t } = useI18n();
+ const router = useRouter();
+
const settingsStore = useSettingsStore();
const serverStore = useServerStore();
@@ -54,7 +71,39 @@ export default defineComponent({
const saving = ref(false);
- return { t, fields, initialValues, edits, noEdits, saving, save };
+ const isSavable = computed(() => {
+ if (noEdits.value === true) return false;
+ return noEdits.value;
+ });
+
+ unsavedChanges(isSavable);
+
+ const confirmLeave = ref(false);
+ const leaveTo = ref(null);
+
+ const editsGuard: NavigationGuard = (to) => {
+ if (!noEdits.value) {
+ confirmLeave.value = true;
+ leaveTo.value = to.fullPath;
+ return false;
+ }
+ };
+ onBeforeRouteUpdate(editsGuard);
+ onBeforeRouteLeave(editsGuard);
+
+ return {
+ t,
+ fields,
+ initialValues,
+ edits,
+ noEdits,
+ saving,
+ isSavable,
+ confirmLeave,
+ leaveTo,
+ save,
+ discardAndLeave,
+ };
async function save() {
if (edits.value === null) return;
@@ -65,6 +114,13 @@ export default defineComponent({
saving.value = false;
initialValues.value = clone(settingsStore.settings);
}
+
+ function discardAndLeave() {
+ if (!leaveTo.value) return;
+ edits.value = {};
+ confirmLeave.value = false;
+ router.push(leaveTo.value);
+ }
},
});
diff --git a/app/src/modules/settings/routes/roles/item/item.vue b/app/src/modules/settings/routes/roles/item/item.vue
index 2b9cd9dfb4..9b5658a0e4 100644
--- a/app/src/modules/settings/routes/roles/item/item.vue
+++ b/app/src/modules/settings/routes/roles/item/item.vue
@@ -84,6 +84,19 @@
+
+
+
+ {{ t('unsaved_changes') }}
+ {{ t('unsaved_changes_copy') }}
+
+
+ {{ t('discard_changes') }}
+
+ {{ t('keep_editing') }}
+
+
+
@@ -92,13 +105,14 @@ import { useI18n } from 'vue-i18n';
import { defineComponent, computed, toRefs, ref } from 'vue';
import SettingsNavigation from '../../../components/navigation.vue';
-import { useRouter } from 'vue-router';
+import { useRouter, onBeforeRouteUpdate, onBeforeRouteLeave, NavigationGuard } from 'vue-router';
import RevisionsDrawerDetail from '@/views/private/components/revisions-drawer-detail';
import useItem from '@/composables/use-item';
import { useUserStore } from '@/stores/';
import RoleInfoSidebarDetail from './components/role-info-sidebar-detail.vue';
import PermissionsOverview from './components/permissions-overview.vue';
import UsersInvite from '@/views/private/components/users-invite';
+import unsavedChanges from '@/composables/unsaved-changes';
export default defineComponent({
name: 'RolesItem',
@@ -149,6 +163,26 @@ export default defineComponent({
return !!values.app_access;
});
+ const isSavable = computed(() => {
+ if (hasEdits.value === true) return true;
+ return hasEdits.value;
+ });
+
+ unsavedChanges(isSavable);
+
+ const confirmLeave = ref(false);
+ const leaveTo = ref(null);
+
+ const editsGuard: NavigationGuard = (to) => {
+ if (hasEdits.value) {
+ confirmLeave.value = true;
+ leaveTo.value = to.fullPath;
+ return false;
+ }
+ };
+ onBeforeRouteUpdate(editsGuard);
+ onBeforeRouteLeave(editsGuard);
+
return {
t,
item,
@@ -165,6 +199,10 @@ export default defineComponent({
adminEnabled,
userInviteModalActive,
appAccess,
+ isSavable,
+ confirmLeave,
+ leaveTo,
+ discardAndLeave,
};
/**
@@ -184,6 +222,13 @@ export default defineComponent({
await remove();
router.push(`/settings/roles`);
}
+
+ function discardAndLeave() {
+ if (!leaveTo.value) return;
+ edits.value = {};
+ confirmLeave.value = false;
+ router.push(leaveTo.value);
+ }
},
});
diff --git a/app/src/modules/settings/routes/webhooks/item.vue b/app/src/modules/settings/routes/webhooks/item.vue
index 021c1ed1f4..033e870eae 100644
--- a/app/src/modules/settings/routes/webhooks/item.vue
+++ b/app/src/modules/settings/routes/webhooks/item.vue
@@ -64,6 +64,19 @@
+
+
+
+ {{ t('unsaved_changes') }}
+ {{ t('unsaved_changes_copy') }}
+
+
+ {{ t('discard_changes') }}
+
+ {{ t('keep_editing') }}
+
+
+
@@ -72,10 +85,11 @@ import { useI18n } from 'vue-i18n';
import { defineComponent, computed, toRefs, ref } from 'vue';
import SettingsNavigation from '../../components/navigation.vue';
-import { useRouter } from 'vue-router';
+import { useRouter, onBeforeRouteUpdate, onBeforeRouteLeave, NavigationGuard } from 'vue-router';
import RevisionsDrawerDetail from '@/views/private/components/revisions-drawer-detail';
import useItem from '@/composables/use-item';
import SaveOptions from '@/views/private/components/save-options';
+import unsavedChanges from '@/composables/unsaved-changes';
export default defineComponent({
name: 'WebhooksItem',
@@ -117,6 +131,26 @@ export default defineComponent({
return item.value?.name;
});
+ const isSavable = computed(() => {
+ if (hasEdits.value === true) return true;
+ return hasEdits.value;
+ });
+
+ unsavedChanges(isSavable);
+
+ const confirmLeave = ref(false);
+ const leaveTo = ref(null);
+
+ const editsGuard: NavigationGuard = (to) => {
+ if (hasEdits.value) {
+ confirmLeave.value = true;
+ leaveTo.value = to.fullPath;
+ return false;
+ }
+ };
+ onBeforeRouteUpdate(editsGuard);
+ onBeforeRouteLeave(editsGuard);
+
return {
t,
item,
@@ -136,6 +170,10 @@ export default defineComponent({
isBatch,
title,
validationErrors,
+ isSavable,
+ confirmLeave,
+ leaveTo,
+ discardAndLeave,
};
async function saveAndQuit() {
@@ -161,6 +199,13 @@ export default defineComponent({
await remove();
router.push(`/settings/webhooks`);
}
+
+ function discardAndLeave() {
+ if (!leaveTo.value) return;
+ edits.value = {};
+ confirmLeave.value = false;
+ router.push(leaveTo.value);
+ }
},
});