Members and org settings page (#424)
* Members and org settings page. * Adding link back to applications * Making UI look better. * Fixing some more css * Table dropdown onBlur, UI transparency, Popover issue fixed. Added sort arrow (#413) * table dropdown ui issue and onblur click fixed * position props value fixed * tabs tripple click fixed * Position props in table dropdown made optional * tabs background color bug removed * General settings page is bug free. * Getting the manage users page to work. * Moving general settings into its own file. * Removing some unwanted URLs * User cant change their role or remove themselves * Changing onClick prop for Icon. * Fixing tests * Added loading states to text inputs. * Added validators for text input * Fixed border of skeleton for text input * Adding a loader for Members screen * Adding Noop to icon button * Removing console.log * Fixing imports. * Moving org reducer to immer. * Using utils email validator. * Removing placeholder from text input props. Co-authored-by: devrk96 <rohit.kumawat@primathon.in>
This commit is contained in:
parent
0b22fc67b3
commit
e2b939416a
|
|
@ -98,11 +98,11 @@ describe("Create new org and share with a user", function() {
|
|||
homePage.viewerRole,
|
||||
);
|
||||
cy.navigateToOrgSettings(orgid);
|
||||
cy.get(homePage.emailList).then(function($lis) {
|
||||
expect($lis).to.have.length(3);
|
||||
expect($lis.eq(0)).to.contain(Cypress.env("USERNAME"));
|
||||
expect($lis.eq(1)).to.contain(Cypress.env("TESTUSERNAME1"));
|
||||
expect($lis.eq(2)).to.contain(Cypress.env("TESTUSERNAME2"));
|
||||
cy.get(homePage.emailList).then(function($list) {
|
||||
expect($list).to.have.length(3);
|
||||
expect($list.eq(0)).to.contain(Cypress.env("USERNAME"));
|
||||
expect($list.eq(1)).to.contain(Cypress.env("TESTUSERNAME1"));
|
||||
expect($list.eq(2)).to.contain(Cypress.env("TESTUSERNAME2"));
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -117,14 +117,15 @@ describe("Create new org and share with a user", function() {
|
|||
cy.get(homePage.searchInput).type(appid);
|
||||
cy.wait(2000);
|
||||
cy.navigateToOrgSettings(orgid);
|
||||
cy.get(homePage.emailList).then(function($lis) {
|
||||
expect($lis).to.have.length(3);
|
||||
expect($lis.eq(0)).to.contain(Cypress.env("USERNAME"));
|
||||
expect($lis.eq(1)).to.contain(Cypress.env("TESTUSERNAME1"));
|
||||
expect($lis.eq(2)).to.contain(Cypress.env("TESTUSERNAME2"));
|
||||
cy.get(homePage.emailList).then(function($list) {
|
||||
expect($list).to.have.length(3);
|
||||
expect($list.eq(0)).to.contain(Cypress.env("USERNAME"));
|
||||
expect($list.eq(1)).to.contain(Cypress.env("TESTUSERNAME1"));
|
||||
expect($list.eq(2)).to.contain(Cypress.env("TESTUSERNAME2"));
|
||||
});
|
||||
cy.xpath(homePage.appHome)
|
||||
.should("be.visible")
|
||||
.first()
|
||||
.click();
|
||||
cy.wait("@applications").should(
|
||||
"have.nested.property",
|
||||
|
|
|
|||
|
|
@ -1,15 +1,15 @@
|
|||
{
|
||||
"CreateApp":"span[class='bp3-button-text']",
|
||||
"CreateApp": "span[class='bp3-button-text']",
|
||||
"searchInput": "input[type='text']",
|
||||
"appEditIcon": ".t--application-edit-link",
|
||||
"publishButton":".t--application-publish-btn",
|
||||
"shareButton":".t--application-share-btn",
|
||||
"publishCrossButton":"span[icon='small-cross']",
|
||||
"homePageID":"//div[@id='root']",
|
||||
"appMoreIcon":".bp3-popover-wrapper.more .bp3-popover-target",
|
||||
"deleteButton":".bp3-menu-item.bp3-popover-dismiss",
|
||||
"selectAction":"#Base",
|
||||
"deleteApp":".bp3-menu-item",
|
||||
"publishButton": ".t--application-publish-btn",
|
||||
"shareButton": ".t--application-share-btn",
|
||||
"publishCrossButton": "span[icon='small-cross']",
|
||||
"homePageID": "//div[@id='root']",
|
||||
"appMoreIcon": ".bp3-popover-wrapper.more .bp3-popover-target",
|
||||
"deleteButton": ".bp3-menu-item.bp3-popover-dismiss",
|
||||
"selectAction": "#Base",
|
||||
"deleteApp": ".bp3-menu-item",
|
||||
"homeIcon": ".t--appsmith-logo",
|
||||
"inputAppName": "input[name=applicationName]",
|
||||
"createNew": ".createnew",
|
||||
|
|
@ -20,7 +20,9 @@
|
|||
"members": "//div[contains(text(),'Members')]",
|
||||
"share": "//div[contains(text(),'Share')]",
|
||||
"OrgSettings": "//div[contains(text(),'Organization Settings')]",
|
||||
"MemberSettings": "//div[contains(text(),'Members')]",
|
||||
"inviteUser": "//span[text()='Invite Users']/parent::button",
|
||||
"inviteUserMembersPage": "[data-cy=t--invite-users]",
|
||||
"email": "//input[@type='email']",
|
||||
"selectRole": "//span[text()='Select a role']",
|
||||
"adminRole": "//div[@class='bp3-overlay bp3-overlay-open']//div[contains(text(),'Administrator')]",
|
||||
|
|
@ -28,7 +30,7 @@
|
|||
"developerRole": "//div[@class='bp3-overlay bp3-overlay-open']//div[contains(text(),'Developer')]",
|
||||
"inviteBtn": "//span[text()='Invite']/parent::button",
|
||||
"manageUsers": ".manageUsers",
|
||||
"DeleteBtn": "//div[@data-colindex='3']//*[local-name()='svg']",
|
||||
"DeleteBtn": "[data-cy=t--deleteUser]",
|
||||
"ShareBtn": "//span[text()='Share']/parent::button",
|
||||
"launchBtn": "//span[text()='Launch']/parent::button",
|
||||
"appView": ".t--application-view-link",
|
||||
|
|
@ -40,4 +42,4 @@
|
|||
"shareOrg": ") .bp3-button-text:contains('Share')",
|
||||
"orgSection": ".bp3-button-text:contains(",
|
||||
"createAppFrOrg": ") .t--create-app-popup"
|
||||
}
|
||||
}
|
||||
|
|
@ -39,13 +39,14 @@ Cypress.Commands.add("navigateToOrgSettings", orgName => {
|
|||
.scrollIntoView()
|
||||
.should("be.visible");
|
||||
cy.get(".t--org-name").click({ force: true });
|
||||
cy.xpath(homePage.OrgSettings).click({ force: true });
|
||||
cy.xpath(homePage.MemberSettings).click({ force: true });
|
||||
cy.wait("@getOrganisation");
|
||||
cy.wait("@getRoles").should(
|
||||
"have.nested.property",
|
||||
"response.body.responseMeta.status",
|
||||
200,
|
||||
);
|
||||
cy.xpath(homePage.inviteUser).should("be.visible");
|
||||
cy.get(homePage.inviteUserMembersPage).should("be.visible");
|
||||
});
|
||||
|
||||
Cypress.Commands.add("inviteUserForOrg", (orgName, email, role) => {
|
||||
|
|
@ -70,6 +71,7 @@ Cypress.Commands.add("inviteUserForOrg", (orgName, email, role) => {
|
|||
cy.contains(email);
|
||||
cy.get(homePage.manageUsers).click({ force: true });
|
||||
cy.xpath(homePage.appHome)
|
||||
.first()
|
||||
.should("be.visible")
|
||||
.click();
|
||||
});
|
||||
|
|
@ -79,14 +81,16 @@ Cypress.Commands.add("deleteUserFromOrg", (orgName, email) => {
|
|||
.scrollIntoView()
|
||||
.should("be.visible");
|
||||
cy.get(".t--org-name").click({ force: true });
|
||||
cy.xpath(homePage.OrgSettings).click({ force: true });
|
||||
cy.xpath(homePage.MemberSettings).click({ force: true });
|
||||
cy.wait("@getOrganisation");
|
||||
cy.wait("@getRoles").should(
|
||||
"have.nested.property",
|
||||
"response.body.responseMeta.status",
|
||||
200,
|
||||
);
|
||||
cy.xpath(homePage.DeleteBtn).click({ force: true });
|
||||
cy.get(homePage.DeleteBtn).click({ force: true });
|
||||
cy.xpath(homePage.appHome)
|
||||
.first()
|
||||
.should("be.visible")
|
||||
.click();
|
||||
cy.wait("@applications").should(
|
||||
|
|
@ -101,13 +105,13 @@ Cypress.Commands.add("updateUserRoleForOrg", (orgName, email, role) => {
|
|||
.scrollIntoView()
|
||||
.should("be.visible");
|
||||
cy.get(".t--org-name").click({ force: true });
|
||||
cy.xpath(homePage.OrgSettings).click({ force: true });
|
||||
cy.xpath(homePage.MemberSettings).click({ force: true });
|
||||
cy.wait("@getRoles").should(
|
||||
"have.nested.property",
|
||||
"response.body.responseMeta.status",
|
||||
200,
|
||||
);
|
||||
cy.xpath(homePage.inviteUser).click({ force: true });
|
||||
cy.get(homePage.inviteUserMembersPage).click({ force: true });
|
||||
cy.xpath(homePage.email)
|
||||
.click({ force: true })
|
||||
.type(email);
|
||||
|
|
@ -122,6 +126,7 @@ Cypress.Commands.add("updateUserRoleForOrg", (orgName, email, role) => {
|
|||
cy.contains(email);
|
||||
cy.get(".bp3-icon-small-cross").click({ force: true });
|
||||
cy.xpath(homePage.appHome)
|
||||
.first()
|
||||
.should("be.visible")
|
||||
.click();
|
||||
cy.wait("@applications").should(
|
||||
|
|
@ -1444,6 +1449,8 @@ Cypress.Commands.add("startServerAndRoutes", () => {
|
|||
cy.route("DELETE", "/api/v1/datasources/*").as("deleteDatasource");
|
||||
|
||||
cy.route("GET", "/api/v1/organizations").as("organizations");
|
||||
cy.route("GET", "/api/v1/organizations/*").as("getOrganisation");
|
||||
|
||||
cy.route("POST", "/api/v1/actions/execute").as("executeAction");
|
||||
cy.route("POST", "/api/v1/applications/publish/*").as("publishApp");
|
||||
cy.route("PUT", "/api/v1/layouts/*/pages/*").as("updateLayout");
|
||||
|
|
|
|||
68
app/client/src/actions/orgActions.ts
Normal file
68
app/client/src/actions/orgActions.ts
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
import { ReduxActionTypes } from "constants/ReduxActionConstants";
|
||||
import { SaveOrgRequest } from "api/OrgApi";
|
||||
|
||||
export const fetchOrg = (orgId: string) => {
|
||||
return {
|
||||
type: ReduxActionTypes.FETCH_CURRENT_ORG,
|
||||
payload: {
|
||||
orgId,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const changeOrgName = (name: string) => {
|
||||
return {
|
||||
type: ReduxActionTypes.UPDATE_ORG_NAME_INIT,
|
||||
payload: {
|
||||
name,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const changeOrgUserRole = (
|
||||
orgId: string,
|
||||
role: string,
|
||||
username: string,
|
||||
) => {
|
||||
return {
|
||||
type: ReduxActionTypes.CHANGE_ORG_USER_ROLE_INIT,
|
||||
payload: {
|
||||
orgId,
|
||||
role,
|
||||
username,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const deleteOrgUser = (orgId: string, username: string) => {
|
||||
return {
|
||||
type: ReduxActionTypes.DELETE_ORG_USER_INIT,
|
||||
payload: {
|
||||
orgId,
|
||||
username,
|
||||
},
|
||||
};
|
||||
};
|
||||
export const fetchUsersForOrg = (orgId: string) => {
|
||||
return {
|
||||
type: ReduxActionTypes.FETCH_ALL_USERS_INIT,
|
||||
payload: {
|
||||
orgId,
|
||||
},
|
||||
};
|
||||
};
|
||||
export const fetchRolesForOrg = (orgId: string) => {
|
||||
return {
|
||||
type: ReduxActionTypes.FETCH_ALL_ROLES_INIT,
|
||||
payload: {
|
||||
orgId,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const saveOrg = (orgSettings: SaveOrgRequest) => {
|
||||
return {
|
||||
type: ReduxActionTypes.SAVE_ORG_INIT,
|
||||
payload: orgSettings,
|
||||
};
|
||||
};
|
||||
|
|
@ -47,8 +47,9 @@ export interface FetchAllRolesRequest {
|
|||
|
||||
export interface SaveOrgRequest {
|
||||
id: string;
|
||||
name: string;
|
||||
website: string;
|
||||
name?: string;
|
||||
website?: string;
|
||||
email?: string;
|
||||
}
|
||||
|
||||
export interface CreateOrgRequest {
|
||||
|
|
|
|||
3
app/client/src/assets/icons/ads/upper_arrow.svg
Normal file
3
app/client/src/assets/icons/ads/upper_arrow.svg
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<svg width="6" height="4" viewBox="0 0 6 4" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M6 4L3 0L0 4L6 4Z" fill="#6D6D6D"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 184 B |
|
|
@ -293,7 +293,7 @@ const StyledButton = styled("button")<ThemeProp & ButtonProps>`
|
|||
|
||||
Button.defaultProps = {
|
||||
category: Category.primary,
|
||||
variant: Variant.success,
|
||||
variant: Variant.info,
|
||||
size: Size.small,
|
||||
isLoading: false,
|
||||
disabled: false,
|
||||
|
|
|
|||
|
|
@ -11,6 +11,8 @@ import { ReactComponent as CloseIcon } from "assets/icons/ads/close.svg";
|
|||
import styled from "styled-components";
|
||||
import { Size } from "./Button";
|
||||
import { sizeHandler } from "./Spinner";
|
||||
import { CommonComponentProps } from "./common";
|
||||
import { noop } from "lodash";
|
||||
|
||||
export type IconName =
|
||||
| "Select icon"
|
||||
|
|
@ -61,10 +63,10 @@ export type IconProps = {
|
|||
name?: IconName;
|
||||
invisible?: boolean;
|
||||
className?: string;
|
||||
click?: () => void;
|
||||
onClick?: () => void;
|
||||
};
|
||||
|
||||
const Icon = (props: IconProps) => {
|
||||
const Icon = (props: IconProps & CommonComponentProps) => {
|
||||
let returnIcon;
|
||||
switch (props.name) {
|
||||
case "delete":
|
||||
|
|
@ -101,8 +103,9 @@ const Icon = (props: IconProps) => {
|
|||
return returnIcon ? (
|
||||
<IconWrapper
|
||||
className={props.className ? props.className : "ads-icon"}
|
||||
data-cy={props.cypressSelector}
|
||||
{...props}
|
||||
onClick={() => props.click && props.click()}
|
||||
onClick={props.onClick || noop}
|
||||
>
|
||||
{returnIcon}
|
||||
</IconWrapper>
|
||||
|
|
|
|||
|
|
@ -132,7 +132,7 @@ const SearchInput = forwardRef(
|
|||
name="close"
|
||||
size={Size.large}
|
||||
className="close-icon"
|
||||
click={() => setSearchValue("")}
|
||||
onClick={() => setSearchValue("")}
|
||||
/>
|
||||
) : null}
|
||||
</InputWrapper>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
import { useTable, useSortBy } from "react-table";
|
||||
import React from "react";
|
||||
import styled from "styled-components";
|
||||
import { ReactComponent as DownArrow } from "assets/icons/ads/down_arrow.svg";
|
||||
import { ReactComponent as DownArrow } from "../../assets/icons/ads/down_arrow.svg";
|
||||
import { ReactComponent as UpperArrow } from "../../assets/icons/ads/upper_arrow.svg";
|
||||
|
||||
const Styles = styled.div`
|
||||
table {
|
||||
|
|
@ -80,10 +81,13 @@ const Styles = styled.div`
|
|||
}
|
||||
`;
|
||||
|
||||
function Table(props: any) {
|
||||
const data = React.useMemo(() => props.data, []);
|
||||
interface TableProps {
|
||||
data: any[];
|
||||
columns: any[];
|
||||
}
|
||||
|
||||
const columns = React.useMemo(() => props.columns, []);
|
||||
function Table(props: TableProps) {
|
||||
const { data, columns } = props;
|
||||
|
||||
const {
|
||||
getTableProps,
|
||||
|
|
@ -107,7 +111,7 @@ function Table(props: any) {
|
|||
{column.render("Header")}
|
||||
{column.isSorted ? (
|
||||
column.isSortedDesc ? (
|
||||
" 🔼"
|
||||
<UpperArrow />
|
||||
) : (
|
||||
<DownArrow />
|
||||
)
|
||||
|
|
@ -126,7 +130,11 @@ function Table(props: any) {
|
|||
<tr {...row.getRowProps()} key={index}>
|
||||
{row.cells.map((cell, index) => {
|
||||
return (
|
||||
<td {...cell.getCellProps()} key={index}>
|
||||
<td
|
||||
{...cell.getCellProps()}
|
||||
key={index}
|
||||
data-colindex={index}
|
||||
>
|
||||
{cell.render("Cell")}
|
||||
</td>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -3,23 +3,24 @@ import { CommonComponentProps, hexToRgba } from "./common";
|
|||
import { ReactComponent as DownArrow } from "assets/icons/ads/down_arrow.svg";
|
||||
import Text, { TextType } from "./Text";
|
||||
import styled from "styled-components";
|
||||
import {
|
||||
Popover,
|
||||
PopoverInteractionKind,
|
||||
} from "@blueprintjs/core/lib/esm/components/popover/popover";
|
||||
import { Position } from "@blueprintjs/core/lib/esm/common/position";
|
||||
|
||||
type DropdownOption = {
|
||||
label: string;
|
||||
value: string;
|
||||
name: string;
|
||||
desc: string;
|
||||
};
|
||||
|
||||
type DropdownProps = CommonComponentProps & {
|
||||
options: DropdownOption[];
|
||||
onSelect: (selectedValue: string) => void;
|
||||
selectedOption: DropdownOption;
|
||||
onSelect: (selectedValue: DropdownOption) => void;
|
||||
selectedIndex: number;
|
||||
position?: Position;
|
||||
};
|
||||
|
||||
const DropdownWrapper = styled.div`
|
||||
width: 100%;
|
||||
position: relative;
|
||||
`;
|
||||
|
||||
const SelectedItem = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
|
@ -32,9 +33,6 @@ const SelectedItem = styled.div`
|
|||
`;
|
||||
|
||||
const OptionsWrapper = styled.div`
|
||||
position: absolute;
|
||||
margin-top: ${props => props.theme.spaces[8]}px;
|
||||
left: -60px;
|
||||
width: 200px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
|
@ -45,17 +43,16 @@ const OptionsWrapper = styled.div`
|
|||
`;
|
||||
|
||||
const DropdownOption = styled.div<{
|
||||
selected: DropdownOption;
|
||||
option: DropdownOption;
|
||||
isSelected: boolean;
|
||||
}>`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 10px 12px;
|
||||
cursor: pointer;
|
||||
background-color: ${props =>
|
||||
props.option.label === props.selected.label
|
||||
? props.theme.colors.blackShades[4]
|
||||
: "transparent"};
|
||||
${props =>
|
||||
props.isSelected
|
||||
? `background-color: ${props.theme.colors.blackShades[4]}`
|
||||
: null};
|
||||
|
||||
span:last-child {
|
||||
margin-top: ${props => props.theme.spaces[1] + 1}px;
|
||||
|
|
@ -69,40 +66,44 @@ const DropdownOption = styled.div<{
|
|||
`;
|
||||
|
||||
const TableDropdown = (props: DropdownProps) => {
|
||||
const [selected, setSelected] = useState(props.selectedOption);
|
||||
const [selectedIndex, setSelectedIndex] = useState(props.selectedIndex);
|
||||
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
|
||||
const [selectedOption, setSelectedOption] = useState(
|
||||
props.options[props.selectedIndex] || {},
|
||||
);
|
||||
|
||||
const dropdownHandler = () => {
|
||||
setIsDropdownOpen(!isDropdownOpen);
|
||||
};
|
||||
|
||||
const optionSelector = (option: DropdownOption) => {
|
||||
setSelected(option);
|
||||
const optionSelector = (index: number) => {
|
||||
setSelectedIndex(index);
|
||||
setSelectedOption(props.options[index]);
|
||||
props.onSelect && props.onSelect(props.options[index]);
|
||||
setIsDropdownOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<DropdownWrapper>
|
||||
<SelectedItem onClick={() => dropdownHandler()}>
|
||||
<Text type={TextType.P1}>{selected.label}</Text>
|
||||
<Popover
|
||||
usePortal={false}
|
||||
position={props.position || Position.BOTTOM_LEFT}
|
||||
isOpen={isDropdownOpen}
|
||||
onInteraction={state => setIsDropdownOpen(state)}
|
||||
interactionKind={PopoverInteractionKind.CLICK}
|
||||
>
|
||||
<SelectedItem>
|
||||
<Text type={TextType.P1}>{selectedOption.name}</Text>
|
||||
<DownArrow />
|
||||
</SelectedItem>
|
||||
{isDropdownOpen ? (
|
||||
<OptionsWrapper>
|
||||
{props.options.map((el: DropdownOption, index: number) => (
|
||||
<DropdownOption
|
||||
key={index}
|
||||
selected={selected}
|
||||
option={el}
|
||||
onClick={() => optionSelector(el)}
|
||||
>
|
||||
<Text type={TextType.H5}>{el.label}</Text>
|
||||
<Text type={TextType.P3}>{el.value}</Text>
|
||||
</DropdownOption>
|
||||
))}
|
||||
</OptionsWrapper>
|
||||
) : null}
|
||||
</DropdownWrapper>
|
||||
<OptionsWrapper>
|
||||
{props.options.map((el: DropdownOption, index: number) => (
|
||||
<DropdownOption
|
||||
key={index}
|
||||
isSelected={selectedIndex === index}
|
||||
onClick={() => optionSelector(index)}
|
||||
>
|
||||
<Text type={TextType.H5}>{el.name}</Text>
|
||||
<Text type={TextType.P3}>{el.desc}</Text>
|
||||
</DropdownOption>
|
||||
))}
|
||||
</OptionsWrapper>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,13 @@ import styled from "styled-components";
|
|||
import Icon, { IconName } from "./Icon";
|
||||
import { Size } from "./Button";
|
||||
|
||||
export type TabProp = {
|
||||
key: string;
|
||||
title: string;
|
||||
panelComponent: JSX.Element;
|
||||
icon: IconName;
|
||||
};
|
||||
|
||||
const TabsWrapper = styled.div<{ shouldOverflow?: boolean }>`
|
||||
user-select: none;
|
||||
border-radius: 0px;
|
||||
|
|
@ -59,14 +66,6 @@ const TabsWrapper = styled.div<{ shouldOverflow?: boolean }>`
|
|||
fill: ${props => props.theme.colors.blackShades[9]};
|
||||
}
|
||||
}
|
||||
.react-tabs__tab:focus {
|
||||
box-shadow: none;
|
||||
border-bottom: ${props => props.theme.colors.info.main}
|
||||
${props => props.theme.spaces[1] - 2}px solid;
|
||||
path {
|
||||
fill: ${props => props.theme.colors.blackShades[9]};
|
||||
}
|
||||
}
|
||||
.react-tabs__tab--selected {
|
||||
color: ${props => props.theme.colors.blackShades[9]};
|
||||
background-color: transparent;
|
||||
|
|
@ -85,10 +84,21 @@ const TabsWrapper = styled.div<{ shouldOverflow?: boolean }>`
|
|||
background-color: ${props => props.theme.colors.info.main};
|
||||
}
|
||||
}
|
||||
.react-tabs__tab:focus:after {
|
||||
content: none;
|
||||
height: ${props => props.theme.spaces[1] - 2}px;
|
||||
background: ${props => props.theme.colors.info.main};
|
||||
.react-tabs__tab:focus {
|
||||
&::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
bottom: ${props => props.theme.spaces[0] - 1}px;
|
||||
left: ${props => props.theme.spaces[0]}px;
|
||||
height: ${props => props.theme.spaces[1] - 2}px;
|
||||
background-color: ${props => props.theme.colors.info.main};
|
||||
}
|
||||
box-shadow: none;
|
||||
border-color: transparent;
|
||||
path {
|
||||
fill: ${props => props.theme.colors.blackShades[9]};
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
|
|
@ -100,14 +110,9 @@ const TabTitle = styled.span`
|
|||
`;
|
||||
|
||||
type TabbedViewComponentType = {
|
||||
tabs: Array<{
|
||||
key: string;
|
||||
title: string;
|
||||
panelComponent: JSX.Element;
|
||||
icon?: IconName;
|
||||
}>;
|
||||
tabs: Array<TabProp>;
|
||||
selectedIndex?: number;
|
||||
setSelectedIndex?: Function;
|
||||
onSelect?: Function;
|
||||
overflow?: boolean;
|
||||
};
|
||||
|
||||
|
|
@ -117,7 +122,7 @@ export const TabComponent = (props: TabbedViewComponentType) => {
|
|||
<Tabs
|
||||
selectedIndex={props.selectedIndex}
|
||||
onSelect={(index: number) => {
|
||||
props.setSelectedIndex && props.setSelectedIndex(index);
|
||||
props.onSelect && props.onSelect(index);
|
||||
}}
|
||||
>
|
||||
<TabList>
|
||||
|
|
|
|||
|
|
@ -3,6 +3,34 @@ import { CommonComponentProps, hexToRgba } from "./common";
|
|||
import styled from "styled-components";
|
||||
import Text, { TextType } from "./Text";
|
||||
import { theme } from "constants/DefaultTheme";
|
||||
import {
|
||||
FORM_VALIDATION_INVALID_EMAIL,
|
||||
ERROR_MESSAGE_NAME_EMPTY,
|
||||
} from "constants/messages";
|
||||
import { isEmail } from "utils/formhelpers";
|
||||
|
||||
export type Validator = (
|
||||
value: string,
|
||||
) => {
|
||||
isValid: boolean;
|
||||
message: string;
|
||||
};
|
||||
|
||||
export function emailValidator(email: string) {
|
||||
const isValid = isEmail(email);
|
||||
return {
|
||||
isValid: isValid,
|
||||
message: !isValid ? FORM_VALIDATION_INVALID_EMAIL : "",
|
||||
};
|
||||
}
|
||||
|
||||
export function notEmptyValidator(value: string) {
|
||||
const isValid = !!value;
|
||||
return {
|
||||
isValid: isValid,
|
||||
message: !isValid ? ERROR_MESSAGE_NAME_EMPTY : "",
|
||||
};
|
||||
}
|
||||
|
||||
export type TextInputProps = CommonComponentProps & {
|
||||
placeholder?: string;
|
||||
|
|
@ -43,7 +71,6 @@ const StyledInput = styled.input<
|
|||
border-radius: 0;
|
||||
outline: 0;
|
||||
box-shadow: none;
|
||||
margin-bottom: ${props => props.theme.spaces[1]}px;
|
||||
border: 1px solid ${props => props.inputStyle.borderColor};
|
||||
padding: ${props => props.theme.spaces[4]}px
|
||||
${props => props.theme.spaces[6]}px;
|
||||
|
|
@ -73,12 +100,17 @@ const InputWrapper = styled.div`
|
|||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
position: relative;
|
||||
|
||||
span {
|
||||
color: ${props => props.theme.colors.danger.main};
|
||||
}
|
||||
`;
|
||||
|
||||
const ErrorWrapper = styled.div`
|
||||
position absolute;
|
||||
bottom: -17px;
|
||||
`;
|
||||
const TextInput = forwardRef(
|
||||
(props: TextInputProps, ref: Ref<HTMLInputElement>) => {
|
||||
const initialValidation = () => {
|
||||
|
|
@ -101,13 +133,26 @@ const TextInput = forwardRef(
|
|||
|
||||
const memoizedChangeHandler = useCallback(
|
||||
el => {
|
||||
props.validator && setValidation(props.validator(el.target.value));
|
||||
return props.onChange && props.onChange(el.target.value);
|
||||
const validation = props.validator && props.validator(el.target.value);
|
||||
if (validation) {
|
||||
props.validator && setValidation(validation);
|
||||
return (
|
||||
validation.isValid &&
|
||||
props.onChange &&
|
||||
props.onChange(el.target.value)
|
||||
);
|
||||
} else {
|
||||
return props.onChange && props.onChange(el.target.value);
|
||||
}
|
||||
},
|
||||
[props],
|
||||
);
|
||||
|
||||
const ErrorMessage = <Text type={TextType.P3}>{validation.message}</Text>;
|
||||
const ErrorMessage = (
|
||||
<ErrorWrapper>
|
||||
<Text type={TextType.P3}>{validation.message}</Text>
|
||||
</ErrorWrapper>
|
||||
);
|
||||
|
||||
return (
|
||||
<InputWrapper>
|
||||
|
|
@ -118,19 +163,15 @@ const TextInput = forwardRef(
|
|||
isValid={validation.isValid}
|
||||
defaultValue={props.defaultValue}
|
||||
{...props}
|
||||
placeholder={props.placeholder ? props.placeholder : ""}
|
||||
placeholder={props.placeholder}
|
||||
onChange={memoizedChangeHandler}
|
||||
/>
|
||||
{validation.isValid ? null : ErrorMessage}
|
||||
{ErrorMessage}
|
||||
</InputWrapper>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
TextInput.defaultProps = {
|
||||
fill: false,
|
||||
};
|
||||
|
||||
TextInput.displayName = "TextInput";
|
||||
|
||||
export default TextInput;
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import React from "react";
|
||||
import Icon from "components/ads/Icon";
|
||||
import Button, { Size, Category, Variant } from "components/ads/Button";
|
||||
import { withKnobs, select, boolean } from "@storybook/addon-knobs";
|
||||
import { withDesign } from "storybook-addon-designs";
|
||||
import Icon from "components/ads/Icon";
|
||||
import AppIcon, { AppIconName } from "components/ads/AppIcon";
|
||||
import { StoryWrapper } from "./Tabs.stories";
|
||||
|
||||
|
|
|
|||
|
|
@ -2,8 +2,10 @@ import React from "react";
|
|||
import Table from "components/ads/Table";
|
||||
import Button, { Category, Variant, Size } from "components/ads/Button";
|
||||
import Icon from "components/ads/Icon";
|
||||
import TableDropdown from "components/ads/TableDropdown";
|
||||
import { Position } from "@blueprintjs/core/lib/esm/common/position";
|
||||
import { StoryWrapper } from "./Tabs.stories";
|
||||
|
||||
|
||||
export default {
|
||||
title: "Table",
|
||||
component: Table,
|
||||
|
|
@ -36,11 +38,33 @@ const columns = [
|
|||
},
|
||||
];
|
||||
|
||||
const options = [
|
||||
{
|
||||
name: "Admin",
|
||||
desc: "Can edit, view and invite other user to an app",
|
||||
},
|
||||
{
|
||||
name: "Developer",
|
||||
desc: "Can view and invite other user to an app",
|
||||
},
|
||||
{
|
||||
name: "User",
|
||||
desc: "Can view and invite other user to an app and...",
|
||||
},
|
||||
];
|
||||
|
||||
const data = [
|
||||
{
|
||||
col1: "Dustin Howard",
|
||||
col2: "dustin_01@jlegue.com",
|
||||
col3: "Developer",
|
||||
col3: (
|
||||
<TableDropdown
|
||||
position={Position.BOTTOM}
|
||||
options={options}
|
||||
onSelect={selectedValue => console.log(selectedValue)}
|
||||
selectedIndex={0}
|
||||
></TableDropdown>
|
||||
),
|
||||
col4: "App Access",
|
||||
col5: (
|
||||
<Button
|
||||
|
|
@ -55,7 +79,14 @@ const data = [
|
|||
{
|
||||
col1: "Austin Howard",
|
||||
col2: "dustin_02@jlegue.com",
|
||||
col3: "User",
|
||||
col3: (
|
||||
<TableDropdown
|
||||
position={Position.BOTTOM}
|
||||
options={options}
|
||||
onSelect={selectedValue => console.log(selectedValue)}
|
||||
selectedIndex={1}
|
||||
></TableDropdown>
|
||||
),
|
||||
col4: "Map Access",
|
||||
col5: (
|
||||
<Button
|
||||
|
|
@ -70,7 +101,14 @@ const data = [
|
|||
{
|
||||
col1: "Justing Howard",
|
||||
col2: "dustin_03@jlegue.com",
|
||||
col3: "Admin",
|
||||
col3: (
|
||||
<TableDropdown
|
||||
position={Position.BOTTOM}
|
||||
options={options}
|
||||
onSelect={selectedValue => console.log(selectedValue)}
|
||||
selectedIndex={2}
|
||||
></TableDropdown>
|
||||
),
|
||||
col4: "Dm Access",
|
||||
col5: (
|
||||
<Button
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import React from "react";
|
|||
import { withKnobs, select, boolean, text } from "@storybook/addon-knobs";
|
||||
import { withDesign } from "storybook-addon-designs";
|
||||
import TableDropdown from "components/ads/TableDropdown";
|
||||
import { Position } from "@blueprintjs/core/lib/esm/common/position";
|
||||
import { StoryWrapper } from "./Tabs.stories";
|
||||
|
||||
export default {
|
||||
|
|
@ -12,25 +13,30 @@ export default {
|
|||
|
||||
const options = [
|
||||
{
|
||||
label: "Admin",
|
||||
value: "Can edit, view and invite other user to an app",
|
||||
name: "Admin",
|
||||
desc: "Can edit, view and invite other user to an app",
|
||||
},
|
||||
{
|
||||
label: "Developer",
|
||||
value: "Can view and invite other user to an app",
|
||||
name: "Developer",
|
||||
desc: "Can view and invite other user to an app",
|
||||
},
|
||||
{
|
||||
label: "User",
|
||||
value: "Can view and invite other user to an app and...",
|
||||
name: "User",
|
||||
desc: "Can view and invite other user to an app and...",
|
||||
},
|
||||
];
|
||||
|
||||
export const TableDropdownStory = () => (
|
||||
<StoryWrapper>
|
||||
<TableDropdown
|
||||
position={select(
|
||||
"position",
|
||||
[Position.RIGHT, Position.LEFT, Position.BOTTOM, Position.TOP],
|
||||
Position.BOTTOM,
|
||||
)}
|
||||
options={options}
|
||||
onSelect={(selectedValue: string) => console.log(selectedValue)}
|
||||
selectedOption={options[0]}
|
||||
onSelect={selectedValue => console.log(selectedValue)}
|
||||
selectedIndex={0}
|
||||
></TableDropdown>
|
||||
</StoryWrapper>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import React from "react";
|
||||
import { TabComponent } from "components/ads/Tabs";
|
||||
import { TabComponent, TabProp } from "components/ads/Tabs";
|
||||
import { select, text, withKnobs } from "@storybook/addon-knobs";
|
||||
import { withDesign } from "storybook-addon-designs";
|
||||
import { IconName } from "components/ads/Icon";
|
||||
|
|
@ -11,15 +11,8 @@ export default {
|
|||
decorators: [withKnobs, withDesign],
|
||||
};
|
||||
|
||||
type tabSingle = {
|
||||
key: string;
|
||||
title: string;
|
||||
panelComponent: JSX.Element;
|
||||
icon: IconName;
|
||||
};
|
||||
|
||||
const TabStory = (props: any) => {
|
||||
const tabArr: tabSingle[] = [
|
||||
const tabArr: TabProp[] = [
|
||||
{
|
||||
key: "1",
|
||||
title: props.title1,
|
||||
|
|
|
|||
|
|
@ -137,6 +137,8 @@ export const ReduxActionTypes: { [key: string]: string } = {
|
|||
FETCH_ORGS_SUCCESS: "FETCH_ORGS_SUCCES",
|
||||
FETCH_ORGS_INIT: "FETCH_ORGS_INIT",
|
||||
SAVE_ORG_INIT: "SAVE_ORG_INIT",
|
||||
SAVE_ORG_SUCCESS: "SAVE_ORG_SUCCESS",
|
||||
SET_CURRENT_ORG: "SET_CURRENT_ORG",
|
||||
SET_CURRENT_ORG_ID: "SET_CURRENT_ORG_ID",
|
||||
FETCH_CURRENT_ORG: "FETCH_CURRENT_ORG",
|
||||
STORE_DATASOURCE_REFS: "STORE_DATASOURCE_REFS",
|
||||
|
|
@ -233,10 +235,8 @@ export const ReduxActionTypes: { [key: string]: string } = {
|
|||
FETCH_ALL_ROLES_INIT: "FETCH_ALL_ROLES_INIT",
|
||||
DELETE_ORG_USER_INIT: "DELETE_ORG_USER_INIT",
|
||||
DELETE_ORG_USER_SUCCESS: "DELETE_ORG_USER_SUCCESS",
|
||||
DELETE_ORG_USER_ERROR: "DELETE_ORG_USER_ERROR",
|
||||
CHANGE_ORG_USER_ROLE_INIT: "CHANGE_ORG_USER_ROLE_INIT",
|
||||
CHANGE_ORG_USER_ROLE_SUCCESS: "CHANGE_ORG_USER_ROLE_SUCCESS",
|
||||
CHANGE_ORG_USER_ROLE_ERROR: "CHANGE_ORG_USER_ROLE_ERROR",
|
||||
SET_DEFAULT_REFINEMENT: "SET_DEFAULT_REFINEMENT",
|
||||
SET_HELP_MODAL_OPEN: "SET_HELP_MODAL_OPEN",
|
||||
SAVE_ACTION_NAME_INIT: "SAVE_ACTION_NAME_INIT",
|
||||
|
|
@ -276,6 +276,7 @@ export const ReduxActionErrorTypes: { [key: string]: string } = {
|
|||
WIDGET_ADD_CHILD_ERROR: "WIDGET_ADD_CHILD_ERROR",
|
||||
FETCH_PAGE_ERROR: "FETCH_PAGE_ERROR",
|
||||
SAVE_PAGE_ERROR: "SAVE_PAGE_ERROR",
|
||||
DELETE_ORG_USER_ERROR: "DELETE_ORG_USER_ERROR",
|
||||
FETCH_WIDGET_CARDS_ERROR: "FETCH_WIDGET_CARDS_ERROR",
|
||||
WIDGET_OPERATION_ERROR: "WIDGET_OPERATION_ERROR",
|
||||
FETCH_PROPERTY_PANE_CONFIGS_ERROR: "FETCH_PROPERTY_PANE_CONFIGS_ERROR",
|
||||
|
|
@ -305,6 +306,7 @@ export const ReduxActionErrorTypes: { [key: string]: string } = {
|
|||
LOGIN_USER_ERROR: "LOGIN_USER_ERROR",
|
||||
CREATE_USER_ERROR: "CREATE_USER_ERROR",
|
||||
RESET_USER_PASSWORD_ERROR: "RESET_USER_PASSWORD_ERROR",
|
||||
CHANGE_ORG_USER_ROLE_ERROR: "CHANGE_ORG_USER_ROLE_ERROR",
|
||||
SAVE_JS_EXECUTION_RECORD: "SAVE_JS_EXECUTION_RECORD",
|
||||
FETCH_PLUGINS_ERROR: "FETCH_PLUGINS_ERROR",
|
||||
UPDATE_ORG_NAME_ERROR: "UPDATE_ORG_NAME_ERROR",
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
export const ERROR_MESSAGE_SELECT_ACTION = "Please select an action";
|
||||
export const ERROR_MESSAGE_SELECT_ACTION_TYPE = "Please select an action type";
|
||||
export const ERROR_MESSAGE_NAME_EMPTY = "Please select a name";
|
||||
export const ERROR_MESSAGE_CREATE_APPLICATION =
|
||||
"We could not create the Application";
|
||||
export const API_PATH_START_WITH_SLASH_ERROR = "Path cannot start with /";
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ export type Org = {
|
|||
id: string;
|
||||
name: string;
|
||||
website?: string;
|
||||
email?: string;
|
||||
};
|
||||
|
||||
export type OrgUser = {
|
||||
|
|
|
|||
|
|
@ -173,7 +173,7 @@ class Applications extends Component<
|
|||
content: "Organization Settings",
|
||||
onSelect: () =>
|
||||
getOnSelectAction(DropdownOnSelectActions.REDIRECT, {
|
||||
path: `/org/${orgId}/settings`,
|
||||
path: `/org/${orgId}/settings/general`,
|
||||
}),
|
||||
},
|
||||
{
|
||||
|
|
@ -187,7 +187,7 @@ class Applications extends Component<
|
|||
content: "Members",
|
||||
onSelect: () =>
|
||||
getOnSelectAction(DropdownOnSelectActions.REDIRECT, {
|
||||
path: `/org/${orgId}/settings`,
|
||||
path: `/org/${orgId}/settings/members`,
|
||||
}),
|
||||
},
|
||||
],
|
||||
|
|
|
|||
|
|
@ -1,8 +1,12 @@
|
|||
import React, { ReactNode } from "react";
|
||||
import { Helmet } from "react-helmet";
|
||||
import styled from "styled-components";
|
||||
import { useLocation } from "react-router";
|
||||
|
||||
const Wrapper = styled.section`
|
||||
const Wrapper = styled.section<{
|
||||
background: string;
|
||||
}>`
|
||||
background: ${props => props.background};
|
||||
&& .fade {
|
||||
position: relative;
|
||||
}
|
||||
|
|
@ -34,7 +38,8 @@ const PageBody = styled.div`
|
|||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
align-items: flex-start;
|
||||
margin: ${props => props.theme.spaces[12]}px auto;
|
||||
padding-top: ${props => props.theme.spaces[12]}px;
|
||||
margin: 0 auto;
|
||||
& > * {
|
||||
width: 100%;
|
||||
}
|
||||
|
|
@ -45,15 +50,20 @@ type PageWrapperProps = {
|
|||
displayName?: string;
|
||||
};
|
||||
|
||||
export const PageWrapper = (props: PageWrapperProps) => (
|
||||
<Wrapper>
|
||||
<Helmet>
|
||||
<title>{`${
|
||||
props.displayName ? `${props.displayName} | ` : ""
|
||||
}Appsmith`}</title>
|
||||
</Helmet>
|
||||
<PageBody>{props.children}</PageBody>
|
||||
</Wrapper>
|
||||
);
|
||||
export const PageWrapper = (props: PageWrapperProps) => {
|
||||
const location = useLocation();
|
||||
const isSettingsPage = location.pathname.indexOf("settings") !== -1;
|
||||
|
||||
return (
|
||||
<Wrapper background={isSettingsPage ? "#1B1B1D" : "inherit"}>
|
||||
<Helmet>
|
||||
<title>{`${
|
||||
props.displayName ? `${props.displayName} | ` : ""
|
||||
}Appsmith`}</title>
|
||||
</Helmet>
|
||||
<PageBody>{props.children}</PageBody>
|
||||
</Wrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default PageWrapper;
|
||||
|
|
|
|||
123
app/client/src/pages/organization/General.tsx
Normal file
123
app/client/src/pages/organization/General.tsx
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
import React from "react";
|
||||
|
||||
import { saveOrg } from "actions/orgActions";
|
||||
import { SaveOrgRequest } from "api/OrgApi";
|
||||
import { throttle } from "lodash";
|
||||
import TextInput, {
|
||||
emailValidator,
|
||||
notEmptyValidator,
|
||||
} from "components/ads/TextInput";
|
||||
import { useSelector, useDispatch } from "react-redux";
|
||||
import { getCurrentOrg } from "selectors/organizationSelectors";
|
||||
import { useParams } from "react-router-dom";
|
||||
import styled from "styled-components";
|
||||
import Text, { TextType } from "components/ads/Text";
|
||||
import { Classes } from "@blueprintjs/core";
|
||||
import { getOrgLoadingStates } from "selectors/organizationSelectors";
|
||||
const InputLabelWrapper = styled.div`
|
||||
width: 200px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
const SettingWrapper = styled.div`
|
||||
width: 520px;
|
||||
display: flex;
|
||||
margin-bottom: 25px;
|
||||
`;
|
||||
|
||||
export const SettingsHeading = styled(Text)`
|
||||
color: white;
|
||||
display: inline-block;
|
||||
margin-top: 25px;
|
||||
margin-bottom: 32px;
|
||||
`;
|
||||
|
||||
const Loader = styled.div`
|
||||
height: 38px;
|
||||
width: 260px;
|
||||
border-radius: 0;
|
||||
`;
|
||||
|
||||
export function GeneralSettings() {
|
||||
const { orgId } = useParams();
|
||||
const dispatch = useDispatch();
|
||||
const currentOrg = useSelector(getCurrentOrg);
|
||||
function saveChanges(settings: SaveOrgRequest) {
|
||||
dispatch(saveOrg(settings));
|
||||
}
|
||||
|
||||
const throttleTimeout = 1000;
|
||||
|
||||
const onWorkspaceNameChange = throttle((newName: string) => {
|
||||
saveChanges({
|
||||
id: orgId as string,
|
||||
name: newName,
|
||||
});
|
||||
}, throttleTimeout);
|
||||
|
||||
const onWebsiteChange = throttle((newWebsite: string) => {
|
||||
saveChanges({
|
||||
id: orgId as string,
|
||||
website: newWebsite,
|
||||
});
|
||||
}, throttleTimeout);
|
||||
|
||||
const onEmailChange = throttle((newEmail: string) => {
|
||||
saveChanges({
|
||||
id: orgId as string,
|
||||
email: newEmail,
|
||||
});
|
||||
}, throttleTimeout);
|
||||
|
||||
const { isFetchingOrg } = useSelector(getOrgLoadingStates);
|
||||
|
||||
return (
|
||||
<>
|
||||
<SettingsHeading type={TextType.H2}>General</SettingsHeading>
|
||||
<SettingWrapper>
|
||||
<InputLabelWrapper>
|
||||
<Text type={TextType.H4}>Workspace</Text>
|
||||
</InputLabelWrapper>
|
||||
{isFetchingOrg && <Loader className={Classes.SKELETON}></Loader>}
|
||||
{!isFetchingOrg && (
|
||||
<TextInput
|
||||
validator={notEmptyValidator}
|
||||
placeholder="Workspace name"
|
||||
onChange={onWorkspaceNameChange}
|
||||
defaultValue={currentOrg.name}
|
||||
></TextInput>
|
||||
)}
|
||||
</SettingWrapper>
|
||||
|
||||
<SettingWrapper>
|
||||
<InputLabelWrapper>
|
||||
<Text type={TextType.H4}>Website</Text>
|
||||
</InputLabelWrapper>
|
||||
{isFetchingOrg && <Loader className={Classes.SKELETON}></Loader>}
|
||||
{!isFetchingOrg && (
|
||||
<TextInput
|
||||
placeholder="Your website"
|
||||
onChange={onWebsiteChange}
|
||||
defaultValue={currentOrg.website || ""}
|
||||
></TextInput>
|
||||
)}
|
||||
</SettingWrapper>
|
||||
|
||||
<SettingWrapper>
|
||||
<InputLabelWrapper>
|
||||
<Text type={TextType.H4}>Email</Text>
|
||||
</InputLabelWrapper>
|
||||
{isFetchingOrg && <Loader className={Classes.SKELETON}></Loader>}
|
||||
{!isFetchingOrg && (
|
||||
<TextInput
|
||||
validator={emailValidator}
|
||||
placeholder="Email"
|
||||
onChange={onEmailChange}
|
||||
defaultValue={currentOrg.email || ""}
|
||||
></TextInput>
|
||||
)}
|
||||
</SettingWrapper>
|
||||
</>
|
||||
);
|
||||
}
|
||||
171
app/client/src/pages/organization/Members.tsx
Normal file
171
app/client/src/pages/organization/Members.tsx
Normal file
|
|
@ -0,0 +1,171 @@
|
|||
import React, { useEffect } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
|
||||
import {
|
||||
getAllUsers,
|
||||
getAllRoles,
|
||||
getCurrentOrg,
|
||||
getOrgLoadingStates,
|
||||
} from "selectors/organizationSelectors";
|
||||
import PageSectionHeader from "pages/common/PageSectionHeader";
|
||||
import OrgInviteUsersForm from "pages/organization/OrgInviteUsersForm";
|
||||
import { RouteComponentProps } from "react-router";
|
||||
// import Spinner from "components/editorComponents/Spinner";
|
||||
import FormDialogComponent from "components/editorComponents/form/FormDialogComponent";
|
||||
import { getCurrentUser } from "selectors/usersSelectors";
|
||||
import Table from "components/ads/Table";
|
||||
import Icon from "components/ads/Icon";
|
||||
import {
|
||||
fetchUsersForOrg,
|
||||
fetchRolesForOrg,
|
||||
fetchOrg,
|
||||
changeOrgUserRole,
|
||||
deleteOrgUser,
|
||||
} from "actions/orgActions";
|
||||
import Button, { Size, Variant } from "components/ads/Button";
|
||||
import TableDropdown from "components/ads/TableDropdown";
|
||||
import { TextType } from "components/ads/Text";
|
||||
import { SettingsHeading } from "./General";
|
||||
import styled from "styled-components";
|
||||
import { Classes } from "@blueprintjs/core";
|
||||
|
||||
export type PageProps = RouteComponentProps<{
|
||||
orgId: string;
|
||||
}>;
|
||||
|
||||
const Loader = styled.div`
|
||||
height: 120px;
|
||||
width: 100%;
|
||||
`;
|
||||
export default function MemberSettings(props: PageProps) {
|
||||
const {
|
||||
match: {
|
||||
params: { orgId },
|
||||
},
|
||||
// deleteOrgUser,
|
||||
// changeOrgUserRole,
|
||||
} = props;
|
||||
|
||||
const dispatch = useDispatch();
|
||||
useEffect(() => {
|
||||
dispatch(fetchUsersForOrg(orgId));
|
||||
dispatch(fetchRolesForOrg(orgId));
|
||||
dispatch(fetchOrg(orgId));
|
||||
}, [orgId]);
|
||||
|
||||
const { isFetchingAllUsers, isFetchingAllRoles } = useSelector(
|
||||
getOrgLoadingStates,
|
||||
);
|
||||
const allUsers = useSelector(getAllUsers);
|
||||
const currentUser = useSelector(getCurrentUser);
|
||||
const currentOrg = useSelector(getCurrentOrg);
|
||||
|
||||
const userTableData = allUsers.map(user => ({
|
||||
...user,
|
||||
isCurrentUser: user.username === currentUser?.username,
|
||||
}));
|
||||
|
||||
const columns = [
|
||||
{
|
||||
Header: "Email",
|
||||
accessor: "username",
|
||||
},
|
||||
{
|
||||
Header: "Name",
|
||||
accessor: "name",
|
||||
},
|
||||
{
|
||||
Header: "Role",
|
||||
accessor: "roleName",
|
||||
Cell: function DropdownCell(cellProps: any) {
|
||||
const allRoles = useSelector(getAllRoles);
|
||||
const roles = allRoles
|
||||
? Object.keys(allRoles).map(role => {
|
||||
return {
|
||||
name: role,
|
||||
desc: allRoles[role],
|
||||
};
|
||||
})
|
||||
: [];
|
||||
const index = roles.findIndex(
|
||||
(role: { name: string; desc: string }) =>
|
||||
role.name === cellProps.cell.value,
|
||||
);
|
||||
if (
|
||||
cellProps.cell.row.values.username ===
|
||||
useSelector(getCurrentUser)?.username
|
||||
) {
|
||||
return cellProps.cell.value;
|
||||
}
|
||||
return (
|
||||
<TableDropdown
|
||||
selectedIndex={index}
|
||||
options={roles}
|
||||
onSelect={option => {
|
||||
dispatch(
|
||||
changeOrgUserRole(
|
||||
orgId,
|
||||
option.name,
|
||||
cellProps.cell.row.values.username,
|
||||
),
|
||||
);
|
||||
}}
|
||||
></TableDropdown>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
Header: "Delete",
|
||||
accessor: "delete",
|
||||
Cell: function DeleteCell(cellProps: any) {
|
||||
if (
|
||||
cellProps.cell.row.values.username ===
|
||||
useSelector(getCurrentUser)?.username
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<Icon
|
||||
name={"delete"}
|
||||
size={Size.large}
|
||||
cypressSelector="t--deleteUser"
|
||||
onClick={() => {
|
||||
dispatch(
|
||||
deleteOrgUser(orgId, cellProps.cell.row.values.username),
|
||||
);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const currentOrgName = currentOrg?.name ?? "";
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<PageSectionHeader>
|
||||
<SettingsHeading type={TextType.H2}>Manage Users</SettingsHeading>
|
||||
<FormDialogComponent
|
||||
trigger={
|
||||
<Button
|
||||
cypressSelector="t--invite-users"
|
||||
variant={Variant.info}
|
||||
text="Invite Users"
|
||||
size={Size.medium}
|
||||
></Button>
|
||||
}
|
||||
canOutsideClickClose={true}
|
||||
Form={OrgInviteUsersForm}
|
||||
orgId={orgId}
|
||||
title={`Invite Users to ${currentOrgName}`}
|
||||
/>
|
||||
</PageSectionHeader>
|
||||
{isFetchingAllUsers && isFetchingAllRoles ? (
|
||||
<Loader className={Classes.SKELETON} />
|
||||
) : (
|
||||
<Table data={userTableData} columns={columns}></Table>
|
||||
)}
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
|
@ -302,7 +302,7 @@ const OrgInviteUsersForm = (props: any) => {
|
|||
filled
|
||||
intent="primary"
|
||||
onClick={() => {
|
||||
history.push(`/org/${props.orgId}/settings`);
|
||||
history.push(`/org/${props.orgId}/settings/members`);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import React from "react";
|
||||
import { Switch, useRouteMatch, useLocation } from "react-router-dom";
|
||||
import PageWrapper from "pages/common/PageWrapper";
|
||||
import Settings from "./settings";
|
||||
import DefaultOrgPage from "./defaultOrgPage";
|
||||
import AppRoute from "pages/common/AppRoute";
|
||||
import Settings from "./settings";
|
||||
export const Organization = () => {
|
||||
const { path } = useRouteMatch();
|
||||
const location = useLocation();
|
||||
|
|
@ -11,7 +11,6 @@ export const Organization = () => {
|
|||
<PageWrapper displayName="Organization Settings">
|
||||
<Switch location={location}>
|
||||
<AppRoute
|
||||
exact
|
||||
path={`${path}/:orgId/settings`}
|
||||
component={Settings}
|
||||
name={"Settings"}
|
||||
|
|
|
|||
|
|
@ -1,393 +1,97 @@
|
|||
import React, { useEffect } from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { Icon } from "@blueprintjs/core";
|
||||
import { TableWrapper } from "components/designSystems/appsmith/TableStyledWrappers";
|
||||
import { CompactModeTypes, TABLE_SIZES } from "widgets/TableWidget";
|
||||
import { Colors } from "constants/Colors";
|
||||
import { AppState } from "reducers";
|
||||
import {
|
||||
getAllUsers,
|
||||
getAllRoles,
|
||||
getCurrentOrg,
|
||||
} from "selectors/organizationSelectors";
|
||||
import PageSectionDivider from "pages/common/PageSectionDivider";
|
||||
import PageSectionHeader from "pages/common/PageSectionHeader";
|
||||
import { ReduxActionTypes } from "constants/ReduxActionConstants";
|
||||
import OrgInviteUsersForm from "pages/organization/OrgInviteUsersForm";
|
||||
import Button from "components/editorComponents/Button";
|
||||
import { OrgUser, Org } from "constants/orgConstants";
|
||||
import { Menu, MenuItem, Popover, Position } from "@blueprintjs/core";
|
||||
import { useRouteMatch, useLocation, useParams, Link } from "react-router-dom";
|
||||
import AppRoute from "pages/common/AppRoute";
|
||||
import { getCurrentOrg } from "selectors/organizationSelectors";
|
||||
import { useSelector, useDispatch } from "react-redux";
|
||||
import { TabComponent, TabProp } from "components/ads/Tabs";
|
||||
import Text, { TextType } from "components/ads/Text";
|
||||
import history from "utils/history";
|
||||
import styled from "styled-components";
|
||||
import { FormIcons } from "icons/FormIcons";
|
||||
import { RouteComponentProps } from "react-router";
|
||||
import Spinner from "components/editorComponents/Spinner";
|
||||
import FormDialogComponent from "components/editorComponents/form/FormDialogComponent";
|
||||
import { getCurrentUser } from "selectors/usersSelectors";
|
||||
import { User } from "constants/userConstants";
|
||||
import { useTable, useFlexLayout } from "react-table";
|
||||
|
||||
type OrgProps = {
|
||||
currentOrg: Org;
|
||||
changeOrgName: (value: string) => void;
|
||||
fetchCurrentOrg: (orgId: string) => void;
|
||||
fetchUser: (orgId: string) => void;
|
||||
fetchAllRoles: (orgId: string) => void;
|
||||
deleteOrgUser: (orgId: string, username: string) => void;
|
||||
changeOrgUserRole: (orgId: string, role: string, username: string) => void;
|
||||
allUsers: OrgUser[];
|
||||
allRole: object;
|
||||
currentUser: User | undefined;
|
||||
isFetchAllUsers: boolean;
|
||||
isFetchAllRoles: boolean;
|
||||
};
|
||||
import MemberSettings from "./Members";
|
||||
import IconComponent from "components/designSystems/appsmith/IconComponent";
|
||||
import { fetchOrg } from "actions/orgActions";
|
||||
import { GeneralSettings } from "./General";
|
||||
|
||||
export type PageProps = OrgProps &
|
||||
RouteComponentProps<{
|
||||
orgId: string;
|
||||
}>;
|
||||
|
||||
export type MenuItemProps = {
|
||||
rolename: string;
|
||||
};
|
||||
|
||||
type DropdownProps = {
|
||||
activeItem: string;
|
||||
userRoles: object;
|
||||
username: string;
|
||||
changeOrgUserRole: (orgId: string, role: string, username: string) => void;
|
||||
orgId: string;
|
||||
};
|
||||
|
||||
const StyledDropDown = styled.div`
|
||||
cursor: pointer;
|
||||
`;
|
||||
|
||||
const StyledTableWrapped = styled(TableWrapper)`
|
||||
height: ${props => props.height}px;
|
||||
overflow: visible;
|
||||
.tableWrap {
|
||||
height: ${props => props.height}px;
|
||||
const LinkToApplications = styled(Link)`
|
||||
margin-bottom: 35px;
|
||||
width: auto;
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
.table {
|
||||
.tbody {
|
||||
height: ${props => props.height}px;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledMenu = styled(Menu)`
|
||||
&&&&.bp3-menu {
|
||||
max-width: 250px;
|
||||
svg {
|
||||
cursor: pointer;
|
||||
}
|
||||
`;
|
||||
|
||||
const RoleNameCell = (props: any) => {
|
||||
const {
|
||||
roleName,
|
||||
roles,
|
||||
username,
|
||||
isCurrentUser,
|
||||
isChangingRole,
|
||||
} = props.cellProps.row.original;
|
||||
|
||||
if (isCurrentUser) {
|
||||
return <div>{roleName}</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<Popover
|
||||
content={
|
||||
<Dropdown
|
||||
activeItem={roleName}
|
||||
userRoles={roles}
|
||||
username={username}
|
||||
changeOrgUserRole={props.changeOrgUserRole}
|
||||
orgId={props.orgId}
|
||||
/>
|
||||
}
|
||||
position={Position.BOTTOM_LEFT}
|
||||
>
|
||||
<StyledDropDown>
|
||||
{roleName}
|
||||
<Icon icon="chevron-down" />
|
||||
{isChangingRole ? <Spinner size={20} /> : undefined}
|
||||
</StyledDropDown>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
|
||||
const DeleteActionCell = (props: any) => {
|
||||
const { username, isCurrentUser, isDeleting } = props.cellProps.row.original;
|
||||
|
||||
return (
|
||||
!isCurrentUser &&
|
||||
(isDeleting ? (
|
||||
<Spinner size={20} />
|
||||
) : (
|
||||
<FormIcons.DELETE_ICON
|
||||
height={20}
|
||||
width={20}
|
||||
color={"grey"}
|
||||
background={"grey"}
|
||||
onClick={() => props.deleteOrgUser(props.orgId, username)}
|
||||
style={{ alignSelf: "center", cursor: "pointer" }}
|
||||
/>
|
||||
))
|
||||
);
|
||||
};
|
||||
|
||||
const Dropdown = (props: DropdownProps) => {
|
||||
return (
|
||||
<StyledMenu>
|
||||
{Object.entries(props.userRoles).map((role, index) => {
|
||||
const MenuContent = (
|
||||
<div>
|
||||
<span>
|
||||
<b>{role[0]}</b>
|
||||
</span>
|
||||
<div>{role[1]}</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<MenuItem
|
||||
multiline
|
||||
key={index}
|
||||
onClick={() =>
|
||||
props.changeOrgUserRole(props.orgId, role[0], props.username)
|
||||
}
|
||||
active={props.activeItem === role[0]}
|
||||
text={MenuContent}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</StyledMenu>
|
||||
);
|
||||
};
|
||||
|
||||
export const OrgSettings = (props: PageProps) => {
|
||||
const {
|
||||
match: {
|
||||
params: { orgId },
|
||||
},
|
||||
deleteOrgUser,
|
||||
changeOrgUserRole,
|
||||
fetchCurrentOrg,
|
||||
fetchUser,
|
||||
fetchAllRoles,
|
||||
currentOrg,
|
||||
} = props;
|
||||
|
||||
const userTableData = props.allUsers.map(user => ({
|
||||
...user,
|
||||
roles: props.allRole,
|
||||
isCurrentUser: user.username === props.currentUser?.username,
|
||||
}));
|
||||
const data = React.useMemo(() => userTableData, [userTableData]);
|
||||
|
||||
const tableHeight = React.useMemo(() => {
|
||||
const tableDataLength =
|
||||
userTableData.length * TABLE_SIZES[CompactModeTypes.DEFAULT].ROW_HEIGHT +
|
||||
TABLE_SIZES[CompactModeTypes.DEFAULT].COLUMN_HEADER_HEIGHT;
|
||||
return tableDataLength;
|
||||
}, [userTableData]);
|
||||
|
||||
const columns = React.useMemo(() => {
|
||||
return [
|
||||
{
|
||||
Header: "Email",
|
||||
accessor: "username",
|
||||
},
|
||||
{
|
||||
Header: "Name",
|
||||
accessor: "name",
|
||||
},
|
||||
{
|
||||
Header: "Role",
|
||||
accessor: "roleName",
|
||||
Cell: (cellProps: any) => {
|
||||
return RoleNameCell({ cellProps, changeOrgUserRole, orgId });
|
||||
},
|
||||
},
|
||||
{
|
||||
Header: "Delete",
|
||||
accessor: "delete",
|
||||
Cell: (cellProps: any) => {
|
||||
return DeleteActionCell({ cellProps, deleteOrgUser, orgId });
|
||||
},
|
||||
},
|
||||
];
|
||||
}, [orgId, deleteOrgUser, changeOrgUserRole]);
|
||||
|
||||
const currentOrgName = currentOrg?.name ?? "";
|
||||
const {
|
||||
getTableProps,
|
||||
getTableBodyProps,
|
||||
headerGroups,
|
||||
rows,
|
||||
prepareRow,
|
||||
} = useTable(
|
||||
{
|
||||
columns,
|
||||
data,
|
||||
manualPagination: true,
|
||||
},
|
||||
useFlexLayout,
|
||||
);
|
||||
|
||||
export default function Settings() {
|
||||
const { orgId } = useParams();
|
||||
const currentOrg = useSelector(getCurrentOrg);
|
||||
const { path } = useRouteMatch();
|
||||
const location = useLocation();
|
||||
const dispatch = useDispatch();
|
||||
useEffect(() => {
|
||||
fetchUser(orgId);
|
||||
fetchAllRoles(orgId);
|
||||
fetchCurrentOrg(orgId);
|
||||
}, [orgId, fetchUser, fetchAllRoles, fetchCurrentOrg]);
|
||||
return (
|
||||
<React.Fragment>
|
||||
<PageSectionHeader>
|
||||
<h2>{currentOrgName}</h2>
|
||||
</PageSectionHeader>
|
||||
<PageSectionDivider />
|
||||
<PageSectionHeader>
|
||||
<h2>Users</h2>
|
||||
<FormDialogComponent
|
||||
trigger={
|
||||
<Button
|
||||
intent="primary"
|
||||
text="Invite Users"
|
||||
icon="plus"
|
||||
iconAlignment="left"
|
||||
filled
|
||||
/>
|
||||
}
|
||||
canOutsideClickClose={true}
|
||||
Form={OrgInviteUsersForm}
|
||||
orgId={orgId}
|
||||
title={`Invite Users to ${currentOrgName}`}
|
||||
/>
|
||||
</PageSectionHeader>
|
||||
{props.isFetchAllUsers && props.isFetchAllRoles ? (
|
||||
<Spinner size={30} />
|
||||
) : (
|
||||
<StyledTableWrapped
|
||||
width={200}
|
||||
height={tableHeight}
|
||||
tableSizes={TABLE_SIZES[CompactModeTypes.DEFAULT]}
|
||||
backgroundColor={Colors.ATHENS_GRAY_DARKER}
|
||||
>
|
||||
<div className="tableWrap">
|
||||
<div {...getTableProps()} className="table">
|
||||
{headerGroups.map((headerGroup: any, index: number) => (
|
||||
<div
|
||||
key={index}
|
||||
{...headerGroup.getHeaderGroupProps()}
|
||||
className="tr"
|
||||
>
|
||||
{headerGroup.headers.map(
|
||||
(column: any, columnIndex: number) => (
|
||||
<div
|
||||
key={columnIndex}
|
||||
{...column.getHeaderProps()}
|
||||
className="th header-reorder"
|
||||
>
|
||||
<div
|
||||
className={
|
||||
!column.isHidden
|
||||
? "draggable-header"
|
||||
: "hidden-header"
|
||||
}
|
||||
>
|
||||
{column.render("Header")}
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
<div {...getTableBodyProps()} className="tbody">
|
||||
{rows.map((row: any, index: number) => {
|
||||
prepareRow(row);
|
||||
return (
|
||||
<div key={index} {...row.getRowProps()} className={"tr"}>
|
||||
{row.cells.map((cell: any, cellIndex: number) => {
|
||||
return (
|
||||
<div
|
||||
key={cellIndex}
|
||||
{...cell.getCellProps()}
|
||||
className="td"
|
||||
data-rowindex={index}
|
||||
data-colindex={cellIndex}
|
||||
>
|
||||
{cell.render("Cell")}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</StyledTableWrapped>
|
||||
)}
|
||||
</React.Fragment>
|
||||
dispatch(fetchOrg(orgId as string));
|
||||
}, []);
|
||||
|
||||
const SettingsRenderer = (
|
||||
<div>
|
||||
<AppRoute
|
||||
path={`${path}/general`}
|
||||
component={GeneralSettings}
|
||||
location={location}
|
||||
name={"Settings"}
|
||||
/>
|
||||
<AppRoute
|
||||
path={`${path}/members`}
|
||||
component={MemberSettings}
|
||||
location={location}
|
||||
name={"Settings"}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const mapStateToProps = (state: AppState) => ({
|
||||
allUsers: getAllUsers(state),
|
||||
allRole: getAllRoles(state),
|
||||
isFetchAllUsers: state.ui.orgs.loadingStates.isFetchAllUsers,
|
||||
isFetchAllRoles: state.ui.orgs.loadingStates.isFetchAllRoles,
|
||||
currentOrg: getCurrentOrg(state),
|
||||
currentUser: getCurrentUser(state),
|
||||
});
|
||||
const tabArr: TabProp[] = [
|
||||
{
|
||||
key: "general",
|
||||
title: "General",
|
||||
panelComponent: SettingsRenderer,
|
||||
icon: "general",
|
||||
},
|
||||
{
|
||||
key: "members",
|
||||
title: "Members",
|
||||
panelComponent: SettingsRenderer,
|
||||
icon: "user",
|
||||
},
|
||||
];
|
||||
const isMembersPage = location.pathname.indexOf("members") !== -1;
|
||||
|
||||
const mapDispatchToProps = (dispatch: any) => ({
|
||||
fetchCurrentOrg: (orgId: string) =>
|
||||
dispatch({
|
||||
type: ReduxActionTypes.FETCH_CURRENT_ORG,
|
||||
payload: {
|
||||
orgId,
|
||||
},
|
||||
}),
|
||||
changeOrgName: (name: string) =>
|
||||
dispatch({
|
||||
type: ReduxActionTypes.UPDATE_ORG_NAME_INIT,
|
||||
payload: {
|
||||
name,
|
||||
},
|
||||
}),
|
||||
changeOrgUserRole: (orgId: string, role: string, username: string) =>
|
||||
dispatch({
|
||||
type: ReduxActionTypes.CHANGE_ORG_USER_ROLE_INIT,
|
||||
payload: {
|
||||
orgId,
|
||||
role,
|
||||
username,
|
||||
},
|
||||
}),
|
||||
deleteOrgUser: (orgId: string, username: string) =>
|
||||
dispatch({
|
||||
type: ReduxActionTypes.DELETE_ORG_USER_INIT,
|
||||
payload: {
|
||||
orgId,
|
||||
username,
|
||||
},
|
||||
}),
|
||||
fetchUser: (orgId: string) =>
|
||||
dispatch({
|
||||
type: ReduxActionTypes.FETCH_ALL_USERS_INIT,
|
||||
payload: {
|
||||
orgId,
|
||||
},
|
||||
}),
|
||||
fetchAllRoles: (orgId: string) =>
|
||||
dispatch({
|
||||
type: ReduxActionTypes.FETCH_ALL_ROLES_INIT,
|
||||
payload: {
|
||||
orgId,
|
||||
},
|
||||
}),
|
||||
});
|
||||
return (
|
||||
<>
|
||||
<LinkToApplications to={"/applications"}>
|
||||
<IconComponent iconName="chevron-left" color="#9F9F9F"></IconComponent>
|
||||
<Text type={TextType.H1}>{currentOrg.name}</Text>
|
||||
</LinkToApplications>
|
||||
<TabComponent
|
||||
tabs={tabArr}
|
||||
selectedIndex={isMembersPage ? 1 : 0}
|
||||
onSelect={(index: number) => {
|
||||
const settingsStartIndex = location.pathname.indexOf("settings");
|
||||
const settingsEndIndex = settingsStartIndex + "settings".length;
|
||||
const hasSlash = location.pathname[settingsEndIndex] === "/";
|
||||
let newUrl = "";
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(OrgSettings);
|
||||
if (hasSlash) {
|
||||
newUrl = `${location.pathname.substr(0, settingsEndIndex)}/${
|
||||
tabArr[index].key
|
||||
}`;
|
||||
} else {
|
||||
newUrl = `${location.pathname}/${tabArr[index].key}`;
|
||||
}
|
||||
history.push(newUrl);
|
||||
}}
|
||||
></TabComponent>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { createReducer } from "utils/AppsmithUtils";
|
||||
import { createImmerReducer } from "utils/AppsmithUtils";
|
||||
import {
|
||||
ReduxAction,
|
||||
ReduxActionTypes,
|
||||
|
|
@ -11,6 +11,7 @@ const initialState: OrgReduxState = {
|
|||
fetchingRoles: false,
|
||||
isFetchAllRoles: false,
|
||||
isFetchAllUsers: false,
|
||||
isFetchingOrg: false,
|
||||
},
|
||||
orgUsers: [],
|
||||
orgRoles: [],
|
||||
|
|
@ -20,161 +21,121 @@ const initialState: OrgReduxState = {
|
|||
},
|
||||
};
|
||||
|
||||
const orgReducer = createReducer(initialState, {
|
||||
[ReduxActionTypes.FETCH_ORG_ROLES_INIT]: (state: OrgReduxState) => ({
|
||||
...state,
|
||||
loadingStates: {
|
||||
...state.loadingStates,
|
||||
fetchingRoles: true,
|
||||
},
|
||||
}),
|
||||
[ReduxActionTypes.FETCH_ALL_ROLES_INIT]: (state: OrgReduxState) => ({
|
||||
...state,
|
||||
loadingStates: {
|
||||
...state.loadingStates,
|
||||
isFetchAllRoles: true,
|
||||
},
|
||||
}),
|
||||
[ReduxActionTypes.FETCH_ALL_USERS_INIT]: (state: OrgReduxState) => ({
|
||||
...state,
|
||||
loadingStates: {
|
||||
...state.loadingStates,
|
||||
isFetchAllUsers: true,
|
||||
},
|
||||
}),
|
||||
|
||||
const orgReducer = createImmerReducer(initialState, {
|
||||
[ReduxActionTypes.FETCH_ORG_ROLES_INIT]: (draftState: OrgReduxState) => {
|
||||
draftState.loadingStates.isFetchAllRoles = true;
|
||||
},
|
||||
[ReduxActionTypes.FETCH_ALL_ROLES_INIT]: (draftState: OrgReduxState) => {
|
||||
draftState.loadingStates.isFetchAllRoles = true;
|
||||
},
|
||||
[ReduxActionTypes.FETCH_ALL_USERS_INIT]: (draftState: OrgReduxState) => {
|
||||
draftState.loadingStates.isFetchAllUsers = true;
|
||||
},
|
||||
[ReduxActionTypes.FETCH_ORG_ROLES_SUCCESS]: (
|
||||
state: OrgReduxState,
|
||||
draftState: OrgReduxState,
|
||||
action: ReduxAction<OrgRole[]>,
|
||||
) => ({
|
||||
...state,
|
||||
roles: action.payload,
|
||||
loadingStates: {
|
||||
...state.loadingStates,
|
||||
fetchingRoles: false,
|
||||
},
|
||||
}),
|
||||
[ReduxActionErrorTypes.FETCH_ORG_ROLES_ERROR]: (state: OrgReduxState) => ({
|
||||
...state,
|
||||
loadingStates: {
|
||||
...state.loadingStates,
|
||||
fetchingRoles: false,
|
||||
},
|
||||
}),
|
||||
) => {
|
||||
draftState.orgRoles = action.payload;
|
||||
draftState.loadingStates.fetchingRoles = false;
|
||||
},
|
||||
[ReduxActionErrorTypes.FETCH_ORG_ROLES_ERROR]: (
|
||||
draftState: OrgReduxState,
|
||||
) => {
|
||||
draftState.loadingStates.fetchingRoles = false;
|
||||
},
|
||||
[ReduxActionTypes.FETCH_ALL_USERS_SUCCESS]: (
|
||||
state: OrgReduxState,
|
||||
action: ReduxAction<Org[]>,
|
||||
) => ({
|
||||
...state,
|
||||
orgUsers: action.payload,
|
||||
loadingStates: {
|
||||
...state.loadingStates,
|
||||
isFetchAllUsers: false,
|
||||
},
|
||||
}),
|
||||
draftState: OrgReduxState,
|
||||
action: ReduxAction<OrgUser[]>,
|
||||
) => {
|
||||
draftState.orgUsers = action.payload;
|
||||
draftState.loadingStates.isFetchAllUsers = false;
|
||||
},
|
||||
[ReduxActionTypes.FETCH_ALL_ROLES_SUCCESS]: (
|
||||
state: OrgReduxState,
|
||||
draftState: OrgReduxState,
|
||||
action: ReduxAction<Org[]>,
|
||||
) => ({
|
||||
...state,
|
||||
orgRoles: action.payload,
|
||||
loadingStates: {
|
||||
...state.loadingStates,
|
||||
isFetchAllRoles: false,
|
||||
},
|
||||
}),
|
||||
) => {
|
||||
draftState.orgRoles = action.payload;
|
||||
draftState.loadingStates.isFetchAllRoles = false;
|
||||
},
|
||||
[ReduxActionTypes.CHANGE_ORG_USER_ROLE_SUCCESS]: (
|
||||
state: OrgReduxState,
|
||||
draftState: OrgReduxState,
|
||||
action: ReduxAction<{ username: string; roleName: string }>,
|
||||
) => {
|
||||
const _orgUsers = state.orgUsers.map((user: OrgUser) => {
|
||||
draftState.orgUsers.forEach((user: OrgUser) => {
|
||||
if (user.username === action.payload.username) {
|
||||
return {
|
||||
...user,
|
||||
roleName: action.payload.roleName,
|
||||
isChangingRole: false,
|
||||
};
|
||||
user.roleName = action.payload.roleName;
|
||||
}
|
||||
return user;
|
||||
});
|
||||
return {
|
||||
...state,
|
||||
orgUsers: _orgUsers,
|
||||
};
|
||||
},
|
||||
[ReduxActionTypes.CHANGE_ORG_USER_ROLE_INIT]: (
|
||||
state: OrgReduxState,
|
||||
draftState: OrgReduxState,
|
||||
action: ReduxAction<{ username: string }>,
|
||||
) => {
|
||||
const _orgUsers = state.orgUsers.map((user: OrgUser) => {
|
||||
draftState.orgUsers.forEach((user: OrgUser) => {
|
||||
if (user.username === action.payload.username) {
|
||||
return {
|
||||
...user,
|
||||
isChangingRole: true,
|
||||
};
|
||||
user.isChangingRole = true;
|
||||
}
|
||||
return user;
|
||||
});
|
||||
return { ...state, orgUsers: _orgUsers };
|
||||
},
|
||||
[ReduxActionTypes.DELETE_ORG_USER_INIT]: (
|
||||
state: OrgReduxState,
|
||||
draftState: OrgReduxState,
|
||||
action: ReduxAction<{ username: string }>,
|
||||
) => {
|
||||
const _orgUsers = state.orgUsers.map((user: OrgUser) => {
|
||||
draftState.orgUsers.forEach((user: OrgUser) => {
|
||||
if (user.username === action.payload.username) {
|
||||
return {
|
||||
...user,
|
||||
isDeleting: true,
|
||||
};
|
||||
user.isDeleting = true;
|
||||
}
|
||||
return user;
|
||||
});
|
||||
return { ...state, orgUsers: _orgUsers };
|
||||
},
|
||||
[ReduxActionTypes.DELETE_ORG_USER_SUCCESS]: (
|
||||
state: OrgReduxState,
|
||||
draftState: OrgReduxState,
|
||||
action: ReduxAction<{ username: string }>,
|
||||
) => {
|
||||
const _orgUsers = state.orgUsers.filter(
|
||||
draftState.orgUsers = draftState.orgUsers.filter(
|
||||
(user: OrgUser) => user.username !== action.payload.username,
|
||||
);
|
||||
return {
|
||||
...state,
|
||||
orgUsers: _orgUsers,
|
||||
};
|
||||
},
|
||||
[ReduxActionTypes.CHANGE_ORG_USER_ROLE_ERROR]: (state: OrgReduxState) => {
|
||||
const _orgUsers = state.orgUsers.map(user => ({
|
||||
...user,
|
||||
isChangingRole: false,
|
||||
}));
|
||||
return { ...state, orgUsers: _orgUsers };
|
||||
[ReduxActionErrorTypes.CHANGE_ORG_USER_ROLE_ERROR]: (
|
||||
draftState: OrgReduxState,
|
||||
) => {
|
||||
draftState.orgUsers.forEach((user: OrgUser) => {
|
||||
//TODO: This will change the status to false even if one role change api fails.
|
||||
user.isChangingRole = false;
|
||||
});
|
||||
},
|
||||
[ReduxActionTypes.DELETE_ORG_USER_ERROR]: (state: OrgReduxState) => {
|
||||
const _orgUsers = state.orgUsers.map(user => ({
|
||||
...user,
|
||||
isDeleting: false,
|
||||
}));
|
||||
return { ...state, orgUsers: _orgUsers };
|
||||
[ReduxActionErrorTypes.DELETE_ORG_USER_ERROR]: (
|
||||
draftState: OrgReduxState,
|
||||
) => {
|
||||
draftState.orgUsers.forEach((user: OrgUser) => {
|
||||
//TODO: This will change the status to false even if one delete fails.
|
||||
user.isDeleting = false;
|
||||
});
|
||||
},
|
||||
[ReduxActionTypes.SET_CURRENT_ORG_ID]: (
|
||||
state: OrgReduxState,
|
||||
draftState: OrgReduxState,
|
||||
action: ReduxAction<{ orgId: string }>,
|
||||
) => ({
|
||||
...state,
|
||||
currentOrg: {
|
||||
...state.currentOrg,
|
||||
id: action.payload.orgId,
|
||||
},
|
||||
}),
|
||||
[ReduxActionTypes.FETCH_ORG_SUCCESS]: (
|
||||
state: OrgReduxState,
|
||||
) => {
|
||||
draftState.currentOrg.id = action.payload.orgId;
|
||||
},
|
||||
[ReduxActionTypes.SET_CURRENT_ORG]: (
|
||||
draftState: OrgReduxState,
|
||||
action: ReduxAction<Org>,
|
||||
) => ({
|
||||
...state,
|
||||
currentOrg: action.payload,
|
||||
}),
|
||||
) => {
|
||||
draftState.currentOrg = action.payload;
|
||||
},
|
||||
[ReduxActionTypes.FETCH_CURRENT_ORG]: (draftState: OrgReduxState) => {
|
||||
draftState.loadingStates.isFetchingOrg = true;
|
||||
},
|
||||
[ReduxActionTypes.FETCH_ORG_SUCCESS]: (
|
||||
draftState: OrgReduxState,
|
||||
action: ReduxAction<Org>,
|
||||
) => {
|
||||
draftState.currentOrg = action.payload;
|
||||
draftState.loadingStates.isFetchingOrg = false;
|
||||
},
|
||||
[ReduxActionErrorTypes.FETCH_ORG_ERROR]: (draftState: OrgReduxState) => {
|
||||
draftState.loadingStates.isFetchingOrg = false;
|
||||
},
|
||||
});
|
||||
|
||||
export interface OrgReduxState {
|
||||
|
|
@ -184,6 +145,7 @@ export interface OrgReduxState {
|
|||
fetchingRoles: boolean;
|
||||
isFetchAllRoles: boolean;
|
||||
isFetchAllUsers: boolean;
|
||||
isFetchingOrg: boolean;
|
||||
};
|
||||
orgUsers: OrgUser[];
|
||||
orgRoles: any;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { call, takeLatest, put, all } from "redux-saga/effects";
|
||||
import { call, takeLatest, put, all, select } from "redux-saga/effects";
|
||||
import {
|
||||
ReduxActionTypes,
|
||||
ReduxAction,
|
||||
|
|
@ -26,6 +26,7 @@ import OrgApi, {
|
|||
import { ApiResponse } from "api/ApiResponses";
|
||||
import { AppToaster } from "components/editorComponents/ToastComponent";
|
||||
import { ToastType } from "react-toastify";
|
||||
import { getCurrentOrg } from "selectors/organizationSelectors";
|
||||
|
||||
export function* fetchRolesSaga() {
|
||||
try {
|
||||
|
|
@ -114,6 +115,9 @@ export function* changeOrgUserRoleSaga(
|
|||
} catch (error) {
|
||||
yield put({
|
||||
type: ReduxActionErrorTypes.CHANGE_ORG_USER_ROLE_ERROR,
|
||||
payload: {
|
||||
error,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -172,6 +176,17 @@ export function* saveOrgSaga(action: ReduxAction<SaveOrgRequest>) {
|
|||
const response: ApiResponse = yield call(OrgApi.saveOrg, request);
|
||||
const isValidResponse = yield validateResponse(response);
|
||||
if (isValidResponse) {
|
||||
const currentOrg = yield select(getCurrentOrg);
|
||||
if (currentOrg && currentOrg.id === request.id) {
|
||||
const updatedOrg = {
|
||||
...currentOrg,
|
||||
...request,
|
||||
};
|
||||
yield put({
|
||||
type: ReduxActionTypes.SET_CURRENT_ORG,
|
||||
payload: updatedOrg,
|
||||
});
|
||||
}
|
||||
yield put({
|
||||
type: ReduxActionTypes.SAVE_ORG_SUCCESS,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -6,6 +6,14 @@ export const getRolesFromState = (state: AppState) => {
|
|||
return state.ui.orgs.roles;
|
||||
};
|
||||
|
||||
export const getOrgLoadingStates = (state: AppState) => {
|
||||
return {
|
||||
isFetchingOrg: state.ui.orgs.loadingStates.isFetchingOrg,
|
||||
isFetchingAllUsers: state.ui.orgs.loadingStates.isFetchAllUsers,
|
||||
isFetchingAllRoles: state.ui.orgs.loadingStates.isFetchAllRoles,
|
||||
};
|
||||
};
|
||||
|
||||
export const getCurrentOrgId = (state: AppState) => state.ui.orgs.currentOrg.id;
|
||||
export const getOrgs = (state: AppState) => {
|
||||
return state.ui.applications.userOrgs;
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import * as log from "loglevel";
|
|||
import { LogLevelDesc } from "loglevel";
|
||||
import FeatureFlag from "utils/featureFlags";
|
||||
import { appCardColors } from "constants/AppConstants";
|
||||
import produce from "immer";
|
||||
|
||||
export const createReducer = (
|
||||
initialState: any,
|
||||
|
|
@ -24,6 +25,19 @@ export const createReducer = (
|
|||
};
|
||||
};
|
||||
|
||||
export const createImmerReducer = (
|
||||
initialState: any,
|
||||
handlers: { [type: string]: any },
|
||||
) => {
|
||||
return function reducer(state = initialState, action: ReduxAction<any>) {
|
||||
if (handlers.hasOwnProperty(action.type)) {
|
||||
return produce(handlers[action.type])(state, action);
|
||||
} else {
|
||||
return state;
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
export const appInitializer = () => {
|
||||
FormControlRegistry.registerFormControlBuilders();
|
||||
const appsmithConfigs = getAppsmithConfigs();
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user