From 68e048761a569f55b75126cf6c7a4d09ded67fe2 Mon Sep 17 00:00:00 2001 From: Tejaaswini Narendra <67053685+tejaaswini-narendra@users.noreply.github.com> Date: Wed, 12 Aug 2020 17:11:56 +0530 Subject: [PATCH] fix: restructure code and show content based on permission (#261) * fix: restructure code and show content based on permission - Restructure Invite user forms into separate App and Org forms - Show Modal content based on permissions. * update: Permission handling. * fix: Modify permission handling * fix: check manage permission for manage users btn and show copy to clipboard always. * Dummy commit to run the tests * Fix: Test cases * Another dummy commit to run the tests Co-authored-by: Trisha Anand --- app/client/README.md | 2 +- .../OrganisationTests/CreateOrgTests_spec.js | 2 +- app/client/cypress/support/commands.js | 4 +- app/client/src/api/OrgApi.ts | 6 +- .../appsmith/CopyToClipBoard.tsx | 77 ++++++ .../AppViewer/viewer/AppViewerHeader.tsx | 53 ++-- app/client/src/pages/Applications/index.tsx | 95 ++++--- .../pages/Applications/permissionHelpers.tsx | 2 + app/client/src/pages/Editor/EditorHeader.tsx | 63 ++--- .../CustomizedDropdown/OrgDropdownData.tsx | 5 +- .../pages/organization/AppInviteUsersForm.tsx | 146 ++++++++++ .../pages/organization/InviteUsersForm.tsx | 255 ------------------ ...UsersFromv2.tsx => OrgInviteUsersForm.tsx} | 54 ++-- .../pages/organization/ShareWithPublic.tsx | 140 ---------- app/client/src/pages/organization/index.tsx | 7 - app/client/src/pages/organization/invite.tsx | 14 - .../src/pages/organization/settings.tsx | 4 +- .../src/reducers/uiReducers/orgReducer.ts | 7 - app/client/src/sagas/OrgSagas.ts | 9 +- app/client/src/sagas/userSagas.tsx | 6 +- 20 files changed, 369 insertions(+), 582 deletions(-) create mode 100644 app/client/src/components/designSystems/appsmith/CopyToClipBoard.tsx create mode 100644 app/client/src/pages/organization/AppInviteUsersForm.tsx delete mode 100644 app/client/src/pages/organization/InviteUsersForm.tsx rename app/client/src/pages/organization/{InviteUsersFromv2.tsx => OrgInviteUsersForm.tsx} (85%) delete mode 100644 app/client/src/pages/organization/ShareWithPublic.tsx delete mode 100644 app/client/src/pages/organization/invite.tsx diff --git a/app/client/README.md b/app/client/README.md index b795585608..aa4dde788b 100755 --- a/app/client/README.md +++ b/app/client/README.md @@ -7,7 +7,7 @@ - `cd internal-tools-client` Change directory to the project directory - `nvm install` Install the version of `node` and `npm` required by the project using `nvm` - `yarn` Install packages and run setup scripts -- `yarn start` Deploy locally +- `yarn start` Start the client locally using this > For more details on how to run this locally, please visit: [Notion Doc](https://www.notion.so/appsmith/How-to-run-the-code-e031545454874419b9f72cd51feb90ff) diff --git a/app/client/cypress/integration/Smoke_TestSuite/OrganisationTests/CreateOrgTests_spec.js b/app/client/cypress/integration/Smoke_TestSuite/OrganisationTests/CreateOrgTests_spec.js index 18ae9ec00b..8a6939c17f 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/OrganisationTests/CreateOrgTests_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/OrganisationTests/CreateOrgTests_spec.js @@ -58,7 +58,7 @@ describe("Create new org and share with a user", function() { cy.get(homePage.searchInput).type(appid); cy.wait(2000); cy.contains(orgid); - cy.xpath(homePage.ShareBtn).should("not.be.visible"); + cy.xpath(homePage.ShareBtn).should("be.visible"); cy.get(homePage.appEditIcon) .first() .click({ force: true }); diff --git a/app/client/cypress/support/commands.js b/app/client/cypress/support/commands.js index 3c795888f1..c51b7f3540 100644 --- a/app/client/cypress/support/commands.js +++ b/app/client/cypress/support/commands.js @@ -1371,7 +1371,9 @@ Cypress.Commands.add("startServerAndRoutes", () => { cy.route("POST", "/api/v1/organizations").as("createOrg"); cy.route("POST", "/api/v1/users/invite").as("postInvite"); - cy.route("GET", "/api/v1/organizations/roles").as("getRoles"); + cy.route("GET", "/api/v1/organizations/roles?organizationId=*").as( + "getRoles", + ); cy.route("GET", "/api/v1/users/me").as("getUser"); cy.route("POST", "/api/v1/pages").as("createPage"); }); diff --git a/app/client/src/api/OrgApi.ts b/app/client/src/api/OrgApi.ts index 8d915ebeb0..8c3f4791c4 100644 --- a/app/client/src/api/OrgApi.ts +++ b/app/client/src/api/OrgApi.ts @@ -78,8 +78,10 @@ class OrgApi extends Api { ): AxiosPromise { return Api.get(OrgApi.orgsURL + "/" + request.orgId + "/members"); } - static fetchAllRoles(): AxiosPromise { - return Api.get(OrgApi.orgsURL + "/roles"); + static fetchAllRoles( + request: FetchAllRolesRequest, + ): AxiosPromise { + return Api.get(OrgApi.orgsURL + `/roles?organizationId=${request.orgId}`); } static changeOrgUserRole( request: ChangeUserRoleRequest, diff --git a/app/client/src/components/designSystems/appsmith/CopyToClipBoard.tsx b/app/client/src/components/designSystems/appsmith/CopyToClipBoard.tsx new file mode 100644 index 0000000000..22bea611a6 --- /dev/null +++ b/app/client/src/components/designSystems/appsmith/CopyToClipBoard.tsx @@ -0,0 +1,77 @@ +import React, { createRef, useState } from "react"; +import styled from "styled-components"; +import copy from "copy-to-clipboard"; +import { BaseButton } from "components/designSystems/blueprint/ButtonComponent"; + +const Wrapper = styled.div` + display: flex; + flex-direction: row; +`; +const StyledInput = styled.input` + flex: 1; + border: 1px solid #d3dee3; + border-right: none; + padding: 6px 12px; + font-size: 14px; + color: #768896; + border-radius: 4px 0 0 4px; + width: 90%; + overflow: hidden; +`; + +const SelectButton = styled(BaseButton)` + &&&& { + max-width: 70px; + margin: 0 0px; + min-height: 32px; + border-radius: 0px 4px 4px 0px; + font-weight: bold; + background-color: #f6f7f8; + font-size: 14px; + &.bp3-button { + padding: 0px 0px; + } + } +`; + +const CopyToClipboard = (props: any) => { + const { copyText } = props; + const copyURLInput = createRef(); + const [isCopied, setIsCopied] = useState(false); + + const copyToClipboard = (url: string) => { + copy(url); + setIsCopied(true); + setTimeout(() => { + setIsCopied(false); + }, 3000); + }; + + const selectText = () => { + if (copyURLInput.current) { + copyURLInput.current.setSelectionRange(0, copyText.length); + } + }; + return ( + + { + selectText(); + }} + value={copyText} + /> + { + copyToClipboard(copyText); + }} + /> + + ); +}; + +export default CopyToClipboard; diff --git a/app/client/src/pages/AppViewer/viewer/AppViewerHeader.tsx b/app/client/src/pages/AppViewer/viewer/AppViewerHeader.tsx index 963eff4c04..0030dfd47e 100644 --- a/app/client/src/pages/AppViewer/viewer/AppViewerHeader.tsx +++ b/app/client/src/pages/AppViewer/viewer/AppViewerHeader.tsx @@ -22,7 +22,7 @@ import { AppState } from "reducers"; import { getEditorURL } from "selectors/appViewSelectors"; import { getPageList } from "selectors/editorSelectors"; import { FormDialogComponent } from "components/editorComponents/form/FormDialogComponent"; -import InviteUsersFormv2 from "pages/organization/InviteUsersFromv2"; +import AppInviteUsersForm from "pages/organization/AppInviteUsersForm"; import { getCurrentOrgId } from "selectors/organizationSelectors"; import { HeaderIcons } from "icons/HeaderIcons"; import { Colors } from "constants/Colors"; @@ -119,10 +119,6 @@ export const AppViewerHeader = (props: AppViewerHeaderProps) => { const userPermissions = currentApplicationDetails?.userPermissions ?? []; const permissionRequired = PERMISSION_TYPE.MANAGE_APPLICATION; const canEdit = isPermitted(userPermissions, permissionRequired); - const canShare = isPermitted( - userPermissions, - PERMISSION_TYPE.MANAGE_APPLICATION, - ); return ( 1}> @@ -142,30 +138,29 @@ export const AppViewerHeader = (props: AppViewerHeaderProps) => { {currentApplicationDetails && ( <> - {canShare && ( - - } - /> - } - Form={InviteUsersFormv2} - orgId={currentOrgId} - applicationId={currentApplicationDetails.id} - title={currentApplicationDetails.name} - /> - )} + + } + /> + } + Form={AppInviteUsersForm} + orgId={currentOrgId} + applicationId={currentApplicationDetails.id} + title={currentApplicationDetails.name} + /> + {props.url && canEdit && ( - {!isPermitted( - organization.userPermissions, - PERMISSION_TYPE.MANAGE_ORGANIZATION, - ) ? ( - - {MenuIcons.ORG_ICON({ - color: IntentColors["secondary"], - width: 16, - height: 16, - })} - {organization.name} - - ) : ( - - {this.props.currentUser && ( - - )} + + {!isPermitted( + organization.userPermissions, + PERMISSION_TYPE.MANAGE_ORGANIZATION, + ) ? ( + + {MenuIcons.ORG_ICON({ + color: IntentColors["secondary"], + width: 16, + height: 16, + })} + {organization.name} + + ) : ( + <> + {this.props.currentUser && ( + + )} - - this.setState({ - selectedOrgId: "", - }) - } - isOpen={this.state.selectedOrgId === organization.id} - setMaxWidth - > -
-
-
-
+ + this.setState({ + selectedOrgId: "", + }) + } + isOpen={this.state.selectedOrgId === organization.id} + setMaxWidth + > +
+ +
+
+ + )} + {isPermitted( + organization.userPermissions, + PERMISSION_TYPE.INVITE_USER_TO_ORGANIZATION, + ) && ( } canOutsideClickClose={true} - Form={InviteUsersFormv2} + Form={OrgInviteUsersForm} orgId={organization.id} title={`Invite Users to ${organization.name}`} /> -
- )} + )} +
{ diff --git a/app/client/src/pages/Editor/EditorHeader.tsx b/app/client/src/pages/Editor/EditorHeader.tsx index 0e44ccbd83..e8c77a8adf 100644 --- a/app/client/src/pages/Editor/EditorHeader.tsx +++ b/app/client/src/pages/Editor/EditorHeader.tsx @@ -8,11 +8,7 @@ import { APPLICATIONS_URL, getApplicationViewerPageURL, } from "constants/routes"; -import { - PERMISSION_TYPE, - isPermitted, -} from "pages/Applications/permissionHelpers"; -import InviteUsersFormv2 from "pages/organization/InviteUsersFromv2"; +import AppInviteUsersForm from "pages/organization/AppInviteUsersForm"; import Button from "components/editorComponents/Button"; import StyledHeader from "components/designSystems/appsmith/StyledHeader"; import AnalyticsUtil from "utils/AnalyticsUtil"; @@ -183,9 +179,6 @@ export const EditorHeader = (props: EditorHeaderProps) => { ); } } - const applicationPermissions = currentApplication?.userPermissions - ? currentApplication.userPermissions - : []; return ( @@ -218,35 +211,31 @@ export const EditorHeader = (props: EditorHeaderProps) => { } /> - {isPermitted( - applicationPermissions, - PERMISSION_TYPE.MANAGE_APPLICATION, - ) && ( - - } - /> - } - Form={InviteUsersFormv2} - orgId={orgId} - applicationId={applicationId} - title={ - currentApplication ? currentApplication.name : "Share Application" - } - /> - )} + + } + /> + } + canOutsideClickClose={true} + Form={AppInviteUsersForm} + orgId={orgId} + applicationId={applicationId} + title={ + currentApplication ? currentApplication.name : "Share Application" + } + /> { + const { + isFetchingApplication, + isChangingViewAccess, + currentApplicationDetails, + changeAppViewAccess, + applicationId, + fetchCurrentOrg, + currentOrg, + currentUser, + } = props; + + const userOrgPermissions = currentOrg?.userPermissions ?? []; + const userAppPermissions = currentApplicationDetails?.userPermissions ?? []; + const canInviteToOrg = isPermitted( + userOrgPermissions, + PERMISSION_TYPE.INVITE_USER_TO_ORGANIZATION, + ); + const canShareWithPublic = isPermitted( + userAppPermissions, + PERMISSION_TYPE.MAKE_PUBLIC_APPLICATION, + ); + + const getViewApplicationURL = () => { + const defaultPageId = getDefaultPageId(currentApplicationDetails.pages); + const appViewEndPoint = getApplicationViewerPageURL( + applicationId, + defaultPageId, + ); + return window.location.origin.toString() + appViewEndPoint; + }; + + useEffect(() => { + if (currentUser.name !== "anonymousUser") { + fetchCurrentOrg(props.orgId); + } + }, [props.orgId, fetchCurrentOrg, currentUser.name]); + + return ( + <> + {canShareWithPublic && ( + <> + + Make the application public + + {(isChangingViewAccess || isFetchingApplication) && ( + + )} + {currentApplicationDetails && ( + { + changeAppViewAccess( + applicationId, + !currentApplicationDetails.isPublic, + ); + }} + disabled={isChangingViewAccess || isFetchingApplication} + checked={currentApplicationDetails.isPublic} + large + /> + )} + + + + )} + Get Shareable link for this for this application + + + {canInviteToOrg && ( + + )} + + ); +}; + +export default connect( + (state: AppState) => { + return { + currentOrg: getCurrentOrg(state), + currentUser: getCurrentUser(state), + currentApplicationDetails: state.ui.applications.currentApplication, + isFetchingApplication: state.ui.applications.isFetchingApplication, + isChangingViewAccess: state.ui.applications.isChangingViewAccess, + }; + }, + (dispatch: any) => ({ + changeAppViewAccess: (applicationId: string, publicAccess: boolean) => + dispatch({ + type: ReduxActionTypes.CHANGE_APPVIEW_ACCESS_INIT, + payload: { + applicationId, + publicAccess, + }, + }), + fetchCurrentOrg: (orgId: string) => + dispatch({ + type: ReduxActionTypes.FETCH_CURRENT_ORG, + payload: { + orgId, + }, + }), + }), +)(AppInviteUsersForm); diff --git a/app/client/src/pages/organization/InviteUsersForm.tsx b/app/client/src/pages/organization/InviteUsersForm.tsx deleted file mode 100644 index e0ae866b69..0000000000 --- a/app/client/src/pages/organization/InviteUsersForm.tsx +++ /dev/null @@ -1,255 +0,0 @@ -import React, { useEffect } from "react"; -import { connect } from "react-redux"; -import styled from "styled-components"; -import { useHistory } from "react-router-dom"; -import { - FieldArray, - reduxForm, - InjectedFormProps, - WrappedFieldArrayProps, -} from "redux-form"; -import FormMessage from "components/editorComponents/form/FormMessage"; -import { INVITE_USERS_TO_ORG_FORM } from "constants/forms"; -import { - INVITE_USERS_VALIDATION_EMAIL_LIST, - INVITE_USERS_VALIDATION_ROLE_EMPTY, - INVITE_USERS_EMAIL_LIST_LABEL, - INVITE_USERS_EMAIL_LIST_PLACEHOLDER, - INVITE_USERS_ROLE_SELECT_LABEL, - INVITE_USERS_ROLE_SELECT_PLACEHOLDER, - INVITE_USERS_ADD_EMAIL_LIST_FIELD, - INVITE_USERS_SUBMIT_BUTTON_TEXT, - INVITE_USERS_SUBMIT_ERROR, - INVITE_USERS_SUBMIT_SUCCESS, - INVITE_USERS_VALIDATION_EMAILS_EMPTY, -} from "constants/messages"; -import { - InviteUsersToOrgFormValues, - InviteUsersToOrgByRoleValues, - inviteUsersToOrgSubmitHandler, -} from "./helpers"; -import { generateReactKey } from "utils/generators"; -import TagListField from "components/editorComponents/form/fields/TagListField"; -import { FormIcons } from "icons/FormIcons"; -import FormFooter from "components/editorComponents/form/FormFooter"; -import FormActionButton from "components/editorComponents/form/FormActionButton"; -import FormGroup from "components/editorComponents/form/FormGroup"; -import SelectField from "components/editorComponents/form/fields/SelectField"; -import { ReduxActionTypes } from "constants/ReduxActionConstants"; -import { AppState } from "reducers"; -import { getRoles, getDefaultRole } from "selectors/organizationSelectors"; -import { OrgRole } from "constants/orgConstants"; -import { isEmail } from "utils/formhelpers"; - -const validate = (values: InviteUsersToOrgFormValues) => { - const errors: any = { usersByRole: [] }; - if (values.usersByRole && values.usersByRole.length) { - values.usersByRole.forEach((role, index) => { - errors.usersByRole[index] = { id: "", users: "", role: "" }; - // If we have users entered for a role. - if (role.users && role.users.length > 0) { - // Split the users CSV string to an array. - const _users = role.users.split(",").filter(Boolean); - // Check if each entry is an email - _users.forEach(user => { - if (!isEmail(user)) { - if (errors.usersByRole[index].users) - errors.usersByRole[index].users += `${user}, `; - else errors.usersByRole[index].users = `${user}, `; - } - }); - if ( - errors.usersByRole[index].users && - errors.usersByRole[index].users.length > 0 - ) { - errors.usersByRole[ - index - ].users = `${INVITE_USERS_VALIDATION_EMAIL_LIST} ${errors.usersByRole[ - index - ].users.slice(0, -2)}`; - } - // Check if role has been specified - if (role.role === undefined || role.role?.trim().length === 0) { - errors.usersByRole[index].role = INVITE_USERS_VALIDATION_ROLE_EMPTY; - } - } else { - errors.usersByRole[index].users = INVITE_USERS_VALIDATION_EMAILS_EMPTY; - } - }); - } - return errors; -}; - -const StyledForm = styled.div` - width: 100%; - background: white; - padding: ${props => props.theme.spaces[11]}px; -`; - -const StyledInviteFieldGroup = styled.div` - && { - display: flex; - flex-direction: row; - flex-wrap: none; - justify-content: space-between; - align-items: flex-start; - & > div:first-of-type { - } - & > div { - min-width: 150px; - margin: 0em 1em 1em 0em; - } - & > div:last-of-type { - min-width: 0; - display: flex; - align-self: center; - } - } -`; - -const renderInviteUsersByRoleForm = ( - renderer: WrappedFieldArrayProps & { - roles?: OrgRole[]; - role?: OrgRole; - }, -) => { - const { fields, roles, role } = renderer; - return ( - - {fields.map((field, index) => { - return ( - - - - - {roles && ( - - - - )} - fields.remove(index)} - /> - - ); - })} - - fields.push({ - id: generateReactKey(), - role: !!role ? role.id : undefined, - }) - } - text={INVITE_USERS_ADD_EMAIL_LIST_FIELD} - icon="plus" - /> - - ); -}; - -type InviteUsersFormProps = InjectedFormProps< - InviteUsersToOrgFormValues, - { - fetchRoles: () => void; - roles?: OrgRole[]; - defaultRole?: OrgRole; - } -> & { - fetchRoles: () => void; - roles?: OrgRole[]; - defaultRole?: OrgRole; -}; - -export const InviteUsersForm = (props: InviteUsersFormProps) => { - const { - handleSubmit, - submitting, - submitFailed, - submitSucceeded, - error, - fetchRoles, - roles, - initialize, - defaultRole, - anyTouched, - } = props; - const history = useHistory(); - useEffect(() => { - if (!roles) { - fetchRoles(); - } else { - initialize({ - usersByRole: [ - { - id: generateReactKey(), - role: !!defaultRole ? defaultRole.id : undefined, - }, - ], - }); - } - }, [fetchRoles, roles, defaultRole, initialize]); - - return ( - - {submitSucceeded && ( - - )} - {submitFailed && error && ( - - )} - - - history.goBack()} - submitOnEnter={false} - submitText={INVITE_USERS_SUBMIT_BUTTON_TEXT} - > - - ); -}; - -export default connect( - (state: AppState) => { - return { - roles: getRoles(state), - defaultRole: getDefaultRole(state), - }; - }, - (dispatch: any) => ({ - fetchRoles: () => dispatch({ type: ReduxActionTypes.FETCH_ORG_ROLES_INIT }), - }), -)( - reduxForm< - InviteUsersToOrgFormValues, - { fetchRoles: () => void; roles?: OrgRole[]; defaultRole?: OrgRole } - >({ - form: INVITE_USERS_TO_ORG_FORM, - validate, - })(InviteUsersForm), -); diff --git a/app/client/src/pages/organization/InviteUsersFromv2.tsx b/app/client/src/pages/organization/OrgInviteUsersForm.tsx similarity index 85% rename from app/client/src/pages/organization/InviteUsersFromv2.tsx rename to app/client/src/pages/organization/OrgInviteUsersForm.tsx index 21e8a79731..115d8899de 100644 --- a/app/client/src/pages/organization/InviteUsersFromv2.tsx +++ b/app/client/src/pages/organization/OrgInviteUsersForm.tsx @@ -1,14 +1,14 @@ -import React, { useEffect, useState, createRef } from "react"; +import React, { useEffect } from "react"; import styled from "styled-components"; import { useLocation } from "react-router-dom"; import TagListField from "components/editorComponents/form/fields/TagListField"; import { reduxForm, SubmissionError } from "redux-form"; import SelectField from "components/editorComponents/form/fields/SelectField"; +import Divider from "components/editorComponents/Divider"; import Button from "components/editorComponents/Button"; import { connect } from "react-redux"; import { AppState } from "reducers"; import { - getDefaultRole, getRolesForField, getAllUsers, getCurrentOrg, @@ -27,8 +27,10 @@ import { import history from "utils/history"; import { Colors } from "constants/Colors"; import { isEmail } from "utils/formhelpers"; -import ShareWithPublic from "./ShareWithPublic"; -import Divider from "components/editorComponents/Divider"; +import { + isPermitted, + PERMISSION_TYPE, +} from "../Applications/permissionHelpers"; const OrgInviteTitle = styled.div` font-weight: bold; @@ -68,6 +70,7 @@ const StyledForm = styled.form` margin-top: 20px; } `; + const StyledInviteFieldGroup = styled.div` display: flex; align-items: center; @@ -140,7 +143,7 @@ const validate = (values: any) => { return errors; }; -const InviteUsersForm = (props: any) => { +const OrgInviteUsersForm = (props: any) => { const { handleSubmit, allUsers, @@ -152,19 +155,20 @@ const InviteUsersForm = (props: any) => { fetchUser, fetchAllRoles, valid, - onCancel, - isFetchingApplication, - isChangingViewAccess, - currentApplicationDetails, - changeAppViewAccess, - applicationId, fetchCurrentOrg, currentOrg, + isApplicationInvite, } = props; const currentPath = useLocation().pathname; const pathRegex = /(?:\/org\/)\w+(?:\/settings)/; + const userOrgPermissions = currentOrg?.userPermissions ?? []; + const canManage = isPermitted( + userOrgPermissions, + PERMISSION_TYPE.MANAGE_ORGANIZATION, + ); + useEffect(() => { fetchUser(props.orgId); fetchAllRoles(props.orgId); @@ -186,20 +190,12 @@ const InviteUsersForm = (props: any) => { return ( <> - {applicationId && ( + {isApplicationInvite && ( <> - Invite Users to {currentOrg?.name} )} - { validateFormValues(values); @@ -251,7 +247,7 @@ const InviteUsersForm = (props: any) => { ); })} - {!pathRegex.test(currentPath) && ( + {!pathRegex.test(currentPath) && canManage && (