diff --git a/frontend/src/Editor/Editor.jsx b/frontend/src/Editor/Editor.jsx index 49e6f4a6f1..7b0119b3b1 100644 --- a/frontend/src/Editor/Editor.jsx +++ b/frontend/src/Editor/Editor.jsx @@ -1,14 +1,15 @@ -import React from 'react'; +import React, { useEffect, useLayoutEffect, useRef, useState } from 'react'; import { - appsService, + appService, authenticationService, appVersionService, orgEnvironmentVariableService, + appEnvironmentService, orgEnvironmentConstantService, } from '@/_services'; import { DndProvider } from 'react-dnd'; import { HTML5Backend } from 'react-dnd-html5-backend'; -import _, { defaults, cloneDeep, isEqual, isEmpty, debounce, omit } from 'lodash'; +import _, { cloneDeep, isEqual, isEmpty, debounce, omit } from 'lodash'; import { Container } from './Container'; import { EditorKeyHooks } from './EditorKeyHooks'; import { CustomDragLayer } from './CustomDragLayer'; @@ -22,12 +23,12 @@ import { onEvent, onQueryConfirmOrCancel, runQuery, - setStateAsync, computeComponentState, debuggerActions, cloneComponents, removeSelectedComponent, - computeQueryState, + buildAppDefinition, + buildComponentMetaDefinition, } from '@/_helpers/appUtils'; import { Confirm } from './Viewer/Confirm'; import { Tooltip as ReactTooltip } from 'react-tooltip'; @@ -36,7 +37,7 @@ 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'); +const { produce, enablePatches, setAutoFreeze } = require('immer'); import { createWebsocketConnection } from '@/_helpers/websocketConnection'; import RealtimeCursors from '@/Editor/RealtimeCursors'; import { initEditorWalkThrough } from '@/_helpers/createWalkThrough'; @@ -51,95 +52,124 @@ import '@/_styles/editor/react-select-search.scss'; import { withRouter } from '@/_hoc/withRouter'; 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 { useDataQueries, useDataQueriesStore } from '@/_stores/dataQueriesStore'; +import { useAppVersionStore, useAppVersionActions, useAppVersionState } from '@/_stores/appVersionStore'; import { useQueryPanelStore } from '@/_stores/queryPanelStore'; -import { useAppDataStore } from '@/_stores/appDataStore'; -import { useCurrentStateStore, useCurrentState } from '@/_stores/currentStateStore'; -import { resetAllStores } from '@/_stores/utils'; +import { useCurrentStateStore, useCurrentState, getCurrentState } from '@/_stores/currentStateStore'; +import { computeAppDiff, computeComponentPropertyDiff, isParamFromTableColumn, resetAllStores } from '@/_stores/utils'; import { setCookie } from '@/_helpers/cookie'; -import { shallow } from 'zustand/shallow'; +import { useEditorActions, useEditorState, useEditorStore } from '@/_stores/editorStore'; +import { useAppDataActions, useAppInfo, useAppDataStore } from '@/_stores/appDataStore'; +import { useMounted } from '@/_hooks/use-mount'; +// eslint-disable-next-line import/no-unresolved +import { diff } from 'deep-object-diff'; + +import useDebouncedArrowKeyPress from '@/_hooks/useDebouncedArrowKeyPress'; setAutoFreeze(false); enablePatches(); -class EditorComponent extends React.Component { - constructor(props) { - super(props); +function setWindowTitle(name) { + document.title = name ? `${name} - Tooljet` : `My App - Tooljet`; +} +const decimalToHex = (alpha) => (alpha === 0 ? '00' : Math.round(255 * alpha).toString(16)); + +const EditorComponent = (props) => { + const { socket } = createWebsocketConnection(props?.params?.id); + const mounted = useMounted(); + + const { + updateState, + updateAppDefinitionDiff, + updateAppVersion, + setIsSaving, + createAppVersionEventHandlers, + setAppPreviewLink, + } = useAppDataActions(); + const { updateEditorState, updateQueryConfirmationList, setSelectedComponents, setCurrentPageId } = + useEditorActions(); + + const { setAppVersions } = useAppVersionActions(); + const { isVersionReleased, editingVersion, releasedVersionId } = useAppVersionState(); + + const { + appDefinition, + selectedComponents, + currentLayout, + canUndo, + canRedo, + isUpdatingEditorStateInProcess, + saveError, + scrollOptions, + currentSidebarTab, + isLoading, + defaultComponentStateComputed, + showComments, + showLeftSidebar, + queryConfirmationList, + currentPageId, + } = useEditorState(); + + const dataQueries = useDataQueries(); + + const { + isMaintenanceOn, + appId, + app, + appName, + slug, + currentUser, + currentVersionId, + appDefinitionDiff, + appDiffOptions, + events, + areOthersOnSameVersionAndPage, + } = useAppInfo(); + + const currentState = useCurrentState(); + + const [zoomLevel, setZoomLevel] = useState(1); + const [isQueryPaneDragging, setIsQueryPaneDragging] = useState(false); + const [isQueryPaneExpanded, setIsQueryPaneExpanded] = useState(false); //!check where this is used + const [selectionInProgress, setSelectionInProgress] = useState(false); + const [hoveredComponent, setHoveredComponent] = useState(null); + const [editorMarginLeft, setEditorMarginLeft] = useState(0); + + const [isDragging, setIsDragging] = useState(false); + + const [showPageDeletionConfirmation, setShowPageDeletionConfirmation] = useState(null); + const [isDeletingPage, setIsDeletingPage] = useState(false); + + const [currentSessionId, setCurrentSessionId] = useState(null); + + const [undoStack, setUndoStack] = useState([]); + const [redoStack, setRedoStack] = useState([]); + const [optsStack, setOptsStack] = useState({ + undo: [], + redo: [], + }); + + // refs + const canvasContainerRef = useRef(null); + const dataSourceModalRef = useRef(null); + const selectionDragRef = useRef(null); + const selectionRef = useRef(null); + + const prevAppDefinition = useRef(appDefinition); + + useLayoutEffect(() => { resetAllStores(); - const appId = props.id; - useAppDataStore.getState().actions.setAppId(appId); - useEditorStore.getState().actions.setIsEditorActive(true); - const { socket } = createWebsocketConnection(appId); - this.socket = socket; - this.renameQueryNameId = React.createRef(); - const defaultPageId = uuid(); - this.subscription = null; - this.defaultDefinition = { - showViewerNavigation: true, - homePageId: defaultPageId, - pages: { - [defaultPageId]: { - components: {}, - handle: 'home', - name: 'Home', - }, - }, - globalSettings: { - hideHeader: false, - appInMaintenance: false, - canvasMaxWidth: 100, - canvasMaxWidthType: '%', - canvasMaxHeight: 2400, - canvasBackgroundColor: props.darkMode ? '#2f3c4c' : '#F2F2F5', - backgroundFxQuery: '', - }, - }; + }, []); - this.dataSourceModalRef = React.createRef(); - this.canvasContainerRef = React.createRef(); - this.selectionRef = React.createRef(); - this.selectionDragRef = React.createRef(); - this.queryManagerPreferences = JSON.parse(localStorage.getItem('queryManagerPreferences')) ?? {}; - this.state = { - currentUser: {}, - app: {}, - allComponentTypes: componentTypes, - isLoading: true, - users: null, - appId, - showLeftSidebar: true, - zoomLevel: 1.0, - deviceWindowWidth: 450, - appDefinition: this.defaultDefinition, - apps: [], - queryConfirmationList: [], - isSourceSelected: false, - isSaving: false, - isUnsavedQueriesAvailable: false, - scrollOptions: {}, - currentPageId: defaultPageId, - pages: {}, - selectedDataSource: null, - }; + useEffect(() => { + updateState({ isLoading: true }); - this.autoSave = debounce(this.saveEditingVersion, 3000); - this.realtimeSave = debounce(this.appDefinitionChanged, 500); - } - - setWindowTitle(name) { - document.title = name ? `${name} - Tooljet` : `My App - Tooljet`; - } - - onVersionDelete = () => { - this.fetchApp(this.props.params.pageHandle); - }; - getCurrentOrganizationDetails() { const currentSession = authenticationService.currentSessionValue; const currentUser = currentSession?.current_user; - this.subscription = authenticationService.currentSession.subscribe((currentSession) => { + + // Subscribe to changes in the current session using RxJS observable pattern + const subscription = authenticationService.currentSession.subscribe((currentSession) => { if (currentUser && currentSession?.group_permissions) { const userVars = { email: currentUser.email, @@ -147,25 +177,103 @@ class EditorComponent extends React.Component { lastName: currentUser.last_name, groups: currentSession.group_permissions?.map((group) => group.group), }; - this.setState({ currentUser }); + + const appUserDetails = { + ...currentUser, + current_organization_id: currentSession.current_organization_id, + }; + + updateState({ + currentUser: appUserDetails, + }); useCurrentStateStore.getState().actions.setCurrentState({ globals: { - ...this.props.currentState.globals, + ...currentState.globals, + theme: { name: props?.darkMode ? 'dark' : 'light' }, + urlparams: JSON.parse(JSON.stringify(queryString.parse(props.location.search))), currentUser: userVars, + /* Constant value.it will only change for viewer */ + mode: { + value: 'edit', + }, }, }); } }); - } - /** - * - * ThandleMessage event listener in the login component fir iframe communication. - * It now checks if the received message has a type of 'redirectTo' and extracts the redirectPath value from the payload. - * If the value is present, it sets a cookie named 'redirectPath' with the received value and a one-day expiration. - * This allows for redirection to a specific path after the login process is completed. - */ - handleMessage = (event) => { + $componentDidMount(); + + setCurrentSessionId(() => uuid()); + + // 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); + prevAppDefinition.current = null; + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + const lastKeyPressTimestamp = useDebouncedArrowKeyPress(500); // 500 milliseconds delay + // Handle appDefinition updates + useEffect(() => { + const didAppDefinitionChanged = !_.isEqual(appDefinition, prevAppDefinition.current); + + if (didAppDefinitionChanged) { + prevAppDefinition.current = appDefinition; + } + + if (mounted && didAppDefinitionChanged && currentPageId) { + const components = appDefinition?.pages[currentPageId]?.components || {}; + + computeComponentState(components); + + if (isUpdatingEditorStateInProcess) { + autoSave(); + } + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [JSON.stringify({ appDefinition, currentPageId, dataQueries })]); + + useEffect( + () => { + const components = appDefinition?.pages?.[currentPageId]?.components || {}; + computeComponentState(components); + }, + // eslint-disable-next-line react-hooks/exhaustive-deps + [currentPageId] + ); + + useEffect(() => { + // This effect runs when lastKeyPressTimestamp changes + if (Date.now() - lastKeyPressTimestamp < 500) { + updateEditorState({ + isUpdatingEditorStateInProcess: true, + }); + autoSave(); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [JSON.stringify({ appDefinition, lastKeyPressTimestamp })]); + + useEffect(() => { + if (!isEmpty(canvasContainerRef?.current)) { + canvasContainerRef.current.scrollLeft += editorMarginLeft; + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [editorMarginLeft, canvasContainerRef?.current]); + + useEffect(() => { + if (mounted) { + useCurrentStateStore.getState().actions.setCurrentState({ + layout: currentLayout, + }); + } + }, [currentLayout, mounted]); + + const handleMessage = (event) => { const { data } = event; if (data?.type === 'redirectTo') { @@ -174,71 +282,25 @@ class EditorComponent extends React.Component { } }; - async componentDidMount() { - window.addEventListener('message', this.handleMessage); - await this.getCurrentOrganizationDetails(); - this.autoSave(); - this.fetchApps(0); - this.fetchApp(this.props.params.pageHandle); - this.fetchOrgEnvironmentConstants(); // for ce - this.fetchOrgEnvironmentVariables(); - this.initComponentVersioning(); - this.initRealtimeSave(); - this.initEventListeners(); - this.setState({ - currentSidebarTab: 2, - selectedComponents: [], - scrollOptions: { - container: this.canvasContainerRef.current, - throttleTime: 30, - threshold: 0, - }, - }); - const globals = { - ...this.props.currentState.globals, - theme: { name: this.props.darkMode ? 'dark' : 'light' }, - urlparams: JSON.parse(JSON.stringify(queryString.parse(this.props.location.search))), - /* Constant value.it will only change for viewer */ - mode: { - value: 'edit', - }, + const getEditorRef = () => { + const editorRef = { + appDefinition: useEditorStore.getState().appDefinition, + queryConfirmationList: useEditorStore.getState().queryConfirmationList, + updateQueryConfirmationList: updateQueryConfirmationList, + navigate: props.navigate, + switchPage: switchPage, + currentPageId: useEditorStore.getState().currentPageId, }; - const page = { - ...this.props.currentState.page, - handle: this.props.pageHandle, - variables: {}, - }; - useCurrentStateStore.getState().actions.setCurrentState({ globals, page }); - - this.appDataStoreListner = useAppDataStore.subscribe(({ isSaving } = {}) => { - if (isSaving !== this.state.isSaving) { - this.setState({ isSaving }); - } - }); - - this.dataQueriesStoreListner = useDataQueriesStore.subscribe(({ dataQueries }) => { - computeQueryState(dataQueries, this); - }, shallow); - } - - /** - * 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 - */ - initRealtimeSave = () => { - if (!config.ENABLE_MULTIPLAYER_EDITING) return null; - - this.props.ymap?.observe(() => { - if (!isEqual(this.props.editingVersion?.id, this.props.ymap?.get('appDef').editingVersionId)) return; - if (isEqual(this.state.appDefinition, this.props.ymap?.get('appDef').newDefinition)) return; - - this.realtimeSave(this.props.ymap?.get('appDef').newDefinition, { skipAutoSave: true, skipYmapUpdate: true }); - }); + return editorRef; }; - fetchOrgEnvironmentVariables = () => { + 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 = {}; @@ -257,7 +319,7 @@ class EditorComponent extends React.Component { }); }; - fetchOrgEnvironmentConstants = () => { + const fetchOrgEnvironmentConstants = () => { //! for @ee: get the constants from `getConstantsFromEnvironment ` -- '/organization-constants/:environmentId' orgEnvironmentConstantService.getAll().then(({ constants }) => { const orgConstants = {}; @@ -272,75 +334,106 @@ 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); - } - - if (!isEqual(prevState.editorMarginLeft, this.state.editorMarginLeft)) { - this.canvasContainerRef.current.scrollLeft += this.state.editorMarginLeft; - } - } - - initEventListeners() { - this.socket?.addEventListener('message', (event) => { - const data = event.data.replace(/^"(.+(?="$))"$/, '$1'); - if (data === 'versionReleased') { - this.fetchApp(); - // } else if (data === 'dataQueriesChanged') { //Commented since this need additional BE changes to work. - // this.fetchDataQueries(this.state.editingVersion?.id); //Also needs revamping to exclude notifying the client of their own changes. - } else if (data === 'dataSourcesChanged') { - this.fetchDataSources(this.props.editingVersion?.id); - } + const initComponentVersioning = () => { + updateEditorState({ + canUndo: false, + canRedo: false, }); - } - - componentWillUnmount() { - document.title = 'Tooljet - Dashboard'; - this.socket && this.socket?.close(); - this.subscription && this.subscription.unsubscribe(); - if (config.ENABLE_MULTIPLAYER_EDITING) this.props?.provider?.disconnect(); - this.appDataStoreListner && this.appDataStoreListner(); - this.dataQueriesStoreListner && this.dataQueriesStoreListner(); - useEditorStore.getState().actions.setIsEditorActive(false); - } - - // 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. - initComponentVersioning = () => { - this.currentVersion = { - [this.state.currentPageId]: -1, - }; - this.currentVersionChanges = {}; - this.noOfVersionsSupported = 100; - this.canUndo = false; - this.canRedo = false; }; - fetchDataSources = (id) => { + /** + * Initializes real-time saving of application definitions if multiplayer editing is enabled. + * Monitors changes in the 'appDef' property of the provided 'ymap' object and triggers a real-time save + * when all conditions are met. + */ + const initRealtimeSave = () => { + // Check if multiplayer editing is enabled; if not, return early + if (!config.ENABLE_MULTIPLAYER_EDITING) return null; + + // Observe changes in the 'appDef' property of the 'ymap' object + props.ymap?.observe(() => { + const ymapUpdates = props.ymap?.get('appDef'); + + // Check if there is a new session and if others are on the same version and page + if (!ymapUpdates.currentSessionId || ymapUpdates.currentSessionId === currentSessionId) return; + + // Check if others are on the same version and page + if (!ymapUpdates.areOthersOnSameVersionAndPage) return; + + // Check if the new application definition is different from the current one + if (isEqual(appDefinition, ymapUpdates.newDefinition)) return; + + // Trigger real-time save with specific options + realtimeSave(props.ymap?.get('appDef').newDefinition, { + skipAutoSave: true, + skipYmapUpdate: true, + currentSessionId: ymapUpdates.currentSessionId, + }); + }); + }; + + //! websocket events do not work + const initEventListeners = () => { + socket?.addEventListener('message', (event) => { + const data = event.data.replace(/^"(.+(?="$))"$/, '$1'); + if (data === 'versionReleased') fetchApp(); + // else if (data === 'dataQueriesChanged') { + // fetchDataQueries(editingVersion?.id); + // } else if (data === 'dataSourcesChanged') { + // fetchDataSources(editingVersion?.id); + // } + }); + }; + + const $componentDidMount = async () => { + window.addEventListener('message', handleMessage); + + await fetchApp(props.params.pageHandle, true); + await fetchApps(0); + await fetchOrgEnvironmentVariables(); + await fetchOrgEnvironmentConstants(); + initComponentVersioning(); + initRealtimeSave(); + initEventListeners(); + updateEditorState({ + currentSidebarTab: 2, + selectedComponents: [], + scrollOptions: { + container: canvasContainerRef.current, + throttleTime: 30, + threshold: 0, + }, + }); + + getCanvasWidth(); + initEditorWalkThrough(); + }; + + const fetchDataQueries = async (id, selectFirstQuery = false, runQueriesOnAppLoad = false) => { + await useDataQueriesStore + .getState() + .actions.fetchDataQueries(id, selectFirstQuery, runQueriesOnAppLoad, getEditorRef()); + }; + + const fetchDataSources = (id) => { useDataSourcesStore.getState().actions.fetchDataSources(id); }; - fetchGlobalDataSources = () => { - const { current_organization_id: organizationId } = this.state.currentUser; + const fetchGlobalDataSources = () => { + const { current_organization_id: organizationId } = currentUser; useDataSourcesStore.getState().actions.fetchGlobalDataSources(organizationId); }; - fetchDataQueries = async (id, selectFirstQuery = false, runQueriesOnAppLoad = false) => { - await useDataQueriesStore.getState().actions.fetchDataQueries(id, selectFirstQuery, runQueriesOnAppLoad, this); + const onVersionDelete = () => { + fetchApp(props.params.pageHandle); }; - toggleAppMaintenance = () => { - const newState = !this.state.app.is_maintenance_on; + const toggleAppMaintenance = () => { + const newState = !isMaintenanceOn; - // eslint-disable-next-line no-unused-vars - appsService.setMaintenance(this.state.app.id, newState).then((data) => { - this.setState({ - app: { - ...this.state.app, - is_maintenance_on: newState, - }, + appService.setMaintenance(appId, newState).then(() => { + updateState({ + isMaintenanceOn: newState, }); if (newState) { @@ -351,331 +444,705 @@ class EditorComponent extends React.Component { }); }; - fetchApps = (page) => { - appsService.getAll(page).then((data) => - this.setState({ - apps: data.apps, - }) - ); - }; - - fetchApp = (startingPageHandle) => { - const appId = this.props.id; - - const callBack = async (data) => { - let dataDefinition = defaults(data.definition, this.defaultDefinition); - - 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; - - useCurrentStateStore.getState().actions.setCurrentState({ - page: { - handle: dataDefinition.pages[homePageId]?.handle, - name: dataDefinition.pages[homePageId]?.name, - id: homePageId, - variables: {}, - }, - }); - useAppVersionStore.getState().actions.updateEditingVersion(data.editing_version); - useAppVersionStore.getState().actions.updateReleasedVersionId(data.current_version_id); - this.setState( - { - app: data, - isLoading: false, - appDefinition: dataDefinition, - slug: data.slug, - currentPageId: homePageId, - }, - - async () => { - computeComponentState(this, this.state.appDefinition.pages[homePageId]?.components ?? {}).then(async () => { - this.setWindowTitle(data.name); - useEditorStore.getState().actions.setShowComments(!!queryString.parse(this.props.location.search).threadId); - }); - } - ); - useCurrentStateStore.getState().actions.setCurrentState({ - page: { - handle: dataDefinition.pages[homePageId]?.handle, - name: dataDefinition.pages[homePageId]?.name, - id: homePageId, - variables: {}, - }, - }); - - this.fetchDataSources(data.editing_version?.id); - await this.fetchDataQueries(data.editing_version?.id, true, true); - this.fetchGlobalDataSources(); - initEditorWalkThrough(); - for (const event of dataDefinition.pages[homePageId]?.events ?? []) { - await this.handleEvent(event.eventId, event); - } - }; - - this.setState( - { - isLoading: true, - }, - () => { - appsService.getApp(appId).then(callBack); - } - ); - }; - - setAppDefinitionFromVersion = (version, shouldWeEditVersion = true) => { - if (version?.id !== this.props.editingVersion?.id) { - this.appDefinitionChanged(defaults(version.definition, this.defaultDefinition), { - skipAutoSave: true, - skipYmapUpdate: true, - versionChanged: true, - }); - if (version?.id === this.state.app?.current_version_id) { - (this.canUndo = false), (this.canRedo = false); - } - useAppDataStore.getState().actions.setIsSaving(false); - useAppVersionStore.getState().actions.updateEditingVersion(version); - - shouldWeEditVersion && this.saveEditingVersion(true); - this.fetchDataSources(this.props.editingVersion?.id); - this.fetchDataQueries(this.props.editingVersion?.id, true); - this.initComponentVersioning(); - } - }; - - /** - * https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/readyState - */ - dataSourcesChanged = () => { - if (this.socket instanceof WebSocket && this.socket?.readyState === WebSocket.OPEN) { - this.socket?.send( + const dataSourcesChanged = () => { + if (socket instanceof WebSocket && socket?.readyState === WebSocket.OPEN) { + socket?.send( JSON.stringify({ event: 'events', - data: { message: 'dataSourcesChanged', appId: this.state.appId }, + data: { message: 'dataSourcesChanged', appId: appId }, }) ); } else { - this.fetchDataSources(this.props.editingVersion?.id); + fetchDataSources(editingVersion?.id); } }; - globalDataSourcesChanged = () => { - this.fetchGlobalDataSources(); + const globalDataSourcesChanged = () => { + fetchGlobalDataSources(); }; - dataQueriesChanged = (options) => { - /** - * https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/readyState - */ - if (this.socket instanceof WebSocket && this.socket?.readyState === WebSocket.OPEN) { - this.socket?.send( + const dataQueriesChanged = () => { + if (socket instanceof WebSocket && socket?.readyState === WebSocket.OPEN) { + socket?.send( JSON.stringify({ event: 'events', - data: { message: 'dataQueriesChanged', appId: this.state.appId }, + data: { message: 'dataQueriesChanged', appId: appId }, }) ); + } else { + fetchDataQueries(editingVersion?.id); } - options?.isReloadSelf && this.fetchDataQueries(this.props.editingVersion?.id, true); }; - switchSidebarTab = (tabIndex) => { - this.setState({ + const switchSidebarTab = (tabIndex) => { + updateEditorState({ currentSidebarTab: tabIndex, }); }; - filterComponents = (event) => { - const searchText = event.currentTarget.value; - let filteredComponents = this.state.allComponentTypes; - - if (searchText !== '') { - filteredComponents = this.state.allComponentTypes.filter( - (e) => e.name.toLowerCase() === searchText.toLowerCase() - ); - } - - this.setState({ componentTypes: filteredComponents }); + const handleInspectorView = () => { + switchSidebarTab(2); }; - handleAddPatch = (patches, inversePatches) => { - if (isEmpty(patches) && isEmpty(inversePatches)) return; - if (isEqual(patches, inversePatches)) return; + const onNameChanged = (newName) => { + updateState({ appName: newName }); + setWindowTitle(newName); + }; - const currentPage = this.state.currentPageId; - const currentVersion = this.currentVersion[currentPage] ?? -1; + const onZoomChanged = (zoom) => { + setZoomLevel(zoom); + }; - this.currentVersionChanges[currentPage] = this.currentVersionChanges[currentPage] ?? {}; + const getCanvasWidth = () => { + const canvasBoundingRect = document.getElementsByClassName('canvas-area')[0]?.getBoundingClientRect(); - this.currentVersionChanges[currentPage][currentVersion] = { - redo: patches, - undo: inversePatches, + const _canvasWidth = canvasBoundingRect?.width; + return _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); + + 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: { + ...currentState.globals, + theme: { name: newMode ? 'dark' : 'light' }, + }, + }); + props.switchDarkMode(newMode); + }; + + const handleEvent = (eventName, event, options) => { + return onEvent(getEditorRef(), eventName, event, 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 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) { + setSelectedComponents([{ id, component }], multiSelect); + } + }; + + 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 onVersionRelease = (versionId) => { + useAppVersionStore.getState().actions.updateReleasedVersionId(versionId); + + socket.send( + JSON.stringify({ + event: 'events', + data: { message: 'versionReleased', appId: appId }, + }) + ); + }; + + const computeCanvasBackgroundColor = () => { + 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 = (globalOptions) => { + const copyOfAppDefinition = JSON.parse(JSON.stringify(appDefinition)); + const newAppDefinition = _.cloneDeep(copyOfAppDefinition); + + for (const [key, value] of Object.entries(globalOptions)) { + if (value?.[1]?.a == undefined) newAppDefinition.globalSettings[key] = value; + else { + const hexCode = `${value?.[0]}${decimalToHex(value?.[1]?.a)}`; + newAppDefinition.globalSettings[key] = hexCode; + } + } + + updateEditorState({ + isUpdatingEditorStateInProcess: true, + }); + + appDefinitionChanged(newAppDefinition, { + globalSettings: true, + }); + }; + + const callBack = async (data, startingPageHandle, versionSwitched = false) => { + setWindowTitle(data.name); + useAppVersionStore.getState().actions.updateEditingVersion(data.editing_version); + if (!releasedVersionId || !versionSwitched) { + useAppVersionStore.getState().actions.updateReleasedVersionId(data.current_version_id); + } + + const appVersions = await appEnvironmentService.getVersionsByEnvironment(data?.id); + setAppVersions(appVersions.appVersions); + const currentOrgId = data?.organization_id || data?.organizationId; + + updateState({ + slug: data.slug, + isMaintenanceOn: data?.is_maintenance_on, + organizationId: currentOrgId, + isPublic: data?.is_public, + appName: data?.name, + userId: data?.user_id, + appId: data?.id, + events: data.events, + currentVersionId: data?.editing_version?.id, + app: data, + }); + + const appDefData = buildAppDefinition(data); + + const appJson = appDefData; + const pages = data.pages; + + const startingPageId = pages.filter((page) => page.handle === startingPageHandle)[0]?.id; + const homePageId = !startingPageId || startingPageId === 'null' ? appJson.homePageId : startingPageId; + + const currentpageData = { + handle: appJson.pages[homePageId]?.handle, + name: appJson.pages[homePageId]?.name, + id: homePageId, + variables: {}, }; - this.canUndo = this.currentVersionChanges[currentPage].hasOwnProperty(currentVersion); - this.canRedo = this.currentVersionChanges[currentPage].hasOwnProperty(currentVersion + 1); + setCurrentPageId(homePageId); - this.currentVersion[currentPage] = currentVersion + 1; + useCurrentStateStore.getState().actions.setCurrentState({ + page: currentpageData, + }); - delete this.currentVersionChanges[currentPage][currentVersion + 1]; - delete this.currentVersionChanges[currentPage][currentVersion - this.noOfVersionsSupported]; - }; + updateEditorState({ + isLoading: false, + appDefinition: appJson, + isUpdatingEditorStateInProcess: false, + }); - handleUndo = () => { - if (this.canUndo) { - let currentVersion = this.currentVersion[this.state.currentPageId]; - - const appDefinition = applyPatches( - this.state.appDefinition, - this.currentVersionChanges[this.state.currentPageId][currentVersion - 1].undo - ); - - this.canUndo = this.currentVersionChanges[this.state.currentPageId].hasOwnProperty(currentVersion - 1); - this.canRedo = true; - this.currentVersion[this.state.currentPageId] = currentVersion - 1; - - if (!appDefinition) return; - useAppDataStore.getState().actions.setIsSaving(true); - this.setState( - { - appDefinition, + if (versionSwitched) { + props?.navigate(`/${getWorkspaceId()}/apps/${appId}/${appJson.pages[homePageId]?.handle}`, { + state: { + isSwitchingPage: true, }, - () => { - this.props.ymap?.set('appDef', { - newDefinition: appDefinition, - editingVersionId: this.props.editingVersion?.id, - }); - - this.autoSave(); - } - ); - } - }; - - handleRedo = () => { - if (this.canRedo) { - let currentVersion = this.currentVersion[this.state.currentPageId]; - - const appDefinition = applyPatches( - this.state.appDefinition, - this.currentVersionChanges[this.state.currentPageId][currentVersion].redo - ); - - this.canUndo = true; - this.canRedo = this.currentVersionChanges[this.state.currentPageId].hasOwnProperty(currentVersion + 1); - this.currentVersion[this.state.currentPageId] = currentVersion + 1; - - if (!appDefinition) return; - useAppDataStore.getState().actions.setIsSaving(true); - this.setState( - { - appDefinition, - }, - () => { - this.props.ymap?.set('appDef', { - newDefinition: appDefinition, - editingVersionId: this.props.editingVersion?.id, - }); - - this.autoSave(); - } - ); - } - }; - - appDefinitionChanged = (newDefinition, opts = {}) => { - let currentPageId = this.state.currentPageId; - if (isEqual(this.state.appDefinition, newDefinition)) return; - if (config.ENABLE_MULTIPLAYER_EDITING && !opts.skipYmapUpdate) { - this.props.ymap?.set('appDef', { - newDefinition, - editingVersionId: this.props.editingVersion?.id, }); } + await useDataSourcesStore.getState().actions.fetchGlobalDataSources(data?.organization_id); + await fetchDataSources(data.editing_version?.id); + await fetchDataQueries(data.editing_version?.id, true, true); + const currentPageEvents = data.events.filter((event) => event.target === 'page' && event.sourceId === homePageId); + + await handleEvent('onPageLoad', currentPageEvents, {}, true); + }; + + const fetchApp = async (startingPageHandle, onMount = false) => { + const _appId = props?.params?.id || props?.params?.slug; + + if (!onMount) { + await appService.fetchApp(_appId).then((data) => callBack(data, startingPageHandle)); + } else { + callBack(app, startingPageHandle); + } + }; + + const setAppDefinitionFromVersion = (appData) => { + const version = appData?.editing_version?.id; + if (version?.id !== editingVersion?.id) { + if (version?.id === currentVersionId) { + updateEditorState({ + canUndo: false, + canRedo: false, + }); + } + + updateEditorState({ + isLoading: true, + }); + + callBack(appData, null, true); + initComponentVersioning(); + } + }; + + const diffToPatches = (diffObj) => { + return Object.keys(diffObj).reduce((patches, path) => { + const value = diffObj[path]; + return [...patches, { path: path.split('.'), value, op: 'replace' }]; + }, []); + }; + + const appDefinitionChanged = async (newDefinition, opts = {}) => { if (opts?.versionChanged) { - currentPageId = newDefinition.homePageId; - useAppDataStore.getState().actions.setIsSaving(true); - this.setState( - { - currentPageId: currentPageId, - appDefinition: newDefinition, - appDefinitionLocalVersion: uuid(), - }, - () => { - if (!opts.skipAutoSave) this.autoSave(); - this.switchPage(currentPageId); + setCurrentPageId(newDefinition.homePageId); + + return new Promise((resolve) => { + updateEditorState({ + isUpdatingEditorStateInProcess: true, + }); + + resolve(); + }); + } + let updatedAppDefinition; + const copyOfAppDefinition = JSON.parse(JSON.stringify(appDefinition)); + + if (opts?.skipYmapUpdate && opts?.currentSessionId !== currentSessionId) { + updatedAppDefinition = newDefinition; + } else { + updatedAppDefinition = produce(copyOfAppDefinition, (draft) => { + if (_.isEmpty(draft)) return; + + if (opts?.containerChanges || opts?.componentDefinitionChanged) { + const currentPageComponents = newDefinition.pages[currentPageId]?.components; + + draft.pages[currentPageId].components = currentPageComponents; } - ); + + if (opts?.pageDefinitionChanged) { + draft.pages = newDefinition.pages; + } + + if (opts?.homePageChanged) { + draft.homePageId = newDefinition.homePageId; + } + + if (opts?.generalAppDefinitionChanged || opts?.globalSettings || isEmpty(opts)) { + Object.assign(draft, newDefinition); + } + }); + } + + const diffPatches = diff(appDefinition, updatedAppDefinition); + + const inversePatches = diff(updatedAppDefinition, appDefinition); + const shouldUpdate = !_.isEmpty(diffPatches) && !isEqual(appDefinitionDiff, diffPatches); + + if (shouldUpdate) { + const undoPatches = diffToPatches(inversePatches); + + if ( + opts?.componentAdded || + opts?.componentDefinitionChanged || + opts?.componentDeleted || + opts?.containerChanges + ) { + setUndoStack((prev) => [...prev, undoPatches]); + setOptsStack((prev) => ({ ...prev, undo: [...prev.undo, opts] })); + } + + updateAppDefinitionDiff(diffPatches); + + const isParamDiffFromTableColumn = opts?.containerChanges + ? isParamFromTableColumn(diffPatches, updatedAppDefinition) + : false; + + if (isParamDiffFromTableColumn) { + opts.componentDefinitionChanged = true; + opts.isParamFromTableColumn = true; + delete opts.containerChanges; + } + + updateState({ + appDiffOptions: opts, + }); + + let updatingEditorStateInProcess = true; + + if (opts?.widgetMovedWithKeyboard === true) { + updatingEditorStateInProcess = false; + } + + updateEditorState({ + isUpdatingEditorStateInProcess: updatingEditorStateInProcess, + appDefinition: updatedAppDefinition, + }); + } + + if (config.ENABLE_MULTIPLAYER_EDITING && !opts?.skipYmapUpdate && opts?.currentSessionId !== currentSessionId) { + props.ymap?.set('appDef', { + newDefinition: updatedAppDefinition, + editingVersionId: editingVersion?.id, + currentSessionId, + areOthersOnSameVersionAndPage, + }); + } + }; + + const cloneEventsForClonedComponents = (componentUpdateDiff, operation, componentMap) => { + function getKeyFromComponentMap(componentMap, newItem) { + for (const key in componentMap) { + if (componentMap.hasOwnProperty(key) && componentMap[key] === newItem) { + return key; + } + } + return null; + } + + if (operation !== 'create') return; + + const newComponentIds = Object.keys(componentUpdateDiff); + + newComponentIds.forEach((componentId) => { + const sourceComponentId = getKeyFromComponentMap(componentMap, componentId); + if (!sourceComponentId) return; + + appVersionService + .findAllEventsWithSourceId(appId, currentVersionId, sourceComponentId) + .then((componentEvents) => { + if (!componentEvents) return; + componentEvents.forEach((event) => { + const newEvent = { + event: { + ...event?.event, + }, + eventType: event?.target, + attachedTo: componentMap[event?.sourceId], + index: event?.index, + }; + + createAppVersionEventHandlers(newEvent); + }); + }); + }); + }; + + const saveEditingVersion = (isUserSwitchedVersion = false) => { + if (isVersionReleased && !isUserSwitchedVersion) { + updateEditorState({ + isUpdatingEditorStateInProcess: false, + }); + } else if (!isEmpty(editingVersion)) { + //! The computeComponentPropertyDiff function manages the calculation of differences in table columns by requiring complete column data. Without this complete data, the resulting JSON structure may be incorrect. + const paramDiff = computeComponentPropertyDiff(appDefinitionDiff, appDefinition, appDiffOptions); + const updateDiff = computeAppDiff(paramDiff, currentPageId, appDiffOptions, currentLayout); + + if (updateDiff['error']) { + const platform = navigator?.userAgentData?.platform || navigator?.platform || 'unknown'; + const isPlatformMac = platform.toLowerCase().indexOf('mac') > -1; + const toastMessage = `Unable to save changes! ${isPlatformMac ? '(⌘ + Z to undo)' : '(ctrl + Z to undo)'}`; + + toast(toastMessage, { + icon: '🚫', + }); + + return updateEditorState({ + saveError: true, + isUpdatingEditorStateInProcess: false, + }); + } + + updateAppVersion(appId, editingVersion?.id, currentPageId, updateDiff, isUserSwitchedVersion) + .then(() => { + const _editingVersion = { + ...editingVersion, + ...{ definition: appDefinition }, + }; + useAppVersionStore.getState().actions.updateEditingVersion(_editingVersion); + + if ( + updateDiff?.type === 'components' && + updateDiff?.operation === 'delete' && + !appDiffOptions?.componentCut + ) { + const appEvents = Array.isArray(events) && events.length > 0 ? JSON.parse(JSON.stringify(events)) : []; + + const updatedEvents = appEvents.filter((event) => { + return !updateDiff?.updateDiff.includes(event.sourceId); + }); + + updateState({ + events: updatedEvents, + }); + } + + updateEditorState({ + saveError: false, + isUpdatingEditorStateInProcess: false, + }); + }) + .catch(() => { + updateEditorState({ + saveError: true, + isUpdatingEditorStateInProcess: false, + }); + toast.error('App could not save.'); + }) + .finally(() => { + updateState({ + appDiffOptions: {}, + }); + }) + .finally(() => { + if (appDiffOptions?.cloningComponent) { + cloneEventsForClonedComponents( + updateDiff.updateDiff, + updateDiff.operation, + appDiffOptions?.cloningComponent + ); + } + }); + } + + updateEditorState({ + saveError: false, + isUpdatingEditorStateInProcess: false, + }); + }; + + const realtimeSave = debounce(appDefinitionChanged, 500); + const autoSave = debounce(saveEditingVersion, 200); + + function handlePaths(prevPatch, path = [], appJSON) { + const paths = [...path]; + + for (let key in prevPatch) { + const type = typeof prevPatch[key]; + + if (type === 'object') { + handlePaths(prevPatch[key], [...paths, key], appJSON); + } else { + const currentpath = [...paths, key].join('.'); + _.update(appJSON, currentpath, () => prevPatch[key]); + } + } + } + function removeUndefined(obj) { + Object.keys(obj).forEach((key) => { + if (obj[key] && typeof obj[key] === 'object') removeUndefined(obj[key]); + else if (obj[key] === undefined) delete obj[key]; + }); + + return obj; + } + + const handleUndo = () => { + if (canUndo) { + const patchesToUndo = undoStack[undoStack.length - 1]; + + const updatedAppDefinition = JSON.parse(JSON.stringify(appDefinition)); + + handlePaths(patchesToUndo[0]?.value, [...patchesToUndo[0].path], updatedAppDefinition); + + removeUndefined(updatedAppDefinition); + + const _diffPatches = diff(updatedAppDefinition, appDefinition); + const undoDiff = diff(appDefinition, updatedAppDefinition); + + updateAppDefinitionDiff(undoDiff); + setUndoStack((prev) => prev.slice(0, prev.length - 1)); + setRedoStack((prev) => [...prev, diffToPatches(_diffPatches)]); + + let undoOpts = optsStack.undo[optsStack.undo.length - 1]; + + if (undoOpts?.componentDeleted) { + undoOpts = { + componentAdded: true, + }; + } else if (undoOpts?.componentAdded) { + undoOpts = { + componentDeleted: true, + }; + } + + updateState({ + appDiffOptions: undoOpts, + }); + + setOptsStack((prev) => ({ + ...prev, + undo: [...prev.undo.slice(0, prev.undo.length - 1)], + redo: [...prev.redo, optsStack.undo[optsStack.undo.length - 1]], + })); + + updateEditorState({ + appDefinition: updatedAppDefinition, + currentSidebarTab: 2, + isUpdatingEditorStateInProcess: true, + }); + } + }; + + const handleRedo = () => { + if (canRedo) { + const patchesToRedo = redoStack[redoStack.length - 1]; + + const updatedAppDefinition = JSON.parse(JSON.stringify(appDefinition)); + + handlePaths(patchesToRedo[0]?.value, [...patchesToRedo[0].path], updatedAppDefinition); + removeUndefined(updatedAppDefinition); + const _diffPatches = diff(updatedAppDefinition, appDefinition); + const redoDiff = diff(appDefinition, updatedAppDefinition); + updateAppDefinitionDiff(redoDiff); + setRedoStack((prev) => prev.slice(0, prev.length - 1)); + setUndoStack((prev) => [...prev, diffToPatches(_diffPatches)]); + + updateState({ + appDiffOptions: optsStack.redo[optsStack.redo.length - 1], + }); + + setOptsStack((prev) => ({ + ...prev, + undo: [...prev.undo, appDiffOptions], + redo: [...prev.redo.slice(0, prev.redo.length - 1)], + })); + + updateEditorState({ + appDefinition: updatedAppDefinition, + isUpdatingEditorStateInProcess: true, + }); + } + }; + + useEffect(() => { + updateEditorState({ + canUndo: undoStack.length > 0, + canRedo: redoStack.length > 0, + }); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [JSON.stringify(undoStack), JSON.stringify(redoStack)]); + + const componentDefinitionChanged = (componentDefinition, props) => { + if (isVersionReleased) { + useAppVersionStore.getState().actions.enableReleasedVersionPopupState(); return; } - produce( - this.state.appDefinition, - (draft) => { - draft.pages[currentPageId].components = newDefinition.pages[currentPageId]?.components ?? {}; - }, - this.handleAddPatch - ); - useAppDataStore.getState().actions.setIsSaving(true); - this.setState( - { - appDefinition: newDefinition, - appDefinitionLocalVersion: uuid(), - }, - () => { - if (!opts.skipAutoSave) this.autoSave(); - } - ); - }; + if (appDefinition?.pages[currentPageId]?.components[componentDefinition.id]) { + // Create a new copy of appDefinition with lodash's cloneDeep + const updatedAppDefinition = _.cloneDeep(appDefinition); - handleInspectorView = () => { - this.switchSidebarTab(2); - }; + // Update the component definition in the copy + updatedAppDefinition.pages[currentPageId].components[componentDefinition.id].component = + componentDefinition.component; - handleSlugChange = (newSlug) => { - this.setState({ slug: newSlug }); - }; - - removeComponents = () => { - const selectedComponents = this.props?.selectedComponents; - - if (!this.props.isVersionReleased && selectedComponents?.length > 1) { - let newDefinition = cloneDeep(this.state.appDefinition); - removeSelectedComponent(this.state.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: '🗑️', - }); - } - this.appDefinitionChanged(newDefinition, { - skipAutoSave: this.props.isVersionReleased, + updateEditorState({ + isUpdatingEditorStateInProcess: true, }); - this.handleInspectorView(); - } else if (this.props.isVersionReleased) { - useAppVersionStore.getState().actions.enableReleasedVersionPopupState(); + + const diffPatches = diff(appDefinition, updatedAppDefinition); + + if (!isEmpty(diffPatches)) { + appDefinitionChanged(updatedAppDefinition, { skipAutoSave: true, componentDefinitionChanged: true, ...props }); + } } }; - removeComponent = (component) => { - const currentPageId = this.state.currentPageId; - if (!this.props.isVersionReleased) { - let newDefinition = cloneDeep(this.state.appDefinition); - // Delete child components when parent is deleted + const removeComponent = (componentId) => { + if (!isVersionReleased) { + let newDefinition = cloneDeep(appDefinition); let childComponents = []; - if (newDefinition.pages[currentPageId].components?.[component.id].component.component === 'Tabs') { + if (newDefinition.pages[currentPageId].components?.[componentId].component.component === 'Tabs') { childComponents = Object.keys(newDefinition.pages[currentPageId].components).filter((key) => - newDefinition.pages[currentPageId].components[key].parent?.startsWith(component.id) + newDefinition.pages[currentPageId].components[key].component.parent?.startsWith(componentId) ); } else { childComponents = Object.keys(newDefinition.pages[currentPageId].components).filter( - (key) => newDefinition.pages[currentPageId].components[key].parent === component.id + (key) => newDefinition.pages[currentPageId].components[key].component.parent === componentId ); } @@ -683,7 +1150,7 @@ class EditorComponent extends React.Component { delete newDefinition.pages[currentPageId].components[componentId]; }); - delete newDefinition.pages[currentPageId].components[component.id]; + delete newDefinition.pages[currentPageId].components[componentId]; const platform = navigator?.userAgentData?.platform || navigator?.platform || 'unknown'; if (platform.toLowerCase().indexOf('mac') > -1) { toast('Component deleted! (⌘ + Z to undo)', { @@ -694,333 +1161,136 @@ class EditorComponent extends React.Component { icon: '🗑️', }); } - this.appDefinitionChanged(newDefinition, { - skipAutoSave: this.props.isVersionReleased, + appDefinitionChanged(newDefinition, { + componentDefinitionChanged: true, + componentDeleted: true, }); - this.handleInspectorView(); + handleInspectorView(); } else { useAppVersionStore.getState().actions.enableReleasedVersionPopupState(); } }; - componentDefinitionChanged = (componentDefinition) => { - if (this.props.isVersionReleased) { - useAppVersionStore.getState().actions.enableReleasedVersionPopupState(); - return; - } - let _self = this; - const currentPageId = this.state.currentPageId; + 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; - if (this.state.appDefinition?.pages[currentPageId].components[componentDefinition.id]) { - const newDefinition = { - appDefinition: produce(this.state.appDefinition, (draft) => { - draft.pages[currentPageId].components[componentDefinition.id].component = componentDefinition.component; - }), - }; + for (const selectedComponent of selectedComponents) { + let top = newComponents[selectedComponent.id].layouts[currentLayout].top; + let left = newComponents[selectedComponent.id].layouts[currentLayout].left; - produce( - this.state.appDefinition, - (draft) => { - draft.pages[currentPageId].components[componentDefinition.id].component = componentDefinition.component; - }, - this.handleAddPatch - ); - setStateAsync(_self, newDefinition).then(() => { - computeComponentState(_self, _self.state.appDefinition.pages[currentPageId].components); - useAppDataStore.getState().actions.setIsSaving(true); - this.setState({ appDefinitionLocalVersion: uuid() }); - this.autoSave(); - this.props.ymap?.set('appDef', { - newDefinition: newDefinition.appDefinition, - editingVersionId: this.props.editingVersion?.id, - }); - }); - } - }; - - handleEditorEscapeKeyPress = () => { - if (this.props?.selectedComponents?.length > 0) { - this.props.setSelectedComponents([]); - this.handleInspectorView(); - } - }; - - moveComponents = (direction) => { - let appDefinition = JSON.parse(JSON.stringify(this.state.appDefinition)); - let newComponents = appDefinition.pages[this.state.currentPageId].components; - - for (const selectedComponent of this.props.selectedComponents) { - newComponents = produce(newComponents, (draft) => { - let top = draft[selectedComponent.id].layouts[this.props.currentLayout].top; - let left = draft[selectedComponent.id].layouts[this.props.currentLayout].left; - - const gridWidth = (1 * 100) / 43; // width of the canvas grid in percentage - - 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; - } - - draft[selectedComponent.id].layouts[this.props.currentLayout].top = top; - draft[selectedComponent.id].layouts[this.props.currentLayout].left = left; - }); - } - appDefinition.pages[this.state.currentPageId].components = newComponents; - this.appDefinitionChanged(appDefinition); - }; - - cutComponents = () => { - if (this.props.isVersionReleased) { - useAppVersionStore.getState().actions.enableReleasedVersionPopupState(); - - return; - } - cloneComponents(this, this.appDefinitionChanged, false, true); - }; - - copyComponents = () => cloneComponents(this, this.appDefinitionChanged, false); - - cloneComponents = () => { - if (this.props.isVersionReleased) { - useAppVersionStore.getState().actions.enableReleasedVersionPopupState(); - return; - } - cloneComponents(this, this.appDefinitionChanged, true); - }; - - decimalToHex = (alpha) => (alpha === 0 ? '00' : Math.round(255 * alpha).toString(16)); - - globalSettingsChanged = (key, value) => { - const appDefinition = { ...this.state.appDefinition }; - if (value?.[1]?.a == undefined) appDefinition.globalSettings[key] = value; - else { - const hexCode = `${value?.[0]}${this.decimalToHex(value?.[1]?.a)}`; - appDefinition.globalSettings[key] = hexCode; - } - useAppDataStore.getState().actions.setIsSaving(true); - this.setState( - { - appDefinition, - }, - () => { - this.props.ymap?.set('appDef', { - newDefinition: appDefinition, - editingVersionId: this.props.editingVersion?.id, - }); - this.autoSave(); + 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; } - ); - }; - onNameChanged = (newName) => { - this.setState({ - app: { ...this.state.app, name: newName }, - }); - this.setWindowTitle(newName); - }; - - setSelectedComponent = (id, component, multiSelect = false) => { - if (this.props.selectedComponents.length === 0 || !multiSelect) { - this.switchSidebarTab(1); - } else { - this.switchSidebarTab(2); + newComponents[selectedComponent.id].layouts[currentLayout].top = top; + newComponents[selectedComponent.id].layouts[currentLayout].left = left; } - const isAlreadySelected = this.props.selectedComponents.find((component) => component.id === id); + _appDefinition.pages[currentPageId].components = newComponents; - if (!isAlreadySelected) { - this.props.setSelectedComponents([...(multiSelect ? this.props.selectedComponents : []), { id, component }]); + appDefinitionChanged(_appDefinition, { containerChanges: true, widgetMovedWithKeyboard: true }); + }; + + const copyComponents = () => + cloneComponents(selectedComponents, appDefinition, currentPageId, appDefinitionChanged, false); + + const cutComponents = () => { + if (isVersionReleased) { + useAppVersionStore.getState().actions.enableReleasedVersionPopupState(); + + return; } + + cloneComponents(selectedComponents, appDefinition, currentPageId, appDefinitionChanged, false, true); }; - onVersionRelease = (versionId) => { - useAppVersionStore.getState().actions.updateReleasedVersionId(versionId); - this.setState( - { - app: { - ...this.state.app, - current_version_id: versionId, - }, - }, - () => { - this.socket.send( - JSON.stringify({ - event: 'events', - data: { message: 'versionReleased', appId: this.state.appId }, - }) - ); - } - ); - }; + const cloningComponents = () => { + if (isVersionReleased) { + useAppVersionStore.getState().actions.enableReleasedVersionPopupState(); - onZoomChanged = (zoom) => { - this.setState({ - zoomLevel: zoom, - }); - }; - - getCanvasWidth = () => { - const canvasBoundingRect = document.getElementsByClassName('canvas-area')[0].getBoundingClientRect(); - return canvasBoundingRect?.width; - }; - - computeCanvasBackgroundColor = () => { - const { canvasBackgroundColor } = this.state.appDefinition?.globalSettings ?? '#F2F2F5'; - if (['#2f3c4c', '#F2F2F5', '#edeff5'].includes(canvasBackgroundColor)) { - return this.props.darkMode ? '#2f3c4c' : '#F2F2F5'; + return; } - return canvasBackgroundColor; + cloneComponents(selectedComponents, appDefinition, currentPageId, appDefinitionChanged, true, false); }; - 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)`; - }; - - handleQueryPaneDragging = (isQueryPaneDragging) => this.setState({ isQueryPaneDragging }); - handleQueryPaneExpanding = (isQueryPaneExpanded) => this.setState({ isQueryPaneExpanded }); - - saveEditingVersion = (isUserSwitchedVersion = false) => { - if (this.props.isVersionReleased && !isUserSwitchedVersion) { - useAppDataStore.getState().actions.setIsSaving(false); - } else if (!isEmpty(this.props?.editingVersion)) { - appVersionService - .save( - this.state.appId, - this.props.editingVersion?.id, - { definition: this.state.appDefinition }, - isUserSwitchedVersion - ) - .then(() => { - const _editingVersion = { - ...this.props.editingVersion, - ...{ definition: this.state.appDefinition }, - }; - useAppVersionStore.getState().actions.updateEditingVersion(_editingVersion); - this.setState( - { - saveError: false, - }, - () => { - useAppDataStore.getState().actions.setIsSaving(false); - } - ); - }) - .catch(() => { - useAppDataStore.getState().actions.setIsSaving(false); - this.setState({ saveError: true }, () => { - toast.error('App could not save.'); - }); - }); - } - }; - - handleOnComponentOptionChanged = (component, optionName, value) => { - return onComponentOptionChanged(this, component, optionName, value); - }; - - handleOnComponentOptionsChanged = (component, options) => { - return onComponentOptionsChanged(this, component, options); - }; - - handleComponentClick = (id, component) => { - this.switchSidebarTab(1); - }; - - sideBarDebugger = { - error: (data) => { - debuggerActions.error(this, data); - }, - flush: () => { - debuggerActions.flush(this); - }, - generateErrorLogs: (errors) => debuggerActions.generateErrorLogs(errors), - }; - - changeDarkMode = (newMode) => { - useCurrentStateStore.getState().actions.setCurrentState({ - globals: { - ...this.props.currentState.globals, - theme: { name: newMode ? 'dark' : 'light' }, - }, - }); - this.props.switchDarkMode(newMode); - }; - - handleEvent = (eventName, options) => onEvent(this, eventName, options, 'edit'); - - runQuery = (queryId, queryName) => runQuery(this, queryId, queryName); - - onAreaSelectionStart = (e) => { - const isMultiSelect = e.inputEvent.shiftKey || this.props.selectedComponents.length > 0; - this.props.setSelectionInProgress(true); - this.props.setSelectedComponents([...(isMultiSelect ? this.props.selectedComponents : [])]); - }; - - onAreaSelection = (e) => { - e.added.forEach((el) => { - el.classList.add('resizer-select'); - }); - - if (this.props.selectionInProgress) { - e.removed.forEach((el) => { - el.classList.remove('resizer-select'); + const handleEditorEscapeKeyPress = () => { + if (selectedComponents?.length > 0) { + updateEditorState({ + selectedComponents: [], }); + handleInspectorView(); } }; - onAreaSelectionEnd = (e) => { - const currentPageId = this.state.currentPageId; - this.props.setSelectionInProgress(false); - e.selected.forEach((el, index) => { - const id = el.getAttribute('widgetid'); - const component = this.state.appDefinition.pages[currentPageId].components[id].component; - const isMultiSelect = e.inputEvent.shiftKey || (!e.isClick && index != 0); - this.setSelectedComponent(id, component, isMultiSelect); - }); - }; + const removeComponents = () => { + if (!isVersionReleased && selectedComponents?.length > 1) { + let newDefinition = cloneDeep(appDefinition); - onAreaSelectionDragStart = (e) => { - if (e.inputEvent.target.getAttribute('id') !== 'real-canvas') { - this.selectionDragRef.current = true; - } else { - this.selectionDragRef.current = false; + removeSelectedComponent(currentPageId, newDefinition, selectedComponents, appDefinitionChanged); + 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: '🗑️', + }); + } + + handleInspectorView(); + } else if (isVersionReleased) { + useAppVersionStore.getState().actions.enableReleasedVersionPopupState(); } }; - onAreaSelectionDrag = (e) => { - if (this.selectionDragRef.current) { - e.stop(); - this.props.selectionInProgress && this.props.setSelectionInProgress(false); + //Page actions + const renamePage = (pageId, newName) => { + if (Object.entries(appDefinition.pages).some(([pId, { name }]) => newName === name && pId !== pageId)) { + return toast.error('Page name already exists'); } + if (newName.trim().length === 0) { + toast.error('Page name cannot be empty'); + return; + } + + setCurrentPageId(pageId); + + const copyOfAppDefinition = JSON.parse(JSON.stringify(appDefinition)); + + copyOfAppDefinition.pages[pageId].name = newName; + + appDefinitionChanged(copyOfAppDefinition, { pageDefinitionChanged: true }); }; - onAreaSelectionDragEnd = () => { - this.selectionDragRef.current = false; - this.props.selectionInProgress && this.props.setSelectionInProgress(false); - }; - - addNewPage = ({ name, handle }) => { + const addNewPage = ({ name, handle }) => { // check for unique page handles - const pageExists = Object.values(this.state.appDefinition.pages).some((page) => page.name === name); + const pageExists = Object.values(appDefinition.pages).some((page) => page.name === name); if (pageExists) { toast.error('Page name already exists'); return; } - const pageHandles = Object.values(this.state.appDefinition.pages).map((page) => page.handle); + if (name.length > 32) { + toast.error('Page name cannot be more than 32 characters'); + return; + } + + const pageHandles = Object.values(appDefinition.pages).map((page) => page.handle); let newHandle = handle; // If handle already exists, finds a unique handle by incrementing a number until it is not found in the array of existing page handles. @@ -1028,180 +1298,259 @@ class EditorComponent extends React.Component { newHandle = `${handle}-${handleIndex}`; } - const newAppDefinition = { - ...this.state.appDefinition, - pages: { - ...this.state.appDefinition.pages, - [uuid()]: { - name, - handle: newHandle, - components: {}, - }, - }, + const copyOfAppDefinition = JSON.parse(JSON.stringify(appDefinition)); + const newPageId = uuid(); + + copyOfAppDefinition.pages[newPageId] = { + id: newPageId, + name, + handle: newHandle, + components: {}, + index: Object.keys(copyOfAppDefinition.pages).length + 1, }; - useAppDataStore.getState().actions.setIsSaving(true); - this.setState( - { - appDefinition: newAppDefinition, - appDefinitionLocalVersion: uuid(), + setCurrentPageId(newPageId); + setHoveredComponent(null); + updateEditorState({ + currentSidebarTab: 2, + selectedComponents: [], + }); + + appDefinitionChanged(copyOfAppDefinition, { + pageDefinitionChanged: true, + addNewPage: true, + switchPage: true, + pageId: newPageId, + }); + props?.navigate(`/${getWorkspaceId()}/apps/${appId}/${newHandle}`, { + state: { + isSwitchingPage: true, }, - () => { - const newPageId = cloneDeep(Object.keys(newAppDefinition.pages)).pop(); - this.switchPage(newPageId); - this.autoSave(); - } - ); + }); + + const { globals: existingGlobals } = currentState; + + const page = { + id: newPageId, + name, + handle, + variables: copyOfAppDefinition.pages[newPageId]?.variables ?? {}, + }; + + const globals = { + ...existingGlobals, + }; + useCurrentStateStore.getState().actions.setCurrentState({ globals, page }); }; - deletePageRequest = (pageId, isHomePage = false, pageName = '') => { - this.setState({ - showPageDeletionConfirmation: { - isOpen: true, - pageId, - isHomePage, - pageName, + const switchPage = (pageId, queryParams = []) => { + // This are fetched from store to handle runQueriesOnAppLoad + const currentPageId = useEditorStore.getState().currentPageId; + const appDefinition = useEditorStore.getState().appDefinition; + const appId = useAppDataStore.getState()?.appId; + const pageHandle = getCurrentState().pageHandle; + + if (currentPageId === pageId && pageHandle === appDefinition?.pages[pageId]?.handle) { + return; + } + const { name, handle } = appDefinition.pages[pageId]; + + if (!name || !handle) return; + const copyOfAppDefinition = JSON.parse(JSON.stringify(appDefinition)); + const queryParamsString = queryParams.map(([key, value]) => `${key}=${value}`).join('&'); + + props?.navigate(`/${getWorkspaceId()}/apps/${appId}/${handle}?${queryParamsString}`, { + state: { + isSwitchingPage: true, }, }); + + const { globals: existingGlobals } = currentState; + + const page = { + id: pageId, + name, + handle, + variables: copyOfAppDefinition.pages[pageId]?.variables ?? {}, + }; + + const globals = { + ...existingGlobals, + urlparams: JSON.parse(JSON.stringify(queryString.parse(queryParamsString))), + }; + useCurrentStateStore.getState().actions.setCurrentState({ globals, page }); + + setCurrentPageId(pageId); + handleInspectorView(); + + const currentPageEvents = events.filter((event) => event.target === 'page' && event.sourceId === page.id); + + handleEvent('onPageLoad', currentPageEvents); + }; + + const deletePageRequest = (pageId, isHomePage = false, pageName = '') => { + setShowPageDeletionConfirmation({ + isOpen: true, + pageId, + isHomePage, + pageName, + }); }; - cancelDeletePageRequest = () => { - this.setState({ - showPageDeletionConfirmation: { - isOpen: false, - pageId: null, - isHomePage: false, - pageName: null, - }, + const cancelDeletePageRequest = () => { + setShowPageDeletionConfirmation({ + isOpen: false, + pageId: null, + isHomePage: false, + pageName: null, }); }; - executeDeletepageRequest = () => { - const pageId = this.state.showPageDeletionConfirmation.pageId; - const isHomePage = this.state.showPageDeletionConfirmation.isHomePage; - if (Object.keys(this.state.appDefinition.pages).length === 1) { + const executeDeletepageRequest = () => { + const pageId = showPageDeletionConfirmation.pageId; + const isHomePage = showPageDeletionConfirmation.isHomePage; + if (Object.keys(appDefinition.pages).length === 1) { toast.error('You cannot delete the only page in your app.'); return; } - this.setState({ + setIsDeletingPage({ isDeletingPage: true, }); - const toBeDeletedPage = this.state.appDefinition.pages[pageId]; + const copyOfAppDefinition = JSON.parse(JSON.stringify(appDefinition)); + + const toBeDeletedPage = copyOfAppDefinition.pages[pageId]; const newAppDefinition = { - ...this.state.appDefinition, - pages: omit(this.state.appDefinition.pages, pageId), + ...copyOfAppDefinition, + pages: omit(copyOfAppDefinition.pages, pageId), }; - const newCurrentPageId = isHomePage - ? Object.keys(this.state.appDefinition.pages)[0] - : this.state.appDefinition.homePageId; + const newCurrentPageId = isHomePage ? Object.keys(copyOfAppDefinition.pages)[0] : copyOfAppDefinition.homePageId; - useAppDataStore.getState().actions.setIsSaving(true); - this.setState( - { - currentPageId: newCurrentPageId, - appDefinition: newAppDefinition, - appDefinitionLocalVersion: uuid(), - isDeletingPage: false, - }, - () => { - toast.success(`${toBeDeletedPage.name} page deleted.`); + setCurrentPageId(newCurrentPageId); + updateEditorState({ + isUpdatingEditorStateInProcess: true, + }); + setIsDeletingPage(false); - this.switchPage(newCurrentPageId); - this.autoSave(); - } - ); + appDefinitionChanged(newAppDefinition, { + pageDefinitionChanged: true, + deletePageRequest: true, + }); + + toast.success(`${toBeDeletedPage.name} page deleted.`); + + switchPage(newCurrentPageId); }; - updateHomePage = (pageId) => { - useAppDataStore.getState().actions.setIsSaving(true); - this.setState( - { - appDefinition: { - ...this.state.appDefinition, - homePageId: pageId, - }, - appDefinitionLocalVersion: uuid(), - }, - () => { - this.autoSave(); - } - ); + const disableEnablePage = ({ pageId, isDisabled }) => { + updateEditorState({ + isUpdatingEditorStateInProcess: true, + }); + + const copyOfAppDefinition = JSON.parse(JSON.stringify(appDefinition)); + + const newAppDefinition = _.cloneDeep(copyOfAppDefinition); + + newAppDefinition.pages[pageId].disabled = isDisabled ?? false; + + switchPage(pageId); + appDefinitionChanged(newAppDefinition, { + pageDefinitionChanged: true, + }); }; - clonePage = (pageId) => { - const currentPage = this.state.appDefinition.pages[pageId]; - const newPageId = uuid(); - let newPageName = `${currentPage.name} (copy)`; - let newPageHandle = `${currentPage.handle}-copy`; - let i = 1; - while (Object.values(this.state.appDefinition.pages).some((page) => page.handle === newPageHandle)) { - newPageName = `${currentPage.name} (copy ${i})`; - newPageHandle = `${currentPage.handle}-copy-${i}`; - i++; - } + const hidePage = (pageId) => { + updateEditorState({ + isUpdatingEditorStateInProcess: true, + }); - const newPageData = cloneDeep(currentPage); - const oldToNewIdMapping = {}; - if (!isEmpty(currentPage?.components)) { - newPageData.components = Object.keys(newPageData.components).reduce((acc, key) => { - const newComponentId = uuid(); - acc[newComponentId] = newPageData.components[key]; - acc[newComponentId].id = newComponentId; - oldToNewIdMapping[key] = newComponentId; - return acc; - }, {}); + const copyOfAppDefinition = JSON.parse(JSON.stringify(appDefinition)); - Object.values(newPageData.components).map((comp) => { - if (comp.parent) { - let newParentId = oldToNewIdMapping[comp.parent]; - if (newParentId) { - comp.parent = newParentId; - } else { - const oldParentId = Object.keys(oldToNewIdMapping).find( - (parentId) => - comp.parent.startsWith(parentId) && - ['Tabs', 'Calendar'].includes(currentPage?.components[parentId]?.component?.component) - ); - const childTabId = comp.parent.split('-').at(-1); - comp.parent = `${oldToNewIdMapping[oldParentId]}-${childTabId}`; - } - } - return comp; - }); - } + const newAppDefinition = _.cloneDeep(copyOfAppDefinition); - const newPage = { - ...newPageData, - name: newPageName, - handle: newPageHandle, - }; + newAppDefinition.pages[pageId].hidden = true; - const newAppDefinition = { - ...this.state.appDefinition, - pages: { - ...this.state.appDefinition.pages, - [newPageId]: newPage, - }, - }; - - useAppDataStore.getState().actions.setIsSaving(true); - this.setState( - { - appDefinition: newAppDefinition, - appDefinitionLocalVersion: uuid(), - }, - () => { - this.autoSave(); - } - ); + switchPage(pageId); + appDefinitionChanged(newAppDefinition, { + pageDefinitionChanged: true, + }); }; - updatePageHandle = (pageId, newHandle) => { - const pageExists = Object.values(this.state.appDefinition.pages).some((page) => page.handle === newHandle); + const unHidePage = (pageId) => { + updateEditorState({ + isUpdatingEditorStateInProcess: true, + }); + + const copyOfAppDefinition = JSON.parse(JSON.stringify(appDefinition)); + + const newAppDefinition = _.cloneDeep(copyOfAppDefinition); + + newAppDefinition.pages[pageId].hidden = false; + switchPage(pageId); + appDefinitionChanged(newAppDefinition, { + pageDefinitionChanged: true, + }); + }; + + const clonePage = (pageId) => { + setIsSaving(true); + appVersionService + .clonePage(appId, editingVersion?.id, pageId) + .then((data) => { + const copyOfAppDefinition = JSON.parse(JSON.stringify(appDefinition)); + + const pages = data.pages.reduce((acc, page) => { + const currentComponents = buildComponentMetaDefinition(_.cloneDeep(page?.components)); + + page.components = currentComponents; + + acc[page.id] = page; + + return acc; + }, {}); + + const newAppDefinition = { + ...copyOfAppDefinition, + pages: { + ...copyOfAppDefinition.pages, + ...pages, + }, + }; + updateState({ + events: data.events, + }); + appDefinitionChanged(newAppDefinition); + }) + .finally(() => setIsSaving(false)); + }; + + const updateHomePage = (pageId) => { + updateEditorState({ + isUpdatingEditorStateInProcess: true, + }); + + const copyOfAppDefinition = JSON.parse(JSON.stringify(appDefinition)); + + const newAppDefinition = _.cloneDeep(copyOfAppDefinition); + + newAppDefinition.homePageId = pageId; + + appDefinitionChanged(newAppDefinition, { + homePageChanged: true, + }); + }; + + const updatePageHandle = (pageId, newHandle) => { + const copyOfAppDefinition = JSON.parse(JSON.stringify(appDefinition)); + + updateEditorState({ + isUpdatingEditorStateInProcess: true, + }); + + const pageExists = Object.values(copyOfAppDefinition.pages).some((page) => page.handle === newHandle); if (pageExists) { toast.error('Page with same handle already exists'); @@ -1213,634 +1562,360 @@ class EditorComponent extends React.Component { return; } - useAppDataStore.getState().actions.setIsSaving(true); - this.setState( - { - appDefinition: { - ...this.state.appDefinition, - pages: { - ...this.state.appDefinition.pages, - [pageId]: { - ...this.state.appDefinition.pages[pageId], - handle: newHandle, - }, - }, - }, - appDefinitionLocalVersion: uuid(), - }, - () => { - toast.success('Page handle updated successfully'); - this.switchPage(pageId); - this.autoSave(); - } - ); - }; + const newDefinition = _.cloneDeep(copyOfAppDefinition); - updateOnPageLoadEvents = (pageId, events) => { - useAppDataStore.getState().actions.setIsSaving(true); - this.setState( - { - appDefinition: { - ...this.state.appDefinition, - pages: { - ...this.state.appDefinition.pages, - [pageId]: { - ...this.state.appDefinition.pages[pageId], - events, - }, - }, - }, - appDefinitionLocalVersion: uuid(), - }, - () => { - this.autoSave(); - } - ); - }; + newDefinition.pages[pageId].handle = newHandle; - showHideViewerNavigation = () => { - const newAppDefinition = { - ...this.state.appDefinition, - showViewerNavigation: !this.state.appDefinition.showViewerNavigation, - }; - - useAppDataStore.getState().actions.setIsSaving(true); - this.setState( - { - appDefinition: newAppDefinition, - appDefinitionLocalVersion: uuid(), - }, - () => this.autoSave() - ); - }; - - renamePage = (pageId, newName) => { - if (Object.entries(this.state.appDefinition.pages).some(([pId, { name }]) => newName === name && pId !== pageId)) { - return toast.error('Page name already exists'); - } - if (newName.trim().length === 0) { - toast.error('Page name cannot be empty'); - return; - } - - const newAppDefinition = { - ...this.state.appDefinition, - pages: { - ...this.state.appDefinition.pages, - [pageId]: { - ...this.state.appDefinition.pages[pageId], - name: newName, - }, - }, - }; - - useAppDataStore.getState().actions.setIsSaving(true); - this.setState( - { - appDefinition: newAppDefinition, - appDefinitionLocalVersion: uuid(), - }, - () => { - this.autoSave(); - } - ); - }; - - hidePage = (pageId) => { - const newAppDefinition = { - ...this.state.appDefinition, - pages: { - ...this.state.appDefinition.pages, - [pageId]: { - ...this.state.appDefinition.pages[pageId], - hidden: true, - }, - }, - }; - - useAppDataStore.getState().actions.setIsSaving(true); - this.setState( - { - appDefinition: newAppDefinition, - appDefinitionLocalVersion: uuid(), - }, - () => { - this.autoSave(); - } - ); - }; - - unHidePage = (pageId) => { - const newAppDefinition = { - ...this.state.appDefinition, - pages: { - ...this.state.appDefinition.pages, - [pageId]: { - ...this.state.appDefinition.pages[pageId], - hidden: false, - }, - }, - }; - - useAppDataStore.getState().actions.setIsSaving(true); - this.setState( - { - appDefinition: newAppDefinition, - appDefinitionLocalVersion: uuid(), - }, - () => { - this.autoSave(); - } - ); - }; - - disableEnablePage = ({ pageId, isDisabled }) => { - const newAppDefinition = { - ...this.state.appDefinition, - pages: { - ...this.state.appDefinition.pages, - [pageId]: { - ...this.state.appDefinition.pages[pageId], - disabled: isDisabled ?? false, - }, - }, - }; - - this.setState( - { - isSaving: true, - appDefinition: newAppDefinition, - appDefinitionLocalVersion: uuid(), - }, - () => { - this.autoSave(); - } - ); - }; - - switchPage = (pageId, queryParams = []) => { - document.getElementById('real-canvas').scrollIntoView(); - if ( - this.state.currentPageId === pageId && - this.props.currentState.page.handle === this.state.appDefinition?.pages[pageId]?.handle - ) { - return; - } - const { name, handle, events } = this.state.appDefinition.pages[pageId]; - const currentPageId = this.state.currentPageId; - - if (!name || !handle) return; - - const queryParamsString = queryParams.map(([key, value]) => `${key}=${value}`).join('&'); - - this.props.navigate(`/${getWorkspaceId()}/apps/${this.state.slug}/${handle}?${queryParamsString}`, { - state: { - isSwitchingPage: true, - }, + appDefinitionChanged(newDefinition, { + pageDefinitionChanged: true, }); - - const { globals: existingGlobals } = this.props.currentState; - - const page = { - id: pageId, - name, - handle, - variables: this.state.pages?.[pageId]?.variables ?? {}, - }; - - const globals = { - ...existingGlobals, - urlparams: JSON.parse(JSON.stringify(queryString.parse(queryParamsString))), - }; - useCurrentStateStore.getState().actions.setCurrentState({ globals, page }); - this.setState( - { - pages: { - ...this.state.pages, - [currentPageId]: { - ...(this.state.pages?.[currentPageId] ?? {}), - variables: { - ...(this.props.currentState?.page?.variables ?? {}), - }, - }, - }, - currentPageId: pageId, - }, - () => { - // Move this callback to zustand - computeComponentState(this, this.state.appDefinition.pages[pageId]?.components ?? {}).then(async () => { - for (const event of events ?? []) { - await this.handleEvent(event.eventId, event); - } - }); - } - ); }; - updateOnSortingPages = (newSortedPages) => { - const pagesObj = newSortedPages.reduce((acc, page) => { - acc[page.id] = this.state.appDefinition.pages[page.id]; + const updateOnSortingPages = (newSortedPages) => { + const copyOfAppDefinition = JSON.parse(JSON.stringify(appDefinition)); + const pagesObj = newSortedPages.reduce((acc, page, index) => { + acc[page.id] = { + ...page, + index: index + 1, + }; return acc; }, {}); - const newAppDefinition = { - ...this.state.appDefinition, - pages: pagesObj, - }; + const newAppDefinition = _.cloneDeep(copyOfAppDefinition); - useAppDataStore.getState().actions.setIsSaving(true); - this.setState( - { - appDefinition: newAppDefinition, - appDefinitionLocalVersion: uuid(), - }, - () => { - this.autoSave(); - } - ); + newAppDefinition.pages = pagesObj; + + appDefinitionChanged(newAppDefinition, { + pageDefinitionChanged: true, + pageSortingChanged: true, + }); }; - getPagesWithIds = () => { - return Object.entries(this.state.appDefinition.pages).map(([id, page]) => ({ ...page, id })); + const showHideViewerNavigation = () => { + const copyOfAppDefinition = JSON.parse(JSON.stringify(appDefinition)); + const newAppDefinition = _.cloneDeep(copyOfAppDefinition); + console.log(newAppDefinition.showViewerNavigation, 'Bedore'); + newAppDefinition.showViewerNavigation = !newAppDefinition.showViewerNavigation; + console.log(newAppDefinition.showViewerNavigation, 'newAppDefinition.showViewerNavigation'); + appDefinitionChanged(newAppDefinition, { + generalAppDefinitionChanged: true, + }); }; - getCanvasMinWidth = () => { - /** - * minWidth will be min(default canvas min width, user set max width). Done to avoid conflict between two - * default canvas min width = calc((total view width - width component editor side bar) - width of editor sidebar on left) - **/ - const defaultCanvasMinWidth = `calc((100vw - 300px) - 48px)`; - const canvasMaxWidthType = this.state.appDefinition.globalSettings.canvasMaxWidthType || 'px'; - const canvasMaxWidth = this.state.appDefinition.globalSettings.canvasMaxWidth; - const currentLayout = this.state.currentLayout; - - const userSetMaxWidth = currentLayout === 'desktop' ? `${+canvasMaxWidth + canvasMaxWidthType}` : '450px'; - - if (this.state.appDefinition.globalSettings.canvasMaxWidth && canvasMaxWidthType !== '%') { - return `min(${defaultCanvasMinWidth}, ${userSetMaxWidth})`; - } else { - return defaultCanvasMinWidth; - } - }; - handleCanvasContainerMouseUp = (e) => { - if (['real-canvas', 'modal'].includes(e.target.className)) { - this.props.setSelectedComponents([]); - this.setState({ currentSidebarTab: 2 }); - } - }; - - handleEditorMarginLeftChange = (value) => this.setState({ editorMarginLeft: value }); - - render() { - const { - currentSidebarTab, - appDefinition, - appId, - slug, - app, - showLeftSidebar, - isLoading, - zoomLevel, - deviceWindowWidth, - apps, - defaultComponentStateComputed, - queryConfirmationList, - } = this.state; - const selectedComponents = this?.props?.selectedComponents; - const currentState = this.props?.currentState; - const editingVersion = this.props?.editingVersion; + useEffect(() => { const previewQuery = queryString.stringify({ version: editingVersion?.name }); const appVersionPreviewLink = editingVersion ? `/applications/${slug || appId}/${currentState.page.handle}${ !_.isEmpty(previewQuery) ? `?${previewQuery}` : '' }` : ''; - return ( -
- 0} - message={`Do you want to run this query - ${queryConfirmationList[0]?.queryName}?`} - onConfirm={(queryConfirmationData) => onQueryConfirmOrCancel(this, queryConfirmationData, true)} - onCancel={() => onQueryConfirmOrCancel(this, queryConfirmationList[0])} - queryConfirmationData={queryConfirmationList[0]} - darkMode={this.props.darkMode} - key={queryConfirmationList[0]?.queryName} - /> - this.executeDeletepageRequest()} - onCancel={() => this.cancelDeletePageRequest()} - darkMode={this.props.darkMode} - /> - {this.props.isVersionReleased && } - - - -
- runQuery(this, queryId, queryName)} - ref={this.dataSourceModalRef} - isSaving={this.state.isSaving} - currentPageId={this.state.currentPageId} - addNewPage={this.addNewPage} - switchPage={this.switchPage} - deletePage={this.deletePageRequest} - renamePage={this.renamePage} - clonePage={this.clonePage} - hidePage={this.hidePage} - disableEnablePage={this.disableEnablePage} - unHidePage={this.unHidePage} - updateHomePage={this.updateHomePage} - updatePageHandle={this.updatePageHandle} - updateOnPageLoadEvents={this.updateOnPageLoadEvents} - showHideViewerNavigationControls={this.showHideViewerNavigation} - updateOnSortingPages={this.updateOnSortingPages} - apps={apps} - setEditorMarginLeft={this.handleEditorMarginLeftChange} - slug={slug} - handleSlugChange={this.handleSlugChange} - /> - {!this.props.showComments && ( - { - this.canvasContainerRef.current.scrollBy(e.direction[0] * 10, e.direction[1] * 10); - }} - /> - )} + setAppPreviewLink(appVersionPreviewLink); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [slug, currentVersionId]); + + const deviceWindowWidth = 450; + + const editorRef = { + appDefinition: appDefinition, + queryConfirmationList: queryConfirmationList, + updateQueryConfirmationList: updateQueryConfirmationList, + navigate: props.navigate, + switchPage: switchPage, + currentPageId: currentPageId, + }; + + if (isLoading) { + return ( +
+
+
+
+
+
+ +
+ {Array.from(Array(4)).map((_item, index) => ( + + ))} +
+ +
+ + +
+
+
+
+
+ ); + } + + //! Need to move conditionally rendered components to separate components => Widget Manger or Widget Inspector + const shouldrenderWidgetInspector = + currentSidebarTab === 1 && + selectedComponents?.length === 1 && + !isEmpty(appDefinition?.pages[currentPageId]?.components) && + !isEmpty(appDefinition?.pages[currentPageId]?.components[selectedComponents[0]?.id]); + + return ( +
+ 0} + message={`Do you want to run this query - ${queryConfirmationList[0]?.queryName}?`} + onConfirm={(queryConfirmationData) => onQueryConfirmOrCancel(editorRef, queryConfirmationData, true)} + onCancel={() => onQueryConfirmOrCancel(editorRef, queryConfirmationList[0])} + queryConfirmationData={queryConfirmationList[0]} + darkMode={props.darkMode} + key={queryConfirmationList[0]?.queryName} + /> + executeDeletepageRequest()} + onCancel={() => cancelDeletePageRequest()} + darkMode={props.darkMode} + /> + {isVersionReleased && } + + + +
+ handleRunQuery(queryId, queryName)} + ref={dataSourceModalRef} + isSaving={isUpdatingEditorStateInProcess} + currentPageId={currentPageId} + addNewPage={addNewPage} + switchPage={switchPage} + deletePage={deletePageRequest} + renamePage={renamePage} + clonePage={clonePage} + hidePage={hidePage} + unHidePage={unHidePage} + disableEnablePage={disableEnablePage} + updateHomePage={updateHomePage} + updatePageHandle={updatePageHandle} + showHideViewerNavigationControls={showHideViewerNavigation} + updateOnSortingPages={updateOnSortingPages} + setEditorMarginLeft={handleEditorMarginLeftChange} + isMaintenanceOn={isMaintenanceOn} + toggleAppMaintenance={toggleAppMaintenance} + /> + {!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(); + }} > -
{ - this.selectionRef.current.checkScroll(); - }} - > -
-
- {config.ENABLE_MULTIPLAYER_EDITING && ( - - )} - {isLoading && ( -
-
-
-
-
-
- -
- {Array.from(Array(4)).map((_item, index) => ( - - ))} -
- -
- - +
+
+ {config.ENABLE_MULTIPLAYER_EDITING && ( + + )} + {isLoading && ( +
+
+
+
+
+
+
+ {Array.from(Array(4)).map((_item, index) => ( + + ))} +
+ +
+ +
- )} - {defaultComponentStateComputed && ( - <> - - this.setState({ isDragging })} - /> - - )} -
-
-
- - -
-
- - - {currentSidebarTab === 1 && ( -
- {selectedComponents.length === 1 && - !isEmpty(appDefinition.pages[this.state.currentPageId]?.components) && - !isEmpty(appDefinition.pages[this.state.currentPageId]?.components[selectedComponents[0].id]) ? ( - - ) : ( -
- {this.props.t('editor.inspectComponent', 'Please select a component to inspect')} -
+
+ )} + {defaultComponentStateComputed && ( + <> + + setIsDragging(isDragging)} + /> + )}
- )} - - {currentSidebarTab === 2 && ( - - )} +
- {config.COMMENT_FEATURE_ENABLE && this.props.showComments && ( - + + +
+
+ + + {shouldrenderWidgetInspector ? ( +
+ +
+ ) : ( + )}
- - -
- ); - } -} - -const withStore = (Component) => (props) => { - const { - showComments, - currentLayout, - selectionInProgress, - setSelectionInProgress, - selectedComponents, - setSelectedComponents, - } = useEditorStore( - (state) => ({ - showComments: state?.showComments, - currentLayout: state?.currentLayout, - selectionInProgress: state?.selectionInProgress, - setSelectionInProgress: state?.actions?.setSelectionInProgress, - selectedComponents: state?.selectedComponents, - setSelectedComponents: state?.actions?.setSelectedComponents, - }), - shallow - ); - const { isVersionReleased, editingVersion } = useAppVersionStore( - (state) => ({ isVersionReleased: state.isVersionReleased, editingVersion: state.editingVersion }), - shallow - ); - const currentState = useCurrentState(); - - return ( - + {config.COMMENT_FEATURE_ENABLE && showComments && ( + + )} +
+ + +
); }; -export const Editor = withTranslation()(withRouter(withStore(EditorComponent))); +export const Editor = withTranslation()(withRouter(EditorComponent)); diff --git a/frontend/src/Editor/EditorFunc.jsx b/frontend/src/Editor/EditorFunc.jsx deleted file mode 100644 index 6bfe52b1bb..0000000000 --- a/frontend/src/Editor/EditorFunc.jsx +++ /dev/null @@ -1,1921 +0,0 @@ -import React, { useEffect, useLayoutEffect, useRef, useState } from 'react'; -import { - appService, - authenticationService, - appVersionService, - orgEnvironmentVariableService, - appEnvironmentService, - orgEnvironmentConstantService, -} from '@/_services'; -import { DndProvider } from 'react-dnd'; -import { HTML5Backend } from 'react-dnd-html5-backend'; -import _, { cloneDeep, isEqual, isEmpty, debounce, omit } 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, - buildAppDefinition, - buildComponentMetaDefinition, -} 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 } = 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, useAppVersionActions, useAppVersionState } from '@/_stores/appVersionStore'; -import { useQueryPanelStore } from '@/_stores/queryPanelStore'; -import { useCurrentStateStore, useCurrentState, getCurrentState } from '@/_stores/currentStateStore'; -import { computeAppDiff, computeComponentPropertyDiff, isParamFromTableColumn, resetAllStores } from '@/_stores/utils'; -import { setCookie } from '@/_helpers/cookie'; -import { useEditorActions, useEditorState, useEditorStore } from '@/_stores/editorStore'; -import { useAppDataActions, useAppInfo, useAppDataStore } from '@/_stores/appDataStore'; -import { useMounted } from '@/_hooks/use-mount'; -// eslint-disable-next-line import/no-unresolved -import { diff } from 'deep-object-diff'; - -import useDebouncedArrowKeyPress from '@/_hooks/useDebouncedArrowKeyPress'; - -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 EditorComponent = (props) => { - const { socket } = createWebsocketConnection(props?.params?.id); - const mounted = useMounted(); - - const { - updateState, - updateAppDefinitionDiff, - updateAppVersion, - setIsSaving, - createAppVersionEventHandlers, - setAppPreviewLink, - } = useAppDataActions(); - const { updateEditorState, updateQueryConfirmationList, setSelectedComponents, setCurrentPageId } = - useEditorActions(); - - const { setAppVersions } = useAppVersionActions(); - const { isVersionReleased, editingVersion, releasedVersionId } = useAppVersionState(); - - const { - appDefinition, - selectedComponents, - currentLayout, - canUndo, - canRedo, - isUpdatingEditorStateInProcess, - saveError, - scrollOptions, - currentSidebarTab, - isLoading, - defaultComponentStateComputed, - showComments, - showLeftSidebar, - queryConfirmationList, - currentPageId, - } = useEditorState(); - - const dataQueries = useDataQueries(); - - const { - isMaintenanceOn, - appId, - app, - appName, - slug, - currentUser, - currentVersionId, - appDefinitionDiff, - appDiffOptions, - events, - areOthersOnSameVersionAndPage, - } = useAppInfo(); - - const currentState = useCurrentState(); - - const [zoomLevel, setZoomLevel] = useState(1); - const [isQueryPaneDragging, setIsQueryPaneDragging] = useState(false); - const [isQueryPaneExpanded, setIsQueryPaneExpanded] = useState(false); //!check where this is used - const [selectionInProgress, setSelectionInProgress] = useState(false); - const [hoveredComponent, setHoveredComponent] = useState(null); - const [editorMarginLeft, setEditorMarginLeft] = useState(0); - - const [isDragging, setIsDragging] = useState(false); - - const [showPageDeletionConfirmation, setShowPageDeletionConfirmation] = useState(null); - const [isDeletingPage, setIsDeletingPage] = useState(false); - - const [currentSessionId, setCurrentSessionId] = useState(null); - - const [undoStack, setUndoStack] = useState([]); - const [redoStack, setRedoStack] = useState([]); - const [optsStack, setOptsStack] = useState({ - undo: [], - redo: [], - }); - - // refs - const canvasContainerRef = useRef(null); - const dataSourceModalRef = useRef(null); - const selectionDragRef = useRef(null); - const selectionRef = useRef(null); - - const prevAppDefinition = useRef(appDefinition); - - useLayoutEffect(() => { - resetAllStores(); - }, []); - - useEffect(() => { - updateState({ isLoading: true }); - - const currentSession = authenticationService.currentSessionValue; - const currentUser = currentSession?.current_user; - - // Subscribe to changes in the current session using RxJS observable pattern - const subscription = authenticationService.currentSession.subscribe((currentSession) => { - if (currentUser && currentSession?.group_permissions) { - 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: { - ...currentState.globals, - theme: { name: props?.darkMode ? 'dark' : 'light' }, - urlparams: JSON.parse(JSON.stringify(queryString.parse(props.location.search))), - currentUser: userVars, - /* Constant value.it will only change for viewer */ - mode: { - value: 'edit', - }, - }, - }); - } - }); - - $componentDidMount(); - - setCurrentSessionId(() => uuid()); - - // 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); - prevAppDefinition.current = null; - }; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - const lastKeyPressTimestamp = useDebouncedArrowKeyPress(500); // 500 milliseconds delay - // Handle appDefinition updates - useEffect(() => { - const didAppDefinitionChanged = !_.isEqual(appDefinition, prevAppDefinition.current); - - if (didAppDefinitionChanged) { - prevAppDefinition.current = appDefinition; - } - - if (mounted && didAppDefinitionChanged && currentPageId) { - const components = appDefinition?.pages[currentPageId]?.components || {}; - - computeComponentState(components); - - if (isUpdatingEditorStateInProcess) { - autoSave(); - } - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [JSON.stringify({ appDefinition, currentPageId, dataQueries })]); - - useEffect( - () => { - const components = appDefinition?.pages?.[currentPageId]?.components || {}; - computeComponentState(components); - }, - // eslint-disable-next-line react-hooks/exhaustive-deps - [currentPageId] - ); - - useEffect(() => { - // This effect runs when lastKeyPressTimestamp changes - if (Date.now() - lastKeyPressTimestamp < 500) { - updateEditorState({ - isUpdatingEditorStateInProcess: true, - }); - autoSave(); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [JSON.stringify({ appDefinition, lastKeyPressTimestamp })]); - - useEffect(() => { - if (!isEmpty(canvasContainerRef?.current)) { - canvasContainerRef.current.scrollLeft += editorMarginLeft; - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [editorMarginLeft, canvasContainerRef?.current]); - - useEffect(() => { - if (mounted) { - useCurrentStateStore.getState().actions.setCurrentState({ - layout: currentLayout, - }); - } - }, [currentLayout, mounted]); - - const handleMessage = (event) => { - const { data } = event; - - if (data?.type === 'redirectTo') { - const redirectCookie = data?.payload['redirectPath']; - setCookie('redirectPath', redirectCookie, 1); - } - }; - - const getEditorRef = () => { - const editorRef = { - appDefinition: useEditorStore.getState().appDefinition, - queryConfirmationList: useEditorStore.getState().queryConfirmationList, - updateQueryConfirmationList: updateQueryConfirmationList, - navigate: props.navigate, - switchPage: switchPage, - currentPageId: useEditorStore.getState().currentPageId, - }; - return editorRef; - }; - - 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, - }); - }); - }; - - const fetchOrgEnvironmentConstants = () => { - //! for @ee: get the constants from `getConstantsFromEnvironment ` -- '/organization-constants/:environmentId' - orgEnvironmentConstantService.getAll().then(({ constants }) => { - const orgConstants = {}; - constants.map((constant) => { - const constantValue = constant.values.find((value) => value.environmentName === 'production')['value']; - orgConstants[constant.name] = constantValue; - }); - - useCurrentStateStore.getState().actions.setCurrentState({ - constants: orgConstants, - }); - }); - }; - - const initComponentVersioning = () => { - updateEditorState({ - canUndo: false, - canRedo: false, - }); - }; - - /** - * Initializes real-time saving of application definitions if multiplayer editing is enabled. - * Monitors changes in the 'appDef' property of the provided 'ymap' object and triggers a real-time save - * when all conditions are met. - */ - const initRealtimeSave = () => { - // Check if multiplayer editing is enabled; if not, return early - if (!config.ENABLE_MULTIPLAYER_EDITING) return null; - - // Observe changes in the 'appDef' property of the 'ymap' object - props.ymap?.observe(() => { - const ymapUpdates = props.ymap?.get('appDef'); - - // Check if there is a new session and if others are on the same version and page - if (!ymapUpdates.currentSessionId || ymapUpdates.currentSessionId === currentSessionId) return; - - // Check if others are on the same version and page - if (!ymapUpdates.areOthersOnSameVersionAndPage) return; - - // Check if the new application definition is different from the current one - if (isEqual(appDefinition, ymapUpdates.newDefinition)) return; - - // Trigger real-time save with specific options - realtimeSave(props.ymap?.get('appDef').newDefinition, { - skipAutoSave: true, - skipYmapUpdate: true, - currentSessionId: ymapUpdates.currentSessionId, - }); - }); - }; - - //! websocket events do not work - const initEventListeners = () => { - socket?.addEventListener('message', (event) => { - const data = event.data.replace(/^"(.+(?="$))"$/, '$1'); - if (data === 'versionReleased') fetchApp(); - else if (data === 'dataQueriesChanged') { - fetchDataQueries(editingVersion?.id); - } else if (data === 'dataSourcesChanged') { - fetchDataSources(editingVersion?.id); - } - }); - }; - - const $componentDidMount = async () => { - window.addEventListener('message', handleMessage); - - await fetchApp(props.params.pageHandle, true); - await fetchApps(0); - await fetchOrgEnvironmentVariables(); - await fetchOrgEnvironmentConstants(); - initComponentVersioning(); - initRealtimeSave(); - initEventListeners(); - updateEditorState({ - currentSidebarTab: 2, - selectedComponents: [], - scrollOptions: { - container: canvasContainerRef.current, - throttleTime: 30, - threshold: 0, - }, - }); - - getCanvasWidth(); - initEditorWalkThrough(); - }; - - const fetchDataQueries = async (id, selectFirstQuery = false, runQueriesOnAppLoad = false) => { - await useDataQueriesStore - .getState() - .actions.fetchDataQueries(id, selectFirstQuery, runQueriesOnAppLoad, getEditorRef()); - }; - - 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(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(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 getCanvasWidth = () => { - const canvasBoundingRect = document.getElementsByClassName('canvas-area')[0]?.getBoundingClientRect(); - - const _canvasWidth = canvasBoundingRect?.width; - return _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); - - 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: { - ...currentState.globals, - theme: { name: newMode ? 'dark' : 'light' }, - }, - }); - props.switchDarkMode(newMode); - }; - - const handleEvent = (eventName, event, options) => { - return onEvent(getEditorRef(), eventName, event, 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 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) { - setSelectedComponents([{ id, component }], multiSelect); - } - }; - - 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 onVersionRelease = (versionId) => { - useAppVersionStore.getState().actions.updateReleasedVersionId(versionId); - - socket.send( - JSON.stringify({ - event: 'events', - data: { message: 'versionReleased', appId: appId }, - }) - ); - }; - - const computeCanvasBackgroundColor = () => { - 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 = (globalOptions) => { - const copyOfAppDefinition = JSON.parse(JSON.stringify(appDefinition)); - const newAppDefinition = _.cloneDeep(copyOfAppDefinition); - - for (const [key, value] of Object.entries(globalOptions)) { - if (value?.[1]?.a == undefined) newAppDefinition.globalSettings[key] = value; - else { - const hexCode = `${value?.[0]}${decimalToHex(value?.[1]?.a)}`; - newAppDefinition.globalSettings[key] = hexCode; - } - } - - updateEditorState({ - isUpdatingEditorStateInProcess: true, - }); - - appDefinitionChanged(newAppDefinition, { - globalSettings: true, - }); - }; - - const callBack = async (data, startingPageHandle, versionSwitched = false) => { - setWindowTitle(data.name); - useAppVersionStore.getState().actions.updateEditingVersion(data.editing_version); - if (!releasedVersionId || !versionSwitched) { - useAppVersionStore.getState().actions.updateReleasedVersionId(data.current_version_id); - } - - const appVersions = await appEnvironmentService.getVersionsByEnvironment(data?.id); - setAppVersions(appVersions.appVersions); - const currentOrgId = data?.organization_id || data?.organizationId; - - updateState({ - slug: data.slug, - isMaintenanceOn: data?.is_maintenance_on, - organizationId: currentOrgId, - isPublic: data?.is_public, - appName: data?.name, - userId: data?.user_id, - appId: data?.id, - events: data.events, - currentVersionId: data?.editing_version?.id, - app: data, - }); - - const appDefData = buildAppDefinition(data); - - const appJson = appDefData; - const pages = data.pages; - - const startingPageId = pages.filter((page) => page.handle === startingPageHandle)[0]?.id; - const homePageId = !startingPageId || startingPageId === 'null' ? appJson.homePageId : startingPageId; - - const currentpageData = { - handle: appJson.pages[homePageId]?.handle, - name: appJson.pages[homePageId]?.name, - id: homePageId, - variables: {}, - }; - - setCurrentPageId(homePageId); - - useCurrentStateStore.getState().actions.setCurrentState({ - page: currentpageData, - }); - - updateEditorState({ - isLoading: false, - appDefinition: appJson, - isUpdatingEditorStateInProcess: false, - }); - - if (versionSwitched) { - props?.navigate(`/${getWorkspaceId()}/apps/${appId}/${appJson.pages[homePageId]?.handle}`, { - state: { - isSwitchingPage: true, - }, - }); - } - - await useDataSourcesStore.getState().actions.fetchGlobalDataSources(data?.organization_id); - await fetchDataSources(data.editing_version?.id); - await fetchDataQueries(data.editing_version?.id, true, true); - const currentPageEvents = data.events.filter((event) => event.target === 'page' && event.sourceId === homePageId); - - await handleEvent('onPageLoad', currentPageEvents, {}, true); - }; - - const fetchApp = async (startingPageHandle, onMount = false) => { - const _appId = props?.params?.id || props?.params?.slug; - - if (!onMount) { - await appService.fetchApp(_appId).then((data) => callBack(data, startingPageHandle)); - } else { - callBack(app, startingPageHandle); - } - }; - - const setAppDefinitionFromVersion = (appData) => { - const version = appData?.editing_version?.id; - if (version?.id !== editingVersion?.id) { - if (version?.id === currentVersionId) { - updateEditorState({ - canUndo: false, - canRedo: false, - }); - } - - updateEditorState({ - isLoading: true, - }); - - callBack(appData, null, true); - initComponentVersioning(); - } - }; - - const diffToPatches = (diffObj) => { - return Object.keys(diffObj).reduce((patches, path) => { - const value = diffObj[path]; - return [...patches, { path: path.split('.'), value, op: 'replace' }]; - }, []); - }; - - const appDefinitionChanged = async (newDefinition, opts = {}) => { - if (opts?.versionChanged) { - setCurrentPageId(newDefinition.homePageId); - - return new Promise((resolve) => { - updateEditorState({ - isUpdatingEditorStateInProcess: true, - }); - - resolve(); - }); - } - let updatedAppDefinition; - const copyOfAppDefinition = JSON.parse(JSON.stringify(appDefinition)); - - if (opts?.skipYmapUpdate && opts?.currentSessionId !== currentSessionId) { - updatedAppDefinition = newDefinition; - } else { - updatedAppDefinition = produce(copyOfAppDefinition, (draft) => { - if (_.isEmpty(draft)) return; - - if (opts?.containerChanges || opts?.componentDefinitionChanged) { - const currentPageComponents = newDefinition.pages[currentPageId]?.components; - - draft.pages[currentPageId].components = currentPageComponents; - } - - if (opts?.pageDefinitionChanged) { - draft.pages = newDefinition.pages; - } - - if (opts?.homePageChanged) { - draft.homePageId = newDefinition.homePageId; - } - - if (opts?.generalAppDefinitionChanged || opts?.globalSettings || isEmpty(opts)) { - Object.assign(draft, newDefinition); - } - }); - } - - const diffPatches = diff(appDefinition, updatedAppDefinition); - - const inversePatches = diff(updatedAppDefinition, appDefinition); - const shouldUpdate = !_.isEmpty(diffPatches) && !isEqual(appDefinitionDiff, diffPatches); - - if (shouldUpdate) { - const undoPatches = diffToPatches(inversePatches); - - if ( - opts?.componentAdded || - opts?.componentDefinitionChanged || - opts?.componentDeleted || - opts?.containerChanges - ) { - setUndoStack((prev) => [...prev, undoPatches]); - setOptsStack((prev) => ({ ...prev, undo: [...prev.undo, opts] })); - } - - updateAppDefinitionDiff(diffPatches); - - const isParamDiffFromTableColumn = opts?.containerChanges - ? isParamFromTableColumn(diffPatches, updatedAppDefinition) - : false; - - if (isParamDiffFromTableColumn) { - opts.componentDefinitionChanged = true; - opts.isParamFromTableColumn = true; - delete opts.containerChanges; - } - - updateState({ - appDiffOptions: opts, - }); - - let updatingEditorStateInProcess = true; - - if (opts?.widgetMovedWithKeyboard === true) { - updatingEditorStateInProcess = false; - } - - updateEditorState({ - isUpdatingEditorStateInProcess: updatingEditorStateInProcess, - appDefinition: updatedAppDefinition, - }); - } - - if (config.ENABLE_MULTIPLAYER_EDITING && !opts?.skipYmapUpdate && opts?.currentSessionId !== currentSessionId) { - props.ymap?.set('appDef', { - newDefinition: updatedAppDefinition, - editingVersionId: editingVersion?.id, - currentSessionId, - areOthersOnSameVersionAndPage, - }); - } - }; - - const cloneEventsForClonedComponents = (componentUpdateDiff, operation, componentMap) => { - function getKeyFromComponentMap(componentMap, newItem) { - for (const key in componentMap) { - if (componentMap.hasOwnProperty(key) && componentMap[key] === newItem) { - return key; - } - } - return null; - } - - if (operation !== 'create') return; - - const newComponentIds = Object.keys(componentUpdateDiff); - - newComponentIds.forEach((componentId) => { - const sourceComponentId = getKeyFromComponentMap(componentMap, componentId); - if (!sourceComponentId) return; - - appVersionService - .findAllEventsWithSourceId(appId, currentVersionId, sourceComponentId) - .then((componentEvents) => { - if (!componentEvents) return; - componentEvents.forEach((event) => { - const newEvent = { - event: { - ...event?.event, - }, - eventType: event?.target, - attachedTo: componentMap[event?.sourceId], - index: event?.index, - }; - - createAppVersionEventHandlers(newEvent); - }); - }); - }); - }; - - const saveEditingVersion = (isUserSwitchedVersion = false) => { - if (isVersionReleased && !isUserSwitchedVersion) { - updateEditorState({ - isUpdatingEditorStateInProcess: false, - }); - } else if (!isEmpty(editingVersion)) { - //! The computeComponentPropertyDiff function manages the calculation of differences in table columns by requiring complete column data. Without this complete data, the resulting JSON structure may be incorrect. - const paramDiff = computeComponentPropertyDiff(appDefinitionDiff, appDefinition, appDiffOptions); - const updateDiff = computeAppDiff(paramDiff, currentPageId, appDiffOptions, currentLayout); - - if (updateDiff['error']) { - const platform = navigator?.userAgentData?.platform || navigator?.platform || 'unknown'; - const isPlatformMac = platform.toLowerCase().indexOf('mac') > -1; - const toastMessage = `Unable to save changes! ${isPlatformMac ? '(⌘ + Z to undo)' : '(ctrl + Z to undo)'}`; - - toast(toastMessage, { - icon: '🚫', - }); - - return updateEditorState({ - saveError: true, - isUpdatingEditorStateInProcess: false, - }); - } - - updateAppVersion(appId, editingVersion?.id, currentPageId, updateDiff, isUserSwitchedVersion) - .then(() => { - const _editingVersion = { - ...editingVersion, - ...{ definition: appDefinition }, - }; - useAppVersionStore.getState().actions.updateEditingVersion(_editingVersion); - - if ( - updateDiff?.type === 'components' && - updateDiff?.operation === 'delete' && - !appDiffOptions?.componentCut - ) { - const appEvents = Array.isArray(events) && events.length > 0 ? JSON.parse(JSON.stringify(events)) : []; - - const updatedEvents = appEvents.filter((event) => { - return !updateDiff?.updateDiff.includes(event.sourceId); - }); - - updateState({ - events: updatedEvents, - }); - } - - updateEditorState({ - saveError: false, - isUpdatingEditorStateInProcess: false, - }); - }) - .catch(() => { - updateEditorState({ - saveError: true, - isUpdatingEditorStateInProcess: false, - }); - toast.error('App could not save.'); - }) - .finally(() => { - updateState({ - appDiffOptions: {}, - }); - }) - .finally(() => { - if (appDiffOptions?.cloningComponent) { - cloneEventsForClonedComponents( - updateDiff.updateDiff, - updateDiff.operation, - appDiffOptions?.cloningComponent - ); - } - }); - } - - updateEditorState({ - saveError: false, - isUpdatingEditorStateInProcess: false, - }); - }; - - const realtimeSave = debounce(appDefinitionChanged, 500); - const autoSave = debounce(saveEditingVersion, 200); - - function handlePaths(prevPatch, path = [], appJSON) { - const paths = [...path]; - - for (let key in prevPatch) { - const type = typeof prevPatch[key]; - - if (type === 'object') { - handlePaths(prevPatch[key], [...paths, key], appJSON); - } else { - const currentpath = [...paths, key].join('.'); - _.update(appJSON, currentpath, () => prevPatch[key]); - } - } - } - function removeUndefined(obj) { - Object.keys(obj).forEach((key) => { - if (obj[key] && typeof obj[key] === 'object') removeUndefined(obj[key]); - else if (obj[key] === undefined) delete obj[key]; - }); - - return obj; - } - - const handleUndo = () => { - if (canUndo) { - const patchesToUndo = undoStack[undoStack.length - 1]; - - const updatedAppDefinition = JSON.parse(JSON.stringify(appDefinition)); - - handlePaths(patchesToUndo[0]?.value, [...patchesToUndo[0].path], updatedAppDefinition); - - removeUndefined(updatedAppDefinition); - - const _diffPatches = diff(updatedAppDefinition, appDefinition); - const undoDiff = diff(appDefinition, updatedAppDefinition); - - updateAppDefinitionDiff(undoDiff); - setUndoStack((prev) => prev.slice(0, prev.length - 1)); - setRedoStack((prev) => [...prev, diffToPatches(_diffPatches)]); - - let undoOpts = optsStack.undo[optsStack.undo.length - 1]; - - if (undoOpts?.componentDeleted) { - undoOpts = { - componentAdded: true, - }; - } else if (undoOpts?.componentAdded) { - undoOpts = { - componentDeleted: true, - }; - } - - updateState({ - appDiffOptions: undoOpts, - }); - - setOptsStack((prev) => ({ - ...prev, - undo: [...prev.undo.slice(0, prev.undo.length - 1)], - redo: [...prev.redo, optsStack.undo[optsStack.undo.length - 1]], - })); - - updateEditorState({ - appDefinition: updatedAppDefinition, - currentSidebarTab: 2, - isUpdatingEditorStateInProcess: true, - }); - } - }; - - const handleRedo = () => { - if (canRedo) { - const patchesToRedo = redoStack[redoStack.length - 1]; - - const updatedAppDefinition = JSON.parse(JSON.stringify(appDefinition)); - - handlePaths(patchesToRedo[0]?.value, [...patchesToRedo[0].path], updatedAppDefinition); - removeUndefined(updatedAppDefinition); - const _diffPatches = diff(updatedAppDefinition, appDefinition); - const redoDiff = diff(appDefinition, updatedAppDefinition); - updateAppDefinitionDiff(redoDiff); - setRedoStack((prev) => prev.slice(0, prev.length - 1)); - setUndoStack((prev) => [...prev, diffToPatches(_diffPatches)]); - - updateState({ - appDiffOptions: optsStack.redo[optsStack.redo.length - 1], - }); - - setOptsStack((prev) => ({ - ...prev, - undo: [...prev.undo, appDiffOptions], - redo: [...prev.redo.slice(0, prev.redo.length - 1)], - })); - - updateEditorState({ - appDefinition: updatedAppDefinition, - isUpdatingEditorStateInProcess: true, - }); - } - }; - - useEffect(() => { - updateEditorState({ - canUndo: undoStack.length > 0, - canRedo: redoStack.length > 0, - }); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [JSON.stringify(undoStack), JSON.stringify(redoStack)]); - - const componentDefinitionChanged = (componentDefinition, props) => { - if (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; - - updateEditorState({ - isUpdatingEditorStateInProcess: true, - }); - - const diffPatches = diff(appDefinition, updatedAppDefinition); - - if (!isEmpty(diffPatches)) { - appDefinitionChanged(updatedAppDefinition, { skipAutoSave: true, componentDefinitionChanged: true, ...props }); - } - } - }; - - const removeComponent = (componentId) => { - if (!isVersionReleased) { - let newDefinition = cloneDeep(appDefinition); - - let childComponents = []; - - if (newDefinition.pages[currentPageId].components?.[componentId].component.component === 'Tabs') { - childComponents = Object.keys(newDefinition.pages[currentPageId].components).filter((key) => - newDefinition.pages[currentPageId].components[key].component.parent?.startsWith(componentId) - ); - } else { - childComponents = Object.keys(newDefinition.pages[currentPageId].components).filter( - (key) => newDefinition.pages[currentPageId].components[key].component.parent === componentId - ); - } - - childComponents.forEach((componentId) => { - delete newDefinition.pages[currentPageId].components[componentId]; - }); - - delete newDefinition.pages[currentPageId].components[componentId]; - 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, { - componentDefinitionChanged: true, - componentDeleted: true, - }); - 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[currentLayout].top; - let left = newComponents[selectedComponent.id].layouts[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[currentLayout].top = top; - newComponents[selectedComponent.id].layouts[currentLayout].left = left; - } - - _appDefinition.pages[currentPageId].components = newComponents; - - appDefinitionChanged(_appDefinition, { containerChanges: true, widgetMovedWithKeyboard: true }); - }; - - const copyComponents = () => - cloneComponents(selectedComponents, appDefinition, currentPageId, appDefinitionChanged, false); - - const cutComponents = () => { - if (isVersionReleased) { - useAppVersionStore.getState().actions.enableReleasedVersionPopupState(); - - return; - } - - cloneComponents(selectedComponents, appDefinition, currentPageId, appDefinitionChanged, false, true); - }; - - const cloningComponents = () => { - if (isVersionReleased) { - useAppVersionStore.getState().actions.enableReleasedVersionPopupState(); - - return; - } - cloneComponents(selectedComponents, appDefinition, currentPageId, appDefinitionChanged, true, false); - }; - - const handleEditorEscapeKeyPress = () => { - if (selectedComponents?.length > 0) { - updateEditorState({ - selectedComponents: [], - }); - handleInspectorView(); - } - }; - - const removeComponents = () => { - if (!isVersionReleased && selectedComponents?.length > 1) { - let newDefinition = cloneDeep(appDefinition); - - removeSelectedComponent(currentPageId, newDefinition, selectedComponents, appDefinitionChanged); - 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: '🗑️', - }); - } - - handleInspectorView(); - } else if (isVersionReleased) { - useAppVersionStore.getState().actions.enableReleasedVersionPopupState(); - } - }; - - //Page actions - const renamePage = (pageId, newName) => { - if (Object.entries(appDefinition.pages).some(([pId, { name }]) => newName === name && pId !== pageId)) { - return toast.error('Page name already exists'); - } - if (newName.trim().length === 0) { - toast.error('Page name cannot be empty'); - return; - } - - setCurrentPageId(pageId); - - const copyOfAppDefinition = JSON.parse(JSON.stringify(appDefinition)); - - copyOfAppDefinition.pages[pageId].name = newName; - - appDefinitionChanged(copyOfAppDefinition, { pageDefinitionChanged: true }); - }; - - const addNewPage = ({ name, handle }) => { - // check for unique page handles - const pageExists = Object.values(appDefinition.pages).some((page) => page.name === name); - - if (pageExists) { - toast.error('Page name already exists'); - return; - } - - if (name.length > 32) { - toast.error('Page name cannot be more than 32 characters'); - return; - } - - const pageHandles = Object.values(appDefinition.pages).map((page) => page.handle); - - let newHandle = handle; - // If handle already exists, finds a unique handle by incrementing a number until it is not found in the array of existing page handles. - for (let handleIndex = 1; pageHandles.includes(newHandle); handleIndex++) { - newHandle = `${handle}-${handleIndex}`; - } - - const copyOfAppDefinition = JSON.parse(JSON.stringify(appDefinition)); - const newPageId = uuid(); - - copyOfAppDefinition.pages[newPageId] = { - id: newPageId, - name, - handle: newHandle, - components: {}, - index: Object.keys(copyOfAppDefinition.pages).length + 1, - }; - - setCurrentPageId(newPageId); - setHoveredComponent(null); - updateEditorState({ - currentSidebarTab: 2, - selectedComponents: [], - }); - - appDefinitionChanged(copyOfAppDefinition, { - pageDefinitionChanged: true, - addNewPage: true, - switchPage: true, - pageId: newPageId, - }); - props?.navigate(`/${getWorkspaceId()}/apps/${appId}/${newHandle}`, { - state: { - isSwitchingPage: true, - }, - }); - - const { globals: existingGlobals } = currentState; - - const page = { - id: newPageId, - name, - handle, - variables: copyOfAppDefinition.pages[newPageId]?.variables ?? {}, - }; - - const globals = { - ...existingGlobals, - }; - useCurrentStateStore.getState().actions.setCurrentState({ globals, page }); - }; - - const switchPage = (pageId, queryParams = []) => { - // This are fetched from store to handle runQueriesOnAppLoad - const currentPageId = useEditorStore.getState().currentPageId; - const appDefinition = useEditorStore.getState().appDefinition; - const appId = useAppDataStore.getState()?.appId; - const pageHandle = getCurrentState().pageHandle; - - if (currentPageId === pageId && pageHandle === appDefinition?.pages[pageId]?.handle) { - return; - } - const { name, handle } = appDefinition.pages[pageId]; - - if (!name || !handle) return; - const copyOfAppDefinition = JSON.parse(JSON.stringify(appDefinition)); - const queryParamsString = queryParams.map(([key, value]) => `${key}=${value}`).join('&'); - - props?.navigate(`/${getWorkspaceId()}/apps/${appId}/${handle}?${queryParamsString}`, { - state: { - isSwitchingPage: true, - }, - }); - - const { globals: existingGlobals } = currentState; - - const page = { - id: pageId, - name, - handle, - variables: copyOfAppDefinition.pages[pageId]?.variables ?? {}, - }; - - const globals = { - ...existingGlobals, - urlparams: JSON.parse(JSON.stringify(queryString.parse(queryParamsString))), - }; - useCurrentStateStore.getState().actions.setCurrentState({ globals, page }); - - setCurrentPageId(pageId); - handleInspectorView(); - - const currentPageEvents = events.filter((event) => event.target === 'page' && event.sourceId === page.id); - - handleEvent('onPageLoad', currentPageEvents); - }; - - const deletePageRequest = (pageId, isHomePage = false, pageName = '') => { - setShowPageDeletionConfirmation({ - isOpen: true, - pageId, - isHomePage, - pageName, - }); - }; - - const cancelDeletePageRequest = () => { - setShowPageDeletionConfirmation({ - isOpen: false, - pageId: null, - isHomePage: false, - pageName: null, - }); - }; - - const executeDeletepageRequest = () => { - const pageId = showPageDeletionConfirmation.pageId; - const isHomePage = showPageDeletionConfirmation.isHomePage; - if (Object.keys(appDefinition.pages).length === 1) { - toast.error('You cannot delete the only page in your app.'); - return; - } - - setIsDeletingPage({ - isDeletingPage: true, - }); - - const copyOfAppDefinition = JSON.parse(JSON.stringify(appDefinition)); - - const toBeDeletedPage = copyOfAppDefinition.pages[pageId]; - - const newAppDefinition = { - ...copyOfAppDefinition, - pages: omit(copyOfAppDefinition.pages, pageId), - }; - - const newCurrentPageId = isHomePage ? Object.keys(copyOfAppDefinition.pages)[0] : copyOfAppDefinition.homePageId; - - setCurrentPageId(newCurrentPageId); - updateEditorState({ - isUpdatingEditorStateInProcess: true, - }); - setIsDeletingPage(false); - - appDefinitionChanged(newAppDefinition, { - pageDefinitionChanged: true, - deletePageRequest: true, - }); - - toast.success(`${toBeDeletedPage.name} page deleted.`); - - switchPage(newCurrentPageId); - }; - - const disableEnablePage = ({ pageId, isDisabled }) => { - updateEditorState({ - isUpdatingEditorStateInProcess: true, - }); - - const copyOfAppDefinition = JSON.parse(JSON.stringify(appDefinition)); - - const newAppDefinition = _.cloneDeep(copyOfAppDefinition); - - newAppDefinition.pages[pageId].disabled = isDisabled ?? false; - - switchPage(pageId); - appDefinitionChanged(newAppDefinition, { - pageDefinitionChanged: true, - }); - }; - - const hidePage = (pageId) => { - updateEditorState({ - isUpdatingEditorStateInProcess: true, - }); - - const copyOfAppDefinition = JSON.parse(JSON.stringify(appDefinition)); - - const newAppDefinition = _.cloneDeep(copyOfAppDefinition); - - newAppDefinition.pages[pageId].hidden = true; - - switchPage(pageId); - appDefinitionChanged(newAppDefinition, { - pageDefinitionChanged: true, - }); - }; - - const unHidePage = (pageId) => { - updateEditorState({ - isUpdatingEditorStateInProcess: true, - }); - - const copyOfAppDefinition = JSON.parse(JSON.stringify(appDefinition)); - - const newAppDefinition = _.cloneDeep(copyOfAppDefinition); - - newAppDefinition.pages[pageId].hidden = false; - switchPage(pageId); - appDefinitionChanged(newAppDefinition, { - pageDefinitionChanged: true, - }); - }; - - const clonePage = (pageId) => { - setIsSaving(true); - appVersionService - .clonePage(appId, editingVersion?.id, pageId) - .then((data) => { - const copyOfAppDefinition = JSON.parse(JSON.stringify(appDefinition)); - - const pages = data.pages.reduce((acc, page) => { - const currentComponents = buildComponentMetaDefinition(_.cloneDeep(page?.components)); - - page.components = currentComponents; - - acc[page.id] = page; - - return acc; - }, {}); - - const newAppDefinition = { - ...copyOfAppDefinition, - pages: { - ...copyOfAppDefinition.pages, - ...pages, - }, - }; - updateState({ - events: data.events, - }); - appDefinitionChanged(newAppDefinition); - }) - .finally(() => setIsSaving(false)); - }; - - const updateHomePage = (pageId) => { - updateEditorState({ - isUpdatingEditorStateInProcess: true, - }); - - const copyOfAppDefinition = JSON.parse(JSON.stringify(appDefinition)); - - const newAppDefinition = _.cloneDeep(copyOfAppDefinition); - - newAppDefinition.homePageId = pageId; - - appDefinitionChanged(newAppDefinition, { - homePageChanged: true, - }); - }; - - const updatePageHandle = (pageId, newHandle) => { - const copyOfAppDefinition = JSON.parse(JSON.stringify(appDefinition)); - - updateEditorState({ - isUpdatingEditorStateInProcess: true, - }); - - const pageExists = Object.values(copyOfAppDefinition.pages).some((page) => page.handle === newHandle); - - if (pageExists) { - toast.error('Page with same handle already exists'); - return; - } - - if (newHandle.trim().length === 0) { - toast.error('Page handle cannot be empty'); - return; - } - - const newDefinition = _.cloneDeep(copyOfAppDefinition); - - newDefinition.pages[pageId].handle = newHandle; - - appDefinitionChanged(newDefinition, { - pageDefinitionChanged: true, - }); - }; - - const updateOnSortingPages = (newSortedPages) => { - const copyOfAppDefinition = JSON.parse(JSON.stringify(appDefinition)); - const pagesObj = newSortedPages.reduce((acc, page, index) => { - acc[page.id] = { - ...page, - index: index + 1, - }; - return acc; - }, {}); - - const newAppDefinition = _.cloneDeep(copyOfAppDefinition); - - newAppDefinition.pages = pagesObj; - - appDefinitionChanged(newAppDefinition, { - pageDefinitionChanged: true, - pageSortingChanged: true, - }); - }; - - const showHideViewerNavigation = () => { - const copyOfAppDefinition = JSON.parse(JSON.stringify(appDefinition)); - const newAppDefinition = _.cloneDeep(copyOfAppDefinition); - console.log(newAppDefinition.showViewerNavigation, 'Bedore'); - newAppDefinition.showViewerNavigation = !newAppDefinition.showViewerNavigation; - console.log(newAppDefinition.showViewerNavigation, 'newAppDefinition.showViewerNavigation'); - appDefinitionChanged(newAppDefinition, { - generalAppDefinitionChanged: true, - }); - }; - - useEffect(() => { - const previewQuery = queryString.stringify({ version: editingVersion?.name }); - const appVersionPreviewLink = editingVersion - ? `/applications/${slug || appId}/${currentState.page.handle}${ - !_.isEmpty(previewQuery) ? `?${previewQuery}` : '' - }` - : ''; - - setAppPreviewLink(appVersionPreviewLink); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [slug, currentVersionId]); - - const deviceWindowWidth = 450; - - const editorRef = { - appDefinition: appDefinition, - queryConfirmationList: queryConfirmationList, - updateQueryConfirmationList: updateQueryConfirmationList, - navigate: props.navigate, - switchPage: switchPage, - currentPageId: currentPageId, - }; - - if (isLoading) { - return ( -
-
-
-
-
-
- -
- {Array.from(Array(4)).map((_item, index) => ( - - ))} -
- -
- - -
-
-
-
-
- ); - } - - //! Need to move conditionally rendered components to separate components => Widget Manger or Widget Inspector - const shouldrenderWidgetInspector = - currentSidebarTab === 1 && - selectedComponents?.length === 1 && - !isEmpty(appDefinition?.pages[currentPageId]?.components) && - !isEmpty(appDefinition?.pages[currentPageId]?.components[selectedComponents[0]?.id]); - - return ( -
- 0} - message={`Do you want to run this query - ${queryConfirmationList[0]?.queryName}?`} - onConfirm={(queryConfirmationData) => onQueryConfirmOrCancel(editorRef, queryConfirmationData, true)} - onCancel={() => onQueryConfirmOrCancel(editorRef, queryConfirmationList[0])} - queryConfirmationData={queryConfirmationList[0]} - darkMode={props.darkMode} - key={queryConfirmationList[0]?.queryName} - /> - executeDeletepageRequest()} - onCancel={() => cancelDeletePageRequest()} - darkMode={props.darkMode} - /> - {isVersionReleased && } - - - -
- handleRunQuery(queryId, queryName)} - ref={dataSourceModalRef} - isSaving={isUpdatingEditorStateInProcess} - currentPageId={currentPageId} - addNewPage={addNewPage} - switchPage={switchPage} - deletePage={deletePageRequest} - renamePage={renamePage} - clonePage={clonePage} - hidePage={hidePage} - unHidePage={unHidePage} - disableEnablePage={disableEnablePage} - updateHomePage={updateHomePage} - updatePageHandle={updatePageHandle} - showHideViewerNavigationControls={showHideViewerNavigation} - updateOnSortingPages={updateOnSortingPages} - setEditorMarginLeft={handleEditorMarginLeftChange} - isMaintenanceOn={isMaintenanceOn} - toggleAppMaintenance={toggleAppMaintenance} - /> - {!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)} - /> - - )} -
-
-
- - -
-
- - - {shouldrenderWidgetInspector ? ( -
- -
- ) : ( - - )} -
- {config.COMMENT_FEATURE_ENABLE && showComments && ( - - )} -
-
-
-
- ); -}; - -export const EditorFunc = withTranslation()(withRouter(EditorComponent)); diff --git a/frontend/src/Editor/RealtimeEditor.jsx b/frontend/src/Editor/RealtimeEditor.jsx index 48ab79f0b9..6fa90e0996 100644 --- a/frontend/src/Editor/RealtimeEditor.jsx +++ b/frontend/src/Editor/RealtimeEditor.jsx @@ -3,7 +3,7 @@ import React from 'react'; import config from 'config'; import { RoomProvider } from '@y-presence/react'; import Spinner from '@/_ui/Spinner'; -import { EditorFunc } from '@/Editor'; //!Update: EditorFunc to Editor.jsx +import { Editor } from '@/Editor'; const Y = require('yjs'); const psl = require('psl'); const { WebsocketProvider } = require('y-websocket'); @@ -65,7 +65,7 @@ export const RealtimeEditor = (props) => { return ( - + ); }; diff --git a/frontend/src/Editor/index.js b/frontend/src/Editor/index.js index a5bdf8c189..8e539b2b32 100644 --- a/frontend/src/Editor/index.js +++ b/frontend/src/Editor/index.js @@ -2,4 +2,3 @@ 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 1e9c8b6e82..3c58d31775 100644 --- a/frontend/src/_helpers/appUtils.js +++ b/frontend/src/_helpers/appUtils.js @@ -1471,6 +1471,35 @@ export const cloneComponents = ( }); }; +// const getAllChildComponents = (allComponents, parentId) => { +// const childComponents = []; + +// Object.keys(allComponents).forEach((componentId) => { +// const componentParentId = allComponents[componentId].component?.parent; + +// const isParentTabORCalendar = +// allComponents[parentId]?.component?.component === 'Tabs' || +// allComponents[parentId]?.component?.component === 'Calendar'; + +// if (componentParentId && isParentTabORCalendar) { +// const childComponent = allComponents[componentId]; +// const childTabId = componentParentId.split('-').at(-1); +// if (componentParentId === `${parentId}-${childTabId}`) { +// childComponent.componentId = componentId; +// childComponents.push(childComponent); +// } +// } + +// if (componentParentId === parentId) { +// const childComponent = allComponents[componentId]; +// childComponent.componentId = componentId; +// childComponents.push(childComponent); +// } +// }); + +// return childComponents; +// }; + const getAllChildComponents = (allComponents, parentId) => { const childComponents = []; @@ -1487,6 +1516,10 @@ const getAllChildComponents = (allComponents, parentId) => { if (componentParentId === `${parentId}-${childTabId}`) { childComponent.componentId = componentId; childComponents.push(childComponent); + + // Recursively find children of the current child component + const childrenOfChild = getAllChildComponents(allComponents, componentId); + childComponents.push(...childrenOfChild); } } @@ -1494,6 +1527,10 @@ const getAllChildComponents = (allComponents, parentId) => { const childComponent = allComponents[componentId]; childComponent.componentId = componentId; childComponents.push(childComponent); + + // Recursively find children of the current child component + const childrenOfChild = getAllChildComponents(allComponents, componentId); + childComponents.push(...childrenOfChild); } }); @@ -1598,6 +1635,8 @@ export const addComponents = ( }; finalComponents[newComponentId] = newComponent; + + // const doesComponentHaveChildren = getAllChildComponents }); if (currentPageId === pageId) {