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:
parent
7422fe393b
commit
6ca9f6d2c6
@ -61,6 +61,7 @@ export class ProjectsApiGql extends BaseGql implements ProjectsApi {
|
||||
publicId
|
||||
description
|
||||
createdAt
|
||||
memberCount
|
||||
ownerId
|
||||
}
|
||||
}`;
|
||||
@ -74,6 +75,8 @@ export class ProjectsApiGql extends BaseGql implements ProjectsApi {
|
||||
project.description,
|
||||
project.createdAt,
|
||||
project.ownerId,
|
||||
false,
|
||||
project.memberCount,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
@ -18,12 +18,16 @@
|
||||
<p v-for="str in val" :key="str" class="array-val">{{ str }}</p>
|
||||
</div>
|
||||
<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" />
|
||||
</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" />
|
||||
<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>
|
||||
</p>
|
||||
<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 TxtIcon from '@/../static/images/objects/txt.svg';
|
||||
import ZipIcon from '@/../static/images/objects/zip.svg';
|
||||
import ProjectIcon from '@/../static/images/navigation/project.svg';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
selectDisabled?: boolean;
|
||||
@ -97,10 +102,23 @@ const icons = new Map<string, string>([
|
||||
['image', PictureIcon],
|
||||
['text', TxtIcon],
|
||||
['archive', ZipIcon],
|
||||
['project', ProjectIcon],
|
||||
]);
|
||||
|
||||
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 {
|
||||
emit('selectClicked', event);
|
||||
}
|
||||
@ -188,6 +206,10 @@ function cellContentClicked(cellIndex: number, event: Event) {
|
||||
|
||||
.primary {
|
||||
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 {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@ -244,4 +278,18 @@ function cellContentClicked(cellIndex: number, event: Event) {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.project-owner {
|
||||
|
||||
:deep(path) {
|
||||
fill: var(--c-purple-4);
|
||||
}
|
||||
}
|
||||
|
||||
.project-member {
|
||||
|
||||
:deep(path) {
|
||||
fill: var(--c-yellow-5);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
67
web/satellite/src/components/common/VChip.vue
Normal file
67
web/satellite/src/components/common/VChip.vue
Normal 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>
|
@ -13,7 +13,6 @@ import { LimitToChange } from '@/types/projects';
|
||||
class AppState {
|
||||
public fetchState = FetchState.LOADING;
|
||||
public isSuccessfulPasswordResetShown = false;
|
||||
public isUpdateSessionTimeoutBanner = !LocalData.getSessionTimeoutBannerAcknowledged();
|
||||
public hasJustLoggedIn = false;
|
||||
public onbAGStepBackRoute = '';
|
||||
public onbAPIKeyStepBackRoute = '';
|
||||
@ -35,6 +34,7 @@ class AppState {
|
||||
public isLargeUploadNotificationShown = true;
|
||||
public isLargeUploadWarningNotificationShown = false;
|
||||
public activeChangeLimit: LimitToChange = LimitToChange.Storage;
|
||||
public isProjectTableViewEnabled = LocalData.getProjectTableViewEnabled();
|
||||
}
|
||||
|
||||
class ErrorPageState {
|
||||
@ -85,6 +85,19 @@ export const useAppStore = defineStore('app', () => {
|
||||
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 {
|
||||
state.fetchState = newFetchState;
|
||||
}
|
||||
@ -141,12 +154,6 @@ export const useAppStore = defineStore('app', () => {
|
||||
state.isGalleryView = value;
|
||||
}
|
||||
|
||||
function closeUpdateSessionTimeoutBanner(): void {
|
||||
LocalData.setSessionTimeoutBannerAcknowledged();
|
||||
|
||||
state.isUpdateSessionTimeoutBanner = false;
|
||||
}
|
||||
|
||||
function closeDropdowns(): void {
|
||||
state.activeDropdown = '';
|
||||
}
|
||||
@ -177,12 +184,16 @@ export const useAppStore = defineStore('app', () => {
|
||||
state.isUploadingModal = false;
|
||||
state.error.visible = false;
|
||||
state.isGalleryView = false;
|
||||
state.isProjectTableViewEnabled = false;
|
||||
LocalData.removeProjectTableViewConfig();
|
||||
}
|
||||
|
||||
return {
|
||||
state,
|
||||
toggleActiveDropdown,
|
||||
toggleSuccessfulPasswordReset,
|
||||
toggleProjectTableViewEnabled,
|
||||
hasProjectTableViewConfigured,
|
||||
updateActiveModal,
|
||||
removeActiveModal,
|
||||
toggleHasJustLoggedIn,
|
||||
@ -201,7 +212,6 @@ export const useAppStore = defineStore('app', () => {
|
||||
setLargeUploadWarningNotification,
|
||||
setLargeUploadNotification,
|
||||
closeDropdowns,
|
||||
closeUpdateSessionTimeoutBanner,
|
||||
setErrorPage,
|
||||
removeErrorPage,
|
||||
clear,
|
||||
|
@ -15,6 +15,7 @@ export class LocalData {
|
||||
private static largeUploadNotificationDismissed = 'largeUploadNotificationDismissed';
|
||||
private static sessionExpirationDate = 'sessionExpirationDate';
|
||||
private static projectLimitBannerHidden = 'projectLimitBannerHidden';
|
||||
private static projectTableViewEnabled = 'projectTableViewEnabled';
|
||||
|
||||
public static getSelectedProjectId(): string | null {
|
||||
return localStorage.getItem(LocalData.selectedProjectId);
|
||||
@ -62,14 +63,6 @@ export class LocalData {
|
||||
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
|
||||
*/
|
||||
@ -122,8 +115,26 @@ export class LocalData {
|
||||
localStorage.setItem(LocalData.projectLimitBannerHidden, 'true');
|
||||
}
|
||||
|
||||
public static getProjectLimitBannerHidden(): boolean {
|
||||
const value = localStorage.getItem(LocalData.projectLimitBannerHidden);
|
||||
public static getProjectTableViewEnabled(): boolean {
|
||||
const value = localStorage.getItem(LocalData.projectTableViewEnabled);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
@ -18,22 +18,11 @@
|
||||
<div class="dashboard__wrap__main-area__content-wrap__container__content banners">
|
||||
<ProjectInvitationBanner v-if="isProjectInvitationBannerShown" />
|
||||
|
||||
<UpdateSessionTimeoutBanner
|
||||
v-if="isUpdateSessionTimeoutBanner && dashboardContent"
|
||||
:dashboard-ref="dashboardContent"
|
||||
/>
|
||||
|
||||
<UpgradeNotification
|
||||
v-if="isPaidTierBannerShown"
|
||||
:open-add-p-m-modal="togglePMModal"
|
||||
/>
|
||||
|
||||
<ProjectLimitBanner
|
||||
v-if="isProjectLimitBannerShown"
|
||||
:dashboard-ref="dashboardContent"
|
||||
:on-upgrade-clicked="togglePMModal"
|
||||
/>
|
||||
|
||||
<v-banner
|
||||
v-if="isAccountFrozen && !isLoading && dashboardContent"
|
||||
severity="critical"
|
||||
@ -177,7 +166,6 @@ import UpgradeNotification from '@/components/notifications/UpgradeNotification.
|
||||
import ProjectLimitBanner from '@/components/notifications/ProjectLimitBanner.vue';
|
||||
import ProjectInvitationBanner from '@/components/notifications/ProjectInvitationBanner.vue';
|
||||
import BrandedLoader from '@/components/common/BrandedLoader.vue';
|
||||
import UpdateSessionTimeoutBanner from '@/components/notifications/UpdateSessionTimeoutBanner.vue';
|
||||
import ObjectsUploadingModal from '@/components/modals/objectUpload/ObjectsUploadingModal.vue';
|
||||
|
||||
import CloudIcon from '@/../static/images/notifications/cloudAlert.svg';
|
||||
@ -245,13 +233,6 @@ const isObjectsUploadModal = computed((): boolean => {
|
||||
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.
|
||||
*/
|
||||
@ -354,13 +335,6 @@ const isNavigationHidden = computed((): boolean => {
|
||||
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.
|
||||
*/
|
||||
@ -751,10 +725,6 @@ onMounted(async () => {
|
||||
usersStore.getSettings(),
|
||||
]);
|
||||
|
||||
if (usersStore.state.settings.sessionDuration && appStore.state.isUpdateSessionTimeoutBanner) {
|
||||
appStore.closeUpdateSessionTimeoutBanner();
|
||||
}
|
||||
|
||||
setupSessionTimers();
|
||||
} catch (error) {
|
||||
if (!(error instanceof ErrorUnauthorized)) {
|
||||
|
@ -18,24 +18,12 @@
|
||||
<div class="all-dashboard__content__divider" />
|
||||
|
||||
<div class="all-dashboard__banners">
|
||||
<UpdateSessionTimeoutBanner
|
||||
v-if="isUpdateSessionTimeoutBanner && dashboardContent"
|
||||
:dashboard-ref="dashboardContent"
|
||||
/>
|
||||
|
||||
<UpgradeNotification
|
||||
v-if="isPaidTierBannerShown"
|
||||
class="all-dashboard__banners__upgrade"
|
||||
: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-if="isAccountFrozen && !isLoading && dashboardContent"
|
||||
class="all-dashboard__banners__freeze"
|
||||
@ -156,8 +144,6 @@ import AllModals from '@/components/modals/AllModals.vue';
|
||||
import LimitWarningModal from '@/components/modals/LimitWarningModal.vue';
|
||||
import VBanner from '@/components/common/VBanner.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';
|
||||
|
||||
@ -216,13 +202,6 @@ const sessionRefreshInterval = computed((): number => {
|
||||
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.
|
||||
*/
|
||||
@ -347,13 +326,6 @@ const showMFARecoveryCodeBar = computed((): boolean => {
|
||||
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.
|
||||
*/
|
||||
@ -619,10 +591,6 @@ onMounted(async () => {
|
||||
usersStore.getSettings(),
|
||||
]);
|
||||
|
||||
if (usersStore.state.settings.sessionDuration && appStore.state.isUpdateSessionTimeoutBanner) {
|
||||
appStore.closeUpdateSessionTimeoutBanner();
|
||||
}
|
||||
|
||||
setupSessionTimers();
|
||||
} catch (error) {
|
||||
if (!(error instanceof ErrorUnauthorized)) {
|
||||
@ -709,6 +677,7 @@ onBeforeUnmount(() => {
|
||||
overflow-y: auto;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: var(--c-grey-1);
|
||||
|
||||
&__bars {
|
||||
display: contents;
|
||||
|
@ -6,19 +6,38 @@
|
||||
<div class="my-projects__header">
|
||||
<span class="my-projects__header__title">My Projects</span>
|
||||
|
||||
<span class="my-projects__header__right">
|
||||
<span class="my-projects__header__right__text">View</span>
|
||||
<v-chip
|
||||
label="Table"
|
||||
:is-selected="isTableViewSelected"
|
||||
:icon="TableIcon"
|
||||
@select="() => onViewChangeClicked('table')"
|
||||
/>
|
||||
|
||||
<v-chip
|
||||
label="Cards"
|
||||
:is-selected="!isTableViewSelected"
|
||||
:icon="CardsIcon"
|
||||
@select="() => onViewChangeClicked('cards')"
|
||||
/>
|
||||
|
||||
<VButton
|
||||
class="my-projects__header__button"
|
||||
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">
|
||||
<projects-table v-if="isTableViewSelected" class="my-projects__list__table" />
|
||||
<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 v-else class="my-projects__empty-area">
|
||||
<empty-project-item class="my-projects__empty-area__item" />
|
||||
<rocket-icon class="my-projects__empty-area__icon" />
|
||||
@ -27,7 +46,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { Project, ProjectInvitation } from '@/types/projects';
|
||||
import { RouteConfig } from '@/router';
|
||||
@ -43,17 +62,36 @@ import ProjectInvitationItem from '@/views/all-dashboard/components/ProjectInvit
|
||||
import { useUsersStore } from '@/store/modules/usersStore';
|
||||
import { useAppStore } from '@/store/modules/appStore';
|
||||
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 VChip from '@/components/common/VChip.vue';
|
||||
|
||||
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 configStore = useConfigStore();
|
||||
const usersStore = useUsersStore();
|
||||
const projectsStore = useProjectsStore();
|
||||
|
||||
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.
|
||||
*/
|
||||
@ -69,6 +107,11 @@ const invites = computed((): ProjectInvitation[] => {
|
||||
.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.
|
||||
*/
|
||||
@ -111,14 +154,30 @@ function onCreateProjectClicked(): void {
|
||||
line-height: 31px;
|
||||
}
|
||||
|
||||
&__right {
|
||||
font-family: 'font_regular', sans-serif;
|
||||
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 {
|
||||
margin-top: 20px;
|
||||
|
||||
&__cards {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
@ -139,6 +198,7 @@ function onCreateProjectClicked(): void {
|
||||
grid-template-columns: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__empty-area {
|
||||
display: flex;
|
||||
|
@ -206,10 +206,11 @@ async function goToProjectEdit(): Promise<void> {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 15px 25px;
|
||||
font-family: 'font_regular', sans-serif;
|
||||
color: var(--c-grey-6);
|
||||
cursor: pointer;
|
||||
|
||||
&__label {
|
||||
font-family: 'font_regular', sans-serif;
|
||||
margin: 0 0 0 10px;
|
||||
}
|
||||
|
||||
|
@ -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>
|
@ -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>
|
6
web/satellite/static/images/common/cardsIcon.svg
Normal file
6
web/satellite/static/images/common/cardsIcon.svg
Normal 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 |
3
web/satellite/static/images/common/horizontalDots.svg
Normal file
3
web/satellite/static/images/common/horizontalDots.svg
Normal 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 |
8
web/satellite/static/images/common/tableIcon.svg
Normal file
8
web/satellite/static/images/common/tableIcon.svg
Normal 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 |
Loading…
Reference in New Issue
Block a user