Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/staging' into add-notifications-…
Browse files Browse the repository at this point in the history
…related-to-currently-displayed-scenario
  • Loading branch information
mgoworko committed Nov 27, 2024
2 parents e031e7b + 0f94b80 commit 144ebdc
Show file tree
Hide file tree
Showing 159 changed files with 2,596 additions and 897 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -316,7 +316,7 @@ object ProcessCompilationError {
extends PartSubGraphCompilationError
with InASingleNode

final case class RequireValueFromEmptyFixedList(paramName: ParameterName, nodeIds: Set[String])
final case class EmptyFixedListForRequiredField(paramName: ParameterName, nodeIds: Set[String])
extends PartSubGraphCompilationError

final case class InitialValueNotPresentInPossibleValues(paramName: ParameterName, nodeIds: Set[String])
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package pl.touk.nussknacker.engine.api.json

import io.circe.Json
import pl.touk.nussknacker.engine.util.Implicits._

import scala.jdk.CollectionConverters._

object FromJsonDecoder {

def jsonToAny(json: Json): Any = json.fold(
jsonNull = null,
jsonBoolean = identity[Boolean],
jsonNumber = jsonNumber =>
// we pick the narrowest type as possible to reduce the amount of memory and computations overheads
jsonNumber.toInt orElse
jsonNumber.toLong orElse
// We prefer java big decimal over float/double
jsonNumber.toBigDecimal.map(_.bigDecimal)
getOrElse (throw new IllegalArgumentException(s"Not supported json number: $jsonNumber")),
jsonString = identity[String],
jsonArray = _.map(jsonToAny).asJava,
jsonObject = _.toMap.mapValuesNow(jsonToAny).asJava
)

}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package pl.touk.nussknacker.engine.api.typed

import cats.implicits.toTraverseOps
import io.circe.{ACursor, Decoder, DecodingFailure, Json}
import pl.touk.nussknacker.engine.api.json.FromJsonDecoder
import pl.touk.nussknacker.engine.api.typed.typing._

import java.math.BigInteger
Expand Down Expand Up @@ -58,19 +59,15 @@ object ValueDecoder {
case record: TypedObjectTypingResult =>
for {
fieldsJson <- obj.as[Map[String, Json]]
decodedFields <- record.fields.toList.traverse { case (fieldName, fieldType) =>
fieldsJson.get(fieldName) match {
case Some(fieldJson) => decodeValue(fieldType, fieldJson.hcursor).map(fieldName -> _)
case None =>
Left(
DecodingFailure(
s"Record field '$fieldName' isn't present in encoded Record fields: $fieldsJson",
List()
)
)
decodedFields <-
fieldsJson.toList.traverse { case (fieldName, fieldJson) =>
val fieldType = record.fields.getOrElse(fieldName, Unknown)
decodeValue(fieldType, fieldJson.hcursor).map(fieldName -> _)
}
}
} yield decodedFields.toMap.asJava
case Unknown =>
/// For Unknown we fallback to generic json to any conversion. It won't work for some types such as LocalDate but for others should work correctly
obj.as[Json].map(FromJsonDecoder.jsonToAny)
case typ => Left(DecodingFailure(s"Decoding of type [$typ] is not supported.", List()))
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package pl.touk.nussknacker.engine.api.json

import io.circe.Json
import org.scalatest.OptionValues
import org.scalatest.funsuite.AnyFunSuiteLike
import org.scalatest.matchers.should.Matchers

class FromJsonDecoderTest extends AnyFunSuiteLike with Matchers with OptionValues {

test("json number decoding pick the narrowest type") {
FromJsonDecoder.jsonToAny(Json.fromInt(1)) shouldBe 1
FromJsonDecoder.jsonToAny(Json.fromInt(Integer.MAX_VALUE)) shouldBe Integer.MAX_VALUE
FromJsonDecoder.jsonToAny(Json.fromLong(Long.MaxValue)) shouldBe Long.MaxValue
FromJsonDecoder.jsonToAny(
Json.fromBigDecimal(java.math.BigDecimal.valueOf(Double.MaxValue))
) shouldBe java.math.BigDecimal.valueOf(Double.MaxValue)
val moreThanLongMaxValue = BigDecimal(Long.MaxValue) * 10
FromJsonDecoder.jsonToAny(Json.fromBigDecimal(moreThanLongMaxValue)) shouldBe moreThanLongMaxValue.bigDecimal
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,20 @@ class TypingResultDecoderSpec
Map("a" -> TypedObjectWithValue(Typed.typedClass[Int], 1))
),
List(Map("a" -> 1).asJava).asJava
)
),
typedListWithElementValues(
Typed.record(
List(
"a" -> Typed.typedClass[Int],
"b" -> Typed.typedClass[Int]
)
),
List(Map("a" -> 1).asJava, Map("b" -> 2).asJava).asJava
),
typedListWithElementValues(
Unknown,
List(Map("a" -> 1).asJava, 2).asJava
),
).foreach { typing =>
val encoded = TypeEncoders.typingResultEncoder(typing)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ class ValueDecoderSpec extends AnyFunSuite with EitherValuesDetailedMessage with
)
}

