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