Merge pull request #12936 from ToolJet/gh-12680-toggle-app-mode

Add action to toggle app mode (light/dark)
This commit is contained in:
Johnson Cherian 2025-07-10 19:43:53 +05:30 committed by GitHub
commit f1f9a57e3d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 126 additions and 24 deletions

View file

@ -88,6 +88,7 @@ export function getSuggestionKeys(refState) {
'unsetPageVariable',
'unsetAllPageVariables',
'switchPage',
'toggleAppMode',
];
// eslint-disable-next-line no-unused-vars

View file

@ -6,7 +6,7 @@ import useAppDarkMode from '@/_hooks/useAppDarkMode';
import useStore from '@/AppBuilder/_stores/store';
import { shallow } from 'zustand/shallow';
const APP_MODES = [
export const APP_MODES = [
{ label: 'Auto', value: 'auto' },
{ label: 'Light', value: 'light' },
{ label: 'Dark', value: 'dark' },
@ -15,12 +15,7 @@ const APP_MODES = [
const AppModeToggle = ({ darkMode }) => {
const { onAppModeChange, appMode } = useAppDarkMode();
const { t } = useTranslation();
const { globalSettingsChanged } = useStore(
(state) => ({
globalSettingsChanged: state.globalSettingsChanged,
}),
shallow
);
const setResolvedGlobals = useStore((state) => state.setResolvedGlobals);
return (
@ -33,8 +28,7 @@ const AppModeToggle = ({ darkMode }) => {
if (value === 'auto') {
exposedTheme = darkMode ? 'dark' : 'light';
}
onAppModeChange({ appMode: value });
// globalSettingsChanged({ theme: { name: exposedTheme } });
onAppModeChange(value);
setResolvedGlobals('theme', { name: exposedTheme });
}}
defaultValue={appMode}

View file

@ -37,6 +37,7 @@ import ToggleGroupItem from '@/ToolJetUI/SwitchGroup/ToggleGroupItem';
import usePopoverObserver from '@/AppBuilder/_hooks/usePopoverObserver';
import SolidIcon from '@/_ui/Icon/SolidIcons';
import { components as selectComponents } from 'react-select';
import { APP_MODES } from '@/AppBuilder/LeftSidebar/GlobalSettings/AppModeToggle';
export const EventManager = ({
sourceId,
@ -75,7 +76,6 @@ export const EventManager = ({
const eventToDeleteLoaderIndex = useStore((state) => state.eventsSlice.getEventToDeleteLoaderIndex(), shallow);
const { handleYmapEventUpdates } = useContext(EditorContext) || {};
const { updateState } = useAppDataActions();
const currentEvents = allAppEvents?.filter((event) => {
@ -1044,6 +1044,29 @@ export const EventManager = ({
})}
</>
)}
{event.actionId === 'toggle-app-mode' && (
<>
<div className="row">
<div className="col-3 p-2">{t('editor.inspector.eventManager.appMode', 'App mode')}</div>
<div className="col-9" data-cy="query-selection-field">
<Select
className={`${darkMode ? 'select-search-dark' : 'select-search'} w-100`}
options={APP_MODES}
value={event?.appMode}
search={true}
onChange={(value) => {
handlerChanged(index, 'appMode', value);
}}
placeholder={t('globals.select', 'Select') + '...'}
styles={styles}
useMenuPortal={false}
useCustomStyles={true}
/>
</div>
</div>
<RunjsParameters event={event} darkMode={darkMode} index={index} handlerChanged={handlerChanged} />
</>
)}
<div className="row mt-3">
<div className="col-3 p-2">{t('editor.inspector.eventManager.debounce', 'Debounce')}</div>
<div className="col-9">

View file

@ -334,11 +334,10 @@ const useAppData = (
appData.editing_version?.homePageId || appData.editing_version?.home_page_id || appData.home_page_id;
appTypeRef.current = appData.type;
setApp(
{
appName: appData.name,
appId: appData.id,
appId: appId || appData?.appId || appData?.app_id,
slug: appData.slug,
currentAppEnvironmentId: editorEnvironment.id,
isMaintenanceOn:

View file

@ -172,6 +172,20 @@ export const createAppSlice = (set, get) => ({
console.error('Error updating page:', error);
}
},
updateAppMode: async (appMode, moduleId = 'canvas') => {
const { appStore, currentVersionId } = get();
try {
const res = await appVersionService.updateAppMode(
appStore.modules[moduleId].app.appId,
currentVersionId,
appMode
);
set((state) => ({ globalSettings: { ...state.globalSettings, appMode } }));
} catch (error) {
toast.error('App mode could not be updated.');
console.error('Error updating app mode:', error);
}
},
switchPage: (pageId, handle, queryParams = [], moduleId = 'canvas', isBackOrForward = false) => {
get().debugger.resetUnreadErrorCount();
// reset stores

View file

@ -1,9 +1,7 @@
import { appVersionService } from '@/_services';
import toast from 'react-hot-toast';
import { findAllEntityReferences } from '@/_stores/utils';
import { debounce, extractAndReplaceReferencesFromString, resolveCode, replaceEntityReferencesWithIds } from '../utils';
import { deepClone } from '@/_helpers/utilities/utils.helpers';
import { dfs } from '@/_stores/handleReferenceTransactions';
import { debounce, replaceEntityReferencesWithIds } from '../utils';
import { isQueryRunnable, isValidUUID, serializeNestedObjectToQueryParams } from '@/_helpers/utils';
import useStore from '@/AppBuilder/_stores/store';
import _ from 'lodash';
@ -874,6 +872,11 @@ export const createEventsSlice = (set, get) => ({
return Promise.reject(error);
}
}
case 'toggle-app-mode': {
const { updateAppMode } = get();
updateAppMode(event.appMode);
return Promise.resolve();
}
case 'switch-page': {
try {
const { pageId } = event;
@ -928,7 +931,14 @@ export const createEventsSlice = (set, get) => ({
}),
generateAppActions: (queryId, mode, isPreview = false, moduleId = 'canvas') => {
const { getCurrentPageComponents, dataQuery, eventsSlice, queryPanel, modules } = get();
const {
getCurrentPageComponents,
dataQuery,
eventsSlice,
queryPanel,
modules,
globalSettings: { appMode },
} = get();
const { previewQuery } = queryPanel;
const { executeAction } = eventsSlice;
const currentComponents = Object.entries(getCurrentPageComponents(moduleId));
@ -1202,6 +1212,20 @@ export const createEventsSlice = (set, get) => ({
return executeAction(event, mode, {}, moduleId);
};
const toggleAppMode = (value) => {
if (value && value !== 'light' && value !== 'dark' && value !== 'auto') {
return;
}
if (!value) {
value = appMode === 'dark' ? 'light' : 'dark';
}
const event = {
actionId: 'toggle-app-mode',
appMode: value,
};
return executeAction(event, mode, {});
};
return {
runQuery,
setVariable,
@ -1224,6 +1248,7 @@ export const createEventsSlice = (set, get) => ({
logInfo,
log,
logError,
toggleAppMode,
};
},
// Selectors

View file

@ -492,6 +492,7 @@ export function createReferencesLookup(currentState, forQueryParams = false, ini
'logInfo',
'log',
'logError',
'toggleAppMode',
];
const suggestionList = [];

View file

@ -139,4 +139,10 @@ export const ActionTypes = [
options: [{ name: 'copy-to-clipboard', type: 'text', default: '' }],
group: 'other',
},
{
name: 'Toggle app mode',
id: 'toggle-app-mode',
options: [{ name: 'appMode', type: 'text', default: '' }],
group: 'other',
},
];

View file

@ -3,10 +3,10 @@ import { shallow } from 'zustand/shallow';
import useStore from '@/AppBuilder/_stores/store';
const useAppDarkMode = () => {
const { appMode, globalSettingsChanged, isTJDarkMode } = useStore(
const { appMode, updateAppMode, isTJDarkMode } = useStore(
(state) => ({
appMode: state.globalSettings.appMode,
globalSettingsChanged: state.globalSettingsChanged,
updateAppMode: state.updateAppMode,
isTJDarkMode: state.isTJDarkMode,
}),
shallow
@ -23,7 +23,7 @@ const useAppDarkMode = () => {
}, [appMode, isTJDarkMode]);
return {
onAppModeChange: globalSettingsChanged,
onAppModeChange: updateAppMode,
appMode,
isAppDarkMode,
};

View file

@ -15,6 +15,7 @@ export const appVersionService = {
deleteAppVersionEventHandler,
clonePage,
findAllEventsWithSourceId,
updateAppMode,
cloneGroup,
};
@ -39,7 +40,9 @@ function promoteEnvironment(appId, versionId, currentEnvironmentId) {
}
function getAppVersionData(appId, versionId, mode) {
const requestOptions = { method: 'GET', headers: authHeader(), credentials: 'include' };
return fetch(`${config.apiUrl}/v2/apps/${appId}/versions/${versionId}?mode=${mode}`, requestOptions).then(handleResponse);
return fetch(`${config.apiUrl}/v2/apps/${appId}/versions/${versionId}?mode=${mode}`, requestOptions).then(
handleResponse
);
}
function create(appId, versionName, versionFromId, currentEnvironmentId) {
@ -140,12 +143,23 @@ function autoSaveApp(
credentials: 'include',
body: JSON.stringify(body),
};
const url = `${config.apiUrl}/v2/apps/${appId}/versions/${versionId}/${type ?? ''}`;
return fetch(url, requestOptions).then(handleResponse);
}
function updateAppMode(appId, versionId, appMode) {
const requestOptions = {
method: 'PUT',
headers: authHeader(),
credentials: 'include',
body: JSON.stringify({ appId, versionId, appMode }),
};
const url = `${config.apiUrl}/v2/apps/${appId}/versions/${versionId}/global_settings/app_mode`;
return fetch(url, requestOptions).then(handleResponse);
}
function saveAppVersionEventHandlers(appId, versionId, events, updateType = 'update') {
const body = {
events,

View file

@ -365,6 +365,7 @@ export class AppsService implements IAppsService {
globalSettings: { ...versionToLoad.globalSettings, theme: appTheme },
showViewerNavigation: versionToLoad.showViewerNavigation,
pageSettings: versionToLoad?.pageSettings,
appId: app.id,
};
};

View file

@ -1,4 +1,4 @@
import { Body, Controller, Get, Put, Query, UseGuards } from '@nestjs/common';
import { Body, Controller, Get, Put, Param, Query, UseGuards } from '@nestjs/common';
import { VersionService } from './service';
import { InitModule } from '@modules/app/decorators/init-module';
import { MODULES } from '@modules/app/constants/modules';
@ -21,7 +21,7 @@ import { IVersionControllerV2 } from './interfaces/IControllerV2';
version: '2',
})
export class VersionControllerV2 implements IVersionControllerV2 {
constructor(protected readonly versionService: VersionService) {}
constructor(protected readonly versionService: VersionService) { }
@InitFeature(FEATURE_KEY.GET_ONE)
@UseGuards(JwtAuthGuard, ValidAppGuard, FeatureAbilityGuard)
@ -37,6 +37,13 @@ export class VersionControllerV2 implements IVersionControllerV2 {
return this.versionService.update(app, user, appVersionUpdateDto);
}
// If we want to update app mode in public app, we use this endpoint
@InitFeature(FEATURE_KEY.UPDATE_SETTINGS)
@Put(':id/versions/:versionId/global_settings/app_mode')
updateAppMode(@Body() @Param('appMode') appMode: 'light' | 'dark' | 'auto', @Param('id') appId: string, @Param('versionId') versionId: string) {
return this.versionService.updateAppMode(appId, versionId, appMode);
}
@InitFeature(FEATURE_KEY.UPDATE_SETTINGS)
@UseGuards(JwtAuthGuard, ValidAppGuard, FeatureAbilityGuard)
@Put([':id/versions/:versionId/global_settings', ':id/versions/:versionId/page_settings'])
@ -48,6 +55,8 @@ export class VersionControllerV2 implements IVersionControllerV2 {
return this.versionService.updateSettings(app, user, appVersionUpdateDto);
}
@InitFeature(FEATURE_KEY.PROMOTE)
@UseGuards(JwtAuthGuard, ValidAppGuard, FeatureAbilityGuard)
@Put(':id/versions/:versionId/promote')

View file

@ -7,5 +7,6 @@ export interface IVersionControllerV2 {
getVersion(user: UserEntity, app: AppEntity, mode?: string): Promise<any>;
updateVersion(user: UserEntity, app: AppEntity, appVersionUpdateDto: AppVersionUpdateDto): Promise<any>;
updateGlobalSettings(user: UserEntity, app: AppEntity, appVersionUpdateDto: AppVersionUpdateDto): Promise<any>;
updateAppMode(appId: string, versionId: string, appMode: 'light' | 'dark' | 'auto'): Promise<any>;
promoteVersion(user: UserEntity, app: AppEntity, promoteVersionDto: PromoteVersionDto): Promise<any>;
}

View file

@ -203,7 +203,7 @@ export class VersionService implements IVersionService {
async updateSettings(app: App, user: User, appVersionUpdateDto: AppVersionUpdateDto) {
const appVersion = await this.versionRepository.findById(app.appVersions[0].id, app.id);
await this.versionsUtilService.updateVersion(appVersion, appVersionUpdateDto);
RequestContext.setLocals(AUDIT_LOGS_REQUEST_CONTEXT_KEY, {
@ -215,6 +215,20 @@ export class VersionService implements IVersionService {
});
return;
}
async updateAppMode(appId: string, versionId: string, appMode: 'light' | 'dark' | 'auto') {
const appVersion = await this.versionRepository.findById(versionId, appId);
const updateDto: Partial<AppVersionUpdateDto> = {
globalSettings: {
theme: {
appMode,
},
},
};
return await this.versionsUtilService.updateVersion(appVersion, updateDto as AppVersionUpdateDto);
}
promoteVersion(app: App, user: User, promoteVersionDto: PromoteVersionDto) {
return dbTransactionWrap(async (manager: EntityManager) => {