diff --git a/epictrack-api/src/api/models/project.py b/epictrack-api/src/api/models/project.py index 76c6303fa..2e3de362c 100644 --- a/epictrack-api/src/api/models/project.py +++ b/epictrack-api/src/api/models/project.py @@ -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): diff --git a/epictrack-api/src/api/models/work.py b/epictrack-api/src/api/models/work.py index bcad609d4..10d5a22bb 100644 --- a/epictrack-api/src/api/models/work.py +++ b/epictrack-api/src/api/models/work.py @@ -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 diff --git a/epictrack-api/src/api/resources/project.py b/epictrack-api/src/api/resources/project.py index 8da95db3e..5315c4323 100644 --- a/epictrack-api/src/api/resources/project.py +++ b/epictrack-api/src/api/resources/project.py @@ -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) diff --git a/epictrack-api/src/api/services/project.py b/epictrack-api/src/api/services/project.py index 4b47c6162..d594f8fbd 100644 --- a/epictrack-api/src/api/services/project.py +++ b/epictrack-api/src/api/services/project.py @@ -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.""" diff --git a/epictrack-web/src/components/myWorkplans/Filters/NameFilter.tsx b/epictrack-web/src/components/myWorkplans/Filters/NameFilter.tsx index 15a6ea2ff..c14061b74 100644 --- a/epictrack-web/src/components/myWorkplans/Filters/NameFilter.tsx +++ b/epictrack-web/src/components/myWorkplans/Filters/NameFilter.tsx @@ -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(""); + const [loading, setLoading] = useState(true); + const [options, setOptions] = useState([]); + const [searchText, setSearchText] = useState(""); + + const { setSearchOptions } = useContext(MyWorkplansContext); - const { setSearchOptions } = React.useContext(MyWorkplansContext); const handleSearchOptions = (searchText: string) => { setSearchOptions( (prev: WorkPlanSearchOptions) => @@ -20,25 +28,65 @@ 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 + try { + const with_works = true; + const response = (await projectService.getAll( + PROJECT_RETURN_TYPE.LIST_TYPE, + with_works + )) as { data: ListType[] }; - const handleChange = (event: React.ChangeEvent) => { - 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 ( - = SEARCH_TEXT_THRESHOLD ? options : []} + onInputChange={(_, newValue) => { + setSearchText(newValue ?? ""); + }} + onChange={(_, newValue) => { + handleSearchOptions(newValue ?? ""); + }} + renderInput={(params) => ( + + + + ), + }} + /> + )} fullWidth - value={searchText} - onChange={handleChange} + clearOnBlur + noOptionsText="" + renderOption={(props, option, state) => ( +
  • {highlightText(option, state.inputValue)}
  • + )} + disabled={loading} /> ); }; diff --git a/epictrack-web/src/components/myWorkplans/Filters/index.tsx b/epictrack-web/src/components/myWorkplans/Filters/index.tsx index aaf5b2fd8..61f394997 100644 --- a/epictrack-web/src/components/myWorkplans/Filters/index.tsx +++ b/epictrack-web/src/components/myWorkplans/Filters/index.tsx @@ -15,7 +15,7 @@ const Filters = () => { justifyContent={"space-between"} direction="row" > - + diff --git a/epictrack-web/src/components/shared/filterSelect/FilterSelect.tsx b/epictrack-web/src/components/shared/filterSelect/FilterSelect.tsx index 029cdc964..cb429114e 100644 --- a/epictrack-web/src/components/shared/filterSelect/FilterSelect.tsx +++ b/epictrack-web/src/components/shared/filterSelect/FilterSelect.tsx @@ -9,21 +9,8 @@ import SingleValue from "./components/SingleValueContainer"; import DropdownIndicator from "./components/DropDownIndicator"; import { MET_Header_Font_Weight_Regular } from "../../../styles/constants"; -// const useStyle = makeStyles({ -// infoSelect: { -// pointerEvents: "auto", -// borderRadius: "4px", -// "& > div:first-child": { -// paddingRight: 0, -// }, -// "&:hover": { -// backgroundColor: Palette.neutral.bg.main, -// }, -// }, -// }); - +const INPUT_SIZE = "0.875rem"; const FilterSelect = (props: SelectProps) => { - // const classes = useStyle(); const { name, isMulti, defaultValue } = props; const standardDefault = isMulti ? [] : ""; const [options, setOptions] = React.useState([]); @@ -92,7 +79,6 @@ const FilterSelect = (props: SelectProps) => { }; const applyFilters = () => { - // header.column.setFilterValue(selectedOptions); if (props.filterAppliedCallback) { props.filterAppliedCallback(selectedOptions); } @@ -117,7 +103,6 @@ const FilterSelect = (props: SelectProps) => { const clearFilters = () => { setSelectedOptions([]); setSelectValue(isMulti ? [] : ""); - // header.column.setFilterValue(isMulti ? [] : ""); if (props.filterClearedCallback) { props.filterClearedCallback(isMulti ? [] : ""); } @@ -139,6 +124,16 @@ const FilterSelect = (props: SelectProps) => { setOptions(filterOptions); }, [props.options]); + const isSearchable = () => { + if (props.isSearchable !== undefined) return props.isSearchable; + + if (selectValue instanceof Array) { + return selectValue.length === 0; + } + + return !selectValue; + }; + return (