forked from cerc-io/snowballtools-base
		
	Add GQL query for projects search in app (#38)
* Find projects based on search text * Use get search projects client method in UI * Fetch searched projects inside useCombobox hook * Get searched project from project entity * Remove non required search projects filtering * Fetch projects if user is owner or project member --------- Co-authored-by: neeraj <neeraj.rtly@gmail.com>
This commit is contained in:
		
							parent
							
								
									cbc394f9f8
								
							
						
					
					
						commit
						c2b997a17b
					
				| @ -190,4 +190,20 @@ export class Database { | ||||
| 
 | ||||
|     return projectMemberWithProject[0]; | ||||
|   } | ||||
| 
 | ||||
|   async getProjectsBySearchText (userId: number, searchText: string): Promise<Project[]> { | ||||
|     const projectRepository = this.dataSource.getRepository(Project); | ||||
| 
 | ||||
|     const projects = await projectRepository | ||||
|       .createQueryBuilder('project') | ||||
|       .leftJoinAndSelect('project.organization', 'organization') | ||||
|       .leftJoin('project.projectMembers', 'projectMembers') | ||||
|       .where('(project.owner = :userId OR projectMembers.member.id = :userId) AND project.name LIKE :searchText', { | ||||
|         userId, | ||||
|         searchText: `%${searchText}%` | ||||
|       }) | ||||
|       .getMany(); | ||||
| 
 | ||||
|     return projects; | ||||
|   } | ||||
| } | ||||
|  | ||||
| @ -5,11 +5,13 @@ import { | ||||
|   CreateDateColumn, | ||||
|   UpdateDateColumn, | ||||
|   ManyToOne, | ||||
|   JoinColumn | ||||
|   JoinColumn, | ||||
|   OneToMany | ||||
| } from 'typeorm'; | ||||
| 
 | ||||
| import { User } from './User'; | ||||
| import { Organization } from './Organization'; | ||||
| import { ProjectMember } from './ProjectMember'; | ||||
| 
 | ||||
| @Entity() | ||||
| export class Project { | ||||
| @ -52,4 +54,7 @@ export class Project { | ||||
| 
 | ||||
|   @UpdateDateColumn() | ||||
|     updatedAt!: Date; | ||||
| 
 | ||||
|   @OneToMany(() => ProjectMember, projectMember => projectMember.project) | ||||
|     projectMembers!: ProjectMember[]; | ||||
| } | ||||
|  | ||||
| @ -21,11 +21,11 @@ export class ProjectMember { | ||||
|   @PrimaryGeneratedColumn() | ||||
|     id!: number; | ||||
| 
 | ||||
|   @ManyToOne(() => User, { onDelete: 'CASCADE' }) | ||||
|   @ManyToOne(() => User, (user) => user.projectMembers, { onDelete: 'CASCADE' }) | ||||
|   @JoinColumn({ name: 'userId' }) | ||||
|     member!: User; | ||||
| 
 | ||||
|   @ManyToOne(() => Project, { onDelete: 'CASCADE' }) | ||||
|   @ManyToOne(() => Project, (project) => project.projectMembers, { onDelete: 'CASCADE' }) | ||||
|   @JoinColumn({ name: 'projectId' }) | ||||
|     project!: Project; | ||||
| 
 | ||||
|  | ||||
| @ -2,8 +2,10 @@ import { | ||||
|   Entity, | ||||
|   PrimaryGeneratedColumn, | ||||
|   Column, | ||||
|   CreateDateColumn | ||||
|   CreateDateColumn, | ||||
|   OneToMany | ||||
| } from 'typeorm'; | ||||
| import { ProjectMember } from './ProjectMember'; | ||||
| 
 | ||||
| @Entity() | ||||
| export class User { | ||||
| @ -21,4 +23,7 @@ export class User { | ||||
| 
 | ||||
|   @CreateDateColumn() | ||||
|     updatedAt!: Date; | ||||
| 
 | ||||
|   @OneToMany(() => ProjectMember, projectMember => projectMember.project) | ||||
|     projectMembers!: ProjectMember[]; | ||||
| } | ||||
|  | ||||
| @ -66,6 +66,16 @@ export const createResolvers = async (db: Database): Promise<any> => { | ||||
|         }); | ||||
| 
 | ||||
|         return projectMembers; | ||||
|       }, | ||||
| 
 | ||||
|       searchProjects: async (_: any, { searchText }: { searchText: string }, context: any) => { | ||||
|         const dbProjects = await db.getProjectsBySearchText(context.userId, searchText); | ||||
| 
 | ||||
|         const projects = dbProjects.map((project) => { | ||||
|           return projectToGqlType(project, [], []); | ||||
|         }); | ||||
| 
 | ||||
|         return projects; | ||||
|       } | ||||
|     }, | ||||
| 
 | ||||
