Add GQL mutation to delete project (#43)

* Implement delete project functionality

* Use delete project client method in UI

* Refetch projects information on deleting project

* Use project's current deployment domain name for url

* Handle review changes

---------

Co-authored-by: neeraj <neeraj.rtly@gmail.com>
This commit is contained in:
Nabarun Gogoi 2024-01-29 16:31:03 +05:30 committed by Ashwin Phatak
parent cfb4b4637c
commit 1ae1564878
18 changed files with 279 additions and 96 deletions

View File

@ -78,22 +78,39 @@ export class Database {
return projects; return projects;
} }
async getProjectByProjectId (projectId: string): Promise<Project | null> { async getProjectById (projectId: string): Promise<Project | null> {
const projectRepository = this.dataSource.getRepository(Project); const projectRepository = this.dataSource.getRepository(Project);
const project = await projectRepository.findOne({ const project = await projectRepository
relations: { .createQueryBuilder('project')
organization: true, .leftJoinAndSelect('project.deployments', 'deployments', 'deployments.isCurrent = true')
owner: true .leftJoinAndSelect('deployments.domain', 'domain')
}, .leftJoinAndSelect('project.owner', 'owner')
where: { .where('project.id = :projectId', {
id: projectId projectId
} })
}); .getOne();
return project; return project;
} }
async getProjectsInOrganization (userId: string, organizationId: string): Promise<Project[]> {
const projectRepository = this.dataSource.getRepository(Project);
const projects = await projectRepository
.createQueryBuilder('project')
.leftJoinAndSelect('project.deployments', 'deployments', 'deployments.isCurrent = true')
.leftJoinAndSelect('deployments.domain', 'domain')
.leftJoin('project.projectMembers', 'projectMembers')
.where('(project.ownerId = :userId OR projectMembers.userId = :userId) AND project.organizationId = :organizationId', {
userId,
organizationId
})
.getMany();
return projects;
}
async getDeploymentsByProjectId (projectId: string): Promise<Deployment[]> { async getDeploymentsByProjectId (projectId: string): Promise<Deployment[]> {
const deploymentRepository = this.dataSource.getRepository(Deployment); const deploymentRepository = this.dataSource.getRepository(Deployment);
@ -275,4 +292,15 @@ export class Database {
return false; return false;
} }
} }
async deleteProjectById (projectId: string): Promise<boolean> {
const projectRepository = this.dataSource.getRepository(Project);
const deleteResult = await projectRepository.softDelete({ id: projectId });
if (deleteResult.affected) {
return deleteResult.affected > 0;
} else {
return false;
}
}
} }

View File

@ -6,12 +6,14 @@ import {
UpdateDateColumn, UpdateDateColumn,
ManyToOne, ManyToOne,
JoinColumn, JoinColumn,
OneToMany OneToMany,
DeleteDateColumn
} from 'typeorm'; } from 'typeorm';
import { User } from './User'; import { User } from './User';
import { Organization } from './Organization'; import { Organization } from './Organization';
import { ProjectMember } from './ProjectMember'; import { ProjectMember } from './ProjectMember';
import { Deployment } from './Deployment';
@Entity() @Entity()
export class Project { export class Project {
@ -49,12 +51,21 @@ export class Project {
}) })
webhooks!: string[]; webhooks!: string[];
@Column('varchar')
icon!: string;
@CreateDateColumn() @CreateDateColumn()
createdAt!: Date; createdAt!: Date;
@UpdateDateColumn() @UpdateDateColumn()
updatedAt!: Date; updatedAt!: Date;
@DeleteDateColumn()
deletedAt?: Date;
@OneToMany(() => ProjectMember, projectMember => projectMember.project) @OneToMany(() => ProjectMember, projectMember => projectMember.project)
projectMembers!: ProjectMember[]; projectMembers!: ProjectMember[];
@OneToMany(() => Deployment, (deployment) => deployment.project)
deployments!: Deployment[];
} }

View File

