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

feat(Admin System): Admin Panel Search Page and Edit Privileges #996

Merged
merged 8 commits into from
Jun 24, 2023
166 changes: 166 additions & 0 deletions src/client/components/pages/admin-panel-search.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
/*
* Copyright (C) 2023 Shivam Awasthi
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/

import * as React from 'react';
import AdminPanelSearchField from './parts/admin-panel-search-field';
import AdminPanelSearchResults from './parts/admin-panel-search-results';
import {Card} from 'react-bootstrap';
import PagerElement from './parts/pager';
import PropTypes from 'prop-types';

type Props = {
from?: number,
initialResults?: any[],
nextEnabled: boolean,
query?: string,
resultsPerPage?: number,
user: Record<string, unknown>
};

type State = {
query: string | null | undefined;
results: any[];
};

class AdminPanelSearchPage extends React.Component<Props, State> {
static displayName = 'AdminPanelSearchPage';

static propTypes = {
from: PropTypes.number,
initialResults: PropTypes.array,
nextEnabled: PropTypes.bool.isRequired,
query: PropTypes.string,
resultsPerPage: PropTypes.number,
user: PropTypes.object.isRequired
};

static defaultProps = {
from: 0,
initialResults: [],
query: '',
resultsPerPage: 20
};

/**
* Initializes component state to default values and binds class
* methods to proper context so that they can be directly invoked
* without explicit binding.
*
* @param {object} props - Properties object passed down from parents.
*/
constructor(props) {
super(props);

this.state = {
query: props.query,
results: props.initialResults
};

this.paginationUrl = './admin-panel/search';
}

paginationUrl: string;

/**
* Gets user text query from the browser's URL search parameters and
* sets it in the state to be passed down to AdminPanelSearchField and Pager components
*
* @param {string} query - Query string entered by user.
*/
handleSearch = (query: string) => {
this.setState({query});
};

/**
* The Pager component deals with fetching the query from the server.
* We use this callback to set the results on this component's state.
*
* @param {array} newResults - The array of results from the query
*/
searchResultsCallback = (newResults: any[]) => {
this.setState({results: newResults});
};

/**
* The Pager component is set up to react to browser history navigation (prev/next buttons),
* and we use this callback to set the query and type on this component's state.
*
* @param {URLSearchParams} searchParams - The URL search parameters passed up from the pager component
*/
searchParamsChangeCallback = (searchParams: URLSearchParams) => {
let query;
if (searchParams.has('q')) {
query = searchParams.get('q');
}
if (query === this.state.query) {
return;
}
this.handleSearch(query);
};

/**
* Renders the component: Search bar with results table located vertically
* below it.
*
* @returns {object} - JSX to render.
*/
render() {
const {query, results} = this.state;
const querySearchParams = `q=${query}&type=editor`;
return (
<Card>
<Card.Header as="h2">
Admin Panel
</Card.Header>
<Card.Body>
<div id="pageWithPagination">
<AdminPanelSearchField
query={query}
onSearch={this.handleSearch}
/>
<AdminPanelSearchResults
results={this.state.results}
user={this.props.user}
/>
<PagerElement
from={this.props.from}
nextEnabled={this.props.nextEnabled}
paginationUrl={this.paginationUrl}
querySearchParams={querySearchParams}
results={results}
searchParamsChangeCallback={this.searchParamsChangeCallback}
searchResultsCallback={this.searchResultsCallback}
size={this.props.resultsPerPage}
/>
<div className="text-center">
{results.length === 0 && query.length !== 0 &&
<div>
<hr className="thin"/>
<h2 style={{color: '#754e37'}}>
No results found
</h2>
</div>}
</div>
</div>
</Card.Body>
</Card>
);
}
}

export default AdminPanelSearchPage;
128 changes: 128 additions & 0 deletions src/client/components/pages/parts/admin-panel-search-field.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
/*
* Copyright (C) 2023 Shivam Awasthi
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/

import * as React from 'react';
import * as bootstrap from 'react-bootstrap';

import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
import PropTypes from 'prop-types';
import _ from 'lodash';
import {faSearch} from '@fortawesome/free-solid-svg-icons';

const {Button, Col, InputGroup, Form, Row} = bootstrap;

const SearchButton = (
<Button
block
type="submit"
variant="success"
>
<FontAwesomeIcon icon={faSearch}/>&nbsp;Search
</Button>
);

const updateDelay = 1000;

type AdminPanelSearchFieldState = {
query: string
};
type AdminPanelSearchFieldProps = {
onSearch: (query: string) => void,
query?: string
};

class AdminPanelSearchField extends React.Component<AdminPanelSearchFieldProps, AdminPanelSearchFieldState> {
static displayName = 'AdminPanelSearchField';

static propTypes = {
onSearch: PropTypes.func.isRequired,
query: PropTypes.string
};

static defaultProps = {
query: ''
};

constructor(props: AdminPanelSearchFieldProps) {
super(props);

this.state = {
query: props.query || ''
};
this.debouncedTriggerOnSearch = _.debounce(this.triggerOnSearch, updateDelay, {});
}

// If search term is changed outside this component (for example browser navigation),
// reflects those changes
componentDidUpdate(prevProps: AdminPanelSearchFieldProps) {
if (prevProps.query !== this.props.query) {
// eslint-disable-next-line react/no-did-update-set-state
this.setState({query: this.props.query});
}
}

debouncedTriggerOnSearch: () => void;

triggerOnSearch() {
const {query} = this.state;
this.props.onSearch(query);
}

handleSubmit = event => {
event.preventDefault();
event.stopPropagation();
this.triggerOnSearch();
};

handleChange = event => {
if (!event.target.value.match(/^ +$/) && event.target.value !== this.state.query) {
this.setState({query: event.target.value}, this.debouncedTriggerOnSearch);
}
};

render() {
return (
<Row>
<Col lg={{offset: 3, span: 6}}>
<form
action="/admin-panel"
className="form-horizontal whole-page-form"
role="search"
onSubmit={this.handleSubmit}
>
<Form.Group>
<InputGroup>
<Form.Control
name="q"
type="text"
value={this.state.query}
onChange={this.handleChange}
/>
<InputGroup.Append>
{SearchButton}
</InputGroup.Append>
</InputGroup>
</Form.Group>
</form>
</Col>
</Row>
);
}
}

export default AdminPanelSearchField;
Loading