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%; `;