Merge pull request #11245 from ToolJet/release/v3.0

Release: v3.0.0
This commit is contained in:
Akshay 2024-11-12 13:20:58 +05:30 committed by GitHub
commit 0a006b5150
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 92 additions and 48 deletions

View file

@ -1 +1 @@
3.0.0-ce-beta.1
3.0.0-ce

View file

@ -1 +1 @@
3.0.0-ce-beta.1
3.0.0-ce

View file

@ -156,7 +156,15 @@ const useAppData = (appId, moduleId, mode = 'edit', { environmentId, versionId }
let editorEnvironment = result.editorEnvironment;
const editorEnvironmentId = result.editing_version?.current_environment_id;
if (isPreviewForVersion) {
const rawDataQueries = appData?.data_queries;
const rawEditingVersionDataQueries = appData?.editing_version?.data_queries;
appData = convertAllKeysToSnakeCase(appData);
appData.data_queries = rawDataQueries;
if (appData.editing_version && rawEditingVersionDataQueries) {
appData.editing_version.data_queries = rawEditingVersionDataQueries;
}
editorEnvironment = {
id: environmentId,
name: queryParams.env,

View file

@ -164,7 +164,7 @@ export const Multiselect = function Multiselect({
<label
style={{ marginRight: label ? '1rem' : '', marginBottom: 0 }}
className={`form-label py-1 ${darkMode ? 'text-light' : 'text-secondary'}`}
data-cy={`multiselect-label-${componentName.toLowerCase()}`}
data-cy={`multiselect-label-${componentName?.toLowerCase()}`}
>
{label}
</label>

View file

@ -1,4 +1,4 @@
import React, { useEffect, useState } from 'react';
import React, { useEffect, useState, useRef } from 'react';
import { authenticationService } from '@/_services';
import { CustomSelect } from './CustomSelect';
import { getAvatar, decodeEntities } from '@/_helpers/utils';
@ -29,14 +29,21 @@ export const OrganizationList = function () {
fetchOrganizations();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const switchOrganization = (id) => {
const newTabRef = useRef(false);
const switchOrganization = (id, newTab = false) => {
newTabRef.current = newTab;
const organization = organizationList.find((org) => org.id === id);
if (![id, organization.slug].includes(getWorkspaceIdOrSlugFromURL())) {
const newPath = appendWorkspaceId(organization.slug || id, location.pathname, true);
window.open(newPath, '_blank');
newTab ? window.open(newPath, '_blank') : (window.location = newPath);
}
};
const handleOnChange = (id) => {
if (!newTabRef.current) {
switchOrganization(id, false);
}
newTabRef.current = false;
};
const options = organizationList
.map((org) => ({
@ -81,7 +88,7 @@ export const OrganizationList = function () {
<div
className="current-org-indicator"
data-cy="current-org-indicator"
onClick={() => switchOrganization(org.id)}
onClick={() => switchOrganization(org.id, true)}
>
<SolidIcon name="newtab" fill="var(--icon-strong)" width="16" className="add-new-workspace-icon" />
</div>
@ -103,7 +110,7 @@ export const OrganizationList = function () {
isLoading={isGettingOrganizations}
options={options}
value={current_organization_id}
onChange={(id) => switchOrganization(id)}
onChange={handleOnChange}
className={`tj-org-select ${darkMode && 'dark-theme'}`}
/>
</div>

View file

@ -179,6 +179,7 @@
.datasource-list-container {
overflow-y: auto;
max-height: calc(100vh - 64px);
padding-left: 20px;
.datasource-list {
width: 976px;

View file

@ -3656,6 +3656,9 @@ input:focus-visible {
}
.main-wrapper.theme-dark {
position: relative;
min-height: 100%;
min-width: 100%;
background-color: #2b394b;
}
@ -7796,7 +7799,11 @@ tbody {
.audit-log-nav-item {
bottom: 40px;
}
.workspace-content-wrapper{
overflow-x: auto;
overflow-y: auto;
padding-left: 20px;
}
.workspace-content-wrapper,
.database-page-content-wrap {
background-color: var(--page-default);
@ -10560,6 +10567,7 @@ tbody {
.manage-groups-body {
padding: 12px 12px 10px 12px;
font-size: 12px;
overflow-y: auto;
// overflow-y: auto;
height: calc(100vh - 300px);

View file

@ -196,7 +196,6 @@ module.exports = {
COMMENT_FEATURE_ENABLE: process.env.COMMENT_FEATURE_ENABLE ?? true,
ENABLE_MULTIPLAYER_EDITING: true,
ENABLE_MARKETPLACE_DEV_MODE: process.env.ENABLE_MARKETPLACE_DEV_MODE,
TJDB_SQL_MODE_DISABLE: process.env.TJDB_SQL_MODE_DISABLE ?? false,
TOOLJET_MARKETPLACE_URL:
process.env.TOOLJET_MARKETPLACE_URL || 'https://tooljet-plugins-production.s3.us-east-2.amazonaws.com',
}),

View file

@ -5,14 +5,6 @@
"type": "database",
"defaults": {},
"properties": {
"index": {
"label": "Index",
"key": "index",
"type": "text",
"description": "Enter the index name (e.g., example-index)",
"placeholder": "example-index",
"height": "36px"
},
"operation": {
"label": "Operation",
"key": "operation",

View file

@ -82,8 +82,7 @@ export const sanitizeHeaders = (
queryOptions: any,
hasDataSource = true
): { [k: string]: string } => {
const cleanHeaders = (headers) =>
headers.filter(([_, v]) => !isEmpty(v)).map(([k, v]) => [k.trim().toLowerCase(), v]);
const cleanHeaders = (headers) => headers.filter(([k, _]) => k !== '').map(([k, v]) => [k.trim(), v]);
const _queryHeaders = cleanHeaders(queryOptions.headers || []);
const queryHeaders = Object.fromEntries(_queryHeaders);

View file

@ -1 +1 @@
3.0.0-ce-beta.1
3.0.0-ce

View file

@ -0,0 +1,26 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class PopulateManualConnectionTypeForOldPostgresDs1731283187529 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
UPDATE data_source_options
SET options = jsonb_set(
COALESCE(options::jsonb, '{}'),
'{connection_type}',
'{"value": "manual", "encrypted": false}'::jsonb
)
WHERE data_source_id IN (
SELECT ds.id
FROM data_sources ds
WHERE ds.kind = 'postgresql'
)
AND NOT EXISTS (
SELECT 1
FROM jsonb_object_keys(COALESCE(options::jsonb, '{}')) AS keys
WHERE keys = 'connection_type'
);
`);
}
public async down(queryRunner: QueryRunner): Promise<void> {}
}

View file

@ -573,7 +573,6 @@ export class DataQueriesService {
finalResult += str.slice(lastIndex);
return finalResult;
}
async parseQueryOptions(
object: any,
options: object,
@ -581,10 +580,8 @@ export class DataQueriesService {
environmentId?: string
): Promise<object> {
const stack: any[] = [{ obj: object, key: null, parent: null }];
while (stack.length > 0) {
const { obj, key, parent } = stack.pop();
// Case 1: Object
if (typeof obj === 'object' && obj !== null && !Array.isArray(obj)) {
Object.keys(obj).forEach((k) => {
@ -592,7 +589,6 @@ export class DataQueriesService {
});
continue;
}
// Case 2: Array
if (Array.isArray(obj)) {
obj.forEach((element, index) => {
@ -600,52 +596,54 @@ export class DataQueriesService {
});
continue;
}
// Case 3: String
if (typeof obj === 'string') {
let resolvedValue = obj.replace(/\n/g, ' ');
// a: Handle strings with both {{ }} and %%
if (resolvedValue.includes('{{') && resolvedValue.includes('}}') && resolvedValue.includes('%%')) {
if (
typeof resolvedValue === 'string' &&
resolvedValue.includes('{{') &&
resolvedValue.includes('}}') &&
resolvedValue?.includes('%%')
) {
let resolvedVar = options[resolvedValue];
// find all server variables in the string
if (resolvedValue.includes(`server.`)) {
if (typeof resolvedValue === 'string' && resolvedValue.includes(`server.`)) {
let serverVariables: string[] = resolvedValue.match(/server.(.*?)%%/g) || [];
// Map each variable by replacing '%%' and 'server.'
serverVariables = serverVariables?.map((variable) => {
return variable.replace('%%', '').replace('server.', '');
});
const resolvedOrgVar: string[] = [];
for (const variable of serverVariables || []) {
const resolvedVariable = await this.resolveVariable(variable, organization_id);
resolvedOrgVar.push(resolvedVariable);
}
serverVariables?.forEach((i) => {
resolvedVar = resolvedVar.replace('HiddenEnvironmentVariable', resolvedOrgVar[i]);
});
}
if (parent && key !== null) {
parent[key] = resolvedVar;
}
}
// b: Handle {{constants.}} or {{secrets.}}
if (resolvedValue?.includes('{{constants.') || resolvedValue.includes('{{secrets.')) {
if (
(typeof resolvedValue === 'string' && resolvedValue.includes('{{constants.')) ||
resolvedValue.includes('{{secrets.')
) {
const resolvingConstant = await this.resolveConstants(resolvedValue, organization_id, environmentId);
resolvedValue = resolvingConstant;
if (parent && key !== null) {
parent[key] = resolvedValue;
}
}
// c: Replace all occurrences of {{ }} variables
if (resolvedValue?.match(/\{\{(.*?)\}\}/g)?.length > 0) {
if (
typeof resolvedValue === 'string' &&
resolvedValue?.match(/\{\{(.*?)\}\}/g)?.length > 0 &&
!(resolvedValue.startsWith('{{') && resolvedValue.endsWith('}}'))
) {
const variables = resolvedValue.match(/\{\{(.*?)\}\}/g);
for (const variable of variables || []) {
@ -655,18 +653,25 @@ export class DataQueriesService {
// Ensure parent is a non-empty array before attempting to access its first element
if (Array.isArray(parent) && parent.length > 0) {
// Assign replacement value based on the first item in the parent array
replacement = replacement[parent[0]];
replacement = replacement[parent[0]] || replacement;
}
}
resolvedValue = resolvedValue.replace(variable, replacement);
// Check type of replacement and assign accordingly
if (typeof replacement === 'string' || typeof replacement === 'number') {
// If replacement is a string, perform the replace
resolvedValue = resolvedValue.replace(variable, String(replacement));
} else {
// If replacement is an object or an array, assign the whole value to resolvedValue
resolvedValue = resolvedValue.replace(variable, JSON.stringify(replacement));
}
}
if (parent && key !== null) {
parent[key] = resolvedValue;
}
}
// d: Simple variable replacement for single {{variable}}
if (
typeof resolvedValue === 'string' &&
resolvedValue.startsWith('{{') &&
resolvedValue.endsWith('}}') &&
(resolvedValue.match(/{{/g) || [])?.length === 1
@ -676,9 +681,9 @@ export class DataQueriesService {
parent[key] = resolvedValue;
}
}
// e: Handle strings with %%
if (
typeof resolvedValue === 'string' &&
resolvedValue.startsWith('%%') &&
resolvedValue.endsWith('%%') &&
(resolvedValue.match(/%%/g) || [])?.length === 2
@ -692,9 +697,9 @@ export class DataQueriesService {
parent[key] = resolvedValue;
}
}
// f: Replace all %% variables
const variables = resolvedValue?.match(/%%(.*?)%%/g);
//disallow strings with spaces in between '%%' eg. '%% hghgh hg %%'
const variables = typeof resolvedValue === 'string' && resolvedValue?.match(/%%(?:client|server)\.[^\s%%]+%%/g);
if (variables?.length > 0) {
for (const variable of variables) {
if (variable.includes(`server.`)) {
@ -710,7 +715,6 @@ export class DataQueriesService {
}
}
}
return object;
}

View file

@ -845,7 +845,7 @@ export class OrganizationsService {
...(slug && { slug }),
},
});
if (result) throw new ConflictException(`${name ? 'Name' : 'Slug'} must be unique`);
if (result) throw new ConflictException(`Workspace ${name ? 'name' : 'slug'} already exists`);
return;
}