mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2025-08-23 16:38:17 +00:00
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> (cherry picked from commit 1b029d2bf2793c6a4f016c91a4634c490462d7f4)
This commit is contained in:
parent
075c67b8b3
commit
e320a1b516
@ -118,7 +118,8 @@ public class TypeRegistry {
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
throw EntityNotFoundException.byMessage(
|
||||
CatalogExceptionMessage.entityNotFound(Entity.TYPE, String.valueOf(type)));
|
||||
}
|
||||
|
||||
public static String getCustomPropertyConfig(String entityType, String propertyName) {
|
||||
@ -128,7 +129,13 @@ public class TypeRegistry {
|
||||
if (property.getName().equals(propertyName)
|
||||
&& property.getCustomPropertyConfig() != 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -67,6 +67,7 @@ import static org.openmetadata.service.util.EntityUtil.objectMatch;
|
||||
import static org.openmetadata.service.util.EntityUtil.tagLabelMatch;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.node.ArrayNode;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import com.google.common.cache.CacheBuilder;
|
||||
@ -100,6 +101,7 @@ import java.util.concurrent.TimeUnit;
|
||||
import java.util.function.BiPredicate;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.StreamSupport;
|
||||
import javax.json.JsonPatch;
|
||||
import javax.validation.constraints.NotNull;
|
||||
import javax.ws.rs.core.Response.Status;
|
||||
@ -145,6 +147,7 @@ import org.openmetadata.schema.type.api.BulkAssets;
|
||||
import org.openmetadata.schema.type.api.BulkOperationResult;
|
||||
import org.openmetadata.schema.type.api.BulkResponse;
|
||||
import org.openmetadata.schema.type.csv.CsvImportResult;
|
||||
import org.openmetadata.schema.type.customproperties.EnumWithDescriptionsConfig;
|
||||
import org.openmetadata.schema.utils.EntityInterfaceUtil;
|
||||
import org.openmetadata.service.Entity;
|
||||
import org.openmetadata.service.OpenMetadataApplicationConfig;
|
||||
@ -1454,34 +1457,20 @@ public abstract class EntityRepository<T extends EntityInterface> {
|
||||
}
|
||||
String customPropertyType = TypeRegistry.getCustomPropertyType(entityType, fieldName);
|
||||
String propertyConfig = TypeRegistry.getCustomPropertyConfig(entityType, fieldName);
|
||||
DateTimeFormatter formatter = null;
|
||||
try {
|
||||
if ("date-cp".equals(customPropertyType)) {
|
||||
DateTimeFormatter inputFormatter =
|
||||
DateTimeFormatter.ofPattern(Objects.requireNonNull(propertyConfig), Locale.ENGLISH);
|
||||
|
||||
// Parse the input string into a TemporalAccessor
|
||||
TemporalAccessor date = inputFormatter.parse(fieldValue.textValue());
|
||||
|
||||
// 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));
|
||||
}
|
||||
validateAndUpdateExtensionBasedOnPropertyType(
|
||||
entity,
|
||||
(ObjectNode) jsonNode,
|
||||
fieldName,
|
||||
fieldValue,
|
||||
customPropertyType,
|
||||
propertyConfig);
|
||||
} catch (DateTimeParseException e) {
|
||||
throw new IllegalArgumentException(
|
||||
CatalogExceptionMessage.dateTimeValidationError(
|
||||
fieldName, TypeRegistry.getCustomPropertyConfig(entityType, fieldName)));
|
||||
CatalogExceptionMessage.dateTimeValidationError(fieldName, propertyConfig));
|
||||
}
|
||||
Set<ValidationMessage> validationMessages = jsonSchema.validate(fieldValue);
|
||||
|
||||
Set<ValidationMessage> validationMessages = jsonSchema.validate(entry.getValue());
|
||||
if (!validationMessages.isEmpty()) {
|
||||
throw new IllegalArgumentException(
|
||||
CatalogExceptionMessage.jsonValidationError(fieldName, validationMessages.toString()));
|
||||
@ -1489,6 +1478,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) {
|
||||
JsonNode jsonNode = JsonUtils.valueToTree(entity.getExtension());
|
||||
Iterator<Entry<String, JsonNode>> customFields = jsonNode.fields();
|
||||
|
@ -28,6 +28,7 @@ import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
import javax.ws.rs.core.UriInfo;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
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.Relationship;
|
||||
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.TypeRegistry;
|
||||
import org.openmetadata.service.resources.types.TypeResource;
|
||||
@ -169,24 +172,9 @@ public class TypeRepository extends EntityRepository<Type> {
|
||||
|
||||
private void validateProperty(CustomProperty customProperty) {
|
||||
switch (customProperty.getPropertyType().getName()) {
|
||||
case "enum" -> {
|
||||
CustomPropertyConfig config = customProperty.getCustomPropertyConfig();
|
||||
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.");
|
||||
}
|
||||
}
|
||||
case "enum" -> validateEnumConfig(customProperty.getCustomPropertyConfig());
|
||||
case "enumWithDescriptions" -> validateEnumWithDescriptionsConfig(
|
||||
customProperty.getCustomPropertyConfig());
|
||||
case "date-cp" -> validateDateFormat(
|
||||
customProperty.getCustomPropertyConfig(), getDateTokens(), "Invalid date format");
|
||||
case "dateTime-cp" -> validateDateFormat(
|
||||
@ -229,6 +217,44 @@ public class TypeRepository extends EntityRepository<Type> {
|
||||
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. */
|
||||
public class TypeUpdater extends EntityUpdater {
|
||||
public TypeUpdater(Type original, Type updated, Operation operation) {
|
||||
@ -387,6 +413,27 @@ public class TypeRepository extends EntityRepository<Type> {
|
||||
throw new IllegalArgumentException(
|
||||
"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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -387,6 +387,8 @@ public abstract class EntityResourceTest<T extends EntityInterface, K extends Cr
|
||||
|
||||
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
|
||||
// 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.
|
||||
|
@ -45,6 +45,8 @@ import org.openmetadata.schema.type.ChangeDescription;
|
||||
import org.openmetadata.schema.type.CustomPropertyConfig;
|
||||
import org.openmetadata.schema.type.EntityReference;
|
||||
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.resources.EntityResourceTest;
|
||||
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);
|
||||
STRING_TYPE = getEntityByName("string", "", ADMIN_AUTH_HEADERS);
|
||||
ENUM_TYPE = getEntityByName("enum", "", ADMIN_AUTH_HEADERS);
|
||||
ENUM_WITH_DESCRIPTIONS_TYPE = getEntityByName("enumWithDescriptions", "", ADMIN_AUTH_HEADERS);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -280,6 +283,177 @@ public class TypeResourceTest extends EntityResourceTest<Type, CreateType> {
|
||||
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
|
||||
void put_customPropertyToPropertyType_4xx() {
|
||||
// Adding a custom property to a property type is not allowed (only entity type is allowed)
|
||||
|
@ -105,6 +105,22 @@
|
||||
"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": {
|
||||
"description": "Timezone of the user in the format `America/Los_Angeles`, `Brazil/East`, etc.",
|
||||
"type": "string",
|
||||
|
@ -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
|
||||
}
|
@ -33,6 +33,9 @@
|
||||
},
|
||||
{
|
||||
"$ref": "#/definitions/entityTypes"
|
||||
},
|
||||
{
|
||||
"$ref": "../type/customProperties/enumWithDescriptionsConfig.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -47,6 +47,19 @@ export const CUSTOM_PROPERTIES_ENTITIES = {
|
||||
values: ['enum1', 'enum2', 'enum3'],
|
||||
multiSelect: false,
|
||||
},
|
||||
enumWithDescriptionConfig: {
|
||||
values: [
|
||||
{
|
||||
key: 'enumWithDescription1',
|
||||
description: 'This is enumWithDescription1',
|
||||
},
|
||||
{
|
||||
key: 'enumWithDescription2',
|
||||
description: 'This is enumWithDescription2',
|
||||
},
|
||||
],
|
||||
multiSelect: false,
|
||||
},
|
||||
dateFormatConfig: 'yyyy-MM-dd',
|
||||
dateTimeFormatConfig: 'yyyy-MM-dd HH:mm:ss',
|
||||
timeFormatConfig: 'HH:mm:ss',
|
||||
@ -65,6 +78,19 @@ export const CUSTOM_PROPERTIES_ENTITIES = {
|
||||
values: ['enum1', 'enum2', 'enum3'],
|
||||
multiSelect: false,
|
||||
},
|
||||
enumWithDescriptionConfig: {
|
||||
values: [
|
||||
{
|
||||
key: 'enumWithDescription1',
|
||||
description: 'This is enumWithDescription1',
|
||||
},
|
||||
{
|
||||
key: 'enumWithDescription2',
|
||||
description: 'This is enumWithDescription2',
|
||||
},
|
||||
],
|
||||
multiSelect: false,
|
||||
},
|
||||
dateFormatConfig: 'yyyy-MM-dd',
|
||||
dateTimeFormatConfig: 'yyyy-MM-dd HH:mm:ss',
|
||||
timeFormatConfig: 'HH:mm:ss',
|
||||
@ -83,6 +109,19 @@ export const CUSTOM_PROPERTIES_ENTITIES = {
|
||||
values: ['enum1', 'enum2', 'enum3'],
|
||||
multiSelect: false,
|
||||
},
|
||||
enumWithDescriptionConfig: {
|
||||
values: [
|
||||
{
|
||||
key: 'enumWithDescription1',
|
||||
description: 'This is enumWithDescription1',
|
||||
},
|
||||
{
|
||||
key: 'enumWithDescription2',
|
||||
description: 'This is enumWithDescription2',
|
||||
},
|
||||
],
|
||||
multiSelect: false,
|
||||
},
|
||||
dateFormatConfig: 'yyyy-MM-dd',
|
||||
dateTimeFormatConfig: 'yyyy-MM-dd HH:mm:ss',
|
||||
timeFormatConfig: 'HH:mm:ss',
|
||||
@ -101,6 +140,19 @@ export const CUSTOM_PROPERTIES_ENTITIES = {
|
||||
values: ['enum1', 'enum2', 'enum3'],
|
||||
multiSelect: false,
|
||||
},
|
||||
enumWithDescriptionConfig: {
|
||||
values: [
|
||||
{
|
||||
key: 'enumWithDescription1',
|
||||
description: 'This is enumWithDescription1',
|
||||
},
|
||||
{
|
||||
key: 'enumWithDescription2',
|
||||
description: 'This is enumWithDescription2',
|
||||
},
|
||||
],
|
||||
multiSelect: false,
|
||||
},
|
||||
dateFormatConfig: 'yyyy-MM-dd',
|
||||
dateTimeFormatConfig: 'yyyy-MM-dd HH:mm:ss',
|
||||
timeFormatConfig: 'HH:mm:ss',
|
||||
@ -119,6 +171,19 @@ export const CUSTOM_PROPERTIES_ENTITIES = {
|
||||
values: ['enum1', 'enum2', 'enum3'],
|
||||
multiSelect: false,
|
||||
},
|
||||
enumWithDescriptionConfig: {
|
||||
values: [
|
||||
{
|
||||
key: 'enumWithDescription1',
|
||||
description: 'This is enumWithDescription1',
|
||||
},
|
||||
{
|
||||
key: 'enumWithDescription2',
|
||||
description: 'This is enumWithDescription2',
|
||||
},
|
||||
],
|
||||
multiSelect: false,
|
||||
},
|
||||
dateFormatConfig: 'yyyy-MM-dd',
|
||||
dateTimeFormatConfig: 'yyyy-MM-dd HH:mm:ss',
|
||||
timeFormatConfig: 'HH:mm:ss',
|
||||
@ -137,6 +202,19 @@ export const CUSTOM_PROPERTIES_ENTITIES = {
|
||||
values: ['enum1', 'enum2', 'enum3'],
|
||||
multiSelect: false,
|
||||
},
|
||||
enumWithDescriptionConfig: {
|
||||
values: [
|
||||
{
|
||||
key: 'enumWithDescription1',
|
||||
description: 'This is enumWithDescription1',
|
||||
},
|
||||
{
|
||||
key: 'enumWithDescription2',
|
||||
description: 'This is enumWithDescription2',
|
||||
},
|
||||
],
|
||||
multiSelect: false,
|
||||
},
|
||||
dateFormatConfig: 'yyyy-MM-dd',
|
||||
dateTimeFormatConfig: 'yyyy-MM-dd HH:mm:ss',
|
||||
timeFormatConfig: 'HH:mm:ss',
|
||||
@ -155,6 +233,19 @@ export const CUSTOM_PROPERTIES_ENTITIES = {
|
||||
values: ['enum1', 'enum2', 'enum3'],
|
||||
multiSelect: true,
|
||||
},
|
||||
enumWithDescriptionConfig: {
|
||||
values: [
|
||||
{
|
||||
key: 'enumWithDescription1',
|
||||
description: 'This is enumWithDescription1',
|
||||
},
|
||||
{
|
||||
key: 'enumWithDescription2',
|
||||
description: 'This is enumWithDescription2',
|
||||
},
|
||||
],
|
||||
multiSelect: false,
|
||||
},
|
||||
dateFormatConfig: 'yyyy-MM-dd',
|
||||
dateTimeFormatConfig: 'yyyy-MM-dd HH:mm:ss',
|
||||
timeFormatConfig: 'HH:mm:ss',
|
||||
@ -173,6 +264,19 @@ export const CUSTOM_PROPERTIES_ENTITIES = {
|
||||
values: ['enum1', 'enum2', 'enum3'],
|
||||
multiSelect: false,
|
||||
},
|
||||
enumWithDescriptionConfig: {
|
||||
values: [
|
||||
{
|
||||
key: 'enumWithDescription1',
|
||||
description: 'This is enumWithDescription1',
|
||||
},
|
||||
{
|
||||
key: 'enumWithDescription2',
|
||||
description: 'This is enumWithDescription2',
|
||||
},
|
||||
],
|
||||
multiSelect: false,
|
||||
},
|
||||
dateFormatConfig: 'yyyy-MM-dd',
|
||||
dateTimeFormatConfig: 'yyyy-MM-dd HH:mm:ss',
|
||||
timeFormatConfig: 'HH:mm:ss',
|
||||
@ -191,6 +295,19 @@ export const CUSTOM_PROPERTIES_ENTITIES = {
|
||||
values: ['enum1', 'enum2', 'enum3'],
|
||||
multiSelect: false,
|
||||
},
|
||||
enumWithDescriptionConfig: {
|
||||
values: [
|
||||
{
|
||||
key: 'enumWithDescription1',
|
||||
description: 'This is enumWithDescription1',
|
||||
},
|
||||
{
|
||||
key: 'enumWithDescription2',
|
||||
description: 'This is enumWithDescription2',
|
||||
},
|
||||
],
|
||||
multiSelect: false,
|
||||
},
|
||||
dateFormatConfig: 'yyyy-MM-dd',
|
||||
dateTimeFormatConfig: 'yyyy-MM-dd HH:mm:ss',
|
||||
timeFormatConfig: 'HH:mm:ss',
|
||||
@ -209,6 +326,19 @@ export const CUSTOM_PROPERTIES_ENTITIES = {
|
||||
values: ['enum1', 'enum2', 'enum3'],
|
||||
multiSelect: false,
|
||||
},
|
||||
enumWithDescriptionConfig: {
|
||||
values: [
|
||||
{
|
||||
key: 'enumWithDescription1',
|
||||
description: 'This is enumWithDescription1',
|
||||
},
|
||||
{
|
||||
key: 'enumWithDescription2',
|
||||
description: 'This is enumWithDescription2',
|
||||
},
|
||||
],
|
||||
multiSelect: false,
|
||||
},
|
||||
dateFormatConfig: 'yyyy-MM-dd',
|
||||
dateTimeFormatConfig: 'yyyy-MM-dd HH:mm:ss',
|
||||
timeFormatConfig: 'HH:mm:ss',
|
||||
@ -227,6 +357,19 @@ export const CUSTOM_PROPERTIES_ENTITIES = {
|
||||
values: ['enum1', 'enum2', 'enum3'],
|
||||
multiSelect: false,
|
||||
},
|
||||
enumWithDescriptionConfig: {
|
||||
values: [
|
||||
{
|
||||
key: 'enumWithDescription1',
|
||||
description: 'This is enumWithDescription1',
|
||||
},
|
||||
{
|
||||
key: 'enumWithDescription2',
|
||||
description: 'This is enumWithDescription2',
|
||||
},
|
||||
],
|
||||
multiSelect: false,
|
||||
},
|
||||
dateFormatConfig: 'yyyy-MM-dd',
|
||||
dateTimeFormatConfig: 'yyyy-MM-dd HH:mm:ss',
|
||||
timeFormatConfig: 'HH:mm:ss',
|
||||
@ -244,6 +387,19 @@ export const CUSTOM_PROPERTIES_ENTITIES = {
|
||||
values: ['enum1', 'enum2', 'enum3'],
|
||||
multiSelect: false,
|
||||
},
|
||||
enumWithDescriptionConfig: {
|
||||
values: [
|
||||
{
|
||||
key: 'enumWithDescription1',
|
||||
description: 'This is enumWithDescription1',
|
||||
},
|
||||
{
|
||||
key: 'enumWithDescription2',
|
||||
description: 'This is enumWithDescription2',
|
||||
},
|
||||
],
|
||||
multiSelect: false,
|
||||
},
|
||||
dateFormatConfig: 'yyyy-MM-dd',
|
||||
dateTimeFormatConfig: 'yyyy-MM-dd HH:mm:ss',
|
||||
timeFormatConfig: 'HH:mm:ss',
|
||||
@ -262,6 +418,19 @@ export const CUSTOM_PROPERTIES_ENTITIES = {
|
||||
values: ['enum1', 'enum2', 'enum3'],
|
||||
multiSelect: false,
|
||||
},
|
||||
enumWithDescriptionConfig: {
|
||||
values: [
|
||||
{
|
||||
key: 'enumWithDescription1',
|
||||
description: 'This is enumWithDescription1',
|
||||
},
|
||||
{
|
||||
key: 'enumWithDescription2',
|
||||
description: 'This is enumWithDescription2',
|
||||
},
|
||||
],
|
||||
multiSelect: false,
|
||||
},
|
||||
dateFormatConfig: 'yyyy-MM-dd',
|
||||
dateTimeFormatConfig: 'yyyy-MM-dd HH:mm:ss',
|
||||
timeFormatConfig: 'HH:mm:ss',
|
||||
@ -279,6 +448,19 @@ export const CUSTOM_PROPERTIES_ENTITIES = {
|
||||
values: ['enum1', 'enum2', 'enum3'],
|
||||
multiSelect: false,
|
||||
},
|
||||
enumWithDescriptionConfig: {
|
||||
values: [
|
||||
{
|
||||
key: 'enumWithDescription1',
|
||||
description: 'This is enumWithDescription1',
|
||||
},
|
||||
{
|
||||
key: 'enumWithDescription2',
|
||||
description: 'This is enumWithDescription2',
|
||||
},
|
||||
],
|
||||
multiSelect: false,
|
||||
},
|
||||
dateFormatConfig: 'yyyy-MM-dd',
|
||||
dateTimeFormatConfig: 'yyyy-MM-dd HH:mm:ss',
|
||||
timeFormatConfig: 'HH:mm:ss',
|
||||
@ -296,6 +478,19 @@ export const CUSTOM_PROPERTIES_ENTITIES = {
|
||||
values: ['enum1', 'enum2', 'enum3'],
|
||||
multiSelect: false,
|
||||
},
|
||||
enumWithDescriptionConfig: {
|
||||
values: [
|
||||
{
|
||||
key: 'enumWithDescription1',
|
||||
description: 'This is enumWithDescription1',
|
||||
},
|
||||
{
|
||||
key: 'enumWithDescription2',
|
||||
description: 'This is enumWithDescription2',
|
||||
},
|
||||
],
|
||||
multiSelect: false,
|
||||
},
|
||||
dateFormatConfig: 'yyyy-MM-dd',
|
||||
dateTimeFormatConfig: 'yyyy-MM-dd HH:mm:ss',
|
||||
timeFormatConfig: 'HH:mm:ss',
|
||||
|
@ -60,12 +60,24 @@ test('CustomProperty Dashboard Filter', async ({ page }) => {
|
||||
await test.step('Add Custom Property in Dashboard', async () => {
|
||||
await dashboardEntity.visitEntityPage(page);
|
||||
|
||||
const container = page.locator(
|
||||
`[data-testid="custom-property-${propertyName}-card"]`
|
||||
);
|
||||
|
||||
await page.getByTestId('custom_properties').click();
|
||||
|
||||
await page
|
||||
.getByRole('row', { name: `${propertyName} No data` })
|
||||
.locator('svg')
|
||||
.click();
|
||||
await expect(
|
||||
page.locator(
|
||||
`[data-testid="custom-property-${propertyName}-card"] [data-testid="property-name"]`
|
||||
)
|
||||
).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);
|
||||
|
||||
@ -75,9 +87,7 @@ test('CustomProperty Dashboard Filter', async ({ page }) => {
|
||||
|
||||
await saveResponse;
|
||||
|
||||
expect(
|
||||
page.getByLabel('Custom Properties').getByTestId('value')
|
||||
).toContainText(propertyValue);
|
||||
await expect(container.getByTestId('value')).toContainText(propertyValue);
|
||||
});
|
||||
|
||||
await test.step(
|
||||
|
@ -45,13 +45,46 @@ test.describe('Custom properties with custom property config', () => {
|
||||
enumConfig: entity.enumConfig,
|
||||
});
|
||||
|
||||
await editCreatedProperty(page, propertyName);
|
||||
await editCreatedProperty(page, propertyName, 'Enum');
|
||||
|
||||
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(
|
||||
'Add update and delete Entity Reference custom properties',
|
||||
() => {
|
||||
@ -73,7 +106,7 @@ test.describe('Custom properties with custom property config', () => {
|
||||
entityReferenceConfig: entity.entityReferenceConfig,
|
||||
});
|
||||
|
||||
await editCreatedProperty(page, propertyName);
|
||||
await editCreatedProperty(page, propertyName, 'Entity Reference');
|
||||
|
||||
await deleteCreatedProperty(page, propertyName);
|
||||
});
|
||||
@ -102,7 +135,11 @@ test.describe('Custom properties with custom property config', () => {
|
||||
entityReferenceConfig: entity.entityReferenceConfig,
|
||||
});
|
||||
|
||||
await editCreatedProperty(page, propertyName);
|
||||
await editCreatedProperty(
|
||||
page,
|
||||
propertyName,
|
||||
'Entity Reference List'
|
||||
);
|
||||
|
||||
await deleteCreatedProperty(page, propertyName);
|
||||
});
|
||||
|
@ -44,6 +44,7 @@ export enum CustomPropertyTypeByName {
|
||||
TIME_CP = 'time-cp',
|
||||
DATE_CP = 'date-cp',
|
||||
DATE_TIME_CP = 'dateTime-cp',
|
||||
ENUM_WITH_DESCRIPTION = 'enumWithDescriptions',
|
||||
}
|
||||
|
||||
export interface CustomProperty {
|
||||
@ -66,12 +67,18 @@ export const setValueForProperty = async (data: {
|
||||
const { page, propertyName, value, propertyType, endpoint } = data;
|
||||
await page.click('[data-testid="custom_properties"]');
|
||||
|
||||
await expect(page.getByRole('cell', { name: propertyName })).toContainText(
|
||||
propertyName
|
||||
const container = page.locator(
|
||||
`[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(
|
||||
`[data-row-key="${propertyName}"] [data-testid="edit-icon"]`
|
||||
`[data-testid="custom-property-${propertyName}-card"] [data-testid="edit-icon"]`
|
||||
);
|
||||
await editButton.scrollIntoViewIfNeeded();
|
||||
await editButton.click({ force: true });
|
||||
@ -96,14 +103,14 @@ export const setValueForProperty = async (data: {
|
||||
case 'email':
|
||||
await page.locator('[data-testid="email-input"]').isVisible();
|
||||
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;
|
||||
|
||||
case 'duration':
|
||||
await page.locator('[data-testid="duration-input"]').isVisible();
|
||||
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;
|
||||
|
||||
@ -112,21 +119,30 @@ export const setValueForProperty = async (data: {
|
||||
await page.fill('#enumValues', value);
|
||||
await page.press('#enumValues', 'Enter');
|
||||
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;
|
||||
|
||||
case 'sqlQuery':
|
||||
await page.locator("pre[role='presentation']").last().click();
|
||||
await page.keyboard.type(value);
|
||||
await page.locator('[data-testid="inline-save-btn"]').click();
|
||||
await container.locator('[data-testid="inline-save-btn"]').click();
|
||||
|
||||
break;
|
||||
|
||||
case 'timestamp':
|
||||
await page.locator('[data-testid="timestamp-input"]').isVisible();
|
||||
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;
|
||||
|
||||
@ -136,7 +152,7 @@ export const setValueForProperty = async (data: {
|
||||
await page.locator('[data-testid="start-input"]').fill(startValue);
|
||||
await page.locator('[data-testid="end-input"]').isVisible();
|
||||
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;
|
||||
}
|
||||
@ -146,7 +162,7 @@ export const setValueForProperty = async (data: {
|
||||
await page.locator('[data-testid="time-picker"]').click();
|
||||
await page.locator('[data-testid="time-picker"]').fill(value);
|
||||
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;
|
||||
}
|
||||
@ -161,7 +177,7 @@ export const setValueForProperty = async (data: {
|
||||
} else {
|
||||
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;
|
||||
}
|
||||
@ -171,7 +187,7 @@ export const setValueForProperty = async (data: {
|
||||
case 'number':
|
||||
await page.locator('[data-testid="value-input"]').isVisible();
|
||||
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;
|
||||
|
||||
@ -191,7 +207,9 @@ export const setValueForProperty = async (data: {
|
||||
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;
|
||||
}
|
||||
@ -208,23 +226,43 @@ export const validateValueForProperty = async (data: {
|
||||
const { page, propertyName, value, propertyType } = data;
|
||||
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') {
|
||||
await expect(
|
||||
page.getByLabel('Custom Properties').getByTestId('enum-value')
|
||||
).toContainText(value);
|
||||
await expect(container.getByTestId('enum-value')).toContainText(value);
|
||||
} else if (propertyType === 'timeInterval') {
|
||||
const [startValue, endValue] = value.split(',');
|
||||
|
||||
await expect(
|
||||
page.getByLabel('Custom Properties').getByTestId('time-interval-value')
|
||||
).toContainText(startValue);
|
||||
await expect(
|
||||
page.getByLabel('Custom Properties').getByTestId('time-interval-value')
|
||||
).toContainText(endValue);
|
||||
await expect(container.getByTestId('time-interval-value')).toContainText(
|
||||
startValue
|
||||
);
|
||||
await expect(container.getByTestId('time-interval-value')).toContainText(
|
||||
endValue
|
||||
);
|
||||
} else if (propertyType === 'sqlQuery') {
|
||||
await expect(container.locator('.CodeMirror-scroll')).toContainText(value);
|
||||
} else if (propertyType === 'enumWithDescriptions') {
|
||||
await expect(
|
||||
page.getByLabel('Custom Properties').locator('.CodeMirror-scroll')
|
||||
).toContainText(value);
|
||||
container.locator('[data-testid="enum-with-description-table"]')
|
||||
).toBeVisible();
|
||||
|
||||
await expect(
|
||||
container
|
||||
.locator('[data-testid="enum-with-description-table"]')
|
||||
.getByText(value, {
|
||||
exact: true,
|
||||
})
|
||||
).toBeVisible();
|
||||
} else if (
|
||||
![
|
||||
'entityReference',
|
||||
@ -233,9 +271,7 @@ export const validateValueForProperty = async (data: {
|
||||
'dateTime-cp',
|
||||
].includes(propertyType)
|
||||
) {
|
||||
await expect(page.getByRole('row', { name: propertyName })).toContainText(
|
||||
value.replace(/\*|_/gi, '')
|
||||
);
|
||||
await expect(container).toContainText(value.replace(/\*|_/gi, ''));
|
||||
}
|
||||
};
|
||||
|
||||
@ -280,6 +316,11 @@ export const getPropertyValues = (
|
||||
value: 'small',
|
||||
newValue: 'medium',
|
||||
};
|
||||
case 'enumWithDescriptions':
|
||||
return {
|
||||
value: 'enumWithDescription1',
|
||||
newValue: 'enumWithDescription2',
|
||||
};
|
||||
case 'sqlQuery':
|
||||
return {
|
||||
value: 'Select * from table',
|
||||
@ -403,6 +444,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)
|
||||
? {
|
||||
customPropertyConfig: {
|
||||
@ -465,12 +525,17 @@ export const addCustomPropertiesForEntity = async ({
|
||||
enumConfig,
|
||||
formatConfig,
|
||||
entityReferenceConfig,
|
||||
enumWithDescriptionConfig,
|
||||
}: {
|
||||
page: Page;
|
||||
propertyName: string;
|
||||
customPropertyData: { description: string };
|
||||
customType: string;
|
||||
enumConfig?: { values: string[]; multiSelect: boolean };
|
||||
enumWithDescriptionConfig?: {
|
||||
values: { key: string; description: string }[];
|
||||
multiSelect: boolean;
|
||||
};
|
||||
formatConfig?: string;
|
||||
entityReferenceConfig?: string[];
|
||||
}) => {
|
||||
@ -550,6 +615,27 @@ export const addCustomPropertiesForEntity = async ({
|
||||
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
|
||||
if (
|
||||
@ -577,7 +663,9 @@ export const addCustomPropertiesForEntity = async ({
|
||||
}
|
||||
|
||||
// Description
|
||||
await page.fill(descriptionBox, customPropertyData.description);
|
||||
if (customType !== 'Enum With Descriptions') {
|
||||
await page.fill(descriptionBox, customPropertyData.description);
|
||||
}
|
||||
|
||||
const createPropertyPromise = page.waitForResponse(
|
||||
'/api/v1/metadata/types/name/*?fields=customProperties'
|
||||
@ -612,10 +700,22 @@ export const editCreatedProperty = async (
|
||||
).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 page.locator(descriptionBox).fill('');
|
||||
await page.locator(descriptionBox).fill('This is new description');
|
||||
if (type !== 'Enum With Descriptions') {
|
||||
await page.locator(descriptionBox).fill('');
|
||||
await page.locator(descriptionBox).fill('This is new description');
|
||||
}
|
||||
|
||||
if (type === 'Enum') {
|
||||
await page.click('#root\\/customPropertyConfig');
|
||||
@ -623,6 +723,10 @@ export const editCreatedProperty = async (
|
||||
await page.press('#root\\/customPropertyConfig', 'Enter');
|
||||
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 ?? '')) {
|
||||
await page.click('#root\\/customPropertyConfig');
|
||||
@ -655,6 +759,17 @@ export const editCreatedProperty = async (
|
||||
)
|
||||
).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 ?? '')) {
|
||||
await expect(
|
||||
page.locator(
|
||||
|
@ -11,7 +11,8 @@
|
||||
* 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 { t } from 'i18next';
|
||||
import { isUndefined, map, omit, omitBy, startCase } from 'lodash';
|
||||
@ -23,8 +24,10 @@ import React, {
|
||||
useState,
|
||||
} from 'react';
|
||||
import { useHistory, useParams } from 'react-router-dom';
|
||||
import { ReactComponent as DeleteIcon } from '../../../../assets/svg/ic-delete.svg';
|
||||
import {
|
||||
ENTITY_REFERENCE_OPTIONS,
|
||||
ENUM_WITH_DESCRIPTION,
|
||||
PROPERTY_TYPES_WITH_ENTITY_REFERENCE,
|
||||
PROPERTY_TYPES_WITH_FORMAT,
|
||||
SUPPORTED_FORMAT_MAP,
|
||||
@ -38,6 +41,7 @@ import {
|
||||
import { EntityType } from '../../../../enums/entity.enum';
|
||||
import { ServiceCategory } from '../../../../enums/service.enum';
|
||||
import { Category, Type } from '../../../../generated/entity/type';
|
||||
import { EnumWithDescriptionsConfig } from '../../../../generated/type/customProperties/enumWithDescriptionsConfig';
|
||||
import { CustomProperty } from '../../../../generated/type/customProperty';
|
||||
import {
|
||||
FieldProp,
|
||||
@ -54,6 +58,7 @@ import { getSettingOptionByEntityType } from '../../../../utils/GlobalSettingsUt
|
||||
import { getSettingPath } from '../../../../utils/RouterUtils';
|
||||
import { showErrorToast } from '../../../../utils/ToastUtils';
|
||||
import ResizablePanels from '../../../common/ResizablePanels/ResizablePanels';
|
||||
import RichTextEditor from '../../../common/RichTextEditor/RichTextEditor';
|
||||
import ServiceDocPanel from '../../../common/ServiceDocPanel/ServiceDocPanel';
|
||||
import TitleBreadcrumb from '../../../common/TitleBreadcrumb/TitleBreadcrumb.component';
|
||||
|
||||
@ -107,6 +112,7 @@ const AddCustomProperty = () => {
|
||||
hasFormatConfig,
|
||||
hasEntityReferenceConfig,
|
||||
watchedOption,
|
||||
hasEnumWithDescriptionConfig,
|
||||
} = useMemo(() => {
|
||||
const watchedOption = propertyTypeOptions.find(
|
||||
(option) => option.value === watchedPropertyType
|
||||
@ -115,6 +121,9 @@ const AddCustomProperty = () => {
|
||||
|
||||
const hasEnumConfig = watchedOptionKey === 'enum';
|
||||
|
||||
const hasEnumWithDescriptionConfig =
|
||||
watchedOptionKey === ENUM_WITH_DESCRIPTION;
|
||||
|
||||
const hasFormatConfig =
|
||||
PROPERTY_TYPES_WITH_FORMAT.includes(watchedOptionKey);
|
||||
|
||||
@ -126,6 +135,7 @@ const AddCustomProperty = () => {
|
||||
hasFormatConfig,
|
||||
hasEntityReferenceConfig,
|
||||
watchedOption,
|
||||
hasEnumWithDescriptionConfig,
|
||||
};
|
||||
}, [watchedPropertyType, propertyTypeOptions]);
|
||||
|
||||
@ -166,6 +176,7 @@ const AddCustomProperty = () => {
|
||||
formatConfig: string;
|
||||
entityReferenceConfig: string[];
|
||||
multiSelect?: boolean;
|
||||
enumWithDescriptionsConfig?: EnumWithDescriptionsConfig['values'];
|
||||
}
|
||||
) => {
|
||||
if (isUndefined(typeDetail)) {
|
||||
@ -197,6 +208,15 @@ const AddCustomProperty = () => {
|
||||
};
|
||||
}
|
||||
|
||||
if (hasEnumWithDescriptionConfig) {
|
||||
customPropertyConfig = {
|
||||
config: {
|
||||
multiSelect: Boolean(data?.multiSelect),
|
||||
values: data.enumWithDescriptionsConfig,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const payload = omitBy(
|
||||
{
|
||||
...omit(data, [
|
||||
@ -204,6 +224,7 @@ const AddCustomProperty = () => {
|
||||
'formatConfig',
|
||||
'entityReferenceConfig',
|
||||
'enumConfig',
|
||||
'enumWithDescriptionsConfig',
|
||||
]),
|
||||
propertyType: {
|
||||
id: data.propertyType,
|
||||
@ -393,6 +414,95 @@ const AddCustomProperty = () => {
|
||||
hasEntityReferenceConfig &&
|
||||
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])}
|
||||
<Row justify="end">
|
||||
<Col>
|
||||
|
@ -10,17 +10,19 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* 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 { isArray, isEmpty, isString, isUndefined } from 'lodash';
|
||||
import React, { FC, Fragment, useEffect, useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ReactComponent as IconEdit } from '../../../assets/svg/edit-new.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 { NO_PERMISSION_FOR_ACTION } from '../../../constants/HelperTextUtil';
|
||||
import { TABLE_SCROLL_VALUE } from '../../../constants/Table.constants';
|
||||
import { ERROR_PLACEHOLDER_TYPE, OPERATION } from '../../../enums/common.enum';
|
||||
import { EnumWithDescriptionsConfig } from '../../../generated/type/customProperties/enumWithDescriptionsConfig';
|
||||
import { CustomProperty } from '../../../generated/type/customProperty';
|
||||
import { columnSorter, getEntityName } from '../../../utils/EntityUtils';
|
||||
import ErrorPlaceHolder from '../../common/ErrorWithPlaceholder/ErrorPlaceHolder';
|
||||
@ -68,7 +70,9 @@ export const CustomPropertyTable: FC<CustomPropertyTableProp> = ({
|
||||
const updatedProperties = customProperties.map((property) => {
|
||||
if (property.name === selectedProperty.name) {
|
||||
const config = data.customPropertyConfig;
|
||||
const isEnumType = selectedProperty.propertyType.name === 'enum';
|
||||
const isEnumType =
|
||||
selectedProperty.propertyType.name === 'enum' ||
|
||||
selectedProperty.propertyType.name === ENUM_WITH_DESCRIPTION;
|
||||
|
||||
return {
|
||||
...property,
|
||||
@ -81,7 +85,7 @@ export const CustomPropertyTable: FC<CustomPropertyTableProp> = ({
|
||||
multiSelect: Boolean(data?.multiSelect),
|
||||
values: config,
|
||||
}
|
||||
: config,
|
||||
: (config as string[]),
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
@ -116,7 +120,7 @@ export const CustomPropertyTable: FC<CustomPropertyTableProp> = ({
|
||||
title: t('label.type'),
|
||||
dataIndex: 'propertyType',
|
||||
key: 'propertyType',
|
||||
render: (text) => getEntityName(text),
|
||||
render: (propertyType) => getEntityName(propertyType),
|
||||
},
|
||||
{
|
||||
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 (!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 (
|
||||
<div
|
||||
className="w-full d-flex gap-2 flex-column"
|
||||
|
@ -10,17 +10,30 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { Form, Modal, Typography } from 'antd';
|
||||
import { isUndefined, uniq } from 'lodash';
|
||||
import { InfoCircleOutlined, PlusOutlined } from '@ant-design/icons';
|
||||
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 { useTranslation } from 'react-i18next';
|
||||
import { ReactComponent as DeleteIcon } from '../../../../assets/svg/ic-delete.svg';
|
||||
import {
|
||||
ENTITY_REFERENCE_OPTIONS,
|
||||
ENUM_WITH_DESCRIPTION,
|
||||
PROPERTY_TYPES_WITH_ENTITY_REFERENCE,
|
||||
} from '../../../../constants/CustomProperty.constants';
|
||||
import {
|
||||
CustomProperty,
|
||||
EnumConfig,
|
||||
ValueClass,
|
||||
} from '../../../../generated/type/customProperty';
|
||||
import {
|
||||
FieldProp,
|
||||
@ -28,10 +41,11 @@ import {
|
||||
FormItemLayout,
|
||||
} from '../../../../interface/FormUtils.interface';
|
||||
import { generateFormFields } from '../../../../utils/formUtils';
|
||||
import RichTextEditor from '../../../common/RichTextEditor/RichTextEditor';
|
||||
|
||||
export interface FormData {
|
||||
description: string;
|
||||
customPropertyConfig: string[];
|
||||
customPropertyConfig: string[] | ValueClass[];
|
||||
multiSelect?: boolean;
|
||||
}
|
||||
|
||||
@ -58,15 +72,21 @@ const EditCustomPropertyModal: FC<EditCustomPropertyModalProps> = ({
|
||||
setIsSaving(false);
|
||||
};
|
||||
|
||||
const { hasEnumConfig, hasEntityReferenceConfig } = useMemo(() => {
|
||||
const {
|
||||
hasEnumConfig,
|
||||
hasEntityReferenceConfig,
|
||||
hasEnumWithDescriptionConfig,
|
||||
} = useMemo(() => {
|
||||
const propertyName = customProperty.propertyType.name ?? '';
|
||||
const hasEnumConfig = propertyName === 'enum';
|
||||
const hasEnumWithDescriptionConfig = propertyName === ENUM_WITH_DESCRIPTION;
|
||||
const hasEntityReferenceConfig =
|
||||
PROPERTY_TYPES_WITH_ENTITY_REFERENCE.includes(propertyName);
|
||||
|
||||
return {
|
||||
hasEnumConfig,
|
||||
hasEntityReferenceConfig,
|
||||
hasEnumWithDescriptionConfig,
|
||||
};
|
||||
}, [customProperty]);
|
||||
|
||||
@ -155,7 +175,7 @@ const EditCustomPropertyModal: FC<EditCustomPropertyModalProps> = ({
|
||||
};
|
||||
|
||||
const initialValues = useMemo(() => {
|
||||
if (hasEnumConfig) {
|
||||
if (hasEnumConfig || hasEnumWithDescriptionConfig) {
|
||||
const enumConfig = customProperty.customPropertyConfig
|
||||
?.config as EnumConfig;
|
||||
|
||||
@ -170,7 +190,7 @@ const EditCustomPropertyModal: FC<EditCustomPropertyModalProps> = ({
|
||||
description: customProperty.description,
|
||||
customPropertyConfig: customProperty.customPropertyConfig?.config,
|
||||
};
|
||||
}, [customProperty, hasEnumConfig]);
|
||||
}, [customProperty, hasEnumConfig, hasEnumWithDescriptionConfig]);
|
||||
|
||||
const note = (
|
||||
<Typography.Text
|
||||
@ -205,7 +225,7 @@ const EditCustomPropertyModal: FC<EditCustomPropertyModalProps> = ({
|
||||
})}
|
||||
</Typography.Text>
|
||||
}
|
||||
width={750}
|
||||
width={800}
|
||||
onCancel={onCancel}>
|
||||
<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 && (
|
||||
<>
|
||||
{generateFormFields([entityReferenceConfigField])}
|
||||
|
@ -153,17 +153,13 @@ describe('Test CustomProperty Table Component', () => {
|
||||
<CustomPropertyTable {...mockProp} entityType={EntityType.TABLE} />
|
||||
);
|
||||
});
|
||||
const table = await screen.findByTestId('custom-properties-table');
|
||||
const table = await screen.findByTestId('custom-properties-card');
|
||||
|
||||
expect(table).toBeInTheDocument();
|
||||
|
||||
const propertyName = await screen.findByText('label.name');
|
||||
const propertyValue = await screen.findByText('label.value');
|
||||
const rows = await screen.findAllByRole('row');
|
||||
const propertyValue = await screen.findByText('PropertyValue');
|
||||
|
||||
expect(propertyName).toBeInTheDocument();
|
||||
expect(propertyValue).toBeInTheDocument();
|
||||
expect(rows).toHaveLength(mockCustomProperties.length + 1);
|
||||
});
|
||||
|
||||
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} />
|
||||
);
|
||||
});
|
||||
const tableRowTitle = await screen.findByText('xName');
|
||||
|
||||
const tableRowValue = await screen.findByText('PropertyValue');
|
||||
|
||||
expect(tableRowTitle).toBeInTheDocument();
|
||||
expect(tableRowValue).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
@ -11,11 +11,16 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Skeleton, Typography } from 'antd';
|
||||
import { ColumnsType } from 'antd/lib/table';
|
||||
import { Col, Divider, Row, Skeleton, Typography } from 'antd';
|
||||
import { AxiosError } from 'axios';
|
||||
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 { Link } from 'react-router-dom';
|
||||
import { CUSTOM_PROPERTIES_DOCS } from '../../../constants/docs.constants';
|
||||
@ -28,11 +33,9 @@ import {
|
||||
import { ERROR_PLACEHOLDER_TYPE } from '../../../enums/common.enum';
|
||||
import { EntityTabs, EntityType } from '../../../enums/entity.enum';
|
||||
import { ChangeDescription, Type } from '../../../generated/entity/type';
|
||||
import { CustomProperty } from '../../../generated/type/customProperty';
|
||||
import { getTypeByFQN } from '../../../rest/metadataTypeAPI';
|
||||
import { Transi18next } from '../../../utils/CommonUtils';
|
||||
import entityUtilClassBase from '../../../utils/EntityUtilClassBase';
|
||||
import { columnSorter, getEntityName } from '../../../utils/EntityUtils';
|
||||
import {
|
||||
getChangedEntityNewValue,
|
||||
getDiffByFieldName,
|
||||
@ -40,7 +43,6 @@ import {
|
||||
} from '../../../utils/EntityVersionUtils';
|
||||
import { showErrorToast } from '../../../utils/ToastUtils';
|
||||
import ErrorPlaceHolder from '../ErrorWithPlaceholder/ErrorPlaceHolder';
|
||||
import Table from '../Table/Table';
|
||||
import {
|
||||
CustomPropertyProps,
|
||||
ExtentionEntities,
|
||||
@ -146,44 +148,6 @@ export const CustomPropertyTable = <T extends ExtentionEntitiesKeys>({
|
||||
return { extensionObject: 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 customProp = entityTypeDetail.customProperties ?? [];
|
||||
|
||||
@ -212,6 +176,14 @@ export const CustomPropertyTable = <T extends ExtentionEntitiesKeys>({
|
||||
maxDataCap,
|
||||
]);
|
||||
|
||||
const dataSource = useMemo(() => {
|
||||
const customProperties = entityTypeDetail?.customProperties ?? [];
|
||||
|
||||
return Array.isArray(customProperties)
|
||||
? customProperties.slice(0, maxDataCap)
|
||||
: [];
|
||||
}, [maxDataCap, entityTypeDetail?.customProperties]);
|
||||
|
||||
useEffect(() => {
|
||||
if (typePermission?.ViewAll || typePermission?.ViewBasic) {
|
||||
fetchTypeDetail();
|
||||
@ -277,21 +249,44 @@ export const CustomPropertyTable = <T extends ExtentionEntitiesKeys>({
|
||||
</Typography.Text>
|
||||
{viewAllBtn}
|
||||
</div>
|
||||
<Table
|
||||
bordered
|
||||
resizableColumns
|
||||
columns={tableColumn}
|
||||
data-testid="custom-properties-table"
|
||||
dataSource={entityTypeDetail?.customProperties?.slice(
|
||||
0,
|
||||
maxDataCap
|
||||
)}
|
||||
loading={entityTypeDetailLoading}
|
||||
pagination={false}
|
||||
rowKey="name"
|
||||
scroll={isRenderedInRightPanel ? { x: true } : undefined}
|
||||
size="small"
|
||||
/>
|
||||
|
||||
{isRenderedInRightPanel ? (
|
||||
<>
|
||||
{dataSource.map((record, index) => (
|
||||
<Fragment key={record.name}>
|
||||
<PropertyValue
|
||||
extension={extensionObject.extensionObject}
|
||||
hasEditPermissions={hasEditAccess}
|
||||
isRenderedInRightPanel={isRenderedInRightPanel}
|
||||
isVersionView={isVersionView}
|
||||
key={record.name}
|
||||
property={record}
|
||||
versionDataKeys={extensionObject.addedKeysList}
|
||||
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>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
|
@ -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');
|
||||
|
||||
expect(valueElement).toBeInTheDocument();
|
||||
expect(valueElement).toHaveLength(2);
|
||||
expect(iconElement).toBeInTheDocument();
|
||||
|
||||
await act(async () => {
|
||||
@ -146,7 +146,7 @@ describe('Test PropertyValue Component', () => {
|
||||
const iconElement = await screen.findByTestId('edit-icon');
|
||||
|
||||
expect(await screen.findByTestId('enum-value')).toHaveTextContent(
|
||||
'enumValue1, enumValue2'
|
||||
'enumValue1enumValue2'
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
|
@ -11,17 +11,23 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import Icon from '@ant-design/icons';
|
||||
import Icon, { DownOutlined, UpOutlined } from '@ant-design/icons';
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
Col,
|
||||
DatePicker,
|
||||
Divider,
|
||||
Form,
|
||||
Input,
|
||||
Row,
|
||||
Select,
|
||||
Tag,
|
||||
TimePicker,
|
||||
Tooltip,
|
||||
Typography,
|
||||
} from 'antd';
|
||||
import { ColumnsType } from 'antd/lib/table';
|
||||
import { AxiosError } from 'axios';
|
||||
import { t } from 'i18next';
|
||||
import {
|
||||
@ -35,7 +41,14 @@ import {
|
||||
toUpper,
|
||||
} from 'lodash';
|
||||
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 { ReactComponent as EditIconComponent } from '../../../assets/svg/edit-new.svg';
|
||||
import {
|
||||
@ -43,11 +56,12 @@ import {
|
||||
ICON_DIMENSION,
|
||||
VALIDATION_MESSAGES,
|
||||
} from '../../../constants/constants';
|
||||
import { ENUM_WITH_DESCRIPTION } from '../../../constants/CustomProperty.constants';
|
||||
import { TIMESTAMP_UNIX_IN_MILLISECONDS_REGEX } from '../../../constants/regex.constants';
|
||||
import { CSMode } from '../../../enums/codemirror.enum';
|
||||
import { SearchIndex } from '../../../enums/search.enum';
|
||||
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 { getEntityName } from '../../../utils/EntityUtils';
|
||||
import searchClassBase from '../../../utils/SearchClassBase';
|
||||
@ -59,6 +73,7 @@ import { ModalWithMarkdownEditor } from '../../Modals/ModalWithMarkdownEditor/Mo
|
||||
import InlineEdit from '../InlineEdit/InlineEdit.component';
|
||||
import ProfilePicture from '../ProfilePicture/ProfilePicture';
|
||||
import RichTextEditorPreviewer from '../RichTextEditor/RichTextEditorPreviewer';
|
||||
import Table from '../Table/Table';
|
||||
import {
|
||||
PropertyValueProps,
|
||||
PropertyValueType,
|
||||
@ -76,28 +91,55 @@ export const PropertyValue: FC<PropertyValueProps> = ({
|
||||
property,
|
||||
isRenderedInRightPanel = false,
|
||||
}) => {
|
||||
const propertyName = property.name;
|
||||
const propertyType = property.propertyType;
|
||||
const { propertyName, propertyType, value } = useMemo(() => {
|
||||
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 [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 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 isEnum = propertyType.name === 'enum';
|
||||
const isEnumWithDescription = propertyType.name === ENUM_WITH_DESCRIPTION;
|
||||
|
||||
const isArrayType = isArray(updatedValue);
|
||||
|
||||
const enumValue = isArrayType ? updatedValue : [updatedValue];
|
||||
|
||||
const propertyValue = isEnum
|
||||
? (enumValue as string[]).filter(Boolean)
|
||||
: updatedValue;
|
||||
const propertyValue =
|
||||
isEnum || isEnumWithDescription
|
||||
? (enumValue as string[]).filter(Boolean)
|
||||
: updatedValue;
|
||||
|
||||
try {
|
||||
// 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 'dateTime-cp': {
|
||||
// Default format is 'yyyy-mm-dd'
|
||||
@ -609,13 +705,22 @@ export const PropertyValue: FC<PropertyValueProps> = ({
|
||||
onFinish={(values: {
|
||||
entityReference: DataAssetOption | DataAssetOption[];
|
||||
}) => {
|
||||
if (isArray(values.entityReference)) {
|
||||
onInputSave(
|
||||
values.entityReference.map((item) => item.reference)
|
||||
);
|
||||
} else {
|
||||
onInputSave(values?.entityReference?.reference);
|
||||
const { entityReference } = values;
|
||||
|
||||
if (Array.isArray(entityReference)) {
|
||||
const references = entityReference
|
||||
.map((item) => findOptionReference(item, initialOptions))
|
||||
.filter(Boolean) as EntityReference[];
|
||||
onInputSave(references);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const reference = findOptionReference(
|
||||
entityReference,
|
||||
initialOptions
|
||||
);
|
||||
onInputSave(reference as EntityReference);
|
||||
}}>
|
||||
<Form.Item name="entityReference" style={commonStyle}>
|
||||
<DataAssetAsyncSelectList
|
||||
@ -656,11 +761,63 @@ export const PropertyValue: FC<PropertyValueProps> = ({
|
||||
|
||||
case 'enum':
|
||||
return (
|
||||
<Typography.Text className="break-all" data-testid="enum-value">
|
||||
{isArray(value) ? value.join(', ') : value}
|
||||
</Typography.Text>
|
||||
<>
|
||||
{isArray(value) ? (
|
||||
<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':
|
||||
return (
|
||||
<SchemaEditor
|
||||
@ -690,7 +847,7 @@ export const PropertyValue: FC<PropertyValueProps> = ({
|
||||
item.fullyQualifiedName as string
|
||||
)}>
|
||||
<Button
|
||||
className="entity-button flex-center p-0 m--ml-1"
|
||||
className="entity-button flex-center p-0"
|
||||
icon={
|
||||
<div className="entity-button-icon m-r-xs">
|
||||
{['user', 'team'].includes(item.type) ? (
|
||||
@ -699,7 +856,7 @@ export const PropertyValue: FC<PropertyValueProps> = ({
|
||||
isTeam={item.type === 'team'}
|
||||
name={item.name ?? ''}
|
||||
type="circle"
|
||||
width="18"
|
||||
width="24"
|
||||
/>
|
||||
) : (
|
||||
searchClassBase.getEntityIcon(item.type)
|
||||
@ -708,7 +865,7 @@ export const PropertyValue: FC<PropertyValueProps> = ({
|
||||
}
|
||||
type="text">
|
||||
<Typography.Text
|
||||
className="text-left text-xs"
|
||||
className="text-left text-lg"
|
||||
ellipsis={{ tooltip: true }}>
|
||||
{getEntityName(item)}
|
||||
</Typography.Text>
|
||||
@ -739,18 +896,18 @@ export const PropertyValue: FC<PropertyValueProps> = ({
|
||||
item.fullyQualifiedName as string
|
||||
)}>
|
||||
<Button
|
||||
className="entity-button flex-center p-0 m--ml-1"
|
||||
className="entity-button flex-center p-0"
|
||||
icon={
|
||||
<div
|
||||
className="entity-button-icon m-r-xs"
|
||||
style={{ width: '18px', display: 'flex' }}>
|
||||
style={{ width: '24px', display: 'flex' }}>
|
||||
{['user', 'team'].includes(item.type) ? (
|
||||
<ProfilePicture
|
||||
className="d-flex"
|
||||
isTeam={item.type === 'team'}
|
||||
name={item.name ?? ''}
|
||||
type="circle"
|
||||
width="18"
|
||||
width="24"
|
||||
/>
|
||||
) : (
|
||||
searchClassBase.getEntityIcon(item.type)
|
||||
@ -759,7 +916,7 @@ export const PropertyValue: FC<PropertyValueProps> = ({
|
||||
}
|
||||
type="text">
|
||||
<Typography.Text
|
||||
className="text-left text-xs"
|
||||
className="text-left text-lg"
|
||||
data-testid="entityReference-value-name"
|
||||
ellipsis={{ tooltip: true }}>
|
||||
{getEntityName(item)}
|
||||
@ -780,9 +937,17 @@ export const PropertyValue: FC<PropertyValueProps> = ({
|
||||
<Typography.Text
|
||||
className="break-all"
|
||||
data-testid="time-interval-value">
|
||||
{`StartTime: ${timeInterval.start}`}
|
||||
<br />
|
||||
{`EndTime: ${timeInterval.end}`}
|
||||
<span>
|
||||
<Typography.Text className="text-xs">{`StartTime: `}</Typography.Text>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
@ -798,7 +963,9 @@ export const PropertyValue: FC<PropertyValueProps> = ({
|
||||
case 'duration':
|
||||
default:
|
||||
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}
|
||||
</Typography.Text>
|
||||
);
|
||||
@ -817,15 +984,34 @@ export const PropertyValue: FC<PropertyValueProps> = ({
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
{showInput ? (
|
||||
getPropertyInput()
|
||||
) : (
|
||||
<Fragment>
|
||||
<div className="d-flex gap-2 items-center">
|
||||
{getValueElement()}
|
||||
{hasEditPermissions && (
|
||||
const toggleExpand = () => {
|
||||
setIsExpanded(!isExpanded);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!contentRef.current || !property) {
|
||||
return;
|
||||
}
|
||||
|
||||
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
|
||||
placement="left"
|
||||
title={t('label.edit-entity', { entity: propertyName })}>
|
||||
@ -839,9 +1025,57 @@ export const PropertyValue: FC<PropertyValueProps> = ({
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
</Fragment>
|
||||
)}
|
||||
</div>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<RichTextEditorPreviewer
|
||||
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>
|
||||
);
|
||||
};
|
||||
|
@ -10,6 +10,10 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
@btn-bg-color: #00000005;
|
||||
@enum-tag-bg-color: #00000008;
|
||||
|
||||
.entity-reference-list-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
@ -21,8 +25,27 @@
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
svg {
|
||||
height: 18px;
|
||||
width: 18px;
|
||||
height: 24px;
|
||||
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;
|
||||
}
|
||||
|
@ -126,3 +126,18 @@ export const SUPPORTED_FORMAT_MAP = {
|
||||
'dateTime-cp': SUPPORTED_DATE_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',
|
||||
];
|
||||
|
@ -619,6 +619,7 @@
|
||||
"june": "Juni",
|
||||
"jwt-token-expiry-time": "JWT token expiry time",
|
||||
"jwt-uppercase": "JWT",
|
||||
"key": "Key",
|
||||
"keyword-lowercase-plural": "Schlüsselwörter",
|
||||
"kill": "Beenden",
|
||||
"kpi-display-name": "KPI",
|
||||
@ -1498,6 +1499,7 @@
|
||||
"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-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-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.",
|
||||
|
@ -619,6 +619,7 @@
|
||||
"june": "June",
|
||||
"jwt-token-expiry-time": "JWT token expiry time",
|
||||
"jwt-uppercase": "JWT",
|
||||
"key": "Key",
|
||||
"keyword-lowercase-plural": "keywords",
|
||||
"kill": "Kill",
|
||||
"kpi-display-name": "KPI Display Name",
|
||||
@ -1498,6 +1499,7 @@
|
||||
"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-transfer-message": "Click on Confirm if you’d 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-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.",
|
||||
@ -1839,7 +1841,7 @@
|
||||
"update-displayName-entity": "Update Display Name for the {{entity}}.",
|
||||
"update-profiler-settings": "Update profiler setting.",
|
||||
"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",
|
||||
"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).",
|
||||
|
@ -619,6 +619,7 @@
|
||||
"june": "Junio",
|
||||
"jwt-token-expiry-time": "Tiempo de caducidad del token JWT",
|
||||
"jwt-uppercase": "JWT",
|
||||
"key": "Key",
|
||||
"keyword-lowercase-plural": "palabras clave",
|
||||
"kill": "Eliminar",
|
||||
"kpi-display-name": "Nombre para mostrar del KPI",
|
||||
@ -1498,6 +1499,7 @@
|
||||
"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-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-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.",
|
||||
|
@ -619,6 +619,7 @@
|
||||
"june": "Juin",
|
||||
"jwt-token-expiry-time": "délai d'expiration du jeton JWT",
|
||||
"jwt-uppercase": "JWT",
|
||||
"key": "Key",
|
||||
"keyword-lowercase-plural": "mots-clés",
|
||||
"kill": "Arrêter",
|
||||
"kpi-display-name": "KPI",
|
||||
@ -1498,6 +1499,7 @@
|
||||
"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-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-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.",
|
||||
|
@ -619,6 +619,7 @@
|
||||
"june": "יוני",
|
||||
"jwt-token-expiry-time": "זמן פג תוקף של אסימון JWT",
|
||||
"jwt-uppercase": "JWT",
|
||||
"key": "Key",
|
||||
"keyword-lowercase-plural": "מילות מפתח",
|
||||
"kill": "הרוג",
|
||||
"kpi-display-name": "שם תצוגת KPI",
|
||||
@ -1498,6 +1499,7 @@
|
||||
"entity-size-in-between": "{{entity}} יכול להיות בגודל בין {{min}} ל-{{max}}",
|
||||
"entity-size-must-be-between-2-and-64": "{{entity}} יכול להיות בגודל בין 2 ל-64",
|
||||
"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-team-transfer-message": "You cannot move to this team as Team Type {{dragTeam}} can't be {{dropTeam}} children",
|
||||
"error-while-fetching-access-token": "שגיאה בעת קבלת טוקן גישה.",
|
||||
|
@ -619,6 +619,7 @@
|
||||
"june": "6月",
|
||||
"jwt-token-expiry-time": "JWT token expiry time",
|
||||
"jwt-uppercase": "JWT",
|
||||
"key": "Key",
|
||||
"keyword-lowercase-plural": "キーワード",
|
||||
"kill": "終了",
|
||||
"kpi-display-name": "KPI表示名",
|
||||
@ -1498,6 +1499,7 @@
|
||||
"entity-size-in-between": "{{entity}}のサイズは{{min}}以上{{max}}以下にしてください",
|
||||
"entity-size-must-be-between-2-and-64": "{{entity}}のサイズは2以上64以下",
|
||||
"entity-transfer-message": "Click on Confirm if you’d 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-team-transfer-message": "You cannot move to this team as Team Type {{dragTeam}} can't be {{dropTeam}} children",
|
||||
"error-while-fetching-access-token": "アクセストークンの取得中にエラーが発生しました。",
|
||||
|
@ -619,6 +619,7 @@
|
||||
"june": "juni",
|
||||
"jwt-token-expiry-time": "Vervaltijd van JWT-token",
|
||||
"jwt-uppercase": "JWT",
|
||||
"key": "Key",
|
||||
"keyword-lowercase-plural": "trefwoorden",
|
||||
"kill": "Stoppen",
|
||||
"kpi-display-name": "Weergavenaam van KPI",
|
||||
@ -1498,6 +1499,7 @@
|
||||
"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-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-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.",
|
||||
|
@ -619,6 +619,7 @@
|
||||
"june": "Junho",
|
||||
"jwt-token-expiry-time": "Tempo de expiração do token JWT",
|
||||
"jwt-uppercase": "JWT",
|
||||
"key": "Key",
|
||||
"keyword-lowercase-plural": "palavras-chave",
|
||||
"kill": "Finalizar",
|
||||
"kpi-display-name": "Nome de Exibição do KPI",
|
||||
@ -1498,6 +1499,7 @@
|
||||
"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-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-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.",
|
||||
|
@ -619,6 +619,7 @@
|
||||
"june": "Июнь",
|
||||
"jwt-token-expiry-time": "JWT token expiry time",
|
||||
"jwt-uppercase": "JWT",
|
||||
"key": "Key",
|
||||
"keyword-lowercase-plural": "ключевые слова",
|
||||
"kill": "Уничтожить",
|
||||
"kpi-display-name": "Отображаемое имя KPI",
|
||||
@ -1498,6 +1499,7 @@
|
||||
"entity-size-in-between": "Размер {{entity}} должен быть между {{min}} и {{max}}",
|
||||
"entity-size-must-be-between-2-and-64": "Размер {{entity}} должен быть от 2 до 64",
|
||||
"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-team-transfer-message": "You cannot move to this team as Team Type {{dragTeam}} can't be {{dropTeam}} children",
|
||||
"error-while-fetching-access-token": "Ошибка при получении токена доступа.",
|
||||
|
@ -619,6 +619,7 @@
|
||||
"june": "六月",
|
||||
"jwt-token-expiry-time": "JWT 令牌到期时间",
|
||||
"jwt-uppercase": "JWT",
|
||||
"key": "Key",
|
||||
"keyword-lowercase-plural": "关键词",
|
||||
"kill": "终止",
|
||||
"kpi-display-name": "KPI 显示名称",
|
||||
@ -1498,6 +1499,7 @@
|
||||
"entity-size-in-between": "{{entity}}大小须介于{{min}}和{{max}}之间",
|
||||
"entity-size-must-be-between-2-and-64": "{{entity}}大小必须介于2和64之间",
|
||||
"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-team-transfer-message": "由于团队类型{{dragTeam}}不能是{{dropTeam}}的子团队, 因此您无法移动到此团队",
|
||||
"error-while-fetching-access-token": "获取访问令牌时出现错误",
|
||||
|
@ -535,7 +535,7 @@ const APICollectionPage: FunctionComponent = () => {
|
||||
);
|
||||
|
||||
const handleExtensionUpdate = async (apiCollectionData: APICollection) => {
|
||||
await saveUpdatedAPICollectionData({
|
||||
const response = await saveUpdatedAPICollectionData({
|
||||
...apiCollection,
|
||||
extension: apiCollectionData.extension,
|
||||
});
|
||||
@ -546,7 +546,7 @@ const APICollectionPage: FunctionComponent = () => {
|
||||
|
||||
return {
|
||||
...prev,
|
||||
extension: apiCollectionData.extension,
|
||||
extension: response.extension,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
@ -555,7 +555,7 @@ const DatabaseSchemaPage: FunctionComponent = () => {
|
||||
);
|
||||
|
||||
const handleExtensionUpdate = async (schema: DatabaseSchema) => {
|
||||
await saveUpdatedDatabaseSchemaData({
|
||||
const response = await saveUpdatedDatabaseSchemaData({
|
||||
...databaseSchema,
|
||||
extension: schema.extension,
|
||||
});
|
||||
@ -566,7 +566,7 @@ const DatabaseSchemaPage: FunctionComponent = () => {
|
||||
|
||||
return {
|
||||
...prev,
|
||||
extension: schema.extension,
|
||||
extension: response.extension,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
@ -477,21 +477,22 @@ const StoredProcedurePage = () => {
|
||||
|
||||
const onExtensionUpdate = useCallback(
|
||||
async (updatedData: StoredProcedure) => {
|
||||
storedProcedure &&
|
||||
(await saveUpdatedStoredProceduresData({
|
||||
if (storedProcedure) {
|
||||
const response = await saveUpdatedStoredProceduresData({
|
||||
...storedProcedure,
|
||||
extension: updatedData.extension,
|
||||
}));
|
||||
setStoredProcedure((prev) => {
|
||||
if (!prev) {
|
||||
return prev;
|
||||
}
|
||||
});
|
||||
setStoredProcedure((prev) => {
|
||||
if (!prev) {
|
||||
return prev;
|
||||
}
|
||||
|
||||
return {
|
||||
...prev,
|
||||
extension: updatedData.extension,
|
||||
};
|
||||
});
|
||||
return {
|
||||
...prev,
|
||||
extension: response.extension,
|
||||
};
|
||||
});
|
||||
}
|
||||
},
|
||||
[saveUpdatedStoredProceduresData, storedProcedure]
|
||||
);
|
||||
|
Loading…
x
Reference in New Issue
Block a user