1
0
Fork 0
mirror of https://github.com/pawelmalak/flame.git synced 2025-07-19 11:39:36 +02:00

Merge pull request #84 from pawelmalak/local-search

Added searching (filtering) of local apps and bookmarks
This commit is contained in:
pawelmalak 2021-09-06 14:43:35 +02:00 committed by GitHub
commit 43f38a2f44
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 421 additions and 4819 deletions

View file

@ -32,7 +32,7 @@ git clone https://github.com/pawelmalak/flame
cd flame cd flame
# run only once # run only once
npm run dev-init npm run dev:init
# start backend and frontend development servers # start backend and frontend development servers
npm run dev npm run dev

View file

@ -7,6 +7,7 @@ import AppCard from '../AppCard/AppCard';
interface ComponentProps { interface ComponentProps {
apps: App[]; apps: App[];
totalApps?: number; totalApps?: number;
searching: boolean;
} }
const AppGrid = (props: ComponentProps): JSX.Element => { const AppGrid = (props: ComponentProps): JSX.Element => {
@ -16,26 +17,37 @@ const AppGrid = (props: ComponentProps): JSX.Element => {
apps = ( apps = (
<div className={classes.AppGrid}> <div className={classes.AppGrid}>
{props.apps.map((app: App): JSX.Element => { {props.apps.map((app: App): JSX.Element => {
return <AppCard return <AppCard key={app.id} app={app} />;
key={app.id}
app={app}
/>
})} })}
</div> </div>
) );
} else { } else {
if (props.totalApps) { if (props.totalApps) {
if (props.searching) {
apps = ( apps = (
<p className={classes.AppsMessage}>There are no pinned applications. You can pin them from the <Link to='/applications'>/applications</Link> menu</p> <p className={classes.AppsMessage}>
No apps match your search criteria
</p>
); );
} else { } else {
apps = ( apps = (
<p className={classes.AppsMessage}>You don't have any applications. You can add a new one from <Link to='/applications'>/applications</Link> menu</p> <p className={classes.AppsMessage}>
There are no pinned applications. You can pin them from the{' '}
<Link to="/applications">/applications</Link> menu
</p>
);
}
} else {
apps = (
<p className={classes.AppsMessage}>
You don't have any applications. You can add a new one from{' '}
<Link to="/applications">/applications</Link> menu
</p>
); );
} }
} }
return apps; return apps;
} };
export default AppGrid; export default AppGrid;

View file

