Functionality to delete and edit custom themes

This commit is contained in:
Paweł Malak 2022-03-25 12:13:19 +01:00
parent ad92de141b
commit 668edb03d3
10 changed files with 157 additions and 12 deletions

View file

@ -1,3 +1,6 @@
### v2.3.0 (TBA)
- Added custom theme editor ([#246](https://github.com/pawelmalak/flame/issues/246))
### v2.2.2 (2022-03-21) ### v2.2.2 (2022-03-21)
- Added option to get user location directly from the app ([#287](https://github.com/pawelmalak/flame/issues/287)) - Added option to get user location directly from the app ([#287](https://github.com/pawelmalak/flame/issues/287))
- Fixed bug with local search not working when using prefix ([#289](https://github.com/pawelmalak/flame/issues/289)) - Fixed bug with local search not working when using prefix ([#289](https://github.com/pawelmalak/flame/issues/289))

View file

@ -11,7 +11,7 @@ Flame is self-hosted startpage for your server. Its design is inspired (heavily)
- 📌 Pin your favourite items to the homescreen for quick and easy access - 📌 Pin your favourite items to the homescreen for quick and easy access
- 🔍 Integrated search bar with local filtering, 11 web search providers and ability to add your own - 🔍 Integrated search bar with local filtering, 11 web search providers and ability to add your own
- 🔑 Authentication system to protect your settings, apps and bookmarks - 🔑 Authentication system to protect your settings, apps and bookmarks
- 🔨 Dozens of options to customize Flame interface to your needs, including support for custom CSS and 15 built-in color themes - 🔨 Dozens of options to customize Flame interface to your needs, including support for custom CSS, 15 built-in color themes and custom theme builder
- ☀️ Weather widget with current temperature, cloud coverage and animated weather status - ☀️ Weather widget with current temperature, cloud coverage and animated weather status
- 🐳 Docker integration to automatically pick and add apps based on their labels - 🐳 Docker integration to automatically pick and add apps based on their labels

View file

@ -1,7 +1,9 @@
import { useState } from 'react'; import { useState, useEffect } from 'react';
// Redux // Redux
import { useSelector } from 'react-redux'; import { useSelector, useDispatch } from 'react-redux';
import { bindActionCreators } from 'redux';
import { actionCreators } from '../../../../store';
import { State } from '../../../../store/reducers'; import { State } from '../../../../store/reducers';
// Other // Other
@ -21,11 +23,21 @@ interface Props {
export const ThemeBuilder = ({ themes }: Props): JSX.Element => { export const ThemeBuilder = ({ themes }: Props): JSX.Element => {
const { const {
auth: { isAuthenticated }, auth: { isAuthenticated },
theme: { themeInEdit },
} = useSelector((state: State) => state); } = useSelector((state: State) => state);
const { editTheme } = bindActionCreators(actionCreators, useDispatch());
const [showModal, toggleShowModal] = useState(false); const [showModal, toggleShowModal] = useState(false);
const [isInEdit, toggleIsInEdit] = useState(false); const [isInEdit, toggleIsInEdit] = useState(false);
useEffect(() => {
if (themeInEdit) {
toggleIsInEdit(false);
toggleShowModal(true);
}
}, [themeInEdit]);
return ( return (
<div className={classes.ThemeBuilder}> <div className={classes.ThemeBuilder}>
{/* MODALS */} {/* MODALS */}
@ -45,6 +57,7 @@ export const ThemeBuilder = ({ themes }: Props): JSX.Element => {
<div className={classes.Buttons}> <div className={classes.Buttons}>
<Button <Button
click={() => { click={() => {
editTheme(null);
toggleIsInEdit(false); toggleIsInEdit(false);
toggleShowModal(!showModal); toggleShowModal(!showModal);
}} }}

View file

@ -19,10 +19,13 @@ interface Props {
export const ThemeCreator = ({ modalHandler }: Props): JSX.Element => { export const ThemeCreator = ({ modalHandler }: Props): JSX.Element => {
const { const {
theme: { activeTheme }, theme: { activeTheme, themeInEdit },
} = useSelector((state: State) => state); } = useSelector((state: State) => state);
const { addTheme } = bindActionCreators(actionCreators, useDispatch()); const { addTheme, updateTheme } = bindActionCreators(
actionCreators,
useDispatch()
);
const [formData, setFormData] = useState<Theme>({ const [formData, setFormData] = useState<Theme>({
name: '', name: '',
@ -38,6 +41,12 @@ export const ThemeCreator = ({ modalHandler }: Props): JSX.Element => {
setFormData({ ...formData, colors: activeTheme.colors }); setFormData({ ...formData, colors: activeTheme.colors });
}, [activeTheme]); }, [activeTheme]);
useEffect(() => {
if (themeInEdit) {
setFormData(themeInEdit);
}
}, [themeInEdit]);
const inputChangeHandler = (e: ChangeEvent<HTMLInputElement>) => { const inputChangeHandler = (e: ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target; const { name, value } = e.target;
@ -62,8 +71,11 @@ export const ThemeCreator = ({ modalHandler }: Props): JSX.Element => {
const formHandler = (e: FormEvent) => { const formHandler = (e: FormEvent) => {
e.preventDefault(); e.preventDefault();
// add new theme if (!themeInEdit) {
addTheme(formData); addTheme(formData);
} else {
updateTheme(formData);
}
// close modal // close modal
modalHandler(); modalHandler();
@ -125,7 +137,11 @@ export const ThemeCreator = ({ modalHandler }: Props): JSX.Element => {
</InputGroup> </InputGroup>
</div> </div>
{!themeInEdit ? (
<Button>Add theme</Button> <Button>Add theme</Button>
) : (
<Button>Update theme</Button>
)}
</ModalForm> </ModalForm>
); );
}; };

View file

@ -19,9 +19,15 @@ export const ThemeEditor = (props: Props): JSX.Element => {
theme: { userThemes }, theme: { userThemes },
} = useSelector((state: State) => state); } = useSelector((state: State) => state);
const { deleteTheme } = bindActionCreators(actionCreators, useDispatch()); const { deleteTheme, editTheme } = bindActionCreators(
actionCreators,
useDispatch()
);
const updateHandler = (theme: Theme) => {}; const updateHandler = (theme: Theme) => {
props.modalHandler();
editTheme(theme);
};
const deleteHandler = (theme: Theme) => { const deleteHandler = (theme: Theme) => {
if (window.confirm(`Are you sure you want to delete this theme?`)) { if (window.confirm(`Are you sure you want to delete this theme?`)) {

View file

@ -1,8 +1,11 @@
import { Dispatch } from 'redux'; import { Dispatch } from 'redux';
import { import {
AddThemeAction, AddThemeAction,
DeleteThemeAction,
EditThemeAction,
FetchThemesAction, FetchThemesAction,
SetThemeAction, SetThemeAction,
UpdateThemeAction,
} from '../actions/theme'; } from '../actions/theme';
import { ActionType } from '../action-types'; import { ActionType } from '../action-types';
import { Theme, ApiResponse, ThemeColors } from '../../interfaces'; import { Theme, ApiResponse, ThemeColors } from '../../interfaces';
@ -71,3 +74,54 @@ export const addTheme =
}); });
} }
}; };
export const deleteTheme =
(name: string) => async (dispatch: Dispatch<DeleteThemeAction>) => {
try {
const res = await axios.delete<ApiResponse<Theme[]>>(
`/api/themes/${name}`,
{ headers: applyAuth() }
);
dispatch({
type: ActionType.deleteTheme,
payload: res.data.data,
});
dispatch<any>({
type: ActionType.createNotification,
payload: {
title: 'Success',
message: 'Theme deleted',
},
});
} catch (err) {
console.log(err);
}
};
export const editTheme =
(theme: Theme | null) => (dispatch: Dispatch<EditThemeAction>) => {
dispatch({
type: ActionType.editTheme,
payload: theme,
});
};
export const updateTheme =
(theme: Theme) => async (dispatch: Dispatch<UpdateThemeAction>) => {
try {
const res = await axios.put<ApiResponse<Theme[]>>(
`/api/themes/${theme.name}`,
theme,
{ headers: applyAuth() }
);
dispatch({
type: ActionType.updateTheme,
payload: res.data.data,
});
} catch (err) {
console.log(err);
}
};

View file

@ -3,6 +3,9 @@ export enum ActionType {
setTheme = 'SET_THEME', setTheme = 'SET_THEME',
fetchThemes = 'FETCH_THEMES', fetchThemes = 'FETCH_THEMES',
addTheme = 'ADD_THEME', addTheme = 'ADD_THEME',
deleteTheme = 'DELETE_THEME',
updateTheme = 'UPDATE_THEME',
editTheme = 'EDIT_THEME',
// CONFIG // CONFIG
getConfig = 'GET_CONFIG', getConfig = 'GET_CONFIG',
updateConfig = 'UPDATE_CONFIG', updateConfig = 'UPDATE_CONFIG',

View file

@ -1,6 +1,13 @@
import { App } from '../../interfaces'; import { App } from '../../interfaces';
import { SetThemeAction } from './theme'; import {
AddThemeAction,
DeleteThemeAction,
EditThemeAction,
FetchThemesAction,
SetThemeAction,
UpdateThemeAction,
} from './theme';
import { import {
AddQueryAction, AddQueryAction,
@ -54,6 +61,11 @@ import {
export type Action = export type Action =
// Theme // Theme
| SetThemeAction | SetThemeAction
| FetchThemesAction
| AddThemeAction
| DeleteThemeAction
| UpdateThemeAction
| EditThemeAction
// Config // Config
| GetConfigAction | GetConfigAction
| UpdateConfigAction | UpdateConfigAction

View file

@ -15,3 +15,18 @@ export interface AddThemeAction {
type: ActionType.addTheme; type: ActionType.addTheme;
payload: Theme; payload: Theme;
} }
export interface DeleteThemeAction {
type: ActionType.deleteTheme;
payload: Theme[];
}
export interface UpdateThemeAction {
type: ActionType.updateTheme;
payload: Theme[];
}
export interface EditThemeAction {
type: ActionType.editTheme;
payload: Theme | null;
}

View file

@ -1,12 +1,13 @@
import { Action } from '../actions'; import { Action } from '../actions';
import { ActionType } from '../action-types'; import { ActionType } from '../action-types';
import { Theme, ThemeColors } from '../../interfaces/Theme'; import { Theme } from '../../interfaces/Theme';
import { arrayPartition, parsePABToTheme } from '../../utility'; import { arrayPartition, parsePABToTheme } from '../../utility';
interface ThemeState { interface ThemeState {
activeTheme: Theme; activeTheme: Theme;
themes: Theme[]; themes: Theme[];
userThemes: Theme[]; userThemes: Theme[];
themeInEdit: Theme | null;
} }
const savedTheme = localStorage.theme const savedTheme = localStorage.theme
@ -23,6 +24,7 @@ const initialState: ThemeState = {
}, },
themes: [], themes: [],
userThemes: [], userThemes: [],
themeInEdit: null,
}; };
export const themeReducer = ( export const themeReducer = (
@ -60,6 +62,27 @@ export const themeReducer = (
}; };
} }
case ActionType.deleteTheme: {
return {
...state,
userThemes: action.payload,
};
}
case ActionType.editTheme: {
return {
...state,
themeInEdit: action.payload,
};
}
case ActionType.updateTheme: {
return {
...state,
userThemes: action.payload,
};
}
default: default:
return state; return state;
} }