Merge pull request #10579 from ToolJet/ce-release-group-cve

Release [CVE + Group Permissions]
This commit is contained in:
Midhun G S 2024-09-25 10:50:30 +05:30 committed by GitHub
commit 0b1ec0d7cd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
486 changed files with 63488 additions and 87649 deletions

View file

@ -48,6 +48,8 @@ GOOGLE_CLIENT_SECRET=
# EMAIL CONFIGURATION
DEFAULT_FROM_EMAIL=hello@tooljet.io
# Set this value true to get preview email on cevelopment env
SMTP_DISABLED=
SMTP_USERNAME=
SMTP_PASSWORD=
SMTP_DOMAIN=
@ -81,3 +83,12 @@ ENABLE_ONBOARDING_QUESTIONS_FOR_ALL_SIGN_UPS=
#session expiry in minutes
USER_SESSION_EXPIRY=
#Disable app embed feature, if true then private and public app embed is not allowed
DISABLE_APP_EMBED=
# if true then private app embed is allowed
ENABLE_PRIVATE_APP_EMBED=
#Enable cors else restricted to TOOLJET_HOST. Set the value true if you are serving front end from diffrent host
ENABLE_CORS=

View file

@ -1 +1 @@
2.66.2
2.67.0

View file

@ -23,5 +23,6 @@
],
"url": "https://raw.githubusercontent.com/ToolJet/ToolJet/develop/plugins/schemas/manifest.schema.json"
}
]
],
"CodeGPT.apiKey": "CodeGPT Plus Beta"
}

22
CODEOWNERS Normal file
View file

@ -0,0 +1,22 @@
# Code owners for specific package.json and package-lock.json files
/server/package.json @shah21 @gsmithun4 @adishm98
/server/package-lock.json @shah21 @gsmithun4 @adishm98
/frontend/package.json @shah21 @gsmithun4 @adishm98
/frontend/package-lock.json @shah21 @gsmithun4 @adishm98
/marketplace/package.json @shah21 @gsmithun4 @adishm98
/marketplace/package-lock.json @shah21 @gsmithun4 @adishm98
/cypress/package.json @shah21 @gsmithun4 @adishm98
/cypress/package-lock.json @shah21 @gsmithun4 @adishm98
/plugins/package.json @shah21 @gsmithun4 @adishm98
/plugins/package-lock.json @shah21 @gsmithun4 @adishm98
/package.json @shah21 @gsmithun4 @adishm98
/package-lock.json @shah21 @gsmithun4 @adishm98
# Server service files
/server/src/services/email.service.ts @shah21 @gsmithun4
/server/src/mails @shah21 @gsmithun4

9621
cli/package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -31,7 +31,7 @@
"eslint-plugin-prettier": "^3.4.1",
"globby": "^11",
"mocha": "^9",
"oclif": "^2.0.0-main.10",
"oclif": "^4.14.14",
"shx": "^0.3.3",
"ts-node": "^10.2.1",
"tslib": "^2.3.1",
@ -79,4 +79,4 @@
"version": "oclif readme && git add README.md"
},
"types": "dist/index.d.ts"
}
}

View file

