Skip to content

Commit

Permalink
Merge branch 'refs/heads/release/1.18' into NU-1890-hide-categories-w…
Browse files Browse the repository at this point in the history
…hen-there-is-only-one-category-available

# Conflicts:
#	designer/client/src/components/toolbars/scenarioDetails/CategoryDetails.tsx
#	docs/Changelog.md
  • Loading branch information
Dzuming committed Nov 21, 2024
2 parents 022b1a2 + 7aa80a8 commit 495e87c
Show file tree
Hide file tree
Showing 33 changed files with 279 additions and 108 deletions.
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
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
Original file line number Diff line number Diff line change
Expand Up @@ -25,18 +25,18 @@ export interface AceWrapperProps extends Pick<IAceEditorProps, "value" | "onChan
customAceEditorCompleter?;
showLineNumbers?: boolean;
commands?: AceKeyCommand[];
enableLiveAutocompletion?: boolean;
}

export const DEFAULT_OPTIONS: IAceOptions = {
indentedSoftWrap: false, //removes weird spaces for multiline strings when wrapEnabled=true
enableLiveAutocompletion: true,
enableSnippets: false,
fontSize: 14,
highlightActiveLine: false,
highlightGutterLine: true,
};

const DEFAULF_EDITOR_PROPS: IEditorProps = {
const DEFAULT_EDITOR_PROPS: IEditorProps = {
$blockScrolling: true,
};

Expand Down Expand Up @@ -133,7 +133,15 @@ function editorLangToMode(language: ExpressionLang | string, editorMode?: Editor
}

export default forwardRef(function AceWrapper(
{ inputProps, customAceEditorCompleter, showLineNumbers, wrapEnabled = true, commands = [], ...props }: AceWrapperProps,
{
inputProps,
customAceEditorCompleter,
showLineNumbers,
wrapEnabled = true,
commands = [],
enableLiveAutocompletion = true,
...props
}: AceWrapperProps,
ref: ForwardedRef<ReactAce>,
): JSX.Element {
const { language, readOnly, rows = 1, editorMode } = inputProps;
Expand Down Expand Up @@ -183,9 +191,10 @@ export default forwardRef(function AceWrapper(
className={readOnly ? " read-only" : ""}
wrapEnabled={!!wrapEnabled}
showGutter={!!showLineNumbers}
editorProps={DEFAULF_EDITOR_PROPS}
editorProps={DEFAULT_EDITOR_PROPS}
setOptions={{
...DEFAULT_OPTIONS,
enableLiveAutocompletion,
showLineNumbers,
}}
enableBasicAutocompletion={customAceEditorCompleter && [customAceEditorCompleter]}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,11 @@ export type CustomCompleterAceEditorProps = {
showValidation?: boolean;
isMarked?: boolean;
className?: string;
enableLiveAutocompletion?: boolean;
};

export function CustomCompleterAceEditor(props: CustomCompleterAceEditorProps): JSX.Element {
const { className, isMarked, showValidation, fieldErrors, validationLabelInfo, completer, isLoading } = props;
const { className, isMarked, showValidation, fieldErrors, validationLabelInfo, completer, isLoading, enableLiveAutocompletion } = props;
const { value, onValueChange, ref, ...inputProps } = props.inputProps;

const [editorFocused, setEditorFocused] = useState(false);
Expand Down Expand Up @@ -65,6 +66,7 @@ export function CustomCompleterAceEditor(props: CustomCompleterAceEditorProps):
...inputProps,
}}
customAceEditorCompleter={completer}
enableLiveAutocompletion={enableLiveAutocompletion}
/>
</Box>
<Fade
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export const MarkdownFormControl = ({ value, onChange, className, children, read
{children}
<CustomCompleterAceEditor
{...props}
enableLiveAutocompletion={false}
inputProps={{
language: ExpressionLang.MD,
className,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@ import React from "react";
import { Skeleton, Typography } from "@mui/material";
import { Scenario } from "../../Process/types";
import { useGetAllCombinations } from "../../useGetAllCombinations";
import { useTranslation } from "react-i18next";

export const CategoryDetails = ({ scenario }: { scenario: Scenario }) => {
const { t } = useTranslation();
const { isAllCombinationsLoading, isCategoryFieldVisible } = useGetAllCombinations({
processCategory: scenario.processCategory,
processingMode: scenario.processingMode,
Expand All @@ -15,7 +17,11 @@ export const CategoryDetails = ({ scenario }: { scenario: Scenario }) => {
{isAllCombinationsLoading ? (
<Skeleton variant="text" sx={{ fontSize: "1.25rem" }} width={"50%"} />
) : (
isCategoryFieldVisible && <Typography variant={"body2"}>{scenario.processCategory} /</Typography>
isCategoryFieldVisible && (
<Typography title={t("panels.scenarioDetails.tooltip.category", "Category")} variant={"body2"}>
{scenario.processCategory} /
</Typography>
)
)}
</>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { css, styled, Typography } from "@mui/material";
import i18next from "i18next";

export const PanelScenarioDetails = styled("div")(
({ theme }) => css`
Expand Down Expand Up @@ -26,6 +27,10 @@ export const ScenarioDetailsItemWrapper = styled("div")(

export const ProcessName = styled(Typography)``;

ProcessName.defaultProps = {
title: i18next.t("panels.scenarioDetails.tooltip.name", "Name"),
};

export const ProcessRename = styled(ProcessName)(({ theme }) => ({
color: theme.palette.warning.main,
}));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,19 @@ import i18next from "i18next";
import { editScenarioLabels } from "../../../actions/nk";
import { debounce } from "lodash";
import { ScenarioLabelValidationError } from "../../Labels/types";
import { useTranslation } from "react-i18next";

interface AddLabelProps {
onClick: () => void;
}

const labelUniqueValidation = (label: string) => ({
label,
messages: [
i18next.t("panels.scenarioDetails.labels.validation.uniqueValue", "This label already exists. Please enter a unique value."),
],
});

const AddLabel = ({ onClick }: AddLabelProps) => {
return (
<Typography
Expand Down Expand Up @@ -100,6 +108,7 @@ interface Props {
}

export const ScenarioLabels = ({ readOnly }: Props) => {
const { t } = useTranslation();
const scenarioLabels = useSelector(getScenarioLabels);
const scenarioLabelOptions: LabelOption[] = useMemo(() => scenarioLabels.map(toLabelOption), [scenarioLabels]);
const initialScenarioLabelOptionsErrors = useSelector(getScenarioLabelsErrors).filter((error) =>
Expand All @@ -125,9 +134,12 @@ export const ScenarioLabels = ({ readOnly }: Props) => {
setIsEdited(true);
};

const isInputInSelectedOptions = (inputValue: string): boolean => {
return scenarioLabelOptions.some((option) => inputValue === toLabelValue(option));
};
const isInputInSelectedOptions = useCallback(
(inputValue: string): boolean => {
return scenarioLabelOptions.some((option) => inputValue === toLabelValue(option));
},
[scenarioLabelOptions],
);

const inputHelperText = useMemo(() => {
if (inputErrors.length !== 0) {
Expand All @@ -151,9 +163,13 @@ export const ScenarioLabels = ({ readOnly }: Props) => {
}
}

if (isInputInSelectedOptions(newInput)) {
setInputErrors((prevState) => [...prevState, labelUniqueValidation(newInput)]);
}

setInputTyping(false);
}, 500);
}, []);
}, [isInputInSelectedOptions]);

const validateSelectedOptions = useMemo(() => {
return debounce(async (labels: LabelOption[]) => {
Expand Down Expand Up @@ -338,6 +354,9 @@ export const ScenarioLabels = ({ readOnly }: Props) => {
const labelError = labelOptionsErrors.find((error) => error.label === toLabelValue(option));
return (
<StyledLabelChip
title={t("panels.scenarioDetails.tooltip.label", "Scenario label: {{label}}", {
label: option.title,
})}
key={key}
data-testid={`scenario-label-${index}`}
color={labelError ? "error" : "default"}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React, { useCallback, useMemo } from "react";
import { Button, Chip, Typography } from "@mui/material";
import { useTranslation } from "react-i18next";

interface Props {
category: string;
Expand All @@ -23,6 +24,7 @@ export function CategoryChip({ category, filterValues, setFilter }: Props): JSX.
}

export function CategoryButton({ category, filterValues, setFilter }: Props): JSX.Element {
const { t } = useTranslation();
const isSelected = useMemo(() => filterValues.includes(category), [filterValues, category]);

const onClick = useCallback(
Expand All @@ -36,6 +38,7 @@ export function CategoryButton({ category, filterValues, setFilter }: Props): JS

return (
<Typography
title={t("scenariosList.tooltip.category", "Category")}
component={Button}
color={isSelected ? "primary" : "inherit"}
sx={{
Expand Down
Loading

0 comments on commit 495e87c

Please sign in to comment.