Merge branch 'bugfix/route-props-cypress' of github.com:ToolJet/ToolJet into bugfix/route-props-cypress

This commit is contained in:
arpitnath 2023-04-26 15:00:39 +05:30
commit 306fc0ce25
497 changed files with 17805 additions and 4987 deletions

View file

@ -36,15 +36,15 @@ jobs:
steps:
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- name: Use Node.js 18.3.0
uses: actions/setup-node@v2
uses: actions/setup-node@v3
with:
node-version: 18.3.0
- name: Cache node modules
uses: actions/cache@v2
uses: actions/cache@v3
env:
cache-name: cache-node-modules
with:
@ -66,15 +66,15 @@ jobs:
github.ref == 'refs/heads/develop'
steps:
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- name: Use Node.js 18.3.0
uses: actions/setup-node@v2
uses: actions/setup-node@v3
with:
node-version: 18.3.0
- name: Cache node modules
uses: actions/cache@v2
uses: actions/cache@v3
env:
cache-name: cache-node-modules
with:
@ -106,9 +106,9 @@ jobs:
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- name: Cache node modules
uses: actions/cache@v2
uses: actions/cache@v3
env:
cache-name: cache-node-modules
with:
@ -144,9 +144,9 @@ jobs:
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- name: Cache node modules
uses: actions/cache@v2
uses: actions/cache@v3
env:
cache-name: cache-node-modules
with:

View file

@ -14,10 +14,10 @@ jobs:
Run-Cypress:
if: ${{ github.event.action == 'labeled' && github.event.label.name == 'run-cypress' }}
runs-on: ubuntu-latest
runs-on: self-hosted
steps:
- name: Checkout
uses: actions/checkout@v2
uses: actions/checkout@v3
with:
ref: ${{ github.event.pull_request.head.ref }}
@ -33,6 +33,12 @@ jobs:
sleep 1
done
- name: Getting cypress secrets
# use quotes around the secret, as its value
# is simply inserted as a string into the command
run: |
echo '${{ secrets.CYPRESS_SECRETS }}' > cypress.env.json
- name: Cypress integration test
uses: cypress-io/github-action@v5
with:
@ -41,7 +47,7 @@ jobs:
config-file: cypress-run.config.js
- name: Capturing screenshots
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v3
if: always()
with:
name: screenshots

View file

@ -17,7 +17,7 @@ jobs:
steps:
- name: Checkout Repository
uses: actions/checkout@v2
uses: actions/checkout@v3
- name: Setting tag
if: "${{ github.event.inputs.version != '' }}"
@ -28,7 +28,7 @@ jobs:
run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV
- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v1
uses: aws-actions/configure-aws-credentials@v2
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}

View file

@ -1 +1 @@
2.4.0
2.4.9

View file

