Merge branch 'appbuilder/sprint-14' into gh-12680-toggle-app-mode

This commit is contained in:
Nakul Nagargade 2025-07-01 18:45:06 +05:30
commit d1292add98
663 changed files with 26362 additions and 6039 deletions

View file

@ -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=

View file

@ -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:

View file

@ -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

View file

@ -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 }}).

View file

@ -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": [

View file

@ -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,

View file

@ -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) => {

View file

@ -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();
}
});

View file

@ -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]",

View 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"]`
},
}

View file

@ -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"]',

View 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",
};

View file

@ -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`
}
};

View file

@ -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!",

View file

@ -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`);
});

View file

@ -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 });
});
});
}
);

View file

@ -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);
});

View file

@ -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]", "");

View file

@ -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", () => {

View file

@ -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");
});
});

View file

@ -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]", "");

View file

@ -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"
);
});
});

View file

@ -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"
);
});
});

View file

@ -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();

View file

@ -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,
1 First Name Last Name Email User Role Group
2 test1 user test1@gmail.com Builder
3 test2 user test3@gmail.com End User
4 Test3 Example test12@gmail.com Admin

View file

@ -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,,
1 First Name Last Name Email User Role Group Metadata
2 test1 user test1@gmail.com Builder
3 test2 user test3@gmail.com End User
4 Test3 Example test12@gmail.com Admin

View 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
1 First Name Last Name Email User Role Group
2 test12empty@gmail.com Admin Admin
3 Test Example test12empty@gmail.com Builder Builder

View file

@ -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,
1 First Name Last Name Email User Role Group Metadata
2 test12empty@gmail.com Admin Admin
3 Test Example test12empty@gmail.com Builder Builder

