Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make the search bar auto complete #1449

Merged
merged 6 commits into from
Dec 14, 2023
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions epictrack-api/src/api/models/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,12 @@ class Project(BaseModelVersioned):
proponent = relationship("Proponent", foreign_keys=[proponent_id], lazy="select")
region_env = relationship("Region", foreign_keys=[region_id_env], lazy="select")
region_flnro = relationship("Region", foreign_keys=[region_id_flnro], lazy="select")
works = relationship('Work', lazy='dynamic')

@classmethod
def find_all_with_works(cls):
"""Return all projects with works."""
return cls.query.filter(cls.works.any()).filter(cls.is_deleted.is_(False)).all()

@classmethod
def check_existence(cls, name, project_id=None):
Expand Down
3 changes: 2 additions & 1 deletion epictrack-api/src/api/models/work.py
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,8 @@ def _filter_by_staff_id(cls, query, staff_id):
@classmethod
def _filter_by_search_text(cls, query, search_text):
if search_text:
query = query.filter(Work.title.ilike(f'%{search_text}%'))
subquery = exists().where(and_(Work.project_id == Project.id, Project.name == search_text))
query = query.filter(subquery)
return query

@classmethod
Expand Down
7 changes: 6 additions & 1 deletion epictrack-api/src/api/resources/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,12 @@ class Projects(Resource):
@profiletime
def get():
"""Return all projects."""
projects = ProjectService.find_all()
with_works = request.args.get(
'with_works',
default=False,
type=lambda v: v.lower() == 'true'
)
projects = ProjectService.find_all(with_works)
return_type = request.args.get("return_type", None)
if return_type == "list_type":
schema = res.ListTypeResponseSchema(many=True)
Expand Down
9 changes: 8 additions & 1 deletion epictrack-api/src/api/services/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,17 @@ def find(cls, project_id):
return project

@classmethod
def find_all(cls):
def find_all(cls, with_works=False):
"""Find all projects"""
if with_works:
return Project.find_all_with_works()
return Project.find_all(default_filters=False)

@classmethod
def find_all_with_works(cls):
"""Find all projects"""
return Project.find_all_with_works()

@classmethod
def create_project(cls, payload: dict):
"""Create a new project."""
Expand Down
87 changes: 67 additions & 20 deletions epictrack-web/src/components/myWorkplans/Filters/NameFilter.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,23 @@
import React, { useRef } from "react";
import { TextField } from "@mui/material";
import debounce from "lodash/debounce";
import React, { useState, useEffect, useContext } from "react";
import { Autocomplete, Box, InputAdornment, TextField } from "@mui/material";
import {
MyWorkplansContext,
WorkPlanSearchOptions,
} from "../MyWorkPlanContext";
import projectService from "../../../services/projectService/projectService";
import { PROJECT_RETURN_TYPE } from "../../../services/projectService/constants";
import { ListType } from "../../../models/code";
import SearchIcon from "../../../assets/images/search.svg";
import { highlightText } from "../../../utils/MatchingTextHighlight";

const SEARCH_TEXT_THRESHOLD = 1;
export const NameFilter = () => {
const [searchText, setSearchText] = React.useState<string>("");
const [loading, setLoading] = useState(true);
const [options, setOptions] = useState<string[]>([]);
const [searchText, setSearchText] = useState<string>("");

const { setSearchOptions } = useContext(MyWorkplansContext);

const { setSearchOptions } = React.useContext(MyWorkplansContext);
const handleSearchOptions = (searchText: string) => {
setSearchOptions(
(prev: WorkPlanSearchOptions) =>
Expand All @@ -20,25 +28,64 @@ export const NameFilter = () => {
);
};

const debouncedSetSearchOptions = useRef(
debounce((searchText: string) => {
handleSearchOptions(searchText);
}, 1000)
).current;
// Fetch project names from the backend when searchText changes
useEffect(() => {
const fetchProjectNames = async () => {
// Replace this with your actual backend API call
TomChapmanGov marked this conversation as resolved.
Show resolved Hide resolved
try {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this comment still relevant?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not, I should remove this

const with_works = true;
const response = (await projectService.getAll(
PROJECT_RETURN_TYPE.LIST_TYPE,
with_works
)) as { data: ListType[] };

const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setSearchText(event.target.value);
debouncedSetSearchOptions(event.target.value);
};
const projectNames = response.data.map((project) => project.name);
setOptions(projectNames);
setLoading(false);
} catch (error) {
console.log(error);
}
};

fetchProjectNames();
}, []);

return (
<TextField
name="searchText"
placeholder="Search for a Project"
variant="outlined"
<Autocomplete
options={searchText.length >= SEARCH_TEXT_THRESHOLD ? options : []}
onInputChange={(_, newValue) => {
setSearchText(newValue ?? "");
}}
onChange={(_, newValue) => {
handleSearchOptions(newValue ?? "");
}}
renderInput={(params) => (
<TextField
{...params}
variant="outlined"
placeholder="Search for a Project"
InputProps={{
...params.InputProps,
startAdornment: (
<InputAdornment position="start">
<Box
component="img"
src={SearchIcon}
alt="Search"
width="16px"
/>
</InputAdornment>
),
}}
/>
)}
fullWidth
value={searchText}
onChange={handleChange}
clearOnBlur
noOptionsText=""
renderOption={(props, option, state) => (
<li {...props}>{highlightText(option, state.inputValue)}</li>
)}
disabled={loading}
/>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ const Filters = () => {
justifyContent={"space-between"}
direction="row"
>
<Grid item>
<Grid item xs={3}>
<NameFilter />
</Grid>
<Grid item xs container spacing={2} justifyContent="flex-end">
Expand Down
3 changes: 3 additions & 0 deletions epictrack-web/src/services/projectService/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export const PROJECT_RETURN_TYPE = {
LIST_TYPE: "LIST_TYPE",
};
7 changes: 5 additions & 2 deletions epictrack-web/src/services/projectService/projectService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,11 @@ import { MasterBase } from "../../models/type";
import { ListType } from "../../models/code";

class ProjectService implements ServiceBase {
async getAll(return_type?: string) {
return await http.GetRequest(Endpoints.Projects.PROJECTS, { return_type });
async getAll(return_type?: string, with_works?: boolean) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

with_works? is includeWorks better?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i am not sure you want project which has a work or you wanna include a work ..so plz feel to disregard this

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

return_type ? is it the project type?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I need only the project with works, I don't need to include the works info.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

return_type is not my addition, it's already there. It decides what schema to use

return await http.GetRequest(Endpoints.Projects.PROJECTS, {
return_type,
with_works,
});
}

async getById(id: string) {
Expand Down
15 changes: 15 additions & 0 deletions epictrack-web/src/utils/MatchingTextHighlight.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
export const highlightText = (text: string, query: string) => {
const index = text.toLowerCase().indexOf(query.toLowerCase());
if (index !== -1) {
return (
<>
{text.substring(0, index)}
<span style={{ backgroundColor: "rgb(255, 203, 127)" }}>
{text.substring(index, index + query.length)}
</span>
{text.substring(index + query.length)}
</>
);
}
return text;
};
Loading