web/satellite/vuetify-poc: add project ID to project-specific routes

A project ID path parameter has been incorporated into project-specific
routes. Previously, project IDs were stored only in local storage,
which made it impossible to bookmark or share links to pages within a
particular project.

Resolves #6076

Change-Id: I4f46f58a95dfc9b67d95fdb1c6a65b50fcafe02b
This commit is contained in:
Jeremy Wharton 2023-07-23 01:48:31 -05:00 committed by Storj Robot
parent 5a03e29fca
commit 28711e30ad
9 changed files with 117 additions and 102 deletions

View File

@ -98,7 +98,6 @@ import { ProjectItemModel, PROJECT_ROLE_COLORS } from '@poc/types/projects';
import { ProjectInvitationResponse } from '@/types/projects'; import { ProjectInvitationResponse } from '@/types/projects';
import { ProjectRole } from '@/types/projectMembers'; import { ProjectRole } from '@/types/projectMembers';
import { useProjectsStore } from '@/store/modules/projectsStore'; import { useProjectsStore } from '@/store/modules/projectsStore';
import { LocalData } from '@/utils/localData';
import IconProject from '@poc/components/icons/IconProject.vue'; import IconProject from '@poc/components/icons/IconProject.vue';
import IconSettings from '@poc/components/icons/IconSettings.vue'; import IconSettings from '@poc/components/icons/IconSettings.vue';
@ -123,8 +122,7 @@ const isDeclining = ref<boolean>(false);
function openProject(): void { function openProject(): void {
if (!props.item) return; if (!props.item) return;
projectsStore.selectProject(props.item.id); projectsStore.selectProject(props.item.id);
LocalData.setSelectedProjectId(props.item.id); router.push(`/projects/${props.item.id}/dashboard`);
router.push('/dashboard');
} }
/** /**

View File

@ -130,7 +130,6 @@ import { ProjectInvitationResponse } from '@/types/projects';
import { ProjectRole } from '@/types/projectMembers'; import { ProjectRole } from '@/types/projectMembers';
import { SHORT_MONTHS_NAMES } from '@/utils/constants/date'; import { SHORT_MONTHS_NAMES } from '@/utils/constants/date';
import { useProjectsStore } from '@/store/modules/projectsStore'; import { useProjectsStore } from '@/store/modules/projectsStore';
import { LocalData } from '@/utils/localData';
import IconSettings from '@poc/components/icons/IconSettings.vue'; import IconSettings from '@poc/components/icons/IconSettings.vue';
import IconTeam from '@poc/components/icons/IconTeam.vue'; import IconTeam from '@poc/components/icons/IconTeam.vue';
@ -170,8 +169,7 @@ function getFormattedDate(date: Date): string {
*/ */
function openProject(item: ProjectItemModel): void { function openProject(item: ProjectItemModel): void {
projectsStore.selectProject(item.id); projectsStore.selectProject(item.id);
LocalData.setSelectedProjectId(item.id); router.push(`/projects/${item.id}/dashboard`);
router.push('/dashboard');
} }
/** /**

View File

@ -77,7 +77,6 @@ import {
import { ProjectInvitationResponse } from '@/types/projects'; import { ProjectInvitationResponse } from '@/types/projects';
import { useProjectsStore } from '@/store/modules/projectsStore'; import { useProjectsStore } from '@/store/modules/projectsStore';
import { LocalData } from '@/utils/localData';
const props = defineProps<{ const props = defineProps<{
modelValue: boolean, modelValue: boolean,
@ -105,8 +104,7 @@ const isDeclining = ref<boolean>(false);
*/ */
function openProject(): void { function openProject(): void {
projectsStore.selectProject(props.id); projectsStore.selectProject(props.id);
LocalData.setSelectedProjectId(props.id); router.push(`/projects/${props.id}/dashboard`);
router.push('/dashboard');
} }
/** /**

View File

@ -5,20 +5,21 @@
<v-navigation-drawer class="py-1"> <v-navigation-drawer class="py-1">
<v-sheet> <v-sheet>
<v-list class="px-2" color="default" variant="flat"> <v-list class="px-2" color="default" variant="flat">
<!-- Project --> <template v-if="pathBeforeAccountPage">
<v-list-item class="pa-4 rounded-lg" link router-link to="/dashboard"> <v-list-item class="pa-4 rounded-lg" link router-link :to="pathBeforeAccountPage">
<template #prepend> <template #prepend>
<img src="@poc/assets/icon-back-tonal.svg" alt="Project"> <img src="@poc/assets/icon-back-tonal.svg" alt="Project">
<!-- <svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> <!-- <svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10.0567 1.05318L10.0849 1.06832L17.4768 5.33727C17.6249 5.42279 17.7084 5.5642 17.7274 5.71277L17.7301 5.73906L17.7314 5.76543L17.7316 14.2407C17.7316 14.4124 17.6452 14.5718 17.5032 14.6657L17.476 14.6826L10.084 18.9322C9.93533 19.0176 9.75438 19.0223 9.60224 18.9463L9.57407 18.9311L2.25387 14.6815C2.10601 14.5956 2.01167 14.4418 2.00108 14.2726L2 14.2407V5.77863L2.00023 5.75974C1.99504 5.58748 2.07759 5.41781 2.22993 5.31793L2.25457 5.30275L9.57482 1.06849C9.71403 0.987969 9.88182 0.978444 10.0278 1.03993L10.0567 1.05318ZM16.7123 6.6615L10.3397 10.3418V17.6094L16.7123 13.9458V6.6615ZM3.01944 6.66573V13.947L9.32037 17.605V10.3402L3.01944 6.66573ZM9.83034 2.09828L3.49475 5.76287L9.83122 9.45833L16.2029 5.7786L9.83034 2.09828Z" fill="currentColor"/> <path d="M10.0567 1.05318L10.0849 1.06832L17.4768 5.33727C17.6249 5.42279 17.7084 5.5642 17.7274 5.71277L17.7301 5.73906L17.7314 5.76543L17.7316 14.2407C17.7316 14.4124 17.6452 14.5718 17.5032 14.6657L17.476 14.6826L10.084 18.9322C9.93533 19.0176 9.75438 19.0223 9.60224 18.9463L9.57407 18.9311L2.25387 14.6815C2.10601 14.5956 2.01167 14.4418 2.00108 14.2726L2 14.2407V5.77863L2.00023 5.75974C1.99504 5.58748 2.07759 5.41781 2.22993 5.31793L2.25457 5.30275L9.57482 1.06849C9.71403 0.987969 9.88182 0.978444 10.0278 1.03993L10.0567 1.05318ZM16.7123 6.6615L10.3397 10.3418V17.6094L16.7123 13.9458V6.6615ZM3.01944 6.66573V13.947L9.32037 17.605V10.3402L3.01944 6.66573ZM9.83034 2.09828L3.49475 5.76287L9.83122 9.45833L16.2029 5.7786L9.83034 2.09828Z" fill="currentColor"/>
</svg> --> </svg> -->
</template> </template>
<v-list-item-title link class="text-body-2 ml-3"> <v-list-item-title link class="text-body-2 ml-3">
Go back Go back
</v-list-item-title> </v-list-item-title>
</v-list-item> </v-list-item>
<v-divider class="my-2" /> <v-divider class="my-2" />
</template>
<!-- All Projects --> <!-- All Projects -->
<v-list-item class="pa-4 rounded-lg" link router-link to="/projects"> <v-list-item class="pa-4 rounded-lg" link router-link to="/projects">
@ -39,7 +40,7 @@
</v-list-item-title> </v-list-item-title>
</v-list-item> </v-list-item>
<v-list-item link router-link to="/account-settings" class="my-1 py-3" rounded="lg"> <v-list-item link router-link to="settings" class="my-1 py-3" rounded="lg">
<template #prepend> <template #prepend>
<!-- <img src="@poc/assets/icon-settings.svg" alt="Account Settings" class="mr-3"> --> <!-- <img src="@poc/assets/icon-settings.svg" alt="Account Settings" class="mr-3"> -->
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
@ -51,7 +52,7 @@
</v-list-item-title> </v-list-item-title>
</v-list-item> </v-list-item>
<v-list-item link router-link to="/billing" class="my-1" rounded="lg"> <v-list-item link router-link to="billing" class="my-1" rounded="lg">
<template #prepend> <template #prepend>
<!-- <img src="@poc/assets/icon-card.svg" alt="Billing" class="mr-3"> --> <!-- <img src="@poc/assets/icon-card.svg" alt="Billing" class="mr-3"> -->
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
@ -70,6 +71,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed } from 'vue';
import { import {
VNavigationDrawer, VNavigationDrawer,
VSheet, VSheet,
@ -78,4 +80,17 @@ import {
VListItemTitle, VListItemTitle,
VDivider, VDivider,
} from 'vuetify/components'; } from 'vuetify/components';
import { useAppStore } from '@poc/store/appStore';
const appStore = useAppStore();
/**
* Returns the path to the most recent non-account-related page.
*/
const pathBeforeAccountPage = computed((): string | null => {
const path = appStore.state.pathBeforeAccountPage;
if (!path || path === '/projects') return null;
return path;
});
</script> </script>

