diff --git a/packages/backend/test/fixtures/organizations.json b/packages/backend/test/fixtures/organizations.json index bd0d121b..1c61eeff 100644 --- a/packages/backend/test/fixtures/organizations.json +++ b/packages/backend/test/fixtures/organizations.json @@ -1,9 +1,11 @@ [ { + "id": "2379cf1f-a232-4ad2-ae14-4d881131cc26", "name": "Snowball Tools", "slug": "snowball-tools" }, { + "id": "7eb9b3eb-eb74-4b53-b59a-69884c82a7fb", "name": "AirFoil", "slug": "airfoil" } diff --git a/packages/frontend/src/assets/templates.json b/packages/frontend/src/assets/templates.json index b6008915..e697f7b2 100644 --- a/packages/frontend/src/assets/templates.json +++ b/packages/frontend/src/assets/templates.json @@ -1,22 +1,27 @@ [ { - "framework": "React", + "id": "1", + "name": "Progressive Web App (PWA)", "icon": "^" }, { - "framework": "Reactnative", + "id": "2", + "name": "Kotlin", "icon": "^" }, { - "framework": "Kotlin", + "id": "3", + "name": "React Native", "icon": "^" }, { - "framework": "Swift", + "id": "4", + "name": "Swift", "icon": "^" }, { - "framework": "Webapp", + "id": "5", + "name": "Web app", "icon": "^" } ] diff --git a/packages/frontend/src/components/Dropdown.tsx b/packages/frontend/src/components/Dropdown.tsx index aa24cafa..13430072 100644 --- a/packages/frontend/src/components/Dropdown.tsx +++ b/packages/frontend/src/components/Dropdown.tsx @@ -5,7 +5,7 @@ import { } from 'react-dropdown'; import 'react-dropdown/style.css'; -interface Option { +export interface Option { value: string; label: string; } diff --git a/packages/frontend/src/components/projects/create/Deploy.tsx b/packages/frontend/src/components/projects/create/Deploy.tsx index 2021f6ed..57945f96 100644 --- a/packages/frontend/src/components/projects/create/Deploy.tsx +++ b/packages/frontend/src/components/projects/create/Deploy.tsx @@ -1,5 +1,5 @@ import React, { useCallback } from 'react'; -import { useNavigate, useParams } from 'react-router-dom'; +import { useNavigate, useParams, useSearchParams } from 'react-router-dom'; import { Button, Typography } from '@material-tailwind/react'; @@ -8,8 +8,12 @@ import { Stopwatch, setStopWatchOffset } from '../../StopWatch'; import ConfirmDialog from '../../shared/ConfirmDialog'; const Deploy = () => { + const [searchParams] = useSearchParams(); + const projectId = searchParams.get('projectId'); + const [open, setOpen] = React.useState(false); const handleOpen = () => setOpen(!open); + const navigate = useNavigate(); const { orgSlug } = useParams(); @@ -70,6 +74,14 @@ const Deploy = () => { status={DeployStatus.NOT_STARTED} step="4" /> + + <Button + onClick={() => { + navigate(`/${orgSlug}/projects/create/success/${projectId}`); + }} + > + VIEW DEMO + </Button> </div> ); }; diff --git a/packages/frontend/src/components/projects/create/ProjectRepoCard.tsx b/packages/frontend/src/components/projects/create/ProjectRepoCard.tsx index d5716231..ac3556b0 100644 --- a/packages/frontend/src/components/projects/create/ProjectRepoCard.tsx +++ b/packages/frontend/src/components/projects/create/ProjectRepoCard.tsx @@ -1,43 +1,61 @@ -import React from 'react'; -import { Link } from 'react-router-dom'; +import React, { useCallback } from 'react'; +import { useNavigate, useParams } from 'react-router-dom'; import { Chip, IconButton } from '@material-tailwind/react'; import { relativeTime } from '../../../utils/time'; import { GitRepositoryDetails } from '../../../types/project'; +import { useGQLClient } from '../../../context/GQLClientContext'; interface ProjectRepoCardProps { repository: GitRepositoryDetails; } const ProjectRepoCard: React.FC<ProjectRepoCardProps> = ({ repository }) => { + const client = useGQLClient(); + const navigate = useNavigate(); + + const { orgSlug } = useParams(); + + const createProject = useCallback(async () => { + if (!repository) { + return; + } + + const { addProject } = await client.addProject(orgSlug!, { + name: `${repository.owner!.login}-${repository.name}`, + // TODO: Get organization id from context or URL + prodBranch: repository.default_branch!, + repository: repository.full_name, + }); + + navigate(`import?projectId=${addProject.id}`); + }, [client, repository]); + return ( - <Link - to={`import?owner=${repository.owner?.login}&repo=${repository.name}`} + <div + className="group flex items-center gap-4 text-gray-500 text-xs hover:bg-gray-100 p-2 cursor-pointer" + onClick={createProject} > - <div className="group flex items-center gap-4 text-gray-500 text-xs hover:bg-gray-100 p-2 cursor-pointer"> - <div>^</div> - <div className="grow"> - <div> - <span className="text-black">{repository.full_name}</span> - {repository.visibility === 'private' ? ( - <Chip - className="normal-case inline ml-6 bg-[#FED7AA] text-[#EA580C] font-normal" - size="sm" - value="Private" - icon={'^'} - /> - ) : ( - '' - )} - </div> - <p>{repository.updated_at && relativeTime(repository.updated_at)}</p> - </div> - <div className="hidden group-hover:block"> - <IconButton size="sm">{'>'}</IconButton> + <div>^</div> + <div className="grow"> + <div> + <span className="text-black">{repository.full_name}</span> + {repository.visibility === 'private' && ( + <Chip + className="normal-case inline ml-6 font-normal" + size="sm" + value="Private" + icon={'^'} + /> + )} </div> + <p>{repository.updated_at && relativeTime(repository.updated_at)}</p> </div> - </Link> + <div className="hidden group-hover:block"> + <IconButton size="sm">{'>'}</IconButton> + </div> + </div> ); }; diff --git a/packages/frontend/src/components/projects/create/TemplateCard.tsx b/packages/frontend/src/components/projects/create/TemplateCard.tsx index 1a62975d..bf6e7f29 100644 --- a/packages/frontend/src/components/projects/create/TemplateCard.tsx +++ b/packages/frontend/src/components/projects/create/TemplateCard.tsx @@ -6,19 +6,20 @@ import { IconButton, Typography } from '@material-tailwind/react'; import { Link } from 'react-router-dom'; interface TemplateDetails { - framework: string; + id: string; + name: string; icon: string; } interface TemplateCardProps { - framework: TemplateDetails; + template: TemplateDetails; isGitAuth: boolean; } -const CardDetails = ({ framework }: { framework: TemplateDetails }) => { +const CardDetails = ({ template }: { template: TemplateDetails }) => { return ( <div className="h-14 group bg-gray-200 border-gray-200 rounded-lg shadow p-4 flex items-center justify-between"> <Typography className="grow"> - {framework.icon} {framework.framework} + {template.icon} {template.name} </Typography> <div> <IconButton size="sm" className="rounded-full hidden group-hover:block"> @@ -29,13 +30,10 @@ const CardDetails = ({ framework }: { framework: TemplateDetails }) => { ); }; -const TemplateCard: React.FC<TemplateCardProps> = ({ - framework, - isGitAuth, -}) => { +const TemplateCard: React.FC<TemplateCardProps> = ({ template, isGitAuth }) => { return isGitAuth ? ( - <Link to="template"> - <CardDetails framework={framework} /> + <Link to={`template?templateId=${template.id}`}> + <CardDetails template={template} /> </Link> ) : ( <a @@ -43,7 +41,7 @@ const TemplateCard: React.FC<TemplateCardProps> = ({ toast.error('Connect Git account to start with a template') } > - <CardDetails framework={framework} /> + <CardDetails template={template} /> </a> ); }; diff --git a/packages/frontend/src/constants.ts b/packages/frontend/src/constants.ts index c964d922..f09a8853 100644 --- a/packages/frontend/src/constants.ts +++ b/packages/frontend/src/constants.ts @@ -3,3 +3,5 @@ export const COMMIT_DETAILS = { createdAt: '2023-12-11T04:20:00', branch: 'main', }; + +export const ORGANIZATION_ID = '2379cf1f-a232-4ad2-ae14-4d881131cc26'; diff --git a/packages/frontend/src/pages/org-slug/projects/create/Import.tsx b/packages/frontend/src/pages/org-slug/projects/create/Import.tsx index 8ee3145e..420c239f 100644 --- a/packages/frontend/src/pages/org-slug/projects/create/Import.tsx +++ b/packages/frontend/src/pages/org-slug/projects/create/Import.tsx @@ -1,65 +1,37 @@ -import React, { useCallback, useEffect, useState } from 'react'; -import { useNavigate, useParams, useSearchParams } from 'react-router-dom'; +import React, { useEffect, useState } from 'react'; +import { useSearchParams } from 'react-router-dom'; +import assert from 'assert'; -import { Button } from '@material-tailwind/react'; - -import { useOctokit } from '../../../../context/OctokitContext'; -import { GitRepositoryDetails } from '../../../../types/project'; import Deploy from '../../../../components/projects/create/Deploy'; import { useGQLClient } from '../../../../context/GQLClientContext'; const Import = () => { const [searchParams] = useSearchParams(); - const { orgSlug } = useParams(); - const navigate = useNavigate(); - const { octokit } = useOctokit(); + const projectId = searchParams.get('projectId'); + assert(projectId); + + const [repoName, setRepoName] = useState<string>(''); const client = useGQLClient(); - const [gitRepo, setGitRepo] = useState<GitRepositoryDetails>(); useEffect(() => { const fetchRepo = async () => { - if (!octokit) { - return; - } + const { project } = await client.getProject(projectId); + assert(project); - const result = await octokit.rest.repos.get({ - owner: searchParams.get('owner') ?? '', - repo: searchParams.get('repo') ?? '', - }); - - setGitRepo(result.data); + setRepoName(project.repository); }; fetchRepo(); - }, [searchParams, octokit]); - - const createProjectAndCreate = useCallback(async () => { - if (!gitRepo) { - return; - } - - const { addProject } = await client.addProject(orgSlug!, { - // TODO: Implement form for setting project name - name: `${gitRepo.owner!.login}-${gitRepo.name}`, - prodBranch: gitRepo.default_branch ?? 'main', - repository: gitRepo.full_name, - }); - - navigate(`/${orgSlug}/projects/create/success/${addProject.id}`); - }, [client, gitRepo]); + }, [projectId]); return ( <div className="flex flex-col items-center"> <div className="flex w-5/6 my-4 bg-gray-200 rounded-xl p-6"> <div>^</div> - <div className="grow">{gitRepo?.full_name}</div> + <div className="grow">{repoName}</div> </div> <Deploy /> - - <Button onClick={createProjectAndCreate}> - CREATE PROJECT (FOR DEMO) - </Button> </div> ); }; diff --git a/packages/frontend/src/pages/org-slug/projects/create/Template.tsx b/packages/frontend/src/pages/org-slug/projects/create/Template.tsx index 87636f58..098597eb 100644 --- a/packages/frontend/src/pages/org-slug/projects/create/Template.tsx +++ b/packages/frontend/src/pages/org-slug/projects/create/Template.tsx @@ -1,7 +1,8 @@ import React, { useMemo } from 'react'; -import { Outlet, useLocation } from 'react-router-dom'; +import { Outlet, useLocation, useSearchParams } from 'react-router-dom'; import Stepper from '../../../../components/Stepper'; +import templateDetails from '../../../../assets/templates.json'; const STEPPER_VALUES = [ { step: 1, route: '/projects/create/template', label: 'Create repository' }, @@ -12,6 +13,12 @@ const STEPPER_VALUES = [ const CreateWithTemplate = () => { const location = useLocation(); + const [searchParams] = useSearchParams(); + + const template = templateDetails.find( + (template) => template.id === searchParams.get('templateId'), + ); + const activeStep = useMemo( () => STEPPER_VALUES.find((data) => data.route === location.pathname)?.step ?? @@ -23,7 +30,7 @@ const CreateWithTemplate = () => { <div className="flex flex-col items-center"> <div className="flex justify-between w-5/6 my-4 bg-gray-200 rounded-xl p-6"> <div>^</div> - <div className="grow">React native</div> + <div className="grow">{template?.name}</div> {/* TODO: Get template Git link from DB */} <div>^snowball-tools/react-native-starter</div> </div> diff --git a/packages/frontend/src/pages/org-slug/projects/create/index.tsx b/packages/frontend/src/pages/org-slug/projects/create/index.tsx index 5c6aaf51..b80731d0 100644 --- a/packages/frontend/src/pages/org-slug/projects/create/index.tsx +++ b/packages/frontend/src/pages/org-slug/projects/create/index.tsx @@ -13,12 +13,12 @@ const NewProject = () => { <> <h5 className="mt-4 ml-4">Start with template</h5> <div className="grid grid-cols-3 p-4 gap-4"> - {templateDetails.map((framework, key) => { + {templateDetails.map((template) => { return ( <TemplateCard isGitAuth={Boolean(octokit)} - framework={framework} - key={key} + template={template} + key={template.id} /> ); })} diff --git a/packages/frontend/src/pages/org-slug/projects/create/template/index.tsx b/packages/frontend/src/pages/org-slug/projects/create/template/index.tsx index 1d0e0f5c..50006ede 100644 --- a/packages/frontend/src/pages/org-slug/projects/create/template/index.tsx +++ b/packages/frontend/src/pages/org-slug/projects/create/template/index.tsx @@ -1,31 +1,104 @@ -import React from 'react'; -import { useForm, Controller } from 'react-hook-form'; -import { Link } from 'react-router-dom'; +import React, { useCallback, useEffect, useState } from 'react'; +import { useForm, Controller, SubmitHandler } from 'react-hook-form'; +import { useNavigate, useParams } from 'react-router-dom'; +import toast from 'react-hot-toast'; +import assert from 'assert'; -import { Typography } from '@material-tailwind/react'; +import { Option, Typography } from '@material-tailwind/react'; -import Dropdown from '../../../../../components/Dropdown'; +import { useOctokit } from '../../../../../context/OctokitContext'; +import { useGQLClient } from '../../../../../context/GQLClientContext'; +import AsyncSelect from '../../../../../components/shared/AsyncSelect'; -const USER_OPTIONS = [ - { value: 'saugatyadav1', label: 'saugatyadav1' }, - { value: 'brad102', label: 'brad102' }, - { value: 'erin20', label: 'erin20' }, -]; +type SubmitRepoValues = { + framework: string; + repoName: string; + isPrivate: boolean; + account: string; +}; const CreateRepo = () => { - const { register, handleSubmit, control } = useForm({ + const { octokit } = useOctokit(); + + const client = useGQLClient(); + + const { orgSlug } = useParams(); + + const navigate = useNavigate(); + + const [gitAccounts, setGitAccounts] = useState<string[]>([]); + + const submitRepoHandler: SubmitHandler<SubmitRepoValues> = useCallback( + async (data) => { + assert(data.account); + + try { + // TODO: Handle this functionality in backend + const gitRepo = await octokit?.rest.repos.createUsingTemplate({ + template_owner: 'github-rest', + template_repo: 'test-progressive-web-app', + owner: data.account, + name: data.repoName, + description: 'This is your first repository', + include_all_branches: false, + private: data.isPrivate, + }); + + if (!gitRepo) { + return; + } + + const { addProject } = await client.addProject(orgSlug!, { + name: `${gitRepo.data.owner!.login}-${gitRepo.data.name}`, + // TODO: Get organization id from context or URL + prodBranch: gitRepo.data.default_branch ?? 'main', + repository: gitRepo.data.full_name, + }); + + navigate( + `/${orgSlug}/projects/create/template/deploy?projectId=${addProject.id}`, + ); + } catch (err) { + toast.error('Error deploying project'); + } + }, + [octokit], + ); + + useEffect(() => { + const fetchUserAndOrgs = async () => { + const user = await octokit?.rest.users.getAuthenticated(); + const orgs = await octokit?.rest.orgs.listForAuthenticatedUser(); + + if (user && orgs) { + const orgsLoginArr = orgs.data.map((org) => org.login); + + setGitAccounts([user.data.login, ...orgsLoginArr]); + } + }; + + fetchUserAndOrgs(); + }, [octokit]); + + const { register, handleSubmit, control, reset } = useForm<SubmitRepoValues>({ defaultValues: { - framework: 'reactNative', + framework: 'React', repoName: '', isPrivate: false, - account: { value: 'saugatyadav1', label: 'saugatyadav1' }, + account: gitAccounts[0], }, }); + useEffect(() => { + if (gitAccounts.length > 0) { + reset({ account: gitAccounts[0] }); + } + }, [gitAccounts]); + // TODO: Get users and orgs from GitHub return ( - <form onSubmit={handleSubmit(() => {})}> + <form onSubmit={handleSubmit(submitRepoHandler)}> <div className="mb-2"> <Typography variant="h6">Create a repository</Typography> <Typography color="gray"> @@ -39,19 +112,19 @@ const CreateRepo = () => { <input type="radio" {...register('framework')} - value="reactNative" + value="React" className="h-5 w-5 text-indigo-600 rounded" /> - <span className="ml-2">^React Native</span> + <span className="ml-2">^React</span> </label> <label className="inline-flex items-center w-1/2 border rounded-lg p-2"> <input type="radio" {...register('framework')} className="h-5 w-5 text-indigo-600 rounded" - value="expo" + value="Next" /> - <span className="ml-2">^Expo</span> + <span className="ml-2">^Next</span> </label> </div> </div> @@ -61,12 +134,17 @@ const CreateRepo = () => { <Controller name="account" control={control} - render={({ field: { onChange, value } }) => ( - <Dropdown - onChange={onChange} - value={value} - options={USER_OPTIONS} - /> + render={({ field }) => ( + <AsyncSelect + {...field} + label={!field.value ? 'Select an account / Organization' : ''} + > + {gitAccounts.map((account, key) => ( + <Option key={key} value={account}> + ^ {account} + </Option> + ))} + </AsyncSelect> )} /> </div> @@ -93,11 +171,9 @@ const CreateRepo = () => { </label> </div> <div className="mb-2"> - <Link to="deploy"> - <button className="bg-blue-500 rounded-xl p-2" type="submit"> - Deploy ^ - </button> - </Link> + <button className="bg-blue-500 rounded-xl p-2" type="submit"> + Deploy ^ + </button> </div> </form> );