View 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,
1 First Name Last Name Email User Role Group
2 Vijay Yadav vjyaav1@gmail.com Admin
3 Vijay Yadav vjyaav2@gmail.com Builder
4 Vijay Yadav vjyaav3@gmail.com Builder
5 Vijay Yadav vjyaav4@gmail.com End User
6 Vijay Yadav vjyaav5@gmail.com End User
7 Vijay Yadav vjyaav6@gmail.com End User
8 Vijay Yadav vjyaav7@gmail.com End User
9 Vijay Yadav vjyaav8@gmail.com End User
10 Vijay Yadav vjyaav9@gmail.com End User
11 Vijay Yadav vjyaav10@gmail.com End User
12 Vijay Yadav vjyaav11@gmail.com End User
13 Vijay Yadav vjyaav12@gmail.com End User
14 Vijay Yadav vjyaav13@gmail.com End User
15 Vijay Yadav vjyaav14@gmail.com End User
16 Vijay Yadav vjyaav15@gmail.com End User
17 Vijay Yadav vjyaav16@gmail.com End User
18 Vijay Yadav vjyaav17@gmail.com End User
19 Vijay Yadav vjyaav18@gmail.com End User
20 Vijay Yadav vjyaav19@gmail.com End User
21 Vijay Yadav vjyaav20@gmail.com End User
22 Vijay Yadav vjyaav21@gmail.com End User
23 Vijay Yadav vjyaav22@gmail.com End User
24 Vijay Yadav vjyaav23@gmail.com End User
25 Vijay Yadav vjyaav24@gmail.com End User
26 Vijay Yadav vjyaav25@gmail.com End User
27 Vijay Yadav vjyaav26@gmail.com End User
28 Vijay Yadav vjyaav27@gmail.com End User
29 Vijay Yadav vjyaav28@gmail.com End User
30 Vijay Yadav vjyaav29@gmail.com End User
31 Vijay Yadav vjyaav30@gmail.com End User
32 Vijay Yadav vjyaav31@gmail.com End User
33 Vijay Yadav vjyaav32@gmail.com End User
34 Vijay Yadav vjyaav33@gmail.com End User
35 Vijay Yadav vjyaav34@gmail.com End User
36 Vijay Yadav vjyaav35@gmail.com End User
37 Vijay Yadav vjyaav36@gmail.com End User
38 Vijay Yadav vjyaav37@gmail.com End User
39 Vijay Yadav vjyaav38@gmail.com End User
40 Vijay Yadav vjyaav39@gmail.com End User
41 Vijay Yadav vjyaav40@gmail.com End User
42 Vijay Yadav vjyaav41@gmail.com End User
43 Vijay Yadav vjyaav42@gmail.com End User
44 Vijay Yadav vjyaav43@gmail.com End User
45 Vijay Yadav vjyaav44@gmail.com End User
46 Vijay Yadav vjyaav45@gmail.com End User
47 Vijay Yadav vjyaav46@gmail.com End User
48 Vijay Yadav vjyaav47@gmail.com End User
49 Vijay Yadav vjyaav48@gmail.com End User
50 Vijay Yadav vjyaav49@gmail.com End User
51 Vijay Yadav vjyaav50@gmail.com End User
52 Vijay Yadav vjyaav51@gmail.com End User
53 Vijay Yadav vjyaav52@gmail.com End User
54 Vijay Yadav vjyaav53@gmail.com End User
55 Vijay Yadav vjyaav54@gmail.com End User
56 Vijay Yadav vjyaav55@gmail.com End User
57 Vijay Yadav vjyaav56@gmail.com End User
58 Vijay Yadav vjyaav57@gmail.com End User
59 Vijay Yadav vjyaav58@gmail.com End User
60 Vijay Yadav vjyaav59@gmail.com End User
61 Vijay Yadav vjyaav60@gmail.com End User
62 Vijay Yadav vjyaav61@gmail.com End User
63 Vijay Yadav vjyaav62@gmail.com End User
64 Vijay Yadav vjyaav63@gmail.com End User
65 Vijay Yadav vjyaav64@gmail.com End User
66 Vijay Yadav vjyaav65@gmail.com End User
67 Vijay Yadav vjyaav66@gmail.com End User
68 Vijay Yadav vjyaav67@gmail.com End User
69 Vijay Yadav vjyaav68@gmail.com End User
70 Vijay Yadav vjyaav69@gmail.com End User
71 Vijay Yadav vjyaav70@gmail.com End User
72 Vijay Yadav vjyaav71@gmail.com End User
73 Vijay Yadav vjyaav72@gmail.com End User
74 Vijay Yadav vjyaav73@gmail.com End User
75 Vijay Yadav vjyaav74@gmail.com End User
76 Vijay Yadav vjyaav75@gmail.com End User
77 Vijay Yadav vjyaav76@gmail.com End User
78 Vijay Yadav vjyaav77@gmail.com End User
79 Vijay Yadav vjyaav78@gmail.com End User
80 Vijay Yadav vjyaav79@gmail.com End User
81 Vijay Yadav vjyaav80@gmail.com End User
82 Vijay Yadav vjyaav81@gmail.com End User
83 Vijay Yadav vjyaav82@gmail.com End User
84 Vijay Yadav vjyaav83@gmail.com End User
85 Vijay Yadav vjyaav84@gmail.com End User
86 Vijay Yadav vjyaav85@gmail.com End User
87 Vijay Yadav vjyaav86@gmail.com End User
88 Vijay Yadav vjyaav87@gmail.com End User
89 Vijay Yadav vjyaav88@gmail.com End User
90 Vijay Yadav vjyaav89@gmail.com End User
91 Vijay Yadav vjyaav90@gmail.com End User
92 Vijay Yadav vjyaav91@gmail.com End User
93 Vijay Yadav vjyaav92@gmail.com End User
94 Vijay Yadav vjyaav93@gmail.com End User
95 Vijay Yadav vjyaav94@gmail.com End User
96 Vijay Yadav vjyaav95@gmail.com End User
97 Vijay Yadav vjyaav96@gmail.com End User
98 Vijay Yadav vjyaav97@gmail.com End User
99 Vijay Yadav vjyaav98@gmail.com End User
100 Vijay Yadav vjyaav99@gmail.com End User
101 Vijay Yadav vjyaav100@gmail.com End User
102 Vijay Yadav vjyaav101@gmail.com End User
103 Vijay Yadav vjyaav102@gmail.com End User
104 Vijay Yadav vjyaav103@gmail.com End User
105 Vijay Yadav vjyaav104@gmail.com End User
106 Vijay Yadav vjyaav105@gmail.com End User
107 Vijay Yadav vjyaav106@gmail.com End User
108 Vijay Yadav vjyaav107@gmail.com End User
109 Vijay Yadav vjyaav108@gmail.com End User
110 Vijay Yadav vjyaav109@gmail.com End User
111 Vijay Yadav vjyaav110@gmail.com End User
112 Vijay Yadav vjyaav111@gmail.com End User
113 Vijay Yadav vjyaav112@gmail.com End User
114 Vijay Yadav vjyaav113@gmail.com End User
115 Vijay Yadav vjyaav114@gmail.com End User
116 Vijay Yadav vjyaav115@gmail.com End User
117 Vijay Yadav vjyaav116@gmail.com End User
118 Vijay Yadav vjyaav117@gmail.com End User
119 Vijay Yadav vjyaav118@gmail.com End User
120 Vijay Yadav vjyaav119@gmail.com End User
121 Vijay Yadav vjyaav120@gmail.com End User
122 Vijay Yadav vjyaav121@gmail.com End User
123 Vijay Yadav vjyaav122@gmail.com End User
124 Vijay Yadav vjyaav123@gmail.com End User
125 Vijay Yadav vjyaav124@gmail.com End User
126 Vijay Yadav vjyaav125@gmail.com End User
127 Vijay Yadav vjyaav126@gmail.com End User
128 Vijay Yadav vjyaav127@gmail.com End User
129 Vijay Yadav vjyaav128@gmail.com End User
130 Vijay Yadav vjyaav129@gmail.com End User
131 Vijay Yadav vjyaav130@gmail.com End User
132 Vijay Yadav vjyaav131@gmail.com End User
133 Vijay Yadav vjyaav132@gmail.com End User
134 Vijay Yadav vjyaav133@gmail.com End User
135 Vijay Yadav vjyaav134@gmail.com End User
136 Vijay Yadav vjyaav135@gmail.com End User
137 Vijay Yadav vjyaav136@gmail.com End User
138 Vijay Yadav vjyaav137@gmail.com End User
139 Vijay Yadav vjyaav138@gmail.com End User
140 Vijay Yadav vjyaav139@gmail.com End User
141 Vijay Yadav vjyaav140@gmail.com End User
142 Vijay Yadav vjyaav141@gmail.com End User
143 Vijay Yadav vjyaav142@gmail.com End User
144 Vijay Yadav vjyaav143@gmail.com End User
145 Vijay Yadav vjyaav144@gmail.com End User
146 Vijay Yadav vjyaav145@gmail.com End User
147 Vijay Yadav vjyaav146@gmail.com End User
148 Vijay Yadav vjyaav147@gmail.com End User
149 Vijay Yadav vjyaav148@gmail.com End User
150 Vijay Yadav vjyaav149@gmail.com End User
151 Vijay Yadav vjyaav150@gmail.com End User
152 Vijay Yadav vjyaav151@gmail.com End User
153 Vijay Yadav vjyaav152@gmail.com End User
154 Vijay Yadav vjyaav153@gmail.com End User
155 Vijay Yadav vjyaav154@gmail.com End User
156 Vijay Yadav vjyaav155@gmail.com End User
157 Vijay Yadav vjyaav156@gmail.com End User
158 Vijay Yadav vjyaav157@gmail.com End User
159 Vijay Yadav vjyaav158@gmail.com End User
160 Vijay Yadav vjyaav159@gmail.com End User
161 Vijay Yadav vjyaav160@gmail.com End User
162 Vijay Yadav vjyaav161@gmail.com End User
163 Vijay Yadav vjyaav162@gmail.com End User
164 Vijay Yadav vjyaav163@gmail.com End User
165 Vijay Yadav vjyaav164@gmail.com End User
166 Vijay Yadav vjyaav165@gmail.com End User
167 Vijay Yadav vjyaav166@gmail.com End User
168 Vijay Yadav vjyaav167@gmail.com End User
169 Vijay Yadav vjyaav168@gmail.com End User
170 Vijay Yadav vjyaav169@gmail.com End User
171 Vijay Yadav vjyaav170@gmail.com End User
172 Vijay Yadav vjyaav171@gmail.com End User
173 Vijay Yadav vjyaav172@gmail.com End User
174 Vijay Yadav vjyaav173@gmail.com End User
175 Vijay Yadav vjyaav174@gmail.com End User
176 Vijay Yadav vjyaav175@gmail.com End User
177 Vijay Yadav vjyaav176@gmail.com End User
178 Vijay Yadav vjyaav177@gmail.com End User
179 Vijay Yadav vjyaav178@gmail.com End User
180 Vijay Yadav vjyaav179@gmail.com End User
181 Vijay Yadav vjyaav180@gmail.com End User
182 Vijay Yadav vjyaav181@gmail.com End User
183 Vijay Yadav vjyaav182@gmail.com End User
184 Vijay Yadav vjyaav183@gmail.com End User
185 Vijay Yadav vjyaav184@gmail.com End User
186 Vijay Yadav vjyaav185@gmail.com End User
187 Vijay Yadav vjyaav186@gmail.com End User
188 Vijay Yadav vjyaav187@gmail.com End User
189 Vijay Yadav vjyaav188@gmail.com End User
190 Vijay Yadav vjyaav189@gmail.com End User
191 Vijay Yadav vjyaav190@gmail.com End User
192 Vijay Yadav vjyaav191@gmail.com End User
193 Vijay Yadav vjyaav192@gmail.com End User
194 Vijay Yadav vjyaav193@gmail.com End User
195 Vijay Yadav vjyaav194@gmail.com End User
196 Vijay Yadav vjyaav195@gmail.com End User
197 Vijay Yadav vjyaav196@gmail.com End User
198 Vijay Yadav vjyaav197@gmail.com End User
199 Vijay Yadav vjyaav198@gmail.com End User
200 Vijay Yadav vjyaav199@gmail.com End User
201 Vijay Yadav vjyaav200@gmail.com End User
202 Vijay Yadav vjyaav201@gmail.com End User
203 Vijay Yadav vjyaav202@gmail.com End User
204 Vijay Yadav vjyaav203@gmail.com End User
205 Vijay Yadav vjyaav204@gmail.com End User
206 Vijay Yadav vjyaav205@gmail.com End User
207 Vijay Yadav vjyaav206@gmail.com End User
208 Vijay Yadav vjyaav207@gmail.com End User
209 Vijay Yadav vjyaav208@gmail.com End User
210 Vijay Yadav vjyaav209@gmail.com End User
211 Vijay Yadav vjyaav210@gmail.com End User
212 Vijay Yadav vjyaav211@gmail.com End User
213 Vijay Yadav vjyaav212@gmail.com End User
214 Vijay Yadav vjyaav213@gmail.com End User
215 Vijay Yadav vjyaav214@gmail.com End User
216 Vijay Yadav vjyaav215@gmail.com End User
217 Vijay Yadav vjyaav216@gmail.com End User
218 Vijay Yadav vjyaav217@gmail.com End User
219 Vijay Yadav vjyaav218@gmail.com End User
220 Vijay Yadav vjyaav219@gmail.com End User
221 Vijay Yadav vjyaav220@gmail.com End User
222 Vijay Yadav vjyaav221@gmail.com End User
223 Vijay Yadav vjyaav222@gmail.com End User
224 Vijay Yadav vjyaav223@gmail.com End User
225 Vijay Yadav vjyaav224@gmail.com End User
226 Vijay Yadav vjyaav225@gmail.com End User
227 Vijay Yadav vjyaav226@gmail.com End User
228 Vijay Yadav vjyaav227@gmail.com End User
229 Vijay Yadav vjyaav228@gmail.com End User
230 Vijay Yadav vjyaav229@gmail.com End User
231 Vijay Yadav vjyaav230@gmail.com End User
232 Vijay Yadav vjyaav231@gmail.com End User
233 Vijay Yadav vjyaav232@gmail.com End User
234 Vijay Yadav vjyaav233@gmail.com End User
235 Vijay Yadav vjyaav234@gmail.com End User
236 Vijay Yadav vjyaav235@gmail.com End User
237 Vijay Yadav vjyaav236@gmail.com End User
238 Vijay Yadav vjyaav237@gmail.com End User
239 Vijay Yadav vjyaav238@gmail.com End User
240 Vijay Yadav vjyaav239@gmail.com End User
241 Vijay Yadav vjyaav240@gmail.com End User
242 Vijay Yadav vjyaav241@gmail.com End User
243 Vijay Yadav vjyaav242@gmail.com End User
244 Vijay Yadav vjyaav243@gmail.com End User
245 Vijay Yadav vjyaav244@gmail.com End User
246 Vijay Yadav vjyaav245@gmail.com End User
247 Vijay Yadav vjyaav246@gmail.com End User
248 Vijay Yadav vjyaav247@gmail.com End User
249 Vijay Yadav vjyaav248@gmail.com End User
250 Vijay Yadav vjyaav249@gmail.com End User
251 Vijay Yadav vjyaav250@gmail.com End User
252 Vijay Yadav vjyaav251@gmail.com End User

