mirror of
https://github.com/directus/directus.git
synced 2026-04-25 03:00:53 -04:00
Bookmarks (#494)
* Add support for bookmarks in collection preset composition * Add strings for bookmark 404 * Use bookmarks prop in collections browse * Use exact for active link in navs * Show bookmarks in navigation * Make bookmark optional in collection preset
This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
:is="component"
|
||||
active-class="active"
|
||||
class="v-list-item"
|
||||
exact
|
||||
:to="to"
|
||||
:class="{
|
||||
active,
|
||||
|
||||
@@ -4,3 +4,18 @@ export type Filter = {
|
||||
operator: string;
|
||||
value: string | number;
|
||||
};
|
||||
|
||||
export type CollectionPreset = {
|
||||
id: number;
|
||||
collection: string;
|
||||
filters: null | Filter[];
|
||||
role: number | null;
|
||||
search_query: string | null;
|
||||
title: string | null;
|
||||
user: number | null;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
view_options: Record<string, any>;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
view_query: Record<string, any>;
|
||||
view_type: string | null;
|
||||
};
|
||||
|
||||
@@ -2,32 +2,44 @@ import useCollectionPresetStore from '@/stores/collection-presets';
|
||||
import { ref, Ref, computed, watch } from '@vue/composition-api';
|
||||
import { debounce } from 'lodash';
|
||||
|
||||
import { Filter } from './types';
|
||||
import { Filter, CollectionPreset } from './types';
|
||||
|
||||
export function useCollectionPreset(collection: Ref<string>) {
|
||||
export function useCollectionPreset(
|
||||
collection: Ref<string>,
|
||||
bookmark: Ref<number | null> = ref(null)
|
||||
) {
|
||||
const collectionPresetsStore = useCollectionPresetStore();
|
||||
|
||||
const localPreset = ref({
|
||||
...collectionPresetsStore.getPresetForCollection(collection.value),
|
||||
});
|
||||
const bookmarkExists = computed(() => {
|
||||
if (!bookmark.value) return false;
|
||||
|
||||
const savePreset = debounce(async (preset) => {
|
||||
await collectionPresetsStore.savePreset(preset);
|
||||
localPreset.value = collectionPresetsStore.getPresetForCollection(collection.value);
|
||||
return !!collectionPresetsStore.state.collectionPresets.find(
|
||||
(preset) => preset.id === bookmark.value
|
||||
);
|
||||
});
|
||||
const localPreset = ref<CollectionPreset>({});
|
||||
initLocalPreset();
|
||||
|
||||
const savePreset = async () => await collectionPresetsStore.savePreset(localPreset.value);
|
||||
|
||||
const autoSave = debounce(async () => {
|
||||
if (!bookmark || bookmark.value === null) {
|
||||
savePreset();
|
||||
}
|
||||
}, 450);
|
||||
|
||||
watch(collection, () => {
|
||||
localPreset.value = {
|
||||
...collectionPresetsStore.getPresetForCollection(collection.value),
|
||||
};
|
||||
});
|
||||
watch(collection, initLocalPreset);
|
||||
watch(bookmark, initLocalPreset);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const viewOptions = computed<Record<string, any>>({
|
||||
get() {
|
||||
if (!localPreset.value.view_type) return null;
|
||||
return localPreset.value.view_options?.[localPreset.value.view_type] || null;
|
||||
},
|
||||
set(val) {
|
||||
if (!localPreset.value.view_type) return null;
|
||||
|
||||
localPreset.value = {
|
||||
...localPreset.value,
|
||||
view_options: {
|
||||
@@ -35,16 +47,19 @@ export function useCollectionPreset(collection: Ref<string>) {
|
||||
[localPreset.value.view_type]: val,
|
||||
},
|
||||
};
|
||||
savePreset(localPreset.value);
|
||||
|
||||
autoSave();
|
||||
},
|
||||
});
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const viewQuery = computed<Record<string, any>>({
|
||||
get() {
|
||||
if (!localPreset.value.view_type) return null;
|
||||
return localPreset.value.view_query?.[localPreset.value.view_type] || null;
|
||||
},
|
||||
set(val) {
|
||||
if (!localPreset.value.view_type) return null;
|
||||
localPreset.value = {
|
||||
...localPreset.value,
|
||||
view_query: {
|
||||
@@ -52,11 +67,12 @@ export function useCollectionPreset(collection: Ref<string>) {
|
||||
[localPreset.value.view_type]: val,
|
||||
},
|
||||
};
|
||||
savePreset(localPreset.value);
|
||||
|
||||
autoSave();
|
||||
},
|
||||
});
|
||||
|
||||
const viewType = computed<string>({
|
||||
const viewType = computed<string | null>({
|
||||
get() {
|
||||
return localPreset.value.view_type || null;
|
||||
},
|
||||
@@ -65,7 +81,8 @@ export function useCollectionPreset(collection: Ref<string>) {
|
||||
...localPreset.value,
|
||||
view_type: val,
|
||||
};
|
||||
savePreset(localPreset.value);
|
||||
|
||||
autoSave();
|
||||
},
|
||||
});
|
||||
|
||||
@@ -78,11 +95,12 @@ export function useCollectionPreset(collection: Ref<string>) {
|
||||
...localPreset.value,
|
||||
filters: val,
|
||||
};
|
||||
savePreset(localPreset.value);
|
||||
|
||||
autoSave();
|
||||
},
|
||||
});
|
||||
|
||||
const searchQuery = computed<Filter[]>({
|
||||
const searchQuery = computed<string | null>({
|
||||
get() {
|
||||
return localPreset.value.search_query || null;
|
||||
},
|
||||
@@ -91,9 +109,24 @@ export function useCollectionPreset(collection: Ref<string>) {
|
||||
...localPreset.value,
|
||||
search_query: val,
|
||||
};
|
||||
savePreset(localPreset.value);
|
||||
|
||||
autoSave();
|
||||
},
|
||||
});
|
||||
|
||||
return { viewType, viewOptions, viewQuery, filters, searchQuery };
|
||||
return { bookmarkExists, viewType, viewOptions, viewQuery, filters, searchQuery, savePreset };
|
||||
|
||||
function initLocalPreset() {
|
||||
if (bookmark.value === null) {
|
||||
localPreset.value = {
|
||||
...collectionPresetsStore.getPresetForCollection(collection.value),
|
||||
};
|
||||
} else {
|
||||
if (bookmarkExists.value === false) return;
|
||||
|
||||
localPreset.value = {
|
||||
...collectionPresetsStore.getBookmark(+bookmark.value),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -146,6 +146,10 @@
|
||||
|
||||
"toggle_manual_sorting": "Toggle Manual Sorting",
|
||||
|
||||
"bookmark_doesnt_exist": "Bookmark Doesn't Exist",
|
||||
"bookmark_doesnt_exist_copy": "The bookmark you're trying to open couldn't be found.",
|
||||
"bookmark_doesnt_exist_cta": "Return to collection",
|
||||
|
||||
"errors": {
|
||||
"3": "Only super admins have access to this",
|
||||
"4": "Super Admin Token not provided",
|
||||
@@ -802,7 +806,6 @@
|
||||
"update_confirm": "Are you sure you want to update {count} items?",
|
||||
"update_field": "Update Field",
|
||||
"upload_exceeds_max_size": "{filename} can't be uploaded. Your server is not configured to handle uploads of this size.",
|
||||
"user_directory": "User Directory",
|
||||
"user_edit_warning": "{first_name} {last_name} is editing this item too. Please coordinate with them so you don't lose your changes.",
|
||||
"validation": "Validation",
|
||||
"value": "Value",
|
||||
|
||||
@@ -4,19 +4,49 @@
|
||||
<v-list-item-icon><v-icon :name="navItem.icon" /></v-list-item-icon>
|
||||
<v-list-item-content>{{ navItem.name }}</v-list-item-content>
|
||||
</v-list-item>
|
||||
|
||||
<template v-if="bookmarks.length > 0">
|
||||
<v-divider />
|
||||
|
||||
<v-list-item v-for="bookmark in bookmarks" :key="bookmark.id" :to="bookmark.to">
|
||||
<v-list-item-icon><v-icon name="bookmark" /></v-list-item-icon>
|
||||
<v-list-item-content>{{ bookmark.title }}</v-list-item-content>
|
||||
</v-list-item>
|
||||
</template>
|
||||
</v-list>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from '@vue/composition-api';
|
||||
import { defineComponent, computed } from '@vue/composition-api';
|
||||
import useNavigation from '../../compositions/use-navigation';
|
||||
import useCollectionPresetsStore from '@/stores/collection-presets';
|
||||
import useProjectsStore from '@/stores/projects';
|
||||
|
||||
export default defineComponent({
|
||||
props: {},
|
||||
setup() {
|
||||
const collectionPresetsStore = useCollectionPresetsStore();
|
||||
const projectsStore = useProjectsStore();
|
||||
const { navItems } = useNavigation();
|
||||
|
||||
return { navItems };
|
||||
const bookmarks = computed(() => {
|
||||
const { currentProjectKey } = projectsStore.state;
|
||||
|
||||
return collectionPresetsStore.state.collectionPresets
|
||||
.filter((preset) => {
|
||||
return (
|
||||
preset.title !== null && preset.collection.startsWith('directus_') === false
|
||||
);
|
||||
})
|
||||
.map((preset) => {
|
||||
return {
|
||||
...preset,
|
||||
to: `/${currentProjectKey}/collections/${preset.collection}?bookmark=${preset.id}`,
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
return { navItems, bookmarks };
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -18,7 +18,10 @@ export default defineModule(({ i18n }) => ({
|
||||
name: 'collections-browse',
|
||||
path: '/:collection',
|
||||
component: CollectionsBrowse,
|
||||
props: true,
|
||||
props: (route) => ({
|
||||
collection: route.params.collection,
|
||||
bookmark: route.query.bookmark,
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: 'collections-detail',
|
||||
|
||||
@@ -53,7 +53,23 @@
|
||||
<collections-navigation />
|
||||
</template>
|
||||
|
||||
<v-info
|
||||
type="warning"
|
||||
v-if="bookmark && bookmarkExists === false"
|
||||
:title="$t('bookmark_doesnt_exist')"
|
||||
icon="bookmark"
|
||||
>
|
||||
{{ $t('bookmark_doesnt_exist_copy') }}
|
||||
|
||||
<template #append>
|
||||
<v-button :to="currentCollectionLink">
|
||||
{{ $t('bookmark_doesnt_exist_cta') }}
|
||||
</v-button>
|
||||
</template>
|
||||
</v-info>
|
||||
|
||||
<component
|
||||
v-else
|
||||
class="layout"
|
||||
ref="layout"
|
||||
:is="`layout-${viewType}`"
|
||||
@@ -129,20 +145,31 @@ export default defineComponent({
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
bookmark: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const layout = ref<LayoutComponent>(null);
|
||||
|
||||
const { collection } = toRefs(props);
|
||||
const bookmarkID = computed(() => (props.bookmark ? +props.bookmark : null));
|
||||
|
||||
const projectsStore = useProjectsStore();
|
||||
|
||||
const { selection } = useSelection();
|
||||
const { info: currentCollection, primaryKeyField } = useCollection(collection);
|
||||
const { addNewLink, batchLink, collectionsLink } = useLinks();
|
||||
const { viewType, viewOptions, viewQuery, filters, searchQuery } = useCollectionPreset(
|
||||
collection
|
||||
);
|
||||
const { addNewLink, batchLink, collectionsLink, currentCollectionLink } = useLinks();
|
||||
const {
|
||||
viewType,
|
||||
viewOptions,
|
||||
viewQuery,
|
||||
filters,
|
||||
searchQuery,
|
||||
savePreset,
|
||||
bookmarkExists,
|
||||
} = useCollectionPreset(collection, bookmarkID);
|
||||
const { confirmDelete, deleting, batchDelete } = useBatchDelete();
|
||||
|
||||
if (viewType.value === null) {
|
||||
@@ -164,6 +191,9 @@ export default defineComponent({
|
||||
viewQuery,
|
||||
viewType,
|
||||
searchQuery,
|
||||
savePreset,
|
||||
bookmarkExists,
|
||||
currentCollectionLink,
|
||||
};
|
||||
|
||||
function useSelection() {
|
||||
@@ -229,7 +259,13 @@ export default defineComponent({
|
||||
return `/${currentProjectKey}/collections`;
|
||||
});
|
||||
|
||||
return { addNewLink, batchLink, collectionsLink };
|
||||
const currentCollectionLink = computed<string>(() => {
|
||||
const currentProjectKey = projectsStore.state.currentProjectKey;
|
||||
|
||||
return `/${currentProjectKey}/collections/${props.collection}`;
|
||||
});
|
||||
|
||||
return { addNewLink, batchLink, collectionsLink, currentCollectionLink };
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -259,4 +295,8 @@ export default defineComponent({
|
||||
.layout {
|
||||
--layout-offset-top: 64px;
|
||||
}
|
||||
|
||||
.v-info {
|
||||
margin: 20vh 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -127,6 +127,10 @@ export const useCollectionPresetsStore = createStore({
|
||||
return collectionPreset;
|
||||
},
|
||||
|
||||
getBookmark(bookmarkID: number) {
|
||||
return this.state.collectionPresets.find((preset) => preset.id === bookmarkID) || null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Saves the given preset. If it's the default preset, it saves it as a new preset. If the
|
||||
* preset already exists, but doesn't have a user associated, it will create a preset for
|
||||
|
||||
Reference in New Issue
Block a user