Skip to content

Commit

Permalink
#1943 Feature request: Allow categories[0].node_types[0].app_data.ui_… (
Browse files Browse the repository at this point in the history
  • Loading branch information
tomlyn authored May 15, 2024
1 parent c2eafc4 commit b9e323c
Show file tree
Hide file tree
Showing 18 changed files with 915 additions and 48 deletions.
13 changes: 13 additions & 0 deletions canvas_modules/common-canvas/src/common-canvas/svg-canvas-d3.scss
Original file line number Diff line number Diff line change
Expand Up @@ -434,6 +434,19 @@ $link-highlight-color: $support-info;
color: $icon-primary;
}

// Styles for JSX node and decoration image. Making the svg
// inherit the width and height from the containing foreignObject
// will cause the SVG to stretch to whatever dimensions the app
// has set for the node or decoration image.
.d3-foreign-object-node-image,
.d3-foreign-object-dec-jsx {
outline: none;
svg {
width: inherit;
height: inherit;
}
}

// Remove the foreign object outline when the foreign object has focus.
.d3-foreign-object-dec-ext:focus {
outline: none;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -786,7 +786,6 @@ export default class SVGCanvasRenderer {
// is dragged from the palette, in case the dimensions of the ghost node
// have changed because the canvas has been zoomed.
getGhostNode(node) {
const that = this;
const ghostDivSel = this.getGhostDivSel();
const zoomScale = this.zoomUtils.getZoomScale();

Expand Down Expand Up @@ -823,13 +822,9 @@ export default class SVGCanvasRenderer {
.attr("height", node.height);

if (!this.nodeLayout.nodeExternalObject) {
const nodeImage = this.getNodeImage(node);
const nodeImageType = this.getImageType(nodeImage);

ghostGrp
.append(nodeImageType)
.attr("class", "d3-node-image")
.each(function() { that.setNodeImageContent(this, node); })
.append(() => this.getImageElement(node))
.each((d, idx, imgs) => this.setNodeImageContent(node, idx, imgs))
.attr("x", this.nodeUtils.getNodeImagePosX(node))
.attr("y", this.nodeUtils.getNodeImagePosY(node))
.attr("width", this.nodeUtils.getNodeImageWidth(node))
Expand Down Expand Up @@ -1682,12 +1677,10 @@ export default class SVGCanvasRenderer {
.data((d) => (d.layout.imageDisplay ? [d] : []), (d) => d.id)
.join(
(enter) =>
enter
.append((d) => this.getImageElement(d))
.attr("class", "d3-node-image")
enter.append((d) => this.getImageElement(d))
)
.datum((d) => this.activePipeline.getNode(d.id))
.each((d, idx, imgs) => this.setNodeImageContent(imgs[idx], d))
.each((d, idx, imgs) => this.setNodeImageContent(d, idx, imgs))
.attr("x", (d) => this.nodeUtils.getNodeImagePosX(d))
.attr("y", (d) => this.nodeUtils.getNodeImagePosY(d))
.attr("width", (d) => this.nodeUtils.getNodeImageWidth(d))
Expand Down Expand Up @@ -1765,6 +1758,13 @@ export default class SVGCanvasRenderer {
}

removeNodes(removeSel) {
// Remove any JSX images for the nodes being removed to
// unmount their React objects.
removeSel
.selectAll(".d3-foreign-object-node-image")
.each((d, idx, exts) =>
this.externalUtils.removeExternalObject(d, idx, exts));

// Remove any JSX decorations for the nodes being removed to
// unmount their React objects.
removeSel
Expand Down Expand Up @@ -2438,7 +2438,7 @@ export default class SVGCanvasRenderer {
.attr("y", this.decUtils.getDecPadding(dec, d, objType))
.attr("width", this.decUtils.getDecWidth(dec, d, objType) - (2 * this.decUtils.getDecPadding(dec, d, objType)))
.attr("height", this.decUtils.getDecHeight(dec, d, objType) - (2 * this.decUtils.getDecPadding(dec, d, objType)))
.each(() => this.setImageContent(imageSel, dec.image));
.each(() => this.setDecImageContent(imageSel, dec.image));
} else {
imageSel.remove();
}
Expand Down Expand Up @@ -2584,21 +2584,33 @@ export default class SVGCanvasRenderer {
}

// Sets the image specified in the node passed in into the DOM image object
// passed in.
setNodeImageContent(imageObj, node) {
const nodeImage = this.getNodeImage(node);
const imageSel = d3.select(imageObj);
this.setImageContent(imageSel, nodeImage);
// passed in specified by imgs[i].
setNodeImageContent(node, i, imgs) {
const image = this.getNodeImage(node);
const imageType = this.getImageType(image);

if (imageType === "jsx") {
this.externalUtils.addNodeImageExternalObject(image, i, imgs);
} else {
const imageSel = d3.select(imgs[i]);
this.setImageContent(imageSel, image, imageType);
}
}

// Sets the image specified for the decoration into the D3 selection
// of the decoration image.
setDecImageContent(imageSel, image) {
const imageType = this.getImageType(image);
this.setImageContent(imageSel, image, imageType);
}

// Sets the image passed in into the D3 image selection passed in. This loads
// svg files as inline SVG while other image files are loaded with href.
setImageContent(imageSel, image) {
setImageContent(imageSel, image, imageType) {
if (image !== imageSel.attr("data-image")) {
const nodeImageType = this.getImageType(image);
// Save image field in DOM object to avoid unnecessary image refreshes.
imageSel.attr("data-image", image);
if (nodeImageType === "svg") {
if (imageType === "svg") {
if (this.config.enableImageDisplay === "LoadSVGToDefs") {
this.loadSVGToDefs(imageSel, image);

Expand Down Expand Up @@ -2663,20 +2675,44 @@ export default class SVGCanvasRenderer {
return d.image;
}

// Returns the type of image passed in, either "svg" or "image". This will
// be used to append an svg or image element to the DOM.
// Returns the type of image passed in, either "svg" or "image" or
// "jsx" or null (if no image was provided).
// This will be used to append an svg or image element to the DOM.
getImageType(nodeImage) {
return nodeImage && nodeImage.endsWith(".svg") && this.config.enableImageDisplay !== "SVGAsImage" ? "svg" : "image";
if (nodeImage) {
if (typeof nodeImage === "object") {
if (this.externalUtils.isValidJsxElement(nodeImage)) {
return "jsx";
}
} else if (typeof nodeImage === "string") {
return nodeImage.endsWith(".svg") && this.config.enableImageDisplay !== "SVGAsImage" ? "svg" : "image";
}
}
return null;
}

// Returns a DOM element for the image of the node passed in.
// Returns a DOM element for the image of the node passed in to be appended
// to the node element.
getImageElement(node) {
const nodeImage = this.getNodeImage(node);
const imageType = this.getImageType(nodeImage);
if (imageType === "image") {
return d3.create("svg:image").node();

if (imageType === "jsx") {
return d3.create("svg:foreignObject")
.attr("tabindex", -1)
.attr("class", "d3-foreign-object-node-image d3-node-image")
.node();

} else if (imageType === "svg") {
return d3.create("svg")
.attr("class", "d3-node-image")
.node();

}
return d3.create("svg").node();
// If imageType is "image" or null, we create an image element
return d3.create("svg:image")
.attr("class", "d3-node-image")
.node();
}

setNodeStyles(d, type, nodeGrp) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ export default class SvgCanvasExternal {
this.ren = renderer;
}

isValidJsxElement(el) {
return React.isValidElement(el);
}

addNodeExternalObject(node, i, foreignObjects) {
const jsx = (
<node.layout.nodeExternalObject
Expand All @@ -35,28 +39,32 @@ export default class SvgCanvasExternal {
this.renderExternalObject(jsx, foreignObjects[i]);
}

addNodeImageExternalObject(image, i, foreignObjects) {
this.renderExternalObject(image, foreignObjects[i]);
}

addDecExternalObject(dec, i, foreignObjects) {
this.renderExternalObject(dec.jsx, foreignObjects[i]);
}

renderExternalObject(jsx, container) {
if (!container.root) {
container.root = createRoot(container);
if (!container.ccExtRoot) {
container.ccExtRoot = createRoot(container);
}
container.root.render(jsx);
container.ccExtRoot.render(jsx);
}

removeExternalObject(obj, i, foreignObjects) {
const container = foreignObjects[i];
if (!container.root) {
container.root = createRoot(container);
if (!container.ccExtRoot) {
container.ccExtRoot = createRoot(container);
}
// Unmount in Timeout to stop this warning from appearing:
// "Warning: Attempted to synchronously unmount a root while
// React was already rendering."
setTimeout(() => {
container.root.unmount();
container.root = null;
container.ccExtRoot.unmount();
container.ccExtRoot = null;
});
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ export default class PipelineOutHandler {
static createNodeUiData(ciNode) {
const uiData = {
label: ciNode.label,
image: ciNode.image,
image: (typeof ciNode.image === "object" ? "" : ciNode.image),
x_pos: ciNode.x_pos,
y_pos: ciNode.y_pos
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ class PaletteContentListItem extends React.Component {
this.onMouseOver = this.onMouseOver.bind(this);
this.onMouseLeave = this.onMouseLeave.bind(this);
this.onMouseDown = this.onMouseDown.bind(this);
this.onKeyPress = this.onKeyPress.bind(this);
this.onKeyDown = this.onKeyDown.bind(this);
}

onMouseDown() {
Expand All @@ -74,7 +74,7 @@ class PaletteContentListItem extends React.Component {
}
}

onKeyPress(e) {
onKeyDown(e) {
// e.key === " " is needed to allow Cypress test in palette.js to run on the build machine!
if (e.key === " " || e.code === "Space" || e.keyCode === 32) {
this.onDoubleClick();
Expand Down Expand Up @@ -279,9 +279,14 @@ class PaletteContentListItem extends React.Component {
} else if (image === USE_DEFAULT_EXT_ICON) {
image = SUPERNODE_EXT_ICON;
}
icon = image.endsWith(".svg")
? <SVG src={image} className="palette-list-item-icon" draggable="false" />
: <img src={image} className="palette-list-item-icon" draggable="false" alt={""} />;

if (typeof image === "object" && React.isValidElement(image)) {
icon = image;
} else if (typeof image === "string") {
icon = image.endsWith(".svg")
? <SVG src={image} className="palette-list-item-icon" draggable="false" />
: <img src={image} className="palette-list-item-icon" draggable="false" alt={""} />;
}
}

if (labelText && (this.props.isPaletteOpen || !icon)) {
Expand Down Expand Up @@ -330,7 +335,7 @@ class PaletteContentListItem extends React.Component {
className={mainDivClass}
onMouseOver={this.onMouseOver}
onMouseLeave={this.onMouseLeave}
onKeyPress={this.props.isEditingEnabled ? this.onKeyPress : null}
onKeyDown={this.props.isEditingEnabled ? this.onKeyDown : null}
onMouseDown={this.props.isEditingEnabled ? this.onMouseDown : null}
onDragStart={this.props.isEditingEnabled ? this.onDragStart : null}
onDragEnd={this.props.isEditingEnabled ? this.onDragEnd : null}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,9 +116,14 @@ class PaletteDialogContentGridNode extends React.Component {
image = SUPERNODE_EXT_ICON;
}

icon = image.endsWith(".svg")
? <SVG src={image} className="node-icon" alt={label} />
: <img src={image} className="node-icon" alt={label} />;
if (typeof image === "object" && React.isValidElement(image)) {
icon = image;

} else if (typeof image === "string") {
icon = image.endsWith(".svg")
? <SVG src={image} className="node-icon" alt={label} />
: <img src={image} className="node-icon" alt={label} />;
}
}

let draggable = this.props.isEditingEnabled ? "true" : "false";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,10 @@ class PaletteFlyoutContentCategory extends React.Component {
getItemImage() {
let itemImage = null;
if (this.props.category.image && this.props.category.image !== "") {
if (this.props.category.image.endsWith(".svg")) {
if (typeof this.props.category.image === "object" && React.isValidElement(this.props.category.image)) {
itemImage = this.props.category.image;

} else if (this.props.category.image.endsWith(".svg")) {
itemImage = (
<div>
<SVG src={this.props.category.image} className="palette-flyout-category-item-icon" draggable="false" />
Expand Down
9 changes: 9 additions & 0 deletions canvas_modules/harness/src/client/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ import ProgressCanvas from "./components/custom-canvases/progress/progress";
import ExplainCanvas from "./components/custom-canvases/explain/explain-canvas";
import Explain2Canvas from "./components/custom-canvases/explain2/explain2-canvas";
import StreamsCanvas from "./components/custom-canvases/streams/streams-canvas";
import JsxIconsCanvas from "./components/custom-canvases/jsx-icons/jsx-icons-canvas";
import ReactNodesCarbonCanvas from "./components/custom-canvases/react-nodes-carbon/react-nodes-carbon";
import ReactNodesMappingCanvas from "./components/custom-canvases/react-nodes-mapping/react-nodes-mapping";

Expand Down Expand Up @@ -113,6 +114,7 @@ import {
EXAMPLE_APP_LOGIC,
EXAMPLE_APP_READ_ONLY,
EXAMPLE_APP_PROGRESS,
EXAMPLE_APP_JSX_ICONS,
EXAMPLE_APP_REACT_NODES_CARBON,
EXAMPLE_APP_REACT_NODES_MAPPING,
CUSTOM,
Expand Down Expand Up @@ -2622,6 +2624,13 @@ class App extends React.Component {
config={commonCanvasConfig}
/>
);
} else if (this.state.selectedExampleApp === EXAMPLE_APP_JSX_ICONS) {
firstCanvas = (
<JsxIconsCanvas
ref={this.canvasRef}
config={commonCanvasConfig}
/>
);
} else if (this.state.selectedExampleApp === EXAMPLE_APP_REACT_NODES_CARBON) {
firstCanvas = (
<ReactNodesCarbonCanvas
Expand Down
Loading

0 comments on commit b9e323c

Please sign in to comment.