web/satellite/vuetify-poc: unmock all projects dashboard data

The all projects dashboard of the Vuetify project has been updated to
display real project information rather than mock data. Additionally,
projects may now be selected and project invitations replied to through
the all projects dashboard.

Resolves #6036
Resolves #6038

Change-Id: I8aef0dc07c172e2bc8e2f7eb26a3d205bd56067f
This commit is contained in:
Jeremy Wharton 2023-07-21 04:52:32 -05:00 committed by Storj Robot
parent 9086078ac7
commit 5a03e29fca
16 changed files with 654 additions and 288 deletions

View File

@ -0,0 +1,3 @@
<svg width="18" height="18" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M18.5039 4.47638C18.7779 4.47638 19 4.69847 19 4.97244C19 5.24641 18.7779 5.4685 18.5039 5.4685L16.6817 5.46836L15.4073 15.876C15.2332 17.2977 14.0259 18.3661 12.5936 18.3661H7.52282C6.10137 18.3661 4.89972 17.3134 4.71279 15.9043L3.32838 5.46836L1.49606 5.4685C1.22209 5.4685 1 5.24641 1 4.97244C1 4.69847 1.22209 4.47638 1.49606 4.47638H18.5039ZM15.6822 5.4685H4.32943L5.6963 15.7738C5.81584 16.6749 6.57385 17.3519 7.47823 17.3735L7.52282 17.374H12.5936C13.5099 17.374 14.2844 16.7014 14.4166 15.7993L14.4225 15.7554L15.6822 5.4685ZM8.47662 8.91592L8.48146 8.9424L8.4849 8.96941L8.98001 13.9215L8.98375 13.9527C9.01639 14.2247 8.82233 14.4717 8.55031 14.5043C8.28736 14.5358 8.04782 14.3556 8.00266 14.0978L7.99869 14.0709L7.99388 14.03L7.4977 9.06813C7.47043 8.79552 7.66933 8.55243 7.94194 8.52517C8.19637 8.49973 8.42509 8.67129 8.47662 8.91592ZM12.3071 8.4919L12.3342 8.49334C12.5982 8.51448 12.7973 8.73863 12.7906 9.00024L12.7891 9.02742L12.7857 9.06844L12.2899 14.0266C12.2627 14.2992 12.0196 14.4981 11.747 14.4708C11.4834 14.4445 11.2888 14.2164 11.3007 13.955L11.3027 13.9278L11.7977 8.97947L11.8002 8.94821C11.8206 8.69332 12.0302 8.49893 12.2801 8.4919H12.3071ZM12.8346 1.5C13.1086 1.5 13.3307 1.72209 13.3307 1.99606C13.3307 2.27003 13.1086 2.49213 12.8346 2.49213H7.16535C6.89139 2.49213 6.66929 2.27003 6.66929 1.99606C6.66929 1.72209 6.89139 1.5 7.16535 1.5H12.8346Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -22,9 +22,9 @@
hover
>
<template #item.name="{ item }">
<v-list-item class="font-weight-bold pl-0">
{{ item.columns.name }}
</v-list-item>
<span class="font-weight-bold">
{{ item.raw.name }}
</span>
</template>
<template #item.status="{ item }">
<v-chip :color="item.raw.status == 'Active' ? 'success' : 'warning'" variant="tonal" size="small" rounded="xl" class="font-weight-bold">
@ -37,7 +37,7 @@
<script setup lang="ts">
import { ref } from 'vue';
import { VCard, VTextField, VListItem, VChip } from 'vuetify/components';
import { VCard, VTextField, VChip } from 'vuetify/components';
import { VDataTable } from 'vuetify/labs/components';
const search = ref<string>('');

View File

