web/satellite: match projects table with the designs

This change updates the projects table on all projects dashboard to
more closely match the designs.

Change-Id: I547a83352fba8c3ad7958802db7b38b342b383e8
This commit is contained in:
Wilfred Asomani 2023-06-29 13:12:00 +00:00 committed by Storj Robot
parent 8d8f6734de
commit f131047f1a
7 changed files with 252 additions and 112 deletions

View File

@ -14,8 +14,13 @@
v-for="(val, keyVal, index) in item" :key="index" class="align-left data" v-for="(val, keyVal, index) in item" :key="index" class="align-left data"
:class="{'overflow-visible': showBucketGuide(index)}" :class="{'overflow-visible': showBucketGuide(index)}"
> >
<div v-if="Array.isArray(val)" class="few-items"> <div v-if="Array.isArray(val)" class="few-items-container">
<p v-for="str in val" :key="str" class="array-val">{{ str }}</p> <div v-if="icon && index === 0 && itemType?.includes('project')" class="item-icon file-background" :class="customIconClasses">
<component :is="icon" />
</div>
<div class="few-items">
<p v-for="str in val" :key="str" class="array-val">{{ str }}</p>
</div>
</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" :class="customIconClasses"> <div v-if="icon && index === 0" class="item-icon file-background" :class="customIconClasses">
@ -83,17 +88,15 @@ const icon = computed((): string => ObjectType.findIcon(props.itemType));
const customIconClasses = computed(() => { const customIconClasses = computed(() => {
const classes = {}; const classes = {};
if (props.itemType === 'project') { if (props.itemType === 'project') {
if (props.item['role'] === ProjectRole.Owner) { classes['project-owner'] = true;
classes['project-owner'] = true; } else if (props.itemType === 'shared-project') {
} else if (props.item['role'] === ProjectRole.Member) { classes['project-member'] = true;
classes['project-member'] = true;
}
} }
return classes; return classes;
}); });
function isProjectRoleIconShown(role: ProjectRole) { function isProjectRoleIconShown(role: ProjectRole) {
return props.itemType === 'project' || role === ProjectRole.Invited || role === ProjectRole.InviteExpired; return props.itemType.includes('project') || role === ProjectRole.Invited || role === ProjectRole.InviteExpired;
} }
function selectClicked(event: Event): void { function selectClicked(event: Event): void {
@ -204,11 +207,27 @@ function cellContentClicked(cellIndex: number, event: Event) {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
&__title {
text-overflow: ellipsis;
overflow: hidden;
}
&__subtitle { &__subtitle {
font-family: 'font_regular', sans-serif; font-family: 'font_regular', sans-serif;
font-size: 12px; font-size: 12px;
line-height: 20px; line-height: 20px;
color: var(--c-grey-6); color: var(--c-grey-6);
text-overflow: ellipsis;
overflow: hidden;
}
}
.few-items-container {
display: flex;
align-items: center;
@media screen and (width <= 370px) {
max-width: 9rem;
} }
} }

View File

@ -33,6 +33,7 @@ export class ObjectType {
['text', TxtIcon], ['text', TxtIcon],
['archive', ZipIcon], ['archive', ZipIcon],
['project', ProjectIcon], ['project', ProjectIcon],
['shared-project', ProjectIcon],
]); ]);
static findIcon(type: string): string { static findIcon(type: string): string {

View File

@ -14,7 +14,7 @@
<heading class="all-dashboard__heading" /> <heading class="all-dashboard__heading" />
<div class="all-dashboard__content"> <div class="all-dashboard__content" :class="{ 'no-x-padding': isMyProjectsPage }">
<div class="all-dashboard__content__divider" /> <div class="all-dashboard__content__divider" />
<router-view /> <router-view />
@ -134,6 +134,10 @@ const sessionDuration = computed((): number => {
return duration; return duration;
}); });
const isMyProjectsPage = computed((): boolean => {
return route.path === RouteConfig.AllProjectsDashboard.path;
});
/** /**
* Returns the session refresh interval from the store. * Returns the session refresh interval from the store.
*/ */
@ -565,6 +569,11 @@ onBeforeUnmount(() => {
} }
} }
.no-x-padding {
padding-left: 0 !important;
padding-right: 0 !important;
}
.all-dashboard { .all-dashboard {
box-sizing: border-box; box-sizing: border-box;
overflow-y: auto; overflow-y: auto;

View File

@ -64,7 +64,10 @@
<all-projects-dashboard-banners v-if="content" :parent-ref="content" /> <all-projects-dashboard-banners v-if="content" :parent-ref="content" />
<div v-if="projects.length || invites.length" class="my-projects__list"> <div
v-if="projects.length || invites.length" class="my-projects__list"
:style="{'padding': isTableViewSelected && isMobile ? '0' : '0 20px'}"
>
<projects-table v-if="isTableViewSelected" :invites="invites" class="my-projects__list__table" /> <projects-table v-if="isTableViewSelected" :invites="invites" class="my-projects__list__table" />
<div v-else-if="!isTableViewSelected" class="my-projects__list__cards"> <div v-else-if="!isTableViewSelected" class="my-projects__list__cards">
<project-invitation-item v-for="invite in invites" :key="invite.projectID" :invitation="invite" /> <project-invitation-item v-for="invite in invites" :key="invite.projectID" :invitation="invite" />
@ -98,6 +101,7 @@ import { useProjectsStore } from '@/store/modules/projectsStore';
import { useConfigStore } from '@/store/modules/configStore'; import { useConfigStore } from '@/store/modules/configStore';
import ProjectsTable from '@/views/all-dashboard/components/ProjectsTable.vue'; import ProjectsTable from '@/views/all-dashboard/components/ProjectsTable.vue';
import AllProjectsDashboardBanners from '@/views/all-dashboard/components/AllProjectsDashboardBanners.vue'; import AllProjectsDashboardBanners from '@/views/all-dashboard/components/AllProjectsDashboardBanners.vue';
import { useResize } from '@/composables/resize';
import VButton from '@/components/common/VButton.vue'; import VButton from '@/components/common/VButton.vue';
import VChip from '@/components/common/VChip.vue'; import VChip from '@/components/common/VChip.vue';
@ -113,6 +117,8 @@ const projectsStore = useProjectsStore();
const analytics = new AnalyticsHttpApi(); const analytics = new AnalyticsHttpApi();
const { isMobile } = useResize();
const content = ref<HTMLElement | null>(null); const content = ref<HTMLElement | null>(null);
const hasProjectTableViewConfigured = ref(appStore.hasProjectTableViewConfigured()); const hasProjectTableViewConfigured = ref(appStore.hasProjectTableViewConfigured());
@ -173,6 +179,7 @@ function onCreateProjectClicked(): void {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
padding: 0 20px;
@media screen and (width <= 500px) { @media screen and (width <= 500px) {
margin-top: 20px; margin-top: 20px;
@ -268,6 +275,10 @@ function onCreateProjectClicked(): void {
} }
} }
& :deep(.all-dashboard-banners) {
padding: 0 20px;
}
&__list { &__list {
margin-top: 20px; margin-top: 20px;
@ -276,7 +287,7 @@ function onCreateProjectClicked(): void {
gap: 10px; gap: 10px;
grid-template-columns: repeat(4, minmax(0, 1fr)); grid-template-columns: repeat(4, minmax(0, 1fr));
& :deep(.project-item) { & :deep(.project-item), &:deep(.invite-item) {
overflow: hidden; overflow: hidden;
} }

View File

@ -3,41 +3,43 @@
<template> <template>
<table-item <table-item
item-type="project" item-type="shared-project"
:item="itemToRender" :item="itemToRender"
class="invitation-item" class="invitation-item"
> >
<template #options> <template #options>
<th class="options overflow-visible"> <th class="overflow-visible">
<v-button <div class="options">
:loading="isLoading" <v-button
:disabled="isLoading" :loading="isLoading"
:on-press="onJoinClicked" :disabled="isLoading"
border-radius="8px" :on-press="onJoinClicked"
font-size="12px" border-radius="8px"
label="Join Project" font-size="12px"
class="invitation-item__menu__button" label="Join Project"
/> class="invitation-item__button"
<v-button />
:loading="isLoading" <v-button
:disabled="isLoading" :loading="isLoading"
:on-press="onJoinClicked" :disabled="isLoading"
border-radius="8px" :on-press="onJoinClicked"
font-size="12px" border-radius="8px"
label="Join" font-size="12px"
class="invitation-item__menu__mobile-button" label="Join"
/> class="invitation-item__mobile-button"
<div class="invitation-item__menu"> />
<div class="invitation-item__menu__icon" @click.stop="toggleDropDown"> <div class="invitation-item__menu">
<div class="invitation-item__menu__icon__content" :class="{open: isDropdownOpen}"> <div class="invitation-item__menu__icon" @click.stop="toggleDropDown">
<menu-icon /> <div class="invitation-item__menu__icon__content" :class="{open: isDropdownOpen}">
<menu-icon />
</div>
</div> </div>
</div>
<div v-if="isDropdownOpen" v-click-outside="closeDropDown" class="invitation-item__menu__dropdown"> <div v-if="isDropdownOpen" v-click-outside="closeDropDown" class="invitation-item__menu__dropdown">
<div class="invitation-item__menu__dropdown__item" @click.stop="onDeclineClicked"> <div class="invitation-item__menu__dropdown__item" @click.stop="onDeclineClicked">
<logout-icon /> <logout-icon />
<p class="invitation-item__menu__dropdown__item__label">Decline invite</p> <p class="invitation-item__menu__dropdown__item__label">Decline invite</p>
</div>
</div> </div>
</div> </div>
</div> </div>
@ -59,6 +61,7 @@ import { useAppStore } from '@/store/modules/appStore';
import { useProjectsStore } from '@/store/modules/projectsStore'; import { useProjectsStore } from '@/store/modules/projectsStore';
import { useResize } from '@/composables/resize'; import { useResize } from '@/composables/resize';
import { AnalyticsHttpApi } from '@/api/analytics'; import { AnalyticsHttpApi } from '@/api/analytics';
import { useLoading } from '@/composables/useLoading';
import VButton from '@/components/common/VButton.vue'; import VButton from '@/components/common/VButton.vue';
import TableItem from '@/components/common/TableItem.vue'; import TableItem from '@/components/common/TableItem.vue';
@ -72,25 +75,36 @@ const notify = useNotify();
const analytics: AnalyticsHttpApi = new AnalyticsHttpApi(); const analytics: AnalyticsHttpApi = new AnalyticsHttpApi();
const isLoading = ref<boolean>(false); const { isLoading, withLoading } = useLoading();
const props = defineProps<{ const props = defineProps<{
invitation: ProjectInvitation, invitation: ProjectInvitation,
}>(); }>();
const { isMobile } = useResize(); const { isMobile, screenWidth } = useResize();
const itemToRender = computed((): { [key: string]: unknown | string[] } => { const itemToRender = computed((): { [key: string]: unknown | string[] } => {
if (!isMobile.value) { if (screenWidth.value <= 600 && !isMobile.value) {
return {
multi: { title: props.invitation.projectName, subtitle: props.invitation.projectDescription },
};
}
if (screenWidth.value <= 850 && !isMobile.value) {
return { return {
multi: { title: props.invitation.projectName, subtitle: props.invitation.projectDescription }, multi: { title: props.invitation.projectName, subtitle: props.invitation.projectDescription },
date: props.invitation.invitedDate,
memberCount: '',
role: ProjectRole.Invited, role: ProjectRole.Invited,
}; };
} }
if (isMobile.value) {
return { info: [ props.invitation.projectName, props.invitation.projectDescription ] };
}
return { info: [ props.invitation.projectName, props.invitation.projectDescription ] }; return {
multi: { title: props.invitation.projectName, subtitle: props.invitation.projectDescription },
date: props.invitation.invitedDate,
memberCount: '',
role: ProjectRole.Invited,
};
}); });
/** /**
@ -111,25 +125,22 @@ function onJoinClicked(): void {
/** /**
* Declines the project member invitation. * Declines the project member invitation.
*/ */
async function onDeclineClicked(): Promise<void> { function onDeclineClicked(): void {
if (isLoading.value) return; withLoading(async () => {
isLoading.value = true; try {
await projectsStore.respondToInvitation(props.invitation.projectID, ProjectInvitationResponse.Decline);
analytics.eventTriggered(AnalyticsEvent.PROJECT_INVITATION_DECLINED);
} catch (error) {
notify.error(`Failed to decline project invitation. ${error.message}`, AnalyticsErrorEventSource.PROJECT_INVITATION);
}
try { try {
await projectsStore.respondToInvitation(props.invitation.projectID, ProjectInvitationResponse.Decline); await projectsStore.getUserInvitations();
analytics.eventTriggered(AnalyticsEvent.PROJECT_INVITATION_DECLINED); await projectsStore.getProjects();
} catch (error) { } catch (error) {
notify.error(`Failed to decline project invitation. ${error.message}`, AnalyticsErrorEventSource.PROJECT_INVITATION); notify.error(`Failed to reload projects and invitations list. ${error.message}`, AnalyticsErrorEventSource.PROJECT_INVITATION);
} }
});
try {
await projectsStore.getUserInvitations();
await projectsStore.getProjects();
} catch (error) {
notify.error(`Failed to reload projects and invitations list. ${error.message}`, AnalyticsErrorEventSource.PROJECT_INVITATION);
}
isLoading.value = false;
} }
function toggleDropDown() { function toggleDropDown() {
@ -149,31 +160,35 @@ function closeDropDown() {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: flex-end; justify-content: flex-end;
column-gap: 10px; column-gap: 20px;
padding-right: 10px; padding-right: 10px;
@media screen and (width <= 900px) {
column-gap: 10px;
}
}
&__button {
padding: 10px 16px;
@media screen and (width <= 900px) {
display: none;
}
}
&__mobile-button {
display: none;
padding: 10px 16px;
@media screen and (width <= 900px) {
display: flex;
}
} }
&__menu { &__menu {
position: relative; position: relative;
cursor: pointer; cursor: pointer;
&__button {
padding: 10px 16px;
@media screen and (width <= 500px) {
display: none;
}
}
&__mobile-button {
display: none;
padding: 10px 16px;
@media screen and (width <= 500px) {
display: flex;
}
}
&__icon { &__icon {
&__content { &__content {

View File

@ -3,28 +3,48 @@
<template> <template>
<table-item <table-item
item-type="project" :item-type="isOwner ? 'project' : 'shared-project'"
:item="itemToRender" :item="itemToRender"
:on-click="onOpenClicked" :on-click="onOpenClicked"
class="project-item" class="project-item"
> >
<template #options> <template #options>
<th class="project-item__menu options overflow-visible" @click.stop="toggleDropDown"> <th class="overflow-visible">
<div class="project-item__menu__icon"> <div class="options">
<div class="project-item__menu__icon__content" :class="{open: isDropdownOpen}"> <v-button
<menu-icon /> :on-press="onOpenClicked"
</div> is-white
</div> border-radius="8px"
font-size="12px"
label="Open Project"
class="project-item__button"
/>
<v-button
:on-press="onOpenClicked"
is-white
border-radius="8px"
font-size="12px"
label="Open"
class="project-item__mobile-button"
/>
<div class="project-item__menu">
<div class="project-item__menu__icon" @click.stop="toggleDropDown">
<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 v-if="isDropdownOpen" v-click-outside="closeDropDown" class="project-item__menu__dropdown">
<div v-if="isOwner" class="project-item__menu__dropdown__item" @click.stop="goToProjectEdit"> <div v-if="isOwner" class="project-item__menu__dropdown__item" @click.stop="goToProjectEdit">
<gear-icon /> <gear-icon />
<p class="project-item__menu__dropdown__item__label">Project settings</p> <p class="project-item__menu__dropdown__item__label">Project settings</p>
</div> </div>
<div class="project-item__menu__dropdown__item" @click.stop="goToProjectMembers"> <div class="project-item__menu__dropdown__item" @click.stop="goToProjectMembers">
<users-icon /> <users-icon />
<p class="project-item__menu__dropdown__item__label">Invite members</p> <p class="project-item__menu__dropdown__item__label">Invite members</p>
</div>
</div>
</div> </div>
</div> </div>
</th> </th>
@ -53,6 +73,7 @@ import { useProjectsStore } from '@/store/modules/projectsStore';
import { useResize } from '@/composables/resize'; import { useResize } from '@/composables/resize';
import TableItem from '@/components/common/TableItem.vue'; import TableItem from '@/components/common/TableItem.vue';
import VButton from '@/components/common/VButton.vue';
import UsersIcon from '@/../static/images/navigation/users.svg'; import UsersIcon from '@/../static/images/navigation/users.svg';
import GearIcon from '@/../static/images/common/gearIcon.svg'; import GearIcon from '@/../static/images/common/gearIcon.svg';
@ -71,19 +92,30 @@ const props = defineProps<{
project: Project, project: Project,
}>(); }>();
const { isMobile } = useResize(); const { isMobile, screenWidth } = useResize();
const itemToRender = computed((): { [key: string]: unknown | string[] } => { const itemToRender = computed((): { [key: string]: unknown | string[] } => {
if (!isMobile.value) { if (screenWidth.value <= 600 && !isMobile.value) {
return {
multi: { title: props.project.name, subtitle: props.project.description },
};
}
if (screenWidth.value <= 850 && !isMobile.value) {
return { return {
multi: { title: props.project.name, subtitle: props.project.description }, multi: { title: props.project.name, subtitle: props.project.description },
date: props.project.createdDate(),
memberCount: props.project.memberCount.toString(),
role: isOwner.value ? ProjectRole.Owner : ProjectRole.Member, role: isOwner.value ? ProjectRole.Owner : ProjectRole.Member,
}; };
} }
if (isMobile.value) {
return { info: [ props.project.name, props.project.description ] };
}
return { info: [ props.project.name, props.project.description ] }; return {
multi: { title: props.project.name, subtitle: props.project.description },
date: props.project.createdDate(),
memberCount: props.project.memberCount.toString(),
role: isOwner.value ? ProjectRole.Owner : ProjectRole.Member,
};
}); });
/** /**
@ -166,8 +198,38 @@ async function goToProjectEdit(): Promise<void> {
<style scoped lang="scss"> <style scoped lang="scss">
.project-item { .project-item {
.options {
display: flex;
align-items: center;
justify-content: flex-end;
column-gap: 20px;
padding-right: 10px;
@media screen and (width <= 900px) {
column-gap: 10px;
}
}
&__button {
padding: 10px 16px;
box-shadow: 0 0 20px 0 rgb(0 0 0 / 4%);
@media screen and (width <= 900px) {
display: none;
}
}
&__mobile-button {
display: none;
padding: 10px 16px;
box-shadow: 0 0 20px 0 rgb(0 0 0 / 4%);
@media screen and (width <= 900px) {
display: flex;
}
}
&__menu { &__menu {
padding: 0 10px;
position: relative; position: relative;
cursor: pointer; cursor: pointer;
@ -176,8 +238,6 @@ async function goToProjectEdit(): Promise<void> {
&__content { &__content {
height: 32px; height: 32px;
width: 32px; width: 32px;
margin-left: auto;
margin-right: 0;
padding: 12px 5px; padding: 12px 5px;
border-radius: 5px; border-radius: 5px;
box-sizing: border-box; box-sizing: border-box;
@ -193,9 +253,9 @@ async function goToProjectEdit(): Promise<void> {
&__dropdown { &__dropdown {
position: absolute; position: absolute;
top: 55px; top: 40px;
right: 10px; right: 0;
background: var(--c-white); background: #fff;
box-shadow: 0 7px 20px rgb(0 0 0 / 15%); box-shadow: 0 7px 20px rgb(0 0 0 / 15%);
border: 1px solid var(--c-grey-2); border: 1px solid var(--c-grey-2);
border-radius: 8px; border-radius: 8px;

View File

@ -8,10 +8,10 @@
items-label="projects" items-label="projects"
> >
<template #head> <template #head>
<th class="sort-header-container__name-item align-left">Project</th> <th class="align-left">Project</th>
<th class="sort-header-container__date-item align-left">Date Added</th> <th class="date-added align-left">Date Added</th>
<th class="sort-header-container__date-item align-left">Members</th> <th class="members align-left">Members</th>
<th class="sort-header-container__date-item align-left">Role</th> <th class="role align-left">Role</th>
</template> </template>
<template #body> <template #body>
<project-table-invitation-item <project-table-invitation-item
@ -54,3 +54,28 @@ const projects = computed((): Project[] => {
}); });
</script> </script>
<style scoped lang="scss">
.projects-table {
@media screen and (width <= 550px) {
:deep(.table-footer), :deep(.base-table) {
border-radius: 0;
}
}
}
@media screen and (width <= 850px) {
.date-added, .members {
display: none;
}
}
@media screen and (width <= 600px) {
.role {
display: none;
}
}
</style>