|  | ||||
| @ -68,6 +68,7 @@ type Project { | ||||
|   environmentVariables: [EnvironmentVariable!] | ||||
|   createdAt: String! | ||||
|   updatedAt: String! | ||||
|   organization: Organization! | ||||
| } | ||||
| 
 | ||||
| type ProjectMember { | ||||
| @ -116,6 +117,7 @@ type Query { | ||||
|   projects: [Project!] | ||||
|   deployments(projectId: String!): [Deployment!] | ||||
|   projectMembers(projectId: String!): [ProjectMember!] | ||||
|   searchProjects(searchText: String!): [Project!] | ||||
| } | ||||
| 
 | ||||
| type Mutation { | ||||
|  | ||||
| @ -39,7 +39,8 @@ export const projectToGqlType = (dbProject: Project, projectMembers: ProjectMemb | ||||
|     members: projectMembers, | ||||
|     environmentVariables: environmentVariables, | ||||
|     createdAt: dbProject.createdAt, | ||||
|     updatedAt: dbProject.updatedAt | ||||
|     updatedAt: dbProject.updatedAt, | ||||
|     organization: dbProject.organization | ||||
|   }; | ||||
| }; | ||||
| 
 | ||||
|  | ||||
| @ -1,4 +1,4 @@ | ||||
| import React, { useState } from 'react'; | ||||
| import React, { useCallback, useState } from 'react'; | ||||
| import { useCombobox } from 'downshift'; | ||||
| 
 | ||||
| import { | ||||
| @ -9,17 +9,19 @@ import { | ||||
|   Typography, | ||||
| } from '@material-tailwind/react'; | ||||
| 
 | ||||
| import { Project } from 'gql-client'; | ||||
| 
 | ||||
| import SearchBar from '../SearchBar'; | ||||
| import { ProjectDetails } from '../../types/project'; | ||||
| import { useGQLClient } from '../../context/GQLClientContext'; | ||||
| 
 | ||||
| interface ProjectsSearchProps { | ||||
|   projects: ProjectDetails[]; | ||||
|   onChange?: (data: ProjectDetails) => void; | ||||
|   onChange?: (data: Project) => void; | ||||
| } | ||||
| 
 | ||||
| const ProjectSearchBar = ({ projects, onChange }: ProjectsSearchProps) => { | ||||
|   const [items, setItems] = useState<ProjectDetails[]>([]); | ||||
|   const [selectedItem, setSelectedItem] = useState<ProjectDetails | null>(null); | ||||
| const ProjectSearchBar = ({ onChange }: ProjectsSearchProps) => { | ||||
|   const [items, setItems] = useState<Project[]>([]); | ||||
|   const [selectedItem, setSelectedItem] = useState<Project | null>(null); | ||||
|   const client = useGQLClient(); | ||||
| 
 | ||||
|   const { | ||||
|     isOpen, | ||||
| @ -30,17 +32,14 @@ const ProjectSearchBar = ({ projects, onChange }: ProjectsSearchProps) => { | ||||
|     inputValue, | ||||
|   } = useCombobox({ | ||||
|     onInputValueChange({ inputValue }) { | ||||
|       setItems( | ||||
|         inputValue | ||||
|           ? projects.filter((project) => | ||||
|               project.title.toLowerCase().includes(inputValue.toLowerCase()), | ||||
|             ) | ||||
|           : [], | ||||
|       ); | ||||
|       if (inputValue) { | ||||
|         // TODO: Use debounce
 | ||||
|         fetchProjects(inputValue); | ||||
|       } | ||||
|     }, | ||||
|     items, | ||||
|     itemToString(item) { | ||||
|       return item ? item.title : ''; | ||||
|       return item ? item.name : ''; | ||||
|     }, | ||||
|     selectedItem, | ||||
|     onSelectedItemChange: ({ selectedItem: newSelectedItem }) => { | ||||
| @ -54,6 +53,14 @@ const ProjectSearchBar = ({ projects, onChange }: ProjectsSearchProps) => { | ||||
|     }, | ||||
|   }); | ||||
| 
 | ||||
|   const fetchProjects = useCallback( | ||||
|     async (inputValue: string) => { | ||||
|       const { searchProjects } = await client.searchProjects(inputValue); | ||||
|       setItems(searchProjects); | ||||
|     }, | ||||
|     [client], | ||||
|   ); | ||||
| 
 | ||||
|   return ( | ||||
|     <div className="relative"> | ||||
|       <SearchBar {...getInputProps()} /> | ||||
| @ -81,14 +88,14 @@ const ProjectSearchBar = ({ projects, onChange }: ProjectsSearchProps) => { | ||||
|                   </ListItemPrefix> | ||||
|                   <div> | ||||
|                     <Typography variant="h6" color="blue-gray"> | ||||
|                       {item.title} | ||||
|                       {item.name} | ||||
|                     </Typography> | ||||
|                     <Typography | ||||
|                       variant="small" | ||||
|                       color="gray" | ||||
|                       className="font-normal" | ||||
|                     > | ||||
|                       {item.organization} | ||||
|                       {item.organization.name} | ||||
|                     </Typography> | ||||
|                   </div> | ||||
|                 </ListItem> | ||||
|  | ||||
| @ -1,5 +1,5 @@ | ||||
| import React, { useEffect, useState } from 'react'; | ||||
| import { Outlet } from 'react-router-dom'; | ||||
| import { Outlet, useNavigate } from 'react-router-dom'; | ||||
| 
 | ||||
| import HorizontalLine from '../components/HorizontalLine'; | ||||
| import { IconButton, Typography } from '@material-tailwind/react'; | ||||
| @ -9,6 +9,7 @@ import { Environments, ProjectDetails } from '../types/project'; | ||||
| 
 | ||||
| const ProjectSearch = () => { | ||||
|   const client = useGQLClient(); | ||||
|   const navigate = useNavigate(); | ||||
|   const [projects, setProjects] = useState<ProjectDetails[]>([]); | ||||
| 
 | ||||
|   useEffect(() => { | ||||
| @ -87,7 +88,11 @@ const ProjectSearch = () => { | ||||
|       <div className="sticky top-0 bg-white z-30"> | ||||
|         <div className="flex p-5"> | ||||
|           <div className="grow mr-2"> | ||||
|             <ProjectSearchBar onChange={() => {}} projects={projects} /> | ||||
|             <ProjectSearchBar | ||||
|               onChange={(project) => { | ||||
|                 navigate(`/projects/${project.id}`); | ||||
|               }} | ||||
|             /> | ||||
|           </div> | ||||
|           <IconButton color="blue" className="rounded-full mr-2"> | ||||
|             <Typography variant="h5">+</Typography> | ||||
|  | ||||
| @ -1 +1,2 @@ | ||||
| export * from './src/client'; | ||||
| export * from './src/types'; | ||||
|  | ||||
| @ -1,7 +1,7 @@ | ||||
| import { ApolloClient, DefaultOptions, InMemoryCache, NormalizedCacheObject } from '@apollo/client'; | ||||
| 
 | ||||
| import { getUser, getOrganizations, getDeployments, getProjectMembers } from './queries'; | ||||
| import { GetDeploymentsResponse, GetOrganizationsResponse, GetProjectMembersResponse, GetUserResponse, RemoveMemberResponse } from './types'; | ||||
| import { getUser, getOrganizations, getDeployments, getProjectMembers, searchProjects } from './queries'; | ||||
| import { GetDeploymentsResponse, GetOrganizationsResponse, GetProjectMembersResponse, SearchProjectsResponse, GetUserResponse, RemoveMemberResponse } from './types'; | ||||
| import { removeMember } from './mutations'; | ||||
| 
 | ||||
| export interface GraphQLConfig { | ||||
| @ -79,4 +79,15 @@ export class GQLClient { | ||||
| 
 | ||||
|     return data; | ||||
|   } | ||||
| 
 | ||||
|   async searchProjects (searchText: string) : Promise<SearchProjectsResponse> { | ||||
|     const { data } = await this.client.query({ | ||||
|       query: searchProjects, | ||||
|       variables: { | ||||
|         searchText | ||||
|       } | ||||
|     }); | ||||
| 
 | ||||
|     return data; | ||||
|   } | ||||
| } | ||||
|  | ||||
| @ -97,3 +97,28 @@ query ($projectId: String!) { | ||||
|   } | ||||
| } | ||||
| `;
 | ||||
| 
 | ||||
| export const searchProjects = gql` | ||||
| query ($searchText: String!) { | ||||
|   searchProjects(searchText: $searchText) { | ||||
|     id | ||||
|     name | ||||
|     prodBranch | ||||
|     repository | ||||
|     createdAt | ||||
|     description | ||||
|     framework | ||||
|     prodBranch | ||||
|     webhooks | ||||
|     updatedAt | ||||
|     template | ||||
|     repository | ||||
|     organization { | ||||
|       id | ||||
|       name | ||||
|       createdAt | ||||
|       updatedAt | ||||
|     } | ||||
|   } | ||||
| } | ||||
| `;
 | ||||
|  | ||||
| @ -84,7 +84,7 @@ export type ProjectMember = { | ||||
|   updatedAt: string | ||||
| } | ||||
| 
 | ||||
| export type Project = { | ||||
| export type OrganizationProject = { | ||||
|   id: string | ||||
|   owner: User | ||||
|   deployments: Deployment[] | ||||
| @ -104,12 +104,30 @@ export type Project = { | ||||
| export type Organization = { | ||||
|   id: string | ||||
|   name: string | ||||
|   projects: Project[] | ||||
|   projects: OrganizationProject[] | ||||
|   createdAt: string | ||||
|   updatedAt: string | ||||
|   members: OrganizationMember[] | ||||
| } | ||||
| 
 | ||||
| export type Project = { | ||||
|   id: string | ||||
|   owner: User | ||||
|   deployments: Deployment[] | ||||
|   name: string | ||||
|   repository: string | ||||
|   prodBranch: string | ||||
|   description: string | ||||
|   template: string | ||||
|   framework: string | ||||
|   webhooks: string[] | ||||
|   members: ProjectMember[] | ||||
|   environmentVariables: EnvironmentVariable[] | ||||
|   createdAt: string | ||||
|   updatedAt: string | ||||
|   organization: Organization | ||||
| } | ||||
| 
 | ||||
| export type GetProjectMembersResponse = { | ||||
|   projectMembers: ProjectMember[] | ||||
| } | ||||
| @ -129,3 +147,7 @@ export type GetOrganizationsResponse = { | ||||
| export type GetUserResponse = { | ||||
|   user: User | ||||
| } | ||||
| 
 | ||||
| export type SearchProjectsResponse = { | ||||
|   searchProjects: Project[] | ||||
| } | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user