From c2b997a17bc7bc6a0fd859bf30a76d6d509d398c Mon Sep 17 00:00:00 2001 From: Nabarun Gogoi Date: Wed, 24 Jan 2024 20:17:43 +0530 Subject: [PATCH] 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 --- packages/backend/src/database.ts | 16 ++++++++ packages/backend/src/entity/Project.ts | 7 +++- packages/backend/src/entity/ProjectMember.ts | 4 +- packages/backend/src/entity/User.ts | 7 +++- packages/backend/src/resolvers.ts | 10 +++++ packages/backend/src/schema.gql | 2 + packages/backend/src/utils.ts | 3 +- .../components/projects/ProjectSearchBar.tsx | 41 +++++++++++-------- .../frontend/src/layouts/ProjectSearch.tsx | 9 +++- packages/gql-client/index.ts | 1 + packages/gql-client/src/client.ts | 15 ++++++- packages/gql-client/src/queries.ts | 25 +++++++++++ packages/gql-client/src/types.ts | 26 +++++++++++- 13 files changed, 138 insertions(+), 28 deletions(-) diff --git a/packages/backend/src/database.ts b/packages/backend/src/database.ts index 71922c0f..1a71758d 100644 --- a/packages/backend/src/database.ts +++ b/packages/backend/src/database.ts @@ -190,4 +190,20 @@ export class Database { return projectMemberWithProject[0]; } + + async getProjectsBySearchText (userId: number, searchText: string): Promise { + 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; + } } diff --git a/packages/backend/src/entity/Project.ts b/packages/backend/src/entity/Project.ts index e709c97f..889d29bd 100644 --- a/packages/backend/src/entity/Project.ts +++ b/packages/backend/src/entity/Project.ts @@ -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[]; } diff --git a/packages/backend/src/entity/ProjectMember.ts b/packages/backend/src/entity/ProjectMember.ts index d1e4cc80..27e9e20d 100644 --- a/packages/backend/src/entity/ProjectMember.ts +++ b/packages/backend/src/entity/ProjectMember.ts @@ -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; diff --git a/packages/backend/src/entity/User.ts b/packages/backend/src/entity/User.ts index ae611ebc..a25ec81e 100644 --- a/packages/backend/src/entity/User.ts +++ b/packages/backend/src/entity/User.ts @@ -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[]; } diff --git a/packages/backend/src/resolvers.ts b/packages/backend/src/resolvers.ts index ff4c2f77..07ffc736 100644 --- a/packages/backend/src/resolvers.ts +++ b/packages/backend/src/resolvers.ts @@ -66,6 +66,16 @@ export const createResolvers = async (db: Database): Promise => { }); 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; } }, diff --git a/packages/backend/src/schema.gql b/packages/backend/src/schema.gql index 5125a454..87824458 100644 --- a/packages/backend/src/schema.gql +++ b/packages/backend/src/schema.gql @@ -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 { diff --git a/packages/backend/src/utils.ts b/packages/backend/src/utils.ts index 64ea47dc..8fcf9a0d 100644 --- a/packages/backend/src/utils.ts +++ b/packages/backend/src/utils.ts @@ -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 }; }; diff --git a/packages/frontend/src/components/projects/ProjectSearchBar.tsx b/packages/frontend/src/components/projects/ProjectSearchBar.tsx index 45655d55..bbba2d07 100644 --- a/packages/frontend/src/components/projects/ProjectSearchBar.tsx +++ b/packages/frontend/src/components/projects/ProjectSearchBar.tsx @@ -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([]); - const [selectedItem, setSelectedItem] = useState(null); +const ProjectSearchBar = ({ onChange }: ProjectsSearchProps) => { + const [items, setItems] = useState([]); + const [selectedItem, setSelectedItem] = useState(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 (
@@ -81,14 +88,14 @@ const ProjectSearchBar = ({ projects, onChange }: ProjectsSearchProps) => {
- {item.title} + {item.name} - {item.organization} + {item.organization.name}
diff --git a/packages/frontend/src/layouts/ProjectSearch.tsx b/packages/frontend/src/layouts/ProjectSearch.tsx index aa234905..6788319c 100644 --- a/packages/frontend/src/layouts/ProjectSearch.tsx +++ b/packages/frontend/src/layouts/ProjectSearch.tsx @@ -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([]); useEffect(() => { @@ -87,7 +88,11 @@ const ProjectSearch = () => {
- {}} projects={projects} /> + { + navigate(`/projects/${project.id}`); + }} + />
+ diff --git a/packages/gql-client/index.ts b/packages/gql-client/index.ts index cf988131..6aea40ad 100644 --- a/packages/gql-client/index.ts +++ b/packages/gql-client/index.ts @@ -1 +1,2 @@ export * from './src/client'; +export * from './src/types'; diff --git a/packages/gql-client/src/client.ts b/packages/gql-client/src/client.ts index a53ab459..ad8f2b41 100644 --- a/packages/gql-client/src/client.ts +++ b/packages/gql-client/src/client.ts @@ -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 { + const { data } = await this.client.query({ + query: searchProjects, + variables: { + searchText + } + }); + + return data; + } } diff --git a/packages/gql-client/src/queries.ts b/packages/gql-client/src/queries.ts index 95ec5637..a730d332 100644 --- a/packages/gql-client/src/queries.ts +++ b/packages/gql-client/src/queries.ts @@ -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 + } + } +} +`; diff --git a/packages/gql-client/src/types.ts b/packages/gql-client/src/types.ts index 04cf9d30..1cec546f 100644 --- a/packages/gql-client/src/types.ts +++ b/packages/gql-client/src/types.ts @@ -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[] +}