@ -76,8 +76,10 @@ module.exports = defineConfig({
experimentalRunAllSpecs: true,
baseUrl: "http://localhost:8082",
specPattern: [
"cypress/e2e/editor/widget/*.cy.js",
"cypress/e2e/editor/multipage/*.cy.js"],
"cypress/e2e/editor/app-version/version.cy.js",
"cypress/e2e/exportImport/export.cy.js",
"cypress/e2e/exportImport/import.cy.js",
"cypress/e2e/database/database.cy.js"],
numTestsKeptInMemory: 1,
redirectionLimit: 10,
experimentalRunAllSpecs: true,

View file

@ -13,7 +13,7 @@ module.exports = defineConfig({
requestTimeout: 10000,
pageLoadTimeout: 20000,
responseTimeout: 10000,
viewportWidth: 1200,
viewportWidth: 1440,
viewportHeight: 960,
chromeWebSecurity: false,
trashAssetsBeforeRuns: true,
@ -50,13 +50,17 @@ module.exports = defineConfig({
on("task", {
deleteFolder(folderName) {
return new Promise((resolve, reject) => {
rmdir(folderName, { maxRetries: 10, recursive: true }, (err) => {
if (err) {
console.error(err);
return reject(err);
}
resolve(null);
});
if (fs.existsSync(folderName)) {
rmdir(folderName, { maxRetries: 10, recursive: true }, (err) => {
if (err) {
console.error(err);
return reject(err);
}
return resolve(null);
});
} else {
return resolve(null);
}
});
},
});
@ -73,8 +77,11 @@ module.exports = defineConfig({
experimentalModfyObstructiveThirdPartyCode: true,
experimentalRunAllSpecs: true,
baseUrl: "http://localhost:8082",
specPattern: "cypress/e2e/**/*.cy.js",
downloadsFolder:"cypress/downloads",
specPattern: [
"cypress/e2e/editor/widget/*.cy.js",
"cypress/e2e/multipage/*.cy.js",
],
downloadsFolder: "cypress/downloads",
numTestsKeptInMemory: 25,
redirectionLimit: 10,
experimentalRunAllSpecs: true,

View file

@ -3,8 +3,9 @@ export const cyParamName = (paramName = "") => {
};
export const commonSelectors = {
toastMessage: ".go3958317564",
toastMessage: ".go3958317564",
oldToastMessage: ".go318386747",
newToastMessage:'.drawer-container > [style="position: fixed; z-index: 9999; inset: 16px; pointer-events: none;"] > .go4109123758 > .go2072408551',
toastCloseButton: '[data-cy="toast-close-button"]',
editButton: "[data-cy=edit-button]",
searchField: "[data-cy=widget-search-box]",
@ -42,7 +43,7 @@ export const commonSelectors = {
createNewFolderButton: "[data-cy=create-new-folder-button]",
folderNameInput: "[data-cy=folder-name-input]",
createFolderButton: "[data-cy=create-folder-button]",
folderList: ".css-2kg7t4-MenuList",
folderList: ".css-169zxdi-MenuList",
empytyFolderImage: "[data-cy=empty-folder-image]",
emptyFolderText: "[data-cy=empty-folder-text]",
allApplicationsLink: "[data-cy=all-applications-link]",
@ -56,9 +57,7 @@ export const commonSelectors = {
viewerPageLogo: '[data-cy="viewer-page-logo"]',
lastPageArrow: '[data-cy="last-page-link"]',
nextPageArrow: '[data-cy="next-page-link"]',
emailFilterInput: '[data-cy="email-filter-input-field"]',
firstNameFilterInput: '[data-cy="first-name-filter-input-field"]',
lastNameFilterInput: '[data-cy="last-name-filter-input-field"]',
inputUserSearch: '[data-cy="input-field-user-filter-search"]',
filterButton: '[data-cy="filter-button"]',
copyIcon: '[data-cy="copy-icon"]',
addWorkspaceButton: '[data-cy="add-new-workspace-link"]',
@ -84,11 +83,11 @@ export const commonSelectors = {
acceptInviteButton: '[data-cy="accept-invite-button"]',
databaseIcon: `[data-cy="database-icon"]`,
profileSettings: '[data-cy="profile-settings"]',
workspaceSettingsIcon: '[data-cy="workspace-settings-icon"]',
manageUsersOption: '[data-cy="manage-users-option"]',
manageGroupsOption: '[data-cy="manage-groups-option"]',
manageSSOOption: '[data-cy="manage-sso-option"]',
workspaceVariableOption: '[data-cy="workspace-variable-option"]',
workspaceSettingsIcon: '[data-cy="icon-workspace-settings"]',
manageUsersOption: '[data-cy="users-list-item"]',
manageGroupsOption: '[data-cy="groups-list-item"]',
manageSSOOption: '[data-cy="sso-list-item"]',
workspaceVariableOption: '[data-cy="workspace-variables-list-item"]',
clearFilterButton: '[data-cy="clear-filter-button"]',
userStatusSelect: '[data-cy="user-status-select-continer"]',
emailInputLabel: '[data-cy="email-input-label"]',
@ -159,6 +158,15 @@ export const commonSelectors = {
resetPasswordButton: '[data-cy="reset-password-button"]',
resetPasswordPageDescription: '[data-cy="reset-password-page-description"]',
backToLoginButton: '[data-cy="back-to-login-button"]',
breadcrumbTitle:'[data-cy="breadcrumb-title"]',
breadcrumbPageTitle:'[data-cy="breadcrumb-page-title"]',
labelFullNameInput: '[data-cy="label-full-name-input-field"]',
inputFieldFullName: '[data-cy="input-field-full-name"]',
labelEmailInput: '[data-cy="label-email-input-field"]',
inputFieldEmailAddress: '[data-cy="input-field-email"]',
closeButton: '[data-cy="close-button"]',
onboardingRadioButton: (radioButtonText) => {
return `[data-cy="${cyParamName(radioButtonText)}-radio-button"]`;

View file

@ -22,7 +22,7 @@ export const dashboardSelector = {
changeButton: "[data-cy=change-button]",
addToFolderTitle: "[data-cy=add-to-folder-title]",
moveAppText: "[data-cy=move-selected-app-to-text]",
selectFolder: '[data-cy="select-folder"]>>>.css-s59k37-ValueContainer',
selectFolder: '[data-cy="select-folder"]>>>>.css-h380uj-Input',
addToFolderButton: "[data-cy=add-to-folder-button]",
appTemplateRow: '[data-cy="app-template-row"]',
homePageContent: '[data-cy="home-page-content"]',

View file

@ -10,12 +10,12 @@ export const appVersionSelectors = {
createNewVersion: '[data-cy="create-new-version-title"]',
versionNamelabel: '[data-cy="version-name-label"]',
appVersionMenuField:
'[data-cy="app-version-selector"] .custom-version-selector__indicators',
'[data-cy="app-version-selector"] .undefined__indicators',
versionNameInputField: '[data-cy="version-name-input-field"]',
createVersionFromLabel: '[data-cy="create-version-from-label"]',
createVersionInputField: '[data-cy="create-version-from-input-field"]',
createNewVersionButton: '[data-cy="create-new-version-button"]',
appVersionContentList: ".custom-version-selector__menu-list",
appVersionContentList: ".undefined__menu-list",
};
export const exportAppModalSelectors = {
selectVersionTitle: '[data-cy= "select-a-version-to-export-title"]',

View file

@ -1,27 +1,31 @@
export const usersSelector = {
dropdown: "[data-cy=workspace-dropdown]",
inviteUserButton: "[data-cy=invite-new-user]",
buttonAddUsers: "[data-cy=button-invite-new-user]",
usersElements: {
pageTitle: "[data-cy=users-page-title]",
nameTitile: "[data-cy=name-title]",
emailTitle: "[data-cy=email-title]",
statusTitle: "[data-cy=status-title]",
usersTableNameColumnHeader: '[data-cy="users-table-name-column-header"]',
usersTableEmailColumnHeader: '[data-cy="users-table-email-column-header"]',
usersTableStatusColumnHeader: '[data-cy="users-table-status-column-header"]',
usersFilterLabel: '[data-cy="users-filter-label"]'
},
usersPageTitle: '[data-cy="title-users-page"]',
userFilterInput: '[data-cy="users-filter-input"]',
adminUserName: "[data-cy=user-name]",
adminUserEmail: "[data-cy=user-email]",
userStatus: "[data-cy=user-status]:eq(0)",
userState: "[data-cy=user-state]:eq(0)",
userStatus: "[data-cy=user-status]",
cardTitle: "[data-cy=add-new-user]",
firstNameInput: "[data-cy=first-name-input]",
addUsersCardTitle: '[data-cy="add-users-card-title"]',
inputFieldName: "[data-cy=first-name-input]",
lastNameInput: "[data-cy=last-name-input]",
emailLabel: "[data-cy=email-label]",
emailInput: "[data-cy=email-input]",
cancelButton: "[data-cy=cancel-button]",
createUserButton: "[data-cy=create-user-button]",
fisrtNameError: "[data-cy=first-name-error]",
lastNameError: "[data-cy=last-name-error]",
emailError: "[data-cy=email-error]",
buttonInviteUsers: '[data-cy="button-invite-users"]',
buttonInviteWithEmail: '[data-cy="button-invite-with-email"]',
buttonUploadCsvFile: '[data-cy="button-upload-csv-file"]',
fullNameError: '[data-cy="error-message-fullname"]',
emailError: '[data-cy="error-message-email"]',
pageLogo: "[data-cy=page-logo]",
invitePageHeader: '[data-cy="invite-page-header"]',
invitePgaeSubHeader: '[data-cy="invite-page-sub-header"]',
@ -47,7 +51,12 @@ export const usersSelector = {
inviteBulkUserButton: '[data-cy="invite-bulk-user-button"]',
bulkUserUploadPageTitle: '[data-cy="bulk-user-upload-page-title"]',
bulkUSerUploadInput: '[data-cy="bulk-user-upload-input"]',
downloadTemplateButton: '[data-cy="download-template-button"]',
createUsersButton: '[data-cy="create-users-button"]',
buttonDownloadTemplate: '[data-cy="button-download-template"]',
buttonUploadUsers: '[data-cy="button-upload-users"]',
helperTextBulkUpload: '[data-cy="helper-text-bulk-upload"]',
iconBulkUpload:'[data-cy="icon-bulk-upload"]',
helperTextSelectFile:'[data-cy="helper-text-select-file"]',
helperTextDropFile: '[data-cy="helper-text-drop-file"]',
inputFieldBulkUpload: '[data-cy="input-field-bulk-upload"]',
copyInvitationLink: '[data-cy="copy-invitation-link"]',
};

View file

@ -123,6 +123,14 @@ export const commonText = {
backToLoginButton: "Back to log in",
resetPasswordPageDescription:
"Your password has been reset successfully, log into ToolJet to continue your session",
labelFullNameInput: "Enter full name",
labelEmailInput: "Email address",
breadcrumbworkspaceSettingTitle:"Workspace settings",
breadcrumbGlobalDatasourceTitle: "Global datasources",
breadcrumbDatabaseTitle: "Databse",
breadcrumbApplications: "Applications",
breadcrumbSettings: "Settings",
emailPageDescription: (email) => {
return `Weve sent an email to ${email} with a verification link. Please use that to verify your email address.`;
},

View file

@ -1,20 +1,21 @@
export const usersText = {
usersElements: {
pageTitle: "Users & Permissions",
nameTitile: "NAME",
emailTitle: "EMAIL",
statusTitle: "STATUS",
usersTableNameColumnHeader: "NAME",
usersTableEmailColumnHeader: "EMAIL",
usersTableStatusColumnHeader: "STATUS",
usersFilterLabel: "Showing"
},
usersPageTitle: "users",
breadcrumbUsersPageTitle: " Users & permissions",
adminUserName: "The Developer",
adminUserEmail: "dev@tooljet.io",
adminUserState: "Archive",
inviteUserButton: "Invite new user",
cardTitle: "Add new user",
buttonAddUsers: "Add users",
addUsersCardTitle: "Add users",
emailLabel: "Email address",
cancelButton: "Cancel",
createUserButton: "Create User",
fieldRequired: "This field is required",
buttonInviteUsers: "Invite users",
errorTextFieldRequired: "This field is required",
exsitingEmail: "User with such email already exists.",
userCreatedToast: "User has been created",
inviteCopiedToast: "Invitation URL copied",
@ -47,6 +48,13 @@ export const usersText = {
"Added to the workspace and password has been set successfully.",
inviteBulkUserButton:"Invite bulk users",
bulkUserUploadPageTitle: "Upload Users",
downloadTemplateButton: 'Download Template',
createUsersButton:"Create Users",
buttonDownloadTemplate: 'Download Template',
buttonUploadUsers:"Upload users",
buttonInviteWithEmail: " Invite with email",
buttonUploadCsvFile: "Upload CSV file",
helperTextBulkUpload: 'Download the ToolJet template to add user details or format your file in the same as the template. ToolJet wont be able to recognise files in any other format. ',
helperTextSelectFile:'Select a CSV file to upload',
helperTextDropFile: 'Or drag and drop it here',
};

View file

@ -22,7 +22,9 @@ describe("User signup", () => {
cy.visit("/");
});
it("Verify sign up page elements", () => {
cy.get(commonSelectors.createAnAccountLink).click();
cy.wait(500);
cy.reload();
cy.get(commonSelectors.createAnAccountLink).realClick();
SignUpPageElements();
cy.clearAndType(commonSelectors.nameInputField, data.fullName);

View file

@ -27,7 +27,7 @@ describe("dashboard", () => {
const data = {};
data.appName = `${fake.companyName}-App`;
data.folderName = `${fake.companyName}-Folder`;
data.cloneAppName = `${data.appName}-Clone`;
data.cloneAppName = `cloned-${data.appName}`;
data.updatedFolderName = `New-${data.folderName}`;
beforeEach(() => {
@ -40,8 +40,11 @@ describe("dashboard", () => {
cy.intercept("GET", "/api/apps?page=1&folder=&searchKey=", {
fixture: "intercept/emptyDashboard.json",
}).as("emptyDashboard");
cy.intercept("GET", "/api/folders?searchKey=",{"folders":[]}).as("folders");
login();
cy.wait("@emptyDashboard");
cy.wait("@folders");
deleteDownloadsFolder();
});
@ -51,6 +54,7 @@ describe("dashboard", () => {
cy.get(
commonSelectors.workspaceName
).verifyVisibleElement("have.text", "My workspace");
cy.get(commonSelectors.workspaceName).click();
cy.get(commonSelectors.workspaceEditButton).should("be.visible");
cy.get(commonSelectors.appCreateButton).verifyVisibleElement(
"have.text",
@ -304,6 +308,7 @@ describe("dashboard", () => {
});
it("Should verify the app CRUD operation", () => {
data.appName = `${fake.companyName}-App`;
cy.appUILogin();
cy.createApp();
cy.renameApp(data.appName);
@ -328,6 +333,7 @@ describe("dashboard", () => {
});
it("Should verify the folder CRUD operation", () => {
data.appName = `${fake.companyName}-App`;
cy.appUILogin();
cy.createApp();
cy.renameApp(data.appName);

View file

@ -1,11 +1,11 @@
import { commonSelectors } from "Selectors/common";
import { commonSelectors } from "Selectors/common"
import { fake } from "Fixtures/fake";
import { usersText } from "Texts/manageUsers"
import { usersSelector } from "Selectors/manageUsers";
import { usersText } from "Texts/manageUsers";
import * as users from "Support/utils/manageUsers";
import * as common from "Support/utils/common";
import { path } from "Texts/common";
import { dashboardSelector } from "../../../constants/selectors/dashboard";
import { dashboardSelector } from "Selectors/dashboard";
const data = {};
data.firstName = fake.firstName;
@ -21,106 +21,43 @@ describe("Manage Users for multiple workspace", () => {
common.navigateToManageUsers();
users.manageUsersElements();
cy.get(usersSelector.cancelButton).click();
cy.get(usersSelector.usersElements.nameTitile).should("be.visible");
cy.get(usersSelector.inviteUserButton).click();
cy.get(commonSelectors.cancelButton).click();
cy.get(usersSelector.usersPageTitle).should("be.visible");
cy.get(usersSelector.buttonAddUsers).click();
cy.get(usersSelector.createUserButton).click();
cy.get(usersSelector.fisrtNameError).verifyVisibleElement(
cy.get(usersSelector.buttonInviteUsers).click();
cy.get(usersSelector.fullNameError).verifyVisibleElement("have.text",usersText.errorTextFieldRequired);
cy.get(usersSelector.emailError).verifyVisibleElement("have.text",usersText.errorTextFieldRequired);
cy.clearAndType(commonSelectors.inputFieldFullName, data.firstName);
cy.get(commonSelectors.inputFieldEmailAddress).clear();
cy.get(usersSelector.buttonInviteUsers).click();
cy.get(usersSelector.emailError).verifyVisibleElement("have.text",usersText.errorTextFieldRequired);
cy.get(commonSelectors.inputFieldFullName).clear();
cy.clearAndType(commonSelectors.inputFieldEmailAddress, data.email);
cy.get(usersSelector.buttonInviteUsers).click();
cy.get(usersSelector.fullNameError).verifyVisibleElement(
"have.text",
usersText.fieldRequired
);
cy.get(usersSelector.lastNameError).verifyVisibleElement(
"have.text",
usersText.fieldRequired
);
cy.get(usersSelector.emailError).verifyVisibleElement(
"have.text",
usersText.fieldRequired
usersText.errorTextFieldRequired
);
cy.clearAndType(usersSelector.firstNameInput, data.firstName);
cy.get(usersSelector.lastNameInput).clear();
cy.get(usersSelector.emailInput).clear();
cy.get(usersSelector.createUserButton).click();
cy.get(usersSelector.lastNameError).verifyVisibleElement(
"have.text",
usersText.fieldRequired
);
cy.get(usersSelector.emailError).verifyVisibleElement(
"have.text",
usersText.fieldRequired
);
cy.get(usersSelector.firstNameInput).clear();
cy.get(usersSelector.emailInput).clear();
cy.clearAndType(usersSelector.lastNameInput, data.lastName);
cy.get(usersSelector.createUserButton).click();
cy.get(usersSelector.fisrtNameError).verifyVisibleElement(
"have.text",
usersText.fieldRequired
);
cy.get(usersSelector.emailError).verifyVisibleElement(
"have.text",
usersText.fieldRequired
);
cy.get(usersSelector.firstNameInput).clear();
cy.get(usersSelector.lastNameInput).clear();
cy.clearAndType(usersSelector.emailInput, data.email);
cy.get(usersSelector.createUserButton).click();
cy.get(usersSelector.fisrtNameError).verifyVisibleElement(
"have.text",
usersText.fieldRequired
);
cy.get(usersSelector.lastNameError).verifyVisibleElement(
"have.text",
usersText.fieldRequired
);
cy.get(usersSelector.firstNameInput).clear();
cy.clearAndType(usersSelector.lastNameInput, data.lastName);
cy.clearAndType(usersSelector.emailInput, data.email);
cy.get(usersSelector.createUserButton).click();
cy.get(usersSelector.fisrtNameError).verifyVisibleElement(
"have.text",
usersText.fieldRequired
);
cy.get(usersSelector.lastNameInput).clear();
cy.clearAndType(usersSelector.firstNameInput, data.firstName);
cy.clearAndType(usersSelector.emailInput, data.email);
cy.get(usersSelector.createUserButton).click();
cy.get(usersSelector.lastNameError).verifyVisibleElement(
"have.text",
usersText.fieldRequired
);
cy.get(usersSelector.emailInput).clear();
cy.clearAndType(usersSelector.firstNameInput, data.firstName);
cy.clearAndType(usersSelector.lastNameInput, data.lastName);
cy.get(usersSelector.createUserButton).click();
cy.get(usersSelector.emailError).verifyVisibleElement(
"have.text",
usersText.fieldRequired
);
cy.clearAndType(usersSelector.firstNameInput, data.firstName);
cy.clearAndType(usersSelector.lastNameInput, data.lastName);
cy.clearAndType(commonSelectors.inputFieldFullName, data.firstName);
cy.clearAndType(
usersSelector.emailInput,
commonSelectors.inputFieldEmailAddress,
usersText.adminUserEmail
);
cy.get(usersSelector.createUserButton).click();
cy.get(usersSelector.buttonInviteUsers).click();
cy.verifyToastMessage(
commonSelectors.toastMessage,
commonSelectors.newToastMessage,
usersText.exsitingEmail
);
});
it("Should verify the confirm invite page and new user account", () => {
common.navigateToManageUsers();
users.inviteUser(data.firstName, data.lastName, data.email);
users.inviteUser(data.firstName, data.email);
users.confirmInviteElements();
cy.clearAndType(commonSelectors.passwordInputField, "pass");
@ -200,7 +137,7 @@ describe("Manage Users for multiple workspace", () => {
cy.contains("td", data.email)
.parent()
.within(() => {
cy.get("td img").click();
cy.get(usersSelector.copyInvitationLink).click();
});
cy.verifyToastMessage(
commonSelectors.toastMessage,
@ -219,7 +156,6 @@ describe("Manage Users for multiple workspace", () => {
cy.get("@copyToClipboardPrompt").then((prompt) => {
common.logout();
cy.visit(prompt.args[0][1]);
cy.url().should("include", path.confirmInvite);
});
cy.get(usersSelector.acceptInvite).click();

View file

@ -11,7 +11,7 @@ import { buttonText } from "Texts/button";
import { verifyComponent, deleteComponentAndVerify } from "Support/utils/basicComponents";
describe("App Export Functionality", () => {
describe("App Version Functionality", () => {
var data = {};
data.appName = `${fake.companyName}-App`;
let currentVersion = "";
@ -73,12 +73,10 @@ describe("App Export Functionality", () => {
deleteVersionAndVerify(currentVersion = "v5", deleteVersionText.deleteToastMessage(currentVersion = "v5"));
cy.reload();
releasedVersionAndVerify(currentVersion = "v3")
releasedVersionAndVerify(currentVersion = "v3");
editVersionAndVerify(currentVersion = "v3", newVersion = ["v5"], releasedVersionText.cannotUpdateReleasedVersionToastMessage);
closeModal(commonText.closeButton);
cy.reload();
closeModal(commonText.closeButton);
deleteVersionAndVerify(currentVersion = "v3", releasedVersionText.cannotDeleteReleasedVersionToastMessage)
navigateToCreateNewVersionModal(currentVersion = "v3");

View file

@ -5,7 +5,7 @@ import { tableSelector } from "Selectors/table";
import {
verifyComponent,
deleteComponentAndVerify,
verifyComponentWithOutLabel
verifyComponentWithOutLabel,
} from "Support/utils/basicComponents";
import {
openAccordion,
@ -52,16 +52,20 @@ describe("Basic components", () => {
editAndVerifyWidgetName("toggleswitch2");
verifyAndModifyParameter(commonWidgetText.parameterLabel, "label");
cy.forceClickOnCanvas()
cy.forceClickOnCanvas();
cy.waitForAutoSave();
cy.get('[data-cy="draggable-widget-toggleswitch2"] > .form-check-label').should('have.text', 'label')
cy.get(
'[data-cy="draggable-widget-toggleswitch2"] > .form-check-label'
).should("have.text", "label");
cy.openInCurrentTab(commonWidgetSelector.previewButton);
verifyComponent("toggleswitch2");
cy.get('[data-cy="draggable-widget-toggleswitch2"] > .form-check-label').should('have.text', 'label')
cy.get(
'[data-cy="draggable-widget-toggleswitch2"] > .form-check-label'
).should("have.text", "label");
cy.go("back");
cy.wait('@appVersion')
cy.wait("@appVersion");
deleteComponentAndVerify("toggleswitch2");
cy.get(commonSelectors.editorPageLogo).click();
@ -331,19 +335,24 @@ describe("Basic components", () => {
cy.deleteApp(data.appName);
});
//needed fix
//needed fix
it.skip("Should verify Custom Component", () => {
cy.dragAndDropWidget("Custom Component", 50, 50);
cy.get('[data-cy="draggable-widget-customcomponent1"]').click({ force: true });
cy.get('[data-cy="draggable-widget-customcomponent1"]').click({
force: true,
});
cy.forceClickOnCanvas();
verifyComponent("customcomponent1");
openEditorSidebar("customcomponent1");
// editAndVerifyWidgetName("customcomponent2", ["Code"]);
closeAccordions(["Code"]);
cy.get(commonWidgetSelector.WidgetNameInputField).type("{selectAll}{backspace}customcomponent2", {delay:30});
cy.forceClickOnCanvas()
cy.get(commonWidgetSelector.WidgetNameInputField).type(
"{selectAll}{backspace}customcomponent2",
{ delay: 30 }
);
cy.forceClickOnCanvas();
cy.get(commonWidgetSelector.draggableWidget(name)).trigger("mouseover");
cy.get(commonWidgetSelector.widgetConfigHandle(name))
.click()
@ -411,13 +420,23 @@ describe("Basic components", () => {
cy.deleteApp(data.appName);
});
//visible issue
//visible issue
it.skip("Should verify Divider", () => {
verifyComponentWithOutLabel("Divider", "divider1", "divider2", data.appName)
verifyComponentWithOutLabel(
"Divider",
"divider1",
"divider2",
data.appName
);
});
it("Should verify File Picker", () => {
verifyComponentWithOutLabel("File Picker", "filepicker1", "filepicker2", data.appName)
verifyComponentWithOutLabel(
"File Picker",
"filepicker1",
"filepicker2",
data.appName
);
});
it("Should verify Form", () => {
@ -463,27 +482,28 @@ describe("Basic components", () => {
});
it("Should verify Icon", () => {
verifyComponentWithOutLabel("Icon", "icon1", "icon2", data.appName)
verifyComponentWithOutLabel("Icon", "icon1", "icon2", data.appName);
});
it("Should verify Iframe", () => {
verifyComponentWithOutLabel("Iframe", "iframe1", "iframe2", data.appName)
verifyComponentWithOutLabel("Iframe", "iframe1", "iframe2", data.appName);
});
it.skip("Should verify Kamban", () => {
verifyComponentWithOutLabel("Kanban", "kanban1", "kanban2", data.appName) });
verifyComponentWithOutLabel("Kanban", "kanban1", "kanban2", data.appName);
});
it("Should verify Link", () => {
verifyComponentWithOutLabel("Link", "link1", "link2", data.appName)
verifyComponentWithOutLabel("Link", "link1", "link2", data.appName);
});
it("Should verify Map", () => {
cy.dragAndDropWidget("Map", 50, 50);
cy.get("body").then($body => {
cy.get("body").then(($body) => {
if ($body.find(".dismissButton").length > 0) {
cy.get('.dismissButton').click();
cy.get(".dismissButton").click();
}
})
});
verifyComponent("map1");
@ -505,7 +525,7 @@ describe("Basic components", () => {
});
it("Should verify Modal", () => {
verifyComponentWithOutLabel("Modal", "modal1", "modal2", data.appName)
verifyComponentWithOutLabel("Modal", "modal1", "modal2", data.appName);
});
it("Should verify PDF", () => {
@ -530,35 +550,70 @@ describe("Basic components", () => {
});
it("Should verify Pagination", () => {
verifyComponentWithOutLabel("Pagination", "pagination1", "pagination2", data.appName)
verifyComponentWithOutLabel(
"Pagination",
"pagination1",
"pagination2",
data.appName
);
});
it("Should verify QR Scanner", () => {
verifyComponentWithOutLabel("QR Scanner", "qrscanner1", "qrscanner2", data.appName)
verifyComponentWithOutLabel(
"QR Scanner",
"qrscanner1",
"qrscanner2",
data.appName
);
});
it("Should verify Range Slider", () => {
verifyComponentWithOutLabel("Range Slider", "rangeslider1", "rangeslider2", data.appName)
it.skip("Should verify Range Slider", () => {
verifyComponentWithOutLabel(
"Range Slider",
"rangeslider1",
"rangeslider2",
data.appName
);
});
it("Should verify Rich Text Editor", () => {
verifyComponentWithOutLabel("Text Editor", "richtexteditor1", "richtexteditor2", data.appName)
verifyComponentWithOutLabel(
"Text Editor",
"richtexteditor1",
"richtexteditor2",
data.appName
);
});
it("Should verify Spinner", () => {
verifyComponentWithOutLabel("Spinner", "spinner1", "spinner2", data.appName);
verifyComponentWithOutLabel(
"Spinner",
"spinner1",
"spinner2",
data.appName
);
});
it("Should verify Statistics", () => {
verifyComponentWithOutLabel("Statistics", "statistics1", "statistics2", data.appName)
verifyComponentWithOutLabel(
"Statistics",
"statistics1",
"statistics2",
data.appName
);
});
it("Should verify Steps", () => {
verifyComponentWithOutLabel("Steps", "steps1", "steps2", data.appName)
verifyComponentWithOutLabel("Steps", "steps1", "steps2", data.appName);
});
it("Should verify SVG Image", () => {
verifyComponentWithOutLabel("SVG Image", "svgimage1", "svgimage2", data.appName)
verifyComponentWithOutLabel(
"SVG Image",
"svgimage1",
"svgimage2",
data.appName
);
});
it("Should verify Tabs", () => {
@ -580,29 +635,49 @@ describe("Basic components", () => {
deleteComponentAndVerify("tabs2");
cy.get(commonSelectors.editorPageLogo).click();
cy.deleteApp(data.appName);
cy.deleteApp(data.appName);
});
it("Should verify Tags", () => {
verifyComponentWithOutLabel("Tags", "tags1", "tags2", data.appName)
verifyComponentWithOutLabel("Tags", "tags1", "tags2", data.appName);
});
it("Should verify Textarea", () => {
verifyComponentWithOutLabel("Textarea", "textarea1", "textarea2", data.appName)
verifyComponentWithOutLabel(
"Textarea",
"textarea1",
"textarea2",
data.appName
);
});
it("Should verify Timeline", () => {
verifyComponentWithOutLabel("Timeline", "timeline1", "timeline2", data.appName)
verifyComponentWithOutLabel(
"Timeline",
"timeline1",
"timeline2",
data.appName
);
});
it("Should verify Timer", () => {
verifyComponentWithOutLabel("Timer", "timer1", "timer2", data.appName)
verifyComponentWithOutLabel("Timer", "timer1", "timer2", data.appName);
});
it("Should verify Tree Select", () => {
verifyComponentWithOutLabel("Tree Select", "treeselect1", "treeselect2", data.appName)
verifyComponentWithOutLabel(
"Tree Select",
"treeselect1",
"treeselect2",
data.appName
);
});
it("Should verify Vertical Divider", () => {
verifyComponentWithOutLabel("Vertical Divider", "verticaldivider1", "verticaldivider2", data.appName)
});
verifyComponentWithOutLabel(
"Vertical Divider",
"verticaldivider1",
"verticaldivider2",
data.appName
);
});
});

View file

@ -1,24 +1,38 @@
import { commonSelectors, commonWidgetSelector } from "Selectors/common";
import { buttonText } from "Texts/button";
import { fake } from "Fixtures/fake";
import { commonWidgetText } from "Texts/common";
import { commonSelectors, commonWidgetSelector } from "Selectors/common";
import { numberInputText } from "Texts/numberInput";
import { verifyControlComponentAction } from "Support/utils/button";
import { selectEvent } from "Support/utils/events";
import {
launchModal,
closeModal,
launchButton,
verifySize,
addAndVerifyColor,
typeOnFx,
} from "Support/utils/modal";
import {
openAccordion,
verifyAndModifyParameter,
openEditorSidebar,
verifyAndModifyToggleFx,
verifyComponentValueFromInspector,
addDefaultEventHandler,
addAndVerifyTooltip,
verifyComponentFromInspector,
verifyAndModifyStylePickerFx,
verifyWidgetColorCss,
selectColourFromColourPicker,
verifyLoaderColor,
fillBoxShadowParams,
verifyBoxShadowCss,
verifyLayout,
verifyTooltip,
editAndVerifyWidgetName,
addTextWidgetToVerifyValue,
verifyPropertiesGeneralAccordion,
verifyStylesGeneralAccordion,
randomNumber,
fillBoxShadowParams,
selectColourFromColourPicker,
} from "Support/utils/commonWidget";
describe("Modal", () => {
@ -28,222 +42,204 @@ describe("Modal", () => {
cy.dragAndDropWidget("Modal");
});
it.only("should verify the properties of the number input widget", () => {
it("should verify the properties of the modal component", () => {
const data = {};
data.appName = `${fake.companyName}-App`;
data.alertMessage = fake.randomSentence;
data.widgetName = fake.widgetName;
data.customTitle = fake.randomSentence;
data.tooltipText = fake.randomSentence;
data.randomNumber = randomNumber(10, 99);
data.minimumvalue = randomNumber(5, 10);
data.maximumValue = randomNumber(90, 99);
data.buttonText = fake.companyName;
cy.renameApp(data.appName);
openEditorSidebar('modal1');
editAndVerifyWidgetName(data.widgetName, ["Options", "Properties", "Layout"]);
cy.get(`${
commonWidgetSelector.draggableWidget(data.widgetName)}>button`
).verifyVisibleElement("have.text", "Launch Modal");
openEditorSidebar(data.widgetName);
openAccordion(commonWidgetText.accordionProperties, [
"Events",
"Properties",
"Layout",
]);
verifyAndModifyParameter(
'Title',
"fakeName"
launchModal("modal1");
cy.get('[data-cy="modal-title"]').verifyVisibleElement(
"have.text",
"This title can be changed"
);
cy.forceClickOnCanvas();
cy.get(`${
commonWidgetSelector.draggableWidget(data.widgetName)}>button`
).click();
cy.get('[data-cy="modal-body"]').should("be.visible");
cy.get('[data-cy="modal-close-button"]').click();
cy.notVisible('[data-cy="modal-title"]');
cy.get('.modal-header').verifyVisibleElement('have.text', "fakeName")
cy.get('.modal-header> [class="cursor-pointer"]').click();
openEditorSidebar(data.widgetName);
openAccordion(commonWidgetText.accordionProperties, [
"Events",
openEditorSidebar("modal1", ["Options", "Properties", "Layout"]);
editAndVerifyWidgetName(data.widgetName, [
"Options",
"Properties",
"Layout",
]);
verifyComponentFromInspector(data.widgetName);
openAccordion(commonWidgetText.accordionProperties);
verifyAndModifyParameter("Title", data.customTitle);
launchModal(data.widgetName);
cy.get('[data-cy="modal-title"]').verifyVisibleElement(
"have.text",
data.customTitle
);
cy.get('[data-cy="modal-close-button"]').click();
verifyAndModifyToggleFx(
'Loading State',
commonWidgetText.codeMirrorLabelFalse
);
buttonText.loadingState,
commonWidgetText.codeMirrorLabelFalse
);
launchModal(data.widgetName);
cy.get(".spinner-border").should("be.visible");
cy.get(`${
commonWidgetSelector.draggableWidget(data.widgetName)}>button`
).click();
cy.get(
commonWidgetSelector.parameterTogglebutton(buttonText.loadingState)
).click();
cy.notVisible(".spinner-border");
cy.get('[class="spinner-border mt-5"]').should('be.visible');
verifyAndModifyToggleFx(
"Hide title bar",
commonWidgetText.codeMirrorLabelFalse
);
cy.notVisible('[data-cy="modal-title"]');
cy.get('[data-cy="hide-title-bar-toggle-button"]').click();
cy.get('[data-cy="modal-title"]').verifyVisibleElement(
"have.text",
data.customTitle
);
verifyAndModifyToggleFx(
'Hide title bar',
commonWidgetText.codeMirrorLabelFalse
);
cy.notVisible('.modal-header')
cy.get('[data-cy="hide-title-bar-toggle-button"]').click()
cy.get('.modal-header').should('be.visible');
verifyAndModifyToggleFx(
'Hide close button',
commonWidgetText.codeMirrorLabelFalse
);
cy.notVisible('.modal-header> [class="cursor-pointer"]')
cy.get('[data-cy="hide-close-button-toggle-button"]').click()
cy.get('.modal-header> [class="cursor-pointer"]').should('be.visible');
cy.realPress("Escape");
cy.notVisible('[data-cy="modal-title"]');
cy.realPress("Escape")
cy.notVisible('.modal-header')
verifyAndModifyToggleFx(
"Close on escape key",
commonWidgetText.codeMirrorLabelTrue
);
launchModal(data.widgetName);
cy.get(`${
commonWidgetSelector.draggableWidget(data.widgetName)}>button`
).click();
verifyAndModifyToggleFx(
'Hide on escape',
commonWidgetText.codeMirrorLabelTrue
);
cy.get('.modal-header> [class="cursor-pointer"]').should('be.visible');
cy.realPress("Escape");
cy.get('[data-cy="modal-title"]')
.verifyVisibleElement("have.text", data.customTitle)
.click();
closeModal(data.widgetName);
launchModal(data.widgetName);
verifySize("Medium");
verifySize("Large");
verifySize("Small");
verifyAndModifyToggleFx(
"Use default trigger button",
commonWidgetText.codeMirrorLabelTrue
);
cy.get('[data-cy="modal-close-button"]').click();
cy.notVisible(launchButton(data.widgetName));
cy.get('[data-cy="use-default-trigger-button-toggle-button"]').click();
// verifyComponentValueFromInspector(data.widgetName, data.randomNumber);
// cy.forceClickOnCanvas();
cy.get(
'[data-cy="trigger-button-label-input-field"]'
).clearAndTypeOnCodeMirror(data.buttonText);
cy.forceClickOnCanvas();
cy.get(launchButton(data.widgetName))
.verifyVisibleElement("have.text", data.buttonText)
.click();
// openEditorSidebar(data.widgetName);
// openAccordion(commonWidgetText.accordionProperties, [
// "Events",
// "Properties",
// "Layout",
// ]);
// verifyAndModifyParameter(
// commonWidgetText.labelMinimumValue,
// data.minimumvalue
// );
// cy.get(commonWidgetSelector.buttonCloseEditorSideBar).click();
// cy.clearAndType(
// commonWidgetSelector.draggableWidget(data.widgetName),
// randomNumber(1, 4)
// );
// cy.get(
// commonWidgetSelector.draggableWidget(data.widgetName)
// ).verifyVisibleElement("have.value", data.minimumvalue);
openAccordion(commonWidgetText.accordionEvents);
selectEvent("On open", "Show Alert");
cy.get('[data-cy="modal-close-button"]').click();
launchModal(data.widgetName);
cy.verifyToastMessage(commonSelectors.toastMessage, "Hello world!");
cy.get('[data-cy="modal-close-button"]').click();
// openEditorSidebar(data.widgetName);
// openAccordion(commonWidgetText.accordionProperties, [
// "Events",
// "Properties",
// "Layout",
// ]);
// verifyAndModifyParameter(
// commonWidgetText.labelMaximumValue,
// data.maximumValue
// );
// cy.clearAndType(
// commonWidgetSelector.draggableWidget(data.widgetName),
// randomNumber(100, 110)
// );
// cy.get(
// commonWidgetSelector.draggableWidget(data.widgetName)
// ).verifyVisibleElement("have.value", data.maximumValue);
verifyLayout(data.widgetName);
// openEditorSidebar(data.widgetName);
// openAccordion(commonWidgetText.accordionProperties, [
// "Events",
// "Properties",
// "Layout",
// ]);
// verifyAndModifyParameter(
// commonWidgetText.labelPlaceHolder,
// data.randomNumber
// );
// cy.get(commonWidgetSelector.buttonCloseEditorSideBar).click();
// cy.get(commonWidgetSelector.draggableWidget(data.widgetName))
// .invoke("attr", "placeholder")
// .should("contain", data.randomNumber);
cy.get(commonWidgetSelector.changeLayoutToDesktopButton).click();
cy.get(
commonWidgetSelector.parameterTogglebutton(
commonWidgetText.parameterShowOnDesktop
)
).click();
// verifyPropertiesGeneralAccordion(data.widgetName, data.tooltipText);
// // verifyLayout(data.widgetName);
// // cy.get(commonWidgetSelector.changeLayoutButton).click();
// // cy.get(
// // commonWidgetSelector.parameterTogglebutton(
// // commonWidgetText.parameterShowOnDesktop
// // )
// // ).click();
// cy.get(commonWidgetSelector.widgetDocumentationLink).should(
// "have.text",
// numberInputText.numberInputDocumentationLink
// );
cy.get(commonWidgetSelector.widgetDocumentationLink).should(
"have.text",
"Modal documentation"
);
cy.get(commonSelectors.editorPageLogo).click();
cy.deleteApp(data.appName);
});
it("should verify the styles of the number Modal component", () => {
it("should verify the styles of the modal widget", () => {
const data = {};
data.appName = `${fake.companyName}-App`;
data.colourHex = fake.randomRgbaHex;
data.boxShadowColor = fake.randomRgba;
data.colourHex = fake.randomRgbaHex;
data.boxShadowParam = fake.boxShadowParam;
data.backgroundColor = fake.randomRgba;
cy.renameApp(data.appName);
openEditorSidebar(numberInputText.defaultWidgetName);
launchModal("modal1");
cy.get(commonWidgetSelector.buttonStylesEditorSideBar).click();
addAndVerifyColor(
"Header background color",
"#ffffffff",
data.backgroundColor,
"[data-cy='modal-header']"
);
data.backgroundColor = fake.randomRgba;
addAndVerifyColor(
"Header title color",
"#000000",
data.backgroundColor,
"[data-cy='modal-header']",
"color"
);
data.backgroundColor = fake.randomRgba;
addAndVerifyColor(
"Body background color",
"#ffffffff",
data.backgroundColor,
"[data-cy='modal-body']"
);
data.backgroundColor = fake.randomRgba;
addAndVerifyColor(
"Trigger button background color",
"#4D72FA",
data.backgroundColor,
launchButton("modal1"),
"background-color"
);
data.backgroundColor = fake.randomRgba;
addAndVerifyColor(
"Trigger button text color",
"#ffffffff",
data.backgroundColor,
launchButton("modal1"),
"color"
);
cy.get("[data-cy='modal-header']").realClick();
verifyAndModifyToggleFx(
commonWidgetText.parameterVisibility,
commonWidgetText.codeMirrorLabelTrue
);
cy.get(
commonWidgetSelector.draggableWidget(numberInputText.defaultWidgetName)
).should("not.be.visible");
cy.get(
commonWidgetSelector.parameterTogglebutton(
commonWidgetText.parameterVisibility
)
).click();
cy.get('[data-cy="modal-close-button"]').click();
cy.get(commonWidgetSelector.draggableWidget("modal1")).should(
"not.be.visible"
);
cy.get(commonWidgetSelector.parameterTogglebutton("Visibility")).click();
verifyAndModifyToggleFx(
commonWidgetText.parameterDisable,
commonWidgetText.codeMirrorLabelFalse
);
cy.waitForAutoSave();
cy.get(
commonWidgetSelector.draggableWidget(numberInputText.defaultWidgetName)
).should("have.attr", "disabled");
cy.get(launchButton("modal1")).should("have.attr", "disabled");
cy.get(
commonWidgetSelector.parameterTogglebutton(
commonWidgetText.parameterDisable
)
).click();
verifyAndModifyParameter(
commonWidgetText.parameterBorderRadius,
commonWidgetText.borderRadiusInput
);
cy.get(commonWidgetSelector.buttonCloseEditorSideBar).click();
cy.get(
commonWidgetSelector.draggableWidget(numberInputText.defaultWidgetName)
).should("have.css", "border-radius", "20px");
verifyStylesGeneralAccordion(
numberInputText.defaultWidgetName,
data.boxShadowParam,
data.colourHex,
data.boxShadowColor,
3
cy.get(commonWidgetSelector.parameterTogglebutton("Disable")).click();
launchModal("modal1");
cy.get('[data-cy="modal-title"]').verifyVisibleElement(
"have.text",
"This title can be changed"
);
cy.get(commonSelectors.editorPageLogo).click();
@ -253,111 +249,171 @@ describe("Modal", () => {
it("should verify the app preview", () => {
const data = {};
data.appName = `${fake.companyName}-App`;
data.tooltipText = fake.randomSentence;
data.colourHex = fake.randomRgbaHex;
data.boxShadowColor = fake.randomRgba;
data.boxShadowParam = fake.boxShadowParam;
data.randomNumber = randomNumber(10, 99);
data.minimumvalue = randomNumber(5, 10);
data.maximumValue = randomNumber(90, 99);
data.bgColor = fake.randomRgba;
data.titleColor = fake.randomRgba;
data.bodyColor = fake.randomRgba;
data.buttonColor = fake.randomRgba;
data.buttonTextColor = fake.randomRgba;
data.customTitle = fake.randomSentence;
cy.get(".close-svg > path").click();
cy.dragAndDropWidget(commonWidgetText.toggleSwitch, 600, 50);
cy.get(".close-svg > path").click();
cy.dragAndDropWidget(commonWidgetText.toggleSwitch, 600, 100);
cy.get(".close-svg > path").click();
cy.dragAndDropWidget(commonWidgetText.toggleSwitch, 600, 150);
cy.get(".close-svg > path").click();
cy.dragAndDropWidget(commonWidgetText.toggleSwitch, 600, 200);
cy.get(".close-svg > path").click();
cy.dragAndDropWidget(commonWidgetText.toggleSwitch, 600, 250);
cy.get(".close-svg > path").click();
cy.renameApp(data.appName);
openEditorSidebar(numberInputText.defaultWidgetName);
verifyAndModifyParameter(
commonWidgetText.labelDefaultValue,
data.randomNumber
);
verifyAndModifyParameter(
commonWidgetText.labelMinimumValue,
data.minimumvalue
);
verifyAndModifyParameter(
commonWidgetText.labelMaximumValue,
data.maximumValue
);
verifyAndModifyParameter(
commonWidgetText.labelPlaceHolder,
data.randomNumber
);
verifyPropertiesGeneralAccordion(
numberInputText.defaultWidgetName,
data.tooltipText
);
openEditorSidebar(numberInputText.defaultWidgetName);
launchModal("modal1");
verifyAndModifyParameter("Title", data.customTitle);
cy.get(commonWidgetSelector.buttonStylesEditorSideBar).click();
verifyAndModifyParameter(
commonWidgetText.parameterBorderRadius,
commonWidgetText.borderRadiusInput
addAndVerifyColor(
"Header background color",
"#ffffffff",
data.bgColor,
"[data-cy='modal-header']"
);
cy.forceClickOnCanvas();
cy.waitForAutoSave();
cy.reload();
openEditorSidebar(numberInputText.defaultWidgetName);
cy.get(commonWidgetSelector.buttonStylesEditorSideBar).click();
openAccordion(commonWidgetText.accordionGenaral, []);
cy.get(commonWidgetSelector.boxShadowColorPicker).click();
fillBoxShadowParams(
commonWidgetSelector.boxShadowDefaultParam,
data.boxShadowParam
addAndVerifyColor(
"Header title color",
"#000000",
data.titleColor,
"[data-cy='modal-header']",
"color"
);
selectColourFromColourPicker(
commonWidgetText.boxShadowColor,
data.boxShadowColor,
3
);
addTextWidgetToVerifyValue("components.numberinput1.value");
addAndVerifyColor(
"Body background color",
"#ffffffff",
data.bodyColor,
"[data-cy='modal-body']"
);
addAndVerifyColor(
"Trigger button background color",
"#4D72FA",
data.buttonColor,
launchButton("modal1"),
"background-color"
);
addAndVerifyColor(
"Trigger button text color",
"#ffffffff",
data.buttonTextColor,
launchButton("modal1"),
"color"
);
cy.get("[data-cy='modal-header']").realClick();
typeOnFx(
commonWidgetText.parameterVisibility,
"{{components.toggleswitch1.value"
);
cy.get("[data-cy='modal-header']").realClick();
typeOnFx(
commonWidgetText.parameterDisable,
"{{components.toggleswitch2.value"
);
cy.get(".close-svg > path").click();
cy.get("[data-cy='modal-header']").realClick();
typeOnFx("Loading State", "{{components.toggleswitch3.value");
cy.get("[data-cy='modal-header']").realClick();
typeOnFx("Hide title bar", "{{components.toggleswitch4.value");
cy.get("[data-cy='modal-header']").realClick();
typeOnFx("Hide close button", "{{components.toggleswitch5.value");
cy.get("[data-cy='modal-header']").realClick();
cy.waitForAutoSave();
cy.openInCurrentTab(commonWidgetSelector.previewButton);
cy.wait(2000);
cy.get(
commonWidgetSelector.draggableWidget(numberInputText.defaultWidgetName)
).verifyVisibleElement("have.value", data.randomNumber);
cy.notVisible(launchButton("modal1"));
cy.get(commonWidgetSelector.draggableWidget("toggleswitch1"))
.find(".form-check-input")
.click();
cy.get(launchButton("modal1")).should("be.visible");
cy.clearAndType(
commonWidgetSelector.draggableWidget(numberInputText.defaultWidgetName),
randomNumber(1, 4)
cy.get(commonWidgetSelector.draggableWidget("toggleswitch2"))
.find(".form-check-input")
.click();
cy.get(launchButton("modal1")).should("have.attr", "disabled");
cy.get(commonWidgetSelector.draggableWidget("toggleswitch2"))
.find(".form-check-input")
.click();
cy.get(commonWidgetSelector.draggableWidget("toggleswitch3"))
.find(".form-check-input")
.click();
launchModal("modal1");
cy.get(".spinner-border").should("be.visible");
cy.realPress("Escape");
cy.get(commonWidgetSelector.draggableWidget("toggleswitch3"))
.find(".form-check-input")
.click();
cy.get(commonWidgetSelector.draggableWidget("toggleswitch4"))
.find(".form-check-input")
.click();
launchModal("modal1");
cy.notVisible('[data-cy="modal-title"]');
cy.realPress("Escape");
cy.get(commonWidgetSelector.draggableWidget("toggleswitch4"))
.find(".form-check-input")
.click();
launchModal("modal1");
verifyWidgetColorCss(
"[data-cy='modal-header']",
"background-color",
data.bgColor,
true
);
cy.get(
commonWidgetSelector.draggableWidget(numberInputText.defaultWidgetName)
).verifyVisibleElement("have.value", data.minimumvalue);
cy.clearAndType(
commonWidgetSelector.draggableWidget(numberInputText.defaultWidgetName),
randomNumber(100, 110)
verifyWidgetColorCss(
"[data-cy='modal-header']",
"color",
data.titleColor,
true
);
cy.get(
commonWidgetSelector.draggableWidget(numberInputText.defaultWidgetName)
).verifyVisibleElement("have.value", data.maximumValue);
cy.get(
commonWidgetSelector.draggableWidget(numberInputText.defaultWidgetName)
)
.invoke("attr", "placeholder")
.should("contain", data.randomNumber);
verifyTooltip(
commonWidgetSelector.draggableWidget(numberInputText.defaultWidgetName),
data.tooltipText
);
cy.get(
commonWidgetSelector.draggableWidget(commonWidgetText.text1)
).verifyVisibleElement("have.text", data.maximumValue);
cy.get(
commonWidgetSelector.draggableWidget(numberInputText.defaultWidgetName)
).should("have.css", "border-radius", "20px");
verifyBoxShadowCss(
numberInputText.defaultWidgetName,
data.boxShadowColor,
data.boxShadowParam
verifyWidgetColorCss(
"[data-cy='modal-body']",
"background-color",
data.bodyColor,
true
);
cy.get(commonSelectors.viewerPageLogo).click();
cy.deleteApp(data.appName);
cy.realPress("Escape");
verifyWidgetColorCss(
launchButton("modal1"),
"color",
data.buttonTextColor,
true
);
verifyWidgetColorCss(
launchButton("modal1"),
"background-color",
data.buttonColor,
true
);
launchModal("modal1");
cy.get('[data-cy="modal-title"]').verifyVisibleElement(
"have.text",
data.customTitle
);
cy.realPress("Escape");
cy.get(commonWidgetSelector.draggableWidget("toggleswitch5"))
.find(".form-check-input")
.click();
launchModal("modal1");
cy.notVisible('[data-cy="modal-close-button"]');
});
});

View file

@ -91,7 +91,10 @@ describe("App Export Functionality", () => {
cy.get(appVersionSelectors.appVersionMenuField)
.should("be.visible")
.click();
createNewVersion((otherVersions = ["v2"]));
createNewVersion(otherVersions = ["v2"], currentVersion = "v1");
cy.wait(500);
cy.dragAndDropWidget("Toggle Switch", 50, 50);
cy.waitForAutoSave();
cy.get(appVersionSelectors.currentVersionField((otherVersions = "v2")))
.should("be.visible")
.invoke("text")

View file

@ -51,7 +51,7 @@ describe("App Import Functionality", () => {
force: true,
});
cy.verifyToastMessage(
commonSelectors.oldToastMessage,
commonSelectors.toastMessage,
importText.couldNotImportAppToastMessage
);
@ -60,7 +60,7 @@ describe("App Import Functionality", () => {
});
cy.get(".driver-close-btn").click();
cy.verifyToastMessage(
commonSelectors.oldToastMessage,
commonSelectors.toastMessage,
importText.appImportedToastMessage
);
cy.get(commonSelectors.appNameInput).verifyVisibleElement(
@ -104,7 +104,7 @@ describe("App Import Functionality", () => {
force: true,
});
cy.verifyToastMessage(
commonSelectors.oldToastMessage,
commonSelectors.toastMessage,
importText.appImportedToastMessage
);
cy.get(
@ -132,10 +132,11 @@ describe("App Import Functionality", () => {
cy.reload();
navigateToAppEditor(data.appReName);
cy.wait(500);
cy.get(appVersionSelectors.appVersionMenuField)
.should("be.visible")
.click();
createNewVersion((otherVersions = ["v2"]));
createNewVersion(otherVersions = ["v2"], currentVersion = "v1");
cy.get(appVersionSelectors.currentVersionField((otherVersions = "v2")))
.should("be.visible")
.click()
@ -174,7 +175,7 @@ describe("App Import Functionality", () => {
}
);
cy.verifyToastMessage(
commonSelectors.oldToastMessage,
commonSelectors.toastMessage,
importText.appImportedToastMessage
);
cy.get(appVersionSelectors.appVersionMenuField).click();

View file

@ -92,7 +92,7 @@ Cypress.Commands.add("waitForAutoSave", () => {
Cypress.Commands.add("createApp", (appName) => {
cy.get("body").then(($title) => {
if ($title.text().includes(commonText.introductionMessage)) {
cy.get(commonSelectors.emptyAppCreateButton).click();
cy.get(commonSelectors.emptyAppCreateButton).eq(0).click();
} else {
cy.get(commonSelectors.appCreateButton).click();
}
@ -237,6 +237,12 @@ Cypress.Commands.add("notVisible", (dataCy) => {
cy.get(dataCy).should("not.be.visible");
}
});
const log = Cypress.log({
name: 'notVisible',
displayName: 'Not Visible',
message: dataCy
})
});
Cypress.Commands.add("resizeWidget", (widgetName, x, y) => {

View file

@ -78,7 +78,7 @@ export const navigateToAppEditor = (appName) => {
.trigger("mousehover")
.trigger("mouseenter")
.find(commonSelectors.editButton)
.click();
.click({force:true});
//cy.wait("@appEditor");
};
@ -152,8 +152,7 @@ export const manageUsersPagination = (email) => {
};
export const searchUser = (email) => {
cy.clearAndType(commonSelectors.emailFilterInput, email);
cy.get(commonSelectors.filterButton).click();
cy.clearAndType(commonSelectors.inputUserSearch, email);
};
export const createWorkspace = (workspaceName) => {

View file

@ -71,7 +71,8 @@ export const addDefaultEventHandler = (message) => {
.click();
cy.get(commonWidgetSelector.eventHandlerCard).click();
cy.get(commonWidgetSelector.alertMessageInputField)
.find('[data-cy*="-input-field"]').eq(0)
.find('[data-cy*="-input-field"]')
.eq(0)
.clearAndTypeOnCodeMirror(message);
};
@ -240,9 +241,16 @@ export const verifyAndModifyStylePickerFx = (
});
};
export const verifyWidgetColorCss = (widgetName, cssProperty, color) => {
export const verifyWidgetColorCss = (
widgetName,
cssProperty,
color,
innerProp = false
) => {
cy.forceClickOnCanvas();
cy.get(commonWidgetSelector.draggableWidget(widgetName)).should(
cy.get(
innerProp ? widgetName : commonWidgetSelector.draggableWidget(widgetName)
).should(
"have.css",
cssProperty,
`rgba(${color[0]}, ${color[1]}, ${color[2]}, ${color[3] / 100})`

View file

@ -17,10 +17,10 @@ import { commonText } from "Texts/common";
export const verifyAllElementsOfPage = () => {
cy.get(databaseSelectors.addTableButton).should("be.visible");
cy.get(databaseSelectors.tablePageHeader).verifyVisibleElement(
"have.text",
databaseText.tablePageHeader
);
// cy.get(databaseSelectors.tablePageHeader).verifyVisibleElement(
// "have.text",
// databaseText.tablePageHeader
// );
cy.get(databaseSelectors.doNotHaveTableText).verifyVisibleElement(
"have.text",
databaseText.doNotHaveTableText

View file

@ -0,0 +1,28 @@
export const selectEvent = (event, action) => {
cy.get('[data-cy="add-event-handler"]').click()
cy.get('[data-cy="event-handler"]').eq(0).click()
cy.get('[data-cy="event-selection"]')
.click()
.find("input")
.type(`{selectAll}{backspace}${event}{enter}`);
cy.get('[data-cy="action-selection"]')
.click()
.find("input")
.type(`{selectAll}{backspace}${action}{enter}`);
};
export const selectCSA = (
component,
componentAction,
dbounce = `{selectAll}{backspace}`
) => {
cy.get('[data-cy="action-options-component-selection-field"]')
.click()
.find("input")
.type(`{selectAll}{backspace}${component}`);
cy.get('[data-cy="action-options-action-selection-field"]')
.click()
.find("input")
.type(`{selectAll}{backspace}${componentAction}`);
cy.get('[data-cy="-input-field"]').type(`{selectAll}{backspace}${debounce}`);
};

View file

@ -70,7 +70,7 @@ export const createNewVersion = (newVersion = [], version) => {
export const clickOnExportButtonAndVerify = (buttonText, appName) => {
cy.get(commonSelectors.buttonSelector(buttonText)).click();
cy.wait(1000);
cy.exec("ls ./cypress/downloads/").then((result) => {
const downloadedAppExportFileName = result.stdout.split("\n")[0];
expect(downloadedAppExportFileName).to.have.string(appName.toLowerCase());

View file

@ -1,19 +1,33 @@
import { path } from "Texts/common";
import { commonSelectors } from "Selectors/common";
import { usersText } from "Texts/manageUsers";
import { commonSelectors } from "Selectors/common"
import { usersText } from "Texts/manageUsers"
import { usersSelector } from "Selectors/manageUsers";
import { ssoSelector } from "Selectors/manageSSO";
import { ssoText } from "Texts/manageSSO";
import * as common from "Support/utils/common";
import { commonText } from "../../constants/texts/common";
import { commonText } from "Texts/common";
export const manageUsersElements = () => {
cy.get(commonSelectors.breadcrumbTitle).should(($el) => {
expect($el.contents().first().text().trim()).to.eq(
commonText.breadcrumbworkspaceSettingTitle
);
});
cy.get(commonSelectors.breadcrumbPageTitle).verifyVisibleElement( "have.text",usersText.breadcrumbUsersPageTitle);
for (const element in usersSelector.usersElements) {
cy.get(usersSelector.usersElements[element]).verifyVisibleElement(
"have.text",
usersText.usersElements[element]
);
}
cy.get(usersSelector.usersPageTitle).should(($el) => {
expect($el.contents().last().text().trim()).to.eq(
usersText.usersPageTitle
);
});
cy.get(commonSelectors.inputUserSearch).should("be.visible")
common.searchUser(usersText.adminUserEmail);
cy.contains("td", usersText.adminUserEmail)
.parent()
@ -35,51 +49,56 @@ export const manageUsersElements = () => {
usersText.adminUserState
);
});
cy.get(commonSelectors.emailFilterInput).should("be.visible");
cy.get(commonSelectors.firstNameFilterInput).should("be.visible");
cy.get(commonSelectors.lastNameFilterInput).should("be.visible");
cy.get(commonSelectors.clearFilterButton).should("be.visible");
cy.get(commonSelectors.userStatusSelect).should("be.visible");
cy.get(usersSelector.userFilterInput).should("be.visible");
cy.get(usersSelector.inviteUserButton)
.verifyVisibleElement("have.text", usersText.inviteUserButton)
cy.get(usersSelector.buttonAddUsers)
.verifyVisibleElement("have.text", usersText.buttonAddUsers)
.click();
cy.get(usersSelector.cardTitle).verifyVisibleElement(
"have.text",
usersText.cardTitle
);
cy.get(usersSelector.firstNameInput).should("be.visible");
cy.get(usersSelector.lastNameInput).should("be.visible");
cy.get(usersSelector.emailLabel).verifyVisibleElement(
"have.text",
usersText.emailLabel
);
cy.get(usersSelector.lastNameInput).should("be.visible");
cy.get(usersSelector.cancelButton).verifyVisibleElement(
"have.text",
usersText.cancelButton
);
cy.get(usersSelector.createUserButton).verifyVisibleElement(
"have.text",
usersText.createUserButton
);
cy.get(usersSelector.cancelButton).click();
cy.get(usersSelector.buttonInviteWithEmail).verifyVisibleElement( "have.text",usersText.buttonInviteWithEmail);
cy.get(usersSelector.buttonUploadCsvFile).verifyVisibleElement("have.text",usersText.buttonUploadCsvFile);
cy.get(usersSelector.addUsersCardTitle).verifyVisibleElement("have.text",usersText.addUsersCardTitle);
cy.get(commonSelectors.labelFullNameInput).verifyVisibleElement("have.text",commonText.labelFullNameInput);
cy.get(commonSelectors.inputFieldFullName).should("be.visible");
cy.get(commonSelectors.labelEmailInput).verifyVisibleElement("have.text",commonText.labelEmailInput);
cy.get(commonSelectors.inputFieldEmailAddress).should("be.visible");
cy.get(commonSelectors.cancelButton).verifyVisibleElement("have.text",usersText.cancelButton);
cy.get(usersSelector.buttonInviteUsers).verifyVisibleElement(
"have.text",
usersText.buttonInviteUsers
);
cy.get(commonSelectors.cancelButton).click();
cy.get(usersSelector.addUsersCardTitle).should("not.exist")
cy.get(usersSelector.buttonAddUsers).click();
cy.get(commonSelectors.closeButton).click();
cy.get(usersSelector.addUsersCardTitle).should("not.exist")
cy.get(usersSelector.buttonAddUsers).click();
cy.get(usersSelector.addUsersCardTitle).verifyVisibleElement("have.text", usersText.addUsersCardTitle);
cy.get(usersSelector.buttonUploadCsvFile).click();
cy.get(usersSelector.helperTextBulkUpload).verifyVisibleElement("have.text", usersText.helperTextBulkUpload);
cy.get(usersSelector.buttonDownloadTemplate).verifyVisibleElement("have.text", usersText.buttonDownloadTemplate);
cy.get(usersSelector.iconBulkUpload).should("be.visible")
cy.get(usersSelector.helperTextSelectFile).verifyVisibleElement("have.text", usersText.helperTextSelectFile);
cy.get(usersSelector.helperTextDropFile).verifyVisibleElement("have.text", usersText.helperTextDropFile);
cy.get(usersSelector.inputFieldBulkUpload).should("exist")
cy.get(usersSelector.buttonUploadUsers).verifyVisibleElement("have.text", usersText.buttonUploadUsers);
cy.get(usersSelector.inviteBulkUserButton).verifyVisibleElement("have.text",usersText.inviteBulkUserButton).click();
cy.get(usersSelector.bulkUserUploadPageTitle).verifyVisibleElement("have.text",usersText.bulkUserUploadPageTitle);
cy.get(usersSelector.bulkUSerUploadInput).should("be.visible");
cy.get(usersSelector.downloadTemplateButton).verifyVisibleElement("have.text",usersText.downloadTemplateButton);
cy.get(usersSelector.cancelButton).verifyVisibleElement("have.text",usersText.cancelButton);
};
export const inviteUser = (firstName, lastName, email) => {
cy.get(usersSelector.inviteUserButton).click();
cy.clearAndType(usersSelector.firstNameInput, firstName);
cy.clearAndType(usersSelector.lastNameInput, lastName);
cy.clearAndType(usersSelector.emailInput, email);
export const inviteUser = (firstName, email) => {
cy.get(usersSelector.buttonAddUsers).click();
cy.clearAndType(commonSelectors.inputFieldFullName, firstName);
cy.clearAndType(commonSelectors.inputFieldEmailAddress, email);
cy.get(usersSelector.createUserButton).click();
cy.get(usersSelector.buttonInviteUsers).click();
cy.verifyToastMessage(
commonSelectors.toastMessage,
usersText.userCreatedToast
@ -91,7 +110,7 @@ export const inviteUser = (firstName, lastName, email) => {
cy.contains("td", email)
.parent()
.within(() => {
cy.get("td img").click();
cy.get(usersSelector.copyInvitationLink).click();
});
cy.verifyToastMessage(
commonSelectors.toastMessage,
@ -100,15 +119,13 @@ export const inviteUser = (firstName, lastName, email) => {
cy.get("@copyToClipboardPrompt").then((prompt) => {
common.logout();
cy.visit(prompt.args[0][1]);
cy.url().should("include", path.confirmInvite);
});
};
export const addNewUser = (firstName, lastName, email) => {
export const addNewUser = (firstName, email) => {
cy.intercept("POST", "/api/organization_users").as("appLibrary");
cy.clearAndType(usersSelector.firstNameInput, firstName);
cy.clearAndType(usersSelector.lastNameInput, lastName);
cy.clearAndType(usersSelector.emailInput, email);
cy.clearAndType(commonSelectors.inputFieldFullName, firstName);
cy.clearAndType(commonSelectors.inputFieldEmailAddress, email);
cy.get(usersSelector.createUserButton).click();
cy.wait("@appLibrary").then((res) => {
@ -121,7 +138,6 @@ export const addNewUser = (firstName, lastName, email) => {
};
export const confirmInviteElements = () => {
cy.url().should("include", '/confirm');
cy.get(commonSelectors.invitePageHeader).verifyVisibleElement(
"have.text",
commonText.invitePageHeader

View file

@ -0,0 +1,58 @@
import { commonWidgetSelector } from "Selectors/common";
import {
verifyAndModifyStylePickerFx,
selectColourFromColourPicker,
verifyWidgetColorCss,
} from "Support/utils/commonWidget";
export const launchModal = (componentName) => {
cy.get(launchButton(componentName)).click();
};
export const launchButton = (componentName) => {
return `[data-cy="draggable-widget-${componentName
.toLowerCase()
.replace(" ", "-")}-launch-button"]`;
};
export const verifySize = (size) => {
const className = {
Small: "sm",
Medium: "lg",
Large: "xl",
};
cy.get('[data-cy="dropdown-modal-size"]')
.click()
.find("input")
.type(`{selectAll}{backspace}${size}{enter}`);
cy.get(
`[class="modal-dialog modal-${className[size]} modal-dialog-scrollable"]`
).should("exist");
};
export const closeModal = (componentName) => {
cy.get('[data-cy="modal-close-button"]').realClick();
};
export const addAndVerifyColor = (
section,
defaultColor,
color,
dataCy,
type = "background-color"
) => {
verifyAndModifyStylePickerFx(section, defaultColor, "data.colourHex");
cy.get(commonWidgetSelector.parameterFxButton(section)).click();
selectColourFromColourPicker(section, color);
verifyWidgetColorCss(dataCy, type, color, true);
cy.get(dataCy).realClick();
cy.get(commonWidgetSelector.buttonStylesEditorSideBar).click();
};
export const typeOnFx = (fx, data) => {
cy.get(commonWidgetSelector.parameterFxButton(fx)).eq(1).realClick();
cy.get(commonWidgetSelector.parameterInputField(fx)).clearAndTypeOnCodeMirror(
data
);
};

View file

@ -9,7 +9,8 @@ import { verifyComponent } from "Support/utils/basicComponents";
export const navigateToCreateNewVersionModal = (value) => {
cy.get(appVersionSelectors.currentVersionField(value)).should("be.visible").click();
cy.contains(appVersionText.createNewVersion).should("be.visible").click();
cy.contains(appVersionText.createNewVersion).should("be.visible");
cy.contains(appVersionText.createNewVersion).click();
}
export const navigateToEditVersionModal = (value) => {
@ -41,6 +42,12 @@ export const verifyElementsOfCreateNewVersionModal = (version = []) => {
export const editVersionAndVerify = (currentVersion, newVersion = [], toastMessageText) => {
cy.reload();
cy.get(appVersionSelectors.currentVersionField(currentVersion)).then(($ele) => {
if ($ele.hasClass("color-light-green")) {
cy.contains(releasedVersionText.releasedModalText).should("be.visible");
closeModal(commonText.closeButton);
}
})
navigateToEditVersionModal(currentVersion)
cy.get(editVersionSelectors.versionNameInputField).verifyVisibleElement("have.value", currentVersion);
@ -49,6 +56,7 @@ export const editVersionAndVerify = (currentVersion, newVersion = [], toastMessa
newVersion[0]
);
cy.get(editVersionSelectors.saveButton).click();
cy.wait(500);
cy.verifyToastMessage(
commonSelectors.toastMessage,
toastMessageText
@ -100,6 +108,7 @@ export const releasedVersionAndVerify = (currentVersion) => {
appVersionSelectors.versionNameInputField
);
cy.contains(releasedVersionText.releasedModalText).should("be.visible");
cy.wait(500);
closeModal(commonText.closeButton);
cy.get(appVersionSelectors.currentVersionField(currentVersion)).should("have.class", "color-light-green");
};

View file

@ -17,6 +17,7 @@ The user details entered while setting up ToolJet will have Super Admin privileg
| Manage Groups in their workspace (Create Group/Add or Delete Users from groups/ Modify Group Permissions) | ✅ | ✅ |
| Manage SSO in their workspace | ✅ | ✅ |
| Manage Workspace Variables in their workspace | ✅ | ✅ |
| [Manage Global datasources for the user group in their workspace](/docs/next/data-sources/overview#permissions) | ✅ | ✅ |
| [Access any user's personal workspace (create, edit or delete apps)](#access-any-workspace) | ❌ | ✅ |
| [Archive Admin or any user of any workspace](#archiveunarchive-users) | ❌ | ✅ |
| [Access any user's ToolJet database (create, edit or delete database)](#access-tooljet-db-in-any-workspace) | ❌ | ✅ |

View file

@ -3,9 +3,13 @@ id: overview
title: Overview
---
# Datasources : Overview
# Global Datasources : Overview
Datasources pull in and push data to any source including databases, external APIs, or services.
Global datasources pull in and push data to any source including databases, external APIs, or services. Once a global datasource is connected to a workspace, the connection can be shared with any app of that workspace.
:::caution
Global datasources are available only on **ToolJet version 2.3.0 and above**.
:::
<div style={{textAlign: 'center'}}>
@ -13,30 +17,115 @@ Datasources pull in and push data to any source including databases, external AP
</div>
## Connecting datasources
## Connecting global datasources
1. After logging in to ToolJet, create a new app from the dashboard
1. From the ToolJet dashboard, go to the **global datasources page** from the left sidebar.
<div style={{textAlign: 'center'}}>
2. There are two ways for connecting a datasource. You can connect from:
1. **Left-sidebar**: On the left sidebar, click on the `datasource` icon and then click on the `+ add datasource` button
<img className="screenshot-full" src="/img/datasource-reference/overview/global.png" alt="Datasources: Overview" />
<div style={{textAlign: 'center'}}>
</div>
<img className="screenshot-full" src="/img/datasource-reference/overview/ls2.png" alt="Datasources: Overview" width="400"/>
2. Click on the **Add new datasource** button, a modal will pop-up with all the available global datasources.
<div style={{textAlign: 'center'}}>
</div>
<img className="screenshot-full" src="/img/datasource-reference/overview/popup.png" alt="Datasources: Overview" />
2. **Query Panel**: Go to the query panel at the bottom, click on the `+Add` button and then click `Add datasource` button
</div>
<div style={{textAlign: 'center'}}>
3. Select the datasource, enter the **Credentials** and **Save** the datasource.
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/datasource-reference/overview/qp2.png" alt="Datasources: Overview"/>
<img className="screenshot-full" src="/img/datasource-reference/overview/connection.png" alt="Datasources: Overview" />
</div>
</div>
3. Follow the steps in the **[Datasource Library](/docs/data-sources/airtable)** specific to the desired datasource
4. Now, go back to the dashboard, create a new app, and the datasource will be available on the query panel under **Global Datasources**. Added datasources will be available on any of the **existing** or the **new applications**.
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/datasource-reference/overview/globalquery.png" alt="Datasources: Overview" />
</div>
5. You can now create queries of the connected global datasource. From the queries, you'll be able to switch to **different connections** of the same datasource if there are more than one connections created.
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/datasource-reference/overview/switch.png" alt="Datasources: Overview" />
</div>
## Changing scope of datasources of an app created on older versions of ToolJet
On ToolJet versions below 2.3.0, the datasource connection was made from within the individual apps. To make it backward compatible, we added an option to change the scope of the datasources and make it global datasource.
1. If you open an app created on previos versions of ToolJet, you'll find the datasource manager on the left sidebar of the App Builder.
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/datasource-reference/overview/leftsidebar.png" alt="Datasources: Overview" />
</div>
2. Click on the kebab menu next to the connected datasource, select the **change scope** option.
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/datasource-reference/overview/changescope.png" alt="Datasources: Overview" />
</div>
3. Once you change the scope of the datasource and make it global, you'll see that the **datasource manager** is removed from the left sidebar and now you'll find the datasource on the **query panel** under Global Datasources. You can now configure the datasource fromt the Global Datasource page on the **dashboard**.
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/datasource-reference/overview/queryadd.png" alt="Datasources: Overview" />
</div>
## Default datasources
By default, 4 datasources will be available on every app on ToolJet:
- **[ToolJet Database](/docs/tooljet-database/)**
- **[RestAPI](/docs/data-sources/restapi/)**
- **[Run JavaScript Query](/docs/data-sources/run-js/)**
- **[Run Python Query](/docs/data-sources/run-py/)**
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/datasource-reference/overview/default.png" alt="Datasources: Overview" />
</div>
## Permissions
Only **Admins** and **[Super Admins](/docs/Enterprise/superadmin)** of the workspace can change the **[Permissions](/docs/tutorial/manage-users-groups#group-properties)** for Global Datasource.
From **Workspace Settings** -> **Groups Settings**, Admins and Super Admins can set the permission for a user group to:
- **Create** and **Delete** datasources onto that workspace. If **Create** permission is enabled then the users can add new global datasources and **edit** the datasources as well but cannot **delete** it, and if only **Delete** permission is set then the users of the group will only be able to delete the connected datasources on the workspace.
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/datasource-reference/overview/create.png" alt="Datasources: Overview" />
</div>
- If any of the permission(Create or Delete) is not enabled for a user group then the users of the group will get an error toast when they try to Add or Delete the global datasource.
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/datasource-reference/overview/error.png" alt="Datasources: Overview" />
</div>
- **View** or **Edit** allowed global datasources from the **Datasources** tab. If only **View** permission is set then the users of the group will only be able to connect to the allowed datasource, and if only **Edit** permission is set then the users of the group will be able to update the credentials of the allowed datasources.
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/datasource-reference/overview/view.png" alt="Datasources: Overview" />
</div>
- If any of the permission(View or Edit) is not enabled for a user group then the users of the group will get an error toast when they try to Add or Delete the global datasource.
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/datasource-reference/overview/edit.png" alt="Datasources: Overview" />
</div>
:::info
ToolJet allows you to transform the data returned by datasources using **[Transformations](/docs/tutorial/transformations)**
:::

View file

@ -5,18 +5,24 @@ title: SMTP
# SMTP
SMTP plugin can connect ToolJet applications to **SMTP servers** for sending emails.
The SMTP datasource facilitates the connection between ToolJet applications and email servers, enabling the apps to send emails.
## Connection
A SMTP server can be connected with the following credentails:
- **Host**
- **Port**
- **User**
To connect to an SMTP server, the following credentials are typically required:
- **Host**
- **Port**
- **Username**
- **Password**
:::info
You can also test your connection before saving the configuration by clicking on `Test Connection` button.
:::tip Finding configuration details:
The SMTP configuration details like host and port can usually be obtained from your email service provider. Here are some general settings for the most commonly used email providers:
- **Gmail**: `Host`: smtp.gmail.com; `Port`: 587 or 465 (SSL); `Username`: your full Gmail email address; `Password`: your Gmail password.
- **Yahoo Mail**: `Host`: smtp.mail.yahoo.com; `Port`: 465 (SSL); `Username`: your Yahoo Mail email address; `Password`: your Yahoo Mail password.
- **Outlook.com/Hotmail**: `Host`: smtp.office365.com; `Port`: 587 or 465 (SSL); `Username`: your Outlook.com/Hotmail email address; `Password`: your Outlook.com/Hotmail password.
Before saving the configuration, it's possible to test the connection by clicking the "Test Connection" button.
:::
<div style={{textAlign: 'center'}}>
@ -27,22 +33,25 @@ You can also test your connection before saving the configuration by clicking on
## Querying SMTP
Go to the query manager at the bottom panel of the editor and click on the `+` button on the left to create a new query. Select `SMTP` from the datasource dropdown.
To create a query for sending an email, follow these steps:
To create a query for sending email, you will need to provide the following properties:
- **From** `required` : Email address of the sender
- **From Name** : Name of the sender
- **To** `required` : Recipient's email address
- **Subject** : Subject of the email
1. Open the query panel located at the bottom panel of the editor.
2. Click the `+Add` button on the left to create a new query.
3. Select `SMTP` from the global datasource.
4. Provide the following properties:
- **From** `required` : Email address of the sender
- **From Name** : Name of the sender
- **To** `required` : Recipient's email address
- **CC mail to** : Email address of the recipients that will receive a copy of the email, and their email addresses will be visible to other recipients.
- **BCC mail to** : Email address of the recipients that will receive a copy of the email but the email addressed will be hidden to other recipients.
- **Subject** : Subject of the email.
- **Body** : You can enter the body text of the email in either raw text or html format, in their respective fields.
- **Attachments** : You can add attachments to an SMTP query by referencing the file from the File Picker component in the attachments field.
For instance, you can set the `Attachments` field value to `{{ components.filepicker1.file }}` or pass an array of `{{ name: 'filename.jpg', dataURL: '......' }}` objects to include attachments.
<img className="screenshot-full" src="/img/datasource-reference/smtp/query1.png" alt="smtp query1" />
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/datasource-reference/smtp/querysmtp.png" alt="smtp connect" />
- **Body** : You can enter the body text either in the form of `raw text` or `html` in their respective fields.
- **Attachments** : Attachments can be added to a SMTP query by referencing the file from the `File Picker` component in the attachments field.
For example, you can set the `Attachments` field value to `{{ components.filepicker1.file }}` or you can pass an array of `{{ name: 'filename.jpg', dataURL: '......' }}` object to accomplish this.
<img className="screenshot-full" src="/img/datasource-reference/smtp/query2.png" alt="smtp query2" />
</div>

View file

@ -143,7 +143,7 @@ Learn more about the **[ToolJet Database here](/docs/tooljet-database)**
</div>
:::info
ToolJet application's User interface is constructed using Components like Tables, Forms, Charts, or Buttons etc. Check **Components Catalog** to learn more.
ToolJet application's User interface is constructed using Components like Tables, Forms, Charts, or Buttons etc. Check **[Components Catalog](/docs/widgets/overview)** to learn more.
:::
### Build queries and bind data to UI
@ -156,7 +156,7 @@ ToolJet application's User interface is constructed using Components like Tables
</div>
:::info
ToolJet can connect to several databases, APIs and external services to fetch and modify data. Check **Datasource Catalog** to learn more.
ToolJet can connect to several databases, APIs and external services to fetch and modify data. Check **[Datasource Catalog](/docs/data-sources/overview)** to learn more.
:::
2. Choose a **Table** from the dropdown, Select the **List rows** option from the **Operation** dropdown, You can leave other query parameters. Scroll down and enable **Run this query on application load** - this will trigger the query when the app is loaded.
@ -199,8 +199,8 @@ ToolJet application's User interface is constructed using Components like Tables
</div>
:::info
- You can manipulate the data returned by the queries using **Transformations**
- You can also **Run JS query** or **Python query** to perform custom behavior inside ToolJet
- You can manipulate the data returned by the queries using **[Transformations](/docs/tutorial/transformations)**
- You can also **[Run JavaScript code](/docs/data-sources/run-js)** or **[Run Python code](/docs/data-sources/run-py)** to perform custom behavior inside ToolJet
:::
### Preview, Release and Share app
@ -210,7 +210,7 @@ ToolJet application's User interface is constructed using Components like Tables
3. **Share** option allows you to share the **released version** of the application with other users or you can also make the app **public** and anyone with the URL will be able to use the app.
:::tip
You can control how much access to users have to your ToolJet apps and resources using **Org Management**.
You can control how much access to users have to your ToolJet apps and resources using **[Org Management](/docs/tutorial/manage-users-groups)**.
:::
## What Can I Do With ToolJet

View file

@ -0,0 +1,23 @@
---
id: intentionally-fail-js-query
title: Intentionally fail a RunJS query
---
In this how-to guide, we will create a RunJS query that will throw an error.
- Create a RunJS query and paste the code below. We will use the constructor `ReferenceError` since it is used to create a range error instance.
```js
throw new ReferenceError('This is a reference error.');
```
- Now, add a event handler to show an alert when the query fails. **Save** the query and **Run** it.
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/how-to/failjs/failjs.gif" alt="Intentionally fail a RunJS query" />
</div>
:::info
Most common use-case for intentionally failing a query is **debugging**.
:::

View file

@ -0,0 +1,173 @@
---
id: use-s3-signed-url-to-upload-docs
title: Use S3 signed URL to upload documents
---
# Use S3 signed URL to upload documents
In this how-to guide, you'll learn to upload documents to S3 buckets using the **S3 signed URL** from a ToolJet application.
For this guide, We are going to use one of the existing templates on ToolJet: **S3 File explorer**
:::info using Templates
On ToolJet Dashboard, Click on the down arrow on the right of the **New App** button, from the dropdown choose the **Choose from template** option.
:::
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/how-to/uses3presignedurl/template.png" alt="Use S3 pre-signed URL to upload documents: Choose template" width="700"/>
</div>
- Once you've created a new app using the template, you'll be prompted to create a **new version** of the existing version. After creating a new version, you'll be able to make changes in the app.
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/how-to/uses3presignedurl/newversion.png" alt="Use S3 pre-signed URL to upload documents: new version"/>
</div>
- Go to the **datasource manager** on the left-sidebar, you'll find that the **AWS S3 datasource** is already added. All you need to do is update the datasource **credentials**.
:::tip
Check the [AWS S3 datasource reference](/docs/data-sources/s3) to learn more about connnection and choosing your preferred authentication method.
:::
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/how-to/uses3presignedurl/s3connect.png" alt="Use S3 pre-signed URL to upload documents: add datasource"/>
</div>
- Once the datasource is connected successfully, go to the query manager and **Run** the **getBuckets** query. The operation selected in the getBuckets query is **List Buckets** which will fetch an array of all the buckets.
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/how-to/uses3presignedurl/getbuckets.png" alt="Use S3 pre-signed URL to upload documents: getBuckets query"/>
</div>
- Running the **getBuckets** query will load all the buckets in the dropdown in the app.
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/how-to/uses3presignedurl/dropdown.png" alt="Use S3 pre-signed URL to upload documents: loading buckets"/>
</div>
- Select a **bucket** from the dropdown and click on the **Fetch files** button to list all the files from the selected bucket on the table. The **Fetch files** button has the event handler added that triggers the **s32** query, the **s32** query uses the **List objects in a bucket** operation, and the bucket field in the query gets the value dynamically from the dropdown.
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/how-to/uses3presignedurl/fetchfiles.png" alt="Use S3 pre-signed URL to upload documents: list objects in a bucket"/>
</div>
- Let's go to the **uploadToS3** query and update the field values:
- **Operation**: Signed URL for upload
- **Bucket**: `{{components.dropdown1.value}}` this will fetch the dynamic value from the dropdown
- **Key**: `{{components.filepicker1.file[0].name}}` this will get the file name from the filepickers exposed variables
- **Expires in:** This sets an expiration time of URL, by default its `3600` seconds (1 hour)
- **Content Type**: `{{components.filepicker1.file[0].type}}` this will get the file type from the filepickers exposed variables
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/how-to/uses3presignedurl/upload.png" alt="Use S3 pre-signed URL to upload documents"/>
</div>
- Create two **RunJS** queries:
- Create a **runjs1** query and copy-paste the code below. This query gets the **base64data** from the file picker and convert the file's `base64Data` to into `BLOB`, and returns the file object.
```js
const base64String = components.filepicker1.file[0].base64Data
const decodedArray = new Uint8Array(atob(base64String).split('').map(c => c.charCodeAt(0)));
const file = new Blob([decodedArray], { type: components.filepicker1.file[0].type });
const fileName = components.filepicker1.file[0].name;
const fileObj = new File([file], fileName);
return fileObj
```
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/how-to/uses3presignedurl/runjs1.png" alt="Use S3 pre-signed URL to upload documents"/>
</div>
- Create another **runjs2** query and copy-paste the code below. This query gets the data(file object) returned by the first runjs query, the url returned by the **uploadToS3** query, and then makes PUT request.
```js
const file = queries.runjs2.data
const url = queries.s31.data.url
fetch(url, {
method: 'PUT',
body: file,
mode: 'cors',
headers: {
'Access-Control-Allow-Origin': '*',
'Content-Type': 'application/json'
}
})
.then(response => console.log('Upload successful!'))
.catch(error => console.error('Error uploading file:', error));
```
:::warning Enable Cross Origin Resource Sharing(CORS)
- For the file to be uploaded successfully, you will need to add the CORS policies from the **Permissions** tab of your **Bucket** settings. Here's a sample CORS:
```json
[
{
"AllowedHeaders": [
"*"
],
"AllowedMethods": [
"GET",
"PUT",
"POST"
],
"AllowedOrigins": [
"*"
],
"ExposeHeaders": []
}
]
```
:::
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/how-to/uses3presignedurl/runjs2.png" alt="Use S3 pre-signed URL to upload documents"/>
</div>
- Go to the **uploadToS3**, scroll down and add an event handler to the **uploadToS3** query. Select the **Query Success** event, **Run Query** as the action, and **runjs1** as the query to be triggered. **Save** the query.
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/how-to/uses3presignedurl/eventhandlerupload.png" alt="Use S3 pre-signed URL to upload documents"/>
</div>
- Let's go to the **runjs1** query and add the event handler to run a query on query success event, similar to how we did in the previous step. In the event handler, choose **runjs2** query. **Save** the query.
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/how-to/uses3presignedurl/eventhandlerrunjs2.png" alt="Use S3 pre-signed URL to upload documents"/>
</div>
- Now, let's go the final query **copySignedURL** that is connected to the table's action button. This query copy's the generated **Signed URL for download** onto the **clipboard**.
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/how-to/uses3presignedurl/copysigned.png" alt="Use S3 pre-signed URL to upload documents"/>
</div>
- Now that we have updated all the queries, and connected them through the event handlers. We can go ahead and pick a file from the file picker. Click on the file picker, select a file and then hit the **Upload file to S3** button.
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/how-to/uses3presignedurl/uploadbutton.png" alt="Use S3 pre-signed URL to upload documents"/>
</div>
- Once the button is clicked, the **uploadToS3** will triggered along with the **runjs1** and **runjs2** queries in sequence since we added them in the event handlers.
- You can go to the table and click on the **Copy signed URL** action button on the table, this will trigger the **copySignedURL** query and will copy the URL on the clipboard. You can go to another tab and paste the URL to open the file on the browser.

View file

@ -8,7 +8,7 @@ Permissions allow you to create and share resources to easily ensure what level
Admins can invite **Users** to their workspaces and assign them to the **Groups** that have Permissions to access Apps, folders, or workspace variables.
:::info
See **[Manage Users and Groups](/docs/tutorial/manage-users-groups)** to learn how to invite users to ToolJet.
See **[Manage Users and Groups](/docs/tutorial/manage-users-groups)** to know more about managing users and groups on your workspace.
:::
## Role-Based Access Control (RBAC) Glossary
@ -18,4 +18,4 @@ See **[Manage Users and Groups](/docs/tutorial/manage-users-groups)** to learn h
- **All Users** - Contains all the users in your workspace. When **New Users** are invited they are added to this group by default.
- **Admins** - Contains all Admins in your workspace. Everyone added to this group will Permission to access all the ToolJet resources.
- **Apps, Folder, Workspace Variables -** Resources that Admins can set permissions on.
- **Permissions -** Create, Update and Delete.
- **Permissions -** Create, Update and Delete.

View file

@ -77,7 +77,7 @@ Similar to archiving a user's access, you can enable it again by clicking on **U
## Managing Groups
On ToolJet, Admins can create groups for users added in a workspace and grant them access to particular app(s) with specific permissions. To manage groups, just go to the **Workspace Settings** from the left-sidebar of the dashboard and click on the **Groups**.
On ToolJet, Admins and Super Admins can create groups for users added in a workspace and grant them access to particular app(s) with specific permissions. To manage groups, just go to the **Workspace Settings** from the left-sidebar of the dashboard and click on the **Groups**.
<div style={{textAlign: 'center'}}>
@ -87,11 +87,16 @@ On ToolJet, Admins can create groups for users added in a workspace and grant th
### Group properties
Every group on ToolJet has three sections:
Every group on ToolJet has **four** sections:
- [Apps](#apps)
- [Users](#users)
- [Permissions](#permissions)
- [Datasources](#datasources)
#### Apps:
Admins can add or remove any number of apps for a group of users. To add an app to a group, select an app from the dropdown and click on `Add` button next to it. You can also set app permissions such as `View` or `Edit` for the group. You can set different permissions for different apps in a group.
Admins and Super Admins can add or remove any number of apps for a group of users. To add an app to a group, select an app from the dropdown and click on `Add` button next to it. You can also set app permissions such as `View` or `Edit` for the group. You can set different permissions for different apps in a group.
<div style={{textAlign: 'center'}}>
@ -101,7 +106,7 @@ Admins can add or remove any number of apps for a group of users. To add an app
#### Users:
Admins can add or remove any numbers of users in a group. Just select a user from the dropdown and click on `Add` button to add it to a group. To delete a user from a group, click on `Delete` button next to it.
Admins and Super Admins can add or remove any numbers of users in a group. Just select a user from the dropdown and click on `Add` button to add it to a group. To delete a user from a group, click on `Delete` button next to it.
<div style={{textAlign: 'center'}}>
@ -111,16 +116,30 @@ Admins can add or remove any numbers of users in a group. Just select a user fro
#### Permissions:
Admins can set granular permission like creating/deleting apps or creating folder for a group of users.
Admins and Super Admins can set granular permission for the users added in that particular group, such as:
- **Create** and **Delete** Apps
- **Create**, **Update**, and **Delete** Folders
- **Create**, **Update**, and **Delete** [Workspace Variables](/docs/tutorial/workspace-variables)
- **Create** and **Delete** [Global Datasources](/docs/widgets/overview)
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/tutorial/manage-users-groups/permissionsv2.png" alt="permissions" />
<img className="screenshot-full" src="/img/tutorial/manage-users-groups/dspermission.png" alt="permissions" />
</div>
#### Datasources:
Only Admins and Super Admins can define what datasources can be **viewed** or **edited** by the users of that group.
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/tutorial/manage-users-groups/gdspermission.png" alt="permissions" />
</div>
:::tip
All the activities performed by any Admin or any user in a workspace is logged in `Audit logs` - including any activity related with managing users and groups.
All the activities performed by any Admin, Super Admin or any user in a workspace is logged in [Audit logs](/docs/Enterprise/audit_logs) - including any activity related with managing users and groups.
:::
### Predefined Groups

View file

@ -8,11 +8,64 @@ Tables can be used for both displaying and editing data.
<iframe height="500" src="https://www.youtube.com/embed/hTrdkUtz3aA" title="ToolJet Table Widget" frameborder="0" allowfullscreen width="100%"></iframe>
## Table UI
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/widgets/table/UI.png" alt="ToolJet - Widget Reference - Table" width="900" />
</div>
### Search
At the top-left corner of the table component, there is a search box that allows users to input keywords and search for rows within the table data. You can also **[show/hide the search box](/docs/widgets/table#show-search-box)** from the table from the table properties.
### Add new row
When users click on this button, a popup modal appears which enables them to insert new rows. The modal will have a single row initially, and the columns will have the same column type as those on the table. If the user inputs data into the row, it will be stored on the **[`newRows` variable](/docs/widgets/table#exposed-variables)** of the table. If the user selects the **Discard** button, the data in the variable will be cleared. However, if the user closes the popup without taking any action (neither Save nor Discard), the data will still be retained, and a green indicator will appear on the **Add new row** button. The table has an **[Add new rows event handler](/docs//widgets/table#add-new-rows)** that can be utilized to execute queries that store the data into the datasource whenever the **Save** button is clicked.
:::info
At present, it is not possible to include columns of type Image when adding a new row to the table.
:::
### Filters
The table data can be filtered by clicking on this button. You have the option to choose from various filters, such as:
- **contains**
- **does not contain**
- **matches**
- **does not match**
- **equals**
- **does not equal**
- **is empty**
- **is not empty**
- **greater than**
- **greater than or equal to**
- **less than**
- **less than or equal to**
You have the option to **[hide the filter button](/docs/widgets/table#show-filter-button)** in the table properties.
### Download
The table data can be downloaded in various file formats, including:
- **CSV**
- **Excel**
- **PDF**
You have the option to **[hide the download button](/docs/widgets/table#show-download-button)** in the table properties.
### Column selector button
You can choose which columns to display or hide in the table by clicking on this button. You also have the option to **[hide the column selector button](/docs/widgets/table#show-column-selector-button)** in the table properties.
## Table data
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/widgets/table/data.png" alt="ToolJet - Widget Reference - Table" width="400" />
<img className="screenshot-full" src="/img/widgets/table/data.png" alt="ToolJet - Widget Reference - Table" width="350" />
</div>
@ -197,28 +250,15 @@ If server-side search is enabled, `on search` event is fired after the content o
### Show download button
Show or hide download button at the Table footer.
The download button in the table header is visible by default. You can choose to hide it by disabling this option. You can dynamically set the value to {{true}} or {{false}} to show or hide the download button by clicking on the **Fx** button.
### Hide/Show columns
### Show column selector button
Table header has an option(Eye icon) to show/hide one or many columns on the table.
The column selector button on the table header is visible by default. You can choose to hide it by disabling this option. You can dynamically set the value to {{true}} or {{false}} to show or hide the column selector button by clicking on the **Fx** button.
### Show filter button
Show or hide filter button at the Table header. The following filters are available:
- **contains**
- **does not contain**
- **matches**
- **does not match**
- **equals**
- **does not equal to**
- **is empty**
- **is not empty**
- **greater than**
- **greater than or equal to**
- **less than**
- **less than or equal to**
The filter button in the table header is visible by default. You can choose to hide it by disabling this option. You can dynamically set the value to {{true}} or {{false}} to show or hide the filter button by clicking on the **Fx** button.
### Show update buttons
@ -263,6 +303,7 @@ Loading state shows a loading skeleton for the table. This property can be used
- **[Sort applied](#sort-applied)**
- **[Cell value changed](#cell-value-changed)**
- **[Filter changed](#filter-changed)**
- **[Add new rows](#add-new-rows)**
### Row hovered
@ -300,6 +341,10 @@ If any cell of the table is edited, the `cell value changed` event is triggered.
This event is triggered when filter is added, removed, or updated from the filter section of the table. `filters` property of the table is updated to reflect the status of filters applied. The objects will have properties: `condition`, `value`, and `column`.
### Add new rows
This event is triggered when the **Save** button is clicked from the **Add new row** modal on the table.
## Exposed variables
| variable | description |
@ -311,6 +356,7 @@ This event is triggered when filter is added, removed, or updated from the filte
| dataUpdates | Just like changeSet but includes the data of the entire row |
| selectedRow | The data of the row that was last clicked. `selectedRow` also changes when an action button is clicked |
| searchText | The value of the search field if server-side pagination is enabled |
| newRows| The newRows variable stores an array of objects, each containing data for a row that was added to the table using the "Add new row" button. When the user clicks either the "Save" or "Discard" button in the modal, this data is cleared.|
## Styles

View file

@ -303,8 +303,10 @@ const sidebars = {
'how-to/import-external-libraries-using-runpy',
'how-to/import-external-libraries-using-runjs',
'how-to/run-actions-from-runjs',
'how-to/intentionally-fail-js-query',
'how-to/run-query-at-specified-intervals',
'how-to/access-users-location',
'how-to/use-s3-signed-url-to-upload-docs',
'how-to/s3-custom-endpoints',
'how-to/oauth2-authorization',
'how-to/upload-files-aws',

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 126 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

BIN
docs/static/img/how-to/failjs/failjs.gif vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 796 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 155 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 162 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 183 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 134 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 136 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 272 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

BIN
docs/static/img/widgets/table/ui.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 258 KiB

View file

@ -0,0 +1,23 @@
---
id: intentionally-fail-js-query
title: Intentionally fail a RunJS query
---
In this how-to guide, we will create a RunJS query that will throw an error.
- Create a RunJS query and paste the code below. We will use the constructor `ReferenceError` since it is used to create a range error instance.
```js
throw new ReferenceError('This is a reference error.');
```
- Now, add a event handler to show an alert when the query fails. **Save** the query and **Run** it.
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/how-to/failjs/failjs.gif" alt="Intentionally fail a RunJS query" />
</div>
:::info
Most common use-case for intentionally failing a query is **debugging**.
:::

View file

@ -0,0 +1,173 @@
---
id: use-s3-signed-url-to-upload-docs
title: Use S3 signed URL to upload documents
---
# Use S3 signed URL to upload documents
In this how-to guide, you'll learn to upload documents to S3 buckets using the **S3 signed URL** from a ToolJet application.
For this guide, We are going to use one of the existing templates on ToolJet: **S3 File explorer**
:::info using Templates
On ToolJet Dashboard, Click on the down arrow on the right of the **New App** button, from the dropdown choose the **Choose from template** option.
:::
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/how-to/uses3presignedurl/template.png" alt="Use S3 pre-signed URL to upload documents: Choose template" width="700"/>
</div>
- Once you've created a new app using the template, you'll be prompted to create a **new version** of the existing version. After creating a new version, you'll be able to make changes in the app.
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/how-to/uses3presignedurl/newversion.png" alt="Use S3 pre-signed URL to upload documents: new version"/>
</div>
- Go to the **datasource manager** on the left-sidebar, you'll find that the **AWS S3 datasource** is already added. All you need to do is update the datasource **credentials**.
:::tip
Check the [AWS S3 datasource reference](/docs/data-sources/s3) to learn more about connnection and choosing your preferred authentication method.
:::
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/how-to/uses3presignedurl/s3connect.png" alt="Use S3 pre-signed URL to upload documents: add datasource"/>
</div>
- Once the datasource is connected successfully, go to the query manager and **Run** the **getBuckets** query. The operation selected in the getBuckets query is **List Buckets** which will fetch an array of all the buckets.
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/how-to/uses3presignedurl/getbuckets.png" alt="Use S3 pre-signed URL to upload documents: getBuckets query"/>
</div>
- Running the **getBuckets** query will load all the buckets in the dropdown in the app.
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/how-to/uses3presignedurl/dropdown.png" alt="Use S3 pre-signed URL to upload documents: loading buckets"/>
</div>
- Select a **bucket** from the dropdown and click on the **Fetch files** button to list all the files from the selected bucket on the table. The **Fetch files** button has the event handler added that triggers the **s32** query, the **s32** query uses the **List objects in a bucket** operation, and the bucket field in the query gets the value dynamically from the dropdown.
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/how-to/uses3presignedurl/fetchfiles.png" alt="Use S3 pre-signed URL to upload documents: list objects in a bucket"/>
</div>
- Let's go to the **uploadToS3** query and update the field values:
- **Operation**: Signed URL for upload
- **Bucket**: `{{components.dropdown1.value}}` this will fetch the dynamic value from the dropdown
- **Key**: `{{components.filepicker1.file[0].name}}` this will get the file name from the filepickers exposed variables
- **Expires in:** This sets an expiration time of URL, by default its `3600` seconds (1 hour)
- **Content Type**: `{{components.filepicker1.file[0].type}}` this will get the file type from the filepickers exposed variables
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/how-to/uses3presignedurl/upload.png" alt="Use S3 pre-signed URL to upload documents"/>
</div>
- Create two **RunJS** queries:
- Create a **runjs1** query and copy-paste the code below. This query gets the **base64data** from the file picker and convert the file's `base64Data` to into `BLOB`, and returns the file object.
```js
const base64String = components.filepicker1.file[0].base64Data
const decodedArray = new Uint8Array(atob(base64String).split('').map(c => c.charCodeAt(0)));
const file = new Blob([decodedArray], { type: components.filepicker1.file[0].type });
const fileName = components.filepicker1.file[0].name;
const fileObj = new File([file], fileName);
return fileObj
```
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/how-to/uses3presignedurl/runjs1.png" alt="Use S3 pre-signed URL to upload documents"/>
</div>
- Create another **runjs2** query and copy-paste the code below. This query gets the data(file object) returned by the first runjs query, the url returned by the **uploadToS3** query, and then makes PUT request.
```js
const file = queries.runjs2.data
const url = queries.s31.data.url
fetch(url, {
method: 'PUT',
body: file,
mode: 'cors',
headers: {
'Access-Control-Allow-Origin': '*',
'Content-Type': 'application/json'
}
})
.then(response => console.log('Upload successful!'))
.catch(error => console.error('Error uploading file:', error));
```
:::warning Enable Cross Origin Resource Sharing(CORS)
- For the file to be uploaded successfully, you will need to add the CORS policies from the **Permissions** tab of your **Bucket** settings. Here's a sample CORS:
```json
[
{
"AllowedHeaders": [
"*"
],
"AllowedMethods": [
"GET",
"PUT",
"POST"
],
"AllowedOrigins": [
"*"
],
"ExposeHeaders": []
}
]
```
:::
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/how-to/uses3presignedurl/runjs2.png" alt="Use S3 pre-signed URL to upload documents"/>
</div>
- Go to the **uploadToS3**, scroll down and add an event handler to the **uploadToS3** query. Select the **Query Success** event, **Run Query** as the action, and **runjs1** as the query to be triggered. **Save** the query.
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/how-to/uses3presignedurl/eventhandlerupload.png" alt="Use S3 pre-signed URL to upload documents"/>
</div>
- Let's go to the **runjs1** query and add the event handler to run a query on query success event, similar to how we did in the previous step. In the event handler, choose **runjs2** query. **Save** the query.
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/how-to/uses3presignedurl/eventhandlerrunjs2.png" alt="Use S3 pre-signed URL to upload documents"/>
</div>
- Now, let's go the final query **copySignedURL** that is connected to the table's action button. This query copy's the generated **Signed URL for download** onto the **clipboard**.
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/how-to/uses3presignedurl/copysigned.png" alt="Use S3 pre-signed URL to upload documents"/>
</div>
- Now that we have updated all the queries, and connected them through the event handlers. We can go ahead and pick a file from the file picker. Click on the file picker, select a file and then hit the **Upload file to S3** button.
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/how-to/uses3presignedurl/uploadbutton.png" alt="Use S3 pre-signed URL to upload documents"/>
</div>
- Once the button is clicked, the **uploadToS3** will triggered along with the **runjs1** and **runjs2** queries in sequence since we added them in the event handlers.
- You can go to the table and click on the **Copy signed URL** action button on the table, this will trigger the **copySignedURL** query and will copy the URL on the clipboard. You can go to another tab and paste the URL to open the file on the browser.

View file

@ -143,7 +143,7 @@ Learn more about the **[ToolJet Database here](/docs/tooljet-database)**
</div>
:::info
ToolJet application's User interface is constructed using Components like Tables, Forms, Charts, or Buttons etc. Check **Components Catalog** to learn more.
ToolJet application's User interface is constructed using Components like Tables, Forms, Charts, or Buttons etc. Check **[Components Catalog](/docs/widgets/overview)** to learn more.
:::
### Build queries and bind data to UI
@ -156,7 +156,7 @@ ToolJet application's User interface is constructed using Components like Tables
</div>
:::info
ToolJet can connect to several databases, APIs and external services to fetch and modify data. Check **Datasource Catalog** to learn more.
ToolJet can connect to several databases, APIs and external services to fetch and modify data. Check **[Datasource Catalog](/docs/data-sources/overview)** to learn more.
:::
2. Choose a **Table** from the dropdown, Select the **List rows** option from the **Operation** dropdown, You can leave other query parameters. Scroll down and enable **Run this query on application load** - this will trigger the query when the app is loaded.
@ -199,8 +199,8 @@ ToolJet application's User interface is constructed using Components like Tables
</div>
:::info
- You can manipulate the data returned by the queries using **Transformations**
- You can also **Run JS query** or **Python query** to perform custom behavior inside ToolJet
- You can manipulate the data returned by the queries using **[Transformations](/docs/tutorial/transformations)**
- You can also **[Run JavaScript code](/docs/data-sources/run-js)** or **[Run Python code](/docs/data-sources/run-py)** to perform custom behavior inside ToolJet
:::
### Preview, Release and Share app
@ -210,7 +210,7 @@ ToolJet application's User interface is constructed using Components like Tables
3. **Share** option allows you to share the **released version** of the application with other users or you can also make the app **public** and anyone with the URL will be able to use the app.
:::tip
You can control how much access to users have to your ToolJet apps and resources using **Org Management**.
You can control how much access to users have to your ToolJet apps and resources using **[Org Management](/docs/tutorial/manage-users-groups)**.
:::
## What Can I Do With ToolJet

View file

@ -0,0 +1,23 @@
---
id: intentionally-fail-js-query
title: Intentionally fail a RunJS query
---
In this how-to guide, we will create a RunJS query that will throw an error.
- Create a RunJS query and paste the code below. We will use the constructor `ReferenceError` since it is used to create a range error instance.
```js
throw new ReferenceError('This is a reference error.');
```
- Now, add a event handler to show an alert when the query fails. **Save** the query and **Run** it.
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/how-to/failjs/failjs.gif" alt="Intentionally fail a RunJS query" />
</div>
:::info
Most common use-case for intentionally failing a query is **debugging**.
:::

View file

@ -0,0 +1,173 @@
---
id: use-s3-signed-url-to-upload-docs
title: Use S3 signed URL to upload documents
---
# Use S3 signed URL to upload documents
In this how-to guide, you'll learn to upload documents to S3 buckets using the **S3 signed URL** from a ToolJet application.
For this guide, We are going to use one of the existing templates on ToolJet: **S3 File explorer**
:::info using Templates
On ToolJet Dashboard, Click on the down arrow on the right of the **New App** button, from the dropdown choose the **Choose from template** option.
:::
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/how-to/uses3presignedurl/template.png" alt="Use S3 pre-signed URL to upload documents: Choose template" width="700"/>
</div>
- Once you've created a new app using the template, you'll be prompted to create a **new version** of the existing version. After creating a new version, you'll be able to make changes in the app.
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/how-to/uses3presignedurl/newversion.png" alt="Use S3 pre-signed URL to upload documents: new version"/>
</div>
- Go to the **datasource manager** on the left-sidebar, you'll find that the **AWS S3 datasource** is already added. All you need to do is update the datasource **credentials**.
:::tip
Check the [AWS S3 datasource reference](/docs/data-sources/s3) to learn more about connnection and choosing your preferred authentication method.
:::
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/how-to/uses3presignedurl/s3connect.png" alt="Use S3 pre-signed URL to upload documents: add datasource"/>
</div>
- Once the datasource is connected successfully, go to the query manager and **Run** the **getBuckets** query. The operation selected in the getBuckets query is **List Buckets** which will fetch an array of all the buckets.
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/how-to/uses3presignedurl/getbuckets.png" alt="Use S3 pre-signed URL to upload documents: getBuckets query"/>
</div>
- Running the **getBuckets** query will load all the buckets in the dropdown in the app.
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/how-to/uses3presignedurl/dropdown.png" alt="Use S3 pre-signed URL to upload documents: loading buckets"/>
</div>
- Select a **bucket** from the dropdown and click on the **Fetch files** button to list all the files from the selected bucket on the table. The **Fetch files** button has the event handler added that triggers the **s32** query, the **s32** query uses the **List objects in a bucket** operation, and the bucket field in the query gets the value dynamically from the dropdown.
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/how-to/uses3presignedurl/fetchfiles.png" alt="Use S3 pre-signed URL to upload documents: list objects in a bucket"/>
</div>
- Let's go to the **uploadToS3** query and update the field values:
- **Operation**: Signed URL for upload
- **Bucket**: `{{components.dropdown1.value}}` this will fetch the dynamic value from the dropdown
- **Key**: `{{components.filepicker1.file[0].name}}` this will get the file name from the filepickers exposed variables
- **Expires in:** This sets an expiration time of URL, by default its `3600` seconds (1 hour)
- **Content Type**: `{{components.filepicker1.file[0].type}}` this will get the file type from the filepickers exposed variables
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/how-to/uses3presignedurl/upload.png" alt="Use S3 pre-signed URL to upload documents"/>
</div>
- Create two **RunJS** queries:
- Create a **runjs1** query and copy-paste the code below. This query gets the **base64data** from the file picker and convert the file's `base64Data` to into `BLOB`, and returns the file object.
```js
const base64String = components.filepicker1.file[0].base64Data
const decodedArray = new Uint8Array(atob(base64String).split('').map(c => c.charCodeAt(0)));
const file = new Blob([decodedArray], { type: components.filepicker1.file[0].type });
const fileName = components.filepicker1.file[0].name;
const fileObj = new File([file], fileName);
return fileObj
```
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/how-to/uses3presignedurl/runjs1.png" alt="Use S3 pre-signed URL to upload documents"/>
</div>
- Create another **runjs2** query and copy-paste the code below. This query gets the data(file object) returned by the first runjs query, the url returned by the **uploadToS3** query, and then makes PUT request.
```js
const file = queries.runjs2.data
const url = queries.s31.data.url
fetch(url, {
method: 'PUT',
body: file,
mode: 'cors',
headers: {
'Access-Control-Allow-Origin': '*',
'Content-Type': 'application/json'
}
})
.then(response => console.log('Upload successful!'))
.catch(error => console.error('Error uploading file:', error));
```
:::warning Enable Cross Origin Resource Sharing(CORS)
- For the file to be uploaded successfully, you will need to add the CORS policies from the **Permissions** tab of your **Bucket** settings. Here's a sample CORS:
```json
[
{
"AllowedHeaders": [
"*"
],
"AllowedMethods": [
"GET",
"PUT",
"POST"
],
"AllowedOrigins": [
"*"
],
"ExposeHeaders": []
}
]
```
:::
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/how-to/uses3presignedurl/runjs2.png" alt="Use S3 pre-signed URL to upload documents"/>
</div>
- Go to the **uploadToS3**, scroll down and add an event handler to the **uploadToS3** query. Select the **Query Success** event, **Run Query** as the action, and **runjs1** as the query to be triggered. **Save** the query.
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/how-to/uses3presignedurl/eventhandlerupload.png" alt="Use S3 pre-signed URL to upload documents"/>
</div>
- Let's go to the **runjs1** query and add the event handler to run a query on query success event, similar to how we did in the previous step. In the event handler, choose **runjs2** query. **Save** the query.
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/how-to/uses3presignedurl/eventhandlerrunjs2.png" alt="Use S3 pre-signed URL to upload documents"/>
</div>
- Now, let's go the final query **copySignedURL** that is connected to the table's action button. This query copy's the generated **Signed URL for download** onto the **clipboard**.
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/how-to/uses3presignedurl/copysigned.png" alt="Use S3 pre-signed URL to upload documents"/>
</div>
- Now that we have updated all the queries, and connected them through the event handlers. We can go ahead and pick a file from the file picker. Click on the file picker, select a file and then hit the **Upload file to S3** button.
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/how-to/uses3presignedurl/uploadbutton.png" alt="Use S3 pre-signed URL to upload documents"/>
</div>
- Once the button is clicked, the **uploadToS3** will triggered along with the **runjs1** and **runjs2** queries in sequence since we added them in the event handlers.
- You can go to the table and click on the **Copy signed URL** action button on the table, this will trigger the **copySignedURL** query and will copy the URL on the clipboard. You can go to another tab and paste the URL to open the file on the browser.

View file

@ -143,7 +143,7 @@ Learn more about the **[ToolJet Database here](/docs/tooljet-database)**
</div>
:::info
ToolJet application's User interface is constructed using Components like Tables, Forms, Charts, or Buttons etc. Check **Components Catalog** to learn more.
ToolJet application's User interface is constructed using Components like Tables, Forms, Charts, or Buttons etc. Check **[Components Catalog](/docs/widgets/overview)** to learn more.
:::
### Build queries and bind data to UI
@ -156,7 +156,7 @@ ToolJet application's User interface is constructed using Components like Tables
</div>
:::info
ToolJet can connect to several databases, APIs and external services to fetch and modify data. Check **Datasource Catalog** to learn more.
ToolJet can connect to several databases, APIs and external services to fetch and modify data. Check **[Datasource Catalog](/docs/data-sources/overview)** to learn more.
:::
2. Choose a **Table** from the dropdown, Select the **List rows** option from the **Operation** dropdown, You can leave other query parameters. Scroll down and enable **Run this query on application load** - this will trigger the query when the app is loaded.
@ -199,8 +199,8 @@ ToolJet application's User interface is constructed using Components like Tables
</div>
:::info
- You can manipulate the data returned by the queries using **Transformations**
- You can also **Run JS query** or **Python query** to perform custom behavior inside ToolJet
- You can manipulate the data returned by the queries using **[Transformations](/docs/tutorial/transformations)**
- You can also **[Run JavaScript code](/docs/data-sources/run-js)** or **[Run Python code](/docs/data-sources/run-py)** to perform custom behavior inside ToolJet
:::
### Preview, Release and Share app
@ -210,7 +210,7 @@ ToolJet application's User interface is constructed using Components like Tables
3. **Share** option allows you to share the **released version** of the application with other users or you can also make the app **public** and anyone with the URL will be able to use the app.
:::tip
You can control how much access to users have to your ToolJet apps and resources using **Org Management**.
You can control how much access to users have to your ToolJet apps and resources using **[Org Management](/docs/tutorial/manage-users-groups)**.
:::
## What Can I Do With ToolJet

View file

@ -0,0 +1,23 @@
---
id: intentionally-fail-js-query
title: Intentionally fail a RunJS query
---
In this how-to guide, we will create a RunJS query that will throw an error.
- Create a RunJS query and paste the code below. We will use the constructor `ReferenceError` since it is used to create a range error instance.
```js
throw new ReferenceError('This is a reference error.');
```
- Now, add a event handler to show an alert when the query fails. **Save** the query and **Run** it.
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/how-to/failjs/failjs.gif" alt="Intentionally fail a RunJS query" />
</div>
:::info
Most common use-case for intentionally failing a query is **debugging**.
:::

View file

@ -0,0 +1,173 @@
---
id: use-s3-signed-url-to-upload-docs
title: Use S3 signed URL to upload documents
---
# Use S3 signed URL to upload documents
In this how-to guide, you'll learn to upload documents to S3 buckets using the **S3 signed URL** from a ToolJet application.
For this guide, We are going to use one of the existing templates on ToolJet: **S3 File explorer**
:::info using Templates
On ToolJet Dashboard, Click on the down arrow on the right of the **New App** button, from the dropdown choose the **Choose from template** option.
:::
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/how-to/uses3presignedurl/template.png" alt="Use S3 pre-signed URL to upload documents: Choose template" width="700"/>
</div>
- Once you've created a new app using the template, you'll be prompted to create a **new version** of the existing version. After creating a new version, you'll be able to make changes in the app.
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/how-to/uses3presignedurl/newversion.png" alt="Use S3 pre-signed URL to upload documents: new version"/>
</div>
- Go to the **datasource manager** on the left-sidebar, you'll find that the **AWS S3 datasource** is already added. All you need to do is update the datasource **credentials**.
:::tip
Check the [AWS S3 datasource reference](/docs/data-sources/s3) to learn more about connnection and choosing your preferred authentication method.
:::
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/how-to/uses3presignedurl/s3connect.png" alt="Use S3 pre-signed URL to upload documents: add datasource"/>
</div>
- Once the datasource is connected successfully, go to the query manager and **Run** the **getBuckets** query. The operation selected in the getBuckets query is **List Buckets** which will fetch an array of all the buckets.
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/how-to/uses3presignedurl/getbuckets.png" alt="Use S3 pre-signed URL to upload documents: getBuckets query"/>
</div>
- Running the **getBuckets** query will load all the buckets in the dropdown in the app.
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/how-to/uses3presignedurl/dropdown.png" alt="Use S3 pre-signed URL to upload documents: loading buckets"/>
</div>
- Select a **bucket** from the dropdown and click on the **Fetch files** button to list all the files from the selected bucket on the table. The **Fetch files** button has the event handler added that triggers the **s32** query, the **s32** query uses the **List objects in a bucket** operation, and the bucket field in the query gets the value dynamically from the dropdown.
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/how-to/uses3presignedurl/fetchfiles.png" alt="Use S3 pre-signed URL to upload documents: list objects in a bucket"/>
</div>
- Let's go to the **uploadToS3** query and update the field values:
- **Operation**: Signed URL for upload
- **Bucket**: `{{components.dropdown1.value}}` this will fetch the dynamic value from the dropdown
- **Key**: `{{components.filepicker1.file[0].name}}` this will get the file name from the filepickers exposed variables
- **Expires in:** This sets an expiration time of URL, by default its `3600` seconds (1 hour)
- **Content Type**: `{{components.filepicker1.file[0].type}}` this will get the file type from the filepickers exposed variables
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/how-to/uses3presignedurl/upload.png" alt="Use S3 pre-signed URL to upload documents"/>
</div>
- Create two **RunJS** queries:
- Create a **runjs1** query and copy-paste the code below. This query gets the **base64data** from the file picker and convert the file's `base64Data` to into `BLOB`, and returns the file object.
```js
const base64String = components.filepicker1.file[0].base64Data
const decodedArray = new Uint8Array(atob(base64String).split('').map(c => c.charCodeAt(0)));
const file = new Blob([decodedArray], { type: components.filepicker1.file[0].type });
const fileName = components.filepicker1.file[0].name;
const fileObj = new File([file], fileName);
return fileObj
```
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/how-to/uses3presignedurl/runjs1.png" alt="Use S3 pre-signed URL to upload documents"/>
</div>
- Create another **runjs2** query and copy-paste the code below. This query gets the data(file object) returned by the first runjs query, the url returned by the **uploadToS3** query, and then makes PUT request.
```js
const file = queries.runjs2.data
const url = queries.s31.data.url
fetch(url, {
method: 'PUT',
body: file,
mode: 'cors',
headers: {
'Access-Control-Allow-Origin': '*',
'Content-Type': 'application/json'
}
})
.then(response => console.log('Upload successful!'))
.catch(error => console.error('Error uploading file:', error));
```
:::warning Enable Cross Origin Resource Sharing(CORS)
- For the file to be uploaded successfully, you will need to add the CORS policies from the **Permissions** tab of your **Bucket** settings. Here's a sample CORS:
```json
[
{
"AllowedHeaders": [
"*"
],
"AllowedMethods": [
"GET",
"PUT",
"POST"
],
"AllowedOrigins": [
"*"
],
"ExposeHeaders": []
}
]
```
:::
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/how-to/uses3presignedurl/runjs2.png" alt="Use S3 pre-signed URL to upload documents"/>
</div>
- Go to the **uploadToS3**, scroll down and add an event handler to the **uploadToS3** query. Select the **Query Success** event, **Run Query** as the action, and **runjs1** as the query to be triggered. **Save** the query.
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/how-to/uses3presignedurl/eventhandlerupload.png" alt="Use S3 pre-signed URL to upload documents"/>
</div>
- Let's go to the **runjs1** query and add the event handler to run a query on query success event, similar to how we did in the previous step. In the event handler, choose **runjs2** query. **Save** the query.
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/how-to/uses3presignedurl/eventhandlerrunjs2.png" alt="Use S3 pre-signed URL to upload documents"/>
</div>
- Now, let's go the final query **copySignedURL** that is connected to the table's action button. This query copy's the generated **Signed URL for download** onto the **clipboard**.
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/how-to/uses3presignedurl/copysigned.png" alt="Use S3 pre-signed URL to upload documents"/>
</div>
- Now that we have updated all the queries, and connected them through the event handlers. We can go ahead and pick a file from the file picker. Click on the file picker, select a file and then hit the **Upload file to S3** button.
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/how-to/uses3presignedurl/uploadbutton.png" alt="Use S3 pre-signed URL to upload documents"/>
</div>
- Once the button is clicked, the **uploadToS3** will triggered along with the **runjs1** and **runjs2** queries in sequence since we added them in the event handlers.
- You can go to the table and click on the **Copy signed URL** action button on the table, this will trigger the **copySignedURL** query and will copy the URL on the clipboard. You can go to another tab and paste the URL to open the file on the browser.

View file

@ -143,7 +143,7 @@ Learn more about the **[ToolJet Database here](/docs/tooljet-database)**
</div>
:::info
ToolJet application's User interface is constructed using Components like Tables, Forms, Charts, or Buttons etc. Check **Components Catalog** to learn more.
ToolJet application's User interface is constructed using Components like Tables, Forms, Charts, or Buttons etc. Check **[Components Catalog](/docs/widgets/overview)** to learn more.
:::
### Build queries and bind data to UI
@ -156,7 +156,7 @@ ToolJet application's User interface is constructed using Components like Tables
</div>
:::info
ToolJet can connect to several databases, APIs and external services to fetch and modify data. Check **Datasource Catalog** to learn more.
ToolJet can connect to several databases, APIs and external services to fetch and modify data. Check **[Datasource Catalog](/docs/data-sources/overview)** to learn more.
:::
2. Choose a **Table** from the dropdown, Select the **List rows** option from the **Operation** dropdown, You can leave other query parameters. Scroll down and enable **Run this query on application load** - this will trigger the query when the app is loaded.
@ -199,8 +199,8 @@ ToolJet application's User interface is constructed using Components like Tables
</div>
:::info
- You can manipulate the data returned by the queries using **Transformations**
- You can also **Run JS query** or **Python query** to perform custom behavior inside ToolJet
- You can manipulate the data returned by the queries using **[Transformations](/docs/tutorial/transformations)**
- You can also **[Run JavaScript code](/docs/data-sources/run-js)** or **[Run Python code](/docs/data-sources/run-py)** to perform custom behavior inside ToolJet
:::
### Preview, Release and Share app
@ -210,7 +210,7 @@ ToolJet application's User interface is constructed using Components like Tables
3. **Share** option allows you to share the **released version** of the application with other users or you can also make the app **public** and anyone with the URL will be able to use the app.
:::tip
You can control how much access to users have to your ToolJet apps and resources using **Org Management**.
You can control how much access to users have to your ToolJet apps and resources using **[Org Management](/docs/tutorial/manage-users-groups)**.
:::
## What Can I Do With ToolJet

View file

@ -0,0 +1,23 @@
---
id: intentionally-fail-js-query
title: Intentionally fail a RunJS query
---
In this how-to guide, we will create a RunJS query that will throw an error.
- Create a RunJS query and paste the code below. We will use the constructor `ReferenceError` since it is used to create a range error instance.
```js
throw new ReferenceError('This is a reference error.');
```
- Now, add a event handler to show an alert when the query fails. **Save** the query and **Run** it.
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/how-to/failjs/failjs.gif" alt="Intentionally fail a RunJS query" />
</div>
:::info
Most common use-case for intentionally failing a query is **debugging**.
:::

View file

@ -0,0 +1,173 @@
---
id: use-s3-signed-url-to-upload-docs
title: Use S3 signed URL to upload documents
---
# Use S3 signed URL to upload documents
In this how-to guide, you'll learn to upload documents to S3 buckets using the **S3 signed URL** from a ToolJet application.
For this guide, We are going to use one of the existing templates on ToolJet: **S3 File explorer**
:::info using Templates
On ToolJet Dashboard, Click on the down arrow on the right of the **New App** button, from the dropdown choose the **Choose from template** option.
:::
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/how-to/uses3presignedurl/template.png" alt="Use S3 pre-signed URL to upload documents: Choose template" width="700"/>
</div>
- Once you've created a new app using the template, you'll be prompted to create a **new version** of the existing version. After creating a new version, you'll be able to make changes in the app.
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/how-to/uses3presignedurl/newversion.png" alt="Use S3 pre-signed URL to upload documents: new version"/>
</div>
- Go to the **datasource manager** on the left-sidebar, you'll find that the **AWS S3 datasource** is already added. All you need to do is update the datasource **credentials**.
:::tip
Check the [AWS S3 datasource reference](/docs/data-sources/s3) to learn more about connnection and choosing your preferred authentication method.
:::
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/how-to/uses3presignedurl/s3connect.png" alt="Use S3 pre-signed URL to upload documents: add datasource"/>
</div>
- Once the datasource is connected successfully, go to the query manager and **Run** the **getBuckets** query. The operation selected in the getBuckets query is **List Buckets** which will fetch an array of all the buckets.
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/how-to/uses3presignedurl/getbuckets.png" alt="Use S3 pre-signed URL to upload documents: getBuckets query"/>
</div>
- Running the **getBuckets** query will load all the buckets in the dropdown in the app.
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/how-to/uses3presignedurl/dropdown.png" alt="Use S3 pre-signed URL to upload documents: loading buckets"/>
</div>
- Select a **bucket** from the dropdown and click on the **Fetch files** button to list all the files from the selected bucket on the table. The **Fetch files** button has the event handler added that triggers the **s32** query, the **s32** query uses the **List objects in a bucket** operation, and the bucket field in the query gets the value dynamically from the dropdown.
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/how-to/uses3presignedurl/fetchfiles.png" alt="Use S3 pre-signed URL to upload documents: list objects in a bucket"/>
</div>
- Let's go to the **uploadToS3** query and update the field values:
- **Operation**: Signed URL for upload
- **Bucket**: `{{components.dropdown1.value}}` this will fetch the dynamic value from the dropdown
- **Key**: `{{components.filepicker1.file[0].name}}` this will get the file name from the filepickers exposed variables
- **Expires in:** This sets an expiration time of URL, by default its `3600` seconds (1 hour)
- **Content Type**: `{{components.filepicker1.file[0].type}}` this will get the file type from the filepickers exposed variables
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/how-to/uses3presignedurl/upload.png" alt="Use S3 pre-signed URL to upload documents"/>
</div>
- Create two **RunJS** queries:
- Create a **runjs1** query and copy-paste the code below. This query gets the **base64data** from the file picker and convert the file's `base64Data` to into `BLOB`, and returns the file object.
```js
const base64String = components.filepicker1.file[0].base64Data
const decodedArray = new Uint8Array(atob(base64String).split('').map(c => c.charCodeAt(0)));
const file = new Blob([decodedArray], { type: components.filepicker1.file[0].type });
const fileName = components.filepicker1.file[0].name;
const fileObj = new File([file], fileName);
return fileObj
```
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/how-to/uses3presignedurl/runjs1.png" alt="Use S3 pre-signed URL to upload documents"/>
</div>
- Create another **runjs2** query and copy-paste the code below. This query gets the data(file object) returned by the first runjs query, the url returned by the **uploadToS3** query, and then makes PUT request.
```js
const file = queries.runjs2.data
const url = queries.s31.data.url
fetch(url, {
method: 'PUT',
body: file,
mode: 'cors',
headers: {
'Access-Control-Allow-Origin': '*',
'Content-Type': 'application/json'
}
})
.then(response => console.log('Upload successful!'))
.catch(error => console.error('Error uploading file:', error));
```
:::warning Enable Cross Origin Resource Sharing(CORS)
- For the file to be uploaded successfully, you will need to add the CORS policies from the **Permissions** tab of your **Bucket** settings. Here's a sample CORS:
```json
[
{
"AllowedHeaders": [
"*"
],
"AllowedMethods": [
"GET",
"PUT",
"POST"
],
"AllowedOrigins": [
"*"
],
"ExposeHeaders": []
}
]
```
:::
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/how-to/uses3presignedurl/runjs2.png" alt="Use S3 pre-signed URL to upload documents"/>
</div>
- Go to the **uploadToS3**, scroll down and add an event handler to the **uploadToS3** query. Select the **Query Success** event, **Run Query** as the action, and **runjs1** as the query to be triggered. **Save** the query.
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/how-to/uses3presignedurl/eventhandlerupload.png" alt="Use S3 pre-signed URL to upload documents"/>
</div>
- Let's go to the **runjs1** query and add the event handler to run a query on query success event, similar to how we did in the previous step. In the event handler, choose **runjs2** query. **Save** the query.
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/how-to/uses3presignedurl/eventhandlerrunjs2.png" alt="Use S3 pre-signed URL to upload documents"/>
</div>
- Now, let's go the final query **copySignedURL** that is connected to the table's action button. This query copy's the generated **Signed URL for download** onto the **clipboard**.
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/how-to/uses3presignedurl/copysigned.png" alt="Use S3 pre-signed URL to upload documents"/>
</div>
- Now that we have updated all the queries, and connected them through the event handlers. We can go ahead and pick a file from the file picker. Click on the file picker, select a file and then hit the **Upload file to S3** button.
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/how-to/uses3presignedurl/uploadbutton.png" alt="Use S3 pre-signed URL to upload documents"/>
</div>
- Once the button is clicked, the **uploadToS3** will triggered along with the **runjs1** and **runjs2** queries in sequence since we added them in the event handlers.
- You can go to the table and click on the **Copy signed URL** action button on the table, this will trigger the **copySignedURL** query and will copy the URL on the clipboard. You can go to another tab and paste the URL to open the file on the browser.

View file

@ -17,13 +17,14 @@ The user details entered while setting up ToolJet will have Super Admin privileg
| Manage Groups in their workspace (Create Group/Add or Delete Users from groups/ Modify Group Permissions) | ✅ | ✅ |
| Manage SSO in their workspace | ✅ | ✅ |
| Manage Workspace Variables in their workspace | ✅ | ✅ |
| [Manage Global datasources for the user group in their workspace](/docs/data-sources/overview#permissions) | ✅ | ✅ |
| [Access any user's personal workspace (create, edit or delete apps)](#access-any-workspace) | ❌ | ✅ |
| [Archive Admin or any user of any workspace](#archiveunarchive-users) | ❌ | ✅ |
| [Access any user's ToolJet database (create, edit or delete database)](#access-tooljet-db-in-any-workspace) | ❌ | ✅ |
| [Manage any workspace's setting (Groups/SSO/Workspace Variables)](#manage-workspace-setting-groupsssoworkspace-variables) | ❌ | ✅ |
| [Manage all users from all the workspaces in the instance](#checking-all-the-users-in-the-instance) | ❌ | ✅ |
| [Make any user Super Admin](#make-the-user-super-admin) | ❌ | ✅ |
| [Restrict personal workspace of invited users](#allow-users-to-create-personal-workspace) | ❌ | ✅ |
| [Restrict creation of personal workspace of users](#restrict-creation-of-personal-workspace-of-users) | ❌ | ✅ |
<div style={{textAlign: 'center'}}>
@ -117,11 +118,11 @@ The user will become Super Admin and the Type column will update from **`workspa
</div>
### Allow users to create personal workspace
### Restrict creation of personal workspace of users
When a user joins a workspace, they are provided with their own personal workspace and option to create new workspaces.
Super Admins can control this behavior from the Manage Instance Settings page, they can **toggle off** the option to **Allow personal workspace**. Now whenever a user joins a workspace they won't be provided a personal workspace nor they will be able to create a new workspace in the instance.
Super Admins can **control** this behavior from the Manage Instance Settings page, they can **toggle off** the option to **Allow personal workspace**. Now whenever a user joins a workspace they won't be provided a personal workspace nor they will be able to create a new workspace in the instance.
<div style={{textAlign: 'center'}}>

View file

@ -3,9 +3,13 @@ id: overview
title: Overview
---
# Datasources : Overview
# Global Datasources : Overview
Datasources pull in and push data to any source including databases, external APIs, or services.
Global datasources pull in and push data to any source including databases, external APIs, or services. Once a global datasource is connected to a workspace, the connection can be shared with any app of that workspace.
:::caution
Global datasources are available only on **ToolJet version 2.3.0 and above**.
:::
<div style={{textAlign: 'center'}}>
@ -13,30 +17,115 @@ Datasources pull in and push data to any source including databases, external AP
</div>
## Connecting datasources
## Connecting global datasources
1. After logging in to ToolJet, create a new app from the dashboard
1. From the ToolJet dashboard, go to the **global datasources page** from the left sidebar.
<div style={{textAlign: 'center'}}>
2. There are two ways for connecting a datasource. You can connect from:
1. **Left-sidebar**: On the left sidebar, click on the `datasource` icon and then click on the `+ add datasource` button
<img className="screenshot-full" src="/img/datasource-reference/overview/global.png" alt="Datasources: Overview" />
<div style={{textAlign: 'center'}}>
</div>
<img className="screenshot-full" src="/img/datasource-reference/overview/ls2.png" alt="Datasources: Overview" width="400"/>
2. Click on the **Add new datasource** button, a modal will pop-up with all the available global datasources.
<div style={{textAlign: 'center'}}>
</div>
<img className="screenshot-full" src="/img/datasource-reference/overview/popup.png" alt="Datasources: Overview" />
2. **Query Panel**: Go to the query panel at the bottom, click on the `+Add` button and then click `Add datasource` button
</div>
<div style={{textAlign: 'center'}}>
3. Select the datasource, enter the **Credentials** and **Save** the datasource.
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/datasource-reference/overview/qp2.png" alt="Datasources: Overview"/>
<img className="screenshot-full" src="/img/datasource-reference/overview/connection.png" alt="Datasources: Overview" />
</div>
</div>
3. Follow the steps in the **[Datasource Library](/docs/data-sources/airtable)** specific to the desired datasource
4. Now, go back to the dashboard, create a new app, and the datasource will be available on the query panel under **Global Datasources**. Added datasources will be available on any of the **existing** or the **new applications**.
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/datasource-reference/overview/globalquery.png" alt="Datasources: Overview" />
</div>
5. You can now create queries of the connected global datasource. From the queries, you'll be able to switch to **different connections** of the same datasource if there are more than one connections created.
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/datasource-reference/overview/switch.png" alt="Datasources: Overview" />
</div>
## Changing scope of datasources of an app created on older versions of ToolJet
On ToolJet versions below 2.3.0, the datasource connection was made from within the individual apps. To make it backward compatible, we added an option to change the scope of the datasources and make it global datasource.
1. If you open an app created on previos versions of ToolJet, you'll find the datasource manager on the left sidebar of the App Builder.
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/datasource-reference/overview/leftsidebar.png" alt="Datasources: Overview" />
</div>
2. Click on the kebab menu next to the connected datasource, select the **change scope** option.
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/datasource-reference/overview/changescope.png" alt="Datasources: Overview" />
</div>
3. Once you change the scope of the datasource and make it global, you'll see that the **datasource manager** is removed from the left sidebar and now you'll find the datasource on the **query panel** under Global Datasources. You can now configure the datasource fromt the Global Datasource page on the **dashboard**.
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/datasource-reference/overview/queryadd.png" alt="Datasources: Overview" />
</div>
## Default datasources
By default, 4 datasources will be available on every app on ToolJet:
- **[ToolJet Database](/docs/tooljet-database/)**
- **[RestAPI](/docs/data-sources/restapi/)**
- **[Run JavaScript Query](/docs/data-sources/run-js/)**
- **[Run Python Query](/docs/data-sources/run-py/)**
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/datasource-reference/overview/default.png" alt="Datasources: Overview" />
</div>
## Permissions
Only **Admins** and **[Super Admins](/docs/Enterprise/superadmin)** of the workspace can change the **[Permissions](/docs/tutorial/manage-users-groups#group-properties)** for Global Datasource.
From **Workspace Settings** -> **Groups Settings**, Admins and Super Admins can set the permission for a user group to:
- **Create** and **Delete** datasources onto that workspace. If only **Create** permission is set then the users of the group will only be able to add new datasources on the workspace, and if only **Delete** permission is set then the users of the group will only be able to delete the connected datasources on the workspace.
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/datasource-reference/overview/create.png" alt="Datasources: Overview" />
</div>
- If any of the permission(Create or Delete) is not enabled for a user group then the users of the group will get an error toast when they try to Add or Delete the global datasource.
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/datasource-reference/overview/error.png" alt="Datasources: Overview" />
</div>
- **View** or **Edit** allowed global datasources from the **Datasources** tab. If only **View** permission is set then the users of the group will only be able to connect to the allowed datasource, and if only **Edit** permission is set then the users of the group will be able to update the credentials of the allowed datasources.
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/datasource-reference/overview/view.png" alt="Datasources: Overview" />
</div>
- If any of the permission(View or Edit) is not enabled for a user group then the users of the group will get an error toast when they try to Add or Delete the global datasource.
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/datasource-reference/overview/edit.png" alt="Datasources: Overview" />
</div>
:::info
ToolJet allows you to transform the data returned by datasources using **[Transformations](/docs/tutorial/transformations)**
:::

View file

@ -0,0 +1,23 @@
---
id: intentionally-fail-js-query
title: Intentionally fail a RunJS query
---
In this how-to guide, we will create a RunJS query that will throw an error.
- Create a RunJS query and paste the code below. We will use the constructor `ReferenceError` since it is used to create a range error instance.
```js
throw new ReferenceError('This is a reference error.');
```
- Now, add a event handler to show an alert when the query fails. **Save** the query and **Run** it.
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/how-to/failjs/failjs.gif" alt="Intentionally fail a RunJS query" />
</div>
:::info
Most common use-case for intentionally failing a query is **debugging**.
:::

View file

@ -0,0 +1,173 @@
---
id: use-s3-signed-url-to-upload-docs
title: Use S3 signed URL to upload documents
---
# Use S3 signed URL to upload documents
In this how-to guide, you'll learn to upload documents to S3 buckets using the **S3 signed URL** from a ToolJet application.
For this guide, We are going to use one of the existing templates on ToolJet: **S3 File explorer**
:::info using Templates
On ToolJet Dashboard, Click on the down arrow on the right of the **New App** button, from the dropdown choose the **Choose from template** option.
:::
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/how-to/uses3presignedurl/template.png" alt="Use S3 pre-signed URL to upload documents: Choose template" width="700"/>
</div>
- Once you've created a new app using the template, you'll be prompted to create a **new version** of the existing version. After creating a new version, you'll be able to make changes in the app.
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/how-to/uses3presignedurl/newversion.png" alt="Use S3 pre-signed URL to upload documents: new version"/>
</div>
- Go to the **datasource manager** on the left-sidebar, you'll find that the **AWS S3 datasource** is already added. All you need to do is update the datasource **credentials**.
:::tip
Check the [AWS S3 datasource reference](/docs/data-sources/s3) to learn more about connnection and choosing your preferred authentication method.
:::
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/how-to/uses3presignedurl/s3connect.png" alt="Use S3 pre-signed URL to upload documents: add datasource"/>
</div>
- Once the datasource is connected successfully, go to the query manager and **Run** the **getBuckets** query. The operation selected in the getBuckets query is **List Buckets** which will fetch an array of all the buckets.
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/how-to/uses3presignedurl/getbuckets.png" alt="Use S3 pre-signed URL to upload documents: getBuckets query"/>
</div>
- Running the **getBuckets** query will load all the buckets in the dropdown in the app.
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/how-to/uses3presignedurl/dropdown.png" alt="Use S3 pre-signed URL to upload documents: loading buckets"/>
</div>
- Select a **bucket** from the dropdown and click on the **Fetch files** button to list all the files from the selected bucket on the table. The **Fetch files** button has the event handler added that triggers the **s32** query, the **s32** query uses the **List objects in a bucket** operation, and the bucket field in the query gets the value dynamically from the dropdown.
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/how-to/uses3presignedurl/fetchfiles.png" alt="Use S3 pre-signed URL to upload documents: list objects in a bucket"/>
</div>
- Let's go to the **uploadToS3** query and update the field values:
- **Operation**: Signed URL for upload
- **Bucket**: `{{components.dropdown1.value}}` this will fetch the dynamic value from the dropdown
- **Key**: `{{components.filepicker1.file[0].name}}` this will get the file name from the filepickers exposed variables
- **Expires in:** This sets an expiration time of URL, by default its `3600` seconds (1 hour)
- **Content Type**: `{{components.filepicker1.file[0].type}}` this will get the file type from the filepickers exposed variables
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/how-to/uses3presignedurl/upload.png" alt="Use S3 pre-signed URL to upload documents"/>
</div>
- Create two **RunJS** queries:
- Create a **runjs1** query and copy-paste the code below. This query gets the **base64data** from the file picker and convert the file's `base64Data` to into `BLOB`, and returns the file object.
```js
const base64String = components.filepicker1.file[0].base64Data
const decodedArray = new Uint8Array(atob(base64String).split('').map(c => c.charCodeAt(0)));
const file = new Blob([decodedArray], { type: components.filepicker1.file[0].type });
const fileName = components.filepicker1.file[0].name;
const fileObj = new File([file], fileName);
return fileObj
```
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/how-to/uses3presignedurl/runjs1.png" alt="Use S3 pre-signed URL to upload documents"/>
</div>
- Create another **runjs2** query and copy-paste the code below. This query gets the data(file object) returned by the first runjs query, the url returned by the **uploadToS3** query, and then makes PUT request.
```js
const file = queries.runjs2.data
const url = queries.s31.data.url
fetch(url, {
method: 'PUT',
body: file,
mode: 'cors',
headers: {
'Access-Control-Allow-Origin': '*',
'Content-Type': 'application/json'
}
})
.then(response => console.log('Upload successful!'))
.catch(error => console.error('Error uploading file:', error));
```
:::warning Enable Cross Origin Resource Sharing(CORS)
- For the file to be uploaded successfully, you will need to add the CORS policies from the **Permissions** tab of your **Bucket** settings. Here's a sample CORS:
```json
[
{
"AllowedHeaders": [
"*"
],
"AllowedMethods": [
"GET",
"PUT",
"POST"
],
"AllowedOrigins": [
"*"
],
"ExposeHeaders": []
}
]
```
:::
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/how-to/uses3presignedurl/runjs2.png" alt="Use S3 pre-signed URL to upload documents"/>
</div>
- Go to the **uploadToS3**, scroll down and add an event handler to the **uploadToS3** query. Select the **Query Success** event, **Run Query** as the action, and **runjs1** as the query to be triggered. **Save** the query.
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/how-to/uses3presignedurl/eventhandlerupload.png" alt="Use S3 pre-signed URL to upload documents"/>
</div>
- Let's go to the **runjs1** query and add the event handler to run a query on query success event, similar to how we did in the previous step. In the event handler, choose **runjs2** query. **Save** the query.
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/how-to/uses3presignedurl/eventhandlerrunjs2.png" alt="Use S3 pre-signed URL to upload documents"/>
</div>
- Now, let's go the final query **copySignedURL** that is connected to the table's action button. This query copy's the generated **Signed URL for download** onto the **clipboard**.
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/how-to/uses3presignedurl/copysigned.png" alt="Use S3 pre-signed URL to upload documents"/>
</div>
- Now that we have updated all the queries, and connected them through the event handlers. We can go ahead and pick a file from the file picker. Click on the file picker, select a file and then hit the **Upload file to S3** button.
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/how-to/uses3presignedurl/uploadbutton.png" alt="Use S3 pre-signed URL to upload documents"/>
</div>
- Once the button is clicked, the **uploadToS3** will triggered along with the **runjs1** and **runjs2** queries in sequence since we added them in the event handlers.
- You can go to the table and click on the **Copy signed URL** action button on the table, this will trigger the **copySignedURL** query and will copy the URL on the clipboard. You can go to another tab and paste the URL to open the file on the browser.

View file

@ -8,7 +8,7 @@ Permissions allow you to create and share resources to easily ensure what level
Admins can invite **Users** to their workspaces and assign them to the **Groups** that have Permissions to access Apps, folders, or workspace variables.
:::info
See **[Manage Users and Groups](/docs/tutorial/manage-users-groups)** to learn how to invite users to ToolJet.
See **[Manage Users and Groups](/docs/tutorial/manage-users-groups)** to know more about managing users and groups on your workspace.
:::
## Role-Based Access Control (RBAC) Glossary
@ -18,4 +18,4 @@ See **[Manage Users and Groups](/docs/tutorial/manage-users-groups)** to learn h
- **All Users** - Contains all the users in your workspace. When **New Users** are invited they are added to this group by default.
- **Admins** - Contains all Admins in your workspace. Everyone added to this group will Permission to access all the ToolJet resources.
- **Apps, Folder, Workspace Variables -** Resources that Admins can set permissions on.
- **Permissions -** Create, Update and Delete.
- **Permissions -** Create, Update and Delete.

View file

@ -77,7 +77,7 @@ Similar to archiving a user's access, you can enable it again by clicking on **U
## Managing Groups
On ToolJet, Admins can create groups for users added in a workspace and grant them access to particular app(s) with specific permissions. To manage groups, just go to the **Workspace Settings** from the left-sidebar of the dashboard and click on the **Groups**.
On ToolJet, Admins and Super Admins can create groups for users added in a workspace and grant them access to particular app(s) with specific permissions. To manage groups, just go to the **Workspace Settings** from the left-sidebar of the dashboard and click on the **Groups**.
<div style={{textAlign: 'center'}}>
@ -87,11 +87,16 @@ On ToolJet, Admins can create groups for users added in a workspace and grant th
### Group properties
Every group on ToolJet has three sections:
Every group on ToolJet has **four** sections:
- [Apps](#apps)
- [Users](#users)
- [Permissions](#permissions)
- [Datasources](#datasources)
#### Apps:
Admins can add or remove any number of apps for a group of users. To add an app to a group, select an app from the dropdown and click on `Add` button next to it. You can also set app permissions such as `View` or `Edit` for the group. You can set different permissions for different apps in a group.
Admins and Super Admins can add or remove any number of apps for a group of users. To add an app to a group, select an app from the dropdown and click on `Add` button next to it. You can also set app permissions such as `View` or `Edit` for the group. You can set different permissions for different apps in a group.
<div style={{textAlign: 'center'}}>
@ -101,7 +106,7 @@ Admins can add or remove any number of apps for a group of users. To add an app
#### Users:
Admins can add or remove any numbers of users in a group. Just select a user from the dropdown and click on `Add` button to add it to a group. To delete a user from a group, click on `Delete` button next to it.
Admins and Super Admins can add or remove any numbers of users in a group. Just select a user from the dropdown and click on `Add` button to add it to a group. To delete a user from a group, click on `Delete` button next to it.
<div style={{textAlign: 'center'}}>
@ -111,16 +116,30 @@ Admins can add or remove any numbers of users in a group. Just select a user fro
#### Permissions:
Admins can set granular permission like creating/deleting apps or creating folder for a group of users.
Admins and Super Admins can set granular permission for the users added in that particular group, such as:
- **Create** and **Delete** Apps
- **Create**, **Update**, and **Delete** Folders
- **Create**, **Update**, and **Delete** [Workspace Variables](/docs/tutorial/workspace-variables)
- **Create** and **Delete** [Global Datasources](/docs/data-sources/overview)
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/tutorial/manage-users-groups/permissionsv2.png" alt="permissions" />
<img className="screenshot-full" src="/img/tutorial/manage-users-groups/dspermission.png" alt="permissions" />
</div>
#### Datasources:
Only Admins and Super Admins can define what datasources can be **viewed** or **edited** by the users of that group.
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/tutorial/manage-users-groups/gdspermission.png" alt="permissions" />
</div>
:::tip
All the activities performed by any Admin or any user in a workspace is logged in `Audit logs` - including any activity related with managing users and groups.
All the activities performed by any Admin, Super Admin or any user in a workspace is logged in [Audit logs](/docs/Enterprise/audit_logs) - including any activity related with managing users and groups.
:::
### Predefined Groups

View file

@ -17,6 +17,7 @@ The user details entered while setting up ToolJet will have Super Admin privileg
| Manage Groups in their workspace (Create Group/Add or Delete Users from groups/ Modify Group Permissions) | ✅ | ✅ |
| Manage SSO in their workspace | ✅ | ✅ |
| Manage Workspace Variables in their workspace | ✅ | ✅ |
| [Manage Global datasources for the user group in their workspace](/docs/data-sources/overview#permissions) | ✅ | ✅ |
| [Access any user's personal workspace (create, edit or delete apps)](#access-any-workspace) | ❌ | ✅ |
| [Archive Admin or any user of any workspace](#archiveunarchive-users) | ❌ | ✅ |
| [Access any user's ToolJet database (create, edit or delete database)](#access-tooljet-db-in-any-workspace) | ❌ | ✅ |

View file

@ -41,3 +41,6 @@ For VSCode users, you can set the formatter to `ESLint` in the [**settings.json*
1. **Node version 18.3.0**
2. **npm version 8.11.0**
:::tip
It is recommended to check the VSCode **Setting.json**(Press `ctrl/cmnd + P` and search `>Settings (JSON)`) file to ensure there are no overrides to the eslint config rules. Comment the following rules for eslint: **eslint.options: {...}**.
:::

View file

@ -3,9 +3,13 @@ id: overview
title: Overview
---
# Datasources : Overview
# Global Datasources : Overview
Datasources pull in and push data to any source including databases, external APIs, or services.
Global datasources pull in and push data to any source including databases, external APIs, or services. Once a global datasource is connected to a workspace, the connection can be shared with any app of that workspace.
:::caution
Global datasources are available only on **ToolJet version 2.3.0 and above**.
:::
<div style={{textAlign: 'center'}}>
@ -13,30 +17,115 @@ Datasources pull in and push data to any source including databases, external AP
</div>
## Connecting datasources
## Connecting global datasources
1. After logging in to ToolJet, create a new app from the dashboard
1. From the ToolJet dashboard, go to the **global datasources page** from the left sidebar.
<div style={{textAlign: 'center'}}>
2. There are two ways for connecting a datasource. You can connect from:
1. **Left-sidebar**: On the left sidebar, click on the `datasource` icon and then click on the `+ add datasource` button
<img className="screenshot-full" src="/img/datasource-reference/overview/global.png" alt="Datasources: Overview" />
<div style={{textAlign: 'center'}}>
</div>
<img className="screenshot-full" src="/img/datasource-reference/overview/ls2.png" alt="Datasources: Overview" width="400"/>
2. Click on the **Add new datasource** button, a modal will pop-up with all the available global datasources.
<div style={{textAlign: 'center'}}>
</div>
<img className="screenshot-full" src="/img/datasource-reference/overview/popup.png" alt="Datasources: Overview" />
2. **Query Panel**: Go to the query panel at the bottom, click on the `+Add` button and then click `Add datasource` button
</div>
<div style={{textAlign: 'center'}}>
3. Select the datasource, enter the **Credentials** and **Save** the datasource.
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/datasource-reference/overview/qp2.png" alt="Datasources: Overview"/>
<img className="screenshot-full" src="/img/datasource-reference/overview/connection.png" alt="Datasources: Overview" />
</div>
</div>
3. Follow the steps in the **[Datasource Library](/docs/data-sources/airtable)** specific to the desired datasource
4. Now, go back to the dashboard, create a new app, and the datasource will be available on the query panel under **Global Datasources**. Added datasources will be available on any of the **existing** or the **new applications**.
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/datasource-reference/overview/globalquery.png" alt="Datasources: Overview" />
</div>
5. You can now create queries of the connected global datasource. From the queries, you'll be able to switch to **different connections** of the same datasource if there are more than one connections created.
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/datasource-reference/overview/switch.png" alt="Datasources: Overview" />
</div>
## Changing scope of datasources of an app created on older versions of ToolJet
On ToolJet versions below 2.3.0, the datasource connection was made from within the individual apps. To make it backward compatible, we added an option to change the scope of the datasources and make it global datasource.
1. If you open an app created on previos versions of ToolJet, you'll find the datasource manager on the left sidebar of the App Builder.
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/datasource-reference/overview/leftsidebar.png" alt="Datasources: Overview" />
</div>
2. Click on the kebab menu next to the connected datasource, select the **change scope** option.
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/datasource-reference/overview/changescope.png" alt="Datasources: Overview" />
</div>
3. Once you change the scope of the datasource and make it global, you'll see that the **datasource manager** is removed from the left sidebar and now you'll find the datasource on the **query panel** under Global Datasources. You can now configure the datasource fromt the Global Datasource page on the **dashboard**.
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/datasource-reference/overview/queryadd.png" alt="Datasources: Overview" />
</div>
## Default datasources
By default, 4 datasources will be available on every app on ToolJet:
- **[ToolJet Database](/docs/tooljet-database/)**
- **[RestAPI](/docs/data-sources/restapi/)**
- **[Run JavaScript Query](/docs/data-sources/run-js/)**
- **[Run Python Query](/docs/data-sources/run-py/)**
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/datasource-reference/overview/default.png" alt="Datasources: Overview" />
</div>
## Permissions
Only **Admins** and **[Super Admins](/docs/Enterprise/superadmin)** of the workspace can change the **[Permissions](/docs/tutorial/manage-users-groups#group-properties)** for Global Datasource.
From **Workspace Settings** -> **Groups Settings**, Admins and Super Admins can set the permission for a user group to:
- **Create** and **Delete** datasources onto that workspace. If **Create** permission is enabled then the users can add new global datasources and **edit** the datasources as well but cannot **delete** it, and if only **Delete** permission is set then the users of the group will only be able to delete the connected datasources on the workspace.
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/datasource-reference/overview/create.png" alt="Datasources: Overview" />
</div>
- If any of the permission(Create or Delete) is not enabled for a user group then the users of the group will get an error toast when they try to Add or Delete the global datasource.
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/datasource-reference/overview/error.png" alt="Datasources: Overview" />
</div>
- **View** or **Edit** allowed global datasources from the **Datasources** tab. If only **View** permission is set then the users of the group will only be able to connect to the allowed datasource, and if only **Edit** permission is set then the users of the group will be able to update the credentials of the allowed datasources.
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/datasource-reference/overview/view.png" alt="Datasources: Overview" />
</div>
- If any of the permission(View or Edit) is not enabled for a user group then the users of the group will get an error toast when they try to Add or Delete the global datasource.
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/datasource-reference/overview/edit.png" alt="Datasources: Overview" />
</div>
:::info
ToolJet allows you to transform the data returned by datasources using **[Transformations](/docs/tutorial/transformations)**
:::

View file

@ -143,7 +143,7 @@ Learn more about the **[ToolJet Database here](/docs/tooljet-database)**
</div>
:::info
ToolJet application's User interface is constructed using Components like Tables, Forms, Charts, or Buttons etc. Check **Components Catalog** to learn more.
ToolJet application's User interface is constructed using Components like Tables, Forms, Charts, or Buttons etc. Check **[Components Catalog](/docs/widgets/overview)** to learn more.
:::
### Build queries and bind data to UI
@ -156,7 +156,7 @@ ToolJet application's User interface is constructed using Components like Tables
</div>
:::info
ToolJet can connect to several databases, APIs and external services to fetch and modify data. Check **Datasource Catalog** to learn more.
ToolJet can connect to several databases, APIs and external services to fetch and modify data. Check **[Datasource Catalog](/docs/data-sources/overview)** to learn more.
:::
2. Choose a **Table** from the dropdown, Select the **List rows** option from the **Operation** dropdown, You can leave other query parameters. Scroll down and enable **Run this query on application load** - this will trigger the query when the app is loaded.
@ -199,8 +199,8 @@ ToolJet application's User interface is constructed using Components like Tables
</div>
:::info
- You can manipulate the data returned by the queries using **Transformations**
- You can also **Run JS query** or **Python query** to perform custom behavior inside ToolJet
- You can manipulate the data returned by the queries using **[Transformations](/docs/tutorial/transformations)**
- You can also **[Run JavaScript code](/docs/data-sources/run-js)** or **[Run Python code](/docs/data-sources/run-py)** to perform custom behavior inside ToolJet
:::
### Preview, Release and Share app
@ -210,7 +210,7 @@ ToolJet application's User interface is constructed using Components like Tables
3. **Share** option allows you to share the **released version** of the application with other users or you can also make the app **public** and anyone with the URL will be able to use the app.
:::tip
You can control how much access to users have to your ToolJet apps and resources using **Org Management**.
You can control how much access to users have to your ToolJet apps and resources using **[Org Management](/docs/tutorial/manage-users-groups)**.
:::
## What Can I Do With ToolJet

View file

@ -0,0 +1,23 @@
---
id: intentionally-fail-js-query
title: Intentionally fail a RunJS query
---
In this how-to guide, we will create a RunJS query that will throw an error.
- Create a RunJS query and paste the code below. We will use the constructor `ReferenceError` since it is used to create a range error instance.
```js
throw new ReferenceError('This is a reference error.');
```
- Now, add a event handler to show an alert when the query fails. **Save** the query and **Run** it.
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/how-to/failjs/failjs.gif" alt="Intentionally fail a RunJS query" />
</div>
:::info
Most common use-case for intentionally failing a query is **debugging**.
:::

View file

@ -0,0 +1,173 @@
---
id: use-s3-signed-url-to-upload-docs
title: Use S3 signed URL to upload documents
---
# Use S3 signed URL to upload documents
In this how-to guide, you'll learn to upload documents to S3 buckets using the **S3 signed URL** from a ToolJet application.
For this guide, We are going to use one of the existing templates on ToolJet: **S3 File explorer**
:::info using Templates
On ToolJet Dashboard, Click on the down arrow on the right of the **New App** button, from the dropdown choose the **Choose from template** option.
:::
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/how-to/uses3presignedurl/template.png" alt="Use S3 pre-signed URL to upload documents: Choose template" width="700"/>
</div>
- Once you've created a new app using the template, you'll be prompted to create a **new version** of the existing version. After creating a new version, you'll be able to make changes in the app.
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/how-to/uses3presignedurl/newversion.png" alt="Use S3 pre-signed URL to upload documents: new version"/>
</div>
- Go to the **datasource manager** on the left-sidebar, you'll find that the **AWS S3 datasource** is already added. All you need to do is update the datasource **credentials**.
:::tip
Check the [AWS S3 datasource reference](/docs/data-sources/s3) to learn more about connnection and choosing your preferred authentication method.
:::
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/how-to/uses3presignedurl/s3connect.png" alt="Use S3 pre-signed URL to upload documents: add datasource"/>
</div>
- Once the datasource is connected successfully, go to the query manager and **Run** the **getBuckets** query. The operation selected in the getBuckets query is **List Buckets** which will fetch an array of all the buckets.
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/how-to/uses3presignedurl/getbuckets.png" alt="Use S3 pre-signed URL to upload documents: getBuckets query"/>
</div>
- Running the **getBuckets** query will load all the buckets in the dropdown in the app.
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/how-to/uses3presignedurl/dropdown.png" alt="Use S3 pre-signed URL to upload documents: loading buckets"/>
</div>
- Select a **bucket** from the dropdown and click on the **Fetch files** button to list all the files from the selected bucket on the table. The **Fetch files** button has the event handler added that triggers the **s32** query, the **s32** query uses the **List objects in a bucket** operation, and the bucket field in the query gets the value dynamically from the dropdown.
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/how-to/uses3presignedurl/fetchfiles.png" alt="Use S3 pre-signed URL to upload documents: list objects in a bucket"/>
</div>
- Let's go to the **uploadToS3** query and update the field values:
- **Operation**: Signed URL for upload
- **Bucket**: `{{components.dropdown1.value}}` this will fetch the dynamic value from the dropdown
- **Key**: `{{components.filepicker1.file[0].name}}` this will get the file name from the filepickers exposed variables
- **Expires in:** This sets an expiration time of URL, by default its `3600` seconds (1 hour)
- **Content Type**: `{{components.filepicker1.file[0].type}}` this will get the file type from the filepickers exposed variables
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/how-to/uses3presignedurl/upload.png" alt="Use S3 pre-signed URL to upload documents"/>
</div>
- Create two **RunJS** queries:
- Create a **runjs1** query and copy-paste the code below. This query gets the **base64data** from the file picker and convert the file's `base64Data` to into `BLOB`, and returns the file object.
```js
const base64String = components.filepicker1.file[0].base64Data
const decodedArray = new Uint8Array(atob(base64String).split('').map(c => c.charCodeAt(0)));
const file = new Blob([decodedArray], { type: components.filepicker1.file[0].type });
const fileName = components.filepicker1.file[0].name;
const fileObj = new File([file], fileName);
return fileObj
```
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/how-to/uses3presignedurl/runjs1.png" alt="Use S3 pre-signed URL to upload documents"/>
</div>
- Create another **runjs2** query and copy-paste the code below. This query gets the data(file object) returned by the first runjs query, the url returned by the **uploadToS3** query, and then makes PUT request.
```js
const file = queries.runjs2.data
const url = queries.s31.data.url
fetch(url, {
method: 'PUT',
body: file,
mode: 'cors',
headers: {
'Access-Control-Allow-Origin': '*',
'Content-Type': 'application/json'
}
})
.then(response => console.log('Upload successful!'))
.catch(error => console.error('Error uploading file:', error));
```
:::warning Enable Cross Origin Resource Sharing(CORS)
- For the file to be uploaded successfully, you will need to add the CORS policies from the **Permissions** tab of your **Bucket** settings. Here's a sample CORS:
```json
[
{
"AllowedHeaders": [
"*"
],
"AllowedMethods": [
"GET",
"PUT",
"POST"
],
"AllowedOrigins": [
"*"
],
"ExposeHeaders": []
}
]
```
:::
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/how-to/uses3presignedurl/runjs2.png" alt="Use S3 pre-signed URL to upload documents"/>
</div>
- Go to the **uploadToS3**, scroll down and add an event handler to the **uploadToS3** query. Select the **Query Success** event, **Run Query** as the action, and **runjs1** as the query to be triggered. **Save** the query.
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/how-to/uses3presignedurl/eventhandlerupload.png" alt="Use S3 pre-signed URL to upload documents"/>
</div>
- Let's go to the **runjs1** query and add the event handler to run a query on query success event, similar to how we did in the previous step. In the event handler, choose **runjs2** query. **Save** the query.
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/how-to/uses3presignedurl/eventhandlerrunjs2.png" alt="Use S3 pre-signed URL to upload documents"/>
</div>
- Now, let's go the final query **copySignedURL** that is connected to the table's action button. This query copy's the generated **Signed URL for download** onto the **clipboard**.
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/how-to/uses3presignedurl/copysigned.png" alt="Use S3 pre-signed URL to upload documents"/>
</div>
- Now that we have updated all the queries, and connected them through the event handlers. We can go ahead and pick a file from the file picker. Click on the file picker, select a file and then hit the **Upload file to S3** button.
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/how-to/uses3presignedurl/uploadbutton.png" alt="Use S3 pre-signed URL to upload documents"/>
</div>
- Once the button is clicked, the **uploadToS3** will triggered along with the **runjs1** and **runjs2** queries in sequence since we added them in the event handlers.
- You can go to the table and click on the **Copy signed URL** action button on the table, this will trigger the **copySignedURL** query and will copy the URL on the clipboard. You can go to another tab and paste the URL to open the file on the browser.

View file

@ -8,7 +8,7 @@ Permissions allow you to create and share resources to easily ensure what level
Admins can invite **Users** to their workspaces and assign them to the **Groups** that have Permissions to access Apps, folders, or workspace variables.
:::info
See **[Manage Users and Groups](/docs/tutorial/manage-users-groups)** to learn how to invite users to ToolJet.
See **[Manage Users and Groups](/docs/tutorial/manage-users-groups)** to know more about managing users and groups on your workspace.
:::
## Role-Based Access Control (RBAC) Glossary
@ -18,4 +18,4 @@ See **[Manage Users and Groups](/docs/tutorial/manage-users-groups)** to learn h
- **All Users** - Contains all the users in your workspace. When **New Users** are invited they are added to this group by default.
- **Admins** - Contains all Admins in your workspace. Everyone added to this group will Permission to access all the ToolJet resources.
- **Apps, Folder, Workspace Variables -** Resources that Admins can set permissions on.
- **Permissions -** Create, Update and Delete.
- **Permissions -** Create, Update and Delete.

View file

@ -77,7 +77,7 @@ Similar to archiving a user's access, you can enable it again by clicking on **U
## Managing Groups
On ToolJet, Admins can create groups for users added in a workspace and grant them access to particular app(s) with specific permissions. To manage groups, just go to the **Workspace Settings** from the left-sidebar of the dashboard and click on the **Groups**.
On ToolJet, Admins and Super Admins can create groups for users added in a workspace and grant them access to particular app(s) with specific permissions. To manage groups, just go to the **Workspace Settings** from the left-sidebar of the dashboard and click on the **Groups**.
<div style={{textAlign: 'center'}}>
@ -87,11 +87,16 @@ On ToolJet, Admins can create groups for users added in a workspace and grant th
### Group properties
Every group on ToolJet has three sections:
Every group on ToolJet has **four** sections:
- [Apps](#apps)
- [Users](#users)
- [Permissions](#permissions)
- [Datasources](#datasources)
#### Apps:
Admins can add or remove any number of apps for a group of users. To add an app to a group, select an app from the dropdown and click on `Add` button next to it. You can also set app permissions such as `View` or `Edit` for the group. You can set different permissions for different apps in a group.
Admins and Super Admins can add or remove any number of apps for a group of users. To add an app to a group, select an app from the dropdown and click on `Add` button next to it. You can also set app permissions such as `View` or `Edit` for the group. You can set different permissions for different apps in a group.
<div style={{textAlign: 'center'}}>
@ -101,7 +106,7 @@ Admins can add or remove any number of apps for a group of users. To add an app
#### Users:
Admins can add or remove any numbers of users in a group. Just select a user from the dropdown and click on `Add` button to add it to a group. To delete a user from a group, click on `Delete` button next to it.
Admins and Super Admins can add or remove any numbers of users in a group. Just select a user from the dropdown and click on `Add` button to add it to a group. To delete a user from a group, click on `Delete` button next to it.
<div style={{textAlign: 'center'}}>
@ -111,16 +116,30 @@ Admins can add or remove any numbers of users in a group. Just select a user fro
#### Permissions:
Admins can set granular permission like creating/deleting apps or creating folder for a group of users.
Admins and Super Admins can set granular permission for the users added in that particular group, such as:
- **Create** and **Delete** Apps
- **Create**, **Update**, and **Delete** Folders
- **Create**, **Update**, and **Delete** [Workspace Variables](/docs/tutorial/workspace-variables)
- **Create** and **Delete** [Global Datasources](/docs/data-sources/overview)
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/tutorial/manage-users-groups/permissionsv2.png" alt="permissions" />
<img className="screenshot-full" src="/img/tutorial/manage-users-groups/dspermission.png" alt="permissions" />
</div>
#### Datasources:
Only Admins and Super Admins can define what datasources can be **viewed** or **edited** by the users of that group.
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/tutorial/manage-users-groups/gdspermission.png" alt="permissions" />
</div>
:::tip
All the activities performed by any Admin or any user in a workspace is logged in `Audit logs` - including any activity related with managing users and groups.
All the activities performed by any Admin, Super Admin or any user in a workspace is logged in [Audit logs](/docs/Enterprise/audit_logs) - including any activity related with managing users and groups.
:::
### Predefined Groups

View file

@ -208,11 +208,13 @@
},
"items": [
"how-to/run-actions-from-runjs",
"how-to/intentionally-fail-js-query",
"how-to/run-query-at-specified-intervals",
"how-to/bulk-update-multiple-rows",
"how-to/access-cellvalue-rowdata",
"how-to/access-currentuser",
"how-to/oauth2-authorization",
"how-to/use-s3-signed-url-to-upload-docs",
"how-to/upload-files-aws",
"how-to/upload-files-gcs",
"how-to/access-users-location"
@ -321,4 +323,4 @@
]
}
]
}
}

View file

@ -289,10 +289,12 @@
"how-to/bulk-update-multiple-rows",
"how-to/access-currentuser",
"how-to/use-axios-in-runjs",
"how-to/intentionally-fail-js-query",
"how-to/import-external-libraries-using-runjs",
"how-to/run-actions-from-runjs",
"how-to/run-query-at-specified-intervals",
"how-to/access-users-location",
"how-to/use-s3-signed-url-to-upload-docs",
"how-to/s3-custom-endpoints",
"how-to/oauth2-authorization",
"how-to/upload-files-aws",

View file

@ -290,10 +290,12 @@
"how-to/access-currentuser",
"how-to/import-external-libraries-using-runjs",
"how-to/use-axios-in-runjs",
"how-to/intentionally-fail-js-query",
"how-to/import-external-libraries-using-runpy",
"how-to/run-actions-from-runjs",
"how-to/run-query-at-specified-intervals",
"how-to/access-users-location",
"how-to/use-s3-signed-url-to-upload-docs",
"how-to/s3-custom-endpoints",
"how-to/oauth2-authorization",
"how-to/upload-files-aws",

View file

@ -290,11 +290,13 @@
"how-to/bulk-update-multiple-rows",
"how-to/access-currentuser",
"how-to/use-axios-in-runjs",
"how-to/intentionally-fail-js-query",
"how-to/import-external-libraries-using-runpy",
"how-to/import-external-libraries-using-runjs",
"how-to/run-actions-from-runjs",
"how-to/run-query-at-specified-intervals",
"how-to/access-users-location",
"how-to/use-s3-signed-url-to-upload-docs",
"how-to/s3-custom-endpoints",
"how-to/oauth2-authorization",
"how-to/upload-files-aws",

View file

@ -293,8 +293,10 @@
"how-to/import-external-libraries-using-runpy",
"how-to/import-external-libraries-using-runjs",
"how-to/run-actions-from-runjs",
"how-to/intentionally-fail-js-query",
"how-to/run-query-at-specified-intervals",
"how-to/access-users-location",
"how-to/use-s3-signed-url-to-upload-docs",
"how-to/s3-custom-endpoints",
"how-to/oauth2-authorization",
"how-to/upload-files-aws",

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