queryParamChangeHandler(index, 0, value)}
mode="javascript"
className="form-control codehinter-query-editor-input"
@@ -79,7 +79,7 @@ export function SwitchPage({ getPages, event, handlerChanged, eventIndex, darkMo
queryParamChangeHandler(index, 1, value)}
mode="javascript"
className="form-control codehinter-query-editor-input"
diff --git a/frontend/src/Editor/Inspector/Components/Chart.jsx b/frontend/src/Editor/Inspector/Components/Chart.jsx
index 4c2c20c890..88e55d1e01 100644
--- a/frontend/src/Editor/Inspector/Components/Chart.jsx
+++ b/frontend/src/Editor/Inspector/Components/Chart.jsx
@@ -56,17 +56,16 @@ class Chart extends React.Component {
}
render() {
- const { dataQueries, component, paramUpdated, componentMeta, components, currentState } = this.state;
- const data = this.state.component.component.definition.properties.data;
+ const { dataQueries, component, paramUpdated, componentMeta, components, currentState } = this.props;
+ const data = this.props.component.component.definition.properties.data; // since component is not unmounting on every render in current scenario
- const jsonDescription = this.state.component.component.definition.properties.jsonDescription;
+ const jsonDescription = this.props.component.component.definition.properties.jsonDescription;
const plotFromJson = resolveReferences(
- this.state.component.component.definition.properties.plotFromJson?.value,
+ this.props.component.component.definition.properties.plotFromJson?.value,
currentState
);
-
- const chartType = this.state.component.component.definition.properties.type.value;
+ const chartType = this.props.component.component.definition.properties.type.value;
let items = [];
diff --git a/frontend/src/Editor/Inspector/Components/DefaultComponent.jsx b/frontend/src/Editor/Inspector/Components/DefaultComponent.jsx
index b0c9cf0199..d61d01213d 100644
--- a/frontend/src/Editor/Inspector/Components/DefaultComponent.jsx
+++ b/frontend/src/Editor/Inspector/Components/DefaultComponent.jsx
@@ -98,8 +98,9 @@ export const baseComponentProperties = (
isOpen: true,
children: (
{
- if (component.parent === id && component?.component?.component === 'Button') {
+ if (component.component.parent === id && component?.component?.component === 'Button') {
newOptions.push({ name: component.component.name, value: componentId });
}
});
@@ -94,8 +94,9 @@ export const baseComponentProperties = (
isOpen: true,
children: (
{
const actions = this.props.component.component.definition.properties.actions;
actions.value[index][property] = value;
- this.props.paramUpdated({ name: 'actions' }, 'value', actions.value, 'properties');
+ this.props.paramUpdated({ name: 'actions' }, 'value', actions.value, 'properties', true);
};
actionButtonEventsChanged = (events, index) => {
let actions = this.props.component.component.definition.properties.actions.value;
actions[index]['events'] = events;
- this.props.paramUpdated({ name: 'actions' }, 'value', actions, 'properties');
+ this.props.paramUpdated({ name: 'actions' }, 'value', actions, 'properties', true);
};
actionButtonEventUpdated = (event, value, extraData) => {
@@ -91,7 +92,7 @@ class TableComponent extends React.Component {
actionId: value,
};
- this.props.paramUpdated({ name: 'actions' }, 'value', newValues, 'properties');
+ this.props.paramUpdated({ name: 'actions' }, 'value', newValues, 'properties', true);
};
actionButtonEventOptionUpdated = (event, option, value, extraData) => {
@@ -106,7 +107,7 @@ class TableComponent extends React.Component {
[option]: value,
};
- this.props.paramUpdated({ name: 'actions' }, 'value', newValues, 'properties');
+ this.props.paramUpdated({ name: 'actions' }, 'value', newValues, 'properties', true);
};
columnEventChanged = (columnForWhichEventsAreChanged, events) => {
@@ -454,22 +455,18 @@ class TableComponent extends React.Component {
/>
this.columnEventChanged(column, events)}
apps={this.props.apps}
popOverCallback={(showing) => {
- this.setColumnPopoverRootCloseBlocker('event-manager', showing);
+ this.setState({ actionPopOverRootClose: !showing });
+ this.setState({ showPopOver: showing });
}}
pages={this.props.pages}
/>
@@ -751,6 +748,18 @@ class TableComponent extends React.Component {
);
};
+ deleteEvents = (ref, eventTarget) => {
+ const events = useAppDataStore.getState().events.filter((event) => event.target === eventTarget);
+
+ const toDelete = events?.filter((e) => e.event?.ref === ref.ref);
+
+ return new Promise.all(
+ toDelete?.forEach((e) => {
+ return useAppDataStore.getState().actions.deleteAppVersionEventHandler(e.id);
+ })
+ );
+ };
+
actionPopOver = (action, index) => {
const dummyComponentForActionButton = {
component: {
@@ -760,6 +769,8 @@ class TableComponent extends React.Component {
},
};
+ const actionRef = { ref: `${action?.name}` };
+
return (
@@ -827,8 +838,12 @@ class TableComponent extends React.Component {
paramType="properties"
/>
-
@@ -891,20 +909,21 @@ class TableComponent extends React.Component {
const columns = this.props.component.component.definition.properties.columns;
const newValue = columns.value;
newValue.push({ name: this.generateNewColumnName(columns.value), id: uuidv4() });
- this.props.paramUpdated({ name: 'columns' }, 'value', newValue, 'properties');
+ this.props.paramUpdated({ name: 'columns' }, 'value', newValue, 'properties', true);
};
addNewAction = () => {
const actions = this.props.component.component.definition.properties.actions;
const newValue = actions ? actions.value : [];
newValue.push({ name: computeActionName(actions), buttonText: 'Button', events: [] });
- this.props.paramUpdated({ name: 'actions' }, 'value', newValue, 'properties');
+ this.props.paramUpdated({ name: 'actions' }, 'value', newValue, 'properties', true);
};
- removeAction = (index) => {
+ removeAction = (index, ref) => {
const newValue = this.props.component.component.definition.properties.actions.value;
newValue.splice(index, 1);
- this.props.paramUpdated({ name: 'actions' }, 'value', newValue, 'properties');
+ this.props.paramUpdated({ name: 'actions' }, 'value', newValue, 'properties', true);
+ this.deleteEvents(ref, 'table_action');
};
onColumnItemChange = (index, item, value) => {
@@ -914,7 +933,8 @@ class TableComponent extends React.Component {
column[item] = value;
const newColumns = columns.value;
newColumns[index] = column;
- this.props.paramUpdated({ name: 'columns' }, 'value', newColumns, 'properties');
+
+ this.props.paramUpdated({ name: 'columns' }, 'value', newColumns, 'properties', true);
};
getItemStyle = (isDragging, draggableStyle) => ({
@@ -922,11 +942,11 @@ class TableComponent extends React.Component {
...draggableStyle,
});
- removeColumn = (index) => {
+ removeColumn = (index, ref) => {
const columns = this.props.component.component.definition.properties.columns;
const newValue = columns.value;
const removedColumns = newValue.splice(index, 1);
- this.props.paramUpdated({ name: 'columns' }, 'value', newValue, 'properties');
+ this.props.paramUpdated({ name: 'columns' }, 'value', newValue, 'properties', true);
const existingcolumnDeletionHistory =
this.props.component.component.definition.properties.columnDeletionHistory?.value ?? [];
@@ -934,14 +954,16 @@ class TableComponent extends React.Component {
...existingcolumnDeletionHistory,
...removedColumns.map((column) => column.key || column.name),
];
- this.props.paramUpdated({ name: 'columnDeletionHistory' }, 'value', newcolumnDeletionHistory, 'properties');
+ this.props.paramUpdated({ name: 'columnDeletionHistory' }, 'value', newcolumnDeletionHistory, 'properties', true);
+
+ this.deleteEvents(ref, 'table_column');
};
reorderColumns = (startIndex, endIndex) => {
const result = this.props.component.component.definition.properties.columns.value;
const [removed] = result.splice(startIndex, 1);
result.splice(endIndex, 0, removed);
- this.props.paramUpdated({ name: 'columns' }, 'value', result, 'properties');
+ this.props.paramUpdated({ name: 'columns' }, 'value', result, 'properties', true);
};
onDragEnd({ source, destination }) {
@@ -959,7 +981,6 @@ class TableComponent extends React.Component {
const columns = component.component.definition.properties.columns;
const actions = component.component.definition.properties.actions || { value: [] };
-
if (!component.component.definition.properties.displaySearchBox)
paramUpdated({ name: 'displaySearchBox' }, 'value', true, 'properties');
const displaySearchBox = component.component.definition.properties.displaySearchBox.value;
@@ -1056,7 +1077,8 @@ class TableComponent extends React.Component {
enableActionsMenu
isEditable={item.isEditable === '{{true}}'}
onMenuOptionClick={(listItem, menuOptionLabel) => {
- if (menuOptionLabel === 'Delete') this.removeColumn(index);
+ if (menuOptionLabel === 'Delete')
+ this.removeColumn(index, `${item.name}-${index}`);
}}
darkMode={darkMode}
menuActions={[
@@ -1142,8 +1164,11 @@ class TableComponent extends React.Component {
isOpen: true,
children: (
{
const dataQueries = useDataQueriesStore(({ dataQueries = [] }) => {
if (callerQueryId) {
@@ -41,13 +41,35 @@ export const EventManager = ({
}
return dataQueries;
}, shallow);
- const [events, setEvents] = useState(() => component.component.definition.events || []);
+ const { apps, appId, events: allAppEvents } = useAppInfo();
+
+ const { updateAppVersionEventHandlers, createAppVersionEventHandlers, deleteAppVersionEventHandler } =
+ useAppDataActions();
+
+ const currentEvents = allAppEvents.filter((event) => {
+ if (customEventRefs) {
+ if (event.event.ref !== customEventRefs.ref) {
+ return false;
+ }
+ }
+
+ return event.sourceId === sourceId && event.target === eventSourceType;
+ });
+
+ const [events, setEvents] = useState([]);
const [focusedEventIndex, setFocusedEventIndex] = useState(null);
const { t } = useTranslation();
useEffect(() => {
- setEvents(component.component.definition.events || []);
- }, [component?.component?.definition?.events]);
+ if (_.isEqual(currentEvents, events)) return;
+
+ const sortedEvents = currentEvents.sort((a, b) => {
+ return a.index - b.index;
+ });
+
+ setEvents(sortedEvents || []);
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [JSON.stringify(currentEvents)]);
let actionOptions = ActionTypes.map((action) => {
return { name: action.name, value: action.id };
@@ -100,11 +122,11 @@ export const EventManager = ({
excludeEvents = excludeEvents || [];
/* Filter events based on excludesEvents ( a list of event ids to exclude ) */
- let possibleEvents = Object.keys(componentMeta.events)
+ let possibleEvents = Object.keys(eventMetaDefinition.events)
.filter((eventId) => !excludeEvents.includes(eventId))
.map((eventId) => {
return {
- name: componentMeta.events[eventId].displayName,
+ name: eventMetaDefinition?.events[eventId]?.displayName,
value: eventId,
};
});
@@ -151,7 +173,7 @@ export const EventManager = ({
const actions = targetComponentMeta.actions;
const options = actions.map((action) => ({
- name: action.displayName,
+ name: action?.displayName,
value: action.handle,
}));
@@ -172,7 +194,7 @@ export const EventManager = ({
function getComponentActionDefaultParams(componentId, actionHandle) {
const action = getAction(componentId, actionHandle);
- const defaultParams = (action.params ?? []).map((param) => ({
+ const defaultParams = (action?.params ?? []).map((param) => ({
handle: param.handle,
value: param.defaultValue,
}));
@@ -182,7 +204,7 @@ export const EventManager = ({
function getAllApps() {
let appsOptionsList = [];
apps
- .filter((item) => item.slug !== undefined)
+ .filter((item) => item.slug !== undefined && item.id !== appId)
.forEach((item) => {
appsOptionsList.push({
name: item.name,
@@ -208,51 +230,105 @@ export const EventManager = ({
}));
}
- function handlerChanged(index, param, value) {
- let newEvents = [...events];
-
+ function handleQueryChange(index, updates) {
+ let newEvents = _.cloneDeep(events);
let updatedEvent = newEvents[index];
- updatedEvent[param] = value;
+
+ updatedEvent.event = {
+ ...updatedEvent.event,
+ ...updates,
+ };
newEvents[index] = updatedEvent;
- setEvents(newEvents);
- eventsChanged(newEvents);
+ updateAppVersionEventHandlers(
+ [
+ {
+ event_id: updatedEvent.id,
+ diff: updatedEvent,
+ },
+ ],
+ 'update'
+ );
+ }
+
+ function handlerChanged(index, param, value) {
+ let newEvents = _.cloneDeep(events);
+
+ let updatedEvent = newEvents[index];
+ updatedEvent.event[param] = value;
+
+ if (param === 'componentSpecificActionHandle') {
+ const getDefault = getComponentActionDefaultParams(updatedEvent.event?.componentId, value);
+ updatedEvent.event['componentSpecificActionParams'] = getDefault;
+ }
+
+ newEvents[index] = updatedEvent;
+
+ updateAppVersionEventHandlers(
+ [
+ {
+ event_id: updatedEvent.id,
+ diff: updatedEvent,
+ },
+ ],
+ 'update'
+ );
}
function removeHandler(index) {
- let newEvents = component.component.definition.events;
- newEvents.splice(index, 1);
- setEvents(newEvents);
- eventsChanged(newEvents);
+ const eventsHandler = _.cloneDeep(events);
+
+ const eventId = eventsHandler[index].id;
+
+ deleteAppVersionEventHandler(eventId);
}
function addHandler() {
- let newEvents = component.component.definition.events;
- newEvents.push({
- eventId: Object.keys(componentMeta.events)[0],
- actionId: 'show-alert',
- message: 'Hello world!',
- alertType: 'info',
+ let newEvents = events;
+ const eventIndex = newEvents.length;
+
+ createAppVersionEventHandlers({
+ event: {
+ eventId: Object.keys(eventMetaDefinition?.events)[0],
+ actionId: 'show-alert',
+ message: 'Hello world!',
+ alertType: 'info',
+ ...customEventRefs,
+ },
+ eventType: eventSourceType,
+ attachedTo: sourceId,
+ index: eventIndex,
});
- setEvents(newEvents);
- eventsChanged(newEvents);
}
//following two are functions responsible for on change and value for the control specific actions
const onChangeHandlerForComponentSpecificActionHandle = (value, index, param, event) => {
const newParam = { ...param, value: value };
const params = event?.componentSpecificActionParams ?? [];
- const newParams = params.map((paramOfParamList) =>
- paramOfParamList.handle === param.handle ? newParam : paramOfParamList
- );
+
+ const newParams =
+ params.length > 0
+ ? params.map((paramOfParamList) => {
+ return paramOfParamList.handle === param.handle ? newParam : paramOfParamList;
+ })
+ : [newParam];
+
return handlerChanged(index, 'componentSpecificActionParams', newParams);
};
const valueForComponentSpecificActionHandle = (event, param) => {
- return (
- event?.componentSpecificActionParams?.find((paramItem) => paramItem.handle === param.handle)?.value ??
- param.defaultValue
- );
+ const componentSpecificActionParamsExits = Array.isArray(event?.componentSpecificActionParams);
+ const defaultValue = param.defaultValue ?? '';
+
+ if (componentSpecificActionParamsExits) {
+ const paramValue =
+ event?.componentSpecificActionParams?.find((paramItem) => paramItem.handle === param.handle)?.value ??
+ defaultValue;
+
+ return paramValue;
+ }
+
+ return defaultValue;
};
function eventPopover(event, index) {
@@ -260,10 +336,14 @@ export const EventManager = ({
-
+ {
+ e.stopPropagation();
+ }}
+ >
{t('editor.inspector.eventManager.event', 'Event')}
@@ -443,10 +523,11 @@ export const EventManager = ({
options={dataQueries
.filter((qry) => isQueryRunnable(qry))
.map((qry) => ({ name: qry.name, value: qry.id }))}
- value={event.queryId}
+ value={event?.queryId}
search={true}
onChange={(value) => {
const query = dataQueries.find((dataquery) => dataquery.id === value);
+
const parameters = (query?.options?.parameters ?? []).reduce(
(paramObj, param) => ({
...paramObj,
@@ -454,9 +535,12 @@ export const EventManager = ({
}),
{}
);
- handlerChanged(index, 'queryId', query.id);
- handlerChanged(index, 'queryName', query.name);
- handlerChanged(index, 'parameters', parameters);
+
+ handleQueryChange(index, {
+ queryId: query.id,
+ queryName: query.name,
+ parameters: parameters,
+ });
}}
placeholder={t('globals.select', 'Select') + '...'}
styles={styles}
@@ -688,7 +772,6 @@ export const EventManager = ({
value={event?.componentId}
search={true}
onChange={(value) => {
- handlerChanged(index, 'componentSpecificActionHandle', '');
handlerChanged(index, 'componentId', value);
}}
placeholder={t('globals.select', 'Select') + '...'}
@@ -710,11 +793,6 @@ export const EventManager = ({
search={true}
onChange={(value) => {
handlerChanged(index, 'componentSpecificActionHandle', value);
- handlerChanged(
- index,
- 'componentSpecificActionParams',
- getComponentActionDefaultParams(event?.componentId, value)
- );
}}
placeholder={t('globals.select', 'Select') + '...'}
styles={styles}
@@ -725,10 +803,10 @@ export const EventManager = ({
{event?.componentId &&
event?.componentSpecificActionHandle &&
- (getAction(event?.componentId, event?.componentSpecificActionHandle).params ?? []).map((param) => (
+ (getAction(event?.componentId, event?.componentSpecificActionHandle)?.params ?? []).map((param) => (
-
- {param.displayName}
+
+ {param?.displayName}
{param.type === 'select' ? (
@@ -763,7 +841,7 @@ export const EventManager = ({
enablePreview={true}
type={param?.type}
fieldMeta={{ options: param?.options }}
- cyLabel={param.displayName}
+ cyLabel={param?.displayName}
/>
)}
@@ -789,11 +867,24 @@ export const EventManager = ({
}
const reorderEvents = (startIndex, endIndex) => {
- const result = [...component.component.definition.events];
+ const result = _.cloneDeep(events);
const [removed] = result.splice(startIndex, 1);
result.splice(endIndex, 0, removed);
- setEvents(result);
- eventsChanged(result, true);
+
+ const reorderedEvents = result.map((event, index) => {
+ return {
+ ...event,
+ index: index,
+ };
+ });
+
+ updateAppVersionEventHandlers(
+ reorderedEvents.map((event) => ({
+ event_id: event.id,
+ diff: event,
+ })),
+ 'reorder'
+ );
};
const onDragEnd = ({ source, destination }) => {
@@ -817,8 +908,8 @@ export const EventManager = ({
{({ innerRef, droppableProps, placeholder }) => (
{events.map((event, index) => {
- const actionMeta = ActionTypes.find((action) => action.id === event.actionId);
- const rowClassName = `card-body p-0 ${focusedEventIndex === index ? ' bg-azure-lt' : ''}`;
+ const actionMeta = ActionTypes.find((action) => action.id === event.event.actionId);
+ // const rowClassName = `card-body p-0 ${focusedEventIndex === index ? ' bg-azure-lt' : ''}`;
return (
{renderDraggable((provided, snapshot) => {
@@ -831,14 +922,13 @@ export const EventManager = ({
trigger="click"
placement={popoverPlacement || 'left'}
rootClose={true}
- overlay={eventPopover(event, index)}
+ overlay={eventPopover(event.event, index)}
onHide={() => setFocusedEventIndex(null)}
onToggle={(showing) => {
if (showing) {
setFocusedEventIndex(index);
} else {
setFocusedEventIndex(null);
- eventsChanged(events);
}
if (typeof popOverCallback === 'function') popOverCallback(showing);
}}
@@ -850,7 +940,7 @@ export const EventManager = ({
{...provided.dragHandleProps}
>
+ {renderAddHandlerBtn()}
+ {!hideEmptyEventsAlert ? (
+
+
+ {t(
+ 'editor.inspector.eventManager.emptyMessage',
+ "This {{componentName}} doesn't have any event handlers",
+ {
+ componentName: componentName.toLowerCase(),
+ }
+ )}
+
+
+ ) : null}
+ >
+ );
+ }
+
return (
<>
-
- {renderHandlers(events)}
- {renderAddHandlerBtn()}
-
+ {renderHandlers(events)}
+ {renderAddHandlerBtn()}
>
);
};
diff --git a/frontend/src/Editor/Inspector/Inspector.jsx b/frontend/src/Editor/Inspector/Inspector.jsx
index 840efbdb77..b98ae05050 100644
--- a/frontend/src/Editor/Inspector/Inspector.jsx
+++ b/frontend/src/Editor/Inspector/Inspector.jsx
@@ -56,7 +56,6 @@ export const Inspector = ({
selectedComponentId,
componentDefinitionChanged,
allComponents,
- apps,
darkMode,
switchSidebarTab,
removeComponent,
@@ -66,18 +65,17 @@ export const Inspector = ({
const dataQueries = useDataQueries();
const component = {
id: selectedComponentId,
- component: allComponents[selectedComponentId].component,
+ component: JSON.parse(JSON.stringify(allComponents[selectedComponentId].component)),
layouts: allComponents[selectedComponentId].layouts,
parent: allComponents[selectedComponentId].parent,
};
const currentState = useCurrentState();
const [showWidgetDeleteConfirmation, setWidgetDeleteConfirmation] = useState(false);
- // eslint-disable-next-line no-unused-vars
- const [tabHeight, setTabHeight] = React.useState(0);
+
const componentNameRef = useRef(null);
const [newComponentName, setNewComponentName] = useState(component.component.name);
const [inputRef, setInputFocus] = useFocus();
- const [selectedTab, setSelectedTab] = useState('properties');
+ // const [selectedTab, setSelectedTab] = useState('properties');
const [showHeaderActionsMenu, setShowHeaderActionsMenu] = useState(false);
const { isVersionReleased } = useAppVersionStore(
(state) => ({
@@ -101,13 +99,6 @@ export const Inspector = ({
componentNameRef.current = newComponentName;
}, [newComponentName]);
- useEffect(() => {
- return () => {
- handleComponentNameChange(componentNameRef.current);
- };
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, []);
-
const validateComponentName = (name) => {
const isValid = !Object.values(allComponents)
.map((component) => component.component.name)
@@ -131,9 +122,9 @@ export const Inspector = ({
return setInputFocus();
}
if (validateQueryName(newName)) {
- let newComponent = { ...component };
+ let newComponent = JSON.parse(JSON.stringify(component));
newComponent.component.name = newName;
- componentDefinitionChanged(newComponent);
+ componentDefinitionChanged(newComponent, { componentNameUpdated: true });
} else {
toast.error(
t(
@@ -152,9 +143,9 @@ export const Inspector = ({
return null;
};
- function paramUpdated(param, attr, value, paramType) {
- console.log({ param, attr, value, paramType });
- let newDefinition = _.cloneDeep(component.component.definition);
+ function paramUpdated(param, attr, value, paramType, isParamFromTableColumn = false) {
+ let newComponent = JSON.parse(JSON.stringify(component));
+ let newDefinition = _.cloneDeep(newComponent.component.definition);
let allParams = newDefinition[paramType] || {};
const paramObject = allParams[param.name];
if (!paramObject) {
@@ -163,13 +154,19 @@ export const Inspector = ({
if (attr) {
allParams[param.name][attr] = value;
const defaultValue = getDefaultValue(value);
- // This is needed to have enable pagination as backward compatible
+ // This is needed to have enable pagination in Table as backward compatible
// Whenever enable pagination is false, we turn client and server side pagination as false
- if (param.name === 'enablePagination' && !resolveReferences(value, currentState)) {
+ if (
+ component.component.component === 'Table' &&
+ param.name === 'enablePagination' &&
+ !resolveReferences(value, currentState)
+ ) {
if (allParams?.['clientSidePagination']?.[attr]) {
allParams['clientSidePagination'][attr] = value;
}
- allParams['serverSidePagination'][attr] = value;
+ if (allParams['serverSidePagination']?.[attr]) {
+ allParams['serverSidePagination'][attr] = value;
+ }
}
// This case is required to handle for older apps when serverSidePagination is connected to Fx
if (param.name === 'serverSidePagination' && !allParams?.['enablePagination']?.[attr]) {
@@ -194,12 +191,11 @@ export const Inspector = ({
allParams[param.name] = value;
}
newDefinition[paramType] = allParams;
- let newComponent = _.merge(component, {
- component: {
- definition: newDefinition,
- },
+ newComponent.component.definition = newDefinition;
+ componentDefinitionChanged(newComponent, {
+ componentPropertyUpdated: true,
+ isParamFromTableColumn: isParamFromTableColumn,
});
- componentDefinitionChanged(newComponent);
}
function layoutPropertyChanged(param, attr, value, paramType) {
@@ -207,9 +203,7 @@ export const Inspector = ({
// User wants to show the widget on mobile devices
if (param.name === 'showOnMobile' && value === true) {
- let newComponent = {
- ...component,
- };
+ let newComponent = JSON.parse(JSON.stringify(component));
const { width, height } = newComponent.layouts['desktop'];
@@ -223,7 +217,7 @@ export const Inspector = ({
},
};
- componentDefinitionChanged(newComponent);
+ componentDefinitionChanged(newComponent, { layoutPropertyChanged: true });
// Child components should also have a mobile layout
const childComponents = Object.keys(allComponents).filter((key) => allComponents[key].parent === component.id);
@@ -246,54 +240,11 @@ export const Inspector = ({
},
};
- componentDefinitionChanged(newChild);
+ componentDefinitionChanged(newChild, { withChildLayout: true });
});
}
}
- function eventUpdated(event, actionId) {
- let newDefinition = { ...component.component.definition };
- newDefinition.events[event.name] = { actionId };
-
- let newComponent = {
- ...component,
- };
-
- componentDefinitionChanged(newComponent);
- }
-
- function eventsChanged(newEvents, isReordered = false) {
- let newDefinition;
- if (isReordered) {
- newDefinition = { ...component.component };
- newDefinition.definition.events = newEvents;
- } else {
- newDefinition = { ...component.component.definition };
- newDefinition.events = newEvents;
- }
-
- let newComponent = {
- ...component,
- };
-
- componentDefinitionChanged(newComponent);
- }
-
- function eventOptionUpdated(event, option, value) {
- console.log('eventOptionUpdated', event, option, value);
-
- let newDefinition = { ...component.component.definition };
- let eventDefinition = newDefinition.events[event.name] || { options: {} };
-
- newDefinition.events[event.name] = { ...eventDefinition, options: { ...eventDefinition.options, [option]: value } };
-
- let newComponent = {
- ...component,
- };
-
- componentDefinitionChanged(newComponent);
- }
-
const handleInspectorHeaderActions = (value) => {
if (value === 'rename') {
setTimeout(() => setInputFocus(), 0);
@@ -339,13 +290,13 @@ export const Inspector = ({
paramUpdated={paramUpdated}
dataQueries={dataQueries}
componentMeta={componentMeta}
- eventUpdated={eventUpdated}
- eventOptionUpdated={eventOptionUpdated}
+ // eventUpdated={eventUpdated}
+ // eventOptionUpdated={eventOptionUpdated}
components={allComponents}
currentState={currentState}
darkMode={darkMode}
- eventsChanged={eventsChanged}
- apps={apps}
+ // eventsChanged={eventsChanged}
+ // apps={apps} !check
pages={pages}
allComponents={allComponents}
/>
@@ -382,31 +333,15 @@ export const Inspector = ({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [JSON.stringify({ showHeaderActionsMenu })]);
- const handleDeleteConfirm = React.useCallback(() => {
- switchSidebarTab(2);
- removeComponent(component);
- setWidgetDeleteConfirmation(false);
- }, [switchSidebarTab, removeComponent, component, setWidgetDeleteConfirmation]);
-
- React.useEffect(() => {
- const handleKeyPress = (event) => {
- if (showWidgetDeleteConfirmation && event.key === 'Enter') {
- handleDeleteConfirm();
- }
- };
-
- document.addEventListener('keydown', handleKeyPress);
- return () => {
- document.removeEventListener('keydown', handleKeyPress);
- };
- }, [showWidgetDeleteConfirmation, handleDeleteConfirm]);
-
return (
{
+ switchSidebarTab(2);
+ removeComponent(component.id);
+ }}
onCancel={() => setWidgetDeleteConfirmation(false)}
darkMode={darkMode}
/>
diff --git a/frontend/src/Editor/LeftSidebar/SidebarInspector.jsx b/frontend/src/Editor/LeftSidebar/SidebarInspector.jsx
index 06335ff095..1510463c74 100644
--- a/frontend/src/Editor/LeftSidebar/SidebarInspector.jsx
+++ b/frontend/src/Editor/LeftSidebar/SidebarInspector.jsx
@@ -62,6 +62,7 @@ export const LeftSidebarInspector = ({
delete jsontreeData.server;
delete jsontreeData.actions;
delete jsontreeData.succededQuery;
+ delete jsontreeData.layout;
//*Sorted components and queries alphabetically
const sortedComponents = Object.keys(jsontreeData['components'])
@@ -117,7 +118,7 @@ export const LeftSidebarInspector = ({
const iconsList = useMemo(() => [...queryIcons, ...componentIcons], [queryIcons, componentIcons]);
const handleRemoveComponent = (component) => {
- removeComponent(component);
+ removeComponent(component.id);
};
const handleSelectComponentOnEditor = (component) => {
diff --git a/frontend/src/Editor/LeftSidebar/SidebarPageSelector/GlobalSettings.jsx b/frontend/src/Editor/LeftSidebar/SidebarPageSelector/GlobalSettings.jsx
index d2097a4c72..cda8280925 100644
--- a/frontend/src/Editor/LeftSidebar/SidebarPageSelector/GlobalSettings.jsx
+++ b/frontend/src/Editor/LeftSidebar/SidebarPageSelector/GlobalSettings.jsx
@@ -4,7 +4,7 @@ import { useAppVersionStore } from '@/_stores/appVersionStore';
import { shallow } from 'zustand/shallow';
import SolidIcon from '@/_ui/Icon/SolidIcons';
-export const GlobalSettings = ({ darkMode, showHideViewerNavigationControls, showPageViwerPageNavitation }) => {
+export const GlobalSettings = ({ darkMode, showHideViewerNavigationControls, isViewerNavigationDisabled }) => {
const { isVersionReleased, enableReleasedVersionPopupState } = useAppVersionStore(
(state) => ({
isVersionReleased: state.isVersionReleased,
@@ -35,7 +35,7 @@ export const GlobalSettings = ({ darkMode, showHideViewerNavigationControls, sho
-
+
diff --git a/frontend/src/Editor/LeftSidebar/SidebarPageSelector/PageHandler.jsx b/frontend/src/Editor/LeftSidebar/SidebarPageSelector/PageHandler.jsx
index 57289f787d..b434458452 100644
--- a/frontend/src/Editor/LeftSidebar/SidebarPageSelector/PageHandler.jsx
+++ b/frontend/src/Editor/LeftSidebar/SidebarPageSelector/PageHandler.jsx
@@ -25,7 +25,7 @@ export const PageHandler = ({
currentPageId,
updateHomePage,
updatePageHandle,
- updateOnPageLoadEvents,
+
apps,
pages,
components,
@@ -201,7 +201,6 @@ export const PageHandler = ({
!haveUserPinned && pinPagesPopover(false);
}}
darkMode={darkMode}
- updateOnPageLoadEvents={updateOnPageLoadEvents}
apps={apps}
pages={pages}
components={components}
diff --git a/frontend/src/Editor/LeftSidebar/SidebarPageSelector/SettingsModal.jsx b/frontend/src/Editor/LeftSidebar/SidebarPageSelector/SettingsModal.jsx
index 97c81027e8..b9ccbf4cf2 100644
--- a/frontend/src/Editor/LeftSidebar/SidebarPageSelector/SettingsModal.jsx
+++ b/frontend/src/Editor/LeftSidebar/SidebarPageSelector/SettingsModal.jsx
@@ -8,7 +8,7 @@ export const SettingsModal = ({
show,
handleClose,
darkMode,
- updateOnPageLoadEvents,
+
apps,
pages,
components,
@@ -55,6 +55,7 @@ export const SettingsModal = ({
pinPagesPopover(true)}>
Events
updateOnPageLoadEvents(page.id, events)}
popOverCallback={(showing) => showing}
/>
diff --git a/frontend/src/Editor/LeftSidebar/SidebarPageSelector/index.jsx b/frontend/src/Editor/LeftSidebar/SidebarPageSelector/index.jsx
index 239dbf7901..f39a5e2ad9 100644
--- a/frontend/src/Editor/LeftSidebar/SidebarPageSelector/index.jsx
+++ b/frontend/src/Editor/LeftSidebar/SidebarPageSelector/index.jsx
@@ -30,7 +30,7 @@ const LeftSidebarPageSelector = ({
homePageId,
showHideViewerNavigationControls,
updateOnSortingPages,
- updateOnPageLoadEvents,
+
apps,
pinned,
setPinned,
@@ -89,7 +89,7 @@ const LeftSidebarPageSelector = ({
}
>
@@ -166,7 +166,6 @@ const LeftSidebarPageSelector = ({
updatePageHandle={updatePageHandle}
classNames="page-handler"
onSort={updateOnSortingPages}
- updateOnPageLoadEvents={updateOnPageLoadEvents}
currentState={currentState}
apps={apps}
allpages={pages}
diff --git a/frontend/src/Editor/LeftSidebar/index.jsx b/frontend/src/Editor/LeftSidebar/index.jsx
index 55a8e11107..3fae5b4f9c 100644
--- a/frontend/src/Editor/LeftSidebar/index.jsx
+++ b/frontend/src/Editor/LeftSidebar/index.jsx
@@ -19,8 +19,8 @@ import { useDataSources } from '@/_stores/dataSourcesStore';
import { shallow } from 'zustand/shallow';
import useDebugger from './SidebarDebugger/useDebugger';
import { GlobalSettings } from '../Header/GlobalSettings';
-import { useCurrentState } from '@/_stores/currentStateStore';
import { resolveReferences } from '@/_helpers/utils';
+import { useCurrentState } from '@/_stores/currentStateStore';
export const LeftSidebar = forwardRef((props, ref) => {
const router = useRouter();
@@ -46,7 +46,6 @@ export const LeftSidebar = forwardRef((props, ref) => {
updatePageHandle,
showHideViewerNavigationControls,
updateOnSortingPages,
- updateOnPageLoadEvents,
apps,
clonePage,
setEditorMarginLeft,
@@ -54,10 +53,8 @@ export const LeftSidebar = forwardRef((props, ref) => {
toggleAppMaintenance,
app,
disableEnablePage,
- slug,
- handleSlugChange,
+ isMaintenanceOn,
} = props;
- const { is_maintenance_on } = app;
const dataSources = useDataSources();
const prevSelectedSidebarItem = localStorage.getItem('selectedSidebarItem');
@@ -80,8 +77,9 @@ export const LeftSidebar = forwardRef((props, ref) => {
}),
shallow
);
- const [pinned, setPinned] = useState(!!localStorage.getItem('selectedSidebarItem'));
const currentState = useCurrentState();
+ const [pinned, setPinned] = useState(!!localStorage.getItem('selectedSidebarItem'));
+
const [realState, setRealState] = useState(currentState);
const { errorLogs, clearErrorLogs, unReadErrorCount, allLog } = useDebugger({
@@ -142,9 +140,10 @@ export const LeftSidebar = forwardRef((props, ref) => {
sideBarBtnRefs.current[page] = ref;
};
useEffect(() => {
- setRealState(currentState);
+ setRealState(currentState); //!ceck this
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [currentState.components]);
+
const backgroundFxQuery = appDefinition?.globalSettings?.backgroundFxQuery;
const SELECTED_ITEMS = {
@@ -164,11 +163,14 @@ export const LeftSidebar = forwardRef((props, ref) => {
updateHomePage={updateHomePage}
updatePageHandle={updatePageHandle}
clonePage={clonePage}
- pages={Object.entries(appDefinition.pages).map(([id, page]) => ({ id, ...page })) || []}
+ pages={
+ Object.entries(_.cloneDeep(appDefinition).pages)
+ .map(([id, page]) => ({ id, ...page }))
+ .sort((a, b) => a.index - b.index) || []
+ }
homePageId={appDefinition.homePageId}
showHideViewerNavigationControls={showHideViewerNavigationControls}
updateOnSortingPages={updateOnSortingPages}
- updateOnPageLoadEvents={updateOnPageLoadEvents}
apps={apps}
setPinned={handlePin}
pinned={pinned}
@@ -221,21 +223,19 @@ export const LeftSidebar = forwardRef((props, ref) => {
globalSettings={appDefinition.globalSettings}
darkMode={darkMode}
toggleAppMaintenance={toggleAppMaintenance}
- is_maintenance_on={is_maintenance_on}
+ isMaintenanceOn={isMaintenanceOn}
app={app}
+ realState={currentState}
backgroundFxQuery={backgroundFxQuery}
- realState={realState}
- slug={slug}
- handleSlugChange={handleSlugChange}
/>
),
};
useEffect(() => {
backgroundFxQuery &&
- globalSettingsChanged('canvasBackgroundColor', resolveReferences(backgroundFxQuery, realState));
+ globalSettingsChanged({ canvasBackgroundColor: resolveReferences(backgroundFxQuery, currentState) });
// eslint-disable-next-line react-hooks/exhaustive-deps
- }, [JSON.stringify(resolveReferences(backgroundFxQuery, realState))]);
+ }, [JSON.stringify(resolveReferences(backgroundFxQuery, currentState))]);
return (
diff --git a/frontend/src/Editor/ManageAppUsers.jsx b/frontend/src/Editor/ManageAppUsers.jsx
index ebd8c7e637..42f8ccfc34 100644
--- a/frontend/src/Editor/ManageAppUsers.jsx
+++ b/frontend/src/Editor/ManageAppUsers.jsx
@@ -13,6 +13,7 @@ import SolidIcon from '@/_ui/Icon/SolidIcons';
import cx from 'classnames';
import { ToolTip } from '@/_components/ToolTip';
import { TOOLTIP_MESSAGES } from '@/_helpers/constants';
+import { useAppDataStore } from '@/_stores/appDataStore';
class ManageAppUsersComponent extends React.Component {
constructor(props) {
@@ -21,7 +22,7 @@ class ManageAppUsersComponent extends React.Component {
this.state = {
showModal: false,
- app: { ...props.app },
+ appId: null,
isLoading: true,
isSlugVerificationInProgress: false,
addingUser: false,
@@ -49,14 +50,14 @@ class ManageAppUsersComponent extends React.Component {
};
componentDidMount() {
- const appId = this.props.app.id;
- this.fetchAppUsers();
+ const appId = this.props.appId;
+ this.fetchAppUsers(appId);
this.setState({ appId });
}
- fetchAppUsers = () => {
+ fetchAppUsers = (appId) => {
appsService
- .getAppUsers(this.props.app.id)
+ .getAppUsers(appId)
.then((data) =>
this.setState({
users: data.users,
@@ -65,7 +66,8 @@ class ManageAppUsersComponent extends React.Component {
)
.catch((error) => {
this.setState({ isLoading: false });
- toast.error(error);
+ const errorMessage = error?.message || 'Something went wrong';
+ toast.error(errorMessage);
});
};
@@ -89,11 +91,11 @@ class ManageAppUsersComponent extends React.Component {
const { organizationUserId, role } = this.state.newUser;
appService
- .createAppUser(this.state.app.id, organizationUserId, role)
+ .createAppUser(this.state.appId, organizationUserId, role)
.then(() => {
this.setState({ addingUser: false, newUser: {} });
toast.success('Added user successfully');
- this.fetchAppUsers();
+ this.fetchAppUsers(this.state.appId);
})
.catch(({ error }) => {
this.setState({ addingUser: false });
@@ -102,21 +104,19 @@ class ManageAppUsersComponent extends React.Component {
};
toggleAppVisibility = () => {
- const newState = !this.state.app.is_public;
+ const newState = !this.props.isPublic;
this.setState({
ischangingVisibility: true,
});
+ useAppDataStore.getState().actions.updateState({ isPublic: newState });
+
// eslint-disable-next-line no-unused-vars
appsService
- .setVisibility(this.state.app.id, newState)
+ .setVisibility(this.state.appId, newState)
.then(() => {
this.setState({
ischangingVisibility: false,
- app: {
- ...this.state.app,
- is_public: newState,
- },
});
if (newState) {
@@ -153,7 +153,7 @@ class ManageAppUsersComponent extends React.Component {
isSlugVerificationInProgress: true,
});
appsService
- .setSlug(this.state.app.id, value)
+ .setSlug(this.state.appId, value)
.then(() => {
this.setState({
newSlug: {
@@ -163,8 +163,9 @@ class ManageAppUsersComponent extends React.Component {
isSlugVerificationInProgress: false,
isSlugUpdated: true,
});
- this.props.handleSlugChange(value);
+
replaceEditorURL(value, this.props.pageHandle);
+ useAppDataStore.getState().actions.updateState({ slug: value });
})
.catch(({ error }) => {
this.setState({
@@ -189,8 +190,8 @@ class ManageAppUsersComponent extends React.Component {
};
render() {
- const { isLoading, app, isSlugVerificationInProgress, newSlug, isSlugUpdated } = this.state;
- const appId = app.id;
+ const { isLoading, appId, isSlugVerificationInProgress, newSlug, isSlugUpdated } = this.state;
+
const appLink = `${getHostURL()}/applications/`;
const shareableLink = appLink + (this.props.slug || appId);
const slugButtonClass = !_.isEmpty(newSlug.error) ? 'is-invalid' : 'is-valid';
@@ -249,7 +250,7 @@ class ManageAppUsersComponent extends React.Component {
className="form-check-input"
type="checkbox"
onClick={this.toggleAppVisibility}
- checked={this.state.app.is_public}
+ checked={this?.props?.isPublic}
disabled={this.state.ischangingVisibility}
data-cy="make-public-app-toggle"
/>
@@ -356,7 +357,7 @@ class ManageAppUsersComponent extends React.Component {
)}
- {(this.state.app.is_public || window?.public_config?.ENABLE_PRIVATE_APP_EMBED === 'true') && (
+ {(this?.props?.isPublic || window?.public_config?.ENABLE_PRIVATE_APP_EMBED === 'true') && (
diff --git a/frontend/src/Editor/QueryManager/Components/QueryManagerBody.jsx b/frontend/src/Editor/QueryManager/Components/QueryManagerBody.jsx
index 1d9185ac02..6bfbd12205 100644
--- a/frontend/src/Editor/QueryManager/Components/QueryManagerBody.jsx
+++ b/frontend/src/Editor/QueryManager/Components/QueryManagerBody.jsx
@@ -14,7 +14,7 @@ import { EventManager } from '@/Editor/Inspector/EventManager';
import { staticDataSources, customToggles, mockDataQueryAsComponent } from '../constants';
import { DataSourceTypes } from '../../DataSourceManager/SourceComponents';
import { useDataSources, useGlobalDataSources } from '@/_stores/dataSourcesStore';
-import { useDataQueriesActions, useDataQueriesStore } from '@/_stores/dataQueriesStore';
+import { useDataQueriesActions } from '@/_stores/dataQueriesStore';
import { useSelectedQuery, useSelectedDataSource } from '@/_stores/queryPanelStore';
import { useAppVersionStore } from '@/_stores/appVersionStore';
import { shallow } from 'zustand/shallow';
@@ -97,14 +97,6 @@ export const QueryManagerBody = ({
validateNewOptions(newOptions);
};
- const eventsChanged = (events) => {
- optionchanged('events', events);
- //added this here since the subscriber added in QueryManager component does not detect this change
- useDataQueriesStore
- .getState()
- .actions.saveData({ ...selectedQuery, options: { ...selectedQuery.options, events: events } });
- };
-
const toggleOption = (option) => {
const currentValue = selectedQuery?.options?.[option] ?? false;
optionchanged(option, !currentValue);
@@ -185,9 +177,9 @@ export const QueryManagerBody = ({
{t('editor.queryManager.eventsHandler', 'Events')}
{
+const QueryManager = ({ mode, appId, darkMode, apps, allComponents, appDefinition, editorRef }) => {
const loadingDataSources = useLoadingDataSources();
const dataSources = useDataSources();
const globalDataSources = useGlobalDataSources();
const queryToBeRun = useQueryToBeRun();
- const isCreationInProcess = useQueryCreationLoading();
- const isUpdationInProcess = useQueryUpdationLoading();
const selectedQuery = useSelectedQuery();
const { setSelectedDataSource, setQueryToBeRun } = useQueryPanelActions();
const [options, setOptions] = useState({});
- const mounted = useRef(false);
-
- /** TODO: Below effect primarily used only for websocket invocation post update. Can be removed onece websocket logic is revamped */
- useEffect(() => {
- if (mounted.current && !isCreationInProcess && !isUpdationInProcess) {
- return dataQueriesChanged();
- }
- mounted.current = true;
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [isCreationInProcess, isUpdationInProcess, mounted.current]);
useEffect(() => {
setOptions(selectedQuery?.options || {});
diff --git a/frontend/src/Editor/QueryPanel/QueryPanel.jsx b/frontend/src/Editor/QueryPanel/QueryPanel.jsx
index 9ca51023e7..9f102c6f1d 100644
--- a/frontend/src/Editor/QueryPanel/QueryPanel.jsx
+++ b/frontend/src/Editor/QueryPanel/QueryPanel.jsx
@@ -15,7 +15,6 @@ const QueryPanel = ({
dataQueriesChanged,
fetchDataQueries,
darkMode,
- apps,
allComponents,
appId,
appDefinition,
@@ -203,7 +202,6 @@ const QueryPanel = ({
dataQueriesChanged={updateDataQueries}
appId={appId}
darkMode={darkMode}
- apps={apps}
allComponents={allComponents}
appDefinition={appDefinition}
editorRef={editorRef}
diff --git a/frontend/src/Editor/RealtimeAvatars.jsx b/frontend/src/Editor/RealtimeAvatars.jsx
index 5bc3196767..cdd5c541a8 100644
--- a/frontend/src/Editor/RealtimeAvatars.jsx
+++ b/frontend/src/Editor/RealtimeAvatars.jsx
@@ -1,8 +1,9 @@
-import React from 'react';
+import React, { useEffect } from 'react';
import Popover from '@/_ui/Popover';
import Avatar from '@/_ui/Avatar';
// eslint-disable-next-line import/no-unresolved
import { useOthers, useSelf } from '@y-presence/react';
+import { useAppDataActions, useAppInfo } from '@/_stores/appDataStore';
const MAX_DISPLAY_USERS = 2;
const RealtimeAvatars = ({ darkMode }) => {
@@ -17,6 +18,17 @@ const RealtimeAvatars = ({ darkMode }) => {
const getAvatarText = (presence) => presence.firstName?.charAt(0) + presence.lastName?.charAt(0);
const getAvatarTitle = (presence) => `${presence.firstName} ${presence.lastName}`;
+ const { updateState } = useAppDataActions();
+ const { areOthersOnSameVersionAndPage, currentVersionId } = useAppInfo();
+
+ useEffect(() => {
+ const areActiveUsersOnSameVersionAndPage = othersOnSameVersionAndPage.length > 0;
+ const shouldUpdateState = areActiveUsersOnSameVersionAndPage !== areOthersOnSameVersionAndPage;
+
+ if (shouldUpdateState) updateState({ areOthersOnSameVersionAndPage: areActiveUsersOnSameVersionAndPage });
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [JSON.stringify({ others, self, currentVersionId })]);
+
const popoverContent = () => {
return othersOnSameVersionAndPage
.slice(MAX_DISPLAY_USERS, othersOnSameVersionAndPage.length)
diff --git a/frontend/src/Editor/ReleaseVersionButton.jsx b/frontend/src/Editor/ReleaseVersionButton.jsx
index 0425b2fca4..04973bbd03 100644
--- a/frontend/src/Editor/ReleaseVersionButton.jsx
+++ b/frontend/src/Editor/ReleaseVersionButton.jsx
@@ -8,13 +8,7 @@ import { ConfirmDialog } from '@/_components/ConfirmDialog';
import { shallow } from 'zustand/shallow';
import { ButtonSolid } from '@/_ui/AppButton/AppButton';
-export const ReleaseVersionButton = function DeployVersionButton({
- appId,
- appName,
- fetchApp,
- onVersionRelease,
- saveEditingVersion,
-}) {
+export const ReleaseVersionButton = function DeployVersionButton({ appId, appName, fetchApp, onVersionRelease }) {
const [isReleasing, setIsReleasing] = useState(false);
const { isVersionReleased, editingVersion } = useAppVersionStore(
(state) => ({
@@ -29,7 +23,7 @@ export const ReleaseVersionButton = function DeployVersionButton({
const releaseVersion = (editingVersion) => {
setShowPageDeletionConfirmation(false);
setIsReleasing(true);
- saveEditingVersion();
+
appsService
.saveApp(appId, {
name: appName,
diff --git a/frontend/src/Editor/SubContainer.jsx b/frontend/src/Editor/SubContainer.jsx
index 4f5edad122..06f8d0758d 100644
--- a/frontend/src/Editor/SubContainer.jsx
+++ b/frontend/src/Editor/SubContainer.jsx
@@ -15,6 +15,8 @@ import { useCurrentState } from '@/_stores/currentStateStore';
import { useAppVersionStore } from '@/_stores/appVersionStore';
import { shallow } from 'zustand/shallow';
import { useMounted } from '@/_hooks/use-mount';
+// eslint-disable-next-line import/no-unresolved
+import { diff } from 'deep-object-diff';
const NO_OF_GRIDS = 43;
@@ -92,17 +94,19 @@ export const SubContainer = ({
false;
const getChildWidgets = (components) => {
- let childWidgets = [];
+ let childWidgets = {};
Object.keys(components).forEach((key) => {
- if (components[key].parent === parent) {
+ const componentParent = components[key].component.parent;
+ if (componentParent === parent) {
childWidgets[key] = { ...components[key], component: { ...components[key]['component'], parent } };
}
});
+
return childWidgets;
};
const [boxes, setBoxes] = useState(allComponents);
- const [childWidgets, setChildWidgets] = useState(() => getChildWidgets(allComponents));
+ const [childWidgets, setChildWidgets] = useState([]);
const [isDragging, setIsDragging] = useState(false);
const [isResizing, setIsResizing] = useState(false);
// const [subContainerHeight, setSubContainerHeight] = useState('100%'); //used to determine the height of the sub container for modal
@@ -111,6 +115,7 @@ export const SubContainer = ({
useEffect(() => {
setBoxes(allComponents);
setChildWidgets(() => getChildWidgets(allComponents));
+
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [allComponents, parent]);
@@ -183,8 +188,11 @@ export const SubContainer = ({
);
_.set(childrenBoxes, newComponent.id, {
- component: newComponent.component,
- parent: parentComponent.component === 'Tabs' ? parentId + '-' + tab : parentId,
+ component: {
+ ...newComponent.component,
+ parent: parentComponent.component === 'Tabs' ? parentId + '-' + tab : parentId,
+ },
+
layouts: {
[currentLayout]: {
...layout,
@@ -233,7 +241,23 @@ export const SubContainer = ({
},
},
};
- appDefinitionChanged(newDefinition);
+
+ const oldComponents = appDefinition.pages[currentPageId]?.components ?? {};
+ const newComponents = boxes;
+
+ const componendAdded = Object.keys(newComponents).length > Object.keys(oldComponents).length;
+
+ const opts = { containerChanges: true };
+
+ if (componendAdded) {
+ opts.componentAdded = true;
+ }
+
+ const shouldUpdate = !_.isEmpty(diff(appDefinition, newDefinition));
+
+ if (shouldUpdate) {
+ appDefinitionChanged(newDefinition, opts);
+ }
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [boxes]);
@@ -271,6 +295,7 @@ export const SubContainer = ({
}
});
+ //!Todo: need to check: this never gets called as draggingState is always false
useEffect(() => {
setIsDragging(draggingState);
}, [draggingState]);
@@ -306,8 +331,10 @@ export const SubContainer = ({
setBoxes({
...boxes,
[newComponent.id]: {
- component: newComponent.component,
- parent: parentRef.current.id,
+ component: {
+ ...newComponent.component,
+ parent: parentRef.current.id,
+ },
layouts: {
...newComponent.layout,
},
@@ -357,6 +384,7 @@ export const SubContainer = ({
enableReleasedVersionPopupState();
return;
}
+
const canvasWidth = getContainerCanvasWidth();
const nodeBounds = direction.node.getBoundingClientRect();
@@ -430,7 +458,12 @@ export const SubContainer = ({
}
//round the width to nearest multiple of gridwidth before converting to %
- const currentWidth = (_containerCanvasWidth * width) / NO_OF_GRIDS;
+ let currentWidth = (_containerCanvasWidth * width) / NO_OF_GRIDS;
+
+ if (currentWidth > _containerCanvasWidth) {
+ currentWidth = _containerCanvasWidth;
+ }
+
let newWidth = currentWidth + deltaWidth;
newWidth = Math.round(newWidth / gridWidth) * gridWidth;
width = (newWidth * NO_OF_GRIDS) / _containerCanvasWidth;
@@ -536,9 +569,11 @@ export const SubContainer = ({
Object.keys(childWidgets).map((key) => {
const addDefaultChildren = childWidgets[key]['withDefaultChildren'] || false;
const box = childWidgets[key];
+
const canShowInCurrentLayout =
box.component.definition.others[currentLayout === 'mobile' ? 'showOnMobile' : 'showOnDesktop'].value;
- if (box.parent && resolveReferences(canShowInCurrentLayout, currentState)) {
+
+ if (box.component.parent && resolveReferences(canShowInCurrentLayout, currentState)) {
return (
{
- const copyDefinition = _.cloneDeep(data.definition);
- const pagesObj = copyDefinition.pages || {};
-
- const newDefinition = {
- ...copyDefinition,
- pages: pagesObj,
+ 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: newDefinition || { components: {} },
+ appDefinition: { ...appDefData },
+ pages: appDefData.pages,
});
};
- setStateForContainer = async (data) => {
+ setStateForContainer = async (data, appVersionId) => {
+ const appDefData = buildAppDefinition(data);
+
const currentUser = this.state.currentUser;
let userVars = {};
@@ -94,38 +108,57 @@ class ViewerComponent extends React.Component {
let mobileLayoutHasWidgets = false;
if (this.props.currentLayout === 'mobile') {
- const currentComponents = data.definition.pages[data.definition.homePageId].components;
+ const currentComponents = appDefData.pages[appDefData.homePageId].components;
mobileLayoutHasWidgets =
Object.keys(currentComponents).filter((componentId) => currentComponents[componentId]['layouts']['mobile'])
.length > 0;
}
let queryState = {};
- data.data_queries.forEach((query) => {
- if (query.pluginId || query?.plugin?.id) {
- queryState[query.name] = {
- ...query.plugin.manifestFile.data.source.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],
- };
- }
- });
+ 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) {
+ queryState[query.name] = {
+ ...query.plugin.manifestFile.data.source.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 = Object.entries(data.definition.pages).map(([pageId, page]) => ({ id: pageId, ...page }));
- const homePageId = data.definition.homePageId;
+ 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(data.data_queries);
+ useDataQueriesStore.getState().actions.setDataQueries(dataQueries);
this.props.setCurrentState({
queries: queryState,
components: {},
@@ -148,6 +181,7 @@ class ViewerComponent extends React.Component {
...constants,
});
useEditorStore.getState().actions.toggleCurrentLayout(mobileLayoutHasWidgets ? 'mobile' : 'desktop');
+ this.props.updateState({ events: data.events ?? [] });
this.setState(
{
currentUser,
@@ -159,21 +193,24 @@ class ViewerComponent extends React.Component {
? `${this.state.deviceWindowWidth}px`
: '1292px',
selectedComponent: null,
- dataQueries: data.data_queries,
+ dataQueries: dataQueries,
currentPageId: currentPage.id,
- pages: {},
- homepage: this.state.appDefinition?.pages?.[this.state.appDefinition?.homePageId]?.handle,
+ homepage: appDefData?.pages?.[this.state.appDefinition?.homePageId]?.handle,
+ events: data.events ?? [],
},
() => {
- computeComponentState(this, data?.definition?.pages[currentPage.id]?.components).then(async () => {
- this.setState({ initialComputationOfStateDone: true });
+ 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(data.data_queries);
- // eslint-disable-next-line no-unsafe-optional-chaining
- const { events } = this.state.appDefinition?.pages[this.state.currentPageId];
- for (const event of events ?? []) {
- await this.handleEvent(event.eventId, event);
- }
+ this.runQueries(dataQueries);
+
+ const currentPageEvents = this.state.events.filter(
+ (event) => event.target === 'page' && event.sourceId === this.state.currentPageId
+ );
+
+ await this.handleEvent('onPageLoad', currentPageEvents);
});
}
);
@@ -182,7 +219,7 @@ class ViewerComponent extends React.Component {
runQueries = (data_queries) => {
data_queries.forEach((query) => {
if (query.options.runOnPageLoad && isQueryRunnable(query)) {
- runQuery(this, query.id, query.name, undefined, 'view');
+ runQuery(this.getViewerRef(), query.id, query.name, undefined, 'view');
}
});
};
@@ -239,13 +276,14 @@ class ViewerComponent extends React.Component {
};
loadApplicationBySlug = (slug, authentication_failed = false) => {
- appsService
- .getAppBySlug(slug)
+ appService
+ .fetchAppBySlug(slug)
.then((data) => {
- if (authentication_failed && !data.current_version_id) {
- redirectToErrorPage(ERROR_TYPES.URL_UNAVAILABLE, {});
+ const isAppPublic = data?.is_public;
+ if (authentication_failed && !isAppPublic) {
+ return redirectToErrorPage(ERROR_TYPES.URL_UNAVAILABLE, {});
}
- this.setStateForApp(data);
+ this.setStateForApp(data, true);
this.setStateForContainer(data);
this.setWindowTitle(data.name);
})
@@ -265,11 +303,11 @@ class ViewerComponent extends React.Component {
};
loadApplicationByVersion = (appId, versionId) => {
- appsService
- .getAppByVersion(appId, versionId)
+ appService
+ .fetchAppByVersion(appId, versionId)
.then((data) => {
this.setStateForApp(data);
- this.setStateForContainer(data);
+ this.setStateForContainer(data, versionId);
})
.catch(() => {
this.setState({
@@ -278,6 +316,9 @@ class ViewerComponent extends React.Component {
});
};
+ updateQueryConfirmationList = (queryConfirmationList) =>
+ useEditorStore.getState().actions.updateQueryConfirmationList(queryConfirmationList);
+
setupViewer() {
this.subscription = authenticationService.currentSession.subscribe((currentSession) => {
const slug = this.props.params.slug;
@@ -306,6 +347,7 @@ class ViewerComponent extends React.Component {
userVars,
versionId,
});
+
versionId ? this.loadApplicationByVersion(appId, versionId) : this.loadApplicationBySlug(slug);
} else if (currentSession?.authentication_failed) {
this.loadApplicationBySlug(slug, true);
@@ -352,9 +394,13 @@ class ViewerComponent extends React.Component {
handlePageSwitchingBasedOnURLparam() {
const handleOnURL = this.props.params.pageHandle;
- const pageIdCorrespondingToHandleOnURL = handleOnURL
- ? this.findPageIdFromHandle(handleOnURL)
- : this.state.appDefinition.homePageId;
+
+ const shouldShowPage = handleOnURL ? this.validatePageHandle(handleOnURL) : true;
+
+ if (!shouldShowPage) return this.switchPage(this.state.appDefinition.homePageId);
+
+ const pageIdCorrespondingToHandleOnURL =
+ handleOnURL && shouldShowPage ? this.findPageIdFromHandle(handleOnURL) : this.state.appDefinition.homePageId;
const currentPageId = this.state.currentPageId;
if (pageIdCorrespondingToHandleOnURL != this.state.currentPageId) {
@@ -388,20 +434,23 @@ class ViewerComponent extends React.Component {
name: targetPage.name,
},
async () => {
- computeComponentState(this, this.state.appDefinition?.pages[this.state.currentPageId].components).then(
- async () => {
- // eslint-disable-next-line no-unsafe-optional-chaining
- const { events } = this.state.appDefinition?.pages[this.state.currentPageId];
- for (const event of events ?? []) {
- await this.handleEvent(event.eventId, event);
- }
- }
- );
+ computeComponentState(this.state.appDefinition?.pages[this.state.currentPageId].components).then(async () => {
+ const currentPageEvents = this.state.events.filter(
+ (event) => event.target === 'page' && event.sourceId === this.state.currentPageId
+ );
+
+ await this.handleEvent('onPageLoad', currentPageEvents);
+ });
}
);
}
}
+ validatePageHandle(handle) {
+ const allPages = this.state.appDefinition.pages;
+ return Object.values(allPages).some((page) => page.handle === handle && !page.disabled);
+ }
+
findPageIdFromHandle(handle) {
return (
Object.entries(this.state.appDefinition.pages).filter(([_id, page]) => page.handle === handle)?.[0]?.[0] ??
@@ -466,7 +515,9 @@ class ViewerComponent extends React.Component {
);
};
- handleEvent = (eventName, options) => onEvent(this, eventName, options, 'view');
+ handleEvent = (eventName, events, options) => {
+ return onEvent(this.getViewerRef(), eventName, events, options, 'view');
+ };
computeCanvasMaxWidth = () => {
const { appDefinition } = this.state;
@@ -493,11 +544,11 @@ class ViewerComponent extends React.Component {
deviceWindowWidth,
defaultComponentStateComputed,
dataQueries,
- queryConfirmationList,
canvasWidth,
} = this.state;
const currentCanvasWidth = canvasWidth;
+ const queryConfirmationList = this.props?.queryConfirmationList ?? [];
const canvasMaxWidth = this.computeCanvasMaxWidth();
@@ -531,8 +582,10 @@ class ViewerComponent extends React.Component {
0}
message={'Do you want to run this query?'}
- onConfirm={(queryConfirmationData) => onQueryConfirmOrCancel(this, queryConfirmationData, true, 'view')}
- onCancel={() => onQueryConfirmOrCancel(this, queryConfirmationList[0], false, 'view')}
+ onConfirm={(queryConfirmationData) =>
+ onQueryConfirmOrCancel(this.getViewerRef(), queryConfirmationData, true, 'view')
+ }
+ onCancel={() => onQueryConfirmOrCancel(this.getViewerRef(), queryConfirmationList[0], false, 'view')}
queryConfirmationData={queryConfirmationList[0]}
key={queryConfirmationList[0]?.queryName}
/>
@@ -591,7 +644,7 @@ class ViewerComponent extends React.Component {
snapToGrid={true}
appLoading={isLoading}
darkMode={this.props.darkMode}
- onEvent={(eventName, options) => onEvent(this, eventName, options, 'view')}
+ onEvent={this.handleEvent}
mode="view"
deviceWindowWidth={deviceWindowWidth}
selectedComponent={this.state.selectedComponent}
@@ -602,11 +655,9 @@ class ViewerComponent extends React.Component {
onComponentClick(this, id, component, 'view');
}}
onComponentOptionChanged={(component, optionName, value) => {
- return onComponentOptionChanged(this, component, optionName, value);
+ return onComponentOptionChanged(component, optionName, value);
}}
- onComponentOptionsChanged={(component, options) =>
- onComponentOptionsChanged(this, component, options)
- }
+ onComponentOptionsChanged={onComponentOptionsChanged}
canvasWidth={this.getCanvasWidth()}
dataQueries={dataQueries}
currentPageId={this.state.currentPageId}
@@ -629,19 +680,23 @@ class ViewerComponent extends React.Component {
}
const withStore = (Component) => (props) => {
const currentState = useCurrentStateStore();
- const { currentLayout } = useEditorStore(
+ const { currentLayout, queryConfirmationList } = useEditorStore(
(state) => ({
currentLayout: state?.currentLayout,
+ queryConfirmationList: state?.queryConfirmationList,
}),
shallow
);
+ const { updateState } = useAppDataActions();
return (
);
};
diff --git a/frontend/src/HomePage/ExportAppModal.jsx b/frontend/src/HomePage/ExportAppModal.jsx
index 8942deb386..d523f1d04b 100644
--- a/frontend/src/HomePage/ExportAppModal.jsx
+++ b/frontend/src/HomePage/ExportAppModal.jsx
@@ -54,7 +54,7 @@ export default function ExportAppModal({ title, show, closeModal, customClassNam
const requestBody = {
...appOpts,
...(exportTjDb && { tooljet_database: tables }),
- organization_id: app.organization_id,
+ organization_id: app.organization_id ?? app.organizationId,
};
appsService
diff --git a/frontend/src/_helpers/appUtils.js b/frontend/src/_helpers/appUtils.js
index 130374910b..2ce5c10812 100644
--- a/frontend/src/_helpers/appUtils.js
+++ b/frontend/src/_helpers/appUtils.js
@@ -32,6 +32,8 @@ import { useDataQueriesStore } from '@/_stores/dataQueriesStore';
import { useQueryPanelStore } from '@/_stores/queryPanelStore';
import { useCurrentStateStore, getCurrentState } from '@/_stores/currentStateStore';
import { useAppVersionStore } from '@/_stores/appVersionStore';
+import { camelizeKeys } from 'humps';
+import { useAppDataStore } from '@/_stores/appDataStore';
import { useEditorStore } from '@/_stores/editorStore';
const ERROR_TYPES = Object.freeze({
@@ -60,7 +62,7 @@ export function setCurrentStateAsync(_ref, changes) {
});
}
-export function onComponentOptionsChanged(_ref, component, options) {
+export function onComponentOptionsChanged(component, options) {
const componentName = component.name;
const components = getCurrentState().components;
let componentData = components[componentName];
@@ -76,7 +78,7 @@ export function onComponentOptionsChanged(_ref, component, options) {
return Promise.resolve();
}
-export function onComponentOptionChanged(_ref, component, option_name, value) {
+export function onComponentOptionChanged(component, option_name, value) {
const componentName = component.name;
const components = getCurrentState().components;
let componentData = components[componentName];
@@ -322,12 +324,11 @@ export async function runTransformation(
}
}
-export async function executeActionsForEventId(_ref, eventId, component, mode, customVariables) {
- const events = component?.definition?.events || [];
- const filteredEvents = events.filter((event) => event.eventId === eventId);
+export async function executeActionsForEventId(_ref, eventId, events = [], mode, customVariables) {
+ const filteredEvents = events.filter((event) => event?.event.eventId === eventId);
for (const event of filteredEvents) {
- await executeAction(_ref, event, mode, customVariables); // skipcq: JS-0032
+ await executeAction(_ref, event.event, mode, customVariables); // skipcq: JS-0032
}
}
@@ -336,13 +337,11 @@ export function onComponentClick(_ref, id, component, mode = 'edit') {
}
export function onQueryConfirmOrCancel(_ref, queryConfirmationData, isConfirm = false, mode = 'edit') {
- const filtertedQueryConfirmation = _ref.state?.queryConfirmationList.filter(
+ const filtertedQueryConfirmation = _ref?.queryConfirmationList.filter(
(query) => query.queryId !== queryConfirmationData.queryId
);
- _ref.setState({
- queryConfirmationList: filtertedQueryConfirmation,
- });
+ _ref.updateQueryConfirmationList(filtertedQueryConfirmation, 'check');
isConfirm && runQuery(_ref, queryConfirmationData.queryId, queryConfirmationData.queryName, true, mode);
}
@@ -362,7 +361,7 @@ function showModal(_ref, modal, show) {
return Promise.resolve();
}
- const modalMeta = _ref.state.appDefinition.pages[_ref.state.currentPageId].components[modalId];
+ const modalMeta = _ref.appDefinition.pages[_ref.currentPageId].components[modalId]; //! NeedToFix
const _components = {
...getCurrentState().components,
@@ -377,7 +376,7 @@ function showModal(_ref, modal, show) {
return Promise.resolve();
}
-function logoutAction(_ref) {
+function logoutAction() {
localStorage.clear();
authenticationService.logout(true);
@@ -442,7 +441,7 @@ function executeActionWithDebounce(_ref, event, mode, customVariables) {
return runQuery(_ref, queryId, name, undefined, mode, resolvedParams);
}
case 'logout': {
- return logoutAction(_ref);
+ return logoutAction();
}
case 'open-webpage': {
@@ -477,7 +476,7 @@ function executeActionWithDebounce(_ref, event, mode, customVariables) {
}
if (mode === 'view') {
- _ref.props.navigate(url);
+ _ref.navigate(url);
} else {
if (confirm('The app will be opened in a new tab as the action is triggered from the editor.')) {
window.open(urlJoin(window.public_config?.TOOLJET_HOST, url));
@@ -522,7 +521,7 @@ function executeActionWithDebounce(_ref, event, mode, customVariables) {
}
case 'set-table-page': {
- setTablePageIndex(_ref, event.table, event.pageIndex);
+ setTablePageIndex(event.table, event.pageIndex);
break;
}
@@ -585,12 +584,13 @@ function executeActionWithDebounce(_ref, event, mode, customVariables) {
}
case 'switch-page': {
- const { name, disabled } = _ref.state.appDefinition.pages[event.pageId];
+ const { name, disabled } = _ref.appDefinition.pages[event.pageId];
+
// Don't allow switching to disabled page in editor as well as viewer
if (!disabled) {
_ref.switchPage(event.pageId, resolveReferences(event.queryParams, getCurrentState(), [], customVariables));
}
- if (_ref.state.appDefinition.pages[event.pageId]) {
+ if (_ref.appDefinition.pages[event.pageId]) {
if (disabled) {
const generalProps = {
navToDisablePage: {
@@ -612,12 +612,15 @@ function executeActionWithDebounce(_ref, event, mode, customVariables) {
}
}
-export async function onEvent(_ref, eventName, options, mode = 'edit') {
+export async function onEvent(_ref, eventName, events, options = {}, mode = 'edit') {
let _self = _ref;
const { customVariables } = options;
if (eventName === 'onPageLoad') {
- await executeActionsForEventId(_ref, 'onPageLoad', { definition: { events: [options] } }, mode, customVariables);
+ //hack to make sure that the page is loaded before executing the actions
+ setTimeout(async () => {
+ return await executeActionsForEventId(_ref, 'onPageLoad', events, mode, customVariables);
+ }, 0);
}
if (eventName === 'onTrigger') {
@@ -635,6 +638,7 @@ export async function onEvent(_ref, eventName, options, mode = 'edit') {
if (eventName === 'onCalendarEventSelect') {
const { component, calendarEvent } = options;
+
useCurrentStateStore.getState().actions.setCurrentState({
components: {
...getCurrentState().components,
@@ -644,7 +648,8 @@ export async function onEvent(_ref, eventName, options, mode = 'edit') {
},
},
});
- executeActionsForEventId(_ref, 'onCalendarEventSelect', component, mode, customVariables);
+
+ executeActionsForEventId(_ref, 'onCalendarEventSelect', events, mode, customVariables);
}
if (eventName === 'onCalendarSlotSelect') {
@@ -658,26 +663,18 @@ export async function onEvent(_ref, eventName, options, mode = 'edit') {
},
},
});
- executeActionsForEventId(_ref, 'onCalendarSlotSelect', component, mode, customVariables);
+
+ executeActionsForEventId(_ref, 'onCalendarSlotSelect', events, mode, customVariables);
}
if (eventName === 'onTableActionButtonClicked') {
- const { component, data, action, rowId } = options;
- useCurrentStateStore.getState().actions.setCurrentState({
- components: {
- ...getCurrentState().components,
- [component.name]: {
- ...getCurrentState().components[component.name],
- selectedRow: data,
- selectedRowId: rowId,
- },
- },
- });
- if (action && action.events) {
- for (const event of action.events) {
- if (event.actionId) {
- // the event param uses a hacky workaround for using same format used by event manager ( multiple handlers )
- await executeAction(_self, { ...event, ...event.options }, mode, customVariables);
+ const { action, tableActionEvents } = options;
+ const executeableActions = tableActionEvents.filter((event) => event?.event?.ref === action?.name);
+
+ if (action && executeableActions) {
+ for (const event of executeableActions) {
+ if (event?.event?.actionId) {
+ await executeAction(_self, event.event, mode, customVariables);
}
}
} else {
@@ -686,23 +683,12 @@ export async function onEvent(_ref, eventName, options, mode = 'edit') {
}
if (eventName === 'OnTableToggleCellChanged') {
- const { component, column, rowId, row } = options;
- useCurrentStateStore.getState().actions.setCurrentState({
- components: {
- ...getCurrentState().components,
- [component.name]: {
- ...getCurrentState().components[component.name],
- selectedRow: row,
- selectedRowId: rowId,
- },
- },
- });
+ const { column, tableColumnEvents } = options;
- if (column && column.events) {
- for (const event of column.events) {
- if (event.actionId) {
- // the event param uses a hacky workaround for using same format used by event manager ( multiple handlers )
- await executeAction(_self, { ...event, ...event.options }, mode, customVariables);
+ if (column && tableColumnEvents) {
+ for (const event of tableColumnEvents) {
+ if (event?.event?.actionId) {
+ await executeAction(_self, event.event, mode, customVariables);
}
}
} else {
@@ -763,18 +749,15 @@ export async function onEvent(_ref, eventName, options, mode = 'edit') {
'onNewRowsAdded',
].includes(eventName)
) {
- const { component } = options;
- executeActionsForEventId(_ref, eventName, component, mode, customVariables);
+ executeActionsForEventId(_ref, eventName, events, mode, customVariables);
}
if (eventName === 'onBulkUpdate') {
- onComponentOptionChanged(_self, options.component, 'isSavingChanges', true);
- await executeActionsForEventId(_self, eventName, options.component, mode, customVariables);
- onComponentOptionChanged(_self, options.component, 'isSavingChanges', false);
+ await executeActionsForEventId(_self, eventName, events, mode, customVariables);
}
if (['onDataQuerySuccess', 'onDataQueryFailure'].includes(eventName)) {
- await executeActionsForEventId(_self, eventName, options, mode, customVariables);
+ await executeActionsForEventId(_self, eventName, events, mode, customVariables);
}
}
@@ -940,6 +923,10 @@ export function previewQuery(_ref, query, calledFromQuery = false, parameters =
export function runQuery(_ref, queryId, queryName, confirmed = undefined, mode = 'edit', parameters = {}) {
const query = useDataQueriesStore.getState().dataQueries.find((query) => query.id === queryId);
+ const queryEvents = useAppDataStore
+ .getState()
+ .events.filter((event) => event.target === 'data_query' && event.sourceId === queryId);
+
let dataQuery = {};
if (query) {
@@ -951,9 +938,11 @@ export function runQuery(_ref, queryId, queryName, confirmed = undefined, mode =
const options = getQueryVariables(dataQuery.options, getCurrentState());
- if (dataQuery.options.requestConfirmation) {
- // eslint-disable-next-line no-unsafe-optional-chaining
- const queryConfirmationList = _ref.state?.queryConfirmationList ? [..._ref.state?.queryConfirmationList] : [];
+ if (dataQuery.options?.requestConfirmation) {
+ const queryConfirmationList = useEditorStore.getState().queryConfirmationList
+ ? [...useEditorStore.getState().queryConfirmationList]
+ : [];
+
const queryConfirmation = {
queryId,
queryName,
@@ -963,9 +952,8 @@ export function runQuery(_ref, queryId, queryName, confirmed = undefined, mode =
}
if (confirmed === undefined) {
- _ref.setState({
- queryConfirmationList,
- });
+ //!check
+ _ref.updateQueryConfirmationList(queryConfirmationList);
return;
}
}
@@ -1071,9 +1059,7 @@ export function runQuery(_ref, queryId, queryName, confirmed = undefined, mode =
},
});
resolve(data);
- onEvent(_self, 'onDataQueryFailure', {
- definition: { events: dataQuery.options.events },
- });
+ onEvent(_self, 'onDataQueryFailure', queryEvents);
if (mode !== 'view') {
const err = query.kind == 'tooljetdb' ? data?.error || data : _.isEmpty(data.data) ? data : data.data;
toast.error(err?.message);
@@ -1111,9 +1097,7 @@ export function runQuery(_ref, queryId, queryName, confirmed = undefined, mode =
},
});
resolve(finalData);
- onEvent(_self, 'onDataQueryFailure', {
- definition: { events: dataQuery.options.events },
- });
+ onEvent(_self, 'onDataQueryFailure', queryEvents);
return;
}
}
@@ -1152,7 +1136,7 @@ export function runQuery(_ref, queryId, queryName, confirmed = undefined, mode =
},
});
resolve({ status: 'ok', data: finalData });
- onEvent(_self, 'onDataQuerySuccess', { definition: { events: dataQuery.options.events } }, mode);
+ onEvent(_self, 'onDataQuerySuccess', queryEvents, mode);
}
})
.catch(({ error }) => {
@@ -1171,7 +1155,7 @@ export function runQuery(_ref, queryId, queryName, confirmed = undefined, mode =
});
}
-export function setTablePageIndex(_ref, tableId, index) {
+export function setTablePageIndex(tableId, index) {
if (_.isEmpty(tableId)) {
console.log('No table is associated with this event.');
return Promise.resolve();
@@ -1192,52 +1176,69 @@ export function renderTooltip({ props, text }) {
);
}
-export function computeComponentState(_ref, components = {}) {
- let componentState = {};
- const currentComponents = getCurrentState().components;
- Object.keys(components).forEach((key) => {
- const component = components[key];
- const componentMeta = componentTypes.find((comp) => component.component.component === comp.component);
+/*
+@computeComponentState: (components = {}) => Promise
+This change is made to enhance the code readability by optimizing the logic
+for computing component state. It replaces the previous try-catch block with
+a more efficient approach, precomputing the parent component types and using
+conditional checks for better performance and error handling.*/
- const existingComponentName = Object.keys(currentComponents).find((comp) => currentComponents[comp].id === key);
- const existingValues = currentComponents[existingComponentName];
+export function computeComponentState(components = {}) {
+ try {
+ let componentState = {};
+ const currentComponents = getCurrentState().components;
- if (component.parent) {
- const parentComponent = components[component.parent];
- let isListView = false,
- isForm = false;
- try {
- isListView = parentComponent.component.component === 'Listview';
- isForm = parentComponent.component.component === 'Form';
- } catch {
- console.log('error');
- }
+ // Precompute parent component types
+ const parentComponentTypes = {};
+ Object.keys(components).forEach((key) => {
+ const { component } = components[key];
+ parentComponentTypes[key] = component.component;
+ });
- if (!isListView && !isForm) {
- componentState[component.component.name] = {
+ Object.keys(components).forEach((key) => {
+ if (!components[key]) return;
+
+ const { component } = components[key];
+ const componentMeta = componentTypes.find((comp) => component.component === comp.component);
+
+ const existingComponentName = Object.keys(currentComponents).find((comp) => currentComponents[comp].id === key);
+ const existingValues = currentComponents[existingComponentName];
+
+ if (component.parent) {
+ const parentComponentType = parentComponentTypes[component.parent];
+
+ if (parentComponentType !== 'Listview' && parentComponentType !== 'Form') {
+ componentState[component.name] = {
+ ...componentMeta.exposedVariables,
+ id: key,
+ ...existingValues,
+ };
+ }
+ } else {
+ componentState[component.name] = {
...componentMeta.exposedVariables,
id: key,
...existingValues,
};
}
- } else {
- componentState[component.component.name] = {
- ...componentMeta.exposedVariables,
- id: key,
- ...existingValues,
- };
- }
- });
+ });
- useCurrentStateStore.getState().actions.setCurrentState({
- components: {
- ...componentState,
- },
- });
+ useCurrentStateStore.getState().actions.setCurrentState({
+ components: {
+ ...componentState,
+ },
+ });
- return setStateAsync(_ref, {
- defaultComponentStateComputed: true,
- });
+ return new Promise((resolve) => {
+ useEditorStore.getState().actions.updateEditorState({
+ defaultComponentStateComputed: true,
+ });
+ resolve();
+ });
+ } catch (error) {
+ console.log(error);
+ return Promise.reject(error);
+ }
}
export const getSvgIcon = (key, height = 50, width = 50, iconFile = undefined, styles = {}) => {
@@ -1253,13 +1254,13 @@ export const getSvgIcon = (key, height = 50, width = 50, iconFile = undefined, s
};
export const debuggerActions = {
- error: (_self, errors) => {
+ error: (errors) => {
useCurrentStateStore.getState().actions.setErrors({
...errors,
});
},
- flush: (_self) => {
+ flush: () => {
useCurrentStateStore.getState().actions.setCurrentState({
errors: {},
});
@@ -1370,105 +1371,148 @@ export const getComponentName = (currentState, id) => {
}
};
-const updateNewComponents = (pageId, appDefinition, newComponents, updateAppDefinition) => {
+const updateNewComponents = (pageId, appDefinition, newComponents, updateAppDefinition, componentMap, isCut) => {
const newAppDefinition = JSON.parse(JSON.stringify(appDefinition));
- newComponents.forEach((newComponent) => {
- newComponent.component.name = computeComponentName(
- newComponent.component.component,
- newAppDefinition.pages[pageId].components
- );
- newAppDefinition.pages[pageId].components[newComponent.id] = newComponent;
- });
- updateAppDefinition(newAppDefinition);
+
+ newAppDefinition.pages[pageId].components = {
+ ...newAppDefinition.pages[pageId].components,
+ ...newComponents,
+ };
+
+ const opts = {
+ componentAdded: true,
+ containerChanges: true,
+ };
+
+ if (!isCut) {
+ opts.cloningComponent = componentMap;
+ }
+
+ updateAppDefinition(newAppDefinition, opts);
};
-export const cloneComponents = (_ref, updateAppDefinition, isCloning = true, isCut = false) => {
- const { appDefinition, currentPageId } = _ref.state;
- const selectedComponents = useEditorStore.getState().selectedComponents;
+export const cloneComponents = (
+ selectedComponents,
+ appDefinition,
+ currentPageId,
+ updateAppDefinition,
+ isCloning = true,
+ isCut = false
+) => {
if (selectedComponents.length < 1) return getSelectedText();
+
const { components: allComponents } = appDefinition.pages[currentPageId];
+
+ // if parent is selected, then remove the parent from the selected components
+ const filteredSelectedComponents = selectedComponents.filter((component) => {
+ const parentComponentId = component.component?.parent;
+ if (parentComponentId) {
+ // Check if the parent component is also selected
+ const isParentSelected = selectedComponents.some((comp) => comp.id === parentComponentId);
+
+ // If the parent is selected, filter out the child component
+ if (isParentSelected) {
+ return false;
+ }
+ }
+ return true;
+ });
+
let newDefinition = _.cloneDeep(appDefinition);
let newComponents = [],
newComponentObj = {},
addedComponentId = new Set();
- for (let selectedComponent of selectedComponents) {
+ for (let selectedComponent of filteredSelectedComponents) {
if (addedComponentId.has(selectedComponent.id)) continue;
const component = {
- id: selectedComponent.id,
component: allComponents[selectedComponent.id]?.component,
layouts: allComponents[selectedComponent.id]?.layouts,
parent: allComponents[selectedComponent.id]?.parent,
+ componentId: selectedComponent.id,
};
addedComponentId.add(selectedComponent.id);
let clonedComponent = JSON.parse(JSON.stringify(component));
- clonedComponent.parent = undefined;
- clonedComponent.children = [];
- clonedComponent.children = [...getChildComponents(allComponents, component, clonedComponent, addedComponentId)];
- newComponents = [...newComponents, clonedComponent];
+
+ newComponents.push(clonedComponent);
+ const children = getAllChildComponents(allComponents, selectedComponent.id);
+
+ if (children.length > 0) {
+ newComponents.push(...children);
+ }
+
newComponentObj = {
newComponents,
isCloning,
isCut,
+ currentPageId,
};
}
+
if (isCloning) {
- addComponents(currentPageId, appDefinition, updateAppDefinition, undefined, newComponentObj);
+ const parentId = selectedComponents[0]['component']?.parent ?? undefined;
+
+ addComponents(currentPageId, appDefinition, updateAppDefinition, parentId, newComponentObj, true);
toast.success('Component cloned succesfully');
} else if (isCut) {
navigator.clipboard.writeText(JSON.stringify(newComponentObj));
- removeSelectedComponent(currentPageId, newDefinition, selectedComponents);
- updateAppDefinition(newDefinition);
+ removeSelectedComponent(currentPageId, newDefinition, selectedComponents, updateAppDefinition);
} else {
navigator.clipboard.writeText(JSON.stringify(newComponentObj));
const successMessage =
newComponentObj.newComponents.length > 1 ? 'Components copied successfully' : 'Component copied successfully';
toast.success(successMessage);
}
- _ref.setState({ currentSidebarTab: 2 });
+
+ return new Promise((resolve) => {
+ useEditorStore.getState().actions.updateEditorState({
+ currentSidebarTab: 2,
+ });
+ resolve();
+ });
};
-const getChildComponents = (allComponents, component, parentComponent, addedComponentId) => {
- let childComponents = [],
- selectedChildComponents = [];
+const getAllChildComponents = (allComponents, parentId) => {
+ const childComponents = [];
- if (component.component.component === 'Tabs' || component.component.component === 'Calendar') {
- childComponents = Object.keys(allComponents).filter((key) => allComponents[key].parent?.startsWith(component.id));
- } else {
- childComponents = Object.keys(allComponents).filter((key) => allComponents[key].parent === component.id);
- }
+ Object.keys(allComponents).forEach((componentId) => {
+ const componentParentId = allComponents[componentId].component?.parent;
- childComponents.forEach((componentId) => {
- let childComponent = JSON.parse(JSON.stringify(allComponents[componentId]));
- childComponent.id = componentId;
- const newComponent = JSON.parse(
- JSON.stringify({
- id: componentId,
- component: allComponents[componentId]?.component,
- layouts: allComponents[componentId]?.layouts,
- parent: allComponents[componentId]?.parent,
- })
- );
- addedComponentId.add(componentId);
+ const isParentTabORCalendar =
+ allComponents[parentId]?.component?.component === 'Tabs' ||
+ allComponents[parentId]?.component?.component === 'Calendar';
- if ((component.component.component === 'Tabs') | (component.component.component === 'Calendar')) {
- const childTabId = childComponent.parent.split('-').at(-1);
- childComponent.parent = `${parentComponent.id}-${childTabId}`;
- } else {
- childComponent.parent = parentComponent.id;
+ if (componentParentId && isParentTabORCalendar) {
+ const childComponent = allComponents[componentId];
+ const childTabId = componentParentId.split('-').at(-1);
+ 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);
+ }
+ }
+
+ if (componentParentId === 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);
}
- parentComponent.children = [...(parentComponent.children || []), childComponent];
- childComponent.children = [...getChildComponents(allComponents, newComponent, childComponent, addedComponentId)];
- selectedChildComponents.push(childComponent);
});
- return selectedChildComponents;
+ return childComponents;
};
const updateComponentLayout = (components, parentId, isCut = false) => {
let prevComponent;
components.forEach((component, index) => {
Object.keys(component.layouts).map((layout) => {
- if (parentId !== undefined) {
+ if (parentId !== undefined && !component?.component?.parent) {
if (index > 0) {
component.layouts[layout].top = prevComponent.layouts[layout].top + prevComponent.layouts[layout].height;
component.layouts[layout].left = 0;
@@ -1477,56 +1521,100 @@ const updateComponentLayout = (components, parentId, isCut = false) => {
component.layouts[layout].left = 0;
}
prevComponent = component;
- } else if (!isCut) {
+ } else if (!isCut && !component.component.parent) {
component.layouts[layout].top = component.layouts[layout].top + component.layouts[layout].height;
}
});
});
};
+//
+const isChildOfTabsOrCalendar = (component, allComponents = [], componentParentId = undefined) => {
+ const parentId = componentParentId ?? component.component?.parent?.split('-').slice(0, -1).join('-');
-export const addComponents = (pageId, appDefinition, appDefinitionChanged, parentId = undefined, newComponentObj) => {
- const finalComponents = [];
+ const parentComponent = allComponents.find((comp) => comp.componentId === parentId);
+
+ if (parentComponent) {
+ return parentComponent.component.component === 'Tabs' || parentComponent.component.component === 'Calendar';
+ }
+
+ return false;
+};
+
+export const addComponents = (
+ pageId,
+ appDefinition,
+ appDefinitionChanged,
+ parentId = undefined,
+ newComponentObj,
+ fromClipboard = false
+) => {
+ const finalComponents = {};
+ const componentMap = {};
let parentComponent = undefined;
- const { isCloning, isCut, newComponents: pastedComponent = [] } = newComponentObj;
+ const { isCloning, isCut, newComponents: pastedComponents = [], currentPageId } = newComponentObj;
if (parentId) {
const id = Object.keys(appDefinition.pages[pageId].components).filter((key) => parentId.startsWith(key));
parentComponent = JSON.parse(JSON.stringify(appDefinition.pages[pageId].components[id[0]]));
- parentComponent.id = parentId;
}
- !isCloning && updateComponentLayout(pastedComponent, parentId, isCut);
+ pastedComponents.forEach((component) => {
+ const newComponentId = isCut ? component.componentId : uuidv4();
+ const componentName = computeComponentName(component.component.component, {
+ ...appDefinition.pages[pageId].components,
+ ...finalComponents,
+ });
- const buildComponents = (components, parentComponent = undefined, skipTabCalendarCheck = false) => {
- if (Array.isArray(components) && components.length > 0) {
- components.forEach((component) => {
- const newComponent = {
- id: uuidv4(),
- component: component?.component,
- layouts: component?.layouts,
- };
- if (parentComponent) {
- if (
- !skipTabCalendarCheck &&
- (parentComponent.component.component === 'Tabs' || parentComponent.component.component === 'Calendar')
- ) {
- const childTabId = component.parent.split('-').at(-1);
- newComponent.parent = `${parentComponent.id}-${childTabId}`;
- } else {
- newComponent.parent = parentComponent.id;
- }
- }
- finalComponents.push(newComponent);
- if (component.children.length > 0) {
- buildComponents(component.children, newComponent);
- }
- });
+ const isParentTabOrCalendar = isChildOfTabsOrCalendar(component, pastedComponents, parentId);
+ const parentRef = isParentTabOrCalendar
+ ? component.component.parent.split('-').slice(0, -1).join('-')
+ : component.component.parent;
+ const isParentAlsoCopied = parentRef && componentMap[parentRef];
+
+ componentMap[component.componentId] = newComponentId;
+ let isChild = isParentAlsoCopied ? component.component.parent : parentId;
+ const componentData = JSON.parse(JSON.stringify(component.component));
+
+ if (isCloning && parentId && !componentData.parent) {
+ isChild = component.component.parent;
}
- };
- buildComponents(pastedComponent, parentComponent, true);
+ if (!parentComponent && !isParentAlsoCopied && fromClipboard) {
+ isChild = undefined;
+ componentData.parent = undefined;
+ }
- updateNewComponents(pageId, appDefinition, finalComponents, appDefinitionChanged);
+ if (!isCloning && parentComponent && fromClipboard) {
+ componentData.parent = isParentAlsoCopied ?? parentId;
+ } else if (isChild && isChildOfTabsOrCalendar(component, pastedComponents, parentId)) {
+ const parentId = component.component.parent.split('-').slice(0, -1).join('-');
+ const childTabId = component.component.parent.split('-').at(-1);
+
+ componentData.parent = `${componentMap[parentId]}-${childTabId}`;
+ } else if (isChild) {
+ const isParentInMap = componentMap[isChild] !== undefined;
+
+ componentData.parent = isParentInMap ? componentMap[isChild] : isChild;
+ }
+
+ const newComponent = {
+ component: {
+ ...componentData,
+ name: componentName,
+ },
+ layouts: component.layouts,
+ };
+
+ finalComponents[newComponentId] = newComponent;
+
+ // const doesComponentHaveChildren = getAllChildComponents
+ });
+
+ if (currentPageId === pageId) {
+ updateComponentLayout(pastedComponents, parentId, isCut);
+ }
+
+ updateNewComponents(pageId, appDefinition, finalComponents, appDefinitionChanged, componentMap, isCut);
!isCloning && toast.success('Component pasted succesfully');
};
@@ -1594,6 +1682,7 @@ export const addNewWidgetToTheEditor = (
const widgetsWithDefaultComponents = ['Listview', 'Tabs', 'Form', 'Kanban'];
+ const nonActiveLayout = currentLayout === 'desktop' ? 'mobile' : 'desktop';
const newComponent = {
id: uuidv4(),
component: componentData,
@@ -1604,6 +1693,12 @@ export const addNewWidgetToTheEditor = (
width: defaultWidth,
height: defaultHeight,
},
+ [nonActiveLayout]: {
+ top: top,
+ left: left,
+ width: defaultWidth,
+ height: defaultHeight,
+ },
},
withDefaultChildren: widgetsWithDefaultComponents.includes(componentData.component),
@@ -1619,26 +1714,38 @@ export function snapToGrid(canvasWidth, x, y) {
const snappedY = Math.round(y / 10) * 10;
return [snappedX, snappedY];
}
-export const removeSelectedComponent = (pageId, newDefinition, selectedComponents) => {
- selectedComponents.forEach((component) => {
- let childComponents = [];
+export const removeSelectedComponent = (pageId, newDefinition, selectedComponents, updateAppDefinition) => {
+ const toDeleteComponents = [];
- if (newDefinition.pages[pageId].components[component.id]?.component?.component === 'Tabs') {
- childComponents = Object.keys(newDefinition.pages[pageId].components).filter((key) =>
- newDefinition.pages[pageId].components[key].parent?.startsWith(component.id)
- );
- } else {
- childComponents = Object.keys(newDefinition.pages[pageId].components).filter(
- (key) => newDefinition.pages[pageId].components[key].parent === component.id
- );
+ if (selectedComponents.length < 1) return getSelectedText();
+
+ const { components: allComponents } = newDefinition.pages[pageId];
+
+ const findAllChildComponents = (componentId) => {
+ if (!toDeleteComponents.includes(componentId)) {
+ toDeleteComponents.push(componentId);
+
+ // Find the children of this component
+ const children = getAllChildComponents(allComponents, componentId).map((child) => child.componentId);
+
+ if (children.length > 0) {
+ // Recursively find children of children
+ children.forEach((child) => {
+ findAllChildComponents(child);
+ });
+ }
}
+ };
- childComponents.forEach((componentId) => {
- delete newDefinition.pages[pageId].components[componentId];
- });
-
- delete newDefinition.pages[pageId].components[component.id];
+ selectedComponents.forEach((component) => {
+ findAllChildComponents(component.id);
});
+
+ toDeleteComponents.forEach((componentId) => {
+ delete newDefinition.pages[pageId].components[componentId];
+ });
+
+ updateAppDefinition(newDefinition, { componentDefinitionChanged: true, componentDeleted: true, componentCut: true });
};
const getSelectedText = () => {
@@ -1678,7 +1785,7 @@ export const runQueries = (queries, _ref) => {
});
};
-export const computeQueryState = (queries, _ref) => {
+export const computeQueryState = (queries) => {
let queryState = {};
queries.forEach((query) => {
if (query.plugin?.plugin_id) {
@@ -1705,6 +1812,87 @@ export const computeQueryState = (queries, _ref) => {
}
};
+export const buildComponentMetaDefinition = (components = {}) => {
+ for (const componentId in components) {
+ const currentComponentData = components[componentId];
+
+ const componentMeta = componentTypes.find((comp) => currentComponentData.component.component === comp.component);
+
+ const mergedDefinition = {
+ ...componentMeta.definition,
+
+ properties: {
+ ...componentMeta.definition.properties,
+ ...currentComponentData?.component.definition.properties,
+ },
+
+ styles: {
+ ...componentMeta.definition.styles,
+ ...currentComponentData?.component.definition.styles,
+ },
+ generalStyles: {
+ ...componentMeta.definition.generalStyles,
+ ...currentComponentData?.component.definition.generalStyles,
+ },
+ validation: {
+ ...componentMeta.definition.validation,
+ ...currentComponentData?.component.definition.validation,
+ },
+ others: {
+ ...componentMeta.definition.others,
+ ...currentComponentData?.component.definition.others,
+ },
+ general: {
+ ...componentMeta.definition.general,
+ ...currentComponentData?.component.definition.general,
+ },
+ };
+
+ const mergedComponent = {
+ component: {
+ ...componentMeta,
+ ...currentComponentData.component,
+ },
+ layouts: {
+ ...currentComponentData.layouts,
+ },
+ withDefaultChildren: componentMeta.withDefaultChildren ?? false,
+ };
+
+ mergedComponent.component.definition = mergedDefinition;
+
+ components[componentId] = mergedComponent;
+ }
+
+ return components;
+};
+
+export const buildAppDefinition = (data) => {
+ const editingVersion = _.omit(camelizeKeys(data.editing_version), ['definition', 'updatedAt', 'createdAt', 'name']);
+
+ editingVersion['currentVersionId'] = editingVersion.id;
+ _.unset(editingVersion, 'id');
+
+ const pages = data.pages.reduce((acc, page) => {
+ const currentComponents = buildComponentMetaDefinition(_.cloneDeep(page?.components));
+
+ page.components = currentComponents;
+
+ acc[page.id] = page;
+
+ return acc;
+ }, {});
+
+ const appJSON = {
+ globalSettings: editingVersion.globalSettings,
+ homePageId: editingVersion.homePageId,
+ showViewerNavigation: editingVersion.showViewerNavigation ?? true,
+ pages: pages,
+ };
+
+ return appJSON;
+};
+
export const removeFunctionObjects = (obj) => {
for (const key in obj) {
if (typeof obj[key] === 'function') {
diff --git a/frontend/src/_helpers/utils.js b/frontend/src/_helpers/utils.js
index c37cfb423d..3f7795ca06 100644
--- a/frontend/src/_helpers/utils.js
+++ b/frontend/src/_helpers/utils.js
@@ -570,10 +570,11 @@ export const hightlightMentionedUserInComment = (comment) => {
};
export const generateAppActions = (_ref, queryId, mode, isPreview = false) => {
- const currentPageId = _ref.state.currentPageId;
- const currentComponents = _ref.state?.appDefinition?.pages[currentPageId]?.components
- ? Object.entries(_ref.state.appDefinition.pages[currentPageId]?.components)
+ const currentPageId = _ref.currentPageId;
+ const currentComponents = _ref.appDefinition?.pages[currentPageId]?.components
+ ? Object.entries(_ref.appDefinition.pages[currentPageId]?.components)
: {};
+
const runQuery = (queryName = '', parameters) => {
const query = useDataQueriesStore.getState().dataQueries.find((query) => {
const isFound = query.name === queryName;
@@ -743,7 +744,7 @@ export const generateAppActions = (_ref, queryId, mode, isPreview = false) => {
});
return Promise.resolve();
}
- const pages = _ref.state.appDefinition.pages;
+ const pages = _ref.appDefinition.pages;
const pageId = Object.keys(pages).find((key) => pages[key].handle === pageHandle);
if (!pageId) {
diff --git a/frontend/src/_hooks/useDebouncedArrowKeyPress.js b/frontend/src/_hooks/useDebouncedArrowKeyPress.js
new file mode 100644
index 0000000000..0e041f5eeb
--- /dev/null
+++ b/frontend/src/_hooks/useDebouncedArrowKeyPress.js
@@ -0,0 +1,37 @@
+// useDebouncedArrowKeyPress.js
+import { useEffect, useState } from 'react';
+
+function useDebouncedArrowKeyPress(delay) {
+ const [lastKeyPressTimestamp, setLastKeyPressTimestamp] = useState(0);
+
+ useEffect(() => {
+ let timer;
+
+ function handleKeyPress(event) {
+ if (
+ event.key === 'ArrowUp' ||
+ event.key === 'ArrowDown' ||
+ event.key === 'ArrowLeft' ||
+ event.key === 'ArrowRight'
+ ) {
+ // Arrow key was pressed; debounce the update
+ clearTimeout(timer);
+
+ timer = setTimeout(() => {
+ // Trigger the update only after the specified delay
+ setLastKeyPressTimestamp(Date.now());
+ }, delay);
+ }
+ }
+
+ document.addEventListener('keydown', handleKeyPress);
+
+ return () => {
+ document.removeEventListener('keydown', handleKeyPress);
+ };
+ }, [delay]);
+
+ return lastKeyPressTimestamp;
+}
+
+export default useDebouncedArrowKeyPress;
diff --git a/frontend/src/_services/app.service.js b/frontend/src/_services/app.service.js
index b530bb2b1c..55820bee2b 100644
--- a/frontend/src/_services/app.service.js
+++ b/frontend/src/_services/app.service.js
@@ -3,6 +3,24 @@ import { authHeader, handleResponse } from '@/_helpers';
export const appService = {
getConfig,
+ getAll,
+ createApp,
+ cloneApp,
+ exportApp,
+ importApp,
+ exportResource,
+ importResource,
+ cloneResource,
+ changeIcon,
+ deleteApp,
+ getApp,
+ fetchApp,
+ getAppBySlug,
+ fetchAppBySlug,
+ getAppByVersion,
+ fetchAppByVersion,
+ saveApp,
+ getAppUsers,
createAppUser,
setPasswordFromToken,
acceptInvite,
@@ -13,6 +31,142 @@ function getConfig() {
return fetch(`${config.apiUrl}/config`, requestOptions).then(handleResponse);
}
+function getAll(page, folder, searchKey) {
+ const requestOptions = { method: 'GET', headers: authHeader(), credentials: 'include' };
+ if (page === 0) return fetch(`${config.apiUrl}/apps`, requestOptions).then(handleResponse);
+ else
+ return fetch(
+ `${config.apiUrl}/apps?page=${page}&folder=${folder || ''}&searchKey=${searchKey}`,
+ requestOptions
+ ).then(handleResponse);
+}
+
+function createApp(body = {}) {
+ const requestOptions = { method: 'POST', headers: authHeader(), credentials: 'include', body: JSON.stringify(body) };
+ return fetch(`${config.apiUrl}/apps`, requestOptions).then(handleResponse);
+}
+
+function cloneApp(id) {
+ const requestOptions = { method: 'POST', headers: authHeader(), credentials: 'include' };
+ return fetch(`${config.apiUrl}/apps/${id}/clone`, requestOptions).then(handleResponse);
+}
+
+function exportApp(id, versionId) {
+ const requestOptions = { method: 'GET', headers: authHeader(), credentials: 'include' };
+ return fetch(`${config.apiUrl}/apps/${id}/export${versionId ? `?versionId=${versionId}` : ''}`, requestOptions).then(
+ handleResponse
+ );
+}
+
+function exportResource(body) {
+ const requestOptions = {
+ method: 'POST',
+ headers: authHeader(),
+ body: JSON.stringify(body),
+ credentials: 'include',
+ };
+
+ return fetch(`${config.apiUrl}/v2/resources/export`, requestOptions).then(handleResponse);
+}
+
+function importResource(body) {
+ const requestOptions = {
+ method: 'POST',
+ headers: authHeader(),
+ credentials: 'include',
+ body: JSON.stringify(body),
+ };
+ return fetch(`${config.apiUrl}/v2/resources/import`, requestOptions).then(handleResponse);
+}
+
+function cloneResource(body) {
+ const requestOptions = {
+ method: 'POST',
+ headers: authHeader(),
+ body: JSON.stringify(body),
+ credentials: 'include',
+ };
+
+ return fetch(`${config.apiUrl}/v2/resources/clone`, requestOptions).then(handleResponse);
+}
+
+function getVersions(id) {
+ const requestOptions = { method: 'GET', headers: authHeader(), credentials: 'include' };
+ return fetch(`${config.apiUrl}/apps/${id}/versions`, requestOptions).then(handleResponse);
+}
+
+function importApp(body) {
+ const requestOptions = { method: 'POST', headers: authHeader(), credentials: 'include', body: JSON.stringify(body) };
+ return fetch(`${config.apiUrl}/apps/import`, requestOptions).then(handleResponse);
+}
+
+function getTables(id) {
+ const requestOptions = { method: 'GET', headers: authHeader(), credentials: 'include' };
+ return fetch(`${config.apiUrl}/apps/${id}/tables`, requestOptions).then(handleResponse);
+}
+
+function changeIcon(icon, appId) {
+ const requestOptions = {
+ method: 'PUT',
+ headers: authHeader(),
+ credentials: 'include',
+ body: JSON.stringify({ icon }),
+ };
+ return fetch(`${config.apiUrl}/apps/${appId}/icons`, requestOptions).then(handleResponse);
+}
+
+function getApp(id, accessType) {
+ const requestOptions = { method: 'GET', headers: authHeader(), credentials: 'include' };
+ return fetch(`${config.apiUrl}/apps/${id}${accessType ? `?access_type=${accessType}` : ''}`, requestOptions).then(
+ handleResponse
+ );
+}
+
+// v2 api for fetching app
+function fetchApp(id) {
+ const requestOptions = { method: 'GET', headers: authHeader(), credentials: 'include' };
+ return fetch(`${config.apiUrl}/v2/apps/${id}`, requestOptions).then(handleResponse);
+}
+
+function deleteApp(id) {
+ const requestOptions = { method: 'DELETE', headers: authHeader(), credentials: 'include' };
+ return fetch(`${config.apiUrl}/apps/${id}`, requestOptions).then(handleResponse);
+}
+
+function getAppBySlug(slug) {
+ const requestOptions = { method: 'GET', headers: authHeader(), credentials: 'include' };
+ return fetch(`${config.apiUrl}/apps/slugs/${slug}`, requestOptions).then(handleResponse);
+}
+
+function fetchAppBySlug(slug) {
+ const requestOptions = { method: 'GET', headers: authHeader(), credentials: 'include' };
+ return fetch(`${config.apiUrl}/v2/apps/slugs/${slug}`, requestOptions).then((resp) => handleResponse(resp, true));
+}
+
+function getAppByVersion(appId, versionId) {
+ const requestOptions = { method: 'GET', headers: authHeader(), credentials: 'include' };
+ return fetch(`${config.apiUrl}/apps/${appId}/versions/${versionId}`, requestOptions).then(handleResponse);
+}
+function fetchAppByVersion(appId, versionId) {
+ const requestOptions = { method: 'GET', headers: authHeader(), credentials: 'include' };
+ return fetch(`${config.apiUrl}/v2/apps/${appId}/versions/${versionId}`, requestOptions).then(handleResponse);
+}
+
+function saveApp(id, attributes) {
+ const requestOptions = {
+ method: 'PUT',
+ headers: authHeader(),
+ credentials: 'include',
+ body: JSON.stringify({ app: attributes }),
+ };
+ return fetch(`${config.apiUrl}/apps/${id}`, requestOptions).then(handleResponse);
+}
+
+function getAppUsers(id) {
+ const requestOptions = { method: 'GET', headers: authHeader(), credentials: 'include' };
+ return fetch(`${config.apiUrl}/apps/${id}/users`, requestOptions).then(handleResponse);
+}
+
function createAppUser(app_id, org_user_id, role) {
const body = {
app_id,
diff --git a/frontend/src/_services/appVersion.service.js b/frontend/src/_services/appVersion.service.js
index ab81ba8a9b..85a462a855 100644
--- a/frontend/src/_services/appVersion.service.js
+++ b/frontend/src/_services/appVersion.service.js
@@ -4,9 +4,16 @@ import { authHeader, handleResponse } from '@/_helpers';
export const appVersionService = {
getAll,
getOne,
+ getAppVersionData,
create,
del,
save,
+ autoSaveApp,
+ saveAppVersionEventHandlers,
+ createAppVersionEventHandler,
+ deleteAppVersionEventHandler,
+ clonePage,
+ findAllEventsWithSourceId,
};
function getAll(appId) {
@@ -18,6 +25,10 @@ function getOne(appId, versionId) {
const requestOptions = { method: 'GET', headers: authHeader(), credentials: 'include' };
return fetch(`${config.apiUrl}/apps/${appId}/versions/${versionId}`, requestOptions).then(handleResponse);
}
+function getAppVersionData(appId, versionId) {
+ const requestOptions = { method: 'GET', headers: authHeader(), credentials: 'include' };
+ return fetch(`${config.apiUrl}/v2/apps/${appId}/versions/${versionId}`, requestOptions).then(handleResponse);
+}
function create(appId, versionName, versionFromId) {
const body = {
@@ -47,6 +58,7 @@ function save(appId, versionId, values, isUserSwitchedVersion = false) {
const body = { is_user_switched_version: isUserSwitchedVersion };
if (values.definition) body['definition'] = values.definition;
if (values.name) body['name'] = values.name;
+ if (values.diff) body['app_diff'] = values.diff;
const requestOptions = {
method: 'PUT',
@@ -56,3 +68,118 @@ function save(appId, versionId, values, isUserSwitchedVersion = false) {
};
return fetch(`${config.apiUrl}/apps/${appId}/versions/${versionId}`, requestOptions).then(handleResponse);
}
+
+function autoSaveApp(
+ appId,
+ versionId,
+ diff,
+ type,
+ pageId,
+ operation,
+ isUserSwitchedVersion = false,
+ isComponentCutProcess = false
+) {
+ const OPERATION = {
+ create: 'POST',
+ update: 'PUT',
+ delete: 'DELETE',
+ };
+
+ const bodyMappings = {
+ pages: {
+ create: { ...diff },
+ delete: { ...diff },
+ },
+ global_settings: {
+ update: { ...diff },
+ },
+ };
+
+ const body = !type
+ ? { ...diff }
+ : bodyMappings[type]?.[operation] || {
+ is_user_switched_version: isUserSwitchedVersion,
+ pageId,
+ diff,
+ };
+
+ if (type === 'components' && operation === 'delete' && isComponentCutProcess) {
+ body['is_component_cut'] = true;
+ }
+
+ const requestOptions = {
+ method: OPERATION[operation],
+ headers: authHeader(),
+ credentials: 'include',
+ body: JSON.stringify(body),
+ };
+
+ const url = `${config.apiUrl}/v2/apps/${appId}/versions/${versionId}/${type ?? ''}`;
+
+ return fetch(url, requestOptions).then(handleResponse);
+}
+
+function saveAppVersionEventHandlers(appId, versionId, events, updateType = 'update') {
+ const body = {
+ events,
+ updateType,
+ };
+
+ const requestOptions = {
+ method: 'PUT',
+ headers: authHeader(),
+ credentials: 'include',
+ body: JSON.stringify(body),
+ };
+ return fetch(`${config.apiUrl}/v2/apps/${appId}/versions/${versionId}/events`, requestOptions).then(handleResponse);
+}
+
+function createAppVersionEventHandler(appId, versionId, event) {
+ const body = {
+ ...event,
+ };
+
+ const requestOptions = {
+ method: 'POST',
+ headers: authHeader(),
+ credentials: 'include',
+ body: JSON.stringify(body),
+ };
+ return fetch(`${config.apiUrl}/v2/apps/${appId}/versions/${versionId}/events`, requestOptions).then(handleResponse);
+}
+
+function deleteAppVersionEventHandler(appId, versionId, eventId) {
+ const requestOptions = {
+ method: 'DELETE',
+ headers: authHeader(),
+ credentials: 'include',
+ };
+ return fetch(`${config.apiUrl}/v2/apps/${appId}/versions/${versionId}/events/${eventId}`, requestOptions).then(
+ handleResponse
+ );
+}
+
+function clonePage(appId, versionId, pageId) {
+ const requestOptions = {
+ method: 'POST',
+ headers: authHeader(),
+ credentials: 'include',
+ };
+ return fetch(`${config.apiUrl}/v2/apps/${appId}/versions/${versionId}/pages/${pageId}/clone`, requestOptions).then(
+ handleResponse
+ );
+}
+
+function findAllEventsWithSourceId(appId, versionId, sourceId = undefined) {
+ const requestOptions = {
+ method: 'GET',
+ headers: authHeader(),
+ credentials: 'include',
+ };
+
+ return fetch(
+ `${config.apiUrl}/v2/apps/${appId}/versions/${versionId}/events${sourceId ? `?sourceId=${sourceId}` : ''}
+ `,
+ requestOptions
+ ).then(handleResponse);
+}
diff --git a/frontend/src/_stores/appDataStore.js b/frontend/src/_stores/appDataStore.js
index a3128308aa..1f5d36870f 100644
--- a/frontend/src/_stores/appDataStore.js
+++ b/frontend/src/_stores/appDataStore.js
@@ -1,19 +1,126 @@
+import { appVersionService } from '@/_services';
import { create, zustandDevTools } from './utils';
const initialState = {
editingVersion: null,
+ currentUser: null,
+ apps: [],
+ appName: null,
+ slug: null,
+ isPublic: null,
+ isMaintenanceOn: null,
+ organizationId: null,
+ currentVersionId: null,
+ userId: null,
+ app: {},
+ components: [],
+ pages: [],
+ layouts: [],
+ events: [],
+ eventHandlers: [],
+ appDefinitionDiff: null,
+ appDiffOptions: {},
isSaving: false,
appId: null,
+ areOthersOnSameVersionAndPage: false,
+ appVersionPreviewLink: null,
};
export const useAppDataStore = create(
zustandDevTools(
- (set) => ({
+ (set, get) => ({
...initialState,
actions: {
updateEditingVersion: (version) => set(() => ({ editingVersion: version })),
+ updateApps: (apps) => set(() => ({ apps: apps })),
+ updateState: (state) => set((prev) => ({ ...prev, ...state })),
+ updateAppDefinitionDiff: (appDefinitionDiff) => set(() => ({ appDefinitionDiff: appDefinitionDiff })),
+ updateAppVersion: (appId, versionId, pageId, appDefinitionDiff, isUserSwitchedVersion = false) => {
+ return new Promise((resolve, reject) => {
+ useAppDataStore.getState().actions.setIsSaving(true);
+ const isComponentCutProcess = get().appDiffOptions?.componentCut === true;
+
+ appVersionService
+ .autoSaveApp(
+ appId,
+ versionId,
+ appDefinitionDiff.updateDiff,
+ appDefinitionDiff.type,
+ pageId,
+ appDefinitionDiff.operation,
+ isUserSwitchedVersion,
+ isComponentCutProcess
+ )
+ .then(() => {
+ useAppDataStore.getState().actions.setIsSaving(false);
+ })
+ .catch((error) => {
+ useAppDataStore.getState().actions.setIsSaving(false);
+ reject(error);
+ })
+ .finally(() => resolve());
+ });
+ },
+ updateAppVersionEventHandlers: async (events, updateType = 'update') => {
+ useAppDataStore.getState().actions.setIsSaving(true);
+ const appId = get().appId;
+ const versionId = get().currentVersionId;
+
+ const response = await appVersionService.saveAppVersionEventHandlers(appId, versionId, events, updateType);
+
+ useAppDataStore.getState().actions.setIsSaving(false);
+ const updatedEvents = get().events;
+
+ updatedEvents.forEach((e, index) => {
+ const toUpdate = response.find((r) => r.id === e.id);
+ if (toUpdate) {
+ updatedEvents[index] = toUpdate;
+ }
+ });
+
+ set(() => ({ events: updatedEvents }));
+ },
+
+ createAppVersionEventHandlers: async (event) => {
+ useAppDataStore.getState().actions.setIsSaving(true);
+ const appId = get().appId;
+ const versionId = get().currentVersionId;
+
+ const updatedEvents = get().events;
+ const response = await appVersionService.createAppVersionEventHandler(appId, versionId, event);
+ useAppDataStore.getState().actions.setIsSaving(false);
+ updatedEvents.push(response);
+
+ set(() => ({ events: updatedEvents }));
+ },
+
+ deleteAppVersionEventHandler: async (eventId) => {
+ useAppDataStore.getState().actions.setIsSaving(true);
+ const appId = get().appId;
+ const versionId = get().currentVersionId;
+
+ const updatedEvents = get().events;
+
+ const response = await appVersionService.deleteAppVersionEventHandler(appId, versionId, eventId);
+ useAppDataStore.getState().actions.setIsSaving(false);
+ if (response?.affected === 1) {
+ updatedEvents.splice(
+ updatedEvents.findIndex((e) => e.id === eventId),
+ 1
+ );
+
+ set(() => ({ events: updatedEvents }));
+ }
+ },
+ autoUpdateEventStore: async (versionId) => {
+ const appId = get().appId;
+ const response = await appVersionService.findAllEventsWithSourceId(appId, versionId);
+
+ set(() => ({ events: response }));
+ },
setIsSaving: (isSaving) => set(() => ({ isSaving })),
setAppId: (appId) => set(() => ({ appId })),
+ setAppPreviewLink: (appVersionPreviewLink) => set(() => ({ appVersionPreviewLink })),
},
}),
{ name: 'App Data Store' }
@@ -23,3 +130,6 @@ export const useAppDataStore = create(
export const useEditingVersion = () => useAppDataStore((state) => state.editingVersion);
export const useIsSaving = () => useAppDataStore((state) => state.isSaving);
export const useUpdateEditingVersion = () => useAppDataStore((state) => state.actions);
+export const useCurrentUser = () => useAppDataStore((state) => state.currentUser);
+export const useAppInfo = () => useAppDataStore((state) => state);
+export const useAppDataActions = () => useAppDataStore((state) => state.actions);
diff --git a/frontend/src/_stores/appVersionStore.js b/frontend/src/_stores/appVersionStore.js
index 38e456479f..1824c28e87 100644
--- a/frontend/src/_stores/appVersionStore.js
+++ b/frontend/src/_stores/appVersionStore.js
@@ -5,6 +5,7 @@ const initialState = {
isUserEditingTheVersion: false,
releasedVersionId: null,
isVersionReleased: false,
+ appVersions: [],
};
export const useAppVersionStore = create(
@@ -21,8 +22,12 @@ export const useAppVersionStore = create(
releasedVersionId: versionId,
isVersionReleased: get().editingVersion?.id ? get().editingVersion?.id === versionId : false,
}),
+ setAppVersions: (versions) => set({ appVersions: versions }),
},
}),
{ name: 'App Version Manager Store' }
)
);
+
+export const useAppVersionActions = () => useAppVersionStore((state) => state.actions);
+export const useAppVersionState = () => useAppVersionStore((state) => state);
diff --git a/frontend/src/_stores/currentStateStore.js b/frontend/src/_stores/currentStateStore.js
index 4a805a8467..2e6c6f3eec 100644
--- a/frontend/src/_stores/currentStateStore.js
+++ b/frontend/src/_stores/currentStateStore.js
@@ -51,6 +51,7 @@ export const useCurrentState = () =>
page: state.page,
succededQuery: state.succededQuery,
constants: state.constants,
+ layout: state.layout,
};
}, shallow);
diff --git a/frontend/src/_stores/dataQueriesStore.js b/frontend/src/_stores/dataQueriesStore.js
index ebc89dc89c..72f4e22c99 100644
--- a/frontend/src/_stores/dataQueriesStore.js
+++ b/frontend/src/_stores/dataQueriesStore.js
@@ -1,7 +1,7 @@
import { create, zustandDevTools } from './utils';
import { getDefaultOptions } from './storeHelper';
import { dataqueryService } from '@/_services';
-import debounce from 'lodash/debounce';
+// import debounce from 'lodash/debounce';
import { useAppDataStore } from '@/_stores/appDataStore';
import { useQueryPanelStore } from '@/_stores/queryPanelStore';
import { useAppVersionStore } from '@/_stores/appVersionStore';
@@ -9,6 +9,7 @@ import { runQueries } from '@/_helpers/appUtils';
import { v4 as uuidv4 } from 'uuid';
import { toast } from 'react-hot-toast';
import { isEmpty, throttle } from 'lodash';
+import { useEditorStore } from './editorStore';
const initialState = {
dataQueries: [],
@@ -30,15 +31,27 @@ export const useDataQueriesStore = create(
...initialState,
actions: {
// TODO: Remove editor state while changing currentState
- fetchDataQueries: async (appId, selectFirstQuery = false, runQueriesOnAppLoad = false, editorRef) => {
+ fetchDataQueries: async (appVersionId, selectFirstQuery = false, runQueriesOnAppLoad = false, ref) => {
set({ loadingDataQueries: true });
- const data = await dataqueryService.getAll(appId);
+ const data = await dataqueryService.getAll(appVersionId);
set((state) => ({
dataQueries: sortByAttribute(data.data_queries, state.sortBy, state.sortOrder),
loadingDataQueries: false,
}));
- // Runs query on loading application
- if (runQueriesOnAppLoad) runQueries(data.data_queries, editorRef);
+
+ if (data.data_queries.length !== 0) {
+ const queryConfirmationList = [];
+ data.data_queries.forEach(({ id, name, options }) => {
+ if (options && options?.requestConfirmation && options?.runOnPageLoad) {
+ queryConfirmationList.push({ queryId: id, queryName: name });
+ }
+ });
+
+ if (queryConfirmationList.length !== 0) {
+ useEditorStore.getState().actions.updateQueryConfirmationList(queryConfirmationList);
+ }
+ }
+
// Compute query state to be added in the current state
const { actions, selectedQuery } = useQueryPanelStore.getState();
if (selectFirstQuery) {
@@ -47,6 +60,9 @@ export const useDataQueriesStore = create(
const query = data.data_queries.find((query) => query.id === selectedQuery?.id);
actions.setSelectedQuery(query?.id);
}
+
+ // Runs query on loading application
+ if (runQueriesOnAppLoad) runQueries(data.data_queries, ref);
},
setDataQueries: (dataQueries) => set({ dataQueries }),
deleteDataQueries: (queryId) => {
@@ -232,7 +248,7 @@ export const useDataQueriesStore = create(
newName = queryToClone.name + '_copy' + count.toString();
}
queryToClone.name = newName;
- delete queryToClone.id;
+
useAppDataStore.getState().actions.setIsSaving(true);
dataqueryService
.create(
@@ -250,6 +266,26 @@ export const useDataQueriesStore = create(
dataQueries: [{ ...data, data_source_id: queryToClone.data_source_id }, ...state.dataQueries],
}));
actions.setSelectedQuery(data.id, { ...data, data_source_id: queryToClone.data_source_id });
+
+ const dataQueryEvents = useAppDataStore
+ .getState()
+ .events?.filter((event) => event.target === 'data_query' && event.sourceId === queryToClone.id);
+
+ if (dataQueryEvents?.length === 0) return;
+
+ return Promise.all(
+ dataQueryEvents.map((event) => {
+ const newEvent = {
+ event: {
+ ...event?.event,
+ },
+ eventType: event?.target,
+ attachedTo: data.id,
+ index: event?.index,
+ };
+ useAppDataStore.getState().actions?.createAppVersionEventHandlers(newEvent);
+ })
+ );
})
.catch((error) => {
console.error('error', error);
diff --git a/frontend/src/_stores/editorStore.js b/frontend/src/_stores/editorStore.js
index ba1a1b5269..6140f7d790 100644
--- a/frontend/src/_stores/editorStore.js
+++ b/frontend/src/_stores/editorStore.js
@@ -1,5 +1,5 @@
import { create, zustandDevTools } from './utils';
-
+import { v4 as uuid } from 'uuid';
const STORE_NAME = 'Editor';
const ACTIONS = {
@@ -19,53 +19,79 @@ const initialState = {
selectionInProgress: false,
selectedComponents: [],
isEditorActive: false,
+ currentSidebarTab: 2,
+ selectedComponent: null,
+ scrollOptions: {
+ container: null,
+ throttleTime: 0,
+ threshold: 0,
+ },
+ canUndo: false,
+ canRedo: false,
+ currentVersion: {},
+ noOfVersionsSupported: 100,
+ appDefinition: {},
+ // isSaving: false,
+ isUpdatingEditorStateInProcess: false,
+ saveError: false,
+ isLoading: true,
+ defaultComponentStateComputed: false,
+ showLeftSidebar: true,
+ queryConfirmationList: [],
+ currentPageId: null,
+ currentSessionId: uuid(),
};
export const useEditorStore = create(
- zustandDevTools(
- (set, get) => ({
- ...initialState,
- actions: {
- setShowComments: (showComments) =>
- set({ showComments }, false, {
- type: ACTIONS.SET_HOVERED_COMPONENT,
- showComments,
- }),
- toggleComments: () =>
- set({ showComments: !get().showComments }, false, {
- type: ACTIONS.TOGGLE_COMMENTS,
- }),
- toggleCurrentLayout: (currentLayout) =>
- set({ currentLayout }, false, {
- type: ACTIONS.TOGGLE_CURRENT_LAYOUT,
- currentLayout,
- }),
- setIsEditorActive: (isEditorActive) => set(() => ({ isEditorActive })),
- setHoveredComponent: (hoveredComponent) =>
- set({ hoveredComponent }, false, {
- type: ACTIONS.SET_HOVERED_COMPONENT,
- hoveredComponent,
- }),
- setSelectionInProgress: (isSelectionInProgress) => {
- set(
- {
- isSelectionInProgress,
- },
- false,
- { type: ACTIONS.SET_SELECTION_IN_PROGRESS }
- );
- },
- setSelectedComponents: (selectedComponents) => {
- set(
- {
- selectedComponents,
- },
- false,
- { type: ACTIONS.SET_SELECTED_COMPONENTS }
- );
- },
+ // Dev tools for this store are disabled comments since its freezing chrome tab
+ (set, get) => ({
+ ...initialState,
+ actions: {
+ setShowComments: (showComments) =>
+ set({ showComments }, false, {
+ type: ACTIONS.SET_HOVERED_COMPONENT,
+ showComments,
+ }),
+ toggleComments: () =>
+ set({ showComments: !get().showComments }, false, {
+ type: ACTIONS.TOGGLE_COMMENTS,
+ }),
+ toggleCurrentLayout: (currentLayout) =>
+ set({ currentLayout }, false, {
+ type: ACTIONS.TOGGLE_CURRENT_LAYOUT,
+ currentLayout,
+ }),
+ setIsEditorActive: (isEditorActive) => set(() => ({ isEditorActive })),
+ updateEditorState: (state) => set((prev) => ({ ...prev, ...state })),
+ updateQueryConfirmationList: (queryConfirmationList) => set({ queryConfirmationList }),
+ setHoveredComponent: (hoveredComponent) =>
+ set({ hoveredComponent }, false, {
+ type: ACTIONS.SET_HOVERED_COMPONENT,
+ hoveredComponent,
+ }),
+ setSelectionInProgress: (isSelectionInProgress) => {
+ set(
+ {
+ isSelectionInProgress,
+ },
+ false,
+ { type: ACTIONS.SET_SELECTION_IN_PROGRESS }
+ );
},
- }),
- { name: STORE_NAME }
- )
+ setSelectedComponents: (selectedComponents, isMulti = false) => {
+ const newSelectedComponents = isMulti
+ ? [...get().selectedComponents, ...selectedComponents]
+ : selectedComponents;
+
+ set({
+ selectedComponents: newSelectedComponents,
+ });
+ },
+ setCurrentPageId: (currentPageId) => set({ currentPageId }),
+ },
+ }),
+ { name: STORE_NAME }
);
+
+export const useEditorActions = () => useEditorStore((state) => state.actions);
+export const useEditorState = () => useEditorStore((state) => state);
diff --git a/frontend/src/_stores/utils.js b/frontend/src/_stores/utils.js
index 8b883b726d..db72f51d60 100644
--- a/frontend/src/_stores/utils.js
+++ b/frontend/src/_stores/utils.js
@@ -1,5 +1,9 @@
import { create as _create } from 'zustand';
import { devtools } from 'zustand/middleware';
+// eslint-disable-next-line import/no-unresolved
+import { diff } from 'deep-object-diff';
+import { componentTypes } from '@/Editor/WidgetManager/components';
+import _ from 'lodash';
export const zustandDevTools = (fn, options = {}) =>
devtools(fn, { ...options, enabled: process.env.NODE_ENV === 'production' ? false : true });
@@ -21,3 +25,304 @@ export const resetAllStores = () => {
resetter();
}
};
+
+const defaultComponent = {
+ name: '',
+ properties: {},
+ styles: {},
+ validation: {},
+ type: '',
+ others: {
+ showOnDesktop: { value: '{{true}}' },
+ showOnMobile: { value: '{{false}}' },
+ },
+};
+
+const updateType = Object.freeze({
+ pageDefinitionChanged: 'pages',
+ containerChanges: 'components/layout',
+ componentAdded: 'components',
+ componentDefinitionChanged: 'components',
+ componentDeleted: 'components',
+});
+
+export const computeAppDiff = (appDiff, currentPageId, opts, currentLayout) => {
+ const { updateDiff, type, operation, error } = updateFor(appDiff, currentPageId, opts, currentLayout);
+
+ return { updateDiff, type, operation, error };
+};
+
+// for table column diffs, we need to compute the diff for each column separately and send the the entire column data
+function generatePath(obj, targetKey, currentPath = '') {
+ for (const key in obj) {
+ const newPath = currentPath ? currentPath + '.' + key : key;
+
+ if (key === targetKey) {
+ return newPath;
+ }
+
+ if (typeof obj[key] === 'object' && obj[key] !== null) {
+ const result = generatePath(obj[key], targetKey, newPath);
+ if (result) {
+ return result;
+ }
+ }
+ }
+ return null;
+}
+
+function getValueFromJson(json, path) {
+ if (!path || typeof path !== 'string') return null;
+
+ let value = json;
+ path.split('.').forEach((key) => {
+ value = value[key];
+ });
+ return value;
+}
+
+function updateValueInJson(json, path, value) {
+ let obj = json;
+ const keys = path?.split('.');
+
+ if (!keys) {
+ return null;
+ }
+
+ const lastKey = keys.pop();
+ keys.forEach((key) => {
+ obj = obj[key];
+ });
+ obj[lastKey] = value;
+ return json;
+}
+
+export function isParamFromTableColumn(appDiff, definition) {
+ const path = generatePath(appDiff, 'columns') || generatePath(appDiff, 'actions');
+ if (!path) {
+ return false;
+ }
+
+ const value2 = getValueFromJson(definition, path);
+
+ return value2 !== undefined;
+}
+
+export const computeComponentPropertyDiff = (appDiff, definition, opts) => {
+ if (!opts?.isParamFromTableColumn) {
+ return appDiff;
+ }
+ const columnsPath = generatePath(appDiff, 'columns');
+ const actionsPath = generatePath(appDiff, 'actions');
+ const deletionHistoryPath = generatePath(appDiff, 'columnDeletionHistory');
+
+ let _diff = _.cloneDeep(appDiff);
+
+ if (columnsPath) {
+ const columnsValue = getValueFromJson(definition, columnsPath);
+ _diff = updateValueInJson(_diff, columnsPath, columnsValue);
+ }
+
+ if (actionsPath) {
+ const actionsValue = getValueFromJson(definition, actionsPath);
+ _diff = updateValueInJson(_diff, actionsPath, actionsValue);
+ }
+
+ if (deletionHistoryPath) {
+ const deletionHistoryValue = getValueFromJson(definition, deletionHistoryPath);
+ _diff = updateValueInJson(_diff, deletionHistoryPath, deletionHistoryValue);
+ }
+
+ return _diff;
+};
+
+const updateFor = (appDiff, currentPageId, opts, currentLayout) => {
+ const updateTypeMappings = [
+ {
+ updateTypes: ['componentAdded', 'componentDefinitionChanged', 'componentDeleted', 'containerChanges'],
+ processingFunction: computeComponentDiff,
+ },
+ {
+ updateTypes: ['pageDefinitionChanged', 'pageSortingChanged', 'deletePageRequest', 'addNewPage'],
+ processingFunction: computePageUpdate,
+ },
+ {
+ updateTypes: ['homePageChanged'],
+ processingFunction: () => ({
+ updateDiff: appDiff,
+ type: null,
+ operation: 'update',
+ }),
+ },
+ {
+ updateTypes: ['globalSettings', 'generalAppDefinitionChanged'],
+ processingFunction: () => ({
+ updateDiff: appDiff,
+ type: 'global_settings',
+ operation: 'update',
+ }),
+ },
+ ];
+
+ const options = _.keys(opts);
+
+ for (const { updateTypes, processingFunction } of updateTypeMappings) {
+ const optionsTypes = _.intersection(options, updateTypes);
+
+ if (optionsTypes.length > 0) {
+ try {
+ return processingFunction(appDiff, currentPageId, optionsTypes, currentLayout);
+ } catch (error) {
+ return { error, updateDiff: {}, type: null, operation: null };
+ }
+ }
+ }
+
+ return null;
+};
+
+const computePageUpdate = (appDiff, currentPageId, opts) => {
+ let type;
+ let updateDiff;
+ let operation = 'update';
+
+ if (opts.includes('deletePageRequest')) {
+ const deletePageId = _.keys(appDiff?.pages).map((pageId) => {
+ if (appDiff?.pages[pageId]?.pageId === undefined) {
+ return pageId;
+ }
+ })[0];
+
+ updateDiff = {
+ pageId: deletePageId,
+ };
+
+ type = updateType.pageDefinitionChanged;
+ operation = 'delete';
+ } else if (opts.includes('pageSortingChanged')) {
+ updateDiff = appDiff?.pages;
+
+ type = updateType.pageDefinitionChanged;
+ } else if (opts.includes('pageDefinitionChanged')) {
+ updateDiff = appDiff?.pages[currentPageId];
+
+ type = updateType.pageDefinitionChanged;
+
+ if (opts.includes('addNewPage')) {
+ operation = 'create';
+ }
+ }
+
+ return { updateDiff, type, operation };
+};
+
+const computeComponentDiff = (appDiff, currentPageId, opts, currentLayout) => {
+ let type;
+ let updateDiff;
+ let operation = 'update';
+
+ if (opts.includes('componentDeleted')) {
+ const currentPageComponents = appDiff?.pages[currentPageId]?.components;
+
+ updateDiff = _.keys(currentPageComponents);
+
+ type = updateType.componentDeleted;
+
+ operation = 'delete';
+ } else if (opts.includes('componentAdded')) {
+ const currentPageComponents = appDiff?.pages[currentPageId]?.components;
+
+ updateDiff = _.toPairs(currentPageComponents ?? []).reduce((result, [id, component]) => {
+ if (_.keys(component).length === 1 && component.withDefaultChildren !== undefined) {
+ return result;
+ }
+
+ const componentMeta = componentTypes.find((comp) => comp.component === component.component.component);
+
+ if (!componentMeta) {
+ return result;
+ }
+
+ const metaDiff = diff(componentMeta, component.component);
+
+ result[id] = _.defaultsDeep(metaDiff, defaultComponent);
+
+ if (metaDiff.definition && !_.isEmpty(metaDiff.definition)) {
+ const metaAttributes = _.keys(metaDiff.definition);
+
+ metaAttributes.forEach((attribute) => {
+ const doesActionsExist =
+ metaDiff.definition[attribute]?.actions && !_.isEmpty(metaDiff.definition[attribute]?.actions?.value);
+ const doesColumnsExist =
+ metaDiff.definition[attribute]?.columns && !_.isEmpty(metaDiff.definition[attribute]?.columns?.value);
+
+ if (doesActionsExist || doesColumnsExist) {
+ const actions = _.toArray(metaDiff.definition[attribute]?.actions?.value) || [];
+ const columns = _.toArray(metaDiff.definition[attribute]?.columns?.value) || [];
+
+ metaDiff.definition = {
+ ...metaDiff.definition,
+ [attribute]: {
+ ...metaDiff.definition[attribute],
+ actions: {
+ value: actions,
+ },
+ columns: {
+ value: columns,
+ },
+ },
+ };
+ }
+ result[id][attribute] = metaDiff.definition[attribute];
+ });
+ }
+
+ const currentDisplayPreference = currentLayout;
+
+ if (currentDisplayPreference === 'mobile') {
+ result[id].others.showOnMobile = { value: '{{true}}' };
+ result[id].others.showOnDesktop = { value: '{{false}}' };
+ }
+
+ if (result[id]?.definition) {
+ delete result[id].definition;
+ }
+
+ result[id].type = componentMeta.component;
+ result[id].parent = component.component.parent ?? null;
+ result[id].layouts = appDiff.pages[currentPageId].components[id].layouts;
+
+ operation = 'create';
+
+ return result;
+ }, {});
+
+ type = updateType.componentDefinitionChanged;
+ } else if (
+ (opts.includes('containerChanges') || opts.includes('componentDefinitionChanged')) &&
+ !opts.includes('componentAdded')
+ ) {
+ const currentPageComponents = appDiff?.pages[currentPageId]?.components;
+
+ updateDiff = toRemoveExposedvariablesFromComponentDiff(currentPageComponents);
+
+ type = opts.includes('containerChanges') ? updateType.containerChanges : updateType.componentDefinitionChanged;
+ }
+
+ return { updateDiff, type, operation };
+};
+
+function toRemoveExposedvariablesFromComponentDiff(object) {
+ const copy = _.cloneDeep(object);
+ const componentIds = _.keys(copy);
+
+ componentIds.forEach((componentId) => {
+ const { component } = copy[componentId];
+
+ if (component?.exposedVariables) {
+ delete component.exposedVariables;
+ }
+ });
+
+ return copy;
+}
diff --git a/frontend/src/_ui/Layout/index.jsx b/frontend/src/_ui/Layout/index.jsx
index 4f1de55694..da59085ebe 100644
--- a/frontend/src/_ui/Layout/index.jsx
+++ b/frontend/src/_ui/Layout/index.jsx
@@ -1,4 +1,4 @@
-import React, { useCallback, useState } from 'react';
+import React from 'react';
import { Link } from 'react-router-dom';
import useRouter from '@/_hooks/use-router';
import { ToolTip } from '@/_components/ToolTip';
diff --git a/server/.version b/server/.version
index e9763f6bfe..ad2261920c 100644
--- a/server/.version
+++ b/server/.version
@@ -1 +1 @@
-2.23.0
+2.24.0
diff --git a/server/data-migrations/1688977149516-ListviewDefaultMode.ts b/server/data-migrations/1688977149516-ListviewDefaultMode.ts
index d8e1d503cd..672774fcf3 100644
--- a/server/data-migrations/1688977149516-ListviewDefaultMode.ts
+++ b/server/data-migrations/1688977149516-ListviewDefaultMode.ts
@@ -6,39 +6,28 @@ export class ListviewDefaultMode1688977149516 implements MigrationInterface {
const entityManager = queryRunner.manager;
const appVersions = await entityManager.find(AppVersion);
for (const version of appVersions) {
- const definition = version['definition'];
+ const definition = JSON.parse(JSON.stringify(version?.definition));
if (definition) {
const pages = definition['pages'];
- if (pages) {
+ if (Object.keys(pages).length > 0) {
for (const pageId of Object.keys(pages)) {
const components = definition['pages'][pageId]['components'];
- if (components) {
+ if (Object.keys(components).length > 0) {
for (const componentId of Object.keys(components)) {
const component = components[componentId];
- if (component?.component?.component === 'Listview') {
- component['component']['definition']['properties']['mode'] = {
- value: 'list',
- };
-
- components[componentId] = {
- ...component,
- component: {
- ...component.component,
- definition: {
- ...component.component.definition,
- },
- },
- };
+ if (
+ component?.component?.component === 'Listview' &&
+ component.component?.definition?.properties?.mode
+ ) {
+ component.component.definition.properties.mode['value'] = 'list';
}
}
}
-
- definition['components'] = components;
- version.definition = definition;
}
}
+ version.definition = definition;
await entityManager.update(AppVersion, { id: version.id }, { definition });
}
}
diff --git a/server/data-migrations/1692973078520-CellSizeRegularCondensed.ts b/server/data-migrations/1692973078520-CellSizeRegularCondensed.ts
index 0833ebb753..76709c348b 100644
--- a/server/data-migrations/1692973078520-CellSizeRegularCondensed.ts
+++ b/server/data-migrations/1692973078520-CellSizeRegularCondensed.ts
@@ -4,25 +4,23 @@ import { AppVersion } from '../src/entities/app_version.entity';
export class CellSizeRegularCondensed1692973078520 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise {
const entityManager = queryRunner.manager;
- const queryBuilder = queryRunner.connection.createQueryBuilder();
+
const appVersionRepository = entityManager.getRepository(AppVersion);
const appVersions = await appVersionRepository.find();
for (const version of appVersions) {
- const definition = version?.['definition'];
+ const definition = JSON.parse(JSON.stringify(version?.definition));
if (definition) {
const pages = definition?.['pages'];
- if (pages) {
+ if (Object.keys(pages).length > 0) {
for (const pageId of Object.keys(pages)) {
- const components = pages?.[pageId]?.['components'];
- if (components) {
+ const components = pages[pageId]?.['components'];
+
+ if (Object.keys(components).length > 0) {
for (const componentId of Object.keys(components)) {
const component = components[componentId];
- if (component?.component?.component === 'Table') {
- component.component.definition.styles.cellSize = {
- value: 'regular',
- };
+ if (component?.component?.component === 'Table' && component.component?.definition?.styles?.cellSize) {
component.component.styles.cellSize = {
...component.component.styles.cellSize,
options: [
@@ -30,27 +28,19 @@ export class CellSizeRegularCondensed1692973078520 implements MigrationInterface
{ name: 'Regular', value: 'regular' },
],
};
- components[componentId] = {
- ...component,
- component: {
- ...component.component,
- definition: {
- ...component.component.definition,
- },
- },
+
+ component.component.definition.styles.cellSize = {
+ value: 'regular',
};
}
}
- pages[pageId]['components'] = components;
}
}
}
- definition['pages'] = pages;
-
version.definition = definition;
- await queryBuilder.update(AppVersion).set({ definition }).where('id = :id', { id: version.id }).execute();
+ await entityManager.update(AppVersion, { id: version.id }, { definition });
}
}
}
diff --git a/server/data-migrations/1692974311591-TableRowCellStyle.ts b/server/data-migrations/1692974311591-TableRowCellStyle.ts
index 114152b059..ca79b042e1 100644
--- a/server/data-migrations/1692974311591-TableRowCellStyle.ts
+++ b/server/data-migrations/1692974311591-TableRowCellStyle.ts
@@ -4,20 +4,20 @@ import { AppVersion } from '../src/entities/app_version.entity';
export class TableRowCellStyle1692974311591 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise {
const entityManager = queryRunner.manager;
- const queryBuilder = queryRunner.connection.createQueryBuilder();
+
const appVersionRepository = entityManager.getRepository(AppVersion);
const appVersions = await appVersionRepository.find();
for (const version of appVersions) {
- const definition = version['definition'];
+ const definition = JSON.parse(JSON.stringify(version?.definition));
if (definition) {
const pages = definition['pages'];
- if (pages) {
+ if (Object.keys(pages).length > 0) {
for (const pageId of Object.keys(pages)) {
const components = pages[pageId]['components'];
- if (components) {
+ if (Object.keys(components).length > 0) {
for (const componentId of Object.keys(components)) {
const component = components[componentId];
if (component?.component?.component === 'Table') {
@@ -32,27 +32,15 @@ export class TableRowCellStyle1692974311591 implements MigrationInterface {
{ name: 'Striped', value: 'table-striped' },
],
};
- components[componentId] = {
- ...component,
- component: {
- ...component.component,
- definition: {
- ...component.component.definition,
- },
- },
- };
}
}
}
- pages[pageId]['components'] = components;
}
}
- definition['pages'] = pages;
-
version.definition = definition;
- await queryBuilder.update(AppVersion).set({ definition }).where('id = :id', { id: version.id }).execute();
+ await entityManager.update(AppVersion, { id: version.id }, { definition });
}
}
}
diff --git a/server/data-migrations/1697473340856-MigrateAppsDefinitionSchemaTransition.ts b/server/data-migrations/1697473340856-MigrateAppsDefinitionSchemaTransition.ts
new file mode 100644
index 0000000000..fef4a4f74e
--- /dev/null
+++ b/server/data-migrations/1697473340856-MigrateAppsDefinitionSchemaTransition.ts
@@ -0,0 +1,361 @@
+import { In, MigrationInterface, QueryRunner, EntityManager } from 'typeorm';
+import { AppVersion } from '../src/entities/app_version.entity';
+import { Component } from 'src/entities/component.entity';
+import { Page } from 'src/entities/page.entity';
+import { Layout } from 'src/entities/layout.entity';
+import { EventHandler, Target } from 'src/entities/event_handler.entity';
+import { DataQuery } from 'src/entities/data_query.entity';
+import { MigrationProgress, processDataInBatches } from 'src/helpers/utils.helper';
+import { v4 as uuid } from 'uuid';
+
+interface AppResourceMappings {
+ pagesMapping: Record;
+ componentsMapping: Record;
+}
+
+export class MigrateAppsDefinitionSchemaTransition1697473340856 implements MigrationInterface {
+ public async up(queryRunner: QueryRunner): Promise {
+ const entityManager = queryRunner.manager;
+ const appVersionRepository = entityManager.getRepository(AppVersion);
+ const appVersions = await appVersionRepository.find();
+ const totalVersions = appVersions.length;
+
+ const migrationProgress = new MigrationProgress(
+ 'MigrateAppsDefinitionSchemaTransition1697473340856',
+ totalVersions
+ );
+
+ const batchSize = 100; // Number of apps to migrate at a time
+
+ await processDataInBatches(
+ entityManager,
+ async (entityManager: EntityManager, skip: number, take: number) => {
+ return entityManager.find(AppVersion, {
+ where: { id: In(appVersions.map((appVersion) => appVersion.id)) },
+ take,
+ skip,
+ });
+ },
+ async (entityManager: EntityManager, versions: AppVersion[]) => {
+ await this.processVersions(entityManager, versions, migrationProgress);
+ },
+ batchSize
+ );
+ }
+
+ private async processVersions(
+ entityManager: EntityManager,
+ versions: AppVersion[],
+ migrationProgress: MigrationProgress
+ ) {
+ for (const version of versions) {
+ const definition = version['definition'];
+
+ if (!definition) return;
+
+ const dataQueriesRepository = entityManager.getRepository(DataQuery);
+ const dataQueries = await dataQueriesRepository.find({
+ where: { appVersionId: version.id },
+ });
+
+ let updateHomepageId = null;
+
+ const appResourceMappings: AppResourceMappings = {
+ pagesMapping: {},
+ componentsMapping: {},
+ };
+ if (definition?.pages) {
+ for (const pageId of Object.keys(definition?.pages)) {
+ const page = definition.pages[pageId];
+ const pagePositionInTheList = Object.keys(definition?.pages).indexOf(pageId);
+ const pageEvents = page.events || [];
+ const pageComponents = page.components;
+
+ const isHomepage = (definition['homePageId'] as any) === pageId;
+
+ const componentEvents = [];
+ const componentLayouts = [];
+ const transformedComponents = this.transformComponentData(
+ pageComponents,
+ componentEvents,
+ appResourceMappings.componentsMapping
+ );
+
+ const newPage = entityManager.create(Page, {
+ name: page.name,
+ handle: page.handle,
+ appVersionId: version.id,
+ disabled: page.disabled || false,
+ hidden: page.hidden || false,
+ index: pagePositionInTheList,
+ });
+
+ const pageCreated = await entityManager.save(newPage);
+
+ appResourceMappings.pagesMapping[pageId] = pageCreated.id;
+
+ transformedComponents.forEach((component) => {
+ component.page = pageCreated;
+ });
+
+ const savedComponents = await entityManager.save(Component, transformedComponents);
+
+ for (const componentId in pageComponents) {
+ const componentLayout = pageComponents[componentId]['layouts'];
+
+ if (componentLayout && appResourceMappings.componentsMapping[componentId]) {
+ for (const type in componentLayout) {
+ const layout = componentLayout[type];
+ const newLayout = new Layout();
+ newLayout.type = type;
+ newLayout.top = layout.top;
+ newLayout.left = layout.left;
+ newLayout.width = layout.width;
+ newLayout.height = layout.height;
+ newLayout.componentId = appResourceMappings.componentsMapping[componentId];
+
+ componentLayouts.push(newLayout);
+ }
+ }
+ }
+
+ await entityManager.save(Layout, componentLayouts);
+
+ if (pageEvents.length > 0) {
+ pageEvents.forEach(async (event, index) => {
+ const newEvent = {
+ name: event.eventId,
+ sourceId: pageCreated.id,
+ target: Target.page,
+ event: event,
+ index: pageEvents.index || index,
+ appVersionId: version.id,
+ };
+
+ await entityManager.save(EventHandler, newEvent);
+ });
+ }
+
+ componentEvents.forEach((eventObj) => {
+ if (eventObj.event?.length === 0) return;
+
+ eventObj.event.forEach(async (event, index) => {
+ const newEvent = {
+ name: event.eventId,
+ sourceId: appResourceMappings.componentsMapping[eventObj.componentId],
+ target: Target.component,
+ event: event,
+ index: eventObj.index || index,
+ appVersionId: version.id,
+ };
+
+ await entityManager.save(EventHandler, newEvent);
+ });
+ });
+
+ savedComponents.forEach(async (component) => {
+ if (component.type === 'Table') {
+ const tableActions = component.properties?.actions?.value || [];
+ const tableColumns = component.properties?.columns?.value || [];
+ const tableActionAndColumnEvents = [];
+
+ tableActions.forEach((action) => {
+ const actionEvents = action.events || [];
+
+ actionEvents.forEach((event, index) => {
+ tableActionAndColumnEvents.push({
+ name: event.eventId,
+ sourceId: component.id,
+ target: Target.tableAction,
+ event: { ...event, ref: action.name },
+ index: event.index ?? index,
+ appVersionId: version.id,
+ });
+ });
+ });
+
+ tableColumns.forEach((column) => {
+ if (column?.columnType !== 'toggle') return;
+ const columnEvents = column.events || [];
+
+ columnEvents.forEach((event, index) => {
+ tableActionAndColumnEvents.push({
+ name: event.eventId,
+ sourceId: component.id,
+ target: Target.tableColumn,
+ event: { ...event, ref: column.name },
+ index: event.index ?? index,
+ appVersionId: version.id,
+ });
+ });
+ });
+
+ await entityManager.save(EventHandler, tableActionAndColumnEvents);
+ }
+ });
+
+ if (isHomepage) {
+ updateHomepageId = pageCreated.id;
+ }
+ }
+ }
+
+ for (const dataQuery of dataQueries) {
+ const queryEvents = dataQuery?.options?.events || [];
+
+ if (queryEvents.length > 0) {
+ queryEvents.forEach(async (event, index) => {
+ const newEvent = {
+ name: event.eventId,
+ sourceId: dataQuery.id,
+ target: Target.dataQuery,
+ event: event,
+ index: queryEvents.index || index,
+ appVersionId: version.id,
+ };
+
+ await entityManager.save(EventHandler, newEvent);
+ });
+ }
+ }
+
+ await entityManager.update(
+ AppVersion,
+ { id: version.id },
+ {
+ homePageId: updateHomepageId,
+ showViewerNavigation: definition?.showViewerNavigation || true,
+ globalSettings: definition.globalSettings,
+ }
+ );
+
+ await this.updateEventActionsForNewVersionWithNewMappingIds(
+ entityManager,
+ version.id,
+ appResourceMappings.componentsMapping,
+ appResourceMappings.pagesMapping
+ );
+
+ migrationProgress.show();
+ }
+ }
+
+ async updateEventActionsForNewVersionWithNewMappingIds(
+ manager: EntityManager,
+ versionId: string,
+ oldComponentToNewComponentMapping: Record,
+ oldPageToNewPageMapping: Record
+ ) {
+ const allEvents = await manager.find(EventHandler, {
+ where: { appVersionId: versionId },
+ });
+
+ for (const event of allEvents) {
+ const eventDefinition = event.event;
+
+ if (eventDefinition?.actionId === 'switch-page') {
+ eventDefinition.pageId = oldPageToNewPageMapping[eventDefinition.pageId];
+ }
+
+ if (eventDefinition?.actionId === 'control-component') {
+ eventDefinition.componentId = oldComponentToNewComponentMapping[eventDefinition.componentId];
+ }
+
+ if (eventDefinition?.actionId == 'show-modal' || eventDefinition?.actionId === 'close-modal') {
+ eventDefinition.modal = oldComponentToNewComponentMapping[eventDefinition.modal];
+ }
+
+ event.event = eventDefinition;
+
+ await manager.save(event);
+ }
+ }
+
+ private transformComponentData(
+ data: object,
+ componentEvents: any[],
+ componentsMapping: Record
+ ): Component[] {
+ const transformedComponents: Component[] = [];
+
+ const allComponents = Object.keys(data).map((key) => {
+ return {
+ id: key,
+ ...data[key],
+ };
+ });
+
+ for (const componentId in data) {
+ const component = data[componentId];
+ const componentData = component['component'];
+
+ let skipComponent = false;
+ const transformedComponent: Component = new Component();
+
+ let parentId = component.parent ? component.parent : null;
+
+ const isParentTabOrCalendar = this.isChildOfTabsOrCalendar(component, allComponents, parentId);
+
+ if (isParentTabOrCalendar) {
+ const childTabId = component.parent.split('-')[component.parent.split('-').length - 1];
+ const _parentId = component?.parent?.split('-').slice(0, -1).join('-');
+ const mappedParentId = componentsMapping[_parentId];
+
+ parentId = `${mappedParentId}-${childTabId}`;
+ } else {
+ if (component.parent && !componentsMapping[parentId]) {
+ skipComponent = true;
+ }
+ parentId = componentsMapping[parentId];
+ }
+
+ if (!skipComponent) {
+ transformedComponent.id = uuid();
+ transformedComponent.name = componentData.name;
+ transformedComponent.type = componentData.component;
+ transformedComponent.properties = componentData.definition.properties || {};
+ transformedComponent.styles = componentData.definition.styles || {};
+ transformedComponent.validation = componentData.definition.validation || {};
+ transformedComponent.general = componentData.definition.general || {};
+ transformedComponent.generalStyles = componentData.definition.generalStyles || {};
+ transformedComponent.displayPreferences = componentData.definition.others || {};
+ transformedComponent.parent = component.parent ? parentId : null;
+
+ transformedComponents.push(transformedComponent);
+
+ componentEvents.push({
+ componentId: componentId,
+ event: componentData.definition.events,
+ });
+ componentsMapping[componentId] = transformedComponent.id;
+ }
+ }
+
+ return transformedComponents;
+ }
+
+ isChildOfTabsOrCalendar = (component, allComponents = [], componentParentId = undefined) => {
+ if (componentParentId) {
+ const parentId = component?.parent?.split('-').slice(0, -1).join('-');
+
+ const parentComponent = allComponents.find((comp) => comp.id === parentId);
+
+ if (parentComponent) {
+ return parentComponent.component.component === 'Tabs' || parentComponent.component.component === 'Calendar';
+ }
+ }
+
+ return false;
+ };
+
+ public async down(queryRunner: QueryRunner): Promise {
+ await queryRunner.query('DELETE FROM page');
+ await queryRunner.query('DELETE FROM component');
+ await queryRunner.query('DELETE FROM layout');
+ await queryRunner.query('DELETE FROM event_handler');
+
+ await queryRunner.query('ALTER TABLE app_version DROP COLUMN IF EXISTS homePageId');
+ await queryRunner.query('ALTER TABLE app_version DROP COLUMN IF EXISTS globalSettings');
+ await queryRunner.query('ALTER TABLE app_version DROP COLUMN IF EXISTS showViewerNavigation');
+ }
+}
diff --git a/server/migrations/1691004576222-UpdateAppVersionEntity.ts b/server/migrations/1691004576222-UpdateAppVersionEntity.ts
new file mode 100644
index 0000000000..0ef8eb2d6f
--- /dev/null
+++ b/server/migrations/1691004576222-UpdateAppVersionEntity.ts
@@ -0,0 +1,41 @@
+import { MigrationInterface, QueryRunner, TableColumn } from 'typeorm';
+
+export class UpdateAppVersionEntity1691006886222 implements MigrationInterface {
+ public async up(queryRunner: QueryRunner): Promise {
+ // Add the new columns to the app_versions table
+ await queryRunner.addColumn(
+ 'app_versions',
+ new TableColumn({
+ name: 'global_settings',
+ type: 'json',
+ isNullable: true,
+ })
+ );
+
+ await queryRunner.addColumn(
+ 'app_versions',
+ new TableColumn({
+ name: 'show_viewer_navigation',
+ type: 'boolean',
+ default: true,
+ isNullable: false,
+ })
+ );
+
+ await queryRunner.addColumn(
+ 'app_versions',
+ new TableColumn({
+ name: 'home_page_id',
+ type: 'uuid',
+ isNullable: true,
+ })
+ );
+ }
+
+ public async down(queryRunner: QueryRunner): Promise {
+ // Remove the new columns from the app_versions table (if necessary)
+ await queryRunner.dropColumn('app_versions', 'global_settings');
+ await queryRunner.dropColumn('app_versions', 'show_viewer_navigation');
+ await queryRunner.dropColumn('app_versions', 'home_page_id');
+ }
+}
diff --git a/server/migrations/1691004576333-CreatePageTable.ts b/server/migrations/1691004576333-CreatePageTable.ts
new file mode 100644
index 0000000000..09831a9929
--- /dev/null
+++ b/server/migrations/1691004576333-CreatePageTable.ts
@@ -0,0 +1,76 @@
+import { MigrationInterface, QueryRunner, Table, TableForeignKey } from 'typeorm';
+
+export class CreatePageTable1691004576333 implements MigrationInterface {
+ public async up(queryRunner: QueryRunner): Promise {
+ await queryRunner.createTable(
+ new Table({
+ name: 'pages',
+ columns: [
+ {
+ name: 'id',
+ type: 'uuid',
+ isPrimary: true,
+ default: 'gen_random_uuid()',
+ },
+ {
+ name: 'name',
+ type: 'varchar',
+ isNullable: false,
+ },
+ {
+ name: 'index',
+ type: 'int',
+ isNullable: false,
+ },
+ {
+ name: 'page_handle',
+ type: 'varchar',
+ isNullable: false,
+ },
+ {
+ name: 'disabled',
+ type: 'boolean',
+ isNullable: true,
+ },
+ {
+ name: 'hidden',
+ type: 'boolean',
+ isNullable: true,
+ },
+ {
+ name: 'app_version_id',
+ type: 'uuid',
+ isNullable: false,
+ },
+ {
+ name: 'created_at',
+ type: 'timestamp',
+ isNullable: true,
+ default: 'now()',
+ },
+ {
+ name: 'updated_at',
+ type: 'timestamp',
+ isNullable: true,
+ default: 'now()',
+ },
+ ],
+ })
+ );
+
+ // Add foreign key to relate Page with AppVersion
+ await queryRunner.createForeignKey(
+ 'pages',
+ new TableForeignKey({
+ columnNames: ['app_version_id'],
+ referencedColumnNames: ['id'],
+ referencedTableName: 'app_versions',
+ onDelete: 'CASCADE',
+ })
+ );
+ }
+
+ public async down(queryRunner: QueryRunner): Promise {
+ await queryRunner.dropTable('pages');
+ }
+}
diff --git a/server/migrations/1691004706564-CreateEventHandlerTable.ts b/server/migrations/1691004706564-CreateEventHandlerTable.ts
new file mode 100644
index 0000000000..027d91840d
--- /dev/null
+++ b/server/migrations/1691004706564-CreateEventHandlerTable.ts
@@ -0,0 +1,78 @@
+import { MigrationInterface, QueryRunner, Table, TableForeignKey } from 'typeorm';
+
+export class CreateEventHandlerTable1691004706564 implements MigrationInterface {
+ public async up(queryRunner: QueryRunner): Promise {
+ await queryRunner.createTable(
+ new Table({
+ name: 'event_handlers',
+ columns: [
+ {
+ name: 'id',
+ type: 'uuid',
+ isPrimary: true,
+ default: 'gen_random_uuid()',
+ },
+ {
+ name: 'name',
+ type: 'varchar',
+ isNullable: false,
+ },
+ {
+ name: 'index',
+ type: 'int',
+ isNullable: false,
+ },
+ {
+ name: 'event',
+ type: 'jsonb',
+ isNullable: false,
+ },
+ {
+ name: 'app_version_id',
+ type: 'uuid',
+ isNullable: false,
+ },
+ {
+ name: 'source_id',
+ type: 'varchar',
+ isNullable: false,
+ },
+ {
+ name: 'target',
+ type: 'enum',
+ enum: ['page', 'component', 'data_query', 'table_column', 'table_action'],
+ default: "'page'",
+ isNullable: false,
+ },
+ {
+ name: 'created_at',
+ type: 'timestamp',
+ isNullable: true,
+ default: 'now()',
+ },
+ {
+ name: 'updated_at',
+ type: 'timestamp',
+ isNullable: true,
+ default: 'now()',
+ },
+ ],
+ })
+ );
+
+ // Add foreign key to relate EventHandler with AppVersion
+ await queryRunner.createForeignKey(
+ 'event_handlers',
+ new TableForeignKey({
+ columnNames: ['app_version_id'],
+ referencedColumnNames: ['id'],
+ referencedTableName: 'app_versions',
+ onDelete: 'CASCADE',
+ })
+ );
+ }
+
+ public async down(queryRunner: QueryRunner): Promise {
+ await queryRunner.dropTable('event_handlers');
+ }
+}
diff --git a/server/migrations/1691006952074-CreateComponentTable.ts b/server/migrations/1691006952074-CreateComponentTable.ts
new file mode 100644
index 0000000000..5bcfd18b88
--- /dev/null
+++ b/server/migrations/1691006952074-CreateComponentTable.ts
@@ -0,0 +1,108 @@
+import { MigrationInterface, QueryRunner, Table, TableForeignKey, TableIndex } from 'typeorm';
+
+export class CreateComponentTable1691006952074 implements MigrationInterface {
+ public async up(queryRunner: QueryRunner): Promise {
+ await queryRunner.createTable(
+ new Table({
+ name: 'components',
+ columns: [
+ {
+ name: 'id',
+ type: 'uuid',
+ isPrimary: true,
+ default: 'gen_random_uuid()',
+ },
+ {
+ name: 'name',
+ type: 'varchar',
+ isNullable: false,
+ },
+ {
+ name: 'type',
+ type: 'varchar',
+ isNullable: false,
+ },
+ {
+ name: 'page_id',
+ type: 'uuid',
+ isNullable: false,
+ },
+ {
+ name: 'parent',
+ type: 'varchar',
+ isNullable: true,
+ },
+ {
+ name: 'properties',
+ type: 'json',
+ isNullable: true,
+ },
+ {
+ name: 'general_properties',
+ type: 'json',
+ isNullable: true,
+ },
+ {
+ name: 'styles',
+ type: 'json',
+ isNullable: true,
+ },
+ {
+ name: 'general_styles',
+ type: 'json',
+ isNullable: true,
+ },
+ {
+ name: 'display_preferences',
+ type: 'json',
+ isNullable: true,
+ },
+ {
+ name: 'validation',
+ type: 'json',
+ isNullable: true,
+ },
+ {
+ name: 'created_at',
+ type: 'timestamp',
+ isNullable: true,
+ default: 'now()',
+ },
+ {
+ name: 'updated_at',
+ type: 'timestamp',
+ isNullable: true,
+ default: 'now()',
+ },
+ ],
+ })
+ );
+
+ await queryRunner.createForeignKey(
+ 'components',
+ new TableForeignKey({
+ columnNames: ['page_id'],
+ referencedColumnNames: ['id'],
+ referencedTableName: 'pages',
+ onDelete: 'CASCADE',
+ })
+ );
+
+ await queryRunner.createIndex('components', new TableIndex({ columnNames: ['name'] }));
+ await queryRunner.createIndex('components', new TableIndex({ columnNames: ['type'] }));
+ await queryRunner.createIndex('components', new TableIndex({ columnNames: ['page_id'] }));
+ }
+
+ public async down(queryRunner: QueryRunner): Promise {
+ // Drop indexes
+ await queryRunner.dropIndex('components', 'IDX_COMPONENT_NAME');
+ await queryRunner.dropIndex('components', 'IDX_COMPONENT_TYPE');
+ await queryRunner.dropIndex('components', 'IDX_COMPONENT_PAGE');
+
+ // Drop foreign key
+ await queryRunner.dropForeignKey('components', 'FK_COMPONENT_PAGE');
+
+ // Drop table
+ await queryRunner.dropTable('components');
+ }
+}
diff --git a/server/migrations/1691007037021-CreateLayoutTable.ts b/server/migrations/1691007037021-CreateLayoutTable.ts
new file mode 100644
index 0000000000..a6b9900df1
--- /dev/null
+++ b/server/migrations/1691007037021-CreateLayoutTable.ts
@@ -0,0 +1,66 @@
+import { MigrationInterface, QueryRunner, Table, TableForeignKey } from 'typeorm';
+
+export class CreateLayoutTable1691007037021 implements MigrationInterface {
+ public async up(queryRunner: QueryRunner): Promise {
+ await queryRunner.createTable(
+ new Table({
+ name: 'layouts',
+ columns: [
+ {
+ name: 'id',
+ type: 'uuid',
+ isPrimary: true,
+ default: 'gen_random_uuid()',
+ },
+ {
+ name: 'type',
+ type: 'enum',
+ enumName: 'layput_type',
+ enum: ['desktop', 'mobile'],
+ isNullable: false,
+ },
+ {
+ name: 'top',
+ type: 'double precision',
+ isNullable: false,
+ },
+ {
+ name: 'left',
+ type: 'double precision',
+ isNullable: false,
+ },
+ {
+ name: 'width',
+ type: 'double precision',
+ isNullable: false,
+ },
+ {
+ name: 'height',
+ type: 'double precision',
+ isNullable: false,
+ },
+ {
+ name: 'component_id',
+ type: 'uuid',
+ isNullable: false,
+ },
+ ],
+ })
+ );
+
+ // Add foreign key to relate Layout with Component
+ await queryRunner.createForeignKey(
+ 'layouts',
+ new TableForeignKey({
+ columnNames: ['component_id'],
+ referencedColumnNames: ['id'],
+ referencedTableName: 'components',
+ onDelete: 'CASCADE',
+ })
+ );
+ }
+
+ public async down(queryRunner: QueryRunner): Promise {
+ await queryRunner.dropTable('layouts');
+ }
+}
diff --git a/server/package-lock.json b/server/package-lock.json
index 9b6d518c17..9c849e112f 100644
--- a/server/package-lock.json
+++ b/server/package-lock.json
@@ -55,7 +55,7 @@
"request-ip": "^3.3.0",
"rxjs": "^7.2.0",
"sanitize-html": "^2.7.0",
- "semver": "^7.3.5",
+ "semver": "^7.5.4",
"ts-node": "^10.0.0",
"tsconfig-paths": "^3.10.1",
"typeorm": "^0.2.38",
@@ -11858,8 +11858,9 @@
}
},
"node_modules/semver": {
- "version": "7.3.5",
- "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==",
+ "version": "7.5.4",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
+ "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
"dependencies": {
"lru-cache": "^6.0.0"
},
@@ -22603,8 +22604,9 @@
}
},
"semver": {
- "version": "7.3.5",
- "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==",
+ "version": "7.5.4",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
+ "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
"requires": {
"lru-cache": "^6.0.0"
}
diff --git a/server/package.json b/server/package.json
index 327c6c4b02..7b977b5f82 100644
--- a/server/package.json
+++ b/server/package.json
@@ -80,7 +80,7 @@
"request-ip": "^3.3.0",
"rxjs": "^7.2.0",
"sanitize-html": "^2.7.0",
- "semver": "^7.3.5",
+ "semver": "^7.5.4",
"ts-node": "^10.0.0",
"tsconfig-paths": "^3.10.1",
"typeorm": "^0.2.38",
diff --git a/server/src/controllers/apps.controller.v2.ts b/server/src/controllers/apps.controller.v2.ts
new file mode 100644
index 0000000000..45944e7210
--- /dev/null
+++ b/server/src/controllers/apps.controller.v2.ts
@@ -0,0 +1,502 @@
+import {
+ Controller,
+ ForbiddenException,
+ Get,
+ Param,
+ Post,
+ Put,
+ Delete,
+ Query,
+ UseGuards,
+ Body,
+ BadRequestException,
+ UseInterceptors,
+} from '@nestjs/common';
+import { JwtAuthGuard } from '../../src/modules/auth/jwt-auth.guard';
+import { AppAuthGuard } from 'src/modules/auth/app-auth.guard';
+import { AppsService } from '../services/apps.service';
+import { camelizeKeys, decamelizeKeys } from 'humps';
+import { AppsAbilityFactory } from 'src/modules/casl/abilities/apps-ability.factory';
+
+import { App } from 'src/entities/app.entity';
+import { User } from 'src/decorators/user.decorator';
+
+import { CreatePageDto, DeletePageDto } from '@dto/pages.dto';
+import { CreateComponentDto, DeleteComponentDto, UpdateComponentDto, LayoutUpdateDto } from '@dto/component.dto';
+
+import { ValidAppInterceptor } from 'src/interceptors/valid.app.interceptor';
+import { AppDecorator } from 'src/decorators/app.decorator';
+
+import { ComponentsService } from '@services/components.service';
+import { PageService } from '@services/page.service';
+import { EventsService } from '@services/events_handler.service';
+import { AppVersionUpdateDto } from '@dto/app-version-update.dto';
+import { CreateEventHandlerDto, UpdateEventHandlerDto } from '@dto/event-handler.dto';
+
+@Controller({
+ path: 'apps',
+ version: '2',
+})
+export class AppsControllerV2 {
+ constructor(
+ private appsService: AppsService,
+ private componentsService: ComponentsService,
+ private pageService: PageService,
+ private eventsService: EventsService,
+ private eventService: EventsService,
+ private appsAbilityFactory: AppsAbilityFactory
+ ) {}
+
+ @UseGuards(JwtAuthGuard)
+ @UseInterceptors(ValidAppInterceptor)
+ @Get(':id')
+ async show(@User() user, @AppDecorator() app: App, @Query('access_type') accessType: string) {
+ const ability = await this.appsAbilityFactory.appsActions(user, app.id);
+ if (!ability.can('viewApp', app)) {
+ throw new ForbiddenException(
+ JSON.stringify({
+ organizationId: app.organizationId,
+ })
+ );
+ }
+
+ if (accessType === 'edit' && !ability.can('editApp', app)) {
+ throw new ForbiddenException(
+ JSON.stringify({
+ organizationId: app.organizationId,
+ })
+ );
+ }
+
+ const response = decamelizeKeys(app);
+
+ const seralizedQueries = [];
+ const dataQueriesForVersion = app.editingVersion
+ ? await this.appsService.findDataQueriesForVersion(app.editingVersion.id)
+ : [];
+
+ const pagesForVersion = app.editingVersion ? await this.pageService.findPagesForVersion(app.editingVersion.id) : [];
+ const eventsForVersion = app.editingVersion
+ ? await this.eventsService.findEventsForVersion(app.editingVersion.id)
+ : [];
+
+ // serialize queries
+ for (const query of dataQueriesForVersion) {
+ const decamelizedQuery = decamelizeKeys(query);
+ decamelizedQuery['options'] = query.options;
+ seralizedQueries.push(decamelizedQuery);
+ }
+
+ response['data_queries'] = seralizedQueries;
+ response['definition'] = app.editingVersion?.definition;
+ response['pages'] = pagesForVersion;
+ response['events'] = eventsForVersion;
+
+ //! if editing version exists, camelize the definition
+ if (app.editingVersion && app.editingVersion.definition) {
+ response['editing_version'] = {
+ ...response['editing_version'],
+ definition: camelizeKeys(app.editingVersion.definition),
+ };
+ }
+
+ return response;
+ }
+
+ @UseGuards(AppAuthGuard) // This guard will allow access for unauthenticated user if the app is public
+ @Get('slugs/:slug')
+ async appFromSlug(@User() user, @AppDecorator() app: App) {
+ if (user) {
+ const ability = await this.appsAbilityFactory.appsActions(user, app.id);
+
+ if (!ability.can('viewApp', app)) {
+ throw new ForbiddenException(
+ JSON.stringify({
+ organizationId: app.organizationId,
+ })
+ );
+ }
+ }
+
+ const versionToLoad = app.currentVersionId
+ ? await this.appsService.findVersion(app.currentVersionId)
+ : await this.appsService.findVersion(app.editingVersion?.id);
+
+ const pagesForVersion = app.editingVersion ? await this.pageService.findPagesForVersion(versionToLoad.id) : [];
+ const eventsForVersion = app.editingVersion ? await this.eventsService.findEventsForVersion(versionToLoad.id) : [];
+
+ // serialize
+ return {
+ current_version_id: app['currentVersionId'],
+ data_queries: versionToLoad?.dataQueries,
+ definition: versionToLoad?.definition,
+ is_public: app.isPublic,
+ is_maintenance_on: app.isMaintenanceOn,
+ name: app.name,
+ slug: app.slug,
+ events: eventsForVersion,
+ pages: pagesForVersion,
+ homePageId: versionToLoad.homePageId,
+ globalSettings: versionToLoad.globalSettings,
+ showViewerNavigation: versionToLoad.showViewerNavigation,
+ };
+ }
+
+ @UseGuards(JwtAuthGuard)
+ @UseInterceptors(ValidAppInterceptor)
+ @Get(':id/versions/:versionId')
+ async version(@User() user, @Param('id') id, @Param('versionId') versionId) {
+ const appVersion = await this.appsService.findVersion(versionId);
+ const app = appVersion.app;
+
+ if (app.id !== id) {
+ throw new BadRequestException();
+ }
+ const ability = await this.appsAbilityFactory.appsActions(user, app.id);
+
+ if (!ability.can('fetchVersions', app)) {
+ throw new ForbiddenException(
+ JSON.stringify({
+ organizationId: app.organizationId,
+ })
+ );
+ }
+
+ const pagesForVersion = await this.pageService.findPagesForVersion(versionId);
+ const eventsForVersion = await this.eventsService.findEventsForVersion(versionId);
+
+ const appCurrentEditingVersion = JSON.parse(JSON.stringify(appVersion));
+
+ delete appCurrentEditingVersion['app'];
+
+ const appData = {
+ ...app,
+ };
+
+ delete appData['editingVersion'];
+
+ return {
+ ...appData,
+ editing_version: camelizeKeys(appCurrentEditingVersion),
+ pages: pagesForVersion,
+ events: eventsForVersion,
+ };
+ }
+
+ @UseGuards(JwtAuthGuard)
+ @UseInterceptors(ValidAppInterceptor)
+ @Put(':id/versions/:versionId')
+ async updateVersion(
+ @User() user,
+ @Param('id') id,
+ @Param('versionId') versionId,
+ @Body() appVersionUpdateDto: AppVersionUpdateDto
+ ) {
+ const version = await this.appsService.findVersion(versionId);
+ const app = version.app;
+
+ if (app.id !== id) {
+ throw new BadRequestException();
+ }
+ const ability = await this.appsAbilityFactory.appsActions(user, id);
+
+ if (!ability.can('updateVersions', app)) {
+ throw new ForbiddenException('You do not have permissions to perform this action');
+ }
+
+ return await this.appsService.updateAppVersion(version, appVersionUpdateDto);
+ }
+
+ @UseGuards(JwtAuthGuard)
+ @UseInterceptors(ValidAppInterceptor)
+ @Put(':id/versions/:versionId/global_settings')
+ async updateGlobalSettings(
+ @User() user,
+ @Param('id') id,
+ @Param('versionId') versionId,
+ @Body() appVersionUpdateDto: AppVersionUpdateDto
+ ) {
+ const version = await this.appsService.findVersion(versionId);
+ const app = version.app;
+
+ if (app.id !== id) {
+ throw new BadRequestException();
+ }
+ const ability = await this.appsAbilityFactory.appsActions(user, id);
+
+ if (!ability.can('updateVersions', app)) {
+ throw new ForbiddenException('You do not have permissions to perform this action');
+ }
+
+ return await this.appsService.updateAppVersion(version, appVersionUpdateDto);
+ }
+
+ //components api
+ @UseGuards(JwtAuthGuard)
+ @UseInterceptors(ValidAppInterceptor)
+ @Post(':id/versions/:versionId/components')
+ async createComponent(
+ @User() user,
+ @Param('id') id,
+ @Param('versionId') versionId,
+ @Body() createComponentDto: CreateComponentDto
+ ) {
+ const version = await this.appsService.findVersion(versionId);
+ const app = version.app;
+
+ if (app.id !== id) {
+ throw new BadRequestException();
+ }
+ const ability = await this.appsAbilityFactory.appsActions(user, id);
+
+ if (!ability.can('updateVersions', app)) {
+ throw new ForbiddenException('You do not have permissions to perform this action');
+ }
+
+ await this.componentsService.create(createComponentDto.diff, createComponentDto.pageId, versionId);
+ }
+
+ @UseGuards(JwtAuthGuard)
+ @UseInterceptors(ValidAppInterceptor)
+ @Put(':id/versions/:versionId/components')
+ async updateComponent(
+ @User() user,
+ @Param('id') id,
+ @Param('versionId') versionId,
+ @Body() updateComponentDto: UpdateComponentDto
+ ) {
+ const version = await this.appsService.findVersion(versionId);
+ const app = version.app;
+
+ if (app.id !== id) {
+ throw new BadRequestException();
+ }
+ const ability = await this.appsAbilityFactory.appsActions(user, id);
+
+ if (!ability.can('updateVersions', app)) {
+ throw new ForbiddenException('You do not have permissions to perform this action');
+ }
+
+ await this.componentsService.update(updateComponentDto.diff, versionId);
+ }
+
+ @UseGuards(JwtAuthGuard)
+ @UseInterceptors(ValidAppInterceptor)
+ @Delete(':id/versions/:versionId/components')
+ async deleteComponents(
+ @User() user,
+ @Param('id') id,
+ @Param('versionId') versionId,
+ @Body() deleteComponentDto: DeleteComponentDto
+ ) {
+ const version = await this.appsService.findVersion(versionId);
+ const app = version.app;
+
+ if (app.id !== id) {
+ throw new BadRequestException();
+ }
+ const ability = await this.appsAbilityFactory.appsActions(user, id);
+
+ if (!ability.can('updateVersions', app)) {
+ throw new ForbiddenException('You do not have permissions to perform this action');
+ }
+
+ await this.componentsService.delete(deleteComponentDto.diff, versionId, deleteComponentDto.is_component_cut);
+ }
+
+ @UseGuards(JwtAuthGuard)
+ @UseInterceptors(ValidAppInterceptor)
+ @Put(':id/versions/:versionId/components/layout')
+ async updateComponentLayout(
+ @User() user,
+ @Param('id') id,
+ @Param('versionId') versionId,
+ @Body() updateComponentLayout: LayoutUpdateDto
+ ) {
+ const version = await this.appsService.findVersion(versionId);
+ const app = version.app;
+
+ if (app.id !== id) {
+ throw new BadRequestException();
+ }
+ const ability = await this.appsAbilityFactory.appsActions(user, id);
+
+ if (!ability.can('updateVersions', app)) {
+ throw new ForbiddenException('You do not have permissions to perform this action');
+ }
+
+ await this.componentsService.componentLayoutChange(updateComponentLayout.diff, versionId);
+ }
+
+ // pages api
+ @UseGuards(JwtAuthGuard)
+ @UseInterceptors(ValidAppInterceptor)
+ @Post(':id/versions/:versionId/pages')
+ async createPages(
+ @User() user,
+ @Param('id') id,
+ @Param('versionId') versionId,
+ @Body() createPageDto: CreatePageDto
+ ) {
+ const version = await this.appsService.findVersion(versionId);
+ const app = version.app;
+
+ if (app.id !== id) {
+ throw new BadRequestException();
+ }
+ const ability = await this.appsAbilityFactory.appsActions(user, id);
+
+ if (!ability.can('updateVersions', app)) {
+ throw new ForbiddenException('You do not have permissions to perform this action');
+ }
+
+ await this.pageService.createPage(createPageDto, versionId);
+ }
+
+ @UseGuards(JwtAuthGuard)
+ @UseInterceptors(ValidAppInterceptor)
+ @Post(':id/versions/:versionId/pages/:pageId/clone')
+ async clonePage(@User() user, @Param('id') id, @Param('versionId') versionId, @Param('pageId') pageId) {
+ const version = await this.appsService.findVersion(versionId);
+ const app = version.app;
+ const ability = await this.appsAbilityFactory.appsActions(user, id);
+
+ if (!ability.can('updateVersions', app)) {
+ throw new ForbiddenException('You do not have permissions to perform this action');
+ }
+
+ return await this.pageService.clonePage(pageId, versionId);
+ }
+
+ @UseGuards(JwtAuthGuard)
+ @UseInterceptors(ValidAppInterceptor)
+ @Put(':id/versions/:versionId/pages')
+ async updatePages(@User() user, @Param('id') id, @Param('versionId') versionId, @Body() updatePageDto) {
+ const version = await this.appsService.findVersion(versionId);
+ const app = version.app;
+
+ if (app.id !== id) {
+ throw new BadRequestException();
+ }
+ const ability = await this.appsAbilityFactory.appsActions(user, id);
+
+ if (!ability.can('updateVersions', app)) {
+ throw new ForbiddenException('You do not have permissions to perform this action');
+ }
+
+ await this.pageService.updatePage(updatePageDto, versionId);
+ }
+
+ @UseGuards(JwtAuthGuard)
+ @UseInterceptors(ValidAppInterceptor)
+ @Delete(':id/versions/:versionId/pages')
+ async deletePage(@User() user, @Param('id') id, @Param('versionId') versionId, @Body() deletePageDto: DeletePageDto) {
+ const version = await this.appsService.findVersion(versionId);
+ const app = version.app;
+
+ if (app.id !== id) {
+ throw new BadRequestException();
+ }
+ const ability = await this.appsAbilityFactory.appsActions(user, id);
+
+ if (!ability.can('updateVersions', app)) {
+ throw new ForbiddenException('You do not have permissions to perform this action');
+ }
+
+ await this.pageService.deletePage(deletePageDto.pageId, versionId);
+ }
+
+ // events api
+
+ @UseGuards(JwtAuthGuard)
+ @UseInterceptors(ValidAppInterceptor)
+ @Get(':id/versions/:versionId/events')
+ async getEvents(@User() user, @Param('id') id, @Param('versionId') versionId, @Query('sourceId') sourceId) {
+ const version = await this.appsService.findVersion(versionId);
+ const app = version.app;
+
+ if (app.id !== id) {
+ throw new BadRequestException();
+ }
+
+ const ability = await this.appsAbilityFactory.appsActions(user, id);
+
+ if (!ability.can('viewApp', app)) {
+ throw new ForbiddenException('You do not have permissions to perform this action');
+ }
+
+ if (!sourceId) {
+ return this.eventService.findEventsForVersion(versionId);
+ }
+
+ return this.eventService.findAllEventsWithSourceId(sourceId);
+ }
+
+ @UseGuards(JwtAuthGuard)
+ @UseInterceptors(ValidAppInterceptor)
+ @Post(':id/versions/:versionId/events')
+ async createEvent(
+ @User() user,
+ @Param('id') id,
+ @Param('versionId') versionId,
+ @Body() createEventHandlerDto: CreateEventHandlerDto
+ ) {
+ const version = await this.appsService.findVersion(versionId);
+ const app = version.app;
+
+ if (app.id !== id) {
+ throw new BadRequestException();
+ }
+ const ability = await this.appsAbilityFactory.appsActions(user, id);
+
+ if (!ability.can('updateVersions', app)) {
+ throw new ForbiddenException('You do not have permissions to perform this action');
+ }
+
+ return this.eventService.createEvent(createEventHandlerDto, versionId);
+ }
+ @UseGuards(JwtAuthGuard)
+ @UseInterceptors(ValidAppInterceptor)
+ @Put(':id/versions/:versionId/events')
+ async updateEvents(
+ @User() user,
+ @Param('id') id,
+ @Param('versionId') versionId,
+ @Body() updateEventHandlerDto: UpdateEventHandlerDto
+ ) {
+ const version = await this.appsService.findVersion(versionId);
+ const app = version.app;
+
+ if (app.id !== id) {
+ throw new BadRequestException();
+ }
+ const ability = await this.appsAbilityFactory.appsActions(user, id);
+
+ if (!ability.can('updateVersions', app)) {
+ throw new ForbiddenException('You do not have permissions to perform this action');
+ }
+
+ const { events, updateType } = updateEventHandlerDto;
+
+ return await this.eventService.updateEvent(events, updateType, versionId);
+ }
+
+ @UseGuards(JwtAuthGuard)
+ @UseInterceptors(ValidAppInterceptor)
+ @Delete(':id/versions/:versionId/events/:eventId')
+ async deleteEvents(@User() user, @Param('id') id, @Param('versionId') versionId, @Param('eventId') eventId) {
+ const version = await this.appsService.findVersion(versionId);
+ const app = version.app;
+
+ if (app.id !== id) {
+ throw new BadRequestException();
+ }
+ const ability = await this.appsAbilityFactory.appsActions(user, id);
+
+ if (!ability.can('updateVersions', app)) {
+ throw new ForbiddenException('You do not have permissions to perform this action');
+ }
+
+ return await this.eventService.deleteEvent(eventId, versionId);
+ }
+}
diff --git a/server/src/dto/app-version-update.dto.ts b/server/src/dto/app-version-update.dto.ts
new file mode 100644
index 0000000000..53d79b387a
--- /dev/null
+++ b/server/src/dto/app-version-update.dto.ts
@@ -0,0 +1,26 @@
+import { IsBoolean, IsNotEmpty, IsOptional, IsString, IsUUID, MaxLength } from 'class-validator';
+import { Transform } from 'class-transformer';
+import { sanitizeInput } from '../helpers/utils.helper';
+
+export class AppVersionUpdateDto {
+ @IsString()
+ @IsOptional()
+ @Transform(({ value }) => {
+ const newValue = sanitizeInput(value);
+ return newValue.trim();
+ })
+ @IsNotEmpty()
+ @MaxLength(50, { message: 'Maximum length has been reached.' })
+ name: string;
+
+ @IsBoolean()
+ @IsOptional()
+ showViewerNavigation: boolean;
+
+ @IsUUID()
+ @IsOptional()
+ homePageId: string;
+
+ @IsOptional()
+ globalSettings: any;
+}
diff --git a/server/src/dto/component.dto.ts b/server/src/dto/component.dto.ts
new file mode 100644
index 0000000000..1c12fa8ce5
--- /dev/null
+++ b/server/src/dto/component.dto.ts
@@ -0,0 +1,159 @@
+import { Type } from 'class-transformer';
+import {
+ IsArray,
+ IsBoolean,
+ IsNotEmpty,
+ IsNumber,
+ IsObject,
+ IsOptional,
+ IsString,
+ IsUUID,
+ ValidationArguments,
+ ValidatorConstraint,
+ ValidatorConstraintInterface,
+ Validate,
+} from 'class-validator';
+
+export class ComponentLayoutDto {
+ @IsNumber()
+ @IsOptional()
+ top?: number;
+
+ @IsNumber()
+ @IsOptional()
+ left?: number;
+
+ @IsNumber()
+ @IsOptional()
+ width?: number;
+
+ @IsNumber()
+ @IsOptional()
+ height?: number;
+}
+
+export class LayoutData {
+ @IsObject()
+ @IsOptional()
+ desktop?: ComponentLayoutDto;
+
+ @IsObject()
+ @IsOptional()
+ mobile?: ComponentLayoutDto;
+}
+
+@ValidatorConstraint({ name: 'LayoutDataValidator', async: false })
+class LayoutDataValidator implements ValidatorConstraintInterface {
+ validate(value: any) {
+ if (value) {
+ for (const key in value) {
+ if (!value[key] || typeof value[key] !== 'object' || !value[key].layouts) {
+ return false;
+ }
+ }
+ }
+ return true;
+ }
+
+ defaultMessage(args: ValidationArguments) {
+ return `Each key in "diff" must have the structure { layouts: LayoutData }`;
+ }
+}
+
+export class LayoutUpdateDto {
+ @IsBoolean()
+ is_user_switched_version: boolean;
+
+ @IsUUID()
+ pageId: string;
+
+ @IsObject()
+ @IsNotEmpty()
+ @Validate(LayoutDataValidator, { each: true })
+ diff: Record;
+}
+
+class ComponentDto {
+ @IsString()
+ name: string;
+
+ @IsObject()
+ properties: Record;
+
+ @IsObject()
+ styles: Record;
+
+ @IsObject()
+ validation: Record;
+
+ @IsString()
+ type: string;
+
+ @IsObject()
+ others: Record;
+
+ @IsOptional()
+ @Type(() => ComponentLayoutDto)
+ layouts: ComponentLayoutDto;
+
+ @IsOptional()
+ parent: string;
+}
+
+@ValidatorConstraint({ name: 'CreateComponentDtoValidator', async: false })
+class CreateComponentDtoValidator implements ValidatorConstraintInterface {
+ validate(value: any, args: ValidationArguments) {
+ // Check if the diff structure is valid
+ for (const key in value.diff) {
+ if (!value.diff[key] || typeof value.diff[key] !== 'object') {
+ return false;
+ }
+ // You can add additional checks for the component structure here
+ }
+
+ return true;
+ }
+
+ defaultMessage(args: ValidationArguments) {
+ return `Invalid structure in diff for CreateComponentDto`;
+ }
+}
+
+export class CreateComponentDto {
+ @IsBoolean()
+ is_user_switched_version: boolean;
+
+ @IsUUID()
+ pageId: string;
+
+ @IsObject()
+ @Validate(CreateComponentDtoValidator)
+ diff: Record;
+}
+
+export class UpdateComponentDto {
+ @IsBoolean()
+ is_user_switched_version: boolean;
+
+ @IsUUID()
+ pageId: string;
+
+ @IsObject()
+ @Validate(CreateComponentDtoValidator)
+ diff: Record;
+}
+
+export class DeleteComponentDto {
+ @IsBoolean()
+ is_user_switched_version: boolean;
+
+ @IsUUID()
+ pageId: string;
+
+ @IsArray()
+ diff: string[];
+
+ @IsBoolean()
+ @IsOptional()
+ is_component_cut: boolean;
+}
diff --git a/server/src/dto/event-handler.dto.ts b/server/src/dto/event-handler.dto.ts
new file mode 100644
index 0000000000..80623de736
--- /dev/null
+++ b/server/src/dto/event-handler.dto.ts
@@ -0,0 +1,44 @@
+import { IsArray, IsIn, IsNumber, IsObject, IsString, IsUUID, ValidateNested } from 'class-validator';
+import { Target } from 'src/entities/event_handler.entity';
+
+export class CreateEventHandlerDto {
+ @IsObject()
+ event: any;
+
+ @IsString()
+ eventType: Target;
+
+ @IsString()
+ attachedTo: string;
+
+ @IsNumber()
+ index: number;
+}
+
+class UpdateEventDiff {
+ @IsString()
+ name: string;
+
+ @IsNumber()
+ index: number;
+
+ @IsObject()
+ @ValidateNested()
+ event: any;
+}
+
+export class UpdateEvent {
+ @IsUUID()
+ event_id: string;
+
+ @IsObject()
+ diff: UpdateEventDiff;
+}
+
+export class UpdateEventHandlerDto {
+ @IsArray()
+ events: UpdateEvent[];
+
+ @IsIn(['update', 'reorder'])
+ updateType: 'update' | 'reorder';
+}
diff --git a/server/src/dto/pages.dto.ts b/server/src/dto/pages.dto.ts
new file mode 100644
index 0000000000..875585b3ce
--- /dev/null
+++ b/server/src/dto/pages.dto.ts
@@ -0,0 +1,38 @@
+import { IsNotEmpty, IsNumber, IsOptional, IsString, IsUUID, MaxLength } from 'class-validator';
+
+export class CreatePageDto {
+ @IsUUID()
+ @IsNotEmpty()
+ id: string;
+
+ @IsString()
+ @IsNotEmpty()
+ @MaxLength(32)
+ name: string;
+
+ @IsString()
+ @IsNotEmpty()
+ @MaxLength(50)
+ handle: string;
+
+ @IsNumber()
+ @IsNotEmpty()
+ index: number;
+
+ @IsOptional()
+ disabled: boolean;
+
+ @IsOptional()
+ hidden: boolean;
+}
+
+export class DeletePageDto {
+ @IsUUID()
+ @IsNotEmpty()
+ pageId: string;
+}
+
+export class UpdatePageDto {
+ pageId: string;
+ diff: Partial;
+}
diff --git a/server/src/dto/version-edit.dto.ts b/server/src/dto/version-edit.dto.ts
index 5bbdf2a90c..2736652982 100644
--- a/server/src/dto/version-edit.dto.ts
+++ b/server/src/dto/version-edit.dto.ts
@@ -20,4 +20,11 @@ export class VersionEditDto {
@IsOptional()
@IsBoolean()
is_user_switched_version: boolean;
+
+ @IsOptional()
+ diff: any;
+
+ @IsOptional()
+ @IsString()
+ pageId: string;
}
diff --git a/server/src/entities/app_version.entity.ts b/server/src/entities/app_version.entity.ts
index 17027de2f5..683d7f7c5d 100644
--- a/server/src/entities/app_version.entity.ts
+++ b/server/src/entities/app_version.entity.ts
@@ -13,6 +13,8 @@ import {
import { App } from './app.entity';
import { DataQuery } from './data_query.entity';
import { DataSource } from './data_source.entity';
+import { Page } from './page.entity';
+import { EventHandler } from './event_handler.entity';
@Entity({ name: 'app_versions' })
@Unique(['name', 'appId'])
@@ -26,6 +28,15 @@ export class AppVersion extends BaseEntity {
@Column('simple-json', { name: 'definition' })
definition;
+ @Column('simple-json', { name: 'global_settings' })
+ globalSettings;
+
+ @Column({ name: 'show_viewer_navigation' })
+ showViewerNavigation: boolean;
+
+ @Column({ name: 'home_page_id' })
+ homePageId: string;
+
@Column({ name: 'app_id' })
appId: string;
@@ -47,4 +58,12 @@ export class AppVersion extends BaseEntity {
@OneToMany(() => DataQuery, (dataQuery) => dataQuery.appVersion)
dataQueries: DataQuery[];
+
+ @OneToMany(() => Page, (page) => page.appVersion, { onDelete: 'CASCADE' })
+ pages: Page[];
+
+ @OneToMany(() => EventHandler, (eventHandler) => eventHandler.appVersion, {
+ onDelete: 'CASCADE',
+ })
+ eventHandlers: EventHandler[];
}
diff --git a/server/src/entities/component.entity.ts b/server/src/entities/component.entity.ts
new file mode 100644
index 0000000000..bb22c7dcd4
--- /dev/null
+++ b/server/src/entities/component.entity.ts
@@ -0,0 +1,63 @@
+import {
+ Entity,
+ PrimaryGeneratedColumn,
+ Column,
+ ManyToOne,
+ OneToMany,
+ JoinColumn,
+ Index,
+ CreateDateColumn,
+ UpdateDateColumn,
+} from 'typeorm';
+import { Page } from './page.entity';
+import { Layout } from './layout.entity';
+
+@Entity({ name: 'components' })
+@Index('idx_component_page_id', ['pageId'])
+export class Component {
+ @PrimaryGeneratedColumn('uuid')
+ id: string;
+
+ @Column()
+ name: string;
+
+ @Column()
+ type: string;
+
+ @Column({ name: 'page_id' })
+ pageId: string;
+
+ @Column({ nullable: true })
+ parent: string;
+
+ @Column('simple-json')
+ properties: any;
+
+ @Column('simple-json', { name: 'general_properties', nullable: true })
+ general: any;
+
+ @Column('simple-json')
+ styles: any;
+
+ @Column('simple-json', { name: 'general_styles', nullable: true })
+ generalStyles: any;
+
+ @Column('simple-json', { name: 'display_preferences', nullable: true })
+ displayPreferences: any;
+
+ @Column('simple-json')
+ validation: any;
+
+ @CreateDateColumn({ default: () => 'now()', name: 'created_at' })
+ createdAt: Date;
+
+ @UpdateDateColumn({ default: () => 'now()', name: 'updated_at' })
+ updatedAt: Date;
+
+ @ManyToOne(() => Page, (page) => page.components)
+ @JoinColumn({ name: 'page_id' })
+ page: Page;
+
+ @OneToMany(() => Layout, (layout) => layout.component)
+ layouts: Layout[];
+}
diff --git a/server/src/entities/event_handler.entity.ts b/server/src/entities/event_handler.entity.ts
new file mode 100644
index 0000000000..6585d640e0
--- /dev/null
+++ b/server/src/entities/event_handler.entity.ts
@@ -0,0 +1,52 @@
+import {
+ Entity,
+ PrimaryGeneratedColumn,
+ Column,
+ ManyToOne,
+ JoinColumn,
+ CreateDateColumn,
+ UpdateDateColumn,
+} from 'typeorm';
+import { AppVersion } from './app_version.entity';
+
+export enum Target {
+ page = 'page',
+ component = 'component',
+ dataQuery = 'data_query',
+ tableColumn = 'table_column',
+ tableAction = 'table_action',
+}
+
+@Entity({ name: 'event_handlers' })
+export class EventHandler {
+ @PrimaryGeneratedColumn('uuid')
+ id: string;
+
+ @Column()
+ name: string;
+
+ @Column()
+ index: number;
+
+ @Column('simple-json')
+ event: any;
+
+ @Column({ name: 'source_id' })
+ sourceId: string;
+
+ @Column()
+ target: Target;
+
+ @Column({ name: 'app_version_id' })
+ appVersionId: string;
+
+ @CreateDateColumn({ default: () => 'now()', name: 'created_at' })
+ createdAt: Date;
+
+ @UpdateDateColumn({ default: () => 'now()', name: 'updated_at' })
+ updatedAt: Date;
+
+ @ManyToOne(() => AppVersion, (appVersion) => appVersion.pages)
+ @JoinColumn({ name: 'app_version_id' })
+ appVersion: AppVersion;
+}
diff --git a/server/src/entities/layout.entity.ts b/server/src/entities/layout.entity.ts
new file mode 100644
index 0000000000..42beecdd62
--- /dev/null
+++ b/server/src/entities/layout.entity.ts
@@ -0,0 +1,30 @@
+import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn } from 'typeorm';
+import { Component } from './component.entity';
+
+@Entity({ name: 'layouts' })
+export class Layout {
+ @PrimaryGeneratedColumn('uuid')
+ id: string;
+
+ @Column({ type: 'enum', enumName: 'layout_type', enum: ['desktop', 'mobile'] })
+ type: string;
+
+ @Column({ type: 'double precision' })
+ top: number;
+
+ @Column({ type: 'double precision' })
+ left: number;
+
+ @Column({ type: 'double precision' })
+ width: number;
+
+ @Column({ type: 'double precision' })
+ height: number;
+
+ @Column({ name: 'component_id' })
+ componentId: string;
+
+ @ManyToOne(() => Component, (component) => component.layouts)
+ @JoinColumn({ name: 'component_id' })
+ component: Component;
+}
diff --git a/server/src/entities/page.entity.ts b/server/src/entities/page.entity.ts
new file mode 100644
index 0000000000..2731d301e7
--- /dev/null
+++ b/server/src/entities/page.entity.ts
@@ -0,0 +1,49 @@
+import {
+ Entity,
+ PrimaryGeneratedColumn,
+ Column,
+ ManyToOne,
+ OneToMany,
+ JoinColumn,
+ CreateDateColumn,
+ UpdateDateColumn,
+} from 'typeorm';
+import { AppVersion } from './app_version.entity';
+import { Component } from './component.entity';
+
+@Entity({ name: 'pages' })
+export class Page {
+ @PrimaryGeneratedColumn('uuid')
+ id: string;
+
+ @Column()
+ name: string;
+
+ @Column({ name: 'page_handle' })
+ handle: string;
+
+ @Column()
+ index: number;
+
+ @Column()
+ disabled: boolean;
+
+ @Column()
+ hidden: boolean;
+
+ @CreateDateColumn({ default: () => 'now()', name: 'created_at' })
+ createdAt: Date;
+
+ @UpdateDateColumn({ default: () => 'now()', name: 'updated_at' })
+ updatedAt: Date;
+
+ @Column({ name: 'app_version_id' })
+ appVersionId: string;
+
+ @ManyToOne(() => AppVersion, (appVersion) => appVersion.pages)
+ @JoinColumn({ name: 'app_version_id' })
+ appVersion: AppVersion;
+
+ @OneToMany(() => Component, (component) => component.page)
+ components: Component[];
+}
diff --git a/server/src/helpers/utils.helper.ts b/server/src/helpers/utils.helper.ts
index ff16e9337d..471b88c52a 100644
--- a/server/src/helpers/utils.helper.ts
+++ b/server/src/helpers/utils.helper.ts
@@ -5,6 +5,7 @@ import { isEmpty } from 'lodash';
import { ConflictException } from '@nestjs/common';
import { DataBaseConstraints } from './db_constraints.constants';
const protobuf = require('protobufjs');
+const semver = require('semver');
export function maybeSetSubPath(path) {
const hasSubPath = process.env.SUB_PATH !== undefined;
@@ -80,6 +81,26 @@ export async function dbTransactionWrap(operation: (...args) => any, manager?: E
}
}
+export const updateTimestampForAppVersion = async (manager, appVersionId) => {
+ const appVersion = await manager.findOne('app_versions', appVersionId);
+ if (appVersion) {
+ await manager.update('app_versions', appVersionId, { updatedAt: new Date() });
+ }
+};
+
+export async function dbTransactionForAppVersionAssociationsUpdate(
+ operation: (...args) => any,
+ appVersionId: string
+): Promise {
+ return await getManager().transaction(async (manager) => {
+ const result = await operation(manager);
+
+ await updateTimestampForAppVersion(manager, appVersionId);
+
+ return result;
+ });
+}
+
type DbContraintAndMsg = {
dbConstraint: DataBaseConstraints;
message: string;
@@ -196,3 +217,21 @@ export const generateOrgInviteURL = (organizationToken: string, organizationId?:
organizationId ? `?oid=${organizationId}` : ''
}`;
};
+
+export function extractMajorVersion(version) {
+ return semver.valid(semver.coerce(version));
+}
+
+/**
+ * Checks if a given Tooljet version is compatible with normalized app definition schemas.
+ *
+ * This function uses the 'semver' library to compare the provided version with a minimum version requirement
+ * for normalized app definition schemas (2.24.1). It returns true if the version is greater than or equal to
+ * the required version, indicating compatibility.
+ *
+ * @param {string} version - The Tooljet version to check.
+ * @returns {boolean} - True if the version is compatible, false otherwise.
+ */
+export function isTooljetVersionWithNormalizedAppDefinitionSchem(version) {
+ return semver.satisfies(semver.coerce(version), '>= 2.24.0');
+}
diff --git a/server/src/modules/apps/apps.module.ts b/server/src/modules/apps/apps.module.ts
index 400ad7d44b..551d81de68 100644
--- a/server/src/modules/apps/apps.module.ts
+++ b/server/src/modules/apps/apps.module.ts
@@ -3,6 +3,7 @@ import { TypeOrmModule } from '@nestjs/typeorm';
import { App } from '../../entities/app.entity';
import { File } from '../../entities/file.entity';
import { AppsController } from '../../controllers/apps.controller';
+import { AppsControllerV2 } from '../../controllers/apps.controller.v2';
import { AppsService } from '../../services/apps.service';
import { AppVersion } from '../../../src/entities/app_version.entity';
import { DataQuery } from '../../../src/entities/data_query.entity';
@@ -33,6 +34,15 @@ import { Plugin } from 'src/entities/plugin.entity';
import { PluginsHelper } from 'src/helpers/plugins.helper';
import { AppEnvironmentService } from '@services/app_environments.service';
+import { Component } from 'src/entities/component.entity';
+import { Page } from 'src/entities/page.entity';
+import { EventHandler } from 'src/entities/event_handler.entity';
+import { Layout } from 'src/entities/layout.entity';
+
+import { ComponentsService } from '@services/components.service';
+import { PageService } from '@services/page.service';
+import { EventsService } from '@services/events_handler.service';
+
@Module({
imports: [
TypeOrmModule.forFeature([
@@ -52,6 +62,10 @@ import { AppEnvironmentService } from '@services/app_environments.service';
Credential,
File,
Plugin,
+ Component,
+ Page,
+ EventHandler,
+ Layout,
]),
CaslModule,
],
@@ -68,7 +82,10 @@ import { AppEnvironmentService } from '@services/app_environments.service';
PluginsService,
PluginsHelper,
AppEnvironmentService,
+ ComponentsService,
+ PageService,
+ EventsService,
],
- controllers: [AppsController, AppUsersController, AppsImportExportController],
+ controllers: [AppsController, AppsControllerV2, AppUsersController, AppsImportExportController],
})
export class AppsModule {}
diff --git a/server/src/services/app_environments.service.ts b/server/src/services/app_environments.service.ts
index 1223ec1f93..461f486e1c 100644
--- a/server/src/services/app_environments.service.ts
+++ b/server/src/services/app_environments.service.ts
@@ -114,6 +114,7 @@ export class AppEnvironmentService {
order: {
createdAt: 'DESC',
},
+ select: ['id', 'name', 'appId'],
});
});
}
diff --git a/server/src/services/app_import_export.service.ts b/server/src/services/app_import_export.service.ts
index 9708bf6283..aed78b38a9 100644
--- a/server/src/services/app_import_export.service.ts
+++ b/server/src/services/app_import_export.service.ts
@@ -11,7 +11,13 @@ import { GroupPermission } from 'src/entities/group_permission.entity';
import { User } from 'src/entities/user.entity';
import { EntityManager } from 'typeorm';
import { DataSourcesService } from './data_sources.service';
-import { dbTransactionWrap, defaultAppEnvironments, catchDbException } from 'src/helpers/utils.helper';
+import {
+ dbTransactionWrap,
+ defaultAppEnvironments,
+ catchDbException,
+ extractMajorVersion,
+ isTooljetVersionWithNormalizedAppDefinitionSchem,
+} from 'src/helpers/utils.helper';
import { AppEnvironmentService } from './app_environments.service';
import { convertAppDefinitionFromSinglePageToMultiPage } from '../../lib/single-page-to-and-from-multipage-definition-conversion';
import { DataSourceScopes, DataSourceTypes } from 'src/helpers/data_source.constants';
@@ -19,6 +25,11 @@ import { Organization } from 'src/entities/organization.entity';
import { DataBaseConstraints } from 'src/helpers/db_constraints.constants';
import { QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity';
import { Plugin } from 'src/entities/plugin.entity';
+import { Page } from 'src/entities/page.entity';
+import { Component } from 'src/entities/component.entity';
+import { Layout } from 'src/entities/layout.entity';
+import { EventHandler, Target } from 'src/entities/event_handler.entity';
+import { v4 as uuid } from 'uuid';
interface AppResourceMappings {
defaultDataSourceIdMapping: Record;
@@ -26,6 +37,8 @@ interface AppResourceMappings {
appVersionMapping: Record;
appEnvironmentMapping: Record;
appDefaultEnvironmentMapping: Record;
+ pagesMapping: Record;
+ componentsMapping: Record;
}
type DefaultDataSourceKind = 'restapi' | 'runjs' | 'runpy' | 'tooljetdb' | 'workflows';
@@ -137,6 +150,37 @@ export class AppImportExportService {
});
}
+ const pages = await manager
+ .createQueryBuilder(Page, 'pages')
+ .where('pages.appVersionId IN(:...versionId)', {
+ versionId: appVersions.map((v) => v.id),
+ })
+ .orderBy('pages.created_at', 'ASC')
+ .getMany();
+
+ const components =
+ pages.length > 0
+ ? await manager
+ .createQueryBuilder(Component, 'components')
+ .leftJoinAndSelect('components.layouts', 'layouts')
+ .where('components.pageId IN(:...pageId)', {
+ pageId: pages.map((v) => v.id),
+ })
+ .orderBy('components.created_at', 'ASC')
+ .getMany()
+ : [];
+
+ const events = await manager
+ .createQueryBuilder(EventHandler, 'event_handlers')
+ .where('event_handlers.appVersionId IN(:...versionId)', {
+ versionId: appVersions.map((v) => v.id),
+ })
+ .orderBy('event_handlers.created_at', 'ASC')
+ .getMany();
+
+ appToExport['components'] = components;
+ appToExport['pages'] = pages;
+ appToExport['events'] = events;
appToExport['dataQueries'] = dataQueries;
appToExport['dataSources'] = dataSources;
appToExport['appVersions'] = appVersions;
@@ -152,7 +196,14 @@ export class AppImportExportService {
});
}
- async import(user: User, appParamsObj: any, appName: string, externalResourceMappings = {}): Promise {
+ async import(
+ user: User,
+ appParamsObj: any,
+ appName: string,
+ externalResourceMappings = {},
+ tooljetVersion = '',
+ cloning = false
+ ): Promise {
if (typeof appParamsObj !== 'object') {
throw new BadRequestException('Invalid params for app import');
}
@@ -167,24 +218,27 @@ export class AppImportExportService {
throw new BadRequestException('Invalid params for app import');
}
- let importedApp: App;
-
const schemaUnifiedAppParams = appParams?.schemaDetails?.multiPages
? appParams
: convertSinglePageSchemaToMultiPageSchema(appParams);
schemaUnifiedAppParams.name = appName;
- await dbTransactionWrap(async (manager) => {
- importedApp = await this.createImportedAppForUser(manager, schemaUnifiedAppParams, user);
- await this.setupImportedAppAssociations(
- manager,
- importedApp,
- schemaUnifiedAppParams,
- user,
- externalResourceMappings
- );
- await this.createAdminGroupPermissions(manager, importedApp);
- });
+ const importedAppTooljetVersion = !cloning && extractMajorVersion(tooljetVersion);
+ const isNormalizedAppDefinitionSchema = cloning
+ ? true
+ : isTooljetVersionWithNormalizedAppDefinitionSchem(importedAppTooljetVersion);
+
+ const importedApp = await this.createImportedAppForUser(this.entityManager, schemaUnifiedAppParams, user);
+
+ await this.setupImportedAppAssociations(
+ this.entityManager,
+ importedApp,
+ schemaUnifiedAppParams,
+ user,
+ externalResourceMappings,
+ isNormalizedAppDefinitionSchema
+ );
+ await this.createAdminGroupPermissions(this.entityManager, importedApp);
// NOTE: App slug updation callback doesn't work while wrapped in transaction
// hence updating slug explicitly
@@ -207,6 +261,7 @@ export class AppImportExportService {
createdAt: new Date(),
updatedAt: new Date(),
});
+
await manager.save(importedApp);
return importedApp;
}, [{ dbConstraint: DataBaseConstraints.APP_NAME_UNIQUE, message: 'This app name is already taken.' }]);
@@ -219,6 +274,9 @@ export class AppImportExportService {
importingAppEnvironments: AppEnvironment[];
importingDataSourceOptions: DataSourceOptions[];
importingDefaultAppEnvironmentId: string;
+ importingPages: Page[];
+ importingComponents: Component[];
+ importingEvents: EventHandler[];
} {
const importingDataSources = appParams?.dataSources || [];
const importingDataQueries = appParams?.dataQueries || [];
@@ -229,6 +287,10 @@ export class AppImportExportService {
(env: { isDefault: any }) => env.isDefault
)?.id;
+ const importingPages = appParams?.pages || [];
+ const importingComponents = appParams?.components || [];
+ const importingEvents = appParams?.events || [];
+
return {
importingDataSources,
importingDataQueries,
@@ -236,6 +298,9 @@ export class AppImportExportService {
importingAppEnvironments,
importingDataSourceOptions,
importingDefaultAppEnvironmentId,
+ importingPages,
+ importingComponents,
+ importingEvents,
};
}
@@ -248,7 +313,8 @@ export class AppImportExportService {
importedApp: App,
appParams: any,
user: User,
- externalResourceMappings: Record
+ externalResourceMappings: Record,
+ isNormalizedAppDefinitionSchema: boolean
) {
// Old version without app version
// Handle exports prior to 0.12.0
@@ -265,8 +331,9 @@ export class AppImportExportService {
appVersionMapping: {},
appEnvironmentMapping: {},
appDefaultEnvironmentMapping: {},
+ pagesMapping: {},
+ componentsMapping: {},
};
-
const {
importingDataSources,
importingDataQueries,
@@ -274,6 +341,9 @@ export class AppImportExportService {
importingAppEnvironments,
importingDataSourceOptions,
importingDefaultAppEnvironmentId,
+ importingPages,
+ importingComponents,
+ importingEvents,
} = this.extractImportDataFromAppParams(appParams);
const { appDefaultEnvironmentMapping, appVersionMapping } = await this.createAppVersionsForImportedApp(
@@ -281,7 +351,8 @@ export class AppImportExportService {
user,
importedApp,
importingAppVersions,
- appResourceMappings
+ appResourceMappings,
+ isNormalizedAppDefinitionSchema
);
appResourceMappings.appDefaultEnvironmentMapping = appDefaultEnvironmentMapping;
appResourceMappings.appVersionMapping = appVersionMapping;
@@ -296,24 +367,201 @@ export class AppImportExportService {
importingDataSources,
importingDataSourceOptions,
importingDataQueries,
- importingDefaultAppEnvironmentId
+ importingDefaultAppEnvironmentId,
+ importingPages,
+ importingComponents,
+ importingEvents
);
- for (const importingAppVersion of importingAppVersions) {
- const updatedDefinition = this.replaceDataQueryIdWithinDefinitions(
- importingAppVersion.definition,
- appResourceMappings.dataQueryMapping
- );
- await manager.update(
- AppVersion,
- { id: appResourceMappings.appVersionMapping[importingAppVersion.id] },
- {
- definition: updatedDefinition,
+ if (!isNormalizedAppDefinitionSchema) {
+ for (const importingAppVersion of importingAppVersions) {
+ const updatedDefinition = this.replaceDataQueryIdWithinDefinitions(
+ importingAppVersion.definition,
+ appResourceMappings.dataQueryMapping
+ );
+
+ let updateHomepageId = null;
+
+ if (updatedDefinition?.pages) {
+ for (const pageId of Object.keys(updatedDefinition?.pages)) {
+ const page = updatedDefinition.pages[pageId];
+
+ const pageEvents = page.events || [];
+ const componentEvents = [];
+
+ const pagePostionIntheList = Object.keys(updatedDefinition?.pages).indexOf(pageId);
+
+ const isHompage = (updatedDefinition['homePageId'] as any) === pageId;
+
+ const pageComponents = page.components;
+
+ const mappedComponents = transformComponentData(
+ pageComponents,
+ componentEvents,
+ appResourceMappings.componentsMapping
+ );
+
+ const componentLayouts = [];
+
+ const newPage = manager.create(Page, {
+ name: page.name,
+ handle: page.handle,
+ appVersionId: appResourceMappings.appVersionMapping[importingAppVersion.id],
+ index: pagePostionIntheList,
+ disabled: page.disabled || false,
+ hidden: page.hidden || false,
+ });
+ const pageCreated = await manager.save(newPage);
+
+ appResourceMappings.pagesMapping[pageId] = pageCreated.id;
+
+ mappedComponents.forEach((component) => {
+ component.page = pageCreated;
+ });
+
+ const savedComponents = await manager.save(Component, mappedComponents);
+
+ for (const componentId in pageComponents) {
+ const componentLayout = pageComponents[componentId]['layouts'];
+
+ if (componentLayout && appResourceMappings.componentsMapping[componentId]) {
+ for (const type in componentLayout) {
+ const layout = componentLayout[type];
+ const newLayout = new Layout();
+ newLayout.type = type;
+ newLayout.top = layout.top;
+ newLayout.left = layout.left;
+ newLayout.width = layout.width;
+ newLayout.height = layout.height;
+ newLayout.componentId = appResourceMappings.componentsMapping[componentId];
+
+ componentLayouts.push(newLayout);
+ }
+ }
+ }
+
+ await manager.save(Layout, componentLayouts);
+
+ //Event handlers
+
+ if (pageEvents.length > 0) {
+ pageEvents.forEach(async (event, index) => {
+ const newEvent = {
+ name: event.eventId,
+ sourceId: pageCreated.id,
+ target: Target.page,
+ event: event,
+ index: pageEvents.index || index,
+ appVersionId: appResourceMappings.appVersionMapping[importingAppVersion.id],
+ };
+
+ await manager.save(EventHandler, newEvent);
+ });
+ }
+
+ componentEvents.forEach((eventObj) => {
+ if (eventObj.event?.length === 0) return;
+
+ eventObj.event.forEach(async (event, index) => {
+ const newEvent = await manager.create(EventHandler, {
+ name: event.eventId,
+ sourceId: appResourceMappings.componentsMapping[eventObj.componentId],
+ target: Target.component,
+ event: event,
+ index: eventObj.index || index,
+ appVersionId: appResourceMappings.appVersionMapping[importingAppVersion.id],
+ });
+
+ await manager.save(EventHandler, newEvent);
+ });
+ });
+
+ savedComponents.forEach(async (component) => {
+ if (component.type === 'Table') {
+ const tableActions = component.properties?.actions?.value || [];
+ const tableColumns = component.properties?.columns?.value || [];
+
+ const tableActionAndColumnEvents = [];
+
+ tableActions.forEach((action) => {
+ const actionEvents = action.events || [];
+
+ actionEvents.forEach((event, index) => {
+ tableActionAndColumnEvents.push({
+ name: event.eventId,
+ sourceId: component.id,
+ target: Target.tableAction,
+ event: { ...event, ref: action.name },
+ index: event.index ?? index,
+ appVersionId: appResourceMappings.appVersionMapping[importingAppVersion.id],
+ });
+ });
+ });
+
+ tableColumns.forEach((column) => {
+ if (column?.columnType !== 'toggle') return;
+ const columnEvents = column.events || [];
+
+ columnEvents.forEach((event, index) => {
+ tableActionAndColumnEvents.push({
+ name: event.eventId,
+ sourceId: component.id,
+ target: Target.tableColumn,
+ event: { ...event, ref: column.name },
+ index: event.index ?? index,
+ appVersionId: appResourceMappings.appVersionMapping[importingAppVersion.id],
+ });
+ });
+ });
+
+ await manager.save(EventHandler, tableActionAndColumnEvents);
+ }
+ });
+
+ if (isHompage) {
+ updateHomepageId = pageCreated.id;
+ }
+ }
}
- );
+
+ await manager.update(
+ AppVersion,
+ { id: appResourceMappings.appVersionMapping[importingAppVersion.id] },
+ {
+ definition: updatedDefinition,
+ homePageId: updateHomepageId,
+ }
+ );
+
+ await this.updateEventActionsForNewVersionWithNewMappingIds(
+ manager,
+ appResourceMappings.appVersionMapping[importingAppVersion.id],
+ appResourceMappings.dataQueryMapping,
+ appResourceMappings.componentsMapping,
+ appResourceMappings.pagesMapping,
+ isNormalizedAppDefinitionSchema
+ );
+ }
+ }
+
+ if (isNormalizedAppDefinitionSchema) {
+ const appVersionIds = Object.values(appResourceMappings.appVersionMapping);
+
+ for (const appVersionId of appVersionIds) {
+ await this.updateEventActionsForNewVersionWithNewMappingIds(
+ manager,
+ appVersionId,
+ appResourceMappings.dataQueryMapping,
+ appResourceMappings.componentsMapping,
+ appResourceMappings.pagesMapping,
+ isNormalizedAppDefinitionSchema
+ );
+ }
}
await this.setEditingVersionAsLatestVersion(manager, appResourceMappings.appVersionMapping, importingAppVersions);
+
+ return appResourceMappings;
}
async setupAppVersionAssociations(
@@ -326,11 +574,17 @@ export class AppImportExportService {
importingDataSources: DataSource[],
importingDataSourceOptions: DataSourceOptions[],
importingDataQueries: DataQuery[],
- importingDefaultAppEnvironmentId: string
+ importingDefaultAppEnvironmentId: string,
+ importingPages: Page[],
+ importingComponents: Component[],
+ importingEvents: EventHandler[]
): Promise {
appResourceMappings = { ...appResourceMappings };
for (const importingAppVersion of importingAppVersions) {
+ let isHomePage = false;
+ let updateHomepageId = null;
+
const { appEnvironmentMapping } = await this.associateAppEnvironmentsToAppVersion(
manager,
user,
@@ -417,18 +671,185 @@ export class AppImportExportService {
appResourceMappings.dataQueryMapping = dataQueryMapping;
}
+ const pagesOfAppVersion = importingPages.filter((page) => page.appVersionId === importingAppVersion.id);
+
+ for (const page of pagesOfAppVersion) {
+ const newPage = manager.create(Page, {
+ name: page.name,
+ handle: page.handle,
+ appVersionId: appResourceMappings.appVersionMapping[importingAppVersion.id],
+ index: page.index,
+ disabled: page.disabled || false,
+ hidden: page.hidden || false,
+ });
+
+ const pageCreated = await manager.save(newPage);
+
+ appResourceMappings.pagesMapping[page.id] = pageCreated.id;
+
+ isHomePage = importingAppVersion.homePageId === page.id;
+
+ if (isHomePage) {
+ updateHomepageId = pageCreated.id;
+ }
+
+ const pageComponents = importingComponents.filter((component) => component.pageId === page.id);
+
+ const newComponentIdsMap = {};
+
+ for (const component of pageComponents) {
+ newComponentIdsMap[component.id] = uuid();
+ }
+
+ for (const component of pageComponents) {
+ let skipComponent = false;
+ const newComponent = new Component();
+
+ let parentId = component.parent ? component.parent : null;
+
+ const isParentTabOrCalendar = isChildOfTabsOrCalendar(component, pageComponents, parentId);
+
+ if (isParentTabOrCalendar) {
+ const childTabId = component.parent.split('-')[component.parent.split('-').length - 1];
+ const _parentId = component?.parent?.split('-').slice(0, -1).join('-');
+ const mappedParentId = newComponentIdsMap[_parentId];
+
+ parentId = `${mappedParentId}-${childTabId}`;
+ } else {
+ if (component.parent && !newComponentIdsMap[parentId]) {
+ skipComponent = true;
+ }
+
+ parentId = newComponentIdsMap[parentId];
+ }
+
+ if (!skipComponent) {
+ newComponent.id = newComponentIdsMap[component.id];
+ newComponent.name = component.name;
+ newComponent.type = component.type;
+ newComponent.properties = component.properties;
+ newComponent.styles = component.styles;
+ newComponent.validation = component.validation;
+ newComponent.parent = component.parent ? parentId : null;
+
+ newComponent.page = pageCreated;
+
+ const savedComponent = await manager.save(newComponent);
+
+ appResourceMappings.componentsMapping[component.id] = savedComponent.id;
+ const componentLayout = component.layouts;
+
+ componentLayout.forEach(async (layout) => {
+ const newLayout = new Layout();
+ newLayout.type = layout.type;
+ newLayout.top = layout.top;
+ newLayout.left = layout.left;
+ newLayout.width = layout.width;
+ newLayout.height = layout.height;
+ newLayout.component = savedComponent;
+
+ await manager.save(newLayout);
+ });
+
+ const componentEvents = importingEvents.filter((event) => event.sourceId === component.id);
+
+ if (componentEvents.length > 0) {
+ componentEvents.forEach(async (componentEvent) => {
+ const newEvent = new EventHandler();
+ newEvent.name = componentEvent.name;
+ newEvent.sourceId = savedComponent.id;
+ newEvent.target = componentEvent.target;
+ newEvent.event = componentEvent.event;
+ newEvent.index = componentEvent.index;
+ newEvent.appVersionId = appResourceMappings.appVersionMapping[importingAppVersion.id];
+
+ await manager.save(EventHandler, newEvent);
+ });
+ }
+ }
+ }
+
+ const pageEvents = importingEvents.filter((event) => event.sourceId === page.id);
+
+ if (pageEvents.length > 0) {
+ pageEvents.forEach(async (pageEvent) => {
+ const newEvent = {
+ name: pageEvent.name,
+ sourceId: pageCreated.id,
+ target: pageEvent.target,
+ event: pageEvent.event,
+ index: pageEvent.index,
+ appVersionId: appResourceMappings.appVersionMapping[importingAppVersion.id],
+ };
+
+ await manager.save(EventHandler, newEvent);
+ });
+ }
+ }
+
const newDataQueries = await manager.find(DataQuery, {
where: { appVersionId: appResourceMappings.appVersionMapping[importingAppVersion.id] },
});
- for (const newQuery of newDataQueries) {
- const newOptions = this.replaceDataQueryOptionsWithNewDataQueryIds(
- newQuery.options,
- appResourceMappings.dataQueryMapping
+ for (const importedDataQuery of importingDataQueriesForAppVersion) {
+ const mappedNewDataQuery = newDataQueries.find(
+ (dq) => dq.id === appResourceMappings.dataQueryMapping[importedDataQuery.id]
);
- newQuery.options = newOptions;
- await manager.save(newQuery);
+
+ if (!mappedNewDataQuery) continue;
+
+ const importingQueryEvents = importingEvents.filter(
+ (event) => event.target === Target.dataQuery && event.sourceId === importedDataQuery.id
+ );
+
+ if (importingQueryEvents.length > 0) {
+ importingQueryEvents.forEach(async (dataQueryEvent) => {
+ const newEvent = {
+ name: dataQueryEvent.name,
+ sourceId: mappedNewDataQuery.id,
+ target: dataQueryEvent.target,
+ event: dataQueryEvent.event,
+ index: dataQueryEvent.index,
+ appVersionId: appResourceMappings.appVersionMapping[importingAppVersion.id],
+ };
+
+ await manager.save(EventHandler, newEvent);
+ });
+ } else {
+ this.replaceDataQueryOptionsWithNewDataQueryIds(
+ mappedNewDataQuery?.options,
+ appResourceMappings.dataQueryMapping
+ );
+ const queryEvents = mappedNewDataQuery?.options?.events || [];
+
+ delete mappedNewDataQuery?.options?.events;
+
+ if (queryEvents.length > 0) {
+ queryEvents.forEach(async (event, index) => {
+ const newEvent = {
+ name: event.eventId,
+ sourceId: mappedNewDataQuery.id,
+ target: Target.dataQuery,
+ event: event,
+ index: queryEvents.index || index,
+ appVersionId: mappedNewDataQuery.appVersionId,
+ };
+
+ await manager.save(EventHandler, newEvent);
+ });
+ }
+ }
+
+ await manager.save(mappedNewDataQuery);
}
+
+ await manager.update(
+ AppVersion,
+ { id: appResourceMappings.appVersionMapping[importingAppVersion.id] },
+ {
+ homePageId: updateHomepageId,
+ }
+ );
}
return appResourceMappings;
@@ -489,6 +910,7 @@ export class AppImportExportService {
dataSourceId: dataSourceForAppVersion.id,
appVersionId: appResourceMappings.appVersionMapping[importingAppVersion.id],
});
+
await manager.save(newQuery);
appResourceMappings.dataQueryMapping[importingQuery.id] = newQuery.id;
}
@@ -718,7 +1140,8 @@ export class AppImportExportService {
user: User,
importedApp: App,
appVersions: AppVersion[],
- appResourceMappings: AppResourceMappings
+ appResourceMappings: AppResourceMappings,
+ isNormalizedAppDefinitionSchema: boolean
) {
appResourceMappings = { ...appResourceMappings };
const { appVersionMapping, appDefaultEnvironmentMapping } = appResourceMappings;
@@ -739,7 +1162,7 @@ export class AppImportExportService {
currentEnvironmentId = organization.appEnvironments.find((env) => env.priority === 1)?.id;
}
- const version = manager.create(AppVersion, {
+ const version = await manager.create(AppVersion, {
appId: importedApp.id,
definition: appVersion.definition,
name: appVersion.name,
@@ -747,6 +1170,30 @@ export class AppImportExportService {
createdAt: new Date(),
updatedAt: new Date(),
});
+
+ if (isNormalizedAppDefinitionSchema) {
+ version.showViewerNavigation = appVersion.showViewerNavigation;
+ version.homePageId = appVersion.homePageId;
+ version.globalSettings = appVersion.globalSettings;
+ } else {
+ version.showViewerNavigation = appVersion.definition.showViewerNavigation || true;
+ version.homePageId = appVersion.definition?.homePageId;
+
+ if (!appVersion.definition?.globalSettings) {
+ version.globalSettings = {
+ hideHeader: false,
+ appInMaintenance: false,
+ canvasMaxWidth: 100,
+ canvasMaxWidthType: '%',
+ canvasMaxHeight: 2400,
+ canvasBackgroundColor: '#edeff5',
+ backgroundFxQuery: '',
+ };
+ } else {
+ version.globalSettings = appVersion.definition?.globalSettings;
+ }
+ }
+
await manager.save(version);
appDefaultEnvironmentMapping[appVersion.id] = appEnvIds;
@@ -1031,8 +1478,24 @@ export class AppImportExportService {
for (const newQuery of newDataQueries) {
const newOptions = this.replaceDataQueryOptionsWithNewDataQueryIds(newQuery.options, dataQueryMapping);
+ const queryEvents = newQuery.options?.events || [];
+ delete newOptions?.events;
+
newQuery.options = newOptions;
await manager.save(newQuery);
+
+ queryEvents.forEach(async (event, index) => {
+ const newEvent = {
+ name: event.eventId,
+ sourceId: newQuery.id,
+ target: Target.dataQuery,
+ event: event,
+ index: queryEvents.index || index,
+ appVersionId: newQuery.appVersionId,
+ };
+
+ await manager.save(EventHandler, newEvent);
+ });
}
await manager.update(
@@ -1045,6 +1508,43 @@ export class AppImportExportService {
replaceTooljetDbTableIds(queryOptions: any, tooljetDatabaseMapping: any) {
return { ...queryOptions, table_id: tooljetDatabaseMapping[queryOptions.table_id]?.id };
}
+
+ async updateEventActionsForNewVersionWithNewMappingIds(
+ manager: EntityManager,
+ versionId: string,
+ oldDataQueryToNewMapping: Record,
+ oldComponentToNewComponentMapping: Record,
+ oldPageToNewPageMapping: Record,
+ isNormalizedAppDefinitionSchema: boolean
+ ) {
+ const allEvents = await manager.find(EventHandler, {
+ where: { appVersionId: versionId },
+ });
+
+ for (const event of allEvents) {
+ const eventDefinition = event.event;
+
+ if (isNormalizedAppDefinitionSchema && eventDefinition?.actionId === 'run-query') {
+ eventDefinition.queryId = oldDataQueryToNewMapping[eventDefinition.queryId];
+ }
+
+ if (eventDefinition?.actionId === 'control-component') {
+ eventDefinition.componentId = oldComponentToNewComponentMapping[eventDefinition.componentId];
+ }
+
+ if (eventDefinition?.actionId === 'switch-page') {
+ eventDefinition.pageId = oldPageToNewPageMapping[eventDefinition.pageId];
+ }
+
+ if (eventDefinition?.actionId == 'show-modal' || eventDefinition?.actionId === 'close-modal') {
+ eventDefinition.modal = oldComponentToNewComponentMapping[eventDefinition.modal];
+ }
+
+ event.event = eventDefinition;
+
+ await manager.save(event);
+ }
+ }
}
function convertSinglePageSchemaToMultiPageSchema(appParams: any) {
@@ -1057,3 +1557,84 @@ function convertSinglePageSchemaToMultiPageSchema(appParams: any) {
};
return appParamsWithMultipageSchema;
}
+
+function transformComponentData(
+ data: object,
+ componentEvents: any[],
+ componentsMapping: Record
+): Component[] {
+ const transformedComponents: Component[] = [];
+
+ const allComponents = Object.keys(data).map((key) => {
+ return {
+ id: key,
+ ...data[key],
+ };
+ });
+
+ for (const componentId in data) {
+ const component = data[componentId];
+ const componentData = component['component'];
+
+ let skipComponent = false;
+ const transformedComponent: Component = new Component();
+
+ let parentId = component.parent ? component.parent : null;
+
+ const isParentTabOrCalendar = isChildOfTabsOrCalendar(component, allComponents, parentId);
+
+ if (isParentTabOrCalendar) {
+ const childTabId = component.parent.split('-')[component.parent.split('-').length - 1];
+ const _parentId = component?.parent?.split('-').slice(0, -1).join('-');
+ const mappedParentId = componentsMapping[_parentId];
+
+ parentId = `${mappedParentId}-${childTabId}`;
+ } else {
+ if (component.parent && !componentsMapping[parentId]) {
+ skipComponent = true;
+ }
+ parentId = componentsMapping[parentId];
+ }
+
+ if (!skipComponent) {
+ transformedComponent.id = uuid();
+ transformedComponent.name = componentData.name;
+ transformedComponent.type = componentData.component;
+ transformedComponent.properties = componentData.definition.properties || {};
+ transformedComponent.styles = componentData.definition.styles || {};
+ transformedComponent.validation = componentData.definition.validation || {};
+ transformedComponent.general = componentData.definition.general || {};
+ transformedComponent.generalStyles = componentData.definition.generalStyles || {};
+ transformedComponent.displayPreferences = componentData.definition.others || {};
+ transformedComponent.parent = component.parent ? parentId : null;
+
+ transformedComponents.push(transformedComponent);
+
+ componentEvents.push({
+ componentId: componentId,
+ event: componentData.definition.events,
+ });
+ componentsMapping[componentId] = transformedComponent.id;
+ }
+ }
+
+ // if (skippedComponents.length) {
+
+ // }
+
+ return transformedComponents;
+}
+
+const isChildOfTabsOrCalendar = (component, allComponents = [], componentParentId = undefined) => {
+ if (componentParentId) {
+ const parentId = component?.parent?.split('-').slice(0, -1).join('-');
+
+ const parentComponent = allComponents.find((comp) => comp.id === parentId);
+
+ if (parentComponent) {
+ return parentComponent.type === 'Tabs' || parentComponent.type === 'Calendar';
+ }
+ }
+
+ return false;
+};
diff --git a/server/src/services/apps.service.ts b/server/src/services/apps.service.ts
index 223e6c0d8e..4f3727e82b 100644
--- a/server/src/services/apps.service.ts
+++ b/server/src/services/apps.service.ts
@@ -22,7 +22,14 @@ import { AppEnvironmentService } from './app_environments.service';
import { decode } from 'js-base64';
import { DataSourceScopes } from 'src/helpers/data_source.constants';
import { DataBaseConstraints } from 'src/helpers/db_constraints.constants';
+import { Page } from 'src/entities/page.entity';
+import { AppVersionUpdateDto } from '@dto/app-version-update.dto';
+import { Layout } from 'src/entities/layout.entity';
+import { Component } from 'src/entities/component.entity';
+import { EventHandler } from 'src/entities/event_handler.entity';
+
+const uuid = require('uuid');
@Injectable()
export class AppsService {
constructor(
@@ -117,7 +124,30 @@ export class AppsService {
);
//create default app version
- await this.createVersion(user, app, 'v1', null, null, manager);
+ const appVersion = await this.createVersion(user, app, 'v1', null, null, manager);
+
+ const defaultHomePage = await manager.save(
+ manager.create(Page, {
+ name: 'Home',
+ handle: 'home',
+ appVersionId: appVersion.id,
+ index: 1,
+ })
+ );
+
+ // Set default values for app version
+ appVersion.showViewerNavigation = true;
+ appVersion.homePageId = defaultHomePage.id;
+ appVersion.globalSettings = {
+ hideHeader: false,
+ appInMaintenance: false,
+ canvasMaxWidth: 100,
+ canvasMaxWidthType: '%',
+ canvasMaxHeight: 2400,
+ canvasBackgroundColor: '#edeff5',
+ backgroundFxQuery: '',
+ };
+ await manager.save(appVersion);
await manager.save(
manager.create(AppUser, {
@@ -345,11 +375,221 @@ export class AppsService {
})
);
- await this.createNewDataSourcesAndQueriesForVersion(manager, appVersion, versionFrom, organizationId);
+ if (versionFrom) {
+ (appVersion.showViewerNavigation = versionFrom.showViewerNavigation),
+ (appVersion.globalSettings = versionFrom.globalSettings),
+ await manager.save(appVersion);
+
+ const oldDataQueryToNewMapping = await this.createNewDataSourcesAndQueriesForVersion(
+ manager,
+ appVersion,
+ versionFrom,
+ organizationId
+ );
+
+ const { oldComponentToNewComponentMapping, oldPageToNewPageMapping } =
+ await this.createNewPagesAndComponentsForVersion(manager, appVersion, versionFrom.id, versionFrom.homePageId);
+
+ await this.updateEventActionsForNewVersionWithNewMappingIds(
+ manager,
+ appVersion.id,
+ oldDataQueryToNewMapping,
+ oldComponentToNewComponentMapping,
+ oldPageToNewPageMapping
+ );
+ }
+
return appVersion;
}, manager);
}
+ async updateEventActionsForNewVersionWithNewMappingIds(
+ manager: EntityManager,
+ versionId: string,
+ oldDataQueryToNewMapping: Record,
+ oldComponentToNewComponentMapping: Record,
+ oldPageToNewPageMapping: Record
+ ) {
+ const allEvents = await manager.find(EventHandler, {
+ where: { appVersionId: versionId },
+ });
+
+ for (const event of allEvents) {
+ const eventDefinition = event.event;
+
+ if (eventDefinition?.actionId === 'run-query') {
+ eventDefinition.queryId = oldDataQueryToNewMapping[eventDefinition.queryId];
+ }
+
+ if (eventDefinition?.actionId === 'control-component') {
+ eventDefinition.componentId = oldComponentToNewComponentMapping[eventDefinition.componentId];
+ }
+
+ if (eventDefinition?.actionId === 'switch-page') {
+ eventDefinition.pageId = oldPageToNewPageMapping[eventDefinition.pageId];
+ }
+
+ if (eventDefinition?.actionId == 'show-modal' || eventDefinition?.actionId === 'close-modal') {
+ eventDefinition.modal = oldComponentToNewComponentMapping[eventDefinition.modal];
+ }
+
+ event.event = eventDefinition;
+
+ await manager.save(event);
+ }
+ }
+
+ async createNewPagesAndComponentsForVersion(
+ manager: EntityManager,
+ appVersion: AppVersion,
+ versionFromId: string,
+ prevHomePagePage: string
+ ) {
+ const pages = await manager
+ .createQueryBuilder(Page, 'page')
+ .leftJoinAndSelect('page.components', 'component')
+ .leftJoinAndSelect('component.layouts', 'layout')
+ .where('page.appVersionId = :appVersionId', { appVersionId: versionFromId })
+ .getMany();
+
+ const allEvents = await manager.find(EventHandler, {
+ where: { appVersionId: versionFromId },
+ });
+
+ let homePageId = prevHomePagePage;
+
+ const newComponents = [];
+ const newComponentLayouts = [];
+ const oldComponentToNewComponentMapping = {};
+ const oldPageToNewPageMapping = {};
+
+ const isChildOfTabsOrCalendar = (component, allComponents = [], componentParentId = undefined) => {
+ if (componentParentId) {
+ const parentId = component?.parent?.split('-').slice(0, -1).join('-');
+
+ const parentComponent = allComponents.find((comp) => comp.id === parentId);
+
+ if (parentComponent) {
+ return parentComponent.type === 'Tabs' || parentComponent.type === 'Calendar';
+ }
+ }
+
+ return false;
+ };
+
+ for (const page of pages) {
+ const savedPage = await manager.save(
+ manager.create(Page, {
+ name: page.name,
+ handle: page.handle,
+ index: page.index,
+ disabled: page.disabled,
+ hidden: page.hidden,
+ appVersionId: appVersion.id,
+ })
+ );
+ oldPageToNewPageMapping[page.id] = savedPage.id;
+ if (page.id === prevHomePagePage) {
+ homePageId = savedPage.id;
+ }
+
+ const pageEvents = allEvents.filter((event) => event.sourceId === page.id);
+
+ pageEvents.forEach(async (event, index) => {
+ const newEvent = new EventHandler();
+
+ newEvent.id = uuid.v4();
+ newEvent.name = event.name;
+ newEvent.sourceId = savedPage.id;
+ newEvent.target = event.target;
+ newEvent.event = event.event;
+ newEvent.index = event.index ?? index;
+ newEvent.appVersionId = appVersion.id;
+
+ await manager.save(newEvent);
+ });
+
+ page.components.forEach(async (component) => {
+ const newComponent = new Component();
+ const componentEvents = allEvents.filter((event) => event.sourceId === component.id);
+
+ newComponent.id = uuid.v4();
+
+ oldComponentToNewComponentMapping[component.id] = newComponent.id;
+
+ newComponent.name = component.name;
+ newComponent.type = component.type;
+ newComponent.pageId = savedPage.id;
+ newComponent.properties = component.properties;
+ newComponent.styles = component.styles;
+ newComponent.validation = component.validation;
+ newComponent.general = component.general;
+ newComponent.generalStyles = component.generalStyles;
+ newComponent.displayPreferences = component.displayPreferences;
+ newComponent.parent = component.parent;
+ newComponent.page = savedPage;
+
+ newComponents.push(newComponent);
+
+ component.layouts.forEach((layout) => {
+ const newLayout = new Layout();
+ newLayout.id = uuid.v4();
+ newLayout.type = layout.type;
+ newLayout.top = layout.top;
+ newLayout.left = layout.left;
+ newLayout.width = layout.width;
+ newLayout.height = layout.height;
+ newLayout.componentId = layout.componentId;
+
+ newLayout.component = newComponent;
+
+ newComponentLayouts.push(newLayout);
+ });
+
+ componentEvents.forEach(async (event, index) => {
+ const newEvent = new EventHandler();
+
+ newEvent.id = uuid.v4();
+ newEvent.name = event.name;
+ newEvent.sourceId = newComponent.id;
+ newEvent.target = event.target;
+ newEvent.event = event.event;
+ newEvent.index = event.index ?? index;
+ newEvent.appVersionId = appVersion.id;
+
+ await manager.save(newEvent);
+ });
+ });
+
+ newComponents.forEach((component) => {
+ let parentId = component.parent ? component.parent : null;
+
+ if (!parentId) return;
+
+ const isParentTabOrCalendar = isChildOfTabsOrCalendar(component, page.components, parentId);
+
+ if (isParentTabOrCalendar) {
+ const childTabId = component.parent.split('-')[component.parent.split('-').length - 1];
+ const _parentId = component?.parent?.split('-').slice(0, -1).join('-');
+ const mappedParentId = oldComponentToNewComponentMapping[_parentId];
+
+ parentId = `${mappedParentId}-${childTabId}`;
+ } else {
+ parentId = oldComponentToNewComponentMapping[parentId];
+ }
+
+ component.parent = parentId;
+ });
+
+ await manager.save(newComponents);
+ await manager.save(newComponentLayouts);
+ }
+
+ await manager.update(AppVersion, { id: appVersion.id }, { homePageId });
+
+ return { oldComponentToNewComponentMapping, oldPageToNewPageMapping };
+ }
+
async deleteVersion(app: App, version: AppVersion): Promise {
if (app.currentVersionId === version.id) {
throw new BadRequestException('You cannot delete a released version');
@@ -399,8 +639,11 @@ export class AppsService {
const dataSources = versionFrom?.dataSources;
const dataSourceMapping = {};
const newDataQueries = [];
+ const allEvents = await manager.find(EventHandler, {
+ where: { appVersionId: versionFrom?.id, target: 'data_query' },
+ });
- if (dataSources?.length) {
+ if (dataSources?.length > 0) {
for (const dataSource of dataSources) {
const dataSourceParams: Partial = {
name: dataSource.name,
@@ -420,64 +663,92 @@ export class AppsService {
dataSourceId: newDataSource.id,
appVersionId: appVersion.id,
};
-
const newQuery = await manager.save(manager.create(DataQuery, dataQueryParams));
+
+ const dataQueryEvents = allEvents.filter((event) => event.sourceId === dataQuery.id);
+
+ dataQueryEvents.forEach(async (event, index) => {
+ const newEvent = new EventHandler();
+
+ newEvent.id = uuid.v4();
+ newEvent.name = event.name;
+ newEvent.sourceId = newQuery.id;
+ newEvent.target = event.target;
+ newEvent.event = event.event;
+ newEvent.index = event.index ?? index;
+ newEvent.appVersionId = appVersion.id;
+
+ await manager.save(newEvent);
+ });
+
oldDataQueryToNewMapping[dataQuery.id] = newQuery.id;
newDataQueries.push(newQuery);
}
}
+ }
- if (globalQueries?.length) {
- for (const globalQuery of globalQueries) {
- const dataQueryParams = {
- name: globalQuery.name,
- options: globalQuery.options,
- dataSourceId: globalQuery.dataSourceId,
- appVersionId: appVersion.id,
- };
+ if (globalQueries?.length > 0) {
+ for (const globalQuery of globalQueries) {
+ const dataQueryParams = {
+ name: globalQuery.name,
+ options: globalQuery.options,
+ dataSourceId: globalQuery.dataSourceId,
+ appVersionId: appVersion.id,
+ };
- const newQuery = await manager.save(manager.create(DataQuery, dataQueryParams));
- oldDataQueryToNewMapping[globalQuery.id] = newQuery.id;
- newDataQueries.push(newQuery);
- }
+ const newQuery = await manager.save(manager.create(DataQuery, dataQueryParams));
+ const dataQueryEvents = allEvents.filter((event) => event.sourceId === globalQuery.id);
+
+ dataQueryEvents.forEach(async (event, index) => {
+ const newEvent = new EventHandler();
+
+ newEvent.id = uuid.v4();
+ newEvent.name = event.name;
+ newEvent.sourceId = newQuery.id;
+ newEvent.target = event.target;
+ newEvent.event = event.event;
+ newEvent.index = event.index ?? index;
+ newEvent.appVersionId = appVersion.id;
+
+ await manager.save(newEvent);
+ });
+ oldDataQueryToNewMapping[globalQuery.id] = newQuery.id;
+ newDataQueries.push(newQuery);
}
+ }
- for (const newQuery of newDataQueries) {
- const newOptions = this.replaceDataQueryOptionsWithNewDataQueryIds(
- newQuery.options,
- oldDataQueryToNewMapping
+ for (const newQuery of newDataQueries) {
+ const newOptions = this.replaceDataQueryOptionsWithNewDataQueryIds(newQuery.options, oldDataQueryToNewMapping);
+ newQuery.options = newOptions;
+
+ await manager.save(newQuery);
+ }
+
+ appVersion.definition = this.replaceDataQueryIdWithinDefinitions(appVersion.definition, oldDataQueryToNewMapping);
+ await manager.save(appVersion);
+
+ for (const appEnvironment of appEnvironments) {
+ for (const dataSource of dataSources) {
+ const dataSourceOption = await manager.findOneOrFail(DataSourceOptions, {
+ where: { dataSourceId: dataSource.id, environmentId: appEnvironment.id },
+ });
+
+ const convertedOptions = this.convertToArrayOfKeyValuePairs(dataSourceOption.options);
+ const newOptions = await this.dataSourcesService.parseOptionsForCreate(convertedOptions, false, manager);
+ await this.setNewCredentialValueFromOldValue(newOptions, convertedOptions, manager);
+
+ await manager.save(
+ manager.create(DataSourceOptions, {
+ options: newOptions,
+ dataSourceId: dataSourceMapping[dataSource.id],
+ environmentId: appEnvironment.id,
+ })
);
- newQuery.options = newOptions;
- await manager.save(newQuery);
- }
-
- appVersion.definition = this.replaceDataQueryIdWithinDefinitions(
- appVersion.definition,
- oldDataQueryToNewMapping
- );
- await manager.save(appVersion);
-
- for (const appEnvironment of appEnvironments) {
- for (const dataSource of dataSources) {
- const dataSourceOption = await manager.findOneOrFail(DataSourceOptions, {
- where: { dataSourceId: dataSource.id, environmentId: appEnvironment.id },
- });
-
- const convertedOptions = this.convertToArrayOfKeyValuePairs(dataSourceOption.options);
- const newOptions = await this.dataSourcesService.parseOptionsForCreate(convertedOptions, false, manager);
- await this.setNewCredentialValueFromOldValue(newOptions, convertedOptions, manager);
-
- await manager.save(
- manager.create(DataSourceOptions, {
- options: newOptions,
- dataSourceId: dataSourceMapping[dataSource.id],
- environmentId: appEnvironment.id,
- })
- );
- }
}
}
}
+
+ return oldDataQueryToNewMapping;
}
private async createEnvironments(appEnvironments: any[], manager: EntityManager, organizationId: string) {
@@ -643,6 +914,31 @@ export class AppsService {
return await this.appVersionsRepository.update(version.id, editableParams);
}
+ async updateAppVersion(version: AppVersion, body: AppVersionUpdateDto) {
+ const editableParams = {};
+
+ const { globalSettings, homePageId } = await this.appVersionsRepository.findOne({
+ where: { id: version.id },
+ });
+
+ if (body?.homePageId && homePageId !== body.homePageId) {
+ editableParams['homePageId'] = body.homePageId;
+ }
+
+ if (body?.globalSettings) {
+ editableParams['globalSettings'] = {
+ ...globalSettings,
+ ...body.globalSettings,
+ };
+ }
+
+ if (typeof body?.showViewerNavigation === 'boolean') {
+ editableParams['showViewerNavigation'] = body.showViewerNavigation;
+ }
+
+ return await this.appVersionsRepository.update(version.id, editableParams);
+ }
+
convertToArrayOfKeyValuePairs(options): Array