diff --git a/README.md b/README.md index dadb428..669b7b0 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/client/src/components/Apps/AppGrid/AppGrid.tsx b/client/src/components/Apps/AppGrid/AppGrid.tsx index cacc19c..30d5c8c 100644 --- a/client/src/components/Apps/AppGrid/AppGrid.tsx +++ b/client/src/components/Apps/AppGrid/AppGrid.tsx @@ -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 = (
{props.apps.map((app: App): JSX.Element => { - return + return ; })}
- ) + ); } else { if (props.totalApps) { - apps = ( -

There are no pinned applications. You can pin them from the /applications menu

- ); + if (props.searching) { + apps = ( +

+ No apps match your search criteria +

+ ); + } else { + apps = ( +

+ There are no pinned applications. You can pin them from the{' '} + /applications menu +

+ ); + } } else { apps = ( -

You don't have any applications. You can add a new one from /applications menu

+

+ You don't have any applications. You can add a new one from{' '} + /applications menu +

); } } return apps; -} +}; -export default AppGrid; \ No newline at end of file +export default AppGrid; diff --git a/client/src/components/Apps/Apps.tsx b/client/src/components/Apps/Apps.tsx index 88c3fff..751a196 100644 --- a/client/src/components/Apps/Apps.tsx +++ b/client/src/components/Apps/Apps.tsx @@ -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 ( - {!isInUpdate - ? - : - } + {!isInUpdate ? ( + + ) : ( + + )} Go back)} + title="All Applications" + subtitle={Go back} /> - +
- - + +
- {loading - ? - : (!isInEdit - ? - : ) - } + {loading ? ( + + ) : !isInEdit ? ( + + ) : ( + + )}
- ) -} + ); +}; const mapStateToProps = (state: GlobalState) => { return { apps: state.app.apps, - loading: state.app.loading - } -} + loading: state.app.loading, + }; +}; -export default connect(mapStateToProps, { getApps })(Apps); \ No newline at end of file +export default connect(mapStateToProps, { getApps })(Apps); diff --git a/client/src/components/Bookmarks/BookmarkGrid/BookmarkGrid.tsx b/client/src/components/Bookmarks/BookmarkGrid/BookmarkGrid.tsx index c316f31..bf17c81 100644 --- a/client/src/components/Bookmarks/BookmarkGrid/BookmarkGrid.tsx +++ b/client/src/components/Bookmarks/BookmarkGrid/BookmarkGrid.tsx @@ -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 = ( -
- {props.categories.map((category: Category): JSX.Element => )} -
- ); - } else { - if (props.totalCategories) { + if (props.searching && props.categories[0].bookmarks.length === 0) { bookmarks = ( -

There are no pinned categories. You can pin them from the /bookmarks menu

+

+ No bookmarks match your search criteria +

); } else { bookmarks = ( -

You don't have any bookmarks. You can add a new one from /bookmarks menu

+
+ {props.categories.map( + (category: Category): JSX.Element => ( + + ) + )} +
+ ); + } + } else { + if (props.totalCategories) { + bookmarks = ( +

+ There are no pinned categories. You can pin them from the{' '} + /bookmarks menu +

+ ); + } else { + bookmarks = ( +

+ You don't have any bookmarks. You can add a new one from{' '} + /bookmarks menu +

); } } return bookmarks; -} +}; -export default BookmarkGrid; \ No newline at end of file +export default BookmarkGrid; diff --git a/client/src/components/Bookmarks/Bookmarks.tsx b/client/src/components/Bookmarks/Bookmarks.tsx index 7a2deb2..88d9cdb 100644 --- a/client/src/components/Bookmarks/Bookmarks.tsx +++ b/client/src/components/Bookmarks/Bookmarks.tsx @@ -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({ 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({ 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 ( - {!isInUpdate - ? - : formContentType === ContentType.category - ? - : - } + {!isInUpdate ? ( + + ) : formContentType === ContentType.category ? ( + + ) : ( + + )} - Go back)} - /> - + Go back} /> +
addActionHandler(ContentType.category)} /> addActionHandler(ContentType.bookmark)} /> editActionHandler(ContentType.category)} /> editActionHandler(ContentType.bookmark)} />
- {loading - ? - : (!isInEdit - ? - : - ) - } + {loading ? ( + + ) : !isInEdit ? ( + + ) : ( + + )}
- ) -} + ); +}; const mapStateToProps = (state: GlobalState) => { return { loading: state.bookmark.loading, - categories: state.bookmark.categories - } -} + categories: state.bookmark.categories, + }; +}; -export default connect(mapStateToProps, { getCategories })(Bookmarks); \ No newline at end of file +export default connect(mapStateToProps, { getCategories })(Bookmarks); diff --git a/client/src/components/Home/Home.tsx b/client/src/components/Home/Home.tsx index ece4a8a..fd711aa 100644 --- a/client/src/components/Home/Home.tsx +++ b/client/src/components/Home/Home.tsx @@ -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); // 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 ( - {searchConfig('hideSearch', 0) !== 1 - ? - :
- } + {searchConfig('hideSearch', 0) !== 1 ? ( + + ) : ( +
+ )} - {searchConfig('hideHeader', 0) !== 1 - ? ( -
-

{header.dateTime}

- Go to Settings - -

{header.greeting}

- -
-
- ) - :
- } - - {searchConfig('hideApps', 0) !== 1 - ? ( - - {appsLoading - ? - : app.isPinned)} - totalApps={apps.length} - /> - } -
-
) - :
- } + {searchConfig('hideHeader', 0) !== 1 ? ( +
+

{header.dateTime}

+ + Go to Settings + + +

{header.greeting}

+ +
+
+ ) : ( +
+ )} - {searchConfig('hideCategories', 0) !== 1 - ? ( - - {categoriesLoading - ? - : category.isPinned)} - totalCategories={categories.length} - /> - } - ) - :
- } + {searchConfig('hideApps', 0) !== 1 ? ( + + + {appsLoading ? ( + + ) : ( + isPinned) + : apps.filter(({ name }) => + new RegExp(localSearch, 'i').test(name) + ) + } + totalApps={apps.length} + searching={!!localSearch} + /> + )} +
+
+ ) : ( +
+ )} - - + {searchConfig('hideCategories', 0) !== 1 ? ( + + + {categoriesLoading ? ( + + ) : ( + isPinned) + : searchBookmarks(localSearch) + } + totalCategories={categories.length} + searching={!!localSearch} + /> + )} + + ) : ( +
+ )} + + +
- ) -} + ); +}; 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); \ No newline at end of file +export default connect(mapStateToProps, { getApps, getCategories })(Home); diff --git a/client/src/components/SearchBar/SearchBar.tsx b/client/src/components/SearchBar/SearchBar.tsx index 029f175..f2ccdec 100644 --- a/client/src/components/SearchBar/SearchBar.tsx +++ b/client/src/components/SearchBar/SearchBar.tsx @@ -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(document.createElement('input')); useEffect(() => { inputRef.current.focus(); - }, []) + }, []); const searchHandler = (e: KeyboardEvent) => { - 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 ( searchHandler(e)} + onKeyUp={(e) => searchHandler(e)} /> - ) -} + ); +}; -export default connect(null, { createNotification })(SearchBar); \ No newline at end of file +export default connect(null, { createNotification })(SearchBar); diff --git a/client/src/components/Settings/OtherSettings/OtherSettings.tsx b/client/src/components/Settings/OtherSettings/OtherSettings.tsx index 31bbd52..afaf072 100644 --- a/client/src/components/Settings/OtherSettings/OtherSettings.tsx +++ b/client/src/components/Settings/OtherSettings/OtherSettings.tsx @@ -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 ( -
formSubmitHandler(e)}> + formSubmitHandler(e)}> {/* OTHER OPTIONS */}

Miscellaneous

- + inputChangeHandler(e)} + onChange={(e) => inputChangeHandler(e)} /> {/* BEAHVIOR OPTIONS */}

App Behavior

- - - + - + - - + - + inputChangeHandler(e, true)} + onChange={(e) => inputChangeHandler(e, true)} > - + - + - + inputChangeHandler(e, true)} + onChange={(e) => inputChangeHandler(e, true)} > -