Skip to content

Commit

Permalink
feat(project): modal to create project (#426)
Browse files Browse the repository at this point in the history
  • Loading branch information
RemiBonnet committed Dec 22, 2022
1 parent 861c047 commit 5286300
Show file tree
Hide file tree
Showing 23 changed files with 323 additions and 43 deletions.
2 changes: 2 additions & 0 deletions apps/console/src/app/app.tsx
Expand Up @@ -15,6 +15,7 @@ import { LOGIN_URL, LOGOUT_URL, ProtectedRoute } from '@qovery/shared/router'
import { LoadingScreen } from '@qovery/shared/ui'
import { useAuthInterceptor, useDocumentTitle } from '@qovery/shared/utils'
import { environment } from '../environments/environment'
import ScrollToTop from './components/scroll-to-top'
import { ROUTER } from './router/main.router'

export function App() {
Expand Down Expand Up @@ -73,6 +74,7 @@ export function App() {

return (
<GTMProvider state={gtmParams}>
<ScrollToTop />
<Routes>
<Route path={`${LOGIN_URL}/*`} element={<PageLogin />} />
<Route path={LOGOUT_URL} element={<PageLogoutFeature />} />
Expand Down
12 changes: 12 additions & 0 deletions apps/console/src/app/components/scroll-to-top.tsx
@@ -0,0 +1,12 @@
import { useEffect } from 'react'
import { useLocation } from 'react-router-dom'

export default function ScrollToTop() {
const { pathname } = useLocation()

useEffect(() => {
window.scrollTo(0, 0)
}, [pathname])

return null
}
3 changes: 3 additions & 0 deletions libs/domains/projects/src/lib/slices/projects.slice.ts
Expand Up @@ -89,6 +89,9 @@ export const projectsSlice = createSlice({
})
.addCase(postProject.fulfilled, (state: ProjectsState, action: PayloadAction<Project>) => {
projectsAdapter.upsertOne(state, action.payload)
state.joinOrganizationProject = addOneToManyRelation(action.payload.organization?.id, action.payload.id, {
...state.joinOrganizationProject,
})
state.loadingStatus = 'loaded'
})
.addCase(postProject.rejected, (state: ProjectsState, action) => {
Expand Down
Expand Up @@ -19,7 +19,7 @@ import {
ButtonStyle,
Icon,
IconAwesomeEnum,
MenuItemProps,
MenuData,
Skeleton,
StatusChip,
Tabs,
Expand Down Expand Up @@ -102,13 +102,7 @@ export function TabsFeature() {
APPLICATION_URL(organizationId, projectId, environmentId, applicationId) + APPLICATION_VARIABLES_URL
)

let menuForContentRight: {
items: MenuItemProps[]
title?: string
button?: string
buttonLink?: string
search?: boolean
}[] = []
let menuForContentRight: MenuData = []

if (matchEnvVariableRoute) {
menuForContentRight = [
Expand Down
Expand Up @@ -34,7 +34,7 @@ describe('CardCluster', () => {
})

it('should have a status message', () => {
const status = props.cluster.extendedStatus?.status?.status || StateEnum.BUILDING
const status = props.cluster.extendedStatus?.status?.status

const { baseElement } = render(<CardCluster {...props} />)

Expand Down
11 changes: 10 additions & 1 deletion libs/pages/layout/src/lib/feature/breadcrumb/breadcrumb.tsx
Expand Up @@ -5,7 +5,8 @@ import { selectDatabasesEntitiesByEnvId } from '@qovery/domains/database'
import { selectEnvironmentsEntitiesByProjectId } from '@qovery/domains/environment'
import { selectAllOrganization, selectClustersEntitiesByOrganizationId } from '@qovery/domains/organization'
import { selectProjectsEntitiesByOrgId } from '@qovery/domains/projects'
import { Breadcrumb } from '@qovery/shared/ui'
import { CreateProjectModalFeature } from '@qovery/shared/console-shared'
import { Breadcrumb, useModal } from '@qovery/shared/ui'
import { RootState } from '@qovery/store'

export function BreadcrumbFeature() {
Expand All @@ -16,6 +17,13 @@ export function BreadcrumbFeature() {
const databases = useSelector((state: RootState) => selectDatabasesEntitiesByEnvId(state, environmentId))
const environments = useSelector((state: RootState) => selectEnvironmentsEntitiesByProjectId(state, projectId))
const projects = useSelector((state: RootState) => selectProjectsEntitiesByOrgId(state, organizationId))
const { openModal, closeModal } = useModal()

const createProjectModal = () => {
openModal({
content: <CreateProjectModalFeature onClose={closeModal} organizationId={organizationId} />,
})
}

return (
<Breadcrumb
Expand All @@ -25,6 +33,7 @@ export function BreadcrumbFeature() {
databases={databases}
environments={environments}
projects={projects}
createProjectModal={createProjectModal}
/>
)
}
Expand Down
1 change: 0 additions & 1 deletion libs/pages/settings/src/lib/page-settings.tsx
Expand Up @@ -22,7 +22,6 @@ export function PageSettings() {
const { organizationId = '' } = useParams()

const pathSettings = SETTINGS_URL(organizationId)

const projects = useSelector((state: RootState) => selectProjectsEntitiesByOrgId(state, organizationId))

const organizationLinks = [
Expand Down
14 changes: 13 additions & 1 deletion libs/pages/settings/src/lib/ui/container/container.tsx
@@ -1,5 +1,7 @@
import { ReactNode } from 'react'
import { NavigationLeft, NavigationLeftLinkProps } from '@qovery/shared/ui'
import { useParams } from 'react-router-dom'
import { CreateProjectModalFeature } from '@qovery/shared/console-shared'
import { NavigationLeft, NavigationLeftLinkProps, useModal } from '@qovery/shared/ui'

export interface ContainerProps {
organizationLinks: NavigationLeftLinkProps[]
Expand All @@ -9,7 +11,9 @@ export interface ContainerProps {
}

export function Container(props: ContainerProps) {
const { organizationId = '' } = useParams()
const { organizationLinks, projectLinks, accountLinks, children } = props
const { openModal, closeModal } = useModal()

return (
<div className="bg-white flex rounded-t">
Expand All @@ -19,6 +23,14 @@ export function Container(props: ContainerProps) {
<NavigationLeft
title="Projects"
links={projectLinks}
link={{
title: 'New',
onClick: () => {
openModal({
content: <CreateProjectModalFeature onClose={closeModal} organizationId={organizationId} />,
})
},
}}
className="py-6 border-t border-element-light-lighter-400"
/>
<NavigationLeft
Expand Down
2 changes: 1 addition & 1 deletion libs/shared/console-shared/src/index.ts
Expand Up @@ -4,7 +4,6 @@ export * from './lib/entrypoint-cmd-inputs/ui/entrypoint-cmd-inputs'
export * from './lib/environment-buttons-actions/ui/environment-buttons-actions'
export * from './lib/setting-resources/ui/setting-resources'
export * from './lib/git-repository-settings/feature/git-repository-settings-feature'
export * from './lib/git-repository-settings/ui/git-repository-settings'
export * from './lib/general-container-settings/ui/general-container-settings'
export * from './lib/deploy-other-commit-modal/feature/deploy-other-commit-modal-feature'
export * from './lib/application-buttons-actions/ui/application-buttons-actions'
Expand All @@ -15,3 +14,4 @@ export * from './lib/flow-create-port/ui/flow-create-port'
export * from './lib/flow-create-variable/ui/flow-create-variable'
export * from './lib/create-general-git-application/ui/create-general-git-application'
export * from './lib/git-repository-settings/edit-git-repository-settings-feature/edit-git-repository-settings-feature'
export * from './lib/create-project-modal/feature/create-project-modal-feature'
Expand Up @@ -39,9 +39,7 @@ export function CreateCloneEnvironmentModalFeature(props: CreateCloneEnvironment
},
})

methods.watch((data) => {
enableAlertClickOutside(methods.formState.isDirty)
})
methods.watch(() => enableAlertClickOutside(methods.formState.isDirty))

const dispatch = useDispatch<AppDispatch>()

Expand Down
@@ -0,0 +1,69 @@
import { act, fireEvent, render } from '__tests__/utils/setup-jest'
import * as storeProjects from '@qovery/domains/projects'
import CreateProjectModalFeature, { CreateProjectModalFeatureProps } from './create-project-modal-feature'

import SpyInstance = jest.SpyInstance

const mockedUsedNavigate = jest.fn()
jest.mock('react-router-dom', () => ({
...(jest.requireActual('react-router-dom') as any),
useNavigate: () => mockedUsedNavigate,
}))

jest.mock('@qovery/domains/projects', () => ({
...jest.requireActual('@qovery/domains/projects'),
postProject: jest.fn().mockImplementation(() => Promise.resolve()),
}))

const mockDispatch = jest.fn()
jest.mock('react-redux', () => ({
...jest.requireActual('react-redux'),
useDispatch: () => mockDispatch,
}))

describe('CreateProjectModalFeature', () => {
const props: CreateProjectModalFeatureProps = {
onClose: jest.fn(),
organizationId: '0',
goToEnvironment: false,
}

it('should render successfully', () => {
const { baseElement } = render(<CreateProjectModalFeature {...props} />)
expect(baseElement).toBeTruthy()
})

it('should dispatch postProject if form is submitted', async () => {
const postProjectSpy: SpyInstance = jest.spyOn(storeProjects, 'postProject')
mockDispatch.mockImplementation(() => ({
unwrap: () =>
Promise.resolve({
data: {},
}),
}))

const { getByTestId } = render(<CreateProjectModalFeature {...props} />)

await act(() => {
const inputName = getByTestId('input-name')
fireEvent.input(inputName, { target: { value: 'hello-world' } })
})

await act(() => {
const inputName = getByTestId('input-description')
fireEvent.input(inputName, { target: { value: 'description' } })
})

expect(getByTestId('submit-button')).not.toBeDisabled()

await act(() => {
getByTestId('submit-button').click()
})

expect(postProjectSpy).toHaveBeenCalledWith({
name: 'hello-world',
description: 'description',
organizationId: '0',
})
})
})
@@ -0,0 +1,51 @@
import { useState } from 'react'
import { FieldValues, FormProvider, useForm } from 'react-hook-form'
import { useDispatch } from 'react-redux'
import { useNavigate } from 'react-router-dom'
import { postProject } from '@qovery/domains/projects'
import { ENVIRONMENTS_GENERAL_URL, ENVIRONMENTS_URL } from '@qovery/shared/router'
import { AppDispatch } from '@qovery/store'
import CreateProjectModal from '../ui/create-project-modal'

export interface CreateProjectModalFeatureProps {
onClose: () => void
organizationId: string
}

export function CreateProjectModalFeature(props: CreateProjectModalFeatureProps) {
const { onClose, organizationId } = props

const navigate = useNavigate()
const dispatch = useDispatch<AppDispatch>()
const methods = useForm({
mode: 'onChange',
})
const [loading, setLoading] = useState(false)

const onSubmit = methods.handleSubmit((data: FieldValues) => {
setLoading(true)

dispatch(
postProject({
organizationId: organizationId,
name: data['name'],
description: data['description'],
})
)
.unwrap()
.then((project) => {
navigate(ENVIRONMENTS_URL(organizationId, project.id) + ENVIRONMENTS_GENERAL_URL)
setLoading(false)
onClose()
})
.catch(() => setLoading(false))
})

return (
<FormProvider {...methods}>
<CreateProjectModal closeModal={onClose} onSubmit={onSubmit} loading={loading} />
</FormProvider>
)
}

export default CreateProjectModalFeature
@@ -0,0 +1,40 @@
import { act, fireEvent, render, waitFor } from '__tests__/utils/setup-jest'
import { wrapWithReactHookForm } from '__tests__/utils/wrap-with-react-hook-form'
import CreateProjectModal, { CreateProjectModalProps } from './create-project-modal'

describe('CreateProjectModal', () => {
const props: CreateProjectModalProps = {
onSubmit: jest.fn(),
closeModal: jest.fn(),
loading: false,
}

it('should render successfully', () => {
const { baseElement } = render(wrapWithReactHookForm(<CreateProjectModal {...props} />))
expect(baseElement).toBeTruthy()
})

it('should submit the form', async () => {
const spy = jest.fn().mockImplementation((e) => e.preventDefault())
props.onSubmit = spy

const { getByTestId } = render(wrapWithReactHookForm(<CreateProjectModal {...props} />))

const inputName = getByTestId('input-name')
const inputDescription = getByTestId('input-description')
const button = getByTestId('submit-button')

expect(button).toBeDisabled()

await act(() => {
fireEvent.input(inputName, { target: { value: 'hello world' } })
fireEvent.input(inputDescription, { target: { value: 'hello' } })
})

await waitFor(() => {
button.click()
expect(button).not.toBeDisabled()
expect(spy).toHaveBeenCalled()
})
})
})
@@ -0,0 +1,59 @@
import { Controller, useFormContext } from 'react-hook-form'
import { InputText, ModalCrud } from '@qovery/shared/ui'

export interface CreateProjectModalProps {
onSubmit: () => void
closeModal: () => void
loading: boolean
}

export function CreateProjectModal(props: CreateProjectModalProps) {
const { onSubmit, closeModal, loading } = props
const { control } = useFormContext()

return (
<ModalCrud
title="New project"
description="You will have the possibility to modify the parameters once created"
onClose={closeModal}
onSubmit={onSubmit}
submitLabel="Create"
loading={loading}
>
<Controller
name="name"
control={control}
rules={{
required: 'Please enter a name.',
}}
render={({ field, fieldState: { error } }) => (
<InputText
className="mb-3"
dataTestId="input-name"
label="Project name"
name={field.name}
onChange={field.onChange}
value={field.value}
error={error?.message}
/>
)}
/>
<Controller
name="description"
control={control}
render={({ field }) => (
<InputText
className="mb-6"
dataTestId="input-description"
label="Description"
name={field.name}
onChange={field.onChange}
value={field.value}
/>
)}
/>
</ModalCrud>
)
}

export default CreateProjectModal
@@ -1,5 +1,4 @@
import { act, screen } from '@testing-library/react'
import { render } from '__tests__/utils/setup-jest'
import { act, render, screen } from '__tests__/utils/setup-jest'
import { wrapWithReactHookForm } from '__tests__/utils/wrap-with-react-hook-form'
import { GitProviderEnum } from 'qovery-typescript-axios'
import { authProviderFactoryMock } from '@qovery/domains/organization'
Expand Down

0 comments on commit 5286300

Please sign in to comment.