Module collections override (#614)

* Fix errors in repeater

* Use custom module listing in sidebar

* Refresh user store on role update

* Add custom module info to type

* Add collections interface

* Add collections interface translations

* Add collections listing types

* Use custom collections listing in collections module nav

* Remove outdated nav test
This commit is contained in:
Rijk van Zanten
2020-05-22 17:04:20 -04:00
committed by GitHub
parent 82d4210ff3
commit 085f6dc581
13 changed files with 220 additions and 157 deletions

View File

@@ -0,0 +1,45 @@
<template>
<v-select :value="value" :disabled="disabled" :items="items" @input="$emit('input', $event)" />
</template>
<script lang="ts">
import { defineComponent, computed } from '@vue/composition-api';
import useCollectionsStore from '@/stores/collections';
export default defineComponent({
props: {
value: {
type: String,
default: null,
},
disabled: {
type: Boolean,
default: false,
},
includeSystem: {
type: Boolean,
default: false,
},
},
setup(props) {
const collectionsStore = useCollectionsStore();
const collections = computed(() => {
if (props.includeSystem) return collectionsStore.state.collections;
return collectionsStore.state.collections.filter(
(collection) => collection.collection.startsWith('directus_') === false
);
});
const items = computed(() => {
return collections.value.map((collection) => ({
text: collection.name,
value: collection.collection,
}));
});
return { items };
},
});
</script>

View File

@@ -0,0 +1,21 @@
import { defineInterface } from '@/interfaces/define';
import InterfaceCollections from './collections.vue';
export default defineInterface(({ i18n }) => ({
id: 'collections',
name: i18n.t('collections'),
icon: 'featured_play_list',
component: InterfaceCollections,
options: [
{
field: 'includeSystem',
name: i18n.t('system'),
width: 'half',
interface: 'toggle',
options: {
label: i18n.t('include_system_collections'),
},
default_value: false,
},
],
}));

View File

@@ -8,7 +8,12 @@
@focus="activate"
>
<template #prepend>
<v-icon @click="activate" :name="value" :class="{ active: value }" />
<v-icon
v-if="value"
@click="activate"
:name="value"
:class="{ active: value }"
/>
</template>
<template #append>

View File

@@ -23,6 +23,7 @@ import InterfaceUser from './user';
import InterfaceTags from './tags';
import InterfaceRepeater from './repeater';
import InterfaceFile from './file';
import InterfaceCollections from './collections';
export const interfaces = [
InterfaceTextInput,
@@ -50,6 +51,7 @@ export const interfaces = [
InterfaceTags,
InterfaceRepeater,
InterfaceFile,
InterfaceCollections,
];
export default interfaces;

View File

@@ -5,6 +5,7 @@
:disabled="disabled"
:fields="fields"
:edits="value"
primary-key="+"
@input="$emit('input', $event)"
/>
</div>

View File

@@ -105,6 +105,9 @@
"submit": "Submit",
"system": "System",
"include_system_collections": "Include System Collections",
"add_field_related": "Add Field to Related Collection",
"create_corresponding_field": "Create Corresponding Field",
"corresponding_field_name": "Corresponding Field Name",

View File

@@ -1,33 +0,0 @@
import CollectionsNavigation from './navigation.vue';
import VueCompositionAPI from '@vue/composition-api';
import { shallowMount, createLocalVue } from '@vue/test-utils';
import * as useNavigation from '../../composables/use-navigation';
import VList, {
VListItem,
VListItemContent,
VListItemIcon,
VListItemTitle,
} from '@/components/v-list';
import VIcon from '@/components/v-icon';
const localVue = createLocalVue();
localVue.use(VueCompositionAPI);
localVue.component('v-list', VList);
localVue.component('v-list-item', VListItem);
localVue.component('v-list-item-content', VListItemContent);
localVue.component('v-list-item-title', VListItemTitle);
localVue.component('v-list-item-icon', VListItemIcon);
localVue.component('v-icon', VIcon);
describe('Modules / Collections / Components / CollectionsNavigation', () => {
it('Uses useNavigation to get navigation links', () => {
jest.spyOn(useNavigation, 'default').mockImplementation(
() =>
({
navItems: [],
} as any)
);
shallowMount(CollectionsNavigation, { localVue });
expect(useNavigation.default).toHaveBeenCalled();
});
});

View File

@@ -1,6 +1,28 @@
<template>
<v-list nav>
<v-list-item :exact="exact" v-for="navItem in navItems" :key="navItem.to" :to="navItem.to">
<template v-if="customNavItems">
<div :key="group.name" v-for="(group, index) in customNavItems">
<div class="group-name">{{ group.name }}</div>
<v-list-item
:exact="exact"
v-for="navItem in group.items"
:key="navItem.to"
:to="navItem.to"
>
<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>
<v-divider v-if="index !== customNavItems.length - 1" />
</div>
</template>
<v-list-item
v-else
:exact="exact"
v-for="navItem in navItems"
:key="navItem.to"
:to="navItem.to"
>
<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>
@@ -32,7 +54,7 @@ export default defineComponent({
setup() {
const collectionPresetsStore = useCollectionPresetsStore();
const projectsStore = useProjectsStore();
const { navItems } = useNavigation();
const { customNavItems, navItems } = useNavigation();
const bookmarks = computed(() => {
const { currentProjectKey } = projectsStore.state;
@@ -51,7 +73,14 @@ export default defineComponent({
});
});
return { navItems, bookmarks };
return { navItems, bookmarks, customNavItems };
},
});
</script>
<style lang="scss" scoped>
.group-name {
padding-left: 8px;
font-weight: 600;
}
</style>

View File

@@ -1,100 +0,0 @@
import mountComposable from '../../../../.jest/mount-composable';
import { useProjectsStore } from '@/stores/projects';
import { useCollectionsStore } from '@/stores/collections';
import useNavigation from './use-navigation';
describe('Modules / Collections / Composables / useNavigation', () => {
afterEach(() => {
useProjectsStore().reset();
useCollectionsStore().reset();
});
it('Converts the visible collections to navigation links', () => {
const projectsStore = useProjectsStore();
const collectionsStore = useCollectionsStore();
projectsStore.state.currentProjectKey = 'my-project';
collectionsStore.state.collections = [
{
collection: 'test',
name: 'Test',
icon: 'box',
note: null,
hidden: false,
managed: true,
single: false,
translation: null,
display_template: null,
},
];
let navItems: any;
mountComposable(() => {
navItems = useNavigation().navItems;
});
expect(navItems.value).toEqual([
{
collection: 'test',
name: 'Test',
to: '/my-project/collections/test',
icon: 'box',
},
]);
});
it('Sorts the collections alphabetically by name', () => {
const projectsStore = useProjectsStore();
const collectionsStore = useCollectionsStore();
projectsStore.state.currentProjectKey = 'my-project';
collectionsStore.state.collections = [
{
collection: 'test',
name: 'B Test',
icon: 'box',
note: null,
hidden: false,
managed: true,
single: false,
translation: null,
display_template: null,
},
{
collection: 'test2',
name: 'A Test',
icon: 'box',
note: null,
hidden: false,
managed: true,
single: false,
translation: null,
display_template: null,
},
{
collection: 'test3',
name: 'C Test',
icon: 'box',
note: null,
hidden: false,
managed: true,
single: false,
translation: null,
display_template: null,
},
];
let navItems: any;
mountComposable(() => {
navItems = useNavigation().navItems;
});
expect(navItems.value[0].name).toBe('A Test');
expect(navItems.value[1].name).toBe('B Test');
expect(navItems.value[2].name).toBe('C Test');
});
});

View File

@@ -3,6 +3,7 @@ import { useProjectsStore } from '@/stores/projects/';
import { useCollectionsStore } from '@/stores/collections/';
import { Collection } from '@/stores/collections/types';
import VueI18n from 'vue-i18n';
import useUserStore from '@/stores/user';
export type NavItem = {
collection: string;
@@ -11,9 +12,44 @@ export type NavItem = {
icon: string;
};
export type NavItemGroup = {
name: string;
items: NavItem[];
};
export default function useNavigation() {
const collectionsStore = useCollectionsStore();
const projectsStore = useProjectsStore();
const userStore = useUserStore();
const customNavItems = computed<NavItemGroup[] | null>(() => {
if (!userStore.state.currentUser) return null;
if (!userStore.state.currentUser.role.collection_listing) return null;
return userStore.state.currentUser?.role.collection_listing.map((groupRaw) => {
const group: NavItemGroup = {
name: groupRaw.group_name,
items: groupRaw.collections
.map(({ collection }) => {
const collectionInfo = collectionsStore.getCollection(collection);
if (!collectionInfo) return null;
const navItem: NavItem = {
collection: collection,
name: collectionInfo.name,
icon: collectionInfo.icon,
to: `/${projectsStore.state.currentProjectKey}/collections/${collection}`,
};
return navItem;
})
.filter((c) => c) as NavItem[],
};
return group;
});
});
const navItems = computed<NavItem[]>(() => {
return collectionsStore.visibleCollections.value
@@ -32,5 +68,5 @@ export default function useNavigation() {
});
});
return { navItems: navItems };
return { customNavItems, navItems };
}

View File

@@ -74,6 +74,7 @@
</div>
<v-form
collection="directus_roles"
:primary-key="primaryKey"
:loading="loading"
:initial-values="item"
:batch-mode="isBatch"
@@ -100,6 +101,7 @@ import RevisionsDrawerDetail from '@/views/private/components/revisions-drawer-d
import useItem from '@/composables/use-item';
import SaveOptions from '@/views/private/components/save-options';
import PermissionsManagement from './components/permissions-management';
import useUserStore from '@/stores/user';
type Values = {
[field: string]: any;
@@ -116,6 +118,8 @@ export default defineComponent({
},
setup(props) {
const projectsStore = useProjectsStore();
const userStore = useUserStore();
const { currentProjectKey } = toRefs(projectsStore.state);
const { primaryKey } = toRefs(props);
@@ -156,17 +160,27 @@ export default defineComponent({
currentProjectKey,
};
/**
* @NOTE
* The userStore contains the information about the role of the current user. We want to
* update the userstore to make sure the role information is accurate with the latest changes
* in case we're changing the current user's role
*/
async function saveAndQuit() {
await save();
await userStore.hydrate();
router.push(`/${currentProjectKey.value}/settings/roles`);
}
async function saveAndStay() {
await save();
await userStore.hydrate();
}
async function saveAndAddNew() {
await save();
await userStore.hydrate();
router.push(`/${currentProjectKey.value}/settings/roles/+`);
}

View File

@@ -2,8 +2,21 @@ export type Role = {
id: number;
name: string;
description: string;
collection_listing: null;
module_listing: null;
collection_listing:
| null
| {
group_name: string;
collections: {
collection: string;
}[];
}[];
module_listing:
| null
| {
link: string;
name: string;
icon: string;
}[];
enforce_2fa: null | boolean;
external_id: null | string;
ip_whitelist: string[];

View File

@@ -27,11 +27,12 @@
</template>
<script lang="ts">
import { defineComponent, Ref } from '@vue/composition-api';
import { defineComponent, Ref, computed } from '@vue/composition-api';
import { useProjectsStore } from '@/stores/projects';
import { modules } from '@/modules/';
import ModuleBarLogo from '../module-bar-logo/';
import ModuleBarAvatar from '../module-bar-avatar/';
import useUserStore from '@/stores/user';
export default defineComponent({
components: {
@@ -40,25 +41,51 @@ export default defineComponent({
},
setup() {
const projectsStore = useProjectsStore();
const userStore = useUserStore();
const { currentProjectKey } = projectsStore.state;
const _modules = modules
.map((module) => ({
...module,
href: module.link || null,
to: module.link === undefined ? `/${currentProjectKey}/${module.id}/` : null,
}))
.filter((module) => {
if (module.hidden !== undefined) {
if (
(module.hidden as boolean) === true ||
(module.hidden as Ref<boolean>).value === true
) {
return false;
const _modules = computed(() => {
const customModuleListing = userStore.state.currentUser?.role.module_listing;
if (
customModuleListing &&
Array.isArray(customModuleListing) &&
customModuleListing.length > 0
) {
return customModuleListing.map((custom) => {
if (custom.link.startsWith('http') || custom.link.startsWith('//')) {
return {
...custom,
href: custom.link,
};
} else {
return {
...custom,
to: custom.link,
};
}
}
return true;
});
});
}
return modules
.map((module) => ({
...module,
href: module.link || null,
to: module.link === undefined ? `/${currentProjectKey}/${module.id}/` : null,
}))
.filter((module) => {
if (module.hidden !== undefined) {
if (
(module.hidden as boolean) === true ||
(module.hidden as Ref<boolean>).value === true
) {
return false;
}
}
return true;
});
});
return { _modules };
},