Skip to content

Commit

Permalink
Merge pull request #996 from the-good-boy/adminPanel
Browse files Browse the repository at this point in the history
feat(Admin System): Admin Panel Search Page and Edit Privileges
  • Loading branch information
anshg1214 committed Jun 24, 2023
2 parents 119d9dc + b38d114 commit c55fcb6
Show file tree
Hide file tree
Showing 17 changed files with 1,051 additions and 2 deletions.
186 changes: 186 additions & 0 deletions src/client/components/pages/admin-panel-search.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
/*
* 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[];
updateResultsTrigger: number;
};

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,
updateResultsTrigger: 0
};

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);
};

/**
* When we update the privileges of the user, we need to update the information in the results page
* The updateResultsTrigger is passed as a prop in the Pager Component, whenever we want to
* get updated results, we need to just use the triggerSearch function in the Pager Component. In order to
* trigger that function without any change in query, we can just update this prop by flipping it between 0 and 1.
* This will trigger the componentDidUpdate lifecycle method in Pager component and then we can run the
* triggerSearch function
*/
updateResultsOnPrivsChange = () => {
const {updateResultsTrigger: trigger} = this.state;
this.setState({
updateResultsTrigger: 1 - trigger
});
};

/**
* Renders the component: Search bar with results table located vertically
* below it.
*
* @returns {object} - JSX to render.
*/
render() {
const {query, results, updateResultsTrigger} = 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}
updateResultsOnPrivsChange={this.updateResultsOnPrivsChange}
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}
updateResultsTrigger={updateResultsTrigger}
/>
<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;
129 changes: 129 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,129 @@
/*
* 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

0 comments on commit c55fcb6

Please sign in to comment.