import React from 'react';
import {
authenticationService,
orgEnvironmentVariableService,
orgEnvironmentConstantService,
dataqueryService,
appService,
} from '@/_services';
import { DndProvider } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend';
import { Container } from './Container';
import { Confirm } from './Viewer/Confirm';
import { ViewerNavigation } from './Viewer/ViewerNavigation';
import {
onComponentOptionChanged,
onComponentOptionsChanged,
onComponentClick,
onQueryConfirmOrCancel,
onEvent,
runQuery,
computeComponentState,
buildAppDefinition,
} from '@/_helpers/appUtils';
import queryString from 'query-string';
import ViewerLogoIcon from './Icons/viewer-logo.svg';
import { DataSourceTypes } from './DataSourceManager/SourceComponents';
import { resolveReferences, isQueryRunnable } from '@/_helpers/utils';
import { withTranslation } from 'react-i18next';
import _ from 'lodash';
import { Navigate } from 'react-router-dom';
import Spinner from '@/_ui/Spinner';
import { withRouter } from '@/_hoc/withRouter';
import { useEditorStore } from '@/_stores/editorStore';
import { setCookie } from '@/_helpers/cookie';
import { useDataQueriesStore } from '@/_stores/dataQueriesStore';
import { useCurrentStateStore } from '@/_stores/currentStateStore';
import { shallow } from 'zustand/shallow';
import { useAppDataActions, useAppDataStore } from '@/_stores/appDataStore';
import { getPreviewQueryParams, redirectToErrorPage } from '@/_helpers/routes';
import { ERROR_TYPES } from '@/_helpers/constants';
import { camelizeKeys } from 'humps';
class ViewerComponent extends React.Component {
constructor(props) {
super(props);
const deviceWindowWidth = window.screen.width - 5;
const slug = this.props.params.slug;
this.subscription = null;
this.state = {
slug,
deviceWindowWidth,
currentUser: null,
isLoading: true,
users: null,
appDefinition: { pages: {} },
isAppLoaded: false,
pages: {},
homepage: null,
};
}
getViewerRef() {
return {
appDefinition: this.state.appDefinition,
queryConfirmationList: this.props.queryConfirmationList,
updateQueryConfirmationList: this.updateQueryConfirmationList,
navigate: this.props.navigate,
switchPage: this.switchPage,
currentPageId: this.state.currentPageId,
};
}
setStateForApp = (data, byAppSlug = false) => {
const appDefData = buildAppDefinition(data);
if (byAppSlug) {
appDefData.globalSettings = data.globalSettings;
appDefData.homePageId = data.homePageId;
appDefData.showViewerNavigation = data.showViewerNavigation;
}
this.setState({
app: data,
isLoading: false,
isAppLoaded: true,
appDefinition: { ...appDefData },
pages: appDefData.pages,
});
};
setStateForContainer = async (data, appVersionId) => {
const appDefData = buildAppDefinition(data);
const currentUser = this.state.currentUser;
let userVars = {};
if (currentUser) {
userVars = {
email: currentUser.email,
firstName: currentUser.first_name,
lastName: currentUser.last_name,
groups: authenticationService.currentSessionValue?.group_permissions.map((group) => group.group),
};
}
let mobileLayoutHasWidgets = false;
if (this.props.currentLayout === 'mobile') {
const currentComponents = appDefData.pages[appDefData.homePageId].components;
mobileLayoutHasWidgets =
Object.keys(currentComponents).filter((componentId) => currentComponents[componentId]['layouts']['mobile'])
.length > 0;
}
let queryState = {};
let dataQueries = [];
if (appVersionId) {
const { data_queries } = await dataqueryService.getAll(appVersionId);
dataQueries = data_queries;
} else {
dataQueries = data.data_queries;
}
const queryConfirmationList = [];
if (dataQueries.length > 0) {
dataQueries.forEach((query) => {
if (query?.options && query?.options?.requestConfirmation && query?.options?.runOnPageLoad) {
queryConfirmationList.push({ queryId: query.id, queryName: query.name });
}
if (query.pluginId || query?.plugin?.id) {
const exposedVariables =
query.plugin?.manifestFile?.data?.source?.exposedVariables ||
query.plugin?.manifest_file?.data?.source?.exposed_variables;
queryState[query.name] = {
...exposedVariables,
...this.props.currentState.queries[query.name],
};
} else {
const dataSourceTypeDetail = DataSourceTypes.find((source) => source.kind === query.kind);
queryState[query.name] = {
...dataSourceTypeDetail.exposedVariables,
...this.props.currentState.queries[query.name],
};
}
});
}
if (queryConfirmationList.length !== 0) {
this.updateQueryConfirmationList(queryConfirmationList);
}
const variables = await this.fetchOrgEnvironmentVariables(data.slug, data.is_public);
const constants = await this.fetchOrgEnvironmentConstants(data.slug, data.is_public);
const pages = data.pages;
const homePageId = appVersionId ? data.editing_version.homePageId : data?.homePageId;
const startingPageHandle = this.props?.params?.pageHandle;
const currentPageId = pages.filter((page) => page.handle === startingPageHandle)[0]?.id ?? homePageId;
const currentPage = pages.find((page) => page.id === currentPageId);
useDataQueriesStore.getState().actions.setDataQueries(dataQueries);
this.props.setCurrentState({
queries: queryState,
components: {},
globals: {
currentUser: userVars, // currentUser is updated in setupViewer function as well
theme: { name: this.props.darkMode ? 'dark' : 'light' },
urlparams: JSON.parse(JSON.stringify(queryString.parse(this.props.location.search))),
mode: {
value: this.state.slug ? 'view' : 'preview',
},
},
variables: {},
page: {
id: currentPage.id,
handle: currentPage.handle,
name: currentPage.name,
variables: {},
},
...variables,
...constants,
});
useEditorStore.getState().actions.toggleCurrentLayout(mobileLayoutHasWidgets ? 'mobile' : 'desktop');
this.props.updateState({ events: data.events ?? [] });
this.setState(
{
currentUser,
currentSidebarTab: 2,
canvasWidth:
this.props.currentLayout === 'desktop'
? '100%'
: mobileLayoutHasWidgets
? `${this.state.deviceWindowWidth}px`
: '1292px',
selectedComponent: null,
dataQueries: dataQueries,
currentPageId: currentPage.id,
homepage: appDefData?.pages?.[this.state.appDefinition?.homePageId]?.handle,
events: data.events ?? [],
},
() => {
const components = appDefData?.pages[currentPageId]?.components || {};
computeComponentState(components).then(async () => {
this.setState({ initialComputationOfStateDone: true, defaultComponentStateComputed: true });
console.log('Default component state computed and set');
this.runQueries(dataQueries);
const currentPageEvents = this.state.events.filter(
(event) => event.target === 'page' && event.sourceId === this.state.currentPageId
);
await this.handleEvent('onPageLoad', currentPageEvents);
});
}
);
};
runQueries = (data_queries) => {
data_queries.forEach((query) => {
if (query.options.runOnPageLoad && isQueryRunnable(query)) {
runQuery(this.getViewerRef(), query.id, query.name, undefined, 'view');
}
});
};
fetchOrgEnvironmentConstants = async (slug, isPublic) => {
const orgConstants = {};
let variablesResult;
if (!isPublic) {
const { constants } = await orgEnvironmentConstantService.getAll();
variablesResult = constants;
} else {
const { constants } = await orgEnvironmentConstantService.getConstantsFromPublicApp(slug);
variablesResult = constants;
}
if (variablesResult && Array.isArray(variablesResult)) {
variablesResult.map((constant) => {
const constantValue = constant.values.find((value) => value.environmentName === 'production')['value'];
orgConstants[constant.name] = constantValue;
});
return {
constants: orgConstants,
};
}
return { constants: {} };
};
fetchOrgEnvironmentVariables = async (slug, isPublic) => {
const variables = {
client: {},
server: {},
};
let variablesResult;
if (!isPublic) {
variablesResult = await orgEnvironmentVariableService.getVariables();
} else {
variablesResult = await orgEnvironmentVariableService.getVariablesFromPublicApp(slug);
}
variablesResult.variables.map((variable) => {
variables[variable.variable_type][variable.variable_name] =
variable.variable_type === 'server' ? 'HiddenEnvironmentVariable' : variable.value;
});
return variables;
};
loadApplicationBySlug = (slug, authentication_failed = false) => {
appService
.fetchAppBySlug(slug)
.then((data) => {
const isAppPublic = data?.is_public;
if (authentication_failed && !isAppPublic) {
return redirectToErrorPage(ERROR_TYPES.URL_UNAVAILABLE, {});
}
this.setStateForApp(data, true);
this.setStateForContainer(data);
this.setWindowTitle(data.name);
})
.catch((error) => {
this.setState({
isLoading: false,
});
if (error?.statusCode === 404) {
/* User is not authenticated. but the app url is wrong */
redirectToErrorPage(ERROR_TYPES.INVALID);
} else if (error?.statusCode === 403) {
redirectToErrorPage(ERROR_TYPES.RESTRICTED);
} else if (error?.statusCode !== 401) {
redirectToErrorPage(ERROR_TYPES.UNKNOWN);
}
});
};
loadApplicationByVersion = (appId, versionId) => {
appService
.fetchAppByVersion(appId, versionId)
.then((data) => {
this.setStateForApp(data);
this.setStateForContainer(data, versionId);
})
.catch(() => {
this.setState({
isLoading: false,
});
});
};
updateQueryConfirmationList = (queryConfirmationList) =>
useEditorStore.getState().actions.updateQueryConfirmationList(queryConfirmationList);
setupViewer() {
this.subscription = authenticationService.currentSession.subscribe((currentSession) => {
const slug = this.props.params.slug;
const appId = this.props.id;
const versionId = this.props.versionId;
if (currentSession?.load_app && slug) {
if (currentSession?.group_permissions) {
useAppDataStore.getState().actions.setAppId(appId);
const currentUser = currentSession.current_user;
const userVars = {
email: currentUser.email,
firstName: currentUser.first_name,
lastName: currentUser.last_name,
groups: currentSession?.group_permissions?.map((group) => group.group),
};
this.props.setCurrentState({
globals: {
...this.props.currentState.globals,
currentUser: userVars, // currentUser is updated in setStateForContainer function as well
},
});
this.setState({
currentUser,
userVars,
versionId,
});
versionId ? this.loadApplicationByVersion(appId, versionId) : this.loadApplicationBySlug(slug);
} else if (currentSession?.authentication_failed) {
this.loadApplicationBySlug(slug, true);
}
}
this.setState({ isLoading: false });
});
}
/**
*
* 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) => {
const { data } = event;
if (data?.type === 'redirectTo') {
const redirectCookie = data?.payload['redirectPath'];
setCookie('redirectPath', redirectCookie, 1);
}
};
componentDidMount() {
this.setupViewer();
const isMobileDevice = this.state.deviceWindowWidth < 600;
useEditorStore.getState().actions.toggleCurrentLayout(isMobileDevice ? 'mobile' : 'desktop');
window.addEventListener('message', this.handleMessage);
}
componentDidUpdate(prevProps, prevState) {
if (this.props.params.slug && this.props.params.slug !== prevProps.params.slug) {
this.setState({ isLoading: true });
this.loadApplicationBySlug(this.props.params.slug);
}
if (this.state.initialComputationOfStateDone) this.handlePageSwitchingBasedOnURLparam();
if (this.state.homepage !== prevState.homepage && !this.state.isLoading) {