diff --git a/.version b/.version index 92536a9e48..4eba2a62eb 100644 --- a/.version +++ b/.version @@ -1 +1 @@ -3.12.0 +3.13.0 diff --git a/frontend/.version b/frontend/.version index 92536a9e48..4eba2a62eb 100644 --- a/frontend/.version +++ b/frontend/.version @@ -1 +1 @@ -3.12.0 +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 913d3a22df..5f362ba0b3 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 ( + { + if (getTooljetEdition() !== TOOLJET_EDITIONS.EE) { + console.log('Skipping migration as it is not EE edition'); + return; + } + + // Check if default workspace URL is configured + const defaultWorkspaceUrl = getCustomEnvVars('TOOLJET_DEFAULT_WORKSPACE_URL'); + if (defaultWorkspaceUrl) { + try { + const url = new URL(defaultWorkspaceUrl); + const pathParts = url.pathname.split('/'); + const workspaceSlug = pathParts[pathParts.length - 1]; + if (workspaceSlug) { + const organization = await queryRunner.manager.findOne(Organization, { + where: { slug: workspaceSlug, status: WORKSPACE_STATUS.ACTIVE }, + select: ['id'], + }); + if (organization){ + await queryRunner.query(` + UPDATE organizations + SET is_default = true + WHERE slug = $1 + `, [workspaceSlug]); + return; + } + console.log(`No active organization found with slug: ${workspaceSlug}`); + } + } catch (err) { + console.log('Invalid TOOLJET_DEFAULT_WORKSPACE_URL format'); + } + } + + // Set the first created organization as default + await queryRunner.query(` + UPDATE organizations + SET is_default = true + WHERE id = ( + SELECT id + FROM organizations + WHERE status = '${WORKSPACE_STATUS.ACTIVE}' + ORDER BY created_at ASC + LIMIT 1 + ); + `); + } + + public async down(queryRunner: QueryRunner): Promise { + if (getTooljetEdition() !== TOOLJET_EDITIONS.EE) { + return; + } + + // Unset all default workspaces + await queryRunner.query(` + UPDATE organizations + SET is_default = false; + `); + } +} diff --git a/server/data-migrations/1742369436314-StepsV2Migration.ts b/server/data-migrations/1742369436314-StepsV2Migration.ts new file mode 100644 index 0000000000..dcb041db1f --- /dev/null +++ b/server/data-migrations/1742369436314-StepsV2Migration.ts @@ -0,0 +1,81 @@ +import { Component } from '@entities/component.entity'; +import { EntityManager, MigrationInterface, QueryRunner } from 'typeorm'; +import { processDataInBatches } from '@helpers/migration.helper'; + +export class StepsV2Migration1742369436314 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + 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/migrations/1740401000000-AddIsDefaultToOrganizations.ts b/server/migrations/1740401000000-AddIsDefaultToOrganizations.ts new file mode 100644 index 0000000000..6df32a7117 --- /dev/null +++ b/server/migrations/1740401000000-AddIsDefaultToOrganizations.ts @@ -0,0 +1,30 @@ +import { MigrationInterface, QueryRunner, TableColumn } from 'typeorm'; + +export class AddIsDefaultToOrganizations1740401000000 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + // Add is_default column + await queryRunner.addColumn( + 'organizations', + new TableColumn({ + name: 'is_default', + type: 'boolean', + default: false, + isNullable: false, + }) + ); + + // Create a partial unique index to ensure only one default workspace + await queryRunner.query(` + CREATE UNIQUE INDEX idx_organizations_single_default + ON organizations (is_default) + WHERE is_default = true; + `); + } + + public async down(queryRunner: QueryRunner): Promise { + // Drop the unique index first + await queryRunner.query(`DROP INDEX IF EXISTS idx_organizations_single_default;`); + // Then drop the column + await queryRunner.dropColumn('organizations', 'is_default'); + } +} 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/entities/organization.entity.ts b/server/src/entities/organization.entity.ts index c1f8cb211a..865b124dcb 100644 --- a/server/src/entities/organization.entity.ts +++ b/server/src/entities/organization.entity.ts @@ -35,6 +35,9 @@ export class Organization extends BaseEntity { @Column({ name: 'domain' }) domain: string; + @Column({ name: 'is_default', default: false }) + isDefault: boolean; + @Column({ name: 'enable_sign_up' }) enableSignUp: boolean; diff --git a/server/src/helpers/utils.helper.ts b/server/src/helpers/utils.helper.ts index 0eb0498812..0054a4a719 100644 --- a/server/src/helpers/utils.helper.ts +++ b/server/src/helpers/utils.helper.ts @@ -5,6 +5,8 @@ import { isEmpty } from 'lodash'; import { USER_TYPE } from '@modules/users/constants/lifecycle'; import { ConflictException } from '@nestjs/common'; import { DataBaseConstraints } from './db_constraints.constants'; +import { getEnvVars } from 'scripts/database-config-utils'; + const semver = require('semver'); @@ -449,5 +451,11 @@ export const getSubpath = () => { }; export function getTooljetEdition(): string { - return process.env.TOOLJET_EDITION?.toLowerCase() || 'ce'; + const envVars = getEnvVars(); + return envVars['TOOLJET_EDITION']?.toLowerCase() || 'ce'; +} + +export function getCustomEnvVars(name: string) { + const envVars = getEnvVars(); + return envVars[name] || ''; } 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 6dc9a679a4..04ddf805d9 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/auth/oauth/service.ts b/server/src/modules/auth/oauth/service.ts index 79b2c41fa2..438673c29e 100644 --- a/server/src/modules/auth/oauth/service.ts +++ b/server/src/modules/auth/oauth/service.ts @@ -174,7 +174,7 @@ export class OauthService implements IOAuthService { // Not logging in to specific organization, creating new const { name, slug } = generateNextNameAndSlug('My workspace'); - defaultOrganization = await this.setupOrganizationsUtilService.create(name, slug, null, manager); + defaultOrganization = await this.setupOrganizationsUtilService.create({ name, slug }, null, manager); userDetails = await this.userRepository.createOrUpdate( { @@ -221,7 +221,7 @@ export class OauthService implements IOAuthService { if (!isInviteRedirect) { // no SSO login enabled organization available for user - creating new one const { name, slug } = generateNextNameAndSlug('My workspace'); - organizationDetails = await this.setupOrganizationsUtilService.create(name, slug, userDetails, manager); + organizationDetails = await this.setupOrganizationsUtilService.create({ name, slug }, userDetails, manager); await this.userRepository.updateOne( userDetails.id, { defaultOrganizationId: organizationDetails.id }, diff --git a/server/src/modules/auth/service.ts b/server/src/modules/auth/service.ts index 901dd99ad2..7c9fb3ad89 100644 --- a/server/src/modules/auth/service.ts +++ b/server/src/modules/auth/service.ts @@ -85,7 +85,7 @@ export class AuthService implements IAuthService { } else if (allowPersonalWorkspace && !isInviteRedirect) { // no form login enabled organization available for user - creating new one const { name, slug } = generateNextNameAndSlug('My workspace'); - organization = await this.setupOrganizationsUtilService.create(name, slug, user, manager); + organization = await this.setupOrganizationsUtilService.create({ name, slug }, user, manager); } else { if (!isInviteRedirect) throw new UnauthorizedException('User is not assigned to any workspaces'); } diff --git a/server/src/modules/auth/util.service.ts b/server/src/modules/auth/util.service.ts index 41d11c6bf9..e98402776e 100644 --- a/server/src/modules/auth/util.service.ts +++ b/server/src/modules/auth/util.service.ts @@ -149,7 +149,7 @@ export class AuthUtilService implements IAuthUtilService { if (!user && allowPersonalWorkspace) { const { name, slug } = generateNextNameAndSlug('My workspace'); - defaultOrganization = await this.setupOrganizationsUtilService.create(name, slug, null, manager); + defaultOrganization = await this.setupOrganizationsUtilService.create({ name, slug }, null, manager); } const { source, status } = getUserStatusAndSource(lifecycleEvents.USER_SSO_ACTIVATE, sso); 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/login-configs/service.ts b/server/src/modules/login-configs/service.ts index af7ed67143..34143b5fdd 100644 --- a/server/src/modules/login-configs/service.ts +++ b/server/src/modules/login-configs/service.ts @@ -26,7 +26,7 @@ export class LoginConfigsService implements ILoginConfigsService { throw new NotFoundException(); } if (!organizationId) { - const result = this.loginConfigsUtilService.constructSSOConfigs(); + const result = await this.loginConfigsUtilService.constructSSOConfigs(); return result; } diff --git a/server/src/modules/onboarding/controller.ts b/server/src/modules/onboarding/controller.ts index b7d56938a3..a523e0e029 100644 --- a/server/src/modules/onboarding/controller.ts +++ b/server/src/modules/onboarding/controller.ts @@ -49,7 +49,6 @@ export class OnboardingController implements IOnboardingController { @InitFeature(FEATURE_KEY.SIGNUP) @UseGuards( SignupDisableGuard, - AllowPersonalWorkspaceGuard, UserCountGuard, EditorUserCountGuard, FirstUserSignupDisableGuard, diff --git a/server/src/modules/onboarding/interfaces/IUtilService.ts b/server/src/modules/onboarding/interfaces/IUtilService.ts index b0001e5b46..4a083de348 100644 --- a/server/src/modules/onboarding/interfaces/IUtilService.ts +++ b/server/src/modules/onboarding/interfaces/IUtilService.ts @@ -26,6 +26,7 @@ export interface IOnboardingUtilService { signingUpOrganization: Organization, userParams: { firstName: string; lastName: string; password: string }, redirectTo?: string, + defaultWorkspace?: Organization, manager?: EntityManager ): Promise; processOrganizationSignup( @@ -40,4 +41,10 @@ export interface IOnboardingUtilService { organizationInviteUrl: string; }>; splitName(name: string): { firstName: string; lastName: string }; + updateExistingUserDefaultWorkspace( + userParams: { password: string; firstName: string; lastName: string }, + existingUser: User, + defaultWorkspace: Organization, + manager?: EntityManager + ) } diff --git a/server/src/modules/onboarding/service.ts b/server/src/modules/onboarding/service.ts index 3facd8357b..c05c9502d2 100644 --- a/server/src/modules/onboarding/service.ts +++ b/server/src/modules/onboarding/service.ts @@ -119,6 +119,9 @@ export class OnboardingService implements IOnboardingService { const { firstName, lastName } = names; const userParams = { email, password, firstName, lastName }; + // Find the default workspace + const defaultWorkspace = await this.organizationRepository. getDefaultWorkspaceOfInstance(); + if (existingUser) { // Handling instance and workspace level signup for existing user return await this.onboardingUtilService.whatIfTheSignUpIsAtTheWorkspaceLevel( @@ -126,9 +129,18 @@ export class OnboardingService implements IOnboardingService { signingUpOrganization, userParams, redirectTo, + defaultWorkspace, manager ); } else { + if(defaultWorkspace && !signingUpOrganization) { + return await this.onboardingUtilService.createUserInDefaultWorkspace( + userParams, + defaultWorkspace, + redirectTo, + manager + ); + } return await this.onboardingUtilService.createUserOrPersonalWorkspace( userParams, existingUser, @@ -149,8 +161,7 @@ export class OnboardingService implements IOnboardingService { const result = await dbTransactionWrap(async (manager: EntityManager) => { // Create first organization const organization = await this.organizationRepository.createOne( - workspace || 'My workspace', - 'my-workspace', + { name: workspace || 'My workspace', slug: 'my-workspace' }, manager ); @@ -226,7 +237,8 @@ export class OnboardingService implements IOnboardingService { (await this.instanceSettingsUtilService.getSettings(INSTANCE_USER_SETTINGS.ALLOW_PERSONAL_WORKSPACE)) === 'true'; - if (!(allowPersonalWorkspace || organizationToken)) { + const defaultWorkspace = await this.organizationRepository.getDefaultWorkspaceOfInstance(); + if (!(defaultWorkspace || allowPersonalWorkspace || organizationToken)) { throw new BadRequestException('Invalid invitation link'); } if (organizationToken) { @@ -251,7 +263,8 @@ export class OnboardingService implements IOnboardingService { throw new BadRequestException('Please enter password'); } - if (allowPersonalWorkspace) { + const activateDefaultWorkspace = (defaultWorkspace && defaultWorkspace.id === user.defaultOrganizationId) || allowPersonalWorkspace; + if (activateDefaultWorkspace) { // Getting default workspace const defaultOrganizationUser: OrganizationUser = user.organizationUsers.find( (ou) => ou.organizationId === user.defaultOrganizationId @@ -264,6 +277,14 @@ export class OnboardingService implements IOnboardingService { // Activate default workspace await this.organizationUsersUtilService.activateOrganization(defaultOrganizationUser, manager); + if(defaultWorkspace && defaultWorkspace.id === user.defaultOrganizationId){ + const personalWorkspaces = await this.organizationUsersUtilService.personalWorkspaces(user.id); + for(const personalWorkspace of personalWorkspaces){ + // if any personal workspace left. activate those + await this.organizationUsersUtilService.activateOrganization(personalWorkspace, manager); + } + } + if (workspaceName) { const { slug } = generateNextNameAndSlug('My workspace'); await this.organizationRepository.updateOne( @@ -449,10 +470,10 @@ export class OnboardingService implements IOnboardingService { onboarding_details: { status: user.onboardingStatus, password: isPasswordMandatory(user.source), // Should accept password if user is setting up first time - questions: - (this.configService.get('ENABLE_ONBOARDING_QUESTIONS_FOR_ALL_SIGN_UPS') === 'true' && - !organizationUser) || // Should ask onboarding questions if first user of the instance. If ENABLE_ONBOARDING_QUESTIONS_FOR_ALL_SIGN_UPS=true, then will ask questions to all signup users - (await this.userRepository.count({ where: { status: USER_STATUS.ACTIVE } })) === 0, + // questions: + // (this.configService.get('ENABLE_ONBOARDING_QUESTIONS_FOR_ALL_SIGN_UPS') === 'true' && + // !organizationUser) || // Should ask onboarding questions if first user of the instance. If ENABLE_ONBOARDING_QUESTIONS_FOR_ALL_SIGN_UPS=true, then will ask questions to all signup users + // (await this.userRepository.count({ where: { status: USER_STATUS.ACTIVE } })) === 0, }, }; } @@ -686,8 +707,7 @@ export class OnboardingService implements IOnboardingService { // Create first organization const workspaceSlug = generateWorkspaceSlug(workspaceName || 'My workspace'); const organization = await this.setupOrganizationsUtilService.create( - workspaceName || 'My workspace', - workspaceSlug, + { name: workspaceName || 'My workspace', slug: workspaceSlug }, null, manager ); diff --git a/server/src/modules/onboarding/util.service.ts b/server/src/modules/onboarding/util.service.ts index 48c638b0b1..5c32ef89e8 100644 --- a/server/src/modules/onboarding/util.service.ts +++ b/server/src/modules/onboarding/util.service.ts @@ -151,6 +151,7 @@ export class OnboardingUtilService implements IOnboardingUtilService { signingUpOrganization: Organization, userParams: { firstName: string; lastName: string; password: string }, redirectTo?: string, + defaultWorkspace?: Organization, manager?: EntityManager ) => { return dbTransactionWrap(async (manager: EntityManager) => { @@ -251,19 +252,28 @@ export class OnboardingUtilService implements IOnboardingUtilService { case hasWorkspaceInviteButUserWantsInstanceSignup: { const firstTimeSignup = ![SOURCE.SIGNUP, SOURCE.WORKSPACE_SIGNUP].includes(existingUser.source as SOURCE); if (firstTimeSignup) { + if(defaultWorkspace) { + return this.updateExistingUserDefaultWorkspace({ + password, + firstName, + lastName + },existingUser, defaultWorkspace, manager); + } + /* Invite user doing instance signup. So reset name fields and set password */ let defaultOrganizationId = existingUser.defaultOrganizationId; const isPersonalWorkspaceAllowed = (await this.instanceSettingsUtilService.getSettings(INSTANCE_USER_SETTINGS.ALLOW_PERSONAL_WORKSPACE)) === 'true'; - if (!existingUser.defaultOrganizationId && isPersonalWorkspaceAllowed) { + + if (!existingUser.defaultOrganizationId && isPersonalWorkspaceAllowed) { const personalWorkspaces = await this.organizationUsersUtilService.personalWorkspaces(existingUser.id); if (personalWorkspaces.length) { defaultOrganizationId = personalWorkspaces[0].organizationId; } else { /* Create a personal workspace for the user */ const { name, slug } = generateNextNameAndSlug('My workspace'); - const defaultOrganization = await this.organizationRepository.createOne(name, slug, manager); + const defaultOrganization = await this.organizationRepository.createOne({ name, slug }, manager); defaultOrganizationId = defaultOrganization.id; await this.organizationUserRepository.createOne(existingUser, defaultOrganization, true, manager); } @@ -272,7 +282,6 @@ export class OnboardingUtilService implements IOnboardingUtilService { userId: existingUser.id, }); } - await this.userRepository.updateOne( existingUser.id, { @@ -398,7 +407,7 @@ export class OnboardingUtilService implements IOnboardingUtilService { let personalWorkspace: Organization; if (isPersonalWorkspaceEnabled) { const { name, slug } = generateNextNameAndSlug('My workspace'); - personalWorkspace = await this.setupOrganizationsUtilService.create(name, slug, null, manager); + personalWorkspace = await this.setupOrganizationsUtilService.create({ name, slug }, null, manager); } const organizationRole = personalWorkspace ? USER_ROLE.ADMIN : USER_ROLE.END_USER; @@ -604,4 +613,130 @@ export class OnboardingUtilService implements IOnboardingUtilService { manager ); } + + createUserInDefaultWorkspace = async ( + userParams: { email: string; password: string; firstName: string; lastName: string }, + defaultWorkspace: Organization, + redirectTo?: string, + manager?: EntityManager + ) => { + return await dbTransactionWrap(async (manager: EntityManager) => { + const { email, password, firstName, lastName } = userParams; + + if (!defaultWorkspace) { + throw new Error('No default workspace found in the instance'); + } + + // Create user with end-user role in default workspace + const lifeCycleParms = getUserStatusAndSource(lifecycleEvents.USER_SIGN_UP); + + const user = await this.create( + { + email, + password, + ...(firstName && { firstName }), + ...(lastName && { lastName }), + ...lifeCycleParms, + }, + defaultWorkspace.id, + USER_ROLE.END_USER, + null, + true, + null, + manager, + false + ); + + // Create organization user entry + await this.organizationUserRepository.createOne( + user, + defaultWorkspace, + true, + manager, + WORKSPACE_USER_SOURCE.SIGNUP + ); + + // Validate license + await this.licenseUserService.validateUser(manager); + + // Send welcome email + this.eventEmitter.emit('emailEvent', { + type: EMAIL_EVENTS.SEND_WELCOME_EMAIL, + payload: { + to: user.email, + name: user.firstName, + invitationtoken: user.invitationToken, + }, + }); + + return {}; + }, manager); + }; + + updateExistingUserDefaultWorkspace = async ( + userParams: { password: string; firstName: string; lastName: string }, + existingUser: User, + defaultWorkspace: Organization, + manager?: EntityManager + ) => { + return await dbTransactionWrap(async (manager: EntityManager) => { + const { password, firstName, lastName } = userParams; + // Create organization user entry if not exists + const existingOrgUser = await this.organizationUserRepository.findOne({ + where: { + userId: existingUser.id, + organizationId: defaultWorkspace.id, + } + }); + + if(existingOrgUser){ + throw new NotAcceptableException( + 'The user is already registered. Please check your inbox for the activation link' + ); + } + + // Update user's default organization ID + await this.userRepository.updateOne( + existingUser.id, + { + password, + firstName, + lastName, + source: SOURCE.SIGNUP, + defaultOrganizationId: defaultWorkspace.id, + }, + manager + ); + + await this.organizationUserRepository.createOne( + existingUser, + defaultWorkspace, + true, + manager, + WORKSPACE_USER_SOURCE.SIGNUP + ); + + // Add end-user role in default workspace if not already present + await this.rolesUtilService.addUserRole( + defaultWorkspace.id, + { role: USER_ROLE.END_USER, userId: existingUser.id }, + manager + ); + + // Validate license + await this.licenseUserService.validateUser(manager); + + // send welcome email + this.eventEmitter.emit('emailEvent', { + type: EMAIL_EVENTS.SEND_WELCOME_EMAIL, + payload: { + to: existingUser.email, + name: existingUser.firstName, + invitationtoken: existingUser.invitationToken, + }, + }); + + return {}; + }, manager); + }; } 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/organization-users/util.service.ts b/server/src/modules/organization-users/util.service.ts index 618291b732..c040e2cff7 100644 --- a/server/src/modules/organization-users/util.service.ts +++ b/server/src/modules/organization-users/util.service.ts @@ -7,6 +7,7 @@ import { lifecycleEvents, USER_STATUS, USER_TYPE, + WORKSPACE_USER_SOURCE, WORKSPACE_USER_STATUS, } from '@modules/users/constants/lifecycle'; import { BadRequestException, ConflictException, Injectable } from '@nestjs/common'; @@ -212,7 +213,7 @@ export class OrganizationUsersUtilService implements IOrganizationUsersUtilServi async createDefaultOrganization(manager: EntityManager) { const { name, slug } = generateNextNameAndSlug('My workspace'); - return await this.setupOrganizationsUtilService.create(name, slug, null, manager); + return await this.setupOrganizationsUtilService.create({ name, slug }, null, manager); } addUserAsAdmin(userId: string, organizationId: string, manager: EntityManager) { @@ -343,7 +344,7 @@ export class OrganizationUsersUtilService implements IOrganizationUsersUtilServi async personalWorkspaces(userId: string): Promise { const personalWorkspaces: Partial = await this.organizationUsersRepository.find({ - select: ['organizationId', 'invitationToken'], + select: ['organizationId', 'invitationToken', 'id'], where: { userId }, }); const personalWorkspaceArray: OrganizationUser[] = []; @@ -578,4 +579,41 @@ export class OrganizationUsersUtilService implements IOrganizationUsersUtilServi user.organizationUserSource = organizationUser.source; return user; } + + addUserToWorkspace = async ( + user: User, + workspace: Organization, + manager?: EntityManager + ) => { + return await dbTransactionWrap(async (manager: EntityManager) => { + // Create organization user entry if not exists + let existingOrgUser = await this.organizationUsersRepository.findOne({ + where: { + userId: user.id, + organizationId: workspace.id, + } + }); + + if(existingOrgUser){ + return existingOrgUser; + } + + const organizationUser = await this.organizationUsersRepository.createOne( + user, + workspace, + true, + manager, + WORKSPACE_USER_SOURCE.SIGNUP + ); + + // Add end-user role in default workspace if not already present + await this.rolesUtilService.addUserRole( + workspace.id, + { role: USER_ROLE.END_USER, userId: user.id }, + manager + ); + + return organizationUser; + }, manager); + }; } diff --git a/server/src/modules/organizations/ability/index.ts b/server/src/modules/organizations/ability/index.ts index acfd4a6078..49eaa256dd 100644 --- a/server/src/modules/organizations/ability/index.ts +++ b/server/src/modules/organizations/ability/index.ts @@ -44,7 +44,7 @@ export class FeatureAbilityFactory extends AbilityFactory can([FEATURE_KEY.UPDATE, FEATURE_KEY.GET, FEATURE_KEY.CHECK_UNIQUE], Organization); } if (superAdmin) { - can([FEATURE_KEY.WORKSPACE_STATUS_UPDATE], Organization); + can([FEATURE_KEY.WORKSPACE_STATUS_UPDATE, FEATURE_KEY.SET_DEFAULT], Organization); } } } diff --git a/server/src/modules/organizations/constants/feature.ts b/server/src/modules/organizations/constants/feature.ts index af1c847553..7276f855fb 100644 --- a/server/src/modules/organizations/constants/feature.ts +++ b/server/src/modules/organizations/constants/feature.ts @@ -14,5 +14,6 @@ export const FEATURES: FeaturesConfig = { [FEATURE_KEY.CHECK_UNIQUE_ONBOARDING]: { isPublic: true, }, + [FEATURE_KEY.SET_DEFAULT]: {}, }, }; diff --git a/server/src/modules/organizations/constants/index.ts b/server/src/modules/organizations/constants/index.ts index b829014d86..45b2f85044 100644 --- a/server/src/modules/organizations/constants/index.ts +++ b/server/src/modules/organizations/constants/index.ts @@ -26,4 +26,5 @@ export enum FEATURE_KEY { CHECK_UNIQUE = 'check_unique', CREATE = 'create', CHECK_UNIQUE_ONBOARDING = 'check_unique_onboarding', + SET_DEFAULT = 'set_default', } diff --git a/server/src/modules/organizations/controller.ts b/server/src/modules/organizations/controller.ts index 0dcc325048..2de2ed9936 100644 --- a/server/src/modules/organizations/controller.ts +++ b/server/src/modules/organizations/controller.ts @@ -1,4 +1,4 @@ -import { Body, Controller, Get, Patch, UseGuards, Query, Param } from '@nestjs/common'; +import { Body, Controller, Get, Patch, UseGuards, Query, Param, NotImplementedException } from '@nestjs/common'; import { OrganizationsService } from '@modules/organizations/service'; import { decamelizeKeys } from 'humps'; import { User } from '@modules/app/decorators/user.decorator'; @@ -17,7 +17,7 @@ import { OrganizationAuthGuard } from '@modules/session/guards/organization-auth @Controller('organizations') @InitModule(MODULES.ORGANIZATIONS) export class OrganizationsController implements IOrganizationsController { - constructor(private organizationsService: OrganizationsService) {} + constructor(protected organizationsService: OrganizationsService) {} @InitFeature(FEATURE_KEY.GET) // TODO: Change to jwt auth guard - check why we need OrganizationAuthGuard here @@ -41,6 +41,15 @@ export class OrganizationsController implements IOrganizationsController { await this.organizationsService.updateOrganizationNameAndSlug(user.organizationId, organizationUpdateDto); return; } + + @InitFeature(FEATURE_KEY.SET_DEFAULT) + @UseGuards(JwtAuthGuard, FeatureAbilityGuard) + @Patch(':id/set-default') + async setDefaultWorkspace(@Param('id') id: string) { + await this.organizationsService.setDefaultWorkspace(id); + return; + } + // Note : This endpoint is used for archive/unarchive workspaces. @InitFeature(FEATURE_KEY.WORKSPACE_STATUS_UPDATE) @UseGuards(JwtAuthGuard) diff --git a/server/src/modules/organizations/interfaces/IController.ts b/server/src/modules/organizations/interfaces/IController.ts index ad5feb0638..2654fe53db 100644 --- a/server/src/modules/organizations/interfaces/IController.ts +++ b/server/src/modules/organizations/interfaces/IController.ts @@ -11,4 +11,6 @@ export interface IOrganizationsController { checkWorkspaceUnique(name: string, slug: string): Promise; checkUniqueWorkspaceName(name: string): Promise; + + setDefaultWorkspace(id: string): Promise; } diff --git a/server/src/modules/organizations/interfaces/IService.ts b/server/src/modules/organizations/interfaces/IService.ts index aad08934d0..658418e4f5 100644 --- a/server/src/modules/organizations/interfaces/IService.ts +++ b/server/src/modules/organizations/interfaces/IService.ts @@ -1,5 +1,6 @@ import { Organization } from 'src/entities/organization.entity'; import { OrganizationUpdateDto, OrganizationStatusUpdateDto } from '@modules/organizations/dto'; +import { EntityManager } from 'typeorm'; export interface IOrganizationsService { fetchOrganizations( @@ -15,4 +16,8 @@ export interface IOrganizationsService { updateOrganizationStatus(organizationId: string, updatableData: OrganizationStatusUpdateDto): Promise; checkWorkspaceUniqueness(name: string, slug: string): Promise; + + checkWorkspaceNameUniqueness(name: string): Promise; + + setDefaultWorkspace(organizationId: string, manager?: EntityManager): Promise; } 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/repository.ts b/server/src/modules/organizations/repository.ts index 240639dbe3..25ef5e7929 100644 --- a/server/src/modules/organizations/repository.ts +++ b/server/src/modules/organizations/repository.ts @@ -7,6 +7,7 @@ import { catchDbException, isSuperAdmin } from '@helpers/utils.helper'; import { ConfigScope, SSOType } from '@entities/sso_config.entity'; import { WORKSPACE_STATUS, WORKSPACE_USER_STATUS } from '@modules/users/constants/lifecycle'; import { CONSTRAINTS } from './constants'; +import { OrganizationInputs } from '@modules/setup-organization/types/organization-inputs'; @Injectable() export class OrganizationRepository extends Repository { @@ -106,7 +107,8 @@ export class OrganizationRepository extends Repository { }, manager); } - createOne(name: string, slug: string, manager?: EntityManager): Promise { + createOne(organizationInputs: OrganizationInputs, manager?: EntityManager): Promise { + const { name, slug, isDefault } = organizationInputs; return dbTransactionWrap((manager: EntityManager) => { return catchDbException(() => { return manager.save( @@ -120,6 +122,7 @@ export class OrganizationRepository extends Repository { ], name, slug, + isDefault, createdAt: new Date(), updatedAt: new Date(), }) @@ -201,4 +204,27 @@ export class OrganizationRepository extends Repository { }); }); } + + async getDefaultWorkspaceOfInstance(): Promise{ + return dbTransactionWrap(async (manager: EntityManager) => { + try { + return await manager.findOneOrFail(Organization, { + where: { isDefault: true }, + }); + } catch (error) { + console.error('No default workspace in this instance'); + return null; + } + }); + } + + async changeDefaultWorkspace(organizationId: string, manager?: EntityManager): Promise { + return await dbTransactionWrap(async (manager: EntityManager) => { + // First, unset any existing default workspace + await manager.update(Organization, { isDefault: true }, { isDefault: false }); + + // Then set the new default workspace + await manager.update(Organization, { id: organizationId }, { isDefault: true }); + }, manager || this.manager); + } } diff --git a/server/src/modules/organizations/service.ts b/server/src/modules/organizations/service.ts index b2a25570c9..8e8f715399 100644 --- a/server/src/modules/organizations/service.ts +++ b/server/src/modules/organizations/service.ts @@ -1,4 +1,4 @@ -import { ConflictException, Injectable, NotAcceptableException } from '@nestjs/common'; +import { ConflictException, Injectable, NotAcceptableException, NotImplementedException } from '@nestjs/common'; import { Organization } from 'src/entities/organization.entity'; import { isSuperAdmin } from 'src/helpers/utils.helper'; import { dbTransactionWrap } from 'src/helpers/database.helper'; @@ -51,6 +51,11 @@ export class OrganizationsService implements IOrganizationsService { updatableData: OrganizationStatusUpdateDto ): Promise { return await dbTransactionWrap(async (manager: EntityManager) => { + const organization = await this.organizationRepository.findOne({ where: { id: organizationId } }); + if (organization.isDefault) { + throw new NotAcceptableException('Default workspace cannot be archived'); + } + await this.organizationRepository.updateOne(organizationId, updatableData, manager); if (updatableData.status === WORKSPACE_STATUS.ACTIVE) { await this.licenseOrganizationService.validateOrganization(manager); //Check for only unarchiving @@ -85,4 +90,8 @@ export class OrganizationsService implements IOrganizationsService { if (result) throw new ConflictException('Workspace name must be unique'); return; } + + async setDefaultWorkspace(organizationId: string, manager?: EntityManager): Promise { + throw new NotImplementedException('This feature is only available in Enterprise Edition'); + } } diff --git a/server/src/modules/organizations/types/index.ts b/server/src/modules/organizations/types/index.ts index 1b3321eae6..ea5fc8c08f 100644 --- a/server/src/modules/organizations/types/index.ts +++ b/server/src/modules/organizations/types/index.ts @@ -9,6 +9,7 @@ interface Features { [FEATURE_KEY.CREATE]: FeatureConfig; [FEATURE_KEY.CHECK_UNIQUE_ONBOARDING]: FeatureConfig; [FEATURE_KEY.WORKSPACE_STATUS_UPDATE]: FeatureConfig; + [FEATURE_KEY.SET_DEFAULT]: FeatureConfig; } export interface FeaturesConfig { 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/session/util.service.ts b/server/src/modules/session/util.service.ts index 92a12e1d88..282a6179e5 100644 --- a/server/src/modules/session/util.service.ts +++ b/server/src/modules/session/util.service.ts @@ -368,8 +368,8 @@ export class SessionUtilService { async #onboardingFlags(user: User) { let isFirstUserOnboardingCompleted = true; let isOnboardingCompleted = true; - const isOnboardingQuestionsEnabled = - this.configService.get('ENABLE_ONBOARDING_QUESTIONS_FOR_ALL_SIGN_UPS') === 'true'; + // const isOnboardingQuestionsEnabled = + // this.configService.get('ENABLE_ONBOARDING_QUESTIONS_FOR_ALL_SIGN_UPS') === 'true'; const instanceUsersCount = await this.userRepository.count({ where: { status: USER_STATUS.ACTIVE }, @@ -383,14 +383,14 @@ export class SessionUtilService { } /* Signed up user check */ - if ( - instanceUsersCount > 1 && - isOnboardingQuestionsEnabled && - user.onboardingStatus !== OnboardingStatus.ONBOARDING_COMPLETED - ) { - /* Signed up user went through onboarding flow, didn't complete */ - isOnboardingCompleted = false; - } + // if ( + // instanceUsersCount > 1 && + // isOnboardingQuestionsEnabled && + // user.onboardingStatus !== OnboardingStatus.ONBOARDING_COMPLETED + // ) { + // /* Signed up user went through onboarding flow, didn't complete */ + // isOnboardingCompleted = false; + // } return { isFirstUserOnboardingCompleted, isOnboardingCompleted }; } diff --git a/server/src/modules/setup-organization/controller.ts b/server/src/modules/setup-organization/controller.ts index 7b377738fd..0b16e05b65 100644 --- a/server/src/modules/setup-organization/controller.ts +++ b/server/src/modules/setup-organization/controller.ts @@ -29,8 +29,7 @@ export class SetupOrganizationsController implements ISetupOrganizationsControll @Res({ passthrough: true }) response: Response ) { const result = await this.setupOrganizationsService.create( - organizationCreateDto.name, - organizationCreateDto.slug, + { name: organizationCreateDto.name, slug: organizationCreateDto.slug }, user ); diff --git a/server/src/modules/setup-organization/interfaces/IService.ts b/server/src/modules/setup-organization/interfaces/IService.ts index 557b438223..4e463784c3 100644 --- a/server/src/modules/setup-organization/interfaces/IService.ts +++ b/server/src/modules/setup-organization/interfaces/IService.ts @@ -1,7 +1,8 @@ import { User } from 'src/entities/user.entity'; import { Organization } from 'src/entities/organization.entity'; import { EntityManager } from 'typeorm'; +import { OrganizationInputs } from '../types/organization-inputs'; export interface ISetupOrganizationsService { - create(name: string, slug: string, user?: User, manager?: EntityManager): Promise; + create(organizationInputs: OrganizationInputs, user?: User, manager?: EntityManager): Promise; } diff --git a/server/src/modules/setup-organization/interfaces/IUtilService.ts b/server/src/modules/setup-organization/interfaces/IUtilService.ts index 0aaa5a9350..874ac52c35 100644 --- a/server/src/modules/setup-organization/interfaces/IUtilService.ts +++ b/server/src/modules/setup-organization/interfaces/IUtilService.ts @@ -1,7 +1,8 @@ import { User } from 'src/entities/user.entity'; import { EntityManager } from 'typeorm'; import { Organization } from '@entities/organization.entity'; +import { OrganizationInputs } from '../types/organization-inputs'; export interface ISetupOrganizationsUtilService { - create(name: string, slug: string, user?: User, manager?: EntityManager): Promise; + create(organizationInputs: OrganizationInputs, user?: User, manager?: EntityManager): Promise; } diff --git a/server/src/modules/setup-organization/service.ts b/server/src/modules/setup-organization/service.ts index 78a066e0bb..925e8a75eb 100644 --- a/server/src/modules/setup-organization/service.ts +++ b/server/src/modules/setup-organization/service.ts @@ -4,12 +4,13 @@ import { User } from 'src/entities/user.entity'; import { EntityManager } from 'typeorm'; import { SetupOrganizationsUtilService } from './util.service'; import { ISetupOrganizationsService } from './interfaces/IService'; +import { OrganizationInputs } from './types/organization-inputs'; @Injectable() export class SetupOrganizationsService implements ISetupOrganizationsService { constructor(protected readonly setupOrganizationsUtilService: SetupOrganizationsUtilService) {} - async create(name: string, slug: string, user?: User, manager?: EntityManager): Promise { - return this.setupOrganizationsUtilService.create(name, slug, user, manager); + async create(organizationInputs: OrganizationInputs, user?: User, manager?: EntityManager): Promise { + return this.setupOrganizationsUtilService.create(organizationInputs, user, manager); } } diff --git a/server/src/modules/setup-organization/types/organization-inputs.ts b/server/src/modules/setup-organization/types/organization-inputs.ts new file mode 100644 index 0000000000..af427cf785 --- /dev/null +++ b/server/src/modules/setup-organization/types/organization-inputs.ts @@ -0,0 +1,5 @@ +export interface OrganizationInputs { + name: string; + slug: string; + isDefault?: boolean; +} diff --git a/server/src/modules/setup-organization/util.service.ts b/server/src/modules/setup-organization/util.service.ts index e053e60b9e..8eac88ec33 100644 --- a/server/src/modules/setup-organization/util.service.ts +++ b/server/src/modules/setup-organization/util.service.ts @@ -15,6 +15,7 @@ import { OrganizationUsersRepository } from '@modules/organization-users/reposit import { SampleDataSourceService } from '@modules/data-sources/services/sample-ds.service'; import { ISetupOrganizationsUtilService } from './interfaces/IUtilService'; import { TooljetDbTableOperationsService } from '@modules/tooljet-db/services/tooljet-db-table-operations.service'; +import { OrganizationInputs } from './types/organization-inputs'; @Injectable() export class SetupOrganizationsUtilService implements ISetupOrganizationsUtilService { @@ -31,9 +32,9 @@ export class SetupOrganizationsUtilService implements ISetupOrganizationsUtilSer protected readonly organizationUserRepository: OrganizationUsersRepository ) {} - async create(name: string, slug: string, user?: User, manager?: EntityManager): Promise { + async create(organizationInputs: OrganizationInputs, user?: User, manager?: EntityManager): Promise { return await dbTransactionWrap(async (manager: EntityManager) => { - const organization = await this.organizationRepository.createOne(name, slug, manager); + const organization = await this.organizationRepository.createOne(organizationInputs, manager); await this.appEnvironmentUtilService.createDefaultEnvironments(organization.id, manager); await this.groupPermissionUtilService.createDefaultGroups(organization.id, manager); 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', + }, + }); + } }