diff --git a/.changeset/stupid-oranges-listen.md b/.changeset/stupid-oranges-listen.md new file mode 100644 index 0000000000..bc10f0a3de --- /dev/null +++ b/.changeset/stupid-oranges-listen.md @@ -0,0 +1,5 @@ +--- +'@directus/app': minor +--- + +Integrated the Content Version in the browser URL / history to enable browser navigation and page refresh diff --git a/app/package.json b/app/package.json index baa9211db7..186d798ff2 100644 --- a/app/package.json +++ b/app/package.json @@ -98,6 +98,7 @@ "@vitejs/plugin-vue": "5.0.4", "@vue/test-utils": "2.4.4", "@vueuse/core": "10.7.2", + "@vueuse/router": "10.9.0", "apexcharts": "3.46.0", "axios": "1.6.7", "base-64": "1.0.0", diff --git a/app/src/composables/use-edits-guard.ts b/app/src/composables/use-edits-guard.ts index f8a07cb42c..00828a27fa 100644 --- a/app/src/composables/use-edits-guard.ts +++ b/app/src/composables/use-edits-guard.ts @@ -1,9 +1,11 @@ -import { ref, unref, type Ref } from 'vue'; -import { useRoute } from 'vue-router'; +import { isEqual } from 'lodash'; +import { MaybeRef, Ref, ref, unref } from 'vue'; +import { LocationQuery, useRoute } from 'vue-router'; import { useNavigationGuard } from './use-navigation-guard'; type EditsGuardOptions = { - ignorePrefix?: string | Ref; + ignorePrefix?: MaybeRef; + compareQuery?: MaybeRef; }; export function useEditsGuard(hasEdits: Ref, opts?: EditsGuardOptions) { @@ -11,9 +13,12 @@ export function useEditsGuard(hasEdits: Ref, opts?: EditsGuardOptions) const leaveTo = ref(null); useNavigationGuard(hasEdits, (to) => { - const { path } = useRoute(); + const { path, query } = useRoute(); - if (hasEdits.value && !isSubpath(path, to.path) && !isIgnoredPath(unref(opts?.ignorePrefix), to.path)) { + if ( + hasEdits.value && + ((!isSubpath(path, to.path) && !isIgnoredPath(to.path)) || hasChangedQuery(query, to.query)) + ) { confirmLeave.value = true; leaveTo.value = to.fullPath; return false; @@ -23,16 +28,31 @@ export function useEditsGuard(hasEdits: Ref, opts?: EditsGuardOptions) }); return { confirmLeave, leaveTo }; -} -function isSubpath(currentPath: string, newPath: string) { - return ( - currentPath === newPath || (newPath.startsWith(currentPath) && newPath.substring(currentPath.length).includes('/')) - ); -} + function isSubpath(currentPath: string, newPath: string) { + return ( + currentPath === newPath || + (newPath.startsWith(currentPath) && newPath.substring(currentPath.length).includes('/')) + ); + } -function isIgnoredPath(ignorePrefix: string | undefined, newPath: string) { - if (!ignorePrefix) return false; + function isIgnoredPath(newPath: string) { + const ignorePrefix = unref(opts?.ignorePrefix); - return newPath.startsWith(ignorePrefix); + if (!ignorePrefix) return false; + + return newPath.startsWith(ignorePrefix); + } + + function hasChangedQuery(currentQuery: LocationQuery, newQuery: LocationQuery) { + const compareQuery = unref(opts?.compareQuery); + + if (!compareQuery) return false; + + for (const query of compareQuery) { + if (!isEqual(currentQuery[query], newQuery[query])) return true; + } + + return false; + } } diff --git a/app/src/composables/use-versions.ts b/app/src/composables/use-versions.ts index 2253600232..9ff643f9c6 100644 --- a/app/src/composables/use-versions.ts +++ b/app/src/composables/use-versions.ts @@ -1,17 +1,45 @@ import api from '@/api'; import { unexpectedError } from '@/utils/unexpected-error'; import { ContentVersion, Filter, Query } from '@directus/types'; +import { useRouteQuery } from '@vueuse/router'; import { Ref, computed, ref, unref, watch } from 'vue'; import { useCollectionPermissions } from './use-permissions'; export function useVersions(collection: Ref, isSingleton: Ref, primaryKey: Ref) { - const { readAllowed: readVersionsAllowed } = useCollectionPermissions('directus_versions'); - const currentVersion = ref(null); const versions = ref(null); const loading = ref(false); const saveVersionLoading = ref(false); + const { readAllowed: readVersionsAllowed } = useCollectionPermissions('directus_versions'); + + const queryVersion = useRouteQuery('version', null, { + transform: (value) => (Array.isArray(value) ? value[0] : value), + mode: 'push', + }); + + watch( + [queryVersion, versions], + ([newQueryVersion, newVersions]) => { + if (!newVersions) return; + + let version; + + if (queryVersion) { + version = newVersions.find((version) => version.key === newQueryVersion); + } + + if (version?.key === currentVersion.value?.key) return; + + currentVersion.value = version ?? null; + }, + { immediate: true }, + ); + + watch(currentVersion, (newCurrentVersion) => { + queryVersion.value = newCurrentVersion?.key ?? null; + }); + const query = computed(() => { if (!currentVersion.value) return {}; @@ -22,7 +50,7 @@ export function useVersions(collection: Ref, isSingleton: Ref, watch( [collection, isSingleton, primaryKey], - ([newCollection, _newIsSingleton, _newPrimaryKey], [oldCollection, _oldIsSingleton, _oldPrimaryKey]) => { + ([newCollection], [oldCollection]) => { if (newCollection !== oldCollection) currentVersion.value = null; getVersions(); }, diff --git a/app/src/modules/content/routes/item.vue b/app/src/modules/content/routes/item.vue index 69a0cb1388..82772846d1 100644 --- a/app/src/modules/content/routes/item.vue +++ b/app/src/modules/content/routes/item.vue @@ -89,7 +89,7 @@ const { const { templateData } = useTemplateData(collectionInfo, primaryKey); -const { confirmLeave, leaveTo } = useEditsGuard(hasEdits); +const { confirmLeave, leaveTo } = useEditsGuard(hasEdits, { compareQuery: ['version'] }); const confirmDelete = ref(false); const confirmArchive = ref(false); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 869fd64680..75a1580001 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -721,6 +721,9 @@ importers: '@vueuse/core': specifier: 10.7.2 version: 10.7.2(vue@3.4.19) + '@vueuse/router': + specifier: 10.9.0 + version: 10.9.0(vue-router@4.3.0)(vue@3.4.19) apexcharts: specifier: 3.46.0 version: 3.46.0 @@ -961,7 +964,7 @@ importers: version: 5.3.3 vite: specifier: 5.1.1 - version: 5.1.1(sass@1.71.1) + version: 5.1.1(@types/node@18.19.17) vite-plugin-dts: specifier: 3.7.3 version: 3.7.3(rollup@4.10.0)(typescript@5.3.3)(vite@5.1.1) @@ -1890,7 +1893,7 @@ importers: version: 5.3.3 vite: specifier: 5.1.1 - version: 5.1.1(sass@1.71.1) + version: 5.1.1(@types/node@18.19.17) vite-plugin-dts: specifier: 3.7.3 version: 3.7.3(rollup@4.10.0)(typescript@5.3.3)(vite@5.1.1) @@ -8761,6 +8764,19 @@ packages: /@vueuse/metadata@10.7.2: resolution: {integrity: sha512-kCWPb4J2KGrwLtn1eJwaJD742u1k5h6v/St5wFe8Quih90+k2a0JP8BS4Zp34XUuJqS2AxFYMb1wjUL8HfhWsQ==} + /@vueuse/router@10.9.0(vue-router@4.3.0)(vue@3.4.19): + resolution: {integrity: sha512-MOmrCMQlRuPS4PExE1hy8T0XbZUXaNbEuh7CAG5mC8kdvdgANQMkdvJ7vIEOP27n5mXK/4YjvXJOZSsur4E0QQ==} + peerDependencies: + vue-router: '>=4.0.0-rc.1' + dependencies: + '@vueuse/shared': 10.9.0(vue@3.4.19) + vue-demi: 0.14.7(vue@3.4.19) + vue-router: 4.3.0(vue@3.4.19) + transitivePeerDependencies: + - '@vue/composition-api' + - vue + dev: true + /@vueuse/shared@10.7.0(vue@3.3.8): resolution: {integrity: sha512-kc00uV6CiaTdc3i1CDC4a3lBxzaBE9AgYNtFN87B5OOscqeWElj/uza8qVDmk7/U8JbqoONLbtqiLJ5LGRuqlw==} dependencies: @@ -8787,6 +8803,15 @@ packages: - '@vue/composition-api' - vue + /@vueuse/shared@10.9.0(vue@3.4.19): + resolution: {integrity: sha512-Uud2IWncmAfJvRaFYzv5OHDli+FbOzxiVEQdLCKQKLyhz94PIyFC3CHcH7EDMwIn8NPtD06+PNbC/PiO0LGLtw==} + dependencies: + vue-demi: 0.14.7(vue@3.4.19) + transitivePeerDependencies: + - '@vue/composition-api' + - vue + dev: true + /@xmldom/xmldom@0.8.10: resolution: {integrity: sha512-2WALfTl4xo2SkGCYRt6rDTFfk9R1czmBvUQy12gK2KuRKIpWEhcbbzy8EZXtz/jkRqHX8bFEc6FC1HjX4TUWYw==} engines: {node: '>=10.0.0'}