View File

@ -107,7 +107,7 @@
</v-list-item-title> </v-list-item-title>
</v-list-item> </v-list-item>
<v-list-item link class="my-1 rounded-lg" router-link to="/billing"> <v-list-item link class="my-1 rounded-lg" router-link to="/account/billing">
<template #prepend> <template #prepend>
<img src="@poc/assets/icon-card.svg" alt="Billing"> <img src="@poc/assets/icon-card.svg" alt="Billing">
</template> </template>
@ -116,7 +116,7 @@
</v-list-item-title> </v-list-item-title>
</v-list-item> </v-list-item>
<v-list-item link class="my-1 rounded-lg" router-link to="/account-settings"> <v-list-item link class="my-1 rounded-lg" router-link to="/account/settings">
<template #prepend> <template #prepend>
<img src="@poc/assets/icon-settings.svg" alt="Account Settings"> <img src="@poc/assets/icon-settings.svg" alt="Account Settings">
</template> </template>

View File

@ -3,16 +3,21 @@
<template> <template>
<v-app> <v-app>
<default-bar show-nav-drawer-button /> <div v-if="isLoading" class="d-flex align-center justify-center w-100 h-100">
<ProjectNav v-if="appStore.state.isNavigationDrawerShown" /> <v-progress-circular color="primary" indeterminate size="64" />
<default-view /> </div>
<template v-else>
<default-bar show-nav-drawer-button />
<ProjectNav v-if="appStore.state.isNavigationDrawerShown" />
<default-view />
</template>
</v-app> </v-app>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { onBeforeMount } from 'vue'; import { onBeforeMount, ref, watch } from 'vue';
import { onBeforeRouteLeave, useRouter } from 'vue-router'; import { useRoute, useRouter } from 'vue-router';
import { VApp } from 'vuetify/components'; import { VApp, VProgressCircular } from 'vuetify/components';
import DefaultBar from './AppBar.vue'; import DefaultBar from './AppBar.vue';
import ProjectNav from './ProjectNav.vue'; import ProjectNav from './ProjectNav.vue';
@ -24,10 +29,10 @@ import { useBillingStore } from '@/store/modules/billingStore';
import { useUsersStore } from '@/store/modules/usersStore'; import { useUsersStore } from '@/store/modules/usersStore';
import { useABTestingStore } from '@/store/modules/abTestingStore'; import { useABTestingStore } from '@/store/modules/abTestingStore';
import { useProjectsStore } from '@/store/modules/projectsStore'; import { useProjectsStore } from '@/store/modules/projectsStore';
import { LocalData } from '@/utils/localData';
import { useAppStore } from '@poc/store/appStore'; import { useAppStore } from '@poc/store/appStore';
const router = useRouter(); const router = useRouter();
const route = useRoute();
const billingStore = useBillingStore(); const billingStore = useBillingStore();
const usersStore = useUsersStore(); const usersStore = useUsersStore();
@ -35,33 +40,33 @@ const abTestingStore = useABTestingStore();
const projectsStore = useProjectsStore(); const projectsStore = useProjectsStore();
const appStore = useAppStore(); const appStore = useAppStore();
/** const isLoading = ref<boolean>(true);
* Stores project to vuex store and browser's local storage.
* @param projectID - project id string
*/
function storeProject(projectID: string): void {
projectsStore.selectProject(projectID);
LocalData.setSelectedProjectId(projectID);
}
/** /**
* Checks if stored project is in fetched projects array and selects it. * Selects the project with the given ID, redirecting to the
* Selects first fetched project if check is not successful. * all projects dashboard if no such project exists.
* @param fetchedProjects - fetched projects array
*/ */
function selectProject(fetchedProjects: Project[]): void { async function selectProject(projectId: string): Promise<void> {
const storedProjectID = LocalData.getSelectedProjectId(); isLoading.value = true;
const isProjectInFetchedProjects = fetchedProjects.some(project => project.id === storedProjectID);
if (storedProjectID && isProjectInFetchedProjects) {
storeProject(storedProjectID);
let projects: Project[];
try {
projects = await projectsStore.getProjects();
} catch (_) {
router.push('/projects');
return; return;
} }
if (!projects.some(p => p.id === projectId)) {
router.push('/projects');
return;
}
projectsStore.selectProject(projectId);
// Length of fetchedProjects array is checked before selectProject() function call. isLoading.value = false;
storeProject(fetchedProjects[0].id);
} }
watch(() => route.params.projectId, async newId => selectProject(newId as string));
/** /**
* Lifecycle hook after initial render. * Lifecycle hook after initial render.
* Pre fetches user`s and project information. * Pre fetches user`s and project information.
@ -87,16 +92,6 @@ onBeforeMount(async () => {
await billingStore.getCreditCards(); await billingStore.getCreditCards();
} catch (error) { /* empty */ } } catch (error) { /* empty */ }
let projects: Project[] = []; selectProject(route.params.projectId as string);
try {
projects = await projectsStore.getProjects();
} catch (error) {
return;
}
if (projects.length) {
selectProject(projects);
}
}); });
</script> </script>

