diff --git a/app/client/package.json b/app/client/package.json index e5b6f1dc09..5ec3ce20c1 100644 --- a/app/client/package.json +++ b/app/client/package.json @@ -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", diff --git a/app/client/src/ce/pages/Applications/index.tsx b/app/client/src/ce/pages/Applications/index.tsx index 444af92f18..c4d25d2d6f 100644 --- a/app/client/src/ce/pages/Applications/index.tsx +++ b/app/client/src/ce/pages/Applications/index.tsx @@ -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"; diff --git a/app/client/src/ce/pages/workspace/InviteUsersForm.tsx b/app/client/src/ce/pages/workspace/InviteUsersForm.tsx new file mode 100644 index 0000000000..1d960c5474 --- /dev/null +++ b/app/client/src/ce/pages/workspace/InviteUsersForm.tsx @@ -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[]; +}) => { + 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 ( + + {canShowRamp && isApplicationPage ? ( + <> + {createMessage(INVITE_USER_RAMP_TEXT)} + + {createMessage(BUSINESS_EDITION_TEXT)} + + + ) : ( + createMessage(USERS_HAVE_ACCESS_TO_ALL_APPS) + )} + + ); +} + +export function CustomRolesRamp() { + const [dynamicProps, setDynamicProps] = useState({}); + const rampLinkSelector = getRampLink({ + section: RampSection.WorkspaceShare, + feature: RampFeature.Gac, + }); + const rampLink = useSelector(rampLinkSelector); + const rampText = ( + + {createMessage(CUSTOM_ROLES_RAMP_TEXT)}{" "} + { + 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)} + + + ); + return ( + +
+
+ + {createMessage(CUSTOM_ROLE_TEXT)} + + +
+ + {createMessage(CUSTOM_ROLE_DISABLED_OPTION_TEXT)} + +
+
+ ); +} + +function InviteUsersForm(props: any) { + const [emailError, setEmailError] = useState(""); + const [selectedOption, setSelectedOption] = useState([]); + 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(); + + 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 ( + { + 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, + ); + })} + > + +
+ errorHandler(err)} + data-testid="t--invite-email-input" + intent="success" + label="Emails" + name="users" + placeholder={placeholder || "Enter email address(es)"} + type="email" + /> + {emailError && ( + + + + {emailError} + + + )} +
+
+ +
+
+ +
+
+ {!isAclFlow && ( +
+ + + + +
+ )} + + {submitFailed && error && {error}} + +
+ ); +} + +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({ + validate, + })(InviteUsersForm), +); diff --git a/app/client/src/ce/pages/workspace/Members.tsx b/app/client/src/ce/pages/workspace/Members.tsx index 343d272f7d..4af0e0f4bb 100644 --- a/app/client/src/ce/pages/workspace/Members.tsx +++ b/app/client/src/ce/pages/workspace/Members.tsx @@ -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"; diff --git a/app/client/src/ce/pages/workspace/WorkspaceInviteUsersForm.tsx b/app/client/src/ce/pages/workspace/WorkspaceInviteUsersForm.tsx deleted file mode 100644 index f4c15f293e..0000000000 --- a/app/client/src/ce/pages/workspace/WorkspaceInviteUsersForm.tsx +++ /dev/null @@ -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[]; -}) => { - 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 ( - - {canShowRamp && isApplicationInvite ? ( - <> - {createMessage(INVITE_USER_RAMP_TEXT)} - - {createMessage(BUSINESS_EDITION_TEXT)} - - - ) : ( - createMessage(USERS_HAVE_ACCESS_TO_ALL_APPS) - )} - - ); -} - -export function CustomRolesRamp() { - const [dynamicProps, setDynamicProps] = useState({}); - const rampLinkSelector = getRampLink({ - section: RampSection.WorkspaceShare, - feature: RampFeature.Gac, - }); - const rampLink = useSelector(rampLinkSelector); - const rampText = ( - - {createMessage(CUSTOM_ROLES_RAMP_TEXT)}{" "} - { - 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)} - - - ); - return ( - -
-
- - {createMessage(CUSTOM_ROLE_TEXT)} - - -
- - {createMessage(CUSTOM_ROLE_DISABLED_OPTION_TEXT)} - -
-
- ); -} - -function WorkspaceInviteUsersForm(props: any) { - const [emailError, setEmailError] = useState(""); - const [selectedOption, setSelectedOption] = useState([]); - const userRef = React.createRef(); - // 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(); - const emailOutsideCurrentDomain = useRef(); - 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 ( - - { - 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, - ); - })} - > - -
- errorHandler(err)} - data-testid="t--invite-email-input" - intent="success" - label="Emails" - name="users" - placeholder={placeholder || "Enter email address(es)"} - type="email" - /> - {emailError && ( - - - - {emailError} - - - )} -
-
- -
-
- -
-
-
- - - - -
- {!cloudHosting && - showPartnerProgramCallout && - emailOutsideCurrentDomain.current && ( -
- { - setShowPartnerProgramCallout(false); - setPartnerProgramCalloutShown(); - emailOutsideCurrentDomain.current = undefined; - }} - /> -
- )} - {isLoading ? ( -
- -
- ) : ( - <> - {allUsers.length === 0 && ( - - - {createMessage(NO_USERS_INVITED)} - - )} - {!disableUserList && ( - - {allUsersProfiles.map( - (user: { - username: string; - name: string; - roles: WorkspaceUserRoles[]; - initials: string; - photoId?: string; - }) => { - return ( - - - - - - - {user.name} - - - - - - - {user.roles?.[0]?.name?.split(" - ")[0] || ""} - - - - ); - }, - )} - - )} - - )} - - {submitFailed && error && {error}} - - {canManage && !disableManageUsers && ( - - - - )} -
-
- ); -} - -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), -); diff --git a/app/client/src/ce/pages/workspace/helpers.ts b/app/client/src/ce/pages/workspace/helpers.ts index 71ec62a351..b6b24a7edb 100644 --- a/app/client/src/ce/pages/workspace/helpers.ts +++ b/app/client/src/ce/pages/workspace/helpers.ts @@ -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; }; diff --git a/app/client/src/ce/reducers/uiReducers/applicationsReducer.tsx b/app/client/src/ce/reducers/uiReducers/applicationsReducer.tsx index a9e09294d3..f96cff7052 100644 --- a/app/client/src/ce/reducers/uiReducers/applicationsReducer.tsx +++ b/app/client/src/ce/reducers/uiReducers/applicationsReducer.tsx @@ -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 { diff --git a/app/client/src/ce/selectors/applicationSelectors.tsx b/app/client/src/ce/selectors/applicationSelectors.tsx index 0e2930a309..11c53605f1 100644 --- a/app/client/src/ce/selectors/applicationSelectors.tsx +++ b/app/client/src/ce/selectors/applicationSelectors.tsx @@ -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 = () => []; diff --git a/app/client/src/ee/pages/workspace/InviteUsersForm.tsx b/app/client/src/ee/pages/workspace/InviteUsersForm.tsx new file mode 100644 index 0000000000..ace73cb9a7 --- /dev/null +++ b/app/client/src/ee/pages/workspace/InviteUsersForm.tsx @@ -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; diff --git a/app/client/src/ee/pages/workspace/WorkspaceInviteUsersForm.tsx b/app/client/src/ee/pages/workspace/WorkspaceInviteUsersForm.tsx deleted file mode 100644 index 0d83193f87..0000000000 --- a/app/client/src/ee/pages/workspace/WorkspaceInviteUsersForm.tsx +++ /dev/null @@ -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; diff --git a/app/client/src/pages/Editor/GuidedTour/Guide.tsx b/app/client/src/pages/Editor/GuidedTour/Guide.tsx index 808337b4e6..ee5cb493ca 100644 --- a/app/client/src/pages/Editor/GuidedTour/Guide.tsx +++ b/app/client/src/pages/Editor/GuidedTour/Guide.tsx @@ -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` diff --git a/app/client/src/pages/workspace/AppInviteUsersForm.tsx b/app/client/src/pages/workspace/AppInviteUsersForm.tsx index 9d0052781d..91e6b7a857 100644 --- a/app/client/src/pages/workspace/AppInviteUsersForm.tsx +++ b/app/client/src/pages/workspace/AppInviteUsersForm.tsx @@ -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 && ( diff --git a/app/client/src/ce/pages/workspace/PartnerProgramCallout.tsx b/app/client/src/pages/workspace/PartnerProgramCallout.tsx similarity index 100% rename from app/client/src/ce/pages/workspace/PartnerProgramCallout.tsx rename to app/client/src/pages/workspace/PartnerProgramCallout.tsx diff --git a/app/client/src/pages/workspace/WorkspaceInviteUsersForm.tsx b/app/client/src/pages/workspace/WorkspaceInviteUsersForm.tsx new file mode 100644 index 0000000000..a875f3217f --- /dev/null +++ b/app/client/src/pages/workspace/WorkspaceInviteUsersForm.tsx @@ -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(); + 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(); + 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 ( + + + {!cloudHosting && + showPartnerProgramCallout && + emailOutsideCurrentDomain.current && ( +
+ { + setShowPartnerProgramCallout(false); + setPartnerProgramCalloutShown(); + emailOutsideCurrentDomain.current = undefined; + }} + /> +
+ )} + {isLoading ? ( +
+ +
+ ) : ( + <> + {allUsers.length === 0 && ( + + + {createMessage(NO_USERS_INVITED)} + + )} + + {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?.userGroupId ? ( + <> + + + + {user.name} + + + + ) : ( + <> + + + + + {user.name} + + + + + )} + + + + {user.roles?.[0]?.name?.split(" - ")[0] || ""} + + + + ) : null; + }, + )} + + + )} + {canManage && ( + + + + )} +
+ ); +} + +export default WorkspaceInviteUsers; diff --git a/app/client/src/pages/workspace/settings.tsx b/app/client/src/pages/workspace/settings.tsx index c37b944886..f1f59cdadd 100644 --- a/app/client/src/pages/workspace/settings.tsx +++ b/app/client/src/pages/workspace/settings.tsx @@ -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 { diff --git a/app/client/yarn.lock b/app/client/yarn.lock index 2b6f3f6d36..d937cd91bf 100644 --- a/app/client/yarn.lock +++ b/app/client/yarn.lock @@ -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