diff --git a/app/client/packages/design-system/ads/src/Badge/Badge.styles.tsx b/app/client/packages/design-system/ads/src/Badge/Badge.styles.tsx
index 195913bd4ada..8be40659a473 100644
--- a/app/client/packages/design-system/ads/src/Badge/Badge.styles.tsx
+++ b/app/client/packages/design-system/ads/src/Badge/Badge.styles.tsx
@@ -1,5 +1,5 @@
import styled, { css } from "styled-components";
-import type { BadgeKind } from "./Badge.types";
+import type { BadgeKind, BadgeSize } from "./Badge.types";
const Kind = {
error: css`
@@ -11,14 +11,28 @@ const Kind = {
success: css`
--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<{
kind?: BadgeKind;
+ size?: BadgeSize;
}>`
- width: 8px;
- height: 8px;
background-color: var(--badge-color-bg);
border-radius: 50%;
${({ kind }) => kind && Kind[kind]}
+ ${({ size }) => size && Size[size]}
`;
diff --git a/app/client/packages/design-system/ads/src/Badge/Badge.tsx b/app/client/packages/design-system/ads/src/Badge/Badge.tsx
index 87da65a36012..e71659a470e5 100644
--- a/app/client/packages/design-system/ads/src/Badge/Badge.tsx
+++ b/app/client/packages/design-system/ads/src/Badge/Badge.tsx
@@ -10,6 +10,13 @@ import { StyledBadge } from "./Badge.styles";
* @param className
* @constructor
*/
-export function Badge({ className, kind = "success", ...rest }: BadgeProps) {
- return ;
+export function Badge({
+ className,
+ kind = "success",
+ size = "medium",
+ ...rest
+}: BadgeProps) {
+ return (
+
+ );
}
diff --git a/app/client/packages/design-system/ads/src/Badge/Badge.types.ts b/app/client/packages/design-system/ads/src/Badge/Badge.types.ts
index e7ce9a9b6944..f0771b817002 100644
--- a/app/client/packages/design-system/ads/src/Badge/Badge.types.ts
+++ b/app/client/packages/design-system/ads/src/Badge/Badge.types.ts
@@ -1,10 +1,13 @@
import type { Kind } from "../__config__/types";
-export type BadgeKind = Exclude;
+export type BadgeKind = Exclude;
+export type BadgeSize = "small" | "medium";
export interface BadgeProps {
/** visual style to be used indicating type of badge */
kind?: BadgeKind;
/** (try not to) pass addition classes here */
className?: string;
+ /** Size of the badge */
+ size?: BadgeSize;
}
diff --git a/app/client/packages/design-system/ads/src/List/List.styles.tsx b/app/client/packages/design-system/ads/src/List/List.styles.tsx
index c42bdd78ee70..03074c703b45 100644
--- a/app/client/packages/design-system/ads/src/List/List.styles.tsx
+++ b/app/client/packages/design-system/ads/src/List/List.styles.tsx
@@ -44,6 +44,14 @@ export const RightControlWrapper = styled.div`
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`
display: flex;
align-items: center;
diff --git a/app/client/packages/design-system/ads/src/List/List.tsx b/app/client/packages/design-system/ads/src/List/List.tsx
index 7ae36989c255..47e60b309166 100644
--- a/app/client/packages/design-system/ads/src/List/List.tsx
+++ b/app/client/packages/design-system/ads/src/List/List.tsx
@@ -13,6 +13,7 @@ import {
StyledListItem,
TooltipTextWrapper,
TopContentWrapper,
+ UnsavedChangesWrapper,
} from "./List.styles";
import type { TextProps } from "../Text";
import { Text } from "../Text";
@@ -26,6 +27,7 @@ import {
ListItemTitleClassName,
} from "./List.constants";
import { useEventCallback } from "usehooks-ts";
+import { Badge } from "../Badge";
function List({ children, className, groupTitle, ...rest }: ListProps) {
return groupTitle ? (
@@ -86,6 +88,7 @@ function ListItem(props: ListItemProps) {
hasError,
rightControl,
rightControlVisibility = "always",
+ showUnsavedChanges,
size = "md",
startIcon,
title,
@@ -159,6 +162,11 @@ function ListItem(props: ListItemProps) {
{rightControl}
)}
+ {showUnsavedChanges ? (
+
+
+
+ ) : null}
{isBlockDescription && (
diff --git a/app/client/packages/design-system/ads/src/List/List.types.tsx b/app/client/packages/design-system/ads/src/List/List.types.tsx
index 51f08661d5c3..b8829243ca6b 100644
--- a/app/client/packages/design-system/ads/src/List/List.types.tsx
+++ b/app/client/packages/design-system/ads/src/List/List.types.tsx
@@ -38,6 +38,8 @@ export interface ListItemProps {
customTitleComponent?: ReactNode | ReactNode[];
/** dataTestId which will be used in automated tests */
dataTestId?: string;
+ /** Whether to show the unsaved changes indicator */
+ showUnsavedChanges?: boolean;
}
export interface ListProps {
diff --git a/app/client/packages/design-system/ads/src/Templates/EditableDismissibleTab/EditableDismissibleTab.tsx b/app/client/packages/design-system/ads/src/Templates/EditableDismissibleTab/EditableDismissibleTab.tsx
index 18eba022a6ae..58665d383d15 100644
--- a/app/client/packages/design-system/ads/src/Templates/EditableDismissibleTab/EditableDismissibleTab.tsx
+++ b/app/client/packages/design-system/ads/src/Templates/EditableDismissibleTab/EditableDismissibleTab.tsx
@@ -7,6 +7,7 @@ import { EditableEntityName } from "../EditableEntityName";
import type { EditableDismissibleTabProps } from "./EditableDismissibleTab.types";
import { useActiveDoubleClick } from "../../__hooks__";
+import { Badge } from "../../Badge";
export const EditableDismissibleTab = (props: EditableDismissibleTabProps) => {
const {
@@ -22,6 +23,7 @@ export const EditableDismissibleTab = (props: EditableDismissibleTabProps) => {
onEnterEditMode: propOnEnterEditMode,
onExitEditMode: propOnExitEditMode,
onNameSave,
+ showUnsavedChanges,
validateName,
} = props;
@@ -60,6 +62,7 @@ export const EditableDismissibleTab = (props: EditableDismissibleTabProps) => {
onNameSave={onNameSave}
validateName={validateName}
/>
+ {showUnsavedChanges ? : null}
);
};
diff --git a/app/client/packages/design-system/ads/src/Templates/EditableDismissibleTab/EditableDismissibleTab.types.ts b/app/client/packages/design-system/ads/src/Templates/EditableDismissibleTab/EditableDismissibleTab.types.ts
index f2cdbb8db267..c1ec71a4c33a 100644
--- a/app/client/packages/design-system/ads/src/Templates/EditableDismissibleTab/EditableDismissibleTab.types.ts
+++ b/app/client/packages/design-system/ads/src/Templates/EditableDismissibleTab/EditableDismissibleTab.types.ts
@@ -28,4 +28,6 @@ export interface EditableDismissibleTabProps {
onNameSave: (name: string) => void;
/** Function to validate the name. */
validateName: (name: string) => string | null;
+ /** Show unsaved changes indicator. */
+ showUnsavedChanges?: boolean;
}
diff --git a/app/client/src/ce/pages/Editor/JSEditor/utils/getHighlightedLines.ts b/app/client/src/ce/pages/Editor/JSEditor/utils/getHighlightedLines.ts
new file mode 100644
index 000000000000..51bc50ad1b77
--- /dev/null
+++ b/app/client/src/ce/pages/Editor/JSEditor/utils/getHighlightedLines.ts
@@ -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 [];
+};
diff --git a/app/client/src/ce/selectors/entitiesSelector.ts b/app/client/src/ce/selectors/entitiesSelector.ts
index 7868eac45bfb..802eea3d00b7 100644
--- a/app/client/src/ce/selectors/entitiesSelector.ts
+++ b/app/client/src/ce/selectors/entitiesSelector.ts
@@ -454,6 +454,12 @@ export const getActions = (state: AppState): ActionDataState =>
export const getJSCollections = (state: AppState): JSCollectionDataState =>
state.entities.jsActions;
+export const getAllJSCollectionActions = (state: AppState) => {
+ return state.entities.jsActions.flatMap(
+ (jsCollection) => jsCollection.config.actions,
+ );
+};
+
export const getDatasource = (
state: AppState,
datasourceId: string,
@@ -1752,3 +1758,40 @@ export const getIsSavingEntityName = (
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;
+ },
+);
diff --git a/app/client/src/components/editorComponents/CodeEditor/index.tsx b/app/client/src/components/editorComponents/CodeEditor/index.tsx
index 9c4719c0d0a0..705ecf6273df 100644
--- a/app/client/src/components/editorComponents/CodeEditor/index.tsx
+++ b/app/client/src/components/editorComponents/CodeEditor/index.tsx
@@ -260,6 +260,7 @@ export type EditorProps = EditorStyleProps &
removeHoverAndFocusStyle?: boolean;
customErrors?: LintError[];
+ highlightedLines?: number[]; // Array of line numbers to highlight
};
interface Props extends ReduxStateProps, EditorProps, ReduxDispatchProps {}
@@ -445,11 +446,11 @@ class CodeEditor extends Component {
this: CodeEditor,
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.
//
// 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
// changes here are completed
//
@@ -500,7 +501,13 @@ class CodeEditor extends Component {
// Finally create the Codemirror editor
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()`.
}
@@ -612,6 +619,11 @@ class CodeEditor extends Component {
}
}
+ // Check if highlighted lines have changed
+ if (!isEqual(prevProps.highlightedLines, this.props.highlightedLines)) {
+ this.updateLineHighlighting(this.props.highlightedLines || []);
+ }
+
this.editor.operation(() => {
const editorValue = this.editor.getValue();
// Safe update of value of the editor when value updated outside the editor
@@ -1634,6 +1646,21 @@ class CodeEditor extends Component {
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() {
const {
border,
diff --git a/app/client/src/components/editorComponents/CodeEditor/styledComponents.ts b/app/client/src/components/editorComponents/CodeEditor/styledComponents.ts
index 6df20f60ac43..6e0621b99b78 100644
--- a/app/client/src/components/editorComponents/CodeEditor/styledComponents.ts
+++ b/app/client/src/components/editorComponents/CodeEditor/styledComponents.ts
@@ -256,6 +256,10 @@ export const EditorWrapper = styled.div<{
.CodeMirror-activeline-background {
background-color: #ececec;
}
+
+ .highlighted-line {
+ background-color: rgba(255, 255, 0, 0.2);
+ }
}
.CodeMirror-guttermarker-subtle {
color: var(--ads-v2-color-fg-subtle);
diff --git a/app/client/src/ee/pages/Editor/JSEditor/utils/getHighlightedLines.ts b/app/client/src/ee/pages/Editor/JSEditor/utils/getHighlightedLines.ts
new file mode 100644
index 000000000000..f6b1a7cf15b8
--- /dev/null
+++ b/app/client/src/ee/pages/Editor/JSEditor/utils/getHighlightedLines.ts
@@ -0,0 +1 @@
+export { getHighlightedLines } from "ce/pages/Editor/JSEditor/utils/getHighlightedLines";
diff --git a/app/client/src/pages/AppIDE/components/JSEntityItem/JSEntityItem.tsx b/app/client/src/pages/AppIDE/components/JSEntityItem/JSEntityItem.tsx
index 24a6599f25fd..eea5b809cfdc 100644
--- a/app/client/src/pages/AppIDE/components/JSEntityItem/JSEntityItem.tsx
+++ b/app/client/src/pages/AppIDE/components/JSEntityItem/JSEntityItem.tsx
@@ -1,7 +1,10 @@
import React, { useCallback, useMemo } from "react";
import { EntityItem, EntityContextMenu } from "@appsmith/ads";
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 { useFeatureFlag } from "utils/hooks/useFeatureFlag";
import { FEATURE_FLAG } from "ee/entities/FeatureFlag";
@@ -105,6 +108,10 @@ export const JSEntityItem = ({ item }: { item: EntityItemProps }) => {
validateName,
]);
+ const isJSActionSchemaDirty = useSelector((state: AppState) =>
+ getJSCollectionSchemaDirtyState(state, item.key),
+ );
+
return (
{
onDoubleClick={() => enterEditMode(jsAction.id)}
rightControl={contextMenu}
rightControlVisibility="hover"
+ showUnsavedChanges={isJSActionSchemaDirty}
startIcon={JsFileIconV2(16, 16)}
title={item.title}
/>
diff --git a/app/client/src/pages/AppIDE/components/QueryEntityItem/QueryEntityItem.tsx b/app/client/src/pages/AppIDE/components/QueryEntityItem/QueryEntityItem.tsx
index 3574a814f255..5a8e23501a0f 100644
--- a/app/client/src/pages/AppIDE/components/QueryEntityItem/QueryEntityItem.tsx
+++ b/app/client/src/pages/AppIDE/components/QueryEntityItem/QueryEntityItem.tsx
@@ -3,6 +3,7 @@ import { EntityItem, EntityContextMenu } from "@appsmith/ads";
import type { AppState } from "ee/reducers";
import {
getActionByBaseId,
+ getActionSchemaDirtyState,
getDatasource,
getPlugins,
} from "ee/selectors/entitiesSelector";
@@ -117,6 +118,10 @@ export const QueryEntityItem = ({ item }: { item: EntityItemProps }) => {
validateName,
]);
+ const isActionSchemaDirty = useSelector((state: AppState) =>
+ getActionSchemaDirtyState(state, action.id),
+ );
+
return (
{
onDoubleClick={() => enterEditMode(action.id)}
rightControl={contextMenu}
rightControlVisibility="hover"
+ showUnsavedChanges={isActionSchemaDirty}
startIcon={icon}
title={item.title}
/>
diff --git a/app/client/src/pages/AppIDE/layouts/components/EditorTabs/EditableTab.tsx b/app/client/src/pages/AppIDE/layouts/components/EditorTabs/EditableTab.tsx
index a4a9dea726d6..4c17d4dab5cf 100644
--- a/app/client/src/pages/AppIDE/layouts/components/EditorTabs/EditableTab.tsx
+++ b/app/client/src/pages/AppIDE/layouts/components/EditorTabs/EditableTab.tsx
@@ -6,7 +6,11 @@ import { EditableDismissibleTab } from "@appsmith/ads";
import { type EntityItem } from "ee/IDE/Interfaces/EntityItem";
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 { sanitizeString } from "utils/URLUtils";
@@ -61,6 +65,16 @@ export function EditableTab(props: EditableTabProps) {
[dispatch, entity, id, segment],
);
+ const isJSActionSchemaDirty = useSelector((state) =>
+ getJSCollectionSchemaDirtyState(state, id),
+ );
+
+ const isActionSchemaDirty = useSelector((state) =>
+ getActionSchemaDirtyState(state, id),
+ );
+
+ const isSchemaDirty = isJSActionSchemaDirty || isActionSchemaDirty;
+
return (
);
diff --git a/app/client/src/pages/Editor/JSEditor/Form.tsx b/app/client/src/pages/Editor/JSEditor/Form.tsx
index 1cc944e0226c..cf71aa1564a7 100644
--- a/app/client/src/pages/Editor/JSEditor/Form.tsx
+++ b/app/client/src/pages/Editor/JSEditor/Form.tsx
@@ -57,6 +57,7 @@ import {
getJSActionOption,
type OnUpdateSettingsProps,
} from "./JSEditorToolbar";
+import { getHighlightedLines } from "ee/pages/Editor/JSEditor/utils/getHighlightedLines";
interface JSFormProps {
jsCollectionData: JSCollectionData;
@@ -287,6 +288,8 @@ function JSEditorForm({
}
};
+ const highlightedLines = getHighlightedLines(currentJSCollection);
+
useEffect(() => {
if (parseErrors.length || isEmpty(jsActions)) {
setDisableRunFunctionality(true);
@@ -373,6 +376,7 @@ function JSEditorForm({
currentJSCollection={currentJSCollection}
customGutter={JSGutters}
executing={isExecutingCurrentJSAction}
+ highlightedLines={highlightedLines}
onChange={handleEditorChange}
onUpdateSettings={onUpdateSettings}
onValueChange={(string) =>
diff --git a/app/client/src/pages/Editor/JSEditor/JSEditorForm/JSEditorForm.tsx b/app/client/src/pages/Editor/JSEditor/JSEditorForm/JSEditorForm.tsx
index ec3b5865022d..c851f7bccb18 100644
--- a/app/client/src/pages/Editor/JSEditor/JSEditorForm/JSEditorForm.tsx
+++ b/app/client/src/pages/Editor/JSEditor/JSEditorForm/JSEditorForm.tsx
@@ -16,6 +16,7 @@ import { Flex } from "@appsmith/ads";
interface Props {
executing: boolean;
+ highlightedLines?: number[];
onValueChange: (value: string) => void;
value: JSEditorTab;
showSettings: undefined | boolean;
@@ -44,6 +45,7 @@ export const JSEditorForm = (props: Props) => {
folding
height={"100%"}
hideEvaluatedValue
+ highlightedLines={props.highlightedLines}
input={{
value: props.currentJSCollection.body,
onChange: props.onChange,
diff --git a/app/client/src/pages/Editor/JSEditor/index.tsx b/app/client/src/pages/Editor/JSEditor/index.tsx
index adee1b954e26..bc696590ca16 100644
--- a/app/client/src/pages/Editor/JSEditor/index.tsx
+++ b/app/client/src/pages/Editor/JSEditor/index.tsx
@@ -12,7 +12,6 @@ import AppJSEditorContextMenu from "./AppJSEditorContextMenu";
import { updateFunctionProperty } from "actions/jsPaneActions";
import type { OnUpdateSettingsProps } from "./JSEditorToolbar";
import { saveJSObjectName } from "actions/jsActionActions";
-
const LoadingContainer = styled(CenteredWrapper)`
height: 50%;
`;