diff --git a/src/app.vue b/src/app.vue
new file mode 100644
index 0000000000..496db0ced4
--- /dev/null
+++ b/src/app.vue
@@ -0,0 +1,50 @@
+
+
+
+
+
+
+
diff --git a/src/auth.ts b/src/auth.ts
index 37505b1df3..f2c575df54 100644
--- a/src/auth.ts
+++ b/src/auth.ts
@@ -12,8 +12,12 @@ export async function checkAuth() {
if (!currentProjectKey) return false;
- const response = await api.get(`/${currentProjectKey}/auth/check`);
- return response.data.data.authenticated;
+ try {
+ const response = await api.get(`/${currentProjectKey}/auth/check`);
+ return response.data.data.authenticated;
+ } catch {
+ return false;
+ }
}
export type LoginCredentials = {
diff --git a/src/components/register.ts b/src/components/register.ts
index f16449ea58..fe0db97fe3 100644
--- a/src/components/register.ts
+++ b/src/components/register.ts
@@ -17,6 +17,7 @@ import VList, {
VListItemTitle,
VListGroup
} from './v-list/';
+import VNotice from './v-notice/';
import VOverlay from './v-overlay/';
import VProgressLinear from './v-progress/linear/';
import VProgressCircular from './v-progress/circular/';
@@ -42,6 +43,7 @@ Vue.component('v-list-item-icon', VListItemIcon);
Vue.component('v-list-item-subtitle', VListItemSubtitle);
Vue.component('v-list-item-title', VListItemTitle);
Vue.component('v-list-group', VListGroup);
+Vue.component('v-notice', VNotice);
Vue.component('v-overlay', VOverlay);
Vue.component('v-progress-linear', VProgressLinear);
Vue.component('v-progress-circular', VProgressCircular);
diff --git a/src/components/v-button/v-button.test.ts b/src/components/v-button/v-button.test.ts
index 4f1b57fdab..2a7e0a9592 100644
--- a/src/components/v-button/v-button.test.ts
+++ b/src/components/v-button/v-button.test.ts
@@ -33,15 +33,15 @@ describe('Button', () => {
expect(component.classes()).toContain('outlined');
});
- it('Adds the block class for block buttons', () => {
+ it('Adds the full-width class for full-width buttons', () => {
const component = mount(VButton, {
localVue,
propsData: {
- block: true
+ fullWidth: true
}
});
- expect(component.classes()).toContain('block');
+ expect(component.classes()).toContain('full-width');
});
it('Adds the rounded class for rounded buttons', () => {
diff --git a/src/components/v-button/v-button.vue b/src/components/v-button/v-button.vue
index 3bcdb284a0..4b879db51a 100644
--- a/src/components/v-button/v-button.vue
+++ b/src/components/v-button/v-button.vue
@@ -3,7 +3,10 @@
:is="component"
active-class="activated"
class="v-button"
- :class="[sizeClass, { block, rounded, icon, outlined, loading, secondary }]"
+ :class="[
+ sizeClass,
+ { 'full-width': fullWidth, rounded, icon, outlined, loading, secondary }
+ ]"
:type="type"
:disabled="disabled"
:to="to"
@@ -25,7 +28,7 @@ import useSizeClass, { sizeProps } from '@/compositions/size-class';
export default defineComponent({
props: {
- block: {
+ fullWidth: {
type: Boolean,
default: false
},
@@ -140,8 +143,8 @@ export default defineComponent({
}
}
- &.block {
- display: block;
+ &.full-width {
+ display: flex;
min-width: 100%;
}
diff --git a/src/main.ts b/src/main.ts
index a09c5e53f5..2f18a51c63 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -13,10 +13,12 @@ import './modules/register';
import './layouts/register';
import './interfaces/register';
+import App from './app.vue';
+
Vue.config.productionTip = false;
new Vue({
- render: h => h('router-view'),
+ render: h => h(App),
router,
i18n
}).$mount('#app');
diff --git a/src/modules/collections/compositions/use-navigation.test.ts b/src/modules/collections/compositions/use-navigation.test.ts
index c67128bd45..7535537dc0 100644
--- a/src/modules/collections/compositions/use-navigation.test.ts
+++ b/src/modules/collections/compositions/use-navigation.test.ts
@@ -34,7 +34,7 @@ describe('Modules / Collections / Compositions / useNavigation', () => {
navItems = useNavigation().navItems;
});
- expect(navItems).toEqual([
+ expect(navItems.value).toEqual([
{
collection: 'test',
name: 'Test',
@@ -89,8 +89,8 @@ describe('Modules / Collections / Compositions / useNavigation', () => {
navItems = useNavigation().navItems;
});
- expect(navItems[0].name).toBe('A Test');
- expect(navItems[1].name).toBe('B Test');
- expect(navItems[2].name).toBe('C Test');
+ expect(navItems.value[0].name).toBe('A Test');
+ expect(navItems.value[1].name).toBe('B Test');
+ expect(navItems.value[2].name).toBe('C Test');
});
});
diff --git a/src/modules/collections/compositions/use-navigation.ts b/src/modules/collections/compositions/use-navigation.ts
index e1e2057b68..be93146d1d 100644
--- a/src/modules/collections/compositions/use-navigation.ts
+++ b/src/modules/collections/compositions/use-navigation.ts
@@ -31,5 +31,5 @@ export default function useNavigation() {
});
});
- return { navItems: navItems.value };
+ return { navItems: navItems };
}
diff --git a/src/modules/collections/routes/browse/browse.vue b/src/modules/collections/routes/browse/browse.vue
index aec5a818b2..61d0b5823e 100644
--- a/src/modules/collections/routes/browse/browse.vue
+++ b/src/modules/collections/routes/browse/browse.vue
@@ -29,6 +29,7 @@ import { Collection } from '@/stores/collections/types';
import CollectionsNavigation from '../../components/navigation/';
export default defineComponent({
+ name: 'collections-browse',
components: { CollectionsNavigation },
props: {
collection: {
diff --git a/src/modules/collections/routes/detail/detail.vue b/src/modules/collections/routes/detail/detail.vue
index 28ce86340a..a9c0665622 100644
--- a/src/modules/collections/routes/detail/detail.vue
+++ b/src/modules/collections/routes/detail/detail.vue
@@ -19,6 +19,7 @@ import api from '@/api';
import CollectionsNavigation from '../../components/navigation/';
export default defineComponent({
+ name: 'collections-detail',
components: { CollectionsNavigation },
props: {
collection: {
diff --git a/src/modules/collections/routes/overview/overview.vue b/src/modules/collections/routes/overview/overview.vue
index e168c3d44a..4f3ea41d12 100644
--- a/src/modules/collections/routes/overview/overview.vue
+++ b/src/modules/collections/routes/overview/overview.vue
@@ -24,6 +24,7 @@ import useNavigation, { NavItem } from '../../compositions/use-navigation';
import router from '@/router';
export default defineComponent({
+ name: 'collections-overview',
components: {
CollectionsNavigation
},
diff --git a/src/router.ts b/src/router.ts
index a6488e5923..826c2c65e4 100644
--- a/src/router.ts
+++ b/src/router.ts
@@ -4,7 +4,8 @@ import { useProjectsStore } from '@/stores/projects';
import LoginRoute from '@/routes/login';
import ProjectChooserRoute from '@/routes/project-chooser';
import { checkAuth, logout } from '@/auth';
-import { hydrate } from '@/hydrate';
+import { hydrate, dehydrate } from '@/hydrate';
+import useAppStore from '@/stores/app';
export const onBeforeEnterProjectChooser: NavigationGuard = (to, from, next) => {
const projectsStore = useProjectsStore();
@@ -94,13 +95,10 @@ export function replaceRoutes(routeFilter: (routes: RouteConfig[]) => RouteConfi
export const onBeforeEach: NavigationGuard = async (to, from, next) => {
const projectsStore = useProjectsStore();
+ const appStore = useAppStore();
- // Only on first load is from.name null. On subsequent requests, from.name is undefined | string
- const firstLoad = from.name === null;
-
- // Before we do anything, we have to make sure we're aware of the projects that exist in the
- // platform. We can also use this to (async) register all the globally available modules
- if (firstLoad) {
+ // Make sure the projects store is aware of all projects that exist
+ if (projectsStore.state.projects === null) {
await projectsStore.getProjects();
}
@@ -112,26 +110,28 @@ export const onBeforeEach: NavigationGuard = async (to, from, next) => {
// Keep the projects store currentProjectKey in sync with the route
if (to.params.project && projectsStore.state.currentProjectKey !== to.params.project) {
+ // If the store is hydrated for the current project, make sure to dehydrate it
+ if (appStore.state.hydrated === true) {
+ await dehydrate();
+ }
+
const projectExists = await projectsStore.setCurrentProject(to.params.project);
+ // If the project you're trying to access doesn't exist, redirect to `/`
if (to.path !== '/' && projectExists === false) {
return next('/');
}
}
- // If you're trying to load a full URL (including) project, redirect to the login page of that
- // project if you're not logged in yet. Otherwise, redirect to the
- if (firstLoad) {
- const loggedIn = await checkAuth();
+ // The store can only be hydrated if you're an authenticated user. If the store is hydrated, we
+ // can safely assume you're logged in
+ if (appStore.state.hydrated === false) {
+ const authenticated = await checkAuth();
- if (loggedIn === true) {
+ if (authenticated === true) {
await hydrate();
- } else {
- if (to.meta?.public === true) {
- return next();
- } else {
- return next(`/${projectsStore.state.currentProjectKey}/login`);
- }
+ } else if (to.meta?.public !== true) {
+ return next(`/${to.params.project}/login`);
}
}
diff --git a/src/routes/login/components/login-form/login-form.vue b/src/routes/login/components/login-form/login-form.vue
index 686953d363..58c11c18c0 100644
--- a/src/routes/login/components/login-form/login-form.vue
+++ b/src/routes/login/components/login-form/login-form.vue
@@ -1,7 +1,20 @@
diff --git a/src/routes/login/components/project-error/index.ts b/src/routes/login/components/project-error/index.ts
new file mode 100644
index 0000000000..8ada6cf6db
--- /dev/null
+++ b/src/routes/login/components/project-error/index.ts
@@ -0,0 +1,4 @@
+import ProjectError from './project-error.vue';
+
+export { ProjectError };
+export default ProjectError;
diff --git a/src/routes/login/components/project-error/project-error.vue b/src/routes/login/components/project-error/project-error.vue
new file mode 100644
index 0000000000..5ef2b7b2e0
--- /dev/null
+++ b/src/routes/login/components/project-error/project-error.vue
@@ -0,0 +1,20 @@
+
+ [{{ props.status }}] {{ props.error }}
+
+
+
diff --git a/src/routes/login/login.vue b/src/routes/login/login.vue
index 9d15ee7692..1b439e544f 100644
--- a/src/routes/login/login.vue
+++ b/src/routes/login/login.vue
@@ -3,6 +3,11 @@
{{ $t('sign_in') }}
+
@@ -18,16 +23,21 @@ import { useUserStore } from '@/stores/user';
import { notEmpty } from '@/utils/is-empty';
import LoginForm from './components/login-form/';
import ContinueAs from './components/continue-as/';
+import ProjectError from './components/project-error/';
+import useProjectsStore from '../../stores/projects';
export default defineComponent({
- components: { LoginForm, ContinueAs },
+ components: { LoginForm, ContinueAs, ProjectError },
setup() {
const userStore = useUserStore();
+ const projectsStore = useProjectsStore();
+ const currentProject = projectsStore.currentProject;
+
const alreadyAuthenticated = computed(() =>
notEmpty(userStore.state.currentUser?.id)
);
- return { alreadyAuthenticated };
+ return { alreadyAuthenticated, currentProject };
}
});
diff --git a/src/routes/project-chooser/project-chooser.vue b/src/routes/project-chooser/project-chooser.vue
index 900923dd9f..3f88bc7967 100644
--- a/src/routes/project-chooser/project-chooser.vue
+++ b/src/routes/project-chooser/project-chooser.vue
@@ -1,24 +1,39 @@
-
- This is where you would be asked to choose the project you're trying to login to, or
- enter the project key if the project is private
-
+ {{ $t('choose_project') }}
+
+
+ {{ (project.api && project.api.project_name) || project.key }}
+
diff --git a/src/stores/projects/projects.test.ts b/src/stores/projects/projects.test.ts
index 43130e0235..21e6459db8 100644
--- a/src/stores/projects/projects.test.ts
+++ b/src/stores/projects/projects.test.ts
@@ -67,7 +67,7 @@ describe('Stores / Projects', () => {
.mockImplementation(() => Promise.resolve({ data: { data: {} } }));
await projectsStore.setCurrentProject('my-project');
expect(spy).toHaveBeenCalledWith('/my-project/');
- expect(projectsStore.state.projects[0]).toEqual({ key: 'my-project' });
+ expect(projectsStore.state.projects?.[0]).toEqual({ key: 'my-project' });
});
it('Returns true if the project exists', async () => {
diff --git a/src/stores/projects/projects.ts b/src/stores/projects/projects.ts
index a6e1b426fa..615c78293e 100644
--- a/src/stores/projects/projects.ts
+++ b/src/stores/projects/projects.ts
@@ -12,12 +12,12 @@ export const useProjectsStore = createStore({
state: () => ({
needsInstall: false,
error: null as LoadingError,
- projects: [] as Projects,
+ projects: null as Projects | null,
currentProjectKey: null as string | null
}),
getters: {
currentProject: (state): ProjectWithKey | ProjectError | null => {
- return state.projects.find(({ key }) => key === state.currentProjectKey) || null;
+ return state.projects?.find(({ key }) => key === state.currentProjectKey) || null;
}
},
actions: {
@@ -29,7 +29,8 @@ export const useProjectsStore = createStore({
* Returns a boolean if the operation succeeded or not.
*/
async setCurrentProject(key: string): Promise {
- const projectKeys = this.state.projects.map(project => project.key);
+ const projects = this.state.projects || ([] as Projects);
+ const projectKeys = projects.map(project => project.key);
if (projectKeys.includes(key) === false) {
try {
@@ -38,7 +39,7 @@ export const useProjectsStore = createStore({
key: key,
...projectInfoResponse.data.data
};
- this.state.projects = [...this.state.projects, project];
+ this.state.projects = [...projects, project];
} catch {
return false;
}
diff --git a/src/views/private/components/project-chooser/index.ts b/src/views/private/components/project-chooser/index.ts
new file mode 100644
index 0000000000..02de24a344
--- /dev/null
+++ b/src/views/private/components/project-chooser/index.ts
@@ -0,0 +1,4 @@
+import ProjectChooser from './project-chooser.vue';
+
+export { ProjectChooser };
+export default ProjectChooser;
diff --git a/src/views/private/components/project-chooser/project-chooser.vue b/src/views/private/components/project-chooser/project-chooser.vue
new file mode 100644
index 0000000000..ca4faa2bc4
--- /dev/null
+++ b/src/views/private/components/project-chooser/project-chooser.vue
@@ -0,0 +1,65 @@
+
+
+ {{ currentProjectKey }}
+
+
+
+
+
+
+
diff --git a/src/views/private/private-view.test.ts b/src/views/private/private-view.test.ts
index 8e60ddb1ac..032c6abe29 100644
--- a/src/views/private/private-view.test.ts
+++ b/src/views/private/private-view.test.ts
@@ -2,10 +2,12 @@ import { createLocalVue, shallowMount } from '@vue/test-utils';
import VueCompositionAPI from '@vue/composition-api';
import PrivateView from './private-view.vue';
import VOverlay from '@/components/v-overlay';
+import VProgressCircular from '@/components/v-progress/circular';
const localVue = createLocalVue();
localVue.use(VueCompositionAPI);
localVue.component('v-overlay', VOverlay);
+localVue.component('v-progress-circular', VProgressCircular);
describe('Views / Private', () => {
it('Adds the is-open class to the nav', async () => {
diff --git a/src/views/private/private-view.vue b/src/views/private/private-view.vue
index 1260050eb4..beed7af781 100644
--- a/src/views/private/private-view.vue
+++ b/src/views/private/private-view.vue
@@ -8,11 +8,8 @@
>
-
- PROJECT CHOOSER
-
+
+
@@ -58,13 +55,15 @@ import { defineComponent, ref, provide, computed } from '@vue/composition-api';
import ModuleBar from './components/module-bar/';
import DrawerDetailGroup from './components/drawer-detail-group/';
import HeaderBar from './components/header-bar';
+import ProjectChooser from './components/project-chooser';
import { throttle } from 'lodash';
export default defineComponent({
components: {
ModuleBar,
DrawerDetailGroup,
- HeaderBar
+ HeaderBar,
+ ProjectChooser
},
props: {
title: {