View 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,,
1 First Name Last Name Email User Role Group Metadata
2 Vijay Yadav vjyaav1@gmail.com Admin
3 Vijay Yadav vjyaav2@gmail.com Builder
4 Vijay Yadav vjyaav3@gmail.com Builder
5 Vijay Yadav vjyaav4@gmail.com End User
6 Vijay Yadav vjyaav5@gmail.com End User
7 Vijay Yadav vjyaav6@gmail.com End User
8 Vijay Yadav vjyaav7@gmail.com End User
9 Vijay Yadav vjyaav8@gmail.com End User
10 Vijay Yadav vjyaav9@gmail.com End User
11 Vijay Yadav vjyaav10@gmail.com End User
12 Vijay Yadav vjyaav11@gmail.com End User
13 Vijay Yadav vjyaav12@gmail.com End User
14 Vijay Yadav vjyaav13@gmail.com End User
15 Vijay Yadav vjyaav14@gmail.com End User
16 Vijay Yadav vjyaav15@gmail.com End User
17 Vijay Yadav vjyaav16@gmail.com End User
18 Vijay Yadav vjyaav17@gmail.com End User
19 Vijay Yadav vjyaav18@gmail.com End User
20 Vijay Yadav vjyaav19@gmail.com End User
21 Vijay Yadav vjyaav20@gmail.com End User
22 Vijay Yadav vjyaav21@gmail.com End User
23 Vijay Yadav vjyaav22@gmail.com End User
24 Vijay Yadav vjyaav23@gmail.com End User
25 Vijay Yadav vjyaav24@gmail.com End User
26 Vijay Yadav vjyaav25@gmail.com End User
27 Vijay Yadav vjyaav26@gmail.com End User
28 Vijay Yadav vjyaav27@gmail.com End User
29 Vijay Yadav vjyaav28@gmail.com End User
30 Vijay Yadav vjyaav29@gmail.com End User
31 Vijay Yadav vjyaav30@gmail.com End User
32 Vijay Yadav vjyaav31@gmail.com End User
33 Vijay Yadav vjyaav32@gmail.com End User
34 Vijay Yadav vjyaav33@gmail.com End User
35 Vijay Yadav vjyaav34@gmail.com End User
36 Vijay Yadav vjyaav35@gmail.com End User
37 Vijay Yadav vjyaav36@gmail.com End User
38 Vijay Yadav vjyaav37@gmail.com End User
39 Vijay Yadav vjyaav38@gmail.com End User
40 Vijay Yadav vjyaav39@gmail.com End User
41 Vijay Yadav vjyaav40@gmail.com End User
42 Vijay Yadav vjyaav41@gmail.com End User
43 Vijay Yadav vjyaav42@gmail.com End User
44 Vijay Yadav vjyaav43@gmail.com End User
45 Vijay Yadav vjyaav44@gmail.com End User
46 Vijay Yadav vjyaav45@gmail.com End User
47 Vijay Yadav vjyaav46@gmail.com End User
48 Vijay Yadav vjyaav47@gmail.com End User
49 Vijay Yadav vjyaav48@gmail.com End User
50 Vijay Yadav vjyaav49@gmail.com End User
51 Vijay Yadav vjyaav50@gmail.com End User
52 Vijay Yadav vjyaav51@gmail.com End User
53 Vijay Yadav vjyaav52@gmail.com End User
54 Vijay Yadav vjyaav53@gmail.com End User
55 Vijay Yadav vjyaav54@gmail.com End User
56 Vijay Yadav vjyaav55@gmail.com End User
57 Vijay Yadav vjyaav56@gmail.com End User
58 Vijay Yadav vjyaav57@gmail.com End User
59 Vijay Yadav vjyaav58@gmail.com End User
60 Vijay Yadav vjyaav59@gmail.com End User
61 Vijay Yadav vjyaav60@gmail.com End User
62 Vijay Yadav vjyaav61@gmail.com End User
63 Vijay Yadav vjyaav62@gmail.com End User
64 Vijay Yadav vjyaav63@gmail.com End User
65 Vijay Yadav vjyaav64@gmail.com End User
66 Vijay Yadav vjyaav65@gmail.com End User
67 Vijay Yadav vjyaav66@gmail.com End User
68 Vijay Yadav vjyaav67@gmail.com End User
69 Vijay Yadav vjyaav68@gmail.com End User
70 Vijay Yadav vjyaav69@gmail.com End User
71 Vijay Yadav vjyaav70@gmail.com End User
72 Vijay Yadav vjyaav71@gmail.com End User
73 Vijay Yadav vjyaav72@gmail.com End User
74 Vijay Yadav vjyaav73@gmail.com End User
75 Vijay Yadav vjyaav74@gmail.com End User
76 Vijay Yadav vjyaav75@gmail.com End User
77 Vijay Yadav vjyaav76@gmail.com End User
78 Vijay Yadav vjyaav77@gmail.com End User
79 Vijay Yadav vjyaav78@gmail.com End User
80 Vijay Yadav vjyaav79@gmail.com End User
81 Vijay Yadav vjyaav80@gmail.com End User
82 Vijay Yadav vjyaav81@gmail.com End User
83 Vijay Yadav vjyaav82@gmail.com End User
84 Vijay Yadav vjyaav83@gmail.com End User
85 Vijay Yadav vjyaav84@gmail.com End User
86 Vijay Yadav vjyaav85@gmail.com End User
87 Vijay Yadav vjyaav86@gmail.com End User
88 Vijay Yadav vjyaav87@gmail.com End User
89 Vijay Yadav vjyaav88@gmail.com End User
90 Vijay Yadav vjyaav89@gmail.com End User
91 Vijay Yadav vjyaav90@gmail.com End User
92 Vijay Yadav vjyaav91@gmail.com End User
93 Vijay Yadav vjyaav92@gmail.com End User
94 Vijay Yadav vjyaav93@gmail.com End User
95 Vijay Yadav vjyaav94@gmail.com End User
96 Vijay Yadav vjyaav95@gmail.com End User
97 Vijay Yadav vjyaav96@gmail.com End User
98 Vijay Yadav vjyaav97@gmail.com End User
99 Vijay Yadav vjyaav98@gmail.com End User
100 Vijay Yadav vjyaav99@gmail.com End User
101 Vijay Yadav vjyaav100@gmail.com End User
102 Vijay Yadav vjyaav101@gmail.com End User
103 Vijay Yadav vjyaav102@gmail.com End User
104 Vijay Yadav vjyaav103@gmail.com End User
105 Vijay Yadav vjyaav104@gmail.com End User
106 Vijay Yadav vjyaav105@gmail.com End User
107 Vijay Yadav vjyaav106@gmail.com End User
108 Vijay Yadav vjyaav107@gmail.com End User
109 Vijay Yadav vjyaav108@gmail.com End User
110 Vijay Yadav vjyaav109@gmail.com End User
111 Vijay Yadav vjyaav110@gmail.com End User
112 Vijay Yadav vjyaav111@gmail.com End User
113 Vijay Yadav vjyaav112@gmail.com End User
114 Vijay Yadav vjyaav113@gmail.com End User
115 Vijay Yadav vjyaav114@gmail.com End User
116 Vijay Yadav vjyaav115@gmail.com End User
117 Vijay Yadav vjyaav116@gmail.com End User
118 Vijay Yadav vjyaav117@gmail.com End User
119 Vijay Yadav vjyaav118@gmail.com End User
120 Vijay Yadav vjyaav119@gmail.com End User
121 Vijay Yadav vjyaav120@gmail.com End User
122 Vijay Yadav vjyaav121@gmail.com End User
123 Vijay Yadav vjyaav122@gmail.com End User
124 Vijay Yadav vjyaav123@gmail.com End User
125 Vijay Yadav vjyaav124@gmail.com End User
126 Vijay Yadav vjyaav125@gmail.com End User
127 Vijay Yadav vjyaav126@gmail.com End User
128 Vijay Yadav vjyaav127@gmail.com End User
129 Vijay Yadav vjyaav128@gmail.com End User
130 Vijay Yadav vjyaav129@gmail.com End User
131 Vijay Yadav vjyaav130@gmail.com End User
132 Vijay Yadav vjyaav131@gmail.com End User
133 Vijay Yadav vjyaav132@gmail.com End User
134 Vijay Yadav vjyaav133@gmail.com End User
135 Vijay Yadav vjyaav134@gmail.com End User
136 Vijay Yadav vjyaav135@gmail.com End User
137 Vijay Yadav vjyaav136@gmail.com End User
138 Vijay Yadav vjyaav137@gmail.com End User
139 Vijay Yadav vjyaav138@gmail.com End User
140 Vijay Yadav vjyaav139@gmail.com End User
141 Vijay Yadav vjyaav140@gmail.com End User
142 Vijay Yadav vjyaav141@gmail.com End User
143 Vijay Yadav vjyaav142@gmail.com End User
144 Vijay Yadav vjyaav143@gmail.com End User
145 Vijay Yadav vjyaav144@gmail.com End User
146 Vijay Yadav vjyaav145@gmail.com End User
147 Vijay Yadav vjyaav146@gmail.com End User
148 Vijay Yadav vjyaav147@gmail.com End User
149 Vijay Yadav vjyaav148@gmail.com End User
150 Vijay Yadav vjyaav149@gmail.com End User
151 Vijay Yadav vjyaav150@gmail.com End User
152 Vijay Yadav vjyaav151@gmail.com End User
153 Vijay Yadav vjyaav152@gmail.com End User
154 Vijay Yadav vjyaav153@gmail.com End User
155 Vijay Yadav vjyaav154@gmail.com End User
156 Vijay Yadav vjyaav155@gmail.com End User
157 Vijay Yadav vjyaav156@gmail.com End User
158 Vijay Yadav vjyaav157@gmail.com End User
159 Vijay Yadav vjyaav158@gmail.com End User
160 Vijay Yadav vjyaav159@gmail.com End User
161 Vijay Yadav vjyaav160@gmail.com End User
162 Vijay Yadav vjyaav161@gmail.com End User
163 Vijay Yadav vjyaav162@gmail.com End User
164 Vijay Yadav vjyaav163@gmail.com End User
165 Vijay Yadav vjyaav164@gmail.com End User
166 Vijay Yadav vjyaav165@gmail.com End User
167 Vijay Yadav vjyaav166@gmail.com End User
168 Vijay Yadav vjyaav167@gmail.com End User
169 Vijay Yadav vjyaav168@gmail.com End User
170 Vijay Yadav vjyaav169@gmail.com End User
171 Vijay Yadav vjyaav170@gmail.com End User
172 Vijay Yadav vjyaav171@gmail.com End User
173 Vijay Yadav vjyaav172@gmail.com End User
174 Vijay Yadav vjyaav173@gmail.com End User
175 Vijay Yadav vjyaav174@gmail.com End User
176 Vijay Yadav vjyaav175@gmail.com End User
177 Vijay Yadav vjyaav176@gmail.com End User
178 Vijay Yadav vjyaav177@gmail.com End User
179 Vijay Yadav vjyaav178@gmail.com End User
180 Vijay Yadav vjyaav179@gmail.com End User
181 Vijay Yadav vjyaav180@gmail.com End User
182 Vijay Yadav vjyaav181@gmail.com End User
183 Vijay Yadav vjyaav182@gmail.com End User
184 Vijay Yadav vjyaav183@gmail.com End User
185 Vijay Yadav vjyaav184@gmail.com End User
186 Vijay Yadav vjyaav185@gmail.com End User
187 Vijay Yadav vjyaav186@gmail.com End User
188 Vijay Yadav vjyaav187@gmail.com End User
189 Vijay Yadav vjyaav188@gmail.com End User
190 Vijay Yadav vjyaav189@gmail.com End User
191 Vijay Yadav vjyaav190@gmail.com End User
192 Vijay Yadav vjyaav191@gmail.com End User
193 Vijay Yadav vjyaav192@gmail.com End User
194 Vijay Yadav vjyaav193@gmail.com End User
195 Vijay Yadav vjyaav194@gmail.com End User
196 Vijay Yadav vjyaav195@gmail.com End User
197 Vijay Yadav vjyaav196@gmail.com End User
198 Vijay Yadav vjyaav197@gmail.com End User
199 Vijay Yadav vjyaav198@gmail.com End User
200 Vijay Yadav vjyaav199@gmail.com End User
201 Vijay Yadav vjyaav200@gmail.com End User
202 Vijay Yadav vjyaav201@gmail.com End User
203 Vijay Yadav vjyaav202@gmail.com End User
204 Vijay Yadav vjyaav203@gmail.com End User
205 Vijay Yadav vjyaav204@gmail.com End User
206 Vijay Yadav vjyaav205@gmail.com End User
207 Vijay Yadav vjyaav206@gmail.com End User
208 Vijay Yadav vjyaav207@gmail.com End User
209 Vijay Yadav vjyaav208@gmail.com End User
210 Vijay Yadav vjyaav209@gmail.com End User
211 Vijay Yadav vjyaav210@gmail.com End User
212 Vijay Yadav vjyaav211@gmail.com End User
213 Vijay Yadav vjyaav212@gmail.com End User
214 Vijay Yadav vjyaav213@gmail.com End User
215 Vijay Yadav vjyaav214@gmail.com End User
216 Vijay Yadav vjyaav215@gmail.com End User
217 Vijay Yadav vjyaav216@gmail.com End User
218 Vijay Yadav vjyaav217@gmail.com End User
219 Vijay Yadav vjyaav218@gmail.com End User
220 Vijay Yadav vjyaav219@gmail.com End User
221 Vijay Yadav vjyaav220@gmail.com End User
222 Vijay Yadav vjyaav221@gmail.com End User
223 Vijay Yadav vjyaav222@gmail.com End User
224 Vijay Yadav vjyaav223@gmail.com End User
225 Vijay Yadav vjyaav224@gmail.com End User
226 Vijay Yadav vjyaav225@gmail.com End User
227 Vijay Yadav vjyaav226@gmail.com End User
228 Vijay Yadav vjyaav227@gmail.com End User
229 Vijay Yadav vjyaav228@gmail.com End User
230 Vijay Yadav vjyaav229@gmail.com End User
231 Vijay Yadav vjyaav230@gmail.com End User
232 Vijay Yadav vjyaav231@gmail.com End User
233 Vijay Yadav vjyaav232@gmail.com End User
234 Vijay Yadav vjyaav233@gmail.com End User
235 Vijay Yadav vjyaav234@gmail.com End User
236 Vijay Yadav vjyaav235@gmail.com End User
237 Vijay Yadav vjyaav236@gmail.com End User
238 Vijay Yadav vjyaav237@gmail.com End User
239 Vijay Yadav vjyaav238@gmail.com End User
240 Vijay Yadav vjyaav239@gmail.com End User
241 Vijay Yadav vjyaav240@gmail.com End User
242 Vijay Yadav vjyaav241@gmail.com End User
243 Vijay Yadav vjyaav242@gmail.com End User
244 Vijay Yadav vjyaav243@gmail.com End User
245 Vijay Yadav vjyaav244@gmail.com End User
246 Vijay Yadav vjyaav245@gmail.com End User
247 Vijay Yadav vjyaav246@gmail.com End User
248 Vijay Yadav vjyaav247@gmail.com End User
249 Vijay Yadav vjyaav248@gmail.com End User
250 Vijay Yadav vjyaav249@gmail.com End User
251 Vijay Yadav vjyaav250@gmail.com End User
252 Vijay Yadav vjyaav251@gmail.com End User

