diff --git a/app/client/src/assets/icons/ads/edit.svg b/app/client/src/assets/icons/ads/edit.svg
new file mode 100644
index 0000000000..93eaaa6946
--- /dev/null
+++ b/app/client/src/assets/icons/ads/edit.svg
@@ -0,0 +1,3 @@
+
diff --git a/app/client/src/assets/icons/ads/error.svg b/app/client/src/assets/icons/ads/error.svg
new file mode 100644
index 0000000000..43f866b350
--- /dev/null
+++ b/app/client/src/assets/icons/ads/error.svg
@@ -0,0 +1,4 @@
+
diff --git a/app/client/src/assets/icons/ads/success.svg b/app/client/src/assets/icons/ads/success.svg
new file mode 100644
index 0000000000..c5c3d40e41
--- /dev/null
+++ b/app/client/src/assets/icons/ads/success.svg
@@ -0,0 +1,4 @@
+
diff --git a/app/client/src/components/ads/EditableInput.tsx b/app/client/src/components/ads/EditableInput.tsx
deleted file mode 100644
index 816ad2d1b3..0000000000
--- a/app/client/src/components/ads/EditableInput.tsx
+++ /dev/null
@@ -1,26 +0,0 @@
-import { CommonComponentProps } from "./common";
-
-export enum EditInteractionKind {
- SINGLE,
- DOUBLE,
-}
-
-type EditableTextProps = CommonComponentProps & {
- type: "text" | "password" | "email" | "phone" | "date";
- defaultValue: string;
- onTextChanged: (value: string) => void;
- placeholder: string;
- cypressSelector?: string;
- valueTransform?: (value: string) => string;
- isEditingDefault?: boolean;
- forceDefault?: boolean;
- updating?: boolean;
- isInvalid?: (value: string) => string | boolean;
- editInteractionKind: EditInteractionKind;
- hideEditIcon?: boolean;
-};
-
-// Check EditableText Component
-export default function(props: EditableTextProps) {
- return null;
-}
diff --git a/app/client/src/components/ads/EditableText.tsx b/app/client/src/components/ads/EditableText.tsx
new file mode 100644
index 0000000000..1a4aa7f9a6
--- /dev/null
+++ b/app/client/src/components/ads/EditableText.tsx
@@ -0,0 +1,292 @@
+import React, { useState, useEffect, useMemo, useCallback } from "react";
+import { EditableText as BlueprintEditableText } from "@blueprintjs/core";
+import styled from "styled-components";
+import { Size } from "./Button";
+import Text, { TextType } from "./Text";
+import Spinner from "./Spinner";
+import { hexToRgba } from "./common";
+import { theme } from "constants/DefaultTheme";
+import { noop } from "lodash";
+import Icon from "./Icon";
+
+export enum EditInteractionKind {
+ SINGLE = "SINGLE",
+ DOUBLE = "DOUBLE",
+}
+
+export type SavingStateHandler = (
+ isSaving: boolean,
+ state?: SavingState,
+) => void;
+
+export enum SavingState {
+ NOT_STARTED = "NOT_STARTED",
+ SUCCESS = "SUCCESS",
+ ERROR = "ERROR",
+}
+
+type EditableTextProps = {
+ defaultValue: string;
+ onTextChanged: (value: string) => void;
+ placeholder: string;
+ className?: string;
+ valueTransform?: (value: string) => string;
+ isEditingDefault?: boolean;
+ forceDefault?: boolean;
+ updating?: boolean;
+ isInvalid?: (value: string) => string | boolean;
+ editInteractionKind: EditInteractionKind;
+ hideEditIcon?: boolean;
+ fill?: boolean;
+ onSubmit: (
+ value: string,
+ callback: SavingStateHandler,
+ ) => { saving: SavingState };
+};
+
+const EditableTextWrapper = styled.div<{
+ fill?: boolean;
+}>`
+ width: ${props => (!props.fill ? "234px" : "100%")};
+ .error-message {
+ color: ${props => props.theme.colors.danger.main};
+ }
+`;
+
+const editModeBgcolor = (
+ isInvalid: boolean,
+ isEditing: boolean,
+ savingState: { isSaving: boolean; name?: SavingState },
+): string => {
+ if (
+ (isInvalid && isEditing) ||
+ (!savingState.isSaving && savingState.name === SavingState.ERROR)
+ ) {
+ return hexToRgba(theme.colors.danger.main, 0.08);
+ } else if (!isInvalid && isEditing) {
+ return theme.colors.blackShades[2];
+ } else {
+ return "transparent";
+ }
+};
+
+const TextContainer = styled.div<{
+ isInvalid: boolean;
+ isEditing: boolean;
+ bgColor: string;
+}>`
+ display: flex;
+ align-items: center;
+ ${props =>
+ props.isEditing && props.isInvalid
+ ? `margin-bottom: ${props.theme.spaces[2]}px`
+ : null};
+ .bp3-editable-text.bp3-editable-text-editing::before,
+ .bp3-editable-text.bp3-disabled::before {
+ display: none;
+ }
+
+ &&& .bp3-editable-text-content,
+ &&& .bp3-editable-text-input {
+ font-size: ${props => props.theme.typography.p1.fontSize}px;
+ line-height: ${props => props.theme.typography.p1.lineHeight}px;
+ letter-spacing: ${props => props.theme.typography.p1.letterSpacing}px;
+ font-weight: ${props => props.theme.typography.p1.fontWeight}px;
+ }
+
+ & .bp3-editable-text-content {
+ cursor: pointer;
+ color: ${props => props.theme.colors.blackShades[9]};
+ overflow: hidden;
+ text-overflow: ellipsis;
+ ${props => (props.isEditing ? "display: none" : "display: block")};
+ }
+
+ & .bp3-editable-text-input {
+ border: none;
+ outline: none;
+ height: ${props => props.theme.spaces[13] + 3}px;
+ padding: ${props => props.theme.spaces[0]}px;
+ color: ${props => props.theme.colors.blackShades[9]};
+ min-width: 100%;
+ border-radius: ${props => props.theme.spaces[0]}px;
+ }
+
+ & .bp3-editable-text {
+ overflow: hidden;
+ padding: ${props => props.theme.spaces[4]}px
+ ${props => props.theme.spaces[5]}px;
+ width: calc(100% - 40px);
+ background-color: ${props => props.bgColor};
+ }
+
+ .icon-wrapper {
+ background-color: ${props => props.bgColor};
+ }
+`;
+
+const IconWrapper = styled.div`
+ width: ${props => props.theme.spaces[13] + 4}px;
+ padding-right: ${props => props.theme.spaces[5]}px;
+ height: ${props => props.theme.spaces[13] + 3}px;
+ display: flex;
+ align-items: center;
+ justify-content: flex-end;
+`;
+
+export const AdsEditableText = (props: EditableTextProps) => {
+ const [isEditing, setIsEditing] = useState(!!props.isEditingDefault);
+ const [value, setValue] = useState(props.defaultValue);
+ const [lastValidValue, setLastValidValue] = useState(props.defaultValue);
+ const [isInvalid, setIsInvalid] = useState(false);
+ const [changeStarted, setChangeStarted] = useState(false);
+ const [savingState, setSavingState] = useState<{
+ isSaving: boolean;
+ name?: SavingState;
+ }>({ isSaving: false, name: SavingState.NOT_STARTED });
+
+ useEffect(() => {
+ setValue(props.defaultValue);
+ setIsEditing(!!props.isEditingDefault);
+ }, [props.defaultValue, props.isEditingDefault]);
+
+ useEffect(() => {
+ if (props.forceDefault === true) setValue(props.defaultValue);
+ }, [props.forceDefault, props.defaultValue]);
+
+ const bgColor = useMemo(
+ () => editModeBgcolor(!!isInvalid, isEditing, savingState),
+ [isInvalid, isEditing, savingState],
+ );
+
+ /* should I write ? */
+ const editMode = useCallback((e: React.MouseEvent) => {
+ setIsEditing(true);
+ const errorMessage = props.isInvalid && props.isInvalid(props.defaultValue);
+ setIsInvalid(errorMessage ? errorMessage : false);
+ e.preventDefault();
+ e.stopPropagation();
+ }, []);
+
+ const onConfirm = (_value: string) => {
+ if (
+ (!savingState.isSaving && savingState.name === SavingState.ERROR) ||
+ isInvalid
+ ) {
+ setValue(lastValidValue);
+ setSavingState({ isSaving: false, name: SavingState.NOT_STARTED });
+ } else if (changeStarted) {
+ props.onTextChanged(_value);
+ props.onSubmit(_value, SavingStateHandler);
+ }
+ setIsEditing(false);
+ setChangeStarted(false);
+ };
+
+ const onInputchange = useCallback((_value: string) => {
+ let finalVal: string = _value;
+ if (props.valueTransform) {
+ finalVal = props.valueTransform(_value);
+ }
+ setValue(finalVal);
+
+ const errorMessage = props.isInvalid && props.isInvalid(finalVal);
+ const error = errorMessage ? errorMessage : false;
+ if (!error) {
+ setLastValidValue(finalVal);
+ }
+ setIsInvalid(error);
+ setChangeStarted(true);
+ }, []);
+
+ const SavingStateHandler = (isSaving: boolean, state?: SavingState) => {
+ setIsEditing(false);
+ if (isSaving) {
+ setSavingState({ isSaving: true });
+ } else {
+ switch (state) {
+ case SavingState.SUCCESS:
+ setSavingState({ isSaving: false, name: SavingState.SUCCESS });
+ break;
+ default:
+ setValue(props.defaultValue);
+ setSavingState({ isSaving: false, name: SavingState.NOT_STARTED });
+ break;
+ }
+ }
+ };
+
+ const iconName =
+ !isEditing && savingState.name === SavingState.NOT_STARTED
+ ? "edit"
+ : !isEditing && savingState.name === SavingState.SUCCESS
+ ? "success"
+ : (isEditing && savingState.name === SavingState.ERROR) ||
+ (isEditing && !!isInvalid)
+ ? "error"
+ : undefined;
+
+ const nonEditMode = () => {
+ if (
+ !isEditing &&
+ !savingState.isSaving &&
+ savingState.name === SavingState.SUCCESS
+ ) {
+ setSavingState({ isSaving: false, name: SavingState.NOT_STARTED });
+ }
+ };
+
+ return (
+
+
+
+
+
+ {savingState.isSaving ? (
+
+ ) : (
+
+ )}
+
+
+ {isEditing && !!isInvalid ? (
+
+ {isInvalid}
+
+ ) : null}
+
+ );
+};
+
+AdsEditableText.defaultProps = {
+ fill: false,
+};
+
+export default AdsEditableText;
diff --git a/app/client/src/components/ads/Icon.tsx b/app/client/src/components/ads/Icon.tsx
index c6a3a078fb..3f547a05d9 100644
--- a/app/client/src/components/ads/Icon.tsx
+++ b/app/client/src/components/ads/Icon.tsx
@@ -3,6 +3,9 @@ import { ReactComponent as DeleteIcon } from "assets/icons/ads/delete.svg";
import { ReactComponent as UserIcon } from "assets/icons/ads/user.svg";
import { ReactComponent as GeneralIcon } from "assets/icons/ads/general.svg";
import { ReactComponent as BillingIcon } from "assets/icons/ads/billing.svg";
+import { ReactComponent as EditIcon } from "assets/icons/ads/edit.svg";
+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 styled from "styled-components";
@@ -15,6 +18,9 @@ export type IconName =
| "user"
| "general"
| "billing"
+ | "edit"
+ | "error"
+ | "success"
| "search"
| "close"
| undefined;
@@ -25,8 +31,10 @@ const IconWrapper = styled.div`
}
display: flex;
svg {
- width: ${props => sizeHandler(props)}px;
- height: ${props => sizeHandler(props)}px;
+ width: ${props =>
+ props.size ? sizeHandler(props) : props.theme.spaces[9]}px;
+ height: ${props =>
+ props.size ? sizeHandler(props) : props.theme.spaces[9]}px;
path {
fill: ${props => props.theme.colors.blackShades[4]};
}
@@ -71,6 +79,15 @@ const Icon = (props: IconProps) => {
case "billing":
returnIcon = ;
break;
+ case "edit":
+ returnIcon = ;
+ break;
+ case "error":
+ returnIcon = ;
+ break;
+ case "success":
+ returnIcon = ;
+ break;
case "search":
returnIcon = ;
break;
diff --git a/app/client/src/components/stories/ColorSelector.stories.tsx b/app/client/src/components/stories/ColorSelector.stories.tsx
index 8303226525..9ca90b491c 100644
--- a/app/client/src/components/stories/ColorSelector.stories.tsx
+++ b/app/client/src/components/stories/ColorSelector.stories.tsx
@@ -1,6 +1,6 @@
import React from "react";
import { action } from "@storybook/addon-actions";
-import ColorSelector, { appColorPalette } from "../ads/ColorSelector";
+import ColorSelector, { appColorPalette } from "components/ads/ColorSelector";
import { withKnobs, array, boolean } from "@storybook/addon-knobs";
import { withDesign } from "storybook-addon-designs";
diff --git a/app/client/src/components/stories/EditableText.stories.tsx b/app/client/src/components/stories/EditableText.stories.tsx
new file mode 100644
index 0000000000..de5a12f3b3
--- /dev/null
+++ b/app/client/src/components/stories/EditableText.stories.tsx
@@ -0,0 +1,60 @@
+import React from "react";
+import { boolean, select, text, withKnobs } from "@storybook/addon-knobs";
+import { withDesign } from "storybook-addon-designs";
+import AdsEditableText, {
+ EditInteractionKind,
+ SavingStateHandler,
+ SavingState,
+} from "../ads/EditableText";
+import { action } from "@storybook/addon-actions";
+
+export default {
+ title: "EditableText",
+ component: AdsEditableText,
+ decorators: [withKnobs, withDesign],
+};
+
+const calls = (value: string, callback: any) => {
+ console.log("value", value);
+
+ // setTimeout(() => {
+ // return callback(SavingState.ERROR);
+ // }, 2000);
+
+ setTimeout(() => {
+ return callback(false, SavingState.SUCCESS);
+ }, 2000);
+
+ return callback(true);
+};
+
+const errorFunction = (name: string) => {
+ if (name === "") {
+ return "Name cannot be empty";
+ } else {
+ return false;
+ }
+};
+
+export const EditableTextStory = () => (
+
+
value.toUpperCase()}
+ placeholder={text("placeholder", "Edit input")}
+ hideEditIcon={boolean("hideEditIcon", false)}
+ isInvalid={name => errorFunction(name)}
+ isEditingDefault={boolean("isEditingDefault", false)}
+ fill={boolean("fill", false)}
+ onSubmit={(value: string, callback: SavingStateHandler) =>
+ calls(value, callback)
+ }
+ >
+
+);