diff --git a/.version b/.version index 171a6a93d6..4eba2a62eb 100644 --- a/.version +++ b/.version @@ -1 +1 @@ -3.12.1 +3.13.0 diff --git a/frontend/.version b/frontend/.version index 171a6a93d6..4eba2a62eb 100644 --- a/frontend/.version +++ b/frontend/.version @@ -1 +1 @@ -3.12.1 +3.13.0 diff --git a/frontend/ee b/frontend/ee index 518f3334b1..777446d71e 160000 --- a/frontend/ee +++ b/frontend/ee @@ -1 +1 @@ -Subproject commit 518f3334b12a83785fd37dd53b0245d72848211a +Subproject commit 777446d71e78e5941d34353606a12d982820438f diff --git a/frontend/package.json b/frontend/package.json index 45d94532f5..3821a370f5 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -58,6 +58,7 @@ "dotenv": "^16.0.3", "draft-js": "^0.11.7", "draft-js-export-html": "^1.4.1", + "draft-js-import-html": "^1.4.1", "driver.js": "^0.9.8", "emoji-mart": "^5.5.2", "file-loader": "^6.2.0", diff --git a/frontend/src/AppBuilder/AppCanvas/appCanvasUtils.js b/frontend/src/AppBuilder/AppCanvas/appCanvasUtils.js index af092b20e4..9798631f36 100644 --- a/frontend/src/AppBuilder/AppCanvas/appCanvasUtils.js +++ b/frontend/src/AppBuilder/AppCanvas/appCanvasUtils.js @@ -232,6 +232,7 @@ export const getAllChildComponents = (allComponents, parentId) => { const childTabId = componentParentId.split('-').at(-1); if (componentParentId === `${parentId}-${childTabId}`) { childComponent.isParentTabORCalendar = true; + childComponent.events = useStore.getState().eventsSlice.getEventsByComponentsId(componentId); childComponents.push(childComponent); // Recursively find children of the current child component const childrenOfChild = getAllChildComponents(allComponents, componentId); @@ -242,6 +243,7 @@ export const getAllChildComponents = (allComponents, parentId) => { if (componentParentId === parentId) { let childComponent = deepClone(allComponents[componentId]); childComponent.id = componentId; + childComponent.events = useStore.getState().eventsSlice.getEventsByComponentsId(componentId); childComponents.push(childComponent); // Recursively find children of the current child component diff --git a/frontend/src/AppBuilder/CodeEditor/CodehinterOverlayTriggers.jsx b/frontend/src/AppBuilder/CodeEditor/CodehinterOverlayTriggers.jsx new file mode 100644 index 0000000000..c79c473169 --- /dev/null +++ b/frontend/src/AppBuilder/CodeEditor/CodehinterOverlayTriggers.jsx @@ -0,0 +1,27 @@ +/* eslint-disable import/no-unresolved */ +import React from 'react'; +import { openSearchPanel } from '@codemirror/search'; +import './SearchBox.scss'; +import { Button as ButtonComponent } from '@/components/ui/Button/Button.jsx'; + +export const CodeHinterBtns = ({ view, isPanelOpen, renderCopilot }) => { + return ( +
+ {!isPanelOpen && ( + openSearchPanel(view)} + /> + )} + {renderCopilot && renderCopilot()} +
+ ); +}; diff --git a/frontend/src/AppBuilder/CodeEditor/MultiLineCodeEditor.jsx b/frontend/src/AppBuilder/CodeEditor/MultiLineCodeEditor.jsx index cbebcb0425..98af1dc9e4 100644 --- a/frontend/src/AppBuilder/CodeEditor/MultiLineCodeEditor.jsx +++ b/frontend/src/AppBuilder/CodeEditor/MultiLineCodeEditor.jsx @@ -20,10 +20,12 @@ import { PreviewBox } from './PreviewBox'; import { removeNestedDoubleCurlyBraces } from '@/_helpers/utils'; import useStore from '@/AppBuilder/_stores/store'; import { shallow } from 'zustand/shallow'; +import { syntaxTree } from '@codemirror/language'; import { search, searchKeymap, searchPanelOpen } from '@codemirror/search'; -import { handleSearchPanel, SearchBtn } from './SearchBox'; +import { handleSearchPanel } from './SearchBox'; import { useQueryPanelKeyHooks } from './useQueryPanelKeyHooks'; import { isInsideParent } from './utils'; +import { CodeHinterBtns } from './CodehinterOverlayTriggers'; const langSupport = Object.freeze({ javascript: javascript(), @@ -66,7 +68,7 @@ const MultiLineCodeEditor = (props) => { const context = useContext(CodeHinterContext); - const { suggestionList } = createReferencesLookup(context, true); + const { suggestionList: paramList } = createReferencesLookup(context, true); const currentValueRef = useRef(initialValue); @@ -74,6 +76,7 @@ const MultiLineCodeEditor = (props) => { const [editorView, setEditorView] = React.useState(null); + const [isSearchPanelOpen, setIsSearchPanelOpen] = React.useState(false); const { queryPanelKeybindings } = useQueryPanelKeyHooks(onChange, currentValueRef, 'multiline'); const handleOnBlur = () => { @@ -146,8 +149,29 @@ const MultiLineCodeEditor = (props) => { return suggestion.hint.includes(nearestSubstring); }); + const localVariables = new Set(); + + // Traverse the syntax tree to extract variable declarations + syntaxTree(context.state).iterate({ + enter: (node) => { + // JavaScript: Detect variable declarations (var, let, const) + if (node.name === 'VariableDefinition') { + const varName = context.state.sliceDoc(node.from, node.to); + if (varName && varName.startsWith(nearestSubstring)) localVariables.add(varName); + } + }, + }); + + // Convert Set to an array of completion suggestions + const localVariableSuggestions = [...localVariables].map((varName) => ({ + hint: varName, + type: 'variable', + })); + + const suggestionList = paramList.filter((paramSuggestion) => paramSuggestion.hint.includes(nearestSubstring)); + const suggestions = generateHints( - [...JSLangHints, ...autoSuggestionList, ...suggestionList], + [...localVariableSuggestions, ...JSLangHints, ...autoSuggestionList, ...suggestionList], null, nearestSubstring ).map((hint) => { @@ -204,6 +228,7 @@ const MultiLineCodeEditor = (props) => { return { from: context.pos, options: [...suggestions], + filter: false, }; } @@ -237,7 +262,7 @@ const MultiLineCodeEditor = (props) => { ]); // eslint-disable-next-line react-hooks/exhaustive-deps - const overRideFunction = React.useCallback((context) => autoCompleteExtensionConfig(context), []); + const overRideFunction = React.useCallback((context) => autoCompleteExtensionConfig(context), [paramList]); const { handleTogglePopupExapand, isOpen, setIsOpen, forceUpdate } = portalProps; let cyLabel = paramLabel ? paramLabel.toLowerCase().trim().replace(/\s+/g, '-') : props.cyLabel; @@ -258,7 +283,7 @@ const MultiLineCodeEditor = (props) => { ref={wrapperRef} >
- + { isMultiEditor={true} isQueryManager={isInsideQueryPane} /> - {renderCopilot && renderCopilot()} { readOnly={readOnly} editable={editable} //for transformations in query manager onCreateEditor={(view) => setEditorView(view)} - onUpdate={(view) => { - const icon = document.querySelector('.codehinter-search-btn'); - if (searchPanelOpen(view.state)) { - icon.style.display = 'none'; - } else icon.style.display = 'block'; - }} + onUpdate={(view) => setIsSearchPanelOpen(searchPanelOpen(view.state))} />
{showPreview && ( diff --git a/frontend/src/AppBuilder/CodeEditor/SearchBox.jsx b/frontend/src/AppBuilder/CodeEditor/SearchBox.jsx index 28f7451b95..140ff2a7db 100644 --- a/frontend/src/AppBuilder/CodeEditor/SearchBox.jsx +++ b/frontend/src/AppBuilder/CodeEditor/SearchBox.jsx @@ -9,7 +9,6 @@ import { findPrevious, replaceNext, replaceAll, - openSearchPanel, } from '@codemirror/search'; import './SearchBox.scss'; import InputComponent from '@/components/ui/Input/Index.jsx'; @@ -162,22 +161,3 @@ function SearchPanel({ view }) { ); } - -export const SearchBtn = ({ view }) => { - return ( -
- openSearchPanel(view)} - /> -
- ); -}; diff --git a/frontend/src/AppBuilder/CodeEditor/SearchBox.scss b/frontend/src/AppBuilder/CodeEditor/SearchBox.scss index 24c948b0ee..79de28f28a 100644 --- a/frontend/src/AppBuilder/CodeEditor/SearchBox.scss +++ b/frontend/src/AppBuilder/CodeEditor/SearchBox.scss @@ -44,7 +44,5 @@ } .code-hinter-wrapper .codehinter-search-btn { - display: block; - padding-top: 1px; - z-index: 10000; + z-index: 1000; } \ No newline at end of file diff --git a/frontend/src/AppBuilder/CodeEditor/SingleLineCodeEditor.jsx b/frontend/src/AppBuilder/CodeEditor/SingleLineCodeEditor.jsx index 65e6f2eadd..16514ca409 100644 --- a/frontend/src/AppBuilder/CodeEditor/SingleLineCodeEditor.jsx +++ b/frontend/src/AppBuilder/CodeEditor/SingleLineCodeEditor.jsx @@ -1,12 +1,18 @@ /* eslint-disable import/no-unresolved */ -import React, { useEffect, useMemo, useRef, useState } from 'react'; +import React, { useEffect, useMemo, useRef, useState, useContext } from 'react'; import { PreviewBox } from './PreviewBox'; import { ToolTip } from '@/Editor/Inspector/Elements/Components/ToolTip'; import { useTranslation } from 'react-i18next'; import { camelCase, isEmpty, noop, get } from 'lodash'; import CodeMirror from '@uiw/react-codemirror'; import { javascript } from '@codemirror/lang-javascript'; -import { autocompletion, completionKeymap, completionStatus, acceptCompletion } from '@codemirror/autocomplete'; +import { + autocompletion, + completionKeymap, + completionStatus, + acceptCompletion, + startCompletion, +} from '@codemirror/autocomplete'; import { defaultKeymap } from '@codemirror/commands'; import { keymap } from '@codemirror/view'; import FxButton from '../CodeBuilder/Elements/FxButton'; @@ -22,6 +28,8 @@ import CodeHinter from './CodeHinter'; import { removeNestedDoubleCurlyBraces } from '@/_helpers/utils'; import useStore from '@/AppBuilder/_stores/store'; import { shallow } from 'zustand/shallow'; +import { CodeHinterContext } from '../CodeBuilder/CodeHinterContext'; +import { createReferencesLookup } from '@/_stores/utils'; import { useQueryPanelKeyHooks } from './useQueryPanelKeyHooks'; const SingleLineCodeEditor = ({ componentName, fieldMeta = {}, componentId, ...restProps }) => { @@ -73,6 +81,7 @@ const SingleLineCodeEditor = ({ componentName, fieldMeta = {}, componentId, ...r if (typeof initialValue === 'string' && (initialValue?.includes('components') || initialValue?.includes('queries'))) { newInitialValue = replaceIdsWithName(initialValue); } + //! Re render the component when the componentName changes as the initialValue is not updated // const { variablesExposedForPreview } = useContext(EditorContext) || {}; @@ -199,9 +208,14 @@ const EditorInput = ({ wrapperRef, showSuggestions, }) => { - const getServerSideGlobalSuggestions = useStore((state) => state.getServerSideGlobalSuggestions, shallow); + const codeHinterContext = useContext(CodeHinterContext); + const { suggestionList: paramHints } = createReferencesLookup(codeHinterContext, true); const getSuggestions = useStore((state) => state.getSuggestions, shallow); + const [codeMirrorView, setCodeMirrorView] = useState(undefined); + + const getServerSideGlobalSuggestions = useStore((state) => state.getServerSideGlobalSuggestions, shallow); + const { queryPanelKeybindings } = useQueryPanelKeyHooks(onBlurUpdate, currentValue, 'singleline'); const isInsideQueryManager = useMemo( @@ -209,16 +223,16 @@ const EditorInput = ({ [wrapperRef.current] ); function autoCompleteExtensionConfig(context) { - const hints = getSuggestions(); + const hintsWithoutParamHints = getSuggestions(); const serverHints = getServerSideGlobalSuggestions(isInsideQueryManager); - const allHints = { - ...hints, - appHints: [...hints.appHints, ...serverHints], - }; - let word = context.matchBefore(/\w*/); + const hints = { + ...hintsWithoutParamHints, + appHints: [...hintsWithoutParamHints.appHints, ...serverHints, ...paramHints], + }; + const totalReferences = (context.state.doc.toString().match(/{{/g) || []).length; let queryInput = context.state.doc.toString(); @@ -247,17 +261,18 @@ const EditorInput = ({ queryInput = '{{' + currentWord + '}}'; } - let completions = getAutocompletion(queryInput, validationType, allHints, totalReferences, originalQueryInput); + let completions = getAutocompletion(queryInput, validationType, hints, totalReferences, originalQueryInput); return { from: word.from, options: completions, validFor: /^\{\{.*\}\}$/, + filter: false, }; } // eslint-disable-next-line react-hooks/exhaustive-deps - const overRideFunction = React.useCallback((context) => autoCompleteExtensionConfig(context), [isInsideQueryManager]); + const overRideFunction = React.useCallback((context) => autoCompleteExtensionConfig(context), [isInsideQueryManager, paramHints]); const autoCompleteConfig = autocompletion({ override: [overRideFunction], @@ -424,6 +439,9 @@ const EditorInput = ({ ref={previewRef} > { + setCodeMirrorView(view); + }} value={currentValue} placeholder={placeholder} height={isInsideQueryPane ? '100%' : showLineNumbers ? '400px' : '100%'} @@ -460,11 +478,16 @@ const EditorInput = ({ theme={theme} indentWithTab={false} readOnly={disabled} + onKeyDown={(event) => { + if (event.key === 'Backspace') { + startCompletion(codeMirrorView); + } + }} /> - - - + + + ); }; diff --git a/frontend/src/AppBuilder/CodeEditor/autocompleteExtensionConfig.js b/frontend/src/AppBuilder/CodeEditor/autocompleteExtensionConfig.js index e1d597c957..d845fa521e 100644 --- a/frontend/src/AppBuilder/CodeEditor/autocompleteExtensionConfig.js +++ b/frontend/src/AppBuilder/CodeEditor/autocompleteExtensionConfig.js @@ -67,7 +67,8 @@ export const getAutocompletion = (input, fieldType, hints, totalReferences = 1, originalQueryInput, searchInput ); - return orderSuggestions(suggestions, fieldType); + + return suggestions; }; function orderSuggestions(suggestions, validationType) { @@ -90,10 +91,18 @@ export const generateHints = (hints, totalReferences = 1, input, searchText) => const hasDepth = currentWord.includes('.'); const lastDepth = getLastSubstring(currentWord); - const displayLabel = getLastDepth(displayedHint); + let displayLabel = getLastDepth(displayedHint); + + if (type != 'js_method') { + const currentWordDepth = currentWord.split('.').length; + displayLabel = hint + .split('.') + .slice(currentWordDepth - 1) + .join('.'); + } return { - displayLabel: lastDepth === '' ? displayedHint : displayLabel, + displayLabel, label: displayedHint, info: displayedHint, type: type === 'js_method' ? 'js_methods' : type?.toLowerCase(), @@ -154,40 +163,24 @@ export const generateHints = (hints, totalReferences = 1, input, searchText) => }; function filterHintsByDepth(input, hints) { - if (input === '') return hints; + const inputParts = input.split('.'); + const inputDepth = inputParts.length + 1; - const inputDepth = input.includes('.') ? input.split('.').length : 0; - - const filteredHints = hints.filter((cm) => { - const hintParts = cm.hint.split('.'); - - let shouldInclude = - (cm.hint.startsWith(input) && hintParts.length === inputDepth + 1) || - (cm.hint.startsWith(input) && hintParts.length === inputDepth); - - const shouldFuzzyMatch = !shouldInclude ? hintParts.length > inputDepth : false; - - if (shouldFuzzyMatch) { - // fuzzy match - let matchedDepth = -1; - for (let i = 0; i < hintParts.length; i++) { - if (hintParts[i].includes(input)) { - matchedDepth = i; - break; - } - } - - if (matchedDepth !== -1) { - shouldInclude = hintParts.length === matchedDepth + 1; - } - } else if (input.endsWith('.')) { - shouldInclude = cm.hint.startsWith(input) && hintParts.length === inputDepth; - } - - return shouldInclude; + const hintsWithDepth = hints.map((hint) => { + const hintParts = hint.hint.split('.'); + return { + ...hint, + depth: hintParts.length, + }; }); - return filteredHints; + const filteredHints = hintsWithDepth.filter((hint) => { + return hint.depth <= inputDepth; + }); + + const sortedHints = filteredHints.sort((hint1, hint2) => hint1.depth - hint2.depth); + + return sortedHints; } export function findNearestSubstring(inputStr, currentCurosorPos) { diff --git a/frontend/src/AppBuilder/QueryManager/Components/ChangeDataSource.jsx b/frontend/src/AppBuilder/QueryManager/Components/ChangeDataSource.jsx index 244668cf8a..85958cd97b 100644 --- a/frontend/src/AppBuilder/QueryManager/Components/ChangeDataSource.jsx +++ b/frontend/src/AppBuilder/QueryManager/Components/ChangeDataSource.jsx @@ -1,8 +1,20 @@ -import React from 'react'; +import React, { useState } from 'react'; import Select from '@/_ui/Select'; import { decodeEntities } from '@/_helpers/utils'; +import usePopoverObserver from '@/AppBuilder/_hooks/usePopoverObserver'; export const ChangeDataSource = ({ dataSources, onChange, value, isVersionReleased }) => { + const [isMenuOpen, setIsMenuOpen] = useState(false); + + usePopoverObserver( + document.getElementsByClassName('query-details')[0], + document.querySelector('.change-data-source-select.react-select__control'), + document.querySelector('.change-data-source-select.react-select__menu'), + isMenuOpen, + () => (document.querySelector('.change-data-source-select.react-select__menu').style.display = 'block'), + () => (document.querySelector('.change-data-source-select.react-select__menu').style.display = 'none') + ); + return ( + { + const componentTypes = ['Steps']; + const batchSize = 100; + const entityManager = queryRunner.manager; + + for (const componentType of componentTypes) { + await processDataInBatches( + entityManager, + async (entityManager: EntityManager) => { + return await entityManager.find(Component, { + where: { type: componentType }, + order: { createdAt: 'ASC' }, + }); + }, + async (entityManager: EntityManager, components: Component[]) => { + await this.processUpdates(entityManager, components); + }, + batchSize + ); + } + } + + public async down(queryRunner: QueryRunner): Promise {} + + private async processUpdates(entityManager, components) { + for (const component of components) { + const properties = component.properties; + const styles = component.styles; + const general = component.general; + const generalStyles = component.generalStyles; + const validation = component.validation; + + if (styles.visibility) { + properties.visibility = styles.visibility; + delete styles.visibility; + } + if (styles.theme) { + properties['variant'] = styles.theme; + delete styles.theme; + } + if (styles.color) { + styles['completedAccent'] = styles.color; + } + delete styles.color; + if (styles.textColor) { + styles['completedLabel'] = styles.textColor; + styles['incompletedLabel'] = styles.textColor; + styles['currentStepLabel'] = styles.textColor; + } + delete styles.textColor; + if (properties.steps) { + properties['schema'] = properties.steps; + delete properties.steps; + properties['advanced'] = { value: '{{true}}' }; + } + + // if (properties.stepsSelectable) { + // properties.disabledState = styles.disabledState; + // delete styles.disabledState; + // } + + // if (generalStyles?.boxShadow) { + // styles.boxShadow = generalStyles?.boxShadow; + // delete generalStyles?.boxShadow; + // } + + await entityManager.update(Component, component.id, { + properties, + styles, + general, + generalStyles, + validation, + }); + } + } +} diff --git a/server/ee b/server/ee index b9e73f87b9..30dbfa7545 160000 --- a/server/ee +++ b/server/ee @@ -1 +1 @@ -Subproject commit b9e73f87b9062e06c49c2c73add6b82ba21dcacf +Subproject commit 30dbfa754562d00f8d64181d5006e113798bd668 diff --git a/server/src/dto/validators/tooljet-database.validator.ts b/server/src/dto/validators/tooljet-database.validator.ts index 6ebea6bd17..164087bd0e 100644 --- a/server/src/dto/validators/tooljet-database.validator.ts +++ b/server/src/dto/validators/tooljet-database.validator.ts @@ -10,6 +10,7 @@ import Ajv from 'ajv'; import * as path from 'path'; import * as fs from 'fs'; import { ImportResourcesDto } from '@dto/import-resources.dto'; +import { AppImportRequestDto } from '@modules/external-apis/dto'; const ajv = new Ajv({ allErrors: true, coerceTypes: true }); const logger = new Logger('TooljetDatabaseSchemaValidator'); @@ -109,3 +110,15 @@ export function ValidateTooljetDatabaseSchema(validationOptions?: ValidationOpti }); }; } + +export function ValidateTooljetDatabaseImportSchema(validationOptions?: ValidationOptions) { + return function (object: AppImportRequestDto, propertyName: string) { + registerDecorator({ + target: object.constructor, + propertyName: propertyName, + options: validationOptions, + constraints: [], + validator: ValidateTooljetDatabaseConstraint, + }); + }; +} diff --git a/server/src/modules/apps/module.ts b/server/src/modules/apps/module.ts index 15b5903fb2..6565c17ed1 100644 --- a/server/src/modules/apps/module.ts +++ b/server/src/modules/apps/module.ts @@ -21,6 +21,7 @@ import { AppsSubscriber } from './subscribers/apps.subscriber'; import { AiModule } from '@modules/ai/module'; import { AppPermissionsModule } from '@modules/app-permissions/module'; import { RolesRepository } from '@modules/roles/repository'; +import { UsersModule } from '@modules/users/module'; @Module({}) export class AppsModule { static async register(configs: { IS_GET_CONTEXT: boolean }): Promise { @@ -55,6 +56,7 @@ export class AppsModule { await DataSourcesModule.register(configs), await AiModule.register(configs), await AppPermissionsModule.register(configs), + await UsersModule.register(configs), ], controllers: [AppsController], providers: [ @@ -74,7 +76,7 @@ export class AppsModule { AppImportExportService, RolesRepository, ], - exports: [AppsUtilService], + exports: [AppsUtilService, AppImportExportService], }; } } diff --git a/server/src/modules/apps/repository.ts b/server/src/modules/apps/repository.ts index 98c76b5634..172689e303 100644 --- a/server/src/modules/apps/repository.ts +++ b/server/src/modules/apps/repository.ts @@ -2,6 +2,7 @@ import { App } from '@entities/app.entity'; import { Injectable } from '@nestjs/common'; import { DataSource, Repository } from 'typeorm'; import { SessionAppData } from './types'; +import { WorkspaceAppsResponseDto } from '@modules/external-apis/dto'; @Injectable() export class AppsRepository extends Repository { @@ -63,4 +64,23 @@ export class AppsRepository extends Repository { }, }); } + + async findAllOrganizationApps(organizationId: string): Promise { + return await this.createQueryBuilder('app') + .select([ + 'app.id AS id', + 'app.name AS name', + 'app.slug AS slug', + 'app.created_at AS createdAt', + 'app.organization_id AS organizationId', + 'version.id AS versionId', + 'version.name AS versionName', + 'version.created_at AS versionCreatedAt', + ]) + .leftJoin('app_versions', 'version', 'version.app_id = app.id') + .where('app.organizationId = :organizationId', { organizationId }) + .orderBy('app.created_At', 'ASC') + .orderBy('version.created_at', 'ASC') + .getRawMany(); + } } diff --git a/server/src/modules/apps/service.ts b/server/src/modules/apps/service.ts index f76ce660ab..27fd03c034 100644 --- a/server/src/modules/apps/service.ts +++ b/server/src/modules/apps/service.ts @@ -29,9 +29,6 @@ import { VersionRepository } from '@modules/versions/repository'; import { AppsRepository } from './repository'; import { FoldersUtilService } from '@modules/folders/util.service'; import { FolderAppsUtilService } from '@modules/folder-apps/util.service'; -import { DataQuery } from '@entities/data_query.entity'; -import { DataSource } from '@entities/data_source.entity'; -import { AppVersion } from '@entities/app_version.entity'; import { PageService } from './services/page.service'; import { EventsService } from './services/event.service'; import { LICENSE_FIELD } from '@modules/licensing/constants'; @@ -224,40 +221,7 @@ export class AppsService implements IAppsService { } async findTooljetDbTables(appId: string): Promise<{ table_id: string }[]> { - return await dbTransactionWrap(async (manager: EntityManager) => { - const tooljetDbDataQueries = await manager - .createQueryBuilder(DataQuery, 'data_queries') - .innerJoin(DataSource, 'data_sources', 'data_queries.data_source_id = data_sources.id') - .innerJoin(AppVersion, 'app_versions', 'app_versions.id = data_sources.app_version_id') - .where('app_versions.app_id = :appId', { appId }) - .andWhere('data_sources.kind = :kind', { kind: 'tooljetdb' }) - .getMany(); - - const uniqTableIds = new Set(); - tooljetDbDataQueries.forEach((dq) => { - if (dq.options?.operation === 'join_tables') { - const joinOptions = dq.options?.join_table?.joins ?? []; - (joinOptions || []).forEach((join) => { - const { table, conditions } = join; - if (table) uniqTableIds.add(table); - conditions?.conditionsList?.forEach((condition) => { - const { leftField, rightField } = condition; - if (leftField?.table) { - uniqTableIds.add(leftField?.table); - } - if (rightField?.table) { - uniqTableIds.add(rightField?.table); - } - }); - }); - } - if (dq.options.table_id) uniqTableIds.add(dq.options.table_id); - }); - - return [...uniqTableIds].map((table_id) => { - return { table_id }; - }); - }); + return await this.appsUtilService.findTooljetDbTables(appId); //moved to util } async getOne(app: App, user: User): Promise { diff --git a/server/src/modules/apps/services/app-import-export.service.ts b/server/src/modules/apps/services/app-import-export.service.ts index 6cd6b3ce8f..16fa8289f6 100644 --- a/server/src/modules/apps/services/app-import-export.service.ts +++ b/server/src/modules/apps/services/app-import-export.service.ts @@ -33,6 +33,7 @@ import { DataSourcesUtilService } from '@modules/data-sources/util.service'; import { DataSourcesRepository } from '@modules/data-sources/repository'; import { AppEnvironmentUtilService } from '@modules/app-environments/util.service'; import { ComponentsService } from './component.service'; +import { UsersUtilService } from '@modules/users/util.service'; interface AppResourceMappings { defaultDataSourceIdMapping: Record; dataQueryMapping: Record; @@ -51,7 +52,17 @@ type DefaultDataSourceName = | 'tooljetdbdefault' | 'workflowsdefault'; -type NewRevampedComponent = 'Text' | 'TextInput' | 'PasswordInput' | 'NumberInput' | 'Table' | 'Button' | 'Checkbox' | 'Divider' | 'VerticalDivider' | 'Link'; +type NewRevampedComponent = + | 'Text' + | 'TextInput' + | 'PasswordInput' + | 'NumberInput' + | 'Table' + | 'Button' + | 'Checkbox' + | 'Divider' + | 'VerticalDivider' + | 'Link'; const DefaultDataSourceNames: DefaultDataSourceName[] = [ 'restapidefault', @@ -80,9 +91,10 @@ export class AppImportExportService { protected dataSourcesUtilService: DataSourcesUtilService, protected dataSourcesRepository: DataSourcesRepository, protected appEnvironmentUtilService: AppEnvironmentUtilService, + protected usersUtilService: UsersUtilService, protected readonly entityManager: EntityManager, protected componentsService: ComponentsService - ) { } + ) {} async export(user: User, id: string, searchParams: any = {}): Promise<{ appV2: App }> { // https://github.com/typeorm/typeorm/issues/3857 @@ -94,7 +106,7 @@ export class AppImportExportService { .createQueryBuilder(App, 'apps') .where('apps.id = :id AND apps.organization_id = :organizationId', { id, - organizationId: user.organizationId, + organizationId: user?.organizationId, }); const appToExport = await queryForAppToExport.getOne(); @@ -123,7 +135,7 @@ export class AppImportExportService { const appEnvironments = await manager .createQueryBuilder(AppEnvironment, 'app_environments') .where('app_environments.organizationId = :organizationId', { - organizationId: user.organizationId, + organizationId: user?.organizationId, }) .orderBy('app_environments.createdAt', 'ASC') .getMany(); @@ -184,13 +196,13 @@ export class AppImportExportService { const components = pages.length > 0 ? await manager - .createQueryBuilder(Component, 'components') - .leftJoinAndSelect('components.layouts', 'layouts') - .where('components.pageId IN(:...pageId)', { - pageId: pages.map((v) => v.id), - }) - .orderBy('components.created_at', 'ASC') - .getMany() + .createQueryBuilder(Component, 'components') + .leftJoinAndSelect('components.layouts', 'layouts') + .where('components.pageId IN(:...pageId)', { + pageId: pages.map((v) => v.id), + }) + .orderBy('components.created_at', 'ASC') + .getMany() : []; const events = await manager @@ -340,8 +352,8 @@ export class AppImportExportService { return await catchDbException(async () => { const importedApp = manager.create(App, { name: appParams.name, - organizationId: user.organizationId, - userId: user.id, + organizationId: user?.organizationId, + userId: user.id, //fetch super admin user id for EE slug: null, icon: appParams.icon, creationMode: `${isGitApp ? 'GIT' : 'DEFAULT'}`, @@ -762,7 +774,7 @@ export class AppImportExportService { const { dataQueryMapping } = await this.createDataQueriesForAppVersion( manager, - user.organizationId, + user?.organizationId, importingDataQueriesForAppVersion, importingDataSource, dataSourceForAppVersion, @@ -1059,10 +1071,10 @@ export class AppImportExportService { const options = importingDataSource.kind === 'tooljetdb' ? this.replaceTooljetDbTableIds( - importingQuery.options, - externalResourceMappings['tooljet_database'], - organizationId - ) + importingQuery.options, + externalResourceMappings['tooljet_database'], + organizationId + ) : importingQuery.options; const newQuery = manager.create(DataQuery, { @@ -1153,7 +1165,7 @@ export class AppImportExportService { appResourceMappings: AppResourceMappings ) { const defaultDataSourceIds = await this.createDefaultDataSourceForVersion( - user.organizationId, + user?.organizationId, appResourceMappings.appVersionMapping[appVersion.id], DefaultDataSourceKinds, manager @@ -1192,7 +1204,7 @@ export class AppImportExportService { kind: dataSource.kind, type: DataSourceTypes.DEFAULT, scope: 'global', - organizationId: user.organizationId, + organizationId: user?.organizationId, }, }); }; @@ -1203,7 +1215,7 @@ export class AppImportExportService { kind: dataSource.kind, type: In([DataSourceTypes.DEFAULT, DataSourceTypes.SAMPLE]), scope: 'global', - organizationId: user.organizationId, + organizationId: user?.organizationId, }, }); }; @@ -1221,7 +1233,7 @@ export class AppImportExportService { if (plugin) { const newDataSource = manager.create(DataSource, { - organizationId: user.organizationId, + organizationId: user?.organizationId, name: dataSource.name, kind: dataSource.kind, type: DataSourceTypes.DEFAULT, @@ -1236,7 +1248,7 @@ export class AppImportExportService { const createNewGlobalDs = async (ds: DataSource): Promise => { const newDataSource = manager.create(DataSource, { - organizationId: user.organizationId, + organizationId: user?.organizationId, name: dataSource.name, kind: dataSource.kind, type: DataSourceTypes.DEFAULT, @@ -1264,7 +1276,7 @@ export class AppImportExportService { ) { appResourceMappings = { ...appResourceMappings }; const currentOrgEnvironments = await this.appEnvironmentUtilService.getAll( - user.organizationId, + user?.organizationId, appVersion.appId, manager ); @@ -1326,7 +1338,7 @@ export class AppImportExportService { appResourceMappings = { ...appResourceMappings }; const { appVersionMapping, appDefaultEnvironmentMapping } = appResourceMappings; const organization: Organization = await manager.findOne(Organization, { - where: { id: user.organizationId }, + where: { id: user?.organizationId }, relations: ['appEnvironments'], }); let currentEnvironmentId: string; @@ -1545,7 +1557,7 @@ export class AppImportExportService { // Create default data sources const defaultDataSourceIds = await this.createDefaultDataSourceForVersion( - user.organizationId, + user?.organizationId, version.id, DefaultDataSourceKinds, manager @@ -1553,7 +1565,7 @@ export class AppImportExportService { let envIdArray: string[] = []; const organization: Organization = await manager.findOne(Organization, { - where: { id: user.organizationId }, + where: { id: user?.organizationId }, relations: ['appEnvironments'], }); envIdArray = [...organization.appEnvironments.map((env) => env.id)]; @@ -1562,7 +1574,7 @@ export class AppImportExportService { await Promise.all( defaultAppEnvironments.map(async (en) => { const env = manager.create(AppEnvironment, { - organizationId: user.organizationId, + organizationId: user?.organizationId, name: en.name, isDefault: en.isDefault, priority: en.priority, @@ -1627,10 +1639,10 @@ export class AppImportExportService { options: dataSourceId == defaultDataSourceIds['tooljetdb'] ? this.replaceTooljetDbTableIds( - query.options, - externalResourceMappings['tooljet_database'], - user.organizationId - ) + query.options, + externalResourceMappings['tooljet_database'], + user?.organizationId + ) : query.options, }); await manager.save(newQuery); diff --git a/server/src/modules/apps/services/component.service.ts b/server/src/modules/apps/services/component.service.ts index a7538f5f40..fcc01e52f0 100644 --- a/server/src/modules/apps/services/component.service.ts +++ b/server/src/modules/apps/services/component.service.ts @@ -95,7 +95,9 @@ export class ComponentsService implements IComponentsService { if (componentData.type === 'Table' && _.isArray(objValue)) { return srcValue; } else if ( - (componentData.type === 'DropdownV2' || componentData.type === 'MultiselectV2') && + (componentData.type === 'DropdownV2' || + componentData.type === 'MultiselectV2' || + componentData.type === 'Steps') && _.isArray(objValue) ) { return _.isArray(srcValue) ? srcValue : Object.values(srcValue); diff --git a/server/src/modules/apps/services/widget-config/container.js b/server/src/modules/apps/services/widget-config/container.js index 406e4fa8bb..ed04d8e730 100644 --- a/server/src/modules/apps/services/widget-config/container.js +++ b/server/src/modules/apps/services/widget-config/container.js @@ -3,8 +3,8 @@ export const containerConfig = { displayName: 'Container', description: 'Group components', defaultSize: { - width: 10, - height: 200, + width: 13, + height: 480, }, component: 'Container', others: { diff --git a/server/src/modules/apps/services/widget-config/steps.js b/server/src/modules/apps/services/widget-config/steps.js index c8b9753d9a..4400a19137 100644 --- a/server/src/modules/apps/services/widget-config/steps.js +++ b/server/src/modules/apps/services/widget-config/steps.js @@ -4,25 +4,38 @@ export const stepsConfig = { description: 'Step-by-step navigation aid', component: 'Steps', properties: { + variant: { + type: 'switch', + displayName: 'Variant', + validation: { schema: { type: 'string' }, defaultValue: 'titles' }, + options: [ + { displayName: 'Label', value: 'titles' }, + { displayName: 'Number', value: 'numbers' }, + { displayName: 'Plain', value: 'plain' }, + ], + accordian: 'label', + }, + schema: { + type: 'code', + displayName: 'Schema', + conditionallyRender: { + key: 'advanced', + value: true, + }, + accordian: 'Options', + }, steps: { type: 'code', - displayName: 'Steps', + displayName: '', + showLabel: false, validation: { schema: { type: 'array', - element: { type: 'object', object: { id: { type: 'number' } } }, + element: { type: 'object' }, }, defaultValue: `[{ name: 'step 1'}, {name: 'step 2'}]`, }, }, - currentStep: { - type: 'code', - displayName: 'Current step', - validation: { - schema: { type: 'number' }, - defaultValue: 1, - }, - }, stepsSelectable: { type: 'toggle', displayName: 'Steps selectable', @@ -30,6 +43,36 @@ export const stepsConfig = { schema: { type: 'boolean' }, defaultValue: false, }, + section: 'additionalActions', + }, + disabledState: { + type: 'toggle', + displayName: 'Disable', + validation: { schema: { type: 'boolean' } }, + section: 'additionalActions', + }, + visibility: { + type: 'toggle', + displayName: 'Visibility', + validation: { schema: { type: 'boolean' }, defaultValue: true }, + section: 'additionalActions', + }, + advanced: { + type: 'toggle', + displayName: 'Dynamic options', + validation: { + schema: { type: 'boolean' }, + defaultValue: true, + }, + accordian: 'Options', + }, + currentStep: { + type: 'code', + displayName: 'Current step', + validation: { + schema: { type: 'number' }, + defaultValue: 1, + }, }, }, defaultSize: { @@ -40,46 +83,126 @@ export const stepsConfig = { showOnDesktop: { type: 'toggle', displayName: 'Show on desktop' }, showOnMobile: { type: 'toggle', displayName: 'Show on mobile' }, }, + actions: [ + { + handle: 'setStep', + displayName: 'Set step', + params: [ + { + handle: 'option', + displayName: 'Option', + }, + ], + }, + { + handle: 'setVisibility', + displayName: 'Set visibility', + params: [{ handle: 'visible', displayName: 'Value', defaultValue: '{{false}}', type: 'toggle' }], + }, + { + handle: 'setDisabled', + displayName: 'Set disabled', + params: [{ handle: 'disable', displayName: 'Value', defaultValue: '{{true}}', type: 'toggle' }], + }, + { + handle: 'resetSteps', + displayName: 'Reset steps', + params: [], + }, + { + handle: 'setStepVisible', + displayName: 'Set step visible', + params: [ + { + handle: 'id', + displayName: 'Step id', + }, + { + handle: 'visibility', + displayName: 'visibility', + defaultValue: '{{false}}', + type: 'toggle', + }, + ], + }, + { + handle: 'setStepDisable', + displayName: 'Set step disable', + params: [ + { + handle: 'id', + displayName: 'Step id', + }, + { + handle: 'disabled', + displayName: 'disabled', + defaultValue: '{{true}}', + type: 'toggle', + }, + ], + }, + ], events: { onSelect: { displayName: 'On select' }, }, styles: { - color: { + incompletedAccent: { type: 'colorSwatches', - displayName: 'Color', + displayName: 'Incompleted accent', + validation: { + schema: { type: 'string' }, + defaultValue: '#CCD1D5', + }, + accordian: 'steps', + }, + incompletedLabel: { + type: 'colorSwatches', + displayName: 'Incompleted label', + validation: { + schema: { type: 'string' }, + defaultValue: '#1B1F24', + }, + accordian: 'steps', + }, + completedAccent: { + type: 'colorSwatches', + displayName: 'Completed accent', validation: { schema: { type: 'string' }, defaultValue: 'var(--primary-brand)', }, + accordian: 'steps', }, - textColor: { + completedLabel: { type: 'colorSwatches', - displayName: 'Text color', + displayName: 'Completed label', validation: { schema: { type: 'string' }, - defaultValue: '#000000', + defaultValue: '#1B1F24', }, + accordian: 'steps', }, - theme: { - type: 'select', - displayName: 'Theme', + currentStepLabel: { + type: 'colorSwatches', + displayName: 'Current step label', + validation: { + schema: { type: 'string' }, + defaultValue: '#1B1F24', + }, + accordian: 'steps', + }, + padding: { + type: 'switch', + displayName: 'Padding', + validation: { + schema: { type: 'union', schemas: [{ type: 'string' }, { type: 'number' }] }, + defaultValue: 'default', + }, options: [ - { name: 'titles', value: 'titles' }, - { name: 'numbers', value: 'numbers' }, - { name: 'plain', value: 'plain' }, + { displayName: 'Default', value: 'default' }, + { displayName: 'None', value: 'none' }, ], - validation: { - schema: { type: 'string' }, - defaultValue: 'titles', - }, - }, - visibility: { - type: 'toggle', - displayName: 'Visibility', - validation: { - schema: { type: 'boolean' }, - defaultValue: true, - }, + accordian: 'container', }, }, exposedVariables: { @@ -92,17 +215,35 @@ export const stepsConfig = { }, properties: { steps: { - value: `{{ [{ name: 'step 1', tooltip: 'some tooltip', id: 1},{ name: 'step 2', tooltip: 'some tooltip', id: 2},{ name: 'step 3', tooltip: 'some tooltip', id: 3},{ name: 'step 4', tooltip: 'some tooltip', id: 4},{ name: 'step 5', tooltip: 'some tooltip', id: 5}]}}`, + value: [ + { name: 'step 1', tooltip: '', id: 1, visible: { value: true }, disabled: { value: false } }, + { name: 'step 2', tooltip: '', id: 2, visible: { value: true }, disabled: { value: false } }, + { name: 'step 3', tooltip: '', id: 3, visible: { value: true }, disabled: { value: false } }, + { name: 'step 4', tooltip: '', id: 4, visible: { value: true }, disabled: { value: false } }, + { name: 'step 5', tooltip: '', id: 5, visible: { value: true }, disabled: { value: false } }, + ], }, + schema: { + value: `{{ [{ name: 'step 1', tooltip: '', id: 1,visible: true, disabled: false},{ name: 'step 2', tooltip: '', id: 2,visible: true, disabled: false},{ name: 'step 3', tooltip: '', id: 3,visible: true, disabled: false},{ name: 'step 4', tooltip: '', id: 4,visible: true, disabled: false},{ name: 'step 5', tooltip: '', id: 5,visible: true, disabled: false}]}}`, + }, + disabledState: { value: '{{false}}' }, + variant: { value: 'titles' }, currentStep: { value: '{{3}}' }, stepsSelectable: { value: true }, + advanced: { value: `{{false}}` }, + visibility: { value: '{{true}}' }, }, events: [], styles: { visibility: { value: '{{true}}' }, - theme: { value: 'titles' }, - color: { value: 'var(--primary-brand)' }, - textColor: { value: '' }, + // color: { value: '' }, + // textColor: { value: '' }, + padding: { value: 'default' }, + incompletedAccent: { value: '#E4E7EB' }, + incompletedLabel: { value: '#1B1F24' }, + completedAccent: { value: '#4368E3' }, + completedLabel: { value: '#1B1F24' }, + currentStepLabel: { value: '#1B1F24' }, }, }, }; diff --git a/server/src/modules/apps/util.service.ts b/server/src/modules/apps/util.service.ts index 36def10913..3db7df4a99 100644 --- a/server/src/modules/apps/util.service.ts +++ b/server/src/modules/apps/util.service.ts @@ -37,6 +37,9 @@ import { DataSourcesRepository } from '@modules/data-sources/repository'; import { IAppsUtilService } from './interfaces/IUtilService'; import { DataSourcesUtilService } from '@modules/data-sources/util.service'; import { AppVersionUpdateDto } from '@dto/app-version-update.dto'; +import { WorkspaceAppsResponseDto } from '@modules/external-apis/dto'; +import { DataQuery } from '@entities/data_query.entity'; +import { DataSource } from '@entities/data_source.entity'; @Injectable() export class AppsUtilService implements IAppsUtilService { @@ -487,7 +490,7 @@ export class AppsUtilService implements IAppsUtilService { if (['Table'].includes(currentComponentData?.component?.component) && isArray(objValue)) { return srcValue; } else if ( - ['DropdownV2', 'MultiselectV2'].includes(currentComponentData?.component?.component) && + ['DropdownV2', 'MultiselectV2', 'Steps'].includes(currentComponentData?.component?.component) && isArray(objValue) ) { return isArray(srcValue) ? srcValue : Object.values(srcValue); @@ -522,4 +525,45 @@ export class AppsUtilService implements IAppsUtilService { return components; } + + async findAllOrganizationApps(organizationId: string): Promise { + return await this.appRepository.findAllOrganizationApps(organizationId); + } + + async findTooljetDbTables(appId: string): Promise<{ table_id: string }[]> { + return await dbTransactionWrap(async (manager: EntityManager) => { + const tooljetDbDataQueries = await manager + .createQueryBuilder(DataQuery, 'data_queries') + .innerJoin(DataSource, 'data_sources', 'data_queries.data_source_id = data_sources.id') + .innerJoin(AppVersion, 'app_versions', 'app_versions.id = data_sources.app_version_id') + .where('app_versions.app_id = :appId', { appId }) + .andWhere('data_sources.kind = :kind', { kind: 'tooljetdb' }) + .getMany(); + + const uniqTableIds = new Set(); + tooljetDbDataQueries.forEach((dq) => { + if (dq.options?.operation === 'join_tables') { + const joinOptions = dq.options?.join_table?.joins ?? []; + (joinOptions || []).forEach((join) => { + const { table, conditions } = join; + if (table) uniqTableIds.add(table); + conditions?.conditionsList?.forEach((condition) => { + const { leftField, rightField } = condition; + if (leftField?.table) { + uniqTableIds.add(leftField?.table); + } + if (rightField?.table) { + uniqTableIds.add(rightField?.table); + } + }); + }); + } + if (dq.options.table_id) uniqTableIds.add(dq.options.table_id); + }); + + return [...uniqTableIds].map((table_id) => { + return { table_id }; + }); + }); + } } diff --git a/server/src/modules/auth/guards/external-api-security.guard.ts b/server/src/modules/auth/guards/external-api-security.guard.ts index 0d6ab91863..f6aced14d5 100644 --- a/server/src/modules/auth/guards/external-api-security.guard.ts +++ b/server/src/modules/auth/guards/external-api-security.guard.ts @@ -14,7 +14,7 @@ export class ExternalApiSecurityGuard implements CanActivate { throw new ForbiddenException('External API is disabled'); } - // Check the authorization header + // // Check the authorization header const authHeader = request.headers['authorization']; const externalApiAccessToken = this.configService.get('EXTERNAL_API_ACCESS_TOKEN'); diff --git a/server/src/modules/data-queries/service.ts b/server/src/modules/data-queries/service.ts index 48aa236cee..5337c6739a 100644 --- a/server/src/modules/data-queries/service.ts +++ b/server/src/modules/data-queries/service.ts @@ -22,7 +22,7 @@ export class DataQueriesService implements IDataQueriesService { protected readonly dataQueryRepository: DataQueryRepository, protected readonly dataQueryUtilService: DataQueriesUtilService, protected readonly dataSourceRepository: DataSourcesRepository - ) {} + ) { } async getAll(versionId: string) { const queries = await this.dataQueryRepository.getAll(versionId); @@ -30,9 +30,6 @@ export class DataQueriesService implements IDataQueriesService { // serialize for (const query of queries) { - if (query.dataSource.type === DataSourceTypes.STATIC) { - delete query['dataSourceId']; - } delete query['dataSource']; const decamelizeQuery = decamelizeKeys(query); diff --git a/server/src/modules/external-apis/constants/feature.ts b/server/src/modules/external-apis/constants/feature.ts index 6dcccf4e70..5551dd64e0 100644 --- a/server/src/modules/external-apis/constants/feature.ts +++ b/server/src/modules/external-apis/constants/feature.ts @@ -37,5 +37,17 @@ export const FEATURES: FeaturesConfig = { license: LICENSE_FIELD.EXTERNAL_API, isPublic: true, }, + [FEATURE_KEY.GET_ALL_WORKSPACE_APPS]: { + license: LICENSE_FIELD.EXTERNAL_API, + isPublic: true, + }, + [FEATURE_KEY.IMPORT_APP]: { + license: LICENSE_FIELD.EXTERNAL_API, + isPublic: true, + }, + [FEATURE_KEY.EXPORT_APP]: { + license: LICENSE_FIELD.EXTERNAL_API, + isPublic: true, + }, }, }; diff --git a/server/src/modules/external-apis/constants/index.ts b/server/src/modules/external-apis/constants/index.ts index 8fdca84235..97030f67a7 100644 --- a/server/src/modules/external-apis/constants/index.ts +++ b/server/src/modules/external-apis/constants/index.ts @@ -7,4 +7,41 @@ export enum FEATURE_KEY { UPDATE_USER_WORKSPACE = 'UPDATE_USER_WORKSPACE', GET_ALL_WORKSPACES = 'GET_ALL_WORKSPACES', UPDATE_USER_ROLE = 'UPDATE_USER_ROLE', + GET_ALL_WORKSPACE_APPS = 'GET_ALL_WORKSPACE_APPS', + IMPORT_APP = 'IMPORT_APP', + EXPORT_APP = 'EXPORT_APP', } + +export type DefaultDataSourceKind = 'restapi' | 'runjs' | 'runpy' | 'tooljetdb' | 'workflows'; +export type NewRevampedComponent = + | 'Text' + | 'TextInput' + | 'PasswordInput' + | 'NumberInput' + | 'Table' + | 'Button' + | 'Checkbox'; +export type DefaultDataSourceName = + | 'restapidefault' + | 'runjsdefault' + | 'runpydefault' + | 'tooljetdbdefault' + | 'workflowsdefault'; + +export const DefaultDataSourceKinds: DefaultDataSourceKind[] = ['restapi', 'runjs', 'runpy', 'tooljetdb', 'workflows']; +export const DefaultDataSourceNames: DefaultDataSourceName[] = [ + 'restapidefault', + 'runjsdefault', + 'runpydefault', + 'tooljetdbdefault', + 'workflowsdefault', +]; +export const NewRevampedComponents: NewRevampedComponent[] = [ + 'Text', + 'TextInput', + 'PasswordInput', + 'NumberInput', + 'Table', + 'Checkbox', + 'Button', +]; diff --git a/server/src/modules/external-apis/controller.ts b/server/src/modules/external-apis/controller.ts index 3a6f533243..7180ea23fb 100644 --- a/server/src/modules/external-apis/controller.ts +++ b/server/src/modules/external-apis/controller.ts @@ -1,8 +1,8 @@ import { Controller, Get, Param, UseGuards, Body, Patch, Post, Put, NotFoundException } from '@nestjs/common'; -import { ExternalApiSecurityGuard } from './guards/external-api-security.guard'; import { UpdateUserDto, WorkspaceDto, UpdateGivenWorkspaceDto, CreateUserDto } from './dto'; import { IExternalApisController } from './Interfaces/IController'; import { EditUserRoleDto } from '@modules/roles/dto'; +import { ExternalApiSecurityGuard } from '@modules/auth/guards/external-api-security.guard'; @Controller('ext') export class ExternalApisController implements IExternalApisController { diff --git a/server/src/modules/external-apis/dto/index.ts b/server/src/modules/external-apis/dto/index.ts index 71fe51141b..9d8d6a0f9d 100644 --- a/server/src/modules/external-apis/dto/index.ts +++ b/server/src/modules/external-apis/dto/index.ts @@ -10,10 +10,13 @@ import { MaxLength, ValidateIf, IsNotEmpty, + IsDefined, + IsObject, } from 'class-validator'; -import { Type } from 'class-transformer'; +import { Transform, Type } from 'class-transformer'; import { USER_ROLE } from '@modules/group-permissions/constants'; - +import { TjdbSchemaToLatestVersion } from '@dto/transformers/resource-transformer'; +import { ValidateTooljetDatabaseImportSchema } from '@dto/validators/tooljet-database.validator'; export enum Status { ACTIVE = 'active', ARCHIVED = 'archived', @@ -131,3 +134,73 @@ export class UpdateUserWorkspaceDto { @IsOptional() groups?: GroupDto[]; } + +export class VersionDto { + id: string; + name: string; + createdAt?: Date; +} + +export class AppWithVersionsDto { + id: string; + name: string; + slug: string; + createdAt: Date; + organizationId: string; + versions: VersionDto[]; + versionCount: number; +} + +export class WorkspaceAppsResponseDto { + apps: AppWithVersionsDto[]; + total: number; +} + +export class AppImportRequestDto { + @IsString() + tooljet_version: string; + + // TODO: Add transformation and validation for app similar to tooljet_database + @IsOptional() + app: AppImportDto[]; + + // Optional parameter -> To be provided in import request to import app with custom name. + @IsOptional() + @IsString() + appName: string; + + // TJ-DB field + @IsOptional() + // Transform the input data to the latest schema version + // This should be applied first to ensure the data is in + // the correct format before validation + @Transform(TjdbSchemaToLatestVersion) + @ValidateNested({ each: true }) + // Ensure each item is properly instantiated as ImportTooljetDatabaseDto + // This is crucial for nested validation to work correctly + @Type(() => ImportTooljetDatabaseDto) + // Custom validator to check against the tooljet database schema + // This should be applied last to validate the transformed + // and instantiated data + @ValidateTooljetDatabaseImportSchema({ each: true }) + tooljet_database: ImportTooljetDatabaseDto[]; +} +export class AppImportDto { + @IsDefined() + @IsObject() + definition: any; +} + +export class ImportTooljetDatabaseDto { + @IsUUID() + id: string; + + @IsString() + table_name: string; + + @IsDefined() + schema: any; + + // @IsOptional() + // data: boolean; +} diff --git a/server/src/modules/external-apis/guards/external-api-security.guard.ts b/server/src/modules/external-apis/guards/external-api-security.guard.ts deleted file mode 100644 index 0d6ab91863..0000000000 --- a/server/src/modules/external-apis/guards/external-api-security.guard.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { Injectable, CanActivate, ExecutionContext, ForbiddenException } from '@nestjs/common'; -import { ConfigService } from '@nestjs/config'; - -@Injectable() -export class ExternalApiSecurityGuard implements CanActivate { - constructor(protected configService: ConfigService) {} - - canActivate(context: ExecutionContext): boolean { - const request = context.switchToHttp().getRequest(); - - // Check if external API is enabled - const isExternalApiEnabled = this.configService.get('ENABLE_EXTERNAL_API') === 'true'; - if (!isExternalApiEnabled) { - throw new ForbiddenException('External API is disabled'); - } - - // Check the authorization header - const authHeader = request.headers['authorization']; - const externalApiAccessToken = this.configService.get('EXTERNAL_API_ACCESS_TOKEN'); - - if (!authHeader || authHeader !== `Basic ${externalApiAccessToken}`) { - throw new ForbiddenException('Unauthorized'); - } - - return true; - } -} diff --git a/server/src/modules/external-apis/module.ts b/server/src/modules/external-apis/module.ts index c4a209c0bc..22621363e6 100644 --- a/server/src/modules/external-apis/module.ts +++ b/server/src/modules/external-apis/module.ts @@ -3,8 +3,12 @@ import { GroupPermissionsModule } from '@modules/group-permissions/module'; import { RolesModule } from '@modules/roles/module'; import { DynamicModule } from '@nestjs/common'; import { getImportPath } from '@modules/app/constants'; -import { ExternalApiSecurityGuard } from './guards/external-api-security.guard'; import { RolesRepository } from '@modules/roles/repository'; +import { TooljetDbModule } from '@modules/tooljet-db/module'; +import { AppsModule } from '@modules/apps/module'; +import { OrganizationsModule } from '@modules/organizations/module'; +import { VersionModule } from '@modules/versions/module'; +import { UsersModule } from '@modules/users/module'; export class ExternalApiModule { static async register(configs?: { IS_GET_CONTEXT: boolean }): Promise { const importPath = await getImportPath(configs?.IS_GET_CONTEXT); @@ -14,14 +18,16 @@ export class ExternalApiModule { return { module: ExternalApiModule, - imports: [await RolesModule.register(configs), await GroupPermissionsModule.register(configs)], - providers: [ - ExternalApiUtilService, - ExternalApisService, - ExternalApiSecurityGuard, - FeatureAbilityFactory, - RolesRepository, + imports: [ + await UsersModule.register(configs), + await RolesModule.register(configs), + await GroupPermissionsModule.register(configs), + await TooljetDbModule.register(configs), + await AppsModule.register(configs), + await OrganizationsModule.register(configs), + await VersionModule.register(configs), ], + providers: [ExternalApiUtilService, ExternalApisService, FeatureAbilityFactory, RolesRepository], controllers: [ExternalApisController], exports: [ExternalApiUtilService], }; diff --git a/server/src/modules/external-apis/types/index.ts b/server/src/modules/external-apis/types/index.ts index 8c782681a6..5ed8d5c323 100644 --- a/server/src/modules/external-apis/types/index.ts +++ b/server/src/modules/external-apis/types/index.ts @@ -11,6 +11,9 @@ interface Features { [FEATURE_KEY.UPDATE_USER_WORKSPACE]: FeatureConfig; [FEATURE_KEY.GET_ALL_WORKSPACES]: FeatureConfig; [FEATURE_KEY.UPDATE_USER_ROLE]: FeatureConfig; + [FEATURE_KEY.GET_ALL_WORKSPACE_APPS]: FeatureConfig; + [FEATURE_KEY.IMPORT_APP]: FeatureConfig; + [FEATURE_KEY.EXPORT_APP]: FeatureConfig; } export interface FeaturesConfig { @@ -22,3 +25,13 @@ export interface ValidateEditUserGroupAdditionObject { groupsToAddIds: string[]; organizationId: string; } + +export interface AppResourceMappings { + defaultDataSourceIdMapping: Record; + dataQueryMapping: Record; + appVersionMapping: Record; + appEnvironmentMapping: Record; + appDefaultEnvironmentMapping: Record; + pagesMapping: Record; + componentsMapping: Record; +} diff --git a/server/src/modules/organization-themes/constants/index.ts b/server/src/modules/organization-themes/constants/index.ts index 5f8041bc52..581c2181c0 100644 --- a/server/src/modules/organization-themes/constants/index.ts +++ b/server/src/modules/organization-themes/constants/index.ts @@ -31,11 +31,11 @@ export const TJDefaultTheme: Definition = { light: '#1B1F24', dark: '#CFD3D8', }, - secondary: { + placeholder: { light: '#6A727C', dark: '#858C94', }, - tertiary: { + disabled: { light: '#ACB2B9', dark: '#545B64', }, @@ -48,15 +48,15 @@ export const TJDefaultTheme: Definition = { large: 0, }, colors: { - primary: { + default: { light: '#CCD1D5', dark: '#3C434B', }, - secondary: { + weak: { light: '#E4E7EB', dark: '#EEF0F1', }, - tertiary: { + disabled: { light: '#E4E7EB', dark: '#F6F8FA', }, @@ -64,15 +64,15 @@ export const TJDefaultTheme: Definition = { }, systemStatus: { colors: { - primary: { + success: { light: '#1E823B', dark: '#318344', }, - secondary: { + error: { light: '#D72D39', dark: '#D03F43', }, - tertiary: { + warning: { light: '#BF4F03', dark: '#BA5722', }, @@ -84,6 +84,18 @@ export const TJDefaultTheme: Definition = { light: '#F6F6F6', dark: '#121518', }, + surface1: { + light: '#FFFFFF', + dark: '#1E2226', + }, + surface2: { + light: '#F6F8FA', + dark: '#2B3036', + }, + surface3: { + light: '#E4E7EB', + dark: '#3C434B', + }, }, }, }; diff --git a/server/src/modules/organization-themes/dto/index.ts b/server/src/modules/organization-themes/dto/index.ts index c87f7b1565..eb7ac608cd 100644 --- a/server/src/modules/organization-themes/dto/index.ts +++ b/server/src/modules/organization-themes/dto/index.ts @@ -33,12 +33,12 @@ class TextColors { @IsOptional() @ValidateNested() @Type(() => Color) - secondary?: Color; + placeholder?: Color; @IsOptional() @ValidateNested() @Type(() => Color) - tertiary?: Color; + disabled?: Color; } class Text { @@ -64,17 +64,17 @@ class BorderRadius { class BorderColors { @ValidateNested() @Type(() => Color) - primary: Color; + default: Color; @IsOptional() @ValidateNested() @Type(() => Color) - secondary?: Color; + weak?: Color; @IsOptional() @ValidateNested() @Type(() => Color) - tertiary?: Color; + disabled?: Color; } class Border { @@ -90,17 +90,17 @@ class Border { class SystemStatusColors { @ValidateNested() @Type(() => Color) - primary: Color; + success: Color; @IsOptional() @ValidateNested() @Type(() => Color) - secondary?: Color; + error?: Color; @IsOptional() @ValidateNested() @Type(() => Color) - tertiary?: Color; + warning?: Color; } class SystemStatus { @@ -121,6 +121,18 @@ class SurfaceColors { @ValidateNested() @Type(() => AppBackgroundColor) appBackground: AppBackgroundColor; + + @ValidateNested() + @Type(() => Color) + surface1: Color; + + @ValidateNested() + @Type(() => Color) + surface2: Color; + + @ValidateNested() + @Type(() => Color) + surface3: Color; } class Surface { diff --git a/server/src/modules/organizations/interfaces/IUtilService.ts b/server/src/modules/organizations/interfaces/IUtilService.ts new file mode 100644 index 0000000000..a6162c3cef --- /dev/null +++ b/server/src/modules/organizations/interfaces/IUtilService.ts @@ -0,0 +1,3 @@ +export interface IOrganizationUtilService { + validateWorkspaceExists(workspaceId: string): Promise; +} diff --git a/server/src/modules/organizations/module.ts b/server/src/modules/organizations/module.ts index b7432d5e4c..e533607557 100644 --- a/server/src/modules/organizations/module.ts +++ b/server/src/modules/organizations/module.ts @@ -2,6 +2,7 @@ import { DynamicModule } from '@nestjs/common'; import { getImportPath } from '@modules/app/constants'; import { InstanceSettingsModule } from '@modules/instance-settings/module'; import { OrganizationRepository } from './repository'; +import { AppEnvironmentsModule } from '@modules/app-environments/module'; export class OrganizationsModule { static async register(configs?: { IS_GET_CONTEXT: boolean }): Promise { @@ -9,13 +10,14 @@ export class OrganizationsModule { const { OrganizationsService } = await import(`${importPath}/organizations/service`); const { OrganizationsController } = await import(`${importPath}/organizations/controller`); const { FeatureAbilityFactory } = await import(`${importPath}/organizations/ability`); - const { AppEnvironmentUtilService } = await import(`${importPath}/app-environments/util.service`); + const { OrganizationsUtilService } = await import(`${importPath}/organizations/util.service`); return { module: OrganizationsModule, - imports: [await InstanceSettingsModule.register(configs)], + imports: [await InstanceSettingsModule.register(configs), await AppEnvironmentsModule.register(configs)], controllers: [OrganizationsController], - providers: [OrganizationsService, OrganizationRepository, FeatureAbilityFactory, AppEnvironmentUtilService], + providers: [OrganizationsService, OrganizationRepository, FeatureAbilityFactory, OrganizationsUtilService], + exports: [OrganizationsUtilService], }; } } diff --git a/server/src/modules/organizations/util.service.ts b/server/src/modules/organizations/util.service.ts new file mode 100644 index 0000000000..9d501eb7f5 --- /dev/null +++ b/server/src/modules/organizations/util.service.ts @@ -0,0 +1,18 @@ +import { Injectable } from '@nestjs/common'; +import { OrganizationRepository } from './repository'; +import { BadRequestException } from '@nestjs/common'; +import { IOrganizationUtilService } from './interfaces/IUtilService'; + +@Injectable() +export class OrganizationsUtilService implements IOrganizationUtilService { + constructor(protected readonly organizationRepository: OrganizationRepository) {} + + async validateWorkspaceExists(workspaceId: string) { + const existingWorkspace = await this.organizationRepository.findOne({ + where: { id: workspaceId }, + }); + if (!existingWorkspace) { + throw new BadRequestException(`Invalid workspaceId: ${workspaceId}`); + } + } +} diff --git a/server/src/modules/roles/service.ts b/server/src/modules/roles/service.ts index d638f10635..85bc6f87d7 100644 --- a/server/src/modules/roles/service.ts +++ b/server/src/modules/roles/service.ts @@ -1,13 +1,13 @@ import { BadRequestException, Injectable } from '@nestjs/common'; import { EditUserRoleDto } from './dto'; import { RolesUtilService } from './util.service'; -import { ERROR_HANDLER } from '../group-permissions/constants/error'; -import { _ } from 'lodash'; -import { LicenseUserService } from '@modules/licensing/services/user.service'; -import { dbTransactionWrap } from '@helpers/database.helper'; -import { EntityManager } from 'typeorm'; import { RolesRepository } from './repository'; import { IRolesService } from './interfaces/IService'; +import { EntityManager } from 'typeorm'; +import { dbTransactionWrap } from '@helpers/database.helper'; +import { LicenseUserService } from '@modules/licensing/services/user.service'; +import { ERROR_HANDLER } from '@modules/group-permissions/constants/error'; +import { _ } from 'lodash'; @Injectable() export class RolesService implements IRolesService { diff --git a/server/src/modules/users/module.ts b/server/src/modules/users/module.ts index bd91972dba..963eb08fca 100644 --- a/server/src/modules/users/module.ts +++ b/server/src/modules/users/module.ts @@ -16,6 +16,7 @@ export class UsersModule { imports: [await SessionModule.register(configs)], controllers: [UsersController], providers: [UsersService, UserRepository, UsersUtilService, FeatureAbilityFactory], + exports: [UsersUtilService], }; } } diff --git a/server/src/modules/users/repository.ts b/server/src/modules/users/repository.ts index 236e8de8fa..e3417e5e50 100644 --- a/server/src/modules/users/repository.ts +++ b/server/src/modules/users/repository.ts @@ -18,6 +18,7 @@ import { Organization } from '@entities/organization.entity'; import { OrganizationUser } from '@entities/organization_user.entity'; import { isSuperAdmin } from '@helpers/utils.helper'; import * as uuid from 'uuid'; +import { USER_ROLE } from '@modules/group-permissions/constants'; type UserFilterOptions = { searchText?: string; status?: string; page?: number }; @@ -168,6 +169,18 @@ export class UserRepository extends Repository { await manager.upsert(UserDetails, updatableParams, conflictsPaths); } + async getUserWithAdminRole(organizationId: string, manager?: EntityManager): Promise { + return dbTransactionWrap((manager: EntityManager) => { + return manager + .createQueryBuilder(User, 'user') + .innerJoin('user.userGroups', 'groupUsers') + .innerJoin('groupUsers.group', 'group') + .where('group.name = :groupName', { groupName: USER_ROLE.ADMIN }) + .andWhere('group.organizationId = :organizationId', { organizationId }) + .getOne(); + }, manager || this.manager); + } + async findByEmail( email: string, organizationId?: string, diff --git a/server/src/modules/versions/interfaces/IUtilService.ts b/server/src/modules/versions/interfaces/IUtilService.ts index 2b384ff789..768de2afdb 100644 --- a/server/src/modules/versions/interfaces/IUtilService.ts +++ b/server/src/modules/versions/interfaces/IUtilService.ts @@ -3,4 +3,5 @@ import { AppVersionUpdateDto } from '@dto/app-version-update.dto'; export interface IVersionUtilService { updateVersion(appVersion: AppVersion, appVersionUpdateDto: AppVersionUpdateDto): Promise; + fetchVersions(appId: string): Promise; } diff --git a/server/src/modules/versions/module.ts b/server/src/modules/versions/module.ts index 1f08dc76bb..261401a18d 100644 --- a/server/src/modules/versions/module.ts +++ b/server/src/modules/versions/module.ts @@ -51,6 +51,7 @@ export class VersionModule { VersionUtilService, FeatureAbilityFactory, ], + exports: [VersionUtilService], }; } } diff --git a/server/src/modules/versions/util.service.ts b/server/src/modules/versions/util.service.ts index f5723e0377..124a3c2ab3 100644 --- a/server/src/modules/versions/util.service.ts +++ b/server/src/modules/versions/util.service.ts @@ -72,4 +72,13 @@ export class VersionUtilService implements IVersionUtilService { await this.versionRepository.update(appVersion.id, editableParams); return; } + + async fetchVersions(appId: string): Promise { + return await this.versionRepository.find({ + where: { appId }, + order: { + createdAt: 'DESC', + }, + }); + } }