web/satellite/vuetify-poc: add project creation dialog

This change implements a dialog box through which users can create
projects.

Resolves #6172

Change-Id: I99311f43ef49dc04bb852a6736771b42f26a3276
This commit is contained in:
Jeremy Wharton 2023-08-22 00:25:39 -05:00 committed by Storj Robot
parent b66fc6dcdb
commit 973b54365e
4 changed files with 209 additions and 100 deletions

View File

@ -53,7 +53,9 @@
</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>
<v-btn v-if="!item" color="primary" size="small" class="mr-2" @click="emit('createClick')">
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
@ -111,7 +113,8 @@ const props = defineProps<{
}>();
const emit = defineEmits<{
(event: 'joinClick'): void;
'joinClick': [];
'createClick': [];
}>();
const analyticsStore = useAnalyticsStore();

View File

@ -0,0 +1,192 @@
// Copyright (C) 2023 Storj Labs, Inc.
// See LICENSE for copying information.
<template>
<v-dialog
v-model="model"
width="410px"
transition="fade-transition"
:persistent="isLoading"
>
<v-card rounded="xlg">
<v-card-item class="pl-7 py-4">
<template #prepend>
<img class="d-block" src="@/../static/images/common/blueBox.svg" alt="Box">
</template>
<v-card-title class="font-weight-bold">
{{ !isProjectLimitReached ? 'Create New Project' : 'Get More Projects' }}
</v-card-title>
<template #append>
<v-btn
icon="$close"
variant="text"
size="small"
color="default"
:disabled="isLoading"
@click="model = false"
/>
</template>
</v-card-item>
<v-divider />
<v-form v-model="formValid" class="pa-7" @submit.prevent>
<v-row>
<template v-if="!isProjectLimitReached">
<v-col cols="12">
Projects are where you and your team can upload and manage data, and view usage statistics and billing.
</v-col>
<v-col cols="12">
<v-text-field
v-model="name"
variant="outlined"
:rules="nameRules"
label="Project Name"
:counter="MAX_NAME_LENGTH"
persistent-counter
autofocus
/>
<v-btn
v-if="!isDescriptionShown"
variant="text"
size="small"
color="default"
prepend-icon="mdi-plus"
@click="isDescriptionShown = true"
>
Add Description (Optional)
</v-btn>
</v-col>
<v-col v-if="isDescriptionShown" cols="12">
<v-text-field
v-model="description"
variant="outlined"
:rules="descriptionRules"
label="Project Description (Optional)"
:counter="MAX_DESCRIPTION_LENGTH"
persistent-counter
/>
</v-col>
</template>
<v-col v-else>
Upgrade to Pro Account to create more projects and gain access to higher limits.
</v-col>
</v-row>
</v-form>
<v-divider />
<v-card-actions class="pa-7">
<v-row>
<v-col>
<v-btn variant="outlined" color="default" block :disabled="isLoading" @click="model = false">
Cancel
</v-btn>
</v-col>
<v-col>
<v-btn
color="primary"
variant="flat"
:loading="isLoading"
block
@click="() => !isProjectLimitReached && onCreateClicked()"
>
{{ !isProjectLimitReached ? 'Create Project' : 'Upgrade' }}
</v-btn>
</v-col>
</v-row>
</v-card-actions>
</v-card>
</v-dialog>
</template>
<script setup lang="ts">
import { ref, computed, watch, Component } from 'vue';
import { useRouter } from 'vue-router';
import {
VDialog,
VCard,
VCardItem,
VCardTitle,
VCardActions,
VBtn,
VDivider,
VForm,
VRow,
VCol,
VTextField,
} from 'vuetify/components';
import { RequiredRule, ValidationRule } from '@poc/types/common';
import { MAX_DESCRIPTION_LENGTH, MAX_NAME_LENGTH, ProjectFields } from '@/types/projects';
import { useLoading } from '@/composables/useLoading';
import { useProjectsStore } from '@/store/modules/projectsStore';
import { useUsersStore } from '@/store/modules/usersStore';
import { useNotify } from '@/utils/hooks';
import { AnalyticsErrorEventSource } from '@/utils/constants/analyticsEventNames';
const props = defineProps<{
modelValue: boolean,
}>();
const emit = defineEmits<{
'update:modelValue': [value: boolean],
}>();
const model = computed<boolean>({
get: () => props.modelValue,
set: value => emit('update:modelValue', value),
});
const projectsStore = useProjectsStore();
const usersStore = useUsersStore();
const { isLoading, withLoading } = useLoading();
const notify = useNotify();
const router = useRouter();
const formValid = ref<boolean>(false);
const name = ref<string>('');
const description = ref<string>('');
const isDescriptionShown = ref<boolean>(false);
const isProjectLimitReached = ref<boolean>(false);
const nameRules: ValidationRule<string>[] = [
RequiredRule,
v => v.length <= MAX_NAME_LENGTH || 'Name is too long',
];
const descriptionRules: ValidationRule<string>[] = [
v => v.length <= MAX_DESCRIPTION_LENGTH || 'Description is too long',
];
/**
* Creates new project.
*/
async function onCreateClicked(): Promise<void> {
if (!formValid.value) return;
await withLoading(async () => {
let id: string;
try {
const fields = new ProjectFields(name.value, description.value, usersStore.state.user.id);
id = await projectsStore.createProject(fields);
} catch (error) {
error.message = `Failed to create project. ${error.message}`;
notify.notifyError(error, AnalyticsErrorEventSource.CREATE_PROJECT_MODAL);
return;
}
model.value = false;
router.push(`/projects/${id}/dashboard`);
notify.success('Project created.');
});
}
watch(() => model.value, shown => {
if (!shown) return;
isProjectLimitReached.value = projectsStore.state.projects.length >= usersStore.state.user.projectLimit;
isDescriptionShown.value = false;
name.value = '';
description.value = '';
});
</script>

