From f1211ef595cd37c469dbfacab5bf67aca760edbb Mon Sep 17 00:00:00 2001 From: arpitnath Date: Sat, 5 Aug 2023 04:12:37 +0530 Subject: [PATCH] new editor and removed all references --- frontend/src/Editor/Container.jsx | 17 +- frontend/src/Editor/Editor.jsx | 123 +- frontend/src/Editor/EditorFunc.jsx | 1337 +++++++++++++++++ frontend/src/Editor/Header/GlobalSettings.jsx | 9 +- frontend/src/Editor/Header/index.js | 34 +- .../src/Editor/Inspector/EventManager.jsx | 6 +- frontend/src/Editor/Inspector/Inspector.jsx | 2 - .../Components/QueryManagerBody.jsx | 2 - .../src/Editor/QueryManager/QueryManager.jsx | 2 - frontend/src/Editor/QueryPanel/QueryPanel.jsx | 2 - frontend/src/Editor/RealtimeEditor.jsx | 5 +- frontend/src/Editor/Viewer.jsx | 16 +- frontend/src/Editor/index.js | 1 + frontend/src/_helpers/appUtils.js | 57 +- frontend/src/_helpers/utils.js | 7 +- frontend/src/_stores/appDataStore.js | 21 + frontend/src/_stores/dataQueriesStore.js | 11 +- frontend/src/_stores/editorStore.js | 24 + 18 files changed, 1557 insertions(+), 119 deletions(-) create mode 100644 frontend/src/Editor/EditorFunc.jsx diff --git a/frontend/src/Editor/Container.jsx b/frontend/src/Editor/Container.jsx index 325970fbe2..6e573c7e4a 100644 --- a/frontend/src/Editor/Container.jsx +++ b/frontend/src/Editor/Container.jsx @@ -54,8 +54,11 @@ export const Container = ({ backgroundSize: `${gridWidth}px 10px`, }; - // eslint-disable-next-line react-hooks/exhaustive-deps - const components = appDefinition.pages[currentPageId]?.components ?? {}; + const components = useMemo( + () => appDefinition.pages[currentPageId]?.components ?? {}, + // eslint-disable-next-line react-hooks/exhaustive-deps + [appDefinition.pages[currentPageId]] + ); const currentState = useCurrentState(); const { appVersionsId, enableReleasedVersionPopupState, isVersionReleased } = useAppVersionStore( (state) => ({ @@ -87,10 +90,12 @@ export const Container = ({ useHotkeys('meta+shift+z, control+shift+z', () => handleRedo()); useHotkeys( 'meta+v, control+v', - () => { + async () => { if (isContainerFocused && !isVersionReleased) { - navigator.clipboard.readText().then((cliptext) => { + // Check if the clipboard API is available + if (navigator.clipboard && typeof navigator.clipboard.readText === 'function') { try { + const cliptext = await navigator.clipboard.readText(); addComponents( currentPageId, appDefinition, @@ -101,7 +106,9 @@ export const Container = ({ } catch (err) { console.log(err); } - }); + } else { + console.log('Clipboard API is not available in this browser.'); + } } enableReleasedVersionPopupState(); }, diff --git a/frontend/src/Editor/Editor.jsx b/frontend/src/Editor/Editor.jsx index 691f24ba0a..8ba542cc96 100644 --- a/frontend/src/Editor/Editor.jsx +++ b/frontend/src/Editor/Editor.jsx @@ -46,12 +46,13 @@ import { ReleasedVersionError } from './AppVersionsManager/ReleasedVersionError' import { useDataSourcesStore } from '@/_stores/dataSourcesStore'; import { useDataQueriesStore } from '@/_stores/dataQueriesStore'; import { useAppVersionStore } from '@/_stores/appVersionStore'; -import { useEditorStore } from '@/_stores/editorStore'; +import { useEditorState, useEditorStore } from '@/_stores/editorStore'; import { useQueryPanelStore } from '@/_stores/queryPanelStore'; import { useCurrentStateStore, useCurrentState } from '@/_stores/currentStateStore'; import { resetAllStores } from '@/_stores/utils'; import { setCookie } from '@/_helpers/cookie'; import { shallow } from 'zustand/shallow'; +import { useAppDataActions, useAppDataStore } from '@/_stores/appDataStore'; setAutoFreeze(false); enablePatches(); @@ -110,7 +111,7 @@ class EditorComponent extends React.Component { deviceWindowWidth: 450, appDefinition: this.defaultDefinition, apps: [], - queryConfirmationList: [], + // queryConfirmationList: [], isSourceSelected: false, isSaving: false, isUnsavedQueriesAvailable: false, @@ -124,6 +125,8 @@ class EditorComponent extends React.Component { this.autoSave = debounce(this.saveEditingVersion, 3000); this.realtimeSave = debounce(this.appDefinitionChanged, 500); + + this.appActions = useAppDataStore.getState().actions; } setWindowTitle(name) { @@ -145,6 +148,9 @@ class EditorComponent extends React.Component { groups: currentSession.group_permissions?.map((group) => group.group), }; this.setState({ currentUser }); + + this.appActions.updateCurrentUser(userVars); + useCurrentStateStore.getState().actions.setCurrentState({ globals: { ...this.props.currentState.globals, @@ -176,7 +182,7 @@ class EditorComponent extends React.Component { this.getCurrentOrganizationDetails(); this.autoSave(); this.fetchApps(0); - this.fetchApp(this.props.params.pageHandle); + await this.fetchApp(this.props.params.pageHandle); await this.fetchOrgEnvironmentVariables(); this.initComponentVersioning(); this.initRealtimeSave(); @@ -200,6 +206,7 @@ class EditorComponent extends React.Component { handle: this.props.pageHandle, variables: {}, }; + this.appActions.updateState({ appId: this.props.params.id }); useCurrentStateStore.getState().actions.setCurrentState({ globals, page }); } @@ -241,7 +248,7 @@ class EditorComponent extends React.Component { componentDidUpdate(prevProps, prevState) { if (!isEqual(prevState.appDefinition, this.state.appDefinition)) { - computeComponentState(this, this.state.appDefinition.pages[this.state.currentPageId]?.components); + computeComponentState(this.state.appDefinition.pages[this.state.currentPageId]?.components); } if (!isEqual(prevState.editorMarginLeft, this.state.editorMarginLeft)) { @@ -300,11 +307,8 @@ class EditorComponent extends React.Component { // eslint-disable-next-line no-unused-vars appService.setMaintenance(this.state.app.id, newState).then((data) => { - this.setState({ - app: { - ...this.state.app, - is_maintenance_on: newState, - }, + this.appActions.updateState({ + isMaintenanceOn: newState, }); if (newState) { @@ -316,11 +320,12 @@ class EditorComponent extends React.Component { }; fetchApps = (page) => { - appService.getAll(page).then((data) => + appService.getAll(page).then((data) => { + this.appActions.updateState({ apps: data.apps.map((app) => ({ id: app.id, name: app.name, slug: app.slug })) }); this.setState({ apps: data.apps, - }) - ); + }); + }); }; fetchApp = (startingPageHandle) => { @@ -333,6 +338,15 @@ class EditorComponent extends React.Component { const startingPageId = pages.filter((page) => page.handle === startingPageHandle)[0]?.id; const homePageId = startingPageId ?? dataDefinition.homePageId; + this.appActions.updateState({ + slug: data.slug, + isMaintenanceOn: data?.is_maintenance_on, + organizationId: data?.organization_id, + isPublic: data?.is_public, + appName: data?.name, + userId: data?.user_id, + }); + useCurrentStateStore.getState().actions.setCurrentState({ page: { handle: dataDefinition.pages[homePageId]?.handle, @@ -343,6 +357,7 @@ class EditorComponent extends React.Component { }); useAppVersionStore.getState().actions.updateEditingVersion(data.editing_version); useAppVersionStore.getState().actions.updateReleasedVersionId(data.current_version_id); + this.setState( { app: data, @@ -353,7 +368,7 @@ class EditorComponent extends React.Component { }, async () => { - computeComponentState(this, this.state.appDefinition.pages[homePageId]?.components ?? {}).then(async () => { + computeComponentState(this.state.appDefinition.pages[homePageId]?.components ?? {}).then(async () => { this.setWindowTitle(data.name); useEditorStore.getState().actions.setShowComments(!!queryString.parse(this.props.location.search).threadId); @@ -425,7 +440,7 @@ class EditorComponent extends React.Component { this.socket?.send( JSON.stringify({ event: 'events', - data: { message: 'dataSourcesChanged', appId: this.state.appId }, + data: { message: 'dataSourcesChanged', appId: this.state.app?.id }, }) ); } else { @@ -445,7 +460,7 @@ class EditorComponent extends React.Component { this.socket?.send( JSON.stringify({ event: 'events', - data: { message: 'dataQueriesChanged', appId: this.state.appId }, + data: { message: 'dataQueriesChanged', appId: this.state.app?.id }, }) ); } else { @@ -601,10 +616,6 @@ class EditorComponent extends React.Component { this.switchSidebarTab(2); }; - handleSlugChange = (newSlug) => { - this.setState({ slug: newSlug }); - }; - removeComponents = () => { if (!this.props.isVersionReleased && this.state?.selectedComponents?.length > 1) { let newDefinition = cloneDeep(this.state.appDefinition); @@ -695,7 +706,7 @@ class EditorComponent extends React.Component { this.handleAddPatch ); setStateAsync(_self, newDefinition).then(() => { - computeComponentState(_self, _self.state.appDefinition.pages[currentPageId].components); + computeComponentState(_self.state.appDefinition.pages[currentPageId].components); this.setState({ isSaving: true, appDefinitionLocalVersion: uuid() }); this.autoSave(); this.props.ymap?.set('appDef', { @@ -753,17 +764,36 @@ class EditorComponent extends React.Component { return; } - cloneComponents(this, this.appDefinitionChanged, false, true); + cloneComponents( + this.state.selectedComponents, + this.state.appDefinition, + this.state.currentPageId, + this.appDefinitionChanged, + false + ); }; - copyComponents = () => cloneComponents(this, this.appDefinitionChanged, false); + copyComponents = () => + cloneComponents( + this.state.selectedComponents, + this.state.appDefinition, + this.state.currentPageId, + this.appDefinitionChanged, + false + ); cloneComponents = () => { if (this.props.isVersionReleased) { useAppVersionStore.getState().actions.enableReleasedVersionPopupState(); return; } - cloneComponents(this, this.appDefinitionChanged, true); + cloneComponents( + this.state.selectedComponents, + this.state.appDefinition, + this.state.currentPageId, + this.appDefinitionChanged, + true + ); }; decimalToHex = (alpha) => (alpha === 0 ? '00' : Math.round(255 * alpha).toString(16)); @@ -828,7 +858,7 @@ class EditorComponent extends React.Component { this.socket.send( JSON.stringify({ event: 'events', - data: { message: 'versionReleased', appId: this.state.appId }, + data: { message: 'versionReleased', appId: this.state.app?.id }, }) ); } @@ -869,7 +899,7 @@ class EditorComponent extends React.Component { } else if (!isEmpty(this.props?.editingVersion)) { appVersionService .save( - this.state.appId, + this.state.app?.id, this.props.editingVersion?.id, { definition: this.state.appDefinition }, isUserSwitchedVersion @@ -900,7 +930,7 @@ class EditorComponent extends React.Component { }; handleOnComponentOptionChanged = (component, optionName, value) => { - return onComponentOptionChanged(this, component, optionName, value); + return onComponentOptionChanged(component, optionName, value); }; handleOnComponentOptionsChanged = (component, options) => { @@ -923,10 +953,10 @@ class EditorComponent extends React.Component { sideBarDebugger = { error: (data) => { - debuggerActions.error(this, data); + debuggerActions.error(data); }, flush: () => { - debuggerActions.flush(this); + debuggerActions.flush(); }, generateErrorLogs: (errors) => debuggerActions.generateErrorLogs(errors), }; @@ -1359,7 +1389,7 @@ class EditorComponent extends React.Component { const queryParamsString = queryParams.map(([key, value]) => `${key}=${value}`).join('&'); - this.props.navigate(`/${getWorkspaceId()}/apps/${this.state.appId}/${handle}?${queryParamsString}`); + this.props.navigate(`/${getWorkspaceId()}/apps/${this.state.app?.id}/${handle}?${queryParamsString}`); const { globals: existingGlobals } = this.props.currentState; @@ -1390,7 +1420,7 @@ class EditorComponent extends React.Component { }, () => { // Move this callback to zustand - computeComponentState(this, this.state.appDefinition.pages[pageId]?.components ?? {}).then(async () => { + computeComponentState(this.state.appDefinition.pages[pageId]?.components ?? {}).then(async () => { for (const event of events ?? []) { await this.handleEvent(event.eventId, event); } @@ -1452,23 +1482,22 @@ class EditorComponent extends React.Component { currentSidebarTab, selectedComponents = [], appDefinition, - appId, - slug, app, showLeftSidebar, isLoading, zoomLevel, deviceWindowWidth, - apps, - defaultComponentStateComputed, hoveredComponent, - queryConfirmationList, } = this.state; const currentState = this.props?.currentState; const editingVersion = this.props?.editingVersion; const appVersionPreviewLink = editingVersion ? `/applications/${app.id}/versions/${editingVersion.id}/${currentState.page.handle}` : ''; + const appId = app?.id; + + const { queryConfirmationList = [], defaultComponentStateComputed } = this.props; + return (
@@ -1553,7 +1578,6 @@ class EditorComponent extends React.Component { updateOnPageLoadEvents={this.updateOnPageLoadEvents} showHideViewerNavigationControls={this.showHideViewerNavigation} updateOnSortingPages={this.updateOnSortingPages} - apps={apps} setEditorMarginLeft={this.handleEditorMarginLeftChange} /> {!this.props.showComments && ( @@ -1691,12 +1715,17 @@ class EditorComponent extends React.Component { dataQueriesChanged={this.dataQueriesChanged} fetchDataQueries={this.fetchDataQueries} darkMode={this.props.darkMode} - apps={apps} allComponents={appDefinition.pages[this.state.currentPageId]?.components ?? {}} appId={appId} appDefinition={appDefinition} dataSourceModalHandler={this.dataSourceModalHandler} - editorRef={this} + editorRef={{ + appDefinition: this.state.appDefinition, + queryConfirmationList, + updateQueryConfirmationList: this.props.updateQueryConfirmationList, + navigate: this.props.navigate, + currentPageId: this.state.currentPageId, + }} />
@@ -1723,7 +1752,6 @@ class EditorComponent extends React.Component { allComponents={appDefinition.pages[this.state.currentPageId]?.components} key={selectedComponents[0].id} switchSidebarTab={this.switchSidebarTab} - apps={apps} darkMode={this.props.darkMode} appDefinitionLocalVersion={this.state.appDefinitionLocalVersion} pages={this.getPagesWithIds()} @@ -1756,13 +1784,17 @@ class EditorComponent extends React.Component { } const withStore = (Component) => (props) => { - const { showComments, currentLayout } = useEditorStore( + const { showComments, currentLayout, queryConfirmationList, defaultComponentStateComputed, actions } = useEditorStore( (state) => ({ showComments: state?.showComments, currentLayout: state?.currentLayout, + queryConfirmationList: state?.queryConfirmationList, + defaultComponentStateComputed: state?.defaultComponentStateComputed, + actions: state?.actions, }), shallow ); + const { isVersionReleased, editingVersion } = useAppVersionStore( (state) => ({ isVersionReleased: state.isVersionReleased, editingVersion: state.editingVersion }), shallow @@ -1778,6 +1810,9 @@ const withStore = (Component) => (props) => { currentState={currentState} showComments={showComments} currentLayout={currentLayout} + queryConfirmationList={queryConfirmationList} + updateQueryConfirmationList={actions.updateQueryConfirmationList} + defaultComponentStateComputed={defaultComponentStateComputed} /> ); }; diff --git a/frontend/src/Editor/EditorFunc.jsx b/frontend/src/Editor/EditorFunc.jsx new file mode 100644 index 0000000000..af72c4174a --- /dev/null +++ b/frontend/src/Editor/EditorFunc.jsx @@ -0,0 +1,1337 @@ +import React, { useEffect, useLayoutEffect, useRef, useState } from 'react'; +import { appService, authenticationService, appVersionService, orgEnvironmentVariableService } from '@/_services'; +import { DndProvider } from 'react-dnd'; +import { HTML5Backend } from 'react-dnd-html5-backend'; +import _, { defaults, cloneDeep, isEqual, isEmpty, debounce, omit, update } from 'lodash'; +import { Container } from './Container'; +import { EditorKeyHooks } from './EditorKeyHooks'; +import { CustomDragLayer } from './CustomDragLayer'; +import { LeftSidebar } from './LeftSidebar'; +import { componentTypes } from './WidgetManager/components'; +import { Inspector } from './Inspector/Inspector'; +import QueryPanel from './QueryPanel/QueryPanel'; +import { + onComponentOptionChanged, + onComponentOptionsChanged, + onEvent, + onQueryConfirmOrCancel, + runQuery, + computeComponentState, + debuggerActions, + cloneComponents, + removeSelectedComponent, +} from '@/_helpers/appUtils'; +import { Confirm } from './Viewer/Confirm'; +import { Tooltip as ReactTooltip } from 'react-tooltip'; +import CommentNotifications from './CommentNotifications'; +import { WidgetManager } from './WidgetManager'; +import config from 'config'; +import queryString from 'query-string'; +import { toast } from 'react-hot-toast'; +const { produce, enablePatches, setAutoFreeze, applyPatches } = require('immer'); +import { createWebsocketConnection } from '@/_helpers/websocketConnection'; +import RealtimeCursors from '@/Editor/RealtimeCursors'; +import { initEditorWalkThrough } from '@/_helpers/createWalkThrough'; +import { EditorContextWrapper } from './Context/EditorContextWrapper'; +import Selecto from 'react-selecto'; +import { withTranslation } from 'react-i18next'; +import { v4 as uuid } from 'uuid'; +import Skeleton from 'react-loading-skeleton'; +import EditorHeader from './Header'; +import { getWorkspaceId } from '@/_helpers/utils'; +import '@/_styles/editor/react-select-search.scss'; +import { withRouter } from '@/_hoc/withRouter'; +import { ReleasedVersionError } from './AppVersionsManager/ReleasedVersionError'; +import { useDataSourcesStore } from '@/_stores/dataSourcesStore'; +import { useDataQueries, useDataQueriesStore } from '@/_stores/dataQueriesStore'; +import { useAppVersionStore } from '@/_stores/appVersionStore'; +import { useQueryPanelStore } from '@/_stores/queryPanelStore'; +import { useCurrentStateStore, useCurrentState } from '@/_stores/currentStateStore'; +import { resetAllStores } from '@/_stores/utils'; +import { setCookie } from '@/_helpers/cookie'; +import { shallow } from 'zustand/shallow'; +import { useEditorActions, useEditorState, useEditorStore } from '@/_stores/editorStore'; +import { useAppDataActions, useAppDataStore, useAppInfo } from '@/_stores/appDataStore'; +import { useMounted } from '@/_hooks/use-mount'; + +setAutoFreeze(false); +enablePatches(); + +function setWindowTitle(name) { + document.title = name ? `${name} - Tooljet` : `My App - Tooljet`; +} + +const decimalToHex = (alpha) => (alpha === 0 ? '00' : Math.round(255 * alpha).toString(16)); + +const defaultDefinition = (darkMode) => { + const defaultPageId = uuid(); + return { + showViewerNavigation: true, + homePageId: defaultPageId, + pages: { + [defaultPageId]: { + components: {}, + handle: 'home', + name: 'Home', + }, + }, + globalSettings: { + hideHeader: false, + appInMaintenance: false, + canvasMaxWidth: 1292, + canvasMaxWidthType: 'px', + canvasMaxHeight: 2400, + canvasBackgroundColor: darkMode ? '#2f3c4c' : '#edeff5', + backgroundFxQuery: '', + }, + }; +}; + +const EditorComponent = (props) => { + const { socket } = createWebsocketConnection(props?.params?.id); + const mounted = useMounted(); + + const { updateState } = useAppDataActions(); + const { updateEditorState, updateQueryConfirmationList } = useEditorActions(); + const { + noOfVersionsSupported, + appDefinition, + selectedComponents, + currentLayout, + canUndo, + canRedo, + isSaving, + saveError, + scrollOptions, + currentSidebarTab, + isLoading, + defaultComponentStateComputed, + currentVersion, + showLeftSidebar, + queryConfirmationList, + } = useEditorState(); + + const dataQueries = useDataQueries(); + + const { isMaintenanceOn, appId, app, currentUser, currentVersionId } = useAppInfo(); + + const [currentVersionChanges, setCurrentVersionChanges] = useState({}); + const [currentPageId, setCurrentPageId] = useState(null); + const [zoomLevel, setZoomLevel] = useState(1); + const [isQueryPaneDragging, setIsQueryPaneDragging] = useState(false); + const [isQueryPaneExpanded, setIsQueryPaneExpanded] = useState(false); + const [selectionInProgress, setSelectionInProgress] = useState(false); + const [hoveredComponent, setHoveredComponent] = useState(null); + const [editorMarginLeft, setEditorMarginLeft] = useState(0); + + const [isDragging, setIsDragging] = useState(false); + + // refs + const canvasContainerRef = useRef(null); + const dataSourceModalRef = useRef(null); + const selectionDragRef = useRef(null); + const selectionRef = useRef(null); + + useLayoutEffect(() => { + resetAllStores(); + }, []); + + useEffect(() => { + updateState({ isLoading: true }); + // 1. Get the current session and current user from the authentication service + const currentSession = authenticationService.currentSessionValue; + const currentUser = currentSession?.current_user; + + // 2. Subscribe to changes in the current session using RxJS observable pattern + const subscription = authenticationService.currentSession.subscribe((currentSession) => { + // 3. Check if the current user and group permissions are available + if (currentUser && currentSession?.group_permissions) { + // 4. Prepare user details in a format suitable for the application + const userVars = { + email: currentUser.email, + firstName: currentUser.first_name, + lastName: currentUser.last_name, + groups: currentSession.group_permissions?.map((group) => group.group), + }; + + const appUserDetails = { + ...currentUser, + current_organization_id: currentSession.current_organization_id, + }; + + updateState({ + currentUser: appUserDetails, + }); + + useCurrentStateStore.getState().actions.setCurrentState({ + globals: { + ...props.currentState.globals, + currentUser: userVars, + }, + }); + } + }); + + $componentDidMount(); + + // 6. Unsubscribe from the observable when the component is unmounted + return () => { + document.title = 'Tooljet - Dashboard'; + socket && socket?.close(); + subscription.unsubscribe(); + if (config.ENABLE_MULTIPLAYER_EDITING) props?.provider?.disconnect(); + useEditorStore.getState().actions.setIsEditorActive(false); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + // Ref to store the previous appDefinition for comparison + const prevAppDefinition = useRef(appDefinition); + + useEffect(() => { + if (currentUser?.current_organization_id) { + fetchGlobalDataSources(); + } + }, [JSON.stringify(currentUser?.current_organization_id)]); + + // Handle appDefinition updates + useEffect(() => { + const didAppDefinitionChanged = !_.isEqual(appDefinition, prevAppDefinition.current); + + console.log('---arpit-- appDefinitionChanged', { dataQueries, didAppDefinitionChanged, appDefinition }); + if (mounted && didAppDefinitionChanged && currentPageId) { + console.log('---arpit-- appDefinitionChanged [mounted=true]', { appDefinition }); + const components = appDefinition?.pages[currentPageId]?.components || {}; + + computeComponentState(components); + + if (isSaving) { + autoSave(); + } + } + prevAppDefinition.current = appDefinition; + }, [JSON.stringify({ appDefinition, currentPageId, dataQueries })]); + + const editorRef = { + appDefinition: appDefinition, + queryConfirmationList: queryConfirmationList, + updateQueryConfirmationList: updateQueryConfirmationList, + navigate: props.navigate, + }; + + const handleMessage = (event) => { + const { data } = event; + + if (data?.type === 'redirectTo') { + const redirectCookie = data?.payload['redirectPath']; + setCookie('redirectPath', redirectCookie, 1); + } + }; + + const fetchApps = async (page) => { + const { apps } = await appService.getAll(page); + + updateState({ apps: apps.map((app) => ({ id: app.id, name: app.name, slug: app.slug })) }); + }; + + const fetchOrgEnvironmentVariables = () => { + orgEnvironmentVariableService.getVariables().then((data) => { + const client_variables = {}; + const server_variables = {}; + data.variables.map((variable) => { + if (variable.variable_type === 'server') { + server_variables[variable.variable_name] = 'HiddenEnvironmentVariable'; + } else { + client_variables[variable.variable_name] = variable.value; + } + }); + + useCurrentStateStore.getState().actions.setCurrentState({ + server: server_variables, + client: client_variables, + }); + }); + }; + + // 1. When we receive an undoable action – we can always undo but cannot redo anymore. + // 2. Whenever you perform an undo – you can always redo and keep doing undo as long as we have a patch for it. + // 3. Whenever you redo – you can always undo and keep doing redo as long as we have a patch for it. + const initComponentVersioning = () => { + const currentVersion = { + [currentPageId]: -1, + }; + setCurrentVersionChanges({}); + updateEditorState({ + canUndo: false, + canRedo: false, + currentVersion, + }); + }; + + /** + * When a new update is received over-the-websocket connection + * the useEffect in Container.jsx is triggered, but already appDef had been updated + * to avoid ymap observe going into a infinite loop a check is added where if the + * current appDef is equal to the newAppDef then we do not trigger a realtimeSave + */ + const initRealtimeSave = () => { + if (!config.ENABLE_MULTIPLAYER_EDITING) return null; + + props.ymap?.observe(() => { + if (!isEqual(props.editingVersion?.id, props.ymap?.get('appDef').editingVersionId)) return; + if (isEqual(appDefinition, props.ymap?.get('appDef').newDefinition)) return; + + realtimeSave(props.ymap?.get('appDef').newDefinition, { skipAutoSave: true, skipYmapUpdate: true }); + }); + }; + + const initEventListeners = () => { + socket?.addEventListener('message', (event) => { + const data = event.data.replace(/^"(.+(?="$))"$/, '$1'); + if (data === 'versionReleased') fetchApp(); + else if (data === 'dataQueriesChanged') { + fetchDataQueries(props.editingVersion?.id); + } else if (data === 'dataSourcesChanged') { + fetchDataSources(props.editingVersion?.id); + } + }); + }; + + const $componentDidMount = async () => { + console.log('---arpit-- componentDidMounted effect', { appDefinition, currentPageId }); + window.addEventListener('message', handleMessage); + // autoSave(); + + await fetchApps(0); + await fetchApp(props.params.pageHandle); + await fetchOrgEnvironmentVariables(); + initComponentVersioning(); + initRealtimeSave(); + initEventListeners(); + updateEditorState({ + currentSidebarTab: 2, + selectedComponents: [], + scrollOptions: { + container: canvasContainerRef.current, + throttleTime: 30, + threshold: 0, + }, + }); + + const globals = { + ...props.currentState.globals, + theme: { name: props?.darkMode ? 'dark' : 'light' }, + urlparams: JSON.parse(JSON.stringify(queryString.parse(props.location.search))), + }; + const page = { + ...props?.currentState?.page, + handle: props?.pageHandle, + variables: {}, + }; + updateState({ appId: props?.params?.id }); + useCurrentStateStore.getState().actions.setCurrentState({ globals, page }); + }; + + const fetchDataQueries = async (id, selectFirstQuery = false, runQueriesOnAppLoad = false) => { + await useDataQueriesStore.getState().actions.fetchDataQueries(id, selectFirstQuery, runQueriesOnAppLoad, editorRef); + }; + + const fetchDataSources = (id) => { + useDataSourcesStore.getState().actions.fetchDataSources(id); + }; + + const fetchGlobalDataSources = () => { + const { current_organization_id: organizationId } = currentUser; + useDataSourcesStore.getState().actions.fetchGlobalDataSources(organizationId); + }; + + const onVersionDelete = () => { + fetchApp(props.params.pageHandle); + }; + + const toggleAppMaintenance = () => { + const newState = !isMaintenanceOn; + + appService.setMaintenance(appId, newState).then(() => { + updateState({ + isMaintenanceOn: newState, + }); + + if (newState) { + toast.success('Application is on maintenance.'); + } else { + toast.success('Application maintenance is completed'); + } + }); + }; + + const dataSourcesChanged = () => { + if (socket instanceof WebSocket && socket?.readyState === WebSocket.OPEN) { + socket?.send( + JSON.stringify({ + event: 'events', + data: { message: 'dataSourcesChanged', appId: appId }, + }) + ); + } else { + fetchDataSources(props.editingVersion?.id); + } + }; + + const globalDataSourcesChanged = () => { + fetchGlobalDataSources(); + }; + + const dataQueriesChanged = () => { + if (socket instanceof WebSocket && socket?.readyState === WebSocket.OPEN) { + socket?.send( + JSON.stringify({ + event: 'events', + data: { message: 'dataQueriesChanged', appId: appId }, + }) + ); + } else { + fetchDataQueries(props.editingVersion?.id); + } + }; + + const switchSidebarTab = (tabIndex) => { + updateEditorState({ + currentSidebarTab: tabIndex, + }); + }; + + const handleInspectorView = () => { + switchSidebarTab(2); + }; + + const onNameChanged = (newName) => { + updateState({ appName: newName }); + setWindowTitle(newName); + }; + + const onZoomChanged = (zoom) => { + setZoomLevel(zoom); + }; + + const [canvasWidth, setCanvasWidth] = useState(1092); + + const getCanvasWidth = () => { + const canvasBoundingRect = document.getElementsByClassName('canvas-area')[0]?.getBoundingClientRect(); + + const _canvasWidth = canvasBoundingRect?.width; + + if (_canvasWidth) { + setCanvasWidth(_canvasWidth); + } + }; + + const computeCanvasContainerHeight = () => { + // 45 = (height of header) + // 85 = (the height of the query panel header when minimised) + (height of header) + return `calc(${100}% - ${Math.max(useQueryPanelStore.getState().queryPanelHeight + 45, 85)}px)`; + }; + + const handleQueryPaneDragging = (bool) => setIsQueryPaneDragging(bool); + const handleQueryPaneExpanding = (bool) => setIsQueryPaneExpanded(bool); + + //! Needs attention + const handleOnComponentOptionChanged = (component, optionName, value) => { + return onComponentOptionChanged(component, optionName, value); + }; + + const handleOnComponentOptionsChanged = (component, options) => { + return onComponentOptionsChanged(component, options); + }; + + const handleComponentClick = (id, component) => { + updateEditorState({ + selectedComponent: { id, component }, + }); + switchSidebarTab(1); + }; + + const handleComponentHover = (id) => { + if (selectionInProgress) return; + setHoveredComponent(id); + }; + + const sideBarDebugger = { + error: (data) => { + debuggerActions.error(data); + }, + flush: () => { + debuggerActions.flush(); + }, + generateErrorLogs: (errors) => debuggerActions.generateErrorLogs(errors), + }; + + const changeDarkMode = (newMode) => { + useCurrentStateStore.getState().actions.setCurrentState({ + globals: { + ...props.currentState.globals, + theme: { name: newMode ? 'dark' : 'light' }, + }, + }); + props.switchDarkMode(newMode); + }; + + //! Needs attention + const handleEvent = (eventName, options) => onEvent(editorRef, eventName, options, 'edit'); + + const handleRunQuery = (queryId, queryName) => runQuery(editorRef, queryId, queryName); + + const dataSourceModalHandler = () => { + dataSourceModalRef.current.dataSourceModalToggleStateHandler(); + }; + + const onAreaSelectionStart = (e) => { + const isMultiSelect = e.inputEvent.shiftKey || selectedComponents.length > 0; + setSelectionInProgress(true); + const prevSelectedComponents = [...selectedComponents]; + updateEditorState({ + selectedComponents: [...(isMultiSelect ? prevSelectedComponents : [])], + }); + }; + + const onAreaSelection = (e) => { + e.added.forEach((el) => { + el.classList.add('resizer-select'); + }); + if (selectionInProgress) { + e.removed.forEach((el) => { + el.classList.remove('resizer-select'); + }); + } + }; + + const onAreaSelectionEnd = (e) => { + setSelectionInProgress(false); + e.selected.forEach((el, index) => { + const id = el.getAttribute('widgetid'); + const component = appDefinition?.pages[currentPageId].components[id].component; + const isMultiSelect = e.inputEvent.shiftKey || (!e.isClick && index != 0); + setSelectedComponent(id, component, isMultiSelect); + }); + }; + + const setSelectedComponent = (id, component, multiSelect = false) => { + if (selectedComponents.length === 0 || !multiSelect) { + switchSidebarTab(1); + } else { + switchSidebarTab(2); + } + + const isAlreadySelected = selectedComponents.find((component) => component.id === id); + + if (!isAlreadySelected) { + const prevSelectedComponents = [...selectedComponents]; + updateEditorState({ + selectedComponents: [...(multiSelect ? prevSelectedComponents : []), { id, component }], + }); + } + }; + + const onVersionRelease = (versionId) => { + useAppVersionStore.getState().actions.updateReleasedVersionId(versionId); + + updateState({ + currentVersionId: versionId, + }); + + socket.send( + JSON.stringify({ + event: 'events', + data: { message: 'versionReleased', appId: appId }, + }) + ); + }; + + const computeCanvasBackgroundColor = () => { + //!Global settings needs to be out + const { canvasBackgroundColor } = appDefinition?.globalSettings ?? '#edeff5'; + if (['#2f3c4c', '#edeff5'].includes(canvasBackgroundColor)) { + return props.darkMode ? '#2f3c4c' : '#edeff5'; + } + return canvasBackgroundColor; + }; + + const onAreaSelectionDragStart = (e) => { + if (e.inputEvent.target.getAttribute('id') !== 'real-canvas') { + selectionDragRef.current = true; + } else { + selectionDragRef.current = false; + } + }; + + const onAreaSelectionDrag = (e) => { + if (selectionDragRef.current) { + e.stop(); + selectionInProgress && setSelectionInProgress(false); + } + }; + + const onAreaSelectionDragEnd = () => { + selectionDragRef.current = false; + selectionInProgress && setSelectionInProgress(false); + }; + + const getPagesWithIds = () => { + //! Needs attention + return Object.entries(appDefinition?.pages).map(([id, page]) => ({ ...page, id })); + }; + + const handleEditorMarginLeftChange = (value) => setEditorMarginLeft(value); + + const globalSettingsChanged = (key, value) => { + if (value?.[1]?.a == undefined) appDefinition.globalSettings[key] = value; + else { + const hexCode = `${value?.[0]}${decimalToHex(value?.[1]?.a)}`; + appDefinition.globalSettings[key] = hexCode; + } + + updateEditorState({ + isSaving: true, + appDefinition, + }); + + props.ymap?.set('appDef', { + newDefinition: appDefinition, + editingVersionId: props.editingVersion?.id, + }); + // autoSave(); + }; + + //!-------- + const fetchApp = async (startingPageHandle) => { + const _appId = props?.params?.id; + + const callBack = async (data) => { + useAppVersionStore.getState().actions.updateEditingVersion(data.editing_version); + useAppVersionStore.getState().actions.updateReleasedVersionId(data.current_version_id); + await fetchDataSources(data.editing_version?.id); + await fetchDataQueries(data.editing_version?.id, true, true); + + // let dataDefinition = defaults(data.definition, defaultDefinition(props.darkMode)); + let dataDefinition = data.definition ?? defaults(data.definition, defaultDefinition(props.darkMode)); + console.log('---arpit-- fetching app data', { startingPageHandle, data, dataDefinition }); + + const pages = Object.entries(dataDefinition.pages).map(([pageId, page]) => ({ id: pageId, ...page })); + const startingPageId = pages.filter((page) => page.handle === startingPageHandle)[0]?.id; + const homePageId = startingPageId || dataDefinition.homePageId; + + setCurrentPageId(homePageId); + + updateState({ + app: data, + slug: data.slug, + isMaintenanceOn: data?.is_maintenance_on, + organizationId: data?.organization_id, + isPublic: data?.is_public, + appName: data?.name, + userId: data?.user_id, + appId: data?.id, + }); + + useCurrentStateStore.getState().actions.setCurrentState({ + page: { + handle: dataDefinition.pages[homePageId]?.handle, + name: dataDefinition.pages[homePageId]?.name, + id: homePageId, + variables: {}, + }, + }); + + updateEditorState({ + isLoading: false, + appDefinition: dataDefinition, + }); + + appDefinitionChanged(dataDefinition, { + skipAutoSave: true, + skipYmapUpdate: true, + }); + + for (const event of dataDefinition.pages[homePageId]?.events ?? []) { + await handleEvent(event.eventId, event); + } + getCanvasWidth(); + }; + + updateState({ + isLoading: true, + }); + + await appService + .getApp(_appId) + .then(callBack) + .finally(() => initEditorWalkThrough()); + }; + + // !-------- + const setAppDefinitionFromVersion = (version, shouldWeEditVersion = true) => { + if (version?.id !== props.editingVersion?.id) { + appDefinitionChanged(defaults(version.definition, defaultDefinition(props.darkMode)), { + skipAutoSave: true, + skipYmapUpdate: true, + versionChanged: true, + }); + if (version?.id === currentVersionId) { + updateEditorState({ + canUndo: false, + canRedo: false, + }); + } + useAppVersionStore.getState().actions.updateEditingVersion(version); + + updateEditorState({ + isSaving: false, + }); + + shouldWeEditVersion && saveEditingVersion(true); + fetchDataSources(props.editingVersion?.id); + fetchDataQueries(props.editingVersion?.id, true); + initComponentVersioning(); + } + }; + + const appDefinitionChanged = (newDefinition, opts = {}) => { + console.log('--arpit | appDefinitionChanged func()'); + if (_.isEqual(appDefinition, newDefinition)) return; + if (config.ENABLE_MULTIPLAYER_EDITING && !opts.skipYmapUpdate) { + props.ymap?.set('appDef', { + newDefinition, + editingVersionId: props.editingVersion?.id, + }); + } + + if (opts?.versionChanged) { + setCurrentPageId(newDefinition.homePageId); + + return new Promise((resolve) => { + updateEditorState({ + isSaving: true, + appDefinition: newDefinition, + // appDefinitionLocalVersion: uuid(), + }); + + // if (!opts.skipAutoSave) autoSave(); + + resolve(); + }); + } + + // Create a new copy of appDefinition with lodash's cloneDeep + const updatedAppDefinition = _.cloneDeep(appDefinition); + const currentPageComponents = newDefinition.pages[currentPageId]?.components; + + if (!updatedAppDefinition.pages[currentPageId]) { + updatedAppDefinition.pages[currentPageId] = {}; // Create a new object for the page if it doesn't exist + } + + updatedAppDefinition.pages[currentPageId].components = currentPageComponents || {}; + + // Call handleAddPatch with the list of changes (if needed) + handleAddPatch(); + + // Update the editor state with the new appDefinition + updateEditorState({ + isSaving: true, + appDefinition: updatedAppDefinition, + // appDefinitionLocalVersion: uuid(), + }); + + // if (!opts.skipAutoSave) autoSave(); + }; + + const saveEditingVersion = (isUserSwitchedVersion = false) => { + if (props.isVersionReleased && !isUserSwitchedVersion) { + updateEditorState({ + isSaving: false, + }); + } else if (!isEmpty(props?.editingVersion)) { + appVersionService + .save(appId, props.editingVersion?.id, { definition: appDefinition }, isUserSwitchedVersion) + .then(() => { + const _editingVersion = { + ...props.editingVersion, + ...{ definition: appDefinition }, + }; + useAppVersionStore.getState().actions.updateEditingVersion(_editingVersion); + updateEditorState({ + saveError: false, + isSaving: false, + }); + }) + .catch(() => { + updateEditorState({ + saveError: true, + isSaving: false, + }); + toast.error('App could not save.'); + }); + } + + updateEditorState({ + saveError: false, + isSaving: false, + }); + }; + + const realtimeSave = debounce(appDefinitionChanged, 500); + const autoSave = debounce(saveEditingVersion, 3000); + + const handleAddPatch = (patches, inversePatches) => { + if (isEmpty(patches) && isEmpty(inversePatches)) return; + if (isEqual(patches, inversePatches)) return; + + const currentPage = currentPageId; + const _currentVersion = currentVersion[currentPage] ?? -1; + + let _currentVersionChanges = {}; + + _currentVersionChanges[currentPage] = currentVersionChanges[currentPage] ?? {}; + + _currentVersionChanges[currentPage][_currentVersion] = { + redo: patches, + undo: inversePatches, + }; + + setCurrentVersionChanges(_currentVersionChanges); + + const _canUndo = _currentVersionChanges[currentPage].hasOwnProperty(_currentVersion); + const _canRedo = _currentVersionChanges[currentPage].hasOwnProperty(_currentVersion + 1); + + _currentVersion[currentPage] = currentVersion + 1; + + delete _currentVersionChanges[currentPage][_currentVersion + 1]; + delete _currentVersionChanges[currentPage][_currentVersion - noOfVersionsSupported]; + + setCurrentVersionChanges(_currentVersionChanges); + + updateEditorState({ + canUndo: _canUndo, + canRedo: _canRedo, + currentVersion: _currentVersion, + }); + }; + + const handleUndo = () => { + if (canUndo) { + let _currentVersion = currentVersion[currentPageId]; + + const _appDefinition = applyPatches(appDefinition, currentVersionChanges[currentPageId][currentVersion - 1].undo); + + const _canUndo = currentVersionChanges[currentPageId].hasOwnProperty(currentVersion - 1); + const _canRedo = true; + _currentVersion[currentPageId] = _currentVersion - 1; + + if (!_appDefinition) return; + + updateEditorState({ + appDefinition: _appDefinition, + canUndo: _canUndo, + canRedo: _canRedo, + currentVersion: _currentVersion, + isSaving: true, + }); + + props.ymap?.set('appDef', { + newDefinition: appDefinition, + editingVersionId: props.editingVersion?.id, + }); + + // autoSave(); + } + }; + + const handleRedo = () => { + if (canRedo) { + let _currentVersion = currentVersion[currentPageId]; + + const _appDefinition = applyPatches(appDefinition, currentVersionChanges[currentPageId][currentVersion].redo); + + const _canUndo = true; + const _canRedo = currentVersionChanges[currentPageId].hasOwnProperty(currentVersion + 1); + _currentVersion[currentPageId] = currentVersion + 1; + + if (!_appDefinition) return; + + updateEditorState({ + appDefinition: _appDefinition, + canUndo: _canUndo, + canRedo: _canRedo, + currentVersion: _currentVersion, + isSaving: true, + }); + + props.ymap?.set('appDef', { + newDefinition: appDefinition, + editingVersionId: props.editingVersion?.id, + }); + + // autoSave(); + } + }; + + const componentDefinitionChanged = (componentDefinition, props) => { + console.log('---arpit [componentDefinitionChanged]', { props, componentDefinition }); + if (props?.isVersionReleased) { + useAppVersionStore.getState().actions.enableReleasedVersionPopupState(); + return; + } + + if (appDefinition?.pages[currentPageId]?.components[componentDefinition.id]) { + // Create a new copy of appDefinition with lodash's cloneDeep + const updatedAppDefinition = _.cloneDeep(appDefinition); + + // Update the component definition in the copy + updatedAppDefinition.pages[currentPageId].components[componentDefinition.id].component = + componentDefinition.component; + + // Call handleAddPatch with the list of changes (if needed) + handleAddPatch(); + + // Update the editor state with the new appDefinition + updateEditorState({ + isSaving: true, + appDefinition: updatedAppDefinition, + // appDefinitionLocalVersion: uuid() + }); + + // Other actions can be performed here if needed, like autoSave, ymap, etc. + computeComponentState(updatedAppDefinition.pages[currentPageId]?.components); + // autoSave(); + props.ymap?.set('appDef', { + newDefinition: updatedAppDefinition, + editingVersionId: props.editingVersion?.id, + }); + } + }; + + const removeComponent = (component) => { + if (!props.isVersionReleased) { + let newDefinition = cloneDeep(appDefinition); + // Delete child components when parent is deleted + + let childComponents = []; + + if (newDefinition.pages[currentPageId].components?.[component.id].component.component === 'Tabs') { + childComponents = Object.keys(newDefinition.pages[currentPageId].components).filter((key) => + newDefinition.pages[currentPageId].components[key].parent?.startsWith(component.id) + ); + } else { + childComponents = Object.keys(newDefinition.pages[currentPageId].components).filter( + (key) => newDefinition.pages[currentPageId].components[key].parent === component.id + ); + } + + childComponents.forEach((componentId) => { + delete newDefinition.pages[currentPageId].components[componentId]; + }); + + delete newDefinition.pages[currentPageId].components[component.id]; + const platform = navigator?.userAgentData?.platform || navigator?.platform || 'unknown'; + if (platform.toLowerCase().indexOf('mac') > -1) { + toast('Component deleted! (⌘ + Z to undo)', { + icon: '🗑️', + }); + } else { + toast('Component deleted! (ctrl + Z to undo)', { + icon: '🗑️', + }); + } + appDefinitionChanged(newDefinition, { + skipAutoSave: props.isVersionReleased, + }); + handleInspectorView(); + } else { + useAppVersionStore.getState().actions.enableReleasedVersionPopupState(); + } + }; + + const moveComponents = (direction) => { + const gridWidth = (1 * 100) / 43; // width of the canvas grid in percentage + const _appDefinition = _.cloneDeep(appDefinition); + let newComponents = _appDefinition?.pages[currentPageId].components; + + for (const selectedComponent of selectedComponents) { + let top = newComponents[selectedComponent.id].layouts[props.currentLayout].top; + let left = newComponents[selectedComponent.id].layouts[props.currentLayout].left; + + switch (direction) { + case 'ArrowLeft': + left = left - gridWidth; + break; + case 'ArrowRight': + left = left + gridWidth; + break; + case 'ArrowDown': + top = top + 10; + break; + case 'ArrowUp': + top = top - 10; + break; + } + + newComponents[selectedComponent.id].layouts[props.currentLayout].top = top; + newComponents[selectedComponent.id].layouts[props.currentLayout].left = left; + } + + _appDefinition.pages[currentPageId].components = newComponents; + + appDefinitionChanged(_appDefinition); + }; + + const copyComponents = () => + cloneComponents(selectedComponents, appDefinition, currentPageId, appDefinitionChanged, false); + + const cutComponents = () => { + if (props.isVersionReleased) { + useAppVersionStore.getState().actions.enableReleasedVersionPopupState(); + + return; + } + + cloneComponents(selectedComponents, appDefinition, currentPageId, appDefinitionChanged, false); + }; + + const handleEditorEscapeKeyPress = () => { + if (selectedComponents?.length > 0) { + updateEditorState({ + selectedComponents: [], + }); + handleInspectorView(); + } + }; + + const removeComponents = () => { + if (!props.isVersionReleased && selectedComponents?.length > 1) { + let newDefinition = cloneDeep(appDefinition); + + removeSelectedComponent(currentPageId, newDefinition, selectedComponents); + const platform = navigator?.userAgentData?.platform || navigator?.platform || 'unknown'; + if (platform.toLowerCase().indexOf('mac') > -1) { + toast('Selected components deleted! (⌘ + Z to undo)', { + icon: '🗑️', + }); + } else { + toast('Selected components deleted! (ctrl + Z to undo)', { + icon: '🗑️', + }); + } + appDefinitionChanged(newDefinition, { + skipAutoSave: props.isVersionReleased, + }); + handleInspectorView(); + } else if (props.isVersionReleased) { + useAppVersionStore.getState().actions.enableReleasedVersionPopupState(); + } + }; + + // !------- + + const currentState = props?.currentState; + const editingVersion = props?.editingVersion; + const appVersionPreviewLink = editingVersion + ? `/applications/${app.id}/versions/${editingVersion.id}/${currentState.page.handle}` + : ''; + const deviceWindowWidth = 450; + + if (!appDefinition?.homePageId) { + return ( +
+
+
{/* */}
+
Loading...
+
+
+ ); + } + + return ( +
+ {props.isVersionReleased && } + + + +
+ handleRunQuery(queryId, queryName)} + ref={dataSourceModalRef} + isSaving={isSaving} + currentPageId={currentPageId} + // addNewPage={addNewPage} + // switchPage={switchPage} + // deletePage={deletePageRequest} + // renamePage={renamePage} + // clonePage={clonePage} + // hidePage={hidePage} + // unHidePage={unHidePage} + // updateHomePage={updateHomePage} + // updatePageHandle={updatePageHandle} + // updateOnPageLoadEvents={updateOnPageLoadEvents} + // showHideViewerNavigationControls={showHideViewerNavigation} + // updateOnSortingPages={updateOnSortingPages} + setEditorMarginLeft={handleEditorMarginLeftChange} + /> + {!props.showComments && ( + { + canvasContainerRef.current.scrollBy(e.direction[0] * 10, e.direction[1] * 10); + }} + /> + )} +
+
{ + if (['real-canvas', 'modal'].includes(e.target.className)) { + updateEditorState({ + currentSidebarTab: 2, + selectedComponents: [], + }); + setHoveredComponent(null); + } + }} + ref={canvasContainerRef} + onScroll={() => { + selectionRef.current.checkScroll(); + }} + > +
+
+ {config.ENABLE_MULTIPLAYER_EDITING && ( + + )} + {isLoading && ( +
+
+
+
+
+
+ +
+ {Array.from(Array(4)).map((_item, index) => ( + + ))} +
+ +
+ + +
+
+
+
+
+ )} + {defaultComponentStateComputed && ( + <> + + setIsDragging(isDragging)} + /> + + )} +
+
+
+ + +
+
+ + + {currentSidebarTab === 1 && ( +
+ {selectedComponents.length === 1 && + !isEmpty(appDefinition?.pages[currentPageId]?.components) && + !isEmpty(appDefinition?.pages[currentPageId]?.components[selectedComponents[0].id]) ? ( + + ) : ( +
+ {props.t('editor.inspectComponent', 'Please select a component to inspect')} +
+ )} +
+ )} + + {currentSidebarTab === 2 && ( + + )} +
+ {config.COMMENT_FEATURE_ENABLE && props.showComments && ( + + )} +
+
+
+
+ ); +}; + +const withStore = (Component) => (props) => { + const { showComments, currentLayout } = useEditorStore( + (state) => ({ + showComments: state?.showComments, + currentLayout: state?.currentLayout, + }), + shallow + ); + const { isVersionReleased, editingVersion } = useAppVersionStore( + (state) => ({ isVersionReleased: state.isVersionReleased, editingVersion: state.editingVersion }), + shallow + ); + + const currentState = useCurrentState(); + + return ( + + ); +}; + +export const EditorFunc = withTranslation()(withRouter(withStore(EditorComponent))); diff --git a/frontend/src/Editor/Header/GlobalSettings.jsx b/frontend/src/Editor/Header/GlobalSettings.jsx index f0d62fd1fc..9b169ea7dc 100644 --- a/frontend/src/Editor/Header/GlobalSettings.jsx +++ b/frontend/src/Editor/Header/GlobalSettings.jsx @@ -19,11 +19,10 @@ export const GlobalSettings = ({ globalSettingsChanged, darkMode, toggleAppMaintenance, - is_maintenance_on, + isMaintenanceOn, }) => { const { t } = useTranslation(); - const { hideHeader, canvasMaxWidth, canvasMaxWidthType, canvasMaxHeight, canvasBackgroundColor, backgroundFxQuery } = - globalSettings; + const { hideHeader, canvasMaxWidth, canvasMaxWidthType, canvasBackgroundColor, backgroundFxQuery } = globalSettings; const [showPicker, setShowPicker] = React.useState(false); const currentState = useCurrentState(); const [forceCodeBox, setForceCodeBox] = React.useState(true); @@ -87,7 +86,7 @@ export const GlobalSettings = ({ data-cy={`toggle-maintenance-mode`} className="form-check-input" type="checkbox" - checked={is_maintenance_on} + checked={isMaintenanceOn} onChange={() => setConfirmationShow(true)} />
@@ -237,7 +236,7 @@ export const GlobalSettings = ({ { + updateState({ slug: newSlug }); + }; + const { isVersionReleased, editingVersion } = useAppVersionStore( (state) => ({ isVersionReleased: state.isVersionReleased, @@ -87,9 +93,9 @@ export default function EditorHeader({ globalSettings={appDefinition.globalSettings} darkMode={darkMode} toggleAppMaintenance={toggleAppMaintenance} - is_maintenance_on={is_maintenance_on} + isMaintenanceOn={isMaintenanceOn} /> - +
@@ -167,14 +173,12 @@ export default function EditorHeader({
- {app.id && ( - - )} +
diff --git a/frontend/src/Editor/Inspector/EventManager.jsx b/frontend/src/Editor/Inspector/EventManager.jsx index 3cdf997c16..8e787d16be 100644 --- a/frontend/src/Editor/Inspector/EventManager.jsx +++ b/frontend/src/Editor/Inspector/EventManager.jsx @@ -15,13 +15,13 @@ import { useTranslation } from 'react-i18next'; import { useDataQueries } from '@/_stores/dataQueriesStore'; import RunjsParameters from './ActionConfigurationPanels/RunjsParamters'; +import { useAppInfo } from '@/_stores/appDataStore'; export const EventManager = ({ component, componentMeta, components, eventsChanged, - apps, excludeEvents, popOverCallback, popoverPlacement, @@ -29,6 +29,8 @@ export const EventManager = ({ hideEmptyEventsAlert, }) => { const dataQueries = useDataQueries(); + const { apps, appId } = useAppInfo(); + const [events, setEvents] = useState(() => component.component.definition.events || []); const [focusedEventIndex, setFocusedEventIndex] = useState(null); const { t } = useTranslation(); @@ -170,7 +172,7 @@ export const EventManager = ({ function getAllApps() { let appsOptionsList = []; apps - .filter((item) => item.slug !== undefined) + .filter((item) => item.slug !== undefined && item.id !== appId) .forEach((item) => { appsOptionsList.push({ name: item.name, diff --git a/frontend/src/Editor/Inspector/Inspector.jsx b/frontend/src/Editor/Inspector/Inspector.jsx index f349abe3a7..e5eb84f818 100644 --- a/frontend/src/Editor/Inspector/Inspector.jsx +++ b/frontend/src/Editor/Inspector/Inspector.jsx @@ -28,7 +28,6 @@ export const Inspector = ({ selectedComponentId, componentDefinitionChanged, allComponents, - apps, darkMode, switchSidebarTab, removeComponent, @@ -285,7 +284,6 @@ export const Inspector = ({ currentState={currentState} darkMode={darkMode} eventsChanged={eventsChanged} - apps={apps} pages={pages} allComponents={allComponents} /> diff --git a/frontend/src/Editor/QueryManager/Components/QueryManagerBody.jsx b/frontend/src/Editor/QueryManager/Components/QueryManagerBody.jsx index 4aed281625..67eedb47dd 100644 --- a/frontend/src/Editor/QueryManager/Components/QueryManagerBody.jsx +++ b/frontend/src/Editor/QueryManager/Components/QueryManagerBody.jsx @@ -39,7 +39,6 @@ export const QueryManagerBody = forwardRef( previewLoading, queryPreviewData, allComponents, - apps, appDefinition, createDraftQuery, setOptions, @@ -309,7 +308,6 @@ export const QueryManagerBody = forwardRef( componentMeta={queryComponent.componentMeta} dataQueries={dataQueries} components={allComponents} - apps={apps} popoverPlacement="top" pages={ appDefinition?.pages ? Object.entries(appDefinition?.pages).map(([id, page]) => ({ ...page, id })) : [] diff --git a/frontend/src/Editor/QueryManager/QueryManager.jsx b/frontend/src/Editor/QueryManager/QueryManager.jsx index 957b472a9d..1ca04a92fa 100644 --- a/frontend/src/Editor/QueryManager/QueryManager.jsx +++ b/frontend/src/Editor/QueryManager/QueryManager.jsx @@ -22,7 +22,6 @@ const QueryManager = ({ dataQueriesChanged, appId, darkMode, - apps, allComponents, dataSourceModalHandler, appDefinition, @@ -105,7 +104,6 @@ const QueryManager = ({ previewLoading={previewLoading} queryPreviewData={queryPreviewData} allComponents={allComponents} - apps={apps} appDefinition={appDefinition} createDraftQuery={createDraftQuery} setOptions={setOptions} diff --git a/frontend/src/Editor/QueryPanel/QueryPanel.jsx b/frontend/src/Editor/QueryPanel/QueryPanel.jsx index 6dd633fca7..3508d01f47 100644 --- a/frontend/src/Editor/QueryPanel/QueryPanel.jsx +++ b/frontend/src/Editor/QueryPanel/QueryPanel.jsx @@ -13,7 +13,6 @@ const QueryPanel = ({ dataQueriesChanged, fetchDataQueries, darkMode, - apps, allComponents, appId, appDefinition, @@ -268,7 +267,6 @@ const QueryPanel = ({ dataQueriesChanged={updateDataQueries} appId={appId} darkMode={darkMode} - apps={apps} allComponents={allComponents} dataSourceModalHandler={dataSourceModalHandler} appDefinition={appDefinition} diff --git a/frontend/src/Editor/RealtimeEditor.jsx b/frontend/src/Editor/RealtimeEditor.jsx index 605af7ba7f..41779f4d32 100644 --- a/frontend/src/Editor/RealtimeEditor.jsx +++ b/frontend/src/Editor/RealtimeEditor.jsx @@ -3,7 +3,8 @@ import React from 'react'; import config from 'config'; import { RoomProvider } from '@y-presence/react'; import Spinner from '@/_ui/Spinner'; -import { Editor } from '@/Editor'; +import { Editor, EditorFunc } from '@/Editor'; + import useRouter from '@/_hooks/use-router'; import { useParams } from 'react-router-dom'; const Y = require('yjs'); @@ -70,7 +71,7 @@ export const RealtimeEditor = (props) => { return ( - + ); }; diff --git a/frontend/src/Editor/Viewer.jsx b/frontend/src/Editor/Viewer.jsx index d50821e8f6..ceb35437b0 100644 --- a/frontend/src/Editor/Viewer.jsx +++ b/frontend/src/Editor/Viewer.jsx @@ -168,7 +168,7 @@ class ViewerComponent extends React.Component { homepage: this.state.appDefinition?.pages?.[this.state.appDefinition?.homePageId]?.handle, }, () => { - computeComponentState(this, data?.definition?.pages[currentPage.id]?.components).then(async () => { + computeComponentState(data?.definition?.pages[currentPage.id]?.components).then(async () => { this.setState({ initialComputationOfStateDone: true }); console.log('Default component state computed and set'); this.runQueries(data.data_queries); @@ -407,15 +407,13 @@ class ViewerComponent extends React.Component { name: targetPage.name, }, async () => { - computeComponentState(this, this.state.appDefinition?.pages[this.state.currentPageId].components).then( - async () => { - // eslint-disable-next-line no-unsafe-optional-chaining - const { events } = this.state.appDefinition?.pages[this.state.currentPageId]; - for (const event of events ?? []) { - await this.handleEvent(event.eventId, event); - } + computeComponentState(this.state.appDefinition?.pages[this.state.currentPageId].components).then(async () => { + // eslint-disable-next-line no-unsafe-optional-chaining + const { events } = this.state.appDefinition?.pages[this.state.currentPageId]; + for (const event of events ?? []) { + await this.handleEvent(event.eventId, event); } - ); + }); } ); } diff --git a/frontend/src/Editor/index.js b/frontend/src/Editor/index.js index 8e539b2b32..a5bdf8c189 100644 --- a/frontend/src/Editor/index.js +++ b/frontend/src/Editor/index.js @@ -2,3 +2,4 @@ export * from './Editor'; export * from './Viewer'; export * from './DataSourceManager'; +export * from './EditorFunc'; diff --git a/frontend/src/_helpers/appUtils.js b/frontend/src/_helpers/appUtils.js index 2733e47b66..7e981a29c7 100644 --- a/frontend/src/_helpers/appUtils.js +++ b/frontend/src/_helpers/appUtils.js @@ -31,6 +31,7 @@ import { useDataQueriesStore } from '@/_stores/dataQueriesStore'; import { useQueryPanelStore } from '@/_stores/queryPanelStore'; import { useCurrentStateStore, getCurrentState } from '@/_stores/currentStateStore'; import { useAppVersionStore } from '@/_stores/appVersionStore'; +import { useEditorStore } from '@/_stores/editorStore'; const ERROR_TYPES = Object.freeze({ ReferenceError: 'ReferenceError', @@ -58,7 +59,7 @@ export function setCurrentStateAsync(_ref, changes) { }); } -export function onComponentOptionsChanged(_ref, component, options) { +export function onComponentOptionsChanged(component, options) { const componentName = component.name; const components = getCurrentState().components; let componentData = components[componentName]; @@ -74,7 +75,7 @@ export function onComponentOptionsChanged(_ref, component, options) { return Promise.resolve(); } -export function onComponentOptionChanged(_ref, component, option_name, value) { +export function onComponentOptionChanged(component, option_name, value) { const componentName = component.name; const components = getCurrentState().components; @@ -351,7 +352,7 @@ function showModal(_ref, modal, show) { return Promise.resolve(); } - const modalMeta = _ref.state.appDefinition.pages[_ref.state.currentPageId].components[modalId]; + const modalMeta = _ref.appDefinition.pages[_ref.state.currentPageId].components[modalId]; //! NeedToFix const _components = { ...getCurrentState().components, @@ -366,7 +367,7 @@ function showModal(_ref, modal, show) { return Promise.resolve(); } -function logoutAction(_ref) { +function logoutAction() { localStorage.clear(); authenticationService.logout(true); @@ -431,7 +432,7 @@ function executeActionWithDebounce(_ref, event, mode, customVariables) { return runQuery(_ref, queryId, name, undefined, mode, resolvedParams); } case 'logout': { - return logoutAction(_ref); + return logoutAction(); } case 'open-webpage': { @@ -511,7 +512,7 @@ function executeActionWithDebounce(_ref, event, mode, customVariables) { } case 'set-table-page': { - setTablePageIndex(_ref, event.table, event.pageIndex); + setTablePageIndex(event.table, event.pageIndex); break; } @@ -574,7 +575,7 @@ function executeActionWithDebounce(_ref, event, mode, customVariables) { } case 'switch-page': { - _ref.switchPage(event.pageId, resolveReferences(event.queryParams, getCurrentState(), [], customVariables)); + _ref.switchPage(event.pageId, resolveReferences(event.queryParams, getCurrentState(), [], customVariables)); // arpit [switchPage] return Promise.resolve(); } } @@ -600,7 +601,7 @@ export async function onEvent(_ref, eventName, options, mode = 'edit') { }, }, }); - runQuery(_ref, queryId, queryName, true, mode, parameters); + runQuery(_ref, queryId, queryName, true, mode, parameters); //arpit [runQuery] } if (eventName === 'onCalendarEventSelect') { @@ -737,9 +738,9 @@ export async function onEvent(_ref, eventName, options, mode = 'edit') { } if (eventName === 'onBulkUpdate') { - onComponentOptionChanged(_self, options.component, 'isSavingChanges', true); + onComponentOptionChanged(options.component, 'isSavingChanges', true); await executeActionsForEventId(_self, eventName, options.component, mode, customVariables); - onComponentOptionChanged(_self, options.component, 'isSavingChanges', false); + onComponentOptionChanged(options.component, 'isSavingChanges', false); } if (['onDataQuerySuccess', 'onDataQueryFailure'].includes(eventName)) { @@ -1105,7 +1106,7 @@ export function runQuery(_ref, queryId, queryName, confirmed = undefined, mode = }); } -export function setTablePageIndex(_ref, tableId, index) { +export function setTablePageIndex(tableId, index) { if (_.isEmpty(tableId)) { console.log('No table is associated with this event.'); return Promise.resolve(); @@ -1126,7 +1127,7 @@ export function renderTooltip({ props, text }) { ); } -export function computeComponentState(_ref, components = {}) { +export function computeComponentState(components = {}) { let componentState = {}; const currentComponents = getCurrentState().components; Object.keys(components).forEach((key) => { @@ -1168,8 +1169,11 @@ export function computeComponentState(_ref, components = {}) { }, }); - return setStateAsync(_ref, { - defaultComponentStateComputed: true, + return new Promise((resolve) => { + useEditorStore.getState().actions.updateEditorState({ + defaultComponentStateComputed: true, + }); + resolve(); }); } @@ -1186,13 +1190,13 @@ export const getSvgIcon = (key, height = 50, width = 50, iconFile = undefined, s }; export const debuggerActions = { - error: (_self, errors) => { + error: (errors) => { useCurrentStateStore.getState().actions.setErrors({ ...errors, }); }, - flush: (_self) => { + flush: () => { useCurrentStateStore.getState().actions.setCurrentState({ errors: {}, }); @@ -1312,9 +1316,16 @@ const updateNewComponents = (pageId, appDefinition, newComponents, updateAppDefi updateAppDefinition(newAppDefinition); }; -export const cloneComponents = (_ref, updateAppDefinition, isCloning = true, isCut = false) => { - const { selectedComponents, appDefinition, currentPageId } = _ref.state; +export const cloneComponents = ( + selectedComponents, + appDefinition, + currentPageId, + updateAppDefinition, + isCloning = true, + isCut = false +) => { if (selectedComponents.length < 1) return getSelectedText(); + const { components: allComponents } = appDefinition.pages[currentPageId]; let newDefinition = _.cloneDeep(appDefinition); let newComponents = [], @@ -1351,7 +1362,13 @@ export const cloneComponents = (_ref, updateAppDefinition, isCloning = true, isC navigator.clipboard.writeText(JSON.stringify(newComponentObj)); toast.success('Component copied succesfully'); } - _ref.setState({ currentSidebarTab: 2 }); + + return new Promise((resolve) => { + useEditorStore.getState().actions.updateEditorState({ + currentSidebarTab: 2, + }); + resolve(); + }); }; const getChildComponents = (allComponents, component, parentComponent, addedComponentId) => { @@ -1606,7 +1623,7 @@ export const runQueries = (queries, _ref) => { }); }; -export const computeQueryState = (queries, _ref) => { +export const computeQueryState = (queries) => { let queryState = {}; queries.forEach((query) => { if (query.plugin?.plugin_id) { diff --git a/frontend/src/_helpers/utils.js b/frontend/src/_helpers/utils.js index 51d7344b43..12431a4e91 100644 --- a/frontend/src/_helpers/utils.js +++ b/frontend/src/_helpers/utils.js @@ -544,10 +544,11 @@ export const hightlightMentionedUserInComment = (comment) => { }; export const generateAppActions = (_ref, queryId, mode, isPreview = false) => { - const currentPageId = _ref.state.currentPageId; - const currentComponents = _ref.state?.appDefinition?.pages[currentPageId]?.components - ? Object.entries(_ref.state.appDefinition.pages[currentPageId]?.components) + const currentPageId = _ref.currentPageId; + const currentComponents = _ref.appDefinition?.pages[currentPageId]?.components + ? Object.entries(_ref.appDefinition.pages[currentPageId]?.components) : {}; + const runQuery = (queryName = '', parameters) => { const query = useDataQueriesStore.getState().dataQueries.find((query) => query.name === queryName); diff --git a/frontend/src/_stores/appDataStore.js b/frontend/src/_stores/appDataStore.js index 7c6ccd612c..aac92a9be1 100644 --- a/frontend/src/_stores/appDataStore.js +++ b/frontend/src/_stores/appDataStore.js @@ -1,7 +1,23 @@ +import { shallow } from 'zustand/shallow'; import { create, zustandDevTools } from './utils'; const initialState = { editingVersion: null, + currentUser: null, + apps: [], + appId: null, + appName: null, + slug: null, + isPublic: null, + isMaintenanceOn: null, + organizationId: null, + currentVersionId: null, + userId: null, + app: {}, + components: [], + pages: [], + layouts: [], + eventHandlers: [], }; export const useAppDataStore = create( @@ -10,6 +26,8 @@ export const useAppDataStore = create( ...initialState, actions: { updateEditingVersion: (version) => set(() => ({ editingVersion: version })), + updateApps: (apps) => set(() => ({ apps: apps })), + updateState: (state) => set((prev) => ({ ...prev, ...state })), }, }), { name: 'App Data Store' } @@ -18,3 +36,6 @@ export const useAppDataStore = create( export const useEditingVersion = () => useAppDataStore((state) => state.editingVersion); export const useUpdateEditingVersion = () => useAppDataStore((state) => state.actions); +export const useCurrentUser = () => useAppDataStore((state) => state.currentUser); +export const useAppInfo = () => useAppDataStore((state) => state); +export const useAppDataActions = () => useAppDataStore((state) => state.actions); diff --git a/frontend/src/_stores/dataQueriesStore.js b/frontend/src/_stores/dataQueriesStore.js index bcfcfaadab..369f311573 100644 --- a/frontend/src/_stores/dataQueriesStore.js +++ b/frontend/src/_stores/dataQueriesStore.js @@ -29,7 +29,7 @@ export const useDataQueriesStore = create( // Runs query on loading application if (runQueriesOnAppLoad) runQueries(data.data_queries, editorRef); // Compute query state to be added in the current state - computeQueryState(data.data_queries, editorRef); + computeQueryState(data.data_queries); const { actions, selectedQuery } = useQueryPanelStore.getState(); if (selectFirstQuery || selectedQuery?.id === 'draftQuery') { actions.setSelectedQuery(data.data_queries[0]?.id, data.data_queries[0]); @@ -40,7 +40,7 @@ export const useDataQueriesStore = create( }); }, setDataQueries: (dataQueries) => set({ dataQueries }), - deleteDataQueries: (queryId, editorRef) => { + deleteDataQueries: (queryId) => { set({ isDeletingQueryInProcess: true }); dataqueryService .del(queryId) @@ -57,8 +57,7 @@ export const useDataQueriesStore = create( get().actions.fetchDataQueries( useAppVersionStore.getState().editingVersion?.id, selectedQuery?.id === queryId, - false, - editorRef + false ); }) .catch(({ error }) => { @@ -122,12 +121,12 @@ export const useDataQueriesStore = create( }); }); }, - renameQuery: (id, newName, editorRef) => { + renameQuery: (id, newName) => { dataqueryService .update(id, newName) .then(() => { toast.success('Query Name Updated'); - get().actions.fetchDataQueries(useAppVersionStore.getState().editingVersion?.id, false, false, editorRef); + get().actions.fetchDataQueries(useAppVersionStore.getState().editingVersion?.id, false, false); }) .catch(({ error }) => { toast.error(error); diff --git a/frontend/src/_stores/editorStore.js b/frontend/src/_stores/editorStore.js index bb4b83ba8c..87515e1617 100644 --- a/frontend/src/_stores/editorStore.js +++ b/frontend/src/_stores/editorStore.js @@ -4,6 +4,25 @@ const initialState = { currentLayout: 'desktop', showComments: false, isEditorActive: false, + currentSidebarTab: 2, + selectedComponents: [], + selectedComponent: null, + scrollOptions: { + container: null, + throttleTime: 0, + threshold: 0, + }, + canUndo: false, + canRedo: false, + currentVersion: {}, + noOfVersionsSupported: 100, + appDefinition: {}, + isSaving: false, + saveError: false, + isLoading: true, + defaultComponentStateComputed: false, + showLeftSidebar: true, + queryConfirmationList: [], }; export const useEditorStore = create( @@ -15,8 +34,13 @@ export const useEditorStore = create( toggleComments: () => set({ showComments: !get().showComments }), toggleCurrentLayout: (currentLayout) => set({ currentLayout }), setIsEditorActive: (isEditorActive) => set(() => ({ isEditorActive })), + updateEditorState: (state) => set((prev) => ({ ...prev, ...state })), + updateQueryConfirmationList: (queryConfirmationList) => set({ queryConfirmationList }), }, }), { name: 'Editor Store' } ) ); + +export const useEditorActions = () => useEditorStore((state) => state.actions); +export const useEditorState = () => useEditorStore((state) => state);