diff --git a/CHANGELOG.md b/CHANGELOG.md
index e6df0e5..b1bd00e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,6 @@
+### v1.6.3 (TBA)
+- Added support for custom SVG icons ([#73](https://github.com/pawelmalak/flame/issues/73))
+
### v1.6.2 (2021-08-06)
- Fixed changelog link
- Added support for Docker API ([#14](https://github.com/pawelmalak/flame/issues/14))
diff --git a/client/.env b/client/.env
index 0c25886..3ab31e3 100644
--- a/client/.env
+++ b/client/.env
@@ -1 +1 @@
-REACT_APP_VERSION=1.6.2
\ No newline at end of file
+REACT_APP_VERSION=1.6.3
\ No newline at end of file
diff --git a/client/package-lock.json b/client/package-lock.json
index 66b371f..8326fc6 100644
--- a/client/package-lock.json
+++ b/client/package-lock.json
@@ -6462,6 +6462,14 @@
}
}
},
+ "external-svg-loader": {
+ "version": "1.3.4",
+ "resolved": "https://registry.npmjs.org/external-svg-loader/-/external-svg-loader-1.3.4.tgz",
+ "integrity": "sha512-73h7/rYYA4KnIV74M/0r6zHWPLuY/8QHnwKymwh+46tbQAZ0ZtoN98TJZI+CUYTfP2nXgqslCgSsxcr7eOw45w==",
+ "requires": {
+ "idb-keyval": "^3.2.0"
+ }
+ },
"extglob": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz",
@@ -7527,6 +7535,11 @@
"postcss": "^7.0.14"
}
},
+ "idb-keyval": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/idb-keyval/-/idb-keyval-3.2.0.tgz",
+ "integrity": "sha512-slx8Q6oywCCSfKgPgL0sEsXtPVnSbTLWpyiDcu6msHOyKOLari1TD1qocXVCft80umnkk3/Qqh3lwoFt8T/BPQ=="
+ },
"identity-obj-proxy": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/identity-obj-proxy/-/identity-obj-proxy-3.0.0.tgz",
diff --git a/client/package.json b/client/package.json
index 832d079..f50079e 100644
--- a/client/package.json
+++ b/client/package.json
@@ -16,6 +16,7 @@
"@types/react-redux": "^7.1.16",
"@types/react-router-dom": "^5.1.7",
"axios": "^0.21.1",
+ "external-svg-loader": "^1.3.4",
"http-proxy-middleware": "^2.0.0",
"react": "^17.0.2",
"react-beautiful-dnd": "^13.1.0",
diff --git a/client/src/App.tsx b/client/src/App.tsx
index 157206e..05db805 100644
--- a/client/src/App.tsx
+++ b/client/src/App.tsx
@@ -1,5 +1,6 @@
import { BrowserRouter, Route, Switch } from 'react-router-dom';
import { getConfig, setTheme } from './store/actions';
+import 'external-svg-loader';
// Redux
import { store } from './store/store';
@@ -40,6 +41,6 @@ const App = (): JSX.Element => {
);
-}
+};
-export default App;
\ No newline at end of file
+export default App;
diff --git a/client/src/components/Apps/AppCard/AppCard.module.css b/client/src/components/Apps/AppCard/AppCard.module.css
index 768ef8e..d6b13a8 100644
--- a/client/src/components/Apps/AppCard/AppCard.module.css
+++ b/client/src/components/Apps/AppCard/AppCard.module.css
@@ -33,11 +33,11 @@
.AppCard {
padding: 2px;
border-radius: 4px;
- transition: all 0.10s;
+ transition: all 0.1s;
}
.AppCard:hover {
- background-color: rgba(0,0,0,0.2);
+ background-color: rgba(0, 0, 0, 0.2);
}
}
@@ -47,4 +47,4 @@
margin-top: 2px;
margin-left: 2px;
object-fit: contain;
-}
\ No newline at end of file
+}
diff --git a/client/src/components/Apps/AppCard/AppCard.tsx b/client/src/components/Apps/AppCard/AppCard.tsx
index 79ad3d8..172a680 100644
--- a/client/src/components/Apps/AppCard/AppCard.tsx
+++ b/client/src/components/Apps/AppCard/AppCard.tsx
@@ -13,6 +13,31 @@ interface ComponentProps {
const AppCard = (props: ComponentProps): JSX.Element => {
const [displayUrl, redirectUrl] = urlParser(props.app.url);
+ let iconEl: JSX.Element;
+ const { icon } = props.app;
+
+ if (/.(jpeg|jpg|png)$/i.test(icon)) {
+ iconEl = (
+
+ );
+ } else if (/.(svg)$/i.test(icon)) {
+ iconEl = (
+
+
+
+ );
+ } else {
+ iconEl = ;
+ }
+
return (
{
rel='noreferrer'
className={classes.AppCard}
>
-
- {(/.(jpeg|jpg|png)$/i).test(props.app.icon)
- ?

- :
- }
-
+ {iconEl}
{props.app.name}
{displayUrl}
- )
-}
+ );
+};
-export default AppCard;
\ No newline at end of file
+export default AppCard;
diff --git a/client/src/components/Apps/AppForm/AppForm.tsx b/client/src/components/Apps/AppForm/AppForm.tsx
index 72d8db2..5d05f0a 100644
--- a/client/src/components/Apps/AppForm/AppForm.tsx
+++ b/client/src/components/Apps/AppForm/AppForm.tsx
@@ -31,28 +31,28 @@ const AppForm = (props: ComponentProps): JSX.Element => {
name: props.app.name,
url: props.app.url,
icon: props.app.icon
- })
+ });
} else {
setFormData({
name: '',
url: '',
icon: ''
- })
+ });
}
- }, [props.app])
+ }, [props.app]);
const inputChangeHandler = (e: ChangeEvent): void => {
setFormData({
...formData,
[e.target.name]: e.target.value
- })
- }
+ });
+ };
const fileChangeHandler = (e: ChangeEvent): void => {
if (e.target.files) {
setCustomIcon(e.target.files[0]);
}
- }
+ };
const formSubmitHandler = (e: SyntheticEvent): void => {
e.preventDefault();
@@ -66,7 +66,7 @@ const AppForm = (props: ComponentProps): JSX.Element => {
data.append('url', formData.url);
return data;
- }
+ };
if (!props.app) {
if (customIcon) {
@@ -89,10 +89,10 @@ const AppForm = (props: ComponentProps): JSX.Element => {
name: '',
url: '',
icon: ''
- })
+ });
setCustomIcon(null);
- }
+ };
return (
{
placeholder='Bookstack'
required
value={formData.name}
- onChange={(e) => inputChangeHandler(e)}
+ onChange={e => inputChangeHandler(e)}
/>
@@ -120,7 +120,7 @@ const AppForm = (props: ComponentProps): JSX.Element => {
placeholder='bookstack.example.com'
required
value={formData.url}
- onChange={(e) => inputChangeHandler(e)}
+ onChange={e => inputChangeHandler(e)}
/>
{
target='_blank'
rel='noreferrer'
>
- {' '}Check supported URL formats
+ {' '}
+ Check supported URL formats
- {!useCustomIcon
+ {!useCustomIcon ? (
// use mdi icon
- ? (
-
- inputChangeHandler(e)}
- />
-
- Use icon name from MDI.
-
- {' '}Click here for reference
-
-
- toggleUseCustomIcon(!useCustomIcon)}
- className={classes.Switch}>
- Switch to custom icon upload
-
- )
+
+
+ inputChangeHandler(e)}
+ />
+
+ Use icon name from MDI.
+
+ {' '}
+ Click here for reference
+
+
+ toggleUseCustomIcon(!useCustomIcon)}
+ className={classes.Switch}
+ >
+ Switch to custom icon upload
+
+
+ ) : (
// upload custom icon
- : (
-
- fileChangeHandler(e)}
- accept='.jpg,.jpeg,.png'
- />
- toggleUseCustomIcon(!useCustomIcon)}
- className={classes.Switch}>
- Switch to MDI
-
- )
- }
- {!props.app
- ?
- :
- }
+
+
+ fileChangeHandler(e)}
+ accept='.jpg,.jpeg,.png,.svg'
+ />
+ toggleUseCustomIcon(!useCustomIcon)}
+ className={classes.Switch}
+ >
+ Switch to MDI
+
+
+ )}
+ {!props.app ? (
+
+ ) : (
+
+ )}
- )
-}
+ );
+};
-export default connect(null, { addApp, updateApp })(AppForm);
\ No newline at end of file
+export default connect(null, { addApp, updateApp })(AppForm);
diff --git a/client/src/components/Bookmarks/BookmarkCard/BookmarkCard.module.css b/client/src/components/Bookmarks/BookmarkCard/BookmarkCard.module.css
index b21ed42..ec5cbfd 100644
--- a/client/src/components/Bookmarks/BookmarkCard/BookmarkCard.module.css
+++ b/client/src/components/Bookmarks/BookmarkCard/BookmarkCard.module.css
@@ -32,4 +32,14 @@
display: flex;
margin-top: 3px;
margin-right: 2px;
-}
\ No newline at end of file
+ justify-content: center;
+ align-items: center;
+}
+
+.BookmarkIconSvg {
+ width: 80%;
+ height: 80%;
+ margin-top: 2px;
+ margin-left: 2px;
+ object-fit: contain;
+}
diff --git a/client/src/components/Bookmarks/BookmarkCard/BookmarkCard.tsx b/client/src/components/Bookmarks/BookmarkCard/BookmarkCard.tsx
index fe2198b..d3c0b2d 100644
--- a/client/src/components/Bookmarks/BookmarkCard/BookmarkCard.tsx
+++ b/client/src/components/Bookmarks/BookmarkCard/BookmarkCard.tsx
@@ -16,31 +16,52 @@ const BookmarkCard = (props: ComponentProps): JSX.Element => {
{props.category.bookmarks.map((bookmark: Bookmark) => {
const redirectUrl = urlParser(bookmark.url)[1];
+ let iconEl: JSX.Element;
+ const { icon, name } = bookmark;
+
+ if (/.(jpeg|jpg|png)$/i.test(icon)) {
+ iconEl = (
+
+

+
+ );
+ } else if (/.(svg)$/i.test(icon)) {
+ iconEl = (
+
+
+
+ );
+ } else {
+ iconEl = (
+
+
+
+ );
+ }
+
return (
- {bookmark.icon && (
-
- {(/.(jpeg|jpg|png)$/i).test(bookmark.icon)
- ?

- :
- }
-
- )}
+ key={`bookmark-${bookmark.id}`}
+ >
+ {icon && iconEl}
{bookmark.name}
- )
+ );
})}
- )
-}
+ );
+};
-export default BookmarkCard;
\ No newline at end of file
+export default BookmarkCard;
diff --git a/client/src/components/Bookmarks/BookmarkForm/BookmarkForm.tsx b/client/src/components/Bookmarks/BookmarkForm/BookmarkForm.tsx
index 67059ae..10d6de2 100644
--- a/client/src/components/Bookmarks/BookmarkForm/BookmarkForm.tsx
+++ b/client/src/components/Bookmarks/BookmarkForm/BookmarkForm.tsx
@@ -1,11 +1,31 @@
-import { useState, SyntheticEvent, Fragment, ChangeEvent, useEffect } from 'react';
+import {
+ useState,
+ SyntheticEvent,
+ Fragment,
+ ChangeEvent,
+ useEffect
+} from 'react';
import { connect } from 'react-redux';
import ModalForm from '../../UI/Forms/ModalForm/ModalForm';
import InputGroup from '../../UI/Forms/InputGroup/InputGroup';
-import { Bookmark, Category, GlobalState, NewBookmark, NewCategory, NewNotification } from '../../../interfaces';
+import {
+ Bookmark,
+ Category,
+ GlobalState,
+ NewBookmark,
+ NewCategory,
+ NewNotification
+} from '../../../interfaces';
import { ContentType } from '../Bookmarks';
-import { getCategories, addCategory, addBookmark, updateCategory, updateBookmark, createNotification } from '../../../store/actions';
+import {
+ getCategories,
+ addCategory,
+ addBookmark,
+ updateCategory,
+ updateBookmark,
+ createNotification
+} from '../../../store/actions';
import Button from '../../UI/Buttons/Button/Button';
import classes from './BookmarkForm.module.css';
@@ -22,8 +42,8 @@ interface ComponentProps {
id: number,
formData: NewBookmark | FormData,
category: {
- prev: number,
- curr: number
+ prev: number;
+ curr: number;
}
) => void;
createNotification: (notification: NewNotification) => void;
@@ -34,14 +54,14 @@ const BookmarkForm = (props: ComponentProps): JSX.Element => {
const [customIcon, setCustomIcon] = useState(null);
const [categoryName, setCategoryName] = useState({
name: ''
- })
+ });
const [formData, setFormData] = useState({
name: '',
url: '',
categoryId: -1,
icon: ''
- })
+ });
// Load category data if provided for editing
useEffect(() => {
@@ -50,7 +70,7 @@ const BookmarkForm = (props: ComponentProps): JSX.Element => {
} else {
setCategoryName({ name: '' });
}
- }, [props.category])
+ }, [props.category]);
// Load bookmark data if provided for editing
useEffect(() => {
@@ -60,16 +80,16 @@ const BookmarkForm = (props: ComponentProps): JSX.Element => {
url: props.bookmark.url,
categoryId: props.bookmark.categoryId,
icon: props.bookmark.icon
- })
+ });
} else {
setFormData({
name: '',
url: '',
categoryId: -1,
icon: ''
- })
+ });
}
- }, [props.bookmark])
+ }, [props.bookmark]);
const formSubmitHandler = (e: SyntheticEvent): void => {
e.preventDefault();
@@ -84,7 +104,7 @@ const BookmarkForm = (props: ComponentProps): JSX.Element => {
data.append('categoryId', `${formData.categoryId}`);
return data;
- }
+ };
if (!props.category && !props.bookmark) {
// Add new
@@ -98,7 +118,7 @@ const BookmarkForm = (props: ComponentProps): JSX.Element => {
props.createNotification({
title: 'Error',
message: 'Please select category'
- })
+ });
return;
}
@@ -108,15 +128,15 @@ const BookmarkForm = (props: ComponentProps): JSX.Element => {
} else {
props.addBookmark(formData);
}
-
+
setFormData({
name: '',
url: '',
categoryId: formData.categoryId,
icon: ''
- })
+ });
- setCustomIcon(null)
+ setCustomIcon(null);
}
} else {
// Update
@@ -128,23 +148,15 @@ const BookmarkForm = (props: ComponentProps): JSX.Element => {
// Update bookmark
if (customIcon) {
const data = createFormData();
- props.updateBookmark(
- props.bookmark.id,
- data,
- {
- prev: props.bookmark.categoryId,
- curr: formData.categoryId
- }
- )
+ props.updateBookmark(props.bookmark.id, data, {
+ prev: props.bookmark.categoryId,
+ curr: formData.categoryId
+ });
} else {
- props.updateBookmark(
- props.bookmark.id,
- formData,
- {
- prev: props.bookmark.categoryId,
- curr: formData.categoryId
- }
- );
+ props.updateBookmark(props.bookmark.id, formData, {
+ prev: props.bookmark.categoryId,
+ curr: formData.categoryId
+ });
}
setFormData({
@@ -152,36 +164,36 @@ const BookmarkForm = (props: ComponentProps): JSX.Element => {
url: '',
categoryId: -1,
icon: ''
- })
+ });
- setCustomIcon(null)
+ setCustomIcon(null);
}
props.modalHandler();
}
- }
+ };
const inputChangeHandler = (e: ChangeEvent): void => {
setFormData({
...formData,
[e.target.name]: e.target.value
- })
- }
+ });
+ };
const selectChangeHandler = (e: ChangeEvent): void => {
setFormData({
...formData,
categoryId: parseInt(e.target.value)
- })
- }
+ });
+ };
const fileChangeHandler = (e: ChangeEvent): void => {
if (e.target.files) {
setCustomIcon(e.target.files[0]);
}
- }
+ };
- let button =
+ let button = ;
if (!props.category && !props.bookmark) {
if (props.contentType === ContentType.category) {
@@ -190,9 +202,9 @@ const BookmarkForm = (props: ComponentProps): JSX.Element => {
button = ;
}
} else if (props.category) {
- button =
+ button = ;
} else if (props.bookmark) {
- button =
+ button = ;
}
return (
@@ -200,136 +212,133 @@ const BookmarkForm = (props: ComponentProps): JSX.Element => {
modalHandler={props.modalHandler}
formHandler={formSubmitHandler}
>
- {props.contentType === ContentType.category
- ? (
-
+ {props.contentType === ContentType.category ? (
+
+
+
+ setCategoryName({ name: e.target.value })}
+ />
+
+
+ ) : (
+
+
+
+ inputChangeHandler(e)}
+ />
+
+
+
+ inputChangeHandler(e)}
+ />
+
+
+ {' '}
+ Check supported URL formats
+
+
+
+
+
+
+
+ {!useCustomIcon ? (
+ // mdi
-
+
setCategoryName({ name: e.target.value })}
- />
-
-
- )
- : (
-
-
-
- inputChangeHandler(e)}
- />
-
-
-
- inputChangeHandler(e)}
+ name='icon'
+ id='icon'
+ placeholder='book-open-outline'
+ value={formData.icon}
+ onChange={e => inputChangeHandler(e)}
/>
-
- {' '}Check supported URL formats
+ Use icon name from MDI.
+
+ {' '}
+ Click here for reference
-
-
-
-
+ Switch to custom icon upload
+
- {!useCustomIcon
- // mdi
- ? (
-
- inputChangeHandler(e)}
- />
-
- Use icon name from MDI.
-
- {' '}Click here for reference
-
-
- toggleUseCustomIcon(!useCustomIcon)}
- className={classes.Switch}>
- Switch to custom icon upload
-
- )
- // custom
- : (
-
- fileChangeHandler(e)}
- accept='.jpg,.jpeg,.png'
- />
- toggleUseCustomIcon(!useCustomIcon)}
- className={classes.Switch}>
- Switch to MDI
-
- )
- }
-
- )
- }
+ ) : (
+ // custom
+
+
+ fileChangeHandler(e)}
+ accept='.jpg,.jpeg,.png,.svg'
+ />
+ toggleUseCustomIcon(!useCustomIcon)}
+ className={classes.Switch}
+ >
+ Switch to MDI
+
+
+ )}
+
+ )}
{button}
- )
-}
+ );
+};
const mapStateToProps = (state: GlobalState) => {
return {
categories: state.bookmark.categories
- }
-}
+ };
+};
const dispatchMap = {
getCategories,
@@ -338,6 +347,6 @@ const dispatchMap = {
updateCategory,
updateBookmark,
createNotification
-}
+};
-export default connect(mapStateToProps, dispatchMap)(BookmarkForm);
\ No newline at end of file
+export default connect(mapStateToProps, dispatchMap)(BookmarkForm);
diff --git a/controllers/apps.js b/controllers/apps.js
index e4fa1bc..ab59f2c 100644
--- a/controllers/apps.js
+++ b/controllers/apps.js
@@ -126,8 +126,16 @@ exports.getApps = asyncWrapper(async (req, res, next) => {
});
}
- // Set header to fetch containers info every time
- res.status(200).setHeader('Cache-Control', 'no-store').json({
+ 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
});
diff --git a/db.js b/db.js
index 9761efe..f9cbcfd 100644
--- a/db.js
+++ b/db.js
@@ -6,15 +6,15 @@ const sequelize = new Sequelize({
dialect: 'sqlite',
storage: './data/db.sqlite',
logging: false
-})
+});
const connectDB = async () => {
try {
await sequelize.authenticate();
logger.log('Connected to database');
-
+
const syncModels = true;
-
+
if (syncModels) {
logger.log('Starting model synchronization');
await sequelize.sync({ alter: true });
@@ -24,9 +24,9 @@ const connectDB = async () => {
logger.log(`Unable to connect to the database: ${error.message}`, 'ERROR');
process.exit(1);
}
-}
+};
module.exports = {
connectDB,
sequelize
-}
\ No newline at end of file
+};
diff --git a/middleware/multer.js b/middleware/multer.js
index b1314a9..bd493f5 100644
--- a/middleware/multer.js
+++ b/middleware/multer.js
@@ -12,9 +12,9 @@ const storage = multer.diskStorage({
filename: (req, file, cb) => {
cb(null, Date.now() + '--' + file.originalname);
}
-})
+});
-const supportedTypes = ['jpg', 'jpeg', 'png'];
+const supportedTypes = ['jpg', 'jpeg', 'png', 'svg', 'svg+xml'];
const fileFilter = (req, file, cb) => {
if (supportedTypes.includes(file.mimetype.split('/')[1])) {
@@ -22,8 +22,8 @@ const fileFilter = (req, file, cb) => {
} else {
cb(null, false);
}
-}
+};
const upload = multer({ storage, fileFilter });
-module.exports = upload.single('icon');
\ No newline at end of file
+module.exports = upload.single('icon');