diff --git a/api/gwtsrc/org/labkey/api/gwt/client/model/GWTDomain.java b/api/gwtsrc/org/labkey/api/gwt/client/model/GWTDomain.java index eecd41589ee..fc2e69896d7 100644 --- a/api/gwtsrc/org/labkey/api/gwt/client/model/GWTDomain.java +++ b/api/gwtsrc/org/labkey/api/gwt/client/model/GWTDomain.java @@ -42,6 +42,7 @@ public class GWTDomain implements IsSer @Getter @Setter private boolean allowAttachmentProperties; @Getter @Setter private boolean allowFlagProperties; @Getter @Setter private boolean allowTextChoiceProperties; + @Getter @Setter private boolean allowMultiChoiceProperties; @Getter @Setter private boolean allowSampleSubjectProperties; @Getter @Setter private boolean allowTimepointProperties; @Getter @Setter private boolean allowUniqueConstraintProperties; @@ -90,6 +91,7 @@ public GWTDomain(GWTDomain src) this.allowAttachmentProperties = src.allowAttachmentProperties; this.allowFlagProperties = src.allowFlagProperties; this.allowTextChoiceProperties = src.allowTextChoiceProperties; + this.allowMultiChoiceProperties = src.allowMultiChoiceProperties; this.allowSampleSubjectProperties = src.allowSampleSubjectProperties; this.allowTimepointProperties = src.allowTimepointProperties; this.allowUniqueConstraintProperties = src.allowUniqueConstraintProperties; diff --git a/api/schemas/queryCustomView.xsd b/api/schemas/queryCustomView.xsd index b0abe2a107d..3c0ce4e96ec 100644 --- a/api/schemas/queryCustomView.xsd +++ b/api/schemas/queryCustomView.xsd @@ -93,6 +93,13 @@ + + + + + + + diff --git a/api/src/org/labkey/api/assay/AbstractAssayTsvDataHandler.java b/api/src/org/labkey/api/assay/AbstractAssayTsvDataHandler.java index f2432753ab3..ea3243e7a5f 100644 --- a/api/src/org/labkey/api/assay/AbstractAssayTsvDataHandler.java +++ b/api/src/org/labkey/api/assay/AbstractAssayTsvDataHandler.java @@ -38,6 +38,7 @@ import org.labkey.api.data.ExpDataFileConverter; import org.labkey.api.data.ForeignKey; import org.labkey.api.data.ImportAliasable; +import org.labkey.api.data.MultiChoice; import org.labkey.api.data.MvUtil; import org.labkey.api.data.RemapCache; import org.labkey.api.data.RuntimeSQLException; @@ -863,7 +864,11 @@ else if (entry.getKey().equalsIgnoreCase(ProvenanceService.PROVENANCE_INPUT_PROP for (DomainProperty pd : columns) { Object o = map.get(pd.getName()); - if (o instanceof String) + if (PropertyType.MULTI_CHOICE == pd.getPropertyType()) + { + o = MultiChoice.Converter.getInstance().convert(MultiChoice.Array.class, o); + } + else if (o instanceof String) { o = StringUtils.trimToNull((String) o); map.put(pd.getName(), o); diff --git a/api/src/org/labkey/api/data/CompareType.java b/api/src/org/labkey/api/data/CompareType.java index 8d20eb3a243..d9165ae67da 100644 --- a/api/src/org/labkey/api/data/CompareType.java +++ b/api/src/org/labkey/api/data/CompareType.java @@ -808,6 +808,341 @@ public QClause createFilterClause(@NotNull FieldKey fieldKey, Object value) } }; + protected Collection getCollectionParam(Object value) + { + if (value instanceof Collection) + { + return (Collection)value; + } + else + { + List values = new ArrayList<>(); + if (value != null) + values.addAll(parseParams(value, getValueSeparator(), isNewLineSeparatorAllowed())); + return values; + } + } + + /** TODO: + * + * + */ + public static final CompareType ARRAY_CONTAINS_ALL = new CompareType("Contains All", "arraycontainsall", "ARRAYCONTAINSALL", true, null, OperatorType.ARRAYCONTAINSALL) + { + @Override + public ArrayContainsAllClause createFilterClause(@NotNull FieldKey fieldKey, Object value) + { + return new ArrayContainsAllClause(fieldKey, getCollectionParam(value)); + } + + @Override + public boolean meetsCriteria(ColumnRenderProperties col, Object value, Object[] filterValues) + { + throw new UnsupportedOperationException("Conditional formatting not yet supported for Multi Choices"); + } + + @Override + public String getValueSeparator() + { + return ArrayClause.ARRAY_VALUE_SEPARATOR; + } + }; + + public static final CompareType ARRAY_CONTAINS_ANY = new CompareType("Contains Any", "arraycontainsany", "ARRAYCONTAINSANY", true, null, OperatorType.ARRAYCONTAINSANY) + { + @Override + public ArrayContainsAnyClause createFilterClause(@NotNull FieldKey fieldKey, Object value) + { + return new ArrayContainsAnyClause(fieldKey, getCollectionParam(value)); + } + + @Override + public boolean meetsCriteria(ColumnRenderProperties col, Object value, Object[] filterValues) + { + throw new UnsupportedOperationException("Conditional formatting not yet supported for Multi Choices"); + } + + @Override + public String getValueSeparator() + { + return ArrayClause.ARRAY_VALUE_SEPARATOR; + } + }; + + public static final CompareType ARRAY_CONTAINS_NONE = new CompareType("Contains None", "arraycontainsnone", "ARRAYCONTAINSNONE", true, null, OperatorType.ARRAYCONTAINSNONE) + { + @Override + public ArrayContainsNoneClause createFilterClause(@NotNull FieldKey fieldKey, Object value) + { + return new ArrayContainsNoneClause(fieldKey, getCollectionParam(value)); + } + + @Override + public boolean meetsCriteria(ColumnRenderProperties col, Object value, Object[] filterValues) + { + throw new UnsupportedOperationException("Conditional formatting not yet supported for Multi Choices"); + } + + @Override + public String getValueSeparator() + { + return ArrayClause.ARRAY_VALUE_SEPARATOR; + } + }; + + public static final CompareType ARRAY_MATCHES = new CompareType("Contains Exactly", "arraymatches", "ARRAYMATCHES", true, null, OperatorType.ARRAYMATCHES) + { + @Override + public ArrayMatchesClause createFilterClause(@NotNull FieldKey fieldKey, Object value) + { + return new ArrayMatchesClause(fieldKey, getCollectionParam(value)); + } + + @Override + public boolean meetsCriteria(ColumnRenderProperties col, Object value, Object[] filterValues) + { + throw new UnsupportedOperationException("Conditional formatting not yet supported for Multi Choices"); + } + + @Override + public String getValueSeparator() + { + return ArrayClause.ARRAY_VALUE_SEPARATOR; + } + }; + + public static final CompareType ARRAY_NOT_MATCHES = new CompareType("Does Not Contain Exactly", "arraynotmatches", "ARRAYNOTMATCHES", true, null, OperatorType.ARRAYNOTMATCHES) + { + @Override + public ArrayNotMatchesClause createFilterClause(@NotNull FieldKey fieldKey, Object value) + { + return new ArrayNotMatchesClause(fieldKey, getCollectionParam(value)); + } + + @Override + public boolean meetsCriteria(ColumnRenderProperties col, Object value, Object[] filterValues) + { + throw new UnsupportedOperationException("Conditional formatting not yet supported for Multi Choices"); + } + + @Override + public String getValueSeparator() + { + return ArrayClause.ARRAY_VALUE_SEPARATOR; + } + }; + + public static abstract class ArrayClause extends SimpleFilter.MultiValuedFilterClause + { + public static final String ARRAY_VALUE_SEPARATOR = ","; + + public ArrayClause(@NotNull FieldKey fieldKey, CompareType comparison, Collection params, boolean negated) + { + super(fieldKey, comparison, params, negated); + } + + public SQLFragment[] getParamSQLFragments(SqlDialect dialect) + { + Object[] params = getParamVals(); + SQLFragment[] fragments = new SQLFragment[params.length]; + + JdbcType type = null; + // Try to infer the type from the first non-null parameter + for (Object param : params) + { + if (param != null) + { + type = JdbcType.valueOf(param.getClass()); + break; + } + } + + for (int i = 0; i < params.length; i++) + fragments[i] = new SQLFragment().append(escapeLabKeySqlValue(params[i], type)); + + return fragments; + } + + public Pair getSqlFragments(Map columnMap, SqlDialect dialect) + { + SQLFragment[] paramValues = getParamSQLFragments(dialect); + + if (paramValues == null || paramValues.length == 0) + return null; + + ColumnInfo colInfo = columnMap != null ? columnMap.get(_fieldKey) : null; + var alias = SimpleFilter.getAliasForColumnFilter(dialect, colInfo, _fieldKey); + + SQLFragment valuesFragment = dialect.array_construct(paramValues); + SQLFragment columnFragment = new SQLFragment().appendIdentifier(alias); + + return new Pair<>(valuesFragment, columnFragment); + } + + } + + private static class ArrayContainsAllClause extends ArrayClause + { + + public ArrayContainsAllClause(@NotNull FieldKey fieldKey, Collection params) + { + super(fieldKey, CompareType.ARRAY_CONTAINS_ALL, params, false); + } + + @Override + public SQLFragment toSQLFragment(Map columnMap, SqlDialect dialect) + { + Pair valueFieldSql = getSqlFragments(columnMap, dialect); + if (valueFieldSql == null) + return new SQLFragment("1=2"); + + return dialect.array_all_in_array(valueFieldSql.first, valueFieldSql.second); + } + + @Override + public String getLabKeySQLWhereClause(Map columnMap) + { + return "array_contains_all(" + getLabKeySQLColName(_fieldKey) + ", " + getParamVals()[0] + ")"; + } + + @Override + public void appendFilterText(StringBuilder sb, ColumnNameFormatter formatter) + { + Object[] params = getParamVals(); + sb.append("contains all of ").append(Arrays.toString(params)); + } + + } + + private static class ArrayContainsAnyClause extends ArrayClause + { + + public ArrayContainsAnyClause(@NotNull FieldKey fieldKey, CompareType comparison, Collection params, boolean negated) + { + super(fieldKey, comparison, params, negated); + } + + public ArrayContainsAnyClause(@NotNull FieldKey fieldKey, Collection params) + { + this(fieldKey, CompareType.ARRAY_CONTAINS_ANY, params, false); + } + + @Override + public SQLFragment toSQLFragment(Map columnMap, SqlDialect dialect) + { + Pair valueFieldSql = getSqlFragments(columnMap, dialect); + if (valueFieldSql == null) + return new SQLFragment("1=2"); + + SQLFragment sql = dialect.array_some_in_array(valueFieldSql.first, valueFieldSql.second); + if (!_negated) + return sql; + return new SQLFragment(" NOT (").append(sql).append(")"); + } + + @Override + public String getLabKeySQLWhereClause(Map columnMap) + { + return "array_contains_any(" + getLabKeySQLColName(_fieldKey) + ", " + getParamVals()[0] + ")"; + } + + @Override + public void appendFilterText(StringBuilder sb, ColumnNameFormatter formatter) + { + Object[] params = getParamVals(); + sb.append("contains at least one of ").append(Arrays.toString(params)); + } + + } + + private static class ArrayContainsNoneClause extends ArrayContainsAnyClause + { + + public ArrayContainsNoneClause(@NotNull FieldKey fieldKey, Collection params) + { + super(fieldKey, CompareType.ARRAY_CONTAINS_NONE, params, true); + } + + @Override + public String getLabKeySQLWhereClause(Map columnMap) + { + return "array_contains_none(" + getLabKeySQLColName(_fieldKey) + ", " + getParamVals()[0] + ")"; + } + + @Override + public void appendFilterText(StringBuilder sb, ColumnNameFormatter formatter) + { + Object[] params = getParamVals(); + sb.append("contains none of ").append(Arrays.toString(params)); + } + + } + + private static class ArrayMatchesClause extends ArrayClause + { + + public ArrayMatchesClause(@NotNull FieldKey fieldKey, CompareType comparison, Collection params, boolean negated) + { + super(fieldKey, comparison, params, negated); + } + + public ArrayMatchesClause(@NotNull FieldKey fieldKey, Collection params) + { + this(fieldKey, CompareType.ARRAY_MATCHES, params, false); + } + + @Override + public SQLFragment toSQLFragment(Map columnMap, SqlDialect dialect) + { + Pair valueFieldSql = getSqlFragments(columnMap, dialect); + if (valueFieldSql == null) + return new SQLFragment("1=2"); + + SQLFragment sql = dialect.array_same_array(valueFieldSql.first, valueFieldSql.second); + if (!_negated) + return sql; + return new SQLFragment(" NOT (").append(sql).append(")"); + } + + @Override + public String getLabKeySQLWhereClause(Map columnMap) + { + return "array_is_same(" + getLabKeySQLColName(_fieldKey) + ", " + getParamVals()[0] + ")"; + } + + @Override + public void appendFilterText(StringBuilder sb, ColumnNameFormatter formatter) + { + Object[] params = getParamVals(); + sb.append("contains the same elements as ").append(Arrays.toString(params)); + } + + } + + private static class ArrayNotMatchesClause extends ArrayMatchesClause + { + + public ArrayNotMatchesClause(@NotNull FieldKey fieldKey, Collection params) + { + super(fieldKey, CompareType.ARRAY_NOT_MATCHES, params, true); + } + + @Override + public String getLabKeySQLWhereClause(Map columnMap) + { + return "NOT array_is_same(" + getLabKeySQLColName(_fieldKey) + ", " + getParamVals()[0] + ")"; + } + + @Override + public void appendFilterText(StringBuilder sb, ColumnNameFormatter formatter) + { + Object[] params = getParamVals(); + sb.append("does not contain the same elements as ").append(Arrays.toString(params)); + } + + } + + private static class QClause extends CompareType.CompareClause { private List _queryColumns = null; diff --git a/api/src/org/labkey/api/data/ConnectionWrapper.java b/api/src/org/labkey/api/data/ConnectionWrapper.java index 94b46667dae..097db7aecc9 100644 --- a/api/src/org/labkey/api/data/ConnectionWrapper.java +++ b/api/src/org/labkey/api/data/ConnectionWrapper.java @@ -1025,7 +1025,7 @@ public Array createArrayOf(String unused, Object[] array) throws SQLException try { SqlDialect dialect = _scope.getSqlDialect(); - String typeName = dialect.getJDBCArrayType(array[0]); + String typeName = dialect.getJDBCArrayType(array); return _connection.createArrayOf(typeName, array); } catch (SQLException e) diff --git a/api/src/org/labkey/api/data/ConvertHelper.java b/api/src/org/labkey/api/data/ConvertHelper.java index a8918d2bc9a..7fa0b37eb7a 100644 --- a/api/src/org/labkey/api/data/ConvertHelper.java +++ b/api/src/org/labkey/api/data/ConvertHelper.java @@ -76,6 +76,7 @@ import java.io.File; import java.math.BigDecimal; import java.math.BigInteger; +import java.sql.Array; import java.sql.Blob; import java.sql.Clob; import java.sql.SQLException; @@ -189,6 +190,7 @@ protected void register() _register(new JSONTypeConverter(), JSONObject.class); _register(new ShortURLRecordConverter(), ShortURLRecord.class); _register(new ColumnHeaderType.Converter(), ColumnHeaderType.class); + _register(MultiChoice.Converter.getInstance(), MultiChoice.Array.class); } diff --git a/api/src/org/labkey/api/data/DataColumn.java b/api/src/org/labkey/api/data/DataColumn.java index f8f6253c82b..7ae99e64a0e 100644 --- a/api/src/org/labkey/api/data/DataColumn.java +++ b/api/src/org/labkey/api/data/DataColumn.java @@ -735,15 +735,17 @@ else if (_inputType.equalsIgnoreCase("checkbox")) private void renderSelectFormInput(HtmlWriter out, String formFieldName, Object value, List strValues, boolean disabledInput, NamedObjectList entryList) { + boolean isMultiple = "select.multiple".equalsIgnoreCase(_inputType); SelectBuilder select = new SelectBuilder() .disabled(disabledInput) - .multiple("select.multiple".equalsIgnoreCase(_inputType)) + .multiple(isMultiple) .name(formFieldName); List options = new ArrayList<>(); // add empty option - options.add(new OptionBuilder().build()); + if (!isMultiple) + options.add(new OptionBuilder().build()); Set selectedValues = strValues.isEmpty() ? Set.of() : strValues.size()==1 ? (null == strValues.get(0) ? Set.of() : Set.of(strValues.get(0))) : diff --git a/api/src/org/labkey/api/data/JdbcType.java b/api/src/org/labkey/api/data/JdbcType.java index e427d25382a..480694853a6 100644 --- a/api/src/org/labkey/api/data/JdbcType.java +++ b/api/src/org/labkey/api/data/JdbcType.java @@ -21,6 +21,7 @@ import org.apache.commons.lang3.StringUtils; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import org.json.JSONArray; import org.junit.Assert; import org.junit.Test; import org.labkey.api.collections.IntHashMap; @@ -34,6 +35,7 @@ import java.sql.Time; import java.sql.Timestamp; import java.sql.Types; +import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Date; @@ -300,7 +302,36 @@ protected Collection getSqlTypes() } }, - ARRAY(Types.ARRAY, Array.class), + ARRAY(Types.ARRAY, Array.class) + { + @Override + public Object convert(Object o) throws ConversionException + { + if ((o instanceof java.sql.Array array)) + return array; + if (o instanceof JSONArray jsonArray) + { + // convert jsonArray to array + Object[] elements = new Object[jsonArray.length()]; + for (int i = 0; i < jsonArray.length(); i++) + { + elements[i] = jsonArray.get(i); + } + return new MultiChoice.Array(Arrays.stream(elements)); + } + if (o instanceof Collection collection) + { + return new MultiChoice.Array(collection.stream().map(String::valueOf)); + } + if (o != null) + { + String s = String.valueOf(o); + return new MultiChoice.Array(Arrays.stream(new String[] {s})); + } + + return null; + } + }, NULL(Types.NULL, Object.class), diff --git a/api/src/org/labkey/api/data/MultiChoice.java b/api/src/org/labkey/api/data/MultiChoice.java index 7dda1715437..266fce36ab8 100644 --- a/api/src/org/labkey/api/data/MultiChoice.java +++ b/api/src/org/labkey/api/data/MultiChoice.java @@ -6,7 +6,6 @@ import org.json.JSONArray; import org.junit.Assert; import org.junit.Test; -import org.labkey.api.collections.CaseInsensitiveHashSet; import org.labkey.api.exp.property.IPropertyValidator; import org.labkey.api.exp.property.PropertyService; import org.labkey.api.gwt.client.model.PropertyValidatorType; @@ -31,6 +30,7 @@ import java.util.Map; import java.util.Objects; import java.util.Set; +import java.util.TreeSet; import java.util.function.BiConsumer; import java.util.function.BinaryOperator; import java.util.function.Function; @@ -39,6 +39,7 @@ import java.util.stream.Stream; import java.util.stream.StreamSupport; +import static org.apache.commons.lang3.StringUtils.isBlank; import static org.labkey.api.util.DOM.Attribute.style; import static org.labkey.api.util.DOM.DIV; import static org.labkey.api.util.DOM.SPAN; @@ -56,6 +57,11 @@ public DisplayColumn(ColumnInfo col) @Override public Object getValue(RenderContext ctx) + { + return getArrayValue(ctx); + } + + public Array getArrayValue(RenderContext ctx) { Object v = super.getValue(ctx); if (!(v instanceof java.sql.Array array)) @@ -129,6 +135,29 @@ public void renderInputCell(RenderContext ctx, HtmlWriter out) DIV(array.stream().map(v -> SPAN(at(style,"border:solid 1px black; border-radius:3px;"), v)) .collect(new JoinRenderable(HtmlString.SP)))); } + + @Override + public String getTsvFormattedValue(RenderContext ctx) + { + Array values = getArrayValue(ctx); + if (null != values && !values.isEmpty()) + { + return PageFlowUtil.joinValuesToStringForExport(values); + } + return null; + } + + @Override + public Object getExcelCompatibleValue(RenderContext ctx) + { + return getTsvFormattedValue(ctx); + } + + @Override + public Object getExportCompatibleValue(RenderContext ctx) + { + return getTsvFormattedValue(ctx); + } } @@ -182,17 +211,19 @@ public Set characteristics() // LK impl to help with conversions public static class Array implements List, java.sql.Array { + public static final Array EMPTY = new Array(new String[0]); + final String[] array; List list = null; protected Array(Stream str) { - CaseInsensitiveHashSet set = new CaseInsensitiveHashSet(); - array = str.filter(Objects::nonNull) + TreeSet setCaseSensitive = new TreeSet<>(); + str.filter(Objects::nonNull) .map(s -> StringUtils.trimToNull(s.toString())) .filter(Objects::nonNull) - .filter(set::add) - .toArray(String[]::new); + .forEach(setCaseSensitive::add); + array = setCaseSensitive.toArray(new String[0]); } protected Array(Object[] array) @@ -244,6 +275,8 @@ public static Array from(@NotNull org.json.JSONArray array) public static Array from(@NotNull String s) { + if (isBlank(s)) + return EMPTY; List split = PageFlowUtil.splitStringToValuesForImport(s); return from(split.toArray()); } @@ -310,7 +343,7 @@ public boolean contains(Object o) @Override public @NotNull Object[] toArray() { - return array; + return 0==array.length ? array : array.clone(); } @Override @@ -511,7 +544,7 @@ public static Converter getInstance() public T convert(Class aClass, Object o) { if (null == o) - return (T) Array.from(new String[]{}); + return (T) Array.EMPTY; if (o instanceof MultiChoice.Array arr) return (T)arr; if (o instanceof String s) @@ -546,6 +579,13 @@ public void testConvert() throws Exception assertEquals(expected, _converter.convert(Array.class, new String[]{"a,","b\"","c "})); assertEquals(expected, _converter.convert(Array.class, List.of("a,","b\"","c "))); assertEquals(expected, _converter.convert(Array.class, new JSONArray(List.of("a,","b\"","c ")))); + // test that result is ordered + assertEquals(expected, _converter.convert(Array.class, "\"c \",\"b\"\"\",\"a,\"")); + + // empty + assertEquals(0, _converter.convert(Array.class, " ").size()); + assertEquals(0, _converter.convert(Array.class, "").size()); + assertEquals(0, _converter.convert(Array.class, null).size()); } @Test diff --git a/api/src/org/labkey/api/data/PropertyStorageSpec.java b/api/src/org/labkey/api/data/PropertyStorageSpec.java index 9003a17fae0..db20b5052d3 100644 --- a/api/src/org/labkey/api/data/PropertyStorageSpec.java +++ b/api/src/org/labkey/api/data/PropertyStorageSpec.java @@ -169,6 +169,7 @@ public PropertyStorageSpec(PropertyDescriptor propertyDescriptor) setMvEnabled(propertyDescriptor.isMvEnabled()); setDescription(propertyDescriptor.getDescription()); setImportAliases(propertyDescriptor.getImportAliases()); + setTypeURI(propertyDescriptor.getRangeURI()); if (null != propertyDescriptor.getDatabaseDefaultValue()) setDefaultValue(propertyDescriptor.getDatabaseDefaultValue()); diff --git a/api/src/org/labkey/api/data/SimpleConnectionWrapper.java b/api/src/org/labkey/api/data/SimpleConnectionWrapper.java index 8d8a4402d84..68004fdd162 100644 --- a/api/src/org/labkey/api/data/SimpleConnectionWrapper.java +++ b/api/src/org/labkey/api/data/SimpleConnectionWrapper.java @@ -40,7 +40,7 @@ public SimpleConnectionWrapper(Connection connection, DbScope scope) public Array createArrayOf(String unused, Object[] array) throws SQLException { SqlDialect dialect = _scope.getSqlDialect(); - String typeName = dialect.getJDBCArrayType(array[0]); + String typeName = dialect.getJDBCArrayType(array); return _connection.createArrayOf(typeName, array); } diff --git a/api/src/org/labkey/api/data/TSVMapWriter.java b/api/src/org/labkey/api/data/TSVMapWriter.java index fa78e854a48..9ee93e9703a 100644 --- a/api/src/org/labkey/api/data/TSVMapWriter.java +++ b/api/src/org/labkey/api/data/TSVMapWriter.java @@ -19,6 +19,7 @@ import org.junit.Assert; import org.junit.Test; import org.labkey.api.iterator.MarkableIterator; +import org.labkey.api.util.PageFlowUtil; import java.io.IOException; import java.util.ArrayList; @@ -116,6 +117,8 @@ protected void writeRow(final Map row) { Iterable values = _columns.stream().map(col -> { Object o = row.get(col); + if (o instanceof List list && list.getFirst() instanceof String) + return PageFlowUtil.joinValuesToStringForExport(list); return o == null ? "" : String.valueOf(o); }).toList(); diff --git a/api/src/org/labkey/api/data/dialect/BasePostgreSqlDialect.java b/api/src/org/labkey/api/data/dialect/BasePostgreSqlDialect.java index f193447804b..c7e38673c4c 100644 --- a/api/src/org/labkey/api/data/dialect/BasePostgreSqlDialect.java +++ b/api/src/org/labkey/api/data/dialect/BasePostgreSqlDialect.java @@ -42,6 +42,7 @@ import org.labkey.api.data.TableInfo; import org.labkey.api.data.dialect.LimitRowsSqlGenerator.LimitRowsCustomizer; import org.labkey.api.data.dialect.LimitRowsSqlGenerator.StandardLimitRowsCustomizer; +import org.labkey.api.exp.PropertyType; import org.labkey.api.util.ExceptionUtil; import org.labkey.api.util.HtmlString; import org.labkey.api.util.StringUtilsLabKey; @@ -189,6 +190,8 @@ protected void addSqlTypeInts(Map sqlTypeIntMap) sqlTypeIntMap.put(Types.TIMESTAMP, "TIMESTAMP"); sqlTypeIntMap.put(Types.DOUBLE, "DOUBLE PRECISION"); sqlTypeIntMap.put(Types.FLOAT, "DOUBLE PRECISION"); + + sqlTypeIntMap.put(Types.ARRAY, "text[]"); // only support text arrays for now } @Override @@ -765,6 +768,10 @@ else if (prop.getJdbcType() == JdbcType.VARCHAR && (prop.getSize() == -1 || prop { return getSqlTypeName(JdbcType.LONGVARCHAR); } + else if (PropertyType.MULTI_CHOICE.getTypeUri().equals(prop.getTypeURI()) && prop.getJdbcType() == JdbcType.ARRAY) + { + return "text[]"; + } else { return getSqlTypeName(prop.getJdbcType()); diff --git a/api/src/org/labkey/api/data/dialect/SqlDialect.java b/api/src/org/labkey/api/data/dialect/SqlDialect.java index 390adcc71a9..85cc60ec933 100644 --- a/api/src/org/labkey/api/data/dialect/SqlDialect.java +++ b/api/src/org/labkey/api/data/dialect/SqlDialect.java @@ -1300,6 +1300,55 @@ public String getJDBCArrayType(Object object) return StringUtils.lowerCase(getSqlTypeNameFromObject(object)); } + public String getJDBCArrayType(Object[] array) + { + String typeName; + if (array.length == 0 || array[0] == null) + { + // Handle empty arrays by inferring the SQL element type from the Java component type. + // Primary target is String[0], but handle a reasonable set of common types defensively. + Class componentType = array.getClass().getComponentType(); + if (String.class.equals(componentType)) + { + // Use dialect mapping for a String instance + typeName = getJDBCArrayType(""); + } + else if (Integer.class.equals(componentType)) + { + typeName = getJDBCArrayType(Integer.valueOf(0)); + } + else if (Long.class.equals(componentType)) + { + typeName = getJDBCArrayType(Long.valueOf(0L)); + } + else if (Double.class.equals(componentType)) + { + typeName = getJDBCArrayType(Double.valueOf(0.0d)); + } + else if (Float.class.equals(componentType)) + { + typeName = getJDBCArrayType(Float.valueOf(0.0f)); + } + else if (Boolean.class.equals(componentType)) + { + typeName = getJDBCArrayType(Boolean.FALSE); + } + else + { + // Fallback to VARCHAR which is the safest for most text use-cases + typeName = getSqlTypeName(JdbcType.VARCHAR); + if (typeName != null) + typeName = typeName.toLowerCase(); + } + } + else + { + typeName = getJDBCArrayType(array[0]); + } + + return typeName; + } + public Collection getScriptWarnings(String name, String sql) { return Collections.emptyList(); diff --git a/api/src/org/labkey/api/exp/ObjectProperty.java b/api/src/org/labkey/api/exp/ObjectProperty.java index a0d4a0f1e2f..72a61647493 100644 --- a/api/src/org/labkey/api/exp/ObjectProperty.java +++ b/api/src/org/labkey/api/exp/ObjectProperty.java @@ -19,10 +19,12 @@ import org.labkey.api.data.BeanObjectFactory; import org.labkey.api.data.Container; import org.labkey.api.data.ContainerManager; +import org.labkey.api.data.MultiChoice; import org.labkey.api.data.MvUtil; import org.labkey.api.util.GUID; import java.io.File; +import java.sql.Array; import java.util.Collections; import java.util.Date; import java.util.Map; @@ -49,6 +51,7 @@ public class ObjectProperty extends OntologyManager.PropertyRow // ObjectProperty protected Identifiable objectValue; private Map _childProperties; + protected MultiChoice.Array arrayValue; // Don't delete this -- it's accessed via introspection public ObjectProperty() diff --git a/api/src/org/labkey/api/exp/PropertyType.java b/api/src/org/labkey/api/exp/PropertyType.java index a30ac08695f..917492ad1e6 100644 --- a/api/src/org/labkey/api/exp/PropertyType.java +++ b/api/src/org/labkey/api/exp/PropertyType.java @@ -36,6 +36,8 @@ import java.io.File; import java.math.BigDecimal; import java.nio.ByteBuffer; +import java.sql.Array; +import java.sql.SQLException; import java.sql.Time; import java.text.DateFormat; import java.text.ParseException; @@ -234,13 +236,13 @@ protected void init(PropertyRow row, Object value) @Override protected void setValue(ObjectProperty property, Object value) { - throw new UnsupportedOperationException("TODO MultiChoice"); + property.arrayValue = MultiChoice.Converter.getInstance().convert(MultiChoice.Array.class, value); } @Override protected Object getValue(ObjectProperty property) { - throw new UnsupportedOperationException("TODO MultiChoice"); + return property.arrayValue; } @Override diff --git a/api/src/org/labkey/api/exp/api/SampleTypeDomainKind.java b/api/src/org/labkey/api/exp/api/SampleTypeDomainKind.java index da47258672a..6b06f23ebb0 100644 --- a/api/src/org/labkey/api/exp/api/SampleTypeDomainKind.java +++ b/api/src/org/labkey/api/exp/api/SampleTypeDomainKind.java @@ -206,6 +206,12 @@ public boolean allowFileLinkProperties() return true; } + @Override + public boolean allowMultiChoiceProperties() + { + return true; + } + @Override public boolean allowTimepointProperties() { diff --git a/api/src/org/labkey/api/exp/property/DomainKind.java b/api/src/org/labkey/api/exp/property/DomainKind.java index f2a8ff5aad9..1de58f1c673 100644 --- a/api/src/org/labkey/api/exp/property/DomainKind.java +++ b/api/src/org/labkey/api/exp/property/DomainKind.java @@ -323,7 +323,7 @@ public boolean matchesTemplateXML(String templateName, DomainTemplateType templa public boolean allowAttachmentProperties() { return false; } public boolean allowFlagProperties() { return true; } public boolean allowTextChoiceProperties() { return true; } - public boolean allowMultiTextChoiceProperties() { return false; } + public boolean allowMultiChoiceProperties() { return false; } public boolean allowSampleSubjectProperties() { return true; } public boolean allowTimepointProperties() { return false; } public boolean allowUniqueConstraintProperties() { return false; } diff --git a/api/src/org/labkey/api/exp/property/DomainUtil.java b/api/src/org/labkey/api/exp/property/DomainUtil.java index af2309f42c8..cf9f540bf25 100644 --- a/api/src/org/labkey/api/exp/property/DomainUtil.java +++ b/api/src/org/labkey/api/exp/property/DomainUtil.java @@ -35,6 +35,7 @@ import org.labkey.api.data.ContainerFilter; import org.labkey.api.data.ContainerManager; import org.labkey.api.data.ContainerService; +import org.labkey.api.data.CoreSchema; import org.labkey.api.data.NameGenerator; import org.labkey.api.data.PHI; import org.labkey.api.data.PropertyStorageSpec; @@ -78,6 +79,8 @@ import org.labkey.api.query.UserSchema; import org.labkey.api.query.ValidationException; import org.labkey.api.security.User; +import org.labkey.api.settings.AppProps; +import org.labkey.api.settings.OptionalFeatureService; import org.labkey.api.util.DateUtil; import org.labkey.api.util.GUID; import org.labkey.api.util.JdbcUtil; @@ -428,6 +431,15 @@ public static List getCalculatedFieldsForDefaultView(@NotNull TableInf return calculatedFieldKeys; } + public static boolean allowMultiChoice(DomainKind kind) + { + if (!kind.allowMultiChoiceProperties()) + return false; + if (!OptionalFeatureService.get().isFeatureEnabled(AppProps.MULTI_VALUE_TEXT_CHOICE)) + return false; + return CoreSchema.getInstance().getSqlDialect().isPostgreSQL(); + } + private static GWTDomain getDomain(Domain dd) { GWTDomain gwtDomain = new GWTDomain<>(); @@ -447,6 +459,7 @@ private static GWTDomain getDomain(Domain dd) gwtDomain.setAllowFileLinkProperties(kind.allowFileLinkProperties()); gwtDomain.setAllowFlagProperties(kind.allowFlagProperties()); gwtDomain.setAllowTextChoiceProperties(kind.allowTextChoiceProperties()); + gwtDomain.setAllowMultiChoiceProperties(allowMultiChoice(kind)); gwtDomain.setAllowSampleSubjectProperties(kind.allowSampleSubjectProperties()); gwtDomain.setAllowTimepointProperties(kind.allowTimepointProperties()); gwtDomain.setAllowUniqueConstraintProperties(kind.allowUniqueConstraintProperties()); @@ -466,6 +479,7 @@ public static GWTDomain getTemplateDomainForDomainKind(Do gwtDomain.setAllowFileLinkProperties(kind.allowFileLinkProperties()); gwtDomain.setAllowFlagProperties(kind.allowFlagProperties()); gwtDomain.setAllowTextChoiceProperties(kind.allowTextChoiceProperties()); + gwtDomain.setAllowMultiChoiceProperties(allowMultiChoice(kind)); gwtDomain.setAllowSampleSubjectProperties(kind.allowSampleSubjectProperties()); gwtDomain.setAllowTimepointProperties(kind.allowTimepointProperties()); gwtDomain.setShowDefaultValueSettings(kind.showDefaultValueSettings()); diff --git a/api/src/org/labkey/api/reader/TabLoader.java b/api/src/org/labkey/api/reader/TabLoader.java index 865abbf6091..635b4866d2f 100644 --- a/api/src/org/labkey/api/reader/TabLoader.java +++ b/api/src/org/labkey/api/reader/TabLoader.java @@ -26,6 +26,7 @@ import org.junit.Assert; import org.junit.Test; import org.labkey.api.data.Container; +import org.labkey.api.data.MultiChoice; import org.labkey.api.dataiterator.HashDataIterator; import org.labkey.api.iterator.BeanIterator; import org.labkey.api.iterator.CloseableIterator; @@ -405,6 +406,16 @@ private String[] readFields(TabBufferedReader r, @Nullable ColumnDescriptor[] co char ch = buf.charAt(start); char chQuote = '"'; + boolean isArrayColumn = false; + if (columns != null && colIndex < columns.length) + { + var column = columns[colIndex]; + if (column != null) + isArrayColumn = column.clazz == MultiChoice.class || column.clazz == MultiChoice.Array.class; + } + + boolean parseEnclosedQuotes = _parseEnclosedQuotes || isArrayColumn; + colIndex++; boolean isDelimiterOrQuote = false; @@ -431,7 +442,7 @@ else if (ch == chQuote) if (nextLine == null) { // We've reached the end of the input, so there's nothing else to append - if (_parseEnclosedQuotes) + if (parseEnclosedQuotes) isDelimiterOrQuote = false; break; } @@ -446,7 +457,7 @@ else if (ch == chQuote) // " a, " b should be parsed as [" a, " b], not [a, b] // if the next quote is before the end of the buffer and the next non-blank character is not the delimiter, // retain the quote as a mid-field value. - if (_parseEnclosedQuotes && end != buf.length() - 1 && (fieldEnd == -1 || !_whitespacePattern.matcher(buf.substring(end+1, fieldEnd)).matches())) + if (parseEnclosedQuotes && end != buf.length() - 1 && (fieldEnd == -1 || !_whitespacePattern.matcher(buf.substring(end+1, fieldEnd)).matches())) isDelimiterOrQuote = false; break; } diff --git a/api/src/org/labkey/api/settings/AppProps.java b/api/src/org/labkey/api/settings/AppProps.java index 6395044aeb2..ba493df5f8d 100644 --- a/api/src/org/labkey/api/settings/AppProps.java +++ b/api/src/org/labkey/api/settings/AppProps.java @@ -51,6 +51,7 @@ public interface AppProps String QUANTITY_COLUMN_SUFFIX_TESTING = "quantityColumnSuffixTesting"; String GENERATE_CONTROLLER_FIRST_URLS = "generateControllerFirstUrls"; String REJECT_CONTROLLER_FIRST_URLS = "rejectControllerFirstUrls"; + String MULTI_VALUE_TEXT_CHOICE = "multiChoiceDataType"; String UNKNOWN_VERSION = "Unknown Release Version"; diff --git a/api/webapp/clientapi/ext3/FilterDialog.js b/api/webapp/clientapi/ext3/FilterDialog.js index 7e85ba94490..b4c25180a66 100644 --- a/api/webapp/clientapi/ext3/FilterDialog.js +++ b/api/webapp/clientapi/ext3/FilterDialog.js @@ -916,6 +916,8 @@ LABKEY.FilterDialog.View.Default = Ext.extend(LABKEY.FilterDialog.ViewPanel, { }, validateMultiValueInput : function(inputValues, multiValueSeparator, minOccurs, maxOccurs) { + if (!inputValues) + return true; // Used when "Equals One Of.." or "Between" is selected. Calls validateInputField on each value entered. const sep = inputValues.indexOf('\n') > 0 ? '\n' : multiValueSeparator; var values = inputValues.split(sep); diff --git a/assay/api-src/org/labkey/api/assay/AssayResultDomainKind.java b/assay/api-src/org/labkey/api/assay/AssayResultDomainKind.java index 707d9644d66..c6de4802ecb 100644 --- a/assay/api-src/org/labkey/api/assay/AssayResultDomainKind.java +++ b/assay/api-src/org/labkey/api/assay/AssayResultDomainKind.java @@ -188,4 +188,11 @@ public String getDomainFileDirectory() { return DIR_NAME; } + + @Override + public boolean allowMultiChoiceProperties() + { + return true; + } + } diff --git a/assay/package-lock.json b/assay/package-lock.json index 4843a841ff3..155452c9304 100644 --- a/assay/package-lock.json +++ b/assay/package-lock.json @@ -8,7 +8,7 @@ "name": "assay", "version": "0.0.0", "dependencies": { - "@labkey/components": "7.12.0" + "@labkey/components": "7.13.0-fb-mvtc.4" }, "devDependencies": { "@labkey/build": "8.7.0", @@ -2482,9 +2482,9 @@ } }, "node_modules/@labkey/api": { - "version": "1.44.1", - "resolved": "https://labkey.jfrog.io/artifactory/api/npm/libs-client/@labkey/api/-/@labkey/api-1.44.1.tgz", - "integrity": "sha512-VUS4KLfwAsE45A3MnJUU3j97ei0ncQHv6OVVAN3kitID0xe8+mZ7B39zETVye3Dqgwa8TbYvsCp2t46QmBmwVQ==", + "version": "1.45.0-fb-mvtc.2", + "resolved": "https://labkey.jfrog.io/artifactory/api/npm/libs-client/@labkey/api/-/@labkey/api-1.45.0-fb-mvtc.2.tgz", + "integrity": "sha512-26xRLDWTZTOIGvoseGCeLVzMlwftHs6oxOlft6uhdmJpYBdm6+AyK2w+jU0Fns5SaEtaZAGofrBJFi8zCXttCg==", "license": "Apache-2.0" }, "node_modules/@labkey/build": { @@ -2525,13 +2525,13 @@ } }, "node_modules/@labkey/components": { - "version": "7.12.0", - "resolved": "https://labkey.jfrog.io/artifactory/api/npm/libs-client/@labkey/components/-/@labkey/components-7.12.0.tgz", - "integrity": "sha512-iwB+3m7JWcwJ7sPA8V+lPXeW6J0tSq/uueiRJ7EzzuaBubXgYwX4ISpZNVt4MzxtAybImzOGLU4ef4+2NFyVRw==", + "version": "7.13.0-fb-mvtc.4", + "resolved": "https://labkey.jfrog.io/artifactory/api/npm/libs-client/@labkey/components/-/@labkey/components-7.13.0-fb-mvtc.4.tgz", + "integrity": "sha512-eMcwnnL0LugsMWQpxh76H1RN5rW0lH3iCpk69oUOPRi/DqSGthA0xJsWbjYmrlK3yQQQiS1Pj+Efn35haKay+w==", "license": "SEE LICENSE IN LICENSE.txt", "dependencies": { "@hello-pangea/dnd": "18.0.1", - "@labkey/api": "1.44.1", + "@labkey/api": "1.45.0-fb-mvtc.2", "@testing-library/dom": "~10.4.1", "@testing-library/jest-dom": "~6.9.1", "@testing-library/react": "~16.3.0", diff --git a/assay/package.json b/assay/package.json index bba449b3803..a6da49980b9 100644 --- a/assay/package.json +++ b/assay/package.json @@ -12,7 +12,7 @@ "clean": "rimraf resources/web/assay/gen && rimraf resources/views/gen && rimraf resources/web/gen" }, "dependencies": { - "@labkey/components": "7.12.0" + "@labkey/components": "7.13.0-fb-mvtc.4" }, "devDependencies": { "@labkey/build": "8.7.0", diff --git a/core/package-lock.json b/core/package-lock.json index 5c3d14eb20d..131a4ec302e 100644 --- a/core/package-lock.json +++ b/core/package-lock.json @@ -8,7 +8,7 @@ "name": "labkey-core", "version": "0.0.0", "dependencies": { - "@labkey/components": "7.12.0", + "@labkey/components": "7.13.0-fb-mvtc.20", "@labkey/themes": "1.5.0" }, "devDependencies": { @@ -3504,9 +3504,9 @@ } }, "node_modules/@labkey/api": { - "version": "1.44.1", - "resolved": "https://labkey.jfrog.io/artifactory/api/npm/libs-client/@labkey/api/-/@labkey/api-1.44.1.tgz", - "integrity": "sha512-VUS4KLfwAsE45A3MnJUU3j97ei0ncQHv6OVVAN3kitID0xe8+mZ7B39zETVye3Dqgwa8TbYvsCp2t46QmBmwVQ==", + "version": "1.45.0-fb-mvtc.4", + "resolved": "https://labkey.jfrog.io/artifactory/api/npm/libs-client/@labkey/api/-/@labkey/api-1.45.0-fb-mvtc.4.tgz", + "integrity": "sha512-IeN9uvY6hqeBftEzbTbk8AFVP7hq0IaFl8ewbXj7wQXjBzPYSIpAym2lpurn7FYcYXJ/5F7RRj9z0bZKeAC+7g==", "license": "Apache-2.0" }, "node_modules/@labkey/build": { @@ -3547,13 +3547,13 @@ } }, "node_modules/@labkey/components": { - "version": "7.12.0", - "resolved": "https://labkey.jfrog.io/artifactory/api/npm/libs-client/@labkey/components/-/@labkey/components-7.12.0.tgz", - "integrity": "sha512-iwB+3m7JWcwJ7sPA8V+lPXeW6J0tSq/uueiRJ7EzzuaBubXgYwX4ISpZNVt4MzxtAybImzOGLU4ef4+2NFyVRw==", + "version": "7.13.0-fb-mvtc.20", + "resolved": "https://labkey.jfrog.io/artifactory/api/npm/libs-client/@labkey/components/-/@labkey/components-7.13.0-fb-mvtc.20.tgz", + "integrity": "sha512-YlZZJkiJd9Wc6lofA6ixim5HtGhFj24PKu3q9dsXVBAICXLSmryV9Y3EoGwfid49HVP1WGBhToGRQBouVYUa2w==", "license": "SEE LICENSE IN LICENSE.txt", "dependencies": { "@hello-pangea/dnd": "18.0.1", - "@labkey/api": "1.44.1", + "@labkey/api": "1.45.0-fb-mvtc.4", "@testing-library/dom": "~10.4.1", "@testing-library/jest-dom": "~6.9.1", "@testing-library/react": "~16.3.0", diff --git a/core/package.json b/core/package.json index 49e27874a49..3a909d90870 100644 --- a/core/package.json +++ b/core/package.json @@ -53,7 +53,7 @@ } }, "dependencies": { - "@labkey/components": "7.12.0", + "@labkey/components": "7.13.0-fb-mvtc.20", "@labkey/themes": "1.5.0" }, "devDependencies": { diff --git a/experiment/package-lock.json b/experiment/package-lock.json index d09c9ad5605..d77b9c36dbb 100644 --- a/experiment/package-lock.json +++ b/experiment/package-lock.json @@ -8,7 +8,7 @@ "name": "experiment", "version": "0.0.0", "dependencies": { - "@labkey/components": "7.12.0" + "@labkey/components": "7.13.0-fb-mvtc.20" }, "devDependencies": { "@labkey/build": "8.7.0", @@ -3271,9 +3271,9 @@ } }, "node_modules/@labkey/api": { - "version": "1.44.1", - "resolved": "https://labkey.jfrog.io/artifactory/api/npm/libs-client/@labkey/api/-/@labkey/api-1.44.1.tgz", - "integrity": "sha512-VUS4KLfwAsE45A3MnJUU3j97ei0ncQHv6OVVAN3kitID0xe8+mZ7B39zETVye3Dqgwa8TbYvsCp2t46QmBmwVQ==", + "version": "1.45.0-fb-mvtc.4", + "resolved": "https://labkey.jfrog.io/artifactory/api/npm/libs-client/@labkey/api/-/@labkey/api-1.45.0-fb-mvtc.4.tgz", + "integrity": "sha512-IeN9uvY6hqeBftEzbTbk8AFVP7hq0IaFl8ewbXj7wQXjBzPYSIpAym2lpurn7FYcYXJ/5F7RRj9z0bZKeAC+7g==", "license": "Apache-2.0" }, "node_modules/@labkey/build": { @@ -3314,13 +3314,13 @@ } }, "node_modules/@labkey/components": { - "version": "7.12.0", - "resolved": "https://labkey.jfrog.io/artifactory/api/npm/libs-client/@labkey/components/-/@labkey/components-7.12.0.tgz", - "integrity": "sha512-iwB+3m7JWcwJ7sPA8V+lPXeW6J0tSq/uueiRJ7EzzuaBubXgYwX4ISpZNVt4MzxtAybImzOGLU4ef4+2NFyVRw==", + "version": "7.13.0-fb-mvtc.20", + "resolved": "https://labkey.jfrog.io/artifactory/api/npm/libs-client/@labkey/components/-/@labkey/components-7.13.0-fb-mvtc.20.tgz", + "integrity": "sha512-YlZZJkiJd9Wc6lofA6ixim5HtGhFj24PKu3q9dsXVBAICXLSmryV9Y3EoGwfid49HVP1WGBhToGRQBouVYUa2w==", "license": "SEE LICENSE IN LICENSE.txt", "dependencies": { "@hello-pangea/dnd": "18.0.1", - "@labkey/api": "1.44.1", + "@labkey/api": "1.45.0-fb-mvtc.4", "@testing-library/dom": "~10.4.1", "@testing-library/jest-dom": "~6.9.1", "@testing-library/react": "~16.3.0", diff --git a/experiment/package.json b/experiment/package.json index d1aacc6542c..320781a4eda 100644 --- a/experiment/package.json +++ b/experiment/package.json @@ -13,7 +13,7 @@ "test-integration": "cross-env NODE_ENV=test jest --ci --runInBand -c test/js/jest.config.integration.js" }, "dependencies": { - "@labkey/components": "7.12.0" + "@labkey/components": "7.13.0-fb-mvtc.20" }, "devDependencies": { "@labkey/build": "8.7.0", diff --git a/experiment/src/org/labkey/experiment/ExperimentModule.java b/experiment/src/org/labkey/experiment/ExperimentModule.java index ab459df94f7..e94b8ee1c15 100644 --- a/experiment/src/org/labkey/experiment/ExperimentModule.java +++ b/experiment/src/org/labkey/experiment/ExperimentModule.java @@ -269,6 +269,8 @@ protected void init() { OptionalFeatureService.get().addExperimentalFeatureFlag(NameGenerator.EXPERIMENTAL_ALLOW_GAP_COUNTER, "Allow gap with withCounter and rootSampleCount expression", "Check this option if gaps in the count generated by withCounter or rootSampleCount name expression are allowed.", true); + OptionalFeatureService.get().addExperimentalFeatureFlag(AppProps.MULTI_VALUE_TEXT_CHOICE, "Allow multi-value Text Choice properties", + "Support selecting more than one value for text choice fields", false); } OptionalFeatureService.get().addExperimentalFeatureFlag(AppProps.QUANTITY_COLUMN_SUFFIX_TESTING, "Quantity column suffix testing", "If a column name contains a \"__\" suffix, this feature allows for testing it as a Quantity display column", false); @@ -855,6 +857,7 @@ SELECT COUNT(DISTINCT DD.DomainURI) FROM WHERE DD.storageSchemaName = ? AND D.rangeURI = ?""", DataClassDomainKind.PROVISIONED_SCHEMA_NAME, PropertyType.BOOLEAN.getTypeUri()).getObject(Long.class)); results.put("textChoiceColumnCount", new SqlSelector(schema, "SELECT COUNT(*) FROM exp.propertydescriptor WHERE concepturi = ?", TEXT_CHOICE_CONCEPT_URI).getObject(Long.class)); + results.put("multiValueTextChoiceColumnCount", new SqlSelector(schema, "SELECT COUNT(*) FROM exp.propertydescriptor WHERE rangeuri = ?", PropertyType.MULTI_CHOICE.getTypeUri()).getObject(Long.class)); results.put("domainsWithDateTimeColumnCount", new SqlSelector(schema, """ SELECT COUNT(DISTINCT DD.DomainURI) FROM diff --git a/experiment/src/org/labkey/experiment/api/DataClassDomainKind.java b/experiment/src/org/labkey/experiment/api/DataClassDomainKind.java index e440a6fb275..af1d3f26651 100644 --- a/experiment/src/org/labkey/experiment/api/DataClassDomainKind.java +++ b/experiment/src/org/labkey/experiment/api/DataClassDomainKind.java @@ -204,6 +204,12 @@ public boolean allowAttachmentProperties() return true; } + @Override + public boolean allowMultiChoiceProperties() + { + return true; + } + @Override public ActionURL urlCreateDefinition(String schemaName, String queryName, Container container, User user) { diff --git a/experiment/src/org/labkey/experiment/api/property/TextChoiceValidator.java b/experiment/src/org/labkey/experiment/api/property/TextChoiceValidator.java index 5c4be86ac7e..dcca8bba1e5 100644 --- a/experiment/src/org/labkey/experiment/api/property/TextChoiceValidator.java +++ b/experiment/src/org/labkey/experiment/api/property/TextChoiceValidator.java @@ -24,6 +24,7 @@ import org.labkey.api.exp.property.ValidatorContext; import org.labkey.api.exp.property.ValidatorKind; import org.labkey.api.gwt.client.model.PropertyValidatorType; +import org.labkey.api.query.SimpleValidationError; import org.labkey.api.query.ValidationError; import java.util.Collection; @@ -75,6 +76,11 @@ public boolean validate(IPropertyValidator validator, ColumnRenderProperties fie } else if (value instanceof Collection col) { + if (col.size() > 10) + { + errors.add(new SimpleValidationError("At most 10 values are allowed for field '" + field.getNonBlankCaption() + "'")); + return false; + } for (Object item : col) { if (null == item || !validValues.contains(Objects.toString(item))) diff --git a/list/src/org/labkey/list/model/ListDomainKind.java b/list/src/org/labkey/list/model/ListDomainKind.java index acf70af615a..8ae2881d322 100644 --- a/list/src/org/labkey/list/model/ListDomainKind.java +++ b/list/src/org/labkey/list/model/ListDomainKind.java @@ -153,6 +153,12 @@ public boolean allowAttachmentProperties() return true; } + @Override + public boolean allowMultiChoiceProperties() + { + return true; + } + @Override public boolean showDefaultValueSettings() { diff --git a/pipeline/package-lock.json b/pipeline/package-lock.json index 7ba89586f45..921467d259a 100644 --- a/pipeline/package-lock.json +++ b/pipeline/package-lock.json @@ -8,7 +8,7 @@ "name": "pipeline", "version": "0.0.0", "dependencies": { - "@labkey/components": "7.12.0" + "@labkey/components": "7.13.0-fb-mvtc.4" }, "devDependencies": { "@labkey/build": "8.7.0", @@ -2716,9 +2716,9 @@ } }, "node_modules/@labkey/api": { - "version": "1.44.1", - "resolved": "https://labkey.jfrog.io/artifactory/api/npm/libs-client/@labkey/api/-/@labkey/api-1.44.1.tgz", - "integrity": "sha512-VUS4KLfwAsE45A3MnJUU3j97ei0ncQHv6OVVAN3kitID0xe8+mZ7B39zETVye3Dqgwa8TbYvsCp2t46QmBmwVQ==", + "version": "1.45.0-fb-mvtc.2", + "resolved": "https://labkey.jfrog.io/artifactory/api/npm/libs-client/@labkey/api/-/@labkey/api-1.45.0-fb-mvtc.2.tgz", + "integrity": "sha512-26xRLDWTZTOIGvoseGCeLVzMlwftHs6oxOlft6uhdmJpYBdm6+AyK2w+jU0Fns5SaEtaZAGofrBJFi8zCXttCg==", "license": "Apache-2.0" }, "node_modules/@labkey/build": { @@ -2759,13 +2759,13 @@ } }, "node_modules/@labkey/components": { - "version": "7.12.0", - "resolved": "https://labkey.jfrog.io/artifactory/api/npm/libs-client/@labkey/components/-/@labkey/components-7.12.0.tgz", - "integrity": "sha512-iwB+3m7JWcwJ7sPA8V+lPXeW6J0tSq/uueiRJ7EzzuaBubXgYwX4ISpZNVt4MzxtAybImzOGLU4ef4+2NFyVRw==", + "version": "7.13.0-fb-mvtc.4", + "resolved": "https://labkey.jfrog.io/artifactory/api/npm/libs-client/@labkey/components/-/@labkey/components-7.13.0-fb-mvtc.4.tgz", + "integrity": "sha512-eMcwnnL0LugsMWQpxh76H1RN5rW0lH3iCpk69oUOPRi/DqSGthA0xJsWbjYmrlK3yQQQiS1Pj+Efn35haKay+w==", "license": "SEE LICENSE IN LICENSE.txt", "dependencies": { "@hello-pangea/dnd": "18.0.1", - "@labkey/api": "1.44.1", + "@labkey/api": "1.45.0-fb-mvtc.2", "@testing-library/dom": "~10.4.1", "@testing-library/jest-dom": "~6.9.1", "@testing-library/react": "~16.3.0", diff --git a/pipeline/package.json b/pipeline/package.json index f4f0a663aba..8e4b629b05f 100644 --- a/pipeline/package.json +++ b/pipeline/package.json @@ -14,7 +14,7 @@ "build-prod": "npm run clean && cross-env NODE_ENV=production PROD_SOURCE_MAP=source-map webpack --config node_modules/@labkey/build/webpack/prod.config.js --color --progress --profile" }, "dependencies": { - "@labkey/components": "7.12.0" + "@labkey/components": "7.13.0-fb-mvtc.4" }, "devDependencies": { "@labkey/build": "8.7.0", diff --git a/query/src/org/labkey/query/QueryServiceImpl.java b/query/src/org/labkey/query/QueryServiceImpl.java index 45749c18d19..d44943d4f2a 100644 --- a/query/src/org/labkey/query/QueryServiceImpl.java +++ b/query/src/org/labkey/query/QueryServiceImpl.java @@ -309,6 +309,11 @@ public void moduleChanged(Module module) CompareType.NONBLANK, CompareType.MV_INDICATOR, CompareType.NO_MV_INDICATOR, + CompareType.ARRAY_CONTAINS_ALL, + CompareType.ARRAY_CONTAINS_ANY, + CompareType.ARRAY_CONTAINS_NONE, + CompareType.ARRAY_MATCHES, + CompareType.ARRAY_NOT_MATCHES, CompareType.Q, WHERE, INDESCENDANTSOF, diff --git a/study/src/org/labkey/study/model/DatasetDomainKind.java b/study/src/org/labkey/study/model/DatasetDomainKind.java index be77b3f371d..c55fb877f22 100644 --- a/study/src/org/labkey/study/model/DatasetDomainKind.java +++ b/study/src/org/labkey/study/model/DatasetDomainKind.java @@ -257,6 +257,12 @@ public boolean allowFileLinkProperties() return true; } + @Override + public boolean allowMultiChoiceProperties() + { + return true; + } + @Override public boolean showDefaultValueSettings() {