View File

@ -79,7 +79,7 @@
</v-list-item>
<!-- Create New Project -->
<v-list-item link rounded="lg">
<v-list-item link rounded="lg" @click="isCreateProjectDialogShown = true">
<template #prepend>
<IconNew />
</template>
@ -231,10 +231,12 @@
</v-list>
</v-sheet>
</v-navigation-drawer>
<create-project-dialog v-model="isCreateProjectDialogShown" />
</template>
<script setup lang="ts">
import { computed } from 'vue';
import { computed, ref } from 'vue';
import {
VNavigationDrawer,
VSheet,
@ -265,10 +267,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';
import CreateProjectDialog from '@poc/components/dialogs/CreateProjectDialog.vue';
const analyticsStore = useAnalyticsStore();
const projectsStore = useProjectsStore();
const isCreateProjectDialogShown = ref<boolean>(false);
/**
* Returns the selected project from the store.
*/

View File

@ -13,107 +13,13 @@
color="default"
variant="outlined"
density="comfortable"
@click="isCreateProjectDialogShown = true"
>
<svg width="14" height="14" class="mr-2" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10 1C14.9706 1 19 5.02944 19 10C19 14.9706 14.9706 19 10 19C5.02944 19 1 14.9706 1 10C1 5.02944 5.02944 1 10 1ZM10 2.65C5.94071 2.65 2.65 5.94071 2.65 10C2.65 14.0593 5.94071 17.35 10 17.35C14.0593 17.35 17.35 14.0593 17.35 10C17.35 5.94071 14.0593 2.65 10 2.65ZM10.7496 6.8989L10.7499 6.91218L10.7499 9.223H12.9926C13.4529 9.223 13.8302 9.58799 13.8456 10.048C13.8602 10.4887 13.5148 10.8579 13.0741 10.8726L13.0608 10.8729L10.7499 10.873L10.75 13.171C10.75 13.6266 10.3806 13.996 9.925 13.996C9.48048 13.996 9.11807 13.6444 9.10066 13.2042L9.1 13.171L9.09985 10.873H6.802C6.34637 10.873 5.977 10.5036 5.977 10.048C5.977 9.60348 6.32857 9.24107 6.76882 9.22366L6.802 9.223H9.09985L9.1 6.98036C9.1 6.5201 9.46499 6.14276 9.925 6.12745C10.3657 6.11279 10.7349 6.45818 10.7496 6.8989Z" fill="currentColor" />
</svg>
<!-- <IconNew class="mr-2" width="12px"/> -->
Create Project
<v-dialog
v-model="dialog"
activator="parent"
width="auto"
min-width="400px"
transition="fade-transition"
>
<v-card rounded="xlg">
<v-sheet>
<v-card-item class="pl-7 py-4">
<template #prepend>
<v-card-title class="font-weight-bold">
<!-- <v-icon
>
<img src="../assets/icon-project.svg" alt="Project">
</v-icon> -->
Create New Project
</v-card-title>
</template>
<template #append>
<v-btn
icon="$close"
variant="text"
size="small"
color="default"
@click="dialog = false"
/>
</template>
</v-card-item>
</v-sheet>
<v-divider />
<v-form v-model="valid" class="pa-7 pb-4">
<v-row>
<v-col>
<p>Enter project name (max 20 characters).</p>
</v-col>
</v-row>
<v-row>
<v-col
cols="12"
>
<v-text-field
v-model="name"
variant="outlined"
:rules="nameRules"
label="Project Name"
class="mt-2"
required
autofocus
counter
maxlength="20"
/>
</v-col>
</v-row>
<v-row>
<v-col>
<v-btn variant="text" size="small" class="mb-4" color="default">+ Add Description (Optional)</v-btn>
</v-col>
</v-row>
<!-- <v-row>
<v-col
cols="12"
>
<v-text-field
v-model="description"
variant="outlined"
label="Project Description (Optional)"
class="mt-2"
multiple
></v-text-field>
</v-col>
</v-row> -->
</v-form>
<v-divider />
<v-card-actions class="pa-7">
<v-row>
<v-col>
<v-btn variant="outlined" color="default" block @click="dialog = false">Cancel</v-btn>
</v-col>
<v-col>
<v-btn color="primary" variant="flat" block>Create Project</v-btn>
</v-col>
</v-row>
</v-card-actions>
</v-card>
</v-dialog>
</v-btn>
</v-col>
@ -182,7 +88,7 @@
<v-row v-else>
<!-- Card view -->
<v-col v-if="!items.length" cols="12" sm="6" md="4" lg="3">
<ProjectCard class="h-100" />
<ProjectCard class="h-100" @create-click="isCreateProjectDialogShown = true" />
</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)" />
@ -196,6 +102,7 @@
v-model="isJoinProjectDialogShown"
:name="joiningItem.name"
/>
<create-project-dialog v-model="isCreateProjectDialogShown" />
</template>
<script setup lang="ts">
@ -229,6 +136,7 @@ import ProjectCard from '@poc/components/ProjectCard.vue';
import PageTitleComponent from '@poc/components/PageTitleComponent.vue';
import ProjectsTableComponent from '@poc/components/ProjectsTableComponent.vue';
import JoinProjectDialog from '@poc/components/dialogs/JoinProjectDialog.vue';
import CreateProjectDialog from '@poc/components/dialogs/CreateProjectDialog.vue';
const appStore = useAppStore();
const projectsStore = useProjectsStore();
@ -241,6 +149,7 @@ const isLoading = ref<boolean>(true);
const joiningItem = ref<ProjectItemModel | null>(null);
const isJoinProjectDialogShown = ref<boolean>(false);
const isCreateProjectDialogShown = ref<boolean>(false);
const nameRules = [
value => (!!value || 'Project name is required.'),