View file

@ -0,0 +1,3 @@
First Name,Last Name,Email,User Role,Group
test,test,,Admin,Admin
test,test,,Builder,Builder
1 First Name Last Name Email User Role Group
2 test test Admin Admin
3 test test Builder Builder

View file

@ -0,0 +1,3 @@
First Name,Last Name,Email,User Role,Group,Metadata
,,withoutname1@gmail.com,Admin,Admin,
,,withoutname2@gmail.com,Builder,Builder,
1 First Name Last Name Email User Role Group Metadata
2 withoutname1@gmail.com Admin Admin
3 withoutname2@gmail.com Builder Builder

View file

@ -0,0 +1,3 @@
First Name,Last Name,Email,User Role,Group
,,withoutname1@gmail.com,Admin,Admin
,,withoutname2@gmail.com,Builder,Builder
1 First Name Last Name Email User Role Group
2 withoutname1@gmail.com Admin Admin
3 withoutname2@gmail.com Builder Builder

View file

@ -0,0 +1,3 @@
First Name,Last Name,Email,User Role,Group,Metadata
,,withoutname1@gmail.com,Admin,Admin,
,,withoutname2@gmail.com,Builder,Builder,
1 First Name Last Name Email User Role Group Metadata
2 withoutname1@gmail.com Admin Admin
3 withoutname2@gmail.com Builder Builder

