From a2a47a1862cc85b52c452d32654de8951f29fb11 Mon Sep 17 00:00:00 2001 From: arpitnath Date: Thu, 3 Aug 2023 03:28:47 +0530 Subject: [PATCH 001/704] tables for `Page`, `Component`, `Layout`,`EventHandler. with repective migrations --- .../1691004576333-CreatePageTable.ts | 54 +++++++++++++++++ .../1691004706564-CreateEventHandlerTable.ts | 56 ++++++++++++++++++ .../1691006886775-UpdateAppVersionEntity.ts | 41 +++++++++++++ .../1691006952074-CreateComponentTable.ts | 59 +++++++++++++++++++ .../1691007037021-CreateLayoutTable.ts | 59 +++++++++++++++++++ server/src/entities/app_version.entity.ts | 19 ++++++ server/src/entities/component.entity.ts | 30 ++++++++++ server/src/entities/event_handler.entity.ts | 29 +++++++++ server/src/entities/layout.entity.ts | 26 ++++++++ server/src/entities/page.entity.ts | 27 +++++++++ server/src/modules/apps/apps.module.ts | 9 +++ 11 files changed, 409 insertions(+) create mode 100644 server/migrations/1691004576333-CreatePageTable.ts create mode 100644 server/migrations/1691004706564-CreateEventHandlerTable.ts create mode 100644 server/migrations/1691006886775-UpdateAppVersionEntity.ts create mode 100644 server/migrations/1691006952074-CreateComponentTable.ts create mode 100644 server/migrations/1691007037021-CreateLayoutTable.ts create mode 100644 server/src/entities/component.entity.ts create mode 100644 server/src/entities/event_handler.entity.ts create mode 100644 server/src/entities/layout.entity.ts create mode 100644 server/src/entities/page.entity.ts diff --git a/server/migrations/1691004576333-CreatePageTable.ts b/server/migrations/1691004576333-CreatePageTable.ts new file mode 100644 index 0000000000..73a42c0c9e --- /dev/null +++ b/server/migrations/1691004576333-CreatePageTable.ts @@ -0,0 +1,54 @@ +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: 'app_version_id', + type: 'uuid', + isNullable: false, + }, + { + name: 'update_id', + type: 'varchar', + isNullable: false, + }, + { + name: 'last_hashed_diff', + type: 'varchar', + isNullable: false, + }, + ], + }) + ); + + // 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..78343cd7e2 --- /dev/null +++ b/server/migrations/1691004706564-CreateEventHandlerTable.ts @@ -0,0 +1,56 @@ +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: 'app_version_id', + type: 'uuid', + isNullable: false, + }, + { + name: 'source_id', + type: 'varchar', + isNullable: false, + }, + { + name: 'target', + type: 'enum', + enum: ['page', 'component', 'data_query'], + default: "'page'", + isNullable: false, + }, + ], + }) + ); + + // 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/1691006886775-UpdateAppVersionEntity.ts b/server/migrations/1691006886775-UpdateAppVersionEntity.ts new file mode 100644 index 0000000000..91dc1162f0 --- /dev/null +++ b/server/migrations/1691006886775-UpdateAppVersionEntity.ts @@ -0,0 +1,41 @@ +import { MigrationInterface, QueryRunner, TableColumn } from 'typeorm'; + +export class UpdateAppVersionEntity1691006886775 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: false, + 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/1691006952074-CreateComponentTable.ts b/server/migrations/1691006952074-CreateComponentTable.ts new file mode 100644 index 0000000000..3aab390d2a --- /dev/null +++ b/server/migrations/1691006952074-CreateComponentTable.ts @@ -0,0 +1,59 @@ +import { MigrationInterface, QueryRunner, Table, TableForeignKey } 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: 'page_id', + type: 'uuid', + isNullable: false, + }, + { + name: 'properties', + type: 'json', + isNullable: true, + }, + { + name: 'styles', + type: 'json', + isNullable: true, + }, + { + name: 'validations', + type: 'json', + isNullable: true, + }, + ], + }) + ); + + // Add foreign key to relate Component with Page + await queryRunner.createForeignKey( + 'components', + new TableForeignKey({ + columnNames: ['page_id'], + referencedColumnNames: ['id'], + referencedTableName: 'pages', + onDelete: 'CASCADE', + }) + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.dropTable('components'); + } +} diff --git a/server/migrations/1691007037021-CreateLayoutTable.ts b/server/migrations/1691007037021-CreateLayoutTable.ts new file mode 100644 index 0000000000..aedd4e84d6 --- /dev/null +++ b/server/migrations/1691007037021-CreateLayoutTable.ts @@ -0,0 +1,59 @@ +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: 'name', + type: 'varchar', + isNullable: false, + }, + { + name: 'type', + type: 'varchar', + isNullable: false, + }, + { + name: 'top', + type: 'integer', + isNullable: false, + }, + { + name: 'left', + type: 'integer', + 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/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..68649090f4 --- /dev/null +++ b/server/src/entities/component.entity.ts @@ -0,0 +1,30 @@ +import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, OneToMany } from 'typeorm'; +import { Page } from './page.entity'; +import { Layout } from './layout.entity'; + +@Entity({ name: 'components' }) +export class Component { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ name: 'name' }) + name: string; + + @Column({ name: 'page_id' }) + PageId: string; + + @Column('simple-json') + properties: any; + + @Column('simple-json') + styles: any; + + @Column('simple-json') + validations: any; + + @ManyToOne(() => Page, (page) => page.components) + 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..944ce9882e --- /dev/null +++ b/server/src/entities/event_handler.entity.ts @@ -0,0 +1,29 @@ +import { Entity, PrimaryGeneratedColumn, Column, ManyToOne } from 'typeorm'; +import { AppVersion } from './app_version.entity'; + +enum Target { + page = 'page', + component = 'component', + dataQuery = 'data_query', +} + +@Entity({ name: 'event_handlers' }) +export class EventHandler { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ name: 'name' }) + name: string; + + @Column({ name: 'app_version_id' }) + AppVersionId: string; + + @Column({ name: 'source_id' }) + sourceId: string; + + @Column({ name: 'target' }) + target: Target; + + @ManyToOne(() => AppVersion, (appVersion) => appVersion.eventHandlers) + appVersion: AppVersion; +} diff --git a/server/src/entities/layout.entity.ts b/server/src/entities/layout.entity.ts new file mode 100644 index 0000000000..c0ee7ac8c6 --- /dev/null +++ b/server/src/entities/layout.entity.ts @@ -0,0 +1,26 @@ +import { Entity, PrimaryGeneratedColumn, Column, ManyToOne } from 'typeorm'; +import { Component } from './component.entity'; + +@Entity({ name: 'layouts' }) +export class Layout { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ name: 'name' }) + name: string; + + @Column({ name: 'type' }) + type: string; + + @Column({ name: 'top' }) + top: number; + + @Column({ name: 'left' }) + left: number; + + @Column({ name: 'component_id' }) + ComponentId: string; + + @ManyToOne(() => Component, (component) => component.layouts) + component: Component; +} diff --git a/server/src/entities/page.entity.ts b/server/src/entities/page.entity.ts new file mode 100644 index 0000000000..0260851136 --- /dev/null +++ b/server/src/entities/page.entity.ts @@ -0,0 +1,27 @@ +import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, OneToMany } 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() + AppVersionId: string; + + @Column() + updateId: string; + + @Column() + lastHashedDiff: string; + + @ManyToOne(() => AppVersion, (appVersion) => appVersion.pages) + appVersion: AppVersion; + + @OneToMany(() => Component, (component) => component.page) + components: Component[]; +} diff --git a/server/src/modules/apps/apps.module.ts b/server/src/modules/apps/apps.module.ts index 400ad7d44b..a197ec5088 100644 --- a/server/src/modules/apps/apps.module.ts +++ b/server/src/modules/apps/apps.module.ts @@ -33,6 +33,11 @@ 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'; + @Module({ imports: [ TypeOrmModule.forFeature([ @@ -52,6 +57,10 @@ import { AppEnvironmentService } from '@services/app_environments.service'; Credential, File, Plugin, + Component, + Page, + EventHandler, + Layout, ]), CaslModule, ], From f1211ef595cd37c469dbfacab5bf67aca760edbb Mon Sep 17 00:00:00 2001 From: arpitnath Date: Sat, 5 Aug 2023 04:12:37 +0530 Subject: [PATCH 002/704] new editor and removed all references --- frontend/src/Editor/Container.jsx | 17 +- frontend/src/Editor/Editor.jsx | 123 +- frontend/src/Editor/EditorFunc.jsx | 1337 +++++++++++++++++ frontend/src/Editor/Header/GlobalSettings.jsx | 9 +- frontend/src/Editor/Header/index.js | 34 +- .../src/Editor/Inspector/EventManager.jsx | 6 +- frontend/src/Editor/Inspector/Inspector.jsx | 2 - .../Components/QueryManagerBody.jsx | 2 - .../src/Editor/QueryManager/QueryManager.jsx | 2 - frontend/src/Editor/QueryPanel/QueryPanel.jsx | 2 - frontend/src/Editor/RealtimeEditor.jsx | 5 +- frontend/src/Editor/Viewer.jsx | 16 +- frontend/src/Editor/index.js | 1 + frontend/src/_helpers/appUtils.js | 57 +- frontend/src/_helpers/utils.js | 7 +- frontend/src/_stores/appDataStore.js | 21 + frontend/src/_stores/dataQueriesStore.js | 11 +- frontend/src/_stores/editorStore.js | 24 + 18 files changed, 1557 insertions(+), 119 deletions(-) create mode 100644 frontend/src/Editor/EditorFunc.jsx diff --git a/frontend/src/Editor/Container.jsx b/frontend/src/Editor/Container.jsx index 325970fbe2..6e573c7e4a 100644 --- a/frontend/src/Editor/Container.jsx +++ b/frontend/src/Editor/Container.jsx @@ -54,8 +54,11 @@ export const Container = ({ backgroundSize: `${gridWidth}px 10px`, }; - // eslint-disable-next-line react-hooks/exhaustive-deps - const components = appDefinition.pages[currentPageId]?.components ?? {}; + const components = useMemo( + () => appDefinition.pages[currentPageId]?.components ?? {}, + // eslint-disable-next-line react-hooks/exhaustive-deps + [appDefinition.pages[currentPageId]] + ); const currentState = useCurrentState(); const { appVersionsId, enableReleasedVersionPopupState, isVersionReleased } = useAppVersionStore( (state) => ({ @@ -87,10 +90,12 @@ export const Container = ({ useHotkeys('meta+shift+z, control+shift+z', () => handleRedo()); useHotkeys( 'meta+v, control+v', - () => { + async () => { if (isContainerFocused && !isVersionReleased) { - navigator.clipboard.readText().then((cliptext) => { + // Check if the clipboard API is available + if (navigator.clipboard && typeof navigator.clipboard.readText === 'function') { try { + const cliptext = await navigator.clipboard.readText(); addComponents( currentPageId, appDefinition, @@ -101,7 +106,9 @@ export const Container = ({ } catch (err) { console.log(err); } - }); + } else { + console.log('Clipboard API is not available in this browser.'); + } } enableReleasedVersionPopupState(); }, diff --git a/frontend/src/Editor/Editor.jsx b/frontend/src/Editor/Editor.jsx index 691f24ba0a..8ba542cc96 100644 --- a/frontend/src/Editor/Editor.jsx +++ b/frontend/src/Editor/Editor.jsx @@ -46,12 +46,13 @@ import { ReleasedVersionError } from './AppVersionsManager/ReleasedVersionError' import { useDataSourcesStore } from '@/_stores/dataSourcesStore'; import { useDataQueriesStore } from '@/_stores/dataQueriesStore'; import { useAppVersionStore } from '@/_stores/appVersionStore'; -import { useEditorStore } from '@/_stores/editorStore'; +import { useEditorState, useEditorStore } from '@/_stores/editorStore'; import { useQueryPanelStore } from '@/_stores/queryPanelStore'; import { useCurrentStateStore, useCurrentState } from '@/_stores/currentStateStore'; import { resetAllStores } from '@/_stores/utils'; import { setCookie } from '@/_helpers/cookie'; import { shallow } from 'zustand/shallow'; +import { useAppDataActions, useAppDataStore } from '@/_stores/appDataStore'; setAutoFreeze(false); enablePatches(); @@ -110,7 +111,7 @@ class EditorComponent extends React.Component { deviceWindowWidth: 450, appDefinition: this.defaultDefinition, apps: [], - queryConfirmationList: [], + // queryConfirmationList: [], isSourceSelected: false, isSaving: false, isUnsavedQueriesAvailable: false, @@ -124,6 +125,8 @@ class EditorComponent extends React.Component { this.autoSave = debounce(this.saveEditingVersion, 3000); this.realtimeSave = debounce(this.appDefinitionChanged, 500); + + this.appActions = useAppDataStore.getState().actions; } setWindowTitle(name) { @@ -145,6 +148,9 @@ class EditorComponent extends React.Component { groups: currentSession.group_permissions?.map((group) => group.group), }; this.setState({ currentUser }); + + this.appActions.updateCurrentUser(userVars); + useCurrentStateStore.getState().actions.setCurrentState({ globals: { ...this.props.currentState.globals, @@ -176,7 +182,7 @@ class EditorComponent extends React.Component { this.getCurrentOrganizationDetails(); this.autoSave(); this.fetchApps(0); - this.fetchApp(this.props.params.pageHandle); + await this.fetchApp(this.props.params.pageHandle); await this.fetchOrgEnvironmentVariables(); this.initComponentVersioning(); this.initRealtimeSave(); @@ -200,6 +206,7 @@ class EditorComponent extends React.Component { handle: this.props.pageHandle, variables: {}, }; + this.appActions.updateState({ appId: this.props.params.id }); useCurrentStateStore.getState().actions.setCurrentState({ globals, page }); } @@ -241,7 +248,7 @@ class EditorComponent extends React.Component { componentDidUpdate(prevProps, prevState) { if (!isEqual(prevState.appDefinition, this.state.appDefinition)) { - computeComponentState(this, this.state.appDefinition.pages[this.state.currentPageId]?.components); + computeComponentState(this.state.appDefinition.pages[this.state.currentPageId]?.components); } if (!isEqual(prevState.editorMarginLeft, this.state.editorMarginLeft)) { @@ -300,11 +307,8 @@ class EditorComponent extends React.Component { // eslint-disable-next-line no-unused-vars appService.setMaintenance(this.state.app.id, newState).then((data) => { - this.setState({ - app: { - ...this.state.app, - is_maintenance_on: newState, - }, + this.appActions.updateState({ + isMaintenanceOn: newState, }); if (newState) { @@ -316,11 +320,12 @@ class EditorComponent extends React.Component { }; fetchApps = (page) => { - appService.getAll(page).then((data) => + appService.getAll(page).then((data) => { + this.appActions.updateState({ apps: data.apps.map((app) => ({ id: app.id, name: app.name, slug: app.slug })) }); this.setState({ apps: data.apps, - }) - ); + }); + }); }; fetchApp = (startingPageHandle) => { @@ -333,6 +338,15 @@ class EditorComponent extends React.Component { const startingPageId = pages.filter((page) => page.handle === startingPageHandle)[0]?.id; const homePageId = startingPageId ?? dataDefinition.homePageId; + this.appActions.updateState({ + slug: data.slug, + isMaintenanceOn: data?.is_maintenance_on, + organizationId: data?.organization_id, + isPublic: data?.is_public, + appName: data?.name, + userId: data?.user_id, + }); + useCurrentStateStore.getState().actions.setCurrentState({ page: { handle: dataDefinition.pages[homePageId]?.handle, @@ -343,6 +357,7 @@ class EditorComponent extends React.Component { }); useAppVersionStore.getState().actions.updateEditingVersion(data.editing_version); useAppVersionStore.getState().actions.updateReleasedVersionId(data.current_version_id); + this.setState( { app: data, @@ -353,7 +368,7 @@ class EditorComponent extends React.Component { }, async () => { - computeComponentState(this, this.state.appDefinition.pages[homePageId]?.components ?? {}).then(async () => { + computeComponentState(this.state.appDefinition.pages[homePageId]?.components ?? {}).then(async () => { this.setWindowTitle(data.name); useEditorStore.getState().actions.setShowComments(!!queryString.parse(this.props.location.search).threadId); @@ -425,7 +440,7 @@ class EditorComponent extends React.Component { this.socket?.send( JSON.stringify({ event: 'events', - data: { message: 'dataSourcesChanged', appId: this.state.appId }, + data: { message: 'dataSourcesChanged', appId: this.state.app?.id }, }) ); } else { @@ -445,7 +460,7 @@ class EditorComponent extends React.Component { this.socket?.send( JSON.stringify({ event: 'events', - data: { message: 'dataQueriesChanged', appId: this.state.appId }, + data: { message: 'dataQueriesChanged', appId: this.state.app?.id }, }) ); } else { @@ -601,10 +616,6 @@ class EditorComponent extends React.Component { this.switchSidebarTab(2); }; - handleSlugChange = (newSlug) => { - this.setState({ slug: newSlug }); - }; - removeComponents = () => { if (!this.props.isVersionReleased && this.state?.selectedComponents?.length > 1) { let newDefinition = cloneDeep(this.state.appDefinition); @@ -695,7 +706,7 @@ class EditorComponent extends React.Component { this.handleAddPatch ); setStateAsync(_self, newDefinition).then(() => { - computeComponentState(_self, _self.state.appDefinition.pages[currentPageId].components); + computeComponentState(_self.state.appDefinition.pages[currentPageId].components); this.setState({ isSaving: true, appDefinitionLocalVersion: uuid() }); this.autoSave(); this.props.ymap?.set('appDef', { @@ -753,17 +764,36 @@ class EditorComponent extends React.Component { return; } - cloneComponents(this, this.appDefinitionChanged, false, true); + cloneComponents( + this.state.selectedComponents, + this.state.appDefinition, + this.state.currentPageId, + this.appDefinitionChanged, + false + ); }; - copyComponents = () => cloneComponents(this, this.appDefinitionChanged, false); + copyComponents = () => + cloneComponents( + this.state.selectedComponents, + this.state.appDefinition, + this.state.currentPageId, + this.appDefinitionChanged, + false + ); cloneComponents = () => { if (this.props.isVersionReleased) { useAppVersionStore.getState().actions.enableReleasedVersionPopupState(); return; } - cloneComponents(this, this.appDefinitionChanged, true); + cloneComponents( + this.state.selectedComponents, + this.state.appDefinition, + this.state.currentPageId, + this.appDefinitionChanged, + true + ); }; decimalToHex = (alpha) => (alpha === 0 ? '00' : Math.round(255 * alpha).toString(16)); @@ -828,7 +858,7 @@ class EditorComponent extends React.Component { this.socket.send( JSON.stringify({ event: 'events', - data: { message: 'versionReleased', appId: this.state.appId }, + data: { message: 'versionReleased', appId: this.state.app?.id }, }) ); } @@ -869,7 +899,7 @@ class EditorComponent extends React.Component { } else if (!isEmpty(this.props?.editingVersion)) { appVersionService .save( - this.state.appId, + this.state.app?.id, this.props.editingVersion?.id, { definition: this.state.appDefinition }, isUserSwitchedVersion @@ -900,7 +930,7 @@ class EditorComponent extends React.Component { }; handleOnComponentOptionChanged = (component, optionName, value) => { - return onComponentOptionChanged(this, component, optionName, value); + return onComponentOptionChanged(component, optionName, value); }; handleOnComponentOptionsChanged = (component, options) => { @@ -923,10 +953,10 @@ class EditorComponent extends React.Component { sideBarDebugger = { error: (data) => { - debuggerActions.error(this, data); + debuggerActions.error(data); }, flush: () => { - debuggerActions.flush(this); + debuggerActions.flush(); }, generateErrorLogs: (errors) => debuggerActions.generateErrorLogs(errors), }; @@ -1359,7 +1389,7 @@ class EditorComponent extends React.Component { const queryParamsString = queryParams.map(([key, value]) => `${key}=${value}`).join('&'); - this.props.navigate(`/${getWorkspaceId()}/apps/${this.state.appId}/${handle}?${queryParamsString}`); + this.props.navigate(`/${getWorkspaceId()}/apps/${this.state.app?.id}/${handle}?${queryParamsString}`); const { globals: existingGlobals } = this.props.currentState; @@ -1390,7 +1420,7 @@ class EditorComponent extends React.Component { }, () => { // Move this callback to zustand - computeComponentState(this, this.state.appDefinition.pages[pageId]?.components ?? {}).then(async () => { + computeComponentState(this.state.appDefinition.pages[pageId]?.components ?? {}).then(async () => { for (const event of events ?? []) { await this.handleEvent(event.eventId, event); } @@ -1452,23 +1482,22 @@ class EditorComponent extends React.Component { currentSidebarTab, selectedComponents = [], appDefinition, - appId, - slug, app, showLeftSidebar, isLoading, zoomLevel, deviceWindowWidth, - apps, - defaultComponentStateComputed, hoveredComponent, - queryConfirmationList, } = this.state; const currentState = this.props?.currentState; const editingVersion = this.props?.editingVersion; const appVersionPreviewLink = editingVersion ? `/applications/${app.id}/versions/${editingVersion.id}/${currentState.page.handle}` : ''; + const appId = app?.id; + + const { queryConfirmationList = [], defaultComponentStateComputed } = this.props; + return (
@@ -1553,7 +1578,6 @@ class EditorComponent extends React.Component { updateOnPageLoadEvents={this.updateOnPageLoadEvents} showHideViewerNavigationControls={this.showHideViewerNavigation} updateOnSortingPages={this.updateOnSortingPages} - apps={apps} setEditorMarginLeft={this.handleEditorMarginLeftChange} /> {!this.props.showComments && ( @@ -1691,12 +1715,17 @@ class EditorComponent extends React.Component { dataQueriesChanged={this.dataQueriesChanged} fetchDataQueries={this.fetchDataQueries} darkMode={this.props.darkMode} - apps={apps} allComponents={appDefinition.pages[this.state.currentPageId]?.components ?? {}} appId={appId} appDefinition={appDefinition} dataSourceModalHandler={this.dataSourceModalHandler} - editorRef={this} + editorRef={{ + appDefinition: this.state.appDefinition, + queryConfirmationList, + updateQueryConfirmationList: this.props.updateQueryConfirmationList, + navigate: this.props.navigate, + currentPageId: this.state.currentPageId, + }} />
@@ -1723,7 +1752,6 @@ class EditorComponent extends React.Component { allComponents={appDefinition.pages[this.state.currentPageId]?.components} key={selectedComponents[0].id} switchSidebarTab={this.switchSidebarTab} - apps={apps} darkMode={this.props.darkMode} appDefinitionLocalVersion={this.state.appDefinitionLocalVersion} pages={this.getPagesWithIds()} @@ -1756,13 +1784,17 @@ class EditorComponent extends React.Component { } const withStore = (Component) => (props) => { - const { showComments, currentLayout } = useEditorStore( + const { showComments, currentLayout, queryConfirmationList, defaultComponentStateComputed, actions } = useEditorStore( (state) => ({ showComments: state?.showComments, currentLayout: state?.currentLayout, + queryConfirmationList: state?.queryConfirmationList, + defaultComponentStateComputed: state?.defaultComponentStateComputed, + actions: state?.actions, }), shallow ); + const { isVersionReleased, editingVersion } = useAppVersionStore( (state) => ({ isVersionReleased: state.isVersionReleased, editingVersion: state.editingVersion }), shallow @@ -1778,6 +1810,9 @@ const withStore = (Component) => (props) => { currentState={currentState} showComments={showComments} currentLayout={currentLayout} + queryConfirmationList={queryConfirmationList} + updateQueryConfirmationList={actions.updateQueryConfirmationList} + defaultComponentStateComputed={defaultComponentStateComputed} /> ); }; diff --git a/frontend/src/Editor/EditorFunc.jsx b/frontend/src/Editor/EditorFunc.jsx new file mode 100644 index 0000000000..af72c4174a --- /dev/null +++ b/frontend/src/Editor/EditorFunc.jsx @@ -0,0 +1,1337 @@ +import React, { useEffect, useLayoutEffect, useRef, useState } from 'react'; +import { appService, authenticationService, appVersionService, orgEnvironmentVariableService } from '@/_services'; +import { DndProvider } from 'react-dnd'; +import { HTML5Backend } from 'react-dnd-html5-backend'; +import _, { defaults, cloneDeep, isEqual, isEmpty, debounce, omit, update } from 'lodash'; +import { Container } from './Container'; +import { EditorKeyHooks } from './EditorKeyHooks'; +import { CustomDragLayer } from './CustomDragLayer'; +import { LeftSidebar } from './LeftSidebar'; +import { componentTypes } from './WidgetManager/components'; +import { Inspector } from './Inspector/Inspector'; +import QueryPanel from './QueryPanel/QueryPanel'; +import { + onComponentOptionChanged, + onComponentOptionsChanged, + onEvent, + onQueryConfirmOrCancel, + runQuery, + computeComponentState, + debuggerActions, + cloneComponents, + removeSelectedComponent, +} from '@/_helpers/appUtils'; +import { Confirm } from './Viewer/Confirm'; +import { Tooltip as ReactTooltip } from 'react-tooltip'; +import CommentNotifications from './CommentNotifications'; +import { WidgetManager } from './WidgetManager'; +import config from 'config'; +import queryString from 'query-string'; +import { toast } from 'react-hot-toast'; +const { produce, enablePatches, setAutoFreeze, applyPatches } = require('immer'); +import { createWebsocketConnection } from '@/_helpers/websocketConnection'; +import RealtimeCursors from '@/Editor/RealtimeCursors'; +import { initEditorWalkThrough } from '@/_helpers/createWalkThrough'; +import { EditorContextWrapper } from './Context/EditorContextWrapper'; +import Selecto from 'react-selecto'; +import { withTranslation } from 'react-i18next'; +import { v4 as uuid } from 'uuid'; +import Skeleton from 'react-loading-skeleton'; +import EditorHeader from './Header'; +import { getWorkspaceId } from '@/_helpers/utils'; +import '@/_styles/editor/react-select-search.scss'; +import { withRouter } from '@/_hoc/withRouter'; +import { ReleasedVersionError } from './AppVersionsManager/ReleasedVersionError'; +import { useDataSourcesStore } from '@/_stores/dataSourcesStore'; +import { useDataQueries, useDataQueriesStore } from '@/_stores/dataQueriesStore'; +import { useAppVersionStore } from '@/_stores/appVersionStore'; +import { useQueryPanelStore } from '@/_stores/queryPanelStore'; +import { useCurrentStateStore, useCurrentState } from '@/_stores/currentStateStore'; +import { resetAllStores } from '@/_stores/utils'; +import { setCookie } from '@/_helpers/cookie'; +import { shallow } from 'zustand/shallow'; +import { useEditorActions, useEditorState, useEditorStore } from '@/_stores/editorStore'; +import { useAppDataActions, useAppDataStore, useAppInfo } from '@/_stores/appDataStore'; +import { useMounted } from '@/_hooks/use-mount'; + +setAutoFreeze(false); +enablePatches(); + +function setWindowTitle(name) { + document.title = name ? `${name} - Tooljet` : `My App - Tooljet`; +} + +const decimalToHex = (alpha) => (alpha === 0 ? '00' : Math.round(255 * alpha).toString(16)); + +const defaultDefinition = (darkMode) => { + const defaultPageId = uuid(); + return { + showViewerNavigation: true, + homePageId: defaultPageId, + pages: { + [defaultPageId]: { + components: {}, + handle: 'home', + name: 'Home', + }, + }, + globalSettings: { + hideHeader: false, + appInMaintenance: false, + canvasMaxWidth: 1292, + canvasMaxWidthType: 'px', + canvasMaxHeight: 2400, + canvasBackgroundColor: darkMode ? '#2f3c4c' : '#edeff5', + backgroundFxQuery: '', + }, + }; +}; + +const EditorComponent = (props) => { + const { socket } = createWebsocketConnection(props?.params?.id); + const mounted = useMounted(); + + const { updateState } = useAppDataActions(); + const { updateEditorState, updateQueryConfirmationList } = useEditorActions(); + const { + noOfVersionsSupported, + appDefinition, + selectedComponents, + currentLayout, + canUndo, + canRedo, + isSaving, + saveError, + scrollOptions, + currentSidebarTab, + isLoading, + defaultComponentStateComputed, + currentVersion, + showLeftSidebar, + queryConfirmationList, + } = useEditorState(); + + const dataQueries = useDataQueries(); + + const { isMaintenanceOn, appId, app, currentUser, currentVersionId } = useAppInfo(); + + const [currentVersionChanges, setCurrentVersionChanges] = useState({}); + const [currentPageId, setCurrentPageId] = useState(null); + const [zoomLevel, setZoomLevel] = useState(1); + const [isQueryPaneDragging, setIsQueryPaneDragging] = useState(false); + const [isQueryPaneExpanded, setIsQueryPaneExpanded] = useState(false); + const [selectionInProgress, setSelectionInProgress] = useState(false); + const [hoveredComponent, setHoveredComponent] = useState(null); + const [editorMarginLeft, setEditorMarginLeft] = useState(0); + + const [isDragging, setIsDragging] = useState(false); + + // refs + const canvasContainerRef = useRef(null); + const dataSourceModalRef = useRef(null); + const selectionDragRef = useRef(null); + const selectionRef = useRef(null); + + useLayoutEffect(() => { + resetAllStores(); + }, []); + + useEffect(() => { + updateState({ isLoading: true }); + // 1. Get the current session and current user from the authentication service + const currentSession = authenticationService.currentSessionValue; + const currentUser = currentSession?.current_user; + + // 2. Subscribe to changes in the current session using RxJS observable pattern + const subscription = authenticationService.currentSession.subscribe((currentSession) => { + // 3. Check if the current user and group permissions are available + if (currentUser && currentSession?.group_permissions) { + // 4. Prepare user details in a format suitable for the application + const userVars = { + email: currentUser.email, + firstName: currentUser.first_name, + lastName: currentUser.last_name, + groups: currentSession.group_permissions?.map((group) => group.group), + }; + + const appUserDetails = { + ...currentUser, + current_organization_id: currentSession.current_organization_id, + }; + + updateState({ + currentUser: appUserDetails, + }); + + useCurrentStateStore.getState().actions.setCurrentState({ + globals: { + ...props.currentState.globals, + currentUser: userVars, + }, + }); + } + }); + + $componentDidMount(); + + // 6. Unsubscribe from the observable when the component is unmounted + return () => { + document.title = 'Tooljet - Dashboard'; + socket && socket?.close(); + subscription.unsubscribe(); + if (config.ENABLE_MULTIPLAYER_EDITING) props?.provider?.disconnect(); + useEditorStore.getState().actions.setIsEditorActive(false); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + // Ref to store the previous appDefinition for comparison + const prevAppDefinition = useRef(appDefinition); + + useEffect(() => { + if (currentUser?.current_organization_id) { + fetchGlobalDataSources(); + } + }, [JSON.stringify(currentUser?.current_organization_id)]); + + // Handle appDefinition updates + useEffect(() => { + const didAppDefinitionChanged = !_.isEqual(appDefinition, prevAppDefinition.current); + + console.log('---arpit-- appDefinitionChanged', { dataQueries, didAppDefinitionChanged, appDefinition }); + if (mounted && didAppDefinitionChanged && currentPageId) { + console.log('---arpit-- appDefinitionChanged [mounted=true]', { appDefinition }); + const components = appDefinition?.pages[currentPageId]?.components || {}; + + computeComponentState(components); + + if (isSaving) { + autoSave(); + } + } + prevAppDefinition.current = appDefinition; + }, [JSON.stringify({ appDefinition, currentPageId, dataQueries })]); + + const editorRef = { + appDefinition: appDefinition, + queryConfirmationList: queryConfirmationList, + updateQueryConfirmationList: updateQueryConfirmationList, + navigate: props.navigate, + }; + + const handleMessage = (event) => { + const { data } = event; + + if (data?.type === 'redirectTo') { + const redirectCookie = data?.payload['redirectPath']; + setCookie('redirectPath', redirectCookie, 1); + } + }; + + const fetchApps = async (page) => { + const { apps } = await appService.getAll(page); + + updateState({ apps: apps.map((app) => ({ id: app.id, name: app.name, slug: app.slug })) }); + }; + + const fetchOrgEnvironmentVariables = () => { + orgEnvironmentVariableService.getVariables().then((data) => { + const client_variables = {}; + const server_variables = {}; + data.variables.map((variable) => { + if (variable.variable_type === 'server') { + server_variables[variable.variable_name] = 'HiddenEnvironmentVariable'; + } else { + client_variables[variable.variable_name] = variable.value; + } + }); + + useCurrentStateStore.getState().actions.setCurrentState({ + server: server_variables, + client: client_variables, + }); + }); + }; + + // 1. When we receive an undoable action – we can always undo but cannot redo anymore. + // 2. Whenever you perform an undo – you can always redo and keep doing undo as long as we have a patch for it. + // 3. Whenever you redo – you can always undo and keep doing redo as long as we have a patch for it. + const initComponentVersioning = () => { + const currentVersion = { + [currentPageId]: -1, + }; + setCurrentVersionChanges({}); + updateEditorState({ + canUndo: false, + canRedo: false, + currentVersion, + }); + }; + + /** + * When a new update is received over-the-websocket connection + * the useEffect in Container.jsx is triggered, but already appDef had been updated + * to avoid ymap observe going into a infinite loop a check is added where if the + * current appDef is equal to the newAppDef then we do not trigger a realtimeSave + */ + const initRealtimeSave = () => { + if (!config.ENABLE_MULTIPLAYER_EDITING) return null; + + props.ymap?.observe(() => { + if (!isEqual(props.editingVersion?.id, props.ymap?.get('appDef').editingVersionId)) return; + if (isEqual(appDefinition, props.ymap?.get('appDef').newDefinition)) return; + + realtimeSave(props.ymap?.get('appDef').newDefinition, { skipAutoSave: true, skipYmapUpdate: true }); + }); + }; + + const initEventListeners = () => { + socket?.addEventListener('message', (event) => { + const data = event.data.replace(/^"(.+(?="$))"$/, '$1'); + if (data === 'versionReleased') fetchApp(); + else if (data === 'dataQueriesChanged') { + fetchDataQueries(props.editingVersion?.id); + } else if (data === 'dataSourcesChanged') { + fetchDataSources(props.editingVersion?.id); + } + }); + }; + + const $componentDidMount = async () => { + console.log('---arpit-- componentDidMounted effect', { appDefinition, currentPageId }); + window.addEventListener('message', handleMessage); + // autoSave(); + + await fetchApps(0); + await fetchApp(props.params.pageHandle); + await fetchOrgEnvironmentVariables(); + initComponentVersioning(); + initRealtimeSave(); + initEventListeners(); + updateEditorState({ + currentSidebarTab: 2, + selectedComponents: [], + scrollOptions: { + container: canvasContainerRef.current, + throttleTime: 30, + threshold: 0, + }, + }); + + const globals = { + ...props.currentState.globals, + theme: { name: props?.darkMode ? 'dark' : 'light' }, + urlparams: JSON.parse(JSON.stringify(queryString.parse(props.location.search))), + }; + const page = { + ...props?.currentState?.page, + handle: props?.pageHandle, + variables: {}, + }; + updateState({ appId: props?.params?.id }); + useCurrentStateStore.getState().actions.setCurrentState({ globals, page }); + }; + + const fetchDataQueries = async (id, selectFirstQuery = false, runQueriesOnAppLoad = false) => { + await useDataQueriesStore.getState().actions.fetchDataQueries(id, selectFirstQuery, runQueriesOnAppLoad, editorRef); + }; + + const fetchDataSources = (id) => { + useDataSourcesStore.getState().actions.fetchDataSources(id); + }; + + const fetchGlobalDataSources = () => { + const { current_organization_id: organizationId } = currentUser; + useDataSourcesStore.getState().actions.fetchGlobalDataSources(organizationId); + }; + + const onVersionDelete = () => { + fetchApp(props.params.pageHandle); + }; + + const toggleAppMaintenance = () => { + const newState = !isMaintenanceOn; + + appService.setMaintenance(appId, newState).then(() => { + updateState({ + isMaintenanceOn: newState, + }); + + if (newState) { + toast.success('Application is on maintenance.'); + } else { + toast.success('Application maintenance is completed'); + } + }); + }; + + const dataSourcesChanged = () => { + if (socket instanceof WebSocket && socket?.readyState === WebSocket.OPEN) { + socket?.send( + JSON.stringify({ + event: 'events', + data: { message: 'dataSourcesChanged', appId: appId }, + }) + ); + } else { + fetchDataSources(props.editingVersion?.id); + } + }; + + const globalDataSourcesChanged = () => { + fetchGlobalDataSources(); + }; + + const dataQueriesChanged = () => { + if (socket instanceof WebSocket && socket?.readyState === WebSocket.OPEN) { + socket?.send( + JSON.stringify({ + event: 'events', + data: { message: 'dataQueriesChanged', appId: appId }, + }) + ); + } else { + fetchDataQueries(props.editingVersion?.id); + } + }; + + const switchSidebarTab = (tabIndex) => { + updateEditorState({ + currentSidebarTab: tabIndex, + }); + }; + + const handleInspectorView = () => { + switchSidebarTab(2); + }; + + const onNameChanged = (newName) => { + updateState({ appName: newName }); + setWindowTitle(newName); + }; + + const onZoomChanged = (zoom) => { + setZoomLevel(zoom); + }; + + const [canvasWidth, setCanvasWidth] = useState(1092); + + const getCanvasWidth = () => { + const canvasBoundingRect = document.getElementsByClassName('canvas-area')[0]?.getBoundingClientRect(); + + const _canvasWidth = canvasBoundingRect?.width; + + if (_canvasWidth) { + setCanvasWidth(_canvasWidth); + } + }; + + const computeCanvasContainerHeight = () => { + // 45 = (height of header) + // 85 = (the height of the query panel header when minimised) + (height of header) + return `calc(${100}% - ${Math.max(useQueryPanelStore.getState().queryPanelHeight + 45, 85)}px)`; + }; + + const handleQueryPaneDragging = (bool) => setIsQueryPaneDragging(bool); + const handleQueryPaneExpanding = (bool) => setIsQueryPaneExpanded(bool); + + //! Needs attention + const handleOnComponentOptionChanged = (component, optionName, value) => { + return onComponentOptionChanged(component, optionName, value); + }; + + const handleOnComponentOptionsChanged = (component, options) => { + return onComponentOptionsChanged(component, options); + }; + + const handleComponentClick = (id, component) => { + updateEditorState({ + selectedComponent: { id, component }, + }); + switchSidebarTab(1); + }; + + const handleComponentHover = (id) => { + if (selectionInProgress) return; + setHoveredComponent(id); + }; + + const sideBarDebugger = { + error: (data) => { + debuggerActions.error(data); + }, + flush: () => { + debuggerActions.flush(); + }, + generateErrorLogs: (errors) => debuggerActions.generateErrorLogs(errors), + }; + + const changeDarkMode = (newMode) => { + useCurrentStateStore.getState().actions.setCurrentState({ + globals: { + ...props.currentState.globals, + theme: { name: newMode ? 'dark' : 'light' }, + }, + }); + props.switchDarkMode(newMode); + }; + + //! Needs attention + const handleEvent = (eventName, options) => onEvent(editorRef, eventName, options, 'edit'); + + const handleRunQuery = (queryId, queryName) => runQuery(editorRef, queryId, queryName); + + const dataSourceModalHandler = () => { + dataSourceModalRef.current.dataSourceModalToggleStateHandler(); + }; + + const onAreaSelectionStart = (e) => { + const isMultiSelect = e.inputEvent.shiftKey || selectedComponents.length > 0; + setSelectionInProgress(true); + const prevSelectedComponents = [...selectedComponents]; + updateEditorState({ + selectedComponents: [...(isMultiSelect ? prevSelectedComponents : [])], + }); + }; + + const onAreaSelection = (e) => { + e.added.forEach((el) => { + el.classList.add('resizer-select'); + }); + if (selectionInProgress) { + e.removed.forEach((el) => { + el.classList.remove('resizer-select'); + }); + } + }; + + const onAreaSelectionEnd = (e) => { + setSelectionInProgress(false); + e.selected.forEach((el, index) => { + const id = el.getAttribute('widgetid'); + const component = appDefinition?.pages[currentPageId].components[id].component; + const isMultiSelect = e.inputEvent.shiftKey || (!e.isClick && index != 0); + setSelectedComponent(id, component, isMultiSelect); + }); + }; + + const setSelectedComponent = (id, component, multiSelect = false) => { + if (selectedComponents.length === 0 || !multiSelect) { + switchSidebarTab(1); + } else { + switchSidebarTab(2); + } + + const isAlreadySelected = selectedComponents.find((component) => component.id === id); + + if (!isAlreadySelected) { + const prevSelectedComponents = [...selectedComponents]; + updateEditorState({ + selectedComponents: [...(multiSelect ? prevSelectedComponents : []), { id, component }], + }); + } + }; + + const onVersionRelease = (versionId) => { + useAppVersionStore.getState().actions.updateReleasedVersionId(versionId); + + updateState({ + currentVersionId: versionId, + }); + + socket.send( + JSON.stringify({ + event: 'events', + data: { message: 'versionReleased', appId: appId }, + }) + ); + }; + + const computeCanvasBackgroundColor = () => { + //!Global settings needs to be out + const { canvasBackgroundColor } = appDefinition?.globalSettings ?? '#edeff5'; + if (['#2f3c4c', '#edeff5'].includes(canvasBackgroundColor)) { + return props.darkMode ? '#2f3c4c' : '#edeff5'; + } + return canvasBackgroundColor; + }; + + const onAreaSelectionDragStart = (e) => { + if (e.inputEvent.target.getAttribute('id') !== 'real-canvas') { + selectionDragRef.current = true; + } else { + selectionDragRef.current = false; + } + }; + + const onAreaSelectionDrag = (e) => { + if (selectionDragRef.current) { + e.stop(); + selectionInProgress && setSelectionInProgress(false); + } + }; + + const onAreaSelectionDragEnd = () => { + selectionDragRef.current = false; + selectionInProgress && setSelectionInProgress(false); + }; + + const getPagesWithIds = () => { + //! Needs attention + return Object.entries(appDefinition?.pages).map(([id, page]) => ({ ...page, id })); + }; + + const handleEditorMarginLeftChange = (value) => setEditorMarginLeft(value); + + const globalSettingsChanged = (key, value) => { + if (value?.[1]?.a == undefined) appDefinition.globalSettings[key] = value; + else { + const hexCode = `${value?.[0]}${decimalToHex(value?.[1]?.a)}`; + appDefinition.globalSettings[key] = hexCode; + } + + updateEditorState({ + isSaving: true, + appDefinition, + }); + + props.ymap?.set('appDef', { + newDefinition: appDefinition, + editingVersionId: props.editingVersion?.id, + }); + // autoSave(); + }; + + //!-------- + const fetchApp = async (startingPageHandle) => { + const _appId = props?.params?.id; + + const callBack = async (data) => { + useAppVersionStore.getState().actions.updateEditingVersion(data.editing_version); + useAppVersionStore.getState().actions.updateReleasedVersionId(data.current_version_id); + await fetchDataSources(data.editing_version?.id); + await fetchDataQueries(data.editing_version?.id, true, true); + + // let dataDefinition = defaults(data.definition, defaultDefinition(props.darkMode)); + let dataDefinition = data.definition ?? defaults(data.definition, defaultDefinition(props.darkMode)); + console.log('---arpit-- fetching app data', { startingPageHandle, data, dataDefinition }); + + const pages = Object.entries(dataDefinition.pages).map(([pageId, page]) => ({ id: pageId, ...page })); + const startingPageId = pages.filter((page) => page.handle === startingPageHandle)[0]?.id; + const homePageId = startingPageId || dataDefinition.homePageId; + + setCurrentPageId(homePageId); + + updateState({ + app: data, + slug: data.slug, + isMaintenanceOn: data?.is_maintenance_on, + organizationId: data?.organization_id, + isPublic: data?.is_public, + appName: data?.name, + userId: data?.user_id, + appId: data?.id, + }); + + useCurrentStateStore.getState().actions.setCurrentState({ + page: { + handle: dataDefinition.pages[homePageId]?.handle, + name: dataDefinition.pages[homePageId]?.name, + id: homePageId, + variables: {}, + }, + }); + + updateEditorState({ + isLoading: false, + appDefinition: dataDefinition, + }); + + appDefinitionChanged(dataDefinition, { + skipAutoSave: true, + skipYmapUpdate: true, + }); + + for (const event of dataDefinition.pages[homePageId]?.events ?? []) { + await handleEvent(event.eventId, event); + } + getCanvasWidth(); + }; + + updateState({ + isLoading: true, + }); + + await appService + .getApp(_appId) + .then(callBack) + .finally(() => initEditorWalkThrough()); + }; + + // !-------- + const setAppDefinitionFromVersion = (version, shouldWeEditVersion = true) => { + if (version?.id !== props.editingVersion?.id) { + appDefinitionChanged(defaults(version.definition, defaultDefinition(props.darkMode)), { + skipAutoSave: true, + skipYmapUpdate: true, + versionChanged: true, + }); + if (version?.id === currentVersionId) { + updateEditorState({ + canUndo: false, + canRedo: false, + }); + } + useAppVersionStore.getState().actions.updateEditingVersion(version); + + updateEditorState({ + isSaving: false, + }); + + shouldWeEditVersion && saveEditingVersion(true); + fetchDataSources(props.editingVersion?.id); + fetchDataQueries(props.editingVersion?.id, true); + initComponentVersioning(); + } + }; + + const appDefinitionChanged = (newDefinition, opts = {}) => { + console.log('--arpit | appDefinitionChanged func()'); + if (_.isEqual(appDefinition, newDefinition)) return; + if (config.ENABLE_MULTIPLAYER_EDITING && !opts.skipYmapUpdate) { + props.ymap?.set('appDef', { + newDefinition, + editingVersionId: props.editingVersion?.id, + }); + } + + if (opts?.versionChanged) { + setCurrentPageId(newDefinition.homePageId); + + return new Promise((resolve) => { + updateEditorState({ + isSaving: true, + appDefinition: newDefinition, + // appDefinitionLocalVersion: uuid(), + }); + + // if (!opts.skipAutoSave) autoSave(); + + resolve(); + }); + } + + // Create a new copy of appDefinition with lodash's cloneDeep + const updatedAppDefinition = _.cloneDeep(appDefinition); + const currentPageComponents = newDefinition.pages[currentPageId]?.components; + + if (!updatedAppDefinition.pages[currentPageId]) { + updatedAppDefinition.pages[currentPageId] = {}; // Create a new object for the page if it doesn't exist + } + + updatedAppDefinition.pages[currentPageId].components = currentPageComponents || {}; + + // Call handleAddPatch with the list of changes (if needed) + handleAddPatch(); + + // Update the editor state with the new appDefinition + updateEditorState({ + isSaving: true, + appDefinition: updatedAppDefinition, + // appDefinitionLocalVersion: uuid(), + }); + + // if (!opts.skipAutoSave) autoSave(); + }; + + const saveEditingVersion = (isUserSwitchedVersion = false) => { + if (props.isVersionReleased && !isUserSwitchedVersion) { + updateEditorState({ + isSaving: false, + }); + } else if (!isEmpty(props?.editingVersion)) { + appVersionService + .save(appId, props.editingVersion?.id, { definition: appDefinition }, isUserSwitchedVersion) + .then(() => { + const _editingVersion = { + ...props.editingVersion, + ...{ definition: appDefinition }, + }; + useAppVersionStore.getState().actions.updateEditingVersion(_editingVersion); + updateEditorState({ + saveError: false, + isSaving: false, + }); + }) + .catch(() => { + updateEditorState({ + saveError: true, + isSaving: false, + }); + toast.error('App could not save.'); + }); + } + + updateEditorState({ + saveError: false, + isSaving: false, + }); + }; + + const realtimeSave = debounce(appDefinitionChanged, 500); + const autoSave = debounce(saveEditingVersion, 3000); + + const handleAddPatch = (patches, inversePatches) => { + if (isEmpty(patches) && isEmpty(inversePatches)) return; + if (isEqual(patches, inversePatches)) return; + + const currentPage = currentPageId; + const _currentVersion = currentVersion[currentPage] ?? -1; + + let _currentVersionChanges = {}; + + _currentVersionChanges[currentPage] = currentVersionChanges[currentPage] ?? {}; + + _currentVersionChanges[currentPage][_currentVersion] = { + redo: patches, + undo: inversePatches, + }; + + setCurrentVersionChanges(_currentVersionChanges); + + const _canUndo = _currentVersionChanges[currentPage].hasOwnProperty(_currentVersion); + const _canRedo = _currentVersionChanges[currentPage].hasOwnProperty(_currentVersion + 1); + + _currentVersion[currentPage] = currentVersion + 1; + + delete _currentVersionChanges[currentPage][_currentVersion + 1]; + delete _currentVersionChanges[currentPage][_currentVersion - noOfVersionsSupported]; + + setCurrentVersionChanges(_currentVersionChanges); + + updateEditorState({ + canUndo: _canUndo, + canRedo: _canRedo, + currentVersion: _currentVersion, + }); + }; + + const handleUndo = () => { + if (canUndo) { + let _currentVersion = currentVersion[currentPageId]; + + const _appDefinition = applyPatches(appDefinition, currentVersionChanges[currentPageId][currentVersion - 1].undo); + + const _canUndo = currentVersionChanges[currentPageId].hasOwnProperty(currentVersion - 1); + const _canRedo = true; + _currentVersion[currentPageId] = _currentVersion - 1; + + if (!_appDefinition) return; + + updateEditorState({ + appDefinition: _appDefinition, + canUndo: _canUndo, + canRedo: _canRedo, + currentVersion: _currentVersion, + isSaving: true, + }); + + props.ymap?.set('appDef', { + newDefinition: appDefinition, + editingVersionId: props.editingVersion?.id, + }); + + // autoSave(); + } + }; + + const handleRedo = () => { + if (canRedo) { + let _currentVersion = currentVersion[currentPageId]; + + const _appDefinition = applyPatches(appDefinition, currentVersionChanges[currentPageId][currentVersion].redo); + + const _canUndo = true; + const _canRedo = currentVersionChanges[currentPageId].hasOwnProperty(currentVersion + 1); + _currentVersion[currentPageId] = currentVersion + 1; + + if (!_appDefinition) return; + + updateEditorState({ + appDefinition: _appDefinition, + canUndo: _canUndo, + canRedo: _canRedo, + currentVersion: _currentVersion, + isSaving: true, + }); + + props.ymap?.set('appDef', { + newDefinition: appDefinition, + editingVersionId: props.editingVersion?.id, + }); + + // autoSave(); + } + }; + + const componentDefinitionChanged = (componentDefinition, props) => { + console.log('---arpit [componentDefinitionChanged]', { props, componentDefinition }); + if (props?.isVersionReleased) { + useAppVersionStore.getState().actions.enableReleasedVersionPopupState(); + return; + } + + if (appDefinition?.pages[currentPageId]?.components[componentDefinition.id]) { + // Create a new copy of appDefinition with lodash's cloneDeep + const updatedAppDefinition = _.cloneDeep(appDefinition); + + // Update the component definition in the copy + updatedAppDefinition.pages[currentPageId].components[componentDefinition.id].component = + componentDefinition.component; + + // Call handleAddPatch with the list of changes (if needed) + handleAddPatch(); + + // Update the editor state with the new appDefinition + updateEditorState({ + isSaving: true, + appDefinition: updatedAppDefinition, + // appDefinitionLocalVersion: uuid() + }); + + // Other actions can be performed here if needed, like autoSave, ymap, etc. + computeComponentState(updatedAppDefinition.pages[currentPageId]?.components); + // autoSave(); + props.ymap?.set('appDef', { + newDefinition: updatedAppDefinition, + editingVersionId: props.editingVersion?.id, + }); + } + }; + + const removeComponent = (component) => { + if (!props.isVersionReleased) { + let newDefinition = cloneDeep(appDefinition); + // Delete child components when parent is deleted + + let childComponents = []; + + if (newDefinition.pages[currentPageId].components?.[component.id].component.component === 'Tabs') { + childComponents = Object.keys(newDefinition.pages[currentPageId].components).filter((key) => + newDefinition.pages[currentPageId].components[key].parent?.startsWith(component.id) + ); + } else { + childComponents = Object.keys(newDefinition.pages[currentPageId].components).filter( + (key) => newDefinition.pages[currentPageId].components[key].parent === component.id + ); + } + + childComponents.forEach((componentId) => { + delete newDefinition.pages[currentPageId].components[componentId]; + }); + + delete newDefinition.pages[currentPageId].components[component.id]; + const platform = navigator?.userAgentData?.platform || navigator?.platform || 'unknown'; + if (platform.toLowerCase().indexOf('mac') > -1) { + toast('Component deleted! (⌘ + Z to undo)', { + icon: '🗑️', + }); + } else { + toast('Component deleted! (ctrl + Z to undo)', { + icon: '🗑️', + }); + } + appDefinitionChanged(newDefinition, { + skipAutoSave: props.isVersionReleased, + }); + handleInspectorView(); + } else { + useAppVersionStore.getState().actions.enableReleasedVersionPopupState(); + } + }; + + const moveComponents = (direction) => { + const gridWidth = (1 * 100) / 43; // width of the canvas grid in percentage + const _appDefinition = _.cloneDeep(appDefinition); + let newComponents = _appDefinition?.pages[currentPageId].components; + + for (const selectedComponent of selectedComponents) { + let top = newComponents[selectedComponent.id].layouts[props.currentLayout].top; + let left = newComponents[selectedComponent.id].layouts[props.currentLayout].left; + + switch (direction) { + case 'ArrowLeft': + left = left - gridWidth; + break; + case 'ArrowRight': + left = left + gridWidth; + break; + case 'ArrowDown': + top = top + 10; + break; + case 'ArrowUp': + top = top - 10; + break; + } + + newComponents[selectedComponent.id].layouts[props.currentLayout].top = top; + newComponents[selectedComponent.id].layouts[props.currentLayout].left = left; + } + + _appDefinition.pages[currentPageId].components = newComponents; + + appDefinitionChanged(_appDefinition); + }; + + const copyComponents = () => + cloneComponents(selectedComponents, appDefinition, currentPageId, appDefinitionChanged, false); + + const cutComponents = () => { + if (props.isVersionReleased) { + useAppVersionStore.getState().actions.enableReleasedVersionPopupState(); + + return; + } + + cloneComponents(selectedComponents, appDefinition, currentPageId, appDefinitionChanged, false); + }; + + const handleEditorEscapeKeyPress = () => { + if (selectedComponents?.length > 0) { + updateEditorState({ + selectedComponents: [], + }); + handleInspectorView(); + } + }; + + const removeComponents = () => { + if (!props.isVersionReleased && selectedComponents?.length > 1) { + let newDefinition = cloneDeep(appDefinition); + + removeSelectedComponent(currentPageId, newDefinition, selectedComponents); + const platform = navigator?.userAgentData?.platform || navigator?.platform || 'unknown'; + if (platform.toLowerCase().indexOf('mac') > -1) { + toast('Selected components deleted! (⌘ + Z to undo)', { + icon: '🗑️', + }); + } else { + toast('Selected components deleted! (ctrl + Z to undo)', { + icon: '🗑️', + }); + } + appDefinitionChanged(newDefinition, { + skipAutoSave: props.isVersionReleased, + }); + handleInspectorView(); + } else if (props.isVersionReleased) { + useAppVersionStore.getState().actions.enableReleasedVersionPopupState(); + } + }; + + // !------- + + const currentState = props?.currentState; + const editingVersion = props?.editingVersion; + const appVersionPreviewLink = editingVersion + ? `/applications/${app.id}/versions/${editingVersion.id}/${currentState.page.handle}` + : ''; + const deviceWindowWidth = 450; + + if (!appDefinition?.homePageId) { + return ( +
+
+
{/* */}
+
Loading...
+
+
+ ); + } + + return ( +
+ {props.isVersionReleased && } + + + +
+ handleRunQuery(queryId, queryName)} + ref={dataSourceModalRef} + isSaving={isSaving} + currentPageId={currentPageId} + // addNewPage={addNewPage} + // switchPage={switchPage} + // deletePage={deletePageRequest} + // renamePage={renamePage} + // clonePage={clonePage} + // hidePage={hidePage} + // unHidePage={unHidePage} + // updateHomePage={updateHomePage} + // updatePageHandle={updatePageHandle} + // updateOnPageLoadEvents={updateOnPageLoadEvents} + // showHideViewerNavigationControls={showHideViewerNavigation} + // updateOnSortingPages={updateOnSortingPages} + setEditorMarginLeft={handleEditorMarginLeftChange} + /> + {!props.showComments && ( + { + canvasContainerRef.current.scrollBy(e.direction[0] * 10, e.direction[1] * 10); + }} + /> + )} +
+
{ + if (['real-canvas', 'modal'].includes(e.target.className)) { + updateEditorState({ + currentSidebarTab: 2, + selectedComponents: [], + }); + setHoveredComponent(null); + } + }} + ref={canvasContainerRef} + onScroll={() => { + selectionRef.current.checkScroll(); + }} + > +
+
+ {config.ENABLE_MULTIPLAYER_EDITING && ( + + )} + {isLoading && ( +
+
+
+
+
+
+ +
+ {Array.from(Array(4)).map((_item, index) => ( + + ))} +
+ +
+ + +
+
+
+
+
+ )} + {defaultComponentStateComputed && ( + <> + + setIsDragging(isDragging)} + /> + + )} +
+
+
+ + +
+
+ + + {currentSidebarTab === 1 && ( +
+ {selectedComponents.length === 1 && + !isEmpty(appDefinition?.pages[currentPageId]?.components) && + !isEmpty(appDefinition?.pages[currentPageId]?.components[selectedComponents[0].id]) ? ( + + ) : ( +
+ {props.t('editor.inspectComponent', 'Please select a component to inspect')} +
+ )} +
+ )} + + {currentSidebarTab === 2 && ( + + )} +
+ {config.COMMENT_FEATURE_ENABLE && props.showComments && ( + + )} +
+
+
+
+ ); +}; + +const withStore = (Component) => (props) => { + const { showComments, currentLayout } = useEditorStore( + (state) => ({ + showComments: state?.showComments, + currentLayout: state?.currentLayout, + }), + shallow + ); + const { isVersionReleased, editingVersion } = useAppVersionStore( + (state) => ({ isVersionReleased: state.isVersionReleased, editingVersion: state.editingVersion }), + shallow + ); + + const currentState = useCurrentState(); + + return ( + + ); +}; + +export const EditorFunc = withTranslation()(withRouter(withStore(EditorComponent))); diff --git a/frontend/src/Editor/Header/GlobalSettings.jsx b/frontend/src/Editor/Header/GlobalSettings.jsx index f0d62fd1fc..9b169ea7dc 100644 --- a/frontend/src/Editor/Header/GlobalSettings.jsx +++ b/frontend/src/Editor/Header/GlobalSettings.jsx @@ -19,11 +19,10 @@ export const GlobalSettings = ({ globalSettingsChanged, darkMode, toggleAppMaintenance, - is_maintenance_on, + isMaintenanceOn, }) => { const { t } = useTranslation(); - const { hideHeader, canvasMaxWidth, canvasMaxWidthType, canvasMaxHeight, canvasBackgroundColor, backgroundFxQuery } = - globalSettings; + const { hideHeader, canvasMaxWidth, canvasMaxWidthType, canvasBackgroundColor, backgroundFxQuery } = globalSettings; const [showPicker, setShowPicker] = React.useState(false); const currentState = useCurrentState(); const [forceCodeBox, setForceCodeBox] = React.useState(true); @@ -87,7 +86,7 @@ export const GlobalSettings = ({ data-cy={`toggle-maintenance-mode`} className="form-check-input" type="checkbox" - checked={is_maintenance_on} + checked={isMaintenanceOn} onChange={() => setConfirmationShow(true)} />
@@ -237,7 +236,7 @@ export const GlobalSettings = ({ { + updateState({ slug: newSlug }); + }; + const { isVersionReleased, editingVersion } = useAppVersionStore( (state) => ({ isVersionReleased: state.isVersionReleased, @@ -87,9 +93,9 @@ export default function EditorHeader({ globalSettings={appDefinition.globalSettings} darkMode={darkMode} toggleAppMaintenance={toggleAppMaintenance} - is_maintenance_on={is_maintenance_on} + isMaintenanceOn={isMaintenanceOn} /> - +
@@ -167,14 +173,12 @@ export default function EditorHeader({
- {app.id && ( - - )} +
diff --git a/frontend/src/Editor/Inspector/EventManager.jsx b/frontend/src/Editor/Inspector/EventManager.jsx index 3cdf997c16..8e787d16be 100644 --- a/frontend/src/Editor/Inspector/EventManager.jsx +++ b/frontend/src/Editor/Inspector/EventManager.jsx @@ -15,13 +15,13 @@ import { useTranslation } from 'react-i18next'; import { useDataQueries } from '@/_stores/dataQueriesStore'; import RunjsParameters from './ActionConfigurationPanels/RunjsParamters'; +import { useAppInfo } from '@/_stores/appDataStore'; export const EventManager = ({ component, componentMeta, components, eventsChanged, - apps, excludeEvents, popOverCallback, popoverPlacement, @@ -29,6 +29,8 @@ export const EventManager = ({ hideEmptyEventsAlert, }) => { const dataQueries = useDataQueries(); + const { apps, appId } = useAppInfo(); + const [events, setEvents] = useState(() => component.component.definition.events || []); const [focusedEventIndex, setFocusedEventIndex] = useState(null); const { t } = useTranslation(); @@ -170,7 +172,7 @@ export const EventManager = ({ function getAllApps() { let appsOptionsList = []; apps - .filter((item) => item.slug !== undefined) + .filter((item) => item.slug !== undefined && item.id !== appId) .forEach((item) => { appsOptionsList.push({ name: item.name, diff --git a/frontend/src/Editor/Inspector/Inspector.jsx b/frontend/src/Editor/Inspector/Inspector.jsx index f349abe3a7..e5eb84f818 100644 --- a/frontend/src/Editor/Inspector/Inspector.jsx +++ b/frontend/src/Editor/Inspector/Inspector.jsx @@ -28,7 +28,6 @@ export const Inspector = ({ selectedComponentId, componentDefinitionChanged, allComponents, - apps, darkMode, switchSidebarTab, removeComponent, @@ -285,7 +284,6 @@ export const Inspector = ({ currentState={currentState} darkMode={darkMode} eventsChanged={eventsChanged} - apps={apps} pages={pages} allComponents={allComponents} /> diff --git a/frontend/src/Editor/QueryManager/Components/QueryManagerBody.jsx b/frontend/src/Editor/QueryManager/Components/QueryManagerBody.jsx index 4aed281625..67eedb47dd 100644 --- a/frontend/src/Editor/QueryManager/Components/QueryManagerBody.jsx +++ b/frontend/src/Editor/QueryManager/Components/QueryManagerBody.jsx @@ -39,7 +39,6 @@ export const QueryManagerBody = forwardRef( previewLoading, queryPreviewData, allComponents, - apps, appDefinition, createDraftQuery, setOptions, @@ -309,7 +308,6 @@ export const QueryManagerBody = forwardRef( componentMeta={queryComponent.componentMeta} dataQueries={dataQueries} components={allComponents} - apps={apps} popoverPlacement="top" pages={ appDefinition?.pages ? Object.entries(appDefinition?.pages).map(([id, page]) => ({ ...page, id })) : [] diff --git a/frontend/src/Editor/QueryManager/QueryManager.jsx b/frontend/src/Editor/QueryManager/QueryManager.jsx index 957b472a9d..1ca04a92fa 100644 --- a/frontend/src/Editor/QueryManager/QueryManager.jsx +++ b/frontend/src/Editor/QueryManager/QueryManager.jsx @@ -22,7 +22,6 @@ const QueryManager = ({ dataQueriesChanged, appId, darkMode, - apps, allComponents, dataSourceModalHandler, appDefinition, @@ -105,7 +104,6 @@ const QueryManager = ({ previewLoading={previewLoading} queryPreviewData={queryPreviewData} allComponents={allComponents} - apps={apps} appDefinition={appDefinition} createDraftQuery={createDraftQuery} setOptions={setOptions} diff --git a/frontend/src/Editor/QueryPanel/QueryPanel.jsx b/frontend/src/Editor/QueryPanel/QueryPanel.jsx index 6dd633fca7..3508d01f47 100644 --- a/frontend/src/Editor/QueryPanel/QueryPanel.jsx +++ b/frontend/src/Editor/QueryPanel/QueryPanel.jsx @@ -13,7 +13,6 @@ const QueryPanel = ({ dataQueriesChanged, fetchDataQueries, darkMode, - apps, allComponents, appId, appDefinition, @@ -268,7 +267,6 @@ const QueryPanel = ({ dataQueriesChanged={updateDataQueries} appId={appId} darkMode={darkMode} - apps={apps} allComponents={allComponents} dataSourceModalHandler={dataSourceModalHandler} appDefinition={appDefinition} diff --git a/frontend/src/Editor/RealtimeEditor.jsx b/frontend/src/Editor/RealtimeEditor.jsx index 605af7ba7f..41779f4d32 100644 --- a/frontend/src/Editor/RealtimeEditor.jsx +++ b/frontend/src/Editor/RealtimeEditor.jsx @@ -3,7 +3,8 @@ import React from 'react'; import config from 'config'; import { RoomProvider } from '@y-presence/react'; import Spinner from '@/_ui/Spinner'; -import { Editor } from '@/Editor'; +import { Editor, EditorFunc } from '@/Editor'; + import useRouter from '@/_hooks/use-router'; import { useParams } from 'react-router-dom'; const Y = require('yjs'); @@ -70,7 +71,7 @@ export const RealtimeEditor = (props) => { return ( - + ); }; diff --git a/frontend/src/Editor/Viewer.jsx b/frontend/src/Editor/Viewer.jsx index d50821e8f6..ceb35437b0 100644 --- a/frontend/src/Editor/Viewer.jsx +++ b/frontend/src/Editor/Viewer.jsx @@ -168,7 +168,7 @@ class ViewerComponent extends React.Component { homepage: this.state.appDefinition?.pages?.[this.state.appDefinition?.homePageId]?.handle, }, () => { - computeComponentState(this, data?.definition?.pages[currentPage.id]?.components).then(async () => { + computeComponentState(data?.definition?.pages[currentPage.id]?.components).then(async () => { this.setState({ initialComputationOfStateDone: true }); console.log('Default component state computed and set'); this.runQueries(data.data_queries); @@ -407,15 +407,13 @@ class ViewerComponent extends React.Component { name: targetPage.name, }, async () => { - computeComponentState(this, this.state.appDefinition?.pages[this.state.currentPageId].components).then( - async () => { - // eslint-disable-next-line no-unsafe-optional-chaining - const { events } = this.state.appDefinition?.pages[this.state.currentPageId]; - for (const event of events ?? []) { - await this.handleEvent(event.eventId, event); - } + computeComponentState(this.state.appDefinition?.pages[this.state.currentPageId].components).then(async () => { + // eslint-disable-next-line no-unsafe-optional-chaining + const { events } = this.state.appDefinition?.pages[this.state.currentPageId]; + for (const event of events ?? []) { + await this.handleEvent(event.eventId, event); } - ); + }); } ); } diff --git a/frontend/src/Editor/index.js b/frontend/src/Editor/index.js index 8e539b2b32..a5bdf8c189 100644 --- a/frontend/src/Editor/index.js +++ b/frontend/src/Editor/index.js @@ -2,3 +2,4 @@ export * from './Editor'; export * from './Viewer'; export * from './DataSourceManager'; +export * from './EditorFunc'; diff --git a/frontend/src/_helpers/appUtils.js b/frontend/src/_helpers/appUtils.js index 2733e47b66..7e981a29c7 100644 --- a/frontend/src/_helpers/appUtils.js +++ b/frontend/src/_helpers/appUtils.js @@ -31,6 +31,7 @@ import { useDataQueriesStore } from '@/_stores/dataQueriesStore'; import { useQueryPanelStore } from '@/_stores/queryPanelStore'; import { useCurrentStateStore, getCurrentState } from '@/_stores/currentStateStore'; import { useAppVersionStore } from '@/_stores/appVersionStore'; +import { useEditorStore } from '@/_stores/editorStore'; const ERROR_TYPES = Object.freeze({ ReferenceError: 'ReferenceError', @@ -58,7 +59,7 @@ export function setCurrentStateAsync(_ref, changes) { }); } -export function onComponentOptionsChanged(_ref, component, options) { +export function onComponentOptionsChanged(component, options) { const componentName = component.name; const components = getCurrentState().components; let componentData = components[componentName]; @@ -74,7 +75,7 @@ export function onComponentOptionsChanged(_ref, component, options) { return Promise.resolve(); } -export function onComponentOptionChanged(_ref, component, option_name, value) { +export function onComponentOptionChanged(component, option_name, value) { const componentName = component.name; const components = getCurrentState().components; @@ -351,7 +352,7 @@ function showModal(_ref, modal, show) { return Promise.resolve(); } - const modalMeta = _ref.state.appDefinition.pages[_ref.state.currentPageId].components[modalId]; + const modalMeta = _ref.appDefinition.pages[_ref.state.currentPageId].components[modalId]; //! NeedToFix const _components = { ...getCurrentState().components, @@ -366,7 +367,7 @@ function showModal(_ref, modal, show) { return Promise.resolve(); } -function logoutAction(_ref) { +function logoutAction() { localStorage.clear(); authenticationService.logout(true); @@ -431,7 +432,7 @@ function executeActionWithDebounce(_ref, event, mode, customVariables) { return runQuery(_ref, queryId, name, undefined, mode, resolvedParams); } case 'logout': { - return logoutAction(_ref); + return logoutAction(); } case 'open-webpage': { @@ -511,7 +512,7 @@ function executeActionWithDebounce(_ref, event, mode, customVariables) { } case 'set-table-page': { - setTablePageIndex(_ref, event.table, event.pageIndex); + setTablePageIndex(event.table, event.pageIndex); break; } @@ -574,7 +575,7 @@ function executeActionWithDebounce(_ref, event, mode, customVariables) { } case 'switch-page': { - _ref.switchPage(event.pageId, resolveReferences(event.queryParams, getCurrentState(), [], customVariables)); + _ref.switchPage(event.pageId, resolveReferences(event.queryParams, getCurrentState(), [], customVariables)); // arpit [switchPage] return Promise.resolve(); } } @@ -600,7 +601,7 @@ export async function onEvent(_ref, eventName, options, mode = 'edit') { }, }, }); - runQuery(_ref, queryId, queryName, true, mode, parameters); + runQuery(_ref, queryId, queryName, true, mode, parameters); //arpit [runQuery] } if (eventName === 'onCalendarEventSelect') { @@ -737,9 +738,9 @@ export async function onEvent(_ref, eventName, options, mode = 'edit') { } if (eventName === 'onBulkUpdate') { - onComponentOptionChanged(_self, options.component, 'isSavingChanges', true); + onComponentOptionChanged(options.component, 'isSavingChanges', true); await executeActionsForEventId(_self, eventName, options.component, mode, customVariables); - onComponentOptionChanged(_self, options.component, 'isSavingChanges', false); + onComponentOptionChanged(options.component, 'isSavingChanges', false); } if (['onDataQuerySuccess', 'onDataQueryFailure'].includes(eventName)) { @@ -1105,7 +1106,7 @@ export function runQuery(_ref, queryId, queryName, confirmed = undefined, mode = }); } -export function setTablePageIndex(_ref, tableId, index) { +export function setTablePageIndex(tableId, index) { if (_.isEmpty(tableId)) { console.log('No table is associated with this event.'); return Promise.resolve(); @@ -1126,7 +1127,7 @@ export function renderTooltip({ props, text }) { ); } -export function computeComponentState(_ref, components = {}) { +export function computeComponentState(components = {}) { let componentState = {}; const currentComponents = getCurrentState().components; Object.keys(components).forEach((key) => { @@ -1168,8 +1169,11 @@ export function computeComponentState(_ref, components = {}) { }, }); - return setStateAsync(_ref, { - defaultComponentStateComputed: true, + return new Promise((resolve) => { + useEditorStore.getState().actions.updateEditorState({ + defaultComponentStateComputed: true, + }); + resolve(); }); } @@ -1186,13 +1190,13 @@ export const getSvgIcon = (key, height = 50, width = 50, iconFile = undefined, s }; export const debuggerActions = { - error: (_self, errors) => { + error: (errors) => { useCurrentStateStore.getState().actions.setErrors({ ...errors, }); }, - flush: (_self) => { + flush: () => { useCurrentStateStore.getState().actions.setCurrentState({ errors: {}, }); @@ -1312,9 +1316,16 @@ const updateNewComponents = (pageId, appDefinition, newComponents, updateAppDefi updateAppDefinition(newAppDefinition); }; -export const cloneComponents = (_ref, updateAppDefinition, isCloning = true, isCut = false) => { - const { selectedComponents, appDefinition, currentPageId } = _ref.state; +export const cloneComponents = ( + selectedComponents, + appDefinition, + currentPageId, + updateAppDefinition, + isCloning = true, + isCut = false +) => { if (selectedComponents.length < 1) return getSelectedText(); + const { components: allComponents } = appDefinition.pages[currentPageId]; let newDefinition = _.cloneDeep(appDefinition); let newComponents = [], @@ -1351,7 +1362,13 @@ export const cloneComponents = (_ref, updateAppDefinition, isCloning = true, isC navigator.clipboard.writeText(JSON.stringify(newComponentObj)); toast.success('Component copied succesfully'); } - _ref.setState({ currentSidebarTab: 2 }); + + return new Promise((resolve) => { + useEditorStore.getState().actions.updateEditorState({ + currentSidebarTab: 2, + }); + resolve(); + }); }; const getChildComponents = (allComponents, component, parentComponent, addedComponentId) => { @@ -1606,7 +1623,7 @@ export const runQueries = (queries, _ref) => { }); }; -export const computeQueryState = (queries, _ref) => { +export const computeQueryState = (queries) => { let queryState = {}; queries.forEach((query) => { if (query.plugin?.plugin_id) { diff --git a/frontend/src/_helpers/utils.js b/frontend/src/_helpers/utils.js index 51d7344b43..12431a4e91 100644 --- a/frontend/src/_helpers/utils.js +++ b/frontend/src/_helpers/utils.js @@ -544,10 +544,11 @@ export const hightlightMentionedUserInComment = (comment) => { }; export const generateAppActions = (_ref, queryId, mode, isPreview = false) => { - const currentPageId = _ref.state.currentPageId; - const currentComponents = _ref.state?.appDefinition?.pages[currentPageId]?.components - ? Object.entries(_ref.state.appDefinition.pages[currentPageId]?.components) + const currentPageId = _ref.currentPageId; + const currentComponents = _ref.appDefinition?.pages[currentPageId]?.components + ? Object.entries(_ref.appDefinition.pages[currentPageId]?.components) : {}; + const runQuery = (queryName = '', parameters) => { const query = useDataQueriesStore.getState().dataQueries.find((query) => query.name === queryName); diff --git a/frontend/src/_stores/appDataStore.js b/frontend/src/_stores/appDataStore.js index 7c6ccd612c..aac92a9be1 100644 --- a/frontend/src/_stores/appDataStore.js +++ b/frontend/src/_stores/appDataStore.js @@ -1,7 +1,23 @@ +import { shallow } from 'zustand/shallow'; import { create, zustandDevTools } from './utils'; const initialState = { editingVersion: null, + currentUser: null, + apps: [], + appId: null, + appName: null, + slug: null, + isPublic: null, + isMaintenanceOn: null, + organizationId: null, + currentVersionId: null, + userId: null, + app: {}, + components: [], + pages: [], + layouts: [], + eventHandlers: [], }; export const useAppDataStore = create( @@ -10,6 +26,8 @@ export const useAppDataStore = create( ...initialState, actions: { updateEditingVersion: (version) => set(() => ({ editingVersion: version })), + updateApps: (apps) => set(() => ({ apps: apps })), + updateState: (state) => set((prev) => ({ ...prev, ...state })), }, }), { name: 'App Data Store' } @@ -18,3 +36,6 @@ export const useAppDataStore = create( export const useEditingVersion = () => useAppDataStore((state) => state.editingVersion); export const useUpdateEditingVersion = () => useAppDataStore((state) => state.actions); +export const useCurrentUser = () => useAppDataStore((state) => state.currentUser); +export const useAppInfo = () => useAppDataStore((state) => state); +export const useAppDataActions = () => useAppDataStore((state) => state.actions); diff --git a/frontend/src/_stores/dataQueriesStore.js b/frontend/src/_stores/dataQueriesStore.js index bcfcfaadab..369f311573 100644 --- a/frontend/src/_stores/dataQueriesStore.js +++ b/frontend/src/_stores/dataQueriesStore.js @@ -29,7 +29,7 @@ export const useDataQueriesStore = create( // Runs query on loading application if (runQueriesOnAppLoad) runQueries(data.data_queries, editorRef); // Compute query state to be added in the current state - computeQueryState(data.data_queries, editorRef); + computeQueryState(data.data_queries); const { actions, selectedQuery } = useQueryPanelStore.getState(); if (selectFirstQuery || selectedQuery?.id === 'draftQuery') { actions.setSelectedQuery(data.data_queries[0]?.id, data.data_queries[0]); @@ -40,7 +40,7 @@ export const useDataQueriesStore = create( }); }, setDataQueries: (dataQueries) => set({ dataQueries }), - deleteDataQueries: (queryId, editorRef) => { + deleteDataQueries: (queryId) => { set({ isDeletingQueryInProcess: true }); dataqueryService .del(queryId) @@ -57,8 +57,7 @@ export const useDataQueriesStore = create( get().actions.fetchDataQueries( useAppVersionStore.getState().editingVersion?.id, selectedQuery?.id === queryId, - false, - editorRef + false ); }) .catch(({ error }) => { @@ -122,12 +121,12 @@ export const useDataQueriesStore = create( }); }); }, - renameQuery: (id, newName, editorRef) => { + renameQuery: (id, newName) => { dataqueryService .update(id, newName) .then(() => { toast.success('Query Name Updated'); - get().actions.fetchDataQueries(useAppVersionStore.getState().editingVersion?.id, false, false, editorRef); + get().actions.fetchDataQueries(useAppVersionStore.getState().editingVersion?.id, false, false); }) .catch(({ error }) => { toast.error(error); diff --git a/frontend/src/_stores/editorStore.js b/frontend/src/_stores/editorStore.js index bb4b83ba8c..87515e1617 100644 --- a/frontend/src/_stores/editorStore.js +++ b/frontend/src/_stores/editorStore.js @@ -4,6 +4,25 @@ const initialState = { currentLayout: 'desktop', showComments: false, isEditorActive: false, + currentSidebarTab: 2, + selectedComponents: [], + selectedComponent: null, + scrollOptions: { + container: null, + throttleTime: 0, + threshold: 0, + }, + canUndo: false, + canRedo: false, + currentVersion: {}, + noOfVersionsSupported: 100, + appDefinition: {}, + isSaving: false, + saveError: false, + isLoading: true, + defaultComponentStateComputed: false, + showLeftSidebar: true, + queryConfirmationList: [], }; export const useEditorStore = create( @@ -15,8 +34,13 @@ export const useEditorStore = create( toggleComments: () => set({ showComments: !get().showComments }), toggleCurrentLayout: (currentLayout) => set({ currentLayout }), setIsEditorActive: (isEditorActive) => set(() => ({ isEditorActive })), + updateEditorState: (state) => set((prev) => ({ ...prev, ...state })), + updateQueryConfirmationList: (queryConfirmationList) => set({ queryConfirmationList }), }, }), { name: 'Editor Store' } ) ); + +export const useEditorActions = () => useEditorStore((state) => state.actions); +export const useEditorState = () => useEditorStore((state) => state); From ea0ab5b5c7551248a485b7b325bf1cabf238f7ba Mon Sep 17 00:00:00 2001 From: arpitnath Date: Sat, 5 Aug 2023 19:18:58 +0530 Subject: [PATCH 003/704] fixes: mutating of components from appDefinitions --- .../src/Editor/CodeBuilder/CodeHinter.jsx | 2 +- frontend/src/Editor/Container.jsx | 5 +- frontend/src/Editor/EditorFunc.jsx | 118 ++++++++++-------- frontend/src/Editor/Inspector/Inspector.jsx | 28 ++--- frontend/src/Editor/ManageAppUsers.jsx | 3 +- frontend/src/_helpers/appUtils.js | 2 +- 6 files changed, 88 insertions(+), 70 deletions(-) diff --git a/frontend/src/Editor/CodeBuilder/CodeHinter.jsx b/frontend/src/Editor/CodeBuilder/CodeHinter.jsx index 04f8daf553..8ab7fdf403 100644 --- a/frontend/src/Editor/CodeBuilder/CodeHinter.jsx +++ b/frontend/src/Editor/CodeBuilder/CodeHinter.jsx @@ -351,7 +351,7 @@ export function CodeHinter({ height={'100%'} onFocus={() => setFocused(true)} onBlur={(editor, e) => { - e.stopPropagation(); + e?.stopPropagation(); const value = editor.getValue()?.trimEnd(); onChange(value); if (!isPreviewFocused.current) { diff --git a/frontend/src/Editor/Container.jsx b/frontend/src/Editor/Container.jsx index 6e573c7e4a..126f2485ce 100644 --- a/frontend/src/Editor/Container.jsx +++ b/frontend/src/Editor/Container.jsx @@ -57,8 +57,9 @@ export const Container = ({ const components = useMemo( () => appDefinition.pages[currentPageId]?.components ?? {}, // eslint-disable-next-line react-hooks/exhaustive-deps - [appDefinition.pages[currentPageId]] + [JSON.stringify(appDefinition)] ); + const currentState = useCurrentState(); const { appVersionsId, enableReleasedVersionPopupState, isVersionReleased } = useAppVersionStore( (state) => ({ @@ -180,7 +181,7 @@ export const Container = ({ }, }; - appDefinitionChanged(newDefinition); + appDefinitionChanged(newDefinition, { containerChanges: true }); // eslint-disable-next-line react-hooks/exhaustive-deps }, [boxes]); diff --git a/frontend/src/Editor/EditorFunc.jsx b/frontend/src/Editor/EditorFunc.jsx index af72c4174a..4280cf6b07 100644 --- a/frontend/src/Editor/EditorFunc.jsx +++ b/frontend/src/Editor/EditorFunc.jsx @@ -2,7 +2,7 @@ import React, { useEffect, useLayoutEffect, useRef, useState } from 'react'; import { appService, authenticationService, appVersionService, orgEnvironmentVariableService } from '@/_services'; import { DndProvider } from 'react-dnd'; import { HTML5Backend } from 'react-dnd-html5-backend'; -import _, { defaults, cloneDeep, isEqual, isEmpty, debounce, omit, update } from 'lodash'; +import _, { defaults, cloneDeep, isEqual, isEmpty, debounce, omit, update, difference } from 'lodash'; import { Container } from './Container'; import { EditorKeyHooks } from './EditorKeyHooks'; import { CustomDragLayer } from './CustomDragLayer'; @@ -53,6 +53,8 @@ import { shallow } from 'zustand/shallow'; import { useEditorActions, useEditorState, useEditorStore } from '@/_stores/editorStore'; import { useAppDataActions, useAppDataStore, useAppInfo } from '@/_stores/appDataStore'; import { useMounted } from '@/_hooks/use-mount'; +// eslint-disable-next-line import/no-unresolved +import { diff } from 'deep-object-diff'; setAutoFreeze(false); enablePatches(); @@ -132,6 +134,8 @@ const EditorComponent = (props) => { const selectionDragRef = useRef(null); const selectionRef = useRef(null); + const prevAppDefinition = useRef(appDefinition); + useLayoutEffect(() => { resetAllStores(); }, []); @@ -181,12 +185,12 @@ const EditorComponent = (props) => { subscription.unsubscribe(); if (config.ENABLE_MULTIPLAYER_EDITING) props?.provider?.disconnect(); useEditorStore.getState().actions.setIsEditorActive(false); + prevAppDefinition.current = null; }; // eslint-disable-next-line react-hooks/exhaustive-deps }, []); // Ref to store the previous appDefinition for comparison - const prevAppDefinition = useRef(appDefinition); useEffect(() => { if (currentUser?.current_organization_id) { @@ -198,9 +202,17 @@ const EditorComponent = (props) => { useEffect(() => { const didAppDefinitionChanged = !_.isEqual(appDefinition, prevAppDefinition.current); - console.log('---arpit-- appDefinitionChanged', { dataQueries, didAppDefinitionChanged, appDefinition }); + console.log('---arpit-- appDefinitionChanged [useEffect]', { didAppDefinitionChanged, isSaving }); + + if (didAppDefinitionChanged) { + console.log('---arpit-- updating [prevAppDefinition] [useEffect]', { + prev: prevAppDefinition.current, + curr: appDefinition, + }); + prevAppDefinition.current = appDefinition; + } + if (mounted && didAppDefinitionChanged && currentPageId) { - console.log('---arpit-- appDefinitionChanged [mounted=true]', { appDefinition }); const components = appDefinition?.pages[currentPageId]?.components || {}; computeComponentState(components); @@ -209,7 +221,6 @@ const EditorComponent = (props) => { autoSave(); } } - prevAppDefinition.current = appDefinition; }, [JSON.stringify({ appDefinition, currentPageId, dataQueries })]); const editorRef = { @@ -298,7 +309,7 @@ const EditorComponent = (props) => { }; const $componentDidMount = async () => { - console.log('---arpit-- componentDidMounted effect', { appDefinition, currentPageId }); + // console.log('---arpit-- componentDidMounted effect', { appDefinition, currentPageId }); window.addEventListener('message', handleMessage); // autoSave(); @@ -612,13 +623,16 @@ const EditorComponent = (props) => { await fetchDataSources(data.editing_version?.id); await fetchDataQueries(data.editing_version?.id, true, true); - // let dataDefinition = defaults(data.definition, defaultDefinition(props.darkMode)); let dataDefinition = data.definition ?? defaults(data.definition, defaultDefinition(props.darkMode)); - console.log('---arpit-- fetching app data', { startingPageHandle, data, dataDefinition }); const pages = Object.entries(dataDefinition.pages).map(([pageId, page]) => ({ id: pageId, ...page })); const startingPageId = pages.filter((page) => page.handle === startingPageHandle)[0]?.id; - const homePageId = startingPageId || dataDefinition.homePageId; + const homePageId = !startingPageHandle || startingPageId === 'null' ? dataDefinition.homePageId : startingPageId; + + console.log('---arpit-- fetching app data', { + data, + dataDefinition, + }); setCurrentPageId(homePageId); @@ -647,11 +661,6 @@ const EditorComponent = (props) => { appDefinition: dataDefinition, }); - appDefinitionChanged(dataDefinition, { - skipAutoSave: true, - skipYmapUpdate: true, - }); - for (const event of dataDefinition.pages[homePageId]?.events ?? []) { await handleEvent(event.eventId, event); } @@ -670,6 +679,7 @@ const EditorComponent = (props) => { // !-------- const setAppDefinitionFromVersion = (version, shouldWeEditVersion = true) => { + console.log('---arpit [setAppFromVersion]--', version); if (version?.id !== props.editingVersion?.id) { appDefinitionChanged(defaults(version.definition, defaultDefinition(props.darkMode)), { skipAutoSave: true, @@ -696,8 +706,11 @@ const EditorComponent = (props) => { }; const appDefinitionChanged = (newDefinition, opts = {}) => { - console.log('--arpit | appDefinitionChanged func()'); - if (_.isEqual(appDefinition, newDefinition)) return; + console.log('--arpit | appDefinitionChanged func() called', { + opts, + }); + + // if (_.isEqual(prevAppDefinition.current, newDefinition)) return; if (config.ENABLE_MULTIPLAYER_EDITING && !opts.skipYmapUpdate) { props.ymap?.set('appDef', { newDefinition, @@ -711,40 +724,44 @@ const EditorComponent = (props) => { return new Promise((resolve) => { updateEditorState({ isSaving: true, - appDefinition: newDefinition, - // appDefinitionLocalVersion: uuid(), }); - // if (!opts.skipAutoSave) autoSave(); - resolve(); }); } // Create a new copy of appDefinition with lodash's cloneDeep const updatedAppDefinition = _.cloneDeep(appDefinition); + + if (_.isEmpty(updatedAppDefinition)) return; + const currentPageComponents = newDefinition.pages[currentPageId]?.components; - if (!updatedAppDefinition.pages[currentPageId]) { - updatedAppDefinition.pages[currentPageId] = {}; // Create a new object for the page if it doesn't exist - } + updatedAppDefinition.pages[currentPageId].components = currentPageComponents; - updatedAppDefinition.pages[currentPageId].components = currentPageComponents || {}; + const diffPatches = diff(appDefinition, updatedAppDefinition); - // Call handleAddPatch with the list of changes (if needed) - handleAddPatch(); - - // Update the editor state with the new appDefinition - updateEditorState({ - isSaving: true, - appDefinition: updatedAppDefinition, - // appDefinitionLocalVersion: uuid(), + console.log('--arpit | appDefinitionChanged func() | diffPatches', { + // appDefinition, + // updatedAppDefinition, + diffPatches, }); + if (!_.isEmpty(diffPatches)) { + updateEditorState({ + isSaving: true, + appDefinition: updatedAppDefinition, + }); + computeComponentState(updatedAppDefinition.pages[currentPageId]?.components); + } else { + toast.error('No changes in diff [appDefinitionChanged]'); + } + // if (!opts.skipAutoSave) autoSave(); }; const saveEditingVersion = (isUserSwitchedVersion = false) => { + console.log('---arpit [saving - editionversion]--'); if (props.isVersionReleased && !isUserSwitchedVersion) { updateEditorState({ isSaving: false, @@ -875,7 +892,6 @@ const EditorComponent = (props) => { }; const componentDefinitionChanged = (componentDefinition, props) => { - console.log('---arpit [componentDefinitionChanged]', { props, componentDefinition }); if (props?.isVersionReleased) { useAppVersionStore.getState().actions.enableReleasedVersionPopupState(); return; @@ -889,24 +905,31 @@ const EditorComponent = (props) => { updatedAppDefinition.pages[currentPageId].components[componentDefinition.id].component = componentDefinition.component; - // Call handleAddPatch with the list of changes (if needed) - handleAddPatch(); - - // Update the editor state with the new appDefinition + // // Update the editor state with the new appDefinition updateEditorState({ isSaving: true, - appDefinition: updatedAppDefinition, - // appDefinitionLocalVersion: uuid() }); - // Other actions can be performed here if needed, like autoSave, ymap, etc. - computeComponentState(updatedAppDefinition.pages[currentPageId]?.components); - // autoSave(); - props.ymap?.set('appDef', { - newDefinition: updatedAppDefinition, - editingVersionId: props.editingVersion?.id, + const diffPatches = diff(appDefinition, updatedAppDefinition); + console.log('---arpit [componentDefinitionChanged]', { + props, + diffPatches, }); + + if (!isEmpty(diffPatches)) { + // handleAddPatch(diffPatches, diff(updatedAppDefinition, appDefinition)); + appDefinitionChanged(updatedAppDefinition, { skipAutoSave: true, componentDefinitionChanged: true }); + } } + + // // Other actions can be performed here if needed, like autoSave, ymap, etc. + // // computeComponentState(updatedAppDefinition.pages[currentPageId]?.components); + // // autoSave(); + // // props.ymap?.set('appDef', { + // // newDefinition: updatedAppDefinition, + // // editingVersionId: props.editingVersion?.id, + // // }); + // } }; const removeComponent = (component) => { @@ -1168,10 +1191,6 @@ const EditorComponent = (props) => { width: currentLayout === 'desktop' ? '100%' : '450px', maxWidth: +appDefinition.globalSettings.canvasMaxWidth + appDefinition.globalSettings.canvasMaxWidthType, - /** - * minWidth will be min(default canvas min width, user set max width). Done to avoid conflict between two - * default canvas min width = calc(((screen width - width component editor side bar) - width of editor sidebar on left) - width of left sidebar popover) - **/ backgroundColor: computeCanvasBackgroundColor(), transform: 'translateZ(0)', //Hack to make modal position respect canvas container, else it positions w.r.t window. @@ -1278,7 +1297,6 @@ const EditorComponent = (props) => { key={selectedComponents[0].id} switchSidebarTab={switchSidebarTab} darkMode={props.darkMode} - // appDefinitionLocalVersion={appDefinitionLocalVersion} pages={getPagesWithIds()} > ) : ( diff --git a/frontend/src/Editor/Inspector/Inspector.jsx b/frontend/src/Editor/Inspector/Inspector.jsx index e5eb84f818..01928d1e98 100644 --- a/frontend/src/Editor/Inspector/Inspector.jsx +++ b/frontend/src/Editor/Inspector/Inspector.jsx @@ -99,9 +99,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( @@ -150,7 +150,7 @@ export const Inspector = ({ definition: newDefinition, }, }); - componentDefinitionChanged(newComponent); + componentDefinitionChanged(newComponent, { componentPropertyUpdated: true }); } function layoutPropertyChanged(param, attr, value, paramType) { @@ -158,9 +158,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']; @@ -174,7 +172,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); @@ -197,29 +195,29 @@ export const Inspector = ({ }, }; - componentDefinitionChanged(newChild); + componentDefinitionChanged(newChild, { withChildLayout: true }); }); } } function eventUpdated(event, actionId) { - let newDefinition = { ...component.component.definition }; + let newDefinition = JSON.parse(JSON.stringify(component.component.definition)); newDefinition.events[event.name] = { actionId }; let newComponent = { ...component, }; - componentDefinitionChanged(newComponent); + componentDefinitionChanged(newComponent, { eventUpdated: true }); } function eventsChanged(newEvents, isReordered = false) { let newDefinition; if (isReordered) { - newDefinition = { ...component.component }; + newDefinition = JSON.parse(JSON.stringify(component.component)); newDefinition.definition.events = newEvents; } else { - newDefinition = { ...component.component.definition }; + newDefinition = JSON.parse(JSON.stringify(component.component.definition)); newDefinition.events = newEvents; } @@ -227,13 +225,13 @@ export const Inspector = ({ ...component, }; - componentDefinitionChanged(newComponent); + componentDefinitionChanged(newComponent, { eventsChanged: true }); } function eventOptionUpdated(event, option, value) { console.log('eventOptionUpdated', event, option, value); - let newDefinition = { ...component.component.definition }; + let newDefinition = JSON.parse(JSON.stringify(component.component.definition)); let eventDefinition = newDefinition.events[event.name] || { options: {} }; newDefinition.events[event.name] = { ...eventDefinition, options: { ...eventDefinition.options, [option]: value } }; @@ -242,7 +240,7 @@ export const Inspector = ({ ...component, }; - componentDefinitionChanged(newComponent); + componentDefinitionChanged(newComponent, { eventOptionUpdated: true }); } const buildGeneralStyle = () => { diff --git a/frontend/src/Editor/ManageAppUsers.jsx b/frontend/src/Editor/ManageAppUsers.jsx index bb977ea4e8..9d7de6a07d 100644 --- a/frontend/src/Editor/ManageAppUsers.jsx +++ b/frontend/src/Editor/ManageAppUsers.jsx @@ -43,7 +43,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); }); }; diff --git a/frontend/src/_helpers/appUtils.js b/frontend/src/_helpers/appUtils.js index 7e981a29c7..1a97a53e9b 100644 --- a/frontend/src/_helpers/appUtils.js +++ b/frontend/src/_helpers/appUtils.js @@ -1313,7 +1313,7 @@ const updateNewComponents = (pageId, appDefinition, newComponents, updateAppDefi ); newAppDefinition.pages[pageId].components[newComponent.id] = newComponent; }); - updateAppDefinition(newAppDefinition); + updateAppDefinition(newAppDefinition, { addComponents: true }); }; export const cloneComponents = ( From 099337c90b4d0af479e7ab8db530fa073366442c Mon Sep 17 00:00:00 2001 From: arpitnath Date: Sat, 5 Aug 2023 22:22:27 +0530 Subject: [PATCH 004/704] fixes app def updates from inspector and containers --- frontend/src/Editor/EditorFunc.jsx | 39 ++++----------------- frontend/src/Editor/Inspector/Inspector.jsx | 30 ++++++---------- 2 files changed, 17 insertions(+), 52 deletions(-) diff --git a/frontend/src/Editor/EditorFunc.jsx b/frontend/src/Editor/EditorFunc.jsx index 4280cf6b07..6d3b56b380 100644 --- a/frontend/src/Editor/EditorFunc.jsx +++ b/frontend/src/Editor/EditorFunc.jsx @@ -202,13 +202,7 @@ const EditorComponent = (props) => { useEffect(() => { const didAppDefinitionChanged = !_.isEqual(appDefinition, prevAppDefinition.current); - console.log('---arpit-- appDefinitionChanged [useEffect]', { didAppDefinitionChanged, isSaving }); - if (didAppDefinitionChanged) { - console.log('---arpit-- updating [prevAppDefinition] [useEffect]', { - prev: prevAppDefinition.current, - curr: appDefinition, - }); prevAppDefinition.current = appDefinition; } @@ -309,9 +303,7 @@ const EditorComponent = (props) => { }; const $componentDidMount = async () => { - // console.log('---arpit-- componentDidMounted effect', { appDefinition, currentPageId }); window.addEventListener('message', handleMessage); - // autoSave(); await fetchApps(0); await fetchApp(props.params.pageHandle); @@ -629,11 +621,6 @@ const EditorComponent = (props) => { const startingPageId = pages.filter((page) => page.handle === startingPageHandle)[0]?.id; const homePageId = !startingPageHandle || startingPageId === 'null' ? dataDefinition.homePageId : startingPageId; - console.log('---arpit-- fetching app data', { - data, - dataDefinition, - }); - setCurrentPageId(homePageId); updateState({ @@ -706,11 +693,6 @@ const EditorComponent = (props) => { }; const appDefinitionChanged = (newDefinition, opts = {}) => { - console.log('--arpit | appDefinitionChanged func() called', { - opts, - }); - - // if (_.isEqual(prevAppDefinition.current, newDefinition)) return; if (config.ENABLE_MULTIPLAYER_EDITING && !opts.skipYmapUpdate) { props.ymap?.set('appDef', { newDefinition, @@ -742,8 +724,6 @@ const EditorComponent = (props) => { const diffPatches = diff(appDefinition, updatedAppDefinition); console.log('--arpit | appDefinitionChanged func() | diffPatches', { - // appDefinition, - // updatedAppDefinition, diffPatches, }); @@ -905,16 +885,11 @@ const EditorComponent = (props) => { updatedAppDefinition.pages[currentPageId].components[componentDefinition.id].component = componentDefinition.component; - // // Update the editor state with the new appDefinition updateEditorState({ isSaving: true, }); const diffPatches = diff(appDefinition, updatedAppDefinition); - console.log('---arpit [componentDefinitionChanged]', { - props, - diffPatches, - }); if (!isEmpty(diffPatches)) { // handleAddPatch(diffPatches, diff(updatedAppDefinition, appDefinition)); @@ -922,13 +897,13 @@ const EditorComponent = (props) => { } } - // // Other actions can be performed here if needed, like autoSave, ymap, etc. - // // computeComponentState(updatedAppDefinition.pages[currentPageId]?.components); - // // autoSave(); - // // props.ymap?.set('appDef', { - // // newDefinition: updatedAppDefinition, - // // editingVersionId: props.editingVersion?.id, - // // }); + // Other actions can be performed here if needed, like autoSave, ymap, etc. + // computeComponentState(updatedAppDefinition.pages[currentPageId]?.components); + // autoSave(); + // props.ymap?.set('appDef', { + // newDefinition: updatedAppDefinition, + // editingVersionId: props.editingVersion?.id, + // }); // } }; diff --git a/frontend/src/Editor/Inspector/Inspector.jsx b/frontend/src/Editor/Inspector/Inspector.jsx index 01928d1e98..f45b3647f1 100644 --- a/frontend/src/Editor/Inspector/Inspector.jsx +++ b/frontend/src/Editor/Inspector/Inspector.jsx @@ -36,14 +36,13 @@ 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(); @@ -122,7 +121,8 @@ export const Inspector = ({ function paramUpdated(param, attr, value, paramType) { console.log({ param, attr, value, paramType }); - let newDefinition = _.cloneDeep(component.component.definition); + let newComponent = JSON.parse(JSON.stringify(component)); + let newDefinition = _.cloneDeep(newComponent.component.definition); let allParams = newDefinition[paramType] || {}; const paramObject = allParams[param.name]; if (!paramObject) { @@ -145,11 +145,7 @@ 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 }); } @@ -212,18 +208,12 @@ export const Inspector = ({ } function eventsChanged(newEvents, isReordered = false) { - let newDefinition; - if (isReordered) { - newDefinition = JSON.parse(JSON.stringify(component.component)); - newDefinition.definition.events = newEvents; - } else { - newDefinition = JSON.parse(JSON.stringify(component.component.definition)); - newDefinition.events = newEvents; - } + let newComponent = JSON.parse(JSON.stringify(component)); + let newDefinition = JSON.parse(JSON.stringify(newComponent.component.definition)); - let newComponent = { - ...component, - }; + newDefinition.events = newEvents; + + newComponent.component.definition = newDefinition; componentDefinitionChanged(newComponent, { eventsChanged: true }); } From dbb330c16bae268c5a6702895da06a2ae5b0228b Mon Sep 17 00:00:00 2001 From: arpitnath Date: Sat, 5 Aug 2023 23:26:05 +0530 Subject: [PATCH 005/704] fixes: leftsidebar border issues --- frontend/src/Editor/EditorFunc.jsx | 2 -- frontend/src/Editor/LeftSidebar/index.jsx | 2 -- 2 files changed, 4 deletions(-) diff --git a/frontend/src/Editor/EditorFunc.jsx b/frontend/src/Editor/EditorFunc.jsx index 6d3b56b380..136fc30e98 100644 --- a/frontend/src/Editor/EditorFunc.jsx +++ b/frontend/src/Editor/EditorFunc.jsx @@ -733,8 +733,6 @@ const EditorComponent = (props) => { appDefinition: updatedAppDefinition, }); computeComponentState(updatedAppDefinition.pages[currentPageId]?.components); - } else { - toast.error('No changes in diff [appDefinitionChanged]'); } // if (!opts.skipAutoSave) autoSave(); diff --git a/frontend/src/Editor/LeftSidebar/index.jsx b/frontend/src/Editor/LeftSidebar/index.jsx index 09a4ed00cd..9f353cb85a 100644 --- a/frontend/src/Editor/LeftSidebar/index.jsx +++ b/frontend/src/Editor/LeftSidebar/index.jsx @@ -88,8 +88,6 @@ export const LeftSidebar = forwardRef((props, ref) => { useEffect(() => { if (!selectedSidebarItem) { setEditorMarginLeft(0); - } else { - setEditorMarginLeft(350); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [selectedSidebarItem]); From 1fd08cac6ccabf33f061c651ace227759c77010f Mon Sep 17 00:00:00 2001 From: arpitnath Date: Sun, 6 Aug 2023 01:28:59 +0530 Subject: [PATCH 006/704] compute the diff and update it to the store --- frontend/src/Editor/EditorFunc.jsx | 17 ++++++++++------- frontend/src/_stores/appDataStore.js | 2 ++ 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/frontend/src/Editor/EditorFunc.jsx b/frontend/src/Editor/EditorFunc.jsx index 136fc30e98..ca7aa92979 100644 --- a/frontend/src/Editor/EditorFunc.jsx +++ b/frontend/src/Editor/EditorFunc.jsx @@ -93,7 +93,7 @@ const EditorComponent = (props) => { const { socket } = createWebsocketConnection(props?.params?.id); const mounted = useMounted(); - const { updateState } = useAppDataActions(); + const { updateState, updateAppDefinitionDiff } = useAppDataActions(); const { updateEditorState, updateQueryConfirmationList } = useEditorActions(); const { noOfVersionsSupported, @@ -115,7 +115,7 @@ const EditorComponent = (props) => { const dataQueries = useDataQueries(); - const { isMaintenanceOn, appId, app, currentUser, currentVersionId } = useAppInfo(); + const { isMaintenanceOn, appId, app, currentUser, currentVersionId, appDefinitionDiff } = useAppInfo(); const [currentVersionChanges, setCurrentVersionChanges] = useState({}); const [currentPageId, setCurrentPageId] = useState(null); @@ -722,16 +722,20 @@ const EditorComponent = (props) => { updatedAppDefinition.pages[currentPageId].components = currentPageComponents; const diffPatches = diff(appDefinition, updatedAppDefinition); + const shouldUpdate = !_.isEmpty(diffPatches) && !isEqual(appDefinitionDiff, diffPatches); - console.log('--arpit | appDefinitionChanged func() | diffPatches', { - diffPatches, - }); + // console.log('--arpit | appDefinitionChanged func() | diffPatches', { + // diffPatches, + // appDefinitionDiff, + // shouldUpdate, + // }); - if (!_.isEmpty(diffPatches)) { + if (shouldUpdate) { updateEditorState({ isSaving: true, appDefinition: updatedAppDefinition, }); + updateAppDefinitionDiff(diffPatches); computeComponentState(updatedAppDefinition.pages[currentPageId]?.components); } @@ -890,7 +894,6 @@ const EditorComponent = (props) => { const diffPatches = diff(appDefinition, updatedAppDefinition); if (!isEmpty(diffPatches)) { - // handleAddPatch(diffPatches, diff(updatedAppDefinition, appDefinition)); appDefinitionChanged(updatedAppDefinition, { skipAutoSave: true, componentDefinitionChanged: true }); } } diff --git a/frontend/src/_stores/appDataStore.js b/frontend/src/_stores/appDataStore.js index aac92a9be1..5fa8a424ab 100644 --- a/frontend/src/_stores/appDataStore.js +++ b/frontend/src/_stores/appDataStore.js @@ -18,6 +18,7 @@ const initialState = { pages: [], layouts: [], eventHandlers: [], + appDefinitionDiff: null, }; export const useAppDataStore = create( @@ -28,6 +29,7 @@ export const useAppDataStore = create( updateEditingVersion: (version) => set(() => ({ editingVersion: version })), updateApps: (apps) => set(() => ({ apps: apps })), updateState: (state) => set((prev) => ({ ...prev, ...state })), + updateAppDefinitionDiff: (appDefinitionDiff) => set(() => ({ appDefinitionDiff: appDefinitionDiff })), }, }), { name: 'App Data Store' } From 1942bacbbf24151a4c827e7ca7603aa9df1942c3 Mon Sep 17 00:00:00 2001 From: arpitnath Date: Sun, 6 Aug 2023 02:43:29 +0530 Subject: [PATCH 007/704] save --- frontend/src/Editor/EditorFunc.jsx | 9 +++++++-- frontend/src/_services/appVersion.service.js | 1 + 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/frontend/src/Editor/EditorFunc.jsx b/frontend/src/Editor/EditorFunc.jsx index ca7aa92979..aed625ff7a 100644 --- a/frontend/src/Editor/EditorFunc.jsx +++ b/frontend/src/Editor/EditorFunc.jsx @@ -743,14 +743,19 @@ const EditorComponent = (props) => { }; const saveEditingVersion = (isUserSwitchedVersion = false) => { - console.log('---arpit [saving - editionversion]--'); + console.log('---arpit [saving - editionversion]--', { appDefinitionDiff }); if (props.isVersionReleased && !isUserSwitchedVersion) { updateEditorState({ isSaving: false, }); } else if (!isEmpty(props?.editingVersion)) { appVersionService - .save(appId, props.editingVersion?.id, { definition: appDefinition }, isUserSwitchedVersion) + .save( + appId, + props.editingVersion?.id, + { definition: appDefinition, diff: appDefinitionDiff }, + isUserSwitchedVersion + ) .then(() => { const _editingVersion = { ...props.editingVersion, diff --git a/frontend/src/_services/appVersion.service.js b/frontend/src/_services/appVersion.service.js index ab81ba8a9b..fd73304379 100644 --- a/frontend/src/_services/appVersion.service.js +++ b/frontend/src/_services/appVersion.service.js @@ -48,6 +48,7 @@ function save(appId, versionId, values, isUserSwitchedVersion = false) { if (values.definition) body['definition'] = values.definition; if (values.name) body['name'] = values.name; + console.log('---arpit [app version service]', { values }); const requestOptions = { method: 'PUT', headers: authHeader(), From 437e3a45acc3c3248201a0146b4a8bd624d2dca3 Mon Sep 17 00:00:00 2001 From: arpitnath Date: Sun, 6 Aug 2023 16:20:21 +0530 Subject: [PATCH 008/704] fixed page actions: add, rename, delete --- frontend/src/Editor/Container.jsx | 2 +- frontend/src/Editor/EditorFunc.jsx | 191 ++++++++++++++++++++++++++--- frontend/src/_helpers/appUtils.js | 1 + 3 files changed, 178 insertions(+), 16 deletions(-) diff --git a/frontend/src/Editor/Container.jsx b/frontend/src/Editor/Container.jsx index 126f2485ce..c4a1bce38b 100644 --- a/frontend/src/Editor/Container.jsx +++ b/frontend/src/Editor/Container.jsx @@ -57,7 +57,7 @@ export const Container = ({ const components = useMemo( () => appDefinition.pages[currentPageId]?.components ?? {}, // eslint-disable-next-line react-hooks/exhaustive-deps - [JSON.stringify(appDefinition)] + [JSON.stringify(appDefinition), currentPageId] ); const currentState = useCurrentState(); diff --git a/frontend/src/Editor/EditorFunc.jsx b/frontend/src/Editor/EditorFunc.jsx index aed625ff7a..2db3c36366 100644 --- a/frontend/src/Editor/EditorFunc.jsx +++ b/frontend/src/Editor/EditorFunc.jsx @@ -128,6 +128,9 @@ const EditorComponent = (props) => { const [isDragging, setIsDragging] = useState(false); + const [showPageDeletionConfirmation, setShowPageDeletionConfirmation] = useState(null); + const [isDeletingPage, setIsDeletingPage] = useState(false); + // refs const canvasContainerRef = useRef(null); const dataSourceModalRef = useRef(null); @@ -712,23 +715,31 @@ const EditorComponent = (props) => { }); } - // Create a new copy of appDefinition with lodash's cloneDeep - const updatedAppDefinition = _.cloneDeep(appDefinition); + let updatedAppDefinition; + + updatedAppDefinition = _.cloneDeep(appDefinition); if (_.isEmpty(updatedAppDefinition)) return; - const currentPageComponents = newDefinition.pages[currentPageId]?.components; + if (opts?.containerChanges || opts?.componentDefinitionChanged) { + const currentPageComponents = newDefinition.pages[currentPageId]?.components; - updatedAppDefinition.pages[currentPageId].components = currentPageComponents; + updatedAppDefinition.pages[currentPageId].components = currentPageComponents; + } + + if (opts?.pageDefinitionChanged) { + updatedAppDefinition.pages = newDefinition.pages; + } const diffPatches = diff(appDefinition, updatedAppDefinition); const shouldUpdate = !_.isEmpty(diffPatches) && !isEqual(appDefinitionDiff, diffPatches); - // console.log('--arpit | appDefinitionChanged func() | diffPatches', { - // diffPatches, - // appDefinitionDiff, - // shouldUpdate, - // }); + console.log('--arpit | appDefinitionChanged func() | diffPatches', { + diffPatches, + appDefinitionDiff, + shouldUpdate, + opts, + }); if (shouldUpdate) { updateEditorState({ @@ -738,8 +749,6 @@ const EditorComponent = (props) => { updateAppDefinitionDiff(diffPatches); computeComponentState(updatedAppDefinition.pages[currentPageId]?.components); } - - // if (!opts.skipAutoSave) autoSave(); }; const saveEditingVersion = (isUserSwitchedVersion = false) => { @@ -1033,6 +1042,149 @@ const EditorComponent = (props) => { } }; + //Page actions + const renamePage = (pageId, newName) => { + if (Object.entries(appDefinition.pages).some(([pId, { name }]) => newName === name && pId !== pageId)) { + return toast.error('Page name already exists'); + } + if (newName.trim().length === 0) { + toast.error('Page name cannot be empty'); + return; + } + + const copyOfAppDefinition = JSON.parse(JSON.stringify(appDefinition)); + + copyOfAppDefinition.pages[pageId].name = newName; + + appDefinitionChanged(copyOfAppDefinition, { pageDefinitionChanged: true }); + }; + + const addNewPage = ({ name, handle }) => { + // check for unique page handles + const pageExists = Object.values(appDefinition.pages).some((page) => page.name === name); + + if (pageExists) { + toast.error('Page name already exists'); + return; + } + + const pageHandles = Object.values(appDefinition.pages).map((page) => page.handle); + + let newHandle = handle; + // If handle already exists, finds a unique handle by incrementing a number until it is not found in the array of existing page handles. + for (let handleIndex = 1; pageHandles.includes(newHandle); handleIndex++) { + newHandle = `${handle}-${handleIndex}`; + } + + const copyOfAppDefinition = JSON.parse(JSON.stringify(appDefinition)); + const newPageId = uuid(); + + copyOfAppDefinition.pages[newPageId] = { + name, + handle: newHandle, + components: {}, + }; + + setCurrentPageId(newPageId); + + appDefinitionChanged(copyOfAppDefinition, { pageDefinitionChanged: true, switchPage: true, pageId: newPageId }); + }; + + const switchPage = (pageId, queryParams = []) => { + // document.getElementById('real-canvas').scrollIntoView(); + if (currentPageId === pageId && currentState.page.handle === appDefinition?.pages[pageId]?.handle) { + return; + } + const { name, handle, events } = appDefinition.pages[pageId]; + + if (!name || !handle) return; + + const copyOfAppDefinition = JSON.parse(JSON.stringify(appDefinition)); + const queryParamsString = queryParams.map(([key, value]) => `${key}=${value}`).join('&'); + + props?.navigate(`/${getWorkspaceId()}/apps/${appId}/${handle}?${queryParamsString}`); + + const { globals: existingGlobals } = currentState; + + const page = { + id: pageId, + name, + handle, + variables: copyOfAppDefinition.pages[pageId]?.variables ?? {}, + }; + + const globals = { + ...existingGlobals, + urlparams: JSON.parse(JSON.stringify(queryString.parse(queryParamsString))), + }; + useCurrentStateStore.getState().actions.setCurrentState({ globals, page }); + + setCurrentPageId(pageId); + handleInspectorView(); + + // computeComponentState(copyOfAppDefinition.pages[pageId]?.components ?? {}).then(async () => { + // for (const event of events ?? []) { + // await handleEvent(event.eventId, event); + // } + // }); + }; + + const deletePageRequest = (pageId, isHomePage = false, pageName = '') => { + setShowPageDeletionConfirmation({ + isOpen: true, + pageId, + isHomePage, + pageName, + }); + }; + + const cancelDeletePageRequest = () => { + setShowPageDeletionConfirmation({ + isOpen: false, + pageId: null, + isHomePage: false, + pageName: null, + }); + }; + + const executeDeletepageRequest = () => { + const pageId = showPageDeletionConfirmation.pageId; + const isHomePage = showPageDeletionConfirmation.isHomePage; + if (Object.keys(appDefinition.pages).length === 1) { + toast.error('You cannot delete the only page in your app.'); + return; + } + + setIsDeletingPage({ + isDeletingPage: true, + }); + + const copyOfAppDefinition = JSON.parse(JSON.stringify(appDefinition)); + + const toBeDeletedPage = copyOfAppDefinition.pages[pageId]; + + const newAppDefinition = { + ...copyOfAppDefinition, + pages: omit(copyOfAppDefinition.pages, pageId), + }; + + const newCurrentPageId = isHomePage ? Object.keys(copyOfAppDefinition.pages)[0] : copyOfAppDefinition.homePageId; + + setCurrentPageId(newCurrentPageId); + updateEditorState({ + isSaving: true, + }); + setIsDeletingPage(false); + + appDefinitionChanged(newAppDefinition, { + pageDefinitionChanged: true, + }); + + toast.success(`${toBeDeletedPage.name} page deleted.`); + + switchPage(newCurrentPageId); + }; + // !------- const currentState = props?.currentState; @@ -1055,6 +1207,15 @@ const EditorComponent = (props) => { return (
+ executeDeletepageRequest()} + onCancel={() => cancelDeletePageRequest()} + darkMode={props.darkMode} + /> {props.isVersionReleased && } { ref={dataSourceModalRef} isSaving={isSaving} currentPageId={currentPageId} - // addNewPage={addNewPage} - // switchPage={switchPage} - // deletePage={deletePageRequest} - // renamePage={renamePage} + addNewPage={addNewPage} + switchPage={switchPage} + deletePage={deletePageRequest} + renamePage={renamePage} // clonePage={clonePage} // hidePage={hidePage} // unHidePage={unHidePage} diff --git a/frontend/src/_helpers/appUtils.js b/frontend/src/_helpers/appUtils.js index 1a97a53e9b..06fa159639 100644 --- a/frontend/src/_helpers/appUtils.js +++ b/frontend/src/_helpers/appUtils.js @@ -1163,6 +1163,7 @@ export function computeComponentState(components = {}) { }; } }); + useCurrentStateStore.getState().actions.setCurrentState({ components: { ...componentState, From 8ed4be52624df38b2cd1fb0b46e2ac615d8236ec Mon Sep 17 00:00:00 2001 From: arpitnath Date: Sun, 6 Aug 2023 17:11:35 +0530 Subject: [PATCH 009/704] fixed page actions: hide, unhide, clone, settingHomePage --- frontend/src/Editor/EditorFunc.jsx | 144 ++++++++++++++++++++++++++++- 1 file changed, 139 insertions(+), 5 deletions(-) diff --git a/frontend/src/Editor/EditorFunc.jsx b/frontend/src/Editor/EditorFunc.jsx index 2db3c36366..57452162c3 100644 --- a/frontend/src/Editor/EditorFunc.jsx +++ b/frontend/src/Editor/EditorFunc.jsx @@ -2,7 +2,18 @@ import React, { useEffect, useLayoutEffect, useRef, useState } from 'react'; import { appService, authenticationService, appVersionService, orgEnvironmentVariableService } from '@/_services'; import { DndProvider } from 'react-dnd'; import { HTML5Backend } from 'react-dnd-html5-backend'; -import _, { defaults, cloneDeep, isEqual, isEmpty, debounce, omit, update, difference } from 'lodash'; +import _, { + defaults, + cloneDeep, + isEqual, + isEmpty, + debounce, + omit, + update, + difference, + isNull, + isUndefined, +} from 'lodash'; import { Container } from './Container'; import { EditorKeyHooks } from './EditorKeyHooks'; import { CustomDragLayer } from './CustomDragLayer'; @@ -731,6 +742,10 @@ const EditorComponent = (props) => { updatedAppDefinition.pages = newDefinition.pages; } + if (opts?.homePageChanged) { + updatedAppDefinition.homePageId = newDefinition.homePageId; + } + const diffPatches = diff(appDefinition, updatedAppDefinition); const shouldUpdate = !_.isEmpty(diffPatches) && !isEqual(appDefinitionDiff, diffPatches); @@ -1185,6 +1200,125 @@ const EditorComponent = (props) => { switchPage(newCurrentPageId); }; + const hidePage = (pageId) => { + updateEditorState({ + isSaving: true, + }); + + const copyOfAppDefinition = JSON.parse(JSON.stringify(appDefinition)); + + const newAppDefinition = _.cloneDeep(copyOfAppDefinition); + + newAppDefinition.pages[pageId].hidden = true; + + appDefinitionChanged(newAppDefinition, { + pageDefinitionChanged: true, + }); + }; + + const unHidePage = (pageId) => { + updateEditorState({ + isSaving: true, + }); + + const copyOfAppDefinition = JSON.parse(JSON.stringify(appDefinition)); + + const newAppDefinition = _.cloneDeep(copyOfAppDefinition); + + newAppDefinition.pages[pageId].hidden = false; + + appDefinitionChanged(newAppDefinition, { + pageDefinitionChanged: true, + }); + }; + + const clonePage = (pageId) => { + const copyOfAppDefinition = JSON.parse(JSON.stringify(appDefinition)); + + updateEditorState({ + isSaving: true, + }); + + const currentPage = copyOfAppDefinition.pages[pageId]; + const newPageId = uuid(); + let newPageName = `${currentPage.name} (copy)`; + let newPageHandle = `${currentPage.handle}-copy`; + let i = 1; + while ( + !isNull(copyOfAppDefinition.pages[pageId]?.pages) && + !isUndefined(copyOfAppDefinition.pages[pageId]?.pages) && + Object.values(copyOfAppDefinition.pages[pageId]?.pages)?.some((page) => page.handle === newPageHandle) + ) { + newPageName = `${currentPage.name} (copy ${i})`; + newPageHandle = `${currentPage.handle}-copy-${i}`; + i++; + } + + const newPageData = cloneDeep(currentPage); + const oldToNewIdMapping = {}; + if (!isEmpty(currentPage?.components)) { + newPageData.components = Object.keys(newPageData.components).reduce((acc, key) => { + const newComponentId = uuid(); + acc[newComponentId] = newPageData.components[key]; + acc[newComponentId].id = newComponentId; + oldToNewIdMapping[key] = newComponentId; + return acc; + }, {}); + + Object.values(newPageData.components).map((comp) => { + if (comp.parent) { + let newParentId = oldToNewIdMapping[comp.parent]; + if (newParentId) { + comp.parent = newParentId; + } else { + const oldParentId = Object.keys(oldToNewIdMapping).find( + (parentId) => + comp.parent.startsWith(parentId) && + ['Tabs', 'Calendar'].includes(currentPage?.components[parentId]?.component?.component) + ); + const childTabId = comp.parent.split('-').at(-1); + comp.parent = `${oldToNewIdMapping[oldParentId]}-${childTabId}`; + } + } + return comp; + }); + } + + const newPage = { + ...newPageData, + name: newPageName, + handle: newPageHandle, + }; + + const newAppDefinition = { + ...copyOfAppDefinition, + pages: { + ...copyOfAppDefinition.pages, + [newPageId]: newPage, + }, + }; + + appDefinitionChanged(newAppDefinition, { + pageDefinitionChanged: true, + }); + }; + + const updateHomePage = (pageId) => { + updateEditorState({ + isSaving: true, + }); + + const copyOfAppDefinition = JSON.parse(JSON.stringify(appDefinition)); + + const newAppDefinition = _.cloneDeep(copyOfAppDefinition); + + newAppDefinition.homePageId = pageId; + + appDefinitionChanged(newAppDefinition, { + homePageChanged: true, + }); + }; + // !------- const currentState = props?.currentState; @@ -1268,10 +1402,10 @@ const EditorComponent = (props) => { switchPage={switchPage} deletePage={deletePageRequest} renamePage={renamePage} - // clonePage={clonePage} - // hidePage={hidePage} - // unHidePage={unHidePage} - // updateHomePage={updateHomePage} + clonePage={clonePage} + hidePage={hidePage} + unHidePage={unHidePage} + updateHomePage={updateHomePage} // updatePageHandle={updatePageHandle} // updateOnPageLoadEvents={updateOnPageLoadEvents} // showHideViewerNavigationControls={showHideViewerNavigation} From 3ba9b0d2f41222a2d80204064ff2ecb3ea40529f Mon Sep 17 00:00:00 2001 From: arpitnath Date: Sun, 6 Aug 2023 17:15:46 +0530 Subject: [PATCH 010/704] fixed page actions: updatePageHandle --- frontend/src/Editor/EditorFunc.jsx | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/frontend/src/Editor/EditorFunc.jsx b/frontend/src/Editor/EditorFunc.jsx index 57452162c3..22972cf2a4 100644 --- a/frontend/src/Editor/EditorFunc.jsx +++ b/frontend/src/Editor/EditorFunc.jsx @@ -1319,6 +1319,34 @@ const EditorComponent = (props) => { }); }; + const updatePageHandle = (pageId, newHandle) => { + const copyOfAppDefinition = JSON.parse(JSON.stringify(appDefinition)); + + updateEditorState({ + isSaving: true, + }); + + const pageExists = Object.values(copyOfAppDefinition.pages).some((page) => page.handle === newHandle); + + if (pageExists) { + toast.error('Page with same handle already exists'); + return; + } + + if (newHandle.trim().length === 0) { + toast.error('Page handle cannot be empty'); + return; + } + + const newDefinition = _.cloneDeep(copyOfAppDefinition); + + newDefinition.pages[pageId].handle = newHandle; + + appDefinitionChanged(newDefinition, { + pageDefinitionChanged: true, + }); + }; + // !------- const currentState = props?.currentState; @@ -1406,7 +1434,7 @@ const EditorComponent = (props) => { hidePage={hidePage} unHidePage={unHidePage} updateHomePage={updateHomePage} - // updatePageHandle={updatePageHandle} + updatePageHandle={updatePageHandle} // updateOnPageLoadEvents={updateOnPageLoadEvents} // showHideViewerNavigationControls={showHideViewerNavigation} // updateOnSortingPages={updateOnSortingPages} From 97c8d9bf9a5964168c8746060944a99586f4cf6e Mon Sep 17 00:00:00 2001 From: arpitnath Date: Mon, 7 Aug 2023 01:22:54 +0530 Subject: [PATCH 011/704] fixed page actions: sorting pages --- frontend/src/Editor/EditorFunc.jsx | 25 +++++++++++++++++++++-- frontend/src/Editor/LeftSidebar/index.jsx | 7 +++++-- 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/frontend/src/Editor/EditorFunc.jsx b/frontend/src/Editor/EditorFunc.jsx index 22972cf2a4..b0198fc052 100644 --- a/frontend/src/Editor/EditorFunc.jsx +++ b/frontend/src/Editor/EditorFunc.jsx @@ -86,6 +86,7 @@ const defaultDefinition = (darkMode) => { components: {}, handle: 'home', name: 'Home', + index: 0, }, }, globalSettings: { @@ -1098,6 +1099,7 @@ const EditorComponent = (props) => { name, handle: newHandle, components: {}, + index: Object.keys(copyOfAppDefinition.pages).length, }; setCurrentPageId(newPageId); @@ -1347,6 +1349,25 @@ const EditorComponent = (props) => { }); }; + const updateOnSortingPages = (newSortedPages) => { + const copyOfAppDefinition = JSON.parse(JSON.stringify(appDefinition)); + const pagesObj = newSortedPages.reduce((acc, page, index) => { + acc[page.id] = { + ...page, + index: index, + }; + return acc; + }, {}); + + const newAppDefinition = _.cloneDeep(copyOfAppDefinition); + + newAppDefinition.pages = pagesObj; + + appDefinitionChanged(newAppDefinition, { + pageDefinitionChanged: true, + }); + }; + // !------- const currentState = props?.currentState; @@ -1383,7 +1404,7 @@ const EditorComponent = (props) => { { updatePageHandle={updatePageHandle} // updateOnPageLoadEvents={updateOnPageLoadEvents} // showHideViewerNavigationControls={showHideViewerNavigation} - // updateOnSortingPages={updateOnSortingPages} + updateOnSortingPages={updateOnSortingPages} setEditorMarginLeft={handleEditorMarginLeftChange} /> {!props.showComments && ( diff --git a/frontend/src/Editor/LeftSidebar/index.jsx b/frontend/src/Editor/LeftSidebar/index.jsx index 9f353cb85a..87c5329a73 100644 --- a/frontend/src/Editor/LeftSidebar/index.jsx +++ b/frontend/src/Editor/LeftSidebar/index.jsx @@ -28,7 +28,6 @@ export const LeftSidebar = forwardRef((props, ref) => { dataSourcesChanged, globalDataSourcesChanged, dataQueriesChanged, - appDefinition, setSelectedComponent, removeComponent, @@ -145,7 +144,11 @@ 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} From 690bd8f260d0a125551cd870b0df986ebffa5fdc Mon Sep 17 00:00:00 2001 From: arpitnath Date: Mon, 7 Aug 2023 01:51:55 +0530 Subject: [PATCH 012/704] fixes: page events --- frontend/src/Editor/EditorFunc.jsx | 41 +++++++++++++++++++++++++----- 1 file changed, 34 insertions(+), 7 deletions(-) diff --git a/frontend/src/Editor/EditorFunc.jsx b/frontend/src/Editor/EditorFunc.jsx index b0198fc052..1f10b81447 100644 --- a/frontend/src/Editor/EditorFunc.jsx +++ b/frontend/src/Editor/EditorFunc.jsx @@ -747,6 +747,10 @@ const EditorComponent = (props) => { updatedAppDefinition.homePageId = newDefinition.homePageId; } + if (opts?.generalAppDefinitionChanged) { + updatedAppDefinition = newDefinition; + } + const diffPatches = diff(appDefinition, updatedAppDefinition); const shouldUpdate = !_.isEmpty(diffPatches) && !isEqual(appDefinitionDiff, diffPatches); @@ -1139,11 +1143,11 @@ const EditorComponent = (props) => { setCurrentPageId(pageId); handleInspectorView(); - // computeComponentState(copyOfAppDefinition.pages[pageId]?.components ?? {}).then(async () => { - // for (const event of events ?? []) { - // await handleEvent(event.eventId, event); - // } - // }); + (async () => { + for (const event of events ?? []) { + await handleEvent(event.eventId, event); + } + })(); }; const deletePageRequest = (pageId, isHomePage = false, pageName = '') => { @@ -1368,6 +1372,29 @@ const EditorComponent = (props) => { }); }; + const showHideViewerNavigation = () => { + const copyOfAppDefinition = JSON.parse(JSON.stringify(appDefinition)); + const newAppDefinition = _.cloneDeep(copyOfAppDefinition); + + newAppDefinition.showViewerNavigation = !newAppDefinition.showViewerNavigation; + + appDefinitionChanged(newAppDefinition, { + generalAppDefinitionChanged: true, + }); + }; + + const updateOnPageLoadEvents = (pageId, events) => { + const copyOfAppDefinition = JSON.parse(JSON.stringify(appDefinition)); + + const newAppDefinition = _.cloneDeep(copyOfAppDefinition); + + newAppDefinition.pages[pageId].events = events; + + appDefinitionChanged(newAppDefinition, { + pageDefinitionChanged: true, + }); + }; + // !------- const currentState = props?.currentState; @@ -1456,8 +1483,8 @@ const EditorComponent = (props) => { unHidePage={unHidePage} updateHomePage={updateHomePage} updatePageHandle={updatePageHandle} - // updateOnPageLoadEvents={updateOnPageLoadEvents} - // showHideViewerNavigationControls={showHideViewerNavigation} + updateOnPageLoadEvents={updateOnPageLoadEvents} + showHideViewerNavigationControls={showHideViewerNavigation} updateOnSortingPages={updateOnSortingPages} setEditorMarginLeft={handleEditorMarginLeftChange} /> From e556715e5f4792a6cf76953346a02a321740e044 Mon Sep 17 00:00:00 2001 From: arpitnath Date: Mon, 7 Aug 2023 01:57:53 +0530 Subject: [PATCH 013/704] fixes: global settings updates --- frontend/src/Editor/EditorFunc.jsx | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/frontend/src/Editor/EditorFunc.jsx b/frontend/src/Editor/EditorFunc.jsx index 1f10b81447..4d1e7e9c02 100644 --- a/frontend/src/Editor/EditorFunc.jsx +++ b/frontend/src/Editor/EditorFunc.jsx @@ -602,21 +602,28 @@ const EditorComponent = (props) => { const handleEditorMarginLeftChange = (value) => setEditorMarginLeft(value); const globalSettingsChanged = (key, value) => { - if (value?.[1]?.a == undefined) appDefinition.globalSettings[key] = value; + const copyOfAppDefinition = JSON.parse(JSON.stringify(appDefinition)); + const newAppDefinition = _.cloneDeep(copyOfAppDefinition); + + if (value?.[1]?.a == undefined) newAppDefinition.globalSettings[key] = value; else { const hexCode = `${value?.[0]}${decimalToHex(value?.[1]?.a)}`; - appDefinition.globalSettings[key] = hexCode; + newAppDefinition.globalSettings[key] = hexCode; } updateEditorState({ isSaving: true, - appDefinition, + // appDefinition, }); - props.ymap?.set('appDef', { - newDefinition: appDefinition, - editingVersionId: props.editingVersion?.id, + appDefinitionChanged(newAppDefinition, { + globalSettings: true, }); + + // props.ymap?.set('appDef', { + // newDefinition: appDefinition, + // editingVersionId: props.editingVersion?.id, + // }); // autoSave(); }; @@ -747,7 +754,7 @@ const EditorComponent = (props) => { updatedAppDefinition.homePageId = newDefinition.homePageId; } - if (opts?.generalAppDefinitionChanged) { + if (opts?.generalAppDefinitionChanged || opts?.globalSettings) { updatedAppDefinition = newDefinition; } @@ -768,6 +775,10 @@ const EditorComponent = (props) => { }); updateAppDefinitionDiff(diffPatches); computeComponentState(updatedAppDefinition.pages[currentPageId]?.components); + props.ymap?.set('appDef', { + newDefinition: updatedAppDefinition, + editingVersionId: props.editingVersion?.id, + }); } }; From 453405d7f2621acfe7b2dc0cee6d89ca37389d88 Mon Sep 17 00:00:00 2001 From: arpitnath Date: Wed, 9 Aug 2023 03:41:16 +0530 Subject: [PATCH 014/704] fixes: undo/redo actions --- frontend/src/Editor/EditorFunc.jsx | 187 ++++++++++++++--------------- 1 file changed, 93 insertions(+), 94 deletions(-) diff --git a/frontend/src/Editor/EditorFunc.jsx b/frontend/src/Editor/EditorFunc.jsx index 4d1e7e9c02..df2bd818dd 100644 --- a/frontend/src/Editor/EditorFunc.jsx +++ b/frontend/src/Editor/EditorFunc.jsx @@ -129,7 +129,6 @@ const EditorComponent = (props) => { const { isMaintenanceOn, appId, app, currentUser, currentVersionId, appDefinitionDiff } = useAppInfo(); - const [currentVersionChanges, setCurrentVersionChanges] = useState({}); const [currentPageId, setCurrentPageId] = useState(null); const [zoomLevel, setZoomLevel] = useState(1); const [isQueryPaneDragging, setIsQueryPaneDragging] = useState(false); @@ -143,6 +142,9 @@ const EditorComponent = (props) => { const [showPageDeletionConfirmation, setShowPageDeletionConfirmation] = useState(null); const [isDeletingPage, setIsDeletingPage] = useState(false); + const [undoStack, setUndoStack] = useState([]); + const [redoStack, setRedoStack] = useState([]); + // refs const canvasContainerRef = useRef(null); const dataSourceModalRef = useRef(null); @@ -280,7 +282,7 @@ const EditorComponent = (props) => { const currentVersion = { [currentPageId]: -1, }; - setCurrentVersionChanges({}); + updateEditorState({ canUndo: false, canRedo: false, @@ -714,7 +716,14 @@ const EditorComponent = (props) => { } }; - const appDefinitionChanged = (newDefinition, opts = {}) => { + const diffToPatches = (diffObj) => { + return Object.keys(diffObj).reduce((patches, path) => { + const value = diffObj[path]; + return [...patches, { path: path.split('.'), value, op: 'replace' }]; + }, []); + }; + + const appDefinitionChanged = async (newDefinition, opts = {}) => { if (config.ENABLE_MULTIPLAYER_EDITING && !opts.skipYmapUpdate) { props.ymap?.set('appDef', { newDefinition, @@ -735,45 +744,51 @@ const EditorComponent = (props) => { } let updatedAppDefinition; + const copyOfAppDefinition = JSON.parse(JSON.stringify(appDefinition)); - updatedAppDefinition = _.cloneDeep(appDefinition); - - if (_.isEmpty(updatedAppDefinition)) return; - - if (opts?.containerChanges || opts?.componentDefinitionChanged) { - const currentPageComponents = newDefinition.pages[currentPageId]?.components; - - updatedAppDefinition.pages[currentPageId].components = currentPageComponents; - } - - if (opts?.pageDefinitionChanged) { - updatedAppDefinition.pages = newDefinition.pages; - } - - if (opts?.homePageChanged) { - updatedAppDefinition.homePageId = newDefinition.homePageId; - } - - if (opts?.generalAppDefinitionChanged || opts?.globalSettings) { - updatedAppDefinition = newDefinition; - } - - const diffPatches = diff(appDefinition, updatedAppDefinition); - const shouldUpdate = !_.isEmpty(diffPatches) && !isEqual(appDefinitionDiff, diffPatches); - - console.log('--arpit | appDefinitionChanged func() | diffPatches', { - diffPatches, - appDefinitionDiff, - shouldUpdate, + console.log('--arpit | appDefinitionChanged func()', { opts, + newDefinition, }); + updatedAppDefinition = produce(copyOfAppDefinition, (draft) => { + if (_.isEmpty(draft)) return; + + if (opts?.containerChanges || opts?.componentDefinitionChanged) { + const currentPageComponents = newDefinition.pages[currentPageId]?.components; + + draft.pages[currentPageId].components = currentPageComponents; + } + + if (opts?.pageDefinitionChanged) { + draft.pages = newDefinition.pages; + } + + if (opts?.homePageChanged) { + draft.homePageId = newDefinition.homePageId; + } + + if (opts?.generalAppDefinitionChanged || opts?.globalSettings) { + Object.assign(draft, newDefinition); + } + }); + + const diffPatches = diff(appDefinition, updatedAppDefinition); + const inversePatches = diff(updatedAppDefinition, appDefinition); + const shouldUpdate = !_.isEmpty(diffPatches) && !isEqual(appDefinitionDiff, diffPatches); + if (shouldUpdate) { + // const redoPatch = diffToPatches(diffPatches); + const undoPatches = diffToPatches(inversePatches); + + setUndoStack((prev) => [...prev, undoPatches]); + + updateAppDefinitionDiff(diffPatches); updateEditorState({ isSaving: true, appDefinition: updatedAppDefinition, }); - updateAppDefinitionDiff(diffPatches); + computeComponentState(updatedAppDefinition.pages[currentPageId]?.components); props.ymap?.set('appDef', { newDefinition: updatedAppDefinition, @@ -825,99 +840,83 @@ const EditorComponent = (props) => { const realtimeSave = debounce(appDefinitionChanged, 500); const autoSave = debounce(saveEditingVersion, 3000); - const handleAddPatch = (patches, inversePatches) => { - if (isEmpty(patches) && isEmpty(inversePatches)) return; - if (isEqual(patches, inversePatches)) return; + function handlePaths(prevPatch, path = [], appJSON) { + const paths = [...path]; - const currentPage = currentPageId; - const _currentVersion = currentVersion[currentPage] ?? -1; + for (let key in prevPatch) { + const type = typeof prevPatch[key]; - let _currentVersionChanges = {}; - - _currentVersionChanges[currentPage] = currentVersionChanges[currentPage] ?? {}; - - _currentVersionChanges[currentPage][_currentVersion] = { - redo: patches, - undo: inversePatches, - }; - - setCurrentVersionChanges(_currentVersionChanges); - - const _canUndo = _currentVersionChanges[currentPage].hasOwnProperty(_currentVersion); - const _canRedo = _currentVersionChanges[currentPage].hasOwnProperty(_currentVersion + 1); - - _currentVersion[currentPage] = currentVersion + 1; - - delete _currentVersionChanges[currentPage][_currentVersion + 1]; - delete _currentVersionChanges[currentPage][_currentVersion - noOfVersionsSupported]; - - setCurrentVersionChanges(_currentVersionChanges); - - updateEditorState({ - canUndo: _canUndo, - canRedo: _canRedo, - currentVersion: _currentVersion, + if (type === 'object') { + handlePaths(prevPatch[key], [...paths, key], appJSON); + } else { + const currentpath = [...paths, key].join('.'); + _.update(appJSON, currentpath, () => prevPatch[key]); + } + } + } + function removeUndefined(obj) { + Object.keys(obj).forEach((key) => { + if (obj[key] && typeof obj[key] === 'object') removeUndefined(obj[key]); + else if (obj[key] === undefined) delete obj[key]; }); - }; + + return obj; + } const handleUndo = () => { if (canUndo) { - let _currentVersion = currentVersion[currentPageId]; + const patchesToUndo = undoStack[undoStack.length - 1]; - const _appDefinition = applyPatches(appDefinition, currentVersionChanges[currentPageId][currentVersion - 1].undo); + const updatedAppDefinition = JSON.parse(JSON.stringify(appDefinition)); - const _canUndo = currentVersionChanges[currentPageId].hasOwnProperty(currentVersion - 1); - const _canRedo = true; - _currentVersion[currentPageId] = _currentVersion - 1; + handlePaths(patchesToUndo[0]?.value, [...patchesToUndo[0].path], updatedAppDefinition); - if (!_appDefinition) return; + removeUndefined(updatedAppDefinition); + + const _diffPatches = diff(updatedAppDefinition, appDefinition); + + setUndoStack((prev) => prev.slice(0, prev.length - 1)); + setRedoStack((prev) => [...prev, diffToPatches(_diffPatches)]); updateEditorState({ - appDefinition: _appDefinition, - canUndo: _canUndo, - canRedo: _canRedo, - currentVersion: _currentVersion, + appDefinition: updatedAppDefinition, + currentSidebarTab: 2, isSaving: true, }); - props.ymap?.set('appDef', { - newDefinition: appDefinition, - editingVersionId: props.editingVersion?.id, - }); - // autoSave(); } }; const handleRedo = () => { if (canRedo) { - let _currentVersion = currentVersion[currentPageId]; + const patchesToRedo = redoStack[redoStack.length - 1]; - const _appDefinition = applyPatches(appDefinition, currentVersionChanges[currentPageId][currentVersion].redo); + const updatedAppDefinition = JSON.parse(JSON.stringify(appDefinition)); - const _canUndo = true; - const _canRedo = currentVersionChanges[currentPageId].hasOwnProperty(currentVersion + 1); - _currentVersion[currentPageId] = currentVersion + 1; + handlePaths(patchesToRedo[0]?.value, [...patchesToRedo[0].path], updatedAppDefinition); - if (!_appDefinition) return; + const _diffPatches = diff(updatedAppDefinition, appDefinition); + + setRedoStack((prev) => prev.slice(0, prev.length - 1)); + setUndoStack((prev) => [...prev, diffToPatches(_diffPatches)]); updateEditorState({ - appDefinition: _appDefinition, - canUndo: _canUndo, - canRedo: _canRedo, - currentVersion: _currentVersion, + appDefinition: updatedAppDefinition, isSaving: true, }); - props.ymap?.set('appDef', { - newDefinition: appDefinition, - editingVersionId: props.editingVersion?.id, - }); - // autoSave(); } }; + useEffect(() => { + updateEditorState({ + canUndo: undoStack.length > 0, + canRedo: redoStack.length > 0, + }); + }, [JSON.stringify(undoStack), JSON.stringify(redoStack)]); + const componentDefinitionChanged = (componentDefinition, props) => { if (props?.isVersionReleased) { useAppVersionStore.getState().actions.enableReleasedVersionPopupState(); From 9770df8b3fa7ec836e38608b2ed6d991e8fa93bc Mon Sep 17 00:00:00 2001 From: arpitnath Date: Wed, 9 Aug 2023 19:09:52 +0530 Subject: [PATCH 015/704] remove widget/component(s): fixed --- frontend/src/Editor/ConfigHandle.jsx | 2 +- frontend/src/Editor/EditorFunc.jsx | 22 ++++++++++------------ frontend/src/_helpers/appUtils.js | 4 +++- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/frontend/src/Editor/ConfigHandle.jsx b/frontend/src/Editor/ConfigHandle.jsx index 36769ffe17..669c4ffa1d 100644 --- a/frontend/src/Editor/ConfigHandle.jsx +++ b/frontend/src/Editor/ConfigHandle.jsx @@ -56,7 +56,7 @@ export const ConfigHandle = function ConfigHandle({ role="button" height="12" draggable="false" - onClick={() => removeComponent({ id })} + onClick={() => removeComponent(id)} data-cy={`${component.name.toLowerCase()}-delete-button`} className="delete-icon" /> diff --git a/frontend/src/Editor/EditorFunc.jsx b/frontend/src/Editor/EditorFunc.jsx index df2bd818dd..22b0aab833 100644 --- a/frontend/src/Editor/EditorFunc.jsx +++ b/frontend/src/Editor/EditorFunc.jsx @@ -768,7 +768,7 @@ const EditorComponent = (props) => { draft.homePageId = newDefinition.homePageId; } - if (opts?.generalAppDefinitionChanged || opts?.globalSettings) { + if (opts?.generalAppDefinitionChanged || opts?.globalSettings || isEmpty(opts)) { Object.assign(draft, newDefinition); } }); @@ -952,20 +952,20 @@ const EditorComponent = (props) => { // } }; - const removeComponent = (component) => { + const removeComponent = (componentId) => { if (!props.isVersionReleased) { let newDefinition = cloneDeep(appDefinition); // Delete child components when parent is deleted let childComponents = []; - if (newDefinition.pages[currentPageId].components?.[component.id].component.component === 'Tabs') { + if (newDefinition.pages[currentPageId].components?.[componentId].component.component === 'Tabs') { childComponents = Object.keys(newDefinition.pages[currentPageId].components).filter((key) => - newDefinition.pages[currentPageId].components[key].parent?.startsWith(component.id) + newDefinition.pages[currentPageId].components[key].parent?.startsWith(componentId) ); } else { childComponents = Object.keys(newDefinition.pages[currentPageId].components).filter( - (key) => newDefinition.pages[currentPageId].components[key].parent === component.id + (key) => newDefinition.pages[currentPageId].components[key].parent === componentId ); } @@ -973,7 +973,7 @@ const EditorComponent = (props) => { delete newDefinition.pages[currentPageId].components[componentId]; }); - delete newDefinition.pages[currentPageId].components[component.id]; + delete newDefinition.pages[currentPageId].components[componentId]; const platform = navigator?.userAgentData?.platform || navigator?.platform || 'unknown'; if (platform.toLowerCase().indexOf('mac') > -1) { toast('Component deleted! (⌘ + Z to undo)', { @@ -985,7 +985,7 @@ const EditorComponent = (props) => { }); } appDefinitionChanged(newDefinition, { - skipAutoSave: props.isVersionReleased, + componentDefinitionChanged: true, }); handleInspectorView(); } else { @@ -1049,10 +1049,10 @@ const EditorComponent = (props) => { }; const removeComponents = () => { - if (!props.isVersionReleased && selectedComponents?.length > 1) { + if (!props.isVersionReleased && selectedComponents?.length >= 1) { let newDefinition = cloneDeep(appDefinition); - removeSelectedComponent(currentPageId, newDefinition, selectedComponents); + removeSelectedComponent(currentPageId, newDefinition, selectedComponents, appDefinitionChanged); const platform = navigator?.userAgentData?.platform || navigator?.platform || 'unknown'; if (platform.toLowerCase().indexOf('mac') > -1) { toast('Selected components deleted! (⌘ + Z to undo)', { @@ -1063,9 +1063,7 @@ const EditorComponent = (props) => { icon: '🗑️', }); } - appDefinitionChanged(newDefinition, { - skipAutoSave: props.isVersionReleased, - }); + // appDefinitionChanged(newDefinition); handleInspectorView(); } else if (props.isVersionReleased) { useAppVersionStore.getState().actions.enableReleasedVersionPopupState(); diff --git a/frontend/src/_helpers/appUtils.js b/frontend/src/_helpers/appUtils.js index 06fa159639..c3c21b7cd5 100644 --- a/frontend/src/_helpers/appUtils.js +++ b/frontend/src/_helpers/appUtils.js @@ -1565,7 +1565,7 @@ export function snapToGrid(canvasWidth, x, y) { const snappedY = Math.round(y / 10) * 10; return [snappedX, snappedY]; } -export const removeSelectedComponent = (pageId, newDefinition, selectedComponents) => { +export const removeSelectedComponent = (pageId, newDefinition, selectedComponents, updateAppDefinition) => { selectedComponents.forEach((component) => { let childComponents = []; @@ -1585,6 +1585,8 @@ export const removeSelectedComponent = (pageId, newDefinition, selectedComponent delete newDefinition.pages[pageId].components[component.id]; }); + + updateAppDefinition(newDefinition, { componentDefinitionChanged: true }); }; const getSelectedText = () => { From 537c423e06fe10429af0faa26bddcaeb324e0e05 Mon Sep 17 00:00:00 2001 From: arpitnath Date: Wed, 9 Aug 2023 19:32:38 +0530 Subject: [PATCH 016/704] fixes: copy/paste --- frontend/src/Editor/EditorKeyHooks.jsx | 2 ++ frontend/src/_helpers/appUtils.js | 12 ++++++------ 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/frontend/src/Editor/EditorKeyHooks.jsx b/frontend/src/Editor/EditorKeyHooks.jsx index 1f9b6cb797..df79bf779b 100644 --- a/frontend/src/Editor/EditorKeyHooks.jsx +++ b/frontend/src/Editor/EditorKeyHooks.jsx @@ -10,6 +10,7 @@ export const EditorKeyHooks = ({ removeMultipleComponents, }) => { const handleHotKeysCallback = (key) => { + console.log('---arpit-- hotkeys', { key }); switch (key) { case 'Escape': handleEditorEscapeKeyPress(); @@ -18,6 +19,7 @@ export const EditorKeyHooks = ({ removeMultipleComponents(); break; case 'KeyD': + console.log('---arpit-- paste component'); cloneComponents(); break; case 'KeyC': diff --git a/frontend/src/_helpers/appUtils.js b/frontend/src/_helpers/appUtils.js index c3c21b7cd5..75faf228c4 100644 --- a/frontend/src/_helpers/appUtils.js +++ b/frontend/src/_helpers/appUtils.js @@ -575,7 +575,7 @@ function executeActionWithDebounce(_ref, event, mode, customVariables) { } case 'switch-page': { - _ref.switchPage(event.pageId, resolveReferences(event.queryParams, getCurrentState(), [], customVariables)); // arpit [switchPage] + _ref.switchPage(event.pageId, resolveReferences(event.queryParams, getCurrentState(), [], customVariables)); return Promise.resolve(); } } @@ -601,7 +601,7 @@ export async function onEvent(_ref, eventName, options, mode = 'edit') { }, }, }); - runQuery(_ref, queryId, queryName, true, mode, parameters); //arpit [runQuery] + runQuery(_ref, queryId, queryName, true, mode, parameters); } if (eventName === 'onCalendarEventSelect') { @@ -1314,7 +1314,8 @@ const updateNewComponents = (pageId, appDefinition, newComponents, updateAppDefi ); newAppDefinition.pages[pageId].components[newComponent.id] = newComponent; }); - updateAppDefinition(newAppDefinition, { addComponents: true }); + + updateAppDefinition(newAppDefinition, { componentDefinitionChanged: true }); }; export const cloneComponents = ( @@ -1352,13 +1353,13 @@ export const cloneComponents = ( isCut, }; } + if (isCloning) { addComponents(currentPageId, appDefinition, updateAppDefinition, undefined, newComponentObj); 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)); toast.success('Component copied succesfully'); @@ -1430,7 +1431,6 @@ const updateComponentLayout = (components, parentId, isCut = false) => { }; export const addComponents = (pageId, appDefinition, appDefinitionChanged, parentId = undefined, newComponentObj) => { - console.log({ pageId, newComponentObj }); const finalComponents = []; let parentComponent = undefined; const { isCloning, isCut, newComponents: pastedComponent = [] } = newComponentObj; From b132098c4ca87c72b797a4d3cb5434130e91e090 Mon Sep 17 00:00:00 2001 From: arpitnath Date: Thu, 10 Aug 2023 17:01:03 +0530 Subject: [PATCH 017/704] moved out pages from app definition --- frontend/src/Editor/Container.jsx | 16 +++++- frontend/src/Editor/EditorFunc.jsx | 28 ++++++---- frontend/src/_services/appVersion.service.js | 3 +- frontend/src/_stores/appDataStore.js | 11 +++- frontend/src/_stores/utils.js | 55 +++++++++++++++++++ ...> 1691004576222-UpdateAppVersionEntity.ts} | 2 +- .../1691004576333-CreatePageTable.ts | 15 ++--- server/src/controllers/apps.controller.ts | 16 ++++++ server/src/dto/version-edit.dto.ts | 3 + server/src/entities/component.entity.ts | 5 +- server/src/entities/page.entity.ts | 14 ++--- server/src/modules/apps/apps.module.ts | 3 + server/src/services/apps.service.ts | 34 +++++++++++- server/src/services/components.service.ts | 20 +++++++ 14 files changed, 189 insertions(+), 36 deletions(-) rename server/migrations/{1691006886775-UpdateAppVersionEntity.ts => 1691004576222-UpdateAppVersionEntity.ts} (94%) create mode 100644 server/src/services/components.service.ts diff --git a/frontend/src/Editor/Container.jsx b/frontend/src/Editor/Container.jsx index c4a1bce38b..74c812d46e 100644 --- a/frontend/src/Editor/Container.jsx +++ b/frontend/src/Editor/Container.jsx @@ -84,6 +84,7 @@ export const Container = ({ const [newThread, addNewThread] = useState({}); const [isContainerFocused, setContainerFocus] = useState(false); const [canvasHeight, setCanvasHeight] = useState(null); + const router = useRouter(); const canvasRef = useRef(null); const focusedParentIdRef = useRef(undefined); @@ -181,7 +182,20 @@ export const Container = ({ }, }; - appDefinitionChanged(newDefinition, { containerChanges: true }); + //need to check if a new component is added or deleted + + 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; + } + + appDefinitionChanged(newDefinition, opts); // eslint-disable-next-line react-hooks/exhaustive-deps }, [boxes]); diff --git a/frontend/src/Editor/EditorFunc.jsx b/frontend/src/Editor/EditorFunc.jsx index 22b0aab833..2e2e058c49 100644 --- a/frontend/src/Editor/EditorFunc.jsx +++ b/frontend/src/Editor/EditorFunc.jsx @@ -58,7 +58,7 @@ import { useDataQueries, useDataQueriesStore } from '@/_stores/dataQueriesStore' import { useAppVersionStore } from '@/_stores/appVersionStore'; import { useQueryPanelStore } from '@/_stores/queryPanelStore'; import { useCurrentStateStore, useCurrentState } from '@/_stores/currentStateStore'; -import { resetAllStores } from '@/_stores/utils'; +import { computeAppDiff, resetAllStores } from '@/_stores/utils'; import { setCookie } from '@/_helpers/cookie'; import { shallow } from 'zustand/shallow'; import { useEditorActions, useEditorState, useEditorStore } from '@/_stores/editorStore'; @@ -105,7 +105,7 @@ const EditorComponent = (props) => { const { socket } = createWebsocketConnection(props?.params?.id); const mounted = useMounted(); - const { updateState, updateAppDefinitionDiff } = useAppDataActions(); + const { updateState, updateAppDefinitionDiff, updateAppVersion } = useAppDataActions(); const { updateEditorState, updateQueryConfirmationList } = useEditorActions(); const { noOfVersionsSupported, @@ -127,7 +127,8 @@ const EditorComponent = (props) => { const dataQueries = useDataQueries(); - const { isMaintenanceOn, appId, app, currentUser, currentVersionId, appDefinitionDiff } = useAppInfo(); + const { isMaintenanceOn, appId, app, currentUser, currentVersionId, appDefinitionDiff, appDiffOptions } = + useAppInfo(); const [currentPageId, setCurrentPageId] = useState(null); const [zoomLevel, setZoomLevel] = useState(1); @@ -213,6 +214,7 @@ const EditorComponent = (props) => { if (currentUser?.current_organization_id) { fetchGlobalDataSources(); } + // eslint-disable-next-line react-hooks/exhaustive-deps }, [JSON.stringify(currentUser?.current_organization_id)]); // Handle appDefinition updates @@ -630,6 +632,7 @@ const EditorComponent = (props) => { }; //!-------- + const fetchApp = async (startingPageHandle) => { const _appId = props?.params?.id; @@ -638,7 +641,7 @@ const EditorComponent = (props) => { useAppVersionStore.getState().actions.updateReleasedVersionId(data.current_version_id); await fetchDataSources(data.editing_version?.id); await fetchDataQueries(data.editing_version?.id, true, true); - + console.log('---piku [fetching app]---]', { data, defaultDef: defaultDefinition(props.darkMode) }); let dataDefinition = data.definition ?? defaults(data.definition, defaultDefinition(props.darkMode)); const pages = Object.entries(dataDefinition.pages).map(([pageId, page]) => ({ id: pageId, ...page })); @@ -774,6 +777,7 @@ const EditorComponent = (props) => { }); const diffPatches = diff(appDefinition, updatedAppDefinition); + const inversePatches = diff(updatedAppDefinition, appDefinition); const shouldUpdate = !_.isEmpty(diffPatches) && !isEqual(appDefinitionDiff, diffPatches); @@ -784,6 +788,9 @@ const EditorComponent = (props) => { setUndoStack((prev) => [...prev, undoPatches]); updateAppDefinitionDiff(diffPatches); + updateState({ + appDiffOptions: opts, + }); updateEditorState({ isSaving: true, appDefinition: updatedAppDefinition, @@ -804,13 +811,11 @@ const EditorComponent = (props) => { isSaving: false, }); } else if (!isEmpty(props?.editingVersion)) { - appVersionService - .save( - appId, - props.editingVersion?.id, - { definition: appDefinition, diff: appDefinitionDiff }, - isUserSwitchedVersion - ) + const componentDiff = computeAppDiff(appDefinitionDiff, currentPageId, appDiffOptions); + + // console.log('---piku [componentDiff]--', componentDiff); + + updateAppVersion(appId, props.editingVersion?.id, appDefinition, componentDiff, isUserSwitchedVersion) .then(() => { const _editingVersion = { ...props.editingVersion, @@ -918,6 +923,7 @@ const EditorComponent = (props) => { }, [JSON.stringify(undoStack), JSON.stringify(redoStack)]); const componentDefinitionChanged = (componentDefinition, props) => { + console.log('---arpit checking:::: ', { props }); if (props?.isVersionReleased) { useAppVersionStore.getState().actions.enableReleasedVersionPopupState(); return; diff --git a/frontend/src/_services/appVersion.service.js b/frontend/src/_services/appVersion.service.js index fd73304379..26c6893851 100644 --- a/frontend/src/_services/appVersion.service.js +++ b/frontend/src/_services/appVersion.service.js @@ -44,11 +44,12 @@ function del(appId, versionId) { } function save(appId, versionId, values, isUserSwitchedVersion = false) { + console.log('---piku [version saved]', { values }); 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; - console.log('---arpit [app version service]', { values }); const requestOptions = { method: 'PUT', headers: authHeader(), diff --git a/frontend/src/_stores/appDataStore.js b/frontend/src/_stores/appDataStore.js index 5fa8a424ab..716672c656 100644 --- a/frontend/src/_stores/appDataStore.js +++ b/frontend/src/_stores/appDataStore.js @@ -1,4 +1,4 @@ -import { shallow } from 'zustand/shallow'; +import { appVersionService } from '@/_services'; import { create, zustandDevTools } from './utils'; const initialState = { @@ -19,6 +19,7 @@ const initialState = { layouts: [], eventHandlers: [], appDefinitionDiff: null, + appDiffOptions: {}, }; export const useAppDataStore = create( @@ -30,6 +31,14 @@ export const useAppDataStore = create( updateApps: (apps) => set(() => ({ apps: apps })), updateState: (state) => set((prev) => ({ ...prev, ...state })), updateAppDefinitionDiff: (appDefinitionDiff) => set(() => ({ appDefinitionDiff: appDefinitionDiff })), + updateAppVersion: async (appId, versionId, appDefinition, appDefinitionDiff, isUserSwitchedVersion = false) => { + return await appVersionService.save( + appId, + versionId, + { definition: appDefinition, diff: appDefinitionDiff }, + isUserSwitchedVersion + ); + }, }, }), { name: 'App Data Store' } diff --git a/frontend/src/_stores/utils.js b/frontend/src/_stores/utils.js index 8b883b726d..0d76eecfdf 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,54 @@ export const resetAllStores = () => { resetter(); } }; + +const defaultComponent = { + name: '', + properties: {}, + styles: {}, + validation: {}, + events: [], +}; + +const updateType = Object.freeze({ + pageDefinitionChanged: 'pages', + containerChanges: 'layout', + componentAdded: 'components', + componentDefinitionChanged: 'components', +}); + +export const computeAppDiff = (appDiff, currentPageId, opts) => { + let type; + let updateDiff; + + if (opts?.pageDefinitionChanged) { + updateDiff = appDiff?.pages[currentPageId]; + + type = updateType.pageDefinitionChanged; + } else if ((opts?.containerChanges || opts?.componentDefinitionChanged) && !opts?.componentAdded) { + const currentPageComponents = appDiff?.pages[currentPageId]?.components; + + updateDiff = currentPageComponents; + type = opts?.componentDefinitionChanged ? updateType.componentDefinitionChanged : updateType.containerChanges; + } else if (opts?.componentAdded) { + const currentPageComponents = appDiff?.pages[currentPageId]?.components; + + updateDiff = _.toPairs(currentPageComponents ?? []).reduce((result, [id, component]) => { + const componentMeta = componentTypes.find((comp) => comp.component === component.component.component); + + const metaDiff = diff(componentMeta, component.component); + + result[id] = _.defaultsDeep(metaDiff, defaultComponent); + + return result; + // result[id].componentId = id; + // return { ..._.defaultsDeep(metaDiff, defaultComponent), componentId: id }; + }, {}); + + type = updateType.componentDefinitionChanged; + } + + console.log('---piku [currentPageComponents]', { updateDiff, opts, type }); + + return { updateDiff, type }; +}; diff --git a/server/migrations/1691006886775-UpdateAppVersionEntity.ts b/server/migrations/1691004576222-UpdateAppVersionEntity.ts similarity index 94% rename from server/migrations/1691006886775-UpdateAppVersionEntity.ts rename to server/migrations/1691004576222-UpdateAppVersionEntity.ts index 91dc1162f0..c1e7fac828 100644 --- a/server/migrations/1691006886775-UpdateAppVersionEntity.ts +++ b/server/migrations/1691004576222-UpdateAppVersionEntity.ts @@ -1,6 +1,6 @@ import { MigrationInterface, QueryRunner, TableColumn } from 'typeorm'; -export class UpdateAppVersionEntity1691006886775 implements MigrationInterface { +export class UpdateAppVersionEntity1691006886222 implements MigrationInterface { public async up(queryRunner: QueryRunner): Promise { // Add the new columns to the app_versions table await queryRunner.addColumn( diff --git a/server/migrations/1691004576333-CreatePageTable.ts b/server/migrations/1691004576333-CreatePageTable.ts index 73a42c0c9e..98694a5e34 100644 --- a/server/migrations/1691004576333-CreatePageTable.ts +++ b/server/migrations/1691004576333-CreatePageTable.ts @@ -17,21 +17,16 @@ export class CreatePageTable1691004576333 implements MigrationInterface { type: 'varchar', isNullable: false, }, + { + name: 'page_handle', + type: 'varchar', + isNullable: false, + }, { name: 'app_version_id', type: 'uuid', isNullable: false, }, - { - name: 'update_id', - type: 'varchar', - isNullable: false, - }, - { - name: 'last_hashed_diff', - type: 'varchar', - isNullable: false, - }, ], }) ); diff --git a/server/src/controllers/apps.controller.ts b/server/src/controllers/apps.controller.ts index dc9a00d34b..022253f8e9 100644 --- a/server/src/controllers/apps.controller.ts +++ b/server/src/controllers/apps.controller.ts @@ -28,11 +28,14 @@ import { EntityManager } from 'typeorm'; import { ValidAppInterceptor } from 'src/interceptors/valid.app.interceptor'; import { AppDecorator } from 'src/decorators/app.decorator'; +import { ComponentsService } from '@services/components.service'; + @Controller('apps') export class AppsController { constructor( private appsService: AppsService, private foldersService: FoldersService, + private componentsService: ComponentsService, private appsAbilityFactory: AppsAbilityFactory ) {} @@ -79,11 +82,16 @@ export class AppsController { } const response = decamelizeKeys(app); + const seralizedQueries = []; const dataQueriesForVersion = app.editingVersion ? await this.appsService.findDataQueriesForVersion(app.editingVersion.id) : []; + const pagesForVersion = app.editingVersion ? await this.appsService.findPagesForVersion(app.editingVersion.id) : []; + + console.log('------arpit [pagesforVersion]', { pagesForVersion }); + // serialize queries for (const query of dataQueriesForVersion) { const decamelizedQuery = decamelizeKeys(query); @@ -93,6 +101,7 @@ export class AppsController { response['data_queries'] = seralizedQueries; response['definition'] = app.editingVersion?.definition; + response['pages'] = decamelizeKeys(pagesForVersion); //! if editing version exists, camelize the definition if (app.editingVersion && app.editingVersion.definition) { @@ -311,6 +320,13 @@ export class AppsController { throw new ForbiddenException('You do not have permissions to perform this action'); } + const updateType = versionEditDto.app_diff?.type; + console.log('----arpit apps controller => ', { updateType }); + + // if(updateType=== 'components') { + // await this.componentsService.createOrUpdate(versionEditDto.app_diff.components, versionId) + // } + await this.appsService.updateVersion(version, versionEditDto, app.organizationId); return; } diff --git a/server/src/dto/version-edit.dto.ts b/server/src/dto/version-edit.dto.ts index 5bbdf2a90c..305055af69 100644 --- a/server/src/dto/version-edit.dto.ts +++ b/server/src/dto/version-edit.dto.ts @@ -20,4 +20,7 @@ export class VersionEditDto { @IsOptional() @IsBoolean() is_user_switched_version: boolean; + + @IsOptional() + app_diff: any; } diff --git a/server/src/entities/component.entity.ts b/server/src/entities/component.entity.ts index 68649090f4..ec01f4baa5 100644 --- a/server/src/entities/component.entity.ts +++ b/server/src/entities/component.entity.ts @@ -1,4 +1,4 @@ -import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, OneToMany } from 'typeorm'; +import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, OneToMany, JoinColumn } from 'typeorm'; import { Page } from './page.entity'; import { Layout } from './layout.entity'; @@ -11,7 +11,7 @@ export class Component { name: string; @Column({ name: 'page_id' }) - PageId: string; + pageId: string; @Column('simple-json') properties: any; @@ -23,6 +23,7 @@ export class Component { validations: any; @ManyToOne(() => Page, (page) => page.components) + @JoinColumn({ name: 'page_id' }) page: Page; @OneToMany(() => Layout, (layout) => layout.component) diff --git a/server/src/entities/page.entity.ts b/server/src/entities/page.entity.ts index 0260851136..247ae1178a 100644 --- a/server/src/entities/page.entity.ts +++ b/server/src/entities/page.entity.ts @@ -1,4 +1,4 @@ -import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, OneToMany } from 'typeorm'; +import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, OneToMany, JoinColumn } from 'typeorm'; import { AppVersion } from './app_version.entity'; import { Component } from './component.entity'; @@ -10,16 +10,14 @@ export class Page { @Column() name: string; - @Column() - AppVersionId: string; + @Column({ name: 'page_handle' }) + pageHandle: string; - @Column() - updateId: string; - - @Column() - lastHashedDiff: string; + @Column({ name: 'app_version_id' }) + appVersionId: string; @ManyToOne(() => AppVersion, (appVersion) => appVersion.pages) + @JoinColumn({ name: 'app_version_id' }) appVersion: AppVersion; @OneToMany(() => Component, (component) => component.page) diff --git a/server/src/modules/apps/apps.module.ts b/server/src/modules/apps/apps.module.ts index a197ec5088..f89d7a0dde 100644 --- a/server/src/modules/apps/apps.module.ts +++ b/server/src/modules/apps/apps.module.ts @@ -38,6 +38,8 @@ 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'; + @Module({ imports: [ TypeOrmModule.forFeature([ @@ -77,6 +79,7 @@ import { Layout } from 'src/entities/layout.entity'; PluginsService, PluginsHelper, AppEnvironmentService, + ComponentsService, ], controllers: [AppsController, AppUsersController, AppsImportExportController], }) diff --git a/server/src/services/apps.service.ts b/server/src/services/apps.service.ts index a3f8c68b3f..09193c2caf 100644 --- a/server/src/services/apps.service.ts +++ b/server/src/services/apps.service.ts @@ -28,6 +28,7 @@ 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'; @Injectable() export class AppsService { @@ -117,7 +118,29 @@ 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', + pageHandle: 'home', + appVersionId: appVersion.id, + }) + ); + + // Set default values for app version + appVersion.showViewerNavigation = true; + appVersion.homePageId = defaultHomePage.id; + appVersion.globalSettings = { + hideHeader: false, + appInMaintenance: false, + canvasMaxWidth: 1292, + canvasMaxWidthType: 'px', + canvasMaxHeight: 2400, + canvasBackgroundColor: '#edeff5', + backgroundFxQuery: '', + }; + await manager.save(appVersion); await manager.save( manager.create(AppUser, { @@ -363,6 +386,15 @@ export class AppsService { }); } + async findPagesForVersion(appVersionId: string): Promise { + return await dbTransactionWrap(async (manager: EntityManager) => { + return manager + .createQueryBuilder(Page, 'pages') + .where('pages.appVersionId = :appVersionId', { appVersionId }) // Replace 'AppVersionId' with the actual column name + .getMany(); + }); + } + async createNewDataSourcesAndQueriesForVersion( manager: EntityManager, appVersion: AppVersion, diff --git a/server/src/services/components.service.ts b/server/src/services/components.service.ts new file mode 100644 index 0000000000..d25bbb8cb6 --- /dev/null +++ b/server/src/services/components.service.ts @@ -0,0 +1,20 @@ +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { Component } from 'src/entities/component.entity'; // Adjust the import path as per your project structure + +@Injectable() +export class ComponentsService { + constructor( + @InjectRepository(Component) + private componentsRepository: Repository + ) {} + + async findOne(id: string): Promise { + return this.componentsRepository.findOne(id); + } + + async createOrUpdate(componentDiff: any) { + console.log('----arpit:::: component service', { componentDiff }); + } +} From ec58c5cbd03db133b0b45f3bb9fc32b64377c3ae Mon Sep 17 00:00:00 2001 From: arpitnath Date: Fri, 11 Aug 2023 14:26:58 +0530 Subject: [PATCH 018/704] updating components and pages into seperate tables --- frontend/src/Editor/Container.jsx | 2 + frontend/src/Editor/EditorFunc.jsx | 74 +++++-- .../Editor/LeftSidebar/SidebarInspector.jsx | 1 + frontend/src/_helpers/appUtils.js | 2 +- frontend/src/_services/appVersion.service.js | 22 +- frontend/src/_stores/appDataStore.js | 10 +- frontend/src/_stores/utils.js | 10 +- .../1691007037021-CreateLayoutTable.ts | 23 ++- server/src/controllers/apps.controller.ts | 89 +++++++- server/src/controllers/apps.controller.v2.ts | 12 ++ server/src/dto/version-edit.dto.ts | 6 +- server/src/entities/layout.entity.ts | 20 +- server/src/modules/apps/apps.module.ts | 2 + server/src/services/apps.service.ts | 9 - server/src/services/components.service.ts | 194 +++++++++++++++++- server/src/services/page.service.ts | 35 ++++ 16 files changed, 442 insertions(+), 69 deletions(-) create mode 100644 server/src/controllers/apps.controller.v2.ts create mode 100644 server/src/services/page.service.ts diff --git a/frontend/src/Editor/Container.jsx b/frontend/src/Editor/Container.jsx index 74c812d46e..3362897c45 100644 --- a/frontend/src/Editor/Container.jsx +++ b/frontend/src/Editor/Container.jsx @@ -60,6 +60,8 @@ export const Container = ({ [JSON.stringify(appDefinition), currentPageId] ); + console.log('----mohaaan components', { components }); + const currentState = useCurrentState(); const { appVersionsId, enableReleasedVersionPopupState, isVersionReleased } = useAppVersionStore( (state) => ({ diff --git a/frontend/src/Editor/EditorFunc.jsx b/frontend/src/Editor/EditorFunc.jsx index 2e2e058c49..b2460694e6 100644 --- a/frontend/src/Editor/EditorFunc.jsx +++ b/frontend/src/Editor/EditorFunc.jsx @@ -66,6 +66,7 @@ import { useAppDataActions, useAppDataStore, useAppInfo } from '@/_stores/appDat import { useMounted } from '@/_hooks/use-mount'; // eslint-disable-next-line import/no-unresolved import { diff } from 'deep-object-diff'; +import { camelizeKeys } from 'humps'; setAutoFreeze(false); enablePatches(); @@ -226,6 +227,7 @@ const EditorComponent = (props) => { } if (mounted && didAppDefinitionChanged && currentPageId) { + console.log('----mohaaan: useEffecr', { appDefinition }); const components = appDefinition?.pages[currentPageId]?.components || {}; computeComponentState(components); @@ -345,13 +347,9 @@ const EditorComponent = (props) => { theme: { name: props?.darkMode ? 'dark' : 'light' }, urlparams: JSON.parse(JSON.stringify(queryString.parse(props.location.search))), }; - const page = { - ...props?.currentState?.page, - handle: props?.pageHandle, - variables: {}, - }; + updateState({ appId: props?.params?.id }); - useCurrentStateStore.getState().actions.setCurrentState({ globals, page }); + useCurrentStateStore.getState().actions.setCurrentState({ globals }); }; const fetchDataQueries = async (id, selectFirstQuery = false, runQueriesOnAppLoad = false) => { @@ -632,6 +630,29 @@ const EditorComponent = (props) => { }; //!-------- + 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) => { + acc[page.id] = page; + + return acc; + }, {}); + + const appJSON = { + globalSettings: editingVersion.globalSettings, + homePageId: editingVersion.homePageId, + showHideViewerNavigation: editingVersion.showHideViewerNavigation ?? true, + pages: pages, + }; + + return appJSON; + }; + + //****** */ const fetchApp = async (startingPageHandle) => { const _appId = props?.params?.id; @@ -641,12 +662,30 @@ const EditorComponent = (props) => { useAppVersionStore.getState().actions.updateReleasedVersionId(data.current_version_id); await fetchDataSources(data.editing_version?.id); await fetchDataQueries(data.editing_version?.id, true, true); - console.log('---piku [fetching app]---]', { data, defaultDef: defaultDefinition(props.darkMode) }); - let dataDefinition = data.definition ?? defaults(data.definition, defaultDefinition(props.darkMode)); + const appDefData = buildAppDefinition(data); - const pages = Object.entries(dataDefinition.pages).map(([pageId, page]) => ({ id: pageId, ...page })); + // let dataDefinition = data.definition ?? defaults(data.definition, defaultDefinition(props.darkMode)); + + // const pages = Object.entries(dataDefinition.pages).map(([pageId, page]) => ({ id: pageId, ...page })); + // const startingPageId = pages.filter((page) => page.handle === startingPageHandle)[0]?.id; + // const homePageId = !startingPageHandle || startingPageId === 'null' ? dataDefinition.homePageId : startingPageId; + + // !------ + const appJson = appDefData; + const pages = data.pages; const startingPageId = pages.filter((page) => page.handle === startingPageHandle)[0]?.id; - const homePageId = !startingPageHandle || startingPageId === 'null' ? dataDefinition.homePageId : startingPageId; + const homePageId = !startingPageId || startingPageId === 'null' ? appJson.homePageId : startingPageId; + + const currentComponents = appJson.pages[homePageId]?.components ?? {}; + console.log('---piku [fetching app] [pages] ==> ', { currentComponents }); + const currentpageData = { + handle: appJson.pages[homePageId]?.handle, + name: appJson.pages[homePageId]?.name, + id: homePageId, + variables: {}, + }; + + // !------ setCurrentPageId(homePageId); @@ -662,20 +701,15 @@ const EditorComponent = (props) => { }); useCurrentStateStore.getState().actions.setCurrentState({ - page: { - handle: dataDefinition.pages[homePageId]?.handle, - name: dataDefinition.pages[homePageId]?.name, - id: homePageId, - variables: {}, - }, + page: currentpageData, }); updateEditorState({ isLoading: false, - appDefinition: dataDefinition, + appDefinition: appJson, }); - for (const event of dataDefinition.pages[homePageId]?.events ?? []) { + for (const event of appJson.pages[homePageId]?.events ?? []) { await handleEvent(event.eventId, event); } getCanvasWidth(); @@ -813,9 +847,7 @@ const EditorComponent = (props) => { } else if (!isEmpty(props?.editingVersion)) { const componentDiff = computeAppDiff(appDefinitionDiff, currentPageId, appDiffOptions); - // console.log('---piku [componentDiff]--', componentDiff); - - updateAppVersion(appId, props.editingVersion?.id, appDefinition, componentDiff, isUserSwitchedVersion) + updateAppVersion(appId, props.editingVersion?.id, currentPageId, componentDiff, isUserSwitchedVersion) .then(() => { const _editingVersion = { ...props.editingVersion, diff --git a/frontend/src/Editor/LeftSidebar/SidebarInspector.jsx b/frontend/src/Editor/LeftSidebar/SidebarInspector.jsx index d2b1a8be7b..5a556c4281 100644 --- a/frontend/src/Editor/LeftSidebar/SidebarInspector.jsx +++ b/frontend/src/Editor/LeftSidebar/SidebarInspector.jsx @@ -44,6 +44,7 @@ export const LeftSidebarInspector = ({ // eslint-disable-next-line react-hooks/exhaustive-deps }, [appDefinition['selectedComponent']]); const currentState = useCurrentState(); + const queries = {}; if (!_.isEmpty(dataQueries)) { diff --git a/frontend/src/_helpers/appUtils.js b/frontend/src/_helpers/appUtils.js index 75faf228c4..373a6bd57a 100644 --- a/frontend/src/_helpers/appUtils.js +++ b/frontend/src/_helpers/appUtils.js @@ -1133,7 +1133,7 @@ export function computeComponentState(components = {}) { Object.keys(components).forEach((key) => { const component = components[key]; const componentMeta = componentTypes.find((comp) => component.component.component === comp.component); - + console.log('------tj: computeComponentState', { component, currentComponents }); const existingComponentName = Object.keys(currentComponents).find((comp) => currentComponents[comp].id === key); const existingValues = currentComponents[existingComponentName]; diff --git a/frontend/src/_services/appVersion.service.js b/frontend/src/_services/appVersion.service.js index 26c6893851..07b9036bd6 100644 --- a/frontend/src/_services/appVersion.service.js +++ b/frontend/src/_services/appVersion.service.js @@ -7,6 +7,7 @@ export const appVersionService = { create, del, save, + autoSaveApp, }; function getAll(appId) { @@ -44,7 +45,7 @@ function del(appId, versionId) { } function save(appId, versionId, values, isUserSwitchedVersion = false) { - console.log('---piku [version saved]', { values }); + // console.log('---piku [version saved]', { values }); const body = { is_user_switched_version: isUserSwitchedVersion }; if (values.definition) body['definition'] = values.definition; if (values.name) body['name'] = values.name; @@ -58,3 +59,22 @@ 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) { + console.log('---piku [version saved] [v2]', { operation, type, diff }); + + const OPERATION = Object.freeze({ + create: 'POST', + update: 'PUT', + delete: 'DELETE', + }); + + const body = { is_user_switched_version: isUserSwitchedVersion, pageId, diff: diff }; + + const requestOptions = { + method: OPERATION[operation], + headers: authHeader(), + credentials: 'include', + body: JSON.stringify(body), + }; + return fetch(`${config.apiUrl}/apps/${appId}/versions/${versionId}/${type}`, requestOptions).then(handleResponse); +} diff --git a/frontend/src/_stores/appDataStore.js b/frontend/src/_stores/appDataStore.js index 716672c656..778142f29b 100644 --- a/frontend/src/_stores/appDataStore.js +++ b/frontend/src/_stores/appDataStore.js @@ -31,11 +31,15 @@ export const useAppDataStore = create( updateApps: (apps) => set(() => ({ apps: apps })), updateState: (state) => set((prev) => ({ ...prev, ...state })), updateAppDefinitionDiff: (appDefinitionDiff) => set(() => ({ appDefinitionDiff: appDefinitionDiff })), - updateAppVersion: async (appId, versionId, appDefinition, appDefinitionDiff, isUserSwitchedVersion = false) => { - return await appVersionService.save( + updateAppVersion: async (appId, versionId, pageId, appDefinitionDiff, isUserSwitchedVersion = false) => { + // console.log('-piku :: from store', { appDefinitionDiff }); + return await appVersionService.autoSaveApp( appId, versionId, - { definition: appDefinition, diff: appDefinitionDiff }, + appDefinitionDiff.updateDiff, + appDefinitionDiff.type, + pageId, + appDefinitionDiff.operation, isUserSwitchedVersion ); }, diff --git a/frontend/src/_stores/utils.js b/frontend/src/_stores/utils.js index 0d76eecfdf..78ee079f4e 100644 --- a/frontend/src/_stores/utils.js +++ b/frontend/src/_stores/utils.js @@ -36,7 +36,7 @@ const defaultComponent = { const updateType = Object.freeze({ pageDefinitionChanged: 'pages', - containerChanges: 'layout', + containerChanges: 'components/layout', componentAdded: 'components', componentDefinitionChanged: 'components', }); @@ -44,6 +44,7 @@ const updateType = Object.freeze({ export const computeAppDiff = (appDiff, currentPageId, opts) => { let type; let updateDiff; + let operation = 'update'; if (opts?.pageDefinitionChanged) { updateDiff = appDiff?.pages[currentPageId]; @@ -64,9 +65,10 @@ export const computeAppDiff = (appDiff, currentPageId, opts) => { result[id] = _.defaultsDeep(metaDiff, defaultComponent); + result[id].layouts = appDiff.pages[currentPageId].components[id].layouts; + + operation = 'create'; return result; - // result[id].componentId = id; - // return { ..._.defaultsDeep(metaDiff, defaultComponent), componentId: id }; }, {}); type = updateType.componentDefinitionChanged; @@ -74,5 +76,5 @@ export const computeAppDiff = (appDiff, currentPageId, opts) => { console.log('---piku [currentPageComponents]', { updateDiff, opts, type }); - return { updateDiff, type }; + return { updateDiff, type, operation }; }; diff --git a/server/migrations/1691007037021-CreateLayoutTable.ts b/server/migrations/1691007037021-CreateLayoutTable.ts index aedd4e84d6..a6b9900df1 100644 --- a/server/migrations/1691007037021-CreateLayoutTable.ts +++ b/server/migrations/1691007037021-CreateLayoutTable.ts @@ -12,24 +12,31 @@ export class CreateLayoutTable1691007037021 implements MigrationInterface { isPrimary: true, default: 'gen_random_uuid()', }, - { - name: 'name', - type: 'varchar', - isNullable: false, - }, { name: 'type', - type: 'varchar', + type: 'enum', + enumName: 'layput_type', + enum: ['desktop', 'mobile'], isNullable: false, }, { name: 'top', - type: 'integer', + type: 'double precision', isNullable: false, }, { name: 'left', - type: 'integer', + type: 'double precision', + isNullable: false, + }, + { + name: 'width', + type: 'double precision', + isNullable: false, + }, + { + name: 'height', + type: 'double precision', isNullable: false, }, { diff --git a/server/src/controllers/apps.controller.ts b/server/src/controllers/apps.controller.ts index 022253f8e9..e06d0e834f 100644 --- a/server/src/controllers/apps.controller.ts +++ b/server/src/controllers/apps.controller.ts @@ -29,6 +29,7 @@ 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'; @Controller('apps') export class AppsController { @@ -36,6 +37,7 @@ export class AppsController { private appsService: AppsService, private foldersService: FoldersService, private componentsService: ComponentsService, + private pageService: PageService, private appsAbilityFactory: AppsAbilityFactory ) {} @@ -88,9 +90,7 @@ export class AppsController { ? await this.appsService.findDataQueriesForVersion(app.editingVersion.id) : []; - const pagesForVersion = app.editingVersion ? await this.appsService.findPagesForVersion(app.editingVersion.id) : []; - - console.log('------arpit [pagesforVersion]', { pagesForVersion }); + const pagesForVersion = app.editingVersion ? await this.pageService.findPagesForVersion(app.editingVersion.id) : []; // serialize queries for (const query of dataQueriesForVersion) { @@ -320,16 +320,87 @@ export class AppsController { throw new ForbiddenException('You do not have permissions to perform this action'); } - const updateType = versionEditDto.app_diff?.type; - console.log('----arpit apps controller => ', { updateType }); - - // if(updateType=== 'components') { - // await this.componentsService.createOrUpdate(versionEditDto.app_diff.components, versionId) - // } + // const updateType = versionEditDto.app_diff?.type; + // console.log('----arpit apps controller => ', { updateType }); await this.appsService.updateVersion(version, versionEditDto, app.organizationId); return; } + @UseGuards(JwtAuthGuard) + @UseInterceptors(ValidAppInterceptor) + @Post(':id/versions/:versionId/components') + async createComponent( + @User() user, + @Param('id') id, + @Param('versionId') versionId, + @Body() versionEditDto: VersionEditDto + ) { + 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 updateType = versionEditDto.app_diff?.type; + console.log('----arpit apps controller v2 => ', { versionEditDto }); + await this.componentsService.create(versionEditDto.diff, versionEditDto.pageId); + } + @UseGuards(JwtAuthGuard) + @UseInterceptors(ValidAppInterceptor) + @Put(':id/versions/:versionId/components') + async updateComponent( + @User() user, + @Param('id') id, + @Param('versionId') versionId, + @Body() versionEditDto: VersionEditDto + ) { + 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 updateType = versionEditDto.app_diff?.type; + console.log('----arpit apps controller v2 [update] => ', { versionEditDto }); + await this.componentsService.update(versionEditDto.diff); + } + @UseGuards(JwtAuthGuard) + @UseInterceptors(ValidAppInterceptor) + @Put(':id/versions/:versionId/components/layout') + async updateComponentLayput( + @User() user, + @Param('id') id, + @Param('versionId') versionId, + @Body() versionEditDto: VersionEditDto + ) { + 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 updateType = versionEditDto.app_diff?.type; + console.log('----arpit apps controller v2 |layput | [update] => ', { versionEditDto }); + await this.componentsService.componentLayoutChange(versionEditDto.diff); + } @UseGuards(JwtAuthGuard) @UseInterceptors(ValidAppInterceptor) diff --git a/server/src/controllers/apps.controller.v2.ts b/server/src/controllers/apps.controller.v2.ts new file mode 100644 index 0000000000..786bda3135 --- /dev/null +++ b/server/src/controllers/apps.controller.v2.ts @@ -0,0 +1,12 @@ +import { Controller } from '@nestjs/common'; +// import { JwtAuthGuard } from '../../src/modules/auth/jwt-auth.guard'; + +@Controller({ + path: 'apps', + version: '2', // Set the version to '2' +}) +export class AppsControllerV2 { + constructor(/* Add your services and dependencies here */) {} + + // Add your new version 2 methods here +} diff --git a/server/src/dto/version-edit.dto.ts b/server/src/dto/version-edit.dto.ts index 305055af69..2736652982 100644 --- a/server/src/dto/version-edit.dto.ts +++ b/server/src/dto/version-edit.dto.ts @@ -22,5 +22,9 @@ export class VersionEditDto { is_user_switched_version: boolean; @IsOptional() - app_diff: any; + diff: any; + + @IsOptional() + @IsString() + pageId: string; } diff --git a/server/src/entities/layout.entity.ts b/server/src/entities/layout.entity.ts index c0ee7ac8c6..82dea912aa 100644 --- a/server/src/entities/layout.entity.ts +++ b/server/src/entities/layout.entity.ts @@ -1,4 +1,4 @@ -import { Entity, PrimaryGeneratedColumn, Column, ManyToOne } from 'typeorm'; +import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn } from 'typeorm'; import { Component } from './component.entity'; @Entity({ name: 'layouts' }) @@ -6,21 +6,25 @@ export class Layout { @PrimaryGeneratedColumn('uuid') id: string; - @Column({ name: 'name' }) - name: string; - - @Column({ name: 'type' }) + @Column({ type: 'enum', enumName: 'layout_type', name: 'type', enum: ['desktop', 'mobile'] }) type: string; - @Column({ name: 'top' }) + @Column({ name: 'top', type: 'double precision' }) top: number; - @Column({ name: 'left' }) + @Column({ name: 'left', type: 'double precision' }) left: number; + @Column({ name: 'width', type: 'double precision' }) + width: number; + + @Column({ name: 'height', type: 'double precision' }) + height: number; + @Column({ name: 'component_id' }) - ComponentId: string; + componentId: string; @ManyToOne(() => Component, (component) => component.layouts) + @JoinColumn({ name: 'component_id' }) component: Component; } diff --git a/server/src/modules/apps/apps.module.ts b/server/src/modules/apps/apps.module.ts index f89d7a0dde..6923858b8b 100644 --- a/server/src/modules/apps/apps.module.ts +++ b/server/src/modules/apps/apps.module.ts @@ -39,6 +39,7 @@ 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'; @Module({ imports: [ @@ -80,6 +81,7 @@ import { ComponentsService } from '@services/components.service'; PluginsHelper, AppEnvironmentService, ComponentsService, + PageService, ], controllers: [AppsController, AppUsersController, AppsImportExportController], }) diff --git a/server/src/services/apps.service.ts b/server/src/services/apps.service.ts index 09193c2caf..d1bb036c18 100644 --- a/server/src/services/apps.service.ts +++ b/server/src/services/apps.service.ts @@ -386,15 +386,6 @@ export class AppsService { }); } - async findPagesForVersion(appVersionId: string): Promise { - return await dbTransactionWrap(async (manager: EntityManager) => { - return manager - .createQueryBuilder(Page, 'pages') - .where('pages.appVersionId = :appVersionId', { appVersionId }) // Replace 'AppVersionId' with the actual column name - .getMany(); - }); - } - async createNewDataSourcesAndQueriesForVersion( manager: EntityManager, appVersion: AppVersion, diff --git a/server/src/services/components.service.ts b/server/src/services/components.service.ts index d25bbb8cb6..07fca7f185 100644 --- a/server/src/services/components.service.ts +++ b/server/src/services/components.service.ts @@ -1,7 +1,10 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; -import { Repository } from 'typeorm'; -import { Component } from 'src/entities/component.entity'; // Adjust the import path as per your project structure +import { EntityManager, Repository } from 'typeorm'; +import { Component } from 'src/entities/component.entity'; +import { Layout } from 'src/entities/layout.entity'; +import { Page } from 'src/entities/page.entity'; +import { dbTransactionWrap } from 'src/helpers/utils.helper'; @Injectable() export class ComponentsService { @@ -14,7 +17,190 @@ export class ComponentsService { return this.componentsRepository.findOne(id); } - async createOrUpdate(componentDiff: any) { - console.log('----arpit:::: component service', { componentDiff }); + async create(componentDiff: object, pageId: string) { + return dbTransactionWrap(async (manager: EntityManager) => { + const page = await manager.findOne(Page, pageId); + + const newComponents = this.transformComponentData(componentDiff); + const componentLayouts = []; + + newComponents.forEach((component) => { + component.page = page; + }); + + const savedComponents = await manager.save(Component, newComponents); + + savedComponents.forEach((component) => { + const componentLayout = componentDiff[component.id].layouts; + + if (componentLayout) { + 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.component = component; + + componentLayouts.push(newLayout); + } + } + }); + + await manager.save(Layout, componentLayouts); + + return {}; + }); + } + + async update(componentDiff: object) { + return dbTransactionWrap(async (manager) => { + for (const componentId in componentDiff) { + const { component } = componentDiff[componentId]; + + const componentData = await manager.findOne(Component, componentId); + + if (!componentData) { + return { + error: { + message: `Component with id ${componentId} does not exist`, + }, + }; + } + + const isComponentDefinitionChanged = component.definition ? true : false; + + if (isComponentDefinitionChanged) { + const updatedDefinition = component.definition; + const columnsUpdated = Object.keys(updatedDefinition); + + const newComponentData = columnsUpdated.reduce((acc, column) => { + const newColumnData = { + ...componentData[column], + ...updatedDefinition[column], + }; + + acc[column] = newColumnData; + return acc; + }, {}); + + await manager.update(Component, componentId, newComponentData); + return; + } + + await manager.update(Component, componentId, component); + + return; + } + }); + } + + async componentLayoutChange(componenstLayoutDiff: object) { + return dbTransactionWrap(async (manager: EntityManager) => { + for (const componentId in componenstLayoutDiff) { + const { layouts } = componenstLayoutDiff[componentId]; + + const componentLayout = await manager.findOne(Layout, { componentId }); + + if (!componentLayout) { + return { + error: { + message: `Component with id ${componentId} does not exist`, + }, + }; + } + + for (const type in layouts) { + const layout = { + type, + ...layouts[type], + }; + const currentLayout = Object.assign({}, componentLayout); + + const newLayout = { + ...currentLayout, + ...layout, + }; + + console.log('--arpit [layput changed]', { type, layout, componentLayout, newLayout }); + + await manager.update(Layout, { id: componentLayout.id }, newLayout); + } + } + }); + } + + async getAllComponents(pageId: string) { + // need to get all components for a page with their layouts + + return dbTransactionWrap(async (manager: EntityManager) => { + return manager + .createQueryBuilder(Component, 'component') + .leftJoinAndSelect('component.layouts', 'layout') + .where('component.pageId = :pageId', { pageId }) + .getMany() + .then((components) => { + return components.reduce((acc, component) => { + const componentId = component.id; + const componentData = component; + const componentLayout = component.layouts[0]; + + const transformedData = this.createComponentWithLayout(componentData, componentLayout); + + acc[componentId] = transformedData[componentId]; + + return acc; + }, {}); + }); + }); + } + + transformComponentData(data: object): Component[] { + const transformedComponents: Component[] = []; + + for (const componentId in data) { + const componentData = data[componentId]; + + const transformedComponent: Component = new Component(); + transformedComponent.id = componentId; + transformedComponent.name = componentData.name; + transformedComponent.properties = componentData.properties || {}; + transformedComponent.styles = componentData.styles || {}; + transformedComponent.validations = componentData.validation || {}; + + transformedComponents.push(transformedComponent); + } + + return transformedComponents; + } + + createComponentWithLayout(componentData, layoutData) { + const { id, name, properties, styles, validations } = componentData; + const { type, top, left, width, height } = layoutData; + + const componentWithLayout = { + [id]: { + component: { + name, + definition: { + properties, + styles, + validations, + }, + }, + layouts: { + [type]: { + top, + left, + width, + height, + }, + }, + }, + }; + + return componentWithLayout; } } diff --git a/server/src/services/page.service.ts b/server/src/services/page.service.ts new file mode 100644 index 0000000000..38574285f2 --- /dev/null +++ b/server/src/services/page.service.ts @@ -0,0 +1,35 @@ +import { Injectable } from '@nestjs/common'; +import { EntityManager, Repository } from 'typeorm'; +import { InjectRepository } from '@nestjs/typeorm'; + +import { Page } from 'src/entities/page.entity'; +import { ComponentsService } from './components.service'; + +@Injectable() +export class PageService { + constructor( + private readonly manager: EntityManager, + @InjectRepository(Page) + private readonly pageRepository: Repository, + + private componentsService: ComponentsService + ) {} + + async findPagesForVersion(appVersionId: string): Promise { + const allPages = await this.pageRepository.find({ appVersionId }); + + const pagesWithComponents = await Promise.all( + allPages.map(async (page) => { + const components = await this.componentsService.getAllComponents(page.id); + delete page.appVersionId; + return { ...page, components }; + }) + ); + + return pagesWithComponents; + } + + async findOne(id: string): Promise { + return this.pageRepository.findOne(id); + } +} From 08274d53ff6de8487e62265f888602cd5e850f5c Mon Sep 17 00:00:00 2001 From: arpitnath Date: Sun, 13 Aug 2023 01:42:56 +0530 Subject: [PATCH 019/704] new controllers for components and layouts --- frontend/src/Editor/Container.jsx | 3 +- frontend/src/Editor/EditorFunc.jsx | 65 +++++++++++++++---- frontend/src/Editor/EditorKeyHooks.jsx | 2 - frontend/src/_helpers/appUtils.js | 13 ++-- frontend/src/_services/appVersion.service.js | 2 - frontend/src/_stores/appDataStore.js | 1 - frontend/src/_stores/utils.js | 17 ++++- .../1691006952074-CreateComponentTable.ts | 5 ++ server/src/controllers/apps.controller.ts | 36 +++++++--- server/src/entities/component.entity.ts | 3 + server/src/services/apps.service.ts | 4 +- server/src/services/components.service.ts | 20 +++++- 12 files changed, 130 insertions(+), 41 deletions(-) diff --git a/frontend/src/Editor/Container.jsx b/frontend/src/Editor/Container.jsx index 3362897c45..6731c4611a 100644 --- a/frontend/src/Editor/Container.jsx +++ b/frontend/src/Editor/Container.jsx @@ -60,8 +60,6 @@ export const Container = ({ [JSON.stringify(appDefinition), currentPageId] ); - console.log('----mohaaan components', { components }); - const currentState = useCurrentState(); const { appVersionsId, enableReleasedVersionPopupState, isVersionReleased } = useAppVersionStore( (state) => ({ @@ -149,6 +147,7 @@ export const Container = ({ const noOfBoxs = Object.values(boxes || []).length; useEffect(() => { updateCanvasHeight(boxes); + // eslint-disable-next-line react-hooks/exhaustive-deps }, [noOfBoxs]); const moveBox = useCallback( diff --git a/frontend/src/Editor/EditorFunc.jsx b/frontend/src/Editor/EditorFunc.jsx index b2460694e6..a0c856156e 100644 --- a/frontend/src/Editor/EditorFunc.jsx +++ b/frontend/src/Editor/EditorFunc.jsx @@ -64,9 +64,10 @@ import { shallow } from 'zustand/shallow'; import { useEditorActions, useEditorState, useEditorStore } from '@/_stores/editorStore'; import { useAppDataActions, useAppDataStore, useAppInfo } from '@/_stores/appDataStore'; import { useMounted } from '@/_hooks/use-mount'; + // eslint-disable-next-line import/no-unresolved import { diff } from 'deep-object-diff'; -import { camelizeKeys } from 'humps'; +import { camelizeKeys, decamelizeKeys } from 'humps'; setAutoFreeze(false); enablePatches(); @@ -227,7 +228,6 @@ const EditorComponent = (props) => { } if (mounted && didAppDefinitionChanged && currentPageId) { - console.log('----mohaaan: useEffecr', { appDefinition }); const components = appDefinition?.pages[currentPageId]?.components || {}; computeComponentState(components); @@ -236,6 +236,7 @@ const EditorComponent = (props) => { autoSave(); } } + // eslint-disable-next-line react-hooks/exhaustive-deps }, [JSON.stringify({ appDefinition, currentPageId, dataQueries })]); const editorRef = { @@ -630,6 +631,47 @@ const EditorComponent = (props) => { }; //!-------- + + 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, + }, + validations: { + ...componentMeta.definition.validations, + ...currentComponentData?.component.definition.validations, + }, + }; + + const mergedComponent = { + component: { + ...componentMeta, + ...currentComponentData.component, + }, + layouts: { + ...currentComponentData.layouts, + }, + withDefaultChildren: componentMeta.withDefaultChildren ?? false, + }; + + mergedComponent.component.definition = mergedDefinition; + + components[componentId] = mergedComponent; + } + return components; + }; + const buildAppDefinition = (data) => { const editingVersion = _.omit(camelizeKeys(data.editing_version), ['definition', 'updatedAt', 'createdAt', 'name']); @@ -637,6 +679,10 @@ const EditorComponent = (props) => { _.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; @@ -649,6 +695,8 @@ const EditorComponent = (props) => { pages: pages, }; + // const componentMeta = componentTypes.find((comp) => component.component === comp.component); + return appJSON; }; @@ -677,7 +725,7 @@ const EditorComponent = (props) => { const homePageId = !startingPageId || startingPageId === 'null' ? appJson.homePageId : startingPageId; const currentComponents = appJson.pages[homePageId]?.components ?? {}; - console.log('---piku [fetching app] [pages] ==> ', { currentComponents }); + console.log('---arpit [fetching app] [pages] ==> ', { currentComponents }); const currentpageData = { handle: appJson.pages[homePageId]?.handle, name: appJson.pages[homePageId]?.name, @@ -727,7 +775,6 @@ const EditorComponent = (props) => { // !-------- const setAppDefinitionFromVersion = (version, shouldWeEditVersion = true) => { - console.log('---arpit [setAppFromVersion]--', version); if (version?.id !== props.editingVersion?.id) { appDefinitionChanged(defaults(version.definition, defaultDefinition(props.darkMode)), { skipAutoSave: true, @@ -783,11 +830,6 @@ const EditorComponent = (props) => { let updatedAppDefinition; const copyOfAppDefinition = JSON.parse(JSON.stringify(appDefinition)); - console.log('--arpit | appDefinitionChanged func()', { - opts, - newDefinition, - }); - updatedAppDefinition = produce(copyOfAppDefinition, (draft) => { if (_.isEmpty(draft)) return; @@ -822,6 +864,7 @@ const EditorComponent = (props) => { setUndoStack((prev) => [...prev, undoPatches]); updateAppDefinitionDiff(diffPatches); + updateState({ appDiffOptions: opts, }); @@ -839,7 +882,6 @@ const EditorComponent = (props) => { }; const saveEditingVersion = (isUserSwitchedVersion = false) => { - console.log('---arpit [saving - editionversion]--', { appDefinitionDiff }); if (props.isVersionReleased && !isUserSwitchedVersion) { updateEditorState({ isSaving: false, @@ -952,10 +994,10 @@ const EditorComponent = (props) => { canUndo: undoStack.length > 0, canRedo: redoStack.length > 0, }); + // eslint-disable-next-line react-hooks/exhaustive-deps }, [JSON.stringify(undoStack), JSON.stringify(redoStack)]); const componentDefinitionChanged = (componentDefinition, props) => { - console.log('---arpit checking:::: ', { props }); if (props?.isVersionReleased) { useAppVersionStore.getState().actions.enableReleasedVersionPopupState(); return; @@ -1024,6 +1066,7 @@ const EditorComponent = (props) => { } appDefinitionChanged(newDefinition, { componentDefinitionChanged: true, + componentDeleted: true, }); handleInspectorView(); } else { diff --git a/frontend/src/Editor/EditorKeyHooks.jsx b/frontend/src/Editor/EditorKeyHooks.jsx index df79bf779b..1f9b6cb797 100644 --- a/frontend/src/Editor/EditorKeyHooks.jsx +++ b/frontend/src/Editor/EditorKeyHooks.jsx @@ -10,7 +10,6 @@ export const EditorKeyHooks = ({ removeMultipleComponents, }) => { const handleHotKeysCallback = (key) => { - console.log('---arpit-- hotkeys', { key }); switch (key) { case 'Escape': handleEditorEscapeKeyPress(); @@ -19,7 +18,6 @@ export const EditorKeyHooks = ({ removeMultipleComponents(); break; case 'KeyD': - console.log('---arpit-- paste component'); cloneComponents(); break; case 'KeyC': diff --git a/frontend/src/_helpers/appUtils.js b/frontend/src/_helpers/appUtils.js index 373a6bd57a..6ff61378c7 100644 --- a/frontend/src/_helpers/appUtils.js +++ b/frontend/src/_helpers/appUtils.js @@ -1130,10 +1130,11 @@ export function renderTooltip({ props, text }) { export function computeComponentState(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); - console.log('------tj: computeComponentState', { component, currentComponents }); + 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]; @@ -1149,14 +1150,14 @@ export function computeComponentState(components = {}) { } if (!isListView && !isForm) { - componentState[component.component.name] = { + componentState[component.name] = { ...componentMeta.exposedVariables, id: key, ...existingValues, }; } } else { - componentState[component.component.name] = { + componentState[component.name] = { ...componentMeta.exposedVariables, id: key, ...existingValues, @@ -1586,7 +1587,7 @@ export const removeSelectedComponent = (pageId, newDefinition, selectedComponent delete newDefinition.pages[pageId].components[component.id]; }); - updateAppDefinition(newDefinition, { componentDefinitionChanged: true }); + updateAppDefinition(newDefinition, { componentDefinitionChanged: true, componentDeleted: true }); }; const getSelectedText = () => { diff --git a/frontend/src/_services/appVersion.service.js b/frontend/src/_services/appVersion.service.js index 07b9036bd6..9af120a865 100644 --- a/frontend/src/_services/appVersion.service.js +++ b/frontend/src/_services/appVersion.service.js @@ -60,8 +60,6 @@ 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) { - console.log('---piku [version saved] [v2]', { operation, type, diff }); - const OPERATION = Object.freeze({ create: 'POST', update: 'PUT', diff --git a/frontend/src/_stores/appDataStore.js b/frontend/src/_stores/appDataStore.js index 778142f29b..a7f06dbe91 100644 --- a/frontend/src/_stores/appDataStore.js +++ b/frontend/src/_stores/appDataStore.js @@ -32,7 +32,6 @@ export const useAppDataStore = create( updateState: (state) => set((prev) => ({ ...prev, ...state })), updateAppDefinitionDiff: (appDefinitionDiff) => set(() => ({ appDefinitionDiff: appDefinitionDiff })), updateAppVersion: async (appId, versionId, pageId, appDefinitionDiff, isUserSwitchedVersion = false) => { - // console.log('-piku :: from store', { appDefinitionDiff }); return await appVersionService.autoSaveApp( appId, versionId, diff --git a/frontend/src/_stores/utils.js b/frontend/src/_stores/utils.js index 78ee079f4e..428fda719d 100644 --- a/frontend/src/_stores/utils.js +++ b/frontend/src/_stores/utils.js @@ -32,6 +32,7 @@ const defaultComponent = { styles: {}, validation: {}, events: [], + type: '', }; const updateType = Object.freeze({ @@ -39,6 +40,7 @@ const updateType = Object.freeze({ containerChanges: 'components/layout', componentAdded: 'components', componentDefinitionChanged: 'components', + componentDeleted: 'components', }); export const computeAppDiff = (appDiff, currentPageId, opts) => { @@ -46,10 +48,20 @@ export const computeAppDiff = (appDiff, currentPageId, opts) => { let updateDiff; let operation = 'update'; + console.log('---piku [computeAppDiff]', { appDiff, currentPageId, opts }); + if (opts?.pageDefinitionChanged) { updateDiff = appDiff?.pages[currentPageId]; type = updateType.pageDefinitionChanged; + } else if (opts?.componentDeleted) { + const currentPageComponents = appDiff?.pages[currentPageId]?.components; + + updateDiff = _.keys(currentPageComponents); + + type = updateType.componentDeleted; + + operation = 'delete'; } else if ((opts?.containerChanges || opts?.componentDefinitionChanged) && !opts?.componentAdded) { const currentPageComponents = appDiff?.pages[currentPageId]?.components; @@ -65,16 +77,15 @@ export const computeAppDiff = (appDiff, currentPageId, opts) => { result[id] = _.defaultsDeep(metaDiff, defaultComponent); + result[id].type = componentMeta.component; result[id].layouts = appDiff.pages[currentPageId].components[id].layouts; - operation = 'create'; + return result; }, {}); type = updateType.componentDefinitionChanged; } - console.log('---piku [currentPageComponents]', { updateDiff, opts, type }); - return { updateDiff, type, operation }; }; diff --git a/server/migrations/1691006952074-CreateComponentTable.ts b/server/migrations/1691006952074-CreateComponentTable.ts index 3aab390d2a..2016b55eb2 100644 --- a/server/migrations/1691006952074-CreateComponentTable.ts +++ b/server/migrations/1691006952074-CreateComponentTable.ts @@ -17,6 +17,11 @@ export class CreateComponentTable1691006952074 implements MigrationInterface { type: 'varchar', isNullable: false, }, + { + name: 'type', + type: 'varchar', + isNullable: false, + }, { name: 'page_id', type: 'uuid', diff --git a/server/src/controllers/apps.controller.ts b/server/src/controllers/apps.controller.ts index e06d0e834f..f449fb572e 100644 --- a/server/src/controllers/apps.controller.ts +++ b/server/src/controllers/apps.controller.ts @@ -101,7 +101,7 @@ export class AppsController { response['data_queries'] = seralizedQueries; response['definition'] = app.editingVersion?.definition; - response['pages'] = decamelizeKeys(pagesForVersion); + response['pages'] = pagesForVersion; //! if editing version exists, camelize the definition if (app.editingVersion && app.editingVersion.definition) { @@ -320,9 +320,6 @@ export class AppsController { throw new ForbiddenException('You do not have permissions to perform this action'); } - // const updateType = versionEditDto.app_diff?.type; - // console.log('----arpit apps controller => ', { updateType }); - await this.appsService.updateVersion(version, versionEditDto, app.organizationId); return; } @@ -347,8 +344,6 @@ export class AppsController { throw new ForbiddenException('You do not have permissions to perform this action'); } - // const updateType = versionEditDto.app_diff?.type; - console.log('----arpit apps controller v2 => ', { versionEditDto }); await this.componentsService.create(versionEditDto.diff, versionEditDto.pageId); } @UseGuards(JwtAuthGuard) @@ -372,10 +367,33 @@ export class AppsController { throw new ForbiddenException('You do not have permissions to perform this action'); } - // const updateType = versionEditDto.app_diff?.type; - console.log('----arpit apps controller v2 [update] => ', { versionEditDto }); await this.componentsService.update(versionEditDto.diff); } + + @UseGuards(JwtAuthGuard) + @UseInterceptors(ValidAppInterceptor) + @Delete(':id/versions/:versionId/components') + async deleteComponents( + @User() user, + @Param('id') id, + @Param('versionId') versionId, + @Body() versionEditDto: VersionEditDto + ) { + 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(versionEditDto.diff); + } + @UseGuards(JwtAuthGuard) @UseInterceptors(ValidAppInterceptor) @Put(':id/versions/:versionId/components/layout') @@ -397,8 +415,6 @@ export class AppsController { throw new ForbiddenException('You do not have permissions to perform this action'); } - // const updateType = versionEditDto.app_diff?.type; - console.log('----arpit apps controller v2 |layput | [update] => ', { versionEditDto }); await this.componentsService.componentLayoutChange(versionEditDto.diff); } diff --git a/server/src/entities/component.entity.ts b/server/src/entities/component.entity.ts index ec01f4baa5..b32d4fae80 100644 --- a/server/src/entities/component.entity.ts +++ b/server/src/entities/component.entity.ts @@ -10,6 +10,9 @@ export class Component { @Column({ name: 'name' }) name: string; + @Column({ name: 'type' }) + type: string; + @Column({ name: 'page_id' }) pageId: string; diff --git a/server/src/services/apps.service.ts b/server/src/services/apps.service.ts index d1bb036c18..b4a53a5cde 100644 --- a/server/src/services/apps.service.ts +++ b/server/src/services/apps.service.ts @@ -122,8 +122,8 @@ export class AppsService { const defaultHomePage = await manager.save( manager.create(Page, { - name: 'Home', - pageHandle: 'home', + name: 'Defualt Page', + pageHandle: 'defaultpage', appVersionId: appVersion.id, }) ); diff --git a/server/src/services/components.service.ts b/server/src/services/components.service.ts index 07fca7f185..03019ce66b 100644 --- a/server/src/services/components.service.ts +++ b/server/src/services/components.service.ts @@ -97,6 +97,22 @@ export class ComponentsService { }); } + async delete(componentIds: string[]) { + return dbTransactionWrap(async (manager: EntityManager) => { + const components = await manager.findByIds(Component, componentIds); + + if (!components.length) { + return { + error: { + message: `Components with ids ${componentIds} do not exist`, + }, + }; + } + + await manager.delete(Component, componentIds); + }); + } + async componentLayoutChange(componenstLayoutDiff: object) { return dbTransactionWrap(async (manager: EntityManager) => { for (const componentId in componenstLayoutDiff) { @@ -124,8 +140,6 @@ export class ComponentsService { ...layout, }; - console.log('--arpit [layput changed]', { type, layout, componentLayout, newLayout }); - await manager.update(Layout, { id: componentLayout.id }, newLayout); } } @@ -166,6 +180,7 @@ export class ComponentsService { const transformedComponent: Component = new Component(); transformedComponent.id = componentId; transformedComponent.name = componentData.name; + transformedComponent.type = componentData.type; transformedComponent.properties = componentData.properties || {}; transformedComponent.styles = componentData.styles || {}; transformedComponent.validations = componentData.validation || {}; @@ -184,6 +199,7 @@ export class ComponentsService { [id]: { component: { name, + component: componentData.type, definition: { properties, styles, From c5167203f5757597e65161140d17983ffe4b8971 Mon Sep 17 00:00:00 2001 From: arpitnath Date: Mon, 14 Aug 2023 23:26:27 +0530 Subject: [PATCH 020/704] new v2 controllers for apps --- frontend/src/Editor/EditorFunc.jsx | 15 +- frontend/src/_services/appVersion.service.js | 12 +- frontend/src/_stores/utils.js | 23 +- .../1691004576333-CreatePageTable.ts | 5 + server/src/controllers/apps.controller.ts | 96 ------- server/src/controllers/apps.controller.v2.ts | 260 +++++++++++++++++- server/src/dto/pages.dto.ts | 17 ++ server/src/entities/page.entity.ts | 5 +- server/src/modules/apps/apps.module.ts | 3 +- server/src/services/apps.service.ts | 3 +- server/src/services/page.service.ts | 86 +++++- 11 files changed, 412 insertions(+), 113 deletions(-) create mode 100644 server/src/dto/pages.dto.ts diff --git a/frontend/src/Editor/EditorFunc.jsx b/frontend/src/Editor/EditorFunc.jsx index a0c856156e..5542793b67 100644 --- a/frontend/src/Editor/EditorFunc.jsx +++ b/frontend/src/Editor/EditorFunc.jsx @@ -1161,6 +1161,8 @@ const EditorComponent = (props) => { return; } + setCurrentPageId(pageId); + const copyOfAppDefinition = JSON.parse(JSON.stringify(appDefinition)); copyOfAppDefinition.pages[pageId].name = newName; @@ -1192,12 +1194,17 @@ const EditorComponent = (props) => { name, handle: newHandle, components: {}, - index: Object.keys(copyOfAppDefinition.pages).length, + index: Object.keys(copyOfAppDefinition.pages).length + 1, }; setCurrentPageId(newPageId); - appDefinitionChanged(copyOfAppDefinition, { pageDefinitionChanged: true, switchPage: true, pageId: newPageId }); + appDefinitionChanged(copyOfAppDefinition, { + pageDefinitionChanged: true, + addNewPage: true, + switchPage: true, + pageId: newPageId, + }); }; const switchPage = (pageId, queryParams = []) => { @@ -1288,6 +1295,7 @@ const EditorComponent = (props) => { appDefinitionChanged(newAppDefinition, { pageDefinitionChanged: true, + deletePageRequest: true, }); toast.success(`${toBeDeletedPage.name} page deleted.`); @@ -1447,7 +1455,7 @@ const EditorComponent = (props) => { const pagesObj = newSortedPages.reduce((acc, page, index) => { acc[page.id] = { ...page, - index: index, + index: index + 1, }; return acc; }, {}); @@ -1458,6 +1466,7 @@ const EditorComponent = (props) => { appDefinitionChanged(newAppDefinition, { pageDefinitionChanged: true, + pageSortingChanged: true, }); }; diff --git a/frontend/src/_services/appVersion.service.js b/frontend/src/_services/appVersion.service.js index 9af120a865..2fca17429a 100644 --- a/frontend/src/_services/appVersion.service.js +++ b/frontend/src/_services/appVersion.service.js @@ -66,7 +66,15 @@ function autoSaveApp(appId, versionId, diff, type, pageId, operation, isUserSwit delete: 'DELETE', }); - const body = { is_user_switched_version: isUserSwitchedVersion, pageId, diff: diff }; + let body = {}; + + if ((type === 'pages' && operation === 'create') || operation === 'delete') { + body = { + ...diff, + }; + } else { + body = { is_user_switched_version: isUserSwitchedVersion, pageId, diff: diff }; + } const requestOptions = { method: OPERATION[operation], @@ -74,5 +82,5 @@ function autoSaveApp(appId, versionId, diff, type, pageId, operation, isUserSwit credentials: 'include', body: JSON.stringify(body), }; - return fetch(`${config.apiUrl}/apps/${appId}/versions/${versionId}/${type}`, requestOptions).then(handleResponse); + return fetch(`${config.apiUrl}/v2/apps/${appId}/versions/${versionId}/${type}`, requestOptions).then(handleResponse); } diff --git a/frontend/src/_stores/utils.js b/frontend/src/_stores/utils.js index 428fda719d..921d1b3515 100644 --- a/frontend/src/_stores/utils.js +++ b/frontend/src/_stores/utils.js @@ -48,12 +48,31 @@ export const computeAppDiff = (appDiff, currentPageId, opts) => { let updateDiff; let operation = 'update'; - console.log('---piku [computeAppDiff]', { appDiff, currentPageId, opts }); + if (opts?.deletePageRequest) { + const deletePageId = _.keys(appDiff?.pages).map((pageId) => { + if (appDiff?.pages[pageId]?.pageId === undefined) { + return pageId; + } + })[0]; - if (opts?.pageDefinitionChanged) { + updateDiff = { + pageId: deletePageId, + }; + + type = updateType.pageDefinitionChanged; + operation = 'delete'; + } else if (opts?.pageSortingChanged) { + updateDiff = appDiff?.pages; + + type = updateType.pageDefinitionChanged; + } else if (opts?.pageDefinitionChanged) { updateDiff = appDiff?.pages[currentPageId]; type = updateType.pageDefinitionChanged; + + if (opts?.addNewPage) { + operation = 'create'; + } } else if (opts?.componentDeleted) { const currentPageComponents = appDiff?.pages[currentPageId]?.components; diff --git a/server/migrations/1691004576333-CreatePageTable.ts b/server/migrations/1691004576333-CreatePageTable.ts index 98694a5e34..729cc11349 100644 --- a/server/migrations/1691004576333-CreatePageTable.ts +++ b/server/migrations/1691004576333-CreatePageTable.ts @@ -17,6 +17,11 @@ export class CreatePageTable1691004576333 implements MigrationInterface { type: 'varchar', isNullable: false, }, + { + name: 'index', + type: 'int', + isNullable: false, + }, { name: 'page_handle', type: 'varchar', diff --git a/server/src/controllers/apps.controller.ts b/server/src/controllers/apps.controller.ts index f449fb572e..af89025946 100644 --- a/server/src/controllers/apps.controller.ts +++ b/server/src/controllers/apps.controller.ts @@ -28,7 +28,6 @@ import { EntityManager } from 'typeorm'; 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'; @Controller('apps') @@ -36,7 +35,6 @@ export class AppsController { constructor( private appsService: AppsService, private foldersService: FoldersService, - private componentsService: ComponentsService, private pageService: PageService, private appsAbilityFactory: AppsAbilityFactory ) {} @@ -323,100 +321,6 @@ export class AppsController { await this.appsService.updateVersion(version, versionEditDto, app.organizationId); return; } - @UseGuards(JwtAuthGuard) - @UseInterceptors(ValidAppInterceptor) - @Post(':id/versions/:versionId/components') - async createComponent( - @User() user, - @Param('id') id, - @Param('versionId') versionId, - @Body() versionEditDto: VersionEditDto - ) { - 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(versionEditDto.diff, versionEditDto.pageId); - } - @UseGuards(JwtAuthGuard) - @UseInterceptors(ValidAppInterceptor) - @Put(':id/versions/:versionId/components') - async updateComponent( - @User() user, - @Param('id') id, - @Param('versionId') versionId, - @Body() versionEditDto: VersionEditDto - ) { - 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(versionEditDto.diff); - } - - @UseGuards(JwtAuthGuard) - @UseInterceptors(ValidAppInterceptor) - @Delete(':id/versions/:versionId/components') - async deleteComponents( - @User() user, - @Param('id') id, - @Param('versionId') versionId, - @Body() versionEditDto: VersionEditDto - ) { - 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(versionEditDto.diff); - } - - @UseGuards(JwtAuthGuard) - @UseInterceptors(ValidAppInterceptor) - @Put(':id/versions/:versionId/components/layout') - async updateComponentLayput( - @User() user, - @Param('id') id, - @Param('versionId') versionId, - @Body() versionEditDto: VersionEditDto - ) { - 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(versionEditDto.diff); - } @UseGuards(JwtAuthGuard) @UseInterceptors(ValidAppInterceptor) diff --git a/server/src/controllers/apps.controller.v2.ts b/server/src/controllers/apps.controller.v2.ts index 786bda3135..f3247c0bd5 100644 --- a/server/src/controllers/apps.controller.v2.ts +++ b/server/src/controllers/apps.controller.v2.ts @@ -1,12 +1,262 @@ -import { Controller } from '@nestjs/common'; -// import { JwtAuthGuard } from '../../src/modules/auth/jwt-auth.guard'; +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 { AppsService } from '../services/apps.service'; +import { camelizeKeys, decamelizeKeys } from 'humps'; +import { AppsAbilityFactory } from 'src/modules/casl/abilities/apps-ability.factory'; +// import { AppAuthGuard } from 'src/modules/auth/app-auth.guard'; +import { App } from 'src/entities/app.entity'; +import { User } from 'src/decorators/user.decorator'; + +import { VersionEditDto } from '@dto/version-edit.dto'; +import { CreatePageDto, UpdatePageDto } from '@dto/pages.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'; @Controller({ path: 'apps', - version: '2', // Set the version to '2' + version: '2', }) export class AppsControllerV2 { - constructor(/* Add your services and dependencies here */) {} + constructor( + private appsService: AppsService, + private componentsService: ComponentsService, + private pageService: PageService, + private appsAbilityFactory: AppsAbilityFactory + ) {} - // Add your new version 2 methods here + @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) : []; + + // 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; + + //! 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; + } + + //components api + @UseGuards(JwtAuthGuard) + @UseInterceptors(ValidAppInterceptor) + @Post(':id/versions/:versionId/components') + async createComponent( + @User() user, + @Param('id') id, + @Param('versionId') versionId, + @Body() versionEditDto: VersionEditDto + ) { + 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(versionEditDto.diff, versionEditDto.pageId); + } + + @UseGuards(JwtAuthGuard) + @UseInterceptors(ValidAppInterceptor) + @Put(':id/versions/:versionId/components') + async updateComponent( + @User() user, + @Param('id') id, + @Param('versionId') versionId, + @Body() versionEditDto: VersionEditDto + ) { + 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(versionEditDto.diff); + } + + @UseGuards(JwtAuthGuard) + @UseInterceptors(ValidAppInterceptor) + @Delete(':id/versions/:versionId/components') + async deleteComponents( + @User() user, + @Param('id') id, + @Param('versionId') versionId, + @Body() versionEditDto: VersionEditDto + ) { + 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(versionEditDto.diff); + } + + @UseGuards(JwtAuthGuard) + @UseInterceptors(ValidAppInterceptor) + @Put(':id/versions/:versionId/components/layout') + async updateComponentLayout( + @User() user, + @Param('id') id, + @Param('versionId') versionId, + @Body() versionEditDto: VersionEditDto + ) { + 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(versionEditDto.diff); + } + + // 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) + @Put(':id/versions/:versionId/pages') + async updatePages( + @User() user, + @Param('id') id, + @Param('versionId') versionId, + @Body() updatePageDto: Partial + ) { + 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({ pageId: updatePageDto.pageId, diff: updatePageDto.diff }); + } + + @UseGuards(JwtAuthGuard) + @UseInterceptors(ValidAppInterceptor) + @Delete(':id/versions/:versionId/pages') + async deletePage(@User() user, @Param('id') id, @Param('versionId') versionId, @Body() body) { + 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 { pageId } = body; + + if (pageId) { + await this.pageService.deletePage(pageId, versionId); + } + } } diff --git a/server/src/dto/pages.dto.ts b/server/src/dto/pages.dto.ts new file mode 100644 index 0000000000..f72dbe6d6a --- /dev/null +++ b/server/src/dto/pages.dto.ts @@ -0,0 +1,17 @@ +import { IsNumber, IsString } from 'class-validator'; + +export class CreatePageDto { + @IsString() + name: string; + + @IsString() + handle: string; + + @IsNumber() + index: number; +} + +export class UpdatePageDto { + pageId: string; + diff: Partial; +} diff --git a/server/src/entities/page.entity.ts b/server/src/entities/page.entity.ts index 247ae1178a..ee029c6049 100644 --- a/server/src/entities/page.entity.ts +++ b/server/src/entities/page.entity.ts @@ -11,7 +11,10 @@ export class Page { name: string; @Column({ name: 'page_handle' }) - pageHandle: string; + handle: string; + + @Column() + index: number; @Column({ name: 'app_version_id' }) appVersionId: string; diff --git a/server/src/modules/apps/apps.module.ts b/server/src/modules/apps/apps.module.ts index 6923858b8b..695a52d25b 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'; @@ -83,6 +84,6 @@ import { PageService } from '@services/page.service'; ComponentsService, PageService, ], - controllers: [AppsController, AppUsersController, AppsImportExportController], + controllers: [AppsController, AppsControllerV2, AppUsersController, AppsImportExportController], }) export class AppsModule {} diff --git a/server/src/services/apps.service.ts b/server/src/services/apps.service.ts index b4a53a5cde..0dceab44f2 100644 --- a/server/src/services/apps.service.ts +++ b/server/src/services/apps.service.ts @@ -123,8 +123,9 @@ export class AppsService { const defaultHomePage = await manager.save( manager.create(Page, { name: 'Defualt Page', - pageHandle: 'defaultpage', + handle: 'defaultpage', appVersionId: appVersion.id, + index: 1, }) ); diff --git a/server/src/services/page.service.ts b/server/src/services/page.service.ts index 38574285f2..27c1966d4b 100644 --- a/server/src/services/page.service.ts +++ b/server/src/services/page.service.ts @@ -4,15 +4,18 @@ import { InjectRepository } from '@nestjs/typeorm'; import { Page } from 'src/entities/page.entity'; import { ComponentsService } from './components.service'; +import { CreatePageDto, UpdatePageDto } from '@dto/pages.dto'; +import { AppsService } from './apps.service'; +import { dbTransactionWrap } from 'src/helpers/utils.helper'; @Injectable() export class PageService { constructor( - private readonly manager: EntityManager, @InjectRepository(Page) private readonly pageRepository: Repository, - private componentsService: ComponentsService + private componentsService: ComponentsService, + private appService: AppsService ) {} async findPagesForVersion(appVersionId: string): Promise { @@ -32,4 +35,83 @@ export class PageService { async findOne(id: string): Promise { return this.pageRepository.findOne(id); } + + async createPage(page: CreatePageDto, appVersionId: string): Promise { + const newPage = { + ...page, + appVersionId: appVersionId, + }; + + return this.pageRepository.save(newPage); + } + + async updatePage(pageUpdates: UpdatePageDto) { + if (Object.keys(pageUpdates.diff).length > 1) { + return this.updatePagesOrder(pageUpdates.diff); + } + + const currentPage = await this.pageRepository.findOne(pageUpdates.pageId); + + if (!currentPage) { + throw new Error('Page not found'); + } + return this.pageRepository.update(pageUpdates.pageId, pageUpdates.diff); + } + + async updatePagesOrder(pages) { + const pagesToPage = Object.keys(pages).map((pageId) => { + return { + id: pageId, + index: pages[pageId].index, + }; + }); + + return await dbTransactionWrap(async (manager: EntityManager) => { + await Promise.all( + pagesToPage.map(async (page) => { + await manager.update(Page, page.id, page); + }) + ); + }); + } + + async deletePage(pageId: string, appVersionId: string) { + const pageExists = await this.pageRepository.findOne(pageId); + const { editingVersion } = await this.appService.findAppFromVersion(appVersionId); + + if (!pageExists) { + throw new Error('Page not found'); + } + + if (editingVersion?.homePageId === pageId) { + throw new Error('Cannot delete home page'); + } + const pageDeletedIndex = pageExists.index; + const pageDeleted = await this.pageRepository.delete(pageId); + + if (pageDeleted.affected === 0) { + throw new Error('Page not deleted'); + } + + const pages = await this.pageRepository.find({ appVersionId: pageExists.appVersionId }); + + const rearrangedPages = this.rearrangePagesOnDelete(pages, pageDeletedIndex); + + await this.pageRepository.save(rearrangedPages); + } + + rearrangePagesOnDelete(pages: Page[], pageDeletedIndex: number) { + const rearrangedPages = pages.map((page, index) => { + if (index + 1 >= pageDeletedIndex) { + return { + ...page, + index: page.index - 1, + }; + } + + return page; + }); + + return rearrangedPages; + } } From 4a8296faeac08da997f592e63d8a3acb437f5726 Mon Sep 17 00:00:00 2001 From: arpitnath Date: Mon, 14 Aug 2023 23:42:46 +0530 Subject: [PATCH 021/704] refactored computing types of diff in clientside --- frontend/src/Editor/EditorFunc.jsx | 4 ++-- frontend/src/_stores/utils.js | 31 +++++++++++++++++++++++++++++- 2 files changed, 32 insertions(+), 3 deletions(-) diff --git a/frontend/src/Editor/EditorFunc.jsx b/frontend/src/Editor/EditorFunc.jsx index 5542793b67..255e3bb690 100644 --- a/frontend/src/Editor/EditorFunc.jsx +++ b/frontend/src/Editor/EditorFunc.jsx @@ -887,9 +887,9 @@ const EditorComponent = (props) => { isSaving: false, }); } else if (!isEmpty(props?.editingVersion)) { - const componentDiff = computeAppDiff(appDefinitionDiff, currentPageId, appDiffOptions); + const updateDiff = computeAppDiff(appDefinitionDiff, currentPageId, appDiffOptions); - updateAppVersion(appId, props.editingVersion?.id, currentPageId, componentDiff, isUserSwitchedVersion) + updateAppVersion(appId, props.editingVersion?.id, currentPageId, updateDiff, isUserSwitchedVersion) .then(() => { const _editingVersion = { ...props.editingVersion, diff --git a/frontend/src/_stores/utils.js b/frontend/src/_stores/utils.js index 921d1b3515..a391055ce4 100644 --- a/frontend/src/_stores/utils.js +++ b/frontend/src/_stores/utils.js @@ -44,6 +44,25 @@ const updateType = Object.freeze({ }); export const computeAppDiff = (appDiff, currentPageId, opts) => { + const { updateDiff, type, operation } = updateFor(appDiff, currentPageId, opts); + + return { updateDiff, type, operation }; +}; + +const updateFor = (appDiff, currentPageId, opts) => { + const componentUpdates = ['componentAdded', 'componentDefinitionChanged', 'componentDeleted', 'containerChanges']; + const pageUpdates = ['pageDefinitionChanged', 'pageSortingChanged', 'deletePageRequest', 'addNewPage']; + + const options = _.keys(opts); + + if (_.intersection(options, componentUpdates).length > 0) { + return computeComponentDiff(appDiff, currentPageId, opts); + } else if (_.intersection(options, pageUpdates).length > 0) { + return computePageUpdate(appDiff, currentPageId, opts); + } +}; + +const computePageUpdate = (appDiff, currentPageId, opts) => { let type; let updateDiff; let operation = 'update'; @@ -73,7 +92,17 @@ export const computeAppDiff = (appDiff, currentPageId, opts) => { if (opts?.addNewPage) { operation = 'create'; } - } else if (opts?.componentDeleted) { + } + + return { updateDiff, type, operation }; +}; + +const computeComponentDiff = (appDiff, currentPageId, opts) => { + let type; + let updateDiff; + let operation = 'update'; + + if (opts?.componentDeleted) { const currentPageComponents = appDiff?.pages[currentPageId]?.components; updateDiff = _.keys(currentPageComponents); From 461863d0a2102ca15f1f6370dcb12ad9f012e627 Mon Sep 17 00:00:00 2001 From: arpitnath Date: Tue, 15 Aug 2023 00:38:17 +0530 Subject: [PATCH 022/704] v2 endpoint to update app versions --- frontend/src/Editor/EditorFunc.jsx | 1 - frontend/src/_services/appVersion.service.js | 6 +++-- frontend/src/_stores/utils.js | 7 ++++++ server/src/controllers/apps.controller.v2.ts | 25 +++++++++++++++++++ server/src/dto/app-version-update.dto.ts | 26 ++++++++++++++++++++ server/src/services/apps.service.ts | 11 +++++++++ 6 files changed, 73 insertions(+), 3 deletions(-) create mode 100644 server/src/dto/app-version-update.dto.ts diff --git a/frontend/src/Editor/EditorFunc.jsx b/frontend/src/Editor/EditorFunc.jsx index 255e3bb690..b6cd82805a 100644 --- a/frontend/src/Editor/EditorFunc.jsx +++ b/frontend/src/Editor/EditorFunc.jsx @@ -858,7 +858,6 @@ const EditorComponent = (props) => { const shouldUpdate = !_.isEmpty(diffPatches) && !isEqual(appDefinitionDiff, diffPatches); if (shouldUpdate) { - // const redoPatch = diffToPatches(diffPatches); const undoPatches = diffToPatches(inversePatches); setUndoStack((prev) => [...prev, undoPatches]); diff --git a/frontend/src/_services/appVersion.service.js b/frontend/src/_services/appVersion.service.js index 2fca17429a..131702c17c 100644 --- a/frontend/src/_services/appVersion.service.js +++ b/frontend/src/_services/appVersion.service.js @@ -68,7 +68,7 @@ function autoSaveApp(appId, versionId, diff, type, pageId, operation, isUserSwit let body = {}; - if ((type === 'pages' && operation === 'create') || operation === 'delete') { + if (!type || (type === 'pages' && operation === 'create') || operation === 'delete') { body = { ...diff, }; @@ -82,5 +82,7 @@ function autoSaveApp(appId, versionId, diff, type, pageId, operation, isUserSwit credentials: 'include', body: JSON.stringify(body), }; - return fetch(`${config.apiUrl}/v2/apps/${appId}/versions/${versionId}/${type}`, requestOptions).then(handleResponse); + return fetch(`${config.apiUrl}/v2/apps/${appId}/versions/${versionId}/${type ?? ''}`, requestOptions).then( + handleResponse + ); } diff --git a/frontend/src/_stores/utils.js b/frontend/src/_stores/utils.js index a391055ce4..7ab32bf566 100644 --- a/frontend/src/_stores/utils.js +++ b/frontend/src/_stores/utils.js @@ -52,6 +52,7 @@ export const computeAppDiff = (appDiff, currentPageId, opts) => { const updateFor = (appDiff, currentPageId, opts) => { const componentUpdates = ['componentAdded', 'componentDefinitionChanged', 'componentDeleted', 'containerChanges']; const pageUpdates = ['pageDefinitionChanged', 'pageSortingChanged', 'deletePageRequest', 'addNewPage']; + const appUpdates = ['homePageChanged']; const options = _.keys(opts); @@ -59,6 +60,12 @@ const updateFor = (appDiff, currentPageId, opts) => { return computeComponentDiff(appDiff, currentPageId, opts); } else if (_.intersection(options, pageUpdates).length > 0) { return computePageUpdate(appDiff, currentPageId, opts); + } else if (_.intersection(options, appUpdates).length > 0) { + return { + updateDiff: appDiff, + type: null, + operation: 'update', + }; } }; diff --git a/server/src/controllers/apps.controller.v2.ts b/server/src/controllers/apps.controller.v2.ts index f3247c0bd5..6b3660812c 100644 --- a/server/src/controllers/apps.controller.v2.ts +++ b/server/src/controllers/apps.controller.v2.ts @@ -28,6 +28,7 @@ import { AppDecorator } from 'src/decorators/app.decorator'; import { ComponentsService } from '@services/components.service'; import { PageService } from '@services/page.service'; +import { AppVersionUpdateDto } from '@dto/app-version-update.dto'; @Controller({ path: 'apps', @@ -92,6 +93,30 @@ export class AppsControllerV2 { return response; } + @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); + } + //components api @UseGuards(JwtAuthGuard) @UseInterceptors(ValidAppInterceptor) 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..00e52d4f6c --- /dev/null +++ b/server/src/dto/app-version-update.dto.ts @@ -0,0 +1,26 @@ +import { IsBoolean, IsNotEmpty, IsOptional, IsString, 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; + + @IsString() + @IsOptional() + homePageId: string; + + @IsOptional() + globalSettings: any; +} diff --git a/server/src/services/apps.service.ts b/server/src/services/apps.service.ts index 0dceab44f2..535c8891b0 100644 --- a/server/src/services/apps.service.ts +++ b/server/src/services/apps.service.ts @@ -29,6 +29,7 @@ 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'; @Injectable() export class AppsService { @@ -667,6 +668,16 @@ export class AppsService { return await this.appVersionsRepository.update(version.id, editableParams); } + async updateAppVersion(version: AppVersion, body: AppVersionUpdateDto) { + const editableParams = {}; + + if (body?.homePageId) { + editableParams['homePageId'] = body.homePageId; + } + + return await this.appVersionsRepository.update(version.id, editableParams); + } + convertToArrayOfKeyValuePairs(options): Array { if (!options) return; return Object.keys(options).map((key) => { From c1a42e7012753be87f66bfc7336dd2468fab2996 Mon Sep 17 00:00:00 2001 From: arpitnath Date: Tue, 15 Aug 2023 03:36:40 +0530 Subject: [PATCH 023/704] v2 api for global settings --- frontend/src/Editor/EditorFunc.jsx | 49 ++++----------- frontend/src/Editor/Header/GlobalSettings.jsx | 59 ++++++++----------- frontend/src/_services/appVersion.service.js | 6 +- frontend/src/_stores/utils.js | 7 +++ server/src/controllers/apps.controller.v2.ts | 23 ++++++++ server/src/services/apps.service.ts | 4 ++ 6 files changed, 74 insertions(+), 74 deletions(-) diff --git a/frontend/src/Editor/EditorFunc.jsx b/frontend/src/Editor/EditorFunc.jsx index b6cd82805a..2bc04a8450 100644 --- a/frontend/src/Editor/EditorFunc.jsx +++ b/frontend/src/Editor/EditorFunc.jsx @@ -78,31 +78,6 @@ function setWindowTitle(name) { const decimalToHex = (alpha) => (alpha === 0 ? '00' : Math.round(255 * alpha).toString(16)); -const defaultDefinition = (darkMode) => { - const defaultPageId = uuid(); - return { - showViewerNavigation: true, - homePageId: defaultPageId, - pages: { - [defaultPageId]: { - components: {}, - handle: 'home', - name: 'Home', - index: 0, - }, - }, - globalSettings: { - hideHeader: false, - appInMaintenance: false, - canvasMaxWidth: 1292, - canvasMaxWidthType: 'px', - canvasMaxHeight: 2400, - canvasBackgroundColor: darkMode ? '#2f3c4c' : '#edeff5', - backgroundFxQuery: '', - }, - }; -}; - const EditorComponent = (props) => { const { socket } = createWebsocketConnection(props?.params?.id); const mounted = useMounted(); @@ -569,7 +544,6 @@ const EditorComponent = (props) => { }; const computeCanvasBackgroundColor = () => { - //!Global settings needs to be out const { canvasBackgroundColor } = appDefinition?.globalSettings ?? '#edeff5'; if (['#2f3c4c', '#edeff5'].includes(canvasBackgroundColor)) { return props.darkMode ? '#2f3c4c' : '#edeff5'; @@ -604,19 +578,20 @@ const EditorComponent = (props) => { const handleEditorMarginLeftChange = (value) => setEditorMarginLeft(value); - const globalSettingsChanged = (key, value) => { + const globalSettingsChanged = (globalOptions) => { const copyOfAppDefinition = JSON.parse(JSON.stringify(appDefinition)); const newAppDefinition = _.cloneDeep(copyOfAppDefinition); - if (value?.[1]?.a == undefined) newAppDefinition.globalSettings[key] = value; - else { - const hexCode = `${value?.[0]}${decimalToHex(value?.[1]?.a)}`; - newAppDefinition.globalSettings[key] = hexCode; + for (const [key, value] of Object.entries(globalOptions)) { + if (value?.[1]?.a == undefined) newAppDefinition.globalSettings[key] = value; + else { + const hexCode = `${value?.[0]}${decimalToHex(value?.[1]?.a)}`; + newAppDefinition.globalSettings[key] = hexCode; + } } updateEditorState({ isSaving: true, - // appDefinition, }); appDefinitionChanged(newAppDefinition, { @@ -776,11 +751,11 @@ const EditorComponent = (props) => { // !-------- const setAppDefinitionFromVersion = (version, shouldWeEditVersion = true) => { if (version?.id !== props.editingVersion?.id) { - appDefinitionChanged(defaults(version.definition, defaultDefinition(props.darkMode)), { - skipAutoSave: true, - skipYmapUpdate: true, - versionChanged: true, - }); + // appDefinitionChanged(defaults(version.definition, defaultDefinition(props.darkMode)), { + // skipAutoSave: true, + // skipYmapUpdate: true, + // versionChanged: true, + // }); if (version?.id === currentVersionId) { updateEditorState({ canUndo: false, diff --git a/frontend/src/Editor/Header/GlobalSettings.jsx b/frontend/src/Editor/Header/GlobalSettings.jsx index 9b169ea7dc..a8abcc7bb5 100644 --- a/frontend/src/Editor/Header/GlobalSettings.jsx +++ b/frontend/src/Editor/Header/GlobalSettings.jsx @@ -49,12 +49,6 @@ export const GlobalSettings = ({ // eslint-disable-next-line react-hooks/exhaustive-deps }, [currentState.components]); - React.useEffect(() => { - backgroundFxQuery && - globalSettingsChanged('canvasBackgroundColor', resolveReferences(backgroundFxQuery, realState)); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [JSON.stringify(resolveReferences(backgroundFxQuery, realState))]); - const popoverContent = (
@@ -73,7 +67,7 @@ export const GlobalSettings = ({ className="form-check-input" type="checkbox" checked={hideHeader} - onChange={(e) => globalSettingsChanged('hideHeader', e.target.checked)} + onChange={(e) => globalSettingsChanged({ hideHeader: e.target.checked })} />
@@ -104,7 +98,7 @@ export const GlobalSettings = ({ placeholder={'0'} onChange={(e) => { const width = e.target.value; - if (!Number.isNaN(width) && width >= 0) globalSettingsChanged('canvasMaxWidth', width); + if (!Number.isNaN(width) && width >= 0) globalSettingsChanged({ canvasMaxWidth: width }); }} value={canvasMaxWidth} /> @@ -114,12 +108,17 @@ export const GlobalSettings = ({ aria-label="Select canvas width type" onChange={(event) => { const newCanvasMaxWidthType = event.currentTarget.value; - globalSettingsChanged('canvasMaxWidthType', newCanvasMaxWidthType); + + const options = { + canvasMaxWidthType: newCanvasMaxWidthType, + }; + if (newCanvasMaxWidthType === '%') { - globalSettingsChanged('canvasMaxWidth', 100); + options.canvasMaxWidth = 100; } else if (newCanvasMaxWidthType === 'px') { - globalSettingsChanged('canvasMaxWidth', 1292); + options.canvasMaxWidth = 1292; } + globalSettingsChanged(options); }} >