mirror of
https://github.com/pawelmalak/flame.git
synced 2025-07-21 20:39:36 +02:00
commit
0ec77c33bf
69 changed files with 1855 additions and 1565 deletions
2
.env
2
.env
|
@ -1,3 +1,3 @@
|
||||||
PORT=5005
|
PORT=5005
|
||||||
NODE_ENV=development
|
NODE_ENV=development
|
||||||
VERSION=1.7.0
|
VERSION=1.7.1
|
|
@ -1,3 +1,11 @@
|
||||||
|
### v1.7.1 (2021-10-22)
|
||||||
|
- Fixed search action not being triggered by Numpad Enter
|
||||||
|
- Added option to change date formatting ([#92](https://github.com/pawelmalak/flame/issues/92))
|
||||||
|
- Added shortcuts (Esc and double click) to clear search bar ([#100](https://github.com/pawelmalak/flame/issues/100))
|
||||||
|
- Added Traefik integration ([#102](https://github.com/pawelmalak/flame/issues/102))
|
||||||
|
- Fixed search bar not redirecting to valid URL if it starts with capital letter ([#118](https://github.com/pawelmalak/flame/issues/118))
|
||||||
|
- Performance improvements
|
||||||
|
|
||||||
### v1.7.0 (2021-10-11)
|
### v1.7.0 (2021-10-11)
|
||||||
- Search bar will now redirect if valid URL or IP is provided ([#67](https://github.com/pawelmalak/flame/issues/67))
|
- Search bar will now redirect if valid URL or IP is provided ([#67](https://github.com/pawelmalak/flame/issues/67))
|
||||||
- Users can now add their custom search providers ([#71](https://github.com/pawelmalak/flame/issues/71))
|
- Users can now add their custom search providers ([#71](https://github.com/pawelmalak/flame/issues/71))
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
REACT_APP_VERSION=1.7.0
|
REACT_APP_VERSION=1.7.1
|
|
@ -16,7 +16,7 @@ import Settings from './components/Settings/Settings';
|
||||||
import Bookmarks from './components/Bookmarks/Bookmarks';
|
import Bookmarks from './components/Bookmarks/Bookmarks';
|
||||||
import NotificationCenter from './components/NotificationCenter/NotificationCenter';
|
import NotificationCenter from './components/NotificationCenter/NotificationCenter';
|
||||||
|
|
||||||
// Get config pairs from database
|
// Load config
|
||||||
store.dispatch<any>(getConfig());
|
store.dispatch<any>(getConfig());
|
||||||
|
|
||||||
// Set theme
|
// Set theme
|
||||||
|
|
|
@ -2,12 +2,13 @@ import classes from './AppCard.module.css';
|
||||||
import Icon from '../../UI/Icons/Icon/Icon';
|
import Icon from '../../UI/Icons/Icon/Icon';
|
||||||
import { iconParser, urlParser } from '../../../utility';
|
import { iconParser, urlParser } from '../../../utility';
|
||||||
|
|
||||||
import { App } from '../../../interfaces';
|
import { App, Config, GlobalState } from '../../../interfaces';
|
||||||
import { searchConfig } from '../../../utility';
|
import { connect } from 'react-redux';
|
||||||
|
|
||||||
interface ComponentProps {
|
interface ComponentProps {
|
||||||
app: App;
|
app: App;
|
||||||
pinHandler?: Function;
|
pinHandler?: Function;
|
||||||
|
config: Config;
|
||||||
}
|
}
|
||||||
|
|
||||||
const AppCard = (props: ComponentProps): JSX.Element => {
|
const AppCard = (props: ComponentProps): JSX.Element => {
|
||||||
|
@ -29,7 +30,7 @@ const AppCard = (props: ComponentProps): JSX.Element => {
|
||||||
<div className={classes.CustomIcon}>
|
<div className={classes.CustomIcon}>
|
||||||
<svg
|
<svg
|
||||||
data-src={`/uploads/${icon}`}
|
data-src={`/uploads/${icon}`}
|
||||||
fill='var(--color-primary)'
|
fill="var(--color-primary)"
|
||||||
className={classes.CustomIcon}
|
className={classes.CustomIcon}
|
||||||
></svg>
|
></svg>
|
||||||
</div>
|
</div>
|
||||||
|
@ -41,8 +42,8 @@ const AppCard = (props: ComponentProps): JSX.Element => {
|
||||||
return (
|
return (
|
||||||
<a
|
<a
|
||||||
href={redirectUrl}
|
href={redirectUrl}
|
||||||
target={searchConfig('appsSameTab', false) ? '' : '_blank'}
|
target={props.config.appsSameTab ? '' : '_blank'}
|
||||||
rel='noreferrer'
|
rel="noreferrer"
|
||||||
className={classes.AppCard}
|
className={classes.AppCard}
|
||||||
>
|
>
|
||||||
<div className={classes.AppCardIcon}>{iconEl}</div>
|
<div className={classes.AppCardIcon}>{iconEl}</div>
|
||||||
|
@ -54,4 +55,10 @@ const AppCard = (props: ComponentProps): JSX.Element => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default AppCard;
|
const mapStateToProps = (state: GlobalState) => {
|
||||||
|
return {
|
||||||
|
config: state.config.config,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default connect(mapStateToProps)(AppCard);
|
||||||
|
|
|
@ -1,13 +1,24 @@
|
||||||
import { Fragment, KeyboardEvent, useState, useEffect } from 'react';
|
import { Fragment, KeyboardEvent, useState, useEffect } from 'react';
|
||||||
import { DragDropContext, Droppable, Draggable, DropResult } from 'react-beautiful-dnd';
|
import {
|
||||||
|
DragDropContext,
|
||||||
|
Droppable,
|
||||||
|
Draggable,
|
||||||
|
DropResult,
|
||||||
|
} from 'react-beautiful-dnd';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
// Redux
|
// Redux
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { pinApp, deleteApp, reorderApps, updateConfig, createNotification } from '../../../store/actions';
|
import {
|
||||||
|
pinApp,
|
||||||
|
deleteApp,
|
||||||
|
reorderApps,
|
||||||
|
updateConfig,
|
||||||
|
createNotification,
|
||||||
|
} from '../../../store/actions';
|
||||||
|
|
||||||
// Typescript
|
// Typescript
|
||||||
import { App, GlobalState, NewNotification } from '../../../interfaces';
|
import { App, Config, GlobalState, NewNotification } from '../../../interfaces';
|
||||||
|
|
||||||
// CSS
|
// CSS
|
||||||
import classes from './AppTable.module.css';
|
import classes from './AppTable.module.css';
|
||||||
|
@ -16,11 +27,9 @@ import classes from './AppTable.module.css';
|
||||||
import Icon from '../../UI/Icons/Icon/Icon';
|
import Icon from '../../UI/Icons/Icon/Icon';
|
||||||
import Table from '../../UI/Table/Table';
|
import Table from '../../UI/Table/Table';
|
||||||
|
|
||||||
// Utils
|
|
||||||
import { searchConfig } from '../../../utility';
|
|
||||||
|
|
||||||
interface ComponentProps {
|
interface ComponentProps {
|
||||||
apps: App[];
|
apps: App[];
|
||||||
|
config: Config;
|
||||||
pinApp: (app: App) => void;
|
pinApp: (app: App) => void;
|
||||||
deleteApp: (id: number) => void;
|
deleteApp: (id: number) => void;
|
||||||
updateAppHandler: (app: App) => void;
|
updateAppHandler: (app: App) => void;
|
||||||
|
@ -36,38 +45,44 @@ const AppTable = (props: ComponentProps): JSX.Element => {
|
||||||
// Copy apps array
|
// Copy apps array
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setLocalApps([...props.apps]);
|
setLocalApps([...props.apps]);
|
||||||
}, [props.apps])
|
}, [props.apps]);
|
||||||
|
|
||||||
// Check ordering
|
// Check ordering
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const order = searchConfig('useOrdering', '');
|
const order = props.config.useOrdering;
|
||||||
|
|
||||||
if (order === 'orderId') {
|
if (order === 'orderId') {
|
||||||
setIsCustomOrder(true);
|
setIsCustomOrder(true);
|
||||||
}
|
}
|
||||||
}, [])
|
}, []);
|
||||||
|
|
||||||
const deleteAppHandler = (app: App): void => {
|
const deleteAppHandler = (app: App): void => {
|
||||||
const proceed = window.confirm(`Are you sure you want to delete ${app.name} at ${app.url} ?`);
|
const proceed = window.confirm(
|
||||||
|
`Are you sure you want to delete ${app.name} at ${app.url} ?`
|
||||||
|
);
|
||||||
|
|
||||||
if (proceed) {
|
if (proceed) {
|
||||||
props.deleteApp(app.id);
|
props.deleteApp(app.id);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
// Support keyboard navigation for actions
|
// Support keyboard navigation for actions
|
||||||
const keyboardActionHandler = (e: KeyboardEvent, app: App, handler: Function) => {
|
const keyboardActionHandler = (
|
||||||
|
e: KeyboardEvent,
|
||||||
|
app: App,
|
||||||
|
handler: Function
|
||||||
|
) => {
|
||||||
if (e.key === 'Enter') {
|
if (e.key === 'Enter') {
|
||||||
handler(app);
|
handler(app);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
const dragEndHanlder = (result: DropResult): void => {
|
const dragEndHanlder = (result: DropResult): void => {
|
||||||
if (!isCustomOrder) {
|
if (!isCustomOrder) {
|
||||||
props.createNotification({
|
props.createNotification({
|
||||||
title: 'Error',
|
title: 'Error',
|
||||||
message: 'Custom order is disabled'
|
message: 'Custom order is disabled',
|
||||||
})
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -81,32 +96,39 @@ const AppTable = (props: ComponentProps): JSX.Element => {
|
||||||
|
|
||||||
setLocalApps(tmpApps);
|
setLocalApps(tmpApps);
|
||||||
props.reorderApps(tmpApps);
|
props.reorderApps(tmpApps);
|
||||||
}
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<div className={classes.Message}>
|
<div className={classes.Message}>
|
||||||
{isCustomOrder
|
{isCustomOrder ? (
|
||||||
? <p>You can drag and drop single rows to reorder application</p>
|
<p>You can drag and drop single rows to reorder application</p>
|
||||||
: <p>Custom order is disabled. You can change it in <Link to='/settings/other'>settings</Link></p>
|
) : (
|
||||||
}
|
<p>
|
||||||
|
Custom order is disabled. You can change it in{' '}
|
||||||
|
<Link to="/settings/other">settings</Link>
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<DragDropContext onDragEnd={dragEndHanlder}>
|
<DragDropContext onDragEnd={dragEndHanlder}>
|
||||||
<Droppable droppableId='apps'>
|
<Droppable droppableId="apps">
|
||||||
{(provided) => (
|
{(provided) => (
|
||||||
<Table headers={[
|
<Table
|
||||||
'Name',
|
headers={['Name', 'URL', 'Icon', 'Actions']}
|
||||||
'URL',
|
innerRef={provided.innerRef}
|
||||||
'Icon',
|
>
|
||||||
'Actions'
|
|
||||||
]}
|
|
||||||
innerRef={provided.innerRef}>
|
|
||||||
{localApps.map((app: App, index): JSX.Element => {
|
{localApps.map((app: App, index): JSX.Element => {
|
||||||
return (
|
return (
|
||||||
<Draggable key={app.id} draggableId={app.id.toString()} index={index}>
|
<Draggable
|
||||||
|
key={app.id}
|
||||||
|
draggableId={app.id.toString()}
|
||||||
|
index={index}
|
||||||
|
>
|
||||||
{(provided, snapshot) => {
|
{(provided, snapshot) => {
|
||||||
const style = {
|
const style = {
|
||||||
border: snapshot.isDragging ? '1px solid var(--color-accent)' : 'none',
|
border: snapshot.isDragging
|
||||||
|
? '1px solid var(--color-accent)'
|
||||||
|
: 'none',
|
||||||
borderRadius: '4px',
|
borderRadius: '4px',
|
||||||
...provided.draggableProps.style,
|
...provided.draggableProps.style,
|
||||||
};
|
};
|
||||||
|
@ -126,55 +148,77 @@ const AppTable = (props: ComponentProps): JSX.Element => {
|
||||||
<div
|
<div
|
||||||
className={classes.TableAction}
|
className={classes.TableAction}
|
||||||
onClick={() => deleteAppHandler(app)}
|
onClick={() => deleteAppHandler(app)}
|
||||||
onKeyDown={(e) => keyboardActionHandler(e, app, deleteAppHandler)}
|
onKeyDown={(e) =>
|
||||||
tabIndex={0}>
|
keyboardActionHandler(
|
||||||
<Icon icon='mdiDelete' />
|
e,
|
||||||
|
app,
|
||||||
|
deleteAppHandler
|
||||||
|
)
|
||||||
|
}
|
||||||
|
tabIndex={0}
|
||||||
|
>
|
||||||
|
<Icon icon="mdiDelete" />
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className={classes.TableAction}
|
className={classes.TableAction}
|
||||||
onClick={() => props.updateAppHandler(app)}
|
onClick={() => props.updateAppHandler(app)}
|
||||||
onKeyDown={(e) => keyboardActionHandler(e, app, props.updateAppHandler)}
|
onKeyDown={(e) =>
|
||||||
tabIndex={0}>
|
keyboardActionHandler(
|
||||||
<Icon icon='mdiPencil' />
|
e,
|
||||||
|
app,
|
||||||
|
props.updateAppHandler
|
||||||
|
)
|
||||||
|
}
|
||||||
|
tabIndex={0}
|
||||||
|
>
|
||||||
|
<Icon icon="mdiPencil" />
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className={classes.TableAction}
|
className={classes.TableAction}
|
||||||
onClick={() => props.pinApp(app)}
|
onClick={() => props.pinApp(app)}
|
||||||
onKeyDown={(e) => keyboardActionHandler(e, app, props.pinApp)}
|
onKeyDown={(e) =>
|
||||||
tabIndex={0}>
|
keyboardActionHandler(e, app, props.pinApp)
|
||||||
{app.isPinned
|
|
||||||
? <Icon icon='mdiPinOff' color='var(--color-accent)' />
|
|
||||||
: <Icon icon='mdiPin' />
|
|
||||||
}
|
}
|
||||||
|
tabIndex={0}
|
||||||
|
>
|
||||||
|
{app.isPinned ? (
|
||||||
|
<Icon
|
||||||
|
icon="mdiPinOff"
|
||||||
|
color="var(--color-accent)"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Icon icon="mdiPin" />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
)}
|
)}
|
||||||
</tr>
|
</tr>
|
||||||
)
|
);
|
||||||
}}
|
}}
|
||||||
</Draggable>
|
</Draggable>
|
||||||
)
|
);
|
||||||
})}
|
})}
|
||||||
</Table>
|
</Table>
|
||||||
)}
|
)}
|
||||||
</Droppable>
|
</Droppable>
|
||||||
</DragDropContext>
|
</DragDropContext>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
const mapStateToProps = (state: GlobalState) => {
|
const mapStateToProps = (state: GlobalState) => {
|
||||||
return {
|
return {
|
||||||
apps: state.app.apps
|
apps: state.app.apps,
|
||||||
}
|
config: state.config.config,
|
||||||
}
|
};
|
||||||
|
};
|
||||||
|
|
||||||
const actions = {
|
const actions = {
|
||||||
pinApp,
|
pinApp,
|
||||||
deleteApp,
|
deleteApp,
|
||||||
reorderApps,
|
reorderApps,
|
||||||
updateConfig,
|
updateConfig,
|
||||||
createNotification
|
createNotification,
|
||||||
}
|
};
|
||||||
|
|
||||||
export default connect(mapStateToProps, actions)(AppTable);
|
export default connect(mapStateToProps, actions)(AppTable);
|
|
@ -1,12 +1,14 @@
|
||||||
import { Bookmark, Category } from '../../../interfaces';
|
import { Bookmark, Category, Config, GlobalState } from '../../../interfaces';
|
||||||
import classes from './BookmarkCard.module.css';
|
import classes from './BookmarkCard.module.css';
|
||||||
|
|
||||||
import Icon from '../../UI/Icons/Icon/Icon';
|
import Icon from '../../UI/Icons/Icon/Icon';
|
||||||
import { iconParser, urlParser, searchConfig } from '../../../utility';
|
import { iconParser, urlParser } from '../../../utility';
|
||||||
import { Fragment } from 'react';
|
import { Fragment } from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
|
||||||
interface ComponentProps {
|
interface ComponentProps {
|
||||||
category: Category;
|
category: Category;
|
||||||
|
config: Config;
|
||||||
}
|
}
|
||||||
|
|
||||||
const BookmarkCard = (props: ComponentProps): JSX.Element => {
|
const BookmarkCard = (props: ComponentProps): JSX.Element => {
|
||||||
|
@ -54,7 +56,7 @@ const BookmarkCard = (props: ComponentProps): JSX.Element => {
|
||||||
return (
|
return (
|
||||||
<a
|
<a
|
||||||
href={redirectUrl}
|
href={redirectUrl}
|
||||||
target={searchConfig('bookmarksSameTab', false) ? '' : '_blank'}
|
target={props.config.bookmarksSameTab ? '' : '_blank'}
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
key={`bookmark-${bookmark.id}`}
|
key={`bookmark-${bookmark.id}`}
|
||||||
>
|
>
|
||||||
|
@ -68,4 +70,10 @@ const BookmarkCard = (props: ComponentProps): JSX.Element => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default BookmarkCard;
|
const mapStateToProps = (state: GlobalState) => {
|
||||||
|
return {
|
||||||
|
config: state.config.config,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default connect(mapStateToProps)(BookmarkCard);
|
||||||
|
|
|
@ -1,13 +1,30 @@
|
||||||
import { KeyboardEvent, useState, useEffect, Fragment } from 'react';
|
import { KeyboardEvent, useState, useEffect, Fragment } from 'react';
|
||||||
import { DragDropContext, Droppable, Draggable, DropResult } from 'react-beautiful-dnd';
|
import {
|
||||||
|
DragDropContext,
|
||||||
|
Droppable,
|
||||||
|
Draggable,
|
||||||
|
DropResult,
|
||||||
|
} from 'react-beautiful-dnd';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
// Redux
|
// Redux
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { pinCategory, deleteCategory, deleteBookmark, createNotification, reorderCategories } from '../../../store/actions';
|
import {
|
||||||
|
pinCategory,
|
||||||
|
deleteCategory,
|
||||||
|
deleteBookmark,
|
||||||
|
createNotification,
|
||||||
|
reorderCategories,
|
||||||
|
} from '../../../store/actions';
|
||||||
|
|
||||||
// Typescript
|
// Typescript
|
||||||
import { Bookmark, Category, NewNotification } from '../../../interfaces';
|
import {
|
||||||
|
Bookmark,
|
||||||
|
Category,
|
||||||
|
Config,
|
||||||
|
GlobalState,
|
||||||
|
NewNotification,
|
||||||
|
} from '../../../interfaces';
|
||||||
import { ContentType } from '../Bookmarks';
|
import { ContentType } from '../Bookmarks';
|
||||||
|
|
||||||
// CSS
|
// CSS
|
||||||
|
@ -17,12 +34,10 @@ import classes from './BookmarkTable.module.css';
|
||||||
import Table from '../../UI/Table/Table';
|
import Table from '../../UI/Table/Table';
|
||||||
import Icon from '../../UI/Icons/Icon/Icon';
|
import Icon from '../../UI/Icons/Icon/Icon';
|
||||||
|
|
||||||
// Utils
|
|
||||||
import { searchConfig } from '../../../utility';
|
|
||||||
|
|
||||||
interface ComponentProps {
|
interface ComponentProps {
|
||||||
contentType: ContentType;
|
contentType: ContentType;
|
||||||
categories: Category[];
|
categories: Category[];
|
||||||
|
config: Config;
|
||||||
pinCategory: (category: Category) => void;
|
pinCategory: (category: Category) => void;
|
||||||
deleteCategory: (id: number) => void;
|
deleteCategory: (id: number) => void;
|
||||||
updateHandler: (data: Category | Bookmark) => void;
|
updateHandler: (data: Category | Bookmark) => void;
|
||||||
|
@ -38,45 +53,53 @@ const BookmarkTable = (props: ComponentProps): JSX.Element => {
|
||||||
// Copy categories array
|
// Copy categories array
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setLocalCategories([...props.categories]);
|
setLocalCategories([...props.categories]);
|
||||||
}, [props.categories])
|
}, [props.categories]);
|
||||||
|
|
||||||
// Check ordering
|
// Check ordering
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const order = searchConfig('useOrdering', '');
|
const order = props.config.useOrdering;
|
||||||
|
|
||||||
if (order === 'orderId') {
|
if (order === 'orderId') {
|
||||||
setIsCustomOrder(true);
|
setIsCustomOrder(true);
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
|
|
||||||
const deleteCategoryHandler = (category: Category): void => {
|
const deleteCategoryHandler = (category: Category): void => {
|
||||||
const proceed = window.confirm(`Are you sure you want to delete ${category.name}? It will delete ALL assigned bookmarks`);
|
const proceed = window.confirm(
|
||||||
|
`Are you sure you want to delete ${category.name}? It will delete ALL assigned bookmarks`
|
||||||
|
);
|
||||||
|
|
||||||
if (proceed) {
|
if (proceed) {
|
||||||
props.deleteCategory(category.id);
|
props.deleteCategory(category.id);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
const deleteBookmarkHandler = (bookmark: Bookmark): void => {
|
const deleteBookmarkHandler = (bookmark: Bookmark): void => {
|
||||||
const proceed = window.confirm(`Are you sure you want to delete ${bookmark.name}?`);
|
const proceed = window.confirm(
|
||||||
|
`Are you sure you want to delete ${bookmark.name}?`
|
||||||
|
);
|
||||||
|
|
||||||
if (proceed) {
|
if (proceed) {
|
||||||
props.deleteBookmark(bookmark.id, bookmark.categoryId);
|
props.deleteBookmark(bookmark.id, bookmark.categoryId);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
const keyboardActionHandler = (e: KeyboardEvent, category: Category, handler: Function) => {
|
const keyboardActionHandler = (
|
||||||
|
e: KeyboardEvent,
|
||||||
|
category: Category,
|
||||||
|
handler: Function
|
||||||
|
) => {
|
||||||
if (e.key === 'Enter') {
|
if (e.key === 'Enter') {
|
||||||
handler(category);
|
handler(category);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
const dragEndHanlder = (result: DropResult): void => {
|
const dragEndHanlder = (result: DropResult): void => {
|
||||||
if (!isCustomOrder) {
|
if (!isCustomOrder) {
|
||||||
props.createNotification({
|
props.createNotification({
|
||||||
title: 'Error',
|
title: 'Error',
|
||||||
message: 'Custom order is disabled'
|
message: 'Custom order is disabled',
|
||||||
})
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -90,31 +113,38 @@ const BookmarkTable = (props: ComponentProps): JSX.Element => {
|
||||||
|
|
||||||
setLocalCategories(tmpCategories);
|
setLocalCategories(tmpCategories);
|
||||||
props.reorderCategories(tmpCategories);
|
props.reorderCategories(tmpCategories);
|
||||||
}
|
};
|
||||||
|
|
||||||
if (props.contentType === ContentType.category) {
|
if (props.contentType === ContentType.category) {
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<div className={classes.Message}>
|
<div className={classes.Message}>
|
||||||
{isCustomOrder
|
{isCustomOrder ? (
|
||||||
? <p>You can drag and drop single rows to reorder categories</p>
|
<p>You can drag and drop single rows to reorder categories</p>
|
||||||
: <p>Custom order is disabled. You can change it in <Link to='/settings/other'>settings</Link></p>
|
) : (
|
||||||
}
|
<p>
|
||||||
|
Custom order is disabled. You can change it in{' '}
|
||||||
|
<Link to="/settings/other">settings</Link>
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<DragDropContext onDragEnd={dragEndHanlder}>
|
<DragDropContext onDragEnd={dragEndHanlder}>
|
||||||
<Droppable droppableId='categories'>
|
<Droppable droppableId="categories">
|
||||||
{(provided) => (
|
{(provided) => (
|
||||||
<Table headers={[
|
<Table headers={['Name', 'Actions']} innerRef={provided.innerRef}>
|
||||||
'Name',
|
{localCategories.map(
|
||||||
'Actions'
|
(category: Category, index): JSX.Element => {
|
||||||
]}
|
|
||||||
innerRef={provided.innerRef}>
|
|
||||||
{localCategories.map((category: Category, index): JSX.Element => {
|
|
||||||
return (
|
return (
|
||||||
<Draggable key={category.id} draggableId={category.id.toString()} index={index}>
|
<Draggable
|
||||||
|
key={category.id}
|
||||||
|
draggableId={category.id.toString()}
|
||||||
|
index={index}
|
||||||
|
>
|
||||||
{(provided, snapshot) => {
|
{(provided, snapshot) => {
|
||||||
const style = {
|
const style = {
|
||||||
border: snapshot.isDragging ? '1px solid var(--color-accent)' : 'none',
|
border: snapshot.isDragging
|
||||||
|
? '1px solid var(--color-accent)'
|
||||||
|
: 'none',
|
||||||
borderRadius: '4px',
|
borderRadius: '4px',
|
||||||
...provided.draggableProps.style,
|
...provided.draggableProps.style,
|
||||||
};
|
};
|
||||||
|
@ -131,61 +161,80 @@ const BookmarkTable = (props: ComponentProps): JSX.Element => {
|
||||||
<td className={classes.TableActions}>
|
<td className={classes.TableActions}>
|
||||||
<div
|
<div
|
||||||
className={classes.TableAction}
|
className={classes.TableAction}
|
||||||
onClick={() => deleteCategoryHandler(category)}
|
onClick={() =>
|
||||||
onKeyDown={(e) => keyboardActionHandler(e, category, deleteCategoryHandler)}
|
deleteCategoryHandler(category)
|
||||||
tabIndex={0}>
|
}
|
||||||
<Icon icon='mdiDelete' />
|
onKeyDown={(e) =>
|
||||||
|
keyboardActionHandler(
|
||||||
|
e,
|
||||||
|
category,
|
||||||
|
deleteCategoryHandler
|
||||||
|
)
|
||||||
|
}
|
||||||
|
tabIndex={0}
|
||||||
|
>
|
||||||
|
<Icon icon="mdiDelete" />
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className={classes.TableAction}
|
className={classes.TableAction}
|
||||||
onClick={() => props.updateHandler(category)}
|
onClick={() =>
|
||||||
tabIndex={0}>
|
props.updateHandler(category)
|
||||||
<Icon icon='mdiPencil' />
|
}
|
||||||
|
tabIndex={0}
|
||||||
|
>
|
||||||
|
<Icon icon="mdiPencil" />
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className={classes.TableAction}
|
className={classes.TableAction}
|
||||||
onClick={() => props.pinCategory(category)}
|
onClick={() => props.pinCategory(category)}
|
||||||
onKeyDown={(e) => keyboardActionHandler(e, category, props.pinCategory)}
|
onKeyDown={(e) =>
|
||||||
tabIndex={0}>
|
keyboardActionHandler(
|
||||||
{category.isPinned
|
e,
|
||||||
? <Icon icon='mdiPinOff' color='var(--color-accent)' />
|
category,
|
||||||
: <Icon icon='mdiPin' />
|
props.pinCategory
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
tabIndex={0}
|
||||||
|
>
|
||||||
|
{category.isPinned ? (
|
||||||
|
<Icon
|
||||||
|
icon="mdiPinOff"
|
||||||
|
color="var(--color-accent)"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Icon icon="mdiPin" />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
)}
|
)}
|
||||||
</tr>
|
</tr>
|
||||||
)
|
);
|
||||||
}}
|
}}
|
||||||
</Draggable>
|
</Draggable>
|
||||||
)
|
);
|
||||||
})}
|
}
|
||||||
|
)}
|
||||||
</Table>
|
</Table>
|
||||||
)}
|
)}
|
||||||
</Droppable>
|
</Droppable>
|
||||||
</DragDropContext>
|
</DragDropContext>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
)
|
);
|
||||||
} else {
|
} else {
|
||||||
const bookmarks: {bookmark: Bookmark, categoryName: string}[] = [];
|
const bookmarks: { bookmark: Bookmark; categoryName: string }[] = [];
|
||||||
props.categories.forEach((category: Category) => {
|
props.categories.forEach((category: Category) => {
|
||||||
category.bookmarks.forEach((bookmark: Bookmark) => {
|
category.bookmarks.forEach((bookmark: Bookmark) => {
|
||||||
bookmarks.push({
|
bookmarks.push({
|
||||||
bookmark,
|
bookmark,
|
||||||
categoryName: category.name
|
categoryName: category.name,
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Table headers={[
|
<Table headers={['Name', 'URL', 'Icon', 'Category', 'Actions']}>
|
||||||
'Name',
|
{bookmarks.map(
|
||||||
'URL',
|
(bookmark: { bookmark: Bookmark; categoryName: string }) => {
|
||||||
'Icon',
|
|
||||||
'Category',
|
|
||||||
'Actions'
|
|
||||||
]}>
|
|
||||||
{bookmarks.map((bookmark: {bookmark: Bookmark, categoryName: string}) => {
|
|
||||||
return (
|
return (
|
||||||
<tr key={bookmark.bookmark.id}>
|
<tr key={bookmark.bookmark.id}>
|
||||||
<td>{bookmark.bookmark.name}</td>
|
<td>{bookmark.bookmark.name}</td>
|
||||||
|
@ -196,30 +245,39 @@ const BookmarkTable = (props: ComponentProps): JSX.Element => {
|
||||||
<div
|
<div
|
||||||
className={classes.TableAction}
|
className={classes.TableAction}
|
||||||
onClick={() => deleteBookmarkHandler(bookmark.bookmark)}
|
onClick={() => deleteBookmarkHandler(bookmark.bookmark)}
|
||||||
tabIndex={0}>
|
tabIndex={0}
|
||||||
<Icon icon='mdiDelete' />
|
>
|
||||||
|
<Icon icon="mdiDelete" />
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className={classes.TableAction}
|
className={classes.TableAction}
|
||||||
onClick={() => props.updateHandler(bookmark.bookmark)}
|
onClick={() => props.updateHandler(bookmark.bookmark)}
|
||||||
tabIndex={0}>
|
tabIndex={0}
|
||||||
<Icon icon='mdiPencil' />
|
>
|
||||||
|
<Icon icon="mdiPencil" />
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
)
|
);
|
||||||
})}
|
}
|
||||||
|
)}
|
||||||
</Table>
|
</Table>
|
||||||
)
|
);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const mapStateToProps = (state: GlobalState) => {
|
||||||
|
return {
|
||||||
|
config: state.config.config,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
const actions = {
|
const actions = {
|
||||||
pinCategory,
|
pinCategory,
|
||||||
deleteCategory,
|
deleteCategory,
|
||||||
deleteBookmark,
|
deleteBookmark,
|
||||||
createNotification,
|
createNotification,
|
||||||
reorderCategories
|
reorderCategories,
|
||||||
}
|
};
|
||||||
|
|
||||||
export default connect(null, actions)(BookmarkTable);
|
export default connect(mapStateToProps, actions)(BookmarkTable);
|
||||||
|
|
|
@ -7,7 +7,7 @@ import { getApps, getCategories } from '../../store/actions';
|
||||||
|
|
||||||
// Typescript
|
// Typescript
|
||||||
import { GlobalState } from '../../interfaces/GlobalState';
|
import { GlobalState } from '../../interfaces/GlobalState';
|
||||||
import { App, Category } from '../../interfaces';
|
import { App, Category, Config } from '../../interfaces';
|
||||||
|
|
||||||
// UI
|
// UI
|
||||||
import Icon from '../UI/Icons/Icon/Icon';
|
import Icon from '../UI/Icons/Icon/Icon';
|
||||||
|
@ -28,9 +28,6 @@ import SearchBar from '../SearchBar/SearchBar';
|
||||||
import { greeter } from './functions/greeter';
|
import { greeter } from './functions/greeter';
|
||||||
import { dateTime } from './functions/dateTime';
|
import { dateTime } from './functions/dateTime';
|
||||||
|
|
||||||
// Utils
|
|
||||||
import { searchConfig } from '../../utility';
|
|
||||||
|
|
||||||
interface ComponentProps {
|
interface ComponentProps {
|
||||||
getApps: Function;
|
getApps: Function;
|
||||||
getCategories: Function;
|
getCategories: Function;
|
||||||
|
@ -38,6 +35,7 @@ interface ComponentProps {
|
||||||
apps: App[];
|
apps: App[];
|
||||||
categoriesLoading: boolean;
|
categoriesLoading: boolean;
|
||||||
categories: Category[];
|
categories: Category[];
|
||||||
|
config: Config;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Home = (props: ComponentProps): JSX.Element => {
|
const Home = (props: ComponentProps): JSX.Element => {
|
||||||
|
@ -77,7 +75,7 @@ const Home = (props: ComponentProps): JSX.Element => {
|
||||||
let interval: any;
|
let interval: any;
|
||||||
|
|
||||||
// Start interval only when hideHeader is false
|
// Start interval only when hideHeader is false
|
||||||
if (searchConfig('hideHeader', 0) !== 1) {
|
if (!props.config.hideHeader) {
|
||||||
interval = setInterval(() => {
|
interval = setInterval(() => {
|
||||||
setHeader({
|
setHeader({
|
||||||
dateTime: dateTime(),
|
dateTime: dateTime(),
|
||||||
|
@ -103,13 +101,13 @@ const Home = (props: ComponentProps): JSX.Element => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container>
|
<Container>
|
||||||
{searchConfig('hideSearch', 0) !== 1 ? (
|
{!props.config.hideSearch ? (
|
||||||
<SearchBar setLocalSearch={setLocalSearch} />
|
<SearchBar setLocalSearch={setLocalSearch} />
|
||||||
) : (
|
) : (
|
||||||
<div></div>
|
<div></div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{searchConfig('hideHeader', 0) !== 1 ? (
|
{!props.config.hideHeader ? (
|
||||||
<header className={classes.Header}>
|
<header className={classes.Header}>
|
||||||
<p>{header.dateTime}</p>
|
<p>{header.dateTime}</p>
|
||||||
<Link to="/settings" className={classes.SettingsLink}>
|
<Link to="/settings" className={classes.SettingsLink}>
|
||||||
|
@ -124,7 +122,7 @@ const Home = (props: ComponentProps): JSX.Element => {
|
||||||
<div></div>
|
<div></div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{searchConfig('hideApps', 0) !== 1 ? (
|
{!props.config.hideApps ? (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<SectionHeadline title="Applications" link="/applications" />
|
<SectionHeadline title="Applications" link="/applications" />
|
||||||
{appsLoading ? (
|
{appsLoading ? (
|
||||||
|
@ -148,7 +146,7 @@ const Home = (props: ComponentProps): JSX.Element => {
|
||||||
<div></div>
|
<div></div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{searchConfig('hideCategories', 0) !== 1 ? (
|
{!props.config.hideCategories ? (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<SectionHeadline title="Bookmarks" link="/bookmarks" />
|
<SectionHeadline title="Bookmarks" link="/bookmarks" />
|
||||||
{categoriesLoading ? (
|
{categoriesLoading ? (
|
||||||
|
@ -182,6 +180,7 @@ const mapStateToProps = (state: GlobalState) => {
|
||||||
apps: state.app.apps,
|
apps: state.app.apps,
|
||||||
categoriesLoading: state.bookmark.loading,
|
categoriesLoading: state.bookmark.loading,
|
||||||
categories: state.bookmark.categories,
|
categories: state.bookmark.categories,
|
||||||
|
config: state.config.config,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,39 @@
|
||||||
export const dateTime = (): string => {
|
export const dateTime = (): string => {
|
||||||
const days = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
|
const days = [
|
||||||
const months = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
|
'Sunday',
|
||||||
|
'Monday',
|
||||||
|
'Tuesday',
|
||||||
|
'Wednesday',
|
||||||
|
'Thursday',
|
||||||
|
'Friday',
|
||||||
|
'Saturday',
|
||||||
|
];
|
||||||
|
const months = [
|
||||||
|
'January',
|
||||||
|
'February',
|
||||||
|
'March',
|
||||||
|
'April',
|
||||||
|
'May',
|
||||||
|
'June',
|
||||||
|
'July',
|
||||||
|
'August',
|
||||||
|
'September',
|
||||||
|
'October',
|
||||||
|
'November',
|
||||||
|
'December',
|
||||||
|
];
|
||||||
|
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
|
|
||||||
return `${days[now.getDay()]}, ${now.getDate()} ${months[now.getMonth()]} ${now.getFullYear()}`;
|
const useAmericanDate = localStorage.useAmericanDate === 'true';
|
||||||
|
|
||||||
|
if (!useAmericanDate) {
|
||||||
|
return `${days[now.getDay()]}, ${now.getDate()} ${
|
||||||
|
months[now.getMonth()]
|
||||||
|
} ${now.getFullYear()}`;
|
||||||
|
} else {
|
||||||
|
return `${days[now.getDay()]}, ${
|
||||||
|
months[now.getMonth()]
|
||||||
|
} ${now.getDate()} ${now.getFullYear()}`;
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
|
@ -27,6 +27,11 @@ const SearchBar = (props: ComponentProps): JSX.Element => {
|
||||||
inputRef.current.focus();
|
inputRef.current.focus();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const clearSearch = () => {
|
||||||
|
inputRef.current.value = '';
|
||||||
|
setLocalSearch('');
|
||||||
|
};
|
||||||
|
|
||||||
const searchHandler = (e: KeyboardEvent<HTMLInputElement>) => {
|
const searchHandler = (e: KeyboardEvent<HTMLInputElement>) => {
|
||||||
const { isLocal, search, query, isURL, sameTab } = searchParser(
|
const { isLocal, search, query, isURL, sameTab } = searchParser(
|
||||||
inputRef.current.value
|
inputRef.current.value
|
||||||
|
@ -36,31 +41,40 @@ const SearchBar = (props: ComponentProps): JSX.Element => {
|
||||||
setLocalSearch(search);
|
setLocalSearch(search);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (e.code === 'Enter') {
|
if (e.code === 'Enter' || e.code === 'NumpadEnter') {
|
||||||
if (!query.prefix) {
|
if (!query.prefix) {
|
||||||
|
// Prefix not found -> emit notification
|
||||||
createNotification({
|
createNotification({
|
||||||
title: 'Error',
|
title: 'Error',
|
||||||
message: 'Prefix not found',
|
message: 'Prefix not found',
|
||||||
});
|
});
|
||||||
} else if (isURL) {
|
} else if (isURL) {
|
||||||
|
// URL or IP passed -> redirect
|
||||||
const url = urlParser(inputRef.current.value)[1];
|
const url = urlParser(inputRef.current.value)[1];
|
||||||
redirectUrl(url, sameTab);
|
redirectUrl(url, sameTab);
|
||||||
} else if (isLocal) {
|
} else if (isLocal) {
|
||||||
|
// Local query -> filter apps and bookmarks
|
||||||
setLocalSearch(search);
|
setLocalSearch(search);
|
||||||
} else {
|
} else {
|
||||||
|
// Valid query -> redirect to search results
|
||||||
const url = `${query.template}${search}`;
|
const url = `${query.template}${search}`;
|
||||||
redirectUrl(url, sameTab);
|
redirectUrl(url, sameTab);
|
||||||
}
|
}
|
||||||
|
} else if (e.code === 'Escape') {
|
||||||
|
clearSearch();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<div className={classes.SearchContainer}>
|
||||||
<input
|
<input
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
type="text"
|
type="text"
|
||||||
className={classes.SearchBar}
|
className={classes.SearchBar}
|
||||||
onKeyUp={(e) => searchHandler(e)}
|
onKeyUp={(e) => searchHandler(e)}
|
||||||
|
onDoubleClick={clearSearch}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -11,9 +11,10 @@ import {
|
||||||
|
|
||||||
// Typescript
|
// Typescript
|
||||||
import {
|
import {
|
||||||
|
Config,
|
||||||
GlobalState,
|
GlobalState,
|
||||||
NewNotification,
|
NewNotification,
|
||||||
SettingsForm,
|
OtherSettingsForm,
|
||||||
} from '../../../interfaces';
|
} from '../../../interfaces';
|
||||||
|
|
||||||
// UI
|
// UI
|
||||||
|
@ -22,50 +23,29 @@ import Button from '../../UI/Buttons/Button/Button';
|
||||||
import SettingsHeadline from '../../UI/Headlines/SettingsHeadline/SettingsHeadline';
|
import SettingsHeadline from '../../UI/Headlines/SettingsHeadline/SettingsHeadline';
|
||||||
|
|
||||||
// Utils
|
// Utils
|
||||||
import { searchConfig } from '../../../utility';
|
import { otherSettingsTemplate, inputHandler } from '../../../utility';
|
||||||
|
|
||||||
interface ComponentProps {
|
interface ComponentProps {
|
||||||
createNotification: (notification: NewNotification) => void;
|
createNotification: (notification: NewNotification) => void;
|
||||||
updateConfig: (formData: SettingsForm) => void;
|
updateConfig: (formData: OtherSettingsForm) => void;
|
||||||
sortApps: () => void;
|
sortApps: () => void;
|
||||||
sortCategories: () => void;
|
sortCategories: () => void;
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
|
config: Config;
|
||||||
}
|
}
|
||||||
|
|
||||||
const OtherSettings = (props: ComponentProps): JSX.Element => {
|
const OtherSettings = (props: ComponentProps): JSX.Element => {
|
||||||
|
const { config } = props;
|
||||||
|
|
||||||
// Initial state
|
// Initial state
|
||||||
const [formData, setFormData] = useState<SettingsForm>({
|
const [formData, setFormData] = useState<OtherSettingsForm>(
|
||||||
customTitle: document.title,
|
otherSettingsTemplate
|
||||||
pinAppsByDefault: 1,
|
);
|
||||||
pinCategoriesByDefault: 1,
|
|
||||||
hideHeader: 0,
|
|
||||||
hideApps: 0,
|
|
||||||
hideCategories: 0,
|
|
||||||
useOrdering: 'createdAt',
|
|
||||||
appsSameTab: 0,
|
|
||||||
bookmarksSameTab: 0,
|
|
||||||
dockerApps: 1,
|
|
||||||
dockerHost: 'localhost',
|
|
||||||
kubernetesApps: 1,
|
|
||||||
unpinStoppedApps: 1,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Get config
|
// Get config
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setFormData({
|
setFormData({
|
||||||
customTitle: searchConfig('customTitle', 'Flame'),
|
...config,
|
||||||
pinAppsByDefault: searchConfig('pinAppsByDefault', 1),
|
|
||||||
pinCategoriesByDefault: searchConfig('pinCategoriesByDefault', 1),
|
|
||||||
hideHeader: searchConfig('hideHeader', 0),
|
|
||||||
hideApps: searchConfig('hideApps', 0),
|
|
||||||
hideCategories: searchConfig('hideCategories', 0),
|
|
||||||
useOrdering: searchConfig('useOrdering', 'createdAt'),
|
|
||||||
appsSameTab: searchConfig('appsSameTab', 0),
|
|
||||||
bookmarksSameTab: searchConfig('bookmarksSameTab', 0),
|
|
||||||
dockerApps: searchConfig('dockerApps', 0),
|
|
||||||
dockerHost: searchConfig('dockerHost', 'localhost'),
|
|
||||||
kubernetesApps: searchConfig('kubernetesApps', 0),
|
|
||||||
unpinStoppedApps: searchConfig('unpinStoppedApps', 0),
|
|
||||||
});
|
});
|
||||||
}, [props.loading]);
|
}, [props.loading]);
|
||||||
|
|
||||||
|
@ -87,17 +67,13 @@ const OtherSettings = (props: ComponentProps): JSX.Element => {
|
||||||
// Input handler
|
// Input handler
|
||||||
const inputChangeHandler = (
|
const inputChangeHandler = (
|
||||||
e: ChangeEvent<HTMLInputElement | HTMLSelectElement>,
|
e: ChangeEvent<HTMLInputElement | HTMLSelectElement>,
|
||||||
isNumber?: boolean
|
options?: { isNumber?: boolean; isBool?: boolean }
|
||||||
) => {
|
) => {
|
||||||
let value: string | number = e.target.value;
|
inputHandler<OtherSettingsForm>({
|
||||||
|
e,
|
||||||
if (isNumber) {
|
options,
|
||||||
value = parseFloat(value);
|
setStateHandler: setFormData,
|
||||||
}
|
state: formData,
|
||||||
|
|
||||||
setFormData({
|
|
||||||
...formData,
|
|
||||||
[e.target.name]: value,
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -116,6 +92,18 @@ const OtherSettings = (props: ComponentProps): JSX.Element => {
|
||||||
onChange={(e) => inputChangeHandler(e)}
|
onChange={(e) => inputChangeHandler(e)}
|
||||||
/>
|
/>
|
||||||
</InputGroup>
|
</InputGroup>
|
||||||
|
<InputGroup>
|
||||||
|
<label htmlFor="useAmericanDate">Date formatting</label>
|
||||||
|
<select
|
||||||
|
id="useAmericanDate"
|
||||||
|
name="useAmericanDate"
|
||||||
|
value={formData.useAmericanDate ? 1 : 0}
|
||||||
|
onChange={(e) => inputChangeHandler(e, { isBool: true })}
|
||||||
|
>
|
||||||
|
<option value={1}>Friday, October 22 2021</option>
|
||||||
|
<option value={0}>Friday, 22 October 2021</option>
|
||||||
|
</select>
|
||||||
|
</InputGroup>
|
||||||
|
|
||||||
{/* BEAHVIOR OPTIONS */}
|
{/* BEAHVIOR OPTIONS */}
|
||||||
<SettingsHeadline text="App Behavior" />
|
<SettingsHeadline text="App Behavior" />
|
||||||
|
@ -126,8 +114,8 @@ const OtherSettings = (props: ComponentProps): JSX.Element => {
|
||||||
<select
|
<select
|
||||||
id="pinAppsByDefault"
|
id="pinAppsByDefault"
|
||||||
name="pinAppsByDefault"
|
name="pinAppsByDefault"
|
||||||
value={formData.pinAppsByDefault}
|
value={formData.pinAppsByDefault ? 1 : 0}
|
||||||
onChange={(e) => inputChangeHandler(e, true)}
|
onChange={(e) => inputChangeHandler(e, { isBool: true })}
|
||||||
>
|
>
|
||||||
<option value={1}>True</option>
|
<option value={1}>True</option>
|
||||||
<option value={0}>False</option>
|
<option value={0}>False</option>
|
||||||
|
@ -140,8 +128,8 @@ const OtherSettings = (props: ComponentProps): JSX.Element => {
|
||||||
<select
|
<select
|
||||||
id="pinCategoriesByDefault"
|
id="pinCategoriesByDefault"
|
||||||
name="pinCategoriesByDefault"
|
name="pinCategoriesByDefault"
|
||||||
value={formData.pinCategoriesByDefault}
|
value={formData.pinCategoriesByDefault ? 1 : 0}
|
||||||
onChange={(e) => inputChangeHandler(e, true)}
|
onChange={(e) => inputChangeHandler(e, { isBool: true })}
|
||||||
>
|
>
|
||||||
<option value={1}>True</option>
|
<option value={1}>True</option>
|
||||||
<option value={0}>False</option>
|
<option value={0}>False</option>
|
||||||
|
@ -165,8 +153,8 @@ const OtherSettings = (props: ComponentProps): JSX.Element => {
|
||||||
<select
|
<select
|
||||||
id="appsSameTab"
|
id="appsSameTab"
|
||||||
name="appsSameTab"
|
name="appsSameTab"
|
||||||
value={formData.appsSameTab}
|
value={formData.appsSameTab ? 1 : 0}
|
||||||
onChange={(e) => inputChangeHandler(e, true)}
|
onChange={(e) => inputChangeHandler(e, { isBool: true })}
|
||||||
>
|
>
|
||||||
<option value={1}>True</option>
|
<option value={1}>True</option>
|
||||||
<option value={0}>False</option>
|
<option value={0}>False</option>
|
||||||
|
@ -177,8 +165,8 @@ const OtherSettings = (props: ComponentProps): JSX.Element => {
|
||||||
<select
|
<select
|
||||||
id="bookmarksSameTab"
|
id="bookmarksSameTab"
|
||||||
name="bookmarksSameTab"
|
name="bookmarksSameTab"
|
||||||
value={formData.bookmarksSameTab}
|
value={formData.bookmarksSameTab ? 1 : 0}
|
||||||
onChange={(e) => inputChangeHandler(e, true)}
|
onChange={(e) => inputChangeHandler(e, { isBool: true })}
|
||||||
>
|
>
|
||||||
<option value={1}>True</option>
|
<option value={1}>True</option>
|
||||||
<option value={0}>False</option>
|
<option value={0}>False</option>
|
||||||
|
@ -192,8 +180,8 @@ const OtherSettings = (props: ComponentProps): JSX.Element => {
|
||||||
<select
|
<select
|
||||||
id="hideHeader"
|
id="hideHeader"
|
||||||
name="hideHeader"
|
name="hideHeader"
|
||||||
value={formData.hideHeader}
|
value={formData.hideHeader ? 1 : 0}
|
||||||
onChange={(e) => inputChangeHandler(e, true)}
|
onChange={(e) => inputChangeHandler(e, { isBool: true })}
|
||||||
>
|
>
|
||||||
<option value={1}>True</option>
|
<option value={1}>True</option>
|
||||||
<option value={0}>False</option>
|
<option value={0}>False</option>
|
||||||
|
@ -204,8 +192,8 @@ const OtherSettings = (props: ComponentProps): JSX.Element => {
|
||||||
<select
|
<select
|
||||||
id="hideApps"
|
id="hideApps"
|
||||||
name="hideApps"
|
name="hideApps"
|
||||||
value={formData.hideApps}
|
value={formData.hideApps ? 1 : 0}
|
||||||
onChange={(e) => inputChangeHandler(e, true)}
|
onChange={(e) => inputChangeHandler(e, { isBool: true })}
|
||||||
>
|
>
|
||||||
<option value={1}>True</option>
|
<option value={1}>True</option>
|
||||||
<option value={0}>False</option>
|
<option value={0}>False</option>
|
||||||
|
@ -216,8 +204,8 @@ const OtherSettings = (props: ComponentProps): JSX.Element => {
|
||||||
<select
|
<select
|
||||||
id="hideCategories"
|
id="hideCategories"
|
||||||
name="hideCategories"
|
name="hideCategories"
|
||||||
value={formData.hideCategories}
|
value={formData.hideCategories ? 1 : 0}
|
||||||
onChange={(e) => inputChangeHandler(e, true)}
|
onChange={(e) => inputChangeHandler(e, { isBool: true })}
|
||||||
>
|
>
|
||||||
<option value={1}>True</option>
|
<option value={1}>True</option>
|
||||||
<option value={0}>False</option>
|
<option value={0}>False</option>
|
||||||
|
@ -242,8 +230,8 @@ const OtherSettings = (props: ComponentProps): JSX.Element => {
|
||||||
<select
|
<select
|
||||||
id="dockerApps"
|
id="dockerApps"
|
||||||
name="dockerApps"
|
name="dockerApps"
|
||||||
value={formData.dockerApps}
|
value={formData.dockerApps ? 1 : 0}
|
||||||
onChange={(e) => inputChangeHandler(e, true)}
|
onChange={(e) => inputChangeHandler(e, { isBool: true })}
|
||||||
>
|
>
|
||||||
<option value={1}>True</option>
|
<option value={1}>True</option>
|
||||||
<option value={0}>False</option>
|
<option value={0}>False</option>
|
||||||
|
@ -256,8 +244,8 @@ const OtherSettings = (props: ComponentProps): JSX.Element => {
|
||||||
<select
|
<select
|
||||||
id="unpinStoppedApps"
|
id="unpinStoppedApps"
|
||||||
name="unpinStoppedApps"
|
name="unpinStoppedApps"
|
||||||
value={formData.unpinStoppedApps}
|
value={formData.unpinStoppedApps ? 1 : 0}
|
||||||
onChange={(e) => inputChangeHandler(e, true)}
|
onChange={(e) => inputChangeHandler(e, { isBool: true })}
|
||||||
>
|
>
|
||||||
<option value={1}>True</option>
|
<option value={1}>True</option>
|
||||||
<option value={0}>False</option>
|
<option value={0}>False</option>
|
||||||
|
@ -271,8 +259,8 @@ const OtherSettings = (props: ComponentProps): JSX.Element => {
|
||||||
<select
|
<select
|
||||||
id="kubernetesApps"
|
id="kubernetesApps"
|
||||||
name="kubernetesApps"
|
name="kubernetesApps"
|
||||||
value={formData.kubernetesApps}
|
value={formData.kubernetesApps ? 1 : 0}
|
||||||
onChange={(e) => inputChangeHandler(e, true)}
|
onChange={(e) => inputChangeHandler(e, { isBool: true })}
|
||||||
>
|
>
|
||||||
<option value={1}>True</option>
|
<option value={1}>True</option>
|
||||||
<option value={0}>False</option>
|
<option value={0}>False</option>
|
||||||
|
@ -286,6 +274,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,
|
||||||
|
config: state.config.config,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -5,16 +5,21 @@ import classes from './CustomQueries.module.css';
|
||||||
|
|
||||||
import Modal from '../../../UI/Modal/Modal';
|
import Modal from '../../../UI/Modal/Modal';
|
||||||
import Icon from '../../../UI/Icons/Icon/Icon';
|
import Icon from '../../../UI/Icons/Icon/Icon';
|
||||||
import { GlobalState, NewNotification, Query } from '../../../../interfaces';
|
import {
|
||||||
|
Config,
|
||||||
|
GlobalState,
|
||||||
|
NewNotification,
|
||||||
|
Query,
|
||||||
|
} from '../../../../interfaces';
|
||||||
import QueriesForm from './QueriesForm';
|
import QueriesForm from './QueriesForm';
|
||||||
import { deleteQuery, createNotification } from '../../../../store/actions';
|
import { deleteQuery, createNotification } from '../../../../store/actions';
|
||||||
import Button from '../../../UI/Buttons/Button/Button';
|
import Button from '../../../UI/Buttons/Button/Button';
|
||||||
import { searchConfig } from '../../../../utility';
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
customQueries: Query[];
|
customQueries: Query[];
|
||||||
deleteQuery: (prefix: string) => {};
|
deleteQuery: (prefix: string) => {};
|
||||||
createNotification: (notification: NewNotification) => void;
|
createNotification: (notification: NewNotification) => void;
|
||||||
|
config: Config;
|
||||||
}
|
}
|
||||||
|
|
||||||
const CustomQueries = (props: Props): JSX.Element => {
|
const CustomQueries = (props: Props): JSX.Element => {
|
||||||
|
@ -29,7 +34,7 @@ const CustomQueries = (props: Props): JSX.Element => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const deleteHandler = (query: Query) => {
|
const deleteHandler = (query: Query) => {
|
||||||
const currentProvider = searchConfig('defaultSearchProvider', 'l');
|
const currentProvider = props.config.defaultSearchProvider;
|
||||||
const isCurrent = currentProvider === query.prefix;
|
const isCurrent = currentProvider === query.prefix;
|
||||||
|
|
||||||
if (isCurrent) {
|
if (isCurrent) {
|
||||||
|
@ -104,6 +109,7 @@ const CustomQueries = (props: Props): JSX.Element => {
|
||||||
const mapStateToProps = (state: GlobalState) => {
|
const mapStateToProps = (state: GlobalState) => {
|
||||||
return {
|
return {
|
||||||
customQueries: state.config.customQueries,
|
customQueries: state.config.customQueries,
|
||||||
|
config: state.config.config,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -7,6 +7,7 @@ import { createNotification, updateConfig } from '../../../store/actions';
|
||||||
|
|
||||||
// Typescript
|
// Typescript
|
||||||
import {
|
import {
|
||||||
|
Config,
|
||||||
GlobalState,
|
GlobalState,
|
||||||
NewNotification,
|
NewNotification,
|
||||||
Query,
|
Query,
|
||||||
|
@ -22,7 +23,7 @@ import SettingsHeadline from '../../UI/Headlines/SettingsHeadline/SettingsHeadli
|
||||||
import InputGroup from '../../UI/Forms/InputGroup/InputGroup';
|
import InputGroup from '../../UI/Forms/InputGroup/InputGroup';
|
||||||
|
|
||||||
// Utils
|
// Utils
|
||||||
import { searchConfig } from '../../../utility';
|
import { inputHandler, searchSettingsTemplate } from '../../../utility';
|
||||||
|
|
||||||
// Data
|
// Data
|
||||||
import { queries } from '../../../utility/searchQueries.json';
|
import { queries } from '../../../utility/searchQueries.json';
|
||||||
|
@ -32,22 +33,17 @@ interface Props {
|
||||||
updateConfig: (formData: SearchForm) => void;
|
updateConfig: (formData: SearchForm) => void;
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
customQueries: Query[];
|
customQueries: Query[];
|
||||||
|
config: Config;
|
||||||
}
|
}
|
||||||
|
|
||||||
const SearchSettings = (props: Props): JSX.Element => {
|
const SearchSettings = (props: Props): JSX.Element => {
|
||||||
// Initial state
|
// Initial state
|
||||||
const [formData, setFormData] = useState<SearchForm>({
|
const [formData, setFormData] = useState<SearchForm>(searchSettingsTemplate);
|
||||||
hideSearch: 0,
|
|
||||||
defaultSearchProvider: 'l',
|
|
||||||
searchSameTab: 0,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Get config
|
// Get config
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setFormData({
|
setFormData({
|
||||||
hideSearch: searchConfig('hideSearch', 0),
|
...props.config,
|
||||||
defaultSearchProvider: searchConfig('defaultSearchProvider', 'l'),
|
|
||||||
searchSameTab: searchConfig('searchSameTab', 0),
|
|
||||||
});
|
});
|
||||||
}, [props.loading]);
|
}, [props.loading]);
|
||||||
|
|
||||||
|
@ -62,17 +58,13 @@ const SearchSettings = (props: Props): JSX.Element => {
|
||||||
// Input handler
|
// Input handler
|
||||||
const inputChangeHandler = (
|
const inputChangeHandler = (
|
||||||
e: ChangeEvent<HTMLInputElement | HTMLSelectElement>,
|
e: ChangeEvent<HTMLInputElement | HTMLSelectElement>,
|
||||||
isNumber?: boolean
|
options?: { isNumber?: boolean; isBool?: boolean }
|
||||||
) => {
|
) => {
|
||||||
let value: string | number = e.target.value;
|
inputHandler<SearchForm>({
|
||||||
|
e,
|
||||||
if (isNumber) {
|
options,
|
||||||
value = parseFloat(value);
|
setStateHandler: setFormData,
|
||||||
}
|
state: formData,
|
||||||
|
|
||||||
setFormData({
|
|
||||||
...formData,
|
|
||||||
[e.target.name]: value,
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -110,8 +102,8 @@ const SearchSettings = (props: Props): JSX.Element => {
|
||||||
<select
|
<select
|
||||||
id="searchSameTab"
|
id="searchSameTab"
|
||||||
name="searchSameTab"
|
name="searchSameTab"
|
||||||
value={formData.searchSameTab}
|
value={formData.searchSameTab ? 1 : 0}
|
||||||
onChange={(e) => inputChangeHandler(e, true)}
|
onChange={(e) => inputChangeHandler(e, { isBool: true })}
|
||||||
>
|
>
|
||||||
<option value={1}>True</option>
|
<option value={1}>True</option>
|
||||||
<option value={0}>False</option>
|
<option value={0}>False</option>
|
||||||
|
@ -122,8 +114,8 @@ const SearchSettings = (props: Props): JSX.Element => {
|
||||||
<select
|
<select
|
||||||
id="hideSearch"
|
id="hideSearch"
|
||||||
name="hideSearch"
|
name="hideSearch"
|
||||||
value={formData.hideSearch}
|
value={formData.hideSearch ? 1 : 0}
|
||||||
onChange={(e) => inputChangeHandler(e, true)}
|
onChange={(e) => inputChangeHandler(e, { isBool: true })}
|
||||||
>
|
>
|
||||||
<option value={1}>True</option>
|
<option value={1}>True</option>
|
||||||
<option value={0}>False</option>
|
<option value={0}>False</option>
|
||||||
|
@ -143,6 +135,7 @@ const mapStateToProps = (state: GlobalState) => {
|
||||||
return {
|
return {
|
||||||
loading: state.config.loading,
|
loading: state.config.loading,
|
||||||
customQueries: state.config.customQueries,
|
customQueries: state.config.customQueries,
|
||||||
|
config: state.config.config,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -6,38 +6,40 @@ import { connect } from 'react-redux';
|
||||||
import { createNotification, updateConfig } from '../../../store/actions';
|
import { createNotification, updateConfig } from '../../../store/actions';
|
||||||
|
|
||||||
// Typescript
|
// Typescript
|
||||||
import { ApiResponse, GlobalState, NewNotification, Weather, WeatherForm } from '../../../interfaces';
|
import {
|
||||||
|
ApiResponse,
|
||||||
|
Config,
|
||||||
|
GlobalState,
|
||||||
|
NewNotification,
|
||||||
|
Weather,
|
||||||
|
WeatherForm,
|
||||||
|
} from '../../../interfaces';
|
||||||
|
|
||||||
// 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';
|
||||||
|
|
||||||
// Utils
|
// Utils
|
||||||
import { searchConfig } from '../../../utility';
|
import { inputHandler, weatherSettingsTemplate } from '../../../utility';
|
||||||
|
|
||||||
interface ComponentProps {
|
interface ComponentProps {
|
||||||
createNotification: (notification: NewNotification) => void;
|
createNotification: (notification: NewNotification) => void;
|
||||||
updateConfig: (formData: WeatherForm) => void;
|
updateConfig: (formData: WeatherForm) => void;
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
|
config: Config;
|
||||||
}
|
}
|
||||||
|
|
||||||
const WeatherSettings = (props: ComponentProps): JSX.Element => {
|
const WeatherSettings = (props: ComponentProps): JSX.Element => {
|
||||||
// Initial state
|
// Initial state
|
||||||
const [formData, setFormData] = useState<WeatherForm>({
|
const [formData, setFormData] = useState<WeatherForm>(
|
||||||
WEATHER_API_KEY: '',
|
weatherSettingsTemplate
|
||||||
lat: 0,
|
);
|
||||||
long: 0,
|
|
||||||
isCelsius: 1
|
|
||||||
})
|
|
||||||
|
|
||||||
// Get config
|
// Get config
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setFormData({
|
setFormData({
|
||||||
WEATHER_API_KEY: searchConfig('WEATHER_API_KEY', ''),
|
...props.config,
|
||||||
lat: searchConfig('lat', 0),
|
});
|
||||||
long: searchConfig('long', 0),
|
|
||||||
isCelsius: searchConfig('isCelsius', 1)
|
|
||||||
})
|
|
||||||
}, [props.loading]);
|
}, [props.loading]);
|
||||||
|
|
||||||
// Form handler
|
// Form handler
|
||||||
|
@ -48,106 +50,107 @@ const WeatherSettings = (props: ComponentProps): JSX.Element => {
|
||||||
if ((formData.lat || formData.long) && !formData.WEATHER_API_KEY) {
|
if ((formData.lat || formData.long) && !formData.WEATHER_API_KEY) {
|
||||||
props.createNotification({
|
props.createNotification({
|
||||||
title: 'Warning',
|
title: 'Warning',
|
||||||
message: 'API key is missing. Weather Module will NOT work'
|
message: 'API key is missing. Weather Module will NOT work',
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save settings
|
// Save settings
|
||||||
await props.updateConfig(formData);
|
await props.updateConfig(formData);
|
||||||
|
|
||||||
// Update weather
|
// Update weather
|
||||||
axios.get<ApiResponse<Weather>>('/api/weather/update')
|
axios
|
||||||
|
.get<ApiResponse<Weather>>('/api/weather/update')
|
||||||
.then(() => {
|
.then(() => {
|
||||||
props.createNotification({
|
props.createNotification({
|
||||||
title: 'Success',
|
title: 'Success',
|
||||||
message: 'Weather updated'
|
message: 'Weather updated',
|
||||||
})
|
});
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
props.createNotification({
|
props.createNotification({
|
||||||
title: 'Error',
|
title: 'Error',
|
||||||
message: err.response.data.error
|
message: err.response.data.error,
|
||||||
})
|
|
||||||
});
|
});
|
||||||
}
|
});
|
||||||
|
};
|
||||||
|
|
||||||
// Input handler
|
// Input handler
|
||||||
const inputChangeHandler = (e: ChangeEvent<HTMLInputElement | HTMLSelectElement>, isNumber?: boolean) => {
|
const inputChangeHandler = (
|
||||||
let value: string | number = e.target.value;
|
e: ChangeEvent<HTMLInputElement | HTMLSelectElement>,
|
||||||
|
options?: { isNumber?: boolean; isBool?: boolean }
|
||||||
if (isNumber) {
|
) => {
|
||||||
value = parseFloat(value);
|
inputHandler<WeatherForm>({
|
||||||
}
|
e,
|
||||||
|
options,
|
||||||
setFormData({
|
setStateHandler: setFormData,
|
||||||
...formData,
|
state: formData,
|
||||||
[e.target.name]: value
|
});
|
||||||
})
|
};
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={(e) => formSubmitHandler(e)}>
|
<form onSubmit={(e) => formSubmitHandler(e)}>
|
||||||
<InputGroup>
|
<InputGroup>
|
||||||
<label htmlFor='WEATHER_API_KEY'>API key</label>
|
<label htmlFor="WEATHER_API_KEY">API key</label>
|
||||||
<input
|
<input
|
||||||
type='text'
|
type="text"
|
||||||
id='WEATHER_API_KEY'
|
id="WEATHER_API_KEY"
|
||||||
name='WEATHER_API_KEY'
|
name="WEATHER_API_KEY"
|
||||||
placeholder='secret'
|
placeholder="secret"
|
||||||
value={formData.WEATHER_API_KEY}
|
value={formData.WEATHER_API_KEY}
|
||||||
onChange={(e) => inputChangeHandler(e)}
|
onChange={(e) => inputChangeHandler(e)}
|
||||||
/>
|
/>
|
||||||
<span>
|
<span>
|
||||||
Using
|
Using
|
||||||
<a
|
<a href="https://www.weatherapi.com/pricing.aspx" target="blank">
|
||||||
href='https://www.weatherapi.com/pricing.aspx'
|
{' '}
|
||||||
target='blank'>
|
Weather API
|
||||||
{' '}Weather API
|
|
||||||
</a>
|
</a>
|
||||||
. Key is required for weather module to work.
|
. Key is required for weather module to work.
|
||||||
</span>
|
</span>
|
||||||
</InputGroup>
|
</InputGroup>
|
||||||
<InputGroup>
|
<InputGroup>
|
||||||
<label htmlFor='lat'>Location latitude</label>
|
<label htmlFor="lat">Location latitude</label>
|
||||||
<input
|
<input
|
||||||
type='number'
|
type="number"
|
||||||
id='lat'
|
id="lat"
|
||||||
name='lat'
|
name="lat"
|
||||||
placeholder='52.22'
|
placeholder="52.22"
|
||||||
value={formData.lat}
|
value={formData.lat}
|
||||||
onChange={(e) => inputChangeHandler(e, true)}
|
onChange={(e) => inputChangeHandler(e, { isNumber: true })}
|
||||||
step='any'
|
step="any"
|
||||||
lang='en-150'
|
lang="en-150"
|
||||||
/>
|
/>
|
||||||
<span>
|
<span>
|
||||||
You can use
|
You can use
|
||||||
<a
|
<a
|
||||||
href='https://www.latlong.net/convert-address-to-lat-long.html'
|
href="https://www.latlong.net/convert-address-to-lat-long.html"
|
||||||
target='blank'>
|
target="blank"
|
||||||
{' '}latlong.net
|
>
|
||||||
|
{' '}
|
||||||
|
latlong.net
|
||||||
</a>
|
</a>
|
||||||
</span>
|
</span>
|
||||||
</InputGroup>
|
</InputGroup>
|
||||||
<InputGroup>
|
<InputGroup>
|
||||||
<label htmlFor='long'>Location longitude</label>
|
<label htmlFor="long">Location longitude</label>
|
||||||
<input
|
<input
|
||||||
type='number'
|
type="number"
|
||||||
id='long'
|
id="long"
|
||||||
name='long'
|
name="long"
|
||||||
placeholder='21.01'
|
placeholder="21.01"
|
||||||
value={formData.long}
|
value={formData.long}
|
||||||
onChange={(e) => inputChangeHandler(e, true)}
|
onChange={(e) => inputChangeHandler(e, { isNumber: true })}
|
||||||
step='any'
|
step="any"
|
||||||
lang='en-150'
|
lang="en-150"
|
||||||
/>
|
/>
|
||||||
</InputGroup>
|
</InputGroup>
|
||||||
<InputGroup>
|
<InputGroup>
|
||||||
<label htmlFor='isCelsius'>Temperature unit</label>
|
<label htmlFor="isCelsius">Temperature unit</label>
|
||||||
<select
|
<select
|
||||||
id='isCelsius'
|
id="isCelsius"
|
||||||
name='isCelsius'
|
name="isCelsius"
|
||||||
onChange={(e) => inputChangeHandler(e, true)}
|
onChange={(e) => inputChangeHandler(e, { isBool: true })}
|
||||||
value={formData.isCelsius}
|
value={formData.isCelsius ? 1 : 0}
|
||||||
>
|
>
|
||||||
<option value={1}>Celsius</option>
|
<option value={1}>Celsius</option>
|
||||||
<option value={0}>Fahrenheit</option>
|
<option value={0}>Fahrenheit</option>
|
||||||
|
@ -155,13 +158,16 @@ const WeatherSettings = (props: ComponentProps): JSX.Element => {
|
||||||
</InputGroup>
|
</InputGroup>
|
||||||
<Button>Save changes</Button>
|
<Button>Save changes</Button>
|
||||||
</form>
|
</form>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
const mapStateToProps = (state: GlobalState) => {
|
const mapStateToProps = (state: GlobalState) => {
|
||||||
return {
|
return {
|
||||||
loading: state.config.loading
|
loading: state.config.loading,
|
||||||
}
|
config: state.config.config,
|
||||||
}
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export default connect(mapStateToProps, { createNotification, updateConfig })(WeatherSettings);
|
export default connect(mapStateToProps, { createNotification, updateConfig })(
|
||||||
|
WeatherSettings
|
||||||
|
);
|
||||||
|
|
|
@ -5,7 +5,7 @@ import axios from 'axios';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
|
|
||||||
// Typescript
|
// Typescript
|
||||||
import { Weather, ApiResponse, Config, GlobalState } from '../../../interfaces';
|
import { Weather, ApiResponse, GlobalState, Config } from '../../../interfaces';
|
||||||
|
|
||||||
// CSS
|
// CSS
|
||||||
import classes from './WeatherWidget.module.css';
|
import classes from './WeatherWidget.module.css';
|
||||||
|
@ -13,12 +13,9 @@ import classes from './WeatherWidget.module.css';
|
||||||
// UI
|
// UI
|
||||||
import WeatherIcon from '../../UI/Icons/WeatherIcon/WeatherIcon';
|
import WeatherIcon from '../../UI/Icons/WeatherIcon/WeatherIcon';
|
||||||
|
|
||||||
// Utils
|
|
||||||
import { searchConfig } from '../../../utility';
|
|
||||||
|
|
||||||
interface ComponentProps {
|
interface ComponentProps {
|
||||||
configLoading: boolean;
|
configLoading: boolean;
|
||||||
config: Config[];
|
config: Config;
|
||||||
}
|
}
|
||||||
|
|
||||||
const WeatherWidget = (props: ComponentProps): JSX.Element => {
|
const WeatherWidget = (props: ComponentProps): JSX.Element => {
|
||||||
|
@ -32,26 +29,28 @@ const WeatherWidget = (props: ComponentProps): JSX.Element => {
|
||||||
conditionCode: 1000,
|
conditionCode: 1000,
|
||||||
id: -1,
|
id: -1,
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
updatedAt: new Date()
|
updatedAt: new Date(),
|
||||||
});
|
});
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
|
||||||
// Initial request to get data
|
// Initial request to get data
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
axios.get<ApiResponse<Weather[]>>('/api/weather')
|
axios
|
||||||
.then(data => {
|
.get<ApiResponse<Weather[]>>('/api/weather')
|
||||||
|
.then((data) => {
|
||||||
const weatherData = data.data.data[0];
|
const weatherData = data.data.data[0];
|
||||||
if (weatherData) {
|
if (weatherData) {
|
||||||
setWeather(weatherData);
|
setWeather(weatherData);
|
||||||
}
|
}
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
})
|
})
|
||||||
.catch(err => console.log(err));
|
.catch((err) => console.log(err));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Open socket for data updates
|
// Open socket for data updates
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const socketProtocol = document.location.protocol === 'http:' ? 'ws:' : 'wss:';
|
const socketProtocol =
|
||||||
|
document.location.protocol === 'http:' ? 'ws:' : 'wss:';
|
||||||
const socketAddress = `${socketProtocol}//${window.location.host}/socket`;
|
const socketAddress = `${socketProtocol}//${window.location.host}/socket`;
|
||||||
const webSocketClient = new WebSocket(socketAddress);
|
const webSocketClient = new WebSocket(socketAddress);
|
||||||
|
|
||||||
|
@ -59,18 +58,19 @@ const WeatherWidget = (props: ComponentProps): JSX.Element => {
|
||||||
const data = JSON.parse(e.data);
|
const data = JSON.parse(e.data);
|
||||||
setWeather({
|
setWeather({
|
||||||
...weather,
|
...weather,
|
||||||
...data
|
...data,
|
||||||
})
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
return () => webSocketClient.close();
|
return () => webSocketClient.close();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={classes.WeatherWidget}>
|
<div className={classes.WeatherWidget}>
|
||||||
{(isLoading || props.configLoading || searchConfig('WEATHER_API_KEY', '')) &&
|
{isLoading ||
|
||||||
(weather.id > 0 &&
|
props.configLoading ||
|
||||||
(<Fragment>
|
(props.config.WEATHER_API_KEY && weather.id > 0 && (
|
||||||
|
<Fragment>
|
||||||
<div className={classes.WeatherIcon}>
|
<div className={classes.WeatherIcon}>
|
||||||
<WeatherIcon
|
<WeatherIcon
|
||||||
weatherStatusCode={weather.conditionCode}
|
weatherStatusCode={weather.conditionCode}
|
||||||
|
@ -78,24 +78,24 @@ const WeatherWidget = (props: ComponentProps): JSX.Element => {
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className={classes.WeatherDetails}>
|
<div className={classes.WeatherDetails}>
|
||||||
{searchConfig('isCelsius', true)
|
{props.config.isCelsius ? (
|
||||||
? <span>{weather.tempC}°C</span>
|
<span>{weather.tempC}°C</span>
|
||||||
: <span>{weather.tempF}°F</span>
|
) : (
|
||||||
}
|
<span>{weather.tempF}°F</span>
|
||||||
|
)}
|
||||||
<span>{weather.cloud}%</span>
|
<span>{weather.cloud}%</span>
|
||||||
</div>
|
</div>
|
||||||
</Fragment>)
|
</Fragment>
|
||||||
)
|
))}
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
const mapStateToProps = (state: GlobalState) => {
|
const mapStateToProps = (state: GlobalState) => {
|
||||||
return {
|
return {
|
||||||
configLoading: state.config.loading,
|
configLoading: state.config.loading,
|
||||||
config: state.config.config
|
config: state.config.config,
|
||||||
}
|
};
|
||||||
}
|
};
|
||||||
|
|
||||||
export default connect(mapStateToProps)(WeatherWidget);
|
export default connect(mapStateToProps)(WeatherWidget);
|
|
@ -1,8 +1,23 @@
|
||||||
import { Model } from './';
|
export interface Config {
|
||||||
|
WEATHER_API_KEY: string;
|
||||||
export interface Config extends Model {
|
lat: number;
|
||||||
key: string;
|
long: number;
|
||||||
value: string;
|
isCelsius: boolean;
|
||||||
valueType: string;
|
customTitle: string;
|
||||||
isLocked: boolean;
|
pinAppsByDefault: boolean;
|
||||||
|
pinCategoriesByDefault: boolean;
|
||||||
|
hideHeader: boolean;
|
||||||
|
useOrdering: string;
|
||||||
|
appsSameTab: boolean;
|
||||||
|
bookmarksSameTab: boolean;
|
||||||
|
searchSameTab: boolean;
|
||||||
|
hideApps: boolean;
|
||||||
|
hideCategories: boolean;
|
||||||
|
hideSearch: boolean;
|
||||||
|
defaultSearchProvider: string;
|
||||||
|
dockerApps: boolean;
|
||||||
|
dockerHost: string;
|
||||||
|
kubernetesApps: boolean;
|
||||||
|
unpinStoppedApps: boolean;
|
||||||
|
useAmericanDate: boolean;
|
||||||
}
|
}
|
|
@ -2,30 +2,28 @@ export interface WeatherForm {
|
||||||
WEATHER_API_KEY: string;
|
WEATHER_API_KEY: string;
|
||||||
lat: number;
|
lat: number;
|
||||||
long: number;
|
long: number;
|
||||||
isCelsius: number;
|
isCelsius: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SearchForm {
|
export interface SearchForm {
|
||||||
hideSearch: number;
|
hideSearch: boolean;
|
||||||
defaultSearchProvider: string;
|
defaultSearchProvider: string;
|
||||||
searchSameTab: number;
|
searchSameTab: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SettingsForm {
|
export interface OtherSettingsForm {
|
||||||
customTitle: string;
|
customTitle: string;
|
||||||
pinAppsByDefault: number;
|
pinAppsByDefault: boolean;
|
||||||
pinCategoriesByDefault: number;
|
pinCategoriesByDefault: boolean;
|
||||||
hideHeader: number;
|
hideHeader: boolean;
|
||||||
hideApps: number;
|
hideApps: boolean;
|
||||||
hideCategories: number;
|
hideCategories: boolean;
|
||||||
// hideSearch: number;
|
|
||||||
// defaultSearchProvider: string;
|
|
||||||
useOrdering: string;
|
useOrdering: string;
|
||||||
appsSameTab: number;
|
appsSameTab: boolean;
|
||||||
bookmarksSameTab: number;
|
bookmarksSameTab: boolean;
|
||||||
// searchSameTab: number;
|
dockerApps: boolean;
|
||||||
dockerApps: number;
|
|
||||||
dockerHost: string;
|
dockerHost: string;
|
||||||
kubernetesApps: number;
|
kubernetesApps: boolean;
|
||||||
unpinStoppedApps: number;
|
unpinStoppedApps: boolean;
|
||||||
|
useAmericanDate: boolean;
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,14 +5,17 @@ import { App, ApiResponse, NewApp, Config } from '../../interfaces';
|
||||||
import { CreateNotificationAction } from './notification';
|
import { CreateNotificationAction } from './notification';
|
||||||
|
|
||||||
export interface GetAppsAction<T> {
|
export interface GetAppsAction<T> {
|
||||||
type: ActionTypes.getApps | ActionTypes.getAppsSuccess | ActionTypes.getAppsError;
|
type:
|
||||||
|
| ActionTypes.getApps
|
||||||
|
| ActionTypes.getAppsSuccess
|
||||||
|
| ActionTypes.getAppsError;
|
||||||
payload: T;
|
payload: T;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getApps = () => async (dispatch: Dispatch) => {
|
export const getApps = () => async (dispatch: Dispatch) => {
|
||||||
dispatch<GetAppsAction<undefined>>({
|
dispatch<GetAppsAction<undefined>>({
|
||||||
type: ActionTypes.getApps,
|
type: ActionTypes.getApps,
|
||||||
payload: undefined
|
payload: undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
@ -20,12 +23,12 @@ export const getApps = () => async (dispatch: Dispatch) => {
|
||||||
|
|
||||||
dispatch<GetAppsAction<App[]>>({
|
dispatch<GetAppsAction<App[]>>({
|
||||||
type: ActionTypes.getAppsSuccess,
|
type: ActionTypes.getAppsSuccess,
|
||||||
payload: res.data.data
|
payload: res.data.data,
|
||||||
})
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log(err);
|
console.log(err);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
export interface PinAppAction {
|
export interface PinAppAction {
|
||||||
type: ActionTypes.pinApp;
|
type: ActionTypes.pinApp;
|
||||||
|
@ -35,33 +38,38 @@ export interface PinAppAction {
|
||||||
export const pinApp = (app: App) => async (dispatch: Dispatch) => {
|
export const pinApp = (app: App) => async (dispatch: Dispatch) => {
|
||||||
try {
|
try {
|
||||||
const { id, isPinned, name } = app;
|
const { id, isPinned, name } = app;
|
||||||
const res = await axios.put<ApiResponse<App>>(`/api/apps/${id}`, { isPinned: !isPinned });
|
const res = await axios.put<ApiResponse<App>>(`/api/apps/${id}`, {
|
||||||
|
isPinned: !isPinned,
|
||||||
|
});
|
||||||
|
|
||||||
const status = isPinned ? 'unpinned from Homescreen' : 'pinned to Homescreen';
|
const status = isPinned
|
||||||
|
? 'unpinned from Homescreen'
|
||||||
|
: 'pinned to Homescreen';
|
||||||
|
|
||||||
dispatch<CreateNotificationAction>({
|
dispatch<CreateNotificationAction>({
|
||||||
type: ActionTypes.createNotification,
|
type: ActionTypes.createNotification,
|
||||||
payload: {
|
payload: {
|
||||||
title: 'Success',
|
title: 'Success',
|
||||||
message: `App ${name} ${status}`
|
message: `App ${name} ${status}`,
|
||||||
}
|
},
|
||||||
})
|
});
|
||||||
|
|
||||||
dispatch<PinAppAction>({
|
dispatch<PinAppAction>({
|
||||||
type: ActionTypes.pinApp,
|
type: ActionTypes.pinApp,
|
||||||
payload: res.data.data
|
payload: res.data.data,
|
||||||
})
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log(err);
|
console.log(err);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
export interface AddAppAction {
|
export interface AddAppAction {
|
||||||
type: ActionTypes.addAppSuccess;
|
type: ActionTypes.addAppSuccess;
|
||||||
payload: App;
|
payload: App;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const addApp = (formData: NewApp | FormData) => async (dispatch: Dispatch) => {
|
export const addApp =
|
||||||
|
(formData: NewApp | FormData) => async (dispatch: Dispatch) => {
|
||||||
try {
|
try {
|
||||||
const res = await axios.post<ApiResponse<App>>('/api/apps', formData);
|
const res = await axios.post<ApiResponse<App>>('/api/apps', formData);
|
||||||
|
|
||||||
|
@ -69,25 +77,25 @@ export const addApp = (formData: NewApp | FormData) => async (dispatch: Dispatch
|
||||||
type: ActionTypes.createNotification,
|
type: ActionTypes.createNotification,
|
||||||
payload: {
|
payload: {
|
||||||
title: 'Success',
|
title: 'Success',
|
||||||
message: `App added`
|
message: `App added`,
|
||||||
}
|
},
|
||||||
})
|
});
|
||||||
|
|
||||||
await dispatch<AddAppAction>({
|
await dispatch<AddAppAction>({
|
||||||
type: ActionTypes.addAppSuccess,
|
type: ActionTypes.addAppSuccess,
|
||||||
payload: res.data.data
|
payload: res.data.data,
|
||||||
})
|
});
|
||||||
|
|
||||||
// Sort apps
|
// Sort apps
|
||||||
dispatch<any>(sortApps())
|
dispatch<any>(sortApps());
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log(err);
|
console.log(err);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
export interface DeleteAppAction {
|
export interface DeleteAppAction {
|
||||||
type: ActionTypes.deleteApp,
|
type: ActionTypes.deleteApp;
|
||||||
payload: number
|
payload: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const deleteApp = (id: number) => async (dispatch: Dispatch) => {
|
export const deleteApp = (id: number) => async (dispatch: Dispatch) => {
|
||||||
|
@ -98,79 +106,85 @@ export const deleteApp = (id: number) => async (dispatch: Dispatch) => {
|
||||||
type: ActionTypes.createNotification,
|
type: ActionTypes.createNotification,
|
||||||
payload: {
|
payload: {
|
||||||
title: 'Success',
|
title: 'Success',
|
||||||
message: 'App deleted'
|
message: 'App deleted',
|
||||||
}
|
},
|
||||||
})
|
});
|
||||||
|
|
||||||
dispatch<DeleteAppAction>({
|
dispatch<DeleteAppAction>({
|
||||||
type: ActionTypes.deleteApp,
|
type: ActionTypes.deleteApp,
|
||||||
payload: id
|
payload: id,
|
||||||
})
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log(err);
|
console.log(err);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
export interface UpdateAppAction {
|
export interface UpdateAppAction {
|
||||||
type: ActionTypes.updateApp;
|
type: ActionTypes.updateApp;
|
||||||
payload: App;
|
payload: App;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const updateApp = (id: number, formData: NewApp | FormData) => async (dispatch: Dispatch) => {
|
export const updateApp =
|
||||||
|
(id: number, formData: NewApp | FormData) => async (dispatch: Dispatch) => {
|
||||||
try {
|
try {
|
||||||
const res = await axios.put<ApiResponse<App>>(`/api/apps/${id}`, formData);
|
const res = await axios.put<ApiResponse<App>>(
|
||||||
|
`/api/apps/${id}`,
|
||||||
|
formData
|
||||||
|
);
|
||||||
|
|
||||||
dispatch<CreateNotificationAction>({
|
dispatch<CreateNotificationAction>({
|
||||||
type: ActionTypes.createNotification,
|
type: ActionTypes.createNotification,
|
||||||
payload: {
|
payload: {
|
||||||
title: 'Success',
|
title: 'Success',
|
||||||
message: `App updated`
|
message: `App updated`,
|
||||||
}
|
},
|
||||||
})
|
});
|
||||||
|
|
||||||
await dispatch<UpdateAppAction>({
|
await dispatch<UpdateAppAction>({
|
||||||
type: ActionTypes.updateApp,
|
type: ActionTypes.updateApp,
|
||||||
payload: res.data.data
|
payload: res.data.data,
|
||||||
})
|
});
|
||||||
|
|
||||||
// Sort apps
|
// Sort apps
|
||||||
dispatch<any>(sortApps())
|
dispatch<any>(sortApps());
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log(err);
|
console.log(err);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
export interface ReorderAppsAction {
|
export interface ReorderAppsAction {
|
||||||
type: ActionTypes.reorderApps;
|
type: ActionTypes.reorderApps;
|
||||||
payload: App[]
|
payload: App[];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ReorderQuery {
|
interface ReorderQuery {
|
||||||
apps: {
|
apps: {
|
||||||
id: number;
|
id: number;
|
||||||
orderId: number;
|
orderId: number;
|
||||||
}[]
|
}[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const reorderApps = (apps: App[]) => async (dispatch: Dispatch) => {
|
export const reorderApps = (apps: App[]) => async (dispatch: Dispatch) => {
|
||||||
try {
|
try {
|
||||||
const updateQuery: ReorderQuery = { apps: [] }
|
const updateQuery: ReorderQuery = { apps: [] };
|
||||||
|
|
||||||
apps.forEach((app, index) => updateQuery.apps.push({
|
apps.forEach((app, index) =>
|
||||||
|
updateQuery.apps.push({
|
||||||
id: app.id,
|
id: app.id,
|
||||||
orderId: index + 1
|
orderId: index + 1,
|
||||||
}))
|
})
|
||||||
|
);
|
||||||
|
|
||||||
await axios.put<ApiResponse<{}>>('/api/apps/0/reorder', updateQuery);
|
await axios.put<ApiResponse<{}>>('/api/apps/0/reorder', updateQuery);
|
||||||
|
|
||||||
dispatch<ReorderAppsAction>({
|
dispatch<ReorderAppsAction>({
|
||||||
type: ActionTypes.reorderApps,
|
type: ActionTypes.reorderApps,
|
||||||
payload: apps
|
payload: apps,
|
||||||
})
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log(err);
|
console.log(err);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
export interface SortAppsAction {
|
export interface SortAppsAction {
|
||||||
type: ActionTypes.sortApps;
|
type: ActionTypes.sortApps;
|
||||||
|
@ -179,13 +193,13 @@ export interface SortAppsAction {
|
||||||
|
|
||||||
export const sortApps = () => async (dispatch: Dispatch) => {
|
export const sortApps = () => async (dispatch: Dispatch) => {
|
||||||
try {
|
try {
|
||||||
const res = await axios.get<ApiResponse<Config>>('/api/config/useOrdering');
|
const res = await axios.get<ApiResponse<Config>>('/api/config');
|
||||||
|
|
||||||
dispatch<SortAppsAction>({
|
dispatch<SortAppsAction>({
|
||||||
type: ActionTypes.sortApps,
|
type: ActionTypes.sortApps,
|
||||||
payload: res.data.data.value
|
payload: res.data.data.useOrdering,
|
||||||
})
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log(err);
|
console.log(err);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
|
@ -1,133 +1,157 @@
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { Dispatch } from 'redux';
|
import { Dispatch } from 'redux';
|
||||||
import { ActionTypes } from './actionTypes';
|
import { ActionTypes } from './actionTypes';
|
||||||
import { Category, ApiResponse, NewCategory, Bookmark, NewBookmark, Config } from '../../interfaces';
|
import {
|
||||||
|
Category,
|
||||||
|
ApiResponse,
|
||||||
|
NewCategory,
|
||||||
|
Bookmark,
|
||||||
|
NewBookmark,
|
||||||
|
Config,
|
||||||
|
} from '../../interfaces';
|
||||||
import { CreateNotificationAction } from './notification';
|
import { CreateNotificationAction } from './notification';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* GET CATEGORIES
|
* GET CATEGORIES
|
||||||
*/
|
*/
|
||||||
export interface GetCategoriesAction<T> {
|
export interface GetCategoriesAction<T> {
|
||||||
type: ActionTypes.getCategories | ActionTypes.getCategoriesSuccess | ActionTypes.getCategoriesError;
|
type:
|
||||||
|
| ActionTypes.getCategories
|
||||||
|
| ActionTypes.getCategoriesSuccess
|
||||||
|
| ActionTypes.getCategoriesError;
|
||||||
payload: T;
|
payload: T;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getCategories = () => async (dispatch: Dispatch) => {
|
export const getCategories = () => async (dispatch: Dispatch) => {
|
||||||
dispatch<GetCategoriesAction<undefined>>({
|
dispatch<GetCategoriesAction<undefined>>({
|
||||||
type: ActionTypes.getCategories,
|
type: ActionTypes.getCategories,
|
||||||
payload: undefined
|
payload: undefined,
|
||||||
})
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await axios.get<ApiResponse<Category[]>>('/api/categories');
|
const res = await axios.get<ApiResponse<Category[]>>('/api/categories');
|
||||||
|
|
||||||
dispatch<GetCategoriesAction<Category[]>>({
|
dispatch<GetCategoriesAction<Category[]>>({
|
||||||
type: ActionTypes.getCategoriesSuccess,
|
type: ActionTypes.getCategoriesSuccess,
|
||||||
payload: res.data.data
|
payload: res.data.data,
|
||||||
})
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log(err);
|
console.log(err);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ADD CATEGORY
|
* ADD CATEGORY
|
||||||
*/
|
*/
|
||||||
export interface AddCategoryAction {
|
export interface AddCategoryAction {
|
||||||
type: ActionTypes.addCategory,
|
type: ActionTypes.addCategory;
|
||||||
payload: Category
|
payload: Category;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const addCategory = (formData: NewCategory) => async (dispatch: Dispatch) => {
|
export const addCategory =
|
||||||
|
(formData: NewCategory) => async (dispatch: Dispatch) => {
|
||||||
try {
|
try {
|
||||||
const res = await axios.post<ApiResponse<Category>>('/api/categories', formData);
|
const res = await axios.post<ApiResponse<Category>>(
|
||||||
|
'/api/categories',
|
||||||
|
formData
|
||||||
|
);
|
||||||
|
|
||||||
dispatch<CreateNotificationAction>({
|
dispatch<CreateNotificationAction>({
|
||||||
type: ActionTypes.createNotification,
|
type: ActionTypes.createNotification,
|
||||||
payload: {
|
payload: {
|
||||||
title: 'Success',
|
title: 'Success',
|
||||||
message: `Category ${formData.name} created`
|
message: `Category ${formData.name} created`,
|
||||||
}
|
},
|
||||||
})
|
});
|
||||||
|
|
||||||
dispatch<AddCategoryAction>({
|
dispatch<AddCategoryAction>({
|
||||||
type: ActionTypes.addCategory,
|
type: ActionTypes.addCategory,
|
||||||
payload: res.data.data
|
payload: res.data.data,
|
||||||
})
|
});
|
||||||
|
|
||||||
dispatch<any>(sortCategories());
|
dispatch<any>(sortCategories());
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log(err);
|
console.log(err);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ADD BOOKMARK
|
* ADD BOOKMARK
|
||||||
*/
|
*/
|
||||||
export interface AddBookmarkAction {
|
export interface AddBookmarkAction {
|
||||||
type: ActionTypes.addBookmark,
|
type: ActionTypes.addBookmark;
|
||||||
payload: Bookmark
|
payload: Bookmark;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const addBookmark = (formData: NewBookmark | FormData) => async (dispatch: Dispatch) => {
|
export const addBookmark =
|
||||||
|
(formData: NewBookmark | FormData) => async (dispatch: Dispatch) => {
|
||||||
try {
|
try {
|
||||||
const res = await axios.post<ApiResponse<Bookmark>>('/api/bookmarks', formData);
|
const res = await axios.post<ApiResponse<Bookmark>>(
|
||||||
|
'/api/bookmarks',
|
||||||
|
formData
|
||||||
|
);
|
||||||
|
|
||||||
dispatch<CreateNotificationAction>({
|
dispatch<CreateNotificationAction>({
|
||||||
type: ActionTypes.createNotification,
|
type: ActionTypes.createNotification,
|
||||||
payload: {
|
payload: {
|
||||||
title: 'Success',
|
title: 'Success',
|
||||||
message: `Bookmark created`
|
message: `Bookmark created`,
|
||||||
}
|
},
|
||||||
})
|
});
|
||||||
|
|
||||||
dispatch<AddBookmarkAction>({
|
dispatch<AddBookmarkAction>({
|
||||||
type: ActionTypes.addBookmark,
|
type: ActionTypes.addBookmark,
|
||||||
payload: res.data.data
|
payload: res.data.data,
|
||||||
})
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log(err);
|
console.log(err);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* PIN CATEGORY
|
* PIN CATEGORY
|
||||||
*/
|
*/
|
||||||
export interface PinCategoryAction {
|
export interface PinCategoryAction {
|
||||||
type: ActionTypes.pinCategory,
|
type: ActionTypes.pinCategory;
|
||||||
payload: Category
|
payload: Category;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const pinCategory = (category: Category) => async (dispatch: Dispatch) => {
|
export const pinCategory =
|
||||||
|
(category: Category) => async (dispatch: Dispatch) => {
|
||||||
try {
|
try {
|
||||||
const { id, isPinned, name } = category;
|
const { id, isPinned, name } = category;
|
||||||
const res = await axios.put<ApiResponse<Category>>(`/api/categories/${id}`, { isPinned: !isPinned });
|
const res = await axios.put<ApiResponse<Category>>(
|
||||||
|
`/api/categories/${id}`,
|
||||||
|
{ isPinned: !isPinned }
|
||||||
|
);
|
||||||
|
|
||||||
const status = isPinned ? 'unpinned from Homescreen' : 'pinned to Homescreen';
|
const status = isPinned
|
||||||
|
? 'unpinned from Homescreen'
|
||||||
|
: 'pinned to Homescreen';
|
||||||
|
|
||||||
dispatch<CreateNotificationAction>({
|
dispatch<CreateNotificationAction>({
|
||||||
type: ActionTypes.createNotification,
|
type: ActionTypes.createNotification,
|
||||||
payload: {
|
payload: {
|
||||||
title: 'Success',
|
title: 'Success',
|
||||||
message: `Category ${name} ${status}`
|
message: `Category ${name} ${status}`,
|
||||||
}
|
},
|
||||||
})
|
});
|
||||||
|
|
||||||
dispatch<PinCategoryAction>({
|
dispatch<PinCategoryAction>({
|
||||||
type: ActionTypes.pinCategory,
|
type: ActionTypes.pinCategory,
|
||||||
payload: res.data.data
|
payload: res.data.data,
|
||||||
})
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log(err);
|
console.log(err);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* DELETE CATEGORY
|
* DELETE CATEGORY
|
||||||
*/
|
*/
|
||||||
export interface DeleteCategoryAction {
|
export interface DeleteCategoryAction {
|
||||||
type: ActionTypes.deleteCategory,
|
type: ActionTypes.deleteCategory;
|
||||||
payload: number
|
payload: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const deleteCategory = (id: number) => async (dispatch: Dispatch) => {
|
export const deleteCategory = (id: number) => async (dispatch: Dispatch) => {
|
||||||
|
@ -138,62 +162,67 @@ export const deleteCategory = (id: number) => async (dispatch: Dispatch) => {
|
||||||
type: ActionTypes.createNotification,
|
type: ActionTypes.createNotification,
|
||||||
payload: {
|
payload: {
|
||||||
title: 'Success',
|
title: 'Success',
|
||||||
message: `Category deleted`
|
message: `Category deleted`,
|
||||||
}
|
},
|
||||||
})
|
});
|
||||||
|
|
||||||
dispatch<DeleteCategoryAction>({
|
dispatch<DeleteCategoryAction>({
|
||||||
type: ActionTypes.deleteCategory,
|
type: ActionTypes.deleteCategory,
|
||||||
payload: id
|
payload: id,
|
||||||
})
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log(err);
|
console.log(err);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* UPDATE CATEGORY
|
* UPDATE CATEGORY
|
||||||
*/
|
*/
|
||||||
export interface UpdateCategoryAction {
|
export interface UpdateCategoryAction {
|
||||||
type: ActionTypes.updateCategory,
|
type: ActionTypes.updateCategory;
|
||||||
payload: Category
|
payload: Category;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const updateCategory = (id: number, formData: NewCategory) => async (dispatch: Dispatch) => {
|
export const updateCategory =
|
||||||
|
(id: number, formData: NewCategory) => async (dispatch: Dispatch) => {
|
||||||
try {
|
try {
|
||||||
const res = await axios.put<ApiResponse<Category>>(`/api/categories/${id}`, formData);
|
const res = await axios.put<ApiResponse<Category>>(
|
||||||
|
`/api/categories/${id}`,
|
||||||
|
formData
|
||||||
|
);
|
||||||
|
|
||||||
dispatch<CreateNotificationAction>({
|
dispatch<CreateNotificationAction>({
|
||||||
type: ActionTypes.createNotification,
|
type: ActionTypes.createNotification,
|
||||||
payload: {
|
payload: {
|
||||||
title: 'Success',
|
title: 'Success',
|
||||||
message: `Category ${formData.name} updated`
|
message: `Category ${formData.name} updated`,
|
||||||
}
|
},
|
||||||
})
|
});
|
||||||
|
|
||||||
dispatch<UpdateCategoryAction>({
|
dispatch<UpdateCategoryAction>({
|
||||||
type: ActionTypes.updateCategory,
|
type: ActionTypes.updateCategory,
|
||||||
payload: res.data.data
|
payload: res.data.data,
|
||||||
})
|
});
|
||||||
|
|
||||||
dispatch<any>(sortCategories());
|
dispatch<any>(sortCategories());
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log(err);
|
console.log(err);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* DELETE BOOKMARK
|
* DELETE BOOKMARK
|
||||||
*/
|
*/
|
||||||
export interface DeleteBookmarkAction {
|
export interface DeleteBookmarkAction {
|
||||||
type: ActionTypes.deleteBookmark,
|
type: ActionTypes.deleteBookmark;
|
||||||
payload: {
|
payload: {
|
||||||
bookmarkId: number,
|
bookmarkId: number;
|
||||||
categoryId: number
|
categoryId: number;
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export const deleteBookmark = (bookmarkId: number, categoryId: number) => async (dispatch: Dispatch) => {
|
export const deleteBookmark =
|
||||||
|
(bookmarkId: number, categoryId: number) => async (dispatch: Dispatch) => {
|
||||||
try {
|
try {
|
||||||
await axios.delete<ApiResponse<{}>>(`/api/bookmarks/${bookmarkId}`);
|
await axios.delete<ApiResponse<{}>>(`/api/bookmarks/${bookmarkId}`);
|
||||||
|
|
||||||
|
@ -201,48 +230,53 @@ export const deleteBookmark = (bookmarkId: number, categoryId: number) => async
|
||||||
type: ActionTypes.createNotification,
|
type: ActionTypes.createNotification,
|
||||||
payload: {
|
payload: {
|
||||||
title: 'Success',
|
title: 'Success',
|
||||||
message: 'Bookmark deleted'
|
message: 'Bookmark deleted',
|
||||||
}
|
},
|
||||||
})
|
});
|
||||||
|
|
||||||
dispatch<DeleteBookmarkAction>({
|
dispatch<DeleteBookmarkAction>({
|
||||||
type: ActionTypes.deleteBookmark,
|
type: ActionTypes.deleteBookmark,
|
||||||
payload: {
|
payload: {
|
||||||
bookmarkId,
|
bookmarkId,
|
||||||
categoryId
|
categoryId,
|
||||||
}
|
},
|
||||||
})
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log(err);
|
console.log(err);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* UPDATE BOOKMARK
|
* UPDATE BOOKMARK
|
||||||
*/
|
*/
|
||||||
export interface UpdateBookmarkAction {
|
export interface UpdateBookmarkAction {
|
||||||
type: ActionTypes.updateBookmark,
|
type: ActionTypes.updateBookmark;
|
||||||
payload: Bookmark
|
payload: Bookmark;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const updateBookmark = (
|
export const updateBookmark =
|
||||||
|
(
|
||||||
bookmarkId: number,
|
bookmarkId: number,
|
||||||
formData: NewBookmark | FormData,
|
formData: NewBookmark | FormData,
|
||||||
category: {
|
category: {
|
||||||
prev: number,
|
prev: number;
|
||||||
curr: number
|
curr: number;
|
||||||
}
|
}
|
||||||
) => async (dispatch: Dispatch) => {
|
) =>
|
||||||
|
async (dispatch: Dispatch) => {
|
||||||
try {
|
try {
|
||||||
const res = await axios.put<ApiResponse<Bookmark>>(`/api/bookmarks/${bookmarkId}`, formData);
|
const res = await axios.put<ApiResponse<Bookmark>>(
|
||||||
|
`/api/bookmarks/${bookmarkId}`,
|
||||||
|
formData
|
||||||
|
);
|
||||||
|
|
||||||
dispatch<CreateNotificationAction>({
|
dispatch<CreateNotificationAction>({
|
||||||
type: ActionTypes.createNotification,
|
type: ActionTypes.createNotification,
|
||||||
payload: {
|
payload: {
|
||||||
title: 'Success',
|
title: 'Success',
|
||||||
message: `Bookmark updated`
|
message: `Bookmark updated`,
|
||||||
}
|
},
|
||||||
})
|
});
|
||||||
|
|
||||||
// Check if category was changed
|
// Check if category was changed
|
||||||
const categoryWasChanged = category.curr !== category.prev;
|
const categoryWasChanged = category.curr !== category.prev;
|
||||||
|
@ -253,26 +287,26 @@ export const updateBookmark = (
|
||||||
type: ActionTypes.deleteBookmark,
|
type: ActionTypes.deleteBookmark,
|
||||||
payload: {
|
payload: {
|
||||||
bookmarkId,
|
bookmarkId,
|
||||||
categoryId: category.prev
|
categoryId: category.prev,
|
||||||
}
|
},
|
||||||
})
|
});
|
||||||
|
|
||||||
// Add bookmark to the new category
|
// Add bookmark to the new category
|
||||||
dispatch<AddBookmarkAction>({
|
dispatch<AddBookmarkAction>({
|
||||||
type: ActionTypes.addBookmark,
|
type: ActionTypes.addBookmark,
|
||||||
payload: res.data.data
|
payload: res.data.data,
|
||||||
})
|
});
|
||||||
} else {
|
} else {
|
||||||
// Else update only name/url/icon
|
// Else update only name/url/icon
|
||||||
dispatch<UpdateBookmarkAction>({
|
dispatch<UpdateBookmarkAction>({
|
||||||
type: ActionTypes.updateBookmark,
|
type: ActionTypes.updateBookmark,
|
||||||
payload: res.data.data
|
payload: res.data.data,
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log(err);
|
console.log(err);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* SORT CATEGORIES
|
* SORT CATEGORIES
|
||||||
|
@ -284,16 +318,16 @@ export interface SortCategoriesAction {
|
||||||
|
|
||||||
export const sortCategories = () => async (dispatch: Dispatch) => {
|
export const sortCategories = () => async (dispatch: Dispatch) => {
|
||||||
try {
|
try {
|
||||||
const res = await axios.get<ApiResponse<Config>>('/api/config/useOrdering');
|
const res = await axios.get<ApiResponse<Config>>('/api/config');
|
||||||
|
|
||||||
dispatch<SortCategoriesAction>({
|
dispatch<SortCategoriesAction>({
|
||||||
type: ActionTypes.sortCategories,
|
type: ActionTypes.sortCategories,
|
||||||
payload: res.data.data.value
|
payload: res.data.data.useOrdering,
|
||||||
})
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log(err);
|
console.log(err);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* REORDER CATEGORIES
|
* REORDER CATEGORIES
|
||||||
|
@ -307,25 +341,31 @@ interface ReorderQuery {
|
||||||
categories: {
|
categories: {
|
||||||
id: number;
|
id: number;
|
||||||
orderId: number;
|
orderId: number;
|
||||||
}[]
|
}[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const reorderCategories = (categories: Category[]) => async (dispatch: Dispatch) => {
|
export const reorderCategories =
|
||||||
|
(categories: Category[]) => async (dispatch: Dispatch) => {
|
||||||
try {
|
try {
|
||||||
const updateQuery: ReorderQuery = { categories: [] }
|
const updateQuery: ReorderQuery = { categories: [] };
|
||||||
|
|
||||||
categories.forEach((category, index) => updateQuery.categories.push({
|
categories.forEach((category, index) =>
|
||||||
|
updateQuery.categories.push({
|
||||||
id: category.id,
|
id: category.id,
|
||||||
orderId: index + 1
|
orderId: index + 1,
|
||||||
}))
|
})
|
||||||
|
);
|
||||||
|
|
||||||
await axios.put<ApiResponse<{}>>('/api/categories/0/reorder', updateQuery);
|
await axios.put<ApiResponse<{}>>(
|
||||||
|
'/api/categories/0/reorder',
|
||||||
|
updateQuery
|
||||||
|
);
|
||||||
|
|
||||||
dispatch<ReorderCategoriesAction>({
|
dispatch<ReorderCategoriesAction>({
|
||||||
type: ActionTypes.reorderCategories,
|
type: ActionTypes.reorderCategories,
|
||||||
payload: categories
|
payload: categories,
|
||||||
})
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log(err);
|
console.log(err);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
|
@ -3,16 +3,15 @@ import { Dispatch } from 'redux';
|
||||||
import { ActionTypes } from './actionTypes';
|
import { ActionTypes } from './actionTypes';
|
||||||
import { Config, ApiResponse, Query } from '../../interfaces';
|
import { Config, ApiResponse, Query } from '../../interfaces';
|
||||||
import { CreateNotificationAction } from './notification';
|
import { CreateNotificationAction } from './notification';
|
||||||
import { searchConfig } from '../../utility';
|
|
||||||
|
|
||||||
export interface GetConfigAction {
|
export interface GetConfigAction {
|
||||||
type: ActionTypes.getConfig;
|
type: ActionTypes.getConfig;
|
||||||
payload: Config[];
|
payload: Config;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getConfig = () => async (dispatch: Dispatch) => {
|
export const getConfig = () => async (dispatch: Dispatch) => {
|
||||||
try {
|
try {
|
||||||
const res = await axios.get<ApiResponse<Config[]>>('/api/config');
|
const res = await axios.get<ApiResponse<Config>>('/api/config');
|
||||||
|
|
||||||
dispatch<GetConfigAction>({
|
dispatch<GetConfigAction>({
|
||||||
type: ActionTypes.getConfig,
|
type: ActionTypes.getConfig,
|
||||||
|
@ -20,7 +19,10 @@ export const getConfig = () => async (dispatch: Dispatch) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
// Set custom page title if set
|
// Set custom page title if set
|
||||||
document.title = searchConfig('customTitle', 'Flame');
|
document.title = res.data.data.customTitle;
|
||||||
|
|
||||||
|
// Store settings for priority UI elements
|
||||||
|
localStorage.setItem('useAmericanDate', `${res.data.data.useAmericanDate}`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log(err);
|
console.log(err);
|
||||||
}
|
}
|
||||||
|
@ -28,12 +30,12 @@ export const getConfig = () => async (dispatch: Dispatch) => {
|
||||||
|
|
||||||
export interface UpdateConfigAction {
|
export interface UpdateConfigAction {
|
||||||
type: ActionTypes.updateConfig;
|
type: ActionTypes.updateConfig;
|
||||||
payload: Config[];
|
payload: Config;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const updateConfig = (formData: any) => async (dispatch: Dispatch) => {
|
export const updateConfig = (formData: any) => async (dispatch: Dispatch) => {
|
||||||
try {
|
try {
|
||||||
const res = await axios.put<ApiResponse<Config[]>>('/api/config', formData);
|
const res = await axios.put<ApiResponse<Config>>('/api/config', formData);
|
||||||
|
|
||||||
dispatch<CreateNotificationAction>({
|
dispatch<CreateNotificationAction>({
|
||||||
type: ActionTypes.createNotification,
|
type: ActionTypes.createNotification,
|
||||||
|
@ -47,6 +49,9 @@ export const updateConfig = (formData: any) => async (dispatch: Dispatch) => {
|
||||||
type: ActionTypes.updateConfig,
|
type: ActionTypes.updateConfig,
|
||||||
payload: res.data.data,
|
payload: res.data.data,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Store settings for priority UI elements
|
||||||
|
localStorage.setItem('useAmericanDate', `${res.data.data.useAmericanDate}`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log(err);
|
console.log(err);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,15 +1,16 @@
|
||||||
import { ActionTypes, Action } from '../actions';
|
import { ActionTypes, Action } from '../actions';
|
||||||
import { Config, Query } from '../../interfaces';
|
import { Config, Query } from '../../interfaces';
|
||||||
|
import { configTemplate } from '../../utility';
|
||||||
|
|
||||||
export interface State {
|
export interface State {
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
config: Config[];
|
config: Config;
|
||||||
customQueries: Query[];
|
customQueries: Query[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const initialState: State = {
|
const initialState: State = {
|
||||||
loading: true,
|
loading: true,
|
||||||
config: [],
|
config: configTemplate,
|
||||||
customQueries: [],
|
customQueries: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -7,20 +7,22 @@ export interface State {
|
||||||
|
|
||||||
const initialState: State = {
|
const initialState: State = {
|
||||||
theme: {
|
theme: {
|
||||||
name: 'blues',
|
name: 'tron',
|
||||||
colors: {
|
colors: {
|
||||||
background: '#2B2C56',
|
background: '#242B33',
|
||||||
primary: '#EFF1FC',
|
primary: '#EFFBFF',
|
||||||
accent: '#6677EB'
|
accent: '#6EE2FF',
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
};
|
||||||
|
|
||||||
const themeReducer = (state = initialState, action: Action) => {
|
const themeReducer = (state = initialState, action: Action) => {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case ActionTypes.setTheme: return { theme: action.payload };
|
case ActionTypes.setTheme:
|
||||||
default: return state;
|
return { theme: action.payload };
|
||||||
}
|
default:
|
||||||
|
return state;
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export default themeReducer;
|
export default themeReducer;
|
|
@ -1,7 +1,8 @@
|
||||||
export * from './iconParser';
|
export * from './iconParser';
|
||||||
export * from './urlParser';
|
export * from './urlParser';
|
||||||
export * from './searchConfig';
|
|
||||||
export * from './checkVersion';
|
export * from './checkVersion';
|
||||||
export * from './sortData';
|
export * from './sortData';
|
||||||
export * from './searchParser';
|
export * from './searchParser';
|
||||||
export * from './redirectUrl';
|
export * from './redirectUrl';
|
||||||
|
export * from './templateObjects';
|
||||||
|
export * from './inputHandler';
|
||||||
|
|
39
client/src/utility/inputHandler.ts
Normal file
39
client/src/utility/inputHandler.ts
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
import { ChangeEvent, SetStateAction } from 'react';
|
||||||
|
|
||||||
|
type Event = ChangeEvent<HTMLInputElement | HTMLSelectElement>;
|
||||||
|
|
||||||
|
interface Options {
|
||||||
|
isNumber?: boolean;
|
||||||
|
isBool?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Params<T> {
|
||||||
|
e: Event;
|
||||||
|
options?: Options;
|
||||||
|
setStateHandler: (v: SetStateAction<T>) => void;
|
||||||
|
state: T;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const inputHandler = <T>(params: Params<T>): void => {
|
||||||
|
const { e, options, setStateHandler, state } = params;
|
||||||
|
|
||||||
|
const rawValue = e.target.value;
|
||||||
|
let value: string | number | boolean = e.target.value;
|
||||||
|
|
||||||
|
if (options) {
|
||||||
|
const { isNumber = false, isBool = false } = options;
|
||||||
|
|
||||||
|
if (isNumber) {
|
||||||
|
value = parseFloat(rawValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isBool) {
|
||||||
|
value = !!parseInt(rawValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setStateHandler({
|
||||||
|
...state,
|
||||||
|
[e.target.name]: value,
|
||||||
|
});
|
||||||
|
};
|
|
@ -1,24 +0,0 @@
|
||||||
import { store } from '../store/store';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Search config store with given key
|
|
||||||
* @param key Config pair key to search
|
|
||||||
* @param _default Value to return if key is not found
|
|
||||||
*/
|
|
||||||
export const searchConfig = (key: string, _default: any) => {
|
|
||||||
const state = store.getState();
|
|
||||||
|
|
||||||
const pair = state.config.config.find(p => p.key === key);
|
|
||||||
|
|
||||||
if (pair) {
|
|
||||||
if (pair.valueType === 'number') {
|
|
||||||
return parseFloat(pair.value);
|
|
||||||
} else if (pair.valueType === 'boolean') {
|
|
||||||
return parseInt(pair.value);
|
|
||||||
} else {
|
|
||||||
return pair.value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return _default;
|
|
||||||
}
|
|
|
@ -1,7 +1,6 @@
|
||||||
import { queries } from './searchQueries.json';
|
import { queries } from './searchQueries.json';
|
||||||
import { Query, SearchResult } from '../interfaces';
|
import { Query, SearchResult } from '../interfaces';
|
||||||
import { store } from '../store/store';
|
import { store } from '../store/store';
|
||||||
import { searchConfig } from '.';
|
|
||||||
|
|
||||||
export const searchParser = (searchQuery: string): SearchResult => {
|
export const searchParser = (searchQuery: string): SearchResult => {
|
||||||
const result: SearchResult = {
|
const result: SearchResult = {
|
||||||
|
@ -16,20 +15,18 @@ export const searchParser = (searchQuery: string): SearchResult => {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const customQueries = store.getState().config.customQueries;
|
const { customQueries, config } = store.getState().config;
|
||||||
|
|
||||||
// Check if url or ip was passed
|
// Check if url or ip was passed
|
||||||
const urlRegex =
|
const urlRegex =
|
||||||
/^(http:\/\/www\.|https:\/\/www\.|http:\/\/|https:\/\/)?[a-z0-9]+([\-\.]{1}[a-z0-9]+)*\.[a-z]{2,5}(:[0-9]{1,5})?(\/.*)?|^((http:\/\/www\.|https:\/\/www\.|http:\/\/|https:\/\/)?([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$/;
|
/^(http:\/\/www\.|https:\/\/www\.|http:\/\/|https:\/\/)?[a-z0-9]+([\-\.]{1}[a-z0-9]+)*\.[a-z]{2,5}(:[0-9]{1,5})?(\/.*)?|^((http:\/\/www\.|https:\/\/www\.|http:\/\/|https:\/\/)?([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$/i;
|
||||||
|
|
||||||
result.isURL = urlRegex.test(searchQuery);
|
result.isURL = urlRegex.test(searchQuery);
|
||||||
|
|
||||||
// Match prefix and query
|
// Match prefix and query
|
||||||
const splitQuery = searchQuery.match(/^\/([a-z]+)[ ](.+)$/i);
|
const splitQuery = searchQuery.match(/^\/([a-z]+)[ ](.+)$/i);
|
||||||
|
|
||||||
const prefix = splitQuery
|
const prefix = splitQuery ? splitQuery[1] : config.defaultSearchProvider;
|
||||||
? splitQuery[1]
|
|
||||||
: searchConfig('defaultSearchProvider', 'l');
|
|
||||||
|
|
||||||
const search = splitQuery
|
const search = splitQuery
|
||||||
? encodeURIComponent(splitQuery[2])
|
? encodeURIComponent(splitQuery[2])
|
||||||
|
@ -47,7 +44,7 @@ export const searchParser = (searchQuery: string): SearchResult => {
|
||||||
if (prefix === 'l') {
|
if (prefix === 'l') {
|
||||||
result.isLocal = true;
|
result.isLocal = true;
|
||||||
} else {
|
} else {
|
||||||
result.sameTab = searchConfig('searchSameTab', false);
|
result.sameTab = config.searchSameTab;
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
|
|
25
client/src/utility/templateObjects/configTemplate.ts
Normal file
25
client/src/utility/templateObjects/configTemplate.ts
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
import { Config } from '../../interfaces';
|
||||||
|
|
||||||
|
export const configTemplate: Config = {
|
||||||
|
WEATHER_API_KEY: '',
|
||||||
|
lat: 0,
|
||||||
|
long: 0,
|
||||||
|
isCelsius: true,
|
||||||
|
customTitle: 'Flame',
|
||||||
|
pinAppsByDefault: true,
|
||||||
|
pinCategoriesByDefault: true,
|
||||||
|
hideHeader: false,
|
||||||
|
useOrdering: 'createdAt',
|
||||||
|
appsSameTab: false,
|
||||||
|
bookmarksSameTab: false,
|
||||||
|
searchSameTab: false,
|
||||||
|
hideApps: false,
|
||||||
|
hideCategories: false,
|
||||||
|
hideSearch: false,
|
||||||
|
defaultSearchProvider: 'l',
|
||||||
|
dockerApps: false,
|
||||||
|
dockerHost: 'localhost',
|
||||||
|
kubernetesApps: false,
|
||||||
|
unpinStoppedApps: false,
|
||||||
|
useAmericanDate: false,
|
||||||
|
};
|
2
client/src/utility/templateObjects/index.ts
Normal file
2
client/src/utility/templateObjects/index.ts
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
export * from './configTemplate';
|
||||||
|
export * from './settingsTemplate';
|
31
client/src/utility/templateObjects/settingsTemplate.ts
Normal file
31
client/src/utility/templateObjects/settingsTemplate.ts
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
import { OtherSettingsForm, SearchForm, WeatherForm } from '../../interfaces';
|
||||||
|
|
||||||
|
export const otherSettingsTemplate: OtherSettingsForm = {
|
||||||
|
customTitle: document.title,
|
||||||
|
pinAppsByDefault: true,
|
||||||
|
pinCategoriesByDefault: true,
|
||||||
|
hideHeader: false,
|
||||||
|
hideApps: false,
|
||||||
|
hideCategories: false,
|
||||||
|
useOrdering: 'createdAt',
|
||||||
|
appsSameTab: false,
|
||||||
|
bookmarksSameTab: false,
|
||||||
|
dockerApps: true,
|
||||||
|
dockerHost: 'localhost',
|
||||||
|
kubernetesApps: true,
|
||||||
|
unpinStoppedApps: true,
|
||||||
|
useAmericanDate: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const weatherSettingsTemplate: WeatherForm = {
|
||||||
|
WEATHER_API_KEY: '',
|
||||||
|
lat: 0,
|
||||||
|
long: 0,
|
||||||
|
isCelsius: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const searchSettingsTemplate: SearchForm = {
|
||||||
|
hideSearch: false,
|
||||||
|
searchSameTab: false,
|
||||||
|
defaultSearchProvider: 'l',
|
||||||
|
};
|
|
@ -1,326 +0,0 @@
|
||||||
const asyncWrapper = require('../middleware/asyncWrapper');
|
|
||||||
const ErrorResponse = require('../utils/ErrorResponse');
|
|
||||||
const App = require('../models/App');
|
|
||||||
const Config = require('../models/Config');
|
|
||||||
const { Sequelize } = require('sequelize');
|
|
||||||
const axios = require('axios');
|
|
||||||
const Logger = require('../utils/Logger');
|
|
||||||
const logger = new Logger();
|
|
||||||
const k8s = require('@kubernetes/client-node');
|
|
||||||
|
|
||||||
// @desc Create new app
|
|
||||||
// @route POST /api/apps
|
|
||||||
// @access Public
|
|
||||||
exports.createApp = asyncWrapper(async (req, res, next) => {
|
|
||||||
// Get config from database
|
|
||||||
const pinApps = await Config.findOne({
|
|
||||||
where: { key: 'pinAppsByDefault' },
|
|
||||||
});
|
|
||||||
|
|
||||||
let app;
|
|
||||||
let _body = { ...req.body };
|
|
||||||
|
|
||||||
if (req.file) {
|
|
||||||
_body.icon = req.file.filename;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (pinApps) {
|
|
||||||
if (parseInt(pinApps.value)) {
|
|
||||||
app = await App.create({
|
|
||||||
..._body,
|
|
||||||
isPinned: true,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
app = await App.create(req.body);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
res.status(201).json({
|
|
||||||
success: true,
|
|
||||||
data: app,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// @desc Get all apps
|
|
||||||
// @route GET /api/apps
|
|
||||||
// @access Public
|
|
||||||
exports.getApps = asyncWrapper(async (req, res, next) => {
|
|
||||||
// Get config from database
|
|
||||||
const useOrdering = await Config.findOne({
|
|
||||||
where: { key: 'useOrdering' },
|
|
||||||
});
|
|
||||||
const useDockerApi = await Config.findOne({
|
|
||||||
where: { key: 'dockerApps' },
|
|
||||||
});
|
|
||||||
const useKubernetesApi = await Config.findOne({
|
|
||||||
where: { key: 'kubernetesApps' },
|
|
||||||
});
|
|
||||||
const unpinStoppedApps = await Config.findOne({
|
|
||||||
where: { key: 'unpinStoppedApps' },
|
|
||||||
});
|
|
||||||
|
|
||||||
const orderType = useOrdering ? useOrdering.value : 'createdAt';
|
|
||||||
let apps;
|
|
||||||
|
|
||||||
if (useDockerApi && useDockerApi.value == 1) {
|
|
||||||
let containers = null;
|
|
||||||
|
|
||||||
const host = await Config.findOne({
|
|
||||||
where: { key: 'dockerHost' },
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (host.value.includes('localhost')) {
|
|
||||||
let { data } = await axios.get(
|
|
||||||
`http://${host.value}/containers/json?{"status":["running"]}`,
|
|
||||||
{
|
|
||||||
socketPath: '/var/run/docker.sock',
|
|
||||||
}
|
|
||||||
);
|
|
||||||
containers = data;
|
|
||||||
} else {
|
|
||||||
let { data } = await axios.get(
|
|
||||||
`http://${host.value}/containers/json?{"status":["running"]}`
|
|
||||||
);
|
|
||||||
containers = data;
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
logger.log(`Can't connect to the docker api on ${host.value}`, 'ERROR');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (containers) {
|
|
||||||
apps = await App.findAll({
|
|
||||||
order: [[orderType, 'ASC']],
|
|
||||||
});
|
|
||||||
|
|
||||||
containers = containers.filter((e) => Object.keys(e.Labels).length !== 0);
|
|
||||||
const dockerApps = [];
|
|
||||||
for (const container of containers) {
|
|
||||||
const labels = container.Labels;
|
|
||||||
|
|
||||||
if (
|
|
||||||
'flame.name' in labels &&
|
|
||||||
'flame.url' in labels &&
|
|
||||||
/^app/.test(labels['flame.type'])
|
|
||||||
) {
|
|
||||||
for (let i = 0; i < labels['flame.name'].split(';').length; i++) {
|
|
||||||
const names = labels['flame.name'].split(';');
|
|
||||||
const urls = labels['flame.url'].split(';');
|
|
||||||
let icons = '';
|
|
||||||
|
|
||||||
if ('flame.icon' in labels) {
|
|
||||||
icons = labels['flame.icon'].split(';');
|
|
||||||
}
|
|
||||||
|
|
||||||
dockerApps.push({
|
|
||||||
name: names[i] || names[0],
|
|
||||||
url: urls[i] || urls[0],
|
|
||||||
icon: icons[i] || 'docker',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (unpinStoppedApps && unpinStoppedApps.value == 1) {
|
|
||||||
for (const app of apps) {
|
|
||||||
await app.update({ isPinned: false });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const item of dockerApps) {
|
|
||||||
if (apps.some((app) => app.name === item.name)) {
|
|
||||||
const app = apps.filter((e) => e.name === item.name)[0];
|
|
||||||
|
|
||||||
if (
|
|
||||||
item.icon === 'custom' ||
|
|
||||||
(item.icon === 'docker' && app.icon != 'docker')
|
|
||||||
) {
|
|
||||||
await app.update({
|
|
||||||
name: item.name,
|
|
||||||
url: item.url,
|
|
||||||
isPinned: true,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
await app.update({
|
|
||||||
name: item.name,
|
|
||||||
url: item.url,
|
|
||||||
icon: item.icon,
|
|
||||||
isPinned: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
await App.create({
|
|
||||||
name: item.name,
|
|
||||||
url: item.url,
|
|
||||||
icon: item.icon === 'custom' ? 'docker' : item.icon,
|
|
||||||
isPinned: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (useKubernetesApi && useKubernetesApi.value == 1) {
|
|
||||||
let ingresses = null;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const kc = new k8s.KubeConfig();
|
|
||||||
kc.loadFromCluster();
|
|
||||||
const k8sNetworkingV1Api = kc.makeApiClient(k8s.NetworkingV1Api);
|
|
||||||
await k8sNetworkingV1Api.listIngressForAllNamespaces().then((res) => {
|
|
||||||
ingresses = res.body.items;
|
|
||||||
});
|
|
||||||
} catch {
|
|
||||||
logger.log("Can't connect to the kubernetes api", 'ERROR');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ingresses) {
|
|
||||||
apps = await App.findAll({
|
|
||||||
order: [[orderType, 'ASC']],
|
|
||||||
});
|
|
||||||
|
|
||||||
ingresses = ingresses.filter(
|
|
||||||
(e) => Object.keys(e.metadata.annotations).length !== 0
|
|
||||||
);
|
|
||||||
const kubernetesApps = [];
|
|
||||||
for (const ingress of ingresses) {
|
|
||||||
const annotations = ingress.metadata.annotations;
|
|
||||||
|
|
||||||
if (
|
|
||||||
'flame.pawelmalak/name' in annotations &&
|
|
||||||
'flame.pawelmalak/url' in annotations &&
|
|
||||||
/^app/.test(annotations['flame.pawelmalak/type'])
|
|
||||||
) {
|
|
||||||
kubernetesApps.push({
|
|
||||||
name: annotations['flame.pawelmalak/name'],
|
|
||||||
url: annotations['flame.pawelmalak/url'],
|
|
||||||
icon: annotations['flame.pawelmalak/icon'] || 'kubernetes',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (unpinStoppedApps && unpinStoppedApps.value == 1) {
|
|
||||||
for (const app of apps) {
|
|
||||||
await app.update({ isPinned: false });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const item of kubernetesApps) {
|
|
||||||
if (apps.some((app) => app.name === item.name)) {
|
|
||||||
const app = apps.filter((e) => e.name === item.name)[0];
|
|
||||||
await app.update({ ...item, isPinned: true });
|
|
||||||
} else {
|
|
||||||
await App.create({
|
|
||||||
...item,
|
|
||||||
isPinned: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (orderType == 'name') {
|
|
||||||
apps = await App.findAll({
|
|
||||||
order: [[Sequelize.fn('lower', Sequelize.col('name')), 'ASC']],
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
apps = await App.findAll({
|
|
||||||
order: [[orderType, 'ASC']],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (process.env.NODE_ENV === 'production') {
|
|
||||||
// Set header to fetch containers info every time
|
|
||||||
res.status(200).setHeader('Cache-Control', 'no-store').json({
|
|
||||||
success: true,
|
|
||||||
data: apps,
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
res.status(200).json({
|
|
||||||
success: true,
|
|
||||||
data: apps,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// @desc Get single app
|
|
||||||
// @route GET /api/apps/:id
|
|
||||||
// @access Public
|
|
||||||
exports.getApp = asyncWrapper(async (req, res, next) => {
|
|
||||||
const app = await App.findOne({
|
|
||||||
where: { id: req.params.id },
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!app) {
|
|
||||||
return next(
|
|
||||||
new ErrorResponse(`App with id of ${req.params.id} was not found`, 404)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
res.status(200).json({
|
|
||||||
success: true,
|
|
||||||
data: app,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// @desc Update app
|
|
||||||
// @route PUT /api/apps/:id
|
|
||||||
// @access Public
|
|
||||||
exports.updateApp = asyncWrapper(async (req, res, next) => {
|
|
||||||
let app = await App.findOne({
|
|
||||||
where: { id: req.params.id },
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!app) {
|
|
||||||
return next(
|
|
||||||
new ErrorResponse(`App with id of ${req.params.id} was not found`, 404)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
let _body = { ...req.body };
|
|
||||||
|
|
||||||
if (req.file) {
|
|
||||||
_body.icon = req.file.filename;
|
|
||||||
}
|
|
||||||
|
|
||||||
app = await app.update(_body);
|
|
||||||
|
|
||||||
res.status(200).json({
|
|
||||||
success: true,
|
|
||||||
data: app,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// @desc Delete app
|
|
||||||
// @route DELETE /api/apps/:id
|
|
||||||
// @access Public
|
|
||||||
exports.deleteApp = asyncWrapper(async (req, res, next) => {
|
|
||||||
await App.destroy({
|
|
||||||
where: { id: req.params.id },
|
|
||||||
});
|
|
||||||
|
|
||||||
res.status(200).json({
|
|
||||||
success: true,
|
|
||||||
data: {},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// @desc Reorder apps
|
|
||||||
// @route PUT /api/apps/0/reorder
|
|
||||||
// @access Public
|
|
||||||
exports.reorderApps = asyncWrapper(async (req, res, next) => {
|
|
||||||
req.body.apps.forEach(async ({ id, orderId }) => {
|
|
||||||
await App.update(
|
|
||||||
{ orderId },
|
|
||||||
{
|
|
||||||
where: { id },
|
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
res.status(200).json({
|
|
||||||
success: true,
|
|
||||||
data: {},
|
|
||||||
});
|
|
||||||
});
|
|
33
controllers/apps/createApp.js
Normal file
33
controllers/apps/createApp.js
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
const asyncWrapper = require('../../middleware/asyncWrapper');
|
||||||
|
const App = require('../../models/App');
|
||||||
|
const loadConfig = require('../../utils/loadConfig');
|
||||||
|
|
||||||
|
// @desc Create new app
|
||||||
|
// @route POST /api/apps
|
||||||
|
// @access Public
|
||||||
|
const createApp = asyncWrapper(async (req, res, next) => {
|
||||||
|
const { pinAppsByDefault } = await loadConfig();
|
||||||
|
|
||||||
|
let app;
|
||||||
|
let _body = { ...req.body };
|
||||||
|
|
||||||
|
if (req.file) {
|
||||||
|
_body.icon = req.file.filename;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pinAppsByDefault) {
|
||||||
|
app = await App.create({
|
||||||
|
..._body,
|
||||||
|
isPinned: true,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
app = await App.create(req.body);
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(201).json({
|
||||||
|
success: true,
|
||||||
|
data: app,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = createApp;
|
18
controllers/apps/deleteApp.js
Normal file
18
controllers/apps/deleteApp.js
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
const asyncWrapper = require('../../middleware/asyncWrapper');
|
||||||
|
const App = require('../../models/App');
|
||||||
|
|
||||||
|
// @desc Delete app
|
||||||
|
// @route DELETE /api/apps/:id
|
||||||
|
// @access Public
|
||||||
|
const deleteApp = asyncWrapper(async (req, res, next) => {
|
||||||
|
await App.destroy({
|
||||||
|
where: { id: req.params.id },
|
||||||
|
});
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
data: {},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = deleteApp;
|
4
controllers/apps/docker/index.js
Normal file
4
controllers/apps/docker/index.js
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
module.exports = {
|
||||||
|
useKubernetes: require('./useKubernetes'),
|
||||||
|
useDocker: require('./useDocker'),
|
||||||
|
};
|
148
controllers/apps/docker/useDocker.js
Normal file
148
controllers/apps/docker/useDocker.js
Normal file
|
@ -0,0 +1,148 @@
|
||||||
|
const App = require('../../../models/App');
|
||||||
|
const axios = require('axios');
|
||||||
|
const Logger = require('../../../utils/Logger');
|
||||||
|
const logger = new Logger();
|
||||||
|
const loadConfig = require('../../../utils/loadConfig');
|
||||||
|
|
||||||
|
const useDocker = async (apps) => {
|
||||||
|
const {
|
||||||
|
useOrdering: orderType,
|
||||||
|
unpinStoppedApps,
|
||||||
|
dockerHost: host,
|
||||||
|
} = await loadConfig();
|
||||||
|
|
||||||
|
let containers = null;
|
||||||
|
|
||||||
|
// Get list of containers
|
||||||
|
try {
|
||||||
|
if (host.includes('localhost')) {
|
||||||
|
// Use default host
|
||||||
|
let { data } = await axios.get(
|
||||||
|
`http://${host}/containers/json?{"status":["running"]}`,
|
||||||
|
{
|
||||||
|
socketPath: '/var/run/docker.sock',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
containers = data;
|
||||||
|
} else {
|
||||||
|
// Use custom host
|
||||||
|
let { data } = await axios.get(
|
||||||
|
`http://${host}/containers/json?{"status":["running"]}`
|
||||||
|
);
|
||||||
|
|
||||||
|
containers = data;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
logger.log(`Can't connect to the Docker API on ${host}`, 'ERROR');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (containers) {
|
||||||
|
apps = await App.findAll({
|
||||||
|
order: [[orderType, 'ASC']],
|
||||||
|
});
|
||||||
|
|
||||||
|
// Filter out containers without any annotations
|
||||||
|
containers = containers.filter((e) => Object.keys(e.Labels).length !== 0);
|
||||||
|
|
||||||
|
const dockerApps = [];
|
||||||
|
|
||||||
|
for (const container of containers) {
|
||||||
|
let labels = container.Labels;
|
||||||
|
|
||||||
|
// Traefik labels for URL configuration
|
||||||
|
if (!('flame.url' in labels)) {
|
||||||
|
for (const label of Object.keys(labels)) {
|
||||||
|
if (/^traefik.*.frontend.rule/.test(label)) {
|
||||||
|
// Traefik 1.x
|
||||||
|
let value = labels[label];
|
||||||
|
|
||||||
|
if (value.indexOf('Host') !== -1) {
|
||||||
|
value = value.split('Host:')[1];
|
||||||
|
labels['flame.url'] =
|
||||||
|
'https://' + value.split(',').join(';https://');
|
||||||
|
}
|
||||||
|
} else if (/^traefik.*?\.rule/.test(label)) {
|
||||||
|
// Traefik 2.x
|
||||||
|
const value = labels[label];
|
||||||
|
|
||||||
|
if (value.indexOf('Host') !== -1) {
|
||||||
|
const regex = /\`([a-zA-Z0-9\.\-]+)\`/g;
|
||||||
|
const domains = [];
|
||||||
|
|
||||||
|
while ((match = regex.exec(value)) != null) {
|
||||||
|
domains.push('http://' + match[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (domains.length > 0) {
|
||||||
|
labels['flame.url'] = domains.join(';');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// add each container as flame formatted app
|
||||||
|
if (
|
||||||
|
'flame.name' in labels &&
|
||||||
|
'flame.url' in labels &&
|
||||||
|
/^app/.test(labels['flame.type'])
|
||||||
|
) {
|
||||||
|
for (let i = 0; i < labels['flame.name'].split(';').length; i++) {
|
||||||
|
const names = labels['flame.name'].split(';');
|
||||||
|
const urls = labels['flame.url'].split(';');
|
||||||
|
let icons = '';
|
||||||
|
|
||||||
|
if ('flame.icon' in labels) {
|
||||||
|
icons = labels['flame.icon'].split(';');
|
||||||
|
}
|
||||||
|
|
||||||
|
dockerApps.push({
|
||||||
|
name: names[i] || names[0],
|
||||||
|
url: urls[i] || urls[0],
|
||||||
|
icon: icons[i] || 'docker',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (unpinStoppedApps) {
|
||||||
|
for (const app of apps) {
|
||||||
|
await app.update({ isPinned: false });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const item of dockerApps) {
|
||||||
|
// If app already exists, update it
|
||||||
|
if (apps.some((app) => app.name === item.name)) {
|
||||||
|
const app = apps.find((a) => a.name === item.name);
|
||||||
|
|
||||||
|
if (
|
||||||
|
item.icon === 'custom' ||
|
||||||
|
(item.icon === 'docker' && app.icon != 'docker')
|
||||||
|
) {
|
||||||
|
// update without overriding icon
|
||||||
|
await app.update({
|
||||||
|
name: item.name,
|
||||||
|
url: item.url,
|
||||||
|
isPinned: true,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await app.update({
|
||||||
|
...item,
|
||||||
|
isPinned: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// else create new app
|
||||||
|
await App.create({
|
||||||
|
...item,
|
||||||
|
icon: item.icon === 'custom' ? 'docker' : item.icon,
|
||||||
|
isPinned: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = useDocker;
|
70
controllers/apps/docker/useKubernetes.js
Normal file
70
controllers/apps/docker/useKubernetes.js
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
const App = require('../../../models/App');
|
||||||
|
const k8s = require('@kubernetes/client-node');
|
||||||
|
const Logger = require('../../../utils/Logger');
|
||||||
|
const logger = new Logger();
|
||||||
|
const loadConfig = require('../../../utils/loadConfig');
|
||||||
|
|
||||||
|
const useKubernetes = async (apps) => {
|
||||||
|
const { useOrdering: orderType, unpinStoppedApps } = await loadConfig();
|
||||||
|
|
||||||
|
let ingresses = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const kc = new k8s.KubeConfig();
|
||||||
|
kc.loadFromCluster();
|
||||||
|
const k8sNetworkingV1Api = kc.makeApiClient(k8s.NetworkingV1Api);
|
||||||
|
await k8sNetworkingV1Api.listIngressForAllNamespaces().then((res) => {
|
||||||
|
ingresses = res.body.items;
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
logger.log("Can't connect to the Kubernetes API", 'ERROR');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ingresses) {
|
||||||
|
apps = await App.findAll({
|
||||||
|
order: [[orderType, 'ASC']],
|
||||||
|
});
|
||||||
|
|
||||||
|
ingresses = ingresses.filter(
|
||||||
|
(e) => Object.keys(e.metadata.annotations).length !== 0
|
||||||
|
);
|
||||||
|
|
||||||
|
const kubernetesApps = [];
|
||||||
|
|
||||||
|
for (const ingress of ingresses) {
|
||||||
|
const annotations = ingress.metadata.annotations;
|
||||||
|
|
||||||
|
if (
|
||||||
|
'flame.pawelmalak/name' in annotations &&
|
||||||
|
'flame.pawelmalak/url' in annotations &&
|
||||||
|
/^app/.test(annotations['flame.pawelmalak/type'])
|
||||||
|
) {
|
||||||
|
kubernetesApps.push({
|
||||||
|
name: annotations['flame.pawelmalak/name'],
|
||||||
|
url: annotations['flame.pawelmalak/url'],
|
||||||
|
icon: annotations['flame.pawelmalak/icon'] || 'kubernetes',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (unpinStoppedApps) {
|
||||||
|
for (const app of apps) {
|
||||||
|
await app.update({ isPinned: false });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const item of kubernetesApps) {
|
||||||
|
if (apps.some((app) => app.name === item.name)) {
|
||||||
|
const app = apps.find((a) => a.name === item.name);
|
||||||
|
await app.update({ ...item, isPinned: true });
|
||||||
|
} else {
|
||||||
|
await App.create({
|
||||||
|
...item,
|
||||||
|
isPinned: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = useKubernetes;
|
52
controllers/apps/getAllApps.js
Normal file
52
controllers/apps/getAllApps.js
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
const asyncWrapper = require('../../middleware/asyncWrapper');
|
||||||
|
const App = require('../../models/App');
|
||||||
|
const { Sequelize } = require('sequelize');
|
||||||
|
const loadConfig = require('../../utils/loadConfig');
|
||||||
|
|
||||||
|
const { useKubernetes, useDocker } = require('./docker');
|
||||||
|
|
||||||
|
// @desc Get all apps
|
||||||
|
// @route GET /api/apps
|
||||||
|
// @access Public
|
||||||
|
const getAllApps = asyncWrapper(async (req, res, next) => {
|
||||||
|
const {
|
||||||
|
useOrdering: orderType,
|
||||||
|
dockerApps: useDockerAPI,
|
||||||
|
kubernetesApps: useKubernetesAPI,
|
||||||
|
} = await loadConfig();
|
||||||
|
|
||||||
|
let apps;
|
||||||
|
|
||||||
|
if (useDockerAPI) {
|
||||||
|
await useDocker(apps);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (useKubernetesAPI) {
|
||||||
|
await useKubernetes(apps);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (orderType == 'name') {
|
||||||
|
apps = await App.findAll({
|
||||||
|
order: [[Sequelize.fn('lower', Sequelize.col('name')), 'ASC']],
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
apps = await App.findAll({
|
||||||
|
order: [[orderType, 'ASC']],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.env.NODE_ENV === 'production') {
|
||||||
|
// Set header to fetch containers info every time
|
||||||
|
return res.status(200).setHeader('Cache-Control', 'no-store').json({
|
||||||
|
success: true,
|
||||||
|
data: apps,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
data: apps,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = getAllApps;
|
27
controllers/apps/getSingleApp.js
Normal file
27
controllers/apps/getSingleApp.js
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
const asyncWrapper = require('../../middleware/asyncWrapper');
|
||||||
|
const App = require('../../models/App');
|
||||||
|
|
||||||
|
// @desc Get single app
|
||||||
|
// @route GET /api/apps/:id
|
||||||
|
// @access Public
|
||||||
|
const getSingleApp = asyncWrapper(async (req, res, next) => {
|
||||||
|
const app = await App.findOne({
|
||||||
|
where: { id: req.params.id },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!app) {
|
||||||
|
return next(
|
||||||
|
new ErrorResponse(
|
||||||
|
`App with the id of ${req.params.id} was not found`,
|
||||||
|
404
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
data: app,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = getSingleApp;
|
8
controllers/apps/index.js
Normal file
8
controllers/apps/index.js
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
module.exports = {
|
||||||
|
createApp: require('./createApp'),
|
||||||
|
getSingleApp: require('./getSingleApp'),
|
||||||
|
deleteApp: require('./deleteApp'),
|
||||||
|
updateApp: require('./updateApp'),
|
||||||
|
reorderApps: require('./reorderApps'),
|
||||||
|
getAllApps: require('./getAllApps'),
|
||||||
|
};
|
23
controllers/apps/reorderApps.js
Normal file
23
controllers/apps/reorderApps.js
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
const asyncWrapper = require('../../middleware/asyncWrapper');
|
||||||
|
const App = require('../../models/App');
|
||||||
|
|
||||||
|
// @desc Reorder apps
|
||||||
|
// @route PUT /api/apps/0/reorder
|
||||||
|
// @access Public
|
||||||
|
const reorderApps = asyncWrapper(async (req, res, next) => {
|
||||||
|
req.body.apps.forEach(async ({ id, orderId }) => {
|
||||||
|
await App.update(
|
||||||
|
{ orderId },
|
||||||
|
{
|
||||||
|
where: { id },
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
data: {},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = reorderApps;
|
35
controllers/apps/updateApp.js
Normal file
35
controllers/apps/updateApp.js
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
const asyncWrapper = require('../../middleware/asyncWrapper');
|
||||||
|
const App = require('../../models/App');
|
||||||
|
|
||||||
|
// @desc Update app
|
||||||
|
// @route PUT /api/apps/:id
|
||||||
|
// @access Public
|
||||||
|
const updateApp = asyncWrapper(async (req, res, next) => {
|
||||||
|
let app = await App.findOne({
|
||||||
|
where: { id: req.params.id },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!app) {
|
||||||
|
return next(
|
||||||
|
new ErrorResponse(
|
||||||
|
`App with the id of ${req.params.id} was not found`,
|
||||||
|
404
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let _body = { ...req.body };
|
||||||
|
|
||||||
|
if (req.file) {
|
||||||
|
_body.icon = req.file.filename;
|
||||||
|
}
|
||||||
|
|
||||||
|
app = await app.update(_body);
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
data: app,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = updateApp;
|
|
@ -1,112 +0,0 @@
|
||||||
const asyncWrapper = require('../middleware/asyncWrapper');
|
|
||||||
const ErrorResponse = require('../utils/ErrorResponse');
|
|
||||||
const Bookmark = require('../models/Bookmark');
|
|
||||||
const { Sequelize } = require('sequelize');
|
|
||||||
|
|
||||||
// @desc Create new bookmark
|
|
||||||
// @route POST /api/bookmarks
|
|
||||||
// @access Public
|
|
||||||
exports.createBookmark = asyncWrapper(async (req, res, next) => {
|
|
||||||
let bookmark;
|
|
||||||
|
|
||||||
let _body = {
|
|
||||||
...req.body,
|
|
||||||
categoryId: parseInt(req.body.categoryId),
|
|
||||||
};
|
|
||||||
|
|
||||||
if (req.file) {
|
|
||||||
_body.icon = req.file.filename;
|
|
||||||
}
|
|
||||||
|
|
||||||
bookmark = await Bookmark.create(_body);
|
|
||||||
|
|
||||||
res.status(201).json({
|
|
||||||
success: true,
|
|
||||||
data: bookmark,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// @desc Get all bookmarks
|
|
||||||
// @route GET /api/bookmarks
|
|
||||||
// @access Public
|
|
||||||
exports.getBookmarks = asyncWrapper(async (req, res, next) => {
|
|
||||||
const bookmarks = await Bookmark.findAll({
|
|
||||||
order: [[Sequelize.fn('lower', Sequelize.col('name')), 'ASC']],
|
|
||||||
});
|
|
||||||
|
|
||||||
res.status(200).json({
|
|
||||||
success: true,
|
|
||||||
data: bookmarks,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// @desc Get single bookmark
|
|
||||||
// @route GET /api/bookmarks/:id
|
|
||||||
// @access Public
|
|
||||||
exports.getBookmark = asyncWrapper(async (req, res, next) => {
|
|
||||||
const bookmark = await Bookmark.findOne({
|
|
||||||
where: { id: req.params.id },
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!bookmark) {
|
|
||||||
return next(
|
|
||||||
new ErrorResponse(
|
|
||||||
`Bookmark with id of ${req.params.id} was not found`,
|
|
||||||
404
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
res.status(200).json({
|
|
||||||
success: true,
|
|
||||||
data: bookmark,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// @desc Update bookmark
|
|
||||||
// @route PUT /api/bookmarks/:id
|
|
||||||
// @access Public
|
|
||||||
exports.updateBookmark = asyncWrapper(async (req, res, next) => {
|
|
||||||
let bookmark = await Bookmark.findOne({
|
|
||||||
where: { id: req.params.id },
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!bookmark) {
|
|
||||||
return next(
|
|
||||||
new ErrorResponse(
|
|
||||||
`Bookmark with id of ${req.params.id} was not found`,
|
|
||||||
404
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
let _body = {
|
|
||||||
...req.body,
|
|
||||||
categoryId: parseInt(req.body.categoryId),
|
|
||||||
};
|
|
||||||
|
|
||||||
if (req.file) {
|
|
||||||
_body.icon = req.file.filename;
|
|
||||||
}
|
|
||||||
|
|
||||||
bookmark = await bookmark.update(_body);
|
|
||||||
|
|
||||||
res.status(200).json({
|
|
||||||
success: true,
|
|
||||||
data: bookmark,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// @desc Delete bookmark
|
|
||||||
// @route DELETE /api/bookmarks/:id
|
|
||||||
// @access Public
|
|
||||||
exports.deleteBookmark = asyncWrapper(async (req, res, next) => {
|
|
||||||
await Bookmark.destroy({
|
|
||||||
where: { id: req.params.id },
|
|
||||||
});
|
|
||||||
|
|
||||||
res.status(200).json({
|
|
||||||
success: true,
|
|
||||||
data: {},
|
|
||||||
});
|
|
||||||
});
|
|
27
controllers/bookmarks/createBookmark.js
Normal file
27
controllers/bookmarks/createBookmark.js
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
const asyncWrapper = require('../../middleware/asyncWrapper');
|
||||||
|
const Bookmark = require('../../models/Bookmark');
|
||||||
|
|
||||||
|
// @desc Create new bookmark
|
||||||
|
// @route POST /api/bookmarks
|
||||||
|
// @access Public
|
||||||
|
const createBookmark = asyncWrapper(async (req, res, next) => {
|
||||||
|
let bookmark;
|
||||||
|
|
||||||
|
let _body = {
|
||||||
|
...req.body,
|
||||||
|
categoryId: parseInt(req.body.categoryId),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (req.file) {
|
||||||
|
_body.icon = req.file.filename;
|
||||||
|
}
|
||||||
|
|
||||||
|
bookmark = await Bookmark.create(_body);
|
||||||
|
|
||||||
|
res.status(201).json({
|
||||||
|
success: true,
|
||||||
|
data: bookmark,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = createBookmark;
|
18
controllers/bookmarks/deleteBookmark.js
Normal file
18
controllers/bookmarks/deleteBookmark.js
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
const asyncWrapper = require('../../middleware/asyncWrapper');
|
||||||
|
const Bookmark = require('../../models/Bookmark');
|
||||||
|
|
||||||
|
// @desc Delete bookmark
|
||||||
|
// @route DELETE /api/bookmarks/:id
|
||||||
|
// @access Public
|
||||||
|
const deleteBookmark = asyncWrapper(async (req, res, next) => {
|
||||||
|
await Bookmark.destroy({
|
||||||
|
where: { id: req.params.id },
|
||||||
|
});
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
data: {},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = deleteBookmark;
|
19
controllers/bookmarks/getAllBookmarks.js
Normal file
19
controllers/bookmarks/getAllBookmarks.js
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
const asyncWrapper = require('../../middleware/asyncWrapper');
|
||||||
|
const Bookmark = require('../../models/Bookmark');
|
||||||
|
const { Sequelize } = require('sequelize');
|
||||||
|
|
||||||
|
// @desc Get all bookmarks
|
||||||
|
// @route GET /api/bookmarks
|
||||||
|
// @access Public
|
||||||
|
const getAllBookmarks = asyncWrapper(async (req, res, next) => {
|
||||||
|
const bookmarks = await Bookmark.findAll({
|
||||||
|
order: [[Sequelize.fn('lower', Sequelize.col('name')), 'ASC']],
|
||||||
|
});
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
data: bookmarks,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = getAllBookmarks;
|
28
controllers/bookmarks/getSingleBookmark.js
Normal file
28
controllers/bookmarks/getSingleBookmark.js
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
const asyncWrapper = require('../../middleware/asyncWrapper');
|
||||||
|
const ErrorResponse = require('../../utils/ErrorResponse');
|
||||||
|
const Bookmark = require('../../models/Bookmark');
|
||||||
|
|
||||||
|
// @desc Get single bookmark
|
||||||
|
// @route GET /api/bookmarks/:id
|
||||||
|
// @access Public
|
||||||
|
const getSingleBookmark = asyncWrapper(async (req, res, next) => {
|
||||||
|
const bookmark = await Bookmark.findOne({
|
||||||
|
where: { id: req.params.id },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!bookmark) {
|
||||||
|
return next(
|
||||||
|
new ErrorResponse(
|
||||||
|
`Bookmark with the id of ${req.params.id} was not found`,
|
||||||
|
404
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
data: bookmark,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = getSingleBookmark;
|
7
controllers/bookmarks/index.js
Normal file
7
controllers/bookmarks/index.js
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
module.exports = {
|
||||||
|
createBookmark: require('./createBookmark'),
|
||||||
|
getAllBookmarks: require('./getAllBookmarks'),
|
||||||
|
getSingleBookmark: require('./getSingleBookmark'),
|
||||||
|
updateBookmark: require('./updateBookmark'),
|
||||||
|
deleteBookmark: require('./deleteBookmark'),
|
||||||
|
};
|
39
controllers/bookmarks/updateBookmark.js
Normal file
39
controllers/bookmarks/updateBookmark.js
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
const asyncWrapper = require('../../middleware/asyncWrapper');
|
||||||
|
const ErrorResponse = require('../../utils/ErrorResponse');
|
||||||
|
const Bookmark = require('../../models/Bookmark');
|
||||||
|
|
||||||
|
// @desc Update bookmark
|
||||||
|
// @route PUT /api/bookmarks/:id
|
||||||
|
// @access Public
|
||||||
|
const updateBookmark = asyncWrapper(async (req, res, next) => {
|
||||||
|
let bookmark = await Bookmark.findOne({
|
||||||
|
where: { id: req.params.id },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!bookmark) {
|
||||||
|
return next(
|
||||||
|
new ErrorResponse(
|
||||||
|
`Bookmark with id of ${req.params.id} was not found`,
|
||||||
|
404
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let _body = {
|
||||||
|
...req.body,
|
||||||
|
categoryId: parseInt(req.body.categoryId),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (req.file) {
|
||||||
|
_body.icon = req.file.filename;
|
||||||
|
}
|
||||||
|
|
||||||
|
bookmark = await bookmark.update(_body);
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
data: bookmark,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = updateBookmark;
|
|
@ -4,20 +4,17 @@ const Category = require('../models/Category');
|
||||||
const Bookmark = require('../models/Bookmark');
|
const Bookmark = require('../models/Bookmark');
|
||||||
const Config = require('../models/Config');
|
const Config = require('../models/Config');
|
||||||
const { Sequelize } = require('sequelize');
|
const { Sequelize } = require('sequelize');
|
||||||
|
const loadConfig = require('../utils/loadConfig');
|
||||||
|
|
||||||
// @desc Create new category
|
// @desc Create new category
|
||||||
// @route POST /api/categories
|
// @route POST /api/categories
|
||||||
// @access Public
|
// @access Public
|
||||||
exports.createCategory = asyncWrapper(async (req, res, next) => {
|
exports.createCategory = asyncWrapper(async (req, res, next) => {
|
||||||
// Get config from database
|
const { pinCategoriesByDefault: pinCategories } = await loadConfig();
|
||||||
const pinCategories = await Config.findOne({
|
|
||||||
where: { key: 'pinCategoriesByDefault' },
|
|
||||||
});
|
|
||||||
|
|
||||||
let category;
|
let category;
|
||||||
|
|
||||||
if (pinCategories) {
|
if (pinCategories) {
|
||||||
if (parseInt(pinCategories.value)) {
|
|
||||||
category = await Category.create({
|
category = await Category.create({
|
||||||
...req.body,
|
...req.body,
|
||||||
isPinned: true,
|
isPinned: true,
|
||||||
|
@ -25,7 +22,6 @@ exports.createCategory = asyncWrapper(async (req, res, next) => {
|
||||||
} else {
|
} else {
|
||||||
category = await Category.create(req.body);
|
category = await Category.create(req.body);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
res.status(201).json({
|
res.status(201).json({
|
||||||
success: true,
|
success: true,
|
||||||
|
@ -37,12 +33,8 @@ exports.createCategory = asyncWrapper(async (req, res, next) => {
|
||||||
// @route GET /api/categories
|
// @route GET /api/categories
|
||||||
// @access Public
|
// @access Public
|
||||||
exports.getCategories = asyncWrapper(async (req, res, next) => {
|
exports.getCategories = asyncWrapper(async (req, res, next) => {
|
||||||
// Get config from database
|
const { useOrdering: orderType } = await loadConfig();
|
||||||
const useOrdering = await Config.findOne({
|
|
||||||
where: { key: 'useOrdering' },
|
|
||||||
});
|
|
||||||
|
|
||||||
const orderType = useOrdering ? useOrdering.value : 'createdAt';
|
|
||||||
let categories;
|
let categories;
|
||||||
|
|
||||||
if (orderType == 'name') {
|
if (orderType == 'name') {
|
||||||
|
|
|
@ -1,177 +0,0 @@
|
||||||
const asyncWrapper = require('../middleware/asyncWrapper');
|
|
||||||
const ErrorResponse = require('../utils/ErrorResponse');
|
|
||||||
const Config = require('../models/Config');
|
|
||||||
const { Op } = require('sequelize');
|
|
||||||
const File = require('../utils/File');
|
|
||||||
const { join } = require('path');
|
|
||||||
const fs = require('fs');
|
|
||||||
|
|
||||||
// @desc Insert new key:value pair
|
|
||||||
// @route POST /api/config
|
|
||||||
// @access Public
|
|
||||||
exports.createPair = asyncWrapper(async (req, res, next) => {
|
|
||||||
const pair = await Config.create(req.body);
|
|
||||||
|
|
||||||
res.status(201).json({
|
|
||||||
success: true,
|
|
||||||
data: pair,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// @desc Get all key:value pairs
|
|
||||||
// @route GET /api/config
|
|
||||||
// @route GET /api/config?keys=foo,bar,baz
|
|
||||||
// @access Public
|
|
||||||
exports.getAllPairs = asyncWrapper(async (req, res, next) => {
|
|
||||||
let pairs;
|
|
||||||
|
|
||||||
if (req.query.keys) {
|
|
||||||
// Check for specific keys to get in a single query
|
|
||||||
const keys = req.query.keys.split(',').map((key) => {
|
|
||||||
return { key };
|
|
||||||
});
|
|
||||||
|
|
||||||
pairs = await Config.findAll({
|
|
||||||
where: {
|
|
||||||
[Op.or]: keys,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// Else get all
|
|
||||||
pairs = await Config.findAll();
|
|
||||||
}
|
|
||||||
|
|
||||||
res.status(200).json({
|
|
||||||
success: true,
|
|
||||||
data: pairs,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// @desc Get single key:value pair
|
|
||||||
// @route GET /api/config/:key
|
|
||||||
// @access Public
|
|
||||||
exports.getSinglePair = asyncWrapper(async (req, res, next) => {
|
|
||||||
const pair = await Config.findOne({
|
|
||||||
where: { key: req.params.key },
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!pair) {
|
|
||||||
return next(new ErrorResponse(`Key ${req.params.key} was not found`, 404));
|
|
||||||
}
|
|
||||||
|
|
||||||
res.status(200).json({
|
|
||||||
success: true,
|
|
||||||
data: pair,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// @desc Update value
|
|
||||||
// @route PUT /api/config/:key
|
|
||||||
// @access Public
|
|
||||||
exports.updateValue = asyncWrapper(async (req, res, next) => {
|
|
||||||
let pair = await Config.findOne({
|
|
||||||
where: { key: req.params.key },
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!pair) {
|
|
||||||
return next(new ErrorResponse(`Key ${req.params.key} was not found`, 404));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (pair.isLocked) {
|
|
||||||
return next(
|
|
||||||
new ErrorResponse(
|
|
||||||
`Value of key ${req.params.key} is locked and can not be changed`,
|
|
||||||
400
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
pair = await pair.update({ ...req.body });
|
|
||||||
|
|
||||||
res.status(200).json({
|
|
||||||
success: true,
|
|
||||||
data: pair,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// @desc Update multiple values
|
|
||||||
// @route PUT /api/config/
|
|
||||||
// @access Public
|
|
||||||
exports.updateValues = asyncWrapper(async (req, res, next) => {
|
|
||||||
Object.entries(req.body).forEach(async ([key, value]) => {
|
|
||||||
await Config.update(
|
|
||||||
{ value },
|
|
||||||
{
|
|
||||||
where: { key },
|
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
const config = await Config.findAll();
|
|
||||||
|
|
||||||
res.status(200).send({
|
|
||||||
success: true,
|
|
||||||
data: config,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// @desc Delete key:value pair
|
|
||||||
// @route DELETE /api/config/:key
|
|
||||||
// @access Public
|
|
||||||
exports.deletePair = asyncWrapper(async (req, res, next) => {
|
|
||||||
const pair = await Config.findOne({
|
|
||||||
where: { key: req.params.key },
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!pair) {
|
|
||||||
return next(new ErrorResponse(`Key ${req.params.key} was not found`, 404));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (pair.isLocked) {
|
|
||||||
return next(
|
|
||||||
new ErrorResponse(
|
|
||||||
`Value of key ${req.params.key} is locked and can not be deleted`,
|
|
||||||
400
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
await pair.destroy();
|
|
||||||
|
|
||||||
res.status(200).json({
|
|
||||||
success: true,
|
|
||||||
data: {},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// @desc Get custom CSS file
|
|
||||||
// @route GET /api/config/0/css
|
|
||||||
// @access Public
|
|
||||||
exports.getCss = asyncWrapper(async (req, res, next) => {
|
|
||||||
const file = new File(join(__dirname, '../public/flame.css'));
|
|
||||||
const content = file.read();
|
|
||||||
|
|
||||||
res.status(200).json({
|
|
||||||
success: true,
|
|
||||||
data: content,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// @desc Update custom CSS file
|
|
||||||
// @route PUT /api/config/0/css
|
|
||||||
// @access Public
|
|
||||||
exports.updateCss = asyncWrapper(async (req, res, next) => {
|
|
||||||
const file = new File(join(__dirname, '../public/flame.css'));
|
|
||||||
file.write(req.body.styles, false);
|
|
||||||
|
|
||||||
// Copy file to docker volume
|
|
||||||
fs.copyFileSync(
|
|
||||||
join(__dirname, '../public/flame.css'),
|
|
||||||
join(__dirname, '../data/flame.css')
|
|
||||||
);
|
|
||||||
|
|
||||||
res.status(200).json({
|
|
||||||
success: true,
|
|
||||||
data: {},
|
|
||||||
});
|
|
||||||
});
|
|
18
controllers/config/getCSS.js
Normal file
18
controllers/config/getCSS.js
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
const asyncWrapper = require('../../middleware/asyncWrapper');
|
||||||
|
const File = require('../../utils/File');
|
||||||
|
const { join } = require('path');
|
||||||
|
|
||||||
|
// @desc Get custom CSS file
|
||||||
|
// @route GET /api/config/0/css
|
||||||
|
// @access Public
|
||||||
|
const getCSS = asyncWrapper(async (req, res, next) => {
|
||||||
|
const file = new File(join(__dirname, '../../public/flame.css'));
|
||||||
|
const content = file.read();
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
data: content,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = getCSS;
|
16
controllers/config/getConfig.js
Normal file
16
controllers/config/getConfig.js
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
const asyncWrapper = require('../../middleware/asyncWrapper');
|
||||||
|
const loadConfig = require('../../utils/loadConfig');
|
||||||
|
|
||||||
|
// @desc Get config
|
||||||
|
// @route GET /api/config
|
||||||
|
// @access Public
|
||||||
|
const getConfig = asyncWrapper(async (req, res, next) => {
|
||||||
|
const config = await loadConfig();
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
data: config,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = getConfig;
|
6
controllers/config/index.js
Normal file
6
controllers/config/index.js
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
module.exports = {
|
||||||
|
getCSS: require('./getCSS'),
|
||||||
|
updateCSS: require('./updateCSS'),
|
||||||
|
getConfig: require('./getConfig'),
|
||||||
|
updateConfig: require('./updateConfig'),
|
||||||
|
};
|
24
controllers/config/updateCSS.js
Normal file
24
controllers/config/updateCSS.js
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
const asyncWrapper = require('../../middleware/asyncWrapper');
|
||||||
|
const File = require('../../utils/File');
|
||||||
|
const { join } = require('path');
|
||||||
|
|
||||||
|
// @desc Update custom CSS file
|
||||||
|
// @route PUT /api/config/0/css
|
||||||
|
// @access Public
|
||||||
|
const updateCSS = asyncWrapper(async (req, res, next) => {
|
||||||
|
const file = new File(join(__dirname, '../../public/flame.css'));
|
||||||
|
file.write(req.body.styles, false);
|
||||||
|
|
||||||
|
// Copy file to docker volume
|
||||||
|
fs.copyFileSync(
|
||||||
|
join(__dirname, '../../public/flame.css'),
|
||||||
|
join(__dirname, '../../data/flame.css')
|
||||||
|
);
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
data: {},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = updateCSS;
|
24
controllers/config/updateConfig.js
Normal file
24
controllers/config/updateConfig.js
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
const asyncWrapper = require('../../middleware/asyncWrapper');
|
||||||
|
const loadConfig = require('../../utils/loadConfig');
|
||||||
|
const { writeFile } = require('fs/promises');
|
||||||
|
|
||||||
|
// @desc Update config
|
||||||
|
// @route PUT /api/config/
|
||||||
|
// @access Public
|
||||||
|
const updateConfig = asyncWrapper(async (req, res, next) => {
|
||||||
|
const existingConfig = await loadConfig();
|
||||||
|
|
||||||
|
const newConfig = {
|
||||||
|
...existingConfig,
|
||||||
|
...req.body,
|
||||||
|
};
|
||||||
|
|
||||||
|
await writeFile('data/config.json', JSON.stringify(newConfig));
|
||||||
|
|
||||||
|
res.status(200).send({
|
||||||
|
success: true,
|
||||||
|
data: newConfig,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = updateConfig;
|
|
@ -1,6 +1,5 @@
|
||||||
const { Sequelize } = require('sequelize');
|
const { Sequelize } = require('sequelize');
|
||||||
const { join } = require('path');
|
const { join } = require('path');
|
||||||
const fs = require('fs');
|
|
||||||
const Umzug = require('umzug');
|
const Umzug = require('umzug');
|
||||||
const backupDB = require('./utils/backupDb');
|
const backupDB = require('./utils/backupDb');
|
||||||
|
|
||||||
|
|
37
db/migrations/01_new-config.js
Normal file
37
db/migrations/01_new-config.js
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
const { readFile, writeFile, copyFile } = require('fs/promises');
|
||||||
|
const Config = require('../../models/Config');
|
||||||
|
|
||||||
|
const up = async (query) => {
|
||||||
|
await copyFile('utils/init/initialConfig.json', 'data/config.json');
|
||||||
|
|
||||||
|
const initConfigFile = await readFile('data/config.json', 'utf-8');
|
||||||
|
const parsedNewConfig = JSON.parse(initConfigFile);
|
||||||
|
|
||||||
|
const existingConfig = await Config.findAll({ raw: true });
|
||||||
|
|
||||||
|
for (let pair of existingConfig) {
|
||||||
|
const { key, value, valueType } = pair;
|
||||||
|
|
||||||
|
let newValue = value;
|
||||||
|
|
||||||
|
if (valueType == 'number') {
|
||||||
|
newValue = parseFloat(value);
|
||||||
|
} else if (valueType == 'boolean') {
|
||||||
|
newValue = value == 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
parsedNewConfig[key] = newValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newConfig = JSON.stringify(parsedNewConfig);
|
||||||
|
await writeFile('data/config.json', newConfig);
|
||||||
|
|
||||||
|
await query.dropTable('config');
|
||||||
|
};
|
||||||
|
|
||||||
|
const down = async (query) => {};
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
up,
|
||||||
|
down,
|
||||||
|
};
|
|
@ -1,17 +1,7 @@
|
||||||
// const asyncWrapper = foo => (req, res, next) => {
|
|
||||||
// return Promise
|
|
||||||
// .resolve(foo(req, res, next))
|
|
||||||
// .catch(next);
|
|
||||||
// }
|
|
||||||
|
|
||||||
// module.exports = asyncWrapper;
|
|
||||||
|
|
||||||
function asyncWrapper(foo) {
|
function asyncWrapper(foo) {
|
||||||
return function (req, res, next) {
|
return function (req, res, next) {
|
||||||
return Promise
|
return Promise.resolve(foo(req, res, next)).catch(next);
|
||||||
.resolve(foo(req, res, next))
|
};
|
||||||
.catch(next);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = asyncWrapper;
|
module.exports = asyncWrapper;
|
|
@ -14,10 +14,14 @@ const errorHandler = (err, req, res, next) => {
|
||||||
|
|
||||||
logger.log(error.message.split(',')[0], 'ERROR');
|
logger.log(error.message.split(',')[0], 'ERROR');
|
||||||
|
|
||||||
res.status(err.statusCode || 500).json({
|
if (process.env.NODE_ENV == 'development') {
|
||||||
success: false,
|
console.log(err);
|
||||||
error: error.message || 'Server Error'
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
res.status(err.statusCode || 500).json({
|
||||||
|
success: false,
|
||||||
|
error: error.message || 'Server Error',
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
module.exports = errorHandler;
|
module.exports = errorHandler;
|
|
@ -11,7 +11,7 @@ const storage = multer.diskStorage({
|
||||||
},
|
},
|
||||||
filename: (req, file, cb) => {
|
filename: (req, file, cb) => {
|
||||||
cb(null, Date.now() + '--' + file.originalname);
|
cb(null, Date.now() + '--' + file.originalname);
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const supportedTypes = ['jpg', 'jpeg', 'png', 'svg', 'svg+xml'];
|
const supportedTypes = ['jpg', 'jpeg', 'png', 'svg', 'svg+xml'];
|
||||||
|
|
|
@ -4,26 +4,17 @@ const upload = require('../middleware/multer');
|
||||||
|
|
||||||
const {
|
const {
|
||||||
createApp,
|
createApp,
|
||||||
getApps,
|
getAllApps,
|
||||||
getApp,
|
getSingleApp,
|
||||||
updateApp,
|
updateApp,
|
||||||
deleteApp,
|
deleteApp,
|
||||||
reorderApps
|
reorderApps,
|
||||||
} = require('../controllers/apps');
|
} = require('../controllers/apps');
|
||||||
|
|
||||||
router
|
router.route('/').post(upload, createApp).get(getAllApps);
|
||||||
.route('/')
|
|
||||||
.post(upload, createApp)
|
|
||||||
.get(getApps);
|
|
||||||
|
|
||||||
router
|
router.route('/:id').get(getSingleApp).put(upload, updateApp).delete(deleteApp);
|
||||||
.route('/:id')
|
|
||||||
.get(getApp)
|
|
||||||
.put(upload, updateApp)
|
|
||||||
.delete(deleteApp);
|
|
||||||
|
|
||||||
router
|
router.route('/0/reorder').put(reorderApps);
|
||||||
.route('/0/reorder')
|
|
||||||
.put(reorderApps);
|
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
|
@ -4,20 +4,17 @@ const upload = require('../middleware/multer');
|
||||||
|
|
||||||
const {
|
const {
|
||||||
createBookmark,
|
createBookmark,
|
||||||
getBookmarks,
|
getAllBookmarks,
|
||||||
getBookmark,
|
getSingleBookmark,
|
||||||
updateBookmark,
|
updateBookmark,
|
||||||
deleteBookmark
|
deleteBookmark,
|
||||||
} = require('../controllers/bookmark');
|
} = require('../controllers/bookmarks');
|
||||||
|
|
||||||
router
|
router.route('/').post(upload, createBookmark).get(getAllBookmarks);
|
||||||
.route('/')
|
|
||||||
.post(upload, createBookmark)
|
|
||||||
.get(getBookmarks);
|
|
||||||
|
|
||||||
router
|
router
|
||||||
.route('/:id')
|
.route('/:id')
|
||||||
.get(getBookmark)
|
.get(getSingleBookmark)
|
||||||
.put(upload, updateBookmark)
|
.put(upload, updateBookmark)
|
||||||
.delete(deleteBookmark);
|
.delete(deleteBookmark);
|
||||||
|
|
||||||
|
|
|
@ -2,20 +2,14 @@ const express = require('express');
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
createPair,
|
getCSS,
|
||||||
getAllPairs,
|
updateCSS,
|
||||||
getSinglePair,
|
getConfig,
|
||||||
updateValue,
|
updateConfig,
|
||||||
updateValues,
|
|
||||||
deletePair,
|
|
||||||
updateCss,
|
|
||||||
getCss,
|
|
||||||
} = require('../controllers/config');
|
} = require('../controllers/config');
|
||||||
|
|
||||||
router.route('/').post(createPair).get(getAllPairs).put(updateValues);
|
router.route('/').get(getConfig).put(updateConfig);
|
||||||
|
|
||||||
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);
|
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
|
|
10
utils/checkFileExists.js
Normal file
10
utils/checkFileExists.js
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
const fs = require('fs');
|
||||||
|
|
||||||
|
const checkFileExists = (path) => {
|
||||||
|
return fs.promises
|
||||||
|
.access(path, fs.constants.F_OK)
|
||||||
|
.then(() => true)
|
||||||
|
.catch(() => false);
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = checkFileExists;
|
|
@ -1,15 +1,9 @@
|
||||||
const Config = require('../models/Config');
|
|
||||||
const Weather = require('../models/Weather');
|
const Weather = require('../models/Weather');
|
||||||
const axios = require('axios');
|
const axios = require('axios');
|
||||||
|
const loadConfig = require('./loadConfig');
|
||||||
|
|
||||||
const getExternalWeather = async () => {
|
const getExternalWeather = async () => {
|
||||||
// Get config from database
|
const { WEATHER_API_KEY: secret, lat, long } = await loadConfig();
|
||||||
const config = await Config.findAll();
|
|
||||||
|
|
||||||
// Find and check values
|
|
||||||
const secret = config.find(pair => pair.key === 'WEATHER_API_KEY');
|
|
||||||
const lat = config.find(pair => pair.key === 'lat');
|
|
||||||
const long = config.find(pair => pair.key === 'long');
|
|
||||||
|
|
||||||
if (!secret) {
|
if (!secret) {
|
||||||
throw new Error('API key was not found. Weather updated failed');
|
throw new Error('API key was not found. Weather updated failed');
|
||||||
|
@ -21,7 +15,9 @@ const getExternalWeather = async () => {
|
||||||
|
|
||||||
// Fetch data from external API
|
// Fetch data from external API
|
||||||
try {
|
try {
|
||||||
const res = await axios.get(`http://api.weatherapi.com/v1/current.json?key=${secret.value}&q=${lat.value},${long.value}`);
|
const res = await axios.get(
|
||||||
|
`http://api.weatherapi.com/v1/current.json?key=${secret}&q=${lat},${long}`
|
||||||
|
);
|
||||||
|
|
||||||
// Save weather data
|
// Save weather data
|
||||||
const cursor = res.data.current;
|
const cursor = res.data.current;
|
||||||
|
@ -32,12 +28,12 @@ const getExternalWeather = async () => {
|
||||||
isDay: cursor.is_day,
|
isDay: cursor.is_day,
|
||||||
cloud: cursor.cloud,
|
cloud: cursor.cloud,
|
||||||
conditionText: cursor.condition.text,
|
conditionText: cursor.condition.text,
|
||||||
conditionCode: cursor.condition.code
|
conditionCode: cursor.condition.code,
|
||||||
});
|
});
|
||||||
return weatherData;
|
return weatherData;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
throw new Error('External API request failed');
|
throw new Error('External API request failed');
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
module.exports = getExternalWeather;
|
module.exports = getExternalWeather;
|
|
@ -2,8 +2,8 @@ const initConfig = require('./initConfig');
|
||||||
const initFiles = require('./initFiles');
|
const initFiles = require('./initFiles');
|
||||||
|
|
||||||
const initApp = async () => {
|
const initApp = async () => {
|
||||||
await initConfig();
|
|
||||||
await initFiles();
|
await initFiles();
|
||||||
|
await initConfig();
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = initApp;
|
module.exports = initApp;
|
||||||
|
|
|
@ -1,39 +1,25 @@
|
||||||
const { Op } = require('sequelize');
|
const { copyFile, readFile, writeFile } = require('fs/promises');
|
||||||
const Config = require('../../models/Config');
|
const checkFileExists = require('../checkFileExists');
|
||||||
const { config } = require('./initialConfig.json');
|
const initialConfig = require('./initialConfig.json');
|
||||||
|
|
||||||
const Logger = require('../Logger');
|
|
||||||
const logger = new Logger();
|
|
||||||
|
|
||||||
const initConfig = async () => {
|
const initConfig = async () => {
|
||||||
// Get config values
|
const configExists = await checkFileExists('data/config.json');
|
||||||
const configPairs = await Config.findAll({
|
|
||||||
where: {
|
|
||||||
key: {
|
|
||||||
[Op.or]: config.map((pair) => pair.key),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Get key from each pair
|
if (!configExists) {
|
||||||
const configKeys = configPairs.map((pair) => pair.key);
|
await copyFile('utils/init/initialConfig.json', 'data/config.json');
|
||||||
|
|
||||||
// Create missing pairs
|
|
||||||
config.forEach(async ({ key, value }) => {
|
|
||||||
if (!configKeys.includes(key)) {
|
|
||||||
await Config.create({
|
|
||||||
key,
|
|
||||||
value,
|
|
||||||
valueType: typeof value,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (process.env.NODE_ENV == 'development') {
|
|
||||||
logger.log('Initial config created');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return;
|
const existingConfig = await readFile('data/config.json', 'utf-8');
|
||||||
|
const parsedConfig = JSON.parse(existingConfig);
|
||||||
|
|
||||||
|
// Add new config pairs if necessary
|
||||||
|
for (let key in initialConfig) {
|
||||||
|
if (!Object.keys(parsedConfig).includes(key)) {
|
||||||
|
parsedConfig[key] = initialConfig[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await writeFile('data/config.json', JSON.stringify(parsedConfig));
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = initConfig;
|
module.exports = initConfig;
|
||||||
|
|
|
@ -1,84 +1,22 @@
|
||||||
{
|
{
|
||||||
"config": [
|
"WEATHER_API_KEY": "",
|
||||||
{
|
"lat": 0,
|
||||||
"key": "WEATHER_API_KEY",
|
"long": 0,
|
||||||
"value": ""
|
"isCelsius": true,
|
||||||
},
|
"customTitle": "Flame",
|
||||||
{
|
"pinAppsByDefault": true,
|
||||||
"key": "lat",
|
"pinCategoriesByDefault": true,
|
||||||
"value": 0
|
"hideHeader": false,
|
||||||
},
|
"useOrdering": "createdAt",
|
||||||
{
|
"appsSameTab": false,
|
||||||
"key": "long",
|
"bookmarksSameTab": false,
|
||||||
"value": 0
|
"searchSameTab": false,
|
||||||
},
|
"hideApps": false,
|
||||||
{
|
"hideCategories": false,
|
||||||
"key": "isCelsius",
|
"hideSearch": false,
|
||||||
"value": true
|
"defaultSearchProvider": "l",
|
||||||
},
|
"dockerApps": false,
|
||||||
{
|
"dockerHost": "localhost",
|
||||||
"key": "customTitle",
|
"kubernetesApps": false,
|
||||||
"value": "Flame"
|
"unpinStoppedApps": false
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "pinAppsByDefault",
|
|
||||||
"value": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "pinCategoriesByDefault",
|
|
||||||
"value": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "hideHeader",
|
|
||||||
"value": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "useOrdering",
|
|
||||||
"value": "createdAt"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "appsSameTab",
|
|
||||||
"value": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "bookmarksSameTab",
|
|
||||||
"value": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "searchSameTab",
|
|
||||||
"value": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "hideApps",
|
|
||||||
"value": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "hideCategories",
|
|
||||||
"value": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "hideSearch",
|
|
||||||
"value": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "defaultSearchProvider",
|
|
||||||
"value": "l"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "dockerApps",
|
|
||||||
"value": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "dockerHost",
|
|
||||||
"value": "localhost"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "kubernetesApps",
|
|
||||||
"value": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "unpinStoppedApps",
|
|
||||||
"value": false
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
18
utils/loadConfig.js
Normal file
18
utils/loadConfig.js
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
const { readFile } = require('fs/promises');
|
||||||
|
const checkFileExists = require('../utils/checkFileExists');
|
||||||
|
const initConfig = require('../utils/init/initConfig');
|
||||||
|
|
||||||
|
const loadConfig = async () => {
|
||||||
|
const configExists = await checkFileExists('data/config.json');
|
||||||
|
|
||||||
|
if (!configExists) {
|
||||||
|
await initConfig();
|
||||||
|
}
|
||||||
|
|
||||||
|
const config = await readFile('data/config.json', 'utf-8');
|
||||||
|
const parsedConfig = JSON.parse(config);
|
||||||
|
|
||||||
|
return parsedConfig;
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = loadConfig;
|
Loading…
Add table
Add a link
Reference in a new issue