Skip to content
Open
3 changes: 3 additions & 0 deletions api/src/org/labkey/api/data/ContainerManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -1952,7 +1952,10 @@ private static boolean delete(final Container c, User user, @Nullable String com

ExperimentService experimentService = ExperimentService.get();
if (experimentService != null)
{
experimentService.removeContainerDataTypeExclusions(c.getId());
experimentService.removeContainerDataColorExclusions(c.getId());
}

// Issue 17015: After we've committed the transaction, be sure that we remove this container from the cache
tx.addCommitTask(() ->
Expand Down
4 changes: 4 additions & 0 deletions api/src/org/labkey/api/exp/api/ExpMaterial.java
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,10 @@ public interface ExpMaterial extends ExpRunItem

void setSampleStateId(Long stateId);

Long getSampleColorId();

void setSampleColorId(Long colorId);

Date getMaterialExpDate();

ActionURL detailsURL(Container container, boolean checkForOverride);
Expand Down
21 changes: 21 additions & 0 deletions api/src/org/labkey/api/exp/api/ExperimentService.java
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,8 @@ public interface ExperimentService extends ExperimentRunTypeSource

String EXPERIMENTAL_FEATURE_ALLOW_ROW_ID_MERGE = "org.labkey.experiment.api.SampleTypeUpdateServiceDI#ALLOW_ROW_ID_SAMPLE_MERGE";

String EXPERIMENTAL_SAMPLE_COLORS = "org.labkey.api.exp.api.ExperimentService#SAMPLE_COLORS";

int SIMPLE_PROTOCOL_FIRST_STEP_SEQUENCE = 1;
int SIMPLE_PROTOCOL_CORE_STEP_SEQUENCE = 10;
int SIMPLE_PROTOCOL_EXTRA_STEP_SEQUENCE = 15;
Expand Down Expand Up @@ -710,6 +712,8 @@ static void validateParentAlias(Map<String, String> aliasMap, Set<String> reserv

SampleStatusTable createSampleStatusTable(ExpSchema expSchema, ContainerFilter cf);

TableInfo createDataColorTable(ExpSchema expSchema, ContainerFilter cf);

ExpUnreferencedSampleFilesTable createUnreferencedSampleFilesTable(ExpSchema expSchema, ContainerFilter cf);

FilteredTable<ExpSchema> createFieldsTable(ExpSchema expSchema, ContainerFilter cf);
Expand Down Expand Up @@ -1139,6 +1143,23 @@ List<? extends ExpProtocol> getExpProtocolsWithParameterValue(

String getDisabledDataTypeAuditMsg(DataTypeForExclusion type, List<Long> ids, boolean isUpdate);

@NotNull Set<Long> getDataTypeExcludedColors(DataTypeForExclusion dataType, long dataTypeId);

/** The data type rowIds (e.g. sample type rowIds) that currently exclude the given color. Inverse of {@link #getDataTypeExcludedColors}. */
@NotNull Set<Long> getDataTypesExcludingColor(DataTypeForExclusion dataType, long colorRowId);

@NotNull Set<Long> getActiveDataTypeColors(@NotNull Container container, DataTypeForExclusion dataType, long dataTypeId);

boolean ensureDataColorExclusions(long dataTypeId, DataTypeForExclusion dataType, @Nullable Collection<Long> disabledColorRowIds, @NotNull Container container, User user);

@NotNull Set<Long> updateColorDataTypeExclusions(long colorRowId, DataTypeForExclusion dataType, @Nullable Collection<Long> newlyDisabledDataTypeIds, @Nullable Collection<Long> newlyEnabledDataTypeIds, @NotNull Container container, User user);

void removeDataColorExclusionsForColor(long colorRowId);

void removeDataColorExclusionsForDataType(long dataTypeId, DataTypeForExclusion dataType);

void removeContainerDataColorExclusions(String containerId);

void registerRunInputsViewProvider(QueryViewProvider<ExpRun> provider);

void registerRunOutputsViewProvider(QueryViewProvider<ExpRun> providers);
Expand Down
5 changes: 4 additions & 1 deletion api/src/org/labkey/api/exp/api/SampleTypeDomainKind.java
Original file line number Diff line number Diff line change
Expand Up @@ -535,6 +535,8 @@ public Domain createDomain(GWTDomain<GWTPropertyDescriptor> domain, @Nullable Sa
Map<String, Map<String, Object>> aliases = null;
List<String> excludedContainerIds = null;
List<String> excludedDashboardContainerIds = null;
List<Integer> excludedSampleColorIds = null;


if (arguments != null)
{
Expand All @@ -556,12 +558,13 @@ public Domain createDomain(GWTDomain<GWTPropertyDescriptor> domain, @Nullable Sa
aliases = arguments.getImportAliases();
excludedContainerIds = arguments.getExcludedContainerIds();
excludedDashboardContainerIds = arguments.getExcludedDashboardContainerIds();
excludedSampleColorIds = arguments.getDisabledSampleColorRowIds();
}
ExpSampleType st;
try
{
st = SampleTypeService.get().createSampleType(container, user, name, description, properties, indices, idCol1, idCol2, idCol3, parentCol, nameExpression, aliquotNameExpression,
templateInfo, aliases, labelColor, metricUnit, autoLinkTargetContainer, autoLinkCategory, category, domain.getDisabledSystemFields(), excludedContainerIds, excludedDashboardContainerIds, arguments != null ? arguments.getAuditRecordMap() : null);
templateInfo, aliases, labelColor, metricUnit, autoLinkTargetContainer, autoLinkCategory, category, domain.getDisabledSystemFields(), excludedContainerIds, excludedDashboardContainerIds, excludedSampleColorIds, arguments != null ? arguments.getAuditRecordMap() : null);
}
catch (ExperimentException e)
{
Expand Down
11 changes: 11 additions & 0 deletions api/src/org/labkey/api/exp/api/SampleTypeDomainKindProperties.java
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ public SampleTypeDomainKindProperties(ExpSampleType st)
private String category;
private List<String> excludedContainerIds;
private List<String> excludedDashboardContainerIds;
private List<Integer> disabledSampleColorRowIds;

//Ignored on import/save, use Domain.name & Domain.description instead
private String name;
Expand Down Expand Up @@ -285,6 +286,16 @@ public List<String> getExcludedDashboardContainerIds()
return excludedDashboardContainerIds;
}

public List<Integer> getDisabledSampleColorRowIds()
{
return disabledSampleColorRowIds;
}

public void setDisabledSampleColorRowIds(List<Integer> disabledSampleColorRowIds)
{
this.disabledSampleColorRowIds = disabledSampleColorRowIds;
}

public void setExcludedDashboardContainerIds(List<String> excludedDashboardContainerIds)
{
this.excludedDashboardContainerIds = excludedDashboardContainerIds;
Expand Down
4 changes: 3 additions & 1 deletion api/src/org/labkey/api/exp/api/SampleTypeService.java
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,8 @@ static void setInstance(SampleTypeService impl)

Map<String, ExpSampleType> getSampleTypesForRoles(Container container, ContainerFilter filter, ExpProtocol.ApplicationType type);

void auditSampleColorExclusion(Container container, long sampleTypeRowId, @Nullable String auditUserComment, User user);

/**
* Create a new SampleType with the provided properties.
* If a 'Name' property exists in the list, it will be used as the 'id' property of the SampleType.
Expand Down Expand Up @@ -150,7 +152,7 @@ ExpSampleType createSampleType(Container container, User user, String name, Stri
ExpSampleType createSampleType(Container c, User u, String name, String description, List<GWTPropertyDescriptor> properties, List<GWTIndex> indices, int idCol1, int idCol2, int idCol3, int parentCol,
String nameExpression, String aliquotNameExpression, @Nullable TemplateInfo templateInfo, @Nullable Map<String, Map<String, Object>> importAliases, @Nullable String labelColor, @Nullable String metricUnit,
@Nullable Container autoLinkTargetContainer, @Nullable String autoLinkCategory, @Nullable String category, @Nullable List<String> disabledSystemField,
@Nullable List<String> excludedContainerIds, @Nullable List<String> excludedDashboardContainerIds, @Nullable Map<String, Object> changeDetails)
@Nullable List<String> excludedContainerIds, @Nullable List<String> excludedDashboardContainerIds, @Nullable List<Integer> excludedSampleColorIds, @Nullable Map<String, Object> changeDetails)
throws ExperimentException;

@NotNull
Expand Down
1 change: 1 addition & 0 deletions api/src/org/labkey/api/exp/query/ExpMaterialTable.java
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ enum Column
RunId, // database table only
RunApplication,
RunApplicationOutput,
ExpMaterialColor,
SampleSet,
SampleState,
SourceApplicationId, // database table only
Expand Down
8 changes: 8 additions & 0 deletions api/src/org/labkey/api/exp/query/ExpSchema.java
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,14 @@ public TableInfo createTable(ExpSchema expSchema, String queryName, ContainerFil
return ExperimentService.get().createSampleStatusTable(expSchema, cf);
}
},
DataColors
{
@Override
public TableInfo createTable(ExpSchema expSchema, String queryName, ContainerFilter cf)
{
return ExperimentService.get().createDataColorTable(expSchema, cf);
}
},
Fields
{
@Override
Expand Down
7 changes: 4 additions & 3 deletions api/src/org/labkey/api/query/AbstractQueryUpdateService.java
Original file line number Diff line number Diff line change
Expand Up @@ -596,7 +596,8 @@ protected DataIteratorBuilder _toDataIteratorBuilder(String debugName, List<Map<

/** @deprecated switch to using DIB based method */
@Deprecated
protected List<Map<String, Object>> _insertRowsUsingInsertRow(User user, Container container, List<Map<String, Object>> rows, BatchValidationException errors, Map<String, Object> extraScriptContext)
protected List<Map<String, Object>> _insertRowsUsingInsertRow(User user, Container container, List<Map<String, Object>> rows, BatchValidationException errors,
@Nullable Map<Enum, Object> configParameters, Map<String, Object> extraScriptContext)
throws DuplicateKeyException, BatchValidationException, QueryUpdateServiceException, SQLException
{
if (!hasInsertRowsPermission(user))
Expand Down Expand Up @@ -664,7 +665,7 @@ else if (SqlDialect.isTransactionException(sqlx) && errors.hasErrors())
if (hasTableScript)
getQueryTable().fireBatchTrigger(container, user, TableInfo.TriggerType.INSERT, null, false, errors, extraScriptContext);

addAuditEvent(user, container, QueryService.AuditAction.INSERT, null, result, null, providedValues);
addAuditEvent(user, container, QueryService.AuditAction.INSERT, configParameters, result, null, providedValues);

return result;
}
Expand Down Expand Up @@ -715,7 +716,7 @@ public List<Map<String, Object>> insertRows(User user, Container container, List
{
try
{
List<Map<String,Object>> ret = _insertRowsUsingInsertRow(user, container, rows, errors, extraScriptContext);
List<Map<String,Object>> ret = _insertRowsUsingInsertRow(user, container, rows, errors, configParameters, extraScriptContext);
afterInsertUpdate(null==ret?0:ret.size(), errors);
if (errors.hasErrors())
return null;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/*
* Copyright (c) 2026 LabKey Corporation
*
* Licensed under the Apache License, Version 2.0: http://www.apache.org/licenses/LICENSE-2.0
*/

CREATE TABLE exp.DataColors
(
RowId SERIAL NOT NULL,
Container ENTITYID NOT NULL,
Label VARCHAR(64) NOT NULL,
Color VARCHAR(7) NOT NULL,
Archived BOOLEAN NOT NULL DEFAULT FALSE,
Created TIMESTAMP,
CreatedBy INT,
Modified TIMESTAMP,
ModifiedBy INT,

CONSTRAINT PK_DataColors PRIMARY KEY (RowId),
CONSTRAINT UQ_DataColors_Label UNIQUE (Container, Label)
);

ALTER TABLE exp.Material ADD COLUMN ExpMaterialColor INT;
ALTER TABLE exp.Material ADD CONSTRAINT FK_Material_ExpMaterialColor FOREIGN KEY (ExpMaterialColor) REFERENCES exp.DataColors (RowId);

CREATE TABLE exp.DataTypeColorExclusion
(
RowId SERIAL NOT NULL,
Container ENTITYID NOT NULL,
DataTypeRowId INT NOT NULL,
DataType VARCHAR(20) NOT NULL,
ColorRowId INT NOT NULL,
Created TIMESTAMP,
CreatedBy INT,
Modified TIMESTAMP,
ModifiedBy INT,

CONSTRAINT PK_DataTypeColorExclusion PRIMARY KEY (RowId),
CONSTRAINT UQ_DataTypeColorExclusion UNIQUE (DataTypeRowId, DataType, ColorRowId)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should there be a FOREIGN KEY here for ColorRowId to the exp.DataColors table?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also, is the convention to have FKs to core.Containers? if so, this should be added for both of the new tables.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

finally, asking Claude to review this file specifically, it suggested some other missing indices:
CREATE INDEX IX_Material_ExpMaterialColor ON exp.Material (ExpMaterialColor);
CREATE INDEX IX_DataTypeColorExclusion_Container ON exp.DataTypeColorExclusion (Container);
CREATE INDEX IX_DataTypeColorExclusion_ColorRowId ON exp.DataTypeColorExclusion (ColorRowId);

);
30 changes: 30 additions & 0 deletions experiment/resources/schemas/exp.xml
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,9 @@
<column columnName="SampleState">
<description>Represents the status of the sample</description>
</column>
<column columnName="ExpMaterialColor">
<description>The color assigned to this individual sample</description>
</column>
<column columnName="StoredAmount">
<columnTitle>Amount</columnTitle>
</column>
Expand Down Expand Up @@ -1047,6 +1050,7 @@
<column columnName="RootMaterialRowId"/>
<column columnName="AliquotedFromLSID"/>
<column columnName="SampleState"/>
<column columnName="ExpMaterialColor"/>
<column columnName="AliquotCount"/>
<column columnName="AliquotVolume"/>
<column columnName="AliquotUnit"/>
Expand Down Expand Up @@ -1255,4 +1259,30 @@
<column columnName="ModifiedBy"/>
</columns>
</table>
<table tableName="DataColors" tableDbType="TABLE">
<columns>
<column columnName="RowId"/>
<column columnName="Container"/>
<column columnName="Label"/>
<column columnName="Color"/>
<column columnName="Archived"/>
<column columnName="Created"/>
<column columnName="CreatedBy"/>
<column columnName="Modified"/>
<column columnName="ModifiedBy"/>
</columns>
</table>
<table tableName="DataTypeColorExclusion" tableDbType="TABLE">
<columns>
<column columnName="RowId"/>
<column columnName="Container"/>
<column columnName="DataTypeRowId"/>
<column columnName="DataType"/>
<column columnName="ColorRowId"/>
<column columnName="Created"/>
<column columnName="CreatedBy"/>
<column columnName="Modified"/>
<column columnName="ModifiedBy"/>
</columns>
</table>
</tables>
26 changes: 25 additions & 1 deletion experiment/src/org/labkey/experiment/ExperimentModule.java
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@
import org.labkey.api.webdav.WebdavService;
import org.labkey.api.writer.ContainerUser;
import org.labkey.experiment.api.DataClassDomainKind;
import org.labkey.experiment.api.DataColorManager;
import org.labkey.experiment.api.EdgeDiagnosticsTestCase;
import org.labkey.experiment.api.ExpDataClassImpl;
import org.labkey.experiment.api.ExpDataClassTableImpl;
Expand Down Expand Up @@ -209,7 +210,7 @@ public String getName()
@Override
public Double getSchemaVersion()
{
return 26.007;
return 26.008;
}

@Nullable
Expand Down Expand Up @@ -295,6 +296,8 @@ protected void init()
"Support for querying lineage of experiment objects", false, true);
OptionalFeatureService.get().addExperimentalFeatureFlag(ExperimentService.EXPERIMENTAL_FEATURE_ALLOW_ROW_ID_MERGE, "Allow RowId to be accepted when merging samples or data class data",
"If the incoming data includes a RowId column we will allow the column but ignore it's values.", false, true);
OptionalFeatureService.get().addExperimentalFeatureFlag(ExperimentService.EXPERIMENTAL_SAMPLE_COLORS, "Sample Colors",
"Enable assigning custom colors to individual samples, with an app-level color palette configurable per sample type.", false, true);

RoleManager.registerPermission(new DesignVocabularyPermission(), true);
RoleManager.registerRole(new SampleTypeDesignerRole());
Expand Down Expand Up @@ -553,6 +556,8 @@ protected void startupAfterSpringConfig(ModuleContext moduleContext)
AuditLogService.get().registerAuditType(new SampleTypeAuditProvider());
AuditLogService.get().registerAuditType(new SampleTimelineAuditProvider());

DataColorManager.getInstance().registerHandler(SampleTypeServiceImpl.get());

FileContentService fileContentService = FileContentService.get();
if (null != fileContentService)
{
Expand Down Expand Up @@ -793,6 +798,24 @@ SELECT COUNT(DISTINCT DD.DomainURI) FROM
results.put("sampleTypesWithVolumeTypeUnit", new SqlSelector(schema, "SELECT COUNT(*) from exp.materialSource WHERE category IS NULL AND metricunit IN ('L', 'mL', 'uL')").getObject(Long.class));
results.put("sampleTypesWithCountTypeUnit", new SqlSelector(schema, "SELECT COUNT(*) from exp.materialSource WHERE category IS NULL AND metricunit = ?", "unit").getObject(Long.class));

Map<String, Object> sampleColorMetrics = new HashMap<>();
Long colorCount = new SqlSelector(schema, "SELECT COUNT(*) FROM exp.datacolors").getObject(Long.class);
sampleColorMetrics.put("colorCount", colorCount);
if (colorCount > 0)
{
Long archivedColorCount = new SqlSelector(schema, new SQLFragment("SELECT COUNT(*) FROM exp.datacolors WHERE archived = " + schema.getSqlDialect().getBooleanTRUE())).getObject(Long.class);
sampleColorMetrics.put("archivedColorCount", archivedColorCount);
sampleColorMetrics.put("samplesWithColorCount", new SqlSelector(schema, "SELECT COUNT(*) FROM exp.material WHERE expmaterialcolor IS NOT NULL").getObject(Long.class));
sampleColorMetrics.put("samplesWithArchivedColorCount", new SqlSelector(schema, new SQLFragment("SELECT COUNT(*) FROM exp.material m JOIN exp.datacolors dc ON m.expmaterialcolor = dc.rowid WHERE dc.archived = " + schema.getSqlDialect().getBooleanTRUE())).getObject(Long.class));

sampleColorMetrics.put("sampleTypesWithColorsEnabledCount", new SqlSelector(schema, new SQLFragment(
"SELECT COUNT(*) FROM exp.materialsource ms WHERE EXISTS (" +
"SELECT 1 FROM exp.datacolors dc WHERE dc.container = ms.container AND dc.archived = " + schema.getSqlDialect().getBooleanFALSE() + " AND NOT EXISTS (" +
"SELECT 1 FROM exp.datatypecolorexclusion e WHERE e.datatype = ? AND e.datatyperowid = ms.rowid AND e.colorrowid = dc.rowid))")
.add(ExperimentService.DataTypeForExclusion.SampleType.name())).getObject(Long.class));
}
results.put("sampleColors", sampleColorMetrics);

results.put("duplicateSampleMaterialNameCount", new SqlSelector(schema, "SELECT COUNT(*) as duplicateCount FROM " +
"(SELECT name, cpastype FROM exp.material WHERE cpastype <> 'Material' GROUP BY name, cpastype HAVING COUNT(*) > 1) d").getObject(Long.class));
results.put("duplicateSpecimenMaterialNameCount", new SqlSelector(schema, "SELECT COUNT(*) as duplicateCount FROM " +
Expand Down Expand Up @@ -1197,6 +1220,7 @@ public JSONObject getPageContextJson(ContainerUser context)
{
JSONObject json = super.getPageContextJson(context);
json.put(SAMPLE_FILES_TABLE, OptionalFeatureService.get().isFeatureEnabled(SAMPLE_FILES_TABLE));
json.put("SampleColors", OptionalFeatureService.get().isFeatureEnabled(ExperimentService.EXPERIMENTAL_SAMPLE_COLORS));
return json;
}
}
Loading