From 0a35ecdcaf4f2f9c58b1739c74111f2c9e312faa Mon Sep 17 00:00:00 2001 From: Vivek Pandey Date: Sun, 22 Nov 2020 20:16:27 +0530 Subject: [PATCH 01/48] show number of the records --- .../src/components/designSystems/appsmith/TableHeader.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/client/src/components/designSystems/appsmith/TableHeader.tsx b/app/client/src/components/designSystems/appsmith/TableHeader.tsx index e3b8c3ed02a..a5cc3efeddf 100644 --- a/app/client/src/components/designSystems/appsmith/TableHeader.tsx +++ b/app/client/src/components/designSystems/appsmith/TableHeader.tsx @@ -173,7 +173,7 @@ const TableHeader = (props: TableHeaderProps) => { {!props.serverSidePaginationEnabled && ( - Showing {props.currentPageIndex + 1}-{props.tableData?.length} items + Show {props.tableData?.length} records Date: Tue, 24 Nov 2020 12:31:37 +0530 Subject: [PATCH 02/48] Feature: Toast component (#476) --- app/client/src/assets/icons/ads/warning.svg | 3 + app/client/src/components/ads/Button.tsx | 15 +- app/client/src/components/ads/Callout.tsx | 3 +- app/client/src/components/ads/Icon.tsx | 5 + app/client/src/components/ads/Toast.tsx | 182 ++++++++++++++++-- app/client/src/components/ads/common.tsx | 30 +++ .../blueprint/ButtonComponent.tsx | 9 +- .../editorComponents/EditableText.tsx | 9 +- .../editorComponents/ToastComponent.tsx | 121 ------------ .../src/components/stories/Button.stories.tsx | 4 +- .../components/stories/Callout.stories.tsx | 3 +- .../src/components/stories/Icon.stories.tsx | 4 +- .../src/components/stories/Table.stories.tsx | 4 +- .../src/components/stories/Toast.stories.tsx | 51 +++++ app/client/src/constants/DefaultTheme.tsx | 21 ++ app/client/src/index.tsx | 7 +- .../Editor/DataSourceEditor/Connected.tsx | 9 +- app/client/src/pages/organization/Members.tsx | 3 +- .../pages/organization/OrgInviteUsersForm.tsx | 4 +- app/client/src/sagas/ActionExecutionSagas.ts | 74 ++++--- app/client/src/sagas/ActionSagas.ts | 46 ++--- app/client/src/sagas/ApplicationSagas.tsx | 23 ++- app/client/src/sagas/CurlImportSagas.ts | 9 +- app/client/src/sagas/DatasourcesSagas.ts | 38 ++-- app/client/src/sagas/ErrorSagas.tsx | 6 +- app/client/src/sagas/OrgSagas.ts | 10 +- app/client/src/sagas/ProvidersSaga.ts | 10 +- app/client/src/sagas/WidgetOperationSagas.tsx | 39 ++-- app/client/src/sagas/evaluationsSaga.ts | 16 +- 29 files changed, 458 insertions(+), 300 deletions(-) create mode 100644 app/client/src/assets/icons/ads/warning.svg delete mode 100644 app/client/src/components/editorComponents/ToastComponent.tsx create mode 100644 app/client/src/components/stories/Toast.stories.tsx diff --git a/app/client/src/assets/icons/ads/warning.svg b/app/client/src/assets/icons/ads/warning.svg new file mode 100644 index 00000000000..30a30a92bf1 --- /dev/null +++ b/app/client/src/assets/icons/ads/warning.svg @@ -0,0 +1,3 @@ + + + diff --git a/app/client/src/components/ads/Button.tsx b/app/client/src/components/ads/Button.tsx index 164c0798434..80556eb5ca9 100644 --- a/app/client/src/components/ads/Button.tsx +++ b/app/client/src/components/ads/Button.tsx @@ -1,5 +1,11 @@ import React from "react"; -import { CommonComponentProps, hexToRgba, ThemeProp, Classes } from "./common"; +import { + CommonComponentProps, + hexToRgba, + ThemeProp, + Classes, + Variant, +} from "./common"; import styled, { css } from "styled-components"; import Icon, { IconName, IconSize } from "./Icon"; import Spinner from "./Spinner"; @@ -11,13 +17,6 @@ export enum Category { tertiary = "tertiary", } -export enum Variant { - success = "success", - info = "info", - warning = "warning", - danger = "danger", -} - export enum Size { small = "small", medium = "medium", diff --git a/app/client/src/components/ads/Callout.tsx b/app/client/src/components/ads/Callout.tsx index 7c5236dca3d..efd24b278d9 100644 --- a/app/client/src/components/ads/Callout.tsx +++ b/app/client/src/components/ads/Callout.tsx @@ -1,8 +1,7 @@ import React from "react"; -import { CommonComponentProps, Classes } from "./common"; +import { CommonComponentProps, Classes, Variant } from "./common"; import Text, { TextType } from "./Text"; import styled from "styled-components"; -import { Variant } from "./Button"; type CalloutProps = CommonComponentProps & { variant?: Variant; diff --git a/app/client/src/components/ads/Icon.tsx b/app/client/src/components/ads/Icon.tsx index f01ff745943..719fbbd4a22 100644 --- a/app/client/src/components/ads/Icon.tsx +++ b/app/client/src/components/ads/Icon.tsx @@ -8,6 +8,7 @@ import { ReactComponent as ErrorIcon } from "assets/icons/ads/error.svg"; import { ReactComponent as SuccessIcon } from "assets/icons/ads/success.svg"; import { ReactComponent as SearchIcon } from "assets/icons/ads/search.svg"; import { ReactComponent as CloseIcon } from "assets/icons/ads/close.svg"; +import { ReactComponent as WarningIcon } from "assets/icons/ads/warning.svg"; import { ReactComponent as DownArrow } from "assets/icons/ads/down_arrow.svg"; import { ReactComponent as ShareIcon } from "assets/icons/ads/share.svg"; import { ReactComponent as RocketIcon } from "assets/icons/ads/launch.svg"; @@ -86,6 +87,7 @@ export const IconCollection = [ "plus", "invite-user", "view-all", + "warning", "downArrow", "context-menu", "duplicate", @@ -197,6 +199,9 @@ const Icon = forwardRef( case "manage": returnIcon = ; break; + case "warning": + returnIcon = ; + break; default: returnIcon = null; break; diff --git a/app/client/src/components/ads/Toast.tsx b/app/client/src/components/ads/Toast.tsx index bf9425130ff..8d8f6388280 100644 --- a/app/client/src/components/ads/Toast.tsx +++ b/app/client/src/components/ads/Toast.tsx @@ -1,20 +1,166 @@ -import { CommonComponentProps } from "./common"; - -type ToastProps = CommonComponentProps & { - text: string; - duration: number; - variant?: "success" | "info" | "warning" | "danger"; //default info - keepOnHover?: boolean; - onComplete?: any; - position: - | "top-right" - | "top-center" - | "top-left" - | "bottom-right" - | "bottom-center" - | "bottom-left"; +import React from "react"; +import { CommonComponentProps, Classes, Variant } from "./common"; +import styled from "styled-components"; +import Icon, { IconSize } from "./Icon"; +import Text, { TextType } from "./Text"; +import { toast, ToastOptions, ToastContainer } from "react-toastify"; +import "react-toastify/dist/ReactToastify.css"; +import { ReduxActionType } from "constants/ReduxActionConstants"; +import { useDispatch } from "react-redux"; + +type ToastProps = ToastOptions & + CommonComponentProps & { + text: string; + variant?: Variant; + duration?: number; + onUndo?: () => void; + dispatchableAction?: { type: ReduxActionType; payload: any }; + hideProgressBar?: boolean; + }; + +const WrappedToastContainer = styled.div` + .Toastify__toast-container { + width: auto; + padding: 0px; + } + .Toastify__toast--default { + background: transparent; + } + .Toastify__toast { + cursor: auto; + min-height: auto; + border-radius: 0px !important; + font-family: ${props => props.theme.fonts.text}; + margin-bottom: ${props => props.theme.spaces[4]}px; + } + .Toastify__toast-container--top-right { + top: 4em; + } +`; +export const StyledToastContainer = (props: ToastOptions) => { + return ( + + + + ); +}; + +const ToastBody = styled.div<{ + variant?: Variant; + onUndo?: () => void; + dispatchableAction?: { type: ReduxActionType; payload: any }; +}>` + width: 264px; + background: ${props => props.theme.colors.toast.bg}; + padding: ${props => props.theme.spaces[4]}px + ${props => props.theme.spaces[5]}px; + display: flex; + align-items: center; + + .${Classes.ICON} { + cursor: auto; + margin-right: ${props => props.theme.spaces[3]}px; + svg { + path { + fill: ${props => + props.variant === Variant.warning + ? props.theme.colors.toast.warningColor + : props.variant === Variant.danger + ? "#FFFFFF" + : "#9F9F9F"}; + } + rect { + ${props => + props.variant === Variant.danger + ? `fill: ${props.theme.colors.toast.dangerColor}` + : null}; + } + } + } + + .${Classes.TEXT} { + color: ${props => props.theme.colors.toast.textColor}; + } + + ${props => + props.onUndo || props.dispatchableAction + ? ` + .${Classes.TEXT}:last-child { + cursor: pointer; + margin-left: ${props.theme.spaces[3]}px; + color: ${props.theme.colors.toast.undo}; + line-height: 18px; + font-weight: 600; + } + ` + : null} +`; + +const ToastComponent = (props: ToastProps & { undoAction?: () => void }) => { + const dispatch = useDispatch(); + + return ( + + {props.variant === Variant.success ? ( + + ) : props.variant === Variant.warning ? ( + + ) : null} + {props.variant === Variant.danger ? ( + + ) : null} + {props.text} + {props.onUndo || props.dispatchableAction ? ( + { + if (props.dispatchableAction) { + dispatch(props.dispatchableAction); + props.undoAction && props.undoAction(); + } else { + props.undoAction && props.undoAction(); + } + }} + > + UNDO + + ) : null} + + ); }; -export default function Toast(props: ToastProps) { - return null; -} +export const Toaster = { + show: (config: ToastProps) => { + if (typeof config.text !== "string") { + console.error("Toast message needs to be a string"); + return; + } + if (config.variant && !Object.values(Variant).includes(config.variant)) { + console.error( + "Toast type needs to be a one of " + Object.values(Variant).join(", "), + ); + return; + } + const toastId = toast( + { + toast.dismiss(toastId); + config.onUndo && config.onUndo(); + }} + {...config} + />, + { + pauseOnHover: true, + autoClose: config.duration || 5000, + closeOnClick: false, + hideProgressBar: config.hideProgressBar, + }, + ); + }, + clear: () => toast.dismiss(), +}; diff --git a/app/client/src/components/ads/common.tsx b/app/client/src/components/ads/common.tsx index 6c3d702c91d..1a89afec5a9 100644 --- a/app/client/src/components/ads/common.tsx +++ b/app/client/src/components/ads/common.tsx @@ -1,6 +1,7 @@ import { Theme } from "constants/DefaultTheme"; import tinycolor from "tinycolor2"; import styled from "styled-components"; +import { toast } from "react-toastify"; export interface CommonComponentProps { isLoading?: boolean; //default false @@ -64,3 +65,32 @@ export const StoryWrapper = styled.div` height: 700px; padding: 50px 100px; `; + +export enum Variant { + success = "success", + info = "info", + warning = "warning", + danger = "danger", +} + +export const ToastVariant = (type: any) => { + let variant: Variant; + switch (type) { + case toast.TYPE.ERROR === type: + variant = Variant.danger; + break; + case toast.TYPE.INFO === type: + variant = Variant.info; + break; + case toast.TYPE.SUCCESS === type: + variant = Variant.success; + break; + case toast.TYPE.WARNING === type: + variant = Variant.warning; + break; + default: + variant = Variant.info; + break; + } + return variant; +}; diff --git a/app/client/src/components/designSystems/blueprint/ButtonComponent.tsx b/app/client/src/components/designSystems/blueprint/ButtonComponent.tsx index d2146327da7..6c52cfb6db6 100644 --- a/app/client/src/components/designSystems/blueprint/ButtonComponent.tsx +++ b/app/client/src/components/designSystems/blueprint/ButtonComponent.tsx @@ -11,11 +11,12 @@ import { Theme, darkenHover, darkenActive } from "constants/DefaultTheme"; import _ from "lodash"; import { ComponentProps } from "components/designSystems/appsmith/BaseComponent"; import useScript from "utils/hooks/useScript"; -import { AppToaster } from "components/editorComponents/ToastComponent"; import { GOOGLE_RECAPTCHA_KEY_ERROR, GOOGLE_RECAPTCHA_DOMAIN_ERROR, } from "constants/messages"; +import { Variant } from "components/ads/common"; +import { Toaster } from "components/ads/Toast"; const getButtonColorStyles = (props: { theme: Theme } & ButtonStyleProps) => { if (props.filled) return props.theme.colors.textOnDarkBG; @@ -166,9 +167,9 @@ const RecaptchaComponent = ( } & RecaptchaProps, ) => { function handleError(event: React.MouseEvent, error: string) { - AppToaster.show({ - message: error, - type: "error", + Toaster.show({ + text: error, + variant: Variant.danger, }); props.onClick && props.onClick(event); } diff --git a/app/client/src/components/editorComponents/EditableText.tsx b/app/client/src/components/editorComponents/EditableText.tsx index b65892182ed..ac469ee618f 100644 --- a/app/client/src/components/editorComponents/EditableText.tsx +++ b/app/client/src/components/editorComponents/EditableText.tsx @@ -8,7 +8,8 @@ import _ from "lodash"; import Edit from "assets/images/EditPen.svg"; import ErrorTooltip from "./ErrorTooltip"; import { Colors } from "constants/Colors"; -import { AppToaster } from "components/editorComponents/ToastComponent"; +import { Toaster } from "components/ads/Toast"; +import { Variant } from "components/ads/common"; export enum EditInteractionKind { SINGLE, @@ -111,9 +112,9 @@ export const EditableText = (props: EditableTextProps) => { props.onTextChanged(_value); setIsEditing(false); } else { - AppToaster.show({ - message: "Invalid name", - type: "error", + Toaster.show({ + text: "Invalid name", + variant: Variant.danger, }); } }; diff --git a/app/client/src/components/editorComponents/ToastComponent.tsx b/app/client/src/components/editorComponents/ToastComponent.tsx deleted file mode 100644 index 1d8161bc430..00000000000 --- a/app/client/src/components/editorComponents/ToastComponent.tsx +++ /dev/null @@ -1,121 +0,0 @@ -import React from "react"; -import { toast, ToastOptions, TypeOptions, ToastType } from "react-toastify"; -import "react-toastify/dist/ReactToastify.css"; -import styled from "styled-components"; -import { theme } from "constants/DefaultTheme"; -import { AlertIcons } from "icons/AlertIcons"; -import { ReduxAction } from "constants/ReduxActionConstants"; -import { useDispatch } from "react-redux"; - -const ToastBody = styled.div<{ type: TypeOptions; action: string }>` - height: 100%; - border-left: 4px solid ${({ type }) => theme.alert[type].color}; - border-radius: 4px; - background-color: white; - color: black; - padding-left: 5px; - display: grid; - grid-template-columns: ${props => - props.action === "enabled" ? "20px 212px 60px" : "20px auto"}; - align-items: center; -`; - -const ToastMessage = styled.span` - font-size: ${props => props.theme.fontSizes[3]}px; - margin: 0 5px; -`; - -const ToastAction = styled.button` - border: none; - background: rgba(214, 65, 95, 0.08); - - color: #d6415f; - font-size: 12px; - font-weight: bold; - padding: 10px; - text-transform: uppercase; - cursor: pointer; - float: right; - &:hover { - background: rgba(214, 65, 95, 0.2); - } -`; - -export const ToastTypeOptions = [ - "info", - "success", - "warning", - "error", - "default", -]; - -const ToastIcon = { - info: AlertIcons.INFO, - success: AlertIcons.SUCCESS, - error: AlertIcons.ERROR, - warning: AlertIcons.WARNING, - default: AlertIcons.INFO, -}; - -type Props = ToastOptions & { - message: string; - closeToast?: () => void; - action?: { text: string; dispatchableAction: ReduxAction }; -}; - -const ToastComponent = (props: Props) => { - const dispatch = useDispatch(); - const alertType = props.type || ToastType.INFO; - const Icon = ToastIcon[alertType]; - return ( - - - {props.message} - {props.action && ( - { - dispatch(props.action?.dispatchableAction); - props.closeToast && props.closeToast(); - }} - > - {props.action.text} - - )} - - ); -}; - -const Toaster = { - show: (config: Props) => { - if (typeof config.message !== "string") { - console.error("Toast message needs to be a string"); - return; - } - if (config.type && !ToastTypeOptions.includes(config.type)) { - console.error( - "Toast type needs to be a one of " + ToastTypeOptions.join(", "), - ); - return; - } - toast( - , - { - pauseOnHover: false, - pauseOnFocusLoss: false, - autoClose: config.autoClose || 4000, - hideProgressBar: - config.hideProgressBar === undefined ? true : config.hideProgressBar, - }, - ); - }, - clear: () => toast.dismiss(), -}; - -export const AppToaster = Toaster; diff --git a/app/client/src/components/stories/Button.stories.tsx b/app/client/src/components/stories/Button.stories.tsx index 4f6a7f7931f..9cf2e82c1cd 100644 --- a/app/client/src/components/stories/Button.stories.tsx +++ b/app/client/src/components/stories/Button.stories.tsx @@ -1,8 +1,8 @@ import React from "react"; -import Button, { Size, Category, Variant } from "components/ads/Button"; +import Button, { Size, Category } from "components/ads/Button"; import { withKnobs, select, boolean, text } from "@storybook/addon-knobs"; import { withDesign } from "storybook-addon-designs"; -import { StoryWrapper } from "components/ads/common"; +import { StoryWrapper, Variant } from "components/ads/common"; import { IconCollection, IconName } from "components/ads/Icon"; export default { diff --git a/app/client/src/components/stories/Callout.stories.tsx b/app/client/src/components/stories/Callout.stories.tsx index 38c8cf36bf6..8da26a91393 100644 --- a/app/client/src/components/stories/Callout.stories.tsx +++ b/app/client/src/components/stories/Callout.stories.tsx @@ -1,8 +1,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"; +import { StoryWrapper, Variant } from "components/ads/common"; export default { title: "Callout", diff --git a/app/client/src/components/stories/Icon.stories.tsx b/app/client/src/components/stories/Icon.stories.tsx index 526bbf9cb12..9341b21c9f4 100644 --- a/app/client/src/components/stories/Icon.stories.tsx +++ b/app/client/src/components/stories/Icon.stories.tsx @@ -1,10 +1,10 @@ import React from "react"; import Icon, { IconSize, IconCollection } from "components/ads/Icon"; -import Button, { Size, Category, Variant } from "components/ads/Button"; +import Button, { Size, Category } from "components/ads/Button"; import { withKnobs, select, boolean } from "@storybook/addon-knobs"; import { withDesign } from "storybook-addon-designs"; import AppIcon, { AppIconCollection } from "components/ads/AppIcon"; -import { StoryWrapper } from "components/ads/common"; +import { StoryWrapper, Variant } from "components/ads/common"; export default { title: "Icon", diff --git a/app/client/src/components/stories/Table.stories.tsx b/app/client/src/components/stories/Table.stories.tsx index 7b21131027b..e89c46d2031 100644 --- a/app/client/src/components/stories/Table.stories.tsx +++ b/app/client/src/components/stories/Table.stories.tsx @@ -1,10 +1,10 @@ import React from "react"; import Table from "components/ads/Table"; -import Button, { Category, Variant, Size } from "components/ads/Button"; +import Button, { Category, Size } from "components/ads/Button"; import Icon, { IconSize } from "components/ads/Icon"; import TableDropdown from "components/ads/TableDropdown"; import { Position } from "@blueprintjs/core/lib/esm/common/position"; -import { StoryWrapper } from "components/ads/common"; +import { StoryWrapper, Variant } from "components/ads/common"; export default { title: "Table", diff --git a/app/client/src/components/stories/Toast.stories.tsx b/app/client/src/components/stories/Toast.stories.tsx new file mode 100644 index 00000000000..55ee91dc694 --- /dev/null +++ b/app/client/src/components/stories/Toast.stories.tsx @@ -0,0 +1,51 @@ +import React, { useEffect } from "react"; +import { withKnobs, text, number, select } from "@storybook/addon-knobs"; +import { Toaster, StyledToastContainer } from "components/ads/Toast"; +import Button, { Size, Category } from "components/ads/Button"; +import { action } from "@storybook/addon-actions"; +import { Slide } from "react-toastify"; +import { StoryWrapper, Variant } from "components/ads/common"; + +export default { + title: "Toast", + component: Toaster, + decorators: [withKnobs], +}; + +export const ToastStory = () => { + useEffect(() => { + Toaster.show({ + text: text("message", "Archived successfully"), + duration: number("duration", 5000), + variant: select("variant", Object.values(Variant), Variant.info), + onUndo: action("on-undo"), + }); + }, []); + + return ( + + +