View File

@ -117,7 +117,7 @@
<v-divider class="my-2" /> <v-divider class="my-2" />
<v-list-item link router-link to="/dashboard" class="my-1 py-3" rounded="lg"> <v-list-item link router-link to="dashboard" class="my-1 py-3" rounded="lg">
<template #prepend> <template #prepend>
<IconDashboard /> <IconDashboard />
</template> </template>
@ -126,7 +126,7 @@
</v-list-item-title> </v-list-item-title>
</v-list-item> </v-list-item>
<v-list-item link router-link to="/buckets" class="my-1" rounded="lg"> <v-list-item link router-link to="buckets" class="my-1" rounded="lg">
<template #prepend> <template #prepend>
<IconBucket /> <IconBucket />
</template> </template>
@ -135,7 +135,7 @@
</v-list-item-title> </v-list-item-title>
</v-list-item> </v-list-item>
<v-list-item link router-link to="/bucket" class="my-1" rounded="lg"> <v-list-item link router-link to="bucket" class="my-1" rounded="lg">
<template #prepend> <template #prepend>
<IconBrowse /> <IconBrowse />
</template> </template>
@ -144,7 +144,7 @@
</v-list-item-title> </v-list-item-title>
</v-list-item> </v-list-item>
<v-list-item link router-link to="/access" class="my-1" rounded="lg"> <v-list-item link router-link to="access" class="my-1" rounded="lg">
<template #prepend> <template #prepend>
<IconAccess /> <IconAccess />
</template> </template>
@ -153,7 +153,7 @@
</v-list-item-title> </v-list-item-title>
</v-list-item> </v-list-item>
<v-list-item link router-link to="/team" class="my-1" rounded="lg"> <v-list-item link router-link to="team" class="my-1" rounded="lg">
<template #prepend> <template #prepend>
<IconTeam /> <IconTeam />
</template> </template>

