mirror of
https://github.com/directus/directus.git
synced 2026-04-25 03:00:53 -04:00
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:
45
src/interfaces/collections/collections.vue
Normal file
45
src/interfaces/collections/collections.vue
Normal 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>
|
||||
21
src/interfaces/collections/index.ts
Normal file
21
src/interfaces/collections/index.ts
Normal 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,
|
||||
},
|
||||
],
|
||||
}));
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
:disabled="disabled"
|
||||
:fields="fields"
|
||||
:edits="value"
|
||||
primary-key="+"
|
||||
@input="$emit('input', $event)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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>
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
@@ -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/+`);
|
||||
}
|
||||
|
||||
|
||||
@@ -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[];
|
||||
|
||||
@@ -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 };
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user