Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Draft] Sticky notes #7149

Closed
wants to merge 21 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
cae10e8
Create StickyNotes DB entity
philemone Oct 28, 2024
fe09855
Add PoC httpApiService for StickyNotes - add slick migration
philemone Nov 4, 2024
5752344
Add put and delete endpoints, use value classes for ID in stickyNotes…
philemone Nov 6, 2024
e76d963
Add StickyNote PoC to FE
philemone Nov 8, 2024
562dcbe
Resize and update StickyNotes
philemone Nov 12, 2024
3805074
Handle note removal
philemone Nov 13, 2024
7dae014
Remove some unused fragments
philemone Nov 13, 2024
ee9734c
Update openApi definition
philemone Nov 13, 2024
c5f4e9a
Resize stickyNote without visual-lag
philemone Nov 14, 2024
698982d
Disable stickyNotes when scenario is not saved
philemone Nov 14, 2024
76c528b
Show/hide tools on graph actions
philemone Nov 14, 2024
0bf0c49
Edit stickyNote on graph
philemone Nov 14, 2024
f7582f2
Fix some suggestions made by rabbit
philemone Nov 15, 2024
507f2dc
Update openApi definitions
philemone Nov 15, 2024
3961433
Add white characters to textarea in stickyNote markdown editor
philemone Nov 15, 2024
da4d8db
Allow focus to stay in markdown editor
philemone Nov 19, 2024
cdf599c
Allow switch viewer to editor witgh left mouse click
philemone Nov 19, 2024
4bf3249
Add stickyNotes length and count validation, add stickyNotes configur…
philemone Nov 19, 2024
e239e73
Remove node specific code from StickyNotePreview, update openApi defs
philemone Nov 20, 2024
b3c74ed
Add some fixes, improve types, add max width and height for stickyNote
philemone Nov 20, 2024
894ecb3
Add STICKY_NOTE_CONSTRAINTS with config values
philemone Nov 20, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions designer/client/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions designer/client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"d3-transition": "3.0.1",
"d3-zoom": "3.0.0",
"dagre": "0.8.5",
"dompurify": "3.2.0",
"event-from": "1.0.0",
"file-saver": "2.0.5",
"flattenizer": "1.1.1",
Expand Down
2 changes: 2 additions & 0 deletions designer/client/src/actions/actionTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ export type ActionTypes =
| "NODES_DISCONNECTED"
| "NODE_ADDED"
| "NODES_WITH_EDGES_ADDED"
| "STICKY_NOTES_UPDATED"
| "STICKY_NOTE_DELETED"
| "VALIDATION_RESULT"
| "COPY_SELECTION"
| "CUT_SELECTION"
Expand Down
48 changes: 48 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,50 @@ export function displayTestCapabilities(processName: ProcessName, scenarioGraph:
);
}

export function fetchStickyNotesForScenario(scenarioName: string, scenarioVersionId: number): ThunkAction {
return (dispatch) => {
HttpService.getStickyNotes(scenarioName, scenarioVersionId).then((stickyNotes) => {
dispatch({ type: "STICKY_NOTES_UPDATED", stickyNotes: stickyNotes.data });
});
};
}

export function stickyNoteUpdated(scenarioName: string, scenarioVersionId: number, stickyNote: StickyNote): ThunkAction {
return (dispatch) => {
HttpService.updateStickyNote(scenarioName, scenarioVersionId, stickyNote).then((_) => {
HttpService.getStickyNotes(scenarioName, scenarioVersionId).then((stickyNotes) => {
flushSync(() => {
dispatch({ type: "STICKY_NOTES_UPDATED", stickyNotes: stickyNotes.data });
dispatch(layoutChanged());
});
});
});
};
}

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((_) => {
HttpService.getStickyNotes(scenarioName, scenarioVersionId).then((stickyNotes) => {
flushSync(() => {
dispatch({ type: "STICKY_NOTES_UPDATED", stickyNotes: stickyNotes.data });
dispatch(layoutChanged());
});
});
});
};
}

export function displayCurrentProcessVersion(processName: ProcessName) {
return fetchProcessToDisplay(processName);
}
Expand Down
5 changes: 3 additions & 2 deletions designer/client/src/actions/notificationActions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,11 @@ export function success(message: string): Action {
});
}

