feat: Adds a status indicator setting in List Item and Tab (#39938)
## Description Adds ability across UI elements to show warning indicators ## Automation /ok-to-test tags="@tag.Sanity" ### 🔍 Cypress test results <!-- This is an auto-generated comment: Cypress test results --> > [!TIP] > 🟢 🟢 🟢 All cypress tests have passed! 🎉 🎉 🎉 > Workflow run: <https://github.com/appsmithorg/appsmith/actions/runs/14083540998> > Commit: 022e693f2474a12a70ed3ae317ed26d9eb7caed6 > <a href="https://internal.appsmith.com/app/cypress-dashboard/rundetails-65890b3c81d7400d08fa9ee5?branch=master&workflowId=14083540998&attempt=1" target="_blank">Cypress dashboard</a>. > Tags: `@tag.Sanity` > Spec: > <hr>Wed, 26 Mar 2025 13:24:06 UTC <!-- end of auto-generated comment: Cypress test results --> ## Communication Should the DevRel and Marketing teams inform users about this change? - [ ] Yes - [ ] No <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Added visual indicators for unsaved changes across list and tab components—badges now appear to alert users of pending modifications. - Enhanced the code editor by introducing dynamic line highlighting, allowing users to easily identify and navigate to specific code sections. - Introduced new selectors to improve management of JavaScript collection actions and their schema states. - **Style** - Upgraded badge styling with a new "info" variant and adjustable size options, resulting in a more refined and consistent user experience. - Added a new styled component for unsaved changes, enhancing visual feedback for users. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
parent
e3a49845e7
commit
074fe6fd37
|
|
@ -1,5 +1,5 @@
|
||||||
import styled, { css } from "styled-components";
|
import styled, { css } from "styled-components";
|
||||||
import type { BadgeKind } from "./Badge.types";
|
import type { BadgeKind, BadgeSize } from "./Badge.types";
|
||||||
|
|
||||||
const Kind = {
|
const Kind = {
|
||||||
error: css`
|
error: css`
|
||||||
|
|
@ -11,14 +11,28 @@ const Kind = {
|
||||||
success: css`
|
success: css`
|
||||||
--badge-color-bg: var(--ads-v2-color-fg-success);
|
--badge-color-bg: var(--ads-v2-color-fg-success);
|
||||||
`,
|
`,
|
||||||
|
info: css`
|
||||||
|
--badge-color-bg: var(--ads-v2-color-fg);
|
||||||
|
`,
|
||||||
|
};
|
||||||
|
|
||||||
|
const Size = {
|
||||||
|
small: css`
|
||||||
|
width: 5px;
|
||||||
|
height: 5px;
|
||||||
|
`,
|
||||||
|
medium: css`
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
`,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const StyledBadge = styled.div<{
|
export const StyledBadge = styled.div<{
|
||||||
kind?: BadgeKind;
|
kind?: BadgeKind;
|
||||||
|
size?: BadgeSize;
|
||||||
}>`
|
}>`
|
||||||
width: 8px;
|
|
||||||
height: 8px;
|
|
||||||
background-color: var(--badge-color-bg);
|
background-color: var(--badge-color-bg);
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
${({ kind }) => kind && Kind[kind]}
|
${({ kind }) => kind && Kind[kind]}
|
||||||
|
${({ size }) => size && Size[size]}
|
||||||
`;
|
`;
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,13 @@ import { StyledBadge } from "./Badge.styles";
|
||||||
* @param className
|
* @param className
|
||||||
* @constructor
|
* @constructor
|
||||||
*/
|
*/
|
||||||
export function Badge({ className, kind = "success", ...rest }: BadgeProps) {
|
export function Badge({
|
||||||
return <StyledBadge className={className} kind={kind} {...rest} />;
|
className,
|
||||||
|
kind = "success",
|
||||||
|
size = "medium",
|
||||||
|
...rest
|
||||||
|
}: BadgeProps) {
|
||||||
|
return (
|
||||||
|
<StyledBadge className={className} kind={kind} size={size} {...rest} />
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,13 @@
|
||||||
import type { Kind } from "../__config__/types";
|
import type { Kind } from "../__config__/types";
|
||||||
|
|
||||||
export type BadgeKind = Exclude<Kind, "info" | undefined>;
|
export type BadgeKind = Exclude<Kind, undefined>;
|
||||||
|
export type BadgeSize = "small" | "medium";
|
||||||
|
|
||||||
export interface BadgeProps {
|
export interface BadgeProps {
|
||||||
/** visual style to be used indicating type of badge */
|
/** visual style to be used indicating type of badge */
|
||||||
kind?: BadgeKind;
|
kind?: BadgeKind;
|
||||||
/** (try not to) pass addition classes here */
|
/** (try not to) pass addition classes here */
|
||||||
className?: string;
|
className?: string;
|
||||||
|
/** Size of the badge */
|
||||||
|
size?: BadgeSize;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -44,6 +44,14 @@ export const RightControlWrapper = styled.div`
|
||||||
align-items: center;
|
align-items: center;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
export const UnsavedChangesWrapper = styled.div`
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
`;
|
||||||
|
|
||||||
export const TopContentWrapper = styled.div`
|
export const TopContentWrapper = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ import {
|
||||||
StyledListItem,
|
StyledListItem,
|
||||||
TooltipTextWrapper,
|
TooltipTextWrapper,
|
||||||
TopContentWrapper,
|
TopContentWrapper,
|
||||||
|
UnsavedChangesWrapper,
|
||||||
} from "./List.styles";
|
} from "./List.styles";
|
||||||
import type { TextProps } from "../Text";
|
import type { TextProps } from "../Text";
|
||||||
import { Text } from "../Text";
|
import { Text } from "../Text";
|
||||||
|
|
@ -26,6 +27,7 @@ import {
|
||||||
ListItemTitleClassName,
|
ListItemTitleClassName,
|
||||||
} from "./List.constants";
|
} from "./List.constants";
|
||||||
import { useEventCallback } from "usehooks-ts";
|
import { useEventCallback } from "usehooks-ts";
|
||||||
|
import { Badge } from "../Badge";
|
||||||
|
|
||||||
function List({ children, className, groupTitle, ...rest }: ListProps) {
|
function List({ children, className, groupTitle, ...rest }: ListProps) {
|
||||||
return groupTitle ? (
|
return groupTitle ? (
|
||||||
|
|
@ -86,6 +88,7 @@ function ListItem(props: ListItemProps) {
|
||||||
hasError,
|
hasError,
|
||||||
rightControl,
|
rightControl,
|
||||||
rightControlVisibility = "always",
|
rightControlVisibility = "always",
|
||||||
|
showUnsavedChanges,
|
||||||
size = "md",
|
size = "md",
|
||||||
startIcon,
|
startIcon,
|
||||||
title,
|
title,
|
||||||
|
|
@ -159,6 +162,11 @@ function ListItem(props: ListItemProps) {
|
||||||
{rightControl}
|
{rightControl}
|
||||||
</RightControlWrapper>
|
</RightControlWrapper>
|
||||||
)}
|
)}
|
||||||
|
{showUnsavedChanges ? (
|
||||||
|
<UnsavedChangesWrapper>
|
||||||
|
<Badge kind="info" size="small" />
|
||||||
|
</UnsavedChangesWrapper>
|
||||||
|
) : null}
|
||||||
</TopContentWrapper>
|
</TopContentWrapper>
|
||||||
{isBlockDescription && (
|
{isBlockDescription && (
|
||||||
<BottomContentWrapper data-isiconpresent={Boolean(startIcon)}>
|
<BottomContentWrapper data-isiconpresent={Boolean(startIcon)}>
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,8 @@ export interface ListItemProps {
|
||||||
customTitleComponent?: ReactNode | ReactNode[];
|
customTitleComponent?: ReactNode | ReactNode[];
|
||||||
/** dataTestId which will be used in automated tests */
|
/** dataTestId which will be used in automated tests */
|
||||||
dataTestId?: string;
|
dataTestId?: string;
|
||||||
|
/** Whether to show the unsaved changes indicator */
|
||||||
|
showUnsavedChanges?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ListProps {
|
export interface ListProps {
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import { EditableEntityName } from "../EditableEntityName";
|
||||||
|
|
||||||
import type { EditableDismissibleTabProps } from "./EditableDismissibleTab.types";
|
import type { EditableDismissibleTabProps } from "./EditableDismissibleTab.types";
|
||||||
import { useActiveDoubleClick } from "../../__hooks__";
|
import { useActiveDoubleClick } from "../../__hooks__";
|
||||||
|
import { Badge } from "../../Badge";
|
||||||
|
|
||||||
export const EditableDismissibleTab = (props: EditableDismissibleTabProps) => {
|
export const EditableDismissibleTab = (props: EditableDismissibleTabProps) => {
|
||||||
const {
|
const {
|
||||||
|
|
@ -22,6 +23,7 @@ export const EditableDismissibleTab = (props: EditableDismissibleTabProps) => {
|
||||||
onEnterEditMode: propOnEnterEditMode,
|
onEnterEditMode: propOnEnterEditMode,
|
||||||
onExitEditMode: propOnExitEditMode,
|
onExitEditMode: propOnExitEditMode,
|
||||||
onNameSave,
|
onNameSave,
|
||||||
|
showUnsavedChanges,
|
||||||
validateName,
|
validateName,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
|
|
@ -60,6 +62,7 @@ export const EditableDismissibleTab = (props: EditableDismissibleTabProps) => {
|
||||||
onNameSave={onNameSave}
|
onNameSave={onNameSave}
|
||||||
validateName={validateName}
|
validateName={validateName}
|
||||||
/>
|
/>
|
||||||
|
{showUnsavedChanges ? <Badge kind="info" size="small" /> : null}
|
||||||
</DismissibleTab>
|
</DismissibleTab>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -28,4 +28,6 @@ export interface EditableDismissibleTabProps {
|
||||||
onNameSave: (name: string) => void;
|
onNameSave: (name: string) => void;
|
||||||
/** Function to validate the name. */
|
/** Function to validate the name. */
|
||||||
validateName: (name: string) => string | null;
|
validateName: (name: string) => string | null;
|
||||||
|
/** Show unsaved changes indicator. */
|
||||||
|
showUnsavedChanges?: boolean;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
import type { JSCollection } from "entities/JSCollection";
|
||||||
|
|
||||||
|
// Implementation exists in ee
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
export const getHighlightedLines = (jsCollection: JSCollection): number[] => {
|
||||||
|
return [];
|
||||||
|
};
|
||||||
|
|
@ -454,6 +454,12 @@ export const getActions = (state: AppState): ActionDataState =>
|
||||||
export const getJSCollections = (state: AppState): JSCollectionDataState =>
|
export const getJSCollections = (state: AppState): JSCollectionDataState =>
|
||||||
state.entities.jsActions;
|
state.entities.jsActions;
|
||||||
|
|
||||||
|
export const getAllJSCollectionActions = (state: AppState) => {
|
||||||
|
return state.entities.jsActions.flatMap(
|
||||||
|
(jsCollection) => jsCollection.config.actions,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export const getDatasource = (
|
export const getDatasource = (
|
||||||
state: AppState,
|
state: AppState,
|
||||||
datasourceId: string,
|
datasourceId: string,
|
||||||
|
|
@ -1752,3 +1758,40 @@ export const getIsSavingEntityName = (
|
||||||
|
|
||||||
return isSavingEntityName;
|
return isSavingEntityName;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getActionSchemaDirtyState = createSelector(
|
||||||
|
getAction,
|
||||||
|
(state: AppState) =>
|
||||||
|
getPluginByPackageName(state, PluginPackageName.APPSMITH_AI),
|
||||||
|
(action, agentPlugin) => {
|
||||||
|
if (!action) return false;
|
||||||
|
|
||||||
|
if (agentPlugin?.id === action.pluginId) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return action.isDirtyMap?.SCHEMA_GENERATION;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
export const getJSCollectionSchemaDirtyState = createSelector(
|
||||||
|
(state: AppState, collectionId: string) =>
|
||||||
|
getJSCollection(state, collectionId),
|
||||||
|
(jsCollection) => {
|
||||||
|
if (!jsCollection) return false;
|
||||||
|
|
||||||
|
return jsCollection.actions.some(
|
||||||
|
(action) => action.isDirtyMap?.SCHEMA_GENERATION,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
export const getJSCollectionActionSchemaDirtyState = createSelector(
|
||||||
|
(state: AppState, collectionId: string, actionId: string) =>
|
||||||
|
getJSCollectionAction(state, collectionId, actionId),
|
||||||
|
(action) => {
|
||||||
|
if (!action) return false;
|
||||||
|
|
||||||
|
return action.isDirtyMap?.SCHEMA_GENERATION;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
|
||||||
|
|
@ -260,6 +260,7 @@ export type EditorProps = EditorStyleProps &
|
||||||
removeHoverAndFocusStyle?: boolean;
|
removeHoverAndFocusStyle?: boolean;
|
||||||
|
|
||||||
customErrors?: LintError[];
|
customErrors?: LintError[];
|
||||||
|
highlightedLines?: number[]; // Array of line numbers to highlight
|
||||||
};
|
};
|
||||||
|
|
||||||
interface Props extends ReduxStateProps, EditorProps, ReduxDispatchProps {}
|
interface Props extends ReduxStateProps, EditorProps, ReduxDispatchProps {}
|
||||||
|
|
@ -445,11 +446,11 @@ class CodeEditor extends Component<Props, State> {
|
||||||
this: CodeEditor,
|
this: CodeEditor,
|
||||||
editor: CodeMirror.Editor,
|
editor: CodeMirror.Editor,
|
||||||
) {
|
) {
|
||||||
// If you need to do something with the editor right after it’s been created,
|
// If you need to do something with the editor right after it's been created,
|
||||||
// put that code here.
|
// put that code here.
|
||||||
//
|
//
|
||||||
// This helps with performance: finishInit() is called inside
|
// This helps with performance: finishInit() is called inside
|
||||||
// CodeMirror’s `operation()` (https://codemirror.net/doc/manual.html#operation
|
// CodeMirror's `operation()` (https://codemirror.net/doc/manual.html#operation
|
||||||
// which means CodeMirror recalculates itself only one time, once all CodeMirror
|
// which means CodeMirror recalculates itself only one time, once all CodeMirror
|
||||||
// changes here are completed
|
// changes here are completed
|
||||||
//
|
//
|
||||||
|
|
@ -500,7 +501,13 @@ class CodeEditor extends Component<Props, State> {
|
||||||
|
|
||||||
// Finally create the Codemirror editor
|
// Finally create the Codemirror editor
|
||||||
this.editor = CodeMirror(this.codeEditorTarget.current, options);
|
this.editor = CodeMirror(this.codeEditorTarget.current, options);
|
||||||
// DO NOT ADD CODE BELOW. If you need to do something with the editor right after it’s created,
|
|
||||||
|
// Add highlighting for initial render
|
||||||
|
if (this.props.highlightedLines?.length) {
|
||||||
|
this.updateLineHighlighting(this.props.highlightedLines);
|
||||||
|
}
|
||||||
|
|
||||||
|
// DO NOT ADD CODE BELOW. If you need to do something with the editor right after it's created,
|
||||||
// put that code into `options.finishInit()`.
|
// put that code into `options.finishInit()`.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -612,6 +619,11 @@ class CodeEditor extends Component<Props, State> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if highlighted lines have changed
|
||||||
|
if (!isEqual(prevProps.highlightedLines, this.props.highlightedLines)) {
|
||||||
|
this.updateLineHighlighting(this.props.highlightedLines || []);
|
||||||
|
}
|
||||||
|
|
||||||
this.editor.operation(() => {
|
this.editor.operation(() => {
|
||||||
const editorValue = this.editor.getValue();
|
const editorValue = this.editor.getValue();
|
||||||
// Safe update of value of the editor when value updated outside the editor
|
// Safe update of value of the editor when value updated outside the editor
|
||||||
|
|
@ -1634,6 +1646,21 @@ class CodeEditor extends Component<Props, State> {
|
||||||
this.editor.setValue(value);
|
this.editor.setValue(value);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Add new method to handle line highlighting
|
||||||
|
private updateLineHighlighting = (lines: number[]) => {
|
||||||
|
// Clear existing highlights
|
||||||
|
for (let i = 0; i < this.editor.lineCount(); i++) {
|
||||||
|
this.editor.removeLineClass(i, "background", "highlighted-line");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add new highlights
|
||||||
|
lines.forEach((lineNumber) => {
|
||||||
|
if (lineNumber >= 0 && lineNumber < this.editor.lineCount()) {
|
||||||
|
this.editor.addLineClass(lineNumber, "background", "highlighted-line");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
border,
|
border,
|
||||||
|
|
|
||||||
|
|
@ -256,6 +256,10 @@ export const EditorWrapper = styled.div<{
|
||||||
.CodeMirror-activeline-background {
|
.CodeMirror-activeline-background {
|
||||||
background-color: #ececec;
|
background-color: #ececec;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.highlighted-line {
|
||||||
|
background-color: rgba(255, 255, 0, 0.2);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.CodeMirror-guttermarker-subtle {
|
.CodeMirror-guttermarker-subtle {
|
||||||
color: var(--ads-v2-color-fg-subtle);
|
color: var(--ads-v2-color-fg-subtle);
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
export { getHighlightedLines } from "ce/pages/Editor/JSEditor/utils/getHighlightedLines";
|
||||||
|
|
@ -1,7 +1,10 @@
|
||||||
import React, { useCallback, useMemo } from "react";
|
import React, { useCallback, useMemo } from "react";
|
||||||
import { EntityItem, EntityContextMenu } from "@appsmith/ads";
|
import { EntityItem, EntityContextMenu } from "@appsmith/ads";
|
||||||
import type { AppState } from "ee/reducers";
|
import type { AppState } from "ee/reducers";
|
||||||
import { getJsCollectionByBaseId } from "ee/selectors/entitiesSelector";
|
import {
|
||||||
|
getJsCollectionByBaseId,
|
||||||
|
getJSCollectionSchemaDirtyState,
|
||||||
|
} from "ee/selectors/entitiesSelector";
|
||||||
import { useDispatch, useSelector } from "react-redux";
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
import { useFeatureFlag } from "utils/hooks/useFeatureFlag";
|
import { useFeatureFlag } from "utils/hooks/useFeatureFlag";
|
||||||
import { FEATURE_FLAG } from "ee/entities/FeatureFlag";
|
import { FEATURE_FLAG } from "ee/entities/FeatureFlag";
|
||||||
|
|
@ -105,6 +108,10 @@ export const JSEntityItem = ({ item }: { item: EntityItemProps }) => {
|
||||||
validateName,
|
validateName,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
const isJSActionSchemaDirty = useSelector((state: AppState) =>
|
||||||
|
getJSCollectionSchemaDirtyState(state, item.key),
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<EntityItem
|
<EntityItem
|
||||||
className={clsx("t--jsaction", {
|
className={clsx("t--jsaction", {
|
||||||
|
|
@ -118,6 +125,7 @@ export const JSEntityItem = ({ item }: { item: EntityItemProps }) => {
|
||||||
onDoubleClick={() => enterEditMode(jsAction.id)}
|
onDoubleClick={() => enterEditMode(jsAction.id)}
|
||||||
rightControl={contextMenu}
|
rightControl={contextMenu}
|
||||||
rightControlVisibility="hover"
|
rightControlVisibility="hover"
|
||||||
|
showUnsavedChanges={isJSActionSchemaDirty}
|
||||||
startIcon={JsFileIconV2(16, 16)}
|
startIcon={JsFileIconV2(16, 16)}
|
||||||
title={item.title}
|
title={item.title}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import { EntityItem, EntityContextMenu } from "@appsmith/ads";
|
||||||
import type { AppState } from "ee/reducers";
|
import type { AppState } from "ee/reducers";
|
||||||
import {
|
import {
|
||||||
getActionByBaseId,
|
getActionByBaseId,
|
||||||
|
getActionSchemaDirtyState,
|
||||||
getDatasource,
|
getDatasource,
|
||||||
getPlugins,
|
getPlugins,
|
||||||
} from "ee/selectors/entitiesSelector";
|
} from "ee/selectors/entitiesSelector";
|
||||||
|
|
@ -117,6 +118,10 @@ export const QueryEntityItem = ({ item }: { item: EntityItemProps }) => {
|
||||||
validateName,
|
validateName,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
const isActionSchemaDirty = useSelector((state: AppState) =>
|
||||||
|
getActionSchemaDirtyState(state, action.id),
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<EntityItem
|
<EntityItem
|
||||||
className="action t--action-entity"
|
className="action t--action-entity"
|
||||||
|
|
@ -128,6 +133,7 @@ export const QueryEntityItem = ({ item }: { item: EntityItemProps }) => {
|
||||||
onDoubleClick={() => enterEditMode(action.id)}
|
onDoubleClick={() => enterEditMode(action.id)}
|
||||||
rightControl={contextMenu}
|
rightControl={contextMenu}
|
||||||
rightControlVisibility="hover"
|
rightControlVisibility="hover"
|
||||||
|
showUnsavedChanges={isActionSchemaDirty}
|
||||||
startIcon={icon}
|
startIcon={icon}
|
||||||
title={item.title}
|
title={item.title}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,11 @@ import { EditableDismissibleTab } from "@appsmith/ads";
|
||||||
|
|
||||||
import { type EntityItem } from "ee/IDE/Interfaces/EntityItem";
|
import { type EntityItem } from "ee/IDE/Interfaces/EntityItem";
|
||||||
import { FEATURE_FLAG } from "ee/entities/FeatureFlag";
|
import { FEATURE_FLAG } from "ee/entities/FeatureFlag";
|
||||||
import { getIsSavingEntityName } from "ee/selectors/entitiesSelector";
|
import {
|
||||||
|
getActionSchemaDirtyState,
|
||||||
|
getIsSavingEntityName,
|
||||||
|
getJSCollectionSchemaDirtyState,
|
||||||
|
} from "ee/selectors/entitiesSelector";
|
||||||
|
|
||||||
import { useFeatureFlag } from "utils/hooks/useFeatureFlag";
|
import { useFeatureFlag } from "utils/hooks/useFeatureFlag";
|
||||||
import { sanitizeString } from "utils/URLUtils";
|
import { sanitizeString } from "utils/URLUtils";
|
||||||
|
|
@ -61,6 +65,16 @@ export function EditableTab(props: EditableTabProps) {
|
||||||
[dispatch, entity, id, segment],
|
[dispatch, entity, id, segment],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const isJSActionSchemaDirty = useSelector((state) =>
|
||||||
|
getJSCollectionSchemaDirtyState(state, id),
|
||||||
|
);
|
||||||
|
|
||||||
|
const isActionSchemaDirty = useSelector((state) =>
|
||||||
|
getActionSchemaDirtyState(state, id),
|
||||||
|
);
|
||||||
|
|
||||||
|
const isSchemaDirty = isJSActionSchemaDirty || isActionSchemaDirty;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<EditableDismissibleTab
|
<EditableDismissibleTab
|
||||||
dataTestId={`t--ide-tab-${sanitizeString(title)}`}
|
dataTestId={`t--ide-tab-${sanitizeString(title)}`}
|
||||||
|
|
@ -75,6 +89,7 @@ export function EditableTab(props: EditableTabProps) {
|
||||||
onEnterEditMode={enterEditMode}
|
onEnterEditMode={enterEditMode}
|
||||||
onExitEditMode={exitEditMode}
|
onExitEditMode={exitEditMode}
|
||||||
onNameSave={handleNameSave}
|
onNameSave={handleNameSave}
|
||||||
|
showUnsavedChanges={isSchemaDirty}
|
||||||
validateName={validateName}
|
validateName={validateName}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -57,6 +57,7 @@ import {
|
||||||
getJSActionOption,
|
getJSActionOption,
|
||||||
type OnUpdateSettingsProps,
|
type OnUpdateSettingsProps,
|
||||||
} from "./JSEditorToolbar";
|
} from "./JSEditorToolbar";
|
||||||
|
import { getHighlightedLines } from "ee/pages/Editor/JSEditor/utils/getHighlightedLines";
|
||||||
|
|
||||||
interface JSFormProps {
|
interface JSFormProps {
|
||||||
jsCollectionData: JSCollectionData;
|
jsCollectionData: JSCollectionData;
|
||||||
|
|
@ -287,6 +288,8 @@ function JSEditorForm({
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const highlightedLines = getHighlightedLines(currentJSCollection);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (parseErrors.length || isEmpty(jsActions)) {
|
if (parseErrors.length || isEmpty(jsActions)) {
|
||||||
setDisableRunFunctionality(true);
|
setDisableRunFunctionality(true);
|
||||||
|
|
@ -373,6 +376,7 @@ function JSEditorForm({
|
||||||
currentJSCollection={currentJSCollection}
|
currentJSCollection={currentJSCollection}
|
||||||
customGutter={JSGutters}
|
customGutter={JSGutters}
|
||||||
executing={isExecutingCurrentJSAction}
|
executing={isExecutingCurrentJSAction}
|
||||||
|
highlightedLines={highlightedLines}
|
||||||
onChange={handleEditorChange}
|
onChange={handleEditorChange}
|
||||||
onUpdateSettings={onUpdateSettings}
|
onUpdateSettings={onUpdateSettings}
|
||||||
onValueChange={(string) =>
|
onValueChange={(string) =>
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ import { Flex } from "@appsmith/ads";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
executing: boolean;
|
executing: boolean;
|
||||||
|
highlightedLines?: number[];
|
||||||
onValueChange: (value: string) => void;
|
onValueChange: (value: string) => void;
|
||||||
value: JSEditorTab;
|
value: JSEditorTab;
|
||||||
showSettings: undefined | boolean;
|
showSettings: undefined | boolean;
|
||||||
|
|
@ -44,6 +45,7 @@ export const JSEditorForm = (props: Props) => {
|
||||||
folding
|
folding
|
||||||
height={"100%"}
|
height={"100%"}
|
||||||
hideEvaluatedValue
|
hideEvaluatedValue
|
||||||
|
highlightedLines={props.highlightedLines}
|
||||||
input={{
|
input={{
|
||||||
value: props.currentJSCollection.body,
|
value: props.currentJSCollection.body,
|
||||||
onChange: props.onChange,
|
onChange: props.onChange,
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,6 @@ import AppJSEditorContextMenu from "./AppJSEditorContextMenu";
|
||||||
import { updateFunctionProperty } from "actions/jsPaneActions";
|
import { updateFunctionProperty } from "actions/jsPaneActions";
|
||||||
import type { OnUpdateSettingsProps } from "./JSEditorToolbar";
|
import type { OnUpdateSettingsProps } from "./JSEditorToolbar";
|
||||||
import { saveJSObjectName } from "actions/jsActionActions";
|
import { saveJSObjectName } from "actions/jsActionActions";
|
||||||
|
|
||||||
const LoadingContainer = styled(CenteredWrapper)`
|
const LoadingContainer = styled(CenteredWrapper)`
|
||||||
height: 50%;
|
height: 50%;
|
||||||
`;
|
`;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user