mirror of
https://github.com/directus/directus.git
synced 2026-04-03 03:00:39 -04:00
Add fields store (#144)
* Add fields store * Add test coverage for fields store * Remove hydration tests It doesn't do anything itself, but just calls init / reset methods of stores * Rename store methods to hydrate / dehydrate * DRY that sucker * Move hydration logic into a store * Fix tests for new store * Rename hydrate store to app store, fix tests in auth * Fix tests of router * Fix tests in module-bar-logo * bunch of things * Fix tests in hydrate * Fix router tests * Clean up auth tests * Update tests for collections / fields stores * Use stores instead of mocks in tests * Add test for store getter in collections
This commit is contained in:
10
src/stores/app/app.ts
Normal file
10
src/stores/app/app.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { createStore } from 'pinia';
|
||||
|
||||
export const useAppStore = createStore({
|
||||
id: 'app',
|
||||
state: () => ({
|
||||
hydrated: false,
|
||||
hydrating: false,
|
||||
error: null
|
||||
})
|
||||
});
|
||||
4
src/stores/app/index.ts
Normal file
4
src/stores/app/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import { useAppStore } from './app';
|
||||
|
||||
export { useAppStore };
|
||||
export default useAppStore;
|
||||
179
src/stores/collections/collections.test.ts
Normal file
179
src/stores/collections/collections.test.ts
Normal file
@@ -0,0 +1,179 @@
|
||||
import api from '@/api';
|
||||
import Vue from 'vue';
|
||||
import VueCompositionAPI from '@vue/composition-api';
|
||||
import formatTitle from '@directus/format-title';
|
||||
import i18n from '@/lang';
|
||||
|
||||
import { useProjectsStore } from '@/stores/projects';
|
||||
import { useCollectionsStore } from './collections';
|
||||
|
||||
jest.mock('@directus/format-title');
|
||||
jest.mock('@/api');
|
||||
jest.mock('@/lang');
|
||||
|
||||
describe('Stores / collections', () => {
|
||||
let req: any = {};
|
||||
|
||||
beforeAll(() => {
|
||||
Vue.config.productionTip = false;
|
||||
Vue.config.devtools = false;
|
||||
Vue.use(VueCompositionAPI);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
req = {};
|
||||
});
|
||||
|
||||
describe('Getters / visibleCollections', () => {
|
||||
it('Filters collections starting with directus_', () => {
|
||||
const collectionsStore = useCollectionsStore(req);
|
||||
collectionsStore.state.collections = [
|
||||
{
|
||||
collection: 'test-1'
|
||||
},
|
||||
{
|
||||
collection: 'test-2'
|
||||
},
|
||||
{
|
||||
collection: 'directus_test'
|
||||
},
|
||||
{
|
||||
collection: 'test-3'
|
||||
}
|
||||
] as any;
|
||||
|
||||
expect(collectionsStore.visibleCollections).toEqual([
|
||||
{
|
||||
collection: 'test-1'
|
||||
},
|
||||
{
|
||||
collection: 'test-2'
|
||||
},
|
||||
{
|
||||
collection: 'test-3'
|
||||
}
|
||||
]);
|
||||
});
|
||||
|
||||
it('Filters collections that have the hidden flag true', () => {
|
||||
const collectionsStore = useCollectionsStore(req);
|
||||
collectionsStore.state.collections = [
|
||||
{
|
||||
collection: 'test-1',
|
||||
hidden: true
|
||||
},
|
||||
{
|
||||
collection: 'test-2',
|
||||
hidden: false
|
||||
},
|
||||
{
|
||||
collection: 'test-3',
|
||||
hidden: null
|
||||
}
|
||||
] as any;
|
||||
|
||||
expect(collectionsStore.visibleCollections).toEqual([
|
||||
{
|
||||
collection: 'test-2'
|
||||
},
|
||||
{
|
||||
collection: 'test-3'
|
||||
}
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Actions / Hydrate', () => {
|
||||
it('Calls the right endpoint', async () => {
|
||||
(api.get as jest.Mock).mockImplementation(() =>
|
||||
Promise.resolve({
|
||||
data: {
|
||||
data: []
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
const projectsStore = useProjectsStore(req);
|
||||
const collectionsStore = useCollectionsStore(req);
|
||||
|
||||
projectsStore.state.currentProjectKey = 'my-project';
|
||||
await collectionsStore.hydrate();
|
||||
|
||||
expect(api.get).toHaveBeenCalledWith('/my-project/collections');
|
||||
});
|
||||
|
||||
it('Formats the title to use as name', async () => {
|
||||
(api.get as jest.Mock).mockImplementation(() =>
|
||||
Promise.resolve({
|
||||
data: {
|
||||
data: [
|
||||
{
|
||||
collection: 'test_collection'
|
||||
}
|
||||
]
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
const projectsStore = useProjectsStore(req);
|
||||
const collectionsStore = useCollectionsStore(req);
|
||||
|
||||
projectsStore.state.currentProjectKey = 'my-project';
|
||||
await collectionsStore.hydrate();
|
||||
|
||||
expect(formatTitle).toHaveBeenCalledWith('test_collection');
|
||||
expect(collectionsStore.state.collections[0].hasOwnProperty('name')).toBe(true);
|
||||
});
|
||||
|
||||
it('Registers the passed translations to i18n to be registered', async () => {
|
||||
(api.get as jest.Mock).mockImplementation(() =>
|
||||
Promise.resolve({
|
||||
data: {
|
||||
data: [
|
||||
{
|
||||
collection: 'test_collection',
|
||||
translation: [
|
||||
{
|
||||
locale: 'en-US',
|
||||
translation: 'Test collection'
|
||||
},
|
||||
{
|
||||
locale: 'nl-NL',
|
||||
translation: 'Test verzameling'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
const projectsStore = useProjectsStore(req);
|
||||
projectsStore.state.currentProjectKey = 'my-project';
|
||||
|
||||
const collectionsStore = useCollectionsStore(req);
|
||||
await collectionsStore.hydrate();
|
||||
|
||||
expect(i18n.mergeLocaleMessage).toHaveBeenCalledWith('en-US', {
|
||||
collections: {
|
||||
test_collection: 'Test collection'
|
||||
}
|
||||
});
|
||||
|
||||
expect(i18n.mergeLocaleMessage).toHaveBeenCalledWith('nl-NL', {
|
||||
collections: {
|
||||
test_collection: 'Test verzameling'
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Actions/ Dehydrate', () => {
|
||||
it('Calls reset on dehydrate', async () => {
|
||||
const collectionsStore = useCollectionsStore(req);
|
||||
jest.spyOn(collectionsStore, 'reset');
|
||||
await collectionsStore.dehydrate();
|
||||
expect(collectionsStore.reset).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,7 +1,7 @@
|
||||
import { createStore } from 'pinia';
|
||||
import api from '@/api';
|
||||
import { Collection, CollectionRaw } from './types';
|
||||
import useProjectsStore from '@/stores/projects';
|
||||
import { useProjectsStore } from '@/stores/projects';
|
||||
import i18n from '@/lang/';
|
||||
import { notEmpty } from '@/utils/is-empty';
|
||||
import VueI18n from 'vue-i18n';
|
||||
@@ -20,9 +20,9 @@ export const useCollectionsStore = createStore({
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
async getCollections() {
|
||||
async hydrate() {
|
||||
const projectsStore = useProjectsStore();
|
||||
const { currentProjectKey } = projectsStore.state;
|
||||
const currentProjectKey = projectsStore.state.currentProjectKey;
|
||||
|
||||
const response = await api.get(`/${currentProjectKey}/collections`);
|
||||
|
||||
@@ -54,6 +54,9 @@ export const useCollectionsStore = createStore({
|
||||
icon
|
||||
};
|
||||
});
|
||||
},
|
||||
async dehydrate() {
|
||||
this.reset();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
120
src/stores/fields/fields.test.ts
Normal file
120
src/stores/fields/fields.test.ts
Normal file
@@ -0,0 +1,120 @@
|
||||
import api from '@/api';
|
||||
import Vue from 'vue';
|
||||
import VueCompositionAPI from '@vue/composition-api';
|
||||
import formatTitle from '@directus/format-title';
|
||||
import i18n from '@/lang';
|
||||
|
||||
import { useProjectsStore } from '@/stores/projects';
|
||||
import { useFieldsStore } from './fields';
|
||||
|
||||
jest.mock('@directus/format-title');
|
||||
jest.mock('@/api');
|
||||
jest.mock('@/lang');
|
||||
|
||||
describe('Stores / Fields', () => {
|
||||
let req: any = {};
|
||||
|
||||
beforeAll(() => {
|
||||
Vue.config.productionTip = false;
|
||||
Vue.config.devtools = false;
|
||||
Vue.use(VueCompositionAPI);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
req = {};
|
||||
});
|
||||
|
||||
describe('Hydrate', () => {
|
||||
it('Calls the right endpoint', () => {
|
||||
(api.get as jest.Mock).mockImplementation(() =>
|
||||
Promise.resolve({
|
||||
data: {
|
||||
data: []
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
const projectsStore = useProjectsStore(req);
|
||||
projectsStore.state.currentProjectKey = 'my-project';
|
||||
const fieldsStore = useFieldsStore(req);
|
||||
|
||||
fieldsStore.hydrate().then(() => {
|
||||
expect(api.get).toHaveBeenCalledWith('/my-project/fields');
|
||||
});
|
||||
});
|
||||
|
||||
it('Formats the title to use as name', async () => {
|
||||
(api.get as jest.Mock).mockImplementation(() =>
|
||||
Promise.resolve({
|
||||
data: {
|
||||
data: [
|
||||
{
|
||||
field: 'test_field'
|
||||
}
|
||||
]
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
const projectsStore = useProjectsStore(req);
|
||||
projectsStore.state.currentProjectKey = 'my-project';
|
||||
const fieldsStore = useFieldsStore(req);
|
||||
|
||||
await fieldsStore.hydrate();
|
||||
|
||||
expect(formatTitle).toHaveBeenCalledWith('test_field');
|
||||
expect(fieldsStore.state.fields[0].hasOwnProperty('name')).toBe(true);
|
||||
});
|
||||
|
||||
it('Registers the passed translations to i18n to be registered', async () => {
|
||||
(api.get as jest.Mock).mockImplementation(() =>
|
||||
Promise.resolve({
|
||||
data: {
|
||||
data: [
|
||||
{
|
||||
field: 'test_field',
|
||||
translation: [
|
||||
{
|
||||
locale: 'en-US',
|
||||
translation: 'Test field'
|
||||
},
|
||||
{
|
||||
locale: 'nl-NL',
|
||||
translation: 'Test veld'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
const projectsStore = useProjectsStore(req);
|
||||
projectsStore.state.currentProjectKey = 'my-project';
|
||||
const fieldsStore = useFieldsStore(req);
|
||||
|
||||
await fieldsStore.hydrate();
|
||||
|
||||
expect(i18n.mergeLocaleMessage).toHaveBeenCalledWith('en-US', {
|
||||
fields: {
|
||||
test_field: 'Test field'
|
||||
}
|
||||
});
|
||||
|
||||
expect(i18n.mergeLocaleMessage).toHaveBeenCalledWith('nl-NL', {
|
||||
fields: {
|
||||
test_field: 'Test veld'
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Dehydrate', () => {
|
||||
it('Calls reset on dehydrate', async () => {
|
||||
const fieldsStore: any = useFieldsStore(req);
|
||||
jest.spyOn(fieldsStore, 'reset');
|
||||
await fieldsStore.dehydrate();
|
||||
expect(fieldsStore.reset).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
53
src/stores/fields/fields.ts
Normal file
53
src/stores/fields/fields.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import { createStore } from 'pinia';
|
||||
import { FieldRaw, Field } from './types';
|
||||
import api from '@/api';
|
||||
import { useProjectsStore } from '@/stores/projects';
|
||||
import VueI18n from 'vue-i18n';
|
||||
import { notEmpty } from '@/utils/is-empty';
|
||||
import { i18n } from '@/lang';
|
||||
import formatTitle from '@directus/format-title';
|
||||
|
||||
export const useFieldsStore = createStore({
|
||||
id: 'fields',
|
||||
state: () => ({
|
||||
fields: [] as Field[]
|
||||
}),
|
||||
actions: {
|
||||
async hydrate() {
|
||||
const projectsStore = useProjectsStore();
|
||||
const currentProjectKey = projectsStore.state.currentProjectKey;
|
||||
|
||||
const response = await api.get(`/${currentProjectKey}/fields`);
|
||||
|
||||
const fields: FieldRaw[] = response.data.data;
|
||||
|
||||
this.state.fields = fields.map(field => {
|
||||
let name: string | VueI18n.TranslateResult;
|
||||
|
||||
if (notEmpty(field.translation)) {
|
||||
for (let i = 0; i < field.translation.length; i++) {
|
||||
const { locale, translation } = field.translation[i];
|
||||
|
||||
i18n.mergeLocaleMessage(locale, {
|
||||
fields: {
|
||||
[field.field]: translation
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
name = i18n.t(`fields.${field.field}`);
|
||||
} else {
|
||||
name = formatTitle(field.field);
|
||||
}
|
||||
|
||||
return {
|
||||
...field,
|
||||
name
|
||||
};
|
||||
});
|
||||
},
|
||||
async dehydrate() {
|
||||
this.reset();
|
||||
}
|
||||
}
|
||||
});
|
||||
4
src/stores/fields/index.ts
Normal file
4
src/stores/fields/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import { useFieldsStore } from './fields';
|
||||
|
||||
export { useFieldsStore };
|
||||
export default useFieldsStore;
|
||||
39
src/stores/fields/types.ts
Normal file
39
src/stores/fields/types.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import VueI18n from 'vue-i18n';
|
||||
|
||||
type Translation = {
|
||||
locale: string;
|
||||
translation: string;
|
||||
};
|
||||
|
||||
type Width = 'half' | 'half-left' | 'half-right' | 'full' | 'fill';
|
||||
|
||||
export interface FieldRaw {
|
||||
id: number;
|
||||
collection: string;
|
||||
field: string;
|
||||
datatype: string;
|
||||
unique: boolean;
|
||||
primary_key: boolean;
|
||||
auto_increment: boolean;
|
||||
default_value: any; // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||
note: string;
|
||||
signed: boolean;
|
||||
type: string;
|
||||
sort: null | number;
|
||||
interface: string;
|
||||
hidden_detail: boolean;
|
||||
hidden_browse: boolean;
|
||||
required: boolean;
|
||||
options: null | { [key: string]: any }; // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||
locked: boolean;
|
||||
translation: null | Translation[];
|
||||
readonly: boolean;
|
||||
width: null | Width;
|
||||
validaton: string;
|
||||
group: number;
|
||||
length: string | number;
|
||||
}
|
||||
|
||||
export interface Field extends FieldRaw {
|
||||
name: string | VueI18n.TranslateResult;
|
||||
}
|
||||
@@ -8,10 +8,6 @@ describe('Stores / Projects', () => {
|
||||
Vue.use(VueCompositionAPI);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('Getters / currentProject', () => {
|
||||
const dummyProject = {
|
||||
key: 'my-project',
|
||||
|
||||
@@ -10,9 +10,6 @@ export const useRequestsStore = createStore({
|
||||
queueHasItems: state => state.queue.length > 0
|
||||
},
|
||||
actions: {
|
||||
reset() {
|
||||
this.state.queue = [];
|
||||
},
|
||||
startRequest() {
|
||||
const id = nanoid();
|
||||
this.state.queue = [...this.state.queue, id];
|
||||
|
||||
Reference in New Issue
Block a user