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:
satbir121 2020-08-29 00:21:16 +05:30 committed by GitHub
parent 0b22fc67b3
commit e2b939416a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 841 additions and 654 deletions

View File

@ -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",

View File

@ -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"
}
}

View File

@ -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");

View 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,
};
};

View File

@ -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 {

View 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

View File

@ -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,

View File

@ -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>

View File

@ -132,7 +132,7 @@ const SearchInput = forwardRef(
name="close"
size={Size.large}
className="close-icon"
click={() => setSearchValue("")}
onClick={() => setSearchValue("")}
/>
) : null}
</InputWrapper>

View File

@ -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>
);

View File

@ -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>
);
};

View File

@ -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>

View File

@ -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;

View File

@ -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";

View File

@ -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

View File

@ -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>
);

View File

@ -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,

View File

@ -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",

View File

@ -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 /";

View File

@ -11,6 +11,7 @@ export type Org = {
id: string;
name: string;
website?: string;
email?: string;
};
export type OrgUser = {

View File

@ -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`,
}),
},
],

View File

@ -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;

View 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>
</>
);
}

View 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>
);
}

View File

@ -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`);
}}
/>
)}

View File

@ -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"}

View File

@ -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>
</>
);
}

View File

@ -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;

View File

@ -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,
});

View File

@ -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;

View File

@ -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();