From dc393ed9bab243263df0d3ab93169eafeefca39b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Varela?= Date: Thu, 13 Jan 2022 16:49:01 +0000 Subject: [PATCH] Create composable `use-edits-guard` (#11018) --- app/src/composables/unsaved-changes/index.ts | 4 --- .../unsaved-changes/unsaved-changes.ts | 18 ---------- app/src/composables/use-edits-guard/index.ts | 4 +++ .../use-edits-guard/use-edits-guard.ts | 36 +++++++++++++++++++ app/src/modules/content/routes/item.vue | 20 ++--------- app/src/modules/files/routes/item.vue | 26 +++++--------- .../routes/data-model/fields/fields.vue | 19 ++-------- .../modules/settings/routes/presets/item.vue | 19 ++-------- .../settings/routes/project/project.vue | 19 ++-------- .../settings/routes/roles/item/item.vue | 19 ++-------- .../modules/settings/routes/webhooks/item.vue | 19 ++-------- app/src/modules/users/routes/item.vue | 26 +++++--------- 12 files changed, 74 insertions(+), 155 deletions(-) delete mode 100644 app/src/composables/unsaved-changes/index.ts delete mode 100644 app/src/composables/unsaved-changes/unsaved-changes.ts create mode 100644 app/src/composables/use-edits-guard/index.ts create mode 100644 app/src/composables/use-edits-guard/use-edits-guard.ts diff --git a/app/src/composables/unsaved-changes/index.ts b/app/src/composables/unsaved-changes/index.ts deleted file mode 100644 index 43bde56840..0000000000 --- a/app/src/composables/unsaved-changes/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -import unsavedChanges from './unsaved-changes'; - -export { unsavedChanges }; -export default unsavedChanges; diff --git a/app/src/composables/unsaved-changes/unsaved-changes.ts b/app/src/composables/unsaved-changes/unsaved-changes.ts deleted file mode 100644 index 1135dcb799..0000000000 --- a/app/src/composables/unsaved-changes/unsaved-changes.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { onBeforeMount, onBeforeUnmount, Ref } from 'vue'; - -export default function unsavedChanges(isSavable: Ref): void { - onBeforeMount(() => { - window.addEventListener('beforeunload', beforeUnload); - }); - - onBeforeUnmount(() => { - window.removeEventListener('beforeunload', beforeUnload); - }); - function beforeUnload(event: BeforeUnloadEvent) { - if (isSavable.value) { - event.preventDefault(); - event.returnValue = ''; - return ''; - } - } -} diff --git a/app/src/composables/use-edits-guard/index.ts b/app/src/composables/use-edits-guard/index.ts new file mode 100644 index 0000000000..df1fe99c90 --- /dev/null +++ b/app/src/composables/use-edits-guard/index.ts @@ -0,0 +1,4 @@ +import { useEditsGuard } from './use-edits-guard'; + +export { useEditsGuard }; +export default useEditsGuard; diff --git a/app/src/composables/use-edits-guard/use-edits-guard.ts b/app/src/composables/use-edits-guard/use-edits-guard.ts new file mode 100644 index 0000000000..b5a04076c9 --- /dev/null +++ b/app/src/composables/use-edits-guard/use-edits-guard.ts @@ -0,0 +1,36 @@ +import { ref, Ref, onBeforeMount, onBeforeUnmount } from 'vue'; +import { onBeforeRouteUpdate, onBeforeRouteLeave, NavigationGuard } from 'vue-router'; + +export function useEditsGuard(isSavable: Ref) { + const confirmLeave = ref(false); + const leaveTo = ref(null); + + const beforeUnload = (event: BeforeUnloadEvent) => { + if (isSavable.value) { + event.preventDefault(); + event.returnValue = ''; + return ''; + } + }; + + const editsGuard: NavigationGuard = (to) => { + if (isSavable.value) { + confirmLeave.value = true; + leaveTo.value = to.fullPath; + return false; + } + }; + + onBeforeMount(() => { + window.addEventListener('beforeunload', beforeUnload); + }); + + onBeforeUnmount(() => { + window.removeEventListener('beforeunload', beforeUnload); + }); + + onBeforeRouteUpdate(editsGuard); + onBeforeRouteLeave(editsGuard); + + return { confirmLeave, leaveTo }; +} diff --git a/app/src/modules/content/routes/item.vue b/app/src/modules/content/routes/item.vue index 842a0852db..f312dd0017 100644 --- a/app/src/modules/content/routes/item.vue +++ b/app/src/modules/content/routes/item.vue @@ -212,9 +212,9 @@ import SharesSidebarDetail from '@/views/private/components/shares-sidebar-detai import useItem from '@/composables/use-item'; import SaveOptions from '@/views/private/components/save-options'; import useShortcut from '@/composables/use-shortcut'; -import { useRouter, onBeforeRouteUpdate, onBeforeRouteLeave, NavigationGuard } from 'vue-router'; +import { useRouter } from 'vue-router'; import { usePermissions } from '@/composables/use-permissions'; -import unsavedChanges from '@/composables/unsaved-changes'; +import useEditsGuard from '@/composables/use-edits-guard'; import { useTitle } from '@/composables/use-title'; import { renderStringTemplate } from '@/utils/render-string-template'; import useTemplateData from '@/composables/use-template-data'; @@ -302,14 +302,10 @@ export default defineComponent({ return hasEdits.value; }); - unsavedChanges(isSavable); - + const { confirmLeave, leaveTo } = useEditsGuard(isSavable); const confirmDelete = ref(false); const confirmArchive = ref(false); - const confirmLeave = ref(false); - const leaveTo = ref(null); - const title = computed(() => { if (te(`collection_names_singular.${props.collection}`)) { return isNew.value @@ -349,16 +345,6 @@ export default defineComponent({ useShortcut('meta+s', saveAndStay, form); useShortcut('meta+shift+s', saveAndAddNew, form); - const editsGuard: NavigationGuard = (to) => { - if (hasEdits.value) { - confirmLeave.value = true; - leaveTo.value = to.fullPath; - return false; - } - }; - onBeforeRouteUpdate(editsGuard); - onBeforeRouteLeave(editsGuard); - const { deleteAllowed, archiveAllowed, saveAllowed, updateAllowed, shareAllowed, fields, revisionsAllowed } = usePermissions(collection, item, isNew); diff --git a/app/src/modules/files/routes/item.vue b/app/src/modules/files/routes/item.vue index 430d5791fe..1126c7b27d 100644 --- a/app/src/modules/files/routes/item.vue +++ b/app/src/modules/files/routes/item.vue @@ -88,11 +88,11 @@ @@ -180,7 +180,7 @@ import { useI18n } from 'vue-i18n'; import { defineComponent, computed, toRefs, ref, watch, ComponentPublicInstance } from 'vue'; import FilesNavigation from '../components/navigation.vue'; -import { useRouter, onBeforeRouteUpdate, onBeforeRouteLeave, NavigationGuard } from 'vue-router'; +import { useRouter } from 'vue-router'; import RevisionsDrawerDetail from '@/views/private/components/revisions-drawer-detail'; import CommentsSidebarDetail from '@/views/private/components/comments-sidebar-detail'; import useItem from '@/composables/use-item'; @@ -198,7 +198,7 @@ import ReplaceFile from '../components/replace-file.vue'; import { usePermissions } from '@/composables/use-permissions'; import { notify } from '@/utils/notify'; import { unexpectedError } from '@/utils/unexpected-error'; -import unsavedChanges from '@/composables/unsaved-changes'; +import useEditsGuard from '@/composables/use-edits-guard'; export default defineComponent({ name: 'FilesItem', @@ -249,7 +249,9 @@ export default defineComponent({ validationErrors, } = useItem(ref('directus_files'), primaryKey); - unsavedChanges(hasEdits); + const isSavable = computed(() => saveAllowed.value && hasEdits.value); + + const { confirmLeave, leaveTo } = useEditsGuard(isSavable); const confirmDelete = ref(false); const editActive = ref(false); @@ -285,23 +287,10 @@ export default defineComponent({ else return '/files'; }); - const confirmLeave = ref(false); - const leaveTo = ref(null); - const { moveToDialogActive, moveToFolder, moving, selectedFolder } = useMovetoFolder(); useShortcut('meta+s', saveAndStay, form); - const editsGuard: NavigationGuard = (to) => { - if (hasEdits.value) { - confirmLeave.value = true; - leaveTo.value = to.fullPath; - return false; - } - }; - onBeforeRouteUpdate(editsGuard); - onBeforeRouteLeave(editsGuard); - const { deleteAllowed, saveAllowed, updateAllowed, fields, revisionsAllowed } = usePermissions( ref('directus_files'), item, @@ -353,6 +342,7 @@ export default defineComponent({ fieldsFiltered, revisionsAllowed, validationErrors, + isSavable, }; function useBreadcrumb() { 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 083a00d34d..f795808ff9 100644 --- a/app/src/modules/settings/routes/data-model/fields/fields.vue +++ b/app/src/modules/settings/routes/data-model/fields/fields.vue @@ -106,10 +106,10 @@ import { useCollection } from '@directus/shared/composables'; import FieldsManagement from './components/fields-management.vue'; import useItem from '@/composables/use-item'; -import { useRouter, onBeforeRouteUpdate, onBeforeRouteLeave, NavigationGuard } from 'vue-router'; +import { useRouter } from 'vue-router'; import { useCollectionsStore, useFieldsStore } from '@/stores'; import useShortcut from '@/composables/use-shortcut'; -import unsavedChanges from '@/composables/unsaved-changes'; +import useEditsGuard from '@/composables/use-edits-guard'; export default defineComponent({ components: { SettingsNavigation, FieldsManagement }, @@ -160,20 +160,7 @@ export default defineComponent({ 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); + const { confirmLeave, leaveTo } = useEditsGuard(isSavable); return { t, diff --git a/app/src/modules/settings/routes/presets/item.vue b/app/src/modules/settings/routes/presets/item.vue index b9ac763239..a88faac445 100644 --- a/app/src/modules/settings/routes/presets/item.vue +++ b/app/src/modules/settings/routes/presets/item.vue @@ -141,11 +141,11 @@ import { Preset, Filter } from '@directus/shared/types'; import api from '@/api'; import { useCollectionsStore, usePresetsStore } from '@/stores'; import { getLayouts } from '@/layouts'; -import { useRouter, onBeforeRouteUpdate, onBeforeRouteLeave, NavigationGuard } from 'vue-router'; +import { useRouter } from 'vue-router'; import { unexpectedError } from '@/utils/unexpected-error'; import { useLayout } from '@directus/shared/composables'; import useShortcut from '@/composables/use-shortcut'; -import unsavedChanges from '@/composables/unsaved-changes'; +import useEditsGuard from '@/composables/use-edits-guard'; import { isEqual } from 'lodash'; type FormattedPreset = { @@ -210,20 +210,7 @@ export default defineComponent({ 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); + const { confirmLeave, leaveTo } = useEditsGuard(isSavable); return { t, diff --git a/app/src/modules/settings/routes/project/project.vue b/app/src/modules/settings/routes/project/project.vue index a19c1217e6..6373ec30be 100644 --- a/app/src/modules/settings/routes/project/project.vue +++ b/app/src/modules/settings/routes/project/project.vue @@ -49,8 +49,8 @@ import { useSettingsStore, useServerStore } from '@/stores'; import ProjectInfoSidebarDetail from './components/project-info-sidebar-detail.vue'; import { clone } from 'lodash'; import useShortcut from '@/composables/use-shortcut'; -import unsavedChanges from '@/composables/unsaved-changes'; -import { useRouter, onBeforeRouteUpdate, onBeforeRouteLeave, NavigationGuard } from 'vue-router'; +import useEditsGuard from '@/composables/use-edits-guard'; +import { useRouter } from 'vue-router'; export default defineComponent({ components: { SettingsNavigation, ProjectInfoSidebarDetail }, @@ -81,20 +81,7 @@ export default defineComponent({ 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); + const { confirmLeave, leaveTo } = useEditsGuard(isSavable); return { t, diff --git a/app/src/modules/settings/routes/roles/item/item.vue b/app/src/modules/settings/routes/roles/item/item.vue index ab268691b2..c5b95e6b19 100644 --- a/app/src/modules/settings/routes/roles/item/item.vue +++ b/app/src/modules/settings/routes/roles/item/item.vue @@ -107,7 +107,7 @@ import { useI18n } from 'vue-i18n'; import { defineComponent, computed, toRefs, ref } from 'vue'; import SettingsNavigation from '../../../components/navigation.vue'; -import { useRouter, onBeforeRouteUpdate, onBeforeRouteLeave, NavigationGuard } from 'vue-router'; +import { useRouter } from 'vue-router'; import RevisionsDrawerDetail from '@/views/private/components/revisions-drawer-detail'; import useItem from '@/composables/use-item'; import { useUserStore } from '@/stores/'; @@ -115,7 +115,7 @@ 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 useShortcut from '@/composables/use-shortcut'; -import unsavedChanges from '@/composables/unsaved-changes'; +import useEditsGuard from '@/composables/use-edits-guard'; export default defineComponent({ name: 'RolesItem', @@ -173,20 +173,7 @@ export default defineComponent({ 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); + const { confirmLeave, leaveTo } = useEditsGuard(isSavable); return { t, diff --git a/app/src/modules/settings/routes/webhooks/item.vue b/app/src/modules/settings/routes/webhooks/item.vue index e58e53eaf3..0f0d0b8be6 100644 --- a/app/src/modules/settings/routes/webhooks/item.vue +++ b/app/src/modules/settings/routes/webhooks/item.vue @@ -88,12 +88,12 @@ import { useI18n } from 'vue-i18n'; import { defineComponent, computed, toRefs, ref } from 'vue'; import SettingsNavigation from '../../components/navigation.vue'; -import { useRouter, onBeforeRouteUpdate, onBeforeRouteLeave, NavigationGuard } from 'vue-router'; +import { useRouter } 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 useShortcut from '@/composables/use-shortcut'; -import unsavedChanges from '@/composables/unsaved-changes'; +import useEditsGuard from '@/composables/use-edits-guard'; export default defineComponent({ name: 'WebhooksItem', @@ -148,20 +148,7 @@ export default defineComponent({ 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); + const { confirmLeave, leaveTo } = useEditsGuard(isSavable); return { t, diff --git a/app/src/modules/users/routes/item.vue b/app/src/modules/users/routes/item.vue index 2225883025..75e3661d04 100644 --- a/app/src/modules/users/routes/item.vue +++ b/app/src/modules/users/routes/item.vue @@ -74,11 +74,11 @@ @@ -185,7 +185,7 @@ import { defineComponent, computed, toRefs, ref, watch, ComponentPublicInstance import UsersNavigation from '../components/navigation.vue'; import { setLanguage } from '@/lang/set-language'; -import { useRouter, onBeforeRouteUpdate, onBeforeRouteLeave, NavigationGuard } from 'vue-router'; +import { useRouter } from 'vue-router'; import RevisionsDrawerDetail from '@/views/private/components/revisions-drawer-detail'; import CommentsSidebarDetail from '@/views/private/components/comments-sidebar-detail'; import useItem from '@/composables/use-item'; @@ -203,7 +203,7 @@ import { usePermissions } from '@/composables/use-permissions'; import { unexpectedError } from '@/utils/unexpected-error'; import { addTokenToURL } from '@/api'; import { useUserStore } from '@/stores'; -import unsavedChanges from '@/composables/unsaved-changes'; +import useEditsGuard from '@/composables/use-edits-guard'; export default defineComponent({ name: 'UsersItem', @@ -260,7 +260,9 @@ export default defineComponent({ }; } - unsavedChanges(hasEdits); + const isSavable = computed(() => saveAllowed.value && hasEdits.value); + + const { confirmLeave, leaveTo } = useEditsGuard(isSavable); const confirmDelete = ref(false); const confirmArchive = ref(false); @@ -280,19 +282,6 @@ export default defineComponent({ const { loading: previewLoading, avatarSrc, roleName } = useUserPreview(); - 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); - const { deleteAllowed, archiveAllowed, saveAllowed, updateAllowed, revisionsAllowed, fields } = usePermissions( ref('directus_users'), item, @@ -376,6 +365,7 @@ export default defineComponent({ validationErrors, revert, avatarError, + isSavable, }; function useBreadcrumb() {