From 1699146f799b27da4b40798d407c1e63c37807ee Mon Sep 17 00:00:00 2001 From: unknown Date: Fri, 6 Aug 2021 15:15:54 +0200 Subject: [PATCH] Added support for custom SVG icons --- CHANGELOG.md | 3 + client/.env | 2 +- client/package-lock.json | 13 + client/package.json | 1 + client/src/App.tsx | 5 +- .../Apps/AppCard/AppCard.module.css | 6 +- .../src/components/Apps/AppCard/AppCard.tsx | 42 ++- .../src/components/Apps/AppForm/AppForm.tsx | 128 +++---- .../BookmarkCard/BookmarkCard.module.css | 12 +- .../Bookmarks/BookmarkCard/BookmarkCard.tsx | 55 ++- .../Bookmarks/BookmarkForm/BookmarkForm.tsx | 329 +++++++++--------- controllers/apps.js | 12 +- db.js | 10 +- middleware/multer.js | 8 +- 14 files changed, 356 insertions(+), 270 deletions(-) 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 = ( + {`${props.app.name} + ); + } else if (/.(svg)$/i.test(icon)) { + iconEl = ( +
+ +
+ ); + } else { + iconEl = ; + } + return ( { rel='noreferrer' className={classes.AppCard} > -
- {(/.(jpeg|jpg|png)$/i).test(props.app.icon) - ? {`${props.app.name} - : - } -
+
{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 = ( +
+ {`${name} +
+ ); + } else if (/.(svg)$/i.test(icon)) { + iconEl = ( +
+ +
+ ); + } else { + iconEl = ( +
+ +
+ ); + } + return ( - {bookmark.icon && ( -
- {(/.(jpeg|jpg|png)$/i).test(bookmark.icon) - ? {`${bookmark.name} - : - } -
- )} + 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');