test("decodeValue should fail when a required Record field is missing") {
test("decodeValue should ignore missing Record field") {
val typedRecord = Typed.record(
Map(
"name" -> Typed.fromInstance("Alice"),
Expand All @@ -45,12 +45,10 @@ class ValueDecoderSpec extends AnyFunSuite with EitherValuesDetailedMessage with
"name" -> "Alice".asJson
)

ValueDecoder.decodeValue(typedRecord, json.hcursor).leftValue.message should include(
"Record field 'age' isn't present in encoded Record fields"
)
ValueDecoder.decodeValue(typedRecord, json.hcursor).rightValue shouldBe Map("name" -> "Alice").asJava
}

test("decodeValue should not include extra fields that aren't typed") {
test("decodeValue should decode extra fields using generic json decoding strategy") {
val typedRecord = Typed.record(
Map(
"name" -> Typed.fromInstance("Alice"),
Expand All @@ -66,8 +64,9 @@ class ValueDecoderSpec extends AnyFunSuite with EitherValuesDetailedMessage with

ValueDecoder.decodeValue(typedRecord, json.hcursor) shouldEqual Right(
Map(
"name" -> "Alice",
"age" -> 30
"name" -> "Alice",
"age" -> 30,
"occupation" -> "nurse"
).asJava
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@ package pl.touk.nussknacker.openapi.extractor

import java.util.Collections
import io.circe.Json
import pl.touk.nussknacker.engine.json.swagger.extractor.FromJsonDecoder
import pl.touk.nussknacker.engine.json.swagger.decode.FromJsonSchemaBasedDecoder
import pl.touk.nussknacker.engine.json.swagger.{SwaggerArray, SwaggerTyped}

object HandleResponse {

def apply(res: Option[Json], responseType: SwaggerTyped): AnyRef = res match {
case Some(json) =>
FromJsonDecoder.decode(json, responseType)
FromJsonSchemaBasedDecoder.decode(json, responseType)
case None =>
responseType match {
case _: SwaggerArray => Collections.EMPTY_LIST
Expand Down
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.
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/activities.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ const makeScreenshot = () => {
cy.get('[data-testid="activities-panel"]').matchImage({
maxDiffThreshold: 0.01,
screenshotConfig: {
blackout: ["[data-testid='activity-date']"],
blackout: [":has(>[data-testid='activity-date'])"],
},
});
};
Expand Down
2 changes: 1 addition & 1 deletion designer/client/cypress/e2e/fragment.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ describe("Fragment", () => {
cy.get("[data-testid='settings:4']").contains("Typing...").should("not.exist");
cy.get("[data-testid='settings:4']").find("[id='ace-editor']").type("{enter}");
cy.get("[data-testid='settings:4']")
.contains(/Add list item/i)
.contains(/Suggested values/i)
.siblings()
.eq(0)
.find("[data-testid='form-helper-text']")
Expand Down
8 changes: 8 additions & 0 deletions designer/client/cypress/e2e/labels.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,14 @@ describe("Scenario labels", () => {

cy.get("[data-testid=scenario-label-0]").should("be.visible").contains("tagX");

cy.get("@labelInput").should("be.visible").click().type("tagX");

cy.wait("@labelvalidation");

cy.get("@labelInput").should("be.visible").contains("This label already exists. Please enter a unique value.");

cy.get("@labelInput").find("input").clear();

cy.get("@labelInput").should("be.visible").click().type("tag2");

cy.wait("@labelvalidation");
Expand Down
2 changes: 1 addition & 1 deletion designer/client/cypress/e2e/process.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -333,7 +333,7 @@ describe("Process", () => {
cy.layoutScenario();

cy.contains("button", "ad hoc").should("be.enabled").click();
cy.get("[data-testid=window]").should("be.visible").find("input").type("10"); //There should be only one input field
cy.get("[data-testid=window]").should("be.visible").find("#ace-editor").type("10");
cy.get("[data-testid=window]")
.contains(/^test$/i)
.should("be.enabled")
Expand Down
15 changes: 7 additions & 8 deletions designer/client/package-lock.json

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

2 changes: 1 addition & 1 deletion designer/client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
"@mui/icons-material": "5.15.7",
"@mui/lab": "5.0.0-alpha.165",
"@mui/material": "5.15.7",
"@touk/federated-component": "1.0.0",
"@touk/federated-component": "1.1.0",
"@touk/window-manager": "1.9.0",
"ace-builds": "1.34.2",
"axios": "1.7.5",
Expand Down
2 changes: 1 addition & 1 deletion designer/client/progressBar.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ function getElapsed() {

function getBar(percentage) {
const { barLength, almostDone } = options;
const bar = padEnd(repeat("◼", Math.ceil(percentage * barLength)), barLength, "");
const bar = padEnd(repeat("◼", Math.ceil(percentage * barLength)), barLength, "_");
const barColor = percentage > almostDone ? chalk.green : percentage > (almostDone * 2) / 3 ? chalk.yellow : chalk.red;
return barColor(bar);
}
Expand Down
2 changes: 0 additions & 2 deletions designer/client/src/actions/actionTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,6 @@ export type ActionTypes =
| "DELETE_NODES"
| "NODES_CONNECTED"
| "NODES_DISCONNECTED"
| "NODE_ADDED"
| "NODES_WITH_EDGES_ADDED"
| "VALIDATION_RESULT"
| "COPY_SELECTION"
| "CUT_SELECTION"
Expand Down
43 changes: 30 additions & 13 deletions designer/client/src/actions/nk/node.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import { Dictionary } from "lodash";
import { flushSync } from "react-dom";
import NodeUtils from "../../components/graph/NodeUtils";
import { batchGroupBy } from "../../reducers/graph/batchGroupBy";
import { prepareNewNodesWithLayout } from "../../reducers/graph/utils";
import { getScenarioGraph } from "../../reducers/selectors/graph";
import { getProcessDefinitionData } from "../../reducers/selectors/settings";
import { Edge, EdgeType, NodeId, NodeType, ProcessDefinitionData, ValidationResult } from "../../types";
import { ThunkAction } from "../reduxTypes";
import { layoutChanged, Position } from "./ui/layout";
import { EditNodeAction, EditScenarioLabels } from "./editNode";
import { getProcessDefinitionData } from "../../reducers/selectors/settings";
import { batchGroupBy } from "../../reducers/graph/batchGroupBy";
import NodeUtils from "../../components/graph/NodeUtils";
import { getScenarioGraph } from "../../reducers/selectors/graph";
import { flushSync } from "react-dom";
import { layoutChanged, NodePosition, Position } from "./ui/layout";

export type NodesWithPositions = { node: NodeType; position: Position }[];

Expand All @@ -31,7 +33,9 @@ type NodesDisonnectedAction = {

type NodesWithEdgesAddedAction = {
type: "NODES_WITH_EDGES_ADDED";
nodesWithPositions: NodesWithPositions;
nodes: NodeType[];
layout: NodePosition[];
idMapping: Dictionary<string>;
edges: Edge[];
processDefinitionData: ProcessDefinitionData;
};
Expand All @@ -43,8 +47,8 @@ type ValidationResultAction = {

type NodeAddedAction = {
type: "NODE_ADDED";
node: NodeType;
position: Position;
nodes: NodeType[];
layout: NodePosition[];
};

export function deleteNodes(ids: NodeId[]): ThunkAction {
Expand Down Expand Up @@ -118,13 +122,20 @@ export function injectNode(from: NodeType, middle: NodeType, to: NodeType, { edg
}

export function nodeAdded(node: NodeType, position: Position): ThunkAction {
return (dispatch) => {
return (dispatch, getState) => {
batchGroupBy.startOrContinue();

// We need to disable automatic React batching https://react.dev/blog/2022/03/29/react-v18#new-feature-automatic-batching
// since it breaks redux undo in this case
flushSync(() => {
dispatch({ type: "NODE_ADDED", node, position });
const scenarioGraph = getScenarioGraph(getState());
const { nodes, layout } = prepareNewNodesWithLayout(scenarioGraph.nodes, [{ node, position }], false);

dispatch({
type: "NODE_ADDED",
nodes,
layout,
});
dispatch(layoutChanged());
});
batchGroupBy.end();
Expand All @@ -133,11 +144,17 @@ export function nodeAdded(node: NodeType, position: Position): ThunkAction {

export function nodesWithEdgesAdded(nodesWithPositions: NodesWithPositions, edges: Edge[]): ThunkAction {
return (dispatch, getState) => {
const processDefinitionData = getProcessDefinitionData(getState());
const state = getState();
const processDefinitionData = getProcessDefinitionData(state);
const scenarioGraph = getScenarioGraph(state);
const { nodes, layout, idMapping } = prepareNewNodesWithLayout(scenarioGraph.nodes, nodesWithPositions, true);

batchGroupBy.startOrContinue();
dispatch({
type: "NODES_WITH_EDGES_ADDED",
nodesWithPositions,
nodes,
layout,
idMapping,
edges,
processDefinitionData,
});
Expand Down
Loading

0 comments on commit 144ebdc

Please sign in to comment.