@ -82,7 +82,7 @@ module.exports = defineConfig({
experimentalModfyObstructiveThirdPartyCode: true,
experimentalRunAllSpecs: true,
baseUrl: "http://localhost:8082",
specPattern: "cypress/e2e/**/*.cy.js",
specPattern: "cypress/e2e/happyPath/platform/commonTestcases/**/*.cy.js",
downloadsFolder: "cypress/downloads",
numTestsKeptInMemory: 0,
redirectionLimit: 10,
@ -93,5 +93,6 @@ module.exports = defineConfig({
codeCoverageTasksRegistered: true,
video: false,
videoUploadOnPasses: false,
experimentalStudio: true,
},
});

View file

@ -189,6 +189,7 @@ Cypress.Commands.add("userInviteApi", (userName, userEmail) => {
first_name: userName,
email: userEmail,
groups: [],
role: "end-user",
},
},
{ log: false }

View file

@ -373,9 +373,9 @@ export const commonWidgetSelector = {
modalCloseButton: '[data-cy="modal-close-button"]',
iframeLinkLabel: '[data-cy="iframe-link-label"]',
ifameLinkCopyButton: '[data-cy="iframe-link-copy-button"]',
appSlugLabel: '[data-cy="app-slug-label"]',
appSlugLabel: '[data-cy="input-field-label"]',
appSlugInput: '[data-cy="app-slug-input-field"]',
appSlugInfoLabel: '[data-cy="app-slug-info-label"]',
appSlugInfoLabel: '[data-cy="helper-text"]',
appLinkLabel: '[data-cy="app-link-label"]',
appLinkField: '[data-cy="app-link-field"]',
appSlugErrorLabel: '[data-cy="app-slug-error-label"]',

View file

@ -6,7 +6,7 @@ export const groupsSelector = {
createNewGroupButton: "[data-cy=create-new-group-button]",
tableHeader: "[data-cy=table-header]",
groupName: "[data-cy=group-name]",
addNewGroupModalTitle: '[data-cy="add-new-group-title"]',
addNewGroupModalTitle: '[data-cy="create-new-group-title"]',
groupNameInput: "[data-cy=group-name-input]",
cancelButton: "[data-cy=cancel-button]",
createGroupButton: "[data-cy=create-group-button]",

View file

@ -5,7 +5,8 @@ export const usersSelector = {
buttonAddUsers: "[data-cy=button-invite-new-user]",
usersElements: {
usersTableNameColumnHeader: '[data-cy="users-table-name-column-header"]',
usersTableEmailColumnHeader: '[data-cy="users-table-email-column-header"]',
usersTableRolesColumnHeader: '[data-cy="users-table-roles-column-header"]',
usersTableGroupsColumnHeader: '[data-cy="users-table-groups-column-header"]',
usersTableStatusColumnHeader:
'[data-cy="users-table-status-column-header"]',
usersFilterLabel: '[data-cy="users-filter-label"]',

View file

@ -4,7 +4,7 @@ export const groupsText = {
tableHeader: "Name",
allUsers: "All users",
admin: "Admin",
cardTitle: "Add new group",
cardTitle: "Create new group",
cancelButton: "Cancel",
createGroupButton: "Create Group",
groupNameExistToast: "Group name already exist",

View file

@ -1,7 +1,8 @@
export const usersText = {
usersElements: {
usersTableNameColumnHeader: "NAME",
usersTableEmailColumnHeader: "EMAIL",
usersTableRolesColumnHeader: "User role",
usersTableGroupsColumnHeader: "Custom groups",
usersTableStatusColumnHeader: "STATUS",
usersFilterLabel: "Showing",
},

View file

@ -50,20 +50,22 @@ describe("App slug", () => {
"have.text",
"App slug can't be empty"
);
cy.clearAndType(commonWidgetSelector.appSlugInput, "_2#");
cy.wait(500)
cy.get(commonWidgetSelector.appSlugErrorLabel).verifyVisibleElement(
"have.text",
"Special characters are not accepted."
);
cy.clearAndType(commonWidgetSelector.appSlugInput, "t ");
cy.wait(500)
cy.get(commonWidgetSelector.appSlugErrorLabel).verifyVisibleElement(
"have.text",
"Cannot contain spaces"
);
cy.clearAndType(commonWidgetSelector.appSlugInput, "T");
cy.wait(500)
cy.get(commonWidgetSelector.appSlugErrorLabel).verifyVisibleElement(
"have.text",
"Only lowercase letters are accepted."
@ -71,7 +73,8 @@ describe("App slug", () => {
cy.get(commonWidgetSelector.appSlugInput).clear();
cy.clearAndType(commonWidgetSelector.appSlugInput, data.slug);
cy.get(commonWidgetSelector.appSlugErrorLabel).verifyVisibleElement(
cy.wait(500)
cy.get('[data-cy="app-slug-accepted-label"]').verifyVisibleElement(
"have.text",
"Slug accepted!"
);
@ -112,6 +115,7 @@ describe("App slug", () => {
cy.get(commonSelectors.leftSideBarSettingsButton).click();
cy.get(commonWidgetSelector.appSlugInput).clear();
cy.clearAndType(commonWidgetSelector.appSlugInput, data.slug);
cy.wait(500)
cy.get(commonWidgetSelector.appSlugErrorLabel).verifyVisibleElement(
"have.text",
"This app slug is already taken."

View file

@ -26,7 +26,7 @@ describe(
const data = {};
beforeEach(() => {
cy.defaultWorkspaceLogin();
cy.removeAssignedApps();
// cy.removeAssignedApps();
cy.skipWalkthrough();
});

View file

@ -12,10 +12,8 @@ const newGroupname = `New ${groupName}`;
describe("Manage Groups", () => {
beforeEach(() => {
cy.defaultWorkspaceLogin();
permissions.reset("all_users");
});
it("Should verify the elements and functionalities on manage groups page", () => {
cy.removeAssignedApps();
common.navigateToManageGroups();
cy.get(commonSelectors.breadcrumbTitle).should(($el) => {
expect($el.contents().first().text().trim()).to.eq("Workspace settings");
@ -26,6 +24,21 @@ describe("Manage Groups", () => {
);
groups.manageGroupsElements();
cy.get(groupsSelector.createNewGroupButton).should("be.visible").click();
cy.get(groupsSelector.addNewGroupModalTitle).verifyVisibleElement(
"have.text",
groupsText.cardTitle
);
cy.get(groupsSelector.groupNameInput).should("be.visible");
cy.get(groupsSelector.cancelButton).verifyVisibleElement(
"have.text",
groupsText.cancelButton
);
cy.get(groupsSelector.createGroupButton).verifyVisibleElement(
"have.text",
groupsText.createGroupButton
);
cy.get(groupsSelector.cancelButton).click();
cy.get(groupsSelector.createNewGroupButton).click();
cy.clearAndType(groupsSelector.groupNameInput, groupsText.admin);
@ -53,24 +66,16 @@ describe("Manage Groups", () => {
cy.get(groupsSelector.groupPageTitle(groupName)).verifyVisibleElement(
"have.text",
groupName
`${groupName} (0)`
);
cy.get('[data-cy="group-name-update-link"]').should("be.visible");
groups.OpenGroupCardOption(groupName);
cy.get(groupsSelector.updateGroupNameLink(groupName)).verifyVisibleElement(
"have.text",
groupsText.editGroupNameButton
);
cy.get(groupsSelector.deleteGroupOption).verifyVisibleElement(
"have.text",
groupsText.deleteGroupButton
);
cy.get(groupsSelector.appsLink).verifyVisibleElement(
"have.text",
groupsText.appsLink
);
cy.get(groupsSelector.usersLink).verifyVisibleElement(
"have.text",
groupsText.usersLink
@ -79,30 +84,10 @@ describe("Manage Groups", () => {
"have.text",
groupsText.permissionsLink
);
cy.get(groupsSelector.appsLink).click();
cy.get(groupsSelector.searchBox).should("be.visible");
cy.get(groupsSelector.selectAddButton).verifyVisibleElement(
cy.get('[data-cy="granular-access-link"]').verifyVisibleElement(
"have.text",
groupsText.addButton
"Granular access"
);
cy.get(groupsSelector.nameTableHeader).verifyVisibleElement(
"have.text",
groupsText.textAppName
);
cy.get(groupsSelector.permissionstableHedaer).verifyVisibleElement(
"have.text",
groupsText.permissionstableHedaer
);
cy.get(groupsSelector.helperTextNoAppsAdded)
.eq(0)
.verifyVisibleElement("have.text", groupsText.helperTextNoAppsAdded);
cy.get(groupsSelector.searchBox).should("be.visible");
cy.get(groupsSelector.usersLink).click();
cy.get(groupsSelector.nameTableHeader).verifyVisibleElement(
"have.text",
@ -112,6 +97,15 @@ describe("Manage Groups", () => {
"have.text",
groupsText.emailTableHeader
);
cy.get('[data-cy="user-empty-page-icon"]').should("be.visible");
cy.get('[data-cy="user-empty-page"]').verifyVisibleElement(
"have.text",
"No users added yet"
);
cy.get('[data-cy="user-empty-page-info-text"]').verifyVisibleElement(
"have.text",
"Add users to this group to configure permissions for them!"
);
cy.get(groupsSelector.permissionsLink).click();
cy.get(groupsSelector.resourcesApps).verifyVisibleElement(
@ -127,7 +121,13 @@ describe("Manage Groups", () => {
"have.text",
groupsText.resourcesApps
);
cy.get(groupsSelector.appsCreateCheck).should("be.visible").check();
cy.get(groupsSelector.appsCreateCheck).should("be.visible");
cy.get(groupsSelector.appsCreateCheck).check();
cy.verifyToastMessage(
commonSelectors.toastMessage,
groupsText.permissionUpdatedToast
);
cy.get(groupsSelector.appsCreateCheck).uncheck();
cy.verifyToastMessage(
commonSelectors.toastMessage,
groupsText.permissionUpdatedToast
@ -136,37 +136,277 @@ describe("Manage Groups", () => {
"have.text",
groupsText.createLabel
);
cy.get(groupsSelector.appsCreateCheck).uncheck();
cy.get(groupsSelector.appsDeleteCheck).should("be.visible").check();
cy.get('[data-cy="app-create-helper-text"]').verifyVisibleElement(
"have.text",
"Create apps in this workspace"
);
cy.get(groupsSelector.appsDeleteCheck).should("be.visible");
cy.get(groupsSelector.appsDeleteLabel).verifyVisibleElement(
"have.text",
groupsText.deleteLabel
);
cy.get('[data-cy="app-delete-helper-text"]').verifyVisibleElement(
"have.text",
"Delete any app in this workspace"
);
cy.get(groupsSelector.appsDeleteCheck).check();
cy.verifyToastMessage(
commonSelectors.toastMessage,
groupsText.permissionUpdatedToast
);
cy.get(groupsSelector.appsDeleteCheck).uncheck();
cy.verifyToastMessage(
commonSelectors.toastMessage,
groupsText.permissionUpdatedToast
);
cy.get(groupsSelector.resourcesFolders).verifyVisibleElement(
"have.text",
groupsText.resourcesFolders
);
cy.get(groupsSelector.foldersCreateCheck).should("be.visible").check();
cy.get(groupsSelector.foldersCreateCheck).should("be.visible");
cy.get(groupsSelector.foldersCreateLabel).verifyVisibleElement(
"have.text",
groupsText.folderCreateLabel
);
cy.get('[data-cy="folder-helper-text"]').verifyVisibleElement(
"have.text",
"All operations on folders"
);
cy.get(groupsSelector.foldersCreateCheck).check();
cy.verifyToastMessage(
commonSelectors.toastMessage,
groupsText.permissionUpdatedToast
);
cy.get(groupsSelector.foldersCreateCheck).uncheck();
cy.verifyToastMessage(
commonSelectors.toastMessage,
groupsText.permissionUpdatedToast
);
cy.get(groupsSelector.resourcesWorkspaceVar).verifyVisibleElement(
"have.text",
groupsText.resourcesWorkspaceVar
);
cy.get(groupsSelector.workspaceVarCheckbox).should("be.visible");
cy.get('[data-cy="workspace-constants-helper-text"]').verifyVisibleElement(
"have.text",
"All operations on workspace constants"
);
cy.get(groupsSelector.workspaceVarCheckbox).check();
cy.verifyToastMessage(
commonSelectors.toastMessage,
groupsText.permissionUpdatedToast
);
cy.get(groupsSelector.workspaceVarCheckbox).uncheck();
cy.verifyToastMessage(
commonSelectors.toastMessage,
groupsText.permissionUpdatedToast
);
cy.get(groupsSelector.updateGroupNameLink(groupName)).click();
cy.get('[data-cy="granular-access-link"]').click();
cy.get('[data-cy="add-apps-buton"]').click();
cy.get('[data-cy="modal-title"]:eq(2)').verifyVisibleElement(
"have.text",
"Add app permissions"
);
cy.get('[data-cy="modal-close-button"]').should("be.visible").click();
cy.get('[data-cy="add-apps-buton"]').click();
cy.get('[data-cy="modal-title"]:eq(2)').verifyVisibleElement(
"have.text",
"Add app permissions"
);
cy.get('[data-cy="permission-name-label"]').verifyVisibleElement(
"have.text",
"Permission name"
);
cy.get('[data-cy="permission-name-input"]')
.should("be.visible")
.and("have.attr", "placeholder", "Eg. Product analytics apps");
cy.get('[data-cy="permission-name-help-text"]').verifyVisibleElement(
"have.text",
"Permission name must be unique and max 50 characters"
);
cy.get('[data-cy="permission-label"]').verifyVisibleElement(
"have.text",
"Permission"
);
cy.get('[data-cy="edit-permission-radio"]').should("be.visible");
cy.get('[data-cy="edit-permission-label"]').verifyVisibleElement(
"have.text",
"Edit"
);
cy.get('[data-cy="edit-permission-info-text"]').verifyVisibleElement(
"have.text",
"Access to app builder"
);
cy.get('[data-cy="view-permission-radio"]')
.should("be.visible")
.and("be.checked");
cy.get('[data-cy="view-permission-label"]').verifyVisibleElement(
"have.text",
"View"
);
cy.get('[data-cy="view-permission-info-text"]').verifyVisibleElement(
"have.text",
"Only access released version of apps"
);
cy.get('[data-cy="hide-from-dashboard-permission-input"]').should(
"be.visible"
);
cy.get(
'[data-cy="hide-from-dashboard-permission-label"]'
).verifyVisibleElement("have.text", "Hide from dashboard");
cy.get(
'[data-cy="hide-from-dashboard-permission-info-text"]'
).verifyVisibleElement("have.text", "App will be accessible by URL only");
cy.get('[data-cy="resource-label"]').verifyVisibleElement(
"have.text",
"Resources"
);
cy.get('[data-cy="all-apps-radio"]').should("be.visible").and("be.checked");
cy.get('[data-cy="all-apps-label"]').verifyVisibleElement(
"have.text",
"All apps"
);
cy.get('[data-cy="all-apps-info-text"]').verifyVisibleElement(
"have.text",
"This will select all apps in the workspace including any new apps created"
);
cy.get('[ data-cy="custom-radio"]').should("be.visible");
cy.get('[data-cy="custom-label"]').verifyVisibleElement(
"have.text",
"Custom"
);
cy.get('[data-cy="custom-info-text"]').verifyVisibleElement(
"have.text",
"Select specific applications you want to add to the group"
);
cy.get('[data-cy="resources-container"]>>>>').should("be.visible");
cy.get('[data-cy="confim-button"]').verifyVisibleElement(
"have.text",
"Add"
);
cy.get('[data-cy="confim-button"]').should('be.disabled')
cy.get('[data-cy="cancel-button"]')
.verifyVisibleElement("have.text", "Cancel")
.click();
cy.get('[data-cy="add-apps-buton"]').click();
cy.clearAndType('[data-cy="permission-name-input"]', groupName)
cy.get('[data-cy="confim-button"]').click()
cy.get(`[data-cy="${groupName.toLowerCase()}-text"]`).click();
cy.get('[data-cy="modal-title"]:eq(2)').verifyVisibleElement(
"have.text",
"Edit app permissions"
);
cy.get('[data-cy="delete-button"]').should('be.visible');
cy.get('[data-cy="modal-close-button"]').should("be.visible").click();
cy.get(`[data-cy="${groupName.toLowerCase()}-text"]`).click();
cy.get('[data-cy="modal-title"]:eq(2)').verifyVisibleElement(
"have.text",
"Edit app permissions"
);
cy.get('[data-cy="permission-name-label"]').verifyVisibleElement(
"have.text",
"Permission name"
);
cy.get('[data-cy="permission-name-input"]')
.should("be.visible")
.and("have.value", groupName);
cy.get('[data-cy="permission-name-help-text"]').verifyVisibleElement(
"have.text",
"Permission name must be unique and max 50 characters"
);
cy.get('[data-cy="permission-label"]').verifyVisibleElement(
"have.text",
"Permission"
);
cy.get('[data-cy="edit-permission-radio"]').should("be.visible").check();
cy.get('[data-cy="edit-permission-label"]').verifyVisibleElement(
"have.text",
"Edit"
);
cy.get('[data-cy="edit-permission-info-text"]').verifyVisibleElement(
"have.text",
"Access to app builder"
);
cy.get('[data-cy="view-permission-radio"]')
.should("be.visible")
.and("not.be.checked");
cy.get('[data-cy="view-permission-label"]').verifyVisibleElement(
"have.text",
"View"
);
cy.get('[data-cy="view-permission-info-text"]').verifyVisibleElement(
"have.text",
"Only access released version of apps"
);
cy.get('[data-cy="hide-from-dashboard-permission-input"]').should(
"be.visible"
);
cy.get(
'[data-cy="hide-from-dashboard-permission-label"]'
).verifyVisibleElement("have.text", "Hide from dashboard");
cy.get(
'[data-cy="hide-from-dashboard-permission-info-text"]'
).verifyVisibleElement("have.text", "App will be accessible by URL only");
cy.get('[data-cy="resource-label"]').verifyVisibleElement(
"have.text",
"Resources"
);
cy.get('[data-cy="all-apps-radio"]').should("be.visible").and("be.checked");
cy.get('[data-cy="all-apps-label"]').verifyVisibleElement(
"have.text",
"All apps"
);
cy.get('[data-cy="all-apps-info-text"]').verifyVisibleElement(
"have.text",
"This will select all apps in the workspace including any new apps created"
);
cy.get('[ data-cy="custom-radio"]').should("be.visible");
cy.get('[data-cy="custom-label"]').verifyVisibleElement(
"have.text",
"Custom"
);
cy.get('[data-cy="custom-info-text"]').verifyVisibleElement(
"have.text",
"Select specific applications you want to add to the group"
);
cy.get('[data-cy="resources-container"]>>>>').should("be.visible");
cy.get('[data-cy="confim-button"]').verifyVisibleElement(
"have.text",
"Update"
);
cy.get('[data-cy="confim-button"]').should('be.enabled')
cy.get('[data-cy="cancel-button"]')
.verifyVisibleElement("have.text", "Cancel")
.click();
cy.get(`[data-cy="${groupName.toLowerCase()}-text"]`).click();
cy.clearAndType('[data-cy="permission-name-input"]', groupName)
cy.get('[data-cy="edit-permission-radio"]').check();
cy.get('[data-cy="confim-button"]').click()
cy.get('[data-cy="group-name-update-link"]').click();
cy.get(groupsSelector.updateGroupNameModalTitle).verifyVisibleElement(
"have.text",
groupsText.updateGroupNameModalTitle
@ -182,7 +422,7 @@ describe("Manage Groups", () => {
);
cy.get(groupsSelector.cancelButton).click();
cy.get(groupsSelector.updateGroupNameLink(groupName)).click();
cy.get('[data-cy="group-name-update-link"]').click();
cy.clearAndType(groupsSelector.groupNameInput, newGroupname);

View file

@ -8,6 +8,7 @@ import {
confirmInviteElements,
selectUserGroup,
inviteUserWithUserGroups,
inviteUserWithUserRole,
fetchAndVisitInviteLink,
} from "Support/utils/manageUsers";
import {
@ -23,7 +24,6 @@ import { addNewUser, visitWorkspaceInvitation } from "Support/utils/onboarding";
import { commonText } from "Texts/common";
const data = {};
data.groupName = fake.firstName.replaceAll("[^A-Za-z]", "");
describe("Manage Users", () => {
beforeEach(() => {
@ -45,31 +45,17 @@ describe("Manage Users", () => {
cy.get(usersSelector.usersPageTitle).should("be.visible");
cy.get(usersSelector.buttonAddUsers).click();
cy.get(usersSelector.buttonInviteUsers).click();
cy.get(usersSelector.fullNameError).verifyVisibleElement(
"have.text",
usersText.errorTextFieldRequired
);
cy.get(usersSelector.emailError).verifyVisibleElement(
"have.text",
usersText.errorTextFieldRequired
);
cy.get(usersSelector.buttonInviteUsers).should('be.disabled');
cy.clearAndType(commonSelectors.inputFieldFullName, data.firstName);
cy.clearAndType(commonSelectors.inputFieldEmailAddress, data.email);
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.errorTextFieldRequired
"Email is not valid"
);
cy.get(usersSelector.buttonInviteUsers).should('be.disabled');
cy.clearAndType(commonSelectors.inputFieldFullName, data.firstName);
cy.clearAndType(
@ -78,16 +64,18 @@ describe("Manage Users", () => {
);
cy.get(usersSelector.buttonInviteUsers).click();
cy.get(commonSelectors.newToastMessage).should(
"have.text",
usersText.exsitingEmail
);
cy.get('[data-cy="modal-icon"]').should('be.visible')
cy.get('[data-cy="modal-header"]').verifyVisibleElement("have.text", "Duplicate email");
cy.get(commonSelectors.modalMessage).verifyVisibleElement("have.text", "Duplicate email found. Please provide a unique email address.")
cy.get('[data-cy="close-button"]:eq(1)').should('be.visible').click();
cy.get(commonSelectors.inputFieldEmailAddress).should("have.value", usersText.adminUserEmail)
});
it("Should verify the confirm invite page and new user account", () => {
data.firstName = fake.firstName;
data.email = fake.email.toLowerCase().replaceAll("[^A-Za-z]", "");
cy.removeAssignedApps();
// cy.removeAssignedApps();
navigateToManageUsers();
fillUserInviteForm(data.firstName, data.email);
@ -208,6 +196,10 @@ describe("Manage Users", () => {
it("Should verify the user onboarding with groups", () => {
data.firstName = fake.firstName;
data.email = fake.email.toLowerCase().replaceAll("[^A-Za-z]", "");
data.groupName1 = fake.firstName.replaceAll("[^A-Za-z]", "");
data.groupName2 = fake.firstName.replaceAll("[^A-Za-z]", "");
const groupNames = ["All users", "Admin"];
navigateToManageUsers();
@ -233,13 +225,10 @@ describe("Manage Users", () => {
cy.get(commonSelectors.cancelButton).click();
cy.get(usersSelector.buttonAddUsers).click();
cy.get(".css-1jqq78o-placeholder").should(
"have.text",
"Select groups to add for this user"
);
cy.get('.selected-value').should('have.text', "End-user")
cy.get(commonSelectors.cancelButton).click();
inviteUserWithUserGroups(data.firstName, data.email, "All users", "Admin");
inviteUserWithUserRole(data.firstName, data.email, "Admin");
navigateToManageGroups();
cy.get(groupsSelector.groupLink("Admin")).click();
@ -250,25 +239,31 @@ describe("Manage Users", () => {
data.email = fake.email.toLowerCase().replaceAll("[^A-Za-z]", "");
cy.get(groupsSelector.createNewGroupButton).click();
cy.clearAndType(groupsSelector.groupNameInput, data.groupName);
cy.clearAndType(groupsSelector.groupNameInput, data.groupName1);
cy.get(groupsSelector.createGroupButton).click();
cy.verifyToastMessage(
commonSelectors.toastMessage,
groupsText.groupCreatedToast
);
cy.get(groupsSelector.createNewGroupButton).click();
cy.clearAndType(groupsSelector.groupNameInput, data.groupName2);
cy.get(groupsSelector.createGroupButton).click();
navigateToManageUsers();
inviteUserWithUserGroups(
data.firstName,
data.email,
"All users",
data.groupName
data.groupName1,
data.groupName2
);
logout();
cy.defaultWorkspaceLogin();
navigateToManageGroups();
cy.get(groupsSelector.groupLink(data.groupName)).click();
cy.get(groupsSelector.groupLink(data.groupName1)).click();
cy.get(groupsSelector.usersLink).click();
cy.get(groupsSelector.userRow(data.email)).should("be.visible");
cy.get(groupsSelector.groupLink(data.groupName2)).click();
cy.get(groupsSelector.usersLink).click();
cy.get(groupsSelector.userRow(data.email)).should("be.visible");
});
@ -341,13 +336,55 @@ describe("Manage Users", () => {
cy.get('[data-cy="group-check-input"]').eq(0).check();
cy.get(usersSelector.buttonInviteUsers).click();
cy.get('[data-cy="modal-title"] > .tj-text-md').verifyVisibleElement("have.text", "Edit user role")
cy.get('[data-cy="user-email"]').verifyVisibleElement("have.text", data.email);
cy.get('[data-cy="modal-body"]>').verifyVisibleElement("have.text", "Are you sure you want to continue?");
cy.get('.modal-footer > [data-cy="cancel-button"]').verifyVisibleElement("have.text", "Cancel");
cy.get('[data-cy="confim-button"]').verifyVisibleElement("have.text", "Continue");
cy.get('[data-cy="modal-close-button"]').should('be.visible').click();
cy.get(usersSelector.userActionButton).click();
cy.get(usersSelector.editUserDetailsButton).click();
cy.get('[data-cy="user-group-select"]>>>>>').eq(0).type("Admin");
cy.wait(1000);
cy.get('[data-cy="group-check-input"]').eq(0).check();
cy.get(commonSelectors.cancelButton).click();
cy.get(usersSelector.userActionButton).click();
cy.get(usersSelector.editUserDetailsButton).click();
cy.get('[data-cy="user-group-select"]>>>>>').eq(0).type("Admin");
cy.wait(1000);
cy.get('[data-cy="group-check-input"]').eq(0).check();
cy.get(usersSelector.buttonInviteUsers).click();
cy.get('.modal-footer > [data-cy="cancel-button"]').click()
cy.get(usersSelector.userActionButton).click();
cy.get(usersSelector.editUserDetailsButton).click();
cy.get('[data-cy="user-group-select"]>>>>>').eq(0).type("Admin");
cy.wait(1000);
cy.get('[data-cy="group-check-input"]').eq(0).check();
cy.get(commonSelectors.cancelButton).click();
cy.get(usersSelector.userActionButton).click();
cy.get(usersSelector.editUserDetailsButton).click();
cy.get('[data-cy="user-group-select"]>>>>>').eq(0).type("Admin");
cy.wait(1000);
cy.get('[data-cy="group-check-input"]').eq(0).check();
cy.get(usersSelector.buttonInviteUsers).click();
cy.get('[data-cy="confim-button"]').click()
cy.verifyToastMessage(
commonSelectors.toastMessage,
"User has been updated"
);
searchUser(data.email);
cy.get(usersSelector.groupChip).eq(1).should("have.text", "Admin");
cy.get('[data-name="role-header"] [data-cy="group-chip"]').should("have.text", "Admin");
});
it("Should verify exisiting user invite flow", () => {

View file

@ -2,34 +2,39 @@ import { groupsSelector } from "Selectors/manageGroups";
import { groupsText } from "Texts/manageGroups";
import { commonSelectors } from "Selectors/common";
import { commonText } from "Texts/common";
import { navigateToAllUserGroup, createGroup, navigateToManageGroups } from "Support/utils/common";
import { cyParamName } from "../../constants/selectors/common";
import {
navigateToAllUserGroup,
createGroup,
navigateToManageGroups,
} from "Support/utils/common";
import { cyParamName } from "Selectors/common";
export const manageGroupsElements = () => {
cy.get(groupsSelector.groupLink("All users")).verifyVisibleElement(
cy.get('[data-cy="page-title"]').should(($el) => {
expect($el.contents().last().text().trim()).to.eq("Groups");
});
cy.get('[data-cy="admin-list-item"]').verifyVisibleElement(
"have.text",
groupsText.allUsers
"Admin"
);
cy.get('[data-cy="user-role-title"]').verifyVisibleElement(
"have.text",
"USER ROLE"
);
cy.get('[data-cy="admin-title"]').verifyVisibleElement(
"have.text",
"Admin (1)"
);
cy.get(groupsSelector.groupLink("Admin")).verifyVisibleElement(
"have.text",
groupsText.admin
);
navigateToAllUserGroup();
cy.get(groupsSelector.groupPageTitle("All Users")).verifyVisibleElement(
"have.text",
groupsText.allUsers
);
cy.get(groupsSelector.createNewGroupButton).verifyVisibleElement(
"have.text",
groupsText.createNewGroupButton
);
cy.get(groupsSelector.appsLink).verifyVisibleElement(
"have.text",
groupsText.appsLink
);
cy.get(groupsSelector.usersLink).verifyVisibleElement(
"have.text",
groupsText.usersLink
@ -38,66 +43,183 @@ export const manageGroupsElements = () => {
"have.text",
groupsText.permissionsLink
);
cy.get('[data-cy="granular-access-link"]').verifyVisibleElement(
"have.text",
"Granular access"
);
cy.get(groupsSelector.appsLink).click();
// cy.get(groupsSelector.appsLink).click();
cy.get(groupsSelector.textDefaultGroup).verifyVisibleElement(
"have.text",
groupsText.textDefaultGroup
);
cy.get(groupsSelector.searchBox).should("be.visible");
cy.get(groupsSelector.selectAddButton).verifyVisibleElement(
"have.text",
groupsText.addButton
);
cy.get(groupsSelector.nameTableHeader).verifyVisibleElement(
"have.text",
groupsText.textAppName
groupsText.userNameTableHeader
);
cy.get(groupsSelector.emailTableHeader).verifyVisibleElement(
"have.text",
groupsText.emailTableHeader
);
cy.get(groupsSelector.permissionsLink).click();
cy.get('[data-cy="helper-text-admin-app-access"]')
.eq(0)
.verifyVisibleElement(
"have.text",
" Admin has edit access to all apps. These are not editableread documentation to know more !"
);
cy.get(groupsSelector.resourcesApps).verifyVisibleElement(
"have.text",
groupsText.resourcesApps
);
cy.get(groupsSelector.permissionstableHedaer).verifyVisibleElement(
"have.text",
groupsText.permissionstableHedaer
);
cy.get("body").then(($title) => {
if ($title.find(groupsSelector.helperTextNoAppsAdded).length > 0) {
cy.get(groupsSelector.helperTextNoAppsAdded)
.eq(0)
.verifyVisibleElement("have.text", groupsText.helperTextNoAppsAdded);
cy.get(groupsSelector.helperTextPermissions)
.eq(0)
.verifyVisibleElement("have.text", groupsText.helperTextPermissions);
}
});
cy.get(groupsSelector.createNewGroupButton).should("be.visible").click();
cy.get(groupsSelector.addNewGroupModalTitle).verifyVisibleElement(
cy.get(groupsSelector.resourcesApps).verifyVisibleElement(
"have.text",
groupsText.cardTitle
);
cy.get(groupsSelector.groupNameInput).should("be.visible");
cy.get(groupsSelector.cancelButton).verifyVisibleElement(
"have.text",
groupsText.cancelButton
);
cy.get(groupsSelector.createGroupButton).verifyVisibleElement(
"have.text",
groupsText.createGroupButton
);
cy.get(groupsSelector.cancelButton).click();
cy.get(groupsSelector.helperTextAllUsersIncluded).verifyVisibleElement(
"have.text",
groupsText.helperTextAllUsersIncluded
groupsText.resourcesApps
);
// cy.get(groupsSelector.usersLink).click();
// cy.get(groupsSelector.helperTextAllUsersIncluded).verifyVisibleElement(
// "have.text",
// groupsText.helperTextAllUsersIncluded
// );
cy.get(groupsSelector.appsCreateCheck)
.should("be.visible")
.and("have.attr", "disabled");
cy.get(groupsSelector.appsCreateLabel).verifyVisibleElement(
"have.text",
groupsText.createLabel
);
cy.get('[data-cy="app-create-helper-text"]').verifyVisibleElement(
"have.text",
"Create apps in this workspace"
);
cy.get(groupsSelector.appsDeleteCheck)
.should("be.visible")
.and("have.attr", "disabled");
cy.get(groupsSelector.appsDeleteLabel).verifyVisibleElement(
"have.text",
groupsText.deleteLabel
);
cy.get('[data-cy="app-delete-helper-text"]').verifyVisibleElement(
"have.text",
"Delete any app in this workspace"
);
cy.get(groupsSelector.resourcesFolders).verifyVisibleElement(
"have.text",
groupsText.resourcesFolders
);
cy.get(groupsSelector.foldersCreateCheck)
.should("be.visible")
.and("have.attr", "disabled");
cy.get(groupsSelector.foldersCreateLabel).verifyVisibleElement(
"have.text",
groupsText.folderCreateLabel
);
cy.get('[data-cy="folder-helper-text"]').verifyVisibleElement(
"have.text",
"All operations on folders"
);
cy.get(groupsSelector.resourcesWorkspaceVar).verifyVisibleElement(
"have.text",
groupsText.resourcesWorkspaceVar
);
cy.get(groupsSelector.workspaceVarCheckbox)
.should("be.visible")
.and("have.attr", "disabled");
cy.get('[data-cy="workspace-constants-helper-text"]').verifyVisibleElement(
"have.text",
"All operations on workspace constants"
);
cy.get('[data-cy="granular-access-link"]').click();
cy.get(groupsSelector.nameTableHeader).verifyVisibleElement(
"have.text",
"Name"
);
cy.get(groupsSelector.permissionstableHedaer).verifyVisibleElement(
"have.text",
"Permission"
);
cy.get('[data-cy="resource-header"]:eq(1)').verifyVisibleElement(
"have.text",
"Resource"
);
cy.get('[data-cy="apps-text"]').verifyVisibleElement("have.text", " Apps");
cy.get('[data-cy="app-edit-radio"]')
.should("be.visible")
.and("have.attr", "disabled");
cy.get('[data-cy="app-edit-radio"]').should("be.checked");
cy.get('[data-cy="app-edit-label"]').verifyVisibleElement(
"have.text",
"Edit"
);
cy.get('[data-cy="app-edit-helper-text"]').verifyVisibleElement(
"have.text",
"Access to app builder"
);
cy.get('[data-cy="app-view-radio"]')
.should("be.visible")
.and("have.attr", "disabled");
cy.get('[data-cy="app-view-label"]').verifyVisibleElement(
"have.text",
"View"
);
cy.get('[data-cy="app-view-helper-text"]').verifyVisibleElement(
"have.text",
"Only access released version of apps"
);
cy.get('[data-cy="app-hide-from-dashboard-radio"]')
.should("be.visible")
.and("have.attr", "disabled");
cy.get(
'[data-cy="app-hide-from-dashboard-helper-text"]'
).verifyVisibleElement("have.text", "App will be accessible by URL only");
cy.get('[data-cy="group-chip"]').verifyVisibleElement(
"have.text",
"All apps"
);
cy.get('[data-cy="add-apps-buton"]').verifyVisibleElement(
"have.text",
"Add apps"
);
cy.get(groupsSelector.groupLink("Builder")).click();
cy.get(groupsSelector.groupLink("Builder")).verifyVisibleElement(
"have.text",
"Builder"
);
cy.get('[data-cy="builder-title"]').verifyVisibleElement(
"have.text",
"Builder (1)"
);
cy.get(groupsSelector.createNewGroupButton).verifyVisibleElement(
"have.text",
groupsText.createNewGroupButton
);
cy.get(groupsSelector.usersLink).verifyVisibleElement(
"have.text",
groupsText.usersLink
);
cy.get(groupsSelector.permissionsLink).verifyVisibleElement(
"have.text",
groupsText.permissionsLink
);
cy.get('[data-cy="granular-access-link"]').verifyVisibleElement(
"have.text",
"Granular access"
);
cy.get(groupsSelector.usersLink).click();
cy.get(groupsSelector.nameTableHeader).verifyVisibleElement(
"have.text",
groupsText.userNameTableHeader
@ -121,7 +243,13 @@ export const manageGroupsElements = () => {
"have.text",
groupsText.resourcesApps
);
cy.get(groupsSelector.appsCreateCheck).should("be.visible").check();
cy.get(groupsSelector.appsCreateCheck).should("be.visible").and("be.checked");
cy.get(groupsSelector.appsCreateCheck).uncheck();
cy.verifyToastMessage(
commonSelectors.toastMessage,
groupsText.permissionUpdatedToast
);
cy.get(groupsSelector.appsCreateCheck).check();
cy.verifyToastMessage(
commonSelectors.toastMessage,
groupsText.permissionUpdatedToast
@ -130,76 +258,155 @@ export const manageGroupsElements = () => {
"have.text",
groupsText.createLabel
);
cy.get(groupsSelector.appsCreateCheck).uncheck();
cy.get(groupsSelector.appsDeleteCheck).should("be.visible").check();
cy.get('[data-cy="app-create-helper-text"]').verifyVisibleElement(
"have.text",
"Create apps in this workspace"
);
cy.get(groupsSelector.appsDeleteCheck).should("be.visible").and("be.checked");
cy.get(groupsSelector.appsDeleteLabel).verifyVisibleElement(
"have.text",
groupsText.deleteLabel
);
cy.get('[data-cy="app-delete-helper-text"]').verifyVisibleElement(
"have.text",
"Delete any app in this workspace"
);
cy.get(groupsSelector.appsDeleteCheck).uncheck();
cy.verifyToastMessage(
commonSelectors.toastMessage,
groupsText.permissionUpdatedToast
);
cy.get(groupsSelector.appsDeleteCheck).check();
cy.verifyToastMessage(
commonSelectors.toastMessage,
groupsText.permissionUpdatedToast
);
cy.get(groupsSelector.resourcesFolders).verifyVisibleElement(
"have.text",
groupsText.resourcesFolders
);
cy.get(groupsSelector.foldersCreateCheck).should("be.visible").check();
cy.get(groupsSelector.foldersCreateCheck)
.should("be.visible")
.and("be.checked");
cy.get(groupsSelector.foldersCreateLabel).verifyVisibleElement(
"have.text",
groupsText.folderCreateLabel
);
cy.get('[data-cy="folder-helper-text"]').verifyVisibleElement(
"have.text",
"All operations on folders"
);
cy.get(groupsSelector.foldersCreateCheck).uncheck();
cy.verifyToastMessage(
commonSelectors.toastMessage,
groupsText.permissionUpdatedToast
);
cy.get(groupsSelector.foldersCreateCheck).check();
cy.verifyToastMessage(
commonSelectors.toastMessage,
groupsText.permissionUpdatedToast
);
cy.get(groupsSelector.resourcesWorkspaceVar).verifyVisibleElement(
"have.text",
groupsText.resourcesWorkspaceVar
);
cy.get(groupsSelector.workspaceVarCheckbox)
.should("be.visible")
.and("be.checked");
cy.get('[data-cy="workspace-constants-helper-text"]').verifyVisibleElement(
"have.text",
"All operations on workspace constants"
);
cy.get(groupsSelector.workspaceVarCheckbox).uncheck();
cy.verifyToastMessage(
commonSelectors.toastMessage,
groupsText.permissionUpdatedToast
);
cy.get(groupsSelector.workspaceVarCheckbox).check();
cy.verifyToastMessage(
commonSelectors.toastMessage,
groupsText.permissionUpdatedToast
);
cy.get(groupsSelector.workspaceVarCheckbox).uncheck();
navigateToAllUserGroup();
cy.get(groupsSelector.groupLink("Admin")).click();
cy.get(groupsSelector.groupLink("Admin")).verifyVisibleElement(
"have.text",
groupsText.admin
);
cy.get(groupsSelector.appsLink).click();
cy.get(groupsSelector.textDefaultGroup).verifyVisibleElement(
"have.text",
groupsText.textDefaultGroup
);
cy.get('[data-cy="granular-access-link"]').click();
cy.get(groupsSelector.nameTableHeader).verifyVisibleElement(
"have.text",
groupsText.textAppName
"Name"
);
cy.get(groupsSelector.permissionstableHedaer).verifyVisibleElement(
"have.text",
groupsText.permissionstableHedaer
"Permission"
);
cy.get("body").then(($title) => {
if ($title.find(groupsSelector.helperTextNoAppsAdded).length > 0) {
cy.get(groupsSelector.helperTextNoAppsAdded)
.eq(0)
.verifyVisibleElement("have.text", groupsText.helperTextNoAppsAdded);
cy.get(groupsSelector.helperTextPermissions)
.eq(0)
.verifyVisibleElement("have.text", groupsText.helperTextPermissions);
}
});
cy.get(groupsSelector.usersLink).click();
cy.get(groupsSelector.multiSelectSearch).should("be.visible");
cy.get(groupsSelector.mutiSelectAddButton("Admin")).verifyVisibleElement(
cy.get('[data-cy="resource-header"]:eq(1)').verifyVisibleElement(
"have.text",
groupsText.addUsersButton
"Resource"
);
cy.get('[data-cy="apps-text"]').verifyVisibleElement("have.text", " Apps");
cy.get('[data-cy="app-edit-radio"]').should("be.visible").and("be.checked");
cy.get('[data-cy="app-edit-label"]').verifyVisibleElement(
"have.text",
"Edit"
);
cy.get('[data-cy="app-edit-helper-text"]').verifyVisibleElement(
"have.text",
"Access to app builder"
);
cy.get('[data-cy="app-view-radio"]').should("be.visible");
cy.get('[data-cy="app-view-label"]').verifyVisibleElement(
"have.text",
"View"
);
cy.get('[data-cy="app-view-helper-text"]').verifyVisibleElement(
"have.text",
"Only access released version of apps"
);
cy.get('[data-cy="app-hide-from-dashboard-radio"]').should("be.visible");
cy.get(
'[data-cy="app-hide-from-dashboard-helper-text"]'
).verifyVisibleElement("have.text", "App will be accessible by URL only");
cy.get('[data-cy="group-chip"]').verifyVisibleElement(
"have.text",
"All apps"
);
cy.get('[data-cy="add-apps-buton"]').verifyVisibleElement(
"have.text",
"Add apps"
);
cy.get(groupsSelector.groupLink("End-user")).click();
cy.get(groupsSelector.groupLink("End-user")).verifyVisibleElement(
"have.text",
"End-user"
);
cy.get('[data-cy="end-user-title"]').verifyVisibleElement(
"have.text",
"End-user (0)"
);
cy.get(groupsSelector.createNewGroupButton).verifyVisibleElement(
"have.text",
groupsText.createNewGroupButton
);
cy.get(groupsSelector.usersLink).verifyVisibleElement(
"have.text",
groupsText.usersLink
);
cy.get(groupsSelector.permissionsLink).verifyVisibleElement(
"have.text",
groupsText.permissionsLink
);
cy.get('[data-cy="granular-access-link"]').verifyVisibleElement(
"have.text",
"Granular access"
);
cy.get(groupsSelector.usersLink).click();
cy.get(groupsSelector.nameTableHeader).verifyVisibleElement(
"have.text",
groupsText.userNameTableHeader
@ -223,15 +430,109 @@ export const manageGroupsElements = () => {
"have.text",
groupsText.resourcesApps
);
cy.get(groupsSelector.appsCreateCheck).verifyVisibleElement("be.disabled");
cy.get(groupsSelector.appsDeleteCheck).verifyVisibleElement("be.disabled");
cy.get(groupsSelector.appsCreateCheck)
.should("be.visible")
.and("have.attr", "disabled");
cy.get(groupsSelector.appsCreateLabel).verifyVisibleElement(
"have.text",
groupsText.createLabel
);
cy.get('[data-cy="app-create-helper-text"]').verifyVisibleElement(
"have.text",
"Create apps in this workspace"
);
cy.get(groupsSelector.appsDeleteCheck)
.should("be.visible")
.and("have.attr", "disabled");
cy.get(groupsSelector.appsDeleteLabel).verifyVisibleElement(
"have.text",
groupsText.deleteLabel
);
cy.get('[data-cy="app-delete-helper-text"]').verifyVisibleElement(
"have.text",
"Delete any app in this workspace"
);
cy.get(groupsSelector.resourcesFolders).verifyVisibleElement(
"have.text",
groupsText.resourcesFolders
);
cy.get(groupsSelector.foldersCreateCheck)
.should("be.visible")
.and("have.attr", "disabled");
cy.get(groupsSelector.foldersCreateLabel).verifyVisibleElement(
"have.text",
groupsText.folderCreateLabel
);
cy.get(groupsSelector.foldersCreateCheck).verifyVisibleElement("be.disabled");
cy.get(groupsSelector.workspaceVarCheckbox).verifyVisibleElement(
"be.disabled"
cy.get('[data-cy="folder-helper-text"]').verifyVisibleElement(
"have.text",
"All operations on folders"
);
cy.get(groupsSelector.resourcesWorkspaceVar).verifyVisibleElement(
"have.text",
groupsText.resourcesWorkspaceVar
);
cy.get(groupsSelector.workspaceVarCheckbox)
.should("be.visible")
.and("have.attr", "disabled");
cy.get('[data-cy="workspace-constants-helper-text"]').verifyVisibleElement(
"have.text",
"All operations on workspace constants"
);
cy.get('[data-cy="granular-access-link"]').click();
cy.get(groupsSelector.nameTableHeader).verifyVisibleElement(
"have.text",
"Name"
);
cy.get(groupsSelector.permissionstableHedaer).verifyVisibleElement(
"have.text",
"Permission"
);
cy.get('[data-cy="resource-header"]:eq(1)').verifyVisibleElement(
"have.text",
"Resource"
);
cy.get('[data-cy="apps-text"]').verifyVisibleElement("have.text", " Apps");
cy.get('[data-cy="app-edit-radio"]')
.should("be.visible")
.and("have.attr", "disabled");
cy.get('[data-cy="app-edit-label"]').verifyVisibleElement(
"have.text",
"Edit"
);
cy.get('[data-cy="app-edit-helper-text"]').verifyVisibleElement(
"have.text",
"Access to app builder"
);
cy.get('[data-cy="app-view-radio"]')
.should("be.visible")
.and("have.attr", "disabled");
cy.get('[data-cy="app-view-radio"]').should("be.checked");
cy.get('[data-cy="app-view-label"]').verifyVisibleElement(
"have.text",
"View"
);
cy.get('[data-cy="app-view-helper-text"]').verifyVisibleElement(
"have.text",
"Only access released version of apps"
);
cy.get('[data-cy="app-hide-from-dashboard-radio"]').should("be.visible");
cy.get(
'[data-cy="app-hide-from-dashboard-helper-text"]'
).verifyVisibleElement("have.text", "App will be accessible by URL only");
cy.get('[data-cy="group-chip"]').verifyVisibleElement(
"have.text",
"All apps"
);
cy.get('[data-cy="add-apps-buton"]').verifyVisibleElement(
"have.text",
"Add apps"
);
};
@ -341,9 +642,13 @@ export const verifyGroupCardOptions = (groupName) => {
);
};
export const groupPermission = (fieldsToCheckOrUncheck, groupName = "All users", shouldCheck = false,) => {
export const groupPermission = (
fieldsToCheckOrUncheck,
groupName = "All users",
shouldCheck = false
) => {
navigateToManageGroups();
cy.get(groupsSelector.groupLink(groupName))
cy.get(groupsSelector.groupLink(groupName));
cy.get(groupsSelector.permissionsLink).click();
fieldsToCheckOrUncheck.forEach((field) => {
@ -362,5 +667,5 @@ export const groupPermission = (fieldsToCheckOrUncheck, groupName = "All users",
export const duplicateGroup = () => {
OpenGroupCardOption(groupName);
cy.get(groupsSelector.duplicateOption).click()
}
cy.get(groupsSelector.duplicateOption).click();
};

View file

@ -122,8 +122,8 @@ export const manageUsersElements = () => {
"have.text",
usersText.buttonDownloadTemplate
);
cy.wait(3000)
cy.exec("cd ./cypress/downloads/ && rm -rf *");
cy.wait(3000)
cy.get(usersSelector.buttonDownloadTemplate).click();
cy.wait(4000)
cy.exec("ls ./cypress/downloads/").then((result) => {
@ -340,3 +340,40 @@ export const fetchAndVisitInviteLink = (email) => {
});
});
};
export const inviteUserWithUserRole = (
firstName,
email,
role,
) => {
fillUserInviteForm(firstName, email);
cy.wait(2000);
cy.get("body").then(($body) => {
const selectDropdown = $body.find('[data-cy="user-group-select"]>>>>>');
if (selectDropdown.length === 0) {
cy.get('[data-cy="user-group-select"]>>>>>').click();
}
cy.get('[data-cy="user-group-select"]>>>>>').eq(0).type(role);
cy.wait(1000);
cy.get('[data-cy="group-check-input"]').eq(0).check()
cy.wait(1000);
});
cy.get(usersSelector.buttonInviteUsers).click();
cy.verifyToastMessage(
commonSelectors.toastMessage,
usersText.userCreatedToast
);
cy.wait(1000);
fetchAndVisitInviteLink(email);
cy.clearAndType(commonSelectors.passwordInputField, "password");
cy.get(commonSelectors.signUpButton).click();
cy.wait(2000);
cy.get(commonSelectors.acceptInviteButton).click();
};

File diff suppressed because it is too large Load diff

View file

@ -15,7 +15,7 @@
"dependencies": {
"cypress-real-events": "^1.7.6",
"moment": "^2.29.4",
"node-xlsx": "^0.21.0",
"node-xlsx": "^0.4.0",
"pdf-parse": "^1.1.1",
"pg": "^8.8.0"
}

View file

@ -1 +1 @@
2.66.2
2.67.0

View file

@ -1,2 +1,2 @@
First Name,Last Name,Email,Group
test,user,test@gmail.com,For multiple groups separate using pipe (|) operator e.g. All Users|Admin
First Name,Last Name,Email,User Role,Group
test,user,test@gmail.com,"Assign each user a role: Admin, Builder or End User. User role value should be exact same","For multiple groups separate using pipe (|) operator e.g. Groups1|Group2 or leave blank if no group assign"
1 First Name Last Name Email User Role Group
2 test user test@gmail.com Assign each user a role: Admin, Builder or End User. User role value should be exact same For multiple groups separate using pipe (|) operator e.g. All Users|Admin For multiple groups separate using pipe (|) operator e.g. Groups1|Group2 or leave blank if no group assign

View file

@ -9,6 +9,7 @@
"search": "Search",
"update": "Update",
"delete": "Delete",
"remove": "Remove",
"add": "Add",
"view": "View",
"create": "Create",
@ -104,7 +105,7 @@
"passwordConfirmation": "Password Confirmation",
"newToTooljet": "New to ToolJet?",
"newToWorkspace": "New to this workspace?",
"enterWorkEmail": "Enter your email",
"enterWorkEmail": "Enter your work email",
"enterPassword": "Enter password",
"forgot": "Forgot?",
"workEmail": "Email",
@ -269,7 +270,7 @@
"userGroups": "User Groups",
"createNewGroup": "Create new group",
"updateGroup": "Update group",
"addNewGroup": "Add new group",
"addNewGroup": "Create new group",
"enterName": "Enter group name",
"createGroup": "Create Group",
"name": "Name"
@ -418,6 +419,7 @@
},
"noApplicationFound": "No Applications found",
"thisFolderIsEmpty": "This folder is empty",
"nonAccessibleFolderApps": "You do not have access to any applications in this folder.",
"deleteAppAndData": "The app {{appName}} and the associated data will be permanently deleted, do you want to continue?",
"removeAppFromFolder": "The app will be removed from this folder, do you want to continue?",
"change": "Change",
@ -960,4 +962,4 @@
"tip": "Back to Home"
}
}
}
}

View file

@ -9,7 +9,7 @@ const userStatusOptions = [
{ name: 'Archived', value: 'archived' },
];
const UsersFilter = ({ filterList }) => {
const UsersFilter = ({ filterList, resetSearch }) => {
const [options, setOptions] = useState({ searchText: '', status: '' });
const [statusVal, setStatusVal] = useState('');
const [queryVal, setQueryVal] = useState();
@ -44,6 +44,12 @@ const UsersFilter = ({ filterList }) => {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [options.searchText, options.status]);
useEffect(() => {
setOptions({ searchText: '', status: '' });
setStatusVal('');
setQueryVal('');
}, [resetSearch]);
return (
<div className="workspace-settings-table-wrap workspace-settings-filter-wrap">
<div className="row workspace-settings-filters">
@ -79,6 +85,7 @@ const UsersFilter = ({ filterList }) => {
setQueryVal(e.target.value);
queryValuesChanged(e);
}}
value={options.searchText}
data-cy="input-field-user-filter-search"
/>
</div>

View file

@ -7,6 +7,8 @@ import SolidIcon from '@/_ui/Icon/SolidIcons';
import { Tooltip } from 'react-tooltip';
import UsersActionMenu from './UsersActionMenu';
import { humanizeifDefaultGroupName, decodeEntities } from '@/_helpers/utils';
import { ToolTip } from '@/_components/ToolTip';
import Spinner from 'react-bootstrap/Spinner';
const UsersTable = ({
isLoading,
@ -33,10 +35,10 @@ const UsersTable = ({
<th data-cy="users-table-name-column-header">
{translator('header.organization.menus.manageUsers.name', 'Name')}
</th>
<th data-cy="users-table-email-column-header">
{translator('header.organization.menus.manageUsers.email', 'Email')}
<th data-cy="users-table-roles-column-header" data-name="role-header">
User role
</th>
<th data-cy="users-table-groups-column-header">Groups</th>
<th data-cy="users-table-groups-column-header">Custom groups</th>
{users && users[0]?.status ? (
<th data-cy="users-table-status-column-header">
{translator('header.organization.menus.manageUsers.status', 'Status')}
@ -65,7 +67,7 @@ const UsersTable = ({
{Array.isArray(users) &&
users.length > 0 &&
users.map((user) => (
<tr key={user.id}>
<tr key={user.id} data-cy={`${user.name.toLowerCase().replace(/\s+/g, '-')}-user-row`}>
<td>
<Avatar
avatarId={user.avatar_id}
@ -73,22 +75,24 @@ const UsersTable = ({
user.last_name ? user.last_name[0] : ''
}`}
/>
<span
className="mx-3 tj-text tj-text-sm"
data-cy={`${user.name.toLowerCase().replace(/\s+/g, '-')}-user-name`}
>
{decodeEntities(user.name)}
</span>
<div className="user-detail">
<span
className="mx-3 tj-text tj-text-sm"
data-cy={`${user.name.toLowerCase().replace(/\s+/g, '-')}-user-name`}
>
{decodeEntities(user.name)}
</span>
<span
style={{ color: '#687076' }}
className="user-email mx-3 tj-text-xsm"
data-cy={`${user.name.toLowerCase().replace(/\s+/g, '-')}-user-email`}
>
{user.email}
</span>
</div>
</td>
<td className="text-muted">
<a
className="text-reset user-email tj-text-sm"
data-cy={`${user.name.toLowerCase().replace(/\s+/g, '-')}-user-email`}
>
{user.email}
</a>
</td>
<GroupChipTD groups={user.groups} />
<GroupChipTD groups={user.role_group.map((group) => group.name)} isRole={true} />
<GroupChipTD groups={user.groups.map((group) => group.name)} />
{user.status && (
<td className="text-muted">
<span
@ -165,7 +169,7 @@ const UsersTable = ({
export default UsersTable;
const GroupChipTD = ({ groups = [] }) => {
const GroupChipTD = ({ groups = [], isRole = false }) => {
const [showAllGroups, setShowAllGroups] = useState(false);
const groupsListRef = useRef();
@ -196,20 +200,23 @@ const GroupChipTD = ({ groups = [] }) => {
return arr;
}
const orderedArray = moveValuesToLast(groups, ['all_users', 'admin']);
const orderedArray = groups;
const toggleAllGroupsList = (e) => {
setShowAllGroups(!showAllGroups);
};
const renderGroupChip = (group, index) => (
<span className="group-chip" key={index} data-cy="group-chip">
{humanizeifDefaultGroupName(group)}
</span>
<ToolTip message={group}>
<span className="group-chip" key={index} data-cy="group-chip">
{humanizeifDefaultGroupName(group)}
</span>
</ToolTip>
);
return (
<td
data-name={isRole ? 'role-header' : ''}
data-active={showAllGroups}
ref={groupsListRef}
onClick={(e) => {
@ -218,28 +225,31 @@ const GroupChipTD = ({ groups = [] }) => {
className={cx('text-muted groups-name-cell', { 'groups-hover': orderedArray.length > 2 })}
>
<div className="groups-name-container tj-text-sm font-weight-500">
{orderedArray.slice(0, 2).map((group, index) => {
if (orderedArray.length <= 2) {
return renderGroupChip(group, index);
}
{orderedArray.length === 0 ? (
<div className="empty-text">-</div>
) : (
orderedArray.slice(0, 2).map((group, index) => {
if (orderedArray.length <= 2) {
return renderGroupChip(group, index);
}
if (orderedArray.length > 2) {
if (index === 1) {
if (orderedArray.length > 2 && index === 1) {
return (
<>
<span className="group-chip" key={index}>
{' '}
+{orderedArray.length - 1} more
</span>
<React.Fragment key={index}>
{renderGroupChip(group, index)}
<span className="group-chip">+{orderedArray.length - 2} more</span>
{showAllGroups && (
<div className="all-groups-list">{groups.map((group, index) => renderGroupChip(group, index))}</div>
<div className="all-groups-list">
{orderedArray.slice(2).map((group, index) => renderGroupChip(group, index))}
</div>
)}
</>
</React.Fragment>
);
}
return renderGroupChip(group, index);
}
})}
})
)}
</div>
</td>
);

38653
frontend/package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -180,7 +180,7 @@
"terser-webpack-plugin": "^5.3.6",
"webpack": "^5.75.0",
"webpack-cli": "^5.0.1",
"webpack-dev-server": "^4.11.1"
"webpack-dev-server": "4.11.1"
},
"overrides": {
"react-dates": {

View file

@ -6,17 +6,12 @@ import { authenticationService, tooljetService } from '@/_services';
import { withRouter } from '@/_hoc/withRouter';
import { PrivateRoute, AdminRoute, AppsRoute, SwitchWorkspaceRoute, OrganizationInviteRoute } from '@/Routes';
import { HomePage } from '@/HomePage';
import { LoginPage } from '@/LoginPage';
import { SignupPage } from '@/SignupPage';
import { TooljetDatabase } from '@/TooljetDatabase';
import { OrganizationInvitationPage } from '@/ConfirmationPage';
import { Authorize } from '@/Oauth2';
import { Authorize as Oauth } from '@/Oauth';
import { Viewer } from '@/Editor';
import { OrganizationSettings } from '@/OrganizationSettingsPage';
import { SettingsPage } from '../SettingsPage/SettingsPage';
import { ForgotPassword } from '@/ForgotPassword';
import { ResetPassword } from '@/ResetPassword';
import { MarketplacePage } from '@/MarketplacePage';
import SwitchWorkspacePage from '@/HomePage/SwitchWorkspacePage';
import { GlobalDatasources } from '@/GlobalDatasources';
@ -25,21 +20,20 @@ import Toast from '@/_ui/Toast';
import { VerificationSuccessInfoScreen } from '@/SuccessInfoScreen';
import '@/_styles/theme.scss';
import { AppLoader } from '@/AppLoader';
import SetupScreenSelfHost from '../SuccessInfoScreen/SetupScreenSelfHost';
export const BreadCrumbContext = React.createContext({});
import 'react-tooltip/dist/react-tooltip.css';
import { getWorkspaceIdOrSlugFromURL } from '@/_helpers/routes';
import ErrorPage from '@/_components/ErrorComponents/ErrorPage';
import WorkspaceConstants from '@/WorkspaceConstants';
import { AuthRoute } from '@/Routes/AuthRoute';
import { useAppDataStore } from '@/_stores/appDataStore';
import cx from 'classnames';
import useAppDarkMode from '@/_hooks/useAppDarkMode';
import { ManageOrgUsers } from '@/ManageOrgUsers';
import { ManageGroupPermissions } from '@/ManageGroupPermissions';
import OrganizationLogin from '@/_components/OrganizationLogin/OrganizationLogin';
import { ManageOrgVars } from '@/ManageOrgVars';
import { ManageGroupPermissionsV2 } from '@/ManageGroupPermissionsV2/ManageGroupPermissionsV2';
import { setFaviconAndTitle } from '@white-label/whiteLabelling';
import { onboarding, auth } from '@/modules';
const AppWrapper = (props) => {
const { isAppDarkMode } = useAppDarkMode();
@ -165,49 +159,10 @@ class AppComponent extends React.Component {
)}
<BreadCrumbContext.Provider value={{ sidebarNav, updateSidebarNAV }}>
<Routes>
<Route
path="/login/:organizationId"
exact
element={
<AuthRoute {...this.props}>
<LoginPage {...this.props} />
</AuthRoute>
}
/>
<Route
path="/login"
exact
element={
<AuthRoute {...this.props}>
<LoginPage {...this.props} />
</AuthRoute>
}
/>
<Route path="/setup" exact element={<SetupScreenSelfHost {...this.props} darkMode={darkMode} />} />
{onboarding(this.props)}
{auth(this.props)}
<Route path="/sso/:origin/:configId" exact element={<Oauth {...this.props} />} />
<Route path="/sso/:origin" exact element={<Oauth {...this.props} />} />
<Route
path="/signup/:organizationId"
exact
element={
<AuthRoute {...this.props}>
<SignupPage {...this.props} />
</AuthRoute>
}
/>
<Route
path="/signup"
exact
element={
<AuthRoute {...this.props}>
<SignupPage {...this.props} />
</AuthRoute>
}
/>
<Route path="/forgot-password" element={<ForgotPassword />} />
<Route path="/reset-password/:token" element={<ResetPassword />} />
<Route path="/reset-password" element={<ResetPassword />} />
<Route path="/invitations/:token" element={<VerificationSuccessInfoScreen />} />
<Route
path="/invitations/:token/workspaces/:organizationToken"
element={
@ -216,14 +171,6 @@ class AppComponent extends React.Component {
</OrganizationInviteRoute>
}
/>
<Route
path="/organization-invitations/:token"
element={
<OrganizationInviteRoute {...this.props} isOrgazanizationOnlyInvite={true}>
<OrganizationInvitationPage {...this.props} darkMode={darkMode} />
</OrganizationInviteRoute>
}
/>
<Route
exact
path="/:workspaceId/apps/:slug/:pageHandle?/*"
@ -310,7 +257,7 @@ class AppComponent extends React.Component {
path="groups"
element={
<AdminRoute>
<ManageGroupPermissions switchDarkMode={this.switchDarkMode} darkMode={darkMode} />
<ManageGroupPermissionsV2 switchDarkMode={this.switchDarkMode} darkMode={darkMode} />
</AdminRoute>
}
/>

View file

@ -4,12 +4,14 @@ import CodeHinter from '.';
import { copyToClipboard } from '@/_helpers/appUtils';
import { Alert } from '@/_ui/Alert/Alert';
import _, { isEmpty } from 'lodash';
import { handleCircularStructureToJSON, hasCircularDependency } from '@/_helpers/utils';
import { handleCircularStructureToJSON, hasCircularDependency, verifyConstant } from '@/_helpers/utils';
import OverlayTrigger from 'react-bootstrap/OverlayTrigger';
import Popover from 'react-bootstrap/Popover';
import Card from 'react-bootstrap/Card';
// eslint-disable-next-line import/no-unresolved
import { JsonViewer } from '@textea/json-viewer';
import { useCurrentStateStore } from '@/_stores/currentStateStore';
import { useDataQueriesStore } from '@/_stores/dataQueriesStore';
import { reservedKeywordReplacer } from '@/_lib/reserved-keyword-replacer';
const sanitizeLargeDataset = (data, callback) => {
@ -111,6 +113,19 @@ export const PreviewBox = ({
let previewType = getCurrentNodeType(resolvedValue);
let previewContent = resolvedValue;
let isGlobalConstant = currentValue && currentValue.includes('{{constants.');
let isSecretConstant = currentValue && currentValue.includes('{{secrets.');
let invalidConstants = null;
let undefinedError = null;
if (isGlobalConstant || isSecretConstant) {
const secrets = useDataQueriesStore.getState().secrets;
const globals = useCurrentStateStore.getState().constants;
invalidConstants = verifyConstant(currentValue, globals, secrets);
}
if (invalidConstants?.length) {
undefinedError = { type: 'Invalid constants' };
}
const ifCoersionErrorHasCircularDependency = (value) => {
if (hasCircularDependency(value)) {
@ -133,7 +148,17 @@ export const PreviewBox = ({
}, [error]);
useEffect(() => {
const [valid, _error, newValue, resolvedValue] = resolveReferences(currentValue, validationSchema, customVariables);
const [valid, _error, rawNewValue, rawResolvedValue] = resolveReferences(
currentValue,
validationSchema,
customVariables
);
const resolvedValue = typeof rawResolvedValue === 'function' ? undefined : rawResolvedValue;
const newValue = typeof rawNewValue === 'function' ? undefined : rawNewValue;
const isSecretError =
currentValue?.includes('secrets.') || _error?.includes('ReferenceError: secrets is not defined');
if (isWorkspaceVariable || !validationSchema || isEmpty(validationSchema)) {
return setResolvedValue(newValue);
@ -142,7 +167,7 @@ export const PreviewBox = ({
// we dont need to add or update the resolved value if the value has deep children
const _resolveValue = sanitizeLargeDataset(resolvedValue, setLargeDataset);
if (valid) {
if (valid && !isSecretError) {
const [coercionPreview, typeAfterCoercion, typeBeforeCoercion] = computeCoercion(resolvedValue, newValue);
setResolvedValue(_resolveValue);
@ -153,11 +178,13 @@ export const PreviewBox = ({
typeBeforeCoercion,
});
setError(null);
} else if (!valid && !newValue && !resolvedValue) {
} else if (!valid && !newValue && !resolvedValue && !isSecretError) {
const err = !error ? `Invalid value for ${validationSchema?.schema?.type}` : `${_error}`;
setError({ message: err, value: resolvedValue, type: 'Invalid' });
} else {
const jsErrorType = _error?.includes('ReferenceError')
const jsErrorType = isSecretError
? 'Error'
: _error?.includes('ReferenceError')
? 'ReferenceError'
: _error?.includes('TypeError')
? 'TypeError'
@ -168,9 +195,9 @@ export const PreviewBox = ({
const errValue = ifCoersionErrorHasCircularDependency(_resolveValue);
setError({
message: _error,
value: jsErrorType === 'Invalid' ? JSON.stringify(errValue, reservedKeywordReplacer) : resolvedValue,
type: jsErrorType,
message: isSecretError ? 'secrets cannot be used in apps' : _error,
value: isSecretError ? 'Undefined' : jsErrorType === 'Invalid' ? JSON.stringify(errValue) : resolvedValue,
type: isSecretError ? 'Error' : jsErrorType,
});
setCoersionData(null);
}
@ -180,13 +207,14 @@ export const PreviewBox = ({
return (
<>
<PreviewBox.RenderResolvedValue
error={error}
error={error || undefinedError}
currentValue={currentValue}
previewType={previewType}
resolvedValue={content}
coersionData={coersionData}
withValidation={!isEmpty(validationSchema)}
isWorkspaceVariable={isWorkspaceVariable}
isSecretConstant={isSecretConstant || false}
isLargeDataset={largeDataset}
/>
<CodeHinter.PopupIcon
@ -205,6 +233,7 @@ const RenderResolvedValue = ({
coersionData,
withValidation,
isWorkspaceVariable,
isSecretConstant = false,
isLargeDataset,
}) => {
const computeCoersionPreview = (resolvedValue, coersionData) => {
@ -229,7 +258,11 @@ const RenderResolvedValue = ({
}`
: previewType;
const previewContent = !withValidation ? resolvedValue : computeCoersionPreview(resolvedValue, coersionData);
const previewContent = isSecretConstant
? 'Values of secret constants are hidden'
: !withValidation
? resolvedValue
: computeCoersionPreview(resolvedValue, coersionData);
const cls = error ? 'codehinter-error-banner' : 'codehinter-success-banner';

View file

@ -281,7 +281,11 @@ export const resolveReferences = (query, validationSchema, customResolvers = {})
const queryHasJSCode = queryHasStringOtherThanVariable(query);
let useJSResolvers = queryHasJSCode || getDynamicVariables(query)?.length > 1;
if (!queryHasJSCode && getDynamicVariables(query)?.length === 1 && !query.startsWith('{{') && query.includes('{{')) {
if (
!queryHasJSCode &&
getDynamicVariables(query)?.length === 1 &&
((!query.startsWith('{{') && query.includes('{{')) || (query.startsWith('{{') && !query.endsWith('}}')))
) {
useJSResolvers = true;
}

View file

@ -47,7 +47,7 @@ import { withTranslation } from 'react-i18next';
import { v4 as uuid } from 'uuid';
import Skeleton from 'react-loading-skeleton';
import EditorHeader from './Header';
import { getWorkspaceId, isValidUUID } from '@/_helpers/utils';
import { getWorkspaceId, isValidUUID, Constants } from '@/_helpers/utils';
import { fetchAndSetWindowTitle, pageTitles, defaultWhiteLabellingSettings } from '@white-label/whiteLabelling';
import '@/_styles/editor/react-select-search.scss';
import { withRouter } from '@/_hoc/withRouter';
@ -229,12 +229,15 @@ const EditorComponent = (props) => {
// Subscribe to changes in the current session using RxJS observable pattern
const subscription = authenticationService.currentSession.subscribe((currentSession) => {
if (currentUser && currentSession?.group_permissions) {
if (currentUser && (currentSession?.group_permissions || currentSession?.role)) {
const userVars = {
email: currentUser.email,
firstName: currentUser.first_name,
lastName: currentUser.last_name,
groups: currentSession.group_permissions?.map((group) => group.group),
groups: currentSession?.group_permissions
? ['all_users', ...currentSession.group_permissions.map((group) => group.name)]
: ['all_users'],
role: currentSession?.role?.name,
};
const appUserDetails = {
@ -400,19 +403,26 @@ const EditorComponent = (props) => {
});
};
const fetchOrgEnvironmentConstants = () => {
//! for @ee: get the constants from `getConstantsFromEnvironment ` -- '/organization-constants/:environmentId'
orgEnvironmentConstantService.getAll().then(({ constants }) => {
const fetchOrgEnvironmentConstants = async (environmentId) => {
try {
const { constants } = await orgEnvironmentConstantService.getConstantsFromEnvironment(
environmentId,
Constants.Global
);
const orgConstants = {};
constants.map((constant) => {
const constantValue = constant.values.find((value) => value.environmentName === 'production')['value'];
orgConstants[constant.name] = constantValue;
constants.forEach((constant) => {
orgConstants[constant.name] = constant.value;
});
useCurrentStateStore.getState().actions.setCurrentState({
constants: orgConstants,
});
});
} catch (error) {
toast.error('Failed to fetch organization environment constants', {
position: 'top-center',
});
}
};
const initComponentVersioning = () => {
@ -717,7 +727,8 @@ const EditorComponent = (props) => {
fetchAndSetWindowTitle({ page: pageTitles.EDITOR, appName });
useAppVersionStore.getState().actions.updateEditingVersion(editing_version);
current_version_id && useAppVersionStore.getState().actions.updateReleasedVersionId(current_version_id);
await fetchOrgEnvironmentConstants();
const environmentId = editing_version?.current_environment_id;
await fetchOrgEnvironmentConstants(environmentId);
updateState({
slug,
isMaintenanceOn,

View file

@ -1,4 +1,4 @@
import React from 'react';
import React, { useState } from 'react';
import { appService, appsService, authenticationService } from '@/_services';
import Modal from 'react-bootstrap/Modal';
import { toast } from 'react-hot-toast';
@ -15,6 +15,7 @@ import { ToolTip } from '@/_components/ToolTip';
import { TOOLTIP_MESSAGES } from '@/_helpers/constants';
import { useAppDataStore } from '@/_stores/appDataStore';
import { retrieveWhiteLabelText } from '@white-label/whiteLabelling';
import InfoIcon from '@assets/images/icons/info.svg';
class ManageAppUsersComponent extends React.Component {
constructor(props) {
@ -32,6 +33,7 @@ class ManageAppUsersComponent extends React.Component {
value: null,
error: '',
},
isHovered: false,
isSlugUpdated: false,
};
}
@ -85,7 +87,6 @@ class ManageAppUsersComponent extends React.Component {
toast.error(error);
});
};
toggleAppVisibility = () => {
const newState = !this.props.isPublic;
this.setState({
@ -170,7 +171,13 @@ class ManageAppUsersComponent extends React.Component {
});
}
};
handleMouseEnter = () => {
this.setState({ isHovered: true });
};
handleMouseLeave = () => {
this.setState({ isHovered: false });
};
render() {
const { appId, isSlugVerificationInProgress, newSlug, isSlugUpdated } = this.state;
@ -178,66 +185,104 @@ class ManageAppUsersComponent extends React.Component {
const shareableLink = appLink + (this.props.slug || appId);
const slugButtonClass = !_.isEmpty(newSlug.error) ? 'is-invalid' : 'is-valid';
const embeddableLink = `<iframe width="560" height="315" src="${appLink}${this.props.slug}" title="${this.whiteLabelText} app - ${this.props.slug}" frameborder="0" allowfullscreen></iframe>`;
const shouldWeDisableShareModal = !this.props.isVersionReleased;
const { isHovered } = this.state.isHovered;
return (
<ToolTip
message={TOOLTIP_MESSAGES.SHARE_URL_UNAVAILABLE}
placement={!this.props.isVersionReleased ? 'bottom' : 'left'}
show={shouldWeDisableShareModal}
>
<div
title={!shouldWeDisableShareModal ? 'Share' : ''}
className="manage-app-users editor-header-icon tj-secondary-btn"
data-cy="share-button-link"
<div title={'Share'} className="manage-app-users" data-cy="share-button-link">
<span
className="manage-app-users tj-secondary-btn editor-header-icon cursor-pointer"
onClick={() => {
this.validateThePreExistingSlugs();
this.setState({ showModal: true });
}}
>
<span
className={cx('d-flex', {
'share-disabled': shouldWeDisableShareModal,
'share-disabled': false,
})}
onClick={() => {
this.validateThePreExistingSlugs();
!shouldWeDisableShareModal && this.setState({ showModal: true });
}}
>
<SolidIcon name="share" width="14" className="cursor-pointer" fill="#3E63DD" />
</span>
<Modal
show={this.state.showModal}
size="lg"
backdrop="static"
centered={true}
keyboard={true}
animation={false}
onEscapeKeyDown={this.hideModal}
className={`app-sharing-modal animation-fade ${this.props.darkMode ? 'dark-theme' : ''}`}
contentClassName={this.props.darkMode ? 'dark-theme' : ''}
>
<Modal.Header>
<Modal.Title data-cy="modal-header">{this.props.t('editor.share', 'Share')}</Modal.Title>
<span onClick={this.hideModal} data-cy="modal-close-button">
<SolidIcon name="remove" className="cursor-pointer" aria-label="Close" />
</span>
</Modal.Header>
<Modal.Body>
{
<div class="shareable-link-container">
<div className="make-public mb-3">
<div className="form-check form-switch d-flex align-items-center">
<input
className="form-check-input"
type="checkbox"
onClick={this.toggleAppVisibility}
checked={this?.props?.isPublic}
disabled={this.state.ischangingVisibility}
data-cy="make-public-app-toggle"
/>
<span className="form-check-label field-name" data-cy="make-public-app-label">
{this.props.t('editor.shareModal.makeApplicationPublic', 'Make application public')}
</span>
</div>
</div>
</span>
<Modal
show={this.state.showModal}
size="lg"
backdrop="static"
centered={true}
keyboard={true}
animation={false}
onEscapeKeyDown={this.hideModal}
className={`app-sharing-modal animation-fade ${this.props.darkMode ? 'dark-theme' : ''}`}
contentClassName={this.props.darkMode ? 'dark-theme' : ''}
>
<Modal.Header>
<Modal.Title data-cy="modal-header">{this.props.t('editor.share', 'Share')}</Modal.Title>
<span onClick={this.hideModal} data-cy="modal-close-button">
<SolidIcon name="remove" className="cursor-pointer" aria-label="Close" />
</span>
</Modal.Header>
<Modal.Body>
{
<div class="shareable-link-container">
<div className="make-public mb-3">
<div className="form-check form-switch d-flex align-items-center">
{this.props.isVersionReleased ? (
<div>
<input
className="form-check-input"
type="checkbox"
onClick={this.toggleAppVisibility}
checked={this?.props?.isPublic}
disabled={this.state.ischangingVisibility}
data-cy="make-public-app-toggle"
/>
<span className="form-check-label field-name" data-cy="make-public-app-label">
{this.props.t('editor.shareModal.makeApplicationPublic', 'Make application public')}
</span>
</div>
) : (
<div style={{ display: 'flex', alignItems: 'left', gap: '8px' }}>
<ToolTip
message={TOOLTIP_MESSAGES.RELEASE_VERSION_URL_UNAVAILABLE}
placement={'top'}
show={isHovered}
>
<div
onMouseEnter={this.handleMouseEnter}
onMouseLeave={this.handleMouseLeave}
style={{
width: '32px',
height: '18px',
marginLeft: '-40px',
}}
>
<input
className="form-check-input"
type="checkbox"
disabled
style={{
opacity: 0.3,
cursor: 'default',
margin: 0,
padding: 0,
}}
/>
</div>
</ToolTip>
<span
className="form-check-label field-name"
data-cy="make-public-app-label"
style={{ opacity: 0.6 }}
>
{this.props.t('editor.shareModal.makeApplicationPublic', 'Make application public')}
</span>
</div>
)}
</div>
</div>
{this.props.isVersionReleased ? (
<div className="shareable-link tj-app-input mb-2">
<label data-cy="shareable-app-link-label" className="field-name">
{this.props.t('editor.shareModal.shareableLink', 'Shareable app link')}
@ -259,6 +304,7 @@ class ManageAppUsersComponent extends React.Component {
style={{ maxWidth: '150px' }}
defaultValue={this.props.slug}
data-cy="app-name-slug-input"
disabled={!this.props.isVersionReleased}
/>
{isSlugVerificationInProgress && (
<div className="icon-container">
@ -344,60 +390,69 @@ class ManageAppUsersComponent extends React.Component {
>{`URL-friendly 'slug' consists of lowercase letters, numbers, and hyphens`}</label>
)}
</div>
{(this?.props?.isPublic || window?.public_config?.ENABLE_PRIVATE_APP_EMBED === 'true') && (
<div className="tj-app-input">
<label className="field-name" data-cy="iframe-link-label">
Embedded app link
</label>
<span className={`tj-text-input justify-content-between ${this.props.darkMode ? 'dark' : ''}`}>
<span data-cy="iframe-link">{embeddableLink}</span>
<span className="copy-container">
<CopyToClipboard
text={embeddableLink}
onCopy={() => toast.success('Link copied to clipboard')}
>
<svg
className="cursor-pointer"
width="17"
height="18"
viewBox="0 0 17 18"
fill="none"
xmlns="http://www.w3.org/2000/svg"
data-cy="iframe-link-copy-button"
>
<path
d="M9.11154 5.18031H5.88668V4.83302C5.88668 3.29859 7.13059 2.05469 8.66502 2.05469H12.8325C14.3669 2.05469 15.6109 3.29859 15.6109 4.83302V9.00052C15.6109 10.535 14.3669 11.7789 12.8325 11.7789H12.4852V8.554C12.4852 6.69076 10.9748 5.18031 9.11154 5.18031Z"
fill="#889096"
/>
<path
d="M8.66502 15.9464H4.49752C2.96309 15.9464 1.71918 14.7025 1.71918 13.168V9.00052C1.71918 7.46609 2.96309 6.22219 4.49752 6.22219H8.66502C10.1994 6.22219 11.4434 7.46609 11.4434 9.00052V13.168C11.4434 14.7025 10.1994 15.9464 8.66502 15.9464Z"
fill="#889096"
/>
</svg>
</CopyToClipboard>
</span>
</span>
) : (
<div className="shareable-link tj-app-input mb-2">
<label data-cy="shareable-app-link-label" className="field-name">
{this.props.t('editor.shareModal.shareableLink', 'Shareable app link')}
</label>
<div className="empty-version">
<InfoIcon style={{ width: '12px', marginRight: '5px' }} />
<span>This version has not been released yet</span>
</div>
)}
</div>
}
</Modal.Body>
</div>
)}
<Modal.Footer className="manage-app-users-footer">
{this.isUserAdmin && (
<Link
to={getPrivateRoute('workspace_settings')}
target="_blank"
className={`btn border-0 default-secondary-button float-right1`}
data-cy="manage-users-button"
>
Manage users
</Link>
)}
</Modal.Footer>
</Modal>
</div>
</ToolTip>
{((this?.props?.isVersionReleased && this?.props?.isPublic) ||
window?.public_config?.ENABLE_PRIVATE_APP_EMBED === 'true') && (
<div className="tj-app-input">
<label className="field-name" data-cy="iframe-link-label">
Embedded app link
</label>
<span className={`tj-text-input justify-content-between ${this.props.darkMode ? 'dark' : ''}`}>
<span data-cy="iframe-link">{embeddableLink}</span>
<span className="copy-container">
<CopyToClipboard text={embeddableLink} onCopy={() => toast.success('Link copied to clipboard')}>
<svg
className="cursor-pointer"
width="17"
height="18"
viewBox="0 0 17 18"
fill="none"
xmlns="http://www.w3.org/2000/svg"
data-cy="iframe-link-copy-button"
>
<path
d="M9.11154 5.18031H5.88668V4.83302C5.88668 3.29859 7.13059 2.05469 8.66502 2.05469H12.8325C14.3669 2.05469 15.6109 3.29859 15.6109 4.83302V9.00052C15.6109 10.535 14.3669 11.7789 12.8325 11.7789H12.4852V8.554C12.4852 6.69076 10.9748 5.18031 9.11154 5.18031Z"
fill="#889096"
/>
<path
d="M8.66502 15.9464H4.49752C2.96309 15.9464 1.71918 14.7025 1.71918 13.168V9.00052C1.71918 7.46609 2.96309 6.22219 4.49752 6.22219H8.66502C10.1994 6.22219 11.4434 7.46609 11.4434 9.00052V13.168C11.4434 14.7025 10.1994 15.9464 8.66502 15.9464Z"
fill="#889096"
/>
</svg>
</CopyToClipboard>
</span>
</span>
</div>
)}
</div>
}
</Modal.Body>
<Modal.Footer className="manage-app-users-footer">
{this.isUserAdmin && (
<Link
to={getPrivateRoute('workspace_settings')}
target="_blank"
className={`btn border-0 default-secondary-button float-right1`}
data-cy="manage-users-button"
>
Manage users
</Link>
)}
</Modal.Footer>
</Modal>
</div>
);
}
}

View file

@ -4,6 +4,12 @@ import Information from '@/_ui/Icon/solidIcons/Information';
import CodeHinter from '@/Editor/CodeEditor';
const isValidVariableName = (str) => /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(str);
const isConstant = (str) => {
if (typeof str !== 'string') {
return false;
}
return str.includes('secrets.') || str.includes('constants.');
};
const ParameterForm = ({
darkMode,
@ -38,7 +44,9 @@ const ParameterForm = ({
};
useEffect(() => {
if (!isValidVariableName(name)) {
if (isConstant(name)) {
setError('Constants cannot be used in params');
} else if (!isValidVariableName(name)) {
setError('Variable name invalid');
} else if (name && otherParams.some((param) => param.name === name.trim())) {
setError('Variable name exists');

View file

@ -26,7 +26,7 @@ import {
import queryString from 'query-string';
import ViewerLogoIcon from './Icons/viewer-logo.svg';
import { DataSourceTypes } from './DataSourceManager/SourceComponents';
import { resolveReferences, isQueryRunnable, isValidUUID } from '@/_helpers/utils';
import { resolveReferences, isQueryRunnable, isValidUUID, Constants } from '@/_helpers/utils';
import { withTranslation } from 'react-i18next';
import _ from 'lodash';
import { Navigate } from 'react-router-dom';
@ -283,13 +283,16 @@ class ViewerComponent extends React.Component {
const currentUser = this.state.currentUser;
let userVars = {};
const currentSessionValue = authenticationService.currentSessionValue;
if (currentUser) {
userVars = {
email: currentUser.email,
firstName: currentUser.first_name,
lastName: currentUser.last_name,
groups: authenticationService.currentSessionValue?.group_permissions.map((group) => group.group),
groups: currentSessionValue?.group_permissions
? ['All Users', ...currentSessionValue.group_permissions.map((group) => group.name)]
: ['All Users'],
role: currentSessionValue?.role?.name,
};
}
@ -438,11 +441,10 @@ class ViewerComponent extends React.Component {
let variablesResult;
if (!isPublic) {
const { constants } = await orgEnvironmentConstantService.getAll();
const { constants } = await orgEnvironmentConstantService.getConstantsFromApp(slug);
variablesResult = constants;
} else {
const { constants } = await orgEnvironmentConstantService.getConstantsFromPublicApp(slug);
variablesResult = constants;
}
@ -564,15 +566,18 @@ class ViewerComponent extends React.Component {
const versionId = this.props.versionId;
if (currentSession?.load_app && slug) {
if (currentSession?.group_permissions) {
if (currentSession?.group_permissions || currentSession?.role) {
useAppDataStore.getState().actions.setAppId(appId);
const currentUser = currentSession.current_user;
const currentSessionValue = authenticationService.currentSessionValue;
const userVars = {
email: currentUser.email,
firstName: currentUser.first_name,
lastName: currentUser.last_name,
groups: currentSession?.group_permissions?.map((group) => group.group),
groups: currentSessionValue?.group_permissions
? ['All Users', ...currentSessionValue.group_permissions.map((group) => group.name)]
: ['All Users'],
};
this.props.setCurrentState({
globals: {

View file

@ -31,7 +31,6 @@ export const GlobalDataSourcesPage = ({ darkMode = false, updateSelectedDatasour
const [addingDataSource, setAddingDataSource] = useState(false);
const [suggestingDataSource, setSuggestingDataSource] = useState(false);
const { t } = useTranslation();
const navigate = useNavigate();
const { admin } = authenticationService.currentSessionValue;
const marketplaceEnabled = admin && window.public_config?.ENABLE_MARKETPLACE_FEATURE == 'true';
const [modalProps, setModalProps] = useState({
@ -296,34 +295,6 @@ export const GlobalDataSourcesPage = ({ darkMode = false, updateSelectedDatasour
};
const renderCardGroup = (source, type) => {
if (type === 'Plugins' && source.length === 0) {
return (
<div className="add-plugins-container">
<div className="warning-container mb-2">
<SolidIcon name="warning" />
</div>
<div className="tj-text-sm font-weight-500 tj-text">No plugins added</div>
{admin && (
<>
<div className="tj-text-xsm font-weight-400 mt-2 mb-3">
Browse through plugins in marketplace to add them as a Data Source.{' '}
</div>
<ButtonSolid
onClick={() => {
marketplaceEnabled
? navigate('/integrations')
: toast.error('Please enable marketplace to add plugins');
}}
style={{ margin: 'auto' }}
variant="secondary"
>
Add plugins
</ButtonSolid>
</>
)}
</div>
);
}
const addDataSourceBtn = (item) => (
<ButtonSolid
disabled={addingDataSource}
@ -366,6 +337,36 @@ export const GlobalDataSourcesPage = ({ darkMode = false, updateSelectedDatasour
titleClassName={'datasource-card-title'}
/>
))}
{type === 'Plugins' && (
<div style={{ height: '122px', width: '164px' }} className={`col-md-2 mb-4 `}>
<div
className="card add-plugin-card"
role="button"
onClick={() => {
if (marketplaceEnabled) {
window.open('/integrations', '_blank');
} else {
toast.error('Please enable marketplace to add plugins');
}
}}
data-cy={`data-source-add-plugin`}
>
<div className="card-body">
<center style={{ paddingTop: '5px' }}>
<SolidIcon
name="plus"
fill={'var(--icon-default)'}
width={35}
height={35}
style={{ marginBottom: '8px' }}
/>
<br></br>
<span className={`datasource-card-title add-plugin-card-title`}>Add plugin</span>
</center>
</div>
</div>
</div>
)}
</div>
</>
);

View file

@ -48,14 +48,17 @@ const AppList = (props) => {
</div>
</div>
)}
{!props.isLoading && props.currentFolder.count === 0 && (
{!props.isLoading && props.apps?.length === 0 && (
<div className="text-center d-block">
<EmptyFoldersIllustration className="mb-4" data-cy="empty-folder-image" />
<span
className={`d-block text-center text-body ${props.darkMode && 'text-white-50'}`}
data-cy="empty-folder-text"
>
{t('homePage.thisFolderIsEmpty', 'This folder is empty')}
{/* removed this error message display for now -> as it was leading to multiple message being shown in the UI*/}
{/* {props.currentFolder?.count == 0
? t('homePage.thisFolderIsEmpty', 'This folder is empty')
: t('homePage.nonAccessibleFolderApps', 'You do not have access to any applications in this folder.')} */}
</span>
</div>
)}

View file

@ -3,6 +3,7 @@ import cx from 'classnames';
import { appsService, folderService, authenticationService, libraryAppService } from '@/_services';
import { ConfirmDialog, AppModal } from '@/_components';
import Select from '@/_ui/Select';
import _, { sample, isEmpty } from 'lodash';
import { Folders } from './Folders';
import { BlankPage } from './BlankPage';
import { toast } from 'react-hot-toast';
@ -14,7 +15,6 @@ import HomeHeader from './Header';
import Modal from './Modal';
import configs from './Configs/AppIcon.json';
import { withTranslation } from 'react-i18next';
import { sample, isEmpty } from 'lodash';
import ExportAppModal from './ExportAppModal';
import Footer from './Footer';
import { OrganizationList } from '@/_components/OrganizationManager/List';
@ -25,7 +25,6 @@ import { getQueryParams } from '@/_helpers/routes';
import { withRouter } from '@/_hoc/withRouter';
import FolderFilter from './FolderFilter';
import { APP_ERROR_TYPE } from '@/_helpers/error_constants';
import Skeleton, { SkeletonTheme } from 'react-loading-skeleton';
import { fetchAndSetWindowTitle, pageTitles } from '@white-label/whiteLabelling';
import HeaderSkeleton from '@/_ui/FolderSkeleton/HeaderSkeleton';
@ -304,23 +303,27 @@ class HomePageComponent extends React.Component {
canUserPerform(user, action, app) {
const currentSession = authenticationService.currentSessionValue;
const appPermission = currentSession.app_group_permissions;
const canUpdateApp =
appPermission && (appPermission.is_all_editable || appPermission.editable_apps_id.includes(app?.id));
const canReadApp =
(appPermission && canUpdateApp) ||
appPermission.is_all_viewable ||
appPermission.viewable_apps_id.includes(app?.id);
let permissionGrant;
switch (action) {
case 'create':
permissionGrant = this.canAnyGroupPerformAction('app_create', currentSession.group_permissions);
permissionGrant = currentSession.user_permissions.app_create;
break;
case 'read':
permissionGrant = this.isUserOwnerOfApp(user, app) || canReadApp;
break;
case 'update':
permissionGrant =
this.canAnyGroupPerformActionOnApp(action, currentSession.app_group_permissions, app) ||
this.isUserOwnerOfApp(user, app);
permissionGrant = canUpdateApp || this.isUserOwnerOfApp(user, app);
break;
case 'delete':
permissionGrant =
this.canAnyGroupPerformActionOnApp('delete', currentSession.app_group_permissions, app) ||
this.canAnyGroupPerformAction('app_delete', currentSession.group_permissions) ||
this.isUserOwnerOfApp(user, app);
permissionGrant = currentSession.user_permissions.app_delete || this.isUserOwnerOfApp(user, app);
break;
default:
permissionGrant = false;
@ -364,15 +367,15 @@ class HomePageComponent extends React.Component {
};
canCreateFolder = () => {
return this.canAnyGroupPerformAction('folder_create', authenticationService.currentSessionValue?.group_permissions);
return authenticationService.currentSessionValue?.user_permissions?.folder_c_r_u_d;
};
canDeleteFolder = () => {
return this.canAnyGroupPerformAction('folder_delete', authenticationService.currentSessionValue?.group_permissions);
return authenticationService.currentSessionValue?.user_permissions?.folder_c_r_u_d;
};
canUpdateFolder = () => {
return this.canAnyGroupPerformAction('folder_update', authenticationService.currentSessionValue?.group_permissions);
return authenticationService.currentSessionValue?.user_permissions?.folder_c_r_u_d;
};
cancelDeleteAppDialog = () => {
@ -853,8 +856,12 @@ class HomePageComponent extends React.Component {
{isLoading && !appSearchKey && <HeaderSkeleton />}
{(meta?.total_count > 0 || appSearchKey) && (
<>
<HomeHeader onSearchSubmit={this.onSearchSubmit} darkMode={this.props.darkMode} />
<div className="liner"></div>
{!(isLoading && !appSearchKey) && (
<>
<HomeHeader onSearchSubmit={this.onSearchSubmit} darkMode={this.props.darkMode} />
<div className="liner"></div>
</>
)}
<div className="filter-container">
<span>{currentFolder?.count ?? meta?.total_count} APPS</span>
<div className="d-flex align-items-center">
@ -900,7 +907,7 @@ class HomePageComponent extends React.Component {
</span>
</div>
)}
{(isLoading || meta.total_count > 0 || currentFolder.count === 0) && (
{(isLoading || meta.total_count > 0 || !_.isEmpty(currentFolder)) && (
<AppList
apps={apps}
canCreateApp={this.canCreateApp}

View file

@ -0,0 +1,188 @@
import React from 'react';
import '../../ManageGroupPermissionsV2/groupPermissions.theme.scss';
import ModalBase from '@/_ui/Modal';
import { AppsSelect } from '@/_ui/Modal/AppsSelect';
import AppPermissionsActions from './AppPermissionActionContainer';
import { OverlayTrigger, Tooltip } from 'react-bootstrap';
function AddEditResourcePermissionsModal({
handleClose,
handleConfirm,
updateParentState,
resourceType,
currentState,
show,
title,
confirmBtnProps,
disableBuilderLevelUpdate,
selectedApps,
setSelectedApps,
addableApps,
darkMode,
groupName,
}) {
const isCustom = currentState?.isCustom;
const newPermissionName = currentState?.newPermissionName;
const initialPermissionState = currentState?.initialPermissionState;
const errors = currentState?.errors;
const isAll = currentState?.isAll;
return (
<ModalBase
size="md"
show={show}
handleClose={handleClose}
handleConfirm={handleConfirm}
className="permission-manager-modal"
title={title}
confirmBtnProps={confirmBtnProps}
darkMode={darkMode}
>
<div className="form-group mb-3">
<label className="form-label bold-text" data-cy="permission-name-label">
Permission name
</label>
<div className="tj-app-input">
<input
type="text"
className={`form-control ${newPermissionName?.length == 50 ? 'error-input' : ''}`}
placeholder={'Eg. Product analytics apps'}
name="permissionName"
value={newPermissionName}
onChange={(e) => {
let value = e.target.value;
if (value?.length > 50) {
value = value.slice(0, 50);
}
updateParentState(() => ({
newPermissionName: value,
}));
}}
data-cy="permission-name-input"
/>
<span className="text-danger" data-cy="permission-name-error">
{errors['permissionName']}
</span>
</div>
<div className={`mt-1 tj-text-xxsm ${newPermissionName?.length > 50 ? 'error-text' : ''}`}>
<div data-cy="permission-name-help-text">Permission name must be unique and max 50 characters</div>
</div>
</div>
{/* Till here */}
<div className="form-group mb-3">
<label className="form-label bold-text" data-cy="permission-label">
Permission
</label>
<AppPermissionsActions
handleClickEdit={() => {
updateParentState((prevState) => ({
initialPermissionState: {
...prevState.initialPermissionState,
canEdit: !prevState.initialPermissionState.canEdit,
canView: prevState.initialPermissionState.canEdit,
...(!prevState.initialPermissionState.canEdit && { hideFromDashboard: false }),
},
}));
}}
handleClickView={() => {
updateParentState((prevState) => ({
initialPermissionState: {
...prevState.initialPermissionState,
canView: !prevState.initialPermissionState.canView,
canEdit: prevState.initialPermissionState.canView,
...(prevState.initialPermissionState.canEdit && { hideFromDashboard: false }),
},
}));
}}
handleHideFromDashboard={() => {
updateParentState((prevState) => ({
initialPermissionState: {
...initialPermissionState,
hideFromDashboard: !prevState.initialPermissionState.hideFromDashboard,
},
}));
}}
disableBuilderLevelUpdate={disableBuilderLevelUpdate}
initialPermissionState={initialPermissionState}
/>
</div>
<div className="form-group mb-3">
<label className="form-label bold-text" data-cy="resource-label">
Resources
</label>
<div className="resources-container" data-cy="resources-container">
<label className="form-check form-check-inline">
<input
className="form-check-input"
type="radio"
checked={isAll}
onClick={() => {
!isAll && updateParentState((prevState) => ({ isAll: !prevState.isAll, isCustom: !!prevState.isAll }));
}}
data-cy="all-apps-radio"
/>
<div>
<span className="form-check-label" data-cy="all-apps-label">
All apps
</span>
<span className="tj-text-xsm" data-cy="all-apps-info-text">
This will select all apps in the workspace including any new apps created
</span>
</div>
</label>
<OverlayTrigger
overlay={
disableBuilderLevelUpdate || resourceType === '' || groupName === 'builder' ? (
<Tooltip id="tooltip-disable-edit-update">Use custom groups to select custom resources</Tooltip>
) : (
<span></span>
)
}
placement="left"
>
<label className="form-check form-check-inline">
<input
className="form-check-input"
type="radio"
disabled={addableApps.length === 0 || disableBuilderLevelUpdate || groupName === 'builder'}
checked={isCustom}
onClick={() => {
!isCustom &&
updateParentState((prevState) => ({ isCustom: !prevState.isCustom, isAll: prevState.isCustom }));
}}
data-cy="custom-radio"
/>
<div>
<span
className="form-check-label"
style={{ color: disableBuilderLevelUpdate ? 'var(--text-disabled)' : '' }}
data-cy="custom-label"
>
Custom
</span>
<span
className="tj-text-xsm"
style={{ color: disableBuilderLevelUpdate ? 'var(--text-disabled)' : '' }}
data-cy="custom-info-text"
>
Select specific applications you want to add to the group
</span>
</div>
</label>
</OverlayTrigger>
<AppsSelect
disabled={!isCustom}
allowSelectAll={true}
value={selectedApps}
onChange={setSelectedApps}
options={addableApps}
data-value="test"
/>
</div>
</div>
</ModalBase>
);
}
export default AddEditResourcePermissionsModal;

View file

@ -0,0 +1,110 @@
import React from 'react';
import '../../ManageGroupPermissionsV2/groupPermissions.theme.scss';
import { OverlayTrigger, Tooltip } from 'react-bootstrap';
function AppPermissionsActions({
handleClickEdit,
handleClickView,
handleHideFromDashboard,
disableBuilderLevelUpdate,
initialPermissionState,
}) {
return (
<div className="type-container">
<div className="left-container">
<OverlayTrigger
overlay={
disableBuilderLevelUpdate ? (
<Tooltip id="tooltip-disable-edit-update">End-user cannot have edit permission</Tooltip>
) : (
<span></span>
)
}
placement="left"
>
<label className="form-check form-check-inline">
<input
className="form-check-input"
type="radio"
disabled={disableBuilderLevelUpdate}
checked={initialPermissionState.canEdit}
onClick={() => {
!initialPermissionState.canEdit && handleClickEdit();
}}
data-cy="edit-permission-radio"
/>
<div>
<span
className="form-check-label"
style={{ color: disableBuilderLevelUpdate ? 'var(--text-disabled)' : '' }}
data-cy="edit-permission-label"
>
Edit
</span>
<span
className="tj-text-xsm"
style={{ color: disableBuilderLevelUpdate ? 'var(--text-disabled)' : '' }}
data-cy="edit-permission-info-text"
>
Access to app builder
</span>
</div>
</label>
</OverlayTrigger>
</div>
<div className="right-container">
<label className="form-check form-check-inline">
<input
className="form-check-input"
type="radio"
disabled={disableBuilderLevelUpdate}
checked={initialPermissionState.canView}
onClick={() => {
!initialPermissionState.canView && handleClickView();
}}
data-cy="view-permission-radio"
/>
<div>
<span
className="form-check-label"
style={{ color: disableBuilderLevelUpdate ? 'var(--text-disabled)' : '' }}
data-cy="view-permission-label"
>
View
</span>
<span
className="tj-text-xsm"
style={{ color: disableBuilderLevelUpdate ? 'var(--text-disabled)' : '' }}
data-cy="view-permission-info-text"
>
Only access released version of apps
</span>
</div>
</label>
<label className="form-check form-check-inline">
<input
className="form-check-input"
type="checkbox"
disabled={!initialPermissionState.canView}
checked={initialPermissionState.hideFromDashboard}
onClick={() => {
handleHideFromDashboard();
}}
data-cy="hide-from-dashboard-permission-input"
/>
<div>
<span className={`form-check-label`} data-cy="hide-from-dashboard-permission-label">
Hide from dashboard
</span>
<span className="tj-text-xsm" data-cy="hide-from-dashboard-permission-info-text">
App will be accessible by URL only
</span>
</div>
</label>
</div>
</div>
);
}
export default AppPermissionsActions;

View file

@ -0,0 +1,62 @@
import React from 'react';
import '../ManageGroupPermissionsV2/groupPermissions.theme.scss';
import { ButtonSolid } from '@/_ui/AppButton/AppButton';
import { OverlayTrigger } from 'react-bootstrap';
function AddResourcePermissionsMenu({ openAddPermissionModal, resourcesOptions, currentGroupPermission }) {
return resourcesOptions.length > 1 ? (
<OverlayTrigger
rootClose={true}
trigger="click"
placement={'bottom'}
overlay={
<div className={`settings-card tj-text card ${this.props.darkMode && 'dark-theme'}`}>
<ButtonSolid
variant="tertiary"
iconWidth="17"
fill="var(--slate9)"
className="apps-remove-btn permission-type remove-decoration tj-text-xsm font-weight-600"
leftIcon="dashboard"
onClick={() => {
openAddPermissionModal();
}}
>
Apps
</ButtonSolid>
</div>
}
>
<div className={'cursor-pointer'}>
<ButtonSolid
variant="tertiary"
iconWidth="17"
fill="var(--slate9)"
className="add-icon tj-text-xsm font-weight-600"
leftIcon="plus"
disabled={currentGroupPermission.name === 'admin'}
>
Add permission
</ButtonSolid>
</div>
</OverlayTrigger>
) : (
<div className={'cursor-pointer'}>
<ButtonSolid
variant="tertiary"
iconWidth="17"
fill="var(--slate9)"
className="add-icon tj-text-xsm font-weight-600"
leftIcon="plus"
disabled={currentGroupPermission.name === 'admin'}
onClick={() => {
openAddPermissionModal();
}}
data-cy="add-apps-buton"
>
Add apps
</ButtonSolid>
</div>
);
}
export default AddResourcePermissionsMenu;

View file

@ -0,0 +1,174 @@
import React, { useState } from 'react';
import GroupChipTD from '@/ManageGroupPermissionsV2/ResourceChip';
import '../ManageGroupPermissionsV2/groupPermissions.theme.scss';
import SolidIcon from '@/_ui/Icon/SolidIcons';
import { ButtonSolid } from '@/_ui/AppButton/AppButton';
import { OverlayTrigger, Tooltip } from 'react-bootstrap';
import OverflowTooltip from '@/_components/OverflowTooltip';
function AppResourcePermissions({
updateOnlyGranularPermissions,
permissions,
currentGroupPermission,
openEditPermissionModal,
}) {
const [onHover, setHover] = useState(false);
const [notClickable, setNotClickable] = useState(false);
const isRoleGroup = currentGroupPermission.name == 'admin';
const disableEditUpdate = currentGroupPermission.name == 'end-user';
const appsPermissions = permissions.appsGroupPermissions;
let apps = appsPermissions?.groupApps?.map((app) => {
return app?.app?.name;
});
if (permissions.isAll) apps = ['All apps'];
return (
<div
className="manage-resource-permission"
onMouseEnter={() => {
setHover(true);
}}
onMouseLeave={() => {
setHover(false);
}}
onClick={() => {
!isRoleGroup && !notClickable && openEditPermissionModal(permissions);
}}
>
<div className="resource-name d-flex">
<SolidIcon name="app" width="20px" className="resource-icon" />
<div className="resource-text" data-cy={`${permissions.name.toLowerCase().replace(/\s+/g, '-')}-text`}>
<OverflowTooltip width={160}>{` ${permissions.name}`}</OverflowTooltip>
</div>
</div>
<div className="text-muted">
<div className="d-flex apps-permission-wrap flex-column">
<OverlayTrigger
overlay={
disableEditUpdate ? (
<Tooltip id="tooltip-disable-edit-update">End-user cannot have edit permission</Tooltip>
) : (
<span></span>
)
}
placement="top"
>
<div
onMouseEnter={() => {
setNotClickable(true);
}}
onMouseLeave={() => {
setNotClickable(false);
}}
>
<label className="form-check form-check-inline">
<input
className="form-check-input"
type="radio"
onClick={() => {
!appsPermissions.canEdit &&
updateOnlyGranularPermissions(permissions, {
canEdit: !appsPermissions.canEdit,
canView: appsPermissions.canEdit,
...(!appsPermissions.canEdit && { hideFromDashboard: false }),
});
}}
checked={appsPermissions.canEdit}
disabled={isRoleGroup || disableEditUpdate}
data-cy="app-edit-radio"
/>
<span className="form-check-label" data-cy="app-edit-label">
{'Edit'}
</span>
{/* <span class={`text-muted tj-text-xxsm ${isRoleGroup && 'check-label-disable'}`}>Create apps in this workspace</span> */}
<span class={`tj-text-xxsm`} data-cy="app-edit-helper-text">
Access to app builder
</span>
</label>
</div>
</OverlayTrigger>
<div
onMouseEnter={() => {
setNotClickable(true);
}}
onMouseLeave={() => {
setNotClickable(false);
}}
>
<label className="form-check form-check-inline">
<input
className="form-check-input"
type="radio"
onClick={() => {
!appsPermissions.canView &&
updateOnlyGranularPermissions(permissions, {
canView: !appsPermissions.canView,
canEdit: appsPermissions.canView,
});
}}
checked={appsPermissions.canView}
disabled={isRoleGroup || disableEditUpdate}
data-cy="app-view-radio"
/>
<span className="form-check-label" data-cy="app-view-label">
{'View'}
</span>
<span class={`tj-text-xxsm`} data-cy="app-view-helper-text">
Only access released version of apps
</span>
</label>
</div>
<div
onMouseEnter={() => {
setNotClickable(true);
}}
onMouseLeave={() => {
setNotClickable(false);
}}
>
<label className="form-check form-check-inline">
<input
className="form-check-input"
type="checkbox"
onChange={() => {
updateOnlyGranularPermissions(permissions, {
hideFromDashboard: !appsPermissions.hideFromDashboard,
});
}}
checked={appsPermissions.hideFromDashboard}
disabled={isRoleGroup || !appsPermissions.canView}
data-cy="app-hide-from-dashboard-radio"
/>
<span className="form-check-label" data-cy="app-hide-from-dashboard-label">
{'Hide from dashbaord'}
</span>
<span class={`tj-text-xxsm`} data-cy="app-hide-from-dashboard-helper-text">
App will be accessible by URL only
</span>
</label>
</div>
</div>
</div>
<div>
<GroupChipTD groups={apps} />
</div>
<div className="edit-icon-container">
{onHover && (
<ButtonSolid
leftIcon="editrectangle"
className="edit-permission-custom"
iconWidth="14"
onClick={() => {
openEditPermissionModal(permissions);
}}
disabled={isRoleGroup}
data-cy="edit-permission-button"
/>
)}
</div>
</div>
);
}
export default AppResourcePermissions;

View file

@ -0,0 +1,546 @@
import { ButtonSolid } from '@/_ui/AppButton/AppButton';
import SolidIcon from '@/_ui/Icon/SolidIcons';
import React from 'react';
import { OverlayTrigger, Tooltip } from 'react-bootstrap';
import { withTranslation } from 'react-i18next';
import { groupPermissionV2Service } from '@/_services';
import { toast } from 'react-hot-toast';
import '../ManageGroupPermissionsV2/groupPermissions.theme.scss';
import ChangeRoleModal from '@/ManageGroupPermissionResourcesV2/ChangeRoleModal';
import AppResourcePermissions from '@/ManageGranularAccess/AppResourcePermission';
import AddResourcePermissionsMenu from '@/ManageGranularAccess/AddResourcePermissionsMenu';
import { ConfirmDialog } from '@/_components';
import AddEditResourcePermissionsModal from '@/ManageGranularAccess/AddEditResourceModal/AddEditResourcePermissionsModal';
import Spinner from 'react-bootstrap/Spinner';
import { ToolTip } from '@/_components/ToolTip';
class ManageGranularAccessComponent extends React.Component {
constructor(props) {
super(props);
this.state = {
isLoading: false,
granularPermissions: [],
showAddPermissionModal: false,
errors: {},
values: {},
customSelected: true,
selectedApps: [],
type: null,
newPermissionName: null,
initialPermissionState: {
canEdit: false,
canView: false,
hideFromDashboard: false,
},
currentEditingPermissions: null,
isAll: true,
isCustom: false,
addableApps: [],
modalType: 'add',
modalTitle: 'Add app permissions',
showAutoRoleChangeModal: false,
autoRoleChangeModalMessage: '',
autoRoleChangeModalList: [],
autoRoleChangeMessageType: '',
updateParam: {},
updatingPermission: {},
updateType: '',
deleteConfirmationModal: false,
deletingPermissions: false,
};
}
componentDidMount() {
this.fetchAppsCanBeAdded();
this.fetchGranularPermissions(this.props.groupPermissionId);
}
fetchAppsCanBeAdded = () => {
groupPermissionV2Service
.fetchAddableApps()
.then((data) => {
const addableApps = data.map((app) => {
return {
name: app.name,
value: app.id,
label: app.name,
};
});
this.setState({
addableApps,
});
})
.catch((err) => {
toast.error(err.error);
});
};
fetchGranularPermissions = (groupPermissionId) => {
this.setState({
isLoading: true,
});
groupPermissionV2Service.fetchGranularPermissions(groupPermissionId).then((data) => {
this.setState({
granularPermissions: data,
isLoading: false,
});
});
};
deleteGranularPermissions = () => {
const { currentEditingPermissions } = this.state;
this.setState({
deleteGranularPermissions: true,
});
groupPermissionV2Service
.deleteGranularPermission(currentEditingPermissions.id)
.then(() => {
toast.success('Deleted permission successfully');
this.fetchGranularPermissions(this.props.groupPermissionId);
this.closeAddPermissionModal();
})
.catch((err) => {
toast.error(err.error);
})
.finally(() => {
this.setState({
deleteConfirmationModal: false,
deleteGranularPermissions: false,
});
});
};
createGranularPermissions = () => {
const { initialPermissionState, isAll, newPermissionName, isCustom, selectedApps } = this.state;
if (isCustom && selectedApps.length == 0) {
toast.error('Please select the apps');
return;
}
const body = {
name: newPermissionName,
type: 'app',
groupId: this.props.groupPermissionId,
isAll: isAll,
createAppsPermissionsObject: {
...initialPermissionState,
resourcesToAdd: selectedApps.filter((apps) => !apps?.isAllField)?.map((option) => ({ appId: option.value })),
},
};
groupPermissionV2Service
.createGranularPermission(body)
.then(() => {
this.fetchGranularPermissions(this.props.groupPermissionId);
this.closeAddPermissionModal();
toast.success('Permission created successfully!');
})
.catch(({ error }) => {
this.closeAddPermissionModal();
if (error?.error) {
this.props.updateParentState({
showEditRoleErrorModal: true,
errorTitle: error?.title ? error?.title : 'Cannot add granular permissions',
errorMessage: error.error,
errorIconName: 'usergear',
errorListItems: error.data,
});
return;
}
toast.error(error, {
style: {
maxWidth: '500px',
},
});
});
// .then(())
};
openEditPermissionModal = (granularPermission) => {
const currentApps = granularPermission?.appsGroupPermissions?.groupApps;
const appsGroupPermission = granularPermission?.appsGroupPermissions;
this.setState({
currentEditingPermissions: granularPermission,
modalTitle: 'Edit app permissions',
showAddPermissionModal: true,
modalType: 'edit',
isAll: !!granularPermission.isAll,
isCustom: !granularPermission.isAll,
newPermissionName: granularPermission.name,
initialPermissionState: {
canEdit: appsGroupPermission.canEdit,
canView: appsGroupPermission.canView,
hideFromDashboard: appsGroupPermission.hideFromDashboard,
},
selectedApps:
currentApps?.length > 0
? currentApps?.map(({ app }) => {
return {
name: app.name,
value: app.id,
label: app.name,
};
})
: [],
});
};
updateOnlyGranularPermissions = (permission, actions = {}, allowRoleChange) => {
const body = {
actions: actions,
allowRoleChange,
};
groupPermissionV2Service
.updateGranularPermission(permission.id, body)
.then(() => {
this.fetchGranularPermissions(this.props.groupPermissionId);
this.closeAddPermissionModal();
toast.success('Permission updated successfully');
})
.catch(({ error }) => {
if (error?.type) {
this.setState({
showAutoRoleChangeModal: true,
autoRoleChangeModalMessage: error?.error,
autoRoleChangeModalList: error?.data,
autoRoleChangeMessageType: error?.type,
updateParam: actions,
updatingPermission: permission,
updateType: 'ONLY_PERMISSIONS',
});
return;
}
this.props.updateParentState({
showEditRoleErrorModal: true,
errorTitle: error?.title ? error?.title : 'Cannot update the permissions',
errorMessage: error.error,
errorIconName: 'usergear',
errorListItems: error.data,
showAddPermissionModal: false,
});
});
};
updateGranularPermissions = (allowRoleChange) => {
const { currentEditingPermissions, selectedApps, newPermissionName, isAll, initialPermissionState } = this.state;
const currentResource = currentEditingPermissions?.appsGroupPermissions?.groupApps?.map((app) => {
return app.app.id;
});
const selectedResource = selectedApps.filter((apps) => !apps?.isAllField)?.map((resource) => resource.value);
const resourcesToAdd = selectedResource
?.filter((item) => !currentResource.includes(item))
.map((id) => {
return {
appId: id,
};
});
const appsToDelete = currentResource?.filter((item) => !selectedResource?.includes(item));
const groupAppsToDelete = currentEditingPermissions?.appsGroupPermissions?.groupApps?.filter((groupApp) =>
appsToDelete?.includes(groupApp.appId)
);
const resourcesToDelete = groupAppsToDelete?.map(({ id }) => {
return {
id: id,
};
});
const body = {
name: newPermissionName,
isAll: isAll,
actions: initialPermissionState,
resourcesToAdd,
resourcesToDelete,
allowRoleChange,
};
groupPermissionV2Service
.updateGranularPermission(currentEditingPermissions.id, body)
.then(() => {
this.fetchGranularPermissions(this.props.groupPermissionId);
this.closeAddPermissionModal();
toast.success('Permission updated successfully');
})
.catch(({ error }) => {
if (error?.type) {
this.setState({
showEditRoleErrorModal: false,
showAutoRoleChangeModal: true,
autoRoleChangeModalMessage: error?.error,
autoRoleChangeModalList: error?.data,
autoRoleChangeMessageType: error?.type,
updateType: '',
showAddPermissionModal: false,
});
return;
}
toast.error(error, {
style: {
maxWidth: '500px',
},
});
this.closeAddPermissionModal();
});
};
showPermissionText = (groupPermission) => {
const text =
groupPermission.name === 'admin'
? 'Admin has edit access to all apps. These are not editable'
: 'End-user can only have permission to view apps';
return (
<div className="manage-granular-permissions-info">
<p
className="tj-text-xsm"
style={{ display: 'flex', alignItems: 'center', gap: '4px' }}
data-cy="helper-text-admin-app-access"
>
<SolidIcon name="informationcircle" fill="#3E63DD" /> {text}
<a
style={{ margin: '0', padding: '0', textDecoration: 'underline', color: '#3E63DD' }}
href="https://docs.tooljet.com/docs/tutorial/manage-users-groups/"
target="_blank"
rel="noopener noreferrer"
>
read documentation
</a>{' '}
to know more !
</p>
</div>
);
};
openAddPermissionModal = () => {
this.setState((prevState) => ({
showAddPermissionModal: true,
initialPermissionState: { ...prevState.initialPermissionState, canView: true },
isAll: true,
}));
};
closeAddPermissionModal = () => {
this.setState({
currentEditingPermissions: null,
modalTitle: 'Add app permissions',
showAddPermissionModal: false,
modalType: 'add',
isAll: false,
isCustom: false,
newPermissionName: '',
initialPermissionState: {
canEdit: false,
canView: false,
hideFromDashboard: false,
},
selectedApps: [],
});
};
setSelectedApps = (values) => {
this.setState({ selectedApps: values });
};
handleAutoRoleChangeModalClose = () => {
this.setState({
showAutoRoleChangeModal: false,
autoRoleChangeModalMessage: '',
autoRoleChangeModalList: [],
autoRoleChangeMessageType: '',
updateParam: {},
isLoading: false,
updatingPermission: {},
updateType: '',
});
};
handleConfirmAutoRoleChangeGroupUpdate = () => {
this.updateGranularPermissions(true);
this.handleAutoRoleChangeModalClose();
};
updateState = (stateUpdater) => {
this.setState((prevState) => stateUpdater(prevState));
};
handleConfirmAutoRoleChangeOnlyGroupUpdate = () => {
const { updateParam, updatingPermission } = this.state;
this.updateOnlyGranularPermissions(updatingPermission, updateParam, true);
this.handleAutoRoleChangeModalClose();
};
render() {
const {
showAddPermissionModal,
selectedApps,
isCustom,
granularPermissions,
isLoading,
addableApps,
modalTitle,
modalType,
newPermissionName,
showAutoRoleChangeModal,
autoRoleChangeModalList,
autoRoleChangeMessageType,
updateType,
deleteConfirmationModal,
deletingPermissions,
} = this.state;
const resourcesOptions = ['Apps'];
const currentGroupPermission = this.props?.groupPermission;
const isRoleGroup = currentGroupPermission.name == 'admin';
const defaultGroup = currentGroupPermission.type === 'default';
const showPermissionInfo = currentGroupPermission.name == 'admin' || currentGroupPermission.name == 'end-user';
const disableEditUpdate = currentGroupPermission.name == 'end-user';
const addPermissionTooltipMessage = !newPermissionName
? 'Please input permissions name'
: isCustom && selectedApps.length === 0
? 'Please select apps or select all apps option'
: '';
return (
<div>
<ConfirmDialog
show={deleteConfirmationModal}
message={'This permission will be permanently deleted. Do you want to continue?'}
confirmButtonLoading={deletingPermissions}
onConfirm={() => this.deleteGranularPermissions()}
onCancel={() => {
this.setState({ deleteConfirmationModal: false, deletingPermissions: false });
}}
darkMode={this.props.darkMode}
/>
<ChangeRoleModal
showAutoRoleChangeModal={showAutoRoleChangeModal}
autoRoleChangeModalList={autoRoleChangeModalList}
autoRoleChangeMessageType={autoRoleChangeMessageType}
handleAutoRoleChangeModalClose={this.handleAutoRoleChangeModalClose}
handleConfirmation={
updateType === 'ONLY_PERMISSIONS'
? this.handleConfirmAutoRoleChangeOnlyGroupUpdate
: this.handleConfirmAutoRoleChangeGroupUpdate
}
darkMode={this.props.darkMode}
isLoading={isLoading}
/>
<AddEditResourcePermissionsModal
handleClose={this.closeAddPermissionModal}
handleConfirm={
modalType === 'add'
? this.createGranularPermissions
: () => {
this.updateGranularPermissions();
}
}
updateParentState={this.updateState}
resourceType="app"
currentState={this.state}
show={showAddPermissionModal}
title={
<div className="my-3 permission-manager-title" data-cy="modal-title">
<span className="font-weight-500">
<SolidIcon name="apps" />
</span>
<div className="tj-text-md font-weight-500" data-cy="modal-title">
{modalTitle}
</div>
{modalType === 'edit' && !isRoleGroup && (
<div className="delete-icon-cont">
<ButtonSolid
leftIcon="delete"
iconWidth="15px"
className="icon-class"
variant="tertiary"
onClick={() => {
this.setState({
deleteConfirmationModal: true,
showAddPermissionModal: false,
});
}}
data-cy="delete-button"
/>
</div>
)}
</div>
}
confirmBtnProps={{
title: `${modalType === 'edit' ? 'Update' : 'Add'}`,
iconLeft: 'plus',
disabled: (modalType === 'add' && !newPermissionName) || (isCustom && selectedApps.length === 0),
tooltipMessage: addPermissionTooltipMessage,
}}
disableBuilderLevelUpdate={disableEditUpdate}
selectedApps={selectedApps}
setSelectedApps={this.setSelectedApps}
addableApps={addableApps}
darkMode={this.props.darkMode}
groupName={currentGroupPermission.name}
/>
{!granularPermissions.length && !isLoading ? (
<div className="empty-container">
<div className="icon-container" data-cy="empty-page-svg">
<SolidIcon name="granularaccess" />
</div>
<p className="my-2 tj-text-md font-weight-500" data-cy="empty-page-title">
No permissions added yet
</p>
<p className="tj-text-xsm mb-2" data-cy="empty-page-info-text">
Add assets to configure granular, asset-level permissions for this user group
</p>
<div className="menu">
<AddResourcePermissionsMenu
openAddPermissionModal={this.openAddPermissionModal}
resourcesOptions={resourcesOptions}
currentGroupPermission={currentGroupPermission}
/>
</div>
</div>
) : (
<>
{showPermissionInfo && this.showPermissionText(currentGroupPermission)}
<div className="manage-granular-permission-header">
<p data-cy="name-header" className="tj-text-xsm">
{'Name'}
</p>
<p data-cy="permissions-header" className="tj-text-xsm">
{'Permission'}
</p>
<p data-cy="resource-header" className="tj-text-xsm">
{'Resource'}
</p>
</div>
<div className={showPermissionInfo ? 'permission-body-one' : 'permission-body-two'}>
{isLoading ? (
<div
className="d-flex justify-content-center align-items-center"
style={{ height: 'calc(100vh - 470px)' }}
>
<Spinner variant="primary" />
</div>
) : (
<>
{granularPermissions.map((permissions, index) => (
<AppResourcePermissions
updateOnlyGranularPermissions={this.updateOnlyGranularPermissions}
permissions={permissions}
currentGroupPermission={currentGroupPermission}
openEditPermissionModal={this.openEditPermissionModal}
key={index}
/>
))}
</>
)}
</div>
</>
)}
{granularPermissions.length > 0 && (
<div className="side-button-cont">
<AddResourcePermissionsMenu
openAddPermissionModal={this.openAddPermissionModal}
resourcesOptions={resourcesOptions}
currentGroupPermission={currentGroupPermission}
/>
</div>
)}
</div>
);
}
}
export const ManageGranularAccess = withTranslation()(ManageGranularAccessComponent);

View file

@ -0,0 +1,75 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import ModalBase from '@/_ui/Modal';
import '../ManageGroupPermissionsV2/groupPermissions.theme.scss';
function ChangeRoleModal({
showAutoRoleChangeModal,
autoRoleChangeModalList,
autoRoleChangeMessageType,
handleAutoRoleChangeModalClose,
handleConfirmation,
darkMode,
isLoading,
}) {
const { t } = useTranslation();
const renderUserChangeMessage = (type) => {
const changePermissionMessage = (
<p className="tj-text-sm">
Granting this permission to the user group will result in a role change for the following user(s) from{' '}
<b>end-users</b> to <b>builders</b>. Are you sure you want to continue?
</p>
);
const addUserMessage = (
<p className="tj-text-sm">
Adding the following user(s) to this group will change their default group from <b>end-users</b> to{' '}
<b>builders</b>. Are you sure you want to continue?
</p>
);
const message = type === 'USER_ROLE_CHANGE_ADD_USERS' ? addUserMessage : changePermissionMessage;
return message;
};
const renderUserChangeTitle = (type) => {
const addUserTitle = (
<div className="my-3" data-cy="modal-title">
<span className="tj-text-md font-weight-500">Add user(s)</span>
</div>
);
const updatePermissionTitile = (
<div className="my-3" data-cy="modal-title">
<span className="tj-text-md font-weight-500"> Change in user role</span>
</div>
);
const message = type === 'USER_ROLE_CHANGE_ADD_USERS' ? addUserTitle : updatePermissionTitile;
return message;
};
return (
<ModalBase
title={renderUserChangeTitle(autoRoleChangeMessageType)}
handleConfirm={handleConfirmation}
confirmBtnProps={{ title: 'Continue', tooltipMessage: false }}
show={showAutoRoleChangeModal}
handleClose={handleAutoRoleChangeModalClose}
darkMode={darkMode}
isLoading={isLoading}
className="edit-role-confirm"
>
<>
{renderUserChangeMessage(autoRoleChangeMessageType)}
<p></p>
<div className="item-list">
{autoRoleChangeModalList.map((item, index) => (
<div key={index}>
<span className="tj-text-sm">{`${index + 1}. ${item}`}</span>
</div>
))}
</div>
</>
</ModalBase>
);
}
export default ChangeRoleModal;

View file

@ -0,0 +1,86 @@
import React from 'react';
const endUserToAdminMessage =
"Updating the user's details will change their role from end-user to admin. Are you sure you want to continue?";
const endUserToBuilderMessage =
"Updating the user's details will change their role from end-user to builder. Are you sure you want to continue?";
export const EDIT_ROLE_MESSAGE = {
admin: {
builder: () => {
return (
<div>
<p className="tj-text-sm" style={{ marginBottom: '10px' }}>
Changing your user default group from admin to builder will revoke your access to settings.
</p>
<p className="tj-text-sm">Are you sure you want to continue?</p>
</div>
);
},
'end-user': (isPaidPlan) => {
return (
<div>
<p className="tj-text-sm" style={{ marginBottom: '10px' }}>
Changing your user group from admin to end-user will revoke your access to settings.
{isPaidPlan && 'This will also affect the count of users covered by your plan.'}
</p>
<p className="tj-text-sm">Are you sure you want to continue?</p>
</div>
);
},
},
builder: {
'end-user': (isPaidPlan) => {
return (
<div>
{isPaidPlan && (
<p className="tj-text-sm" style={{ marginBottom: '10px' }}>
Changing user default group from builder to end-user will affect the count of users covered by your plan.
</p>
)}
<p className="tj-text-sm">
This will also remove the user from any custom groups with builder-like permissions.
</p>
<p className="tj-text-sm">Are you sure you want to continue?</p>
</div>
);
},
admin: () => {
return (
<div>
<p className="tj-text-sm" style={{ marginBottom: '10px' }}>
Changing user role from builder to admin will grant access to all resources and settings.
</p>
<p className="tj-text-sm">Are you sure you want to continue?</p>
</div>
);
},
},
'end-user': {
builder: (isPaidPlan) => {
return (
<div>
{isPaidPlan && (
<p className="tj-text-sm" style={{ marginBottom: '10px' }}>
Changing user default group from end-user to builder will affect the count of users covered by your plan.
</p>
)}
<p className="tj-text-sm">{endUserToBuilderMessage}</p>
</div>
);
},
admin: (isPaidPlan) => {
return (
<div>
{isPaidPlan && (
<p className="tj-text-sm" style={{ marginBottom: '10px' }}>
Changing user default group from end-user to admin will affect the count of users covered by your plan.
</p>
)}
<p className="tj-text-sm">{endUserToAdminMessage}</p>
</div>
);
},
},
};

View file

@ -0,0 +1,146 @@
@import '../_styles/colors.scss';
.check-label-disable{
color: var(--slate10) !important;
}
.info-container {
display: flex;
width: auto;
height: auto;
padding: 10px 12px 8px 12px;
border: 1px solid var(--slate5);
background: var(--slate2);
border-radius: 6px 6px 6px 6px;
margin-top: 13px;
.info-btn {
padding: 6px 0px 0px 0px;
margin-right: 5px;
flex: 0 0 24px;
}
.message {
margin-left: 5px;
display: inline-block;
word-wrap: break-word;
width: auto;
height: auto;
font-size: 12px;
line-height: 13px;
color: var(--slate11);
p{
padding: 0;
margin: 0;
}
.open-git-btn {
margin-top: 5px;
color: var(--indigo9);
font-size: 10px;
font-weight: 500;
display: inline-block;
.open-icn {
margin-right: 2px;
}
}
}
}
.search-user-group-btn {
width: 20px;
margin-left: 2px;
margin-right: 7px;
height: 20px;
padding: 0 0;
background: none !important;
background-color: none !important;
box-shadow: none;
}
.searchbox-custom{
.tj-common-search-input-user {
width: 600px;
.input-icon-addon {
padding-right: 8px;
padding-left: 8px;
}
input {
box-sizing: border-box;
display: flex;
flex-direction: row;
align-items: center;
padding: 4px 8px !important;
gap: 16px;
width: 600px !important;
height: 28px !important;
background: var(--base);
border: 1px solid var(--slate7);
border-radius: 6px;
color: var(--slate12);
padding-left: 33px !important;
::placeholder {
color: var(--slate9);
margin-left: 5px !important;
padding-left: 5px !important;
background-color: red !important;
}
&:hover {
background: var(--slate2);
border: 1px solid var(--slate8);
}
&:active {
background: var(--indigo2);
border: 2px solid var(--indigo11);
box-shadow: 0px 0px 0px 2px #C6D4F9;
outline: none;
}
&:focus-visible {
background: var(--slate2);
border: 1px solid var(--slate8);
border-radius: 6px;
outline: none;
padding-left: 12px !important;
}
&:disabled {
background: var(--slate3);
border: 1px solid var(--slate7);
}
}
}
}
.edit-role-confirm {
width: 350px;
.modal-footer {
border-top: 1px solid var(--slate6);;
}
.form-label{
color: var(--slate11);
}
}
.permission-body {
.tj-text-xxsm{
color: var(--slate11)
}
}

File diff suppressed because it is too large Load diff

View file

@ -455,7 +455,7 @@ class ManageGroupPermissionsComponent extends React.Component {
title={
showGroupNameUpdateForm
? this.props.t('header.organization.menus.manageGroups.permissions.updateGroup', 'Update group')
: this.props.t('header.organization.menus.manageGroups.permissions.addNewGroup', 'Add new group')
: this.props.t('header.organization.menus.manageGroups.permissions.addNewGroup', 'Create new group')
}
>
<form

View file

@ -0,0 +1,57 @@
import React from 'react';
import { Modal } from 'react-bootstrap';
import { useTranslation } from 'react-i18next';
import SolidIcon from '@/_ui/Icon/SolidIcons';
import { ButtonSolid } from '@/_ui/AppButton/AppButton';
import '../groupPermissions.theme.scss';
function EditRoleErrorModal({
darkMode,
errorTitle = '',
listItems = [],
errorMessage = '',
show = false,
iconName = '',
onClose,
}) {
const { t } = useTranslation();
return (
<Modal
className={`edit-role-modal ${darkMode ? 'dark-mode' : ''}`}
aria-labelledby="contained-modal-title-vcenter"
centered
show={show}
>
<Modal.Header>
<div className="remove-icon-container">
<ButtonSolid
className="close-btn"
leftIcon="remove"
onClick={() => onClose()}
data-cy="close-button"
iconWidth="20"
/>
</div>
<div className="icon-class" data-cy="modal-icon">
<SolidIcon name={iconName} width="32" fill="#E54D2E" />
</div>
<span className="header-text" data-cy="modal-header">
{errorTitle}
</span>
<p data-cy="modal-message">{errorMessage}</p>
</Modal.Header>
<Modal.Body>
<div className="item-list">
{listItems.map((item, index) => (
<div key={index}>
<span className="tj-text-sm">{`${index + 1}. ${item}`}</span>
</div>
))}
</div>
</Modal.Body>
</Modal>
);
}
export default EditRoleErrorModal;

View file

@ -0,0 +1,799 @@
import React from 'react';
import { groupPermissionV2Service } from '@/_services';
import { Tooltip } from 'react-tooltip';
import { ConfirmDialog } from '@/_components';
import { toast } from 'react-hot-toast';
import { withTranslation } from 'react-i18next';
import ErrorBoundary from '@/Editor/ErrorBoundary';
import Modal from '../HomePage/Modal';
import { ButtonSolid } from '@/_ui/AppButton/AppButton';
import FolderList from '@/_ui/FolderList/FolderList';
import { Loader } from '../ManageSSO/Loader';
import Popover from 'react-bootstrap/Popover';
import SolidIcon from '@/_ui/Icon/solidIcons/index';
import ModalBase from '@/_ui/Modal';
import OverflowTooltip from '@/_components/OverflowTooltip';
import { ManageGroupPermissionResourcesV2 } from '@/ManageGroupPermissionResourcesV2';
import './groupPermissions.theme.scss';
import { SearchBox } from '@/_components/SearchBox';
class ManageGroupPermissionsComponent extends React.Component {
constructor(props) {
super(props);
this.state = {
isLoading: true,
groups: [],
defaultGroups: [],
creatingGroup: false,
showNewGroupForm: false,
newGroupName: '',
isDeletingGroup: false,
isUpdatingGroupName: false,
showGroupDeletionConfirmation: false,
showGroupNameUpdateForm: false,
groupToBeUpdated: null,
isSaveBtnDisabled: false,
selectedGroupPermissionId: null,
selectedGroup: 'Admin',
isDuplicatingGroup: false,
selectedGroupObject: null,
groupDuplicateOption: { addPermission: true, addApps: true, addUsers: true },
showDuplicateGroupModal: false,
groupToDuplicate: '',
showGroupSearchBar: false,
filteredGroup: [],
groupNameMessage: 'Group name must be unique and max 50 characters',
};
}
componentDidMount() {
this.fetchGroups();
}
findCurrentGroupDetails = (data) => {
let currentUpdatedGroup = data.find((item) => {
return item.name?.trim() == this.state.newGroupName?.trim();
});
this.setState({ selectedGroup: currentUpdatedGroup.name });
return currentUpdatedGroup.id;
};
duplicateGroup = () => {
const { groupDuplicateOption, groupToDuplicate } = this.state;
this.setState({ isDuplicatingGroup: true });
groupPermissionV2Service
.duplicate(groupToDuplicate, groupDuplicateOption)
.then((data) => {
this.setState({
newGroupName: data?.name,
});
this.fetchGroups('current', () => {
this.setState({
newGroupName: '',
creatingGroup: false,
selectedGroupPermissionId: data?.id,
selectedGroup: data?.name,
isDuplicatingGroup: false,
showDuplicateGroupModal: false,
groupDuplicateOption: { addPermission: true, addApps: true, addUsers: true },
});
});
toast.success('Group duplicated successfully!');
})
.catch((err) => {
this.setState({
isDuplicatingGroup: false,
creatingGroup: false,
groupDuplicateOption: { addPermission: true, addApps: true, addUsers: true },
});
console.error('Error occured in duplicating: ', err);
toast.error('Could not duplicate group.\nPlease try again!');
});
};
toggleShowDuplicateModal = () => {
this.setState((prevState) => ({
showDuplicateGroupModal: !prevState.showDuplicateGroupModal,
groupToDuplicate: '',
groupDuplicateOption: { addPermission: true, addApps: true, addUsers: true },
}));
};
renderPopoverContent = (props, compoParam) => {
const { groupName, id } = compoParam;
const deleteGroup = () => {
this.deleteGroup(id);
};
const duplicateGroup = () => {
this.showDuplicateDiologBox(id);
};
const isDefaultGroup = groupName == 'end-user' || groupName == 'admin' || groupName == 'builder';
return (
<div
{...props}
style={{
position: 'absolute',
...props.style,
}}
>
<Popover
id="popover-group-menu"
className={this.props.darkMode ? 'popover-group-menu dark-theme' : 'popover-group-menu'}
placement="bottom"
>
<Popover.Body bsPrefix="popover-body">
<div>
<Field
customClass={this.props.darkMode ? 'dark-theme' : ''}
leftIcon="copy"
leftIconWidth="20"
leftViewBox="0 0 20 20"
text={'Duplicate group'}
onClick={duplicateGroup}
/>
<Field
customClass={this.props.darkMode ? 'dark-theme' : ''}
leftIcon="delete"
leftIconWidth="18"
leftIconHeight="18"
leftViewBox="0 0 20 20"
text={'Delete group'}
tooltipId="tooltip-for-delete"
tooltipContent="Cannot delete default group"
onClick={isDefaultGroup ? {} : deleteGroup}
buttonDisable={isDefaultGroup}
darkMode={this.props.darkMode}
/>
</div>
</Popover.Body>
</Popover>
{(groupName == 'all_users' || groupName == 'admin' || groupName == 'builder' || groupName == 'end-user') && (
<Tooltip
id="tooltip-for-delete"
className="tooltip"
place="left"
style={{
zIndex: 99999,
}}
show={isDefaultGroup}
/>
)}
</div>
);
};
sortDefaultGroup = (list) => {
const priority = {
admin: 1,
builder: 2,
'end-user': 3,
};
list.sort((a, b) => {
const priorityA = priority[a.name] || 4; // default to 4 if not found
const priorityB = priority[b.name] || 4; // default to 4 if not found
return priorityA - priorityB;
});
return list;
};
fetchGroups = (type = 'admin', callback = () => {}) => {
this.setState({
isLoading: true,
});
groupPermissionV2Service
.getGroups()
.then((data) => {
const groupPermissions = data.groupPermissions;
const defaultGroups = this.sortDefaultGroup(groupPermissions.filter((group) => group.type === 'default'));
const currentGroupId =
type == 'admin'
? defaultGroups[0].id
: type == 'current'
? this.findCurrentGroupDetails(groupPermissions)
: groupPermissions.at(-1).id;
this.setState(
{
groups: groupPermissions.filter((group) => group.type === 'custom'),
defaultGroups: defaultGroups,
filteredGroup: groupPermissions.filter((group) => group.type === 'custom'),
isLoading: false,
selectedGroupPermissionId: currentGroupId,
selectedGroupObject: groupPermissions.find((group) => group.id === currentGroupId),
},
callback
);
})
.catch(({ error }) => {
toast.error(error);
this.setState({
isLoading: false,
});
});
};
handleGroupSearch = (e) => {
const { groups } = this.state;
let filteredGroup = groups;
const value = e?.target?.value;
if (value) {
filteredGroup = groups.filter((group) => group.name.toLowerCase().includes(value.toLowerCase()));
}
this.setState({
filteredGroup,
});
};
changeNewGroupName = (value) => {
if (value.length > 50) {
this.setState({
newGroupName: value?.slice(0, 50).trim(),
isSaveBtnDisabled: false,
});
return;
}
this.setState({
newGroupName: value,
isSaveBtnDisabled: false,
groupNameMessage: 'Group name must be unique and max 50 characters',
});
if ((this.state.groupToBeUpdated && this.state.groupToBeUpdated.name === value) || !value) {
this.setState({
isSaveBtnDisabled: true,
});
}
};
humanizeifDefaultGroupName = (groupName) => {
switch (groupName) {
case 'end-user':
return 'End-user';
case 'admin':
return 'Admin';
case 'builder':
return 'Builder';
default:
return groupName;
}
};
createGroup = () => {
const regex = /^[a-zA-Z0-9_ -]+$/;
if (!regex.test(this.state.newGroupName)) {
toast.error('Group name can only contain letters, numbers, underscores and hyphens');
return;
}
this.setState({ creatingGroup: true });
groupPermissionV2Service
.create(this.state.newGroupName)
.then(() => {
this.setState({
creatingGroup: false,
showNewGroupForm: false,
newGroupName: null,
selectedGroup: this.state.newGroupName,
});
toast.success('Group has been created');
this.fetchGroups('new');
})
.catch(({ error }) => {
toast.error(error, {
style: {
maxWidth: '500px',
},
});
this.setState({
creatingGroup: false,
showNewGroupForm: true,
});
});
};
deleteGroup = (groupPermissionId) => {
this.setState({
showGroupDeletionConfirmation: true,
groupToBeDeleted: groupPermissionId,
});
};
updateGroupName = (groupPermission) => {
this.setState({
showGroupNameUpdateForm: true,
groupToBeUpdated: groupPermission,
newGroupName: groupPermission.name,
isSaveBtnDisabled: true,
});
};
cancelDeleteGroupDialog = () => {
this.setState({
isDeletingGroup: false,
groupToBeDeleted: null,
showGroupDeletionConfirmation: false,
});
};
executeGroupDeletion = () => {
this.setState({ isDeletingGroup: true });
groupPermissionV2Service
.del(this.state.groupToBeDeleted)
.then(() => {
toast.success('Group deleted successfully');
this.fetchGroups();
this.setState({ selectedGroup: 'Admin', isDeletingGroup: false });
})
.catch(({ error }) => {
toast.error(error);
})
.finally(() => {
this.cancelDeleteGroupDialog();
});
};
handleGroupSearchClose = () => {
this.setState((prevState) => ({
showGroupSearchBar: false,
filteredGroup: prevState.groups,
}));
};
showDuplicateDiologBox = (id) => {
this.setState({ groupToDuplicate: id, showDuplicateGroupModal: true, isDuplicatingGroup: false });
};
executeGroupUpdation = () => {
this.setState({ isUpdatingGroupName: true });
groupPermissionV2Service
.update(this.state.groupToBeUpdated?.id, { name: this.state.newGroupName })
.then(() => {
toast.success('Group name updated successfully');
this.fetchGroups('current');
this.setState({
isUpdatingGroupName: false,
groupToBeUpdated: null,
showGroupNameUpdateForm: false,
selectedGroup: this.state.newGroupName,
});
})
.catch(({ error }) => {
toast.error(error, {
style: {
maxWidth: '500px',
},
});
this.setState({
isUpdatingGroupName: false,
});
});
};
render() {
const {
isLoading,
showNewGroupForm,
showGroupNameUpdateForm,
creatingGroup,
isUpdatingGroupName,
groups,
isDeletingGroup,
showGroupDeletionConfirmation,
showDuplicateGroupModal,
isDuplicatingGroup,
groupDuplicateOption,
defaultGroups,
filteredGroup,
showGroupSearchBar,
} = this.state;
const grounNameErrorStyle =
this.state.newGroupName?.length > 50 ? { color: '#ff0000', borderColor: '#ff0000' } : {};
const { addPermission, addApps, addUsers } = groupDuplicateOption;
const allFalse = [addPermission, addApps, addUsers].every((value) => !value);
return (
<ErrorBoundary showFallback={true}>
<div className="wrapper org-users-page animation-fade">
<div className="org-users-page-container">
<ConfirmDialog
show={showGroupDeletionConfirmation}
message={'This group will be permanently deleted. Do you want to continue?'}
confirmButtonLoading={isDeletingGroup}
onConfirm={() => this.executeGroupDeletion()}
onCancel={() => this.cancelDeleteGroupDialog()}
darkMode={this.props.darkMode}
/>
<ModalBase
show={showDuplicateGroupModal}
handleConfirm={this.duplicateGroup}
handleClose={this.toggleShowDuplicateModal}
title="Duplicate group"
confirmBtnProps={{ title: 'Duplicate', disabled: allFalse, tooltipMessage: false }}
isLoading={isDuplicatingGroup}
cancelDisabled={isDuplicatingGroup}
data-cy="modal-title"
darkMode={this.props.darkMode}
>
<div className="tj-text" data-cy="modal-message">
Duplicate the following parts of the group
</div>
<div className="group-duplcate-modal-body">
<div className="row check-row">
<div className="col-1 ">
<input
class="form-check-input"
checked={addUsers}
type="checkbox"
onChange={() => {
this.setState((prevState) => ({
groupDuplicateOption: {
...prevState.groupDuplicateOption,
addUsers: !prevState.groupDuplicateOption.addUsers,
},
}));
}}
data-cy="users-check-input"
/>
</div>
<div className="col-11">
<div className="tj-text " data-cy="users-label">
Users
</div>
</div>
</div>
<div className="row check-row">
<div className="col-1 ">
<input
class="form-check-input"
checked={addPermission}
type="checkbox"
onChange={() => {
this.setState((prevState) => ({
groupDuplicateOption: {
...prevState.groupDuplicateOption,
addPermission: !prevState.groupDuplicateOption.addPermission,
},
}));
}}
data-cy="permissions-check-input"
/>
</div>
<div className="col-11">
<div className="tj-text " data-cy="permissions-label">
Permissions
</div>
</div>
</div>
<div className="row check-row">
<div className="col-1 ">
<input
class="form-check-input"
checked={addApps}
type="checkbox"
onChange={() => {
this.setState((prevState) => ({
groupDuplicateOption: {
...prevState.groupDuplicateOption,
addApps: !prevState.groupDuplicateOption.addApps,
},
}));
}}
data-cy="apps-check-input"
/>
</div>
<div className="col-11">
<div className="tj-text " data-cy="apps-label">
Apps
</div>
</div>
</div>
</div>
</ModalBase>
<div className="d-flex groups-btn-container">
<p className="tj-text" data-cy="page-title">
{groups?.length} Groups
</p>
{!showNewGroupForm && !showGroupNameUpdateForm && (
<ButtonSolid
className="btn btn-primary create-new-group-button"
onClick={(e) => {
e.preventDefault();
this.setState({ newGroupName: '', showNewGroupForm: true, isSaveBtnDisabled: true });
}}
data-cy="create-new-group-button"
leftIcon="plus"
isLoading={isLoading}
iconWidth="16"
fill={'#FDFDFE'}
>
{this.props.t(
'header.organization.menus.manageGroups.permissions.createNewGroup',
'Create new group'
)}
</ButtonSolid>
)}
</div>
<Modal
show={showNewGroupForm || showGroupNameUpdateForm}
closeModal={() =>
this.setState({
showNewGroupForm: false,
showGroupNameUpdateForm: false,
newGroupName: null,
})
}
title={
showGroupNameUpdateForm
? this.props.t('header.organization.menus.manageGroups.permissions.updateGroup', 'Update group')
: this.props.t('header.organization.menus.manageGroups.permissions.addNewGroup', 'Create new group')
}
>
<form
id="my-form"
onSubmit={(e) => {
e.preventDefault();
if (showNewGroupForm) {
this.createGroup();
} else {
this.executeGroupUpdation();
}
}}
>
<div className="form-group mb-3 ">
<div className="row">
<div className="col tj-app-input">
<input
type="text"
required
className={`form-control ${this.state.newGroupName?.length >= 50 ? 'custom-input-error' : ''}`}
placeholder={this.props.t(
'header.organization.menus.manageGroups.permissions.enterName',
'Enter group name'
)}
onChange={(e) => {
this.changeNewGroupName(e.target.value);
}}
value={this.state.newGroupName}
data-cy="group-name-input"
autoFocus
/>
<span className="tj-text-xxsm" style={grounNameErrorStyle} data-cy="group-name-info-text">
{this.state.groupNameMessage}
</span>
</div>
</div>
</div>
<div className="form-footer d-flex create-group-modal-footer">
<ButtonSolid
onClick={() =>
this.setState({
showNewGroupForm: false,
showGroupNameUpdateForm: false,
newGroupName: null,
})
}
disabled={creatingGroup}
data-cy="cancel-button"
variant="tertiary"
>
{this.props.t('globals.cancel', 'Cancel')}
</ButtonSolid>
<ButtonSolid
type="submit"
id="my-form"
disabled={creatingGroup || this.state.isSaveBtnDisabled}
data-cy="create-group-button"
isLoading={creatingGroup || isUpdatingGroupName}
leftIcon="plus"
fill={creatingGroup || this.state.isSaveBtnDisabled ? '#4C5155' : '#FDFDFE'}
>
{showGroupNameUpdateForm
? this.props.t('globals.save', 'Save')
: this.props.t('header.organization.menus.manageGroups.permissions.createGroup', 'Create Group')}
</ButtonSolid>
</div>
</form>
</Modal>
<div className="org-users-page-card-wrap">
<div className="org-users-page-sidebar">
<div className="default-group-list-container">
<div className="mb-2 d-flex align-items-center">
<SolidIcon name="usergear" />
<span className="ml-1 group-title" data-cy="user-role-title">
USER ROLE
</span>
</div>
{defaultGroups.map((permissionGroup) => {
return (
<FolderList
key={permissionGroup.id}
listId={permissionGroup.id}
overlayFunctionParam={{
id: permissionGroup.id,
groupName: permissionGroup.name,
}}
selectedItem={this.state.selectedGroup == this.humanizeifDefaultGroupName(permissionGroup.name)}
onClick={() => {
this.setState({
selectedGroupPermissionId: permissionGroup.id,
selectedGroup: this.humanizeifDefaultGroupName(permissionGroup.name),
selectedGroupObject: permissionGroup,
});
}}
toolTipText={this.humanizeifDefaultGroupName(permissionGroup.name)}
overLayComponent={this.renderPopoverContent}
className="groups-folder-list"
dataCy={this.humanizeifDefaultGroupName(permissionGroup.name)
.toLowerCase()
.replace(/\s+/g, '-')}
>
<span>
<OverflowTooltip>{this.humanizeifDefaultGroupName(permissionGroup.name)}</OverflowTooltip>
</span>
</FolderList>
);
})}
</div>
<div>
{!showGroupSearchBar ? (
<div className="mb-2 d-flex align-items-center">
<SolidIcon name="usergroup" width="18px" fill="#889096" />
<span className="ml-1 group-title" data-cy="custom-groups-title">
CUSTOM GROUPS
</span>
<div className="create-group-cont">
<ButtonSolid
onClick={(e) => {
e.preventDefault();
this.setState({ showGroupSearchBar: true });
}}
size="xsm"
rightIcon="search"
iconWidth="15"
fill="#889096"
className="create-group-custom"
data-cy="custom-group-search"
/>
<ButtonSolid
onClick={(e) => {
e.preventDefault();
this.setState({ newGroupName: null, showNewGroupForm: true, isSaveBtnDisabled: true });
}}
size="sm"
fill="#889096"
rightIcon="plus"
iconWidth="20"
className="create-group-custom"
data-cy="create-custom-group-button"
/>
</div>
</div>
) : (
<div className="searchbox-custom">
<SearchBox
dataCy={`custom-group`}
width="70px !important"
callBack={this.handleGroupSearch}
placeholder={'Search'}
customClass="tj-common-search-input-group"
onClearCallback={this.handleGroupSearchClose}
autoFocus={true}
/>
</div>
)}
{groups.length ? (
filteredGroup.map((permissionGroup) => {
return (
<FolderList
key={permissionGroup.id}
listId={permissionGroup.id}
overlayFunctionParam={{
id: permissionGroup.id,
groupName: permissionGroup.name,
}}
selectedItem={
this.state.selectedGroup == this.humanizeifDefaultGroupName(permissionGroup.name)
}
onClick={() => {
this.setState({
selectedGroupPermissionId: permissionGroup.id,
selectedGroup: this.humanizeifDefaultGroupName(permissionGroup.name),
selectedGroupObject: permissionGroup,
});
}}
toolTipText={this.humanizeifDefaultGroupName(permissionGroup.name)}
overLayComponent={this.renderPopoverContent}
className="groups-folder-list"
dataCy={this.humanizeifDefaultGroupName(permissionGroup.name)
.toLowerCase()
.replace(/\s+/g, '-')}
>
<span>
<OverflowTooltip>{this.humanizeifDefaultGroupName(permissionGroup.name)}</OverflowTooltip>
</span>
</FolderList>
);
})
) : (
<div className="empty-custom-group-info">
<SolidIcon className="info-icon" name="information" width="18px" />
<span className="tj-text-xsm text-center info-label" data-cy="empty-custom-group-info">
No custom groups added
</span>
</div>
)}
</div>
</div>
<div className="org-users-page-card-body">
{isLoading ? (
<Loader />
) : (
<ManageGroupPermissionResourcesV2
groupPermissionId={this.state.selectedGroupPermissionId}
darkMode={this.props.darkMode}
selectedGroup={this.state.selectedGroup}
selectedGroupObject={this.state.selectedGroupObject}
updateGroupName={this.updateGroupName}
deleteGroup={this.deleteGroup}
roleOptions={defaultGroups.map((group) => {
return {
name: this.humanizeifDefaultGroupName(group.name),
value: group.name,
};
})}
/>
)}
</div>
</div>
</div>
</div>
</ErrorBoundary>
);
}
}
export const ManageGroupPermissionsV2 = withTranslation()(ManageGroupPermissionsComponent);
const Field = ({
text,
onClick,
customClass,
leftIcon,
leftIconWidth,
leftIconHeight = '18',
leftIconClassName,
buttonDisable = false,
tooltipContent = '',
tooltipId = '',
darkMode = false,
}) => {
return (
<div className={`field ${customClass ? ` ${customClass}` : ''}`}>
<span
className="row option-row"
role="button"
onClick={!buttonDisable && onClick}
data-cy={`${text.toLowerCase().replace(/\s+/g, '-')}-card-option`}
data-tooltip-content={tooltipContent}
data-tooltip-id={tooltipId}
>
<div className={`col-2 ${leftIconClassName}`}>
{leftIcon && (
<SolidIcon
name={leftIcon}
width={leftIconWidth}
height={leftIconHeight}
{...(buttonDisable ? { fill: '#D7DBDF' } : {})}
></SolidIcon>
)}
</div>
<div className={`col ${buttonDisable ? 'disable' : ''} ${darkMode ? 'dark-theme' : ''}`}>{text}</div>
</span>
</div>
);
};

View file

@ -0,0 +1,105 @@
import React, { useEffect, useState, useRef } from 'react';
import cx from 'classnames'; // Assuming you're using the classnames package
import { humanizeifDefaultGroupName } from '@/_helpers/utils';
import './groupPermissions.theme.scss';
const GroupChipTD = ({ groups = [] }) => {
const [showAllGroups, setShowAllGroups] = useState(false);
const groupsListRef = useRef();
useEffect(() => {
const onCloseHandler = (e) => {
if (groupsListRef.current && !groupsListRef.current.contains(e.target)) {
setShowAllGroups(false);
}
};
window.addEventListener('click', onCloseHandler);
return () => {
window.removeEventListener('click', onCloseHandler);
};
}, [showAllGroups]);
function moveValuesToLast(arr, valuesToMove) {
const validValuesToMove = valuesToMove.filter((value) => arr.includes(value));
validValuesToMove.forEach((value) => {
const index = arr.indexOf(value);
if (index !== -1) {
const removedItem = arr.splice(index, 1);
arr.push(removedItem[0]);
}
});
return arr;
}
const orderedArray = groups;
const toggleAllGroupsList = (e) => {
setShowAllGroups(!showAllGroups);
};
const renderGroupChip = (group, index) => (
<span className="group-chip" key={index} data-cy="group-chip">
{humanizeifDefaultGroupName(group)}
</span>
);
return (
<div
data-active={showAllGroups}
onClick={(e) => {
orderedArray.length > 2 && toggleAllGroupsList(e);
}}
className={cx('text-muted resource-name-cell', { 'groups-hover': orderedArray.length > 2 })}
>
<div className="groups-name-container tj-text-sm font-weight-500">
{orderedArray.length === 0 ? (
<div className="groups-name-row">
<div className="empty-text">-</div>
</div>
) : (
<>
<div className="groups-name-row">
{orderedArray.slice(0, 2).map((group, index) => {
return renderGroupChip(group, index);
})}
</div>
<div className="groups-name-row">
{orderedArray.slice(2, 4).map((group, index) => {
return renderGroupChip(group, index);
})}
</div>
{orderedArray.length > 4 && (
<React.Fragment key={4}>
<div className="groups-name-row" ref={groupsListRef}>
<span className="group-chip">+{orderedArray.length - 4} more</span>
</div>
{showAllGroups && (
<div className="all-groups-list">
{orderedArray.slice(4).map((group, index) => renderGroupChip(group, index))}
</div>
)}
</React.Fragment>
)}
{/* orderedArray.slice(0, 2).map((group, index) => {
if (orderedArray.length <= 2) {
return renderGroupChip(group, index);
}
if (orderedArray.length > 2 && index === 1) {
}
return renderGroupChip(group, index);
}) */}
</>
)}
</div>
</div>
);
};
export default GroupChipTD;

View file

@ -0,0 +1,455 @@
@import "../_styles/colors.scss";
.default-group-list-container {
margin-bottom: 20px;
}
.empty-custom-group-info{
margin-top: 15px;
border-radius: 6px;
border: 1px dashed var(--slate8) !important;
width: 100%;
height: 32px;
padding: 3px 12px;
display: flex;
flex-direction: row;
.info-icon{
margin-top: 3px;
}
.info-label{
margin-left: 4px;
color: var(--slate9);
}
}
.group-title {
font-weight: 500;
color: var(--slate11);
font-size: 12px;
margin-left: 5px;
}
.create-group-cont {
margin-left:10px ;
margin-right: auto;
display: flex;
flex-direction: row;
.create-group-custom {
width: 20px;
margin-left: 2px;
margin-right: 2px;
height: 20px;
padding: 0 0;
background: none !important;
background-color: none !important;
box-shadow: none;
}
}
.searchbox-custom{
margin-bottom: 10px;
.tj-common-search-input-group {
.input-icon-addon {
padding-right: 8px;
padding-left: 8px;
}
input {
box-sizing: border-box;
display: flex;
flex-direction: row;
align-items: center;
padding: 4px 8px !important;
gap: 16px;
width: 190px !important;
height: 28px !important;
background: var(--base);
border: 1px solid var(--slate7);
border-radius: 6px;
color: var(--slate12);
padding-left: 33px !important;
::placeholder {
color: var(--slate9);
margin-left: 5px !important;
padding-left: 5px !important;
background-color: red !important;
}
&:hover {
background: var(--slate2);
border: 1px solid var(--slate8);
}
&:active {
background: var(--indigo2);
border: 2px solid var(--indigo11);
box-shadow: 0px 0px 0px 2px #C6D4F9;
outline: none;
}
&:focus-visible {
background: var(--slate2);
border: 1px solid var(--slate8);
border-radius: 6px;
outline: none;
padding-left: 12px !important;
}
&:disabled {
background: var(--slate3);
border: 1px solid var(--slate7);
}
}
}
}
.edit-role-modal {
font-family: 'IBM Plex Sans';
.modal-dialog {
width: 320px;
}
.modal-content {
background: linear-gradient(0deg, #FFFFFF, #FFFFFF),
linear-gradient(0deg, #DFE3E6, #DFE3E6);
}
.modal-header {
justify-content: center !important;
flex-direction: column;
padding: 30px 32px 20px 32px;
border: none;
.remove-icon-container{
display: flex;
justify-content: flex-end;
margin-right: 10px;
.close-btn {
width: 20px;
margin: 5px 5px 5px 5px;
height: 20px;
padding: 0 0;
background: none !important;
background-color: none !important;
box-shadow: none;
}
}
.icon-class {
display: flex; /* Enable flexbox */
justify-content: center; /* Center items horizontally */
align-items: center; /* Center items vertically */
justify-content: center;
width: 64px;
height: 64px;
background-color: var(--tomato3);
border-radius: 6px;
}
.header-text {
font-style: normal;
font-weight: 600;
font-size: 16px;
text-align: center;
// line-height: 36px;
margin: 12px 0 5px 0;
}
p {
font-style: normal;
font-weight: 400;
font-size: 14px;
color: #687076;
text-align: Center;
margin-bottom: 0px;
}
}
.modal-body {
border: none;
padding: 12px 12px 22px 27px;
.item-list {
display: flex;
gap: 5px;
flex-direction: column;
max-height: 100px; /* Set a fixed height or max-height */
overflow-y: scroll; /* Enable vertical scrolling */
}
}
}
.edit-role-modal.dark-mode {
.modal-footer,
.modal-header {
border-color: #232e3c !important;
p {
color: var(--base) !important;
}
}
.modal-body,
.modal-footer,
.modal-header,
.modal-content {
color: white;
background-color: #2b394a;
}
.modal-content {
border: none;
}
}
.resource-name-cell {
transition: 0.3s all;
border-radius: 6px;
max-width: 170px;
position: relative !important;
overflow: visible !important;
.groups-name-container {
display: flex;
flex-direction: column;
row-gap: 8px;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
max-height: 200px;
max-width: 170px;
.empty-text{
display: flex;
justify-content: left;
margin-left: 20px;
}
.groups-name-row {
display: flex;
column-gap: 8px;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
max-width: 170px;
}
}
.group-chip {
padding: 2px 8px;
margin: 0;
border-radius: 6px;
background-color: var(--slate3);
color: var(--slate11);
min-height: 24px;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
max-width: 100px;
font-size: 12px;
}
.all-groups-list {
position: absolute;
width: 100%;
top: 59px;
display: flex;
flex-direction: column;
background: var(--slate1);
align-items: flex-start;
border-radius: 6px;
border: 1px solid var(--slate1);
box-shadow: 0px 4px 6px -2px rgba(16, 24, 40, 0.03), 0px 12px 16px -4px rgba(16, 24, 40, 0.08);
padding: 9px 10px;
gap: 10px;
cursor: default;
max-height: 240px;
overflow: auto;
left: 0px;
z-index: 1;
.group-chip {
padding: 2px 8px;
margin: 0;
border-radius: 6px;
background-color: var(--slate3);
color: var(--slate11);
min-height: 24px;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
max-width: 200px;
}
}
}
.groups-name-cell[data-active="true"] {
display: flex;
background: var(--gray5) !important;
justify-content: center;
.groups-name-container {
padding-left: 6px;
justify-content: center;
}
.group-chip {
max-width: unset !important;
}
}
.role-name-cell {
transition: 0.3s all;
border-radius: 6px;
width: 120px !important;
position: relative !important;
overflow: visible !important;
.groups-name-container {
display: flex;
column-gap: 8px;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
max-width: 185px;
}
.group-chip {
padding: 2px 8px;
margin: 0;
border-radius: 6px;
background-color: var(--slate3);
// color: var(--slate11);
min-height: 24px;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
max-width: 95px;
}
.all-groups-list {
position: absolute;
width: 100%;
top: 41px;
display: flex;
flex-direction: column;
background: var(--slate1);
align-items: flex-start;
border-radius: 6px;
border: 1px solid var(--slate1);
box-shadow: 0px 4px 6px -2px rgba(16, 24, 40, 0.03), 0px 12px 16px -4px rgba(16, 24, 40, 0.08);
padding: 9px 10px;
gap: 10px;
cursor: default;
max-height: 240px;
overflow: auto;
left: 0px;
z-index: 1;
}
}
.role-name-cell[data-active="true"] {
display: flex;
background: var(--gray5) !important;
justify-content: center;
.groups-name-container {
padding-left: 6px;
justify-content: center;
}
.group-chip {
max-width: unset !important;
}
}
.edit-icon-container{
max-width: 10px;
display: flex;
justify-content: flex-end;
align-items: flex-start;
padding: 0;
height: 100%;
margin-bottom: auto;
}
.edit-permission-custom {
width: 20px;
margin-left: 2px;
margin-right: 2px;
height: 20px;
padding: 0 0;
background: none !important;
background-color: none !important;
box-shadow: none;
}
.manage-resource-permission{
.tj-text-xxsm{
color: var(--slate11)
}
transition: background-color 0.3s ease;
border-bottom: 1px solid var(--slate5);
display: flex;
align-items:flex-start;
padding: 12px;
gap: 10px;
.resource-name {
display: flex;
gap: 5px;
width: 195px;
padding-right: 10px;
.resource-icon {
}
.resource-text{
max-width: 160px;
padding-top: 1px;
overflow-x: hidden;
}
}
div {
width: 206px;
}
&:hover {
background-color: var(--slate3);
}
}
.manage-resource-body {
padding: 24px;
font-size: 12px;
overflow-y: auto;
height: calc(100vh - 300px);
}
.error-text{
color: red;
}

View file

@ -6,6 +6,9 @@ import { Tooltip } from 'react-tooltip';
import { FormWrapper, textAreaEnterOnSave } from '@/_components/FormWrapper';
import EyeHide from '../../assets/images/onboardingassets/Icons/EyeHide';
import EyeShow from '../../assets/images/onboardingassets/Icons/EyeShow';
import './ConstantFormStyle.scss';
import { Constants } from '@/_helpers/utils';
import CloseIcon from '@/_ui/Icon/bulkIcons/CloseIcon';
const ConstantForm = ({
selectedConstant,
@ -13,11 +16,12 @@ const ConstantForm = ({
onCancelBtnClicked,
isLoading,
currentEnvironment,
checkIfConstantNameExists,
mode,
}) => {
console.log(isLoading);
const [fields, setFields] = useState(() => ({
...selectedConstant,
type: selectedConstant?.type,
environments: [{ label: currentEnvironment?.name, value: currentEnvironment?.id }],
}));
@ -47,7 +51,7 @@ const ConstantForm = ({
name_already_exists: `Constant with this name already exists in ${capitalize(
currentEnvironment?.name
)} environment`,
invalid_name_length: 'Constant name should be between 1 and 32 characters',
invalid_name_length: 'Constant name has exceeded 50 characters',
max_name_length_reached: 'Maximum length has been reached',
invalid_name:
'Constant name should start with a letter or underscore and can only contain letters, numbers and underscores',
@ -60,16 +64,10 @@ const ConstantForm = ({
if (name !== 'name') return;
const isNameAlreadyExists = checkIfConstantNameExists(value, currentEnvironment?.id);
const invalidNameLength = value.length > 32;
const maxNameLengthReached = value.length === 32;
const invalidNameLength = value.length > 50;
const maxNameLengthReached = value.length === 50;
const invalidName = !isValidPropertyName(value);
if (isNameAlreadyExists) {
return setError({
name: ERROR_MESSAGES.name_already_exists,
});
}
if (invalidNameLength) {
return setError({
name: ERROR_MESSAGES.invalid_name_length,
@ -133,6 +131,7 @@ const ConstantForm = ({
!isActiveErrorState(error) &&
fields['name'] &&
fields['value'] &&
fields['type'] &&
(fields['name'].length > 0 || fields['value'].length > 0)
? false
: true;
@ -160,6 +159,9 @@ const ConstantForm = ({
<h3 className="card-title" data-cy="constant-form-title">
{!selectedConstant ? 'Add new constant' : 'Update constant'} in {currentEnvironment?.name}{' '}
</h3>
<div style={{ marginLeft: '200px' }} onClick={onCancelBtnClicked}>
<CloseIcon width="15" fill="#3E63DD" opacity="1" secondaryFill="#ffffff" />
</div>
</div>
<div className="card-body org-constant-form">
<FormWrapper callback={handlecreateOrUpdate} id="variable-form">
@ -167,9 +169,6 @@ const ConstantForm = ({
<div className="d-flex mb-3">
<div
className="col tj-app-input"
style={{
marginRight: '10px',
}}
onMouseEnter={() => setIsOpen(true)}
onMouseLeave={() => setIsOpen(false)}
>
@ -179,7 +178,7 @@ const ConstantForm = ({
<input
type="text"
className={`tj-input-element ${error['name'] ? 'tj-input-error-state' : ''}`}
placeholder={'Enter Constant Name'}
placeholder={'Enter constant name'}
name="name"
onChange={handleFieldChange}
value={fields['name']}
@ -193,8 +192,55 @@ const ConstantForm = ({
<span className="text-danger" data-cy="name-error-text">
{error['name']}
</span>
{!error['name'] && (
<small style={{ color: 'var(--text-placeholder)' }}>Name must be unique and max 50 characters</small>
)}
</div>
</div>
<div className="form-group">
<label className="form-label" data-cy="name-label">
Type
</label>
<div className="radio-group" data-tooltip-id="type-tooltip">
<div className="radio-item">
<label style={{ color: mode === 'edit' ? '#adb5bd' : 'inherit' }}>
<input
type="radio"
name="type"
value="Global"
checked={fields['type'] === Constants.Global}
onChange={handleFieldChange}
disabled={mode === 'edit'}
/>
Global constants
</label>
<small style={{ color: mode === 'edit' ? '#adb5bd' : 'inherit' }}>
The values can be used anywhere in the product
</small>
</div>
<div className="radio-item">
<label style={{ color: mode === 'edit' ? '#adb5bd' : 'inherit' }}>
<input
type="radio"
name="type"
value="Secret"
checked={fields['type'] === Constants.Secret}
onChange={handleFieldChange}
disabled={mode === 'edit'}
/>
Secrets
</label>
<small style={{ color: mode === 'edit' ? '#adb5bd' : 'inherit' }}>
The values are hidden and can only be used in data sources and queries
</small>
</div>
</div>
{mode === 'edit' && (
<Tooltip id="type-tooltip" place="top">
Cannot edit constant type
</Tooltip>
)}
</div>
<div className="col tj-app-input">
<div className="d-flex justify-content-between align-items-center w-100">
<label className="form-label" data-cy="value-label">
@ -294,7 +340,11 @@ const ConstantForm = ({
<ButtonSolid
type="submit"
isLoading={isLoading}
disabled={isLoading || shouldDisableButton || selectedConstant?.value === fields['value']}
disabled={
isLoading ||
shouldDisableButton ||
(selectedConstant?.value === fields['value'] && selectedConstant?.type === fields['type'])
}
data-cy="add-constant-button"
form="variable-form"
>

View file

@ -0,0 +1,320 @@
.form-group {
margin-bottom: 1rem;
font-size: 12px !important;
}
.form-label {
font-size: 12px !important;
}
.radio-group {
display: flex;
flex-wrap: wrap;
gap: 2rem;
}
.radio-item {
display: flex;
flex-direction: column;
align-items: left;
flex: 1;
max-width: 300px;
}
.radio-item label {
display: flex;
align-items: center;
gap: 0.5rem;
}
.radio-item input[type="radio"]:checked + label {
color: var(--indigo9);
}
.radio-item input[type="radio"]:checked {
accent-color: var(--indigo9);
}
.radio-item small {
margin-top: 0.25rem;
margin-left: 20px;
color: #666;
}
.constant-wrapper {
background-color: #f8f9fa;
padding: 0px;
}
.constant-page-wrapper {
background-color: #ffffff;
border: 1px solid #e9ece;
box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.1);
overflow: hidden;
width: 920px;
height: 620px;
padding: 20px;
border-radius: 4px;
}
.workspace-constant-header {
display: flex;
justify-content: space-between;
align-items: center;
padding-left: 20px;
padding-right: 20px;
border-bottom: 1px solid #e9ecef;
}
.dark-theme .workspace-constant-header {
border-bottom: 1px solid var(--border-weak, #2B3036);
}
.workspace-constant-header .tj-text-sm {
font-size: 14px;
font-weight: bold;
color: #495057;
}
.manage-sso-wrapper-card {
width: 100%;
outline: none
}
.manage-sso-container {
width: 100%;
}
.add-new-constant-button {
background-color: #3e63dd;
color: white;
border-radius: 4px;
padding: 0.5rem 1rem;
font-size: 14px;
font-weight: bold;
display: flex;
align-items: center;
}
.add-new-constant-button:hover {
background-color: #2b50ba;
}
.workspace-variable-container-wrap {
margin-top: 20px;
}
.align-items-center {
align-items: center;
}
.p-3 {
padding: 16px;
padding-left: 0px !important;
}
.workspace-setting-buttons-wrap {
display: flex;
align-items: center;
justify-content: flex-end;
}
.left-menu ul {
list-style: none;
padding-left: 0;
}
.left-menu li {
margin-bottom: 0.5rem;
}
.left-menu li.selected {
font-weight: bold;
}
.left-menu li:hover {
cursor: pointer;
color: #3e63dd;
}
.button {
display: flex;
align-items: center;
justify-content: center;
padding: 8px 16px;
font-size: 14px;
font-weight: 500;
color: white;
background-color: #3e63dd;
border: none;
border-radius: 4px;
cursor: pointer;
transition: background-color 0.3s ease;
}
.button:hover {
background-color: #2b50ba;
}
.button:disabled {
background-color: #adb5bd;
cursor: not-allowed;
}
.add-new-constant-button {
min-width: 200px;
height: 32px;
}
.tabs-and-search {
display: flex;
justify-content: space-between;
width: inherit;
height: inherit;
.tab {
padding-bottom: 0px;
background-color: transparent;
border: none;
padding-top: 10px;
cursor: pointer;
font-size: 14px;
font-weight: 500;
color: #687076;
border-bottom: 1px solid transparent;
.workspace-constant-text {
color: #687076;
}
}
}
.tabs {
display: flex;
gap: 2rem;
}
.tab.active {
border-radius: 0px;
color: #3e63dd;
padding-bottom: 8px;
border-bottom: 2px solid #3e63dd;
.workspace-constant-text {
padding-bottom: 8px;
color: #3e63dd;
border-bottom: 2px solid #3e63dd;
}
}
.tab.active {
border-radius: 0px;
color: #3e63dd;
.workspace-constant-text {
padding-bottom: 8px;
color: #3e63dd;
border-bottom: 2px solid #3e63dd;
}
}
.dark-theme{
.tab {
.workspace-constant-text {
color: var(--text-medium, #CFD3D8);
}
.tab-count{
margin-left: 4px;
font-size: 12px;
color: var(--text-medium, #CFD3D8);
}
.tab-count.active {
color: #3e63dd;
}
}
}
.tab-count {
margin-left: 4px;
font-size: 12px;
color: #687076;
}
.tab-count.active {
color: #3e63dd;
}
.search-bar {
display: flex;
justify-content: flex-end;
height: 45px !important;
padding-bottom: 15px;
width: 300px;
}
.search-input {
width: 100%;
font-size: 14px;
border: 1px solid #e0e0e0;
border-radius: 4px;
background-color: #ffffff;
color: #495057;
padding: 10px;
}
.search-input::placeholder {
color: #adb5bd;
}
/* Dark Theme Styles */
.dark-theme .constant-wrapper,
.theme-dark .constant-wrapper {
background-color: var(--slate2);
}
.dark-theme .constant-page-wrapper,
.theme-dark .constant-page-wrapper {
background-color: var(--base);
border: 1px solid #6c757d;
box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.3);
}
.dark-theme .workspace-constant-header .tj-text-sm,
.theme-dark .workspace-constant-header .tj-text-sm {
color: #cccccc;
}
.dark-theme .radio-item small,
.theme-dark .radio-item small {
color: #cccccc;
}
/* Additional dark theme styles for other components */
.dark-theme .search-input,
.theme-dark .search-input {
background-color: #2b394b;
color: #ffffff;
border: 1px solid #6c757d;
}
.dark-theme .search-input::placeholder,
.theme-dark .search-input::placeholder {
color: #ced4da;
}
/* Adjust the button styles for dark theme */
.dark-theme .button,
.theme-dark .button {
background-color: #2b394b;
color: #ffffff;
}
.dark-theme .button:hover,
.theme-dark .button:hover {
background-color: #2b394b;
}
.enabled-tag{
color: var(--grass9);
}
.disabled-tag{
color: var(--tomato9);
}
.dark-theme .tj-input-element {
&::placeholder {
color: #858C94;
}
}

View file

@ -21,6 +21,9 @@ const ConstantTable = ({
};
const darkMode = localStorage.getItem('darkMode') === 'true';
const displayValue = (constant) => {
if (typeof constant.value === 'undefined' || constant.value === null) {
return '';
}
return String(constant.value).length > (canUpdateDeleteConstant ? 30 : 50)
? String(constant.value).substring(0, canUpdateDeleteConstant ? 30 : 50) + '...'
: constant.value;
@ -32,10 +35,10 @@ const ConstantTable = ({
};
return (
<div className="container-xl">
<div>
<div className="card constant-table-card" style={{ border: 'none' }}>
<div
className="fixedHeader table-responsive constant-table-wrapper px-2"
className="fixedHeader table-responsive px-2"
ref={tableRef}
style={{ maxHeight: tableRef.current && calculateOffset() }}
>
@ -44,7 +47,24 @@ const ConstantTable = ({
<tr>
<th data-cy="workspace-variable-table-name-header">Name</th>
<th data-cy="workspace-variable-table-value-header">Value</th>
{canUpdateDeleteConstant && <th className="w-1"></th>}
{canUpdateDeleteConstant && (
<th className="w-1" style={{ paddingRight: '16px' }}>
{' '}
<small
className="text-green d-flex align-items-center justify-content-end"
data-cy="encrypted-label"
>
<img
className="encrypted-icon me-1"
src="assets/images/icons/padlock.svg"
alt="Encrypted"
width="12"
height="12"
/>
Encrypted
</small>
</th>
)}
</tr>
</thead>
{isLoading ? (

View file

@ -1,21 +1,23 @@
import React from 'react';
import { ButtonSolid } from '@/_ui/AppButton/AppButton';
const EmptyState = ({ canCreateVariable, setIsManageVarDrawerOpen, isLoading }) => {
const EmptyState = ({ canCreateVariable, setIsManageVarDrawerOpen, isLoading, searchTerm = '' }) => {
if (isLoading) return null;
return (
<div className="w-100 workspace-constant-card-body">
<div className="w-100 constant-card-body">
<div className="align-items-center p-3 justify-content-between">
<div className="empty-state-org-constants">
<center className={`empty-result`}>
<img src="assets/images/icons/org-constants.svg" width="64" height="64" data-cy="empty-state-image" />
<div className="w-50 mt-2">
<h3 data-cy="empty-state-header">No Workspace constants yet</h3>
<h3 data-cy="empty-state-header">
{searchTerm === '' ? 'No Workspace constants yet' : 'No workspace constants found'}
</h3>
<p className="info mt-2" data-cy="empty-state-text">
Use workspace constants seamlessly in both the app builder and data source connections across ToolJet.
</p>
{canCreateVariable && (
{canCreateVariable && searchTerm === '' && (
<ButtonSolid
data-cy="add-new-constant-button"
vaiant="primary"

View file

@ -1,3 +1,4 @@
//Do not merge changes from ee
import React, { useContext, useEffect, useState } from 'react';
import { authenticationService, orgEnvironmentConstantService, appEnvironmentService } from '@/_services';
import { ConfirmDialog } from '@/_components';
@ -13,6 +14,8 @@ import ConstantForm from './ConstantForm';
import EmptyState from './EmptyState';
import FolderList from '@/_ui/FolderList/FolderList';
import { BreadCrumbContext } from '@/App';
import './ConstantFormStyle.scss';
import { Constants } from '@/_helpers/utils';
const MODES = Object.freeze({
CREATE: 'create',
@ -30,7 +33,7 @@ const ManageOrgConstantsComponent = ({ darkMode }) => {
const [mode, setMode] = useState(MODES.NULL);
const perPage = 7;
const perPage = 6;
const [currentPage, setCurrentPage] = useState(1);
const [totalPages, setTotalPages] = useState(1);
const [currentTableData, setTableData] = useState([]);
@ -40,6 +43,25 @@ const ManageOrgConstantsComponent = ({ darkMode }) => {
const [selectedConstant, setSelectedConstant] = useState(null);
const { updateSidebarNAV } = useContext(BreadCrumbContext);
const [activeTab, setActiveTab] = useState(Constants.Global);
const [searchTerm, setSearchTerm] = useState('');
const [globalCount, setGlobalCount] = useState(0);
const [secretCount, setSecretCount] = useState(0);
const handleTabChange = (tab) => {
setCurrentPage(1);
updateTableData(constants, activeTabEnvironment?.name, 0, perPage, true, tab, searchTerm);
setActiveTab(tab);
};
const handleSearchChange = (e) => {
const searchTerm = e?.target?.value.toLowerCase();
setSearchTerm(searchTerm);
// Re-filter the constants based on the current search term and active tab
updateTableData(constants, activeTabEnvironment?.name, 0, perPage, true, activeTab, searchTerm);
};
const onCancelBtnClicked = () => {
setSelectedConstant(null);
setIsManageVarDrawerOpen(false);
@ -69,52 +91,107 @@ const ManageOrgConstantsComponent = ({ darkMode }) => {
}
setCurrentPage(1);
const envName = activeTabEnvironment ? activeTabEnvironment.name : environment.name;
updateTableData(allConstants, envName, 0, perPage, true);
const envName = activeTabEnvironment ? activeTabEnvironment?.name : environment?.name;
updateTableData(allConstants, envName, 0, perPage, true, activeTab, searchTerm);
// Calculate counts for Global and Secret constants
const globalCount =
allConstants.length > 0
? allConstants.filter(
(constant) =>
constant.type === Constants.Global &&
constant.values.find((env) => env.environmentName === envName)?.value !== ''
).length
: 0;
const secretCount =
allConstants.length > 0
? allConstants.filter(
(constant) =>
constant.type === Constants.Secret &&
constant.values.find((env) => env.environmentName === envName)?.value !== ''
).length
: 0;
setGlobalCount(globalCount);
setSecretCount(secretCount);
};
const updateTableData = (orgContants, envName, start, end, activeTabChanged = false) => {
const constantsForEnvironment = orgContants
const updateTableData = (orgConstants, envName, start, end, activeTabChanged = false, tab = null, search = '') => {
if (!Array.isArray(orgConstants)) {
return;
}
const filteredConstants = orgConstants
.filter((constant) => {
const envConstant = constant?.values.find((value) => value.environmentName === envName);
// Filter based on the active tab: 'Global' or 'Secret'
if (tab === Constants.Global) {
return envConstant && envConstant.value !== '' && constant.type === Constants.Global;
} else if (tab === Constants.Secret) {
return envConstant && envConstant.value !== '' && constant.type === Constants.Secret;
}
return envConstant && envConstant.value !== '';
})
.map((constant) => {
return {
id: constant.id,
name: constant.name,
value: findValueForEnvironment(constant.values, envName),
};
});
.filter((constant) => {
// Filter based on the search term
return constant.name.toLowerCase().includes(search.toLowerCase());
})
.map((constant) => ({
id: constant.id,
name: constant.name,
type: constant.type,
value: findValueForEnvironment(constant.values, envName),
}));
let globalTabCount = 0;
let secretTabCount = 0;
globalTabCount = orgConstants.filter(
(constant) =>
constant.type === Constants.Global &&
constant.name.toLowerCase().includes(search.toLowerCase()) &&
constant?.values.find((value) => value.environmentName === envName && value.value !== '')
).length;
secretTabCount = orgConstants.filter(
(constant) =>
constant.type === Constants.Secret &&
constant.name.toLowerCase().includes(search.toLowerCase()) &&
constant?.values.find((value) => value.environmentName === envName && value.value !== '')
).length;
setGlobalCount(globalTabCount);
setSecretCount(secretTabCount);
if (activeTabChanged) {
computeTotalPages(constantsForEnvironment.length);
computeTotalPages(filteredConstants.length || 1);
}
const paginatedConstants = filteredConstants ? filteredConstants.slice(start, end) : filteredConstants;
setTableData(paginatedConstants);
if (tab === Constants.Global) {
setGlobalCount(filteredConstants.length);
} else {
setSecretCount(filteredConstants.length);
}
const envConstantants = constantsForEnvironment.slice(start, end);
setTableData(envConstantants);
};
const goToNextPage = () => {
setCurrentPage(currentPage + 1);
const start = (currentPage + 1 - 1) * perPage;
const start = currentPage * perPage;
const end = start + perPage;
const envName = activeTabEnvironment.name;
updateTableData(constants, envName, start, end);
const envName = activeTabEnvironment?.name;
updateTableData(constants, envName, start, end, false, activeTab, searchTerm);
};
const goToPreviousPage = () => {
setCurrentPage(currentPage - 1);
const start = (currentPage - 1 - 1) * perPage;
const start = (currentPage - 2) * perPage;
const end = start + perPage;
const envName = activeTabEnvironment.name;
updateTableData(constants, envName, start, end);
const envName = activeTabEnvironment?.name;
updateTableData(constants, envName, start, end, false, activeTab, searchTerm);
};
const canAnyGroupPerformAction = (action, permissions) => {
@ -126,24 +203,15 @@ const ManageOrgConstantsComponent = ({ darkMode }) => {
};
const canCreateVariable = () => {
return canAnyGroupPerformAction(
'org_environment_variable_create',
authenticationService.currentSessionValue.group_permissions
);
return authenticationService.currentSessionValue.user_permissions.org_constant_c_r_u_d;
};
const canUpdateVariable = () => {
return canAnyGroupPerformAction(
'org_environment_variable_update',
authenticationService.currentSessionValue.group_permissions
);
return authenticationService.currentSessionValue.user_permissions.org_constant_c_r_u_d;
};
const canDeleteVariable = () => {
return canAnyGroupPerformAction(
'org_environment_variable_delete',
authenticationService.currentSessionValue.group_permissions
);
return authenticationService.currentSessionValue.user_permissions.org_constant_c_r_u_d;
};
const fetchEnvironments = () => {
@ -172,7 +240,7 @@ const ManageOrgConstantsComponent = ({ darkMode }) => {
};
const fetchConstantsAndEnvironments = async () => {
const orgConstants = await orgEnvironmentConstantService.getAll(true);
const orgConstants = await orgEnvironmentConstantService.getAll();
if (orgConstants?.constants?.length > 1) {
orgConstants.constants.sort((a, b) => {
@ -189,29 +257,61 @@ const ManageOrgConstantsComponent = ({ darkMode }) => {
setIsLoading(false);
setSelectedConstant(null);
const start = (currentPage - 1 - 1) * perPage;
const end = start + perPage;
const envName = activeTabEnvironment ? activeTabEnvironment?.name : currentEnvironment?.name;
updateTableData(orgConstants, envName, start, end, activeTab, searchTerm);
};
const checkIfConstantNameExists = (name, environementId) => {
const checkIfConstantNameExists = (name, type, environementId) => {
if (!environementId) {
return constants.some((constant) => constant.name === name);
return constants.some((constant) => constant.name === name && constant.type === type);
}
const existingConstants = constants.filter((constant) => {
return (
constant.type === type && constant.values.some((value) => value.id === environementId && value.value !== '')
);
});
return existingConstants.some((constant) => constant.name === name);
};
const checkIfConstantNameExistsInDiffEnv = (name, type, environementId) => {
if (!environementId) {
return constants.some((constant) => constant.name === name && constant.type === type);
}
const envConstants = constants.filter((constant) => {
return constant.values.some((value) => value.id === environementId && value.value !== '');
return (
constant.type === type && constant.values.some((value) => value.id === environementId && value.value === '')
);
});
return envConstants.some((constant) => constant.name === name);
};
const createOrUpdate = (variable, shouldUpdate = false) => {
const currentEnv = activeTabEnvironment;
const constantExistsInDiffEnv = checkIfConstantNameExists(variable.name);
const constantExists = checkIfConstantNameExists(
variable?.name,
variable?.type,
variable?.environments?.[0]?.value
);
const constantExistsInDiffEnv = checkIfConstantNameExistsInDiffEnv(
variable?.name,
variable?.type,
variable?.environments?.[0]?.value
);
if (constantExists && !shouldUpdate) {
toast.error(`${variable.type} constant already exists!`);
return;
}
const shouldUpdateConstant = mode === 'edit' && shouldUpdate ? true : constantExistsInDiffEnv;
if (shouldUpdateConstant) {
const variableId = constantExistsInDiffEnv
? constants.find((constant) => constant.name === variable.name).id
? constants.find((constant) => constant.name === variable.name && constant.type === variable.type).id
: variable.id;
return orgEnvironmentConstantService
@ -228,9 +328,9 @@ const ManageOrgConstantsComponent = ({ darkMode }) => {
}
return orgEnvironmentConstantService
.create(variable.name, variable.value, [currentEnv['id']])
.create(variable.name, variable.value, variable.type, [currentEnv['id']])
.then(() => {
toast.success('Constant has been created');
toast.success(`${variable.type} constant created successfully!`);
onCancelBtnClicked();
})
.catch(({ error }) => {
@ -288,7 +388,7 @@ const ManageOrgConstantsComponent = ({ darkMode }) => {
</span>
);
return (
<div className="wrapper org-constant-page org-variables-page animation-fade">
<div className="constant-wrapper org-constant-page org-variables-page animation-fade">
<ConfirmDialog
show={showConstantDeleteConfirmation}
message={confirmMessage}
@ -311,14 +411,71 @@ const ManageOrgConstantsComponent = ({ darkMode }) => {
/>
</Drawer>
)}
<div className="page-wrapper">
<div className="align-items-center d-flex justify-content-between" style={{ marginBottom: '10px' }}>
<div className="tj-text-sm font-weight-500" data-cy="env-name">
{capitalize(activeTabEnvironment?.name)} ({globalCount + secretCount})
</div>
<div className="workspace-setting-buttons-wrap">
{canCreateVariable() && (
<ButtonSolid
data-cy="add-new-constant-button"
variant="primary"
onClick={() => {
setMode(() => MODES.CREATE);
setIsManageVarDrawerOpen(() => true);
}}
className="add-new-constant-button"
customStyles={{ minWidth: '200px', height: '32px' }}
disabled={isManageVarDrawerOpen}
>
+ Create new constant
</ButtonSolid>
)}
</div>
</div>
<div className="constant-page-wrapper">
<div className="container-xl">
<div>
<div className="page-header workspace-constant-header">
<div className="tj-text-sm font-weight-500" data-cy="constants-count-title">
{constants.length} constants
<div className="workspace-constant-header">
<div className="tabs-and-search">
<div className="tabs">
<button
className={`tab ${activeTab === Constants.Global ? 'active' : ''}`}
onClick={() => handleTabChange(Constants.Global)}
>
Global constants
<span className={`tab-count ${activeTab === Constants.Global ? 'active' : ''}`}>
({globalCount})
</span>
</button>
<button
className={`tab ${activeTab === Constants.Secret ? 'active' : ''}`}
onClick={() => handleTabChange(Constants.Secret)}
>
Secrets
<span className={`tab-count ${activeTab === Constants.Secret ? 'active' : ''}`}>
({secretCount})
</span>
</button>
</div>
<div className="search-bar">
<input
type="text"
placeholder={activeTab === Constants.Global ? 'Search global constants' : 'Search secrets'}
value={searchTerm}
onChange={handleSearchChange}
className="search-input"
/>
</div>
</div>
</div>
</div>
</div>
<div className="workspace-variable-container-wrap mt-2">
<div className="container-xl" style={{ width: '880px', padding: '0px' }}>
<div className="workspace-constant-table-card">
<div className="mt-3">
<Alert svg="tj-info">
<div
@ -330,9 +487,19 @@ const ManageOrgConstantsComponent = ({ darkMode }) => {
}}
>
<div className="text-muted" data-cy="workspace-constant-helper-text">
To resolve a Workspace constant use{' '}
<strong style={{ fontWeight: 500, color: '#3E63DD' }}>{'{{constants.access_token}}'}</strong>
{activeTab === Constants.Global ? (
<>
To resolve a global workspace constant use{' '}
<strong style={{ fontWeight: 500, color: '#3E63DD' }}>{'{{constants.access_token}}'}</strong>
</>
) : (
<>
To resolve a secret workspace constant use{' '}
<strong style={{ fontWeight: 500, color: '#3E63DD' }}>{'{{secrets.access_token}}'}</strong>
</>
)}
</div>
<div>
<Button
// Todo: Update link to documentation: workspace constants
@ -350,21 +517,14 @@ const ManageOrgConstantsComponent = ({ darkMode }) => {
fontWeight: 500,
}}
>
<Button.Content title={'Read Documentation'} iconSrc="assets/images/icons/student.svg" />
<Button.Content title={'Read documentation'} iconSrc="assets/images/icons/student.svg" />
</Button>
</div>
</div>
</Alert>
</div>
</div>
</div>
</div>
<div className="workspace-variable-container-wrap mt-2">
<div className="container-xl">
<div className="workspace-constant-table-card">
<div className="manage-sso-container h-100">
<div className="d-flex manage-sso-wrapper-card h-100">
<div className="d-flex manage-constant-wrapper-card">
<ManageOrgConstantsComponent.EnvironmentsTabs
allEnvironments={environments}
currentEnvironment={activeTabEnvironment}
@ -372,30 +532,9 @@ const ManageOrgConstantsComponent = ({ darkMode }) => {
isLoading={isLoading}
allConstants={constants}
/>
{constants.length > 0 ? (
<div className="w-100 workspace-constant-card-body">
<div className="align-items-center d-flex p-3 justify-content-between">
<div className="tj-text-sm font-weight-500" data-cy="env-name">
{capitalize(activeTabEnvironment?.name)}
</div>
<div className="workspace-setting-buttons-wrap">
{canCreateVariable() && (
<ButtonSolid
data-cy="add-new-constant-button"
vaiant="primary"
onClick={() => {
setMode(MODES.CREATE);
setIsManageVarDrawerOpen(true);
}}
className="add-new-constant-button"
customStyles={{ minWidth: '200px', height: '32px' }}
disabled={isManageVarDrawerOpen}
>
Create new constant
</ButtonSolid>
)}
</div>
</div>
{(activeTab === Constants.Global && globalCount > 0) ||
(activeTab === Constants.Secret && secretCount > 0) ? (
<div className="w-100">
<ConstantTable
constants={currentTableData}
onEditBtnClicked={onEditBtnClicked}
@ -418,6 +557,7 @@ const ManageOrgConstantsComponent = ({ darkMode }) => {
canCreateVariable={canCreateVariable()}
setIsManageVarDrawerOpen={setIsManageVarDrawerOpen}
isLoading={isLoading}
searchTerm={searchTerm}
/>
)}
</div>
@ -510,5 +650,4 @@ const Footer = ({ darkMode, totalPage, pageCount, dataLoading, gotoNextPage, got
ManageOrgConstantsComponent.EnvironmentsTabs = RenderEnvironmentsTab;
ManageOrgConstantsComponent.Footer = Footer;
export default ManageOrgConstantsComponent;

View file

@ -3,7 +3,15 @@ import { useDropzone } from 'react-dropzone';
import BulkIcon from '@/_ui/Icon/BulkIcons';
import { toast } from 'react-hot-toast';
export function FileDropzone({ handleClick, hiddenFileInput, errors, handleFileChange, inviteBulkUsers, onDrop }) {
export function FileDropzone({
handleClick,
hiddenFileInput,
errors,
handleFileChange,
inviteBulkUsers,
onDrop,
setFileUpload,
}) {
const [fileData, setFileData] = useState();
const { getRootProps, getInputProps, isDragActive, acceptedFiles } = useDropzone({
accept: { parsedFileType: ['text/csv'] },
@ -53,11 +61,15 @@ export function FileDropzone({ handleClick, hiddenFileInput, errors, handleFileC
onChange={(e) => {
const file = e.target.files[0];
setFileData(file);
if (file === undefined) {
setFileUpload(false);
}
if (Math.round(file.size / 1024) > 1024) {
toast.error('File size cannot exceed more than 1MB');
e.target.value = null;
} else {
handleFileChange(file);
setFileUpload(true);
}
}}
accept=".csv"

View file

@ -8,6 +8,8 @@ import { toast } from 'react-hot-toast';
import { FileDropzone } from './FileDropzone';
import { USER_DRAWER_MODES } from '@/_helpers/utils';
import { UserGroupsSelect } from './UserGroupsSelect';
import { EDIT_ROLE_MESSAGE } from '@/ManageGroupPermissionResourcesV2/constant';
import ModalBase from '@/_ui/Modal';
function InviteUsersForm({
onClose,
@ -24,29 +26,61 @@ function InviteUsersForm({
userDrawerMode,
setUserValues,
creatingUser,
darkMode,
}) {
const { t } = useTranslation();
const [activeTab, setActiveTab] = useState(1);
const [selectedGroups, setSelectedGroups] = useState([]);
const [existingGroups, setExistingGroups] = useState([]);
const [newRole, setNewRole] = useState(null);
const customGroups = groups.filter((group) => group.groupType === 'custom');
const roleGroups = groups
.filter((group) => group.groupType === 'default')
.sort((a, b) => {
const sortOrder = ['admin', 'builder', 'end-user'];
const indexA = sortOrder.indexOf(a.value);
const indexB = sortOrder.indexOf(b.value);
return indexA - indexB;
});
const [isChangeRoleModalOpen, setIsChangeRoleModalOpen] = useState(false);
const [fileUpload, setFileUpload] = useState(false);
const groupedOptions = [
{
label: 'default',
options: roleGroups,
},
{
label: 'custom',
options: customGroups,
},
];
const [selectedGroups, setSelectedGroups] = useState([]);
useEffect(() => {
setFileUpload(false);
}, [activeTab]);
const hiddenFileInput = useRef(null);
useEffect(() => {
if (currentEditingUser && groups.length) {
const { first_name, last_name, email, groups: addedToGroups } = currentEditingUser;
const { first_name, last_name, email, groups: addedToCustomGroups, role_group } = currentEditingUser;
const addedToGroups = [...addedToCustomGroups, ...role_group];
setUserValues({
fullName: `${first_name}${last_name && ` ${last_name}`}`,
email: email,
});
const preSelectedGroups = groups
.filter((group) => addedToGroups.includes(group.value))
.filter((group) => addedToGroups.map((group) => group.name).includes(group.value))
.map((filteredGroup) => ({
...filteredGroup,
label: filteredGroup.name,
}));
setExistingGroups(groups.filter((group) => addedToGroups.includes(group.value)).map((g) => g.value));
setExistingGroups(
groups.filter((group) => addedToCustomGroups.map((gp) => gp.name).includes(group.value)).map((g) => g.id)
);
onChangeHandler(preSelectedGroups);
} else {
onChangeHandler(roleGroups.filter((group) => group.value === 'end-user'));
}
}, [currentEditingUser, groups]);
@ -56,6 +90,7 @@ function InviteUsersForm({
toast.error('File size cannot exceed more than 1MB');
} else {
handleFileChange(file);
setFileUpload(true);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
@ -65,44 +100,91 @@ function InviteUsersForm({
};
const onChangeHandler = (items) => {
setSelectedGroups(items);
let finalGroup = items;
const roleGroups = items.filter((group) => group.groupType === 'default');
const currentRole = selectedGroups.find((group) => group.groupType === 'default');
if (roleGroups.length == 2) {
finalGroup = items.filter((group) => group.value !== currentRole.value);
}
if (roleGroups.length === 0) return;
if (currentEditingUser) {
const role = finalGroup.find(
(group) =>
group.groupType === 'default' && !currentEditingUser.role_group.map((role) => role.name).includes(group.value)
);
setNewRole(role);
}
setSelectedGroups(finalGroup);
};
const handleCreateUser = (e) => {
e.preventDefault();
const selectedGroupsIds = selectedGroups.map((group) => group.value);
manageUser(currentEditingUser?.id, selectedGroupsIds);
const role = selectedGroups.find((group) => group.groupType === 'default').value;
const selectedGroupsIds = selectedGroups.filter((group) => group.groupType !== 'default').map((group) => group.id);
manageUser(currentEditingUser?.id, selectedGroupsIds, role);
};
const handleEditUser = (e) => {
e.preventDefault();
const selectedGroupsIds = selectedGroups.map((group) => group.value);
const newGroupsToAdd = selectedGroupsIds.filter((selectedGroupId) => !existingGroups.includes(selectedGroupId));
const groupsToRemove = existingGroups.filter((existingGroup) => !selectedGroupsIds.includes(existingGroup));
manageUser(currentEditingUser.id, selectedGroupsIds, newGroupsToAdd, groupsToRemove);
if (newRole && EDIT_ROLE_MESSAGE?.[currentEditingUser?.role_group?.[0]?.name]?.[newRole?.value]?.())
setIsChangeRoleModalOpen(true);
else {
editUser();
}
};
const editUser = () => {
const { newGroupsToAdd, groupsToRemove, selectedGroupsIds } = getEditedGroups();
manageUser(currentEditingUser.id, selectedGroupsIds, newRole?.value, newGroupsToAdd, groupsToRemove);
setIsChangeRoleModalOpen(false);
};
const getEditedGroups = () => {
const selectedGroupsIds = selectedGroups.map((group) => group.value);
const selectedGroupsIds = selectedGroups.filter((group) => group.groupType !== 'default').map((group) => group.id);
const newGroupsToAdd = selectedGroupsIds.filter((selectedGroupId) => !existingGroups.includes(selectedGroupId));
const groupsToRemove = existingGroups.filter((existingGroup) => !selectedGroupsIds.includes(existingGroup));
return { newGroupsToAdd, groupsToRemove };
return { newGroupsToAdd, groupsToRemove, selectedGroupsIds };
};
const isEdited = () => {
const { newGroupsToAdd, groupsToRemove } = getEditedGroups();
const inValidUserDetail = !(fields?.['fullName'] && fields?.['email']);
const { first_name, last_name } = currentEditingUser || {};
return isEditing
? fields['fullName'] !== `${first_name}${last_name && ` ${last_name}`}` ||
groupsToRemove.length ||
newRole ||
newGroupsToAdd.length
: true;
: !inValidUserDetail || activeTab == 2;
};
const isEditing = userDrawerMode === USER_DRAWER_MODES.EDIT;
return (
<div>
{isChangeRoleModalOpen && (
<ModalBase
title={
<div className="my-3" data-cy="modal-title">
<span className="tj-text-md font-weight-500">Edit user role</span>
<div className="tj-text-sm text-muted" data-cy="user-email">
{currentEditingUser?.email}
</div>
</div>
}
handleConfirm={editUser}
show={isChangeRoleModalOpen}
darkMode={darkMode}
handleClose={() => {
setIsChangeRoleModalOpen(false);
onCancel();
onClose();
}}
confirmBtnProps={{ title: 'Continue', tooltipMessage: false }}
>
<div>{EDIT_ROLE_MESSAGE?.[currentEditingUser?.role_group?.[0]?.name]?.[newRole?.value]?.()}</div>
</ModalBase>
)}
<div className="animation-fade invite-user-drawer-wrap">
<div className="drawer-card-wrap invite-user-drawer-wrap">
<div className="card-header">
@ -216,7 +298,7 @@ function InviteUsersForm({
? 'User groups'
: t('header.organization.menus.manageUsers.selectGroup', 'Select Group')}
</label>
<UserGroupsSelect value={selectedGroups} onChange={onChangeHandler} options={groups} />
<UserGroupsSelect value={selectedGroups} onChange={onChangeHandler} options={groupedOptions} />
</div>
</form>
</div>
@ -234,7 +316,9 @@ function InviteUsersForm({
ToolJet wont be able to recognise files in any other format.{' '}
</p>
<ButtonSolid
href="../../assets/csv/sample_upload.csv"
href={`${window.public_config?.TOOLJET_HOST}${
window.public_config?.SUB_PATH ? window.public_config?.SUB_PATH : '/'
}assets/csv/sample_upload.csv`}
download="sample_upload.csv"
variant="tertiary"
className="download-template-btn"
@ -255,6 +339,7 @@ function InviteUsersForm({
handleFileChange={handleFileChange}
inviteBulkUsers={inviteBulkUsers}
onDrop={onDrop}
setFileUpload={setFileUpload}
/>
</div>
)}
@ -274,7 +359,7 @@ function InviteUsersForm({
form={activeTab == 1 ? 'inviteByEmail' : 'inviteBulkUsers'}
type="submit"
variant="primary"
disabled={uploadingUsers || creatingUser || !isEdited()}
disabled={uploadingUsers || creatingUser || !isEdited() || (activeTab !== 1 && !fileUpload)}
data-cy={activeTab == 1 ? 'button-invite-users' : 'button-upload-users'}
leftIcon={activeTab == 1 ? 'sent' : 'fileupload'}
width="20"

View file

@ -11,7 +11,9 @@ import { ButtonSolid } from '@/_ui/AppButton/AppButton';
import ManageOrgUsersDrawer from './ManageOrgUsersDrawer';
import { USER_DRAWER_MODES } from '@/_helpers/utils';
import { getQueryParams } from '@/_helpers/routes';
import EditRoleErrorModal from '@/ManageGroupPermissionsV2/ErrorModal/ErrorModal';
import HeaderSkeleton from '@/_ui/FolderSkeleton/HeaderSkeleton';
import SolidIcon from '@/_ui/Icon/SolidIcons';
class ManageOrgUsersComponent extends React.Component {
constructor(props) {
@ -28,6 +30,7 @@ class ManageOrgUsersComponent extends React.Component {
errors: {},
meta: {
total_count: 0,
currentPage: 1,
},
currentPage: 1,
options: {},
@ -37,6 +40,12 @@ class ManageOrgUsersComponent extends React.Component {
userDrawerMode: USER_DRAWER_MODES.CREATE,
newSelectedGroups: [],
existingGroupsToRemove: [],
showErrorModal: false,
errorModalMessage: '',
errorItemList: [],
errorTitle: '',
errorIconName: 'usergear',
resetSearch: false,
};
}
@ -74,7 +83,6 @@ class ManageOrgUsersComponent extends React.Component {
if (!this.state.file) {
errors['file'] = 'This field is required';
}
this.setState({ errors: errors });
return Object.keys(errors).length === 0;
}
@ -97,11 +105,22 @@ class ManageOrgUsersComponent extends React.Component {
changeNewUserOption = (name, e) => {
let fields = this.state.fields;
let errors = {};
fields[name] = e.target.value;
this.setState({
fields,
});
if (name === 'email') {
if (!this.validateEmail(fields['email'])) {
errors['email'] = 'Email is not valid';
this.setState({ errors });
} else {
errors['email'] = '';
this.setState({ errors });
}
}
};
archiveOrgUser = (id) => {
@ -111,7 +130,7 @@ class ManageOrgUsersComponent extends React.Component {
.archive(id)
.then(() => {
toast.success('The user has been archived');
this.setState({ archivingUser: null });
this.setState({ archivingUser: null, resetSearch: !this.state.resetSearch });
this.fetchUsers(this.state.currentPage, this.state.options);
})
.catch(({ error }) => {
@ -127,7 +146,7 @@ class ManageOrgUsersComponent extends React.Component {
.unarchive(id)
.then(() => {
toast.success('The user has been unarchived');
this.setState({ unarchivingUser: null });
this.setState({ unarchivingUser: null, resetSearch: !this.state.resetSearch });
this.fetchUsers(this.state.currentPage, this.state.options);
})
.catch(({ error }) => {
@ -136,6 +155,7 @@ class ManageOrgUsersComponent extends React.Component {
});
};
//Need to work on that
inviteBulkUsers = (event) => {
event.preventDefault();
if (this.handleFileValidation()) {
@ -159,7 +179,25 @@ class ManageOrgUsersComponent extends React.Component {
});
})
.catch(({ error }) => {
toast.error(error, { position: 'top-center' });
if (error?.error) {
this.setState({
showErrorModal: true,
errorModalMessage: error.error,
errorTitle: error?.title || 'Conflicting permissions',
errorItemList: error?.data,
errorIconName: 'usergear',
});
this.setState({ creatingUser: false, uploadingUsers: false });
return;
}
toast.error(error || 'Please check the format of CSV file', {
position: 'top-center',
style: {
minWidth: '200px',
whiteSpace: 'nowrap', // Prevent text from wrapping to the next line
wordBreak: 'keep-all', // Prevent word breaks
},
});
this.setState({ uploadingUsers: false });
});
}
@ -179,7 +217,7 @@ class ManageOrgUsersComponent extends React.Component {
});
};
manageUser = (currentOrgUserId, selectedGroups, groupsToAdd, groupsToRemove) => {
manageUser = (currentOrgUserId, selectedGroups, role, groupsToAdd, groupsToRemove) => {
const isEditing = this.state.userDrawerMode === USER_DRAWER_MODES.EDIT;
if (this.handleValidation()) {
if (!this.state.fields.fullName?.trim()) {
@ -202,11 +240,13 @@ class ManageOrgUsersComponent extends React.Component {
last_name: this.state.fields.lastName,
email: this.state.fields.email,
groups: selectedGroups,
role: role,
};
const updateUserBody = {
addGroups: groupsToAdd,
removeGroups: groupsToRemove,
...(role && { role: role }),
};
service(currentOrgUserId, isEditing ? updateUserBody : createUserBody)
.then(() => {
@ -218,11 +258,22 @@ class ManageOrgUsersComponent extends React.Component {
isInviteUsersDrawerOpen: false,
currentEditingUser: null,
userDrawerMode: USER_DRAWER_MODES.CREATE,
resetSearch: !this.state.resetSearch,
});
})
.catch(({ error }) => {
if (error?.error) {
this.setState({
showErrorModal: true,
errorModalMessage: error.error,
errorTitle: error?.title || 'Conflicting permissions',
errorItemList: error?.data,
errorIconName: 'usergear',
});
this.setState({ creatingUser: false });
return;
}
toast.error(error);
this.setState({ creatingUser: false });
});
} else {
this.setState({ creatingUser: false, file: null, isInviteUsersDrawerOpen: true });
@ -252,6 +303,16 @@ class ManageOrgUsersComponent extends React.Component {
toast.success('Invitation URL copied');
};
clearErrorState = () => {
this.setState({
showErrorModal: false,
errorModalMessage: '',
errorItemList: [],
errorTitle: '',
errorIconName: '',
});
};
pageChanged = (page) => {
this.fetchUsers(page, this.state.options);
};
@ -291,10 +352,25 @@ class ManageOrgUsersComponent extends React.Component {
meta,
currentEditingUser,
userDrawerMode,
showErrorModal,
errorModalMessage,
errorItemList,
errorTitle,
errorIconName,
resetSearch,
} = this.state;
return (
<ErrorBoundary showFallback={true}>
<div className="wrapper org-users-page animation-fade">
<EditRoleErrorModal
darkMode={this.props.darkMode}
show={showErrorModal}
errorMessage={errorModalMessage}
errorTitle={errorTitle}
listItems={errorItemList}
iconName={errorIconName}
onClose={this.clearErrorState}
/>
{this.state.isInviteUsersDrawerOpen && (
<ManageOrgUsersDrawer
isInviteUsersDrawerOpen={this.state.isInviteUsersDrawerOpen}
@ -311,6 +387,7 @@ class ManageOrgUsersComponent extends React.Component {
currentEditingUser={currentEditingUser}
setUserValues={this.setUserValues}
creatingUser={this.state.creatingUser}
darkMode={this.props.darkMode}
/>
)}
@ -346,16 +423,24 @@ class ManageOrgUsersComponent extends React.Component {
filterList={this.filterList}
darkMode={this.props.darkMode}
clearIconPressed={() => this.fetchUsers()}
resetSearch={resetSearch}
/>
{users?.length === 0 && (
<div className="workspace-settings-table-wrap">
<div className="d-flex justify-content-center flex-column tj-user-table-wrapper">
<div className="d-flex justify-content-center align-items-center mb-2">
<div className="user-not-found-svg">
<SolidIcon name="warning-user-notfound" fill="var(--icon-strong)" />
</div>
</div>
<span className="text-center font-weight-bold" data-cy="text-no-result-found">
No result found
</span>
<small className="text-center text-muted" data-cy="text-try-changing-filters">
Try changing the filters
<small className="text-center text-secondary" data-cy="text-try-changing-filters">
There were no results found for your search. Please
<br />
try changing the filters and try again.
</small>
</div>
</div>

View file

@ -1,9 +1,10 @@
import React, { useEffect, useState } from 'react';
import Drawer from '@/_ui/Drawer';
import InviteUsersForm from './InviteUsersForm';
import { groupPermissionService } from '@/_services';
import { groupPermissionService, groupPermissionV2Service } from '@/_services';
import { authenticationService } from '../_services/authentication.service';
import { USER_DRAWER_MODES } from '@/_helpers/utils';
import { propTypes } from 'react-bootstrap/esm/Image';
const ManageOrgUsersDrawer = ({
isInviteUsersDrawerOpen,
@ -20,6 +21,7 @@ const ManageOrgUsersDrawer = ({
userDrawerMode,
setUserValues,
creatingUser,
darkMode,
}) => {
const [groups, setGroups] = useState([]);
@ -27,11 +29,13 @@ const ManageOrgUsersDrawer = ({
const humanizeifDefaultGroupName = (groupName) => {
switch (groupName) {
case 'all_users':
return 'All users';
case 'end-user':
return 'End-user';
case 'admin':
return 'Admin';
case 'builder':
return 'Builder';
default:
return groupName;
@ -41,19 +45,17 @@ const ManageOrgUsersDrawer = ({
const fetchOrganizations = () => {
const { current_organization_id } = authenticationService.currentSessionValue;
groupPermissionService
groupPermissionV2Service
.getGroups()
.then(({ group_permissions }) => {
const orgGroups = group_permissions
.filter((group) => group.organization_id === current_organization_id)
.map(({ group }) => ({
label:
group === 'all_users' && isEditing
? `${humanizeifDefaultGroupName(group)} (Default group)`
: humanizeifDefaultGroupName(group),
name: humanizeifDefaultGroupName(group),
value: group,
...(group === 'all_users' && isEditing && { isDisabled: true, isFixed: true }),
.then(({ groupPermissions }) => {
const orgGroups = groupPermissions
.filter((group) => group.organizationId === current_organization_id)
.map(({ name, type, id }) => ({
label: humanizeifDefaultGroupName(name),
name: humanizeifDefaultGroupName(name),
value: name,
groupType: type,
id: id,
}));
setGroups(orgGroups);
})
@ -93,6 +95,7 @@ const ManageOrgUsersDrawer = ({
userDrawerMode={userDrawerMode}
setUserValues={setUserValues}
creatingUser={creatingUser}
darkMode={darkMode}
/>
</Drawer>
);

View file

@ -3,6 +3,7 @@ import { ButtonSolid } from '@/_ui/AppButton/AppButton';
import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import Select, { components } from 'react-select';
import SolidIcon from '@/_ui/Icon/solidIcons/index';
export function UserGroupsSelect(props) {
const navigate = useNavigate();
@ -30,12 +31,22 @@ export function UserGroupsSelect(props) {
);
};
const InputOption = ({ getStyles, Icon, isDisabled, isFocused, isSelected, children, innerProps, ...rest }) => {
const formatGroupLabel = (data) => {
const type = data.label;
return (
<div className="mb-2 d-flex align-items-center">
<SolidIcon name={type === 'default' ? 'usergear' : 'usergroup'} />
<span className="ml-1 group-title">{type === 'default' ? 'USER ROLE' : 'Custom groups'}</span>
{type === 'default' && <span style={{ color: 'red' }}>*</span>}
</div>
);
};
const InputOption = ({ getStyles, Icon, isDisabled, isFocused, isSelected, children, data, innerProps, ...rest }) => {
const [isActive, setIsActive] = useState(false);
const onMouseDown = () => setIsActive(true);
const onMouseUp = () => setIsActive(false);
const onMouseLeave = () => setIsActive(false);
const style = {
alignItems: 'center',
backgroundColor: 'transparent',
@ -50,7 +61,6 @@ export function UserGroupsSelect(props) {
onMouseLeave,
style,
};
return (
<components.Option
{...rest}
@ -62,8 +72,12 @@ export function UserGroupsSelect(props) {
className={isDisabled && 'disabled'}
>
<input
style={{ width: '1.2rem', height: '1.2rem', borderRadius: '6px !important' }}
type="checkbox"
style={
data.groupType === 'default'
? { height: '1.3rem' }
: { width: '1.2rem', height: '1.2rem', borderRadius: '6px !important' }
}
type={data.groupType === 'default' ? 'radio' : 'checkbox'}
className="form-check-input"
checked={isSelected}
data-cy="group-check-input"
@ -72,6 +86,13 @@ export function UserGroupsSelect(props) {
</components.Option>
);
};
const MultiValueRemove = (props) => {
// Conditionally render the close icon
if (props.data.groupType === 'default') {
return null; // Do not render the close icon
}
return <components.MultiValueRemove {...props} />;
};
const MultiValue = (props) => (
<components.MultiValue {...props}>
@ -80,6 +101,11 @@ export function UserGroupsSelect(props) {
);
const selectStyles = {
placeholder: (base) => ({
...base,
fontSize: '12px',
color: '#A0A0A0',
}),
indicatorSeparator: (base) => ({
...base,
display: 'none',
@ -122,6 +148,7 @@ export function UserGroupsSelect(props) {
border: '1px solid var(--slate7)',
boxShadow: 'none',
borderRadius: '6px',
background: 'unset',
'&:hover': {
border: '1px solid var(--slate8)',
@ -129,7 +156,7 @@ export function UserGroupsSelect(props) {
}),
menu: (base) => ({
...base,
background: 'unset',
background: 'var(--surfaces-app-bg-default)',
'.add-group-btn': {
display: 'flex',
justifyContent: 'flex-end',
@ -155,10 +182,11 @@ export function UserGroupsSelect(props) {
closeMenuOnSelect={false}
hideSelectedOptions={false}
className={darkMode && 'theme-dark dark-theme'}
components={{ Option: InputOption, MultiValue, IndicatorSeparator: null }}
formatGroupLabel={formatGroupLabel}
components={{ Option: InputOption, MultiValue, MultiValueRemove, IndicatorSeparator: null }}
{...props}
styles={selectStyles}
placeholder="Select groups to add for this user"
placeholder="Select user groups and role .."
noOptionsMessage={() => 'No groups found'}
/>
);

View file

@ -244,10 +244,7 @@ class RawManageOrgVarsComponent extends React.Component {
}
canDeleteVariable = () => {
return this.canAnyGroupPerformAction(
'org_environment_variable_delete',
authenticationService.currentSessionValue.group_permissions
);
return authenticationService.currentSessionValue.org_constant_c_r_u_d;
};
setIsManageVarDrawerOpen = (val) => {
this.setState({ isManageVarDrawerOpen: val });

View file

@ -69,7 +69,7 @@ const MarketplacePage = ({ darkMode, switchDarkMode }) => {
<div className="marketplace-body">
<div className="p-3">
<div className="row g-4">
<div className="marketplace-page-sidebar mt-3">
<div className="marketplace-page-sidebar mt-3 mx-3">
<div className="subheader mb-2">Plugins</div>
<div className="list-group mb-3">
{['Installed', 'Marketplace'].map((item, index) => (

View file

@ -0,0 +1,6 @@
export const workspaceSettingsLinks = [
{ id: 'users', name: 'Users', route: 'users', conditions: ['admin'] },
{ id: 'groups', name: 'Groups', route: 'groups', conditions: ['admin'] },
{ id: 'workspacelogin', name: 'Workspace login', route: 'workspace-login', conditions: ['admin'] },
{ id: 'workspacevariables', name: 'Workspace variables', route: 'workspace-variables', conditions: ['admin'] },
];

View file

@ -1,57 +1,59 @@
import React, { useEffect, useState, useContext } from 'react';
import cx from 'classnames';
import { useParams, Outlet, Link, useNavigate, useLocation } from 'react-router-dom';
import { Outlet, Link, useNavigate, useLocation } from 'react-router-dom';
import Layout from '@/_ui/Layout';
import { authenticationService } from '@/_services';
import { BreadCrumbContext } from '../App/App';
import FolderList from '@/_ui/FolderList/FolderList';
import { OrganizationList } from '../_components/OrganizationManager/List';
import { getWorkspaceId } from '@/_helpers/utils';
import { getSubpath } from '@/_helpers/routes';
import { workspaceSettingsLinks } from './constant';
export function OrganizationSettings(props) {
const [admin, setAdmin] = useState(authenticationService.currentSessionValue?.admin);
const [selectedTab, setSelectedTab] = useState(admin ? 'Users & permissions' : 'manageEnvVars');
const admin = authenticationService.currentSessionValue?.admin;
const [selectedTab, setSelectedTab] = useState(admin ? workspaceSettingsLinks[0].id : 'workspacevariables');
const navigate = useNavigate();
const location = useLocation();
const { updateSidebarNAV } = useContext(BreadCrumbContext);
const { workspaceId } = useParams();
const [conditionObj, setConditionObj] = useState({ admin: authenticationService.currentSessionValue?.admin });
const sideBarNavs = ['Users', 'Groups', 'Workspace login', 'Workspace variables'];
const defaultOrgName = (groupName) => {
switch (groupName) {
case 'users':
return 'Users';
case 'groups':
return 'Groups';
case 'workspace-login':
return 'Workspace login';
case 'workspace-variables':
return 'Workspace variables';
default:
return groupName;
const checkConditions = (conditions, conditionsObj) => {
if (!conditions || conditions.length === 0) {
return true;
}
return conditions.every((condition) => conditionsObj?.[condition] === true);
};
//Filtered Links from the workspace settings links array
const filteredLinks = () =>
workspaceSettingsLinks.filter((item) => {
return checkConditions(item.conditions, conditionObj);
});
const getMenuFromRoute = (route) => {
return workspaceSettingsLinks?.find((e) => e.route === route) || {};
};
useEffect(() => {
const subscription = authenticationService.currentSession.subscribe((newOrd) => {
setAdmin(newOrd?.admin);
setConditionObj({ admin: newOrd?.admin });
});
admin ? updateSidebarNAV('Users') : updateSidebarNAV('Workspace variables');
() => subscription.unsubsciption();
const selectedTabFromRoute = location.pathname.split('/').pop();
if (selectedTabFromRoute === 'workspace-settings') {
setSelectedTab(admin ? 'Users' : 'Workspace variables');
const subPath = getSubpath();
const path = subPath ? `${subPath}/${workspaceId}/workspace-settings` : `/${workspaceId}/workspace-settings`;
window.location.href = admin ? `${path}/users` : `${path}/workspace-variables`;
// No Sub routes added loading first one
setSelectedTab(admin ? workspaceSettingsLinks[0].id : 'workspacevariables');
} else {
setSelectedTab(defaultOrgName(selectedTabFromRoute));
setSelectedTab(getMenuFromRoute(selectedTabFromRoute)?.id);
}
updateSidebarNAV(defaultOrgName(selectedTabFromRoute));
}, [navigate, workspaceId, authenticationService.currentSessionValue?.admin]);
return () => subscription.unsubscribe();
}, [authenticationService.currentSessionValue?.admin]);
useEffect(() => {
const menu = workspaceSettingsLinks?.find((m) => m.id === selectedTab);
updateSidebarNAV(menu?.name || '');
navigate(menu?.route || '');
}, [selectedTab]);
return (
<Layout switchDarkMode={props.switchDarkMode} darkMode={props.darkMode}>
@ -59,12 +61,11 @@ export function OrganizationSettings(props) {
<div className="row gx-0">
<div className="organization-page-sidebar col ">
<div className="workspace-nav-list-wrap">
{sideBarNavs.map((item, index) => {
{filteredLinks().map((item, index) => {
const Wrapper = ({ children }) => <>{children}</>;
return (
<Wrapper key={index}>
<Link
to={`/${workspaceId}/workspace-settings/${item.toLowerCase().replace(/\s+/g, '-')}`} // Update the URL path here
key={index}
style={{
textDecoration: 'none',
@ -74,30 +75,26 @@ export function OrganizationSettings(props) {
backgroundColor: 'inherit',
}}
>
{admin && (
<FolderList
className="workspace-settings-nav-items"
key={index}
onClick={() => {
setSelectedTab(defaultOrgName(item));
if (item == 'Users') updateSidebarNAV('Users');
else updateSidebarNAV(item);
}}
selectedItem={selectedTab == defaultOrgName(item)}
renderBadgeForItems={['Workspace constants']}
renderBadge={() => (
<span
style={{ width: '40px', textTransform: 'lowercase' }}
className="badge bg-color-primary badge-pill"
>
new
</span>
)}
dataCy={item.toLowerCase().replace(/\s+/g, '-')}
>
{item}
</FolderList>
)}
<FolderList
className="workspace-settings-nav-items"
key={index}
onClick={() => {
setSelectedTab(item.id);
}}
selectedItem={selectedTab == item.id}
renderBadgeForItems={[]}
renderBadge={() => (
<span
style={{ width: '40px', textTransform: 'lowercase' }}
className="badge bg-color-primary badge-pill"
>
new
</span>
)}
dataCy={item.name.toLowerCase().replace(/\s+/g, '-')}
>
{item.name}
</FolderList>
</Link>
</Wrapper>
);

View file

@ -3,3 +3,4 @@ export * from './AdminRoute';
export * from './AppsRoute';
export * from './SwitchWorkspaceRoute';
export * from './OrganizationInviteRoute';
export * from './AuthRoute';

View file

@ -333,7 +333,7 @@ class SignupPageComponent extends React.Component {
<a href="https://www.tooljet.com/terms" data-cy="terms-of-service-link">
Terms of Service{' '}
</a>
&
<span>& </span>
<a href="https://www.tooljet.com/privacy" data-cy="privacy-policy-link">
{' '}
Privacy Policy

View file

@ -183,7 +183,7 @@
input.form-control:disabled {
gap: 16px !important;
background: #f4f6fa !important;
background: var(--base) !important;
border: 1px solid var(--slate7) !important;
border-radius: 6px !important;
margin-bottom: 4px !important;

View file

@ -17,10 +17,7 @@ export default function WorkspaceConstants({ darkMode, switchDarkMode }) {
};
const canCreateVariableOrConstant = () => {
return canAnyGroupPerformAction(
'org_environment_variable_create',
authenticationService.currentSessionValue.group_permissions
);
return authenticationService.currentSessionValue.user_permissions.org_constant_c_r_u_d;
};
useEffect(() => {

View file

@ -18,6 +18,7 @@ import ToolJetDbOperations from '@/Editor/QueryManager/QueryEditors/TooljetDatab
import { orgEnvironmentVariableService, orgEnvironmentConstantService } from '../_services';
import { find, isEmpty } from 'lodash';
import { ButtonSolid } from './AppButton';
import { Constants } from '@/_helpers/utils';
const DynamicForm = ({
schema,
@ -53,9 +54,16 @@ const DynamicForm = ({
React.useEffect(() => {
if (isGDS) {
orgEnvironmentConstantService.getConstantsFromEnvironment(currentAppEnvironmentId).then((data) => {
const constants = {};
data.constants.map((constant) => {
constants[constant.name] = constant.value;
const constants = {
globals: {},
secrets: {},
};
data.constants.forEach((constant) => {
if (constant.type === Constants.Secret) {
constants.secrets[constant.name] = constant.value;
} else {
constants.globals[constant.name] = constant.value;
}
});
setCurrentOrgEnvironmentConstants(constants);
@ -202,7 +210,8 @@ const DynamicForm = ({
return {
type,
placeholder: useEncrypted ? '**************' : description,
className: `form-control${handleToggle(controller)}`,
className: `form-control${handleToggle(controller)} mb-0`,
style: { marginBottom: '0px !important' },
value: options?.[key]?.value || '',
...(type === 'textarea' && { rows: rows }),
...(helpText && { helpText }),

View file

@ -38,16 +38,17 @@ export default function LogoNavDropdown({ darkMode }) {
<span>Database</span>
</Link>
)}
<Link
to={getPrivateRoute('data_sources')}
className="dropdown-item tj-text tj-text-xsm"
target="_blank"
data-cy="data-source-option"
>
<SolidIcon name="datasource" width="20" />
<span>Data sources</span>
</Link>
{admin && (
<Link
to={getPrivateRoute('data_sources')}
className="dropdown-item tj-text tj-text-xsm"
target="_blank"
data-cy="data-source-option"
>
<SolidIcon name="datasource" width="20" />
<span>Data sources</span>
</Link>
)}
<Link
to={getPrivateRoute('workspace_constants')}
className="dropdown-item tj-text tj-text-xsm"

View file

@ -19,9 +19,9 @@ function MultiSelectUser({
const listOfOptions = useRef([]);
useEffect(() => {
setOptions(filterOptions(listOfOptions.current));
setOptions(listOfOptions.current);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [selectedValues, listOfOptions.current]);
}, [JSON.stringify(selectedValues), listOfOptions.current]);
const searchFunction = useCallback(
async (query) => {
@ -35,14 +35,19 @@ function MultiSelectUser({
);
function renderCustom(props, option) {
const valuePresent = selectedValues.some((item) => item.value === option.value);
return (
<div className={`item-renderer`}>
<div>
<input
type="checkbox"
// eslint-disable-next-line no-unused-vars
checked={valuePresent}
onClick={(e) => {
onSelect([...selectedValues, option]);
if (!valuePresent) {
onSelect([...selectedValues, option]);
} else {
onSelect([...selectedValues.filter((item) => item.value !== option.value)]);
}
}}
/>
<div className="d-flex flex-column" style={{ marginLeft: '12px' }}>
@ -67,7 +72,7 @@ function MultiSelectUser({
[selectedValues]
);
return (
<div className="tj-ms tj-ms-count">
<div className="tj-ms tj-ms-count" style={{ width: '100%', paddingRight: '0px' }}>
<FilterPreview text={`${selectedValues.length} selected`} onClose={selectedValues.length ? onReset : undefined} />
<Select
className={className}
@ -76,7 +81,7 @@ function MultiSelectUser({
closeOnSelect={false}
search={true}
multiple
value={{ name: '' }}
value={selectedValues}
onChange={(id, value) => onSelect([...selectedValues, ...value])}
placeholder={placeholder}
debounce={onSearch ? 300 : undefined}

View file

@ -72,7 +72,7 @@ export const NotificationCenter = ({ darkMode }) => {
<p className="empty-title mb-1" data-cy="empty-notification-title">
{t('header.notificationCenter.youAreCaughtUp', `You're all caught up!`)}
</p>
<p className="empty-subtitle text-muted" data-cy="empty-notification-subtitle">
<p className="empty-subtitle" data-cy="empty-notification-subtitle">
{`${t('header.notificationCenter.youDontHaveany', `You don't have any`)} ${
!isRead ? t('header.notificationCenter.un', 'un') : ''
}${t('header.notificationCenter.read', 'read')} ${t(
@ -91,7 +91,7 @@ export const NotificationCenter = ({ darkMode }) => {
</div>
<div className="card-footer text-center margin-auto">
<span
className="text-muted text-decoration-none cursor-pointer"
className="text-decoration-none cursor-pointer"
onClick={() => setIsRead(!isRead)}
data-cy="notifications-card-footer"
>

View file

@ -1,5 +1,5 @@
import { useSpring, config, animated } from 'react-spring';
import { resolveReferences } from '../../_helpers/utils';
import { resolveReferences, verifyConstant } from '../../_helpers/utils';
import { Alert } from '../../_ui/Alert';
import useHeight from '@/_hooks/use-height-transition';
import React from 'react';
@ -7,9 +7,11 @@ import React from 'react';
export const OrgConstantVariablesPreviewBox = ({ workspaceVariables, workspaceConstants, value, isFocused }) => {
const getResolveValueType = (currentValue) => {
if (!currentValue) return null;
if (currentValue.includes('secrets')) {
return 'Workspace secret constant';
}
if (currentValue.includes('constants')) {
return 'Workspace Constant';
return 'Workspace global constant';
}
if (currentValue.includes('client')) {
@ -25,7 +27,8 @@ export const OrgConstantVariablesPreviewBox = ({ workspaceVariables, workspaceCo
const shouldResolve =
typeof value === 'string' &&
((value.includes('%%') && (value.includes('client.') || value.includes('server.'))) ||
(value.includes('{{') && value.includes('constants.')));
(value.includes('{{') && value.includes('constants.')) ||
(value.includes('{{') && value.includes('secrets.')));
if (!shouldResolve) return null;
@ -35,34 +38,31 @@ export const OrgConstantVariablesPreviewBox = ({ workspaceVariables, workspaceCo
<ResolvedValue
value={value}
isFocused={isFocused}
state={{ ...workspaceVariables, constants: workspaceConstants }}
state={{
...workspaceVariables,
constants: workspaceConstants?.globals || {},
secrets: workspaceConstants?.secrets || {},
}}
type={valueType}
/>
);
};
const verifyConstant = (value, definedConstants) => {
const constantRegex = /{{constants\.([a-zA-Z0-9_]+)}}/g;
if (typeof value !== 'string') {
return [];
}
const matches = value.match(constantRegex);
if (!matches) {
return [];
}
const resolvedMatches = matches.map((match) => {
const cleanedMatch = match.replace(/{{constants\./, '').replace(/}}/, '');
return Object.keys(definedConstants).includes(cleanedMatch) ? null : cleanedMatch;
});
const invalidConstants = resolvedMatches?.filter((item) => item != null);
if (invalidConstants?.length) {
return invalidConstants;
}
};
const ResolvedValue = ({ value, isFocused, state = {}, type }) => {
const [preview, error] = resolveReferences(value, null, {}, true, true);
const isSecret = type === 'Workspace secret constant';
const hiddenSecretText = 'Values of secret constants are hidden';
const invalidConstants = verifyConstant(value, state.constants, state.secrets);
let preview;
let error;
if (invalidConstants?.length) {
[preview, error] = [value, `Undefined constants: ${invalidConstants}`];
} else {
[preview, error] = resolveReferences(value, state, null, {}, true, true);
if (isSecret && !error) {
preview = hiddenSecretText;
}
}
const previewType = typeof preview;
let resolvedValue = preview;
@ -71,8 +71,9 @@ const ResolvedValue = ({ value, isFocused, state = {}, type }) => {
? 'HiddenEnvironmentVariable'
: error?.toString();
const isValidError = error && errorMessage !== 'HiddenEnvironmentVariable';
const isUndefinedConstantsError = error && errorMessage.includes('Undefined constants:');
if (error && !isValidError) {
if (error && (!isValidError || isUndefinedConstantsError)) {
resolvedValue = errorMessage;
}
@ -97,7 +98,7 @@ const ResolvedValue = ({ value, isFocused, state = {}, type }) => {
}
};
const isConstant = type === 'Workspace Constant';
const isConstant = type === 'Workspace global constant' || type === 'Workspace secret constant';
const [heightRef, currentHeight] = useHeight();
@ -117,10 +118,14 @@ const ResolvedValue = ({ value, isFocused, state = {}, type }) => {
ref={heightRef}
className={`dynamic-variable-preview px-1 py-1 ${isValidError ? 'bg-red-lt' : 'bg-green-lt'}`}
>
<div className="alert-banner-type-text">
<div className="alert-banner-type-text" data-cy="variable-preview">
<div className="d-flex my-1">
<div className="flex-grow-1" style={{ fontWeight: 800, textTransform: 'capitalize' }}>
{isValidError ? 'Error' : ` ${type} - ${previewType}`}
<div
className="flex-grow-1"
style={{ fontWeight: 800, textTransform: 'capitalize' }}
data-cy="alert-banner-type-text"
>
{isValidError ? 'Error' : isConstant ? null : ` ${type} - ${previewType}`}
</div>
</div>
{getPreviewContent(resolvedValue, previewType)}

View file

@ -262,7 +262,7 @@ export function GithubSSOModal({ settings, onClose, onUpdateSSOSettings, isInsta
<label className="form-label" data-cy="redirect-url-label">
{t('header.organization.menus.manageSSO.github.redirectUrl', 'Redirect URL')}
</label>
<div className="d-flex justify-content-between form-control align-items-center">
<div className="d-flex justify-content-between form-control-org-login align-items-center">
<p data-cy="redirect-url" id="redirect-url">{`${window.public_config?.TOOLJET_HOST}${
window.public_config?.SUB_PATH ? window.public_config?.SUB_PATH : '/'
}sso/git/${configId}`}</p>

View file

@ -193,7 +193,7 @@ export function GoogleSSOModal({ settings, onClose, onUpdateSSOSettings, isInsta
<label className="form-label" data-cy="redirect-url-label">
{t('header.organization.menus.manageSSO.google.redirectUrl', 'Redirect URL')}
</label>
<div className="d-flex justify-content-between form-control align-items-center">
<div className="d-flex justify-content-between form-control-org-login align-items-center">
<p data-cy="redirect-url" id="redirect-url">{`${window.public_config?.TOOLJET_HOST}${
window.public_config?.SUB_PATH ? window.public_config?.SUB_PATH : '/'
}sso/google/${configId}`}</p>

View file

@ -341,7 +341,7 @@ class OrganizationLogin extends React.Component {
</label>
<div className="help-text danger-text-login">
<div data-cy="enable-sign-up-helper-text">
Users will be able to sign up without being invited
Users will be able to sign up as end-users without being invited
</div>
</div>
</div>

View file

@ -80,11 +80,13 @@ class SSOConfiguration extends React.Component {
componentDidMount() {
const initialState = this.initializeOptionStates(this.props.ssoOptions);
const enabledSSOCount = this.getCountOfEnabledSSO();
this.setState({ ...initialState });
this.setState({ ssoOptions: this.props.ssoOptions });
this.setState({ defaultSSO: this.props.defaultSSO });
this.setState({ isAnySSOEnabled: this.props.isAnySSOEnabled });
this.setState({ instanceSSO: this.props.instanceSSO });
this.setState({ inheritedInstanceSSO: enabledSSOCount });
}
componentDidUpdate(prevProps) {

View file

@ -1,4 +1,4 @@
import React, { useState } from 'react';
import React, { useState, useRef, useEffect } from 'react';
import { organizationService } from '@/_services';
import AlertDialog from '@/_ui/AlertDialog';
import { useTranslation } from 'react-i18next';
@ -7,6 +7,7 @@ import { validateName, handleHttpErrorMessages } from '@/_helpers/utils';
import { appendWorkspaceId, getHostURL } from '@/_helpers/routes';
import _ from 'lodash';
import { FormWrapper } from '@/_components/FormWrapper';
import { toast } from 'react-hot-toast';
export const CreateOrganization = ({ showCreateOrg, setShowCreateOrg }) => {
const [isCreating, setIsCreating] = useState(false);
@ -18,10 +19,10 @@ export const CreateOrganization = ({ showCreateOrg, setShowCreateOrg }) => {
const [isSlugDisabled, setSlugDisabled] = useState(true);
const darkMode = localStorage.getItem('darkMode') === 'true';
const { t } = useTranslation();
const isSlugSet = useRef(false); // Flag to track if slug has been initially set
const sluginput = useRef('');
const createOrganization = () => {
let emptyError = false;
[name, slug].map((field, index) => {
if (!field?.value?.trim()) {
index === 0
@ -42,6 +43,7 @@ export const CreateOrganization = ({ showCreateOrg, setShowCreateOrg }) => {
setIsCreating(true);
organizationService.createOrganization({ name: name.value, slug: slugValue }).then(
() => {
toast.success('Workspace created successfully');
setIsCreating(false);
const newPath = appendWorkspaceId(slugValue, location.pathname, true);
window.history.replaceState(null, null, newPath);
@ -77,7 +79,6 @@ export const CreateOrganization = ({ showCreateOrg, setShowCreateOrg }) => {
!(field === 'slug'),
field === 'slug'
);
/* If the basic validation is passing. then check the uniqueness */
if (error?.status === true) {
try {
@ -92,7 +93,6 @@ export const CreateOrganization = ({ showCreateOrg, setShowCreateOrg }) => {
};
}
}
const disabled = !error?.status;
const updatedValue = {
value,
@ -126,19 +126,56 @@ export const CreateOrganization = ({ showCreateOrg, setShowCreateOrg }) => {
setShowCreateOrg(false);
setNameDisabled(true);
setSlugDisabled(true);
isSlugSet.current = false;
};
const delayedSlugChange = _.debounce(async (value) => {
setSlugProgress(true);
await handleInputChange(value, 'slug');
}, 300);
const delayedNameChange = _.debounce(async (value) => {
setWorkspaceNameProgress(true);
await handleInputChange(value, 'name');
}, 300);
useEffect(() => {
if (!isSlugSet.current && name.value && !slugProgress && !workspaceNameProgress) {
const defaultValue =
name?.value
.replace(/\s+/g, '')
.toLowerCase()
.replace(/[^a-z0-9-\s]/g, '') || '';
setSlug({ value: defaultValue, error: '' });
const isDisabled = isCreating || isNameDisabled || isSlugDisabled || slugProgress || workspaceNameProgress;
const checkWorkspaceUniqueness = async () => {
try {
await organizationService.checkWorkspaceUniqueness(null, defaultValue);
sluginput.current.value = defaultValue;
} catch (errResponse) {
let error = {
status: false,
errorMsg: errResponse?.error,
};
setSlug({ value: defaultValue, error: error?.errorMsg });
sluginput.current.value = defaultValue;
}
};
checkWorkspaceUniqueness();
setSlugDisabled(false);
setSlugProgress(false);
}
if (slugProgress && !isSlugSet.current) {
// this is to denote that the user has tried editing the slug -- so now slug and name are independent of each other
isSlugSet.current = true;
}
}, [name.value, slug.value, slugProgress, workspaceNameProgress, isSlugSet]);
const isDisabled =
isCreating ||
isNameDisabled ||
isSlugDisabled ||
slugProgress ||
workspaceNameProgress ||
slug?.error ||
name?.error;
return (
<AlertDialog
@ -168,6 +205,8 @@ export const CreateOrganization = ({ showCreateOrg, setShowCreateOrg }) => {
<label className="label tj-input-error" data-cy="workspace-error-label">
{name?.error || ''}
</label>
) : name.value && !workspaceNameProgress ? (
<label className="label label-success" data-cy="slug-sucess-label">{`Workspace name accepted!`}</label>
) : (
<label className="label label-info" data-cy="workspace-name-info-label">
Name must be unique and max 50 characters
@ -183,6 +222,7 @@ export const CreateOrganization = ({ showCreateOrg, setShowCreateOrg }) => {
className={`form-control ${slug?.error ? 'is-invalid' : 'is-valid'}`}
placeholder={t('header.organization.workspaceSlug', 'Unique workspace slug')}
disabled={isCreating}
ref={sluginput}
maxLength={50}
onChange={async (e) => {
e.persist();
@ -203,6 +243,7 @@ export const CreateOrganization = ({ showCreateOrg, setShowCreateOrg }) => {
</svg>
</div>
)}
{slug?.error ? (
<label className="label tj-input-error" data-cy="input-label-error">
{slug?.error || ''}

View file

@ -38,12 +38,24 @@ export const SearchBox = forwardRef(
onClearCallback?.();
};
const handleClickOutside = (event) => {
if (ref.current && !ref.current.contains(event.target)) {
clearSearchText();
// Your function to be triggered
}
};
const mounted = useMounted();
useEffect(() => {
document.addEventListener('mousedown', handleClickOutside);
if (mounted) {
onSubmit?.(debouncedSearchTerm);
}
return () => {
// Cleanup event listener on component unmount
document.removeEventListener('mousedown', handleClickOutside);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [debouncedSearchTerm, onSubmit]);

View file

@ -91,6 +91,7 @@ export const ERROR_MESSAGES = {
export const TOOLTIP_MESSAGES = {
SHARE_URL_UNAVAILABLE: 'Share URL is unavailable until current version is released',
RELEASE_VERSION_URL_UNAVAILABLE: 'Release the version to make it public',
};
export const DATA_SOURCE_TYPE = {

View file

@ -1,5 +1,5 @@
export const APP_ERROR_TYPE = {
IMPORT_EXPORT_SERVICE: {
UNSUPPORTED_VERSION_ERROR: "Can't import higher version application to lower version",
UNSUPPORTED_VERSION_ERROR: 'Apps built on later versions of ToolJet cannot be imported',
},
};

View file

@ -17,6 +17,37 @@ import { componentTypes } from '@/Editor/WidgetManager/components';
const reservedKeyword = ['app', 'window'];
export const Constants = {
Global: 'Global',
Secret: 'Secret',
};
export const verifyConstant = (value, definedConstants = {}, definedSecrets = {}) => {
const globalConstantRegex = /{{constants\.([a-zA-Z0-9_]+)}}/g;
const secretConstantRegex = /{{secrets\.([a-zA-Z0-9_]+)}}/g;
if (typeof value !== 'string') {
return [];
}
const matches = [...(value.match(globalConstantRegex) || []), ...(value.match(secretConstantRegex) || [])];
if (!matches) {
return [];
}
const resolvedMatches = matches.map((match) => {
const cleanedMatch = match
.replace(/{{constants\./, '')
.replace(/{{secrets\./, '')
.replace(/}}/, '');
return Object.keys(definedConstants).includes(cleanedMatch) || Object.keys(definedSecrets).includes(cleanedMatch)
? null
: cleanedMatch;
});
const invalidConstants = resolvedMatches?.filter((item) => item != null);
if (invalidConstants?.length) {
return invalidConstants;
}
};
export function findProp(obj, prop, defval) {
if (typeof defval === 'undefined') defval = null;
prop = prop.split('.');
@ -73,6 +104,7 @@ function resolveCode(code, state, customObjects = {}, withError = false, reserve
'client',
'server',
'constants',
'secrets',
'parameters',
'moment',
'_',
@ -90,6 +122,7 @@ function resolveCode(code, state, customObjects = {}, withError = false, reserve
isJsCode ? undefined : state?.client,
isJsCode ? undefined : state?.server,
state?.constants, // Passing constants as an argument allows the evaluated code to access and utilize the constants value correctly.
state?.secrets || {},
state?.parameters,
moment,
_,
@ -98,10 +131,8 @@ function resolveCode(code, state, customObjects = {}, withError = false, reserve
);
} catch (err) {
error = err;
// console.log('eval_error', err);
}
}
if (withError) return [result, error];
return result;
}
@ -227,10 +258,10 @@ export function resolveReferences(
if (dynamicVariables) {
if (dynamicVariables.length === 1 && dynamicVariables[0] === object) {
object = resolveReferences(dynamicVariables[0], null, customObjects);
object = resolveReferences(dynamicVariables[0], state, null, customObjects, false, false);
} else {
for (const dynamicVariable of dynamicVariables) {
const value = resolveReferences(dynamicVariable, null, customObjects);
const value = resolveReferences(dynamicVariable, state, null, customObjects, false, false);
if (typeof value !== 'function') {
object = object.replace(dynamicVariable, value);
}
@ -1068,7 +1099,7 @@ export const validateName = (
checkReservedWords = false,
allowAllCases = false
) => {
const newName = name;
const newName = name.trim();
let errorMsg = '';
if (emptyCheck && !newName) {
errorMsg = `${nameType} can't be empty`;
@ -1266,11 +1297,13 @@ export const USER_DRAWER_MODES = {
export const humanizeifDefaultGroupName = (groupName) => {
switch (groupName) {
case 'all_users':
return 'All users';
case 'end-user':
return 'End-user';
case 'admin':
return 'Admin';
case 'builder':
return 'Builder';
default:
return groupName;
@ -1403,3 +1436,14 @@ export const removeNestedDoubleCurlyBraces = (str) => {
return transformedInput.join('');
};
export const validatePassword = (value) => {
if (!value.trim()) {
return 'Password is required';
}
if (value.length < 5) {
return 'Password must be at least 5 characters long';
}
if (value.length > 100) {
return 'Password can be at max 100 characters long';
}
};

View file

@ -17,8 +17,10 @@ const currentSessionSubject = new BehaviorSubject({
current_organization_name: null,
super_admin: null,
admin: null,
user_permissions: null,
group_permissions: null,
app_group_permissions: null,
role: null,
organizations: [],
isUserLoggingIn: false,
authentication_status: null,

View file

@ -0,0 +1,190 @@
import config from 'config';
import { authHeader, handleResponse } from '@/_helpers';
export const groupPermissionV2Service = {
create,
update,
del,
getGroup,
getGroups,
fetchAddableApps,
getUsersInGroup,
getUsersNotInGroup,
updateUserRole,
addUsersInGroups,
deleteUserFromGroup,
createGranularPermission,
fetchGranularPermissions,
deleteGranularPermission,
updateGranularPermission,
duplicate,
};
function create(name) {
const body = {
name,
};
const requestOptions = {
method: 'POST',
headers: authHeader(),
credentials: 'include',
body: JSON.stringify(body),
};
return fetch(`${config.apiUrl}/v2/group_permissions`, requestOptions).then(handleResponse);
}
function update(groupPermissionId, body) {
const requestOptions = {
method: 'PUT',
headers: authHeader(),
credentials: 'include',
body: JSON.stringify(body),
};
return fetch(`${config.apiUrl}/v2/group_permissions/${groupPermissionId}`, requestOptions).then(handleResponse);
}
function del(groupPermissionId) {
const requestOptions = {
method: 'DELETE',
headers: authHeader(),
credentials: 'include',
};
return fetch(`${config.apiUrl}/v2/group_permissions/${groupPermissionId}`, requestOptions).then(handleResponse);
}
function getGroup(groupPermissionId) {
const requestOptions = {
method: 'GET',
headers: authHeader(),
credentials: 'include',
};
return fetch(`${config.apiUrl}/v2/group_permissions/${groupPermissionId}`, requestOptions).then(handleResponse);
}
function fetchAddableApps() {
const requestOptions = {
method: 'GET',
headers: authHeader(),
credentials: 'include',
};
return fetch(`${config.apiUrl}/v2/group_permissions/granular-permissions/addable-apps`, requestOptions).then(
handleResponse
);
}
function getGroups() {
const requestOptions = {
method: 'GET',
headers: authHeader(),
credentials: 'include',
};
return fetch(`${config.apiUrl}/v2/group_permissions`, requestOptions).then(handleResponse);
}
function addUsersInGroups(body) {
const requestOptions = {
method: 'POST',
headers: authHeader(),
credentials: 'include',
body: JSON.stringify(body),
};
return fetch(`${config.apiUrl}/v2/group_permissions/group-user`, requestOptions).then(handleResponse);
}
function deleteUserFromGroup(id) {
const requestOptions = {
method: 'DELETE',
headers: authHeader(),
credentials: 'include',
};
return fetch(`${config.apiUrl}/v2/group_permissions/group-user/${id}`, requestOptions).then(handleResponse);
}
function createGranularPermission(body) {
const requestOptions = {
method: 'POST',
headers: authHeader(),
credentials: 'include',
body: JSON.stringify(body),
};
return fetch(`${config.apiUrl}/v2/group_permissions/granular-permissions`, requestOptions).then(handleResponse);
}
function updateGranularPermission(id, body) {
const requestOptions = {
method: 'PUT',
headers: authHeader(),
credentials: 'include',
body: JSON.stringify(body),
};
return fetch(`${config.apiUrl}/v2/group_permissions/granular-permissions/update/${id}`, requestOptions).then(
handleResponse
);
}
function deleteGranularPermission(id) {
const requestOptions = {
method: 'DELETE',
headers: authHeader(),
credentials: 'include',
};
return fetch(`${config.apiUrl}/v2/group_permissions/granular-permissions/${id}`, requestOptions).then(handleResponse);
}
function fetchGranularPermissions(groupPermissionId) {
const requestOptions = {
method: 'GET',
headers: authHeader(),
credentials: 'include',
};
return fetch(`${config.apiUrl}/v2/group_permissions/${groupPermissionId}/granular-permissions`, requestOptions).then(
handleResponse
);
}
function updateUserRole(body) {
const requestOptions = {
method: 'PUT',
headers: authHeader(),
credentials: 'include',
body: JSON.stringify(body),
};
return fetch(`${config.apiUrl}/v2/group_permissions/user-role/edit`, requestOptions).then(handleResponse);
}
function getUsersInGroup(groupPermissionId, searchInput = '') {
const requestOptions = {
method: 'GET',
headers: authHeader(),
credentials: 'include',
};
return fetch(
`${config.apiUrl}/v2/group_permissions/${groupPermissionId}/group-user?input=${searchInput && searchInput?.trim()}`,
requestOptions
).then(handleResponse);
}
function getUsersNotInGroup(searchInput, groupPermissionId) {
const requestOptions = {
method: 'GET',
headers: authHeader(),
credentials: 'include',
};
return fetch(
`${config.apiUrl}/v2/group_permissions/${groupPermissionId}/group-user/addable-users?input=${searchInput.trim()}`,
requestOptions
).then(handleResponse);
}
function duplicate(groupPermissionId, body) {
const requestOptions = {
method: 'POST',
headers: authHeader(),
credentials: 'include',
body: JSON.stringify(body),
};
return fetch(`${config.apiUrl}/v2/group_permissions/${groupPermissionId}/duplicate`, requestOptions).then(
handleResponse
);
}

View file

@ -22,3 +22,4 @@ export * from './globalDatasource.service';
export * from './app_environment.service';
export * from './copilot.service';
export * from './organization_constants.service';
export * from './groupPermission.v2.service';

View file

@ -7,20 +7,22 @@ export const orgEnvironmentConstantService = {
update,
remove,
getConstantsFromEnvironment,
getConstantsFromApp,
getConstantsFromPublicApp,
getAllSecrets,
};
function getAll(decryptValue = false) {
function getAll(type = null) {
const requestOptions = { method: 'GET', headers: authHeader(), credentials: 'include' };
return fetch(`${config.apiUrl}/organization-constants?decryptValue=${decryptValue}`, requestOptions).then(
handleResponse
);
const queryParams = type ? `?type=${type}` : '';
return fetch(`${config.apiUrl}/organization-constants${queryParams}`, requestOptions).then(handleResponse);
}
function create(name, value, environments) {
function create(name, value, type, environments) {
const body = {
constant_name: name,
value: value,
type: type,
environments: environments,
};
@ -28,10 +30,10 @@ function create(name, value, environments) {
return fetch(`${config.apiUrl}/organization-constants`, requestOptions).then(handleResponse);
}
function update(id, value, envronmentId) {
function update(id, value, environmentId) {
const body = {
value,
environment_id: envronmentId,
environment_id: environmentId,
};
const requestOptions = { method: 'PATCH', headers: authHeader(), credentials: 'include', body: JSON.stringify(body) };
@ -45,17 +47,26 @@ function remove(id, environmentId) {
);
}
function getConstantsFromEnvironment(environmentId, decryptValue = false) {
function getConstantsFromEnvironment(environmentId, type = null) {
const requestOptions = { method: 'GET', headers: authHeader(), credentials: 'include' };
const queryParams = type ? `?type=${type}` : '';
return fetch(
`${config.apiUrl}/organization-constants/environment/${environmentId}?decryptValue=${decryptValue}`,
`${config.apiUrl}/organization-constants/environment/${environmentId}${queryParams}`,
requestOptions
).then(handleResponse);
}
function getConstantsFromPublicApp(slug, decryptValue = false) {
const requestOptions = { method: 'GET' };
return fetch(`${config.apiUrl}/organization-constants/${slug}?decryptValue=${decryptValue}`, requestOptions).then(
handleResponse
);
function getAllSecrets() {
const requestOptions = { method: 'GET', headers: authHeader(), credentials: 'include' };
return fetch(`${config.apiUrl}/organization-constants/secrets`, requestOptions).then(handleResponse);
}
function getConstantsFromApp(slug) {
const requestOptions = { method: 'GET', headers: authHeader(), credentials: 'include' };
return fetch(`${config.apiUrl}/organization-constants/${slug}`, requestOptions).then(handleResponse);
}
function getConstantsFromPublicApp(slug) {
const requestOptions = { method: 'GET', headers: authHeader(), credentials: 'include' };
return fetch(`${config.apiUrl}/organization-constants/public/${slug}`, requestOptions).then(handleResponse);
}

View file

@ -7,13 +7,15 @@ import { useEditorStore } from '@/_stores/editorStore';
import { useQueryPanelStore } from '@/_stores/queryPanelStore';
import update from 'immutability-helper';
const { diff } = require('deep-object-diff');
import { useAppDataStore } from './appDataStore';
import { authenticationService } from '@/_services';
const initialState = {
queries: {},
components: {},
globals: {
theme: { name: 'light' },
urlparams: null,
currentUser: {},
},
errors: {},
variables: {},
@ -53,9 +55,25 @@ export const useCurrentStateStore = create(
},
setEditorReady: (isEditorReady) => set({ isEditorReady }),
initializeCurrentStateOnVersionSwitch: () => {
//fetch user for current app
const currentSession = authenticationService.currentSessionValue;
const currentUser = useAppDataStore.getState().currentUser;
const userVars = {
email: currentUser?.email,
firstName: currentUser?.first_name,
lastName: currentUser?.last_name,
groups: currentSession?.group_permissions
? ['all_users', ...currentSession.group_permissions.map((group) => group.name)]
: ['all_users'],
role: currentSession?.role?.name,
};
const newInitialState = {
...initialState,
constants: get().constants,
globals: {
...get().globals,
currentUser: userVars,
},
};
set({ ...newInitialState }, false, {
type: 'INITIALIZE_CURRENT_STATE_ON_VERSION_SWITCH',

View file

@ -1,6 +1,6 @@
import { create, zustandDevTools } from './utils';
import { getDefaultOptions } from './storeHelper';
import { dataqueryService } from '@/_services';
import { dataqueryService, orgEnvironmentConstantService } from '@/_services';
// import debounce from 'lodash/debounce';
import { useAppDataStore } from '@/_stores/appDataStore';
import { v4 as uuidv4 } from 'uuid';
@ -13,9 +13,12 @@ import { handleReferenceTransactions } from './handleReferenceTransactions';
import { useAppVersionStore } from '@/_stores/appVersionStore';
import { useEditorStore } from '@/_stores/editorStore';
import { useQueryPanelStore } from '@/_stores/queryPanelStore';
import { Constants } from '@/_helpers/utils';
const secretValue = '**********';
const initialState = {
dataQueries: [],
secrets: [],
sortBy: 'updated_at',
sortOrder: 'desc',
loadingDataQueries: true,
@ -38,6 +41,7 @@ export const useDataQueriesStore = create(
fetchDataQueries: async (appVersionId, selectFirstQuery = false, runQueriesOnAppLoad = false, ref) => {
get().loadingDataQueries && set({ loadingDataQueries: true });
const data = await dataqueryService.getAll(appVersionId);
const { constants } = await orgEnvironmentConstantService.getAllSecrets();
const diff = _.differenceWith(data.data_queries, get().dataQueries, _.isEqual);
const referencesManager = useResolveStore.getState().referenceMapper;
@ -57,6 +61,10 @@ export const useDataQueriesStore = create(
set((state) => ({
dataQueries: sortByAttribute(data.data_queries, state.sortBy, state.sortOrder),
loadingDataQueries: false,
secrets: constants.reduce((acc, constant) => {
acc[constant.name] = secretValue;
return acc;
}, {}),
}));
// Compute query state to be added in the current state

View file

@ -300,4 +300,37 @@
border-radius: 6px;
margin: auto;
}
}
.add-plugin-card{
border: 1px dashed var(--border-default, #CCD1D5);
background-color: var(--interactive-weak);
&:hover{
border: 1px dashed var(--border-strong, #CCD1D5);
background-color: var(--interactive-default);
}
}
.add-plugin-card-title{
font-weight: 500;
font-size: 14px;
line-height: 20px;
color: var(--text-default);
}
.dark-theme{
.add-plugin-card{
border: 1px dashed var(--border-default, #CCD1D5);
background-color: var(--interactive-weak);
&:hover{
border: 1px dashed var(--border-strong, #CCD1D5);
background-color: var(--interactive-default);
}
}
}

View file

@ -0,0 +1,159 @@
@import "./typography.scss";
@import "./designtheme.scss";
.manage-granular-permissions-info {
display: flex;
height: 48px;
width: 612px;
border-radius: 6px;
padding: 12px 24px 12px 24px;
background: var(--slate3);
border: 1px solid var(--slate5);
border-radius: 6px;
margin-bottom: 16px;
p {
color: var(--slate12);
// gap: 14px;
display: flex;
align-items: center;
}
}
.manage-granular-permission-header {
border-bottom: 1px solid var(--slate5);
display: flex;
p {
padding: 8px 12px;
// gap: 10px;
width: 230px;
height: 36px;
font-weight: 500;
color: var(--slate11) !important;
}
}
.empty-container {
flex-shrink: 0; /* Prevent shrinking */
min-height: calc(100vh - 300px - 100px);
display: flex;
align-items: center; /* Center items vertically */
justify-content: center; /* Center items horizontally */
text-align: center;
flex-direction: column;
width: 100%;
.menu{
width: 100%;
display: flex;
align-items: center; /* Center items vertically */
justify-content: center; /* Center items horizontally */
}
.icon-container {
width: 55px;
height: 55px;
background: var(--indigo4);
border-radius: 6px;
svg {
width: 45px;
height: 45px;
path {
fill: var(--indigo9);
}
}
}
.add-permission-btn {
width: 190px;
}
.add-icon {
width: 135px;
height: 30px;
}
}
.permission-body-one {
flex-grow: 1; /* Allow this to grow and fill available space */
overflow-y: auto;
border-bottom: 1px solid var(--slate5);
margin: 0; /* Ensure no margin */
padding: 0; /* Ensure no padding */
max-height: calc(100vh - 370px - 100px);
min-height: calc(100vh - 370px - 100px);
}
.permission-body-two {
flex-grow: 1; /* Allow this to grow and fill available space */
overflow-y: auto;
border-bottom: 1px solid var(--slate5);
margin: 0; /* Ensure no margin */
padding: 0; /* Ensure no padding */
max-height: calc(100vh - 300px - 100px);
min-height: calc(100vh - 300px - 100px);
}
.side-button-cont {
justify-self: flex-end;
display: flex;
align-items: center; /* Ensure the content is centered vertically */
height: 50px;
flex-shrink: 0; /* Prevent shrinking */
margin: 0; /* Ensure no margin */
padding: 12px; /* Ensure no padding */
justify-content: flex-end;
// margin-bottom: 20px;
// margin-top: auto;
.add-icon {
width: 135px;
height: 30px;
}
}
.permission-type {
border: 0px !important;
width: 100% !important;
justify-content: flex-start;
padding-left: 1rem;
}
.permission-manager-modal {
.permission-manager-title {
display: flex;
align-items: center;
gap: 5px;
}
.type-container {
display: flex;
justify-content: space-between;
.right-container {
display: flex;
flex-direction: column;
}
}
.tj-text-xsm{
color: var(--slate11);
}
}
.delete-icon-cont {
margin-left: 200px;
.icon-class{
border: none !important;
background-color: none !important;
}
}

View file

@ -461,9 +461,7 @@
line-break: anywhere;
text-align: center;
span {
color: #466BF2;
}
}
.signup-password-wrap,

View file

@ -2293,6 +2293,24 @@ progress {
transition: border-color .15s ease-in-out, box-shadow .15s ease-in-out
}
.form-control-org-login {
display: block;
width: 100%;
padding: .4375rem .75rem;
font-size: .875rem;
font-weight: 400;
line-height: 1.4285714;
background: var(--base);
background-clip: padding-box;
border: 1px solid var(--slate7);
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
border-radius: 4px;
transition: border-color .15s ease-in-out, box-shadow .15s ease-in-out
}
@media (prefers-reduced-motion:reduce) {
.form-control {
transition: none
@ -3546,7 +3564,8 @@ fieldset:disabled .btn {
}
.dropdown-menu.show {
display: block
display: block;
z-index: 9999;
}
.dropdown-header {

View file

@ -12,6 +12,7 @@
@import "./ui-operations.scss";
@import 'react-loading-skeleton/dist/skeleton.css';
@import './table-component.scss';
@import './groups-permissions.scss';
@import 'tailwindcss/base';
@import 'tailwindcss/components';
@import 'tailwindcss/utilities';
@ -6028,7 +6029,7 @@ div#driver-page-overlay {
&:focus-visible {
background: var(--slate1);
border: 1px solid var(--slate8);
border: 1px solid var(--indigo9);
outline: none;
}
@ -6044,6 +6045,10 @@ div#driver-page-overlay {
color: var(--slate9);
cursor: not-allowed;
}
&::placeholder {
color: var(--slate9);
font-weight: 400;
}
}
@ -8961,25 +8966,38 @@ tbody {
padding: 16px;
tbody {
tr>td>span,
tr>td>a {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 140px;
tr{
td {
border-bottom-width: 0px !important;
display: flex;
align-items: center;
flex: 9%;
padding-left: 0px !important;
padding-right: 0px !important;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
&[data-name="role-header"] {
max-width: 98px !important;
}
span,a {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 140px;
}
}
}
}
thead {
tr {
padding: 0px 6px;
padding: 6px 0px 0px 6px;
gap: 8px;
width: 848px;
height: 40px;
display: flex;
align-items: center;
margin-top: 6px;
}
tr>th {
@ -8987,6 +9005,10 @@ tbody {
border-bottom: none !important;
padding: 0 !important;
width: 282px;
&[data-name="role-header"] {
width:120px !important;
}
}
}
@ -9000,17 +9022,21 @@ tbody {
gap: 8px;
}
tr>td {
border-bottom-width: 0px !important;
display: flex;
align-items: center;
flex: 9%;
padding-left: 0px !important;
padding-right: 0px !important;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
// tr>td {
// border-bottom-width: 0px !important;
// display: flex;
// align-items: center;
// flex: 9%;
// padding-left: 0px !important;
// padding-right: 0px !important;
// white-space: nowrap;
// overflow: hidden;
// text-overflow: ellipsis;
// &[data-name="role-header"] {
// width:120px !important;
// }
// }
}
.user-actions-button {
@ -9477,15 +9503,21 @@ tbody {
}
.manage-groups-body {
padding: 24px;
padding: 12px 12px 10px 12px;
font-size: 12px;
overflow-y: auto;
// overflow-y: auto;
height: calc(100vh - 300px);
.group-users-list-container{
height: calc(100vh - 300px - 100px); /* Set a fixed height */
overflow-y: auto; /* Enable vertical scrolling */
border-bottom: 1px solid var(--slate6) !important;
}
}
.groups-sub-header-wrap {
width: 612px;
// width: 612px;
height: 36px;
border-bottom: 1px solid var(--slate5) !important;
@ -9655,14 +9687,15 @@ tbody {
}
.apps-permission-wrap {
height: 72px;
height: auto;
justify-content: center;
width: auto;
gap: 12px;
}
.apps-folder-permission-wrap,
.apps--variable-permission-wrap {
height: 44px;
height: auto;
}
.manage-group-permision-header {
@ -9672,7 +9705,7 @@ tbody {
p {
padding: 8px 12px;
gap: 10px;
width: 206px;
width: 230px;
height: 36px;
font-weight: 500;
color: var(--slate11) !important;
@ -9719,13 +9752,19 @@ tbody {
.default-group-wrap {
gap: 10px;
width: 119px;
height: 28px;
width: 130px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
background: var(--grass3);
background: var(--indigo3);
border-radius: 100px;
border: 2px solid var(--indigo7);
color: var(--indigo9);
path {
fill: var(--indigo9);
}
}
.sso-icon-wrapper {
@ -9796,6 +9835,9 @@ tbody {
text-transform: capitalize;
}
.manage-group-users-row {
display: flex;
flex-direction: row;
@ -9806,13 +9848,13 @@ tbody {
border-bottom: 1px solid var(--slate5);
p {
width: 272px;
width: 262px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
span {
max-width: 150px;
max-width: 140px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
@ -9822,8 +9864,17 @@ tbody {
&:hover .apps-remove-btn {
display: flex;
}
.edit-role-btn{
margin-left: auto;
margin-right: 20px;
display: flex;
align-items: center;
padding-top: 5px;
}
}
.manage-group-app-table-body {
width: 602px !important;
@ -9890,7 +9941,7 @@ tbody {
border-bottom: 1px solid var(--slate5);
width: 612px;
height: 36px;
padding: 8px 12px;
padding: 8px 12px 8px 2px;
align-items: center;
@ -9900,6 +9951,15 @@ tbody {
font-weight: 500;
}
.edit-role-btn{
margin-left: auto;
margin-right: 50px;
display: flex;
width: 20px;
align-items: center;
padding-top: 5px;
}
}
.manage-groups-permission-apps,
@ -9925,12 +9985,12 @@ tbody {
.apps-variable-permission-wrap,
.apps-constant-permission-wrap {
gap: 10px;
height: 72px;
height: auto;
}
.apps-folder-permission-wrap,
.apps-variable-permission-wrap {
height: 44px;
height: auto;
border-bottom: 1px solid var(--slate5);
}
@ -10837,6 +10897,12 @@ tbody {
border-bottom-left-radius: 6px;
border-bottom-right-radius: 6px;
.user-detail{
display: flex;
flex-direction: column;
}
}
.user-filter-search {
@ -11514,7 +11580,10 @@ tbody {
}
.constant-table-card {
min-height: 370px;
min-height: 420px;
padding: 16px;
padding-top: 0px;
padding-bottom: 0px;
}
.card-footer {
@ -12821,7 +12890,7 @@ tbody {
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
max-width: 185px;
max-width: 210px;
}
.group-chip {
@ -13207,6 +13276,27 @@ tbody {
}
}
.modal-base {
.modal-footer {
padding: 1rem;
.tj-btn-left-icon {
svg {
width: 20px;
height: 20px;
path {
fill: var(--indigo1);
}
}
}
.tj-large-btn {
font-weight: 500;
font-size: 14px;
}
}
}
.component-spinner {
animation: l13 1s infinite linear;
position: absolute;
@ -13220,7 +13310,6 @@ tbody {
.widget-version-identifier {
position: absolute;
right: 0px;
top: 0px;
border-radius: 0px 8px 0px 8px;
height: 16px;
@ -13333,8 +13422,22 @@ div.ds-svg-container svg {
}
}
.tabs-component{
.tab-pane{
top: initial !important;
}
}
.mb-0 {
margin-bottom: 0px !important;
}
.tabs-component{
.tab-pane{
top: initial !important;
}
}
.user-not-found-svg{
display: flex;
align-items: center;
justify-content: center;
background: var(--slate3);
width: 36px;
height: 36px;
padding: 4px;
}

View file

@ -181,6 +181,10 @@
}
}
.tj-ms-usergroup{
width: auto;
}
.tj-ms-count {
border-radius: 2px;
display: flex;

View file

@ -1,7 +1,7 @@
@import "../../_styles/designtheme.scss";
.tj-base-btn {
box-sizing: border-box;
// box-sizing: border-box;
display: flex;
flex-direction: row;
justify-content: center;

View file

@ -87,6 +87,7 @@ function FolderList({
variant="tertiary"
onMouseEnter={handleMouseEnterInside}
onMouseLeave={handleMouseLeaveInside}
data-cy="groups-list-option-button"
></ButtonSolid>
</div>
<Overlay

View file

@ -11,8 +11,9 @@ export default ({ getter, options = [['', '']], optionchanged, isRenderedAsQuery
}
function removeKeyValuePair(index) {
options.splice(index, 1);
optionchanged(getter, options);
const newOptions = [...options];
newOptions.splice(index, 1);
optionchanged(getter, newOptions);
}
function keyValuePairValueChanged(value, keyIndex, index) {

View file

@ -1,78 +1,94 @@
input.form-control,
textarea,
.input-control {
gap: 16px !important;
background: var(--base) !important;
border: 1px solid var(--slate7) !important;
border-radius: 6px;
margin-bottom: 4px !important;
color: var(--slate12) !important;
transition: none;
height: 35px;
padding-left: 0.4375rem;
padding-right: 0.4375rem;
padding-top: 0.75rem;
padding-bottom: 0.75rem;
overflow-x: 'auto';
white-space: 'nowrap';
textarea,
.input-control {
gap: 16px !important;
background: var(--base) !important;
border: 1px solid var(--slate7) !important;
border-radius: 6px;
margin-bottom: 4px !important;
color: var(--slate12) !important;
transition: none;
height: 35px;
padding-left: 0.4375rem;
padding-right: 0.4375rem;
padding-top: 0.75rem;
padding-bottom: 0.75rem;
overflow-x: 'auto';
white-space: 'nowrap';
&:hover {
background: var(--slate1) !important;
border: 1px solid var(--slate8) !important;
-webkit-box-shadow: none !important;
box-shadow: none !important;
outline: none;
}
&:focus-visible {
background: var(--indigo2) !important;
border: 1px solid var(--indigo9) !important;
box-shadow: none !important;
}
&.input-error-border {
border-color: #DB4324 !important;
}
&:-webkit-autofill {
box-shadow: 0 0 0 1000px var(--base) inset !important;
-webkit-text-fill-color: var(--slate12) !important;
&:hover {
background: var(--slate1) !important;
border: 1px solid var(--slate8) !important;
-webkit-box-shadow: none !important;
box-shadow: none !important;
outline: none;
box-shadow: 0 0 0 1000px var(--slate1) inset !important;
-webkit-text-fill-color: var(--slate12) !important;
}
&:focus-visible {
background: var(--indigo2) !important;
border: 1px solid var(--indigo9) !important;
box-shadow: none !important;
}
&.input-error-border {
border-color: #DB4324 !important;
}
&:-webkit-autofill {
box-shadow: 0 0 0 1000px var(--base) inset !important;
box-shadow: 0 0 0 1000px var(--indigo2) inset !important;
-webkit-text-fill-color: var(--slate12) !important;
&:hover {
box-shadow: 0 0 0 1000px var(--slate1) inset !important;
-webkit-text-fill-color: var(--slate12) !important;
}
&:focus-visible {
box-shadow: 0 0 0 1000px var(--indigo2) inset !important;
-webkit-text-fill-color: var(--slate12) !important;
}
}
}
.empty-key-value {
border-radius: 6px;
padding: 10px;
text-align: center;
width: 625px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 10px;
color: #687076;
font-size: 12px;
font-weight: 400;
line-height: 20px;
border: 1px dashed #E6E8EB;
}
}
.trash {
height: 32px;
display: flex;
justify-content: 'center';
align-items: 'center';
}
.empty-key-value {
border-radius: 6px;
padding: 10px;
text-align: center;
width: 625px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 10px;
color: #687076;
font-size: 12px;
font-weight: 400;
line-height: 20px;
border: 1px dashed #E6E8EB;
}
.trash {
height: 32px;
display: flex;
justify-content: 'center';
align-items: 'center';
}
.empty-version {
border-radius: 6px;
padding: 10px;
text-align: center;
width: auto;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 10px;
color: #687076;
font-size: 12px;
font-weight: 400;
line-height: 20px;
border: 1px dashed #E6E8EB;
}

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