Skip to content

Commit

Permalink
Sticky notes (#7277)
Browse files Browse the repository at this point in the history
* Create StickyNotes DB entity

* Add PoC httpApiService for StickyNotes - add slick migration

* Add put and delete endpoints, use value classes for ID in stickyNotes model

* Add StickyNote PoC to FE

* Resize and update StickyNotes

* Handle note removal

* Remove some unused fragments

* Update openApi definition

* Resize stickyNote without visual-lag

* Disable stickyNotes when scenario is not saved

* Show/hide tools on graph actions

* Edit stickyNote on graph

* Fix some suggestions made by rabbit

* Update openApi definitions

* Add white characters to textarea in stickyNote markdown editor

* Allow focus to stay in markdown editor

* Allow switch viewer to editor witgh left mouse click

* Add stickyNotes length and count validation, add stickyNotes configuration, fix error method (or did i?)

* Remove node specific code from StickyNotePreview, update openApi defs

* Add some fixes, improve types, add max width and height for stickyNote

* Add STICKY_NOTE_CONSTRAINTS with config values

* Reuse common code, restore default color, remove duplicated update method

* Restore CSS class, remove unused imports

* Remove stickyNotePanel, add stickyNote to creatos panel

* Update cypress test

* Add Changelog and migrationGuide entries

* Updated snapshots (#7275)

Co-authored-by: philemone <[email protected]>

* remove space

* Remove DOMpurify and use XSS instead

* Remove StickyNotePreview from ComponentPreview

* Rename stickyNotes panel to sticky notes

* Fix markdown preview

* Change shape of stickyNote, fix 'changed' detection for notes

* Add notifications to graph, prevent from making changes to old stickyNotes versions

* Revert fix in error notification msg

* Fix warn messages

* Add strict type for color #xxxxxx, remove unnecessary generated noteId, add more optimistic tests

* Update pointer, add cypress tests, remove todo from uncomplete method, remove unused dompurify from package

* Add regex in sticky note tests for note id

* Change remove tool color and margins, update cypress tests

* update cypress tests

* Add maxDiffThreshold to flaky cypress test

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: philemone <[email protected]>
  • Loading branch information
3 people authored Jan 14, 2025
1 parent 8675c17 commit 16d7ca5
Show file tree
Hide file tree
Showing 69 changed files with 2,898 additions and 72 deletions.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion designer/client/cypress/e2e/components.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,7 @@ describe("Components list", () => {
cy.matchQuery("?TEXT=xxx");
cy.viewport(1600, 500);
cy.wait(500); //ensure "loading" mask is hidden
cy.get("#app-container>main").matchImage();
cy.get("#app-container>main").matchImage({ maxDiffThreshold: 0.01 });
});

it("should allow filtering by processing mode", () => {
Expand Down
1 change: 1 addition & 0 deletions designer/client/cypress/e2e/creatorToolbar.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ describe("Creator toolbar", () => {
cy.contains(/^types$/i).click();
cy.contains(/^services$/i).click();
cy.contains(/^sinks$/i).click();
cy.contains(/^sticky notes$/i).click();
cy.reload();
cy.get("@toolbar").matchImage();
cy.get("@toolbar").find("input").type("var");
Expand Down
65 changes: 65 additions & 0 deletions designer/client/cypress/e2e/stickyNotes.cy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
describe("Sticky notes", () => {
const seed = "stickyNotes";

before(() => {
cy.deleteAllTestProcesses({ filter: seed, force: true });
});

beforeEach(() => {
cy.visitNewProcess(seed, "stickyNotes");
});

const screenshotOptions: Cypress.MatchImageOptions = {
screenshotConfig: { clip: { x: 0, y: 0, width: 1400, height: 600 } },
};

it("should allow to drag sticky note", () => {
cy.layoutScenario();
cy.contains(/^sticky notes$/i)
.should("exist")
.scrollIntoView();
cy.get("[data-testid='component:sticky note']")
.should("be.visible")
.drag("#nk-graph-main", {
target: {
x: 600,
y: 300,
},
force: true,
});

cy.get("[data-testid=graphPage]").matchImage(screenshotOptions);
});

it("should add text to note and display it as markdown", () => {
cy.layoutScenario();
cy.contains(/^sticky notes$/i)
.should("exist")
.scrollIntoView();
cy.get("[data-testid='component:sticky note']")
.should("be.visible")
.drag("#nk-graph-main", {
target: {
x: 600,
y: 300,
},
force: true,
});
cy.get(".sticky-note-content").dblclick();
cy.get(".sticky-note-content textarea").type("# Title\n- p1\n- p2\n\n[link](href)");
cy.get("[model-id='request']").click();
cy.get("[data-testid=graphPage]").matchImage(screenshotOptions);
});

it("should disable sticky note when scenario is not saved", () => {
cy.layoutScenario();
cy.contains(/^sticky notes$/i)
.should("exist")
.scrollIntoView();

cy.dragNode("request", { x: 600, y: 300 });

cy.get("[data-testid='component:sticky note']").should("have.class", "tool disabled");
cy.get("[data-testid=graphPage]").matchImage(screenshotOptions);
});
});
58 changes: 58 additions & 0 deletions designer/client/cypress/fixtures/stickyNotes.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
{
"metaData": {
"id": "sticky",
"additionalFields": {
"description": null,
"properties": {
"inputSchema": "{}",
"outputSchema": "{}",
"slug": "sticky"
},
"metaDataType": "RequestResponseMetaData",
"showDescription": false
}
},
"nodes": [
{
"id": "request",
"ref": {
"typ": "request",
"parameters": []
},
"additionalFields": {
"description": null,
"layoutData": {
"x": 0,
"y": 0
}
},
"type": "Source"
},
{
"id": "response",
"ref": {
"typ": "response",
"parameters": [
{
"name": "Raw editor",
"expression": {
"language": "spel",
"expression": "false"
}
}
]
},
"endResult": null,
"isDisabled": null,
"additionalFields": {
"description": null,
"layoutData": {
"x": 0,
"y": 180
}
},
"type": "Sink"
}
],
"additionalBranches": []
}
2 changes: 2 additions & 0 deletions designer/client/src/actions/actionTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ export type ActionTypes =
| "DELETE_NODES"
| "NODES_CONNECTED"
| "NODES_DISCONNECTED"
| "STICKY_NOTES_UPDATED"
| "STICKY_NOTE_DELETED"
| "VALIDATION_RESULT"
| "COPY_SELECTION"
| "CUT_SELECTION"
Expand Down
7 changes: 7 additions & 0 deletions designer/client/src/actions/nk/assignSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,13 @@ export type FeaturesSettings = {
redirectAfterArchive: boolean;
usageStatisticsReports: UsageStatisticsReports;
surveySettings: SurveySettings;
stickyNotesSettings: StickyNotesSettings;
};

export type StickyNotesSettings = {
maxContentLength: number;
maxNotesCount: number;
enabled: boolean;
};

export type TestDataSettings = {
Expand Down
43 changes: 43 additions & 0 deletions designer/client/src/actions/nk/process.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ import { getProcessDefinitionData } from "../../reducers/selectors/settings";
import { ProcessDefinitionData, ScenarioGraph } from "../../types";
import { ThunkAction } from "../reduxTypes";
import HttpService from "./../../http/HttpService";
import { layoutChanged, Position } from "./ui/layout";
import { flushSync } from "react-dom";
import { Dimensions, StickyNote } from "../../common/StickyNote";

export type ScenarioActions =
| { type: "CORRECT_INVALID_SCENARIO"; processDefinitionData: ProcessDefinitionData }
Expand All @@ -17,6 +20,7 @@ export function fetchProcessToDisplay(processName: ProcessName, versionId?: Proc

return HttpService.fetchProcessDetails(processName, versionId).then((response) => {
dispatch(displayTestCapabilities(processName, response.data.scenarioGraph));
dispatch(fetchStickyNotesForScenario(processName, response.data.processVersionId));
dispatch({
type: "DISPLAY_PROCESS",
scenario: response.data,
Expand Down Expand Up @@ -56,6 +60,45 @@ export function displayTestCapabilities(processName: ProcessName, scenarioGraph:
);
}

const refreshStickyNotes = (dispatch, scenarioName: string, scenarioVersionId: number) => {
return HttpService.getStickyNotes(scenarioName, scenarioVersionId).then((stickyNotes) => {
flushSync(() => {
dispatch({ type: "STICKY_NOTES_UPDATED", stickyNotes: stickyNotes.data });
dispatch(layoutChanged());
});
});
};

export function fetchStickyNotesForScenario(scenarioName: string, scenarioVersionId: number): ThunkAction {
return (dispatch) => refreshStickyNotes(dispatch, scenarioName, scenarioVersionId);
}

export function stickyNoteUpdated(scenarioName: string, scenarioVersionId: number, stickyNote: StickyNote): ThunkAction {
return (dispatch) => {
HttpService.updateStickyNote(scenarioName, scenarioVersionId, stickyNote).then((_) => {
refreshStickyNotes(dispatch, scenarioName, scenarioVersionId);
});
};
}

export function stickyNoteDeleted(scenarioName: string, stickyNoteId: number): ThunkAction {
return (dispatch) => {
HttpService.deleteStickyNote(scenarioName, stickyNoteId).then(() => {
flushSync(() => {
dispatch({ type: "STICKY_NOTE_DELETED", stickyNoteId });
});
});
};
}

export function stickyNoteAdded(scenarioName: string, scenarioVersionId: number, position: Position, dimensions: Dimensions): ThunkAction {
return (dispatch) => {
HttpService.addStickyNote(scenarioName, scenarioVersionId, position, dimensions).then((_) => {
refreshStickyNotes(dispatch, scenarioName, scenarioVersionId);
});
};
}

export function displayCurrentProcessVersion(processName: ProcessName) {
return fetchProcessToDisplay(processName);
}
Expand Down
8 changes: 8 additions & 0 deletions designer/client/src/actions/notificationActions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import React from "react";
import Notifications from "react-notification-system-redux";
import CheckCircleOutlinedIcon from "@mui/icons-material/CheckCircleOutlined";
import InfoOutlinedIcon from "@mui/icons-material/InfoOutlined";
import WarningAmberOutlinedIcon from "@mui/icons-material/WarningAmberOutlined";
import Notification from "../components/notifications/Notification";
import { Action } from "./reduxTypes";

Expand All @@ -25,3 +26,10 @@ export function info(message: string): Action {
children: <Notification type={"info"} icon={<InfoOutlinedIcon />} message={message} />,
});
}

export function warn(message: string): Action {
return Notifications.warning({
autoDismiss: 10,
children: <Notification type={"warning"} icon={<WarningAmberOutlinedIcon />} message={message} />,
});
}
3 changes: 3 additions & 0 deletions designer/client/src/assets/json/nodeAttributes.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@
"Aggregate": {
"name": "Aggregate"
},
"StickyNote": {
"name": "StickyNote"
},
"CustomNode": {
"name": "CustomNode"
},
Expand Down
16 changes: 16 additions & 0 deletions designer/client/src/common/StickyNote.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { LayoutData } from "../types";

export type Dimensions = { width: number; height: number };
export type ColorValueHex = `#${string}`;

export interface StickyNote {
id?: string;
noteId: number;
content: string;
layoutData: LayoutData;
dimensions: Dimensions;
color: ColorValueHex;
targetEdge?: string;
editedBy: string;
editedAt: string;
}
35 changes: 21 additions & 14 deletions designer/client/src/components/ComponentDragPreview.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { css } from "@emotion/css";
import React, { forwardRef, useEffect, useMemo, useState } from "react";
import React, { forwardRef, ReactPortal, useEffect, useMemo, useState } from "react";
import { useDragDropManager, useDragLayer } from "react-dnd";
import { createPortal } from "react-dom";
import { useDebouncedValue } from "rooks";
import { NodeType } from "../types";
import { ComponentPreview } from "./ComponentPreview";
import { DndTypes } from "./toolbars/creator/Tool";
import { StickyNotePreview } from "./StickyNotePreview";
import { StickyNoteType } from "../types/stickyNote";

function useNotNull<T>(value: T) {
const [current, setCurrent] = useState(() => value);
Expand Down Expand Up @@ -53,17 +55,22 @@ export const ComponentDragPreview = forwardRef<HTMLDivElement, { scale: () => nu
return null;
}

return createPortal(
<div ref={forwardedRef} className={wrapperStyles} style={{ transform: `translate(${x}px, ${y}px)` }}>
<div
style={{
transformOrigin: "top left",
transform: `scale(${scale()})`,
}}
>
<ComponentPreview node={node} isActive={active} isOver={isOver} />
</div>
</div>,
document.body,
);
function createPortalForPreview(child: JSX.Element): ReactPortal {
return createPortal(
<div ref={forwardedRef} className={wrapperStyles} style={{ transform: `translate(${x}px, ${y}px)` }}>
<div
style={{
transformOrigin: "top left",
transform: `scale(${scale()})`,
}}
>
{child}
</div>
</div>,
document.body,
);
}

if (node?.type === StickyNoteType) return createPortalForPreview(<StickyNotePreview isActive={active} isOver={isOver} />);
return createPortalForPreview(<ComponentPreview node={node} isActive={active} isOver={isOver} />);
});
1 change: 1 addition & 0 deletions designer/client/src/components/ComponentPreview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ export function ComponentPreview({ node, isActive, isOver }: { node: NodeType; i
}));

const colors = isOver ? nodeColorsHover : nodeColors;

return (
<div className={cx(colors, nodeStyles)}>
<div className={cx(imageStyles, imageColors)}>
Expand Down
62 changes: 62 additions & 0 deletions designer/client/src/components/StickyNotePreview.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { css, cx } from "@emotion/css";
import React from "react";
import { BORDER_RADIUS, CONTENT_PADDING, iconBackgroundSize, iconSize } from "./graph/EspNode/esp";
import { PreloadedIcon, stickyNoteIconSrc } from "./toolbars/creator/ComponentIcon";
import { alpha, useTheme } from "@mui/material";
import { getBorderColor, getStickyNoteBackgroundColor } from "../containers/theme/helpers";
import { STICKY_NOTE_CONSTRAINTS, STICKY_NOTE_DEFAULT_COLOR } from "./graph/EspNode/stickyNote";

const PREVIEW_SCALE = 0.9;
const ACTIVE_ROTATION = 2;
const INACTIVE_SCALE = 1.5;

export function StickyNotePreview({ isActive, isOver }: { isActive?: boolean; isOver?: boolean }): JSX.Element {
const theme = useTheme();
const scale = isOver ? 1 : PREVIEW_SCALE;
const rotation = isActive ? (isOver ? -ACTIVE_ROTATION : ACTIVE_ROTATION) : 0;
const finalScale = isActive ? 1 : INACTIVE_SCALE;

const nodeStyles = css({
position: "relative",
width: STICKY_NOTE_CONSTRAINTS.DEFAULT_WIDTH,
height: STICKY_NOTE_CONSTRAINTS.DEFAULT_HEIGHT,
borderRadius: BORDER_RADIUS,
boxSizing: "content-box",
display: "inline-flex",
filter: `drop-shadow(0 4px 8px ${alpha(theme.palette.common.black, 0.5)})`,
borderWidth: 0.5,
borderStyle: "solid",
transformOrigin: "80% 50%",
transform: `translate(-80%, -50%) scale(${scale}) rotate(${rotation}deg) scale(${finalScale})`,
opacity: isActive ? undefined : 0,
transition: "all .5s, opacity .3s",
willChange: "transform, opacity, border-color, background-color",
});

const colors = css({
opacity: 0.5,
borderColor: getBorderColor(theme),
backgroundColor: getStickyNoteBackgroundColor(theme, STICKY_NOTE_DEFAULT_COLOR).main,
});

const imageStyles = css({
padding: iconSize / 2 - CONTENT_PADDING / 2,
margin: CONTENT_PADDING / 2,
borderRadius: BORDER_RADIUS,
width: iconBackgroundSize / 2,
height: iconBackgroundSize / 2,
color: theme.palette.common.black,
"> svg": {
height: iconSize,
width: iconSize,
},
});

return (
<div className={cx(colors, nodeStyles)}>
<div className={cx(imageStyles, colors)}>
<PreloadedIcon src={stickyNoteIconSrc} />
</div>
</div>
);
}
Loading

0 comments on commit 16d7ca5

Please sign in to comment.