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]; |     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, |   CreateDateColumn, | ||||||
|   UpdateDateColumn, |   UpdateDateColumn, | ||||||
|   ManyToOne, |   ManyToOne, | ||||||
|   JoinColumn |   JoinColumn, | ||||||
|  |   OneToMany | ||||||
| } from 'typeorm'; | } from 'typeorm'; | ||||||
| 
 | 
 | ||||||
| import { User } from './User'; | import { User } from './User'; | ||||||
| import { Organization } from './Organization'; | import { Organization } from './Organization'; | ||||||
|  | import { ProjectMember } from './ProjectMember'; | ||||||
| 
 | 
 | ||||||
| @Entity() | @Entity() | ||||||
| export class Project { | export class Project { | ||||||
| @ -52,4 +54,7 @@ export class Project { | |||||||
| 
 | 
 | ||||||
|   @UpdateDateColumn() |   @UpdateDateColumn() | ||||||
|     updatedAt!: Date; |     updatedAt!: Date; | ||||||
|  | 
 | ||||||
|  |   @OneToMany(() => ProjectMember, projectMember => projectMember.project) | ||||||
|  |     projectMembers!: ProjectMember[]; | ||||||
| } | } | ||||||
|  | |||||||
| @ -21,11 +21,11 @@ export class ProjectMember { | |||||||
|   @PrimaryGeneratedColumn() |   @PrimaryGeneratedColumn() | ||||||
|     id!: number; |     id!: number; | ||||||
| 
 | 
 | ||||||
|   @ManyToOne(() => User, { onDelete: 'CASCADE' }) |   @ManyToOne(() => User, (user) => user.projectMembers, { onDelete: 'CASCADE' }) | ||||||
|   @JoinColumn({ name: 'userId' }) |   @JoinColumn({ name: 'userId' }) | ||||||
|     member!: User; |     member!: User; | ||||||
| 
 | 
 | ||||||
|   @ManyToOne(() => Project, { onDelete: 'CASCADE' }) |   @ManyToOne(() => Project, (project) => project.projectMembers, { onDelete: 'CASCADE' }) | ||||||
|   @JoinColumn({ name: 'projectId' }) |   @JoinColumn({ name: 'projectId' }) | ||||||
|     project!: Project; |     project!: Project; | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -2,8 +2,10 @@ import { | |||||||
|   Entity, |   Entity, | ||||||
|   PrimaryGeneratedColumn, |   PrimaryGeneratedColumn, | ||||||
|   Column, |   Column, | ||||||
|   CreateDateColumn |   CreateDateColumn, | ||||||
|  |   OneToMany | ||||||
| } from 'typeorm'; | } from 'typeorm'; | ||||||
|  | import { ProjectMember } from './ProjectMember'; | ||||||
| 
 | 
 | ||||||
| @Entity() | @Entity() | ||||||
| export class User { | export class User { | ||||||
| @ -21,4 +23,7 @@ export class User { | |||||||
| 
 | 
 | ||||||
|   @CreateDateColumn() |   @CreateDateColumn() | ||||||
|     updatedAt!: Date; |     updatedAt!: Date; | ||||||
|  | 
 | ||||||
|  |   @OneToMany(() => ProjectMember, projectMember => projectMember.project) | ||||||
|  |     projectMembers!: ProjectMember[]; | ||||||
| } | } | ||||||
|  | |||||||
| @ -66,6 +66,16 @@ export const createResolvers = async (db: Database): Promise<any> => { | |||||||
|         }); |         }); | ||||||
| 
 | 
 | ||||||
