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 <trisha@appsmith.com>
This commit is contained in:
Tejaaswini Narendra 2020-08-12 17:11:56 +05:30 committed by GitHub
parent 368ed79f4b
commit 68e048761a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 369 additions and 582 deletions

View File

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

View File

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

View File

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

View File

@ -78,8 +78,10 @@ class OrgApi extends Api {
): AxiosPromise<FetchAllUsersResponse> {
return Api.get(OrgApi.orgsURL + "/" + request.orgId + "/members");
}
static fetchAllRoles(): AxiosPromise<FetchAllRolesResponse> {
return Api.get(OrgApi.orgsURL + "/roles");
static fetchAllRoles(
request: FetchAllRolesRequest,
): AxiosPromise<FetchAllRolesResponse> {
return Api.get(OrgApi.orgsURL + `/roles?organizationId=${request.orgId}`);
}
static changeOrgUserRole(
request: ChangeUserRoleRequest,

View File

@ -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<HTMLInputElement>();
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 (
<Wrapper>
<StyledInput
type="text"
ref={copyURLInput}
readOnly
onClick={() => {
selectText();
}}
value={copyText}
/>
<SelectButton
text={isCopied ? "Copied" : "Copy"}
accent="secondary"
onClick={() => {
copyToClipboard(copyText);
}}
/>
</Wrapper>
);
};
export default CopyToClipboard;

View File

@ -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 (
<HeaderWrapper hasPages={pages.length > 1}>
@ -142,30 +138,29 @@ export const AppViewerHeader = (props: AppViewerHeaderProps) => {
<HeaderSection justify={"flex-end"}>
{currentApplicationDetails && (
<>
{canShare && (
<FormDialogComponent
trigger={
<ShareButton
text="Share"
intent="none"
outline
size="small"
className="t--application-share-btn"
icon={
<HeaderIcons.SHARE
color={Colors.WHITE}
width={13}
height={13}
/>
}
/>
}
Form={InviteUsersFormv2}
orgId={currentOrgId}
applicationId={currentApplicationDetails.id}
title={currentApplicationDetails.name}
/>
)}
<FormDialogComponent
trigger={
<ShareButton
text="Share"
intent="none"
outline
size="small"
className="t--application-share-btn"
icon={
<HeaderIcons.SHARE
color={Colors.WHITE}
width={13}
height={13}
/>
}
/>
}
Form={AppInviteUsersForm}
orgId={currentOrgId}
applicationId={currentApplicationDetails.id}
title={currentApplicationDetails.name}
/>
{props.url && canEdit && (
<BackToEditorButton
className="t--back-to-editor"

View File

@ -21,7 +21,7 @@ import SubHeader from "pages/common/SubHeader";
import PageSectionDivider from "pages/common/PageSectionDivider";
import ApplicationCard from "./ApplicationCard";
import CreateApplicationForm from "./CreateApplicationForm";
import InviteUsersFormv2 from "pages/organization/InviteUsersFromv2";
import OrgInviteUsersForm from "pages/organization/OrgInviteUsersForm";
import { PERMISSION_TYPE, isPermitted } from "./permissionHelpers";
import { MenuIcons } from "icons/MenuIcons";
import { DELETING_APPLICATION } from "constants/messages";
@ -143,7 +143,7 @@ class Applications extends Component<
}
public render() {
const Form: any = InviteUsersFormv2;
const Form: any = OrgInviteUsersForm;
const DropdownProps = (
user: User,
orgName: string,
@ -230,46 +230,53 @@ class Applications extends Component<
return (
<OrgSection className="t--org-section" key={index}>
{!isPermitted(
organization.userPermissions,
PERMISSION_TYPE.MANAGE_ORGANIZATION,
) ? (
<OrgName>
{MenuIcons.ORG_ICON({
color: IntentColors["secondary"],
width: 16,
height: 16,
})}
{organization.name}
</OrgName>
) : (
<OrgDropDown>
{this.props.currentUser && (
<CustomizedDropdown
{...DropdownProps(
this.props.currentUser,
organization.name,
organization.id,
)}
/>
)}
<OrgDropDown>
{!isPermitted(
organization.userPermissions,
PERMISSION_TYPE.MANAGE_ORGANIZATION,
) ? (
<OrgName>
{MenuIcons.ORG_ICON({
color: IntentColors["secondary"],
width: 16,
height: 16,
})}
{organization.name}
</OrgName>
) : (
<>
{this.props.currentUser && (
<CustomizedDropdown
{...DropdownProps(
this.props.currentUser,
organization.name,
organization.id,
)}
/>
)}
<StyledDialog
canOutsideClickClose={false}
canEscapeKeyClose={false}
title={`Invite Users to ${organization.name}`}
onClose={() =>
this.setState({
selectedOrgId: "",
})
}
isOpen={this.state.selectedOrgId === organization.id}
setMaxWidth
>
<div className={Classes.DIALOG_BODY}>
<Form orgId={organization.id} />
</div>
</StyledDialog>
<StyledDialog
canOutsideClickClose={false}
canEscapeKeyClose={false}
title={`Invite Users to ${organization.name}`}
onClose={() =>
this.setState({
selectedOrgId: "",
})
}
isOpen={this.state.selectedOrgId === organization.id}
setMaxWidth
>
<div className={Classes.DIALOG_BODY}>
<Form orgId={organization.id} />
</div>
</StyledDialog>
</>
)}
{isPermitted(
organization.userPermissions,
PERMISSION_TYPE.INVITE_USER_TO_ORGANIZATION,
) && (
<FormDialogComponent
trigger={
<Button
@ -280,12 +287,12 @@ class Applications extends Component<
/>
}
canOutsideClickClose={true}
Form={InviteUsersFormv2}
Form={OrgInviteUsersForm}
orgId={organization.id}
title={`Invite Users to ${organization.name}`}
/>
</OrgDropDown>
)}
)}
</OrgDropDown>
<ApplicationCardsWrapper key={organization.id}>
<FormDialogComponent
permissions={organization.userPermissions}

View File

@ -4,6 +4,8 @@ export enum PERMISSION_TYPE {
MANAGE_APPLICATION = "manage:applications",
READ_APPLICATION = "read:applications",
READ_ORGANIZATION = "read:organizations",
INVITE_USER_TO_ORGANIZATION = "inviteUsers:organization",
MAKE_PUBLIC_APPLICATION = "makePublic:applications",
}
export const isPermitted = (permissions: string[], type: string) => {

View File

@ -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 (
<HeaderWrapper>
@ -218,35 +211,31 @@ export const EditorHeader = (props: EditorHeaderProps) => {
<HeaderIcons.FEEDBACK color={Colors.WHITE} width={13} height={13} />
}
/>
{isPermitted(
applicationPermissions,
PERMISSION_TYPE.MANAGE_APPLICATION,
) && (
<FormDialogComponent
trigger={
<ShareButton
text="Share"
intent="none"
outline
size="small"
className="t--application-share-btn"
icon={
<HeaderIcons.SHARE
color={Colors.WHITE}
width={13}
height={13}
/>
}
/>
}
Form={InviteUsersFormv2}
orgId={orgId}
applicationId={applicationId}
title={
currentApplication ? currentApplication.name : "Share Application"
}
/>
)}
<FormDialogComponent
trigger={
<ShareButton
text="Share"
intent="none"
outline
size="small"
className="t--application-share-btn"
icon={
<HeaderIcons.SHARE
color={Colors.WHITE}
width={13}
height={13}
/>
}
/>
}
canOutsideClickClose={true}
Form={AppInviteUsersForm}
orgId={orgId}
applicationId={applicationId}
title={
currentApplication ? currentApplication.name : "Share Application"
}
/>
<DeploySection>
<DeployButton
onClick={handlePublish}

View File

@ -3,13 +3,10 @@ import Badge from "./Badge";
import { Directions } from "utils/helpers";
import { ReduxActionTypes } from "constants/ReduxActionConstants";
import { getOnSelectAction, DropdownOnSelectActions } from "./dropdownHelpers";
import DropdownComponent, { CustomizedDropdownProps } from "./index";
import { CustomizedDropdownProps } from "./index";
import { Org } from "constants/orgConstants";
import { User } from "constants/userConstants";
import InviteUsersFormv2 from "pages/organization/InviteUsersFromv2";
import _ from "lodash";
import FormDialogComponent from "components/editorComponents/form/FormDialogComponent";
import { Button } from "@blueprintjs/core";
const switchdropdown = (
orgs: Org[],

View File

@ -0,0 +1,146 @@
import React, { useEffect } from "react";
import styled from "styled-components";
import { connect } from "react-redux";
import { AppState } from "reducers";
import { getCurrentOrg } from "selectors/organizationSelectors";
import { ReduxActionTypes } from "constants/ReduxActionConstants";
import CopyToClipBoard from "components/designSystems/appsmith/CopyToClipBoard";
import {
isPermitted,
PERMISSION_TYPE,
} from "../Applications/permissionHelpers";
import { getDefaultPageId } from "sagas/SagaUtils";
import { getApplicationViewerPageURL } from "constants/routes";
import OrgInviteUsersForm from "./OrgInviteUsersForm";
import { StyledSwitch } from "components/propertyControls/StyledControls";
import Spinner from "components/editorComponents/Spinner";
import { getCurrentUser } from "selectors/usersSelectors";
const Title = styled.div`
font-weight: bold;
padding: 10px 0px;
`;
const ShareWithPublicOption = styled.div`
{
display: flex;
padding: 10px 0px;
justify-content: space-between;
}
`;
const ShareToggle = styled.div`
{
&&& label {
margin-bottom: 0px;
}
&&& div {
margin-right: 5px;
}
display: flex;
}
`;
const AppInviteUsersForm = (props: any) => {
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 && (
<>
<ShareWithPublicOption>
Make the application public
<ShareToggle>
{(isChangingViewAccess || isFetchingApplication) && (
<Spinner size={20} />
)}
{currentApplicationDetails && (
<StyledSwitch
onChange={() => {
changeAppViewAccess(
applicationId,
!currentApplicationDetails.isPublic,
);
}}
disabled={isChangingViewAccess || isFetchingApplication}
checked={currentApplicationDetails.isPublic}
large
/>
)}
</ShareToggle>
</ShareWithPublicOption>
</>
)}
<Title>Get Shareable link for this for this application </Title>
<CopyToClipBoard copyText={getViewApplicationURL()} />
{canInviteToOrg && (
<OrgInviteUsersForm orgId={props.orgId} isApplicationInvite={true} />
)}
</>
);
};
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);

View File

@ -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<InviteUsersToOrgByRoleValues> & {
roles?: OrgRole[];
role?: OrgRole;
},
) => {
const { fields, roles, role } = renderer;
return (
<React.Fragment>
{fields.map((field, index) => {
return (
<StyledInviteFieldGroup key={`${field}.id`}>
<FormGroup fill label={INVITE_USERS_EMAIL_LIST_LABEL}>
<TagListField
name={`${field}.users`}
placeholder={INVITE_USERS_EMAIL_LIST_PLACEHOLDER}
type="email"
label="Emails"
intent="success"
/>
</FormGroup>
{roles && (
<FormGroup label={INVITE_USERS_ROLE_SELECT_LABEL}>
<SelectField
name={`${field}.role`}
placeholder={INVITE_USERS_ROLE_SELECT_PLACEHOLDER}
options={roles}
size="large"
/>
</FormGroup>
)}
<FormIcons.DELETE_ICON
width={32}
height={32}
style={{
cursor: "pointer",
}}
onClick={() => fields.remove(index)}
/>
</StyledInviteFieldGroup>
);
})}
<FormActionButton
onClick={() =>
fields.push({
id: generateReactKey(),
role: !!role ? role.id : undefined,
})
}
text={INVITE_USERS_ADD_EMAIL_LIST_FIELD}
icon="plus"
/>
</React.Fragment>
);
};
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 (
<StyledForm>
{submitSucceeded && (
<FormMessage intent="primary" message={INVITE_USERS_SUBMIT_SUCCESS} />
)}
{submitFailed && error && (
<FormMessage
intent="danger"
message={`${INVITE_USERS_SUBMIT_ERROR}: ${error}`}
/>
)}
<FieldArray
name="usersByRole"
component={renderInviteUsersByRoleForm}
props={{ roles: roles }}
/>
<FormFooter
divider
onSubmit={handleSubmit(inviteUsersToOrgSubmitHandler)}
submitting={submitting && !(submitFailed && !anyTouched)}
onCancel={() => history.goBack()}
submitOnEnter={false}
submitText={INVITE_USERS_SUBMIT_BUTTON_TEXT}
></FormFooter>
</StyledForm>
);
};
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),
);

View File

@ -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 && (
<>
<ShareWithPublic
changeAppViewAccess={changeAppViewAccess}
isFetchingApplication={isFetchingApplication}
currentApplicationDetails={currentApplicationDetails}
applicationId={applicationId}
isChangingViewAccess={isChangingViewAccess}
/>
<Divider />
<OrgInviteTitle>Invite Users to {currentOrg?.name} </OrgInviteTitle>
</>
)}
<StyledForm
onSubmit={handleSubmit((values: any, dispatch: any) => {
validateFormValues(values);
@ -251,7 +247,7 @@ const InviteUsersForm = (props: any) => {
);
})}
</UserList>
{!pathRegex.test(currentPath) && (
{!pathRegex.test(currentPath) && canManage && (
<Button
className="manageUsers"
text="Manage Users"
@ -271,12 +267,8 @@ export default connect(
(state: AppState) => {
return {
roles: getRolesForField(state),
defaultRole: getDefaultRole(state),
allUsers: getAllUsers(state),
currentOrg: getCurrentOrg(state),
currentApplicationDetails: state.ui.applications.currentApplication,
isFetchingApplication: state.ui.applications.isFetchingApplication,
isChangingViewAccess: state.ui.applications.isChangingViewAccess,
};
},
(dispatch: any) => ({
@ -301,14 +293,6 @@ export default connect(
orgId,
},
}),
changeAppViewAccess: (applicationId: string, publicAccess: boolean) =>
dispatch({
type: ReduxActionTypes.CHANGE_APPVIEW_ACCESS_INIT,
payload: {
applicationId,
publicAccess,
},
}),
}),
)(
reduxForm<
@ -317,9 +301,11 @@ export default connect(
fetchAllRoles: (orgId: string) => void;
roles?: any;
applicationId?: string;
orgId?: string;
isApplicationInvite?: boolean;
}
>({
validate,
form: INVITE_USERS_TO_ORG_FORM,
})(InviteUsersForm),
})(OrgInviteUsersForm),
);

View File

@ -1,140 +0,0 @@
import React, { createRef, useState } from "react";
import styled from "styled-components";
import copy from "copy-to-clipboard";
import { StyledSwitch } from "components/propertyControls/StyledControls";
import Spinner from "components/editorComponents/Spinner";
import { BaseButton } from "components/designSystems/blueprint/ButtonComponent";
import { getDefaultPageId } from "sagas/SagaUtils";
import { getApplicationViewerPageURL } from "constants/routes";
const ShareWithPublicOption = styled.div`
{
display: flex;
padding: 10px 0px;
justify-content: space-between;
}
`;
const CopyToClipboard = 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 ShareToggle = styled.div`
{
&&& label {
margin-bottom: 0px;
}
&&& div {
margin-right: 5px;
}
display: flex;
}
`;
const ShareWithPublic = (props: any) => {
const {
currentApplicationDetails,
applicationId,
changeAppViewAccess,
isChangingViewAccess,
isFetchingApplication,
} = props;
const copyURLInput = createRef<HTMLInputElement>();
const defaultPageId = getDefaultPageId(currentApplicationDetails.pages);
const appViewEndPoint = getApplicationViewerPageURL(
applicationId,
defaultPageId,
);
const viewApplicationURL =
window.location.origin.toString() + appViewEndPoint;
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, viewApplicationURL.length);
}
};
return (
<>
<ShareWithPublicOption>
Make the application public
<ShareToggle>
{(isChangingViewAccess || isFetchingApplication) && (
<Spinner size={20} />
)}
{currentApplicationDetails && (
<StyledSwitch
onChange={() => {
changeAppViewAccess(
applicationId,
!currentApplicationDetails.isPublic,
);
}}
disabled={isChangingViewAccess || isFetchingApplication}
checked={currentApplicationDetails.isPublic}
large
/>
)}
</ShareToggle>
</ShareWithPublicOption>
{currentApplicationDetails.isPublic && (
<CopyToClipboard>
<StyledInput
type="text"
ref={copyURLInput}
readOnly
onClick={() => {
selectText();
}}
value={viewApplicationURL}
/>
<SelectButton
text={isCopied ? "Copied" : "Copy"}
accent="secondary"
onClick={() => {
copyToClipboard(viewApplicationURL);
}}
/>
</CopyToClipboard>
)}
</>
);
};
export default ShareWithPublic;

View File

@ -2,7 +2,6 @@ import React from "react";
import { Switch, useRouteMatch, useLocation } from "react-router-dom";
import PageWrapper from "pages/common/PageWrapper";
import Settings from "./settings";
import Invite from "./invite";
import DefaultOrgPage from "./defaultOrgPage";
import AppRoute from "pages/common/AppRoute";
export const Organization = () => {
@ -17,12 +16,6 @@ export const Organization = () => {
component={Settings}
name={"Settings"}
/>
<AppRoute
exact
path={`${path}/invite`}
component={Invite}
name={"Invite"}
/>
<AppRoute component={DefaultOrgPage} name={"DefaultOrgPage"} />
</Switch>
</PageWrapper>

View File

@ -1,14 +0,0 @@
import React from "react";
import InviteUsersForm from "./InviteUsersForm";
export const Invite = () => {
// const handleInviteUsersSubmit = (values: InviteUsersFormValues) => {};
return (
<React.Fragment>
<h2>Invite Users</h2>
<InviteUsersForm />
</React.Fragment>
);
};
export default Invite;

View File

@ -12,7 +12,7 @@ import {
import PageSectionDivider from "pages/common/PageSectionDivider";
import PageSectionHeader from "pages/common/PageSectionHeader";
import { ReduxActionTypes } from "constants/ReduxActionConstants";
import InviteUsersFormv2 from "pages/organization/InviteUsersFromv2";
import OrgInviteUsersForm from "pages/organization/OrgInviteUsersForm";
import Button from "components/editorComponents/Button";
import { OrgUser, Org } from "constants/orgConstants";
import { Menu, MenuItem, Popover, Position } from "@blueprintjs/core";
@ -258,7 +258,7 @@ export const OrgSettings = (props: PageProps) => {
/>
}
canOutsideClickClose={true}
Form={InviteUsersFormv2}
Form={OrgInviteUsersForm}
orgId={orgId}
title={`Invite Users to ${currentOrgName}`}
/>

View File

@ -72,13 +72,6 @@ const orgReducer = createReducer(initialState, {
isFetchAllUsers: false,
},
}),
[ReduxActionTypes.INVITE_USERS_TO_ORG_SUCCESS]: (
state: OrgReduxState,
action: ReduxAction<OrgUser[]>,
) => ({
...state,
orgUsers: [...state.orgUsers, ...action.payload],
}),
[ReduxActionTypes.FETCH_ALL_ROLES_SUCCESS]: (
state: OrgReduxState,
action: ReduxAction<Org[]>,

View File

@ -21,6 +21,7 @@ import OrgApi, {
FetchAllRolesResponse,
DeleteOrgUserRequest,
ChangeUserRoleRequest,
FetchAllRolesRequest,
} from "api/OrgApi";
import { ApiResponse } from "api/ApiResponses";
import { AppToaster } from "components/editorComponents/ToastComponent";
@ -144,9 +145,13 @@ export function* deleteOrgUserSaga(action: ReduxAction<DeleteOrgUserRequest>) {
}
}
export function* fetchAllRolesSaga(action: ReduxAction<DeleteOrgUserRequest>) {
export function* fetchAllRolesSaga(action: ReduxAction<FetchAllRolesRequest>) {
try {
const response: FetchAllRolesResponse = yield call(OrgApi.fetchAllRoles);
const request: FetchAllRolesRequest = action.payload;
const response: FetchAllRolesResponse = yield call(
OrgApi.fetchAllRoles,
request,
);
const isValidResponse = yield validateResponse(response);
if (isValidResponse) {
yield put({

View File

@ -227,8 +227,10 @@ export function* inviteUsers(
yield call(reject, { _error: errorMessage });
}
yield put({
type: ReduxActionTypes.INVITE_USERS_TO_ORG_SUCCESS,
payload: response.data,
type: ReduxActionTypes.FETCH_ALL_USERS_INIT,
payload: {
orgId: data.orgId,
},
});
yield call(resolve);
yield put(reset(INVITE_USERS_TO_ORG_FORM));