Feature : Add table-type custom property (#18135)

This commit is contained in:
sonika-shah 2024-10-11 02:13:19 +05:30 committed by Sachin Chaurasiya
parent 2ef829052d
commit 1252698869
41 changed files with 2227 additions and 2309 deletions

View File

@ -320,11 +320,7 @@ public final class CsvUtil {
return "";
}
if (list.get(0) instanceof Map && isEnumWithDescriptions((Map<String, Object>) list.get(0))) {
return list.stream()
.map(item -> ((Map<String, Object>) item).get("key").toString())
.collect(Collectors.joining(INTERNAL_ARRAY_SEPARATOR));
} else if (list.get(0) instanceof Map) {
if (list.get(0) instanceof Map) {
return list.stream()
.map(item -> formatMapValue((Map<String, Object>) item))
.collect(Collectors.joining(INTERNAL_ARRAY_SEPARATOR));
@ -343,10 +339,6 @@ public final class CsvUtil {
return valueMap.containsKey("start") && valueMap.containsKey("end");
}
private static boolean isEnumWithDescriptions(Map<String, Object> valueMap) {
return valueMap.containsKey("key") && valueMap.containsKey("description");
}
private static String formatEntityReference(Map<String, Object> valueMap) {
return valueMap.get("type") + ENTITY_TYPE_SEPARATOR + valueMap.get("fullyQualifiedName");
}

View File

@ -46,8 +46,6 @@ import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
import java.util.stream.StreamSupport;
import javax.ws.rs.core.Response;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.csv.CSVFormat;
@ -68,7 +66,6 @@ import org.openmetadata.schema.type.csv.CsvErrorType;
import org.openmetadata.schema.type.csv.CsvFile;
import org.openmetadata.schema.type.csv.CsvHeader;
import org.openmetadata.schema.type.csv.CsvImportResult;
import org.openmetadata.schema.type.customproperties.EnumWithDescriptionsConfig;
import org.openmetadata.service.Entity;
import org.openmetadata.service.TypeRegistry;
import org.openmetadata.service.jdbi3.EntityRepository;
@ -408,16 +405,6 @@ public abstract class EntityCsv<T extends EntityInterface> {
fieldValue = null;
}
}
case "enumWithDescriptions" -> {
fieldValue =
parseEnumWithDescriptions(
printer,
csvRecord,
fieldNumber,
fieldName,
fieldValue.toString(),
propertyConfig);
}
default -> {}
}
// Validate the field against the JSON schema
@ -461,70 +448,6 @@ public abstract class EntityCsv<T extends EntityInterface> {
return isList ? entityReferences : entityReferences.isEmpty() ? null : entityReferences.get(0);
}
private Object parseEnumWithDescriptions(
CSVPrinter printer,
CSVRecord csvRecord,
int fieldNumber,
String fieldName,
String fieldValue,
String propertyConfig)
throws IOException {
List<String> enumKeys = listOrEmpty(fieldToInternalArray(fieldValue));
List<Object> enumObjects = new ArrayList<>();
JsonNode propertyConfigNode = JsonUtils.readTree(propertyConfig);
if (propertyConfigNode == null) {
importFailure(
printer,
invalidCustomPropertyFieldFormat(
fieldNumber,
fieldName,
"enumWithDescriptions",
"Invalid propertyConfig of enumWithDescriptions: " + fieldValue),
csvRecord);
return null;
}
Map<String, JsonNode> keyToObjectMap =
StreamSupport.stream(propertyConfigNode.get("values").spliterator(), false)
.collect(Collectors.toMap(node -> node.get("key").asText(), node -> node));
EnumWithDescriptionsConfig config =
JsonUtils.treeToValue(propertyConfigNode, EnumWithDescriptionsConfig.class);
if (!config.getMultiSelect() && enumKeys.size() > 1) {
importFailure(
printer,
invalidCustomPropertyFieldFormat(
fieldNumber,
fieldName,
"enumWithDescriptions",
"only one key is allowed for non-multiSelect enumWithDescriptions"),
csvRecord);
return null;
}
for (String key : enumKeys) {
try {
JsonNode valueObject = keyToObjectMap.get(key);
if (valueObject == null) {
importFailure(
printer,
invalidCustomPropertyValue(
fieldNumber,
fieldName,
"enumWithDescriptions",
key + " not found in propertyConfig of " + fieldName),
csvRecord);
return null;
}
enumObjects.add(valueObject);
} catch (Exception e) {
importFailure(printer, e.getMessage(), csvRecord);
}
}
return enumObjects;
}
protected String getFormattedDateTimeField(
CSVPrinter printer,
CSVRecord csvRecord,

View File

@ -253,6 +253,10 @@ public final class CatalogExceptionMessage {
return String.format("Custom field %s has invalid JSON %s", fieldName, validationMessages);
}
public static String customPropertyConfigError(String fieldName, String validationMessages) {
return String.format("Custom Property %s has invalid value %s", fieldName, validationMessages);
}
public static String invalidParent(Team parent, String child, TeamType childType) {
return String.format(
"Team %s of type %s can't be of parent of team %s of type %s",

View File

@ -67,7 +67,6 @@ 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;
@ -101,8 +100,8 @@ 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.ConstraintViolationException;
import javax.validation.constraints.NotNull;
import javax.ws.rs.core.Response.Status;
import javax.ws.rs.core.UriInfo;
@ -147,7 +146,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.type.customproperties.TableConfig;
import org.openmetadata.schema.utils.EntityInterfaceUtil;
import org.openmetadata.service.Entity;
import org.openmetadata.service.OpenMetadataApplicationConfig;
@ -1493,8 +1492,7 @@ public abstract class EntityRepository<T extends EntityInterface> {
fieldValue.textValue(), customPropertyType, propertyConfig, fieldName);
jsonNode.put(fieldName, formattedValue);
}
case "enumWithDescriptions" -> handleEnumWithDescriptions(
fieldName, fieldValue, propertyConfig, jsonNode, entity);
case "table-cp" -> validateTableType(fieldValue, propertyConfig, fieldName);
default -> {}
}
}
@ -1532,37 +1530,54 @@ public abstract class EntityRepository<T extends EntityInterface> {
}
}
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);
private void validateTableType(JsonNode fieldValue, String propertyConfig, String fieldName) {
TableConfig tableConfig =
JsonUtils.convertValue(JsonUtils.readTree(propertyConfig), TableConfig.class);
org.openmetadata.schema.type.customproperties.Table tableValue =
JsonUtils.convertValue(
JsonUtils.readTree(String.valueOf(fieldValue)),
org.openmetadata.schema.type.customproperties.Table.class);
Set<String> configColumns = tableConfig.getColumns();
try {
JsonUtils.validateJsonSchema(
tableValue, org.openmetadata.schema.type.customproperties.Table.class);
Set<String> fieldColumns = new HashSet<>();
fieldValue.get("columns").forEach(column -> fieldColumns.add(column.asText()));
Set<String> undefinedColumns = new HashSet<>(fieldColumns);
undefinedColumns.removeAll(configColumns);
if (!undefinedColumns.isEmpty()) {
throw new IllegalArgumentException(
"Expected columns: "
+ configColumns
+ ", but found undefined columns: "
+ undefinedColumns);
}
if (fieldValue.get("rows").size() > tableConfig.getRowCount()) {
throw new IllegalArgumentException(
"Number of rows should be less than or equal to the expected row count "
+ tableConfig.getRowCount());
}
Set<String> rowFieldNames = new HashSet<>();
fieldValue.get("rows").forEach(row -> row.fieldNames().forEachRemaining(rowFieldNames::add));
undefinedColumns = new HashSet<>(rowFieldNames);
undefinedColumns.removeAll(configColumns);
if (!undefinedColumns.isEmpty()) {
throw new IllegalArgumentException("Rows contain undefined columns: " + undefinedColumns);
}
} catch (ConstraintViolationException e) {
String validationErrors =
e.getConstraintViolations().stream()
.map(violation -> violation.getPropertyPath() + " " + violation.getMessage())
.collect(Collectors.joining(", "));
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));
CatalogExceptionMessage.jsonValidationError(fieldName, validationErrors));
}
}

View File