View File

@ -1,58 +1,32 @@
// Copyright (C) 2023 Storj Labs, Inc. // Copyright (C) 2023 Storj Labs, Inc.
// See LICENSE for copying information. // See LICENSE for copying information.
// Composables import { RouteRecordRaw, createRouter, createWebHistory } from 'vue-router';
import { createRouter, createWebHistory } from 'vue-router';
const routes = [ import { useAppStore } from '@poc/store/appStore';
const routes: RouteRecordRaw[] = [
{ {
path: '/vuetifypoc', path: '/vuetifypoc',
redirect: { path: '/projects' }, // redirect redirect: { path: '/projects' }, // redirect
component: () => import('@poc/layouts/default/Default.vue'),
children: [
{
path: '/dashboard',
name: 'Dashboard',
component: () => import(/* webpackChunkName: "home" */ '@poc/views/Dashboard.vue'),
},
{
path: '/buckets',
name: 'Buckets',
component: () => import(/* webpackChunkName: "Buckets" */ '@poc/views/Buckets.vue'),
},
{
path: '/bucket',
name: 'Bucket',
component: () => import(/* webpackChunkName: "Buckets" */ '@poc/views/Bucket.vue'),
},
{
path: '/access',
name: 'Access',
component: () => import(/* webpackChunkName: "Access" */ '@poc/views/Access.vue'),
},
{
path: '/team',
name: 'Team',
component: () => import(/* webpackChunkName: "Team" */ '@poc/views/Team.vue'),
},
],
}, },
{ {
path: '/account', path: '/account',
component: () => import('@poc/layouts/default/Account.vue'), component: () => import('@poc/layouts/default/Account.vue'),
beforeEnter: (_, from) => useAppStore().setPathBeforeAccountPage(from.path),
children: [ children: [
{ {
path: '/billing', path: 'billing',
name: 'Billing', name: 'Billing',
component: () => import(/* webpackChunkName: "Billing" */ '@poc/views/Billing.vue'), component: () => import(/* webpackChunkName: "Billing" */ '@poc/views/Billing.vue'),
}, },
{ {
path: '/account-settings', path: 'settings',
name: 'Account Settings', name: 'Account Settings',
component: () => import(/* webpackChunkName: "MyAccount" */ '@poc/views/AccountSettings.vue'), component: () => import(/* webpackChunkName: "MyAccount" */ '@poc/views/AccountSettings.vue'),
}, },
{ {
path: '/design-library', path: 'design-library',
name: 'Design Library', name: 'Design Library',
component: () => import(/* webpackChunkName: "DesignLibrary" */ '@poc/views/DesignLibrary.vue'), component: () => import(/* webpackChunkName: "DesignLibrary" */ '@poc/views/DesignLibrary.vue'),
}, },
@ -63,12 +37,43 @@ const routes = [
component: () => import('@poc/layouts/default/AllProjects.vue'), component: () => import('@poc/layouts/default/AllProjects.vue'),
children: [ children: [
{ {
path: '/projects', path: '',
name: 'Projects', name: 'Projects',
component: () => import(/* webpackChunkName: "Projects" */ '@poc/views/Projects.vue'), component: () => import(/* webpackChunkName: "Projects" */ '@poc/views/Projects.vue'),
}, },
], ],
}, },
{
path: '/projects/:projectId',
component: () => import('@poc/layouts/default/Default.vue'),
children: [
{
path: 'dashboard',
name: 'Dashboard',
component: () => import(/* webpackChunkName: "home" */ '@poc/views/Dashboard.vue'),
},
{
path: 'buckets',
name: 'Buckets',
component: () => import(/* webpackChunkName: "Buckets" */ '@poc/views/Buckets.vue'),
},
{
path: 'bucket',
name: 'Bucket',
component: () => import(/* webpackChunkName: "Buckets" */ '@poc/views/Bucket.vue'),
},
{
path: 'access',
name: 'Access',
component: () => import(/* webpackChunkName: "Access" */ '@poc/views/Access.vue'),
},
{
path: 'team',
name: 'Team',
component: () => import(/* webpackChunkName: "Team" */ '@poc/views/Team.vue'),
},
],
},
]; ];
const router = createRouter({ const router = createRouter({

View File

@ -6,6 +6,7 @@ import { reactive } from 'vue';
class AppState { class AppState {
public isNavigationDrawerShown = true; public isNavigationDrawerShown = true;
public pathBeforeAccountPage: string | null = null;
} }
export const useAppStore = defineStore('vuetifyApp', () => { export const useAppStore = defineStore('vuetifyApp', () => {
@ -15,8 +16,13 @@ export const useAppStore = defineStore('vuetifyApp', () => {
state.isNavigationDrawerShown = !state.isNavigationDrawerShown; state.isNavigationDrawerShown = !state.isNavigationDrawerShown;
} }
function setPathBeforeAccountPage(path: string) {
state.pathBeforeAccountPage = path;
}
return { return {
state, state,
toggleNavigationDrawer, toggleNavigationDrawer,
setPathBeforeAccountPage,
}; };
}); });