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",
|
||||
"deep-diff": "^1.0.2",
|
||||
"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",
|
||||
"echarts": "^5.4.2",
|
||||
"echarts-gl": "^2.0.9",
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ import type { ApplicationPayload } from "@appsmith/constants/ReduxActionConstant
|
|||
import { ReduxActionTypes } from "@appsmith/constants/ReduxActionConstants";
|
||||
import PageWrapper from "pages/common/PageWrapper";
|
||||
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 { getCurrentUser } from "selectors/usersSelectors";
|
||||
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,
|
||||
} from "@appsmith/utils/permissionHelpers";
|
||||
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 { RAMP_NAME } from "utils/ProductRamps/RampsControlList";
|
||||
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 { SubmissionError } from "redux-form";
|
||||
import type { RouteChildrenProps, RouteComponentProps } from "react-router-dom";
|
||||
import type { DefaultOptionType } from "rc-select/lib/Select";
|
||||
export type InviteUsersToWorkspaceByRoleValues = {
|
||||
id: string;
|
||||
users?: string;
|
||||
|
|
@ -12,6 +13,19 @@ export type InviteUsersToWorkspaceFormValues = {
|
|||
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 = {
|
||||
name: string;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -52,6 +52,10 @@ export const initialState: ApplicationsReduxState = {
|
|||
isUploadingNavigationLogo: false,
|
||||
isDeletingNavigationLogo: false,
|
||||
deletingMultipleApps: {},
|
||||
loadingStates: {
|
||||
isFetchingAllRoles: false,
|
||||
isFetchingAllUsers: false,
|
||||
},
|
||||
};
|
||||
|
||||
export const handlers = {
|
||||
|
|
@ -850,6 +854,10 @@ export interface ApplicationsReduxState {
|
|||
isUploadingNavigationLogo: boolean;
|
||||
isDeletingNavigationLogo: boolean;
|
||||
deletingMultipleApps: DeletingMultipleApps;
|
||||
loadingStates: {
|
||||
isFetchingAllRoles: boolean;
|
||||
isFetchingAllUsers: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export interface Application {
|
||||
|
|
|
|||
|
|
@ -278,3 +278,9 @@ export const selectEvaluationVersion = (state: AppState) =>
|
|||
export const getDeletingMultipleApps = (state: AppState) => {
|
||||
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")};
|
||||
margin-top: var(--ads-v2-spaces-2);
|
||||
flex: 1;
|
||||
display: flex;
|
||||
`;
|
||||
|
||||
const UpperContent = styled.div`
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import {
|
|||
isPermitted,
|
||||
PERMISSION_TYPE,
|
||||
} from "@appsmith/utils/permissionHelpers";
|
||||
import WorkspaceInviteUsersForm from "@appsmith/pages/workspace/WorkspaceInviteUsersForm";
|
||||
import WorkspaceInviteUsersForm from "pages/workspace/WorkspaceInviteUsersForm";
|
||||
import { getCurrentUser } from "selectors/usersSelectors";
|
||||
import { ANONYMOUS_USERNAME } from "constants/userConstants";
|
||||
import { viewerURL } from "RouteBuilder";
|
||||
|
|
@ -112,7 +112,7 @@ function AppInviteUsersForm(props: any) {
|
|||
{canInviteToApplication && (
|
||||
<WorkspaceInviteUsersForm
|
||||
applicationId={applicationId}
|
||||
isApplicationInvite
|
||||
isApplicationPage
|
||||
placeholder={createMessage(INVITE_USERS_PLACEHOLDER, !isGACEnabled)}
|
||||
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 { BackButton, StickyHeader } from "components/utils/helperComponents";
|
||||
import { debounce } from "lodash";
|
||||
import WorkspaceInviteUsersForm from "@appsmith/pages/workspace/WorkspaceInviteUsersForm";
|
||||
import WorkspaceInviteUsersForm from "pages/workspace/WorkspaceInviteUsersForm";
|
||||
import { SettingsPageHeader } from "./SettingsPageHeader";
|
||||
import { navigateToTab } from "@appsmith/pages/workspace/helpers";
|
||||
import {
|
||||
|
|
|
|||
|
|
@ -10332,7 +10332,7 @@ __metadata:
|
|||
dayjs: ^1.10.6
|
||||
deep-diff: ^1.0.2
|
||||
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
|
||||
dotenv: ^8.1.0
|
||||
downloadjs: ^1.4.7
|
||||
|
|
@ -14329,9 +14329,9 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"design-system-old@npm:@appsmithorg/design-system-old@1.1.11":
|
||||
version: 1.1.11
|
||||
resolution: "@appsmithorg/design-system-old@npm:1.1.11"
|
||||
"design-system-old@npm:@appsmithorg/design-system-old@1.1.12":
|
||||
version: 1.1.12
|
||||
resolution: "@appsmithorg/design-system-old@npm:1.1.12"
|
||||
dependencies:
|
||||
emoji-mart: 3.0.1
|
||||
peerDependencies:
|
||||
|
|
@ -14351,7 +14351,7 @@ __metadata:
|
|||
remixicon-react: ^1.0.0
|
||||
styled-components: 5.3.6
|
||||
tinycolor2: ^1.4.2
|
||||
checksum: 968fc1be2ded862c2cac3bc8ae8eab6642d16ffda7b586aeb87ed69d1bad08ee5e3d1c05098adfb9e80f9c1a6fa6d4bc05d0cd464b700aa39005a5bb6c63b7ca
|
||||
checksum: 2232dee3caa17e1735de8a3a63290ad6e86b929c2bb7848c09b6ddc71ecea633eb0ea51cd6172d8402d5ecbef931eee53ef8cc6a32e0873cba5ad943a034c845
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user