Skip to content

Commit

Permalink
Merge pull request #433 from factly/feat/global-search
Browse files Browse the repository at this point in the history
Global search
  • Loading branch information
shreeharsha-factly authored Sep 2, 2021
2 parents 7cb9258 + e710e1b commit e64263f
Show file tree
Hide file tree
Showing 7 changed files with 270 additions and 3 deletions.
2 changes: 1 addition & 1 deletion server/service/core/action/search/route.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ package search
import "github.com/go-chi/chi"

type searchQuery struct {
Query string `json:"q" validate:"required,min=3"`
Query string `json:"q"`
Limit int64 `json:"limit" validate:"lte=20"`
Filters string `json:"filters"`
FacetFilters []string `json:"facetFilters"`
Expand Down
45 changes: 45 additions & 0 deletions studio/src/actions/search.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import axios from 'axios';
import {
ADD_SEARCH_DETAIL,
SET_SEARCH_DETAILS_LOADING,
SEARCH_DETAILS_API,
} from '../constants/search';
import { addErrorNotification } from './notifications';
import getError from '../utils/getError';

export const getSearchDetails = (query) => {
return (dispatch, getState) => {
dispatch(loadingSearchDetails());

return axios
.post(SEARCH_DETAILS_API, query)
.then((response) => {
const state = getState();
dispatch(
addSearchDetails({
data: response.data,
formats: state.formats.details,
}),
);
})
.catch((error) => {
dispatch(addErrorNotification(getError(error)));
})
.finally(() => dispatch(stopLoading()));
};
};

export const addSearchDetails = (data) => ({
type: ADD_SEARCH_DETAIL,
payload: data,
});

export const loadingSearchDetails = () => ({
type: SET_SEARCH_DETAILS_LOADING,
payload: true,
});

export const stopLoading = () => ({
type: SET_SEARCH_DETAILS_LOADING,
payload: false,
});
19 changes: 17 additions & 2 deletions studio/src/components/GlobalNav/Sidebar.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { setCollapse } from './../../actions/sidebar';
import SpaceSelector from './SpaceSelector';
import AccountMenu from './AccountMenu';
import { AppstoreOutlined, MenuFoldOutlined, MenuUnfoldOutlined } from '@ant-design/icons';
import Search from '../Search';

const { Sider } = Layout;
const { SubMenu } = Menu;
Expand Down Expand Up @@ -137,9 +138,23 @@ function Sidebar({ superOrg, permission, orgs, loading, applications }) {
height: '100vh',
}}
>
<div className="menu-header" style={{ padding: collapsed ? '0 0.5rem' : '0 24px' }}>
<SpaceSelector collapsed={collapsed} />
<div
style={{
display: 'flex',
flexDirection: collapsed ? 'column' : 'row',
justifyContent: 'space-between',
alignItems: 'center',
padding: collapsed ? '0 0.5rem' : '0 24px',
}}
>
<Link to="/">
<div className="menu-header" style={{}}>
<SpaceSelector collapsed={collapsed} />
</div>
</Link>
<Search collapsed={collapsed} />
</div>

