Task/5463 Navigation Bar menu Dropdown (#5575)
* Feature/ Navigation Bar menu * Cypress test fixes and minor style changes * Changed some more integration test files and fixed some styling post feedback * Updated failing tests * Minor Cypress test change * requested design changes and Analytics * resolving conflicts * minor style fix * current deploy verbage change Co-authored-by: Rishabh Saxena <rishabh.robben@gmail.com> * addressing review comments * Updating discord link with permanent invite Co-authored-by: Rishabh Saxena <rishabh.robben@gmail.com> Co-authored-by: Rahul R <rahulramesha@Rahuls-MacBook-Pro.local>
This commit is contained in:
parent
0bd58971e3
commit
3042adbdea
|
|
@ -13,6 +13,7 @@ describe("Update Application", function() {
|
|||
it("Open the application menu and update name and then check whether update is reflected in the application card", function() {
|
||||
cy.get(commonlocators.homeIcon).click({ force: true });
|
||||
appname = localStorage.getItem("AppName");
|
||||
cy.get(homePage.searchInput).clear();
|
||||
cy.get(homePage.searchInput).type(appname);
|
||||
// eslint-disable-next-line cypress/no-unnecessary-waiting
|
||||
cy.wait(2000);
|
||||
|
|
@ -53,6 +54,7 @@ describe("Update Application", function() {
|
|||
|
||||
it("Check for errors in updating application name", function() {
|
||||
cy.get(commonlocators.homeIcon).click({ force: true });
|
||||
cy.get(homePage.searchInput).clear();
|
||||
cy.get(homePage.searchInput).type(appname);
|
||||
// eslint-disable-next-line cypress/no-unnecessary-waiting
|
||||
cy.wait(2000);
|
||||
|
|
@ -65,11 +67,15 @@ describe("Update Application", function() {
|
|||
cy.get("#loading").should("not.exist");
|
||||
// eslint-disable-next-line cypress/no-unnecessary-waiting
|
||||
cy.wait(2000);
|
||||
|
||||
cy.AppSetupForRename();
|
||||
cy.get(homePage.applicationName).type(" ");
|
||||
cy.get(homePage.toastMessage).should(
|
||||
"contain",
|
||||
"Application name can't be empty",
|
||||
);
|
||||
|
||||
cy.AppSetupForRename();
|
||||
cy.get(homePage.applicationName).type(" " + "{enter}");
|
||||
cy.wait("@updateApplication").should(
|
||||
"have.nested.property",
|
||||
|
|
|
|||
|
|
@ -52,6 +52,8 @@
|
|||
"enablePublicAccess": ".slider",
|
||||
"closeBtn": ".bp3-dialog-close-button",
|
||||
"applicationName": ".t--application-name",
|
||||
"editingAppName": "bp3-editable-text-editing",
|
||||
"portalMenuItem": ".bp3-portal .bp3-menu-item",
|
||||
"profileMenu": ".bp3-popover-wrapper.profile-menu",
|
||||
"signOutIcon": ".t--logout-icon",
|
||||
"headerAppSmithLogo": ".t--Appsmith-logo-image",
|
||||
|
|
|
|||
|
|
@ -241,6 +241,17 @@ Cypress.Commands.add("launchApp", (appName) => {
|
|||
);
|
||||
});
|
||||
|
||||
Cypress.Commands.add("AppSetupForRename", () => {
|
||||
cy.get(homePage.applicationName).then(($appName) => {
|
||||
if (!$appName.hasClass(homePage.editingAppName)) {
|
||||
cy.get(homePage.applicationName).click();
|
||||
cy.get(homePage.portalMenuItem)
|
||||
.contains("Rename", { matchCase: false })
|
||||
.click();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Cypress.Commands.add("CreateAppForOrg", (orgName, appname) => {
|
||||
cy.get(homePage.orgList.concat(orgName).concat(homePage.createAppFrOrg))
|
||||
.scrollIntoView()
|
||||
|
|
@ -251,8 +262,11 @@ Cypress.Commands.add("CreateAppForOrg", (orgName, appname) => {
|
|||
"response.body.responseMeta.status",
|
||||
201,
|
||||
);
|
||||
cy.get("#loading").should("not.exist");
|
||||
// eslint-disable-next-line cypress/no-unnecessary-waiting
|
||||
cy.wait(1000);
|
||||
cy.wait(2000);
|
||||
|
||||
cy.AppSetupForRename();
|
||||
cy.get(homePage.applicationName).type(appname + "{enter}");
|
||||
cy.wait("@updateApplication").should(
|
||||
"have.nested.property",
|
||||
|
|
@ -272,8 +286,9 @@ Cypress.Commands.add("CreateAppInFirstListedOrg", (appname) => {
|
|||
);
|
||||
cy.get("#loading").should("not.exist");
|
||||
// eslint-disable-next-line cypress/no-unnecessary-waiting
|
||||
cy.wait(1000);
|
||||
cy.wait(2000);
|
||||
|
||||
cy.AppSetupForRename();
|
||||
cy.get(homePage.applicationName).type(appname + "{enter}");
|
||||
cy.wait("@updateApplication").should(
|
||||
"have.nested.property",
|
||||
|
|
|
|||
|
|
@ -1,32 +1,15 @@
|
|||
import React, {
|
||||
useState,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useCallback,
|
||||
useContext,
|
||||
} from "react";
|
||||
import {
|
||||
EditableText as BlueprintEditableText,
|
||||
Classes as BlueprintClasses,
|
||||
} from "@blueprintjs/core";
|
||||
import styled, { ThemeContext } from "styled-components";
|
||||
import Text, { TextType } from "./Text";
|
||||
import Spinner from "./Spinner";
|
||||
import { CommonComponentProps } from "./common";
|
||||
import React, { useState, useCallback } from "react";
|
||||
|
||||
import styled from "styled-components";
|
||||
import { noop } from "lodash";
|
||||
import Icon, { IconSize } from "./Icon";
|
||||
|
||||
export enum EditInteractionKind {
|
||||
SINGLE = "SINGLE",
|
||||
DOUBLE = "DOUBLE",
|
||||
}
|
||||
import { CommonComponentProps } from "./common";
|
||||
import EditableTextSubComponent, {
|
||||
EditInteractionKind,
|
||||
SavingState,
|
||||
} from "./EditableTextSubComponent";
|
||||
|
||||
export enum SavingState {
|
||||
STARTED = "STARTED",
|
||||
NOT_STARTED = "NOT_STARTED",
|
||||
SUCCESS = "SUCCESS",
|
||||
ERROR = "ERROR",
|
||||
}
|
||||
export { EditInteractionKind, SavingState };
|
||||
|
||||
export type EditableTextProps = CommonComponentProps & {
|
||||
defaultValue: string;
|
||||
|
|
@ -64,145 +47,20 @@ export const EditableTextWrapper = styled.div<{
|
|||
}
|
||||
`;
|
||||
|
||||
const editModeBgcolor = (
|
||||
isInvalid: boolean,
|
||||
isEditing: boolean,
|
||||
savingState: SavingState,
|
||||
theme: any,
|
||||
): string => {
|
||||
if ((isInvalid && isEditing) || savingState === SavingState.ERROR) {
|
||||
return theme.colors.editableText.dangerBg;
|
||||
} else if (!isInvalid && isEditing) {
|
||||
return theme.colors.editableText.bg;
|
||||
} else {
|
||||
return "transparent";
|
||||
}
|
||||
};
|
||||
|
||||
const TextContainer = styled.div<{
|
||||
isInvalid: boolean;
|
||||
isEditing: boolean;
|
||||
bgColor: string;
|
||||
underline?: boolean;
|
||||
}>`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
.bp3-editable-text.bp3-editable-text-editing::before,
|
||||
.bp3-editable-text.bp3-disabled::before {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&&&
|
||||
.${BlueprintClasses.EDITABLE_TEXT_CONTENT},
|
||||
&&&
|
||||
.${BlueprintClasses.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};
|
||||
}
|
||||
|
||||
&&& .${BlueprintClasses.EDITABLE_TEXT_CONTENT} {
|
||||
cursor: pointer;
|
||||
color: ${(props) => props.theme.colors.editableText.color};
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
${(props) => (props.isEditing ? "display: none" : "display: block")};
|
||||
width: fit-content !important;
|
||||
min-width: auto !important;
|
||||
}
|
||||
|
||||
&&& .${BlueprintClasses.EDITABLE_TEXT_CONTENT}:hover {
|
||||
${(props) =>
|
||||
props.underline && !props.isEditing
|
||||
? `
|
||||
border-bottom-style: solid;
|
||||
border-bottom-width: 1px;
|
||||
width: fit-content;
|
||||
`
|
||||
: null}
|
||||
}
|
||||
&&& .${BlueprintClasses.EDITABLE_TEXT_INPUT} {
|
||||
border: none;
|
||||
outline: none;
|
||||
height: ${(props) => props.theme.spaces[14] + 1}px;
|
||||
color: ${(props) => props.theme.colors.editableText.color};
|
||||
min-width: 100%;
|
||||
border-radius: ${(props) => props.theme.spaces[0]}px;
|
||||
}
|
||||
|
||||
&&& .${BlueprintClasses.EDITABLE_TEXT} {
|
||||
overflow: hidden;
|
||||
height: ${(props) => props.theme.spaces[14] + 1}px;
|
||||
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[15]}px;
|
||||
padding-right: ${(props) => props.theme.spaces[5]}px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
`;
|
||||
|
||||
export function EditableText(props: EditableTextProps) {
|
||||
const {
|
||||
defaultValue,
|
||||
isEditingDefault,
|
||||
isError,
|
||||
isInvalid: inputValidation,
|
||||
onBlur,
|
||||
onTextChanged,
|
||||
valueTransform,
|
||||
savingState: defaultSavingState,
|
||||
...others
|
||||
} = props;
|
||||
const [isEditing, setIsEditing] = useState(!!isEditingDefault);
|
||||
const [value, setValue] = useState(defaultValue);
|
||||
const [lastValidValue, setLastValidValue] = useState(defaultValue);
|
||||
const [isInvalid, setIsInvalid] = useState<string | boolean>(false);
|
||||
const [changeStarted, setChangeStarted] = useState<boolean>(false);
|
||||
const [savingState, setSavingState] = useState<SavingState>(
|
||||
SavingState.NOT_STARTED,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (isError) {
|
||||
// if there is any error occurs while saving appname.
|
||||
// last saved app name will be shown to user.
|
||||
setValue(defaultValue);
|
||||
}
|
||||
}, [isError]);
|
||||
|
||||
useEffect(() => {
|
||||
setSavingState(props.savingState);
|
||||
}, [props.savingState]);
|
||||
|
||||
useEffect(() => {
|
||||
setValue(defaultValue);
|
||||
}, [defaultValue]);
|
||||
|
||||
useEffect(() => {
|
||||
setIsEditing(!!isEditingDefault);
|
||||
}, [defaultValue, isEditingDefault]);
|
||||
|
||||
useEffect(() => {
|
||||
if (props.forceDefault === true) setValue(defaultValue);
|
||||
}, [props.forceDefault, defaultValue]);
|
||||
|
||||
const theme = useContext(ThemeContext);
|
||||
|
||||
const bgColor = useMemo(
|
||||
() => editModeBgcolor(!!isInvalid, isEditing, savingState, theme),
|
||||
[isInvalid, isEditing, savingState, theme],
|
||||
);
|
||||
|
||||
const editMode = useCallback(
|
||||
(e: React.MouseEvent) => {
|
||||
setIsEditing(true);
|
||||
|
|
@ -214,61 +72,6 @@ export function EditableText(props: EditableTextProps) {
|
|||
[inputValidation, defaultValue],
|
||||
);
|
||||
|
||||
const onConfirm = useCallback(
|
||||
(_value: string) => {
|
||||
const finalVal: string = _value.trim();
|
||||
if (savingState === SavingState.ERROR || isInvalid || finalVal === "") {
|
||||
setValue(lastValidValue);
|
||||
onBlur && onBlur(lastValidValue);
|
||||
setSavingState(SavingState.NOT_STARTED);
|
||||
}
|
||||
if (changeStarted) {
|
||||
onTextChanged && onTextChanged(finalVal);
|
||||
}
|
||||
if (finalVal && finalVal !== defaultValue) {
|
||||
onBlur && onBlur(finalVal);
|
||||
}
|
||||
setIsEditing(false);
|
||||
setChangeStarted(false);
|
||||
},
|
||||
[
|
||||
changeStarted,
|
||||
savingState,
|
||||
isInvalid,
|
||||
lastValidValue,
|
||||
onBlur,
|
||||
onTextChanged,
|
||||
],
|
||||
);
|
||||
|
||||
const onInputchange = useCallback(
|
||||
(_value: string) => {
|
||||
let finalVal: string = _value.indexOf(" ") === 0 ? _value.trim() : _value;
|
||||
if (valueTransform) {
|
||||
finalVal = valueTransform(finalVal);
|
||||
}
|
||||
const errorMessage = inputValidation && inputValidation(finalVal);
|
||||
const error = errorMessage ? errorMessage : false;
|
||||
if (!error && finalVal !== "") {
|
||||
setLastValidValue(finalVal);
|
||||
onTextChanged && onTextChanged(finalVal);
|
||||
}
|
||||
setValue(finalVal);
|
||||
setIsInvalid(error);
|
||||
setChangeStarted(true);
|
||||
},
|
||||
[inputValidation, onTextChanged],
|
||||
);
|
||||
|
||||
const iconName =
|
||||
!isEditing && savingState === SavingState.NOT_STARTED && !props.hideEditIcon
|
||||
? "edit"
|
||||
: !isEditing && savingState === SavingState.SUCCESS
|
||||
? "success"
|
||||
: savingState === SavingState.ERROR || (isEditing && !!isInvalid)
|
||||
? "error"
|
||||
: undefined;
|
||||
|
||||
const nonEditMode = () => {
|
||||
if (!isEditing && savingState === SavingState.SUCCESS) {
|
||||
setSavingState(SavingState.NOT_STARTED);
|
||||
|
|
@ -290,41 +93,19 @@ export function EditableText(props: EditableTextProps) {
|
|||
}
|
||||
onMouseEnter={nonEditMode}
|
||||
>
|
||||
<TextContainer
|
||||
bgColor={bgColor}
|
||||
className="editable-text-container"
|
||||
data-cy={props.cypressSelector}
|
||||
<EditableTextSubComponent
|
||||
defaultSavingState={defaultSavingState}
|
||||
defaultValue={defaultValue}
|
||||
inputValidation={inputValidation}
|
||||
isEditing={isEditing}
|
||||
isInvalid={!!isInvalid}
|
||||
underline={props.underline}
|
||||
>
|
||||
<BlueprintEditableText
|
||||
className={props.className}
|
||||
disabled={!isEditing}
|
||||
isEditing={isEditing}
|
||||
onCancel={onConfirm}
|
||||
onChange={onInputchange}
|
||||
onConfirm={onConfirm}
|
||||
placeholder={props.placeholder || defaultValue}
|
||||
selectAllOnFocus
|
||||
value={value}
|
||||
/>
|
||||
|
||||
{savingState === SavingState.STARTED ? (
|
||||
<IconWrapper className="icon-wrapper">
|
||||
<Spinner size={IconSize.XL} />
|
||||
</IconWrapper>
|
||||
) : value && !props.hideEditIcon ? (
|
||||
<IconWrapper className="icon-wrapper">
|
||||
<Icon name={iconName} size={IconSize.XL} />
|
||||
</IconWrapper>
|
||||
) : null}
|
||||
</TextContainer>
|
||||
{isEditing && !!isInvalid ? (
|
||||
<Text className="error-message" type={TextType.P2}>
|
||||
{isInvalid}
|
||||
</Text>
|
||||
) : null}
|
||||
isEditingDefault={isEditingDefault}
|
||||
isInvalid={isInvalid}
|
||||
savingState={savingState}
|
||||
setIsEditing={setIsEditing}
|
||||
setIsInvalid={setIsInvalid}
|
||||
setSavingState={setSavingState}
|
||||
{...others}
|
||||
/>
|
||||
</EditableTextWrapper>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,311 @@
|
|||
import React, {
|
||||
useState,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useCallback,
|
||||
useContext,
|
||||
} from "react";
|
||||
|
||||
import {
|
||||
EditableText as BlueprintEditableText,
|
||||
Classes as BlueprintClasses,
|
||||
} from "@blueprintjs/core";
|
||||
import styled, { ThemeContext } from "styled-components";
|
||||
import { noop } from "lodash";
|
||||
|
||||
import Text, { TextType } from "./Text";
|
||||
import Spinner from "./Spinner";
|
||||
import { CommonComponentProps } from "./common";
|
||||
import Icon, { IconSize } from "./Icon";
|
||||
|
||||
export enum EditInteractionKind {
|
||||
SINGLE = "SINGLE",
|
||||
DOUBLE = "DOUBLE",
|
||||
}
|
||||
|
||||
export enum SavingState {
|
||||
STARTED = "STARTED",
|
||||
NOT_STARTED = "NOT_STARTED",
|
||||
SUCCESS = "SUCCESS",
|
||||
ERROR = "ERROR",
|
||||
}
|
||||
|
||||
export type EditableTextSubComponentProps = CommonComponentProps & {
|
||||
defaultValue: string;
|
||||
placeholder?: string;
|
||||
editInteractionKind: EditInteractionKind;
|
||||
defaultSavingState: SavingState;
|
||||
savingState: SavingState;
|
||||
setSavingState: typeof noop;
|
||||
onBlur?: (value: string) => void;
|
||||
onTextChanged?: (value: string) => void;
|
||||
valueTransform?: (value: string) => string;
|
||||
isEditingDefault?: boolean;
|
||||
isEditing: boolean;
|
||||
forceDefault?: boolean;
|
||||
updating?: boolean;
|
||||
setIsEditing: typeof noop;
|
||||
inputValidation?: (value: string) => string | boolean;
|
||||
isInvalid: string | boolean;
|
||||
setIsInvalid: typeof noop;
|
||||
hideEditIcon?: boolean;
|
||||
fill?: boolean;
|
||||
underline?: boolean;
|
||||
isError?: boolean;
|
||||
};
|
||||
|
||||
export const EditableTextWrapper = styled.div<{
|
||||
filled: boolean;
|
||||
}>`
|
||||
${(props) =>
|
||||
!props.filled
|
||||
? `
|
||||
width: 243px;
|
||||
`
|
||||
: `
|
||||
width: 100%;
|
||||
flex: 1;
|
||||
`}
|
||||
.error-message {
|
||||
margin-left: ${(props) => props.theme.spaces[5]}px;
|
||||
color: ${(props) => props.theme.colors.danger.main};
|
||||
}
|
||||
`;
|
||||
|
||||
const editModeBgcolor = (
|
||||
isInvalid: boolean,
|
||||
isEditing: boolean,
|
||||
savingState: SavingState,
|
||||
theme: any,
|
||||
): string => {
|
||||
if ((isInvalid && isEditing) || savingState === SavingState.ERROR) {
|
||||
return theme.colors.editableText.dangerBg;
|
||||
} else if (!isInvalid && isEditing) {
|
||||
return theme.colors.editableText.bg;
|
||||
} else {
|
||||
return "transparent";
|
||||
}
|
||||
};
|
||||
|
||||
const TextContainer = styled.div<{
|
||||
isInvalid: boolean;
|
||||
isEditing: boolean;
|
||||
bgColor: string;
|
||||
underline?: boolean;
|
||||
}>`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
.bp3-editable-text.bp3-editable-text-editing::before,
|
||||
.bp3-editable-text.bp3-disabled::before {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&&&
|
||||
.${BlueprintClasses.EDITABLE_TEXT_CONTENT},
|
||||
&&&
|
||||
.${BlueprintClasses.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};
|
||||
}
|
||||
|
||||
&&& .${BlueprintClasses.EDITABLE_TEXT_CONTENT} {
|
||||
cursor: pointer;
|
||||
color: ${(props) => props.theme.colors.editableText.color};
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
${(props) => (props.isEditing ? "display: none" : "display: block")};
|
||||
width: fit-content !important;
|
||||
min-width: auto !important;
|
||||
}
|
||||
|
||||
&&& .${BlueprintClasses.EDITABLE_TEXT_CONTENT}:hover {
|
||||
${(props) =>
|
||||
props.underline && !props.isEditing
|
||||
? `
|
||||
border-bottom-style: solid;
|
||||
border-bottom-width: 1px;
|
||||
width: fit-content;
|
||||
`
|
||||
: null}
|
||||
}
|
||||
&&& .${BlueprintClasses.EDITABLE_TEXT_INPUT} {
|
||||
border: none;
|
||||
outline: none;
|
||||
height: ${(props) => props.theme.spaces[14] + 1}px;
|
||||
color: ${(props) => props.theme.colors.editableText.color};
|
||||
min-width: 100%;
|
||||
border-radius: ${(props) => props.theme.spaces[0]}px;
|
||||
}
|
||||
|
||||
&&& .${BlueprintClasses.EDITABLE_TEXT} {
|
||||
overflow: hidden;
|
||||
height: ${(props) => props.theme.spaces[14] + 1}px;
|
||||
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[15]}px;
|
||||
padding-right: ${(props) => props.theme.spaces[5]}px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
`;
|
||||
|
||||
export function EditableTextSubComponent(props: EditableTextSubComponentProps) {
|
||||
const {
|
||||
defaultValue,
|
||||
inputValidation,
|
||||
isEditing,
|
||||
isEditingDefault,
|
||||
isError,
|
||||
isInvalid,
|
||||
onBlur,
|
||||
onTextChanged,
|
||||
savingState,
|
||||
setIsEditing,
|
||||
setIsInvalid,
|
||||
setSavingState,
|
||||
valueTransform,
|
||||
} = props;
|
||||
const [value, setValue] = useState(defaultValue);
|
||||
const [lastValidValue, setLastValidValue] = useState(defaultValue);
|
||||
const [changeStarted, setChangeStarted] = useState<boolean>(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (isError) {
|
||||
// if there is any error occurs while saving appname.
|
||||
// last saved app name will be shown to user.
|
||||
setValue(defaultValue);
|
||||
}
|
||||
}, [isError]);
|
||||
|
||||
useEffect(() => {
|
||||
setSavingState(props.defaultSavingState);
|
||||
}, [props.defaultSavingState]);
|
||||
|
||||
useEffect(() => {
|
||||
setValue(defaultValue);
|
||||
}, [defaultValue]);
|
||||
|
||||
useEffect(() => {
|
||||
setIsEditing(!!isEditingDefault);
|
||||
}, [defaultValue, isEditingDefault]);
|
||||
|
||||
useEffect(() => {
|
||||
if (props.forceDefault === true) setValue(defaultValue);
|
||||
}, [props.forceDefault, defaultValue]);
|
||||
|
||||
const theme = useContext(ThemeContext);
|
||||
|
||||
const bgColor = useMemo(
|
||||
() => editModeBgcolor(!!isInvalid, isEditing, savingState, theme),
|
||||
[isInvalid, isEditing, savingState, theme],
|
||||
);
|
||||
|
||||
const onConfirm = useCallback(
|
||||
(_value: string) => {
|
||||
const finalVal: string = _value.trim();
|
||||
if (savingState === SavingState.ERROR || isInvalid || finalVal === "") {
|
||||
setValue(lastValidValue);
|
||||
onBlur && onBlur(lastValidValue);
|
||||
setSavingState(SavingState.NOT_STARTED);
|
||||
}
|
||||
if (changeStarted) {
|
||||
onTextChanged && onTextChanged(finalVal);
|
||||
}
|
||||
if (finalVal && finalVal !== defaultValue) {
|
||||
onBlur && onBlur(finalVal);
|
||||
}
|
||||
setIsEditing(false);
|
||||
setChangeStarted(false);
|
||||
},
|
||||
[
|
||||
changeStarted,
|
||||
savingState,
|
||||
isInvalid,
|
||||
lastValidValue,
|
||||
onBlur,
|
||||
onTextChanged,
|
||||
],
|
||||
);
|
||||
|
||||
const onInputchange = useCallback(
|
||||
(_value: string) => {
|
||||
let finalVal: string = _value.indexOf(" ") === 0 ? _value.trim() : _value;
|
||||
if (valueTransform) {
|
||||
finalVal = valueTransform(finalVal);
|
||||
}
|
||||
const errorMessage = inputValidation && inputValidation(finalVal);
|
||||
const error = errorMessage ? errorMessage : false;
|
||||
if (!error && finalVal !== "") {
|
||||
setLastValidValue(finalVal);
|
||||
onTextChanged && onTextChanged(finalVal);
|
||||
}
|
||||
setValue(finalVal);
|
||||
setIsInvalid(error);
|
||||
setChangeStarted(true);
|
||||
},
|
||||
[inputValidation, onTextChanged],
|
||||
);
|
||||
|
||||
const iconName =
|
||||
!isEditing && savingState === SavingState.NOT_STARTED && !props.hideEditIcon
|
||||
? "edit"
|
||||
: !isEditing && savingState === SavingState.SUCCESS
|
||||
? "success"
|
||||
: savingState === SavingState.ERROR || (isEditing && !!isInvalid)
|
||||
? "error"
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<>
|
||||
<TextContainer
|
||||
bgColor={bgColor}
|
||||
className="editable-text-container"
|
||||
data-cy={props.cypressSelector}
|
||||
isEditing={isEditing}
|
||||
isInvalid={!!isInvalid}
|
||||
underline={props.underline}
|
||||
>
|
||||
<BlueprintEditableText
|
||||
className={props.className}
|
||||
disabled={!isEditing}
|
||||
isEditing={isEditing}
|
||||
onCancel={onConfirm}
|
||||
onChange={onInputchange}
|
||||
onConfirm={onConfirm}
|
||||
placeholder={props.placeholder || defaultValue}
|
||||
selectAllOnFocus
|
||||
value={value}
|
||||
/>
|
||||
|
||||
{savingState === SavingState.STARTED ? (
|
||||
<IconWrapper className="icon-wrapper">
|
||||
<Spinner size={IconSize.XL} />
|
||||
</IconWrapper>
|
||||
) : value && !props.hideEditIcon ? (
|
||||
<IconWrapper className="icon-wrapper">
|
||||
<Icon name={iconName} size={IconSize.XL} />
|
||||
</IconWrapper>
|
||||
) : null}
|
||||
</TextContainer>
|
||||
{isEditing && !!isInvalid ? (
|
||||
<Text className="error-message" type={TextType.P2}>
|
||||
{isInvalid}
|
||||
</Text>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default EditableTextSubComponent;
|
||||
|
|
@ -67,6 +67,7 @@ import { ReactComponent as Unpin } from "assets/icons/comments/unpin.svg";
|
|||
import { ReactComponent as Reaction } from "assets/icons/comments/reaction.svg";
|
||||
import { ReactComponent as Reaction2 } from "assets/icons/comments/reaction-2.svg";
|
||||
import { ReactComponent as Upload } from "assets/icons/ads/upload.svg";
|
||||
import { ReactComponent as UpArrow } from "assets/icons/ads/upper_arrow.svg";
|
||||
import { ReactComponent as Download } from "assets/icons/ads/download.svg";
|
||||
import { ReactComponent as ArrowForwardIcon } from "assets/icons/control/arrow_forward.svg";
|
||||
import { ReactComponent as CapSolidIcon } from "assets/icons/control/cap_solid.svg";
|
||||
|
|
@ -155,6 +156,7 @@ export const IconCollection = [
|
|||
"warning",
|
||||
"warning-triangle",
|
||||
"downArrow",
|
||||
"upArrow",
|
||||
"context-menu",
|
||||
"duplicate",
|
||||
"logout",
|
||||
|
|
@ -327,6 +329,9 @@ const Icon = forwardRef(
|
|||
case "downArrow":
|
||||
returnIcon = <DownArrow />;
|
||||
break;
|
||||
case "upArrow":
|
||||
returnIcon = <UpArrow />;
|
||||
break;
|
||||
case "share":
|
||||
returnIcon = <ShareIcon />;
|
||||
break;
|
||||
|
|
|
|||
|
|
@ -350,6 +350,7 @@ export type Theme = {
|
|||
homePage: any;
|
||||
sidebarWidth: string;
|
||||
canvasBottomPadding: number;
|
||||
navbarMenuHeight: string;
|
||||
actionsBottomTabInitialHeight: string;
|
||||
sideNav: {
|
||||
minWidth: number;
|
||||
|
|
@ -609,6 +610,14 @@ type ColorType = {
|
|||
bg: string;
|
||||
};
|
||||
};
|
||||
navigationMenu: {
|
||||
contentActive: string;
|
||||
backgroundActive: string;
|
||||
contentInactive: string;
|
||||
backgroundInactive: string;
|
||||
label: string;
|
||||
warning: string;
|
||||
};
|
||||
colorSelector: {
|
||||
shadow: ShadeColor;
|
||||
checkmark: ShadeColor;
|
||||
|
|
@ -1225,6 +1234,15 @@ const mentionsInput = {
|
|||
mentionsInviteBtnPlusIcon: "#6A86CE",
|
||||
};
|
||||
|
||||
const navigationMenu = {
|
||||
contentActive: "#F0F0F0",
|
||||
backgroundActive: "#222222",
|
||||
contentInactive: "#858282",
|
||||
backgroundInactive: "#090707",
|
||||
label: "#A9A7A7",
|
||||
warning: "#F22B2B",
|
||||
};
|
||||
|
||||
export const dark: ColorType = {
|
||||
overlayColor: "#090707cc",
|
||||
notifications,
|
||||
|
|
@ -1236,6 +1254,7 @@ export const dark: ColorType = {
|
|||
helpModal,
|
||||
globalSearch,
|
||||
comments,
|
||||
navigationMenu,
|
||||
selected: darkShades[10],
|
||||
header: {
|
||||
separator: darkShades[4],
|
||||
|
|
@ -1704,6 +1723,7 @@ export const light: ColorType = {
|
|||
modeIconCircleStroke: "#fff",
|
||||
activeModeIconCircleStroke: "#EBEBEB",
|
||||
},
|
||||
navigationMenu,
|
||||
selected: lightShades[12],
|
||||
header: {
|
||||
separator: "#E0DEDE",
|
||||
|
|
@ -2406,6 +2426,7 @@ export const theme: Theme = {
|
|||
integrationsPageUnusableHeight: "182px",
|
||||
backBanner: "30px",
|
||||
canvasBottomPadding: 200,
|
||||
navbarMenuHeight: "35px",
|
||||
sideNav: {
|
||||
maxWidth: 220,
|
||||
minWidth: 50,
|
||||
|
|
|
|||
|
|
@ -54,6 +54,10 @@ div.bp3-popover-arrow {
|
|||
background: rgb(3, 179, 101) !important;
|
||||
}
|
||||
|
||||
.t--editor-appname-menu-portal {
|
||||
z-index: 9 !important;
|
||||
}
|
||||
|
||||
.bp3-popover .bp3-input {
|
||||
outline: 0;
|
||||
box-shadow: none;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,74 @@
|
|||
import React from "react";
|
||||
|
||||
import styled from "styled-components";
|
||||
import { noop } from "lodash";
|
||||
|
||||
import { CommonComponentProps } from "components/ads/common";
|
||||
import EditableTextSubComponent from "components/ads/EditableTextSubComponent";
|
||||
import { EditInteractionKind, SavingState } from "components/ads/EditableText";
|
||||
|
||||
export type EditableAppNameProps = CommonComponentProps & {
|
||||
defaultValue: string;
|
||||
placeholder?: string;
|
||||
editInteractionKind: EditInteractionKind;
|
||||
defaultSavingState: SavingState;
|
||||
onClick?: typeof noop;
|
||||
onBlur?: (value: string) => void;
|
||||
isEditingDefault?: boolean;
|
||||
inputValidation?: (value: string) => string | boolean;
|
||||
hideEditIcon?: boolean;
|
||||
fill?: boolean;
|
||||
isError?: boolean;
|
||||
isEditing: boolean;
|
||||
setIsEditing: typeof noop;
|
||||
isInvalid: string | boolean;
|
||||
setIsInvalid: typeof noop;
|
||||
savingState: SavingState;
|
||||
setSavingState: typeof noop;
|
||||
};
|
||||
|
||||
export const EditableAppNameWrapper = styled.div<{
|
||||
filled: boolean;
|
||||
}>`
|
||||
${(props) =>
|
||||
!props.filled
|
||||
? `
|
||||
width: 243px;
|
||||
`
|
||||
: `
|
||||
width: 100%;
|
||||
flex: 1;
|
||||
`}
|
||||
.error-message {
|
||||
margin-left: ${(props) => props.theme.spaces[5]}px;
|
||||
color: ${(props) => props.theme.colors.danger.main};
|
||||
}
|
||||
`;
|
||||
|
||||
export function EditableAppName(props: EditableAppNameProps) {
|
||||
const { isEditing, onClick, savingState, setSavingState, ...others } = props;
|
||||
|
||||
const nonEditMode = () => {
|
||||
if (!isEditing && savingState === SavingState.SUCCESS) {
|
||||
setSavingState(SavingState.NOT_STARTED);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<EditableAppNameWrapper
|
||||
filled={!!props.fill}
|
||||
onClick={onClick}
|
||||
onMouseEnter={nonEditMode}
|
||||
>
|
||||
<EditableTextSubComponent
|
||||
isEditing={isEditing}
|
||||
savingState={savingState}
|
||||
setSavingState={setSavingState}
|
||||
underline={false}
|
||||
{...others}
|
||||
/>
|
||||
</EditableAppNameWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
export default EditableAppName;
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
import React from "react";
|
||||
|
||||
import { noop } from "lodash";
|
||||
|
||||
import {
|
||||
NavigationMenuItem,
|
||||
MenuTypes,
|
||||
MenuItemData,
|
||||
} from "./NavigationMenuItem";
|
||||
|
||||
type NavigationMenuProps = {
|
||||
menuItems: MenuItemData[] | undefined;
|
||||
setIsPopoverOpen: typeof noop;
|
||||
};
|
||||
|
||||
export function NavigationMenu(props: NavigationMenuProps) {
|
||||
const { menuItems, setIsPopoverOpen } = props;
|
||||
|
||||
return (
|
||||
// eslint-disable-next-line
|
||||
<>
|
||||
{menuItems?.map((item, idx) => {
|
||||
return item.type === MenuTypes.PARENT ? (
|
||||
<NavigationMenuItem
|
||||
key={idx}
|
||||
menuItemData={item}
|
||||
setIsPopoverOpen={setIsPopoverOpen}
|
||||
>
|
||||
<NavigationMenu
|
||||
menuItems={item?.children}
|
||||
setIsPopoverOpen={setIsPopoverOpen}
|
||||
/>
|
||||
</NavigationMenuItem>
|
||||
) : (
|
||||
<NavigationMenuItem
|
||||
key={idx}
|
||||
menuItemData={item}
|
||||
setIsPopoverOpen={setIsPopoverOpen}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,160 @@
|
|||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { useHistory } from "react-router-dom";
|
||||
import { noop } from "lodash";
|
||||
|
||||
import { Variant } from "components/ads/common";
|
||||
import { Toaster } from "components/ads/Toast";
|
||||
import { ThemeProp } from "components/ads/common";
|
||||
import { setCommentModeInUrl } from "pages/Editor/ToggleModeButton";
|
||||
import { toggleShowGlobalSearchModal } from "actions/globalSearchActions";
|
||||
import { areCommentsEnabledForUserAndApp } from "selectors/commentsSelectors";
|
||||
import { ReduxActionTypes } from "constants/ReduxActionConstants";
|
||||
import { APPLICATIONS_URL } from "constants/routes";
|
||||
|
||||
import { MenuItemData, MenuTypes } from "./NavigationMenuItem";
|
||||
import { useCallback } from "react";
|
||||
|
||||
type NavigationMenuDataProps = ThemeProp & {
|
||||
applicationId: string | undefined;
|
||||
editMode: typeof noop;
|
||||
deploy: typeof noop;
|
||||
currentDeployLink: string;
|
||||
};
|
||||
|
||||
export const GetNavigationMenuData = ({
|
||||
applicationId,
|
||||
currentDeployLink,
|
||||
deploy,
|
||||
editMode,
|
||||
theme,
|
||||
}: NavigationMenuDataProps): MenuItemData[] => {
|
||||
const dispatch = useDispatch();
|
||||
const commentsEnabled = useSelector(areCommentsEnabledForUserAndApp);
|
||||
const history = useHistory();
|
||||
|
||||
const isApplicationIdPresent = !!(applicationId && applicationId.length > 0);
|
||||
|
||||
const openExternalLink = useCallback((link: string) => {
|
||||
if (link) {
|
||||
window.open(link, "_blank");
|
||||
}
|
||||
}, []);
|
||||
|
||||
const deleteApplication = () => {
|
||||
if (applicationId && applicationId.length > 0) {
|
||||
dispatch({
|
||||
type: ReduxActionTypes.DELETE_APPLICATION_INIT,
|
||||
payload: {
|
||||
applicationId,
|
||||
},
|
||||
});
|
||||
history.push(APPLICATIONS_URL);
|
||||
} else {
|
||||
Toaster.show({
|
||||
text: "Error while deleting Application",
|
||||
variant: Variant.danger,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return [
|
||||
{
|
||||
text: "Rename",
|
||||
onClick: editMode,
|
||||
type: MenuTypes.MENU,
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
text: "View Modes",
|
||||
type: MenuTypes.PARENT,
|
||||
isVisible: !!commentsEnabled,
|
||||
children: [
|
||||
{
|
||||
text: "Edit Mode",
|
||||
label: "E",
|
||||
onClick: () => setCommentModeInUrl(false),
|
||||
type: MenuTypes.MENU,
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
text: "Comment Mode",
|
||||
label: "C",
|
||||
onClick: () => setCommentModeInUrl(true),
|
||||
type: MenuTypes.MENU,
|
||||
isVisible: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
text: "Deploy",
|
||||
type: MenuTypes.PARENT,
|
||||
isVisible: true,
|
||||
children: [
|
||||
{
|
||||
text: "Deploy",
|
||||
onClick: deploy,
|
||||
type: MenuTypes.MENU,
|
||||
isVisible: true,
|
||||
isOpensNewWindow: true,
|
||||
},
|
||||
{
|
||||
text: "Current Deployed Version",
|
||||
onClick: () => openExternalLink(currentDeployLink),
|
||||
type: MenuTypes.MENU,
|
||||
isVisible: true,
|
||||
isOpensNewWindow: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
text: "Shortcuts",
|
||||
onClick: () => dispatch(toggleShowGlobalSearchModal()),
|
||||
type: MenuTypes.MENU,
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
text: "Help",
|
||||
type: MenuTypes.PARENT,
|
||||
isVisible: true,
|
||||
children: [
|
||||
{
|
||||
text: "Community Forum",
|
||||
onClick: () => openExternalLink("https://community.appsmith.com/"),
|
||||
type: MenuTypes.MENU,
|
||||
isVisible: true,
|
||||
isOpensNewWindow: true,
|
||||
},
|
||||
{
|
||||
text: "Discord Channel",
|
||||
onClick: () => openExternalLink("https://discord.gg/9deFW7q4kB"),
|
||||
type: MenuTypes.MENU,
|
||||
isVisible: true,
|
||||
isOpensNewWindow: true,
|
||||
},
|
||||
{
|
||||
text: "Github",
|
||||
onClick: () =>
|
||||
openExternalLink("https://github.com/appsmithorg/appsmith/"),
|
||||
type: MenuTypes.MENU,
|
||||
isVisible: true,
|
||||
isOpensNewWindow: true,
|
||||
},
|
||||
{
|
||||
text: "Documentation",
|
||||
onClick: () => openExternalLink("https://docs.appsmith.com/"),
|
||||
type: MenuTypes.MENU,
|
||||
isVisible: true,
|
||||
isOpensNewWindow: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
text: "Delete Application",
|
||||
confirmText: "Are you sure?",
|
||||
onClick: deleteApplication,
|
||||
type: MenuTypes.RECONFIRM,
|
||||
isVisible: isApplicationIdPresent,
|
||||
style: { color: theme.colors.navigationMenu.warning },
|
||||
},
|
||||
];
|
||||
};
|
||||
|
|
@ -0,0 +1,140 @@
|
|||
import React, { useState } from "react";
|
||||
|
||||
import styled from "styled-components";
|
||||
import { Classes, MenuItem } from "@blueprintjs/core";
|
||||
import { noop } from "lodash";
|
||||
|
||||
import { CommonComponentProps } from "components/ads/common";
|
||||
import Icon, { IconSize } from "components/ads/Icon";
|
||||
import AnalyticsUtil from "utils/AnalyticsUtil";
|
||||
|
||||
export enum MenuTypes {
|
||||
MENU = "menu",
|
||||
PARENT = "parent",
|
||||
RECONFIRM = "re-confirm",
|
||||
}
|
||||
|
||||
export interface MenuItemData {
|
||||
text: string;
|
||||
label?: string;
|
||||
onClick?: typeof noop;
|
||||
children?: MenuItemData[];
|
||||
type: MenuTypes;
|
||||
isVisible: boolean;
|
||||
confirmText?: string;
|
||||
isOpensNewWindow?: boolean | undefined;
|
||||
style?: React.CSSProperties;
|
||||
}
|
||||
|
||||
const StyledMenuItem = styled(MenuItem)`
|
||||
width: 240px;
|
||||
background: ${(props) =>
|
||||
props.theme.colors.navigationMenu.backgroundInactive};
|
||||
color: ${(props) => props.theme.colors.navigationMenu.contentInactive};
|
||||
border-radius: 0;
|
||||
height: ${(props) => props.theme.navbarMenuHeight};
|
||||
|
||||
&&&:hover {
|
||||
color: ${(props) => props.theme.colors.navigationMenu.contentActive};
|
||||
background: ${(props) =>
|
||||
props.theme.colors.navigationMenu.backgroundActive};
|
||||
background-color: ${(props) =>
|
||||
props.theme.colors.navigationMenu.backgroundActive};
|
||||
}
|
||||
|
||||
> .${Classes.MENU_ITEM_LABEL} {
|
||||
color: ${(props) => props.theme.colors.navigationMenu.label};
|
||||
}
|
||||
`;
|
||||
|
||||
type NavigationMenuItemProps = CommonComponentProps & {
|
||||
menuItemData: MenuItemData;
|
||||
setIsPopoverOpen: typeof noop;
|
||||
children?: React.ReactNode;
|
||||
};
|
||||
|
||||
export function NavigationMenuItem({
|
||||
children,
|
||||
menuItemData,
|
||||
setIsPopoverOpen,
|
||||
}: NavigationMenuItemProps) {
|
||||
const {
|
||||
confirmText,
|
||||
isOpensNewWindow,
|
||||
isVisible,
|
||||
label,
|
||||
onClick,
|
||||
style,
|
||||
text,
|
||||
} = menuItemData;
|
||||
|
||||
const [confirm, setConfirm] = useState({
|
||||
isConfirm: false,
|
||||
text: text,
|
||||
});
|
||||
|
||||
if (!isVisible) return null;
|
||||
|
||||
const labelElement = isOpensNewWindow && (
|
||||
<Icon name="open" size={IconSize.LARGE} />
|
||||
);
|
||||
|
||||
const handleClick = (e: React.SyntheticEvent) => {
|
||||
setIsPopoverOpen(false);
|
||||
if (onClick) onClick(e);
|
||||
AnalyticsUtil.logEvent("APP_MENU_OPTION_CLICK", {
|
||||
option: text,
|
||||
});
|
||||
};
|
||||
|
||||
const handleReconfirmClick = (e: React.SyntheticEvent) => {
|
||||
if (!confirm.isConfirm && confirmText) {
|
||||
setConfirm({
|
||||
isConfirm: true,
|
||||
text: confirmText,
|
||||
});
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
} else if (onClick) {
|
||||
setIsPopoverOpen(false);
|
||||
onClick(e);
|
||||
AnalyticsUtil.logEvent("APP_MENU_OPTION_CLICK", {
|
||||
option: text,
|
||||
});
|
||||
setConfirm({
|
||||
isConfirm: false,
|
||||
text: text,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
switch (menuItemData.type) {
|
||||
case MenuTypes.MENU:
|
||||
return (
|
||||
<StyledMenuItem
|
||||
label={label}
|
||||
labelElement={labelElement}
|
||||
onClick={handleClick}
|
||||
style={style}
|
||||
text={text}
|
||||
/>
|
||||
);
|
||||
case MenuTypes.PARENT:
|
||||
return (
|
||||
<StyledMenuItem label={label} style={style} text={confirm.text}>
|
||||
{children}
|
||||
</StyledMenuItem>
|
||||
);
|
||||
case MenuTypes.RECONFIRM:
|
||||
return (
|
||||
<StyledMenuItem
|
||||
label={label}
|
||||
onClick={handleReconfirmClick}
|
||||
style={style}
|
||||
text={confirm.text}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
|
@ -0,0 +1,250 @@
|
|||
import React, { useState, useCallback } from "react";
|
||||
|
||||
import styled, { withTheme } from "styled-components";
|
||||
import { Classes, Menu, Position } from "@blueprintjs/core";
|
||||
import { Classes as Popover2Classes, Popover2 } from "@blueprintjs/popover2";
|
||||
import { noop } from "lodash";
|
||||
|
||||
import { Variant } from "components/ads/common";
|
||||
import { Toaster } from "components/ads/Toast";
|
||||
import Icon, { IconSize } from "components/ads/Icon";
|
||||
import { SavingState } from "components/ads/EditableTextSubComponent";
|
||||
import { EditInteractionKind } from "components/ads/EditableText";
|
||||
import { CommonComponentProps, ThemeProp } from "components/ads/common";
|
||||
import { getTypographyByKey } from "constants/DefaultTheme";
|
||||
|
||||
import EditableAppName from "./EditableAppName";
|
||||
import { GetNavigationMenuData } from "./NavigationMenuData";
|
||||
import { NavigationMenu } from "./NavigationMenu";
|
||||
|
||||
type EditorAppNameProps = CommonComponentProps &
|
||||
ThemeProp & {
|
||||
applicationId: string | undefined;
|
||||
defaultValue: string;
|
||||
placeholder?: string;
|
||||
editInteractionKind: EditInteractionKind;
|
||||
defaultSavingState: SavingState;
|
||||
deploy: typeof noop;
|
||||
onBlur?: (value: string) => void;
|
||||
isEditingDefault?: boolean;
|
||||
inputValidation?: (value: string) => string | boolean;
|
||||
hideEditIcon?: boolean;
|
||||
fill?: boolean;
|
||||
isError?: boolean;
|
||||
isNewApp: boolean;
|
||||
currentDeployLink: string;
|
||||
isPopoverOpen: boolean;
|
||||
setIsPopoverOpen: typeof noop;
|
||||
};
|
||||
|
||||
const Container = styled.div<{ isPopoverOpen: boolean }>`
|
||||
display: flex;
|
||||
cursor: pointer;
|
||||
${(props) =>
|
||||
props.isPopoverOpen &&
|
||||
`
|
||||
background-color: ${props.theme.colors.navigationMenu.backgroundInactive};
|
||||
> span {
|
||||
background-color: ${props.theme.colors.navigationMenu.backgroundInactive};
|
||||
}`}
|
||||
&:hover {
|
||||
background-color: ${(props) =>
|
||||
props.theme.colors.navigationMenu.backgroundInactive};
|
||||
> span {
|
||||
background-color: ${(props) =>
|
||||
props.theme.colors.navigationMenu.backgroundInactive};
|
||||
}
|
||||
}
|
||||
|
||||
> span {
|
||||
height: ${(props) => props.theme.smallHeaderHeight};
|
||||
}
|
||||
|
||||
& .${Popover2Classes.POPOVER2_TARGET} {
|
||||
height: 100%;
|
||||
}
|
||||
& .${Classes.EDITABLE_TEXT} {
|
||||
height: ${(props) => props.theme.smallHeaderHeight} !important;
|
||||
display: block;
|
||||
cursor: pointer;
|
||||
}
|
||||
&&&& .${Classes.EDITABLE_TEXT}, &&&& .${Classes.EDITABLE_TEXT_EDITING} {
|
||||
padding: 0 ${(props) => props.theme.spaces[0]}px;
|
||||
width: 100%;
|
||||
}
|
||||
&&&& .${Classes.EDITABLE_TEXT_CONTENT}, &&&& .${Classes.EDITABLE_TEXT_INPUT} {
|
||||
display: block;
|
||||
${(props) => getTypographyByKey(props, "h4")};
|
||||
line-height: 19px !important;
|
||||
padding: 8px 5px;
|
||||
}
|
||||
&&&& .${Classes.EDITABLE_TEXT_INPUT} {
|
||||
margin-right: 20px;
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledIcon = styled(Icon)`
|
||||
height: 100%;
|
||||
padding-right: 10px;
|
||||
align-self: center;
|
||||
|
||||
svg path {
|
||||
fill: ${(props) => props.theme.colors.navigationMenu.contentActive};
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledMenu = styled(Menu)`
|
||||
background: ${(props) =>
|
||||
props.theme.colors.navigationMenu.backgroundInactive};
|
||||
color: ${(props) => props.theme.colors.navigationMenu.contentInactive};
|
||||
${(props) => getTypographyByKey(props, "p1")};
|
||||
border-radius: 0;
|
||||
padding: 0;
|
||||
|
||||
&&& .${Classes.MENU}, &&& .${Classes.MENU_SUBMENU} {
|
||||
background: ${(props) =>
|
||||
props.theme.colors.navigationMenu.backgroundInactive};
|
||||
color: ${(props) => props.theme.colors.navigationMenu.contentInactive};
|
||||
border-radius: 0;
|
||||
padding: 0;
|
||||
|
||||
.${Classes.ICON} {
|
||||
color: ${(props) => props.theme.colors.navigationMenu.contentInactive};
|
||||
}
|
||||
|
||||
.${Classes.POPOVER_TARGET}.${Classes.POPOVER_OPEN} > .${Classes.MENU_ITEM} {
|
||||
color: ${(props) => props.theme.colors.navigationMenu.contentActive};
|
||||
background: ${(props) =>
|
||||
props.theme.colors.navigationMenu.backgroundActive};
|
||||
background-color: ${(props) =>
|
||||
props.theme.colors.navigationMenu.backgroundActive};
|
||||
}
|
||||
}
|
||||
|
||||
&&& .${Classes.MENU_SUBMENU}:hover {
|
||||
.${Classes.ICON} {
|
||||
color: ${(props) => props.theme.colors.navigationMenu.contentActive};
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export function EditorAppName(props: EditorAppNameProps) {
|
||||
const {
|
||||
applicationId,
|
||||
currentDeployLink,
|
||||
defaultSavingState,
|
||||
defaultValue,
|
||||
deploy,
|
||||
isNewApp,
|
||||
isPopoverOpen,
|
||||
setIsPopoverOpen,
|
||||
theme,
|
||||
} = props;
|
||||
|
||||
const [isEditingDefault, setIsEditingDefault] = useState(isNewApp);
|
||||
const [isEditing, setIsEditing] = useState(!!isEditingDefault);
|
||||
const [isInvalid, setIsInvalid] = useState<string | boolean>(false);
|
||||
const [savingState, setSavingState] = useState<SavingState>(
|
||||
SavingState.NOT_STARTED,
|
||||
);
|
||||
|
||||
const onBlur = (value: string) => {
|
||||
if (props.onBlur) props.onBlur(value);
|
||||
setIsEditingDefault(false);
|
||||
};
|
||||
|
||||
const inputValidation = (value: string) => {
|
||||
if (value.trim() === "") {
|
||||
Toaster.show({
|
||||
text: "Application name can't be empty",
|
||||
variant: Variant.danger,
|
||||
});
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const editMode = useCallback(
|
||||
(e: React.MouseEvent) => {
|
||||
setIsEditing(true);
|
||||
const errorMessage = inputValidation && inputValidation(defaultValue);
|
||||
setIsInvalid(errorMessage ? errorMessage : false);
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
},
|
||||
[inputValidation, defaultValue],
|
||||
);
|
||||
|
||||
const handleAppNameClick = useCallback(() => {
|
||||
if (!isEditing) {
|
||||
setIsPopoverOpen((isOpen: boolean) => {
|
||||
return !isOpen;
|
||||
});
|
||||
}
|
||||
}, [isEditing]);
|
||||
|
||||
const handleOnInteraction = useCallback((nextOpenState: boolean) => {
|
||||
if (!nextOpenState) {
|
||||
setIsPopoverOpen(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const NavigationMenuData = GetNavigationMenuData({
|
||||
applicationId,
|
||||
currentDeployLink,
|
||||
editMode,
|
||||
deploy,
|
||||
theme,
|
||||
});
|
||||
|
||||
const NavigationMenuItems = (
|
||||
<StyledMenu>
|
||||
<NavigationMenu
|
||||
menuItems={NavigationMenuData}
|
||||
setIsPopoverOpen={setIsPopoverOpen}
|
||||
/>
|
||||
</StyledMenu>
|
||||
);
|
||||
|
||||
return defaultValue !== "" ? (
|
||||
<Popover2
|
||||
autoFocus={false}
|
||||
content={NavigationMenuItems}
|
||||
isOpen={isPopoverOpen}
|
||||
minimal
|
||||
onInteraction={handleOnInteraction}
|
||||
portalClassName="t--editor-appname-menu-portal"
|
||||
position={Position.BOTTOM_RIGHT}
|
||||
>
|
||||
<Container isPopoverOpen={isPopoverOpen} onClick={handleAppNameClick}>
|
||||
<EditableAppName
|
||||
className={props.className}
|
||||
defaultSavingState={defaultSavingState}
|
||||
defaultValue={defaultValue}
|
||||
editInteractionKind={props.editInteractionKind}
|
||||
fill={props.fill}
|
||||
hideEditIcon
|
||||
inputValidation={inputValidation}
|
||||
isEditing={isEditing}
|
||||
isEditingDefault={isEditingDefault}
|
||||
isError={props.isError}
|
||||
isInvalid={isInvalid}
|
||||
onBlur={onBlur}
|
||||
placeholder={props.placeholder}
|
||||
savingState={savingState}
|
||||
setIsEditing={setIsEditing}
|
||||
setIsInvalid={setIsInvalid}
|
||||
setSavingState={setSavingState}
|
||||
/>
|
||||
{!isEditing && (
|
||||
<StyledIcon
|
||||
fillColor={theme.colors.navigationMenu.contentInactive}
|
||||
name="downArrow"
|
||||
size={IconSize.XXS}
|
||||
/>
|
||||
)}
|
||||
</Container>
|
||||
</Popover2>
|
||||
) : null;
|
||||
}
|
||||
|
||||
export default withTheme(EditorAppName);
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
import React, { useEffect, useState } from "react";
|
||||
import styled, { ThemeProvider } from "styled-components";
|
||||
import { Classes as Popover2Classes } from "@blueprintjs/popover2";
|
||||
import {
|
||||
ApplicationPayload,
|
||||
ReduxActionTypes,
|
||||
|
|
@ -35,7 +36,7 @@ import {
|
|||
getIsErroredSavingAppName,
|
||||
showAppInviteUsersDialogSelector,
|
||||
} from "selectors/applicationSelectors";
|
||||
import EditableAppName from "./EditableAppName";
|
||||
import EditorAppName from "./EditorAppName";
|
||||
import Boxed from "components/editorComponents/Onboarding/Boxed";
|
||||
import OnboardingHelper from "components/editorComponents/Onboarding/Helper";
|
||||
import { OnboardingStep } from "constants/OnboardingConstants";
|
||||
|
|
@ -106,6 +107,10 @@ const HeaderSection = styled.div`
|
|||
:nth-child(3) {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
> .${Popover2Classes.POPOVER2_TARGET} {
|
||||
max-width: calc(100% - 50px);
|
||||
min-width: 100px;
|
||||
}
|
||||
`;
|
||||
|
||||
const AppsmithLogoImg = styled.img`
|
||||
|
|
@ -191,6 +196,8 @@ export function EditorHeader(props: EditorHeaderProps) {
|
|||
};
|
||||
}, [lastUpdatedTime]);
|
||||
|
||||
const [isPopoverOpen, setIsPopoverOpen] = useState<boolean>(false);
|
||||
|
||||
const handlePublish = () => {
|
||||
if (applicationId) {
|
||||
publishApplication(applicationId);
|
||||
|
|
@ -253,9 +260,18 @@ export function EditorHeader(props: EditorHeaderProps) {
|
|||
/>
|
||||
</Link>
|
||||
<Boxed step={OnboardingStep.FINISH}>
|
||||
<EditableAppName
|
||||
<EditorAppName
|
||||
applicationId={applicationId}
|
||||
className="t--application-name editable-application-name"
|
||||
currentDeployLink={getApplicationViewerPageURL(
|
||||
applicationId,
|
||||
pageId,
|
||||
)}
|
||||
defaultSavingState={
|
||||
isSavingName ? SavingState.STARTED : SavingState.NOT_STARTED
|
||||
}
|
||||
defaultValue={currentApplication?.name || ""}
|
||||
deploy={handlePublish}
|
||||
editInteractionKind={EditInteractionKind.SINGLE}
|
||||
fill
|
||||
isError={isErroredSavingName}
|
||||
|
|
@ -263,17 +279,16 @@ export function EditorHeader(props: EditorHeaderProps) {
|
|||
applicationList.filter((el) => el.id === applicationId).length >
|
||||
0
|
||||
}
|
||||
isPopoverOpen={isPopoverOpen}
|
||||
onBlur={(value: string) =>
|
||||
updateApplicationDispatch(applicationId || "", {
|
||||
name: value,
|
||||
currentApp: true,
|
||||
})
|
||||
}
|
||||
savingState={
|
||||
isSavingName ? SavingState.STARTED : SavingState.NOT_STARTED
|
||||
}
|
||||
setIsPopoverOpen={setIsPopoverOpen}
|
||||
/>
|
||||
<ToggleModeButton />
|
||||
<ToggleModeButton showSelectedMode={!isPopoverOpen} />
|
||||
</Boxed>
|
||||
</HeaderSection>
|
||||
<HeaderSection>
|
||||
|
|
|
|||
|
|
@ -23,6 +23,8 @@ import { MAIN_CONTAINER_WIDGET_ID } from "constants/WidgetConstants";
|
|||
describe("Select all hotkey", () => {
|
||||
const mockGetIsFetchingPage = jest.spyOn(utilities, "getIsFetchingPage");
|
||||
const spyGetCanvasWidgetDsl = jest.spyOn(utilities, "getCanvasWidgetDsl");
|
||||
Element.prototype.scrollIntoView = jest.fn();
|
||||
|
||||
function UpdatedMainContainer({ dsl }: any) {
|
||||
useMockDsl(dsl);
|
||||
return <MainContainer />;
|
||||
|
|
|
|||
|
|
@ -43,7 +43,11 @@ const getShowCommentsButtonToolTip = () => {
|
|||
const setShowCommentsButtonToolTip = (value = "") =>
|
||||
localStorage.setItem("ShowCommentsButtonToolTip", value);
|
||||
|
||||
const ModeButton = styled.div<{ active: boolean; type: string }>`
|
||||
const ModeButton = styled.div<{
|
||||
active: boolean;
|
||||
showSelectedMode: boolean;
|
||||
type: string;
|
||||
}>`
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
|
@ -53,7 +57,7 @@ const ModeButton = styled.div<{ active: boolean; type: string }>`
|
|||
height: ${(props) => props.theme.smallHeaderHeight};
|
||||
width: ${(props) => props.theme.smallHeaderHeight};
|
||||
background: ${(props) =>
|
||||
props.active
|
||||
props.active && props.showSelectedMode
|
||||
? props.theme.colors.comments.activeModeBackground
|
||||
: "transparent"};
|
||||
|
||||
|
|
@ -91,6 +95,7 @@ const Container = styled.div`
|
|||
display: flex;
|
||||
flex: 1;
|
||||
z-index: ${Indices.Layer1};
|
||||
margin-left: ${(props) => props.theme.smallHeaderHeight};
|
||||
`;
|
||||
|
||||
/**
|
||||
|
|
@ -222,11 +227,13 @@ function ViewOrEditMode({ mode }: { mode?: APP_MODE }) {
|
|||
function CommentModeBtn({
|
||||
handleSetCommentModeButton,
|
||||
isCommentMode,
|
||||
showSelectedMode,
|
||||
showUnreadIndicator,
|
||||
}: {
|
||||
handleSetCommentModeButton: () => void;
|
||||
isCommentMode: boolean;
|
||||
showUnreadIndicator: boolean;
|
||||
showSelectedMode: boolean;
|
||||
}) {
|
||||
const CommentModeIcon = showUnreadIndicator ? CommentModeUnread : CommentMode;
|
||||
|
||||
|
|
@ -235,6 +242,7 @@ function CommentModeBtn({
|
|||
active={isCommentMode}
|
||||
className="t--switch-comment-mode-on"
|
||||
onClick={handleSetCommentModeButton}
|
||||
showSelectedMode={showSelectedMode}
|
||||
type="stroke"
|
||||
>
|
||||
<TooltipComponent
|
||||
|
|
@ -253,7 +261,13 @@ function CommentModeBtn({
|
|||
);
|
||||
}
|
||||
|
||||
function ToggleCommentModeButton() {
|
||||
type ToggleCommentModeButtonProps = {
|
||||
showSelectedMode?: boolean;
|
||||
};
|
||||
|
||||
function ToggleCommentModeButton({
|
||||
showSelectedMode = true,
|
||||
}: ToggleCommentModeButtonProps) {
|
||||
const commentsEnabled = useSelector(areCommentsEnabledForUserAndAppSelector);
|
||||
const isCommentMode = useSelector(commentModeSelector);
|
||||
const currentUser = useSelector(getCurrentUser);
|
||||
|
|
@ -300,6 +314,7 @@ function ToggleCommentModeButton() {
|
|||
<ModeButton
|
||||
active={!isCommentMode}
|
||||
onClick={() => setCommentModeInUrl(false)}
|
||||
showSelectedMode={showSelectedMode}
|
||||
type="fill"
|
||||
>
|
||||
<ViewOrEditMode mode={mode} />
|
||||
|
|
@ -313,6 +328,7 @@ function ToggleCommentModeButton() {
|
|||
handleSetCommentModeButton,
|
||||
isCommentMode,
|
||||
showUnreadIndicator,
|
||||
showSelectedMode,
|
||||
}}
|
||||
/>
|
||||
</TooltipComponent>
|
||||
|
|
|
|||
|
|
@ -126,6 +126,7 @@ export type EventName =
|
|||
| "CYCLICAL_DEPENDENCY_ERROR"
|
||||
| "DISCORD_LINK_CLICK"
|
||||
| "BINDING_SUCCESS"
|
||||
| "APP_MENU_OPTION_CLICK"
|
||||
| "SLASH_COMMAND"
|
||||
| "DEBUGGER_NEW_ERROR"
|
||||
| "DEBUGGER_RESOLVED_ERROR";
|
||||
|
|
|
|||
Loading…
Reference in New Issue