export function error(message: string): Action {
//TODO please take a look at this method and my changes, am I wrong or was it incomplete (without `error` and `showErrorText`) and had incomplete logic
export function error(message: string, error?: string, showErrorText?: boolean): Action {
return Notifications.error({
autoDismiss: 10,
children: <Notification type={"error"} icon={<InfoOutlinedIcon />} message={message} />,
children: <Notification type={"error"} icon={<InfoOutlinedIcon />} message={showErrorText && error ? error : message} />,
});
}

Expand Down
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
15 changes: 15 additions & 0 deletions designer/client/src/common/StickyNote.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { LayoutData } from "../types";

export type Dimensions = { width: number; height: number };

export interface StickyNote {
id?: string;
noteId: number;
content: string;
layoutData: LayoutData;
dimensions: Dimensions;
color: string;
targetEdge?: string;
editedBy: string;
editedAt: string;
}
7 changes: 6 additions & 1 deletion designer/client/src/components/ComponentPreview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import { ComponentIcon } from "./toolbars/creator/ComponentIcon";
import { alpha, styled, useTheme } from "@mui/material";
import { blend } from "@mui/system";
import { blendLighten, getBorderColor } from "../containers/theme/helpers";
import { StickyNotePreview } from "./StickyNotePreview";
import { StickyNoteType } from "../types/stickyNote";

export function ComponentPreview({ node, isActive, isOver }: { node: NodeType; isActive?: boolean; isOver?: boolean }): JSX.Element {
const theme = useTheme();
Expand Down Expand Up @@ -74,7 +76,10 @@ export function ComponentPreview({ node, isActive, isOver }: { node: NodeType; i
}));

const colors = isOver ? nodeColorsHover : nodeColors;
return (

return node?.type === StickyNoteType ? (
<StickyNotePreview isActive={isActive} isOver={isOver} />
) : (
<div className={cx(colors, nodeStyles)}>
<div className={cx(imageStyles, imageColors)}>
<ComponentIcon node={node} />
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";

export function StickyNotePreview({ isActive, isOver }: { isActive?: boolean; isOver?: boolean }): JSX.Element {
const theme = useTheme();

const PREVIEW_SCALE = 0.9;
const ACTIVE_ROTATION = 2;
const INACTIVE_SCALE = 1.5;
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>
);
}
129 changes: 129 additions & 0 deletions designer/client/src/components/graph/EspNode/stickyNote.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import { Theme } from "@mui/material";
import { dia, shapes, util, V } from "jointjs";
import { getBorderColor } from "../../../containers/theme/helpers";
import { StickyNote } from "../../../common/StickyNote";
import { marked } from "marked";
import { StickyNoteElement } from "../StickyNoteElement";
import MarkupNodeJSON = dia.MarkupNodeJSON;
import DOMPurify from "dompurify";

export const STICKY_NOTE_CONSTRAINTS = {
MIN_WIDTH: 100,
MAX_WIDTH: 3000,
DEFAULT_WIDTH: 300,
MIN_HEIGHT: 100,
MAX_HEIGHT: 3000,
DEFAULT_HEIGHT: 250,
} as const;

export const BORDER_RADIUS = 3;
export const CONTENT_PADDING = 5;
export const ICON_SIZE = 20;
export const STICKY_NOTE_DEFAULT_COLOR = "#13130d";
export const MARKDOWN_EDITOR_NAME = "markdown-editor";

const border: dia.MarkupNodeJSON = {
selector: "border",
tagName: "path",
className: "body",
attributes: {
width: STICKY_NOTE_CONSTRAINTS.DEFAULT_WIDTH,
height: STICKY_NOTE_CONSTRAINTS.DEFAULT_HEIGHT,
strokeWidth: 1,
fill: "none",
rx: BORDER_RADIUS,
},
};

const icon: dia.MarkupNodeJSON = {
selector: "icon",
tagName: "use",
attributes: {
opacity: 1,
width: ICON_SIZE,
height: ICON_SIZE,
x: ICON_SIZE / 2,
y: ICON_SIZE / 2,
},
};

const body: dia.MarkupNodeJSON = {
selector: "body",
tagName: "path",
};

const renderer = new marked.Renderer();
renderer.link = function (href, title, text) {
return `<a target="_blank" rel="noopener noreferrer" href="${href}">${text}</a>`;
};
renderer.image = function (href, title, text) {
// SVG don't support HTML img inside foreignObject
return `<a target="_blank" rel="noopener noreferrer" href="${href}">${text} (attached img)</a>`;
};

const foreignObject = (stickyNote: StickyNote): MarkupNodeJSON => {
let parsed;
try {
parsed = DOMPurify.sanitize(marked.parse(stickyNote.content, { renderer }));
} catch (error) {
console.error("Failed to parse markdown:", error);
parsed = "Error: Could not parse content. See error logs in console";
}
const singleMarkupNode = util.svg/* xml */ `
<foreignObject @selector="foreignObject">
<div @selector="sticky-note-content" class="sticky-note-content">
<textarea @selector="${MARKDOWN_EDITOR_NAME}" class="sticky-note-markdown-editor" name="${MARKDOWN_EDITOR_NAME}" autocomplete="off" disabled="disabled"></textarea>
<div @selector="markdown" class="sticky-note-markdown">${parsed}</div>
</div>
</foreignObject>
`[0];
return singleMarkupNode as MarkupNodeJSON;
};

export const stickyNotePath = "M 0 0 L 10 0 C 10 2.6667 10 5.3333 10 8 C 10 10 9 10 8 10 L 0 10 L 0 0";

const defaults = (theme: Theme) =>
util.defaultsDeep(
{
size: {
width: STICKY_NOTE_CONSTRAINTS.DEFAULT_WIDTH,
height: STICKY_NOTE_CONSTRAINTS.DEFAULT_HEIGHT,
},
attrs: {
body: {
refD: stickyNotePath,
strokeWidth: 2,
fill: "#eae672",
filter: {
name: "dropShadow",
args: {
dx: 1,
dy: 1,
blur: 5,
opacity: 0.4,
},
},
},
foreignObject: {
width: STICKY_NOTE_CONSTRAINTS.DEFAULT_WIDTH,
height: STICKY_NOTE_CONSTRAINTS.DEFAULT_HEIGHT - ICON_SIZE - CONTENT_PADDING * 4,
y: CONTENT_PADDING * 4 + ICON_SIZE,
fill: getBorderColor(theme),
},
border: {
refD: stickyNotePath,
stroke: getBorderColor(theme),
},
},
},
shapes.devs.Model.prototype.defaults,
);

const protoProps = (theme: Theme, stickyNote: StickyNote) => {
return {
markup: [body, border, foreignObject(stickyNote), icon],
};
};

export const StickyNoteShape = (theme: Theme, stickyNote: StickyNote) =>
StickyNoteElement(defaults(theme), protoProps(theme, stickyNote)) as typeof shapes.devs.Model;
Loading
Loading