mirror of
https://github.com/directus/directus.git
synced 2026-01-29 14:48:02 -05:00
Support singleton on the browse level (#781)
* Support singleton on the browse level * Remove broken test
This commit is contained in:
@@ -1,79 +0,0 @@
|
||||
import { useCollection } from './use-collection';
|
||||
import Vue from 'vue';
|
||||
import VueCompositionAPI, { ref } from '@vue/composition-api';
|
||||
import useCollectionsStore from '@/stores/collections/';
|
||||
import useFieldsStore from '@/stores/fields/';
|
||||
|
||||
describe('Composables / useCollection', () => {
|
||||
let req: any = {};
|
||||
|
||||
beforeAll(() => {
|
||||
Vue.use(VueCompositionAPI);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
req = {};
|
||||
});
|
||||
|
||||
it('Gets the collection info from the collections store', () => {
|
||||
useCollectionsStore(req).state.collections = [
|
||||
{
|
||||
collection: 'files',
|
||||
test: true,
|
||||
},
|
||||
{
|
||||
collection: 'another-collection',
|
||||
test: false,
|
||||
},
|
||||
] as any;
|
||||
useFieldsStore(req).state.fields = [
|
||||
{
|
||||
collection: 'files',
|
||||
field: 'id',
|
||||
primary_key: true,
|
||||
test: true,
|
||||
},
|
||||
{
|
||||
collection: 'another-collection',
|
||||
field: 'id',
|
||||
primary_key: true,
|
||||
test: false,
|
||||
},
|
||||
{
|
||||
collection: 'files',
|
||||
field: 'title',
|
||||
primary_key: false,
|
||||
test: true,
|
||||
},
|
||||
] as any;
|
||||
|
||||
const { info, fields, primaryKeyField } = useCollection(ref('files'));
|
||||
|
||||
expect(info.value).toEqual({
|
||||
collection: 'files',
|
||||
test: true,
|
||||
});
|
||||
|
||||
expect(fields.value).toEqual([
|
||||
{
|
||||
collection: 'files',
|
||||
field: 'id',
|
||||
primary_key: true,
|
||||
test: true,
|
||||
},
|
||||
{
|
||||
collection: 'files',
|
||||
field: 'title',
|
||||
primary_key: false,
|
||||
test: true,
|
||||
},
|
||||
]);
|
||||
|
||||
expect(primaryKeyField.value).toEqual({
|
||||
collection: 'files',
|
||||
field: 'id',
|
||||
primary_key: true,
|
||||
test: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -5,8 +5,8 @@ import i18n from '@/lang';
|
||||
import useCollection from '@/composables/use-collection';
|
||||
import { AxiosResponse } from 'axios';
|
||||
|
||||
export function useItem(collection: Ref<string>, primaryKey: Ref<string | number>) {
|
||||
const { primaryKeyField, softDeleteStatus, statusField } = useCollection(collection);
|
||||
export function useItem(collection: Ref<string>, primaryKey: Ref<string | number | null>) {
|
||||
const { info: collectionInfo, primaryKeyField, softDeleteStatus, statusField } = useCollection(collection);
|
||||
|
||||
const item = ref<any>(null);
|
||||
const error = ref(null);
|
||||
@@ -17,6 +17,7 @@ export function useItem(collection: Ref<string>, primaryKey: Ref<string | number
|
||||
const edits = ref({});
|
||||
const isNew = computed(() => primaryKey.value === '+');
|
||||
const isBatch = computed(() => typeof primaryKey.value === 'string' && primaryKey.value.includes(','));
|
||||
const isSingle = computed(() => !!collectionInfo.value?.single);
|
||||
|
||||
const endpoint = computed(() => {
|
||||
return collection.value.startsWith('directus_')
|
||||
@@ -24,6 +25,14 @@ export function useItem(collection: Ref<string>, primaryKey: Ref<string | number
|
||||
: `/items/${collection.value}`;
|
||||
});
|
||||
|
||||
const itemEndpoint = computed(() => {
|
||||
if (isSingle.value) {
|
||||
return endpoint.value;
|
||||
}
|
||||
|
||||
return `${endpoint.value}/${primaryKey.value}`;
|
||||
});
|
||||
|
||||
watch([collection, primaryKey], refresh, { immediate: true });
|
||||
|
||||
return {
|
||||
@@ -47,8 +56,7 @@ export function useItem(collection: Ref<string>, primaryKey: Ref<string | number
|
||||
loading.value = true;
|
||||
|
||||
try {
|
||||
const response = await api.get(`${endpoint.value}/${primaryKey.value}`);
|
||||
|
||||
const response = await api.get(itemEndpoint.value);
|
||||
setItemValueToResponse(response);
|
||||
} catch (err) {
|
||||
error.value = err;
|
||||
@@ -77,7 +85,7 @@ export function useItem(collection: Ref<string>, primaryKey: Ref<string | number
|
||||
type: 'success',
|
||||
});
|
||||
} else {
|
||||
response = await api.patch(`${endpoint.value}/${primaryKey.value}`, edits.value);
|
||||
response = await api.patch(itemEndpoint.value, edits.value);
|
||||
|
||||
notify({
|
||||
title: i18n.tc('item_update_success', isBatch.value ? 2 : 1),
|
||||
@@ -180,11 +188,11 @@ export function useItem(collection: Ref<string>, primaryKey: Ref<string | number
|
||||
throw new Error('[useItem] You cant soft-delete without a status field');
|
||||
}
|
||||
|
||||
await api.patch(`${endpoint.value}/${primaryKey.value}`, {
|
||||
await api.patch(itemEndpoint.value, {
|
||||
[statusField.value.field]: softDeleteStatus.value,
|
||||
});
|
||||
} else {
|
||||
await api.delete(`${endpoint.value}/${primaryKey.value}`);
|
||||
await api.delete(itemEndpoint.value);
|
||||
}
|
||||
|
||||
item.value = null;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { defineModule } from '@/modules/define';
|
||||
import CollectionsOverview from './routes/overview/';
|
||||
import CollectionsBrowse from './routes/browse/';
|
||||
import CollectionsBrowseOrDetail from './routes/browse-or-detail/';
|
||||
import CollectionsDetail from './routes/detail/';
|
||||
import CollectionsItemNotFound from './routes/not-found';
|
||||
|
||||
@@ -17,7 +17,7 @@ export default defineModule(({ i18n }) => ({
|
||||
{
|
||||
name: 'collections-browse',
|
||||
path: '/:collection',
|
||||
component: CollectionsBrowse,
|
||||
component: CollectionsBrowseOrDetail,
|
||||
props: (route) => ({
|
||||
collection: route.params.collection,
|
||||
bookmark: route.query.bookmark,
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
<template>
|
||||
<component ref="component" :is="isSingle ? 'collections-detail' : 'collections-browse'" :collection="collection" />
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref, computed } from '@vue/composition-api';
|
||||
import Vue from 'vue';
|
||||
import CollectionsBrowse from '../browse';
|
||||
import CollectionsDetail from '../detail';
|
||||
import useCollectionsStore from '@/stores/collections';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
CollectionsBrowse,
|
||||
CollectionsDetail,
|
||||
},
|
||||
props: {
|
||||
collection: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const collectionsStore = useCollectionsStore();
|
||||
const component = ref<Vue>();
|
||||
|
||||
const isSingle = computed(() => {
|
||||
const collectionInfo = collectionsStore.getCollection(props.collection);
|
||||
return !!collectionInfo?.single === true;
|
||||
});
|
||||
|
||||
return { component, isSingle };
|
||||
},
|
||||
beforeRouteLeave(to, from, next) {
|
||||
if ((this as any).$refs?.component?.navigationGuard) {
|
||||
return (this as any).$refs.component.navigationGuard(to, from, next);
|
||||
}
|
||||
|
||||
return next();
|
||||
},
|
||||
beforeRouteUpdate(to, from, next) {
|
||||
if ((this as any).$refs?.component?.navigationGuard) {
|
||||
return (this as any).$refs.component.navigationGuard(to, from, next);
|
||||
}
|
||||
|
||||
return next();
|
||||
},
|
||||
});
|
||||
</script>
|
||||
4
src/modules/collections/routes/browse-or-detail/index.ts
Normal file
4
src/modules/collections/routes/browse-or-detail/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import CollectionsBrowseOrDetail from './browse-or-detail.vue';
|
||||
|
||||
export { CollectionsBrowseOrDetail };
|
||||
export default CollectionsBrowseOrDetail;
|
||||
@@ -0,0 +1,3 @@
|
||||
# Browse or Detail
|
||||
|
||||
Renders either the browse page or the detail page depending on whether or not the collection is a singleton
|
||||
@@ -133,10 +133,7 @@
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, computed, ref, watch, toRefs } from '@vue/composition-api';
|
||||
import { NavigationGuard } from 'vue-router';
|
||||
import CollectionsNavigation from '../../components/navigation/';
|
||||
import useCollectionsStore from '@/stores/collections';
|
||||
import useFieldsStore from '@/stores/fields';
|
||||
import api from '@/api';
|
||||
import { LayoutComponent } from '@/layouts/types';
|
||||
import CollectionsNotFound from '../not-found/';
|
||||
@@ -149,40 +146,11 @@ import BookmarkEdit from '@/views/private/components/bookmark-edit';
|
||||
import router from '@/router';
|
||||
import marked from 'marked';
|
||||
|
||||
const redirectIfNeeded: NavigationGuard = async (to, from, next) => {
|
||||
const collectionsStore = useCollectionsStore();
|
||||
const collectionInfo = collectionsStore.getCollection(to.params.collection);
|
||||
|
||||
if (collectionInfo === null) return next();
|
||||
|
||||
if (collectionInfo.single === true) {
|
||||
const fieldsStore = useFieldsStore();
|
||||
|
||||
const primaryKeyField = fieldsStore.getPrimaryKeyFieldForCollection(to.params.collection);
|
||||
|
||||
const item = await api.get(`/items/${to.params.collection}`, {
|
||||
params: {
|
||||
limit: 1,
|
||||
fields: primaryKeyField.field,
|
||||
single: true,
|
||||
},
|
||||
});
|
||||
|
||||
const primaryKey = item.data.data[primaryKeyField.field];
|
||||
|
||||
return next(`/collections/${to.params.collection}/${primaryKey}`);
|
||||
}
|
||||
|
||||
return next();
|
||||
};
|
||||
|
||||
type Item = {
|
||||
[field: string]: any;
|
||||
};
|
||||
|
||||
export default defineComponent({
|
||||
beforeRouteEnter: redirectIfNeeded,
|
||||
beforeRouteUpdate: redirectIfNeeded,
|
||||
name: 'collections-browse',
|
||||
components: {
|
||||
CollectionsNavigation,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<collections-not-found v-if="error && error.code === 404" />
|
||||
<collections-not-found v-if="error || (collectionInfo.single === true && primaryKey !== null)" />
|
||||
<private-view v-else :title="title">
|
||||
<template #title v-if="isNew === false && isBatch === false && collectionInfo.display_template">
|
||||
<v-skeleton-loader class="title-loader" type="text" v-if="loading" />
|
||||
@@ -178,6 +178,7 @@ import SaveOptions from '@/views/private/components/save-options';
|
||||
import i18n from '@/lang';
|
||||
import marked from 'marked';
|
||||
import useShortcut from '@/composables/use-shortcut';
|
||||
import { NavigationGuard } from 'vue-router';
|
||||
|
||||
type Values = {
|
||||
[field: string]: any;
|
||||
@@ -185,18 +186,6 @@ type Values = {
|
||||
|
||||
export default defineComponent({
|
||||
name: 'collections-detail',
|
||||
beforeRouteLeave(to, from, next) {
|
||||
const self = this as any;
|
||||
const hasEdits = Object.keys(self.edits).length > 0;
|
||||
|
||||
if (hasEdits) {
|
||||
self.confirmLeave = true;
|
||||
self.leaveTo = to.fullPath;
|
||||
return next(false);
|
||||
}
|
||||
|
||||
return next();
|
||||
},
|
||||
components: {
|
||||
CollectionsNavigation,
|
||||
CollectionsNotFound,
|
||||
@@ -211,7 +200,7 @@ export default defineComponent({
|
||||
},
|
||||
primaryKey: {
|
||||
type: String,
|
||||
required: true,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
@@ -269,6 +258,18 @@ export default defineComponent({
|
||||
useShortcut('mod+s', saveAndStay);
|
||||
useShortcut('mod+shift+s', saveAndAddNew);
|
||||
|
||||
const navigationGuard: NavigationGuard = (to, from, next) => {
|
||||
const hasEdits = Object.keys(edits.value).length > 0;
|
||||
|
||||
if (hasEdits) {
|
||||
confirmLeave.value = true;
|
||||
leaveTo.value = to.fullPath;
|
||||
return next(false);
|
||||
}
|
||||
|
||||
return next();
|
||||
};
|
||||
|
||||
return {
|
||||
item,
|
||||
loading,
|
||||
@@ -299,6 +300,7 @@ export default defineComponent({
|
||||
confirmLeave,
|
||||
leaveTo,
|
||||
discardAndLeave,
|
||||
navigationGuard,
|
||||
};
|
||||
|
||||
function useBreadcrumb() {
|
||||
|
||||
@@ -14,5 +14,9 @@ export const useLatencyStore = createStore({
|
||||
async dehydrate() {
|
||||
this.reset();
|
||||
},
|
||||
save(latency: Latency) {
|
||||
this.state.latency.push(latency);
|
||||
this.state.latency = this.state.latency.slice(-20);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -53,7 +53,7 @@ export const useUserStore = createStore({
|
||||
|
||||
const end = performance.now();
|
||||
|
||||
latencyStore.state.latency.push({
|
||||
latencyStore.save({
|
||||
timestamp: new Date(),
|
||||
latency: end - start,
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user