|         return projectMembers; |         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!] |   environmentVariables: [EnvironmentVariable!] | ||||||
|   createdAt: String! |   createdAt: String! | ||||||
|   updatedAt: String! |   updatedAt: String! | ||||||
|  |   organization: Organization! | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| type ProjectMember { | type ProjectMember { | ||||||
| @ -116,6 +117,7 @@ type Query { | |||||||
|   projects: [Project!] |   projects: [Project!] | ||||||
|   deployments(projectId: String!): [Deployment!] |   deployments(projectId: String!): [Deployment!] | ||||||
|   projectMembers(projectId: String!): [ProjectMember!] |   projectMembers(projectId: String!): [ProjectMember!] | ||||||
|  |   searchProjects(searchText: String!): [Project!] | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| type Mutation { | type Mutation { | ||||||
|  | |||||||
| @ -39,7 +39,8 @@ export const projectToGqlType = (dbProject: Project, projectMembers: ProjectMemb | |||||||
|     members: projectMembers, |     members: projectMembers, | ||||||
|     environmentVariables: environmentVariables, |     environmentVariables: environmentVariables, | ||||||
|     createdAt: dbProject.createdAt, |     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 { useCombobox } from 'downshift'; | ||||||
| 
 | 
 | ||||||
| import { | import { | ||||||
| @ -9,17 +9,19 @@ import { | |||||||
|   Typography, |   Typography, | ||||||
| } from '@material-tailwind/react'; | } from '@material-tailwind/react'; | ||||||
| 
 | 
 | ||||||
|  | import { Project } from 'gql-client'; | ||||||
|  | 
 | ||||||
| import SearchBar from '../SearchBar'; | import SearchBar from '../SearchBar'; | ||||||
| import { ProjectDetails } from '../../types/project'; | import { useGQLClient } from '../../context/GQLClientContext'; | ||||||
| 
 | 
 | ||||||
| interface ProjectsSearchProps { | interface ProjectsSearchProps { | ||||||
|   projects: ProjectDetails[]; |   onChange?: (data: Project) => void; | ||||||
|   onChange?: (data: ProjectDetails) => void; |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| const ProjectSearchBar = ({ projects, onChange }: ProjectsSearchProps) => { | const ProjectSearchBar = ({ onChange }: ProjectsSearchProps) => { | ||||||
|   const [items, setItems] = useState<ProjectDetails[]>([]); |   const [items, setItems] = useState<Project[]>([]); | ||||||
|   const [selectedItem, setSelectedItem] = useState<ProjectDetails | null>(null); |   const [selectedItem, setSelectedItem] = useState<Project | null>(null); | ||||||
|  |   const client = useGQLClient(); | ||||||
| 
 | 
 | ||||||
|   const { |   const { | ||||||
|     isOpen, |     isOpen, | ||||||
| @ -30,17 +32,14 @@ const ProjectSearchBar = ({ projects, onChange }: ProjectsSearchProps) => { | |||||||
|     inputValue, |     inputValue, | ||||||
|   } = useCombobox({ |   } = useCombobox({ | ||||||
|     onInputValueChange({ inputValue }) { |     onInputValueChange({ inputValue }) { | ||||||
|       setItems( |       if (inputValue) { | ||||||
|         inputValue |         // TODO: Use debounce
 | ||||||
|           ? projects.filter((project) => |         fetchProjects(inputValue); | ||||||
|               project.title.toLowerCase().includes(inputValue.toLowerCase()), |       } | ||||||
|             ) |  | ||||||
|           : [], |  | ||||||
|       ); |  | ||||||
|     }, |     }, | ||||||
|     items, |     items, | ||||||
|     itemToString(item) { |     itemToString(item) { | ||||||
|       return item ? item.title : ''; |       return item ? item.name : ''; | ||||||
|     }, |     }, | ||||||
|     selectedItem, |     selectedItem, | ||||||
|     onSelectedItemChange: ({ selectedItem: newSelectedItem }) => { |     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 ( |   return ( | ||||||
|     <div className="relative"> |     <div className="relative"> | ||||||
|       <SearchBar {...getInputProps()} /> |       <SearchBar {...getInputProps()} /> | ||||||
| @ -81,14 +88,14 @@ const ProjectSearchBar = ({ projects, onChange }: ProjectsSearchProps) => { | |||||||
|                   </ListItemPrefix> |                   </ListItemPrefix> | ||||||
|                   <div> |                   <div> | ||||||
|                     <Typography variant="h6" color="blue-gray"> |                     <Typography variant="h6" color="blue-gray"> | ||||||
|                       {item.title} |                       {item.name} | ||||||
|                     </Typography> |                     </Typography> | ||||||
|                     <Typography |                     <Typography | ||||||
|                       variant="small" |                       variant="small" | ||||||
|                       color="gray" |                       color="gray" | ||||||
|                       className="font-normal" |                       className="font-normal" | ||||||
|                     > |                     > | ||||||
|                       {item.organization} |                       {item.organization.name} | ||||||
|                     </Typography> |                     </Typography> | ||||||
|                   </div> |                   </div> | ||||||
|                 </ListItem> |                 </ListItem> | ||||||
|  | |||||||
| @ -1,5 +1,5 @@ | |||||||
| import React, { useEffect, useState } from 'react'; | 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 HorizontalLine from '../components/HorizontalLine'; | ||||||
| import { IconButton, Typography } from '@material-tailwind/react'; | import { IconButton, Typography } from '@material-tailwind/react'; | ||||||
| @ -9,6 +9,7 @@ import { Environments, ProjectDetails } from '../types/project'; | |||||||
| 
 | 
 | ||||||
| const ProjectSearch = () => { | const ProjectSearch = () => { | ||||||
|   const client = useGQLClient(); |   const client = useGQLClient(); | ||||||
|  |   const navigate = useNavigate(); | ||||||
|   const [projects, setProjects] = useState<ProjectDetails[]>([]); |   const [projects, setProjects] = useState<ProjectDetails[]>([]); | ||||||
| 
 | 
 | ||||||
|   useEffect(() => { |   useEffect(() => { | ||||||
| @ -87,7 +88,11 @@ const ProjectSearch = () => { | |||||||
|       <div className="sticky top-0 bg-white z-30"> |       <div className="sticky top-0 bg-white z-30"> | ||||||
|         <div className="flex p-5"> |         <div className="flex p-5"> | ||||||
|           <div className="grow mr-2"> |           <div className="grow mr-2"> | ||||||
|             <ProjectSearchBar onChange={() => {}} projects={projects} /> |             <ProjectSearchBar | ||||||
|  |               onChange={(project) => { | ||||||
|  |                 navigate(`/projects/${project.id}`); | ||||||
|  |               }} | ||||||
|  |             /> | ||||||
|           </div> |           </div> | ||||||
|           <IconButton color="blue" className="rounded-full mr-2"> |           <IconButton color="blue" className="rounded-full mr-2"> | ||||||
|             <Typography variant="h5">+</Typography> |             <Typography variant="h5">+</Typography> | ||||||
|  | |||||||
| @ -1 +1,2 @@ | |||||||
| export * from './src/client'; | export * from './src/client'; | ||||||
|  | export * from './src/types'; | ||||||
|  | |||||||
| @ -1,7 +1,7 @@ | |||||||
| import { ApolloClient, DefaultOptions, InMemoryCache, NormalizedCacheObject } from '@apollo/client'; | import { ApolloClient, DefaultOptions, InMemoryCache, NormalizedCacheObject } from '@apollo/client'; | ||||||
| 
 | 
 | ||||||
| import { getUser, getOrganizations, getDeployments, getProjectMembers } from './queries'; | import { getUser, getOrganizations, getDeployments, getProjectMembers, searchProjects } from './queries'; | ||||||
| import { GetDeploymentsResponse, GetOrganizationsResponse, GetProjectMembersResponse, GetUserResponse, RemoveMemberResponse } from './types'; | import { GetDeploymentsResponse, GetOrganizationsResponse, GetProjectMembersResponse, SearchProjectsResponse, GetUserResponse, RemoveMemberResponse } from './types'; | ||||||
| import { removeMember } from './mutations'; | import { removeMember } from './mutations'; | ||||||
| 
 | 
 | ||||||
| export interface GraphQLConfig { | export interface GraphQLConfig { | ||||||
| @ -79,4 +79,15 @@ export class GQLClient { | |||||||
| 
 | 
 | ||||||
|     return data; |     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 |   updatedAt: string | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export type Project = { | export type OrganizationProject = { | ||||||
|   id: string |   id: string | ||||||
|   owner: User |   owner: User | ||||||
|   deployments: Deployment[] |   deployments: Deployment[] | ||||||
| @ -104,12 +104,30 @@ export type Project = { | |||||||
| export type Organization = { | export type Organization = { | ||||||
|   id: string |   id: string | ||||||
|   name: string |   name: string | ||||||
|   projects: Project[] |   projects: OrganizationProject[] | ||||||
|   createdAt: string |   createdAt: string | ||||||
|   updatedAt: string |   updatedAt: string | ||||||
|   members: OrganizationMember[] |   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 = { | export type GetProjectMembersResponse = { | ||||||
|   projectMembers: ProjectMember[] |   projectMembers: ProjectMember[] | ||||||
| } | } | ||||||
| @ -129,3 +147,7 @@ export type GetOrganizationsResponse = { | |||||||
| export type GetUserResponse = { | export type GetUserResponse = { | ||||||
|   user: User |   user: User | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | export type SearchProjectsResponse = { | ||||||
|  |   searchProjects: Project[] | ||||||
|  | } | ||||||
|  | |||||||
		Loading…
	
		Reference in New Issue
	
	Block a user