<Menu
theme={navTheme}
mode="inline"
Expand Down
124 changes: 124 additions & 0 deletions studio/src/components/Search/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import { Input, Modal, List, Typography } from 'antd';
import React, { useEffect, useRef, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { Link } from 'react-router-dom';
import { SearchOutlined } from '@ant-design/icons';
import { getSearchDetails } from '../../actions/search';

function Search({ collapsed }) {
const [query, setQuery] = useState('');
const [open, setOpen] = React.useState(false);
const [selected, setSelected] = useState({
entityIndex: 0,
indexItem: 0,
});
const inputRef = useRef(null);
const dispatch = useDispatch();

const searchEntities = ['articles', 'fact-checks', 'pages', 'claims', 'categories', 'tags'];

const { data, total, entitiesLength } = useSelector(({ search }) => {
const entitiesLength = searchEntities.map((entity) => {
return search.details[entity].length;
});
return { data: search.details, total: search.total, entitiesLength: entitiesLength };
});

useEffect(() => {
if (query.length > 0) dispatch(getSearchDetails({ q: query }));
}, [dispatch, query]);

const handleOk = () => {
setOpen(false);
};

const handleCancel = () => {
setOpen(false);
};

return (
<div
onKeyDown={() => {
let isSet = false;
let entityIndex = selected.entityIndex;
let indexItem = selected.indexItem;
entitiesLength.forEach((length, index) => {
if (selected.entityIndex === index && selected.indexItem < length - 1 && !isSet) {
isSet = true;
indexItem = selected.indexItem + 1;
}
if (!isSet && index > selected.entityIndex && length !== 0) {
isSet = true;
indexItem = 0;
entityIndex = index;
}
});
setSelected({ entityIndex, indexItem });
}}
>
<SearchOutlined
style={{ fontSize: collapsed ? '16px' : '20px' }}
onClick={(e) => {
setOpen(true);
setTimeout(() => inputRef.current.focus(), 0); // antd dialog prevents using inputRef directly, don't modify this while refactoring dega studio
}}
/>
<Modal visible={open} footer={null} onOk={handleOk} onCancel={handleCancel} closable={false}>
<div>
<Input
onChange={(e) => {
setQuery(e.target.value);
setSelected({ entityIndex: 0, indexItem: 0 });
}}
ref={inputRef}
placeholder={'search articles, fact-checks, claims, categories ...'}
/>
</div>

{total > 0 && query.length > 0 ? (
<div style={{ height: 600, overflow: 'scroll' }}>
{searchEntities.map((entity, entityIndex) =>
data[entity].length > 0 ? (
<List
key={entity}
header={<h2>{entity.toLocaleUpperCase()}</h2>}
dataSource={data[entity]}
renderItem={(item, indexItem) => {
return (
<Link
to={`/${entity === 'articles' ? 'posts' : entity}/${item.id}/edit`}
onClick={() => setOpen(false)}
>
<List.Item
style={
indexItem === selected.indexItem && entityIndex === selected.entityIndex
? { backgroundColor: '#5468ff', padding: 5 }
: {}
}
>
<Typography.Text
style={
indexItem === selected.indexItem &&
entityIndex === selected.entityIndex
? { color: '#fff' }
: {}
}
onMouseOver={() => setSelected({ indexItem, entityIndex })}
>
{item.title || item.name || item.claim}
</Typography.Text>
</List.Item>
</Link>
);
}}
/>
) : null,
)}
</div>
) : null}
</Modal>
</div>
);
}

export default Search;
6 changes: 6 additions & 0 deletions studio/src/constants/search.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
//Actions
export const ADD_SEARCH_DETAIL = 'ADD_SEARCH_DETAIL';
export const SET_SEARCH_DETAILS_LOADING = 'SET_SEARCH_DETAILS_LOADING';

//API
export const SEARCH_DETAILS_API = '/core/search';
2 changes: 2 additions & 0 deletions studio/src/reducers/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import info from './infoReducer';
import pages from './pagesReducer';
import webhooks from './webhooksReducer';
import profile from './profileReducer';
import search from './searchReducer';

const appReducer = combineReducers({
admin,
Expand Down Expand Up @@ -64,6 +65,7 @@ const appReducer = combineReducers({
events,
webhooks,
profile,
search,
});

const rootReducer = (state, action) => {
Expand Down
75 changes: 75 additions & 0 deletions studio/src/reducers/searchReducer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { SET_SEARCH_DETAILS_LOADING, ADD_SEARCH_DETAIL } from '../constants/search';

const initialState = {
req: [],
details: {
articles: [],
'fact-checks': [],
pages: [],
claims: [],
tags: [],
categories: [],
media: [],
ratings: [],
total: 0,
},
loading: true,
};

export default function authorsReducer(state = initialState, action = {}) {
switch (action.type) {
case SET_SEARCH_DETAILS_LOADING:
return {
...state,
loading: action.payload,
};
case ADD_SEARCH_DETAIL:
const { data, formats } = action.payload;
if (data.length === 0) {
return initialState;
}

const result = {
articles: [],
'fact-checks': [],
pages: [],
claims: [],
tags: [],
categories: [],
media: [],
ratings: [],
total: 0,
};

const kind = {
category: 'categories',
tag: 'tags',
claim: 'claims',
rating: 'ratings',
medium: 'media',
};

data.forEach((each) => {
if (each.kind === 'post' && each.is_page) {
if (each.status !== 'template') result.pages.push(each);
} else if (each.kind === 'post') {
if (formats[each.format_id].slug === 'article' && each.status !== 'template') {
result.articles.push(each);
}
if (formats[each.format_id].slug === 'fact-check' && each.status !== 'template') {
result['fact-checks'].push(each);
}
} else if (kind[each.kind]) {
result[kind[each.kind]].push(each);
}
});

return {
...state,
details: result,
total: data.length,
};
default:
return state;
}
}

0 comments on commit e64263f

Please sign in to comment.