Feature: Invite modal (#927)

* light and dark mode added in Invite modal

* warnings removed

* create org and invite user test cases fixed

* Application invite design implemented

* manage user icon added

* button width fixed

* PR feedback implemented
This commit is contained in:
devrk96 2020-10-05 14:05:05 +05:30 committed by GitHub
parent 7d9ea665d4
commit 63daf74a44
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 520 additions and 288 deletions

View File

@ -25,10 +25,10 @@
"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')]",
"viewerRole": "//div[@class='bp3-overlay bp3-overlay-open']//div[contains(text(),'Viewer')]",
"developerRole": "//div[@class='bp3-overlay bp3-overlay-open']//div[contains(text(),'Developer')]",
"inviteBtn": "//span[text()='Invite']",
"adminRole": "//span[contains(text(),'Administrator')]",
"viewerRole": "//span[contains(text(),'Viewer')]",
"developerRole": "//span[contains(text(),'Developer')]",
"inviteBtn": "//button[text()='Invite']",
"manageUsers": ".manageUsers",
"DeleteBtn": "[data-cy=t--deleteUser]",
"ShareBtn": "//a[text()='Share']",

View File

@ -0,0 +1,4 @@
<svg width="9" height="8" viewBox="0 0 9 8" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0 4H5.4" stroke="#9F9F9F" stroke-width="1.5" stroke-linejoin="round"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M4.40002 0L9.00002 4L4.40002 8V0Z" fill="#9F9F9F"/>
</svg>

After

Width:  |  Height:  |  Size: 281 B

View File

@ -1,6 +1,6 @@
import React from "react";
import { CommonComponentProps, hexToRgba, ThemeProp, Classes } from "./common";
import styled from "styled-components";
import styled, { css } from "styled-components";
import Icon, { IconName, IconSize } from "./Icon";
import Spinner from "./Spinner";
import { mediumButton, smallButton, largeButton } from "constants/DefaultTheme";
@ -58,6 +58,7 @@ type ButtonProps = CommonComponentProps & {
size?: Size;
fill?: boolean;
href?: string;
tag?: "a" | "button";
};
const stateStyles = (
@ -243,7 +244,7 @@ const btnFontStyles = (props: ThemeProp & ButtonProps): BtnFontType => {
return { buttonFont, padding, height };
};
const StyledButton = styled("a")<ThemeProp & ButtonProps>`
const ButtonStyles = css<ThemeProp & ButtonProps>`
width: ${props => (props.fill ? "100%" : "auto")};
height: ${props => btnFontStyles(props).height}px;
border: none;
@ -295,7 +296,7 @@ const StyledButton = styled("a")<ThemeProp & ButtonProps>`
align-items: center;
justify-content: center;
position: relative;
.new-spinner {
.${Classes.SPINNER} {
position: absolute;
left: 0;
right: 0;
@ -304,6 +305,14 @@ const StyledButton = styled("a")<ThemeProp & ButtonProps>`
}
`;
const StyledButton = styled("button")`
${ButtonStyles}
`;
const StyledLinkButton = styled("a")`
${ButtonStyles}
`;
export const VisibilityWrapper = styled.div`
visibility: hidden;
`;
@ -327,6 +336,7 @@ Button.defaultProps = {
isLoading: false,
disabled: false,
fill: false,
tag: "a",
};
function Button(props: ButtonProps) {
@ -336,16 +346,8 @@ function Button(props: ButtonProps) {
const TextLoadingState = <VisibilityWrapper>{props.text}</VisibilityWrapper>;
return (
<StyledButton
href={props.href}
className={props.className}
data-cy={props.cypressSelector}
{...props}
onClick={(e: React.MouseEvent<HTMLElement>) =>
props.onClick && props.onClick(e)
}
>
const buttonContent = (
<>
{props.icon ? (
props.isLoading ? (
IconLoadingState
@ -357,8 +359,37 @@ function Button(props: ButtonProps) {
{props.text ? (props.isLoading ? TextLoadingState : props.text) : null}
{props.isLoading ? <Spinner size={IconSizeProp(props.size)} /> : null}
</StyledButton>
</>
);
if (props.tag === "button") {
return (
<StyledButton
className={props.className}
data-cy={props.cypressSelector}
{...props}
onClick={(e: React.MouseEvent<HTMLElement>) =>
props.onClick && props.onClick(e)
}
>
{buttonContent}
</StyledButton>
);
} else {
return (
<StyledLinkButton
href={props.href}
className={props.className}
data-cy={props.cypressSelector}
{...props}
onClick={(e: React.MouseEvent<HTMLElement>) =>
props.onClick && props.onClick(e)
}
>
{buttonContent}
</StyledLinkButton>
);
}
}
export default Button;

View File

@ -2,13 +2,10 @@ import React from "react";
import { CommonComponentProps, Classes } from "./common";
import Text, { TextType } from "./Text";
import styled from "styled-components";
export type Variant = "note" | "warning";
export type Background = "dark" | "light";
import { Variant } from "./Button";
type CalloutProps = CommonComponentProps & {
variant?: Variant;
background?: Background;
text: string;
fill?: boolean;
};
@ -16,14 +13,11 @@ type CalloutProps = CommonComponentProps & {
const CalloutContainer = styled.div<{
variant?: Variant;
fill?: boolean;
background?: Background;
}>`
padding: ${props => props.theme.spaces[5]}px
${props => props.theme.spaces[11] + 1}px;
background: ${props =>
props.variant && props.background
? props.theme.colors.callout[props.variant][props.background].bgColor
: null};
props.variant ? props.theme.colors.callout[props.variant].bgColor : null};
height: 42px;
${props =>
@ -37,25 +31,18 @@ const CalloutContainer = styled.div<{
.${Classes.TEXT} {
color: ${props =>
props.variant && props.background
? props.theme.colors.callout[props.variant][props.background].color
: null};
props.variant ? props.theme.colors.callout[props.variant].color : null};
}
`;
Callout.defaultProps = {
fill: false,
variant: "note",
background: "dark",
};
function Callout(props: CalloutProps) {
return (
<CalloutContainer
variant={props.variant}
background={props.background}
fill={props.fill}
>
<CalloutContainer variant={props.variant} fill={props.fill}>
<Text type={TextType.P2}>{props.text}</Text>
</CalloutContainer>
);

View File

@ -6,38 +6,45 @@ import Text, { TextType } from "./Text";
type DropdownOption = {
label?: string;
value: string;
value?: string;
id?: string;
icon?: IconName;
onSelect?: (option: DropdownOption) => void;
children?: DropdownOption[];
};
type DropdownProps = CommonComponentProps & {
options: DropdownOption[];
selected: DropdownOption;
selected?: DropdownOption;
placeholder?: string;
input?: {
value?: string;
onChange?: (value?: string) => void;
};
};
const DropdownContainer = styled.div`
width: 260px;
position: relative;
`;
const Selected = styled.div<{ isOpen: boolean; disabled?: boolean }>`
height: 38px;
padding: ${props => props.theme.spaces[4]}px
${props => props.theme.spaces[6]}px;
background: ${props =>
props.disabled
? props.theme.colors.dropdown.header.disabledText
: props.theme.colors.dropdown.header.disabledBg};
? props.theme.colors.dropdown.header.disabledBg
: props.theme.colors.dropdown.header.bg};
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
cursor: pointer;
${props =>
props.isOpen && !props.disabled
? `border: 1.2px solid ${props.theme.colors.info.main}`
: null};
props.isOpen
? `border: 1px solid ${props.theme.colors.info.main}`
: props.disabled
? `border: 1px solid ${props.theme.colors.dropdown.header.disabledBg}`
: `border: 1px solid ${props.theme.colors.dropdown.header.bg}`};
${props =>
props.isOpen && !props.disabled ? "box-sizing: border-box" : null};
${props =>
@ -47,12 +54,16 @@ const Selected = styled.div<{ isOpen: boolean; disabled?: boolean }>`
.${Classes.TEXT} {
${props =>
props.disabled
? `color: ${props.theme.colors.dropdown.text}`
: `color: ${props.theme.colors.dropdown.disabledText}`};
? `color: ${props.theme.colors.dropdown.header.disabledText}`
: `color: ${props.theme.colors.dropdown.header.text}`};
}
`;
const DropdownWrapper = styled.div`
position: absolute;
top: 38px;
left: 0px;
z-index: 1;
margin-top: ${props => props.theme.spaces[2] - 1}px;
background: ${props => props.theme.colors.dropdown.menuBg};
box-shadow: 0px 12px 28px ${props => props.theme.colors.dropdown.menuShadow};
@ -118,17 +129,24 @@ const LabelWrapper = styled.div<{ label?: string }>`
export default function Dropdown(props: DropdownProps) {
const [isOpen, setIsOpen] = useState<boolean>(false);
const [selected, setSelected] = useState<DropdownOption>(props.selected);
const [selected, setSelected] = useState<string>(props.placeholder || "");
useEffect(() => {
setSelected(props.selected);
}, [props.selected]);
if (props.input && props.input.value) {
setSelected(props.input.value);
}
}, [props.input]);
const optionClickHandler = useCallback((option: DropdownOption) => {
setSelected(option);
setIsOpen(false);
option.onSelect && option.onSelect(option);
}, []);
const optionClickHandler = useCallback(
(option: DropdownOption) => {
if (option.value) {
setSelected(option.value);
}
setIsOpen(false);
props.input && props.input.onChange && props.input.onChange(option.id);
},
[props.input],
);
return (
<DropdownContainer
@ -141,8 +159,8 @@ export default function Dropdown(props: DropdownProps) {
disabled={props.disabled}
onClick={() => setIsOpen(!isOpen)}
>
<Text type={TextType.P1}>{selected.value}</Text>
<Icon name="downArrow" size={IconSize.SMALL} />
<Text type={TextType.P1}>{selected}</Text>
<Icon name="downArrow" size={IconSize.XXS} />
</Selected>
{isOpen && !props.disabled ? (
@ -151,7 +169,7 @@ export default function Dropdown(props: DropdownProps) {
return (
<OptionWrapper
key={index}
selected={selected.value === option.value}
selected={selected === option.value}
onClick={() => optionClickHandler(option)}
>
{option.icon ? (

View File

@ -140,22 +140,23 @@ export const EditableText = (props: EditableTextProps) => {
useEffect(() => {
setSavingState(props.savingState);
}, [props.savingState]);
}, [props]);
useEffect(() => {
return () => {
props.onBlur(valueRef.current);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
setValue(props.defaultValue);
setIsEditing(!!props.isEditingDefault);
}, [props.defaultValue, props.isEditingDefault]);
}, [props]);
useEffect(() => {
if (props.forceDefault === true) setValue(props.defaultValue);
}, [props.forceDefault, props.defaultValue]);
}, [props]);
const themeDetails = useSelector(getThemeDetails);
const bgColor = useMemo(
@ -189,7 +190,7 @@ export const EditableText = (props: EditableTextProps) => {
setIsEditing(false);
setChangeStarted(false);
},
[changeStarted, lastValidValue, props.onBlur, props.onTextChanged],
[changeStarted, savingState, isInvalid, lastValidValue, props],
);
const onInputchange = useCallback(
@ -205,7 +206,7 @@ export const EditableText = (props: EditableTextProps) => {
setIsInvalid(error);
setChangeStarted(true);
},
[props.isInvalid],
[props],
);
const iconName =

View File

@ -18,6 +18,7 @@ import { ReactComponent as ViewAllIcon } from "assets/icons/ads/view-all.svg";
import { ReactComponent as ContextMenuIcon } from "assets/icons/ads/context-menu.svg";
import { ReactComponent as DuplicateIcon } from "assets/icons/ads/duplicate.svg";
import { ReactComponent as LogoutIcon } from "assets/icons/ads/logout.svg";
import { ReactComponent as ManageIcon } from "assets/icons/ads/manage.svg";
import styled from "styled-components";
import { CommonComponentProps, Classes } from "./common";
import { noop } from "lodash";
@ -88,6 +89,7 @@ export const IconCollection = [
"context-menu",
"duplicate",
"logout",
"manage",
] as const;
export type IconName = typeof IconCollection[number];
@ -97,6 +99,7 @@ const IconWrapper = styled.span<IconProps>`
outline: none;
}
display: flex;
align-items: center;
svg {
width: ${props => sizeHandler(props.size)}px;
height: ${props => sizeHandler(props.size)}px;
@ -189,6 +192,9 @@ const Icon = (props: IconProps & CommonComponentProps) => {
case "logout":
returnIcon = <LogoutIcon />;
break;
case "manage":
returnIcon = <ManageIcon />;
break;
default:
returnIcon = null;
break;

View File

@ -39,6 +39,7 @@ export type TextInputProps = CommonComponentProps & {
defaultValue?: string;
validator?: (value: string) => { isValid: boolean; message: string };
onChange?: (value: string) => void;
readOnly?: boolean;
};
type boxReturnType = {
@ -56,7 +57,7 @@ const boxStyles = (
let color = theme.colors.textInput.normal.text;
let borderColor = theme.colors.textInput.normal.border;
if (props.disabled) {
if (props.disabled || props.readOnly) {
bgColor = theme.colors.textInput.disable.bg;
color = theme.colors.textInput.disable.text;
borderColor = theme.colors.textInput.disable.border;
@ -77,28 +78,35 @@ const StyledInput = styled.input<
outline: 0;
box-shadow: none;
border: 1px solid ${props => props.inputStyle.borderColor};
padding: ${props => props.theme.spaces[4]}px
${props => props.theme.spaces[6]}px;
padding: 0px ${props => props.theme.spaces[6]}px;
height: 38px;
background-color: ${props => props.inputStyle.bgColor};
color: ${props => props.inputStyle.color};
&::placeholder {
color: ${props => props.theme.colors.textInput.placeholder};
}
&:focus {
border: 1px solid
${props =>
props.isValid
? props.theme.colors.info.main
: props.theme.colors.danger.main};
box-shadow: ${props =>
props.isValid
? "0px 0px 4px 4px rgba(203, 72, 16, 0.18)"
: "0px 0px 4px 4px rgba(226, 44, 44, 0.18)"};
}
&:disabled {
cursor: not-allowed;
}
${props =>
!props.readOnly
? `
&:focus {
border: 1px solid
${
props.isValid
? props.theme.colors.info.main
: props.theme.colors.danger.main
};
box-shadow: ${
props.isValid
? "0px 0px 4px 4px rgba(203, 72, 16, 0.18)"
: "0px 0px 4px 4px rgba(226, 44, 44, 0.18)"
};
}
`
: null};
`;
const InputWrapper = styled.div`
@ -172,6 +180,7 @@ const TextInput = forwardRef(
{...props}
placeholder={props.placeholder}
onChange={memoizedChangeHandler}
readOnly={props.readOnly}
/>
{ErrorMessage}
</InputWrapper>

View File

@ -1,4 +1,4 @@
import { CommonComponentProps, Classes, lighten, darken } from "./common";
import { CommonComponentProps, Classes } from "./common";
import React, { useState, useEffect } from "react";
import styled from "styled-components";
import Spinner from "./Spinner";

View File

@ -1,36 +1,17 @@
import React, { createRef, useState } from "react";
import styled from "styled-components";
import copy from "copy-to-clipboard";
import { BaseButton } from "components/designSystems/blueprint/ButtonComponent";
import TextInput from "components/ads/TextInput";
import Button, { Category, Size } from "components/ads/Button";
const Wrapper = styled.div`
display: flex;
flex-direction: row;
`;
const StyledInput = styled.input`
flex: 1;
border: 1px solid #d3dee3;
border-right: none;
padding: 6px 12px;
font-size: 14px;
color: #768896;
border-radius: 4px 0 0 4px;
width: 90%;
overflow: hidden;
`;
const SelectButton = styled(BaseButton)`
&&&& {
max-width: 70px;
margin: 0 0px;
min-height: 32px;
border-radius: 0px 4px 4px 0px;
font-weight: bold;
background-color: #f6f7f8;
font-size: 14px;
&.bp3-button {
padding: 0px 0px;
}
div {
flex-basis: 82%;
}
a {
flex-basis: 18%;
}
`;
@ -54,21 +35,23 @@ const CopyToClipboard = (props: any) => {
};
return (
<Wrapper>
<StyledInput
type="text"
<TextInput
fill
ref={copyURLInput}
readOnly
onClick={() => {
onChange={() => {
selectText();
}}
value={copyText}
defaultValue={copyText}
/>
<SelectButton
<Button
text={isCopied ? "Copied" : "Copy"}
accent="secondary"
category={Category.tertiary}
onClick={() => {
copyToClipboard(copyText);
}}
size={Size.large}
/>
</Wrapper>
);

View File

@ -1,23 +1,35 @@
import React, { useState, useEffect } from "react";
import styled from "styled-components";
import { TagInput } from "@blueprintjs/core";
import {
Intent,
IntentColors,
getColorWithOpacity,
} from "constants/DefaultTheme";
import { Intent } from "constants/DefaultTheme";
const TagInputWrapper = styled.div<{ intent?: Intent }>`
margin-right: 8px;
&&& {
.bp3-tag-input {
background-color: ${props => props.theme.colors.tagInput.bg};
min-height: 38px;
border: 1px solid ${props => props.theme.colors.tagInput.bg};
border-radius: 0px;
}
.bp3-tag-input.bp3-active {
border: 1px solid ${props => props.theme.colors.info.main};
box-shadow: ${props => props.theme.colors.tagInput.shadow};
}
.bp3-input-ghost {
color: ${props => props.theme.colors.tagInput.text};
&::placeholder {
color: ${props => props.theme.colors.tagInput.placeholder};
}
}
.bp3-tag {
color: ${props => props.theme.colors.textDefault};
font-size: ${props => props.theme.fontSizes[3]}px;
background: ${props =>
props.intent
? getColorWithOpacity(IntentColors[props.intent], 0.2)
: getColorWithOpacity(IntentColors.none, 0.2)};
border: 1px solid
${props =>
props.intent ? IntentColors[props.intent] : IntentColors.none};
padding: 3px 10px;
color: ${props => props.theme.colors.tagInput.tag.text};
background-color: ${props => props.theme.colors.info.main};
border-radius: 0px;
font-size: 11px;
line-height: 13px;
letter-spacing: 0.4px;
}
}
`;

View File

@ -5,10 +5,34 @@ import { isPermitted } from "pages/Applications/permissionHelpers";
const StyledDialog = styled(Dialog)<{ setMaxWidth?: boolean }>`
&& {
background: white;
border-radius: 0px;
padding-bottom: 5px;
background: ${props => props.theme.colors.modal.bg};
width: 640px;
& .bp3-dialog-header {
padding: ${props => props.theme.spaces[4]}px
${props => props.theme.spaces[4]}px;
padding: ${props => props.theme.spaces[4]}px;
background: ${props => props.theme.colors.modal.bg};
box-shadow: none;
.bp3-icon {
color: ${props => props.theme.colors.modal.iconColor};
}
.bp3-heading {
color: ${props => props.theme.colors.modal.headerText};
display: flex;
justify-content: center;
margin-top: 20px;
font-size: 20px;
line-height: 24px;
font-weight: 500;
}
.bp3-button.bp3-minimal:hover {
background-color: ${props => props.theme.colors.modal.bg};
}
}
& .bp3-dialog-body {
margin: ${props => props.theme.spaces[9]}px;
}
& .bp3-dialog-footer-actions {
display: block;

View File

@ -4,7 +4,7 @@ import {
WrappedFieldMetaProps,
WrappedFieldInputProps,
} from "redux-form";
import SelectComponent from "components/editorComponents/SelectComponent";
import Dropdown from "components/ads/Dropdown";
const renderComponent = (
componentProps: SelectFieldProps & {
@ -14,15 +14,15 @@ const renderComponent = (
) => {
return (
<React.Fragment>
<SelectComponent {...componentProps} />
<Dropdown {...componentProps} />
</React.Fragment>
);
};
type SelectFieldProps = {
name: string;
placeholder?: string;
options?: Array<{ id: string; name: string; value?: string }>;
placeholder: string;
options: Array<{ id: string; name: string; value?: string }>;
size?: "large" | "small";
outline?: boolean;
};

View File

@ -2,6 +2,7 @@ import React from "react";
import { withKnobs, select, text, boolean } from "@storybook/addon-knobs";
import Callout from "components/ads/Callout";
import { StoryWrapper } from "components/ads/common";
import { Variant } from "components/ads/Button";
export default {
title: "Callout",
@ -13,9 +14,8 @@ export const CalloutStory = () => (
<StoryWrapper>
<Callout
text={text("text", "Lorem ipsum dolar sit adicipling dolare")}
variant={select("variant", ["note", "warning"], "note")}
background={select("background", ["light", "dark"], "dark")}
fill={boolean("fill", false)}
variant={select("variant", Object.values(Variant), Variant.info)}
/>
</StoryWrapper>
);

View File

@ -19,17 +19,14 @@ export const Text = () => (
{
id: "111abc",
value: text("1st Option", "First option"),
onSelect: action("selected-option"),
},
{
id: "222abc",
value: text("2nd Option", "Second option"),
onSelect: action("selected-option"),
},
{
id: "322abc",
value: text("3rd Option", "Third option"),
onSelect: action("selected-option"),
},
]}
selected={{
@ -49,19 +46,16 @@ export const IconAndText = () => (
id: "111abc",
value: text("1st Option", "Delete"),
icon: select("1st Icon", IconCollection, "delete"),
onSelect: action("selected-option"),
},
{
id: "222abc",
value: text("2nd Option", "User"),
icon: select("2nd Icon", IconCollection, "user"),
onSelect: action("selected-option"),
},
{
id: "322abc",
value: text("3rd Option", "General"),
icon: select("3rd Icon", IconCollection, "general"),
onSelect: action("selected-option"),
},
]}
selected={{
@ -84,13 +78,11 @@ export const LabelAndText = () => (
"1st label",
"Can edit, view and invite other user to an app",
),
onSelect: action("selected-option"),
},
{
id: "222abc",
value: text("2nd Option", "Developer"),
label: text("2nd label", "Can view and invite other user to an app"),
onSelect: action("selected-option"),
},
{
id: "322abc",
@ -99,7 +91,6 @@ export const LabelAndText = () => (
"3rd label",
"Can view and invite other user to an app and…",
),
onSelect: action("selected-option"),
},
]}
selected={{

View File

@ -481,7 +481,6 @@ type ColorType = {
hoverBG: Color;
hoverBGOpacity: number;
hoverBorder: ShadeColor;
targetBg: string;
iconColor: ShadeColor;
};
appCardColors: string[];
@ -524,7 +523,7 @@ type ColorType = {
dropdown: {
header: {
text: ShadeColor;
disabled: ShadeColor;
disabledText: ShadeColor;
bg: ShadeColor;
disabledBg: ShadeColor;
};
@ -639,6 +638,46 @@ type ColorType = {
profileDropdown: {
userName: ShadeColor;
};
modal: {
bg: ShadeColor;
headerText: ShadeColor;
iconColor: string;
user: {
textColor: ShadeColor;
};
email: {
message: ShadeColor;
desc: ShadeColor;
};
manageUser: ShadeColor;
};
tagInput: {
bg: ShadeColor;
tag: {
text: ShadeColor;
};
text: ShadeColor;
placeholder: ShadeColor;
shadow: string;
};
callout: {
info: {
color: string;
bgColor: string;
};
success: {
color: string;
bgColor: string;
};
danger: {
color: string;
bgColor: string;
};
warning: {
color: string;
bgColor: string;
};
};
};
export const dark: ColorType = {
@ -685,7 +724,6 @@ export const dark: ColorType = {
hoverBG: Colors.BLACK,
hoverBGOpacity: 0.5,
hoverBorder: darkShades[4],
targetBg: "rgba(0, 0, 0, 0.1)",
iconColor: darkShades[9],
},
appCardColors: [
@ -738,9 +776,9 @@ export const dark: ColorType = {
dropdown: {
header: {
text: darkShades[7],
disabled: darkShades[6],
bg: darkShades[2],
disabledBg: darkShades[0],
disabledText: darkShades[6],
bg: darkShades[0],
disabledBg: darkShades[2],
},
menuBg: darkShades[3],
menuShadow: "rgba(0, 0, 0, 0.6)",
@ -853,6 +891,46 @@ export const dark: ColorType = {
profileDropdown: {
userName: darkShades[9],
},
modal: {
bg: darkShades[1],
headerText: darkShades[9],
iconColor: "#6D6D6D",
user: {
textColor: darkShades[7],
},
email: {
message: darkShades[8],
desc: darkShades[6],
},
manageUser: darkShades[6],
},
tagInput: {
bg: darkShades[0],
tag: {
text: darkShades[9],
},
text: darkShades[9],
placeholder: darkShades[5],
shadow: "0px 0px 4px 4px rgba(203, 72, 16, 0.18)",
},
callout: {
info: {
color: "#EE5A1A",
bgColor: "#241C1B",
},
success: {
color: "#30CF89",
bgColor: "#17211E",
},
danger: {
color: "#FF4D4D",
bgColor: "#2B1A1D",
},
warning: {
color: "#E0B30E",
bgColor: "#29251A",
},
},
};
export const light: ColorType = {
@ -899,7 +977,6 @@ export const light: ColorType = {
hoverBG: Colors.WHITE,
hoverBGOpacity: 0.7,
hoverBorder: lightShades[2],
targetBg: "rgba(0, 0, 0, 0.1)",
iconColor: lightShades[11],
},
appCardColors: [
@ -952,7 +1029,7 @@ export const light: ColorType = {
dropdown: {
header: {
text: lightShades[9],
disabled: darkShades[6],
disabledText: darkShades[6],
bg: lightShades[2],
disabledBg: lightShades[1],
},
@ -992,7 +1069,7 @@ export const light: ColorType = {
text: lightShades[9],
border: lightShades[2],
},
placeholder: lightShades[6],
placeholder: lightShades[7],
},
menuBorder: lightShades[3],
editableText: {
@ -1067,6 +1144,46 @@ export const light: ColorType = {
profileDropdown: {
userName: lightShades[9],
},
modal: {
bg: lightShades[11],
headerText: lightShades[10],
iconColor: "#A9A7A7",
user: {
textColor: lightShades[9],
},
email: {
message: lightShades[9],
desc: lightShades[7],
},
manageUser: lightShades[6],
},
tagInput: {
bg: lightShades[2],
tag: {
text: lightShades[11],
},
text: lightShades[9],
placeholder: darkShades[7],
shadow: "0px 0px 4px 4px rgba(203, 72, 16, 0.18)",
},
callout: {
info: {
color: "#D44100",
bgColor: "#F8F3F0",
},
success: {
color: "#007340",
bgColor: "#D9FDED",
},
danger: {
color: "#C60707",
bgColor: "#FFE9E9",
},
warning: {
color: "#DCAD00",
bgColor: "#FAF6E6",
},
},
};
export const theme: Theme = {
@ -1173,28 +1290,6 @@ export const theme: Theme = {
lightBg: lightShades[0],
darkBg: lightShades[10],
},
callout: {
note: {
dark: {
color: "#EE5A1A",
bgColor: "#241C1B",
},
light: {
color: "#D44100",
bgColor: "#F8F3F0",
},
},
warning: {
light: {
color: "#DCAD00",
bgColor: "#FAF6E6",
},
dark: {
color: "#E0B30E",
bgColor: "#29251A",
},
},
},
appBackground: "#EFEFEF",
primaryOld: Colors.GREEN,
primaryDarker: Colors.JUNGLE_GREEN,

View File

@ -45,11 +45,10 @@ import { Classes as CsClasses } from "components/ads/common";
type NameWrapperProps = {
hasReadPermission: boolean;
showOverlay: boolean;
isMenuOpen: boolean;
};
const NameWrapper = styled((props: HTMLDivProps & NameWrapperProps) => (
<div {...omit(props, ["hasReadPermission", "showOverlay", "isMenuOpen"])} />
<div {...omit(props, ["hasReadPermission", "showOverlay"])} />
))`
.bp3-card {
border-radius: 0;
@ -81,7 +80,7 @@ const NameWrapper = styled((props: HTMLDivProps & NameWrapperProps) => (
& div.image-container {
background: ${
props.hasReadPermission && !props.isMenuOpen
props.hasReadPermission
? getColorWithOpacity(
props.theme.colors.card.hoverBG,
props.theme.colors.card.hoverBGOpacity,
@ -206,8 +205,6 @@ const ContextDropdownWrapper = styled.div`
.${Classes.POPOVER_TARGET} {
span {
background: ${props => props.theme.colors.card.targetBg};
svg {
path {
fill: ${props => props.theme.colors.card.iconColor};
@ -385,7 +382,6 @@ export const ApplicationCard = (props: ApplicationCardProps) => {
!isMenuOpen && setShowOverlay(false);
}}
hasReadPermission={hasReadPermission}
isMenuOpen={isMenuOpen}
className="t--application-card"
>
<Wrapper

View File

@ -21,9 +21,9 @@ type TagProps = CommonComponentProps & {
userName?: string;
};
const ProfileImage = styled.div<{ backgroundColor?: string }>`
width: 30px;
height: 30px;
export const ProfileImage = styled.div<{ backgroundColor?: string }>`
width: 34px;
height: 34px;
display: flex;
align-items: center;
border-radius: 50%;

View File

@ -12,33 +12,24 @@ import {
import { getDefaultPageId } from "sagas/SagaUtils";
import { getApplicationViewerPageURL } from "constants/routes";
import OrgInviteUsersForm from "./OrgInviteUsersForm";
import { StyledSwitch } from "components/propertyControls/StyledControls";
import Spinner from "components/editorComponents/Spinner";
import { getCurrentUser } from "selectors/usersSelectors";
import Text, { TextType } from "components/ads/Text";
import Toggle from "components/ads/Toggle";
const Title = styled.div`
font-weight: bold;
padding: 10px 0px;
`;
const ShareWithPublicOption = styled.div`
{
display: flex;
padding: 10px 0px;
justify-content: space-between;
}
display: flex;
margin-bottom: 15px;
align-items: center;
justify-content: space-between;
`;
const ShareToggle = styled.div`
{
&&& label {
margin-bottom: 0px;
}
&&& div {
margin-right: 5px;
}
display: flex;
}
flex-basis: 48px;
height: 23px;
`;
const AppInviteUsersForm = (props: any) => {
@ -84,29 +75,30 @@ const AppInviteUsersForm = (props: any) => {
{canShareWithPublic && (
<>
<ShareWithPublicOption>
Make the application public
<Text type={TextType.H5}>Make the application public</Text>
<ShareToggle>
{(isChangingViewAccess || isFetchingApplication) && (
<Spinner size={20} />
)}
{currentApplicationDetails && (
<StyledSwitch
onChange={() => {
<Toggle
isLoading={isChangingViewAccess || isFetchingApplication}
value={currentApplicationDetails.isPublic}
disabled={isChangingViewAccess || isFetchingApplication}
onToggle={() => {
changeAppViewAccess(
applicationId,
!currentApplicationDetails.isPublic,
);
}}
disabled={isChangingViewAccess || isFetchingApplication}
checked={currentApplicationDetails.isPublic}
large
/>
)}
</ShareToggle>
</ShareWithPublicOption>
</>
)}
<Title>Get Shareable link for this for this application </Title>
<Title>
<Text type={TextType.H5}>
Get Shareable link for this for this application
</Text>
</Title>
<CopyToClipBoard copyText={getViewApplicationURL()} />
{canInviteToOrg && (

View File

@ -5,8 +5,7 @@ import TagListField from "components/editorComponents/form/fields/TagListField";
import { reduxForm, SubmissionError } from "redux-form";
import SelectField from "components/editorComponents/form/fields/SelectField";
import Divider from "components/editorComponents/Divider";
import Button from "components/editorComponents/Button";
import { connect } from "react-redux";
import { connect, useSelector } from "react-redux";
import { AppState } from "reducers";
import {
getRolesForField,
@ -17,8 +16,6 @@ import Spinner from "components/editorComponents/Spinner";
import { ReduxActionTypes } from "constants/ReduxActionConstants";
import { InviteUsersToOrgFormValues, inviteUsersToOrg } from "./helpers";
import { INVITE_USERS_TO_ORG_FORM } from "constants/forms";
import { Classes } from "@blueprintjs/core";
import FormMessage from "components/editorComponents/form/FormMessage";
import {
INVITE_USERS_SUBMIT_SUCCESS,
INVITE_USERS_VALIDATION_EMAILS_EMPTY,
@ -26,7 +23,6 @@ import {
INVITE_USERS_VALIDATION_ROLE_EMPTY,
} from "constants/messages";
import history from "utils/history";
import { Colors } from "constants/Colors";
import { isEmail } from "utils/formhelpers";
import {
isPermitted,
@ -35,32 +31,28 @@ import {
import { getAppsmithConfigs } from "configs";
import { ReactComponent as NoEmailConfigImage } from "assets/images/email-not-configured.svg";
import AnalyticsUtil from "utils/AnalyticsUtil";
import Button, { Variant, Size } from "components/ads/Button";
import Text, { TextType } from "components/ads/Text";
import Icon, { IconSize } from "components/ads/Icon";
import { Classes } from "components/ads/common";
import Callout from "components/ads/Callout";
import { getInitialsAndColorCode } from "utils/AppsmithUtils";
import { getThemeDetails } from "selectors/themeSelectors";
import { ProfileImage } from "pages/common/ProfileDropdown";
const OrgInviteTitle = styled.div`
font-weight: bold;
padding: 10px 0px;
`;
const DropDownOption = styled.div`
padding: 10px 0;
`;
const OptionTitle = styled.div`
font-weight: bold;
`;
const OptionDescription = styled.div`
padding: 5px 0px;
max-width: 250px;
`;
const StyledForm = styled.form`
width: 100%;
background: white;
padding: ${props => props.theme.spaces[5]}px;
background: ${props => props.theme.colors.modal.bg};
&&& {
.wrapper > div {
width: 70%;
.wrapper > div:nth-child(1) {
width: 60%;
}
.wrapper > div:nth-child(2) {
width: 40%;
}
.bp3-input {
box-shadow: none;
@ -69,10 +61,39 @@ const StyledForm = styled.form`
padding-top: 5px;
}
}
.manageUsers {
float: right;
margin-top: 20px;
`;
const ManageUsers = styled("a")`
margin-top: 20px;
display: inline-flex;
&&&& {
text-decoration: none;
}
.${Classes.TEXT} {
color: ${props => props.theme.colors.modal.manageUser};
margin-right: ${props => props.theme.spaces[1]}px;
}
.${Classes.ICON} {
svg path {
fill: ${props => props.theme.colors.modal.manageUser};
}
}
&:hover {
.${Classes.TEXT} {
color: ${props => props.theme.colors.modal.headerText};
}
.${Classes.ICON} {
svg path {
fill: ${props => props.theme.colors.modal.headerText};
}
}
}
`;
const ErrorBox = styled.div<{ message?: boolean }>`
${props => (props.message ? `margin: ${props.theme.spaces[9]}px 0px` : null)};
`;
const StyledInviteFieldGroup = styled.div`
@ -84,36 +105,51 @@ const StyledInviteFieldGroup = styled.div`
display: flex;
width: 100%;
flex-direction: row;
align-items: center;
justify-content: space-between;
padding-right: 5px;
border-width: 1px;
margin-right: 5px;
border-right: 0px;
border-style: solid;
border-color: ${Colors.ATHENS_GRAY};
}
`;
const UserList = styled.div`
max-height: 200px;
margin-top: 20px;
overflow-y: scroll;
.user {
display: flex;
flex-direction: row;
justify-content: space-between;
margin-top: 8px;
margin-bottom: 8px;
margin-top: 10px;
`;
const User = styled.div`
display: flex;
align-items: center;
height: 54px;
padding-left: 15px;
justify-content: space-between;
color: ${props => props.theme.colors.modal.user.textColor};
`;
const UserInfo = styled.div`
display: inline-flex;
align-items: center;
`;
const UserRole = styled.div`
flex-basis: 25%;
.${Classes.TEXT} {
color: ${props => props.theme.colors.modal.headerText};
}
`;
const StyledButton = styled(Button)`
&&&.${Classes.BUTTON} {
width: 83px;
height: 31px;
border-radius: 0px;
const UserName = styled.div`
display: flex;
flex-direction: column;
margin-left: 10px;
span:nth-child(1) {
margin-bottom: 1px;
}
`;
const RoleDivider = styled.div`
border-top: 1px solid ${props => props.theme.colors.menuBorder};
`;
const Loading = styled(Spinner)`
padding-top: 10px;
margin: auto;
@ -123,15 +159,16 @@ const Loading = styled(Spinner)`
const MailConfigContainer = styled.div`
display: flex;
flex-direction: column;
padding: 5px;
padding: ${props => props.theme.spaces[9]}px
${props => props.theme.spaces[2]}px;
align-items: center;
&& > span {
color: #2e3d49;
color: ${props => props.theme.colors.modal.email.message};
font-weight: 500;
font-size: 14px;
}
&& > a {
color: rgba(46, 61, 73, 0.5);
color: ${props => props.theme.colors.modal.email.desc};
font-size: 12px;
text-decoration: underline;
}
@ -208,22 +245,39 @@ const OrgInviteUsersForm = (props: any) => {
const styledRoles = props.roles.map((role: any) => {
return {
id: role.id,
name: role.name,
content: (
<DropDownOption>
<OptionTitle>{role.name}</OptionTitle>
<OptionDescription>{role.description}</OptionDescription>
</DropDownOption>
),
value: role.name,
label: role.description,
};
});
const themeDetails = useSelector(getThemeDetails);
const allUsersProfiles = React.useMemo(
() =>
allUsers.map(
(user: { username: string; roleName: string; name: string }) => {
const details = getInitialsAndColorCode(
user.name || user.username,
themeDetails.theme.colors.appCardColors,
);
return {
...user,
imageBackground: details[1],
initials: details[0],
};
},
),
[allUsers, themeDetails],
);
return (
<>
{isApplicationInvite && (
<>
<Divider />
<OrgInviteTitle>Invite Users to {currentOrg?.name} </OrgInviteTitle>
<OrgInviteTitle>
<Text type={TextType.H5}>Invite Users to {currentOrg?.name} </Text>
</OrgInviteTitle>
</>
)}
<StyledForm
@ -233,12 +287,6 @@ const OrgInviteUsersForm = (props: any) => {
return inviteUsersToOrg({ ...values, orgId: props.orgId }, dispatch);
})}
>
{submitSucceeded && (
<FormMessage intent="primary" message={INVITE_USERS_SUBMIT_SUCCESS} />
)}
{submitFailed && error && (
<FormMessage intent="danger" message={error} />
)}
<StyledInviteFieldGroup>
<div className="wrapper">
<TagListField
@ -258,14 +306,14 @@ const OrgInviteUsersForm = (props: any) => {
data-cy="t--invite-role-input"
/>
</div>
<StyledButton
<Button
tag="button"
className="t--invite-user-btn"
disabled={!valid}
text="Invite"
filled
intent="primary"
loading={submitting && !(submitFailed && !anyTouched)}
type="submit"
size={Size.large}
variant={Variant.info}
isLoading={submitting && !(submitFailed && !anyTouched)}
/>
</StyledInviteFieldGroup>
{isLoading ? (
@ -286,27 +334,62 @@ const OrgInviteUsersForm = (props: any) => {
</MailConfigContainer>
)}
<UserList style={{ justifyContent: "space-between" }}>
{allUsers.map((user: { username: string; roleName: string }) => {
return (
<div className="user" key={user.username}>
<div>{user.username}</div>
<div>{user.roleName}</div>
</div>
);
})}
{allUsersProfiles.map(
(user: {
username: string;
name: string;
roleName: string;
imageBackground: string;
initials: string;
}) => {
return (
<>
<User key={user.username}>
<UserInfo>
<ProfileImage backgroundColor={user.imageBackground}>
<Text type={TextType.H6} highlight>
{user.initials}
</Text>
</ProfileImage>
<UserName>
<Text type={TextType.H5}>{user.name}</Text>
<Text type={TextType.P2}>{user.username}</Text>
</UserName>
</UserInfo>
<UserRole>
<Text type={TextType.P1}>{user.roleName}</Text>
</UserRole>
</User>
<RoleDivider />
</>
);
},
)}
</UserList>
</React.Fragment>
)}
<ErrorBox message={submitSucceeded || submitFailed}>
{submitSucceeded && (
<Callout
text={INVITE_USERS_SUBMIT_SUCCESS}
variant={Variant.success}
fill
/>
)}
{submitFailed && error && (
<Callout text={error} variant={Variant.danger} fill />
)}
</ErrorBox>
{!pathRegex.test(currentPath) && canManage && (
<Button
className="manageUsers"
text="Manage Users"
filled
intent="primary"
<ManageUsers
onClick={() => {
history.push(`/org/${props.orgId}/settings/members`);
}}
/>
>
<Text type={TextType.H6}>MANAGE USERS</Text>
<Icon name="manage" size={IconSize.XXS} />
</ManageUsers>
)}
</StyledForm>
</>