Add and delete custom search provider actions and controllers

This commit is contained in:
unknown 2021-10-11 13:03:31 +02:00
parent 459523dfd2
commit a885440fef
15 changed files with 392 additions and 82 deletions

1
api.js
View File

@ -20,6 +20,7 @@ api.use('/api/config', require('./routes/config'));
api.use('/api/weather', require('./routes/weather')); api.use('/api/weather', require('./routes/weather'));
api.use('/api/categories', require('./routes/category')); api.use('/api/categories', require('./routes/category'));
api.use('/api/bookmarks', require('./routes/bookmark')); api.use('/api/bookmarks', require('./routes/bookmark'));
api.use('/api/queries', require('./routes/queries'));
// Custom error handler // Custom error handler
api.use(errorHandler); api.use(errorHandler);

View File

@ -19,9 +19,7 @@ import {
// UI // UI
import InputGroup from '../../UI/Forms/InputGroup/InputGroup'; import InputGroup from '../../UI/Forms/InputGroup/InputGroup';
import Button from '../../UI/Buttons/Button/Button'; import Button from '../../UI/Buttons/Button/Button';
import SettingsHeadline from '../../UI/Headlines/SettingsHeadline/SettingsHeadline';
// CSS
import classes from './OtherSettings.module.css';
// Utils // Utils
import { searchConfig } from '../../../utility'; import { searchConfig } from '../../../utility';
@ -104,7 +102,7 @@ const OtherSettings = (props: ComponentProps): JSX.Element => {
return ( return (
<form onSubmit={(e) => formSubmitHandler(e)}> <form onSubmit={(e) => formSubmitHandler(e)}>
{/* OTHER OPTIONS */} {/* OTHER OPTIONS */}
<h2 className={classes.SettingsSection}>Miscellaneous</h2> <SettingsHeadline text="Miscellaneous" />
<InputGroup> <InputGroup>
<label htmlFor="customTitle">Custom page title</label> <label htmlFor="customTitle">Custom page title</label>
<input <input
@ -118,7 +116,7 @@ const OtherSettings = (props: ComponentProps): JSX.Element => {
</InputGroup> </InputGroup>
{/* BEAHVIOR OPTIONS */} {/* BEAHVIOR OPTIONS */}
<h2 className={classes.SettingsSection}>App Behavior</h2> <SettingsHeadline text="App Behavior" />
<InputGroup> <InputGroup>
<label htmlFor="pinAppsByDefault"> <label htmlFor="pinAppsByDefault">
Pin new applications by default Pin new applications by default
@ -186,7 +184,7 @@ const OtherSettings = (props: ComponentProps): JSX.Element => {
</InputGroup> </InputGroup>
{/* MODULES OPTIONS */} {/* MODULES OPTIONS */}
<h2 className={classes.SettingsSection}>Modules</h2> <SettingsHeadline text="Modules" />
<InputGroup> <InputGroup>
<label htmlFor="hideHeader">Hide greeting and date</label> <label htmlFor="hideHeader">Hide greeting and date</label>
<select <select
@ -225,7 +223,7 @@ const OtherSettings = (props: ComponentProps): JSX.Element => {
</InputGroup> </InputGroup>
{/* DOCKER SETTINGS */} {/* DOCKER SETTINGS */}
<h2 className={classes.SettingsSection}>Docker</h2> <SettingsHeadline text="Docker" />
<InputGroup> <InputGroup>
<label htmlFor="dockerApps">Use Docker API</label> <label htmlFor="dockerApps">Use Docker API</label>
<select <select
@ -254,7 +252,7 @@ const OtherSettings = (props: ComponentProps): JSX.Element => {
</InputGroup> </InputGroup>
{/* KUBERNETES SETTINGS */} {/* KUBERNETES SETTINGS */}
<h2 className={classes.SettingsSection}>Kubernetes</h2> <SettingsHeadline text="Kubernetes" />
<InputGroup> <InputGroup>
<label htmlFor="kubernetesApps">Use Kubernetes Ingress API</label> <label htmlFor="kubernetesApps">Use Kubernetes Ingress API</label>
<select <select

View File

@ -0,0 +1,26 @@
.QueriesGrid {
display: grid;
grid-template-columns: repeat(3, 1fr);
}
.QueriesGrid span {
color: var(--color-primary);
}
.ActionIcons {
display: flex;
}
.ActionIcons svg {
width: 20px;
}
.ActionIcons svg:hover {
cursor: pointer;
}
.Separator {
grid-column: 1 / 4;
border-bottom: 1px solid var(--color-primary);
margin: 10px 0;
}

View File

@ -0,0 +1,82 @@
import { Fragment, useState } from 'react';
import { connect } from 'react-redux';
import classes from './CustomQueries.module.css';
import ModalForm from '../../../UI/Forms/ModalForm/ModalForm';
import Modal from '../../../UI/Modal/Modal';
import Icon from '../../../UI/Icons/Icon/Icon';
import { GlobalState, Query } from '../../../../interfaces';
import InputGroup from '../../../UI/Forms/InputGroup/InputGroup';
import QueriesForm from './QueriesForm';
import { deleteQuery } from '../../../../store/actions';
import Button from '../../../UI/Buttons/Button/Button';
interface Props {
customQueries: Query[];
deleteQuery: (prefix: string) => {};
}
const CustomQueries = (props: Props): JSX.Element => {
const { customQueries, deleteQuery } = props;
const [modalIsOpen, setModalIsOpen] = useState(false);
const deleteHandler = (query: Query) => {
if (window.confirm(`Are you sure you want to delete this provider?`)) {
deleteQuery(query.prefix);
}
};
return (
<Fragment>
<Modal
isOpen={modalIsOpen}
setIsOpen={() => setModalIsOpen(!modalIsOpen)}
>
<QueriesForm modalHandler={() => setModalIsOpen(!modalIsOpen)} />
</Modal>
<div>
<div className={classes.QueriesGrid}>
{customQueries.length > 0 && (
<Fragment>
<span>Name</span>
<span>Prefix</span>
<span>Actions</span>
<div className={classes.Separator}></div>
</Fragment>
)}
{customQueries.map((q: Query, idx) => (
<Fragment key={idx}>
<span>{q.name}</span>
<span>{q.prefix}</span>
<span className={classes.ActionIcons}>
<span>
<Icon icon="mdiPencil" />
</span>
<span onClick={() => deleteHandler(q)}>
<Icon icon="mdiDelete" />
</span>
</span>
</Fragment>
))}
</div>
<Button click={() => setModalIsOpen(true)}>
Add new search provider
</Button>
</div>
</Fragment>
);
};
const mapStateToProps = (state: GlobalState) => {
return {
customQueries: state.config.customQueries,
};
};
export default connect(mapStateToProps, { deleteQuery })(CustomQueries);

View File

@ -0,0 +1,59 @@
import { useState } from 'react';
import Button from '../../../UI/Buttons/Button/Button';
import InputGroup from '../../../UI/Forms/InputGroup/InputGroup';
import ModalForm from '../../../UI/Forms/ModalForm/ModalForm';
interface Props {
modalHandler: () => void;
// addApp: (formData: NewApp | FormData) => any;
// updateApp: (id: number, formData: NewApp | FormData) => any;
// app?: App;
}
const QueriesForm = (props: Props): JSX.Element => {
const [formData, setFormData] = useState();
return (
<ModalForm modalHandler={props.modalHandler} formHandler={() => {}}>
<InputGroup>
<label htmlFor="name">Name</label>
<input
type="text"
name="name"
id="name"
placeholder="Google"
required
// value={formData.name}
// onChange={(e) => inputChangeHandler(e)}
/>
</InputGroup>
<InputGroup>
<label htmlFor="name">Prefix</label>
<input
type="text"
name="name"
id="name"
placeholder="g"
required
// value={formData.name}
// onChange={(e) => inputChangeHandler(e)}
/>
</InputGroup>
<InputGroup>
<label htmlFor="name">Query Template</label>
<input
type="text"
name="name"
id="name"
placeholder="https://www.google.com/search?q="
required
// value={formData.name}
// onChange={(e) => inputChangeHandler(e)}
/>
</InputGroup>
<Button>Add provider</Button>
</ModalForm>
);
};
export default QueriesForm;

View File

@ -1,5 +1,5 @@
// React // React
import { useState, useEffect, FormEvent, ChangeEvent } from 'react'; import { useState, useEffect, FormEvent, ChangeEvent, Fragment } from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
// State // State
@ -13,15 +13,19 @@ import {
SearchForm, SearchForm,
} from '../../../interfaces'; } from '../../../interfaces';
// Utils // Components
import { searchConfig } from '../../../utility'; import CustomQueries from './CustomQueries/CustomQueries';
import InputGroup from '../../UI/Forms/InputGroup/InputGroup';
// Data
import { queries } from '../../../utility/searchQueries.json';
// UI // UI
import Button from '../../UI/Buttons/Button/Button'; import Button from '../../UI/Buttons/Button/Button';
import SettingsHeadline from '../../UI/Headlines/SettingsHeadline/SettingsHeadline';
import InputGroup from '../../UI/Forms/InputGroup/InputGroup';
// Utils
import { searchConfig } from '../../../utility';
// Data
import { queries } from '../../../utility/searchQueries.json';
interface Props { interface Props {
createNotification: (notification: NewNotification) => void; createNotification: (notification: NewNotification) => void;
@ -73,7 +77,13 @@ const SearchSettings = (props: Props): JSX.Element => {
}; };
return ( return (
<form onSubmit={(e) => formSubmitHandler(e)}> <Fragment>
{/* GENERAL SETTINGS */}
<form
onSubmit={(e) => formSubmitHandler(e)}
style={{ marginBottom: '30px' }}
>
<SettingsHeadline text="General" />
<InputGroup> <InputGroup>
<label htmlFor="defaultSearchProvider">Default Search Provider</label> <label htmlFor="defaultSearchProvider">Default Search Provider</label>
<select <select
@ -82,11 +92,15 @@ const SearchSettings = (props: Props): JSX.Element => {
value={formData.defaultSearchProvider} value={formData.defaultSearchProvider}
onChange={(e) => inputChangeHandler(e)} onChange={(e) => inputChangeHandler(e)}
> >
{[...queries, ...props.customQueries].map((query: Query, idx) => ( {[...queries, ...props.customQueries].map((query: Query, idx) => {
const isCustom = idx >= queries.length;
return (
<option key={idx} value={query.prefix}> <option key={idx} value={query.prefix}>
{query.name} {isCustom && '+'} {query.name}
</option> </option>
))} );
})}
</select> </select>
</InputGroup> </InputGroup>
<InputGroup> <InputGroup>
@ -117,6 +131,11 @@ const SearchSettings = (props: Props): JSX.Element => {
</InputGroup> </InputGroup>
<Button>Save changes</Button> <Button>Save changes</Button>
</form> </form>
{/* CUSTOM QUERIES */}
<SettingsHeadline text="Custom search providers" />
<CustomQueries />
</Fragment>
); );
}; };

View File

@ -1,4 +1,4 @@
.SettingsSection { .SettingsHeadline {
color: var(--color-primary); color: var(--color-primary);
padding-bottom: 3px; padding-bottom: 3px;
margin-bottom: 10px; margin-bottom: 10px;

View File

@ -0,0 +1,11 @@
const classes = require('./SettingsHeadline.module.css');
interface Props {
text: string;
}
const SettingsHeadline = (props: Props): JSX.Element => {
return <h2 className={classes.SettingsHeadline}>{props.text}</h2>;
};
export default SettingsHeadline;

View File

@ -28,7 +28,11 @@ import {
GetConfigAction, GetConfigAction,
UpdateConfigAction, UpdateConfigAction,
} from './'; } from './';
import { FetchQueriesAction } from './config'; import {
AddQueryAction,
DeleteQueryAction,
FetchQueriesAction,
} from './config';
export enum ActionTypes { export enum ActionTypes {
// Theme // Theme
@ -65,6 +69,8 @@ export enum ActionTypes {
getConfig = 'GET_CONFIG', getConfig = 'GET_CONFIG',
updateConfig = 'UPDATE_CONFIG', updateConfig = 'UPDATE_CONFIG',
fetchQueries = 'FETCH_QUERIES', fetchQueries = 'FETCH_QUERIES',
addQuery = 'ADD_QUERY',
deleteQuery = 'DELETE_QUERY',
} }
export type Action = export type Action =
@ -96,4 +102,6 @@ export type Action =
// Config // Config
| GetConfigAction | GetConfigAction
| UpdateConfigAction | UpdateConfigAction
| FetchQueriesAction; | FetchQueriesAction
| AddQueryAction
| DeleteQueryAction;

View File

@ -60,9 +60,7 @@ export interface FetchQueriesAction {
export const fetchQueries = export const fetchQueries =
() => async (dispatch: Dispatch<FetchQueriesAction>) => { () => async (dispatch: Dispatch<FetchQueriesAction>) => {
try { try {
const res = await axios.get<ApiResponse<Query[]>>( const res = await axios.get<ApiResponse<Query[]>>('/api/queries');
'/api/config/0/queries'
);
dispatch<FetchQueriesAction>({ dispatch<FetchQueriesAction>({
type: ActionTypes.fetchQueries, type: ActionTypes.fetchQueries,
@ -72,3 +70,43 @@ export const fetchQueries =
console.log(err); console.log(err);
} }
}; };
export interface AddQueryAction {
type: ActionTypes.addQuery;
payload: Query;
}
export const addQuery =
(query: Query) => async (dispatch: Dispatch<AddQueryAction>) => {
try {
const res = await axios.post<ApiResponse<Query>>('/api/queries', query);
dispatch<AddQueryAction>({
type: ActionTypes.addQuery,
payload: res.data.data,
});
} catch (err) {
console.log(err);
}
};
export interface DeleteQueryAction {
type: ActionTypes.deleteQuery;
payload: Query[];
}
export const deleteQuery =
(prefix: string) => async (dispatch: Dispatch<DeleteQueryAction>) => {
try {
const res = await axios.delete<ApiResponse<Query[]>>(
`/api/queries/${prefix}`
);
dispatch<DeleteQueryAction>({
type: ActionTypes.deleteQuery,
payload: res.data.data,
});
} catch (err) {
console.log(err);
}
};

View File

@ -34,6 +34,20 @@ const fetchQueries = (state: State, action: Action): State => {
}; };
}; };
const addQuery = (state: State, action: Action): State => {
return {
...state,
customQueries: [...state.customQueries, action.payload],
};
};
const deleteQuery = (state: State, action: Action): State => {
return {
...state,
customQueries: action.payload,
};
};
const configReducer = (state: State = initialState, action: Action) => { const configReducer = (state: State = initialState, action: Action) => {
switch (action.type) { switch (action.type) {
case ActionTypes.getConfig: case ActionTypes.getConfig:
@ -42,6 +56,10 @@ const configReducer = (state: State = initialState, action: Action) => {
return updateConfig(state, action); return updateConfig(state, action);
case ActionTypes.fetchQueries: case ActionTypes.fetchQueries:
return fetchQueries(state, action); return fetchQueries(state, action);
case ActionTypes.addQuery:
return addQuery(state, action);
case ActionTypes.deleteQuery:
return deleteQuery(state, action);
default: default:
return state; return state;
} }

View File

@ -175,16 +175,3 @@ exports.updateCss = asyncWrapper(async (req, res, next) => {
data: {}, data: {},
}); });
}); });
// @desc Get custom queries file
// @route GET /api/config/0/queries
// @access Public
exports.getQueries = asyncWrapper(async (req, res, next) => {
const file = new File(join(__dirname, '../data/customQueries.json'));
const content = JSON.parse(file.read());
res.status(200).json({
success: true,
data: content.queries,
});
});

View File

@ -0,0 +1,53 @@
const asyncWrapper = require('../../middleware/asyncWrapper');
const File = require('../../utils/File');
const { join } = require('path');
const QUERIES_PATH = join(__dirname, '../../data/customQueries.json');
// @desc Add custom search query
// @route POST /api/queries
// @access Public
exports.addQuery = asyncWrapper(async (req, res, next) => {
const file = new File(QUERIES_PATH);
let content = JSON.parse(file.read());
// Add new query
content.queries.push(req.body);
file.write(content, true);
res.status(201).json({
success: true,
data: req.body,
});
});
// @desc Get custom queries file
// @route GET /api/queries
// @access Public
exports.getQueries = asyncWrapper(async (req, res, next) => {
const file = new File(QUERIES_PATH);
const content = JSON.parse(file.read());
res.status(200).json({
success: true,
data: content.queries,
});
});
// @desc Delete query
// @route DELETE /api/queries/:prefix
// @access Public
exports.deleteQuery = asyncWrapper(async (req, res, next) => {
const file = new File(QUERIES_PATH);
let content = JSON.parse(file.read());
content.queries = content.queries.filter(
(q) => q.prefix != req.params.prefix
);
file.write(content, true);
res.status(200).json({
success: true,
data: content.queries,
});
});

View File

@ -10,7 +10,6 @@ const {
deletePair, deletePair,
updateCss, updateCss,
getCss, getCss,
getQueries,
} = require('../controllers/config'); } = require('../controllers/config');
router.route('/').post(createPair).get(getAllPairs).put(updateValues); router.route('/').post(createPair).get(getAllPairs).put(updateValues);
@ -19,6 +18,4 @@ router.route('/:key').get(getSinglePair).put(updateValue).delete(deletePair);
router.route('/0/css').get(getCss).put(updateCss); router.route('/0/css').get(getCss).put(updateCss);
router.route('/0/queries').get(getQueries);
module.exports = router; module.exports = router;

13
routes/queries.js Normal file
View File

@ -0,0 +1,13 @@
const express = require('express');
const router = express.Router();
const {
getQueries,
addQuery,
deleteQuery,
} = require('../controllers/queries/');
router.route('/').post(addQuery).get(getQueries);
router.route('/:prefix').delete(deleteQuery);
module.exports = router;