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"
:class="{'overflow-visible': showBucketGuide(index)}"
>
<div v-if="Array.isArray(val)" class="few-items">
<p v-for="str in val" :key="str" class="array-val">{{ str }}</p>
<div v-if="Array.isArray(val)" class="few-items-container">
<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 v-else class="table-item">
<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 classes = {};
if (props.itemType === 'project') {
if (props.item['role'] === ProjectRole.Owner) {
classes['project-owner'] = true;
} else if (props.item['role'] === ProjectRole.Member) {
classes['project-member'] = true;
}
classes['project-owner'] = true;
} else if (props.itemType === 'shared-project') {
classes['project-member'] = true;
}
return classes;
});
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 {
@ -204,11 +207,27 @@ function cellContentClicked(cellIndex: number, event: Event) {
display: flex;
flex-direction: column;
&__title {
text-overflow: ellipsis;
overflow: hidden;
}
&__subtitle {
font-family: 'font_regular', sans-serif;
font-size: 12px;
line-height: 20px;
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],
['archive', ZipIcon],
['project', ProjectIcon],
['shared-project', ProjectIcon],
]);
static findIcon(type: string): string {

View File

@ -14,7 +14,7 @@
<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" />
<router-view />
@ -134,6 +134,10 @@ const sessionDuration = computed((): number => {
return duration;
});
const isMyProjectsPage = computed((): boolean => {
return route.path === RouteConfig.AllProjectsDashboard.path;
});
/**
* 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 {
box-sizing: border-box;
overflow-y: auto;

View File

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

View File

@ -3,41 +3,43 @@
<template>
<table-item
item-type="project"
item-type="shared-project"
:item="itemToRender"
class="invitation-item"
>
<template #options>
<th class="options overflow-visible">
<v-button
:loading="isLoading"
:disabled="isLoading"
:on-press="onJoinClicked"
border-radius="8px"
font-size="12px"
label="Join Project"
class="invitation-item__menu__button"
/>
<v-button
:loading="isLoading"
:disabled="isLoading"
:on-press="onJoinClicked"
border-radius="8px"
font-size="12px"
label="Join"
class="invitation-item__menu__mobile-button"
/>
<div class="invitation-item__menu">
<div class="invitation-item__menu__icon" @click.stop="toggleDropDown">
<div class="invitation-item__menu__icon__content" :class="{open: isDropdownOpen}">
<menu-icon />
<th class="overflow-visible">
<div class="options">
<v-button
:loading="isLoading"
:disabled="isLoading"
:on-press="onJoinClicked"
border-radius="8px"
font-size="12px"
label="Join Project"
class="invitation-item__button"
/>
<v-button
:loading="isLoading"
:disabled="isLoading"
:on-press="onJoinClicked"
border-radius="8px"
font-size="12px"
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__icon__content" :class="{open: isDropdownOpen}">
<menu-icon />
</div>
</div>
</div>
<div v-if="isDropdownOpen" v-click-outside="closeDropDown" class="invitation-item__menu__dropdown">
<div class="invitation-item__menu__dropdown__item" @click.stop="onDeclineClicked">
<logout-icon />
<p class="invitation-item__menu__dropdown__item__label">Decline invite</p>
<div v-if="isDropdownOpen" v-click-outside="closeDropDown" class="invitation-item__menu__dropdown">
<div class="invitation-item__menu__dropdown__item" @click.stop="onDeclineClicked">
<logout-icon />
<p class="invitation-item__menu__dropdown__item__label">Decline invite</p>
</div>
</div>
</div>
</div>
@ -59,6 +61,7 @@ import { useAppStore } from '@/store/modules/appStore';
import { useProjectsStore } from '@/store/modules/projectsStore';
import { useResize } from '@/composables/resize';
import { AnalyticsHttpApi } from '@/api/analytics';
import { useLoading } from '@/composables/useLoading';
import VButton from '@/components/common/VButton.vue';
import TableItem from '@/components/common/TableItem.vue';
@ -72,25 +75,36 @@ const notify = useNotify();
const analytics: AnalyticsHttpApi = new AnalyticsHttpApi();
const isLoading = ref<boolean>(false);
const { isLoading, withLoading } = useLoading();
const props = defineProps<{
invitation: ProjectInvitation,
}>();
const { isMobile } = useResize();
const { isMobile, screenWidth } = useResize();
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 {
multi: { title: props.invitation.projectName, subtitle: props.invitation.projectDescription },
date: props.invitation.invitedDate,
memberCount: '',
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.
*/
async function onDeclineClicked(): Promise<void> {
if (isLoading.value) return;
isLoading.value = true;
function onDeclineClicked(): void {
withLoading(async () => {
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 {
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 {
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;
try {
await projectsStore.getUserInvitations();
await projectsStore.getProjects();
} catch (error) {
notify.error(`Failed to reload projects and invitations list. ${error.message}`, AnalyticsErrorEventSource.PROJECT_INVITATION);
}
});
}
function toggleDropDown() {
@ -149,31 +160,35 @@ function closeDropDown() {
display: flex;
align-items: center;
justify-content: flex-end;
column-gap: 10px;
column-gap: 20px;
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 {
position: relative;
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 {
&__content {

View File

@ -3,28 +3,48 @@
<template>
<table-item
item-type="project"
:item-type="isOwner ? 'project' : 'shared-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>
<th class="overflow-visible">
<div class="options">
<v-button
:on-press="onOpenClicked"
is-white
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="isOwner" class="project-item__menu__dropdown__item" @click.stop="goToProjectEdit">
<gear-icon />
<p class="project-item__menu__dropdown__item__label">Project settings</p>
</div>
<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">
<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 class="project-item__menu__dropdown__item" @click.stop="goToProjectMembers">
<users-icon />
<p class="project-item__menu__dropdown__item__label">Invite members</p>
</div>
</div>
</div>
</div>
</th>
@ -53,6 +73,7 @@ import { useProjectsStore } from '@/store/modules/projectsStore';
import { useResize } from '@/composables/resize';
import TableItem from '@/components/common/TableItem.vue';
import VButton from '@/components/common/VButton.vue';
import UsersIcon from '@/../static/images/navigation/users.svg';
import GearIcon from '@/../static/images/common/gearIcon.svg';
@ -71,19 +92,30 @@ const props = defineProps<{
project: Project,
}>();
const { isMobile } = useResize();
const { isMobile, screenWidth } = useResize();
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 {
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,
};
}
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">
.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 {
padding: 0 10px;
position: relative;
cursor: pointer;
@ -176,8 +238,6 @@ async function goToProjectEdit(): Promise<void> {
&__content {
height: 32px;
width: 32px;
margin-left: auto;
margin-right: 0;
padding: 12px 5px;
border-radius: 5px;
box-sizing: border-box;
@ -193,9 +253,9 @@ async function goToProjectEdit(): Promise<void> {
&__dropdown {
position: absolute;
top: 55px;
right: 10px;
background: var(--c-white);
top: 40px;
right: 0;
background: #fff;
box-shadow: 0 7px 20px rgb(0 0 0 / 15%);
border: 1px solid var(--c-grey-2);
border-radius: 8px;

View File

@ -8,10 +8,10 @@
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>
<th class="align-left">Project</th>
<th class="date-added align-left">Date Added</th>
<th class="members align-left">Members</th>
<th class="role align-left">Role</th>
</template>
<template #body>
<project-table-invitation-item
@ -54,3 +54,28 @@ const projects = computed((): Project[] => {
});
</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>