View file

@ -0,0 +1,3 @@
First Name,Last Name,Email,User Role,Group
Test,Example,test12@gmail.com,,
Test,Example,test13@gmail.com,,
1 First Name Last Name Email User Role Group
2 Test Example test12@gmail.com
3 Test Example test13@gmail.com

View file

@ -0,0 +1,3 @@
First Name,Last Name,Email,User Role,Group,Metadata
Test,Example,test12@gmail.com,,,
Test,Example,test13@gmail.com,,,
1 First Name Last Name Email User Role Group Metadata
2 Test Example test12@gmail.com
3 Test Example test13@gmail.com

View file

@ -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,
1 First Name Last Name Email User Role Group Metadata
2 test test demo1@gmail.com Admin test
3 test test demo2@gmail.com Builder abc

View file

@ -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,,
1 First Name Last Name Email User Role Group Metadata
2 test test demo11@gmail.com Admin
3 test test demo11@gmail.com Builder

View file

@ -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();
};
};

View file

@ -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,
});
};

View 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};`,
});
};

View file

@ -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,
});

View file

@ -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

View file

@ -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",

View file

@ -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",

View file

@ -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={() => {

View file

@ -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>

View file

@ -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 />}

View file

@ -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:

View file

@ -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') {

View file

@ -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>
/>
);
};

View file

@ -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;
}

View file

@ -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;

View file

@ -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`;
};