@ -22,6 +22,8 @@ import static org.openmetadata.service.Entity.FIELD_DESCRIPTION;
import static org.openmetadata.service.util.EntityUtil.customFieldMatch;
import static org.openmetadata.service.util.EntityUtil.getCustomField;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.HashSet;
@ -29,6 +31,7 @@ import java.util.List;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
import javax.validation.ConstraintViolationException;
import javax.ws.rs.core.UriInfo;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.tuple.Triple;
@ -41,10 +44,10 @@ 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.schema.type.customproperties.TableConfig;
import org.openmetadata.service.Entity;
import org.openmetadata.service.TypeRegistry;
import org.openmetadata.service.exception.CatalogExceptionMessage;
import org.openmetadata.service.resources.types.TypeResource;
import org.openmetadata.service.util.EntityUtil;
import org.openmetadata.service.util.EntityUtil.Fields;
@ -173,8 +176,7 @@ public class TypeRepository extends EntityRepository<Type> {
private void validateProperty(CustomProperty customProperty) {
switch (customProperty.getPropertyType().getName()) {
case "enum" -> validateEnumConfig(customProperty.getCustomPropertyConfig());
case "enumWithDescriptions" -> validateEnumWithDescriptionsConfig(
customProperty.getCustomPropertyConfig());
case "table-cp" -> validateTableTypeConfig(customProperty.getCustomPropertyConfig());
case "date-cp" -> validateDateFormat(
customProperty.getCustomPropertyConfig(), getDateTokens(), "Invalid date format");
case "dateTime-cp" -> validateDateFormat(
@ -233,25 +235,37 @@ public class TypeRepository extends EntityRepository<Type> {
}
}
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 {
private void validateTableTypeConfig(CustomPropertyConfig config) {
if (config == null) {
throw new IllegalArgumentException("Table Custom Property Type must have config populated.");
}
JsonNode configNode = JsonUtils.valueToTree(config.getConfig());
TableConfig tableConfig = JsonUtils.convertValue(config.getConfig(), TableConfig.class);
// rowCount is optional, if not present set it to the default value
if (!configNode.has("rowCount")) {
((ObjectNode) configNode).put("rowCount", tableConfig.getRowCount());
config.setConfig(configNode);
}
List<String> columns = new ArrayList<>();
configNode.path("columns").forEach(node -> columns.add(node.asText()));
Set<String> uniqueColumns = new HashSet<>(columns);
if (uniqueColumns.size() != columns.size()) {
throw new IllegalArgumentException("Column names must be unique.");
}
try {
JsonUtils.validateJsonSchema(config.getConfig(), TableConfig.class);
} catch (ConstraintViolationException e) {
String validationErrors =
e.getConstraintViolations().stream()
.map(violation -> violation.getPropertyPath() + " " + violation.getMessage())
.collect(Collectors.joining(", "));
throw new IllegalArgumentException(
"EnumWithDescriptions Custom Property Type must have customPropertyConfig.");
CatalogExceptionMessage.customPropertyConfigError("table", validationErrors));
}
}
@ -413,27 +427,6 @@ 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.");
}
}
}
}

View File

@ -398,8 +398,7 @@ public abstract class EntityResourceTest<T extends EntityInterface, K extends Cr
public static Type TIMESTAMP_TYPE;
public static Type ENUM_TYPE;
public static Type ENUM_WITH_DESCRIPTIONS_TYPE;
public static Type TABLE_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,

View File

@ -643,49 +643,6 @@ public class GlossaryResourceTest extends EntityResourceTest<Glossary, CreateGlo
11, "glossaryTermDateCp", DATECP_TYPE.getDisplayName(), "dd-MM-yyyy"))
};
assertRows(result, expectedRows);
// Create glossaryTerm with Invalid custom property of type enumWithDescriptions
CustomProperty glossaryTermEnumCp =
new CustomProperty()
.withName("glossaryTermEnumWithDescriptionsCp")
.withDescription("enumWithDescriptions type custom property with multiselect = true")
.withPropertyType(ENUM_WITH_DESCRIPTIONS_TYPE.getEntityReference())
.withCustomPropertyConfig(
new CustomPropertyConfig()
.withConfig(
Map.of(
"values",
List.of(
Map.of("key", "key1", "description", "description1"),
Map.of("key", "key2", "description", "description2")),
"multiSelect",
true)));
entityType =
typeResourceTest.getEntityByName(
Entity.GLOSSARY_TERM, "customProperties", ADMIN_AUTH_HEADERS);
entityType =
typeResourceTest.addAndCheckCustomProperty(
entityType.getId(), glossaryTermEnumCp, OK, ADMIN_AUTH_HEADERS);
String invalidEnumWithDescriptionRecord =
",g1,dsp1,dsc1,h1;h2;h3,,term1;http://term1,PII.None,,,,glossaryTermEnumWithDescriptionsCp:key1|key3";
String invalidEnumWithDescriptionValue =
",g1,dsp1,dsc1,h1;h2;h3,,term1;http://term1,PII.None,,,,glossaryTermEnumWithDescriptionsCp:key1|key3";
csv = createCsv(GlossaryCsv.HEADERS, listOf(invalidEnumWithDescriptionValue), null);
result = importCsv(glossaryName, csv, false);
Awaitility.await().atMost(4, TimeUnit.SECONDS).until(() -> true);
assertSummary(result, ApiStatus.PARTIAL_SUCCESS, 2, 1, 1);
expectedRows =
new String[] {
resultsHeader,
getFailedRecord(
invalidEnumWithDescriptionRecord,
invalidCustomPropertyValue(
11,
"glossaryTermEnumWithDescriptionsCp",
ENUM_WITH_DESCRIPTIONS_TYPE.getDisplayName(),
"key3 not found in propertyConfig of glossaryTermEnumWithDescriptionsCp"))
};
assertRows(result, expectedRows);
}
@Test
@ -810,21 +767,7 @@ public class GlossaryResourceTest extends EntityResourceTest<Glossary, CreateGlo
.withName("glossaryTermEnumCpMulti")
.withDescription("enum type custom property with multiselect = true")
.withPropertyType(ENUM_TYPE.getEntityReference())
.withCustomPropertyConfig(enumConfig),
new CustomProperty()
.withName("glossaryTermEnumWithDescriptionsCp")
.withDescription("enumWithDescriptions type custom property with multiselect = true")
.withPropertyType(ENUM_WITH_DESCRIPTIONS_TYPE.getEntityReference())
.withCustomPropertyConfig(
new CustomPropertyConfig()
.withConfig(
Map.of(
"values",
List.of(
Map.of("key", "key1", "description", "description1"),
Map.of("key", "key2", "description", "description2")),
"multiSelect",
true)))
.withCustomPropertyConfig(enumConfig)
};
for (CustomProperty customProperty : customProperties) {
@ -845,7 +788,7 @@ public class GlossaryResourceTest extends EntityResourceTest<Glossary, CreateGlo
",g2,dsp2,dsc3,h1;h3;h3,,term2;https://term2,PII.NonSensitive,,user:%s,%s,\"glossaryTermEnumCpMulti:val3|val2|val1|val4|val5;glossaryTermEnumCpSingle:singleVal1;glossaryTermIntegerCp:7777;glossaryTermMarkdownCp:# Sample Markdown Text;glossaryTermNumberCp:123456;\"\"glossaryTermQueryCp:select col,row from table where id ='30';\"\";glossaryTermStringCp:sample string content;glossaryTermTimeCp:10:08:45;glossaryTermTimeIntervalCp:1726142300000:17261420000;glossaryTermTimestampCp:1726142400000\"",
user1, "Approved"),
String.format(
"importExportTest.g1,g11,dsp2,dsc11,h1;h3;h3,,,,user:%s,team:%s,%s,glossaryTermEnumWithDescriptionsCp:key1|key2",
"importExportTest.g1,g11,dsp2,dsc11,h1;h3;h3,,,,user:%s,team:%s,%s,",
reviewerRef.get(0), team11, "Draft"));
// Update terms with change in description
@ -858,7 +801,7 @@ public class GlossaryResourceTest extends EntityResourceTest<Glossary, CreateGlo
",g2,dsp2,new-dsc3,h1;h3;h3,,term2;https://term2,PII.NonSensitive,user:%s,user:%s,%s,\"glossaryTermEnumCpMulti:val3|val2|val1|val4|val5;glossaryTermEnumCpSingle:singleVal1;glossaryTermIntegerCp:7777;glossaryTermMarkdownCp:# Sample Markdown Text;glossaryTermNumberCp:123456;\"\"glossaryTermQueryCp:select col,row from table where id ='30';\"\";glossaryTermStringCp:sample string content;glossaryTermTimeCp:10:08:45;glossaryTermTimeIntervalCp:1726142300000:17261420000;glossaryTermTimestampCp:1726142400000\"",
user1, user2, "Approved"),
String.format(
"importExportTest.g1,g11,dsp2,new-dsc11,h1;h3;h3,,,,user:%s,team:%s,%s,glossaryTermEnumWithDescriptionsCp:key1|key2",
"importExportTest.g1,g11,dsp2,new-dsc11,h1;h3;h3,,,,user:%s,team:%s,%s,",
reviewerRef.get(0), team11, "Draft"));
// Add new row to existing rows

View File