@ -27,14 +27,11 @@ interface ComponentProps {
getApps: Function; getApps: Function;
apps: App[]; apps: App[];
loading: boolean; loading: boolean;
searching: boolean;
} }
const Apps = (props: ComponentProps): JSX.Element => { const Apps = (props: ComponentProps): JSX.Element => {
const { const { getApps, apps, loading, searching = false } = props;
getApps,
apps,
loading
} = props;
const [modalIsOpen, setModalIsOpen] = useState(false); const [modalIsOpen, setModalIsOpen] = useState(false);
const [isInEdit, setIsInEdit] = useState(false); const [isInEdit, setIsInEdit] = useState(false);
@ -47,8 +44,8 @@ const Apps = (props: ComponentProps): JSX.Element => {
orderId: 0, orderId: 0,
id: 0, id: 0,
createdAt: new Date(), createdAt: new Date(),
updatedAt: new Date() updatedAt: new Date(),
}) });
useEffect(() => { useEffect(() => {
if (apps.length === 0) { if (apps.length === 0) {
@ -59,63 +56,57 @@ const Apps = (props: ComponentProps): JSX.Element => {
const toggleModal = (): void => { const toggleModal = (): void => {
setModalIsOpen(!modalIsOpen); setModalIsOpen(!modalIsOpen);
setIsInUpdate(false); setIsInUpdate(false);
} };
const toggleEdit = (): void => { const toggleEdit = (): void => {
setIsInEdit(!isInEdit); setIsInEdit(!isInEdit);
setIsInUpdate(false); setIsInUpdate(false);
} };
const toggleUpdate = (app: App): void => { const toggleUpdate = (app: App): void => {
setAppInUpdate(app); setAppInUpdate(app);
setIsInUpdate(true); setIsInUpdate(true);
setModalIsOpen(true); setModalIsOpen(true);
} };
return ( return (
<Container> <Container>
<Modal isOpen={modalIsOpen} setIsOpen={setModalIsOpen}> <Modal isOpen={modalIsOpen} setIsOpen={setModalIsOpen}>
{!isInUpdate {!isInUpdate ? (
? <AppForm modalHandler={toggleModal} /> <AppForm modalHandler={toggleModal} />
: <AppForm modalHandler={toggleModal} app={appInUpdate} /> ) : (
} <AppForm modalHandler={toggleModal} app={appInUpdate} />
)}
</Modal> </Modal>
<Headline <Headline
title='All Applications' title="All Applications"
subtitle={(<Link to='/'>Go back</Link>)} subtitle={<Link to="/">Go back</Link>}
/> />
<div className={classes.ActionsContainer}> <div className={classes.ActionsContainer}>
<ActionButton <ActionButton name="Add" icon="mdiPlusBox" handler={toggleModal} />
name='Add' <ActionButton name="Edit" icon="mdiPencil" handler={toggleEdit} />
icon='mdiPlusBox'
handler={toggleModal}
/>
<ActionButton
name='Edit'
icon='mdiPencil'
handler={toggleEdit}
/>
</div> </div>
<div className={classes.Apps}> <div className={classes.Apps}>
{loading {loading ? (
? <Spinner /> <Spinner />
: (!isInEdit ) : !isInEdit ? (
? <AppGrid apps={apps} /> <AppGrid apps={apps} searching />
: <AppTable updateAppHandler={toggleUpdate} />) ) : (
} <AppTable updateAppHandler={toggleUpdate} />
)}
</div> </div>
</Container> </Container>
) );
} };
const mapStateToProps = (state: GlobalState) => { const mapStateToProps = (state: GlobalState) => {
return { return {
apps: state.app.apps, apps: state.app.apps,
loading: state.app.loading loading: state.app.loading,
} };
} };
export default connect(mapStateToProps, { getApps })(Apps); export default connect(mapStateToProps, { getApps })(Apps);

View file

@ -9,30 +9,49 @@ import BookmarkCard from '../BookmarkCard/BookmarkCard';
interface ComponentProps { interface ComponentProps {
categories: Category[]; categories: Category[];
totalCategories?: number; totalCategories?: number;
searching: boolean;
} }
const BookmarkGrid = (props: ComponentProps): JSX.Element => { const BookmarkGrid = (props: ComponentProps): JSX.Element => {
let bookmarks: JSX.Element; let bookmarks: JSX.Element;
if (props.categories.length > 0) { if (props.categories.length > 0) {
if (props.searching && props.categories[0].bookmarks.length === 0) {
bookmarks = (
<p className={classes.BookmarksMessage}>
No bookmarks match your search criteria
</p>
);
} else {
bookmarks = ( bookmarks = (
<div className={classes.BookmarkGrid}> <div className={classes.BookmarkGrid}>
{props.categories.map((category: Category): JSX.Element => <BookmarkCard category={category} key={category.id} />)} {props.categories.map(
(category: Category): JSX.Element => (
<BookmarkCard category={category} key={category.id} />
)
)}
</div> </div>
); );
}
} else { } else {
if (props.totalCategories) { if (props.totalCategories) {
bookmarks = ( bookmarks = (
<p className={classes.BookmarksMessage}>There are no pinned categories. You can pin them from the <Link to='/bookmarks'>/bookmarks</Link> menu</p> <p className={classes.BookmarksMessage}>
There are no pinned categories. You can pin them from the{' '}
<Link to="/bookmarks">/bookmarks</Link> menu
</p>
); );
} else { } else {
bookmarks = ( bookmarks = (
<p className={classes.BookmarksMessage}>You don't have any bookmarks. You can add a new one from <Link to='/bookmarks'>/bookmarks</Link> menu</p> <p className={classes.BookmarksMessage}>
You don't have any bookmarks. You can add a new one from{' '}
<Link to="/bookmarks">/bookmarks</Link> menu
</p>
); );
} }
} }
return bookmarks; return bookmarks;
} };
export default BookmarkGrid; export default BookmarkGrid;

View file

@ -20,24 +20,23 @@ interface ComponentProps {
loading: boolean; loading: boolean;
categories: Category[]; categories: Category[];
getCategories: () => void; getCategories: () => void;
searching: boolean;
} }
export enum ContentType { export enum ContentType {
category, category,
bookmark bookmark,
} }
const Bookmarks = (props: ComponentProps): JSX.Element => { const Bookmarks = (props: ComponentProps): JSX.Element => {
const { const { getCategories, categories, loading, searching = false } = props;
getCategories,
categories,
loading
} = props;
const [modalIsOpen, setModalIsOpen] = useState(false); const [modalIsOpen, setModalIsOpen] = useState(false);
const [formContentType, setFormContentType] = useState(ContentType.category); const [formContentType, setFormContentType] = useState(ContentType.category);
const [isInEdit, setIsInEdit] = useState(false); const [isInEdit, setIsInEdit] = useState(false);
const [tableContentType, setTableContentType] = useState(ContentType.category); const [tableContentType, setTableContentType] = useState(
ContentType.category
);
const [isInUpdate, setIsInUpdate] = useState(false); const [isInUpdate, setIsInUpdate] = useState(false);
const [categoryInUpdate, setCategoryInUpdate] = useState<Category>({ const [categoryInUpdate, setCategoryInUpdate] = useState<Category>({
name: '', name: '',
@ -46,8 +45,8 @@ const Bookmarks = (props: ComponentProps): JSX.Element => {
orderId: 0, orderId: 0,
bookmarks: [], bookmarks: [],
createdAt: new Date(), createdAt: new Date(),
updatedAt: new Date() updatedAt: new Date(),
}) });
const [bookmarkInUpdate, setBookmarkInUpdate] = useState<Bookmark>({ const [bookmarkInUpdate, setBookmarkInUpdate] = useState<Bookmark>({
name: '', name: '',
url: '', url: '',
@ -55,24 +54,24 @@ const Bookmarks = (props: ComponentProps): JSX.Element => {
icon: '', icon: '',
id: -1, id: -1,
createdAt: new Date(), createdAt: new Date(),
updatedAt: new Date() updatedAt: new Date(),
}) });
useEffect(() => { useEffect(() => {
if (categories.length === 0) { if (categories.length === 0) {
getCategories(); getCategories();
} }
}, [getCategories]) }, [getCategories]);
const toggleModal = (): void => { const toggleModal = (): void => {
setModalIsOpen(!modalIsOpen); setModalIsOpen(!modalIsOpen);
} };
const addActionHandler = (contentType: ContentType) => { const addActionHandler = (contentType: ContentType) => {
setFormContentType(contentType); setFormContentType(contentType);
setIsInUpdate(false); setIsInUpdate(false);
toggleModal(); toggleModal();
} };
const editActionHandler = (contentType: ContentType) => { const editActionHandler = (contentType: ContentType) => {
// We're in the edit mode and the same button was clicked - go back to list // We're in the edit mode and the same button was clicked - go back to list
@ -82,11 +81,11 @@ const Bookmarks = (props: ComponentProps): JSX.Element => {
setIsInEdit(true); setIsInEdit(true);
setTableContentType(contentType); setTableContentType(contentType);
} }
} };
const instanceOfCategory = (object: any): object is Category => { const instanceOfCategory = (object: any): object is Category => {
return 'bookmarks' in object; return 'bookmarks' in object;
} };
const goToUpdateMode = (data: Category | Bookmark): void => { const goToUpdateMode = (data: Category | Bookmark): void => {
setIsInUpdate(true); setIsInUpdate(true);
@ -98,67 +97,76 @@ const Bookmarks = (props: ComponentProps): JSX.Element => {
setBookmarkInUpdate(data); setBookmarkInUpdate(data);
} }
toggleModal(); toggleModal();
} };
return ( return (
<Container> <Container>
<Modal isOpen={modalIsOpen} setIsOpen={toggleModal}> <Modal isOpen={modalIsOpen} setIsOpen={toggleModal}>
{!isInUpdate {!isInUpdate ? (
? <BookmarkForm modalHandler={toggleModal} contentType={formContentType} /> <BookmarkForm
: formContentType === ContentType.category modalHandler={toggleModal}
? <BookmarkForm modalHandler={toggleModal} contentType={formContentType} category={categoryInUpdate} /> contentType={formContentType}
: <BookmarkForm modalHandler={toggleModal} contentType={formContentType} bookmark={bookmarkInUpdate} /> />
} ) : formContentType === ContentType.category ? (
<BookmarkForm
modalHandler={toggleModal}
contentType={formContentType}
category={categoryInUpdate}
/>
) : (
<BookmarkForm
modalHandler={toggleModal}
contentType={formContentType}
bookmark={bookmarkInUpdate}
/>
)}
</Modal> </Modal>
<Headline <Headline title="All Bookmarks" subtitle={<Link to="/">Go back</Link>} />
title='All Bookmarks'
subtitle={(<Link to='/'>Go back</Link>)}
/>
<div className={classes.ActionsContainer}> <div className={classes.ActionsContainer}>
<ActionButton <ActionButton
name='Add Category' name="Add Category"
icon='mdiPlusBox' icon="mdiPlusBox"
handler={() => addActionHandler(ContentType.category)} handler={() => addActionHandler(ContentType.category)}
/> />
<ActionButton <ActionButton
name='Add Bookmark' name="Add Bookmark"
icon='mdiPlusBox' icon="mdiPlusBox"
handler={() => addActionHandler(ContentType.bookmark)} handler={() => addActionHandler(ContentType.bookmark)}
/> />
<ActionButton <ActionButton
name='Edit Categories' name="Edit Categories"
icon='mdiPencil' icon="mdiPencil"
handler={() => editActionHandler(ContentType.category)} handler={() => editActionHandler(ContentType.category)}
/> />
<ActionButton <ActionButton
name='Edit Bookmarks' name="Edit Bookmarks"
icon='mdiPencil' icon="mdiPencil"
handler={() => editActionHandler(ContentType.bookmark)} handler={() => editActionHandler(ContentType.bookmark)}
/> />
</div> </div>
{loading {loading ? (
? <Spinner /> <Spinner />
: (!isInEdit ) : !isInEdit ? (
? <BookmarkGrid categories={categories} /> <BookmarkGrid categories={categories} searching />
: <BookmarkTable ) : (
<BookmarkTable
contentType={tableContentType} contentType={tableContentType}
categories={categories} categories={categories}
updateHandler={goToUpdateMode} updateHandler={goToUpdateMode}
/> />
) )}
}
</Container> </Container>
) );
} };
const mapStateToProps = (state: GlobalState) => { const mapStateToProps = (state: GlobalState) => {
return { return {
loading: state.bookmark.loading, loading: state.bookmark.loading,
categories: state.bookmark.categories categories: state.bookmark.categories,
} };
} };
export default connect(mapStateToProps, { getCategories })(Bookmarks); export default connect(mapStateToProps, { getCategories })(Bookmarks);

View file

@ -47,13 +47,16 @@ const Home = (props: ComponentProps): JSX.Element => {
appsLoading, appsLoading,
getCategories, getCategories,
categories, categories,
categoriesLoading categoriesLoading,
} = props; } = props;
const [header, setHeader] = useState({ const [header, setHeader] = useState({
dateTime: dateTime(), dateTime: dateTime(),
greeting: greeter() greeting: greeter(),
}) });
// Local search query
const [localSearch, setLocalSearch] = useState<null | string>(null);
// Load applications // Load applications
useEffect(() => { useEffect(() => {
@ -78,78 +81,108 @@ const Home = (props: ComponentProps): JSX.Element => {
interval = setInterval(() => { interval = setInterval(() => {
setHeader({ setHeader({
dateTime: dateTime(), dateTime: dateTime(),
greeting: greeter() greeting: greeter(),
}) });
}, 1000); }, 1000);
} }
return () => clearInterval(interval); return () => clearInterval(interval);
}, []) }, []);
// Search bookmarks
const searchBookmarks = (query: string): Category[] => {
const category = { ...categories[0] };
category.name = 'Search Results';
category.bookmarks = categories
.map(({ bookmarks }) => bookmarks)
.flat()
.filter(({ name }) => new RegExp(query, 'i').test(name));
return [category];
};
return ( return (
<Container> <Container>
{searchConfig('hideSearch', 0) !== 1 {searchConfig('hideSearch', 0) !== 1 ? (
? <SearchBar /> <SearchBar setLocalSearch={setLocalSearch} />
: <div></div> ) : (
} <div></div>
)}
{searchConfig('hideHeader', 0) !== 1 {searchConfig('hideHeader', 0) !== 1 ? (
? (
<header className={classes.Header}> <header className={classes.Header}>
<p>{header.dateTime}</p> <p>{header.dateTime}</p>
<Link to='/settings' className={classes.SettingsLink}>Go to Settings</Link> <Link to="/settings" className={classes.SettingsLink}>
Go to Settings
</Link>
<span className={classes.HeaderMain}> <span className={classes.HeaderMain}>
<h1>{header.greeting}</h1> <h1>{header.greeting}</h1>
<WeatherWidget /> <WeatherWidget />
</span> </span>
</header> </header>
) : (
<div></div>
)}
{searchConfig('hideApps', 0) !== 1 ? (
<Fragment>
<SectionHeadline title="Applications" link="/applications" />
{appsLoading ? (
<Spinner />
) : (
<AppGrid
apps={
!localSearch
? apps.filter(({ isPinned }) => isPinned)
: apps.filter(({ name }) =>
new RegExp(localSearch, 'i').test(name)
) )
: <div></div>
} }
{searchConfig('hideApps', 0) !== 1
? (<Fragment>
<SectionHeadline title='Applications' link='/applications' />
{appsLoading
? <Spinner />
: <AppGrid
apps={apps.filter((app: App) => app.isPinned)}
totalApps={apps.length} totalApps={apps.length}
searching={!!localSearch}
/> />
} )}
<div className={classes.HomeSpace}></div> <div className={classes.HomeSpace}></div>
</Fragment>) </Fragment>
: <div></div> ) : (
} <div></div>
)}
{searchConfig('hideCategories', 0) !== 1 {searchConfig('hideCategories', 0) !== 1 ? (
? (<Fragment> <Fragment>
<SectionHeadline title='Bookmarks' link='/bookmarks' /> <SectionHeadline title="Bookmarks" link="/bookmarks" />
{categoriesLoading {categoriesLoading ? (
? <Spinner /> <Spinner />
: <BookmarkGrid ) : (
categories={categories.filter((category: Category) => category.isPinned)} <BookmarkGrid
categories={
!localSearch
? categories.filter(({ isPinned }) => isPinned)
: searchBookmarks(localSearch)
}
totalCategories={categories.length} totalCategories={categories.length}
searching={!!localSearch}
/> />
} )}
</Fragment>) </Fragment>
: <div></div> ) : (
} <div></div>
)}
<Link to='/settings' className={classes.SettingsButton}> <Link to="/settings" className={classes.SettingsButton}>
<Icon icon='mdiCog' color='var(--color-background)' /> <Icon icon="mdiCog" color="var(--color-background)" />
</Link> </Link>
</Container> </Container>
) );
} };
const mapStateToProps = (state: GlobalState) => { const mapStateToProps = (state: GlobalState) => {
return { return {
appsLoading: state.app.loading, appsLoading: state.app.loading,
apps: state.app.apps, apps: state.app.apps,
categoriesLoading: state.bookmark.loading, categoriesLoading: state.bookmark.loading,
categories: state.bookmark.categories categories: state.bookmark.categories,
} };
} };
export default connect(mapStateToProps, { getApps, getCategories })(Home); export default connect(mapStateToProps, { getApps, getCategories })(Home);

