Merge branch 'appbuilder/sprint-11' into feat/steps-v2-alignment-style-improvement

This commit is contained in:
johnsoncherian 2025-04-25 18:23:39 +05:30
commit ad70e31df3
128 changed files with 2207 additions and 461 deletions

View file

@ -147,13 +147,27 @@ jobs:
name: screenshots-${{ matrix.edition }}
path: cypress-tests/cypress/screenshots
Cypress-Platform-Subpath:
runs-on: ubuntu-22.04
if: |
github.event.action == 'labeled' &&
(github.event.label.name == 'run-cypress-platform-subpath' ||
github.event.label.name == 'run-proxy-platform')
github.event.action == 'labeled' &&
(
github.event.label.name == 'run-cypress-platform-subpath' ||
github.event.label.name == 'run-proxy-platform' ||
github.event.label.name == 'run-ce-cypress-platform-subpath' ||
github.event.label.name == 'run-ee-cypress-platform-subpath'
)
strategy:
matrix:
edition: >-
${{
contains(github.event.pull_request.labels.*.name, 'run-cypress-platform-subpath') && fromJson('["ce", "ee"]') ||
contains(github.event.pull_request.labels.*.name, 'run-proxy-platform') && fromJson('["ce", "ee"]') ||
contains(github.event.pull_request.labels.*.name, 'run-ce-cypress-platform-subpath') && fromJson('["ce"]') ||
contains(github.event.pull_request.labels.*.name, 'run-ee-cypress-platform-subpath') && fromJson('["ee"]') ||
fromJson('[]')
}}
steps:
- name: Setup Node.js
@ -161,11 +175,22 @@ jobs:
with:
node-version: 18.18.2
- name: Checkout
- name: Set up Git authentication for private submodules
run: |
git config --global url."https://x-access-token:${{ secrets.CUSTOM_GITHUB_TOKEN }}@github.com/".insteadOf "https://github.com/"
- name: Checkout with Submodules
uses: actions/checkout@v3
with:
ref: ${{ github.event.pull_request.head.ref }}
- name: Checking out the correct branch for submodules EE
if: matrix.edition == 'ee'
run: |
git submodule update --init --recursive
git submodule foreach --recursive '
git checkout ${{ env.BRANCH_NAME }} 2>/dev/null || git checkout main'
- name: Set up Docker configuration
run: |
mkdir -p ~/.docker/cli-plugins
@ -186,13 +211,14 @@ jobs:
uses: docker/build-push-action@v4
with:
context: .
file: docker/production.Dockerfile
file: ${{ matrix.edition == 'ee' && 'docker/ee/ee-production.Dockerfile' || 'docker/ce-production.Dockerfile' }}
push: true
tags: tooljet/tj-osv:${{ env.SAFE_BRANCH_NAME }}
platforms: linux/amd64
- name: Set up environment variables
run: |
echo "TOOLJET_EDITION=${{ matrix.edition == 'ee' && 'ee' || 'ce' }}" >> .env
echo "TOOLJET_HOST=http://localhost:3000" >> .env
echo "LOCKBOX_MASTER_KEY=cd97331a419c09387bef49787f7da8d2a81d30733f0de6bed23ad8356d2068b2" >> .env
echo "SECRET_KEY_BASE=7073b9a35a15dd20914ae17e36a693093f25b74b96517a5fec461fc901c51e011cd142c731bee48c5081ec8bac321c1f259ef097ef2a16f25df17a3798c03426" >> .env
@ -254,15 +280,30 @@ jobs:
uses: actions/upload-artifact@v4
if: always()
with:
name: screenshots
name: screenshots-${{ matrix.edition }}
path: cypress-tests/cypress/screenshots
Cypress-Platform-Proxy:
runs-on: ubuntu-22.04
if: |
github.event.action == 'labeled' &&
(github.event.label.name == 'run-cypress-platform-proxy' ||
github.event.label.name == 'run-proxy-platform')
github.event.action == 'labeled' &&
(
github.event.label.name == 'run-cypress-platform-proxy' ||
github.event.label.name == 'run-proxy-platform' ||
github.event.label.name == 'run-ce-cypress-platform-proxy' ||
github.event.label.name == 'run-ee-cypress-platform-proxy'
)
strategy:
matrix:
edition: >-
${{
contains(github.event.pull_request.labels.*.name, 'run-cypress-platform-proxy') && fromJson('["ce", "ee"]') ||
contains(github.event.pull_request.labels.*.name, 'run-proxy-platform') && fromJson('["ce", "ee"]') ||
contains(github.event.pull_request.labels.*.name, 'run-ce-cypress-platform-proxy') && fromJson('["ce"]') ||
contains(github.event.pull_request.labels.*.name, 'run-ee-cypress-platform-proxy') && fromJson('["ee"]') ||
fromJson('[]')
}}
steps:
- name: Setup Node.js
@ -270,11 +311,22 @@ jobs:
with:
node-version: 18.18.2
- name: Checkout
- name: Set up Git authentication for private submodules
run: |
git config --global url."https://x-access-token:${{ secrets.CUSTOM_GITHUB_TOKEN }}@github.com/".insteadOf "https://github.com/"
- name: Checkout with Submodules
uses: actions/checkout@v3
with:
ref: ${{ github.event.pull_request.head.ref }}
- name: Checking out the correct branch for submodules EE
if: matrix.edition == 'ee'
run: |
git submodule update --init --recursive
git submodule foreach --recursive '
git checkout ${{ env.BRANCH_NAME }} 2>/dev/null || git checkout main'
- name: Set up Docker configuration
run: |
mkdir -p ~/.docker/cli-plugins
@ -295,13 +347,14 @@ jobs:
uses: docker/build-push-action@v4
with:
context: .
file: docker/production.Dockerfile
file: ${{ matrix.edition == 'ee' && 'docker/ee/ee-production.Dockerfile' || 'docker/ce-production.Dockerfile' }}
push: true
tags: tooljet/tj-osv:${{ env.SAFE_BRANCH_NAME }}
platforms: linux/amd64
- name: Set up environment variables
run: |
echo "TOOLJET_EDITION=${{ matrix.edition == 'ee' && 'ee' || 'ce' }}" >> .env
echo "TOOLJET_HOST=http://localhost:3000" >> .env
echo "LOCKBOX_MASTER_KEY=cd97331a419c09387bef49787f7da8d2a81d30733f0de6bed23ad8356d2068b2" >> .env
echo "SECRET_KEY_BASE=7073b9a35a15dd20914ae17e36a693093f25b74b96517a5fec461fc901c51e011cd142c731bee48c5081ec8bac321c1f259ef097ef2a16f25df17a3798c03426" >> .env
@ -375,15 +428,30 @@ jobs:
uses: actions/upload-artifact@v4
if: always()
with:
name: screenshots
name: screenshots-${{ matrix.edition }}
path: cypress-tests/cypress/screenshots
Cypress-Platform-Proxy-Subpath:
runs-on: ubuntu-22.04
if: |
github.event.action == 'labeled' &&
(github.event.label.name == 'run-cypress-platform-proxy-subpath' ||
github.event.label.name == 'run-proxy-platform')
github.event.action == 'labeled' &&
(
github.event.label.name == 'run-cypress-platform-proxy-subpath' ||
github.event.label.name == 'run-proxy-platform' ||
github.event.label.name == 'run-ce-cypress-platform-proxy-subpath' ||
github.event.label.name == 'run-ee-cypress-platform-proxy-subpath'
)
strategy:
matrix:
edition: >-
${{
contains(github.event.pull_request.labels.*.name, 'run-cypress-platform-proxy-subpath') && fromJson('["ce", "ee"]') ||
contains(github.event.pull_request.labels.*.name, 'run-proxy-platform') && fromJson('["ce", "ee"]') ||
contains(github.event.pull_request.labels.*.name, 'run-ce-cypress-platform-proxy-subpath') && fromJson('["ce"]') ||
contains(github.event.pull_request.labels.*.name, 'run-ee-cypress-platform-proxy-subpath') && fromJson('["ee"]') ||
fromJson('[]')
}}
steps:
- name: Setup Node.js
@ -391,11 +459,22 @@ jobs:
with:
node-version: 18.18.2
- name: Checkout
- name: Set up Git authentication for private submodules
run: |
git config --global url."https://x-access-token:${{ secrets.CUSTOM_GITHUB_TOKEN }}@github.com/".insteadOf "https://github.com/"
- name: Checkout with Submodules
uses: actions/checkout@v3
with:
ref: ${{ github.event.pull_request.head.ref }}
- name: Checking out the correct branch for submodules EE
if: matrix.edition == 'ee'
run: |
git submodule update --init --recursive
git submodule foreach --recursive '
git checkout ${{ env.BRANCH_NAME }} 2>/dev/null || git checkout main'
- name: Set up Docker configuration
run: |
mkdir -p ~/.docker/cli-plugins
@ -416,13 +495,14 @@ jobs:
uses: docker/build-push-action@v4
with:
context: .
file: docker/production.Dockerfile
file: ${{ matrix.edition == 'ee' && 'docker/ee/ee-production.Dockerfile' || 'docker/ce-production.Dockerfile' }}
push: true
tags: tooljet/tj-osv:${{ env.SAFE_BRANCH_NAME }}
platforms: linux/amd64
- name: Set up environment variables
run: |
echo "TOOLJET_EDITION=${{ matrix.edition == 'ee' && 'ee' || 'ce' }}" >> .env
echo "TOOLJET_HOST=http://localhost:3000" >> .env
echo "LOCKBOX_MASTER_KEY=cd97331a419c09387bef49787f7da8d2a81d30733f0de6bed23ad8356d2068b2" >> .env
echo "SECRET_KEY_BASE=7073b9a35a15dd20914ae17e36a693093f25b74b96517a5fec461fc901c51e011cd142c731bee48c5081ec8bac321c1f259ef097ef2a16f25df17a3798c03426" >> .env
@ -497,5 +577,5 @@ jobs:
uses: actions/upload-artifact@v4
if: always()
with:
name: screenshots
name: screenshots-${{ matrix.edition }}
path: cypress-tests/cypress/screenshots

View file

@ -137,7 +137,8 @@ jobs:
uses: docker/build-push-action@v4
with:
context: .
args: ${{ secrets.CUSTOM_GITHUB_TOKEN }}
build-args: |
CUSTOM_GITHUB_TOKEN=${{ secrets.CUSTOM_GITHUB_TOKEN }}
file: docker/ee/ee-production.Dockerfile
push: true
tags: tooljet/tooljet-ee:${{ github.event.release.tag_name }},tooljet/tooljet-ee:ee-lts-latest,tooljet/tooljet:ee-lts-latest,tooljet/tooljet:${{ github.event.release.tag_name }}
@ -152,8 +153,9 @@ jobs:
uses: docker/build-push-action@v4
with:
context: .
args: ${{ secrets.CUSTOM_GITHUB_TOKEN }}
file: docker/ee-production.Dockerfile
build-args: |
CUSTOM_GITHUB_TOKEN=${{ secrets.CUSTOM_GITHUB_TOKEN }}
file: docker/ee/ee-production.Dockerfile
push: true
tags: tooljet/tooljet-ee:${{ github.event.release.tag_name }},tooljet/tooljet-ee:ee-lts-latest,tooljet/tooljet:ee-lts-latest,tooljet/tooljet:${{ github.event.release.tag_name }}
platforms: linux/amd64
@ -230,3 +232,95 @@ jobs:
# fi
# curl -X POST -H 'Content-type: application/json' --data "{\"text\":\"$message\"}" ${{ secrets.SLACK_WEBHOOK_URL }}
try-tooljet-image-build:
runs-on: ubuntu-latest
needs: build-tooljet-image-for-ee-edtion
if: ${{ needs.build-tooljet-image-for-ee-edtion.result == 'success' }}
steps:
- name: Checkout code to develop
if: "!contains(github.event.release.tag_name, 'ee-lts')"
uses: actions/checkout@v2
with:
ref: refs/heads/main
- name: Checkout code to lts-3.0
if: contains(github.event.release.tag_name, '-ee-lts')
uses: actions/checkout@v2
with:
ref: refs/heads/lts-3.0
# Create Docker Buildx builder with platform configuration
- name: Set up Docker Buildx
run: |
mkdir -p ~/.docker/cli-plugins
curl -SL https://github.com/docker/buildx/releases/download/v0.11.0/buildx-v0.11.0.linux-amd64 -o ~/.docker/cli-plugins/docker-buildx
chmod a+x ~/.docker/cli-plugins/docker-buildx
docker buildx create --name mybuilder --platform linux/arm64,linux/amd64,linux/amd64/v2,linux/riscv64,linux/ppc64le,linux/s390x,linux/386,linux/mips64le,linux/mips64,linux/arm/v7,linux/arm/v6
docker buildx use mybuilder
- name: Set DOCKER_CLI_EXPERIMENTAL
run: echo "DOCKER_CLI_EXPERIMENTAL=enabled" >> $GITHUB_ENV
- name: use mybuilder buildx
run: docker buildx use mybuilder
- name: Docker Login
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Check if Docker image is present
id: check-image-presence
run: |
response=$(curl -s "https://hub.docker.com/v2/repositories/tooljet/tooljet/tags/${{ github.event.release.tag_name }}")
if [[ $? -ne 0 ]]; then
echo "Failed to fetch JSON response. Stopping workflow execution."
exit 1
fi
if [[ $response == *"tag '${{ github.event.release.tag_name }}' not found"* ]]; then
echo "Docker image tag '${{ github.event.release.tag_name }}' not present."
exit 1
else
echo "Docker image tag '${{ github.event.release.tag_name }}' is present."
fi
- name: Build and Push Docker image for non-EE-LTS
if: "!contains(github.event.release.tag_name, '-ee-lts')"
uses: docker/build-push-action@v4
with:
context: .
file: docker/ee/ee-try-tooljet.Dockerfile
push: true
tags: tooljet/try:${{ github.event.release.tag_name }},tooljet/try:ee-latest
platforms: linux/amd64
env:
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
- name: Build and Push Docker image for EE-LTS-3.0
if: contains(github.event.release.tag_name, '-ee-lts')
uses: docker/build-push-action@v4
with:
context: .
file: docker/ee/ee-try-tooljet-lts.Dockerfile
push: true
tags: tooljet/try:${{ github.event.release.tag_name }},tooljet/try:ee-lts-latest
platforms: linux/amd64
env:
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
- name: Send Slack Notification
run: |
if [[ "${{ job.status }}" == "success" ]]; then
message="Try-ToolJet image published:\\n\`tooljet/try:${{ github.event.release.tag_name }}\`"
else
message="Job '${{ env.JOB_NAME }}' failed! tooljet/try:${{ github.event.release.tag_name }}"
fi
curl -X POST -H 'Content-type: application/json' --data "{\"text\":\"$message\"}" ${{ secrets.SLACK_WEBHOOK_URL }}

View file

@ -8,16 +8,16 @@ permissions:
issues: write
jobs:
label-stale-deploys:
label-stale-ce-deploys:
runs-on: ubuntu-latest
permissions:
pull-requests: write
pull-requests: write
steps:
- uses: akshaysasidrn/stale-label-fetch@v1.1
id: stale-label
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
stale-label: 'active-review-app'
stale-label: 'active-ce-review-app'
stale-time: '86400'
type: 'pull_request'
- name: Get stale numbers
@ -40,6 +40,42 @@ jobs:
issue_number: prNumber,
owner: context.repo.owner,
repo: context.repo.repo,
labels: ['suspend-review-app']
labels: ['suspend-ce-review-app']
})
}
label-stale-ee-deploys:
runs-on: ubuntu-latest
permissions:
pull-requests: write
steps:
- uses: akshaysasidrn/stale-label-fetch@v1.1
id: stale-label
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
stale-label: 'active-ee-review-app'
stale-time: '86400'
type: 'pull_request'
- name: Get stale numbers
run: echo "Matched PR numbers - ${{ steps.stale-label.outputs.stale-numbers }}"
- name: Add suspend label
uses: actions/github-script@v6
env:
STALE_NUMBERS: ${{ steps.stale-label.outputs.stale-numbers }}
with:
github-token: ${{ secrets.TJ_BOT_PAT }}
script: |
if (!process.env.STALE_NUMBERS) return
const prNumbers = process.env.STALE_NUMBERS.split(",")
console.log(`Adding suspend labels for: ${prNumbers}`)
for (const prNumber of prNumbers) {
github.rest.issues.addLabels({
issue_number: prNumber,
owner: context.repo.owner,
repo: context.repo.repo,
labels: ['suspend-ee-review-app']
})
}

View file

@ -11,7 +11,7 @@ on:
# Schedule the workflow to run every two weeks once
schedule:
- cron: '30 5 */14 * *'
- cron: '30 5 * * 1'
jobs:
PeriodicVulnerability-CheckOn-frontend-code:

View file