@ -25,10 +25,14 @@ import static org.openmetadata.service.util.TestUtils.assertCustomProperties;
import static org.openmetadata.service.util.TestUtils.assertResponse;
import static org.openmetadata.service.util.TestUtils.assertResponseContains;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import javax.ws.rs.client.WebTarget;
import javax.ws.rs.core.Response.Status;
@ -45,8 +49,7 @@ 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.schema.type.customproperties.TableConfig;
import org.openmetadata.service.Entity;
import org.openmetadata.service.resources.EntityResourceTest;
import org.openmetadata.service.resources.types.TypeResource;
@ -73,7 +76,6 @@ public class TypeResourceTest extends EntityResourceTest<Type, CreateType> {
STRING_TYPE = getEntityByName("string", "", ADMIN_AUTH_HEADERS);
EMAIL_TYPE = getEntityByName("email", "", ADMIN_AUTH_HEADERS);
ENUM_TYPE = getEntityByName("enum", "", ADMIN_AUTH_HEADERS);
ENUM_WITH_DESCRIPTIONS_TYPE = getEntityByName("enumWithDescriptions", "", ADMIN_AUTH_HEADERS);
DATECP_TYPE = getEntityByName("date-cp", "", ADMIN_AUTH_HEADERS);
DATETIMECP_TYPE = getEntityByName("dateTime-cp", "", ADMIN_AUTH_HEADERS);
TIMECP_TYPE = getEntityByName("time-cp", "", ADMIN_AUTH_HEADERS);
@ -85,6 +87,7 @@ public class TypeResourceTest extends EntityResourceTest<Type, CreateType> {
NUMBER_TYPE = getEntityByName("number", "", ADMIN_AUTH_HEADERS);
SQLQUERY_TYPE = getEntityByName("sqlQuery", "", ADMIN_AUTH_HEADERS);
TIMESTAMP_TYPE = getEntityByName("timestamp", "", ADMIN_AUTH_HEADERS);
TABLE_TYPE = getEntityByName("table-cp", "", ADMIN_AUTH_HEADERS);
}
@Override
@ -296,174 +299,146 @@ public class TypeResourceTest extends EntityResourceTest<Type, CreateType> {
}
@Test
void put_patch_customProperty_enumWithDescriptions_200() throws IOException {
void put_patch_customProperty_table_200() throws IOException {
Type databaseEntity = getEntityByName("database", "customProperties", ADMIN_AUTH_HEADERS);
TableConfig tableConfig = new TableConfig();
// Add a custom property of type enumWithDescriptions with PUT
CustomProperty enumWithDescriptionsFieldA =
// Add a custom property of type table with PUT
CustomProperty tableTypeFieldA =
new CustomProperty()
.withName("enumWithDescriptionsTest")
.withDescription("enumWithDescriptionsTest")
.withPropertyType(ENUM_WITH_DESCRIPTIONS_TYPE.getEntityReference());
.withName("tableCustomPropertyTest")
.withDescription("tableCustomPropertyTest description")
.withPropertyType(TABLE_TYPE.getEntityReference());
ChangeDescription change = getChangeDescription(databaseEntity, MINOR_UPDATE);
fieldAdded(change, "customProperties", new ArrayList<>(List.of(enumWithDescriptionsFieldA)));
fieldAdded(change, "customProperties", new ArrayList<>(List.of(tableTypeFieldA)));
Type finalDatabaseEntity = databaseEntity;
ChangeDescription finalChange = change;
assertResponseContains(
() ->
addCustomPropertyAndCheck(
finalDatabaseEntity.getId(),
enumWithDescriptionsFieldA,
tableTypeFieldA,
ADMIN_AUTH_HEADERS,
MINOR_UPDATE,
finalChange),
Status.BAD_REQUEST,
"EnumWithDescriptions Custom Property Type must have customPropertyConfig.");
enumWithDescriptionsFieldA.setCustomPropertyConfig(
new CustomPropertyConfig().withConfig(new EnumWithDescriptionsConfig()));
"Table Custom Property Type must have config populated.");
tableTypeFieldA.setCustomPropertyConfig(
new CustomPropertyConfig().withConfig(new TableConfig()));
ChangeDescription change1 = getChangeDescription(databaseEntity, MINOR_UPDATE);
Type databaseEntity1 = databaseEntity;
assertResponseContains(
() ->
addCustomPropertyAndCheck(
databaseEntity1.getId(),
enumWithDescriptionsFieldA,
tableTypeFieldA,
ADMIN_AUTH_HEADERS,
MINOR_UPDATE,
change1),
Status.BAD_REQUEST,
"EnumWithDescriptions Custom Property Type must have customPropertyConfig populated with values.");
"Custom Property table has invalid value columns size must be between "
+ tableConfig.getMinColumns()
+ " and "
+ tableConfig.getMaxColumns());
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(
tableTypeFieldA.setCustomPropertyConfig(
new CustomPropertyConfig()
.withConfig(new EnumWithDescriptionsConfig().withValues(valuesWithDuplicateKey)));
ChangeDescription change7 = getChangeDescription(databaseEntity, MINOR_UPDATE);
.withConfig(
new TableConfig()
.withColumns(Set.of("column-1", "column-2", "column-3"))
.withRowCount(200)));
ChangeDescription change2 = getChangeDescription(databaseEntity, MINOR_UPDATE);
Type databaseEntity2 = databaseEntity;
assertResponseContains(
() ->
addCustomPropertyAndCheck(
databaseEntity2.getId(),
enumWithDescriptionsFieldA,
tableTypeFieldA,
ADMIN_AUTH_HEADERS,
MINOR_UPDATE,
change7),
change2),
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());
"Custom Property table has invalid value rowCount must be less than or equal to "
+ tableConfig.getMaxRows());
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)));
ObjectMapper mapper = new ObjectMapper();
ObjectNode tableConfigJson = mapper.createObjectNode();
ArrayNode columnsArray = tableConfigJson.putArray("columns");
columnsArray.add("col 1");
columnsArray.add("col 2");
columnsArray.add("col");
columnsArray.add("col");
tableTypeFieldA.setCustomPropertyConfig(new CustomPropertyConfig().withConfig(tableConfigJson));
ChangeDescription change3 = getChangeDescription(databaseEntity, MINOR_UPDATE);
Type databaseEntity3 = databaseEntity;
assertResponseContains(
() ->
addCustomPropertyAndCheck(
databaseEntity1.getId(),
enumWithDescriptionsFieldA,
databaseEntity3.getId(),
tableTypeFieldA,
ADMIN_AUTH_HEADERS,
MINOR_UPDATE,
change3),
Status.BAD_REQUEST,
"Existing EnumWithDescriptions Custom Property values cannot be removed.");
"Column names must be unique.");
enumWithDescriptionsFieldA.setCustomPropertyConfig(
tableTypeFieldA.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());
.withConfig(new TableConfig().withColumns(Set.of("column1", "column2", "column3"))));
databaseEntity =
addCustomPropertyAndCheck(
databaseEntity.getId(),
enumWithDescriptionsFieldA,
ADMIN_AUTH_HEADERS,
MINOR_UPDATE,
change5);
databaseEntity.getId(), tableTypeFieldA, ADMIN_AUTH_HEADERS, MINOR_UPDATE, change);
assertCustomProperties(
new ArrayList<>(List.of(enumWithDescriptionsFieldA)), databaseEntity.getCustomProperties());
new ArrayList<>(List.of(tableTypeFieldA)), databaseEntity.getCustomProperties());
CustomPropertyConfig prevConfig = tableTypeFieldA.getCustomPropertyConfig();
// Changing custom property description with PUT
tableTypeFieldA.withDescription("updated tableCustomPropertyTest description");
ChangeDescription change5 = getChangeDescription(databaseEntity, MINOR_UPDATE);
fieldUpdated(
change5,
EntityUtil.getCustomField(tableTypeFieldA, "description"),
"tableCustomPropertyTest description",
"updated tableCustomPropertyTest description");
databaseEntity =
addCustomPropertyAndCheck(
databaseEntity.getId(), tableTypeFieldA, ADMIN_AUTH_HEADERS, MINOR_UPDATE, change5);
assertCustomProperties(
new ArrayList<>(List.of(tableTypeFieldA)), databaseEntity.getCustomProperties());
ChangeDescription change6 = getChangeDescription(databaseEntity, MINOR_UPDATE);
tableTypeFieldA.setCustomPropertyConfig(
new CustomPropertyConfig()
.withConfig(new TableConfig().withColumns(Set.of("column-1", "column-2", "column-3"))));
fieldUpdated(
change6,
EntityUtil.getCustomField(tableTypeFieldA, "customPropertyConfig"),
prevConfig,
tableTypeFieldA.getCustomPropertyConfig());
databaseEntity =
addCustomPropertyAndCheck(
databaseEntity.getId(), tableTypeFieldA, ADMIN_AUTH_HEADERS, MINOR_UPDATE, change6);
assertCustomProperties(
new ArrayList<>(List.of(tableTypeFieldA)), databaseEntity.getCustomProperties());
// Changing custom property description with PATCH
// Changes from this PATCH is consolidated with the previous changes
enumWithDescriptionsFieldA.withDescription("updated2");
tableTypeFieldA.withDescription("updated tableCustomPropertyTest description 2");
String json = JsonUtils.pojoToJson(databaseEntity);
databaseEntity.setCustomProperties(List.of(enumWithDescriptionsFieldA));
databaseEntity.setCustomProperties(List.of(tableTypeFieldA));
change = getChangeDescription(databaseEntity, CHANGE_CONSOLIDATED);
fieldUpdated(
change5,
EntityUtil.getCustomField(enumWithDescriptionsFieldA, "description"),
"updatedEnumWithDescriptionsTest",
"updated2");
change6,
EntityUtil.getCustomField(tableTypeFieldA, "description"),
"updated tableCustomPropertyTest description",
"updated tableCustomPropertyTest description 2");
databaseEntity =
patchEntityAndCheck(databaseEntity, json, ADMIN_AUTH_HEADERS, CHANGE_CONSOLIDATED, change5);
patchEntityAndCheck(databaseEntity, json, ADMIN_AUTH_HEADERS, CHANGE_CONSOLIDATED, change6);
}
@Test

View File

