mirror of
https://github.com/ToolJet/ToolJet
synced 2026-05-23 08:58:26 +00:00
Merge pull request #12447 from ToolJet/fix/multiselect-dropdown-issues
Fix: Triage room issues
This commit is contained in:
commit
923092ebb3
29 changed files with 360 additions and 142 deletions
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -255,7 +255,7 @@ export const PropertiesTabElements = ({
|
|||
paramType="properties"
|
||||
/>
|
||||
</div>
|
||||
{resolveReferences(column?.isEditable) && (
|
||||
{(column?.fxActiveFields?.includes('isEditable') || resolveReferences(column?.isEditable)) && (
|
||||
<ValidationProperties
|
||||
column={column}
|
||||
index={index}
|
||||
|
|
|
|||
|
|
@ -168,6 +168,7 @@ export const Viewer = ({ id: appId, darkMode, moduleId = 'canvas', switchDarkMod
|
|||
showViewerNavigation={!isPagesSidebarHidden}
|
||||
handleAppEnvironmentChanged={handleAppEnvironmentChanged}
|
||||
changeToDarkMode={changeToDarkMode}
|
||||
switchPage={switchPage}
|
||||
/>
|
||||
)}
|
||||
<div className="sub-section">
|
||||
|
|
|
|||
|
|
@ -258,7 +258,11 @@ export const createDataQuerySlice = (set, get) => ({
|
|||
set((state) => {
|
||||
state.dataQuery.creatingQueryInProcessId = null;
|
||||
state.dataQuery.queries.modules[moduleId] = [
|
||||
{ ...data, data_source_id: queryToClone.data_source_id },
|
||||
{
|
||||
...data,
|
||||
data_source_id: queryToClone.data_source_id,
|
||||
plugin: { iconFile: queryToClone.plugin?.iconFile, icon_file: queryToClone.plugin?.icon_file },
|
||||
},
|
||||
...state.dataQuery.queries.modules[moduleId],
|
||||
];
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
/* eslint-disable react/no-string-refs */
|
||||
import React from 'react';
|
||||
import { Editor, EditorState, RichUtils, getDefaultKeyBinding, ContentState, convertFromHTML } from 'draft-js';
|
||||
import { Editor, EditorState, RichUtils, getDefaultKeyBinding } from 'draft-js';
|
||||
import 'draft-js/dist/Draft.css';
|
||||
import { stateFromHTML } from 'draft-js-import-html';
|
||||
import { stateToHTML } from 'draft-js-export-html';
|
||||
import Loader from '@/ToolJetUI/Loader/Loader';
|
||||
import DOMPurify from 'dompurify';
|
||||
|
|
@ -150,11 +151,8 @@ const InlineStyleControls = (props) => {
|
|||
class DraftEditor extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
const blocksFromHTML = convertFromHTML(DOMPurify.sanitize(this.props.defaultValue));
|
||||
this.state = {
|
||||
editorState: EditorState.createWithContent(
|
||||
ContentState.createFromBlockArray(blocksFromHTML.contentBlocks, blocksFromHTML.entityMap)
|
||||
),
|
||||
editorState: EditorState.createWithContent(stateFromHTML(DOMPurify.sanitize(this.props.defaultValue))),
|
||||
};
|
||||
|
||||
this.editorContainerRef = React.createRef();
|
||||
|
|
@ -173,6 +171,18 @@ class DraftEditor extends React.Component {
|
|||
this.toggleInlineStyle = this._toggleInlineStyle.bind(this);
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (prevProps.defaultValue !== this.props.defaultValue) {
|
||||
const newContentState = stateFromHTML(DOMPurify.sanitize(this.props.defaultValue));
|
||||
const newEditorState = EditorState.createWithContent(newContentState);
|
||||
const html = stateToHTML(newContentState);
|
||||
|
||||
this.props.handleChange(html);
|
||||
|
||||
this.setState({ editorState: newEditorState });
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
//For resizing the editor container based on the height of rich text editor controls
|
||||
this.resizeObserver = new ResizeObserver(() => {
|
||||
|
|
@ -193,11 +203,7 @@ class DraftEditor extends React.Component {
|
|||
isVisible: this.props.isVisible,
|
||||
isLoading: this.props.isLoading,
|
||||
setValue: async (text) => {
|
||||
const blocksFromHTML = convertFromHTML(DOMPurify.sanitize(text));
|
||||
const newContentState = ContentState.createFromBlockArray(
|
||||
blocksFromHTML.contentBlocks,
|
||||
blocksFromHTML.entityMap
|
||||
);
|
||||
const newContentState = stateFromHTML(DOMPurify.sanitize(text));
|
||||
const newEditorState = EditorState.createWithContent(newContentState);
|
||||
const html = stateToHTML(newContentState);
|
||||
this.props.handleChange(html);
|
||||
|
|
@ -226,19 +232,6 @@ class DraftEditor extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (prevProps.defaultValue !== this.props.defaultValue) {
|
||||
const blocksFromHTML = convertFromHTML(DOMPurify.sanitize(this.props.defaultValue));
|
||||
const newContentState = ContentState.createFromBlockArray(blocksFromHTML.contentBlocks, blocksFromHTML.entityMap);
|
||||
const newEditorState = EditorState.createWithContent(newContentState);
|
||||
const html = stateToHTML(newContentState);
|
||||
|
||||
this.props.handleChange(html);
|
||||
|
||||
this.setState({ editorState: newEditorState });
|
||||
}
|
||||
}
|
||||
|
||||
_handleKeyCommand(command, editorState) {
|
||||
const newState = RichUtils.handleKeyCommand(editorState, command);
|
||||
if (newState) {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<DynamicModule> {
|
||||
|
|
@ -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],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<App> {
|
||||
|
|
@ -63,4 +64,23 @@ export class AppsRepository extends Repository<App> {
|
|||
},
|
||||
});
|
||||
}
|
||||
|
||||
async findAllOrganizationApps(organizationId: string): Promise<WorkspaceAppsResponseDto[]> {
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<any> {
|
||||
|
|
|
|||
|
|
@ -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<string, string>;
|
||||
dataQueryMapping: Record<string, string>;
|
||||
|
|
@ -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<DataSource> => {
|
||||
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);
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
@ -522,4 +525,45 @@ export class AppsUtilService implements IAppsUtilService {
|
|||
|
||||
return components;
|
||||
}
|
||||
|
||||
async findAllOrganizationApps(organizationId: string): Promise<WorkspaceAppsResponseDto[]> {
|
||||
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 };
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<string>('EXTERNAL_API_ACCESS_TOKEN');
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
];
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<string>('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<string>('EXTERNAL_API_ACCESS_TOKEN');
|
||||
|
||||
if (!authHeader || authHeader !== `Basic ${externalApiAccessToken}`) {
|
||||
throw new ForbiddenException('Unauthorized');
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
@ -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<DynamicModule> {
|
||||
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],
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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<string, string>;
|
||||
dataQueryMapping: Record<string, string>;
|
||||
appVersionMapping: Record<string, string>;
|
||||
appEnvironmentMapping: Record<string, string>;
|
||||
appDefaultEnvironmentMapping: Record<string, string[]>;
|
||||
pagesMapping: Record<string, string>;
|
||||
componentsMapping: Record<string, string>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
export interface IOrganizationUtilService {
|
||||
validateWorkspaceExists(workspaceId: string): Promise<void>;
|
||||
}
|
||||
|
|
@ -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<DynamicModule> {
|
||||
|
|
@ -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],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
18
server/src/modules/organizations/util.service.ts
Normal file
18
server/src/modules/organizations/util.service.ts
Normal file
|
|
@ -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}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ export class UsersModule {
|
|||
imports: [await SessionModule.register(configs)],
|
||||
controllers: [UsersController],
|
||||
providers: [UsersService, UserRepository, UsersUtilService, FeatureAbilityFactory],
|
||||
exports: [UsersUtilService],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<User> {
|
|||
await manager.upsert(UserDetails, updatableParams, conflictsPaths);
|
||||
}
|
||||
|
||||
async getUserWithAdminRole(organizationId: string, manager?: EntityManager): Promise<User | null> {
|
||||
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,
|
||||
|
|
|
|||
|
|
@ -3,4 +3,5 @@ import { AppVersionUpdateDto } from '@dto/app-version-update.dto';
|
|||
|
||||
export interface IVersionUtilService {
|
||||
updateVersion(appVersion: AppVersion, appVersionUpdateDto: AppVersionUpdateDto): Promise<void>;
|
||||
fetchVersions(appId: string): Promise<AppVersion[]>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -51,6 +51,7 @@ export class VersionModule {
|
|||
VersionUtilService,
|
||||
FeatureAbilityFactory,
|
||||
],
|
||||
exports: [VersionUtilService],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -72,4 +72,13 @@ export class VersionUtilService implements IVersionUtilService {
|
|||
await this.versionRepository.update(appVersion.id, editableParams);
|
||||
return;
|
||||
}
|
||||
|
||||
async fetchVersions(appId: string): Promise<AppVersion[]> {
|
||||
return await this.versionRepository.find({
|
||||
where: { appId },
|
||||
order: {
|
||||
createdAt: 'DESC',
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue