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,
|
homePage.viewerRole,
|
||||||
);
|
);
|
||||||
cy.navigateToOrgSettings(orgid);
|
cy.navigateToOrgSettings(orgid);
|
||||||
cy.get(homePage.emailList).then(function($lis) {
|
cy.get(homePage.emailList).then(function($list) {
|
||||||
expect($lis).to.have.length(3);
|
expect($list).to.have.length(3);
|
||||||
expect($lis.eq(0)).to.contain(Cypress.env("USERNAME"));
|
expect($list.eq(0)).to.contain(Cypress.env("USERNAME"));
|
||||||
expect($lis.eq(1)).to.contain(Cypress.env("TESTUSERNAME1"));
|
expect($list.eq(1)).to.contain(Cypress.env("TESTUSERNAME1"));
|
||||||
expect($lis.eq(2)).to.contain(Cypress.env("TESTUSERNAME2"));
|
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.get(homePage.searchInput).type(appid);
|
||||||
cy.wait(2000);
|
cy.wait(2000);
|
||||||
cy.navigateToOrgSettings(orgid);
|
cy.navigateToOrgSettings(orgid);
|
||||||
cy.get(homePage.emailList).then(function($lis) {
|
cy.get(homePage.emailList).then(function($list) {
|
||||||
expect($lis).to.have.length(3);
|
expect($list).to.have.length(3);
|
||||||
expect($lis.eq(0)).to.contain(Cypress.env("USERNAME"));
|
expect($list.eq(0)).to.contain(Cypress.env("USERNAME"));
|
||||||
expect($lis.eq(1)).to.contain(Cypress.env("TESTUSERNAME1"));
|
expect($list.eq(1)).to.contain(Cypress.env("TESTUSERNAME1"));
|
||||||
expect($lis.eq(2)).to.contain(Cypress.env("TESTUSERNAME2"));
|
expect($list.eq(2)).to.contain(Cypress.env("TESTUSERNAME2"));
|
||||||
});
|
});
|
||||||
cy.xpath(homePage.appHome)
|
cy.xpath(homePage.appHome)
|
||||||
.should("be.visible")
|
.should("be.visible")
|
||||||
|
.first()
|
||||||
.click();
|
.click();
|
||||||
cy.wait("@applications").should(
|
cy.wait("@applications").should(
|
||||||
"have.nested.property",
|
"have.nested.property",
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,9 @@
|
||||||
"members": "//div[contains(text(),'Members')]",
|
"members": "//div[contains(text(),'Members')]",
|
||||||
"share": "//div[contains(text(),'Share')]",
|
"share": "//div[contains(text(),'Share')]",
|
||||||
"OrgSettings": "//div[contains(text(),'Organization Settings')]",
|
"OrgSettings": "//div[contains(text(),'Organization Settings')]",
|
||||||
|
"MemberSettings": "//div[contains(text(),'Members')]",
|
||||||
"inviteUser": "//span[text()='Invite Users']/parent::button",
|
"inviteUser": "//span[text()='Invite Users']/parent::button",
|
||||||
|
"inviteUserMembersPage": "[data-cy=t--invite-users]",
|
||||||
"email": "//input[@type='email']",
|
"email": "//input[@type='email']",
|
||||||
"selectRole": "//span[text()='Select a role']",
|
"selectRole": "//span[text()='Select a role']",
|
||||||
"adminRole": "//div[@class='bp3-overlay bp3-overlay-open']//div[contains(text(),'Administrator')]",
|
"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')]",
|
"developerRole": "//div[@class='bp3-overlay bp3-overlay-open']//div[contains(text(),'Developer')]",
|
||||||
"inviteBtn": "//span[text()='Invite']/parent::button",
|
"inviteBtn": "//span[text()='Invite']/parent::button",
|
||||||
"manageUsers": ".manageUsers",
|
"manageUsers": ".manageUsers",
|
||||||
"DeleteBtn": "//div[@data-colindex='3']//*[local-name()='svg']",
|
"DeleteBtn": "[data-cy=t--deleteUser]",
|
||||||
"ShareBtn": "//span[text()='Share']/parent::button",
|
"ShareBtn": "//span[text()='Share']/parent::button",
|
||||||
"launchBtn": "//span[text()='Launch']/parent::button",
|
"launchBtn": "//span[text()='Launch']/parent::button",
|
||||||
"appView": ".t--application-view-link",
|
"appView": ".t--application-view-link",
|
||||||
|
|
|
||||||
|
|
@ -39,13 +39,14 @@ Cypress.Commands.add("navigateToOrgSettings", orgName => {
|
||||||
.scrollIntoView()
|
.scrollIntoView()
|
||||||
.should("be.visible");
|
.should("be.visible");
|
||||||
cy.get(".t--org-name").click({ force: true });
|
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(
|
cy.wait("@getRoles").should(
|
||||||
"have.nested.property",
|
"have.nested.property",
|
||||||
"response.body.responseMeta.status",
|
"response.body.responseMeta.status",
|
||||||
200,
|
200,
|
||||||
);
|
);
|
||||||
cy.xpath(homePage.inviteUser).should("be.visible");
|
cy.get(homePage.inviteUserMembersPage).should("be.visible");
|
||||||
});
|
});
|
||||||
|
|
||||||
Cypress.Commands.add("inviteUserForOrg", (orgName, email, role) => {
|
Cypress.Commands.add("inviteUserForOrg", (orgName, email, role) => {
|
||||||
|
|
@ -70,6 +71,7 @@ Cypress.Commands.add("inviteUserForOrg", (orgName, email, role) => {
|
||||||
cy.contains(email);
|
cy.contains(email);
|
||||||
cy.get(homePage.manageUsers).click({ force: true });
|
cy.get(homePage.manageUsers).click({ force: true });
|
||||||
cy.xpath(homePage.appHome)
|
cy.xpath(homePage.appHome)
|
||||||
|
.first()
|
||||||
.should("be.visible")
|
.should("be.visible")
|
||||||
.click();
|
.click();
|
||||||
});
|
});
|
||||||
|
|
@ -79,14 +81,16 @@ Cypress.Commands.add("deleteUserFromOrg", (orgName, email) => {
|
||||||
.scrollIntoView()
|
.scrollIntoView()
|
||||||
.should("be.visible");
|
.should("be.visible");
|
||||||
cy.get(".t--org-name").click({ force: true });
|
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(
|
cy.wait("@getRoles").should(
|
||||||
"have.nested.property",
|
"have.nested.property",
|
||||||
"response.body.responseMeta.status",
|
"response.body.responseMeta.status",
|
||||||
200,
|
200,
|
||||||
);
|
);
|
||||||
cy.xpath(homePage.DeleteBtn).click({ force: true });
|
cy.get(homePage.DeleteBtn).click({ force: true });
|
||||||
cy.xpath(homePage.appHome)
|
cy.xpath(homePage.appHome)
|
||||||
|
.first()
|
||||||
.should("be.visible")
|
.should("be.visible")
|
||||||
.click();
|
.click();
|
||||||
cy.wait("@applications").should(
|
cy.wait("@applications").should(
|
||||||
|
|
@ -101,13 +105,13 @@ Cypress.Commands.add("updateUserRoleForOrg", (orgName, email, role) => {
|
||||||
.scrollIntoView()
|
.scrollIntoView()
|
||||||
.should("be.visible");
|
.should("be.visible");
|
||||||
cy.get(".t--org-name").click({ force: true });
|
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(
|
cy.wait("@getRoles").should(
|
||||||
"have.nested.property",
|
"have.nested.property",
|
||||||
"response.body.responseMeta.status",
|
"response.body.responseMeta.status",
|
||||||
200,
|
200,
|
||||||
);
|
);
|
||||||
cy.xpath(homePage.inviteUser).click({ force: true });
|
cy.get(homePage.inviteUserMembersPage).click({ force: true });
|
||||||
cy.xpath(homePage.email)
|
cy.xpath(homePage.email)
|
||||||
.click({ force: true })
|
.click({ force: true })
|
||||||
.type(email);
|
.type(email);
|
||||||
|
|
@ -122,6 +126,7 @@ Cypress.Commands.add("updateUserRoleForOrg", (orgName, email, role) => {
|
||||||
cy.contains(email);
|
cy.contains(email);
|
||||||
cy.get(".bp3-icon-small-cross").click({ force: true });
|
cy.get(".bp3-icon-small-cross").click({ force: true });
|
||||||
cy.xpath(homePage.appHome)
|
cy.xpath(homePage.appHome)
|
||||||
|
.first()
|
||||||
.should("be.visible")
|
.should("be.visible")
|
||||||
.click();
|
.click();
|
||||||
cy.wait("@applications").should(
|
cy.wait("@applications").should(
|
||||||
|
|
@ -1444,6 +1449,8 @@ Cypress.Commands.add("startServerAndRoutes", () => {
|
||||||
cy.route("DELETE", "/api/v1/datasources/*").as("deleteDatasource");
|
cy.route("DELETE", "/api/v1/datasources/*").as("deleteDatasource");
|
||||||
|
|
||||||
cy.route("GET", "/api/v1/organizations").as("organizations");
|
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/actions/execute").as("executeAction");
|
||||||
cy.route("POST", "/api/v1/applications/publish/*").as("publishApp");
|
cy.route("POST", "/api/v1/applications/publish/*").as("publishApp");
|
||||||
cy.route("PUT", "/api/v1/layouts/*/pages/*").as("updateLayout");
|
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 {
|
export interface SaveOrgRequest {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name?: string;
|
||||||
website: string;
|
website?: string;
|
||||||
|
email?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CreateOrgRequest {
|
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 = {
|
Button.defaultProps = {
|
||||||
category: Category.primary,
|
category: Category.primary,
|
||||||
variant: Variant.success,
|
variant: Variant.info,
|
||||||
size: Size.small,
|
size: Size.small,
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
disabled: false,
|
disabled: false,
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,8 @@ import { ReactComponent as CloseIcon } from "assets/icons/ads/close.svg";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
import { Size } from "./Button";
|
import { Size } from "./Button";
|
||||||
import { sizeHandler } from "./Spinner";
|
import { sizeHandler } from "./Spinner";
|
||||||
|
import { CommonComponentProps } from "./common";
|
||||||
|
import { noop } from "lodash";
|
||||||
|
|
||||||
export type IconName =
|
export type IconName =
|
||||||
| "Select icon"
|
| "Select icon"
|
||||||
|
|
@ -61,10 +63,10 @@ export type IconProps = {
|
||||||
name?: IconName;
|
name?: IconName;
|
||||||
invisible?: boolean;
|
invisible?: boolean;
|
||||||
className?: string;
|
className?: string;
|
||||||
click?: () => void;
|
onClick?: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const Icon = (props: IconProps) => {
|
const Icon = (props: IconProps & CommonComponentProps) => {
|
||||||
let returnIcon;
|
let returnIcon;
|
||||||
switch (props.name) {
|
switch (props.name) {
|
||||||
case "delete":
|
case "delete":
|
||||||
|
|
@ -101,8 +103,9 @@ const Icon = (props: IconProps) => {
|
||||||
return returnIcon ? (
|
return returnIcon ? (
|
||||||
<IconWrapper
|
<IconWrapper
|
||||||
className={props.className ? props.className : "ads-icon"}
|
className={props.className ? props.className : "ads-icon"}
|
||||||
|
data-cy={props.cypressSelector}
|
||||||
{...props}
|
{...props}
|
||||||
onClick={() => props.click && props.click()}
|
onClick={props.onClick || noop}
|
||||||
>
|
>
|
||||||
{returnIcon}
|
{returnIcon}
|
||||||
</IconWrapper>
|
</IconWrapper>
|
||||||
|
|
|
||||||
|
|
@ -132,7 +132,7 @@ const SearchInput = forwardRef(
|
||||||
name="close"
|
name="close"
|
||||||
size={Size.large}
|
size={Size.large}
|
||||||
className="close-icon"
|
className="close-icon"
|
||||||
click={() => setSearchValue("")}
|
onClick={() => setSearchValue("")}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
</InputWrapper>
|
</InputWrapper>
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
import { useTable, useSortBy } from "react-table";
|
import { useTable, useSortBy } from "react-table";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import styled from "styled-components";
|
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`
|
const Styles = styled.div`
|
||||||
table {
|
table {
|
||||||
|
|
@ -80,10 +81,13 @@ const Styles = styled.div`
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
function Table(props: any) {
|
interface TableProps {
|
||||||
const data = React.useMemo(() => props.data, []);
|
data: any[];
|
||||||
|
columns: any[];
|
||||||
|
}
|
||||||
|
|
||||||
const columns = React.useMemo(() => props.columns, []);
|
function Table(props: TableProps) {
|
||||||
|
const { data, columns } = props;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
getTableProps,
|
getTableProps,
|
||||||
|
|
@ -107,7 +111,7 @@ function Table(props: any) {
|
||||||
{column.render("Header")}
|
{column.render("Header")}
|
||||||
{column.isSorted ? (
|
{column.isSorted ? (
|
||||||
column.isSortedDesc ? (
|
column.isSortedDesc ? (
|
||||||
" 🔼"
|
<UpperArrow />
|
||||||
) : (
|
) : (
|
||||||
<DownArrow />
|
<DownArrow />
|
||||||
)
|
)
|
||||||
|
|
@ -126,7 +130,11 @@ function Table(props: any) {
|
||||||
<tr {...row.getRowProps()} key={index}>
|
<tr {...row.getRowProps()} key={index}>
|
||||||
{row.cells.map((cell, index) => {
|
{row.cells.map((cell, index) => {
|
||||||
return (
|
return (
|
||||||
<td {...cell.getCellProps()} key={index}>
|
<td
|
||||||
|
{...cell.getCellProps()}
|
||||||
|
key={index}
|
||||||
|
data-colindex={index}
|
||||||
|
>
|
||||||
{cell.render("Cell")}
|
{cell.render("Cell")}
|
||||||
</td>
|
</td>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -3,23 +3,24 @@ import { CommonComponentProps, hexToRgba } from "./common";
|
||||||
import { ReactComponent as DownArrow } from "assets/icons/ads/down_arrow.svg";
|
import { ReactComponent as DownArrow } from "assets/icons/ads/down_arrow.svg";
|
||||||
import Text, { TextType } from "./Text";
|
import Text, { TextType } from "./Text";
|
||||||
import styled from "styled-components";
|
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 = {
|
type DropdownOption = {
|
||||||
label: string;
|
name: string;
|
||||||
value: string;
|
desc: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type DropdownProps = CommonComponentProps & {
|
type DropdownProps = CommonComponentProps & {
|
||||||
options: DropdownOption[];
|
options: DropdownOption[];
|
||||||
onSelect: (selectedValue: string) => void;
|
onSelect: (selectedValue: DropdownOption) => void;
|
||||||
selectedOption: DropdownOption;
|
selectedIndex: number;
|
||||||
|
position?: Position;
|
||||||
};
|
};
|
||||||
|
|
||||||
const DropdownWrapper = styled.div`
|
|
||||||
width: 100%;
|
|
||||||
position: relative;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const SelectedItem = styled.div`
|
const SelectedItem = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
@ -32,9 +33,6 @@ const SelectedItem = styled.div`
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const OptionsWrapper = styled.div`
|
const OptionsWrapper = styled.div`
|
||||||
position: absolute;
|
|
||||||
margin-top: ${props => props.theme.spaces[8]}px;
|
|
||||||
left: -60px;
|
|
||||||
width: 200px;
|
width: 200px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
@ -45,17 +43,16 @@ const OptionsWrapper = styled.div`
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const DropdownOption = styled.div<{
|
const DropdownOption = styled.div<{
|
||||||
selected: DropdownOption;
|
isSelected: boolean;
|
||||||
option: DropdownOption;
|
|
||||||
}>`
|
}>`
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
padding: 10px 12px;
|
padding: 10px 12px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
background-color: ${props =>
|
${props =>
|
||||||
props.option.label === props.selected.label
|
props.isSelected
|
||||||
? props.theme.colors.blackShades[4]
|
? `background-color: ${props.theme.colors.blackShades[4]}`
|
||||||
: "transparent"};
|
: null};
|
||||||
|
|
||||||
span:last-child {
|
span:last-child {
|
||||||
margin-top: ${props => props.theme.spaces[1] + 1}px;
|
margin-top: ${props => props.theme.spaces[1] + 1}px;
|
||||||
|
|
@ -69,40 +66,44 @@ const DropdownOption = styled.div<{
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const TableDropdown = (props: DropdownProps) => {
|
const TableDropdown = (props: DropdownProps) => {
|
||||||
const [selected, setSelected] = useState(props.selectedOption);
|
const [selectedIndex, setSelectedIndex] = useState(props.selectedIndex);
|
||||||
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
|
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
|
||||||
|
const [selectedOption, setSelectedOption] = useState(
|
||||||
|
props.options[props.selectedIndex] || {},
|
||||||
|
);
|
||||||
|
|
||||||
const dropdownHandler = () => {
|
const optionSelector = (index: number) => {
|
||||||
setIsDropdownOpen(!isDropdownOpen);
|
setSelectedIndex(index);
|
||||||
};
|
setSelectedOption(props.options[index]);
|
||||||
|
props.onSelect && props.onSelect(props.options[index]);
|
||||||
const optionSelector = (option: DropdownOption) => {
|
|
||||||
setSelected(option);
|
|
||||||
setIsDropdownOpen(false);
|
setIsDropdownOpen(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DropdownWrapper>
|
<Popover
|
||||||
<SelectedItem onClick={() => dropdownHandler()}>
|
usePortal={false}
|
||||||
<Text type={TextType.P1}>{selected.label}</Text>
|
position={props.position || Position.BOTTOM_LEFT}
|
||||||
|
isOpen={isDropdownOpen}
|
||||||
|
onInteraction={state => setIsDropdownOpen(state)}
|
||||||
|
interactionKind={PopoverInteractionKind.CLICK}
|
||||||
|
>
|
||||||
|
<SelectedItem>
|
||||||
|
<Text type={TextType.P1}>{selectedOption.name}</Text>
|
||||||
<DownArrow />
|
<DownArrow />
|
||||||
</SelectedItem>
|
</SelectedItem>
|
||||||
{isDropdownOpen ? (
|
|
||||||
<OptionsWrapper>
|
<OptionsWrapper>
|
||||||
{props.options.map((el: DropdownOption, index: number) => (
|
{props.options.map((el: DropdownOption, index: number) => (
|
||||||
<DropdownOption
|
<DropdownOption
|
||||||
key={index}
|
key={index}
|
||||||
selected={selected}
|
isSelected={selectedIndex === index}
|
||||||
option={el}
|
onClick={() => optionSelector(index)}
|
||||||
onClick={() => optionSelector(el)}
|
|
||||||
>
|
>
|
||||||
<Text type={TextType.H5}>{el.label}</Text>
|
<Text type={TextType.H5}>{el.name}</Text>
|
||||||
<Text type={TextType.P3}>{el.value}</Text>
|
<Text type={TextType.P3}>{el.desc}</Text>
|
||||||
</DropdownOption>
|
</DropdownOption>
|
||||||
))}
|
))}
|
||||||
</OptionsWrapper>
|
</OptionsWrapper>
|
||||||
) : null}
|
</Popover>
|
||||||
</DropdownWrapper>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,13 @@ import styled from "styled-components";
|
||||||
import Icon, { IconName } from "./Icon";
|
import Icon, { IconName } from "./Icon";
|
||||||
import { Size } from "./Button";
|
import { Size } from "./Button";
|
||||||
|
|
||||||
|
export type TabProp = {
|
||||||
|
key: string;
|
||||||
|
title: string;
|
||||||
|
panelComponent: JSX.Element;
|
||||||
|
icon: IconName;
|
||||||
|
};
|
||||||
|
|
||||||
const TabsWrapper = styled.div<{ shouldOverflow?: boolean }>`
|
const TabsWrapper = styled.div<{ shouldOverflow?: boolean }>`
|
||||||
user-select: none;
|
user-select: none;
|
||||||
border-radius: 0px;
|
border-radius: 0px;
|
||||||
|
|
@ -59,14 +66,6 @@ const TabsWrapper = styled.div<{ shouldOverflow?: boolean }>`
|
||||||
fill: ${props => props.theme.colors.blackShades[9]};
|
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 {
|
.react-tabs__tab--selected {
|
||||||
color: ${props => props.theme.colors.blackShades[9]};
|
color: ${props => props.theme.colors.blackShades[9]};
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
|
|
@ -85,10 +84,21 @@ const TabsWrapper = styled.div<{ shouldOverflow?: boolean }>`
|
||||||
background-color: ${props => props.theme.colors.info.main};
|
background-color: ${props => props.theme.colors.info.main};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.react-tabs__tab:focus:after {
|
.react-tabs__tab:focus {
|
||||||
content: none;
|
&::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;
|
height: ${props => props.theme.spaces[1] - 2}px;
|
||||||
background: ${props => props.theme.colors.info.main};
|
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 = {
|
type TabbedViewComponentType = {
|
||||||
tabs: Array<{
|
tabs: Array<TabProp>;
|
||||||
key: string;
|
|
||||||
title: string;
|
|
||||||
panelComponent: JSX.Element;
|
|
||||||
icon?: IconName;
|
|
||||||
}>;
|
|
||||||
selectedIndex?: number;
|
selectedIndex?: number;
|
||||||
setSelectedIndex?: Function;
|
onSelect?: Function;
|
||||||
overflow?: boolean;
|
overflow?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -117,7 +122,7 @@ export const TabComponent = (props: TabbedViewComponentType) => {
|
||||||
<Tabs
|
<Tabs
|
||||||
selectedIndex={props.selectedIndex}
|
selectedIndex={props.selectedIndex}
|
||||||
onSelect={(index: number) => {
|
onSelect={(index: number) => {
|
||||||
props.setSelectedIndex && props.setSelectedIndex(index);
|
props.onSelect && props.onSelect(index);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<TabList>
|
<TabList>
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,34 @@ import { CommonComponentProps, hexToRgba } from "./common";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
import Text, { TextType } from "./Text";
|
import Text, { TextType } from "./Text";
|
||||||
import { theme } from "constants/DefaultTheme";
|
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 & {
|
export type TextInputProps = CommonComponentProps & {
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
|
|
@ -43,7 +71,6 @@ const StyledInput = styled.input<
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
outline: 0;
|
outline: 0;
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
margin-bottom: ${props => props.theme.spaces[1]}px;
|
|
||||||
border: 1px solid ${props => props.inputStyle.borderColor};
|
border: 1px solid ${props => props.inputStyle.borderColor};
|
||||||
padding: ${props => props.theme.spaces[4]}px
|
padding: ${props => props.theme.spaces[4]}px
|
||||||
${props => props.theme.spaces[6]}px;
|
${props => props.theme.spaces[6]}px;
|
||||||
|
|
@ -73,12 +100,17 @@ const InputWrapper = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
span {
|
span {
|
||||||
color: ${props => props.theme.colors.danger.main};
|
color: ${props => props.theme.colors.danger.main};
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
const ErrorWrapper = styled.div`
|
||||||
|
position absolute;
|
||||||
|
bottom: -17px;
|
||||||
|
`;
|
||||||
const TextInput = forwardRef(
|
const TextInput = forwardRef(
|
||||||
(props: TextInputProps, ref: Ref<HTMLInputElement>) => {
|
(props: TextInputProps, ref: Ref<HTMLInputElement>) => {
|
||||||
const initialValidation = () => {
|
const initialValidation = () => {
|
||||||
|
|
@ -101,13 +133,26 @@ const TextInput = forwardRef(
|
||||||
|
|
||||||
const memoizedChangeHandler = useCallback(
|
const memoizedChangeHandler = useCallback(
|
||||||
el => {
|
el => {
|
||||||
props.validator && setValidation(props.validator(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);
|
return props.onChange && props.onChange(el.target.value);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
[props],
|
[props],
|
||||||
);
|
);
|
||||||
|
|
||||||
const ErrorMessage = <Text type={TextType.P3}>{validation.message}</Text>;
|
const ErrorMessage = (
|
||||||
|
<ErrorWrapper>
|
||||||
|
<Text type={TextType.P3}>{validation.message}</Text>
|
||||||
|
</ErrorWrapper>
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<InputWrapper>
|
<InputWrapper>
|
||||||
|
|
@ -118,19 +163,15 @@ const TextInput = forwardRef(
|
||||||
isValid={validation.isValid}
|
isValid={validation.isValid}
|
||||||
defaultValue={props.defaultValue}
|
defaultValue={props.defaultValue}
|
||||||
{...props}
|
{...props}
|
||||||
placeholder={props.placeholder ? props.placeholder : ""}
|
placeholder={props.placeholder}
|
||||||
onChange={memoizedChangeHandler}
|
onChange={memoizedChangeHandler}
|
||||||
/>
|
/>
|
||||||
{validation.isValid ? null : ErrorMessage}
|
{ErrorMessage}
|
||||||
</InputWrapper>
|
</InputWrapper>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
TextInput.defaultProps = {
|
|
||||||
fill: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
TextInput.displayName = "TextInput";
|
TextInput.displayName = "TextInput";
|
||||||
|
|
||||||
export default TextInput;
|
export default TextInput;
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
import Icon from "components/ads/Icon";
|
||||||
import Button, { Size, Category, Variant } from "components/ads/Button";
|
import Button, { Size, Category, Variant } from "components/ads/Button";
|
||||||
import { withKnobs, select, boolean } from "@storybook/addon-knobs";
|
import { withKnobs, select, boolean } from "@storybook/addon-knobs";
|
||||||
import { withDesign } from "storybook-addon-designs";
|
import { withDesign } from "storybook-addon-designs";
|
||||||
import Icon from "components/ads/Icon";
|
|
||||||
import AppIcon, { AppIconName } from "components/ads/AppIcon";
|
import AppIcon, { AppIconName } from "components/ads/AppIcon";
|
||||||
import { StoryWrapper } from "./Tabs.stories";
|
import { StoryWrapper } from "./Tabs.stories";
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,8 @@ import React from "react";
|
||||||
import Table from "components/ads/Table";
|
import Table from "components/ads/Table";
|
||||||
import Button, { Category, Variant, Size } from "components/ads/Button";
|
import Button, { Category, Variant, Size } from "components/ads/Button";
|
||||||
import Icon from "components/ads/Icon";
|
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";
|
import { StoryWrapper } from "./Tabs.stories";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
|
@ -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 = [
|
const data = [
|
||||||
{
|
{
|
||||||
col1: "Dustin Howard",
|
col1: "Dustin Howard",
|
||||||
col2: "dustin_01@jlegue.com",
|
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",
|
col4: "App Access",
|
||||||
col5: (
|
col5: (
|
||||||
<Button
|
<Button
|
||||||
|
|
@ -55,7 +79,14 @@ const data = [
|
||||||
{
|
{
|
||||||
col1: "Austin Howard",
|
col1: "Austin Howard",
|
||||||
col2: "dustin_02@jlegue.com",
|
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",
|
col4: "Map Access",
|
||||||
col5: (
|
col5: (
|
||||||
<Button
|
<Button
|
||||||
|
|
@ -70,7 +101,14 @@ const data = [
|
||||||
{
|
{
|
||||||
col1: "Justing Howard",
|
col1: "Justing Howard",
|
||||||
col2: "dustin_03@jlegue.com",
|
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",
|
col4: "Dm Access",
|
||||||
col5: (
|
col5: (
|
||||||
<Button
|
<Button
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import React from "react";
|
||||||
import { withKnobs, select, boolean, text } from "@storybook/addon-knobs";
|
import { withKnobs, select, boolean, text } from "@storybook/addon-knobs";
|
||||||
import { withDesign } from "storybook-addon-designs";
|
import { withDesign } from "storybook-addon-designs";
|
||||||
import TableDropdown from "components/ads/TableDropdown";
|
import TableDropdown from "components/ads/TableDropdown";
|
||||||
|
import { Position } from "@blueprintjs/core/lib/esm/common/position";
|
||||||
import { StoryWrapper } from "./Tabs.stories";
|
import { StoryWrapper } from "./Tabs.stories";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
|
@ -12,25 +13,30 @@ export default {
|
||||||
|
|
||||||
const options = [
|
const options = [
|
||||||
{
|
{
|
||||||
label: "Admin",
|
name: "Admin",
|
||||||
value: "Can edit, view and invite other user to an app",
|
desc: "Can edit, view and invite other user to an app",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Developer",
|
name: "Developer",
|
||||||
value: "Can view and invite other user to an app",
|
desc: "Can view and invite other user to an app",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "User",
|
name: "User",
|
||||||
value: "Can view and invite other user to an app and...",
|
desc: "Can view and invite other user to an app and...",
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export const TableDropdownStory = () => (
|
export const TableDropdownStory = () => (
|
||||||
<StoryWrapper>
|
<StoryWrapper>
|
||||||
<TableDropdown
|
<TableDropdown
|
||||||
|
position={select(
|
||||||
|
"position",
|
||||||
|
[Position.RIGHT, Position.LEFT, Position.BOTTOM, Position.TOP],
|
||||||
|
Position.BOTTOM,
|
||||||
|
)}
|
||||||
options={options}
|
options={options}
|
||||||
onSelect={(selectedValue: string) => console.log(selectedValue)}
|
onSelect={selectedValue => console.log(selectedValue)}
|
||||||
selectedOption={options[0]}
|
selectedIndex={0}
|
||||||
></TableDropdown>
|
></TableDropdown>
|
||||||
</StoryWrapper>
|
</StoryWrapper>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import React from "react";
|
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 { select, text, withKnobs } from "@storybook/addon-knobs";
|
||||||
import { withDesign } from "storybook-addon-designs";
|
import { withDesign } from "storybook-addon-designs";
|
||||||
import { IconName } from "components/ads/Icon";
|
import { IconName } from "components/ads/Icon";
|
||||||
|
|
@ -11,15 +11,8 @@ export default {
|
||||||
decorators: [withKnobs, withDesign],
|
decorators: [withKnobs, withDesign],
|
||||||
};
|
};
|
||||||
|
|
||||||
type tabSingle = {
|
|
||||||
key: string;
|
|
||||||
title: string;
|
|
||||||
panelComponent: JSX.Element;
|
|
||||||
icon: IconName;
|
|
||||||
};
|
|
||||||
|
|
||||||
const TabStory = (props: any) => {
|
const TabStory = (props: any) => {
|
||||||
const tabArr: tabSingle[] = [
|
const tabArr: TabProp[] = [
|
||||||
{
|
{
|
||||||
key: "1",
|
key: "1",
|
||||||
title: props.title1,
|
title: props.title1,
|
||||||
|
|
|
||||||
|
|
@ -137,6 +137,8 @@ export const ReduxActionTypes: { [key: string]: string } = {
|
||||||
FETCH_ORGS_SUCCESS: "FETCH_ORGS_SUCCES",
|
FETCH_ORGS_SUCCESS: "FETCH_ORGS_SUCCES",
|
||||||
FETCH_ORGS_INIT: "FETCH_ORGS_INIT",
|
FETCH_ORGS_INIT: "FETCH_ORGS_INIT",
|
||||||
SAVE_ORG_INIT: "SAVE_ORG_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",
|
SET_CURRENT_ORG_ID: "SET_CURRENT_ORG_ID",
|
||||||
FETCH_CURRENT_ORG: "FETCH_CURRENT_ORG",
|
FETCH_CURRENT_ORG: "FETCH_CURRENT_ORG",
|
||||||
STORE_DATASOURCE_REFS: "STORE_DATASOURCE_REFS",
|
STORE_DATASOURCE_REFS: "STORE_DATASOURCE_REFS",
|
||||||
|
|
@ -233,10 +235,8 @@ export const ReduxActionTypes: { [key: string]: string } = {
|
||||||
FETCH_ALL_ROLES_INIT: "FETCH_ALL_ROLES_INIT",
|
FETCH_ALL_ROLES_INIT: "FETCH_ALL_ROLES_INIT",
|
||||||
DELETE_ORG_USER_INIT: "DELETE_ORG_USER_INIT",
|
DELETE_ORG_USER_INIT: "DELETE_ORG_USER_INIT",
|
||||||
DELETE_ORG_USER_SUCCESS: "DELETE_ORG_USER_SUCCESS",
|
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_INIT: "CHANGE_ORG_USER_ROLE_INIT",
|
||||||
CHANGE_ORG_USER_ROLE_SUCCESS: "CHANGE_ORG_USER_ROLE_SUCCESS",
|
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_DEFAULT_REFINEMENT: "SET_DEFAULT_REFINEMENT",
|
||||||
SET_HELP_MODAL_OPEN: "SET_HELP_MODAL_OPEN",
|
SET_HELP_MODAL_OPEN: "SET_HELP_MODAL_OPEN",
|
||||||
SAVE_ACTION_NAME_INIT: "SAVE_ACTION_NAME_INIT",
|
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",
|
WIDGET_ADD_CHILD_ERROR: "WIDGET_ADD_CHILD_ERROR",
|
||||||
FETCH_PAGE_ERROR: "FETCH_PAGE_ERROR",
|
FETCH_PAGE_ERROR: "FETCH_PAGE_ERROR",
|
||||||
SAVE_PAGE_ERROR: "SAVE_PAGE_ERROR",
|
SAVE_PAGE_ERROR: "SAVE_PAGE_ERROR",
|
||||||
|
DELETE_ORG_USER_ERROR: "DELETE_ORG_USER_ERROR",
|
||||||
FETCH_WIDGET_CARDS_ERROR: "FETCH_WIDGET_CARDS_ERROR",
|
FETCH_WIDGET_CARDS_ERROR: "FETCH_WIDGET_CARDS_ERROR",
|
||||||
WIDGET_OPERATION_ERROR: "WIDGET_OPERATION_ERROR",
|
WIDGET_OPERATION_ERROR: "WIDGET_OPERATION_ERROR",
|
||||||
FETCH_PROPERTY_PANE_CONFIGS_ERROR: "FETCH_PROPERTY_PANE_CONFIGS_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",
|
LOGIN_USER_ERROR: "LOGIN_USER_ERROR",
|
||||||
CREATE_USER_ERROR: "CREATE_USER_ERROR",
|
CREATE_USER_ERROR: "CREATE_USER_ERROR",
|
||||||
RESET_USER_PASSWORD_ERROR: "RESET_USER_PASSWORD_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",
|
SAVE_JS_EXECUTION_RECORD: "SAVE_JS_EXECUTION_RECORD",
|
||||||
FETCH_PLUGINS_ERROR: "FETCH_PLUGINS_ERROR",
|
FETCH_PLUGINS_ERROR: "FETCH_PLUGINS_ERROR",
|
||||||
UPDATE_ORG_NAME_ERROR: "UPDATE_ORG_NAME_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 = "Please select an action";
|
||||||
export const ERROR_MESSAGE_SELECT_ACTION_TYPE = "Please select an action type";
|
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 =
|
export const ERROR_MESSAGE_CREATE_APPLICATION =
|
||||||
"We could not create the Application";
|
"We could not create the Application";
|
||||||
export const API_PATH_START_WITH_SLASH_ERROR = "Path cannot start with /";
|
export const API_PATH_START_WITH_SLASH_ERROR = "Path cannot start with /";
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ export type Org = {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
website?: string;
|
website?: string;
|
||||||
|
email?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type OrgUser = {
|
export type OrgUser = {
|
||||||
|
|
|
||||||
|
|
@ -173,7 +173,7 @@ class Applications extends Component<
|
||||||
content: "Organization Settings",
|
content: "Organization Settings",
|
||||||
onSelect: () =>
|
onSelect: () =>
|
||||||
getOnSelectAction(DropdownOnSelectActions.REDIRECT, {
|
getOnSelectAction(DropdownOnSelectActions.REDIRECT, {
|
||||||
path: `/org/${orgId}/settings`,
|
path: `/org/${orgId}/settings/general`,
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -187,7 +187,7 @@ class Applications extends Component<
|
||||||
content: "Members",
|
content: "Members",
|
||||||
onSelect: () =>
|
onSelect: () =>
|
||||||
getOnSelectAction(DropdownOnSelectActions.REDIRECT, {
|
getOnSelectAction(DropdownOnSelectActions.REDIRECT, {
|
||||||
path: `/org/${orgId}/settings`,
|
path: `/org/${orgId}/settings/members`,
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,12 @@
|
||||||
import React, { ReactNode } from "react";
|
import React, { ReactNode } from "react";
|
||||||
import { Helmet } from "react-helmet";
|
import { Helmet } from "react-helmet";
|
||||||
import styled from "styled-components";
|
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 {
|
&& .fade {
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
@ -34,7 +38,8 @@ const PageBody = styled.div`
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
align-items: 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%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
@ -45,8 +50,12 @@ type PageWrapperProps = {
|
||||||
displayName?: string;
|
displayName?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const PageWrapper = (props: PageWrapperProps) => (
|
export const PageWrapper = (props: PageWrapperProps) => {
|
||||||
<Wrapper>
|
const location = useLocation();
|
||||||
|
const isSettingsPage = location.pathname.indexOf("settings") !== -1;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Wrapper background={isSettingsPage ? "#1B1B1D" : "inherit"}>
|
||||||
<Helmet>
|
<Helmet>
|
||||||
<title>{`${
|
<title>{`${
|
||||||
props.displayName ? `${props.displayName} | ` : ""
|
props.displayName ? `${props.displayName} | ` : ""
|
||||||
|
|
@ -55,5 +64,6 @@ export const PageWrapper = (props: PageWrapperProps) => (
|
||||||
<PageBody>{props.children}</PageBody>
|
<PageBody>{props.children}</PageBody>
|
||||||
</Wrapper>
|
</Wrapper>
|
||||||
);
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export default PageWrapper;
|
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
|
filled
|
||||||
intent="primary"
|
intent="primary"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
history.push(`/org/${props.orgId}/settings`);
|
history.push(`/org/${props.orgId}/settings/members`);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { Switch, useRouteMatch, useLocation } from "react-router-dom";
|
import { Switch, useRouteMatch, useLocation } from "react-router-dom";
|
||||||
import PageWrapper from "pages/common/PageWrapper";
|
import PageWrapper from "pages/common/PageWrapper";
|
||||||
import Settings from "./settings";
|
|
||||||
import DefaultOrgPage from "./defaultOrgPage";
|
import DefaultOrgPage from "./defaultOrgPage";
|
||||||
import AppRoute from "pages/common/AppRoute";
|
import AppRoute from "pages/common/AppRoute";
|
||||||
|
import Settings from "./settings";
|
||||||
export const Organization = () => {
|
export const Organization = () => {
|
||||||
const { path } = useRouteMatch();
|
const { path } = useRouteMatch();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
|
@ -11,7 +11,6 @@ export const Organization = () => {
|
||||||
<PageWrapper displayName="Organization Settings">
|
<PageWrapper displayName="Organization Settings">
|
||||||
<Switch location={location}>
|
<Switch location={location}>
|
||||||
<AppRoute
|
<AppRoute
|
||||||
exact
|
|
||||||
path={`${path}/:orgId/settings`}
|
path={`${path}/:orgId/settings`}
|
||||||
component={Settings}
|
component={Settings}
|
||||||
name={"Settings"}
|
name={"Settings"}
|
||||||
|
|
|
||||||
|
|
@ -1,393 +1,97 @@
|
||||||
import React, { useEffect } from "react";
|
import React, { useEffect } from "react";
|
||||||
import { connect } from "react-redux";
|
import { useRouteMatch, useLocation, useParams, Link } from "react-router-dom";
|
||||||
import { Icon } from "@blueprintjs/core";
|
import AppRoute from "pages/common/AppRoute";
|
||||||
import { TableWrapper } from "components/designSystems/appsmith/TableStyledWrappers";
|
import { getCurrentOrg } from "selectors/organizationSelectors";
|
||||||
import { CompactModeTypes, TABLE_SIZES } from "widgets/TableWidget";
|
import { useSelector, useDispatch } from "react-redux";
|
||||||
import { Colors } from "constants/Colors";
|
import { TabComponent, TabProp } from "components/ads/Tabs";
|
||||||
import { AppState } from "reducers";
|
import Text, { TextType } from "components/ads/Text";
|
||||||
import {
|
import history from "utils/history";
|
||||||
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 styled from "styled-components";
|
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 = {
|
import MemberSettings from "./Members";
|
||||||
currentOrg: Org;
|
import IconComponent from "components/designSystems/appsmith/IconComponent";
|
||||||
changeOrgName: (value: string) => void;
|
import { fetchOrg } from "actions/orgActions";
|
||||||
fetchCurrentOrg: (orgId: string) => void;
|
import { GeneralSettings } from "./General";
|
||||||
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;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type PageProps = OrgProps &
|
const LinkToApplications = styled(Link)`
|
||||||
RouteComponentProps<{
|
margin-bottom: 35px;
|
||||||
orgId: string;
|
width: auto;
|
||||||
}>;
|
&:hover {
|
||||||
|
text-decoration: none;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
.table {
|
svg {
|
||||||
.tbody {
|
|
||||||
height: ${props => props.height}px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const StyledMenu = styled(Menu)`
|
|
||||||
&&&&.bp3-menu {
|
|
||||||
max-width: 250px;
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const RoleNameCell = (props: any) => {
|
export default function Settings() {
|
||||||
const {
|
const { orgId } = useParams();
|
||||||
roleName,
|
const currentOrg = useSelector(getCurrentOrg);
|
||||||
roles,
|
const { path } = useRouteMatch();
|
||||||
username,
|
const location = useLocation();
|
||||||
isCurrentUser,
|
const dispatch = useDispatch();
|
||||||
isChangingRole,
|
useEffect(() => {
|
||||||
} = props.cellProps.row.original;
|
dispatch(fetchOrg(orgId as string));
|
||||||
|
}, []);
|
||||||
|
|
||||||
if (isCurrentUser) {
|
const SettingsRenderer = (
|
||||||
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>
|
<div>
|
||||||
<span>
|
<AppRoute
|
||||||
<b>{role[0]}</b>
|
path={`${path}/general`}
|
||||||
</span>
|
component={GeneralSettings}
|
||||||
<div>{role[1]}</div>
|
location={location}
|
||||||
|
name={"Settings"}
|
||||||
|
/>
|
||||||
|
<AppRoute
|
||||||
|
path={`${path}/members`}
|
||||||
|
component={MemberSettings}
|
||||||
|
location={location}
|
||||||
|
name={"Settings"}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
const tabArr: TabProp[] = [
|
||||||
<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",
|
key: "general",
|
||||||
accessor: "username",
|
title: "General",
|
||||||
|
panelComponent: SettingsRenderer,
|
||||||
|
icon: "general",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Header: "Name",
|
key: "members",
|
||||||
accessor: "name",
|
title: "Members",
|
||||||
},
|
panelComponent: SettingsRenderer,
|
||||||
{
|
icon: "user",
|
||||||
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 isMembersPage = location.pathname.indexOf("members") !== -1;
|
||||||
|
|
||||||
const currentOrgName = currentOrg?.name ?? "";
|
|
||||||
const {
|
|
||||||
getTableProps,
|
|
||||||
getTableBodyProps,
|
|
||||||
headerGroups,
|
|
||||||
rows,
|
|
||||||
prepareRow,
|
|
||||||
} = useTable(
|
|
||||||
{
|
|
||||||
columns,
|
|
||||||
data,
|
|
||||||
manualPagination: true,
|
|
||||||
},
|
|
||||||
useFlexLayout,
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
fetchUser(orgId);
|
|
||||||
fetchAllRoles(orgId);
|
|
||||||
fetchCurrentOrg(orgId);
|
|
||||||
}, [orgId, fetchUser, fetchAllRoles, fetchCurrentOrg]);
|
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<>
|
||||||
<PageSectionHeader>
|
<LinkToApplications to={"/applications"}>
|
||||||
<h2>{currentOrgName}</h2>
|
<IconComponent iconName="chevron-left" color="#9F9F9F"></IconComponent>
|
||||||
</PageSectionHeader>
|
<Text type={TextType.H1}>{currentOrg.name}</Text>
|
||||||
<PageSectionDivider />
|
</LinkToApplications>
|
||||||
<PageSectionHeader>
|
<TabComponent
|
||||||
<h2>Users</h2>
|
tabs={tabArr}
|
||||||
<FormDialogComponent
|
selectedIndex={isMembersPage ? 1 : 0}
|
||||||
trigger={
|
onSelect={(index: number) => {
|
||||||
<Button
|
const settingsStartIndex = location.pathname.indexOf("settings");
|
||||||
intent="primary"
|
const settingsEndIndex = settingsStartIndex + "settings".length;
|
||||||
text="Invite Users"
|
const hasSlash = location.pathname[settingsEndIndex] === "/";
|
||||||
icon="plus"
|
let newUrl = "";
|
||||||
iconAlignment="left"
|
|
||||||
filled
|
if (hasSlash) {
|
||||||
/>
|
newUrl = `${location.pathname.substr(0, settingsEndIndex)}/${
|
||||||
|
tabArr[index].key
|
||||||
|
}`;
|
||||||
|
} else {
|
||||||
|
newUrl = `${location.pathname}/${tabArr[index].key}`;
|
||||||
}
|
}
|
||||||
canOutsideClickClose={true}
|
history.push(newUrl);
|
||||||
Form={OrgInviteUsersForm}
|
}}
|
||||||
orgId={orgId}
|
></TabComponent>
|
||||||
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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
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 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,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(OrgSettings);
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { createReducer } from "utils/AppsmithUtils";
|
import { createImmerReducer } from "utils/AppsmithUtils";
|
||||||
import {
|
import {
|
||||||
ReduxAction,
|
ReduxAction,
|
||||||
ReduxActionTypes,
|
ReduxActionTypes,
|
||||||
|
|
@ -11,6 +11,7 @@ const initialState: OrgReduxState = {
|
||||||
fetchingRoles: false,
|
fetchingRoles: false,
|
||||||
isFetchAllRoles: false,
|
isFetchAllRoles: false,
|
||||||
isFetchAllUsers: false,
|
isFetchAllUsers: false,
|
||||||
|
isFetchingOrg: false,
|
||||||
},
|
},
|
||||||
orgUsers: [],
|
orgUsers: [],
|
||||||
orgRoles: [],
|
orgRoles: [],
|
||||||
|
|
@ -20,161 +21,121 @@ const initialState: OrgReduxState = {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const orgReducer = createReducer(initialState, {
|
const orgReducer = createImmerReducer(initialState, {
|
||||||
[ReduxActionTypes.FETCH_ORG_ROLES_INIT]: (state: OrgReduxState) => ({
|
[ReduxActionTypes.FETCH_ORG_ROLES_INIT]: (draftState: OrgReduxState) => {
|
||||||
...state,
|
draftState.loadingStates.isFetchAllRoles = true;
|
||||||
loadingStates: {
|
|
||||||
...state.loadingStates,
|
|
||||||
fetchingRoles: true,
|
|
||||||
},
|
},
|
||||||
}),
|
[ReduxActionTypes.FETCH_ALL_ROLES_INIT]: (draftState: OrgReduxState) => {
|
||||||
[ReduxActionTypes.FETCH_ALL_ROLES_INIT]: (state: OrgReduxState) => ({
|
draftState.loadingStates.isFetchAllRoles = true;
|
||||||
...state,
|
|
||||||
loadingStates: {
|
|
||||||
...state.loadingStates,
|
|
||||||
isFetchAllRoles: true,
|
|
||||||
},
|
},
|
||||||
}),
|
[ReduxActionTypes.FETCH_ALL_USERS_INIT]: (draftState: OrgReduxState) => {
|
||||||
[ReduxActionTypes.FETCH_ALL_USERS_INIT]: (state: OrgReduxState) => ({
|
draftState.loadingStates.isFetchAllUsers = true;
|
||||||
...state,
|
|
||||||
loadingStates: {
|
|
||||||
...state.loadingStates,
|
|
||||||
isFetchAllUsers: true,
|
|
||||||
},
|
},
|
||||||
}),
|
|
||||||
|
|
||||||
[ReduxActionTypes.FETCH_ORG_ROLES_SUCCESS]: (
|
[ReduxActionTypes.FETCH_ORG_ROLES_SUCCESS]: (
|
||||||
state: OrgReduxState,
|
draftState: OrgReduxState,
|
||||||
action: ReduxAction<OrgRole[]>,
|
action: ReduxAction<OrgRole[]>,
|
||||||
) => ({
|
) => {
|
||||||
...state,
|
draftState.orgRoles = action.payload;
|
||||||
roles: action.payload,
|
draftState.loadingStates.fetchingRoles = false;
|
||||||
loadingStates: {
|
|
||||||
...state.loadingStates,
|
|
||||||
fetchingRoles: false,
|
|
||||||
},
|
},
|
||||||
}),
|
[ReduxActionErrorTypes.FETCH_ORG_ROLES_ERROR]: (
|
||||||
[ReduxActionErrorTypes.FETCH_ORG_ROLES_ERROR]: (state: OrgReduxState) => ({
|
draftState: OrgReduxState,
|
||||||
...state,
|
) => {
|
||||||
loadingStates: {
|
draftState.loadingStates.fetchingRoles = false;
|
||||||
...state.loadingStates,
|
|
||||||
fetchingRoles: false,
|
|
||||||
},
|
},
|
||||||
}),
|
|
||||||
[ReduxActionTypes.FETCH_ALL_USERS_SUCCESS]: (
|
[ReduxActionTypes.FETCH_ALL_USERS_SUCCESS]: (
|
||||||
state: OrgReduxState,
|
draftState: OrgReduxState,
|
||||||
action: ReduxAction<Org[]>,
|
action: ReduxAction<OrgUser[]>,
|
||||||
) => ({
|
) => {
|
||||||
...state,
|
draftState.orgUsers = action.payload;
|
||||||
orgUsers: action.payload,
|
draftState.loadingStates.isFetchAllUsers = false;
|
||||||
loadingStates: {
|
|
||||||
...state.loadingStates,
|
|
||||||
isFetchAllUsers: false,
|
|
||||||
},
|
},
|
||||||
}),
|
|
||||||
[ReduxActionTypes.FETCH_ALL_ROLES_SUCCESS]: (
|
[ReduxActionTypes.FETCH_ALL_ROLES_SUCCESS]: (
|
||||||
state: OrgReduxState,
|
draftState: OrgReduxState,
|
||||||
action: ReduxAction<Org[]>,
|
action: ReduxAction<Org[]>,
|
||||||
) => ({
|
) => {
|
||||||
...state,
|
draftState.orgRoles = action.payload;
|
||||||
orgRoles: action.payload,
|
draftState.loadingStates.isFetchAllRoles = false;
|
||||||
loadingStates: {
|
|
||||||
...state.loadingStates,
|
|
||||||
isFetchAllRoles: false,
|
|
||||||
},
|
},
|
||||||
}),
|
|
||||||
[ReduxActionTypes.CHANGE_ORG_USER_ROLE_SUCCESS]: (
|
[ReduxActionTypes.CHANGE_ORG_USER_ROLE_SUCCESS]: (
|
||||||
state: OrgReduxState,
|
draftState: OrgReduxState,
|
||||||
action: ReduxAction<{ username: string; roleName: string }>,
|
action: ReduxAction<{ username: string; roleName: string }>,
|
||||||
) => {
|
) => {
|
||||||
const _orgUsers = state.orgUsers.map((user: OrgUser) => {
|
draftState.orgUsers.forEach((user: OrgUser) => {
|
||||||
if (user.username === action.payload.username) {
|
if (user.username === action.payload.username) {
|
||||||
return {
|
user.roleName = action.payload.roleName;
|
||||||
...user,
|
|
||||||
roleName: action.payload.roleName,
|
|
||||||
isChangingRole: false,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
return user;
|
|
||||||
});
|
});
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
orgUsers: _orgUsers,
|
|
||||||
};
|
|
||||||
},
|
},
|
||||||
[ReduxActionTypes.CHANGE_ORG_USER_ROLE_INIT]: (
|
[ReduxActionTypes.CHANGE_ORG_USER_ROLE_INIT]: (
|
||||||
state: OrgReduxState,
|
draftState: OrgReduxState,
|
||||||
action: ReduxAction<{ username: string }>,
|
action: ReduxAction<{ username: string }>,
|
||||||
) => {
|
) => {
|
||||||
const _orgUsers = state.orgUsers.map((user: OrgUser) => {
|
draftState.orgUsers.forEach((user: OrgUser) => {
|
||||||
if (user.username === action.payload.username) {
|
if (user.username === action.payload.username) {
|
||||||
return {
|
user.isChangingRole = true;
|
||||||
...user,
|
|
||||||
isChangingRole: true,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
return user;
|
|
||||||
});
|
});
|
||||||
return { ...state, orgUsers: _orgUsers };
|
|
||||||
},
|
},
|
||||||
[ReduxActionTypes.DELETE_ORG_USER_INIT]: (
|
[ReduxActionTypes.DELETE_ORG_USER_INIT]: (
|
||||||
state: OrgReduxState,
|
draftState: OrgReduxState,
|
||||||
action: ReduxAction<{ username: string }>,
|
action: ReduxAction<{ username: string }>,
|
||||||
) => {
|
) => {
|
||||||
const _orgUsers = state.orgUsers.map((user: OrgUser) => {
|
draftState.orgUsers.forEach((user: OrgUser) => {
|
||||||
if (user.username === action.payload.username) {
|
if (user.username === action.payload.username) {
|
||||||
return {
|
user.isDeleting = true;
|
||||||
...user,
|
|
||||||
isDeleting: true,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
return user;
|
|
||||||
});
|
});
|
||||||
return { ...state, orgUsers: _orgUsers };
|
|
||||||
},
|
},
|
||||||
[ReduxActionTypes.DELETE_ORG_USER_SUCCESS]: (
|
[ReduxActionTypes.DELETE_ORG_USER_SUCCESS]: (
|
||||||
state: OrgReduxState,
|
draftState: OrgReduxState,
|
||||||
action: ReduxAction<{ username: string }>,
|
action: ReduxAction<{ username: string }>,
|
||||||
) => {
|
) => {
|
||||||
const _orgUsers = state.orgUsers.filter(
|
draftState.orgUsers = draftState.orgUsers.filter(
|
||||||
(user: OrgUser) => user.username !== action.payload.username,
|
(user: OrgUser) => user.username !== action.payload.username,
|
||||||
);
|
);
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
orgUsers: _orgUsers,
|
|
||||||
};
|
|
||||||
},
|
},
|
||||||
[ReduxActionTypes.CHANGE_ORG_USER_ROLE_ERROR]: (state: OrgReduxState) => {
|
[ReduxActionErrorTypes.CHANGE_ORG_USER_ROLE_ERROR]: (
|
||||||
const _orgUsers = state.orgUsers.map(user => ({
|
draftState: OrgReduxState,
|
||||||
...user,
|
) => {
|
||||||
isChangingRole: false,
|
draftState.orgUsers.forEach((user: OrgUser) => {
|
||||||
}));
|
//TODO: This will change the status to false even if one role change api fails.
|
||||||
return { ...state, orgUsers: _orgUsers };
|
user.isChangingRole = false;
|
||||||
|
});
|
||||||
},
|
},
|
||||||
[ReduxActionTypes.DELETE_ORG_USER_ERROR]: (state: OrgReduxState) => {
|
[ReduxActionErrorTypes.DELETE_ORG_USER_ERROR]: (
|
||||||
const _orgUsers = state.orgUsers.map(user => ({
|
draftState: OrgReduxState,
|
||||||
...user,
|
) => {
|
||||||
isDeleting: false,
|
draftState.orgUsers.forEach((user: OrgUser) => {
|
||||||
}));
|
//TODO: This will change the status to false even if one delete fails.
|
||||||
return { ...state, orgUsers: _orgUsers };
|
user.isDeleting = false;
|
||||||
|
});
|
||||||
},
|
},
|
||||||
[ReduxActionTypes.SET_CURRENT_ORG_ID]: (
|
[ReduxActionTypes.SET_CURRENT_ORG_ID]: (
|
||||||
state: OrgReduxState,
|
draftState: OrgReduxState,
|
||||||
action: ReduxAction<{ orgId: string }>,
|
action: ReduxAction<{ orgId: string }>,
|
||||||
) => ({
|
) => {
|
||||||
...state,
|
draftState.currentOrg.id = action.payload.orgId;
|
||||||
currentOrg: {
|
|
||||||
...state.currentOrg,
|
|
||||||
id: action.payload.orgId,
|
|
||||||
},
|
},
|
||||||
}),
|
[ReduxActionTypes.SET_CURRENT_ORG]: (
|
||||||
[ReduxActionTypes.FETCH_ORG_SUCCESS]: (
|
draftState: OrgReduxState,
|
||||||
state: OrgReduxState,
|
|
||||||
action: ReduxAction<Org>,
|
action: ReduxAction<Org>,
|
||||||
) => ({
|
) => {
|
||||||
...state,
|
draftState.currentOrg = action.payload;
|
||||||
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 {
|
export interface OrgReduxState {
|
||||||
|
|
@ -184,6 +145,7 @@ export interface OrgReduxState {
|
||||||
fetchingRoles: boolean;
|
fetchingRoles: boolean;
|
||||||
isFetchAllRoles: boolean;
|
isFetchAllRoles: boolean;
|
||||||
isFetchAllUsers: boolean;
|
isFetchAllUsers: boolean;
|
||||||
|
isFetchingOrg: boolean;
|
||||||
};
|
};
|
||||||
orgUsers: OrgUser[];
|
orgUsers: OrgUser[];
|
||||||
orgRoles: any;
|
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 {
|
import {
|
||||||
ReduxActionTypes,
|
ReduxActionTypes,
|
||||||
ReduxAction,
|
ReduxAction,
|
||||||
|
|
@ -26,6 +26,7 @@ import OrgApi, {
|
||||||
import { ApiResponse } from "api/ApiResponses";
|
import { ApiResponse } from "api/ApiResponses";
|
||||||
import { AppToaster } from "components/editorComponents/ToastComponent";
|
import { AppToaster } from "components/editorComponents/ToastComponent";
|
||||||
import { ToastType } from "react-toastify";
|
import { ToastType } from "react-toastify";
|
||||||
|
import { getCurrentOrg } from "selectors/organizationSelectors";
|
||||||
|
|
||||||
export function* fetchRolesSaga() {
|
export function* fetchRolesSaga() {
|
||||||
try {
|
try {
|
||||||
|
|
@ -114,6 +115,9 @@ export function* changeOrgUserRoleSaga(
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
yield put({
|
yield put({
|
||||||
type: ReduxActionErrorTypes.CHANGE_ORG_USER_ROLE_ERROR,
|
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 response: ApiResponse = yield call(OrgApi.saveOrg, request);
|
||||||
const isValidResponse = yield validateResponse(response);
|
const isValidResponse = yield validateResponse(response);
|
||||||
if (isValidResponse) {
|
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({
|
yield put({
|
||||||
type: ReduxActionTypes.SAVE_ORG_SUCCESS,
|
type: ReduxActionTypes.SAVE_ORG_SUCCESS,
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,14 @@ export const getRolesFromState = (state: AppState) => {
|
||||||
return state.ui.orgs.roles;
|
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 getCurrentOrgId = (state: AppState) => state.ui.orgs.currentOrg.id;
|
||||||
export const getOrgs = (state: AppState) => {
|
export const getOrgs = (state: AppState) => {
|
||||||
return state.ui.applications.userOrgs;
|
return state.ui.applications.userOrgs;
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import * as log from "loglevel";
|
||||||
import { LogLevelDesc } from "loglevel";
|
import { LogLevelDesc } from "loglevel";
|
||||||
import FeatureFlag from "utils/featureFlags";
|
import FeatureFlag from "utils/featureFlags";
|
||||||
import { appCardColors } from "constants/AppConstants";
|
import { appCardColors } from "constants/AppConstants";
|
||||||
|
import produce from "immer";
|
||||||
|
|
||||||
export const createReducer = (
|
export const createReducer = (
|
||||||
initialState: any,
|
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 = () => {
|
export const appInitializer = () => {
|
||||||
FormControlRegistry.registerFormControlBuilders();
|
FormControlRegistry.registerFormControlBuilders();
|
||||||
const appsmithConfigs = getAppsmithConfigs();
|
const appsmithConfigs = getAppsmithConfigs();
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user