View file

@ -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]);
};

View file

@ -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)',
}}
>

View file

@ -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>

View file

@ -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} />
</>
);

View file

@ -2,6 +2,12 @@
&:focus-visible{
outline: none;
}
&.page-container {
&.position-top {
flex-direction: column;
}
}
}
.modal-backdrop {

View file

@ -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;

View file

@ -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;
};

View file

@ -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;
};

View 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;

View file

@ -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>
);
};

View file

@ -0,0 +1 @@
export { DropdownMenu as default } from './DropdownMenu';

View file

@ -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;
}
}

View file

@ -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>

View file

@ -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);

View file

@ -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);

View file

@ -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',
};

View file

@ -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}
/>
);

View file

@ -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,
};

View file

@ -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 && (

View file

@ -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;

View file

@ -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>

View file

@ -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% ;
}
}
}

View file

@ -367,6 +367,7 @@ export const FxParamTypeMapping = Object.freeze({
visibility: 'Visibility',
numberInput: 'NumberInput',
tableRowHeightInput: 'TableRowHeightInput',
dropdownMenu: 'DropdownMenu',
query: 'Query',
});

View 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>;
}

View 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');

View file

@ -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

View file

@ -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

View file

@ -149,7 +149,6 @@ function EditAppName() {
value={name}
maxLength={50}
data-cy="app-name-input"
disabled={appCreationMode === 'GIT'}
/>
</ToolTip>
<InfoOrErrorBox

View file

@ -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>

View file

@ -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"

View file

@ -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">

View file

@ -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>
);
};

View file

@ -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);
}
}
}
}
}

View file

@ -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

View file

@ -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}>

View file

@ -70,7 +70,7 @@ const EducativeLabel = ({ darkMode }) => {
faster. It uses OpenAI&apos;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"

View file

@ -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)}
>

View file

@ -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"
>

View file

@ -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

View file

@ -4,6 +4,8 @@
height: 36px;
margin-bottom: 8px;
margin-top: 16px;
margin-left: 16px;
margin-right: 16px;
}
.tj-tabs-container {

View file

@ -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) {

View file

@ -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

View file

@ -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;

View file

@ -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: [],
};

View file

@ -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