mirror of
https://github.com/ToolJet/ToolJet
synced 2026-05-24 09:28:31 +00:00
Merge branch 'appbuilder/sprint-14' into gh-12680-toggle-app-mode
This commit is contained in:
commit
d1292add98
663 changed files with 26362 additions and 6039 deletions
|
|
@ -92,3 +92,9 @@ ENABLE_PRIVATE_APP_EMBED=
|
|||
|
||||
#Enable cors else restricted to TOOLJET_HOST. Set the value true if you are serving front end from diffrent host
|
||||
ENABLE_CORS=
|
||||
|
||||
#pat session expiry in minutes
|
||||
PAT_SESSION_EXPIRY=
|
||||
|
||||
#pat expiry in days
|
||||
PAT_EXPIRY=
|
||||
|
|
|
|||
12
.github/workflows/cypress-platform.yml
vendored
12
.github/workflows/cypress-platform.yml
vendored
|
|
@ -37,21 +37,9 @@ jobs:
|
|||
with:
|
||||
ref: ${{ github.event.pull_request.head.ref }}
|
||||
|
||||
# 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
|
||||
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@v2
|
||||
with:
|
||||
|
|
|
|||
34
.github/workflows/merging-pr.yml
vendored
34
.github/workflows/merging-pr.yml
vendored
|
|
@ -37,6 +37,40 @@ jobs:
|
|||
env:
|
||||
GH_TOKEN: ${{ secrets.TOKEN_PR }}
|
||||
|
||||
|
||||
update-submodule-sha:
|
||||
needs: merge-submodules
|
||||
if: github.event.pull_request.merged == true && github.event.pull_request.base.ref == 'main'
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout base repo
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: ToolJet/ToolJet
|
||||
token: ${{ secrets.TOKEN_PR }}
|
||||
ref: main
|
||||
submodules: recursive
|
||||
|
||||
- name: Update submodules to latest main
|
||||
run: |
|
||||
git config user.name "adishM98 Bot"
|
||||
git config user.email "adish.madhu@gmail.com"
|
||||
|
||||
git submodule update --remote frontend/ee
|
||||
git submodule update --remote server/ee
|
||||
|
||||
git add frontend/ee server/ee
|
||||
|
||||
if git diff --cached --quiet; then
|
||||
echo "No submodule updates found."
|
||||
else
|
||||
git commit -m "🔄 chore: update submodules to latest main after auto-merge"
|
||||
git push origin main
|
||||
fi
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.TOKEN_PR }}
|
||||
|
||||
check-submodule-prs:
|
||||
if: github.event.action == 'labeled' && github.event.label.name == 'ready-to-merge'
|
||||
runs-on: ubuntu-latest
|
||||
|
|
|
|||
52
.github/workflows/vulnerability-ci.yml
vendored
52
.github/workflows/vulnerability-ci.yml
vendored
|
|
@ -24,10 +24,10 @@ jobs:
|
|||
with:
|
||||
ref: refs/heads/main
|
||||
|
||||
- name: Use Node.js 18.18.2
|
||||
- name: Use Node.js 22.15.1
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18.18.2
|
||||
node-version: 22.15.1
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm --prefix frontend install
|
||||
|
|
@ -75,10 +75,10 @@ jobs:
|
|||
with:
|
||||
ref: refs/heads/main
|
||||
|
||||
- name: Use Node.js 18.18.2
|
||||
- name: Use Node.js 22.15.1
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18.18.2
|
||||
node-version: 22.15.1
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm --prefix server install
|
||||
|
|
@ -106,7 +106,7 @@ jobs:
|
|||
|
||||
- name: Send Slack Notification
|
||||
run: |
|
||||
message="### Periodic Security Audit Report Of Server directory\n
|
||||
message="Periodic Security Audit Report Of Server directory\n
|
||||
Node module vulnerabilities summary:\n
|
||||
🔴 Critical: ${{ steps.parse-audit.outputs.critical }}\n
|
||||
🟠 High: ${{ steps.parse-audit.outputs.high }}\n
|
||||
|
|
@ -126,10 +126,10 @@ jobs:
|
|||
with:
|
||||
ref: refs/heads/main
|
||||
|
||||
- name: Use Node.js 18.18.2
|
||||
- name: Use Node.js 22.15.1
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18.18.2
|
||||
node-version: 22.15.1
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm --prefix marketplace install
|
||||
|
|
@ -177,10 +177,10 @@ jobs:
|
|||
with:
|
||||
ref: refs/heads/main
|
||||
|
||||
- name: Use Node.js 18.18.2
|
||||
- name: Use Node.js 22.15.1
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18.18.2
|
||||
node-version: 22.15.1
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm --prefix plugins install
|
||||
|
|
@ -228,10 +228,10 @@ jobs:
|
|||
with:
|
||||
ref: refs/heads/main
|
||||
|
||||
- name: Use Node.js 18.18.2
|
||||
- name: Use Node.js 22.15.1
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18.18.2
|
||||
node-version: 22.15.1
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm --prefix cypress-tests install
|
||||
|
|
@ -279,10 +279,10 @@ jobs:
|
|||
with:
|
||||
ref: refs/heads/main
|
||||
|
||||
- name: Use Node.js 18.18.2
|
||||
- name: Use Node.js 22.15.1
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18.18.2
|
||||
node-version: 22.15.1
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm install
|
||||
|
|
@ -330,10 +330,10 @@ jobs:
|
|||
with:
|
||||
ref: ${{ github.event.pull_request.head.ref }}
|
||||
|
||||
- name: Use Node.js 18.18.2
|
||||
- name: Use Node.js 22.15.1
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18.18.2
|
||||
node-version: 22.15.1
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm --prefix frontend install
|
||||
|
|
@ -385,10 +385,10 @@ jobs:
|
|||
with:
|
||||
ref: ${{ github.event.pull_request.head.ref }}
|
||||
|
||||
- name: Use Node.js 18.18.2
|
||||
- name: Use Node.js 22.15.1
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18.18.2
|
||||
node-version: 22.15.1
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm --prefix server install
|
||||
|
|
@ -440,10 +440,10 @@ jobs:
|
|||
with:
|
||||
ref: ${{ github.event.pull_request.head.ref }}
|
||||
|
||||
- name: Use Node.js 18.18.2
|
||||
- name: Use Node.js 22.15.1
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18.18.2
|
||||
node-version: 22.15.1
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm --prefix marketplace install
|
||||
|
|
@ -494,10 +494,10 @@ jobs:
|
|||
with:
|
||||
ref: ${{ github.event.pull_request.head.ref }}
|
||||
|
||||
- name: Use Node.js 18.18.2
|
||||
- name: Use Node.js 22.15.1
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18.18.2
|
||||
node-version: 22.15.1
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm --prefix plugins install
|
||||
|
|
@ -550,10 +550,10 @@ jobs:
|
|||
with:
|
||||
ref: ${{ github.event.pull_request.head.ref }}
|
||||
|
||||
- name: Use Node.js 18.18.2
|
||||
- name: Use Node.js 22.15.1
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18.18.2
|
||||
node-version: 22.15.1
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm --prefix cypress-tests install
|
||||
|
|
@ -606,10 +606,10 @@ jobs:
|
|||
with:
|
||||
ref: ${{ github.event.pull_request.head.ref }}
|
||||
|
||||
- name: Use Node.js 18.18.2
|
||||
- name: Use Node.js 22.15.1
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18.18.2
|
||||
node-version: 22.15.1
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm install
|
||||
|
|
@ -648,4 +648,4 @@ jobs:
|
|||
🟠 High: ${{ steps.parse-audit.outputs.high }}
|
||||
🟡 Moderate: ${{ steps.parse-audit.outputs.moderate }}
|
||||
|
||||
Please find the JSON file in the [summary page](${{ github.root_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}).
|
||||
Please find the JSON file in the [summary page](${{ github.root_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}).
|
||||
|
|
|
|||
6
.vscode/settings.json
vendored
6
.vscode/settings.json
vendored
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"[javascript, typescript]": {
|
||||
"editor.defaultFormatter": "dbaeumer.vscode-eslint",
|
||||
"editor.defaultFormatter": "dbaeumer.vscode-eslint"
|
||||
},
|
||||
"eslint.validate": [
|
||||
"javascript",
|
||||
|
|
@ -8,8 +8,8 @@
|
|||
"typescript",
|
||||
"typescriptreact"
|
||||
],
|
||||
"eslint.format.enable": false,
|
||||
"editor.formatOnSave": false,
|
||||
"eslint.format.enable": true,
|
||||
"editor.formatOnSave": true,
|
||||
"json.schemas": [
|
||||
{
|
||||
"fileMatch": [
|
||||
|
|
|
|||
|
|
@ -98,7 +98,7 @@ module.exports = defineConfig({
|
|||
configFile: environment.configFile,
|
||||
specPattern: [
|
||||
"cypress/e2e/happyPath/platform/firstUser/firstUserOnboarding.cy.js",
|
||||
"cypress/e2e/happyPath/platform/commonTestcases/**/*.cy.js",
|
||||
"cypress/e2e/happyPath/platform/commonTestcases/userManagment/*.cy.js",
|
||||
"cypress/e2e/happyPath/platform/eeTestcases/**/*.cy.js",
|
||||
],
|
||||
numTestsKeptInMemory: 1,
|
||||
|
|
|
|||
|
|
@ -221,21 +221,21 @@ Cypress.Commands.add(
|
|||
const requestBody =
|
||||
envVar === "Enterprise"
|
||||
? {
|
||||
email: userEmail,
|
||||
firstName: userName,
|
||||
groups: [],
|
||||
lastName: "",
|
||||
role: userRole,
|
||||
userMetadata: metaData,
|
||||
}
|
||||
email: userEmail,
|
||||
firstName: userName,
|
||||
groups: [],
|
||||
lastName: "",
|
||||
role: userRole,
|
||||
userMetadata: metaData,
|
||||
}
|
||||
: {
|
||||
email: userEmail,
|
||||
firstName: userName,
|
||||
groups: [],
|
||||
lastName: "",
|
||||
role: userRole,
|
||||
userMetadata: metaData,
|
||||
};
|
||||
email: userEmail,
|
||||
firstName: userName,
|
||||
groups: [],
|
||||
lastName: "",
|
||||
role: userRole,
|
||||
userMetadata: metaData,
|
||||
};
|
||||
|
||||
cy.getCookie("tj_auth_token").then((cookie) => {
|
||||
cy.request(
|
||||
|
|
@ -509,7 +509,7 @@ Cypress.Commands.add("apiDeleteGranularPermission", (groupName) => {
|
|||
// Delete the granular permission
|
||||
cy.request({
|
||||
method: "DELETE",
|
||||
url: `${Cypress.env("server_host")}/api/v2/group-permissions/granular-permissions/${granularPermissionId}`,
|
||||
url: `${Cypress.env("server_host")}/api/v2/group-permissions/granular-permissions/app/${granularPermissionId}`,
|
||||
headers,
|
||||
log: false,
|
||||
}).then((deleteResponse) => {
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import { selectAppCardOption } from "Support/utils/common";
|
|||
const API_ENDPOINT =
|
||||
Cypress.env("environment") === "Community"
|
||||
? "/api/library_apps"
|
||||
: "/api/library_apps/";
|
||||
: "/api/library_apps";
|
||||
|
||||
Cypress.Commands.add(
|
||||
"appUILogin",
|
||||
|
|
@ -226,9 +226,9 @@ Cypress.Commands.add(
|
|||
.invoke("text")
|
||||
.then((text) => {
|
||||
cy.wrap(subject).realType(createBackspaceText(text)),
|
||||
{
|
||||
delay: 0,
|
||||
};
|
||||
{
|
||||
delay: 0,
|
||||
};
|
||||
});
|
||||
}
|
||||
);
|
||||
|
|
@ -548,7 +548,7 @@ Cypress.Commands.add("installMarketplacePlugin", (pluginName) => {
|
|||
}
|
||||
});
|
||||
|
||||
function installPlugin(pluginName) {
|
||||
function installPlugin (pluginName) {
|
||||
cy.get('[data-cy="-list-item"]').eq(1).click();
|
||||
cy.wait(1000);
|
||||
|
||||
|
|
@ -621,3 +621,12 @@ Cypress.Commands.add(
|
|||
.and("have.text", `${fieldName} is required`);
|
||||
}
|
||||
);
|
||||
|
||||
Cypress.Commands.add('ifEnv', (expectedEnvs, callback) => {
|
||||
const actualEnv = Cypress.env("environment");
|
||||
const envArray = Array.isArray(expectedEnvs) ? expectedEnvs : [expectedEnvs];
|
||||
|
||||
if (envArray.includes(actualEnv)) {
|
||||
callback();
|
||||
}
|
||||
});
|
||||
|
|
@ -18,7 +18,7 @@ export const commonSelectors = {
|
|||
canvas: "[data-cy=real-canvas]",
|
||||
appCardOptionsButton: "[data-cy=app-card-menu-icon]",
|
||||
autoSave: "[data-cy=autosave-indicator]",
|
||||
nameInputFieldd: "[data-cy=name-input-field]",
|
||||
inputFieldName: "[data-cy=name-input-field]",
|
||||
valueInputFieldd: "[data-cy=value-input-field]",
|
||||
skipButton: ".driver-close-btn",
|
||||
skipInstallationModal: "[data-cy=skip-button]",
|
||||
|
|
|
|||
202
cypress-tests/cypress/constants/selectors/eeCommon.js
Normal file
202
cypress-tests/cypress/constants/selectors/eeCommon.js
Normal file
|
|
@ -0,0 +1,202 @@
|
|||
import { cyParamName } from "./common";
|
||||
|
||||
export const commonEeSelectors = {
|
||||
instanceSettingIcon: '[data-cy="instance-settings-option"]',
|
||||
auditLogIcon: '[data-cy="audit-log-option"]',
|
||||
cancelButton: '[data-cy="cancel-button"]',
|
||||
saveButton: '[data-cy="save-button"]',
|
||||
pageTitle: '[data-cy="dashboard-section-header"]',
|
||||
modalTitle: '[data-cy="modal-title"]',
|
||||
modalCloseButton: '[data-cy="modal-close-button"]',
|
||||
saveButton: '[data-cy="save-button"]',
|
||||
cardTitle: '[data-cy="card-title"]',
|
||||
AddQueryButton: '[data-cy="show-ds-popover-button"]',
|
||||
promoteButton: '[data-cy="promote-button"]',
|
||||
settingsIcon: '[data-cy="icon-settings"]',
|
||||
gitSyncIcon: '[data-cy="git-sync-icon"]',
|
||||
confirmButton: '[data-cy="confirm-button"]',
|
||||
importFromGit: '[data-cy="import-from-git-button"]',
|
||||
searchBar: '[data-cy="query-manager-search-bar"]',
|
||||
nameHeader: '[data-cy="name-header"]',
|
||||
modalMessage: '[data-cy="modal-message"]',
|
||||
paginationSection: '[data-cy="pagination-section"]',
|
||||
|
||||
};
|
||||
|
||||
export const ssoEeSelector = {
|
||||
oidc: '[data-cy="openid-connect-sso-card"]',
|
||||
statusLabel: '[data-cy="status-label"]',
|
||||
oidcToggle: '[data-cy="openid-toggle-input"] > .slider',
|
||||
oidcPageElements: {
|
||||
oidcToggleLabel: '[data-cy="openid-toggle-label"]',
|
||||
nameLabel: '[data-cy="name-label"]',
|
||||
clientIdLabel: '[data-cy="client-id-label"]',
|
||||
clientSecretLabel: '[data-cy="client-secret-label"]',
|
||||
encryptedLabel: '[data-cy="encripted-label"]',
|
||||
WellKnownUrlLabel: '[data-cy="well-known-url-label"]',
|
||||
// redirectUrlLabel: '[data-cy="redirect-url-label"]',
|
||||
},
|
||||
nameInput: '[data-cy="name-input"]',
|
||||
clientIdInput: '[data-cy="client-id-input"]',
|
||||
clientSecretInput: '[data-cy="client-secret-input"]',
|
||||
WellKnownUrlInput: '[data-cy="well-known-url-input"]',
|
||||
redirectUrl: '[data-cy="redirect-url"]',
|
||||
copyIcon: '[data-cy="copy-icon]',
|
||||
oidcSSOText: '[data-cy="oidc-sso-button-text"]',
|
||||
oidcSSOIcon: '[data-cy="oidc-so-icon"]',
|
||||
ldapPageElements: {
|
||||
ldapToggleLabel: '[data-cy="ldap-toggle-label"]',
|
||||
nameLabel: '[data-cy="name-label"]',
|
||||
hostLabel: '[data-cy="host-label"]',
|
||||
portLabel: '[data-cy="port-label"]',
|
||||
baseDnLabel: '[data-cy="base-dn-label"]',
|
||||
baseDnHelperText: '[data-cy="base-dn-helper-text"]',
|
||||
sslLabel: '[data-cy="ssl-label"]',
|
||||
},
|
||||
ldapToggle: '[data-cy="ldap-toggle-input"] > .slider',
|
||||
hostInput: '[data-cy="host-input"]',
|
||||
portInput: '[data-cy="port-input"]',
|
||||
baseDnInput: '[data-cy="base-dn-input"]',
|
||||
sslToggleInput: '[data-cy="ssl-toggle-input"]',
|
||||
ldapSSOText: '[data-cy="ldap-sso-button-text"]',
|
||||
userNameInputLabel: '[data-cy="user-name-input-label"]',
|
||||
passwordInputLabel: '[data-cy="password-label"]',
|
||||
passwordInputField: '[data-cy="password-input-field"]',
|
||||
|
||||
samlModalElements: {
|
||||
toggleLabel: '[data-cy="saml-toggle-label"]',
|
||||
NameLabel: '[data-cy="name-label"]',
|
||||
metaDataLabel: '[data-cy="idp-metadata-label"]',
|
||||
baseDNHelperText: '[data-cy="base-dn-helper-text"]',
|
||||
groupAttributeLabel: '[data-cy="group-attribute-label"]',
|
||||
groupAttributeHelperText: '[data-cy="group-attribute-helper-text"]',
|
||||
}
|
||||
};
|
||||
|
||||
export const eeGroupsSelector = {
|
||||
resourceDs: '[data-cy="resource-datasources"]',
|
||||
dsCreateCheck: '[data-cy="checkbox-create-ds"]',
|
||||
dsDeleteCheck: '[data-cy="checkbox-delete-ds"]',
|
||||
datasourceLink: '[data-cy="datasource-link"]',
|
||||
dsSearch: '[data-cy="datasource-select-search"]',
|
||||
AddDsButton: '[data-cy="datasource-add-button"]',
|
||||
dsNameHeader: '[data-cy="datasource-name-header"]',
|
||||
};
|
||||
|
||||
export const instanceSettingsSelector = {
|
||||
allUsersTab: '[data-cy="all-users-list-item"]',
|
||||
manageInstanceSettings: '[data-cy="manage-instance-settings-list-item"]',
|
||||
typeColumnHeader: '[data-cy="users-table-type-column-header"]',
|
||||
workspaceColumnHeader: '[data-cy="users-table-workspaces-column-header"]',
|
||||
userName: (userName) => {
|
||||
return `[data-cy="${cyParamName(userName)}-user-name"]`;
|
||||
},
|
||||
userEmail: (userName) => {
|
||||
return `[data-cy="${cyParamName(userName)}-user-email"]`;
|
||||
},
|
||||
userType: (userName) => {
|
||||
return `[data-cy="${cyParamName(userName)}-user-type"]`;
|
||||
},
|
||||
userStatus: (userName) => {
|
||||
return `[data-cy="${cyParamName(userName)}-user-status"]`;
|
||||
},
|
||||
viewButton: (userName) => {
|
||||
return `[data-cy="${cyParamName(userName)}-user-view-button"]`;
|
||||
},
|
||||
editButton: (userName) => {
|
||||
return `[data-cy="${cyParamName(userName)}-user-edit-button"]`;
|
||||
},
|
||||
viewModalNoColumnHeader: '[data-cy="number-column-header"]',
|
||||
viewModalNameColumnHeader: '[data-cy="name-column-header"]',
|
||||
viewModalStatusColumnHeader: '[data-cy="status-column-header"]',
|
||||
archiveAllButton: '[data-cy="archive-all-button"]',
|
||||
viewModalRow: (workspaceName) => {
|
||||
return `[data-cy="${cyParamName(workspaceName)}-workspace-row"]>`;
|
||||
},
|
||||
|
||||
workspaceName: (workspaceName) => {
|
||||
return `[data-cy="${cyParamName(workspaceName)}-workspace-name"]`;
|
||||
},
|
||||
userStatusChangeButton: '[data-cy="user-state-change-button"]',
|
||||
superAdminToggle: '[data-cy="super-admin-form-check-input"]',
|
||||
superAdminToggleLabel: '[data-cy="super-admin-form-check-label"]',
|
||||
allowWorkspaceToggle: '[data-cy="form-check-input"]',
|
||||
allowWorkspaceToggleLabel: '[data-cy="form-check-label"]',
|
||||
allowWorkspaceHelperText: '[data-cy="instance-settings-help-text"]',
|
||||
allWorkspaceTab: '[data-cy="all-workspaces-list-item"]',
|
||||
};
|
||||
|
||||
|
||||
export const multiEnvSelector = {
|
||||
envContainer: '[data-cy="env-container"]',
|
||||
currentEnvName: '[data-cy="list-current-env-name"]',
|
||||
envArrow: '[data-cy="env-arrow"]',
|
||||
selectedEnvName: '[data-cy="selected-current-env-name"]',
|
||||
envNameList: '[data-cy="env-name-list"]',
|
||||
appVersionLabel: '[data-cy="app-version-label"]',
|
||||
currentVersion: '[data-cy="current-version"]',
|
||||
createNewVersionButton: '[data-cy="create-new-version-button"]',
|
||||
fromLabel: '[data-cy="from-label"]',
|
||||
toLabel: '[data-cy="to-label"]',
|
||||
currEnvName: '[data-cy="current-env-name"]',
|
||||
targetEnvName: '[data-cy="target-env-name"]',
|
||||
stagingLabel: '[data-cy="staging-label"]',
|
||||
productionLabel: '[data-cy="production-label"]',
|
||||
};
|
||||
|
||||
export const whiteLabellingSelectors = {
|
||||
whiteLabelList: '[data-cy="white-labelling-list-item"]',
|
||||
appLogoLabel: '[data-cy="app-logo-label"]',
|
||||
appLogoInput: '[data-cy="input-field-app-logo"]',
|
||||
appLogoHelpText: '[data-cy="app-logo-help-text"]',
|
||||
pageTitleLabel: '[data-cy="page-title-label"]',
|
||||
pageTitleInput: '[data-cy="input-field-page-title"]',
|
||||
pageTitleHelpText: '[data-cy="page-title-help-text"]',
|
||||
favIconLabel: '[data-cy="fav-icon-label"]',
|
||||
favIconInput: '[data-cy="input-field-fav-icon"]',
|
||||
favIconHelpText: '[data-cy="fav-icon-help-text"]',
|
||||
};
|
||||
|
||||
export const gitSyncSelector = {
|
||||
gitCommitInput: '[data-cy="git-commit-input"]',
|
||||
commitHelperText: '[data-cy="commit-helper-text"]',
|
||||
gitRepoInput: '[data-cy="git-repo-input"]',
|
||||
commitMessageInput: '[data-cy="commit-message-input"]',
|
||||
lastCommitInput: '[data-cy="las-commit-message"]',
|
||||
lastCommitVersion: '[data-cy="last-commit-version"]',
|
||||
autherInfo: '[data-cy="auther-info"]',
|
||||
commitButton: '[data-cy="commit-button"]',
|
||||
gitSyncToggleInput: '[data-cy="git-sync-toggle-input"]',
|
||||
gitSyncApphelperText: '[data-cy="sync-app-helper-text"]',
|
||||
connectRepoButton: '[data-cy="connect-repo-button"]',
|
||||
toggleMessage: '[data-cy="toggle-message"]',
|
||||
sshInput: '[data-cy="git-ssh-input"]',
|
||||
generateSshButton: '[data-cy="generate-ssh-key-button"',
|
||||
sshInputHelperText: '[data-cy="git-ssh-input-helper-text"]',
|
||||
configDeleteButton: '[data-cy="button-config-delete"]',
|
||||
testConnectionButton: '[data-cy="test-connection-button"]',
|
||||
sshKey: '[data-cy="ssh-key"]',
|
||||
deployKeyHelperText: '[data-cy="deploy-key-helper-text"]',
|
||||
gitRepoLink: '[data-cy="git-repo-link"]',
|
||||
appNameField: '[data-cy="app-name-field"]',
|
||||
gitRepoInfo: '[data-cy="git-repo-info"]',
|
||||
pullButton: '[data-cy="pull-button"]'
|
||||
|
||||
|
||||
}
|
||||
|
||||
export const workspaceSelector = {
|
||||
activelink: '[data-cy="active-link"]',
|
||||
archivedLik: '[data-cy="archived-link"]',
|
||||
userStatusChange: '[data-cy="button-user-status-change"]',
|
||||
workspaceStatusChange: '[data-cy="button-ws-status-change"]',
|
||||
switchWsModalTitle: '[data-cy="switch-modal-title"]',
|
||||
switchWsModalMessage: '[data-cy="switch-modal-message"]',
|
||||
workspaceName: (workspaceName) => {
|
||||
return `[data-cy="${workspaceName}-workspace-name"]`
|
||||
},
|
||||
workspaceInput: (workspaceName) => {
|
||||
return `[data-cy="${workspaceName}-workspace-input"]`
|
||||
},
|
||||
|
||||
}
|
||||
|
|
@ -54,8 +54,8 @@ export const onboardingSelectors = {
|
|||
basicPlanTitle: '[data-cy="basic-plan-title"]',
|
||||
planPrice: '[data-cy="plan-price"]',
|
||||
pricePeriod: '[data-cy="price-period"]',
|
||||
flexibleTitle: '[data-cy="flexible-title"]',
|
||||
businessTitle: '[data-cy="business-title"]',
|
||||
flexibleTitle: '[data-cy="pro-title"]',
|
||||
businessTitle: '[data-cy="team-title"]',
|
||||
enterpriseTitle: '[data-cy="enterprise-title"]',
|
||||
customPricingHeader: '[data-cy="custom-pricing-header"]',
|
||||
noCreditCardBanner: '[data-cy="no-credit-card-banner"]',
|
||||
|
|
|
|||
75
cypress-tests/cypress/constants/texts/eeCommon.js
Normal file
75
cypress-tests/cypress/constants/texts/eeCommon.js
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
export const commonEeText = {
|
||||
cancelButton: "Cancel",
|
||||
saveButton: "Save changes",
|
||||
closeButton: "Close",
|
||||
defaultWorkspace: "My workspace",
|
||||
};
|
||||
|
||||
export const ssoEeText = {
|
||||
statusLabel: "Disabled",
|
||||
enabledLabel: "Enabled",
|
||||
disabledLabel: "Disabled",
|
||||
oidcPageElements: {
|
||||
oidcToggleLabel: "OpenID Connect",
|
||||
nameLabel: "Name",
|
||||
clientIdLabel: "Client ID",
|
||||
clientSecretLabel: "Client secretEncrypted",
|
||||
encryptedLabel: "Encrypted",
|
||||
WellKnownUrlLabel: "Well known URL",
|
||||
// redirectUrlLabel: "Redirect URL",
|
||||
},
|
||||
oidcEnabledToast: "Enabled OpenId SSO",
|
||||
oidcDisabledToast: "Disabled OpenId SSO",
|
||||
oidcUpdatedToast: "updated SSO configurations",
|
||||
testName: "Tooljet OIDC",
|
||||
testclientId: "24567098-mklj8t20za1smb2if.apps.googleusercontent.com",
|
||||
testclientSecret: "2345-client-id-.apps.googleusercontent.com",
|
||||
testWellknownUrl: "google.com",
|
||||
oidcSSOText: "Sign in with Tooljet OIDC",
|
||||
|
||||
ldapPageElements: {
|
||||
ldapToggleLabel: "LDAP",
|
||||
nameLabel: "Name",
|
||||
hostLabel: "Host name",
|
||||
portLabel: "Port",
|
||||
baseDnLabel: "Base DN",
|
||||
baseDnHelperText: "Location without UID or CN",
|
||||
sslLabel: "SSL",
|
||||
},
|
||||
ldapSSOText: "Sign in with Tooljet LDAP Auth",
|
||||
userNameInputLabel: "Username",
|
||||
samlModalElements: {
|
||||
toggleLabel: "SAML",
|
||||
NameLabel: "Name",
|
||||
metaDataLabel: "Identity provider metadata",
|
||||
baseDNHelperText:
|
||||
"Ensure the Identity provider metadata is in XML format. You can download it from your IdP's site",
|
||||
groupAttributeLabel: "Group attribute",
|
||||
groupAttributeHelperText:
|
||||
"Define attribute for user-to-group mapping based on the IdP",
|
||||
},
|
||||
};
|
||||
export const eeGroupsText = {
|
||||
resourceDs: "Datasources",
|
||||
AddDsButton: "Add",
|
||||
dsNameHeader: "Datasource name",
|
||||
};
|
||||
|
||||
export const instanceSettingsText = {
|
||||
pageTitle: "Settings",
|
||||
allUsersTab: "All users",
|
||||
manageInstanceSettings: "Manage instance settings",
|
||||
typeColumnHeader: "Type",
|
||||
workspaceColumnHeader: "Workspaces",
|
||||
superAdminType: "instance",
|
||||
viewModalTitle: "Workspaces of The Developer",
|
||||
archiveAllButton: "Archive All",
|
||||
archiveState: "Archive",
|
||||
editModalTitle: "Edit user details",
|
||||
superAdminToggleLabel: "Super admin",
|
||||
allowWorkspaceToggleLabel: "Allow personal workspace",
|
||||
allowWorkspaceHelperText:
|
||||
"This feature will enable users to create their own workspace",
|
||||
saveButton: "Save",
|
||||
untitledWorkspace: "Untitled workspace",
|
||||
};
|
||||
|
|
@ -59,4 +59,9 @@ export const ssoText = {
|
|||
alertText: "Danger zone",
|
||||
disablePasswordHelperText:
|
||||
"Disable password login only if your SSO is configured otherwise you will get locked out",
|
||||
disablePasswordHelperText:
|
||||
"Disable password login only if your SSO is configured otherwise you will get locked out",
|
||||
toggleUpdateToast: (toggle) => {
|
||||
return `Saved ${toggle} SSO configurations`
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -23,8 +23,8 @@ export const onboardingText = {
|
|||
endUserPriceText: "$10",
|
||||
comparePlansText: "Compare plans",
|
||||
basicPlanText: "Basic Plan",
|
||||
flexibleText: "Flexible",
|
||||
businessText: "Business",
|
||||
flexibleText: "Pro",
|
||||
businessText: "Team",
|
||||
enterpriseText: "Enterprise",
|
||||
customPricingText: "Custom pricing",
|
||||
noCreditCardText: "No credit card required!",
|
||||
|
|
|
|||
|
|
@ -337,7 +337,7 @@ describe("Data source Rest API", () => {
|
|||
`cypress-${data.dataSourceName}-restapi`,
|
||||
"restapi",
|
||||
[
|
||||
{ key: "url", value: Cypress.env("restAPI_BaseURL") },
|
||||
{ key: "url", value: "https://jsonplaceholder.typicode.com" },
|
||||
{ key: "auth_type", value: "none" },
|
||||
{ key: "grant_type", value: "authorization_code" },
|
||||
{ key: "add_token_to", value: "header" },
|
||||
|
|
@ -370,62 +370,100 @@ describe("Data source Rest API", () => {
|
|||
cy.apiCreateApp(`${fake.companyName}-restAPI-CURD-App`);
|
||||
cy.openApp();
|
||||
createAndRunRestAPIQuery({
|
||||
queryName: "get_beeceptor_data",
|
||||
queryName: "get_all_users",
|
||||
dsName: `cypress-${data.dataSourceName}-restapi`,
|
||||
method: "GET",
|
||||
urlSuffix: "/api/users",
|
||||
urlSuffix: "/users",
|
||||
run: true,
|
||||
expectedResponseShape: {
|
||||
"0.id": true,
|
||||
"0.name": true,
|
||||
"0.email": true,
|
||||
},
|
||||
});
|
||||
createAndRunRestAPIQuery({
|
||||
queryName: "post_restapi",
|
||||
dsName: `cypress-${data.dataSourceName}-restapi`,
|
||||
method: "POST",
|
||||
headersList: [["Content-Type", "application/json"]],
|
||||
rawBody: '{"price": 200,"name": "Violin"}',
|
||||
urlSuffix: "/api/users",
|
||||
expectedResponseShape: { price: 200, name: "Violin", id: true },
|
||||
rawBody: `{
|
||||
"name": "Test User",
|
||||
"username": "testuser",
|
||||
"email": "test@example.com",
|
||||
"address": {
|
||||
"street": "123 Test St",
|
||||
"city": "Testville",
|
||||
"zipcode": "12345"
|
||||
}
|
||||
}`,
|
||||
urlSuffix: "/users",
|
||||
run: true,
|
||||
expectedResponseShape: {
|
||||
id: true,
|
||||
name: "Test User",
|
||||
email: "test@example.com",
|
||||
},
|
||||
});
|
||||
cy.readFile("cypress/fixtures/restAPI/storedId.json").then(
|
||||
(postResponseID) => {
|
||||
const id1 = postResponseID.id;
|
||||
const id1 = 1;
|
||||
|
||||
createAndRunRestAPIQuery({
|
||||
queryName: "put_restapi_id",
|
||||
dsName: `cypress-${data.dataSourceName}-restapi`,
|
||||
method: "PUT",
|
||||
headersList: [["Content-Type", "application/json"]],
|
||||
rawBody: '{"price": 500,"name": "Guitar"}',
|
||||
urlSuffix: `/api/users/${id1}`,
|
||||
expectedResponseShape: { price: 500, name: "Guitar", id: id1 },
|
||||
});
|
||||
createAndRunRestAPIQuery({
|
||||
queryName: "patch_restapi_id",
|
||||
dsName: `cypress-${data.dataSourceName}-restapi`,
|
||||
method: "PATCH",
|
||||
headersList: [["Content-Type", "application/json"]],
|
||||
rawBody: '{"price": 999 }',
|
||||
urlSuffix: `/api/users/${id1}`,
|
||||
run: true,
|
||||
expectedResponseShape: { price: 999, id: id1 },
|
||||
});
|
||||
createAndRunRestAPIQuery({
|
||||
queryName: "get_restapi_id",
|
||||
dsName: `cypress-${data.dataSourceName}-restapi`,
|
||||
method: "GET",
|
||||
urlSuffix: `/api/users/${id1}`,
|
||||
run: true,
|
||||
expectedResponseShape: { price: 999, name: "Guitar", id: id1 },
|
||||
});
|
||||
createAndRunRestAPIQuery({
|
||||
queryName: "delete_restapi_id",
|
||||
dsName: `cypress-${data.dataSourceName}-restapi`,
|
||||
method: "DELETE",
|
||||
urlSuffix: `/api/users/${id1}`,
|
||||
run: true,
|
||||
expectedResponseShape: { success: true },
|
||||
});
|
||||
}
|
||||
);
|
||||
createAndRunRestAPIQuery({
|
||||
queryName: "put_restapi_id",
|
||||
dsName: `cypress-${data.dataSourceName}-restapi`,
|
||||
method: "PUT",
|
||||
headersList: [["Content-Type", "application/json"]],
|
||||
rawBody: `{
|
||||
"id": 1,
|
||||
"name": "Fully Updated User",
|
||||
"username": "updateduser",
|
||||
"email": "updated@example.com",
|
||||
"address": {
|
||||
"street": "456 Updated St",
|
||||
"city": "Updatedville",
|
||||
"zipcode": "54321"
|
||||
}
|
||||
}`,
|
||||
urlSuffix: `/users/${id1}`,
|
||||
run: true,
|
||||
expectedResponseShape: {
|
||||
id: id1,
|
||||
name: "Fully Updated User",
|
||||
email: "updated@example.com",
|
||||
},
|
||||
});
|
||||
createAndRunRestAPIQuery({
|
||||
queryName: "patch_restapi_id",
|
||||
dsName: `cypress-${data.dataSourceName}-restapi`,
|
||||
method: "PATCH",
|
||||
headersList: [["Content-Type", "application/json"]],
|
||||
rawBody: `{
|
||||
"email": "partially.updated@example.com"
|
||||
}`,
|
||||
urlSuffix: `/users/${id1}`,
|
||||
run: true,
|
||||
expectedResponseShape: {
|
||||
id: id1,
|
||||
email: "partially.updated@example.com",
|
||||
},
|
||||
});
|
||||
createAndRunRestAPIQuery({
|
||||
queryName: "get_restapi_id",
|
||||
dsName: `cypress-${data.dataSourceName}-restapi`,
|
||||
method: "GET",
|
||||
urlSuffix: `/users/${id1}`,
|
||||
run: true,
|
||||
expectedResponseShape: {
|
||||
id: id1,
|
||||
email: "Sincere@april.biz",
|
||||
},
|
||||
});
|
||||
createAndRunRestAPIQuery({
|
||||
queryName: "delete_restapi_id",
|
||||
dsName: `cypress-${data.dataSourceName}-restapi`,
|
||||
method: "DELETE",
|
||||
urlSuffix: `/users/${id1}`,
|
||||
run: true,
|
||||
expectedResponseShape: {},
|
||||
});
|
||||
cy.apiDeleteApp(`${fake.companyName}-restAPI-CURD-App`);
|
||||
cy.apiDeleteGDS(`cypress-${data.dataSourceName}-restapi`);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -5,10 +5,7 @@ import { inviteUserToWorkspace } from "Support/utils/manageUsers";
|
|||
import { setSignupStatus } from "Support/utils/manageSSO";
|
||||
import { onboardingSelectors } from "Selectors/onboarding";
|
||||
import { commonText } from "Texts/common";
|
||||
import {
|
||||
userSignUp,
|
||||
addNewUser,
|
||||
} from "Support/utils/onboarding";
|
||||
import { userSignUp, addNewUser } from "Support/utils/onboarding";
|
||||
import {
|
||||
setUpSlug,
|
||||
setupAppWithSlug,
|
||||
|
|
@ -16,347 +13,371 @@ import {
|
|||
onboardUserFromAppLink,
|
||||
} from "Support/utils/apps";
|
||||
|
||||
describe(
|
||||
"Private and Public apps",
|
||||
{
|
||||
retries: { runMode: 2 },
|
||||
},
|
||||
() => {
|
||||
let data;
|
||||
|
||||
describe("Private and Public apps", {
|
||||
retries: { runMode: 2 },
|
||||
}, () => {
|
||||
let data;
|
||||
beforeEach(() => {
|
||||
data = {
|
||||
appName: `${fake.companyName} P P App`,
|
||||
slug: `${fake.companyName} P P App`.toLowerCase().replace(/\s+/g, "-"),
|
||||
firstName: fake.firstName,
|
||||
email: fake.email.toLowerCase(),
|
||||
workspaceName: fake.firstName,
|
||||
workspaceSlug: fake.firstName.toLowerCase().replace(/\s+/g, "-"),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
data = {
|
||||
appName: `${fake.companyName} P P App`,
|
||||
slug: `${fake.companyName} P P App`.toLowerCase().replace(/\s+/g, "-"),
|
||||
firstName: fake.firstName,
|
||||
email: fake.email.toLowerCase(),
|
||||
workspaceName: fake.firstName,
|
||||
workspaceSlug: fake.firstName.toLowerCase().replace(/\s+/g, "-"),
|
||||
}
|
||||
|
||||
cy.defaultWorkspaceLogin();
|
||||
cy.skipWalkthrough();
|
||||
});
|
||||
|
||||
it("Verify private and public app share functionality", () => {
|
||||
cy.apiCreateApp(data.appName);
|
||||
cy.openApp();
|
||||
cy.apiAddComponentToApp(data.appName, "text1");
|
||||
|
||||
// Check unreleased version state
|
||||
cy.get('[data-cy="share-button-link"]>span').should("be.visible").click();
|
||||
cy.contains("This version has not been released yet").should("be.visible");
|
||||
cy.get(commonWidgetSelector.modalCloseButton).click();
|
||||
|
||||
// Release and verify share modal
|
||||
releaseApp();
|
||||
cy.get(commonWidgetSelector.shareAppButton).click();
|
||||
for (const elements in commonWidgetSelector.shareModalElements) {
|
||||
cy.get(commonWidgetSelector.shareModalElements[elements])
|
||||
.verifyVisibleElement("have.text", commonText.shareModalElements[elements]);
|
||||
}
|
||||
|
||||
// Verify share modal elements
|
||||
const shareModalSelectors = [
|
||||
'copyAppLinkButton',
|
||||
'makePublicAppToggle',
|
||||
'appLink',
|
||||
'appNameSlugInput',
|
||||
'modalCloseButton'
|
||||
];
|
||||
shareModalSelectors.forEach(selector => {
|
||||
cy.get(commonWidgetSelector[selector]).should("be.visible");
|
||||
cy.defaultWorkspaceLogin();
|
||||
cy.skipWalkthrough();
|
||||
});
|
||||
|
||||
// Configure and verify slug
|
||||
cy.clearAndType(commonWidgetSelector.appNameSlugInput, data.slug);
|
||||
cy.get('[data-cy="app-slug-accepted-label"]')
|
||||
.should("be.visible")
|
||||
.and("have.text", "Slug accepted!");
|
||||
it("Verify private and public app share functionality", () => {
|
||||
cy.apiCreateApp(data.appName);
|
||||
cy.openApp();
|
||||
cy.apiAddComponentToApp(data.appName, "text1");
|
||||
|
||||
cy.get(commonWidgetSelector.modalCloseButton).click();
|
||||
cy.forceClickOnCanvas();
|
||||
cy.backToApps();
|
||||
// Check unreleased version state
|
||||
cy.get('[data-cy="share-button-link"]>span').should("be.visible").click();
|
||||
cy.contains("This version has not been released yet").should(
|
||||
"be.visible"
|
||||
);
|
||||
cy.get(commonWidgetSelector.modalCloseButton).click();
|
||||
|
||||
// Test private access
|
||||
logout();
|
||||
// Release and verify share modal
|
||||
releaseApp();
|
||||
cy.get(commonWidgetSelector.shareAppButton).click();
|
||||
for (const elements in commonWidgetSelector.shareModalElements) {
|
||||
cy.get(
|
||||
commonWidgetSelector.shareModalElements[elements]
|
||||
).verifyVisibleElement(
|
||||
"have.text",
|
||||
commonText.shareModalElements[elements]
|
||||
);
|
||||
}
|
||||
|
||||
cy.visitSlug({
|
||||
actualUrl: `${Cypress.config("baseUrl")}/applications/${data.slug}`,
|
||||
// Verify share modal elements
|
||||
const shareModalSelectors = [
|
||||
"copyAppLinkButton",
|
||||
"makePublicAppToggle",
|
||||
"appLink",
|
||||
"appNameSlugInput",
|
||||
"modalCloseButton",
|
||||
];
|
||||
shareModalSelectors.forEach((selector) => {
|
||||
cy.get(commonWidgetSelector[selector]).should("be.visible");
|
||||
});
|
||||
|
||||
// Configure and verify slug
|
||||
cy.clearAndType(commonWidgetSelector.appNameSlugInput, data.slug);
|
||||
cy.get('[data-cy="app-slug-accepted-label"]')
|
||||
.should("be.visible")
|
||||
.and("have.text", "Slug accepted!");
|
||||
|
||||
cy.get(commonWidgetSelector.modalCloseButton).click();
|
||||
cy.forceClickOnCanvas();
|
||||
cy.backToApps();
|
||||
|
||||
// Test private access
|
||||
logout();
|
||||
|
||||
cy.visitSlug({
|
||||
actualUrl: `${Cypress.config("baseUrl")}/applications/${data.slug}`,
|
||||
});
|
||||
|
||||
cy.get(onboardingSelectors.signInButton, { timeout: 20000 }).should(
|
||||
"be.visible"
|
||||
);
|
||||
cy.wait(2000);
|
||||
cy.appUILogin();
|
||||
cy.get(commonWidgetSelector.draggableWidget("text1")).should(
|
||||
"be.visible"
|
||||
);
|
||||
|
||||
// Test public access
|
||||
cy.get(commonSelectors.viewerPageLogo).click();
|
||||
cy.openApp(
|
||||
"appSlug",
|
||||
Cypress.env("workspaceId"),
|
||||
Cypress.env("appId"),
|
||||
commonWidgetSelector.draggableWidget("text1")
|
||||
);
|
||||
cy.get(commonWidgetSelector.shareAppButton).click();
|
||||
cy.get(commonWidgetSelector.makePublicAppToggle).check();
|
||||
cy.get(commonWidgetSelector.modalCloseButton).click();
|
||||
cy.backToApps();
|
||||
|
||||
logout();
|
||||
cy.visitSlug({
|
||||
actualUrl: `${Cypress.config("baseUrl")}/applications/${data.slug}`,
|
||||
});
|
||||
cy.get(commonWidgetSelector.draggableWidget("text1")).should(
|
||||
"be.visible"
|
||||
);
|
||||
});
|
||||
|
||||
cy.get(onboardingSelectors.signInButton, { timeout: 20000 }).should("be.visible");
|
||||
cy.wait(2000);
|
||||
cy.appUILogin();
|
||||
cy.get(commonWidgetSelector.draggableWidget("text1")).should("be.visible");
|
||||
it("Verify app private and public app visibility for the same workspace user", () => {
|
||||
setupAppWithSlug(data.appName, data.slug);
|
||||
|
||||
inviteUserToWorkspace(data.firstName, data.email);
|
||||
logout();
|
||||
cy.visit("/");
|
||||
cy.wait(2000);
|
||||
cy.get(onboardingSelectors.signInButton, { timeout: 20000 }).should(
|
||||
"be.visible"
|
||||
);
|
||||
|
||||
// Test public access
|
||||
cy.get(commonSelectors.viewerPageLogo).click();
|
||||
cy.openApp(
|
||||
"appSlug",
|
||||
Cypress.env("workspaceId"),
|
||||
Cypress.env("appId"),
|
||||
commonWidgetSelector.draggableWidget("text1")
|
||||
);
|
||||
cy.get(commonWidgetSelector.shareAppButton).click();
|
||||
cy.get(commonWidgetSelector.makePublicAppToggle).check();
|
||||
cy.get(commonWidgetSelector.modalCloseButton).click();
|
||||
cy.backToApps();
|
||||
// Test private access
|
||||
cy.visitSlug({
|
||||
actualUrl: `${Cypress.config("baseUrl")}/applications/${data.slug}`,
|
||||
});
|
||||
|
||||
logout();
|
||||
cy.visitSlug({
|
||||
actualUrl: `${Cypress.config("baseUrl")}/applications/${data.slug}`,
|
||||
});
|
||||
cy.get(commonWidgetSelector.draggableWidget("text1")).should("be.visible");
|
||||
cy.wait(2000);
|
||||
cy.appUILogin(data.email, "password");
|
||||
|
||||
cy.get(commonWidgetSelector.draggableWidget("text1")).should(
|
||||
"be.visible"
|
||||
);
|
||||
|
||||
});
|
||||
// Test with private app valid session
|
||||
cy.visitSlug({
|
||||
actualUrl: `${Cypress.config("baseUrl")}/applications/${data.slug}`,
|
||||
});
|
||||
cy.get(commonWidgetSelector.draggableWidget("text1")).should(
|
||||
"be.visible"
|
||||
);
|
||||
|
||||
it("Verify app private and public app visibility for the same workspace user", () => {
|
||||
setupAppWithSlug(data.appName, data.slug);
|
||||
cy.get(commonSelectors.viewerPageLogo).click();
|
||||
|
||||
inviteUserToWorkspace(data.firstName, data.email);
|
||||
logout();
|
||||
cy.visit("/");
|
||||
cy.wait(2000);
|
||||
cy.get(onboardingSelectors.signInButton, { timeout: 20000 }).should("be.visible");
|
||||
// Test public access
|
||||
cy.defaultWorkspaceLogin();
|
||||
cy.wait(1000);
|
||||
cy.apiMakeAppPublic();
|
||||
logout();
|
||||
cy.wait(1000);
|
||||
cy.get(onboardingSelectors.signInButton, { timeout: 20000 }).should(
|
||||
"be.visible"
|
||||
);
|
||||
|
||||
// Test private access
|
||||
cy.visitSlug({
|
||||
actualUrl: `${Cypress.config("baseUrl")}/applications/${data.slug}`,
|
||||
cy.visitSlug({
|
||||
actualUrl: `${Cypress.config("baseUrl")}/applications/${data.slug}`,
|
||||
});
|
||||
cy.get(commonWidgetSelector.draggableWidget("text1")).should(
|
||||
"be.visible"
|
||||
);
|
||||
|
||||
// Test with public app with valid session
|
||||
cy.apiLogin(data.email, "password");
|
||||
cy.visitSlug({
|
||||
actualUrl: `${Cypress.config("baseUrl")}/applications/${data.slug}`,
|
||||
});
|
||||
cy.get(commonWidgetSelector.draggableWidget("text1")).should(
|
||||
"be.visible"
|
||||
);
|
||||
});
|
||||
|
||||
cy.wait(2000);
|
||||
cy.appUILogin(data.email, "password");
|
||||
it("Verify app private and public app visibility for the same instance user", () => {
|
||||
setupAppWithSlug(data.appName, data.slug);
|
||||
|
||||
cy.get(commonWidgetSelector.draggableWidget("text1")).should("be.visible");
|
||||
cy.apiLogout();
|
||||
userSignUp(data.firstName, data.email, data.workspaceName);
|
||||
cy.wait(1000);
|
||||
cy.visitSlug({
|
||||
actualUrl: `${Cypress.config("baseUrl")}/applications/${data.slug}`,
|
||||
});
|
||||
|
||||
// Test with private app valid session
|
||||
cy.visitSlug({
|
||||
actualUrl: `${Cypress.config("baseUrl")}/applications/${data.slug}`,
|
||||
});
|
||||
cy.get(commonWidgetSelector.draggableWidget("text1")).should("be.visible");
|
||||
cy.visit("/");
|
||||
logout();
|
||||
|
||||
// Test public access
|
||||
cy.defaultWorkspaceLogin();
|
||||
cy.apiMakeAppPublic();
|
||||
logout();
|
||||
|
||||
cy.get(commonSelectors.viewerPageLogo).click();
|
||||
cy.wait(1000);
|
||||
cy.get(onboardingSelectors.signInButton, { timeout: 20000 }).should(
|
||||
"be.visible"
|
||||
);
|
||||
|
||||
// Test public access
|
||||
cy.defaultWorkspaceLogin();
|
||||
cy.wait(1000);
|
||||
cy.apiMakeAppPublic();
|
||||
logout();
|
||||
cy.wait(1000);
|
||||
cy.get(onboardingSelectors.signInButton, { timeout: 20000 }).should("be.visible");
|
||||
cy.visitSlug({
|
||||
actualUrl: `${Cypress.config("baseUrl")}/applications/${data.slug}`,
|
||||
});
|
||||
cy.get(commonWidgetSelector.draggableWidget("text1")).should(
|
||||
"be.visible"
|
||||
);
|
||||
|
||||
cy.visitSlug({
|
||||
actualUrl: `${Cypress.config("baseUrl")}/applications/${data.slug}`,
|
||||
});
|
||||
cy.get(commonWidgetSelector.draggableWidget("text1")).should("be.visible");
|
||||
|
||||
|
||||
|
||||
// Test with public app with valid session
|
||||
cy.apiLogin(data.email, "password");
|
||||
cy.visitSlug({
|
||||
actualUrl: `${Cypress.config("baseUrl")}/applications/${data.slug}`,
|
||||
});
|
||||
cy.get(commonWidgetSelector.draggableWidget("text1")).should("be.visible");
|
||||
|
||||
|
||||
});
|
||||
|
||||
it("Verify app private and public app visibility for the same instance user", () => {
|
||||
setupAppWithSlug(data.appName, data.slug);
|
||||
|
||||
cy.apiLogout();
|
||||
userSignUp(data.firstName, data.email, data.workspaceName);
|
||||
cy.wait(1000);
|
||||
cy.visitSlug({
|
||||
actualUrl: `${Cypress.config("baseUrl")}/applications/${data.slug}`,
|
||||
// Verify public app with valid session
|
||||
cy.apiLogin(data.email, "password");
|
||||
cy.visitSlug({
|
||||
actualUrl: `${Cypress.config("baseUrl")}/applications/${data.slug}`,
|
||||
});
|
||||
cy.get(commonWidgetSelector.draggableWidget("text1")).should(
|
||||
"be.visible"
|
||||
);
|
||||
});
|
||||
|
||||
cy.visit("/");
|
||||
logout();
|
||||
it("Should redirect to workspace login and handle signup flow of existing and non-existing user", () => {
|
||||
setSignupStatus(true);
|
||||
setupAppWithSlug(data.appName, data.slug);
|
||||
|
||||
// Test public access
|
||||
cy.defaultWorkspaceLogin();
|
||||
cy.apiMakeAppPublic();
|
||||
logout();
|
||||
cy.apiLogout();
|
||||
cy.visitSlug({
|
||||
actualUrl: `${Cypress.config("baseUrl")}/applications/${data.slug}`,
|
||||
});
|
||||
|
||||
cy.wait(1000);
|
||||
cy.get(onboardingSelectors.signInButton, { timeout: 20000 }).should("be.visible");
|
||||
cy.get(commonSelectors.workspaceName).verifyVisibleElement(
|
||||
"have.text",
|
||||
"My workspace"
|
||||
);
|
||||
|
||||
cy.visitSlug({
|
||||
actualUrl: `${Cypress.config("baseUrl")}/applications/${data.slug}`,
|
||||
});
|
||||
cy.get(commonWidgetSelector.draggableWidget("text1")).should("be.visible");
|
||||
// Test signup flow
|
||||
cy.intercept("POST", "/api/onboarding/signup").as("signup");
|
||||
cy.get(commonSelectors.createAnAccountLink).click();
|
||||
cy.wait(3000);
|
||||
cy.clearAndType(commonSelectors.inputFieldFullName, data.firstName);
|
||||
cy.clearAndType(commonSelectors.inputFieldEmailAddress, data.email);
|
||||
cy.clearAndType(onboardingSelectors.loginPasswordInput, "password");
|
||||
cy.get(commonSelectors.signUpButton).click();
|
||||
|
||||
cy.wait("@signup").then((interception) => {
|
||||
expect(interception.response.statusCode).to.eq(201);
|
||||
});
|
||||
|
||||
// Process invitation
|
||||
onboardUserFromAppLink(data.email, data.slug);
|
||||
|
||||
// Verify public app with valid session
|
||||
cy.apiLogin(data.email, "password");
|
||||
cy.visitSlug({
|
||||
actualUrl: `${Cypress.config("baseUrl")}/applications/${data.slug}`,
|
||||
});
|
||||
cy.get(commonWidgetSelector.draggableWidget("text1")).should("be.visible");
|
||||
cy.get(commonWidgetSelector.draggableWidget("text1")).should(
|
||||
"be.visible"
|
||||
);
|
||||
|
||||
cy.get('[data-cy="viewer-page-logo"]').click();
|
||||
logout();
|
||||
cy.wait(1000);
|
||||
cy.get(onboardingSelectors.signInButton, { timeout: 20000 }).should(
|
||||
"be.visible"
|
||||
);
|
||||
|
||||
});
|
||||
// Setup new workspace and app
|
||||
cy.defaultWorkspaceLogin();
|
||||
cy.apiCreateWorkspace(data.workspaceName, data.workspaceSlug);
|
||||
cy.apiLogout();
|
||||
cy.apiLogin();
|
||||
cy.visit(`${data.workspaceSlug}`);
|
||||
setSignupStatus(true, data.workspaceName);
|
||||
|
||||
it("Should redirect to workspace login and handle signup flow of existing and non-existing user", () => {
|
||||
setSignupStatus(true);
|
||||
setupAppWithSlug(data.appName, data.slug);
|
||||
data.slug = fake.firstName.toLowerCase().replace(/\s+/g, "-");
|
||||
|
||||
cy.apiLogout();
|
||||
cy.visitSlug({
|
||||
actualUrl: `${Cypress.config("baseUrl")}/applications/${data.slug}`,
|
||||
cy.createApp(data.appName);
|
||||
cy.dragAndDropWidget("Text", 500, 500);
|
||||
releaseApp();
|
||||
setUpSlug(data.slug);
|
||||
cy.forceClickOnCanvas();
|
||||
cy.backToApps();
|
||||
|
||||
// Test signup flow in new workspace
|
||||
cy.apiLogout();
|
||||
cy.visitSlug({
|
||||
actualUrl: `${Cypress.config("baseUrl")}/applications/${data.slug}`,
|
||||
});
|
||||
|
||||
cy.get(commonSelectors.workspaceName).verifyVisibleElement(
|
||||
"have.text",
|
||||
data.workspaceName
|
||||
);
|
||||
|
||||
cy.get(commonSelectors.createAnAccountLink).click();
|
||||
cy.wait(3000);
|
||||
cy.clearAndType(commonSelectors.inputFieldFullName, data.firstName);
|
||||
cy.clearAndType(commonSelectors.inputFieldEmailAddress, data.email);
|
||||
cy.clearAndType(onboardingSelectors.loginPasswordInput, "password");
|
||||
cy.get(commonSelectors.signUpButton).click();
|
||||
cy.wait("@signup").then((interception) => {
|
||||
expect(interception.response.statusCode).to.eq(201);
|
||||
});
|
||||
|
||||
onboardUserFromAppLink(data.email, data.slug, data.workspaceName, false);
|
||||
cy.get(commonWidgetSelector.draggableWidget("text1")).should(
|
||||
"be.visible"
|
||||
);
|
||||
});
|
||||
|
||||
cy.get(commonSelectors.workspaceName).verifyVisibleElement(
|
||||
"have.text",
|
||||
"My workspace"
|
||||
);
|
||||
it("Should verify restricted app access", () => {
|
||||
data.workspaceName = fake.firstName;
|
||||
data.workspaceSlug = fake.firstName.toLowerCase().replace(/\s+/g, "-");
|
||||
|
||||
// Test signup flow
|
||||
cy.intercept("POST", "/api/onboarding/signup").as("signup");
|
||||
cy.get(commonSelectors.createAnAccountLink).click();
|
||||
cy.wait(3000);
|
||||
cy.clearAndType(commonSelectors.inputFieldFullName, data.firstName);
|
||||
cy.clearAndType(commonSelectors.inputFieldEmailAddress, data.email);
|
||||
cy.clearAndType(onboardingSelectors.loginPasswordInput, "password");
|
||||
cy.get(commonSelectors.signUpButton).click();
|
||||
cy.apiCreateWorkspace(data.workspaceName, data.workspaceSlug);
|
||||
cy.apiLogout();
|
||||
cy.apiLogin();
|
||||
cy.visit(`${data.workspaceSlug}`);
|
||||
cy.apiDeleteGranularPermission("end-user");
|
||||
setSignupStatus(true, data.workspaceName);
|
||||
|
||||
cy.wait('@signup').then((interception) => {
|
||||
expect(interception.response.statusCode).to.eq(201);
|
||||
setupAppWithSlug(data.appName, data.slug);
|
||||
|
||||
inviteUserToWorkspace(data.firstName, data.email);
|
||||
|
||||
// Verify restricted access
|
||||
cy.visitSlug({
|
||||
actualUrl: `${Cypress.config("baseUrl")}/applications/${data.slug}`,
|
||||
});
|
||||
verifyRestrictedAccess();
|
||||
cy.get('[data-cy="back-to-home-button"]').click();
|
||||
cy.get(commonSelectors.homePageLogo).should("be.visible");
|
||||
|
||||
cy.apiLogout();
|
||||
});
|
||||
|
||||
// Process invitation
|
||||
onboardUserFromAppLink(data.email, data.slug);
|
||||
it.skip("Should verify private app access for different workspace users", () => {
|
||||
const firstName1 = fake.firstName;
|
||||
const email1 = fake.email.toLowerCase();
|
||||
const permissionName = fake.firstName.toLowerCase(); // Defined but not used in original
|
||||
const urls = {
|
||||
editor: `${Cypress.config("baseUrl")}/my-workspace/apps/${data.slug}/home`,
|
||||
preview: `${Cypress.config("baseUrl")}/applications/${data.slug}/home?version=v1`,
|
||||
released: `${Cypress.config("baseUrl")}/applications/${data.slug}`,
|
||||
};
|
||||
|
||||
cy.get(commonWidgetSelector.draggableWidget("text1")).should("be.visible");
|
||||
// Setup workspace and app
|
||||
cy.apiCreateWorkspace(data.workspaceName, data.workspaceSlug);
|
||||
cy.apiLogout();
|
||||
cy.apiLogin();
|
||||
cy.visit(`${data.workspaceSlug}`);
|
||||
setupAppWithSlug(data.appName, data.slug);
|
||||
|
||||
// Invite workspace user
|
||||
addNewUser(data.firstName, data.email);
|
||||
cy.wait(500);
|
||||
|
||||
cy.get('[data-cy="viewer-page-logo"]').click();
|
||||
logout();
|
||||
cy.wait(1000);
|
||||
cy.get(onboardingSelectors.signInButton, { timeout: 20000 }).should("be.visible");
|
||||
// Verify access restrictions
|
||||
cy.visitSlug({ actualUrl: urls.editor });
|
||||
verifyRestrictedAccess();
|
||||
cy.get('[data-cy="back-to-home-button"]').click();
|
||||
cy.get(commonSelectors.homePageLogo).should("be.visible");
|
||||
|
||||
// Setup new workspace and app
|
||||
cy.defaultWorkspaceLogin();
|
||||
cy.apiCreateWorkspace(data.workspaceName, data.workspaceSlug);
|
||||
cy.apiLogout();
|
||||
cy.apiLogin();
|
||||
cy.visit(`${data.workspaceSlug}`);
|
||||
setSignupStatus(true, data.workspaceName);
|
||||
cy.visitSlug({ actualUrl: urls.preview });
|
||||
|
||||
data.slug = fake.firstName.toLowerCase().replace(/\s+/g, "-");
|
||||
// Switch users and verify access
|
||||
cy.apiLogout();
|
||||
cy.apiLogin();
|
||||
cy.apiDeleteGranularPermission("end-user");
|
||||
|
||||
cy.createApp(data.appName);
|
||||
cy.dragAndDropWidget("Text", 500, 500);
|
||||
releaseApp();
|
||||
setUpSlug(data.slug);
|
||||
cy.forceClickOnCanvas();
|
||||
cy.backToApps();
|
||||
cy.apiLogin(data.email, "password");
|
||||
cy.visitSlug({ actualUrl: urls.editor });
|
||||
verifyRestrictedAccess();
|
||||
cy.get('[data-cy="back-to-home-button"]').click();
|
||||
cy.get(commonSelectors.homePageLogo).should("be.visible");
|
||||
cy.visitSlug({ actualUrl: urls.preview });
|
||||
|
||||
// Test signup flow in new workspace
|
||||
cy.apiLogout();
|
||||
cy.visitSlug({
|
||||
actualUrl: `${Cypress.config("baseUrl")}/applications/${data.slug}`,
|
||||
cy.apiLogout();
|
||||
|
||||
// Test with new user
|
||||
userSignUp(firstName1, email1, data.workspaceName);
|
||||
cy.visitSlug({ actualUrl: urls.editor });
|
||||
cy.visitSlug({ actualUrl: urls.preview });
|
||||
cy.visitSlug({ actualUrl: urls.released });
|
||||
});
|
||||
|
||||
cy.get(commonSelectors.workspaceName).verifyVisibleElement(
|
||||
"have.text",
|
||||
data.workspaceName
|
||||
);
|
||||
|
||||
cy.get(commonSelectors.createAnAccountLink).click();
|
||||
cy.wait(3000);
|
||||
cy.clearAndType(commonSelectors.inputFieldFullName, data.firstName);
|
||||
cy.clearAndType(commonSelectors.inputFieldEmailAddress, data.email);
|
||||
cy.clearAndType(onboardingSelectors.loginPasswordInput, "password");
|
||||
cy.get(commonSelectors.signUpButton).click();
|
||||
cy.wait('@signup').then((interception) => {
|
||||
expect(interception.response.statusCode).to.eq(201);
|
||||
});
|
||||
|
||||
onboardUserFromAppLink(data.email, data.slug, data.workspaceName, false);
|
||||
cy.get(commonWidgetSelector.draggableWidget("text1")).should("be.visible");
|
||||
|
||||
|
||||
});
|
||||
|
||||
it("Should verify restricted app access", () => {
|
||||
data.workspaceName = fake.firstName;
|
||||
data.workspaceSlug = fake.firstName.toLowerCase().replace(/\s+/g, "-");
|
||||
|
||||
cy.apiCreateWorkspace(data.workspaceName, data.workspaceSlug);
|
||||
cy.apiLogout();
|
||||
cy.apiLogin();
|
||||
cy.visit(`${data.workspaceSlug}`);
|
||||
cy.apiDeleteGranularPermission("end-user");
|
||||
setSignupStatus(true, data.workspaceName);
|
||||
|
||||
setupAppWithSlug(data.appName, data.slug);
|
||||
|
||||
inviteUserToWorkspace(data.firstName, data.email);
|
||||
|
||||
// Verify restricted access
|
||||
cy.visitSlug({
|
||||
actualUrl: `${Cypress.config("baseUrl")}/applications/${data.slug}`,
|
||||
});
|
||||
verifyRestrictedAccess();
|
||||
cy.get('[data-cy="back-to-home-button"]').click();
|
||||
cy.get(commonSelectors.homePageLogo).should("be.visible");
|
||||
|
||||
cy.apiLogout();
|
||||
});
|
||||
|
||||
it.skip("Should verify private app access for different workspace users", () => {
|
||||
const firstName1 = fake.firstName;
|
||||
const email1 = fake.email.toLowerCase();
|
||||
const permissionName = fake.firstName.toLowerCase(); // Defined but not used in original
|
||||
const urls = {
|
||||
editor: `${Cypress.config("baseUrl")}/my-workspace/apps/${data.slug}/home`,
|
||||
preview: `${Cypress.config("baseUrl")}/applications/${data.slug}/home?version=v1`,
|
||||
released: `${Cypress.config("baseUrl")}/applications/${data.slug}`
|
||||
};
|
||||
|
||||
// Setup workspace and app
|
||||
cy.apiCreateWorkspace(data.workspaceName, data.workspaceSlug);
|
||||
cy.apiLogout();
|
||||
cy.apiLogin();
|
||||
cy.visit(`${data.workspaceSlug}`);
|
||||
setupAppWithSlug(data.appName, data.slug);
|
||||
|
||||
// Invite workspace user
|
||||
addNewUser(data.firstName, data.email);
|
||||
cy.wait(500);
|
||||
|
||||
// Verify access restrictions
|
||||
cy.visitSlug({ actualUrl: urls.editor });
|
||||
verifyRestrictedAccess();
|
||||
cy.get('[data-cy="back-to-home-button"]').click();
|
||||
cy.get(commonSelectors.homePageLogo).should("be.visible");
|
||||
|
||||
cy.visitSlug({ actualUrl: urls.preview });
|
||||
|
||||
// Switch users and verify access
|
||||
cy.apiLogout();
|
||||
cy.apiLogin();
|
||||
cy.apiDeleteGranularPermission("end-user");
|
||||
|
||||
cy.apiLogin(data.email, "password");
|
||||
cy.visitSlug({ actualUrl: urls.editor });
|
||||
verifyRestrictedAccess();
|
||||
cy.get('[data-cy="back-to-home-button"]').click();
|
||||
cy.get(commonSelectors.homePageLogo).should("be.visible");
|
||||
cy.visitSlug({ actualUrl: urls.preview });
|
||||
|
||||
cy.apiLogout();
|
||||
|
||||
// Test with new user
|
||||
userSignUp(firstName1, email1, data.workspaceName);
|
||||
cy.visitSlug({ actualUrl: urls.editor });
|
||||
cy.visitSlug({ actualUrl: urls.preview });
|
||||
cy.visitSlug({ actualUrl: urls.released });
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
|
|
|
|||
|
|
@ -50,9 +50,6 @@ describe("Login functionality", () => {
|
|||
|
||||
it("Should be able to login with valid credentials", () => {
|
||||
cy.appUILogin(user.email, user.password);
|
||||
if (envVar === "Enterprise") {
|
||||
cy.get(".btn-close").click();
|
||||
}
|
||||
cy.get(commonSelectors.settingsIcon).click();
|
||||
cy.get(dashboardSelector.logoutLink);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -15,11 +15,19 @@ import {
|
|||
} from "Support/utils/selfHostSignUp";
|
||||
import { onboardingSelectors } from "Selectors/onboarding";
|
||||
import { logout } from "Support/utils/common";
|
||||
import { enableInstanceSignup } from "Support/utils/manageSSO";
|
||||
|
||||
describe("User signup", () => {
|
||||
const data = {};
|
||||
let invitationLink = "";
|
||||
|
||||
before(() => {
|
||||
cy.ifEnv("Enterprise", () => {
|
||||
enableInstanceSignup()
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
it("Verify the signup flow and UI elements", () => {
|
||||
data.fullName = fake.fullName;
|
||||
data.email = fake.email.toLowerCase().replaceAll("[^A-Za-z]", "");
|
||||
|
|
|
|||
|
|
@ -23,6 +23,8 @@ import {
|
|||
import { groupsSelector } from "Selectors/manageGroups";
|
||||
import { groupsText } from "Texts/manageGroups";
|
||||
import { onboardingSelectors } from "Selectors/onboarding";
|
||||
import { enableInstanceSignup } from "Support/utils/manageSSO";
|
||||
|
||||
|
||||
let invitationToken,
|
||||
organizationToken,
|
||||
|
|
@ -36,9 +38,9 @@ const envVar = Cypress.env("environment");
|
|||
describe("user invite flow cases", () => {
|
||||
beforeEach(() => {
|
||||
cy.defaultWorkspaceLogin();
|
||||
if (envVar === "Enterprise") {
|
||||
cy.get(".btn-close").click();
|
||||
}
|
||||
cy.ifEnv("Enterprise", () => {
|
||||
enableInstanceSignup()
|
||||
});
|
||||
});
|
||||
|
||||
it("Should verify the Manage users page", () => {
|
||||
|
|
|
|||
|
|
@ -1,59 +1,126 @@
|
|||
import { commonSelectors } from "Selectors/common";
|
||||
import { usersText } from "Texts/manageUsers";
|
||||
import { usersSelector } from "Selectors/manageUsers";
|
||||
import { groupsSelector } from "Selectors/manageGroups";
|
||||
import { fake } from "Fixtures/fake";
|
||||
import * as common from "Support/utils/common";
|
||||
import { bulkUserUpload } from "Support/utils/manageUsers";
|
||||
|
||||
// Helper to resolve correct test data based on env
|
||||
const getFile = (fileGroup) => {
|
||||
const env = Cypress.env("environment");
|
||||
return env === "Community" ? fileGroup.default : fileGroup.alt;
|
||||
};
|
||||
|
||||
|
||||
describe("Bulk User Upload", () => {
|
||||
// Test data configuration
|
||||
const TEST_FILES = {
|
||||
MISSING_NAME: {
|
||||
path: "cypress/fixtures/bulkUser/without_name.csv",
|
||||
fileName: "without_name",
|
||||
error:
|
||||
"Missing first_name,last_name,groups information in 2 row(s);. No users were uploaded, please update and try again.",
|
||||
default: {
|
||||
path: "cypress/fixtures/bulkUser/missing_name.csv",
|
||||
fileName: "missing_name",
|
||||
error:
|
||||
"Missing first_name,last_name,groups information in 2 row(s);. No users were uploaded, please update and try again.",
|
||||
},
|
||||
alt: {
|
||||
path: "cypress/fixtures/bulkUser/missing_name_ee.csv",
|
||||
fileName: "missing_name_ee",
|
||||
error:
|
||||
"Missing first_name,last_name,groups,metadata,userMetadata information in 2 row(s);. No users were uploaded, please update and try again.",
|
||||
},
|
||||
},
|
||||
MISSING_EMAIL: {
|
||||
path: "cypress/fixtures/bulkUser/without_email.csv",
|
||||
fileName: "without_email",
|
||||
error:
|
||||
"Missing email,groups information in 2 row(s);. No users were uploaded, please update and try again.",
|
||||
default: {
|
||||
path: "cypress/fixtures/bulkUser/missing_email.csv",
|
||||
fileName: "missing_email",
|
||||
error:
|
||||
"Missing email,groups information in 2 row(s);. No users were uploaded, please update and try again.",
|
||||
},
|
||||
alt: {
|
||||
path: "cypress/fixtures/bulkUser/missing_email_ee.csv",
|
||||
fileName: "missing_email_ee",
|
||||
error:
|
||||
"Missing first_name,last_name,groups,metadata,userMetadata information in 2 row(s);. No users were uploaded, please update and try again.",
|
||||
},
|
||||
},
|
||||
DUPLICATE_EMAIL: {
|
||||
path: "cypress/fixtures/bulkUser/same_email.csv",
|
||||
fileName: "same_email",
|
||||
error: "Duplicate email found. Please provide a unique email address.",
|
||||
isDuplicate: true,
|
||||
default: {
|
||||
path: "cypress/fixtures/bulkUser/same_email.csv",
|
||||
fileName: "same_email",
|
||||
error: "Duplicate email found. Please provide a unique email address.",
|
||||
isDuplicate: true,
|
||||
},
|
||||
alt: {
|
||||
path: "cypress/fixtures/bulkUser/same_email_ee.csv",
|
||||
fileName: "same_email_ee",
|
||||
error: "Duplicate email found. Please provide a unique email address.",
|
||||
isDuplicate: true,
|
||||
},
|
||||
},
|
||||
EMPTY_NAMES: {
|
||||
path: "cypress/fixtures/bulkUser/empty_first_and_last_name.csv",
|
||||
fileName: "empty_first_and_last_name",
|
||||
error:
|
||||
"Missing first_name,last_name,groups information in 1 row(s);. No users were uploaded, please update and try again.",
|
||||
default: {
|
||||
path: "cypress/fixtures/bulkUser/empty_names.csv",
|
||||
fileName: "empty_names",
|
||||
error:
|
||||
"Missing first_name,last_name,groups information in 1 row(s);. No users were uploaded, please update and try again.",
|
||||
},
|
||||
alt: {
|
||||
path: "cypress/fixtures/bulkUser/empty_names_ee.csv",
|
||||
fileName: "empty_names_ee",
|
||||
error:
|
||||
"Missing first_name,last_name,groups,metadata,userMetadata information in 1 row(s);. No users were uploaded, please update and try again.",
|
||||
},
|
||||
},
|
||||
LIMIT_EXCEEDED: {
|
||||
path: "cypress/fixtures/bulkUser/500_invite_users.csv",
|
||||
fileName: "500_invite_users",
|
||||
error: "You can only invite 250 users at a time",
|
||||
default: {
|
||||
path: "cypress/fixtures/bulkUser/limit_exceeded.csv",
|
||||
fileName: "limit_exceeded",
|
||||
error: "You can only invite 250 users at a time",
|
||||
},
|
||||
alt: {
|
||||
path: "cypress/fixtures/bulkUser/limit_exceeded_ee.csv",
|
||||
fileName: "limit_exceeded_ee",
|
||||
error: "You can only invite 250 users at a time",
|
||||
},
|
||||
},
|
||||
MISSING_ROLE: {
|
||||
path: "cypress/fixtures/bulkUser/without_role.csv",
|
||||
fileName: "without_role",
|
||||
error:
|
||||
"Missing user_role,groups information in 2 row(s);. No users were uploaded, please update and try again.",
|
||||
default: {
|
||||
path: "cypress/fixtures/bulkUser/missing_role.csv",
|
||||
fileName: "missing_role",
|
||||
error:
|
||||
"Missing user_role,groups information in 2 row(s);. No users were uploaded, please update and try again.",
|
||||
},
|
||||
alt: {
|
||||
path: "cypress/fixtures/bulkUser/missing_role_ee.csv",
|
||||
fileName: "missing_role_ee",
|
||||
error:
|
||||
"Missing user_role,groups,metadata,userMetadata information in 2 row(s);. No users were uploaded, please update and try again.",
|
||||
},
|
||||
},
|
||||
NONEXISTENT_GROUP: {
|
||||
path: "cypress/fixtures/bulkUser/non_existing_group.csv",
|
||||
fileName: "non_existing_group",
|
||||
error: "2 groups doesn't exist. No users were uploaded",
|
||||
default: {
|
||||
path: "cypress/fixtures/bulkUser/non_existing_group.csv",
|
||||
fileName: "non_existing_group",
|
||||
error: "2 groups doesn't exist. No users were uploaded",
|
||||
},
|
||||
alt: {
|
||||
path: "cypress/fixtures/bulkUser/non_existing_group_ee.csv",
|
||||
fileName: "non_existing_group_ee",
|
||||
error: "2 groups doesn't exist. No users were uploaded",
|
||||
},
|
||||
},
|
||||
VALID_USERS: {
|
||||
path: "cypress/fixtures/bulkUser/3usersupload.csv",
|
||||
fileName: "3usersupload",
|
||||
testEmail: "test12@gmail.com",
|
||||
successMessage: "3 users are being added",
|
||||
default: {
|
||||
path: "cypress/fixtures/bulkUser/3_users_upload.csv",
|
||||
fileName: "3_users_upload",
|
||||
successMessage: "3 users are being added",
|
||||
email: "test12@gmail.com",
|
||||
},
|
||||
alt: {
|
||||
path: "cypress/fixtures/bulkUser/3_users_upload_ee.csv",
|
||||
fileName: "3_users_upload_ee",
|
||||
successMessage: "3 users are being added",
|
||||
email: "test12@gmail.com",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
|
|
@ -70,7 +137,6 @@ describe("Bulk User Upload", () => {
|
|||
cy.get(usersSelector.buttonAddUsers).click();
|
||||
cy.get(usersSelector.buttonUploadCsvFile).click();
|
||||
|
||||
// Test all error cases
|
||||
[
|
||||
TEST_FILES.MISSING_ROLE,
|
||||
TEST_FILES.MISSING_NAME,
|
||||
|
|
@ -79,7 +145,8 @@ describe("Bulk User Upload", () => {
|
|||
TEST_FILES.EMPTY_NAMES,
|
||||
TEST_FILES.NONEXISTENT_GROUP,
|
||||
TEST_FILES.LIMIT_EXCEEDED,
|
||||
].forEach((testCase) => {
|
||||
].forEach((testCaseGroup) => {
|
||||
const testCase = getFile(testCaseGroup);
|
||||
bulkUserUpload(
|
||||
testCase.path,
|
||||
testCase.fileName,
|
||||
|
|
@ -90,32 +157,30 @@ describe("Bulk User Upload", () => {
|
|||
});
|
||||
|
||||
it("Should successfully upload valid users", () => {
|
||||
const file = getFile(TEST_FILES.VALID_USERS);
|
||||
cy.get(usersSelector.buttonAddUsers).click();
|
||||
cy.get(usersSelector.buttonUploadCsvFile).click();
|
||||
|
||||
cy.get(usersSelector.inputFieldBulkUpload).selectFile(
|
||||
TEST_FILES.VALID_USERS.path,
|
||||
{
|
||||
force: true,
|
||||
}
|
||||
);
|
||||
cy.get(commonSelectors.fileSelector).should(
|
||||
"contain",
|
||||
TEST_FILES.VALID_USERS.fileName
|
||||
);
|
||||
cy.get(usersSelector.inputFieldBulkUpload).selectFile(file.path, {
|
||||
force: true,
|
||||
});
|
||||
|
||||
cy.get(commonSelectors.fileSelector).should("contain", file.fileName);
|
||||
cy.get(usersSelector.buttonUploadUsers).click();
|
||||
cy.get(".go2072408551")
|
||||
.should("be.visible")
|
||||
.and("have.text", TEST_FILES.VALID_USERS.successMessage);
|
||||
common.searchUser("test12@gmail.com");
|
||||
cy.contains("td", "test12@gmail.com")
|
||||
.and("have.text", file.successMessage);
|
||||
|
||||
common.searchUser(file.email);
|
||||
cy.contains("td", file.email)
|
||||
.parent()
|
||||
.within(() => {
|
||||
cy.get("td small").should("have.text", "invited");
|
||||
});
|
||||
|
||||
common.navigateToManageGroups();
|
||||
cy.get(groupsSelector.groupLink("Admin")).click();
|
||||
cy.get(groupsSelector.usersLink).click();
|
||||
cy.contains("test12@gmail.com").should("be.visible");
|
||||
cy.contains(file.email).should("be.visible");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import {
|
|||
} from "Support/utils/common";
|
||||
|
||||
import { onboardingSelectors } from "Selectors/onboarding";
|
||||
import { enableInstanceSignup } from "Support/utils/manageSSO";
|
||||
|
||||
const data = {};
|
||||
const envVar = Cypress.env("environment");
|
||||
|
|
@ -28,9 +29,9 @@ const envVar = Cypress.env("environment");
|
|||
describe("inviteflow edge cases", () => {
|
||||
beforeEach(() => {
|
||||
cy.defaultWorkspaceLogin();
|
||||
if (envVar === "Enterprise") {
|
||||
cy.get(".btn-close").click();
|
||||
}
|
||||
cy.ifEnv("Enterprise", () => {
|
||||
enableInstanceSignup();
|
||||
});
|
||||
});
|
||||
|
||||
it("Should verify exisiting user invite flow", () => {
|
||||
|
|
@ -69,55 +70,6 @@ describe("inviteflow edge cases", () => {
|
|||
});
|
||||
});
|
||||
|
||||
it("should verify the user signup after invited in a workspace", () => {
|
||||
data.firstName = fake.firstName;
|
||||
data.email = fake.email.toLowerCase().replaceAll("[^A-Za-z]", "");
|
||||
data.signUpName = fake.firstName;
|
||||
data.workspaceName = fake.companyName;
|
||||
|
||||
enableInstanceSignUp();
|
||||
setSignupStatus(true);
|
||||
navigateToManageUsers();
|
||||
fillUserInviteForm(data.firstName, data.email);
|
||||
cy.get(usersSelector.buttonInviteUsers).click();
|
||||
cy.apiLogout();
|
||||
|
||||
cy.visit("/");
|
||||
cy.get(commonSelectors.createAnAccountLink).click();
|
||||
SignUpPageElements();
|
||||
cy.wait(3000);
|
||||
cy.clearAndType(onboardingSelectors.nameInput, data.signUpName);
|
||||
cy.clearAndType(onboardingSelectors.signupEmailInput, data.email);
|
||||
cy.clearAndType(
|
||||
onboardingSelectors.loginPasswordInput,
|
||||
commonText.password
|
||||
);
|
||||
cy.get(commonSelectors.signUpButton).click();
|
||||
cy.wait(1000);
|
||||
signUpLink(data.email);
|
||||
if (envVar === "Enterprise") {
|
||||
verifyOnboardingQuestions(data.workspaceName);
|
||||
cy.wait(1000);
|
||||
cy.get(commonSelectors.skipbutton).click();
|
||||
cy.backToApps();
|
||||
}
|
||||
cy.wait(1000);
|
||||
visitWorkspaceInvitation(data.email, "My workspace");
|
||||
cy.clearAndType(onboardingSelectors.signupEmailInput, data.email);
|
||||
cy.clearAndType(onboardingSelectors.loginPasswordInput, usersText.password);
|
||||
cy.get(onboardingSelectors.signInButton).click();
|
||||
cy.wait(3000);
|
||||
cy.get(commonSelectors.invitedUserName).verifyVisibleElement(
|
||||
"have.text",
|
||||
data.signUpName
|
||||
);
|
||||
cy.get(commonSelectors.acceptInviteButton).click();
|
||||
cy.get(commonSelectors.workspaceName).verifyVisibleElement(
|
||||
"have.text",
|
||||
"My workspace"
|
||||
);
|
||||
});
|
||||
|
||||
it("should verify the user signup with same creds after invited in a workspace", () => {
|
||||
data.firstName = fake.firstName;
|
||||
data.email = fake.email.toLowerCase().replaceAll("[^A-Za-z]", "");
|
||||
|
|
|
|||
|
|
@ -0,0 +1,250 @@
|
|||
import { commonSelectors } from "Selectors/common";
|
||||
import { commonEeSelectors, ssoEeSelector } from "Selectors/eeCommon";
|
||||
import { ssoEeText } from "Texts/eeCommon";
|
||||
import { setSSOStatus, setSignupStatus } from "Support/utils/manageSSO";
|
||||
import { usersText } from "Texts/manageUsers";
|
||||
import { fake } from "Fixtures/fake";
|
||||
import {
|
||||
logout,
|
||||
navigateToManageSSO,
|
||||
navigateToManageUsers,
|
||||
searchUser,
|
||||
pinInspector,
|
||||
navigateToAppEditor,
|
||||
navigateToManageGroups,
|
||||
} from "Support/utils/common";
|
||||
import { ssoText } from "Texts/manageSSO";
|
||||
import { enableToggle, disableToggle } from "Support/utils/platform/eeCommon";
|
||||
import { setupAndUpdateRole } from "Support/utils/manageGroups";
|
||||
|
||||
describe("LDAP flow", () => {
|
||||
const TEST_DATA = {
|
||||
appName: `${fake.companyName} App`,
|
||||
ldapUser: {
|
||||
username: "Hubert J. Farnsworth",
|
||||
password: "professor",
|
||||
email: "professor@planetexpress.com",
|
||||
},
|
||||
ldapConfig: {
|
||||
name: "Tooljet LDAP Auth",
|
||||
host: Cypress.env("ldap_host"),
|
||||
port: "10389",
|
||||
baseDn: Cypress.env("ldap_base_dn"),
|
||||
},
|
||||
};
|
||||
|
||||
const ldapLogin = (
|
||||
username = TEST_DATA.ldapUser.username,
|
||||
password = TEST_DATA.ldapUser.password
|
||||
) => {
|
||||
cy.get(ssoEeSelector.ldapSSOText).click();
|
||||
cy.clearAndType(commonSelectors.inputFieldName, username);
|
||||
cy.clearAndType(ssoEeSelector.passwordInputField, password);
|
||||
cy.get(commonSelectors.signUpButton).click();
|
||||
};
|
||||
|
||||
const toggleUserArchiveStatus = (shouldArchive = true) => {
|
||||
navigateToManageUsers();
|
||||
searchUser(TEST_DATA.ldapUser.email);
|
||||
cy.get('[data-cy="user-actions-button"]').click();
|
||||
cy.get('[data-cy="archive-button"]').click();
|
||||
|
||||
const expectedToast = shouldArchive
|
||||
? usersText.archivedToast
|
||||
: usersText.unarchivedToast;
|
||||
cy.verifyToastMessage(commonSelectors.toastMessage, expectedToast);
|
||||
|
||||
if (shouldArchive) {
|
||||
cy.contains("td", TEST_DATA.ldapUser.email)
|
||||
.parent()
|
||||
.within(() => {
|
||||
cy.get("td small").should("have.text", usersText.archivedStatus);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
cy.visit("/");
|
||||
cy.appUILogin();
|
||||
});
|
||||
|
||||
it("Verify complete LDAP flow: UI, user onboarding, inspector SSO info, and archive functionality", () => {
|
||||
cy.intercept("GET", "api/library_apps").as("apps");
|
||||
|
||||
// ========== SECTION 1: LDAP Configuration and UI Verification ==========
|
||||
setSSOStatus("My workspace", "ldap", false);
|
||||
navigateToManageSSO();
|
||||
cy.wait(1000);
|
||||
|
||||
cy.get('[data-cy="ldap-sso-card"]')
|
||||
.verifyVisibleElement("have.text", "LDAP")
|
||||
.click();
|
||||
|
||||
cy.get(ssoEeSelector.ldapToggle).should("be.visible");
|
||||
|
||||
for (const element in ssoEeSelector.ldapPageElements) {
|
||||
cy.get(ssoEeSelector.ldapPageElements[element]).verifyVisibleElement(
|
||||
"have.text",
|
||||
ssoEeText.ldapPageElements[element]
|
||||
);
|
||||
}
|
||||
|
||||
const formElements = [
|
||||
ssoEeSelector.statusLabel,
|
||||
ssoEeSelector.nameInput,
|
||||
ssoEeSelector.hostInput,
|
||||
ssoEeSelector.portInput,
|
||||
ssoEeSelector.baseDnInput,
|
||||
ssoEeSelector.sslToggleInput,
|
||||
];
|
||||
|
||||
formElements.forEach((selector) => {
|
||||
cy.get(selector).should("be.visible");
|
||||
});
|
||||
|
||||
cy.get(commonSelectors.cancelButton)
|
||||
.eq(1)
|
||||
.verifyVisibleElement("have.text", "Cancel");
|
||||
cy.get(commonEeSelectors.saveButton)
|
||||
.eq(1)
|
||||
.verifyVisibleElement("have.text", "Save changes");
|
||||
|
||||
enableToggle(ssoEeSelector.sslToggleInput);
|
||||
cy.get(ssoEeSelector.ldapPageElements.sslLabel)
|
||||
.eq(1)
|
||||
.verifyVisibleElement("have.text", "SSL certificate");
|
||||
cy.get(".css-1x65k0v-control").should("be.visible");
|
||||
|
||||
cy.clearAndType(ssoEeSelector.nameInput, TEST_DATA.ldapConfig.name);
|
||||
cy.clearAndType(ssoEeSelector.hostInput, TEST_DATA.ldapConfig.host);
|
||||
cy.clearAndType(ssoEeSelector.portInput, TEST_DATA.ldapConfig.port);
|
||||
cy.clearAndType(ssoEeSelector.baseDnInput, TEST_DATA.ldapConfig.baseDn);
|
||||
|
||||
cy.get(ssoEeSelector.sslToggleInput).uncheck();
|
||||
cy.get(ssoEeSelector.ldapToggle).click();
|
||||
disableToggle(ssoEeSelector.sslToggleInput);
|
||||
|
||||
cy.get(commonEeSelectors.saveButton).eq(1).click();
|
||||
cy.verifyToastMessage(
|
||||
commonSelectors.toastMessage,
|
||||
ssoText.toggleUpdateToast("LDAP")
|
||||
);
|
||||
cy.get(commonSelectors.cancelButton).eq(1).click();
|
||||
logout();
|
||||
|
||||
// ========== SECTION 2: LDAP Login Page and User Onboarding ==========
|
||||
cy.get(ssoEeSelector.ldapSSOText).verifyVisibleElement(
|
||||
"have.text",
|
||||
ssoEeText.ldapSSOText
|
||||
);
|
||||
|
||||
cy.get(ssoEeSelector.ldapSSOText).click();
|
||||
|
||||
const loginPageElements = [
|
||||
{ selector: '[data-cy="key-logo"]', assertion: "be.visible" },
|
||||
{
|
||||
selector: ssoEeSelector.userNameInputLabel,
|
||||
text: ssoEeText.userNameInputLabel,
|
||||
},
|
||||
{ selector: commonSelectors.inputFieldName, assertion: "be.visible" },
|
||||
{ selector: ssoEeSelector.passwordInputLabel, text: "Password" },
|
||||
{ selector: ssoEeSelector.passwordInputField, assertion: "be.visible" },
|
||||
{ selector: commonSelectors.signUpButton, text: "Sign in" },
|
||||
];
|
||||
|
||||
loginPageElements.forEach((element) => {
|
||||
if (element.text) {
|
||||
cy.get(element.selector).verifyVisibleElement(
|
||||
"have.text",
|
||||
element.text
|
||||
);
|
||||
} else {
|
||||
cy.get(element.selector).should(element.assertion);
|
||||
}
|
||||
});
|
||||
|
||||
// Test failed login (user doesn't exist in workspace)
|
||||
cy.clearAndType(
|
||||
commonSelectors.inputFieldName,
|
||||
TEST_DATA.ldapUser.username
|
||||
);
|
||||
cy.clearAndType(
|
||||
ssoEeSelector.passwordInputField,
|
||||
TEST_DATA.ldapUser.password
|
||||
);
|
||||
cy.get(commonSelectors.signUpButton).click();
|
||||
cy.verifyToastMessage(
|
||||
commonSelectors.toastMessage,
|
||||
"LDAP login failed - User does not exist in the workspace"
|
||||
);
|
||||
|
||||
cy.defaultWorkspaceLogin();
|
||||
setSignupStatus(true);
|
||||
logout();
|
||||
|
||||
ldapLogin();
|
||||
cy.get(commonSelectors.pageSectionHeader).verifyVisibleElement(
|
||||
"have.text",
|
||||
"Applications"
|
||||
);
|
||||
logout();
|
||||
|
||||
// ========== SECTION 3: Setup App and User Permissions for Inspector Test ==========
|
||||
cy.defaultWorkspaceLogin();
|
||||
cy.apiCreateApp(TEST_DATA.appName);
|
||||
|
||||
navigateToManageGroups();
|
||||
setupAndUpdateRole("End-user", "Builder", TEST_DATA.ldapUser.email);
|
||||
logout();
|
||||
|
||||
// ========== SECTION 4: Verify SSO User Info in Inspector ==========
|
||||
ldapLogin();
|
||||
|
||||
cy.wait("@apps");
|
||||
cy.wait(1000);
|
||||
|
||||
navigateToAppEditor(TEST_DATA.appName);
|
||||
pinInspector();
|
||||
|
||||
const inspectorPath = [
|
||||
'[data-cy="inspector-node-globals"] > .node-key',
|
||||
'[data-cy="inspector-node-currentuser"] > .node-key',
|
||||
'[data-cy="inspector-node-ssouserinfo"] > .node-key',
|
||||
'[data-cy="inspector-node-mail"] > .node-key',
|
||||
];
|
||||
|
||||
inspectorPath.forEach((selector) => cy.get(selector).click());
|
||||
|
||||
cy.get('[data-cy="inspector-node-0"] > .mx-2').verifyVisibleElement(
|
||||
"have.text",
|
||||
`"${TEST_DATA.ldapUser.email}"`
|
||||
);
|
||||
cy.backToApps();
|
||||
logout();
|
||||
|
||||
// ========== SECTION 5: Archive/Unarchive Functionality ==========
|
||||
cy.defaultWorkspaceLogin();
|
||||
|
||||
// Archive user and verify status
|
||||
toggleUserArchiveStatus(true);
|
||||
logout();
|
||||
|
||||
ldapLogin();
|
||||
cy.verifyToastMessage(
|
||||
commonSelectors.toastMessage,
|
||||
"LDAP login failed - User is archived in the workspace"
|
||||
);
|
||||
|
||||
// Unarchive user
|
||||
cy.go("back");
|
||||
cy.appUILogin();
|
||||
toggleUserArchiveStatus(false);
|
||||
logout();
|
||||
|
||||
ldapLogin();
|
||||
cy.get(commonSelectors.pageSectionHeader).verifyVisibleElement(
|
||||
"have.text",
|
||||
"Applications"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,239 @@
|
|||
import * as common from "Support/utils/common";
|
||||
import { ssoText } from "Texts/manageSSO";
|
||||
import {
|
||||
inviteUser,
|
||||
WorkspaceInvitationLink,
|
||||
} from "Support/utils/platform/eeCommon.js";
|
||||
import { commonSelectors } from "Selectors/common";
|
||||
import {
|
||||
commonEeSelectors,
|
||||
ssoEeSelector,
|
||||
instanceSettingsSelector,
|
||||
} from "Selectors/eeCommon";
|
||||
import { commonEeText } from "Texts/eeCommon";
|
||||
import {
|
||||
setSignupStatus,
|
||||
defaultSSO,
|
||||
deleteOrganisationSSO,
|
||||
} from "Support/utils/manageSSO";
|
||||
import { confirmInviteElements } from "Support/utils/manageUsers";
|
||||
import { usersText } from "Texts/manageUsers";
|
||||
import { usersSelector } from "Selectors/manageUsers";
|
||||
|
||||
import { fetchAndVisitInviteLink } from "Support/utils/manageUsers";
|
||||
import { enableInstanceSignup } from "Support/utils/manageSSO";
|
||||
|
||||
describe("Verify OIDC user onboarding", () => {
|
||||
const envVar = Cypress.env("environment");
|
||||
|
||||
beforeEach(() => {
|
||||
cy.defaultWorkspaceLogin();
|
||||
cy.intercept("GET", "api/library_apps").as("apps");
|
||||
cy.wait(2000);
|
||||
defaultSSO(true);
|
||||
});
|
||||
|
||||
it("Verify user onboarding using workspace OIDC", () => {
|
||||
deleteOrganisationSSO("My workspace", ["openid"]);
|
||||
common.navigateToManageSSO();
|
||||
defaultSSO(false);
|
||||
setSignupStatus(false);
|
||||
cy.wait(1000);
|
||||
|
||||
cy.get(ssoEeSelector.oidc).click();
|
||||
cy.get(ssoEeSelector.oidcToggle).click();
|
||||
cy.clearAndType(ssoEeSelector.nameInput, "Tooljet OIDC");
|
||||
cy.clearAndType(
|
||||
ssoEeSelector.clientIdInput,
|
||||
Cypress.env("SSO_OPENID_CLIENT_ID")
|
||||
);
|
||||
cy.clearAndType(
|
||||
ssoEeSelector.clientSecretInput,
|
||||
Cypress.env("SSO_OPENID_CLIENT_SECRET")
|
||||
);
|
||||
cy.clearAndType(
|
||||
ssoEeSelector.WellKnownUrlInput,
|
||||
Cypress.env("SSO_OPENID_WELL_KNOWN_URL")
|
||||
);
|
||||
cy.get(commonEeSelectors.saveButton).eq(1).click();
|
||||
cy.get('[data-cy="enable-button"]').click();
|
||||
cy.verifyToastMessage(
|
||||
commonSelectors.toastMessage,
|
||||
ssoText.toggleUpdateToast("OpenID")
|
||||
);
|
||||
|
||||
cy.apiLogout();
|
||||
cy.visit("/login/my-workspace");
|
||||
cy.get(ssoEeSelector.oidcSSOText).verifyVisibleElement(
|
||||
"have.text",
|
||||
"Sign in with Tooljet OIDC"
|
||||
);
|
||||
cy.get(ssoEeSelector.oidcSSOText).realClick();
|
||||
cy.get(".superadmin-button").click();
|
||||
|
||||
cy.verifyToastMessage(
|
||||
commonSelectors.toastMessage,
|
||||
"Open ID login failed - User does not exist in the workspace"
|
||||
);
|
||||
|
||||
cy.apiLogin();
|
||||
setSignupStatus(true);
|
||||
cy.apiLogout();
|
||||
|
||||
cy.visit("/login/my-workspace");
|
||||
cy.get(ssoEeSelector.oidcSSOText).realClick();
|
||||
cy.get(".superadmin-button").click();
|
||||
|
||||
common.logout();
|
||||
|
||||
cy.defaultWorkspaceLogin();
|
||||
common.navigateToManageUsers();
|
||||
common.searchUser("superadmin@tooljet.com");
|
||||
|
||||
cy.contains("td", "superadmin@tooljet.com")
|
||||
.parent()
|
||||
.within(() => {
|
||||
cy.get("td small").should("have.text", usersText.activeStatus);
|
||||
});
|
||||
|
||||
cy.apiLogout();
|
||||
cy.visit("/my-workspace");
|
||||
cy.get(ssoEeSelector.oidcSSOText).realClick();
|
||||
cy.get(".superadmin-button").click();
|
||||
});
|
||||
|
||||
it("Verify invited user onboarding using instance level OIDC", () => {
|
||||
setSignupStatus(true);
|
||||
|
||||
cy.ifEnv("Enterprise", () => {
|
||||
enableInstanceSignup();
|
||||
});
|
||||
|
||||
common.navigateToManageUsers();
|
||||
inviteUser("user", "user@tooljet.com");
|
||||
confirmInviteElements("user@tooljet.com");
|
||||
cy.wait(2000);
|
||||
cy.get(ssoEeSelector.oidcSSOText).realClick();
|
||||
cy.get(".user-button").click();
|
||||
cy.wait(1000);
|
||||
|
||||
cy.get(commonSelectors.acceptInviteButton).click();
|
||||
cy.wait("@apps");
|
||||
cy.contains("My workspace").should("be.visible");
|
||||
common.logout();
|
||||
|
||||
cy.defaultWorkspaceLogin();
|
||||
setSignupStatus(false);
|
||||
|
||||
common.navigateToManageUsers();
|
||||
cy.wait(500);
|
||||
inviteUser("user", "userthree@tooljet.com");
|
||||
cy.wait(2000);
|
||||
cy.get(ssoEeSelector.oidcSSOText).realClick();
|
||||
cy.get(".user-four-button").click();
|
||||
|
||||
cy.get(commonSelectors.toastMessage)
|
||||
.should("be.visible")
|
||||
.and(
|
||||
"have.text",
|
||||
"Open ID login failed - Invalid Email: Please use the email address provided in the invitation."
|
||||
);
|
||||
cy.wait(500);
|
||||
cy.defaultWorkspaceLogin();
|
||||
|
||||
setSignupStatus(true);
|
||||
fetchAndVisitInviteLink("userthree@tooljet.com");
|
||||
cy.wait(2000);
|
||||
cy.get(ssoEeSelector.oidcSSOText).realClick();
|
||||
cy.get(".user-four-button").click();
|
||||
cy.get(commonSelectors.toastMessage)
|
||||
.should("be.visible")
|
||||
.and(
|
||||
"have.text",
|
||||
"Open ID login failed - Invalid Email: Please use the email address provided in the invitation."
|
||||
);
|
||||
cy.get(ssoEeSelector.oidcSSOText).realClick();
|
||||
cy.get(".superadmin-button").click();
|
||||
cy.get(commonSelectors.toastMessage)
|
||||
.should("be.visible")
|
||||
.and(
|
||||
"have.text",
|
||||
"Open ID login failed - Invalid Email: Please use the email address provided in the invitation."
|
||||
);
|
||||
});
|
||||
|
||||
if (envVar === "Enterprise") {
|
||||
it("Verify user onboarding using instance level OIDC", () => {
|
||||
enableInstanceSignup();
|
||||
cy.apiLogout();
|
||||
|
||||
cy.visit("/");
|
||||
cy.get(ssoEeSelector.oidcSSOText).realClick();
|
||||
cy.get(".admin-button").click();
|
||||
cy.wait(3000);
|
||||
common.logout();
|
||||
|
||||
cy.defaultWorkspaceLogin();
|
||||
cy.get(commonSelectors.settingsIcon).click();
|
||||
cy.get(commonEeSelectors.instanceSettingIcon).click();
|
||||
cy.clearAndType(commonSelectors.inputUserSearch, "admin@tooljet.com");
|
||||
|
||||
cy.get(instanceSettingsSelector.userStatus("admin")).verifyVisibleElement(
|
||||
"have.text",
|
||||
usersText.activeStatus
|
||||
);
|
||||
|
||||
cy.apiLogout();
|
||||
cy.visit("/");
|
||||
cy.get(ssoEeSelector.oidcSSOText).realClick();
|
||||
cy.get(".admin-button").click();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
it("Verify archived user login using OIDC", () => {
|
||||
setSignupStatus(true);
|
||||
cy.ifEnv("Enterprise", () => {
|
||||
enableInstanceSignup();
|
||||
});
|
||||
common.navigateToManageUsers();
|
||||
cy.get(usersSelector.buttonAddUsers).click();
|
||||
cy.get(commonSelectors.inputFieldFullName).type("user two");
|
||||
cy.get(commonSelectors.inputFieldEmailAddress).type("usertwo@tooljet.com");
|
||||
cy.get(usersSelector.buttonInviteUsers).click();
|
||||
WorkspaceInvitationLink("usertwo@tooljet.com");
|
||||
|
||||
cy.wait(2000);
|
||||
cy.get(ssoEeSelector.oidcSSOText).realClick();
|
||||
cy.get(".user-two-button").click();
|
||||
|
||||
cy.get(commonSelectors.acceptInviteButton).click();
|
||||
cy.wait("@apps");
|
||||
cy.contains("My workspace").should("be.visible");
|
||||
common.logout();
|
||||
|
||||
cy.defaultWorkspaceLogin();
|
||||
common.navigateToManageUsers();
|
||||
common.searchUser("usertwo@tooljet.com");
|
||||
cy.get('[data-cy="user-actions-button"]').click();
|
||||
cy.get('[data-cy="archive-button"]').click();
|
||||
|
||||
cy.verifyToastMessage(
|
||||
commonSelectors.toastMessage,
|
||||
usersText.archivedToast
|
||||
);
|
||||
cy.get(instanceSettingsSelector.userStatus("user two"), {
|
||||
timeout: 9000,
|
||||
}).should("have.text", usersText.archivedStatus);
|
||||
cy.apiLogout();
|
||||
cy.visit("/my-workspace");
|
||||
cy.wait(2000);
|
||||
cy.get(ssoEeSelector.oidcSSOText).realClick();
|
||||
cy.get(".user-two-button").click();
|
||||
|
||||
cy.verifyToastMessage(
|
||||
commonSelectors.toastMessage,
|
||||
"Open ID login failed - User is archived in the workspace"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
@ -18,7 +18,7 @@ describe("Self host onboarding", () => {
|
|||
});
|
||||
|
||||
it("verify elements on self host onboarding page", () => {
|
||||
if (envVar === "Enterprise") {
|
||||
cy.ifEnv("Enterprise", () => {
|
||||
cy.get(commonSelectors.HostBanner).should("be.visible");
|
||||
cy.get(commonSelectors.pageLogo).should("be.visible");
|
||||
cy.get('[data-cy="welcome-to-tooljet!-header"]').verifyVisibleElement(
|
||||
|
|
@ -34,7 +34,7 @@ describe("Self host onboarding", () => {
|
|||
"Set up ToolJet"
|
||||
);
|
||||
cy.get('[data-cy="set-up-tooljet-button"]').click();
|
||||
}
|
||||
});
|
||||
|
||||
const commonElements = [
|
||||
{ selector: commonSelectors.HostBanner },
|
||||
|
|
@ -76,20 +76,22 @@ describe("Self host onboarding", () => {
|
|||
cy.get(check.selector).verifyVisibleElement("have.text", check.text);
|
||||
});
|
||||
|
||||
if (envVar === "Community") {
|
||||
cy.ifEnv("Community", () => {
|
||||
cy.get(commonSelectors.signUpTermsHelperText).should(($el) => {
|
||||
expect($el.contents().first().text().trim()).to.eq(
|
||||
// commonText.selfHostSignUpTermsHelperText
|
||||
"By signing up you are agreeing to the"
|
||||
);
|
||||
});
|
||||
} else if (envVar === "Enterprise") {
|
||||
});
|
||||
|
||||
cy.ifEnv("Enterprise", () => {
|
||||
cy.get(commonSelectors.signUpTermsHelperText).should(($el) => {
|
||||
expect($el.contents().first().text().trim()).to.eq(
|
||||
"By signing up you are agreeing to the"
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const links = [
|
||||
{
|
||||
|
|
@ -116,20 +118,15 @@ describe("Self host onboarding", () => {
|
|||
cy.get(onboardingSelectors.passwordInput).type("password");
|
||||
cy.get(commonSelectors.continueButton).click();
|
||||
|
||||
if (envVar === "Enterprise") {
|
||||
cy.ifEnv("Enterprise", () => {
|
||||
bannerElementsVerification();
|
||||
onboardingStepOne();
|
||||
}
|
||||
});
|
||||
|
||||
bannerElementsVerification();
|
||||
onboardingStepTwo();
|
||||
|
||||
// if (envVar === "Enterprise") {
|
||||
// bannerElementsVerification();
|
||||
// onboardingStepTwo();
|
||||
// }
|
||||
|
||||
if (envVar === "Enterprise") {
|
||||
cy.ifEnv("Enterprise", () => {
|
||||
bannerElementsVerification();
|
||||
|
||||
const trialPageTexts = [
|
||||
|
|
@ -173,7 +170,7 @@ describe("Self host onboarding", () => {
|
|||
cy.get(onboardingSelectors.onPremiseLink)
|
||||
.verifyVisibleElement("have.text", "Click here")
|
||||
.and("have.attr", "href")
|
||||
.and("equal", "https://www.tooljet.com/pricing?payment=onpremise");
|
||||
.and("equal", "https://tooljet.ai/pricing?payment=onpremise");
|
||||
|
||||
const planTitles = [
|
||||
{
|
||||
|
|
@ -196,66 +193,59 @@ describe("Self host onboarding", () => {
|
|||
|
||||
const prices = [
|
||||
{ selector: `${onboardingSelectors.planPrice}:eq(0)`, text: "$0" },
|
||||
{ selector: `${onboardingSelectors.planPrice}:eq(1)`, text: "$30" },
|
||||
{
|
||||
selector: '[data-cy="pro-plan-price"]:eq(0)',
|
||||
text: "$79/monthper builder",
|
||||
},
|
||||
{
|
||||
selector: '[data-cy="pro-plan-price"]:eq(1)',
|
||||
text: "$199/monthper builder",
|
||||
},
|
||||
{
|
||||
selector: `${onboardingSelectors.planToggleLabel}:eq(0)`,
|
||||
text: "Yearly20% off",
|
||||
},
|
||||
{
|
||||
selector: `${onboardingSelectors.planToggleLabel}:eq(1)`,
|
||||
text: "Yearly20% off",
|
||||
},
|
||||
];
|
||||
|
||||
prices.forEach((item) => {
|
||||
cy.get(item.selector).should("be.visible").and("have.text", item.text);
|
||||
});
|
||||
|
||||
cy.get(onboardingSelectors.planToggleInput).should("be.visible");
|
||||
cy.get(onboardingSelectors.planToggleLabel).verifyVisibleElement(
|
||||
"have.text",
|
||||
"Yearly20% off"
|
||||
);
|
||||
cy.get(onboardingSelectors.discountDetails).verifyVisibleElement(
|
||||
"have.text",
|
||||
"20% off"
|
||||
);
|
||||
cy.get(onboardingSelectors.planToggleInput).eq(0).should("be.visible");
|
||||
cy.get(onboardingSelectors.planToggleInput).eq(1).should("be.visible");
|
||||
|
||||
cy.get(onboardingSelectors.builderPrice).verifyVisibleElement(
|
||||
"have.text",
|
||||
"$24"
|
||||
);
|
||||
cy.get('[data-cy="builder-price-period"]').verifyVisibleElement(
|
||||
"have.text",
|
||||
onboardingText.priceMonthlyText
|
||||
);
|
||||
cy.get('[data-cy="builder-price-description"]').verifyVisibleElement(
|
||||
"have.text",
|
||||
"per builder"
|
||||
);
|
||||
|
||||
cy.get(onboardingSelectors.endUserPrice).verifyVisibleElement(
|
||||
"have.text",
|
||||
"$8"
|
||||
);
|
||||
cy.get('[data-cy="enduser-price-period"]').verifyVisibleElement(
|
||||
"have.text",
|
||||
onboardingText.priceMonthlyText
|
||||
);
|
||||
cy.get('[data-cy="enduser-price-description"]').verifyVisibleElement(
|
||||
"have.text",
|
||||
"per end user"
|
||||
);
|
||||
|
||||
cy.get(onboardingSelectors.pricingPlanToggle).uncheck({ force: true });
|
||||
cy.get(onboardingSelectors.pricingPlanToggle)
|
||||
.eq(0)
|
||||
.uncheck({ force: true });
|
||||
|
||||
cy.get(onboardingSelectors.planToggleLabel)
|
||||
.first()
|
||||
.eq(0)
|
||||
.verifyVisibleElement("have.text", "Monthly20% off");
|
||||
cy.get(onboardingSelectors.discountDetails)
|
||||
.should("have.css", "text-decoration")
|
||||
.and("include", "line-through");
|
||||
|
||||
cy.get(onboardingSelectors.builderPrice).verifyVisibleElement(
|
||||
"have.text",
|
||||
"$30"
|
||||
);
|
||||
cy.get(onboardingSelectors.endUserPrice).verifyVisibleElement(
|
||||
"have.text",
|
||||
"$10"
|
||||
);
|
||||
cy.get('[data-cy="pro-plan-price"]')
|
||||
.eq(0)
|
||||
.verifyVisibleElement("have.text", "$99/monthper builder");
|
||||
|
||||
cy.get(onboardingSelectors.pricingPlanToggle)
|
||||
.eq(1)
|
||||
.uncheck({ force: true });
|
||||
cy.get(onboardingSelectors.planToggleLabel)
|
||||
.eq(1)
|
||||
.verifyVisibleElement("have.text", "Monthly20% off");
|
||||
cy.get(onboardingSelectors.discountDetails)
|
||||
.should("have.css", "text-decoration")
|
||||
.and("include", "line-through");
|
||||
|
||||
cy.get('[data-cy="pro-plan-price"]')
|
||||
.eq(1)
|
||||
.verifyVisibleElement("have.text", "$249/monthper builder");
|
||||
|
||||
cy.get(onboardingSelectors.enterpriseTitle).verifyVisibleElement(
|
||||
"have.text",
|
||||
|
|
@ -274,19 +264,11 @@ describe("Self host onboarding", () => {
|
|||
|
||||
bannerElementsVerification();
|
||||
onboardingStepThree();
|
||||
}
|
||||
});
|
||||
|
||||
cy.get(commonSelectors.skipbutton).click();
|
||||
cy.backToApps();
|
||||
|
||||
if (envVar === "Enterprise") {
|
||||
cy.get(".btn-close").click();
|
||||
}
|
||||
|
||||
if (envVar === "Enterprise") {
|
||||
cy.get(".btn-close").click();
|
||||
}
|
||||
|
||||
logout();
|
||||
cy.appUILogin();
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,4 @@
|
|||
First Name,Last Name,Email,User Role,Group
|
||||
test1,user,test1@gmail.com,Builder,
|
||||
test2,user,test3@gmail.com,End User,
|
||||
Test3,Example,test12@gmail.com,Admin,
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
First Name,Last Name,Email,User Role,Group,Metadata
|
||||
test1,user,test1@gmail.com,Builder,,
|
||||
test2,user,test3@gmail.com,End User,,
|
||||
Test3,Example,test12@gmail.com,Admin,,
|
||||
|
3
cypress-tests/cypress/fixtures/bulkUser/empty_names.csv
Normal file
3
cypress-tests/cypress/fixtures/bulkUser/empty_names.csv
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
First Name,Last Name,Email,User Role,Group
|
||||
,,test12empty@gmail.com,Admin,Admin
|
||||
Test,Example,test12empty@gmail.com,Builder,Builder
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
First Name,Last Name,Email,User Role,Group,Metadata
|
||||
,,test12empty@gmail.com,Admin,Admin,
|
||||
Test,Example,test12empty@gmail.com,Builder,Builder,
|
||||
|
252
cypress-tests/cypress/fixtures/bulkUser/limit_exceeded.csv
Normal file
252
cypress-tests/cypress/fixtures/bulkUser/limit_exceeded.csv
Normal file
|
|
@ -0,0 +1,252 @@
|
|||
First Name,Last Name,Email,User Role,Group
|
||||
Vijay,Yadav,vjyaav1@gmail.com,Admin,
|
||||
Vijay,Yadav,vjyaav2@gmail.com,Builder,
|
||||
Vijay,Yadav,vjyaav3@gmail.com,Builder,
|
||||
Vijay,Yadav,vjyaav4@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav5@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav6@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav7@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav8@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav9@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav10@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav11@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav12@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav13@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav14@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav15@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav16@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav17@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav18@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav19@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav20@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav21@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav22@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav23@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav24@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav25@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav26@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav27@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav28@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav29@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav30@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav31@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav32@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav33@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav34@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav35@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav36@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav37@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav38@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav39@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav40@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav41@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav42@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav43@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav44@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav45@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav46@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav47@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav48@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav49@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav50@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav51@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav52@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav53@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav54@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav55@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav56@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav57@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav58@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav59@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav60@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav61@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav62@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav63@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav64@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav65@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav66@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav67@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav68@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav69@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav70@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav71@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav72@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav73@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav74@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav75@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav76@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav77@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav78@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav79@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav80@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav81@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav82@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav83@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav84@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav85@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav86@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav87@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav88@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav89@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav90@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav91@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav92@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav93@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav94@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav95@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav96@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav97@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav98@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav99@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav100@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav101@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav102@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav103@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav104@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav105@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav106@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav107@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav108@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav109@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav110@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav111@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav112@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav113@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav114@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav115@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav116@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav117@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav118@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav119@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav120@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav121@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav122@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav123@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav124@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav125@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav126@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav127@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav128@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav129@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav130@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav131@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav132@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav133@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav134@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav135@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav136@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav137@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav138@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav139@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav140@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav141@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav142@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav143@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav144@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav145@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav146@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav147@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav148@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav149@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav150@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav151@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav152@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav153@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav154@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav155@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav156@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav157@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav158@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav159@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav160@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav161@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav162@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav163@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav164@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav165@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav166@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav167@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav168@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav169@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav170@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav171@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav172@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav173@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav174@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav175@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav176@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav177@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav178@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav179@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav180@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav181@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav182@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav183@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav184@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav185@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav186@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav187@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav188@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav189@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav190@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav191@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav192@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav193@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav194@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav195@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav196@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav197@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav198@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav199@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav200@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav201@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav202@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav203@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav204@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav205@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav206@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav207@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav208@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav209@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav210@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav211@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav212@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav213@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav214@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav215@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav216@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav217@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav218@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav219@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav220@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav221@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav222@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav223@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav224@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav225@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav226@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav227@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav228@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav229@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav230@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav231@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav232@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav233@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav234@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav235@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav236@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav237@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav238@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav239@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav240@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav241@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav242@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav243@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav244@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav245@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav246@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav247@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav248@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav249@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav250@gmail.com,End User,
|
||||
Vijay,Yadav,vjyaav251@gmail.com,End User,
|
||||
|
252
cypress-tests/cypress/fixtures/bulkUser/limit_exceeded_ee.csv
Normal file
252
cypress-tests/cypress/fixtures/bulkUser/limit_exceeded_ee.csv
Normal file
|
|
@ -0,0 +1,252 @@
|
|||
First Name,Last Name,Email,User Role,Group,Metadata
|
||||
Vijay,Yadav,vjyaav1@gmail.com,Admin,,
|
||||
Vijay,Yadav,vjyaav2@gmail.com,Builder,,
|
||||
Vijay,Yadav,vjyaav3@gmail.com,Builder,,
|
||||
Vijay,Yadav,vjyaav4@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav5@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav6@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav7@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav8@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav9@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav10@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav11@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav12@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav13@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav14@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav15@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav16@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav17@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav18@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav19@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav20@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav21@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav22@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav23@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav24@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav25@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav26@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav27@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav28@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav29@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav30@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav31@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav32@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav33@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav34@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav35@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav36@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav37@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav38@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav39@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav40@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav41@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav42@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav43@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav44@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav45@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav46@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav47@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav48@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav49@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav50@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav51@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav52@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav53@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav54@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav55@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav56@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav57@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav58@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav59@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav60@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav61@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav62@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav63@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav64@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav65@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav66@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav67@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav68@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav69@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav70@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav71@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav72@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav73@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav74@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav75@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav76@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav77@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav78@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav79@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav80@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav81@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav82@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav83@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav84@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav85@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav86@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav87@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav88@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav89@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav90@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav91@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav92@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav93@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav94@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav95@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav96@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav97@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav98@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav99@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav100@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav101@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav102@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav103@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav104@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav105@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav106@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav107@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav108@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav109@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav110@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav111@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav112@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav113@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav114@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav115@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav116@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav117@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav118@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav119@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav120@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav121@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav122@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav123@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav124@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav125@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav126@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav127@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav128@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav129@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav130@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav131@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav132@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav133@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav134@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav135@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav136@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav137@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav138@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav139@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav140@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav141@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav142@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav143@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav144@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav145@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav146@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav147@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav148@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav149@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav150@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav151@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav152@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav153@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav154@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav155@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav156@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav157@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav158@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav159@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav160@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav161@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav162@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav163@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav164@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav165@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav166@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav167@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav168@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav169@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav170@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav171@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav172@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav173@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav174@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav175@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav176@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav177@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav178@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav179@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav180@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav181@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav182@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav183@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav184@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav185@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav186@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav187@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav188@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav189@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav190@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav191@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav192@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav193@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav194@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav195@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav196@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav197@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav198@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav199@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav200@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav201@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav202@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav203@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav204@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav205@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav206@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav207@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav208@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav209@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav210@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav211@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav212@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav213@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav214@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav215@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav216@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav217@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav218@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav219@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav220@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav221@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav222@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav223@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav224@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav225@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav226@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav227@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav228@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav229@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav230@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav231@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav232@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav233@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav234@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav235@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav236@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav237@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav238@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav239@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav240@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav241@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav242@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav243@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav244@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav245@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav246@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav247@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav248@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav249@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav250@gmail.com,End User,,
|
||||
Vijay,Yadav,vjyaav251@gmail.com,End User,,
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
First Name,Last Name,Email,User Role,Group
|
||||
test,test,,Admin,Admin
|
||||
test,test,,Builder,Builder
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
First Name,Last Name,Email,User Role,Group,Metadata
|
||||
,,withoutname1@gmail.com,Admin,Admin,
|
||||
,,withoutname2@gmail.com,Builder,Builder,
|
||||
|
3
cypress-tests/cypress/fixtures/bulkUser/missing_name.csv
Normal file
3
cypress-tests/cypress/fixtures/bulkUser/missing_name.csv
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
First Name,Last Name,Email,User Role,Group
|
||||
,,withoutname1@gmail.com,Admin,Admin
|
||||
,,withoutname2@gmail.com,Builder,Builder
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
First Name,Last Name,Email,User Role,Group,Metadata
|
||||
,,withoutname1@gmail.com,Admin,Admin,
|
||||
,,withoutname2@gmail.com,Builder,Builder,
|
||||
|
3
cypress-tests/cypress/fixtures/bulkUser/missing_role.csv
Normal file
3
cypress-tests/cypress/fixtures/bulkUser/missing_role.csv
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
First Name,Last Name,Email,User Role,Group
|
||||
Test,Example,test12@gmail.com,,
|
||||
Test,Example,test13@gmail.com,,
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
First Name,Last Name,Email,User Role,Group,Metadata
|
||||
Test,Example,test12@gmail.com,,,
|
||||
Test,Example,test13@gmail.com,,,
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
First Name,Last Name,Email,User Role,Group,Metadata
|
||||
test,test,demo1@gmail.com,Admin,test,
|
||||
test,test,demo2@gmail.com,Builder,abc,
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
First Name,Last Name,Email,User Role,Group,Metadata
|
||||
test,test,demo11@gmail.com,Admin,,
|
||||
test,test,demo11@gmail.com,Builder,,
|
||||
|
||||
|
|
|
@ -658,7 +658,7 @@ export const createGroupAddAppAndUserToGroup = (groupName, email) => {
|
|||
|
||||
cy.request({
|
||||
method: "POST",
|
||||
url: `${Cypress.env("server_host")}/api/v2/group-permissions/${groupId}/granular-permissions`,
|
||||
url: `${Cypress.env("server_host")}/api/v2/group-permissions/${groupId}/granular-permissions/app`,
|
||||
headers: headers,
|
||||
body: {
|
||||
name: "Apps",
|
||||
|
|
@ -676,7 +676,6 @@ export const createGroupAddAppAndUserToGroup = (groupName, email) => {
|
|||
],
|
||||
},
|
||||
},
|
||||
|
||||
}).then((response) => {
|
||||
expect(response.status).to.equal(201);
|
||||
});
|
||||
|
|
@ -727,7 +726,7 @@ export const duplicateMultipleGroups = (groupNames) => {
|
|||
cy.get(commonSelectors.duplicateOption).click(); // Click on the duplicate option
|
||||
cy.get(commonSelectors.confirmDuplicateButton).click(); // Confirm duplication if needed
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const verifyGroupCardOptions = (groupName) => {
|
||||
cy.get(groupsSelector.groupLink(groupName)).click();
|
||||
|
|
@ -865,7 +864,7 @@ export const addUserInGroup = (groupName, email) => {
|
|||
commonSelectors.toastMessage,
|
||||
groupsText.userAddedToast
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export const inviteUserBasedOnRole = (firstName, email, role = "end-user") => {
|
||||
fillUserInviteForm(firstName, email);
|
||||
|
|
@ -891,7 +890,13 @@ export const verifyBasicPermissions = (canCreate = true) => {
|
|||
);
|
||||
};
|
||||
|
||||
export const setupWorkspaceAndInviteUser = (workspaceName, workspaceSlug, firstName, email, role = "end-user") => {
|
||||
export const setupWorkspaceAndInviteUser = (
|
||||
workspaceName,
|
||||
workspaceSlug,
|
||||
firstName,
|
||||
email,
|
||||
role = "end-user"
|
||||
) => {
|
||||
cy.apiCreateWorkspace(workspaceName, workspaceSlug);
|
||||
cy.visit(workspaceSlug);
|
||||
cy.wait(1000);
|
||||
|
|
@ -907,7 +912,10 @@ export const verifySettingsAccess = (shouldExist = true) => {
|
|||
);
|
||||
};
|
||||
|
||||
export const verifyUserPrivileges = (expectedButtonState, shouldHaveWorkspaceSettings) => {
|
||||
export const verifyUserPrivileges = (
|
||||
expectedButtonState,
|
||||
shouldHaveWorkspaceSettings
|
||||
) => {
|
||||
cy.get(commonSelectors.dashboardAppCreateButton).should(expectedButtonState);
|
||||
cy.get(commonSelectors.settingsIcon).click();
|
||||
|
||||
|
|
@ -923,4 +931,4 @@ export const setupAndUpdateRole = (currentRole, endRole, email) => {
|
|||
updateRole(currentRole, endRole, email);
|
||||
cy.wait(1000);
|
||||
cy.apiLogout();
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -371,7 +371,7 @@ export const defaultSSO = (enable) => {
|
|||
});
|
||||
};
|
||||
|
||||
export const setSignupStatus = (enable, workspaceName = 'My workspace') => {
|
||||
export const setSignupStatus = (enable, workspaceName = "My workspace") => {
|
||||
cy.task("dbConnection", {
|
||||
dbconfig: Cypress.env("app_db"),
|
||||
sql: `SELECT id FROM organizations WHERE name = '${workspaceName}'`,
|
||||
|
|
@ -429,3 +429,23 @@ export const resetDomain = () => {
|
|||
});
|
||||
});
|
||||
};
|
||||
|
||||
export const enableInstanceSignup = (
|
||||
allowPersonalWorkspace = true,
|
||||
enableLoginConfig = true,
|
||||
allowedDomains = ""
|
||||
) => {
|
||||
const allowValue = allowPersonalWorkspace ? "true" : "false";
|
||||
const loginConfigValue = enableLoginConfig ? "true" : "false";
|
||||
|
||||
const sql = `
|
||||
UPDATE instance_settings SET value = '${allowValue}' WHERE key = 'ALLOW_PERSONAL_WORKSPACE';
|
||||
UPDATE instance_settings SET value = '${loginConfigValue}' WHERE key = 'ENABLE_SIGNUP';
|
||||
UPDATE instance_settings SET value = '${allowedDomains}' WHERE key = 'ALLOWED_DOMAINS';
|
||||
`;
|
||||
|
||||
cy.task("dbConnection", {
|
||||
dbconfig: Cypress.env("app_db"),
|
||||
sql,
|
||||
});
|
||||
};
|
||||
|
|
|
|||
591
cypress-tests/cypress/support/utils/platform/eeCommon.js
Normal file
591
cypress-tests/cypress/support/utils/platform/eeCommon.js
Normal file
|
|
@ -0,0 +1,591 @@
|
|||
import {
|
||||
commonEeSelectors,
|
||||
ssoEeSelector,
|
||||
instanceSettingsSelector,
|
||||
multiEnvSelector,
|
||||
workspaceSelector,
|
||||
} from "Selectors/eeCommon";
|
||||
import { ssoEeText } from "Texts/eeCommon";
|
||||
import { commonSelectors, commonWidgetSelector } from "Selectors/common";
|
||||
import * as common from "Support/utils/common";
|
||||
import { groupsSelector } from "Selectors/manageGroups";
|
||||
import { groupsText } from "Texts/manageGroups";
|
||||
import { eeGroupsSelector } from "Selectors/eeCommon";
|
||||
import { eeGroupsText } from "Texts/eeCommon";
|
||||
import {
|
||||
// verifyOnboardingQuestions,
|
||||
// verifyCloudOnboardingQuestions,
|
||||
fetchAndVisitInviteLink,
|
||||
} from "Support/utils/manageUsers";
|
||||
import { commonText } from "Texts/common";
|
||||
import { dashboardText } from "Texts/dashboard";
|
||||
import { usersText } from "Texts/manageUsers";
|
||||
import { usersSelector } from "Selectors/manageUsers";
|
||||
import { ssoSelector } from "Selectors/manageSSO";
|
||||
import { ssoText } from "Texts/manageSSO";
|
||||
// import { appPromote } from "Support/utils/multiEnv";
|
||||
|
||||
export const oidcSSOPageElements = () => {
|
||||
cy.get(ssoEeSelector.oidcToggle).click();
|
||||
cy.get(ssoSelector.saveButton).eq(1).click();
|
||||
cy.get('[data-cy="modal-title"]').verifyVisibleElement(
|
||||
"have.text",
|
||||
"Enable OpenID Connect"
|
||||
);
|
||||
cy.get('[data-cy="modal-close-button"]').should("be.visible");
|
||||
cy.get('[data-cy="modal-message"]').verifyVisibleElement(
|
||||
"have.text",
|
||||
"Enabling OpenID Connect at the workspace level will override any OpenID Connect configurations set at the instance level."
|
||||
);
|
||||
cy.get('[data-cy="confirmation-text"]').verifyVisibleElement(
|
||||
"have.text",
|
||||
"Are you sure you want to continue?"
|
||||
);
|
||||
cy.get('[data-cy="cancel-button"]')
|
||||
.eq(2)
|
||||
.verifyVisibleElement("have.text", "Cancel");
|
||||
cy.get('[data-cy="enable-button"]').verifyVisibleElement(
|
||||
"have.text",
|
||||
"Enable"
|
||||
);
|
||||
|
||||
cy.get('[data-cy="cancel-button"]').eq(2).click();
|
||||
cy.get('[data-cy="status-label"]').verifyVisibleElement(
|
||||
"have.text",
|
||||
ssoText.disabledLabel
|
||||
);
|
||||
|
||||
cy.get(ssoEeSelector.oidcToggle).click();
|
||||
cy.get(ssoSelector.saveButton).eq(1).click();
|
||||
cy.get('[data-cy="enable-button"]').click();
|
||||
|
||||
cy.verifyToastMessage(
|
||||
commonSelectors.toastMessage,
|
||||
ssoText.toggleUpdateToast("OpenID")
|
||||
);
|
||||
|
||||
cy.get(ssoEeSelector.statusLabel).verifyVisibleElement(
|
||||
"have.text",
|
||||
ssoEeText.enabledLabel
|
||||
);
|
||||
|
||||
cy.get('[data-cy="redirect-url-label"]').verifyVisibleElement(
|
||||
"have.text",
|
||||
ssoText.redirectUrlLabel
|
||||
);
|
||||
cy.get('[data-cy="redirect-url"]').should("be.visible");
|
||||
cy.get('[data-cy="copy-icon"]').should("be.visible");
|
||||
|
||||
cy.get(ssoEeSelector.oidcToggle).click();
|
||||
cy.get(ssoSelector.saveButton).eq(1).click();
|
||||
// cy.get('[data-cy="enable-button"]').click();
|
||||
cy.verifyToastMessage(
|
||||
commonSelectors.toastMessage,
|
||||
ssoText.toggleUpdateToast("OpenID")
|
||||
);
|
||||
cy.get(ssoSelector.statusLabel).verifyVisibleElement(
|
||||
"have.text",
|
||||
ssoText.disabledLabel
|
||||
);
|
||||
|
||||
cy.get(ssoEeSelector.oidcToggle).click();
|
||||
cy.clearAndType(ssoEeSelector.nameInput, ssoEeText.testName);
|
||||
cy.clearAndType(ssoEeSelector.clientIdInput, ssoEeText.testclientId);
|
||||
cy.clearAndType(ssoEeSelector.clientSecretInput, ssoEeText.testclientSecret);
|
||||
cy.clearAndType(ssoEeSelector.WellKnownUrlInput, ssoEeText.testWellknownUrl);
|
||||
cy.get(ssoSelector.saveButton).eq(1).click();
|
||||
cy.get('[data-cy="enable-button"]').click();
|
||||
cy.verifyToastMessage(
|
||||
commonSelectors.toastMessage,
|
||||
ssoText.toggleUpdateToast("OpenID")
|
||||
);
|
||||
cy.get(ssoEeSelector.nameInput).should("have.value", ssoEeText.testName);
|
||||
cy.get(ssoEeSelector.clientIdInput).should(
|
||||
"have.value",
|
||||
ssoEeText.testclientId
|
||||
);
|
||||
cy.get(ssoEeSelector.clientSecretInput).should(
|
||||
"have.value",
|
||||
ssoEeText.testclientSecret
|
||||
);
|
||||
cy.get(ssoEeSelector.WellKnownUrlInput).should(
|
||||
"have.value",
|
||||
ssoEeText.testWellknownUrl
|
||||
);
|
||||
};
|
||||
|
||||
export const resetDsPermissions = () => {
|
||||
common.navigateToManageGroups();
|
||||
cy.wait(200);
|
||||
cy.get(groupsSelector.permissionsLink).click();
|
||||
|
||||
cy.get(groupsSelector.appsCreateCheck).then(($el) => {
|
||||
if ($el.is(":checked")) {
|
||||
cy.get(groupsSelector.appsCreateCheck).uncheck();
|
||||
}
|
||||
});
|
||||
cy.get(eeGroupsSelector.dsCreateCheck).then(($el) => {
|
||||
if ($el.is(":checked")) {
|
||||
cy.get(eeGroupsSelector.dsCreateCheck).uncheck();
|
||||
}
|
||||
});
|
||||
cy.get(eeGroupsSelector.dsDeleteCheck).then(($el) => {
|
||||
if ($el.is(":checked")) {
|
||||
cy.get(eeGroupsSelector.dsDeleteCheck).uncheck();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const deleteAssignedDatasources = () => {
|
||||
common.navigateToManageGroups();
|
||||
cy.get('[data-cy="datasource-link"]').click();
|
||||
cy.get("body").then(($body) => {
|
||||
const removeAllButtons = $body.find('[data-cy="remove-button"]');
|
||||
if (removeAllButtons.length > 0) {
|
||||
cy.get('[data-cy="remove-button"]').click({ multiple: true });
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const userSignUp = (fullName, email, workspaceName) => {
|
||||
const verificationFunction =
|
||||
Cypress.env("environment") === "Enterprise"
|
||||
? verifyOnboardingQuestions
|
||||
: verifyCloudOnboardingQuestions;
|
||||
|
||||
let invitationLink = "";
|
||||
cy.visit("/");
|
||||
cy.wait(500);
|
||||
cy.get(commonSelectors.createAnAccountLink).realClick();
|
||||
cy.clearAndType(commonSelectors.nameInputField, fullName);
|
||||
cy.clearAndType(commonSelectors.emailInputField, email);
|
||||
cy.clearAndType(commonSelectors.passwordInputField, commonText.password);
|
||||
cy.get(commonSelectors.signUpButton).click();
|
||||
|
||||
cy.wait(500);
|
||||
cy.task("dbConnection", {
|
||||
dbconfig: Cypress.env("app_db"),
|
||||
sql: `select invitation_token from users where email='${email}';`,
|
||||
}).then((resp) => {
|
||||
invitationLink = `/invitations/${resp.rows[0].invitation_token}`;
|
||||
cy.visit(invitationLink);
|
||||
cy.get(commonSelectors.setUpToolJetButton).click();
|
||||
cy.wait(4000);
|
||||
|
||||
verificationFunction(fullName, workspaceName);
|
||||
});
|
||||
};
|
||||
|
||||
export const allowPersonalWorkspace = (allow = true) => {
|
||||
const value = allow ? "true" : "false";
|
||||
cy.task("dbConnection", {
|
||||
dbconfig: Cypress.env("app_db"),
|
||||
sql: `UPDATE instance_settings SET value = '${value}' WHERE key = 'ALLOW_PERSONAL_WORKSPACE';`,
|
||||
});
|
||||
};
|
||||
|
||||
export const addNewUserEE = (firstName, email) => {
|
||||
common.navigateToManageUsers();
|
||||
cy.get(usersSelector.buttonAddUsers).click();
|
||||
cy.get(commonSelectors.inputFieldFullName).type(firstName);
|
||||
cy.get(commonSelectors.inputFieldEmailAddress).type(email);
|
||||
|
||||
cy.get(usersSelector.buttonInviteUsers).click();
|
||||
cy.verifyToastMessage(
|
||||
commonSelectors.toastMessage,
|
||||
usersText.userCreatedToast
|
||||
);
|
||||
WorkspaceInvitationLink(email);
|
||||
cy.clearAndType(commonSelectors.passwordInputField, usersText.password);
|
||||
cy.get(commonSelectors.signUpButton).click();
|
||||
cy.wait(2000);
|
||||
cy.get(commonSelectors.acceptInviteButton).click();
|
||||
cy.get(commonSelectors.workspaceName).verifyVisibleElement(
|
||||
"have.text",
|
||||
"My workspace"
|
||||
);
|
||||
};
|
||||
|
||||
export const inviteUser = (firstName, email) => {
|
||||
cy.get(usersSelector.buttonAddUsers).click();
|
||||
cy.get(commonSelectors.inputFieldFullName).type(firstName);
|
||||
cy.get(commonSelectors.inputFieldEmailAddress).type(email);
|
||||
|
||||
cy.get(usersSelector.buttonInviteUsers).click();
|
||||
cy.verifyToastMessage(
|
||||
commonSelectors.toastMessage,
|
||||
usersText.userCreatedToast
|
||||
);
|
||||
fetchAndVisitInviteLink(email);
|
||||
};
|
||||
|
||||
export const defaultWorkspace = () => {
|
||||
cy.get(".org-select-container").then(($title) => {
|
||||
if (!$title.text().includes("My workspace")) {
|
||||
cy.get(commonSelectors.workspaceName).realClick();
|
||||
cy.contains("My workspace").realClick();
|
||||
cy.wait(2000);
|
||||
defaultWorkspace();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const trunOffAllowPersonalWorkspace = () => {
|
||||
cy.get(commonSelectors.settingsIcon).click();
|
||||
cy.get(commonEeSelectors.instanceSettingIcon).click();
|
||||
cy.get(instanceSettingsSelector.manageInstanceSettings).click();
|
||||
cy.get(instanceSettingsSelector.allowWorkspaceToggle)
|
||||
.eq(0)
|
||||
.then(($el) => {
|
||||
if ($el.is(":checked")) {
|
||||
cy.get(instanceSettingsSelector.allowWorkspaceToggle).eq(0).uncheck();
|
||||
cy.get(commonEeSelectors.saveButton).click();
|
||||
cy.verifyToastMessage(
|
||||
commonSelectors.toastMessage,
|
||||
"Instance settings have been updated"
|
||||
);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const verifySSOSignUpPageElements = () => {
|
||||
cy.get(commonSelectors.invitePageHeader).verifyVisibleElement(
|
||||
"have.text",
|
||||
"Join ToolJet"
|
||||
);
|
||||
cy.get(commonSelectors.invitePageSubHeader).verifyVisibleElement(
|
||||
"have.text",
|
||||
"You are invited to ToolJet."
|
||||
);
|
||||
cy.get(commonSelectors.userNameInputLabel).verifyVisibleElement(
|
||||
"have.text",
|
||||
commonText.userNameInputLabel
|
||||
);
|
||||
cy.get(commonSelectors.invitedUserName).should("be.visible");
|
||||
cy.get(commonSelectors.emailInputLabel).verifyVisibleElement(
|
||||
"have.text",
|
||||
commonText.emailInputLabel
|
||||
);
|
||||
cy.get(commonSelectors.invitedUserEmail).should("be.visible");
|
||||
cy.get(commonSelectors.acceptInviteButton).verifyVisibleElement(
|
||||
"have.text",
|
||||
commonText.acceptInviteButton
|
||||
);
|
||||
|
||||
cy.get(commonSelectors.signUpTermsHelperText).should(($el) => {
|
||||
expect($el.contents().first().text().trim()).to.eq(
|
||||
commonText.signUpTermsHelperText
|
||||
);
|
||||
});
|
||||
cy.get(commonSelectors.termsOfServiceLink)
|
||||
.verifyVisibleElement("have.text", commonText.termsOfServiceLink)
|
||||
.and("have.attr", "href")
|
||||
.and("equal", "https://www.tooljet.com/terms");
|
||||
cy.get(commonSelectors.privacyPolicyLink)
|
||||
.verifyVisibleElement("have.text", commonText.privacyPolicyLink)
|
||||
.and("have.attr", "href")
|
||||
.and("equal", "https://www.tooljet.com/privacy");
|
||||
};
|
||||
|
||||
export const VerifyWorkspaceInvitePageElements = () => {
|
||||
cy.get(commonSelectors.invitePageHeader).verifyVisibleElement(
|
||||
"have.text",
|
||||
commonText.invitePageHeader
|
||||
);
|
||||
cy.get(commonSelectors.invitePageSubHeader).verifyVisibleElement(
|
||||
"have.text",
|
||||
commonText.invitePageSubHeader
|
||||
);
|
||||
cy.verifyLabel(commonText.userNameInputLabel);
|
||||
cy.get(commonSelectors.invitedUserName).should("be.visible");
|
||||
cy.verifyLabel(commonText.emailInputLabel);
|
||||
cy.get(commonSelectors.invitedUserEmail).should("be.visible");
|
||||
cy.get(commonSelectors.acceptInviteButton).verifyVisibleElement(
|
||||
"have.text",
|
||||
commonText.acceptInviteButton
|
||||
);
|
||||
|
||||
cy.get(commonSelectors.signUpTermsHelperText).should(($el) => {
|
||||
expect($el.contents().first().text().trim()).to.eq(
|
||||
commonText.signUpTermsHelperText
|
||||
);
|
||||
});
|
||||
cy.get(commonSelectors.termsOfServiceLink)
|
||||
.verifyVisibleElement("have.text", commonText.termsOfServiceLink)
|
||||
.and("have.attr", "href")
|
||||
.and("equal", "https://www.tooljet.com/terms");
|
||||
cy.get(commonSelectors.privacyPolicyLink)
|
||||
.verifyVisibleElement("have.text", commonText.privacyPolicyLink)
|
||||
.and("have.attr", "href")
|
||||
.and("equal", "https://www.tooljet.com/privacy");
|
||||
|
||||
cy.get("body").then(($el) => {
|
||||
if ($el.text().includes("Google")) {
|
||||
cy.get(ssoSelector.googleSSOText).verifyVisibleElement(
|
||||
"have.text",
|
||||
ssoText.googleSignUpText
|
||||
);
|
||||
cy.get(ssoSelector.gitSSOText).verifyVisibleElement(
|
||||
"have.text",
|
||||
ssoText.gitSignUpText
|
||||
);
|
||||
cy.get(commonSelectors.onboardingSeperator).should("be.visible");
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const WorkspaceInvitationLink = (email) => {
|
||||
let invitationToken,
|
||||
organizationToken,
|
||||
workspaceId,
|
||||
userId,
|
||||
url = "";
|
||||
cy.task("dbConnection", {
|
||||
dbconfig: Cypress.env("app_db"),
|
||||
sql: `select invitation_token from users where email='${email}';`,
|
||||
}).then((resp) => {
|
||||
invitationToken = resp.rows[0].invitation_token;
|
||||
|
||||
cy.task("dbConnection", {
|
||||
dbconfig: Cypress.env("app_db"),
|
||||
sql: "select id from organizations where name='My workspace';",
|
||||
}).then((resp) => {
|
||||
workspaceId = resp.rows[0].id;
|
||||
|
||||
cy.task("dbConnection", {
|
||||
dbconfig: Cypress.env("app_db"),
|
||||
sql: `select id from users where email='${email}';`,
|
||||
}).then((resp) => {
|
||||
userId = resp.rows[0].id;
|
||||
|
||||
cy.task("dbConnection", {
|
||||
dbconfig: Cypress.env("app_db"),
|
||||
sql: `select invitation_token from organization_users where user_id='${userId}';`,
|
||||
}).then((resp) => {
|
||||
organizationToken = resp.rows[0].invitation_token;
|
||||
|
||||
url = `/invitations/${invitationToken}/workspaces/${organizationToken}?oid=${workspaceId}`;
|
||||
common.logout();
|
||||
cy.visit(url);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
export const enableDefaultSSO = () => {
|
||||
common.navigateToManageSSO();
|
||||
cy.get("body").then(($el) => {
|
||||
if (!$el.text().includes("Allowed domains")) {
|
||||
cy.get(ssoSelector.generalSettingsElements.generalSettings).click();
|
||||
}
|
||||
});
|
||||
cy.get(ssoSelector.allowDefaultSSOToggle).then(($el) => {
|
||||
if (!$el.is(":checked")) {
|
||||
cy.get(ssoSelector.allowDefaultSSOToggle).check();
|
||||
cy.get(ssoSelector.saveButton).click();
|
||||
cy.verifyToastMessage(commonSelectors.toastMessage, ssoText.ssoToast);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const disableSSO = (ssoSelector, toggleSelector) => {
|
||||
cy.wait(1000);
|
||||
cy.get(ssoSelector).click();
|
||||
cy.get(toggleSelector).then(($el) => {
|
||||
if ($el.is(":checked")) {
|
||||
cy.get(toggleSelector).uncheck();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const AddDataSourceToGroup = (groupName, dsName) => {
|
||||
common.navigateToManageGroups();
|
||||
cy.get(groupsSelector.groupLink(groupName)).click();
|
||||
cy.get(eeGroupsSelector.datasourceLink).click();
|
||||
cy.wait(500);
|
||||
cy.get(
|
||||
'[data-cy="datasource-select-search"] >> .rmsc > .dropdown-container > .dropdown-heading > .dropdown-heading-value > .gray'
|
||||
).click();
|
||||
cy.contains(dsName).realClick();
|
||||
|
||||
cy.get(eeGroupsSelector.AddDsButton).click();
|
||||
cy.verifyToastMessage(
|
||||
commonSelectors.toastMessage,
|
||||
"Datasources added to the group"
|
||||
);
|
||||
};
|
||||
|
||||
export const enableToggle = (toggleSelector) => {
|
||||
cy.get(toggleSelector).then(($el) => {
|
||||
if (!$el.is(":checked")) {
|
||||
cy.get(toggleSelector).check();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const disableToggle = (toggleSelector) => {
|
||||
cy.get(toggleSelector).then(($el) => {
|
||||
if ($el.is(":checked")) {
|
||||
cy.get(toggleSelector).uncheck();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const verifyPromoteModalUI = (versionName, currEnv, targetEnv) => {
|
||||
cy.get(commonEeSelectors.promoteButton)
|
||||
.verifyVisibleElement("have.text", " Promote ")
|
||||
.click();
|
||||
cy.get(commonEeSelectors.modalTitle).verifyVisibleElement(
|
||||
"have.text",
|
||||
`Promote ${versionName}`
|
||||
);
|
||||
cy.get(commonSelectors.closeButton).should("be.visible");
|
||||
cy.get(multiEnvSelector.fromLabel).verifyVisibleElement("have.text", "FROM");
|
||||
cy.get(multiEnvSelector.toLabel).verifyVisibleElement("have.text", "TO");
|
||||
cy.get(multiEnvSelector.currEnvName).verifyVisibleElement(
|
||||
"have.text",
|
||||
currEnv
|
||||
);
|
||||
cy.get('[data-cy="target-env-name"]').verifyVisibleElement(
|
||||
"have.text",
|
||||
targetEnv
|
||||
);
|
||||
cy.get('[data-cy="cancel-button"]').verifyVisibleElement(
|
||||
"have.text",
|
||||
"Cancel"
|
||||
);
|
||||
cy.get(commonEeSelectors.promoteButton)
|
||||
.eq(1)
|
||||
.verifyVisibleElement("have.text", "Promote ");
|
||||
};
|
||||
|
||||
export const resetPassword = (email) => {
|
||||
cy.visit("/");
|
||||
cy.get(commonSelectors.forgotPasswordLink).click();
|
||||
cy.clearAndType(commonSelectors.emailInputField, email);
|
||||
cy.get(commonSelectors.resetPasswordLinkButton).click();
|
||||
|
||||
cy.task("dbConnection", {
|
||||
dbconfig: Cypress.env("app_db"),
|
||||
sql: `select forgot_password_token from users where email='${email}';`,
|
||||
}).then((resp) => {
|
||||
const passwordResetLink = `/reset-password/${resp.rows[0].forgot_password_token}`;
|
||||
cy.visit(passwordResetLink);
|
||||
});
|
||||
cy.wait(500);
|
||||
|
||||
cy.clearAndType(commonSelectors.newPasswordInputField, "Password");
|
||||
cy.clearAndType(commonSelectors.confirmPasswordInputField, "Password");
|
||||
cy.wait(4000);
|
||||
cy.get(commonSelectors.resetPasswordButton).click();
|
||||
cy.get(commonSelectors.backToLoginButton).click();
|
||||
};
|
||||
|
||||
export const verifyTooltipDisabled = (selector, message) => {
|
||||
cy.get(selector)
|
||||
.trigger("mouseover", { force: true })
|
||||
.then(() => {
|
||||
cy.get(".tooltip-inner").last().should("have.text", message);
|
||||
});
|
||||
};
|
||||
|
||||
export const createAnAppWithSlug = (appName, slug) => {
|
||||
cy.apiCreateApp(appName);
|
||||
cy.openApp();
|
||||
cy.dragAndDropWidget("Table", 250, 250);
|
||||
appPromote("development", "release");
|
||||
cy.get(commonWidgetSelector.shareAppButton).click();
|
||||
cy.clearAndType(commonWidgetSelector.appNameSlugInput, `${slug}`);
|
||||
cy.wait(2000);
|
||||
cy.get(commonWidgetSelector.modalCloseButton).click();
|
||||
};
|
||||
|
||||
export const updateLicense = (key) => {
|
||||
cy.task("dbConnection", {
|
||||
dbconfig: Cypress.env("app_db"),
|
||||
sql: `update instance_settings set value='${key}', updated_at= NOW() where key='LICENSE_KEY';`,
|
||||
});
|
||||
};
|
||||
|
||||
export const openInstanceSettings = () => {
|
||||
cy.get(commonSelectors.settingsIcon).click();
|
||||
cy.get(commonEeSelectors.instanceSettingIcon).click();
|
||||
};
|
||||
|
||||
export const openUserActionMenu = (email) => {
|
||||
cy.clearAndType(commonSelectors.inputUserSearch, email);
|
||||
cy.wait(1000);
|
||||
cy.get('[data-cy="user-actions-button"]').eq(0).click();
|
||||
cy.wait(2000);
|
||||
};
|
||||
|
||||
export const archiveWorkspace = (workspaceName) => {
|
||||
cy.get(instanceSettingsSelector.allWorkspaceTab).click();
|
||||
cy.clearAndType(commonEeSelectors.searchBar, workspaceName);
|
||||
cy.get(workspaceSelector.workspaceStatusChange).eq(0).click();
|
||||
cy.get(commonEeSelectors.confirmButton).click();
|
||||
};
|
||||
|
||||
export const passwordToggle = (enable) => {
|
||||
cy.getCookie("tj_auth_token").then((cookie) => {
|
||||
cy.request(
|
||||
{
|
||||
method: "PATCH",
|
||||
url: "http://localhost:3000/api/organizations/configs",
|
||||
headers: {
|
||||
"Tj-Workspace-Id": Cypress.env("workspaceId"),
|
||||
Cookie: `tj_auth_token=${cookie.value}`,
|
||||
},
|
||||
body: { type: "form", enabled: enable },
|
||||
},
|
||||
{ log: false }
|
||||
).then((response) => {
|
||||
expect(response.status).to.equal(200);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
export const InstanceSSO = (personalWorkspace, enableSignup, workspaceSSO) => {
|
||||
allowPersonalWorkspace(personalWorkspace);
|
||||
|
||||
cy.task("dbConnection", {
|
||||
dbconfig: Cypress.env("app_db"),
|
||||
sql: `UPDATE instance_settings SET value = '${enableSignup}' WHERE key = 'ENABLE_SIGNUP';UPDATE instance_settings SET value = '${workspaceSSO}' WHERE key = 'ENABLE_WORKSPACE_LOGIN_CONFIGURATION';`,
|
||||
});
|
||||
};
|
||||
|
||||
export const resetInstanceDomain = () => {
|
||||
cy.getCookie("tj_auth_token").then((cookie) => {
|
||||
cy.request(
|
||||
{
|
||||
method: "PATCH",
|
||||
url: "http://localhost:3000/api/instance-login-configs",
|
||||
headers: {
|
||||
"Tj-Workspace-Id": Cypress.env("workspaceId"),
|
||||
Cookie: `tj_auth_token=${cookie.value}`,
|
||||
},
|
||||
body: { allowedDomains: "" },
|
||||
},
|
||||
{ log: false }
|
||||
).then((response) => {
|
||||
expect(response.status).to.equal(200);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
export const instanceSSOConfig = (allow = true) => {
|
||||
const value = allow ? "true" : "false";
|
||||
|
||||
cy.task("dbConnection", {
|
||||
dbconfig: Cypress.env("app_db"),
|
||||
sql: `UPDATE sso_configs SET enabled = ${allow} WHERE sso IN ('google', 'git', 'openid')AND organization_id IS NULL;`,
|
||||
});
|
||||
};
|
||||
|
||||
export const updateInstanceSettings = (key, value) => {
|
||||
cy.task("updateSetting", {
|
||||
dbconfig: Cypress.env("app_db"),
|
||||
sql: `UPDATE instance_settings SET value = ${value} WHERE key = ${key};`,
|
||||
});
|
||||
};
|
||||
|
|
@ -122,10 +122,11 @@ export const createAndRunRestAPIQuery = ({
|
|||
}
|
||||
if (Array.isArray(responseData)) {
|
||||
responseData.forEach((item) => {
|
||||
expect(item).to.have.any.keys("id", "name", "price");
|
||||
expect(item).to.have.any.keys("id", "name", "username", "email", "address");
|
||||
});
|
||||
}
|
||||
if (responseData?.id) {
|
||||
cy.log(responseData.id)
|
||||
cy.writeFile("cypress/fixtures/restAPI/storedId.json", {
|
||||
id: responseData.id,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -572,6 +572,7 @@
|
|||
"styles": "Styles",
|
||||
"general": "General",
|
||||
"validation": "Validation",
|
||||
"structure": "Structure",
|
||||
"documentation": "Read documentation for {{componentMeta}}",
|
||||
"widgetNameEmptyError": "Widget name cannot be empty",
|
||||
"componentNameExistsError": "Component name already exists",
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
Subproject commit 9458c8d66f29f8334765b5757dd096139a8d53d2
|
||||
Subproject commit 9da4f776915e328120c3024e551ef6b8032f9f63
|
||||
33
frontend/package-lock.json
generated
33
frontend/package-lock.json
generated
|
|
@ -63,6 +63,7 @@
|
|||
"dotenv": "^16.0.3",
|
||||
"draft-js": "^0.11.7",
|
||||
"draft-js-export-html": "^1.4.1",
|
||||
"draft-js-import-html": "^1.4.1",
|
||||
"driver.js": "^0.9.8",
|
||||
"emoji-mart": "^5.5.2",
|
||||
"file-loader": "^6.2.0",
|
||||
|
|
@ -105,7 +106,7 @@
|
|||
"react-dnd": "^16.0.1",
|
||||
"react-dnd-html5-backend": "^16.0.1",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-dropzone": "^14.2.3",
|
||||
"react-dropzone": "^14.3.8",
|
||||
"react-highlight-words": "^0.21.0",
|
||||
"react-hot-toast": "^2.4.0",
|
||||
"react-hotkeys-hook": "^4.3.5",
|
||||
|
|
@ -16948,6 +16949,31 @@
|
|||
"immutable": "3.x.x"
|
||||
}
|
||||
},
|
||||
"node_modules/draft-js-import-element": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/draft-js-import-element/-/draft-js-import-element-1.4.0.tgz",
|
||||
"integrity": "sha512-WmYT5PrCm47lGL5FkH6sRO3TTAcn7qNHsD3igiPqLG/RXrqyKrqN4+wBgbcT2lhna/yfWTRtgzAbQsSJoS1Meg==",
|
||||
"dependencies": {
|
||||
"draft-js-utils": "^1.4.0",
|
||||
"synthetic-dom": "^1.4.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"draft-js": ">=0.10.0",
|
||||
"immutable": "3.x.x"
|
||||
}
|
||||
},
|
||||
"node_modules/draft-js-import-html": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/draft-js-import-html/-/draft-js-import-html-1.4.1.tgz",
|
||||
"integrity": "sha512-KOZmtgxZriCDgg5Smr3Y09TjubvXe7rHPy/2fuLSsL+aSzwUDwH/aHDA/k47U+WfpmL4qgyg4oZhqx9TYJV0tg==",
|
||||
"dependencies": {
|
||||
"draft-js-import-element": "^1.4.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"draft-js": ">=0.10.0",
|
||||
"immutable": "3.x.x"
|
||||
}
|
||||
},
|
||||
"node_modules/draft-js-utils": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/draft-js-utils/-/draft-js-utils-1.4.1.tgz",
|
||||
|
|
@ -32645,6 +32671,11 @@
|
|||
"dev": true,
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/synthetic-dom": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/synthetic-dom/-/synthetic-dom-1.4.0.tgz",
|
||||
"integrity": "sha512-mHv51ZsmZ+ShT/4s5kg+MGUIhY7Ltq4v03xpN1c8T1Krb5pScsh/lzEjyhrVD0soVDbThbd2e+4dD9vnDG4rhg=="
|
||||
},
|
||||
"node_modules/tabbable": {
|
||||
"version": "6.2.0",
|
||||
"resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz",
|
||||
|
|
|
|||
|
|
@ -101,7 +101,7 @@
|
|||
"react-dnd": "^16.0.1",
|
||||
"react-dnd-html5-backend": "^16.0.1",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-dropzone": "^14.2.3",
|
||||
"react-dropzone": "^14.3.8",
|
||||
"react-highlight-words": "^0.21.0",
|
||||
"react-hot-toast": "^2.4.0",
|
||||
"react-hotkeys-hook": "^4.3.5",
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ import { shallow } from 'zustand/shallow';
|
|||
import useStore from '@/AppBuilder/_stores/store';
|
||||
import { checkIfToolJetCloud } from '@/_helpers/utils';
|
||||
import { BasicPlanMigrationBanner } from '@/HomePage/BasicPlanMigrationBanner/BasicPlanMigrationBanner';
|
||||
import EmbedApp from '@/AppBuilder/EmbedApp';
|
||||
|
||||
const AppWrapper = (props) => {
|
||||
const { isAppDarkMode } = useAppDarkMode();
|
||||
|
|
@ -144,7 +145,7 @@ class AppComponent extends React.Component {
|
|||
const pathname = this.props.location.pathname;
|
||||
if (pathname.includes('/apps/')) {
|
||||
return 'editor';
|
||||
} else if (pathname.includes('/applications/')) {
|
||||
} else if (pathname.includes('/applications/') || pathname.includes('/embed-apps/')) {
|
||||
return 'viewer';
|
||||
}
|
||||
return '';
|
||||
|
|
@ -404,6 +405,7 @@ class AppComponent extends React.Component {
|
|||
</PrivateRoute>
|
||||
}
|
||||
/>
|
||||
<Route exact path="/embed-apps/:appId" element={<EmbedApp />} />
|
||||
<Route
|
||||
path="*"
|
||||
render={() => {
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import EditorHeader from '@/AppBuilder/Header';
|
|||
import LeftSidebar from '@/AppBuilder/LeftSidebar';
|
||||
import Popups from './Popups';
|
||||
import { ModuleProvider } from '@/AppBuilder/_contexts/ModuleContext';
|
||||
import RightSidebarToggle from '@/AppBuilder/RightSideBar/RightSidebarToggle';
|
||||
import { shallow } from 'zustand/shallow';
|
||||
|
||||
// const EditorHeader = lazy(() => import('@/AppBuilder/Header'));
|
||||
|
|
@ -25,6 +26,7 @@ import { shallow } from 'zustand/shallow';
|
|||
// TODO: split Loader into separate component and remove editor loading state from Editor
|
||||
export const Editor = ({ id: appId, darkMode, moduleId = 'canvas', switchDarkMode, appType = 'front-end' }) => {
|
||||
useAppData(appId, moduleId, darkMode);
|
||||
const isRightSidebarOpen = useStore((state) => state.isRightSidebarOpen);
|
||||
const isEditorLoading = useStore((state) => state.loaderStore.modules[moduleId].isEditorLoading, shallow);
|
||||
const currentMode = useStore((state) => state.modeStore.modules[moduleId].currentMode, shallow);
|
||||
const isModuleEditor = appType === 'module';
|
||||
|
|
@ -54,9 +56,10 @@ export const Editor = ({ id: appId, darkMode, moduleId = 'canvas', switchDarkMod
|
|||
</Suspense>
|
||||
{window?.public_config?.ENABLE_MULTIPLAYER_EDITING === 'true' && <RealtimeCursors />}
|
||||
<DndProvider backend={HTML5Backend}>
|
||||
<AppCanvas appId={appId} />
|
||||
<AppCanvas moduleId={moduleId} appId={appId} switchDarkMode={switchDarkMode} darkMode={darkMode} />
|
||||
<QueryPanel darkMode={darkMode} />
|
||||
<RightSideBar darkMode={darkMode} />
|
||||
<RightSidebarToggle darkMode={darkMode} />
|
||||
{isRightSidebarOpen && <RightSideBar darkMode={darkMode} />}{' '}
|
||||
</DndProvider>
|
||||
<Popups darkMode={darkMode} />
|
||||
</ModuleProvider>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import React, { useState, useEffect, useRef, useMemo } from 'react';
|
||||
import React, { useState, useEffect, useRef, useMemo, useCallback } from 'react';
|
||||
import { Container } from './Container';
|
||||
import Grid from './Grid';
|
||||
import { EditorSelecto } from './Selecto';
|
||||
|
|
@ -17,8 +17,13 @@ import useAppDarkMode from '@/_hooks/useAppDarkMode';
|
|||
import useAppCanvasMaxWidth from './useAppCanvasMaxWidth';
|
||||
import { DeleteWidgetConfirmation } from './DeleteWidgetConfirmation';
|
||||
import useSidebarMargin from './useSidebarMargin';
|
||||
import PagesSidebarNavigation from '../RightSideBar/PageSettingsTab/PageMenu/PagesSidebarNavigation';
|
||||
import { resolveReferences } from '@/_helpers/utils';
|
||||
import useRightSidebarMargin from './userRightSidebarMargin';
|
||||
import { DragGhostWidget } from './GhostWidgets';
|
||||
import AppCanvasBanner from '../../AppBuilder/Header/AppCanvasBanner';
|
||||
|
||||
export const AppCanvas = ({ appId, isViewerSidebarPinned, isViewer = false }) => {
|
||||
export const AppCanvas = ({ appId, isViewer = false, switchDarkMode, darkMode }) => {
|
||||
const { moduleId, isModuleMode, appType } = useModuleContext();
|
||||
const canvasContainerRef = useRef();
|
||||
const handleCanvasContainerMouseUp = useStore((state) => state.handleCanvasContainerMouseUp, shallow);
|
||||
|
|
@ -41,9 +46,33 @@ export const AppCanvas = ({ appId, isViewerSidebarPinned, isViewer = false }) =>
|
|||
const setIsComponentLayoutReady = useStore((state) => state.setIsComponentLayoutReady, shallow);
|
||||
const canvasMaxWidth = useAppCanvasMaxWidth({ mode: currentMode });
|
||||
const editorMarginLeft = useSidebarMargin(canvasContainerRef);
|
||||
const isPagesSidebarHidden = useStore((state) => state.getPagesSidebarVisibility('canvas'), shallow);
|
||||
// const editorMarginRight = useRightSidebarMargin(canvasContainerRef);
|
||||
// const isPagesSidebarHidden = useStore((state) => state.getPagesSidebarVisibility('canvas'), shallow);
|
||||
const isSidebarOpen = useStore((state) => state.isSidebarOpen, shallow);
|
||||
const getPageId = useStore((state) => state.getCurrentPageId, shallow);
|
||||
const isRightSidebarOpen = useStore((state) => state.isRightSidebarOpen, shallow);
|
||||
const isRightSidebarPinned = useStore((state) => state.isRightSidebarPinned, shallow);
|
||||
const currentPageId = useStore((state) => state.modules[moduleId].currentPageId);
|
||||
const homePageId = useStore((state) => state.appStore.modules[moduleId].app.homePageId);
|
||||
|
||||
const [isViewerSidebarPinned, setIsSidebarPinned] = useState(
|
||||
localStorage.getItem('isPagesSidebarPinned') !== 'false'
|
||||
);
|
||||
|
||||
const { globalSettings, pages, pageSettings, switchPage } = useStore(
|
||||
(state) => ({
|
||||
globalSettings: state.globalSettings,
|
||||
pages: state.modules.canvas.pages,
|
||||
pageSettings: state.pageSettings,
|
||||
switchPage: state.switchPage,
|
||||
}),
|
||||
shallow
|
||||
);
|
||||
|
||||
const showHeader = !globalSettings?.hideHeader;
|
||||
const { definition: { styles = {}, properties = {} } = {} } = pageSettings ?? {};
|
||||
const { position, disableMenu, showOnDesktop } = properties ?? {};
|
||||
const isPagesSidebarHidden = resolveReferences(disableMenu?.value);
|
||||
|
||||
const hideSidebar = isModuleMode || isPagesSidebarHidden || appType === 'module';
|
||||
|
||||
|
|
@ -78,15 +107,17 @@ export const AppCanvas = ({ appId, isViewerSidebarPinned, isViewer = false }) =>
|
|||
handleResize();
|
||||
|
||||
return () => window.removeEventListener('resize', handleResize);
|
||||
}, [currentLayout, canvasMaxWidth, isViewerSidebarPinned, moduleId]);
|
||||
}, [currentLayout, canvasMaxWidth, isViewerSidebarPinned, moduleId, isRightSidebarOpen]);
|
||||
|
||||
const styles = useMemo(() => {
|
||||
useEffect(() => { }, [isViewerSidebarPinned]);
|
||||
|
||||
const canvasContainerStyles = useMemo(() => {
|
||||
const canvasBgColor =
|
||||
currentMode === 'view'
|
||||
? computeViewerBackgroundColor(isAppDarkMode, canvasBgColor)
|
||||
: !isAppDarkMode
|
||||
? '#EBEBEF'
|
||||
: '#2F3C4C';
|
||||
? '#EBEBEF'
|
||||
: '#2F3C4C';
|
||||
|
||||
if (isModuleMode) {
|
||||
return {
|
||||
|
|
@ -100,24 +131,37 @@ export const AppCanvas = ({ appId, isViewerSidebarPinned, isViewer = false }) =>
|
|||
borderLeft: currentMode === 'edit' && editorMarginLeft + 'px solid',
|
||||
height: currentMode === 'edit' ? canvasContainerHeight : '100%',
|
||||
background: canvasBgColor,
|
||||
marginLeft:
|
||||
isViewerSidebarPinned && !hideSidebar && currentLayout !== 'mobile' && currentMode !== 'edit'
|
||||
? pageSidebarStyle === 'icon'
|
||||
? '65px'
|
||||
: '210px'
|
||||
: 'auto',
|
||||
width: currentMode === 'edit' ? `calc(100% - 96px)` : '100%',
|
||||
alignItems: 'unset',
|
||||
justifyContent: 'unset',
|
||||
borderRight: currentMode === 'edit' && isRightSidebarOpen && '299' + 'px solid',
|
||||
padding: currentMode === 'edit' && '8px',
|
||||
paddingBottom: currentMode === 'edit' && '2px',
|
||||
};
|
||||
}, [
|
||||
currentMode,
|
||||
isAppDarkMode,
|
||||
isModuleMode,
|
||||
editorMarginLeft,
|
||||
canvasContainerHeight,
|
||||
isViewerSidebarPinned,
|
||||
hideSidebar,
|
||||
currentLayout,
|
||||
pageSidebarStyle,
|
||||
]);
|
||||
}, [currentMode, isAppDarkMode, isModuleMode, editorMarginLeft, canvasContainerHeight, isRightSidebarOpen]);
|
||||
|
||||
const toggleSidebarPinned = useCallback(() => {
|
||||
const newValue = !isViewerSidebarPinned;
|
||||
setIsSidebarPinned(newValue);
|
||||
localStorage.setItem('isPagesSidebarPinned', JSON.stringify(newValue));
|
||||
}, [isViewerSidebarPinned]);
|
||||
|
||||
function getMinWidth() {
|
||||
if (isModuleMode) return '100%';
|
||||
|
||||
const shouldAdjust = isSidebarOpen || (isRightSidebarOpen && currentMode === 'edit');
|
||||
|
||||
if (!shouldAdjust) return '';
|
||||
|
||||
let offset;
|
||||
if (isViewerSidebarPinned) {
|
||||
offset = position === 'side' ? '352px' : '126px';
|
||||
} else {
|
||||
offset = position === 'side' ? '171px' : '126px';
|
||||
}
|
||||
|
||||
return `calc(100vw - ${offset})`;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
|
|
@ -125,50 +169,72 @@ export const AppCanvas = ({ appId, isViewerSidebarPinned, isViewer = false }) =>
|
|||
id="main-editor-canvas"
|
||||
onMouseUp={handleCanvasContainerMouseUp}
|
||||
>
|
||||
{creationMode === 'GIT' && <FreezeVersionInfo info={'Apps imported from git repository cannot be edited'} />}
|
||||
{creationMode !== 'GIT' && <FreezeVersionInfo hide={currentMode !== 'edit'} />}
|
||||
<div
|
||||
ref={canvasContainerRef}
|
||||
className={cx(
|
||||
'canvas-container align-items-center page-container',
|
||||
{ 'dark-theme theme-dark': isAppDarkMode, close: !isViewerSidebarPinned },
|
||||
{ 'overflow-x-auto': (currentMode === 'edit' && isSidebarOpen) || currentMode === 'view' },
|
||||
{ 'overflow-x-hidden': moduleId !== 'canvas' } // Disbling horizontal scroll for modules in view mode
|
||||
)}
|
||||
style={styles}
|
||||
>
|
||||
<AppCanvasBanner appId={appId} />
|
||||
<div id="sidebar-page-navigation" className="areas d-flex flex-rows">
|
||||
<div
|
||||
style={{
|
||||
minWidth: isModuleMode ? '100%' : `calc((100vw - 300px) - 48px)`,
|
||||
}}
|
||||
className={`app-${appId} _tooljet-page-${getPageId()}`}
|
||||
>
|
||||
{currentMode === 'edit' && (
|
||||
<AutoComputeMobileLayoutAlert currentLayout={currentLayout} darkMode={isAppDarkMode} />
|
||||
ref={canvasContainerRef}
|
||||
className={cx(
|
||||
'canvas-container d-flex page-container',
|
||||
{ 'dark-theme theme-dark': isAppDarkMode, close: !isViewerSidebarPinned },
|
||||
{ 'overflow-x-auto': currentMode === 'edit' },
|
||||
{ 'position-top': position === 'top' },
|
||||
{ 'overflow-x-hidden': moduleId !== 'canvas' } // Disbling horizontal scroll for modules in view mode
|
||||
)}
|
||||
<DeleteWidgetConfirmation darkMode={isAppDarkMode} />
|
||||
<HotkeyProvider mode={currentMode} canvasMaxWidth={canvasMaxWidth} currentLayout={currentLayout}>
|
||||
{environmentLoadingState !== 'loading' && (
|
||||
<div>
|
||||
<Container
|
||||
id={moduleId}
|
||||
gridWidth={gridWidth}
|
||||
canvasWidth={canvasWidth}
|
||||
canvasHeight={canvasHeight}
|
||||
darkMode={isAppDarkMode}
|
||||
canvasMaxWidth={canvasMaxWidth}
|
||||
isViewerSidebarPinned={isViewerSidebarPinned}
|
||||
pageSidebarStyle={pageSidebarStyle}
|
||||
appType={appType}
|
||||
/>
|
||||
{appType !== 'module' && <div id="component-portal" />}
|
||||
</div>
|
||||
style={canvasContainerStyles}
|
||||
>
|
||||
{showOnDesktop && (
|
||||
<PagesSidebarNavigation
|
||||
showHeader={showHeader}
|
||||
isMobileDevice={currentLayout === 'mobile'}
|
||||
pages={pages}
|
||||
currentPageId={currentPageId ?? homePageId}
|
||||
switchPage={switchPage}
|
||||
height={currentMode === 'edit' ? canvasContainerHeight : '100%'}
|
||||
switchDarkMode={switchDarkMode}
|
||||
isSidebarPinned={isViewerSidebarPinned}
|
||||
toggleSidebarPinned={toggleSidebarPinned}
|
||||
darkMode={darkMode}
|
||||
/>
|
||||
)}
|
||||
<div
|
||||
style={{
|
||||
minWidth: getMinWidth(),
|
||||
scrollbarWidth: 'none',
|
||||
overflow: 'auto',
|
||||
width: currentMode === 'view' ? `calc(100% - ${isViewerSidebarPinned ? '0px' : '0px'})` : '100%',
|
||||
}}
|
||||
className={`app-${appId} _tooljet-page-${getPageId()}`}
|
||||
>
|
||||
{currentMode === 'edit' && (
|
||||
<AutoComputeMobileLayoutAlert currentLayout={currentLayout} darkMode={isAppDarkMode} />
|
||||
)}
|
||||
<DeleteWidgetConfirmation darkMode={isAppDarkMode} />
|
||||
<HotkeyProvider mode={currentMode} canvasMaxWidth={canvasMaxWidth} currentLayout={currentLayout}>
|
||||
{environmentLoadingState !== 'loading' && (
|
||||
<div>
|
||||
<Container
|
||||
id="canvas"
|
||||
gridWidth={gridWidth}
|
||||
canvasWidth={canvasWidth}
|
||||
canvasHeight={canvasHeight}
|
||||
darkMode={isAppDarkMode}
|
||||
canvasMaxWidth={canvasMaxWidth}
|
||||
isViewerSidebarPinned={isViewerSidebarPinned}
|
||||
pageSidebarStyle={pageSidebarStyle}
|
||||
pagePositionType={position}
|
||||
appType={appType}
|
||||
/>
|
||||
<DragGhostWidget />
|
||||
<div id="component-portal" />
|
||||
{appType !== 'module' && <div id="component-portal" />}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{currentMode === 'view' || (currentLayout === 'mobile' && isAutoMobileLayout) ? null : (
|
||||
<Grid currentLayout={currentLayout} gridWidth={gridWidth} />
|
||||
)}
|
||||
</HotkeyProvider>
|
||||
{currentMode === 'view' || (currentLayout === 'mobile' && isAutoMobileLayout) ? null : (
|
||||
<Grid currentLayout={currentLayout} gridWidth={gridWidth} />
|
||||
)}
|
||||
</HotkeyProvider>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{currentMode === 'edit' && <EditorSelecto />}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import './configHandle.scss';
|
|||
import useStore from '@/AppBuilder/_stores/store';
|
||||
import { findHighestLevelofSelection } from '../Grid/gridUtils';
|
||||
import SolidIcon from '@/_ui/Icon/solidIcons/index';
|
||||
import { ToolTip } from '@/_components/ToolTip';
|
||||
import { useModuleContext } from '@/AppBuilder/_contexts/ModuleContext';
|
||||
import { DROPPABLE_PARENTS } from '../appCanvasConstants';
|
||||
|
||||
|
|
@ -52,7 +53,40 @@ export const ConfigHandle = ({
|
|||
);
|
||||
}, shallow);
|
||||
|
||||
const currentPageIndex = useStore((state) => state.modules.canvas.currentPageIndex);
|
||||
const component = useStore((state) => state.modules.canvas.pages[currentPageIndex].components[id]);
|
||||
const featureAccess = useStore((state) => state?.license?.featureAccess, shallow);
|
||||
const licenseValid = !featureAccess?.licenseStatus?.isExpired && featureAccess?.licenseStatus?.isLicenseValid;
|
||||
const isRestricted = component.permissions && component.permissions.length !== 0;
|
||||
const draggingComponentId = useStore((state) => state.draggingComponentId);
|
||||
|
||||
let height = visibility === false ? 10 : widgetHeight;
|
||||
|
||||
const getTooltip = () => {
|
||||
const permission = component.permissions?.[0];
|
||||
if (!permission) return null;
|
||||
|
||||
const users = permission.groups || permission.users || [];
|
||||
if (users.length === 0) return null;
|
||||
|
||||
const isSingle = permission.type === 'SINGLE';
|
||||
const isGroup = permission.type === 'GROUP';
|
||||
|
||||
if (isSingle) {
|
||||
return users.length === 1
|
||||
? `Access restricted to ${users[0].user.email}`
|
||||
: `Access restricted to ${users.length} users`;
|
||||
}
|
||||
|
||||
if (isGroup) {
|
||||
return users.length === 1
|
||||
? `Access restricted to ${users[0].permission_group?.name || users[0].permissionGroup?.name} group`
|
||||
: `Access restricted to ${users.length} user groups`;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`config-handle ${customClassName}`}
|
||||
|
|
@ -78,6 +112,22 @@ export const ConfigHandle = ({
|
|||
}
|
||||
}}
|
||||
>
|
||||
{licenseValid && isRestricted && (
|
||||
<ToolTip message={getTooltip()} show={licenseValid && isRestricted && !draggingComponentId}>
|
||||
<span
|
||||
style={{
|
||||
background:
|
||||
visibility === false ? '#c6cad0' : componentType === 'Modal' && isModalOpen ? '#c6cad0' : '#4D72FA',
|
||||
border: position === 'bottom' ? '1px solid white' : 'none',
|
||||
color: visibility === false && 'var(--text-placeholder)',
|
||||
marginRight: '4px',
|
||||
}}
|
||||
className="badge handle-content"
|
||||
>
|
||||
<SolidIcon width="12" name="lock" fill="var(--icon-on-solid)" />
|
||||
</span>
|
||||
</ToolTip>
|
||||
)}
|
||||
<span
|
||||
style={{
|
||||
background:
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import {
|
|||
addNewWidgetToTheEditor,
|
||||
computeViewerBackgroundColor,
|
||||
getSubContainerWidthAfterPadding,
|
||||
addDefaultButtonIdToForm,
|
||||
} from './appCanvasUtils';
|
||||
import {
|
||||
CANVAS_WIDTHS,
|
||||
|
|
@ -52,6 +53,7 @@ export const Container = React.memo(
|
|||
canvasMaxWidth,
|
||||
isViewerSidebarPinned,
|
||||
pageSidebarStyle,
|
||||
pagePositionType,
|
||||
componentType,
|
||||
appType,
|
||||
}) => {
|
||||
|
|
@ -84,18 +86,12 @@ export const Container = React.memo(
|
|||
item.canvasWidth = getContainerCanvasWidth();
|
||||
},
|
||||
drop: async ({ componentType, component }, monitor) => {
|
||||
setShowModuleBorder(false); // Hide the module border when dropping
|
||||
setShowModuleBorder(false);
|
||||
if (currentMode === 'view' || (appType === 'module' && componentType !== 'ModuleContainer')) return;
|
||||
|
||||
const didDrop = monitor.didDrop();
|
||||
if (didDrop) return;
|
||||
if (componentType === 'PDF' && !isPDFSupported()) {
|
||||
toast.error(
|
||||
'PDF is not supported in this version of browser. We recommend upgrading to the latest version for full support.'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// IMPORTANT: This logic needs to be changed when we implement the module versioning
|
||||
const moduleInfo = component?.moduleId
|
||||
? {
|
||||
moduleId: component.moduleId,
|
||||
|
|
@ -106,8 +102,10 @@ export const Container = React.memo(
|
|||
}
|
||||
: undefined;
|
||||
|
||||
let addedComponent;
|
||||
|
||||
if (WIDGETS_WITH_DEFAULT_CHILDREN.includes(componentType)) {
|
||||
const parentComponent = addNewWidgetToTheEditor(
|
||||
let parentComponent = addNewWidgetToTheEditor(
|
||||
componentType,
|
||||
monitor,
|
||||
currentLayout,
|
||||
|
|
@ -116,10 +114,11 @@ export const Container = React.memo(
|
|||
moduleInfo
|
||||
);
|
||||
const childComponents = addChildrenWidgetsToParent(componentType, parentComponent?.id, currentLayout);
|
||||
const newComponents = [parentComponent, ...childComponents];
|
||||
await addComponentToCurrentPage(newComponents);
|
||||
// setSelectedComponents([parentComponent?.id]);
|
||||
setActiveRightSideBarTab(RIGHT_SIDE_BAR_TAB.CONFIGURATION);
|
||||
if (componentType === 'Form') {
|
||||
parentComponent = addDefaultButtonIdToForm(parentComponent, childComponents);
|
||||
}
|
||||
addedComponent = [parentComponent, ...childComponents];
|
||||
await addComponentToCurrentPage(addedComponent);
|
||||
} else {
|
||||
const newComponent = addNewWidgetToTheEditor(
|
||||
componentType,
|
||||
|
|
@ -129,11 +128,32 @@ export const Container = React.memo(
|
|||
id,
|
||||
moduleInfo
|
||||
);
|
||||
await addComponentToCurrentPage([newComponent]);
|
||||
// setSelectedComponents([newComponent?.id]);
|
||||
setActiveRightSideBarTab(RIGHT_SIDE_BAR_TAB.CONFIGURATION);
|
||||
addedComponent = [newComponent];
|
||||
await addComponentToCurrentPage(addedComponent);
|
||||
}
|
||||
|
||||
setActiveRightSideBarTab(RIGHT_SIDE_BAR_TAB.CONFIGURATION);
|
||||
|
||||
const canvas = document.querySelector('.canvas-container');
|
||||
const sidebar = document.querySelector('.editor-sidebar');
|
||||
const droppedElem = document.getElementById(addedComponent?.[0]?.id);
|
||||
|
||||
if (!canvas || !sidebar || !droppedElem) return;
|
||||
|
||||
const droppedRect = droppedElem.getBoundingClientRect();
|
||||
const sidebarRect = sidebar.getBoundingClientRect();
|
||||
|
||||
const isOverlapping = droppedRect.right > sidebarRect.left && droppedRect.left < sidebarRect.right;
|
||||
|
||||
if (isOverlapping) {
|
||||
const overlap = droppedRect.right - sidebarRect.left;
|
||||
canvas.scrollTo({
|
||||
left: canvas.scrollLeft + overlap,
|
||||
behavior: 'smooth',
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
collect: (monitor) => ({
|
||||
isOverCurrent: monitor.isOver({ shallow: true }),
|
||||
}),
|
||||
|
|
@ -161,18 +181,27 @@ export const Container = React.memo(
|
|||
}, [canvasWidth, listViewMode, columns]);
|
||||
|
||||
const getCanvasWidth = useCallback(() => {
|
||||
if (
|
||||
id === 'canvas' &&
|
||||
!isPagesSidebarHidden &&
|
||||
isViewerSidebarPinned &&
|
||||
currentLayout !== 'mobile' &&
|
||||
currentMode !== 'edit' &&
|
||||
appType !== 'module'
|
||||
) {
|
||||
return `calc(100% - ${pageSidebarStyle === 'icon' ? '65px' : '210px'})`;
|
||||
}
|
||||
// if (
|
||||
// id === 'canvas' &&
|
||||
// !isPagesSidebarHidden &&
|
||||
// isViewerSidebarPinned &&
|
||||
// currentLayout !== 'mobile' &&
|
||||
// pagePositionType == 'side' &&
|
||||
// appType !== 'module'
|
||||
// ) {
|
||||
// return `calc(100% - ${pageSidebarStyle === 'icon' ? '85px' : '226px'})`;
|
||||
// }
|
||||
// if (
|
||||
// id === 'canvas' &&
|
||||
// !isPagesSidebarHidden &&
|
||||
// !isViewerSidebarPinned &&
|
||||
// currentLayout !== 'mobile' &&
|
||||
// pagePositionType == 'side'
|
||||
// ) {
|
||||
// return `calc(100% - ${'44px'})`;
|
||||
// }
|
||||
return '100%';
|
||||
}, [isViewerSidebarPinned, currentLayout, id, currentMode, pageSidebarStyle]);
|
||||
}, [id, isPagesSidebarHidden, isViewerSidebarPinned, currentLayout, pagePositionType, pageSidebarStyle]);
|
||||
|
||||
const handleCanvasClick = useCallback(
|
||||
(e) => {
|
||||
|
|
@ -224,7 +253,7 @@ export const Container = React.memo(
|
|||
: id === 'canvas'
|
||||
? canvasBgColor
|
||||
: '#f0f0f0',
|
||||
width: getCanvasWidth(),
|
||||
width: '100%',
|
||||
maxWidth: (() => {
|
||||
// For Main Canvas
|
||||
if (id === 'canvas') {
|
||||
|
|
|
|||
|
|
@ -1,17 +1,24 @@
|
|||
import React from 'react';
|
||||
import useStore from '@/AppBuilder/_stores/store';
|
||||
|
||||
export const DragGhostWidget = () => {
|
||||
const draggingComponentId = useStore((state) => state.draggingComponentId);
|
||||
|
||||
if (!draggingComponentId) return null;
|
||||
|
||||
export const DragGhostWidget = ({ isDragging }) => {
|
||||
if (!isDragging) return '';
|
||||
return (
|
||||
<div
|
||||
id={'moveable-drag-ghost'}
|
||||
id="moveable-drag-ghost"
|
||||
style={{
|
||||
zIndex: 4,
|
||||
position: 'absolute',
|
||||
background: '#D9E2FC',
|
||||
opacity: '0.7',
|
||||
pointerEvents: 'none',
|
||||
left: 0,
|
||||
top: 0,
|
||||
}}
|
||||
></div>
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,176 +1,207 @@
|
|||
.target, .nested-target {
|
||||
position: absolute;
|
||||
box-sizing: border-box;
|
||||
.target,
|
||||
.nested-target {
|
||||
position: absolute;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.target.hovered{
|
||||
z-index: 2;
|
||||
.target.hovered {
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.moveable-control-box>.moveable-control-box:not(.moveable-control-box-d-block, .moveable-dragging, .selected-component){
|
||||
visibility: hidden !important;
|
||||
}
|
||||
.moveable-control-box>.moveable-control-box:hover, .selected-component{
|
||||
visibility: visible !important;
|
||||
}
|
||||
.moveable-control-box>.moveable-control-box:hover, .moveable-control-box>.moveable-dragging{
|
||||
visibility: visible !important;
|
||||
}
|
||||
.moveable-control-box.modal-moveable{
|
||||
z-index: 3001 !important;
|
||||
.moveable-control-box
|
||||
> .moveable-control-box:not(
|
||||
.moveable-control-box-d-block,
|
||||
.moveable-dragging,
|
||||
.selected-component
|
||||
) {
|
||||
visibility: hidden !important;
|
||||
}
|
||||
|
||||
.moveable-control-box > .moveable-control-box:hover,
|
||||
.selected-component {
|
||||
visibility: visible !important;
|
||||
}
|
||||
|
||||
.moveable-e.moveable-control{
|
||||
/* height: 24px !important;
|
||||
.moveable-control-box > .moveable-control-box:hover,
|
||||
.moveable-control-box > .moveable-dragging {
|
||||
visibility: visible !important;
|
||||
}
|
||||
|
||||
.moveable-control-box.modal-moveable {
|
||||
z-index: 3001 !important;
|
||||
}
|
||||
|
||||
.moveable-e.moveable-control {
|
||||
/* height: 24px !important;
|
||||
top: -5px !important; */
|
||||
border-radius: 2px !important;
|
||||
border: 1px solid #3E63DD !important;
|
||||
background: #fff !important;
|
||||
width: 6px !important;
|
||||
left: 4px !important;
|
||||
border-radius: 2px !important;
|
||||
border: 1px solid #3e63dd !important;
|
||||
background: #fff !important;
|
||||
width: 6px !important;
|
||||
left: 4px !important;
|
||||
}
|
||||
|
||||
.moveable-w.moveable-control{
|
||||
/* height: 24px !important;
|
||||
.moveable-w.moveable-control {
|
||||
/* height: 24px !important;
|
||||
top: -5px !important; */
|
||||
border-radius: 2px !important;
|
||||
border: 1px solid #3E63DD !important;
|
||||
background: #fff !important;
|
||||
width: 6px !important;
|
||||
left: 4px !important;
|
||||
border-radius: 2px !important;
|
||||
border: 1px solid #3e63dd !important;
|
||||
background: #fff !important;
|
||||
width: 6px !important;
|
||||
left: 4px !important;
|
||||
}
|
||||
|
||||
.moveable-n.moveable-control{
|
||||
/* height: 24px !important; */
|
||||
top: 4px !important;
|
||||
border-radius: 2px !important;
|
||||
border: 1px solid #3E63DD !important;
|
||||
background: #fff !important;
|
||||
height: 6px !important;
|
||||
/* left: 3px !important; */
|
||||
.moveable-horizontal-only {
|
||||
.moveable-direction.moveable-w:not(.moveable-edge),
|
||||
.moveable-direction.moveable-e:not(.moveable-edge) {
|
||||
height: 20px !important;
|
||||
width: 7.5px !important;
|
||||
opacity: 1 !important;
|
||||
background-color: #fff !important;
|
||||
border-radius: 10px !important;
|
||||
}
|
||||
|
||||
.moveable-direction.moveable-w:not(.moveable-edge) {
|
||||
left: 1px !important;
|
||||
top: -6.5px !important;
|
||||
}
|
||||
|
||||
.moveable-direction.moveable-e:not(.moveable-edge) {
|
||||
left: 1px !important;
|
||||
top: -6.5px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.moveable-s.moveable-control{
|
||||
/* height: 24px !important; */
|
||||
top: 4px !important;
|
||||
border-radius: 2px !important;
|
||||
border: 1px solid #3E63DD !important;
|
||||
background: #fff !important;
|
||||
height: 6px !important;
|
||||
/* left: 3px !important; */
|
||||
.moveable-n.moveable-control {
|
||||
/* height: 24px !important; */
|
||||
top: 4px !important;
|
||||
border-radius: 2px !important;
|
||||
border: 1px solid #3e63dd !important;
|
||||
background: #fff !important;
|
||||
height: 6px !important;
|
||||
/* left: 3px !important; */
|
||||
}
|
||||
|
||||
.moveable-s.moveable-control {
|
||||
/* height: 24px !important; */
|
||||
top: 4px !important;
|
||||
border-radius: 2px !important;
|
||||
border: 1px solid #3e63dd !important;
|
||||
background: #fff !important;
|
||||
height: 6px !important;
|
||||
/* left: 3px !important; */
|
||||
}
|
||||
|
||||
.grid-guide-lines {
|
||||
background: #8DA4EF !important;
|
||||
background: #8da4ef !important;
|
||||
}
|
||||
|
||||
.moveable-control-box:not([data-able-groupable]) .moveable-control-box:not(:hover) {
|
||||
opacity: 0;
|
||||
.moveable-control-box:not([data-able-groupable])
|
||||
.moveable-control-box:not(:hover) {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.dragged-movable-control-box, [data-hovered-control="true"] {
|
||||
opacity: 1 !important;
|
||||
.dragged-movable-control-box,
|
||||
[data-hovered-control="true"] {
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
.moveable-line.moveable-e,
|
||||
.moveable-line.moveable-w {
|
||||
border: 5px solid #fff0;
|
||||
border: 5px solid #fff0;
|
||||
}
|
||||
|
||||
.moveable-line.moveable-n {
|
||||
border-bottom: 5px solid #fff0;
|
||||
border-bottom: 5px solid #fff0;
|
||||
}
|
||||
|
||||
.moveable-line.moveable-s {
|
||||
border-bottom: 5px solid #fff0;
|
||||
border-bottom: 5px solid #fff0;
|
||||
}
|
||||
|
||||
.moveable-control[data-rotation="0"], .moveable-control[data-rotation="90"],
|
||||
.moveable-around-control[data-rotation="0"], .moveable-around-control[data-rotation="90"] {
|
||||
opacity: 0;
|
||||
width: 0px !important;
|
||||
height: 0px !important;
|
||||
.moveable-control[data-rotation="0"],
|
||||
.moveable-control[data-rotation="90"],
|
||||
.moveable-around-control[data-rotation="0"],
|
||||
.moveable-around-control[data-rotation="90"] {
|
||||
opacity: 0;
|
||||
width: 0px !important;
|
||||
height: 0px !important;
|
||||
}
|
||||
|
||||
|
||||
.moveable-control {
|
||||
width: 8px !important;
|
||||
height: 8px !important;
|
||||
border: 1px solid var(--moveable-color) !important;
|
||||
background: #fff !important;
|
||||
margin-top: -4px !important;
|
||||
margin-left: -4px !important;
|
||||
width: 8px !important;
|
||||
height: 8px !important;
|
||||
border: 1px solid var(--moveable-color) !important;
|
||||
background: #fff !important;
|
||||
margin-top: -4px !important;
|
||||
margin-left: -4px !important;
|
||||
}
|
||||
|
||||
.moveable-around-control{
|
||||
height: 10px !important;
|
||||
width: 10px !important;
|
||||
.moveable-around-control {
|
||||
height: 10px !important;
|
||||
width: 10px !important;
|
||||
}
|
||||
|
||||
.moveable-around-control[data-direction*="nw"] {
|
||||
left: -11px;
|
||||
top: -11px;
|
||||
left: -11px;
|
||||
top: -11px;
|
||||
}
|
||||
|
||||
.moveable-around-control[data-direction*="ne"] {
|
||||
top: -11px;
|
||||
top: -11px;
|
||||
}
|
||||
|
||||
.moveable-around-control[data-direction*="ne"] {
|
||||
top: -11px;
|
||||
top: -11px;
|
||||
}
|
||||
|
||||
.moveable-around-control[data-direction*="sw"] {
|
||||
left: -11px;
|
||||
top: -1px;
|
||||
left: -11px;
|
||||
top: -1px;
|
||||
}
|
||||
|
||||
.moveable-draggable-dragging {
|
||||
opacity: 1 !important;
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
[data-off-screen="true"] {
|
||||
display: none;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.moveable-guideline {
|
||||
background: #97AEFC !important;
|
||||
opacity: 0.8;
|
||||
z-index: 9999;
|
||||
background: #97aefc !important;
|
||||
opacity: 0.8;
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
.moveable-guideline.moveable-horizontal {
|
||||
height: 1px !important;
|
||||
width: 100% !important;
|
||||
background: #97AEFC !important;
|
||||
left: 0 !important;
|
||||
|
||||
height: 1px !important;
|
||||
width: 100% !important;
|
||||
background: #97aefc !important;
|
||||
left: 0 !important;
|
||||
}
|
||||
|
||||
.moveable-guideline.moveable-vertical {
|
||||
width: 1px !important;
|
||||
height: 100% !important;
|
||||
background: #97AEFC !important;
|
||||
top: 0 !important;
|
||||
|
||||
width: 1px !important;
|
||||
height: 100% !important;
|
||||
background: #97aefc !important;
|
||||
top: 0 !important;
|
||||
}
|
||||
|
||||
.moveable-guideline-group {
|
||||
z-index: 9999;
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
.dragging-component-canvas {
|
||||
outline: 1px solid var(--border-accent-strong) !important;
|
||||
outline-offset: 0px; /* Creates space between element and outline */
|
||||
z-index: 999 !important;
|
||||
outline: 1px solid var(--border-accent-strong) !important;
|
||||
outline-offset: 0px;
|
||||
/* Creates space between element and outline */
|
||||
z-index: 999 !important;
|
||||
}
|
||||
|
||||
.non-dragging-component {
|
||||
outline: 1px dotted var(--border-accent-weak) !important;
|
||||
outline-offset: 0px; /* Creates space between element and outline */
|
||||
z-index: 999 !important;
|
||||
.non-dragging-component {
|
||||
outline: 1px dotted var(--border-accent-weak) !important;
|
||||
outline-offset: 0px;
|
||||
/* Creates space between element and outline */
|
||||
z-index: 999 !important;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -24,10 +24,13 @@ import {
|
|||
handleActivateNonDraggingComponents,
|
||||
computeScrollDelta,
|
||||
computeScrollDeltaOnDrag,
|
||||
getDraggingWidgetWidth,
|
||||
positionDragGhostWidget,
|
||||
} from './gridUtils';
|
||||
import { dragContextBuilder, getAdjustedDropPosition } from './helpers/dragEnd';
|
||||
import useStore from '@/AppBuilder/_stores/store';
|
||||
import './Grid.css';
|
||||
import { useGroupedTargetsScrollHandler } from './hooks/useGroupedTargetsScrollHandler';
|
||||
import { DROPPABLE_PARENTS, NO_OF_GRIDS, SUBCONTAINER_WIDGETS } from '../appCanvasConstants';
|
||||
import { useModuleContext } from '@/AppBuilder/_contexts/ModuleContext';
|
||||
const CANVAS_BOUNDS = { left: 0, top: 0, right: 0, position: 'css' };
|
||||
|
|
@ -35,6 +38,12 @@ const RESIZABLE_CONFIG = {
|
|||
edge: ['nw', 'n', 'ne', 'w', 'e', 'sw', 's', 'se'],
|
||||
renderDirections: ['nw', 'n', 'ne', 'w', 'e', 'sw', 's', 'se'],
|
||||
};
|
||||
|
||||
const HORIZONTAL_CONFIG = {
|
||||
edge: ['e', 'w'],
|
||||
renderDirections: ['w', 'e'],
|
||||
};
|
||||
|
||||
export const GRID_HEIGHT = 10;
|
||||
|
||||
export default function Grid({ gridWidth, currentLayout }) {
|
||||
|
|
@ -49,8 +58,9 @@ export default function Grid({ gridWidth, currentLayout }) {
|
|||
const setSelectedComponents = useStore((state) => state.setSelectedComponents, shallow);
|
||||
const getComponentTypeFromId = useStore((state) => state.getComponentTypeFromId, shallow);
|
||||
const getResolvedValue = useStore((state) => state.getResolvedValue, shallow);
|
||||
const temporaryHeight = useStore((state) => state.temporaryLayouts?.[selectedComponents?.[0]]?.height, shallow);
|
||||
const isGroupHandleHoverd = useIsGroupHandleHoverd();
|
||||
|
||||
const checkHoveredComponentDynamicHeight = useStore((state) => state.checkHoveredComponentDynamicHeight, shallow);
|
||||
const openModalWidgetId = useOpenModalWidgetId();
|
||||
const moveableRef = useRef(null);
|
||||
const triggerCanvasUpdater = useStore((state) => state.triggerCanvasUpdater, shallow);
|
||||
|
|
@ -60,9 +70,10 @@ export default function Grid({ gridWidth, currentLayout }) {
|
|||
const canvasWidth = NO_OF_GRIDS * gridWidth;
|
||||
const getHoveredComponentForGrid = useStore((state) => state.getHoveredComponentForGrid, shallow);
|
||||
const getResolvedComponent = useStore((state) => state.getResolvedComponent, shallow);
|
||||
const getTemporaryLayouts = useStore((state) => state.getTemporaryLayouts, shallow);
|
||||
const updateContainerAutoHeight = useStore((state) => state.updateContainerAutoHeight, shallow);
|
||||
const [canvasBounds, setCanvasBounds] = useState(CANVAS_BOUNDS);
|
||||
const draggingComponentId = useStore((state) => state.draggingComponentId, shallow);
|
||||
const draggingComponentId = useGridStore((state) => state.draggingComponentId, shallow);
|
||||
const resizingComponentId = useGridStore((state) => state.resizingComponentId, shallow);
|
||||
const [dragParentId, setDragParentId] = useState(null);
|
||||
const [elementGuidelines, setElementGuidelines] = useState([]);
|
||||
|
|
@ -73,6 +84,8 @@ export default function Grid({ gridWidth, currentLayout }) {
|
|||
const checkIfAnyWidgetVisibilityChanged = useStore((state) => state.checkIfAnyWidgetVisibilityChanged(), shallow);
|
||||
const getExposedValueOfComponent = useStore((state) => state.getExposedValueOfComponent, shallow);
|
||||
const setReorderContainerChildren = useStore((state) => state.setReorderContainerChildren, shallow);
|
||||
const [isVerticalExpansionRestricted, setIsVerticalExpansionRestricted] = useState(false);
|
||||
const toggleRightSidebar = useStore((state) => state.toggleRightSidebar, shallow);
|
||||
|
||||
useEffect(() => {
|
||||
const selectedSet = new Set(selectedComponents);
|
||||
|
|
@ -121,6 +134,7 @@ export default function Grid({ gridWidth, currentLayout }) {
|
|||
top: widget?.layouts?.[currentLayout]?.top,
|
||||
width: widget?.layouts?.[currentLayout]?.width,
|
||||
parent: widget?.component?.parent,
|
||||
componentType: widget?.component?.component,
|
||||
component: widget?.component,
|
||||
};
|
||||
})
|
||||
|
|
@ -143,24 +157,26 @@ export default function Grid({ gridWidth, currentLayout }) {
|
|||
|
||||
const handleResizeStop = useCallback(
|
||||
(boxList) => {
|
||||
const transformedBoxes = boxList.reduce((acc, box) => {
|
||||
acc[box.id] = box;
|
||||
return acc;
|
||||
}, {});
|
||||
const temporaryLayouts = getTemporaryLayouts();
|
||||
|
||||
boxList.forEach(({ id, height, width, x, y, gw }) => {
|
||||
const _canvasWidth = gw ? gw * NO_OF_GRIDS : canvasWidth;
|
||||
let newWidth = Math.round((width * NO_OF_GRIDS) / _canvasWidth);
|
||||
y = Math.round(y / GRID_HEIGHT) * GRID_HEIGHT;
|
||||
|
||||
// Consider temporary layout position if it exists
|
||||
const temporaryLayout = temporaryLayouts[id];
|
||||
y = temporaryLayout?.top ?? Math.round(y / GRID_HEIGHT) * GRID_HEIGHT;
|
||||
|
||||
gw = gw ? gw : gridWidth;
|
||||
|
||||
const parent = transformedBoxes[id]?.component?.parent;
|
||||
const parent = boxList.find((box) => box.id === id)?.component?.parent;
|
||||
if (y < 0) {
|
||||
y = 0;
|
||||
}
|
||||
if (parent) {
|
||||
const parentElem = document.getElementById(`canvas-${parent}`);
|
||||
const parentId = parent.includes('-') ? parent?.split('-').slice(0, -1).join('-') : parent;
|
||||
const componentType = transformedBoxes.find((box) => box.id === parentId)?.component.component;
|
||||
const componentType = boxList.find((box) => box.id === parentId)?.component.component;
|
||||
var parentHeight = parentElem?.clientHeight || height;
|
||||
if (height > parentHeight && ['Tabs', 'Listview'].includes(componentType)) {
|
||||
height = parentHeight;
|
||||
|
|
@ -253,10 +269,16 @@ export default function Grid({ gridWidth, currentLayout }) {
|
|||
}
|
||||
e.props.target.classList.add('hovered');
|
||||
e.controlBox.classList.add('moveable-control-box-d-block');
|
||||
const isHorizontallyExpandable = checkHoveredComponentDynamicHeight();
|
||||
if (isHorizontallyExpandable) {
|
||||
e.controlBox.classList.add('moveable-horizontal-only');
|
||||
}
|
||||
setIsVerticalExpansionRestricted(!!isHorizontallyExpandable);
|
||||
},
|
||||
mouseLeave(e) {
|
||||
e.props.target.classList.remove('hovered');
|
||||
e.controlBox.classList.remove('moveable-control-box-d-block');
|
||||
e.controlBox.classList.remove('moveable-horizonta-only');
|
||||
},
|
||||
};
|
||||
|
||||
|
|
@ -321,6 +343,11 @@ export default function Grid({ gridWidth, currentLayout }) {
|
|||
|
||||
const groupedTargets = [...findHighestLevelofSelection().map((component) => '.ele-' + component.id)];
|
||||
|
||||
useEffect(() => {
|
||||
if (moveableRef.current) {
|
||||
moveableRef.current.updateTarget();
|
||||
}
|
||||
}, [temporaryHeight]);
|
||||
useEffect(() => {
|
||||
reloadGrid();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
|
|
@ -580,6 +607,8 @@ export default function Grid({ gridWidth, currentLayout }) {
|
|||
}
|
||||
}, [draggingComponentId, resizingComponentId, isGroupDragging, selectedComponents]);
|
||||
|
||||
useGroupedTargetsScrollHandler(groupedTargets, boxList, moveableRef);
|
||||
|
||||
if (mode !== 'edit') return null;
|
||||
|
||||
return (
|
||||
|
|
@ -598,23 +627,34 @@ export default function Grid({ gridWidth, currentLayout }) {
|
|||
origin={false}
|
||||
individualGroupable={groupedTargets.length <= 1}
|
||||
draggable={!shouldFreeze && mode !== 'view'}
|
||||
resizable={!shouldFreeze ? RESIZABLE_CONFIG : false && mode !== 'view'}
|
||||
resizable={
|
||||
!shouldFreeze
|
||||
? isVerticalExpansionRestricted
|
||||
? HORIZONTAL_CONFIG
|
||||
: RESIZABLE_CONFIG
|
||||
: false && mode !== 'view'
|
||||
}
|
||||
keepRatio={false}
|
||||
individualGroupableProps={individualGroupableProps}
|
||||
onResize={(e) => {
|
||||
const temporaryLayouts = getTemporaryLayouts();
|
||||
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;
|
||||
|
||||
// Show grid during resize
|
||||
if (currentWidget.component?.parent) {
|
||||
document.getElementById('canvas-' + currentWidget.component?.parent)?.classList.add('show-grid');
|
||||
setDragParentId(currentWidget.component?.parent);
|
||||
} else {
|
||||
document.getElementById('real-canvas').classList.add('show-grid');
|
||||
}
|
||||
|
||||
handleActivateTargets(currentWidget.component?.parent);
|
||||
const currentWidth = currentWidget.width * _gridWidth;
|
||||
const diffWidth = e.width - currentWidth;
|
||||
|
|
@ -622,20 +662,30 @@ export default function Grid({ gridWidth, currentLayout }) {
|
|||
const isLeftChanged = e.direction[0] === -1;
|
||||
const isTopChanged = e.direction[1] === -1;
|
||||
|
||||
// Calculate positions considering temporary layouts'
|
||||
let transformX = currentWidget.left * _gridWidth;
|
||||
let transformY = currentWidget.top;
|
||||
let transformY = temporaryLayouts[currentWidget.id]?.top ?? currentWidget.top;
|
||||
|
||||
if (isLeftChanged) {
|
||||
transformX = currentWidget.left * _gridWidth - diffWidth;
|
||||
// Left resize
|
||||
transformX = transformX - diffWidth;
|
||||
}
|
||||
if (isTopChanged) {
|
||||
transformY = currentWidget.top - diffHeight;
|
||||
// Top resize
|
||||
transformY = transformY - diffHeight;
|
||||
}
|
||||
|
||||
// Apply container bounds
|
||||
const elemContainer = e.target.closest('.real-canvas');
|
||||
const containerHeight = elemContainer.clientHeight;
|
||||
const containerWidth = elemContainer.clientWidth;
|
||||
const maxY = containerHeight - e.target.clientHeight;
|
||||
const maxLeft = containerWidth - e.target.clientWidth;
|
||||
|
||||
transformY = Math.max(0, Math.min(transformY, maxY));
|
||||
transformX = Math.max(0, Math.min(transformX, maxLeft));
|
||||
|
||||
// Update element style
|
||||
const maxWidthHit = transformX < 0 || transformX >= maxLeft;
|
||||
const maxHeightHit = transformY < 0 || transformY >= maxY;
|
||||
if (!maxWidthHit || e.width < e.target.clientWidth) {
|
||||
|
|
@ -645,14 +695,8 @@ export default function Grid({ gridWidth, currentLayout }) {
|
|||
e.target.style.height = `${e.height}px`;
|
||||
}
|
||||
e.target.style.transform = `translate(${transformX}px, ${transformY}px)`;
|
||||
// Postion ghost element exactly with respect to resizing element
|
||||
if (document.getElementById('resize-ghost-widget')) {
|
||||
document.getElementById(
|
||||
'resize-ghost-widget'
|
||||
).style.transform = `translate(${transformX}px, ${transformY}px)`;
|
||||
document.getElementById('resize-ghost-widget').style.width = `${e.target.clientWidth}px`;
|
||||
document.getElementById('resize-ghost-widget').style.height = `${e.target.clientHeight}px`;
|
||||
}
|
||||
if (e.width > 0) e.target.style.width = `${e.width}px`;
|
||||
if (e.height > 0) e.target.style.height = `${e.height}px`;
|
||||
}}
|
||||
onResizeStart={(e) => {
|
||||
if (
|
||||
|
|
@ -827,6 +871,7 @@ export default function Grid({ gridWidth, currentLayout }) {
|
|||
if (getHoveredComponentForGrid() !== e.target.id) {
|
||||
return false;
|
||||
}
|
||||
toggleRightSidebar();
|
||||
newDragParentId.current = boxList.find((box) => box.id === e.target.id)?.parent;
|
||||
e?.moveable?.controlBox?.removeAttribute('data-off-screen');
|
||||
|
||||
|
|
@ -949,6 +994,9 @@ export default function Grid({ gridWidth, currentLayout }) {
|
|||
let left = Math.round(e.translate[0] / _gridWidth) * _gridWidth;
|
||||
let top = Math.round(e.translate[1] / GRID_HEIGHT) * GRID_HEIGHT;
|
||||
|
||||
const draggingWidgetWidth = getDraggingWidgetWidth(_dragParentId, e.target.clientWidth);
|
||||
e.target.style.width = `${draggingWidgetWidth}px`;
|
||||
|
||||
// This logic is to handle the case when the dragged element is over a new canvas
|
||||
if (_dragParentId !== currentParentId) {
|
||||
left = e.translate[0];
|
||||
|
|
@ -1014,6 +1062,7 @@ export default function Grid({ gridWidth, currentLayout }) {
|
|||
} else if (parentComponent?.component?.component === 'Modal') {
|
||||
// Never update parentId for Modal
|
||||
newParentId = parentComponent?.id;
|
||||
e.target.style.width = `${e.target.clientWidth}px`;
|
||||
}
|
||||
|
||||
if (newParentId !== prevDragParentId.current) {
|
||||
|
|
@ -1034,12 +1083,7 @@ export default function Grid({ gridWidth, currentLayout }) {
|
|||
`translate: ${e.translate[0]} | Round: ${Math.round(e.translate[0] / gridWidth) * gridWidth} | ${gridWidth}`
|
||||
);
|
||||
|
||||
// Postion ghost element exactly as same at dragged element
|
||||
if (document.getElementById(`moveable-drag-ghost`)) {
|
||||
document.getElementById(`moveable-drag-ghost`).style.transform = `translate(${left}px, ${top}px)`;
|
||||
document.getElementById(`moveable-drag-ghost`).style.width = `${e.target.clientWidth}px`;
|
||||
document.getElementById(`moveable-drag-ghost`).style.height = `${e.target.clientHeight}px`;
|
||||
}
|
||||
positionDragGhostWidget(e.target);
|
||||
}}
|
||||
onDragGroup={(ev) => {
|
||||
const { events } = ev;
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@ import { useGridStore } from '@/_stores/gridStore';
|
|||
import { isEmpty } from 'lodash';
|
||||
import useStore from '@/AppBuilder/_stores/store';
|
||||
import { getTabId, getSubContainerIdWithSlots } from '../appCanvasUtils';
|
||||
import { NO_OF_GRIDS } from '../appCanvasConstants';
|
||||
|
||||
export function correctBounds(layout, bounds) {
|
||||
layout = scaleLayouts(layout);
|
||||
const collidesWith = [];
|
||||
|
|
@ -517,3 +519,35 @@ export const computeScrollDelta = ({ source }) => {
|
|||
};
|
||||
|
||||
export const computeScrollDeltaOnDrag = computeScrollDelta;
|
||||
|
||||
export const getDraggingWidgetWidth = (canvasParentId, widgetWidth) => {
|
||||
const targetCanvasWidth =
|
||||
document.getElementById(`canvas-${canvasParentId}`)?.offsetWidth ||
|
||||
document.getElementById('real-canvas')?.offsetWidth;
|
||||
const gridUnitWidth = targetCanvasWidth / NO_OF_GRIDS;
|
||||
const gridUnits = Math.round(widgetWidth / gridUnitWidth);
|
||||
const draggingWidgetWidth = gridUnits * gridUnitWidth;
|
||||
return draggingWidgetWidth;
|
||||
};
|
||||
|
||||
export const positionDragGhostWidget = (draggedElement) => {
|
||||
const ghostElement = document.getElementById('moveable-drag-ghost');
|
||||
|
||||
if (!ghostElement || !draggedElement) return;
|
||||
|
||||
const mainCanvas = document.getElementById('real-canvas');
|
||||
if (!mainCanvas) return;
|
||||
|
||||
const mainCanvasRect = mainCanvas.getBoundingClientRect();
|
||||
const draggedRect = draggedElement.getBoundingClientRect();
|
||||
|
||||
// Calculate position relative to main canvas
|
||||
const relativeLeft = draggedRect.left - mainCanvasRect.left;
|
||||
const relativeTop = draggedRect.top - mainCanvasRect.top;
|
||||
|
||||
// Apply the position
|
||||
ghostElement.style.left = `${relativeLeft}px`;
|
||||
ghostElement.style.top = `${relativeTop}px`;
|
||||
ghostElement.style.width = `${draggedRect.width}px`;
|
||||
ghostElement.style.height = `${draggedRect.height}px`;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -0,0 +1,49 @@
|
|||
import { useEffect, useMemo, useCallback, useRef } from 'react';
|
||||
|
||||
export const useGroupedTargetsScrollHandler = (groupedTargets, boxList, moveableRef) => {
|
||||
const scrollRAF = useRef(null); // // Stores the requestAnimationFrame ID
|
||||
|
||||
const parentCanvasId = useMemo(() => {
|
||||
if (!groupedTargets?.[0] || groupedTargets.length === 0) return null;
|
||||
|
||||
const targetId = groupedTargets[0].replace('.ele-', '');
|
||||
const targetBox = boxList.find((box) => box.id === targetId);
|
||||
return targetBox?.parent || null;
|
||||
}, [groupedTargets, boxList]);
|
||||
|
||||
const containerId = useMemo(() => {
|
||||
return parentCanvasId ? `canvas-${parentCanvasId}` : null;
|
||||
}, [parentCanvasId]);
|
||||
|
||||
const scrollHandler = useCallback(() => {
|
||||
if (!scrollRAF.current) {
|
||||
scrollRAF.current = requestAnimationFrame(() => {
|
||||
if (groupedTargets.length > 1 && moveableRef.current) {
|
||||
moveableRef.current.updateRect();
|
||||
}
|
||||
scrollRAF.current = null;
|
||||
});
|
||||
}
|
||||
}, [groupedTargets.length, moveableRef]);
|
||||
|
||||
useEffect(() => {
|
||||
// Early return if no container ID or not enough grouped targets
|
||||
if (!containerId || groupedTargets.length <= 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
const canvasContainer = document.getElementById(containerId);
|
||||
if (!canvasContainer) {
|
||||
return;
|
||||
}
|
||||
|
||||
canvasContainer.addEventListener('scroll', scrollHandler, { passive: true });
|
||||
|
||||
return () => {
|
||||
canvasContainer.removeEventListener('scroll', scrollHandler);
|
||||
if (scrollRAF.current) {
|
||||
cancelAnimationFrame(scrollRAF.current);
|
||||
}
|
||||
};
|
||||
}, [containerId, groupedTargets.length, scrollHandler]);
|
||||
};
|
||||
|
|
@ -136,7 +136,7 @@ export const HotkeyProvider = ({ children, mode, currentLayout, canvasMaxWidth }
|
|||
style={{
|
||||
width: currentLayout == 'mobile' ? '450px' : '100%',
|
||||
maxWidth: canvasMaxWidth,
|
||||
margin: '0 auto',
|
||||
// margin: '0 auto',
|
||||
transform: 'translateZ(0)',
|
||||
}}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ const SHOULD_ADD_BOX_SHADOW_AND_VISIBILITY = [
|
|||
'VerticalDivider',
|
||||
'Link',
|
||||
'Form',
|
||||
'FilePicker',
|
||||
];
|
||||
|
||||
const RenderWidget = ({
|
||||
|
|
@ -51,6 +52,8 @@ const RenderWidget = ({
|
|||
const { moduleId } = useModuleContext();
|
||||
const componentDefinition = useStore((state) => state.getComponentDefinition(id, moduleId), shallow);
|
||||
const getDefaultStyles = useStore((state) => state.debugger.getDefaultStyles, shallow);
|
||||
const adjustComponentPositions = useStore((state) => state.adjustComponentPositions, shallow);
|
||||
const componentCount = useStore((state) => state.getContainerChildrenMapping(id)?.length || 0, shallow);
|
||||
const component = componentDefinition?.component;
|
||||
const componentName = component?.name;
|
||||
const [key, setKey] = useState(Math.random());
|
||||
|
|
@ -152,6 +155,9 @@ const RenderWidget = ({
|
|||
}, []);
|
||||
if (!componentDefinition?.component) return null;
|
||||
|
||||
const disabledState = resolvedProperties?.disabledState;
|
||||
const loadingState = resolvedProperties?.loadingState;
|
||||
|
||||
return (
|
||||
<ErrorBoundary>
|
||||
<OverlayTrigger
|
||||
|
|
@ -185,7 +191,9 @@ const RenderWidget = ({
|
|||
padding: resolvedStyles?.padding == 'none' ? '0px' : `${BOX_PADDING}px`, //chart and image has a padding property other than container padding
|
||||
}}
|
||||
role={'Box'}
|
||||
className={inCanvas ? `_tooljet-${component?.component} _tooljet-${component?.name}` : ''} //required for custom CSS
|
||||
className={`canvas-component ${
|
||||
inCanvas ? `_tooljet-${component?.component} _tooljet-${component?.name}` : ''
|
||||
} ${disabledState || loadingState ? 'disabled' : ''}`} //required for custom CSS
|
||||
>
|
||||
<ComponentToRender
|
||||
id={id}
|
||||
|
|
@ -202,6 +210,8 @@ const RenderWidget = ({
|
|||
onComponentClick={onComponentClick}
|
||||
darkMode={darkMode}
|
||||
componentName={componentName}
|
||||
adjustComponentPositions={adjustComponentPositions}
|
||||
componentCount={componentCount}
|
||||
dataCy={`draggable-widget-${componentName}`}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ const WidgetWrapper = memo(
|
|||
(state) => state.getComponentDefinition(id, moduleId)?.layouts?.[currentLayout],
|
||||
shallow
|
||||
);
|
||||
const temporaryLayouts = useStore((state) => state.temporaryLayouts?.[id], shallow);
|
||||
const isWidgetActive = useStore((state) => state.selectedComponents.find((sc) => sc === id) && !readOnly, shallow);
|
||||
const isDragging = useStore((state) => state.draggingComponentId === id);
|
||||
const isResizing = useGridStore((state) => state.resizingComponentId === id);
|
||||
|
|
@ -106,8 +107,8 @@ const WidgetWrapper = memo(
|
|||
{mode == 'edit' && (
|
||||
<ConfigHandle
|
||||
id={id}
|
||||
widgetTop={newLayoutData.top}
|
||||
widgetHeight={newLayoutData.height}
|
||||
widgetTop={temporaryLayouts?.top ?? layoutData.top}
|
||||
widgetHeight={temporaryLayouts?.height ?? layoutData.height}
|
||||
showHandle={isWidgetActive}
|
||||
componentType={componentType}
|
||||
visibility={visibility}
|
||||
|
|
@ -128,7 +129,6 @@ const WidgetWrapper = memo(
|
|||
onOptionsChange={onOptionsChange}
|
||||
/>
|
||||
</div>
|
||||
<DragGhostWidget isDragging={isDragging} />
|
||||
<ResizeGhostWidget isResizing={isResizing} />
|
||||
</>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -2,6 +2,12 @@
|
|||
&:focus-visible{
|
||||
outline: none;
|
||||
}
|
||||
|
||||
&.page-container {
|
||||
&.position-top {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.modal-backdrop {
|
||||
|
|
|
|||
|
|
@ -16,6 +16,8 @@ export const APP_HEADER_HEIGHT = 47;
|
|||
|
||||
export const LEFT_SIDEBAR_WIDTH = 348; // exclusive of border
|
||||
|
||||
export const RIGHT_SIDEBAR_WIDTH = 299;
|
||||
|
||||
export const SUBCONTAINER_WIDGETS = ['Container', 'Tabs', 'Listview', 'Kanban', 'Form'];
|
||||
|
||||
export const CONTAINER_FORM_CANVAS_PADDING = 7;
|
||||
|
|
|
|||
|
|
@ -800,3 +800,9 @@ export const getSubContainerWidthAfterPadding = (canvasWidth, componentType, com
|
|||
}
|
||||
return canvasWidth - padding;
|
||||
};
|
||||
|
||||
export const addDefaultButtonIdToForm = (formComponent, defaultChildComponents) => {
|
||||
const { id } = defaultChildComponents[defaultChildComponents.length - 1]; // Assuming the last child is the button
|
||||
formComponent.component.definition.properties.buttonToSubmit = { value: id };
|
||||
return formComponent;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -7,12 +7,15 @@ import debounce from 'lodash/debounce';
|
|||
const useAppCanvasMaxWidth = ({ mode }) => {
|
||||
const canvasMaxWidth = useStore((state) => state.globalSettings.canvasMaxWidth, shallow);
|
||||
const canvasMaxWidthType = useStore((state) => state.globalSettings.canvasMaxWidthType, shallow);
|
||||
const isRightSidebarOpen = useStore((state) => state.isRightSidebarOpen, shallow);
|
||||
const isRightSidebarPinned = useStore((state) => state.isRightSidebarPinned, shallow);
|
||||
let [maxWidth, setMaxWidth] = useState(0);
|
||||
|
||||
const getEditorCanvasWidth = useCallback(() => {
|
||||
let _maxWidth;
|
||||
const windowWidth = window.innerWidth;
|
||||
const widthInPx = windowWidth - (CANVAS_WIDTHS.leftSideBarWidth + CANVAS_WIDTHS.rightSideBarWidth);
|
||||
const widthInPx = windowWidth - CANVAS_WIDTHS.leftSideBarWidth;
|
||||
|
||||
if (canvasMaxWidthType === 'px') {
|
||||
_maxWidth = +canvasMaxWidth;
|
||||
}
|
||||
|
|
@ -51,7 +54,7 @@ const useAppCanvasMaxWidth = ({ mode }) => {
|
|||
debouncedGetCanvasWidth.cancel(); // Cancel any pending debounced calls
|
||||
}
|
||||
};
|
||||
}, [debouncedGetCanvasWidth, getEditorCanvasWidth, getViewerWidth, mode]);
|
||||
}, [debouncedGetCanvasWidth, getEditorCanvasWidth, getViewerWidth, mode, isRightSidebarOpen, isRightSidebarPinned]);
|
||||
|
||||
return maxWidth;
|
||||
};
|
||||
|
|
|
|||
28
frontend/src/AppBuilder/AppCanvas/userRightSidebarMargin.jsx
Normal file
28
frontend/src/AppBuilder/AppCanvas/userRightSidebarMargin.jsx
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
import { useState, useEffect } from 'react';
|
||||
import { isEmpty } from 'lodash';
|
||||
import useStore from '@/AppBuilder/_stores/store';
|
||||
import { shallow } from 'zustand/shallow';
|
||||
import { RIGHT_SIDEBAR_WIDTH } from './appCanvasConstants';
|
||||
import { useModuleContext } from '@/AppBuilder/_contexts/ModuleContext';
|
||||
|
||||
const useRightSidebarMargin = (canvasContainerRef) => {
|
||||
const { moduleId } = useModuleContext();
|
||||
const [editorMarginRight, setEditorMarginRight] = useState(0);
|
||||
const isRightSidebarOpen = useStore((state) => state.isRightSidebarOpen, shallow);
|
||||
const mode = useStore((state) => state.modeStore.modules[moduleId].currentMode, shallow);
|
||||
|
||||
useEffect(() => {
|
||||
if (mode !== 'view') setEditorMarginRight(isRightSidebarOpen ? RIGHT_SIDEBAR_WIDTH : 0);
|
||||
else setEditorMarginRight(0);
|
||||
}, [isRightSidebarOpen, mode]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isEmpty(canvasContainerRef?.current)) {
|
||||
canvasContainerRef.current.scrollRight += editorMarginRight;
|
||||
}
|
||||
}, [editorMarginRight, canvasContainerRef]);
|
||||
|
||||
return editorMarginRight;
|
||||
};
|
||||
|
||||
export default useRightSidebarMargin;
|
||||
|
|
@ -0,0 +1,179 @@
|
|||
import React, { useMemo, useState, useRef, useEffect } from 'react';
|
||||
import useStore from '@/AppBuilder/_stores/store';
|
||||
import { shallow } from 'zustand/shallow';
|
||||
import DataSourceIcon from '@/AppBuilder/QueryManager/Components/DataSourceIcon';
|
||||
import SolidIcon from '@/_ui/Icon/SolidIcons';
|
||||
import { LabeledDivider } from '@/AppBuilder/RightSideBar/Inspector/Components/Form/_components';
|
||||
import cx from 'classnames';
|
||||
import './styles.scss';
|
||||
|
||||
export const DropdownMenu = (props) => {
|
||||
const { value, onChange, forceCodeBox } = props;
|
||||
|
||||
const dataQueries = useStore((state) => state.dataQuery.queries.modules.canvas, shallow);
|
||||
|
||||
// Simple emoji/text icons instead of lucide icons
|
||||
const sourceOptions = useMemo(
|
||||
() => [
|
||||
{ id: 'rawJson', label: 'Raw JSON', icon: <SolidIcon name="curlybraces" /> },
|
||||
{ id: 'jsonSchema', label: 'JSON schema', icon: <SolidIcon name="curlybraces" /> },
|
||||
// { id: 'json-schema', label: 'JSON schema' },
|
||||
],
|
||||
[]
|
||||
);
|
||||
|
||||
const queryOptions = useMemo(() => {
|
||||
return dataQueries.map((query) => ({
|
||||
id: query.id,
|
||||
value: `{{queries.${query.id}.data}}`,
|
||||
label: query.name,
|
||||
icon: <DataSourceIcon source={query} height={16} />,
|
||||
type: 'query',
|
||||
}));
|
||||
}, [dataQueries]);
|
||||
|
||||
const getSelectedSource = (value) => {
|
||||
if (!value) return null;
|
||||
const selectedItem = sourceOptions.find((option) => option.id === value);
|
||||
if (selectedItem) {
|
||||
return selectedItem;
|
||||
}
|
||||
if (!value.startsWith('{{queries.')) {
|
||||
return null;
|
||||
}
|
||||
const queryName = value.split('.')[1]?.replace('}}', '');
|
||||
const selectedQuery = queryOptions.find((option) => option.label === queryName);
|
||||
if (selectedQuery) {
|
||||
return selectedQuery;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [selectedSource, setSelectedSource] = useState(() => getSelectedSource(value));
|
||||
const dropdownRef = useRef(null);
|
||||
|
||||
// Handle outside clicks
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event) => {
|
||||
if (dropdownRef.current && !dropdownRef.current.contains(event.target)) {
|
||||
setIsOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (isOpen) {
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
} else {
|
||||
document.removeEventListener('mousedown', handleClickOutside);
|
||||
}
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handleClickOutside);
|
||||
};
|
||||
}, [isOpen]);
|
||||
|
||||
const toggleDropdown = () => {
|
||||
setIsOpen(!isOpen);
|
||||
};
|
||||
|
||||
const selectSource = (source) => {
|
||||
setSelectedSource(source);
|
||||
setIsOpen(false);
|
||||
if (source.id === 'rawJson' || source.id === 'jsonSchema') {
|
||||
onChange(source.id);
|
||||
} else if (source.type === 'query') {
|
||||
onChange(source.value);
|
||||
forceCodeBox();
|
||||
}
|
||||
};
|
||||
|
||||
const renderCheckIcon = ({ id }) => {
|
||||
if (value === id) {
|
||||
return <SolidIcon name="check" width="16" height="16" fill="#4368E3" viewBox="0 0 16 16" />;
|
||||
} else {
|
||||
return <div style={{ width: '16px', height: '16px' }}></div>;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="tw-w-full tw-max-w-md dropdown-menu-inspector" ref={dropdownRef}>
|
||||
<div className="tw-relative">
|
||||
{/* Dropdown trigger div */}
|
||||
<button
|
||||
onClick={toggleDropdown}
|
||||
className={cx(
|
||||
'tw-flex tw-items-center tw-justify-between tw-w-full tw-px-4 tw-py-2 tw-text-left tw-bg-white dropdown-menu-trigger',
|
||||
{
|
||||
'is-open': isOpen,
|
||||
}
|
||||
)}
|
||||
>
|
||||
<div className="tw-flex tw-items-center">
|
||||
{selectedSource ? (
|
||||
<>
|
||||
<span className="tw-mr-2">{selectedSource.icon}</span>
|
||||
<span>{selectedSource.label}</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<span className="tw-mr-2 tw-text-gray-400">
|
||||
<SolidIcon name="code" width="16" height="16" fill="#CCD1D5" />
|
||||
</span>
|
||||
<span className="tw-text-gray-400 dropdown-menu-placeholder">Select a source</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<span className="tw-ml-2">
|
||||
{isOpen ? (
|
||||
<SolidIcon name="TriangleDownCenter" width={16} />
|
||||
) : (
|
||||
<SolidIcon name="TriangleUpCenter" width={16} />
|
||||
)}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
{/* Dropdown menu */}
|
||||
{isOpen && (
|
||||
<div className="tw-absolute tw-z-10 tw-w-full tw-mt-1 tw-bg-white tw-border tw-border-gray-300 tw-rounded-md tw-shadow-lg tw-p-2">
|
||||
{/* Source options section */}
|
||||
<div className="tw-py-1 dropdown-menu-items">
|
||||
{sourceOptions.map((option) => (
|
||||
<div
|
||||
key={option.id}
|
||||
onClick={() => selectSource(option)}
|
||||
className="tw-flex tw-items-center tw-w-full tw-px-4 tw-py-2 tw-text-left tw-hover:bg-gray-100"
|
||||
>
|
||||
{renderCheckIcon(option)}
|
||||
<span className="icon-image">{option.icon}</span>
|
||||
<span>{option.label}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{dataQueries.length > 0 && (
|
||||
<>
|
||||
{/* Divider with "From query" text */}
|
||||
<LabeledDivider label="From query" />
|
||||
|
||||
{/* Query options section */}
|
||||
<div className="tw-py-1 dropdown-menu-items">
|
||||
{queryOptions.map((option) => (
|
||||
<div
|
||||
key={option.id}
|
||||
onClick={() => selectSource(option)}
|
||||
className="tw-flex tw-items-center tw-w-full tw-px-4 tw-py-2 tw-text-left tw-hover:bg-gray-100"
|
||||
>
|
||||
{renderCheckIcon(option)}
|
||||
<span className="icon-image">{option.icon}</span>
|
||||
<span>{option.label}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { DropdownMenu as default } from './DropdownMenu';
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
.dropdown-menu-inspector {
|
||||
margin-top: 2px;
|
||||
font-size: 12px;
|
||||
|
||||
.dropdown-menu-trigger {
|
||||
height: 34px;
|
||||
padding: 7px 12px;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
align-self: stretch;
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--border-default, #CCD1D5);
|
||||
|
||||
&.is-open {
|
||||
border: 2px solid var(--interactive-focus-outline, #4368E3);
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-menu-placeholder {
|
||||
color: var(--text-placeholder, #6A727C);
|
||||
}
|
||||
|
||||
.dropdown-menu-items {
|
||||
color: var(--text-default, #1B1F24);
|
||||
|
||||
>div {
|
||||
border-radius: 6px;
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
background-color: var(--slate4);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.icon-image {
|
||||
margin: 0px 6px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.custom-line {
|
||||
border-color: var(--border-default, #CCD1D5);
|
||||
border-top: 0px
|
||||
}
|
||||
|
||||
.separator-text {
|
||||
color: var(--text-placeholder, #6A727C);
|
||||
background-color: white;
|
||||
padding: 0 6px;
|
||||
}
|
||||
}
|
||||
|
|
@ -7,7 +7,14 @@ import * as Icons from '@tabler/icons-react';
|
|||
import { VirtuosoGrid } from 'react-virtuoso';
|
||||
import { Visibility } from './Visibility';
|
||||
|
||||
export const Icon = ({ value, onChange, onVisibilityChange, styleDefinition, component }) => {
|
||||
export const Icon = ({
|
||||
value,
|
||||
onChange,
|
||||
onVisibilityChange,
|
||||
styleDefinition,
|
||||
component,
|
||||
isVisibilityEnabled = true,
|
||||
}) => {
|
||||
const [searchText, setSearchText] = useState('');
|
||||
const [showPopOver, setPopOverVisibility] = useState(false);
|
||||
const iconList = useRef(Object.keys(Icons));
|
||||
|
|
@ -111,13 +118,15 @@ export const Icon = ({ value, onChange, onVisibilityChange, styleDefinition, com
|
|||
>
|
||||
{String(value)}
|
||||
</div>
|
||||
<Visibility
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
onVisibilityChange={onVisibilityChange}
|
||||
component={component}
|
||||
styleDefinition={styleDefinition}
|
||||
/>
|
||||
{isVisibilityEnabled && (
|
||||
<Visibility
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
onVisibilityChange={onVisibilityChange}
|
||||
component={component}
|
||||
styleDefinition={styleDefinition}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</OverlayTrigger>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ export const Input = ({ value, onChange, cyLabel, meta }) => {
|
|||
className="tj-input-element tj-text-xsm"
|
||||
value={value}
|
||||
placeholder=""
|
||||
key={`${String(cyLabel)}-input`}
|
||||
id="labelId"
|
||||
onChange={(e) => {
|
||||
onChange(e.target.value);
|
||||
|
|
|
|||
|
|
@ -1,13 +1,18 @@
|
|||
import React, { useState } from 'react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
export const Number = ({ value, onChange, cyLabel }) => {
|
||||
const [number, setNumber] = useState(value ? value : 0);
|
||||
|
||||
useEffect(() => {
|
||||
setNumber(value);
|
||||
}, [value]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="field tj-app-input" style={{ padding: '0.225rem 0.35rem' }}>
|
||||
<input
|
||||
className={'inspector-field-number'}
|
||||
key={`${String(cyLabel)}-input`}
|
||||
type="number"
|
||||
onChange={(e) => {
|
||||
setNumber(e.target.value);
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import { drop } from 'lodash';
|
||||
|
||||
export const TypeMapping = {
|
||||
text: 'Text',
|
||||
string: 'Text',
|
||||
|
|
@ -20,5 +22,6 @@ export const TypeMapping = {
|
|||
visibility: 'Visibility',
|
||||
numberInput: 'NumberInput',
|
||||
tableRowHeightInput: 'TableRowHeightInput',
|
||||
dropdownMenu: 'DropdownMenu',
|
||||
query: 'Query',
|
||||
};
|
||||
|
|
|
|||
|
|
@ -20,7 +20,15 @@ const CODE_EDITOR_TYPE = {
|
|||
tjdbHinter: TJDBCodeEditor,
|
||||
};
|
||||
|
||||
const CodeHinter = ({ type = 'basic', initialValue, componentName, disabled, renderCopilot, ...restProps }) => {
|
||||
const CodeHinter = ({
|
||||
type = 'basic',
|
||||
initialValue,
|
||||
componentName,
|
||||
disabled,
|
||||
renderCopilot,
|
||||
setCodeEditorView,
|
||||
...restProps
|
||||
}) => {
|
||||
const darkMode = localStorage.getItem('darkMode') === 'true';
|
||||
|
||||
const [isOpen, setIsOpen] = React.useState(false);
|
||||
|
|
@ -71,6 +79,7 @@ const CodeHinter = ({ type = 'basic', initialValue, componentName, disabled, ren
|
|||
}}
|
||||
componentName={componentName}
|
||||
disabled={disabled}
|
||||
setCodeEditorView={setCodeEditorView}
|
||||
{...restProps}
|
||||
/>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import { Visibility } from '../CodeBuilder/Elements/Visibility';
|
|||
import { NumberInput } from '../CodeBuilder/Elements/NumberInput';
|
||||
import { Datepicker } from '../CodeBuilder/Elements/Datepicker';
|
||||
import TableRowHeightInput from '../CodeBuilder/Elements/TableRowHeightInput';
|
||||
import DropdownMenu from '../CodeBuilder/Elements/DropdownMenu';
|
||||
import { TimePicker } from '../CodeBuilder/Elements/TimePicker';
|
||||
import { Query } from '../CodeBuilder/Elements/Query';
|
||||
import { ColorSwatches } from '@/modules/Appbuilder/components';
|
||||
|
|
@ -41,6 +42,7 @@ const AllElements = {
|
|||
TableRowHeightInput,
|
||||
Datepicker,
|
||||
TimePicker,
|
||||
DropdownMenu,
|
||||
Query,
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -54,11 +54,15 @@ const MultiLineCodeEditor = (props) => {
|
|||
readOnly = false,
|
||||
editable = true,
|
||||
renderCopilot,
|
||||
setCodeEditorView,
|
||||
} = props;
|
||||
const replaceIdsWithName = useStore((state) => state.replaceIdsWithName, shallow);
|
||||
const wrapperRef = useRef(null);
|
||||
const getSuggestions = useStore((state) => state.getSuggestions, shallow);
|
||||
const getServerSideGlobalResolveSuggestions = useStore((state) => state.getServerSideGlobalResolveSuggestions, shallow);
|
||||
const getServerSideGlobalResolveSuggestions = useStore(
|
||||
(state) => state.getServerSideGlobalResolveSuggestions,
|
||||
shallow
|
||||
);
|
||||
|
||||
const isInsideQueryPane = !!document.querySelector('.code-hinter-wrapper')?.closest('.query-details');
|
||||
const isInsideQueryManager = useMemo(
|
||||
|
|
@ -72,13 +76,48 @@ const MultiLineCodeEditor = (props) => {
|
|||
|
||||
const currentValueRef = useRef(initialValue);
|
||||
|
||||
const handleChange = (val) => (currentValueRef.current = val);
|
||||
|
||||
const [editorView, setEditorView] = React.useState(null);
|
||||
|
||||
const [isSearchPanelOpen, setIsSearchPanelOpen] = React.useState(false);
|
||||
const { queryPanelKeybindings } = useQueryPanelKeyHooks(onChange, currentValueRef, 'multiline');
|
||||
|
||||
// Add state for tracking autocomplete visibility
|
||||
const [showSuggestions, setShowSuggestions] = React.useState(true);
|
||||
const currentLineObserverRef = useRef(null);
|
||||
const isObserverTriggeredRef = useRef(false);
|
||||
|
||||
// Intersection observer to detect when current line goes out of view
|
||||
useEffect(() => {
|
||||
const observer = new IntersectionObserver(
|
||||
([entry]) => {
|
||||
if (entry.intersectionRatio < 1) {
|
||||
setShowSuggestions(false);
|
||||
isObserverTriggeredRef.current = true;
|
||||
// Close autocomplete dropdown by dispatching a selection change
|
||||
if (editorView) {
|
||||
editorView.dispatch({
|
||||
selection: editorView.state.selection,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
setShowSuggestions(true);
|
||||
isObserverTriggeredRef.current = false;
|
||||
}
|
||||
},
|
||||
{ root: null, threshold: [1] }
|
||||
);
|
||||
|
||||
currentLineObserverRef.current = observer;
|
||||
|
||||
return () => {
|
||||
if (currentLineObserverRef.current) {
|
||||
currentLineObserverRef.current.disconnect();
|
||||
}
|
||||
};
|
||||
}, [editorView]);
|
||||
|
||||
const handleChange = (val) => (currentValueRef.current = val);
|
||||
|
||||
const handleOnBlur = () => {
|
||||
if (!delayOnChange) return onChange(currentValueRef.current);
|
||||
setTimeout(() => {
|
||||
|
|
@ -276,6 +315,21 @@ const MultiLineCodeEditor = (props) => {
|
|||
return initialValue;
|
||||
}, [initialValue, replaceIdsWithName]);
|
||||
|
||||
function updateCurrentLineObserver(editorView) {
|
||||
if (!editorView || !editorView?.view?.dom) return;
|
||||
const cursorPos = editorView.state.selection.main.head;
|
||||
const line = editorView.state.doc.lineAt(cursorPos);
|
||||
const lineNumber = line.number;
|
||||
const cmLines = editorView.view.dom.querySelectorAll('.cm-line');
|
||||
const currentLineDiv = cmLines[lineNumber - 1] || null;
|
||||
|
||||
// Update intersection observer to watch the current line
|
||||
if (currentLineObserverRef.current && currentLineDiv && !isObserverTriggeredRef.current) {
|
||||
currentLineObserverRef.current.disconnect();
|
||||
currentLineObserverRef.current.observe(currentLineDiv);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`code-hinter-wrapper position-relative ${isInsideQueryPane ? 'code-editor-query-panel' : ''}`}
|
||||
|
|
@ -349,8 +403,16 @@ const MultiLineCodeEditor = (props) => {
|
|||
indentWithTab={false}
|
||||
readOnly={readOnly}
|
||||
editable={editable} //for transformations in query manager
|
||||
onCreateEditor={(view) => setEditorView(view)}
|
||||
onUpdate={(view) => setIsSearchPanelOpen(searchPanelOpen(view.state))}
|
||||
onCreateEditor={(view) => {
|
||||
setEditorView(view);
|
||||
if (setCodeEditorView) {
|
||||
setCodeEditorView(view);
|
||||
}
|
||||
}}
|
||||
onUpdate={(view) => {
|
||||
setIsSearchPanelOpen(searchPanelOpen(view.state));
|
||||
updateCurrentLineObserver(view);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{showPreview && (
|
||||
|
|
|
|||
|
|
@ -489,7 +489,14 @@ const PreviewContainer = ({
|
|||
};
|
||||
|
||||
const PreviewCodeBlock = ({ code, isExpectValue = false, isLargeDataset }) => {
|
||||
let preview = code && code.trim ? code?.trim() : `${code}`;
|
||||
let preview;
|
||||
if (typeof code === 'string') {
|
||||
preview = code.trim();
|
||||
} else if (typeof code === 'symbol') {
|
||||
preview = code.toString();
|
||||
} else {
|
||||
preview = String(code);
|
||||
}
|
||||
|
||||
const shouldTrim = preview.length > 35;
|
||||
let showJSONTree = false;
|
||||
|
|
|
|||
|
|
@ -28,10 +28,12 @@ import CodeHinter from './CodeHinter';
|
|||
import { removeNestedDoubleCurlyBraces } from '@/_helpers/utils';
|
||||
import useStore from '@/AppBuilder/_stores/store';
|
||||
import { shallow } from 'zustand/shallow';
|
||||
import { getCssVarValue } from '@/Editor/Components/utils';
|
||||
import { useModuleContext } from '@/AppBuilder/_contexts/ModuleContext';
|
||||
import { CodeHinterContext } from '../CodeBuilder/CodeHinterContext';
|
||||
import { createReferencesLookup } from '@/_stores/utils';
|
||||
import { useQueryPanelKeyHooks } from './useQueryPanelKeyHooks';
|
||||
import Icon from '@/_ui/Icon/solidIcons/index';
|
||||
|
||||
const SingleLineCodeEditor = ({ componentName, fieldMeta = {}, componentId, ...restProps }) => {
|
||||
const { moduleId } = useModuleContext();
|
||||
|
|
@ -79,7 +81,6 @@ const SingleLineCodeEditor = ({ componentName, fieldMeta = {}, componentId, ...r
|
|||
|
||||
const replaceIdsWithName = useStore((state) => state.replaceIdsWithName, shallow);
|
||||
let newInitialValue = initialValue;
|
||||
|
||||
if (typeof initialValue === 'string' && (initialValue?.includes('components') || initialValue?.includes('queries'))) {
|
||||
newInitialValue = replaceIdsWithName(initialValue);
|
||||
}
|
||||
|
|
@ -209,6 +210,7 @@ const EditorInput = ({
|
|||
onInputChange,
|
||||
wrapperRef,
|
||||
showSuggestions,
|
||||
setCodeEditorView = null, // Function to set the CodeMirror view
|
||||
}) => {
|
||||
const codeHinterContext = useContext(CodeHinterContext);
|
||||
const { suggestionList: paramHints } = createReferencesLookup(codeHinterContext, true);
|
||||
|
|
@ -216,7 +218,10 @@ const EditorInput = ({
|
|||
const getSuggestions = useStore((state) => state.getSuggestions, shallow);
|
||||
const [codeMirrorView, setCodeMirrorView] = useState(undefined);
|
||||
|
||||
const getServerSideGlobalResolveSuggestions = useStore((state) => state.getServerSideGlobalResolveSuggestions, shallow);
|
||||
const getServerSideGlobalResolveSuggestions = useStore(
|
||||
(state) => state.getServerSideGlobalResolveSuggestions,
|
||||
shallow
|
||||
);
|
||||
|
||||
const { queryPanelKeybindings } = useQueryPanelKeyHooks(onBlurUpdate, currentValue, 'singleline');
|
||||
|
||||
|
|
@ -274,7 +279,10 @@ const EditorInput = ({
|
|||
}
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
const overRideFunction = React.useCallback((context) => autoCompleteExtensionConfig(context), [isInsideQueryManager, paramHints]);
|
||||
const overRideFunction = React.useCallback(
|
||||
(context) => autoCompleteExtensionConfig(context),
|
||||
[isInsideQueryManager, paramHints]
|
||||
);
|
||||
|
||||
const autoCompleteConfig = autocompletion({
|
||||
override: [overRideFunction],
|
||||
|
|
@ -443,6 +451,9 @@ const EditorInput = ({
|
|||
<CodeMirror
|
||||
onCreateEditor={(view) => {
|
||||
setCodeMirrorView(view);
|
||||
if (setCodeEditorView) {
|
||||
setCodeEditorView(view);
|
||||
}
|
||||
}}
|
||||
value={currentValue}
|
||||
placeholder={placeholder}
|
||||
|
|
@ -451,11 +462,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) => {
|
||||
|
|
@ -487,9 +498,9 @@ const EditorInput = ({
|
|||
}}
|
||||
/>
|
||||
</div>
|
||||
</ErrorBoundary >
|
||||
</CodeHinter.Portal >
|
||||
</div >
|
||||
</ErrorBoundary>
|
||||
</CodeHinter.Portal>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
@ -514,24 +525,49 @@ const DynamicEditorBridge = (props) => {
|
|||
|
||||
const [forceCodeBox, setForceCodeBox] = React.useState(fxActive);
|
||||
const codeShow = paramType === 'code' || forceCodeBox;
|
||||
const HIDDEN_CODE_HINTER_LABELS = ['Table data', 'Column data', 'Text Format'];
|
||||
const { isFxNotRequired } = fieldMeta;
|
||||
const HIDDEN_CODE_HINTER_LABELS = ['Table data', 'Column data', 'Text Format', 'Slider type'];
|
||||
const { isFxNotRequired, newLine = false, section = '' } = fieldMeta;
|
||||
const isDeprecated = section === 'deprecated';
|
||||
const { t } = useTranslation();
|
||||
const [_, error, value] = type === 'fxEditor' ? resolveReferences(initialValue) : [];
|
||||
const replaceIdsWithName = useStore((state) => state.replaceIdsWithName, shallow);
|
||||
let newInitialValue = initialValue,
|
||||
shouldResolve = true;
|
||||
|
||||
// This is to handle the case when the initial value is a string and contains components or queries
|
||||
// and we need to replace the ids with names
|
||||
// but we don't want to resolve the references as it needs to be displayed as it is
|
||||
if (paramName === 'generateFormFrom') {
|
||||
if (
|
||||
typeof initialValue === 'string' &&
|
||||
(initialValue?.includes('components') || initialValue?.includes('queries'))
|
||||
) {
|
||||
newInitialValue = replaceIdsWithName(initialValue);
|
||||
shouldResolve = false;
|
||||
}
|
||||
}
|
||||
const [_, error, value] =
|
||||
type === 'fxEditor' ? (shouldResolve ? resolveReferences(newInitialValue) : [false, '', newInitialValue]) : [];
|
||||
let cyLabel = paramLabel ? paramLabel.toLowerCase().trim().replace(/\s+/g, '-') : props.cyLabel;
|
||||
|
||||
useEffect(() => {
|
||||
setForceCodeBox(fxActive);
|
||||
}, [component, fxActive]);
|
||||
|
||||
let modifiedValue = initialValue;
|
||||
if (paramType === 'colorSwatches' && typeof initialValue === 'string' && initialValue?.includes('var(')) {
|
||||
modifiedValue = getCssVarValue(document.documentElement, initialValue);
|
||||
}
|
||||
|
||||
const renderFx = () => {
|
||||
if (paramType === 'query' || !(paramLabel !== 'Type' && isFxNotRequired === undefined)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<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}
|
||||
|
|
@ -539,6 +575,9 @@ const DynamicEditorBridge = (props) => {
|
|||
if (codeShow) {
|
||||
setForceCodeBox(false);
|
||||
onFxPress(false);
|
||||
if (paramType === 'colorSwatches') {
|
||||
onChange(modifiedValue);
|
||||
}
|
||||
} else {
|
||||
setForceCodeBox(true);
|
||||
onFxPress(true);
|
||||
|
|
@ -551,48 +590,69 @@ const DynamicEditorBridge = (props) => {
|
|||
};
|
||||
|
||||
const fxClass = isEventManagerParam ? 'justify-content-start' : 'justify-content-end';
|
||||
return (
|
||||
<div className={cx({ 'codeShow-active': codeShow }, 'wrapper-div-code-editor')}>
|
||||
<div className={cx('d-flex align-items-center justify-content-between code-flex-wrapper')}>
|
||||
|
||||
const renderedLabel = () => {
|
||||
return (
|
||||
<>
|
||||
{paramLabel !== ' ' && !HIDDEN_CODE_HINTER_LABELS.includes(paramLabel) && (
|
||||
<div className={`field ${className}`} data-cy={`${cyLabel}-widget-parameter-label`}>
|
||||
<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'
|
||||
}`}
|
||||
/>
|
||||
{isDeprecated && (
|
||||
<span className={'list-item-deprecated-column-type'}>
|
||||
<Icon name={'warning'} height={14} width={14} fill="#DB4324" />
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const renderDynamicFx = () => {
|
||||
if (codeShow) return null;
|
||||
return (
|
||||
<DynamicFxTypeRenderer
|
||||
value={!error ? value : ''}
|
||||
onChange={onChange}
|
||||
paramName={paramName}
|
||||
paramLabel={paramLabel}
|
||||
paramType={paramType}
|
||||
forceCodeBox={() => {
|
||||
setForceCodeBox(true);
|
||||
onFxPress(true);
|
||||
}}
|
||||
meta={fieldMeta}
|
||||
cyLabel={cyLabel}
|
||||
styleDefinition={styleDefinition}
|
||||
component={component}
|
||||
onVisibilityChange={onVisibilityChange}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={cx({ 'codeShow-active': codeShow }, 'wrapper-div-code-editor')}>
|
||||
<div className={cx('d-flex align-items-center justify-content-between code-flex-wrapper')}>
|
||||
{renderedLabel()}
|
||||
<div className={`${(paramType ?? 'code') === 'code' ? 'd-none' : ''} flex-grow-1`}>
|
||||
<div style={{ marginBottom: codeShow ? '0.5rem' : '0px' }} className={`d-flex align-items-center ${fxClass}`}>
|
||||
{renderFx()}
|
||||
</div>
|
||||
</div>
|
||||
{!codeShow && (
|
||||
<DynamicFxTypeRenderer
|
||||
value={!error ? value : ''}
|
||||
onChange={onChange}
|
||||
paramName={paramName}
|
||||
paramLabel={paramLabel}
|
||||
paramType={paramType}
|
||||
forceCodeBox={() => {
|
||||
setForceCodeBox(true);
|
||||
onFxPress(true);
|
||||
}}
|
||||
meta={fieldMeta}
|
||||
cyLabel={cyLabel}
|
||||
styleDefinition={styleDefinition}
|
||||
component={component}
|
||||
onVisibilityChange={onVisibilityChange}
|
||||
/>
|
||||
)}
|
||||
{!newLine && renderDynamicFx()}
|
||||
</div>
|
||||
{newLine && renderDynamicFx()}
|
||||
{codeShow && (
|
||||
<div className={`row custom-row`} style={{ display: codeShow ? 'flex' : 'none' }}>
|
||||
<div className={`col code-hinter-col`}>
|
||||
<div className="d-flex">
|
||||
<SingleLineCodeEditor initialValue {...props} />
|
||||
<SingleLineCodeEditor {...props} initialValue={modifiedValue} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -660,6 +660,13 @@
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
.code-editor-component {
|
||||
.cm-editor {
|
||||
min-height: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.cm-searchMatch.cm-searchMatch-selected {
|
||||
background-color: #F28F2D !important;
|
||||
}
|
||||
|
|
@ -673,4 +680,4 @@
|
|||
.cm-theme{
|
||||
height: 100% ;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -367,6 +367,7 @@ export const FxParamTypeMapping = Object.freeze({
|
|||
visibility: 'Visibility',
|
||||
numberInput: 'NumberInput',
|
||||
tableRowHeightInput: 'TableRowHeightInput',
|
||||
dropdownMenu: 'DropdownMenu',
|
||||
query: 'Query',
|
||||
});
|
||||
|
||||
|
|
|
|||
73
frontend/src/AppBuilder/EmbedApp.jsx
Normal file
73
frontend/src/AppBuilder/EmbedApp.jsx
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
import React, { useEffect } from 'react';
|
||||
import useRouter from '@/_hooks/use-router';
|
||||
import config from 'config';
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
// In-memory PAT token store
|
||||
let inMemoryPatToken = null;
|
||||
|
||||
export function setPatToken(patToken) {
|
||||
inMemoryPatToken = patToken;
|
||||
}
|
||||
|
||||
export function getPatToken() {
|
||||
if (inMemoryPatToken) return inMemoryPatToken;
|
||||
}
|
||||
|
||||
export default function EmbedAppRedirect() {
|
||||
const router = useRouter();
|
||||
const { appId } = router.query;
|
||||
|
||||
useEffect(() => {
|
||||
// 🔐 Ensure the page is embedded
|
||||
if (window.self === window.top) {
|
||||
// Not inside an iframe
|
||||
toast.error('This page must be embedded inside a parent application.');
|
||||
return;
|
||||
}
|
||||
const token = new URLSearchParams(window.location.search).get('personal-access-token');
|
||||
|
||||
if (!token || typeof appId !== 'string') {
|
||||
parent?.postMessage({ type: 'TJ_EMBED_APP_LOGOUT', error: 400, message: 'Missing token or appId' }, '*');
|
||||
return;
|
||||
}
|
||||
|
||||
const initiateSession = async () => {
|
||||
try {
|
||||
const res = await fetch(`${config.apiUrl}/ext/users/session`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
credentials: 'include',
|
||||
body: JSON.stringify({ appId, accessToken: token }),
|
||||
});
|
||||
if (!res.ok) {
|
||||
if (res.status === 401 || res.status === 403) {
|
||||
toast.error('Your pat is expired. Please refresh or contact your admin.');
|
||||
// 🔔 Show toast if token is expired or invalid
|
||||
parent?.postMessage(
|
||||
{
|
||||
type: 'TJ_EMBED_APP_LOGOUT',
|
||||
error: res.status,
|
||||
message: 'Your pat is expired. Please refresh or contact your admin.',
|
||||
},
|
||||
'*'
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await res.json();
|
||||
// ✅ Store PAT in memory
|
||||
setPatToken(result.signedPat);
|
||||
window.name = result.signedPat;
|
||||
window.location.href = `applications/${appId}`;
|
||||
} catch (error) {
|
||||
parent?.postMessage({ type: 'TJ_EMBED_APP_LOGOUT', error: 500, message: 'Network error' }, '*');
|
||||
}
|
||||
};
|
||||
|
||||
initiateSession();
|
||||
}, [appId]);
|
||||
|
||||
return <div>Loading embedded app...</div>;
|
||||
}
|
||||
20
frontend/src/AppBuilder/Header/AppCanvasBanner.jsx
Normal file
20
frontend/src/AppBuilder/Header/AppCanvasBanner.jsx
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
import React from 'react';
|
||||
import { withEditionSpecificComponent } from '@/modules/common/helpers/withEditionSpecificComponent';
|
||||
import useStore from '@/AppBuilder/_stores/store';
|
||||
import { useModuleContext } from '@/AppBuilder/_contexts/ModuleContext';
|
||||
import FreezeVersionInfo from '@/AppBuilder/Header/FreezeVersionInfo';
|
||||
import { shallow } from 'zustand/shallow';
|
||||
|
||||
const AppCanvasBanner = ({ appId = '' }) => {
|
||||
const { moduleId } = useModuleContext();
|
||||
const currentMode = useStore((state) => state.modeStore.modules[moduleId].currentMode, shallow);
|
||||
const renderBanner = () => {
|
||||
if (currentMode === 'edit') {
|
||||
return <FreezeVersionInfo hide={false} />;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
return <div>{renderBanner()}</div>;
|
||||
};
|
||||
|
||||
export default withEditionSpecificComponent(AppCanvasBanner, 'Appbuilder');
|
||||
|
|
@ -16,15 +16,11 @@ const CreateVersionModal = ({
|
|||
canCommit,
|
||||
orgGit,
|
||||
fetchingOrgGit,
|
||||
handleCommitOnVersionCreation = () => {},
|
||||
handleCommitOnVersionCreation = () => { },
|
||||
}) => {
|
||||
const { moduleId } = useModuleContext();
|
||||
const [isCreatingVersion, setIsCreatingVersion] = useState(false);
|
||||
const [versionName, setVersionName] = useState('');
|
||||
const gitSyncEnabled =
|
||||
orgGit?.org_git?.git_https?.is_enabled ||
|
||||
orgGit?.org_git?.git_ssh?.is_enabled ||
|
||||
orgGit?.org_git?.git_lab?.is_enabled;
|
||||
|
||||
const {
|
||||
createNewVersionAction,
|
||||
|
|
@ -33,6 +29,7 @@ const CreateVersionModal = ({
|
|||
appId,
|
||||
setCurrentVersionId,
|
||||
selectedVersion,
|
||||
currentMode,
|
||||
} = useStore(
|
||||
(state) => ({
|
||||
createNewVersionAction: state.createNewVersionAction,
|
||||
|
|
@ -45,6 +42,7 @@ const CreateVersionModal = ({
|
|||
currentVersionId: state.currentVersionId,
|
||||
setCurrentVersionId: state.setCurrentVersionId,
|
||||
selectedVersion: state.selectedVersion,
|
||||
currentMode: state.currentMode,
|
||||
}),
|
||||
shallow
|
||||
);
|
||||
|
|
@ -94,7 +92,7 @@ const CreateVersionModal = ({
|
|||
setIsCreatingVersion(false);
|
||||
setShowCreateAppVersion(false);
|
||||
appVersionService
|
||||
.getAppVersionData(appId, newVersion.id)
|
||||
.getAppVersionData(appId, newVersion.id, currentMode)
|
||||
.then((data) => {
|
||||
setCurrentVersionId(newVersion.id);
|
||||
handleCommitOnVersionCreation(data);
|
||||
|
|
@ -104,8 +102,8 @@ const CreateVersionModal = ({
|
|||
});
|
||||
},
|
||||
(error) => {
|
||||
if (error?.data?.code === '23505') {
|
||||
toast.error('Version name already exists.');
|
||||
if (error?.data?.code === "23505") {
|
||||
toast.error("Version name already exists.");
|
||||
} else {
|
||||
toast.error(error?.error);
|
||||
}
|
||||
|
|
@ -174,7 +172,7 @@ const CreateVersionModal = ({
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{gitSyncEnabled && (
|
||||
{orgGit?.org_git?.is_enabled && (
|
||||
<div className="commit-changes" style={{ marginTop: '-1rem', marginBottom: '2rem' }}>
|
||||
<div>
|
||||
<input
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import React, { useState } from 'react';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import cx from 'classnames';
|
||||
import Select from '@/_ui/Select';
|
||||
import { components } from 'react-select';
|
||||
|
|
@ -8,18 +8,27 @@ import { ToolTip } from '@/_components/ToolTip';
|
|||
import EditWhite from '@assets/images/icons/edit-white.svg';
|
||||
import { defaultAppEnvironments, decodeEntities } from '@/_helpers/utils';
|
||||
import { CreateVersionModal } from '@/modules/Appbuilder/components';
|
||||
import useStore from '@/AppBuilder/_stores/store';
|
||||
|
||||
// TODO: edit version modal and add version modal
|
||||
const Menu = (props) => {
|
||||
const isEditable = props.selectProps.isEditable;
|
||||
const creationMode = props?.selectProps?.appCreationMode;
|
||||
const allowAppEdit = useStore((state) => state.allowEditing);
|
||||
const [isVersionCreationEnabled, setIsVersionCreationEnabled] = useState(
|
||||
creationMode !== 'GIT' || (creationMode === 'GIT' && allowAppEdit)
|
||||
);
|
||||
useEffect(() => {
|
||||
setIsVersionCreationEnabled(creationMode !== 'GIT' || (creationMode === 'GIT' && allowAppEdit));
|
||||
}, [allowAppEdit, creationMode]);
|
||||
|
||||
return (
|
||||
<components.Menu {...props}>
|
||||
<div>
|
||||
{isEditable && !props?.selectProps?.value?.isReleasedVersion && (
|
||||
<ToolTip
|
||||
message="Versions created from git cannot be edited"
|
||||
show={props?.selectProps?.appCreationMode === 'GIT'}
|
||||
message="New versions cannot be created for non-editable apps"
|
||||
show={!isVersionCreationEnabled}
|
||||
placement="right"
|
||||
>
|
||||
<div
|
||||
|
|
@ -27,7 +36,7 @@ const Menu = (props) => {
|
|||
style={{ padding: '8px 12px' }}
|
||||
onClick={() =>
|
||||
!props?.selectProps?.value?.isReleasedVersion &&
|
||||
props?.selectProps?.appCreationMode !== 'GIT' &&
|
||||
isVersionCreationEnabled &&
|
||||
props.selectProps.setShowEditAppVersion(true)
|
||||
}
|
||||
>
|
||||
|
|
@ -49,18 +58,18 @@ const Menu = (props) => {
|
|||
<div>{props.children}</div>
|
||||
{isEditable && (
|
||||
<ToolTip
|
||||
message={'New versions cannot be created for git imported apps'}
|
||||
show={props?.selectProps?.appCreationMode === 'GIT'}
|
||||
message={'New versions cannot be created for non-editable apps'}
|
||||
show={!isVersionCreationEnabled}
|
||||
placement="right"
|
||||
>
|
||||
<div
|
||||
className="cursor-pointer tj-text-xsm"
|
||||
style={{
|
||||
padding: '8px 12px',
|
||||
color: `${props?.selectProps?.appCreationMode !== 'GIT' ? '#3E63DD' : '#C1C8CD'}`,
|
||||
cursor: `${props?.selectProps?.appCreationMode !== 'GIT' ? 'pointer' : 'none'}`,
|
||||
color: `${isVersionCreationEnabled ? '#3E63DD' : '#C1C8CD'}`,
|
||||
cursor: `${isVersionCreationEnabled ? 'pointer' : 'none'}`,
|
||||
}}
|
||||
onClick={() => props?.selectProps?.appCreationMode !== 'GIT' && props?.setShowCreateAppVersion(true)}
|
||||
onClick={() => isVersionCreationEnabled && props?.setShowCreateAppVersion(true)}
|
||||
data-cy="create-new-version-button"
|
||||
>
|
||||
<svg
|
||||
|
|
@ -76,7 +85,7 @@ const Menu = (props) => {
|
|||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M17 11C17.4142 11 17.75 11.3358 17.75 11.75V16.25H22.25C22.6642 16.25 23 16.5858 23 17C23 17.4142 22.6642 17.75 22.25 17.75H17.75V22.25C17.75 22.6642 17.4142 23 17 23C16.5858 23 16.25 22.6642 16.25 22.25V17.75H11.75C11.3358 17.75 11 17.4142 11 17C11 16.5858 11.3358 16.25 11.75 16.25H16.25V11.75C16.25 11.3358 16.5858 11 17 11Z"
|
||||
fill={`${props?.selectProps?.appCreationMode !== 'GIT' ? '#3E63DD' : '#C1C8CD'}`}
|
||||
fill={`${isVersionCreationEnabled ? '#3E63DD' : '#C1C8CD'}`}
|
||||
/>
|
||||
</svg>
|
||||
Create new version
|
||||
|
|
|
|||
|
|
@ -149,7 +149,6 @@ function EditAppName() {
|
|||
value={name}
|
||||
maxLength={50}
|
||||
data-cy="app-name-input"
|
||||
disabled={appCreationMode === 'GIT'}
|
||||
/>
|
||||
</ToolTip>
|
||||
<InfoOrErrorBox
|
||||
|
|
|
|||
|
|
@ -10,8 +10,10 @@ import { resolveReferences } from '@/_helpers/utils';
|
|||
import FxButton from '@/Editor/CodeBuilder/Elements/FxButton';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Confirm } from '@/Editor/Viewer/Confirm';
|
||||
import { ColorSwatches } from '@/modules/Appbuilder/components';
|
||||
import { shallow } from 'zustand/shallow';
|
||||
import { useModuleContext } from '@/AppBuilder/_contexts/ModuleContext';
|
||||
import { getCssVarValue } from '@/Editor/Components/utils';
|
||||
|
||||
const CanvasSettings = ({ darkMode }) => {
|
||||
const { moduleId } = useModuleContext();
|
||||
|
|
@ -117,77 +119,64 @@ const CanvasSettings = ({ darkMode }) => {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div className="d-flex justify-content-between mb-3">
|
||||
<div className="d-flex mb-3" style={{ height: '42px', gap: '20px' }}>
|
||||
<span className="pt-2" data-cy={`label-bg-canvas`}>
|
||||
{t('leftSidebar.Settings.backgroundColorOfCanvas', 'Canvas bavkground')}
|
||||
</span>
|
||||
<div className="canvas-codehinter-container">
|
||||
{showPicker && (
|
||||
<div>
|
||||
<div style={coverStyles} onClick={() => setShowPicker(false)} />
|
||||
<SketchPicker
|
||||
data-cy={`color-picker-canvas`}
|
||||
className="canvas-background-picker"
|
||||
onFocus={() => setShowPicker(true)}
|
||||
color={canvasBackgroundColor}
|
||||
onChangeComplete={(color) => {
|
||||
<div className={`fx-canvas `}>
|
||||
<FxButton
|
||||
dataCy={`canvas-bg-color`}
|
||||
active={!forceCodeBox ? true : false}
|
||||
onPress={async () => {
|
||||
if (typeof canvasBackgroundColor === 'string' && canvasBackgroundColor?.includes('var(')) {
|
||||
const value = getCssVarValue(document.documentElement, canvasBackgroundColor);
|
||||
const options = {
|
||||
canvasBackgroundColor: [color.hex, color.rgb],
|
||||
backgroundFxQuery: '',
|
||||
canvasBackgroundColor: value,
|
||||
backgroundFxQuery: value,
|
||||
};
|
||||
globalSettingsChanged(options);
|
||||
resolveOthers('canvas', true, { canvasBackgroundColor: [color.hex, color.rgb] });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
await Promise.resolve(globalSettingsChanged(options));
|
||||
await Promise.resolve(resolveOthers('canvas', true, { canvasBackgroundColor: value }));
|
||||
}
|
||||
setForceCodeBox(!forceCodeBox);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{forceCodeBox && (
|
||||
<div className="row mx-0 color-picker-input d-flex" onClick={() => setShowPicker(true)} style={outerStyles}>
|
||||
<div
|
||||
data-cy={`canvas-bg-color-picker`}
|
||||
className="col-auto"
|
||||
style={{
|
||||
float: 'right',
|
||||
width: '24px',
|
||||
height: '24px',
|
||||
backgroundColor: canvasBackgroundColor,
|
||||
borderRadius: ' 6px',
|
||||
border: `1px solid var(--slate7, #D7DBDF)`,
|
||||
boxShadow: `0px 1px 2px 0px rgba(16, 24, 40, 0.05)`,
|
||||
}}
|
||||
></div>
|
||||
<div style={{ height: '20px' }} className="col">
|
||||
{canvasBackgroundColor}
|
||||
</div>
|
||||
</div>
|
||||
<ColorSwatches
|
||||
data-cy={`color-picker-canvas`}
|
||||
outerWidth="155px"
|
||||
value={canvasBackgroundColor}
|
||||
onChange={(color) => {
|
||||
const options = {
|
||||
canvasBackgroundColor: resolveReferences(color),
|
||||
backgroundFxQuery: color,
|
||||
};
|
||||
globalSettingsChanged(options);
|
||||
resolveOthers('canvas', true, { canvasBackgroundColor: color });
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<div className={`${!forceCodeBox && 'hinter-canvas-input'} `}>
|
||||
{!forceCodeBox && (
|
||||
<CodeHinter
|
||||
cyLabel={`canvas-bg-colour`}
|
||||
initialValue={backgroundFxQuery ? backgroundFxQuery : canvasBackgroundColor}
|
||||
lang="javascript"
|
||||
className="canvas-hinter-wrap"
|
||||
lineNumbers={false}
|
||||
onChange={(color) => {
|
||||
const options = {
|
||||
canvasBackgroundColor: resolveReferences(color),
|
||||
backgroundFxQuery: color,
|
||||
};
|
||||
globalSettingsChanged(options);
|
||||
resolveOthers('canvas', true, { canvasBackgroundColor: color });
|
||||
}}
|
||||
/>
|
||||
<div className="canvas-hinter-wrap-container">
|
||||
<CodeHinter
|
||||
cyLabel={`canvas-bg-colour`}
|
||||
initialValue={backgroundFxQuery ? backgroundFxQuery : canvasBackgroundColor}
|
||||
lang="javascript"
|
||||
className="canvas-hinter-wrap"
|
||||
lineNumbers={false}
|
||||
onChange={(color) => {
|
||||
const options = {
|
||||
canvasBackgroundColor: resolveReferences(color),
|
||||
backgroundFxQuery: color,
|
||||
};
|
||||
globalSettingsChanged(options);
|
||||
resolveOthers('canvas', true, { canvasBackgroundColor: color });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className={`fx-canvas `}>
|
||||
<FxButton
|
||||
dataCy={`canvas-bg-color`}
|
||||
active={!forceCodeBox ? true : false}
|
||||
onPress={() => {
|
||||
setForceCodeBox(!forceCodeBox);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -5,13 +5,14 @@ import cx from 'classnames';
|
|||
import { shallow } from 'zustand/shallow';
|
||||
import { DarkModeToggle } from '@/_components';
|
||||
import Popover from '@/_ui/Popover';
|
||||
import { PageMenu } from './PageMenu';
|
||||
// import { PageMenu } from './PageMenu';
|
||||
import LeftSidebarInspector from './LeftSidebarInspector/LeftSidebarInspector';
|
||||
import GlobalSettings from './GlobalSettings';
|
||||
import '../../_styles/left-sidebar.scss';
|
||||
import Debugger from './Debugger/Debugger';
|
||||
import { useModuleContext } from '@/AppBuilder/_contexts/ModuleContext';
|
||||
import { withEditionSpecificComponent } from '@/modules/common/helpers/withEditionSpecificComponent';
|
||||
import { PageMenu } from '../RightSideBar/PageSettingsTab/PageMenu';
|
||||
|
||||
// TODO: remove passing refs to LeftSidebarItem and use state
|
||||
// TODO: need to add datasources to the sidebar.
|
||||
|
|
@ -58,7 +59,6 @@ export const BaseLeftSidebar = ({
|
|||
const sideBarBtnRefs = useRef({});
|
||||
|
||||
const handleSelectedSidebarItem = (item) => {
|
||||
pinned && localStorage.setItem('selectedSidebarItem', item);
|
||||
if (item === 'debugger') resetUnreadErrorCount();
|
||||
setSelectedSidebarItem(item);
|
||||
if (item === selectedSidebarItem && !pinned) {
|
||||
|
|
@ -211,15 +211,6 @@ export const BaseLeftSidebar = ({
|
|||
tip: 'Build with AI',
|
||||
ref: setSideBarBtnRefs('tooljetai'),
|
||||
})}
|
||||
<SidebarItem
|
||||
selectedSidebarItem={selectedSidebarItem}
|
||||
onClick={() => handleSelectedSidebarItem('page')}
|
||||
darkMode={darkMode}
|
||||
icon="page"
|
||||
className={`left-sidebar-item left-sidebar-layout left-sidebar-page-selector`}
|
||||
tip="Pages"
|
||||
ref={setSideBarBtnRefs('page')}
|
||||
/>
|
||||
{renderCommonItems()}
|
||||
<SidebarItem
|
||||
icon="settings"
|
||||
|
|
|
|||
|
|
@ -103,7 +103,9 @@ export const Node = (props) => {
|
|||
marginTop: level === 1 ? 4 : 0,
|
||||
marginBottom: level === 1 ? 4 : 0,
|
||||
// borderLeft: level > 1 ? '1px solid var(--slate6, #D7DBDF)' : 'none',
|
||||
cursor: level === 1 ? 'pointer' : 'default',
|
||||
}}
|
||||
{...(level === 1 && { onClick: () => onExpand(props) })}
|
||||
>
|
||||
{/* {!['queries', 'globals', 'variables'].includes(type) && ( */}
|
||||
<div className="node-expansion-icon">
|
||||
|
|
|
|||
|
|
@ -1,317 +0,0 @@
|
|||
import React, { memo, useRef, useState, useCallback } from 'react';
|
||||
import cx from 'classnames';
|
||||
// import { RenameInput } from './RenameInput';
|
||||
// import { PagehandlerMenu } from './PagehandlerMenu';
|
||||
// import { EditModal } from './EditModal';
|
||||
// import { SettingsModal } from './SettingsModal';
|
||||
import { useAppVersionStore } from '@/_stores/appVersionStore';
|
||||
import SolidIcon from '@/_ui/Icon/SolidIcons';
|
||||
import EyeDisable from '@/_ui/Icon/solidIcons/EyeDisable';
|
||||
import FileRemove from '@/_ui/Icon/solidIcons/FIleRemove';
|
||||
import Home from '@/_ui/Icon/solidIcons/Home';
|
||||
import useStore from '@/AppBuilder/_stores/store';
|
||||
import _ from 'lodash';
|
||||
import { toast } from 'react-hot-toast';
|
||||
import { RenameInput } from './RenameInput';
|
||||
import IconSelector from './IconSelector';
|
||||
import { withRouter } from '@/_hoc/withRouter';
|
||||
import OverflowTooltip from '@/_components/OverflowTooltip';
|
||||
import { useModuleContext } from '@/AppBuilder/_contexts/ModuleContext';
|
||||
import { shallow } from 'zustand/shallow';
|
||||
import { ToolTip } from '@/_components/ToolTip';
|
||||
|
||||
export const PageMenuItem = withRouter(
|
||||
memo(({ darkMode, page, navigate }) => {
|
||||
const { moduleId } = useModuleContext();
|
||||
const homePageId = useStore((state) => state.appStore.modules[moduleId].app.homePageId);
|
||||
const isHomePage = page.id === homePageId;
|
||||
const currentPageId = useStore((state) => state.modules[moduleId].currentPageId);
|
||||
const isSelected = page.id === currentPageId;
|
||||
const isHidden = page?.hidden ?? false;
|
||||
const isDisabled = page?.disabled ?? false;
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
const shouldFreeze = useStore((state) => state.getShouldFreeze());
|
||||
const featureAccess = useStore((state) => state?.license?.featureAccess, shallow);
|
||||
const licenseValid = !featureAccess?.licenseStatus?.isExpired && featureAccess?.licenseStatus?.isLicenseValid;
|
||||
const showEditingPopover = useStore((state) => state.showEditingPopover);
|
||||
const restricted = page?.permissions && page?.permissions?.length > 0;
|
||||
const {
|
||||
definition: { styles, properties },
|
||||
} = useStore((state) => state.pageSettings);
|
||||
const setCurrentPageHandle = useStore((state) => state.setCurrentPageHandle);
|
||||
// only update when the page is being edited
|
||||
const editingPage = useStore(
|
||||
(state) => state.editingPage,
|
||||
(prev, next) => {
|
||||
if (next?.id === page?.id) return false;
|
||||
if (prev?.id === page?.id) return false;
|
||||
return true;
|
||||
}
|
||||
);
|
||||
const editingPageName = useStore((state) => state.showEditPageNameInput);
|
||||
const popoverRef = useRef(null);
|
||||
|
||||
const openPageEditPopover = useStore((state) => state.openPageEditPopover);
|
||||
const toggleEditPageNameInput = useStore((state) => state.toggleEditPageNameInput);
|
||||
|
||||
const isEditingPage = editingPage?.id === page?.id;
|
||||
const icon = () => {
|
||||
const iconName = isHomePage && !page.icon ? 'IconHome2' : page.icon;
|
||||
if (!isDisabled && !isHidden) {
|
||||
return <IconSelector iconColor={computedStyles?.icon?.color} iconName={iconName} pageId={page.id} />;
|
||||
}
|
||||
if (isDisabled || (isDisabled && isHidden)) {
|
||||
return (
|
||||
<FileRemove fill={computedStyles?.icon?.fill} className=" " width={16} height={16} viewBox={'0 0 16 16'} />
|
||||
);
|
||||
}
|
||||
if (isHidden && !isDisabled) {
|
||||
return <EyeDisable className="" width={16} height={16} />;
|
||||
}
|
||||
};
|
||||
|
||||
const computeStyles = useCallback(() => {
|
||||
const baseStyles = {
|
||||
pill: {
|
||||
borderRadius: `${styles.pillRadius.value}px`,
|
||||
},
|
||||
icon: {
|
||||
color: !styles.iconColor.isDefault && styles.iconColor.value,
|
||||
fill: !styles.iconColor.isDefault && styles.iconColor.value,
|
||||
},
|
||||
};
|
||||
|
||||
switch (true) {
|
||||
case isSelected: {
|
||||
return {
|
||||
...baseStyles,
|
||||
text: {
|
||||
color: !styles.selectedTextColor.isDefault && styles.selectedTextColor.value,
|
||||
},
|
||||
icon: {
|
||||
stroke: !styles.selectedIconColor.isDefault && styles.selectedIconColor.value,
|
||||
color: !styles.selectedIconColor.isDefault && styles.selectedIconColor.value,
|
||||
fill: !styles.selectedIconColor.isDefault && styles.selectedIconColor.value,
|
||||
},
|
||||
pill: {
|
||||
background: !styles.pillSelectedBackgroundColor.isDefault && styles.pillSelectedBackgroundColor.value,
|
||||
...(page.id === editingPage?.id && {
|
||||
backgroundColor: 'var(--slate1)',
|
||||
}),
|
||||
...baseStyles.pill,
|
||||
},
|
||||
};
|
||||
}
|
||||
case isHovered: {
|
||||
return {
|
||||
...baseStyles,
|
||||
pill: {
|
||||
background: !styles.pillHoverBackgroundColor.isDefault && styles.pillHoverBackgroundColor.value,
|
||||
...baseStyles.pill,
|
||||
},
|
||||
};
|
||||
}
|
||||
default: {
|
||||
return {
|
||||
text: {
|
||||
color: !styles.textColor.isDefault && styles.textColor.value,
|
||||
},
|
||||
icon: {
|
||||
color: !styles.iconColor.isDefault && styles.iconColor.value,
|
||||
fill: !styles.iconColor.isDefault && styles.iconColor.value,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
}, [styles, isSelected, isHovered, page.id, editingPage?.id]);
|
||||
|
||||
const computedStyles = computeStyles();
|
||||
|
||||
const labelStyle = {
|
||||
icon: {
|
||||
hidden: properties.style === 'text',
|
||||
},
|
||||
label: {
|
||||
hidden: properties.style === 'icon',
|
||||
},
|
||||
};
|
||||
|
||||
const switchPage = useStore((state) => state.switchPage);
|
||||
|
||||
const handlePageSwitch = useCallback(() => {
|
||||
if (currentPageId === page.id) {
|
||||
return;
|
||||
}
|
||||
switchPage(page.id, page.handle, [], moduleId);
|
||||
setCurrentPageHandle(page.handle, moduleId);
|
||||
}, [currentPageId, page.id, page.handle, switchPage, setCurrentPageHandle, moduleId]);
|
||||
|
||||
const handlePageMenuSettings = useCallback(
|
||||
(event) => {
|
||||
event.stopPropagation();
|
||||
openPageEditPopover(page, popoverRef);
|
||||
},
|
||||
[popoverRef.current, page]
|
||||
);
|
||||
|
||||
function getTooltip() {
|
||||
const permission = page?.permissions?.length ? page?.permissions[0] : null;
|
||||
if (!permission) return '';
|
||||
const users = permission.users || [];
|
||||
const isSingle = permission.type === 'SINGLE';
|
||||
const isGroup = permission.type === 'GROUP';
|
||||
|
||||
if (users.length === 0) return null;
|
||||
|
||||
if (isSingle) {
|
||||
if (users.length === 1) {
|
||||
const email = users[0].user.email;
|
||||
return `Access restricted to ${email}`;
|
||||
} else {
|
||||
return `Access restricted to ${users.length} users`;
|
||||
}
|
||||
}
|
||||
|
||||
if (isGroup) {
|
||||
if (users.length === 1) {
|
||||
const groupName = users[0].permissionGroup?.name ?? 'Group';
|
||||
return `Access restricted to ${groupName} group`;
|
||||
} else {
|
||||
return `Access restricted to ${users.length} groups`;
|
||||
}
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
onMouseEnter={() => setIsHovered(true)}
|
||||
onMouseLeave={() => setIsHovered(false)}
|
||||
style={{
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
<>
|
||||
<div
|
||||
onClick={handlePageSwitch}
|
||||
className={`page-menu-item ${isSelected && 'is-selected'} ${darkMode && 'dark-theme'}`}
|
||||
style={{
|
||||
position: 'relative',
|
||||
width: '100%',
|
||||
...computedStyles?.pill,
|
||||
}}
|
||||
>
|
||||
{editingPageName && editingPage?.id === page?.id ? (
|
||||
<>
|
||||
{' '}
|
||||
<div className="left">{icon()}</div>
|
||||
<RenameInput
|
||||
page={page}
|
||||
updaterCallback={() => {
|
||||
toggleEditPageNameInput(false);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{' '}
|
||||
<div className="left" data-cy={`pages-name-${page.name.toLowerCase()}`}>
|
||||
{icon()}
|
||||
<OverflowTooltip childrenClassName="page-name" style={{ ...computedStyles?.text }}>
|
||||
{page.name}
|
||||
</OverflowTooltip>
|
||||
<span
|
||||
style={{
|
||||
marginLeft: '8px',
|
||||
}}
|
||||
className="color-slate09 meta-text"
|
||||
>
|
||||
{isHomePage && 'Home'}
|
||||
{isDisabled && 'Disabled'}
|
||||
{isHidden && !isDisabled && 'Hidden'}
|
||||
</span>
|
||||
</div>
|
||||
<div style={{ marginLeft: '8px', marginRight: 'auto' }}>
|
||||
{licenseValid && restricted && (
|
||||
<ToolTip message={getTooltip()}>
|
||||
<div>
|
||||
<SolidIcon width="16" name="lock" fill="var(--icon-strong)" />
|
||||
</div>
|
||||
</ToolTip>
|
||||
)}
|
||||
</div>
|
||||
<div className={cx('right', { 'handler-menu-open': showEditingPopover })}>
|
||||
{!shouldFreeze && (
|
||||
<button
|
||||
style={{
|
||||
backgroundColor: 'transparent',
|
||||
border: 'none',
|
||||
color: 'var(--color-slate12)',
|
||||
cursor: 'pointer',
|
||||
padding: '0',
|
||||
...((isEditingPage || currentPageId === page?.id) && {
|
||||
opacity: 1,
|
||||
}),
|
||||
}}
|
||||
className="edit-page-overlay-toggle"
|
||||
onClick={handlePageMenuSettings}
|
||||
ref={popoverRef}
|
||||
id={`edit-popover-${page.id}`}
|
||||
>
|
||||
<SolidIcon width="20" dataCy={`page-menu`} name="morevertical" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
);
|
||||
|
||||
export const AddingPageHandler = ({ darkMode }) => {
|
||||
const toggleShowAddNewPageInput = useStore((state) => state.toggleShowAddNewPageInput);
|
||||
const addNewPage = useStore((state) => state.addNewPage);
|
||||
const isPageGroup = useStore((state) => state.isPageGroup);
|
||||
const handleAddingNewPage = (pageName) => {
|
||||
if (pageName.trim().length === 0) {
|
||||
toast(`${isPageGroup ? 'Page group' : 'Page'} name should have at least 1 character`, {
|
||||
icon: '⚠️',
|
||||
});
|
||||
} else if (pageName.trim().length > 32) {
|
||||
toast(`${isPageGroup ? 'Page group' : 'Page'} name cannot exceed 32 characters`, {
|
||||
icon: '⚠️',
|
||||
});
|
||||
} else {
|
||||
addNewPage(pageName, _.kebabCase(pageName.toLowerCase()), isPageGroup);
|
||||
}
|
||||
toggleShowAddNewPageInput(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div role="button" style={{ marginTop: '2px' }}>
|
||||
<div>
|
||||
<input
|
||||
type="text"
|
||||
className={`form-control page-name-input color-slate12 ${darkMode && 'bg-transparent'}`}
|
||||
autoFocus
|
||||
onBlur={(event) => {
|
||||
const name = event.target.value;
|
||||
handleAddingNewPage(name);
|
||||
event.stopPropagation();
|
||||
}}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === 'Enter') {
|
||||
const name = event.target.value;
|
||||
handleAddingNewPage(name);
|
||||
event.stopPropagation();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,291 +0,0 @@
|
|||
.PopoverContent {
|
||||
&.page {
|
||||
min-height: unset !important;
|
||||
}
|
||||
}
|
||||
|
||||
.page-handler.ghost {
|
||||
background-color: #ECEEF0;
|
||||
border-radius: 4px;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
opacity: 0.8;
|
||||
|
||||
&.dark-theme {
|
||||
background-color: var(--slate4);
|
||||
}
|
||||
}
|
||||
|
||||
.edit-page-overlay-toggle {
|
||||
opacity: 0;
|
||||
transition: opacity 0.1s;
|
||||
}
|
||||
|
||||
.menu-item-drag-handle {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.page-menu-icon {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.page-menu-icon.rev {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.page-menu-icon-padding {
|
||||
padding-left: 16px;
|
||||
}
|
||||
|
||||
.menu-item-drag-handle {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.page-group-icon-text {
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
line-height: 18px;
|
||||
text-align: left;
|
||||
color: var(--slate11);
|
||||
position: relative;
|
||||
top: -10px;
|
||||
width: 194px;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.page-handler {
|
||||
.page-menu-item {
|
||||
height: 32px;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0px 8px;
|
||||
margin-top: 2px;
|
||||
justify-content: space-between;
|
||||
|
||||
&.highlight {
|
||||
border: 2px solid #3D63DC;
|
||||
}
|
||||
|
||||
&.is-selected {
|
||||
background-color: #f0f4ff;
|
||||
|
||||
&.dark-theme {
|
||||
background-color: var(--slate5);
|
||||
}
|
||||
}
|
||||
|
||||
.page-group-actions {
|
||||
transition: 0.2s ease;
|
||||
opacity: 0;
|
||||
gap: 2px;
|
||||
display: flex;
|
||||
width: unset !important;
|
||||
height: unset !important;
|
||||
|
||||
button {
|
||||
border-radius: var(--2, 4px);
|
||||
border: 1px solid var(--border-default, #CCD1D5);
|
||||
background: var(--button-secondary, #FFF);
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
padding: 0;
|
||||
/* Elevations/000 */
|
||||
box-shadow: 0px 1px 0px 0px var(--_-Dropshadow-000, rgba(0, 0, 0, 0.10));
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
.meta-text {
|
||||
color: var(--slate8);
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 18px;
|
||||
margin-top: 3px;
|
||||
}
|
||||
|
||||
button.edit-page-overlay-toggle {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: #ECEEF0;
|
||||
|
||||
button.edit-page-overlay-toggle {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.page-group-actions {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&.dark-theme {
|
||||
&:hover {
|
||||
background-color: var(--slate4);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
// margin-left: 15px;
|
||||
.page-name {
|
||||
overflow: hidden;
|
||||
color: var(--slate12);
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
max-width: 246px;
|
||||
margin-top: 3px;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.right {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
justify-content: center;
|
||||
display: flex;
|
||||
|
||||
svg {
|
||||
path {
|
||||
fill: var(--slate12);
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: var(--slate1);
|
||||
box-shadow: 0px 1px 1px 1px var(--slate6);
|
||||
border-radius: 3px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.edit-page-overlay-toggle {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.menu-item-drag-handle {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.page-menu-icon {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.page-menu-icon.no-hover {
|
||||
display: block !important;
|
||||
}
|
||||
|
||||
.page-menu-icon.rev {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.page-menu-icon-padding {
|
||||
padding-left: 0px !important;
|
||||
}
|
||||
|
||||
.menu-item-drag-handle {
|
||||
display: flex !important;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
.page-group-trigger-button {
|
||||
display: flex;
|
||||
padding: 5px 10px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
border-radius: 6px;
|
||||
width: 63px;
|
||||
}
|
||||
|
||||
.text {
|
||||
color: var(--text-default, #1B1F24);
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
.page-menu-action-buttons {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
height: 28px;
|
||||
width: 28px;
|
||||
padding: 7px;
|
||||
gap: 6px;
|
||||
border: none;
|
||||
outline: none;
|
||||
border-radius: 6px;
|
||||
background: transparent;
|
||||
|
||||
svg path {
|
||||
fill: #6A727C;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: var(--button-outline-hover, rgba(136, 144, 153, 0.12));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
.left-sidebar-header-btn.trigger {
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--border-weak, #E4E7EB);
|
||||
background: var(--button-secondary, #FFF);
|
||||
box-shadow: 0px 0px 1px 0px var(--dropshadow-100700-layer-1, rgba(48, 50, 51, 0.05)), 0px 1px 1px 0px var(--dropshadow-100400-layer-2, rgba(48, 50, 51, 0.10));
|
||||
|
||||
&:hover {
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--border-default, #CCD1D5);
|
||||
background: linear-gradient(0deg, var(--button-outline-hover, rgba(136, 144, 153, 0.12)) 0%, var(--button-outline-hover, rgba(136, 144, 153, 0.12)) 100%), var(--button-outline, #FFF);
|
||||
}
|
||||
}
|
||||
|
||||
#page-handler-menu-group {
|
||||
border: none;
|
||||
background: transparent;
|
||||
border-radius: 10px;
|
||||
|
||||
&.dark-theme {
|
||||
.popover-body {
|
||||
box-shadow: 0px 0px 1px 0px rgba(0, 0, 0, 0.9), 0px 8px 16px 0px #000000;
|
||||
}
|
||||
}
|
||||
|
||||
.popover-body {
|
||||
width: 160px;
|
||||
padding: 8px;
|
||||
border-radius: 10px;
|
||||
background: var(--background-surface-layer-01);
|
||||
box-shadow: 0px 0px 1px 0px rgba(48, 50, 51, 0.05), 0px 8px 16px 0px rgba(48, 50, 51, 0.1);
|
||||
|
||||
.menu-options {
|
||||
.option {
|
||||
display: flex;
|
||||
padding: 6px 8px;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
height: 30px;
|
||||
justify-content: center;
|
||||
align-self: stretch;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background: rgba(136, 144, 153, 0.08);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -93,7 +93,7 @@ function DataSourcePicker({ darkMode }) {
|
|||
<a
|
||||
data-cy="querymanager-doc-link"
|
||||
target="_blank"
|
||||
href="https://docs.tooljet.com/docs/app-builder/query-panel"
|
||||
href="https://docs.tooljet.ai/docs/app-builder/query-panel"
|
||||
rel="noreferrer"
|
||||
>
|
||||
documentation
|
||||
|
|
|
|||
|
|
@ -280,10 +280,10 @@ export const BaseQueryManagerBody = ({ darkMode, activeTab, renderCopilot = () =
|
|||
}
|
||||
const isSampleDb = selectedDataSource?.type === DATA_SOURCE_TYPE.SAMPLE;
|
||||
const docLink = isSampleDb
|
||||
? 'https://docs.tooljet.com/docs/data-sources/sample-data-sources'
|
||||
? 'https://docs.tooljet.ai/docs/data-sources/sample-data-sources'
|
||||
: selectedDataSource?.plugin_id && selectedDataSource.plugin_id.trim() !== ''
|
||||
? `https://docs.tooljet.com/docs/marketplace/plugins/marketplace-plugin-${selectedDataSource?.kind}/`
|
||||
: `https://docs.tooljet.com/docs/data-sources/${selectedDataSource?.kind}`;
|
||||
? `https://docs.tooljet.ai/docs/marketplace/plugins/marketplace-plugin-${selectedDataSource?.kind}/`
|
||||
: `https://docs.tooljet.ai/docs/data-sources/${selectedDataSource?.kind}`;
|
||||
return (
|
||||
<>
|
||||
<div className="" ref={paramListContainerRef}>
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@ const EducativeLabel = ({ darkMode }) => {
|
|||
faster. It uses OpenAI's GPT-3.5 to suggest queries based on your data.
|
||||
</p>
|
||||
<Button
|
||||
onClick={() => window.open('https://docs.tooljet.com/docs/tooljet-copilot', '_blank')}
|
||||
onClick={() => window.open('https://docs.tooljet.ai/docs/tooljet-copilot', '_blank')}
|
||||
darkMode={darkMode}
|
||||
size="sm"
|
||||
classNames="default-secondary-button"
|
||||
|
|
|
|||
|
|
@ -277,7 +277,11 @@ export const AggregateFilter = ({ darkMode, operation = '' }) => {
|
|||
};
|
||||
|
||||
const aggFxOptions = [
|
||||
{ label: 'Sum', value: 'sum', description: 'Sum of all values in this column' },
|
||||
{
|
||||
label: 'Sum',
|
||||
value: 'sum',
|
||||
description: 'Sum of all values in this column',
|
||||
},
|
||||
{
|
||||
label: 'Count',
|
||||
value: 'count',
|
||||
|
|
@ -402,7 +406,11 @@ export const AggregateFilter = ({ darkMode, operation = '' }) => {
|
|||
/>
|
||||
</div>
|
||||
<div
|
||||
style={{ width: '32px', minWidth: '32px', borderRadius: '0 4px 4px 0' }}
|
||||
style={{
|
||||
width: '32px',
|
||||
minWidth: '32px',
|
||||
borderRadius: '0 4px 4px 0',
|
||||
}}
|
||||
className="d-flex justify-content-center align-items-center border"
|
||||
onClick={() => handleDeleteAggregate(aggregateKey)}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -157,7 +157,7 @@ export const QueryCard = ({ dataQuery, darkMode = false, localDs }) => {
|
|||
<a
|
||||
className="text-truncate"
|
||||
data-tooltip-id="query-card-local-ds-info"
|
||||
href="https://docs.tooljet.com/docs/data-sources/overview/#changing-scope-of-data-sources-on-an-app-created-on-older-versions-of-tooljet"
|
||||
href="https://docs.tooljet.ai/docs/data-sources/overview/#changing-scope-of-data-sources-on-an-app-created-on-older-versions-of-tooljet"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -4,12 +4,33 @@ import { Inspector } from '@/AppBuilder/RightSideBar/Inspector/Inspector';
|
|||
import useStore from '@/AppBuilder/_stores/store';
|
||||
import { RIGHT_SIDE_BAR_TAB } from '@/AppBuilder/RightSideBar/rightSidebarConstants';
|
||||
import { shallow } from 'zustand/shallow';
|
||||
import SolidIcon from '@/_ui/Icon/SolidIcons';
|
||||
|
||||
export const ComponentConfigurationTab = ({ darkMode, isModuleEditor }) => {
|
||||
const selectedComponentId = useStore((state) => state.selectedComponents?.[0], shallow);
|
||||
const activeTab = useStore((state) => state.activeRightSideBarTab, shallow);
|
||||
const toggleRightSidebarPin = useStore((state) => state.toggleRightSidebarPin);
|
||||
const isRightSidebarPinned = useStore((state) => state.isRightSidebarPinned);
|
||||
const setActiveRightSideBarTab = useStore((state) => state.setActiveRightSideBarTab);
|
||||
if (!selectedComponentId) {
|
||||
return setActiveRightSideBarTab(RIGHT_SIDE_BAR_TAB.COMPONENTS);
|
||||
if (!selectedComponentId && activeTab !== RIGHT_SIDE_BAR_TAB.PAGES) {
|
||||
// return setActiveRightSideBarTab(RIGHT_SIDE_BAR_TAB.COMPONENTS);
|
||||
return (
|
||||
<>
|
||||
<div className="empty-configuration-header">
|
||||
<div className="header">Component properties</div>
|
||||
<div className="icon-btn cursor-pointer" onClick={() => toggleRightSidebarPin()}>
|
||||
<SolidIcon fill="var(--icon-strong)" name={isRightSidebarPinned ? 'unpin' : 'pin'} width="16" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="d-flex align-items-center justify-content-center no-component-selected">
|
||||
<SolidIcon name="cursorclick" width="28" />
|
||||
<div className="tj-text-sm font-weight-500 heading">No component selected</div>
|
||||
<div className="tj-text-xsm sub-heading">
|
||||
Click a component on the canvas to view and edit its properties.
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Inspector
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@
|
|||
height: 36px;
|
||||
margin-bottom: 8px;
|
||||
margin-top: 16px;
|
||||
margin-left: 16px;
|
||||
margin-right: 16px;
|
||||
}
|
||||
|
||||
.tj-tabs-container {
|
||||
|
|
|
|||
|
|
@ -7,6 +7,9 @@ import Fuse from 'fuse.js';
|
|||
import { SearchBox } from '@/_components';
|
||||
import { DragLayer } from './DragLayer';
|
||||
import useStore from '@/AppBuilder/_stores/store';
|
||||
import Accordion from '@/_ui/Accordion';
|
||||
import sectionConfig from './sectionConfig';
|
||||
import SolidIcon from '@/_ui/Icon/SolidIcons';
|
||||
import { ModuleManager } from '@/modules/Modules/components';
|
||||
import { ComponentModuleTab } from '@/modules/Appbuilder/components';
|
||||
|
||||
|
|
@ -28,12 +31,11 @@ export const ComponentsManagerTab = ({ darkMode, isModuleEditor }) => {
|
|||
const _shouldFreeze = useStore((state) => state.getShouldFreeze());
|
||||
const isAutoMobileLayout = useStore((state) => state.currentLayout === 'mobile' && state.getIsAutoMobileLayout());
|
||||
const shouldFreeze = _shouldFreeze || isAutoMobileLayout;
|
||||
|
||||
const toggleRightSidebarPin = useStore((state) => state.toggleRightSidebarPin);
|
||||
const isRightSidebarPinned = useStore((state) => state.isRightSidebarPinned);
|
||||
const handleSearchQueryChange = useCallback(
|
||||
debounce((e) => {
|
||||
const { value } = e.target;
|
||||
debounce((value) => {
|
||||
setSearchQuery(value);
|
||||
|
||||
if (activeTab === 1) {
|
||||
filterComponents(value);
|
||||
}
|
||||
|
|
@ -78,11 +80,10 @@ export const ComponentsManagerTab = ({ darkMode, isModuleEditor }) => {
|
|||
);
|
||||
}
|
||||
|
||||
function renderList(header, items) {
|
||||
function renderList(items) {
|
||||
if (isEmpty(items)) return null;
|
||||
return (
|
||||
<div className="component-card-group-container">
|
||||
<span className="widget-header">{header}</span>
|
||||
<div className="component-card-group-wrapper">
|
||||
{items.map((component, i) => renderComponentCard(component, i))}
|
||||
</div>
|
||||
|
|
@ -105,6 +106,7 @@ export const ComponentsManagerTab = ({ darkMode, isModuleEditor }) => {
|
|||
className=" btn-sm tj-tertiary-btn mt-3"
|
||||
onClick={() => {
|
||||
setFilteredComponents([]);
|
||||
handleSearchQueryChange('');
|
||||
}}
|
||||
>
|
||||
{t('widgetManager.clearQuery', 'clear query')}
|
||||
|
|
@ -113,62 +115,31 @@ export const ComponentsManagerTab = ({ darkMode, isModuleEditor }) => {
|
|||
);
|
||||
}
|
||||
|
||||
if (filteredComponents.length != componentList.length) {
|
||||
return <>{renderList(undefined, filteredComponents)}</>;
|
||||
} else {
|
||||
const commonSection = { title: t('widgetManager.commonlyUsed', 'commonly used'), items: [] };
|
||||
const layoutsSection = { title: t('widgetManager.layouts', 'layouts'), items: [] };
|
||||
const formSection = { title: t('widgetManager.forms', 'forms'), items: [] };
|
||||
const integrationSection = { title: t('widgetManager.integrations', 'integrations'), items: [] };
|
||||
const otherSection = { title: t('widgetManager.others', 'others'), items: [] };
|
||||
const legacySection = { title: 'Legacy', items: [] };
|
||||
|
||||
const commonItems = ['Table', 'Button', 'Text', 'TextInput', 'DatetimePickerV2', 'Form'];
|
||||
const formItems = [
|
||||
'Form',
|
||||
'TextInput',
|
||||
'NumberInput',
|
||||
'PasswordInput',
|
||||
'TextArea',
|
||||
'EmailInput',
|
||||
'PhoneInput',
|
||||
'CurrencyInput',
|
||||
'ToggleSwitchV2',
|
||||
'DropdownV2',
|
||||
'MultiselectV2',
|
||||
'RichTextEditor',
|
||||
'Checkbox',
|
||||
'RadioButtonV2',
|
||||
'DatetimePickerV2',
|
||||
'DatePickerV2',
|
||||
'TimePicker',
|
||||
'DaterangePicker',
|
||||
'FilePicker',
|
||||
'StarRating',
|
||||
];
|
||||
const integrationItems = ['Map'];
|
||||
const layoutItems = ['Container', 'Listview', 'Tabs', 'ModalV2'];
|
||||
|
||||
filteredComponents.forEach((f) => {
|
||||
if (commonItems.includes(f)) commonSection.items.push(f);
|
||||
if (formItems.includes(f)) formSection.items.push(f);
|
||||
else if (integrationItems.includes(f)) integrationSection.items.push(f);
|
||||
else if (LEGACY_ITEMS.includes(f)) legacySection.items.push(f);
|
||||
else if (layoutItems.includes(f)) layoutsSection.items.push(f);
|
||||
else otherSection.items.push(f);
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
{renderList(commonSection.title, commonSection.items)}
|
||||
{renderList(layoutsSection.title, layoutsSection.items)}
|
||||
{renderList(formSection.title, formSection.items)}
|
||||
{renderList(otherSection.title, otherSection.items)}
|
||||
{renderList(integrationSection.title, integrationSection.items)}
|
||||
{renderList(legacySection.title, legacySection.items)}
|
||||
</>
|
||||
);
|
||||
if (filteredComponents.length !== componentList.length) {
|
||||
return <>{renderList(filteredComponents)}</>;
|
||||
}
|
||||
|
||||
const sections = Object.entries(sectionConfig).map(([key, config]) => ({
|
||||
title: config.title,
|
||||
items: filteredComponents.filter((component) => config.valueSet.has(component)),
|
||||
}));
|
||||
|
||||
const items = [];
|
||||
sections.forEach((section) => {
|
||||
if (section.items.length > 0) {
|
||||
items.push({
|
||||
title: section.title,
|
||||
isOpen: true,
|
||||
children: renderList(section.items),
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="mt-3">
|
||||
<Accordion items={items} isTitleCase={false} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const handleChangeTab = (tab) => {
|
||||
|
|
@ -195,7 +166,7 @@ export const ComponentsManagerTab = ({ darkMode, isModuleEditor }) => {
|
|||
<SearchBox
|
||||
dataCy={`widget-search-box`}
|
||||
initialValue={''}
|
||||
callBack={(e) => handleSearchQueryChange(e)}
|
||||
callBack={(e) => handleSearchQueryChange(e.target.value)}
|
||||
onClearCallback={() => {
|
||||
setSearchQuery('');
|
||||
if (activeTab === 1) {
|
||||
|
|
|
|||
|
|
@ -11,6 +11,11 @@ import { useModuleContext } from '@/AppBuilder/_contexts/ModuleContext';
|
|||
import { noop } from 'lodash';
|
||||
|
||||
export const DragLayer = ({ index, component, isModuleTab = false }) => {
|
||||
const [isRightSidebarOpen, toggleRightSidebar] = useStore(
|
||||
(state) => [state.isRightSidebarOpen, state.toggleRightSidebar],
|
||||
shallow
|
||||
);
|
||||
const isRightSidebarPinned = useStore((state) => state.isRightSidebarPinned);
|
||||
const { isModuleEditor } = useModuleContext();
|
||||
const setShowModuleBorder = useStore((state) => state.setShowModuleBorder, shallow) || noop;
|
||||
const [{ isDragging }, drag, preview] = useDrag(
|
||||
|
|
@ -28,11 +33,14 @@ export const DragLayer = ({ index, component, isModuleTab = false }) => {
|
|||
|
||||
useEffect(() => {
|
||||
if (isDragging && !isModuleEditor) {
|
||||
if (!isRightSidebarPinned) {
|
||||
toggleRightSidebar(!isRightSidebarOpen);
|
||||
}
|
||||
setShowModuleBorder(true);
|
||||
} else {
|
||||
setShowModuleBorder(false);
|
||||
}
|
||||
}, [isDragging, setShowModuleBorder, isModuleEditor]);
|
||||
}, [isDragging, setShowModuleBorder, isModuleEditor, toggleRightSidebar]);
|
||||
|
||||
// const size = isModuleTab
|
||||
// ? component.module_container.layouts[currentLayout]
|
||||
|
|
@ -55,36 +63,43 @@ const CustomDragLayer = ({ size }) => {
|
|||
currentOffset: monitor.getSourceClientOffset(),
|
||||
item: monitor.getItem(),
|
||||
}));
|
||||
|
||||
console.log(currentOffset, 'currentOffset');
|
||||
if (!currentOffset) return null;
|
||||
|
||||
const canvasWidth = item?.canvasWidth;
|
||||
const canvasBounds = item?.canvasRef?.getBoundingClientRect();
|
||||
const height = size.height;
|
||||
|
||||
const mainCanvasWidth = document.getElementById('real-canvas')?.offsetWidth || 0;
|
||||
const appCanvasWidth = document.getElementById('real-canvas')?.offsetWidth || 0;
|
||||
|
||||
// Calculate width based on the app canvas's grid
|
||||
let width = (appCanvasWidth * size.width) / NO_OF_GRIDS;
|
||||
|
||||
let width = (mainCanvasWidth * size.width) / NO_OF_GRIDS;
|
||||
// Calculate position relative to the current canvas (parent or child)
|
||||
const left = currentOffset.x - (canvasBounds?.left || 0);
|
||||
const top = currentOffset.y - (canvasBounds?.top || 0);
|
||||
|
||||
// Adjust position and width if exceeding grid bounds
|
||||
if (width >= canvasWidth) {
|
||||
// Ensure width doesn't exceed the current container's width
|
||||
if (width > canvasWidth) {
|
||||
width = canvasWidth;
|
||||
}
|
||||
|
||||
// Snap width to grid (round to nearest grid unit)
|
||||
const gridUnitWidth = canvasWidth / NO_OF_GRIDS;
|
||||
const gridUnits = Math.round(width / gridUnitWidth);
|
||||
width = gridUnits * gridUnitWidth;
|
||||
|
||||
const [x, y] = snapToGrid(canvasWidth, left, top);
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
position: 'fixed',
|
||||
pointerEvents: 'none',
|
||||
zIndex: 1000,
|
||||
left: canvasBounds?.left || 0,
|
||||
top: canvasBounds?.top || 0,
|
||||
height: `${height}px`,
|
||||
width: `${width}px`,
|
||||
zIndex: -1,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
|
|
|
|||
|
|
@ -0,0 +1,70 @@
|
|||
const sectionConfig = {
|
||||
commonlyUsed: {
|
||||
title: 'Commonly used',
|
||||
valueSet: new Set(['Table', 'Button', 'Text', 'TextInput', 'DatetimePickerV2', 'Form']),
|
||||
},
|
||||
buttons: {
|
||||
title: 'Buttons',
|
||||
valueSet: new Set(['Button', 'ButtonGroup']),
|
||||
},
|
||||
data: {
|
||||
title: 'Data',
|
||||
valueSet: new Set(['Table', 'Chart']),
|
||||
},
|
||||
layouts: {
|
||||
title: 'Layouts',
|
||||
valueSet: new Set(['Form', 'ModalV2', 'Container', 'Tabs', 'Listview', 'Kanban', 'Calendar']),
|
||||
},
|
||||
textInputs: {
|
||||
title: 'Text inputs',
|
||||
valueSet: new Set(['TextInput', 'TextArea', 'EmailInput', 'PasswordInput', 'RichTextEditor']),
|
||||
},
|
||||
numberInputs: {
|
||||
title: 'Number inputs',
|
||||
valueSet: new Set(['NumberInput', 'PhoneInput', 'CurrencyInput', 'RangeSlider', 'StarRating']),
|
||||
},
|
||||
selectInputs: {
|
||||
title: 'Select inputs',
|
||||
valueSet: new Set(['DropdownV2', 'MultiselectV2', 'ToggleSwitchV2', 'RadioButtonV2', 'Checkbox', 'TreeSelect']),
|
||||
},
|
||||
dateTimeInputs: {
|
||||
title: 'Date and time inputs',
|
||||
valueSet: new Set(['DaterangePicker', 'DatePickerV2', 'TimePicker', 'DatetimePickerV2']),
|
||||
},
|
||||
navigation: {
|
||||
title: 'Navigation',
|
||||
valueSet: new Set(['Link', 'Pagination', 'Steps']),
|
||||
},
|
||||
media: {
|
||||
title: 'Media',
|
||||
valueSet: new Set(['Icon', 'Image', 'SvgImage', 'PDF', 'Map']),
|
||||
},
|
||||
presentation: {
|
||||
title: 'Presentation',
|
||||
valueSet: new Set([
|
||||
'Text',
|
||||
'Tags',
|
||||
'CircularProgressBar',
|
||||
'Timeline',
|
||||
'Divider',
|
||||
'VerticalDivider',
|
||||
'Spinner',
|
||||
'Statistics',
|
||||
'Timer',
|
||||
]),
|
||||
},
|
||||
custom: {
|
||||
title: 'Custom',
|
||||
valueSet: new Set(['CustomComponent', 'Html', 'IFrame']),
|
||||
},
|
||||
miscellaneous: {
|
||||
title: 'Miscellaneous',
|
||||
valueSet: new Set(['FilePicker', 'CodeEditor', 'ColorPicker', 'BoundedBox', 'QrScanner']),
|
||||
},
|
||||
legacy: {
|
||||
title: 'Legacy',
|
||||
valueSet: new Set(['Modal', 'Datepicker', 'RadioButton', 'ToggleSwitch', 'DropDown', 'Multiselect']),
|
||||
},
|
||||
};
|
||||
|
||||
export default sectionConfig;
|
||||
|
|
@ -27,10 +27,18 @@ const SHOW_ADDITIONAL_ACTIONS = [
|
|||
'Button',
|
||||
'RichTextEditor',
|
||||
'Image',
|
||||
'CodeEditor',
|
||||
'TextArea',
|
||||
'Container',
|
||||
'Form',
|
||||
'Divider',
|
||||
'VerticalDivider',
|
||||
'ModalV2',
|
||||
'Tabs',
|
||||
'RangeSlider',
|
||||
'Link',
|
||||
'FilePicker',
|
||||
'Listview',
|
||||
];
|
||||
const PROPERTIES_VS_ACCORDION_TITLE = {
|
||||
Text: 'Data',
|
||||
|
|
@ -46,6 +54,8 @@ const PROPERTIES_VS_ACCORDION_TITLE = {
|
|||
Divider: 'Data',
|
||||
VerticalDivider: 'Data',
|
||||
ModalV2: 'Data',
|
||||
Tabs: 'Data',
|
||||
RangeSlider: 'Data',
|
||||
Link: 'Data',
|
||||
};
|
||||
|
||||
|
|
@ -144,9 +154,12 @@ export const baseComponentProperties = (
|
|||
'DropdownV2',
|
||||
'MultiselectV2',
|
||||
'Image',
|
||||
'RangeSlider',
|
||||
'Divider',
|
||||
'VerticalDivider',
|
||||
'Link',
|
||||
'FilePicker',
|
||||
'Tabs',
|
||||
],
|
||||
Layout: [],
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,8 +1,114 @@
|
|||
import React from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import Accordion from '@/_ui/Accordion';
|
||||
import { renderElement } from '../Utils';
|
||||
import { baseComponentProperties } from './DefaultComponent';
|
||||
import { resolveReferences } from '@/_helpers/utils';
|
||||
import cx from 'classnames';
|
||||
import styles from '@/_ui/Select/styles';
|
||||
import useStore from '@/AppBuilder/_stores/store';
|
||||
|
||||
import Select from '@/_ui/Select';
|
||||
import CodeHinter from '@/AppBuilder/CodeEditor';
|
||||
import FxButton from '@/AppBuilder/CodeBuilder/Elements/FxButton';
|
||||
|
||||
const FILE_TYPE_OPTIONS = [
|
||||
{ value: '*/*', label: 'Any Files' },
|
||||
{ value: 'image/*', label: 'Image files' },
|
||||
{ value: '.pdf,.doc,.docx,.ppt,.pptx', label: 'Document files' },
|
||||
{ value: '.xls,.xlsx,.csv,.ods', label: 'Spreadsheet files' },
|
||||
{ value: 'text/*,.md,.json,.xml,.yaml', label: 'Text files' },
|
||||
{ value: 'audio/*', label: 'Audio files' },
|
||||
{ value: 'video/*', label: 'Video files' },
|
||||
{ value: '.zip,.rar,.7z,.tar,.gz', label: 'Archive/Compressed files' },
|
||||
];
|
||||
|
||||
const FxSelect = ({ label, paramName, initialValue, darkMode, paramUpdated, options, onValueChange }) => {
|
||||
const [isFxActive, setIsFxActive] = useState(false);
|
||||
|
||||
const handleFxButtonClick = () => {
|
||||
paramUpdated({ name: paramName }, 'fxActive', !isFxActive, 'properties');
|
||||
setIsFxActive(!isFxActive);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
data-cy={`input-date-display-format`}
|
||||
className="field mb-2 w-100 input-date-display-format"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div className="field mb-2" onClick={(e) => e.stopPropagation()}>
|
||||
<div className="d-flex justify-content-between mb-1">
|
||||
<label className="form-label">{label}</label>
|
||||
<div className={cx({ 'hide-fx': !isFxActive })}>
|
||||
<FxButton active={isFxActive} onPress={handleFxButtonClick} />
|
||||
</div>
|
||||
</div>
|
||||
{isFxActive ? (
|
||||
<CodeHinter
|
||||
initialValue={initialValue}
|
||||
theme={darkMode ? 'monokai' : 'default'}
|
||||
mode="javascript"
|
||||
lineNumbers={false}
|
||||
onChange={onValueChange}
|
||||
/>
|
||||
) : (
|
||||
<Select
|
||||
options={options}
|
||||
value={initialValue ?? '*/*'}
|
||||
search={true}
|
||||
closeOnSelect={true}
|
||||
onChange={onValueChange}
|
||||
fuzzySearch
|
||||
placeholder="Select.."
|
||||
useCustomStyles={true}
|
||||
styles={styles(darkMode, '100%', 32, { fontSize: '12px' })}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
/** Remove minFileCount and maxFileCount validations if multiple file selection is disabled */
|
||||
const getValidations = (componentMeta, component) => {
|
||||
const validations = Object.keys(componentMeta.validation || {});
|
||||
const enableMultipleValue = resolveReferences(component.component.definition.properties.enableMultiple?.value ?? false);
|
||||
const enableMultipleFxActive = component.component.definition.properties.enableMultiple?.fxActive;
|
||||
|
||||
if (!enableMultipleValue && !enableMultipleFxActive) {
|
||||
return validations.filter((validation) => !['minFileCount', 'maxFileCount'].includes(validation));
|
||||
}
|
||||
return validations;
|
||||
};
|
||||
|
||||
const getPropertiesBySection = (propertiesMeta) => {
|
||||
const properties = [];
|
||||
const additionalActions = [];
|
||||
const dataProperties = [];
|
||||
|
||||
for (const [key, value] of Object.entries(propertiesMeta)) {
|
||||
if (value?.section === 'additionalActions') {
|
||||
additionalActions.push(key);
|
||||
} else if (value?.accordian === 'Data') {
|
||||
dataProperties.push(key);
|
||||
} else {
|
||||
properties.push(key);
|
||||
}
|
||||
}
|
||||
return { properties, additionalActions, dataProperties };
|
||||
};
|
||||
|
||||
const getConditionalAccordionItems = (component, renderCustomElement) => {
|
||||
const parseContent = resolveReferences(component.component.definition.properties.parseContent?.value ?? false);
|
||||
const options = ['parseContent'];
|
||||
let renderOptions = options.map((option) => renderCustomElement(option));
|
||||
|
||||
const conditionalOptions = [{ name: 'parseFileType', condition: parseContent }];
|
||||
conditionalOptions.forEach(({ name, condition }) => {
|
||||
if (condition) renderOptions.push(renderCustomElement(name));
|
||||
});
|
||||
return renderOptions;
|
||||
};
|
||||
|
||||
export const FilePicker = ({ componentMeta, darkMode, ...restProps }) => {
|
||||
const {
|
||||
|
|
@ -16,38 +122,22 @@ export const FilePicker = ({ componentMeta, darkMode, ...restProps }) => {
|
|||
allComponents,
|
||||
} = restProps;
|
||||
|
||||
const renderCustomElement = (param, paramType = 'properties') => {
|
||||
return renderElement(component, componentMeta, paramUpdated, dataQueries, param, paramType, currentState);
|
||||
};
|
||||
const conditionalAccordionItems = (component) => {
|
||||
const parseContent = resolveReferences(component.component.definition.properties.parseContent?.value ?? false);
|
||||
const accordionItems = [];
|
||||
const options = ['parseContent'];
|
||||
const resolvedValidations = useStore((state) => state.getResolvedComponent(component.id)?.validation);
|
||||
const fileTypeValue = resolvedValidations?.fileType;
|
||||
|
||||
let renderOptions = [];
|
||||
const renderCustomElement = (param, paramType = 'properties') =>
|
||||
renderElement(component, componentMeta, paramUpdated, dataQueries, param, paramType, currentState);
|
||||
|
||||
options.map((option) => renderOptions.push(renderCustomElement(option)));
|
||||
// Debug logs
|
||||
// console.log('component.component.definition', component.component.definition);
|
||||
|
||||
const conditionalOptions = [{ name: 'parseFileType', condition: parseContent }];
|
||||
|
||||
conditionalOptions.map(({ name, condition }) => {
|
||||
if (condition) renderOptions.push(renderCustomElement(name));
|
||||
});
|
||||
|
||||
accordionItems.push({
|
||||
title: 'Options',
|
||||
children: renderOptions,
|
||||
});
|
||||
return accordionItems;
|
||||
};
|
||||
|
||||
const properties = Object.keys(componentMeta.properties);
|
||||
const events = Object.keys(componentMeta.events);
|
||||
const validations = Object.keys(componentMeta.validation || {});
|
||||
const validations = getValidations(componentMeta, component);
|
||||
|
||||
const filteredProperties = properties.filter(
|
||||
(property) => property !== 'parseContent' && property !== 'parseFileType'
|
||||
);
|
||||
// console.log('validations', validations, enableMultipleValue, component.component.definition.properties.enableMultiple?.value, enableMultipleFxActive);
|
||||
|
||||
const { additionalActions, dataProperties } = getPropertiesBySection(componentMeta?.properties);
|
||||
const filteredProperties = [...dataProperties];
|
||||
|
||||
const accordionItems = baseComponentProperties(
|
||||
filteredProperties,
|
||||
|
|
@ -62,10 +152,26 @@ export const FilePicker = ({ componentMeta, darkMode, ...restProps }) => {
|
|||
apps,
|
||||
allComponents,
|
||||
validations,
|
||||
darkMode
|
||||
darkMode,
|
||||
[],
|
||||
additionalActions
|
||||
);
|
||||
|
||||
accordionItems.splice(1, 0, ...conditionalAccordionItems(component));
|
||||
// Insert conditional accordion items
|
||||
accordionItems[0].children.push(...getConditionalAccordionItems(component, renderCustomElement));
|
||||
|
||||
// Insert FxSelect for file type
|
||||
accordionItems[2].children[1] = (
|
||||
<FxSelect
|
||||
label={'File type'}
|
||||
paramName="fileType"
|
||||
initialValue={fileTypeValue}
|
||||
darkMode={darkMode}
|
||||
paramUpdated={paramUpdated}
|
||||
options={FILE_TYPE_OPTIONS}
|
||||
onValueChange={(value) => paramUpdated({ name: 'fileType' }, 'value', value, 'validation')}
|
||||
/>
|
||||
);
|
||||
|
||||
return <Accordion items={accordionItems} />;
|
||||
};
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue