Add new ‘enumWithDescriptions’ Custom Property to allow adding Enum Keys with Description (#17777)

* Add new ‘metaEnum’ Custom Property to allow adding Enum Keys with Description

* replace JsonNodeFactory method with JsonUtils

* rename property from metaEnum to enumWithDescriptions, and other method optimizations

* ui: add support for creating enumWithDescription property

* minor locale changes

* ui: add edit support for created enumWithDescription property

* Refactor enum description field layout in AddCustomProperty and EditCustomPropertyModal

* add support for adding values to enumWithDescription custom property type

* Refactor custom property input IDs in AddCustomProperty and EditCustomPropertyModal components

* Refactor custom property table rendering logic and UI components

* Refactor custom property table rendering logic and UI components

* Refactor custom property table rendering logic and UI components

* add basic card layout

* Refactor CustomPropertyTable component to improve UI and functionality

* update playwright test part 1

* Refactor PropertyValue component to conditionally render right panel styles

* fix: entity reference property update

* Refactor CustomPropertyTable component to conditionally render right panel styles

* fix: flaky test

* Refactor CustomPropertyTable test to use updated test IDs and remove unnecessary code

* fix flaky test

* improve the playwright test

* add more test

---------

Co-authored-by: Sachin Chaurasiya <sachinchaurasiyachotey87@gmail.com>
Co-authored-by: Sriharsha Chintalapani <harshach@users.noreply.github.com>
This commit is contained in:
sonika-shah 2024-09-29 00:45:46 +05:30 committed by GitHub
parent f9e70f8db0
commit 1b029d2bf2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
35 changed files with 1528 additions and 227 deletions

View File

@ -112,7 +112,8 @@ public class TypeRegistry {
} }
} }
} }
return null; throw EntityNotFoundException.byMessage(
CatalogExceptionMessage.entityNotFound(Entity.TYPE, String.valueOf(type)));
} }
public static String getCustomPropertyConfig(String entityType, String propertyName) { public static String getCustomPropertyConfig(String entityType, String propertyName) {
@ -122,7 +123,13 @@ public class TypeRegistry {
if (property.getName().equals(propertyName) if (property.getName().equals(propertyName)
&& property.getCustomPropertyConfig() != null && property.getCustomPropertyConfig() != null
&& property.getCustomPropertyConfig().getConfig() != null) { && property.getCustomPropertyConfig().getConfig() != null) {
return property.getCustomPropertyConfig().getConfig().toString(); Object config = property.getCustomPropertyConfig().getConfig();
if (config instanceof String || config instanceof Integer) {
return config.toString(); // for simple type config return as string
} else {
return JsonUtils.pojoToJson(
config); // for complex object in config return as JSON string
}
} }
} }
} }

View File

@ -67,6 +67,7 @@ import static org.openmetadata.service.util.EntityUtil.objectMatch;
import static org.openmetadata.service.util.EntityUtil.tagLabelMatch; import static org.openmetadata.service.util.EntityUtil.tagLabelMatch;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode; import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.annotations.VisibleForTesting; import com.google.common.annotations.VisibleForTesting;
import com.google.common.cache.CacheBuilder; import com.google.common.cache.CacheBuilder;
@ -100,6 +101,7 @@ import java.util.concurrent.TimeUnit;
import java.util.function.BiPredicate; import java.util.function.BiPredicate;
import java.util.function.Function; import java.util.function.Function;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.StreamSupport;
import javax.json.JsonPatch; import javax.json.JsonPatch;
import javax.validation.constraints.NotNull; import javax.validation.constraints.NotNull;
import javax.ws.rs.core.Response.Status; import javax.ws.rs.core.Response.Status;
@ -143,6 +145,7 @@ import org.openmetadata.schema.type.api.BulkAssets;
import org.openmetadata.schema.type.api.BulkOperationResult; import org.openmetadata.schema.type.api.BulkOperationResult;
import org.openmetadata.schema.type.api.BulkResponse; import org.openmetadata.schema.type.api.BulkResponse;
import org.openmetadata.schema.type.csv.CsvImportResult; import org.openmetadata.schema.type.csv.CsvImportResult;
import org.openmetadata.schema.type.customproperties.EnumWithDescriptionsConfig;
import org.openmetadata.schema.utils.EntityInterfaceUtil; import org.openmetadata.schema.utils.EntityInterfaceUtil;
import org.openmetadata.service.Entity; import org.openmetadata.service.Entity;
import org.openmetadata.service.OpenMetadataApplicationConfig; import org.openmetadata.service.OpenMetadataApplicationConfig;
@ -1426,34 +1429,21 @@ public abstract class EntityRepository<T extends EntityInterface> {
} }
String customPropertyType = TypeRegistry.getCustomPropertyType(entityType, fieldName); String customPropertyType = TypeRegistry.getCustomPropertyType(entityType, fieldName);
String propertyConfig = TypeRegistry.getCustomPropertyConfig(entityType, fieldName); String propertyConfig = TypeRegistry.getCustomPropertyConfig(entityType, fieldName);
DateTimeFormatter formatter;
try { try {
if ("date-cp".equals(customPropertyType)) { validateAndUpdateExtensionBasedOnPropertyType(
DateTimeFormatter inputFormatter = entity,
DateTimeFormatter.ofPattern(Objects.requireNonNull(propertyConfig), Locale.ENGLISH); (ObjectNode) jsonNode,
fieldName,
// Parse the input string into a TemporalAccessor fieldValue,
TemporalAccessor date = inputFormatter.parse(fieldValue.textValue()); customPropertyType,
propertyConfig);
// Create a formatter for the desired output format
DateTimeFormatter outputFormatter =
DateTimeFormatter.ofPattern(propertyConfig, Locale.ENGLISH);
((ObjectNode) jsonNode).put(fieldName, outputFormatter.format(date));
} else if ("dateTime-cp".equals(customPropertyType)) {
formatter = DateTimeFormatter.ofPattern(Objects.requireNonNull(propertyConfig));
LocalDateTime dateTime = LocalDateTime.parse(fieldValue.textValue(), formatter);
((ObjectNode) jsonNode).put(fieldName, dateTime.format(formatter));
} else if ("time-cp".equals(customPropertyType)) {
formatter = DateTimeFormatter.ofPattern(Objects.requireNonNull(propertyConfig));
LocalTime time = LocalTime.parse(fieldValue.textValue(), formatter);
((ObjectNode) jsonNode).put(fieldName, time.format(formatter));
}
} catch (DateTimeParseException e) { } catch (DateTimeParseException e) {
throw new IllegalArgumentException( throw new IllegalArgumentException(
CatalogExceptionMessage.dateTimeValidationError( CatalogExceptionMessage.dateTimeValidationError(fieldName, propertyConfig));
fieldName, TypeRegistry.getCustomPropertyConfig(entityType, fieldName)));
} }
Set<ValidationMessage> validationMessages = jsonSchema.validate(fieldValue);
Set<ValidationMessage> validationMessages = jsonSchema.validate(entry.getValue());
if (!validationMessages.isEmpty()) { if (!validationMessages.isEmpty()) {
throw new IllegalArgumentException( throw new IllegalArgumentException(
CatalogExceptionMessage.jsonValidationError(fieldName, validationMessages.toString())); CatalogExceptionMessage.jsonValidationError(fieldName, validationMessages.toString()));
@ -1461,6 +1451,94 @@ public abstract class EntityRepository<T extends EntityInterface> {
} }
} }
private void validateAndUpdateExtensionBasedOnPropertyType(
T entity,
ObjectNode jsonNode,
String fieldName,
JsonNode fieldValue,
String customPropertyType,
String propertyConfig) {
switch (customPropertyType) {
case "date-cp", "dateTime-cp", "time-cp" -> {
String formattedValue =
getFormattedDateTimeField(
fieldValue.textValue(), customPropertyType, propertyConfig, fieldName);
jsonNode.put(fieldName, formattedValue);
}
case "enumWithDescriptions" -> handleEnumWithDescriptions(
fieldName, fieldValue, propertyConfig, jsonNode, entity);
default -> {}
}
}
private String getFormattedDateTimeField(
String fieldValue, String customPropertyType, String propertyConfig, String fieldName) {
DateTimeFormatter formatter;
try {
return switch (customPropertyType) {
case "date-cp" -> {
DateTimeFormatter inputFormatter =
DateTimeFormatter.ofPattern(propertyConfig, Locale.ENGLISH);
TemporalAccessor date = inputFormatter.parse(fieldValue);
DateTimeFormatter outputFormatter =
DateTimeFormatter.ofPattern(propertyConfig, Locale.ENGLISH);
yield outputFormatter.format(date);
}
case "dateTime-cp" -> {
formatter = DateTimeFormatter.ofPattern(propertyConfig);
LocalDateTime dateTime = LocalDateTime.parse(fieldValue, formatter);
yield dateTime.format(formatter);
}
case "time-cp" -> {
formatter = DateTimeFormatter.ofPattern(propertyConfig);
LocalTime time = LocalTime.parse(fieldValue, formatter);
yield time.format(formatter);
}
default -> throw new IllegalArgumentException(
"Unsupported customPropertyType: " + customPropertyType);
};
} catch (DateTimeParseException e) {
throw new IllegalArgumentException(
CatalogExceptionMessage.dateTimeValidationError(fieldName, propertyConfig));
}
}
private void handleEnumWithDescriptions(
String fieldName, JsonNode fieldValue, String propertyConfig, ObjectNode jsonNode, T entity) {
JsonNode propertyConfigNode = JsonUtils.readTree(propertyConfig);
EnumWithDescriptionsConfig config =
JsonUtils.treeToValue(propertyConfigNode, EnumWithDescriptionsConfig.class);
if (!config.getMultiSelect() && fieldValue.size() > 1) {
throw new IllegalArgumentException(
"Only one key is allowed for non-multiSelect enumWithDescriptions");
}
// Replace each enumWithDescriptions key in the fieldValue with the corresponding object from
// the propertyConfig
Map<String, JsonNode> keyToObjectMap =
StreamSupport.stream(propertyConfigNode.get("values").spliterator(), false)
.collect(Collectors.toMap(node -> node.get("key").asText(), node -> node));
if (fieldValue.isArray()) {
ArrayNode newArray = JsonUtils.getObjectNode().arrayNode();
fieldValue.forEach(
valueNode -> {
String key = valueNode.isTextual() ? valueNode.asText() : valueNode.get("key").asText();
JsonNode valueObject = keyToObjectMap.get(key);
if (valueObject == null) {
throw new IllegalArgumentException("Key not found in propertyConfig: " + key);
}
newArray.add(valueNode.isTextual() ? valueObject : valueNode);
});
jsonNode.replace(fieldName, newArray);
entity.setExtension(JsonUtils.treeToValue(jsonNode, Object.class));
}
}
public final void storeExtension(EntityInterface entity) { public final void storeExtension(EntityInterface entity) {
JsonNode jsonNode = JsonUtils.valueToTree(entity.getExtension()); JsonNode jsonNode = JsonUtils.valueToTree(entity.getExtension());
Iterator<Entry<String, JsonNode>> customFields = jsonNode.fields(); Iterator<Entry<String, JsonNode>> customFields = jsonNode.fields();

View File

@ -28,6 +28,7 @@ import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
import java.util.UUID; import java.util.UUID;
import java.util.stream.Collectors;
import javax.ws.rs.core.UriInfo; import javax.ws.rs.core.UriInfo;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.tuple.Triple; import org.apache.commons.lang3.tuple.Triple;
@ -40,6 +41,8 @@ import org.openmetadata.schema.type.EntityReference;
import org.openmetadata.schema.type.Include; import org.openmetadata.schema.type.Include;
import org.openmetadata.schema.type.Relationship; import org.openmetadata.schema.type.Relationship;
import org.openmetadata.schema.type.customproperties.EnumConfig; import org.openmetadata.schema.type.customproperties.EnumConfig;
import org.openmetadata.schema.type.customproperties.EnumWithDescriptionsConfig;
import org.openmetadata.schema.type.customproperties.Value;
import org.openmetadata.service.Entity; import org.openmetadata.service.Entity;
import org.openmetadata.service.TypeRegistry; import org.openmetadata.service.TypeRegistry;
import org.openmetadata.service.resources.types.TypeResource; import org.openmetadata.service.resources.types.TypeResource;
@ -169,24 +172,9 @@ public class TypeRepository extends EntityRepository<Type> {
private void validateProperty(CustomProperty customProperty) { private void validateProperty(CustomProperty customProperty) {
switch (customProperty.getPropertyType().getName()) { switch (customProperty.getPropertyType().getName()) {
case "enum" -> { case "enum" -> validateEnumConfig(customProperty.getCustomPropertyConfig());
CustomPropertyConfig config = customProperty.getCustomPropertyConfig(); case "enumWithDescriptions" -> validateEnumWithDescriptionsConfig(
if (config != null) { customProperty.getCustomPropertyConfig());
EnumConfig enumConfig = JsonUtils.convertValue(config.getConfig(), EnumConfig.class);
if (enumConfig == null
|| (enumConfig.getValues() != null && enumConfig.getValues().isEmpty())) {
throw new IllegalArgumentException(
"Enum Custom Property Type must have EnumConfig populated with values.");
} else if (enumConfig.getValues() != null
&& enumConfig.getValues().stream().distinct().count()
!= enumConfig.getValues().size()) {
throw new IllegalArgumentException(
"Enum Custom Property values cannot have duplicates.");
}
} else {
throw new IllegalArgumentException("Enum Custom Property Type must have EnumConfig.");
}
}
case "date-cp" -> validateDateFormat( case "date-cp" -> validateDateFormat(
customProperty.getCustomPropertyConfig(), getDateTokens(), "Invalid date format"); customProperty.getCustomPropertyConfig(), getDateTokens(), "Invalid date format");
case "dateTime-cp" -> validateDateFormat( case "dateTime-cp" -> validateDateFormat(
@ -229,6 +217,44 @@ public class TypeRepository extends EntityRepository<Type> {
return Set.of('H', 'h', 'm', 's', 'a', 'S'); return Set.of('H', 'h', 'm', 's', 'a', 'S');
} }
private void validateEnumConfig(CustomPropertyConfig config) {
if (config != null) {
EnumConfig enumConfig = JsonUtils.convertValue(config.getConfig(), EnumConfig.class);
if (enumConfig == null
|| (enumConfig.getValues() != null && enumConfig.getValues().isEmpty())) {
throw new IllegalArgumentException(
"Enum Custom Property Type must have EnumConfig populated with values.");
} else if (enumConfig.getValues() != null
&& enumConfig.getValues().stream().distinct().count() != enumConfig.getValues().size()) {
throw new IllegalArgumentException("Enum Custom Property values cannot have duplicates.");
}
} else {
throw new IllegalArgumentException("Enum Custom Property Type must have EnumConfig.");
}
}
private void validateEnumWithDescriptionsConfig(CustomPropertyConfig config) {
if (config != null) {
EnumWithDescriptionsConfig enumWithDescriptionsConfig =
JsonUtils.convertValue(config.getConfig(), EnumWithDescriptionsConfig.class);
if (enumWithDescriptionsConfig == null
|| (enumWithDescriptionsConfig.getValues() != null
&& enumWithDescriptionsConfig.getValues().isEmpty())) {
throw new IllegalArgumentException(
"EnumWithDescriptions Custom Property Type must have customPropertyConfig populated with values.");
}
JsonUtils.validateJsonSchema(config.getConfig(), EnumWithDescriptionsConfig.class);
if (enumWithDescriptionsConfig.getValues().stream().map(Value::getKey).distinct().count()
!= enumWithDescriptionsConfig.getValues().size()) {
throw new IllegalArgumentException(
"EnumWithDescriptions Custom Property key cannot have duplicates.");
}
} else {
throw new IllegalArgumentException(
"EnumWithDescriptions Custom Property Type must have customPropertyConfig.");
}
}
/** Handles entity updated from PUT and POST operation. */ /** Handles entity updated from PUT and POST operation. */
public class TypeUpdater extends EntityUpdater { public class TypeUpdater extends EntityUpdater {
public TypeUpdater(Type original, Type updated, Operation operation) { public TypeUpdater(Type original, Type updated, Operation operation) {
@ -387,6 +413,27 @@ public class TypeRepository extends EntityRepository<Type> {
throw new IllegalArgumentException( throw new IllegalArgumentException(
"Existing Enum Custom Property values cannot be removed."); "Existing Enum Custom Property values cannot be removed.");
} }
} else if (origProperty.getPropertyType().getName().equals("enumWithDescriptions")) {
EnumWithDescriptionsConfig origConfig =
JsonUtils.convertValue(
origProperty.getCustomPropertyConfig().getConfig(),
EnumWithDescriptionsConfig.class);
EnumWithDescriptionsConfig updatedConfig =
JsonUtils.convertValue(
updatedProperty.getCustomPropertyConfig().getConfig(),
EnumWithDescriptionsConfig.class);
HashSet<String> updatedValues =
updatedConfig.getValues().stream()
.map(Value::getKey)
.collect(Collectors.toCollection(HashSet::new));
if (updatedValues.size() != updatedConfig.getValues().size()) {
throw new IllegalArgumentException(
"EnumWithDescriptions Custom Property values cannot have duplicates.");
} else if (!updatedValues.containsAll(
origConfig.getValues().stream().map(Value::getKey).collect(Collectors.toSet()))) {
throw new IllegalArgumentException(
"Existing EnumWithDescriptions Custom Property values cannot be removed.");
}
} }
} }
} }

View File

@ -392,6 +392,8 @@ public abstract class EntityResourceTest<T extends EntityInterface, K extends Cr
public static Type ENUM_TYPE; public static Type ENUM_TYPE;
public static Type ENUM_WITH_DESCRIPTIONS_TYPE;
// Run webhook related tests randomly. This will ensure these tests are not run for every entity // Run webhook related tests randomly. This will ensure these tests are not run for every entity
// evey time junit tests are run to save time. But over the course of development of a release, // evey time junit tests are run to save time. But over the course of development of a release,
// when tests are run enough times, the webhook tests are run for all the entities. // when tests are run enough times, the webhook tests are run for all the entities.

View File

@ -45,6 +45,8 @@ import org.openmetadata.schema.type.ChangeDescription;
import org.openmetadata.schema.type.CustomPropertyConfig; import org.openmetadata.schema.type.CustomPropertyConfig;
import org.openmetadata.schema.type.EntityReference; import org.openmetadata.schema.type.EntityReference;
import org.openmetadata.schema.type.customproperties.EnumConfig; import org.openmetadata.schema.type.customproperties.EnumConfig;
import org.openmetadata.schema.type.customproperties.EnumWithDescriptionsConfig;
import org.openmetadata.schema.type.customproperties.Value;
import org.openmetadata.service.Entity; import org.openmetadata.service.Entity;
import org.openmetadata.service.resources.EntityResourceTest; import org.openmetadata.service.resources.EntityResourceTest;
import org.openmetadata.service.resources.types.TypeResource; import org.openmetadata.service.resources.types.TypeResource;
@ -70,6 +72,7 @@ public class TypeResourceTest extends EntityResourceTest<Type, CreateType> {
INT_TYPE = getEntityByName("integer", "", ADMIN_AUTH_HEADERS); INT_TYPE = getEntityByName("integer", "", ADMIN_AUTH_HEADERS);
STRING_TYPE = getEntityByName("string", "", ADMIN_AUTH_HEADERS); STRING_TYPE = getEntityByName("string", "", ADMIN_AUTH_HEADERS);
ENUM_TYPE = getEntityByName("enum", "", ADMIN_AUTH_HEADERS); ENUM_TYPE = getEntityByName("enum", "", ADMIN_AUTH_HEADERS);
ENUM_WITH_DESCRIPTIONS_TYPE = getEntityByName("enumWithDescriptions", "", ADMIN_AUTH_HEADERS);
} }
@Override @Override
@ -280,6 +283,177 @@ public class TypeResourceTest extends EntityResourceTest<Type, CreateType> {
new ArrayList<>(List.of(fieldA, fieldB)), tableEntity.getCustomProperties());*/ new ArrayList<>(List.of(fieldA, fieldB)), tableEntity.getCustomProperties());*/
} }
@Test
void put_patch_customProperty_enumWithDescriptions_200() throws IOException {
Type databaseEntity = getEntityByName("database", "customProperties", ADMIN_AUTH_HEADERS);
// Add a custom property of type enumWithDescriptions with PUT
CustomProperty enumWithDescriptionsFieldA =
new CustomProperty()
.withName("enumWithDescriptionsTest")
.withDescription("enumWithDescriptionsTest")
.withPropertyType(ENUM_WITH_DESCRIPTIONS_TYPE.getEntityReference());
ChangeDescription change = getChangeDescription(databaseEntity, MINOR_UPDATE);
fieldAdded(change, "customProperties", new ArrayList<>(List.of(enumWithDescriptionsFieldA)));
Type finalDatabaseEntity = databaseEntity;
ChangeDescription finalChange = change;
assertResponseContains(
() ->
addCustomPropertyAndCheck(
finalDatabaseEntity.getId(),
enumWithDescriptionsFieldA,
ADMIN_AUTH_HEADERS,
MINOR_UPDATE,
finalChange),
Status.BAD_REQUEST,
"EnumWithDescriptions Custom Property Type must have customPropertyConfig.");
enumWithDescriptionsFieldA.setCustomPropertyConfig(
new CustomPropertyConfig().withConfig(new EnumWithDescriptionsConfig()));
ChangeDescription change1 = getChangeDescription(databaseEntity, MINOR_UPDATE);
Type databaseEntity1 = databaseEntity;
assertResponseContains(
() ->
addCustomPropertyAndCheck(
databaseEntity1.getId(),
enumWithDescriptionsFieldA,
ADMIN_AUTH_HEADERS,
MINOR_UPDATE,
change1),
Status.BAD_REQUEST,
"EnumWithDescriptions Custom Property Type must have customPropertyConfig populated with values.");
List<Value> valuesWithDuplicateKey =
List.of(
new Value().withKey("A").withDescription("Description A"),
new Value().withKey("B").withDescription("Description B"),
new Value().withKey("C").withDescription("Description C"),
new Value().withKey("C").withDescription("Description C"));
enumWithDescriptionsFieldA.setCustomPropertyConfig(
new CustomPropertyConfig()
.withConfig(new EnumWithDescriptionsConfig().withValues(valuesWithDuplicateKey)));
ChangeDescription change7 = getChangeDescription(databaseEntity, MINOR_UPDATE);
Type databaseEntity2 = databaseEntity;
assertResponseContains(
() ->
addCustomPropertyAndCheck(
databaseEntity2.getId(),
enumWithDescriptionsFieldA,
ADMIN_AUTH_HEADERS,
MINOR_UPDATE,
change7),
Status.BAD_REQUEST,
"EnumWithDescriptions Custom Property key cannot have duplicates.");
List<Value> valuesWithUniqueKey =
List.of(
new Value().withKey("A").withDescription("Description A"),
new Value().withKey("B").withDescription("Description B"),
new Value().withKey("C").withDescription("Description C"));
enumWithDescriptionsFieldA.setCustomPropertyConfig(
new CustomPropertyConfig()
.withConfig(new EnumWithDescriptionsConfig().withValues(valuesWithUniqueKey)));
databaseEntity =
addCustomPropertyAndCheck(
databaseEntity.getId(),
enumWithDescriptionsFieldA,
ADMIN_AUTH_HEADERS,
MINOR_UPDATE,
change);
assertCustomProperties(
new ArrayList<>(List.of(enumWithDescriptionsFieldA)), databaseEntity.getCustomProperties());
CustomPropertyConfig prevConfig = enumWithDescriptionsFieldA.getCustomPropertyConfig();
// Changing custom property description with PUT
enumWithDescriptionsFieldA.withDescription("updatedEnumWithDescriptionsTest");
ChangeDescription change2 = getChangeDescription(databaseEntity, MINOR_UPDATE);
fieldUpdated(
change2,
EntityUtil.getCustomField(enumWithDescriptionsFieldA, "description"),
"enumWithDescriptionsTest",
"updatedEnumWithDescriptionsTest");
databaseEntity =
addCustomPropertyAndCheck(
databaseEntity.getId(),
enumWithDescriptionsFieldA,
ADMIN_AUTH_HEADERS,
MINOR_UPDATE,
change2);
assertCustomProperties(
new ArrayList<>(List.of(enumWithDescriptionsFieldA)), databaseEntity.getCustomProperties());
List<Value> valuesWithUniqueKeyAB =
List.of(
new Value().withKey("A").withDescription("Description A"),
new Value().withKey("B").withDescription("Description B"));
enumWithDescriptionsFieldA.setCustomPropertyConfig(
new CustomPropertyConfig()
.withConfig(new EnumWithDescriptionsConfig().withValues(valuesWithUniqueKeyAB)));
ChangeDescription change3 = getChangeDescription(databaseEntity, MINOR_UPDATE);
assertResponseContains(
() ->
addCustomPropertyAndCheck(
databaseEntity1.getId(),
enumWithDescriptionsFieldA,
ADMIN_AUTH_HEADERS,
MINOR_UPDATE,
change3),
Status.BAD_REQUEST,
"Existing EnumWithDescriptions Custom Property values cannot be removed.");
enumWithDescriptionsFieldA.setCustomPropertyConfig(
new CustomPropertyConfig()
.withConfig(new EnumWithDescriptionsConfig().withValues(valuesWithDuplicateKey)));
ChangeDescription change4 = getChangeDescription(databaseEntity, MINOR_UPDATE);
assertResponseContains(
() ->
addCustomPropertyAndCheck(
databaseEntity1.getId(),
enumWithDescriptionsFieldA,
ADMIN_AUTH_HEADERS,
MINOR_UPDATE,
change4),
Status.BAD_REQUEST,
"EnumWithDescriptions Custom Property key cannot have duplicates.");
valuesWithUniqueKey =
List.of(
new Value().withKey("A").withDescription("Description A"),
new Value().withKey("B").withDescription("Description B"),
new Value().withKey("C").withDescription("Description C"),
new Value().withKey("D").withDescription("Description D"));
ChangeDescription change5 = getChangeDescription(databaseEntity, MINOR_UPDATE);
enumWithDescriptionsFieldA.setCustomPropertyConfig(
new CustomPropertyConfig()
.withConfig(new EnumWithDescriptionsConfig().withValues(valuesWithUniqueKey)));
fieldUpdated(
change5,
EntityUtil.getCustomField(enumWithDescriptionsFieldA, "customPropertyConfig"),
prevConfig,
enumWithDescriptionsFieldA.getCustomPropertyConfig());
databaseEntity =
addCustomPropertyAndCheck(
databaseEntity.getId(),
enumWithDescriptionsFieldA,
ADMIN_AUTH_HEADERS,
MINOR_UPDATE,
change5);
assertCustomProperties(
new ArrayList<>(List.of(enumWithDescriptionsFieldA)), databaseEntity.getCustomProperties());
// Changing custom property description with PATCH
// Changes from this PATCH is consolidated with the previous changes
enumWithDescriptionsFieldA.withDescription("updated2");
String json = JsonUtils.pojoToJson(databaseEntity);
databaseEntity.setCustomProperties(List.of(enumWithDescriptionsFieldA));
change = getChangeDescription(databaseEntity, CHANGE_CONSOLIDATED);
fieldUpdated(
change5,
EntityUtil.getCustomField(enumWithDescriptionsFieldA, "description"),
"updatedEnumWithDescriptionsTest",
"updated2");
databaseEntity =
patchEntityAndCheck(databaseEntity, json, ADMIN_AUTH_HEADERS, CHANGE_CONSOLIDATED, change5);
}
@Test @Test
void put_customPropertyToPropertyType_4xx() { void put_customPropertyToPropertyType_4xx() {
// Adding a custom property to a property type is not allowed (only entity type is allowed) // Adding a custom property to a property type is not allowed (only entity type is allowed)

View File

@ -105,6 +105,22 @@
"type": "string" "type": "string"
} }
}, },
"enumWithDescriptions": {
"$comment": "@om-field-type",
"description": "List of values in Enum with description for each key.",
"type": "array",
"items": {
"type": "object",
"properties": {
"key": {
"type": "string"
},
"description": {
"type": "string"
}
}
}
},
"timezone": { "timezone": {
"description": "Timezone of the user in the format `America/Los_Angeles`, `Brazil/East`, etc.", "description": "Timezone of the user in the format `America/Los_Angeles`, `Brazil/East`, etc.",
"type": "string", "type": "string",

View File

@ -0,0 +1,31 @@
{
"$id": "https://open-metadata.org/schema/type/customPropertyEnumWithDescriptionsConfig.json",
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "EnumWithDescriptionsConfig",
"type": "object",
"javaType": "org.openmetadata.schema.type.customproperties.EnumWithDescriptionsConfig",
"description": "Custom property to define enum values with descriptions and extendable fields",
"properties": {
"multiSelect": {
"type": "boolean",
"default": false
},
"values": {
"type": "array",
"items": {
"type": "object",
"properties": {
"key": {
"type": "string"
},
"description": {
"type": "string"
}
},
"required": ["key","description"],
"additionalProperties": false
}
}
},
"additionalProperties": false
}

View File

@ -33,6 +33,9 @@
}, },
{ {
"$ref": "#/definitions/entityTypes" "$ref": "#/definitions/entityTypes"
},
{
"$ref": "../type/customProperties/enumWithDescriptionsConfig.json"
} }
] ]
} }

View File

@ -48,6 +48,19 @@ export const CUSTOM_PROPERTIES_ENTITIES = {
values: ['enum1', 'enum2', 'enum3'], values: ['enum1', 'enum2', 'enum3'],
multiSelect: false, multiSelect: false,
}, },
enumWithDescriptionConfig: {
values: [
{
key: 'enumWithDescription1',
description: 'This is enumWithDescription1',
},
{
key: 'enumWithDescription2',
description: 'This is enumWithDescription2',
},
],
multiSelect: false,
},
dateFormatConfig: 'yyyy-MM-dd', dateFormatConfig: 'yyyy-MM-dd',
dateTimeFormatConfig: 'yyyy-MM-dd HH:mm:ss', dateTimeFormatConfig: 'yyyy-MM-dd HH:mm:ss',
timeFormatConfig: 'HH:mm:ss', timeFormatConfig: 'HH:mm:ss',
@ -66,6 +79,19 @@ export const CUSTOM_PROPERTIES_ENTITIES = {
values: ['enum1', 'enum2', 'enum3'], values: ['enum1', 'enum2', 'enum3'],
multiSelect: false, multiSelect: false,
}, },
enumWithDescriptionConfig: {
values: [
{
key: 'enumWithDescription1',
description: 'This is enumWithDescription1',
},
{
key: 'enumWithDescription2',
description: 'This is enumWithDescription2',
},
],
multiSelect: false,
},
dateFormatConfig: 'yyyy-MM-dd', dateFormatConfig: 'yyyy-MM-dd',
dateTimeFormatConfig: 'yyyy-MM-dd HH:mm:ss', dateTimeFormatConfig: 'yyyy-MM-dd HH:mm:ss',
timeFormatConfig: 'HH:mm:ss', timeFormatConfig: 'HH:mm:ss',
@ -84,6 +110,19 @@ export const CUSTOM_PROPERTIES_ENTITIES = {
values: ['enum1', 'enum2', 'enum3'], values: ['enum1', 'enum2', 'enum3'],
multiSelect: false, multiSelect: false,
}, },
enumWithDescriptionConfig: {
values: [
{
key: 'enumWithDescription1',
description: 'This is enumWithDescription1',
},
{
key: 'enumWithDescription2',
description: 'This is enumWithDescription2',
},
],
multiSelect: false,
},
dateFormatConfig: 'yyyy-MM-dd', dateFormatConfig: 'yyyy-MM-dd',
dateTimeFormatConfig: 'yyyy-MM-dd HH:mm:ss', dateTimeFormatConfig: 'yyyy-MM-dd HH:mm:ss',
timeFormatConfig: 'HH:mm:ss', timeFormatConfig: 'HH:mm:ss',
@ -102,6 +141,19 @@ export const CUSTOM_PROPERTIES_ENTITIES = {
values: ['enum1', 'enum2', 'enum3'], values: ['enum1', 'enum2', 'enum3'],
multiSelect: false, multiSelect: false,
}, },
enumWithDescriptionConfig: {
values: [
{
key: 'enumWithDescription1',
description: 'This is enumWithDescription1',
},
{
key: 'enumWithDescription2',
description: 'This is enumWithDescription2',
},
],
multiSelect: false,
},
dateFormatConfig: 'yyyy-MM-dd', dateFormatConfig: 'yyyy-MM-dd',
dateTimeFormatConfig: 'yyyy-MM-dd HH:mm:ss', dateTimeFormatConfig: 'yyyy-MM-dd HH:mm:ss',
timeFormatConfig: 'HH:mm:ss', timeFormatConfig: 'HH:mm:ss',
@ -120,6 +172,19 @@ export const CUSTOM_PROPERTIES_ENTITIES = {
values: ['enum1', 'enum2', 'enum3'], values: ['enum1', 'enum2', 'enum3'],
multiSelect: false, multiSelect: false,
}, },
enumWithDescriptionConfig: {
values: [
{
key: 'enumWithDescription1',
description: 'This is enumWithDescription1',
},
{
key: 'enumWithDescription2',
description: 'This is enumWithDescription2',
},
],
multiSelect: false,
},
dateFormatConfig: 'yyyy-MM-dd', dateFormatConfig: 'yyyy-MM-dd',
dateTimeFormatConfig: 'yyyy-MM-dd HH:mm:ss', dateTimeFormatConfig: 'yyyy-MM-dd HH:mm:ss',
timeFormatConfig: 'HH:mm:ss', timeFormatConfig: 'HH:mm:ss',
@ -138,6 +203,19 @@ export const CUSTOM_PROPERTIES_ENTITIES = {
values: ['enum1', 'enum2', 'enum3'], values: ['enum1', 'enum2', 'enum3'],
multiSelect: false, multiSelect: false,
}, },
enumWithDescriptionConfig: {
values: [
{
key: 'enumWithDescription1',
description: 'This is enumWithDescription1',
},
{
key: 'enumWithDescription2',
description: 'This is enumWithDescription2',
},
],
multiSelect: false,
},
dateFormatConfig: 'yyyy-MM-dd', dateFormatConfig: 'yyyy-MM-dd',
dateTimeFormatConfig: 'yyyy-MM-dd HH:mm:ss', dateTimeFormatConfig: 'yyyy-MM-dd HH:mm:ss',
timeFormatConfig: 'HH:mm:ss', timeFormatConfig: 'HH:mm:ss',
@ -156,6 +234,19 @@ export const CUSTOM_PROPERTIES_ENTITIES = {
values: ['enum1', 'enum2', 'enum3'], values: ['enum1', 'enum2', 'enum3'],
multiSelect: true, multiSelect: true,
}, },
enumWithDescriptionConfig: {
values: [
{
key: 'enumWithDescription1',
description: 'This is enumWithDescription1',
},
{
key: 'enumWithDescription2',
description: 'This is enumWithDescription2',
},
],
multiSelect: false,
},
dateFormatConfig: 'yyyy-MM-dd', dateFormatConfig: 'yyyy-MM-dd',
dateTimeFormatConfig: 'yyyy-MM-dd HH:mm:ss', dateTimeFormatConfig: 'yyyy-MM-dd HH:mm:ss',
timeFormatConfig: 'HH:mm:ss', timeFormatConfig: 'HH:mm:ss',
@ -174,6 +265,19 @@ export const CUSTOM_PROPERTIES_ENTITIES = {
values: ['enum1', 'enum2', 'enum3'], values: ['enum1', 'enum2', 'enum3'],
multiSelect: false, multiSelect: false,
}, },
enumWithDescriptionConfig: {
values: [
{
key: 'enumWithDescription1',
description: 'This is enumWithDescription1',
},
{
key: 'enumWithDescription2',
description: 'This is enumWithDescription2',
},
],
multiSelect: false,
},
dateFormatConfig: 'yyyy-MM-dd', dateFormatConfig: 'yyyy-MM-dd',
dateTimeFormatConfig: 'yyyy-MM-dd HH:mm:ss', dateTimeFormatConfig: 'yyyy-MM-dd HH:mm:ss',
timeFormatConfig: 'HH:mm:ss', timeFormatConfig: 'HH:mm:ss',
@ -192,6 +296,19 @@ export const CUSTOM_PROPERTIES_ENTITIES = {
values: ['enum1', 'enum2', 'enum3'], values: ['enum1', 'enum2', 'enum3'],
multiSelect: false, multiSelect: false,
}, },
enumWithDescriptionConfig: {
values: [
{
key: 'enumWithDescription1',
description: 'This is enumWithDescription1',
},
{
key: 'enumWithDescription2',
description: 'This is enumWithDescription2',
},
],
multiSelect: false,
},
dateFormatConfig: 'yyyy-MM-dd', dateFormatConfig: 'yyyy-MM-dd',
dateTimeFormatConfig: 'yyyy-MM-dd HH:mm:ss', dateTimeFormatConfig: 'yyyy-MM-dd HH:mm:ss',
timeFormatConfig: 'HH:mm:ss', timeFormatConfig: 'HH:mm:ss',
@ -210,6 +327,19 @@ export const CUSTOM_PROPERTIES_ENTITIES = {
values: ['enum1', 'enum2', 'enum3'], values: ['enum1', 'enum2', 'enum3'],
multiSelect: false, multiSelect: false,
}, },
enumWithDescriptionConfig: {
values: [
{
key: 'enumWithDescription1',
description: 'This is enumWithDescription1',
},
{
key: 'enumWithDescription2',
description: 'This is enumWithDescription2',
},
],
multiSelect: false,
},
dateFormatConfig: 'yyyy-MM-dd', dateFormatConfig: 'yyyy-MM-dd',
dateTimeFormatConfig: 'yyyy-MM-dd HH:mm:ss', dateTimeFormatConfig: 'yyyy-MM-dd HH:mm:ss',
timeFormatConfig: 'HH:mm:ss', timeFormatConfig: 'HH:mm:ss',
@ -228,6 +358,19 @@ export const CUSTOM_PROPERTIES_ENTITIES = {
values: ['enum1', 'enum2', 'enum3'], values: ['enum1', 'enum2', 'enum3'],
multiSelect: false, multiSelect: false,
}, },
enumWithDescriptionConfig: {
values: [
{
key: 'enumWithDescription1',
description: 'This is enumWithDescription1',
},
{
key: 'enumWithDescription2',
description: 'This is enumWithDescription2',
},
],
multiSelect: false,
},
dateFormatConfig: 'yyyy-MM-dd', dateFormatConfig: 'yyyy-MM-dd',
dateTimeFormatConfig: 'yyyy-MM-dd HH:mm:ss', dateTimeFormatConfig: 'yyyy-MM-dd HH:mm:ss',
timeFormatConfig: 'HH:mm:ss', timeFormatConfig: 'HH:mm:ss',
@ -245,6 +388,19 @@ export const CUSTOM_PROPERTIES_ENTITIES = {
values: ['enum1', 'enum2', 'enum3'], values: ['enum1', 'enum2', 'enum3'],
multiSelect: false, multiSelect: false,
}, },
enumWithDescriptionConfig: {
values: [
{
key: 'enumWithDescription1',
description: 'This is enumWithDescription1',
},
{
key: 'enumWithDescription2',
description: 'This is enumWithDescription2',
},
],
multiSelect: false,
},
dateFormatConfig: 'yyyy-MM-dd', dateFormatConfig: 'yyyy-MM-dd',
dateTimeFormatConfig: 'yyyy-MM-dd HH:mm:ss', dateTimeFormatConfig: 'yyyy-MM-dd HH:mm:ss',
timeFormatConfig: 'HH:mm:ss', timeFormatConfig: 'HH:mm:ss',
@ -263,6 +419,19 @@ export const CUSTOM_PROPERTIES_ENTITIES = {
values: ['enum1', 'enum2', 'enum3'], values: ['enum1', 'enum2', 'enum3'],
multiSelect: false, multiSelect: false,
}, },
enumWithDescriptionConfig: {
values: [
{
key: 'enumWithDescription1',
description: 'This is enumWithDescription1',
},
{
key: 'enumWithDescription2',
description: 'This is enumWithDescription2',
},
],
multiSelect: false,
},
dateFormatConfig: 'yyyy-MM-dd', dateFormatConfig: 'yyyy-MM-dd',
dateTimeFormatConfig: 'yyyy-MM-dd HH:mm:ss', dateTimeFormatConfig: 'yyyy-MM-dd HH:mm:ss',
timeFormatConfig: 'HH:mm:ss', timeFormatConfig: 'HH:mm:ss',
@ -280,6 +449,19 @@ export const CUSTOM_PROPERTIES_ENTITIES = {
values: ['enum1', 'enum2', 'enum3'], values: ['enum1', 'enum2', 'enum3'],
multiSelect: false, multiSelect: false,
}, },
enumWithDescriptionConfig: {
values: [
{
key: 'enumWithDescription1',
description: 'This is enumWithDescription1',
},
{
key: 'enumWithDescription2',
description: 'This is enumWithDescription2',
},
],
multiSelect: false,
},
dateFormatConfig: 'yyyy-MM-dd', dateFormatConfig: 'yyyy-MM-dd',
dateTimeFormatConfig: 'yyyy-MM-dd HH:mm:ss', dateTimeFormatConfig: 'yyyy-MM-dd HH:mm:ss',
timeFormatConfig: 'HH:mm:ss', timeFormatConfig: 'HH:mm:ss',
@ -297,6 +479,19 @@ export const CUSTOM_PROPERTIES_ENTITIES = {
values: ['enum1', 'enum2', 'enum3'], values: ['enum1', 'enum2', 'enum3'],
multiSelect: false, multiSelect: false,
}, },
enumWithDescriptionConfig: {
values: [
{
key: 'enumWithDescription1',
description: 'This is enumWithDescription1',
},
{
key: 'enumWithDescription2',
description: 'This is enumWithDescription2',
},
],
multiSelect: false,
},
dateFormatConfig: 'yyyy-MM-dd', dateFormatConfig: 'yyyy-MM-dd',
dateTimeFormatConfig: 'yyyy-MM-dd HH:mm:ss', dateTimeFormatConfig: 'yyyy-MM-dd HH:mm:ss',
timeFormatConfig: 'HH:mm:ss', timeFormatConfig: 'HH:mm:ss',
@ -314,6 +509,19 @@ export const CUSTOM_PROPERTIES_ENTITIES = {
values: ['enum1', 'enum2', 'enum3'], values: ['enum1', 'enum2', 'enum3'],
multiSelect: false, multiSelect: false,
}, },
enumWithDescriptionConfig: {
values: [
{
key: 'enumWithDescription1',
description: 'This is enumWithDescription1',
},
{
key: 'enumWithDescription2',
description: 'This is enumWithDescription2',
},
],
multiSelect: false,
},
dateFormatConfig: 'yyyy-MM-dd', dateFormatConfig: 'yyyy-MM-dd',
dateTimeFormatConfig: 'yyyy-MM-dd HH:mm:ss', dateTimeFormatConfig: 'yyyy-MM-dd HH:mm:ss',
timeFormatConfig: 'HH:mm:ss', timeFormatConfig: 'HH:mm:ss',

View File

@ -60,12 +60,24 @@ test('CustomProperty Dashboard Filter', async ({ page }) => {
await test.step('Add Custom Property in Dashboard', async () => { await test.step('Add Custom Property in Dashboard', async () => {
await dashboardEntity.visitEntityPage(page); await dashboardEntity.visitEntityPage(page);
const container = page.locator(
`[data-testid="custom-property-${propertyName}-card"]`
);
await page.getByTestId('custom_properties').click(); await page.getByTestId('custom_properties').click();
await page await expect(
.getByRole('row', { name: `${propertyName} No data` }) page.locator(
.locator('svg') `[data-testid="custom-property-${propertyName}-card"] [data-testid="property-name"]`
.click(); )
).toHaveText(propertyName);
const editButton = page.locator(
`[data-testid="custom-property-${propertyName}-card"] [data-testid="edit-icon"]`
);
await editButton.scrollIntoViewIfNeeded();
await editButton.click({ force: true });
await page.getByTestId('value-input').fill(propertyValue); await page.getByTestId('value-input').fill(propertyValue);
@ -75,9 +87,7 @@ test('CustomProperty Dashboard Filter', async ({ page }) => {
await saveResponse; await saveResponse;
expect( await expect(container.getByTestId('value')).toContainText(propertyValue);
page.getByLabel('Custom Properties').getByTestId('value')
).toContainText(propertyValue);
}); });
await test.step( await test.step(

View File

@ -45,13 +45,46 @@ test.describe('Custom properties with custom property config', () => {
enumConfig: entity.enumConfig, enumConfig: entity.enumConfig,
}); });
await editCreatedProperty(page, propertyName); await editCreatedProperty(page, propertyName, 'Enum');
await deleteCreatedProperty(page, propertyName); await deleteCreatedProperty(page, propertyName);
}); });
}); });
}); });
test.describe(
'Add update and delete Enum With Descriptions custom properties',
() => {
Object.values(CUSTOM_PROPERTIES_ENTITIES).forEach(async (entity) => {
const propertyName = `pwcustomproperty${entity.name}test${uuid()}`;
test(`Add Enum With Descriptions custom property for ${entity.name}`, async ({
page,
}) => {
test.slow(true);
await settingClick(page, entity.entityApiType, true);
await addCustomPropertiesForEntity({
page,
propertyName,
customPropertyData: entity,
customType: 'Enum With Descriptions',
enumWithDescriptionConfig: entity.enumWithDescriptionConfig,
});
await editCreatedProperty(
page,
propertyName,
'Enum With Descriptions'
);
await deleteCreatedProperty(page, propertyName);
});
});
}
);
test.describe( test.describe(
'Add update and delete Entity Reference custom properties', 'Add update and delete Entity Reference custom properties',
() => { () => {
@ -73,7 +106,7 @@ test.describe('Custom properties with custom property config', () => {
entityReferenceConfig: entity.entityReferenceConfig, entityReferenceConfig: entity.entityReferenceConfig,
}); });
await editCreatedProperty(page, propertyName); await editCreatedProperty(page, propertyName, 'Entity Reference');
await deleteCreatedProperty(page, propertyName); await deleteCreatedProperty(page, propertyName);
}); });
@ -102,7 +135,11 @@ test.describe('Custom properties with custom property config', () => {
entityReferenceConfig: entity.entityReferenceConfig, entityReferenceConfig: entity.entityReferenceConfig,
}); });
await editCreatedProperty(page, propertyName); await editCreatedProperty(
page,
propertyName,
'Entity Reference List'
);
await deleteCreatedProperty(page, propertyName); await deleteCreatedProperty(page, propertyName);
}); });

View File

@ -44,6 +44,7 @@ export enum CustomPropertyTypeByName {
TIME_CP = 'time-cp', TIME_CP = 'time-cp',
DATE_CP = 'date-cp', DATE_CP = 'date-cp',
DATE_TIME_CP = 'dateTime-cp', DATE_TIME_CP = 'dateTime-cp',
ENUM_WITH_DESCRIPTION = 'enumWithDescriptions',
} }
export interface CustomProperty { export interface CustomProperty {
@ -66,12 +67,18 @@ export const setValueForProperty = async (data: {
const { page, propertyName, value, propertyType, endpoint } = data; const { page, propertyName, value, propertyType, endpoint } = data;
await page.click('[data-testid="custom_properties"]'); await page.click('[data-testid="custom_properties"]');
await expect(page.getByRole('cell', { name: propertyName })).toContainText( const container = page.locator(
propertyName `[data-testid="custom-property-${propertyName}-card"]`
); );
await expect(
page.locator(
`[data-testid="custom-property-${propertyName}-card"] [data-testid="property-name"]`
)
).toHaveText(propertyName);
const editButton = page.locator( const editButton = page.locator(
`[data-row-key="${propertyName}"] [data-testid="edit-icon"]` `[data-testid="custom-property-${propertyName}-card"] [data-testid="edit-icon"]`
); );
await editButton.scrollIntoViewIfNeeded(); await editButton.scrollIntoViewIfNeeded();
await editButton.click({ force: true }); await editButton.click({ force: true });
@ -96,14 +103,14 @@ export const setValueForProperty = async (data: {
case 'email': case 'email':
await page.locator('[data-testid="email-input"]').isVisible(); await page.locator('[data-testid="email-input"]').isVisible();
await page.locator('[data-testid="email-input"]').fill(value); await page.locator('[data-testid="email-input"]').fill(value);
await page.locator('[data-testid="inline-save-btn"]').click(); await container.locator('[data-testid="inline-save-btn"]').click();
break; break;
case 'duration': case 'duration':
await page.locator('[data-testid="duration-input"]').isVisible(); await page.locator('[data-testid="duration-input"]').isVisible();
await page.locator('[data-testid="duration-input"]').fill(value); await page.locator('[data-testid="duration-input"]').fill(value);
await page.locator('[data-testid="inline-save-btn"]').click(); await container.locator('[data-testid="inline-save-btn"]').click();
break; break;
@ -112,21 +119,30 @@ export const setValueForProperty = async (data: {
await page.fill('#enumValues', value, { force: true }); await page.fill('#enumValues', value, { force: true });
await page.press('#enumValues', 'Enter'); await page.press('#enumValues', 'Enter');
await clickOutside(page); await clickOutside(page);
await page.click('[data-testid="inline-save-btn"]'); await container.locator('[data-testid="inline-save-btn"]').click();
break;
case 'enumWithDescriptions':
await page.click('#enumWithDescriptionValues');
await page.fill('#enumWithDescriptionValues', value, { force: true });
await page.press('#enumWithDescriptionValues', 'Enter');
await clickOutside(page);
await container.locator('[data-testid="inline-save-btn"]').click();
break; break;
case 'sqlQuery': case 'sqlQuery':
await page.locator("pre[role='presentation']").last().click(); await page.locator("pre[role='presentation']").last().click();
await page.keyboard.type(value); await page.keyboard.type(value);
await page.locator('[data-testid="inline-save-btn"]').click(); await container.locator('[data-testid="inline-save-btn"]').click();
break; break;
case 'timestamp': case 'timestamp':
await page.locator('[data-testid="timestamp-input"]').isVisible(); await page.locator('[data-testid="timestamp-input"]').isVisible();
await page.locator('[data-testid="timestamp-input"]').fill(value); await page.locator('[data-testid="timestamp-input"]').fill(value);
await page.locator('[data-testid="inline-save-btn"]').click(); await container.locator('[data-testid="inline-save-btn"]').click();
break; break;
@ -136,7 +152,7 @@ export const setValueForProperty = async (data: {
await page.locator('[data-testid="start-input"]').fill(startValue); await page.locator('[data-testid="start-input"]').fill(startValue);
await page.locator('[data-testid="end-input"]').isVisible(); await page.locator('[data-testid="end-input"]').isVisible();
await page.locator('[data-testid="end-input"]').fill(endValue); await page.locator('[data-testid="end-input"]').fill(endValue);
await page.locator('[data-testid="inline-save-btn"]').click(); await container.locator('[data-testid="inline-save-btn"]').click();
break; break;
} }
@ -146,7 +162,7 @@ export const setValueForProperty = async (data: {
await page.locator('[data-testid="time-picker"]').click(); await page.locator('[data-testid="time-picker"]').click();
await page.locator('[data-testid="time-picker"]').fill(value); await page.locator('[data-testid="time-picker"]').fill(value);
await page.getByRole('button', { name: 'OK', exact: true }).click(); await page.getByRole('button', { name: 'OK', exact: true }).click();
await page.locator('[data-testid="inline-save-btn"]').click(); await container.locator('[data-testid="inline-save-btn"]').click();
break; break;
} }
@ -161,7 +177,7 @@ export const setValueForProperty = async (data: {
} else { } else {
await page.getByText('Today', { exact: true }).click(); await page.getByText('Today', { exact: true }).click();
} }
await page.locator('[data-testid="inline-save-btn"]').click(); await container.locator('[data-testid="inline-save-btn"]').click();
break; break;
} }
@ -171,7 +187,7 @@ export const setValueForProperty = async (data: {
case 'number': case 'number':
await page.locator('[data-testid="value-input"]').isVisible(); await page.locator('[data-testid="value-input"]').isVisible();
await page.locator('[data-testid="value-input"]').fill(value); await page.locator('[data-testid="value-input"]').fill(value);
await page.locator('[data-testid="inline-save-btn"]').click(); await container.locator('[data-testid="inline-save-btn"]').click();
break; break;
@ -191,7 +207,9 @@ export const setValueForProperty = async (data: {
await page.locator(`[data-testid="${val}"]`).click(); await page.locator(`[data-testid="${val}"]`).click();
} }
await page.locator('[data-testid="inline-save-btn"]').click(); await clickOutside(page);
await container.locator('[data-testid="inline-save-btn"]').click();
break; break;
} }
@ -208,23 +226,43 @@ export const validateValueForProperty = async (data: {
const { page, propertyName, value, propertyType } = data; const { page, propertyName, value, propertyType } = data;
await page.click('[data-testid="custom_properties"]'); await page.click('[data-testid="custom_properties"]');
const container = page.locator(
`[data-testid="custom-property-${propertyName}-card"]`
);
const toggleBtnVisibility = await container
.locator(`[data-testid="toggle-${propertyName}"]`)
.isVisible();
if (toggleBtnVisibility) {
await container.locator(`[data-testid="toggle-${propertyName}"]`).click();
}
if (propertyType === 'enum') { if (propertyType === 'enum') {
await expect( await expect(container.getByTestId('enum-value')).toContainText(value);
page.getByLabel('Custom Properties').getByTestId('enum-value')
).toContainText(value);
} else if (propertyType === 'timeInterval') { } else if (propertyType === 'timeInterval') {
const [startValue, endValue] = value.split(','); const [startValue, endValue] = value.split(',');
await expect( await expect(container.getByTestId('time-interval-value')).toContainText(
page.getByLabel('Custom Properties').getByTestId('time-interval-value') startValue
).toContainText(startValue); );
await expect( await expect(container.getByTestId('time-interval-value')).toContainText(
page.getByLabel('Custom Properties').getByTestId('time-interval-value') endValue
).toContainText(endValue); );
} else if (propertyType === 'sqlQuery') { } else if (propertyType === 'sqlQuery') {
await expect(container.locator('.CodeMirror-scroll')).toContainText(value);
} else if (propertyType === 'enumWithDescriptions') {
await expect( await expect(
page.getByLabel('Custom Properties').locator('.CodeMirror-scroll') container.locator('[data-testid="enum-with-description-table"]')
).toContainText(value); ).toBeVisible();
await expect(
container
.locator('[data-testid="enum-with-description-table"]')
.getByText(value, {
exact: true,
})
).toBeVisible();
} else if ( } else if (
![ ![
'entityReference', 'entityReference',
@ -233,9 +271,7 @@ export const validateValueForProperty = async (data: {
'dateTime-cp', 'dateTime-cp',
].includes(propertyType) ].includes(propertyType)
) { ) {
await expect(page.getByRole('row', { name: propertyName })).toContainText( await expect(container).toContainText(value.replace(/\*|_/gi, ''));
value.replace(/\*|_/gi, '')
);
} }
}; };
@ -280,6 +316,11 @@ export const getPropertyValues = (
value: 'small', value: 'small',
newValue: 'medium', newValue: 'medium',
}; };
case 'enumWithDescriptions':
return {
value: 'enumWithDescription1',
newValue: 'enumWithDescription2',
};
case 'sqlQuery': case 'sqlQuery':
return { return {
value: 'Select * from table', value: 'Select * from table',
@ -406,6 +447,25 @@ export const createCustomPropertyForEntity = async (
}, },
} }
: {}), : {}),
...(item.name === 'enumWithDescriptions'
? {
customPropertyConfig: {
config: {
multiSelect: true,
values: [
{
key: 'enumWithDescription1',
description: 'This is enumWithDescription1',
},
{
key: 'enumWithDescription2',
description: 'This is enumWithDescription2',
},
],
},
},
}
: {}),
...(['entityReference', 'entityReferenceList'].includes(item.name) ...(['entityReference', 'entityReferenceList'].includes(item.name)
? { ? {
customPropertyConfig: { customPropertyConfig: {
@ -474,12 +534,17 @@ export const addCustomPropertiesForEntity = async ({
enumConfig, enumConfig,
formatConfig, formatConfig,
entityReferenceConfig, entityReferenceConfig,
enumWithDescriptionConfig,
}: { }: {
page: Page; page: Page;
propertyName: string; propertyName: string;
customPropertyData: { description: string }; customPropertyData: { description: string };
customType: string; customType: string;
enumConfig?: { values: string[]; multiSelect: boolean }; enumConfig?: { values: string[]; multiSelect: boolean };
enumWithDescriptionConfig?: {
values: { key: string; description: string }[];
multiSelect: boolean;
};
formatConfig?: string; formatConfig?: string;
entityReferenceConfig?: string[]; entityReferenceConfig?: string[];
}) => { }) => {
@ -559,6 +624,27 @@ export const addCustomPropertiesForEntity = async ({
await page.click('#root\\/multiSelect'); await page.click('#root\\/multiSelect');
} }
} }
// Enum With Description configuration
if (customType === 'Enum With Descriptions' && enumWithDescriptionConfig) {
for await (const [
index,
val,
] of enumWithDescriptionConfig.values.entries()) {
await page.locator('[data-testid="add-enum-description-config"]').click();
await page.locator(`#key-${index}`).fill(val.key);
await page.locator(descriptionBox).nth(index).fill(val.description);
}
await clickOutside(page);
if (enumWithDescriptionConfig.multiSelect) {
await page.click('#root\\/multiSelect');
}
await page
.locator(descriptionBox)
.nth(2)
.fill(customPropertyData.description);
}
// Entity reference configuration // Entity reference configuration
if ( if (
@ -586,7 +672,9 @@ export const addCustomPropertiesForEntity = async ({
} }
// Description // Description
await page.fill(descriptionBox, customPropertyData.description); if (customType !== 'Enum With Descriptions') {
await page.fill(descriptionBox, customPropertyData.description);
}
const createPropertyPromise = page.waitForResponse( const createPropertyPromise = page.waitForResponse(
'/api/v1/metadata/types/name/*?fields=customProperties' '/api/v1/metadata/types/name/*?fields=customProperties'
@ -621,10 +709,22 @@ export const editCreatedProperty = async (
).toContainText('["enum1","enum2","enum3"]'); ).toContainText('["enum1","enum2","enum3"]');
} }
if (type === 'Enum With Descriptions') {
await expect(
page
.getByRole('row', {
name: `${propertyName} enumWithDescriptions enumWithDescription1`,
})
.getByTestId('enum-with-description-config')
).toBeVisible();
}
await editButton.click(); await editButton.click();
await page.locator(descriptionBox).fill(''); if (type !== 'Enum With Descriptions') {
await page.locator(descriptionBox).fill('This is new description'); await page.locator(descriptionBox).fill('');
await page.locator(descriptionBox).fill('This is new description');
}
if (type === 'Enum') { if (type === 'Enum') {
await page.click('#root\\/customPropertyConfig'); await page.click('#root\\/customPropertyConfig');
@ -632,6 +732,10 @@ export const editCreatedProperty = async (
await page.press('#root\\/customPropertyConfig', 'Enter'); await page.press('#root\\/customPropertyConfig', 'Enter');
await clickOutside(page); await clickOutside(page);
} }
if (type === 'Enum With Descriptions') {
await page.locator(descriptionBox).nth(0).fill('');
await page.locator(descriptionBox).nth(0).fill('This is new description');
}
if (ENTITY_REFERENCE_PROPERTIES.includes(type ?? '')) { if (ENTITY_REFERENCE_PROPERTIES.includes(type ?? '')) {
await page.click('#root\\/customPropertyConfig'); await page.click('#root\\/customPropertyConfig');
@ -664,6 +768,17 @@ export const editCreatedProperty = async (
) )
).toContainText('["enum1","enum2","enum3","updatedValue"]'); ).toContainText('["enum1","enum2","enum3","updatedValue"]');
} }
if (type === 'Enum With Descriptions') {
await expect(
page
.getByRole('row', {
name: `${propertyName} enumWithDescriptions enumWithDescription1`,
})
.getByTestId('enum-with-description-config')
).toBeVisible();
}
if (ENTITY_REFERENCE_PROPERTIES.includes(type ?? '')) { if (ENTITY_REFERENCE_PROPERTIES.includes(type ?? '')) {
await expect( await expect(
page.locator( page.locator(

View File

@ -11,7 +11,8 @@
* limitations under the License. * limitations under the License.
*/ */
import { Button, Col, Form, Row } from 'antd'; import { PlusOutlined } from '@ant-design/icons';
import { Button, Col, Form, Input, Row } from 'antd';
import { AxiosError } from 'axios'; import { AxiosError } from 'axios';
import { t } from 'i18next'; import { t } from 'i18next';
import { isUndefined, map, omit, omitBy, startCase } from 'lodash'; import { isUndefined, map, omit, omitBy, startCase } from 'lodash';
@ -23,8 +24,10 @@ import React, {
useState, useState,
} from 'react'; } from 'react';
import { useHistory, useParams } from 'react-router-dom'; import { useHistory, useParams } from 'react-router-dom';
import { ReactComponent as DeleteIcon } from '../../../../assets/svg/ic-delete.svg';
import { import {
ENTITY_REFERENCE_OPTIONS, ENTITY_REFERENCE_OPTIONS,
ENUM_WITH_DESCRIPTION,
PROPERTY_TYPES_WITH_ENTITY_REFERENCE, PROPERTY_TYPES_WITH_ENTITY_REFERENCE,
PROPERTY_TYPES_WITH_FORMAT, PROPERTY_TYPES_WITH_FORMAT,
SUPPORTED_FORMAT_MAP, SUPPORTED_FORMAT_MAP,
@ -38,6 +41,7 @@ import {
import { EntityType } from '../../../../enums/entity.enum'; import { EntityType } from '../../../../enums/entity.enum';
import { ServiceCategory } from '../../../../enums/service.enum'; import { ServiceCategory } from '../../../../enums/service.enum';
import { Category, Type } from '../../../../generated/entity/type'; import { Category, Type } from '../../../../generated/entity/type';
import { EnumWithDescriptionsConfig } from '../../../../generated/type/customProperties/enumWithDescriptionsConfig';
import { CustomProperty } from '../../../../generated/type/customProperty'; import { CustomProperty } from '../../../../generated/type/customProperty';
import { import {
FieldProp, FieldProp,
@ -54,6 +58,7 @@ import { getSettingOptionByEntityType } from '../../../../utils/GlobalSettingsUt
import { getSettingPath } from '../../../../utils/RouterUtils'; import { getSettingPath } from '../../../../utils/RouterUtils';
import { showErrorToast } from '../../../../utils/ToastUtils'; import { showErrorToast } from '../../../../utils/ToastUtils';
import ResizablePanels from '../../../common/ResizablePanels/ResizablePanels'; import ResizablePanels from '../../../common/ResizablePanels/ResizablePanels';
import RichTextEditor from '../../../common/RichTextEditor/RichTextEditor';
import ServiceDocPanel from '../../../common/ServiceDocPanel/ServiceDocPanel'; import ServiceDocPanel from '../../../common/ServiceDocPanel/ServiceDocPanel';
import TitleBreadcrumb from '../../../common/TitleBreadcrumb/TitleBreadcrumb.component'; import TitleBreadcrumb from '../../../common/TitleBreadcrumb/TitleBreadcrumb.component';
@ -107,6 +112,7 @@ const AddCustomProperty = () => {
hasFormatConfig, hasFormatConfig,
hasEntityReferenceConfig, hasEntityReferenceConfig,
watchedOption, watchedOption,
hasEnumWithDescriptionConfig,
} = useMemo(() => { } = useMemo(() => {
const watchedOption = propertyTypeOptions.find( const watchedOption = propertyTypeOptions.find(
(option) => option.value === watchedPropertyType (option) => option.value === watchedPropertyType
@ -115,6 +121,9 @@ const AddCustomProperty = () => {
const hasEnumConfig = watchedOptionKey === 'enum'; const hasEnumConfig = watchedOptionKey === 'enum';
const hasEnumWithDescriptionConfig =
watchedOptionKey === ENUM_WITH_DESCRIPTION;
const hasFormatConfig = const hasFormatConfig =
PROPERTY_TYPES_WITH_FORMAT.includes(watchedOptionKey); PROPERTY_TYPES_WITH_FORMAT.includes(watchedOptionKey);
@ -126,6 +135,7 @@ const AddCustomProperty = () => {
hasFormatConfig, hasFormatConfig,
hasEntityReferenceConfig, hasEntityReferenceConfig,
watchedOption, watchedOption,
hasEnumWithDescriptionConfig,
}; };
}, [watchedPropertyType, propertyTypeOptions]); }, [watchedPropertyType, propertyTypeOptions]);
@ -166,6 +176,7 @@ const AddCustomProperty = () => {
formatConfig: string; formatConfig: string;
entityReferenceConfig: string[]; entityReferenceConfig: string[];
multiSelect?: boolean; multiSelect?: boolean;
enumWithDescriptionsConfig?: EnumWithDescriptionsConfig['values'];
} }
) => { ) => {
if (isUndefined(typeDetail)) { if (isUndefined(typeDetail)) {
@ -197,6 +208,15 @@ const AddCustomProperty = () => {
}; };
} }
if (hasEnumWithDescriptionConfig) {
customPropertyConfig = {
config: {
multiSelect: Boolean(data?.multiSelect),
values: data.enumWithDescriptionsConfig,
},
};
}
const payload = omitBy( const payload = omitBy(
{ {
...omit(data, [ ...omit(data, [
@ -204,6 +224,7 @@ const AddCustomProperty = () => {
'formatConfig', 'formatConfig',
'entityReferenceConfig', 'entityReferenceConfig',
'enumConfig', 'enumConfig',
'enumWithDescriptionsConfig',
]), ]),
propertyType: { propertyType: {
id: data.propertyType, id: data.propertyType,
@ -393,6 +414,95 @@ const AddCustomProperty = () => {
hasEntityReferenceConfig && hasEntityReferenceConfig &&
generateFormFields([entityReferenceConfigField]) generateFormFields([entityReferenceConfigField])
} }
{hasEnumWithDescriptionConfig && (
<>
<Form.List name="enumWithDescriptionsConfig">
{(fields, { add, remove }) => (
<>
<Form.Item
className="form-item-horizontal"
colon={false}
label={t('label.property')}>
<Button
data-testid="add-enum-description-config"
icon={
<PlusOutlined
style={{ color: 'white', fontSize: '12px' }}
/>
}
size="small"
type="primary"
onClick={() => {
add();
}}
/>
</Form.Item>
{fields.map((field, index) => (
<Row gutter={[8, 0]} key={field.key}>
<Col span={23}>
<Row gutter={[8, 0]}>
<Col span={24}>
<Form.Item
name={[field.name, 'key']}
rules={[
{
required: true,
message: `${t(
'message.field-text-is-required',
{
fieldText: t('label.key'),
}
)}`,
},
]}>
<Input
id={`key-${index}`}
placeholder={t('label.key')}
/>
</Form.Item>
</Col>
<Col span={24}>
<Form.Item
name={[field.name, 'description']}
rules={[
{
required: true,
message: `${t(
'message.field-text-is-required',
{
fieldText: t('label.description'),
}
)}`,
},
]}
trigger="onTextChange"
valuePropName="initialValue">
<RichTextEditor height="200px" />
</Form.Item>
</Col>
</Row>
</Col>
<Col span={1}>
<Button
data-testid={`remove-enum-description-config-${index}`}
icon={<DeleteIcon width={16} />}
size="small"
type="text"
onClick={() => {
remove(field.name);
}}
/>
</Col>
</Row>
))}
</>
)}
</Form.List>
{generateFormFields([multiSelectField])}
</>
)}
{generateFormFields([descriptionField])} {generateFormFields([descriptionField])}
<Row justify="end"> <Row justify="end">
<Col> <Col>

View File

@ -10,17 +10,19 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
import { Button, Space, Tooltip, Typography } from 'antd'; import { Button, Space, Tag, Tooltip, Typography } from 'antd';
import { ColumnsType } from 'antd/lib/table'; import { ColumnsType } from 'antd/lib/table';
import { isArray, isEmpty, isString, isUndefined } from 'lodash'; import { isArray, isEmpty, isString, isUndefined } from 'lodash';
import React, { FC, Fragment, useEffect, useMemo, useState } from 'react'; import React, { FC, Fragment, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { ReactComponent as IconEdit } from '../../../assets/svg/edit-new.svg'; import { ReactComponent as IconEdit } from '../../../assets/svg/edit-new.svg';
import { ReactComponent as IconDelete } from '../../../assets/svg/ic-delete.svg'; import { ReactComponent as IconDelete } from '../../../assets/svg/ic-delete.svg';
import { ENUM_WITH_DESCRIPTION } from '../../../constants/CustomProperty.constants';
import { ADD_CUSTOM_PROPERTIES_DOCS } from '../../../constants/docs.constants'; import { ADD_CUSTOM_PROPERTIES_DOCS } from '../../../constants/docs.constants';
import { NO_PERMISSION_FOR_ACTION } from '../../../constants/HelperTextUtil'; import { NO_PERMISSION_FOR_ACTION } from '../../../constants/HelperTextUtil';
import { TABLE_SCROLL_VALUE } from '../../../constants/Table.constants'; import { TABLE_SCROLL_VALUE } from '../../../constants/Table.constants';
import { ERROR_PLACEHOLDER_TYPE, OPERATION } from '../../../enums/common.enum'; import { ERROR_PLACEHOLDER_TYPE, OPERATION } from '../../../enums/common.enum';
import { EnumWithDescriptionsConfig } from '../../../generated/type/customProperties/enumWithDescriptionsConfig';
import { CustomProperty } from '../../../generated/type/customProperty'; import { CustomProperty } from '../../../generated/type/customProperty';
import { columnSorter, getEntityName } from '../../../utils/EntityUtils'; import { columnSorter, getEntityName } from '../../../utils/EntityUtils';
import ErrorPlaceHolder from '../../common/ErrorWithPlaceholder/ErrorPlaceHolder'; import ErrorPlaceHolder from '../../common/ErrorWithPlaceholder/ErrorPlaceHolder';
@ -68,7 +70,9 @@ export const CustomPropertyTable: FC<CustomPropertyTableProp> = ({
const updatedProperties = customProperties.map((property) => { const updatedProperties = customProperties.map((property) => {
if (property.name === selectedProperty.name) { if (property.name === selectedProperty.name) {
const config = data.customPropertyConfig; const config = data.customPropertyConfig;
const isEnumType = selectedProperty.propertyType.name === 'enum'; const isEnumType =
selectedProperty.propertyType.name === 'enum' ||
selectedProperty.propertyType.name === ENUM_WITH_DESCRIPTION;
return { return {
...property, ...property,
@ -81,7 +85,7 @@ export const CustomPropertyTable: FC<CustomPropertyTableProp> = ({
multiSelect: Boolean(data?.multiSelect), multiSelect: Boolean(data?.multiSelect),
values: config, values: config,
} }
: config, : (config as string[]),
}, },
} }
: {}), : {}),
@ -116,7 +120,7 @@ export const CustomPropertyTable: FC<CustomPropertyTableProp> = ({
title: t('label.type'), title: t('label.type'),
dataIndex: 'propertyType', dataIndex: 'propertyType',
key: 'propertyType', key: 'propertyType',
render: (text) => getEntityName(text), render: (propertyType) => getEntityName(propertyType),
}, },
{ {
title: t('label.config'), title: t('label.config'),
@ -140,6 +144,41 @@ export const CustomPropertyTable: FC<CustomPropertyTableProp> = ({
// If config is an object, then it is a enum config // If config is an object, then it is a enum config
if (!isString(config) && !isArray(config)) { if (!isString(config) && !isArray(config)) {
if (record.propertyType.name === ENUM_WITH_DESCRIPTION) {
const values =
(config?.values as EnumWithDescriptionsConfig['values']) ?? [];
return (
<div
className="w-full d-flex gap-2 flex-column"
data-testid="enum-with-description-config">
<div className="w-full d-flex gap-2 flex-column">
{values.map((value) => (
<Tooltip
key={value.key}
title={value.description}
trigger="hover">
<Tag
style={{
width: 'max-content',
margin: '0px',
border: 'none',
padding: '4px',
background: 'rgba(0, 0, 0, 0.03)',
}}>
{value.key}
</Tag>
</Tooltip>
))}
</div>
<Typography.Text>
{t('label.multi-select')}:{' '}
{config?.multiSelect ? t('label.yes') : t('label.no')}
</Typography.Text>
</div>
);
}
return ( return (
<div <div
className="w-full d-flex gap-2 flex-column" className="w-full d-flex gap-2 flex-column"

View File

@ -10,17 +10,30 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
import { Form, Modal, Typography } from 'antd'; import { InfoCircleOutlined, PlusOutlined } from '@ant-design/icons';
import { isUndefined, uniq } from 'lodash'; import {
Button,
Col,
Form,
Input,
Modal,
Row,
Tooltip,
Typography,
} from 'antd';
import { get, isUndefined, uniq } from 'lodash';
import React, { FC, useMemo, useState } from 'react'; import React, { FC, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { ReactComponent as DeleteIcon } from '../../../../assets/svg/ic-delete.svg';
import { import {
ENTITY_REFERENCE_OPTIONS, ENTITY_REFERENCE_OPTIONS,
ENUM_WITH_DESCRIPTION,
PROPERTY_TYPES_WITH_ENTITY_REFERENCE, PROPERTY_TYPES_WITH_ENTITY_REFERENCE,
} from '../../../../constants/CustomProperty.constants'; } from '../../../../constants/CustomProperty.constants';
import { import {
CustomProperty, CustomProperty,
EnumConfig, EnumConfig,
ValueClass,
} from '../../../../generated/type/customProperty'; } from '../../../../generated/type/customProperty';
import { import {
FieldProp, FieldProp,
@ -28,10 +41,11 @@ import {
FormItemLayout, FormItemLayout,
} from '../../../../interface/FormUtils.interface'; } from '../../../../interface/FormUtils.interface';
import { generateFormFields } from '../../../../utils/formUtils'; import { generateFormFields } from '../../../../utils/formUtils';
import RichTextEditor from '../../../common/RichTextEditor/RichTextEditor';
export interface FormData { export interface FormData {
description: string; description: string;
customPropertyConfig: string[]; customPropertyConfig: string[] | ValueClass[];
multiSelect?: boolean; multiSelect?: boolean;
} }
@ -58,15 +72,21 @@ const EditCustomPropertyModal: FC<EditCustomPropertyModalProps> = ({
setIsSaving(false); setIsSaving(false);
}; };
const { hasEnumConfig, hasEntityReferenceConfig } = useMemo(() => { const {
hasEnumConfig,
hasEntityReferenceConfig,
hasEnumWithDescriptionConfig,
} = useMemo(() => {
const propertyName = customProperty.propertyType.name ?? ''; const propertyName = customProperty.propertyType.name ?? '';
const hasEnumConfig = propertyName === 'enum'; const hasEnumConfig = propertyName === 'enum';
const hasEnumWithDescriptionConfig = propertyName === ENUM_WITH_DESCRIPTION;
const hasEntityReferenceConfig = const hasEntityReferenceConfig =
PROPERTY_TYPES_WITH_ENTITY_REFERENCE.includes(propertyName); PROPERTY_TYPES_WITH_ENTITY_REFERENCE.includes(propertyName);
return { return {
hasEnumConfig, hasEnumConfig,
hasEntityReferenceConfig, hasEntityReferenceConfig,
hasEnumWithDescriptionConfig,
}; };
}, [customProperty]); }, [customProperty]);
@ -155,7 +175,7 @@ const EditCustomPropertyModal: FC<EditCustomPropertyModalProps> = ({
}; };
const initialValues = useMemo(() => { const initialValues = useMemo(() => {
if (hasEnumConfig) { if (hasEnumConfig || hasEnumWithDescriptionConfig) {
const enumConfig = customProperty.customPropertyConfig const enumConfig = customProperty.customPropertyConfig
?.config as EnumConfig; ?.config as EnumConfig;
@ -170,7 +190,7 @@ const EditCustomPropertyModal: FC<EditCustomPropertyModalProps> = ({
description: customProperty.description, description: customProperty.description,
customPropertyConfig: customProperty.customPropertyConfig?.config, customPropertyConfig: customProperty.customPropertyConfig?.config,
}; };
}, [customProperty, hasEnumConfig]); }, [customProperty, hasEnumConfig, hasEnumWithDescriptionConfig]);
const note = ( const note = (
<Typography.Text <Typography.Text
@ -205,7 +225,7 @@ const EditCustomPropertyModal: FC<EditCustomPropertyModalProps> = ({
})} })}
</Typography.Text> </Typography.Text>
} }
width={750} width={800}
onCancel={onCancel}> onCancel={onCancel}>
<Form <Form
form={form} form={form}
@ -224,6 +244,125 @@ const EditCustomPropertyModal: FC<EditCustomPropertyModalProps> = ({
</> </>
)} )}
{hasEnumWithDescriptionConfig && (
<>
<Form.List name="customPropertyConfig">
{(fields, { add, remove }) => {
const config =
(initialValues?.customPropertyConfig as ValueClass[]) ??
[];
return (
<>
<Form.Item
className="form-item-horizontal"
colon={false}
label={
<div className="d-flex gap-2 items-center">
<span>{t('label.property')}</span>
<Tooltip
title={t(
'message.enum-with-description-update-note'
)}>
<InfoCircleOutlined
className="m-x-xss"
style={{ color: '#C4C4C4' }}
/>
</Tooltip>
</div>
}>
<Button
data-testid="add-enum-description-config"
icon={
<PlusOutlined
style={{ color: 'white', fontSize: '12px' }}
/>
}
size="small"
type="primary"
onClick={() => {
add();
}}
/>
</Form.Item>
{fields.map((field, index) => {
const isExisting = Boolean(get(config, index, false));
return (
<Row
className="m-t-md"
gutter={[8, 0]}
key={field.key}>
<Col span={23}>
<Row gutter={[8, 0]}>
<Col span={24}>
<Form.Item
name={[field.name, 'key']}
rules={[
{
required: true,
message: `${t(
'message.field-text-is-required',
{
fieldText: t('label.key'),
}
)}`,
},
]}>
<Input
disabled={isExisting}
id={`key-${index}`}
placeholder={t('label.key')}
/>
</Form.Item>
</Col>
<Col span={24}>
<Form.Item
name={[field.name, 'description']}
rules={[
{
required: true,
message: `${t(
'message.field-text-is-required',
{
fieldText: t('label.description'),
}
)}`,
},
]}
trigger="onTextChange"
valuePropName="initialValue">
<RichTextEditor height="200px" />
</Form.Item>
</Col>
</Row>
</Col>
{!isExisting && (
<Col span={1}>
<Button
data-testid={`remove-enum-description-config-${index}`}
icon={<DeleteIcon width={16} />}
size="small"
type="text"
onClick={() => {
remove(field.name);
}}
/>
</Col>
)}
</Row>
);
})}
</>
);
}}
</Form.List>
{generateFormFields([multiSelectField])}
</>
)}
{hasEntityReferenceConfig && ( {hasEntityReferenceConfig && (
<> <>
{generateFormFields([entityReferenceConfigField])} {generateFormFields([entityReferenceConfigField])}

View File

@ -153,17 +153,13 @@ describe('Test CustomProperty Table Component', () => {
<CustomPropertyTable {...mockProp} entityType={EntityType.TABLE} /> <CustomPropertyTable {...mockProp} entityType={EntityType.TABLE} />
); );
}); });
const table = await screen.findByTestId('custom-properties-table'); const table = await screen.findByTestId('custom-properties-card');
expect(table).toBeInTheDocument(); expect(table).toBeInTheDocument();
const propertyName = await screen.findByText('label.name'); const propertyValue = await screen.findByText('PropertyValue');
const propertyValue = await screen.findByText('label.value');
const rows = await screen.findAllByRole('row');
expect(propertyName).toBeInTheDocument();
expect(propertyValue).toBeInTheDocument(); expect(propertyValue).toBeInTheDocument();
expect(rows).toHaveLength(mockCustomProperties.length + 1);
}); });
it('Should render no data placeholder if custom properties list is empty', async () => { it('Should render no data placeholder if custom properties list is empty', async () => {
@ -222,10 +218,9 @@ describe('Test CustomProperty Table Component', () => {
<CustomPropertyTable {...mockProp} entityType={EntityType.TABLE} /> <CustomPropertyTable {...mockProp} entityType={EntityType.TABLE} />
); );
}); });
const tableRowTitle = await screen.findByText('xName');
const tableRowValue = await screen.findByText('PropertyValue'); const tableRowValue = await screen.findByText('PropertyValue');
expect(tableRowTitle).toBeInTheDocument();
expect(tableRowValue).toBeInTheDocument(); expect(tableRowValue).toBeInTheDocument();
}); });
}); });

View File

@ -11,11 +11,16 @@
* limitations under the License. * limitations under the License.
*/ */
import { Skeleton, Typography } from 'antd'; import { Col, Divider, Row, Skeleton, Typography } from 'antd';
import { ColumnsType } from 'antd/lib/table';
import { AxiosError } from 'axios'; import { AxiosError } from 'axios';
import { isEmpty, isUndefined } from 'lodash'; import { isEmpty, isUndefined } from 'lodash';
import React, { useCallback, useEffect, useMemo, useState } from 'react'; import React, {
Fragment,
useCallback,
useEffect,
useMemo,
useState,
} from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { CUSTOM_PROPERTIES_DOCS } from '../../../constants/docs.constants'; import { CUSTOM_PROPERTIES_DOCS } from '../../../constants/docs.constants';
@ -28,11 +33,9 @@ import {
import { ERROR_PLACEHOLDER_TYPE } from '../../../enums/common.enum'; import { ERROR_PLACEHOLDER_TYPE } from '../../../enums/common.enum';
import { EntityTabs, EntityType } from '../../../enums/entity.enum'; import { EntityTabs, EntityType } from '../../../enums/entity.enum';
import { ChangeDescription, Type } from '../../../generated/entity/type'; import { ChangeDescription, Type } from '../../../generated/entity/type';
import { CustomProperty } from '../../../generated/type/customProperty';
import { getTypeByFQN } from '../../../rest/metadataTypeAPI'; import { getTypeByFQN } from '../../../rest/metadataTypeAPI';
import { Transi18next } from '../../../utils/CommonUtils'; import { Transi18next } from '../../../utils/CommonUtils';
import entityUtilClassBase from '../../../utils/EntityUtilClassBase'; import entityUtilClassBase from '../../../utils/EntityUtilClassBase';
import { columnSorter, getEntityName } from '../../../utils/EntityUtils';
import { import {
getChangedEntityNewValue, getChangedEntityNewValue,
getDiffByFieldName, getDiffByFieldName,
@ -40,7 +43,6 @@ import {
} from '../../../utils/EntityVersionUtils'; } from '../../../utils/EntityVersionUtils';
import { showErrorToast } from '../../../utils/ToastUtils'; import { showErrorToast } from '../../../utils/ToastUtils';
import ErrorPlaceHolder from '../ErrorWithPlaceholder/ErrorPlaceHolder'; import ErrorPlaceHolder from '../ErrorWithPlaceholder/ErrorPlaceHolder';
import Table from '../Table/Table';
import { import {
CustomPropertyProps, CustomPropertyProps,
ExtentionEntities, ExtentionEntities,
@ -146,44 +148,6 @@ export const CustomPropertyTable = <T extends ExtentionEntitiesKeys>({
return { extensionObject: entityDetails?.extension }; return { extensionObject: entityDetails?.extension };
}, [isVersionView, entityDetails?.extension]); }, [isVersionView, entityDetails?.extension]);
const tableColumn: ColumnsType<CustomProperty> = useMemo(() => {
return [
{
title: t('label.name'),
dataIndex: 'name',
key: 'name',
ellipsis: true,
width: isRenderedInRightPanel ? 150 : 400,
render: (_, record) => getEntityName(record),
sorter: columnSorter,
},
{
title: t('label.value'),
dataIndex: 'value',
key: 'value',
render: (_, record) => (
<PropertyValue
extension={extensionObject.extensionObject}
hasEditPermissions={hasEditAccess}
isRenderedInRightPanel={isRenderedInRightPanel}
isVersionView={isVersionView}
property={record}
versionDataKeys={extensionObject.addedKeysList}
onExtensionUpdate={onExtensionUpdate}
/>
),
},
];
}, [
entityDetails,
entityDetails?.extension,
hasEditAccess,
extensionObject,
isVersionView,
onExtensionUpdate,
isRenderedInRightPanel,
]);
const viewAllBtn = useMemo(() => { const viewAllBtn = useMemo(() => {
const customProp = entityTypeDetail.customProperties ?? []; const customProp = entityTypeDetail.customProperties ?? [];
@ -212,6 +176,14 @@ export const CustomPropertyTable = <T extends ExtentionEntitiesKeys>({
maxDataCap, maxDataCap,
]); ]);
const dataSource = useMemo(() => {
const customProperties = entityTypeDetail?.customProperties ?? [];
return Array.isArray(customProperties)
? customProperties.slice(0, maxDataCap)
: [];
}, [maxDataCap, entityTypeDetail?.customProperties]);
useEffect(() => { useEffect(() => {
if (typePermission?.ViewAll || typePermission?.ViewBasic) { if (typePermission?.ViewAll || typePermission?.ViewBasic) {
fetchTypeDetail(); fetchTypeDetail();
@ -278,21 +250,44 @@ export const CustomPropertyTable = <T extends ExtentionEntitiesKeys>({
</Typography.Text> </Typography.Text>
{viewAllBtn} {viewAllBtn}
</div> </div>
<Table
bordered {isRenderedInRightPanel ? (
resizableColumns <>
columns={tableColumn} {dataSource.map((record, index) => (
data-testid="custom-properties-table" <Fragment key={record.name}>
dataSource={entityTypeDetail?.customProperties?.slice( <PropertyValue
0, extension={extensionObject.extensionObject}
maxDataCap hasEditPermissions={hasEditAccess}
)} isRenderedInRightPanel={isRenderedInRightPanel}
loading={entityTypeDetailLoading} isVersionView={isVersionView}
pagination={false} key={record.name}
rowKey="name" property={record}
scroll={isRenderedInRightPanel ? { x: true } : undefined} versionDataKeys={extensionObject.addedKeysList}
size="small" onExtensionUpdate={onExtensionUpdate}
/> />
{index !== dataSource.length - 1 && (
<Divider className="m-y-md" />
)}
</Fragment>
))}
</>
) : (
<Row data-testid="custom-properties-card" gutter={[16, 16]}>
{dataSource.map((record) => (
<Col key={record.name} span={8}>
<PropertyValue
extension={extensionObject.extensionObject}
hasEditPermissions={hasEditAccess}
isRenderedInRightPanel={isRenderedInRightPanel}
isVersionView={isVersionView}
property={record}
versionDataKeys={extensionObject.addedKeysList}
onExtensionUpdate={onExtensionUpdate}
/>
</Col>
))}
</Row>
)}
</> </>
)} )}
</> </>

View File

@ -116,10 +116,10 @@ describe('Test PropertyValue Component', () => {
/> />
); );
const valueElement = await screen.findByTestId('RichTextPreviewer'); const valueElement = await screen.findAllByTestId('RichTextPreviewer');
const iconElement = await screen.findByTestId('edit-icon'); const iconElement = await screen.findByTestId('edit-icon');
expect(valueElement).toBeInTheDocument(); expect(valueElement).toHaveLength(2);
expect(iconElement).toBeInTheDocument(); expect(iconElement).toBeInTheDocument();
await act(async () => { await act(async () => {
@ -146,7 +146,7 @@ describe('Test PropertyValue Component', () => {
const iconElement = await screen.findByTestId('edit-icon'); const iconElement = await screen.findByTestId('edit-icon');
expect(await screen.findByTestId('enum-value')).toHaveTextContent( expect(await screen.findByTestId('enum-value')).toHaveTextContent(
'enumValue1, enumValue2' 'enumValue1enumValue2'
); );
await act(async () => { await act(async () => {

View File

@ -11,17 +11,23 @@
* limitations under the License. * limitations under the License.
*/ */
import Icon from '@ant-design/icons'; import Icon, { DownOutlined, UpOutlined } from '@ant-design/icons';
import { import {
Button, Button,
Card,
Col,
DatePicker, DatePicker,
Divider,
Form, Form,
Input, Input,
Row,
Select, Select,
Tag,
TimePicker, TimePicker,
Tooltip, Tooltip,
Typography, Typography,
} from 'antd'; } from 'antd';
import { ColumnsType } from 'antd/lib/table';
import { AxiosError } from 'axios'; import { AxiosError } from 'axios';
import { t } from 'i18next'; import { t } from 'i18next';
import { import {
@ -35,7 +41,14 @@ import {
toUpper, toUpper,
} from 'lodash'; } from 'lodash';
import moment, { Moment } from 'moment'; import moment, { Moment } from 'moment';
import React, { CSSProperties, FC, Fragment, useState } from 'react'; import React, {
CSSProperties,
FC,
useEffect,
useMemo,
useRef,
useState,
} from 'react';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { ReactComponent as EditIconComponent } from '../../../assets/svg/edit-new.svg'; import { ReactComponent as EditIconComponent } from '../../../assets/svg/edit-new.svg';
import { import {
@ -43,11 +56,12 @@ import {
ICON_DIMENSION, ICON_DIMENSION,
VALIDATION_MESSAGES, VALIDATION_MESSAGES,
} from '../../../constants/constants'; } from '../../../constants/constants';
import { ENUM_WITH_DESCRIPTION } from '../../../constants/CustomProperty.constants';
import { TIMESTAMP_UNIX_IN_MILLISECONDS_REGEX } from '../../../constants/regex.constants'; import { TIMESTAMP_UNIX_IN_MILLISECONDS_REGEX } from '../../../constants/regex.constants';
import { CSMode } from '../../../enums/codemirror.enum'; import { CSMode } from '../../../enums/codemirror.enum';
import { SearchIndex } from '../../../enums/search.enum'; import { SearchIndex } from '../../../enums/search.enum';
import { EntityReference } from '../../../generated/entity/type'; import { EntityReference } from '../../../generated/entity/type';
import { EnumConfig } from '../../../generated/type/customProperty'; import { EnumConfig, ValueClass } from '../../../generated/type/customProperty';
import entityUtilClassBase from '../../../utils/EntityUtilClassBase'; import entityUtilClassBase from '../../../utils/EntityUtilClassBase';
import { getEntityName } from '../../../utils/EntityUtils'; import { getEntityName } from '../../../utils/EntityUtils';
import searchClassBase from '../../../utils/SearchClassBase'; import searchClassBase from '../../../utils/SearchClassBase';
@ -59,6 +73,7 @@ import { ModalWithMarkdownEditor } from '../../Modals/ModalWithMarkdownEditor/Mo
import InlineEdit from '../InlineEdit/InlineEdit.component'; import InlineEdit from '../InlineEdit/InlineEdit.component';
import ProfilePicture from '../ProfilePicture/ProfilePicture'; import ProfilePicture from '../ProfilePicture/ProfilePicture';
import RichTextEditorPreviewer from '../RichTextEditor/RichTextEditorPreviewer'; import RichTextEditorPreviewer from '../RichTextEditor/RichTextEditorPreviewer';
import Table from '../Table/Table';
import { import {
PropertyValueProps, PropertyValueProps,
PropertyValueType, PropertyValueType,
@ -76,28 +91,55 @@ export const PropertyValue: FC<PropertyValueProps> = ({
property, property,
isRenderedInRightPanel = false, isRenderedInRightPanel = false,
}) => { }) => {
const propertyName = property.name; const { propertyName, propertyType, value } = useMemo(() => {
const propertyType = property.propertyType; const propertyName = property.name;
const propertyType = property.propertyType;
const value = extension?.[propertyName]; const value = extension?.[propertyName];
return {
propertyName,
propertyType,
value,
};
}, [property, extension]);
const [showInput, setShowInput] = useState<boolean>(false); const [showInput, setShowInput] = useState<boolean>(false);
const [isLoading, setIsLoading] = useState<boolean>(false); const [isLoading, setIsLoading] = useState<boolean>(false);
const [isExpanded, setIsExpanded] = useState(false);
const [isOverflowing, setIsOverflowing] = useState(false);
const contentRef = useRef<HTMLDivElement>(null);
const onShowInput = () => setShowInput(true); const onShowInput = () => setShowInput(true);
const onHideInput = () => setShowInput(false); const onHideInput = () => setShowInput(false);
const findOptionReference = (
item: DataAssetOption | string,
options: DataAssetOption[]
) => {
if (typeof item === 'string') {
const option = options.find((option) => option.value === item);
return option?.reference;
}
return item?.reference;
};
const onInputSave = async (updatedValue: PropertyValueType) => { const onInputSave = async (updatedValue: PropertyValueType) => {
const isEnum = propertyType.name === 'enum'; const isEnum = propertyType.name === 'enum';
const isEnumWithDescription = propertyType.name === ENUM_WITH_DESCRIPTION;
const isArrayType = isArray(updatedValue); const isArrayType = isArray(updatedValue);
const enumValue = isArrayType ? updatedValue : [updatedValue]; const enumValue = isArrayType ? updatedValue : [updatedValue];
const propertyValue = isEnum const propertyValue =
? (enumValue as string[]).filter(Boolean) isEnum || isEnumWithDescription
: updatedValue; ? (enumValue as string[]).filter(Boolean)
: updatedValue;
try { try {
// Omit undefined and empty values // Omit undefined and empty values
@ -225,6 +267,60 @@ export const PropertyValue: FC<PropertyValueProps> = ({
); );
} }
case ENUM_WITH_DESCRIPTION: {
const enumConfig = property.customPropertyConfig?.config as EnumConfig;
const isMultiSelect = Boolean(enumConfig?.multiSelect);
const values = (enumConfig?.values as ValueClass[]) ?? [];
const options = values.map((option) => ({
label: (
<Tooltip title={option.description}>
<span>{option.key}</span>
</Tooltip>
),
value: option.key,
}));
const initialValues = {
enumWithDescriptionValues: (isArray(value) ? value : [value]).filter(
Boolean
),
};
return (
<InlineEdit
isLoading={isLoading}
saveButtonProps={{
disabled: isLoading,
htmlType: 'submit',
form: 'enum-with-description-form',
}}
onCancel={onHideInput}
onSave={noop}>
<Form
id="enum-with-description-form"
initialValues={initialValues}
layout="vertical"
onFinish={(values: {
enumWithDescriptionValues: string | string[];
}) => onInputSave(values.enumWithDescriptionValues)}>
<Form.Item name="enumWithDescriptionValues" style={commonStyle}>
<Select
allowClear
data-testid="enum-with-description-select"
disabled={isLoading}
mode={isMultiSelect ? 'multiple' : undefined}
options={options}
placeholder={t('label.enum-value-plural')}
/>
</Form.Item>
</Form>
</InlineEdit>
);
}
case 'date-cp': case 'date-cp':
case 'dateTime-cp': { case 'dateTime-cp': {
// Default format is 'yyyy-mm-dd' // Default format is 'yyyy-mm-dd'
@ -609,13 +705,22 @@ export const PropertyValue: FC<PropertyValueProps> = ({
onFinish={(values: { onFinish={(values: {
entityReference: DataAssetOption | DataAssetOption[]; entityReference: DataAssetOption | DataAssetOption[];
}) => { }) => {
if (isArray(values.entityReference)) { const { entityReference } = values;
onInputSave(
values.entityReference.map((item) => item.reference) if (Array.isArray(entityReference)) {
); const references = entityReference
} else { .map((item) => findOptionReference(item, initialOptions))
onInputSave(values?.entityReference?.reference); .filter(Boolean) as EntityReference[];
onInputSave(references);
return;
} }
const reference = findOptionReference(
entityReference,
initialOptions
);
onInputSave(reference as EntityReference);
}}> }}>
<Form.Item name="entityReference" style={commonStyle}> <Form.Item name="entityReference" style={commonStyle}>
<DataAssetAsyncSelectList <DataAssetAsyncSelectList
@ -656,11 +761,63 @@ export const PropertyValue: FC<PropertyValueProps> = ({
case 'enum': case 'enum':
return ( return (
<Typography.Text className="break-all" data-testid="enum-value"> <>
{isArray(value) ? value.join(', ') : value} {isArray(value) ? (
</Typography.Text> <div
className="w-full d-flex gap-2 flex-wrap"
data-testid="enum-value">
{value.map((val) => (
<Tooltip key={val} title={val} trigger="hover">
<Tag className="enum-key-tag">{val}</Tag>
</Tooltip>
))}
</div>
) : (
<Tooltip key={value} title={value} trigger="hover">
<Tag className="enum-key-tag" data-testid="enum-value">
{value}
</Tag>
</Tooltip>
)}
</>
); );
case ENUM_WITH_DESCRIPTION: {
const enumWithDescriptionValues = (value as ValueClass[]) ?? [];
const columns: ColumnsType<ValueClass> = [
{
title: 'Key',
dataIndex: 'key',
key: 'key',
render: (key: string) => <Typography>{key}</Typography>,
},
{
title: 'Description',
dataIndex: 'description',
key: 'description',
render: (description: string) => (
<RichTextEditorPreviewer markdown={description || ''} />
),
},
];
return (
<Table
bordered
resizableColumns
className="w-full"
columns={columns}
data-testid="enum-with-description-table"
dataSource={enumWithDescriptionValues}
pagination={false}
rowKey="name"
scroll={isRenderedInRightPanel ? { x: true } : undefined}
size="small"
/>
);
}
case 'sqlQuery': case 'sqlQuery':
return ( return (
<SchemaEditor <SchemaEditor
@ -690,7 +847,7 @@ export const PropertyValue: FC<PropertyValueProps> = ({
item.fullyQualifiedName as string item.fullyQualifiedName as string
)}> )}>
<Button <Button
className="entity-button flex-center p-0 m--ml-1" className="entity-button flex-center p-0"
icon={ icon={
<div className="entity-button-icon m-r-xs"> <div className="entity-button-icon m-r-xs">
{['user', 'team'].includes(item.type) ? ( {['user', 'team'].includes(item.type) ? (
@ -699,7 +856,7 @@ export const PropertyValue: FC<PropertyValueProps> = ({
isTeam={item.type === 'team'} isTeam={item.type === 'team'}
name={item.name ?? ''} name={item.name ?? ''}
type="circle" type="circle"
width="18" width="24"
/> />
) : ( ) : (
searchClassBase.getEntityIcon(item.type) searchClassBase.getEntityIcon(item.type)
@ -708,7 +865,7 @@ export const PropertyValue: FC<PropertyValueProps> = ({
} }
type="text"> type="text">
<Typography.Text <Typography.Text
className="text-left text-xs" className="text-left text-lg"
ellipsis={{ tooltip: true }}> ellipsis={{ tooltip: true }}>
{getEntityName(item)} {getEntityName(item)}
</Typography.Text> </Typography.Text>
@ -739,18 +896,18 @@ export const PropertyValue: FC<PropertyValueProps> = ({
item.fullyQualifiedName as string item.fullyQualifiedName as string
)}> )}>
<Button <Button
className="entity-button flex-center p-0 m--ml-1" className="entity-button flex-center p-0"
icon={ icon={
<div <div
className="entity-button-icon m-r-xs" className="entity-button-icon m-r-xs"
style={{ width: '18px', display: 'flex' }}> style={{ width: '24px', display: 'flex' }}>
{['user', 'team'].includes(item.type) ? ( {['user', 'team'].includes(item.type) ? (
<ProfilePicture <ProfilePicture
className="d-flex" className="d-flex"
isTeam={item.type === 'team'} isTeam={item.type === 'team'}
name={item.name ?? ''} name={item.name ?? ''}
type="circle" type="circle"
width="18" width="24"
/> />
) : ( ) : (
searchClassBase.getEntityIcon(item.type) searchClassBase.getEntityIcon(item.type)
@ -759,7 +916,7 @@ export const PropertyValue: FC<PropertyValueProps> = ({
} }
type="text"> type="text">
<Typography.Text <Typography.Text
className="text-left text-xs" className="text-left text-lg"
data-testid="entityReference-value-name" data-testid="entityReference-value-name"
ellipsis={{ tooltip: true }}> ellipsis={{ tooltip: true }}>
{getEntityName(item)} {getEntityName(item)}
@ -780,9 +937,17 @@ export const PropertyValue: FC<PropertyValueProps> = ({
<Typography.Text <Typography.Text
className="break-all" className="break-all"
data-testid="time-interval-value"> data-testid="time-interval-value">
{`StartTime: ${timeInterval.start}`} <span>
<br /> <Typography.Text className="text-xs">{`StartTime: `}</Typography.Text>
{`EndTime: ${timeInterval.end}`} <Typography.Text className="text-sm font-medium text-grey-body">
{timeInterval.start}
</Typography.Text>
<Divider className="self-center" type="vertical" />
<Typography.Text className="text-xs">{`EndTime: `}</Typography.Text>
<Typography.Text className="text-sm font-medium text-grey-body">
{timeInterval.end}
</Typography.Text>
</span>
</Typography.Text> </Typography.Text>
); );
} }
@ -798,7 +963,9 @@ export const PropertyValue: FC<PropertyValueProps> = ({
case 'duration': case 'duration':
default: default:
return ( return (
<Typography.Text className="break-all" data-testid="value"> <Typography.Text
className="break-all text-xl font-semibold text-grey-body"
data-testid="value">
{value} {value}
</Typography.Text> </Typography.Text>
); );
@ -817,15 +984,34 @@ export const PropertyValue: FC<PropertyValueProps> = ({
); );
}; };
return ( const toggleExpand = () => {
<div> setIsExpanded(!isExpanded);
{showInput ? ( };
getPropertyInput()
) : ( useEffect(() => {
<Fragment> if (!contentRef.current || !property) {
<div className="d-flex gap-2 items-center"> return;
{getValueElement()} }
{hasEditPermissions && (
const isMarkdownWithValue = propertyType.name === 'markdown' && value;
const isOverflowing =
(contentRef.current.scrollHeight > 30 || isMarkdownWithValue) &&
propertyType.name !== 'entityReference';
setIsOverflowing(isOverflowing);
}, [property, extension, contentRef, value]);
const customPropertyElement = (
<Row gutter={[0, 16]}>
<Col span={24}>
<Row gutter={[0, 2]}>
<Col className="d-flex justify-between w-full" span={24}>
<Typography.Text
className="text-md text-grey-body"
data-testid="property-name">
{getEntityName(property)}
</Typography.Text>
{hasEditPermissions && !showInput && (
<Tooltip <Tooltip
placement="left" placement="left"
title={t('label.edit-entity', { entity: propertyName })}> title={t('label.edit-entity', { entity: propertyName })}>
@ -839,9 +1025,57 @@ export const PropertyValue: FC<PropertyValueProps> = ({
/> />
</Tooltip> </Tooltip>
)} )}
</div> </Col>
</Fragment> <Col span={24}>
)} <RichTextEditorPreviewer
</div> className="text-grey-muted"
markdown={property.description || ''}
/>
</Col>
</Row>
</Col>
<Col span={24}>
<Row gutter={[6, 0]}>
<Col
ref={contentRef}
span={22}
style={{
height: isExpanded || showInput ? 'auto' : '30px',
overflow: isExpanded ? 'visible' : 'hidden',
}}>
{showInput ? getPropertyInput() : getValueElement()}
</Col>
{isOverflowing && !showInput && (
<Col span={2}>
<Button
className="custom-property-value-toggle-btn"
data-testid={`toggle-${propertyName}`}
size="small"
type="text"
onClick={toggleExpand}>
{isExpanded ? <UpOutlined /> : <DownOutlined />}
</Button>
</Col>
)}
</Row>
</Col>
</Row>
);
if (isRenderedInRightPanel) {
return (
<div data-testid="custom-property-right-panel-card">
{customPropertyElement}
</div>
);
}
return (
<Card
className="w-full"
data-testid={`custom-property-${propertyName}-card`}>
{customPropertyElement}
</Card>
); );
}; };

View File

@ -10,6 +10,10 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
@btn-bg-color: #00000005;
@enum-tag-bg-color: #00000008;
.entity-reference-list-item:last-child { .entity-reference-list-item:last-child {
border-bottom: none; border-bottom: none;
} }
@ -21,8 +25,27 @@
align-items: center; align-items: center;
justify-content: center; justify-content: center;
svg { svg {
height: 18px; height: 24px;
width: 18px; width: 24px;
} }
} }
} }
.ant-btn-text.custom-property-value-toggle-btn {
border-radius: 100%;
background: @btn-bg-color;
border-color: transparent;
height: 30px;
width: 30px;
.anticon {
vertical-align: middle;
}
}
.enum-key-tag {
width: max-content;
margin: 0px;
border: none;
padding: 4px;
background: @enum-tag-bg-color;
}

View File

@ -126,3 +126,18 @@ export const SUPPORTED_FORMAT_MAP = {
'dateTime-cp': SUPPORTED_DATE_TIME_FORMATS, 'dateTime-cp': SUPPORTED_DATE_TIME_FORMATS,
'time-cp': SUPPORTED_TIME_FORMATS, 'time-cp': SUPPORTED_TIME_FORMATS,
}; };
export const ENUM_WITH_DESCRIPTION = 'enumWithDescriptions';
export const INLINE_PROPERTY_TYPES = [
'date-cp',
'dateTime-cp',
'duration',
'email',
'entityReference',
'integer',
'number',
'string',
'time-cp',
'timestamp',
];

View File

@ -622,6 +622,7 @@
"june": "Juni", "june": "Juni",
"jwt-token-expiry-time": "JWT token expiry time", "jwt-token-expiry-time": "JWT token expiry time",
"jwt-uppercase": "JWT", "jwt-uppercase": "JWT",
"key": "Key",
"keyword-lowercase-plural": "Schlüsselwörter", "keyword-lowercase-plural": "Schlüsselwörter",
"kill": "Beenden", "kill": "Beenden",
"kpi-display-name": "KPI", "kpi-display-name": "KPI",
@ -1507,6 +1508,7 @@
"entity-size-in-between": "{{entity}} Größe muss zwischen {{min}} und {{max}} liegen.", "entity-size-in-between": "{{entity}} Größe muss zwischen {{min}} und {{max}} liegen.",
"entity-size-must-be-between-2-and-64": "{{entity}} Größe muss zwischen 2 und 64 liegen.", "entity-size-must-be-between-2-and-64": "{{entity}} Größe muss zwischen 2 und 64 liegen.",
"entity-transfer-message": "Klicken Sie auf Bestätigen, wenn Sie {{entity}} von <0>{{from}}</0> unter <0>{{to}}</0> {{entity}} verschieben möchten.", "entity-transfer-message": "Klicken Sie auf Bestätigen, wenn Sie {{entity}} von <0>{{from}}</0> unter <0>{{to}}</0> {{entity}} verschieben möchten.",
"enum-with-description-update-note": "Updating existing value keys is not allowed; only the description can be edited. However, adding new values is allowed.",
"error-self-signup-disabled": "Self-signup is currently disabled. To proceed, please reach out to your administrator for further assistance or to request access.", "error-self-signup-disabled": "Self-signup is currently disabled. To proceed, please reach out to your administrator for further assistance or to request access.",
"error-team-transfer-message": "You cannot move to this team as Team Type {{dragTeam}} can't be {{dropTeam}} children", "error-team-transfer-message": "You cannot move to this team as Team Type {{dragTeam}} can't be {{dropTeam}} children",
"error-while-fetching-access-token": "Fehler beim Abrufen des Zugriffstokens.", "error-while-fetching-access-token": "Fehler beim Abrufen des Zugriffstokens.",

View File

@ -622,6 +622,7 @@
"june": "June", "june": "June",
"jwt-token-expiry-time": "JWT token expiry time", "jwt-token-expiry-time": "JWT token expiry time",
"jwt-uppercase": "JWT", "jwt-uppercase": "JWT",
"key": "Key",
"keyword-lowercase-plural": "keywords", "keyword-lowercase-plural": "keywords",
"kill": "Kill", "kill": "Kill",
"kpi-display-name": "KPI Display Name", "kpi-display-name": "KPI Display Name",
@ -1507,6 +1508,7 @@
"entity-size-in-between": "{{entity}} size must be between {{min}} and {{max}}", "entity-size-in-between": "{{entity}} size must be between {{min}} and {{max}}",
"entity-size-must-be-between-2-and-64": "{{entity}} size must be between 2 and 64", "entity-size-must-be-between-2-and-64": "{{entity}} size must be between 2 and 64",
"entity-transfer-message": "Click on Confirm if youd like to move <0>{{from}}</0> {{entity}} under <0>{{to}}</0> {{entity}}.", "entity-transfer-message": "Click on Confirm if youd like to move <0>{{from}}</0> {{entity}} under <0>{{to}}</0> {{entity}}.",
"enum-with-description-update-note": "Updating existing value keys is not allowed; only the description can be edited. However, adding new values is allowed.",
"error-self-signup-disabled": "Self-signup is currently disabled. To proceed, please reach out to your administrator for further assistance or to request access.", "error-self-signup-disabled": "Self-signup is currently disabled. To proceed, please reach out to your administrator for further assistance or to request access.",
"error-team-transfer-message": "You cannot move to this team as Team Type {{dragTeam}} can't be {{dropTeam}} children", "error-team-transfer-message": "You cannot move to this team as Team Type {{dragTeam}} can't be {{dropTeam}} children",
"error-while-fetching-access-token": "Error while fetching access token.", "error-while-fetching-access-token": "Error while fetching access token.",
@ -1849,7 +1851,7 @@
"update-displayName-entity": "Update Display Name for the {{entity}}.", "update-displayName-entity": "Update Display Name for the {{entity}}.",
"update-profiler-settings": "Update profiler setting.", "update-profiler-settings": "Update profiler setting.",
"update-tag-message": "Request to update tags for", "update-tag-message": "Request to update tags for",
"updating-existing-not-possible-can-add-new-values": "Updating existing values is not possible,only the addition of new values is allowed.", "updating-existing-not-possible-can-add-new-values": "Updating existing values is not possible, only the addition of new values is allowed.",
"upload-file": "Upload File", "upload-file": "Upload File",
"upstream-depth-message": "Please select a value for upstream depth", "upstream-depth-message": "Please select a value for upstream depth",
"upstream-depth-tooltip": "Display up to 3 nodes of upstream lineage to identify the source (parent levels).", "upstream-depth-tooltip": "Display up to 3 nodes of upstream lineage to identify the source (parent levels).",

View File

@ -622,6 +622,7 @@
"june": "Junio", "june": "Junio",
"jwt-token-expiry-time": "Tiempo de caducidad del token JWT", "jwt-token-expiry-time": "Tiempo de caducidad del token JWT",
"jwt-uppercase": "JWT", "jwt-uppercase": "JWT",
"key": "Key",
"keyword-lowercase-plural": "palabras clave", "keyword-lowercase-plural": "palabras clave",
"kill": "Eliminar", "kill": "Eliminar",
"kpi-display-name": "Nombre para mostrar del KPI", "kpi-display-name": "Nombre para mostrar del KPI",
@ -1507,6 +1508,7 @@
"entity-size-in-between": "El tamaño de {{entity}} debe estar entre {{min}} y {{max}}", "entity-size-in-between": "El tamaño de {{entity}} debe estar entre {{min}} y {{max}}",
"entity-size-must-be-between-2-and-64": "El tamaño de {{entity}} debe estar entre 2 y 64", "entity-size-must-be-between-2-and-64": "El tamaño de {{entity}} debe estar entre 2 y 64",
"entity-transfer-message": "Haga clic en Confirmar si desea mover <0>{{from}}</0> {{entity}} debajo de <0>{{to}}</0> {{entity}}.", "entity-transfer-message": "Haga clic en Confirmar si desea mover <0>{{from}}</0> {{entity}} debajo de <0>{{to}}</0> {{entity}}.",
"enum-with-description-update-note": "Updating existing value keys is not allowed; only the description can be edited. However, adding new values is allowed.",
"error-self-signup-disabled": "Self-signup is currently disabled. To proceed, please reach out to your administrator for further assistance or to request access.", "error-self-signup-disabled": "Self-signup is currently disabled. To proceed, please reach out to your administrator for further assistance or to request access.",
"error-team-transfer-message": "No puede mover a este equipo ya que el Tipo de equipo {{dragTeam}} no puede ser hijo de {{dropTeam}}", "error-team-transfer-message": "No puede mover a este equipo ya que el Tipo de equipo {{dragTeam}} no puede ser hijo de {{dropTeam}}",
"error-while-fetching-access-token": "Error al obtener el token de acceso.", "error-while-fetching-access-token": "Error al obtener el token de acceso.",

View File

@ -622,6 +622,7 @@
"june": "Juin", "june": "Juin",
"jwt-token-expiry-time": "délai d'expiration du jeton JWT", "jwt-token-expiry-time": "délai d'expiration du jeton JWT",
"jwt-uppercase": "JWT", "jwt-uppercase": "JWT",
"key": "Key",
"keyword-lowercase-plural": "mots-clés", "keyword-lowercase-plural": "mots-clés",
"kill": "Arrêter", "kill": "Arrêter",
"kpi-display-name": "KPI", "kpi-display-name": "KPI",
@ -1507,6 +1508,7 @@
"entity-size-in-between": "{{entity}} taille doit être de {{min}} et {{max}}", "entity-size-in-between": "{{entity}} taille doit être de {{min}} et {{max}}",
"entity-size-must-be-between-2-and-64": "{{entity}} taille doit être comprise entre 2 et 64", "entity-size-must-be-between-2-and-64": "{{entity}} taille doit être comprise entre 2 et 64",
"entity-transfer-message": "Cliquer sur Confirmer si vous souhaitez déplacer <0>{{from}}</0> {{entity}} sous <0>{{to}}</0> {{entity}}.", "entity-transfer-message": "Cliquer sur Confirmer si vous souhaitez déplacer <0>{{from}}</0> {{entity}} sous <0>{{to}}</0> {{entity}}.",
"enum-with-description-update-note": "Updating existing value keys is not allowed; only the description can be edited. However, adding new values is allowed.",
"error-self-signup-disabled": "Self-signup is currently disabled. To proceed, please reach out to your administrator for further assistance or to request access.", "error-self-signup-disabled": "Self-signup is currently disabled. To proceed, please reach out to your administrator for further assistance or to request access.",
"error-team-transfer-message": "Vous ne pouvez pas rejoindre cette équipe car le type d'équipe {{dragTeam}} ne peut être enfant de {{dropTeam}}", "error-team-transfer-message": "Vous ne pouvez pas rejoindre cette équipe car le type d'équipe {{dragTeam}} ne peut être enfant de {{dropTeam}}",
"error-while-fetching-access-token": "Erreur pendant la récupération du jeton d'accès.", "error-while-fetching-access-token": "Erreur pendant la récupération du jeton d'accès.",

View File

@ -622,6 +622,7 @@
"june": "יוני", "june": "יוני",
"jwt-token-expiry-time": "זמן פג תוקף של אסימון JWT", "jwt-token-expiry-time": "זמן פג תוקף של אסימון JWT",
"jwt-uppercase": "JWT", "jwt-uppercase": "JWT",
"key": "Key",
"keyword-lowercase-plural": "מילות מפתח", "keyword-lowercase-plural": "מילות מפתח",
"kill": "הרוג", "kill": "הרוג",
"kpi-display-name": "שם תצוגת KPI", "kpi-display-name": "שם תצוגת KPI",
@ -1507,6 +1508,7 @@
"entity-size-in-between": "{{entity}} יכול להיות בגודל בין {{min}} ל-{{max}}", "entity-size-in-between": "{{entity}} יכול להיות בגודל בין {{min}} ל-{{max}}",
"entity-size-must-be-between-2-and-64": "{{entity}} יכול להיות בגודל בין 2 ל-64", "entity-size-must-be-between-2-and-64": "{{entity}} יכול להיות בגודל בין 2 ל-64",
"entity-transfer-message": "לחץ על אישור אם ברצונך להעביר <0>{{from}}</0> {{entity}} מתחת ל-<0>{{to}}</0> {{entity}}.", "entity-transfer-message": "לחץ על אישור אם ברצונך להעביר <0>{{from}}</0> {{entity}} מתחת ל-<0>{{to}}</0> {{entity}}.",
"enum-with-description-update-note": "Updating existing value keys is not allowed; only the description can be edited. However, adding new values is allowed.",
"error-self-signup-disabled": "Self-signup is currently disabled. To proceed, please reach out to your administrator for further assistance or to request access.", "error-self-signup-disabled": "Self-signup is currently disabled. To proceed, please reach out to your administrator for further assistance or to request access.",
"error-team-transfer-message": "You cannot move to this team as Team Type {{dragTeam}} can't be {{dropTeam}} children", "error-team-transfer-message": "You cannot move to this team as Team Type {{dragTeam}} can't be {{dropTeam}} children",
"error-while-fetching-access-token": "שגיאה בעת קבלת טוקן גישה.", "error-while-fetching-access-token": "שגיאה בעת קבלת טוקן גישה.",

View File

@ -622,6 +622,7 @@
"june": "6月", "june": "6月",
"jwt-token-expiry-time": "JWT token expiry time", "jwt-token-expiry-time": "JWT token expiry time",
"jwt-uppercase": "JWT", "jwt-uppercase": "JWT",
"key": "Key",
"keyword-lowercase-plural": "キーワード", "keyword-lowercase-plural": "キーワード",
"kill": "終了", "kill": "終了",
"kpi-display-name": "KPI表示名", "kpi-display-name": "KPI表示名",
@ -1507,6 +1508,7 @@
"entity-size-in-between": "{{entity}}のサイズは{{min}}以上{{max}}以下にしてください", "entity-size-in-between": "{{entity}}のサイズは{{min}}以上{{max}}以下にしてください",
"entity-size-must-be-between-2-and-64": "{{entity}}のサイズは2以上64以下", "entity-size-must-be-between-2-and-64": "{{entity}}のサイズは2以上64以下",
"entity-transfer-message": "Click on Confirm if youd like to move <0>{{from}}</0> {{entity}} under <0>{{to}}</0> {{entity}}.", "entity-transfer-message": "Click on Confirm if youd like to move <0>{{from}}</0> {{entity}} under <0>{{to}}</0> {{entity}}.",
"enum-with-description-update-note": "Updating existing value keys is not allowed; only the description can be edited. However, adding new values is allowed.",
"error-self-signup-disabled": "Self-signup is currently disabled. To proceed, please reach out to your administrator for further assistance or to request access.", "error-self-signup-disabled": "Self-signup is currently disabled. To proceed, please reach out to your administrator for further assistance or to request access.",
"error-team-transfer-message": "You cannot move to this team as Team Type {{dragTeam}} can't be {{dropTeam}} children", "error-team-transfer-message": "You cannot move to this team as Team Type {{dragTeam}} can't be {{dropTeam}} children",
"error-while-fetching-access-token": "アクセストークンの取得中にエラーが発生しました。", "error-while-fetching-access-token": "アクセストークンの取得中にエラーが発生しました。",

View File

@ -622,6 +622,7 @@
"june": "juni", "june": "juni",
"jwt-token-expiry-time": "Vervaltijd van JWT-token", "jwt-token-expiry-time": "Vervaltijd van JWT-token",
"jwt-uppercase": "JWT", "jwt-uppercase": "JWT",
"key": "Key",
"keyword-lowercase-plural": "trefwoorden", "keyword-lowercase-plural": "trefwoorden",
"kill": "Stoppen", "kill": "Stoppen",
"kpi-display-name": "Weergavenaam van KPI", "kpi-display-name": "Weergavenaam van KPI",
@ -1507,6 +1508,7 @@
"entity-size-in-between": "{{entity}} grootte moet tussen {{min}} en {{max}} liggen", "entity-size-in-between": "{{entity}} grootte moet tussen {{min}} en {{max}} liggen",
"entity-size-must-be-between-2-and-64": "{{entity}} grootte moet tussen 2 en 64 liggen", "entity-size-must-be-between-2-and-64": "{{entity}} grootte moet tussen 2 en 64 liggen",
"entity-transfer-message": "Klik op Bevestigen als je <0>{{from}}</0> {{entity}} wilt verplaatsen naar <0>{{to}}</0> {{entity}}.", "entity-transfer-message": "Klik op Bevestigen als je <0>{{from}}</0> {{entity}} wilt verplaatsen naar <0>{{to}}</0> {{entity}}.",
"enum-with-description-update-note": "Updating existing value keys is not allowed; only the description can be edited. However, adding new values is allowed.",
"error-self-signup-disabled": "Self-signup is currently disabled. To proceed, please reach out to your administrator for further assistance or to request access.", "error-self-signup-disabled": "Self-signup is currently disabled. To proceed, please reach out to your administrator for further assistance or to request access.",
"error-team-transfer-message": "You cannot move to this team as Team Type {{dragTeam}} can't be {{dropTeam}} children", "error-team-transfer-message": "You cannot move to this team as Team Type {{dragTeam}} can't be {{dropTeam}} children",
"error-while-fetching-access-token": "Fout bij het ophalen van toegangstoken.", "error-while-fetching-access-token": "Fout bij het ophalen van toegangstoken.",

View File

@ -622,6 +622,7 @@
"june": "ژوئن", "june": "ژوئن",
"jwt-token-expiry-time": "زمان انقضای توکن JWT", "jwt-token-expiry-time": "زمان انقضای توکن JWT",
"jwt-uppercase": "JWT", "jwt-uppercase": "JWT",
"key": "Key",
"keyword-lowercase-plural": "کلمات کلیدی", "keyword-lowercase-plural": "کلمات کلیدی",
"kill": "کشتن", "kill": "کشتن",
"kpi-display-name": "نام نمایشی KPI", "kpi-display-name": "نام نمایشی KPI",
@ -1507,6 +1508,7 @@
"entity-size-in-between": "اندازه {{entity}} باید بین {{min}} و {{max}} باشد.", "entity-size-in-between": "اندازه {{entity}} باید بین {{min}} و {{max}} باشد.",
"entity-size-must-be-between-2-and-64": "اندازه {{entity}} باید بین 2 تا 64 کاراکتر باشد.", "entity-size-must-be-between-2-and-64": "اندازه {{entity}} باید بین 2 تا 64 کاراکتر باشد.",
"entity-transfer-message": "برای انتقال <0>{{from}}</0> {{entity}} به <0>{{to}}</0> {{entity}}، روی تأیید کلیک کنید.", "entity-transfer-message": "برای انتقال <0>{{from}}</0> {{entity}} به <0>{{to}}</0> {{entity}}، روی تأیید کلیک کنید.",
"enum-with-description-update-note": "Updating existing value keys is not allowed; only the description can be edited. However, adding new values is allowed.",
"error-self-signup-disabled": "ثبت‌نام خودکار غیرفعال است. برای ادامه، لطفاً با مدیر سیستم تماس بگیرید یا درخواست دسترسی کنید.", "error-self-signup-disabled": "ثبت‌نام خودکار غیرفعال است. برای ادامه، لطفاً با مدیر سیستم تماس بگیرید یا درخواست دسترسی کنید.",
"error-team-transfer-message": "شما نمی‌توانید به این تیم منتقل شوید، زیرا نوع تیم {{dragTeam}} نمی‌تواند زیرمجموعه {{dropTeam}} باشد.", "error-team-transfer-message": "شما نمی‌توانید به این تیم منتقل شوید، زیرا نوع تیم {{dragTeam}} نمی‌تواند زیرمجموعه {{dropTeam}} باشد.",
"error-while-fetching-access-token": "خطا در دریافت توکن دسترسی.", "error-while-fetching-access-token": "خطا در دریافت توکن دسترسی.",

View File

@ -622,6 +622,7 @@
"june": "Junho", "june": "Junho",
"jwt-token-expiry-time": "Tempo de expiração do token JWT", "jwt-token-expiry-time": "Tempo de expiração do token JWT",
"jwt-uppercase": "JWT", "jwt-uppercase": "JWT",
"key": "Key",
"keyword-lowercase-plural": "palavras-chave", "keyword-lowercase-plural": "palavras-chave",
"kill": "Finalizar", "kill": "Finalizar",
"kpi-display-name": "Nome de Exibição do KPI", "kpi-display-name": "Nome de Exibição do KPI",
@ -1507,6 +1508,7 @@
"entity-size-in-between": "O tamanho de {{entity}} deve ser entre {{min}} e {{max}}", "entity-size-in-between": "O tamanho de {{entity}} deve ser entre {{min}} e {{max}}",
"entity-size-must-be-between-2-and-64": "O tamanho de {{entity}} deve ser entre 2 e 64", "entity-size-must-be-between-2-and-64": "O tamanho de {{entity}} deve ser entre 2 e 64",
"entity-transfer-message": "Clique em Confirmar se deseja mover <0>{{from}}</0> {{entity}} para <0>{{to}}</0> {{entity}}.", "entity-transfer-message": "Clique em Confirmar se deseja mover <0>{{from}}</0> {{entity}} para <0>{{to}}</0> {{entity}}.",
"enum-with-description-update-note": "Updating existing value keys is not allowed; only the description can be edited. However, adding new values is allowed.",
"error-self-signup-disabled": "Self-signup is currently disabled. To proceed, please reach out to your administrator for further assistance or to request access.", "error-self-signup-disabled": "Self-signup is currently disabled. To proceed, please reach out to your administrator for further assistance or to request access.",
"error-team-transfer-message": "You cannot move to this team as Team Type {{dragTeam}} can't be {{dropTeam}} children", "error-team-transfer-message": "You cannot move to this team as Team Type {{dragTeam}} can't be {{dropTeam}} children",
"error-while-fetching-access-token": "Erro ao buscar token de acesso.", "error-while-fetching-access-token": "Erro ao buscar token de acesso.",

View File

@ -622,6 +622,7 @@
"june": "Июнь", "june": "Июнь",
"jwt-token-expiry-time": "JWT token expiry time", "jwt-token-expiry-time": "JWT token expiry time",
"jwt-uppercase": "JWT", "jwt-uppercase": "JWT",
"key": "Key",
"keyword-lowercase-plural": "ключевые слова", "keyword-lowercase-plural": "ключевые слова",
"kill": "Уничтожить", "kill": "Уничтожить",
"kpi-display-name": "Отображаемое имя KPI", "kpi-display-name": "Отображаемое имя KPI",
@ -1507,6 +1508,7 @@
"entity-size-in-between": "Размер {{entity}} должен быть между {{min}} и {{max}}", "entity-size-in-between": "Размер {{entity}} должен быть между {{min}} и {{max}}",
"entity-size-must-be-between-2-and-64": "Размер {{entity}} должен быть от 2 до 64", "entity-size-must-be-between-2-and-64": "Размер {{entity}} должен быть от 2 до 64",
"entity-transfer-message": "Нажмите «Подтвердить», если вы хотите переместить <0>{{from}}</0> {{entity}} в <0>{{to}}</0> {{entity}}.", "entity-transfer-message": "Нажмите «Подтвердить», если вы хотите переместить <0>{{from}}</0> {{entity}} в <0>{{to}}</0> {{entity}}.",
"enum-with-description-update-note": "Updating existing value keys is not allowed; only the description can be edited. However, adding new values is allowed.",
"error-self-signup-disabled": "Self-signup is currently disabled. To proceed, please reach out to your administrator for further assistance or to request access.", "error-self-signup-disabled": "Self-signup is currently disabled. To proceed, please reach out to your administrator for further assistance or to request access.",
"error-team-transfer-message": "You cannot move to this team as Team Type {{dragTeam}} can't be {{dropTeam}} children", "error-team-transfer-message": "You cannot move to this team as Team Type {{dragTeam}} can't be {{dropTeam}} children",
"error-while-fetching-access-token": "Ошибка при получении токена доступа.", "error-while-fetching-access-token": "Ошибка при получении токена доступа.",

View File

@ -622,6 +622,7 @@
"june": "六月", "june": "六月",
"jwt-token-expiry-time": "JWT 令牌到期时间", "jwt-token-expiry-time": "JWT 令牌到期时间",
"jwt-uppercase": "JWT", "jwt-uppercase": "JWT",
"key": "Key",
"keyword-lowercase-plural": "关键词", "keyword-lowercase-plural": "关键词",
"kill": "终止", "kill": "终止",
"kpi-display-name": "KPI 显示名称", "kpi-display-name": "KPI 显示名称",
@ -1507,6 +1508,7 @@
"entity-size-in-between": "{{entity}}大小须介于{{min}}和{{max}}之间", "entity-size-in-between": "{{entity}}大小须介于{{min}}和{{max}}之间",
"entity-size-must-be-between-2-and-64": "{{entity}}大小必须介于2和64之间", "entity-size-must-be-between-2-and-64": "{{entity}}大小必须介于2和64之间",
"entity-transfer-message": "如果您想将<0>{{from}}</0> {{entity}} 移动到<0>{{to}}</0> {{entity}}中, 请单击确认", "entity-transfer-message": "如果您想将<0>{{from}}</0> {{entity}} 移动到<0>{{to}}</0> {{entity}}中, 请单击确认",
"enum-with-description-update-note": "Updating existing value keys is not allowed; only the description can be edited. However, adding new values is allowed.",
"error-self-signup-disabled": "Self-signup is currently disabled. To proceed, please reach out to your administrator for further assistance or to request access.", "error-self-signup-disabled": "Self-signup is currently disabled. To proceed, please reach out to your administrator for further assistance or to request access.",
"error-team-transfer-message": "由于团队类型{{dragTeam}}不能是{{dropTeam}}的子团队, 因此您无法移动到此团队", "error-team-transfer-message": "由于团队类型{{dragTeam}}不能是{{dropTeam}}的子团队, 因此您无法移动到此团队",
"error-while-fetching-access-token": "获取访问令牌时出现错误", "error-while-fetching-access-token": "获取访问令牌时出现错误",

View File

@ -535,7 +535,7 @@ const APICollectionPage: FunctionComponent = () => {
); );
const handleExtensionUpdate = async (apiCollectionData: APICollection) => { const handleExtensionUpdate = async (apiCollectionData: APICollection) => {
await saveUpdatedAPICollectionData({ const response = await saveUpdatedAPICollectionData({
...apiCollection, ...apiCollection,
extension: apiCollectionData.extension, extension: apiCollectionData.extension,
}); });
@ -546,7 +546,7 @@ const APICollectionPage: FunctionComponent = () => {
return { return {
...prev, ...prev,
extension: apiCollectionData.extension, extension: response.extension,
}; };
}); });
}; };

View File

@ -555,7 +555,7 @@ const DatabaseSchemaPage: FunctionComponent = () => {
); );
const handleExtensionUpdate = async (schema: DatabaseSchema) => { const handleExtensionUpdate = async (schema: DatabaseSchema) => {
await saveUpdatedDatabaseSchemaData({ const response = await saveUpdatedDatabaseSchemaData({
...databaseSchema, ...databaseSchema,
extension: schema.extension, extension: schema.extension,
}); });
@ -566,7 +566,7 @@ const DatabaseSchemaPage: FunctionComponent = () => {
return { return {
...prev, ...prev,
extension: schema.extension, extension: response.extension,
}; };
}); });
}; };

View File

@ -477,21 +477,22 @@ const StoredProcedurePage = () => {
const onExtensionUpdate = useCallback( const onExtensionUpdate = useCallback(
async (updatedData: StoredProcedure) => { async (updatedData: StoredProcedure) => {
storedProcedure && if (storedProcedure) {
(await saveUpdatedStoredProceduresData({ const response = await saveUpdatedStoredProceduresData({
...storedProcedure, ...storedProcedure,
extension: updatedData.extension, extension: updatedData.extension,
})); });
setStoredProcedure((prev) => { setStoredProcedure((prev) => {
if (!prev) { if (!prev) {
return prev; return prev;
} }
return { return {
...prev, ...prev,
extension: updatedData.extension, extension: response.extension,
}; };
}); });
}
}, },
[saveUpdatedStoredProceduresData, storedProcedure] [saveUpdatedStoredProceduresData, storedProcedure]
); );