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