chore: Split share modal component into smaller components (#27681)
## Description Split share modal component into smaller components #### PR fixes following issue(s) Fixes [#27851](https://github.com/appsmithorg/appsmith/issues/27851) [#27671](https://github.com/appsmithorg/appsmith/issues/27671) #### Type of change - Chore (housekeeping or task changes that don't impact user perception) ## Testing #### How Has This Been Tested? - [x] Manual - [ ] JUnit - [ ] Jest - [x] Cypress ## Checklist: #### Dev activity - [x] My code follows the style guidelines of this project - [x] I have performed a self-review of my own code - [x] I have commented my code, particularly in hard-to-understand areas - [ ] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [ ] I have added tests that prove my fix is effective or that my feature works - [x] New and existing unit tests pass locally with my changes - [ ] PR is being merged under a feature flag #### QA activity: - [ ] [Speedbreak features](https://github.com/appsmithorg/TestSmith/wiki/Guidelines-for-test-plans#speedbreakers-) have been covered - [ ] Test plan covers all impacted features and [areas of interest](https://github.com/appsmithorg/TestSmith/wiki/Guidelines-for-test-plans#areas-of-interest-) - [ ] Test plan has been peer reviewed by project stakeholders and other QA members - [ ] Manually tested functionality on DP - [ ] We had an implementation alignment call with stakeholders post QA Round 2 - [ ] Cypress test cases have been added and approved by SDET/manual QA - [ ] Added `Test Plan Approved` label after Cypress tests were reviewed - [ ] Added `Test Plan Approved` label after JUnit tests were reviewed --------- Co-authored-by: Dipyaman Biswas <dipyaman@appsmith.com>
This commit is contained in:
parent
36aec9863b
commit
21f83023a0
|
|
@ -98,7 +98,7 @@
|
||||||
"dayjs": "^1.10.6",
|
"dayjs": "^1.10.6",
|
||||||
"deep-diff": "^1.0.2",
|
"deep-diff": "^1.0.2",
|
||||||
"design-system": "npm:@appsmithorg/design-system@2.1.21",
|
"design-system": "npm:@appsmithorg/design-system@2.1.21",
|
||||||
"design-system-old": "npm:@appsmithorg/design-system-old@1.1.11",
|
"design-system-old": "npm:@appsmithorg/design-system-old@1.1.12",
|
||||||
"downloadjs": "^1.4.7",
|
"downloadjs": "^1.4.7",
|
||||||
"echarts": "^5.4.2",
|
"echarts": "^5.4.2",
|
||||||
"echarts-gl": "^2.0.9",
|
"echarts-gl": "^2.0.9",
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,7 @@ import type { ApplicationPayload } from "@appsmith/constants/ReduxActionConstant
|
||||||
import { ReduxActionTypes } from "@appsmith/constants/ReduxActionConstants";
|
import { ReduxActionTypes } from "@appsmith/constants/ReduxActionConstants";
|
||||||
import PageWrapper from "pages/common/PageWrapper";
|
import PageWrapper from "pages/common/PageWrapper";
|
||||||
import SubHeader from "pages/common/SubHeader";
|
import SubHeader from "pages/common/SubHeader";
|
||||||
import WorkspaceInviteUsersForm from "@appsmith/pages/workspace/WorkspaceInviteUsersForm";
|
import WorkspaceInviteUsersForm from "pages/workspace/WorkspaceInviteUsersForm";
|
||||||
import type { User } from "constants/userConstants";
|
import type { User } from "constants/userConstants";
|
||||||
import { getCurrentUser } from "selectors/usersSelectors";
|
import { getCurrentUser } from "selectors/usersSelectors";
|
||||||
import { CREATE_WORKSPACE_FORM_NAME } from "@appsmith/constants/forms";
|
import { CREATE_WORKSPACE_FORM_NAME } from "@appsmith/constants/forms";
|
||||||
|
|
|
||||||
529
app/client/src/ce/pages/workspace/InviteUsersForm.tsx
Normal file
529
app/client/src/ce/pages/workspace/InviteUsersForm.tsx
Normal file
|
|
@ -0,0 +1,529 @@
|
||||||
|
import React, { useEffect, useState, useMemo, useRef } from "react";
|
||||||
|
import styled from "styled-components";
|
||||||
|
import TagListField from "components/editorComponents/form/fields/TagListField";
|
||||||
|
import { reduxForm, SubmissionError } from "redux-form";
|
||||||
|
import { connect, useSelector } from "react-redux";
|
||||||
|
import type { AppState } from "@appsmith/reducers";
|
||||||
|
import { getRolesForField } from "@appsmith/selectors/workspaceSelectors";
|
||||||
|
import type {
|
||||||
|
InviteUsersToWorkspaceFormValues,
|
||||||
|
InviteUsersProps,
|
||||||
|
} from "@appsmith/pages/workspace/helpers";
|
||||||
|
import { inviteUsersToWorkspace } from "@appsmith/pages/workspace/helpers";
|
||||||
|
import { INVITE_USERS_TO_WORKSPACE_FORM } from "@appsmith/constants/forms";
|
||||||
|
import {
|
||||||
|
createMessage,
|
||||||
|
INVITE_USERS_SUBMIT_SUCCESS,
|
||||||
|
INVITE_USER_SUBMIT_SUCCESS,
|
||||||
|
INVITE_USERS_VALIDATION_EMAILS_EMPTY,
|
||||||
|
INVITE_USERS_VALIDATION_EMAIL_LIST,
|
||||||
|
INVITE_USERS_VALIDATION_ROLE_EMPTY,
|
||||||
|
USERS_HAVE_ACCESS_TO_ALL_APPS,
|
||||||
|
BUSINESS_EDITION_TEXT,
|
||||||
|
INVITE_USER_RAMP_TEXT,
|
||||||
|
CUSTOM_ROLES_RAMP_TEXT,
|
||||||
|
CUSTOM_ROLE_DISABLED_OPTION_TEXT,
|
||||||
|
CUSTOM_ROLE_TEXT,
|
||||||
|
} from "@appsmith/constants/messages";
|
||||||
|
import { isEmail } from "utils/formhelpers";
|
||||||
|
import AnalyticsUtil from "utils/AnalyticsUtil";
|
||||||
|
import type { SelectOptionProps } from "design-system";
|
||||||
|
import { Callout, Checkbox } from "design-system";
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Icon,
|
||||||
|
Select,
|
||||||
|
Text,
|
||||||
|
Option,
|
||||||
|
Tooltip,
|
||||||
|
toast,
|
||||||
|
Link,
|
||||||
|
} from "design-system";
|
||||||
|
import {
|
||||||
|
fetchRolesForWorkspace,
|
||||||
|
fetchUsersForWorkspace,
|
||||||
|
fetchWorkspace,
|
||||||
|
} from "@appsmith/actions/workspaceActions";
|
||||||
|
import {
|
||||||
|
getRampLink,
|
||||||
|
showProductRamps,
|
||||||
|
} from "@appsmith/selectors/rampSelectors";
|
||||||
|
import {
|
||||||
|
RAMP_NAME,
|
||||||
|
RampFeature,
|
||||||
|
RampSection,
|
||||||
|
} from "utils/ProductRamps/RampsControlList";
|
||||||
|
import BusinessTag from "components/BusinessTag";
|
||||||
|
import { selectFeatureFlags } from "@appsmith/selectors/featureFlagsSelectors";
|
||||||
|
import store from "store";
|
||||||
|
import { isGACEnabled } from "@appsmith/utils/planHelpers";
|
||||||
|
import type { DefaultOptionType } from "rc-select/lib/Select";
|
||||||
|
|
||||||
|
const featureFlags = selectFeatureFlags(store.getState());
|
||||||
|
const isFeatureEnabled = isGACEnabled(featureFlags);
|
||||||
|
|
||||||
|
export const StyledForm = styled.form`
|
||||||
|
width: 100%;
|
||||||
|
background: var(--ads-v2-color-bg);
|
||||||
|
&&& {
|
||||||
|
.wrapper > div:nth-child(1) {
|
||||||
|
width: 60%;
|
||||||
|
}
|
||||||
|
.wrapper > div:nth-child(2) {
|
||||||
|
width: 40%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const ErrorBox = styled.div<{ message?: boolean }>`
|
||||||
|
${(props) =>
|
||||||
|
props.message ? `margin: ${props.theme.spaces[9]}px 0px;` : null};
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const StyledInviteFieldGroup = styled.div`
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 0.8rem;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const ErrorTextContainer = styled.div`
|
||||||
|
display: flex;
|
||||||
|
margin-top: 4px;
|
||||||
|
gap: 4px;
|
||||||
|
|
||||||
|
> p {
|
||||||
|
color: var(--ads-v2-color-fg-error);
|
||||||
|
}
|
||||||
|
|
||||||
|
svg {
|
||||||
|
path {
|
||||||
|
fill: var(--ads-v2-color-fg-error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const WorkspaceText = styled.div`
|
||||||
|
a {
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
export const CustomRoleRampTooltip = styled(Tooltip)`
|
||||||
|
pointer-events: auto;
|
||||||
|
`;
|
||||||
|
export const RampLink = styled(Link)`
|
||||||
|
display: inline;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const StyledCheckbox = styled(Checkbox)`
|
||||||
|
height: 16px;
|
||||||
|
|
||||||
|
.ads-v2-checkbox {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const OptionLabel = styled(Text)`
|
||||||
|
overflow: hidden;
|
||||||
|
word-break: break-all;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const validateFormValues = (values: {
|
||||||
|
users: string;
|
||||||
|
role?: string;
|
||||||
|
roles?: Partial<SelectOptionProps>[];
|
||||||
|
}) => {
|
||||||
|
if (values.users && values.users.length > 0) {
|
||||||
|
const _users = values.users.split(",").filter(Boolean);
|
||||||
|
|
||||||
|
_users.forEach((user) => {
|
||||||
|
if (!isEmail(user)) {
|
||||||
|
throw new SubmissionError({
|
||||||
|
_error: createMessage(
|
||||||
|
INVITE_USERS_VALIDATION_EMAIL_LIST,
|
||||||
|
!isFeatureEnabled,
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
throw new SubmissionError({
|
||||||
|
_error: createMessage(INVITE_USERS_VALIDATION_EMAILS_EMPTY),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof values.role === "undefined" || values.role.length === 0) {
|
||||||
|
throw new SubmissionError({
|
||||||
|
_error: createMessage(INVITE_USERS_VALIDATION_ROLE_EMPTY),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const validate = (values: any) => {
|
||||||
|
const errors: any = {};
|
||||||
|
if (!(values.users && values.users.length > 0)) {
|
||||||
|
errors["users"] = createMessage(INVITE_USERS_VALIDATION_EMAILS_EMPTY);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof values.role === "undefined" || values.role.length === 0) {
|
||||||
|
errors["role"] = createMessage(INVITE_USERS_VALIDATION_ROLE_EMPTY);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (values.users && values.users.length > 0) {
|
||||||
|
const _users = values.users.split(",").filter(Boolean);
|
||||||
|
|
||||||
|
_users.forEach((user: string) => {
|
||||||
|
if (!isEmail(user)) {
|
||||||
|
errors["users"] = createMessage(
|
||||||
|
INVITE_USERS_VALIDATION_EMAIL_LIST,
|
||||||
|
!isFeatureEnabled,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return errors;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function InviteUserText({
|
||||||
|
isApplicationPage,
|
||||||
|
}: {
|
||||||
|
isApplicationPage: boolean;
|
||||||
|
}) {
|
||||||
|
const rampLinkSelector = getRampLink({
|
||||||
|
section: RampSection.AppShare,
|
||||||
|
feature: RampFeature.Gac,
|
||||||
|
});
|
||||||
|
const rampLink = useSelector(rampLinkSelector);
|
||||||
|
const showRampSelector = showProductRamps(RAMP_NAME.INVITE_USER_TO_APP);
|
||||||
|
const canShowRamp = useSelector(showRampSelector);
|
||||||
|
return (
|
||||||
|
<Text
|
||||||
|
color="var(--ads-v2-color-fg)"
|
||||||
|
data-testid="helper-message"
|
||||||
|
kind="action-m"
|
||||||
|
>
|
||||||
|
{canShowRamp && isApplicationPage ? (
|
||||||
|
<>
|
||||||
|
{createMessage(INVITE_USER_RAMP_TEXT)}
|
||||||
|
<Link kind="primary" target="_blank" to={rampLink}>
|
||||||
|
{createMessage(BUSINESS_EDITION_TEXT)}
|
||||||
|
</Link>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
createMessage(USERS_HAVE_ACCESS_TO_ALL_APPS)
|
||||||
|
)}
|
||||||
|
</Text>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CustomRolesRamp() {
|
||||||
|
const [dynamicProps, setDynamicProps] = useState<any>({});
|
||||||
|
const rampLinkSelector = getRampLink({
|
||||||
|
section: RampSection.WorkspaceShare,
|
||||||
|
feature: RampFeature.Gac,
|
||||||
|
});
|
||||||
|
const rampLink = useSelector(rampLinkSelector);
|
||||||
|
const rampText = (
|
||||||
|
<Text color="var(--ads-v2-color-white)" kind="action-m">
|
||||||
|
{createMessage(CUSTOM_ROLES_RAMP_TEXT)}{" "}
|
||||||
|
<RampLink
|
||||||
|
className="inline"
|
||||||
|
kind="primary"
|
||||||
|
onClick={() => {
|
||||||
|
setDynamicProps({ visible: false });
|
||||||
|
window.open(rampLink, "_blank");
|
||||||
|
// This reset of prop is required because, else the tooltip will be controlled by the state
|
||||||
|
setTimeout(() => {
|
||||||
|
setDynamicProps({});
|
||||||
|
}, 1);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{createMessage(BUSINESS_EDITION_TEXT)}
|
||||||
|
</RampLink>
|
||||||
|
</Text>
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<CustomRoleRampTooltip
|
||||||
|
content={rampText}
|
||||||
|
placement="right"
|
||||||
|
{...dynamicProps}
|
||||||
|
>
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<div className="flex gap-1">
|
||||||
|
<Text color="var(--ads-v2-color-fg-emphasis)" kind="heading-xs">
|
||||||
|
{createMessage(CUSTOM_ROLE_TEXT)}
|
||||||
|
</Text>
|
||||||
|
<BusinessTag size="md" />
|
||||||
|
</div>
|
||||||
|
<Text kind="body-s">
|
||||||
|
{createMessage(CUSTOM_ROLE_DISABLED_OPTION_TEXT)}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
</CustomRoleRampTooltip>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function InviteUsersForm(props: any) {
|
||||||
|
const [emailError, setEmailError] = useState("");
|
||||||
|
const [selectedOption, setSelectedOption] = useState<any[]>([]);
|
||||||
|
const selectedId = props?.selected?.id;
|
||||||
|
const showRampSelector = showProductRamps(RAMP_NAME.CUSTOM_ROLES);
|
||||||
|
const canShowRamp = useSelector(showRampSelector);
|
||||||
|
|
||||||
|
const selected = useMemo(
|
||||||
|
() =>
|
||||||
|
selectedId &&
|
||||||
|
props.selected && {
|
||||||
|
description: props.selected.rolename,
|
||||||
|
value: props.selected.rolename,
|
||||||
|
key: props.selected.id,
|
||||||
|
},
|
||||||
|
[selectedId],
|
||||||
|
);
|
||||||
|
|
||||||
|
const {
|
||||||
|
anyTouched,
|
||||||
|
customProps = {},
|
||||||
|
error,
|
||||||
|
fetchAllRoles,
|
||||||
|
fetchCurrentWorkspace,
|
||||||
|
fetchUsers,
|
||||||
|
handleSubmit,
|
||||||
|
isAclFlow = false,
|
||||||
|
isApplicationPage = false,
|
||||||
|
isMultiSelectDropdown = false,
|
||||||
|
placeholder = "",
|
||||||
|
submitFailed,
|
||||||
|
submitSucceeded,
|
||||||
|
submitting,
|
||||||
|
valid,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const { disableDropdown = false } = customProps;
|
||||||
|
|
||||||
|
// set state for checking number of users invited
|
||||||
|
const [numberOfUsersInvited, updateNumberOfUsersInvited] = useState(0);
|
||||||
|
|
||||||
|
const invitedEmails = useRef<undefined | string[]>();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setSelectedOption([]);
|
||||||
|
}, [submitSucceeded]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchCurrentWorkspace(props.workspaceId);
|
||||||
|
fetchAllRoles(props.workspaceId);
|
||||||
|
fetchUsers(props.workspaceId);
|
||||||
|
}, [props.workspaceId, fetchAllRoles, fetchCurrentWorkspace, fetchUsers]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (selected) {
|
||||||
|
setSelectedOption([selected]);
|
||||||
|
props.initialize({
|
||||||
|
role: [selected],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (submitSucceeded) {
|
||||||
|
toast.show(
|
||||||
|
numberOfUsersInvited > 1
|
||||||
|
? createMessage(INVITE_USERS_SUBMIT_SUCCESS)
|
||||||
|
: createMessage(INVITE_USER_SUBMIT_SUCCESS),
|
||||||
|
{ kind: "success" },
|
||||||
|
);
|
||||||
|
|
||||||
|
props?.checkIfInvitedUsersFromDifferentDomain?.(invitedEmails.current);
|
||||||
|
}
|
||||||
|
}, [submitSucceeded, invitedEmails.current]);
|
||||||
|
|
||||||
|
const styledRoles =
|
||||||
|
props.options && props.options.length > 0
|
||||||
|
? props.options
|
||||||
|
: props.roles.map((role: any) => {
|
||||||
|
return {
|
||||||
|
key: role.id,
|
||||||
|
value: role.name?.split(" - ")[0],
|
||||||
|
description: role.description,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const onSelect = (value: string, option: DefaultOptionType) => {
|
||||||
|
if (isMultiSelectDropdown) {
|
||||||
|
setSelectedOption((selectedOptions) => [...selectedOptions, option]);
|
||||||
|
} else {
|
||||||
|
setSelectedOption([option]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const errorHandler = (error: string) => {
|
||||||
|
setEmailError(error);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onRemoveOptions = (value: string, option: DefaultOptionType) => {
|
||||||
|
if (isMultiSelectDropdown) {
|
||||||
|
setSelectedOption((selectedOptions) =>
|
||||||
|
selectedOptions.filter((opt) => opt.value !== option.value),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StyledForm
|
||||||
|
onSubmit={handleSubmit((values: any, dispatch: any) => {
|
||||||
|
const roles = isMultiSelectDropdown
|
||||||
|
? selectedOption
|
||||||
|
.map((option: DefaultOptionType) => option.value)
|
||||||
|
.join(",")
|
||||||
|
: selectedOption[0].value;
|
||||||
|
validateFormValues({ ...values, role: roles });
|
||||||
|
const usersAsStringsArray = values.users.split(",");
|
||||||
|
// update state to show success message correctly
|
||||||
|
updateNumberOfUsersInvited(usersAsStringsArray.length);
|
||||||
|
const validEmails = usersAsStringsArray.filter((user: string) =>
|
||||||
|
isEmail(user),
|
||||||
|
);
|
||||||
|
const validEmailsString = validEmails.join(",");
|
||||||
|
invitedEmails.current = validEmails;
|
||||||
|
|
||||||
|
AnalyticsUtil.logEvent("INVITE_USER", {
|
||||||
|
...(!isFeatureEnabled ? { users: usersAsStringsArray } : {}),
|
||||||
|
role: roles,
|
||||||
|
numberOfUsersInvited: usersAsStringsArray.length,
|
||||||
|
orgId: props.workspaceId,
|
||||||
|
});
|
||||||
|
return inviteUsersToWorkspace(
|
||||||
|
{
|
||||||
|
...(props.workspaceId ? { workspaceId: props.workspaceId } : {}),
|
||||||
|
users: validEmailsString,
|
||||||
|
permissionGroupId: roles,
|
||||||
|
},
|
||||||
|
dispatch,
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<StyledInviteFieldGroup>
|
||||||
|
<div style={{ width: "60%" }}>
|
||||||
|
<TagListField
|
||||||
|
autofocus
|
||||||
|
className="ml-0.5"
|
||||||
|
customError={(err: string) => errorHandler(err)}
|
||||||
|
data-testid="t--invite-email-input"
|
||||||
|
intent="success"
|
||||||
|
label="Emails"
|
||||||
|
name="users"
|
||||||
|
placeholder={placeholder || "Enter email address(es)"}
|
||||||
|
type="email"
|
||||||
|
/>
|
||||||
|
{emailError && (
|
||||||
|
<ErrorTextContainer>
|
||||||
|
<Icon name="alert-line" size="sm" />
|
||||||
|
<Text kind="body-s" renderAs="p">
|
||||||
|
{emailError}
|
||||||
|
</Text>
|
||||||
|
</ErrorTextContainer>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div style={{ width: "40%" }}>
|
||||||
|
<Select
|
||||||
|
data-testid="t--invite-role-input"
|
||||||
|
getPopupContainer={(triggerNode) =>
|
||||||
|
triggerNode.parentNode.parentNode
|
||||||
|
}
|
||||||
|
isDisabled={disableDropdown}
|
||||||
|
isMultiSelect={isMultiSelectDropdown}
|
||||||
|
listHeight={400}
|
||||||
|
onDeselect={onRemoveOptions}
|
||||||
|
onSelect={onSelect}
|
||||||
|
optionLabelProp="label"
|
||||||
|
placeholder="Select a role"
|
||||||
|
value={selectedOption}
|
||||||
|
>
|
||||||
|
{styledRoles.map((role: DefaultOptionType) => (
|
||||||
|
<Option key={role.key} label={role.value} value={role.key}>
|
||||||
|
<div className="flex gap-1 items-center">
|
||||||
|
{isMultiSelectDropdown && (
|
||||||
|
<StyledCheckbox
|
||||||
|
isSelected={selectedOption.find((v) => v.key == role.key)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<OptionLabel
|
||||||
|
color="var(--ads-v2-color-fg-emphasis)"
|
||||||
|
kind={role.description && "heading-xs"}
|
||||||
|
>
|
||||||
|
{role.value}
|
||||||
|
</OptionLabel>
|
||||||
|
{role.description && (
|
||||||
|
<Text kind="body-s">{role.description}</Text>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Option>
|
||||||
|
))}
|
||||||
|
{canShowRamp && (
|
||||||
|
<Option disabled>
|
||||||
|
<CustomRolesRamp />
|
||||||
|
</Option>
|
||||||
|
)}
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Button
|
||||||
|
className="t--invite-user-btn"
|
||||||
|
isDisabled={!valid || selectedOption.length === 0}
|
||||||
|
isLoading={submitting && !(submitFailed && !anyTouched)}
|
||||||
|
size="md"
|
||||||
|
type="submit"
|
||||||
|
>
|
||||||
|
Invite
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</StyledInviteFieldGroup>
|
||||||
|
{!isAclFlow && (
|
||||||
|
<div className="flex gap-2 mt-2 items-start">
|
||||||
|
<Icon className="mt-1" name="user-3-line" size="md" />
|
||||||
|
<WorkspaceText>
|
||||||
|
<InviteUserText isApplicationPage={isApplicationPage} />
|
||||||
|
</WorkspaceText>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<ErrorBox message={submitFailed}>
|
||||||
|
{submitFailed && error && <Callout kind="error">{error}</Callout>}
|
||||||
|
</ErrorBox>
|
||||||
|
</StyledForm>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const mapStateToProps = (
|
||||||
|
state: AppState,
|
||||||
|
{ formName }: { formName?: string },
|
||||||
|
) => {
|
||||||
|
return {
|
||||||
|
roles: getRolesForField(state),
|
||||||
|
form: formName || INVITE_USERS_TO_WORKSPACE_FORM,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const mapDispatchToProps = (dispatch: any) => ({
|
||||||
|
fetchAllRoles: (workspaceId: string) =>
|
||||||
|
dispatch(fetchRolesForWorkspace(workspaceId)),
|
||||||
|
fetchCurrentWorkspace: (workspaceId: string) =>
|
||||||
|
dispatch(fetchWorkspace(workspaceId)),
|
||||||
|
fetchUsers: (workspaceId: string) =>
|
||||||
|
dispatch(fetchUsersForWorkspace(workspaceId)),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default connect(
|
||||||
|
mapStateToProps,
|
||||||
|
mapDispatchToProps,
|
||||||
|
)(
|
||||||
|
reduxForm<InviteUsersToWorkspaceFormValues, InviteUsersProps>({
|
||||||
|
validate,
|
||||||
|
})(InviteUsersForm),
|
||||||
|
);
|
||||||
|
|
@ -36,7 +36,7 @@ import {
|
||||||
PERMISSION_TYPE,
|
PERMISSION_TYPE,
|
||||||
} from "@appsmith/utils/permissionHelpers";
|
} from "@appsmith/utils/permissionHelpers";
|
||||||
import { getInitials } from "utils/AppsmithUtils";
|
import { getInitials } from "utils/AppsmithUtils";
|
||||||
import { CustomRolesRamp } from "@appsmith/pages/workspace/WorkspaceInviteUsersForm";
|
import { CustomRolesRamp } from "@appsmith/pages/workspace/InviteUsersForm";
|
||||||
import { showProductRamps } from "@appsmith/selectors/rampSelectors";
|
import { showProductRamps } from "@appsmith/selectors/rampSelectors";
|
||||||
import { RAMP_NAME } from "utils/ProductRamps/RampsControlList";
|
import { RAMP_NAME } from "utils/ProductRamps/RampsControlList";
|
||||||
import { useFeatureFlag } from "utils/hooks/useFeatureFlag";
|
import { useFeatureFlag } from "utils/hooks/useFeatureFlag";
|
||||||
|
|
|
||||||
|
|
@ -1,766 +0,0 @@
|
||||||
import React, { useEffect, useState, useMemo, useRef } from "react";
|
|
||||||
import styled from "styled-components";
|
|
||||||
import TagListField from "components/editorComponents/form/fields/TagListField";
|
|
||||||
import { reduxForm, SubmissionError } from "redux-form";
|
|
||||||
import { connect, useSelector } from "react-redux";
|
|
||||||
import type { AppState } from "@appsmith/reducers";
|
|
||||||
import {
|
|
||||||
getRolesForField,
|
|
||||||
getAllUsers,
|
|
||||||
getCurrentAppWorkspace,
|
|
||||||
} from "@appsmith/selectors/workspaceSelectors";
|
|
||||||
import type { InviteUsersToWorkspaceFormValues } from "@appsmith/pages/workspace/helpers";
|
|
||||||
import { inviteUsersToWorkspace } from "@appsmith/pages/workspace/helpers";
|
|
||||||
import { INVITE_USERS_TO_WORKSPACE_FORM } from "@appsmith/constants/forms";
|
|
||||||
import {
|
|
||||||
createMessage,
|
|
||||||
INVITE_USERS_SUBMIT_SUCCESS,
|
|
||||||
INVITE_USER_SUBMIT_SUCCESS,
|
|
||||||
INVITE_USERS_VALIDATION_EMAILS_EMPTY,
|
|
||||||
INVITE_USERS_VALIDATION_EMAIL_LIST,
|
|
||||||
INVITE_USERS_VALIDATION_ROLE_EMPTY,
|
|
||||||
USERS_HAVE_ACCESS_TO_ALL_APPS,
|
|
||||||
NO_USERS_INVITED,
|
|
||||||
BUSINESS_EDITION_TEXT,
|
|
||||||
INVITE_USER_RAMP_TEXT,
|
|
||||||
CUSTOM_ROLES_RAMP_TEXT,
|
|
||||||
CUSTOM_ROLE_DISABLED_OPTION_TEXT,
|
|
||||||
CUSTOM_ROLE_TEXT,
|
|
||||||
} from "@appsmith/constants/messages";
|
|
||||||
import { isEmail } from "utils/formhelpers";
|
|
||||||
import {
|
|
||||||
isPermitted,
|
|
||||||
PERMISSION_TYPE,
|
|
||||||
} from "@appsmith/utils/permissionHelpers";
|
|
||||||
import { getAppsmithConfigs } from "@appsmith/configs";
|
|
||||||
import AnalyticsUtil from "utils/AnalyticsUtil";
|
|
||||||
import type { SelectOptionProps } from "design-system";
|
|
||||||
import { Callout, Checkbox } from "design-system";
|
|
||||||
import {
|
|
||||||
Avatar,
|
|
||||||
Button,
|
|
||||||
Icon,
|
|
||||||
Select,
|
|
||||||
Spinner,
|
|
||||||
Text,
|
|
||||||
Option,
|
|
||||||
Tooltip,
|
|
||||||
toast,
|
|
||||||
Link,
|
|
||||||
} from "design-system";
|
|
||||||
import { getInitialsFromName } from "utils/AppsmithUtils";
|
|
||||||
import ManageUsers from "pages/workspace/ManageUsers";
|
|
||||||
import {
|
|
||||||
fetchRolesForWorkspace,
|
|
||||||
fetchUsersForWorkspace,
|
|
||||||
fetchWorkspace,
|
|
||||||
} from "@appsmith/actions/workspaceActions";
|
|
||||||
import { USER_PHOTO_ASSET_URL } from "constants/userConstants";
|
|
||||||
import { importSvg } from "design-system-old";
|
|
||||||
import type { WorkspaceUserRoles } from "@appsmith/constants/workspaceConstants";
|
|
||||||
import {
|
|
||||||
getRampLink,
|
|
||||||
showProductRamps,
|
|
||||||
} from "@appsmith/selectors/rampSelectors";
|
|
||||||
import {
|
|
||||||
RAMP_NAME,
|
|
||||||
RampFeature,
|
|
||||||
RampSection,
|
|
||||||
} from "utils/ProductRamps/RampsControlList";
|
|
||||||
import BusinessTag from "components/BusinessTag";
|
|
||||||
import { getDomainFromEmail } from "utils/helpers";
|
|
||||||
import { getCurrentUser } from "selectors/usersSelectors";
|
|
||||||
import PartnerProgramCallout from "./PartnerProgramCallout";
|
|
||||||
import {
|
|
||||||
getPartnerProgramCalloutShown,
|
|
||||||
setPartnerProgramCalloutShown,
|
|
||||||
} from "utils/storage";
|
|
||||||
import { selectFeatureFlags } from "@appsmith/selectors/featureFlagsSelectors";
|
|
||||||
import store from "store";
|
|
||||||
import { isGACEnabled } from "@appsmith/utils/planHelpers";
|
|
||||||
|
|
||||||
const NoEmailConfigImage = importSvg(
|
|
||||||
() => import("assets/images/email-not-configured.svg"),
|
|
||||||
);
|
|
||||||
|
|
||||||
const { cloudHosting } = getAppsmithConfigs();
|
|
||||||
|
|
||||||
const featureFlags = selectFeatureFlags(store.getState());
|
|
||||||
const isFeatureEnabled = isGACEnabled(featureFlags);
|
|
||||||
|
|
||||||
export const WorkspaceInviteWrapper = styled.div`
|
|
||||||
> div {
|
|
||||||
margin-top: 0;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const StyledForm = styled.form`
|
|
||||||
width: 100%;
|
|
||||||
background: var(--ads-v2-color-bg);
|
|
||||||
&&& {
|
|
||||||
.wrapper > div:nth-child(1) {
|
|
||||||
width: 60%;
|
|
||||||
}
|
|
||||||
.wrapper > div:nth-child(2) {
|
|
||||||
width: 40%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const ErrorBox = styled.div<{ message?: boolean }>`
|
|
||||||
${(props) =>
|
|
||||||
props.message ? `margin: ${props.theme.spaces[9]}px 0px;` : null};
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const StyledInviteFieldGroup = styled.div`
|
|
||||||
display: flex;
|
|
||||||
align-items: baseline;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: 0.8rem;
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const UserList = styled.div`
|
|
||||||
margin-top: 10px;
|
|
||||||
max-height: 260px;
|
|
||||||
overflow-y: auto;
|
|
||||||
justify-content: space-between;
|
|
||||||
margin-left: 0.1rem;
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const User = styled.div`
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
min-height: 54px;
|
|
||||||
justify-content: space-between;
|
|
||||||
border-bottom: 1px solid var(--ads-v2-color-border);
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const UserInfo = styled.div`
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
div:first-child {
|
|
||||||
cursor: default;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const UserRole = styled.div`
|
|
||||||
span {
|
|
||||||
word-break: break-word;
|
|
||||||
margin-right: 8px;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const UserName = styled.div`
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
margin: 0 10px;
|
|
||||||
span {
|
|
||||||
word-break: break-word;
|
|
||||||
|
|
||||||
&:nth-child(1) {
|
|
||||||
margin-bottom: 1px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const MailConfigContainer = styled.div`
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
padding: 24px 4px;
|
|
||||||
padding-bottom: 0;
|
|
||||||
align-items: center;
|
|
||||||
&& > span {
|
|
||||||
color: var(--ads-v2-color-fg);
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const ManageUsersContainer = styled.div`
|
|
||||||
padding: 12px 0;
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const ErrorTextContainer = styled.div`
|
|
||||||
display: flex;
|
|
||||||
margin-top: 4px;
|
|
||||||
gap: 4px;
|
|
||||||
|
|
||||||
> p {
|
|
||||||
color: var(--ads-v2-color-fg-error);
|
|
||||||
}
|
|
||||||
|
|
||||||
svg {
|
|
||||||
path {
|
|
||||||
fill: var(--ads-v2-color-fg-error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const WorkspaceText = styled.div`
|
|
||||||
a {
|
|
||||||
display: inline;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
export const CustomRoleRampTooltip = styled(Tooltip)`
|
|
||||||
pointer-events: auto;
|
|
||||||
`;
|
|
||||||
export const RampLink = styled(Link)`
|
|
||||||
display: inline;
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const StyledCheckbox = styled(Checkbox)`
|
|
||||||
height: 16px;
|
|
||||||
|
|
||||||
.ads-v2-checkbox {
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const OptionLabel = styled(Text)`
|
|
||||||
overflow: hidden;
|
|
||||||
word-break: break-all;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
display: -webkit-box;
|
|
||||||
-webkit-line-clamp: 2;
|
|
||||||
-webkit-box-orient: vertical;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const validateFormValues = (values: {
|
|
||||||
users: string;
|
|
||||||
role?: string;
|
|
||||||
roles?: Partial<SelectOptionProps>[];
|
|
||||||
}) => {
|
|
||||||
if (values.users && values.users.length > 0) {
|
|
||||||
const _users = values.users.split(",").filter(Boolean);
|
|
||||||
|
|
||||||
_users.forEach((user) => {
|
|
||||||
if (!isEmail(user)) {
|
|
||||||
throw new SubmissionError({
|
|
||||||
_error: createMessage(
|
|
||||||
INVITE_USERS_VALIDATION_EMAIL_LIST,
|
|
||||||
!isFeatureEnabled,
|
|
||||||
),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
throw new SubmissionError({
|
|
||||||
_error: createMessage(INVITE_USERS_VALIDATION_EMAILS_EMPTY),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof values.role === "undefined" || values.role.length === 0) {
|
|
||||||
throw new SubmissionError({
|
|
||||||
_error: createMessage(INVITE_USERS_VALIDATION_ROLE_EMPTY),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const validate = (values: any) => {
|
|
||||||
const errors: any = {};
|
|
||||||
if (!(values.users && values.users.length > 0)) {
|
|
||||||
errors["users"] = createMessage(INVITE_USERS_VALIDATION_EMAILS_EMPTY);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof values.role === "undefined" || values.role.length === 0) {
|
|
||||||
errors["role"] = createMessage(INVITE_USERS_VALIDATION_ROLE_EMPTY);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (values.users && values.users.length > 0) {
|
|
||||||
const _users = values.users.split(",").filter(Boolean);
|
|
||||||
|
|
||||||
_users.forEach((user: string) => {
|
|
||||||
if (!isEmail(user)) {
|
|
||||||
errors["users"] = createMessage(
|
|
||||||
INVITE_USERS_VALIDATION_EMAIL_LIST,
|
|
||||||
!isFeatureEnabled,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return errors;
|
|
||||||
};
|
|
||||||
|
|
||||||
function InviteUserText({
|
|
||||||
isApplicationInvite,
|
|
||||||
}: {
|
|
||||||
isApplicationInvite: boolean;
|
|
||||||
}) {
|
|
||||||
const rampLinkSelector = getRampLink({
|
|
||||||
section: RampSection.AppShare,
|
|
||||||
feature: RampFeature.Gac,
|
|
||||||
});
|
|
||||||
const rampLink = useSelector(rampLinkSelector);
|
|
||||||
const showRampSelector = showProductRamps(RAMP_NAME.INVITE_USER_TO_APP);
|
|
||||||
const canShowRamp = useSelector(showRampSelector);
|
|
||||||
return (
|
|
||||||
<Text
|
|
||||||
color="var(--ads-v2-color-fg)"
|
|
||||||
data-testid="helper-message"
|
|
||||||
kind="action-m"
|
|
||||||
>
|
|
||||||
{canShowRamp && isApplicationInvite ? (
|
|
||||||
<>
|
|
||||||
{createMessage(INVITE_USER_RAMP_TEXT)}
|
|
||||||
<Link kind="primary" target="_blank" to={rampLink}>
|
|
||||||
{createMessage(BUSINESS_EDITION_TEXT)}
|
|
||||||
</Link>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
createMessage(USERS_HAVE_ACCESS_TO_ALL_APPS)
|
|
||||||
)}
|
|
||||||
</Text>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function CustomRolesRamp() {
|
|
||||||
const [dynamicProps, setDynamicProps] = useState<any>({});
|
|
||||||
const rampLinkSelector = getRampLink({
|
|
||||||
section: RampSection.WorkspaceShare,
|
|
||||||
feature: RampFeature.Gac,
|
|
||||||
});
|
|
||||||
const rampLink = useSelector(rampLinkSelector);
|
|
||||||
const rampText = (
|
|
||||||
<Text color="var(--ads-v2-color-white)" kind="action-m">
|
|
||||||
{createMessage(CUSTOM_ROLES_RAMP_TEXT)}{" "}
|
|
||||||
<RampLink
|
|
||||||
className="inline"
|
|
||||||
kind="primary"
|
|
||||||
onClick={() => {
|
|
||||||
setDynamicProps({ visible: false });
|
|
||||||
window.open(rampLink, "_blank");
|
|
||||||
// This reset of prop is required because, else the tooltip will be controlled by the state
|
|
||||||
setTimeout(() => {
|
|
||||||
setDynamicProps({});
|
|
||||||
}, 1);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{createMessage(BUSINESS_EDITION_TEXT)}
|
|
||||||
</RampLink>
|
|
||||||
</Text>
|
|
||||||
);
|
|
||||||
return (
|
|
||||||
<CustomRoleRampTooltip
|
|
||||||
content={rampText}
|
|
||||||
placement="right"
|
|
||||||
{...dynamicProps}
|
|
||||||
>
|
|
||||||
<div className="flex flex-col gap-1">
|
|
||||||
<div className="flex gap-1">
|
|
||||||
<Text color="var(--ads-v2-color-fg-emphasis)" kind="heading-xs">
|
|
||||||
{createMessage(CUSTOM_ROLE_TEXT)}
|
|
||||||
</Text>
|
|
||||||
<BusinessTag size="md" />
|
|
||||||
</div>
|
|
||||||
<Text kind="body-s">
|
|
||||||
{createMessage(CUSTOM_ROLE_DISABLED_OPTION_TEXT)}
|
|
||||||
</Text>
|
|
||||||
</div>
|
|
||||||
</CustomRoleRampTooltip>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function WorkspaceInviteUsersForm(props: any) {
|
|
||||||
const [emailError, setEmailError] = useState("");
|
|
||||||
const [selectedOption, setSelectedOption] = useState<any[]>([]);
|
|
||||||
const userRef = React.createRef<HTMLDivElement>();
|
|
||||||
// const history = useHistory();
|
|
||||||
const selectedId = props?.selected?.id;
|
|
||||||
const currentUser = useSelector(getCurrentUser);
|
|
||||||
|
|
||||||
const showRampSelector = showProductRamps(RAMP_NAME.CUSTOM_ROLES);
|
|
||||||
const canShowRamp = useSelector(showRampSelector);
|
|
||||||
|
|
||||||
const selected = useMemo(
|
|
||||||
() =>
|
|
||||||
selectedId &&
|
|
||||||
props.selected && {
|
|
||||||
description: props.selected.rolename,
|
|
||||||
value: props.selected.rolename,
|
|
||||||
key: props.selected.id,
|
|
||||||
},
|
|
||||||
[selectedId],
|
|
||||||
);
|
|
||||||
|
|
||||||
const {
|
|
||||||
allUsers,
|
|
||||||
anyTouched,
|
|
||||||
customProps = {},
|
|
||||||
error,
|
|
||||||
fetchAllRoles,
|
|
||||||
fetchCurrentWorkspace,
|
|
||||||
fetchUser,
|
|
||||||
handleSubmit,
|
|
||||||
isApplicationInvite = false,
|
|
||||||
isLoading,
|
|
||||||
isMultiSelectDropdown = false,
|
|
||||||
placeholder = "",
|
|
||||||
submitFailed,
|
|
||||||
submitSucceeded,
|
|
||||||
submitting,
|
|
||||||
valid,
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
const {
|
|
||||||
disableDropdown = false,
|
|
||||||
disableManageUsers = false,
|
|
||||||
disableUserList = false,
|
|
||||||
} = customProps;
|
|
||||||
|
|
||||||
// set state for checking number of users invited
|
|
||||||
const [numberOfUsersInvited, updateNumberOfUsersInvited] = useState(0);
|
|
||||||
const currentWorkspace = useSelector(getCurrentAppWorkspace);
|
|
||||||
|
|
||||||
const invitedEmails = useRef<undefined | string[]>();
|
|
||||||
const emailOutsideCurrentDomain = useRef<undefined | string>();
|
|
||||||
const [showPartnerProgramCallout, setShowPartnerProgramCallout] =
|
|
||||||
useState(false);
|
|
||||||
|
|
||||||
const userWorkspacePermissions = currentWorkspace?.userPermissions ?? [];
|
|
||||||
const canManage = isPermitted(
|
|
||||||
userWorkspacePermissions,
|
|
||||||
PERMISSION_TYPE.MANAGE_WORKSPACE,
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setSelectedOption([]);
|
|
||||||
}, [submitSucceeded]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
fetchUser(props.workspaceId);
|
|
||||||
fetchAllRoles(props.workspaceId);
|
|
||||||
fetchCurrentWorkspace(props.workspaceId);
|
|
||||||
}, [props.workspaceId, fetchUser, fetchAllRoles, fetchCurrentWorkspace]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (selected) {
|
|
||||||
setSelectedOption([selected]);
|
|
||||||
props.initialize({
|
|
||||||
role: [selected],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (submitSucceeded) {
|
|
||||||
toast.show(
|
|
||||||
numberOfUsersInvited > 1
|
|
||||||
? createMessage(INVITE_USERS_SUBMIT_SUCCESS)
|
|
||||||
: createMessage(INVITE_USER_SUBMIT_SUCCESS),
|
|
||||||
{ kind: "success" },
|
|
||||||
);
|
|
||||||
|
|
||||||
checkIfInvitedUsersFromDifferentDomain();
|
|
||||||
}
|
|
||||||
}, [submitSucceeded]);
|
|
||||||
|
|
||||||
const styledRoles =
|
|
||||||
props.options && props.options.length > 0
|
|
||||||
? props.options
|
|
||||||
: props.roles.map((role: any) => {
|
|
||||||
return {
|
|
||||||
key: role.id,
|
|
||||||
value: role.name?.split(" - ")[0],
|
|
||||||
description: role.description,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
const allUsersProfiles = React.useMemo(
|
|
||||||
() =>
|
|
||||||
allUsers.map(
|
|
||||||
(user: {
|
|
||||||
userId: string;
|
|
||||||
username: string;
|
|
||||||
permissionGroupId: string;
|
|
||||||
permissionGroupName: string;
|
|
||||||
name: string;
|
|
||||||
}) => ({
|
|
||||||
...user,
|
|
||||||
initials: getInitialsFromName(user.name || user.username),
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
[allUsers],
|
|
||||||
);
|
|
||||||
|
|
||||||
const onSelect = (value: string, option?: any) => {
|
|
||||||
if (isMultiSelectDropdown) {
|
|
||||||
setSelectedOption((selectedOptions) => [...selectedOptions, option]);
|
|
||||||
} else {
|
|
||||||
setSelectedOption([option]);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const errorHandler = (error: string) => {
|
|
||||||
setEmailError(error);
|
|
||||||
};
|
|
||||||
|
|
||||||
const onRemoveOptions = (value: string, option?: any) => {
|
|
||||||
if (isMultiSelectDropdown) {
|
|
||||||
setSelectedOption((selectedOptions) =>
|
|
||||||
selectedOptions.filter((opt) => opt.value !== option.value),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const checkIfInvitedUsersFromDifferentDomain = async () => {
|
|
||||||
if (!currentUser?.email) return true;
|
|
||||||
|
|
||||||
const currentUserEmail = currentUser?.email;
|
|
||||||
const partnerProgramCalloutShown = await getPartnerProgramCalloutShown();
|
|
||||||
const currentUserDomain = getDomainFromEmail(currentUserEmail);
|
|
||||||
|
|
||||||
if (invitedEmails.current && !partnerProgramCalloutShown) {
|
|
||||||
const _emailOutsideCurrentDomain = invitedEmails.current.find(
|
|
||||||
(email) => getDomainFromEmail(email) !== currentUserDomain,
|
|
||||||
);
|
|
||||||
if (_emailOutsideCurrentDomain) {
|
|
||||||
emailOutsideCurrentDomain.current = _emailOutsideCurrentDomain;
|
|
||||||
invitedEmails.current = undefined;
|
|
||||||
setShowPartnerProgramCallout(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<WorkspaceInviteWrapper>
|
|
||||||
<StyledForm
|
|
||||||
onSubmit={handleSubmit((values: any, dispatch: any) => {
|
|
||||||
const roles = isMultiSelectDropdown
|
|
||||||
? selectedOption.map((option: any) => option.value).join(",")
|
|
||||||
: selectedOption[0].value;
|
|
||||||
validateFormValues({ ...values, role: roles });
|
|
||||||
const usersAsStringsArray = values.users.split(",");
|
|
||||||
// update state to show success message correctly
|
|
||||||
updateNumberOfUsersInvited(usersAsStringsArray.length);
|
|
||||||
const validEmails = usersAsStringsArray.filter((user: any) =>
|
|
||||||
isEmail(user),
|
|
||||||
);
|
|
||||||
const validEmailsString = validEmails.join(",");
|
|
||||||
invitedEmails.current = validEmails;
|
|
||||||
|
|
||||||
AnalyticsUtil.logEvent("INVITE_USER", {
|
|
||||||
...(!isFeatureEnabled ? { users: usersAsStringsArray } : {}),
|
|
||||||
role: roles,
|
|
||||||
numberOfUsersInvited: usersAsStringsArray.length,
|
|
||||||
orgId: props.workspaceId,
|
|
||||||
});
|
|
||||||
return inviteUsersToWorkspace(
|
|
||||||
{
|
|
||||||
...(props.workspaceId ? { workspaceId: props.workspaceId } : {}),
|
|
||||||
users: validEmailsString,
|
|
||||||
permissionGroupId: roles,
|
|
||||||
},
|
|
||||||
dispatch,
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<StyledInviteFieldGroup>
|
|
||||||
<div style={{ width: "60%" }}>
|
|
||||||
<TagListField
|
|
||||||
autofocus
|
|
||||||
className="ml-0.5"
|
|
||||||
customError={(err: string) => errorHandler(err)}
|
|
||||||
data-testid="t--invite-email-input"
|
|
||||||
intent="success"
|
|
||||||
label="Emails"
|
|
||||||
name="users"
|
|
||||||
placeholder={placeholder || "Enter email address(es)"}
|
|
||||||
type="email"
|
|
||||||
/>
|
|
||||||
{emailError && (
|
|
||||||
<ErrorTextContainer>
|
|
||||||
<Icon name="alert-line" size="sm" />
|
|
||||||
<Text kind="body-s" renderAs="p">
|
|
||||||
{emailError}
|
|
||||||
</Text>
|
|
||||||
</ErrorTextContainer>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div style={{ width: "40%" }}>
|
|
||||||
<Select
|
|
||||||
data-testid="t--invite-role-input"
|
|
||||||
getPopupContainer={(triggerNode) =>
|
|
||||||
triggerNode.parentNode.parentNode
|
|
||||||
}
|
|
||||||
isDisabled={disableDropdown}
|
|
||||||
isMultiSelect={isMultiSelectDropdown}
|
|
||||||
listHeight={400}
|
|
||||||
onDeselect={onRemoveOptions}
|
|
||||||
onSelect={onSelect}
|
|
||||||
optionLabelProp="label"
|
|
||||||
placeholder="Select a role"
|
|
||||||
value={selectedOption}
|
|
||||||
>
|
|
||||||
{styledRoles.map((role: any) => (
|
|
||||||
<Option key={role.key} label={role.value} value={role.key}>
|
|
||||||
<div className="flex items-center gap-1">
|
|
||||||
{isMultiSelectDropdown && (
|
|
||||||
<StyledCheckbox
|
|
||||||
isSelected={selectedOption.find(
|
|
||||||
(v) => v.key == role.key,
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<div className="flex flex-col gap-1">
|
|
||||||
<OptionLabel
|
|
||||||
color="var(--ads-v2-color-fg-emphasis)"
|
|
||||||
kind={role.description && "heading-xs"}
|
|
||||||
>
|
|
||||||
{role.value}
|
|
||||||
</OptionLabel>
|
|
||||||
{role.description && (
|
|
||||||
<Text kind="body-s">{role.description}</Text>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Option>
|
|
||||||
))}
|
|
||||||
{canShowRamp && (
|
|
||||||
<Option disabled>
|
|
||||||
<CustomRolesRamp />
|
|
||||||
</Option>
|
|
||||||
)}
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Button
|
|
||||||
className="t--invite-user-btn"
|
|
||||||
isDisabled={!valid || selectedOption.length === 0}
|
|
||||||
isLoading={submitting && !(submitFailed && !anyTouched)}
|
|
||||||
size="md"
|
|
||||||
type="submit"
|
|
||||||
>
|
|
||||||
Invite
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</StyledInviteFieldGroup>
|
|
||||||
<div className="flex items-start gap-2 mt-2">
|
|
||||||
<Icon className="mt-1" name="user-3-line" size="md" />
|
|
||||||
<WorkspaceText>
|
|
||||||
<InviteUserText isApplicationInvite={isApplicationInvite} />
|
|
||||||
</WorkspaceText>
|
|
||||||
</div>
|
|
||||||
{!cloudHosting &&
|
|
||||||
showPartnerProgramCallout &&
|
|
||||||
emailOutsideCurrentDomain.current && (
|
|
||||||
<div className="mt-2">
|
|
||||||
<PartnerProgramCallout
|
|
||||||
email={emailOutsideCurrentDomain.current}
|
|
||||||
onClose={() => {
|
|
||||||
setShowPartnerProgramCallout(false);
|
|
||||||
setPartnerProgramCalloutShown();
|
|
||||||
emailOutsideCurrentDomain.current = undefined;
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{isLoading ? (
|
|
||||||
<div className="pt-4 overflow-hidden">
|
|
||||||
<Spinner size="lg" />
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
{allUsers.length === 0 && (
|
|
||||||
<MailConfigContainer data-testid="no-users-content">
|
|
||||||
<NoEmailConfigImage />
|
|
||||||
<Text kind="action-s">{createMessage(NO_USERS_INVITED)}</Text>
|
|
||||||
</MailConfigContainer>
|
|
||||||
)}
|
|
||||||
{!disableUserList && (
|
|
||||||
<UserList ref={userRef}>
|
|
||||||
{allUsersProfiles.map(
|
|
||||||
(user: {
|
|
||||||
username: string;
|
|
||||||
name: string;
|
|
||||||
roles: WorkspaceUserRoles[];
|
|
||||||
initials: string;
|
|
||||||
photoId?: string;
|
|
||||||
}) => {
|
|
||||||
return (
|
|
||||||
<User key={user.username}>
|
|
||||||
<UserInfo>
|
|
||||||
<Avatar
|
|
||||||
firstLetter={user.initials}
|
|
||||||
image={
|
|
||||||
user.photoId
|
|
||||||
? `/api/${USER_PHOTO_ASSET_URL}/${user.photoId}`
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
isTooltipEnabled={false}
|
|
||||||
label={user.name || user.username}
|
|
||||||
/>
|
|
||||||
<UserName>
|
|
||||||
<Tooltip content={user.username} placement="top">
|
|
||||||
<Text
|
|
||||||
color="var(--ads-v2-color-fg)"
|
|
||||||
kind="heading-xs"
|
|
||||||
>
|
|
||||||
{user.name}
|
|
||||||
</Text>
|
|
||||||
</Tooltip>
|
|
||||||
</UserName>
|
|
||||||
</UserInfo>
|
|
||||||
<UserRole>
|
|
||||||
<Text kind="action-m">
|
|
||||||
{user.roles?.[0]?.name?.split(" - ")[0] || ""}
|
|
||||||
</Text>
|
|
||||||
</UserRole>
|
|
||||||
</User>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
)}
|
|
||||||
</UserList>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
<ErrorBox message={submitFailed}>
|
|
||||||
{submitFailed && error && <Callout kind="error">{error}</Callout>}
|
|
||||||
</ErrorBox>
|
|
||||||
{canManage && !disableManageUsers && (
|
|
||||||
<ManageUsersContainer>
|
|
||||||
<ManageUsers workspaceId={props.workspaceId} />
|
|
||||||
</ManageUsersContainer>
|
|
||||||
)}
|
|
||||||
</StyledForm>
|
|
||||||
</WorkspaceInviteWrapper>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export const mapStateToProps = (
|
|
||||||
state: AppState,
|
|
||||||
{ formName }: { formName?: string },
|
|
||||||
) => ({
|
|
||||||
roles: getRolesForField(state),
|
|
||||||
allUsers: getAllUsers(state),
|
|
||||||
isLoading: state.ui.workspaces.loadingStates.isFetchAllUsers,
|
|
||||||
form: formName || INVITE_USERS_TO_WORKSPACE_FORM,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const mapDispatchToProps = (dispatch: any) => ({
|
|
||||||
fetchAllRoles: (workspaceId: string) =>
|
|
||||||
dispatch(fetchRolesForWorkspace(workspaceId)),
|
|
||||||
fetchCurrentWorkspace: (workspaceId: string) =>
|
|
||||||
dispatch(fetchWorkspace(workspaceId)),
|
|
||||||
fetchUser: (workspaceId: string) =>
|
|
||||||
dispatch(fetchUsersForWorkspace(workspaceId)),
|
|
||||||
});
|
|
||||||
|
|
||||||
export default connect(
|
|
||||||
mapStateToProps,
|
|
||||||
mapDispatchToProps,
|
|
||||||
)(
|
|
||||||
reduxForm<
|
|
||||||
InviteUsersToWorkspaceFormValues,
|
|
||||||
{
|
|
||||||
roles?: any;
|
|
||||||
applicationId?: string;
|
|
||||||
workspaceId?: string;
|
|
||||||
isApplicationInvite?: boolean;
|
|
||||||
placeholder?: string;
|
|
||||||
customProps?: any;
|
|
||||||
selected?: any;
|
|
||||||
options?: any;
|
|
||||||
isMultiSelectDropdown?: boolean;
|
|
||||||
}
|
|
||||||
>({
|
|
||||||
validate,
|
|
||||||
})(WorkspaceInviteUsersForm),
|
|
||||||
);
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { ReduxActionTypes } from "@appsmith/constants/ReduxActionConstants";
|
import { ReduxActionTypes } from "@appsmith/constants/ReduxActionConstants";
|
||||||
import { SubmissionError } from "redux-form";
|
import { SubmissionError } from "redux-form";
|
||||||
import type { RouteChildrenProps, RouteComponentProps } from "react-router-dom";
|
import type { RouteChildrenProps, RouteComponentProps } from "react-router-dom";
|
||||||
|
import type { DefaultOptionType } from "rc-select/lib/Select";
|
||||||
export type InviteUsersToWorkspaceByRoleValues = {
|
export type InviteUsersToWorkspaceByRoleValues = {
|
||||||
id: string;
|
id: string;
|
||||||
users?: string;
|
users?: string;
|
||||||
|
|
@ -12,6 +13,19 @@ export type InviteUsersToWorkspaceFormValues = {
|
||||||
usersByRole: InviteUsersToWorkspaceByRoleValues[];
|
usersByRole: InviteUsersToWorkspaceByRoleValues[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type InviteUsersProps = {
|
||||||
|
roles?: DefaultOptionType[];
|
||||||
|
applicationId?: string;
|
||||||
|
workspaceId?: string;
|
||||||
|
isApplicationPage?: boolean;
|
||||||
|
placeholder?: string;
|
||||||
|
customProps?: any;
|
||||||
|
selected?: any;
|
||||||
|
options?: any;
|
||||||
|
isMultiSelectDropdown?: boolean;
|
||||||
|
checkIfInvitedUsersFromDifferentDomain?: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
export type CreateWorkspaceFormValues = {
|
export type CreateWorkspaceFormValues = {
|
||||||
name: string;
|
name: string;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -52,6 +52,10 @@ export const initialState: ApplicationsReduxState = {
|
||||||
isUploadingNavigationLogo: false,
|
isUploadingNavigationLogo: false,
|
||||||
isDeletingNavigationLogo: false,
|
isDeletingNavigationLogo: false,
|
||||||
deletingMultipleApps: {},
|
deletingMultipleApps: {},
|
||||||
|
loadingStates: {
|
||||||
|
isFetchingAllRoles: false,
|
||||||
|
isFetchingAllUsers: false,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const handlers = {
|
export const handlers = {
|
||||||
|
|
@ -850,6 +854,10 @@ export interface ApplicationsReduxState {
|
||||||
isUploadingNavigationLogo: boolean;
|
isUploadingNavigationLogo: boolean;
|
||||||
isDeletingNavigationLogo: boolean;
|
isDeletingNavigationLogo: boolean;
|
||||||
deletingMultipleApps: DeletingMultipleApps;
|
deletingMultipleApps: DeletingMultipleApps;
|
||||||
|
loadingStates: {
|
||||||
|
isFetchingAllRoles: boolean;
|
||||||
|
isFetchingAllUsers: boolean;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Application {
|
export interface Application {
|
||||||
|
|
|
||||||
|
|
@ -278,3 +278,9 @@ export const selectEvaluationVersion = (state: AppState) =>
|
||||||
export const getDeletingMultipleApps = (state: AppState) => {
|
export const getDeletingMultipleApps = (state: AppState) => {
|
||||||
return state.ui.applications.deletingMultipleApps;
|
return state.ui.applications.deletingMultipleApps;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getApplicationLoadingStates = (state: AppState) => {
|
||||||
|
return state.ui.applications?.loadingStates;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getAllAppUsers = () => [];
|
||||||
|
|
|
||||||
3
app/client/src/ee/pages/workspace/InviteUsersForm.tsx
Normal file
3
app/client/src/ee/pages/workspace/InviteUsersForm.tsx
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
export * from "ce/pages/workspace/InviteUsersForm";
|
||||||
|
import { default as CE_Invite_Users_Form } from "ce/pages/workspace/InviteUsersForm";
|
||||||
|
export default CE_Invite_Users_Form;
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
export * from "ce/pages/workspace/WorkspaceInviteUsersForm";
|
|
||||||
import { default as CE_Workspace_Invite_Users_Form } from "ce/pages/workspace/WorkspaceInviteUsersForm";
|
|
||||||
export default CE_Workspace_Invite_Users_Form;
|
|
||||||
|
|
@ -67,8 +67,6 @@ const Description = styled.span<{ addLeftSpacing?: boolean }>`
|
||||||
|
|
||||||
padding-left: ${(props) => (props.addLeftSpacing ? `20px` : "0")};
|
padding-left: ${(props) => (props.addLeftSpacing ? `20px` : "0")};
|
||||||
margin-top: var(--ads-v2-spaces-2);
|
margin-top: var(--ads-v2-spaces-2);
|
||||||
flex: 1;
|
|
||||||
display: flex;
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const UpperContent = styled.div`
|
const UpperContent = styled.div`
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ import {
|
||||||
isPermitted,
|
isPermitted,
|
||||||
PERMISSION_TYPE,
|
PERMISSION_TYPE,
|
||||||
} from "@appsmith/utils/permissionHelpers";
|
} from "@appsmith/utils/permissionHelpers";
|
||||||
import WorkspaceInviteUsersForm from "@appsmith/pages/workspace/WorkspaceInviteUsersForm";
|
import WorkspaceInviteUsersForm from "pages/workspace/WorkspaceInviteUsersForm";
|
||||||
import { getCurrentUser } from "selectors/usersSelectors";
|
import { getCurrentUser } from "selectors/usersSelectors";
|
||||||
import { ANONYMOUS_USERNAME } from "constants/userConstants";
|
import { ANONYMOUS_USERNAME } from "constants/userConstants";
|
||||||
import { viewerURL } from "RouteBuilder";
|
import { viewerURL } from "RouteBuilder";
|
||||||
|
|
@ -112,7 +112,7 @@ function AppInviteUsersForm(props: any) {
|
||||||
{canInviteToApplication && (
|
{canInviteToApplication && (
|
||||||
<WorkspaceInviteUsersForm
|
<WorkspaceInviteUsersForm
|
||||||
applicationId={applicationId}
|
applicationId={applicationId}
|
||||||
isApplicationInvite
|
isApplicationPage
|
||||||
placeholder={createMessage(INVITE_USERS_PLACEHOLDER, !isGACEnabled)}
|
placeholder={createMessage(INVITE_USERS_PLACEHOLDER, !isGACEnabled)}
|
||||||
workspaceId={props.workspaceId}
|
workspaceId={props.workspaceId}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
293
app/client/src/pages/workspace/WorkspaceInviteUsersForm.tsx
Normal file
293
app/client/src/pages/workspace/WorkspaceInviteUsersForm.tsx
Normal file
|
|
@ -0,0 +1,293 @@
|
||||||
|
import React, { useState, useRef } from "react";
|
||||||
|
import styled from "styled-components";
|
||||||
|
import { useSelector } from "react-redux";
|
||||||
|
import {
|
||||||
|
getAllUsers,
|
||||||
|
getCurrentAppWorkspace,
|
||||||
|
getWorkspaceLoadingStates,
|
||||||
|
} from "@appsmith/selectors/workspaceSelectors";
|
||||||
|
import { createMessage, NO_USERS_INVITED } from "@appsmith/constants/messages";
|
||||||
|
import {
|
||||||
|
isPermitted,
|
||||||
|
PERMISSION_TYPE,
|
||||||
|
} from "@appsmith/utils/permissionHelpers";
|
||||||
|
import { getAppsmithConfigs } from "@appsmith/configs";
|
||||||
|
import { Avatar, Icon, Spinner, Text, Tooltip } from "design-system";
|
||||||
|
import { getInitialsFromName } from "utils/AppsmithUtils";
|
||||||
|
import ManageUsers from "pages/workspace/ManageUsers";
|
||||||
|
import { USER_PHOTO_ASSET_URL } from "constants/userConstants";
|
||||||
|
import { importSvg } from "design-system-old";
|
||||||
|
import type { WorkspaceUserRoles } from "@appsmith/constants/workspaceConstants";
|
||||||
|
import { getDomainFromEmail } from "utils/helpers";
|
||||||
|
import { getCurrentUser } from "selectors/usersSelectors";
|
||||||
|
import PartnerProgramCallout from "pages/workspace/PartnerProgramCallout";
|
||||||
|
import {
|
||||||
|
getPartnerProgramCalloutShown,
|
||||||
|
setPartnerProgramCalloutShown,
|
||||||
|
} from "utils/storage";
|
||||||
|
import InviteUsersForm from "@appsmith/pages/workspace/InviteUsersForm";
|
||||||
|
import { ENTITY_TYPE } from "@appsmith/constants/workspaceConstants";
|
||||||
|
import {
|
||||||
|
getAllAppUsers,
|
||||||
|
getApplicationLoadingStates,
|
||||||
|
} from "@appsmith/selectors/applicationSelectors";
|
||||||
|
import { FEATURE_FLAG } from "@appsmith/entities/FeatureFlag";
|
||||||
|
import { useFeatureFlag } from "utils/hooks/useFeatureFlag";
|
||||||
|
|
||||||
|
const NoEmailConfigImage = importSvg(
|
||||||
|
() => import("assets/images/email-not-configured.svg"),
|
||||||
|
);
|
||||||
|
|
||||||
|
const { cloudHosting } = getAppsmithConfigs();
|
||||||
|
|
||||||
|
export const WorkspaceInviteWrapper = styled.div``;
|
||||||
|
|
||||||
|
export const UserList = styled.div`
|
||||||
|
margin-top: 10px;
|
||||||
|
max-height: 260px;
|
||||||
|
overflow-y: auto;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-left: 0.1rem;
|
||||||
|
|
||||||
|
.user-icons {
|
||||||
|
width: 32px;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const User = styled.div`
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 54px;
|
||||||
|
justify-content: space-between;
|
||||||
|
border-bottom: 1px solid var(--ads-v2-color-border);
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const UserInfo = styled.div`
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
div:first-child {
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const UserRole = styled.div`
|
||||||
|
span {
|
||||||
|
word-break: break-word;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const UserName = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
margin: 0 10px;
|
||||||
|
span {
|
||||||
|
word-break: break-word;
|
||||||
|
|
||||||
|
&:nth-child(1) {
|
||||||
|
margin-bottom: 1px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const MailConfigContainer = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 24px 4px;
|
||||||
|
padding-bottom: 0;
|
||||||
|
align-items: center;
|
||||||
|
&& > span {
|
||||||
|
color: var(--ads-v2-color-fg);
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const ManageUsersContainer = styled.div`
|
||||||
|
padding: 12px 0;
|
||||||
|
`;
|
||||||
|
|
||||||
|
function WorkspaceInviteUsers(props: any) {
|
||||||
|
const isFeatureEnabled = useFeatureFlag(FEATURE_FLAG.license_gac_enabled);
|
||||||
|
const userRef = React.createRef<HTMLDivElement>();
|
||||||
|
const currentUser = useSelector(getCurrentUser);
|
||||||
|
const currentWorkspace = useSelector(getCurrentAppWorkspace);
|
||||||
|
const showAppLevelInviteModal =
|
||||||
|
(isFeatureEnabled && props.isApplicationPage) || false;
|
||||||
|
const allUsers = useSelector(
|
||||||
|
showAppLevelInviteModal ? getAllAppUsers : getAllUsers,
|
||||||
|
);
|
||||||
|
const isLoading: boolean =
|
||||||
|
useSelector(
|
||||||
|
showAppLevelInviteModal
|
||||||
|
? getApplicationLoadingStates
|
||||||
|
: getWorkspaceLoadingStates,
|
||||||
|
)?.isFetchingAllUsers || false;
|
||||||
|
|
||||||
|
const emailOutsideCurrentDomain = useRef<undefined | string>();
|
||||||
|
const [showPartnerProgramCallout, setShowPartnerProgramCallout] =
|
||||||
|
useState(false);
|
||||||
|
|
||||||
|
const userWorkspacePermissions = currentWorkspace?.userPermissions ?? [];
|
||||||
|
const canManage = isPermitted(
|
||||||
|
userWorkspacePermissions,
|
||||||
|
PERMISSION_TYPE.MANAGE_WORKSPACE,
|
||||||
|
);
|
||||||
|
|
||||||
|
const allUsersProfiles = React.useMemo(
|
||||||
|
() =>
|
||||||
|
allUsers.map(
|
||||||
|
(user: {
|
||||||
|
userId: string;
|
||||||
|
username: string;
|
||||||
|
permissionGroupId: string;
|
||||||
|
permissionGroupName: string;
|
||||||
|
name: string;
|
||||||
|
roles: WorkspaceUserRoles[];
|
||||||
|
userGroupId?: string;
|
||||||
|
}) => ({
|
||||||
|
...user,
|
||||||
|
initials: getInitialsFromName(user.name || user.username),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
[allUsers],
|
||||||
|
);
|
||||||
|
|
||||||
|
const checkIfInvitedUsersFromDifferentDomain = async (
|
||||||
|
invitedEmails?: string[],
|
||||||
|
) => {
|
||||||
|
if (!currentUser?.email) return true;
|
||||||
|
|
||||||
|
const currentUserEmail = currentUser?.email;
|
||||||
|
const partnerProgramCalloutShown = await getPartnerProgramCalloutShown();
|
||||||
|
const currentUserDomain = getDomainFromEmail(currentUserEmail);
|
||||||
|
|
||||||
|
if (invitedEmails && !partnerProgramCalloutShown) {
|
||||||
|
const _emailOutsideCurrentDomain = invitedEmails.find(
|
||||||
|
(email) => getDomainFromEmail(email) !== currentUserDomain,
|
||||||
|
);
|
||||||
|
if (_emailOutsideCurrentDomain) {
|
||||||
|
emailOutsideCurrentDomain.current = _emailOutsideCurrentDomain;
|
||||||
|
invitedEmails = undefined;
|
||||||
|
setShowPartnerProgramCallout(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<WorkspaceInviteWrapper>
|
||||||
|
<InviteUsersForm
|
||||||
|
{...props}
|
||||||
|
checkIfInvitedUsersFromDifferentDomain={
|
||||||
|
checkIfInvitedUsersFromDifferentDomain
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
{!cloudHosting &&
|
||||||
|
showPartnerProgramCallout &&
|
||||||
|
emailOutsideCurrentDomain.current && (
|
||||||
|
<div className="mt-2">
|
||||||
|
<PartnerProgramCallout
|
||||||
|
email={emailOutsideCurrentDomain.current}
|
||||||
|
onClose={() => {
|
||||||
|
setShowPartnerProgramCallout(false);
|
||||||
|
setPartnerProgramCalloutShown();
|
||||||
|
emailOutsideCurrentDomain.current = undefined;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{isLoading ? (
|
||||||
|
<div className="pt-4 overflow-hidden">
|
||||||
|
<Spinner size="lg" />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{allUsers.length === 0 && (
|
||||||
|
<MailConfigContainer data-testid="no-users-content">
|
||||||
|
<NoEmailConfigImage />
|
||||||
|
<Text kind="action-s">{createMessage(NO_USERS_INVITED)}</Text>
|
||||||
|
</MailConfigContainer>
|
||||||
|
)}
|
||||||
|
<UserList ref={userRef}>
|
||||||
|
{allUsersProfiles.map(
|
||||||
|
(user: {
|
||||||
|
username: string;
|
||||||
|
name: string;
|
||||||
|
roles: WorkspaceUserRoles[];
|
||||||
|
initials: string;
|
||||||
|
photoId?: string;
|
||||||
|
userId: string;
|
||||||
|
userGroupId?: string;
|
||||||
|
}) => {
|
||||||
|
const showUser =
|
||||||
|
(showAppLevelInviteModal
|
||||||
|
? user.roles?.[0]?.entityType === ENTITY_TYPE.APPLICATION
|
||||||
|
: user.roles?.[0]?.entityType === ENTITY_TYPE.WORKSPACE) &&
|
||||||
|
user.roles?.[0]?.id;
|
||||||
|
return showUser ? (
|
||||||
|
<User
|
||||||
|
key={user?.userGroupId ? user.userGroupId : user.username}
|
||||||
|
>
|
||||||
|
<UserInfo>
|
||||||
|
{user?.userGroupId ? (
|
||||||
|
<>
|
||||||
|
<Icon
|
||||||
|
className="user-icons"
|
||||||
|
name="group-line"
|
||||||
|
size="lg"
|
||||||
|
/>
|
||||||
|
<UserName>
|
||||||
|
<Text
|
||||||
|
color="var(--ads-v2-color-fg)"
|
||||||
|
kind="heading-xs"
|
||||||
|
>
|
||||||
|
{user.name}
|
||||||
|
</Text>
|
||||||
|
</UserName>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Avatar
|
||||||
|
firstLetter={user.initials}
|
||||||
|
image={
|
||||||
|
user.photoId
|
||||||
|
? `/api/${USER_PHOTO_ASSET_URL}/${user.photoId}`
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
isTooltipEnabled={false}
|
||||||
|
label={user.name || user.username}
|
||||||
|
/>
|
||||||
|
<UserName>
|
||||||
|
<Tooltip content={user.username} placement="top">
|
||||||
|
<Text
|
||||||
|
color="var(--ads-v2-color-fg)"
|
||||||
|
kind="heading-xs"
|
||||||
|
>
|
||||||
|
{user.name}
|
||||||
|
</Text>
|
||||||
|
</Tooltip>
|
||||||
|
</UserName>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</UserInfo>
|
||||||
|
<UserRole>
|
||||||
|
<Text kind="action-m">
|
||||||
|
{user.roles?.[0]?.name?.split(" - ")[0] || ""}
|
||||||
|
</Text>
|
||||||
|
</UserRole>
|
||||||
|
</User>
|
||||||
|
) : null;
|
||||||
|
},
|
||||||
|
)}
|
||||||
|
</UserList>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{canManage && (
|
||||||
|
<ManageUsersContainer>
|
||||||
|
<ManageUsers workspaceId={props.workspaceId} />
|
||||||
|
</ManageUsersContainer>
|
||||||
|
)}
|
||||||
|
</WorkspaceInviteWrapper>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default WorkspaceInviteUsers;
|
||||||
|
|
@ -18,7 +18,7 @@ import { getAllApplications } from "@appsmith/actions/applicationActions";
|
||||||
import { useMediaQuery } from "react-responsive";
|
import { useMediaQuery } from "react-responsive";
|
||||||
import { BackButton, StickyHeader } from "components/utils/helperComponents";
|
import { BackButton, StickyHeader } from "components/utils/helperComponents";
|
||||||
import { debounce } from "lodash";
|
import { debounce } from "lodash";
|
||||||
import WorkspaceInviteUsersForm from "@appsmith/pages/workspace/WorkspaceInviteUsersForm";
|
import WorkspaceInviteUsersForm from "pages/workspace/WorkspaceInviteUsersForm";
|
||||||
import { SettingsPageHeader } from "./SettingsPageHeader";
|
import { SettingsPageHeader } from "./SettingsPageHeader";
|
||||||
import { navigateToTab } from "@appsmith/pages/workspace/helpers";
|
import { navigateToTab } from "@appsmith/pages/workspace/helpers";
|
||||||
import {
|
import {
|
||||||
|
|
|
||||||
|
|
@ -10332,7 +10332,7 @@ __metadata:
|
||||||
dayjs: ^1.10.6
|
dayjs: ^1.10.6
|
||||||
deep-diff: ^1.0.2
|
deep-diff: ^1.0.2
|
||||||
design-system: "npm:@appsmithorg/design-system@2.1.21"
|
design-system: "npm:@appsmithorg/design-system@2.1.21"
|
||||||
design-system-old: "npm:@appsmithorg/design-system-old@1.1.11"
|
design-system-old: "npm:@appsmithorg/design-system-old@1.1.12"
|
||||||
diff: ^5.0.0
|
diff: ^5.0.0
|
||||||
dotenv: ^8.1.0
|
dotenv: ^8.1.0
|
||||||
downloadjs: ^1.4.7
|
downloadjs: ^1.4.7
|
||||||
|
|
@ -14329,9 +14329,9 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"design-system-old@npm:@appsmithorg/design-system-old@1.1.11":
|
"design-system-old@npm:@appsmithorg/design-system-old@1.1.12":
|
||||||
version: 1.1.11
|
version: 1.1.12
|
||||||
resolution: "@appsmithorg/design-system-old@npm:1.1.11"
|
resolution: "@appsmithorg/design-system-old@npm:1.1.12"
|
||||||
dependencies:
|
dependencies:
|
||||||
emoji-mart: 3.0.1
|
emoji-mart: 3.0.1
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
|
|
@ -14351,7 +14351,7 @@ __metadata:
|
||||||
remixicon-react: ^1.0.0
|
remixicon-react: ^1.0.0
|
||||||
styled-components: 5.3.6
|
styled-components: 5.3.6
|
||||||
tinycolor2: ^1.4.2
|
tinycolor2: ^1.4.2
|
||||||
checksum: 968fc1be2ded862c2cac3bc8ae8eab6642d16ffda7b586aeb87ed69d1bad08ee5e3d1c05098adfb9e80f9c1a6fa6d4bc05d0cd464b700aa39005a5bb6c63b7ca
|
checksum: 2232dee3caa17e1735de8a3a63290ad6e86b929c2bb7848c09b6ddc71ecea633eb0ea51cd6172d8402d5ecbef931eee53ef8cc6a32e0873cba5ad943a034c845
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user