@ -1,7 +1,6 @@
{
"$id": "https://open-metadata.org/schema/type/customProperties/complexTypes.json",
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Basic",
"description": "This schema defines custom properties complex types.",
"definitions": {
"entityReference": {
@ -99,6 +98,47 @@
}
}
}
},
"table-cp": {
"$comment": "@om-field-type",
"title": "Table",
"description": "A table-type custom property having rows and columns where all column data types are strings.",
"type": "object",
"javaType": "org.openmetadata.schema.type.customproperties.Table",
"properties": {
"columns": {
"type": "array",
"description": "List of column names defined at the entity type level.",
"items": {
"type": "string",
"description": "The name of the column."
},
"minItems": 1,
"maxItems": 3,
"uniqueItems": true
},
"rows": {
"type": "array",
"description": "List of rows added at the entity instance level. Each row contains dynamic fields based on the defined columns.",
"items": {
"type": "object",
"description": "A row in the table, with dynamic key-value pairs corresponding to columns.",
"additionalProperties": {
"type": "string",
"description": "The cell value of each column in the row."
}
},
"minItems": 1,
"maxItems": 10
}
},
"required": ["columns"],
"additionalProperties": false
}
},
"properties": {
"table-cp": {
"$ref": "#/definitions/table-cp"
}
}
}

View File

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

View File

@ -0,0 +1,50 @@
{
"$id": "https://open-metadata.org/schema/type/customProperties/tableConfig.json",
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "TableConfig",
"description": "Custom property configuration for table-type property where all column data types are strings.",
"type": "object",
"javaType": "org.openmetadata.schema.type.customproperties.TableConfig",
"properties": {
"columns": {
"type": "array",
"description": "List of column names defined at the entity type level.",
"items": {
"type": "string",
"description": "The name of the column."
},
"minItems": 1,
"maxItems": 3,
"uniqueItems": true
},
"rowCount": {
"type": "integer",
"default": 10,
"description": "Number of rows. Defaults to maxRows if not explicitly set.",
"minimum": 1,
"maximum": 10
},
"minColumns": {
"type": "integer",
"default": 1,
"$comment": "For internal use only: Minimum number of columns."
},
"maxColumns": {
"type": "integer",
"default": 3,
"$comment": "For internal use only: Maximum number of columns."
},
"minRows": {
"type": "integer",
"default": 1,
"$comment": "For internal use only: Minimum number of rows."
},
"maxRows": {
"type": "integer",
"default": 10,
"$comment": "For internal use only: Maximum number of rows."
}
},
"required": ["columns"],
"additionalProperties": false
}

View File

@ -35,7 +35,7 @@
"$ref": "#/definitions/entityTypes"
},
{
"$ref": "../type/customProperties/enumWithDescriptionsConfig.json"
"$ref": "customProperties/tableConfig.json"
}
]
}

View File