@ -50,9 +50,14 @@ export const createResolvers = async (db: Database): Promise<any> => {
}, },
project: async (_: any, { projectId }: { projectId: string }) => { project: async (_: any, { projectId }: { projectId: string }) => {
const dbProject = await db.getProjectByProjectId(projectId); const dbProject = await db.getProjectById(projectId);
return dbProject ? projectToGqlType(dbProject, [], []) : null; return dbProject || null;
},
projectsInOrganization: async (_: any, { organizationId }: {organizationId: string }, context: any) => {
const dbProject = await db.getProjectsInOrganization(context.userId, organizationId);
return dbProject;
}, },
deployments: async (_: any, { projectId }: { projectId: string }) => { deployments: async (_: any, { projectId }: { projectId: string }) => {
@ -155,6 +160,15 @@ export const createResolvers = async (db: Database): Promise<any> => {
log(err); log(err);
return false; return false;
} }
},
deleteProject: async (_: any, { projectId }: { projectId: string }) => {
try {
return db.deleteProjectById(projectId);
} catch (err) {
log(err);
return false;
}
} }
} }
}; };

View File

@ -69,6 +69,7 @@ type Project {
createdAt: String! createdAt: String!
updatedAt: String! updatedAt: String!
organization: Organization! organization: Organization!
icon: String
} }
type ProjectMember { type ProjectMember {
@ -115,6 +116,7 @@ type Query {
user: User! user: User!
organizations: [Organization!] organizations: [Organization!]
projects: [Project!] projects: [Project!]
projectsInOrganization(organizationId: String!): [Project!]
project(projectId: String!): Project project(projectId: String!): Project
deployments(projectId: String!): [Deployment!] deployments(projectId: String!): [Deployment!]
environmentVariables(projectId: String!): [EnvironmentVariable!] environmentVariables(projectId: String!): [EnvironmentVariable!]
@ -128,6 +130,7 @@ type Mutation {
updateDeploymentToProd(deploymentId: String!): Boolean! updateDeploymentToProd(deploymentId: String!): Boolean!
updateProject(projectId: String!, updateProject: UpdateProjectInput): Boolean! updateProject(projectId: String!, updateProject: UpdateProjectInput): Boolean!
redeployToProd(deploymentId: String!): Boolean! redeployToProd(deploymentId: String!): Boolean!
deleteProject(projectId: String!): Boolean!
} }
input AddEnvironmentVariableInput { input AddEnvironmentVariableInput {

View File

@ -8,7 +8,8 @@
"description": "test", "description": "test",
"template": "test", "template": "test",
"framework": "test", "framework": "test",
"webhooks": [] "webhooks": [],
"icon": ""
}, },
{ {
"ownerIndex": 1, "ownerIndex": 1,
@ -19,7 +20,8 @@
"description": "test-2", "description": "test-2",
"template": "test-2", "template": "test-2",
"framework": "test-2", "framework": "test-2",
"webhooks": [] "webhooks": [],
"icon": ""
}, },
{ {
"ownerIndex": 2, "ownerIndex": 2,
@ -30,7 +32,8 @@
"description": "test-3", "description": "test-3",
"template": "test-3", "template": "test-3",
"framework": "test-3", "framework": "test-3",
"webhooks": [] "webhooks": [],
"icon": ""
}, },
{ {
"ownerIndex": 1, "ownerIndex": 1,
@ -41,7 +44,8 @@
"description": "test-4", "description": "test-4",
"template": "test-4", "template": "test-4",
"framework": "test-4", "framework": "test-4",
"webhooks": [] "webhooks": [],
"icon": ""
}, },
{ {
"ownerIndex": 0, "ownerIndex": 0,
@ -52,6 +56,7 @@
"description": "test-5", "description": "test-5",
"template": "test-5", "template": "test-5",
"framework": "test-5", "framework": "test-5",
"webhooks": [] "webhooks": [],
"icon": ""
} }
] ]

View File

