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:
Nabarun Gogoi 2024-01-24 20:17:43 +05:30 committed by Ashwin Phatak
parent cbc394f9f8
commit c2b997a17b
13 changed files with 138 additions and 28 deletions

View File

@ -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;
}
}

View File

@ -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[];
}

View File

@ -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;

View File

@ -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[];
}

View File

@ -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;
}
},

View File

@ -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 {

View File

@ -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
};
};

View File

@ -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>

View File

@ -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>

View File

@ -1 +1,2 @@
export * from './src/client';
export * from './src/types';

View File

@ -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;
}
}

View File

@ -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
}
}
}
`;

View File

@ -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[]
}