diff --git a/src/entryPoints/HyperSwitchApp.res b/src/entryPoints/HyperSwitchApp.res index d56a0a0ad..9ce951906 100644 --- a/src/entryPoints/HyperSwitchApp.res +++ b/src/entryPoints/HyperSwitchApp.res @@ -317,6 +317,10 @@ let make = () => { + | list{"users", "create-custom-role"} => + + + | list{"users", ...remainingPath} => `${userUrl}/switch/list` | _ => `${userUrl}/${(userType :> string)->String.toLowerCase}` } - | #GET_PERMISSIONS => `${userUrl}/role` + | #GET_PERMISSIONS | #CREATE_CUSTOM_ROLE => `${userUrl}/role` | #SIGNINV2 => `${userUrl}/v2/signin` | #VERIFY_EMAILV2 => `${userUrl}/v2/verify_email` | #ACCEPT_INVITE => `${userUrl}/user/invite/accept` diff --git a/src/screens/APIUtils/APIUtilsTypes.res b/src/screens/APIUtils/APIUtilsTypes.res index 4566a02ed..06e0d76d7 100644 --- a/src/screens/APIUtils/APIUtilsTypes.res +++ b/src/screens/APIUtils/APIUtilsTypes.res @@ -67,5 +67,6 @@ type userType = [ | #CREATE_MERCHANT | #ACCEPT_INVITE | #GET_PERMISSIONS + | #CREATE_CUSTOM_ROLE | #NONE ] diff --git a/src/screens/UserManagement/CreateCustomRole.res b/src/screens/UserManagement/CreateCustomRole.res new file mode 100644 index 000000000..904f97a9f --- /dev/null +++ b/src/screens/UserManagement/CreateCustomRole.res @@ -0,0 +1,180 @@ +module RenderCustomRoles = { + @react.component + let make = (~heading, ~description, ~groupName) => { + let groupsInput = ReactFinalForm.useField(`groups`).input + let groupsAdded = groupsInput.value->LogicUtils.getStrArryFromJson + let (checkboxSelected, setCheckboxSelected) = React.useState(_ => + groupsAdded->Array.includes(groupName) + ) + let onClickGroup = groupName => { + if !(groupsAdded->Array.includes(groupName)) { + let _ = groupsAdded->Array.push(groupName) + groupsInput.onChange(groupsAdded->Identity.arrayOfGenericTypeToFormReactEvent) + } else { + let arr = groupsInput.value->LogicUtils.getStrArryFromJson + + let filteredValue = arr->Array.filter(value => {value !== groupName}) + groupsInput.onChange(filteredValue->Identity.arrayOfGenericTypeToFormReactEvent) + } + setCheckboxSelected(prev => !prev) + } + + PermissionUtils.mapStringToPermissionType !== OrganizationManage}> +
+
+ { + onClickGroup(groupName) + }} + size={Large} + /> +
+
+
{heading->React.string}
+
+ {description->React.string} +
+
+
+
+ } +} + +module NewCustomRoleInputFields = { + open UserManagementUtils + @react.component + let make = () => { + let userRole = HSLocalStorage.getFromUserDetails("user_role") +
+
+ roleScope} + fieldWrapperClass="w-4/5" + labelClass="!text-black !text-base !-ml-[0.5px]" + /> + +
+
+ +
+
+ } +} + +@react.component +let make = (~isInviteUserFlow=true, ~setNewRoleSelected=_ => ()) => { + open APIUtils + open LogicUtils + open UIUtils + let fetchDetails = useGetMethod() + let updateDetails = useUpdateMethod() + + let initialValuesForForm = + [ + ("role_scope", "merchant"->JSON.Encode.string), + ("groups", []->JSON.Encode.array), + ]->Dict.fromArray + + let {permissionInfo, setPermissionInfo} = React.useContext(GlobalProvider.defaultContext) + let (screenState, setScreenState) = React.useState(_ => PageLoaderWrapper.Loading) + let (initalValue, setInitialValues) = React.useState(_ => initialValuesForForm) + + let paddingClass = isInviteUserFlow ? "p-10" : "" + let marginClass = isInviteUserFlow ? "mt-5" : "" + let showToast = ToastState.useShowToast() + let onSubmit = async (values, _) => { + try { + // TODO - Seperate RoleName & RoleId in Backend. role_name as free text and role_id as snake_text + setScreenState(_ => PageLoaderWrapper.Loading) + let copiedJson = Js.Json.parseExn(Js.Json.stringify(values)) + let url = getURL(~entityName=USERS, ~userType=#CREATE_CUSTOM_ROLE, ~methodType=Post, ()) + + let body = copiedJson->getDictFromJsonObject->JSON.Encode.object + let roleNameValue = + body->getDictFromJsonObject->getString("role_name", "")->String.trim->titleToSnake + body->getDictFromJsonObject->Dict.set("role_name", roleNameValue->JSON.Encode.string) + let _ = await updateDetails(url, body, Post, ()) + setScreenState(_ => PageLoaderWrapper.Success) + RescriptReactRouter.replace("/users") + } catch { + | Exn.Error(e) => { + let err = Exn.message(e)->Option.getOr("Something went wrong") + let errorCode = err->safeParse->getDictFromJsonObject->getString("code", "") + let errorMessage = err->safeParse->getDictFromJsonObject->getString("message", "") + if errorCode === "UR_35" { + setInitialValues(_ => values->LogicUtils.getDictFromJsonObject) + setScreenState(_ => PageLoaderWrapper.Success) + } else { + showToast(~message=errorMessage, ~toastType=ToastError, ()) + setScreenState(_ => PageLoaderWrapper.Error(err)) + } + } + } + Nullable.null + } + + let getPermissionInfo = async () => { + try { + setScreenState(_ => PageLoaderWrapper.Loading) + let url = getURL(~entityName=USERS, ~userType=#PERMISSION_INFO, ~methodType=Get, ()) + let res = await fetchDetails(`${url}?groups=true`) + let permissionInfoValue = res->getArrayDataFromJson(ProviderHelper.itemToObjMapperForGetInfo) + setPermissionInfo(_ => permissionInfoValue) + setScreenState(_ => PageLoaderWrapper.Success) + } catch { + | _ => setScreenState(_ => PageLoaderWrapper.Error("Something went wrong!")) + } + } + + React.useEffect0(() => { + if permissionInfo->Array.length === 0 { + getPermissionInfo()->ignore + } else { + setScreenState(_ => PageLoaderWrapper.Success) + } + None + }) + +
+ + + + +
+ +
JSON.Encode.object} + validate={values => values->UserManagementUtils.validateFormForRoles} + onSubmit + formClass="flex flex-col gap-8"> + +
+ {permissionInfo + ->Array.mapWithIndex((ele, index) => { + Int.toString} + heading={`${ele.module_->snakeToTitle}`} + description={ele.description} + groupName={ele.module_} + /> + }) + ->React.array} +
+ + +
+
+
+} diff --git a/src/screens/UserManagement/RoleListTableView.res b/src/screens/UserManagement/RoleListTableView.res new file mode 100644 index 000000000..5d4c138ac --- /dev/null +++ b/src/screens/UserManagement/RoleListTableView.res @@ -0,0 +1,57 @@ +@react.component +let make = () => { + open APIUtils + open RolesEntity + + let fetchDetails = useGetMethod() + let (screenStateRoles, setScreenStateRoles) = React.useState(_ => PageLoaderWrapper.Loading) + let (rolesAvailableData, setRolesAvailableData) = React.useState(_ => []) + let (rolesOffset, setRolesOffset) = React.useState(_ => 0) + + let getRolesAvailable = async () => { + setScreenStateRoles(_ => PageLoaderWrapper.Loading) + try { + let userDataURL = getURL( + ~entityName=USER_MANAGEMENT, + ~methodType=Get, + ~userRoleTypes=ROLE_LIST, + (), + ) + let res = await fetchDetails(`${userDataURL}?groups=true`) + let rolesData = res->LogicUtils.getArrayDataFromJson(itemToObjMapperForRoles) + setRolesAvailableData(_ => rolesData->Array.map(Nullable.make)) + setScreenStateRoles(_ => PageLoaderWrapper.Success) + } catch { + | _ => setScreenStateRoles(_ => PageLoaderWrapper.Error("")) + } + } + + React.useEffect0(() => { + if rolesAvailableData->Array.length == 0 { + getRolesAvailable()->ignore + } else { + setScreenStateRoles(_ => PageLoaderWrapper.Success) + } + + None + }) + +
+ + Array.length} + resultsPerPage=10 + offset=rolesOffset + setOffset=setRolesOffset + entity={rolesEntity} + currrentFetchCount={rolesAvailableData->Array.length} + showSerialNumber=true + collapseTableRow=false + tableheadingClass="h-12" + /> + +
+} diff --git a/src/screens/UserManagement/RolesEntity.res b/src/screens/UserManagement/RolesEntity.res new file mode 100644 index 000000000..d093b42ac --- /dev/null +++ b/src/screens/UserManagement/RolesEntity.res @@ -0,0 +1,65 @@ +open LogicUtils + +type rolesTableTypes = { + role_name: string, + role_scope: string, + groups: array, +} + +type rolesColTypes = + | RoleName + | RoleScope + | ModulePermissions + +let defaultColumnsForRoles = [RoleName, RoleScope, ModulePermissions] + +let allColumnsForUser = [RoleName, RoleScope, ModulePermissions] + +let itemToObjMapperForRoles = dict => { + { + role_name: getString(dict, "role_name", ""), + role_scope: getString(dict, "role_scope", ""), + groups: getArrayFromDict(dict, "groups", []), + } +} + +let getHeadingForRoles = (colType: rolesColTypes) => { + switch colType { + | RoleName => Table.makeHeaderInfo(~key="role_name", ~title="Role name", ~showSort=true, ()) + | RoleScope => Table.makeHeaderInfo(~key="role_scope", ~title="Role scope", ~showSort=true, ()) + | ModulePermissions => Table.makeHeaderInfo(~key="groups", ~title="Module permissions", ()) + } +} + +let getCellForRoles = (data: rolesTableTypes, colType: rolesColTypes): Table.cell => { + switch colType { + | RoleName => Text(data.role_name->LogicUtils.snakeToTitle) + | RoleScope => Text(data.role_scope->LogicUtils.capitalizeString) + | ModulePermissions => + Table.CustomCell( +
+ {data.groups + ->LogicUtils.getStrArrayFromJsonArray + ->Array.map(item => `${item->LogicUtils.snakeToTitle}`) + ->Array.joinWith(", ") + ->React.string} +
, + "", + ) + } +} + +let getrolesData: JSON.t => array = json => { + getArrayDataFromJson(json, itemToObjMapperForRoles) +} + +let rolesEntity = EntityType.makeEntity( + ~uri="", + ~getObjects=getrolesData, + ~defaultColumns=defaultColumnsForRoles, + ~allColumns=allColumnsForUser, + ~getHeading=getHeadingForRoles, + ~getCell=getCellForRoles, + ~dataKey="", + (), +) diff --git a/src/screens/UserManagement/UserManagementTypes.res b/src/screens/UserManagement/UserManagementTypes.res index 4e1bb408c..e218a7d7e 100644 --- a/src/screens/UserManagement/UserManagementTypes.res +++ b/src/screens/UserManagement/UserManagementTypes.res @@ -1,3 +1,5 @@ +type userManagementTypes = Users | Roles + type permissionType = | OperationsView | OperationsManage diff --git a/src/screens/UserManagement/UserManagementUtils.res b/src/screens/UserManagement/UserManagementUtils.res index 2d8bad01f..4bf135e0b 100644 --- a/src/screens/UserManagement/UserManagementUtils.res +++ b/src/screens/UserManagement/UserManagementUtils.res @@ -19,6 +19,38 @@ let inviteEmail = FormRenderer.makeFieldInfo( (), ) +let createCustomRole = FormRenderer.makeFieldInfo( + ~label="Enter custom role name", + ~name="role_name", + ~customInput=InputFields.textInput(~autoComplete="off", ~autoFocus=false, ()), + ~isRequired=true, + (), +) + +let roleScope = userRole => { + let roleScopeArray = ["Merchant", "Organization"]->Array.map(item => { + let option: SelectBox.dropdownOption = { + label: item, + value: item->String.toLowerCase, + } + option + }) + + FormRenderer.makeFieldInfo( + ~label="Role Scope ", + ~name="role_scope", + ~customInput=InputFields.selectInput( + ~options=roleScopeArray, + ~buttonText="Select Option", + ~deselectDisable=true, + ~disableSelect=userRole === "org_admin" ? false : true, + (), + ), + ~isRequired=true, + (), + ) +} + let validateEmptyValue = (key, errors) => { switch key { | "emailList" => Dict.set(errors, "email", "Please enter Invite mails"->JSON.Encode.string) @@ -47,6 +79,24 @@ let validateForm = (values, ~fieldsToValidate: array) => { errors->JSON.Encode.object } +let validateFormForRoles = values => { + let errors = Dict.make() + open LogicUtils + let valuesDict = values->getDictFromJsonObject + if valuesDict->getString("role_scope", "")->isEmptyString { + Dict.set(errors, "role_scope", "Role scope is required"->JSON.Encode.string) + } + if valuesDict->getString("role_name", "")->isEmptyString { + Dict.set(errors, "role_name", "Role name is required"->JSON.Encode.string) + } + if valuesDict->getString("role_name", "")->String.length > 64 { + Dict.set(errors, "role_name", "Role name should be less than 64 characters"->JSON.Encode.string) + } + if valuesDict->getArrayFromDict("groups", [])->Array.length === 0 { + Dict.set(errors, "groups", "Roles required"->JSON.Encode.string) + } + errors->JSON.Encode.object +} let roleListDataMapper: UserRoleEntity.roleListResponse => SelectBox.dropdownOption = ele => { let roleNameToDisplay = switch ele.role_name { @@ -137,3 +187,11 @@ let roleListResponseMapper: Dict.t => UserRoleEntity.roleListResponse = role_name: dict->getString("role_name", ""), } } + +let tabIndeToVariantMapper = index => { + open UserManagementTypes + switch index { + | 0 => Users + | _ => Roles + } +} diff --git a/src/screens/UserManagement/UserRoleEntry.res b/src/screens/UserManagement/UserRoleEntry.res index 06a07d77b..1f64219db 100644 --- a/src/screens/UserManagement/UserRoleEntry.res +++ b/src/screens/UserManagement/UserRoleEntry.res @@ -1,9 +1,9 @@ -type roles = Users | Roles open UserRoleEntity @react.component let make = () => { open APIUtils + open LogicUtils let fetchDetails = useGetMethod() let mixpanelEvent = MixpanelHook.useSendEvent() let userPermissionJson = Recoil.useRecoilValueFromAtom(HyperswitchAtom.userPermissionAtom) @@ -12,7 +12,7 @@ let make = () => { let (screenStateUsers, setScreenStateUsers) = React.useState(_ => PageLoaderWrapper.Loading) let (userOffset, setUserOffset) = React.useState(_ => 0) let (searchText, setSearchText) = React.useState(_ => "") - + let (tabIndex, setTabIndex) = React.useState(_ => 0) let {permissionInfo, setPermissionInfo} = React.useContext(GlobalProvider.defaultContext) let getUserData = async () => { @@ -25,7 +25,7 @@ let make = () => { (), ) let res = await fetchDetails(userDataURL) - let userData = res->LogicUtils.getArrayDataFromJson(itemToObjMapperForUser) + let userData = res->getArrayDataFromJson(itemToObjMapperForUser) setUsersData(_ => userData->Array.map(Nullable.make)) setUsersFilterData(_ => userData->Array.map(Nullable.make)) setScreenStateUsers(_ => PageLoaderWrapper.Success) @@ -38,27 +38,23 @@ let make = () => { try { let url = getURL(~entityName=USERS, ~userType=#PERMISSION_INFO, ~methodType=Get, ()) let res = await fetchDetails(`${url}?groups=true`) - setPermissionInfo(_ => - res->LogicUtils.getArrayDataFromJson(ProviderHelper.itemToObjMapperForGetInfo) - ) + setPermissionInfo(_ => res->getArrayDataFromJson(ProviderHelper.itemToObjMapperForGetInfo)) + let _ = await getUserData() } catch { - | _ => () + | _ => setScreenStateUsers(_ => PageLoaderWrapper.Error("")) } } React.useEffect0(() => { if permissionInfo->Array.length === 0 { getPermissionInfo()->ignore - } - - if usersData->Array.length === 0 { + } else { getUserData()->ignore } None }) let filterLogicForUsers = ReactDebounce.useDebounced(ob => { - open LogicUtils let (searchText, arr) = ob let filteredList = if searchText->isNonEmptyString { arr->Array.filter((obj: Nullable.t) => { @@ -109,13 +105,35 @@ let make = () => { }, { title: "Roles", - renderContent: () => - , + renderContent: () => , }, ] + let buttonValueBasedonTab = switch tabIndex->UserManagementUtils.tabIndeToVariantMapper { + | Users => + { + mixpanelEvent(~eventName="invite_users", ()) + RescriptReactRouter.push("/users/invite-users") + }} + customButtonStyle="w-48" + /> + | Roles => + { + mixpanelEvent(~eventName="invite_users", ()) + RescriptReactRouter.push("/users/create-custom-role") + }} + customButtonStyle="w-48" + /> + } +
{<> { subTitle="Manage user roles and invite members of your organisation" />
-
- { - mixpanelEvent(~eventName="invite_users", ()) - RescriptReactRouter.push("/users/invite-users") - }} - customButtonStyle="w-48" - /> -
+
{buttonValueBasedonTab}
{ includeMargin=false lightThemeColor="black" defaultClasses="font-ibm-plex w-max flex flex-auto flex-row items-center justify-center px-6 font-semibold text-body" + onTitleClick={tabId => setTabIndex(_ => tabId)} />
}