@ -2,7 +2,6 @@ export const CUSTOM_PROPERTIES_TYPES = {
STRING: 'String',
MARKDOWN: 'Markdown',
SQL_QUERY: 'Sql Query',
ENUM_WITH_DESCRIPTIONS: 'Enum With Descriptions',
};
export const FIELD_VALUES_CUSTOM_PROPERTIES = {

View File

@ -52,39 +52,6 @@ test.describe('Custom properties with custom property config', () => {
});
});
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',
() => {

View File

@ -17,8 +17,8 @@ import {
ENTITY_REFERENCE_PROPERTIES,
} from '../constant/customProperty';
import {
EntityTypeEndpoint,
ENTITY_PATH,
EntityTypeEndpoint,
} from '../support/entity/Entity.interface';
import { UserClass } from '../support/user/UserClass';
import { clickOutside, descriptionBox, uuid } from './common';
@ -44,7 +44,6 @@ export enum CustomPropertyTypeByName {
TIME_CP = 'time-cp',
DATE_CP = 'date-cp',
DATE_TIME_CP = 'dateTime-cp',
ENUM_WITH_DESCRIPTION = 'enumWithDescriptions',
}
export interface CustomProperty {
@ -123,15 +122,6 @@ export const setValueForProperty = async (data: {
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);
@ -251,18 +241,6 @@ export const validateValueForProperty = async (data: {
);
} else if (propertyType === 'sqlQuery') {
await expect(container.locator('.CodeMirror-scroll')).toContainText(value);
} else if (propertyType === 'enumWithDescriptions') {
await expect(
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',
@ -316,11 +294,6 @@ export const getPropertyValues = (
value: 'small',
newValue: 'medium',
};
case 'enumWithDescriptions':
return {
value: 'enumWithDescription1',
newValue: 'enumWithDescription2',
};
case 'sqlQuery':
return {
value: 'Select * from table',
@ -447,25 +420,6 @@ 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: {

View File

@ -11,7 +11,6 @@
* limitations under the License.
*/
import { expect, Page } from '@playwright/test';
import { CUSTOM_PROPERTIES_ENTITIES } from '../constant/customProperty';
import {
CUSTOM_PROPERTIES_TYPES,
FIELD_VALUES_CUSTOM_PROPERTIES,
@ -174,32 +173,6 @@ const editGlossaryCustomProperty = async (
await page.getByTestId('inline-save-btn').click();
}
if (type === CUSTOM_PROPERTIES_TYPES.ENUM_WITH_DESCRIPTIONS) {
await page.getByTestId('enum-with-description-select').click();
await page.waitForSelector('.ant-select-dropdown', {
state: 'visible',
});
// await page
// .getByRole('option', {
// name: CUSTOM_PROPERTIES_ENTITIES.entity_glossaryTerm
// .enumWithDescriptionConfig.values[0].key,
// })
// .click();
await page
.locator('span')
.filter({
hasText:
CUSTOM_PROPERTIES_ENTITIES.entity_glossaryTerm
.enumWithDescriptionConfig.values[0].key,
})
.click();
await page.getByTestId('inline-save-btn').click();
}
};
export const fillCustomPropertyDetails = async (

View File

@ -11,14 +11,13 @@
* limitations under the License.
*/
import { Button, Modal, Typography } from 'antd';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import React, { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { AxiosError } from 'axios';
import { isObject } from 'lodash';
import { EntityType } from '../../../enums/entity.enum';
import { GlossaryTerm } from '../../../generated/entity/data/glossaryTerm';
import { EnumConfig, Type, ValueClass } from '../../../generated/entity/type';
import { Type } from '../../../generated/entity/type';
import { getTypeByFQN } from '../../../rest/metadataTypeAPI';
import {
convertCustomPropertyStringToEntityExtension,
@ -47,20 +46,6 @@ export const ModalWithCustomPropertyEditor = ({
useState<ExtensionDataProps>();
const [customPropertyTypes, setCustomPropertyTypes] = useState<Type>();
const enumWithDescriptionsKeyPairValues = useMemo(() => {
const valuesWithEnumKey: Record<string, ValueClass[]> = {};
customPropertyTypes?.customProperties?.forEach((property) => {
if (property.propertyType.name === 'enumWithDescriptions') {
valuesWithEnumKey[property.name] = (
property.customPropertyConfig?.config as EnumConfig
).values as ValueClass[];
}
});
return valuesWithEnumKey;
}, [customPropertyTypes]);
const fetchTypeDetail = async () => {
setIsLoading(true);
try {
@ -87,42 +72,8 @@ export const ModalWithCustomPropertyEditor = ({
setIsSaveLoading(false);
};
// EnumWithDescriptions values are change only contain keys,
// so we need to modify the extension data to include descriptions for them to display in the table
const modifyExtensionData = useCallback(
(extension: ExtensionDataProps) => {
const modifiedExtension = Object.entries(extension).reduce(
(acc, [key, value]) => {
if (enumWithDescriptionsKeyPairValues[key]) {
return {
...acc,
[key]: (value as string[] | ValueClass[]).map((item) => {
if (isObject(item)) {
return item;
}
return {
key: item,
description: enumWithDescriptionsKeyPairValues[key].find(
(val) => val.key === item
)?.description,
};
}),
};
}
return { ...acc, [key]: value };
},
{}
);
return modifiedExtension;
},
[enumWithDescriptionsKeyPairValues]
);
const onExtensionUpdate = async (data: GlossaryTerm) => {
setCustomPropertyValue(modifyExtensionData(data.extension));
setCustomPropertyValue(data.extension);
};
useEffect(() => {

View File

@ -11,11 +11,10 @@
* limitations under the License.
*/
import { PlusOutlined } from '@ant-design/icons';
import { Button, Col, Form, Input, Row } from 'antd';
import { Button, Col, Form, Row } from 'antd';
import { AxiosError } from 'axios';
import { t } from 'i18next';
import { isUndefined, map, omit, omitBy, startCase } from 'lodash';
import { isArray, isUndefined, map, omit, omitBy, startCase } from 'lodash';
import React, {
FocusEvent,
useCallback,
@ -24,13 +23,12 @@ 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,
TABLE_TYPE_CUSTOM_PROPERTY,
} from '../../../../constants/CustomProperty.constants';
import { GlobalSettingsMenuCategory } from '../../../../constants/GlobalSettings.constants';
import { CUSTOM_PROPERTY_NAME_REGEX } from '../../../../constants/regex.constants';
@ -41,7 +39,6 @@ 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,
@ -58,7 +55,6 @@ 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';
@ -112,7 +108,7 @@ const AddCustomProperty = () => {
hasFormatConfig,
hasEntityReferenceConfig,
watchedOption,
hasEnumWithDescriptionConfig,
hasTableTypeConfig,
} = useMemo(() => {
const watchedOption = propertyTypeOptions.find(
(option) => option.value === watchedPropertyType
@ -121,8 +117,7 @@ const AddCustomProperty = () => {
const hasEnumConfig = watchedOptionKey === 'enum';
const hasEnumWithDescriptionConfig =
watchedOptionKey === ENUM_WITH_DESCRIPTION;
const hasTableTypeConfig = watchedOptionKey === TABLE_TYPE_CUSTOM_PROPERTY;
const hasFormatConfig =
PROPERTY_TYPES_WITH_FORMAT.includes(watchedOptionKey);
@ -135,7 +130,7 @@ const AddCustomProperty = () => {
hasFormatConfig,
hasEntityReferenceConfig,
watchedOption,
hasEnumWithDescriptionConfig,
hasTableTypeConfig,
};
}, [watchedPropertyType, propertyTypeOptions]);
@ -176,7 +171,8 @@ const AddCustomProperty = () => {
formatConfig: string;
entityReferenceConfig: string[];
multiSelect?: boolean;
enumWithDescriptionsConfig?: EnumWithDescriptionsConfig['values'];
rowCount: number;
columns: string[];
}
) => {
if (isUndefined(typeDetail)) {
@ -208,11 +204,11 @@ const AddCustomProperty = () => {
};
}
if (hasEnumWithDescriptionConfig) {
if (hasTableTypeConfig) {
customPropertyConfig = {
config: {
multiSelect: Boolean(data?.multiSelect),
values: data.enumWithDescriptionsConfig,
columns: data.columns,
rowCount: data.rowCount ?? 10,
},
};
}
@ -224,7 +220,8 @@ const AddCustomProperty = () => {
'formatConfig',
'entityReferenceConfig',
'enumConfig',
'enumWithDescriptionsConfig',
'rowCount',
'columns',
]),
propertyType: {
id: data.propertyType,
@ -388,6 +385,71 @@ const AddCustomProperty = () => {
},
};
const tableTypePropertyConfig: FieldProp[] = [
{
name: 'columns',
required: true,
label: t('label.column-plural'),
id: 'root/columns',
type: FieldTypes.SELECT,
props: {
'data-testid': 'columns',
mode: 'tags',
placeholder: t('label.column-plural'),
},
rules: [
{
required: true,
validator: async (_, value) => {
if (isArray(value)) {
if (value.length > 3) {
return Promise.reject(
t('message.maximum-count-allowed', {
count: 3,
label: t('label.column-plural'),
})
);
}
return Promise.resolve();
} else {
return Promise.reject(
t('label.field-required', {
field: t('label.column-plural'),
})
);
}
},
},
],
},
{
name: 'rowCount',
label: t('label.row-count'),
type: FieldTypes.NUMBER,
required: false,
id: 'root/rowCount',
props: {
'data-testid': 'rowCount',
size: 'default',
style: { width: '100%' },
placeholder: t('label.row-count'),
},
rules: [
{
min: 1,
type: 'number',
max: 10,
message: t('message.entity-size-in-between', {
entity: t('label.row-count'),
min: 1,
max: 10,
}),
},
],
},
];
const firstPanelChildren = (
<div className="max-width-md w-9/10 service-form-container">
<TitleBreadcrumb titleLinks={slashedBreadcrumb} />
@ -395,6 +457,9 @@ const AddCustomProperty = () => {
className="m-t-md"
data-testid="custom-property-form"
form={form}
initialValues={{
rowCount: 10,
}}
layout="vertical"
onFinish={handleSubmit}
onFocus={handleFieldFocus}>
@ -415,94 +480,8 @@ const AddCustomProperty = () => {
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>
{hasTableTypeConfig && generateFormFields(tableTypePropertyConfig)}
{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>

View File

@ -10,19 +10,17 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Button, Space, Tag, Tooltip, Typography } from 'antd';
import { Button, Space, 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';
@ -70,9 +68,7 @@ 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' ||
selectedProperty.propertyType.name === ENUM_WITH_DESCRIPTION;
const isEnumType = selectedProperty.propertyType.name === 'enum';
return {
...property,
@ -144,36 +140,24 @@ 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']) ?? [];
if (config?.columns) {
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>
<div className="w-full d-flex gap-2 flex-column">
<Typography.Text>
{t('label.multi-select')}:{' '}
{config?.multiSelect ? t('label.yes') : t('label.no')}
<span className="font-medium">{`${t(
'label.column-plural'
)}:`}</span>
<ul className="m-b-0">
{config.columns.map((column) => (
<li key={column}>{column}</li>
))}
</ul>
</Typography.Text>
<Typography.Text>
<span className="font-medium">{`${t(
'label.row-count'
)}: `}</span>
{config?.rowCount ?? 10}
</Typography.Text>
</div>
);

View File

@ -10,30 +10,17 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
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 { Form, Modal, Typography } from 'antd';
import { 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 {
Config,
CustomProperty,
EnumConfig,
ValueClass,
} from '../../../../generated/type/customProperty';
import {
FieldProp,
@ -41,11 +28,10 @@ import {
FormItemLayout,
} from '../../../../interface/FormUtils.interface';
import { generateFormFields } from '../../../../utils/formUtils';
import RichTextEditor from '../../../common/RichTextEditor/RichTextEditor';
export interface FormData {
description: string;
customPropertyConfig: string[] | ValueClass[];
customPropertyConfig: string[];
multiSelect?: boolean;
}
@ -72,21 +58,15 @@ const EditCustomPropertyModal: FC<EditCustomPropertyModalProps> = ({
setIsSaving(false);
};
const {
hasEnumConfig,
hasEntityReferenceConfig,
hasEnumWithDescriptionConfig,
} = useMemo(() => {
const { hasEnumConfig, hasEntityReferenceConfig } = 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]);
@ -116,7 +96,7 @@ const EditCustomPropertyModal: FC<EditCustomPropertyModalProps> = ({
placeholder: t('label.enum-value-plural'),
onChange: (value: string[]) => {
const enumConfig = customProperty.customPropertyConfig
?.config as EnumConfig;
?.config as Config;
const updatedValues = uniq([...value, ...(enumConfig?.values ?? [])]);
form.setFieldsValue({ customPropertyConfig: updatedValues });
},
@ -175,9 +155,8 @@ const EditCustomPropertyModal: FC<EditCustomPropertyModalProps> = ({
};
const initialValues = useMemo(() => {
if (hasEnumConfig || hasEnumWithDescriptionConfig) {
const enumConfig = customProperty.customPropertyConfig
?.config as EnumConfig;
if (hasEnumConfig) {
const enumConfig = customProperty.customPropertyConfig?.config as Config;
return {
description: customProperty.description,
@ -190,7 +169,7 @@ const EditCustomPropertyModal: FC<EditCustomPropertyModalProps> = ({
description: customProperty.description,
customPropertyConfig: customProperty.customPropertyConfig?.config,
};
}, [customProperty, hasEnumConfig, hasEnumWithDescriptionConfig]);
}, [customProperty, hasEnumConfig]);
const note = (
<Typography.Text
@ -244,125 +223,6 @@ 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])}

View File

@ -77,10 +77,16 @@ export type TimeIntervalType = {
end: number;
};
export type TableTypePropertyValueType = {
columns: string[];
rows: Record<string, string>[];
};
export type PropertyValueType =
| string
| number
| string[]
| EntityReference
| EntityReference[]
| TimeIntervalType;
| TimeIntervalType
| TableTypePropertyValueType;

View File

@ -178,12 +178,20 @@ export const CustomPropertyTable = <T extends ExtentionEntitiesKeys>({
maxDataCap,
]);
const dataSource = useMemo(() => {
const { dataSource, dataSourceColumns } = useMemo(() => {
const customProperties = entityTypeDetail?.customProperties ?? [];
return Array.isArray(customProperties)
const dataSource = Array.isArray(customProperties)
? customProperties.slice(0, maxDataCap)
: [];
// Split dataSource into three equal parts
const columnCount = 3;
const columns = Array.from({ length: columnCount }, (_, i) =>
dataSource.filter((_, index) => index % columnCount === i)
);
return { dataSource, dataSourceColumns: columns };
}, [maxDataCap, entityTypeDetail?.customProperties]);
useEffect(() => {
@ -286,17 +294,21 @@ export const CustomPropertyTable = <T extends ExtentionEntitiesKeys>({
</>
) : (
<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}
/>
{dataSourceColumns.map((columns, colIndex) => (
<Col key={colIndex} span={8}>
{columns.map((record) => (
<div key={record.name} style={{ marginBottom: '16px' }}>
<PropertyValue
extension={extensionObject.extensionObject}
hasEditPermissions={hasEditAccess}
isRenderedInRightPanel={isRenderedInRightPanel}
isVersionView={isVersionView}
property={record}
versionDataKeys={extensionObject.addedKeysList}
onExtensionUpdate={onExtensionUpdate}
/>
</div>
))}
</Col>
))}
</Row>

View File

@ -26,7 +26,6 @@ import {
Tooltip,
Typography,
} from 'antd';
import { ColumnsType } from 'antd/lib/table';
import { AxiosError } from 'axios';
import classNames from 'classnames';
import { t } from 'i18next';
@ -60,12 +59,12 @@ import {
ICON_DIMENSION,
VALIDATION_MESSAGES,
} from '../../../constants/constants';
import { ENUM_WITH_DESCRIPTION } from '../../../constants/CustomProperty.constants';
import { TABLE_TYPE_CUSTOM_PROPERTY } 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, ValueClass } from '../../../generated/type/customProperty';
import { Config } from '../../../generated/type/customProperty';
import { calculateInterval } from '../../../utils/date-time/DateTimeUtils';
import entityUtilClassBase from '../../../utils/EntityUtilClassBase';
import { getEntityName } from '../../../utils/EntityUtils';
@ -78,7 +77,6 @@ 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,
@ -86,6 +84,8 @@ import {
} from './CustomPropertyTable.interface';
import './property-value.less';
import { PropertyInput } from './PropertyInput';
import EditTableTypePropertyModal from './TableTypeProperty/EditTableTypePropertyModal';
import TableTypePropertyView from './TableTypeProperty/TableTypePropertyView';
export const PropertyValue: FC<PropertyValueProps> = ({
isVersionView,
@ -96,9 +96,10 @@ export const PropertyValue: FC<PropertyValueProps> = ({
property,
isRenderedInRightPanel = false,
}) => {
const { propertyName, propertyType, value } = useMemo(() => {
const { propertyName, propertyType, value, isTableType } = useMemo(() => {
const propertyName = property.name;
const propertyType = property.propertyType;
const isTableType = propertyType.name === TABLE_TYPE_CUSTOM_PROPERTY;
const value = extension?.[propertyName];
@ -106,13 +107,15 @@ export const PropertyValue: FC<PropertyValueProps> = ({
propertyName,
propertyType,
value,
isTableType,
};
}, [property, extension]);
const [showInput, setShowInput] = useState<boolean>(false);
const [isLoading, setIsLoading] = useState<boolean>(false);
const [isExpanded, setIsExpanded] = useState(false);
// expand the property value by default if it is a "table-type" custom property
const [isExpanded, setIsExpanded] = useState(isTableType);
const [isOverflowing, setIsOverflowing] = useState(false);
const contentRef = useRef<HTMLDivElement>(null);
@ -135,16 +138,14 @@ export const PropertyValue: FC<PropertyValueProps> = ({
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 || isEnumWithDescription
? (enumValue as string[]).filter(Boolean)
: updatedValue;
const propertyValue = isEnum
? (enumValue as string[]).filter(Boolean)
: updatedValue;
try {
// Omit undefined and empty values
@ -227,7 +228,7 @@ export const PropertyValue: FC<PropertyValueProps> = ({
}
case 'enum': {
const enumConfig = property.customPropertyConfig?.config as EnumConfig;
const enumConfig = property.customPropertyConfig?.config as Config;
const isMultiSelect = Boolean(enumConfig?.multiSelect);
@ -272,60 +273,6 @@ 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'
@ -748,6 +695,31 @@ export const PropertyValue: FC<PropertyValueProps> = ({
);
}
case TABLE_TYPE_CUSTOM_PROPERTY: {
const config = property.customPropertyConfig?.config as Config;
const columns = config?.columns ?? [];
const rows = value?.rows ?? [];
return (
<>
{showInput && (
<TableTypePropertyView columns={columns} rows={rows} />
)}
<EditTableTypePropertyModal
columns={columns}
isUpdating={isLoading}
isVisible={showInput}
maxRowCount={config?.rowCount ?? 10}
property={property}
rows={value?.rows ?? []}
onCancel={onHideInput}
onSave={onInputSave}
/>
</>
);
}
default:
return null;
}
@ -791,42 +763,6 @@ export const PropertyValue: FC<PropertyValueProps> = ({
</>
);
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
@ -981,6 +917,14 @@ export const PropertyValue: FC<PropertyValueProps> = ({
);
}
case TABLE_TYPE_CUSTOM_PROPERTY: {
const columns =
(property.customPropertyConfig?.config as Config)?.columns ?? [];
const rows = value?.rows ?? [];
return <TableTypePropertyView columns={columns} rows={rows} />;
}
case 'string':
case 'integer':
case 'number':
@ -1004,7 +948,8 @@ export const PropertyValue: FC<PropertyValueProps> = ({
const getValueElement = () => {
const propertyValue = getPropertyValue();
return !isUndefined(value) ? (
// if value is not undefined or property is a table type(at least show the columns), return the property value
return !isUndefined(value) || isTableType ? (
propertyValue
) : (
<span className="text-grey-muted" data-testid="no-data">
@ -1089,7 +1034,7 @@ export const PropertyValue: FC<PropertyValueProps> = ({
ref={contentRef}
style={{
height: containerStyleFlag ? 'auto' : '30px',
overflow: containerStyleFlag ? 'visible' : 'hidden',
overflow: 'hidden',
}}>
{showInput ? getPropertyInput() : getValueElement()}
</div>

View File

@ -0,0 +1,236 @@
/*
* Copyright 2024 Collate.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import ReactDataGrid from '@inovua/reactdatagrid-community';
import '@inovua/reactdatagrid-community/index.css';
import { TypeComputedProps } from '@inovua/reactdatagrid-community/types';
import { Button, Modal, Tooltip, Typography } from 'antd';
import { isEmpty, omit } from 'lodash';
import React, { FC, MutableRefObject, useCallback, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { CustomProperty } from '../../../../generated/type/customProperty';
import { TableTypePropertyValueType } from '../CustomPropertyTable.interface';
import './edit-table-type-property.less';
import TableTypePropertyView from './TableTypePropertyView';
interface EditTableTypePropertyModalProps {
isVisible: boolean;
isUpdating: boolean;
property: CustomProperty;
columns: string[];
rows: Record<string, string>[];
maxRowCount: number;
onCancel: () => void;
onSave: (data: TableTypePropertyValueType) => Promise<void>;
}
let inEdit = false;
const EditTableTypePropertyModal: FC<EditTableTypePropertyModalProps> = ({
isVisible,
isUpdating,
property,
columns,
rows,
maxRowCount,
onCancel,
onSave,
}) => {
const { t } = useTranslation();
const [dataSource, setDataSource] = useState<
TableTypePropertyValueType['rows']
>(() => rows.map((row, index) => ({ ...row, id: index + '' })));
const [gridRef, setGridRef] = useState<
MutableRefObject<TypeComputedProps | null>
>({ current: null });
const filterColumns = columns.map((column) => ({
name: column,
header: column,
defaultFlex: 1,
sortable: false,
minWidth: 180,
}));
const onEditComplete = useCallback(
({ value, columnId, rowId }) => {
const data = [...dataSource];
data[rowId][columnId] = value;
setDataSource(data);
},
[dataSource]
);
const onEditStart = () => {
inEdit = true;
};
const onEditStop = () => {
requestAnimationFrame(() => {
inEdit = false;
gridRef.current?.focus();
});
};
const onKeyDown = (event: KeyboardEvent) => {
if (inEdit) {
if (event.key === 'Escape') {
const [rowIndex, colIndex] = gridRef.current?.computedActiveCell ?? [
0, 0,
];
const column = gridRef.current?.getColumnBy(colIndex);
gridRef.current?.cancelEdit?.({
rowIndex,
columnId: column?.name ?? '',
});
}
return;
}
const grid = gridRef.current;
if (!grid) {
return;
}
let [rowIndex, colIndex] = grid.computedActiveCell ?? [0, 0];
if (event.key === ' ' || event.key === 'Enter') {
const column = grid.getColumnBy(colIndex);
grid.startEdit?.({ columnId: column.name ?? '', rowIndex });
event.preventDefault();
return;
}
if (event.key !== 'Tab') {
return;
}
event.preventDefault();
event.stopPropagation();
const direction = event.shiftKey ? -1 : 1;
const columns = grid.visibleColumns;
const rowCount = grid.count;
colIndex += direction;
if (colIndex === -1) {
colIndex = columns.length - 1;
rowIndex -= 1;
}
if (colIndex === columns.length) {
rowIndex += 1;
colIndex = 0;
}
if (rowIndex < 0 || rowIndex === rowCount) {
return;
}
grid?.setActiveCell([rowIndex, colIndex]);
};
const handleAddRow = useCallback(() => {
setDataSource((data) => {
setTimeout(() => {
gridRef.current?.scrollToId(data.length + '');
gridRef.current?.focus();
}, 1);
return [...data, { id: data.length + '' }];
});
}, [gridRef]);
const handleUpdate = useCallback(async () => {
const modifiedRows = dataSource
.map((row) => omit(row, 'id'))
// if the row is empty, filter it out
.filter((row) => !isEmpty(row) && Object.values(row).some(Boolean));
await onSave({ rows: modifiedRows, columns });
}, [onSave, dataSource, columns]);
return (
<Modal
centered
destroyOnClose
closable={false}
data-testid="edit-table-type-property-modal"
footer={
<div className="d-flex justify-between">
<Tooltip
title={
dataSource.length === maxRowCount
? t('message.maximum-count-allowed', {
count: maxRowCount,
label: t('label.row-plural'),
})
: t('label.add-entity', { entity: t('label.row') })
}>
<Button
disabled={dataSource.length === maxRowCount || isUpdating}
type="primary"
onClick={handleAddRow}>
{t('label.add-entity', { entity: t('label.row') })}
</Button>
</Tooltip>
<div className="d-flex gap-2">
<Button disabled={isUpdating} onClick={onCancel}>
{t('label.cancel')}
</Button>
<Button
disabled={isUpdating}
loading={isUpdating}
type="primary"
onClick={handleUpdate}>
{t('label.update')}
</Button>
</div>
</div>
}
maskClosable={false}
open={isVisible}
title={
<Typography.Text>
{t('label.edit-entity-name', {
entityType: t('label.property'),
entityName: property.name,
})}
</Typography.Text>
}
width={800}>
{isEmpty(dataSource) ? (
<TableTypePropertyView isInModal columns={columns} rows={rows} />
) : (
<ReactDataGrid
editable
className="edit-table-type-property"
columns={filterColumns}
dataSource={dataSource}
handle={setGridRef}
idProperty="id"
minRowHeight={30}
showZebraRows={false}
style={{ height: '180px' }}
onEditComplete={onEditComplete}
onEditStart={onEditStart}
onEditStop={onEditStop}
onKeyDown={onKeyDown}
/>
)}
</Modal>
);
};
export default EditTableTypePropertyModal;

View File

@ -0,0 +1,65 @@
/*
* Copyright 2024 Collate.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { isArray } from 'lodash';
import React, { FC } from 'react';
import { useTranslation } from 'react-i18next';
import { NO_DATA_PLACEHOLDER } from '../../../../constants/constants';
import Table from '../../Table/Table';
import './table-type-property-view.less';
interface TableTypePropertyViewProps {
columns: string[];
rows: Record<string, string>[];
isInModal?: boolean;
}
const TableTypePropertyView: FC<TableTypePropertyViewProps> = ({
columns,
rows,
}) => {
const { t } = useTranslation();
if (!isArray(columns) || !isArray(rows)) {
return (
<span className="text-grey-muted" data-testid="invalid-data">
{t('label.field-invalid', {
field: `${t('label.column-plural')} or ${t('label.row-plural')}`,
})}
</span>
);
}
const tableColumns = columns.map((column: string) => ({
title: column,
dataIndex: column,
key: column,
render: (text: string) => text ?? NO_DATA_PLACEHOLDER,
}));
return (
<Table
bordered
resizableColumns
className="w-full table-type-custom-property"
columns={tableColumns}
data-testid="table-type-property-value"
dataSource={rows}
pagination={false}
rowKey="name"
scroll={{ x: true }}
size="small"
/>
);
};
export default TableTypePropertyView;

View File

@ -0,0 +1,46 @@
/*
* Copyright 2024 Collate.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
@import url('../../../../styles/variables.less');
.edit-table-type-property {
.InovuaReactDataGrid__column-header:hover {
.InovuaReactDataGrid__column-header__menu-tool {
display: none;
}
}
.InovuaReactDataGrid__header {
border-top-right-radius: 6px;
border-top-left-radius: 6px;
font-weight: 500;
color: @grey-4;
text-transform: uppercase;
background: @grey-1;
.InovuaReactDataGrid__column-header__content {
font-weight: 500;
}
}
.InovuaReactDataGrid__virtual-list {
border-bottom-left-radius: 6px;
border-bottom-right-radius: 6px;
}
.InovuaReactDataGrid__row-cell-wrap {
color: @text-color;
}
border: 1px solid @border-color;
border-radius: 6px;
}

View File

@ -0,0 +1,26 @@
@border-right-color: #0000000f;
.table-type-custom-property.ant-table-wrapper {
.ant-table-bordered {
.ant-table-container {
.ant-table-content > table {
tbody > tr {
td {
border-right: 1px solid @border-right-color;
&:last-child {
border-right: none;
}
}
}
thead > tr {
th {
border-right: 1px solid @border-right-color;
&:last-child {
border-right: none;
}
}
}
}
}
}
}

View File

@ -86,3 +86,9 @@
font-weight: 500;
line-height: 18px;
}
.property-description {
.markdown-parser .toastui-editor-contents {
font-size: 13px;
}
}

View File

@ -127,4 +127,4 @@ export const SUPPORTED_FORMAT_MAP = {
'time-cp': SUPPORTED_TIME_FORMATS,
};
export const ENUM_WITH_DESCRIPTION = 'enumWithDescriptions';
export const TABLE_TYPE_CUSTOM_PROPERTY = 'table-cp';

View File

@ -983,6 +983,7 @@
"role-name": "Rollenname",
"role-plural": "Rollen",
"row": "Zeile",
"row-count": "Row Count",
"row-count-lowercase": "Anzahl der Zeilen",
"row-plural": "Zeilen",
"rtl-ltr-direction": "RTL/LTR direction",
@ -1591,6 +1592,7 @@
"mark-deleted-table-message": "Dies ist eine optionale Konfiguration zur Aktivierung des sanften Löschens von Tabellen. Wenn diese Option aktiviert ist, werden nur Tabellen, die aus der Quelle gelöscht wurden, sanft gelöscht, und dies gilt ausschließlich für das Schema, das gerade über die Pipeline erfasst wird. Alle zugehörigen Entitäten wie Test-Suiten oder Verbindungsdaten, die mit diesen Tabellen verknüpft waren, werden ebenfalls gelöscht.",
"markdown-editor-placeholder": "Verwenden Sie @Erwähnen, um einen Benutzer oder ein Team zu kennzeichnen.\nVerwenden Sie #Erwähnen, um eine Datenressource zu kennzeichnen.",
"marketplace-verify-msg": "OpenMetadata has verified that the publisher controls the domain and meets other requirements.",
"maximum-count-allowed": "Maximum {{count}} {{label}} are allowed",
"maximum-value-error": "The maximum value should be greater than the minimum value.",
"member-description": "Streamline access to users and teams in OpenMetadata.",
"mentioned-you-on-the-lowercase": "hat dich auf der",

View File

@ -983,6 +983,7 @@
"role-name": "Role Name",
"role-plural": "Roles",
"row": "Row",
"row-count": "Row Count",
"row-count-lowercase": "row count",
"row-plural": "Rows",
"rtl-ltr-direction": "RTL/LTR direction",
@ -1591,6 +1592,7 @@
"mark-deleted-table-message": "This is an optional configuration for enabling soft deletion of tables. When this option is enabled, only tables that have been deleted from the source will be soft deleted, and this will apply solely to the schema that is currently being ingested via the pipeline. Any related entities such as test suites or lineage information that were associated with those tables will also be deleted.",
"markdown-editor-placeholder": "Use @mention to tag a user or a team.\nUse #mention to tag a data asset.",
"marketplace-verify-msg": "OpenMetadata has verified that the publisher controls the domain and meets other requirements.",
"maximum-count-allowed": "Maximum {{count}} {{label}} are allowed",
"maximum-value-error": "The maximum value should be greater than the minimum value.",
"member-description": "Streamline access to users and teams in OpenMetadata.",
"mentioned-you-on-the-lowercase": "mentioned you on the",

View File

@ -983,6 +983,7 @@
"role-name": "Nombre del Rol",
"role-plural": "Roles",
"row": "Fila",
"row-count": "Row Count",
"row-count-lowercase": "número de filas",
"row-plural": "Filas",
"rtl-ltr-direction": "dirección RTL/LTR",
@ -1591,6 +1592,7 @@
"mark-deleted-table-message": "Esta es una configuración opcional para habilitar la eliminación suave de tablas. Cuando se habilita esta opción, sólo se eliminarán suavemente las tablas que se hayan eliminado de la fuente, y esto se aplicará únicamente al esquema que se esté ingiriendo actualmente a través de la canalización. Cualquier entidad relacionada, como tests o información de linaje que estuvieran asociados con esas tablas, también se eliminarán.",
"markdown-editor-placeholder": "Usa @mención para etiquetar a un usuario o equipo.\n Usa #mención para etiquetar un activo de datos.",
"marketplace-verify-msg": "OpenMetadata ha verificado que el editor controla el dominio y cumple con otros requisitos.",
"maximum-count-allowed": "Maximum {{count}} {{label}} are allowed",
"maximum-value-error": "The maximum value should be greater than the minimum value.",
"member-description": "Optimiza el acceso a usuarios y equipos en OpenMetadata.",
"mentioned-you-on-the-lowercase": "te mencionó en",

View File

@ -983,6 +983,7 @@
"role-name": "Nom du Rôle",
"role-plural": "Rôles",
"row": "Ligne",
"row-count": "Row Count",
"row-count-lowercase": "Nombre de Ligne",
"row-plural": "Lignes",
"rtl-ltr-direction": "RTL/LTR direction",
@ -1591,6 +1592,7 @@
"mark-deleted-table-message": "Il s'agit d'une configuration optionnelle permettant de permettre la suppression douce des tables. Lorsque cette option est activée, seules les tables supprimées de la source seront supprimées en douceur, et cela s'appliquera uniquement au schéma actuellement ingéré via le pipeline. Toute entité associée telle que les suites de tests ou les informations de lignée qui étaient associées à ces tables sera également supprimée.",
"markdown-editor-placeholder": "Utilisez @mention pour identifier un utilisateur ou une équipe.\nUtilisez #mention pour identifier un actif de données.",
"marketplace-verify-msg": "OpenMetadata has verified that the publisher controls the domain and meets other requirements.",
"maximum-count-allowed": "Maximum {{count}} {{label}} are allowed",
"maximum-value-error": "La valeur maximum doit être plus grande que la valeur minimum.",
"member-description": "Gérez les accès des utilisateurs et équipes à OpenMetadata.",
"mentioned-you-on-the-lowercase": "vous a mentionné dans",

View File

@ -983,6 +983,7 @@
"role-name": "שם התפקיד",
"role-plural": "תפקידים",
"row": "שורה",
"row-count": "Row Count",
"row-count-lowercase": "מספר שורות",
"row-plural": "שורות",
"rtl-ltr-direction": "RTL/LTR direction",
@ -1591,6 +1592,7 @@
"mark-deleted-table-message": "זו הגדרת אופציונלית לאפשר מחיקה רכה של טבלאות. כאשר האפשרות מופעלת, רק טבלאות שנמחקו ממקור הנתונים יימחקו רכה, וזה יחול באופן בלעדי על הסכמה הנמצאת כעת בתהליך קליטת הנתונים דרך הצנרת נתונים. יימחקו גם יישויות קשורות כמו סוויטי ניסויים או מידע של שקפיות המשוייכים לטבלאות אלו.",
"markdown-editor-placeholder": "השתמש ב-@תייג לתייג משתמש או צוות.\nהשתמש ב-#תייג לתייג נכס נתונים.",
"marketplace-verify-msg": "OpenMetadata אישרה שהמוציא לפועל שולט בדומיין ועומד בדרישות האחרות.",
"maximum-count-allowed": "Maximum {{count}} {{label}} are allowed",
"maximum-value-error": "The maximum value should be greater than the minimum value.",
"member-description": "Streamline access to users and teams in OpenMetadata.",
"mentioned-you-on-the-lowercase": "ציין אותך ב",

View File

@ -983,6 +983,7 @@
"role-name": "ロール名",
"role-plural": "ロール",
"row": "行",
"row-count": "Row Count",
"row-count-lowercase": "行数",
"row-plural": "行",
"rtl-ltr-direction": "RTL/LTR direction",
@ -1591,6 +1592,7 @@
"mark-deleted-table-message": "This is an optional configuration for enabling soft deletion of tables. When this option is enabled, only tables that have been deleted from the source will be soft deleted, and this will apply solely to the schema that is currently being ingested via the pipeline. Any related entities such as test suites or lineage information that were associated with those tables will also be deleted.",
"markdown-editor-placeholder": "@mention を使ってユーザやチームをタグ付けしましょう。\n#mentionを使うとデータアセットにタグ付けできます。",
"marketplace-verify-msg": "OpenMetadata has verified that the publisher controls the domain and meets other requirements.",
"maximum-count-allowed": "Maximum {{count}} {{label}} are allowed",
"maximum-value-error": "The maximum value should be greater than the minimum value.",
"member-description": "Streamline access to users and teams in OpenMetadata.",
"mentioned-you-on-the-lowercase": "mentioned you on the",

View File

@ -983,6 +983,7 @@
"role-name": "Rolnaam",
"role-plural": "Rollen",
"row": "Rij",
"row-count": "Row Count",
"row-count-lowercase": "aantal rijen",
"row-plural": "Rijen",
"rtl-ltr-direction": "RTL/LTR-richting",
@ -1591,6 +1592,7 @@
"mark-deleted-table-message": "Dit is een optionele configuratie voor het inschakelen van zachte verwijdering van tabellen. Wanneer deze optie is ingeschakeld, worden alleen tabellen die zijn verwijderd uit de bron zacht verwijderd, en dit is uitsluitend van toepassing op het schema dat momenteel wordt ingenomen via de pipeline. Elke gerelateerde entiteit, zoals testsuites of lineage-informatie, die met die tabellen was geassocieerd, wordt ook verwijderd.",
"markdown-editor-placeholder": "Gebruik @mention om een gebruiker of een team te taggen.\nGebruik #mention om een data-asset te taggen.",
"marketplace-verify-msg": "OpenMetadata heeft geverifieerd dat de publisher het domein beheert en aan andere eisen voldoet.",
"maximum-count-allowed": "Maximum {{count}} {{label}} are allowed",
"maximum-value-error": "The maximum value should be greater than the minimum value.",
"member-description": "Stroomlijn toegang voor gebruikers en teams in OpenMetadata.",
"mentioned-you-on-the-lowercase": "noemde je op de",

View File

@ -983,6 +983,7 @@
"role-name": "Nome da Função",
"role-plural": "Funções",
"row": "Linha",
"row-count": "Row Count",
"row-count-lowercase": "contagem de linhas",
"row-plural": "Linhas",
"rtl-ltr-direction": "Direção RTL/LTR",
@ -1591,6 +1592,7 @@
"mark-deleted-table-message": "Esta é uma configuração opcional para habilitar a exclusão suave de tabelas. Quando esta opção está habilitada, apenas as tabelas que foram excluídas da fonte serão excluídas suavemente, e isso se aplicará somente ao esquema que está sendo ingerido atualmente pelo pipeline. Quaisquer entidades relacionadas, como conjuntos de teste ou informações de linhagem que estavam associadas a essas tabelas, também serão excluídas.",
"markdown-editor-placeholder": "Use @mention para marcar um usuário ou uma equipe.\nUse #mention para marcar um ativo de dados.",
"marketplace-verify-msg": "O OpenMetadata verificou que o editor controla o domínio e atende a outros requisitos.",
"maximum-count-allowed": "Maximum {{count}} {{label}} are allowed",
"maximum-value-error": "The maximum value should be greater than the minimum value.",
"member-description": "Streamline access to users and teams in OpenMetadata.",
"mentioned-you-on-the-lowercase": "mencionou você no",

View File

@ -983,6 +983,7 @@
"role-name": "Наименование роли",
"role-plural": "Роли",
"row": "Строка",
"row-count": "Row Count",
"row-count-lowercase": "количество строк",
"row-plural": "Строки",
"rtl-ltr-direction": "RTL/LTR direction",
@ -1591,6 +1592,7 @@
"mark-deleted-table-message": "Это необязательная конфигурация для включения обратимого удаления таблиц. Если этот параметр включен, обратимому удалению подлежат только таблицы, которые были удалены из источника, и это будет применяться исключительно к схеме, которая в данный момент загружается через конвейер. Любые связанные объекты, такие как наборы тестов или информация о происхождении, которые были связаны с этими таблицами, также будут удалены.",
"markdown-editor-placeholder": "Используйте @упоминание, чтобы отметить пользователя или команду.\nИспользуйте #упоминание, чтобы пометить объект данных.",
"marketplace-verify-msg": "OpenMetadata has verified that the publisher controls the domain and meets other requirements.",
"maximum-count-allowed": "Maximum {{count}} {{label}} are allowed",
"maximum-value-error": "The maximum value should be greater than the minimum value.",
"member-description": "Streamline access to users and teams in OpenMetadata.",
"mentioned-you-on-the-lowercase": "упоминул вас в",

View File

@ -983,6 +983,7 @@
"role-name": "角色名",
"role-plural": "角色",
"row": "行",
"row-count": "Row Count",
"row-count-lowercase": "行计数",
"row-plural": "行",
"rtl-ltr-direction": "RTL/LTR direction",
@ -1591,6 +1592,7 @@
"mark-deleted-table-message": "可选配置, 用于启用数据表的软删除。只有在数据源中被删除的表才会被软删除, 而且仅会作用到当前被工作流提取进来的 Schema(模式)。所有这些数据表的关联实体都将被删除, 例如测试集和血缘信息等。",
"markdown-editor-placeholder": "使用 @提及 标记用户或团队。\n使用 #提及 标记数据资产",
"marketplace-verify-msg": "OpenMetadata 已核实发布者控制该域并符合其他要求",
"maximum-count-allowed": "Maximum {{count}} {{label}} are allowed",
"maximum-value-error": "最大值应大于最小值",
"member-description": "简化对 OpenMetadata 用户和团队的访问",
"mentioned-you-on-the-lowercase": "在以下地方提到了您",

View File

@ -19,15 +19,12 @@ import {
ExtensionDataProps,
ExtensionDataTypes,
} from '../../components/Modals/ModalWithCustomProperty/ModalWithMarkdownEditor.interface';
import { NO_DATA_PLACEHOLDER } from '../../constants/constants';
import { SEMICOLON_SPLITTER } from '../../constants/regex.constants';
import { EntityType } from '../../enums/entity.enum';
import {
CustomProperty,
EntityReference,
EnumConfig,
Type,
ValueClass,
} from '../../generated/entity/type';
import { Status } from '../../generated/type/csvImportResult';
import { removeOuterEscapes } from '../CommonUtils';
@ -193,21 +190,6 @@ const convertCustomPropertyStringToValueExtensionBasedOnType = (
}
}
case 'enumWithDescriptions': {
const propertyEnumValues =
((customProperty?.customPropertyConfig?.config as EnumConfig)
.values as ValueClass[]) ?? [];
const keyAndValue: Record<string, ValueClass> = {};
propertyEnumValues.forEach((cp) => (keyAndValue[cp.key] = cp));
return value.split('|').map((item) => ({
key: item,
description: keyAndValue[item].description ?? NO_DATA_PLACEHOLDER,
}));
}
case 'timeInterval': {
const [start, end] = value.split(':');
@ -246,11 +228,6 @@ const convertCustomPropertyValueExtensionToStringBasedOnType = (
case 'enum':
return (value as unknown as string[]).map((item) => item).join('|');
case 'enumWithDescriptions':
return (value as unknown as ValueClass[])
.map((item) => item.key)
.join('|');
case 'timeInterval': {
const interval = value as { start: string; end: string };