web/satellite: add projects table to all projects dashboard

This change adds a tableview to all projects dashboard. It also
addresses some minor UI issues.

Issues: https://github.com/storj/storj/issues/5925
https://github.com/storj/storj/issues/5924
https://github.com/storj/storj/issues/5923

Change-Id: Ic18566b192005bb1720e0388352bc82a3739a723
This commit is contained in:
Wilfred Asomani 2023-06-05 23:56:40 +00:00
parent 7422fe393b
commit 6ca9f6d2c6
14 changed files with 543 additions and 112 deletions

View File

@ -61,6 +61,7 @@ export class ProjectsApiGql extends BaseGql implements ProjectsApi {
publicId publicId
description description
createdAt createdAt
memberCount
ownerId ownerId
} }
}`; }`;
@ -74,6 +75,8 @@ export class ProjectsApiGql extends BaseGql implements ProjectsApi {
project.description, project.description,
project.createdAt, project.createdAt,
project.ownerId, project.ownerId,
false,
project.memberCount,
); );
}); });
} }

View File

@ -18,12 +18,16 @@
<p v-for="str in val" :key="str" class="array-val">{{ str }}</p> <p v-for="str in val" :key="str" class="array-val">{{ str }}</p>
</div> </div>
<div v-else class="table-item"> <div v-else class="table-item">
<div v-if="icon && index === 0" class="item-icon file-background"> <div v-if="icon && index === 0" class="item-icon file-background" :class="customIconClasses">
<component :is="icon" /> <component :is="icon" />
</div> </div>
<p :class="{primary: index === 0}" :title="val" @click.stop="(e) => cellContentClicked(index, e)"> <p v-if="keyVal === 'multi'" class="multi" :class="{primary: index === 0}" :title="val['title']" @click.stop="(e) => cellContentClicked(index, e)">
<span class="multi__title">{{ val['title'] }}</span>
<span class="multi__subtitle">{{ val['subtitle'] }}</span>
</p>
<p v-else :class="{primary: index === 0}" :title="val" @click.stop="(e) => cellContentClicked(index, e)">
<middle-truncate v-if="keyVal === 'fileName'" :text="val" /> <middle-truncate v-if="keyVal === 'fileName'" :text="val" />
<project-ownership-tag v-else-if="keyVal === 'owner'" no-icon :is-owner="val" /> <project-ownership-tag v-else-if="keyVal === 'owner'" :no-icon="itemType !== 'project'" :is-owner="val" />
<span v-else>{{ val }}</span> <span v-else>{{ val }}</span>
</p> </p>
<div v-if="showBucketGuide(index)" class="animation"> <div v-if="showBucketGuide(index)" class="animation">
@ -56,6 +60,7 @@ import PdfIcon from '@/../static/images/objects/pdf.svg';
import PictureIcon from '@/../static/images/objects/picture.svg'; import PictureIcon from '@/../static/images/objects/picture.svg';
import TxtIcon from '@/../static/images/objects/txt.svg'; import TxtIcon from '@/../static/images/objects/txt.svg';
import ZipIcon from '@/../static/images/objects/zip.svg'; import ZipIcon from '@/../static/images/objects/zip.svg';
import ProjectIcon from '@/../static/images/navigation/project.svg';
const props = withDefaults(defineProps<{ const props = withDefaults(defineProps<{
selectDisabled?: boolean; selectDisabled?: boolean;
@ -97,10 +102,23 @@ const icons = new Map<string, string>([
['image', PictureIcon], ['image', PictureIcon],
['text', TxtIcon], ['text', TxtIcon],
['archive', ZipIcon], ['archive', ZipIcon],
['project', ProjectIcon],
]); ]);
const icon = computed(() => icons.get(props.itemType.toLowerCase())); const icon = computed(() => icons.get(props.itemType.toLowerCase()));
const customIconClasses = computed(() => {
const classes = {};
if (props.itemType === 'project') {
if (props.item['owner']) {
classes['project-owner'] = true;
} else {
classes['project-member'] = true;
}
}
return classes;
});
function selectClicked(event: Event): void { function selectClicked(event: Event): void {
emit('selectClicked', event); emit('selectClicked', event);
} }
@ -188,6 +206,10 @@ function cellContentClicked(cellIndex: number, event: Event) {
.primary { .primary {
color: var(--c-blue-3); color: var(--c-blue-3);
& > .multi__subtitle {
color: var(--c-blue-3);
}
} }
} }
} }
@ -201,6 +223,18 @@ function cellContentClicked(cellIndex: number, event: Event) {
} }
} }
.multi {
display: flex;
flex-direction: column;
&__subtitle {
font-family: 'font_regular', sans-serif;
font-size: 12px;
line-height: 20px;
color: var(--c-grey-6);
}
}
.few-items { .few-items {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -244,4 +278,18 @@ function cellContentClicked(cellIndex: number, event: Event) {
align-items: center; align-items: center;
justify-content: center; justify-content: center;
} }
.project-owner {
:deep(path) {
fill: var(--c-purple-4);
}
}
.project-member {
:deep(path) {
fill: var(--c-yellow-5);
}
}
</style> </style>

View File

@ -0,0 +1,67 @@
// Copyright (C) 2023 Storj Labs, Inc.
// See LICENSE for copying information.
<template>
<div class="chip" :class="{selected: isSelected}" @click="emits('select')">
<component :is="icon" v-if="icon" class="chip__icon" />
<span class="chip__text"> {{ label }} </span>
</div>
</template>
<script setup lang="ts">
import { Component } from 'vue';
const props = withDefaults(defineProps<{
label: string,
isSelected?: boolean,
icon?: string,
}>(), {
isSelected: false,
icon: undefined,
label: '',
});
const emits = defineEmits(['select']);
</script>
<style scoped lang="scss">
.chip {
display: flex;
justify-content: space-between;
align-items: center;
gap: 2px;
padding: 0 8px;
border-radius: 24px;
color: var(--c-grey-6);
background: var(--c-white);
cursor: pointer;
:deep(path) {
fill: var(--c-grey-6);
}
:deep(rect) {
fill: var(--c-grey-6);
}
&__text {
font-family: 'font_regular', sans-serif;
font-size: 12px;
line-height: 20px;
font-weight: 700;
text-transform: uppercase;
}
&.selected {
color: var(--c-black);
background: var(--c-grey-2);
:deep(path),
:deep(rect) {
fill: var(--c-black);
}
}
}
</style>

View File

@ -13,7 +13,6 @@ import { LimitToChange } from '@/types/projects';
class AppState { class AppState {
public fetchState = FetchState.LOADING; public fetchState = FetchState.LOADING;
public isSuccessfulPasswordResetShown = false; public isSuccessfulPasswordResetShown = false;
public isUpdateSessionTimeoutBanner = !LocalData.getSessionTimeoutBannerAcknowledged();
public hasJustLoggedIn = false; public hasJustLoggedIn = false;
public onbAGStepBackRoute = ''; public onbAGStepBackRoute = '';
public onbAPIKeyStepBackRoute = ''; public onbAPIKeyStepBackRoute = '';
@ -35,6 +34,7 @@ class AppState {
public isLargeUploadNotificationShown = true; public isLargeUploadNotificationShown = true;
public isLargeUploadWarningNotificationShown = false; public isLargeUploadWarningNotificationShown = false;
public activeChangeLimit: LimitToChange = LimitToChange.Storage; public activeChangeLimit: LimitToChange = LimitToChange.Storage;
public isProjectTableViewEnabled = LocalData.getProjectTableViewEnabled();
} }
class ErrorPageState { class ErrorPageState {
@ -85,6 +85,19 @@ export const useAppStore = defineStore('app', () => {
state.hasJustLoggedIn = hasJustLoggedIn; state.hasJustLoggedIn = hasJustLoggedIn;
} }
function hasProjectTableViewConfigured(): boolean {
return LocalData.hasProjectTableViewConfigured();
}
function toggleProjectTableViewEnabled(isProjectTableViewEnabled: boolean | null = null): void {
if (isProjectTableViewEnabled === null) {
state.isProjectTableViewEnabled = !state.isProjectTableViewEnabled;
} else {
state.isProjectTableViewEnabled = isProjectTableViewEnabled;
}
LocalData.setProjectTableViewEnabled(state.isProjectTableViewEnabled);
}
function changeState(newFetchState: FetchState): void { function changeState(newFetchState: FetchState): void {
state.fetchState = newFetchState; state.fetchState = newFetchState;
} }
@ -141,12 +154,6 @@ export const useAppStore = defineStore('app', () => {
state.isGalleryView = value; state.isGalleryView = value;
} }
function closeUpdateSessionTimeoutBanner(): void {
LocalData.setSessionTimeoutBannerAcknowledged();
state.isUpdateSessionTimeoutBanner = false;
}
function closeDropdowns(): void { function closeDropdowns(): void {
state.activeDropdown = ''; state.activeDropdown = '';
} }
@ -177,12 +184,16 @@ export const useAppStore = defineStore('app', () => {
state.isUploadingModal = false; state.isUploadingModal = false;
state.error.visible = false; state.error.visible = false;
state.isGalleryView = false; state.isGalleryView = false;
state.isProjectTableViewEnabled = false;
LocalData.removeProjectTableViewConfig();
} }
return { return {
state, state,
toggleActiveDropdown, toggleActiveDropdown,
toggleSuccessfulPasswordReset, toggleSuccessfulPasswordReset,
toggleProjectTableViewEnabled,
hasProjectTableViewConfigured,
updateActiveModal, updateActiveModal,
removeActiveModal, removeActiveModal,
toggleHasJustLoggedIn, toggleHasJustLoggedIn,
@ -201,7 +212,6 @@ export const useAppStore = defineStore('app', () => {
setLargeUploadWarningNotification, setLargeUploadWarningNotification,
setLargeUploadNotification, setLargeUploadNotification,
closeDropdowns, closeDropdowns,
closeUpdateSessionTimeoutBanner,
setErrorPage, setErrorPage,
removeErrorPage, removeErrorPage,
clear, clear,

View File

@ -15,6 +15,7 @@ export class LocalData {
private static largeUploadNotificationDismissed = 'largeUploadNotificationDismissed'; private static largeUploadNotificationDismissed = 'largeUploadNotificationDismissed';
private static sessionExpirationDate = 'sessionExpirationDate'; private static sessionExpirationDate = 'sessionExpirationDate';
private static projectLimitBannerHidden = 'projectLimitBannerHidden'; private static projectLimitBannerHidden = 'projectLimitBannerHidden';
private static projectTableViewEnabled = 'projectTableViewEnabled';
public static getSelectedProjectId(): string | null { public static getSelectedProjectId(): string | null {
return localStorage.getItem(LocalData.selectedProjectId); return localStorage.getItem(LocalData.selectedProjectId);
@ -62,14 +63,6 @@ export class LocalData {
return value === 'true'; return value === 'true';
} }
public static getSessionTimeoutBannerAcknowledged(): boolean {
return Boolean(localStorage.getItem(LocalData.sessionTimeoutBannerAcknowledged));
}
public static setSessionTimeoutBannerAcknowledged(): void {
localStorage.setItem(LocalData.sessionTimeoutBannerAcknowledged, 'true');
}
/** /**
* "Disable" showing the server-side encryption banner on the bucket page * "Disable" showing the server-side encryption banner on the bucket page
*/ */
@ -122,8 +115,26 @@ export class LocalData {
localStorage.setItem(LocalData.projectLimitBannerHidden, 'true'); localStorage.setItem(LocalData.projectLimitBannerHidden, 'true');
} }
public static getProjectLimitBannerHidden(): boolean { public static getProjectTableViewEnabled(): boolean {
const value = localStorage.getItem(LocalData.projectLimitBannerHidden); const value = localStorage.getItem(LocalData.projectTableViewEnabled);
return value === 'true'; return value === 'true';
} }
public static setProjectTableViewEnabled(enabled: boolean): void {
localStorage.setItem(LocalData.projectTableViewEnabled, enabled.toString());
}
/*
* Whether a user defined setting has been made for the projects table
* */
public static hasProjectTableViewConfigured(): boolean {
return localStorage.getItem(LocalData.projectTableViewEnabled) !== null;
}
/*
* Remove the user defined setting for the projects table;
* */
public static removeProjectTableViewConfig() {
return localStorage.removeItem(LocalData.projectTableViewEnabled);
}
} }

View File

@ -18,22 +18,11 @@
<div class="dashboard__wrap__main-area__content-wrap__container__content banners"> <div class="dashboard__wrap__main-area__content-wrap__container__content banners">
<ProjectInvitationBanner v-if="isProjectInvitationBannerShown" /> <ProjectInvitationBanner v-if="isProjectInvitationBannerShown" />
<UpdateSessionTimeoutBanner
v-if="isUpdateSessionTimeoutBanner && dashboardContent"
:dashboard-ref="dashboardContent"
/>
<UpgradeNotification <UpgradeNotification
v-if="isPaidTierBannerShown" v-if="isPaidTierBannerShown"
:open-add-p-m-modal="togglePMModal" :open-add-p-m-modal="togglePMModal"
/> />
<ProjectLimitBanner
v-if="isProjectLimitBannerShown"
:dashboard-ref="dashboardContent"
:on-upgrade-clicked="togglePMModal"
/>
<v-banner <v-banner
v-if="isAccountFrozen && !isLoading && dashboardContent" v-if="isAccountFrozen && !isLoading && dashboardContent"
severity="critical" severity="critical"
@ -177,7 +166,6 @@ import UpgradeNotification from '@/components/notifications/UpgradeNotification.
import ProjectLimitBanner from '@/components/notifications/ProjectLimitBanner.vue'; import ProjectLimitBanner from '@/components/notifications/ProjectLimitBanner.vue';
import ProjectInvitationBanner from '@/components/notifications/ProjectInvitationBanner.vue'; import ProjectInvitationBanner from '@/components/notifications/ProjectInvitationBanner.vue';
import BrandedLoader from '@/components/common/BrandedLoader.vue'; import BrandedLoader from '@/components/common/BrandedLoader.vue';
import UpdateSessionTimeoutBanner from '@/components/notifications/UpdateSessionTimeoutBanner.vue';
import ObjectsUploadingModal from '@/components/modals/objectUpload/ObjectsUploadingModal.vue'; import ObjectsUploadingModal from '@/components/modals/objectUpload/ObjectsUploadingModal.vue';
import CloudIcon from '@/../static/images/notifications/cloudAlert.svg'; import CloudIcon from '@/../static/images/notifications/cloudAlert.svg';
@ -245,13 +233,6 @@ const isObjectsUploadModal = computed((): boolean => {
return configStore.state.config.newUploadModalEnabled && appStore.state.isUploadingModal; return configStore.state.config.newUploadModalEnabled && appStore.state.isUploadingModal;
}); });
/**
* Indicates whether the update session timeout notification should be shown.
*/
const isUpdateSessionTimeoutBanner = computed((): boolean => {
return (route.name !== RouteConfig.Settings.name && !isOnboardingTour.value) && appStore.state.isUpdateSessionTimeoutBanner;
});
/** /**
* Indicates whether to display the session timer for debugging. * Indicates whether to display the session timer for debugging.
*/ */
@ -354,13 +335,6 @@ const isNavigationHidden = computed((): boolean => {
return isOnboardingTour.value || isCreateProjectPage.value; return isOnboardingTour.value || isCreateProjectPage.value;
}); });
/* whether the project limit banner should be shown. */
const isProjectLimitBannerShown = computed((): boolean => {
return !LocalData.getProjectLimitBannerHidden()
&& isProjectListPage.value
&& (hasReachedProjectLimit.value || !usersStore.state.user.paidTier);
});
/** /**
* Returns whether the user has reached project limits. * Returns whether the user has reached project limits.
*/ */
@ -751,10 +725,6 @@ onMounted(async () => {
usersStore.getSettings(), usersStore.getSettings(),
]); ]);
if (usersStore.state.settings.sessionDuration && appStore.state.isUpdateSessionTimeoutBanner) {
appStore.closeUpdateSessionTimeoutBanner();
}
setupSessionTimers(); setupSessionTimers();
} catch (error) { } catch (error) {
if (!(error instanceof ErrorUnauthorized)) { if (!(error instanceof ErrorUnauthorized)) {

View File

@ -18,24 +18,12 @@
<div class="all-dashboard__content__divider" /> <div class="all-dashboard__content__divider" />
<div class="all-dashboard__banners"> <div class="all-dashboard__banners">
<UpdateSessionTimeoutBanner
v-if="isUpdateSessionTimeoutBanner && dashboardContent"
:dashboard-ref="dashboardContent"
/>
<UpgradeNotification <UpgradeNotification
v-if="isPaidTierBannerShown" v-if="isPaidTierBannerShown"
class="all-dashboard__banners__upgrade" class="all-dashboard__banners__upgrade"
:open-add-p-m-modal="togglePMModal" :open-add-p-m-modal="togglePMModal"
/> />
<ProjectLimitBanner
v-if="isProjectLimitBannerShown"
class="all-dashboard__banners__project-limit"
:dashboard-ref="dashboardContent"
:on-upgrade-clicked="togglePMModal"
/>
<v-banner <v-banner
v-if="isAccountFrozen && !isLoading && dashboardContent" v-if="isAccountFrozen && !isLoading && dashboardContent"
class="all-dashboard__banners__freeze" class="all-dashboard__banners__freeze"
@ -156,8 +144,6 @@ import AllModals from '@/components/modals/AllModals.vue';
import LimitWarningModal from '@/components/modals/LimitWarningModal.vue'; import LimitWarningModal from '@/components/modals/LimitWarningModal.vue';
import VBanner from '@/components/common/VBanner.vue'; import VBanner from '@/components/common/VBanner.vue';
import UpgradeNotification from '@/components/notifications/UpgradeNotification.vue'; import UpgradeNotification from '@/components/notifications/UpgradeNotification.vue';
import ProjectLimitBanner from '@/components/notifications/ProjectLimitBanner.vue';
import UpdateSessionTimeoutBanner from '@/components/notifications/UpdateSessionTimeoutBanner.vue';
import LoaderImage from '@/../static/images/common/loadIcon.svg'; import LoaderImage from '@/../static/images/common/loadIcon.svg';
@ -216,13 +202,6 @@ const sessionRefreshInterval = computed((): number => {
return sessionDuration.value / 2; return sessionDuration.value / 2;
}); });
/**
* Indicates whether the update session timeout notification should be shown.
*/
const isUpdateSessionTimeoutBanner = computed((): boolean => {
return route.name !== RouteConfig.Settings2.name && appStore.state.isUpdateSessionTimeoutBanner;
});
/** /**
* Indicates whether to display the session timer for debugging. * Indicates whether to display the session timer for debugging.
*/ */
@ -347,13 +326,6 @@ const showMFARecoveryCodeBar = computed((): boolean => {
return user.isMFAEnabled && user.mfaRecoveryCodeCount < recoveryCodeWarningThreshold; return user.isMFAEnabled && user.mfaRecoveryCodeCount < recoveryCodeWarningThreshold;
}); });
/* whether the project limit banner should be shown. */
const isProjectLimitBannerShown = computed((): boolean => {
return !LocalData.getProjectLimitBannerHidden()
&& !isBillingPage.value
&& (hasReachedProjectLimit.value || !usersStore.state.user.paidTier);
});
/** /**
* Returns whether the user has reached project limits. * Returns whether the user has reached project limits.
*/ */
@ -619,10 +591,6 @@ onMounted(async () => {
usersStore.getSettings(), usersStore.getSettings(),
]); ]);
if (usersStore.state.settings.sessionDuration && appStore.state.isUpdateSessionTimeoutBanner) {
appStore.closeUpdateSessionTimeoutBanner();
}
setupSessionTimers(); setupSessionTimers();
} catch (error) { } catch (error) {
if (!(error instanceof ErrorUnauthorized)) { if (!(error instanceof ErrorUnauthorized)) {
@ -709,6 +677,7 @@ onBeforeUnmount(() => {
overflow-y: auto; overflow-y: auto;
width: 100%; width: 100%;
height: 100%; height: 100%;
background: var(--c-grey-1);
&__bars { &__bars {
display: contents; display: contents;

View File

@ -6,18 +6,37 @@
<div class="my-projects__header"> <div class="my-projects__header">
<span class="my-projects__header__title">My Projects</span> <span class="my-projects__header__title">My Projects</span>
<VButton <span class="my-projects__header__right">
class="my-projects__header__button" <span class="my-projects__header__right__text">View</span>
icon="addcircle" <v-chip
is-white label="Table"
:on-press="onCreateProjectClicked" :is-selected="isTableViewSelected"
label="Create a Project" :icon="TableIcon"
/> @select="() => onViewChangeClicked('table')"
</div> />
<v-chip
label="Cards"
:is-selected="!isTableViewSelected"
:icon="CardsIcon"
@select="() => onViewChangeClicked('cards')"
/>
<VButton
class="my-projects__header__right__button"
icon="addcircle"
is-white
:on-press="onCreateProjectClicked"
label="Create a Project"
/>
</span>
</div>
<div v-if="projects.length || invites.length" class="my-projects__list"> <div v-if="projects.length || invites.length" class="my-projects__list">
<project-item v-for="project in projects" :key="project.id" :project="project" /> <projects-table v-if="isTableViewSelected" class="my-projects__list__table" />
<project-invitation-item v-for="invite in invites" :key="invite.projectID" :invitation="invite" /> <div v-else-if="!isTableViewSelected" class="my-projects__list__cards">
<project-item v-for="project in projects" :key="project.id" :project="project" />
<project-invitation-item v-for="invite in invites" :key="invite.projectID" :invitation="invite" />
</div>
</div> </div>
<div v-else class="my-projects__empty-area"> <div v-else class="my-projects__empty-area">
<empty-project-item class="my-projects__empty-area__item" /> <empty-project-item class="my-projects__empty-area__item" />
@ -27,7 +46,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed } from 'vue'; import { computed, ref } from 'vue';
import { Project, ProjectInvitation } from '@/types/projects'; import { Project, ProjectInvitation } from '@/types/projects';
import { RouteConfig } from '@/router'; import { RouteConfig } from '@/router';
@ -43,17 +62,36 @@ import ProjectInvitationItem from '@/views/all-dashboard/components/ProjectInvit
import { useUsersStore } from '@/store/modules/usersStore'; import { useUsersStore } from '@/store/modules/usersStore';
import { useAppStore } from '@/store/modules/appStore'; import { useAppStore } from '@/store/modules/appStore';
import { useProjectsStore } from '@/store/modules/projectsStore'; import { useProjectsStore } from '@/store/modules/projectsStore';
import { useConfigStore } from '@/store/modules/configStore';
import ProjectsTable from '@/views/all-dashboard/components/ProjectsTable.vue';
import VButton from '@/components/common/VButton.vue'; import VButton from '@/components/common/VButton.vue';
import VChip from '@/components/common/VChip.vue';
import RocketIcon from '@/../static/images/common/rocket.svg'; import RocketIcon from '@/../static/images/common/rocket.svg';
import CardsIcon from '@/../static/images/common/cardsIcon.svg';
import TableIcon from '@/../static/images/common/tableIcon.svg';
const appStore = useAppStore(); const appStore = useAppStore();
const configStore = useConfigStore();
const usersStore = useUsersStore(); const usersStore = useUsersStore();
const projectsStore = useProjectsStore(); const projectsStore = useProjectsStore();
const analytics = new AnalyticsHttpApi(); const analytics = new AnalyticsHttpApi();
const hasProjectTableViewConfigured = ref(appStore.hasProjectTableViewConfigured());
/**
* Whether to use the table view.
*/
const isTableViewSelected = computed((): boolean => {
if (!hasProjectTableViewConfigured.value && projects.value.length > 1) {
// show the table by default if the user has more than 8 projects.
return true;
}
return appStore.state.isProjectTableViewEnabled;
});
/** /**
* Returns projects list from store. * Returns projects list from store.
*/ */
@ -69,6 +107,11 @@ const invites = computed((): ProjectInvitation[] => {
.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime()); .sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
}); });
function onViewChangeClicked(view: string): void {
appStore.toggleProjectTableViewEnabled(view === 'table');
hasProjectTableViewConfigured.value = true;
}
/** /**
* Route to create project page. * Route to create project page.
*/ */
@ -111,32 +154,49 @@ function onCreateProjectClicked(): void {
line-height: 31px; line-height: 31px;
} }
&__button { &__right {
padding: 10px 16px; font-family: 'font_regular', sans-serif;
border-radius: 8px; display: flex;
align-items: center;
justify-content: flex-end;
column-gap: 12px;
&__text {
font-size: 12px;
line-height: 18px;
color: var(--c-grey-6);
}
&__button {
padding: 10px 16px;
border-radius: 8px;
}
} }
} }
&__list { &__list {
margin-top: 20px; margin-top: 20px;
display: grid;
gap: 10px;
grid-template-columns: repeat(4, minmax(0, 1fr));
& :deep(.project-item) { &__cards {
overflow: hidden; display: grid;
} gap: 10px;
grid-template-columns: repeat(4, minmax(0, 1fr));
@media screen and (width <= 1024px) { & :deep(.project-item) {
grid-template-columns: repeat(3, minmax(0, 1fr)); overflow: hidden;
} }
@media screen and (width <= 786px) { @media screen and (width <= 1024px) {
grid-template-columns: repeat(2, minmax(0, 1fr)); grid-template-columns: repeat(3, minmax(0, 1fr));
} }
@media screen and (width <= 425px) { @media screen and (width <= 786px) {
grid-template-columns: auto; grid-template-columns: repeat(2, minmax(0, 1fr));
}
@media screen and (width <= 425px) {
grid-template-columns: auto;
}
} }
} }

