Merge pull request #84 from pawelmalak/local-search
Added searching (filtering) of local apps and bookmarks
This commit is contained in:
commit
43f38a2f44
17 changed files with 421 additions and 4819 deletions
|
@ -32,7 +32,7 @@ git clone https://github.com/pawelmalak/flame
|
|||
cd flame
|
||||
|
||||
# run only once
|
||||
npm run dev-init
|
||||
npm run dev:init
|
||||
|
||||
# start backend and frontend development servers
|
||||
npm run dev
|
||||
|
|
|
@ -7,6 +7,7 @@ import AppCard from '../AppCard/AppCard';
|
|||
interface ComponentProps {
|
||||
apps: App[];
|
||||
totalApps?: number;
|
||||
searching: boolean;
|
||||
}
|
||||
|
||||
const AppGrid = (props: ComponentProps): JSX.Element => {
|
||||
|
@ -16,26 +17,37 @@ const AppGrid = (props: ComponentProps): JSX.Element => {
|
|||
apps = (
|
||||
<div className={classes.AppGrid}>
|
||||
{props.apps.map((app: App): JSX.Element => {
|
||||
return <AppCard
|
||||
key={app.id}
|
||||
app={app}
|
||||
/>
|
||||
return <AppCard key={app.id} app={app} />;
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
);
|
||||
} else {
|
||||
if (props.totalApps) {
|
||||
apps = (
|
||||
<p className={classes.AppsMessage}>There are no pinned applications. You can pin them from the <Link to='/applications'>/applications</Link> menu</p>
|
||||
);
|
||||
if (props.searching) {
|
||||
apps = (
|
||||
<p className={classes.AppsMessage}>
|
||||
No apps match your search criteria
|
||||
</p>
|
||||
);
|
||||
} else {
|
||||
apps = (
|
||||
<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>
|
||||
<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;
|
||||
}
|
||||
};
|
||||
|
||||
export default AppGrid;
|
||||
export default AppGrid;
|
||||
|
|
|
@ -27,14 +27,11 @@ interface ComponentProps {
|
|||
getApps: Function;
|
||||
apps: App[];
|
||||
loading: boolean;
|
||||
searching: boolean;
|
||||
}
|
||||
|
||||
const Apps = (props: ComponentProps): JSX.Element => {
|
||||
const {
|
||||
getApps,
|
||||
apps,
|
||||
loading
|
||||
} = props;
|
||||
const { getApps, apps, loading, searching = false } = props;
|
||||
|
||||
const [modalIsOpen, setModalIsOpen] = useState(false);
|
||||
const [isInEdit, setIsInEdit] = useState(false);
|
||||
|
@ -47,8 +44,8 @@ const Apps = (props: ComponentProps): JSX.Element => {
|
|||
orderId: 0,
|
||||
id: 0,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date()
|
||||
})
|
||||
updatedAt: new Date(),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (apps.length === 0) {
|
||||
|
@ -59,63 +56,57 @@ const Apps = (props: ComponentProps): JSX.Element => {
|
|||
const toggleModal = (): void => {
|
||||
setModalIsOpen(!modalIsOpen);
|
||||
setIsInUpdate(false);
|
||||
}
|
||||
};
|
||||
|
||||
const toggleEdit = (): void => {
|
||||
setIsInEdit(!isInEdit);
|
||||
setIsInUpdate(false);
|
||||
}
|
||||
};
|
||||
|
||||
const toggleUpdate = (app: App): void => {
|
||||
setAppInUpdate(app);
|
||||
setIsInUpdate(true);
|
||||
setModalIsOpen(true);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<Modal isOpen={modalIsOpen} setIsOpen={setModalIsOpen}>
|
||||
{!isInUpdate
|
||||
? <AppForm modalHandler={toggleModal} />
|
||||
: <AppForm modalHandler={toggleModal} app={appInUpdate} />
|
||||
}
|
||||
{!isInUpdate ? (
|
||||
<AppForm modalHandler={toggleModal} />
|
||||
) : (
|
||||
<AppForm modalHandler={toggleModal} app={appInUpdate} />
|
||||
)}
|
||||
</Modal>
|
||||
|
||||
<Headline
|
||||
title='All Applications'
|
||||
subtitle={(<Link to='/'>Go back</Link>)}
|
||||
title="All Applications"
|
||||
subtitle={<Link to="/">Go back</Link>}
|
||||
/>
|
||||
|
||||
|
||||
<div className={classes.ActionsContainer}>
|
||||
<ActionButton
|
||||
name='Add'
|
||||
icon='mdiPlusBox'
|
||||
handler={toggleModal}
|
||||
/>
|
||||
<ActionButton
|
||||
name='Edit'
|
||||
icon='mdiPencil'
|
||||
handler={toggleEdit}
|
||||
/>
|
||||
<ActionButton name="Add" icon="mdiPlusBox" handler={toggleModal} />
|
||||
<ActionButton name="Edit" icon="mdiPencil" handler={toggleEdit} />
|
||||
</div>
|
||||
|
||||
<div className={classes.Apps}>
|
||||
{loading
|
||||
? <Spinner />
|
||||
: (!isInEdit
|
||||
? <AppGrid apps={apps} />
|
||||
: <AppTable updateAppHandler={toggleUpdate} />)
|
||||
}
|
||||
{loading ? (
|
||||
<Spinner />
|
||||
) : !isInEdit ? (
|
||||
<AppGrid apps={apps} searching />
|
||||
) : (
|
||||
<AppTable updateAppHandler={toggleUpdate} />
|
||||
)}
|
||||
</div>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const mapStateToProps = (state: GlobalState) => {
|
||||
return {
|
||||
apps: state.app.apps,
|
||||
loading: state.app.loading
|
||||
}
|
||||
}
|
||||
loading: state.app.loading,
|
||||
};
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps, { getApps })(Apps);
|
||||
export default connect(mapStateToProps, { getApps })(Apps);
|
||||
|
|
|
@ -9,30 +9,49 @@ import BookmarkCard from '../BookmarkCard/BookmarkCard';
|
|||
interface ComponentProps {
|
||||
categories: Category[];
|
||||
totalCategories?: number;
|
||||
searching: boolean;
|
||||
}
|
||||
|
||||
const BookmarkGrid = (props: ComponentProps): JSX.Element => {
|
||||
let bookmarks: JSX.Element;
|
||||
|
||||
if (props.categories.length > 0) {
|
||||
bookmarks = (
|
||||
<div className={classes.BookmarkGrid}>
|
||||
{props.categories.map((category: Category): JSX.Element => <BookmarkCard category={category} key={category.id} />)}
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
if (props.totalCategories) {
|
||||
if (props.searching && props.categories[0].bookmarks.length === 0) {
|
||||
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}>
|
||||
No bookmarks match your search criteria
|
||||
</p>
|
||||
);
|
||||
} else {
|
||||
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>
|
||||
<div className={classes.BookmarkGrid}>
|
||||
{props.categories.map(
|
||||
(category: Category): JSX.Element => (
|
||||
<BookmarkCard category={category} key={category.id} />
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
} else {
|
||||
if (props.totalCategories) {
|
||||
bookmarks = (
|
||||
<p className={classes.BookmarksMessage}>
|
||||
There are no pinned categories. You can pin them from the{' '}
|
||||
<Link to="/bookmarks">/bookmarks</Link> menu
|
||||
</p>
|
||||
);
|
||||
} else {
|
||||
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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return bookmarks;
|
||||
}
|
||||
};
|
||||
|
||||
export default BookmarkGrid;
|
||||
export default BookmarkGrid;
|
||||
|
|
|
@ -20,24 +20,23 @@ interface ComponentProps {
|
|||
loading: boolean;
|
||||
categories: Category[];
|
||||
getCategories: () => void;
|
||||
searching: boolean;
|
||||
}
|
||||
|
||||
export enum ContentType {
|
||||
category,
|
||||
bookmark
|
||||
bookmark,
|
||||
}
|
||||
|
||||
const Bookmarks = (props: ComponentProps): JSX.Element => {
|
||||
const {
|
||||
getCategories,
|
||||
categories,
|
||||
loading
|
||||
} = props;
|
||||
const { getCategories, categories, loading, searching = false } = props;
|
||||
|
||||
const [modalIsOpen, setModalIsOpen] = useState(false);
|
||||
const [formContentType, setFormContentType] = useState(ContentType.category);
|
||||
const [isInEdit, setIsInEdit] = useState(false);
|
||||
const [tableContentType, setTableContentType] = useState(ContentType.category);
|
||||
const [tableContentType, setTableContentType] = useState(
|
||||
ContentType.category
|
||||
);
|
||||
const [isInUpdate, setIsInUpdate] = useState(false);
|
||||
const [categoryInUpdate, setCategoryInUpdate] = useState<Category>({
|
||||
name: '',
|
||||
|
@ -46,8 +45,8 @@ const Bookmarks = (props: ComponentProps): JSX.Element => {
|
|||
orderId: 0,
|
||||
bookmarks: [],
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date()
|
||||
})
|
||||
updatedAt: new Date(),
|
||||
});
|
||||
const [bookmarkInUpdate, setBookmarkInUpdate] = useState<Bookmark>({
|
||||
name: '',
|
||||
url: '',
|
||||
|
@ -55,24 +54,24 @@ const Bookmarks = (props: ComponentProps): JSX.Element => {
|
|||
icon: '',
|
||||
id: -1,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date()
|
||||
})
|
||||
updatedAt: new Date(),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (categories.length === 0) {
|
||||
getCategories();
|
||||
}
|
||||
}, [getCategories])
|
||||
}, [getCategories]);
|
||||
|
||||
const toggleModal = (): void => {
|
||||
setModalIsOpen(!modalIsOpen);
|
||||
}
|
||||
};
|
||||
|
||||
const addActionHandler = (contentType: ContentType) => {
|
||||
setFormContentType(contentType);
|
||||
setIsInUpdate(false);
|
||||
toggleModal();
|
||||
}
|
||||
};
|
||||
|
||||
const editActionHandler = (contentType: ContentType) => {
|
||||
// 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);
|
||||
setTableContentType(contentType);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const instanceOfCategory = (object: any): object is Category => {
|
||||
return 'bookmarks' in object;
|
||||
}
|
||||
};
|
||||
|
||||
const goToUpdateMode = (data: Category | Bookmark): void => {
|
||||
setIsInUpdate(true);
|
||||
|
@ -98,67 +97,76 @@ const Bookmarks = (props: ComponentProps): JSX.Element => {
|
|||
setBookmarkInUpdate(data);
|
||||
}
|
||||
toggleModal();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<Modal isOpen={modalIsOpen} setIsOpen={toggleModal}>
|
||||
{!isInUpdate
|
||||
? <BookmarkForm modalHandler={toggleModal} contentType={formContentType} />
|
||||
: formContentType === ContentType.category
|
||||
? <BookmarkForm modalHandler={toggleModal} contentType={formContentType} category={categoryInUpdate} />
|
||||
: <BookmarkForm modalHandler={toggleModal} contentType={formContentType} bookmark={bookmarkInUpdate} />
|
||||
}
|
||||
{!isInUpdate ? (
|
||||
<BookmarkForm
|
||||
modalHandler={toggleModal}
|
||||
contentType={formContentType}
|
||||
/>
|
||||
) : formContentType === ContentType.category ? (
|
||||
<BookmarkForm
|
||||
modalHandler={toggleModal}
|
||||
contentType={formContentType}
|
||||
category={categoryInUpdate}
|
||||
/>
|
||||
) : (
|
||||
<BookmarkForm
|
||||
modalHandler={toggleModal}
|
||||
contentType={formContentType}
|
||||
bookmark={bookmarkInUpdate}
|
||||
/>
|
||||
)}
|
||||
</Modal>
|
||||
|
||||
<Headline
|
||||
title='All Bookmarks'
|
||||
subtitle={(<Link to='/'>Go back</Link>)}
|
||||
/>
|
||||
|
||||
<Headline title="All Bookmarks" subtitle={<Link to="/">Go back</Link>} />
|
||||
|
||||
<div className={classes.ActionsContainer}>
|
||||
<ActionButton
|
||||
name='Add Category'
|
||||
icon='mdiPlusBox'
|
||||
name="Add Category"
|
||||
icon="mdiPlusBox"
|
||||
handler={() => addActionHandler(ContentType.category)}
|
||||
/>
|
||||
<ActionButton
|
||||
name='Add Bookmark'
|
||||
icon='mdiPlusBox'
|
||||
name="Add Bookmark"
|
||||
icon="mdiPlusBox"
|
||||
handler={() => addActionHandler(ContentType.bookmark)}
|
||||
/>
|
||||
<ActionButton
|
||||
name='Edit Categories'
|
||||
icon='mdiPencil'
|
||||
name="Edit Categories"
|
||||
icon="mdiPencil"
|
||||
handler={() => editActionHandler(ContentType.category)}
|
||||
/>
|
||||
<ActionButton
|
||||
name='Edit Bookmarks'
|
||||
icon='mdiPencil'
|
||||
name="Edit Bookmarks"
|
||||
icon="mdiPencil"
|
||||
handler={() => editActionHandler(ContentType.bookmark)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{loading
|
||||
? <Spinner />
|
||||
: (!isInEdit
|
||||
? <BookmarkGrid categories={categories} />
|
||||
: <BookmarkTable
|
||||
contentType={tableContentType}
|
||||
categories={categories}
|
||||
updateHandler={goToUpdateMode}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{loading ? (
|
||||
<Spinner />
|
||||
) : !isInEdit ? (
|
||||
<BookmarkGrid categories={categories} searching />
|
||||
) : (
|
||||
<BookmarkTable
|
||||
contentType={tableContentType}
|
||||
categories={categories}
|
||||
updateHandler={goToUpdateMode}
|
||||
/>
|
||||
)}
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const mapStateToProps = (state: GlobalState) => {
|
||||
return {
|
||||
loading: state.bookmark.loading,
|
||||
categories: state.bookmark.categories
|
||||
}
|
||||
}
|
||||
categories: state.bookmark.categories,
|
||||
};
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps, { getCategories })(Bookmarks);
|
||||
export default connect(mapStateToProps, { getCategories })(Bookmarks);
|
||||
|
|
|
@ -47,13 +47,16 @@ const Home = (props: ComponentProps): JSX.Element => {
|
|||
appsLoading,
|
||||
getCategories,
|
||||
categories,
|
||||
categoriesLoading
|
||||
categoriesLoading,
|
||||
} = props;
|
||||
|
||||
const [header, setHeader] = useState({
|
||||
dateTime: dateTime(),
|
||||
greeting: greeter()
|
||||
})
|
||||
greeting: greeter(),
|
||||
});
|
||||
|
||||
// Local search query
|
||||
const [localSearch, setLocalSearch] = useState<null | string>(null);
|
||||
|
||||
// Load applications
|
||||
useEffect(() => {
|
||||
|
@ -78,78 +81,108 @@ const Home = (props: ComponentProps): JSX.Element => {
|
|||
interval = setInterval(() => {
|
||||
setHeader({
|
||||
dateTime: dateTime(),
|
||||
greeting: greeter()
|
||||
})
|
||||
greeting: greeter(),
|
||||
});
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
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 (
|
||||
<Container>
|
||||
{searchConfig('hideSearch', 0) !== 1
|
||||
? <SearchBar />
|
||||
: <div></div>
|
||||
}
|
||||
{searchConfig('hideSearch', 0) !== 1 ? (
|
||||
<SearchBar setLocalSearch={setLocalSearch} />
|
||||
) : (
|
||||
<div></div>
|
||||
)}
|
||||
|
||||
{searchConfig('hideHeader', 0) !== 1
|
||||
? (
|
||||
<header className={classes.Header}>
|
||||
<p>{header.dateTime}</p>
|
||||
<Link to='/settings' className={classes.SettingsLink}>Go to Settings</Link>
|
||||
<span className={classes.HeaderMain}>
|
||||
<h1>{header.greeting}</h1>
|
||||
<WeatherWidget />
|
||||
</span>
|
||||
</header>
|
||||
)
|
||||
: <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}
|
||||
/>
|
||||
}
|
||||
<div className={classes.HomeSpace}></div>
|
||||
</Fragment>)
|
||||
: <div></div>
|
||||
}
|
||||
{searchConfig('hideHeader', 0) !== 1 ? (
|
||||
<header className={classes.Header}>
|
||||
<p>{header.dateTime}</p>
|
||||
<Link to="/settings" className={classes.SettingsLink}>
|
||||
Go to Settings
|
||||
</Link>
|
||||
<span className={classes.HeaderMain}>
|
||||
<h1>{header.greeting}</h1>
|
||||
<WeatherWidget />
|
||||
</span>
|
||||
</header>
|
||||
) : (
|
||||
<div></div>
|
||||
)}
|
||||
|
||||
{searchConfig('hideCategories', 0) !== 1
|
||||
? (<Fragment>
|
||||
<SectionHeadline title='Bookmarks' link='/bookmarks' />
|
||||
{categoriesLoading
|
||||
? <Spinner />
|
||||
: <BookmarkGrid
|
||||
categories={categories.filter((category: Category) => category.isPinned)}
|
||||
totalCategories={categories.length}
|
||||
/>
|
||||
}
|
||||
</Fragment>)
|
||||
: <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)
|
||||
)
|
||||
}
|
||||
totalApps={apps.length}
|
||||
searching={!!localSearch}
|
||||
/>
|
||||
)}
|
||||
<div className={classes.HomeSpace}></div>
|
||||
</Fragment>
|
||||
) : (
|
||||
<div></div>
|
||||
)}
|
||||
|
||||
<Link to='/settings' className={classes.SettingsButton}>
|
||||
<Icon icon='mdiCog' color='var(--color-background)' />
|
||||
{searchConfig('hideCategories', 0) !== 1 ? (
|
||||
<Fragment>
|
||||
<SectionHeadline title="Bookmarks" link="/bookmarks" />
|
||||
{categoriesLoading ? (
|
||||
<Spinner />
|
||||
) : (
|
||||
<BookmarkGrid
|
||||
categories={
|
||||
!localSearch
|
||||
? categories.filter(({ isPinned }) => isPinned)
|
||||
: searchBookmarks(localSearch)
|
||||
}
|
||||
totalCategories={categories.length}
|
||||
searching={!!localSearch}
|
||||
/>
|
||||
)}
|
||||
</Fragment>
|
||||
) : (
|
||||
<div></div>
|
||||
)}
|
||||
|
||||
<Link to="/settings" className={classes.SettingsButton}>
|
||||
<Icon icon="mdiCog" color="var(--color-background)" />
|
||||
</Link>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const mapStateToProps = (state: GlobalState) => {
|
||||
return {
|
||||
appsLoading: state.app.loading,
|
||||
apps: state.app.apps,
|
||||
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);
|
||||
|
|
|
@ -15,36 +15,53 @@ import { searchParser } from '../../utility';
|
|||
|
||||
interface ComponentProps {
|
||||
createNotification: (notification: NewNotification) => void;
|
||||
setLocalSearch: (query: string) => void;
|
||||
}
|
||||
|
||||
const SearchBar = (props: ComponentProps): JSX.Element => {
|
||||
const { setLocalSearch, createNotification } = props;
|
||||
|
||||
const inputRef = useRef<HTMLInputElement>(document.createElement('input'));
|
||||
|
||||
useEffect(() => {
|
||||
inputRef.current.focus();
|
||||
}, [])
|
||||
}, []);
|
||||
|
||||
const searchHandler = (e: KeyboardEvent<HTMLInputElement>) => {
|
||||
if (e.code === 'Enter') {
|
||||
const prefixFound = searchParser(inputRef.current.value);
|
||||
const searchResult = searchParser(inputRef.current.value);
|
||||
|
||||
if (!prefixFound) {
|
||||
props.createNotification({
|
||||
if (searchResult.isLocal) {
|
||||
setLocalSearch(searchResult.search);
|
||||
}
|
||||
|
||||
if (e.code === 'Enter') {
|
||||
if (!searchResult.query.prefix) {
|
||||
createNotification({
|
||||
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 (
|
||||
<input
|
||||
ref={inputRef}
|
||||
type='text'
|
||||
type="text"
|
||||
className={classes.SearchBar}
|
||||
onKeyDown={(e) => searchHandler(e)}
|
||||
onKeyUp={(e) => searchHandler(e)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export default connect(null, { createNotification })(SearchBar);
|
||||
export default connect(null, { createNotification })(SearchBar);
|
||||
|
|
|
@ -6,7 +6,7 @@ import {
|
|||
createNotification,
|
||||
updateConfig,
|
||||
sortApps,
|
||||
sortCategories
|
||||
sortCategories,
|
||||
} from '../../../store/actions';
|
||||
|
||||
// Typescript
|
||||
|
@ -14,7 +14,7 @@ import {
|
|||
GlobalState,
|
||||
NewNotification,
|
||||
Query,
|
||||
SettingsForm
|
||||
SettingsForm,
|
||||
} from '../../../interfaces';
|
||||
|
||||
// UI
|
||||
|
@ -53,7 +53,7 @@ const OtherSettings = (props: ComponentProps): JSX.Element => {
|
|||
searchSameTab: 0,
|
||||
dockerApps: 1,
|
||||
kubernetesApps: 1,
|
||||
unpinStoppedApps: 1
|
||||
unpinStoppedApps: 1,
|
||||
});
|
||||
|
||||
// Get config
|
||||
|
@ -73,7 +73,7 @@ const OtherSettings = (props: ComponentProps): JSX.Element => {
|
|||
searchSameTab: searchConfig('searchSameTab', 0),
|
||||
dockerApps: searchConfig('dockerApps', 0),
|
||||
kubernetesApps: searchConfig('kubernetesApps', 0),
|
||||
unpinStoppedApps: searchConfig('unpinStoppedApps', 0)
|
||||
unpinStoppedApps: searchConfig('unpinStoppedApps', 0),
|
||||
});
|
||||
}, [props.loading]);
|
||||
|
||||
|
@ -105,115 +105,117 @@ const OtherSettings = (props: ComponentProps): JSX.Element => {
|
|||
|
||||
setFormData({
|
||||
...formData,
|
||||
[e.target.name]: value
|
||||
[e.target.name]: value,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={e => formSubmitHandler(e)}>
|
||||
<form onSubmit={(e) => formSubmitHandler(e)}>
|
||||
{/* OTHER OPTIONS */}
|
||||
<h2 className={classes.SettingsSection}>Miscellaneous</h2>
|
||||
<InputGroup>
|
||||
<label htmlFor='customTitle'>Custom page title</label>
|
||||
<label htmlFor="customTitle">Custom page title</label>
|
||||
<input
|
||||
type='text'
|
||||
id='customTitle'
|
||||
name='customTitle'
|
||||
placeholder='Flame'
|
||||
type="text"
|
||||
id="customTitle"
|
||||
name="customTitle"
|
||||
placeholder="Flame"
|
||||
value={formData.customTitle}
|
||||
onChange={e => inputChangeHandler(e)}
|
||||
onChange={(e) => inputChangeHandler(e)}
|
||||
/>
|
||||
</InputGroup>
|
||||
|
||||
{/* BEAHVIOR OPTIONS */}
|
||||
<h2 className={classes.SettingsSection}>App Behavior</h2>
|
||||
<InputGroup>
|
||||
<label htmlFor='pinAppsByDefault'>
|
||||
<label htmlFor="pinAppsByDefault">
|
||||
Pin new applications by default
|
||||
</label>
|
||||
<select
|
||||
id='pinAppsByDefault'
|
||||
name='pinAppsByDefault'
|
||||
id="pinAppsByDefault"
|
||||
name="pinAppsByDefault"
|
||||
value={formData.pinAppsByDefault}
|
||||
onChange={e => inputChangeHandler(e, true)}
|
||||
onChange={(e) => inputChangeHandler(e, true)}
|
||||
>
|
||||
<option value={1}>True</option>
|
||||
<option value={0}>False</option>
|
||||
</select>
|
||||
</InputGroup>
|
||||
<InputGroup>
|
||||
<label htmlFor='pinCategoriesByDefault'>
|
||||
<label htmlFor="pinCategoriesByDefault">
|
||||
Pin new categories by default
|
||||
</label>
|
||||
<select
|
||||
id='pinCategoriesByDefault'
|
||||
name='pinCategoriesByDefault'
|
||||
id="pinCategoriesByDefault"
|
||||
name="pinCategoriesByDefault"
|
||||
value={formData.pinCategoriesByDefault}
|
||||
onChange={e => inputChangeHandler(e, true)}
|
||||
onChange={(e) => inputChangeHandler(e, true)}
|
||||
>
|
||||
<option value={1}>True</option>
|
||||
<option value={0}>False</option>
|
||||
</select>
|
||||
</InputGroup>
|
||||
<InputGroup>
|
||||
<label htmlFor='useOrdering'>Sorting type</label>
|
||||
<label htmlFor="useOrdering">Sorting type</label>
|
||||
<select
|
||||
id='useOrdering'
|
||||
name='useOrdering'
|
||||
id="useOrdering"
|
||||
name="useOrdering"
|
||||
value={formData.useOrdering}
|
||||
onChange={e => inputChangeHandler(e)}
|
||||
onChange={(e) => inputChangeHandler(e)}
|
||||
>
|
||||
<option value='createdAt'>By creation date</option>
|
||||
<option value='name'>Alphabetical order</option>
|
||||
<option value='orderId'>Custom order</option>
|
||||
<option value="createdAt">By creation date</option>
|
||||
<option value="name">Alphabetical order</option>
|
||||
<option value="orderId">Custom order</option>
|
||||
</select>
|
||||
</InputGroup>
|
||||
<InputGroup>
|
||||
<label htmlFor='defaultSearchProvider'>Default Search Provider</label>
|
||||
<label htmlFor="defaultSearchProvider">Default Search Provider</label>
|
||||
<select
|
||||
id='defaultSearchProvider'
|
||||
name='defaultSearchProvider'
|
||||
id="defaultSearchProvider"
|
||||
name="defaultSearchProvider"
|
||||
value={formData.defaultSearchProvider}
|
||||
onChange={e => inputChangeHandler(e)}
|
||||
onChange={(e) => inputChangeHandler(e)}
|
||||
>
|
||||
{queries.map((query: Query) => (
|
||||
<option value={query.prefix}>{query.name}</option>
|
||||
{queries.map((query: Query, idx) => (
|
||||
<option key={idx} value={query.prefix}>
|
||||
{query.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</InputGroup>
|
||||
<InputGroup>
|
||||
<label htmlFor='searchSameTab'>
|
||||
<label htmlFor="searchSameTab">
|
||||
Open search results in the same tab
|
||||
</label>
|
||||
<select
|
||||
id='searchSameTab'
|
||||
name='searchSameTab'
|
||||
id="searchSameTab"
|
||||
name="searchSameTab"
|
||||
value={formData.searchSameTab}
|
||||
onChange={e => inputChangeHandler(e, true)}
|
||||
onChange={(e) => inputChangeHandler(e, true)}
|
||||
>
|
||||
<option value={1}>True</option>
|
||||
<option value={0}>False</option>
|
||||
</select>
|
||||
</InputGroup>
|
||||
<InputGroup>
|
||||
<label htmlFor='appsSameTab'>Open applications in the same tab</label>
|
||||
<label htmlFor="appsSameTab">Open applications in the same tab</label>
|
||||
<select
|
||||
id='appsSameTab'
|
||||
name='appsSameTab'
|
||||
id="appsSameTab"
|
||||
name="appsSameTab"
|
||||
value={formData.appsSameTab}
|
||||
onChange={e => inputChangeHandler(e, true)}
|
||||
onChange={(e) => inputChangeHandler(e, true)}
|
||||
>
|
||||
<option value={1}>True</option>
|
||||
<option value={0}>False</option>
|
||||
</select>
|
||||
</InputGroup>
|
||||
<InputGroup>
|
||||
<label htmlFor='bookmarksSameTab'>Open bookmarks in the same tab</label>
|
||||
<label htmlFor="bookmarksSameTab">Open bookmarks in the same tab</label>
|
||||
<select
|
||||
id='bookmarksSameTab'
|
||||
name='bookmarksSameTab'
|
||||
id="bookmarksSameTab"
|
||||
name="bookmarksSameTab"
|
||||
value={formData.bookmarksSameTab}
|
||||
onChange={e => inputChangeHandler(e, true)}
|
||||
onChange={(e) => inputChangeHandler(e, true)}
|
||||
>
|
||||
<option value={1}>True</option>
|
||||
<option value={0}>False</option>
|
||||
|
@ -223,48 +225,48 @@ const OtherSettings = (props: ComponentProps): JSX.Element => {
|
|||
{/* MODULES OPTIONS */}
|
||||
<h2 className={classes.SettingsSection}>Modules</h2>
|
||||
<InputGroup>
|
||||
<label htmlFor='hideSearch'>Hide search bar</label>
|
||||
<label htmlFor="hideSearch">Hide search bar</label>
|
||||
<select
|
||||
id='hideSearch'
|
||||
name='hideSearch'
|
||||
id="hideSearch"
|
||||
name="hideSearch"
|
||||
value={formData.hideSearch}
|
||||
onChange={e => inputChangeHandler(e, true)}
|
||||
onChange={(e) => inputChangeHandler(e, true)}
|
||||
>
|
||||
<option value={1}>True</option>
|
||||
<option value={0}>False</option>
|
||||
</select>
|
||||
</InputGroup>
|
||||
<InputGroup>
|
||||
<label htmlFor='hideHeader'>Hide greeting and date</label>
|
||||
<label htmlFor="hideHeader">Hide greeting and date</label>
|
||||
<select
|
||||
id='hideHeader'
|
||||
name='hideHeader'
|
||||
id="hideHeader"
|
||||
name="hideHeader"
|
||||
value={formData.hideHeader}
|
||||
onChange={e => inputChangeHandler(e, true)}
|
||||
onChange={(e) => inputChangeHandler(e, true)}
|
||||
>
|
||||
<option value={1}>True</option>
|
||||
<option value={0}>False</option>
|
||||
</select>
|
||||
</InputGroup>
|
||||
<InputGroup>
|
||||
<label htmlFor='hideApps'>Hide applications</label>
|
||||
<label htmlFor="hideApps">Hide applications</label>
|
||||
<select
|
||||
id='hideApps'
|
||||
name='hideApps'
|
||||
id="hideApps"
|
||||
name="hideApps"
|
||||
value={formData.hideApps}
|
||||
onChange={e => inputChangeHandler(e, true)}
|
||||
onChange={(e) => inputChangeHandler(e, true)}
|
||||
>
|
||||
<option value={1}>True</option>
|
||||
<option value={0}>False</option>
|
||||
</select>
|
||||
</InputGroup>
|
||||
<InputGroup>
|
||||
<label htmlFor='hideCategories'>Hide categories</label>
|
||||
<label htmlFor="hideCategories">Hide categories</label>
|
||||
<select
|
||||
id='hideCategories'
|
||||
name='hideCategories'
|
||||
id="hideCategories"
|
||||
name="hideCategories"
|
||||
value={formData.hideCategories}
|
||||
onChange={e => inputChangeHandler(e, true)}
|
||||
onChange={(e) => inputChangeHandler(e, true)}
|
||||
>
|
||||
<option value={1}>True</option>
|
||||
<option value={0}>False</option>
|
||||
|
@ -274,26 +276,26 @@ const OtherSettings = (props: ComponentProps): JSX.Element => {
|
|||
{/* DOCKER SETTINGS */}
|
||||
<h2 className={classes.SettingsSection}>Docker</h2>
|
||||
<InputGroup>
|
||||
<label htmlFor='dockerApps'>Use Docker API</label>
|
||||
<label htmlFor="dockerApps">Use Docker API</label>
|
||||
<select
|
||||
id='dockerApps'
|
||||
name='dockerApps'
|
||||
id="dockerApps"
|
||||
name="dockerApps"
|
||||
value={formData.dockerApps}
|
||||
onChange={e => inputChangeHandler(e, true)}
|
||||
onChange={(e) => inputChangeHandler(e, true)}
|
||||
>
|
||||
<option value={1}>True</option>
|
||||
<option value={0}>False</option>
|
||||
</select>
|
||||
</InputGroup>
|
||||
<InputGroup>
|
||||
<label htmlFor='unpinStoppedApps'>
|
||||
<label htmlFor="unpinStoppedApps">
|
||||
Unpin stopped containers / other apps
|
||||
</label>
|
||||
<select
|
||||
id='unpinStoppedApps'
|
||||
name='unpinStoppedApps'
|
||||
id="unpinStoppedApps"
|
||||
name="unpinStoppedApps"
|
||||
value={formData.unpinStoppedApps}
|
||||
onChange={e => inputChangeHandler(e, true)}
|
||||
onChange={(e) => inputChangeHandler(e, true)}
|
||||
>
|
||||
<option value={1}>True</option>
|
||||
<option value={0}>False</option>
|
||||
|
@ -303,12 +305,12 @@ const OtherSettings = (props: ComponentProps): JSX.Element => {
|
|||
{/* KUBERNETES SETTINGS */}
|
||||
<h2 className={classes.SettingsSection}>Kubernetes</h2>
|
||||
<InputGroup>
|
||||
<label htmlFor='kubernetesApps'>Use Kubernetes Ingress API</label>
|
||||
<label htmlFor="kubernetesApps">Use Kubernetes Ingress API</label>
|
||||
<select
|
||||
id='kubernetesApps'
|
||||
name='kubernetesApps'
|
||||
id="kubernetesApps"
|
||||
name="kubernetesApps"
|
||||
value={formData.kubernetesApps}
|
||||
onChange={e => inputChangeHandler(e, true)}
|
||||
onChange={(e) => inputChangeHandler(e, true)}
|
||||
>
|
||||
<option value={1}>True</option>
|
||||
<option value={0}>False</option>
|
||||
|
@ -321,7 +323,7 @@ const OtherSettings = (props: ComponentProps): JSX.Element => {
|
|||
|
||||
const mapStateToProps = (state: GlobalState) => {
|
||||
return {
|
||||
loading: state.config.loading
|
||||
loading: state.config.loading,
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -329,7 +331,7 @@ const actions = {
|
|||
createNotification,
|
||||
updateConfig,
|
||||
sortApps,
|
||||
sortCategories
|
||||
sortCategories,
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps, actions)(OtherSettings);
|
||||
|
|
8
client/src/interfaces/SearchResult.ts
Normal file
8
client/src/interfaces/SearchResult.ts
Normal file
|
@ -0,0 +1,8 @@
|
|||
import { Query } from './Query';
|
||||
|
||||
export interface SearchResult {
|
||||
isLocal: boolean;
|
||||
sameTab: boolean;
|
||||
search: string;
|
||||
query: Query;
|
||||
}
|
|
@ -8,4 +8,5 @@ export * from './Category';
|
|||
export * from './Notification';
|
||||
export * from './Config';
|
||||
export * from './Forms';
|
||||
export * from './Query';
|
||||
export * from './Query';
|
||||
export * from './SearchResult';
|
||||
|
|
|
@ -1,26 +1,44 @@
|
|||
import { queries } from './searchQueries.json';
|
||||
import { Query } from '../interfaces';
|
||||
import { Query, SearchResult } from '../interfaces';
|
||||
|
||||
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 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);
|
||||
|
||||
if (query) {
|
||||
const sameTab = searchConfig('searchSameTab', false);
|
||||
result.query = query;
|
||||
result.search = search;
|
||||
|
||||
if (sameTab) {
|
||||
document.location.replace(`${query.template}${search}`);
|
||||
if (prefix === 'l') {
|
||||
result.isLocal = true;
|
||||
} else {
|
||||
window.open(`${query.template}${search}`);
|
||||
result.sameTab = searchConfig('searchSameTab', false);
|
||||
}
|
||||
|
||||
return true;
|
||||
return result;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
|
|
@ -25,6 +25,11 @@
|
|||
"prefix": "im",
|
||||
"template": "https://www.imdb.com/find?q="
|
||||
},
|
||||
{
|
||||
"name": "Local search",
|
||||
"prefix": "l",
|
||||
"template": "#"
|
||||
},
|
||||
{
|
||||
"name": "Reddit",
|
||||
"prefix": "r",
|
||||
|
|
4
db.js
4
db.js
|
@ -5,7 +5,7 @@ const logger = new Logger();
|
|||
const sequelize = new Sequelize({
|
||||
dialect: 'sqlite',
|
||||
storage: './data/db.sqlite',
|
||||
logging: false
|
||||
logging: false,
|
||||
});
|
||||
|
||||
const connectDB = async () => {
|
||||
|
@ -28,5 +28,5 @@ const connectDB = async () => {
|
|||
|
||||
module.exports = {
|
||||
connectDB,
|
||||
sequelize
|
||||
sequelize,
|
||||
};
|
||||
|
|
4536
package-lock.json
generated
4536
package-lock.json
generated
File diff suppressed because it is too large
Load diff
14
package.json
14
package.json
|
@ -5,13 +5,13 @@
|
|||
"main": "index.js",
|
||||
"scripts": {
|
||||
"start": "node server.js",
|
||||
"init-server": "echo Instaling server dependencies && npm install && mkdir public && touch public/flame.css",
|
||||
"init-client": "cd client && echo Instaling client dependencies && npm install",
|
||||
"dev-init": "npm run init-server && npm run init-client",
|
||||
"dev-server": "nodemon server.js",
|
||||
"dev-client": "npm start --prefix client",
|
||||
"dev": "concurrently \"npm run dev-server\" \"npm run dev-client\"",
|
||||
"skaffold": "concurrently \"npm run init-client\" \"npm run dev-server\""
|
||||
"init:server": "echo Instaling server dependencies && npm install && mkdir public && touch public/flame.css",
|
||||
"init:client": "cd client && echo Instaling client dependencies && npm install",
|
||||
"dev:init": "npm run init:server && npm run init:client",
|
||||
"dev:server": "nodemon server.js",
|
||||
"dev:client": "npm start --prefix client",
|
||||
"dev": "concurrently \"npm run dev:server\" \"npm run dev:client\"",
|
||||
"skaffold": "concurrently \"npm run init:client\" \"npm run dev:server\""
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
|
|
|
@ -9,27 +9,27 @@ const initConfig = async () => {
|
|||
const configPairs = await Config.findAll({
|
||||
where: {
|
||||
key: {
|
||||
[Op.or]: config.map(pair => pair.key)
|
||||
}
|
||||
}
|
||||
})
|
||||
[Op.or]: config.map((pair) => pair.key),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Get key from each pair
|
||||
const configKeys = configPairs.map((pair) => pair.key);
|
||||
|
||||
// Create missing pairs
|
||||
config.forEach(async ({ key, value}) => {
|
||||
config.forEach(async ({ key, value }) => {
|
||||
if (!configKeys.includes(key)) {
|
||||
await Config.create({
|
||||
key,
|
||||
value,
|
||||
valueType: typeof value
|
||||
})
|
||||
valueType: typeof value,
|
||||
});
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
logger.log('Initial config created');
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = initConfig;
|
||||
module.exports = initConfig;
|
||||
|
|
|
@ -62,7 +62,7 @@
|
|||
},
|
||||
{
|
||||
"key": "defaultSearchProvider",
|
||||
"value": "d"
|
||||
"value": "l"
|
||||
},
|
||||
{
|
||||
"key": "dockerApps",
|
||||
|
|
Loading…
Reference in a new issue