import type { ChangeSelectedAppThemeAction, DeleteAppThemeAction, FetchAppThemesAction, FetchSelectedAppThemeAction, SaveAppThemeAction, UpdateSelectedAppThemeAction, } from "actions/appThemingActions"; import { updateisBetaCardShownAction } from "actions/appThemingActions"; import type { ReduxAction } from "@appsmith/constants/ReduxActionConstants"; import { ReduxActionErrorTypes, ReduxActionTypes, } from "@appsmith/constants/ReduxActionConstants"; import ThemingApi from "api/AppThemingApi"; import { all, takeLatest, put, select } from "redux-saga/effects"; import { toast } from "design-system"; import { CHANGE_APP_THEME, createMessage, DELETE_APP_THEME, SAVE_APP_THEME, SET_DEFAULT_SELECTED_THEME, } from "@appsmith/constants/messages"; import { ENTITY_TYPE } from "entities/AppsmithConsole"; import { updateReplayEntity } from "actions/pageActions"; import { getCanvasWidgets } from "@appsmith/selectors/entitiesSelector"; import { getAppMode } from "@appsmith/selectors/applicationSelectors"; import type { APP_MODE } from "entities/App"; import { getCurrentUser } from "selectors/usersSelectors"; import type { User } from "constants/userConstants"; import { getBetaFlag, setBetaFlag, STORAGE_KEYS } from "utils/storage"; import type { UpdateWidgetPropertyPayload } from "actions/controlActions"; import { batchUpdateMultipleWidgetProperties } from "actions/controlActions"; import { getPropertiesToUpdateForReset } from "entities/AppTheming/utils"; import type { ApiResponse } from "api/ApiResponses"; import type { AppTheme } from "entities/AppTheming"; import type { CanvasWidgetsReduxState } from "reducers/entityReducers/canvasWidgetsReducer"; import { getCurrentApplicationId, selectApplicationVersion, } from "selectors/editorSelectors"; import { find } from "lodash"; import * as Sentry from "@sentry/react"; import { Severity } from "@sentry/react"; import { getAllPageIds } from "./selectors"; import type { SagaIterator } from "@redux-saga/types"; import type { AxiosPromise } from "axios"; /** * init app theming */ export function* initAppTheming() { try { const user: User = yield select(getCurrentUser); const { email } = user; if (email) { const appThemingBetaFlag: boolean = yield getBetaFlag( email, STORAGE_KEYS.APP_THEMING_BETA_SHOWN, ); yield put(updateisBetaCardShownAction(appThemingBetaFlag)); } } catch (error) {} } /** * fetches all themes of the application * * @param action */ // eslint-disable-next-line export function* fetchAppThemes(action: ReduxAction) { try { const { applicationId } = action.payload; const response: ApiResponse = yield ThemingApi.fetchThemes(applicationId); yield put({ type: ReduxActionTypes.FETCH_APP_THEMES_SUCCESS, payload: response.data, }); } catch (error) { yield put({ type: ReduxActionErrorTypes.FETCH_APP_THEMES_ERROR, payload: { error }, }); } } /** * fetches the selected theme of the application * * @param action */ export function* fetchAppSelectedTheme( // eslint-disable-next-line action: ReduxAction, ): SagaIterator | AxiosPromise { const { applicationId } = action.payload; const mode: APP_MODE = yield select(getAppMode); const pageIds = yield select(getAllPageIds); const userDetails = yield select(getCurrentUser); const applicationVersion = yield select(selectApplicationVersion); try { // eslint-disable-next-line const response: ApiResponse = yield ThemingApi.fetchSelected( applicationId, mode, ); if (response?.data) { yield put({ type: ReduxActionTypes.FETCH_SELECTED_APP_THEME_SUCCESS, payload: response.data, }); } else { Sentry.captureException("Unable to fetch the selected theme", { level: Severity.Critical, extra: { pageIds, applicationId, applicationVersion, userDetails, themeResponse: response, }, }); // If the response.data is undefined then we set selectedTheme to default Theme yield put({ type: ReduxActionTypes.SET_DEFAULT_SELECTED_THEME_INIT, }); } } catch (error) { yield put({ type: ReduxActionErrorTypes.FETCH_SELECTED_APP_THEME_ERROR, payload: { error }, }); } } /** * updates the selected theme of the application * * @param action */ export function* updateSelectedTheme( action: ReduxAction, ) { // eslint-disable-next-line const { shouldReplay = true, theme, applicationId } = action.payload; const canvasWidgets: CanvasWidgetsReduxState = yield select(getCanvasWidgets); try { yield ThemingApi.updateTheme(applicationId, theme); yield put({ type: ReduxActionTypes.UPDATE_SELECTED_APP_THEME_SUCCESS, payload: theme, }); if (shouldReplay) { yield put( updateReplayEntity( "canvas", { widgets: canvasWidgets, theme }, ENTITY_TYPE.WIDGET, ), ); } } catch (error) { yield put({ type: ReduxActionErrorTypes.UPDATE_SELECTED_APP_THEME_ERROR, payload: { error }, }); } } /** * changes eelcted theme * * @param action */ export function* changeSelectedTheme( action: ReduxAction, ) { const { applicationId, shouldReplay = true, theme } = action.payload; const canvasWidgets: CanvasWidgetsReduxState = yield select(getCanvasWidgets); try { yield ThemingApi.changeTheme(applicationId, theme); yield put({ type: ReduxActionTypes.CHANGE_SELECTED_APP_THEME_SUCCESS, payload: theme, }); // shows toast toast.show(createMessage(CHANGE_APP_THEME, theme.displayName), { kind: "success", }); if (shouldReplay) { yield put( updateReplayEntity( "canvas", { widgets: canvasWidgets, theme }, ENTITY_TYPE.WIDGET, ), ); } } catch (error) { yield put({ type: ReduxActionErrorTypes.UPDATE_SELECTED_APP_THEME_ERROR, payload: { error }, }); } } /** * save and create new theme from selected theme * * @param action */ export function* saveSelectedTheme(action: ReduxAction) { const { applicationId, name } = action.payload; try { const response: ApiResponse = yield ThemingApi.saveTheme( applicationId, { name }, ); yield put({ type: ReduxActionTypes.SAVE_APP_THEME_SUCCESS, payload: response.data, }); // shows toast toast.show(createMessage(SAVE_APP_THEME, name), { kind: "success", }); } catch (error) { yield put({ type: ReduxActionErrorTypes.SAVE_APP_THEME_ERROR, payload: { error }, }); } } /** * deletes custom saved theme * * @param action */ export function* deleteTheme(action: ReduxAction) { const { name, themeId } = action.payload; try { yield ThemingApi.deleteTheme(themeId); yield put({ type: ReduxActionTypes.DELETE_APP_THEME_SUCCESS, payload: { themeId }, }); // shows toast toast.show(createMessage(DELETE_APP_THEME, name), { kind: "success", }); } catch (error) { yield put({ type: ReduxActionErrorTypes.DELETE_APP_THEME_ERROR, payload: { error }, }); } } function* closeisBetaCardShown() { try { const user: User = yield select(getCurrentUser); const { email } = user; if (email) { yield setBetaFlag(email, STORAGE_KEYS.APP_THEMING_BETA_SHOWN, true); } } catch (error) {} } /** * resets widget styles */ function* resetTheme() { try { const canvasWidgets: CanvasWidgetsReduxState = yield select(getCanvasWidgets); const propertiesToUpdate: UpdateWidgetPropertyPayload[] = getPropertiesToUpdateForReset(canvasWidgets); if (propertiesToUpdate.length) { yield put(batchUpdateMultipleWidgetProperties(propertiesToUpdate)); } } catch (error) {} } /** * sets the selectedTheme to default theme when Selected Theme API fails */ function* setDefaultSelectedThemeOnError() { const applicationId: string = yield select(getCurrentApplicationId); try { // Fetch all system themes const response: ApiResponse = yield ThemingApi.fetchThemes(applicationId); // Gets default theme const theme = find(response.data, { name: "Default" }); if (theme) { // Update API call to set current theme to default yield ThemingApi.changeTheme(applicationId, theme); yield put({ type: ReduxActionTypes.FETCH_SELECTED_APP_THEME_SUCCESS, payload: theme, }); // shows toast toast.show(createMessage(SET_DEFAULT_SELECTED_THEME, theme.displayName), { kind: "warning", }); } } catch (error) { yield put({ type: ReduxActionErrorTypes.SET_DEFAULT_SELECTED_THEME_ERROR, payload: { error }, }); } } export default function* appThemingSaga() { yield all([takeLatest(ReduxActionTypes.INITIALIZE_EDITOR, initAppTheming)]); yield all([ takeLatest(ReduxActionTypes.FETCH_APP_THEMES_INIT, fetchAppThemes), takeLatest(ReduxActionTypes.RESET_APP_THEME_INIT, resetTheme), takeLatest( ReduxActionTypes.FETCH_SELECTED_APP_THEME_INIT, fetchAppSelectedTheme, ), takeLatest( ReduxActionTypes.UPDATE_SELECTED_APP_THEME_INIT, updateSelectedTheme, ), takeLatest( ReduxActionTypes.CHANGE_SELECTED_APP_THEME_INIT, changeSelectedTheme, ), takeLatest(ReduxActionTypes.SAVE_APP_THEME_INIT, saveSelectedTheme), takeLatest(ReduxActionTypes.DELETE_APP_THEME_INIT, deleteTheme), takeLatest(ReduxActionTypes.CLOSE_BETA_CARD_SHOWN, closeisBetaCardShown), takeLatest( ReduxActionTypes.SET_DEFAULT_SELECTED_THEME_INIT, setDefaultSelectedThemeOnError, ), ]); }