From 1cafe307d570ffb51534c827e9e9c9c40127ec51 Mon Sep 17 00:00:00 2001 From: satbir121 <39981226+satbir121@users.noreply.github.com> Date: Mon, 12 Oct 2020 12:45:35 +0530 Subject: [PATCH] Feature/invitation modal (#938) * 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 * test cases fixed * used blueprint classes * used calc for width distribution * copy button width fixed * prop passing fixed * copy button size reduced * readonly input field background color fixed * input background theme name ordering changed * TagInputComponent moved to ads * created DropdownWrapper for select field in orgInviteForm * Warnings created due to unique key and depdencies is fixed * Warning fixed in dropdown component * correct prop name used Co-authored-by: Rohit Kumawat --- app/client/cypress/locators/HomePage.json | 8 +- app/client/src/assets/icons/ads/manage.svg | 4 + app/client/src/components/ads/Button.tsx | 59 ++++- app/client/src/components/ads/Callout.tsx | 21 +- app/client/src/components/ads/Dropdown.tsx | 49 ++-- .../src/components/ads/EditableText.tsx | 52 ++-- app/client/src/components/ads/Icon.tsx | 6 + .../TagInputComponent.tsx | 44 ++-- app/client/src/components/ads/TextInput.tsx | 40 ++- app/client/src/components/ads/Toggle.tsx | 2 +- .../appsmith/CopyToClipBoard.tsx | 47 ++-- .../form/FormDialogComponent.tsx | 34 ++- .../form/fields/DropdownWrapper.tsx | 38 +++ .../form/fields/SelectField.tsx | 8 +- .../form/fields/TagListField.tsx | 2 +- .../components/stories/Callout.stories.tsx | 4 +- .../components/stories/Dropdown.stories.tsx | 3 + app/client/src/constants/DefaultTheme.tsx | 172 +++++++++--- .../pages/Applications/ApplicationCard.tsx | 8 +- app/client/src/pages/Applications/index.tsx | 2 +- .../src/pages/common/ProfileDropdown.tsx | 6 +- .../pages/organization/AppInviteUsersForm.tsx | 46 ++-- .../pages/organization/OrgInviteUsersForm.tsx | 244 ++++++++++++------ 23 files changed, 606 insertions(+), 293 deletions(-) create mode 100644 app/client/src/assets/icons/ads/manage.svg rename app/client/src/components/{editorComponents => ads}/TagInputComponent.tsx (76%) create mode 100644 app/client/src/components/editorComponents/form/fields/DropdownWrapper.tsx diff --git a/app/client/cypress/locators/HomePage.json b/app/client/cypress/locators/HomePage.json index a6f5c9ec5f..048dde690b 100644 --- a/app/client/cypress/locators/HomePage.json +++ b/app/client/cypress/locators/HomePage.json @@ -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": "//div[@class='label-title']//span[text()='Administrator']", + "viewerRole": "//div[@class='label-title']//span[text()='App Viewer']", + "developerRole": "//div[@class='label-title']//span[text()='Developer']", + "inviteBtn": "//button[text()='Invite']", "manageUsers": ".manageUsers", "DeleteBtn": "[data-cy=t--deleteUser]", "ShareBtn": "//a[text()='Share']", diff --git a/app/client/src/assets/icons/ads/manage.svg b/app/client/src/assets/icons/ads/manage.svg new file mode 100644 index 0000000000..27ba5fc70a --- /dev/null +++ b/app/client/src/assets/icons/ads/manage.svg @@ -0,0 +1,4 @@ + + + + diff --git a/app/client/src/components/ads/Button.tsx b/app/client/src/components/ads/Button.tsx index 3131abcf11..164c079843 100644 --- a/app/client/src/components/ads/Button.tsx +++ b/app/client/src/components/ads/Button.tsx @@ -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")` +const ButtonStyles = css` width: ${props => (props.fill ? "100%" : "auto")}; height: ${props => btnFontStyles(props).height}px; border: none; @@ -295,7 +296,7 @@ const StyledButton = styled("a")` 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")` } `; +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 = {props.text}; - return ( - ) => - 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 ? : null} - + ); + + if (props.tag === "button") { + return ( + ) => + props.onClick && props.onClick(e) + } + > + {buttonContent} + + ); + } else { + return ( + ) => + props.onClick && props.onClick(e) + } + > + {buttonContent} + + ); + } } export default Button; diff --git a/app/client/src/components/ads/Callout.tsx b/app/client/src/components/ads/Callout.tsx index d3ff387d38..7c5236dca3 100644 --- a/app/client/src/components/ads/Callout.tsx +++ b/app/client/src/components/ads/Callout.tsx @@ -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 ( - + {props.text} ); diff --git a/app/client/src/components/ads/Dropdown.tsx b/app/client/src/components/ads/Dropdown.tsx index c55df20e19..e42ac67e4e 100644 --- a/app/client/src/components/ads/Dropdown.tsx +++ b/app/client/src/components/ads/Dropdown.tsx @@ -6,38 +6,42 @@ import Text, { TextType } from "./Text"; type DropdownOption = { label?: string; - value: string; + value?: string; id?: string; icon?: IconName; - onSelect?: (option: DropdownOption) => void; - children?: DropdownOption[]; + onSelect?: (value?: string) => void; }; type DropdownProps = CommonComponentProps & { options: DropdownOption[]; selected: DropdownOption; + onSelect?: (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 +51,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}; @@ -117,6 +125,7 @@ const LabelWrapper = styled.div<{ label?: string }>` `; export default function Dropdown(props: DropdownProps) { + const { onSelect } = { ...props }; const [isOpen, setIsOpen] = useState(false); const [selected, setSelected] = useState(props.selected); @@ -124,11 +133,15 @@ export default function Dropdown(props: DropdownProps) { setSelected(props.selected); }, [props.selected]); - const optionClickHandler = useCallback((option: DropdownOption) => { - setSelected(option); - setIsOpen(false); - option.onSelect && option.onSelect(option); - }, []); + const optionClickHandler = useCallback( + (option: DropdownOption) => { + setSelected(option); + setIsOpen(false); + onSelect && onSelect(option.value); + option.onSelect && option.onSelect(option.value); + }, + [onSelect], + ); return ( setIsOpen(!isOpen)} > {selected.value} - + {isOpen && !props.disabled ? ( @@ -159,7 +172,9 @@ export default function Dropdown(props: DropdownProps) { ) : null} {option.label ? ( - {option.value} +
+ {option.value} +
) : ( {option.value} )} diff --git a/app/client/src/components/ads/EditableText.tsx b/app/client/src/components/ads/EditableText.tsx index 4eabe3af46..bf3ac6d645 100644 --- a/app/client/src/components/ads/EditableText.tsx +++ b/app/client/src/components/ads/EditableText.tsx @@ -128,15 +128,22 @@ const IconWrapper = styled.div` `; export const EditableText = (props: EditableTextProps) => { - const [isEditing, setIsEditing] = useState(!!props.isEditingDefault); - const [value, setValue] = useState(props.defaultValue); - const [lastValidValue, setLastValidValue] = useState(props.defaultValue); + const { + onBlur, + onTextChanged, + isInvalid: inputValidation, + defaultValue, + isEditingDefault, + } = props; + const [isEditing, setIsEditing] = useState(!!isEditingDefault); + const [value, setValue] = useState(defaultValue); + const [lastValidValue, setLastValidValue] = useState(defaultValue); const [isInvalid, setIsInvalid] = useState(false); const [changeStarted, setChangeStarted] = useState(false); const [savingState, setSavingState] = useState( SavingState.NOT_STARTED, ); - const valueRef = React.useRef(props.defaultValue); + const valueRef = React.useRef(defaultValue); useEffect(() => { setSavingState(props.savingState); @@ -144,18 +151,19 @@ export const EditableText = (props: EditableTextProps) => { useEffect(() => { return () => { - props.onBlur(valueRef.current); + onBlur(valueRef.current); }; + // eslint-disable-next-line react-hooks/exhaustive-deps }, []); useEffect(() => { - setValue(props.defaultValue); - setIsEditing(!!props.isEditingDefault); - }, [props.defaultValue, props.isEditingDefault]); + setValue(defaultValue); + setIsEditing(!!isEditingDefault); + }, [defaultValue, isEditingDefault]); useEffect(() => { - if (props.forceDefault === true) setValue(props.defaultValue); - }, [props.forceDefault, props.defaultValue]); + if (props.forceDefault === true) setValue(defaultValue); + }, [props.forceDefault, defaultValue]); const themeDetails = useSelector(getThemeDetails); const bgColor = useMemo( @@ -167,35 +175,41 @@ export const EditableText = (props: EditableTextProps) => { const editMode = useCallback( (e: React.MouseEvent) => { setIsEditing(true); - const errorMessage = - props.isInvalid && props.isInvalid(props.defaultValue); + const errorMessage = inputValidation && inputValidation(defaultValue); setIsInvalid(errorMessage ? errorMessage : false); e.preventDefault(); e.stopPropagation(); }, - [props], + [inputValidation, defaultValue], ); const onConfirm = useCallback( (_value: string) => { if (savingState === SavingState.ERROR || isInvalid) { setValue(lastValidValue); - props.onBlur(lastValidValue); + onBlur(lastValidValue); setSavingState(SavingState.NOT_STARTED); } else if (changeStarted) { - props.onTextChanged(_value); - props.onBlur(_value); + onTextChanged(_value); + onBlur(_value); } setIsEditing(false); setChangeStarted(false); }, - [changeStarted, lastValidValue, props.onBlur, props.onTextChanged], + [ + changeStarted, + savingState, + isInvalid, + lastValidValue, + onBlur, + onTextChanged, + ], ); const onInputchange = useCallback( (_value: string) => { const finalVal: string = _value; - const errorMessage = props.isInvalid && props.isInvalid(finalVal); + const errorMessage = inputValidation && inputValidation(finalVal); const error = errorMessage ? errorMessage : false; if (!error) { setLastValidValue(finalVal); @@ -205,7 +219,7 @@ export const EditableText = (props: EditableTextProps) => { setIsInvalid(error); setChangeStarted(true); }, - [props.isInvalid], + [inputValidation], ); const iconName = diff --git a/app/client/src/components/ads/Icon.tsx b/app/client/src/components/ads/Icon.tsx index 5db54588aa..647c9cbbf5 100644 --- a/app/client/src/components/ads/Icon.tsx +++ b/app/client/src/components/ads/Icon.tsx @@ -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` 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 = ; break; + case "manage": + returnIcon = ; + break; default: returnIcon = null; break; diff --git a/app/client/src/components/editorComponents/TagInputComponent.tsx b/app/client/src/components/ads/TagInputComponent.tsx similarity index 76% rename from app/client/src/components/editorComponents/TagInputComponent.tsx rename to app/client/src/components/ads/TagInputComponent.tsx index 0fd7307bd4..2bfb538882 100644 --- a/app/client/src/components/editorComponents/TagInputComponent.tsx +++ b/app/client/src/components/ads/TagInputComponent.tsx @@ -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 { Classes, TagInput } from "@blueprintjs/core"; +import { Intent } from "constants/DefaultTheme"; const TagInputWrapper = styled.div<{ intent?: Intent }>` + margin-right: 8px; + &&& { - .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}; + .${Classes.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; + } + .${Classes.TAG_INPUT}.${Classes.ACTIVE} { + border: 1px solid ${props => props.theme.colors.info.main}; + box-shadow: ${props => props.theme.colors.tagInput.shadow}; + } + .${Classes.INPUT_GHOST} { + color: ${props => props.theme.colors.tagInput.text}; + &::placeholder { + color: ${props => props.theme.colors.tagInput.placeholder}; + } + } + .${Classes.TAG} { + 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; } } `; diff --git a/app/client/src/components/ads/TextInput.tsx b/app/client/src/components/ads/TextInput.tsx index b2bc1d87e6..482f2477b7 100644 --- a/app/client/src/components/ads/TextInput.tsx +++ b/app/client/src/components/ads/TextInput.tsx @@ -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 = { @@ -61,6 +62,11 @@ const boxStyles = ( color = theme.colors.textInput.disable.text; borderColor = theme.colors.textInput.disable.border; } + if (props.readOnly) { + bgColor = theme.colors.textInput.readOnly.bg; + color = theme.colors.textInput.readOnly.text; + borderColor = theme.colors.textInput.readOnly.border; + } if (!isValid) { bgColor = hexToRgba(theme.colors.danger.main, 0.1); color = theme.colors.danger.main; @@ -77,28 +83,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 +185,7 @@ const TextInput = forwardRef( {...props} placeholder={props.placeholder} onChange={memoizedChangeHandler} + readOnly={props.readOnly} /> {ErrorMessage} diff --git a/app/client/src/components/ads/Toggle.tsx b/app/client/src/components/ads/Toggle.tsx index edb7f9a21e..589fb704d4 100644 --- a/app/client/src/components/ads/Toggle.tsx +++ b/app/client/src/components/ads/Toggle.tsx @@ -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"; diff --git a/app/client/src/components/designSystems/appsmith/CopyToClipBoard.tsx b/app/client/src/components/designSystems/appsmith/CopyToClipBoard.tsx index 22bea611a6..e6ff4633a3 100644 --- a/app/client/src/components/designSystems/appsmith/CopyToClipBoard.tsx +++ b/app/client/src/components/designSystems/appsmith/CopyToClipBoard.tsx @@ -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: calc(100% - 110px); + } + a { + flex-basis: 110px; } `; @@ -54,21 +35,23 @@ const CopyToClipboard = (props: any) => { }; return ( - { + onChange={() => { selectText(); }} - value={copyText} + defaultValue={copyText} /> - { copyToClipboard(copyText); }} + size={Size.large} /> ); diff --git a/app/client/src/components/editorComponents/form/FormDialogComponent.tsx b/app/client/src/components/editorComponents/form/FormDialogComponent.tsx index 7a7eb67024..b7f9d98ffd 100644 --- a/app/client/src/components/editorComponents/form/FormDialogComponent.tsx +++ b/app/client/src/components/editorComponents/form/FormDialogComponent.tsx @@ -5,12 +5,36 @@ import { isPermitted } from "pages/Applications/permissionHelpers"; const StyledDialog = styled(Dialog)<{ setMaxWidth?: boolean }>` && { - background: white; - & .bp3-dialog-header { - padding: ${props => props.theme.spaces[4]}px - ${props => props.theme.spaces[4]}px; + border-radius: 0px; + padding-bottom: 5px; + background: ${props => props.theme.colors.modal.bg}; + width: 640px; + + & .${Classes.DIALOG_HEADER} { + padding: ${props => props.theme.spaces[4]}px; + background: ${props => props.theme.colors.modal.bg}; + box-shadow: none; + .${Classes.ICON} { + color: ${props => props.theme.colors.modal.iconColor}; + } + .${Classes.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; + } + + .${Classes.BUTTON}.${Classes.MINIMAL}:hover { + background-color: ${props => props.theme.colors.modal.bg}; + } } - & .bp3-dialog-footer-actions { + & .${Classes.DIALOG_BODY} { + margin: ${props => props.theme.spaces[9]}px; + } + & .${Classes.DIALOG_FOOTER_ACTIONS} { display: block; } ${props => props.setMaxWidth && `width: 100vh;`} diff --git a/app/client/src/components/editorComponents/form/fields/DropdownWrapper.tsx b/app/client/src/components/editorComponents/form/fields/DropdownWrapper.tsx new file mode 100644 index 0000000000..99200a880b --- /dev/null +++ b/app/client/src/components/editorComponents/form/fields/DropdownWrapper.tsx @@ -0,0 +1,38 @@ +import Dropdown from "components/ads/Dropdown"; +import React, { useEffect, useState } from "react"; + +type DropdownWrapperProps = { + placeholder: string; + input?: { + value?: string; + onChange?: (value?: string) => void; + }; + options: Array<{ id: string; value: string; label: string }>; +}; + +const DropdownWrapper = (props: DropdownWrapperProps) => { + const [selectedOption, setSelectedOption] = useState({ + value: props.placeholder, + }); + const onSelectHandler = (value?: string) => { + props.input && props.input.onChange && props.input.onChange(value); + }; + + useEffect(() => { + if (props.placeholder) { + setSelectedOption({ value: props.placeholder }); + } else if (props.input && props.input.value) { + setSelectedOption({ value: props.input.value }); + } + }, [props.input, props.placeholder]); + + return ( + + ); +}; + +export default DropdownWrapper; diff --git a/app/client/src/components/editorComponents/form/fields/SelectField.tsx b/app/client/src/components/editorComponents/form/fields/SelectField.tsx index 7d0e3dfa50..a42dd6b1ef 100644 --- a/app/client/src/components/editorComponents/form/fields/SelectField.tsx +++ b/app/client/src/components/editorComponents/form/fields/SelectField.tsx @@ -4,7 +4,7 @@ import { WrappedFieldMetaProps, WrappedFieldInputProps, } from "redux-form"; -import SelectComponent from "components/editorComponents/SelectComponent"; +import DropdownWrapper from "./DropdownWrapper"; const renderComponent = ( componentProps: SelectFieldProps & { @@ -14,15 +14,15 @@ const renderComponent = ( ) => { return ( - + ); }; type SelectFieldProps = { name: string; - placeholder?: string; - options?: Array<{ id: string; name: string; value?: string }>; + placeholder: string; + options: Array<{ id: string; value: string; label: string }>; size?: "large" | "small"; outline?: boolean; }; diff --git a/app/client/src/components/editorComponents/form/fields/TagListField.tsx b/app/client/src/components/editorComponents/form/fields/TagListField.tsx index f34a64ee55..a2d38ccb27 100644 --- a/app/client/src/components/editorComponents/form/fields/TagListField.tsx +++ b/app/client/src/components/editorComponents/form/fields/TagListField.tsx @@ -4,7 +4,7 @@ import { WrappedFieldMetaProps, WrappedFieldInputProps, } from "redux-form"; -import TagInputComponent from "components/editorComponents/TagInputComponent"; +import TagInputComponent from "components/ads/TagInputComponent"; import { Intent } from "constants/DefaultTheme"; const renderComponent = ( diff --git a/app/client/src/components/stories/Callout.stories.tsx b/app/client/src/components/stories/Callout.stories.tsx index 6c61953fb0..38c8cf36bf 100644 --- a/app/client/src/components/stories/Callout.stories.tsx +++ b/app/client/src/components/stories/Callout.stories.tsx @@ -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 = () => ( ); diff --git a/app/client/src/components/stories/Dropdown.stories.tsx b/app/client/src/components/stories/Dropdown.stories.tsx index 74233c6a6a..a968cdbf68 100644 --- a/app/client/src/components/stories/Dropdown.stories.tsx +++ b/app/client/src/components/stories/Dropdown.stories.tsx @@ -32,6 +32,7 @@ export const Text = () => ( onSelect: action("selected-option"), }, ]} + onSelect={action("selected-option")} selected={{ id: select("Selected id", ["111abc", "222abc", "333abc"], "111abc"), value: text("Selected value", "First option"), @@ -64,6 +65,7 @@ export const IconAndText = () => ( onSelect: action("selected-option"), }, ]} + onSelect={action("selected-option")} selected={{ id: select("Selected id", ["111abc", "222abc", "333abc"], "111abc"), value: text("Selected value", "Delete"), @@ -102,6 +104,7 @@ export const LabelAndText = () => ( onSelect: action("selected-option"), }, ]} + onSelect={action("selected-option")} selected={{ id: select("Selected id", ["111abc", "222abc", "333abc"], "111abc"), value: text("Selected value", "Developer"), diff --git a/app/client/src/constants/DefaultTheme.tsx b/app/client/src/constants/DefaultTheme.tsx index 62b4ece3bb..40b073100e 100644 --- a/app/client/src/constants/DefaultTheme.tsx +++ b/app/client/src/constants/DefaultTheme.tsx @@ -479,7 +479,6 @@ type ColorType = { hoverBG: Color; hoverBGOpacity: number; hoverBorder: ShadeColor; - targetBg: string; iconColor: ShadeColor; }; appCardColors: string[]; @@ -522,7 +521,7 @@ type ColorType = { dropdown: { header: { text: ShadeColor; - disabled: ShadeColor; + disabledText: ShadeColor; bg: ShadeColor; disabledBg: ShadeColor; }; @@ -563,6 +562,11 @@ type ColorType = { border: ShadeColor; }; placeholder: ShadeColor; + readOnly: { + bg: ShadeColor; + border: ShadeColor; + text: ShadeColor; + }; }; menuBorder: ShadeColor; editableText: { @@ -637,6 +641,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 = { @@ -683,7 +727,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: [ @@ -736,9 +779,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)", @@ -777,6 +820,11 @@ export const dark: ColorType = { border: darkShades[0], }, placeholder: darkShades[5], + readOnly: { + bg: darkShades[0], + border: darkShades[0], + text: darkShades[7], + }, }, menuBorder: darkShades[4], editableText: { @@ -851,6 +899,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 = { @@ -897,7 +985,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: [ @@ -950,7 +1037,7 @@ export const light: ColorType = { dropdown: { header: { text: lightShades[9], - disabled: darkShades[6], + disabledText: darkShades[6], bg: lightShades[2], disabledBg: lightShades[1], }, @@ -990,7 +1077,12 @@ export const light: ColorType = { text: lightShades[9], border: lightShades[2], }, - placeholder: lightShades[6], + placeholder: lightShades[7], + readOnly: { + bg: lightShades[2], + border: lightShades[2], + text: lightShades[7], + }, }, menuBorder: lightShades[3], editableText: { @@ -1065,6 +1157,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 = { @@ -1171,28 +1303,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, diff --git a/app/client/src/pages/Applications/ApplicationCard.tsx b/app/client/src/pages/Applications/ApplicationCard.tsx index 6f736c8995..6726b6613f 100644 --- a/app/client/src/pages/Applications/ApplicationCard.tsx +++ b/app/client/src/pages/Applications/ApplicationCard.tsx @@ -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) => ( -
+
))` .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" > { {applications.map((application: any) => { return ( application.pages?.length > 0 && ( - + ` - width: 30px; - height: 30px; +export const ProfileImage = styled.div<{ backgroundColor?: string }>` + width: 34px; + height: 34px; display: flex; align-items: center; border-radius: 50%; diff --git a/app/client/src/pages/organization/AppInviteUsersForm.tsx b/app/client/src/pages/organization/AppInviteUsersForm.tsx index 24ba07bdf8..0f22b81c60 100644 --- a/app/client/src/pages/organization/AppInviteUsersForm.tsx +++ b/app/client/src/pages/organization/AppInviteUsersForm.tsx @@ -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 && ( <> - Make the application public + Make the application public - {(isChangingViewAccess || isFetchingApplication) && ( - - )} {currentApplicationDetails && ( - { + { changeAppViewAccess( applicationId, !currentApplicationDetails.isPublic, ); }} - disabled={isChangingViewAccess || isFetchingApplication} - checked={currentApplicationDetails.isPublic} - large /> )} )} - Get Shareable link for this for this application + + <Text type={TextType.H5}> + Get Shareable link for this for this application + </Text> + {canInviteToOrg && ( diff --git a/app/client/src/pages/organization/OrgInviteUsersForm.tsx b/app/client/src/pages/organization/OrgInviteUsersForm.tsx index e453ed39ca..18aabd68e1 100644 --- a/app/client/src/pages/organization/OrgInviteUsersForm.tsx +++ b/app/client/src/pages/organization/OrgInviteUsersForm.tsx @@ -1,12 +1,11 @@ -import React, { useEffect } from "react"; +import React, { Fragment, useEffect } from "react"; import styled from "styled-components"; import { useLocation } from "react-router-dom"; 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: 85%; 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: ( - - {role.name} - {role.description} - - ), + 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 && ( <> - Invite Users to {currentOrg?.name} + + Invite Users to {currentOrg?.name} + )} { return inviteUsersToOrg({ ...values, orgId: props.orgId }, dispatch); })} > - {submitSucceeded && ( - - )} - {submitFailed && error && ( - - )}
{ data-cy="t--invite-role-input" />
-
{isLoading ? ( @@ -286,27 +334,63 @@ const OrgInviteUsersForm = (props: any) => { )} - {allUsers.map((user: { username: string; roleName: string }) => { - return ( -
-
{user.username}
-
{user.roleName}
-
- ); - })} + {allUsersProfiles.map( + (user: { + username: string; + name: string; + roleName: string; + imageBackground: string; + initials: string; + }) => { + return ( + + + + + + {user.initials} + + + + {user.name} + {user.username} + + + + {user.roleName} + + + + + + ); + }, + )}
)} + + {submitSucceeded && ( + + )} + {submitFailed && error && ( + + )} + {!pathRegex.test(currentPath) && canManage && ( -