View file

@ -15,36 +15,53 @@ import { searchParser } from '../../utility';
interface ComponentProps { interface ComponentProps {
createNotification: (notification: NewNotification) => void; createNotification: (notification: NewNotification) => void;
setLocalSearch: (query: string) => void;
} }
const SearchBar = (props: ComponentProps): JSX.Element => { const SearchBar = (props: ComponentProps): JSX.Element => {
const { setLocalSearch, createNotification } = props;
const inputRef = useRef<HTMLInputElement>(document.createElement('input')); const inputRef = useRef<HTMLInputElement>(document.createElement('input'));
useEffect(() => { useEffect(() => {
inputRef.current.focus(); inputRef.current.focus();
}, []) }, []);
const searchHandler = (e: KeyboardEvent<HTMLInputElement>) => { const searchHandler = (e: KeyboardEvent<HTMLInputElement>) => {
if (e.code === 'Enter') { const searchResult = searchParser(inputRef.current.value);
const prefixFound = searchParser(inputRef.current.value);
if (!prefixFound) { if (searchResult.isLocal) {
props.createNotification({ setLocalSearch(searchResult.search);
}
if (e.code === 'Enter') {
if (!searchResult.query.prefix) {
createNotification({
title: 'Error', title: 'Error',
message: 'Prefix not found' message: 'Prefix not found',
}) });
} else if (searchResult.isLocal) {
setLocalSearch(searchResult.search);
} else {
if (searchResult.sameTab) {
document.location.replace(
`${searchResult.query.template}${searchResult.search}`
);
} else {
window.open(`${searchResult.query.template}${searchResult.search}`);
} }
} }
} }
};
return ( return (
<input <input
ref={inputRef} ref={inputRef}
type='text' type="text"
className={classes.SearchBar} className={classes.SearchBar}
onKeyDown={(e) => searchHandler(e)} onKeyUp={(e) => searchHandler(e)}
/> />
) );
} };
export default connect(null, { createNotification })(SearchBar); export default connect(null, { createNotification })(SearchBar);

