mirror of
https://github.com/directus/directus.git
synced 2026-01-27 12:18:25 -05: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:
@@ -1,10 +1,10 @@
|
||||
import Vue from 'vue';
|
||||
import VueCompositionAPI from '@vue/composition-api';
|
||||
import * as hydration from '@/hydrate';
|
||||
import api from '@/api';
|
||||
import { checkAuth, login, logout, LogoutReason } from './auth';
|
||||
import { useProjectsStore } from '@/stores/projects/';
|
||||
import { useProjectsStore } from '@/stores/projects';
|
||||
import router from '@/router';
|
||||
import { hydrate, dehydrate } from '@/hydrate';
|
||||
|
||||
jest.mock('@/api');
|
||||
jest.mock('@/router');
|
||||
@@ -20,13 +20,13 @@ describe('Auth', () => {
|
||||
beforeEach(() => {
|
||||
jest.spyOn(api, 'get');
|
||||
jest.spyOn(api, 'post');
|
||||
jest.spyOn(hydration, 'hydrate');
|
||||
jest.spyOn(hydration, 'dehydrate');
|
||||
});
|
||||
|
||||
describe('checkAuth', () => {
|
||||
it('Does not ping the API if the curent project key is null', async () => {
|
||||
useProjectsStore({}).state.currentProjectKey = null;
|
||||
const projectsStore = useProjectsStore({});
|
||||
projectsStore.state.currentProjectKey = null;
|
||||
|
||||
(api.get as jest.Mock).mockImplementation(() =>
|
||||
Promise.resolve({ data: { data: { authenticated: true } } })
|
||||
);
|
||||
@@ -35,7 +35,9 @@ describe('Auth', () => {
|
||||
});
|
||||
|
||||
it('Calls the api with the correct endpoint', async () => {
|
||||
useProjectsStore({}).state.currentProjectKey = 'test-project';
|
||||
const projectsStore = useProjectsStore({});
|
||||
projectsStore.state.currentProjectKey = 'test-project';
|
||||
|
||||
(api.get as jest.Mock).mockImplementation(() =>
|
||||
Promise.resolve({ data: { data: { authenticated: true } } })
|
||||
);
|
||||
@@ -44,7 +46,9 @@ describe('Auth', () => {
|
||||
});
|
||||
|
||||
it('Returns true if user is logged in', async () => {
|
||||
useProjectsStore({}).state.currentProjectKey = 'test-project';
|
||||
const projectsStore = useProjectsStore({});
|
||||
projectsStore.state.currentProjectKey = 'test-project';
|
||||
|
||||
(api.get as jest.Mock).mockImplementation(() =>
|
||||
Promise.resolve({ data: { data: { authenticated: true } } })
|
||||
);
|
||||
@@ -53,7 +57,9 @@ describe('Auth', () => {
|
||||
});
|
||||
|
||||
it('Returns false if user is logged out', async () => {
|
||||
useProjectsStore({}).state.currentProjectKey = 'test-project';
|
||||
const projectsStore = useProjectsStore({});
|
||||
projectsStore.state.currentProjectKey = 'test-project';
|
||||
|
||||
(api.get as jest.Mock).mockImplementation(() =>
|
||||
Promise.resolve({ data: { data: { authenticated: false } } })
|
||||
);
|
||||
@@ -64,7 +70,9 @@ describe('Auth', () => {
|
||||
|
||||
describe('login', () => {
|
||||
it('Calls /auth/authenticate with the provided credentials', async () => {
|
||||
useProjectsStore({}).state.currentProjectKey = 'test-project';
|
||||
const projectsStore = useProjectsStore({});
|
||||
projectsStore.state.currentProjectKey = 'test-project';
|
||||
|
||||
await login({ email: 'test', password: 'test' });
|
||||
expect(api.post).toHaveBeenCalledWith('/test-project/auth/authenticate', {
|
||||
mode: 'cookie',
|
||||
@@ -74,9 +82,12 @@ describe('Auth', () => {
|
||||
});
|
||||
|
||||
it('Calls hydrate on successful login', async () => {
|
||||
useProjectsStore({}).state.currentProjectKey = 'test-project';
|
||||
const projectsStore = useProjectsStore({});
|
||||
projectsStore.state.currentProjectKey = 'test-project';
|
||||
|
||||
await login({ email: 'test', password: 'test' });
|
||||
expect(hydration.hydrate).toHaveBeenCalled();
|
||||
|
||||
expect(hydrate).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -84,17 +95,21 @@ describe('Auth', () => {
|
||||
it('Does not do anything when there is no current project', async () => {
|
||||
useProjectsStore({});
|
||||
await logout();
|
||||
expect(hydration.dehydrate).not.toHaveBeenCalled();
|
||||
expect(dehydrate).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('Calls dehydrate', async () => {
|
||||
useProjectsStore({}).state.currentProjectKey = 'test-project';
|
||||
const projectsStore = useProjectsStore({});
|
||||
projectsStore.state.currentProjectKey = 'test-project';
|
||||
|
||||
await logout();
|
||||
expect(hydration.dehydrate).toHaveBeenCalled();
|
||||
expect(dehydrate).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('Posts to /logout on regular sign out', async () => {
|
||||
useProjectsStore({}).state.currentProjectKey = 'test-project';
|
||||
const projectsStore = useProjectsStore({});
|
||||
projectsStore.state.currentProjectKey = 'test-project';
|
||||
|
||||
await logout();
|
||||
expect(api.post).toHaveBeenCalledWith('/test-project/auth/logout');
|
||||
});
|
||||
@@ -102,6 +117,7 @@ describe('Auth', () => {
|
||||
it('Navigates to the current projects login page', async () => {
|
||||
const projectsStore = useProjectsStore({});
|
||||
projectsStore.state.currentProjectKey = 'my-project';
|
||||
|
||||
await logout();
|
||||
expect(router.push).toHaveBeenCalledWith({
|
||||
path: '/my-project/login',
|
||||
@@ -114,6 +130,7 @@ describe('Auth', () => {
|
||||
it('Does not navigate when the navigate option is false', async () => {
|
||||
const projectsStore = useProjectsStore({});
|
||||
projectsStore.state.currentProjectKey = 'my-project';
|
||||
|
||||
await logout({ navigate: false });
|
||||
expect(router.push).not.toHaveBeenCalled();
|
||||
});
|
||||
@@ -121,6 +138,7 @@ describe('Auth', () => {
|
||||
it('Adds the reason query param if any non-default reason is given', async () => {
|
||||
const projectsStore = useProjectsStore({});
|
||||
projectsStore.state.currentProjectKey = 'my-project';
|
||||
|
||||
await logout({ reason: LogoutReason.ERROR_SESSION_EXPIRED });
|
||||
expect(router.push).toHaveBeenCalledWith({
|
||||
path: '/my-project/login',
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { RawLocation } from 'vue-router';
|
||||
import { useProjectsStore } from '@/stores/projects/';
|
||||
import api from '@/api';
|
||||
import { hydrate, dehydrate } from './hydrate';
|
||||
import { hydrate, dehydrate } from '@/hydrate';
|
||||
import router from '@/router';
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,61 +1,172 @@
|
||||
import { useCollectionsStore } from './stores/collections/';
|
||||
import { hydrated, hydrate, dehydrate } from './hydrate';
|
||||
|
||||
import Vue from 'vue';
|
||||
import VueCompositionAPI from '@vue/composition-api';
|
||||
import { useAppStore } from '@/stores/app';
|
||||
import { useStores, hydrate, dehydrate } from './hydrate';
|
||||
|
||||
describe('Hydration', () => {
|
||||
describe('Stores / App', () => {
|
||||
beforeAll(() => {
|
||||
Vue.use(VueCompositionAPI);
|
||||
});
|
||||
|
||||
describe('useStores', () => {
|
||||
it('Calls all functions', () => {
|
||||
const mockStores: any = [jest.fn(), jest.fn()];
|
||||
useStores(mockStores);
|
||||
expect(mockStores[0]).toHaveBeenCalled();
|
||||
expect(mockStores[1]).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('Defaults to a global set of stores', () => {
|
||||
expect(useStores).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Hydrate', () => {
|
||||
it('Calls the correct stores', async () => {
|
||||
const collectionsStore = useCollectionsStore({});
|
||||
collectionsStore.getCollections = jest.fn();
|
||||
await hydrate();
|
||||
expect(collectionsStore.getCollections).toHaveBeenCalled();
|
||||
it('Sets hydrating state during hydration', async () => {
|
||||
const appStore = useAppStore({});
|
||||
|
||||
expect(appStore.state.hydrating).toBe(false);
|
||||
|
||||
const promise = hydrate([
|
||||
{
|
||||
id: 'test1',
|
||||
hydrate() {
|
||||
return new Promise(resolve => {
|
||||
setTimeout(() => resolve(), 15);
|
||||
});
|
||||
}
|
||||
}
|
||||
]);
|
||||
|
||||
expect(appStore.state.hydrating).toBe(true);
|
||||
|
||||
promise.then(() => {
|
||||
expect(appStore.state.hydrating).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
it('Sets hydrated let after it is done', async () => {
|
||||
await hydrate();
|
||||
expect(hydrated).toBe(true);
|
||||
it('Calls the hydrate function for all stores', async () => {
|
||||
const mockStores: any = [
|
||||
{
|
||||
hydrate: jest.fn(() => Promise.resolve())
|
||||
},
|
||||
{
|
||||
dehydrate: jest.fn(() => Promise.resolve())
|
||||
},
|
||||
{
|
||||
hydrate: jest.fn(() => Promise.resolve()),
|
||||
dehydrate: jest.fn(() => Promise.resolve())
|
||||
}
|
||||
];
|
||||
|
||||
useAppStore({});
|
||||
|
||||
await hydrate(mockStores);
|
||||
|
||||
expect(mockStores[0].hydrate).toHaveBeenCalled();
|
||||
expect(mockStores[2].hydrate).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('Does not hydrate when already hydrated', async () => {
|
||||
await hydrate();
|
||||
it('Sets the hydrated state to true when done', async () => {
|
||||
const appStore = useAppStore({});
|
||||
|
||||
const collectionsStore = useCollectionsStore({});
|
||||
collectionsStore.getCollections = jest.fn();
|
||||
await hydrate([]);
|
||||
|
||||
await hydrate();
|
||||
expect(appStore.state.hydrated).toBe(true);
|
||||
});
|
||||
|
||||
expect(collectionsStore.getCollections).not.toHaveBeenCalled();
|
||||
it('Does not hydrate when hydrated is true', async () => {
|
||||
const appStore = useAppStore({});
|
||||
appStore.state.hydrated = true;
|
||||
|
||||
const mockStores: any = [
|
||||
{
|
||||
hydrate: jest.fn(() => Promise.resolve())
|
||||
}
|
||||
];
|
||||
|
||||
await hydrate([]);
|
||||
|
||||
expect(mockStores[0].hydrate).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('Does not hydrate when hydrating is true', async () => {
|
||||
const appStore = useAppStore({});
|
||||
appStore.state.hydrating = true;
|
||||
|
||||
const mockStores: any = [
|
||||
{
|
||||
hydrate: jest.fn(() => Promise.resolve())
|
||||
}
|
||||
];
|
||||
|
||||
await hydrate(mockStores);
|
||||
|
||||
expect(mockStores[0].hydrate).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('Sets the error state when one of the hydration functions fails', async () => {
|
||||
const mockStores: any = [
|
||||
{
|
||||
hydrate: jest.fn(() => {
|
||||
throw 'test';
|
||||
})
|
||||
}
|
||||
];
|
||||
|
||||
const appStore = useAppStore({});
|
||||
|
||||
await hydrate(mockStores);
|
||||
|
||||
expect(appStore.state.error).toBe('test');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Dehydrate', () => {
|
||||
it('Calls resets functions of correct stores', async () => {
|
||||
const collectionsStore = useCollectionsStore({});
|
||||
collectionsStore.reset = jest.fn();
|
||||
await dehydrate();
|
||||
expect(collectionsStore.reset).toHaveBeenCalled();
|
||||
it('Calls the dehydrate function for all stores', async () => {
|
||||
const mockStores: any = [
|
||||
{
|
||||
dehydrate: jest.fn(() => Promise.resolve())
|
||||
},
|
||||
{},
|
||||
{
|
||||
dehydrate: jest.fn(() => Promise.resolve())
|
||||
}
|
||||
];
|
||||
|
||||
const appStore = useAppStore({});
|
||||
appStore.state.hydrated = true;
|
||||
|
||||
await dehydrate(mockStores);
|
||||
|
||||
expect(mockStores[0].dehydrate).toHaveBeenCalled();
|
||||
expect(mockStores[2].dehydrate).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('Sets hydrated let after it is done', async () => {
|
||||
await dehydrate();
|
||||
expect(hydrated).toBe(false);
|
||||
it('Sets the hydrated state to false when done', async () => {
|
||||
const mockStores: any = [{}];
|
||||
|
||||
const appStore = useAppStore({});
|
||||
appStore.state.hydrated = true;
|
||||
|
||||
await dehydrate(mockStores);
|
||||
|
||||
expect(appStore.state.hydrated).toBe(false);
|
||||
});
|
||||
|
||||
it('Does not dehydrate when already dehydrated', async () => {
|
||||
await dehydrate();
|
||||
it('Does not dehydrate when store is already dehydrated', async () => {
|
||||
const mockStores: any = [
|
||||
{
|
||||
dehydrate: jest.fn(() => Promise.resolve())
|
||||
}
|
||||
];
|
||||
|
||||
const collectionsStore = useCollectionsStore({});
|
||||
collectionsStore.reset = jest.fn();
|
||||
const appStore = useAppStore({});
|
||||
appStore.state.hydrated = false;
|
||||
|
||||
await dehydrate();
|
||||
await dehydrate(mockStores);
|
||||
|
||||
expect(collectionsStore.reset).not.toHaveBeenCalled();
|
||||
expect(mockStores[0].dehydrate).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,15 +1,51 @@
|
||||
import { useCollectionsStore } from '@/stores/collections';
|
||||
import { useAppStore } from '@/stores/app/';
|
||||
import { useCollectionsStore } from '@/stores/collections/';
|
||||
import { useFieldsStore } from '@/stores/fields/';
|
||||
import { useRequestsStore } from '@/stores/requests/';
|
||||
|
||||
export let hydrated = false;
|
||||
type GenericStore = {
|
||||
id: string;
|
||||
hydrate?: () => Promise<void>;
|
||||
dehydrate?: () => Promise<void>;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
[key: string]: any;
|
||||
};
|
||||
|
||||
export async function hydrate() {
|
||||
if (hydrated) return;
|
||||
await useCollectionsStore().getCollections();
|
||||
hydrated = true;
|
||||
export function useStores(stores = [useCollectionsStore, useFieldsStore, useRequestsStore]) {
|
||||
return stores.map(useStore => useStore()) as GenericStore[];
|
||||
}
|
||||
|
||||
export async function dehydrate() {
|
||||
if (hydrated === false) return;
|
||||
useCollectionsStore().reset();
|
||||
hydrated = false;
|
||||
/* istanbul ignore next: useStores has a test already */
|
||||
export async function hydrate(stores = useStores()) {
|
||||
const appStore = useAppStore();
|
||||
|
||||
if (appStore.state.hydrated) return;
|
||||
if (appStore.state.hydrating) return;
|
||||
|
||||
appStore.state.hydrating = true;
|
||||
|
||||
try {
|
||||
for (const store of stores) {
|
||||
await store.hydrate?.();
|
||||
}
|
||||
} catch (error) {
|
||||
appStore.state.error = error;
|
||||
} finally {
|
||||
appStore.state.hydrating = false;
|
||||
}
|
||||
|
||||
appStore.state.hydrated = true;
|
||||
}
|
||||
|
||||
/* istanbul ignore next: useStores has a test already */
|
||||
export async function dehydrate(stores = useStores()) {
|
||||
const appStore = useAppStore();
|
||||
|
||||
if (appStore.state.hydrated === false) return;
|
||||
|
||||
for (const store of stores) {
|
||||
await store.dehydrate?.();
|
||||
}
|
||||
|
||||
appStore.state.hydrated = false;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import Vue from 'vue';
|
||||
import VueCompositionAPI from '@vue/composition-api';
|
||||
import { Route } from 'vue-router';
|
||||
import * as hydration from '@/hydrate';
|
||||
import {
|
||||
onBeforeEach,
|
||||
onBeforeEnterProjectChooser,
|
||||
@@ -9,12 +8,13 @@ import {
|
||||
defaultRoutes,
|
||||
onBeforeEnterLogout
|
||||
} from './router';
|
||||
import { useProjectsStore } from '@/stores/projects';
|
||||
import api from '@/api';
|
||||
import * as auth from '@/auth';
|
||||
import { useProjectsStore } from '@/stores/projects';
|
||||
import { hydrate } from '@/hydrate';
|
||||
|
||||
jest.mock('@/hydrate');
|
||||
jest.mock('@/auth');
|
||||
jest.mock('@/hydrate');
|
||||
|
||||
const route: Route = {
|
||||
name: undefined,
|
||||
@@ -39,16 +39,18 @@ describe('Router', () => {
|
||||
});
|
||||
|
||||
it('Fetches the projects using projectsStore on first load', async () => {
|
||||
const projectsStore = useProjectsStore({});
|
||||
projectsStore.getProjects = jest.fn();
|
||||
|
||||
const toRoute = route;
|
||||
|
||||
const fromRoute = {
|
||||
...route,
|
||||
name: null
|
||||
};
|
||||
|
||||
const callback = jest.fn();
|
||||
|
||||
const projectsStore = useProjectsStore({});
|
||||
jest.spyOn(projectsStore, 'getProjects');
|
||||
|
||||
await onBeforeEach(toRoute, fromRoute as any, callback);
|
||||
|
||||
expect(projectsStore.getProjects).toHaveBeenCalled();
|
||||
@@ -85,7 +87,8 @@ describe('Router', () => {
|
||||
|
||||
it('Keeps projects store in sync with project in route', async () => {
|
||||
const projectsStore = useProjectsStore({});
|
||||
projectsStore.setCurrentProject = jest.fn();
|
||||
|
||||
jest.spyOn(projectsStore, 'setCurrentProject');
|
||||
|
||||
const toRoute = {
|
||||
...route,
|
||||
@@ -102,8 +105,7 @@ describe('Router', () => {
|
||||
});
|
||||
|
||||
it('Redirects to / when trying to open non-existing project', async () => {
|
||||
const projectsStore = useProjectsStore({});
|
||||
projectsStore.setCurrentProject = jest.fn(() => Promise.resolve(false));
|
||||
useProjectsStore({});
|
||||
|
||||
const toRoute = {
|
||||
...route,
|
||||
@@ -121,8 +123,7 @@ describe('Router', () => {
|
||||
});
|
||||
|
||||
it('Does not redirect to / when trying to open /', async () => {
|
||||
const projectsStore = useProjectsStore({});
|
||||
projectsStore.setCurrentProject = jest.fn(() => Promise.resolve(false));
|
||||
useProjectsStore({});
|
||||
|
||||
const toRoute = {
|
||||
...route,
|
||||
@@ -141,7 +142,7 @@ describe('Router', () => {
|
||||
|
||||
it('Calls next when trying to open public route', async () => {
|
||||
const projectsStore = useProjectsStore({});
|
||||
projectsStore.getProjects = jest.fn();
|
||||
jest.spyOn(projectsStore, 'getProjects').mockResolvedValue();
|
||||
|
||||
const checkAuth = jest.spyOn(auth, 'checkAuth');
|
||||
const toRoute = {
|
||||
@@ -163,14 +164,16 @@ describe('Router', () => {
|
||||
});
|
||||
|
||||
it('Checks if you are authenticated on first load', async () => {
|
||||
const projectsStore = useProjectsStore({});
|
||||
projectsStore.getProjects = jest.fn();
|
||||
jest.spyOn(auth, 'checkAuth').mockImplementation(() => Promise.resolve(false));
|
||||
(projectsStore.state.projects as any) = [
|
||||
|
||||
const projectsStore = useProjectsStore({});
|
||||
jest.spyOn(projectsStore, 'getProjects').mockResolvedValue();
|
||||
|
||||
projectsStore.state.projects = [
|
||||
{
|
||||
key: 'my-project'
|
||||
}
|
||||
];
|
||||
] as any;
|
||||
|
||||
const to = {
|
||||
...route,
|
||||
@@ -185,18 +188,18 @@ describe('Router', () => {
|
||||
await onBeforeEach(to, from as any, next);
|
||||
|
||||
expect(auth.checkAuth).toHaveBeenCalled();
|
||||
expect(next).toHaveBeenCalledWith('/my-project/login');
|
||||
});
|
||||
|
||||
it('Hydrates the store on first load when logged in', async () => {
|
||||
const projectsStore = useProjectsStore({});
|
||||
projectsStore.getProjects = jest.fn();
|
||||
jest.spyOn(auth, 'checkAuth').mockImplementation(() => Promise.resolve(true));
|
||||
(projectsStore.state.projects as any) = [
|
||||
|
||||
const projectsStore = useProjectsStore({});
|
||||
projectsStore.state.projects = [
|
||||
{
|
||||
key: 'my-project'
|
||||
}
|
||||
];
|
||||
] as any;
|
||||
jest.spyOn(projectsStore, 'getProjects').mockResolvedValue();
|
||||
|
||||
const to = {
|
||||
...route,
|
||||
@@ -210,18 +213,19 @@ describe('Router', () => {
|
||||
|
||||
await onBeforeEach(to, from as any, next);
|
||||
|
||||
expect(hydration.hydrate).toHaveBeenCalled();
|
||||
expect(hydrate).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('Calls next when all checks are done', async () => {
|
||||
const projectsStore = useProjectsStore({});
|
||||
projectsStore.getProjects = jest.fn();
|
||||
jest.spyOn(auth, 'checkAuth').mockImplementation(() => Promise.resolve(true));
|
||||
(projectsStore.state.projects as any) = [
|
||||
|
||||
const projectsStore = useProjectsStore({});
|
||||
projectsStore.state.projects = [
|
||||
{
|
||||
key: 'my-project'
|
||||
}
|
||||
];
|
||||
] as any;
|
||||
jest.spyOn(projectsStore, 'getProjects').mockResolvedValue();
|
||||
|
||||
const to = {
|
||||
...route,
|
||||
@@ -243,6 +247,8 @@ describe('Router', () => {
|
||||
it('Sets the current project to null on open', () => {
|
||||
const projectsStore = useProjectsStore({});
|
||||
projectsStore.state.currentProjectKey = 'my-project';
|
||||
jest.spyOn(projectsStore, 'getProjects').mockResolvedValue();
|
||||
|
||||
const to = { ...route, path: '/' };
|
||||
const from = route;
|
||||
const next = jest.fn();
|
||||
|
||||
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];
|
||||
|
||||
@@ -1,30 +1,22 @@
|
||||
import { mount, createLocalVue, Wrapper } from '@vue/test-utils';
|
||||
import { shallowMount, createLocalVue } from '@vue/test-utils';
|
||||
import VueCompositionAPI from '@vue/composition-api';
|
||||
import ModuleBarLogo from './module-bar-logo.vue';
|
||||
import { useProjectsStore } from '@/stores/projects';
|
||||
import { useRequestsStore } from '@/stores/requests';
|
||||
import { useProjectsStore } from '@/stores/projects';
|
||||
|
||||
const localVue = createLocalVue();
|
||||
localVue.use(VueCompositionAPI);
|
||||
|
||||
describe('Views / Private / Module Bar / Logo', () => {
|
||||
let component: Wrapper<Vue>;
|
||||
const projectsStore = useProjectsStore();
|
||||
const requestsStore = useRequestsStore();
|
||||
|
||||
beforeEach(() => {
|
||||
component = mount(ModuleBarLogo, { localVue });
|
||||
projectsStore.reset();
|
||||
requestsStore.reset();
|
||||
});
|
||||
|
||||
it('Renders the default rabbit when were not in a project', () => {
|
||||
it('Renders the default rabbit when we are not in a project', () => {
|
||||
const component = shallowMount(ModuleBarLogo, { localVue });
|
||||
expect((component.vm as any).customLogoPath).toBe(null);
|
||||
});
|
||||
|
||||
it('Renders the default rabbit when the current project errored out', () => {
|
||||
projectsStore.state.projects = [
|
||||
{
|
||||
const projectsStore = useProjectsStore({});
|
||||
projectsStore.currentProject = {
|
||||
value: {
|
||||
key: 'my-project',
|
||||
status: 500,
|
||||
error: {
|
||||
@@ -32,14 +24,17 @@ describe('Views / Private / Module Bar / Logo', () => {
|
||||
message: 'Could not connect to the database'
|
||||
}
|
||||
}
|
||||
];
|
||||
projectsStore.state.currentProjectKey = 'my-project';
|
||||
};
|
||||
|
||||
const component = shallowMount(ModuleBarLogo, { localVue });
|
||||
|
||||
expect((component.vm as any).customLogoPath).toBe(null);
|
||||
});
|
||||
|
||||
it('Renders the default rabbit when the current project does not have a custom logo', () => {
|
||||
projectsStore.state.projects = [
|
||||
{
|
||||
const projectsStore = useProjectsStore({});
|
||||
projectsStore.currentProject = {
|
||||
value: {
|
||||
key: 'my-project',
|
||||
api: {
|
||||
requires2FA: false,
|
||||
@@ -53,14 +48,17 @@ describe('Views / Private / Module Bar / Logo', () => {
|
||||
project_logo: null
|
||||
}
|
||||
}
|
||||
];
|
||||
projectsStore.state.currentProjectKey = 'my-project';
|
||||
};
|
||||
|
||||
const component = shallowMount(ModuleBarLogo, { localVue });
|
||||
|
||||
expect((component.vm as any).customLogoPath).toBe(null);
|
||||
});
|
||||
|
||||
it('Renders the custom logo if set', async () => {
|
||||
projectsStore.state.projects = [
|
||||
{
|
||||
it('Renders the custom logo if set', () => {
|
||||
const projectsStore = useProjectsStore({});
|
||||
projectsStore.currentProject = {
|
||||
value: {
|
||||
key: 'my-project',
|
||||
api: {
|
||||
requires2FA: false,
|
||||
@@ -77,27 +75,31 @@ describe('Views / Private / Module Bar / Logo', () => {
|
||||
}
|
||||
}
|
||||
}
|
||||
];
|
||||
projectsStore.state.currentProjectKey = 'my-project';
|
||||
await component.vm.$nextTick();
|
||||
};
|
||||
|
||||
const component = shallowMount(ModuleBarLogo, { localVue });
|
||||
|
||||
expect((component.vm as any).customLogoPath).toBe('abc');
|
||||
expect(component.find('img').attributes().src).toBe('abc');
|
||||
});
|
||||
|
||||
it('Only stops running if the queue is empty', async () => {
|
||||
requestsStore.state.queue = [];
|
||||
await component.vm.$nextTick();
|
||||
it('Only stops running if the queue is empty', () => {
|
||||
const requestsStore = useRequestsStore({});
|
||||
requestsStore.queueHasItems = { value: false };
|
||||
|
||||
let component = shallowMount(ModuleBarLogo, { localVue });
|
||||
(component.vm as any).isRunning = true;
|
||||
(component.vm as any).stopRunningIfQueueIsEmpty();
|
||||
expect((component.vm as any).isRunning).toBe(false);
|
||||
|
||||
requestsStore.state.queue = ['abc'];
|
||||
await component.vm.$nextTick();
|
||||
requestsStore.queueHasItems = { value: true };
|
||||
component = shallowMount(ModuleBarLogo, { localVue });
|
||||
expect((component.vm as any).isRunning).toBe(true);
|
||||
(component.vm as any).stopRunningIfQueueIsEmpty();
|
||||
expect((component.vm as any).isRunning).toBe(true);
|
||||
requestsStore.state.queue = [];
|
||||
await component.vm.$nextTick();
|
||||
|
||||
requestsStore.queueHasItems = { value: false };
|
||||
component = shallowMount(ModuleBarLogo, { localVue });
|
||||
(component.vm as any).stopRunningIfQueueIsEmpty();
|
||||
expect((component.vm as any).isRunning).toBe(false);
|
||||
});
|
||||
|
||||
@@ -12,9 +12,9 @@
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref, computed, watch } from '@vue/composition-api';
|
||||
import { useProjectsStore } from '@/stores/projects';
|
||||
import { ProjectWithKey, ProjectError } from '@/stores/projects/types';
|
||||
import { useRequestsStore } from '@/stores/requests';
|
||||
import { useProjectsStore } from '@/stores/projects/';
|
||||
import { useRequestsStore } from '@/stores/requests/';
|
||||
|
||||
export default defineComponent({
|
||||
setup() {
|
||||
|
||||
@@ -2,7 +2,7 @@ import Vue from 'vue';
|
||||
import VueCompositionAPI from '@vue/composition-api';
|
||||
import { mount, createLocalVue, Wrapper } from '@vue/test-utils';
|
||||
import VIcon from '@/components/v-icon/';
|
||||
import { useProjectsStore } from '@/stores/projects';
|
||||
import { useProjectsStore } from '@/stores/projects/';
|
||||
import { ProjectWithKey } from '@/stores/projects/types';
|
||||
import Tooltip from '@/directives/tooltip/tooltip';
|
||||
|
||||
|
||||
Reference in New Issue
Block a user