View File

@ -206,10 +206,11 @@ async function goToProjectEdit(): Promise<void> {
display: flex; display: flex;
align-items: center; align-items: center;
padding: 15px 25px; padding: 15px 25px;
font-family: 'font_regular', sans-serif;
color: var(--c-grey-6); color: var(--c-grey-6);
cursor: pointer;
&__label { &__label {
font-family: 'font_regular', sans-serif;
margin: 0 0 0 10px; margin: 0 0 0 10px;
} }

View File

@ -0,0 +1,231 @@
// Copyright (C) 2023 Storj Labs, Inc.
// See LICENSE for copying information.
<template>
<table-item
item-type="project"
:item="itemToRender"
:on-click="onOpenClicked"
class="project-item"
>
<template #options>
<th class="project-item__menu options overflow-visible" @click.stop="toggleDropDown">
<div class="project-item__menu__icon">
<div class="project-item__menu__icon__content" :class="{open: isDropdownOpen}">
<menu-icon />
</div>
</div>
<div v-if="isDropdownOpen" v-click-outside="closeDropDown" class="project-item__menu__dropdown">
<div class="project-item__menu__dropdown__item" @click.stop="goToProjectEdit">
<gear-icon />
<p class="project-item__menu__dropdown__item__label">Project settings</p>
</div>
<div class="project-item__menu__dropdown__item" @click.stop="goToProjectMembers">
<users-icon />
<p class="project-item__menu__dropdown__item__label">Invite members</p>
</div>
</div>
</th>
</template>
<menu-icon />
</table-item>
</template>
<script setup lang="ts">
import { computed } from 'vue';
import { useRouter } from 'vue-router';
import { Project } from '@/types/projects';
import { useNotify } from '@/utils/hooks';
import { AnalyticsEvent } from '@/utils/constants/analyticsEventNames';
import { User } from '@/types/users';
import { AnalyticsHttpApi } from '@/api/analytics';
import { LocalData } from '@/utils/localData';
import { RouteConfig } from '@/router';
import { MODALS } from '@/utils/constants/appStatePopUps';
import { useUsersStore } from '@/store/modules/usersStore';
import { useProjectMembersStore } from '@/store/modules/projectMembersStore';
import { useAppStore } from '@/store/modules/appStore';
import { useBucketsStore } from '@/store/modules/bucketsStore';
import { useProjectsStore } from '@/store/modules/projectsStore';
import { useResize } from '@/composables/resize';
import TableItem from '@/components/common/TableItem.vue';
import UsersIcon from '@/../static/images/navigation/users.svg';
import GearIcon from '@/../static/images/common/gearIcon.svg';
import MenuIcon from '@/../static/images/common/horizontalDots.svg';
const bucketsStore = useBucketsStore();
const appStore = useAppStore();
const pmStore = useProjectMembersStore();
const usersStore = useUsersStore();
const projectsStore = useProjectsStore();
const notify = useNotify();
const router = useRouter();
const analytics = new AnalyticsHttpApi();
const props = defineProps<{
project: Project,
}>();
const { isMobile } = useResize();
const itemToRender = computed((): { [key: string]: unknown | string[] } => {
if (!isMobile.value) {
return {
multi: { title: props.project.name, subtitle: props.project.description },
date: props.project.createdDate(),
memberCount: props.project.memberCount.toString(),
owner: isOwner.value,
};
}
return { info: [ props.project.name, `Created ${props.project.createdDate()}` ] };
});
/**
* isDropdownOpen if dropdown is open.
*/
const isDropdownOpen = computed((): boolean => {
return appStore.state.activeDropdown === props.project.id;
});
/**
* Returns user entity from store.
*/
const user = computed((): User => {
return usersStore.state.user;
});
/**
* Returns if the current user is the owner of this project.
*/
const isOwner = computed((): boolean => {
return props.project.ownerId === user.value.id;
});
function toggleDropDown() {
appStore.toggleActiveDropdown(props.project.id);
}
function closeDropDown() {
appStore.closeDropdowns();
}
/**
* Fetches all project related information.
*/
async function onOpenClicked(): Promise<void> {
await selectProject();
if (usersStore.shouldOnboard) {
analytics.pageVisit(RouteConfig.OnboardingTour.with(RouteConfig.OverviewStep).path);
await router.push(RouteConfig.OnboardingTour.with(RouteConfig.OverviewStep).path);
return;
}
await analytics.eventTriggered(AnalyticsEvent.NAVIGATE_PROJECTS);
if (usersStore.state.settings.passphrasePrompt) {
appStore.updateActiveModal(MODALS.enterPassphrase);
}
analytics.pageVisit(RouteConfig.ProjectDashboard.path);
router.push(RouteConfig.ProjectDashboard.path);
}
async function selectProject() {
projectsStore.selectProject(props.project.id);
LocalData.setSelectedProjectId(props.project.id);
pmStore.setSearchQuery('');
bucketsStore.clearS3Data();
}
/**
* Navigates to project members page.
*/
async function goToProjectMembers(): Promise<void> {
await selectProject();
analytics.pageVisit(RouteConfig.Team.path);
router.push(RouteConfig.Team.path);
closeDropDown();
}
/**
* Fetches all project related information and goes to edit project page.
*/
async function goToProjectEdit(): Promise<void> {
await selectProject();
analytics.pageVisit(RouteConfig.EditProjectDetails.path);
router.push(RouteConfig.EditProjectDetails.path);
closeDropDown();
}
</script>
<style scoped lang="scss">
.project-item {
&__menu {
padding: 0 10px;
position: relative;
cursor: pointer;
&__icon {
&__content {
height: 32px;
width: 32px;
margin-left: auto;
margin-right: 0;
padding: 12px 5px;
border-radius: 5px;
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: center;
&.open {
background: var(--c-grey-3);
}
}
}
&__dropdown {
position: absolute;
top: 55px;
right: 10px;
background: var(--c-white);
box-shadow: 0 7px 20px rgb(0 0 0 / 15%);
border: 1px solid var(--c-grey-2);
border-radius: 8px;
z-index: 100;
overflow: hidden;
&__item {
display: flex;
align-items: center;
padding: 15px 25px;
color: var(--c-grey-6);
cursor: pointer;
&__label {
font-family: 'font_regular', sans-serif;
margin: 0 0 0 10px;
}
&:hover {
font-family: 'font_medium', sans-serif;
color: var(--c-blue-3);
background-color: var(--c-grey-1);
svg :deep(path) {
fill: var(--c-blue-3);
}
}
}
}
}
}
</style>

View File

@ -0,0 +1,44 @@
// Copyright (C) 2023 Storj Labs, Inc.
// See LICENSE for copying information.
<template>
<v-table
:total-items-count="projects.length"
class="projects-table"
items-label="projects"
>
<template #head>
<th class="sort-header-container__name-item align-left">Project</th>
<th class="sort-header-container__date-item align-left">Date Added</th>
<th class="sort-header-container__date-item align-left">Members</th>
<th class="sort-header-container__date-item align-left">Role</th>
</template>
<template #body>
<project-table-item
v-for="(project, key) in projects"
:key="key"
:project="project"
/>
</template>
</v-table>
</template>
<script setup lang="ts">
import { computed } from 'vue';
import { Project } from '@/types/projects';
import { useProjectsStore } from '@/store/modules/projectsStore';
import ProjectTableItem from '@/views/all-dashboard/components/ProjectTableItem.vue';
import VTable from '@/components/common/VTable.vue';
const projectsStore = useProjectsStore();
/**
* Returns projects list from store.
*/
const projects = computed((): Project[] => {
return projectsStore.projects;
});
</script>

View File

@ -0,0 +1,6 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="6.99939" y="6.99951" width="4.0003" height="4.0003" rx="1" fill="#56606D"/>
<rect x="6.99939" y="13.0005" width="4.0003" height="4.0003" rx="1" fill="#56606D"/>
<rect x="12.9996" y="6.99951" width="4.0003" height="4.0003" rx="1" fill="#56606D"/>
<rect x="12.9996" y="13.0005" width="4.0003" height="4.0003" rx="1" fill="#56606D"/>
</svg>

After

Width:  |  Height:  |  Size: 443 B

View File

@ -0,0 +1,3 @@
<svg width="22" height="6" viewBox="0 0 22 6" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10.9999 0.333496C9.53325 0.333496 8.33325 1.5335 8.33325 3.00016C8.33325 4.46683 9.53325 5.66683 10.9999 5.66683C12.4666 5.66683 13.6666 4.46683 13.6666 3.00016C13.6666 1.5335 12.4666 0.333496 10.9999 0.333496ZM18.9999 0.333496C17.5333 0.333496 16.3333 1.5335 16.3333 3.00016C16.3333 4.46683 17.5333 5.66683 18.9999 5.66683C20.4666 5.66683 21.6666 4.46683 21.6666 3.00016C21.6666 1.5335 20.4666 0.333496 18.9999 0.333496ZM2.99992 0.333496C1.53325 0.333496 0.333252 1.5335 0.333252 3.00016C0.333252 4.46683 1.53325 5.66683 2.99992 5.66683C4.46659 5.66683 5.66658 4.46683 5.66658 3.00016C5.66658 1.5335 4.46659 0.333496 2.99992 0.333496Z" fill="#7C8794"/>
</svg>

After

Width:  |  Height:  |  Size: 765 B

View File

@ -0,0 +1,8 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M9 9H7V7H9V9Z" fill="black"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M9 13H7V11H9V13Z" fill="black"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M9 17H7V15H9V17Z" fill="black"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M18 9H10V7H18V9Z" fill="black"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M18 13H10V11H18V13Z" fill="black"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M18 17H10V15H18V17Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 598 B