Skip to content
Snippets Groups Projects
Commit d021037b authored by esikkala's avatar esikkala
Browse files

Building text search perspective

parent 2a428082
No related branches found
No related tags found
No related merge requests found
Showing with 435 additions and 141 deletions
export const FETCH_PAGINATED_RESULTS = 'FETCH_PAGINATED_RESULTS';
export const FETCH_PAGINATED_RESULTS_FAILED = 'FETCH_PAGINATED_RESULTS_FAILED';
export const FETCH_RESULTS = 'FETCH_RESULTS';
export const FETCH_RESULTS_CLIENT_SIDE = 'FETCH_RESULTS_CLIENT_SIDE';
export const FETCH_RESULTS_FAILED = 'FETCH_RESULTS_FAILED';
export const UPDATE_PAGINATED_RESULTS = 'UPDATE_PAGINATED_RESULTS';
export const UPDATE_RESULTS = 'UPDATE_RESULTS';
export const CLEAR_RESULTS = 'CLEAR_RESULTS';
export const SORT_RESULTS = 'SORT_RESULTS';
export const UPDATE_PAGE = 'UPDATE_PAGE';
export const FETCH_BY_URI = 'FETCH_BY_URI';
......@@ -13,6 +15,7 @@ export const FETCH_FACET = 'FETCH_FACET';
export const FETCH_FACET_FAILED = 'FETCH_FACET_FAILED';
export const UPDATE_FACET_VALUES = 'UPDATE_FACET_VALUES';
export const UPDATE_FACET_OPTION = 'UPDATE_FACET_OPTION';
export const UPDATE_CLIENT_SIDE_FILTER = 'UPDATE_CLIENT_SIDE_FILTER';
export const OPEN_MARKER_POPUP = 'OPEN_MARKER_POPUP';
export const SHOW_ERROR = 'SHOW_ERROR';
......@@ -28,6 +31,10 @@ export const fetchResults = ({ resultClass, facetClass, sortBy, variant }) => ({
type: FETCH_RESULTS,
resultClass, facetClass, sortBy, variant
});
export const fetchResultsClientSide = ({ jenaIndex, query }) => ({
type: FETCH_RESULTS_CLIENT_SIDE,
jenaIndex, query
});
export const fetchResultsFailed = (resultClass, error, message) => ({
type: FETCH_RESULTS_FAILED,
resultClass, error, message
......@@ -44,6 +51,10 @@ export const sortResults = (resultClass, sortBy) => ({
type: SORT_RESULTS,
resultClass, sortBy
});
export const clearResults = resultClass => ({
type: CLEAR_RESULTS,
resultClass
});
export const updatePage = (resultClass, page) => ({
type: UPDATE_PAGE,
resultClass, page
......@@ -76,6 +87,10 @@ export const updateFacetOption = ({ facetClass, facetID, option, value }) => ({
type: UPDATE_FACET_OPTION,
facetClass, facetID, option, value
});
export const updateClientSideFilter = filterObj => ({
type: UPDATE_CLIENT_SIDE_FILTER,
filterObj
});
export const openMarkerPopup = uri => ({
type: OPEN_MARKER_POPUP,
uri
......
import React from 'react';
import PropTypes from 'prop-types';
import MaterialTable from 'material-table';
class ResultTable2 extends React.Component {
render() {
return (
<div style={{ maxWidth: '100%' }}>
<MaterialTable
columns={[
{ title: 'Adı', field: 'name' },
{ title: 'Soyadı', field: 'surname' },
{ title: 'Doğum Yılı', field: 'birthYear', type: 'numeric' },
{ title: 'Doğum Yeri', field: 'birthCity', lookup: { 34: 'İstanbul', 63: 'Şanlıurfa' } }
]}
data={[{ name: 'Mehmet', surname: 'Baran', birthYear: 1987, birthCity: 63 }]}
title="Demo Title"
/>
</div>
);
}
}
ResultTable2.propTypes = {
data: PropTypes.object,
};
export default ResultTable2;
import React from 'react';
import PropTypes from 'prop-types';
import { withStyles } from '@material-ui/core/styles';
import IconButton from '@material-ui/core/IconButton';
import SearchIcon from '@material-ui/icons/Search';
import Input from '@material-ui/core/Input';
import InputLabel from '@material-ui/core/InputLabel';
import InputAdornment from '@material-ui/core/InputAdornment';
import FormControl from '@material-ui/core/FormControl';
import CircularProgress from '@material-ui/core/CircularProgress';
const styles = theme => ({
textSearch: {
margin: theme.spacing.unit,
},
});
class SearchField extends React.Component {
state = {
value: '',
};
componentDidUpdate = prevProps => {
if (prevProps.search.query != this.props.search.query) {
this.setState({
value: this.props.search.query
});
}
}
handleChange = (event) => {
this.setState({ value: event.target.value });
};
handleMouseDown = (event) => {
event.preventDefault();
};
handleOnKeyDown = (event) => {
if (event.key === 'Enter' && this.hasDatasets() && this.hasValidQuery()) {
this.props.clearResults();
this.props.updateQuery(this.state.value);
this.props.fetchResults('text', this.state.value);
}
};
handleClick = () => {
if (this.hasDatasets() && this.hasValidQuery()) {
this.props.clearResults();
this.props.updateQuery(this.state.value);
this.props.fetchResults('text', this.state.value);
}
};
hasDatasets = () => {
let hasDs = false;
Object.values(this.props.datasets).forEach(value => {
if (value.selected) {
hasDs = true;
}
});
return hasDs;
}
hasValidQuery = () => {
return this.state.value.length > 2;
}
render() {
const { classes, strings } = this.props;
let searchButton = null;
if (this.props.search.textResultsFetching) {
searchButton = (
<IconButton
aria-label="Search places"
>
<CircularProgress size={24} />
</IconButton>
);
} else {
searchButton = (
<IconButton
aria-label="Search"
onClick={this.handleClick}
onMouseDown={this.handleMouseDown}
>
<SearchIcon />
</IconButton>
);
}
return (
<div className={classes.root}>
<FormControl className={classes.textSearch}>
<InputLabel htmlFor="adornment-search">{strings.searchPlaceNames}</InputLabel>
<Input
id="adornment-search"
type='text'
value={this.state.value}
onChange={this.handleChange}
onKeyDown={this.handleOnKeyDown}
endAdornment={
<InputAdornment position="end">
{searchButton}
</InputAdornment>
}
/>
</FormControl>
</div>
);
}
}
SearchField.propTypes = {
classes: PropTypes.object.isRequired,
search: PropTypes.object.isRequired,
fetchResults: PropTypes.func.isRequired,
clearResults: PropTypes.func.isRequired,
updateQuery: PropTypes.func.isRequired,
datasets: PropTypes.object.isRequired,
strings: PropTypes.object.isRequired
};
export default withStyles(styles)(SearchField);
......@@ -4,15 +4,14 @@ import AppBar from '@material-ui/core/AppBar';
import Toolbar from '@material-ui/core/Toolbar';
import IconButton from '@material-ui/core/IconButton';
import Typography from '@material-ui/core/Typography';
import InputBase from '@material-ui/core/InputBase';
import MenuItem from '@material-ui/core/MenuItem';
import Menu from '@material-ui/core/Menu';
import { fade } from '@material-ui/core/styles/colorManipulator';
import { withStyles } from '@material-ui/core/styles';
import SearchIcon from '@material-ui/icons/Search';
import MoreIcon from '@material-ui/icons/MoreVert';
import Button from '@material-ui/core/Button';
import { Link, NavLink } from 'react-router-dom';
import TopBarSearchField from './TopBarSearchField';
const styles = theme => ({
root: {
......@@ -154,19 +153,10 @@ class TopBar extends React.Component {
MMM
</Typography>
</Button>
<div className={classes.search}>
<div className={classes.searchIcon}>
<SearchIcon />
</div>
<InputBase
disabled
placeholder="Search…"
classes={{
root: classes.inputRoot,
input: classes.inputInput,
}}
/>
</div>
<TopBarSearchField
fetchResultsClientSide={this.props.fetchResultsClientSide}
clearResults={this.props.clearResults}
/>
<div className={classes.grow} />
<div className={classes.sectionDesktop}>
{perspectives.map(perspective =>
......@@ -197,6 +187,8 @@ class TopBar extends React.Component {
TopBar.propTypes = {
classes: PropTypes.object.isRequired,
fetchResultsClientSide: PropTypes.func.isRequired,
clearResults: PropTypes.func.isRequired,
};
export default withStyles(styles)(TopBar);
import React from 'react';
import PropTypes from 'prop-types';
import { withStyles } from '@material-ui/core/styles';
import { fade } from '@material-ui/core/styles/colorManipulator';
import SearchIcon from '@material-ui/icons/Search';
import InputBase from '@material-ui/core/InputBase';
//import CircularProgress from '@material-ui/core/CircularProgress';
const styles = theme => ({
search: {
position: 'relative',
borderRadius: theme.shape.borderRadius,
backgroundColor: fade(theme.palette.common.white, 0.15),
'&:hover': {
backgroundColor: fade(theme.palette.common.white, 0.25),
},
marginRight: theme.spacing.unit * 2,
marginLeft: 0,
width: '100%',
[theme.breakpoints.up('sm')]: {
marginLeft: theme.spacing.unit * 3,
width: 'auto',
},
},
searchIcon: {
width: theme.spacing.unit * 9,
height: '100%',
position: 'absolute',
pointerEvents: 'none',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
},
inputRoot: {
color: 'inherit',
width: '100%',
},
inputInput: {
paddingTop: theme.spacing.unit,
paddingRight: theme.spacing.unit,
paddingBottom: theme.spacing.unit,
paddingLeft: theme.spacing.unit * 10,
transition: theme.transitions.create('width'),
width: '100%',
[theme.breakpoints.up('md')]: {
width: 200,
},
},
});
class TopBarSearchField extends React.Component {
state = {
value: '',
};
handleChange = (event) => {
this.setState({ value: event.target.value });
};
handleMouseDown = (event) => {
event.preventDefault();
};
handleOnKeyDown = (event) => {
if (event.key === 'Enter' && this.hasValidQuery()) {
this.props.clearResults();
this.props.fetchResultsClientSide({
jenaIndex: 'text',
query: this.state.value
});
}
};
handleClick = () => {
if (this.hasValidQuery()) {
this.props.clearResults();
this.props.fetchResultsClientSide({
jenaIndex: 'text',
query: this.state.value
});
}
};
hasValidQuery = () => {
return this.state.value.length > 2;
}
render() {
const { classes } = this.props;
return (
<div className={classes.search}>
<div className={classes.searchIcon}>
<SearchIcon />
</div>
<InputBase
placeholder="Search everything"
classes={{
root: classes.inputRoot,
input: classes.inputInput,
}}
onChange={this.handleChange}
onKeyDown={this.handleOnKeyDown}
/>
</div>
);
}
}
TopBarSearchField.propTypes = {
classes: PropTypes.object.isRequired,
fetchResultsClientSide: PropTypes.func.isRequired,
clearResults: PropTypes.func.isRequired,
};
export default withStyles(styles)(TopBarSearchField);
import React from 'react';
import PropTypes from 'prop-types';
import { Route, Redirect } from 'react-router-dom';
import PerspectiveTabs from '../main_layout/PerspectiveTabs';
//import ResultTable2 from '../facet_results/ResultTable2';
import Typography from '@material-ui/core/Typography';
let All = props => {
const perspectiveUrl = '/all';
return (
<React.Fragment>
<PerspectiveTabs
routeProps={props.routeProps}
tabs={{
[`${perspectiveUrl}/table`]: {
label: 'table',
value: 0,
icon: 'CalendarViewDay',
},
[`${perspectiveUrl}/map`]: {
label: 'map',
value: 1,
icon: 'AddLocation',
},
}}
/>
<Route
exact path={perspectiveUrl}
render={() => <Redirect to={`${perspectiveUrl}/table`} />}
/>
<Route
path={`${perspectiveUrl}/table`}
render={() =>
<Typography>Test</Typography>
//<ResultTable2 />
}
/>
</React.Fragment>
);
};
All.propTypes = {
results: PropTypes.object,
updatePage: PropTypes.func,
sortResults: PropTypes.func,
routeProps: PropTypes.object.isRequired,
};
export default All;
......@@ -18,9 +18,12 @@ import Works from '../components/perspectives/Works';
import Places from '../components//perspectives/Places';
import People from '../components//perspectives/People';
import Organizations from '../components/perspectives/Organizations';
import All from '../components/perspectives/All';
import {
fetchPaginatedResults,
fetchResults,
fetchResultsClientSide,
clearResults,
fetchByURI,
fetchFacet,
sortResults,
......@@ -82,7 +85,11 @@ let SemanticPortal = (props) => {
<div className={classes.appFrame}>
<Message error={error} />
<React.Fragment>
<TopBar />
<TopBar
search={props.clientSideFacetedSearch}
fetchResultsClientSide={props.fetchResultsClientSide}
clearResults={props.clearResults}
/>
<Grid container spacing={8} className={classes.mainContainer}>
<Route exact path="/" component={Main} />
<Route
......@@ -243,6 +250,24 @@ let SemanticPortal = (props) => {
</React.Fragment>
}
/>
<Route
path="/all"
render={routeProps =>
<React.Fragment>
<Grid item sm={12} md={3} className={classes.facetBarContainer}>
</Grid>
<Grid item sm={12} md={9} className={classes.resultsContainer}>
<Paper className={classes.resultsContainerPaper}>
<All
results={props.clientSideFacetedSearch.results}
routeProps={routeProps}
/>
</Paper>
</Grid>
</React.Fragment>
}
/>
</Grid>
</React.Fragment>
<Footer />
......@@ -266,6 +291,7 @@ const mapStateToProps = state => {
organizationsFacets: state.organizationsFacets,
places: state.places,
placesFacets: state.placesFacets,
clientSideFacetedSearch: state.clientSideFacetedSearch,
error: state.error
//browser: state.browser,
};
......@@ -274,9 +300,11 @@ const mapStateToProps = state => {
const mapDispatchToProps = ({
fetchPaginatedResults,
fetchResults,
fetchResultsClientSide,
fetchByURI,
fetchFacet,
sortResults,
clearResults,
updateFacetOption,
updatePage,
showError
......@@ -297,10 +325,13 @@ SemanticPortal.propTypes = {
organizationsFacets: PropTypes.object.isRequired,
places: PropTypes.object.isRequired,
placesFacets: PropTypes.object.isRequired,
clientSideFacetedSearch: PropTypes.object.isRequired,
fetchResults: PropTypes.func.isRequired,
fetchResultsClientSide: PropTypes.func.isRequired,
fetchPaginatedResults: PropTypes.func.isRequired,
fetchByURI: PropTypes.func.isRequired,
sortResults: PropTypes.func.isRequired,
clearResults: PropTypes.func.isRequired,
updatePage: PropTypes.func.isRequired,
updateFacetOption: PropTypes.func.isRequired,
fetchFacet: PropTypes.func.isRequired,
......
import { of } from 'rxjs';
import { ajax } from 'rxjs/ajax';
import { mergeMap, map, withLatestFrom, catchError } from 'rxjs/operators';
import {
mergeMap,
switchMap,
map,
withLatestFrom,
debounceTime,
catchError
} from 'rxjs/operators';
import { combineEpics, ofType } from 'redux-observable';
import querystring from 'querystring';
import { has } from 'lodash';
......@@ -8,6 +15,7 @@ import {
FETCH_PAGINATED_RESULTS,
FETCH_PAGINATED_RESULTS_FAILED,
FETCH_RESULTS,
FETCH_RESULTS_CLIENT_SIDE,
FETCH_RESULTS_FAILED,
FETCH_BY_URI,
FETCH_BY_URI_FAILED,
......@@ -89,6 +97,34 @@ const fetchResultsEpic = (action$, state$) => action$.pipe(
})
);
const fetchResultsClientSideEpic = (action$, state$) => action$.pipe(
ofType(FETCH_RESULTS_CLIENT_SIDE),
withLatestFrom(state$),
debounceTime(500),
switchMap(([action, state]) => {
const searchUrl = apiUrl + 'search';
let requestUrl = '';
if (action.jenaIndex === 'text') {
requestUrl = `${searchUrl}?q=${action.query}`;
} else if (action.jenaIndex === 'spatial') {
const { latMin, longMin, latMax, longMax } = state.map;
requestUrl = `${searchUrl}?latMin=${latMin}&longMin=${longMin}&latMax=${latMax}&longMax=${longMax}`;
}
return ajax.getJSON(requestUrl).pipe(
map(response => updateResults({ resultClass: 'all', data: response })),
catchError(error => of({
type: FETCH_RESULTS_FAILED,
resultClass: 'all',
error: error,
message: {
text: backendErrorText,
title: 'Error'
}
}))
);
})
);
const fetchByURIEpic = (action$, state$) => action$.pipe(
ofType(FETCH_BY_URI),
withLatestFrom(state$),
......@@ -214,6 +250,7 @@ const boundsToValues = bounds => {
const rootEpic = combineEpics(
fetchPaginatedResultsEpic,
fetchResultsEpic,
fetchResultsClientSideEpic,
fetchByURIEpic,
fetchFacetEpic,
);
......
import {
FETCH_RESULTS_CLIENT_SIDE,
UPDATE_RESULTS,
CLEAR_RESULTS,
UPDATE_CLIENT_SIDE_FILTER,
SORT_RESULTS
} from '../actions';
export const INITIAL_STATE = {
results: null,
latestFilter: {
id: '',
},
latestFilterValues: [],
resultsFilter: {
prefLabel: new Set(),
type: new Set()
},
sortBy: 'prefLabel',
sortDirection: 'asc',
//groupBy: 'broaderTypeLabel',
//groupByLabel: 'Paikanlaji',
textResultsFetching: false,
spatialResultsFetching: false,
};
const clientSideFacetedSearch = (state = INITIAL_STATE, action) => {
if (action.resultClass === 'all') {
switch (action.type) {
case FETCH_RESULTS_CLIENT_SIDE:
return {
...state,
//[`${action.jenaIndex}ResultsFetching`]: true
};
case UPDATE_RESULTS:
console.log(action.data)
return {
...state,
results: action.data,
//[`${action.jenaIndex}ResultsFetching`]: false
};
case CLEAR_RESULTS:
return {
...state,
results: null,
fetchingResults: false,
query: '',
resultsFilter: {
prefLabel: new Set(),
type: new Set()
},
};
case UPDATE_CLIENT_SIDE_FILTER:
return updateResultsFilter(state, action);
case SORT_RESULTS:
return {
...state,
sortBy: action.options.sortBy,
sortDirection: action.options.sortDirection,
};
default:
return state;
}
} else return state;
};
const updateResultsFilter = (state, action) => {
const { property, value, latestValues } = action.filterObj;
let nSet = state.resultsFilter[property];
if (nSet.has(value)) {
nSet.delete(value);
} else {
nSet.add(value);
}
const newFilter = {
...state.resultsFilter,
[property]: nSet
};
return {
...state,
resultsFilter: newFilter,
latestFilter: {
id: property,
},
latestFilterValues: latestValues
};
};
export default clientSideFacetedSearch;
......@@ -12,6 +12,7 @@ import worksFacets from './worksFacets';
import peopleFacets from './peopleFacets';
import organizationsFacets from './organizationsFacets';
import placesFacets from './placesFacets';
import clientSideFacetedSearch from './clientSideFacetedSearch';
const reducer = combineReducers({
manuscripts,
......@@ -24,6 +25,7 @@ const reducer = combineReducers({
organizationsFacets,
places,
placesFacets,
clientSideFacetedSearch,
error,
toastr: toastrReducer,
browser: createResponsiveStateReducer({
......
import express from 'express';
import path from 'path';
import bodyParser from 'body-parser';
import { has } from 'lodash';
import { getPaginatedResults, getAllResults, getByURI } from './sparql/FacetResults';
import { getFacet } from './sparql/FacetValues';
import { queryJenaIndex } from './sparql/JenaQuery';
const DEFAULT_PORT = 3001;
const app = express();
app.set('port', process.env.PORT || DEFAULT_PORT);
......@@ -91,6 +93,35 @@ app.get(`${apiPath}/:facetClass/facet/:id`, async (req, res, next) => {
}
});
app.get(`${apiPath}/search`, async (req, res, next) => {
let queryTerm = '';
let latMin = 0;
let longMin = 0;
let latMax = 0;
let longMax = 0;
if (has(req.query, 'q')) {
queryTerm = req.query.q;
}
if (has(req.query, 'latMin')) {
latMin = req.query.latMin;
longMin = req.query.longMin;
latMax = req.query.latMax;
longMax = req.query.longMax;
}
try {
const data = await queryJenaIndex({
queryTerm: queryTerm,
latMin: latMin,
longMin: longMin,
latMax: latMax,
longMax: longMax,
});
res.json(data);
} catch(error) {
next(error);
}
});
/* Routes are matched to a url in order of their definition
Redirect all the the rest for react-router to handle */
app.get('*', function(request, response) {
......
import { runSelectQuery } from './SparqlApi';
import { prefixes } from './SparqlQueriesPrefixes';
import { endpoint, jenaQuery } from './SparqlQueriesGeneral';
import { makeObjectList } from './SparqlObjectMapper';
export const queryJenaIndex = async ({
queryTerm,
latMin,
longMin,
latMax,
longMax,
}) => {
let q = jenaQuery;
q = q.replace('<QUERY>', `
?id text:query ('${queryTerm.toLowerCase()}' 10000) .
`);
const results = await runSelectQuery(prefixes + q, endpoint, makeObjectList);
return results;
};
......@@ -9,6 +9,15 @@ export const countQuery = `
}
`;
export const jenaQuery = `
SELECT ?id ?prefLabel ?type
WHERE {
<QUERY>
?id skos:prefLabel ?prefLabel .
?id a ?type .
}
`;
export const facetResultSetQuery = `
SELECT *
WHERE {
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment