From 0303802d44be8bbfa3de495bc64972499e48ee28 Mon Sep 17 00:00:00 2001 From: Cory Nathe Date: Wed, 21 Jan 2026 09:38:23 -0600 Subject: [PATCH 1/3] GitHub Issue 778: Track selection loading state on DataRegion so we can disable buttons and prevent another click (#7335) - Track selection loading state on DataRegion so we can disable buttons and prevent another click - DataRegion selectAll to use parse non-JSON response --- api/webapp/clientapi/dom/DataRegion.js | 97 ++++++++++++++++++-------- 1 file changed, 68 insertions(+), 29 deletions(-) diff --git a/api/webapp/clientapi/dom/DataRegion.js b/api/webapp/clientapi/dom/DataRegion.js index 29903cbbcd4..a408c2fb949 100644 --- a/api/webapp/clientapi/dom/DataRegion.js +++ b/api/webapp/clientapi/dom/DataRegion.js @@ -493,6 +493,7 @@ if (!LABKEY.DataRegions) { * Non-configurable Options */ this.selectionModified = false; + this.selectionLoading = false; if (this.panelConfigurations === undefined) { this.panelConfigurations = {}; @@ -1171,19 +1172,11 @@ if (!LABKEY.DataRegions) { config.selectionKey = this.selectionKey; config.scope = config.scope || me; - config = _chainSelectionCountCallback(this, config); + // set loading flag for DataRegion, will be cleared in callback + this.selectionLoading = true; + _updateSelectedCountMessage(this); - var failure = LABKEY.Utils.getOnFailure(config); - if ($.isFunction(failure)) { - config.failure = failure; - } - else { - config.failure = function(error) { - let msg = 'Error setting selection'; - if (error && error.exception) msg += ': ' + error.exception; - me.addMessage(msg, 'selection'); - }; - } + config = _chainSelectionCountCallback(this, config); if (config.selectionKey) { LABKEY.DataRegion.setSelected(config); @@ -3058,12 +3051,39 @@ if (!LABKEY.DataRegions) { var _chainSelectionCountCallback = function(region, config) { - var success = LABKEY.Utils.getOnSuccess(config); + const failure = LABKEY.Utils.getOnFailure(config); + config.failure = function(error) { + region.selectionLoading = false; + + let msg = 'Error setting selection'; + if (error && error.exception) msg += ': ' + error.exception; + config.scope.addMessage(msg, 'selection'); + + if ($.isFunction(failure)) { + failure.call(config.scope, error); + } + } // On success, update the current selectedCount on this DataRegion and fire the 'selectchange' event - config.success = function(data) { + const success = LABKEY.Utils.getOnSuccess(config); + config.success = function(data, response) { + + // Workaround for GitHub Issue 778 where the response payload is JSON but the response has been + // configured by the server as non-JSON. + if (!data && response?.responseText) { + try { + data = JSON.parse(response.responseText); + } catch (e) { + const msg = 'failed to parse response'; + console.error(msg, e, response); + config.failure.call(config.scope, { exception: msg }); + return; + } + } + region.removeMessage('selection'); region.selectionModified = true; + region.selectionLoading = false; region.selectedCount = data.count; _onSelectionChange(region); @@ -3363,7 +3383,10 @@ if (!LABKEY.DataRegions) { var _buttonSelectionBind = function(region, cls, fn) { var partEl = region.msgbox.getParent().find('div[data-msgpart="selection"]'); partEl.find('.labkey-button' + cls).off('click').on('click', $.proxy(function() { - fn.call(this); + // if one of the buttons is clicked while another action is loading selections, skip the click (the button should also be disabled) + if (!region.selectionLoading) { + fn.call(this); + } }, region)); }; @@ -3592,18 +3615,20 @@ if (!LABKEY.DataRegions) { var _showSelectMessage = function(region, msg) { if (region.showRecordSelectors) { + const cls = 'labkey-button ' + (region.selectionLoading ? 'disabled ' : ''); + if (_isShowSelectAll(region)) { - msg += " " + _getSelectAllText(region) + ""; + msg += " " + _getSelectAllText(region) + ""; } - msg += " " + "Select None"; + msg += " " + "Select None"; var showOpts = []; if (region.showRows !== 'all' && !_isMaxRowsAllRows(region)) - showOpts.push("Show All"); + showOpts.push("Show All"); if (region.showRows !== 'selected') - showOpts.push("Show Selected"); + showOpts.push("Show Selected"); if (region.showRows !== 'unselected') - showOpts.push("Show Unselected"); + showOpts.push("Show Unselected"); msg += "  " + showOpts.join(" "); } @@ -4081,6 +4106,21 @@ if (!LABKEY.DataRegions) { _setParameters(region, params, [OFFSET_PREFIX].concat(skipPrefixes)); }; + var _updateSelectedCountMessage = function(region) { + // If not all rows are visible and some rows are selected, show selection message + if (region.totalRows && 0 !== region.selectedCount && !region.complete) { + var msg; + if (region.selectedCount === region.totalRows) { + msg = 'All ' + region.totalRows.toLocaleString() + ' rows selected.'; + } else if (region.selectionLoading) { + msg = 'Selected of ' + region.totalRows.toLocaleString() + ' rows.'; + } else { + msg = 'Selected ' + region.selectedCount.toLocaleString() + ' of ' + region.totalRows.toLocaleString() + ' rows.'; + } + _showSelectMessage(region, msg); + } + } + var _updateRequiresSelectionButtons = function(region, selectedCount) { // update the 'select all on page' checkbox state @@ -4100,13 +4140,7 @@ if (!LABKEY.DataRegions) { } }); - // If not all rows are visible and some rows are selected, show selection message - if (region.totalRows && 0 !== region.selectedCount && !region.complete) { - var msg = (region.selectedCount === region.totalRows) ? - 'All ' + region.totalRows.toLocaleString() + ' rows selected.' : - 'Selected ' + region.selectedCount.toLocaleString() + ' of ' + region.totalRows.toLocaleString() + ' rows.'; - _showSelectMessage(region, msg); - } + _updateSelectedCountMessage(region); // Issue 10566: for javascript perf on IE stash the requires selection buttons if (!region._requiresSelectionButtons) { @@ -4446,6 +4480,11 @@ if (!LABKEY.DataRegions) { }; LABKEY.DataRegion.selectAll = function(config) { + // GitHub Issue 778: Track selection loading state on DataRegion so we can disable buttons and prevent another click + var region = config.scope; + region.selectionLoading = true; + _updateSelectedCountMessage(region); + var params = {}; if (!config.url) { // DataRegion doesn't have selectAllURL so generate url and query parameters manually @@ -4485,8 +4524,8 @@ if (!LABKEY.DataRegions) { url: config.url, method: 'POST', params: params, - success: LABKEY.Utils.getCallbackWrapper(LABKEY.Utils.getOnSuccess(config), config.scope), - failure: LABKEY.Utils.getCallbackWrapper(LABKEY.Utils.getOnFailure(config), config.scope, true) + success: LABKEY.Utils.getCallbackWrapper(LABKEY.Utils.getOnSuccess(config), region), + failure: LABKEY.Utils.getCallbackWrapper(LABKEY.Utils.getOnFailure(config), region, true) }); }; From e911d7b3f7c832316bcfc605818803317f40a5a0 Mon Sep 17 00:00:00 2001 From: Susan Hert Date: Wed, 21 Jan 2026 11:16:33 -0800 Subject: [PATCH 2/3] Issue 695: Add feature to allow unreferenced files to be deleted from apps (#7320) --- .../org/labkey/api/exp/OntologyManager.java | 1 + .../labkey/api/exp/api/ExperimentService.java | 4 +- .../labkey/api/exp/query/ExpDataTable.java | 1 + .../org/labkey/api/exp/query/ExpSchema.java | 16 +++ .../ExpUnreferencedSampleFilesTable.java | 8 ++ .../labkey/api/files/FileContentService.java | 10 ++ .../org/labkey/api/files/FileListener.java | 5 + .../api/files/TableUpdaterFileListener.java | 26 +++- .../experiment/ExpDataFileListener.java | 7 + .../labkey/experiment/ExperimentModule.java | 14 ++ .../experiment/FileLinkFileListener.java | 26 +++- .../experiment/api/ExpDataTableImpl.java | 18 +++ .../ExpUnreferencedSampleFilesTableImpl.java | 125 ++++++++++++++++++ .../experiment/api/ExperimentServiceImpl.java | 10 ++ .../ReferenceCountDisplayColumnFactory.java | 53 ++++++++ .../experiment/api/SampleTypeServiceImpl.java | 2 +- .../controllers/exp/ExperimentController.java | 22 ++- .../filecontent/FileContentServiceImpl.java | 100 ++++++++++++-- 18 files changed, 422 insertions(+), 26 deletions(-) create mode 100644 api/src/org/labkey/api/exp/query/ExpUnreferencedSampleFilesTable.java create mode 100644 experiment/src/org/labkey/experiment/api/ExpUnreferencedSampleFilesTableImpl.java create mode 100644 experiment/src/org/labkey/experiment/api/ReferenceCountDisplayColumnFactory.java diff --git a/api/src/org/labkey/api/exp/OntologyManager.java b/api/src/org/labkey/api/exp/OntologyManager.java index 26d1b6d43ec..e2e4119d62e 100644 --- a/api/src/org/labkey/api/exp/OntologyManager.java +++ b/api/src/org/labkey/api/exp/OntologyManager.java @@ -1332,6 +1332,7 @@ public static void deleteAllObjects(Container c, User user) throws ValidationExc String deleteObjPropSql = "DELETE FROM " + getTinfoObjectProperty() + " WHERE ObjectId IN (SELECT ObjectId FROM " + getTinfoObject() + " WHERE Container = ?)"; executor.execute(deleteObjPropSql, c); String deleteObjSql = "DELETE FROM " + getTinfoObject() + " WHERE Container = ?"; + _log.info("Deleting from exp.object in container {}", c); executor.execute(deleteObjSql, c); // delete property validator references on property descriptors diff --git a/api/src/org/labkey/api/exp/api/ExperimentService.java b/api/src/org/labkey/api/exp/api/ExperimentService.java index bdf36dc10f8..c2045444f9a 100644 --- a/api/src/org/labkey/api/exp/api/ExperimentService.java +++ b/api/src/org/labkey/api/exp/api/ExperimentService.java @@ -60,6 +60,7 @@ import org.labkey.api.exp.query.ExpRunTable; import org.labkey.api.exp.query.ExpSampleTypeTable; import org.labkey.api.exp.query.ExpSchema; +import org.labkey.api.exp.query.ExpUnreferencedSampleFilesTable; import org.labkey.api.exp.query.SampleStatusTable; import org.labkey.api.gwt.client.AuditBehaviorType; import org.labkey.api.gwt.client.model.GWTDomain; @@ -81,7 +82,6 @@ import org.labkey.api.util.Pair; import org.labkey.api.util.StringUtilsLabKey; import org.labkey.api.view.HttpView; -import org.labkey.api.view.NotFoundException; import org.labkey.api.view.ViewBackgroundInfo; import org.labkey.api.view.ViewContext; import org.labkey.vfs.FileLike; @@ -670,6 +670,8 @@ static void validateParentAlias(Map aliasMap, Set reserv SampleStatusTable createSampleStatusTable(ExpSchema expSchema, ContainerFilter cf); + ExpUnreferencedSampleFilesTable createUnreferencedSampleFilesTable(ExpSchema expSchema, ContainerFilter cf); + FilteredTable createFieldsTable(ExpSchema expSchema, ContainerFilter cf); FilteredTable createPhiFieldsTable(ExpSchema expSchema, ContainerFilter cf); diff --git a/api/src/org/labkey/api/exp/query/ExpDataTable.java b/api/src/org/labkey/api/exp/query/ExpDataTable.java index b60aa86b18c..7169b39a81b 100644 --- a/api/src/org/labkey/api/exp/query/ExpDataTable.java +++ b/api/src/org/labkey/api/exp/query/ExpDataTable.java @@ -35,6 +35,7 @@ enum Column SourceProtocolApplication, SourceApplicationInput, DataFileUrl, + ReferenceCount, Run, RunApplication, RunApplicationOutput, diff --git a/api/src/org/labkey/api/exp/query/ExpSchema.java b/api/src/org/labkey/api/exp/query/ExpSchema.java index 5fd2cedf774..adedd2fc46a 100644 --- a/api/src/org/labkey/api/exp/query/ExpSchema.java +++ b/api/src/org/labkey/api/exp/query/ExpSchema.java @@ -47,6 +47,7 @@ import org.labkey.api.security.User; import org.labkey.api.security.permissions.InsertPermission; import org.labkey.api.security.permissions.ReadPermission; +import org.labkey.api.settings.AppProps; import org.labkey.api.util.StringExpression; import org.labkey.api.view.ActionURL; import org.labkey.api.view.ViewContext; @@ -70,6 +71,7 @@ public class ExpSchema extends AbstractExpSchema public static final String SAMPLE_STATE_TYPE_TABLE = "SampleStateType"; public static final String SAMPLE_TYPE_CATEGORY_TABLE = "SampleTypeCategoryType"; public static final String MEASUREMENT_UNITS_TABLE = "MeasurementUnits"; + public static final String SAMPLE_FILES_TABLE = "UnreferencedSampleFiles"; public static final SchemaKey SCHEMA_EXP = SchemaKey.fromParts(ExpSchema.SCHEMA_NAME); public static final SchemaKey SCHEMA_EXP_DATA = SchemaKey.fromString(SCHEMA_EXP, ExpSchema.NestedSchemas.data.name()); @@ -220,6 +222,20 @@ public TableInfo createTable(ExpSchema expSchema, String queryName, ContainerFil return expSchema.setupTable(result); } }, + UnreferencedSampleFiles + { + @Override + public TableInfo createTable(ExpSchema expSchema, String queryName, ContainerFilter cf) + { + return ExperimentService.get().createUnreferencedSampleFilesTable(expSchema, cf); + } + + @Override + public boolean includeTable() + { + return AppProps.getInstance().isOptionalFeatureEnabled(SAMPLE_FILES_TABLE); + } + }, SampleStatus { @Override diff --git a/api/src/org/labkey/api/exp/query/ExpUnreferencedSampleFilesTable.java b/api/src/org/labkey/api/exp/query/ExpUnreferencedSampleFilesTable.java new file mode 100644 index 00000000000..49574b391e4 --- /dev/null +++ b/api/src/org/labkey/api/exp/query/ExpUnreferencedSampleFilesTable.java @@ -0,0 +1,8 @@ +package org.labkey.api.exp.query; + +import org.labkey.api.data.ContainerFilterable; +import org.labkey.api.data.TableInfo; + +public interface ExpUnreferencedSampleFilesTable extends ContainerFilterable, TableInfo +{ +} diff --git a/api/src/org/labkey/api/files/FileContentService.java b/api/src/org/labkey/api/files/FileContentService.java index 9791ba68902..fbb60796bbb 100644 --- a/api/src/org/labkey/api/files/FileContentService.java +++ b/api/src/org/labkey/api/files/FileContentService.java @@ -325,6 +325,8 @@ default void fireFileDeletedEvent(@NotNull Path deleted, @Nullable User user, @N */ SQLFragment listFilesQuery(@NotNull User currentUser); + SQLFragment listSampleFilesQuery(@NotNull User currentUser); + void setWebfilesEnabled(boolean enabled, User user); /** @@ -345,6 +347,14 @@ enum PathType { full, serverRelative, folderRelative } */ void ensureFileData(@NotNull ExpDataTable table); + /** + * Fix the container column in the exp.data table for files that were moved as part of a sample move operation + * but did not have their containers updated + * @param admin The user doing the repair + * @return Number of duplicate rows removed from exp.data table + */ + int fixContainerForExpDataFiles(User admin); + /** * Allows a module to register a directory pattern to be checked in the files webpart in order to zip the matching directory before uploading. * @param directoryPattern DirectoryPattern diff --git a/api/src/org/labkey/api/files/FileListener.java b/api/src/org/labkey/api/files/FileListener.java index 858d1f54306..91702ed4b94 100644 --- a/api/src/org/labkey/api/files/FileListener.java +++ b/api/src/org/labkey/api/files/FileListener.java @@ -94,4 +94,9 @@ default void fileDeleted(@NotNull Path deleted, @Nullable User user, @Nullable C * */ SQLFragment listFilesQuery(); + + @Nullable default SQLFragment listSampleFilesQuery() + { + return null; + } } diff --git a/api/src/org/labkey/api/files/TableUpdaterFileListener.java b/api/src/org/labkey/api/files/TableUpdaterFileListener.java index ce46e143c87..b4cc573e01f 100644 --- a/api/src/org/labkey/api/files/TableUpdaterFileListener.java +++ b/api/src/org/labkey/api/files/TableUpdaterFileListener.java @@ -16,7 +16,6 @@ package org.labkey.api.files; import org.apache.commons.lang3.StringUtils; -import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -34,6 +33,7 @@ import org.labkey.api.data.dialect.SqlDialect; import org.labkey.api.security.User; import org.labkey.api.util.FileUtil; +import org.labkey.api.util.logging.LogHelper; import java.io.File; import java.nio.file.Files; @@ -45,13 +45,13 @@ /** * FileListener implementation that can update tables that store file paths in various flavors (URI, standard OS - * paths, etc). + * paths, etc.). * User: jeckels * Date: 11/7/12 */ public class TableUpdaterFileListener implements FileListener { - private static final Logger LOG = LogManager.getLogger(TableUpdaterFileListener.class); + protected static final Logger LOG = LogHelper.getLogger(TableUpdaterFileListener.class, "File listener activity"); public static final String TABLE_ALIAS = "x"; @@ -273,7 +273,7 @@ public int fileMoved(@NotNull Path src, @NotNull Path dest, @Nullable User user, singleEntrySQL.append(")"); int rows = schema.getScope().executeWithRetry(tx -> new SqlExecutor(schema).execute(singleEntrySQL)); - LOG.info("Updated " + rows + " row in " + _table + " for move from " + src + " to " + dest); + LOG.info("Updated {} row in {} for move from {} to {}", rows, _table, src, dest); // Handle updating child paths, unless we know that the entry is a file. If it's not (either it's a // directory or it doesn't exist), then try to fix up child records @@ -305,7 +305,7 @@ public int fileMoved(@NotNull Path src, @NotNull Path dest, @Nullable User user, childPathsSQL.append(whereClause); childRowsUpdated += new SqlExecutor(schema).execute(childPathsSQL); - LOG.info("Updated " + childRowsUpdated + " child paths in " + _table + " rows for move from " + src + " to " + dest); + LOG.info("Updated {} child paths in {} rows for move from {} to {}", childRowsUpdated, _table, src, dest); return childRowsUpdated; } return 0; @@ -353,10 +353,10 @@ public Collection listFiles(@Nullable Container container) @Override public SQLFragment listFilesQuery() { - return listFilesQuery(false, null); + return listFilesQuery(false, null, false); } - public SQLFragment listFilesQuery(boolean skipCreatedModified, String filePath) + public SQLFragment listFilesQuery(boolean skipCreatedModified, CharSequence filePath, boolean extractName) { SQLFragment selectFrag = new SQLFragment(); selectFrag.append("SELECT\n"); @@ -395,6 +395,16 @@ else if (_table.getColumn("Folder") != null) selectFrag.append(" ").appendIdentifier(_pathColumn.getSelectIdentifier()).append(" AS FilePath,\n"); + if (extractName) + { + SqlDialect dialect = _table.getSchema().getSqlDialect(); + SQLFragment fileNameFrag = new SQLFragment(); + fileNameFrag.append("regexp_replace(").appendIdentifier(_pathColumn.getSelectIdentifier()).append(", "); + fileNameFrag.append(dialect.getStringHandler().quoteStringLiteral(".*/")).append(", "); + fileNameFrag.append(dialect.getStringHandler().quoteStringLiteral("")).append(")"); + selectFrag.append(" ").append(fileNameFrag).append(" AS FilePathShort,\n"); + } + if (_keyColumn != null) selectFrag.append(" ").appendIdentifier(_keyColumn.getSelectIdentifier()).append(" AS SourceKey,\n"); else @@ -408,6 +418,8 @@ else if (_table.getColumn("Folder") != null) if (StringUtils.isEmpty(filePath)) selectFrag.append(" IS NOT NULL\n"); + else if (filePath instanceof SQLFragment) + selectFrag.append(" = ").append(filePath).append("\n"); else selectFrag.append(" = ").appendStringLiteral(filePath, _table.getSchema().getSqlDialect()).append("\n"); diff --git a/experiment/src/org/labkey/experiment/ExpDataFileListener.java b/experiment/src/org/labkey/experiment/ExpDataFileListener.java index 42f66d904db..8f1c54d08a6 100644 --- a/experiment/src/org/labkey/experiment/ExpDataFileListener.java +++ b/experiment/src/org/labkey/experiment/ExpDataFileListener.java @@ -27,6 +27,7 @@ import java.io.File; import java.nio.file.Files; import java.nio.file.Path; +import java.util.List; import static org.labkey.api.files.FileContentService.UPLOADED_FILE; @@ -88,7 +89,13 @@ public int fileMoved(@NotNull Path src, @NotNull Path dest, @Nullable User user, data.setName(FileUtil.getFileName(dest)); // if the data object moved containers, set that as well if (targetContainer != null && !targetContainer.equals(sourceContainer)) + { + LOG.info("Updating container for file {} from {} to {}.", dest, sourceContainer, targetContainer); data.setContainer(targetContainer); + ExperimentService svc = ExperimentService.get(); + LOG.info("Updating object container for objectId {} from {} to {}.", data.getObjectId(), sourceContainer, targetContainer); + svc.updateExpObjectContainers(svc.getTinfoData(), List.of(data.getRowId()), targetContainer); + } data.save(user); extra = 1; } diff --git a/experiment/src/org/labkey/experiment/ExperimentModule.java b/experiment/src/org/labkey/experiment/ExperimentModule.java index 435448ca0b7..90312720f61 100644 --- a/experiment/src/org/labkey/experiment/ExperimentModule.java +++ b/experiment/src/org/labkey/experiment/ExperimentModule.java @@ -18,6 +18,7 @@ import org.apache.commons.lang3.math.NumberUtils; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import org.json.JSONObject; import org.labkey.api.admin.FolderSerializationRegistry; import org.labkey.api.assay.AssayProvider; import org.labkey.api.assay.AssayService; @@ -112,6 +113,7 @@ import org.labkey.api.vocabulary.security.DesignVocabularyPermission; import org.labkey.api.webdav.WebdavResource; import org.labkey.api.webdav.WebdavService; +import org.labkey.api.writer.ContainerUser; import org.labkey.experiment.api.DataClassDomainKind; import org.labkey.experiment.api.ExpDataClassImpl; import org.labkey.experiment.api.ExpDataClassTableImpl; @@ -181,6 +183,7 @@ import static org.labkey.api.data.ColumnRenderPropertiesImpl.STORAGE_UNIQUE_ID_CONCEPT_URI; import static org.labkey.api.data.ColumnRenderPropertiesImpl.TEXT_CHOICE_CONCEPT_URI; import static org.labkey.api.exp.api.ExperimentService.MODULE_NAME; +import static org.labkey.api.exp.query.ExpSchema.SAMPLE_FILES_TABLE; public class ExperimentModule extends SpringModule { @@ -266,6 +269,9 @@ protected void init() } else { + OptionalFeatureService.get().addExperimentalFeatureFlag(SAMPLE_FILES_TABLE, "Manage Unreferenced Sample Files", + "Enable 'Unreferenced Sample Files' table to view and delete sample files that are no longer referenced by samples", false); + 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); } @@ -1117,4 +1123,12 @@ public Collection getProvisionedSchemaNames() { return PageFlowUtil.set(DataClassDomainKind.PROVISIONED_SCHEMA_NAME, SampleTypeDomainKind.PROVISIONED_SCHEMA_NAME); } + + @Override + public JSONObject getPageContextJson(ContainerUser context) + { + JSONObject json = super.getPageContextJson(context); + json.put(SAMPLE_FILES_TABLE, OptionalFeatureService.get().isFeatureEnabled(SAMPLE_FILES_TABLE)); + return json; + } } diff --git a/experiment/src/org/labkey/experiment/FileLinkFileListener.java b/experiment/src/org/labkey/experiment/FileLinkFileListener.java index 4a19b2b4ccb..83b1e7ff358 100644 --- a/experiment/src/org/labkey/experiment/FileLinkFileListener.java +++ b/experiment/src/org/labkey/experiment/FileLinkFileListener.java @@ -275,7 +275,7 @@ public SQLFragment listFilesQuery(boolean skipCreatedModified) return listFilesQuery(skipCreatedModified, null); } - public SQLFragment listFilesQuery(boolean skipCreatedModified, String filePath) + public SQLFragment listFilesQuery(boolean skipCreatedModified, CharSequence filePath) { final SQLFragment frag = new SQLFragment(); @@ -298,8 +298,11 @@ public SQLFragment listFilesQuery(boolean skipCreatedModified, String filePath) frag.append("WHERE\n"); if (StringUtils.isEmpty(filePath)) frag.append(" op.StringValue IS NOT NULL AND\n"); + else if (filePath instanceof SQLFragment) + frag.append(" op.StringValue = ").append(filePath).append(" AND\n"); else frag.append(" op.StringValue = ").appendStringLiteral(filePath, OntologyManager.getTinfoObject().getSqlDialect()).append(" AND\n"); + frag.append(" o.ObjectId = op.ObjectId AND\n"); frag.append(" PropertyId IN (\n"); frag.append(" SELECT PropertyId\n"); @@ -311,7 +314,26 @@ public SQLFragment listFilesQuery(boolean skipCreatedModified, String filePath) SQLFragment containerFrag = new SQLFragment("?", containerId); TableUpdaterFileListener updater = new TableUpdaterFileListener(table, pathColumn.getColumnName(), TableUpdaterFileListener.Type.filePath, null, containerFrag); frag.append("UNION").append(StringUtils.isEmpty(filePath) ? "" : " ALL" /*keep duplicate*/).append("\n"); - frag.append(updater.listFilesQuery(skipCreatedModified, filePath)); + frag.append(updater.listFilesQuery(skipCreatedModified, filePath, false)); + }); + + return frag; + } + + @Override + public SQLFragment listSampleFilesQuery() + { + final SQLFragment frag = new SQLFragment(); + + hardTableFileLinkColumns((schema, table, pathColumn, containerId, domainUri) -> { + if (PROVISIONED_SCHEMA_NAME.equalsIgnoreCase(schema.getName())) + { + SQLFragment containerFrag = new SQLFragment("?", containerId); + TableUpdaterFileListener updater = new TableUpdaterFileListener(table, pathColumn.getColumnName(), TableUpdaterFileListener.Type.filePath, "rowid", containerFrag); + if (!frag.isEmpty()) + frag.append("UNION").append("").append("\n"); + frag.append(updater.listFilesQuery(true, null, true)); + } }); return frag; diff --git a/experiment/src/org/labkey/experiment/api/ExpDataTableImpl.java b/experiment/src/org/labkey/experiment/api/ExpDataTableImpl.java index 1896b1eb5b6..12b775493c9 100644 --- a/experiment/src/org/labkey/experiment/api/ExpDataTableImpl.java +++ b/experiment/src/org/labkey/experiment/api/ExpDataTableImpl.java @@ -34,12 +34,14 @@ import org.labkey.api.data.Container; import org.labkey.api.data.ContainerFilter; import org.labkey.api.data.ContainerManager; +import org.labkey.api.data.CoreSchema; import org.labkey.api.data.ExcelWriter; import org.labkey.api.data.JdbcType; import org.labkey.api.data.MutableColumnInfo; import org.labkey.api.data.RenderContext; import org.labkey.api.data.SQLFragment; import org.labkey.api.data.SimpleFilter; +import org.labkey.api.data.SqlSelector; import org.labkey.api.exp.DomainDescriptor; import org.labkey.api.exp.OntologyManager; import org.labkey.api.exp.PropertyColumn; @@ -75,6 +77,7 @@ import org.labkey.api.security.User; import org.labkey.api.security.permissions.InsertPermission; import org.labkey.api.security.permissions.UpdatePermission; +import org.labkey.api.settings.AppProps; import org.labkey.api.util.ConfigurationException; import org.labkey.api.util.FileUtil; import org.labkey.api.util.HtmlString; @@ -84,6 +87,7 @@ import org.labkey.api.writer.HtmlWriter; import org.labkey.api.writer.MemoryVirtualFile; import org.labkey.api.writer.VirtualFile; +import org.labkey.experiment.FileLinkFileListener; import org.labkey.experiment.controllers.exp.ExperimentController; import org.labkey.experiment.lineage.LineageMethod; import org.springframework.beans.MutablePropertyValues; @@ -200,6 +204,8 @@ public List addFileColumns(boolean isFilesTable) addColumn(Column.FileExtension); addColumn(Column.WebDavUrl); addColumn(Column.WebDavUrlRelative); + if (AppProps.getInstance().isOptionalFeatureEnabled(ExpSchema.SAMPLE_FILES_TABLE)) + addColumn(getFileLinkReferenceCountColumn()); var flagCol = addColumn(Column.Flag); if (isFilesTable) flagCol.setLabel("Description"); @@ -227,6 +233,18 @@ public List addFileColumns(boolean isFilesTable) return customProps; } + // This is included in exp.data, not just exp.files because we want to be able to show a filtered view of + // sample files from our applications, and exp.files will not show subfolders + private MutableColumnInfo getFileLinkReferenceCountColumn() + { + var result = wrapColumn(Column.ReferenceCount.name(), _rootTable.getColumn("RowId")); + result.setDescription("The number of references to this file from File fields in any domain."); + result.setJdbcType(JdbcType.INTEGER); + result.setHidden(true); + result.setDisplayColumnFactory(new ReferenceCountDisplayColumnFactory()); + return result; + } + @Override public boolean supportTableRules() // intentional override { diff --git a/experiment/src/org/labkey/experiment/api/ExpUnreferencedSampleFilesTableImpl.java b/experiment/src/org/labkey/experiment/api/ExpUnreferencedSampleFilesTableImpl.java new file mode 100644 index 00000000000..fb4cbe5ab2f --- /dev/null +++ b/experiment/src/org/labkey/experiment/api/ExpUnreferencedSampleFilesTableImpl.java @@ -0,0 +1,125 @@ +package org.labkey.experiment.api; + +import org.apache.commons.lang3.StringUtils; +import org.jetbrains.annotations.NotNull; +import org.labkey.api.data.BaseColumnInfo; +import org.labkey.api.data.ContainerFilter; +import org.labkey.api.data.CoreSchema; +import org.labkey.api.data.JdbcType; +import org.labkey.api.data.SQLFragment; +import org.labkey.api.data.SqlSelector; +import org.labkey.api.data.TableInfo; +import org.labkey.api.data.VirtualTable; +import org.labkey.api.exp.api.ExpData; +import org.labkey.api.exp.api.ExperimentService; +import org.labkey.api.exp.query.ExpSchema; +import org.labkey.api.exp.query.ExpUnreferencedSampleFilesTable; +import org.labkey.api.files.FileContentService; +import org.labkey.api.query.AliasedColumn; +import org.labkey.api.query.FilteredTable; +import org.labkey.api.query.UserIdQueryForeignKey; +import org.labkey.api.query.column.BuiltInColumnTypes; +import org.labkey.api.writer.HtmlWriter; +import org.labkey.experiment.FileLinkFileListener; + + +public class ExpUnreferencedSampleFilesTableImpl extends FilteredTable implements ExpUnreferencedSampleFilesTable +{ + public ExpUnreferencedSampleFilesTableImpl(@NotNull ExpSchema schema, ContainerFilter cf) + { + super(createVirtualTable(schema), schema, cf); + setDescription("Contains all sample files that are not referenced by any domain fields."); + wrapAllColumns(true); + } + + private static TableInfo createVirtualTable(@NotNull ExpSchema schema) + { + return new ExpUnreferencedSampleFilesTableImpl.FileUnionTable(schema); + } + + private static class FileUnionTable extends VirtualTable + { + private final SQLFragment _query; + + public FileUnionTable(@NotNull ExpSchema schema) + { + super(CoreSchema.getInstance().getSchema(), ExpSchema.SAMPLE_FILES_TABLE, schema); + + FileContentService svc = FileContentService.get(); + + _query = new SQLFragment(); + if (svc == null) + return; + + SQLFragment listQuery = svc.listSampleFilesQuery(schema.getUser()); + if (StringUtils.isEmpty(listQuery)) + return; + + TableInfo expDataTable = ExperimentService.get().getTinfoData(); + TableInfo materialTable = ExperimentService.get().getTinfoMaterial(); + + _query.appendComment("", getSchema().getSqlDialect()); + + SQLFragment sampleFileSql = new SQLFragment("SELECT m.Container, if.FilePathShort \n") + .append("FROM (") + .append(svc.listSampleFilesQuery(schema.getUser())) + .append(") AS if \n") + .append("JOIN ") + .append(materialTable, "m") + .append(" ON if.SourceKey = m.RowId"); + + SQLFragment unreferencedFileSql = new SQLFragment("SELECT ed.rowId, ed.name as filename, ed.container, ed.created, ed.createdBy, ed.DataFileUrl FROM ") + .append(expDataTable, "ed") + .append(" LEFT JOIN (") + .append(sampleFileSql) + .append(" ) sf\n") + .append(" ON ed.name = sf.FilePathShort AND ed.container = sf.container\n") + .append(" WHERE ed.datafileurl LIKE ") + .appendValue("%@files/sampletype/%") + .append(" AND sf.FilePathShort IS NULL"); + + _query.append(unreferencedFileSql); + + _query.appendComment("", getSchema().getSqlDialect()); + + var rowIdCol = new BaseColumnInfo("RowId", this, JdbcType.INTEGER); + rowIdCol.setHidden(true); + rowIdCol.setKeyField(true); + addColumn(rowIdCol); + + var fileNameCol = new BaseColumnInfo("FileName", this, JdbcType.VARCHAR); + addColumn(fileNameCol); + + if (schema.getUser().hasApplicationAdminPermission()) + { + var filePathCol = new BaseColumnInfo("DataFileUrl", this, JdbcType.VARCHAR); + filePathCol.setHidden(true); + addColumn(filePathCol); + } + + var containerCol = new BaseColumnInfo("Container", this, JdbcType.VARCHAR); + containerCol.setConceptURI(BuiltInColumnTypes.CONTAINERID_CONCEPT_URI); + addColumn(containerCol); + + var createdCol = new BaseColumnInfo("Created", this, JdbcType.DATE); + addColumn(createdCol); + + var createdByCol = new BaseColumnInfo("CreatedBy", this, JdbcType.INTEGER); + createdByCol.setFk(new UserIdQueryForeignKey(getUserSchema(), true)); + addColumn(createdByCol); + + var referenceCountCol = new AliasedColumn( this, "ReferenceCount", rowIdCol); + referenceCountCol.setKeyField(false); + referenceCountCol.setDisplayColumnFactory(new ReferenceCountDisplayColumnFactory()); + addColumn(referenceCountCol); + } + + @NotNull + @Override + public SQLFragment getFromSQL() + { + return _query; + } + } + +} diff --git a/experiment/src/org/labkey/experiment/api/ExperimentServiceImpl.java b/experiment/src/org/labkey/experiment/api/ExperimentServiceImpl.java index f819696469c..d76b5099256 100644 --- a/experiment/src/org/labkey/experiment/api/ExperimentServiceImpl.java +++ b/experiment/src/org/labkey/experiment/api/ExperimentServiceImpl.java @@ -172,6 +172,7 @@ import org.labkey.api.exp.query.ExpRunTable; import org.labkey.api.exp.query.ExpSampleTypeTable; import org.labkey.api.exp.query.ExpSchema; +import org.labkey.api.exp.query.ExpUnreferencedSampleFilesTable; import org.labkey.api.exp.query.SampleStatusTable; import org.labkey.api.exp.query.SamplesSchema; import org.labkey.api.exp.xar.LSIDRelativizer; @@ -1608,6 +1609,12 @@ public SampleStatusTable createSampleStatusTable(ExpSchema expSchema, ContainerF return new SampleStatusTable(expSchema, containerFilter); } + @Override + public ExpUnreferencedSampleFilesTable createUnreferencedSampleFilesTable(ExpSchema expSchema, ContainerFilter cf) + { + return new ExpUnreferencedSampleFilesTableImpl(expSchema, cf); + } + @Override public FilteredTable createFieldsTable(ExpSchema expSchema, ContainerFilter cf) { @@ -5457,6 +5464,7 @@ public void deleteAllExpObjInContainer(Container c, User user) throws Experiment { if (null == c) return; + LOG.info("Beginning delete of expObj in container {}", c); String sql = "SELECT RowId FROM " + getTinfoExperimentRun() + " WHERE Container = ?"; int[] runIds = ArrayUtils.toPrimitive(new SqlSelector(getExpSchema(), sql, c).getArray(Integer.class)); @@ -5540,8 +5548,10 @@ public void deleteAllExpObjInContainer(Container c, User user) throws Experiment // same drill for data objects sql = "SELECT RowId FROM exp.Data WHERE Container = ?"; Collection dataIds = new SqlSelector(getExpSchema(), sql, c).getCollection(Long.class); + LOG.info("Deleting {} dataIds {} ", dataIds.size(), dataIds); deleteDataByRowIds(user, c, dataIds); + LOG.info("Deleting objects from container {}", c); OntologyManager.deleteAllObjects(c, user); transaction.commit(); diff --git a/experiment/src/org/labkey/experiment/api/ReferenceCountDisplayColumnFactory.java b/experiment/src/org/labkey/experiment/api/ReferenceCountDisplayColumnFactory.java new file mode 100644 index 00000000000..8a089234f32 --- /dev/null +++ b/experiment/src/org/labkey/experiment/api/ReferenceCountDisplayColumnFactory.java @@ -0,0 +1,53 @@ +package org.labkey.experiment.api; + +import org.apache.commons.lang3.StringUtils; +import org.labkey.api.data.ColumnInfo; +import org.labkey.api.data.CoreSchema; +import org.labkey.api.data.DataColumn; +import org.labkey.api.data.DisplayColumn; +import org.labkey.api.data.DisplayColumnFactory; +import org.labkey.api.data.SQLFragment; +import org.labkey.api.data.SqlSelector; +import org.labkey.api.exp.api.ExpData; +import org.labkey.api.writer.HtmlWriter; +import org.labkey.experiment.FileLinkFileListener; + +public class ReferenceCountDisplayColumnFactory implements DisplayColumnFactory +{ + @Override + public DisplayColumn createRenderer(ColumnInfo colInfo) + { + return new ExpDataFileColumn(colInfo) + { + private Long getCount(ExpData data) + { + + if (data == null || StringUtils.isEmpty(data.getDataFileUrl()) || data.getFile() == null) + return null; + else + { + FileLinkFileListener fileListener = new FileLinkFileListener(); + SQLFragment unionSql = fileListener.listFilesQuery(true, data.getFile().getAbsolutePath()); + + return new SqlSelector(CoreSchema.getInstance().getSchema(), unionSql).getRowCount(); + } + } + + @Override + protected void renderData(HtmlWriter out, ExpData data) + { + Long val = getCount(data); + if (val == null) + out.write(""); + else + out.write(val); + } + + @Override + protected Object getJsonValue(ExpData data) + { + return getCount(data); + } + }; + } +} diff --git a/experiment/src/org/labkey/experiment/api/SampleTypeServiceImpl.java b/experiment/src/org/labkey/experiment/api/SampleTypeServiceImpl.java index 013bf5d01ac..a4d9b68b1a9 100644 --- a/experiment/src/org/labkey/experiment/api/SampleTypeServiceImpl.java +++ b/experiment/src/org/labkey/experiment/api/SampleTypeServiceImpl.java @@ -2153,7 +2153,7 @@ private Map> updateSampleFilePaths(ExpSampleType throw new ExperimentException("Sample " + ref.sampleName + " cannot be moved since it references a shared file: " + sourceFile.getName()); // TODO, support batch fireFileMoveEvent to avoid excessive FileLinkFileListener.hardTableFileLinkColumns calls - fileService.fireFileMoveEvent(sourceFile, ref.targetFile, user, targetContainer); + fileService.fireFileMoveEvent(sourceFile.toPath(), ref.targetFile.toPath(), user, ref.sourceContainer, targetContainer); FileSystemAuditProvider.FileSystemAuditEvent event = new FileSystemAuditProvider.FileSystemAuditEvent(targetContainer, "File moved from " + ref.sourceContainer.getPath() + " to " + targetContainer.getPath() + "."); event.setProvidedFileName(sourceFile.getName()); event.setFile(ref.targetFile.getName()); diff --git a/experiment/src/org/labkey/experiment/controllers/exp/ExperimentController.java b/experiment/src/org/labkey/experiment/controllers/exp/ExperimentController.java index 26e4decea5e..087cd82d1d2 100644 --- a/experiment/src/org/labkey/experiment/controllers/exp/ExperimentController.java +++ b/experiment/src/org/labkey/experiment/controllers/exp/ExperimentController.java @@ -207,6 +207,7 @@ import org.labkey.api.study.StudyUrls; import org.labkey.api.study.publish.StudyPublishService; import org.labkey.api.usageMetrics.SimpleMetricsService; +import org.labkey.api.util.CsrfInput; import org.labkey.api.util.DOM; import org.labkey.api.util.DOM.LK; import org.labkey.api.util.ErrorRenderer; @@ -228,7 +229,6 @@ import org.labkey.api.util.StringUtilsLabKey; import org.labkey.api.util.URLHelper; import org.labkey.api.util.UniqueID; -import org.labkey.api.util.CsrfInput; import org.labkey.api.view.ActionURL; import org.labkey.api.view.BadRequestException; import org.labkey.api.view.DataView; @@ -329,7 +329,6 @@ import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; -import java.util.Iterator; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; @@ -7457,6 +7456,25 @@ public Object execute(Object o, BindException errors) throws Exception } } + @Marshal(Marshaller.Jackson) + @RequiresPermission(AdminPermission.class) + public static class RepairExpDataFilesAction extends MutatingApiAction + { + @Override + public Object execute(Object form, BindException errors) + { + FileContentService service = FileContentService.get(); + if (service == null) + { + errors.reject(ERROR_GENERIC, "No FileContentService found"); + return new SimpleResponse<>(false, "No FileContentService found"); + } + + int numDuplicates = service.fixContainerForExpDataFiles(getUser()); + return success(Map.of("hadRepairs", numDuplicates > 0)); + } + } + @RequiresPermission(UpdatePermission.class) public static class UpdateMaterialQueryRowAction extends UserSchemaAction { diff --git a/filecontent/src/org/labkey/filecontent/FileContentServiceImpl.java b/filecontent/src/org/labkey/filecontent/FileContentServiceImpl.java index f3ed5d2eaff..b2fe3f78c4b 100644 --- a/filecontent/src/org/labkey/filecontent/FileContentServiceImpl.java +++ b/filecontent/src/org/labkey/filecontent/FileContentServiceImpl.java @@ -40,6 +40,7 @@ import org.labkey.api.data.CoreSchema; import org.labkey.api.data.SQLFragment; import org.labkey.api.data.SimpleFilter; +import org.labkey.api.data.SqlExecutor; import org.labkey.api.data.TabContainerType; import org.labkey.api.data.Table; import org.labkey.api.data.TableInfo; @@ -51,6 +52,7 @@ import org.labkey.api.exp.api.ExpRun; import org.labkey.api.exp.api.ExperimentService; import org.labkey.api.exp.query.ExpDataTable; +import org.labkey.api.exp.query.ExpSchema; import org.labkey.api.files.DirectoryPattern; import org.labkey.api.files.FileContentService; import org.labkey.api.files.FileListener; @@ -136,6 +138,44 @@ public class FileContentServiceImpl implements FileContentService, WarningProvid private volatile boolean _fileRootSetViaStartupProperty = false; private String _problematicFileRootMessage; + @Override + public int fixContainerForExpDataFiles(User admin) + { + _log.info("Populating exp.data with rows for all the files in all containers"); + ensureFileDataInAllContainers(admin); + + // Delete entries with duplicate datafileurls + SQLFragment sql = new SQLFragment(""" + DELETE FROM exp.data WHERE RowId IN (SELECT RowId + FROM exp.data + WHERE datafileurl IN (SELECT DataFileURL + FROM (SELECT DataFileURL, COUNT(*) AS count + FROM exp.data + WHERE DataFileURL IS NOT NULL + GROUP BY DataFileUrl) c + WHERE c.count > 1))"""); + int count = new SqlExecutor(ExperimentService.get().getSchema().getScope()).execute(sql); + _log.info("Deleted {} duplicate entries from exp.data", count); + _log.info("Repopulating file data in exp.data."); + ensureFileDataInAllContainers(admin); + return count; + } + + private void ensureFileDataInAllContainers(User user) + { + ContainerManager.getAllChildren(ContainerManager.getRoot()).forEach(c -> + { + + ExpDataTable expDataTable = ExperimentService.get().createDataTable("data", new ExpSchema(user, c), null); + if (expDataTable != null) + { + _log.info("Ensuring file data in container {}", c.getPath()); + ensureFileDataUnsynchronized(expDataTable); + } + } + ); + } + enum FileAction { UPLOAD, @@ -1232,6 +1272,27 @@ public Map> listFiles(@NotNull Container container) return files; } + @Override + public SQLFragment listSampleFilesQuery(@NotNull User currentUser) + { + SQLFragment frag = new SQLFragment(); + String union = ""; + frag.append("("); + + for (FileListener fileListener : _fileListeners) + { + SQLFragment subselect = fileListener.listSampleFilesQuery(); + if (subselect != null && !subselect.isEmpty()) + { + frag.append(union); + frag.append(subselect); + union = "UNION\n"; + } + } + frag.append(")"); + return union.isEmpty() ? new SQLFragment() : frag; + } + @Override public SQLFragment listFilesQuery(@NotNull User currentUser) { @@ -1448,14 +1509,10 @@ public String getDataFileRelativeFileRootPath(@NotNull String dataFileUrl, Conta @Override public void ensureFileData(@NotNull ExpDataTable table) { + if (table.getUserSchema() == null) + throw new IllegalArgumentException("getUserSchema() returned null from " + table); + Container container = table.getUserSchema().getContainer(); - // The current user may not have insert permission, and they didn't necessarily upload the files anyway - User user = User.getAdminServiceUser(); - QueryUpdateService qus = table.getUpdateService(); - if (qus == null) - { - throw new IllegalArgumentException("getUpdateServer() returned null from " + table); - } synchronized (_fileDataUpToDateCache) { @@ -1465,6 +1522,23 @@ public void ensureFileData(@NotNull ExpDataTable table) _fileDataUpToDateCache.put(container, true); } + ensureFileDataUnsynchronized(table); + } + + // N.B. Use the synchronized method above. This is exposed only because of the need to use it in for data repair (e.g., in an upgrade script). + private void ensureFileDataUnsynchronized(@NotNull ExpDataTable table) + { + if (table.getUserSchema() == null) + throw new IllegalArgumentException("getUserSchema() returned null from " + table); + + Container container = table.getUserSchema().getContainer(); + // The current user may not have insert permission, and they didn't necessarily upload the files anyway + User user = User.getAdminServiceUser(); + QueryUpdateService qus = table.getUpdateService(); + if (qus == null) + { + throw new IllegalArgumentException("getUpdateServer() returned null from " + table); + } List existingDataFileUrls = getDataFileUrls(container); Collection filesets = getRegisteredDirectories(container); Set> children = getNodes(false, null, container); @@ -1527,13 +1601,13 @@ public void ensureFileData(@NotNull ExpDataTable table) try (Stream pathStream = Files.walk(rootPath, 100)) // prevent symlink loop { pathStream - .filter(path -> !Files.isSymbolicLink(path) && path.compareTo(rootPath) != 0) // exclude symlink & root - .forEach(path -> { - if (!containsUrlOrVariation(existingDataFileUrls, path)) - rows.add(new CaseInsensitiveHashMap<>(Collections.singletonMap("DataFileUrl", path.toUri().toString()))); - }); + .filter(path -> !Files.isSymbolicLink(path) && path.compareTo(rootPath) != 0) // exclude symlink & root + .forEach(path -> { + if (!containsUrlOrVariation(existingDataFileUrls, path)) + rows.add(new CaseInsensitiveHashMap<>(Collections.singletonMap("DataFileUrl", path.toUri().toString()))); + }); } - + _log.debug("Inserting " + rows.size() + " rows into " + table); qus.insertRows(user, container, rows, errors, null, null); } catch (Exception e) From 9028e69578c0ad177d2ab952557e6cef0c1a8c4b Mon Sep 17 00:00:00 2001 From: Nick Kerr Date: Thu, 22 Jan 2026 08:27:43 -0800 Subject: [PATCH 3/3] Exp.Materials.LastIndexed (#7345) --- .../labkey/api/exp/query/ExpMaterialTable.java | 1 + .../labkey/experiment/api/ExpDataTableImpl.java | 5 ++++- .../experiment/api/ExpMaterialTableImpl.java | 15 ++++++++++++++- 3 files changed, 19 insertions(+), 2 deletions(-) diff --git a/api/src/org/labkey/api/exp/query/ExpMaterialTable.java b/api/src/org/labkey/api/exp/query/ExpMaterialTable.java index 6c856381460..0d719a0466b 100644 --- a/api/src/org/labkey/api/exp/query/ExpMaterialTable.java +++ b/api/src/org/labkey/api/exp/query/ExpMaterialTable.java @@ -42,6 +42,7 @@ enum Column Inputs, IsAliquot, IsPlated, + LastIndexed, LSID, MaterialExpDate, MaterialSourceId, diff --git a/experiment/src/org/labkey/experiment/api/ExpDataTableImpl.java b/experiment/src/org/labkey/experiment/api/ExpDataTableImpl.java index 12b775493c9..6cee02b4c56 100644 --- a/experiment/src/org/labkey/experiment/api/ExpDataTableImpl.java +++ b/experiment/src/org/labkey/experiment/api/ExpDataTableImpl.java @@ -314,7 +314,10 @@ public MutableColumnInfo createColumn(String alias, Column column) append(ExperimentServiceImpl.get().getTinfoDataIndexed(), "di"). append(" WHERE di.DataId = "). append(ExprColumn.STR_TABLE_ALIAS).append(".RowId)"); - var lastIndexed = new ExprColumn(this, "LastIndexed", lastIndexedSql, JdbcType.TIMESTAMP); + var lastIndexed = new ExprColumn(this, Column.LastIndexed.name(), lastIndexedSql, JdbcType.TIMESTAMP); + lastIndexed.setDescription("Date when the data was last full-text search indexed in the system"); + lastIndexed.setHidden(true); + lastIndexed.setReadOnly(true); lastIndexed.setUserEditable(false); return lastIndexed; case DataClass: diff --git a/experiment/src/org/labkey/experiment/api/ExpMaterialTableImpl.java b/experiment/src/org/labkey/experiment/api/ExpMaterialTableImpl.java index 80393dc07d5..7251c8e7b6c 100644 --- a/experiment/src/org/labkey/experiment/api/ExpMaterialTableImpl.java +++ b/experiment/src/org/labkey/experiment/api/ExpMaterialTableImpl.java @@ -145,6 +145,7 @@ import static org.labkey.api.exp.query.ExpMaterialTable.Column.AliquotVolume; import static org.labkey.api.exp.query.ExpMaterialTable.Column.AvailableAliquotCount; import static org.labkey.api.exp.query.ExpMaterialTable.Column.AvailableAliquotVolume; +import static org.labkey.api.exp.query.ExpMaterialTable.Column.LastIndexed; import static org.labkey.api.exp.query.ExpMaterialTable.Column.StoredAmount; import static org.labkey.api.exp.query.ExpMaterialTable.Column.Units; import static org.labkey.api.util.StringExpressionFactory.AbstractStringExpression.NullValueBehavior.NullResult; @@ -577,6 +578,18 @@ public StringExpression getURL(ColumnInfo parent) ret.setShownInUpdateView(true); return ret; } + case LastIndexed -> + { + var sql = new SQLFragment("(SELECT LastIndexed FROM ") + .append(ExperimentServiceImpl.get().getTinfoMaterialIndexed(), "mi") + .append(" WHERE mi.MaterialId = ").append(ExprColumn.STR_TABLE_ALIAS).append(".RowId)"); + var ret = new ExprColumn(this, LastIndexed.name(), sql, JdbcType.TIMESTAMP); + ret.setDescription("Date when the material was last full-text search indexed in the system"); + ret.setHidden(true); + ret.setReadOnly(true); + ret.setUserEditable(false); + return ret; + } default -> throw new IllegalArgumentException("Unknown column " + column); } } @@ -903,7 +916,7 @@ public void addQueryFieldKeys(Set keys) addColumn(col); addVocabularyDomains(); - + addColumn(LastIndexed); addColumn(Column.Properties); var colInputs = addColumn(Column.Inputs);