@ -25,7 +25,9 @@ const ProjectCard: React.FC<ProjectCardProps> = ({ project }) => {
<Link to={`projects/${project.id}`}> <Link to={`projects/${project.id}`}>
<Typography>{project.name}</Typography> <Typography>{project.name}</Typography>
<Typography color="gray" variant="small"> <Typography color="gray" variant="small">
{project.url} {project.deployments[0]?.domain.name
? project.deployments[0]?.domain.name
: ''}
</Typography> </Typography>
</Link> </Link>
</div> </div>

View File

@ -31,7 +31,9 @@ const OverviewTabPanel = ({ project, organizationProject }: OverviewProps) => {
<div className="grow"> <div className="grow">
<Typography>{project.name}</Typography> <Typography>{project.name}</Typography>
<Typography variant="small" color="gray"> <Typography variant="small" color="gray">
{organizationProject.url} {project.deployments[0]?.domain.name
? project.deployments[0]?.domain.name
: ''}
</Typography> </Typography>
</div> </div>
</div> </div>

View File

@ -1,6 +1,8 @@
import React from 'react'; import React, { useCallback } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import toast from 'react-hot-toast';
import { Project } from 'gql-client';
import { import {
Button, Button,
@ -11,13 +13,12 @@ import {
Input, Input,
Typography, Typography,
} from '@material-tailwind/react'; } from '@material-tailwind/react';
import { useGQLClient } from '../../../../context/GQLClientContext';
import { ProjectDetails } from '../../../../types/project';
interface DeleteProjectDialogProp { interface DeleteProjectDialogProp {
open: boolean; open: boolean;
handleOpen: () => void; handleOpen: () => void;
project: Partial<ProjectDetails>; project: Project;
} }
const DeleteProjectDialog = ({ const DeleteProjectDialog = ({
@ -26,6 +27,7 @@ const DeleteProjectDialog = ({
project, project,
}: DeleteProjectDialogProp) => { }: DeleteProjectDialogProp) => {
const navigate = useNavigate(); const navigate = useNavigate();
const client = useGQLClient();
const { const {
handleSubmit, handleSubmit,
@ -37,6 +39,18 @@ const DeleteProjectDialog = ({
}, },
}); });
const deleteProjectHandler = useCallback(async () => {
const { deleteProject } = await client.deleteProject(project.id);
if (deleteProject) {
navigate('/');
} else {
toast.error('Project not deleted');
}
handleOpen();
}, [client, project, handleOpen]);
return ( return (
<Dialog open={open} handler={handleOpen}> <Dialog open={open} handler={handleOpen}>
<DialogHeader className="flex justify-between"> <DialogHeader className="flex justify-between">
@ -49,12 +63,7 @@ const DeleteProjectDialog = ({
X X
</Button> </Button>
</DialogHeader> </DialogHeader>
<form <form onSubmit={handleSubmit(deleteProjectHandler)}>
onSubmit={handleSubmit(() => {
handleOpen();
navigate('/');
})}
>
<DialogBody className="flex flex-col gap-2"> <DialogBody className="flex flex-col gap-2">
<Typography variant="paragraph"> <Typography variant="paragraph">
Deleting your project is irreversible. Enter your projects Deleting your project is irreversible. Enter your projects

View File

@ -11,14 +11,10 @@ import {
Card, Card,
} from '@material-tailwind/react'; } from '@material-tailwind/react';
import { import { ProjectDetails, RepositoryDetails } from '../../../../types/project';
DomainDetails,
DomainStatus,
ProjectDetails,
RepositoryDetails,
} from '../../../../types/project';
import ConfirmDialog from '../../../shared/ConfirmDialog'; import ConfirmDialog from '../../../shared/ConfirmDialog';
import EditDomainDialog from './EditDomainDialog'; import EditDomainDialog from './EditDomainDialog';
import { Domain, DomainStatus } from 'gql-client';
enum RefreshStatus { enum RefreshStatus {
IDLE, IDLE,
@ -28,13 +24,20 @@ enum RefreshStatus {
} }
interface DomainCardProps { interface DomainCardProps {
domain: DomainDetails; domain: Domain;
repo: RepositoryDetails; repo: RepositoryDetails;
project: ProjectDetails; project: ProjectDetails;
} }
const CHECK_FAIL_TIMEOUT = 5000; // In milliseconds const CHECK_FAIL_TIMEOUT = 5000; // In milliseconds
// TODO: Get domain record
const DOMAIN_RECORD = {
type: 'A',
name: '@',
value: '56.49.19.21',
};
const DomainCard = ({ domain, repo, project }: DomainCardProps) => { const DomainCard = ({ domain, repo, project }: DomainCardProps) => {
const [refreshStatus, SetRefreshStatus] = useState(RefreshStatus.IDLE); const [refreshStatus, SetRefreshStatus] = useState(RefreshStatus.IDLE);
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
@ -50,7 +53,7 @@ const DomainCard = ({ domain, repo, project }: DomainCardProps) => {
<Chip <Chip
className="w-fit capitalize" className="w-fit capitalize"
value={domain.status} value={domain.status}
color={domain.status === DomainStatus.LIVE ? 'green' : 'orange'} color={domain.status === DomainStatus.Live ? 'green' : 'orange'}
variant="ghost" variant="ghost"
icon={<i>^</i>} icon={<i>^</i>}
/> />
@ -106,7 +109,7 @@ const DomainCard = ({ domain, repo, project }: DomainCardProps) => {
<Typography variant="small"> <Typography variant="small">
Once deleted, the project{' '} Once deleted, the project{' '}
<span className="bg-blue-100 rounded-sm p-0.5 text-blue-700"> <span className="bg-blue-100 rounded-sm p-0.5 text-blue-700">
{project.title} {project.name}
</span>{' '} </span>{' '}
will not be accessible from the domain{' '} will not be accessible from the domain{' '}
<span className="bg-blue-100 rounded-sm p-0.5 text-blue-700"> <span className="bg-blue-100 rounded-sm p-0.5 text-blue-700">
@ -117,7 +120,7 @@ const DomainCard = ({ domain, repo, project }: DomainCardProps) => {
</div> </div>
<Typography variant="small">Production</Typography> <Typography variant="small">Production</Typography>
{domain.status === DomainStatus.PENDING && ( {domain.status === DomainStatus.Pending && (
<Card className="bg-gray-200 p-4 text-sm"> <Card className="bg-gray-200 p-4 text-sm">
{refreshStatus === RefreshStatus.IDLE ? ( {refreshStatus === RefreshStatus.IDLE ? (
<Typography variant="small"> <Typography variant="small">
@ -147,9 +150,9 @@ const DomainCard = ({ domain, repo, project }: DomainCardProps) => {
</thead> </thead>
<tbody> <tbody>
<tr> <tr>
<td>{domain.record.type}</td> <td>{DOMAIN_RECORD.type}</td>
<td>{domain.record.name}</td> <td>{DOMAIN_RECORD.name}</td>
<td>{domain.record.value}</td> <td>{DOMAIN_RECORD.value}</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>

View File

@ -4,10 +4,7 @@ import { useParams, Link, useOutletContext } from 'react-router-dom';
import { Button, Typography } from '@material-tailwind/react'; import { Button, Typography } from '@material-tailwind/react';
import DomainCard from './DomainCard'; import DomainCard from './DomainCard';
import { import { ProjectSearchOutletContext } from '../../../../types/project';
DomainDetails,
ProjectSearchOutletContext,
} from '../../../../types/project';
const Domains = () => { const Domains = () => {
const { id } = useParams(); const { id } = useParams();
@ -21,7 +18,7 @@ const Domains = () => {
}, [id, projects]); }, [id, projects]);
const linkedRepo = useMemo(() => { const linkedRepo = useMemo(() => {
return currentProject?.repositories.find( return currentProject?.repositories?.find(
(repo: any) => repo.id === Number(currentProject?.repositoryId), (repo: any) => repo.id === Number(currentProject?.repositoryId),
); );
}, [currentProject]); }, [currentProject]);
@ -43,7 +40,7 @@ const Domains = () => {
</Link> </Link>
</div> </div>
{(domains as DomainDetails[]).map((domain) => { {domains?.map((domain) => {
return ( return (
<DomainCard <DomainCard
domain={domain} domain={domain}

View File

@ -1,6 +1,7 @@
import React, { useMemo } from 'react'; import React, { useMemo } from 'react';
import { Controller, useForm } from 'react-hook-form'; import { Controller, useForm } from 'react-hook-form';
import toast from 'react-hot-toast'; import toast from 'react-hot-toast';
import { Domain } from 'gql-client';
import { import {
Button, Button,
@ -14,18 +15,14 @@ import {
Option, Option,
} from '@material-tailwind/react'; } from '@material-tailwind/react';
import { import { ProjectDetails, RepositoryDetails } from '../../../../types/project';
DomainDetails,
ProjectDetails,
RepositoryDetails,
} from '../../../../types/project';
const DEFAULT_REDIRECT_OPTIONS = ['none']; const DEFAULT_REDIRECT_OPTIONS = ['none'];
interface EditDomainDialogProp { interface EditDomainDialogProp {
open: boolean; open: boolean;
handleOpen: () => void; handleOpen: () => void;
domain: DomainDetails; domain: Domain;
repo: RepositoryDetails; repo: RepositoryDetails;
project: ProjectDetails; project: ProjectDetails;
} }
@ -37,7 +34,7 @@ const EditDomainDialog = ({
repo, repo,
project, project,
}: EditDomainDialogProp) => { }: EditDomainDialogProp) => {
const getRedirectUrl = (domain: DomainDetails) => { const getRedirectUrl = (domain: Domain) => {
const domainArr = domain.name.split('www.'); const domainArr = domain.name.split('www.');
let redirectUrl = ''; let redirectUrl = '';
if (domain.name.startsWith('www.')) { if (domain.name.startsWith('www.')) {
@ -79,7 +76,7 @@ const EditDomainDialog = ({
defaultValues: { defaultValues: {
name: domain.name, name: domain.name,
branch: repo.branch[0], branch: repo.branch[0],
redirectedTo: !domain.isRedirectedto redirectedTo: !domain.isRedirected
? redirectOptions[0] ? redirectOptions[0]
: redirectOptions[1], : redirectOptions[1],
}, },

View File

@ -206,7 +206,7 @@ const GeneralTabPanel = ({
<DeleteProjectDialog <DeleteProjectDialog
handleOpen={handleDeleteProjectDialog} handleOpen={handleDeleteProjectDialog}
open={openDeleteDialog} open={openDeleteDialog}
project={{ name: 'Iglootools' }} project={project}
/> />
</div> </div>
</> </>

View File

@ -1,14 +1,41 @@
import React from 'react'; import React, { useCallback, useEffect, useState } from 'react';
import { useOutletContext } from 'react-router-dom';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { Button, Typography, Chip } from '@material-tailwind/react'; import { Button, Typography, Chip } from '@material-tailwind/react';
import ProjectCard from '../components/projects/ProjectCard'; import ProjectCard from '../components/projects/ProjectCard';
import { ProjectSearchOutletContext } from '../types/project'; import { useGQLClient } from '../context/GQLClientContext';
import { ProjectDetails } from '../types/project';
// TODO: Implement organization switcher
const USER_ORGANIZATION_ID = '1';
const Projects = () => { const Projects = () => {
const { projects } = useOutletContext<ProjectSearchOutletContext>(); const client = useGQLClient();
const [projects, setProjects] = useState<ProjectDetails[]>([]);
const fetchProjects = useCallback(async () => {
const { projectsInOrganization } =
await client.getProjectsInOrganization(USER_ORGANIZATION_ID);
const updatedProjects = projectsInOrganization.map((project) => {
return {
...project,
// TODO: Populate from github API
latestCommit: {
message: 'subscription added',
createdAt: '2023-12-11T04:20:00',
branch: 'main',
},
};
});
setProjects(updatedProjects);
}, []);
useEffect(() => {
fetchProjects();
}, []);
return ( return (
<div> <div>
@ -33,7 +60,7 @@ const Projects = () => {
</div> </div>
<div className="grid grid-cols-3 gap-5 p-5"> <div className="grid grid-cols-3 gap-5 p-5">
{projects.length !== 0 && {projects.length !== 0 &&
projects.map((project: any, key: number) => { projects.map((project, key) => {
return <ProjectCard project={project} key={key} />; return <ProjectCard project={project} key={key} />;
})} })}
</div> </div>

View File

@ -1,29 +1,19 @@
import { Environment, EnvironmentVariable } from 'gql-client'; import { Environment, Project } from 'gql-client';
export interface ProjectDetails { export interface ProjectDetails extends Project {
icon: string; // TODO: isDomain flag
name: string; domain?: string | null;
title: string; // TODO: Use deployment branch
owner: Member; source?: string;
organization: string;
description: string;
url: string;
domain: string | null;
id: string;
createdAt: string;
createdBy: string;
deployments: DeploymentDetails[];
source: string;
latestCommit: { latestCommit: {
message: string; message: string;
createdAt: string; createdAt: string;
branch: string; branch: string;
}; };
repositoryId: number;
repositories: RepositoryDetails[]; // TODO: Move out of project
members: ProjectMember[]; repositories?: RepositoryDetails[];
ownerId: number; repositoryId?: number;
environmentVariables: EnvironmentVariable[];
} }
export interface ProjectMember { export interface ProjectMember {

View File

@ -1,8 +1,8 @@
import { ApolloClient, DefaultOptions, InMemoryCache, NormalizedCacheObject } from '@apollo/client'; import { ApolloClient, DefaultOptions, InMemoryCache, NormalizedCacheObject } from '@apollo/client';
import { getUser, getOrganizations, getDeployments, getProjectMembers, searchProjects, getEnvironmentVariables, getProject } from './queries'; import { getUser, getOrganizations, getDeployments, getProjectMembers, searchProjects, getEnvironmentVariables, getProject, getProjectsInOrganization } from './queries';
import { AddEnvironmentVariableInput, AddEnvironmentVariablesResponse, GetDeploymentsResponse, GetEnvironmentVariablesResponse, GetOrganizationsResponse, GetProjectMembersResponse, SearchProjectsResponse, GetUserResponse, RemoveMemberResponse, UpdateDeploymentToProdResponse, GetProjectResponse, UpdateProjectResponse, UpdateProjectInput, RedeployToProdResponse } from './types'; import { AddEnvironmentVariableInput, AddEnvironmentVariablesResponse, GetDeploymentsResponse, GetEnvironmentVariablesResponse, GetOrganizationsResponse, GetProjectMembersResponse, SearchProjectsResponse, GetUserResponse, RemoveMemberResponse, UpdateDeploymentToProdResponse, GetProjectResponse, UpdateProjectResponse, UpdateProjectInput, RedeployToProdResponse, DeleteProjectResponse, GetProjectsInOrganizationResponse } from './types';
import { removeMember, addEnvironmentVariables, updateDeploymentToProd, updateProjectMutation, redeployToProd } from './mutations'; import { removeMember, addEnvironmentVariables, updateDeploymentToProd, updateProjectMutation, redeployToProd, deleteProject } from './mutations';
export interface GraphQLConfig { export interface GraphQLConfig {
gqlEndpoint: string; gqlEndpoint: string;
@ -50,6 +50,17 @@ export class GQLClient {
return data; return data;
} }
async getProjectsInOrganization (organizationId: string) : Promise<GetProjectsInOrganizationResponse> {
const { data } = await this.client.query({
query: getProjectsInOrganization,
variables: {
organizationId
}
});
return data;
}
async getOrganizations () : Promise<GetOrganizationsResponse> { async getOrganizations () : Promise<GetOrganizationsResponse> {
const { data } = await this.client.query({ const { data } = await this.client.query({
query: getOrganizations query: getOrganizations
@ -158,4 +169,15 @@ export class GQLClient {
return data; return data;
} }
async deleteProject (projectId: string): Promise<DeleteProjectResponse> {
const { data } = await this.client.mutate({
mutation: deleteProject,
variables: {
projectId
}
});
return data;
}
} }

View File

@ -28,3 +28,9 @@ mutation ($deploymentId: String!) {
redeployToProd(deploymentId: $deploymentId) redeployToProd(deploymentId: $deploymentId)
} }
`; `;
export const deleteProject = gql`
mutation ($projectId: String!) {
deleteProject(projectId: $projectId)
}
`;

View File

@ -25,11 +25,69 @@ query ($projectId: String!) {
framework framework
repository repository
webhooks webhooks
icon
owner { owner {
id id
name name
email email
} }
deployments {
id
branch
isCurrent
status
title
updatedAt
commitHash
createdAt
environment
domain {
status
branch
createdAt
updatedAt
id
name
isRedirected
}
}
}
}
`;
export const getProjectsInOrganization = gql`
query ($organizationId: String!) {
projectsInOrganization(organizationId: $organizationId) {
id
name
createdAt
description
framework
prodBranch
webhooks
repository
updatedAt
icon
deployments {
id
branch
isCurrent
status
title
updatedAt
commitHash
createdAt
environment
domain {
status
branch
createdAt
updatedAt
id
name
isRedirected
}
}
} }
} }
`; `;

View File

@ -126,6 +126,7 @@ export type Project = {
createdAt: string createdAt: string
updatedAt: string updatedAt: string
organization: Organization organization: Organization
icon: string
} }
export type GetProjectMembersResponse = { export type GetProjectMembersResponse = {
@ -156,6 +157,10 @@ export type GetProjectResponse = {
project: Project | null project: Project | null
} }
export type GetProjectsInOrganizationResponse = {
projectsInOrganization: Project[]
}
export type SearchProjectsResponse = { export type SearchProjectsResponse = {
searchProjects: Project[] searchProjects: Project[]
} }
@ -178,6 +183,10 @@ export type UpdateProjectResponse = {
updateProject: boolean; updateProject: boolean;
} }
export type DeleteProjectResponse = {
deleteProject: boolean;
}
export type UpdateProjectInput = { export type UpdateProjectInput = {
name: string name: string
description: string description: string