@ -5,7 +5,7 @@ export const postgreSqlText = {
allDataSources: () => {
return Cypress.env("marketplace_action")
? "All data sources (44)"
: "All data sources (42)";
: "All data sources (43)";
},
commonlyUsed: "Commonly used (5)",
allDatabase: () => {
@ -13,7 +13,7 @@ export const postgreSqlText = {
? "Databases (20)"
: "Databases (18)";
},
allApis: "APIs (20)",
allApis: "APIs (21)",
allCloudStorage: "Cloud Storages (4)",
postgreSQL: "PostgreSQL",

View file

@ -43,8 +43,10 @@ describe("Editor title", () => {
cy.apiDeleteApp();
});
it("should verify titles", () => {
cy.url().should("include", "/my-workspace");
cy.title().should("eq", "Dashboard | ToolJet");
cy.url().should("include", "/tjs-workspace");
// cy.title().should("eq", "Dashboard | ToolJet");
cy.title().should("eq", "ToolJet");
cy.log(data.appName);
cy.openApp();
@ -54,12 +56,20 @@ describe("Editor title", () => {
cy.openInCurrentTab(commonWidgetSelector.previewButton);
cy.url().should("include", `/applications/${Cypress.env("appId")}`);
cy.title().should("eq", `Preview - ${data.appName} | ToolJet`);
cy.title().should("eq", `${data.appName} | ToolJet`);
// cy.title().should("eq", `Preview - ${data.appName} | ToolJet`);
cy.go("back");
cy.releaseApp();
cy.url().then((url) => cy.visit(`/applications/${url.split("/").pop()}`));
cy.url().then((url) => {
const appId = url.split("/").filter(Boolean).pop();
cy.log(appId);
cy.visit(`/applications/${appId}`);
});
cy.url().should("include", `/applications/${Cypress.env("appId")}`);
cy.title().should("eq", `${data.appName}`);
cy.title().should("eq", `${data.appName} | ToolJet`);
// cy.title().should("eq", `${data.appName}`);
});
});

View file

@ -35,13 +35,13 @@ describe("Editor- Global Settings", () => {
"have.text",
"Global settings"
);
cy.get(
'[data-cy="label-hide-header-for-launched-apps"]'
).verifyVisibleElement("have.text", "Hide header for launched apps");
cy.get('[data-cy="label-maintenance-mode"]').verifyVisibleElement(
"have.text",
"Maintenance mode"
);
// cy.get(
// '[data-cy="label-hide-header-for-launched-apps"]'
// ).verifyVisibleElement("have.text", "Hide header for launched apps");
// cy.get('[data-cy="label-maintenance-mode"]').verifyVisibleElement(
// "have.text",
// "Maintenance mode"
// );
cy.hideTooltip();
cy.get('[data-cy="label-max-canvas-width"]').verifyVisibleElement(
"have.text",
@ -60,7 +60,7 @@ describe("Editor- Global Settings", () => {
);
verifyWidgetColorCss(
".canvas-area",
'[data-cy="real-canvas"]',
"background-color",
data.backgroundColor,
true
@ -87,24 +87,25 @@ describe("Editor- Global Settings", () => {
cy.get("[data-cy='left-sidebar-settings-button']").click();
cy.get('[data-cy="toggle-maintenance-mode"]').realClick();
cy.get('[data-cy="modal-confirm-button"]').click();
cy.verifyToastMessage(
commonSelectors.toastMessage,
"Application is on maintenance.",
false
);
// cy.verifyToastMessage(
// commonSelectors.toastMessage,
// "Application is on maintenance.",
// false
// );
cy.forceClickOnCanvas();
cy.wait(500);
cy.waitForAutoSave();
//Fix this after the release. 2.9.0
// cy.get('[data-cy="button-release"]').click();
// cy.get('[data-cy="yes-button"]').click();
// cy.get('[data-cy="editor-page-logo"]').click();
// cy.get(`[data-cy="${data.appName.toLowerCase()}-card"]`)
// .realHover()
// .find('[data-cy="launch-button"]')
// .invoke("attr", "class")
// .should("contains", "disabled-btn");
// Fix this after the release. 2.9.0
cy.get('[data-cy="button-release"]').click();
cy.get('[data-cy="yes-button"]').click();
cy.get('[data-cy="editor-page-logo"]').click();
cy.get('[data-cy="back-to-app-option"]').click();
cy.get(`[data-cy="${data.appName.toLowerCase()}-card"]`)
.realHover().within(() => {
cy.get('[data-cy="launch-button"]').should('have.text', 'Maintenance')
.invoke("attr", "class")
.should("contains", "disabled-btn");
})
cy.apiDeleteApp();
});
});

View file

@ -4,6 +4,7 @@ import { postgreSqlText } from "Texts/postgreSql";
import { bigqueryText } from "Texts/bigquery";
import { firestoreText } from "Texts/firestore";
import { commonSelectors } from "Selectors/common";
import { dataSourceSelector } from "Selectors/dataSource";
import {
fillDataSourceTextField,
selectAndAddDataSource,
@ -16,6 +17,7 @@ const data = {};
describe("Data source BigQuery", () => {
beforeEach(() => {
cy.appUILogin();
cy.defaultWorkspaceLogin();
cy.intercept("GET", "/api/v2/data_sources");
data.dataSourceName = fake.lastName
.toLowerCase()
@ -50,10 +52,19 @@ describe("Data source BigQuery", () => {
postgreSqlText.allCloudStorage
);
selectAndAddDataSource(
"databases",
bigqueryText.bigQuery,
data.dataSourceName
cy.apiCreateGDS(
`${Cypress.env("server_host")}/api/data-sources`,
`cypress-${data.dataSourceName}-bigquery`,
"bigquery",
[{ key: "private_key", value: "", encrypted: true }]
);
cy.reload();
cy.get(`[data-cy="cypress-${data.dataSourceName}-bigquery-button"]`)
.should("be.visible")
.click();
cy.get(dataSourceSelector.dsNameInputField).should(
"have.value",
`cypress-${data.dataSourceName}-bigquery`
);
cy.get('[data-cy="label-private-key"]').verifyVisibleElement(

View file

@ -1,9 +1,8 @@
import { fake } from "Fixtures/fake";
import { postgreSqlSelector } from "Selectors/postgreSql";
import { postgreSqlText } from "Texts/postgreSql";
import { commonWidgetText } from "Texts/common";
import { commonSelectors, commonWidgetSelector } from "Selectors/common";
import { commonText } from "Texts/common";
import { dataSourceSelector } from "Selectors/dataSource";
import { closeDSModal, deleteDatasource } from "Support/utils/dataSource";
import {
addQuery,
@ -21,6 +20,7 @@ const data = {};
describe("Data sources", () => {
beforeEach(() => {
cy.appUILogin();
cy.defaultWorkspaceLogin();
data.dataSourceName = fake.lastName
.toLowerCase()
.replaceAll("[^A-Za-z]", "");
@ -51,13 +51,20 @@ describe("Data sources", () => {
postgreSqlText.allCloudStorage
);
selectAndAddDataSource("databases", "ClickHouse", data.dataSourceName);
// cy.get(postgreSqlSelector.dataSourceNameInputField).should(
// //username,password,host,port,protocol,dbname,usepost, trimquery,gzip,debug,raw
// "have.value",
// "ClickHouse"
// );
cy.apiCreateGDS(
`${Cypress.env("server_host")}/api/data-sources`,
`cypress-${data.dataSourceName}-clickhouse`,
"clickhouse",
[]
);
cy.reload();
cy.get(`[data-cy="cypress-${data.dataSourceName}-clickhouse-button"]`)
.should("be.visible")
.click();
cy.get(dataSourceSelector.dsNameInputField).should(
"have.value",
`cypress-${data.dataSourceName}-clickhouse`
);
cy.get(postgreSqlSelector.labelUserName).verifyVisibleElement(
"have.text",
postgreSqlText.labelUserName
@ -78,7 +85,7 @@ describe("Data sources", () => {
cy.get(postgreSqlSelector.labelDbName).verifyVisibleElement(
"have.text",
postgreSqlText.labelDbName
"Database Name"
);
cy.get('[data-cy="label-protocol"]').verifyVisibleElement(
"have.text",
@ -140,11 +147,7 @@ describe("Data sources", () => {
Cypress.env("pg_host")
);
fillDataSourceTextField(postgreSqlText.labelPort, "8123", "8123");
fillDataSourceTextField(
postgreSqlText.labelDbName,
"database name",
"{del}"
);
fillDataSourceTextField("Database Name", "database name", "{del}");
fillDataSourceTextField(
postgreSqlText.labelUserName,
postgreSqlText.placeholderEnterUserName,

View file

@ -1,9 +1,8 @@
import { fake } from "Fixtures/fake";
import { postgreSqlSelector } from "Selectors/postgreSql";
import { postgreSqlText } from "Texts/postgreSql";
import { commonWidgetText } from "Texts/common";
import { commonSelectors, commonWidgetSelector } from "Selectors/common";
import { commonText } from "Texts/common";
import { dataSourceSelector } from "Selectors/dataSource";
import { closeDSModal, deleteDatasource } from "Support/utils/dataSource";
import {
addQuery,
@ -21,6 +20,7 @@ const data = {};
describe("Data sources", () => {
beforeEach(() => {
cy.appUILogin();
cy.defaultWorkspaceLogin();
data.dataSourceName = fake.lastName
.toLowerCase()
.replaceAll("[^A-Za-z]", "");
@ -50,7 +50,23 @@ describe("Data sources", () => {
"have.text",
postgreSqlText.allCloudStorage
);
selectAndAddDataSource("databases", "CosmosDB", data.dataSourceName);
cy.apiCreateGDS(
`${Cypress.env("server_host")}/api/data-sources`,
`cypress-${data.dataSourceName}-cosmosdb`,
"cosmosdb",
[
{ key: "endpoint", value: "" },
{ key: "key", value: "", encrypted: true },
]
);
cy.reload();
cy.get(`[data-cy="cypress-${data.dataSourceName}-cosmosdb-button"]`)
.should("be.visible")
.click();
cy.get(dataSourceSelector.dsNameInputField).should(
"have.value",
`cypress-${data.dataSourceName}-cosmosdb`
);
cy.get('[data-cy="label-end-point"]').verifyVisibleElement(
"have.text",
@ -92,7 +108,7 @@ describe("Data sources", () => {
deleteDatasource(`cypress-${data.dataSourceName}-cosmosdb`);
});
it.only("Should verify the functionality of CosmosDB connection form.", () => {
it("Should verify the functionality of CosmosDB connection form.", () => {
selectAndAddDataSource("databases", "CosmosDB", data.dataSourceName);
fillDataSourceTextField(

View file

@ -3,7 +3,7 @@ import { postgreSqlSelector } from "Selectors/postgreSql";
import { postgreSqlText } from "Texts/postgreSql";
import { commonWidgetText } from "Texts/common";
import { commonSelectors, commonWidgetSelector } from "Selectors/common";
import { commonText } from "Texts/common";
import { dataSourceSelector } from "Selectors/dataSource";
import { closeDSModal, deleteDatasource } from "Support/utils/dataSource";
import {
@ -22,6 +22,7 @@ const data = {};
describe("Data sources", () => {
beforeEach(() => {
cy.appUILogin();
cy.defaultWorkspaceLogin();
data.dataSourceName = fake.lastName
.toLowerCase()
.replaceAll("[^A-Za-z]", "");
@ -52,7 +53,27 @@ describe("Data sources", () => {
postgreSqlText.allCloudStorage
);
selectAndAddDataSource("databases", "CouchDB", data.dataSourceName);
cy.apiCreateGDS(
`${Cypress.env("server_host")}/api/data-sources`,
`cypress-${data.dataSourceName}-couchdb`,
"couchdb",
[
{ key: "username", value: "", encrypted: false },
{ key: "password", value: "", encrypted: true },
{ key: "database", value: "" },
{ key: "port", value: "5984" },
{ key: "host", value: "" },
{ key: "protocol" },
]
);
cy.reload();
cy.get(`[data-cy="cypress-${data.dataSourceName}-couchdb-button"]`)
.should("be.visible")
.click();
cy.get(dataSourceSelector.dsNameInputField).should(
"have.value",
`cypress-${data.dataSourceName}-couchdb`
);
cy.get(postgreSqlSelector.labelHost).verifyVisibleElement(
"have.text",
@ -72,7 +93,7 @@ describe("Data sources", () => {
);
cy.get(postgreSqlSelector.labelDbName).verifyVisibleElement(
"have.text",
postgreSqlText.labelDbName
"Database Name"
);
cy.get('[data-cy="label-protocol"]').verifyVisibleElement(
@ -122,11 +143,7 @@ describe("Data sources", () => {
Cypress.env("couchdb_host")
);
fillDataSourceTextField(postgreSqlText.labelPort, "5984 ", "5984");
fillDataSourceTextField(
postgreSqlText.labelDbName,
"database name",
"{del}"
);
fillDataSourceTextField("Database Name", "database name", "{del}");
fillDataSourceTextField(
postgreSqlText.labelUserName,
"username for couchDB",

View file

@ -3,7 +3,7 @@ import { postgreSqlSelector } from "Selectors/postgreSql";
import { postgreSqlText } from "Texts/postgreSql";
import { dynamoDbText } from "Texts/dynamodb";
import { commonSelectors } from "Selectors/common";
import { commonText } from "Texts/common";
import { dataSourceSelector } from "Selectors/dataSource";
import {
fillDataSourceTextField,
@ -20,6 +20,7 @@ const data = {};
describe("Data source DynamoDB", () => {
beforeEach(() => {
cy.appUILogin();
cy.defaultWorkspaceLogin();
data.dataSourceName = fake.lastName
.toLowerCase()
.replaceAll("[^A-Za-z]", "");
@ -50,10 +51,28 @@ describe("Data source DynamoDB", () => {
postgreSqlText.allCloudStorage
);
selectAndAddDataSource(
"databases",
dynamoDbText.dynamoDb,
data.dataSourceName
cy.apiCreateGDS(
`${Cypress.env("server_host")}/api/data-sources`,
`cypress-${data.dataSourceName}-dynamodb`,
"dynamodb",
[
{ key: "region", value: "" },
{ key: "access_key", value: "" },
{ key: "secret_key", value: "", encrypted: true },
{
key: "instance_metadata_credentials",
value: "iam_access_keys",
encrypted: false,
},
]
);
cy.reload();
cy.get(`[data-cy="cypress-${data.dataSourceName}-dynamodb-button"]`)
.should("be.visible")
.click();
cy.get(dataSourceSelector.dsNameInputField).should(
"have.value",
`cypress-${data.dataSourceName}-dynamodb`
);
cy.get('[data-cy="label-region"]').verifyVisibleElement(

View file

@ -3,7 +3,7 @@ import { postgreSqlSelector } from "Selectors/postgreSql";
import { postgreSqlText } from "Texts/postgreSql";
import { elasticsearchText } from "Texts/elasticsearch";
import { commonSelectors } from "Selectors/common";
import { commonText } from "Texts/common";
import { dataSourceSelector } from "Selectors/dataSource";
import {
fillDataSourceTextField,
selectAndAddDataSource,
@ -18,6 +18,7 @@ const data = {};
describe("Data source Elasticsearch", () => {
beforeEach(() => {
cy.appUILogin();
cy.defaultWorkspaceLogin();
data.lastName = fake.lastName.toLowerCase().replaceAll("[^A-Za-z]", "");
});
@ -46,12 +47,27 @@ describe("Data source Elasticsearch", () => {
postgreSqlText.allCloudStorage
);
selectAndAddDataSource(
"databases",
elasticsearchText.elasticSearch,
data.lastName
cy.apiCreateGDS(
`${Cypress.env("server_host")}/api/data-sources`,
`cypress-${data.dataSourceName}-elasticsearch`,
"elasticsearch",
[
{ key: "host", value: "localhost" },
{ key: "port", value: 9200 },
{ key: "username", value: "" },
{ key: "password", value: "", encrypted: true },
{ key: "ssl_enabled", value: true, encrypted: false },
{ key: "ssl_certificate", value: "none", encrypted: false },
]
);
cy.reload();
cy.get(`[data-cy="cypress-${data.dataSourceName}-elasticsearch-button"]`)
.should("be.visible")
.click();
cy.get(dataSourceSelector.dsNameInputField).should(
"have.value",
`cypress-${data.dataSourceName}-elasticsearch`
);
cy.get(postgreSqlSelector.labelHost).verifyVisibleElement(
"have.text",
postgreSqlText.labelHost
@ -74,7 +90,7 @@ describe("Data source Elasticsearch", () => {
);
cy.get(postgreSqlSelector.labelSSLCertificate).verifyVisibleElement(
"have.text",
postgreSqlText.sslCertificate
"SSL Certificate"
);
cy.get(postgreSqlSelector.labelIpWhitelist).verifyVisibleElement(
"have.text",

View file

@ -3,7 +3,7 @@ import { postgreSqlSelector } from "Selectors/postgreSql";
import { postgreSqlText } from "Texts/postgreSql";
import { firestoreText } from "Texts/firestore";
import { commonSelectors } from "Selectors/common";
import { commonText } from "Texts/common";
import { dataSourceSelector } from "Selectors/dataSource";
import {
verifyCouldnotConnectWithAlert,
deleteDatasource,
@ -18,6 +18,7 @@ const data = {};
describe("Data source Firestore", () => {
beforeEach(() => {
cy.appUILogin();
cy.defaultWorkspaceLogin();
data.dataSourceName = fake.lastName
.toLowerCase()
.replaceAll("[^A-Za-z]", "");
@ -47,12 +48,20 @@ describe("Data source Firestore", () => {
postgreSqlText.allCloudStorage
);
selectAndAddDataSource(
"databases",
firestoreText.firestore,
data.dataSourceName
cy.apiCreateGDS(
`${Cypress.env("server_host")}/api/data-sources`,
`cypress-${data.dataSourceName}-firestore`,
"firestore",
[{ key: "gcp_key", value: "", encrypted: true }]
);
cy.reload();
cy.get(`[data-cy="cypress-${data.dataSourceName}-firestore-button"]`)
.should("be.visible")
.click();
cy.get(dataSourceSelector.dsNameInputField).should(
"have.value",
`cypress-${data.dataSourceName}-firestore`
);
cy.get('[data-cy="label-private-key"]').verifyVisibleElement(
"have.text",
firestoreText.labelPrivateKey

View file

@ -1,8 +1,8 @@
import { fake } from "Fixtures/fake";
import { postgreSqlSelector } from "Selectors/postgreSql";
import { postgreSqlText } from "Texts/postgreSql";
import { commonWidgetText, commonText } from "Texts/common";
import { commonSelectors, commonWidgetSelector } from "Selectors/common";
import { dataSourceSelector } from "Selectors/dataSource";
import {
addQuery,
fillDataSourceTextField,
@ -24,6 +24,7 @@ const data = {};
describe("Data sources", () => {
beforeEach(() => {
cy.appUILogin();
cy.defaultWorkspaceLogin();
data.dataSourceName = fake.lastName
.toLowerCase()
.replaceAll("[^A-Za-z]", "");
@ -54,7 +55,25 @@ describe("Data sources", () => {
postgreSqlText.allCloudStorage
);
selectAndAddDataSource("databases", "InfluxDB", data.dataSourceName);
cy.apiCreateGDS(
`${Cypress.env("server_host")}/api/data-sources`,
`cypress-${data.dataSourceName}-influxdb`,
"influxdb",
[
{ key: "api_token", value: "", encrypted: true },
{ key: "port", value: "8086", encrypted: false },
{ key: "host", value: "", encrypted: false },
{ key: "protocol", value: "http", encrypted: false },
]
);
cy.reload();
cy.get(`[data-cy="cypress-${data.dataSourceName}-influxdb-button"]`)
.should("be.visible")
.click();
cy.get(dataSourceSelector.dsNameInputField).should(
"have.value",
`cypress-${data.dataSourceName}-influxdb`
);
cy.get('[data-cy="label-api-token"]').verifyVisibleElement(
"have.text",

View file

@ -1,6 +1,6 @@
import { postgreSqlSelector } from "Selectors/postgreSql";
import { postgreSqlText } from "Texts/postgreSql";
import { commonWidgetText, commonText } from "Texts/common";
import { dataSourceSelector } from "Selectors/dataSource";
import { commonSelectors, commonWidgetSelector } from "Selectors/common";
import {
addQuery,
@ -20,6 +20,7 @@ const data = {};
describe("Data sources", () => {
beforeEach(() => {
cy.appUILogin();
cy.defaultWorkspaceLogin();
data.dataSourceName = fake.lastName
.toLowerCase()
.replaceAll("[^A-Za-z]", "");
@ -50,7 +51,20 @@ describe("Data sources", () => {
postgreSqlText.allCloudStorage
);
selectAndAddDataSource("databases", "MariaDB", data.dataSourceName);
cy.apiCreateGDS(
`${Cypress.env("server_host")}/api/data-sources`,
`cypress-${data.dataSourceName}-mariadb`,
"mariadb",
[{ key: "connectionLimit", value: 5 }]
);
cy.reload();
cy.get(`[data-cy="cypress-${data.dataSourceName}-mariadb-button"]`)
.should("be.visible")
.click();
cy.get(dataSourceSelector.dsNameInputField).should(
"have.value",
`cypress-${data.dataSourceName}-mariadb`
);
cy.get(postgreSqlSelector.labelHost).verifyVisibleElement(
"have.text",
@ -83,7 +97,7 @@ describe("Data sources", () => {
cy.get(postgreSqlSelector.labelSSLCertificate).verifyVisibleElement(
"have.text",
postgreSqlText.sslCertificate
"SSL Certificate"
);
cy.get(postgreSqlSelector.labelIpWhitelist).verifyVisibleElement(
"have.text",

View file

@ -3,7 +3,7 @@ import { postgreSqlSelector } from "Selectors/postgreSql";
import { postgreSqlText } from "Texts/postgreSql";
import { mongoDbText } from "Texts/mongoDb";
import { commonSelectors } from "Selectors/common";
import { commonText } from "Texts/common";
import { dataSourceSelector } from "Selectors/dataSource";
import { closeDSModal, deleteDatasource } from "Support/utils/dataSource";
import {
fillDataSourceTextField,
@ -28,6 +28,7 @@ const data = {};
describe("Data source MongoDB", () => {
beforeEach(() => {
cy.appUILogin();
cy.defaultWorkspaceLogin();
data.dataSourceName = fake.lastName
.toLowerCase()
.replaceAll("[^A-Za-z]", "");
@ -56,10 +57,28 @@ describe("Data source MongoDB", () => {
"have.text",
postgreSqlText.allCloudStorage
);
selectAndAddDataSource(
"databases",
mongoDbText.mongoDb,
data.dataSourceName
cy.apiCreateGDS(
`${Cypress.env("server_host")}/api/data-sources`,
`cypress-${data.dataSourceName}-mongodb`,
"mongodb",
[
{ key: "database", value: "", encrypted: false },
{ key: "host", value: "localhost" },
{ key: "port", value: 27017 },
{ key: "username", value: "" },
{ key: "password", value: "", encrypted: true },
{ key: "connection_type", value: "manual" },
{ key: "connection_string", value: "", encrypted: true },
{ key: "tls_certificate", value: "none", encrypted: false },
]
);
cy.reload();
cy.get(`[data-cy="cypress-${data.dataSourceName}-mongodb-button"]`)
.should("be.visible")
.click();
cy.get(dataSourceSelector.dsNameInputField).should(
"have.value",
`cypress-${data.dataSourceName}-mongodb`
);
cy.get(postgreSqlSelector.labelHost).verifyVisibleElement(
@ -72,7 +91,7 @@ describe("Data source MongoDB", () => {
);
cy.get(postgreSqlSelector.labelDbName).verifyVisibleElement(
"have.text",
postgreSqlText.labelDbName
"Database Name"
);
cy.get(postgreSqlSelector.labelUserName).verifyVisibleElement(
"have.text",
@ -168,7 +187,7 @@ describe("Data source MongoDB", () => {
data.dataSourceName
);
cy.get('[data-cy="query-select-dropdown"]').type(
cy.get('[data-cy="connection-type-select-dropdown"]').type(
mongoDbText.optionConnectUsingConnectionString
);

View file

@ -19,6 +19,7 @@ import {
deleteDatasource,
verifyCouldnotConnectWithAlert,
} from "Support/utils/dataSource";
import { dataSourceSelector } from "Selectors/dataSource";
import { realHover } from "cypress-real-events/commands/realHover";
const data = {};
@ -26,6 +27,7 @@ const data = {};
describe("Data sources MySql", () => {
beforeEach(() => {
cy.appUILogin();
cy.defaultWorkspaceLogin();
data.dataSourceName = fake.lastName
.toLowerCase()
.replaceAll("[^A-Za-z]", "");
@ -56,7 +58,30 @@ describe("Data sources MySql", () => {
postgreSqlText.allCloudStorage
);
selectAndAddDataSource("databases", "MySQL", data.dataSourceName);
cy.apiCreateGDS(
`${Cypress.env("server_host")}/api/data-sources`,
`cypress-${data.dataSourceName}-mysql`,
"mysql",
[
{ key: "connection_type", value: "hostname" },
{ key: "host", value: "localhost" },
{ key: "port", value: 3306 },
{ key: "database", value: "" },
{ key: "socket", value: "", encrypted: false },
{ key: "username", value: "" },
{ key: "password", value: "", encrypted: true },
{ key: "ssl_enabled", value: false, encrypted: false },
{ key: "ssl_certificate", value: "none", encrypted: false },
]
);
cy.reload();
cy.get(`[data-cy="cypress-${data.dataSourceName}-mysql-button"]`)
.should("be.visible")
.click();
cy.get(dataSourceSelector.dsNameInputField).should(
"have.value",
`cypress-${data.dataSourceName}-mysql`
);
cy.get(postgreSqlSelector.labelHost).verifyVisibleElement(
"have.text",
@ -110,7 +135,7 @@ describe("Data sources MySql", () => {
deleteDatasource(`cypress-${data.dataSourceName}-mysql`);
});
it.only("Should verify the functionality of MySQL connection form.", () => {
it("Should verify the functionality of MySQL connection form.", () => {
selectAndAddDataSource("databases", "MySQL", data.dataSourceName);
fillDataSourceTextField(
@ -170,9 +195,9 @@ describe("Data sources MySql", () => {
verifyCouldnotConnectWithAlert(
"ER_ACCESS_DENIED_ERROR: Access denied for user 'root'@'103.171.99.42' (using password: YES)"
);
cy.get('[data-cy="-toggle-input"]').then(($el) => {
cy.get('[data-cy="ssl-enabled-toggle-input"]').then(($el) => {
if ($el.is(":checked")) {
cy.get('[data-cy="-toggle-input"]').uncheck();
cy.get('[data-cy="ssl-enabled-toggle-input"]').uncheck();
}
});

View file

@ -3,6 +3,7 @@ import { postgreSqlSelector } from "Selectors/postgreSql";
import { postgreSqlText } from "Texts/postgreSql";
import { commonWidgetText, commonText } from "Texts/common";
import { commonSelectors, commonWidgetSelector } from "Selectors/common";
import { dataSourceSelector } from "Selectors/dataSource";
import {
addQuery,
fillDataSourceTextField,
@ -20,6 +21,7 @@ const data = {};
describe("Data sources", () => {
beforeEach(() => {
cy.appUILogin();
cy.defaultWorkspaceLogin();
data.dataSourceName = fake.lastName
.toLowerCase()
.replaceAll("[^A-Za-z]", "");
@ -52,10 +54,31 @@ describe("Data sources", () => {
postgreSqlText.allCloudStorage
);
selectAndAddDataSource(
"databases",
postgreSqlText.postgreSQL,
data.dataSourceName
cy.apiCreateGDS(
`${Cypress.env("server_host")}/api/data-sources`,
`cypress-${data.dataSourceName}-postgresql`,
"postgresql",
[
{ key: "connection_type", value: "manual", encrypted: false },
{ key: "host", value: "localhost", encrypted: false },
{ key: "port", value: 5432, encrypted: false },
{ key: "ssl_enabled", value: true, encrypted: false },
{ key: "ssl_certificate", value: "none", encrypted: false },
{ key: "password", value: null, encrypted: true },
{ key: "ca_cert", value: null, encrypted: true },
{ key: "client_key", value: null, encrypted: true },
{ key: "client_cert", value: null, encrypted: true },
{ key: "root_cert", value: null, encrypted: true },
{ key: "connection_string", value: null, encrypted: true },
]
);
cy.reload();
cy.get(`[data-cy="cypress-${data.dataSourceName}-postgresql-button"]`)
.should("be.visible")
.click();
cy.get(dataSourceSelector.dsNameInputField).should(
"have.value",
`cypress-${data.dataSourceName}-postgresql`
);
cy.get(postgreSqlSelector.labelHost).verifyVisibleElement(

View file

@ -3,6 +3,7 @@ import { postgreSqlSelector } from "Selectors/postgreSql";
import { postgreSqlText } from "Texts/postgreSql";
import { commonWidgetText, commonText } from "Texts/common";
import { commonSelectors, commonWidgetSelector } from "Selectors/common";
import { dataSourceSelector } from "Selectors/dataSource";
import {
addQuery,
fillDataSourceTextField,
@ -19,6 +20,7 @@ const data = {};
describe("Data sources", () => {
beforeEach(() => {
cy.appUILogin();
cy.defaultWorkspaceLogin();
data.dataSourceName = fake.lastName
.toLowerCase()
.replaceAll("[^A-Za-z]", "");
@ -49,7 +51,26 @@ describe("Data sources", () => {
postgreSqlText.allCloudStorage
);
selectAndAddDataSource("databases", "RethinkDB", data.dataSourceName);
cy.apiCreateGDS(
`${Cypress.env("server_host")}/api/data-sources`,
`cypress-${data.dataSourceName}-rethinkdb`,
"rethinkdb",
[
{ key: "port", value: "28015", encrypted: false },
{ key: "host", value: "", encrypted: false },
{ key: "database", value: "", encrypted: false },
{ key: "username", value: "", encrypted: false },
{ key: "password", value: "", encrypted: true },
]
);
cy.reload();
cy.get(`[data-cy="cypress-${data.dataSourceName}-rethinkdb-button"]`)
.should("be.visible")
.click();
cy.get(dataSourceSelector.dsNameInputField).should(
"have.value",
`cypress-${data.dataSourceName}-rethinkdb`
);
cy.get('[data-cy="label-database"]').verifyVisibleElement(
"have.text",

View file

@ -4,7 +4,7 @@ import { s3Selector } from "Selectors/awss3";
import { postgreSqlText } from "Texts/postgreSql";
import { s3Text } from "Texts/awss3";
import { commonSelectors } from "Selectors/common";
import { commonText } from "Texts/common";
import { dataSourceSelector } from "Selectors/dataSource";
import {
fillDataSourceTextField,
selectAndAddDataSource,
@ -51,7 +51,31 @@ describe("Data sources AWS S3", () => {
postgreSqlText.allCloudStorage
);
selectAndAddDataSource("cloudstorage", s3Text.awsS3, data.dataSourceName);
cy.apiCreateGDS(
`${Cypress.env("server_host")}/api/data-sources`,
`cypress-${data.dataSourceName}-aws-s3`,
"s3",
[
{ key: "access_key", value: "" },
{ key: "secret_key", value: "", encrypted: true },
{ key: "region", value: "" },
{ key: "endpoint", value: "" },
{ key: "endpoint_enabled", value: false, encrypted: false },
{
key: "instance_metadata_credentials",
value: "iam_access_keys",
encrypted: false,
},
]
);
cy.reload();
cy.get(`[data-cy="cypress-${data.dataSourceName}-aws-s3-button"]`)
.should("be.visible")
.click();
cy.get(dataSourceSelector.dsNameInputField).should(
"have.value",
`cypress-${data.dataSourceName}-aws-s3`
);
cy.get(s3Selector.accessKeyLabel).verifyVisibleElement(
"have.text",
s3Text.accessKey

View file

@ -2,7 +2,7 @@ import { fake } from "Fixtures/fake";
import { postgreSqlSelector } from "Selectors/postgreSql";
import { postgreSqlText } from "Texts/postgreSql";
import { commonSelectors } from "Selectors/common";
import { commonText } from "Texts/common";
import { dataSourceSelector } from "Selectors/dataSource";
import {
fillDataSourceTextField,
selectAndAddDataSource,
@ -14,6 +14,7 @@ const data = {};
describe("Data source SMTP", () => {
beforeEach(() => {
cy.appUILogin();
cy.defaultWorkspaceLogin();
data.dataSourceName = fake.lastName
.toLowerCase()
.replaceAll("[^A-Za-z]", "");
@ -43,7 +44,25 @@ describe("Data source SMTP", () => {
postgreSqlText.allCloudStorage
);
selectAndAddDataSource("apis", "SMTP", data.dataSourceName);
cy.apiCreateGDS(
`${Cypress.env("server_host")}/api/data-sources`,
`cypress-${data.dataSourceName}-smtp`,
"smtp",
[
{ key: "host", value: "localhost", encrypted: false },
{ key: "port", value: 465, encrypted: false },
{ key: "user", value: "", encrypted: false },
{ key: "password", value: "", encrypted: true },
]
);
cy.reload();
cy.get(`[data-cy="cypress-${data.dataSourceName}-smtp-button"]`)
.should("be.visible")
.click();
cy.get(dataSourceSelector.dsNameInputField).should(
"have.value",
`cypress-${data.dataSourceName}-smtp`
);
cy.get(postgreSqlSelector.labelHost).verifyVisibleElement(
"have.text",

View file

@ -4,6 +4,7 @@ import { postgreSqlText } from "Texts/postgreSql";
import { commonWidgetText, commonText } from "Texts/common";
import { commonSelectors, commonWidgetSelector } from "Selectors/common";
import { closeDSModal, deleteDatasource } from "Support/utils/dataSource";
import { dataSourceSelector } from "Selectors/dataSource";
import {
addQuery,
@ -20,6 +21,7 @@ const data = {};
describe("Data sources", () => {
beforeEach(() => {
cy.appUILogin();
cy.defaultWorkspaceLogin();
data.dataSourceName = fake.lastName
.toLowerCase()
.replaceAll("[^A-Za-z]", "");
@ -49,8 +51,28 @@ describe("Data sources", () => {
"have.text",
postgreSqlText.allCloudStorage
);
selectAndAddDataSource("databases", "Snowflake", data.dataSourceName);
cy.apiCreateGDS(
`${Cypress.env("server_host")}/api/data-sources`,
`cypress-${data.dataSourceName}-snowflake`,
"snowflake",
[
{ key: "username", value: "" },
{ key: "account", value: "" },
{ key: "password", value: "", encrypted: true },
{ key: "database", value: "" },
{ key: "schema", value: "" },
{ key: "warehouse", value: "" },
{ key: "role", value: "" },
]
);
cy.reload();
cy.get(`[data-cy="cypress-${data.dataSourceName}-snowflake-button"]`)
.should("be.visible")
.click();
cy.get(dataSourceSelector.dsNameInputField).should(
"have.value",
`cypress-${data.dataSourceName}-snowflake`
);
cy.get(postgreSqlSelector.labelUserName).verifyVisibleElement(
"have.text",
postgreSqlText.labelUserName
@ -113,7 +135,7 @@ describe("Data sources", () => {
deleteDatasource(`cypress-${data.dataSourceName}-snowflake`);
});
it.skip("Should verify the functionality of PostgreSQL connection form.", () => {
it.skip("Should verify the functionality of snowflake connection form.", () => {
selectAndAddDataSource("databases", "Snowflake", data.dataSourceName);
fillDataSourceTextField(

View file

@ -4,6 +4,7 @@ import { postgreSqlText } from "Texts/postgreSql";
import { commonWidgetText, commonText } from "Texts/common";
import { commonSelectors, commonWidgetSelector } from "Selectors/common";
import { deleteDatasource, closeDSModal } from "Support/utils/dataSource";
import { dataSourceSelector } from "Selectors/dataSource";
import {
addQuery,
@ -21,6 +22,7 @@ const data = {};
describe("Data sources", () => {
beforeEach(() => {
cy.appUILogin();
cy.defaultWorkspaceLogin();
data.dataSourceName = fake.lastName
.toLowerCase()
.replaceAll("[^A-Za-z]", "");
@ -51,7 +53,28 @@ describe("Data sources", () => {
postgreSqlText.allCloudStorage
);
selectAndAddDataSource("databases", "SQL Server", data.dataSourceName);
cy.apiCreateGDS(
`${Cypress.env("server_host")}/api/data-sources`,
`cypress-${data.dataSourceName}-sql-server`,
"mssql",
[
{ key: "host", value: "localhost" },
{ key: "instanceName", value: "" },
{ key: "port", value: 1433 },
{ key: "database", value: "" },
{ key: "username", value: "" },
{ key: "password", value: "", encrypted: true },
{ key: "azure", value: false, encrypted: false },
]
);
cy.reload();
cy.get(`[data-cy="cypress-${data.dataSourceName}-sql-server-button"]`)
.should("be.visible")
.click();
cy.get(dataSourceSelector.dsNameInputField).should(
"have.value",
`cypress-${data.dataSourceName}-sql-server`
);
cy.get(postgreSqlSelector.labelHost).verifyVisibleElement(
"have.text",
@ -67,7 +90,7 @@ describe("Data sources", () => {
);
cy.get(postgreSqlSelector.labelDbName).verifyVisibleElement(
"have.text",
postgreSqlText.labelDbName
"Database Name"
);
cy.get(postgreSqlSelector.labelUserName).verifyVisibleElement(
"have.text",
@ -78,8 +101,8 @@ describe("Data sources", () => {
"Password"
);
cy.get('[data-cy="label-azure"]').verifyVisibleElement(
"have.text",
cy.get('[data-cy^="label-azure-"]').verifyVisibleElement(
"contain",
"Azure"
);
cy.get(postgreSqlSelector.labelIpWhitelist).verifyVisibleElement(
@ -135,7 +158,7 @@ describe("Data sources", () => {
"1433"
);
fillDataSourceTextField(
postgreSqlText.labelDbName,
"Database Name",
postgreSqlText.placeholderNameOfDB,
Cypress.env("sqlserver_db")
);

View file

@ -40,7 +40,7 @@ export const verifyControlComponentAction = (widgetName, value) => {
export const addBasicData = (data) => {
openEditorSidebar(buttonText.defaultWidgetName);
verifyAndModifyParameter(buttonText.buttonTextLabel, data.widgetName);
verifyAndModifyParameter('Label', data.widgetName);
openAccordion(commonWidgetText.accordionEvents);
addDefaultEventHandler(data.alertMessage);

View file

@ -6,6 +6,7 @@ import { commonText } from "Texts/common";
import { dataSourceSelector } from "Selectors/dataSource";
import { dataSourceText } from "Texts/dataSource";
import { navigateToAppEditor } from "Support/utils/common";
import { verifyAppDelete } from "Support/utils/dashboard";
export const verifyCouldnotConnectWithAlert = (dangerText) => {
cy.get(postgreSqlSelector.connectionFailedText, {
@ -60,6 +61,15 @@ export const deleteDatasource = (datasourceName) => {
// " Databases"
// );
};
export const deleteAppandDatasourceAfterExecution = (
appName,
datasourceName
) => {
cy.backToApps();
cy.deleteApp(appName);
verifyAppDelete(appName);
deleteDatasource(datasourceName);
};
export const closeDSModal = () => {
cy.get("body").then(($body) => {
@ -96,9 +106,7 @@ export const addQueryN = (queryName, query, dbName) => {
export const addQuery = (queryName, query, dbName) => {
cy.get('[data-cy="show-ds-popover-button"]').click();
cy.get(".css-4e90k9").type(`${dbName}`);
cy.intercept("POST", "/api/data-queries/**").as(
"createQuery"
);
cy.intercept("POST", "/api/data-queries/**").as("createQuery");
cy.contains(`[id*="react-select-"]`, dbName).click();
cy.get('[data-cy="query-rename-input"]').clear().type(queryName);
@ -225,7 +233,14 @@ export const createDataQuery = (appName, url, key, value) => {
});
};
export const createRestAPIQuery = (queryName, dsName, key = '', value = '', url = "", run = true) => {
export const createRestAPIQuery = (
queryName,
dsName,
key = "",
value = "",
url = "",
run = true
) => {
cy.getCookie("tj_auth_token").then((cookie) => {
const headers = {
"Tj-Workspace-Id": Cypress.env("workspaceId"),

27
docker/ce-entrypoint.sh Executable file
View file

@ -0,0 +1,27 @@
#!/bin/bash
set -e
if [ -d "./server/dist" ]; then
SETUP_CMD='npm run db:setup:prod'
else
SETUP_CMD='npm run db:setup'
fi
if [ -f "./.env" ]; then
declare $(grep -v '^#' ./.env | xargs)
fi
if [ -z "$DATABASE_URL" ]; then
./server/scripts/wait-for-it.sh $PG_HOST:${PG_PORT:-5432} --strict --timeout=300 -- $SETUP_CMD
else
PG_HOST=$(echo "$DATABASE_URL" | awk -F'[/:@?]' '{print $6}')
PG_PORT=$(echo "$DATABASE_URL" | awk -F'[/:@?]' '{print $7}')
if [ -z "$DATABASE_PORT" ]; then
DATABASE_PORT="5432"
fi
./server/scripts/wait-for-it.sh "$PG_HOST:$PG_PORT" --strict --timeout=300 -- $SETUP_CMD
fi
exec "$@"

View file

@ -88,12 +88,13 @@ COPY --from=builder /app/frontend/build ./app/frontend/build
# copy server build
COPY --from=builder /app/server/package.json ./app/server/package.json
COPY --from=builder /app/server/.version ./app/server/.version
COPY --from=builder /app/server/entrypoint.sh ./app/server/entrypoint.sh
COPY --from=builder /app/server/node_modules ./app/server/node_modules
COPY --from=builder /app/server/templates ./app/server/templates
COPY --from=builder /app/server/scripts ./app/server/scripts
COPY --from=builder /app/server/dist ./app/server/dist
COPY ./docker/ce-entrypoint.sh ./app/server/entrypoint.sh
# Define non-sudo user
RUN useradd --create-home --home-dir /home/appuser appuser \
&& chown -R appuser:0 /app \
@ -111,5 +112,4 @@ WORKDIR /app
# Dependencies for scripts outside nestjs
RUN npm install dotenv@10.0.0 joi@17.4.1
ENTRYPOINT ["./server/entrypoint.sh"]

View file

@ -145,12 +145,13 @@ COPY --from=builder /app/frontend/build ./app/frontend/build
COPY --from=builder /app/server/package.json ./app/server/package.json
COPY --from=builder /app/server/.version ./app/server/.version
COPY --from=builder /app/server/ee/keys ./app/server/ee/keys
COPY --from=builder /app/server/entrypoint.sh ./app/server/entrypoint.sh
COPY --from=builder /app/server/node_modules ./app/server/node_modules
COPY --from=builder /app/server/templates ./app/server/templates
COPY --from=builder /app/server/scripts ./app/server/scripts
COPY --from=builder /app/server/dist ./app/server/dist
COPY ./docker/ee/ee-entrypoint.sh ./app/server/ee-entrypoint.sh
# Define non-sudo user
RUN useradd --create-home --home-dir /home/appuser appuser \
&& chown -R appuser:0 /app \
@ -214,4 +215,4 @@ RUN npm install dotenv@10.0.0 joi@17.4.1
RUN npm cache clean --force
ENTRYPOINT ["./server/entrypoint.sh"]
ENTRYPOINT ["./server/ee-entrypoint.sh"]

View file

@ -0,0 +1,15 @@
#!/bin/bash
set -e
# Start Redis
# service redis-server start
# redis-server /etc/redis/redis.conf
# Start Postgres
service postgresql start
# Export the PORT variable to be used by the application
export PORT=${PORT:-80}
# Start Supervisor
exec supervisord -c /etc/supervisor/conf.d/supervisord.conf

View file

@ -22,7 +22,7 @@ echo "Starting Temporal Server..."
export PORT=${PORT:-80}
# Start Supervisor
/usr/bin/supervisord -n &
exec supervisord -c /etc/supervisor/conf.d/supervisord.conf &
# Wait for Temporal Server to be ready
echo "Waiting for Temporal Server to be ready..."

View file

@ -1,21 +1,31 @@
FROM tooljet/tooljet-ce:latest
FROM tooljet/tooljet:ee-lts-latest
# copy postgrest executable
COPY --from=postgrest/postgrest:v10.1.1.20221215 /bin/postgrest /bin
# Copy PostgREST executable
COPY --from=postgrest/postgrest:v12.2.0 /bin/postgrest /bin
# Install Postgres
# Install PostgreSQL
USER root
RUN wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add -
RUN echo "deb http://apt.postgresql.org/pub/repos/apt/ bullseye-pgdg main" | tee /etc/apt/sources.list.d/pgdg.list
RUN echo "deb http://deb.debian.org/debian"
RUN apt update && apt -y install postgresql-13 postgresql-client-13 supervisor
USER postgres
RUN service postgresql start && \
psql -c "create role tooljet with login superuser password 'postgres';"
USER root
# Install Redis
RUN apt update && apt -y install redis
# Create appuser home & ensure permission for supervisord and services
RUN mkdir -p /var/log/supervisor /var/run/postgresql /var/lib/postgresql /var/lib/redis && \
chown -R appuser:appuser /etc/supervisor /var/log/supervisor /var/lib/redis && \
chown -R postgres:postgres /var/run/postgresql /var/lib/postgresql
# Configure Supervisor to manage PostgREST, ToolJet, and Redis
RUN echo "[supervisord] \n" \
"nodaemon=true \n" \
"user=root \n" \
"\n" \
"[program:postgrest] \n" \
"command=/bin/postgrest \n" \
@ -23,12 +33,23 @@ RUN echo "[supervisord] \n" \
"autorestart=true \n" \
"\n" \
"[program:tooljet] \n" \
"user=appuser \n" \
"command=/bin/bash -c '/app/server/scripts/init-db-boot.sh' \n" \
"autostart=true \n" \
"autorestart=true \n" \
"stderr_logfile=/dev/stdout \n" \
"stderr_logfile_maxbytes=0 \n" \
"stdout_logfile=/dev/stdout \n" \
"stdout_logfile_maxbytes=0 \n" \
"\n" \
"[program:redis] \n" \
"user=appuser \n" \
"command=/usr/bin/redis-server \n" \
"autostart=true \n" \
"autorestart=true \n" \
"stderr_logfile=/dev/stdout \n" \
"stderr_logfile_maxbytes=0 \n" \
"stdout_logfile=/dev/stdout \n" \
"stdout_logfile_maxbytes=0 \n" | sed 's/ //' > /etc/supervisor/conf.d/supervisord.conf
# ENV defaults
@ -49,10 +70,17 @@ ENV TOOLJET_HOST=http://localhost \
PGRST_HOST=http://localhost:3000 \
PGRST_DB_URI=postgres://tooljet:postgres@localhost/tooljet_db \
PGRST_JWT_SECRET=r9iMKoe5CRMgvJBBtp4HrqN7QiPpUToj \
PGRST_DB_PRE_CONFIG=postgrest.pre_config \
ORM_LOGGING=true \
DEPLOYMENT_PLATFORM=docker:local \
HOME=/home/appuser \
REDIS_HOST=localhost \
REDIS_PORT=6379 \
REDIS_USER=default \
REDIS_PASS= \
TERM=xterm
# Prepare DB and start application
ENTRYPOINT service postgresql start 1> /dev/null && /usr/bin/supervisord
# Set the entrypoint
COPY ./docker/ee/ee-try-entrypoint-lts.sh /ee-try-entrypoint-lts.sh
RUN chmod +x /ee-try-entrypoint-lts
ENTRYPOINT ["/ee-try-entrypoint-lts.sh"]

View file

@ -0,0 +1,117 @@
FROM tooljet/tooljet:ee-latest
# Copy postgrest executable
COPY --from=postgrest/postgrest:v12.2.0 /bin/postgrest /bin
# Install Postgres
USER root
RUN wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add -
RUN echo "deb http://apt.postgresql.org/pub/repos/apt/ bullseye-pgdg main" | tee /etc/apt/sources.list.d/pgdg.list
RUN echo "deb http://deb.debian.org/debian"
RUN apt update && apt -y install postgresql-13 postgresql-client-13 supervisor
USER postgres
RUN service postgresql start && \
psql -c "create role tooljet with login superuser password 'postgres';"
USER root
RUN apt update && apt -y install redis
# Create appuser home & ensure permission for supervisord and services
RUN mkdir -p /var/log/supervisor /var/run/postgresql /var/lib/postgresql /var/lib/redis && \
chown -R appuser:appuser /etc/supervisor /var/log/supervisor /var/lib/redis && \
chown -R postgres:postgres /var/run/postgresql /var/lib/postgresql
# Install Temporal Server Binaries
RUN curl -OL https://github.com/temporalio/temporal/releases/download/v1.24.2/temporal_1.24.2_linux_amd64.tar.gz && \
tar -xzf temporal_1.24.2_linux_amd64.tar.gz && \
mv temporal-server /usr/bin/temporal-server && \
chmod +x /usr/bin/temporal-server && \
rm temporal_1.24.2_linux_amd64.tar.gz
# Install Temporal UI Server Binaries
RUN curl -OL https://github.com/temporalio/ui-server/releases/download/v2.28.0/ui-server_2.28.0_linux_amd64.tar.gz && \
tar -xzf ui-server_2.28.0_linux_amd64.tar.gz && \
mv ui-server /usr/bin/temporal-ui-server && \
chmod +x /usr/bin/temporal-ui-server && \
rm ui-server_2.28.0_linux_amd64.tar.gz
# Copy Temporal configuration files
COPY ./docker/ee/temporal-server.yaml /etc/temporal/temporal-server.yaml
COPY ./docker/ee/temporal-ui-server.yaml /etc/temporal/temporal-ui-server.yaml
# Install grpcurl
RUN apt update && apt install -y curl \
&& curl -sSL https://github.com/fullstorydev/grpcurl/releases/download/v1.8.0/grpcurl_1.8.0_linux_x86_64.tar.gz | tar -xzv -C /usr/local/bin grpcurl
# Configure Supervisor to manage PostgREST, ToolJet, and Redis
RUN echo "[supervisord] \n" \
"nodaemon=true \n" \
"user=root \n" \
"\n" \
"[program:postgrest] \n" \
"command=/bin/postgrest \n" \
"autostart=true \n" \
"autorestart=true \n" \
"\n" \
"[program:tooljet] \n" \
"user=appuser \n" \
"command=/bin/bash -c '/app/server/scripts/init-db-boot.sh' \n" \
"autostart=true \n" \
"autorestart=true \n" \
"stderr_logfile=/dev/stdout \n" \
"stderr_logfile_maxbytes=0 \n" \
"stdout_logfile=/dev/stdout \n" \
"stdout_logfile_maxbytes=0 \n" \
"\n" \
"[program:redis] \n" \
"user=appuser \n" \
"command=/usr/bin/redis-server \n" \
"autostart=true \n" \
"autorestart=true \n" \
"stderr_logfile=/dev/stdout \n" \
"stderr_logfile_maxbytes=0 \n" \
"stdout_logfile=/dev/stdout \n" \
"stdout_logfile_maxbytes=0 \n" | sed 's/ //' > /etc/supervisor/conf.d/supervisord.conf
# ENV defaults
ENV TOOLJET_HOST=http://localhost \
TOOLJET_SERVER_URL=http://localhost \
PORT=80 \
NODE_ENV=production \
LOCKBOX_MASTER_KEY=replace_with_lockbox_master_key \
SECRET_KEY_BASE=replace_with_secret_key_base \
PG_DB=tooljet_production \
PG_USER=tooljet \
PG_PASS=postgres \
PG_HOST=localhost \
ENABLE_TOOLJET_DB=true \
TOOLJET_DB_HOST=localhost \
TOOLJET_DB_USER=tooljet \
TOOLJET_DB_PASS=postgres \
TOOLJET_DB=tooljet_db \
PGRST_HOST=http://localhost:3000 \
PGRST_DB_URI=postgres://tooljet:postgres@localhost/tooljet_db \
PGRST_JWT_SECRET=r9iMKoe5CRMgvJBBtp4HrqN7QiPpUToj \
PGRST_DB_PRE_CONFIG=postgrest.pre_config \
ORM_LOGGING=true \
DEPLOYMENT_PLATFORM=docker:local \
HOME=/home/appuser \
REDIS_HOST=localhost \
REDIS_PORT=6379 \
REDIS_USER=default \
REDIS_PASS= \
ENABLE_MARKETPLACE_FEATURE=true \
TERM=xterm \
ENABLE_WORKFLOW_SCHEDULING=true \
TEMPORAL_SERVER_ADDRESS=localhost:7233 \
TEMPORAL_TASK_QUEUE_NAME_FOR_WORKFLOWS=tooljet-workflows \
TOOLJET_WORKFLOWS_TEMPORAL_NAMESPACE=default \
TEMPORAL_ADDRESS=localhost:7233 \
TEMPORAL_CORS_ORIGINS=http://localhost:8080
# Set the entrypoint
COPY ./docker/ee/ee-try-entrypoint.sh /ee-try-entrypoint.sh
RUN chmod +x /ee-try-entrypoint.sh
ENTRYPOINT ["/ee-try-entrypoint.sh"]

View file

@ -0,0 +1,75 @@
log:
stdout: true
level: info
persistence:
defaultStore: sqlite-default
visibilityStore: sqlite-visibility
numHistoryShards: 4
datastores:
sqlite-default:
sql:
pluginName: "sqlite"
databaseName: "/etc/temporal/default.db"
connectAddr: "localhost"
connectProtocol: "tcp"
connectAttributes:
cache: "private"
setup: true
sqlite-visibility:
sql:
pluginName: "sqlite"
databaseName: "/etc/temporal/visibility.db"
connectAddr: "localhost"
connectProtocol: "tcp"
connectAttributes:
cache: "private"
setup: true
global:
membership:
maxJoinDuration: 30s
broadcastAddress: "127.0.0.1"
pprof:
port: 7936
services:
frontend:
rpc:
grpcPort: 7233
membershipPort: 6933
bindOnLocalHost: true
httpPort: 7243
matching:
rpc:
grpcPort: 7235
membershipPort: 6935
bindOnLocalHost: true
history:
rpc:
grpcPort: 7234
membershipPort: 6934
bindOnLocalHost: true
worker:
rpc:
membershipPort: 6939
clusterMetadata:
enableGlobalNamespace: false
failoverVersionIncrement: 10
masterClusterName: "active"
currentClusterName: "active"
clusterInformation:
active:
enabled: true
initialFailoverVersion: 1
rpcName: "frontend"
rpcAddress: "localhost:7236"
httpAddress: "localhost:7243"
dcRedirectionPolicy:
policy: "noop"

View file

@ -0,0 +1,8 @@
temporalGrpcAddress: 127.0.0.1:7233 # Use the correct Temporal server address
host: 0.0.0.0
port: 8080
enableUi: true
cors:
allowOrigins:
- http://localhost:8080
defaultNamespace: default

@ -1 +1 @@
Subproject commit dcd948d284b5f14a868480830e09b90496db8572
Subproject commit a1435b3b0e66a0c731812256d3d495d5bf48d5bb

View file

@ -579,6 +579,11 @@ export default function Grid({ gridWidth, currentLayout }) {
keepRatio={false}
individualGroupableProps={individualGroupableProps}
onResize={(e) => {
if(resizingComponentId !== e.target.id) {
useGridStore.getState().actions.setResizingComponentId(e.target.id);
showGridLines();
}
const currentWidget = boxList.find(({ id }) => id === e.target.id);
let _gridWidth = useGridStore.getState().subContainerWidths[currentWidget.component?.parent] || gridWidth;
if (currentWidget.component?.parent) {
@ -639,9 +644,7 @@ export default function Grid({ gridWidth, currentLayout }) {
return false;
}
handleActivateNonDraggingComponents();
useGridStore.getState().actions.setResizingComponentId(e.target.id);
e.setMin([gridWidth, GRID_HEIGHT]);
showGridLines();
}}
onResizeEnd={(e) => {
try {
@ -945,7 +948,7 @@ export default function Grid({ gridWidth, currentLayout }) {
const isParentModal = isParentNewModal || isParentLegacyModal || isParentModalSlot;
if (isParentModal) {
const modalContainer = e.target.closest('.tj-modal-widget-content');
const modalContainer = e.target.closest('.tj-modal--container');
const mainCanvas = document.getElementById('real-canvas');
const mainRect = mainCanvas.getBoundingClientRect();

View file

@ -192,6 +192,7 @@ const RenderWidget = ({
onComponentClick={onComponentClick}
darkMode={darkMode}
componentName={componentName}
dataCy={`draggable-widget-${componentName}`}
/>
</div>
</OverlayTrigger>

View file

@ -7,6 +7,7 @@ import { keymap } from '@codemirror/view';
import { completionKeymap, acceptCompletion, autocompletion, completionStatus } from '@codemirror/autocomplete';
import { python } from '@codemirror/lang-python';
import { sql } from '@codemirror/lang-sql';
import _ from 'lodash';
import { sass, sassCompletionSource } from '@codemirror/lang-sass';
import { okaidia } from '@uiw/codemirror-theme-okaidia';
import { githubLight } from '@uiw/codemirror-theme-github';
@ -21,6 +22,8 @@ import useStore from '@/AppBuilder/_stores/store';
import { shallow } from 'zustand/shallow';
import { search, searchKeymap, searchPanelOpen } from '@codemirror/search';
import { handleSearchPanel, SearchBtn } from './SearchBox';
import { useQueryPanelKeyHooks } from './useQueryPanelKeyHooks';
import { isInsideParent } from './utils';
const langSupport = Object.freeze({
javascript: javascript(),
@ -51,8 +54,15 @@ const MultiLineCodeEditor = (props) => {
renderCopilot,
} = props;
const replaceIdsWithName = useStore((state) => state.replaceIdsWithName, shallow);
const wrapperRef = useRef(null);
const getSuggestions = useStore((state) => state.getSuggestions, shallow);
const getServerSideGlobalSuggestions = useStore((state) => state.getServerSideGlobalSuggestions, shallow);
const isInsideQueryPane = !!document.querySelector('.code-hinter-wrapper')?.closest('.query-details');
const isInsideQueryManager = useMemo(
() => isInsideParent(wrapperRef?.current, 'query-manager'),
[wrapperRef.current]
);
const context = useContext(CodeHinterContext);
@ -64,6 +74,8 @@ const MultiLineCodeEditor = (props) => {
const [editorView, setEditorView] = React.useState(null);
const { queryPanelKeybindings } = useQueryPanelKeyHooks(onChange, currentValueRef, 'multiline');
const handleOnBlur = () => {
if (!delayOnChange) return onChange(currentValueRef.current);
setTimeout(() => {
@ -85,6 +97,7 @@ const MultiLineCodeEditor = (props) => {
highlightActiveLine: false,
autocompletion: hideSuggestion ?? true,
highlightActiveLineGutter: false,
defaultKeymap: false,
completionKeymap: true,
searchKeymap: false,
};
@ -100,9 +113,16 @@ const MultiLineCodeEditor = (props) => {
const hints = getSuggestions();
const serverHints = getServerSideGlobalSuggestions(isInsideQueryManager);
const allHints = {
...hints,
appHints: [...hints.appHints, ...serverHints],
};
let JSLangHints = [];
if (lang === 'javascript') {
JSLangHints = Object.keys(hints['jsHints'])
JSLangHints = Object.keys(allHints['jsHints'])
.map((key) => {
return hints['jsHints'][key]['methods'].map((hint) => ({
hint: hint,
@ -120,7 +140,7 @@ const MultiLineCodeEditor = (props) => {
});
}
const appHints = hints['appHints'];
const appHints = allHints['appHints'];
let autoSuggestionList = appHints.filter((suggestion) => {
return suggestion.hint.includes(nearestSubstring);
@ -187,7 +207,12 @@ const MultiLineCodeEditor = (props) => {
};
}
const customKeyMaps = [...defaultKeymap, ...completionKeymap, ...searchKeymap];
const customKeyMaps = [
...defaultKeymap.filter((keyBinding) => keyBinding.key !== 'Mod-Enter'), // Remove default keybinding for Mod-Enter
...completionKeymap,
...searchKeymap,
];
const customTabKeymap = keymap.of([
{
key: 'Tab',
@ -208,6 +233,7 @@ const MultiLineCodeEditor = (props) => {
return true;
},
},
...queryPanelKeybindings,
]);
// eslint-disable-next-line react-hooks/exhaustive-deps
@ -229,6 +255,7 @@ const MultiLineCodeEditor = (props) => {
<div
className={`code-hinter-wrapper position-relative ${isInsideQueryPane ? 'code-editor-query-panel' : ''}`}
style={{ width: '100%' }}
ref={wrapperRef}
>
<div className={`${className} ${darkMode && 'cm-codehinter-dark-themed'}`}>
<SearchBtn view={editorView} />

View file

@ -96,6 +96,7 @@ export const PreviewBox = ({
const [largeDataset, setLargeDataset] = useState(false);
const globals = useStore((state) => state.getAllExposedValues().constants || {}, shallow);
const secrets = useStore((state) => state.getSecrets(), shallow);
const globalServerConstantsRegex = /^\{\{.*globals\.server.*\}\}$/;
const getPreviewContent = (content, type) => {
if (content === undefined || content === null) return currentValue;
@ -118,11 +119,11 @@ export const PreviewBox = ({
let previewContent = resolvedValue;
let isGlobalConstant = currentValue && currentValue.includes('{{constants.');
let isSecretConstant = currentValue && currentValue.includes('{{secrets.');
const isServerConstant = currentValue && currentValue.match(globalServerConstantsRegex);
let invalidConstants = null;
let undefinedError = null;
if (isGlobalConstant || isSecretConstant) {
invalidConstants = verifyConstant(currentValue, globals, secrets);
console.log('invalidConstants', invalidConstants);
}
if (invalidConstants?.length) {
undefinedError = { type: 'Invalid constants' };
@ -197,7 +198,11 @@ export const PreviewBox = ({
const errValue = ifCoersionErrorHasCircularDependency(_resolveValue);
setError({
message: isSecretError ? 'secrets cannot be used in apps' : _error,
message: isServerConstant
? 'Server variables cannot be used in apps'
: isSecretError
? 'secrets cannot be used in apps'
: _error,
value: isSecretError
? 'Undefined'
: jsErrorType === 'Invalid'
@ -222,6 +227,7 @@ export const PreviewBox = ({
isWorkspaceVariable={isWorkspaceVariable}
isSecretConstant={isSecretConstant || false}
isLargeDataset={largeDataset}
isServerConstant={isServerConstant}
/>
<CodeHinter.PopupIcon
callback={() => copyToClipboard(error ? error?.value : content)}
@ -240,8 +246,11 @@ const RenderResolvedValue = ({
withValidation,
isWorkspaceVariable,
isSecretConstant = false,
isServerConstant = false,
isLargeDataset,
}) => {
const isServerSideGlobalEnabled = useStore((state) => !!state?.license?.featureAccess?.serverSideGlobal, shallow);
const computeCoersionPreview = (resolvedValue, coersionData) => {
if (coersionData?.typeBeforeCoercion === coersionData?.typeAfterCoercion) return resolvedValue;
@ -264,7 +273,11 @@ const RenderResolvedValue = ({
}`
: previewType;
const previewContent = isSecretConstant
const previewContent = isServerConstant
? isServerSideGlobalEnabled
? 'Server variables would be resolved at runtime'
: 'Server variables are only available in paid plans'
: isSecretConstant
? 'Values of secret constants are hidden'
: !withValidation
? resolvedValue

View file

@ -1,9 +1,9 @@
/* eslint-disable import/no-unresolved */
import React, { useEffect, useRef, useState } from 'react';
import React, { useEffect, useMemo, useRef, useState } from 'react';
import { PreviewBox } from './PreviewBox';
import { ToolTip } from '@/Editor/Inspector/Elements/Components/ToolTip';
import { useTranslation } from 'react-i18next';
import { camelCase, isEmpty, noop } from 'lodash';
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';
@ -12,7 +12,7 @@ import { keymap } from '@codemirror/view';
import FxButton from '../CodeBuilder/Elements/FxButton';
import cx from 'classnames';
import { DynamicFxTypeRenderer } from './DynamicFxTypeRenderer';
import { resolveReferences } from './utils';
import { isInsideParent, resolveReferences } from './utils';
import { okaidia } from '@uiw/codemirror-theme-okaidia';
import { githubLight } from '@uiw/codemirror-theme-github';
import { getAutocompletion } from './autocompleteExtensionConfig';
@ -22,6 +22,7 @@ import CodeHinter from './CodeHinter';
import { removeNestedDoubleCurlyBraces } from '@/_helpers/utils';
import useStore from '@/AppBuilder/_stores/store';
import { shallow } from 'zustand/shallow';
import { useQueryPanelKeyHooks } from './useQueryPanelKeyHooks';
const SingleLineCodeEditor = ({ componentName, fieldMeta = {}, componentId, ...restProps }) => {
const { initialValue, onChange, enablePreview = true, portalProps } = restProps;
@ -161,6 +162,7 @@ const SingleLineCodeEditor = ({ componentName, fieldMeta = {}, componentId, ...r
componentName={componentName}
setShowPreview={setShowPreview}
showPreview={showPreview}
wrapperRef={wrapperRef}
showSuggestions={showSuggestions}
{...restProps}
/>
@ -194,11 +196,27 @@ const EditorInput = ({
previewRef,
setShowPreview,
onInputChange,
wrapperRef,
showSuggestions,
}) => {
const getServerSideGlobalSuggestions = useStore((state) => state.getServerSideGlobalSuggestions, shallow);
const getSuggestions = useStore((state) => state.getSuggestions, shallow);
const { queryPanelKeybindings } = useQueryPanelKeyHooks(onBlurUpdate, currentValue, 'singleline');
const isInsideQueryManager = useMemo(
() => isInsideParent(wrapperRef?.current, 'query-manager'),
[wrapperRef.current]
);
function autoCompleteExtensionConfig(context) {
const hints = getSuggestions();
const serverHints = getServerSideGlobalSuggestions(isInsideQueryManager);
const allHints = {
...hints,
appHints: [...hints.appHints, ...serverHints],
};
let word = context.matchBefore(/\w*/);
const totalReferences = (context.state.doc.toString().match(/{{/g) || []).length;
@ -229,7 +247,7 @@ const EditorInput = ({
queryInput = '{{' + currentWord + '}}';
}
let completions = getAutocompletion(queryInput, validationType, hints, totalReferences, originalQueryInput);
let completions = getAutocompletion(queryInput, validationType, allHints, totalReferences, originalQueryInput);
return {
from: word.from,
@ -239,7 +257,7 @@ const EditorInput = ({
}
// eslint-disable-next-line react-hooks/exhaustive-deps
const overRideFunction = React.useCallback((context) => autoCompleteExtensionConfig(context), []);
const overRideFunction = React.useCallback((context) => autoCompleteExtensionConfig(context), [isInsideQueryManager]);
const autoCompleteConfig = autocompletion({
override: [overRideFunction],
@ -256,7 +274,10 @@ const EditorInput = ({
maxRenderedOptions: 10,
});
const customKeyMaps = [...defaultKeymap, ...completionKeymap];
const customKeyMaps = [
...defaultKeymap.filter((keyBinding) => keyBinding.key !== 'Mod-Enter'), // Remove default keybinding for Mod-Enter
...completionKeymap,
];
const customTabKeymap = keymap.of([
{
key: 'Tab',
@ -278,6 +299,7 @@ const EditorInput = ({
}
},
},
...queryPanelKeybindings,
]);
const handleOnChange = React.useCallback((val) => {
@ -409,11 +431,11 @@ const EditorInput = ({
extensions={
showSuggestions
? [
javascript({ jsx: lang === 'jsx' }),
autoCompleteConfig,
keymap.of([...customKeyMaps]),
customTabKeymap,
]
javascript({ jsx: lang === 'jsx' }),
autoCompleteConfig,
keymap.of([...customKeyMaps]),
customTabKeymap,
]
: [javascript({ jsx: lang === 'jsx' })]
}
onChange={(val) => {
@ -427,7 +449,8 @@ const EditorInput = ({
bracketMatching: true,
foldGutter: false,
highlightActiveLine: false,
autocompletion: showSuggestions,
autocompletion: true,
defaultKeymap: false,
completionKeymap: true,
searchKeymap: false,
}}
@ -485,9 +508,8 @@ const DynamicEditorBridge = (props) => {
<ToolTip
label={t(`widget.commonProperties.${camelCase(paramLabel)}`, paramLabel)}
meta={fieldMeta}
labelClass={`tj-text-xsm color-slate12 ${codeShow ? 'mb-2' : 'mb-0'} ${
darkMode && 'color-whitish-darkmode'
}`}
labelClass={`tj-text-xsm color-slate12 ${codeShow ? 'mb-2' : 'mb-0'} ${darkMode && 'color-whitish-darkmode'
}`}
/>
</div>
)}
@ -495,9 +517,8 @@ const DynamicEditorBridge = (props) => {
<div style={{ marginBottom: codeShow ? '0.5rem' : '0px' }} className={`d-flex align-items-center ${fxClass}`}>
{paramLabel !== 'Type' && isFxNotRequired === undefined && (
<div
className={`col-auto pt-0 fx-common fx-button-container ${
(isEventManagerParam || codeShow) && 'show-fx-button-container'
}`}
className={`col-auto pt-0 fx-common fx-button-container ${(isEventManagerParam || codeShow) && 'show-fx-button-container'
}`}
>
<FxButton
active={codeShow}

View file

@ -220,6 +220,9 @@
.query-hinter{
flex-grow: 1;
}
.cm-editor {
min-height: 150px !important;
}
}
.code-editor-query-panel{
&.show-line-numbers{
@ -398,6 +401,12 @@
}
}
.rest-api-body-codehinter {
.cm-editor {
min-height: 150px !important;
}
}
.border-danger {
.cm-editor {
border: 1px solid red !important;

View file

@ -0,0 +1,58 @@
import { useModuleId } from '@/AppBuilder/_contexts/ModuleContext';
import useStore from '@/AppBuilder/_stores/store';
import { useCallback, useEffect, useState } from 'react';
import { useLocation } from 'react-router-dom';
export const useQueryPanelKeyHooks = (onChange, value, type) => {
const queryPanelHeight = useStore((state) => state.queryPanel.queryPanelHeight);
const runQueryOnShortcut = useStore((state) => state.queryPanel.runQueryOnShortcut);
const previewQueryOnShortcut = useStore((state) => state.queryPanel.previewQueryOnShortcut);
const moduleId = useModuleId();
const location = useLocation();
const { pathname } = location;
const [queryPanelKeybindings, setQueryPanelKeybindings] = useState([]);
const handleRunQuery = useCallback(
(view) => {
const isEditor = pathname.includes('/apps/');
if (queryPanelHeight !== 0 && isEditor) {
onChange(type === 'multiline' ? value.current : value);
runQueryOnShortcut();
}
return true;
},
[queryPanelHeight, onChange, runQueryOnShortcut, value]
);
const handlePreviewQuery = useCallback(
(view) => {
const isEditor = pathname.includes('/apps/');
if (queryPanelHeight !== 0 && isEditor) {
onChange(type === 'multiline' ? value.current : value);
previewQueryOnShortcut(moduleId);
}
return true;
},
[queryPanelHeight, moduleId, onChange, previewQueryOnShortcut, value]
);
useEffect(() => {
setQueryPanelKeybindings([
{
key: 'Mod-Enter',
preventDefault: true,
run: handleRunQuery,
},
{
key: 'Mod-Shift-Enter',
preventDefault: true,
run: handlePreviewQuery,
},
]);
}, [handleRunQuery, handlePreviewQuery]);
return {
queryPanelKeybindings,
};
};

View file

@ -30,6 +30,17 @@ function traverseAST(node, callback) {
}
}
export const isInsideParent = (element, className) => {
while (element) {
if (element.classList?.contains(className)) {
console.log('element.classList', element.classList);
return true;
}
element = element.parentElement;
}
return false;
};
function getMethods(type) {
const arrayMethods = Object.getOwnPropertyNames(Array.prototype).filter(
(p) => typeof Array.prototype[p] === 'function'

View file

@ -381,7 +381,6 @@ export const BaseQueryManagerBody = ({ darkMode, activeTab, renderCopilot = () =
{activeTab === 1 && renderQueryElement()}
{activeTab === 2 && renderTransformation()}
{activeTab === 3 && renderQueryOptions()}
<div className="pb-5" />
<Preview darkMode={darkMode} calculatePreviewHeight={calculatePreviewHeight} />
</>
)}

View file

@ -1,18 +1,17 @@
import React, { useState, forwardRef, useRef, useEffect, useCallback } from 'react';
import RenameIcon from '../Icons/RenameIcon';
import Eye1 from '@/_ui/Icon/solidIcons/Eye1';
import Play from '@/_ui/Icon/solidIcons/Play';
import cx from 'classnames';
import { toast } from 'react-hot-toast';
import { useTranslation } from 'react-i18next';
import { DATA_SOURCE_TYPE } from '@/_helpers/constants';
import { shallow } from 'zustand/shallow';
import { Tooltip } from 'react-tooltip';
import { ToolTip } from '@/_components';
import { Button } from 'react-bootstrap';
import { decodeEntities } from '@/_helpers/utils';
import { canDeleteDataSource, canReadDataSource, canUpdateDataSource } from '@/_helpers';
import useStore from '@/AppBuilder/_stores/store';
import { useModuleId } from '@/AppBuilder/_contexts/ModuleContext';
import { Button as ButtonComponent } from '@/components/ui/Button/Button';
import { debounce } from 'lodash';
export const QueryManagerHeader = forwardRef(({ darkMode, setActiveTab, activeTab }, ref) => {
@ -27,6 +26,7 @@ export const QueryManagerHeader = forwardRef(({ darkMode, setActiveTab, activeTa
const setShowCreateQuery = useStore((state) => state.queryPanel.setShowCreateQuery);
const queryName = selectedQuery?.name ?? '';
const shouldFreeze = useStore((state) => state.getShouldFreeze());
useEffect(() => {
if (selectedQuery?.name) {
setShowCreateQuery(false);
@ -244,34 +244,26 @@ const RunButton = ({ buttonLoadingState }) => {
const isLoading = useStore(
(state) => state.resolvedStore.modules.canvas.exposedValues.queries[selectedQuery?.id]?.isLoading ?? false
);
const isMac = typeof navigator !== 'undefined' && navigator?.userAgent?.toLowerCase().includes('mac');
const shortcutDisplay = isMac ? 'Run query ⌘↩' : 'Run query Ctrl+Enter';
return (
<span
{...(isInDraft && {
'data-tooltip-id': 'query-header-btn-run',
'data-tooltip-content': 'Connect a data source to run',
})}
>
<button
onClick={() => runQuery(selectedQuery?.id, selectedQuery?.name, undefined, 'edit', {}, true)}
className={`border-0 default-secondary-button ${buttonLoadingState(isLoading)}`}
data-cy="query-run-button"
disabled={isInDraft}
{...(isInDraft && {
'data-tooltip-id': 'query-header-btn-run',
'data-tooltip-content': 'Publish the query to run',
})}
>
<span
className={cx({
invisible: isLoading,
})}
<span>
<ToolTip message={shortcutDisplay} placement="bottom" trigger={['hover']} show={true} tooltipClassName="">
<ButtonComponent
size="medium"
variant="secondary"
onClick={() => runQuery(selectedQuery?.id, selectedQuery?.name, undefined, 'edit', {}, true)}
leadingIcon="play01"
disabled={isInDraft}
isLoading={isLoading}
className={isMac ? '!tw-w-[88px]' : '!tw-w-[120px]'}
data-cy="query-run-button"
>
<Play width={14} fill="var(--indigo9)" viewBox="0 0 14 14" />
</span>
<span className="query-manager-btn-name">{isLoading ? ' ' : 'Run'}</span>
</button>
{isInDraft && <Tooltip id="query-header-btn-run" className="tooltip" />}
Run
<span className="query-manager-btn-shortcut">{isMac ? '⌘↩' : 'Ctrl+Enter'}</span>
</ButtonComponent>
</ToolTip>
</span>
);
};
@ -287,20 +279,22 @@ const PreviewButton = ({ buttonLoadingState, onClick }) => {
: true;
const isPreviewQueryLoading = useStore((state) => state.queryPanel.isPreviewQueryLoading);
const { t } = useTranslation();
const isMac = typeof navigator !== 'undefined' && navigator?.userAgent?.toLowerCase().includes('mac');
const shortcutDisplay = `Preview query ${isMac ? '⌘⇧↩' : 'Ctrl+Shift+Enter'}`;
return (
<button
disabled={!hasPermissions}
onClick={onClick}
className={cx(`default-tertiary-button ${buttonLoadingState(isPreviewQueryLoading)} `, {
disabled: !hasPermissions,
})}
data-cy={'query-preview-button'}
>
<span className="query-preview-svg d-flex align-items-center query-icon-wrapper">
<Eye1 width={14} fill="var(--slate9)" />
</span>
<span>{t('editor.queryManager.preview', 'Preview')}</span>
</button>
<ToolTip message={shortcutDisplay} placement="bottom" trigger={['hover']} show={true} tooltipClassName="">
<ButtonComponent
size="medium"
variant="outline"
onClick={onClick}
// className="!tw-w-[100px]"
disabled={!hasPermissions}
isLoading={isPreviewQueryLoading}
data-cy={'query-preview-button'}
>
Preview
</ButtonComponent>
</ToolTip>
);
};

View file

@ -204,7 +204,7 @@ export const Transformation = ({ changeOption, options, darkMode, queryId, rende
<br />
<div className={`d-flex copilot-codehinter-wrap ${!enableTransformation && 'read-only-codehinter'}`}>
<div className="col flex-grow-1">
<div style={{ borderRadius: '6px', marginBottom: '20px', background: darkMode ? '#272822' : '#F8F9FA' }}>
<div style={{ borderRadius: '6px', background: darkMode ? '#272822' : '#F8F9FA' }}>
<div className="py-3 px-3 d-flex justify-content-between copilot-section-header">
<Tab.Container
activeKey={lang}

View file

@ -68,7 +68,7 @@ export default ({
);
})}
{bodyToggle && (
<div>
<div className="rest-api-body-codehinter">
<CodeHinter
type="extendedSingleLine"
initialValue={(rawBody || jsonBody) ?? ''} // If raw_body is not set, set initial value to legacy json_body if present

View file

@ -13,7 +13,7 @@ const Runjs = (props) => {
}, [props.options]);
return (
<Card className="runjs-editor mb-3">
<Card className="runjs-editor mb-3 !tw-mb-0">
<CodeHinter
renderCopilot={props.renderCopilot}
type="multiline"

View file

@ -16,7 +16,7 @@ export class Runpy extends React.Component {
render() {
return (
<div className="runps-editor mb-3">
<div className="runps-editor mb-3 !tw-mb-0">
<CodeHinter
renderCopilot={this.props.renderCopilot}
type="multiline"

View file

@ -0,0 +1,26 @@
import React from 'react';
import useStore from '@/AppBuilder/_stores/store';
import { useHotkeys } from 'react-hotkeys-hook';
import { useModuleId } from '@/AppBuilder/_contexts/ModuleContext';
const QueryKeyHooks = ({ children, isExpanded }) => {
const runQueryOnShortcut = useStore((state) => state.queryPanel.runQueryOnShortcut);
const previewQueryOnShortcut = useStore((state) => state.queryPanel.previewQueryOnShortcut);
const moduleId = useModuleId();
useHotkeys(
['mod+enter', 'mod+shift+enter'],
(event, handler) => {
if (handler.mod && handler.keys[0] === 'enter') {
if (handler.shift) {
previewQueryOnShortcut(moduleId);
} else runQueryOnShortcut();
}
},
{ enabled: isExpanded, enableOnFormTags: ['input'] }
);
return <div className="row main-row">{children}</div>;
};
export default QueryKeyHooks;

View file

@ -11,6 +11,7 @@ import useStore from '@/AppBuilder/_stores/store';
import SectionCollapse from '@/_ui/Icon/solidIcons/SectionCollapse';
import SectionExpand from '@/_ui/Icon/solidIcons/SectionExpand';
import { shallow } from 'zustand/shallow';
import QueryKeyHooks from './QueryKeyHooks';
const MemoizedQueryDataPane = memo(QueryDataPane);
const MemoizedQueryManager = memo(QueryManager);
@ -193,14 +194,14 @@ export const QueryPanel = ({ darkMode }) => {
}}
>
{isExpanded && (
<div className="row main-row">
<QueryKeyHooks isExpanded={isExpanded}>
<MemoizedQueryDataPane darkMode={darkMode} />
<div className="query-definition-pane-wrapper">
<div className="query-definition-pane">
<MemoizedQueryManager darkMode={darkMode} />
</div>
</div>
</div>
</QueryKeyHooks>
)}
</div>
<Tooltip id="tooltip-for-query-panel-footer-btn" className="tooltip" />

View file

@ -40,6 +40,7 @@ export const Form = ({
const { id } = component;
const newOptions = [{ name: 'None', value: 'none' }];
Object.entries(allComponents).forEach(([componentId, _component]) => {
const validParent =
_component.component.parent === id ||
@ -52,6 +53,19 @@ export const Form = ({
tempComponentMeta.properties.buttonToSubmit.options = newOptions;
// Hide header footer if custom schema is turned on
if (component.component.definition.properties.advanced.value === '{{true}}') {
component.component.properties.showHeader = {
...component.component.properties.headerHeight,
isHidden: true,
};
component.component.properties.showFooter = {
...component.component.properties.headerHeight,
isHidden: true,
};
}
const accordionItems = baseComponentProperties(
properties,
events,

View file

@ -131,6 +131,19 @@ export const buttonGroupConfig = {
defaultValue: 'left',
},
},
padding: {
type: 'switch',
displayName: 'Padding',
validation: {
schema: { type: 'union', schemas: [{ type: 'string' }, { type: 'number' }] },
defaultValue: 'default',
},
isFxNotRequired: true,
options: [
{ displayName: 'Default', value: 'default' },
{ displayName: 'None', value: 'none' },
],
},
},
exposedVariables: {
selected: [1],
@ -162,6 +175,7 @@ export const buttonGroupConfig = {
borderRadius: { value: '{{4}}' },
disabledState: { value: '{{false}}' },
selectedTextColor: { value: '#FFFFFF' },
padding: { value: 'default' },
selectedBackgroundColor: { value: 'var(--primary-brand)' },
alignment: { value: 'left' },
},

View file

@ -126,6 +126,20 @@ export const checkboxConfig = {
],
accordian: 'label',
},
padding: {
type: 'switch',
displayName: 'Padding',
validation: {
schema: { type: 'union', schemas: [{ type: 'string' }, { type: 'number' }] },
defaultValue: 'default',
},
isFxNotRequired: true,
options: [
{ displayName: 'Default', value: 'default' },
{ displayName: 'None', value: 'none' },
],
accordian: 'switch',
},
},
exposedVariables: {
value: false,
@ -189,6 +203,7 @@ export const checkboxConfig = {
handleColor: { value: '#FFFFFF' },
alignment: { value: 'right' },
boxShadow: { value: '0px 0px 0px 0px #00000090' },
padding: { value: 'default' },
},
validation: {
mandatory: { value: '{{false}}' },

View file

@ -28,6 +28,19 @@ export const colorPickerConfig = {
},
styles: {
visibility: { type: 'toggle', displayName: 'Visibility' },
padding: {
type: 'switch',
displayName: 'Padding',
validation: {
schema: { type: 'union', schemas: [{ type: 'string' }, { type: 'number' }] },
defaultValue: 'default',
},
isFxNotRequired: true,
options: [
{ displayName: 'Default', value: 'default' },
{ displayName: 'None', value: 'none' },
],
},
},
exposedVariables: {
selectedColorHex: '#000000',
@ -47,6 +60,7 @@ export const colorPickerConfig = {
events: [],
styles: {
visibility: { value: '{{true}}' },
padding: { value: 'default' },
},
},
};

View file

@ -75,6 +75,18 @@ export const dropdownV2Config = {
accordian: 'Options',
isFxNotRequired: true,
},
showClearBtn: {
type: 'toggle',
displayName: 'Show clear selection button',
validation: { schema: { type: 'boolean' }, defaultValue: true },
section: 'additionalActions',
},
showSearchInput: {
type: 'toggle',
displayName: 'Show search in options',
validation: { schema: { type: 'boolean' }, defaultValue: true },
section: 'additionalActions',
},
loadingState: {
type: 'toggle',
displayName: 'Loading state',
@ -314,6 +326,8 @@ export const dropdownV2Config = {
optionsLoadingState: { value: '{{false}}' },
sort: { value: 'asc' },
placeholder: { value: 'Select an option' },
showClearBtn: { value: '{{true}}' },
showSearchInput: { value: '{{true}}' },
visibility: { value: '{{true}}' },
disabledState: { value: '{{false}}' },
loadingState: { value: '{{false}}' },

View file

@ -78,6 +78,29 @@ export const iconConfig = {
},
accordian: 'Icon',
},
padding: {
type: 'switch',
displayName: 'Padding',
validation: {
schema: { type: 'union', schemas: [{ type: 'string' }, { type: 'number' }] },
defaultValue: 'default',
},
isFxNotRequired: true,
options: [
{ displayName: 'Default', value: 'default' },
{ displayName: 'None', value: 'none' },
],
accordian: 'Icon',
},
boxShadow: {
type: 'boxShadow',
displayName: 'Box shadow',
validation: {
schema: { type: 'union', schemas: [{ type: 'string' }, { type: 'number' }] },
defaultValue: '0px 0px 0px 0px #00000040',
},
accordian: 'Icon',
},
},
exposedVariables: {},
actions: [
@ -116,6 +139,8 @@ export const iconConfig = {
styles: {
iconColor: { value: '#000' },
iconAlign: { value: 'center' },
padding: { value: 'default' },
boxShadow: { value: '0px 0px 0px 0px #00000040' },
},
},
};

View file

@ -5,7 +5,7 @@ export const modalV2Config = {
component: 'ModalV2',
defaultSize: {
width: 10,
height: 34,
height: 40,
},
others: {
showOnDesktop: { type: 'toggle', displayName: 'Show on desktop' },
@ -137,7 +137,7 @@ export const modalV2Config = {
layout: {
top: 24,
left: 22,
height: 36,
height: 40,
},
displayName: 'ModalFooterCancel',
properties: ['text'],
@ -154,7 +154,7 @@ export const modalV2Config = {
layout: {
top: 24,
left: 32,
height: 36,
height: 40,
},
displayName: 'ModalFooterConfirm',
properties: ['text'],

View file

@ -142,6 +142,18 @@ export const multiselectV2Config = {
accordian: 'Options',
isFxNotRequired: true,
},
showClearBtn: {
type: 'toggle',
displayName: 'Show clear selection button',
validation: { schema: { type: 'boolean' }, defaultValue: true },
section: 'additionalActions',
},
showSearchInput: {
type: 'toggle',
displayName: 'Show search in options',
validation: { schema: { type: 'boolean' }, defaultValue: true },
section: 'additionalActions',
},
loadingState: {
type: 'toggle',
displayName: 'Loading state',
@ -327,6 +339,8 @@ export const multiselectV2Config = {
optionsLoadingState: { value: '{{false}}' },
sort: { value: 'asc' },
placeholder: { value: 'Select the options' },
showClearBtn: { value: '{{true}}' },
showSearchInput: { value: '{{true}}' },
visibility: { value: '{{true}}' },
disabledState: { value: '{{false}}' },
loadingState: { value: '{{false}}' },

View file

@ -84,6 +84,19 @@ export const rangeSliderConfig = {
defaultValue: true,
},
},
padding: {
type: 'switch',
displayName: 'Padding',
validation: {
schema: { type: 'union', schemas: [{ type: 'string' }, { type: 'number' }] },
defaultValue: 'default',
},
isFxNotRequired: true,
options: [
{ displayName: 'Default', value: 'default' },
{ displayName: 'None', value: 'none' },
],
},
},
exposedVariables: {
value: null,
@ -111,6 +124,7 @@ export const rangeSliderConfig = {
handleColor: { value: '' },
trackColor: { value: 'var(--primary-brand)' },
visibility: { value: '{{true}}' },
padding: { value: 'default' },
},
},
};

View file

@ -89,6 +89,19 @@ export const starratingConfig = {
defaultValue: false,
},
},
padding: {
type: 'switch',
displayName: 'Padding',
validation: {
schema: { type: 'union', schemas: [{ type: 'string' }, { type: 'number' }] },
defaultValue: 'default',
},
isFxNotRequired: true,
options: [
{ displayName: 'Default', value: 'default' },
{ displayName: 'None', value: 'none' },
],
},
},
exposedVariables: {
value: 0,
@ -112,6 +125,7 @@ export const starratingConfig = {
labelColor: { value: '' },
visibility: { value: '{{true}}' },
disabledState: { value: '{{false}}' },
padding: { value: 'default' },
},
},
};

View file

@ -38,6 +38,19 @@ export const tagsConfig = {
defaultValue: true,
},
},
padding: {
type: 'switch',
displayName: 'Padding',
validation: {
schema: { type: 'union', schemas: [{ type: 'string' }, { type: 'number' }] },
defaultValue: 'default',
},
isFxNotRequired: true,
options: [
{ displayName: 'Default', value: 'default' },
{ displayName: 'None', value: 'none' },
],
},
alignment: {
type: 'alignButtons',
displayName: 'Alignment',
@ -62,6 +75,7 @@ export const tagsConfig = {
events: [],
styles: {
visibility: { value: '{{true}}' },
padding: { value: 'default' },
alignment: { value: 'left' },
},
},

View file

@ -126,6 +126,20 @@ export const toggleSwitchV2Config = {
validation: { schema: { type: 'union', schemas: [{ type: 'string' }, { type: 'number' }] } },
accordian: 'switch',
},
padding: {
type: 'switch',
displayName: 'Padding',
validation: {
schema: { type: 'union', schemas: [{ type: 'string' }, { type: 'number' }] },
defaultValue: 'default',
},
isFxNotRequired: true,
options: [
{ displayName: 'Default', value: 'default' },
{ displayName: 'None', value: 'none' },
],
accordian: 'switch',
},
},
exposedVariables: {
value: false,
@ -187,6 +201,7 @@ export const toggleSwitchV2Config = {
handleColor: { value: '#FFFFFF' },
alignment: { value: 'right' },
boxShadow: { value: '0px 0px 0px 0px #00000090' },
padding: { value: 'default' },
},
},
};

View file

@ -299,7 +299,7 @@ export const Form = function Form(props) {
if (e.target.className === 'real-canvas') onComponentClick(id, component);
}} //Hack, should find a better solution - to prevent losing z index+1 when container element is clicked
>
{showHeader && (
{!advanced && showHeader && (
<div style={formHeader} className="wj-form-header">
<SubContainer
id={`${id}-header`}
@ -381,7 +381,7 @@ export const Form = function Form(props) {
</fieldset>
)}
</div>
{showFooter && (
{!advanced && showFooter && (
<div className="jet-form-footer wj-form-footer" style={formFooter}>
<SubContainer
id={`${id}-footer`}

View file

@ -38,3 +38,16 @@
box-sizing: content-box;
padding: 4px 0;
}
.jet-container.jet-container-json-form {
padding: 0px;
.wj-form-header::after,
.wj-form-footer::after {
left: -3px;
right: -3px;
}
.jet-form-body fieldset {
padding: 20px;
}
}

View file

@ -277,7 +277,7 @@ export const Modal = function Modal({
<Modal.Component
show={showModal}
contentClassName="modal-component"
contentClassName="modal-component tj-modal--container"
container={document.getElementsByClassName('real-canvas')[0]}
size={size}
keyboard={true}

View file

@ -56,7 +56,7 @@ export const ModalWidget = ({ ...restProps }) => {
return (
<BootstrapModal
{...restProps}
contentClassName="modal-component tj-modal-widget-content"
contentClassName="modal-component tj-modal--container tj-modal-widget-content"
animation={true}
onEscapeKeyDown={(e) => {
e.preventDefault();

View file

@ -117,7 +117,7 @@ export const Header = memo(
</div>
</div>
{showFilter && (
<Filter table={table} darkMode={darkMode} setFilters={setFilters} setShowFilter={setShowFilter} />
<Filter id={id} table={table} darkMode={darkMode} setFilters={setFilters} setShowFilter={setShowFilter} />
)}
</>
);

View file

@ -6,7 +6,7 @@ import { FilterFooter } from './FilterFooter';
import { FilterHeader } from './FilterHeader';
import { debounce, isEqual } from 'lodash';
export const Filter = memo(({ table, darkMode, setFilters, setShowFilter }) => {
export const Filter = memo(({ id, table, darkMode, setFilters, setShowFilter }) => {
const { t } = useTranslation();
const [localFilters, setLocalFilters] = useState(table.getState().columnFilters);
@ -142,6 +142,7 @@ export const Filter = memo(({ table, darkMode, setFilters, setShowFilter }) => {
<div className="card-body">
{localFilters.map((filter, index) => (
<FilterRow
id={id}
key={index}
filter={filter}
index={index}

View file

@ -1,12 +1,15 @@
import React, { memo } from 'react';
import { useTranslation } from 'react-i18next';
import Select from '@/_ui/Select';
import { components } from 'react-select';
import defaultStyles from '@/_ui/Select/styles';
import { FILTER_OPTIONS } from './filterConstants';
import useStore from '@/AppBuilder/_stores/store';
export const FilterRow = memo(
({ filter, index, columns, darkMode, onColumnChange, onOperationChange, onValueChange, onRemove }) => {
({ id, filter, index, columns, darkMode, onColumnChange, onOperationChange, onValueChange, onRemove }) => {
const { t } = useTranslation();
const isDragging = useStore((state) => state.draggingComponentId === id);
const selectStyles = (width) => {
return {
@ -15,6 +18,10 @@ export const FilterRow = memo(
menuList: (base) => ({
...base,
}),
menu: (base) => ({
...base,
display: isDragging ? 'none' : 'block',
}),
};
};
@ -29,11 +36,13 @@ export const FilterRow = memo(
value={filter.id}
search={true}
onChange={(value) => onColumnChange(index, value)}
components={{ ValueContainer: CustomValueContainer }}
placeholder={t('globals.select', 'Select') + '...'}
className={`${darkMode ? 'select-search-dark' : 'select-search'} mb-0`}
styles={selectStyles('100%')}
useCustomStyles={true}
darkMode={darkMode}
openMenuOnFocus={true}
/>
</div>
<div data-cy={`select-operation-dropdown-${index}`} className="col" style={{ maxWidth: '180px' }}>
@ -42,11 +51,13 @@ export const FilterRow = memo(
value={filter.value.condition}
search={true}
onChange={(value) => onOperationChange(index, value)}
components={{ ValueContainer: CustomValueContainer }}
className={`${darkMode ? 'select-search-dark' : 'select-search'}`}
placeholder={t('globals.select', 'Select') + '...'}
styles={selectStyles('100%')}
useCustomStyles={true}
darkMode={darkMode}
openMenuOnFocus={true}
/>
</div>
<div className="col">
@ -74,3 +85,15 @@ export const FilterRow = memo(
);
}
);
const CustomValueContainer = (props) => {
const handleClick = (e) => {
if (props.selectProps?.selectRef?.current) {
props.selectProps.selectRef.current.focus();
}
if (props.innerProps?.onMouseDown) {
props.innerProps.onMouseDown(e);
}
};
return <components.ValueContainer {...props} innerProps={{ ...props.innerProps, onMouseDown: handleClick }} />;
};

View file

@ -36,4 +36,27 @@ export const createCodeHinterSlice = (set, get) => ({
setSuggestions({ appHints: suggestionList, jsHints: jsHints });
},
getSuggestions: () => get().suggestions,
getServerSideGlobalSuggestions: (isInsideQueryManager) => {
const isServerSideGlobalEnabled = !!get()?.license?.featureAccess?.serverSideGlobal;
const serverHints = [];
const hints = get().getSuggestions();
if (isInsideQueryManager && isServerSideGlobalEnabled) {
serverHints.push({ hint: 'globals.server', type: 'Object' });
hints?.appHints?.forEach((appHint) => {
if (appHint?.hint?.startsWith('globals.currentUser')) {
const key = appHint?.hint?.replace('globals.currentUser', 'globals.server.currentUser');
console.log({
hint: key,
type: appHint?.type,
});
serverHints.push({
hint: key,
type: appHint?.type,
});
}
});
}
return serverHints;
},
});

View file

@ -1092,5 +1092,23 @@ export const createQueryPanelSlice = (set, get) => ({
isQuerySelected: (queryId) => {
return get().queryPanel.selectedQuery?.id === queryId;
},
runQueryOnShortcut: () => {
const { queryPanel } = get();
const { runQuery, selectedQuery } = queryPanel;
runQuery(selectedQuery?.id, selectedQuery?.name, undefined, 'edit', {}, true);
},
previewQueryOnShortcut: (moduleId = 'canvas') => {
const { queryPanel } = get();
const { previewQuery, selectedQuery, selectedDataSource } = queryPanel;
const query = {
data_source_id: selectedDataSource.id === 'null' ? null : selectedDataSource.id,
pluginId: selectedDataSource.pluginId,
options: { ...selectedQuery?.options },
kind: selectedDataSource.kind,
name: selectedQuery?.name ?? '',
id: selectedQuery?.id,
};
previewQuery(query, false, undefined, moduleId);
},
},
});

View file

@ -161,8 +161,8 @@ export const ColorPicker = function ({
: { display: 'none' };
return (
<div>
<div style={{ baseStyle, boxShadow }} className="form-control" data-cy={dataCy}>
<div className="h-100">
<div style={{ baseStyle, boxShadow, height: '100%' }} className="form-control" data-cy={dataCy}>
<div
className="d-flex h-100 justify-content-between align-items-center"
onClick={() => setShowColorPicker(true)}

View file

@ -1,87 +1,118 @@
import React from 'react';
import React, { useEffect, useRef } from 'react';
import { components } from 'react-select';
import SolidIcon from '@/_ui/Icon/SolidIcons';
import Loader from '@/ToolJetUI/Loader/Loader';
import './dropdownV2.scss';
import { FormCheck } from 'react-bootstrap';
// eslint-disable-next-line import/no-unresolved
import { useVirtualizer } from '@tanstack/react-virtual';
import cx from 'classnames';
const { MenuList } = components;
// This Menulist also used in MultiselectV2
const CustomMenuList = ({ selectProps, ...props }) => {
const {
onInputChange,
onMenuInputFocus,
showAllOption,
isSelectAllSelected,
optionsLoadingState,
darkMode,
setSelected,
setIsSelectAllSelected,
fireEvent,
inputValue,
menuId,
} = selectProps;
const { onInputChange, onMenuInputFocus, optionsLoadingState, darkMode, inputValue, menuId, showSearchInput } =
selectProps;
const handleSelectAll = (e) => {
e.target.checked && fireEvent();
if (e.target.checked) {
setSelected(props.options);
} else {
setSelected([]);
const parentRef = useRef(null);
const virtualizer = useVirtualizer({
count: props?.children?.length || 0,
getScrollElement: () => parentRef.current,
estimateSize: () => 40,
overscan: 15,
});
useEffect(() => {
const searchInput = document.querySelector('.dropdown-multiselect-widget-search-box');
if (searchInput) {
searchInput.focus();
}
setIsSelectAllSelected(e.target.checked);
};
}, []);
return (
<div
id={`dropdown-multiselect-widget-custom-menu-list-${menuId}`}
className={cx('dropdown-multiselect-widget-custom-menu-list', { 'theme-dark dark-theme': darkMode })}
onClick={(e) => e.stopPropagation()}
onTouchEnd={(e) => e.stopPropagation()}
>
<div className="dropdown-multiselect-widget-search-box-wrapper">
<span>
<SolidIcon name="search01" width="14" />
</span>
<input
autoCorrect="off"
autoComplete="off"
spellCheck="false"
type="text"
value={inputValue}
onChange={(e) =>
onInputChange(e.currentTarget.value, {
action: 'input-change',
})
}
onMouseDown={(e) => {
e.stopPropagation();
e.target.focus();
}}
onTouchEnd={(e) => {
e.stopPropagation();
e.target.focus();
}}
onFocus={onMenuInputFocus}
placeholder="Search"
className="dropdown-multiselect-widget-search-box"
/>
</div>
{showAllOption && !optionsLoadingState && (
<label htmlFor="select-all-checkbox" className="multiselect-custom-menulist-select-all">
<FormCheck id="select-all-checkbox" checked={isSelectAllSelected} onChange={handleSelectAll} />
<span style={{ marginLeft: '4px' }}>Select all</span>
</label>
{showSearchInput && (
<div className="dropdown-multiselect-widget-search-box-wrapper">
<span>
<SolidIcon name="search01" width="14" />
</span>
<input
autoCorrect="off"
autoComplete="off"
spellCheck="false"
type="text"
value={inputValue}
onChange={(e) =>
onInputChange(e.currentTarget.value, {
action: 'input-change',
})
}
onMouseDown={(e) => {
e.stopPropagation();
e.target.focus();
}}
onTouchEnd={(e) => {
e.stopPropagation();
e.target.focus();
}}
onFocus={onMenuInputFocus}
placeholder="Search"
className="dropdown-multiselect-widget-search-box"
/>
</div>
)}
<MenuList {...props} selectProps={selectProps}>
{optionsLoadingState ? (
<div class="text-center py-4" style={{ minHeight: '188px' }}>
<Loader style={{ zIndex: 3, position: 'absolute' }} width="36" />
{!optionsLoadingState && (
<div
ref={parentRef}
className="dropdown-multiselect-widget-custom-menu-list-body"
style={{
maxHeight: selectProps.maxMenuHeight || 300,
}}
>
<div
style={{
height: `${virtualizer.getTotalSize() || 38}px`,
position: 'relative',
marginTop: '5px',
}}
>
{!virtualizer.getTotalSize() && props.children}
{virtualizer.getVirtualItems().map((virtualItem) => {
const option = props.options[virtualItem.index];
const child = props.children[virtualItem.index];
const isSelectAll = option?.value === 'multiselect-custom-menulist-select-all';
return (
<div
key={option.value}
style={{
position: isSelectAll ? 'sticky' : 'absolute',
width: '100%',
top: 0,
zIndex: isSelectAll && 10,
transform: `translateY(${virtualItem.start}px)`,
}}
data-index={virtualItem.index}
ref={virtualizer.measureElement}
>
<MenuList {...props} selectProps={selectProps}>
<div>{child}</div>
</MenuList>
</div>
);
})}
</div>
) : (
props.children
)}
</MenuList>
</div>
)}
{optionsLoadingState && (
<div className="text-center py-4" style={{ minHeight: '188px' }}>
<Loader style={{ zIndex: 3, position: 'absolute' }} width="36" />
</div>
)}
</div>
);
};

View file

@ -6,7 +6,17 @@ import { highlightText } from './utils';
const CustomOption = (props) => {
return (
<components.Option {...props}>
<components.Option
{...props}
innerProps={{
...props.innerProps,
onTouchEnd: (e) => {
e.preventDefault();
e.stopPropagation();
props.selectOption(props.data);
},
}}
>
<div className="cursor-pointer">
{props.isSelected && (
<span style={{ maxHeight: '20px', marginRight: '8px', marginLeft: '-28px' }}>

View file

@ -15,7 +15,6 @@ import Label from '@/_ui/Label';
import cx from 'classnames';
import { getInputBackgroundColor, getInputBorderColor, getInputFocusedColor, sortArray } from './utils';
import { isMobileDevice } from '@/_helpers/appUtils';
import useStore from '@/AppBuilder/_stores/store';
const { DropdownIndicator, ClearIndicator } = components;
const INDICATOR_CONTAINER_WIDTH = 60;
@ -69,6 +68,8 @@ export const DropdownV2 = ({
disabledState,
optionsLoadingState,
sort,
showClearBtn,
showSearchInput,
} = properties;
const {
selectedTextColor,
@ -104,8 +105,6 @@ export const DropdownV2 = ({
const [isDropdownDisabled, setIsDropdownDisabled] = useState(disabledState);
const [searchInputValue, setSearchInputValue] = useState('');
const [userInteracted, setUserInteracted] = useState(false);
const currentMode = useStore((state) => state.currentMode);
const isEditor = currentMode === 'edit';
const _height = padding === 'default' ? `${height}px` : `${height + 4}px`;
const labelRef = useRef();
@ -173,12 +172,43 @@ export const DropdownV2 = ({
setExposedVariable('isValid', validationStatus?.isValid);
};
const handleClickInEditor = (e) => {
if (e.target.className.includes('clear-indicator') || isMenuOpen) return;
e.stopPropagation();
selectRef.current?.onControlMouseDown(e);
const handleClickInsideSelect = () => {
if (isDropdownDisabled || isDropdownLoading) return;
if (isMenuOpen) {
setIsMenuOpen(false);
fireEvent('onBlur');
setSearchInputValue('');
} else {
setIsMenuOpen(true);
fireEvent('onFocus');
if (!showSearchInput) {
selectRef.current.focus();
}
}
};
const handleClickOutsideSelect = (event) => {
const menu = document.querySelector(`._tooljet-${componentName}`);
if (
isMenuOpen &&
menu &&
dropdownRef.current &&
!dropdownRef.current.contains(event.target) &&
!menu.contains(event.target)
) {
setIsMenuOpen(false);
fireEvent('onBlur');
setSearchInputValue('');
}
};
useEffect(() => {
document.addEventListener('mousedown', handleClickOutsideSelect);
return () => {
document.removeEventListener('mousedown', handleClickOutsideSelect);
};
}, [isMenuOpen, componentName]);
useEffect(() => {
setInputValue(findDefaultItem(advanced ? schema : options));
// eslint-disable-next-line react-hooks/exhaustive-deps
@ -331,8 +361,8 @@ export const DropdownV2 = ({
selectedTextColor !== '#1B1F24'
? selectedTextColor
: isDropdownDisabled || isDropdownLoading
? 'var(--text-disabled)'
: 'var(--text-primary)',
? 'var(--text-disabled)'
: 'var(--text-primary)',
maxWidth:
ref?.current?.offsetWidth -
(iconVisibility ? INDICATOR_CONTAINER_WIDTH + ICON_WIDTH : INDICATOR_CONTAINER_WIDTH),
@ -373,8 +403,8 @@ export const DropdownV2 = ({
selectedTextColor !== '#1B1F24'
? selectedTextColor
: isDropdownDisabled || isDropdownLoading
? 'var(--text-disabled)'
: 'var(--text-primary)',
? 'var(--text-disabled)'
: 'var(--text-primary)',
borderRadius: _state.isFocused && '8px',
padding: '8px 6px 8px 38px',
'&:hover': {
@ -386,7 +416,7 @@ export const DropdownV2 = ({
}),
menuList: (provided) => ({
...provided,
padding: '8px',
padding: '0 8px',
borderRadius: '8px',
// this is needed otherwise :active state doesn't look nice, gap is required
display: 'flex',
@ -410,8 +440,8 @@ export const DropdownV2 = ({
data-cy={`label-${String(componentName).toLowerCase()} `}
className={cx('dropdown-widget', 'd-flex', {
[alignment === 'top' &&
((labelWidth != 0 && label?.length != 0) ||
(labelAutoWidth && labelWidth == 0 && label && label?.length != 0))
((labelWidth != 0 && label?.length != 0) ||
(labelAutoWidth && labelWidth == 0 && label && label?.length != 0))
? 'flex-column'
: 'align-items-center']: true,
'flex-row-reverse': direction === 'right' && alignment === 'side',
@ -446,7 +476,8 @@ export const DropdownV2 = ({
<div
className="w-100 px-0 h-100 dropdownV2-widget"
ref={ref}
onMouseDownCapture={isEditor && handleClickInEditor}
onClick={handleClickInsideSelect}
onTouchEnd={handleClickInsideSelect}
>
<Select
ref={selectRef}
@ -456,17 +487,21 @@ export const DropdownV2 = ({
onChange={(selectedOption, actionProps) => {
if (actionProps.action === 'clear') {
setInputValue(null);
fireEvent('onSelect');
}
if (actionProps.action === 'select-option') {
setInputValue(selectedOption.value);
fireEvent('onSelect');
if (currentValue === selectedOption.value) {
setInputValue(null);
} else setInputValue(selectedOption.value);
}
fireEvent('onSelect');
setSearchInputValue('');
setIsMenuOpen(false);
setUserInteracted(true);
}}
options={selectOptions}
styles={customStyles}
isLoading={isDropdownLoading}
showSearchInput={showSearchInput}
onInputChange={onSearchTextChange}
inputValue={searchInputValue}
placeholder={placeholder}
@ -477,7 +512,7 @@ export const DropdownV2 = ({
Option: CustomOption,
LoadingIndicator: () => <Loader style={{ right: '11px', zIndex: 3, position: 'absolute' }} width="16" />,
DropdownIndicator: isDropdownLoading ? () => null : CustomDropdownIndicator,
ClearIndicator: CustomClearIndicator,
ClearIndicator: showClearBtn ? CustomClearIndicator : () => null,
}}
isClearable
tabSelectsValue={false}
@ -488,24 +523,20 @@ export const DropdownV2 = ({
darkMode={darkMode}
optionsLoadingState={optionsLoadingState && advanced}
menuPlacement="auto"
onMenuOpen={() => {
setIsMenuOpen(true);
fireEvent('onFocus');
}}
onMenuClose={() => {
setIsMenuOpen(false);
fireEvent('onBlur');
}}
onKeyDown={(e) => {
if (e.key === 'Enter' && !isMenuOpen) {
if (e.key === 'Enter' && !isMenuOpen && !isDropdownLoading) {
setIsMenuOpen(true);
fireEvent('onFocus');
e.preventDefault();
}
if (e.key === 'Escape' && isMenuOpen) {
setIsMenuOpen(false);
fireEvent('onBlur');
e.preventDefault();
}
}}
// This is not setting minheight, required to help calculate menuPlacement by providing fixed height upfront before rendering (required in the case of modal)
minMenuHeight={300}
/>
</div>
</div>

View file

@ -0,0 +1,7 @@
.dropdown-multiselect-widget-custom-menu-list-body {
overflow-y: auto;
position: relative;
padding: 0 0 5px;
background-color: var(--surfaces-surface-01) !important;
border-radius: 0 0 8px 8px;
}

View file

@ -17,7 +17,7 @@ export const Icon = ({
}) => {
const isInitialRender = useRef(true);
const { icon, loadingState, disabledState } = properties;
const { iconAlign, iconColor } = styles;
const { iconAlign, iconColor, boxShadow } = styles;
// eslint-disable-next-line import/namespace
const IconElement = Icons[icon];
@ -84,10 +84,10 @@ export const Icon = ({
</div>
) : (
<div
className={cx('icon-widget', { 'd-none': !visibility }, { 'cursor-pointer': false })}
className={cx('icon-widget h-100', { 'd-none': !visibility }, { 'cursor-pointer': false })}
data-cy={dataCy}
data-disabled={isDisabled}
style={{ textAlign: iconAlign }}
style={{ textAlign: iconAlign, boxShadow }}
onMouseEnter={(event) => {
event.stopPropagation();
fireEvent('onHover');
@ -97,7 +97,7 @@ export const Icon = ({
color={color}
style={{
width: height < width ? 'auto' : width,
height: height < width ? height : 'auto',
height: height < width ? '100%' : 'auto',
color: iconColor,
}}
onClick={(event) => {

View file

@ -19,8 +19,7 @@ export const Link = ({ height, properties, styles, fireEvent, setExposedVariable
const computedStyles = {
display: 'flex',
alignItems: verticalAlignment === 'top' ? 'flex-start' : verticalAlignment === 'center' ? 'center' : 'flex-end',
justifyContent:
horizontalAlignment === 'left' ? 'flex-start' : horizontalAlignment === 'center' ? 'center' : 'flex-end',
textAlign: horizontalAlignment === 'left' ? 'left' : horizontalAlignment === 'center' ? 'center' : 'right',
height: '100%',
width: '100%',
boxShadow,
@ -113,10 +112,20 @@ export const Link = ({ height, properties, styles, fireEvent, setExposedVariable
onMouseOver={() => {
fireEvent('onHover');
}}
style={{ color: textColor, fontSize: textSize, cursor: isDisabled ? 'not-allowed' : 'pointer' }}
style={{ width: '100%' }}
ref={clickRef}
>
<span className="d-flex justify-content-center">
<span
className="d-flex"
style={{
fontSize: textSize,
cursor: 'auto',
justifyContent:
horizontalAlignment === 'left' ? 'flex-start' : horizontalAlignment === 'center' ? 'center' : 'flex-end',
color: textColor,
paddingBottom: verticalAlignment === 'bottom' ? '1px' : '0px',
}}
>
{iconVisibility && (
<IconElement
style={{
@ -130,7 +139,7 @@ export const Link = ({ height, properties, styles, fireEvent, setExposedVariable
stroke={1.5}
/>
)}
{linkTextState}
<span className="link-text">{linkTextState}</span>
</span>
</a>
</div>

View file

@ -1,8 +1,18 @@
.link-widget {
a {
text-underline-offset: 32%; // Adds space between text and underline
&:hover {
color: var(--link-hover-color) !important;
}
text-decoration: none !important;
pointer-events: none;
cursor: none !important;
.link-text {
pointer-events: all;
text-underline-offset: 32%; // Adds space between text and underline
cursor: pointer;
&:hover {
text-decoration: underline;
text-decoration-color: var(--link-hover-color) !important;
color: var(--link-hover-color) !important;
}
}
}
}

View file

@ -7,11 +7,23 @@ import { highlightText } from '../DropdownV2/utils';
const CustomOption = (props) => {
return (
<Option {...props}>
<Option
{...props}
innerProps={{
...props.innerProps,
onTouchEnd: (e) => {
e.preventDefault();
e.stopPropagation();
props.selectOption(props.data);
},
}}
>
<div className="d-flex multiselct-widget-option">
<FormCheck checked={props.isSelected} disabled={props?.isDisabled} />
<span style={{ marginLeft: '5px' }}>
{highlightText(props.label?.toString(), props.selectProps.inputValue)}
{props.label?.includes('Select all')
? 'Select all'
: highlightText(props.label?.toString(), props.selectProps.inputValue)}
</span>
</div>
</Option>

View file

@ -42,7 +42,7 @@ const CustomValueContainer = ({ children, ...props }) => {
{/* Rendering children except Placeholder component to preserve the default behavior of react-select like focus
handling */}
{React.Children.map(children, (child) => {
if (child.type !== Placeholder) {
if (child?.type !== Placeholder) {
return child;
}
})}

View file

@ -12,7 +12,6 @@ import Label from '@/_ui/Label';
const tinycolor = require('tinycolor2');
import { CustomDropdownIndicator, CustomClearIndicator } from '../DropdownV2/DropdownV2';
import { getInputBackgroundColor, getInputBorderColor, getInputFocusedColor, sortArray } from '../DropdownV2/utils';
import useStore from '@/AppBuilder/_stores/store';
export const MultiselectV2 = ({
id,
@ -38,6 +37,8 @@ export const MultiselectV2 = ({
loadingState: multiSelectLoadingState,
optionsLoadingState,
sort,
showClearBtn,
showSearchInput,
} = properties;
const {
selectedTextColor,
@ -73,12 +74,9 @@ export const MultiselectV2 = ({
const [visibility, setVisibility] = useState(properties.visibility);
const [isMultiSelectLoading, setIsMultiSelectLoading] = useState(multiSelectLoadingState);
const [isMultiSelectDisabled, setIsMultiSelectDisabled] = useState(disabledState);
const [isSelectAllSelected, setIsSelectAllSelected] = useState(false);
const [searchInputValue, setSearchInputValue] = useState('');
const _height = padding === 'default' ? `${height}px` : `${height + 4}px`;
const [userInteracted, setUserInteracted] = useState(false);
const currentMode = useStore((state) => state.currentMode);
const isEditor = currentMode === 'edit';
const [isMultiselectOpen, setIsMultiselectOpen] = useState(false);
useEffect(() => {
@ -93,18 +91,29 @@ export const MultiselectV2 = ({
const _options = advanced ? schema : options;
let _selectOptions = Array.isArray(_options)
? _options
.filter((data) => data?.visible ?? true)
.map((data) => ({
...data,
label: data?.label,
value: data?.value,
isDisabled: data?.disable ?? false,
}))
.filter((data) => data?.visible ?? true)
.map((data) => ({
...data,
label: data?.label,
value: data?.value,
isDisabled: data?.disable ?? false,
}))
: [];
return sortArray(_selectOptions, sort);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [advanced, JSON.stringify(schema), JSON.stringify(options), sort]);
const modifiedSelectOptions = useMemo(() => {
// Adding select all option dynamically to the options
if (showAllOption && !optionsLoadingState) {
return [
// Appended search input value so that it is always visible
{ label: `Select all ${searchInputValue}`, value: 'multiselect-custom-menulist-select-all' },
...selectOptions,
];
} else return selectOptions;
}, [showAllOption, JSON.stringify(selectOptions), optionsLoadingState, searchInputValue]);
function findDefaultItem(value, isAdvanced, isDefault) {
if (isAdvanced) {
const foundItem = Array.isArray(schema) ? schema.filter((item) => item?.visible && item?.default) : [];
@ -129,8 +138,28 @@ export const MultiselectV2 = ({
}
return false;
}
const onChangeHandler = (items, action) => {
setInputValue(items);
const SELECT_ALL = 'multiselect-custom-menulist-select-all';
if (action.option?.value === SELECT_ALL) {
// Case 1 - If select all is selected
if (action.action === 'select-option') {
setInputValue(modifiedSelectOptions);
} else {
setInputValue([]);
}
} else if (items?.some((item) => item.value === SELECT_ALL)) {
// Case 2 - If select all is not selected but selected options include select all
setInputValue(items.filter((item) => item.value !== SELECT_ALL));
} else if (selectOptions?.length === items?.length) {
// Case 3 - If all options are selected except select all
setInputValue(modifiedSelectOptions);
} else {
// Case 4 - Normal selection
setInputValue(items);
}
fireEvent('onSelect');
setUserInteracted(true);
};
@ -270,26 +299,34 @@ export const MultiselectV2 = ({
fireEvent('onSearchTextChanged');
}
};
const handleClickOutside = (event) => {
const handleClickOutsideSelect = (event) => {
let menu = document.getElementById(`dropdown-multiselect-widget-custom-menu-list-${id}`);
if (
isMultiselectOpen &&
multiselectRef.current &&
!multiselectRef.current.contains(event.target) &&
menu &&
!menu.contains(event.target)
) {
if (isMultiselectOpen) {
fireEvent('onBlur');
setIsMultiselectOpen(false);
setSearchInputValue('');
}
setIsMultiselectOpen(false);
fireEvent('onBlur');
setSearchInputValue('');
}
};
const handleClickInEditor = (e) => {
if (e.target.className.includes('clear-indicator') || isMultiselectOpen) return;
e.stopPropagation();
selectRef.current?.onControlMouseDown(e);
const handleClickInsideSelect = () => {
if (isMultiSelectDisabled || isMultiSelectLoading) return;
if (isMultiselectOpen) {
setIsMultiselectOpen(false);
fireEvent('onBlur');
setSearchInputValue('');
} else {
setIsMultiselectOpen(true);
fireEvent('onFocus');
if (!showSearchInput) {
selectRef.current.focus();
}
}
};
const setInputValue = (values) => {
@ -304,20 +341,11 @@ export const MultiselectV2 = ({
};
useEffect(() => {
document.addEventListener('mousedown', handleClickOutside, { capture: true });
document.addEventListener('mousedown', handleClickOutsideSelect, { capture: true });
return () => {
document.removeEventListener('mousedown', handleClickOutside, { capture: true });
document.removeEventListener('mousedown', handleClickOutsideSelect, { capture: true });
};
}, [isMultiselectOpen]);
// Handle Select all logic
useEffect(() => {
if (selectOptions?.length === selected?.length) {
setIsSelectAllSelected(true);
} else {
setIsSelectAllSelected(false);
}
}, [selectOptions, selected]);
}, [isMultiselectOpen, componentName]);
const customStyles = {
container: (base) => ({
@ -365,8 +393,8 @@ export const MultiselectV2 = ({
selectedTextColor !== '#1B1F24'
? selectedTextColor
: isMultiSelectLoading || isMultiSelectDisabled
? 'var(--text-disabled)'
: 'var(--text-primary)',
? 'var(--text-disabled)'
: 'var(--text-primary)',
}),
input: (provided, _state) => ({
@ -401,10 +429,10 @@ export const MultiselectV2 = ({
color: _state.isDisabled
? 'var(_--text-disbled)'
: selectedTextColor !== '#1B1F24'
? selectedTextColor
: isMultiSelectDisabled || isMultiSelectLoading
? 'var(--text-disabled)'
: 'var(--text-primary)',
? selectedTextColor
: isMultiSelectDisabled || isMultiSelectLoading
? 'var(--text-disabled)'
: 'var(--text-primary)',
borderRadius: _state.isFocused && '8px',
padding: '8px 6px 8px 12px',
'&:hover': {
@ -415,7 +443,7 @@ export const MultiselectV2 = ({
}),
menuList: (provided) => ({
...provided,
padding: '4px',
padding: '0 4px',
// this is needed otherwise :active state doesn't look nice, gap is required
display: 'flex',
flexDirection: 'column',
@ -426,6 +454,7 @@ export const MultiselectV2 = ({
menu: (provided) => ({
...provided,
marginTop: '5px',
borderRadius: '8px',
}),
};
const _width = (labelWidth / 100) * 70; // Max width which label can go is 70% for better UX calculate width based on this value
@ -436,7 +465,7 @@ export const MultiselectV2 = ({
data-cy={`label-${String(componentName).toLowerCase()} `}
className={cx('multiselect-widget', 'd-flex', {
[alignment === 'top' &&
((labelWidth != 0 && label?.length != 0) || (auto && labelWidth == 0 && label && label?.length != 0))
((labelWidth != 0 && label?.length != 0) || (auto && labelWidth == 0 && label && label?.length != 0))
? 'flex-column'
: 'align-items-center']: true,
'flex-row-reverse': direction === 'right' && alignment === 'side',
@ -468,17 +497,18 @@ export const MultiselectV2 = ({
_width={_width}
top={'1px'}
/>
<div className="w-100 px-0 h-100" onMouseDownCapture={isEditor && handleClickInEditor}>
<div className="w-100 px-0 h-100" onClick={handleClickInsideSelect} onTouchEnd={handleClickInsideSelect}>
<Select
ref={selectRef}
menuId={id}
isDisabled={isMultiSelectDisabled}
value={selected}
onChange={onChangeHandler}
options={selectOptions}
options={modifiedSelectOptions}
styles={customStyles}
// Only show loading when dynamic options are enabled
isLoading={isMultiSelectLoading}
showSearchInput={showSearchInput}
onInputChange={onSearchTextChange}
inputValue={searchInputValue}
menuIsOpen={isMultiselectOpen}
@ -488,7 +518,7 @@ export const MultiselectV2 = ({
ValueContainer: CustomValueContainer,
Option: CustomOption,
LoadingIndicator: () => <Loader style={{ right: '11px', zIndex: 3, position: 'absolute' }} width="16" />,
ClearIndicator: CustomClearIndicator,
ClearIndicator: showClearBtn ? CustomClearIndicator : () => null,
DropdownIndicator: isMultiSelectLoading ? () => null : CustomDropdownIndicator,
}}
isClearable
@ -498,21 +528,15 @@ export const MultiselectV2 = ({
tabSelectsValue={false}
controlShouldRenderValue={false}
isSearchable={false}
onMenuOpen={() => {
setIsMultiselectOpen(true);
fireEvent('onFocus');
}}
onMenuClose={() => {
setIsMultiselectOpen(false);
fireEvent('onBlur');
}}
onKeyDown={(e) => {
if (e.key === 'Enter' && !isMultiselectOpen) {
if (e.key === 'Enter' && !isMultiselectOpen && !isMultiSelectLoading) {
setIsMultiselectOpen(true);
fireEvent('onFocus');
e.preventDefault();
}
if (e.key === 'Escape' && isMultiselectOpen) {
setIsMultiselectOpen(false);
fireEvent('onBlur');
e.preventDefault();
}
}}
@ -520,21 +544,13 @@ export const MultiselectV2 = ({
icon={icon}
doShowIcon={iconVisibility}
containerRef={valueContainerRef}
showAllOption={showAllOption}
isSelectAllSelected={isSelectAllSelected}
setIsSelectAllSelected={(value) => {
setIsSelectAllSelected(value);
if (!value) {
fireEvent('onSelect');
}
}}
setSelected={setInputValue}
iconColor={iconColor}
optionsLoadingState={optionsLoadingState && advanced}
darkMode={darkMode}
fireEvent={() => fireEvent('onSelect')}
menuPlacement="auto"
menuPortalTarget={document.body}
// This is not setting minheight, required to help calculate menuPlacement by providing fixed height upfront before rendering (required in the case of modal)
minMenuHeight={300}
/>
</div>
</div>

View file

@ -123,6 +123,19 @@ export const buttonGroupConfig = {
defaultValue: '#007bff',
},
},
padding: {
type: 'switch',
displayName: 'Padding',
validation: {
schema: { type: 'union', schemas: [{ type: 'string' }, { type: 'number' }] },
defaultValue: 'default',
},
isFxNotRequired: true,
options: [
{ displayName: 'Default', value: 'default' },
{ displayName: 'None', value: 'none' },
],
},
},
exposedVariables: {
selected: [1],
@ -148,6 +161,7 @@ export const buttonGroupConfig = {
disabledState: { value: '{{false}}' },
selectedTextColor: { value: '' },
selectedBackgroundColor: { value: '' },
padding: { value: 'default' },
},
},
};

View file

@ -126,6 +126,20 @@ export const checkboxConfig = {
],
accordian: 'label',
},
padding: {
type: 'switch',
displayName: 'Padding',
validation: {
schema: { type: 'union', schemas: [{ type: 'string' }, { type: 'number' }] },
defaultValue: 'default',
},
isFxNotRequired: true,
options: [
{ displayName: 'Default', value: 'default' },
{ displayName: 'None', value: 'none' },
],
accordian: 'switch',
},
},
exposedVariables: {
value: false,
@ -189,6 +203,7 @@ export const checkboxConfig = {
handleColor: { value: '#FFFFFF' },
alignment: { value: 'right' },
boxShadow: { value: '0px 0px 0px 0px #00000090' },
padding: { value: 'default' },
},
validation: {
mandatory: { value: '{{false}}' },

View file

@ -26,6 +26,19 @@ export const colorPickerConfig = {
},
styles: {
visibility: { type: 'toggle', displayName: 'Visibility' },
padding: {
type: 'switch',
displayName: 'Padding',
validation: {
schema: { type: 'union', schemas: [{ type: 'string' }, { type: 'number' }] },
defaultValue: 'default',
},
isFxNotRequired: true,
options: [
{ displayName: 'Default', value: 'default' },
{ displayName: 'None', value: 'none' },
],
},
},
exposedVariables: {
selectedColorHex: '#000000',
@ -45,6 +58,7 @@ export const colorPickerConfig = {
events: [],
styles: {
visibility: { value: '{{true}}' },
padding: { value: 'default' },
},
},
};

View file

@ -75,6 +75,18 @@ export const dropdownV2Config = {
accordian: 'Options',
isFxNotRequired: true,
},
showClearBtn: {
type: 'toggle',
displayName: 'Show clear selection button',
validation: { schema: { type: 'boolean' }, defaultValue: true },
section: 'additionalActions',
},
showSearchInput: {
type: 'toggle',
displayName: 'Show search in options',
validation: { schema: { type: 'boolean' }, defaultValue: true },
section: 'additionalActions',
},
loadingState: {
type: 'toggle',
displayName: 'Loading state',
@ -314,6 +326,8 @@ export const dropdownV2Config = {
optionsLoadingState: { value: '{{false}}' },
sort: { value: 'asc' },
placeholder: { value: 'Select an option' },
showClearBtn: { value: '{{true}}' },
showSearchInput: { value: '{{true}}' },
visibility: { value: '{{true}}' },
disabledState: { value: '{{false}}' },
loadingState: { value: '{{false}}' },

View file

@ -78,6 +78,29 @@ export const iconConfig = {
},
accordian: 'Icon',
},
padding: {
type: 'switch',
displayName: 'Padding',
validation: {
schema: { type: 'union', schemas: [{ type: 'string' }, { type: 'number' }] },
defaultValue: 'default',
},
isFxNotRequired: true,
options: [
{ displayName: 'Default', value: 'default' },
{ displayName: 'None', value: 'none' },
],
accordian: 'Icon',
},
boxShadow: {
type: 'boxShadow',
displayName: 'Box shadow',
validation: {
schema: { type: 'union', schemas: [{ type: 'string' }, { type: 'number' }] },
defaultValue: '0px 0px 0px 0px #00000040',
},
accordian: 'Icon',
},
},
exposedVariables: {},
actions: [
@ -116,6 +139,8 @@ export const iconConfig = {
styles: {
iconColor: { value: '#000' },
iconAlign: { value: 'center' },
padding: { value: 'default' },
boxShadow: { value: '0px 0px 0px 0px #00000040' },
},
},
};

View file

@ -5,7 +5,7 @@ export const modalV2Config = {
component: 'ModalV2',
defaultSize: {
width: 10,
height: 34,
height: 40,
},
others: {
showOnDesktop: { type: 'toggle', displayName: 'Show on desktop' },
@ -137,7 +137,7 @@ export const modalV2Config = {
layout: {
top: 24,
left: 22,
height: 36,
height: 40,
},
displayName: 'ModalFooterCancel',
properties: ['text'],
@ -154,7 +154,7 @@ export const modalV2Config = {
layout: {
top: 24,
left: 32,
height: 36,
height: 40,
},
displayName: 'ModalFooterConfirm',
properties: ['text'],

View file

@ -142,6 +142,18 @@ export const multiselectV2Config = {
accordian: 'Options',
isFxNotRequired: true,
},
showClearBtn: {
type: 'toggle',
displayName: 'Show clear selection button',
validation: { schema: { type: 'boolean' }, defaultValue: true },
section: 'additionalActions',
},
showSearchInput: {
type: 'toggle',
displayName: 'Show search in options',
validation: { schema: { type: 'boolean' }, defaultValue: true },
section: 'additionalActions',
},
loadingState: {
type: 'toggle',
displayName: 'Loading state',
@ -327,6 +339,8 @@ export const multiselectV2Config = {
optionsLoadingState: { value: '{{false}}' },
sort: { value: 'asc' },
placeholder: { value: 'Select the options' },
showClearBtn: { value: '{{true}}' },
showSearchInput: { value: '{{true}}' },
visibility: { value: '{{true}}' },
disabledState: { value: '{{false}}' },
loadingState: { value: '{{false}}' },

View file

@ -84,6 +84,19 @@ export const rangeSliderConfig = {
defaultValue: true,
},
},
padding: {
type: 'switch',
displayName: 'Padding',
validation: {
schema: { type: 'union', schemas: [{ type: 'string' }, { type: 'number' }] },
defaultValue: 'default',
},
isFxNotRequired: true,
options: [
{ displayName: 'Default', value: 'default' },
{ displayName: 'None', value: 'none' },
],
},
},
exposedVariables: {
value: null,
@ -111,6 +124,7 @@ export const rangeSliderConfig = {
handleColor: { value: '' },
trackColor: { value: '' },
visibility: { value: '{{true}}' },
padding: { value: 'default' },
},
},
};

View file

@ -89,6 +89,19 @@ export const starratingConfig = {
defaultValue: false,
},
},
padding: {
type: 'switch',
displayName: 'Padding',
validation: {
schema: { type: 'union', schemas: [{ type: 'string' }, { type: 'number' }] },
defaultValue: 'default',
},
isFxNotRequired: true,
options: [
{ displayName: 'Default', value: 'default' },
{ displayName: 'None', value: 'none' },
],
},
},
exposedVariables: {
value: 0,
@ -112,6 +125,7 @@ export const starratingConfig = {
labelColor: { value: '' },
visibility: { value: '{{true}}' },
disabledState: { value: '{{false}}' },
padding: { value: 'default' },
},
},
};

View file

@ -38,6 +38,19 @@ export const tagsConfig = {
defaultValue: true,
},
},
padding: {
type: 'switch',
displayName: 'Padding',
validation: {
schema: { type: 'union', schemas: [{ type: 'string' }, { type: 'number' }] },
defaultValue: 'default',
},
isFxNotRequired: true,
options: [
{ displayName: 'Default', value: 'default' },
{ displayName: 'None', value: 'none' },
],
},
},
exposedVariables: {},
definition: {
@ -54,6 +67,7 @@ export const tagsConfig = {
events: [],
styles: {
visibility: { value: '{{true}}' },
padding: { value: 'default' },
},
},
};

View file

@ -126,6 +126,20 @@ export const toggleSwitchV2Config = {
validation: { schema: { type: 'union', schemas: [{ type: 'string' }, { type: 'number' }] } },
accordian: 'switch',
},
padding: {
type: 'switch',
displayName: 'Padding',
validation: {
schema: { type: 'union', schemas: [{ type: 'string' }, { type: 'number' }] },
defaultValue: 'default',
},
isFxNotRequired: true,
options: [
{ displayName: 'Default', value: 'default' },
{ displayName: 'None', value: 'none' },
],
accordian: 'switch',
},
},
exposedVariables: {
value: false,
@ -187,6 +201,7 @@ export const toggleSwitchV2Config = {
handleColor: { value: '#FFFFFF' },
alignment: { value: 'right' },
boxShadow: { value: '0px 0px 0px 0px #00000090' },
padding: { value: 'default' },
},
},
};

View file

@ -1250,6 +1250,7 @@ $border-radius: 4px;
color: var(--slate12) !important;
}
}
&.data-source-exists {
.cm-editor {
border-radius: 0 4px 4px 0 !important;
@ -1834,6 +1835,7 @@ $border-radius: 4px;
.cm-scroller {
border-bottom-left-radius: 4px;
overscroll-behavior: auto !important;
}
}
@ -1853,22 +1855,23 @@ $border-radius: 4px;
margin-left: 32px !important;
}
.tjdb-codhinter-wrapper{
.codehinter-input{
.cm-editor{
.tjdb-codhinter-wrapper {
.codehinter-input {
.cm-editor {
// height: 30px !important;
min-height: 30px !important;
max-height:100px !important;
max-height: 100px !important;
border-radius: 0 !important;
border-right: 0 ;
}
border-right: 0;
}
}
}
.tjdb-limit-offset-codehinter{
.cm-editor{
.tjdb-limit-offset-codehinter {
.cm-editor {
// height: 30px !important;
min-height: 30px !important;
max-height:100px !important;
max-height: 100px !important;
}
}
@ -1896,7 +1899,7 @@ $border-radius: 4px;
margin-right: auto;
p {
display: flex;
display: flex;
align-items: center;
span {
@ -1917,8 +1920,13 @@ $border-radius: 4px;
}
.restapi-key-value {
.code-hinter-wrapper, .code-editor-basic-wrapper, .codehinter-container, .cm-codehinter, .code-editor-query-panel{
height:100%;
.code-hinter-wrapper,
.code-editor-basic-wrapper,
.codehinter-container,
.cm-codehinter,
.code-editor-query-panel {
height: 100%;
max-height: 100px;
}
}

View file

@ -8295,6 +8295,10 @@ tbody {
}
}
.query-manager-btn-shortcut {
color: var(--text-disabled) !important;
}
.font-weight-500 {
font-weight: 500;
}

View file

@ -0,0 +1,19 @@
import React from 'react';
const Play01 = ({ fill = '#6A727C', width = '24', className = '', viewBox = '0 0 24 24' }) => (
<svg
width={width}
height={width}
viewBox={viewBox}
className={className}
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M19.5904 14.6487L7.43642 21.5939C5.40287 22.7559 2.87265 21.2875 2.87265 18.9454V5.05514C2.87265 2.71301 5.40287 1.24466 7.43642 2.40669L19.5904 9.35182C21.6397 10.5228 21.6397 13.4777 19.5904 14.6487Z"
fill={fill}
/>
</svg>
);
export default Play01;

View file

@ -87,6 +87,7 @@ import Pin from './Pin.jsx';
import Unpin from './Unpin.jsx';
import AlignRight from './AlignRight';
import Play from './Play.jsx';
import Play01 from './Play01.jsx';
import Plus from './Plus.jsx';
import Plus01 from './Plus01.jsx';
import Reload from './Reload.jsx';
@ -698,6 +699,8 @@ const Icon = (props) => {
return <StudentIcon {...props} />;
case 'ai-crown':
return <AICrown {...props} />;
case 'play01':
return <Play01 {...props} />;
default:
return <Apps {...props} />;
}

Some files were not shown because too many files have changed in this diff Show more