diff --git a/src/App.tsx b/src/App.tsx
index c081066..b636045 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -2,6 +2,7 @@ import React from "react";
import "./App.css";
import ProjectList from "./components/ProjectList";
import FilterBar from "./components/FilterBar";
+import SearchBar from "./components/SearchBar";
const App: React.FC = () => {
return (
@@ -10,6 +11,7 @@ const App: React.FC = () => {
glTF Project Explorer
diff --git a/src/components/SearchBar.css b/src/components/SearchBar.css
new file mode 100644
index 0000000..a114ff8
--- /dev/null
+++ b/src/components/SearchBar.css
@@ -0,0 +1,24 @@
+.search-bar {
+ background-color: #fcfcfc;
+ border-radius: 3px;
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.25);
+ margin: 1rem;
+ padding: 1rem;
+ display: flex;
+ flex-direction: column;
+ align-items: stretch;
+}
+
+.search-bar h1 {
+ font-size: 1.5rem;
+ margin: 0;
+ padding: 0;
+}
+
+.search-bar input {
+ margin: 0.5rem 0 0 0;
+ padding: 0.25rem 0.5rem;
+ height: 2rem;
+ line-height: 1.5rem;
+ font-size: 1rem;
+}
diff --git a/src/components/SearchBar.tsx b/src/components/SearchBar.tsx
new file mode 100644
index 0000000..5990dea
--- /dev/null
+++ b/src/components/SearchBar.tsx
@@ -0,0 +1,52 @@
+import React, { useCallback } from "react";
+import { connect } from "react-redux";
+import { IAppState } from "../interfaces/IAppState";
+import { updateTitleSubstringFilter } from "../store/filters/Actions";
+import "./SearchBar.css";
+
+export interface ISearchBarProps {
+ titleSubstring: string;
+ updateTitleSubstringFilter: typeof updateTitleSubstringFilter;
+}
+
+const SearchBar: React.FC = props => {
+ const { titleSubstring, updateTitleSubstringFilter } = props;
+
+ const handleSearch = useCallback(
+ (event: React.ChangeEvent) => {
+ const newTitleSubstring = event.target.value;
+ updateTitleSubstringFilter(newTitleSubstring);
+ },
+ [updateTitleSubstringFilter]
+ );
+
+ return (
+
+
Search by Title
+
+
+ );
+};
+
+function mapStateToProps(state: IAppState) {
+ const {
+ filters: { titleSubstring }
+ } = state;
+
+ return {
+ titleSubstring
+ };
+}
+
+const mapDispatchToProps = {
+ updateTitleSubstringFilter
+};
+
+export default connect(
+ mapStateToProps,
+ mapDispatchToProps
+)(SearchBar);
diff --git a/src/interfaces/IAppState.ts b/src/interfaces/IAppState.ts
index 1f8aa6c..6fa61cd 100644
--- a/src/interfaces/IAppState.ts
+++ b/src/interfaces/IAppState.ts
@@ -19,6 +19,7 @@ export interface IFiltersState {
types: IFilter[];
languages: IFilter[];
licenses: IFilter[];
+ titleSubstring: string;
selected: Set;
}
diff --git a/src/store/filters/Actions.ts b/src/store/filters/Actions.ts
index 6fe6078..a9ba310 100644
--- a/src/store/filters/Actions.ts
+++ b/src/store/filters/Actions.ts
@@ -1,6 +1,7 @@
import {
IUpdateFiltersAction,
- IUpdateSelectedFiltersAction
+ IUpdateSelectedFiltersAction,
+ IUpdateTitleSubstringFilterAction
} from "./Interfaces";
import { FilterActionTypes } from "./Types";
import { IFilter } from "../../interfaces/IFilter";
@@ -9,14 +10,16 @@ export function updateFilters(
tasks: IFilter[],
types: IFilter[],
licenses: IFilter[],
- languages: IFilter[]
+ languages: IFilter[],
+ titleSubstring: string
): IUpdateFiltersAction {
return {
type: FilterActionTypes.UPDATE_FILTERS,
tasks,
types,
licenses,
- languages
+ languages,
+ titleSubstring
};
}
@@ -30,3 +33,12 @@ export function updateSelectedFilters(
selected
};
}
+
+export function updateTitleSubstringFilter(
+ titleSubstring: string
+): IUpdateTitleSubstringFilterAction {
+ return {
+ type: FilterActionTypes.UPDATE_TITLE_SUBSTRING_FILTER,
+ titleSubstring
+ };
+}
diff --git a/src/store/filters/Interfaces.ts b/src/store/filters/Interfaces.ts
index 8df0e54..36904a7 100644
--- a/src/store/filters/Interfaces.ts
+++ b/src/store/filters/Interfaces.ts
@@ -3,7 +3,8 @@ import { FilterActionTypes } from "./Types";
export type FiltersActions =
| IUpdateFiltersAction
- | IUpdateSelectedFiltersAction;
+ | IUpdateSelectedFiltersAction
+ | IUpdateTitleSubstringFilterAction;
export interface IUpdateFiltersAction {
readonly type: FilterActionTypes.UPDATE_FILTERS;
@@ -11,9 +12,15 @@ export interface IUpdateFiltersAction {
readonly types: IFilter[];
readonly languages: IFilter[];
readonly licenses: IFilter[];
+ readonly titleSubstring: string;
}
export interface IUpdateSelectedFiltersAction {
readonly type: FilterActionTypes.UPDATE_SELECTED_FILTERS;
readonly selected: Set;
}
+
+export interface IUpdateTitleSubstringFilterAction {
+ readonly type: FilterActionTypes.UPDATE_TITLE_SUBSTRING_FILTER;
+ readonly titleSubstring: string;
+}
diff --git a/src/store/filters/Reducers.ts b/src/store/filters/Reducers.ts
index ae76968..9565183 100644
--- a/src/store/filters/Reducers.ts
+++ b/src/store/filters/Reducers.ts
@@ -8,6 +8,7 @@ export function filters(
types: [],
licenses: [],
languages: [],
+ titleSubstring: "",
selected: new Set()
},
action: FiltersActions
@@ -19,13 +20,19 @@ export function filters(
tasks: action.tasks,
types: action.types,
licenses: action.licenses,
- languages: action.languages
+ languages: action.languages,
+ titleSubstring: action.titleSubstring
};
case FilterActionTypes.UPDATE_SELECTED_FILTERS:
return {
...state,
selected: action.selected
};
+ case FilterActionTypes.UPDATE_TITLE_SUBSTRING_FILTER:
+ return {
+ ...state,
+ titleSubstring: action.titleSubstring
+ };
default:
return state;
}
diff --git a/src/store/filters/Sagas.ts b/src/store/filters/Sagas.ts
index 99b2b54..b56aca0 100644
--- a/src/store/filters/Sagas.ts
+++ b/src/store/filters/Sagas.ts
@@ -5,6 +5,8 @@ import { ProjectsActionTypes } from "../projects/Types";
import * as projectSelectors from "../projects/Selectors";
import * as actions from "./Actions";
+const DEFAULT_FULL_TEXT_TITLE_VALUE = "";
+
export function calculateTaskFilters(projects: IProjectInfo[]) {
const tasks = [
...new Set(projects.flatMap(p => p.task).filter(x => x))
@@ -41,7 +43,15 @@ export function* calculateFilters() {
call(calculateLicenseFilters, projects),
call(calculateLanguageFilters, projects)
]);
- yield put(actions.updateFilters(tasks, types, licenses, languages));
+ yield put(
+ actions.updateFilters(
+ tasks,
+ types,
+ licenses,
+ languages,
+ DEFAULT_FULL_TEXT_TITLE_VALUE
+ )
+ );
yield put(actions.updateSelectedFilters(new Set()));
}
diff --git a/src/store/filters/Selectors.ts b/src/store/filters/Selectors.ts
index 199a864..bfc1bd0 100644
--- a/src/store/filters/Selectors.ts
+++ b/src/store/filters/Selectors.ts
@@ -7,3 +7,8 @@ export const getSelectedFilters = createSelector(
getFilters,
filters => filters.selected
);
+
+export const getTitleSubstring = createSelector(
+ getFilters,
+ filters => filters.titleSubstring
+);
diff --git a/src/store/filters/Types.ts b/src/store/filters/Types.ts
index 22df01f..01b4529 100644
--- a/src/store/filters/Types.ts
+++ b/src/store/filters/Types.ts
@@ -1,5 +1,6 @@
export enum FilterActionTypes {
UPDATE_FILTERS = "UPDATE_FILTERS",
UPDATE_SELECTED_FILTERS = "UPDATE_SELECTED_FILTERS",
+ UPDATE_TITLE_SUBSTRING_FILTER = "UPDATE_TITLE_SUBSTRING_FILTER",
PERFORM_SEARCH = "PERFORM_SEARCH"
}
diff --git a/src/store/results/Sagas.ts b/src/store/results/Sagas.ts
index c033e47..e0e37de 100644
--- a/src/store/results/Sagas.ts
+++ b/src/store/results/Sagas.ts
@@ -1,4 +1,4 @@
-import { takeEvery, all, put, select } from "redux-saga/effects";
+import { takeEvery, all, put, select, debounce } from "redux-saga/effects";
import * as projectSelectors from "../projects/Selectors";
import * as filterSelectors from "../filters/Selectors";
import { IProjectInfo } from "../../interfaces/IProjectInfo";
@@ -10,17 +10,12 @@ interface IGroupedFilters {
[dimension: string]: IFilter[];
}
-export function* applyFilters() {
- const [projects, selectedFilters]: [IProjectInfo[], Set] = yield all(
- [
- select(projectSelectors.getProjects),
- select(filterSelectors.getSelectedFilters)
- ]
- );
-
+function applyTagFilters(
+ projects: IProjectInfo[],
+ selectedFilters: Set
+): IProjectInfo[] {
if (selectedFilters.size < 1) {
- yield put(actions.storeResults(projects));
- return;
+ return projects;
}
const dimensions = Object.values(FilterDimension);
@@ -37,7 +32,7 @@ export function* applyFilters() {
{}
);
- const results = projects.filter(project => {
+ return projects.filter(project => {
let match = false;
for (const dimension of dimensions) {
@@ -59,10 +54,41 @@ export function* applyFilters() {
return match;
});
+}
+
+function applyTitleSearchFilter(
+ projects: IProjectInfo[],
+ titleSubstring?: string
+): IProjectInfo[] {
+ if (!titleSubstring) {
+ return projects;
+ }
+
+ return projects.filter(p => p.name.includes(titleSubstring));
+}
+
+export function* applyFilters() {
+ const [projects, selectedFilters, titleSubstring]: [
+ IProjectInfo[],
+ Set,
+ string
+ ] = yield all([
+ select(projectSelectors.getProjects),
+ select(filterSelectors.getSelectedFilters),
+ select(filterSelectors.getTitleSubstring)
+ ]);
+
+ const interimResults = applyTagFilters(projects, selectedFilters);
+ const results = applyTitleSearchFilter(interimResults, titleSubstring);
yield put(actions.storeResults(results));
}
export function* watchForResultUpdates() {
yield takeEvery(FilterActionTypes.UPDATE_SELECTED_FILTERS, applyFilters);
+ yield debounce(
+ 500,
+ FilterActionTypes.UPDATE_TITLE_SUBSTRING_FILTER,
+ applyFilters
+ );
}