View file

@ -6,7 +6,7 @@ import {
createNotification, createNotification,
updateConfig, updateConfig,
sortApps, sortApps,
sortCategories sortCategories,
} from '../../../store/actions'; } from '../../../store/actions';
// Typescript // Typescript
@ -14,7 +14,7 @@ import {
GlobalState, GlobalState,
NewNotification, NewNotification,
Query, Query,
SettingsForm SettingsForm,
} from '../../../interfaces'; } from '../../../interfaces';
// UI // UI
@ -53,7 +53,7 @@ const OtherSettings = (props: ComponentProps): JSX.Element => {
searchSameTab: 0, searchSameTab: 0,
dockerApps: 1, dockerApps: 1,
kubernetesApps: 1, kubernetesApps: 1,
unpinStoppedApps: 1 unpinStoppedApps: 1,
}); });
// Get config // Get config
@ -73,7 +73,7 @@ const OtherSettings = (props: ComponentProps): JSX.Element => {
searchSameTab: searchConfig('searchSameTab', 0), searchSameTab: searchConfig('searchSameTab', 0),
dockerApps: searchConfig('dockerApps', 0), dockerApps: searchConfig('dockerApps', 0),
kubernetesApps: searchConfig('kubernetesApps', 0), kubernetesApps: searchConfig('kubernetesApps', 0),
unpinStoppedApps: searchConfig('unpinStoppedApps', 0) unpinStoppedApps: searchConfig('unpinStoppedApps', 0),
}); });
}, [props.loading]); }, [props.loading]);
@ -105,115 +105,117 @@ const OtherSettings = (props: ComponentProps): JSX.Element => {
setFormData({ setFormData({
...formData, ...formData,
[e.target.name]: value [e.target.name]: value,
}); });
}; };
return ( return (
<form onSubmit={e => formSubmitHandler(e)}> <form onSubmit={(e) => formSubmitHandler(e)}>
{/* OTHER OPTIONS */} {/* OTHER OPTIONS */}
<h2 className={classes.SettingsSection}>Miscellaneous</h2> <h2 className={classes.SettingsSection}>Miscellaneous</h2>
<InputGroup> <InputGroup>
<label htmlFor='customTitle'>Custom page title</label> <label htmlFor="customTitle">Custom page title</label>
<input <input
type='text' type="text"
id='customTitle' id="customTitle"
name='customTitle' name="customTitle"
placeholder='Flame' placeholder="Flame"
value={formData.customTitle} value={formData.customTitle}
onChange={e => inputChangeHandler(e)} onChange={(e) => inputChangeHandler(e)}
/> />
</InputGroup> </InputGroup>
{/* BEAHVIOR OPTIONS */} {/* BEAHVIOR OPTIONS */}
<h2 className={classes.SettingsSection}>App Behavior</h2> <h2 className={classes.SettingsSection}>App Behavior</h2>
<InputGroup> <InputGroup>
<label htmlFor='pinAppsByDefault'> <label htmlFor="pinAppsByDefault">
Pin new applications by default Pin new applications by default
</label> </label>
<select <select
id='pinAppsByDefault' id="pinAppsByDefault"
name='pinAppsByDefault' name="pinAppsByDefault"
value={formData.pinAppsByDefault} value={formData.pinAppsByDefault}
onChange={e => inputChangeHandler(e, true)} onChange={(e) => inputChangeHandler(e, true)}
> >
<option value={1}>True</option> <option value={1}>True</option>
<option value={0}>False</option> <option value={0}>False</option>
</select> </select>
</InputGroup> </InputGroup>
<InputGroup> <InputGroup>
<label htmlFor='pinCategoriesByDefault'> <label htmlFor="pinCategoriesByDefault">
Pin new categories by default Pin new categories by default
</label> </label>
<select <select
id='pinCategoriesByDefault' id="pinCategoriesByDefault"
name='pinCategoriesByDefault' name="pinCategoriesByDefault"
value={formData.pinCategoriesByDefault} value={formData.pinCategoriesByDefault}
onChange={e => inputChangeHandler(e, true)} onChange={(e) => inputChangeHandler(e, true)}
> >
<option value={1}>True</option> <option value={1}>True</option>
<option value={0}>False</option> <option value={0}>False</option>
</select> </select>
</InputGroup> </InputGroup>
<InputGroup> <InputGroup>
<label htmlFor='useOrdering'>Sorting type</label> <label htmlFor="useOrdering">Sorting type</label>
<select <select
id='useOrdering' id="useOrdering"
name='useOrdering' name="useOrdering"
value={formData.useOrdering} value={formData.useOrdering}
onChange={e => inputChangeHandler(e)} onChange={(e) => inputChangeHandler(e)}
> >
<option value='createdAt'>By creation date</option> <option value="createdAt">By creation date</option>
<option value='name'>Alphabetical order</option> <option value="name">Alphabetical order</option>
<option value='orderId'>Custom order</option> <option value="orderId">Custom order</option>
</select> </select>
</InputGroup> </InputGroup>
<InputGroup> <InputGroup>
<label htmlFor='defaultSearchProvider'>Default Search Provider</label> <label htmlFor="defaultSearchProvider">Default Search Provider</label>
<select <select
id='defaultSearchProvider' id="defaultSearchProvider"
name='defaultSearchProvider' name="defaultSearchProvider"
value={formData.defaultSearchProvider} value={formData.defaultSearchProvider}
onChange={e => inputChangeHandler(e)} onChange={(e) => inputChangeHandler(e)}
> >
{queries.map((query: Query) => ( {queries.map((query: Query, idx) => (
<option value={query.prefix}>{query.name}</option> <option key={idx} value={query.prefix}>
{query.name}
</option>
))} ))}
</select> </select>
</InputGroup> </InputGroup>
<InputGroup> <InputGroup>
<label htmlFor='searchSameTab'> <label htmlFor="searchSameTab">
Open search results in the same tab Open search results in the same tab
</label> </label>
<select <select
id='searchSameTab' id="searchSameTab"
name='searchSameTab' name="searchSameTab"
value={formData.searchSameTab} value={formData.searchSameTab}
onChange={e => inputChangeHandler(e, true)} onChange={(e) => inputChangeHandler(e, true)}
> >
<option value={1}>True</option> <option value={1}>True</option>
<option value={0}>False</option> <option value={0}>False</option>
</select> </select>
</InputGroup> </InputGroup>
<InputGroup> <InputGroup>
<label htmlFor='appsSameTab'>Open applications in the same tab</label> <label htmlFor="appsSameTab">Open applications in the same tab</label>
<select <select
id='appsSameTab' id="appsSameTab"
name='appsSameTab' name="appsSameTab"
value={formData.appsSameTab} value={formData.appsSameTab}
onChange={e => inputChangeHandler(e, true)} onChange={(e) => inputChangeHandler(e, true)}
> >
<option value={1}>True</option> <option value={1}>True</option>
<option value={0}>False</option> <option value={0}>False</option>
</select> </select>
</InputGroup> </InputGroup>
<InputGroup> <InputGroup>
<label htmlFor='bookmarksSameTab'>Open bookmarks in the same tab</label> <label htmlFor="bookmarksSameTab">Open bookmarks in the same tab</label>
<select <select
id='bookmarksSameTab' id="bookmarksSameTab"
name='bookmarksSameTab' name="bookmarksSameTab"
value={formData.bookmarksSameTab} value={formData.bookmarksSameTab}
onChange={e => inputChangeHandler(e, true)} onChange={(e) => inputChangeHandler(e, true)}
> >
<option value={1}>True</option> <option value={1}>True</option>
<option value={0}>False</option> <option value={0}>False</option>
@ -223,48 +225,48 @@ const OtherSettings = (props: ComponentProps): JSX.Element => {
{/* MODULES OPTIONS */} {/* MODULES OPTIONS */}
<h2 className={classes.SettingsSection}>Modules</h2> <h2 className={classes.SettingsSection}>Modules</h2>
<InputGroup> <InputGroup>
<label htmlFor='hideSearch'>Hide search bar</label> <label htmlFor="hideSearch">Hide search bar</label>
<select <select
id='hideSearch' id="hideSearch"
name='hideSearch' name="hideSearch"
value={formData.hideSearch} value={formData.hideSearch}
onChange={e => inputChangeHandler(e, true)} onChange={(e) => inputChangeHandler(e, true)}
> >
<option value={1}>True</option> <option value={1}>True</option>
<option value={0}>False</option> <option value={0}>False</option>
</select> </select>
</InputGroup> </InputGroup>
<InputGroup> <InputGroup>
<label htmlFor='hideHeader'>Hide greeting and date</label> <label htmlFor="hideHeader">Hide greeting and date</label>
<select <select
id='hideHeader' id="hideHeader"
name='hideHeader' name="hideHeader"
value={formData.hideHeader} value={formData.hideHeader}
onChange={e => inputChangeHandler(e, true)} onChange={(e) => inputChangeHandler(e, true)}
> >
<option value={1}>True</option> <option value={1}>True</option>
<option value={0}>False</option> <option value={0}>False</option>
</select> </select>
</InputGroup> </InputGroup>
<InputGroup> <InputGroup>
<label htmlFor='hideApps'>Hide applications</label> <label htmlFor="hideApps">Hide applications</label>
<select <select
id='hideApps' id="hideApps"
name='hideApps' name="hideApps"
value={formData.hideApps} value={formData.hideApps}
onChange={e => inputChangeHandler(e, true)} onChange={(e) => inputChangeHandler(e, true)}
> >
<option value={1}>True</option> <option value={1}>True</option>
<option value={0}>False</option> <option value={0}>False</option>
</select> </select>
</InputGroup> </InputGroup>
<InputGroup> <InputGroup>
<label htmlFor='hideCategories'>Hide categories</label> <label htmlFor="hideCategories">Hide categories</label>
<select <select
id='hideCategories' id="hideCategories"
name='hideCategories' name="hideCategories"
value={formData.hideCategories} value={formData.hideCategories}
onChange={e => inputChangeHandler(e, true)} onChange={(e) => inputChangeHandler(e, true)}
> >
<option value={1}>True</option> <option value={1}>True</option>
<option value={0}>False</option> <option value={0}>False</option>
@ -274,26 +276,26 @@ const OtherSettings = (props: ComponentProps): JSX.Element => {
{/* DOCKER SETTINGS */} {/* DOCKER SETTINGS */}
<h2 className={classes.SettingsSection}>Docker</h2> <h2 className={classes.SettingsSection}>Docker</h2>
<InputGroup> <InputGroup>
<label htmlFor='dockerApps'>Use Docker API</label> <label htmlFor="dockerApps">Use Docker API</label>
<select <select
id='dockerApps' id="dockerApps"
name='dockerApps' name="dockerApps"
value={formData.dockerApps} value={formData.dockerApps}
onChange={e => inputChangeHandler(e, true)} onChange={(e) => inputChangeHandler(e, true)}
> >
<option value={1}>True</option> <option value={1}>True</option>
<option value={0}>False</option> <option value={0}>False</option>
</select> </select>
</InputGroup> </InputGroup>
<InputGroup> <InputGroup>
<label htmlFor='unpinStoppedApps'> <label htmlFor="unpinStoppedApps">
Unpin stopped containers / other apps Unpin stopped containers / other apps
</label> </label>
<select <select
id='unpinStoppedApps' id="unpinStoppedApps"
name='unpinStoppedApps' name="unpinStoppedApps"
value={formData.unpinStoppedApps} value={formData.unpinStoppedApps}
onChange={e => inputChangeHandler(e, true)} onChange={(e) => inputChangeHandler(e, true)}
> >
<option value={1}>True</option> <option value={1}>True</option>
<option value={0}>False</option> <option value={0}>False</option>
@ -303,12 +305,12 @@ const OtherSettings = (props: ComponentProps): JSX.Element => {
{/* KUBERNETES SETTINGS */} {/* KUBERNETES SETTINGS */}
<h2 className={classes.SettingsSection}>Kubernetes</h2> <h2 className={classes.SettingsSection}>Kubernetes</h2>
<InputGroup> <InputGroup>
<label htmlFor='kubernetesApps'>Use Kubernetes Ingress API</label> <label htmlFor="kubernetesApps">Use Kubernetes Ingress API</label>
<select <select
id='kubernetesApps' id="kubernetesApps"
name='kubernetesApps' name="kubernetesApps"
value={formData.kubernetesApps} value={formData.kubernetesApps}
onChange={e => inputChangeHandler(e, true)} onChange={(e) => inputChangeHandler(e, true)}
> >
<option value={1}>True</option> <option value={1}>True</option>
<option value={0}>False</option> <option value={0}>False</option>
@ -321,7 +323,7 @@ const OtherSettings = (props: ComponentProps): JSX.Element => {
const mapStateToProps = (state: GlobalState) => { const mapStateToProps = (state: GlobalState) => {
return { return {
loading: state.config.loading loading: state.config.loading,
}; };
}; };
@ -329,7 +331,7 @@ const actions = {
createNotification, createNotification,
updateConfig, updateConfig,
sortApps, sortApps,
sortCategories sortCategories,
}; };
export default connect(mapStateToProps, actions)(OtherSettings); export default connect(mapStateToProps, actions)(OtherSettings);

View file

@ -0,0 +1,8 @@
import { Query } from './Query';
export interface SearchResult {
isLocal: boolean;
sameTab: boolean;
search: string;
query: Query;
}

View file

@ -9,3 +9,4 @@ export * from './Notification';
export * from './Config'; export * from './Config';
export * from './Forms'; export * from './Forms';
export * from './Query'; export * from './Query';
export * from './SearchResult';

View file

@ -1,26 +1,44 @@
import { queries } from './searchQueries.json'; import { queries } from './searchQueries.json';
import { Query } from '../interfaces'; import { Query, SearchResult } from '../interfaces';
import { searchConfig } from '.'; import { searchConfig } from '.';
export const searchParser = (searchQuery: string): boolean => { export const searchParser = (searchQuery: string): SearchResult => {
const result: SearchResult = {
isLocal: false,
sameTab: false,
search: '',
query: {
name: '',
prefix: '',
template: '',
},
};
const splitQuery = searchQuery.match(/^\/([a-z]+)[ ](.+)$/i); const splitQuery = searchQuery.match(/^\/([a-z]+)[ ](.+)$/i);
const prefix = splitQuery ? splitQuery[1] : searchConfig('defaultSearchProvider', 'd');
const search = splitQuery ? encodeURIComponent(splitQuery[2]) : encodeURIComponent(searchQuery); const prefix = splitQuery
? splitQuery[1]
: searchConfig('defaultSearchProvider', 'l');
const search = splitQuery
? encodeURIComponent(splitQuery[2])
: encodeURIComponent(searchQuery);
const query = queries.find((q: Query) => q.prefix === prefix); const query = queries.find((q: Query) => q.prefix === prefix);
if (query) { if (query) {
const sameTab = searchConfig('searchSameTab', false); result.query = query;
result.search = search;
if (sameTab) { if (prefix === 'l') {
document.location.replace(`${query.template}${search}`); result.isLocal = true;
} else { } else {
window.open(`${query.template}${search}`); result.sameTab = searchConfig('searchSameTab', false);
} }
return true; return result;
} }
return false; return result;
} };

View file

@ -25,6 +25,11 @@
"prefix": "im", "prefix": "im",
"template": "https://www.imdb.com/find?q=" "template": "https://www.imdb.com/find?q="
}, },
{
"name": "Local search",
"prefix": "l",
"template": "#"
},
{ {
"name": "Reddit", "name": "Reddit",
"prefix": "r", "prefix": "r",

4
db.js
View file

@ -5,7 +5,7 @@ const logger = new Logger();
const sequelize = new Sequelize({ const sequelize = new Sequelize({
dialect: 'sqlite', dialect: 'sqlite',
storage: './data/db.sqlite', storage: './data/db.sqlite',
logging: false logging: false,
}); });
const connectDB = async () => { const connectDB = async () => {
@ -28,5 +28,5 @@ const connectDB = async () => {
module.exports = { module.exports = {
connectDB, connectDB,
sequelize sequelize,
}; };

4536
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -5,13 +5,13 @@
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {
"start": "node server.js", "start": "node server.js",
"init-server": "echo Instaling server dependencies && npm install && mkdir public && touch public/flame.css", "init:server": "echo Instaling server dependencies && npm install && mkdir public && touch public/flame.css",
"init-client": "cd client && echo Instaling client dependencies && npm install", "init:client": "cd client && echo Instaling client dependencies && npm install",
"dev-init": "npm run init-server && npm run init-client", "dev:init": "npm run init:server && npm run init:client",
"dev-server": "nodemon server.js", "dev:server": "nodemon server.js",
"dev-client": "npm start --prefix client", "dev:client": "npm start --prefix client",
"dev": "concurrently \"npm run dev-server\" \"npm run dev-client\"", "dev": "concurrently \"npm run dev:server\" \"npm run dev:client\"",
"skaffold": "concurrently \"npm run init-client\" \"npm run dev-server\"" "skaffold": "concurrently \"npm run init:client\" \"npm run dev:server\""
}, },
"author": "", "author": "",
"license": "ISC", "license": "ISC",

View file

@ -9,10 +9,10 @@ const initConfig = async () => {
const configPairs = await Config.findAll({ const configPairs = await Config.findAll({
where: { where: {
key: { key: {
[Op.or]: config.map(pair => pair.key) [Op.or]: config.map((pair) => pair.key),
} },
} },
}) });
// Get key from each pair // Get key from each pair
const configKeys = configPairs.map((pair) => pair.key); const configKeys = configPairs.map((pair) => pair.key);
@ -23,13 +23,13 @@ const initConfig = async () => {
await Config.create({ await Config.create({
key, key,
value, value,
valueType: typeof value valueType: typeof value,
}) });
} }
}) });
logger.log('Initial config created'); logger.log('Initial config created');
return; return;
} };
module.exports = initConfig; module.exports = initConfig;

View file

@ -62,7 +62,7 @@
}, },
{ {
"key": "defaultSearchProvider", "key": "defaultSearchProvider",
"value": "d" "value": "l"
}, },
{ {
"key": "dockerApps", "key": "dockerApps",