@ -22,13 +22,18 @@
show-select
>
<template #item.name="{ item }">
<v-list-item class="rounded-lg font-weight-bold pl-1" link @click="previewFile">
<template #prepend>
<!-- Filetype icons -->
<div>
<v-btn
class="rounded-lg w-100 pl-1 pr-4 justify-start font-weight-bold"
variant="text"
height="40"
color="default"
@click="previewFile"
>
<img :src="icons.get(item.raw.icon) || fileIcon" alt="Item icon" class="mr-3">
</template>
{{ item.columns.name }}
</v-list-item>
{{ item.raw.name }}
</v-btn>
</div>
</template>
</v-data-table>
@ -146,7 +151,6 @@ import { ref } from 'vue';
import {
VCard,
VTextField,
VListItem,
VDialog,
VCarousel,
VBtn,

View File

@ -21,12 +21,18 @@
show-select
>
<template #item.name="{ item }">
<v-list-item class="rounded-lg font-weight-bold pl-1" link router-link to="/bucket">
<template #prepend>
<div>
<v-btn
class="rounded-lg w-100 pl-1 pr-4 justify-start font-weight-bold"
variant="text"
height="40"
color="default"
to="/bucket"
>
<img src="../assets/icon-bucket-tonal.svg" alt="Bucket" class="mr-3">
</template>
{{ item.columns.name }}
</v-list-item>
{{ item.raw.name }}
</v-btn>
</div>
</template>
</v-data-table>
</v-card>
@ -34,7 +40,7 @@
<script setup lang="ts">
import { ref } from 'vue';
import { VCard, VTextField, VListItem } from 'vuetify/components';
import { VCard, VTextField, VBtn } from 'vuetify/components';
import { VDataTable } from 'vuetify/labs/components';
const props = defineProps<{

View File

@ -0,0 +1,143 @@
// Copyright (C) 2023 Storj Labs, Inc.
// See LICENSE for copying information.
<template>
<v-card variant="flat" :border="true" rounded="xlg">
<div class="h-100 d-flex flex-column justify-space-between">
<v-card-item>
<div class="d-flex justify-space-between">
<v-chip rounded :color="item ? PROJECT_ROLE_COLORS[item.role] : 'primary'" variant="tonal" class="font-weight-bold my-2" size="small">
<icon-project width="12px" class="mr-1" />
{{ item?.role || 'Project' }}
</v-chip>
<v-btn v-if="item?.role === ProjectRole.Owner" color="default" variant="text" size="small">
<v-icon icon="mdi-dots-vertical" />
<v-menu activator="parent" location="end" transition="scale-transition">
<v-list class="pa-2">
<v-list-item link rounded="lg">
<template #prepend>
<icon-settings />
</template>
<v-list-item-title class="text-body-2 ml-3">
Project Settings
</v-list-item-title>
</v-list-item>
<v-divider class="my-2" />
<v-list-item link class="mt-1" rounded="lg">
<template #prepend>
<icon-team />
</template>
<v-list-item-title class="text-body-2 ml-3">
Invite Members
</v-list-item-title>
</v-list-item>
</v-list>
</v-menu>
</v-btn>
</div>
<v-card-title :class="{ 'text-primary': item && item.role !== ProjectRole.Invited }">
<a v-if="item && item.role !== ProjectRole.Invited" class="link" @click="openProject">
{{ item.name }}
</a>
<template v-else>
{{ item ? item.name : 'Welcome' }}
</template>
</v-card-title>
<v-card-subtitle v-if="!item || item.description">
{{ item ? item.description : 'Create a project to get started.' }}
</v-card-subtitle>
</v-card-item>
<v-card-text class="flex-grow-0">
<v-divider class="mt-1 mb-4" />
<v-btn v-if="!item" color="primary" size="small" class="mr-2">Create Project</v-btn>
<template v-else-if="item?.role === ProjectRole.Invited">
<v-btn color="primary" size="small" class="mr-2" :disabled="isDeclining" @click="emit('joinClick')">
Join Project
</v-btn>
<v-btn
variant="outlined"
color="default"
size="small"
class="mr-2"
:loading="isDeclining"
@click="declineInvitation"
>
Decline
</v-btn>
</template>
<v-btn v-else color="primary" size="small" class="mr-2" @click="openProject">Open Project</v-btn>
</v-card-text>
</div>
</v-card>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { useRouter } from 'vue-router';
import {
VCard,
VCardItem,
VChip,
VBtn,
VIcon,
VMenu,
VList,
VListItem,
VListItemTitle,
VDivider,
VCardTitle,
VCardSubtitle,
VCardText,
} from 'vuetify/components';
import { ProjectItemModel, PROJECT_ROLE_COLORS } from '@poc/types/projects';
import { ProjectInvitationResponse } from '@/types/projects';
import { ProjectRole } from '@/types/projectMembers';
import { useProjectsStore } from '@/store/modules/projectsStore';
import { LocalData } from '@/utils/localData';
import IconProject from '@poc/components/icons/IconProject.vue';
import IconSettings from '@poc/components/icons/IconSettings.vue';
import IconTeam from '@poc/components/icons/IconTeam.vue';
const props = defineProps<{
item?: ProjectItemModel,
}>();
const emit = defineEmits<{
(event: 'joinClick'): void;
}>();
const projectsStore = useProjectsStore();
const router = useRouter();
const isDeclining = ref<boolean>(false);
/**
* Selects the project and navigates to the project dashboard.
*/
function openProject(): void {
if (!props.item) return;
projectsStore.selectProject(props.item.id);
LocalData.setSelectedProjectId(props.item.id);
router.push('/dashboard');
}
/**
* Declines the project invitation.
*/
async function declineInvitation(): Promise<void> {
if (!props.item || isDeclining.value) return;
isDeclining.value = true;
await projectsStore.respondToInvitation(props.item.id, ProjectInvitationResponse.Decline).catch(_ => {});
await projectsStore.getUserInvitations().catch(_ => {});
await projectsStore.getProjects().catch(_ => {});
isDeclining.value = false;
}
</script>

View File

@ -12,39 +12,97 @@
/>
<v-data-table
v-model="selected"
:sort-by="sortBy"
:headers="headers"
:items="files"
:items="items"
:search="search"
class="elevation-1"
item-key="path"
>
<template #item.name="{ item }">
<v-list-item class="rounded-lg font-weight-bold pl-1" link router-link to="/dashboard">
<template #prepend>
<img src="../assets/icon-project-tonal.svg" alt="Bucket" class="mr-3">
<!-- Project Icon -->
<!-- <v-chip :color="getColor(item.raw.role)" variant="tonal" size="small" class="ml-2 mr-2" rounded>
<svg width="16" height="16" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill="currentColor" d="M10.0567 1.05318L10.0849 1.06832L17.4768 5.33727C17.6249 5.42279 17.7084 5.5642 17.7274 5.71277L17.7301 5.73906L17.7314 5.76543L17.7316 14.2407C17.7316 14.4124 17.6452 14.5718 17.5032 14.6657L17.476 14.6826L10.084 18.9322C9.93533 19.0176 9.75438 19.0223 9.60224 18.9463L9.57407 18.9311L2.25387 14.6815C2.10601 14.5956 2.01167 14.4418 2.00108 14.2726L2 14.2407V5.77863L2.00023 5.75974C1.99504 5.58748 2.07759 5.41781 2.22993 5.31793L2.25457 5.30275L9.57482 1.06849C9.71403 0.987969 9.88182 0.978444 10.0278 1.03993L10.0567 1.05318ZM16.7123 6.6615L10.3397 10.3418V17.6094L16.7123 13.9458V6.6615ZM3.01944 6.66573V13.947L9.32037 17.605V10.3402L3.01944 6.66573ZM9.83034 2.09828L3.49475 5.76287L9.83122 9.45833L16.2029 5.7786L9.83034 2.09828Z"/>
</svg>
</v-chip> -->
</template>
{{ item.columns.name }}
</v-list-item>
<div>
<v-btn
class="rounded-lg w-100 pl-1 pr-4 justify-start font-weight-bold"
variant="text"
height="40"
color="default"
@click="openProject(item.raw)"
>
<img src="../assets/icon-project-tonal.svg" alt="Project" class="mr-3">
{{ item.raw.name }}
</v-btn>
</div>
</template>
<template #item.role="{ item }">
<v-chip :color="getColor(item.raw.role)" rounded="xl" size="small" class="font-weight-bold">
<v-chip :color="PROJECT_ROLE_COLORS[item.raw.role]" rounded="xl" size="small" class="font-weight-bold">
{{ item.raw.role }}
</v-chip>
</template>
<template #item.createdAt="{ item }">
{{ getFormattedDate(item.raw.createdAt) }}
</template>
<template #item.actions="{ item }">
<v-btn size="small" link router-link to="/dashboard">
{{ item.raw.actions }}
</v-btn>
<div class="w-100 d-flex align-center justify-space-between">
<v-btn
v-if="item.raw.role === ProjectRole.Invited"
color="primary"
size="small"
:disabled="decliningIds.has(item.raw.id)"
@click="emit('joinClick', item.raw)"
>
Join Project
</v-btn>
<v-btn v-else color="primary" size="small" @click="openProject(item.raw)">Open Project</v-btn>
<v-btn
v-if="item.raw.role === ProjectRole.Owner || item.raw.role === ProjectRole.Invited"
class="ml-2"
icon
color="default"
variant="text"
size="small"
density="comfortable"
:loading="decliningIds.has(item.raw.id)"
>
<v-icon icon="mdi-dots-vertical" size="18" />
<v-menu activator="parent" location="bottom end" transition="scale-transition">
<v-list class="pa-0">
<template v-if="item.raw.role === ProjectRole.Owner">
<v-list-item link>
<template #prepend>
<icon-settings />
</template>
<v-list-item-title class="text-body-2 ml-3">
Project Settings
</v-list-item-title>
</v-list-item>
<v-divider />
<v-list-item link>
<template #prepend>
<icon-team />
</template>
<v-list-item-title class="text-body-2 ml-3">
Invite Members
</v-list-item-title>
</v-list-item>
</template>
<v-list-item v-else link @click="declineInvitation(item.raw)">
<template #prepend>
<img src="@poc/assets/icon-trash.svg" alt="Decline">
</template>
<v-list-item-title class="text-body-2 ml-3">
Decline
</v-list-item-title>
</v-list-item>
</v-list>
</v-menu>
</v-btn>
</div>
</template>
</v-data-table>
</v-card>
@ -52,51 +110,81 @@
<script setup lang="ts">
import { ref } from 'vue';
import { VCard, VTextField, VListItem, VChip, VBtn } from 'vuetify/components';
import { useRouter } from 'vue-router';
import {
VCard,
VTextField,
VListItem,
VChip,
VBtn,
VMenu,
VList,
VIcon,
VListItemTitle,
VDivider,
} from 'vuetify/components';
import { VDataTable } from 'vuetify/labs/components';
import { ProjectItemModel, PROJECT_ROLE_COLORS } from '@poc/types/projects';
import { ProjectInvitationResponse } from '@/types/projects';
import { ProjectRole } from '@/types/projectMembers';
import { SHORT_MONTHS_NAMES } from '@/utils/constants/date';
import { useProjectsStore } from '@/store/modules/projectsStore';
import { LocalData } from '@/utils/localData';
import IconSettings from '@poc/components/icons/IconSettings.vue';
import IconTeam from '@poc/components/icons/IconTeam.vue';
const props = defineProps<{
items: ProjectItemModel[],
}>();
const emit = defineEmits<{
(event: 'joinClick', item: ProjectItemModel): void;
}>();
const search = ref<string>('');
const selected = ref([]);
const decliningIds = ref(new Set<string>());
const projectsStore = useProjectsStore();
const router = useRouter();
const sortBy = [{ key: 'name', order: 'asc' }];
const headers = [
{
title: 'Project',
align: 'start',
key: 'name',
},
{ title: 'Role', key:'role' },
{ title: 'Members', key: 'size' },
{ title: 'Date Added', key: 'date' },
{ title: 'Actions', key: 'actions' },
];
const files = [
{
name: 'My First Project',
role: 'Owner',
size: '1',
date: '02 Mar 2023',
actions: 'Open Project',
},
{
name: 'Storj Labs',
role: 'Member',
size: '23',
date: '21 Apr 2023',
actions: 'Open Project',
},
{
name: 'Invitation Project',
role: 'Invited',
size: '15',
date: '24 Mar 2023',
actions: 'Join Project',
},
{ title: 'Project', key: 'name', align: 'start' },
{ title: 'Role', key: 'role' },
{ title: 'Members', key: 'memberCount' },
{ title: 'Date Added', key: 'createdAt' },
{ title: 'Actions', key: 'actions', sortable: false, width: '0' },
];
function getColor(role: string): string {
if (role === 'Owner') return 'purple2';
if (role === 'Invited') return 'warning';
return 'green';
/**
* Formats the given project creation date.
*/
function getFormattedDate(date: Date): string {
return `${date.getDate()} ${SHORT_MONTHS_NAMES[date.getMonth()]} ${date.getFullYear()}`;
}
/**
* Selects the project and navigates to the project dashboard.
*/
function openProject(item: ProjectItemModel): void {
projectsStore.selectProject(item.id);
LocalData.setSelectedProjectId(item.id);
router.push('/dashboard');
}
/**
* Declines the project invitation.
*/
async function declineInvitation(item: ProjectItemModel): Promise<void> {
if (decliningIds.value.has(item.id)) return;
decliningIds.value.add(item.id);
await projectsStore.respondToInvitation(item.id, ProjectInvitationResponse.Decline).catch(_ => {});
await projectsStore.getUserInvitations().catch(_ => {});
await projectsStore.getProjects().catch(_ => {});
decliningIds.value.delete(item.id);
}
</script>

View File

@ -22,15 +22,12 @@
hover
>
<template #item.name="{ item }">
<v-list-item class="font-weight-bold pl-0">
<!-- <template v-slot:prepend>
<img src="../assets/icon-user-color.svg" alt="Dashboard" class="mr-3">
</template> -->
{{ item.columns.name }}
</v-list-item>
<span class="font-weight-bold">
{{ item.raw.name }}
</span>
</template>
<template #item.role="{ item }">
<v-chip :color="roleColors.get(item.raw.role)" variant="tonal" size="small" rounded="xl" class="font-weight-bold">
<v-chip :color="PROJECT_ROLE_COLORS[item.raw.role]" variant="tonal" size="small" rounded="xl" class="font-weight-bold">
{{ item.raw.role }}
</v-chip>
</template>
@ -40,12 +37,13 @@
<script setup lang="ts">
import { computed, ref } from 'vue';
import { VCard, VTextField, VListItem, VChip } from 'vuetify/components';
import { VCard, VTextField, VChip } from 'vuetify/components';
import { VDataTable } from 'vuetify/labs/components';
import { useProjectMembersStore } from '@/store/modules/projectMembersStore';
import { useProjectsStore } from '@/store/modules/projectsStore';
import { ProjectInvitationItemModel, ProjectRole } from '@/types/projectMembers';
import { PROJECT_ROLE_COLORS } from '@poc/types/projects';
const pmStore = useProjectMembersStore();
const projectsStore = useProjectsStore();
@ -64,13 +62,6 @@ const headers = ref([
{ title: 'Date Added', key: 'date' },
]);
const roleColors = new Map<ProjectRole, string>([
[ProjectRole.Member, 'green'],
[ProjectRole.Owner, 'purple2'],
[ProjectRole.Invited, 'warning'],
[ProjectRole.InviteExpired, 'error'],
]);
/**
* Returns team members of current page from store.
* With project owner pinned to top

View File

@ -0,0 +1,130 @@
// Copyright (C) 2023 Storj Labs, Inc.
// See LICENSE for copying information.
<template>
<v-dialog
v-model="model"
width="auto"
min-width="400px"
transition="fade-transition"
>
<v-card rounded="xlg">
<v-card-item class="pl-7 pa-4">
<template #prepend>
<v-card-title class="font-weight-bold">Join Project</v-card-title>
</template>
<template #append>
<v-btn
icon="$close"
variant="text"
size="small"
color="default"
:disabled="isAccepting || isDeclining"
@click="model = false"
/>
</template>
</v-card-item>
<v-divider />
<div class="px-7 py-4">Join the {{ name }} project.</div>
<v-divider />
<v-card-actions class="pa-7">
<v-row>
<v-col>
<v-btn
variant="outlined"
color="default"
block
:disabled="isAccepting"
:loading="isDeclining"
@click="respondToInvitation(ProjectInvitationResponse.Decline)"
>
Decline
</v-btn>
</v-col>
<v-col>
<v-btn
color="primary"
variant="flat"
block
:disabled="isDeclining"
:loading="isAccepting"
@click="respondToInvitation(ProjectInvitationResponse.Accept)"
>
Join Project
</v-btn>
</v-col>
</v-row>
</v-card-actions>
</v-card>
</v-dialog>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue';
import { useRouter } from 'vue-router';
import {
VDialog,
VCard,
VCardItem,
VCardTitle,
VDivider,
VCardActions,
VRow,
VCol,
VBtn,
} from 'vuetify/components';
import { ProjectInvitationResponse } from '@/types/projects';
import { useProjectsStore } from '@/store/modules/projectsStore';
import { LocalData } from '@/utils/localData';
const props = defineProps<{
modelValue: boolean,
name: string,
id: string,
}>();
const model = computed<boolean>({
get: () => props.modelValue,
set: value => emit('update:modelValue', value),
});
const emit = defineEmits<{
(event: 'update:modelValue', value: boolean): void,
}>();
const projectsStore = useProjectsStore();
const router = useRouter();
const isAccepting = ref<boolean>(false);
const isDeclining = ref<boolean>(false);
/**
* Selects the project and navigates to the project dashboard.
*/
function openProject(): void {
projectsStore.selectProject(props.id);
LocalData.setSelectedProjectId(props.id);
router.push('/dashboard');
}
/**
* Accepts or declines the project invitation.
*/
async function respondToInvitation(response: ProjectInvitationResponse): Promise<void> {
if (isDeclining.value || isAccepting.value) return;
const isLoading = response === ProjectInvitationResponse.Accept ? isAccepting : isDeclining;
isLoading.value = true;
let success = false;
await projectsStore.respondToInvitation(props.id, response).then(() => { success = true; }).catch(_ => {});
await projectsStore.getUserInvitations().catch(_ => {});
await projectsStore.getProjects().catch(_ => { success = false; });
if (response === ProjectInvitationResponse.Accept && success) openProject();
isLoading.value = false;
}
</script>

View File

@ -11,7 +11,7 @@
<script setup lang="ts">
import { onBeforeMount } from 'vue';
import { useRouter } from 'vue-router';
import { onBeforeRouteLeave, useRouter } from 'vue-router';
import { VApp } from 'vuetify/components';
import DefaultBar from './AppBar.vue';

View File

@ -105,13 +105,13 @@
<IconProject />
</template>
<v-list-item-title link class="text-body-2 ml-3">
Project
{{ selectedProject.name }}
</v-list-item-title>
<v-list-item-subtitle class="ml-3">
My first project
{{ selectedProject.description }}
</v-list-item-subtitle>
<template #append>
<img src="@poc/assets/icon-right.svg" alt="Project" width="10">
<img src="@poc/assets/icon-right.svg" class="ml-3" alt="Project" width="10">
</template>
</v-list-item>
@ -234,6 +234,7 @@
</template>
<script setup lang="ts">
import { computed } from 'vue';
import {
VNavigationDrawer,
VSheet,
@ -246,6 +247,9 @@ import {
VDivider,
} from 'vuetify/components';
import { Project } from '@/types/projects';
import { useProjectsStore } from '@/store/modules/projectsStore';
import IconProject from '@poc/components/icons/IconProject.vue';
import IconSettings from '@poc/components/icons/IconSettings.vue';
import IconAllProjects from '@poc/components/icons/IconAllProjects.vue';
@ -260,4 +264,13 @@ import IconDocs from '@poc/components/icons/IconDocs.vue';
import IconForum from '@poc/components/icons/IconForum.vue';
import IconSupport from '@poc/components/icons/IconSupport.vue';
import IconResources from '@poc/components/icons/IconResources.vue';
const projectsStore = useProjectsStore();
/**
* Returns the selected project from the store.
*/
const selectedProject = computed((): Project => {
return projectsStore.state.selectedProject;
});
</script>

View File

@ -226,12 +226,6 @@
letter-spacing: 1px;
}
// Table content
.v-list-item__content {
white-space: nowrap;
overflow: visible;
}
// Table Footer
.v-data-table-footer {
font-size: 14px;
@ -270,6 +264,7 @@ table {
.link:hover {
color: rgb(var(--v-theme-secondary));
background-size: 100% 1px;
cursor: pointer;
}
@ -287,4 +282,4 @@ table {
// Upload Snackbar
.upload-snackbar .v-snackbar__content {
padding: 2px;
}
}

View File

@ -0,0 +1,33 @@
// Copyright (C) 2023 Storj Labs, Inc.
// See LICENSE for copying information.
import { ProjectRole } from '@/types/projectMembers';
/**
* ProjectItemModel represents the view model for project items in the all projects dashboard.
*/
export class ProjectItemModel {
public constructor(
public id: string,
public name: string,
public description: string,
public role: ProjectItemRole,
public memberCount: number | null,
public createdAt: Date,
) {}
}
/**
* ProjectItemRole represents the role of a user for a project item.
*/
export type ProjectItemRole = Exclude<ProjectRole, ProjectRole.InviteExpired>;
/**
* PROJECT_ROLE_COLORS defines what colors project role tags should use.
*/
export const PROJECT_ROLE_COLORS: Record<ProjectRole, string> = {
[ProjectRole.Member]: 'green',
[ProjectRole.Owner]: 'purple2',
[ProjectRole.Invited]: 'warning',
[ProjectRole.InviteExpired]: 'error',
};

View File

@ -178,12 +178,9 @@
hover
>
<template #item.date="{ item }">
<v-list-item class="font-weight-bold pl-0">
<!-- <template v-slot:prepend>
<img src="../assets/icon-user-color.svg" alt="Dashboard" class="mr-3">
</template> -->
{{ item.columns.date }}
</v-list-item>
<span class="font-weight-bold">
{{ item.raw.date }}
</span>
</template>
<template #item.status="{ item }">
<v-chip :color="getColor(item.raw.status)" variant="tonal" size="small" rounded="xl" class="font-weight-bold">
@ -215,12 +212,9 @@
hover
>
<template #item.date="{ item }">
<v-list-item class="font-weight-bold pl-0">
<!-- <template v-slot:prepend>
<img src="../assets/icon-user-color.svg" alt="Dashboard" class="mr-3">
</template> -->
{{ item.columns.date }}
</v-list-item>
<span class="font-weight-bold">
{{ item.raw.date }}
</span>
</template>
<template #item.status="{ item }">
<v-chip :color="getColor(item.raw.status)" variant="tonal" size="small" rounded="xl" class="font-weight-bold">
@ -267,7 +261,6 @@ import {
VExpansionPanels,
VExpansionPanel,
VTextField,
VListItem,
} from 'vuetify/components';
import { VDataTable } from 'vuetify/labs/components';

View File

@ -43,7 +43,7 @@ import PageTitleComponent from '@poc/components/PageTitleComponent.vue';
import BrowserBreadcrumbsComponent from '@poc/components/BrowserBreadcrumbsComponent.vue';
import BrowserSnackbarComponent from '@poc/components/BrowserSnackbarComponent.vue';
import BrowserTableComponent from '@poc/components/BrowserTableComponent.vue';
import BrowserNewFolderDialog from '@poc/components/BrowserNewFolderDialog.vue';
import BrowserNewFolderDialog from '@poc/components/dialogs/BrowserNewFolderDialog.vue';
import IconUpload from '@poc/components/icons/IconUpload.vue';
import IconFolder from '@poc/components/icons/IconFolder.vue';

View File

@ -117,181 +117,89 @@
</v-btn>
</v-col>
<v-spacer />
<template v-if="items.length">
<v-spacer />
<v-col class="text-right">
<!-- Projects Card/Table View -->
<v-btn-toggle
v-model="activeView"
mandatory
border
inset
density="comfortable"
class="pa-1"
>
<v-btn
size="small"
rounded="xl"
active-class="active"
:active="activeView === 'cards'"
aria-label="Toggle Cards View"
@click="toggleView('cards')"
<v-col class="text-right">
<!-- Projects Card/Table View -->
<v-btn-toggle
mandatory
border
inset
density="comfortable"
class="pa-1"
>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="6.99902" y="6.99951" width="4.0003" height="4.0003" rx="1" fill="currentColor" />
<rect x="6.99902" y="13.0005" width="4.0003" height="4.0003" rx="1" fill="currentColor" />
<rect x="12.999" y="6.99951" width="4.0003" height="4.0003" rx="1" fill="currentColor" />
<rect x="12.999" y="13.0005" width="4.0003" height="4.0003" rx="1" fill="currentColor" />
</svg>
Cards
</v-btn>
<v-btn
size="small"
rounded="xl"
active-class="active"
:active="activeView === 'table'"
aria-label="Toggle Table View"
@click="toggleView('table')"
>
<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 8C9 8.55228 8.55228 9 8 9V9C7.44772 9 7 8.55228 7 8V8C7 7.44772 7.44772 7 8 7V7C8.55228 7 9 7.44772 9 8V8Z" fill="currentColor" />
<path fill-rule="evenodd" clip-rule="evenodd" d="M9 12C9 12.5523 8.55228 13 8 13V13C7.44772 13 7 12.5523 7 12V12C7 11.4477 7.44772 11 8 11V11C8.55228 11 9 11.4477 9 12V12Z" fill="currentColor" />
<path fill-rule="evenodd" clip-rule="evenodd" d="M9 16C9 16.5523 8.55228 17 8 17V17C7.44772 17 7 16.5523 7 16V16C7 15.4477 7.44772 15 8 15V15C8.55228 15 9 15.4477 9 16V16Z" fill="currentColor" />
<path fill-rule="evenodd" clip-rule="evenodd" d="M18 8C18 8.55228 17.5523 9 17 9H11C10.4477 9 10 8.55228 10 8V8C10 7.44772 10.4477 7 11 7H17C17.5523 7 18 7.44772 18 8V8Z" fill="currentColor" />
<path fill-rule="evenodd" clip-rule="evenodd" d="M18 12C18 12.5523 17.5523 13 17 13H11C10.4477 13 10 12.5523 10 12V12C10 11.4477 10.4477 11 11 11H17C17.5523 11 18 11.4477 18 12V12Z" fill="currentColor" />
<path fill-rule="evenodd" clip-rule="evenodd" d="M18 16C18 16.5523 17.5523 17 17 17H11C10.4477 17 10 16.5523 10 16V16C10 15.4477 10.4477 15 11 15H17C17.5523 15 18 15.4477 18 16V16Z" fill="currentColor" />
</svg>
Table
</v-btn>
</v-btn-toggle>
</v-col>
<v-btn
size="small"
rounded="xl"
active-class="active"
:active="!isTableView"
aria-label="Toggle Cards View"
@click="isTableView = false"
>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="6.99902" y="6.99951" width="4.0003" height="4.0003" rx="1" fill="currentColor" />
<rect x="6.99902" y="13.0005" width="4.0003" height="4.0003" rx="1" fill="currentColor" />
<rect x="12.999" y="6.99951" width="4.0003" height="4.0003" rx="1" fill="currentColor" />
<rect x="12.999" y="13.0005" width="4.0003" height="4.0003" rx="1" fill="currentColor" />
</svg>
Cards
</v-btn>
<v-btn
size="small"
rounded="xl"
active-class="active"
:active="isTableView"
aria-label="Toggle Table View"
@click="isTableView = true"
>
<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 8C9 8.55228 8.55228 9 8 9V9C7.44772 9 7 8.55228 7 8V8C7 7.44772 7.44772 7 8 7V7C8.55228 7 9 7.44772 9 8V8Z" fill="currentColor" />
<path fill-rule="evenodd" clip-rule="evenodd" d="M9 12C9 12.5523 8.55228 13 8 13V13C7.44772 13 7 12.5523 7 12V12C7 11.4477 7.44772 11 8 11V11C8.55228 11 9 11.4477 9 12V12Z" fill="currentColor" />
<path fill-rule="evenodd" clip-rule="evenodd" d="M9 16C9 16.5523 8.55228 17 8 17V17C7.44772 17 7 16.5523 7 16V16C7 15.4477 7.44772 15 8 15V15C8.55228 15 9 15.4477 9 16V16Z" fill="currentColor" />
<path fill-rule="evenodd" clip-rule="evenodd" d="M18 8C18 8.55228 17.5523 9 17 9H11C10.4477 9 10 8.55228 10 8V8C10 7.44772 10.4477 7 11 7H17C17.5523 7 18 7.44772 18 8V8Z" fill="currentColor" />
<path fill-rule="evenodd" clip-rule="evenodd" d="M18 12C18 12.5523 17.5523 13 17 13H11C10.4477 13 10 12.5523 10 12V12C10 11.4477 10.4477 11 11 11H17C17.5523 11 18 11.4477 18 12V12Z" fill="currentColor" />
<path fill-rule="evenodd" clip-rule="evenodd" d="M18 16C18 16.5523 17.5523 17 17 17H11C10.4477 17 10 16.5523 10 16V16C10 15.4477 10.4477 15 11 15H17C17.5523 15 18 15.4477 18 16V16Z" fill="currentColor" />
</svg>
Table
</v-btn>
</v-btn-toggle>
</v-col>
</template>
</v-row>
<v-row v-if="activeView === 'cards'">
<!-- Card view -->
<v-col cols="12" sm="6" md="4" lg="3">
<v-card variant="flat" :border="true" rounded="xlg">
<v-card-item>
<div class="d-flex justify-space-between">
<v-chip rounded color="purple2" variant="tonal" class="font-weight-bold my-2" size="small"><IconProject width="12px" class="mr-1" />Owner</v-chip>
<!-- <v-btn color="default" variant="text" size="small" icon="mdi-dots-vertical">
</v-btn> -->
<v-btn color="default" variant="text" size="small">
<v-icon icon="mdi-dots-vertical" />
<v-menu activator="parent" location="end" transition="scale-transition">
<!-- Project Menu -->
<v-list class="pa-2">
<!-- Project Settings -->
<v-list-item link rounded="lg">
<template #prepend>
<IconSettings />
</template>
<v-list-item-title class="text-body-2 ml-3">
Project Settings
</v-list-item-title>
</v-list-item>
<v-divider class="my-2" />
<!-- Invite Members -->
<v-list-item link class="mt-1" rounded="lg">
<template #prepend>
<IconTeam />
</template>
<v-list-item-title class="text-body-2 ml-3">
Invite Members
</v-list-item-title>
</v-list-item>
</v-list>
</v-menu>
</v-btn>
</div>
<v-card-title>
<router-link class="link" to="/dashboard">My first project</router-link>
</v-card-title>
<v-card-subtitle>
<p>Project Description</p>
</v-card-subtitle>
</v-card-item>
<v-card-text>
<v-divider class="mt-1 mb-4" />
<v-btn color="primary" size="small" class="mr-2" link router-link to="/dashboard">Open Project</v-btn>
</v-card-text>
</v-card>
</v-col>
<v-col cols="12" sm="6" md="4" lg="3">
<v-card variant="flat" :border="true" rounded="xlg">
<v-card-item>
<v-chip rounded color="green" variant="tonal" class="font-weight-bold my-2" size="small"><IconProject width="12px" class="mr-1" />Member</v-chip>
<v-card-title>
<router-link class="link" to="/dashboard">Storj Labs</router-link>
</v-card-title>
<v-card-subtitle>
<p>Shared team project</p>
</v-card-subtitle>
</v-card-item>
<v-card-text>
<v-divider class="mt-1 mb-4" />
<v-btn color="primary" size="small" class="mr-2" link router-link to="/dashboard">Open Project</v-btn>
</v-card-text>
</v-card>
</v-col>
<v-col cols="12" sm="6" md="4" lg="3">
<v-card variant="flat" :border="true" rounded="xlg">
<v-card-item>
<v-chip rounded color="warning" variant="tonal" class="font-weight-bold my-2" size="small"><IconProject width="12px" class="mr-1" />Invited</v-chip>
<v-card-title>
Invitation Project
</v-card-title>
<v-card-subtitle>
<p>Example invitation.</p>
</v-card-subtitle>
</v-card-item>
<v-card-text>
<v-divider class="mt-1 mb-4" />
<v-btn color="primary" size="small" class="mr-2" link router-link to="/dashboard">Join Project</v-btn>
<v-btn variant="outlined" color="default" size="small" class="mr-2">Decline</v-btn>
</v-card-text>
</v-card>
</v-col>
<v-col cols="12" sm="6" md="4" lg="3">
<v-card variant="flat" :border="true" rounded="xlg">
<v-card-item>
<v-chip rounded color="primary" variant="tonal" class="font-weight-bold my-2" size="small"><IconProject width="12px" class="mr-1" />Project</v-chip>
<v-card-title>
Welcome
</v-card-title>
<v-card-subtitle>
<p>Create a project to get started.</p>
</v-card-subtitle>
</v-card-item>
<v-card-text>
<v-divider class="mt-1 mb-4" />
<v-btn color="primary" size="small" class="mr-2" link router-link to="/dashboard">Create Project</v-btn>
</v-card-text>
</v-card>
</v-col>
<v-row v-if="isLoading" class="justify-center">
<v-progress-circular indeterminate color="primary" size="48" />
</v-row>
<v-row v-else-if="activeView === 'table'">
<v-row v-else-if="isTableView">
<!-- Table view -->
<v-col>
<ProjectsTableComponent />
<ProjectsTableComponent :items="items" @join-click="onJoinClicked" />
</v-col>
</v-row>
<v-row />
<v-row v-else>
<!-- Card view -->
<v-col v-if="!items.length" cols="12" sm="6" md="4" lg="3">
<ProjectCard class="h-100" />
</v-col>
<v-col v-for="item in items" v-else :key="item.id" cols="12" sm="6" md="4" lg="3">
<ProjectCard :item="item" class="h-100" @join-click="onJoinClicked(item)" />
</v-col>
</v-row>
</v-container>
<join-project-dialog
v-if="joiningItem"
:id="joiningItem.id"
v-model="isJoinProjectDialogShown"
:name="joiningItem.name"
/>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { computed, onMounted, ref } from 'vue';
import {
VContainer,
VRow,
@ -308,34 +216,93 @@ import {
VCardActions,
VSpacer,
VBtnToggle,
VChip,
VIcon,
VMenu,
VList,
VListItem,
VListItemTitle,
VCardSubtitle,
VCardText,
VProgressCircular,
} from 'vuetify/components';
import { ProjectItemModel } from '@poc/types/projects';
import { useProjectsStore } from '@/store/modules/projectsStore';
import { useUsersStore } from '@/store/modules/usersStore';
import { ProjectRole } from '@/types/projectMembers';
import { useAppStore } from '@/store/modules/appStore';
import ProjectCard from '@poc/components/ProjectCard.vue';
import PageTitleComponent from '@poc/components/PageTitleComponent.vue';
import ProjectsTableComponent from '@poc/components/ProjectsTableComponent.vue';
import IconProject from '@poc/components/icons/IconProject.vue';
import IconSettings from '@poc/components/icons/IconSettings.vue';
import IconTeam from '@poc/components/icons/IconTeam.vue';
import JoinProjectDialog from '@poc/components/dialogs/JoinProjectDialog.vue';
const appStore = useAppStore();
const projectsStore = useProjectsStore();
const usersStore = useUsersStore();
const dialog = ref<boolean>(false);
const valid = ref<boolean>(false);
const name = ref<string>('');
const activeView = ref<string>(localStorage.getItem('activeView') || 'cards');
const isLoading = ref<boolean>(true);
const joiningItem = ref<ProjectItemModel | null>(null);
const isJoinProjectDialogShown = ref<boolean>(false);
const nameRules = [
value => (!!value || 'Project name is required.'),
value => ((value?.length <= 100) || 'Name must be less than 100 characters.'),
];
function toggleView(view: string): void {
activeView.value = view;
localStorage.setItem('activeView', view);
/**
* Returns whether to use the table view.
*/
const isTableView = computed<boolean>({
get: () => {
if (!items.value.length) return false;
if (!appStore.hasProjectTableViewConfigured() && items.value.length > 8) return true;
return appStore.state.isProjectTableViewEnabled;
},
set: value => appStore.toggleProjectTableViewEnabled(value),
});
/**
* Returns the project items from the store.
*/
const items = computed((): ProjectItemModel[] => {
const projects: ProjectItemModel[] = [];
projects.push(...projectsStore.state.invitations.map<ProjectItemModel>(invite => new ProjectItemModel(
invite.projectID,
invite.projectName,
invite.projectDescription,
ProjectRole.Invited,
null,
invite.createdAt,
)));
projects.push(...projectsStore.projects.map<ProjectItemModel>(project => new ProjectItemModel(
project.id,
project.name,
project.description,
project.ownerId === usersStore.state.user.id ? ProjectRole.Owner : ProjectRole.Member,
project.memberCount,
new Date(project.createdAt),
)).sort((projA, projB) => {
if (projA.role === ProjectRole.Owner && projB.role === ProjectRole.Member) return -1;
if (projA.role === ProjectRole.Member && projB.role === ProjectRole.Owner) return 1;
return 0;
}));
return projects;
});
/**
* Displays the Join Project modal.
*/
function onJoinClicked(item: ProjectItemModel): void {
joiningItem.value = item;
isJoinProjectDialogShown.value = true;
}
onMounted(async (): Promise<void> => {
await usersStore.getUser().catch(_ => {});
await projectsStore.getProjects().catch(_ => {});
await projectsStore.getUserInvitations().catch(_ => {});
isLoading.value = false;
});
</script>